add additional metric three dot settings

This commit is contained in:
Nate Kelley 2025-03-14 18:14:48 -06:00
parent 18c709eb32
commit 5f6ce012e2
No known key found for this signature in database
GPG Key ID: FD90372AB8D98B4F
9 changed files with 112 additions and 69 deletions

7
web/package-lock.json generated
View File

@ -42,6 +42,7 @@
"chartjs-plugin-deferred": "^2.0.0",
"class-variance-authority": "^0.7.1",
"dayjs": "^1.11.13",
"dom-to-image": "^2.6.0",
"email-validator": "^2.0.4",
"font-color-contrast": "^11.1.0",
"framer-motion": "^12.5.0",
@ -10342,6 +10343,12 @@
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/dom-to-image": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/dom-to-image/-/dom-to-image-2.6.0.tgz",
"integrity": "sha512-Dt0QdaHmLpjURjU7Tnu3AgYSF2LuOmksSGsUcE6ItvJoCWTBEmiMXcqBdNSAm9+QbbwD7JMoVsuuKX6ZVQv1qA==",
"license": "MIT"
},
"node_modules/domain-browser": {
"version": "4.23.0",
"resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-4.23.0.tgz",

View File

@ -50,6 +50,7 @@
"chartjs-plugin-deferred": "^2.0.0",
"class-variance-authority": "^0.7.1",
"dayjs": "^1.11.13",
"dom-to-image": "^2.6.0",
"email-validator": "^2.0.4",
"font-color-contrast": "^11.1.0",
"framer-motion": "^12.5.0",

View File

@ -174,7 +174,8 @@ export const DEFAULT_IBUSTER_METRIC: Required<IBusterMetric> = {
public_expiry_date: null,
public_enabled_by: null,
publicly_accessible: false,
public_password: null
public_password: null,
versions: []
};
export const DEFAULT_BUSTER_METRIC_LIST_ITEM: Required<BusterMetricListItem> = {

View File

@ -47,6 +47,7 @@ export const MetricViewChart: React.FC<{ metricId: string }> = React.memo(({ met
dataMetadata={metricData?.data_metadata}
fetchedData={isFetchedMetricData}
errorMessage={metricDataError?.message}
metricId={metricId}
/>
</MetricViewChartCard>

View File

@ -3,6 +3,7 @@ import { ChartType } from '@/api/asset_interfaces/metric/charts';
import { BusterChart } from '@/components/ui/charts';
import { cn } from '@/lib/classMerge';
import React, { useMemo } from 'react';
import { METRIC_CHART_CONTAINER_ID } from './config';
interface MetricViewChartContentProps {
className?: string;
@ -11,10 +12,19 @@ interface MetricViewChartContentProps {
dataMetadata: DataMetadata | undefined;
fetchedData: boolean;
errorMessage: string | null | undefined;
metricId: string;
}
export const MetricViewChartContent: React.FC<MetricViewChartContentProps> = React.memo(
({ className, chartConfig, metricData = null, dataMetadata, fetchedData, errorMessage }) => {
({
className,
chartConfig,
metricData = null,
dataMetadata,
fetchedData,
errorMessage,
metricId
}) => {
const columnMetadata = dataMetadata?.column_metadata;
const isTable = chartConfig?.selectedChartType === ChartType.Table;
@ -30,6 +40,7 @@ export const MetricViewChartContent: React.FC<MetricViewChartContentProps> = Rea
error={errorMessage || undefined}
data={metricData}
columnMetadata={columnMetadata}
id={METRIC_CHART_CONTAINER_ID(metricId)}
{...chartConfig}
/>
</div>

View File

@ -0,0 +1 @@
export const METRIC_CHART_CONTAINER_ID = (metricId: string) => `metric-chart-container-${metricId}`;

View File

@ -41,7 +41,8 @@ import { ShareAssetType, VerificationStatus } from '@/api/asset_interfaces/share
import { useStatusDropdownContent } from '@/components/features/metrics/StatusBadgeIndicator/StatusDropdownContent';
import { StatusBadgeIndicator } from '@/components/features/metrics/StatusBadgeIndicator';
import { useFavoriteStar } from '@/components/features/list/FavoriteStar';
import { exportJSONToCSV } from '@/lib/exportUtils';
import { downloadElementToImage, exportElementToImage, exportJSONToCSV } from '@/lib/exportUtils';
import { METRIC_CHART_CONTAINER_ID } from '@/controllers/MetricController/MetricViewChart/config';
export const ThreeDotMenuButton = React.memo(({ metricId }: { metricId: string }) => {
const { mutateAsync: deleteMetric, isPending: isDeletingMetric } = useDeleteMetric();
@ -57,6 +58,9 @@ export const ThreeDotMenuButton = React.memo(({ metricId }: { metricId: string }
const resultsViewMenu = useResultsViewSelectMenu();
const sqlEditorMenu = useSQLEditorSelectMenu();
const downloadCSVMenu = useDownloadCSVSelectMenu({ metricId });
const downloadPNGMenu = useDownloadPNGSelectMenu({ metricId });
const deleteMetricMenu = useDeleteMetricSelectMenu({ metricId });
const renameMetricMenu = useRenameMetricSelectMenu({ metricId });
const items: DropdownItems = useMemo(
() => [
@ -77,37 +81,16 @@ export const ThreeDotMenuButton = React.memo(({ metricId }: { metricId: string }
sqlEditorMenu,
{ type: 'divider' },
downloadCSVMenu,
// {
// label: 'Download as PNG',
// value: 'download-png',
// icon: <SquareChart />,
// onClick: () => {
// console.log('download png');
// }
// },
downloadPNGMenu,
{ type: 'divider' },
{
label: 'Rename metric',
value: 'rename',
icon: <Pencil />,
onClick: () => {
console.log('rename');
}
},
{
label: 'Delete metric',
value: 'delete',
icon: <Trash />,
loading: isDeletingMetric,
onClick: async () => {
await deleteMetric({ ids: [metricId] });
openSuccessMessage('Metric deleted');
onSetSelectedFile(null);
}
}
renameMetricMenu,
deleteMetricMenu
],
[
dashboardSelectMenu,
deleteMetricMenu,
downloadCSVMenu,
downloadPNGMenu,
isDeletingMetric,
metricId,
openSuccessMessage,
@ -366,6 +349,7 @@ const useDownloadCSVSelectMenu = ({ metricId }: { metricId: string }) => {
label: 'Download as CSV',
value: 'download-csv',
icon: <Download4 />,
loading: isDownloading,
onClick: async () => {
const data = metricData?.data;
if (data && title) {
@ -375,6 +359,73 @@ const useDownloadCSVSelectMenu = ({ metricId }: { metricId: string }) => {
}
}
}),
[metricData, title]
[metricData, isDownloading, title]
);
};
const useDownloadPNGSelectMenu = ({ metricId }: { metricId: string }) => {
const { openSuccessMessage, openErrorMessage } = useBusterNotifications();
const { data: title } = useGetMetric(metricId, (x) => x.title);
const { data: selectedChartType } = useGetMetric(
metricId,
(x) => x.chart_config?.selectedChartType
);
const canDownload = selectedChartType && selectedChartType !== 'table';
return useMemo(
() => ({
label: 'Download as PNG',
value: 'download-png',
disabled: !canDownload,
icon: <SquareChart />,
onClick: async () => {
const node = document.getElementById(METRIC_CHART_CONTAINER_ID(metricId)) as HTMLElement;
if (node) {
try {
return await downloadElementToImage(node, `${title}.png`);
} catch (error) {
console.error(error);
}
}
openErrorMessage('Failed to download PNG');
}
}),
[canDownload]
);
};
const useDeleteMetricSelectMenu = ({ metricId }: { metricId: string }) => {
const { mutateAsync: deleteMetric } = useDeleteMetric();
return useMemo(
() => ({
label: 'Delete metric',
value: 'delete-metric',
icon: <Trash />,
onClick: async () => {
await deleteMetric({ ids: [metricId] });
}
}),
[metricId]
);
};
const useRenameMetricSelectMenu = ({ metricId }: { metricId: string }) => {
const { mutateAsync: updateMetric } = useUpdateMetric();
return useMemo(
() => ({
label: 'Rename metric',
value: 'rename-metric',
icon: <Pencil />,
onClick: async () => {
console.log('rename');
alert('TODO: Implement rename metric');
// await updateMetric({ id: metricId, title: 'New title' });
}
}),
[metricId]
);
};

View File

@ -1,3 +1,5 @@
'use client';
import { timeout } from './timeout';
export async function exportJSONToCSV(
@ -38,44 +40,6 @@ export async function exportJSONToCSV(
downloadFile(`${fileName}.csv`, blob);
}
export async function exportToCsv<R, SR>(gridElement: HTMLDivElement, fileName: string) {
const { head, body, foot } = await getGridContent(gridElement);
const content = [...head, ...body, ...foot]
.map((cells) => cells.map(serialiseCellValue).join(','))
.join('\n');
downloadFile(fileName, new Blob([content], { type: 'text/csv;charset=utf-8;' }));
}
async function getGridContent<R, SR>(gridElement: HTMLDivElement) {
const grid = gridElement as HTMLDivElement;
const hasrdgClass = grid.classList.contains('rdg');
if (!hasrdgClass) {
throw new Error('Element is not a valid ReactDataGrid instance');
}
return {
head: getRows('.rdg-header-row'),
body: getRows('.rdg-row:not(.rdg-summary-row)'),
foot: getRows('.rdg-summary-row')
};
function getRows(selector: string) {
return Array.from(grid.querySelectorAll<HTMLDivElement>(selector)).map((gridRow) => {
return Array.from(gridRow.querySelectorAll<HTMLDivElement>('.rdg-cell')).map(
(gridCell) => gridCell.innerText
);
});
}
}
function serialiseCellValue(value: unknown) {
if (typeof value === 'string') {
const formattedValue = value.replace(/"/g, '""');
return formattedValue.includes(',') ? `"${formattedValue}"` : formattedValue;
}
return value;
}
function downloadFile(fileName: string, data: Blob) {
const downloadLink = document.createElement('a');
downloadLink.download = fileName;
@ -95,6 +59,11 @@ export async function exportElementToImage(element: HTMLElement) {
return dataUrl;
}
export async function downloadElementToImage(element: HTMLElement, fileName: string) {
const imageData = await exportElementToImage(element);
downloadImageData(imageData, fileName);
}
export async function downloadImageData(imageData: string, fileName: string) {
const link = document.createElement('a');
link.href = imageData;

View File

@ -91,6 +91,7 @@ export const createMockMetric = (id: string): IBusterMetric => {
data_metadata: dataMetadata,
status: VerificationStatus.NOT_REQUESTED,
evaluation_score: 'Moderate',
versions: [],
evaluation_summary: faker.lorem.sentence(33),
file: `
metric: