From ba16fcac4aaf81b3260e8eb3506ea1e2412d3532 Mon Sep 17 00:00:00 2001 From: Nate Kelley Date: Wed, 16 Apr 2025 23:47:14 -0600 Subject: [PATCH] versions for dashboard --- .../dashboards/dashboardQueryHelpers.ts | 183 ++++++++++++++++++ .../buster_rest/dashboards/queryHelpers.ts | 109 ----------- .../buster_rest/dashboards/queryRequests.ts | 88 ++++----- .../dashboards/queryServerRequests.ts | 2 +- .../buster_rest/metrics/metricQueryHelpers.ts | 44 ++--- .../buster_rest/metrics/queryReqestsServer.ts | 2 +- .../metrics/updateMetricQueryRequests.ts | 6 +- web/src/api/query_keys/dashboard.ts | 4 +- .../features/sidebars/SidebarPrimary.tsx | 12 +- .../components/ui/popup/PopupContainer.tsx | 5 +- .../context/Assets/BusterAssetsProvider.tsx | 2 +- .../Dashboards/useGetDashboardMemoized.tsx | 2 +- .../Dashboards/useIsDashboardChanged.tsx | 9 +- .../context/Metrics/useGetMetricMemoized.ts | 20 +- .../context/Metrics/useIsMetricChanged.tsx | 5 +- .../DashboardContentController.tsx | 6 +- .../DashboardEmptyState.tsx | 10 + .../MetricViewChart/MetricViewChartHeader.tsx | 1 + .../useMetricRunSQL/useMetricRunSQL.ts | 23 +-- .../AppAssetCheckLayout/useGetAsset.tsx | 11 +- .../ChatHeader/ChatHeaderTitle.tsx | 1 + 21 files changed, 320 insertions(+), 225 deletions(-) create mode 100644 web/src/api/buster_rest/dashboards/dashboardQueryHelpers.ts delete mode 100644 web/src/api/buster_rest/dashboards/queryHelpers.ts diff --git a/web/src/api/buster_rest/dashboards/dashboardQueryHelpers.ts b/web/src/api/buster_rest/dashboards/dashboardQueryHelpers.ts new file mode 100644 index 000000000..b16c28e2b --- /dev/null +++ b/web/src/api/buster_rest/dashboards/dashboardQueryHelpers.ts @@ -0,0 +1,183 @@ +import { BusterDashboardResponse } from '@/api/asset_interfaces/dashboard'; +import { queryKeys } from '@/api/query_keys'; +import { dashboardQueryKeys } from '@/api/query_keys/dashboard'; +import { useBusterAssetsContextSelector } from '@/context/Assets/BusterAssetsProvider'; +import { useBusterNotifications } from '@/context/BusterNotifications'; +import { useOriginalDashboardStore } from '@/context/Dashboards'; +import { useMemoizedFn } from '@/hooks/useMemoizedFn'; +import { upgradeMetricToIMetric } from '@/lib/metrics/upgradeToIMetric'; +import { Query, useQueryClient } from '@tanstack/react-query'; +import { prefetchGetMetricDataClient } from '../metrics/queryRequests'; +import { dashboardsGetDashboard } from './requests'; +import { useParams, useSearchParams } from 'next/navigation'; +import { useMemo } from 'react'; +import { RustApiError } from '../errors'; +import last from 'lodash/last'; + +export const useEnsureDashboardConfig = (prefetchData: boolean = true) => { + const queryClient = useQueryClient(); + const prefetchDashboard = useGetDashboardAndInitializeMetrics(prefetchData); + const { openErrorMessage } = useBusterNotifications(); + const getLatestDashboardVersion = useGetLatestDashboardVersionNumber(); + + const method = useMemoizedFn(async (dashboardId: string) => { + const latestVersion = getLatestDashboardVersion(dashboardId); + const options = dashboardQueryKeys.dashboardGetDashboard(dashboardId, latestVersion); + let dashboardResponse = queryClient.getQueryData(options.queryKey); + if (!dashboardResponse) { + const res = await prefetchDashboard(dashboardId, latestVersion || undefined).catch((e) => { + openErrorMessage('Failed to save metrics to dashboard. Dashboard not found'); + return null; + }); + if (res) { + queryClient.setQueryData(options.queryKey, res); + dashboardResponse = res; + } + } + + return dashboardResponse; + }); + + return method; +}; + +export const useGetDashboardAndInitializeMetrics = (prefetchData: boolean = true) => { + const queryClient = useQueryClient(); + const setOriginalDashboards = useOriginalDashboardStore((x) => x.setOriginalDashboard); + const getAssetPassword = useBusterAssetsContextSelector((state) => state.getAssetPassword); + + const initializeMetrics = useMemoizedFn((metrics: BusterDashboardResponse['metrics']) => { + for (const metric of Object.values(metrics)) { + const prevMetric = queryClient.getQueryData( + queryKeys.metricsGetMetric(metric.id, metric.version_number).queryKey + ); + const upgradedMetric = upgradeMetricToIMetric(metric, prevMetric); + queryClient.setQueryData( + queryKeys.metricsGetMetric(metric.id, metric.version_number).queryKey, + upgradedMetric + ); + if (prefetchData) { + prefetchGetMetricDataClient( + { id: metric.id, version_number: metric.version_number }, + queryClient + ); + } + } + }); + + return useMemoizedFn(async (id: string, version_number: number | null | undefined) => { + const { password } = getAssetPassword?.(id) || {}; + + return dashboardsGetDashboard({ + id: id!, + password, + version_number: version_number || undefined + }).then((data) => { + initializeMetrics(data.metrics); + setOriginalDashboards(data.dashboard); + + if (!version_number && data.dashboard.version_number) { + queryClient.setQueryData( + dashboardQueryKeys.dashboardGetDashboard(id, data.dashboard.version_number).queryKey, + data + ); + } + + return data; + }); + }); +}; + +export const useGetDashboardVersionNumber = (props?: { + versionNumber?: number | null; //if null it will not use a params from the query params +}) => { + const { versionNumber: versionNumberProp } = props || {}; + const { dashboardId: dashboardIdPathParam } = useParams() as { + dashboardId: string | undefined; + }; + const versionNumberQueryParam = useSearchParams().get('dashboard_version_number'); + const versionNumberFromParams = dashboardIdPathParam ? versionNumberQueryParam : undefined; + + const paramVersionNumber = useMemo(() => { + return ( + versionNumberProp ?? + (versionNumberFromParams ? parseInt(versionNumberFromParams!) : undefined) + ); + }, [versionNumberProp, versionNumberFromParams]); + + const latestVersionNumber = useGetLatestDashboardVersion({ dashboardId: dashboardIdPathParam! }); + + const selectedVersionNumber: number | null = useMemo(() => { + if (versionNumberProp === null) return null; + return paramVersionNumber || latestVersionNumber || 0; + }, [paramVersionNumber, latestVersionNumber]); + + return useMemo(() => { + return { selectedVersionNumber, paramVersionNumber, latestVersionNumber }; + }, [selectedVersionNumber, selectedVersionNumber, latestVersionNumber]); +}; + +type PredicateType = (query: Query) => boolean; +const filterMetricPredicate = ((query) => { + const lastKey = last(query.queryKey); + return ( + typeof lastKey === 'number' && + !!lastKey && + query.state.data !== undefined && + typeof query.state.data === 'object' && + query.state.data !== null && + 'versions' in query.state.data + ); +}); + +const getLatestVersionNumber = ( + queries: [readonly unknown[], BusterDashboardResponse | undefined][] +) => { + let maxVersion = -Infinity; + + // Single pass: filter and find max version + for (const [queryKey, data] of queries) { + if (data && typeof data === 'object' && 'versions' in data) { + const lastVersion = last(data.versions); + const version = Number(lastVersion?.version_number); + maxVersion = Math.max(maxVersion, version, data.dashboard.version_number); + } + } + + return maxVersion === -Infinity ? null : maxVersion; +}; + +const useGetLatestDashboardVersion = ({ dashboardId }: { dashboardId: string }) => { + const queryClient = useQueryClient(); + + const memoizedKey = useMemo(() => { + return dashboardQueryKeys.dashboardGetDashboard(dashboardId, null).queryKey.slice(0, -1); + }, [dashboardId]); + + const queries = queryClient.getQueriesData({ + queryKey: memoizedKey, + predicate: filterMetricPredicate + }); + + const latestVersion = useMemo(() => { + return getLatestVersionNumber(queries); + }, [queries.length]); + + return latestVersion; +}; + +//This is a helper function that returns the latest version number for a metric +export const useGetLatestDashboardVersionNumber = () => { + const queryClient = useQueryClient(); + + const method = useMemoizedFn((dashboardId: string) => { + const queries = queryClient.getQueriesData({ + queryKey: dashboardQueryKeys.dashboardGetDashboard(dashboardId, null).queryKey.slice(0, -1), + predicate: filterMetricPredicate + }); + + return getLatestVersionNumber(queries); + }); + + return method; +}; diff --git a/web/src/api/buster_rest/dashboards/queryHelpers.ts b/web/src/api/buster_rest/dashboards/queryHelpers.ts deleted file mode 100644 index 1793c3b79..000000000 --- a/web/src/api/buster_rest/dashboards/queryHelpers.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { BusterDashboardResponse } from '@/api/asset_interfaces/dashboard'; -import { IBusterMetric } from '@/api/asset_interfaces/metric'; -import { queryKeys } from '@/api/query_keys'; -import { dashboardQueryKeys } from '@/api/query_keys/dashboard'; -import { metricsQueryKeys } from '@/api/query_keys/metric'; -import { useBusterAssetsContextSelector } from '@/context/Assets/BusterAssetsProvider'; -import { useBusterNotifications } from '@/context/BusterNotifications'; -import { useOriginalDashboardStore } from '@/context/Dashboards'; -import { useMemoizedFn } from '@/hooks/useMemoizedFn'; -import { upgradeMetricToIMetric } from '@/lib/metrics/upgradeToIMetric'; -import { useQueryClient } from '@tanstack/react-query'; -import { prefetchGetMetricDataClient } from '../metrics/queryRequests'; -import { dashboardsGetDashboard } from './requests'; -import { useParams, useSearchParams } from 'next/navigation'; -import { useMemo } from 'react'; - -export const useEnsureDashboardConfig = (prefetchData: boolean = true) => { - const queryClient = useQueryClient(); - const versionNumber = useGetDashboardVersionNumber(); - const prefetchDashboard = useGetDashboardAndInitializeMetrics(prefetchData); - const { openErrorMessage } = useBusterNotifications(); - - const method = useMemoizedFn(async (dashboardId: string) => { - const options = dashboardQueryKeys.dashboardGetDashboard(dashboardId, versionNumber); - let dashboardResponse = queryClient.getQueryData(options.queryKey); - if (!dashboardResponse) { - const res = await prefetchDashboard(dashboardId).catch((e) => { - openErrorMessage('Failed to save metrics to dashboard. Dashboard not found'); - return null; - }); - if (res) { - queryClient.setQueryData(options.queryKey, res); - dashboardResponse = res; - } - } - - return dashboardResponse; - }); - - return method; -}; - -export const useGetDashboardAndInitializeMetrics = (prefetchData: boolean = true) => { - const queryClient = useQueryClient(); - const setOriginalDashboards = useOriginalDashboardStore((x) => x.setOriginalDashboard); - const getAssetPassword = useBusterAssetsContextSelector((state) => state.getAssetPassword); - - const initializeMetrics = useMemoizedFn((metrics: BusterDashboardResponse['metrics']) => { - for (const metric of Object.values(metrics)) { - const prevMetric = queryClient.getQueryData( - queryKeys.metricsGetMetric(metric.id, metric.version_number).queryKey - ); - const upgradedMetric = upgradeMetricToIMetric(metric, prevMetric); - queryClient.setQueryData( - queryKeys.metricsGetMetric(metric.id, metric.version_number).queryKey, - upgradedMetric - ); - if (prefetchData) { - prefetchGetMetricDataClient( - { id: metric.id, version_number: metric.version_number }, - queryClient - ); - } - } - }); - - return useMemoizedFn(async (id: string, version_number?: number) => { - const { password } = getAssetPassword?.(id) || {}; - - return dashboardsGetDashboard({ id: id!, password, version_number }).then((data) => { - initializeMetrics(data.metrics); - setOriginalDashboards(data.dashboard); - - if (!version_number && data.dashboard.version_number) { - queryClient.setQueryData( - dashboardQueryKeys.dashboardGetDashboard(id, data.dashboard.version_number).queryKey, - data - ); - } - - return data; - }); - }); -}; - -export const useGetDashboardVersionNumber = (props?: { - versionNumber?: number | null; //if null it will not use a params from the query params -}) => { - const { versionNumber: versionNumberProp } = props || {}; - const { versionNumber: versionNumberPathParam, dashboardId: dashboardIdPathParam } = - useParams() as { - versionNumber: string | undefined; - dashboardId: string | undefined; - }; - const versionNumberQueryParam = useSearchParams().get('dashboard_version_number'); - const versionNumberFromParams = dashboardIdPathParam - ? versionNumberQueryParam || versionNumberPathParam - : undefined; - - const versionNumber = useMemo(() => { - if (versionNumberProp === null) return undefined; - return ( - versionNumberProp ?? - (versionNumberFromParams ? parseInt(versionNumberFromParams!) : undefined) - ); - }, [versionNumberProp, versionNumberFromParams]); - - return versionNumber; -}; diff --git a/web/src/api/buster_rest/dashboards/queryRequests.ts b/web/src/api/buster_rest/dashboards/queryRequests.ts index c7d49f39a..180a74898 100644 --- a/web/src/api/buster_rest/dashboards/queryRequests.ts +++ b/web/src/api/buster_rest/dashboards/queryRequests.ts @@ -32,8 +32,9 @@ import { useGetDashboardAndInitializeMetrics, useGetDashboardVersionNumber, useEnsureDashboardConfig -} from './queryHelpers'; +} from './dashboardQueryHelpers'; import { useGetLatestMetricVersionNumber } from '../metrics'; +import { useBusterAssetsContextSelector } from '@/context/Assets/BusterAssetsProvider'; export const useGetDashboard = ( { @@ -46,14 +47,29 @@ export const useGetDashboard = ( > ) => { const queryFn = useGetDashboardAndInitializeMetrics(); - const versionNumber = useGetDashboardVersionNumber({ versionNumber: versionNumberProp }); + const setAssetPasswordError = useBusterAssetsContextSelector((x) => x.setAssetPasswordError); + const { selectedVersionNumber, latestVersionNumber, paramVersionNumber } = + useGetDashboardVersionNumber({ versionNumber: versionNumberProp }); + + const { isFetched: isFetchedInitial, isError: isErrorInitial } = useQuery({ + ...dashboardQueryKeys.dashboardGetDashboard(id!, null), + queryFn: () => queryFn(id!, paramVersionNumber), + enabled: false, //we made this false because we want to be explicit about the fact that we fetch the dashboard server side + retry(failureCount, error) { + if (error?.message !== undefined) { + setAssetPasswordError(id!, error.message || 'An error occurred'); + } + return false; + }, + select: undefined, + ...params + }); return useQuery({ - ...dashboardQueryKeys.dashboardGetDashboard(id!, versionNumber), - queryFn: () => queryFn(id!, versionNumber), - enabled: false, //we made this false because we want to be explicit about the fact that we fetch the dashboard server side - select: params?.select, - ...params + ...dashboardQueryKeys.dashboardGetDashboard(id!, selectedVersionNumber), + queryFn: () => queryFn(id!, selectedVersionNumber), + enabled: !!latestVersionNumber && isFetchedInitial && !isErrorInitial, + select: params?.select }); }; @@ -72,7 +88,6 @@ export const useSaveDashboard = (params?: { updateOnSave?: boolean }) => { const updateOnSave = params?.updateOnSave || false; const queryClient = useQueryClient(); const setOriginalDashboard = useOriginalDashboardStore((x) => x.setOriginalDashboard); - const versionNumber = useGetDashboardVersionNumber(); return useMutation({ mutationFn: dashboardsUpdateDashboard, @@ -89,13 +104,6 @@ export const useSaveDashboard = (params?: { updateOnSave?: boolean }) => { .queryKey, data ); - //We need to update BOTH the versioned and the non-versioned metric for version updates to keep the latest up to date - if (variables.update_version || variables.restore_to_version) { - queryClient.setQueryData( - dashboardQueryKeys.dashboardGetDashboard(data.dashboard.id, undefined).queryKey, - data - ); - } setOriginalDashboard(data.dashboard); } } @@ -110,7 +118,7 @@ export const useUpdateDashboard = (params?: { const { updateOnSave = false, updateVersion = false, saveToServer = false } = params || {}; const queryClient = useQueryClient(); const { mutateAsync: saveDashboard } = useSaveDashboard({ updateOnSave }); - const versionNumber = useGetDashboardVersionNumber(); + const { latestVersionNumber } = useGetDashboardVersionNumber(); const getOriginalDashboard = useOriginalDashboardStore((x) => x.getOriginalDashboard); const mutationFn = useMemoizedFn( @@ -133,8 +141,7 @@ export const useUpdateDashboard = (params?: { }); const queryKey = dashboardQueryKeys.dashboardGetDashboard( variables.id, - versionNumber - // updatedDashboard.version_number + latestVersionNumber ).queryKey; queryClient.setQueryData(queryKey, (previousData) => { @@ -152,7 +159,7 @@ export const useUpdateDashboardConfig = () => { updateVersion: false }); const queryClient = useQueryClient(); - const versionNumber = useGetDashboardVersionNumber(); + const { latestVersionNumber } = useGetDashboardVersionNumber(); const method = useMemoizedFn( async ({ @@ -161,7 +168,7 @@ export const useUpdateDashboardConfig = () => { }: Partial & { dashboardId: string; }) => { - const options = dashboardQueryKeys.dashboardGetDashboard(dashboardId, versionNumber); + const options = dashboardQueryKeys.dashboardGetDashboard(dashboardId, latestVersionNumber); const previousDashboard = queryClient.getQueryData(options.queryKey); const previousConfig = previousDashboard?.dashboard?.config; if (previousConfig) { @@ -304,13 +311,13 @@ export const useRemoveDashboardFromCollection = () => { export const useShareDashboard = () => { const queryClient = useQueryClient(); - const versionNumber = useGetDashboardVersionNumber(); + const { latestVersionNumber } = useGetDashboardVersionNumber(); return useMutation({ mutationFn: shareDashboard, onMutate: (variables) => { const queryKey = dashboardQueryKeys.dashboardGetDashboard( variables.id, - versionNumber + latestVersionNumber ).queryKey; queryClient.setQueryData(queryKey, (previousData) => { return create(previousData!, (draft) => { @@ -323,7 +330,8 @@ export const useShareDashboard = () => { }, onSuccess: (data) => { queryClient.setQueryData( - dashboardQueryKeys.dashboardGetDashboard(data.dashboard.id, versionNumber).queryKey, + dashboardQueryKeys.dashboardGetDashboard(data.dashboard.id, data.dashboard.version_number) + .queryKey, data ); } @@ -332,13 +340,13 @@ export const useShareDashboard = () => { export const useUnshareDashboard = () => { const queryClient = useQueryClient(); - const versionNumber = useGetDashboardVersionNumber(); + const { latestVersionNumber } = useGetDashboardVersionNumber(); return useMutation({ mutationFn: unshareDashboard, onMutate: (variables) => { const queryKey = dashboardQueryKeys.dashboardGetDashboard( variables.id, - versionNumber + latestVersionNumber ).queryKey; queryClient.setQueryData(queryKey, (previousData) => { return create(previousData!, (draft) => { @@ -349,7 +357,8 @@ export const useUnshareDashboard = () => { }, onSuccess: (data) => { queryClient.setQueryData( - dashboardQueryKeys.dashboardGetDashboard(data.dashboard.id, versionNumber).queryKey, + dashboardQueryKeys.dashboardGetDashboard(data.dashboard.id, data.dashboard.version_number) + .queryKey, data ); } @@ -358,11 +367,11 @@ export const useUnshareDashboard = () => { export const useUpdateDashboardShare = () => { const queryClient = useQueryClient(); - const versionNumber = useGetDashboardVersionNumber(); + const { latestVersionNumber } = useGetDashboardVersionNumber(); return useMutation({ mutationFn: updateDashboardShare, onMutate: ({ id, params }) => { - const queryKey = dashboardQueryKeys.dashboardGetDashboard(id, versionNumber).queryKey; + const queryKey = dashboardQueryKeys.dashboardGetDashboard(id, latestVersionNumber).queryKey; queryClient.setQueryData(queryKey, (previousData) => { return create(previousData!, (draft) => { draft.individual_permissions = @@ -429,10 +438,6 @@ export const useAddAndRemoveMetricsFromDashboard = () => { mutationFn: addAndRemoveMetrics, onSuccess: (data, variables) => { if (data) { - queryClient.setQueryData( - dashboardQueryKeys.dashboardGetDashboard(data.dashboard.id, undefined).queryKey, - data - ); queryClient.setQueryData( dashboardQueryKeys.dashboardGetDashboard(data.dashboard.id, data.dashboard.version_number) .queryKey, @@ -486,10 +491,6 @@ export const useAddMetricsToDashboard = () => { }, onSuccess: (data) => { if (data) { - queryClient.setQueryData( - dashboardQueryKeys.dashboardGetDashboard(data.dashboard.id, undefined).queryKey, - data - ); queryClient.setQueryData( dashboardQueryKeys.dashboardGetDashboard(data.dashboard.id, data.dashboard.version_number) .queryKey, @@ -554,10 +555,7 @@ export const useRemoveMetricsFromDashboard = () => { dashboardResponse.dashboard.id, dashboardResponse.dashboard.version_number ); - const nonVersionedOptions = dashboardQueryKeys.dashboardGetDashboard( - dashboardResponse.dashboard.id, - undefined - ); + const newConfig = removeMetricFromDashboardConfig( metricIds, dashboardResponse.dashboard.config @@ -568,11 +566,6 @@ export const useRemoveMetricsFromDashboard = () => { draft.dashboard.config = newConfig; }); }); - queryClient.setQueryData(nonVersionedOptions.queryKey, (currentDashboard) => { - return create(currentDashboard!, (draft) => { - draft.dashboard.config = newConfig; - }); - }); const data = await dashboardsUpdateDashboard({ id: dashboardId, @@ -580,7 +573,10 @@ export const useRemoveMetricsFromDashboard = () => { }); queryClient.setQueryData( - dashboardQueryKeys.dashboardGetDashboard(data.dashboard.id, undefined).queryKey, + dashboardQueryKeys.dashboardGetDashboard( + data.dashboard.id, + data.dashboard.version_number + ).queryKey, data ); queryClient.setQueryData( diff --git a/web/src/api/buster_rest/dashboards/queryServerRequests.ts b/web/src/api/buster_rest/dashboards/queryServerRequests.ts index b6978dfaa..754f46081 100644 --- a/web/src/api/buster_rest/dashboards/queryServerRequests.ts +++ b/web/src/api/buster_rest/dashboards/queryServerRequests.ts @@ -9,7 +9,7 @@ export const prefetchGetDashboard = async ( const queryClient = queryClientProp || new QueryClient(); await queryClient.prefetchQuery({ - ...dashboardQueryKeys.dashboardGetDashboard(params.id, params.version_number), + ...dashboardQueryKeys.dashboardGetDashboard(params.id, params.version_number || null), queryFn: async () => { return await getDashboard_server(params); } diff --git a/web/src/api/buster_rest/metrics/metricQueryHelpers.ts b/web/src/api/buster_rest/metrics/metricQueryHelpers.ts index 804cde968..d54c78fe4 100644 --- a/web/src/api/buster_rest/metrics/metricQueryHelpers.ts +++ b/web/src/api/buster_rest/metrics/metricQueryHelpers.ts @@ -4,7 +4,6 @@ import { Query, useQueryClient } from '@tanstack/react-query'; import { IBusterMetric } from '@/api/asset_interfaces/metric'; import { metricsQueryKeys } from '@/api/query_keys/metric'; import last from 'lodash/last'; -import { INFINITY } from 'chart.js/helpers'; import { useMemoizedFn } from '@/hooks'; import { RustApiError } from '../errors'; @@ -48,6 +47,22 @@ const filterMetricPredicate = ((query) => { ); }); +const getLatestVersionNumber = (queries: [readonly unknown[], IBusterMetric | undefined][]) => { + let latestVersion = -Infinity; + + for (const [queryKey, data] of queries) { + if (data && typeof data === 'object' && 'versions' in data) { + const lastVersion = last(data.versions); + const version = Number(lastVersion?.version_number); + if (!isNaN(version)) { + latestVersion = Math.max(latestVersion, version, data.version_number); + } + } + } + + return latestVersion; +}; + const useGetLatestMetricVersion = ({ metricId }: { metricId: string }) => { const queryClient = useQueryClient(); @@ -61,18 +76,7 @@ const useGetLatestMetricVersion = ({ metricId }: { metricId: string }) => { }); const latestVersion = useMemo(() => { - let maxVersion = -Infinity; - - // Single pass: filter and find max version - for (const [queryKey, data] of queries) { - if (data && typeof data === 'object' && 'versions' in data) { - const lastVersion = last(data.versions); - const version = Number(lastVersion?.version_number); - maxVersion = Math.max(maxVersion, version, data.version_number); - } - } - - return maxVersion === -Infinity ? null : maxVersion; + return getLatestVersionNumber(queries); }, [queries.length]); return latestVersion; @@ -88,19 +92,7 @@ export const useGetLatestMetricVersionNumber = () => { predicate: filterMetricPredicate }); - let latestVersion = -INFINITY; - - for (const [queryKey, data] of queries) { - if (data && typeof data === 'object' && 'versions' in data) { - const lastVersion = last(data.versions); - const version = Number(lastVersion?.version_number); - if (!isNaN(version)) { - latestVersion = Math.max(latestVersion, version, data.version_number); - } - } - } - - return latestVersion; + return getLatestVersionNumber(queries); }); return method; diff --git a/web/src/api/buster_rest/metrics/queryReqestsServer.ts b/web/src/api/buster_rest/metrics/queryReqestsServer.ts index b509d80dc..213a03d8e 100644 --- a/web/src/api/buster_rest/metrics/queryReqestsServer.ts +++ b/web/src/api/buster_rest/metrics/queryReqestsServer.ts @@ -13,7 +13,7 @@ export const prefetchGetMetric = async ( const queryClient = queryClientProp || new QueryClient(); await queryClient.prefetchQuery({ - ...metricsQueryKeys.metricsGetMetric(params.id, params.version_number), + ...metricsQueryKeys.metricsGetMetric(params.id, params.version_number || null), queryFn: async () => { const result = await getMetric_server(params); return upgradeMetricToIMetric(result, null); diff --git a/web/src/api/buster_rest/metrics/updateMetricQueryRequests.ts b/web/src/api/buster_rest/metrics/updateMetricQueryRequests.ts index 24764ee20..e6cb3ead3 100644 --- a/web/src/api/buster_rest/metrics/updateMetricQueryRequests.ts +++ b/web/src/api/buster_rest/metrics/updateMetricQueryRequests.ts @@ -27,7 +27,7 @@ export const useSaveMetric = (params?: { updateOnSave?: boolean }) => { const updateOnSave = params?.updateOnSave || false; const queryClient = useQueryClient(); const setOriginalMetric = useOriginalMetricStore((x) => x.setOriginalMetric); - const { latestVersionNumber, selectedVersionNumber } = useGetMetricVersionNumber(); + const { latestVersionNumber } = useGetMetricVersionNumber(); return useMutation({ mutationFn: updateMetric, @@ -37,7 +37,7 @@ export const useSaveMetric = (params?: { updateOnSave?: boolean }) => { //set the current metric to the previous it is being restored to if (isRestoringVersion) { const oldMetric = queryClient.getQueryData( - metricsQueryKeys.metricsGetMetric(id, selectedVersionNumber).queryKey + metricsQueryKeys.metricsGetMetric(id, latestVersionNumber).queryKey ); const newMetric = queryClient.getQueryData( metricsQueryKeys.metricsGetMetric(id, restore_to_version).queryKey @@ -60,7 +60,7 @@ export const useSaveMetric = (params?: { updateOnSave?: boolean }) => { if (isUpdatingVersion) { const metric = queryClient.getQueryData( - metricsQueryKeys.metricsGetMetric(id, selectedVersionNumber).queryKey + metricsQueryKeys.metricsGetMetric(id, latestVersionNumber).queryKey ); if (!metric) return; const metricVersionNumber = metric?.version_number; diff --git a/web/src/api/query_keys/dashboard.ts b/web/src/api/query_keys/dashboard.ts index 4bcda3679..3b5d3aebd 100644 --- a/web/src/api/query_keys/dashboard.ts +++ b/web/src/api/query_keys/dashboard.ts @@ -15,9 +15,9 @@ const dashboardGetList = ( initialDataUpdatedAt: 0 }); -const dashboardGetDashboard = (dashboardId: string, version_number: number | undefined) => +const dashboardGetDashboard = (dashboardId: string, version_number: number | null) => queryOptions({ - queryKey: ['dashboard', 'get', dashboardId, version_number || 'latest'] as const, + queryKey: ['dashboard', 'get', dashboardId, version_number || 'INITIAL'] as const, staleTime: 10 * 1000 }); diff --git a/web/src/components/features/sidebars/SidebarPrimary.tsx b/web/src/components/features/sidebars/SidebarPrimary.tsx index aa077b824..413c7f5bb 100644 --- a/web/src/components/features/sidebars/SidebarPrimary.tsx +++ b/web/src/components/features/sidebars/SidebarPrimary.tsx @@ -82,12 +82,12 @@ const adminTools: ISidebarGroup = { route: BusterRoutes.APP_LOGS, id: BusterRoutes.APP_LOGS }, - { - label: 'Terms & Definitions', - icon: , - route: BusterRoutes.APP_TERMS, - id: BusterRoutes.APP_TERMS - }, + // { + // label: 'Terms & Definitions', + // icon: , + // route: BusterRoutes.APP_TERMS, + // id: BusterRoutes.APP_TERMS + // }, { label: 'Datasets', icon: , diff --git a/web/src/components/ui/popup/PopupContainer.tsx b/web/src/components/ui/popup/PopupContainer.tsx index 938802a8c..6ab7f7f61 100644 --- a/web/src/components/ui/popup/PopupContainer.tsx +++ b/web/src/components/ui/popup/PopupContainer.tsx @@ -11,7 +11,10 @@ export const PopupContainer: React.FC<{ {show && ( { }); } else if (type === 'dashboard') { await queryClient.invalidateQueries({ - queryKey: queryKeys.dashboardGetDashboard(assetId, undefined).queryKey + queryKey: queryKeys.dashboardGetDashboard(assetId, null).queryKey }); } else if (type === 'collection') { await queryClient.invalidateQueries({ diff --git a/web/src/context/Dashboards/useGetDashboardMemoized.tsx b/web/src/context/Dashboards/useGetDashboardMemoized.tsx index db8870af0..fd46259b9 100644 --- a/web/src/context/Dashboards/useGetDashboardMemoized.tsx +++ b/web/src/context/Dashboards/useGetDashboardMemoized.tsx @@ -5,7 +5,7 @@ import { useQueryClient } from '@tanstack/react-query'; export const useGetDashboardMemoized = () => { const queryClient = useQueryClient(); const getDashboardMemoized = useMemoizedFn((dashboardId: string, versionNumber?: number) => { - const options = dashboardQueryKeys.dashboardGetDashboard(dashboardId, versionNumber); + const options = dashboardQueryKeys.dashboardGetDashboard(dashboardId, versionNumber || null); const data = queryClient.getQueryData(options.queryKey); return data; }); diff --git a/web/src/context/Dashboards/useIsDashboardChanged.tsx b/web/src/context/Dashboards/useIsDashboardChanged.tsx index a5d306fd4..75d97b7a3 100644 --- a/web/src/context/Dashboards/useIsDashboardChanged.tsx +++ b/web/src/context/Dashboards/useIsDashboardChanged.tsx @@ -7,16 +7,11 @@ import { dashboardQueryKeys } from '@/api/query_keys/dashboard'; import { compareObjectsByKeys } from '@/lib/objects'; import { useMemo } from 'react'; import { create } from 'mutative'; -import last from 'lodash/last'; + export const useIsDashboardChanged = ({ dashboardId }: { dashboardId: string }) => { const queryClient = useQueryClient(); const originalDashboard = useOriginalDashboardStore((x) => x.getOriginalDashboard(dashboardId)); - // const { data: latestVersionNumber } = useGetDashboard( - // { id: dashboardId }, - // { select: (x) => last(x.versions)?.version_number } - // ); - const { data: currentDashboard, refetch: refetchCurrentDashboard } = useGetDashboard( { id: dashboardId, versionNumber: undefined }, { @@ -32,7 +27,7 @@ export const useIsDashboardChanged = ({ dashboardId }: { dashboardId: string }) const onResetDashboardToOriginal = useMemoizedFn(() => { const options = dashboardQueryKeys.dashboardGetDashboard( dashboardId, - originalDashboard?.version_number + originalDashboard?.version_number || null ); const currentDashboard = queryClient.getQueryData(options.queryKey); if (originalDashboard && currentDashboard) { diff --git a/web/src/context/Metrics/useGetMetricMemoized.ts b/web/src/context/Metrics/useGetMetricMemoized.ts index 5ef7ad2ef..5668386f8 100644 --- a/web/src/context/Metrics/useGetMetricMemoized.ts +++ b/web/src/context/Metrics/useGetMetricMemoized.ts @@ -1,4 +1,4 @@ -import type { IBusterMetric } from '@/api/asset_interfaces/metric'; +import type { IBusterMetric, IBusterMetricData } from '@/api/asset_interfaces/metric'; import { useGetMetricVersionNumber } from '@/api/buster_rest/metrics'; import { queryKeys } from '@/api/query_keys'; import { useMemoizedFn } from '@/hooks'; @@ -7,7 +7,7 @@ import { useQueryClient } from '@tanstack/react-query'; export const useGetMetricMemoized = () => { const queryClient = useQueryClient(); - const { selectedVersionNumber } = useGetMetricVersionNumber({}); + const { selectedVersionNumber } = useGetMetricVersionNumber(); const getMetricMemoized = useMemoizedFn( (metricId: string, versionNumberProp?: number): IBusterMetric => { const options = queryKeys.metricsGetMetric( @@ -20,3 +20,19 @@ export const useGetMetricMemoized = () => { ); return getMetricMemoized; }; + +export const useGetMetricDataMemoized = () => { + const queryClient = useQueryClient(); + const { selectedVersionNumber, latestVersionNumber } = useGetMetricVersionNumber(); + const getMetricDataMemoized = useMemoizedFn( + (metricId: string, versionNumberProp?: number): IBusterMetricData | undefined => { + const options = queryKeys.metricsGetData( + metricId, + versionNumberProp || selectedVersionNumber || latestVersionNumber! + ); + const data = queryClient.getQueryData(options.queryKey); + return data; + } + ); + return getMetricDataMemoized; +}; diff --git a/web/src/context/Metrics/useIsMetricChanged.tsx b/web/src/context/Metrics/useIsMetricChanged.tsx index 0f14a6a8a..e98cd22ab 100644 --- a/web/src/context/Metrics/useIsMetricChanged.tsx +++ b/web/src/context/Metrics/useIsMetricChanged.tsx @@ -30,7 +30,10 @@ export const useIsMetricChanged = ({ metricId }: { metricId: string }) => { ); const onResetMetricToOriginal = useMemoizedFn(() => { - const options = metricsQueryKeys.metricsGetMetric(metricId, originalMetric?.version_number); + const options = metricsQueryKeys.metricsGetMetric( + metricId, + originalMetric?.version_number || null + ); if (originalMetric) { queryClient.setQueryData(options.queryKey, originalMetric); } diff --git a/web/src/controllers/DashboardController/DashboardViewDashboardController/DashboardContentController/DashboardContentController.tsx b/web/src/controllers/DashboardController/DashboardViewDashboardController/DashboardContentController/DashboardContentController.tsx index 80f81ed53..a9d70c7f1 100644 --- a/web/src/controllers/DashboardController/DashboardViewDashboardController/DashboardContentController/DashboardContentController.tsx +++ b/web/src/controllers/DashboardController/DashboardViewDashboardController/DashboardContentController/DashboardContentController.tsx @@ -17,7 +17,7 @@ import type { BusterDashboardResponse, DashboardConfig } from '@/api/asset_interfaces'; -import { DashboardEmptyState } from './DashboardEmptyState'; +import { DashboardEmptyState, DashboardNoContentReadOnly } from './DashboardEmptyState'; import { type useUpdateDashboardConfig } from '@/api/buster_rest/dashboards'; import last from 'lodash/last'; @@ -136,7 +136,9 @@ export const DashboardContentController: React.FC<{ ) : !readOnly ? ( - ) : null} + ) : ( + + )} ); } diff --git a/web/src/controllers/DashboardController/DashboardViewDashboardController/DashboardContentController/DashboardEmptyState.tsx b/web/src/controllers/DashboardController/DashboardViewDashboardController/DashboardContentController/DashboardEmptyState.tsx index 70516d770..2f52ca98d 100644 --- a/web/src/controllers/DashboardController/DashboardViewDashboardController/DashboardContentController/DashboardEmptyState.tsx +++ b/web/src/controllers/DashboardController/DashboardViewDashboardController/DashboardContentController/DashboardEmptyState.tsx @@ -1,6 +1,7 @@ import { Button } from '@/components/ui/buttons'; import { Plus } from '@/components/ui/icons'; import React from 'react'; +import { Text } from '@/components/ui/typography'; export const DashboardEmptyState: React.FC<{ onOpenAddContentModal: () => void; @@ -14,3 +15,12 @@ export const DashboardEmptyState: React.FC<{ ); }); DashboardEmptyState.displayName = 'DashboardEmptyState'; + +export const DashboardNoContentReadOnly: React.FC = React.memo(() => { + return ( +
+ No items added to dashboard +
+ ); +}); +DashboardNoContentReadOnly.displayName = 'DashboardNoContentReadOnly'; diff --git a/web/src/controllers/MetricController/MetricViewChart/MetricViewChartHeader.tsx b/web/src/controllers/MetricController/MetricViewChart/MetricViewChartHeader.tsx index 03deca089..6723fd4b6 100644 --- a/web/src/controllers/MetricController/MetricViewChart/MetricViewChartHeader.tsx +++ b/web/src/controllers/MetricController/MetricViewChart/MetricViewChartHeader.tsx @@ -28,6 +28,7 @@ export const MetricViewChartHeader: React.FC<{ level={4} readOnly={readOnly} inputClassName="h-auto!" + placeholder="New chart" onChange={onSetTitle}> {name} diff --git a/web/src/controllers/MetricController/MetricViewResults/useMetricRunSQL/useMetricRunSQL.ts b/web/src/controllers/MetricController/MetricViewResults/useMetricRunSQL/useMetricRunSQL.ts index a379727b7..c32400241 100644 --- a/web/src/controllers/MetricController/MetricViewResults/useMetricRunSQL/useMetricRunSQL.ts +++ b/web/src/controllers/MetricController/MetricViewResults/useMetricRunSQL/useMetricRunSQL.ts @@ -11,13 +11,14 @@ import { useQueryClient } from '@tanstack/react-query'; import { useRef } from 'react'; import { didColumnDataChange, simplifyChatConfigForSQLChange } from './helpers'; import { useRunSQL as useRunSQLQuery } from '@/api/buster_rest'; -import { useUpdateMetric } from '@/api/buster_rest/metrics'; -import { useGetMetricMemoized } from '@/context/Metrics'; +import { useGetLatestMetricVersionNumber, useUpdateMetric } from '@/api/buster_rest/metrics'; +import { useGetMetricDataMemoized, useGetMetricMemoized } from '@/context/Metrics'; import { timeout } from '@/lib'; export const useMetricRunSQL = () => { const queryClient = useQueryClient(); const getMetricMemoized = useGetMetricMemoized(); + const getMetricDataMemoized = useGetMetricDataMemoized(); const { mutateAsync: stageMetric } = useUpdateMetric({ updateVersion: false, saveToServer: false, @@ -38,13 +39,7 @@ export const useMetricRunSQL = () => { isPending: isRunningSQL } = useRunSQLQuery(); const { openSuccessNotification } = useBusterNotifications(); - - const getDataByMetricIdMemoized = useMemoizedFn( - (metricId: string): IBusterMetricData | undefined => { - const options = queryKeys.metricsGetData(metricId); - return queryClient.getQueryData(options.queryKey); - } - ); + const getLatestMetricVersion = useGetLatestMetricVersionNumber(); const originalConfigs = useRef<{ chartConfig: IBusterMetricChartConfig; @@ -64,8 +59,9 @@ export const useMetricRunSQL = () => { data_metadata: BusterMetricData['data_metadata']; isDataFromRerun: boolean; }) => { - const options = queryKeys.metricsGetData(metricId); - const currentData = getDataByMetricIdMemoized(metricId); + const latestVersionNumber = getLatestMetricVersion(metricId); + const options = queryKeys.metricsGetData(metricId, latestVersionNumber); + const currentData = getMetricDataMemoized(metricId, latestVersionNumber); if (!currentData) return; const setter = isDataFromRerun ? 'dataFromRerun' : 'data'; @@ -82,7 +78,7 @@ export const useMetricRunSQL = () => { if (metricId) { const { data, data_metadata } = d; const metricMessage = getMetricMemoized(metricId); - const currentMessageData = getDataByMetricIdMemoized(metricId); + const currentMessageData = getMetricDataMemoized(metricId); if (!originalConfigs.current) { originalConfigs.current = { chartConfig: metricMessage?.chart_config!, @@ -185,7 +181,8 @@ export const useMetricRunSQL = () => { }); await timeout(50); - const currentData = getDataByMetricIdMemoized(metricId); + const latestVersionNumber = getLatestMetricVersion(metricId); + const currentData = getMetricDataMemoized(metricId, latestVersionNumber); if (currentData?.data_metadata && currentData?.dataFromRerun) { onSetDataForMetric({ diff --git a/web/src/layouts/AppAssetCheckLayout/useGetAsset.tsx b/web/src/layouts/AppAssetCheckLayout/useGetAsset.tsx index e04ccf6a9..d95ab687e 100644 --- a/web/src/layouts/AppAssetCheckLayout/useGetAsset.tsx +++ b/web/src/layouts/AppAssetCheckLayout/useGetAsset.tsx @@ -21,6 +21,7 @@ interface AssetAccess { hasAccess: boolean; passwordRequired: boolean; isPublic: boolean; + isDeleted: boolean; } interface AssetQueryResult { @@ -32,14 +33,18 @@ interface AssetQueryResult { const getAssetAccess = (error: RustApiError | null): AssetAccess => { if (!error) { - return { hasAccess: true, passwordRequired: false, isPublic: false }; + return { hasAccess: true, passwordRequired: false, isPublic: false, isDeleted: false }; } if (error.status === 418) { - return { hasAccess: false, passwordRequired: true, isPublic: true }; + return { hasAccess: false, passwordRequired: true, isPublic: true, isDeleted: false }; } - return { hasAccess: false, passwordRequired: false, isPublic: false }; + if (error.status === 410) { + return { hasAccess: false, passwordRequired: false, isPublic: false, isDeleted: true }; + } + + return { hasAccess: false, passwordRequired: false, isPublic: false, isDeleted: false }; }; const useVersionNumber = (props: UseGetAssetProps) => { diff --git a/web/src/layouts/ChatLayout/ChatContainer/ChatHeader/ChatHeaderTitle.tsx b/web/src/layouts/ChatLayout/ChatContainer/ChatHeader/ChatHeaderTitle.tsx index b09d13bcd..3d8ae445d 100644 --- a/web/src/layouts/ChatLayout/ChatContainer/ChatHeader/ChatHeaderTitle.tsx +++ b/web/src/layouts/ChatLayout/ChatContainer/ChatHeader/ChatHeaderTitle.tsx @@ -31,6 +31,7 @@ export const ChatHeaderTitle: React.FC<{ className="flex w-full items-center overflow-hidden">