add metrics to dashboard tests

This commit is contained in:
Nate Kelley 2025-03-20 14:13:21 -06:00
parent 025542866d
commit abb54f4dbb
No known key found for this signature in database
GPG Key ID: FD90372AB8D98B4F
14 changed files with 327 additions and 142 deletions

View File

@ -0,0 +1,51 @@
import type { BusterDashboard } from '@/api/asset_interfaces/dashboard';
import { MAX_NUMBER_OF_ITEMS, NUMBER_OF_COLUMNS } from '@/components/ui/grid/helpers';
import { v4 as uuidv4 } from 'uuid';
export const addMetricToDashboardConfig = (
metricIds: string[],
existingConfig: BusterDashboard['config']
) => {
// Create a new config object to avoid mutating the original
const newConfig = {
...existingConfig,
rows: [...(existingConfig.rows || [])]
};
// Filter out metrics that are already in the dashboard
const newMetricIds = metricIds.filter((metricId) => {
return !newConfig.rows?.some((row) => row.items.some((item) => item.id === metricId));
});
if (newMetricIds.length === 0) {
return existingConfig;
}
// Calculate how many rows we need
const totalNewMetrics = newMetricIds.length;
const metricsPerRow = MAX_NUMBER_OF_ITEMS;
const numRowsNeeded = Math.ceil(totalNewMetrics / metricsPerRow);
// Create new rows for the metrics
for (let i = 0; i < numRowsNeeded; i++) {
const startIdx = i * metricsPerRow;
const endIdx = Math.min(startIdx + metricsPerRow, totalNewMetrics);
const metricsInThisRow = newMetricIds.slice(startIdx, endIdx);
// Calculate column sizes for this row - each metric gets equal width
const columnSize = NUMBER_OF_COLUMNS / metricsInThisRow.length;
const columnSizes = Array(metricsInThisRow.length).fill(columnSize);
// Create the new row
const newRow = {
id: uuidv4(),
items: metricsInThisRow.map((id) => ({ id })),
columnSizes,
rowHeight: 320 // Default row height
};
newConfig.rows.push(newRow);
}
return newConfig;
};

View File

@ -0,0 +1,174 @@
import { addMetricToDashboardConfig } from './addMetricToDashboard';
import type { BusterDashboard } from '@/api/asset_interfaces/dashboard';
describe('addMetricToDashboardConfig', () => {
const createEmptyConfig = (): BusterDashboard['config'] => ({
rows: []
});
const createConfigWithRows = (
rows: BusterDashboard['config']['rows']
): BusterDashboard['config'] => ({
rows
});
it('should return the same config if no new metrics are provided', () => {
const config = createEmptyConfig();
const result = addMetricToDashboardConfig([], config);
expect(result).toBe(config);
});
it('should add a single metric to an empty config', () => {
const config = createEmptyConfig();
const result = addMetricToDashboardConfig(['metric1'], config);
expect(result.rows!).toHaveLength(1);
expect(result.rows![0].items).toHaveLength(1);
expect(result.rows![0].items[0].id).toBe('metric1');
expect(result.rows![0].columnSizes).toEqual([12]); // Single column takes full width
});
it('should add multiple metrics up to MAX_NUMBER_OF_ITEMS in a single row', () => {
const config = createEmptyConfig();
const result = addMetricToDashboardConfig(['metric1', 'metric2', 'metric3', 'metric4'], config);
expect(result.rows!).toHaveLength(1);
expect(result.rows![0].items).toHaveLength(4);
expect(result.rows![0].columnSizes).toEqual([3, 3, 3, 3]); // 4 equal columns
});
it('should create multiple rows when metrics exceed MAX_NUMBER_OF_ITEMS', () => {
const config = createEmptyConfig();
const result = addMetricToDashboardConfig(
['metric1', 'metric2', 'metric3', 'metric4', 'metric5', 'metric6'],
config
);
expect(result.rows!).toHaveLength(2);
expect(result.rows![0].items).toHaveLength(4);
expect(result.rows![1].items).toHaveLength(2);
expect(result.rows![0].columnSizes).toEqual([3, 3, 3, 3]);
expect(result.rows![1].columnSizes).toEqual([6, 6]); // 2 equal columns
});
it('should not add duplicate metrics', () => {
const config = createConfigWithRows([
{
id: 'row1',
items: [{ id: 'metric1' }],
columnSizes: [12],
rowHeight: 320
}
]);
const result = addMetricToDashboardConfig(['metric1', 'metric2'], config);
expect(result.rows!).toHaveLength(2);
expect(result.rows![0].items).toHaveLength(1);
expect(result.rows![1].items).toHaveLength(1);
expect(result.rows![1].items[0].id).toBe('metric2');
});
it('should respect MIN_NUMBER_OF_COLUMNS when adding fewer metrics', () => {
const config = createEmptyConfig();
const result = addMetricToDashboardConfig(['metric1', 'metric2'], config);
expect(result.rows!).toHaveLength(1);
expect(result.rows![0].items).toHaveLength(2);
expect(result.rows![0].columnSizes).toEqual([6, 6]); // 2 equal columns
});
it('should respect MAX_NUMBER_OF_COLUMNS when adding many metrics', () => {
const config = createEmptyConfig();
const result = addMetricToDashboardConfig(
[
'metric1',
'metric2',
'metric3',
'metric4',
'metric5',
'metric6',
'metric7',
'metric8',
'metric9',
'metric10',
'metric11',
'metric12',
'metric13'
],
config
);
expect(result.rows!).toHaveLength(4);
expect(result.rows![0].items).toHaveLength(4);
expect(result.rows![1].items).toHaveLength(4);
expect(result.rows![2].items).toHaveLength(4);
expect(result.rows![3].items).toHaveLength(1);
expect(result.rows![0].columnSizes).toEqual([3, 3, 3, 3]);
expect(result.rows![3].columnSizes).toEqual([12]); // Single column for last row
});
it('should preserve existing rows when adding new metrics', () => {
const config = createConfigWithRows([
{
id: 'row1',
items: [{ id: 'existing1' }],
columnSizes: [12],
rowHeight: 320
}
]);
const result = addMetricToDashboardConfig(['metric1', 'metric2'], config);
expect(result.rows!).toHaveLength(2);
expect(result.rows![0].items[0].id).toBe('existing1');
expect(result.rows![1].items[0].id).toBe('metric1');
expect(result.rows![1].items[1].id).toBe('metric2');
});
it('should correctly distribute columns for exactly 2 metrics', () => {
const config = createEmptyConfig();
const result = addMetricToDashboardConfig(['metric1', 'metric2'], config);
expect(result.rows!).toHaveLength(1);
expect(result.rows![0].items).toHaveLength(2);
expect(result.rows![0].items.map((item) => item.id)).toEqual(['metric1', 'metric2']);
expect(result.rows![0].columnSizes).toEqual([6, 6]); // Two equal columns of 6
expect(result.rows![0].columnSizes!.reduce((a, b) => a + b)).toBe(12); // Sum should be 12
});
it('should correctly distribute columns for exactly 3 metrics', () => {
const config = createEmptyConfig();
const result = addMetricToDashboardConfig(['metric1', 'metric2', 'metric3'], config);
expect(result.rows!).toHaveLength(1);
expect(result.rows![0].items).toHaveLength(3);
expect(result.rows![0].items.map((item) => item.id)).toEqual(['metric1', 'metric2', 'metric3']);
expect(result.rows![0].columnSizes).toEqual([4, 4, 4]); // Three equal columns of 4
expect(result.rows![0].columnSizes!.reduce((a, b) => a + b)).toBe(12); // Sum should be 12
});
it('should correctly distribute columns for exactly 4 metrics', () => {
const config = createEmptyConfig();
const result = addMetricToDashboardConfig(['metric1', 'metric2', 'metric3', 'metric4'], config);
expect(result.rows!).toHaveLength(1);
expect(result.rows![0].items).toHaveLength(4);
expect(result.rows![0].items.map((item) => item.id)).toEqual([
'metric1',
'metric2',
'metric3',
'metric4'
]);
expect(result.rows![0].columnSizes).toEqual([3, 3, 3, 3]); // Four equal columns of 3
expect(result.rows![0].columnSizes!.reduce((a, b) => a + b)).toBe(12); // Sum should be 12
});
it('should correctly distribute columns for exactly 0 metrics', () => {
const config = createEmptyConfig();
const result = addMetricToDashboardConfig([], config);
expect(result.rows!).toHaveLength(0);
expect(result.rows).toBe(config.rows);
});
});

View File

