mirror of https://github.com/buster-so/buster.git
add additional metric three dot settings
This commit is contained in:
parent
18c709eb32
commit
5f6ce012e2
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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> = {
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
export const METRIC_CHART_CONTAINER_ID = (metricId: string) => `metric-chart-container-${metricId}`;
|
|
@ -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]
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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:
|
||||
|
|
Loading…
Reference in New Issue