From 34fd70b1178e1bbd6a9fb27f48f0f41b566b7b79 Mon Sep 17 00:00:00 2001 From: Nate Kelley Date: Fri, 14 Mar 2025 15:00:37 -0600 Subject: [PATCH] add a three dot dropdown --- .../api/asset_interfaces/metric/interfaces.ts | 4 + .../ui/dropdown/Dropdown.stories.tsx | 5 +- web/src/components/ui/dropdown/Dropdown.tsx | 9 +- .../components/ui/dropdown/DropdownBase.tsx | 20 ++-- web/src/hooks/index.ts | 1 + web/src/hooks/useSetInterval.ts | 60 ++++++++++++ .../MetricContainerHeaderButtons.tsx | 51 +++------- .../MetricThreeDotMenu.tsx | 93 +++++++++++++++++++ .../MetricContainerHeaderButtons/index.ts | 1 + web/src/lib/date.ts | 1 + 10 files changed, 186 insertions(+), 59 deletions(-) create mode 100644 web/src/hooks/useSetInterval.ts rename web/src/layouts/ChatLayout/FileContainer/FileContainerHeader/{ => MetricContainerHeaderButtons}/MetricContainerHeaderButtons.tsx (69%) create mode 100644 web/src/layouts/ChatLayout/FileContainer/FileContainerHeader/MetricContainerHeaderButtons/MetricThreeDotMenu.tsx create mode 100644 web/src/layouts/ChatLayout/FileContainer/FileContainerHeader/MetricContainerHeaderButtons/index.ts diff --git a/web/src/api/asset_interfaces/metric/interfaces.ts b/web/src/api/asset_interfaces/metric/interfaces.ts index 726f141bb..5ee2b9fa4 100644 --- a/web/src/api/asset_interfaces/metric/interfaces.ts +++ b/web/src/api/asset_interfaces/metric/interfaces.ts @@ -35,6 +35,10 @@ export type BusterMetric = { id: string; name: string; }[]; + versions: { + version_number: number; + updated_at: string; + }[]; } & BusterShare; export type DataMetadata = { diff --git a/web/src/components/ui/dropdown/Dropdown.stories.tsx b/web/src/components/ui/dropdown/Dropdown.stories.tsx index e4ccf7cff..a5b1e43f7 100644 --- a/web/src/components/ui/dropdown/Dropdown.stories.tsx +++ b/web/src/components/ui/dropdown/Dropdown.stories.tsx @@ -320,15 +320,14 @@ export const WithSecondaryLabel: Story = { value: '1', label: 'Profile Settings', secondaryLabel: 'User preferences', - onClick: fn(), - icon: + onClick: fn() }, { value: '2', label: 'Storage', secondaryLabel: '45GB used', onClick: fn(), - icon: + selected: true }, { type: 'divider' }, { diff --git a/web/src/components/ui/dropdown/Dropdown.tsx b/web/src/components/ui/dropdown/Dropdown.tsx index 435fcece4..e54e50949 100644 --- a/web/src/components/ui/dropdown/Dropdown.tsx +++ b/web/src/components/ui/dropdown/Dropdown.tsx @@ -325,9 +325,9 @@ const DropdownItem = ({ <> {icon && !loading && {icon}} -
+
{label} - {secondaryLabel && {secondaryLabel}} + {secondaryLabel && {secondaryLabel}}
{loading && } {shortcut && {shortcut}} @@ -367,7 +367,8 @@ const DropdownItem = ({ ); } - if (selectType === 'single') { + //I do not think this selected check is stable... look into refactoring + if (selectType === 'single' || selected) { return ( ({ {children} - + {items?.map((item, index) => ( {children} -
- +
+
)); @@ -49,7 +45,7 @@ DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayNam const baseContentClass = cn( `data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] overflow-hidden `, 'bg-background text-foreground ', - 'rounded-md border p-1' + 'rounded-md border' ); const DropdownMenuSubContent = React.forwardRef< @@ -126,7 +122,7 @@ DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName; const itemClass = cn( 'focus:bg-item-hover focus:text-foreground', - 'relative flex cursor-pointer items-center rounded-sm py-1.5 text-sm outline-none select-none', + 'relative flex cursor-pointer items-center rounded-sm py-1.5 text-base outline-none select-none', 'data-[disabled]:pointer-events-none data-[disabled]:opacity-50', 'gap-1.5', 'mx-1 dropdown-item [&.dropdown-item:has(+.dropdown-separator)]:mb-1 [&.dropdown-item:has(~.dropdown-separator)]:mt-1 [&.dropdown-item:first-child]:mt-1! [&.dropdown-item:last-child]:mb-1!' @@ -159,7 +155,7 @@ const DropdownMenuCheckboxItemSingle = React.forwardRef< {children} -
+
diff --git a/web/src/hooks/index.ts b/web/src/hooks/index.ts index c1a4ca807..63cceef6a 100644 --- a/web/src/hooks/index.ts +++ b/web/src/hooks/index.ts @@ -21,3 +21,4 @@ export * from './useUpdateLayoutEffect'; export * from './useScroll'; export * from './useUpdateEffect'; export * from './useWhyDidYouUpdate'; +export * from './useSetInterval'; diff --git a/web/src/hooks/useSetInterval.ts b/web/src/hooks/useSetInterval.ts new file mode 100644 index 000000000..ca4dda1fa --- /dev/null +++ b/web/src/hooks/useSetInterval.ts @@ -0,0 +1,60 @@ +'use client'; + +import { useEffect, useRef, useState, useCallback } from 'react'; +import { useMemoizedFn } from './useMemoizedFn'; + +/** + * A hook that provides a safe way to use setInterval in React components. + * The interval will be automatically cleared when the component unmounts. + * The callback and delay will be properly updated when they change. + * + * @param callback The function to be called at each interval + * @param delay The delay in milliseconds between each call. If null, the interval is paused + * @returns An object containing functions to control the interval + * + * @example + * ```tsx + * const { start, stop, isActive } = useSetInterval(() => { + * console.log('This runs every second'); + * }, 1000); + * ``` + */ +export function useSetInterval(callback: () => void, delay: number | null) { + const intervalRef = useRef(); + const savedCallback = useMemoizedFn(callback); + const [isActive, setIsActive] = useState(false); + + useEffect(() => { + if (delay !== null) { + setIsActive(true); + intervalRef.current = setInterval(() => savedCallback(), delay); + return () => { + if (intervalRef.current) { + clearInterval(intervalRef.current); + setIsActive(false); + } + }; + } else { + if (intervalRef.current) { + clearInterval(intervalRef.current); + setIsActive(false); + } + } + }, [delay, savedCallback]); + + const start = useCallback(() => { + if (!isActive && delay !== null) { + setIsActive(true); + intervalRef.current = setInterval(() => savedCallback(), delay); + } + }, [delay, isActive, savedCallback]); + + const stop = useCallback(() => { + if (intervalRef.current) { + clearInterval(intervalRef.current); + setIsActive(false); + } + }, []); + + return { start, stop, isActive } as const; +} diff --git a/web/src/layouts/ChatLayout/FileContainer/FileContainerHeader/MetricContainerHeaderButtons.tsx b/web/src/layouts/ChatLayout/FileContainer/FileContainerHeader/MetricContainerHeaderButtons/MetricContainerHeaderButtons.tsx similarity index 69% rename from web/src/layouts/ChatLayout/FileContainer/FileContainerHeader/MetricContainerHeaderButtons.tsx rename to web/src/layouts/ChatLayout/FileContainer/FileContainerHeader/MetricContainerHeaderButtons/MetricContainerHeaderButtons.tsx index 221c9fd5f..424bf36f0 100644 --- a/web/src/layouts/ChatLayout/FileContainer/FileContainerHeader/MetricContainerHeaderButtons.tsx +++ b/web/src/layouts/ChatLayout/FileContainer/FileContainerHeader/MetricContainerHeaderButtons/MetricContainerHeaderButtons.tsx @@ -1,15 +1,15 @@ import React, { useMemo } from 'react'; -import { FileContainerButtonsProps } from './interfaces'; -import { MetricFileViewSecondary, useChatLayoutContextSelector } from '../../ChatLayoutContext'; +import { FileContainerButtonsProps } from '../interfaces'; +import { MetricFileViewSecondary, useChatLayoutContextSelector } from '../../../ChatLayoutContext'; import { useMemoizedFn } from '@/hooks'; -import { useChatIndividualContextSelector } from '../../ChatContext'; -import { HideButtonContainer } from './HideButtonContainer'; -import { FileButtonContainer } from './FileButtonContainer'; -import { CreateChatButton } from './CreateChatButtont'; -import { SelectableButton } from './SelectableButton'; -import { SaveMetricToCollectionButton } from '../../../../components/features/buttons/SaveMetricToCollectionButton'; -import { SaveMetricToDashboardButton } from '../../../../components/features/buttons/SaveMetricToDashboardButton'; -import { ShareMetricButton } from '../../../../components/features/buttons/ShareMetricButton'; +import { useChatIndividualContextSelector } from '../../../ChatContext'; +import { HideButtonContainer } from '../HideButtonContainer'; +import { FileButtonContainer } from '../FileButtonContainer'; +import { CreateChatButton } from '../CreateChatButtont'; +import { SelectableButton } from '../SelectableButton'; +import { SaveMetricToCollectionButton } from '../../../../../components/features/buttons/SaveMetricToCollectionButton'; +import { SaveMetricToDashboardButton } from '../../../../../components/features/buttons/SaveMetricToDashboardButton'; +import { ShareMetricButton } from '../../../../../components/features/buttons/ShareMetricButton'; import { Code3, Dots, @@ -22,6 +22,7 @@ import { useDeleteMetric, useGetMetric } from '@/api/buster_rest/metrics'; import { Button } from '@/components/ui/buttons'; import { Dropdown, DropdownItems } from '@/components/ui/dropdown'; import { useBusterNotifications } from '@/context/BusterNotifications'; +import { ThreeDotMenuButton } from './MetricThreeDotMenu'; export const MetricContainerHeaderButtons: React.FC = React.memo(() => { const renderViewLayoutKey = useChatLayoutContextSelector((x) => x.renderViewLayoutKey); @@ -111,33 +112,3 @@ const ShareMetricButtonLocal = React.memo(({ metricId }: { metricId: string }) = return ; }); ShareMetricButtonLocal.displayName = 'ShareMetricButtonLocal'; - -const ThreeDotMenuButton = React.memo(({ metricId }: { metricId: string }) => { - const { mutateAsync: deleteMetric, isPending: isDeletingMetric } = useDeleteMetric(); - const { openSuccessMessage } = useBusterNotifications(); - const onSetSelectedFile = useChatLayoutContextSelector((x) => x.onSetSelectedFile); - - const items: DropdownItems = useMemo( - () => [ - { - label: 'Delete', - value: 'delete', - icon: , - loading: isDeletingMetric, - onClick: async () => { - await deleteMetric({ ids: [metricId] }); - openSuccessMessage('Metric deleted'); - onSetSelectedFile(null); - } - } - ], - [deleteMetric, isDeletingMetric, metricId, openSuccessMessage, onSetSelectedFile] - ); - - return ( - -