From 81bddd5216ba442d8b2f08aade69447dc57e0bcd Mon Sep 17 00:00:00 2001 From: Nate Kelley Date: Wed, 6 Aug 2025 22:18:17 -0600 Subject: [PATCH] Add 3 dot menu for report --- .../ui/grid/BusterResizeableGrid.stories.tsx | 2 +- .../ui/report/elements/BlockDraggable.tsx | 2 +- .../elements/MetricElement/MetricElement.tsx | 4 +- .../MetricElement/MetricPlaceholder.tsx | 7 +- .../useMetricContentThreeDotMenuItems.tsx | 111 ++++++++++++++++-- .../ui/report/plugins/banner-plugin.tsx | 12 +- .../report/plugins/character-counter-kit.tsx | 84 +++++++------ .../plugins/metric-plugin/insert-metric.ts | 1 + .../plugins/metric-plugin/metric-plugin.tsx | 1 + .../Metrics/metricDropdownItems/index.ts | 3 + .../useDownloadMetricDataCSV.tsx | 37 ++++++ .../useDownloadMetricDataPNG.tsx | 48 ++++++++ .../useRenameMetricOnPage.tsx | 68 +++++++++++ .../DashboardMetricItem.tsx | 6 +- .../metricCardThreeDotMenuItems.tsx | 17 +-- .../MetricThreeDotMenu.tsx | 110 +++-------------- .../routes/busterRoutes/busterAppRoutes.ts | 9 ++ 17 files changed, 344 insertions(+), 178 deletions(-) create mode 100644 apps/web/src/context/Metrics/metricDropdownItems/index.ts create mode 100644 apps/web/src/context/Metrics/metricDropdownItems/useDownloadMetricDataCSV.tsx create mode 100644 apps/web/src/context/Metrics/metricDropdownItems/useDownloadMetricDataPNG.tsx create mode 100644 apps/web/src/context/Metrics/metricDropdownItems/useRenameMetricOnPage.tsx diff --git a/apps/web/src/components/ui/grid/BusterResizeableGrid.stories.tsx b/apps/web/src/components/ui/grid/BusterResizeableGrid.stories.tsx index 63226ae1b..20a101712 100644 --- a/apps/web/src/components/ui/grid/BusterResizeableGrid.stories.tsx +++ b/apps/web/src/components/ui/grid/BusterResizeableGrid.stories.tsx @@ -7,10 +7,10 @@ import { useContext, useState } from 'react'; import { v4 as uuidv4 } from 'uuid'; import { cn } from '@/lib/classMerge'; import { Hand } from '../icons'; -import { SortableItemContext } from './_BusterSortableItemDragContainer'; import { BusterResizeableGrid } from './BusterResizeableGrid'; import { MIN_ROW_HEIGHT } from './helpers'; import type { BusterResizeableGridRow } from './interfaces'; +import { SortableItemContext } from './SortableItemContext'; const meta: Meta = { title: 'UI/Grid/BusterResizeableGrid', diff --git a/apps/web/src/components/ui/report/elements/BlockDraggable.tsx b/apps/web/src/components/ui/report/elements/BlockDraggable.tsx index 6d9e0a0c0..5c8f8ba31 100644 --- a/apps/web/src/components/ui/report/elements/BlockDraggable.tsx +++ b/apps/web/src/components/ui/report/elements/BlockDraggable.tsx @@ -199,7 +199,7 @@ function Gutter({ children, className, ...props }: React.ComponentProps<'div'>) {...props} className={cn( 'slate-gutterLeft', - 'absolute top-0 z-50 flex h-full -translate-x-full cursor-text hover:opacity-100 sm:opacity-0', + 'absolute top-0 z-50 flex -translate-x-full cursor-text hover:opacity-100 sm:opacity-0', getPluginByType(editor, element.type)?.node.isContainer ? 'group-hover/container:opacity-100' : 'group-hover:opacity-100', diff --git a/apps/web/src/components/ui/report/elements/MetricElement/MetricElement.tsx b/apps/web/src/components/ui/report/elements/MetricElement/MetricElement.tsx index 860823161..31382e772 100644 --- a/apps/web/src/components/ui/report/elements/MetricElement/MetricElement.tsx +++ b/apps/web/src/components/ui/report/elements/MetricElement/MetricElement.tsx @@ -86,8 +86,8 @@ const MetricResizeContainer: React.FC = ({ children }) => { ref={ref} contentEditable={false} className={cn( - 'group relative m-0 my-1.5 w-full cursor-default', - isSelected && 'bg-item-hover/50 ring-ring rounded ring-2 ring-offset-4' + 'group relative m-0 my-1.5 w-full cursor-default transition-all', + isSelected && 'bg-item-hover/10 ring-ring rounded ring-2 ring-offset-4' )}> {
@@ -134,3 +131,5 @@ const MemoizedAddMetricModal = React.memo( ); } ); + +MemoizedAddMetricModal.displayName = 'MemoizedAddMetricModal'; diff --git a/apps/web/src/components/ui/report/elements/MetricElement/useMetricContentThreeDotMenuItems.tsx b/apps/web/src/components/ui/report/elements/MetricElement/useMetricContentThreeDotMenuItems.tsx index b17c3d8b4..cdb8caeea 100644 --- a/apps/web/src/components/ui/report/elements/MetricElement/useMetricContentThreeDotMenuItems.tsx +++ b/apps/web/src/components/ui/report/elements/MetricElement/useMetricContentThreeDotMenuItems.tsx @@ -1,8 +1,15 @@ import React, { useMemo } from 'react'; import type { DropdownItem, DropdownItems } from '@/components/ui/dropdown'; -import { Trash } from '@/components/ui/icons'; +import { Code, SquareChartPen, Table, Trash } from '@/components/ui/icons'; import { assetParamsToRoute } from '@/lib/assets/assetParamsToRoute'; -import { ASSET_ICONS } from '@/components/features/config/assetIcons'; +import { ArrowUpRight } from '@/components/ui/icons'; +import { useEditorRef, useElement, type PlateEditor } from 'platejs/react'; +import type { TElement } from 'platejs'; +import { + useDownloadMetricDataCSV, + useDownloadPNGSelectMenu, + useRenameMetricOnPage +} from '@/context/Metrics/metricDropdownItems'; export const useMetricContentThreeDotMenuItems = ({ metricId, @@ -17,6 +24,9 @@ export const useMetricContentThreeDotMenuItems = ({ reportVersionNumber: number | undefined; reportId: string; }): DropdownItems => { + const editor = useEditorRef(); + const element = useElement(); + const openChartItem = useOpenChartItem({ reportId, metricId, @@ -24,8 +34,42 @@ export const useMetricContentThreeDotMenuItems = ({ reportVersionNumber, metricVersionNumber }); + const removeFromReportItem = useRemoveFromReportItem({ + editor, + element + }); + const navigateToMetricItem = useNavigatetoMetricItem({ + reportId, + metricId, + chatId, + reportVersionNumber, + metricVersionNumber + }); + const downloadCSV = useDownloadMetricDataCSV({ metricId, metricVersionNumber }); + const downloadPNG = useDownloadPNGSelectMenu({ metricId, metricVersionNumber }); + const renameMetric = useRenameMetricOnPage({ metricId, metricVersionNumber }); - return [openChartItem, { type: 'divider' }]; + return useMemo( + () => [ + openChartItem, + removeFromReportItem, + { type: 'divider' }, + ...navigateToMetricItem, + { type: 'divider' }, + downloadCSV, + downloadPNG, + { type: 'divider' }, + renameMetric + ], + [ + openChartItem, + removeFromReportItem, + navigateToMetricItem, + downloadCSV, + downloadPNG, + renameMetric + ] + ); }; const useOpenChartItem = ({ @@ -54,20 +98,20 @@ const useOpenChartItem = ({ () => ({ value: 'open-chart', label: 'Open chart', - icon: , + icon: , link: route, - linkIcon: 'arrow-external' + linkIcon: 'arrow-right' }), [route] ); }; const useRemoveFromReportItem = ({ - reportId, - metricId + editor, + element }: { - reportId: string; - metricId: string; + editor: PlateEditor; + element: TElement; }): DropdownItem => { return useMemo( () => ({ @@ -75,9 +119,56 @@ const useRemoveFromReportItem = ({ label: 'Remove from report', icon: , onClick: () => { - console.log('remove from report'); + const path = editor.api.findPath(element); + editor.tf.removeNodes({ at: path }); } }), [] ); }; + +const useNavigatetoMetricItem = ({ + reportId, + metricId, + chatId, + reportVersionNumber, + metricVersionNumber +}: { + reportId: string; + metricId: string; + metricVersionNumber: number | undefined; + chatId: string | undefined; + reportVersionNumber: number | undefined; +}): DropdownItem[] => { + return useMemo(() => { + const baseParams = { + assetId: metricId, + type: 'metric' as const, + reportVersionNumber, + metricVersionNumber, + reportId, + chatId + }; + + const editChartRoute = assetParamsToRoute({ + ...baseParams, + page: 'chart' + }); + + const resultsChartRoute = assetParamsToRoute({ + ...baseParams, + page: 'results' + }); + + const sqlChartRoute = assetParamsToRoute({ + ...baseParams, + page: 'sql' + }); + + return [ + { value: 'edit-chart', label: 'Edit chart', icon: , link: editChartRoute }, + { value: 'results-chart', label: 'Results chart', icon: , link: resultsChartRoute }, + { value: 'sql-chart', label: 'SQL chart', icon: , link: sqlChartRoute } + ]; + }, [reportId, metricId, chatId, reportVersionNumber, metricVersionNumber]); +}; diff --git a/apps/web/src/components/ui/report/plugins/banner-plugin.tsx b/apps/web/src/components/ui/report/plugins/banner-plugin.tsx index cc16fcf81..ac3d2e3c3 100644 --- a/apps/web/src/components/ui/report/plugins/banner-plugin.tsx +++ b/apps/web/src/components/ui/report/plugins/banner-plugin.tsx @@ -1,16 +1,6 @@ import { type PluginConfig, createTSlatePlugin } from 'platejs'; -export type BannerConfig = PluginConfig< - 'banner', - //api - {}, - //options - {}, - //selectors - {}, - //transforms - {} ->; +export type BannerConfig = PluginConfig<'banner'>; export const BannerPlugin = createTSlatePlugin({ key: 'banner', // unique plugin key diff --git a/apps/web/src/components/ui/report/plugins/character-counter-kit.tsx b/apps/web/src/components/ui/report/plugins/character-counter-kit.tsx index e839ec96a..b4e701749 100644 --- a/apps/web/src/components/ui/report/plugins/character-counter-kit.tsx +++ b/apps/web/src/components/ui/report/plugins/character-counter-kit.tsx @@ -25,6 +25,50 @@ const countCharactersInNodes = (nodes: (TNode | Descendant)[]): number => { return count; }; +const CharacterCounterElement = ({ + attributes, + children, + ...props +}: CharacterElementCounterProps) => { + const { getOptions, element } = props; + const options = getOptions(); + const selected = useSelected(); + const { maxLength, showWarning, warningThreshold } = options; + + // Get the character length of only this component's content + const characterLength = useMemo(() => { + return countCharactersInNodes(element.children); + }, [element.children]); + + // Calculate warning state + const warningLength = maxLength * warningThreshold; + const isOverWarning = characterLength >= warningLength; + const isOverLimit = characterLength > maxLength; + + return ( + +
+ Character count: {characterLength} / {maxLength} + {showWarning && isOverWarning && ( + + {isOverLimit ? '⚠️ Limit exceeded!' : '⚠️ Approaching limit'} + + )} +
+ {children} +
+ ); +}; + export const CharacterCounterPlugin = createPlatePlugin({ key: 'characterCounter', options: { @@ -80,45 +124,7 @@ export const CharacterCounterPlugin = createPlatePlugin({ } }, node: { - component: ({ attributes, children, ...props }: CharacterElementCounterProps) => { - const { getOptions, element } = props; - const options = getOptions(); - const selected = useSelected(); - const { maxLength, showWarning, warningThreshold } = options; - - // Get the character length of only this component's content - const characterLength = useMemo(() => { - return countCharactersInNodes(element.children); - }, [element.children]); - - // Calculate warning state - const warningLength = maxLength * warningThreshold; - const isOverWarning = characterLength >= warningLength; - const isOverLimit = characterLength > maxLength; - - return ( - -
- Character count: {characterLength} / {maxLength} - {showWarning && isOverWarning && ( - - {isOverLimit ? '⚠️ Limit exceeded!' : '⚠️ Approaching limit'} - - )} -
- {children} -
- ); - }, + component: CharacterCounterElement, isElement: true } }); diff --git a/apps/web/src/components/ui/report/plugins/metric-plugin/insert-metric.ts b/apps/web/src/components/ui/report/plugins/metric-plugin/insert-metric.ts index 4936d5d6b..2020a0b3f 100644 --- a/apps/web/src/components/ui/report/plugins/metric-plugin/insert-metric.ts +++ b/apps/web/src/components/ui/report/plugins/metric-plugin/insert-metric.ts @@ -8,6 +8,7 @@ export const insertMetric = (editor: PlateEditor, options?: InsertNodesOptions) { type: CUSTOM_KEYS.metric, metricId: '', + metricVersionNumber: undefined, children: [{ text: '' }] }, { select: true, ...options } diff --git a/apps/web/src/components/ui/report/plugins/metric-plugin/metric-plugin.tsx b/apps/web/src/components/ui/report/plugins/metric-plugin/metric-plugin.tsx index 7e3a7f911..9c6d90849 100644 --- a/apps/web/src/components/ui/report/plugins/metric-plugin/metric-plugin.tsx +++ b/apps/web/src/components/ui/report/plugins/metric-plugin/metric-plugin.tsx @@ -7,6 +7,7 @@ export type MetricPluginOptions = { openMetricModal: boolean; }; +// eslint-disable-next-line @typescript-eslint/no-empty-object-type export type MetricPluginApi = { // the methods are defined in the extendApi function }; diff --git a/apps/web/src/context/Metrics/metricDropdownItems/index.ts b/apps/web/src/context/Metrics/metricDropdownItems/index.ts new file mode 100644 index 000000000..0a9dad696 --- /dev/null +++ b/apps/web/src/context/Metrics/metricDropdownItems/index.ts @@ -0,0 +1,3 @@ +export * from './useDownloadMetricDataCSV'; +export * from './useDownloadMetricDataPNG'; +export * from './useRenameMetricOnPage'; diff --git a/apps/web/src/context/Metrics/metricDropdownItems/useDownloadMetricDataCSV.tsx b/apps/web/src/context/Metrics/metricDropdownItems/useDownloadMetricDataCSV.tsx new file mode 100644 index 000000000..3b4ce27e5 --- /dev/null +++ b/apps/web/src/context/Metrics/metricDropdownItems/useDownloadMetricDataCSV.tsx @@ -0,0 +1,37 @@ +import { useGetMetric, useGetMetricData } from '@/api/buster_rest/metrics'; +import { Download4 } from '@/components/ui/icons'; +import { exportJSONToCSV } from '@/lib/exportUtils'; +import { useMemo, useState } from 'react'; + +export const useDownloadMetricDataCSV = ({ + metricId, + metricVersionNumber +}: { + metricId: string; + metricVersionNumber: number | undefined; +}) => { + const [isDownloading, setIsDownloading] = useState(false); + const { data: metricData } = useGetMetricData( + { id: metricId, versionNumber: metricVersionNumber }, + { enabled: false } + ); + const { data: name } = useGetMetric({ id: metricId }, { select: (x) => x.name }); + + return useMemo( + () => ({ + label: 'Download as CSV', + value: 'download-csv', + icon: , + loading: isDownloading, + onClick: async () => { + const data = metricData?.data; + if (data && name) { + setIsDownloading(true); + await exportJSONToCSV(data, name); + setIsDownloading(false); + } + } + }), + [metricData, isDownloading, name] + ); +}; diff --git a/apps/web/src/context/Metrics/metricDropdownItems/useDownloadMetricDataPNG.tsx b/apps/web/src/context/Metrics/metricDropdownItems/useDownloadMetricDataPNG.tsx new file mode 100644 index 000000000..a9578d2da --- /dev/null +++ b/apps/web/src/context/Metrics/metricDropdownItems/useDownloadMetricDataPNG.tsx @@ -0,0 +1,48 @@ +import { useGetMetric } from '@/api/buster_rest/metrics'; +import { SquareChart } from '@/components/ui/icons'; +import { useBusterNotifications } from '@/context/BusterNotifications'; +import { METRIC_CHART_CONTAINER_ID } from '@/controllers/MetricController/MetricViewChart/config'; +import { downloadElementToImage } from '@/lib/exportUtils'; +import { useMemo } from 'react'; + +export const useDownloadPNGSelectMenu = ({ + metricId, + metricVersionNumber +}: { + metricId: string; + metricVersionNumber: number | undefined; +}) => { + const { openErrorMessage } = useBusterNotifications(); + const { data: name } = useGetMetric( + { id: metricId, versionNumber: metricVersionNumber }, + { select: (x) => x.name } + ); + const { data: selectedChartType } = useGetMetric( + { id: metricId }, + { select: (x) => x.chart_config?.selectedChartType } + ); + + const canDownload = selectedChartType && selectedChartType !== 'table'; + + return useMemo( + () => ({ + label: 'Download as PNG', + value: 'download-png', + disabled: !canDownload, + icon: , + onClick: async () => { + const node = document.getElementById(METRIC_CHART_CONTAINER_ID(metricId)) as HTMLElement; + if (node) { + try { + return await downloadElementToImage(node, `${name}.png`); + } catch (error) { + console.error(error); + } + } + + openErrorMessage('Failed to download PNG'); + } + }), + [canDownload, metricId, name, openErrorMessage] + ); +}; diff --git a/apps/web/src/context/Metrics/metricDropdownItems/useRenameMetricOnPage.tsx b/apps/web/src/context/Metrics/metricDropdownItems/useRenameMetricOnPage.tsx new file mode 100644 index 000000000..db3d50136 --- /dev/null +++ b/apps/web/src/context/Metrics/metricDropdownItems/useRenameMetricOnPage.tsx @@ -0,0 +1,68 @@ +import { Pencil } from '@/components/ui/icons'; +import { useAppLayoutContextSelector } from '@/context/BusterAppLayout'; +import { METRIC_CHART_TITLE_INPUT_ID } from '@/controllers/MetricController/MetricViewChart/MetricViewChartHeader'; +import { useChatLayoutContextSelector } from '@/layouts/ChatLayout/ChatLayoutContext'; +import { assetParamsToRoute } from '@/lib/assets'; +import { ensureElementExists } from '@/lib/element'; +import { timeout } from '@/lib/timeout'; +import { useMemo } from 'react'; + +export const useRenameMetricOnPage = ({ + metricId, + metricVersionNumber +}: { + metricId: string; + metricVersionNumber: number | undefined; +}) => { + const onSetFileView = useChatLayoutContextSelector((x) => x.onSetFileView); + const onChangePage = useAppLayoutContextSelector((x) => x.onChangePage); + const { chatId, dashboardId, reportId, dashboardVersionNumber, reportVersionNumber } = + useChatLayoutContextSelector((x) => ({ + chatId: x.chatId, + dashboardId: x.dashboardId, + reportId: x.reportId, + dashboardVersionNumber: x.dashboardVersionNumber, + reportVersionNumber: x.reportVersionNumber + })); + + return useMemo( + () => ({ + label: 'Rename metric', + value: 'rename-metric', + icon: , + onClick: async () => { + const route = assetParamsToRoute({ + type: 'metric', + assetId: metricId, + chatId, + dashboardId, + reportId, + dashboardVersionNumber, + reportVersionNumber, + metricVersionNumber, + page: 'chart' + }); + await onChangePage(route); + await timeout(100); + const input = await ensureElementExists( + () => document.getElementById(METRIC_CHART_TITLE_INPUT_ID) as HTMLInputElement + ); + if (input) { + input.focus(); + input.select(); + } + } + }), + [ + onSetFileView, + metricId, + metricVersionNumber, + onChangePage, + chatId, + dashboardId, + reportId, + dashboardVersionNumber, + reportVersionNumber + ] + ); +}; diff --git a/apps/web/src/controllers/DashboardController/DashboardViewDashboardController/DashboardContentController/DashboardMetricItem/DashboardMetricItem.tsx b/apps/web/src/controllers/DashboardController/DashboardViewDashboardController/DashboardContentController/DashboardMetricItem/DashboardMetricItem.tsx index e5ca82864..ad0129515 100644 --- a/apps/web/src/controllers/DashboardController/DashboardViewDashboardController/DashboardContentController/DashboardMetricItem/DashboardMetricItem.tsx +++ b/apps/web/src/controllers/DashboardController/DashboardViewDashboardController/DashboardContentController/DashboardMetricItem/DashboardMetricItem.tsx @@ -7,7 +7,7 @@ import { assetParamsToRoute } from '@/lib/assets'; import { MetricCard } from '@/components/ui/metric'; import { useContext } from 'use-context-selector'; import { SortableItemContext } from '@/components/ui/grid/SortableItemContext'; -import { metricCardThreeDotMenuItems } from './metricCardThreeDotMenuItems'; +import { useMetricCardThreeDotMenuItems } from './metricCardThreeDotMenuItems'; const DashboardMetricItemBase: React.FC<{ metricId: string; @@ -63,9 +63,7 @@ const DashboardMetricItemBase: React.FC<{ }); }, [metricId, chatId, dashboardId]); - const threeDotMenuItems = useMemo(() => { - return metricCardThreeDotMenuItems({ dashboardId, metricId }); - }, [dashboardId, metricId]); + const threeDotMenuItems = useMetricCardThreeDotMenuItems({ dashboardId, metricId }); const onInitialAnimationEndPreflight = useMemoizedFn(() => { setInitialAnimationEnded(metricId); diff --git a/apps/web/src/controllers/DashboardController/DashboardViewDashboardController/DashboardContentController/DashboardMetricItem/metricCardThreeDotMenuItems.tsx b/apps/web/src/controllers/DashboardController/DashboardViewDashboardController/DashboardContentController/DashboardMetricItem/metricCardThreeDotMenuItems.tsx index 5b8ae20b6..281ae8119 100644 --- a/apps/web/src/controllers/DashboardController/DashboardViewDashboardController/DashboardContentController/DashboardMetricItem/metricCardThreeDotMenuItems.tsx +++ b/apps/web/src/controllers/DashboardController/DashboardViewDashboardController/DashboardContentController/DashboardMetricItem/metricCardThreeDotMenuItems.tsx @@ -1,18 +1,9 @@ 'use client'; -import React, { useMemo, useState } from 'react'; +import React, { useMemo } from 'react'; import { useRemoveMetricsFromDashboard } from '@/api/buster_rest/dashboards'; -import { Button } from '@/components/ui/buttons'; -import { Dropdown, type DropdownItems, type DropdownItem } from '@/components/ui/dropdown'; -import { - DotsVertical, - Trash, - ShareRight, - PenSparkle, - SquareChartPen, - Code -} from '@/components/ui/icons'; -import { cn } from '@/lib/utils'; +import { type DropdownItems, type DropdownItem } from '@/components/ui/dropdown'; +import { Trash, ShareRight, PenSparkle, SquareChartPen, Code } from '@/components/ui/icons'; import { ASSET_ICONS } from '@/components/features/config/assetIcons'; import { assetParamsToRoute } from '@/lib/assets/assetParamsToRoute'; import { useChatLayoutContextSelector } from '@/layouts/ChatLayout'; @@ -28,7 +19,7 @@ import { useMetricDrilldownItem } from '@/components/features/metrics/ThreeDotMenu'; -export const metricCardThreeDotMenuItems = ({ +export const useMetricCardThreeDotMenuItems = ({ dashboardId, metricId }: { diff --git a/apps/web/src/layouts/ChatLayout/FileContainer/FileContainerHeader/MetricContainerHeaderButtons/MetricThreeDotMenu.tsx b/apps/web/src/layouts/ChatLayout/FileContainer/FileContainerHeader/MetricContainerHeaderButtons/MetricThreeDotMenu.tsx index 871527a2c..d8ba0a643 100644 --- a/apps/web/src/layouts/ChatLayout/FileContainer/FileContainerHeader/MetricContainerHeaderButtons/MetricThreeDotMenu.tsx +++ b/apps/web/src/layouts/ChatLayout/FileContainer/FileContainerHeader/MetricContainerHeaderButtons/MetricThreeDotMenu.tsx @@ -60,6 +60,11 @@ import { useVersionHistorySelectMenu, useMetricDrilldownItem } from '@/components/features/metrics/ThreeDotMenu'; +import { + useDownloadMetricDataCSV, + useDownloadPNGSelectMenu, + useRenameMetricOnPage +} from '@/context/Metrics/metricDropdownItems'; export const ThreeDotMenuButton = React.memo( ({ @@ -82,10 +87,19 @@ export const ThreeDotMenuButton = React.memo( const editChartMenu = useEditChartSelectMenu(); const resultsViewMenu = useResultsViewSelectMenu({ chatId, metricId }); const sqlEditorMenu = useSQLEditorSelectMenu({ chatId, metricId }); - const downloadCSVMenu = useDownloadCSVSelectMenu({ metricId }); - const downloadPNGMenu = useDownloadPNGSelectMenu({ metricId }); + const downloadCSVMenu = useDownloadMetricDataCSV({ + metricId, + metricVersionNumber: versionNumber + }); + const downloadPNGMenu = useDownloadPNGSelectMenu({ + metricId, + metricVersionNumber: versionNumber + }); const deleteMetricMenu = useDeleteMetricSelectMenu({ metricId }); - const renameMetricMenu = useRenameMetricSelectMenu({ metricId }); + const renameMetricMenu = useRenameMetricOnPage({ + metricId, + metricVersionNumber: versionNumber + }); const shareMenu = useShareMenuSelectMenu({ metricId }); const drilldownItem = useMetricDrilldownItem({ metricId }); @@ -359,63 +373,6 @@ const useSQLEditorSelectMenu = ({ ); }; -const useDownloadCSVSelectMenu = ({ metricId }: { metricId: string }) => { - const [isDownloading, setIsDownloading] = useState(false); - const { data: metricData } = useGetMetricData({ id: metricId }, { enabled: false }); - const { data: name } = useGetMetric({ id: metricId }, { select: (x) => x.name }); - - return useMemo( - () => ({ - label: 'Download as CSV', - value: 'download-csv', - icon: , - loading: isDownloading, - onClick: async () => { - const data = metricData?.data; - if (data && name) { - setIsDownloading(true); - await exportJSONToCSV(data, name); - setIsDownloading(false); - } - } - }), - [metricData, isDownloading, name] - ); -}; - -const useDownloadPNGSelectMenu = ({ metricId }: { metricId: string }) => { - const { openErrorMessage } = useBusterNotifications(); - const { data: name } = useGetMetric({ id: metricId }, { select: (x) => x.name }); - const { data: selectedChartType } = useGetMetric( - { id: metricId }, - { select: (x) => x.chart_config?.selectedChartType } - ); - - const canDownload = selectedChartType && selectedChartType !== 'table'; - - return useMemo( - () => ({ - label: 'Download as PNG', - value: 'download-png', - disabled: !canDownload, - icon: , - onClick: async () => { - const node = document.getElementById(METRIC_CHART_CONTAINER_ID(metricId)) as HTMLElement; - if (node) { - try { - return await downloadElementToImage(node, `${name}.png`); - } catch (error) { - console.error(error); - } - } - - openErrorMessage('Failed to download PNG'); - } - }), - [canDownload, metricId, name, openErrorMessage] - ); -}; - const useDeleteMetricSelectMenu = ({ metricId }: { metricId: string }) => { const { mutateAsync: deleteMetric } = useDeleteMetric(); const onChangePage = useAppLayoutContextSelector((x) => x.onChangePage); @@ -434,39 +391,6 @@ const useDeleteMetricSelectMenu = ({ metricId }: { metricId: string }) => { ); }; -const useRenameMetricSelectMenu = ({ metricId }: { metricId: string }) => { - const onSetFileView = useChatLayoutContextSelector((x) => x.onSetFileView); - const onChangePage = useAppLayoutContextSelector((x) => x.onChangePage); - const chatId = useChatLayoutContextSelector((x) => x.chatId); - const dashboardId = useChatLayoutContextSelector((x) => x.dashboardId); - return useMemo( - () => ({ - label: 'Rename metric', - value: 'rename-metric', - icon: , - onClick: async () => { - const route = assetParamsToRoute({ - type: 'metric', - assetId: metricId, - chatId, - dashboardId, - page: 'chart' - }); - await onChangePage(route); - await timeout(100); - const input = await ensureElementExists( - () => document.getElementById(METRIC_CHART_TITLE_INPUT_ID) as HTMLInputElement - ); - if (input) { - input.focus(); - input.select(); - } - } - }), - [onSetFileView] - ); -}; - export const useShareMenuSelectMenu = ({ metricId }: { metricId: string }) => { const { data: shareAssetConfig } = useGetMetric( { id: metricId }, diff --git a/apps/web/src/routes/busterRoutes/busterAppRoutes.ts b/apps/web/src/routes/busterRoutes/busterAppRoutes.ts index f627da223..d8dec99af 100644 --- a/apps/web/src/routes/busterRoutes/busterAppRoutes.ts +++ b/apps/web/src/routes/busterRoutes/busterAppRoutes.ts @@ -252,6 +252,15 @@ export type BusterAppRoutesWithArgs = { metricVersionNumber?: number; reportVersionNumber?: number; }; + [BusterAppRoutes.APP_CHAT_ID_REPORT_ID_METRIC_ID_RESULTS]: { + route: BusterAppRoutes.APP_CHAT_ID_REPORT_ID_METRIC_ID_RESULTS; + chatId: string; + reportId: string; + metricId: string; + metricVersionNumber?: number; + reportVersionNumber?: number; + secondaryView?: MetricFileViewSecondary; + }; [BusterAppRoutes.APP_CHAT_ID_DATASET_ID]: { route: BusterAppRoutes.APP_CHAT_ID_DATASET_ID; chatId: string;