restore version updates

This commit is contained in:
Nate Kelley 2025-03-28 15:15:49 -06:00
parent fbc5af0f2c
commit 26ec6c4589
No known key found for this signature in database
GPG Key ID: FD90372AB8D98B4F
3 changed files with 204 additions and 98 deletions

View File

@ -1,22 +1,26 @@
import { useGetDashboard } from '@/api/buster_rest/dashboards';
import { useGetMetric } from '@/api/buster_rest/metrics';
import React, { useMemo, useRef } from 'react'; import React, { useMemo, useRef } from 'react';
import { Button } from '@/components/ui/buttons'; import { Button } from '@/components/ui/buttons';
import { Xmark } from '@/components/ui/icons'; import { Xmark, History } from '@/components/ui/icons';
import { Check3 } from '@/components/ui/icons/NucleoIconFilled'; import { Check3 } from '@/components/ui/icons/NucleoIconFilled';
import { Text } from '@/components/ui/typography'; import { Text } from '@/components/ui/typography';
import { useCloseVersionHistory } from '@/layouts/ChatLayout/FileContainer/FileContainerHeader/FileContainerHeaderVersionHistory'; import { useCloseVersionHistory } from '@/layouts/ChatLayout/FileContainer/FileContainerHeader/FileContainerHeaderVersionHistory';
import { cn } from '@/lib/classMerge'; import { cn } from '@/lib/classMerge';
import { timeFromNow, timeout } from '@/lib'; import { timeFromNow, timeout } from '@/lib';
import { AppPageLayout } from '@/components/ui/layouts'; import { AppPageLayout } from '@/components/ui/layouts';
import { useSearchParams } from 'next/navigation';
import last from 'lodash/last';
import { useListVersionHistories } from './useListVersionHistories'; import { useListVersionHistories } from './useListVersionHistories';
import { useMount } from '@/hooks'; import { useMount } from '@/hooks';
import { AppTooltip } from '@/components/ui/tooltip';
export const VersionHistoryPanel = React.memo( export const VersionHistoryPanel = React.memo(
({ assetId, type }: { assetId: string; type: 'metric' | 'dashboard' }) => { ({ assetId, type }: { assetId: string; type: 'metric' | 'dashboard' }) => {
const { listItems, selectedVersion, onClickVersionHistory } = useListVersionHistories({ const removeVersionHistoryQueryParams = useCloseVersionHistory();
const {
listItems,
selectedVersion,
selectedQueryVersion,
onClickRestoreVersion,
onClickVersionHistory
} = useListVersionHistories({
assetId, assetId,
type type
}); });
@ -24,7 +28,7 @@ export const VersionHistoryPanel = React.memo(
useMount(async () => { useMount(async () => {
if (bodyRef.current) { if (bodyRef.current) {
await timeout(200); await timeout(250);
const selectedNode = bodyRef.current.querySelector('.selected-version'); const selectedNode = bodyRef.current.querySelector('.selected-version');
if (selectedNode) { if (selectedNode) {
selectedNode.scrollIntoView({ behavior: 'smooth', block: 'start' }); selectedNode.scrollIntoView({ behavior: 'smooth', block: 'start' });
@ -34,22 +38,24 @@ export const VersionHistoryPanel = React.memo(
return ( return (
<AppPageLayout <AppPageLayout
headerClassName="border-l"
header={useMemo( header={useMemo(
() => ( () => (
<PanelHeader /> <PanelHeader removeVersionHistoryQueryParams={removeVersionHistoryQueryParams} />
), ),
[] []
)} )}
scrollable scrollable
headerBorderVariant="ghost" headerBorderVariant="ghost">
headerClassName="border-l">
<div ref={bodyRef} className="mx-1.5 mb-1.5 flex flex-col"> <div ref={bodyRef} className="mx-1.5 mb-1.5 flex flex-col">
{listItems?.map((item) => ( {listItems?.map((item) => (
<ListItem <ListItem
key={item.version_number} key={item.version_number}
{...item} {...item}
selected={item.version_number === selectedVersion} selected={item.version_number === selectedQueryVersion}
showRestoreButton={item.version_number !== selectedVersion}
onClickVersionHistory={onClickVersionHistory} onClickVersionHistory={onClickVersionHistory}
onClickRestoreVersion={onClickRestoreVersion}
/> />
))} ))}
</div> </div>
@ -63,18 +69,22 @@ const ListItem = React.memo(
version_number, version_number,
updated_at, updated_at,
selected, selected,
onClickVersionHistory showRestoreButton,
onClickVersionHistory,
onClickRestoreVersion
}: { }: {
version_number: number; version_number: number;
updated_at: string; updated_at: string;
selected: boolean; selected: boolean;
showRestoreButton: boolean;
onClickVersionHistory: (versionNumber: number) => void; onClickVersionHistory: (versionNumber: number) => void;
onClickRestoreVersion: (versionNumber: number) => void;
}) => { }) => {
return ( return (
<div <div
onClick={() => onClickVersionHistory(version_number)} onClick={() => onClickVersionHistory(version_number)}
className={cn( className={cn(
'hover:bg-item-hover flex cursor-pointer items-center justify-between space-x-2 rounded px-2.5 py-1.5', 'group hover:bg-item-hover flex cursor-pointer items-center justify-between space-x-2 rounded px-2.5 py-1.5',
selected && 'bg-item-select hover:bg-item-select selected-version' selected && 'bg-item-select hover:bg-item-select selected-version'
)}> )}>
<div className="flex flex-col justify-center space-y-0.5"> <div className="flex flex-col justify-center space-y-0.5">
@ -83,27 +93,44 @@ const ListItem = React.memo(
{timeFromNow(updated_at, false)} {timeFromNow(updated_at, false)}
</Text> </Text>
</div> </div>
<div className="text-icon-color animate-in fade-in-0 flex items-center space-x-2 duration-200">
{showRestoreButton && (
<AppTooltip title="Restore version">
<div
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
onClickRestoreVersion(version_number);
}}
className="hover:bg-item-select -mr-1 rounded p-1 opacity-0 group-hover:block group-hover:opacity-100">
<History />
</div>
</AppTooltip>
)}
{selected && ( {selected && (
<div className="text-icon-color animate-in fade-in-0 flex items-center duration-200"> <div className="group-hover:opacity-100">
<Check3 /> <Check3 />
</div> </div>
)} )}
</div> </div>
</div>
); );
} }
); );
ListItem.displayName = 'ListItem'; ListItem.displayName = 'ListItem';
const PanelHeader = React.memo(() => { const PanelHeader = React.memo(
const removeVersionHistoryQueryParams = useCloseVersionHistory(); ({ removeVersionHistoryQueryParams }: { removeVersionHistoryQueryParams: () => void }) => {
return ( return (
<div className="flex w-full items-center justify-between"> <div className="flex w-full items-center justify-between">
<Text>Version History</Text> <Text>Version History</Text>
<Button variant="ghost" prefix={<Xmark />} onClick={removeVersionHistoryQueryParams} /> <Button variant="ghost" prefix={<Xmark />} onClick={removeVersionHistoryQueryParams} />
</div> </div>
); );
}); }
);
PanelHeader.displayName = 'PanelHeader'; PanelHeader.displayName = 'PanelHeader';
VersionHistoryPanel.displayName = 'VersionHistoryPanel'; VersionHistoryPanel.displayName = 'VersionHistoryPanel';

View File

@ -1,7 +1,11 @@
import { useGetDashboard } from '@/api/buster_rest/dashboards'; 'use client';
import { useGetMetric } from '@/api/buster_rest/metrics';
import { useGetDashboard, useUpdateDashboard } from '@/api/buster_rest/dashboards';
import { useGetMetric, useSaveMetric } from '@/api/buster_rest/metrics';
import { useAppLayoutContextSelector } from '@/context/BusterAppLayout'; import { useAppLayoutContextSelector } from '@/context/BusterAppLayout';
import { useMemoizedFn } from '@/hooks'; import { useMemoizedFn } from '@/hooks';
import { useCloseVersionHistory } from '@/layouts/ChatLayout/FileContainer/FileContainerHeader/FileContainerHeaderVersionHistory';
import { BusterRoutes, createBusterRoute } from '@/routes';
import last from 'lodash/last'; import last from 'lodash/last';
import { useSearchParams } from 'next/navigation'; import { useSearchParams } from 'next/navigation';
import { useMemo } from 'react'; import { useMemo } from 'react';
@ -13,14 +17,26 @@ export const useListVersionHistories = ({
assetId: string; assetId: string;
type: 'metric' | 'dashboard'; type: 'metric' | 'dashboard';
}) => { }) => {
const removeVersionHistoryQueryParams = useCloseVersionHistory();
const onChangeQueryParams = useAppLayoutContextSelector((x) => x.onChangeQueryParams); const onChangeQueryParams = useAppLayoutContextSelector((x) => x.onChangeQueryParams);
const { dashboardVersions, selectedVersion: dashboardSelectedVersion } = useListDashboardVersions( const onChangePage = useAppLayoutContextSelector((x) => x.onChangePage);
{ const {
dashboardVersions,
selectedQueryVersion: dashboardSelectedQueryVersion,
selectedVersion: dashboardSelectedVersion,
onRestoreVersion: onRestoreDashboardVersion,
isSavingDashboard
} = useListDashboardVersions({
assetId, assetId,
type type
} });
); const {
const { metricVersions, selectedVersion: metricSelectedVersion } = useListMetricVersions({ metricVersions,
selectedQueryVersion: metricSelectedQueryVersion,
selectedVersion: metricSelectedVersion,
onRestoreVersion: onRestoreMetricVersion,
isSavingMetric
} = useListMetricVersions({
assetId, assetId,
type type
}); });
@ -34,19 +50,65 @@ export const useListVersionHistories = ({
return type === 'metric' ? metricSelectedVersion : dashboardSelectedVersion; return type === 'metric' ? metricSelectedVersion : dashboardSelectedVersion;
}, [type, dashboardSelectedVersion, metricSelectedVersion]); }, [type, dashboardSelectedVersion, metricSelectedVersion]);
const selectedQueryVersion = useMemo(() => {
return type === 'metric' ? metricSelectedQueryVersion : dashboardSelectedQueryVersion;
}, [type, dashboardSelectedQueryVersion, metricSelectedQueryVersion]);
const onClickVersionHistory = useMemoizedFn((versionNumber: number) => { const onClickVersionHistory = useMemoizedFn((versionNumber: number) => {
if (type === 'metric') onChangeQueryParams({ metric_version_number: versionNumber.toString() }); if (type === 'metric') onChangeQueryParams({ metric_version_number: versionNumber.toString() });
if (type === 'dashboard') if (type === 'dashboard')
onChangeQueryParams({ dashboard_version_number: versionNumber.toString() }); onChangeQueryParams({ dashboard_version_number: versionNumber.toString() });
}); });
const onClickRestoreVersion = useMemoizedFn(
async (versionNumber: number, rereouteToAsset: boolean = true) => {
if (type === 'metric') {
await onRestoreMetricVersion(versionNumber);
if (rereouteToAsset) {
await onChangePage(
createBusterRoute({
route: BusterRoutes.APP_METRIC_ID,
metricId: assetId
})
);
}
}
if (type === 'dashboard') {
await onRestoreDashboardVersion(versionNumber);
if (rereouteToAsset) {
await onChangePage(
createBusterRoute({
route: BusterRoutes.APP_DASHBOARD_ID,
dashboardId: assetId
})
);
}
}
removeVersionHistoryQueryParams();
}
);
const isRestoringVersion = useMemo(() => {
return isSavingDashboard || isSavingMetric;
}, [isSavingDashboard, isSavingMetric]);
return useMemo(() => { return useMemo(() => {
return { return {
listItems, listItems,
selectedVersion, selectedVersion,
onClickVersionHistory selectedQueryVersion,
onClickVersionHistory,
onClickRestoreVersion,
isRestoringVersion
}; };
}, [listItems, selectedVersion, onClickVersionHistory]); }, [
listItems,
selectedVersion,
selectedQueryVersion,
onClickVersionHistory,
onClickRestoreVersion
]);
}; };
const useListDashboardVersions = ({ const useListDashboardVersions = ({
@ -57,25 +119,48 @@ const useListDashboardVersions = ({
type: 'metric' | 'dashboard'; type: 'metric' | 'dashboard';
}) => { }) => {
const selectedVersionParam = useSearchParams().get('dashboard_version_number'); const selectedVersionParam = useSearchParams().get('dashboard_version_number');
const { data: dashboardVersions } = useGetDashboard( const { mutateAsync: updateDashboard, isPending: isSavingDashboard } = useUpdateDashboard();
const { data: dashData } = useGetDashboard(
{ {
id: type === 'dashboard' ? assetId : undefined, id: type === 'dashboard' ? assetId : undefined,
version_number: null version_number: null
}, },
(x) => x.dashboard.versions (x) => ({
versions: x.dashboard.versions,
version_number: x.dashboard.version_number
})
); );
const selectedVersion = useMemo(() => { const dashboardVersions = dashData?.versions;
const selectedVersion = dashData?.version_number;
const selectedQueryVersion = useMemo(() => {
if (selectedVersionParam) return parseInt(selectedVersionParam); if (selectedVersionParam) return parseInt(selectedVersionParam);
return last(dashboardVersions)?.version_number; return last(dashboardVersions)?.version_number;
}, [dashboardVersions, selectedVersionParam]); }, [dashboardVersions, selectedVersionParam]);
const onRestoreVersion = useMemoizedFn(async (versionNumber: number) => {
await updateDashboard({
id: assetId,
restore_to_version: versionNumber
});
});
return useMemo(() => { return useMemo(() => {
return { return {
dashboardVersions, dashboardVersions,
selectedVersion selectedQueryVersion,
onRestoreVersion,
selectedVersion,
isSavingDashboard
}; };
}, [dashboardVersions, selectedVersion]); }, [
dashboardVersions,
selectedVersion,
onRestoreVersion,
selectedQueryVersion,
isSavingDashboard
]);
}; };
const useListMetricVersions = ({ const useListMetricVersions = ({
@ -85,16 +170,31 @@ const useListMetricVersions = ({
assetId: string; assetId: string;
type: 'metric' | 'dashboard'; type: 'metric' | 'dashboard';
}) => { }) => {
const { mutateAsync: updateMetric, isPending: isSavingMetric } = useSaveMetric({
updateOnSave: true
});
const selectedVersionParam = useSearchParams().get('metric_version_number'); const selectedVersionParam = useSearchParams().get('metric_version_number');
const { data: metricVersions } = useGetMetric( const { data: metricData } = useGetMetric(
{ {
id: type === 'metric' ? assetId : undefined, id: type === 'metric' ? assetId : undefined,
version_number: null version_number: null
}, },
(x) => x.versions (x) => ({
versions: x.versions,
version_number: x.version_number
})
); );
const metricVersions = metricData?.versions;
const selectedVersion = metricData?.version_number;
const selectedVersion = useMemo(() => { const onRestoreVersion = useMemoizedFn(async (versionNumber: number) => {
await updateMetric({
id: assetId,
restore_to_version: versionNumber
});
});
const selectedQueryVersion = useMemo(() => {
if (selectedVersionParam) return parseInt(selectedVersionParam); if (selectedVersionParam) return parseInt(selectedVersionParam);
return last(metricVersions)?.version_number; return last(metricVersions)?.version_number;
}, [metricVersions, selectedVersionParam]); }, [metricVersions, selectedVersionParam]);
@ -102,7 +202,10 @@ const useListMetricVersions = ({
return useMemo(() => { return useMemo(() => {
return { return {
metricVersions, metricVersions,
selectedVersion selectedQueryVersion,
onRestoreVersion,
selectedVersion,
isSavingMetric
}; };
}, [metricVersions, selectedVersion]); }, [metricVersions, onRestoreVersion, selectedQueryVersion, selectedVersion, isSavingMetric]);
}; };

View File

@ -4,14 +4,10 @@ import { Button } from '@/components/ui/buttons';
import { ArrowLeft, History } from '@/components/ui/icons'; import { ArrowLeft, History } from '@/components/ui/icons';
import React from 'react'; import React from 'react';
import { useChatLayoutContextSelector } from '../../../ChatLayoutContext'; import { useChatLayoutContextSelector } from '../../../ChatLayoutContext';
import last from 'lodash/last';
import first from 'lodash/first'; import first from 'lodash/first';
import { useCloseVersionHistory } from './useCloseVersionHistory'; import { useCloseVersionHistory } from './useCloseVersionHistory';
import { useListVersionHistories } from '@/components/features/versionHistory'; import { useListVersionHistories } from '@/components/features/versionHistory';
import { useMemoizedFn } from '@/hooks'; import { useMemoizedFn } from '@/hooks';
import { useBusterNotifications } from '@/context/BusterNotifications';
import { useSaveMetric } from '@/api/buster_rest/metrics';
import { useUpdateDashboard } from '@/api/buster_rest/dashboards';
export const FileContainerHeaderVersionHistory = React.memo(() => { export const FileContainerHeaderVersionHistory = React.memo(() => {
const removeVersionHistoryQueryParams = useCloseVersionHistory(); const removeVersionHistoryQueryParams = useCloseVersionHistory();
@ -19,63 +15,43 @@ export const FileContainerHeaderVersionHistory = React.memo(() => {
return ( return (
<div className="flex w-full items-center justify-between gap-x-1.5"> <div className="flex w-full items-center justify-between gap-x-1.5">
<ExitVersionHistoryButton removeVersionHistoryQueryParams={removeVersionHistoryQueryParams} /> <ExitVersionHistoryButton removeVersionHistoryQueryParams={removeVersionHistoryQueryParams} />
<DisplayVersionHistory removeVersionHistoryQueryParams={removeVersionHistoryQueryParams} /> <DisplayVersionHistory />
</div> </div>
); );
}); });
FileContainerHeaderVersionHistory.displayName = 'FileContainerHeaderVersionHistory'; FileContainerHeaderVersionHistory.displayName = 'FileContainerHeaderVersionHistory';
const DisplayVersionHistory = React.memo( const DisplayVersionHistory = React.memo(({}: {}) => {
({ removeVersionHistoryQueryParams }: { removeVersionHistoryQueryParams: () => void }) => {
const { openSuccessMessage } = useBusterNotifications();
const selectedFile = useChatLayoutContextSelector((x) => x.selectedFile); const selectedFile = useChatLayoutContextSelector((x) => x.selectedFile);
const { listItems, selectedVersion } = useListVersionHistories({ const { listItems, isRestoringVersion, selectedQueryVersion, onClickRestoreVersion } =
useListVersionHistories({
assetId: selectedFile?.id || '', assetId: selectedFile?.id || '',
type: selectedFile?.type as 'metric' | 'dashboard' type: selectedFile?.type as 'metric' | 'dashboard'
}); });
const { mutateAsync: saveMetric, isPending: isSavingMetric } = useSaveMetric({
updateOnSave: true
});
const { mutateAsync: saveDashboard, isPending: isSavingDashboard } = useUpdateDashboard();
const isSaving = isSavingMetric || isSavingDashboard;
const currentVersion = first(listItems)?.version_number; const currentVersion = first(listItems)?.version_number;
const isSelectedVersionCurrent = selectedVersion === currentVersion; const isSelectedVersionCurrent = selectedQueryVersion === currentVersion;
const onMakeCurrentVersion = useMemoizedFn(async () => { const onClickRestoreVersionPreflight = useMemoizedFn(async () => {
const params = { if (selectedQueryVersion) {
id: selectedFile?.id || '', await onClickRestoreVersion(selectedQueryVersion, true);
update_version: true,
restore_to_version: selectedVersion
};
if (selectedFile?.type === 'metric') {
await saveMetric(params);
} }
if (selectedFile?.type === 'dashboard') {
await saveDashboard(params);
}
removeVersionHistoryQueryParams();
openSuccessMessage('Successfully made current version');
}); });
return ( return (
<div className="flex space-x-1.5"> <div className="flex space-x-1.5">
<Button variant="ghost" prefix={<History />}>{`Version ${selectedVersion || 0}`}</Button> <Button variant="ghost" prefix={<History />}>{`Version ${selectedQueryVersion || 0}`}</Button>
<Button <Button
variant="black" variant="black"
disabled={isSelectedVersionCurrent || !currentVersion} disabled={isSelectedVersionCurrent || !currentVersion}
onClick={onMakeCurrentVersion} onClick={onClickRestoreVersionPreflight}
loading={isSaving}> loading={isRestoringVersion}>
Current version Restore version
</Button> </Button>
</div> </div>
); );
} });
);
DisplayVersionHistory.displayName = 'DisplayVersionHistory'; DisplayVersionHistory.displayName = 'DisplayVersionHistory';
const ExitVersionHistoryButton = React.memo( const ExitVersionHistoryButton = React.memo(