From a28c68c9b4e8b9a1dc5e84e82039985c126d3001 Mon Sep 17 00:00:00 2001 From: Nate Kelley Date: Wed, 26 Mar 2025 10:40:26 -0600 Subject: [PATCH] version menu is working for metrics --- .../api/buster_rest/dashboards/requests.ts | 4 + .../api/buster_rest/metrics/queryRequests.ts | 15 ++- web/src/api/buster_rest/metrics/requests.ts | 4 + .../versionHistory/VersionHistoryPanel.tsx | 44 +++---- .../features/versionHistory/index.ts | 1 + .../useListVersionHistories.tsx | 108 ++++++++++++++++++ .../FileContainerHeader.tsx | 4 +- .../FileContainerHeaderVersionHistory.tsx | 90 +++++++++++++++ .../index.ts | 2 + .../useCloseVersionHistory.ts | 0 .../FileContainerVersionHistory.tsx | 80 ------------- .../FileContainerVersionHistory/index.ts | 2 - 12 files changed, 242 insertions(+), 112 deletions(-) create mode 100644 web/src/components/features/versionHistory/useListVersionHistories.tsx create mode 100644 web/src/layouts/ChatLayout/FileContainer/FileContainerHeader/FileContainerHeaderVersionHistory/FileContainerHeaderVersionHistory.tsx create mode 100644 web/src/layouts/ChatLayout/FileContainer/FileContainerHeader/FileContainerHeaderVersionHistory/index.ts rename web/src/layouts/ChatLayout/FileContainer/FileContainerHeader/{MetricContainerHeaderButtons/FileContainerVersionHistory => FileContainerHeaderVersionHistory}/useCloseVersionHistory.ts (100%) delete mode 100644 web/src/layouts/ChatLayout/FileContainer/FileContainerHeader/MetricContainerHeaderButtons/FileContainerVersionHistory/FileContainerVersionHistory.tsx delete mode 100644 web/src/layouts/ChatLayout/FileContainer/FileContainerHeader/MetricContainerHeaderButtons/FileContainerVersionHistory/index.ts diff --git a/web/src/api/buster_rest/dashboards/requests.ts b/web/src/api/buster_rest/dashboards/requests.ts index 3c7bbe84b..4fbfecab9 100644 --- a/web/src/api/buster_rest/dashboards/requests.ts +++ b/web/src/api/buster_rest/dashboards/requests.ts @@ -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(`/dashboards/${params.id}`, params) diff --git a/web/src/api/buster_rest/metrics/queryRequests.ts b/web/src/api/buster_rest/metrics/queryRequests.ts index 4aecab39a..b6c5bb1ff 100644 --- a/web/src/api/buster_rest/metrics/queryRequests.ts +++ b/web/src/api/buster_rest/metrics/queryRequests.ts @@ -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); + } + } }); }; diff --git a/web/src/api/buster_rest/metrics/requests.ts b/web/src/api/buster_rest/metrics/requests.ts index 7683179b9..b57e23337 100644 --- a/web/src/api/buster_rest/metrics/requests.ts +++ b/web/src/api/buster_rest/metrics/requests.ts @@ -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(`/metrics/${params.id}`, params).then((res) => res.data); }; diff --git a/web/src/components/features/versionHistory/VersionHistoryPanel.tsx b/web/src/components/features/versionHistory/VersionHistoryPanel.tsx index 0a868d149..b7f0c3cac 100644 --- a/web/src/components/features/versionHistory/VersionHistoryPanel.tsx +++ b/web/src/components/features/versionHistory/VersionHistoryPanel.tsx @@ -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(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"> -
+
{listItems?.map((item) => ( 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' )}>
{`Version ${version_number}`} @@ -92,7 +84,7 @@ const ListItem = React.memo(
{selected && ( -
+
)} diff --git a/web/src/components/features/versionHistory/index.ts b/web/src/components/features/versionHistory/index.ts index aa4fbfead..14f846da5 100644 --- a/web/src/components/features/versionHistory/index.ts +++ b/web/src/components/features/versionHistory/index.ts @@ -1 +1,2 @@ export * from './VersionHistoryPanel'; +export * from './useListVersionHistories'; diff --git a/web/src/components/features/versionHistory/useListVersionHistories.tsx b/web/src/components/features/versionHistory/useListVersionHistories.tsx new file mode 100644 index 000000000..cb03d4b27 --- /dev/null +++ b/web/src/components/features/versionHistory/useListVersionHistories.tsx @@ -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]); +}; diff --git a/web/src/layouts/ChatLayout/FileContainer/FileContainerHeader/FileContainerHeader.tsx b/web/src/layouts/ChatLayout/FileContainer/FileContainerHeader/FileContainerHeader.tsx index b39263974..2bd218206 100644 --- a/web/src/layouts/ChatLayout/FileContainer/FileContainerHeader/FileContainerHeader.tsx +++ b/web/src/layouts/ChatLayout/FileContainer/FileContainerHeader/FileContainerHeader.tsx @@ -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 ; + if (isVersionHistoryMode) return ; return ( <> diff --git a/web/src/layouts/ChatLayout/FileContainer/FileContainerHeader/FileContainerHeaderVersionHistory/FileContainerHeaderVersionHistory.tsx b/web/src/layouts/ChatLayout/FileContainer/FileContainerHeader/FileContainerHeaderVersionHistory/FileContainerHeaderVersionHistory.tsx new file mode 100644 index 000000000..1799776c4 --- /dev/null +++ b/web/src/layouts/ChatLayout/FileContainer/FileContainerHeader/FileContainerHeaderVersionHistory/FileContainerHeaderVersionHistory.tsx @@ -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 ( +
+ + +
+ ); +}); + +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 ( +
+ + +
+ ); + } +); +DisplayVersionHistory.displayName = 'DisplayVersionHistory'; + +const ExitVersionHistoryButton = React.memo( + ({ removeVersionHistoryQueryParams }: { removeVersionHistoryQueryParams: () => void }) => { + return ( + + ); + } +); +ExitVersionHistoryButton.displayName = 'ExitVersionHistoryButton'; diff --git a/web/src/layouts/ChatLayout/FileContainer/FileContainerHeader/FileContainerHeaderVersionHistory/index.ts b/web/src/layouts/ChatLayout/FileContainer/FileContainerHeader/FileContainerHeaderVersionHistory/index.ts new file mode 100644 index 000000000..35bf60389 --- /dev/null +++ b/web/src/layouts/ChatLayout/FileContainer/FileContainerHeader/FileContainerHeaderVersionHistory/index.ts @@ -0,0 +1,2 @@ +export * from './FileContainerHeaderVersionHistory'; +export * from './useCloseVersionHistory'; diff --git a/web/src/layouts/ChatLayout/FileContainer/FileContainerHeader/MetricContainerHeaderButtons/FileContainerVersionHistory/useCloseVersionHistory.ts b/web/src/layouts/ChatLayout/FileContainer/FileContainerHeader/FileContainerHeaderVersionHistory/useCloseVersionHistory.ts similarity index 100% rename from web/src/layouts/ChatLayout/FileContainer/FileContainerHeader/MetricContainerHeaderButtons/FileContainerVersionHistory/useCloseVersionHistory.ts rename to web/src/layouts/ChatLayout/FileContainer/FileContainerHeader/FileContainerHeaderVersionHistory/useCloseVersionHistory.ts diff --git a/web/src/layouts/ChatLayout/FileContainer/FileContainerHeader/MetricContainerHeaderButtons/FileContainerVersionHistory/FileContainerVersionHistory.tsx b/web/src/layouts/ChatLayout/FileContainer/FileContainerHeader/MetricContainerHeaderButtons/FileContainerVersionHistory/FileContainerVersionHistory.tsx deleted file mode 100644 index fedfa41c4..000000000 --- a/web/src/layouts/ChatLayout/FileContainer/FileContainerHeader/MetricContainerHeaderButtons/FileContainerVersionHistory/FileContainerVersionHistory.tsx +++ /dev/null @@ -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 ( -
- - -
- ); -}); - -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 ( -
- - -
- ); -}); -DisplayVersionHistory.displayName = 'DisplayVersionHistory'; - -const ExitVersionHistoryButton = React.memo(() => { - const removeVersionHistoryQueryParams = useCloseVersionHistory(); - - return ( - - ); -}); -ExitVersionHistoryButton.displayName = 'ExitVersionHistoryButton'; diff --git a/web/src/layouts/ChatLayout/FileContainer/FileContainerHeader/MetricContainerHeaderButtons/FileContainerVersionHistory/index.ts b/web/src/layouts/ChatLayout/FileContainer/FileContainerHeader/MetricContainerHeaderButtons/FileContainerVersionHistory/index.ts deleted file mode 100644 index 54cace768..000000000 --- a/web/src/layouts/ChatLayout/FileContainer/FileContainerHeader/MetricContainerHeaderButtons/FileContainerVersionHistory/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './FileContainerVersionHistory'; -export * from './useCloseVersionHistory';