From d53e003ce257222a24aa61dd88f54a32c849d1b7 Mon Sep 17 00:00:00 2001 From: Nate Kelley Date: Sat, 1 Mar 2025 15:21:58 -0700 Subject: [PATCH] update segmented components --- web/.cursor/rules/components_ui_stories.mdc | 5 +- .../DatasetHeaderOptions.tsx | 9 +- .../editor/EditorContainerSubHeader.tsx | 6 +- .../_LayoutHeaderAndSegment/UserSegments.tsx | 4 +- .../features/ShareMenu/ShareMenuTopBar.tsx | 4 +- .../features/modal/AddTypeModal.tsx | 4 +- ...d.stories.tsx => AppSegmented.stories.tsx} | 28 +- .../components/ui/segmented/AppSegmented.tsx | 267 ++++++++++++------ web/src/components/ui/segmented/Segmented.tsx | 183 ------------ .../CollectionListHeader.tsx | 4 +- .../MetricStylingAppSegment.stories.tsx | 89 ++++++ .../MetricStylingAppSegment.tsx | 31 +- .../SelectAxisColumnContent/EditLineStyle.tsx | 4 +- .../MetricListContainer/MetricListHeader.tsx | 12 +- .../DashboardContainerHeaderSegment.tsx | 4 +- .../MetricContainerHeaderSegment.tsx | 4 +- .../ReasoningContainerHeaderSegment.tsx | 4 +- 17 files changed, 336 insertions(+), 326 deletions(-) rename web/src/components/ui/segmented/{Segmented.stories.tsx => AppSegmented.stories.tsx} (81%) delete mode 100644 web/src/components/ui/segmented/Segmented.tsx create mode 100644 web/src/controllers/MetricController/MetricViewChart/MetricEditController/MetricStylingApp/MetricStylingAppSegment.stories.tsx diff --git a/web/.cursor/rules/components_ui_stories.mdc b/web/.cursor/rules/components_ui_stories.mdc index 07aa26ade..31e87c44f 100644 --- a/web/.cursor/rules/components_ui_stories.mdc +++ b/web/.cursor/rules/components_ui_stories.mdc @@ -3,4 +3,7 @@ description: Rules for new stories globs: src/components/**/*.stories.tsx alwaysApply: false --- -All new stories title with "UI/directory/{componentName}" unless specified otherwise. \ No newline at end of file +- All new stories title with "UI/directory/{componentName}" unless specified otherwise. +- Instead of console log for click events, you should do native storybook actions +- If a new story is made in the controller directory, I would like it title "Controllers/{controller name or parent controller name}/{componentName}" +- If a new story is found in the features directory, I would like it titled "Features/{featureName or parent feature name}/{componentName}" \ No newline at end of file diff --git a/web/src/app/app/(primary_layout)/datasets/[datasetId]/_DatasetsLayout/DatasetsIndividualHeader/DatasetHeaderOptions.tsx b/web/src/app/app/(primary_layout)/datasets/[datasetId]/_DatasetsLayout/DatasetsIndividualHeader/DatasetHeaderOptions.tsx index 44b1e473a..45d073dd4 100644 --- a/web/src/app/app/(primary_layout)/datasets/[datasetId]/_DatasetsLayout/DatasetsIndividualHeader/DatasetHeaderOptions.tsx +++ b/web/src/app/app/(primary_layout)/datasets/[datasetId]/_DatasetsLayout/DatasetsIndividualHeader/DatasetHeaderOptions.tsx @@ -1,12 +1,11 @@ 'use client'; -import { AppSegmented } from '@/components/ui'; +import { AppSegmented, type SegmentedItem } from '@/components/ui/segmented'; import { useRouter } from 'next/navigation'; import React, { useMemo } from 'react'; import { DatasetApps, DataSetAppText } from '../config'; import { createBusterRoute, BusterRoutes } from '@/routes'; import { useMemoizedFn } from 'ahooks'; -import { SegmentedValue } from 'antd/es/segmented'; export const DatasetsHeaderOptions: React.FC<{ selectedApp: DatasetApps; @@ -22,7 +21,7 @@ export const DatasetsHeaderOptions: React.FC<{ [isAdmin] ); - const options = useMemo( + const options: SegmentedItem[] = useMemo( () => optionsItems.map((key) => ({ label: DataSetAppText[key as DatasetApps], @@ -32,8 +31,8 @@ export const DatasetsHeaderOptions: React.FC<{ [datasetId, optionsItems] ); - const onChangeSegment = useMemoizedFn((value: SegmentedValue) => { - if (datasetId) push(keyToRoute(datasetId, value as DatasetApps)); + const onChangeSegment = useMemoizedFn((value: SegmentedItem) => { + if (datasetId) push(keyToRoute(datasetId, value.value)); }); return ; diff --git a/web/src/app/app/(primary_layout)/datasets/[datasetId]/editor/EditorContainerSubHeader.tsx b/web/src/app/app/(primary_layout)/datasets/[datasetId]/editor/EditorContainerSubHeader.tsx index eedbe01f5..43af0efde 100644 --- a/web/src/app/app/(primary_layout)/datasets/[datasetId]/editor/EditorContainerSubHeader.tsx +++ b/web/src/app/app/(primary_layout)/datasets/[datasetId]/editor/EditorContainerSubHeader.tsx @@ -1,7 +1,7 @@ import { AppSegmented } from '@/components/ui'; import { useMemoizedFn } from 'ahooks'; import { createStyles } from 'antd-style'; -import { SegmentedValue } from 'antd/es/segmented'; +import { type SegmentedItem } from '@/components/ui/segmented'; import React from 'react'; export enum EditorApps { @@ -20,8 +20,8 @@ export const EditorContainerSubHeader: React.FC<{ }> = React.memo(({ selectedApp, setSelectedApp }) => { const { styles, cx } = useStyles(); - const onSegmentedChange = useMemoizedFn((value: SegmentedValue) => { - setSelectedApp(value as EditorApps); + const onSegmentedChange = useMemoizedFn((value: SegmentedItem) => { + setSelectedApp(value.value); }); return ( diff --git a/web/src/app/app/(settings_layout)/settings/(permissions)/users/[userId]/_LayoutHeaderAndSegment/UserSegments.tsx b/web/src/app/app/(settings_layout)/settings/(permissions)/users/[userId]/_LayoutHeaderAndSegment/UserSegments.tsx index 2b9d5cd0f..4129286b6 100644 --- a/web/src/app/app/(settings_layout)/settings/(permissions)/users/[userId]/_LayoutHeaderAndSegment/UserSegments.tsx +++ b/web/src/app/app/(settings_layout)/settings/(permissions)/users/[userId]/_LayoutHeaderAndSegment/UserSegments.tsx @@ -1,7 +1,7 @@ import React, { useMemo } from 'react'; import { AppSegmented } from '@/components/ui/segmented'; import { useMemoizedFn } from 'ahooks'; -import { SegmentedValue } from 'antd/es/segmented'; +import { type SegmentedItem } from '@/components/ui/segmented'; import { Divider } from 'antd'; import { createBusterRoute, BusterRoutes } from '@/routes'; @@ -30,7 +30,7 @@ export const UserSegments: React.FC<{ onSelectApp: (app: UserSegmentsApps) => void; userId: string; }> = React.memo(({ isAdmin, selectedApp, onSelectApp, userId }) => { - const onChange = useMemoizedFn((value: SegmentedValue) => { + const onChange = useMemoizedFn((value: SegmentedItem) => { onSelectApp(value as UserSegmentsApps); }); const options = useMemo( diff --git a/web/src/components/features/ShareMenu/ShareMenuTopBar.tsx b/web/src/components/features/ShareMenu/ShareMenuTopBar.tsx index 043720129..9fa04bd42 100644 --- a/web/src/components/features/ShareMenu/ShareMenuTopBar.tsx +++ b/web/src/components/features/ShareMenu/ShareMenuTopBar.tsx @@ -4,7 +4,7 @@ import { CopyLinkButton } from './CopyLinkButton'; import { ShareAssetType } from '@/api/asset_interfaces'; import { ShareRole } from '@/api/asset_interfaces'; import { useMemoizedFn } from 'ahooks'; -import { SegmentedValue } from 'antd/es/segmented'; +import { type SegmentedItem } from '@/components/ui/segmented'; export enum ShareMenuTopBarOptions { Share = 'Share', @@ -45,7 +45,7 @@ export const ShareMenuTopBar: React.FC<{ .map((o) => ({ ...o, show: undefined })); }, [assetType, isOwner]); - const onChange = useMemoizedFn((v: SegmentedValue) => { + const onChange = useMemoizedFn((v: SegmentedItem) => { onChangeSelectedOption(v as ShareMenuTopBarOptions); }); diff --git a/web/src/components/features/modal/AddTypeModal.tsx b/web/src/components/features/modal/AddTypeModal.tsx index 7b9cd810d..778f6f988 100644 --- a/web/src/components/features/modal/AddTypeModal.tsx +++ b/web/src/components/features/modal/AddTypeModal.tsx @@ -15,7 +15,7 @@ import { useBusterSearchContextSelector } from '@/context/Search'; import isEmpty from 'lodash/isEmpty'; import { useBusterDashboardContextSelector } from '@/context/Dashboards'; import { useBusterCollectionIndividualContextSelector } from '@/context/Collections'; -import { SegmentedValue } from 'antd/es/segmented'; +import { type SegmentedItem } from '@/components/ui/segmented'; import { BusterSearchRequest } from '@/api/buster_socket/search'; import { busterAppStyleConfig } from '@/styles/busterAntDStyleConfig'; @@ -350,7 +350,7 @@ const ModalContent: React.FC<{ onSelectChange, onClose }) => { - const onSetSelectedFiltersPreflight = useMemoizedFn((value: SegmentedValue) => { + const onSetSelectedFiltersPreflight = useMemoizedFn((value: SegmentedItem) => { onSetSelectedFilter(value as string); }); diff --git a/web/src/components/ui/segmented/Segmented.stories.tsx b/web/src/components/ui/segmented/AppSegmented.stories.tsx similarity index 81% rename from web/src/components/ui/segmented/Segmented.stories.tsx rename to web/src/components/ui/segmented/AppSegmented.stories.tsx index 2121156a4..98753ee20 100644 --- a/web/src/components/ui/segmented/Segmented.stories.tsx +++ b/web/src/components/ui/segmented/AppSegmented.stories.tsx @@ -1,10 +1,10 @@ import type { Meta, StoryObj } from '@storybook/react'; -import { Segmented } from './Segmented'; +import { AppSegmented } from './AppSegmented'; import { BottleChampagne, Grid, HouseModern, PaintRoller } from '../icons'; -const meta: Meta = { - title: 'UI/Segmented', - component: Segmented, +const meta: Meta = { + title: 'UI/Segmented/AppSegmented', + component: AppSegmented, parameters: { layout: 'centered' }, @@ -28,14 +28,14 @@ const meta: Meta = { render: (args) => { return (
- +
); } }; export default meta; -type Story = StoryObj; +type Story = StoryObj; const defaultItems = [ { value: 'tab1', label: 'Tab 1', icon: }, @@ -45,20 +45,20 @@ const defaultItems = [ export const Default: Story = { args: { - items: defaultItems + options: defaultItems } }; export const Large: Story = { args: { - items: defaultItems, + options: defaultItems, size: 'large' } }; export const Block: Story = { args: { - items: defaultItems, + options: defaultItems, block: true }, parameters: { @@ -68,7 +68,7 @@ export const Block: Story = { export const LargeBlock: Story = { args: { - items: defaultItems, + options: defaultItems, size: 'large', block: true }, @@ -79,7 +79,7 @@ export const LargeBlock: Story = { export const WithIcons: Story = { args: { - items: [ + options: [ { value: 'list', label: 'List', icon: }, { value: 'grid', label: 'Grid', icon: }, { value: 'gallery', label: 'Gallery', icon: } @@ -89,14 +89,14 @@ export const WithIcons: Story = { export const Controlled: Story = { args: { - items: defaultItems, + options: defaultItems, value: 'tab2' } }; export const WithDisabledItems: Story = { args: { - items: [ + options: [ { value: 'tab1', label: 'Enabled' }, { value: 'tab2', label: 'Disabled', disabled: true }, { value: 'tab3', label: 'Enabled' } @@ -106,7 +106,7 @@ export const WithDisabledItems: Story = { export const CustomStyling: Story = { args: { - items: defaultItems, + options: defaultItems, className: 'bg-blue-100 [&_[data-state=active]]:text-blue-700' } }; diff --git a/web/src/components/ui/segmented/AppSegmented.tsx b/web/src/components/ui/segmented/AppSegmented.tsx index fa1fef7ae..221fd78c6 100644 --- a/web/src/components/ui/segmented/AppSegmented.tsx +++ b/web/src/components/ui/segmented/AppSegmented.tsx @@ -1,108 +1,211 @@ 'use client'; -import React, { useMemo } from 'react'; -import { ConfigProvider, Segmented, SegmentedProps, ThemeConfig } from 'antd'; -import { createStyles } from 'antd-style'; -import { busterAppStyleConfig } from '@/styles/busterAntDStyleConfig'; -import Link from 'next/link'; +import * as React from 'react'; +import * as Tabs from '@radix-ui/react-tabs'; +import { motion } from 'framer-motion'; +import { cn } from '@/lib/classMerge'; +import { useEffect, useState } from 'react'; +import { cva } from 'class-variance-authority'; import { useMemoizedFn } from 'ahooks'; -import { useRouter } from 'next/navigation'; -import { AppTooltip } from '@/components/ui/tooltip'; -const token = busterAppStyleConfig.token!; -type SegmentedOption = { - value: string; - label?: string; - link?: string; - onHover?: () => void; +export interface SegmentedItem { + value: T; + label: React.ReactNode; icon?: React.ReactNode; - tooltip?: string; -}; -export interface AppSegmentedProps extends Omit { - bordered?: boolean; - options: SegmentedOption[]; + disabled?: boolean; } -const useStyles = createStyles(({ css, token }) => { - return { - segmented: css`` - }; -}); +interface AppSegmentedProps { + options: SegmentedItem[]; + value?: T; + onChange?: (value: SegmentedItem) => void; + className?: string; + size?: 'default' | 'large'; + block?: boolean; + type?: 'button' | 'track'; +} -const THEME_CONFIG: ThemeConfig = { - components: { - Segmented: { - itemColor: token.colorTextDescription, - trackBg: 'transparent', - itemSelectedColor: token.colorTextBase, - itemSelectedBg: token.controlItemBgActive, - colorBorder: token.colorBorder, - boxShadowTertiary: 'none' +const segmentedVariants = cva('relative inline-flex items-center rounded-md', { + variants: { + block: { + true: 'w-full', + false: '' + }, + size: { + default: '', + large: '' + }, + type: { + button: 'bg-transparent', + track: 'bg-item-select' } } +}); + +const triggerVariants = cva( + 'relative z-10 flex items-center justify-center gap-x-1.5 gap-y-1 rounded-md transition-colors', + { + variants: { + size: { + default: 'px-2.5 flex-row', + large: 'px-3 flex-col' + }, + block: { + true: 'flex-1', + false: '' + }, + disabled: { + true: '!text-foreground/30 !hover:text-foreground/30 cursor-not-allowed', + false: 'cursor-pointer' + }, + selected: { + true: 'text-foreground', + false: 'text-gray-dark hover:text-foreground' + } + } + } +); + +const gliderVariants = cva('absolute border-border rounded-md border', { + variants: { + type: { + button: 'bg-item-select', + track: 'bg-background' + } + } +}); + +// Create a type for the forwardRef component that includes displayName +type AppSegmentedComponent = (( + props: AppSegmentedProps & { ref?: React.ForwardedRef } +) => React.ReactElement) & { + displayName?: string; }; -export const AppSegmented = React.memo( - ({ size = 'small', bordered = true, onChange, options: optionsProps, ...props }) => { - const { cx, styles } = useStyles(); - const router = useRouter(); +// Update the component definition to properly handle generics +export const AppSegmented: AppSegmentedComponent = React.forwardRef( + ( + { + options, + type = 'track', + value, + onChange, + className, + size = 'default', + block = false + }: AppSegmentedProps, + ref: React.ForwardedRef + ) => { + const tabRefs = React.useRef>(new Map()); + const [selectedValue, setSelectedValue] = useState(value || options[0]?.value); + const [gliderStyle, setGliderStyle] = useState({ + width: 0, + transform: 'translateX(0)' + }); - const options = useMemo(() => { - return optionsProps.map((option) => ({ - value: option.value, - label: - })); - }, [optionsProps]); + const height = size === 'default' ? 'h-[28px]' : 'h-[50px]'; - const onChangePreflight = useMemoizedFn((value: string | number) => { - const link = optionsProps.find((option) => option.value === value)?.link; - if (link) { - router.push(link); + useEffect(() => { + if (value !== undefined && value !== selectedValue) { + setSelectedValue(value); + } + }, [value]); + + useEffect(() => { + const selectedTab = tabRefs.current.get(selectedValue); + if (selectedTab) { + const { offsetWidth, offsetLeft } = selectedTab; + setGliderStyle({ + width: offsetWidth, + transform: `translateX(${offsetLeft}px)` + }); + } + }, [selectedValue]); + + const handleTabClick = useMemoizedFn((value: string) => { + const item = options.find((item) => item.value === value); + if (item && !item.disabled) { + setSelectedValue(item.value); + onChange?.(item); } - onChange?.(value); }); return ( - - + - + + {options.map((item) => ( + + ))} + + ); } -); +) as AppSegmentedComponent; + AppSegmented.displayName = 'AppSegmented'; -const SegmentedItem: React.FC<{ option: SegmentedOption }> = ({ option }) => { - return ( - - -
- {option.icon} - {option.label} -
-
-
- ); -}; +interface SegmentedTriggerProps { + item: SegmentedItem; + selectedValue: string; + size: AppSegmentedProps['size']; + block: AppSegmentedProps['block']; + tabRefs: React.MutableRefObject>; +} -const SegmentedItemLink: React.FC<{ href?: string; children: React.ReactNode }> = ({ - href, - children -}) => { - if (!href) return children; +function SegmentedTriggerComponent( + props: SegmentedTriggerProps +): JSX.Element { + const { item, selectedValue, size, block, tabRefs } = props; return ( - - {children} - + { + if (el) tabRefs.current.set(item.value, el); + }} + className={cn( + 'focus-visible:ring-primary focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2', + triggerVariants({ + size, + block, + disabled: item.disabled, + selected: selectedValue === item.value + }) + )}> + {item.icon && {item.icon}} + {item.label} + ); -}; +} + +SegmentedTriggerComponent.displayName = 'SegmentedTrigger'; + +const SegmentedTrigger = React.memo(SegmentedTriggerComponent) as typeof SegmentedTriggerComponent; +SegmentedTrigger.displayName = 'SegmentedTrigger'; diff --git a/web/src/components/ui/segmented/Segmented.tsx b/web/src/components/ui/segmented/Segmented.tsx deleted file mode 100644 index d7c43520b..000000000 --- a/web/src/components/ui/segmented/Segmented.tsx +++ /dev/null @@ -1,183 +0,0 @@ -'use client'; - -import * as React from 'react'; -import * as Tabs from '@radix-ui/react-tabs'; -import { motion } from 'framer-motion'; -import { cn } from '@/lib/classMerge'; -import { useEffect, useState } from 'react'; -import { cva } from 'class-variance-authority'; -import { useMemoizedFn } from 'ahooks'; - -export interface SegmentedItem { - value: string; - label: React.ReactNode; - icon?: React.ReactNode; - disabled?: boolean; -} - -interface SegmentedProps { - items: SegmentedItem[]; - value?: string; - onChange?: (value: SegmentedItem) => void; - className?: string; - size?: 'default' | 'large'; - block?: boolean; - type?: 'button' | 'track'; -} - -const segmentedVariants = cva('relative inline-flex items-center rounded-md', { - variants: { - block: { - true: 'w-full', - false: '' - }, - size: { - default: '', - large: '' - }, - type: { - button: 'bg-transparent', - track: 'bg-item-select' - } - } -}); - -const triggerVariants = cva( - 'relative z-10 flex items-center justify-center gap-x-1.5 gap-y-1 rounded-md transition-colors', - { - variants: { - size: { - default: 'px-2.5 flex-row', - large: 'px-3 flex-col' - }, - block: { - true: 'flex-1', - false: '' - }, - disabled: { - true: '!text-foreground/30 !hover:text-foreground/30 cursor-not-allowed', - false: 'cursor-pointer' - }, - selected: { - true: 'text-foreground', - false: 'text-gray-dark hover:text-foreground' - } - } - } -); - -const gliderVariants = cva('absolute border-border rounded-md border', { - variants: { - type: { - button: 'bg-item-select', - track: 'bg-background' - } - } -}); -export const Segmented = React.forwardRef( - ({ items, type = 'track', value, onChange, className, size = 'default', block = false }, ref) => { - const tabRefs = React.useRef>(new Map()); - const [selectedValue, setSelectedValue] = useState(value || items[0]?.value); - const [gliderStyle, setGliderStyle] = useState({ - width: 0, - transform: 'translateX(0)' - }); - - const height = size === 'default' ? 'h-[28px]' : 'h-[50px]'; - - useEffect(() => { - if (value !== undefined && value !== selectedValue) { - setSelectedValue(value); - } - }, [value]); - - useEffect(() => { - const selectedTab = tabRefs.current.get(selectedValue); - if (selectedTab) { - const { offsetWidth, offsetLeft } = selectedTab; - setGliderStyle({ - width: offsetWidth, - transform: `translateX(${offsetLeft}px)` - }); - } - }, [selectedValue]); - - const handleTabClick = useMemoizedFn((value: string) => { - const item = items.find((item) => item.value === value); - if (item && !item.disabled) { - setSelectedValue(item.value); - onChange?.(item); - } - }); - - return ( - - - - {items.map((item) => ( - - ))} - - - ); - } -); - -Segmented.displayName = 'Segmented'; - -const SegmentedTrigger = React.memo<{ - item: SegmentedItem; - selectedValue: string; - size: SegmentedProps['size']; - block: SegmentedProps['block']; - tabRefs: React.MutableRefObject>; -}>(({ item, selectedValue, size, block, tabRefs }) => { - return ( - { - if (el) tabRefs.current.set(item.value, el); - }} - className={cn( - 'focus-visible:ring-primary focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2', - triggerVariants({ - size, - block, - disabled: item.disabled, - selected: selectedValue === item.value - }) - )}> - {item.icon && {item.icon}} - {item.label} - - ); -}); - -SegmentedTrigger.displayName = 'SegmentedTrigger'; diff --git a/web/src/controllers/CollectionListController/CollectionListHeader.tsx b/web/src/controllers/CollectionListController/CollectionListHeader.tsx index 7675ebd00..e480892e0 100644 --- a/web/src/controllers/CollectionListController/CollectionListHeader.tsx +++ b/web/src/controllers/CollectionListController/CollectionListHeader.tsx @@ -15,7 +15,7 @@ import { CollectionsListEmit } from '@/api/buster_socket/collections'; import isEmpty from 'lodash/isEmpty'; import omit from 'lodash/omit'; import { useMemoizedFn } from 'ahooks'; -import { SegmentedValue } from 'antd/es/segmented'; +import { type SegmentedItem } from '@/components/ui/segmented'; export const CollectionListHeader: React.FC<{ collectionId?: string; @@ -122,7 +122,7 @@ const CollectionFilters: React.FC<{ return filters.find((f) => f.value === activeFiltersValue)?.value || filters[0].value; }, [filters, collectionListFilters]); - const onChangeFilter = useMemoizedFn((v: SegmentedValue) => { + const onChangeFilter = useMemoizedFn((v: SegmentedItem) => { let parsedValue; try { parsedValue = JSON.parse(v as string); diff --git a/web/src/controllers/MetricController/MetricViewChart/MetricEditController/MetricStylingApp/MetricStylingAppSegment.stories.tsx b/web/src/controllers/MetricController/MetricViewChart/MetricEditController/MetricStylingApp/MetricStylingAppSegment.stories.tsx new file mode 100644 index 000000000..159d9c28b --- /dev/null +++ b/web/src/controllers/MetricController/MetricViewChart/MetricEditController/MetricStylingApp/MetricStylingAppSegment.stories.tsx @@ -0,0 +1,89 @@ +import React from 'react'; +import { Meta, StoryObj } from '@storybook/react'; +import { action } from '@storybook/addon-actions'; +import { MetricStylingAppSegment } from './MetricStylingAppSegment'; +import { MetricStylingAppSegments } from './config'; +import { ChartType } from '@/components/ui/charts/interfaces/enum'; + +const meta: Meta = { + title: 'Controllers/EditMetricController/MetricStylingAppSegment', + component: MetricStylingAppSegment, + parameters: { + layout: 'centered' + }, + tags: ['autodocs'], + argTypes: { + segment: { + control: 'select', + options: Object.values(MetricStylingAppSegments), + description: 'The currently selected segment' + }, + setSegment: { + action: 'setSegment', + description: 'Function called when segment changes' + }, + selectedChartType: { + control: 'select', + options: Object.values(ChartType), + description: 'The type of chart currently selected' + }, + className: { + control: 'text', + description: 'Additional CSS classes to apply' + } + }, + decorators: [ + (Story) => ( +
+ +
+ ) + ] +}; + +export default meta; +type Story = StoryObj; + +// Create a reusable action handler +const handleSetSegment = action('setSegment'); + +export const Default: Story = { + args: { + segment: MetricStylingAppSegments.VISUALIZE, + setSegment: handleSetSegment, + selectedChartType: ChartType.Line, + className: '' + } +}; + +export const WithTableChart: Story = { + args: { + segment: MetricStylingAppSegments.VISUALIZE, + setSegment: handleSetSegment, + selectedChartType: ChartType.Table, + className: '' + }, + parameters: { + docs: { + description: { + story: 'When table chart type is selected, Styling and Colors segments are disabled' + } + } + } +}; + +export const WithMetricChart: Story = { + args: { + segment: MetricStylingAppSegments.VISUALIZE, + setSegment: handleSetSegment, + selectedChartType: ChartType.Metric, + className: '' + }, + parameters: { + docs: { + description: { + story: 'When metric chart type is selected, Styling and Colors segments are disabled' + } + } + } +}; diff --git a/web/src/controllers/MetricController/MetricViewChart/MetricEditController/MetricStylingApp/MetricStylingAppSegment.tsx b/web/src/controllers/MetricController/MetricViewChart/MetricEditController/MetricStylingApp/MetricStylingAppSegment.tsx index 36301a2e9..25d91d160 100644 --- a/web/src/controllers/MetricController/MetricViewChart/MetricEditController/MetricStylingApp/MetricStylingAppSegment.tsx +++ b/web/src/controllers/MetricController/MetricViewChart/MetricEditController/MetricStylingApp/MetricStylingAppSegment.tsx @@ -1,16 +1,9 @@ import React, { useMemo } from 'react'; import { MetricStylingAppSegments } from './config'; -import { SegmentedOptions, SegmentedValue } from 'antd/es/segmented'; import { useMemoizedFn } from 'ahooks'; -import { Segmented } from 'antd'; -import { createStyles } from 'antd-style'; import { IBusterMetricChartConfig } from '@/api/asset_interfaces'; - -const useStyles = createStyles(({ css, token }) => ({ - container: css` - border-bottom: 0.5px solid ${token.colorBorder}; - ` -})); +import { AppSegmented, type SegmentedItem } from '@/components/ui/segmented'; +import { cn } from '@/lib/utils'; export const MetricStylingAppSegment: React.FC<{ segment: MetricStylingAppSegments; @@ -18,13 +11,12 @@ export const MetricStylingAppSegment: React.FC<{ selectedChartType: IBusterMetricChartConfig['selectedChartType']; className?: string; }> = React.memo(({ segment, setSegment, selectedChartType, className = '' }) => { - const { cx, styles } = useStyles(); const isTable = selectedChartType === 'table'; const isMetric = selectedChartType === 'metric'; const disableColors = isTable || isMetric; const disableStyling = isTable || isMetric; - const options: SegmentedOptions = useMemo( + const options: SegmentedItem[] = useMemo( () => [ { label: MetricStylingAppSegments.VISUALIZE, @@ -44,14 +36,21 @@ export const MetricStylingAppSegment: React.FC<{ [disableColors, disableStyling] ); - const onChangeSegment = useMemoizedFn((value: SegmentedValue) => { - setSegment(value as MetricStylingAppSegments); + const onChangeSegment = useMemoizedFn((value: SegmentedItem) => { + setSegment(value.value); }); return ( -
-
- +
+
+
); diff --git a/web/src/controllers/MetricController/MetricViewChart/MetricEditController/MetricStylingApp/StylingAppVisualize/SelectAxis/SelectAxisColumnContent/EditLineStyle.tsx b/web/src/controllers/MetricController/MetricViewChart/MetricEditController/MetricStylingApp/StylingAppVisualize/SelectAxis/SelectAxisColumnContent/EditLineStyle.tsx index 75580db22..80805990d 100644 --- a/web/src/controllers/MetricController/MetricViewChart/MetricEditController/MetricStylingApp/StylingAppVisualize/SelectAxis/SelectAxisColumnContent/EditLineStyle.tsx +++ b/web/src/controllers/MetricController/MetricViewChart/MetricEditController/MetricStylingApp/StylingAppVisualize/SelectAxis/SelectAxisColumnContent/EditLineStyle.tsx @@ -5,7 +5,7 @@ import { AppMaterialIcons, AppSegmented, AppTooltip } from '@/components/ui'; import { useEditAppSegmented } from './useEditAppSegmented'; import { ENABLED_DOTS_ON_LINE_SIZE } from '@/api/asset_interfaces'; import { useMemoizedFn } from 'ahooks'; -import { SegmentedValue } from 'antd/es/segmented'; +import { type SegmentedItem } from '@/components/ui/segmented'; const options: { icon: React.ReactNode; value: LineValue }[] = [ { @@ -106,7 +106,7 @@ export const EditLineStyle: React.FC<{ methodRecord[lineValue](); }); - const onChangeValue = useMemoizedFn((value: SegmentedValue) => { + const onChangeValue = useMemoizedFn((value: SegmentedItem) => { if (value) onClickValue(value as string); }); diff --git a/web/src/controllers/MetricListContainer/MetricListHeader.tsx b/web/src/controllers/MetricListContainer/MetricListHeader.tsx index b917dbf4c..55d13d0a3 100644 --- a/web/src/controllers/MetricListContainer/MetricListHeader.tsx +++ b/web/src/controllers/MetricListContainer/MetricListHeader.tsx @@ -6,7 +6,7 @@ import { AppSegmented } from '@/components/ui'; import { VerificationStatus } from '@/api/asset_interfaces'; import { Text } from '@/components/ui'; import { useMemoizedFn } from 'ahooks'; -import { SegmentedValue } from 'antd/lib/segmented'; +import { type SegmentedItem } from '@/components/ui/segmented'; export const MetricListHeader: React.FC<{ type: 'logs' | 'metrics'; @@ -30,7 +30,7 @@ export const MetricListHeader: React.FC<{ ); }; -const options = [ +const options: SegmentedItem[] = [ { label: 'All', value: 'all' @@ -50,7 +50,7 @@ const MetricsFilters: React.FC<{ filters: VerificationStatus[]; onSetFilters: (filters: VerificationStatus[]) => void; }> = React.memo(({ type, filters, onSetFilters }) => { - const selectedOption = useMemo(() => { + const selectedOption: SegmentedItem | undefined = useMemo(() => { return ( options.find((option) => { return filters.includes(option.value as VerificationStatus); @@ -58,11 +58,11 @@ const MetricsFilters: React.FC<{ ); }, [filters]); - const onChange = useMemoizedFn((v: SegmentedValue) => { - if (v === 'all') { + const onChange = useMemoizedFn((v: SegmentedItem) => { + if (v.value === 'all') { onSetFilters([]); } else { - onSetFilters([v as VerificationStatus]); + onSetFilters([v.value as VerificationStatus]); } }); diff --git a/web/src/layouts/ChatLayout/FileContainer/FileContainerHeader/DashboardContainerHeaderSegment.tsx b/web/src/layouts/ChatLayout/FileContainer/FileContainerHeader/DashboardContainerHeaderSegment.tsx index 63ce06e4b..98734952a 100644 --- a/web/src/layouts/ChatLayout/FileContainer/FileContainerHeader/DashboardContainerHeaderSegment.tsx +++ b/web/src/layouts/ChatLayout/FileContainer/FileContainerHeader/DashboardContainerHeaderSegment.tsx @@ -4,7 +4,7 @@ import type { DashboardFileView, FileView } from '../../ChatLayoutContext/useCha import { AppSegmented } from '@/components/ui/segmented'; import { useChatLayoutContextSelector } from '../../ChatLayoutContext'; import { useMemoizedFn } from 'ahooks'; -import { SegmentedValue } from 'antd/es/segmented'; +import { type SegmentedItem } from '@/components/ui/segmented'; const segmentOptions: { label: string; value: DashboardFileView }[] = [ { label: 'Dashboard', value: 'dashboard' }, @@ -15,7 +15,7 @@ export const DashboardContainerHeaderSegment: React.FC { const onSetFileView = useChatLayoutContextSelector((x) => x.onSetFileView); - const onChange = useMemoizedFn((fileView: SegmentedValue) => { + const onChange = useMemoizedFn((fileView: SegmentedItem) => { onSetFileView({ fileView: fileView as FileView }); }); diff --git a/web/src/layouts/ChatLayout/FileContainer/FileContainerHeader/MetricContainerHeaderSegment.tsx b/web/src/layouts/ChatLayout/FileContainer/FileContainerHeader/MetricContainerHeaderSegment.tsx index 01fe63977..ac3172b64 100644 --- a/web/src/layouts/ChatLayout/FileContainer/FileContainerHeader/MetricContainerHeaderSegment.tsx +++ b/web/src/layouts/ChatLayout/FileContainer/FileContainerHeader/MetricContainerHeaderSegment.tsx @@ -3,7 +3,7 @@ import { FileContainerSegmentProps } from './interfaces'; import { AppSegmented } from '@/components/ui/segmented'; import { useChatLayoutContextSelector } from '../../ChatLayoutContext'; import type { FileView, MetricFileView } from '../../ChatLayoutContext/useChatFileLayout'; -import { SegmentedValue } from 'antd/es/segmented'; +import { type SegmentedItem } from '@/components/ui/segmented'; import { useMemoizedFn } from 'ahooks'; const segmentOptions: { label: string; value: MetricFileView }[] = [ @@ -16,7 +16,7 @@ export const MetricContainerHeaderSegment: React.FC = ({ selectedFileView }) => { const onSetFileView = useChatLayoutContextSelector((x) => x.onSetFileView); - const onChange = useMemoizedFn((fileView: SegmentedValue) => { + const onChange = useMemoizedFn((fileView: SegmentedItem) => { onSetFileView({ fileView: fileView as FileView }); }); diff --git a/web/src/layouts/ChatLayout/FileContainer/FileContainerHeader/ReasoningContainerHeaderSegment.tsx b/web/src/layouts/ChatLayout/FileContainer/FileContainerHeader/ReasoningContainerHeaderSegment.tsx index 371d07e62..572bde52d 100644 --- a/web/src/layouts/ChatLayout/FileContainer/FileContainerHeader/ReasoningContainerHeaderSegment.tsx +++ b/web/src/layouts/ChatLayout/FileContainer/FileContainerHeader/ReasoningContainerHeaderSegment.tsx @@ -3,7 +3,7 @@ import { FileContainerSegmentProps } from './interfaces'; import { AppSegmented } from '@/components/ui/segmented'; import { useChatLayoutContextSelector } from '../../ChatLayoutContext'; import type { FileView, ReasoningFileView } from '../../ChatLayoutContext/useChatFileLayout'; -import { SegmentedValue } from 'antd/es/segmented'; +import { type SegmentedItem } from '@/components/ui/segmented'; import { useMemoizedFn } from 'ahooks'; const segmentOptions: { label: string; value: ReasoningFileView }[] = [ @@ -14,7 +14,7 @@ export const ReasoningContainerHeaderSegment: React.FC { const onSetFileView = useChatLayoutContextSelector((x) => x.onSetFileView); - const onChange = useMemoizedFn((fileView: SegmentedValue) => { + const onChange = useMemoizedFn((fileView: SegmentedItem) => { onSetFileView({ fileView: fileView as FileView }); });