diff --git a/web/src/api/buster_rest/metrics/queryRequests.ts b/web/src/api/buster_rest/metrics/queryRequests.ts index 913d1b7c9..e2d64116f 100644 --- a/web/src/api/buster_rest/metrics/queryRequests.ts +++ b/web/src/api/buster_rest/metrics/queryRequests.ts @@ -1,6 +1,6 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { QueryClient } from '@tanstack/react-query'; -import { useDebounceFn, useMemoizedFn, useMount, useUnmount } from '@/hooks'; +import { useDebounceFn, useMemoizedFn } from '@/hooks'; import { deleteMetrics, duplicateMetric, @@ -26,21 +26,32 @@ import { useAddAssetToCollection, useRemoveAssetFromCollection } from '../collections/queryRequests'; -import debounce from 'lodash/debounce'; +import { useSearchParams } from 'next/navigation'; +/** + * This is a hook that will use the version number from the URL params if it exists. + */ export const useGetMetric = ( - { id, version_number }: { id: string | undefined; version_number?: number }, + { id, version_number: version_number_prop }: { id: string | undefined; version_number?: number }, select?: (data: IBusterMetric) => TData ) => { + const searchParams = useSearchParams(); + const queryVersionNumber = searchParams.get('metric_version_number'); const getAssetPassword = useBusterAssetsContextSelector((x) => x.getAssetPassword); const setAssetPasswordError = useBusterAssetsContextSelector((x) => x.setAssetPasswordError); const { password } = getAssetPassword(id!); const queryClient = useQueryClient(); - const options = metricsQueryKeys.metricsGetMetric(id!); + const version_number = useMemo(() => { + return version_number_prop || queryVersionNumber ? parseInt(queryVersionNumber!) : undefined; + }, [version_number_prop, queryVersionNumber]); + + const options = useMemo(() => { + return metricsQueryKeys.metricsGetMetric(id!, version_number); + }, [id, version_number]); const queryFn = useMemoizedFn(async () => { - const result = await getMetric({ id: id!, password }); + const result = await getMetric({ id: id!, password, version_number }); const oldMetric = queryClient.getQueryData(options.queryKey); return upgradeMetricToIMetric(result, oldMetric || null); }); @@ -65,7 +76,7 @@ export const prefetchGetMetric = async ( ) => { const queryClient = queryClientProp || new QueryClient(); await queryClient.prefetchQuery({ - ...metricsQueryKeys.metricsGetMetric(params.id), + ...metricsQueryKeys.metricsGetMetric(params.id, params.version_number), queryFn: async () => { const result = await getMetric_server(params); return upgradeMetricToIMetric(result, null); @@ -112,33 +123,44 @@ export const prefetchGetMetricsList = async ( return queryClient; }; +/** + * This is a hook that will use the version number from the URL params if it exists. + */ export const useGetMetricData = ({ id, - version_number + version_number: version_number_prop }: { id: string; version_number?: number; }) => { + const searchParams = useSearchParams(); + const queryVersionNumber = searchParams.get('metric_version_number'); + + const version_number = useMemo(() => { + return version_number_prop || queryVersionNumber ? parseInt(queryVersionNumber!) : undefined; + }, [version_number_prop, queryVersionNumber]); + const queryFn = useMemoizedFn(() => { return getMetricData({ id, version_number }); }); + return useQuery({ - ...metricsQueryKeys.metricsGetData(id), + ...metricsQueryKeys.metricsGetData(id, version_number), queryFn, enabled: !!id }); }; export const prefetchGetMetricDataClient = async ( - { id }: { id: string }, + { id, version_number }: { id: string; version_number?: number }, queryClient: QueryClient ) => { - const options = metricsQueryKeys.metricsGetData(id); + const options = metricsQueryKeys.metricsGetData(id, version_number); const existingData = queryClient.getQueryData(options.queryKey); if (!existingData) { await queryClient.prefetchQuery({ ...options, - queryFn: () => getMetricData({ id }) + queryFn: () => getMetricData({ id, version_number }) }); } }; diff --git a/web/src/api/query_keys/metric.ts b/web/src/api/query_keys/metric.ts index d9623e231..124edfb2f 100644 --- a/web/src/api/query_keys/metric.ts +++ b/web/src/api/query_keys/metric.ts @@ -19,9 +19,9 @@ export const metricsGetList = (filters?: Parameters[0]) => staleTime: 10 * 1000 }); -export const metricsGetData = (id: string) => +export const metricsGetData = (id: string, version_number?: number) => queryOptions({ - queryKey: ['metrics', 'data', id] as const, + queryKey: ['metrics', 'data', id, version_number] as const, staleTime: 3 * 60 * 60 * 1000 // 3 hours, }); diff --git a/web/src/app/app/(primary_layout)/(chat_experience)/metrics/[metricId]/page.tsx b/web/src/app/app/(primary_layout)/(chat_experience)/metrics/[metricId]/page.tsx index bd6112f25..610151ac4 100644 --- a/web/src/app/app/(primary_layout)/(chat_experience)/metrics/[metricId]/page.tsx +++ b/web/src/app/app/(primary_layout)/(chat_experience)/metrics/[metricId]/page.tsx @@ -2,7 +2,7 @@ import { MetricController } from '@/controllers/MetricController'; import { AppAssetCheckLayout } from '@/layouts/AppAssetCheckLayout'; export default async function MetricPage(props: { params: Promise<{ metricId: string }> }) { - const params = await props.params; + const [params] = await Promise.all([props.params]); const { metricId } = params; return ( diff --git a/web/src/app/embed/metrics/[metricId]/page.tsx b/web/src/app/embed/metrics/[metricId]/page.tsx index db53e8386..4ea7c2cf6 100644 --- a/web/src/app/embed/metrics/[metricId]/page.tsx +++ b/web/src/app/embed/metrics/[metricId]/page.tsx @@ -4,9 +4,19 @@ import { useGetMetric } from '@/api/buster_rest/metrics'; import { CircleSpinnerLoaderContainer } from '@/components/ui/loaders'; import { MetricViewChart } from '@/controllers/MetricController/MetricViewChart/MetricViewChart'; -export default function EmbedMetricsPage({ params }: { params: { metricId: string } }) { +export default function EmbedMetricsPage({ + params, + searchParams +}: { + params: { metricId: string }; + searchParams: { version_number?: string }; +}) { const { metricId } = params; - const { isFetched } = useGetMetric({ id: metricId }); + const { version_number } = searchParams; + const { isFetched, error, ...rest } = useGetMetric({ + id: metricId, + version_number: version_number ? parseInt(version_number) : undefined + }); if (!isFetched) { return ; diff --git a/web/src/components/ui/card/StatusCard.tsx b/web/src/components/ui/card/StatusCard.tsx index be3c4afad..a1a3604d5 100644 --- a/web/src/components/ui/card/StatusCard.tsx +++ b/web/src/components/ui/card/StatusCard.tsx @@ -8,8 +8,11 @@ const statusVariants = cva('shadow p-3 rounded', { variants: { variant: { danger: 'bg-danger-foreground text-white', - default: 'bg-background text-foreground' + default: 'bg-background text-foreground border' } + }, + defaultVariants: { + variant: 'default' } }); @@ -21,7 +24,7 @@ export const StatusCard: React.FC< onClose?: () => void; extra?: React.ReactNode; } & VariantProps -> = ({ message, title, variant, className, onClose, extra }) => { +> = ({ message, title, variant = 'default', className, onClose, extra }) => { return (
{title && {title}} diff --git a/web/src/controllers/DashboardController/DashboardViewDashboardController/DashboardContentController/DashboardMetricItem/useDashboardMetric.ts b/web/src/controllers/DashboardController/DashboardViewDashboardController/DashboardContentController/DashboardMetricItem/useDashboardMetric.ts index ce2d71e47..5bfc24d1e 100644 --- a/web/src/controllers/DashboardController/DashboardViewDashboardController/DashboardContentController/DashboardMetricItem/useDashboardMetric.ts +++ b/web/src/controllers/DashboardController/DashboardViewDashboardController/DashboardContentController/DashboardMetricItem/useDashboardMetric.ts @@ -4,7 +4,17 @@ import { useInViewport } from '@/hooks'; import { useGetMetric, useGetMetricData } from '@/api/buster_rest/metrics'; export const useDashboardMetric = ({ metricId }: { metricId: string }) => { - const { data: metric, isFetched: isMetricFetched } = useGetMetric({ id: metricId }); + const { data: metric, isFetched: isMetricFetched } = useGetMetric( + { id: metricId }, + ({ name, description, time_frame, permission, evaluation_score, evaluation_summary }) => ({ + name, + description, + time_frame, + permission, + evaluation_score, + evaluation_summary + }) + ); const { data: metricData, isFetched: isFetchedMetricData, diff --git a/web/src/controllers/MetricController/MetricController.tsx b/web/src/controllers/MetricController/MetricController.tsx index 1195b4736..86f4d7ebc 100644 --- a/web/src/controllers/MetricController/MetricController.tsx +++ b/web/src/controllers/MetricController/MetricController.tsx @@ -8,25 +8,34 @@ import { import { MetricViewComponents } from './config'; import { FileIndeterminateLoader } from '@/components/features/FileIndeterminateLoader'; import { useGetMetric, useGetMetricData } from '@/api/buster_rest/metrics'; +import { MetricViewError } from './MetricViewError'; export const MetricController: React.FC<{ metricId: string; }> = React.memo(({ metricId }) => { - const { isFetched: isMetricFetched } = useGetMetric({ id: metricId }); + const { isFetched: isMetricFetched, error: metricError } = useGetMetric({ id: metricId }); const { isFetched: isMetricDataFetched } = useGetMetricData({ id: metricId }); const selectedFileView = useChatLayoutContextSelector((x) => x.selectedFileView) || 'chart'; const showLoader = !isMetricFetched || !isMetricDataFetched; - const Component = - isMetricFetched && selectedFileView in MetricViewComponents - ? MetricViewComponents[selectedFileView as MetricFileView] - : () => <>; + const Component = React.useMemo(() => { + if (metricError) { + return ; + } + + if (isMetricFetched && selectedFileView in MetricViewComponents) { + const Component = MetricViewComponents[selectedFileView as MetricFileView]; + return ; + } + + return null; + }, [isMetricFetched, selectedFileView, metricError, metricId]); return ( <> {showLoader && } - {Component && } + {Component} ); }); diff --git a/web/src/controllers/MetricController/MetricViewChart/MetricEditController/MetricStylingApp/MetricStylingApp.tsx b/web/src/controllers/MetricController/MetricViewChart/MetricEditController/MetricStylingApp/MetricStylingApp.tsx index 344520cc5..ec8bf6745 100644 --- a/web/src/controllers/MetricController/MetricViewChart/MetricEditController/MetricStylingApp/MetricStylingApp.tsx +++ b/web/src/controllers/MetricController/MetricViewChart/MetricEditController/MetricStylingApp/MetricStylingApp.tsx @@ -13,7 +13,6 @@ import { ScatterAxis } from '@/api/asset_interfaces/metric/charts'; import { useGetMetric, useGetMetricData } from '@/api/buster_rest/metrics'; -import { useUnmount } from '@/hooks'; export const MetricStylingApp: React.FC<{ metricId: string; @@ -21,15 +20,14 @@ export const MetricStylingApp: React.FC<{ const [segment, setSegment] = useState( MetricStylingAppSegments.VISUALIZE ); - const { data: metric } = useGetMetric({ id: metricId }); + const { data: chartConfig } = useGetMetric({ id: metricId }, (x) => x.chart_config); const { data: metricData } = useGetMetricData({ id: metricId }); - if (!metric) return null; + if (!chartConfig) return null; const columnMetadata = metricData?.data_metadata?.column_metadata || []; const rowCount = metricData?.data_metadata?.row_count || 0; - const chartConfig = metric.chart_config; const { selectedChartType, lineGroupType, diff --git a/web/src/controllers/MetricController/MetricViewChart/MetricViewChart.tsx b/web/src/controllers/MetricController/MetricViewChart/MetricViewChart.tsx index d9b705405..582eb6b17 100644 --- a/web/src/controllers/MetricController/MetricViewChart/MetricViewChart.tsx +++ b/web/src/controllers/MetricController/MetricViewChart/MetricViewChart.tsx @@ -17,7 +17,26 @@ export const MetricViewChart: React.FC<{ cardClassName?: string; }> = React.memo( ({ metricId, readOnly: readOnlyProp = false, className = '', cardClassName = '' }) => { - const { data: metric } = useGetMetric({ id: metricId }); + const { data: metric } = useGetMetric( + { id: metricId }, + ({ + chart_config, + name, + description, + time_frame, + permission, + evaluation_score, + evaluation_summary + }) => ({ + name, + description, + time_frame, + permission, + evaluation_score, + evaluation_summary, + chart_config + }) + ); const { data: metricData, isFetched: isFetchedMetricData, diff --git a/web/src/controllers/MetricController/MetricViewError/MetricViewError.tsx b/web/src/controllers/MetricController/MetricViewError/MetricViewError.tsx new file mode 100644 index 000000000..f036ca39d --- /dev/null +++ b/web/src/controllers/MetricController/MetricViewError/MetricViewError.tsx @@ -0,0 +1,13 @@ +import React from 'react'; +import { Text, Title } from '@/components/ui/typography'; +import { StatusCard } from '@/components/ui/card/StatusCard'; + +export const MetricViewError: React.FC<{ error: string | undefined }> = ({ + error = `The metric you are trying to view has an error. Please contact support if the problem persists.` +}) => { + return ( +
+ +
+ ); +}; diff --git a/web/src/controllers/MetricController/MetricViewError/index.ts b/web/src/controllers/MetricController/MetricViewError/index.ts new file mode 100644 index 000000000..b6d20b63c --- /dev/null +++ b/web/src/controllers/MetricController/MetricViewError/index.ts @@ -0,0 +1 @@ +export * from './MetricViewError'; diff --git a/web/src/controllers/MetricController/MetricViewFile/MetricViewFile.tsx b/web/src/controllers/MetricController/MetricViewFile/MetricViewFile.tsx index c5498a04a..e4822f9f2 100644 --- a/web/src/controllers/MetricController/MetricViewFile/MetricViewFile.tsx +++ b/web/src/controllers/MetricController/MetricViewFile/MetricViewFile.tsx @@ -7,7 +7,10 @@ import { useBusterNotifications } from '@/context/BusterNotifications'; import { useGetMetric, useUpdateMetric } from '@/api/buster_rest/metrics'; export const MetricViewFile: React.FC = React.memo(({ metricId }) => { - const { data: metric } = useGetMetric({ id: metricId }); + const { data: metric } = useGetMetric({ id: metricId }, ({ file, file_name }) => ({ + file, + file_name + })); const { openSuccessMessage } = useBusterNotifications(); const { mutateAsync: updateMetric } = useUpdateMetric(); diff --git a/web/src/controllers/MetricController/MetricViewResults/MetricViewResultsController.tsx b/web/src/controllers/MetricController/MetricViewResults/MetricViewResultsController.tsx index bbb21d803..14795487a 100644 --- a/web/src/controllers/MetricController/MetricViewResults/MetricViewResultsController.tsx +++ b/web/src/controllers/MetricController/MetricViewResults/MetricViewResultsController.tsx @@ -21,7 +21,10 @@ export const MetricViewResults: React.FC = React.memo(({ metric const { runSQL, resetRunSQLData, saveSQL, warnBeforeNavigating, setWarnBeforeNavigating } = useMetricRunSQL(); - const { data: metric } = useGetMetric({ id: metricId }); + const { data: metric } = useGetMetric({ id: metricId }, ({ sql, data_source_id }) => ({ + sql, + data_source_id + })); const { data: metricData } = useGetMetricData({ id: metricId }); const [sql, setSQL] = React.useState(metric?.sql || ''); diff --git a/web/src/layouts/ChatLayout/FileContainer/FileContainerHeader/MetricContainerHeaderButtons/MetricThreeDotMenu.tsx b/web/src/layouts/ChatLayout/FileContainer/FileContainerHeader/MetricContainerHeaderButtons/MetricThreeDotMenu.tsx index fbad1aa31..1e04f3124 100644 --- a/web/src/layouts/ChatLayout/FileContainer/FileContainerHeader/MetricContainerHeaderButtons/MetricThreeDotMenu.tsx +++ b/web/src/layouts/ChatLayout/FileContainer/FileContainerHeader/MetricContainerHeaderButtons/MetricThreeDotMenu.tsx @@ -261,7 +261,7 @@ const useCollectionSelectMenu = ({ metricId }: { metricId: string }) => { }; const useStatusSelectMenu = ({ metricId }: { metricId: string }) => { - const { data: metric } = useGetMetric({ id: metricId }, (x) => x); + const { data: metricStatus } = useGetMetric({ id: metricId }, (x) => x.status); const { mutateAsync: updateMetric } = useUpdateMetric(); const onChangeStatus = useMemoizedFn(async (status: VerificationStatus) => { @@ -270,7 +270,7 @@ const useStatusSelectMenu = ({ metricId }: { metricId: string }) => { const dropdownProps = useStatusDropdownContent({ isAdmin: true, - selectedStatus: metric?.status || VerificationStatus.NOT_REQUESTED, + selectedStatus: metricStatus || VerificationStatus.NOT_REQUESTED, onChangeStatus }); @@ -282,7 +282,7 @@ const useStatusSelectMenu = ({ metricId }: { metricId: string }) => { () => ({ label: 'Status', value: 'status', - icon: , + icon: , items: [{statusSubMenu}] }), [statusSubMenu]