@ -183,8 +183,7 @@ export const useAddDashboardToCollection = () => {
const mutationFn = useMemoizedFn(
async (variables: { dashboardId: string; collectionIds: string[] }) => {
const { dashboardId, collectionIds } = variables;
await Promise.all(
return await Promise.all(
collectionIds.map((collectionId) =>
addAssetToCollection({
id: dashboardId,
@ -194,7 +193,6 @@ export const useAddDashboardToCollection = () => {
);
}
);
return useMutation({
mutationFn,
onSuccess: (_, { collectionIds }) => {
@ -214,7 +212,7 @@ export const useRemoveDashboardFromCollection = () => {
const mutationFn = useMemoizedFn(
async (variables: { dashboardId: string; collectionIds: string[] }) => {
const { dashboardId, collectionIds } = variables;
await Promise.all(
return await Promise.all(
collectionIds.map((collectionId) =>
removeAssetFromCollection({
id: dashboardId,
@ -245,30 +243,6 @@ export const useRemoveDashboardFromCollection = () => {
});
};
export const useRemoveItemFromDashboard = () => {
const { mutateAsync: updateDashboardMutation } = useUpdateDashboard();
const queryClient = useQueryClient();
const mutationFn = useMemoizedFn(
async (variables: { dashboardId: string; metricId: string | string[] }) => {
const { dashboardId, metricId } = variables;
const options = dashboardQueryKeys.dashboardGetDashboard(dashboardId);
const prevDashboard = queryClient.getQueryData(options.queryKey);
if (prevDashboard) {
const prevMetricsIds = Object.keys(prevDashboard?.metrics);
const newMetricsIds = prevMetricsIds?.filter((t) => !metricId.includes(t));
console.log('TODO: remove metrics from dashboard', dashboardId, metricId);
return updateDashboardMutation({
id: dashboardId
});
}
}
);
return useMutation({
mutationFn
});
};
export const useShareDashboard = () => {
const queryClient = useQueryClient();
return useMutation({
@ -341,3 +315,81 @@ export const useUpdateDashboardShare = () => {
}
});
};
export const useSaveMetricsToDashboard = () => {
const queryClient = useQueryClient();
const saveMetricToDashboard = useMemoizedFn(
async ({ metricIds, dashboardId }: { metricIds: string[]; dashboardId: string }) => {
// await saveMetric({
// id: metricId,
// save_to_dashboard: dashboardIds
// });
}
);
return useMutation({
mutationFn: saveMetricToDashboard,
onSuccess: (data, variables) => {
// queryClient.invalidateQueries({
// queryKey: variables.dashboardIds.map(
// (id) => dashboardQueryKeys.dashboardGetDashboard(id).queryKey
// )
// });
}
});
};
export const useRemoveMetricFromDashboard = () => {
const { openConfirmModal } = useBusterNotifications();
const queryClient = useQueryClient();
const removeMetricFromDashboard = useMemoizedFn(
async ({
metricId,
dashboardId,
useConfirmModal = true
}: {
metricId: string;
dashboardId: string;
useConfirmModal?: boolean;
}) => {
const method = async () => {
// await saveMetric({
// id: metricId,
// remove_from_dashboard: [dashboardId]
// });
};
if (!useConfirmModal) return await method();
return await openConfirmModal({
title: 'Remove from dashboard',
content: 'Are you sure you want to remove this metric from this dashboard?',
onOk: method
});
}
);
return useMutation({
mutationFn: removeMetricFromDashboard,
onMutate: async (variables) => {
const currentDashboard = queryClient.getQueryData(
dashboardQueryKeys.dashboardGetDashboard(variables.dashboardId).queryKey
);
if (currentDashboard) {
queryClient.setQueryData(
dashboardQueryKeys.dashboardGetDashboard(variables.dashboardId).queryKey,
(currentDashboard) => {
if (currentDashboard?.dashboard.config.rows) {
currentDashboard.dashboard.config.rows.forEach((row) => {
row.items = row.items.filter((item) => item.id !== variables.metricId);
});
}
delete currentDashboard!.metrics[variables.metricId];
return currentDashboard;
}
);
}
}
});
};

View File

@ -9,7 +9,7 @@ import {
import { queryKeys } from '@/api/query_keys';
import { useBusterNotifications } from '@/context/BusterNotifications';
export const useListDatasources = (enabled: boolean) => {
export const useListDatasources = (enabled: boolean = true) => {
return useQuery({
...queryKeys.datasourceGetList,
queryFn: listDatasources,

View File

@ -247,85 +247,6 @@ export const useRemoveMetricFromCollection = () => {
});
};
export const useSaveMetricToDashboard = () => {
const queryClient = useQueryClient();
const saveMetricToDashboard = useMemoizedFn(
async ({ metricId, dashboardIds }: { metricId: string; dashboardIds: string[] }) => {
// await saveMetric({
// id: metricId,
// save_to_dashboard: dashboardIds
// });
}
);
return useMutation({
mutationFn: saveMetricToDashboard,
onSuccess: (data, variables) => {
queryClient.invalidateQueries({
queryKey: variables.dashboardIds.map(
(id) => dashboardQueryKeys.dashboardGetDashboard(id).queryKey
)
});
}
});
};
export const useRemoveMetricFromDashboard = () => {
const { openConfirmModal } = useBusterNotifications();
const { mutateAsync: saveMetric } = useSaveMetric();
const queryClient = useQueryClient();
const removeMetricFromDashboard = useMemoizedFn(
async ({
metricId,
dashboardId,
useConfirmModal = true
}: {
metricId: string;
dashboardId: string;
useConfirmModal?: boolean;
}) => {
const method = async () => {
// await saveMetric({
// id: metricId,
// remove_from_dashboard: [dashboardId]
// });
};
if (!useConfirmModal) return await method();
return await openConfirmModal({
title: 'Remove from dashboard',
content: 'Are you sure you want to remove this metric from this dashboard?',
onOk: method
});
}
);
return useMutation({
mutationFn: removeMetricFromDashboard,
onMutate: async (variables) => {
const currentDashboard = queryClient.getQueryData(
dashboardQueryKeys.dashboardGetDashboard(variables.dashboardId).queryKey
);
if (currentDashboard) {
queryClient.setQueryData(
dashboardQueryKeys.dashboardGetDashboard(variables.dashboardId).queryKey,
(currentDashboard) => {
if (currentDashboard?.dashboard.config.rows) {
currentDashboard.dashboard.config.rows.forEach((row) => {
row.items = row.items.filter((item) => item.id !== variables.metricId);
});
}
delete currentDashboard!.metrics[variables.metricId];
return currentDashboard;
}
);
}
}
});
};
export const useDuplicateMetric = () => {
return useMutation({
mutationFn: duplicateMetric

View File

@ -2,13 +2,18 @@ import { BusterListSelectedOptionPopupContainer } from '@/components/ui/list';
import React from 'react';
import { PermissionAssignedButton } from '@/components/features/PermissionComponents';
import { useDatasetUpdateDatasetGroups } from '@/api/buster_rest';
import { useMemoizedFn } from '@/hooks';
export const PermissionDatasetGroupSelectedPopup: React.FC<{
selectedRowKeys: string[];
onSelectChange: (selectedRowKeys: string[]) => void;
datasetId: string;
}> = React.memo(({ selectedRowKeys, onSelectChange, datasetId }) => {
const { mutateAsync: updateDatasetGroups } = useDatasetUpdateDatasetGroups(datasetId);
const { mutateAsync: updateDatasetGroups } = useDatasetUpdateDatasetGroups();
const onUpdate = useMemoizedFn(async (groups: { id: string; assigned: boolean }[]) => {
return updateDatasetGroups({ dataset_id: datasetId, groups });
});
return (
<BusterListSelectedOptionPopupContainer
@ -20,7 +25,7 @@ export const PermissionDatasetGroupSelectedPopup: React.FC<{
text="assigned"
selectedRowKeys={selectedRowKeys}
onSelectChange={onSelectChange}
onUpdate={updateDatasetGroups}
onUpdate={onUpdate}
/>
]}
/>

View File

@ -1,15 +1,9 @@
import { DataSource } from '@/api/asset_interfaces';
import React, { useRef } from 'react';
import { Input } from '@/components/ui/inputs';
import { Select } from '@/components/ui/select';
import { FormWrapper, FormWrapperHandle } from './FormWrapper';
import { FormWrapperHandle } from './FormWrapper';
import { formatDate } from '@/lib';
import {
DatasourceCreateCredentials,
PostgresCreateCredentials
} from '@/api/request_interfaces/datasources';
import { DatasourceCreateCredentials } from '@/api/request_interfaces/datasources';
import { useHotkeys } from 'react-hotkeys-hook';
import { TagInput } from '@/components/ui/inputs/InputTagInput';
const sshModeOptions = ['Do not use SSH credentials', 'Use SSH credentials'].map((item, index) => ({
label: item,

View File

@ -27,7 +27,7 @@ export const SaveDashboardToCollectionButton: React.FC<{
dashboardIds.map((dashboardId) => {
return addDashboardToCollection({
dashboardId,
collectionId: collectionIds
collectionIds: collectionIds
});
})
);
@ -40,7 +40,7 @@ export const SaveDashboardToCollectionButton: React.FC<{
dashboardIds.map((dashboardId) => {
return removeDashboardFromCollection({
dashboardId,
collectionId
collectionIds: [collectionId]
});
})
);

View File

@ -38,7 +38,7 @@ export const SaveMetricToCollectionButton: React.FC<{
const allSelectedButLast = selectedCollections.slice(0, -1);
await Promise.all(
allSelectedButLast.map((metricId) => {
return removeMetricFromCollection({ metricId, collectionId });
return removeMetricFromCollection({ metricId, collectionIds: [collectionId] });
})
);
openInfoMessage('Metrics removed from collections');

View File

@ -85,14 +85,8 @@ const CollectionsButton: React.FC<{
const onRemoveFromCollection = useMemoizedFn(async (collectionId: string) => {
setSelectedCollections((prev) => prev.filter((id) => id !== collectionId));
const allSelectedButLast = selectedRowKeys.slice(0, -1);
const lastMetricId = selectedRowKeys[selectedRowKeys.length - 1];
const allRemoves: Promise<void>[] = allSelectedButLast.map((metricId) => {
return removeMetricFromCollection({ metricId, collectionId });
});
await removeMetricFromCollection({
metricId: lastMetricId,
collectionId
const allRemoves: Promise<void>[] = selectedRowKeys.map((metricId) => {
return removeMetricFromCollection({ metricId, collectionIds: [collectionId] });
});
await Promise.all(allRemoves);
openInfoMessage('Metrics removed from collections');

View File

@ -77,7 +77,7 @@ export const DashboardContentController: React.FC<{
);
const onRowLayoutChange = useMemoizedFn((rows: BusterResizeableGridRow[]) => {
onUpdateDashboardConfig({ rows, id: dashboard!.id });
if (dashboard) onUpdateDashboardConfig({ rows, id: dashboard.id });
});
const onDragEnd = useMemoizedFn(() => {

View File

@ -67,7 +67,7 @@ const CollectionsButton: React.FC<{
selectedRowKeys.map((dashboardId) => {
return onAddDashboardToCollection({
dashboardId,
collectionId: collectionIds
collectionIds
});
})
);
@ -81,7 +81,7 @@ const CollectionsButton: React.FC<{
selectedRowKeys.map((dashboardId) => {
return onRemoveDashboardFromCollection({
dashboardId,
collectionId
collectionIds: [collectionId]
});
})
);

View File

@ -90,14 +90,8 @@ const CollectionsButton: React.FC<{
const onRemoveFromCollection = useMemoizedFn(async (collectionId: string) => {
setSelectedCollections((prev) => prev.filter((id) => id !== collectionId));
const allSelectedButLast = selectedRowKeys.slice(0, -1);
const lastMetricId = selectedRowKeys[selectedRowKeys.length - 1];
const allRemoves: Promise<void>[] = allSelectedButLast.map((metricId) => {
return removeMetricFromCollection({ metricId, collectionId });
});
await removeMetricFromCollection({
metricId: lastMetricId,
collectionId
const allRemoves: Promise<void>[] = selectedRowKeys.map((metricId) => {
return removeMetricFromCollection({ metricId, collectionIds: [collectionId] });
});
await Promise.all(allRemoves);
openInfoMessage('Metrics removed from collections');

View File

@ -122,12 +122,12 @@ const useCollectionSelectMenu = ({ dashboardId }: { dashboardId: string }) => {
}, [collections]);
const onSaveToCollection = useMemoizedFn(async (collectionIds: string[]) => {
await saveDashboardToCollection({ dashboardId, collectionId: collectionIds[0] });
await saveDashboardToCollection({ dashboardId, collectionIds });
openInfoMessage('Dashboard saved to collections');
});
const onRemoveFromCollection = useMemoizedFn(async (collectionId: string) => {
await removeDashboardFromCollection({ dashboardId, collectionId });
await removeDashboardFromCollection({ dashboardId, collectionIds: [collectionId] });
openInfoMessage('Dashboard removed from collections');
});