move disabled to a global hook

This commit is contained in:
Nate Kelley 2025-04-19 22:48:41 -06:00
parent def7d2a341
commit 2bdccb6a7c
No known key found for this signature in database
GPG Key ID: FD90372AB8D98B4F
12 changed files with 141 additions and 64 deletions

View File

@ -20,14 +20,13 @@ import { useSignOut } from '@/components/features/auth/SignOutHandler';
export const SidebarUserFooter: React.FC<{}> = () => { export const SidebarUserFooter: React.FC<{}> = () => {
const user = useUserConfigContextSelector((x) => x.user); const user = useUserConfigContextSelector((x) => x.user);
const handleSignOut = useSignOut();
if (!user) return null; if (!user) return null;
const { name, email } = user; const { name, email } = user;
if (!name || !email) return null; if (!name || !email) return null;
const handleSignOut = useSignOut();
return ( return (
<SidebarUserDropdown signOut={handleSignOut}> <SidebarUserDropdown signOut={handleSignOut}>
<div className="flex w-full"> <div className="flex w-full">

View File

@ -8,7 +8,7 @@ import { compareObjectsByKeys } from '@/lib/objects';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { create } from 'mutative'; import { create } from 'mutative';
export const useIsDashboardChanged = ({ dashboardId }: { dashboardId: string }) => { export const useIsDashboardChanged = ({ dashboardId }: { dashboardId: string | undefined }) => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const originalDashboard = useOriginalDashboardStore((x) => x.getOriginalDashboard(dashboardId)); const originalDashboard = useOriginalDashboardStore((x) => x.getOriginalDashboard(dashboardId));
@ -26,7 +26,7 @@ export const useIsDashboardChanged = ({ dashboardId }: { dashboardId: string })
const onResetDashboardToOriginal = useMemoizedFn(() => { const onResetDashboardToOriginal = useMemoizedFn(() => {
const options = dashboardQueryKeys.dashboardGetDashboard( const options = dashboardQueryKeys.dashboardGetDashboard(
dashboardId, dashboardId || '',
originalDashboard?.version_number || null originalDashboard?.version_number || null
); );
const currentDashboard = queryClient.getQueryData<BusterDashboardResponse>(options.queryKey); const currentDashboard = queryClient.getQueryData<BusterDashboardResponse>(options.queryKey);

View File

@ -8,7 +8,7 @@ type OriginalDashboardStore = {
originalDashboards: Record<string, BusterDashboard>; originalDashboards: Record<string, BusterDashboard>;
bulkAddOriginalDashboards: (dashboards: Record<string, BusterDashboard>) => void; bulkAddOriginalDashboards: (dashboards: Record<string, BusterDashboard>) => void;
setOriginalDashboard: (dashboard: BusterDashboard) => void; setOriginalDashboard: (dashboard: BusterDashboard) => void;
getOriginalDashboard: (dashboardId: string) => BusterDashboard | undefined; getOriginalDashboard: (dashboardId: string | undefined) => BusterDashboard | undefined;
removeOriginalDashboard: (dashboardId: string) => void; removeOriginalDashboard: (dashboardId: string) => void;
}; };
@ -28,7 +28,8 @@ export const useOriginalDashboardStore = create<OriginalDashboardStore>((set, ge
[dashboard.id]: dashboard [dashboard.id]: dashboard
} }
})), })),
getOriginalDashboard: (dashboardId: string) => get().originalDashboards[dashboardId], getOriginalDashboard: (dashboardId: string | undefined) =>
dashboardId ? get().originalDashboards[dashboardId] : undefined,
removeOriginalDashboard: (dashboardId: string) => removeOriginalDashboard: (dashboardId: string) =>
set((state) => { set((state) => {
const { [dashboardId]: removed, ...rest } = state.originalDashboards; const { [dashboardId]: removed, ...rest } = state.originalDashboards;

View File

@ -1,13 +1,12 @@
import { useQuery, useQueryClient } from '@tanstack/react-query'; import { useQueryClient } from '@tanstack/react-query';
import { useOriginalMetricStore } from './useOriginalMetricStore'; import { useOriginalMetricStore } from './useOriginalMetricStore';
import { useMemoizedFn } from '@/hooks'; import { useMemoizedFn } from '@/hooks';
import { metricsQueryKeys } from '@/api/query_keys/metric'; import { metricsQueryKeys } from '@/api/query_keys/metric';
import { useGetMetric } from '@/api/buster_rest/metrics'; import { useGetMetric } from '@/api/buster_rest/metrics';
import { compareObjectsByKeys } from '@/lib/objects'; import { compareObjectsByKeys } from '@/lib/objects';
import { useMemo } from 'react'; import { useMemo } from 'react';
import last from 'lodash/last';
export const useIsMetricChanged = ({ metricId }: { metricId: string }) => { export const useIsMetricChanged = ({ metricId }: { metricId: string | undefined }) => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const originalMetric = useOriginalMetricStore((x) => x.getOriginalMetric(metricId)); const originalMetric = useOriginalMetricStore((x) => x.getOriginalMetric(metricId));
@ -31,7 +30,7 @@ export const useIsMetricChanged = ({ metricId }: { metricId: string }) => {
const onResetMetricToOriginal = useMemoizedFn(() => { const onResetMetricToOriginal = useMemoizedFn(() => {
const options = metricsQueryKeys.metricsGetMetric( const options = metricsQueryKeys.metricsGetMetric(
metricId, metricId || '',
originalMetric?.version_number || null originalMetric?.version_number || null
); );
if (originalMetric) { if (originalMetric) {

View File

@ -8,7 +8,7 @@ type OriginalMetricStore = {
originalMetrics: Record<string, IBusterMetric>; originalMetrics: Record<string, IBusterMetric>;
bulkAddOriginalMetrics: (metrics: Record<string, IBusterMetric>) => void; bulkAddOriginalMetrics: (metrics: Record<string, IBusterMetric>) => void;
setOriginalMetric: (metric: IBusterMetric) => void; setOriginalMetric: (metric: IBusterMetric) => void;
getOriginalMetric: (metricId: string) => IBusterMetric | undefined; getOriginalMetric: (metricId: string | undefined) => IBusterMetric | undefined;
removeOriginalMetric: (metricId: string) => void; removeOriginalMetric: (metricId: string) => void;
}; };
@ -28,7 +28,8 @@ export const useOriginalMetricStore = create<OriginalMetricStore>((set, get) =>
[metric.id]: metric [metric.id]: metric
} }
})), })),
getOriginalMetric: (metricId: string) => get().originalMetrics[metricId], getOriginalMetric: (metricId: string | undefined) =>
metricId ? get().originalMetrics[metricId] : undefined,
removeOriginalMetric: (metricId: string) => removeOriginalMetric: (metricId: string) =>
set((state) => { set((state) => {
const { [metricId]: removed, ...rest } = state.originalMetrics; const { [metricId]: removed, ...rest } = state.originalMetrics;

View File

@ -1,17 +1,17 @@
import { useGetDashboard, useUpdateDashboard } from '@/api/buster_rest/dashboards'; import { useGetDashboard, useUpdateDashboard } from '@/api/buster_rest/dashboards';
import { SaveResetFilePopup } from '@/components/features/popups/SaveResetFilePopup'; import { SaveResetFilePopup } from '@/components/features/popups/SaveResetFilePopup';
import { useIsDashboardChanged } from '@/context/Dashboards';
import { useMemoizedFn } from '@/hooks'; import { useMemoizedFn } from '@/hooks';
import { useChatLayoutContextSelector } from '@/layouts/ChatLayout'; import { useChatLayoutContextSelector } from '@/layouts/ChatLayout';
import { useChatIndividualContextSelector } from '@/layouts/ChatLayout/ChatContext';
import React from 'react'; import React from 'react';
export const DashboardSavePopup: React.FC<{ dashboardId: string }> = React.memo( export const DashboardSaveFilePopup: React.FC<{ dashboardId: string }> = React.memo(
({ dashboardId }) => { ({ dashboardId }) => {
const onResetToOriginal = useChatIndividualContextSelector((x) => x.onResetToOriginal);
const isFileChanged = useChatIndividualContextSelector((x) => x.isFileChanged);
const chatId = useChatLayoutContextSelector((x) => x.chatId); const chatId = useChatLayoutContextSelector((x) => x.chatId);
const { data: dashboardResponse } = useGetDashboard({ id: dashboardId }); const { data: dashboardResponse } = useGetDashboard({ id: dashboardId });
const { isDashboardChanged, onResetDashboardToOriginal } = useIsDashboardChanged({
dashboardId
});
const { mutateAsync: onSaveDashboard, isPending: isSaving } = useUpdateDashboard({ const { mutateAsync: onSaveDashboard, isPending: isSaving } = useUpdateDashboard({
saveToServer: true, saveToServer: true,
updateOnSave: true, updateOnSave: true,
@ -30,8 +30,8 @@ export const DashboardSavePopup: React.FC<{ dashboardId: string }> = React.memo(
return ( return (
<SaveResetFilePopup <SaveResetFilePopup
open={isDashboardChanged} open={isFileChanged}
onReset={onResetDashboardToOriginal} onReset={onResetToOriginal}
onSave={onSaveDashboardFileToServer} onSave={onSaveDashboardFileToServer}
isSaving={isSaving} isSaving={isSaving}
showHotsKeys={false} showHotsKeys={false}

View File

@ -8,7 +8,7 @@ import { useDashboardContentStore } from '@/context/Dashboards';
import { ScrollArea } from '@/components/ui/scroll-area'; import { ScrollArea } from '@/components/ui/scroll-area';
import { StatusCard } from '@/components/ui/card/StatusCard'; import { StatusCard } from '@/components/ui/card/StatusCard';
import { useIsDashboardReadOnly } from '@/context/Dashboards/useIsDashboardReadOnly'; import { useIsDashboardReadOnly } from '@/context/Dashboards/useIsDashboardReadOnly';
import { DashboardSavePopup } from './DashboardSavePopup'; import { DashboardSaveFilePopup } from './DashboardSaveFilePopup';
export const DashboardViewDashboardController: React.FC<{ export const DashboardViewDashboardController: React.FC<{
dashboardId: string; dashboardId: string;
@ -64,7 +64,7 @@ export const DashboardViewDashboardController: React.FC<{
/> />
{!isVersionHistoryMode && !isViewingOldVersion && ( {!isVersionHistoryMode && !isViewingOldVersion && (
<DashboardSavePopup dashboardId={dashboardId} /> <DashboardSaveFilePopup dashboardId={dashboardId} />
)} )}
</div> </div>
</ScrollArea> </ScrollArea>

View File

@ -1,16 +1,17 @@
import { SaveResetFilePopup } from '@/components/features/popups/SaveResetFilePopup'; import { SaveResetFilePopup } from '@/components/features/popups/SaveResetFilePopup';
import { useIsMetricChanged } from '@/context/Metrics/useIsMetricChanged';
import { useUpdateMetricChart } from '@/context/Metrics/useUpdateMetricChart'; import { useUpdateMetricChart } from '@/context/Metrics/useUpdateMetricChart';
import { useChatIndividualContextSelector } from '@/layouts/ChatLayout/ChatContext';
import React from 'react'; import React from 'react';
export const MetricSaveFilePopup: React.FC<{ metricId: string }> = React.memo(({ metricId }) => { export const MetricSaveFilePopup: React.FC<{ metricId: string }> = React.memo(({ metricId }) => {
const { isMetricChanged, onResetMetricToOriginal } = useIsMetricChanged({ metricId }); const onResetToOriginal = useChatIndividualContextSelector((x) => x.onResetToOriginal);
const isFileChanged = useChatIndividualContextSelector((x) => x.isFileChanged);
const { onSaveMetricToServer, isSaving } = useUpdateMetricChart({ metricId }); const { onSaveMetricToServer, isSaving } = useUpdateMetricChart({ metricId });
return ( return (
<SaveResetFilePopup <SaveResetFilePopup
open={isMetricChanged} open={isFileChanged}
onReset={onResetMetricToOriginal} onReset={onResetToOriginal}
onSave={onSaveMetricToServer} onSave={onSaveMetricToServer}
isSaving={isSaving} isSaving={isSaving}
showHotsKeys={false} showHotsKeys={false}

View File

@ -3,8 +3,8 @@
import React, { useMemo } from 'react'; import React, { useMemo } from 'react';
import { MetricViewChartContent } from './MetricViewChartContent'; import { MetricViewChartContent } from './MetricViewChartContent';
import { MetricViewChartHeader } from './MetricViewChartHeader'; import { MetricViewChartHeader } from './MetricViewChartHeader';
import { useGetMetric, useGetMetricData, useUpdateMetric } from '@/api/buster_rest/metrics'; import { useGetMetric, useGetMetricData } from '@/api/buster_rest/metrics';
import { useMemoizedFn, useMount } from '@/hooks'; import { useMemoizedFn } from '@/hooks';
import { inputHasText } from '@/lib/text'; import { inputHasText } from '@/lib/text';
import { MetricChartEvaluation } from './MetricChartEvaluation'; import { MetricChartEvaluation } from './MetricChartEvaluation';
import { ChartType } from '@/api/asset_interfaces/metric/charts/enum'; import { ChartType } from '@/api/asset_interfaces/metric/charts/enum';
@ -12,7 +12,6 @@ import { AnimatePresence, motion } from 'framer-motion';
import { cn } from '@/lib/classMerge'; import { cn } from '@/lib/classMerge';
import { useIsMetricReadOnly } from '@/context/Metrics/useIsMetricReadOnly'; import { useIsMetricReadOnly } from '@/context/Metrics/useIsMetricReadOnly';
import { MetricSaveFilePopup } from './MetricSaveFilePopup'; import { MetricSaveFilePopup } from './MetricSaveFilePopup';
import { useChatLayoutContextSelector } from '@/layouts/ChatLayout';
import { useUpdateMetricChart } from '@/context/Metrics'; import { useUpdateMetricChart } from '@/context/Metrics';
export const MetricViewChart: React.FC<{ export const MetricViewChart: React.FC<{

View File

@ -2,6 +2,8 @@ import React, { useMemo } from 'react';
import { useChatIndividualContextSelector } from '@/layouts/ChatLayout/ChatContext'; import { useChatIndividualContextSelector } from '@/layouts/ChatLayout/ChatContext';
import { useMemoizedFn } from '@/hooks'; import { useMemoizedFn } from '@/hooks';
import { useBusterNewChatContextSelector } from '@/context/Chats'; import { useBusterNewChatContextSelector } from '@/context/Chats';
import { useBusterNotifications } from '@/context/BusterNotifications';
import { timeout } from '@/lib';
type FlowType = 'followup-chat' | 'followup-metric' | 'followup-dashboard' | 'new'; type FlowType = 'followup-chat' | 'followup-metric' | 'followup-dashboard' | 'new';
@ -27,6 +29,9 @@ export const useChatInputFlow = ({
const onStartChatFromFile = useBusterNewChatContextSelector((state) => state.onStartChatFromFile); const onStartChatFromFile = useBusterNewChatContextSelector((state) => state.onStartChatFromFile);
const onStopChatContext = useBusterNewChatContextSelector((state) => state.onStopChat); const onStopChatContext = useBusterNewChatContextSelector((state) => state.onStopChat);
const currentMessageId = useChatIndividualContextSelector((x) => x.currentMessageId); const currentMessageId = useChatIndividualContextSelector((x) => x.currentMessageId);
const isFileChanged = useChatIndividualContextSelector((x) => x.isFileChanged);
const onResetToOriginal = useChatIndividualContextSelector((x) => x.onResetToOriginal);
const { openConfirmModal } = useBusterNotifications();
const flow: FlowType = useMemo(() => { const flow: FlowType = useMemo(() => {
if (hasChat) return 'followup-chat'; if (hasChat) return 'followup-chat';
@ -43,40 +48,65 @@ export const useChatInputFlow = ({
return; return;
} }
switch (flow) { const method = async () => {
case 'followup-chat': switch (flow) {
await onFollowUpChat({ prompt: inputValue, chatId }); case 'followup-chat':
break; await onFollowUpChat({ prompt: inputValue, chatId });
break;
case 'followup-metric': case 'followup-metric':
await onStartChatFromFile({ await onStartChatFromFile({
prompt: inputValue, prompt: inputValue,
fileId: selectedFileId!, fileId: selectedFileId!,
fileType: 'metric' fileType: 'metric'
}); });
break; break;
case 'followup-dashboard': case 'followup-dashboard':
await onStartChatFromFile({ await onStartChatFromFile({
prompt: inputValue, prompt: inputValue,
fileId: selectedFileId!, fileId: selectedFileId!,
fileType: 'dashboard' fileType: 'dashboard'
}); });
break; break;
case 'new': case 'new':
await onStartNewChat({ prompt: inputValue }); await onStartNewChat({ prompt: inputValue });
break; break;
default: default:
const _exhaustiveCheck: never = flow; const _exhaustiveCheck: never = flow;
return _exhaustiveCheck; return _exhaustiveCheck;
}
setInputValue('');
setTimeout(() => {
textAreaRef.current?.focus();
}, 50);
};
if (!isFileChanged) {
return method();
} }
setInputValue(''); await openConfirmModal({
title: 'Unsaved changes',
content: 'Looks like you have unsaved changes. Do you want to save them before continuing?',
primaryButtonProps: {
text: 'Reset to original'
},
cancelButtonProps: {
text: 'Continue'
},
onOk: async () => {
onResetToOriginal();
setTimeout(() => { return await method();
textAreaRef.current?.focus(); },
}, 50); onCancel: async () => {
return await method();
}
});
}); });
const onStopChat = useMemoizedFn(() => { const onStopChat = useMemoizedFn(() => {

View File

@ -7,15 +7,14 @@ import { useQueries } from '@tanstack/react-query';
import { queryKeys } from '@/api/query_keys'; import { queryKeys } from '@/api/query_keys';
import { IBusterChatMessage } from '@/api/asset_interfaces/chat'; import { IBusterChatMessage } from '@/api/asset_interfaces/chat';
import { useChatLayoutContextSelector } from '..'; import { useChatLayoutContextSelector } from '..';
import { useIsFileChanged } from './useIsFileChanged';
const useChatIndividualContext = ({ const useChatIndividualContext = ({
chatId, chatId,
selectedFile, selectedFile
onSetSelectedFile
}: { }: {
chatId?: string; chatId?: string;
selectedFile: SelectedFile | null; selectedFile: SelectedFile | null;
onSetSelectedFile: (file: SelectedFile) => void;
}) => { }) => {
const selectedFileId = selectedFile?.id; const selectedFileId = selectedFile?.id;
const selectedFileType = selectedFile?.type; const selectedFileType = selectedFile?.type;
@ -53,6 +52,11 @@ const useChatIndividualContext = ({
chatId chatId
}); });
const { isFileChanged, onResetToOriginal } = useIsFileChanged({
selectedFileId,
selectedFileType
});
return React.useMemo( return React.useMemo(
() => ({ () => ({
hasChat, hasChat,
@ -63,7 +67,9 @@ const useChatIndividualContext = ({
selectedFileType, selectedFileType,
chatMessageIds, chatMessageIds,
chatId, chatId,
isStreamingMessage isStreamingMessage,
isFileChanged,
onResetToOriginal
}), }),
[ [
hasChat, hasChat,
@ -74,7 +80,9 @@ const useChatIndividualContext = ({
chatTitle, chatTitle,
selectedFileType, selectedFileType,
chatMessageIds, chatMessageIds,
chatId chatId,
isFileChanged,
onResetToOriginal
] ]
); );
}; };
@ -86,11 +94,9 @@ const IndividualChatContext = createContext<ReturnType<typeof useChatIndividualC
export const ChatContextProvider = React.memo(({ children }: PropsWithChildren<{}>) => { export const ChatContextProvider = React.memo(({ children }: PropsWithChildren<{}>) => {
const chatId = useChatLayoutContextSelector((x) => x.chatId); const chatId = useChatLayoutContextSelector((x) => x.chatId);
const selectedFile = useChatLayoutContextSelector((x) => x.selectedFile); const selectedFile = useChatLayoutContextSelector((x) => x.selectedFile);
const onSetSelectedFile = useChatLayoutContextSelector((x) => x.onSetSelectedFile);
const useChatContextValue = useChatIndividualContext({ const useChatContextValue = useChatIndividualContext({
chatId, chatId,
selectedFile, selectedFile
onSetSelectedFile
}); });
return ( return (

View File

@ -0,0 +1,41 @@
import { useIsMetricChanged } from '@/context/Metrics';
import { SelectedFile } from '../interfaces';
import { useIsDashboardChanged } from '@/context/Dashboards';
import { useMemo } from 'react';
type UseIsFileChangeReturn = {
isFileChanged: boolean;
onResetToOriginal: () => void;
};
type UseIsFileChangeParams = {
selectedFileId: SelectedFile['id'] | undefined;
selectedFileType: SelectedFile['type'] | undefined;
};
export const useIsFileChanged = ({
selectedFileId,
selectedFileType
}: UseIsFileChangeParams): UseIsFileChangeReturn => {
const { isMetricChanged, onResetMetricToOriginal } = useIsMetricChanged({
metricId: selectedFileType === 'metric' ? selectedFileId : undefined
});
const { isDashboardChanged, onResetDashboardToOriginal } = useIsDashboardChanged({
dashboardId: selectedFileType === 'dashboard' ? selectedFileId : undefined
});
return useMemo(() => {
if (selectedFileType === 'metric')
return {
isFileChanged: isMetricChanged,
onResetToOriginal: onResetMetricToOriginal
};
if (selectedFileType === 'dashboard')
return {
isFileChanged: isDashboardChanged,
onResetToOriginal: onResetDashboardToOriginal
};
return { isFileChanged: false, onResetToOriginal: () => {} };
}, [isMetricChanged, isDashboardChanged, onResetMetricToOriginal, onResetDashboardToOriginal]);
};