add metric modal updates

This commit is contained in:
Nate Kelley 2025-03-21 13:27:07 -06:00
parent dec98bacfe
commit a625a8ea64
No known key found for this signature in database
GPG Key ID: FD90372AB8D98B4F
21 changed files with 622 additions and 167 deletions

View File

@ -4,6 +4,7 @@ import type { Preview } from '@storybook/react';
import { initialize, mswLoader } from 'msw-storybook-addon';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { BusterStyleProvider } from '../src/context/BusterStyles/BusterStyles';
import { BusterAssetsProvider } from '../src/context/Assets/BusterAssetsProvider';
import '../src/styles/styles.scss';
initialize();
@ -41,7 +42,9 @@ const preview: Preview = {
return (
<BusterStyleProvider>
<QueryClientProvider client={queryClient}>
<Story />
<BusterAssetsProvider>
<Story />
</BusterAssetsProvider>
</QueryClientProvider>
</BusterStyleProvider>
);

View File

@ -58,7 +58,7 @@ const useGetDashboardAndInitializeMetrics = () => {
});
return useMemoizedFn(async (id: string) => {
const { password } = getAssetPassword(id);
const { password } = getAssetPassword?.(id) || {};
return dashboardsGetDashboard({ id: id!, password }).then((data) => {
initializeMetrics(data.metrics);
@ -72,6 +72,7 @@ export const useGetDashboard = <TData = BusterDashboardResponse>(
select?: (data: BusterDashboardResponse) => TData
) => {
const queryFn = useGetDashboardAndInitializeMetrics();
const queryClient = useQueryClient();
return useQuery({
...dashboardQueryKeys.dashboardGetDashboard(id!),

View File

@ -2,14 +2,12 @@ import mainApi from '@/api/buster_rest/instances';
import type {
DashboardsListRequest,
DashboardCreateRequest,
DashboardUpdateRequest,
DashboardSubscribeRequest
DashboardUpdateRequest
} from '@/api/request_interfaces/dashboards/interfaces';
import type {
BusterDashboardListItem,
BusterDashboardResponse
} from '@/api/asset_interfaces/dashboard';
import { ShareRole } from '@/api/asset_interfaces';
import {
ShareDeleteRequest,
SharePostRequest,
@ -22,7 +20,15 @@ export const dashboardsGetList = async (params: DashboardsListRequest) => {
.then((res) => res.data);
};
export const dashboardsGetDashboard = async ({ id, password }: DashboardSubscribeRequest) => {
export const dashboardsGetDashboard = async ({
id,
password
}: {
/** The unique identifier of the dashboard */
id: string;
/** Optional password for accessing protected dashboards */
password?: string;
}) => {
return await mainApi
.get<BusterDashboardResponse>(`/dashboards/${id}`, { params: { password } })
.then((res) => res.data);

View File

@ -15,16 +15,6 @@ export interface DashboardsListRequest {
only_my_dashboards?: boolean;
}
/**
* Interface for subscribing to a dashboard
*/
export interface DashboardSubscribeRequest {
/** The unique identifier of the dashboard */
id: string;
/** Optional password for accessing protected dashboards */
password?: string;
}
/**
* Interface for unsubscribing from a specific dashboard
*/

View File

@ -80,8 +80,7 @@ export const DataSourceFormContent: React.FC<{
route: BusterRoutes.SETTINGS_DATASOURCES_ID,
datasourceId: res.id
});
},
cancelButtonProps: { className: 'hidden!' }
}
});
}
} catch (error) {

View File

@ -3,13 +3,16 @@ import { AddToDashboardModal } from './AddToDashboardModal';
import { http, HttpResponse } from 'msw';
import { fn } from '@storybook/test';
import { BASE_URL } from '@/api/buster_rest/config';
import { BusterMetricListItem, VerificationStatus } from '@/api/asset_interfaces';
import { BusterMetricListItem } from '@/api/asset_interfaces';
import { createMockListMetric } from '@/mocks/metric';
import { generateMockDashboard } from '@/mocks/MOCK_DASHBOARD';
const mockMetrics: BusterMetricListItem[] = Array.from({ length: 100 }, (_, index) =>
createMockListMetric(`${index + 1}`)
);
const { response } = generateMockDashboard(3, 'dashboard-1');
const meta = {
title: 'Features/Modal/AddToDashboardModal',
component: AddToDashboardModal,
@ -17,7 +20,10 @@ const meta = {
layout: 'centered',
msw: {
handlers: [
http.get(`${BASE_URL}/metrics`, () => {
http.get(`${BASE_URL}/dashboards/dashboard-1`, () => {
return HttpResponse.json(response);
}),
http.get(`${BASE_URL}/metrics?page_token=0&page_size=3000`, () => {
return HttpResponse.json(mockMetrics);
})
]
@ -41,14 +47,5 @@ export const EmptyState: Story = {
open: true,
onClose: fn(),
dashboardId: 'dashboard-1'
},
parameters: {
msw: {
handlers: [
http.get(`${BASE_URL}/metrics`, () => {
return HttpResponse.json([]);
})
]
}
}
};

View File

@ -1,86 +1,111 @@
import { useGetMetricsList } from '@/api/buster_rest/metrics';
import { AppModal } from '@/components/ui/modal';
import { useDebounceSearch } from '@/hooks';
import React, { useState } from 'react';
import { BusterList } from '@/components/ui/list';
import { Input } from '@/components/ui/inputs';
import { useDebounceSearch, useMemoizedFn } from '@/hooks';
import React, { useLayoutEffect, useMemo, useState } from 'react';
import { InputSelectModal, InputSelectModalProps } from '@/components/ui/modal/InputSelectModal';
import { formatDate } from '@/lib';
import { Button } from '@/components/ui/buttons';
import { useGetDashboard } from '@/api/buster_rest/dashboards';
import { useQueryClient } from '@tanstack/react-query';
export const AddToDashboardModal: React.FC<{
open: boolean;
onClose: () => void;
dashboardId: string;
}> = React.memo(({ open, onClose, dashboardId }) => {
const { data: dashboard, isFetched: isFetchedDashboard } = useGetDashboard(dashboardId);
const { data: metrics, isFetched: isFetchedMetrics } = useGetMetricsList({});
const [selectedMetrics, setSelectedMetrics] = useState<string[]>([]);
const { filteredItems, handleSearchChange } = useDebounceSearch({
items: metrics || [],
searchPredicate: (item, searchText) => {
return item.title.toLowerCase().includes(searchText.toLowerCase());
}
});
const columns = [
const columns: InputSelectModalProps['columns'] = [
{
title: 'Metric',
dataIndex: 'title',
width: 300
title: 'Title',
dataIndex: 'title'
},
{
title: 'Last edited',
dataIndex: 'last_edited',
width: 132,
render: (value: string, x) => {
return formatDate({
date: value,
format: 'lll'
});
}
}
];
const rows = filteredItems.map((metric) => ({
id: metric.id,
data: {
title: metric.title
}
}));
const rows = useMemo(() => {
return metrics.map((metric) => ({
id: metric.id,
data: metric
}));
}, [metrics.length]);
const handleAddMetrics = async () => {
const handleAddMetrics = useMemoizedFn(async () => {
// TODO: Implement the API call to add metrics to dashboard
console.log('Adding metrics:', selectedMetrics);
onClose();
};
});
const onSelectChange = useMemoizedFn((items: string[]) => {
setSelectedMetrics(items);
});
const isSelectedChanged = useMemo(() => {
const originalIds = Object.keys(dashboard?.metrics || {});
const newIds = selectedMetrics;
return originalIds.length !== newIds.length || originalIds.some((id) => !newIds.includes(id));
}, [dashboard?.metrics, selectedMetrics]);
const emptyState = useMemo(() => {
if (!isFetchedMetrics || !isFetchedDashboard) {
return 'Loading metrics...';
}
if (rows.length === 0) {
return 'No metrics found';
}
return undefined;
}, [isFetchedMetrics, isFetchedDashboard, rows]);
const footer: NonNullable<InputSelectModalProps['footer']> = useMemo(() => {
return {
left:
selectedMetrics.length > 0 ? (
<Button variant="ghost" onClick={() => setSelectedMetrics([])}>
Clear selected
</Button>
) : undefined,
secondaryButton: {
text: 'Cancel',
onClick: onClose
},
primaryButton: {
text: `Update metrics`,
onClick: handleAddMetrics,
disabled: !isSelectedChanged,
tooltip: isSelectedChanged
? `Adding ${selectedMetrics.length} metrics`
: 'No changes to update'
}
};
}, [selectedMetrics.length, isSelectedChanged, handleAddMetrics]);
useLayoutEffect(() => {
if (isFetchedDashboard) {
const metrics = Object.keys(dashboard?.metrics || {});
setSelectedMetrics(metrics);
}
}, [isFetchedDashboard, dashboard?.metrics]);
return (
<AppModal
<InputSelectModal
open={open}
onClose={onClose}
header={{
title: 'Add Metrics to Dashboard',
description: 'Select metrics to add to your dashboard'
}}
footer={{
primaryButton: {
text: 'Add Selected Metrics',
onClick: handleAddMetrics,
disabled: selectedMetrics.length === 0
},
secondaryButton: {
text: 'Cancel',
onClick: onClose
}
}}>
<div className="flex flex-col gap-4">
<Input
placeholder="Search metrics..."
onChange={(e) => handleSearchChange(e.target.value)}
/>
<div className="h-[400px]">
<BusterList
columns={columns}
rows={rows}
selectedRowKeys={selectedMetrics}
onSelectChange={setSelectedMetrics}
emptyState={
!isFetchedMetrics
? 'Loading metrics...'
: filteredItems.length === 0
? 'No metrics found'
: undefined
}
/>
</div>
</div>
</AppModal>
columns={columns}
rows={rows}
onSelectChange={onSelectChange}
selectedRowKeys={selectedMetrics}
footer={footer}
emptyState={emptyState}
/>
);
});

View File

@ -6,6 +6,7 @@ import { Button } from '../buttons/Button';
import React from 'react';
import { ModalProps } from './AppModal';
import { fn } from '@storybook/test';
const meta: Meta<typeof AppModal> = {
title: 'UI/Modal/AppModal',
component: AppModal,

View File

@ -22,7 +22,7 @@ export interface ModalProps {
left?: React.ReactNode;
primaryButton: {
text: string;
onClick: () => Promise<void> | (() => void);
onClick: (() => Promise<void>) | (() => void);
variant?: ButtonProps['variant'];
loading?: boolean;
disabled?: boolean;
@ -46,7 +46,6 @@ export interface ModalProps {
export const AppModal: React.FC<ModalProps> = React.memo(
({ open, onClose, footer, header, width = 600, className, style, children }) => {
const [isLoadingPrimaryButton, setIsLoadingPrimaryButton] = useState(false);
const [isLoadingSecondaryButton, setIsLoadingSecondaryButton] = useState(false);
const onOpenChange = useMemoizedFn((open: boolean) => {
if (!open) {
onClose();
@ -65,22 +64,21 @@ export const AppModal: React.FC<ModalProps> = React.memo(
const onPrimaryButtonClickPreflight = useMemoizedFn(async () => {
setIsLoadingPrimaryButton(true);
await footer.primaryButton.onClick();
setIsLoadingPrimaryButton(false);
});
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className={className} style={memoizedStyle}>
<div className="flex flex-col gap-4 overflow-hidden">
<div className="flex flex-col gap-4 overflow-hidden p-6">
{header && (
<DialogHeader className="px-6 pt-6">
<DialogHeader className="">
{header.title && <DialogTitle>{header.title}</DialogTitle>}
{header.description && <DialogDescription>{header.description}</DialogDescription>}
</DialogHeader>
)}
<div className="px-6"> {children}</div>
{children}
</div>
{footer && (

View File

@ -0,0 +1,113 @@
import type { Meta, StoryObj } from '@storybook/react';
import { BorderedModal } from './BorderedModal';
import { useState } from 'react';
const meta: Meta<typeof BorderedModal> = {
title: 'UI/Modal/ScrollableModal',
component: BorderedModal,
parameters: {
layout: 'centered'
},
tags: ['autodocs']
};
export default meta;
type Story = StoryObj<typeof BorderedModal>;
// Wrapper component to handle state
const ScrollableModalWrapper = (args: any) => {
const [open, setOpen] = useState(true);
return <BorderedModal {...args} open={open} onOpenChange={setOpen} />;
};
export const Basic: Story = {
render: (args) => <ScrollableModalWrapper {...args} />,
args: {
header: {
title: 'Example Modal',
description: 'This is a basic example of the ScrollableModal component'
},
children: (
<div className="space-y-4 py-4">
{Array.from({ length: 20 }).map((_, i) => (
<p key={i}>This is paragraph {i + 1} demonstrating scrollable content in the modal.</p>
))}
</div>
),
footer: {
primaryButton: {
text: 'Save Changes',
onClick: () => console.log('Primary button clicked')
},
secondaryButton: {
text: 'Cancel',
onClick: () => console.log('Secondary button clicked'),
variant: 'ghost'
}
},
width: 600
}
};
export const WithCustomHeader: Story = {
render: (args) => <ScrollableModalWrapper {...args} />,
args: {
header: (
<div className="flex flex-col space-y-2 text-center">
<h3 className="text-2xl font-bold tracking-tight">Custom Header</h3>
<p className="text-muted-foreground">
This example shows how to use a custom header component
</p>
</div>
),
children: (
<div className="space-y-4 py-4">
<p>Modal content with custom header styling.</p>
<p>You can add any React node as the header content.</p>
</div>
),
footer: {
left: <span className="text-muted-foreground text-sm">Footer left content</span>,
primaryButton: {
text: 'Confirm',
onClick: () => console.log('Confirmed'),
variant: 'black'
},
secondaryButton: {
text: 'Back',
onClick: () => console.log('Going back'),
variant: 'ghost'
}
},
width: 500
}
};
export const LoadingState: Story = {
render: (args) => <ScrollableModalWrapper {...args} />,
args: {
header: {
title: 'Loading State Example',
description: 'This example shows the modal with loading state in buttons'
},
children: (
<div className="py-4">
<p>Modal content with loading state buttons.</p>
</div>
),
footer: {
primaryButton: {
text: 'Submit',
onClick: () => new Promise((resolve) => setTimeout(resolve, 2000)),
loading: true
},
secondaryButton: {
text: 'Cancel',
onClick: () => console.log('Cancelled'),
variant: 'ghost',
disabled: true
}
},
width: 400
}
};

View File

@ -0,0 +1,154 @@
'use client';
import { useMemoizedFn } from '@/hooks/useMemoizedFn';
import { Button, ButtonProps } from '../buttons/Button';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
DialogFooter
} from './ModalBase';
import React, { useState, ReactNode, useMemo } from 'react';
import { cn } from '@/lib/classMerge';
import { AppTooltip } from '../tooltip';
export interface BorderedModalProps {
children: React.ReactNode;
width?: number;
footer: {
left?: React.ReactNode;
primaryButton: {
text: string;
onClick: (() => Promise<void>) | (() => void);
variant?: ButtonProps['variant'];
loading?: boolean;
disabled?: boolean;
tooltip?: string;
};
secondaryButton?: {
text: string;
onClick: () => void;
variant?: ButtonProps['variant'];
loading?: boolean;
disabled?: boolean;
};
};
header?:
| {
title: string;
description?: string;
}
| ReactNode;
open: boolean;
onClose: () => void;
className?: string;
scrollAreaClassName?: string;
}
export const BorderedModal = React.memo(
({
children,
width = 600,
footer,
header,
open,
onClose,
className = ''
}: BorderedModalProps) => {
const [isLoadingPrimaryButton, setIsLoadingPrimaryButton] = useState(false);
const onPrimaryButtonClickPreflight = useMemoizedFn(async () => {
setIsLoadingPrimaryButton(true);
await footer.primaryButton.onClick();
setIsLoadingPrimaryButton(false);
});
const onOpenChange = useMemoizedFn((open: boolean) => {
if (!open) {
onClose();
}
});
const memoizedStyle = useMemo(() => {
return {
width: width,
maxWidth: width
};
}, [width]);
const headerIsTitleObject = isHeaderTitleObject(header);
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent
className={cn(className)}
style={memoizedStyle}
showClose={headerIsTitleObject}>
{header && (
<DialogHeader
className={cn('border-b', headerIsTitleObject ? 'px-6 pt-6 pb-4' : 'space-y-0!')}>
{headerIsTitleObject ? (
<>
<DialogTitle>{header.title}</DialogTitle>
<DialogDescription>{header.description}</DialogDescription>
</>
) : (
header
)}
</DialogHeader>
)}
{children}
{footer && (
<DialogFooter
className={cn('flex items-center', footer.left ? 'justify-between' : 'justify-end')}>
{footer.left && footer.left}
<div className={cn('flex items-center space-x-2')}>
{footer.secondaryButton && (
<Button
onClick={footer.secondaryButton.onClick}
variant={footer.secondaryButton.variant ?? 'ghost'}
loading={footer.secondaryButton.loading}
disabled={footer.secondaryButton.disabled}>
{footer.secondaryButton.text}
</Button>
)}
<span>
<AppTooltip
title={footer.primaryButton.tooltip}
sideOffset={8}
delayDuration={400}>
<Button
onClick={onPrimaryButtonClickPreflight}
variant={footer.primaryButton.variant ?? 'black'}
loading={footer.primaryButton.loading ?? isLoadingPrimaryButton}
disabled={footer.primaryButton.disabled}>
{footer.primaryButton.text}
</Button>
</AppTooltip>
</span>
</div>
</DialogFooter>
)}
</DialogContent>
</Dialog>
);
}
);
const isHeaderTitleObject = (
header:
| {
title: string;
description?: string;
}
| ReactNode
): header is {
title: string;
description?: string;
} => {
return typeof header === 'object' && header !== null && 'title' in header;
};

View File

@ -5,7 +5,7 @@ export interface ConfirmProps {
title: string | React.ReactNode;
description?: string | React.ReactNode;
content: string | React.ReactNode;
onOk: () => Promise<void> | (() => void);
onOk: (() => Promise<void>) | (() => void);
onCancel?: () => Promise<void> | void;
width?: number;
cancelButtonProps?: {

View File

@ -0,0 +1,66 @@
import type { Meta, StoryObj } from '@storybook/react';
import { InputSelectModal } from './InputSelectModal';
import React from 'react';
import { fn } from '@storybook/test';
import { faker } from '@faker-js/faker';
import { useSet } from '@/hooks';
const meta: Meta<typeof InputSelectModal> = {
title: 'UI/Modal/InputSelectModal',
component: InputSelectModal,
parameters: {
layout: 'centered'
},
tags: ['autodocs']
};
export default meta;
type Story = StoryObj<typeof InputSelectModal>;
export const Default: Story = {
render: (args) => {
const [selectedItems, { replace }] = useSet<string>();
const onSelectChange = (items: string[]) => {
replace(items);
};
return (
<InputSelectModal
{...args}
open={true}
selectedRowKeys={Array.from(selectedItems)}
onSelectChange={onSelectChange}
/>
);
},
args: {
inputPlaceholder: 'Search items...',
footer: {
primaryButton: {
text: 'Confirm',
onClick: fn()
},
secondaryButton: {
text: 'Cancel',
onClick: fn(),
variant: 'ghost'
}
},
columns: [
{
title: 'Name',
dataIndex: 'name'
},
{
title: 'Email',
dataIndex: 'email'
}
],
selectedRowKeys: [],
rows: Array.from({ length: 3000 }, () => ({
id: faker.string.uuid(),
data: { name: faker.person.fullName(), email: faker.internet.email() }
}))
}
};

View File

@ -0,0 +1,99 @@
import React, { useMemo } from 'react';
import { BorderedModal, BorderedModalProps } from './BorderedModal';
import { Input } from '../inputs/Input';
import { BusterList, BusterListProps } from '../list/BusterList';
import { useDebounceSearch } from '@/hooks';
import { VisuallyHidden } from '@radix-ui/react-visually-hidden';
import { DialogTitle } from '@radix-ui/react-dialog';
import { Text } from '../typography';
export interface InputSelectModalProps extends Omit<BorderedModalProps, 'children'> {
inputPlaceholder?: string;
columns: NonNullable<BusterListProps['columns']>;
rows: NonNullable<BusterListProps['rows']>;
emptyState: BusterListProps['emptyState'];
onSelectChange: NonNullable<BusterListProps['onSelectChange']>;
selectedRowKeys: NonNullable<BusterListProps['selectedRowKeys']>;
showHeader?: NonNullable<BusterListProps['showHeader']>;
}
export const InputSelectModal = React.memo(
({
inputPlaceholder = 'Search...',
columns,
rows,
emptyState,
onSelectChange,
selectedRowKeys,
showHeader = true,
...props
}: InputSelectModalProps) => {
const { filteredItems, handleSearchChange, searchText } = useDebounceSearch({
items: rows,
searchPredicate: (item, searchText) => {
const values = Object.values(item.data || {});
return values.some((value) =>
value.toString().toLowerCase().includes(searchText.toLowerCase())
);
}
});
return (
<BorderedModal
header={
<>
<InputSelecteHeader
searchText={searchText}
handleSearchChange={handleSearchChange}
inputPlaceholder={inputPlaceholder}
/>
<VisuallyHidden>
<DialogTitle>Input Modal</DialogTitle>
</VisuallyHidden>
</>
}
{...props}>
<div
className="max-h-90"
style={{
height: (filteredItems.length || 1) * 48 + (showHeader ? 32 : 0) //32 is the height of the header
}}>
<BusterList
columns={columns}
rows={filteredItems}
onSelectChange={onSelectChange}
emptyState={useMemo(
() => emptyState || <Text variant={'secondary'}>No items found</Text>,
[emptyState]
)}
showHeader={showHeader}
selectedRowKeys={selectedRowKeys}
useRowClickSelectChange={true}
hideLastRowBorder
/>
</div>
</BorderedModal>
);
}
);
InputSelectModal.displayName = 'InputScrollableModal';
const InputSelecteHeader: React.FC<{
inputPlaceholder: string;
searchText: string;
handleSearchChange: (searchText: string) => void;
}> = ({ inputPlaceholder, searchText, handleSearchChange }) => {
return (
<div className="flex items-center justify-between py-1.5">
<Input
value={searchText}
onChange={(e) => handleSearchChange(e.target.value)}
placeholder={inputPlaceholder}
variant={'ghost'}
type="text"
size={'tall'}
autoFocus
/>
</div>
);
};

View File

@ -33,8 +33,10 @@ DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
const DialogContent = React.memo(
React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> & {
showClose?: boolean;
}
>(({ className, children, showClose = true, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
@ -47,13 +49,7 @@ const DialogContent = React.memo(
)}
{...props}>
{children}
<DialogPrimitive.Close
asChild
className={cn(
'absolute top-6 right-6 opacity-70 transition-opacity hover:opacity-100 disabled:pointer-events-none'
)}>
<Button prefix={<Xmark />} variant="ghost" />
</DialogPrimitive.Close>
{showClose && <DialogCloseButton />}
</DialogPrimitive.Content>
</DialogPortal>
))
@ -66,7 +62,7 @@ const DialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivEleme
DialogHeader.displayName = 'DialogHeader';
const DialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn('space-x-2 border-t px-6 py-2.5', className)} {...props} />
<div className={cn('space-x-2 border-t py-2.5 pr-6 pl-5', className)} {...props} />
);
DialogFooter.displayName = 'DialogFooter';
@ -94,6 +90,23 @@ const DialogDescription = React.forwardRef<
));
DialogDescription.displayName = DialogPrimitive.Description.displayName;
const DialogCloseButton = React.forwardRef<
HTMLButtonElement,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Close>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Close
ref={ref}
asChild
className={cn(
'absolute top-6 right-6 opacity-70 transition-opacity hover:opacity-100 disabled:pointer-events-none',
className
)}
{...props}>
<Button prefix={<Xmark />} variant="ghost" />
</DialogPrimitive.Close>
));
DialogCloseButton.displayName = 'DialogCloseButton';
export {
Dialog,
DialogPortal,
@ -104,5 +117,6 @@ export {
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription
DialogDescription,
DialogCloseButton
};

View File

@ -221,7 +221,7 @@ function SegmentedTriggerComponent<T extends string = string>(props: SegmentedTr
const LinkDiv = link ? Link : 'div';
return (
<Tooltip title={tooltip || ''} sideOffset={10} delayDuration={0.15}>
<Tooltip title={tooltip || ''} sideOffset={10} delayDuration={150}>
<Tabs.Trigger
key={value}
value={value}

View File

@ -1 +1,2 @@
//here
export * from './NewChatProvider';

View File

@ -140,7 +140,7 @@ const ChartButton: React.FC<{
}> = React.memo(
({ id, icon: Icon, tooltipText, onSelectChartType, isSelected, disabled, colors }) => {
return (
<AppTooltip title={tooltipText} delayDuration={0.65}>
<AppTooltip title={tooltipText} delayDuration={650}>
<div
key={id}
onClick={() => !disabled && onSelectChartType(id)}

View File

@ -1,66 +1,51 @@
'use client';
import { useState } from 'react';
import { useMemoizedFn } from './useMemoizedFn';
import { useState, useCallback } from 'react';
function useSet<K>(initialValue?: Iterable<K>) {
const getInitValue = () => new Set(initialValue);
const [set, setSet] = useState<Set<K>>(getInitValue);
/**
* A hook that provides a stateful Set with methods to modify it.
* @template T The type of elements in the Set
* @returns A tuple containing the Set and methods to modify it
*/
export function useSet<T>(initialValues: T[] = []): [
Set<T>,
{
add: (value: T) => void;
remove: (value: T) => void;
toggle: (value: T) => void;
clear: () => void;
has: (value: T) => boolean;
}
] {
const [set, setSet] = useState<Set<T>>(() => new Set(initialValues));
const add = useCallback((value: T) => {
const add = (key: K) => {
if (set.has(key)) {
return;
}
setSet((prevSet) => {
const newSet = new Set(prevSet);
newSet.add(value);
return newSet;
const temp = new Set(prevSet);
temp.add(key);
return temp;
});
}, []);
};
const remove = useCallback((value: T) => {
const remove = (key: K) => {
if (!set.has(key)) {
return;
}
setSet((prevSet) => {
const newSet = new Set(prevSet);
newSet.delete(value);
return newSet;
const temp = new Set(prevSet);
temp.delete(key);
return temp;
});
}, []);
};
const toggle = useCallback((value: T) => {
setSet((prevSet) => {
const newSet = new Set(prevSet);
if (newSet.has(value)) {
newSet.delete(value);
} else {
newSet.add(value);
}
return newSet;
});
}, []);
const reset = () => setSet(getInitValue());
const clear = useCallback(() => {
setSet(new Set());
}, []);
const has = (key: K) => set.has(key);
const has = useCallback((value: T) => set.has(value), [set]);
const size = () => set.size;
const replace = (newSet: K[]) => setSet(new Set(newSet));
return [
set,
{
add,
remove,
toggle,
clear,
has
add: useMemoizedFn(add),
replace: useMemoizedFn(replace),
remove: useMemoizedFn(remove),
reset: useMemoizedFn(reset),
has: useMemoizedFn(has),
size: useMemoizedFn(size)
}
];
] as const;
}
export { useSet };

View File

@ -17,9 +17,12 @@ const createMockDashboardRow = (startIndex: number, metrics: string[], columnSiz
items: metrics.map((metricId) => ({ id: metricId }))
});
export const generateMockDashboard = (numMetrics: number): DashboardMockResponse => {
export const generateMockDashboard = (
numMetrics: number,
dashboardId: string = '123'
): DashboardMockResponse => {
// Generate the specified number of metrics
const metrics = Array.from({ length: numMetrics }, (_, i) => createMockMetric(`number${i + 1}`));
const metrics = Array.from({ length: numMetrics }, (_, i) => createMockMetric(`${i + 1}`));
const metricIds = metrics.map((metric) => metric.id);
// Create rows based on number of metrics
@ -71,7 +74,7 @@ export const generateMockDashboard = (numMetrics: number): DashboardMockResponse
}
const dashboard: BusterDashboard = {
id: '123',
id: dashboardId,
name: 'Mock Dashboard',
file: `title: Mock Dashboard
description: A sample dashboard configuration

View File

@ -200,9 +200,9 @@ export const mockMetric30 = createMockMetric('number30');
export const createMockListMetric = (id: string): BusterMetricListItem => ({
id,
title: faker.lorem.words({ min: 2, max: 6 }),
title: faker.commerce.productName(),
last_edited: faker.date.recent().toISOString(),
dataset_name: faker.lorem.words({ min: 2, max: 6 }),
dataset_name: faker.commerce.productName(),
dataset_uuid: faker.string.uuid(),
created_by_id: faker.string.uuid(),
created_by_name: faker.person.fullName(),