mirror of https://github.com/buster-so/buster.git
add a three dot dropdown
This commit is contained in:
parent
5eac58f805
commit
34fd70b117
|
@ -35,6 +35,10 @@ export type BusterMetric = {
|
|||
id: string;
|
||||
name: string;
|
||||
}[];
|
||||
versions: {
|
||||
version_number: number;
|
||||
updated_at: string;
|
||||
}[];
|
||||
} & BusterShare;
|
||||
|
||||
export type DataMetadata = {
|
||||
|
|
|
@ -320,15 +320,14 @@ export const WithSecondaryLabel: Story = {
|
|||
value: '1',
|
||||
label: 'Profile Settings',
|
||||
secondaryLabel: 'User preferences',
|
||||
onClick: fn(),
|
||||
icon: <PaintRoller />
|
||||
onClick: fn()
|
||||
},
|
||||
{
|
||||
value: '2',
|
||||
label: 'Storage',
|
||||
secondaryLabel: '45GB used',
|
||||
onClick: fn(),
|
||||
icon: <Storage />
|
||||
selected: true
|
||||
},
|
||||
{ type: 'divider' },
|
||||
{
|
||||
|
|
|
@ -325,9 +325,9 @@ const DropdownItem = <T,>({
|
|||
<>
|
||||
{icon && !loading && <span className="text-icon-color">{icon}</span>}
|
||||
|
||||
<div className={cn('flex flex-col gap-y-1', truncate && 'overflow-hidden')}>
|
||||
<div className={cn('flex flex-col space-y-2', truncate && 'overflow-hidden')}>
|
||||
<span className={cn(truncate && 'truncate')}>{label}</span>
|
||||
{secondaryLabel && <span className="text-gray-light text2xs">{secondaryLabel}</span>}
|
||||
{secondaryLabel && <span className="text-gray-light text-xs">{secondaryLabel}</span>}
|
||||
</div>
|
||||
{loading && <CircleSpinnerLoader size={9} />}
|
||||
{shortcut && <DropdownMenuShortcut>{shortcut}</DropdownMenuShortcut>}
|
||||
|
@ -367,7 +367,8 @@ const DropdownItem = <T,>({
|
|||
);
|
||||
}
|
||||
|
||||
if (selectType === 'single') {
|
||||
//I do not think this selected check is stable... look into refactoring
|
||||
if (selectType === 'single' || selected) {
|
||||
return (
|
||||
<DropdownMenuCheckboxItemSingle
|
||||
checked={selected}
|
||||
|
@ -426,7 +427,7 @@ const DropdownSubMenuWrapper = <T,>({
|
|||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger>{children}</DropdownMenuSubTrigger>
|
||||
<DropdownMenuPortal>
|
||||
<DropdownMenuSubContent>
|
||||
<DropdownMenuSubContent sideOffset={8}>
|
||||
{items?.map((item, index) => (
|
||||
<DropdownItemSelector
|
||||
key={dropdownItemKey(item, index)}
|
||||
|
|
|
@ -2,13 +2,8 @@
|
|||
|
||||
import * as React from 'react';
|
||||
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
|
||||
import {
|
||||
ArrowRight,
|
||||
ArrowUpRight,
|
||||
CaretRight,
|
||||
Check3 as Check,
|
||||
ChevronRight
|
||||
} from '../icons/NucleoIconOutlined';
|
||||
import { ArrowRight, ArrowUpRight, Check3 as Check } from '../icons/NucleoIconOutlined';
|
||||
import { CaretRight } from '../icons/NucleoIconFilled';
|
||||
import { cn } from '@/lib/classMerge';
|
||||
import { Checkbox } from '../checkbox/Checkbox';
|
||||
import { Button } from '../buttons/Button';
|
||||
|
@ -34,13 +29,14 @@ const DropdownMenuSubTrigger = React.forwardRef<
|
|||
ref={ref}
|
||||
className={cn(
|
||||
'focus:bg-item-hover data-[state=open]:bg-item-hover flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none select-none [&_svg]:pointer-events-none [&_svg]:shrink-0',
|
||||
'dropdown-item mx-1 [&.dropdown-item:first-child]:mt-1! [&.dropdown-item:has(+.dropdown-separator)]:mb-1 [&.dropdown-item:has(~.dropdown-separator)]:mt-1 [&.dropdown-item:last-child]:mb-1!',
|
||||
inset && 'pl-8',
|
||||
className
|
||||
)}
|
||||
{...props}>
|
||||
{children}
|
||||
<div className="ml-auto !text-sm">
|
||||
<ChevronRight />
|
||||
<div className="text-2xs text-icon-color ml-auto">
|
||||
<CaretRight />
|
||||
</div>
|
||||
</DropdownMenuPrimitive.SubTrigger>
|
||||
));
|
||||
|
@ -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}
|
||||
<span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<div className="flex h-4 w-4 items-center justify-center">
|
||||
<div className="text-icon-color flex h-4 w-4 items-center justify-center text-sm">
|
||||
<Check />
|
||||
</div>
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
|
|
|
@ -21,3 +21,4 @@ export * from './useUpdateLayoutEffect';
|
|||
export * from './useScroll';
|
||||
export * from './useUpdateEffect';
|
||||
export * from './useWhyDidYouUpdate';
|
||||
export * from './useSetInterval';
|
||||
|
|
|
@ -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<NodeJS.Timeout>();
|
||||
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;
|
||||
}
|
|
@ -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<FileContainerButtonsProps> = React.memo(() => {
|
||||
const renderViewLayoutKey = useChatLayoutContextSelector((x) => x.renderViewLayoutKey);
|
||||
|
@ -111,33 +112,3 @@ const ShareMetricButtonLocal = React.memo(({ metricId }: { metricId: string }) =
|
|||
return <ShareMetricButton metricId={metricId} />;
|
||||
});
|
||||
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: <Trash />,
|
||||
loading: isDeletingMetric,
|
||||
onClick: async () => {
|
||||
await deleteMetric({ ids: [metricId] });
|
||||
openSuccessMessage('Metric deleted');
|
||||
onSetSelectedFile(null);
|
||||
}
|
||||
}
|
||||
],
|
||||
[deleteMetric, isDeletingMetric, metricId, openSuccessMessage, onSetSelectedFile]
|
||||
);
|
||||
|
||||
return (
|
||||
<Dropdown items={items}>
|
||||
<Button prefix={<Dots />} variant="ghost" />
|
||||
</Dropdown>
|
||||
);
|
||||
});
|
||||
ThreeDotMenuButton.displayName = 'ThreeDotMenuButton';
|
|
@ -0,0 +1,93 @@
|
|||
import { useDeleteMetric, useGetMetric } from '@/api/buster_rest/metrics';
|
||||
import { DropdownItems } from '@/components/ui/dropdown';
|
||||
import { Trash, Dots, Pencil, Chart, SquareChart, Download4, History } from '@/components/ui/icons';
|
||||
import { useBusterNotifications } from '@/context/BusterNotifications';
|
||||
import { useChatLayoutContextSelector } from '@/layouts/ChatLayout/ChatLayoutContext';
|
||||
import { useMemo } from 'react';
|
||||
import { Dropdown } from '@/components/ui/dropdown';
|
||||
import { Button } from '@/components/ui/buttons';
|
||||
import React from 'react';
|
||||
import { timeFromNow } from '@/lib/date';
|
||||
|
||||
export const ThreeDotMenuButton = React.memo(({ metricId }: { metricId: string }) => {
|
||||
const { mutateAsync: deleteMetric, isPending: isDeletingMetric } = useDeleteMetric();
|
||||
const { openSuccessMessage } = useBusterNotifications();
|
||||
const onSetSelectedFile = useChatLayoutContextSelector((x) => x.onSetSelectedFile);
|
||||
const { data } = useGetMetric(metricId, (x) => ({
|
||||
versions: x.versions,
|
||||
version_number: x.version_number
|
||||
}));
|
||||
const { versions = [], version_number } = data || {};
|
||||
|
||||
const versionHistoryItems: DropdownItems = useMemo(() => {
|
||||
return versions.map((x) => ({
|
||||
label: `Version ${x.version_number}`,
|
||||
secondaryLabel: timeFromNow(x.updated_at, false),
|
||||
value: x.version_number.toString(),
|
||||
selected: x.version_number === version_number
|
||||
}));
|
||||
}, [versions, version_number]);
|
||||
|
||||
const items: DropdownItems = useMemo(
|
||||
() => [
|
||||
{
|
||||
label: 'Version history',
|
||||
value: 'version-history',
|
||||
icon: <History />,
|
||||
items: versionHistoryItems
|
||||
},
|
||||
{ type: 'divider' },
|
||||
{
|
||||
label: 'Download as CSV',
|
||||
value: 'download-csv',
|
||||
icon: <Download4 />,
|
||||
onClick: () => {
|
||||
console.log('download csv');
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Download as PNG',
|
||||
value: 'download-png',
|
||||
icon: <SquareChart />,
|
||||
onClick: () => {
|
||||
console.log('download png');
|
||||
}
|
||||
},
|
||||
{ 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);
|
||||
}
|
||||
}
|
||||
],
|
||||
[
|
||||
deleteMetric,
|
||||
isDeletingMetric,
|
||||
metricId,
|
||||
openSuccessMessage,
|
||||
onSetSelectedFile,
|
||||
versionHistoryItems
|
||||
]
|
||||
);
|
||||
|
||||
return (
|
||||
<Dropdown items={items} side="bottom" align="end">
|
||||
<Button prefix={<Dots />} variant="ghost" />
|
||||
</Dropdown>
|
||||
);
|
||||
});
|
||||
ThreeDotMenuButton.displayName = 'ThreeDotMenuButton';
|
|
@ -0,0 +1 @@
|
|||
export * from './MetricContainerHeaderButtons';
|
|
@ -107,6 +107,7 @@ const extractDateForFormatting = (
|
|||
if (isDate(date)) return new Date(date);
|
||||
return String(date);
|
||||
};
|
||||
|
||||
export const formatDate = ({
|
||||
date,
|
||||
format,
|
||||
|
|
Loading…
Reference in New Issue