version menu is working for metrics

This commit is contained in:
Nate Kelley 2025-03-26 10:40:26 -06:00
parent 3106198875
commit a28c68c9b4
No known key found for this signature in database
GPG Key ID: FD90372AB8D98B4F
12 changed files with 242 additions and 112 deletions

View File

@ -62,6 +62,10 @@ export const dashboardsUpdateDashboard = async (params: {
config?: DashboardConfig;
/** The file content of the dashboard */
file?: string;
/** update the version number of the dashboard - default is true */
update_version?: boolean;
/** restore the dashboard to a specific version */
restore_to_version?: number;
}) => {
return await mainApi
.put<BusterDashboardResponse>(`/dashboards/${params.id}`, params)

View File

@ -177,9 +177,20 @@ export const prefetchGetMetricDataClient = async (
* This is a mutation that saves a metric to the server.
* It will simply use the params passed in and not do any special logic.
*/
export const useSaveMetric = () => {
export const useSaveMetric = (params?: { updateOnSave?: boolean }) => {
const updateOnSave = params?.updateOnSave || false;
const queryClient = useQueryClient();
return useMutation({
mutationFn: updateMetric
mutationFn: updateMetric,
onSuccess: (data) => {
if (updateOnSave && data) {
const oldMetric = queryClient.getQueryData(
metricsQueryKeys.metricsGetMetric(data.id).queryKey
);
const newMetric = upgradeMetricToIMetric(data, oldMetric || null);
queryClient.setQueryData(metricsQueryKeys.metricsGetMetric(data.id).queryKey, newMetric);
}
}
});
};

View File

@ -76,6 +76,10 @@ export const updateMetric = async (params: {
status?: VerificationStatus;
/** file in yaml format to update */
file?: string;
/** update the version number of the metric - default is true */
update_version?: boolean;
/** restore the metric to a specific version */
restore_to_version?: number;
}) => {
return mainApi.put<BusterMetric>(`/metrics/${params.id}`, params).then((res) => res.data);
};

View File

@ -1,43 +1,35 @@
import { useGetDashboard } from '@/api/buster_rest/dashboards';
import { useGetMetric } from '@/api/buster_rest/metrics';
import React, { useMemo } from 'react';
import React, { useMemo, useRef } from 'react';
import { Button } from '@/components/ui/buttons';
import { Xmark } from '@/components/ui/icons';
import { Check3 } from '@/components/ui/icons/NucleoIconFilled';
import { Text } from '@/components/ui/typography';
import { useCloseVersionHistory } from '@/layouts/ChatLayout/FileContainer/FileContainerHeader/MetricContainerHeaderButtons/FileContainerVersionHistory';
import { useCloseVersionHistory } from '@/layouts/ChatLayout/FileContainer/FileContainerHeader/FileContainerHeaderVersionHistory';
import { cn } from '@/lib/classMerge';
import { useMemoizedFn } from '@/hooks';
import { useAppLayoutContextSelector } from '@/context/BusterAppLayout';
import { timeFromNow } from '@/lib';
import { timeFromNow, timeout } from '@/lib';
import { AppPageLayout } from '@/components/ui/layouts';
import { useSearchParams } from 'next/navigation';
import last from 'lodash/last';
import { useListVersionHistories } from './useListVersionHistories';
import { useMount } from '@/hooks';
export const VersionHistoryPanel = React.memo(
({ assetId, type }: { assetId: string; type: 'metric' | 'dashboard' }) => {
const onChangeQueryParams = useAppLayoutContextSelector((x) => x.onChangeQueryParams);
const { dashboardVersions, selectedVersion: dashboardSelectedVersion } =
useListDashboardVersions({
assetId,
type
});
const { metricVersions, selectedVersion: metricSelectedVersion } = useListMetricVersions({
const { listItems, selectedVersion, onClickVersionHistory } = useListVersionHistories({
assetId,
type
});
const bodyRef = useRef<HTMLDivElement>(null);
const listItems = useMemo(() => {
const items = type === 'metric' ? metricVersions : dashboardVersions;
return items ? [...items].reverse() : undefined;
}, [type, dashboardVersions, metricVersions]);
const selectedVersion = useMemo(() => {
return type === 'metric' ? metricSelectedVersion : dashboardSelectedVersion;
}, [type, dashboardSelectedVersion, metricSelectedVersion]);
const onClickVersionHistory = useMemoizedFn((versionNumber: number) => {
onChangeQueryParams({ metric_version_number: versionNumber.toString() });
useMount(async () => {
if (bodyRef.current) {
await timeout(200);
const selectedNode = bodyRef.current.querySelector('.selected-version');
if (selectedNode) {
selectedNode.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
}
});
return (
@ -51,7 +43,7 @@ export const VersionHistoryPanel = React.memo(
scrollable
headerBorderVariant="ghost"
headerClassName="border-l">
<div className="mx-1.5 my-1.5 flex flex-col">
<div ref={bodyRef} className="mx-1.5 mb-1.5 flex flex-col">
{listItems?.map((item) => (
<ListItem
key={item.version_number}
@ -83,7 +75,7 @@ const ListItem = React.memo(
onClick={() => onClickVersionHistory(version_number)}
className={cn(
'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 && 'bg-item-select hover:bg-item-select selected-version'
)}>
<div className="flex flex-col justify-center space-y-0.5">
<Text>{`Version ${version_number}`}</Text>
@ -92,7 +84,7 @@ const ListItem = React.memo(
</Text>
</div>
{selected && (
<div className="text-icon-color flex items-center">
<div className="text-icon-color animate-in fade-in-0 flex items-center duration-200">
<Check3 />
</div>
)}

View File

@ -1 +1,2 @@
export * from './VersionHistoryPanel';
export * from './useListVersionHistories';

View File

@ -0,0 +1,108 @@
import { useGetDashboard } from '@/api/buster_rest/dashboards';
import { useGetMetric } from '@/api/buster_rest/metrics';
import { useAppLayoutContextSelector } from '@/context/BusterAppLayout';
import { useMemoizedFn } from '@/hooks';
import last from 'lodash/last';
import { useSearchParams } from 'next/navigation';
import { useMemo } from 'react';
export const useListVersionHistories = ({
assetId,
type
}: {
assetId: string;
type: 'metric' | 'dashboard';
}) => {
const onChangeQueryParams = useAppLayoutContextSelector((x) => x.onChangeQueryParams);
const { dashboardVersions, selectedVersion: dashboardSelectedVersion } = useListDashboardVersions(
{
assetId,
type
}
);
const { metricVersions, selectedVersion: metricSelectedVersion } = useListMetricVersions({
assetId,
type
});
const listItems = useMemo(() => {
const items = type === 'metric' ? metricVersions : dashboardVersions;
return items ? [...items].reverse() : undefined;
}, [type, dashboardVersions, metricVersions]);
const selectedVersion = useMemo(() => {
return type === 'metric' ? metricSelectedVersion : dashboardSelectedVersion;
}, [type, dashboardSelectedVersion, metricSelectedVersion]);
const onClickVersionHistory = useMemoizedFn((versionNumber: number) => {
if (type === 'metric') onChangeQueryParams({ metric_version_number: versionNumber.toString() });
if (type === 'dashboard')
onChangeQueryParams({ dashboard_version_number: versionNumber.toString() });
});
return useMemo(() => {
return {
listItems,
selectedVersion,
onClickVersionHistory
};
}, [listItems, selectedVersion, onClickVersionHistory]);
};
const useListDashboardVersions = ({
assetId,
type
}: {
assetId: string;
type: 'metric' | 'dashboard';
}) => {
const selectedVersionParam = useSearchParams().get('dashboard_version_number');
const { data: dashboardVersions } = useGetDashboard(
{
id: type === 'dashboard' ? assetId : undefined,
version_number: null
},
(x) => x.dashboard.versions
);
const selectedVersion = useMemo(() => {
if (selectedVersionParam) return parseInt(selectedVersionParam);
return last(dashboardVersions)?.version_number;
}, [dashboardVersions, selectedVersionParam]);
return useMemo(() => {
return {
dashboardVersions,
selectedVersion
};
}, [dashboardVersions, selectedVersion]);
};
const useListMetricVersions = ({
assetId,
type
}: {
assetId: string;
type: 'metric' | 'dashboard';
}) => {
const selectedVersionParam = useSearchParams().get('metric_version_number');
const { data: metricVersions } = useGetMetric(
{
id: type === 'metric' ? assetId : undefined,
version_number: null
},
(x) => x.versions
);
const selectedVersion = useMemo(() => {
if (selectedVersionParam) return parseInt(selectedVersionParam);
return last(metricVersions)?.version_number;
}, [metricVersions, selectedVersionParam]);
return useMemo(() => {
return {
metricVersions,
selectedVersion
};
}, [metricVersions, selectedVersion]);
};

View File

@ -11,7 +11,7 @@ import { MetricContainerHeaderButtons } from './MetricContainerHeaderButtons';
import { useChatLayoutContextSelector } from '../../ChatLayoutContext';
import { ReasoningContainerHeaderSegment } from './ReasoningContainerHeaderSegment';
import { useAssetCheck } from '@/api/buster_rest/assets/queryRequests';
import { FileContainerVersionHistory } from './MetricContainerHeaderButtons/FileContainerVersionHistory/FileContainerVersionHistory';
import { FileContainerHeaderVersionHistory } from './FileContainerHeaderVersionHistory';
export const FileContainerHeader: React.FC = React.memo(() => {
const selectedFileType = useChatLayoutContextSelector((x) => x.selectedFile?.type);
@ -44,7 +44,7 @@ export const FileContainerHeader: React.FC = React.memo(() => {
(x) => x.has_access
);
if (isVersionHistoryMode) return <FileContainerVersionHistory />;
if (isVersionHistoryMode) return <FileContainerHeaderVersionHistory />;
return (
<>

View File

@ -0,0 +1,90 @@
'use client';
import { Button } from '@/components/ui/buttons';
import { ArrowLeft, History } from '@/components/ui/icons';
import React from 'react';
import { useChatLayoutContextSelector } from '../../../ChatLayoutContext';
import last from 'lodash/last';
import first from 'lodash/first';
import { useCloseVersionHistory } from './useCloseVersionHistory';
import { useListVersionHistories } from '@/components/features/versionHistory';
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(() => {
const removeVersionHistoryQueryParams = useCloseVersionHistory();
return (
<div className="flex w-full items-center justify-between gap-x-1.5">
<ExitVersionHistoryButton removeVersionHistoryQueryParams={removeVersionHistoryQueryParams} />
<DisplayVersionHistory removeVersionHistoryQueryParams={removeVersionHistoryQueryParams} />
</div>
);
});
FileContainerHeaderVersionHistory.displayName = 'FileContainerHeaderVersionHistory';
const DisplayVersionHistory = React.memo(
({ removeVersionHistoryQueryParams }: { removeVersionHistoryQueryParams: () => void }) => {
const { openSuccessMessage } = useBusterNotifications();
const selectedFile = useChatLayoutContextSelector((x) => x.selectedFile);
const { listItems, selectedVersion } = useListVersionHistories({
assetId: selectedFile?.id || '',
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 isSelectedVersionCurrent = selectedVersion === currentVersion;
const onMakeCurrentVersion = useMemoizedFn(async () => {
const params = {
id: selectedFile?.id || '',
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 (
<div className="flex space-x-1.5">
<Button variant="ghost" prefix={<History />}>{`Version ${selectedVersion || 0}`}</Button>
<Button
variant="black"
disabled={isSelectedVersionCurrent || !currentVersion}
onClick={onMakeCurrentVersion}
loading={isSaving}>
Current version
</Button>
</div>
);
}
);
DisplayVersionHistory.displayName = 'DisplayVersionHistory';
const ExitVersionHistoryButton = React.memo(
({ removeVersionHistoryQueryParams }: { removeVersionHistoryQueryParams: () => void }) => {
return (
<Button variant="link" prefix={<ArrowLeft />} onClick={removeVersionHistoryQueryParams}>
Exit version history
</Button>
);
}
);
ExitVersionHistoryButton.displayName = 'ExitVersionHistoryButton';

View File

@ -0,0 +1,2 @@
export * from './FileContainerHeaderVersionHistory';
export * from './useCloseVersionHistory';

View File

@ -1,80 +0,0 @@
'use client';
import { Button } from '@/components/ui/buttons';
import { ArrowLeft, History } from '@/components/ui/icons';
import React, { useMemo } from 'react';
import { useChatLayoutContextSelector } from '../../../../ChatLayoutContext';
import { useGetMetric } from '@/api/buster_rest/metrics';
import last from 'lodash/last';
import { useGetDashboard } from '@/api/buster_rest/dashboards';
import { useCloseVersionHistory } from './useCloseVersionHistory';
export const FileContainerVersionHistory = React.memo(() => {
return (
<div className="flex w-full items-center justify-between gap-x-1.5">
<ExitVersionHistoryButton />
<DisplayVersionHistory />
</div>
);
});
FileContainerVersionHistory.displayName = 'FileContainerVersionHistory';
const DisplayVersionHistory = React.memo(() => {
const selectedFile = useChatLayoutContextSelector((x) => x.selectedFile);
const type = selectedFile?.type;
const { data: metric } = useGetMetric(
{ id: type === 'metric' ? selectedFile?.id : undefined },
(x) => ({ version_number: x.version_number, latestVersion: last(x.versions) })
);
const { data: dashboard } = useGetDashboard(
{
id: type === 'dashboard' ? selectedFile?.id : undefined
},
(x) => ({
version_number: x.dashboard.version_number,
latestVersion: last(x.dashboard.versions)
})
);
const versionInfo = useMemo(() => {
if (!metric?.version_number && !dashboard?.version_number) return null;
if (type === 'metric') {
return {
isCurrent: metric?.version_number === metric?.latestVersion?.version_number,
versionNumber: metric?.version_number
};
}
if (type === 'dashboard') {
return {
isCurrent: dashboard?.version_number === dashboard?.latestVersion?.version_number,
versionNumber: dashboard?.version_number
};
}
return null;
}, [type, metric, dashboard]);
if (!versionInfo) return null;
return (
<div className="flex space-x-1.5">
<Button variant="ghost" prefix={<History />}>{`Version ${versionInfo.versionNumber}`}</Button>
<Button variant="black" disabled={!versionInfo.isCurrent}>
Current version
</Button>
</div>
);
});
DisplayVersionHistory.displayName = 'DisplayVersionHistory';
const ExitVersionHistoryButton = React.memo(() => {
const removeVersionHistoryQueryParams = useCloseVersionHistory();
return (
<Button variant="link" prefix={<ArrowLeft />} onClick={removeVersionHistoryQueryParams}>
Exit version history
</Button>
);
});
ExitVersionHistoryButton.displayName = 'ExitVersionHistoryButton';

View File

@ -1,2 +0,0 @@
export * from './FileContainerVersionHistory';
export * from './useCloseVersionHistory';