rip out app select component

This commit is contained in:
Nate Kelley 2025-02-26 23:25:53 -07:00
parent f364556f90
commit d3081a6e57
No known key found for this signature in database
GPG Key ID: FD90372AB8D98B4F
38 changed files with 350 additions and 518 deletions

View File

@ -2,7 +2,8 @@ import { ShareAssetType, VerificationStatus, BusterChatListItem } from '@/api/as
import { makeHumanReadble, formatDate } from '@/lib'; import { makeHumanReadble, formatDate } from '@/lib';
import React, { memo, useMemo, useRef, useState } from 'react'; import React, { memo, useMemo, useRef, useState } from 'react';
import { StatusBadgeIndicator, getShareStatus } from '../../../../components/features/Lists'; import { StatusBadgeIndicator, getShareStatus } from '../../../../components/features/Lists';
import { BusterUserAvatar, Text } from '@/components/ui'; import { Text } from '@/components/ui';
import { Avatar } from '@/components/ui/avatar';
import { BusterRoutes, createBusterRoute } from '@/routes'; import { BusterRoutes, createBusterRoute } from '@/routes';
import { useMemoizedFn } from 'ahooks'; import { useMemoizedFn } from 'ahooks';
import { BusterListColumn, BusterListRow } from '@/components/ui/list'; import { BusterListColumn, BusterListRow } from '@/components/ui/list';
@ -180,7 +181,7 @@ TitleCell.displayName = 'TitleCell';
const OwnerCell = memo<{ name: string; image: string | undefined }>(({ name, image }) => ( const OwnerCell = memo<{ name: string; image: string | undefined }>(({ name, image }) => (
<div className="flex pl-0"> <div className="flex pl-0">
<BusterUserAvatar image={image} name={name} size={18} /> <Avatar image={image} name={name} className="h-[18px] w-[18px]" />
</div> </div>
)); ));
OwnerCell.displayName = 'OwnerCell'; OwnerCell.displayName = 'OwnerCell';

View File

@ -1,5 +1,6 @@
import React from 'react'; import React from 'react';
import { BusterUserAvatar, Text, Title, AppMaterialIcons } from '@/components/ui'; import { Text, Title, AppMaterialIcons } from '@/components/ui';
import { Avatar } from '@/components/ui/avatar';
import type { OrganizationUser } from '@/api/asset_interfaces'; import type { OrganizationUser } from '@/api/asset_interfaces';
import { Button } from 'antd'; import { Button } from 'antd';
@ -17,7 +18,7 @@ UserHeader.displayName = 'UserHeader';
const UserInfo: React.FC<{ user: OrganizationUser }> = ({ user }) => { const UserInfo: React.FC<{ user: OrganizationUser }> = ({ user }) => {
return ( return (
<div className="flex items-center space-x-4"> <div className="flex items-center space-x-4">
<BusterUserAvatar size={48} name={user.name} /> <Avatar className="h-[48px] w-[48px]" name={user.name} />
<div className="flex flex-col"> <div className="flex flex-col">
<Title level={4}>{user.name}</Title> <Title level={4}>{user.name}</Title>
<Text size="sm" type="secondary"> <Text size="sm" type="secondary">

View File

@ -6,7 +6,7 @@ import { useUserConfigContextSelector } from '@/context/Users';
import { createStyles } from 'antd-style'; import { createStyles } from 'antd-style';
import { formatDate } from '@/lib/date'; import { formatDate } from '@/lib/date';
import { Text, Title } from '@/components/ui'; import { Text, Title } from '@/components/ui';
import { BusterUserAvatar } from '@/components/ui'; import { Avatar } from '@/components/ui/avatar';
import { Card } from 'antd'; import { Card } from 'antd';
const useStyles = createStyles(({ token, css }) => ({ const useStyles = createStyles(({ token, css }) => ({
@ -41,7 +41,7 @@ export default function ProfilePage() {
body: 'flex flex-col space-y-3' body: 'flex flex-col space-y-3'
}}> }}>
<div className={'flex items-center space-x-2.5'}> <div className={'flex items-center space-x-2.5'}>
<BusterUserAvatar name={name} size={48} /> <Avatar name={name} className="h-[48px] w-[48px]" />
<Title level={4}>{name}</Title> <Title level={4}>{name}</Title>
</div> </div>
<div className={'flex flex-col space-y-0.5'}> <div className={'flex flex-col space-y-0.5'}>

View File

@ -1,5 +1,5 @@
import { AppDropdownSelect } from '@/components/ui/dropdown';
import { AppMaterialIcons } from '@/components/ui'; import { AppMaterialIcons } from '@/components/ui';
import { Dropdown, type DropdownItem, type DropdownProps } from '@/components/ui/dropdown';
import { AppTooltip } from '@/components/ui/tooltip'; import { AppTooltip } from '@/components/ui/tooltip';
import { useAppLayoutContextSelector } from '@/context/BusterAppLayout'; import { useAppLayoutContextSelector } from '@/context/BusterAppLayout';
import { useBusterCollectionListContextSelector } from '@/context/Collections'; import { useBusterCollectionListContextSelector } from '@/context/Collections';
@ -21,12 +21,13 @@ export const SaveToCollectionsDropdown: React.FC<{
const [openCollectionModal, setOpenCollectionModal] = React.useState(false); const [openCollectionModal, setOpenCollectionModal] = React.useState(false);
const [showDropdown, setShowDropdown] = React.useState(false); const [showDropdown, setShowDropdown] = React.useState(false);
const items = useMemo( const items: DropdownProps['items'] = useMemo(
() => () =>
(collectionsList || []).map((collection) => { (collectionsList || []).map<DropdownItem>((collection) => {
return { return {
key: collection.id, value: collection.id,
label: collection.name, label: collection.name,
selected: selectedCollections.some((id) => id === collection.id),
onClick: () => onClickItem(collection), onClick: () => onClickItem(collection),
link: createBusterRoute({ link: createBusterRoute({
route: BusterRoutes.APP_COLLECTIONS_ID, route: BusterRoutes.APP_COLLECTIONS_ID,
@ -34,7 +35,7 @@ export const SaveToCollectionsDropdown: React.FC<{
}) })
}; };
}), }),
[collectionsList] [collectionsList, selectedCollections]
); );
const onClickItem = useMemoizedFn((collection: BusterCollectionListItem) => { const onClickItem = useMemoizedFn((collection: BusterCollectionListItem) => {
@ -84,22 +85,16 @@ export const SaveToCollectionsDropdown: React.FC<{
return ( return (
<> <>
<AppDropdownSelect <Dropdown
trigger={['click']} side="bottom"
placement="bottomRight" align="start"
className="flex! h-fit! items-center" menuHeader={'Save to a collection'}
headerContent={'Save to a collection'}
open={showDropdown} open={showDropdown}
onOpenChange={onOpenChange} onOpenChange={onOpenChange}
footerContent={memoizedButton} footerContent={memoizedButton}
items={items} items={items}>
selectedItems={selectedCollections}> <AppTooltip title={showDropdown ? '' : 'Save to collection'}>{children} </AppTooltip>
{showDropdown ? ( </Dropdown>
<>{children}</>
) : (
<AppTooltip title={showDropdown ? '' : 'Save to collection'}>{children} </AppTooltip>
)}
</AppDropdownSelect>
<NewCollectionModal <NewCollectionModal
open={openCollectionModal} open={openCollectionModal}

View File

@ -7,7 +7,7 @@ import { useMemoizedFn } from 'ahooks';
import React, { useMemo, useState } from 'react'; import React, { useMemo, useState } from 'react';
import { BusterRoutes, createBusterRoute } from '@/routes/busterRoutes'; import { BusterRoutes, createBusterRoute } from '@/routes/busterRoutes';
import { Button } from 'antd'; import { Button } from 'antd';
import { AppDropdownSelect, AppDropdownSelectProps } from '@/components/ui/dropdown'; import { Dropdown, type DropdownProps } from '@/components/ui/dropdown';
import { AppTooltip } from '@/components/ui/tooltip'; import { AppTooltip } from '@/components/ui/tooltip';
import { AppMaterialIcons } from '@/components/ui'; import { AppMaterialIcons } from '@/components/ui';
import type { BusterMetric, BusterDashboardListItem } from '@/api/asset_interfaces'; import type { BusterMetric, BusterDashboardListItem } from '@/api/asset_interfaces';
@ -35,12 +35,13 @@ export const SaveToDashboardDropdown: React.FC<{
} }
}); });
const items = useMemo( const items: DropdownProps['items'] = useMemo(
() => () =>
(dashboardsList || [])?.map((dashboard) => { (dashboardsList || [])?.map((dashboard) => {
return { return {
key: dashboard.id, value: dashboard.id,
label: dashboard.name || 'New dashboard', label: dashboard.name || 'New dashboard',
selected: selectedDashboards.some((d) => d.id === dashboard.id),
onClick: () => onClickItem(dashboard), onClick: () => onClickItem(dashboard),
link: createBusterRoute({ link: createBusterRoute({
route: BusterRoutes.APP_DASHBOARD_ID, route: BusterRoutes.APP_DASHBOARD_ID,
@ -51,10 +52,6 @@ export const SaveToDashboardDropdown: React.FC<{
[dashboardsList] [dashboardsList]
); );
const selectedItems = useMemo(() => {
return selectedDashboards.map((d) => d.id);
}, [selectedDashboards]);
const onClickNewDashboardButton = useMemoizedFn(async () => { const onClickNewDashboardButton = useMemoizedFn(async () => {
const res = await onCreateNewDashboard({ const res = await onCreateNewDashboard({
rerouteToDashboard: false rerouteToDashboard: false
@ -82,8 +79,6 @@ export const SaveToDashboardDropdown: React.FC<{
setShowDropdown(open); setShowDropdown(open);
}); });
const memoizedTrigger = useMemo<AppDropdownSelectProps['trigger']>(() => ['click'], []);
const memoizedButton = useMemo(() => { const memoizedButton = useMemo(() => {
return ( return (
<Button <Button
@ -99,18 +94,15 @@ export const SaveToDashboardDropdown: React.FC<{
}, [isCreatingDashboard, onClickNewDashboardButton]); }, [isCreatingDashboard, onClickNewDashboardButton]);
return ( return (
<> <Dropdown
<AppDropdownSelect side="bottom"
trigger={memoizedTrigger} align="start"
headerContent={'Save to a dashboard'} menuHeader={'Save to a dashboard'}
placement="bottomRight" open={showDropdown}
open={showDropdown} onOpenChange={onOpenChange}
onOpenChange={onOpenChange} footerContent={memoizedButton}
footerContent={memoizedButton} items={items}>
items={items} <AppTooltip title={showDropdown ? '' : 'Save to collection'}>{children} </AppTooltip>
selectedItems={selectedItems}> </Dropdown>
<AppTooltip title={showDropdown ? '' : 'Save to dashboard'}>{children}</AppTooltip>
</AppDropdownSelect>
</>
); );
}; };

View File

@ -1,11 +1,12 @@
import { Text, BusterUserAvatar } from '@/components/ui'; import { Text } from '@/components/ui';
import { Avatar } from '@/components/ui/avatar';
import React from 'react'; import React from 'react';
export const ListUserItem = React.memo(({ name, email }: { name: string; email: string }) => { export const ListUserItem = React.memo(({ name, email }: { name: string; email: string }) => {
return ( return (
<div className="flex w-full items-center space-x-1.5"> <div className="flex w-full items-center space-x-1.5">
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<BusterUserAvatar size={18} name={name} /> <Avatar className="h-[18px] w-[18px]" name={name} />
</div> </div>
<div className="flex flex-col space-y-0"> <div className="flex flex-col space-y-0">

View File

@ -1,4 +1,4 @@
import { BusterUserAvatar } from '@/components/ui'; import { Avatar } from '@/components/ui/avatar';
import { AccessDropdown } from './AccessDropdown'; import { AccessDropdown } from './AccessDropdown';
import React from 'react'; import React from 'react';
@ -24,7 +24,7 @@ export const IndividualSharePerson: React.FC<{
<div className="flex items-center justify-between space-x-2 px-0 py-1"> <div className="flex items-center justify-between space-x-2 px-0 py-1">
<div className="flex h-full items-center space-x-2"> <div className="flex h-full items-center space-x-2">
<div className="flex h-full flex-col items-center justify-center"> <div className="flex h-full flex-col items-center justify-center">
<BusterUserAvatar size={24} name={name || email} /> <Avatar className="h-[24px] w-[24px]" name={name || email} />
</div> </div>
<div className="flex flex-col space-y-0"> <div className="flex flex-col space-y-0">
<Text className="">{name || email}</Text> <Text className="">{name || email}</Text>

View File

@ -5,21 +5,27 @@ import { Tooltip } from '../tooltip/Tooltip';
import { BusterLogo } from '@/assets/svg/BusterLogo'; import { BusterLogo } from '@/assets/svg/BusterLogo';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
export interface BusterUserAvatarProps { export interface AvatarProps {
image?: string; image?: string;
name?: string | null; name?: string | null;
className?: string; className?: string;
useToolTip?: boolean; useToolTip?: boolean;
size?: number;
} }
export const Avatar: React.FC<BusterUserAvatarProps> = React.memo( export const Avatar: React.FC<AvatarProps> = React.memo(
({ image, name, className, useToolTip }) => { ({ image, name, className, useToolTip, size }) => {
const hasName = !!name; const hasName = !!name;
const nameLetters = hasName ? createNameLetters(name, image) : ''; const nameLetters = hasName ? createNameLetters(name, image) : '';
return ( return (
<Tooltip title={useToolTip ? nameLetters : ''}> <Tooltip title={useToolTip ? nameLetters : ''}>
<AvatarBase className={className}> <AvatarBase
className={className}
style={{
width: size,
height: size
}}>
{image && <AvatarImage src={image} />} {image && <AvatarImage src={image} />}
<AvatarFallback className={cn(!hasName && 'border bg-white')}> <AvatarFallback className={cn(!hasName && 'border bg-white')}>
{nameLetters || <BusterAvatarFallback />} {nameLetters || <BusterAvatarFallback />}

View File

@ -2,7 +2,6 @@
import * as React from 'react'; import * as React from 'react';
import * as AvatarPrimitive from '@radix-ui/react-avatar'; import * as AvatarPrimitive from '@radix-ui/react-avatar';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
const Avatar = React.forwardRef< const Avatar = React.forwardRef<

View File

@ -0,0 +1 @@
export * from './Avatar';

View File

@ -1,4 +1,3 @@
import { createStyles } from 'antd-style';
import React from 'react'; import React from 'react';
import { AppCodeEditor } from '../inputs/AppCodeEditor'; import { AppCodeEditor } from '../inputs/AppCodeEditor';
import { useMemoizedFn } from 'ahooks'; import { useMemoizedFn } from 'ahooks';
@ -29,8 +28,6 @@ export const CodeCard: React.FC<{
onChange, onChange,
readOnly = false readOnly = false
}) => { }) => {
const { styles, cx } = useStyles();
const ShownButtons = buttons === true ? <CardButtons fileName={fileName} code={code} /> : buttons; const ShownButtons = buttons === true ? <CardButtons fileName={fileName} code={code} /> : buttons;
return ( return (
@ -42,7 +39,7 @@ export const CodeCard: React.FC<{
</div> </div>
</CardHeader> </CardHeader>
<CardContent className={cn('bg-background overflow-hidden p-0', bodyClassName)}> <CardContent className={cn('bg-background overflow-hidden p-0', bodyClassName)}>
<div className={cx(styles.containerBody, bodyClassName)}> <div className={cn('bg-background', bodyClassName)}>
<AppCodeEditor <AppCodeEditor
language={language} language={language}
value={code} value={code}
@ -102,18 +99,3 @@ const CardButtons: React.FC<{
); );
}); });
CardButtons.displayName = 'CardButtons'; CardButtons.displayName = 'CardButtons';
const useStyles = createStyles(({ token, css }) => ({
container: css`
border-radius: ${token.borderRadius}px;
border: 0.5px solid ${token.colorBorder};
`,
containerHeader: css`
background: ${token.controlItemBgActive};
border-bottom: 0.5px solid ${token.colorBorder};
height: 32px;
`,
containerBody: css`
background: ${token.colorBgBase};
`
}));

View File

@ -302,7 +302,7 @@ export const WithSelectionMultiple: Story = {
<Dropdown <Dropdown
selectType="multiple" selectType="multiple"
items={items} items={items}
menuHeader={{ placeholder: 'Search items...' }} menuHeader={'Search items...'}
onSelect={handleSelect} onSelect={handleSelect}
children={<Button>Selection Menu</Button>} children={<Button>Selection Menu</Button>}
/> />
@ -345,9 +345,7 @@ export const WithSecondaryLabel: Story = {
// Example with search header // Example with search header
export const WithSearchHeader: Story = { export const WithSearchHeader: Story = {
args: { args: {
menuHeader: { menuHeader: 'Search items...',
placeholder: 'Search items...'
},
items: [ items: [
{ {
value: '1', value: '1',
@ -393,9 +391,7 @@ export const WithSearchHeader: Story = {
// Example with long text to test truncation // Example with long text to test truncation
export const WithLongText: Story = { export const WithLongText: Story = {
args: { args: {
menuHeader: { menuHeader: 'Search items...',
placeholder: 'Search items...'
},
items: [ items: [
...Array.from({ length: 100 }).map(() => { ...Array.from({ length: 100 }).map(() => {
const label = faker.commerce.product(); const label = faker.commerce.product();
@ -508,10 +504,38 @@ export const WithLinksAndMultipleSelection: Story = {
open open
selectType="multiple" selectType="multiple"
items={items} items={items}
menuHeader={{ placeholder: 'Search documentation...' }} menuHeader="Search documentation..."
onSelect={handleSelect} onSelect={handleSelect}
children={<Button>Documentation Sections</Button>} children={<Button>Documentation Sections</Button>}
/> />
); );
} }
}; };
export const WithFooterContent: Story = {
args: {
items: [
{
value: '1',
label: 'Option 1',
onClick: () => alert('Option 1 clicked')
},
{
value: '2',
label: 'Option 2',
onClick: () => alert('Option 2 clicked')
},
{
value: '3',
label: 'Option 3',
onClick: () => alert('Option 3 clicked')
}
],
footerContent: (
<Button variant={'black'} block>
Footer Content
</Button>
),
children: <Button>Menu with Footer Content</Button>
}
};

View File

@ -48,9 +48,9 @@ export interface DropdownDivider {
export type DropdownItems = (DropdownItem | DropdownDivider | React.ReactNode)[]; export type DropdownItems = (DropdownItem | DropdownDivider | React.ReactNode)[];
export interface DropdownProps extends DropdownMenuProps { export interface DropdownProps extends DropdownMenuProps {
items?: DropdownItems; items: DropdownItems;
selectType?: 'single' | 'multiple' | 'none'; selectType?: 'single' | 'multiple' | 'none';
menuHeader?: string | React.ReactNode | { placeholder: string }; menuHeader?: string | React.ReactNode; //if string it will render a search box
minWidth?: number; minWidth?: number;
maxWidth?: number; maxWidth?: number;
closeOnSelect?: boolean; closeOnSelect?: boolean;
@ -59,6 +59,7 @@ export interface DropdownProps extends DropdownMenuProps {
side?: 'top' | 'right' | 'bottom' | 'left'; side?: 'top' | 'right' | 'bottom' | 'left';
emptyStateText?: string; emptyStateText?: string;
className?: string; className?: string;
footerContent?: React.ReactNode;
} }
const dropdownItemKey = (item: DropdownItems[number], index: number) => { const dropdownItemKey = (item: DropdownItems[number], index: number) => {
@ -84,6 +85,7 @@ export const Dropdown: React.FC<DropdownProps> = React.memo(
onOpenChange, onOpenChange,
emptyStateText = 'No items found', emptyStateText = 'No items found',
className, className,
footerContent,
...props ...props
}) => { }) => {
const { filteredItems, searchText, handleSearchChange } = useDebounceSearch({ const { filteredItems, searchText, handleSearchChange } = useDebounceSearch({
@ -133,7 +135,8 @@ export const Dropdown: React.FC<DropdownProps> = React.memo(
<DropdownMenuContent <DropdownMenuContent
className={cn('max-w-72 min-w-44', className)} className={cn('max-w-72 min-w-44', className)}
align={align} align={align}
side={side}> side={side}
footerContent={footerContent}>
{menuHeader && ( {menuHeader && (
<> <>
<DropdownMenuHeaderSelector <DropdownMenuHeaderSelector
@ -357,17 +360,11 @@ const DropdownMenuHeaderSelector: React.FC<{
onChange: (text: string) => void; onChange: (text: string) => void;
text: string; text: string;
}> = React.memo(({ menuHeader, onChange, text }) => { }> = React.memo(({ menuHeader, onChange, text }) => {
// if (typeof menuHeader === 'string') {
// return <DropdownMenuLabel>{menuHeader}</DropdownMenuLabel>;
// }
if (typeof menuHeader === 'string') { if (typeof menuHeader === 'string') {
return <DropdownMenuLabel>{menuHeader}</DropdownMenuLabel>; return <DropdownMenuHeaderSearch placeholder={menuHeader} onChange={onChange} text={text} />;
}
if (typeof menuHeader === 'object' && 'placeholder' in menuHeader) {
return (
<DropdownMenuHeaderSearch
placeholder={menuHeader.placeholder}
onChange={onChange}
text={text}
/>
);
} }
return menuHeader; return menuHeader;
}); });

View File

@ -47,7 +47,11 @@ const DropdownMenuSubTrigger = React.forwardRef<
)); ));
DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName; DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName;
const baseContentClass = `bg-background text-foreground 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 rounded-md border p-1`; 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'
);
const DropdownMenuSubContent = React.forwardRef< const DropdownMenuSubContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>, React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
@ -63,17 +67,25 @@ DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayNam
const DropdownMenuContent = React.forwardRef< const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>, React.ElementRef<typeof DropdownMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content> React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content> & {
>(({ className, sideOffset = 4, ...props }, ref) => ( footerContent?: React.ReactNode;
<DropdownMenuPrimitive.Portal> }
<DropdownMenuPrimitive.Content >(({ className, children, sideOffset = 4, footerContent, ...props }, ref) => {
ref={ref} const NodeWrapper = footerContent ? 'div' : React.Fragment;
sideOffset={sideOffset}
className={cn(baseContentClass, 'shadow-md', className)} return (
{...props} <DropdownMenuPrimitive.Portal>
/> <DropdownMenuPrimitive.Content
</DropdownMenuPrimitive.Portal> ref={ref}
)); sideOffset={sideOffset}
className={cn(baseContentClass, 'shadow-md', footerContent && 'p-0', className)}
{...props}>
<NodeWrapper className={cn(footerContent && 'p-2')}>{children}</NodeWrapper>
{footerContent && <div className="border-t p-2">{footerContent}</div>}
</DropdownMenuPrimitive.Content>
</DropdownMenuPrimitive.Portal>
);
});
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName; DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
const DropdownMenuItem = React.forwardRef< const DropdownMenuItem = React.forwardRef<

View File

@ -1,2 +1,2 @@
export * from './AppDropdownSelect';
export * from './DropdownLabel'; export * from './DropdownLabel';
export * from './Dropdown';

View File

@ -23,14 +23,14 @@ export const Default: Story = {
// Error state // Error state
export const WithError: Story = { export const WithError: Story = {
args: { args: {
children: <div>This content won't be visible due to error</div> children: <div>{`This content won't be visible due to error`} </div>
}, },
parameters: { parameters: {
error: new Error('Simulated error for story') error: new Error(`Simulated error for story`)
}, },
render: (args) => { render: (args) => {
const ErrorTrigger = () => { const ErrorTrigger = () => {
throw new Error('Simulated error for story'); throw new Error(`Simulated error for story`);
}; };
return ( return (

View File

@ -3,10 +3,8 @@
import React, { useMemo, useRef, useState } from 'react'; import React, { useMemo, useRef, useState } from 'react';
import { BusterResizeableGridRow } from './interfaces'; import { BusterResizeableGridRow } from './interfaces';
import { BusterResizeColumns } from './BusterResizeColumns'; import { BusterResizeColumns } from './BusterResizeColumns';
import clsx from 'clsx';
import { BusterNewItemDropzone } from './_BusterBusterNewItemDropzone'; import { BusterNewItemDropzone } from './_BusterBusterNewItemDropzone';
import { MIN_ROW_HEIGHT, TOP_SASH_ID, NEW_ROW_ID, MAX_ROW_HEIGHT } from './config'; import { MIN_ROW_HEIGHT, TOP_SASH_ID, NEW_ROW_ID, MAX_ROW_HEIGHT } from './config';
import { createStyles } from 'antd-style';
import clamp from 'lodash/clamp'; import clamp from 'lodash/clamp';
import { useDebounceFn, useMemoizedFn, useUpdateLayoutEffect } from 'ahooks'; import { useDebounceFn, useMemoizedFn, useUpdateLayoutEffect } from 'ahooks';
import { useDroppable } from '@dnd-kit/core'; import { useDroppable } from '@dnd-kit/core';
@ -58,7 +56,7 @@ export const BusterResizeRows: React.FC<{
return ( return (
<div <div
ref={ref} ref={ref}
className={clsx( className={cn(
className, className,
'buster-resize-row relative', 'buster-resize-row relative',
'mb-10 flex h-full w-full flex-col space-y-3 transition', 'mb-10 flex h-full w-full flex-col space-y-3 transition',
@ -120,7 +118,6 @@ const ResizeRowHandle: React.FC<{
hideDropzone?: boolean; hideDropzone?: boolean;
}> = React.memo( }> = React.memo(
({ hideDropzone, top, id, active, allowEdit, setIsDraggingResizeId, index, sizes, onResize }) => { ({ hideDropzone, top, id, active, allowEdit, setIsDraggingResizeId, index, sizes, onResize }) => {
const { styles } = useStyles();
const { setNodeRef, isOver, over } = useDroppable({ const { setNodeRef, isOver, over } = useDroppable({
id: `${NEW_ROW_ID}_${id}}`, id: `${NEW_ROW_ID}_${id}}`,
disabled: !allowEdit, disabled: !allowEdit,
@ -193,24 +190,3 @@ const ResizeRowHandle: React.FC<{
); );
ResizeRowHandle.displayName = 'ResizeRowHandle'; ResizeRowHandle.displayName = 'ResizeRowHandle';
const useStyles = createStyles(({ css, token }) => ({
hitArea: css`
left: 0;
right: 0;
height: 54px; // Reduced from 54px to be more reasonable
position: absolute;
z-index: 9;
pointer-events: none;
opacity: 0.2;
&.top {
top: -36px; // Position the hit area to straddle the dragger
}
&:not(.top) {
bottom: -15px; // Position the hit area to straddle the dragger
}
`
}));

View File

@ -1,3 +1,4 @@
import React from 'react';
import type { Meta, StoryObj } from '@storybook/react'; import type { Meta, StoryObj } from '@storybook/react';
import { BusterResizeableGrid } from './BusterResizeableGrid'; import { BusterResizeableGrid } from './BusterResizeableGrid';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';

View File

@ -0,0 +1,96 @@
import type { Meta, StoryObj } from '@storybook/react';
import { AppDataSourceIcon } from './AppDataSourceIcons';
import { DataSourceTypes } from '@/api/asset_interfaces';
const meta: Meta<typeof AppDataSourceIcon> = {
title: 'Base/Icons/DataSourceIcon',
component: AppDataSourceIcon,
parameters: {
layout: 'centered'
},
tags: ['autodocs'],
argTypes: {
type: {
control: 'select',
options: Object.values(DataSourceTypes),
description: 'The type of data source to display'
},
size: {
control: 'number',
description: 'Size of the icon in pixels'
},
onClick: {
description: 'Optional click handler'
},
className: {
description: 'Optional CSS class name'
}
}
};
export default meta;
type Story = StoryObj<typeof AppDataSourceIcon>;
// Base story showing all data source icons
export const AllDataSourceIcons: Story = {
render: () => (
<div className="grid grid-cols-4 gap-4 p-4">
{Object.values(DataSourceTypes).map((type) => (
<div key={type} className="flex flex-col items-center gap-2">
<AppDataSourceIcon type={type} size={32} />
<span className="text-sm">{type}</span>
</div>
))}
</div>
)
};
// Individual icon stories
export const PostgresIcon: Story = {
args: {
type: DataSourceTypes.postgres,
size: 32
}
};
export const MySQLIcon: Story = {
args: {
type: DataSourceTypes.mysql,
size: 32
}
};
export const SnowflakeIcon: Story = {
args: {
type: DataSourceTypes.snowflake,
size: 32
}
};
export const BigQueryIcon: Story = {
args: {
type: DataSourceTypes.bigquery,
size: 32
}
};
// Interactive example with different sizes
export const InteractiveSizes: Story = {
render: () => (
<div className="flex items-center gap-4">
<AppDataSourceIcon type={DataSourceTypes.postgres} size={16} />
<AppDataSourceIcon type={DataSourceTypes.postgres} size={24} />
<AppDataSourceIcon type={DataSourceTypes.postgres} size={32} />
<AppDataSourceIcon type={DataSourceTypes.postgres} size={48} />
</div>
)
};
// Clickable example
export const Clickable: Story = {
args: {
type: DataSourceTypes.postgres,
size: 32,
onClick: () => alert('Icon clicked!')
}
};

View File

@ -12,6 +12,7 @@ import { AthenaIcon } from './customIcons/athena';
import React from 'react'; import React from 'react';
import { DataSourceTypes } from '@/api/asset_interfaces'; import { DataSourceTypes } from '@/api/asset_interfaces';
import { AppMaterialIcons } from './AppMaterialIcons'; import { AppMaterialIcons } from './AppMaterialIcons';
import { Database } from './NucleoIconOutlined';
const IconRecord: Record<DataSourceTypes, any> = { const IconRecord: Record<DataSourceTypes, any> = {
[DataSourceTypes.postgres]: PostgresIcon, [DataSourceTypes.postgres]: PostgresIcon,
@ -36,10 +37,8 @@ export const AppDataSourceIcon: React.FC<{
}> = ({ type, ...props }) => { }> = ({ type, ...props }) => {
const ChosenIcon = IconRecord[type]; const ChosenIcon = IconRecord[type];
console;
if (!ChosenIcon) { if (!ChosenIcon) {
return <AppMaterialIcons {...props} icon="database" />; return <Database />;
} }
return <ChosenIcon {...props} />; return <ChosenIcon {...props} />;

View File

@ -1,64 +0,0 @@
import { createStyles } from 'antd-style';
import React from 'react';
const useStyles = createStyles(({ token }) => {
return {
ringContainer: {
position: 'relative'
},
ringring: {
borderRadius: '50%',
border: `2px solid ${token.colorPrimary}`,
position: 'absolute',
animation: 'pulsate 1s ease-out',
animationIterationCount: 'infinite',
opacity: 0.0,
height: '12px',
width: '12px',
left: '-6px',
top: '-6px',
transform: 'translate(-50%, -50%)'
},
circle: {
borderRadius: '50%',
backgroundColor: token.colorPrimary,
position: 'absolute',
top: 0,
left: 0,
transform: 'translate(-50%, -50%)'
}
};
});
export const PulsingDot: React.FC<{
size?: number;
style?: React.CSSProperties;
color?: string;
}> = ({ style, size = 7, color }) => {
const { cx, styles } = useStyles();
return (
<>
<span className={cx('pulsing-dot relative', styles.ringContainer)} style={style}>
<span
className={cx(styles.ringring, 'animate-pulse')}
style={{
// height: size * 1.35,
// width: size * 1.35,
// top: -(size * 1.5155) / 2,
// left: -(size * 1.5475) / 2,
borderColor: color
}}
/>
<span
className={cx(styles.circle)}
style={{
width: size,
height: size,
backgroundColor: color
}}
/>
</span>
</>
);
};

View File

@ -1,169 +0,0 @@
import React, { useMemo } from 'react';
import { Avatar, AvatarProps } from 'antd';
import { getFirstTwoCapitalizedLetters } from '@/lib/text';
import { AppTooltip } from '../tooltip';
import type { GroupProps } from 'antd/es/avatar';
import BusterIconWhite from '@/assets/png/buster_icon_small_white.png';
import BusterIconBlack from '@/assets/png/buster_icon_small_black.png';
import { createStyles } from 'antd-style';
export interface BusterUserAvatarProps extends AvatarProps {
color?: string;
image?: string;
name?: string | null;
style?: React.CSSProperties;
useToolTip?: boolean;
}
export const BusterUserAvatar = React.memo(
({ useToolTip = true, ...props }: BusterUserAvatarProps) => {
const { size = 38, ...restProps } = props;
if (!props.name && !props.image) return <BusterAvatar {...restProps} size={size as number} />;
const firstAndLastInitial = createNameLetters(props.name, props.image);
return (
<AppTooltip
title={useToolTip ? props.name || '' : ''}
mouseEnterDelay={0.35}
performant={useToolTip}>
<Avatar
{...props}
className={`${props.className || ''} flex ${props.image ? 'bg-transparent!' : ''}`}
size={size}
style={{
minWidth: size as number,
minHeight: size as number
}}
src={createAvatarImage(props.image, firstAndLastInitial, props.name)}>
{firstAndLastInitial}
</Avatar>
</AppTooltip>
);
}
);
BusterUserAvatar.displayName = 'BusterUserAvatar';
const useStyles = createStyles(({ css, token, isDarkMode }) => {
return {
groupAvatar: css`
.busterv2-avatar {
font-size: ${token.fontSizeSM} !important;
}
`,
avatar: {
background: isDarkMode ? token.colorBgSpotlight : token.colorFillContentHover
}
};
});
export const BusterUserAvatarGroup: React.FC<
{
avatars: BusterUserAvatarProps[];
} & GroupProps
> = React.memo(({ avatars, ...props }) => {
const { styles, cx } = useStyles();
const renderedAvatars = useMemo(
() =>
avatars.map((avatar, index) => (
<BusterUserAvatar size={props.size} key={index} {...avatar} />
)),
[avatars, props.size]
);
return (
<>
<Avatar.Group {...props} className={cx(styles.groupAvatar, props.className)}>
{renderedAvatars}
</Avatar.Group>
</>
);
});
BusterUserAvatarGroup.displayName = 'BusterUserAvatarGroup';
const createNameLetters = (name?: string | null, image?: string | null | React.ReactNode) => {
if (name && !image) {
const firstTwoLetters = getFirstTwoCapitalizedLetters(name);
if (firstTwoLetters.length == 2) return firstTwoLetters;
//Get First Name Initial
const _name = name.split(' ') as [string, string];
const returnName = `${_name[0][0]}`.toUpperCase().replace('@', '');
return returnName;
}
return '';
};
const createAvatarImage = (
image: string | null | React.ReactNode,
initialNames?: null | string,
fullname?: string | null
) => {
if (image) return image;
if (!initialNames) return;
//USER AVATARS
// const removeHash = hex.replace('#', '');
// const baseUrl = `https://ui-avatars.com/api/?background=${removeHash}&color=fff&name=${name}`;
// return baseUrl;
//VERCEL
// const baseUrl = `https://avatar.vercel.sh/${fullname}.svg?text=${initialNames}`;
// return baseUrl;
//DICE BEAR INDENTICON
const baseUrl = `https://api.dicebear.com/9.x/initials/svg?seed=${fullname || initialNames}`;
return baseUrl;
};
export const BusterAvatar: React.FC<{
size?: number;
shape?: 'circle' | 'square';
}> = React.memo(({ size = 24, shape = 'circle' }) => {
const { styles, cx } = useStyles();
const memoizedStyles = useMemo(
() => ({
minWidth: size,
minHeight: size
}),
[size]
);
return (
<AppTooltip title={'Buster'}>
<Avatar
size={size}
shape={shape}
icon={<BusterImage />}
className={cx(styles.avatar)}
style={memoizedStyles}
/>
</AppTooltip>
);
});
BusterAvatar.displayName = 'BusterAvatar';
const BusterImage: React.FC = () => {
const isDarkMode = false;
const image = isDarkMode ? BusterIconBlack.src : BusterIconWhite.src;
return (
<div
className={`flex h-full w-full items-center justify-center ${
isDarkMode ? 'bg-gray-900' : 'bg-white'
}`}>
<img
className="flex items-center justify-center"
style={{
height: '100%',
width: 'auto',
objectFit: 'contain'
}}
src={image}
alt="Company Logo"
/>
</div>
);
};

View File

@ -1 +0,0 @@
export * from './BusterUserAvatar';

View File

@ -4,7 +4,6 @@ export * from './icons';
export * from './loaders'; export * from './loaders';
export * from './inputs'; export * from './inputs';
export * from './tooltip'; export * from './tooltip';
export * from './image';
export * from './select'; export * from './select';
export * from './segmented'; export * from './segmented';
export * from './card'; export * from './card';

View File

@ -0,0 +1,82 @@
import type { Meta, StoryObj } from '@storybook/react';
import { AppCodeEditor } from './AppCodeEditor';
const meta: Meta<typeof AppCodeEditor> = {
title: 'Base/Inputs/AppCodeEditor',
component: AppCodeEditor,
parameters: {
layout: 'centered'
},
tags: ['autodocs'],
decorators: [
(Story) => (
<div className="min-w-[500px]">
<Story />
</div>
)
]
};
export default meta;
type Story = StoryObj<typeof AppCodeEditor>;
const sampleSQLCode = `SELECT users.name, orders.order_date
FROM users
JOIN orders ON users.id = orders.user_id
WHERE orders.status = 'completed'
ORDER BY orders.order_date DESC;`;
const sampleYAMLCode = `version: '3'
services:
web:
image: nginx:latest
ports:
- "80:80"
volumes:
- ./src:/usr/share/nginx/html`;
export const Default: Story = {
args: {
value: sampleSQLCode,
height: '300px',
language: 'pgsql',
variant: 'bordered'
}
};
export const ReadOnly: Story = {
args: {
value: sampleSQLCode,
height: '300px',
language: 'pgsql',
readOnly: true,
variant: 'bordered',
readOnlyMessage: 'This is a read-only view'
}
};
export const YAMLEditor: Story = {
args: {
value: sampleYAMLCode,
height: '300px',
language: 'yaml',
variant: 'bordered'
}
};
export const CustomHeight: Story = {
args: {
value: sampleSQLCode,
height: '500px',
language: 'pgsql',
variant: 'bordered'
}
};
export const EmptyEditor: Story = {
args: {
height: '200px',
language: 'pgsql',
variant: 'bordered'
}
};

View File

@ -6,8 +6,8 @@
import React, { forwardRef, useMemo } from 'react'; import React, { forwardRef, useMemo } from 'react';
import type { editor } from 'monaco-editor/esm/vs/editor/editor.api'; import type { editor } from 'monaco-editor/esm/vs/editor/editor.api';
import { CircleSpinnerLoaderContainer } from '../../loaders/CircleSpinnerLoaderContainer'; import { CircleSpinnerLoaderContainer } from '../../loaders/CircleSpinnerLoaderContainer';
import { createStyles } from 'antd-style';
import { useMemoizedFn } from 'ahooks'; import { useMemoizedFn } from 'ahooks';
import { cn } from '@/lib/classMerge';
import './MonacoWebWorker'; import './MonacoWebWorker';
import { configureMonacoToUseYaml } from './yamlHelper'; import { configureMonacoToUseYaml } from './yamlHelper';
@ -19,19 +19,6 @@ import { configureMonacoToUseYaml } from './yamlHelper';
import { Editor as DynamicEditor } from '@monaco-editor/react'; import { Editor as DynamicEditor } from '@monaco-editor/react';
import { useTheme } from 'next-themes'; import { useTheme } from 'next-themes';
const useStyles = createStyles(({ token }) => ({
code: {
fontSize: '13px',
fontFamily: 'Menlo, Monaco, "Courier New", monospace',
'--font-app': 'Menlo, Monaco, "Courier New", monospace'
},
bordered: {
border: `0.5px solid ${token.colorBorder}`,
borderRadius: `${token.borderRadiusLG}px`,
overflow: 'hidden'
}
}));
export interface AppCodeEditorProps { export interface AppCodeEditorProps {
className?: string; className?: string;
onChangeEditorHeight?: (height: number) => void; onChangeEditorHeight?: (height: number) => void;
@ -75,7 +62,7 @@ export const AppCodeEditor = forwardRef<AppCodeEditorHandle, AppCodeEditorProps>
}, },
ref ref
) => { ) => {
const { cx, styles } = useStyles(); // const { cx, styles } = useStyles();
const isDarkModeContext = useTheme()?.theme === 'dark'; const isDarkModeContext = useTheme()?.theme === 'dark';
const useDarkMode = isDarkMode ?? isDarkModeContext; const useDarkMode = isDarkMode ?? isDarkModeContext;
@ -148,9 +135,11 @@ export const AppCodeEditor = forwardRef<AppCodeEditorHandle, AppCodeEditorProps>
return ( return (
<div <div
className={cx('app-code-editor relative h-full w-full border', className, styles.code, { className={cn(
[styles.bordered]: variant === 'bordered' 'app-code-editor relative h-full w-full border',
})} variant === 'bordered' && 'overflow-hidden border',
className
)}
style={style}> style={style}>
<DynamicEditor <DynamicEditor
key={useDarkMode ? 'dark' : 'light'} key={useDarkMode ? 'dark' : 'light'}

View File

@ -1,44 +0,0 @@
import { useMemoizedFn } from 'ahooks';
import { Input } from 'antd';
import React, { ReactNode } from 'react';
interface AppSearchInputProps {
value: string;
onChange: (value: string) => void;
onBlur?: (value: string) => void;
onPressEnter?: (value: string) => void;
onSearch: (e?: string) => void;
enterButton?: string | ReactNode | boolean;
loading?: boolean;
size?: 'large' | 'middle' | 'small';
disabled?: boolean;
placeholder?: string;
}
export const AppSearchInput: React.FC<AppSearchInputProps> = ({
size = 'middle',
onChange,
onSearch,
...props
}) => {
const onBlurEvent = useMemoizedFn((e: React.FocusEvent<HTMLInputElement, Element>) => {
props.onBlur?.(e.target.value);
});
const onPressEnterEvent = useMemoizedFn((e: React.KeyboardEvent<HTMLInputElement>) => {
props.onPressEnter && props.onPressEnter(props.value);
});
return (
<Input.Search
{...props}
onBlur={onBlurEvent}
onPressEnter={(e) => onPressEnterEvent(e)}
onChange={(e) => onChange(e.target.value)}
size={size}
onSearch={(e) => onSearch(e)}
/>
);
};
AppSearchInput.displayName = 'AppSearchInput';

View File

@ -1,29 +0,0 @@
'use client';
import React, { ChangeEvent } from 'react';
import { Input, InputProps, InputRef } from 'antd';
import { useMemoizedFn } from 'ahooks';
import { AppMaterialIcons } from '../icons';
interface SearchInputProps extends Omit<InputProps, 'onChange'> {
placeholder: string;
onChange: (value: string) => void;
}
export const SearchInput = React.forwardRef<InputRef, SearchInputProps>(
({ onChange, ...rest }, ref) => {
const onChangePreflight = useMemoizedFn((e: ChangeEvent<HTMLInputElement>) => {
onChange(e.target.value);
});
return (
<Input
ref={ref}
prefix={<AppMaterialIcons icon="search" />}
onChange={onChangePreflight}
{...rest}
/>
);
}
);
SearchInput.displayName = 'SearchInput';

View File

@ -1,2 +1 @@
export * from './AppSearchInput'; export * from './Input';
export * from './SearchInput';

View File

@ -1,11 +1,9 @@
'use client'; 'use client';
import { import { useBusterCollectionIndividualContextSelector } from '@/context/Collections';
useBusterCollectionIndividualContextSelector,
useCollectionIndividual
} from '@/context/Collections';
import React, { useMemo, useState } from 'react'; import React, { useMemo, useState } from 'react';
import { AppMaterialIcons, BusterUserAvatar } from '@/components/ui'; import { AppMaterialIcons } from '@/components/ui';
import { Avatar } from '@/components/ui/avatar';
import { createBusterRoute, BusterRoutes } from '@/routes'; import { createBusterRoute, BusterRoutes } from '@/routes';
import { formatDate } from '@/lib'; import { formatDate } from '@/lib';
import { import {
@ -93,11 +91,7 @@ const columns: BusterListColumn[] = [
width: 50, width: 50,
render: (created_by: BusterCollectionListItem['owner']) => { render: (created_by: BusterCollectionListItem['owner']) => {
return ( return (
<BusterUserAvatar <Avatar image={created_by?.avatar_url || undefined} name={created_by?.name} size={18} />
image={created_by?.avatar_url || undefined}
name={created_by?.name}
size={18}
/>
); );
} }
} }

View File

@ -1,7 +1,8 @@
'use client'; 'use client';
import React, { useMemo, useState } from 'react'; import React, { useMemo, useState } from 'react';
import { AppContent, BusterUserAvatar } from '@/components/ui'; import { AppContent } from '@/components/ui';
import { Avatar } from '@/components/ui/avatar';
import { formatDate, makeHumanReadble } from '@/lib'; import { formatDate, makeHumanReadble } from '@/lib';
import { BusterRoutes, createBusterRoute } from '@/routes'; import { BusterRoutes, createBusterRoute } from '@/routes';
import { useBusterCollectionListContextSelector } from '@/context/Collections'; import { useBusterCollectionListContextSelector } from '@/context/Collections';
@ -74,9 +75,7 @@ const columns: BusterListColumn[] = [
title: 'Owner', title: 'Owner',
width: 50, width: 50,
render: (owner: BusterCollectionListItem['owner']) => { render: (owner: BusterCollectionListItem['owner']) => {
return ( return <Avatar image={owner?.avatar_url || undefined} name={owner?.name} size={18} />;
<BusterUserAvatar image={owner?.avatar_url || undefined} name={owner?.name} size={18} />
);
} }
} }
]; ];

View File

@ -3,7 +3,7 @@
import React, { useMemo, useState } from 'react'; import React, { useMemo, useState } from 'react';
import { AppContent } from '@/components/ui/layout/AppContent'; import { AppContent } from '@/components/ui/layout/AppContent';
import { useBusterDashboardContextSelector } from '@/context/Dashboards'; import { useBusterDashboardContextSelector } from '@/context/Dashboards';
import { BusterUserAvatar } from '@/components/ui'; import { Avatar } from '@/components/ui/avatar';
import { formatDate } from '@/lib'; import { formatDate } from '@/lib';
import { BusterList, BusterListColumn, BusterListRow } from '@/components/ui/list'; import { BusterList, BusterListColumn, BusterListRow } from '@/components/ui/list';
import { BusterRoutes, createBusterRoute } from '@/routes'; import { BusterRoutes, createBusterRoute } from '@/routes';
@ -45,7 +45,7 @@ const columns: BusterListColumn[] = [
title: 'Owner', title: 'Owner',
width: 55, width: 55,
render: (_, data) => { render: (_, data) => {
return <BusterUserAvatar image={data?.avatar_url} name={data?.name} size={18} />; return <Avatar image={data?.avatar_url} name={data?.name} size={18} />;
} }
} }
]; ];

View File

@ -2,7 +2,7 @@
import React, { useState, useMemo } from 'react'; import React, { useState, useMemo } from 'react';
import { AppContent } from '@/components/ui/layout/AppContent'; import { AppContent } from '@/components/ui/layout/AppContent';
import { BusterUserAvatar } from '@/components/ui'; import { Avatar } from '@/components/ui/avatar';
import { formatDate } from '@/lib'; import { formatDate } from '@/lib';
import { BusterList, BusterListColumn, BusterListRow } from '@/components/ui/list'; import { BusterList, BusterListColumn, BusterListRow } from '@/components/ui/list';
import { BusterRoutes, createBusterRoute } from '@/routes'; import { BusterRoutes, createBusterRoute } from '@/routes';
@ -45,11 +45,7 @@ const columns: BusterListColumn[] = [
width: 60, width: 60,
render: (_, dataset: BusterDatasetListItem) => ( render: (_, dataset: BusterDatasetListItem) => (
<div className="flex w-full justify-start"> <div className="flex w-full justify-start">
<BusterUserAvatar <Avatar image={dataset.owner.avatar_url || undefined} name={dataset.owner.name} size={18} />
image={dataset.owner.avatar_url || undefined}
name={dataset.owner.name}
size={18}
/>
</div> </div>
) )
} }

View File

@ -2,7 +2,8 @@ import { ShareAssetType, VerificationStatus, BusterMetricListItem } from '@/api/
import { makeHumanReadble, formatDate } from '@/lib'; import { makeHumanReadble, formatDate } from '@/lib';
import React, { memo, useMemo, useRef, useState } from 'react'; import React, { memo, useMemo, useRef, useState } from 'react';
import { StatusBadgeIndicator, getShareStatus } from '@/components/features/Lists'; import { StatusBadgeIndicator, getShareStatus } from '@/components/features/Lists';
import { BusterUserAvatar, Text } from '@/components/ui'; import { Text } from '@/components/ui';
import { Avatar } from '@/components/ui/avatar';
import { BusterRoutes, createBusterRoute } from '@/routes'; import { BusterRoutes, createBusterRoute } from '@/routes';
import { useMemoizedFn } from 'ahooks'; import { useMemoizedFn } from 'ahooks';
import { BusterListColumn, BusterListRow } from '@/components/ui/list'; import { BusterListColumn, BusterListRow } from '@/components/ui/list';
@ -196,7 +197,7 @@ TitleCell.displayName = 'TitleCell';
const OwnerCell = memo<{ name: string; image: string | undefined }>(({ name, image }) => ( const OwnerCell = memo<{ name: string; image: string | undefined }>(({ name, image }) => (
<div className="flex pl-0"> <div className="flex pl-0">
<BusterUserAvatar image={image} name={name} size={18} /> <Avatar image={image} name={name} size={18} />
</div> </div>
)); ));
OwnerCell.displayName = 'OwnerCell'; OwnerCell.displayName = 'OwnerCell';

View File

@ -1,11 +1,12 @@
import { BusterTerm } from '@/api/buster_rest'; import { BusterTerm } from '@/api/buster_rest';
import { AppTooltip, AppMaterialIcons } from '@/components/ui'; import { AppTooltip, AppMaterialIcons } from '@/components/ui';
import { AppDropdownSelectProps, AppDropdownSelect } from '@/components/ui/dropdown'; import { Dropdown, type DropdownProps } from '@/components/ui/dropdown';
import { createStyles } from 'antd-style'; import { createStyles } from 'antd-style';
import React, { useMemo } from 'react'; import React, { useMemo } from 'react';
import { Button } from 'antd'; import { Button } from 'antd';
import { Text } from '@/components/ui'; import { Text } from '@/components/ui';
import { useGetDatasets } from '@/api/buster_rest/datasets'; import { useGetDatasets } from '@/api/buster_rest/datasets';
import { useMemoizedFn } from 'ahooks';
const useStyles = createStyles(({ token, css }) => ({ const useStyles = createStyles(({ token, css }) => ({
datasetItem: css` datasetItem: css`
@ -64,20 +65,18 @@ export const DatasetList: React.FC<{
}); });
DatasetList.displayName = 'DatasetList'; DatasetList.displayName = 'DatasetList';
const memoizedTrigger: AppDropdownSelectProps['trigger'] = ['click'];
const DropdownSelect: React.FC<{ const DropdownSelect: React.FC<{
children: React.ReactNode; children: React.ReactNode;
datasets: BusterTerm['datasets']; datasets: BusterTerm['datasets'];
onChange: (datasets: string[]) => void; onChange: (datasets: string[]) => void;
placement?: AppDropdownSelectProps['placement']; }> = ({ onChange, children, datasets }) => {
}> = ({ onChange, children, datasets, placement = 'bottomRight' }) => {
const { data: datasetsList } = useGetDatasets(); const { data: datasetsList } = useGetDatasets();
const itemsDropdown = useMemo(() => { const itemsDropdown: DropdownProps['items'] = useMemo(() => {
return datasetsList.map((item) => ({ return datasetsList.map<DropdownProps['items'][number]>((item) => ({
key: item.id,
label: item.name, label: item.name,
value: item.id,
selected: datasets.some((i) => i.id === item.id),
onClick: async () => { onClick: async () => {
const isSelected = datasets.find((i) => i.id === item.id); const isSelected = datasets.find((i) => i.id === item.id);
const newDatasets = isSelected const newDatasets = isSelected
@ -88,20 +87,20 @@ const DropdownSelect: React.FC<{
})); }));
}, [datasets, datasetsList]); }, [datasets, datasetsList]);
const selectedItems = useMemo(() => { const onSelect = useMemoizedFn((itemId: string) => {
return datasets.map((item) => item.id); alert('This feature is not implemented yet');
}, [datasets]); });
return ( return (
<AppDropdownSelect <Dropdown
placement={placement} align={'start'}
side="bottom"
selectType="multiple"
items={itemsDropdown} items={itemsDropdown}
destroyPopupOnHide onSelect={onSelect}
headerContent={'Related datasets...'} menuHeader={'Related datasets...'}>
selectedItems={selectedItems}
trigger={memoizedTrigger}>
{children} {children}
</AppDropdownSelect> </Dropdown>
); );
}; };

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { useBusterTermsIndividualContextSelector, useBusterTermsIndividual } from '@/context/Terms'; import { useBusterTermsIndividualContextSelector, useBusterTermsIndividual } from '@/context/Terms';
import { BusterUserAvatar } from '@/components/ui'; import { Avatar } from '@/components/ui/avatar';
import { formatDate } from '@/lib'; import { formatDate } from '@/lib';
import { Text } from '@/components/ui'; import { Text } from '@/components/ui';
import { DatasetList } from './TermDatasetSelect'; import { DatasetList } from './TermDatasetSelect';
@ -44,7 +44,7 @@ export const TermIndividualContentSider: React.FC<{ termId: string }> = ({ termI
</Text> </Text>
<div className="flex items-center space-x-1.5"> <div className="flex items-center space-x-1.5">
<BusterUserAvatar size={24} name={selectedTerm?.created_by.name} /> <Avatar size={24} name={selectedTerm?.created_by.name} />
<Text>{selectedTerm?.created_by.name}</Text> <Text>{selectedTerm?.created_by.name}</Text>
<Text type="secondary"> <Text type="secondary">
( (

View File

@ -2,7 +2,7 @@
import React, { useMemo, useState } from 'react'; import React, { useMemo, useState } from 'react';
import { AppContent } from '@/components/ui/layout/AppContent'; import { AppContent } from '@/components/ui/layout/AppContent';
import { BusterUserAvatar } from '@/components/ui'; import { Avatar } from '@/components/ui/avatar';
import { formatDate } from '@/lib'; import { formatDate } from '@/lib';
import { import {
ListEmptyStateWithButton, ListEmptyStateWithButton,
@ -38,9 +38,7 @@ const columns: BusterListColumn[] = [
dataIndex: 'owner', dataIndex: 'owner',
title: 'Owner', title: 'Owner',
width: 60, width: 60,
render: (_, data: BusterTermListItem) => ( render: (_, data: BusterTermListItem) => <Avatar name={data.created_by.name} size={18} />
<BusterUserAvatar name={data.created_by.name} size={18} />
)
} }
]; ];

View File

@ -1,4 +1,4 @@
import { BusterUserAvatar, BusterAvatar } from '@/components/ui/image'; import { Avatar } from '@/components/ui/avatar';
import { createStyles } from 'antd-style'; import { createStyles } from 'antd-style';
import React from 'react'; import React from 'react';
@ -13,9 +13,9 @@ export const MessageContainer: React.FC<{
return ( return (
<div className={cx('flex w-full space-x-2 overflow-hidden')}> <div className={cx('flex w-full space-x-2 overflow-hidden')}>
{senderName ? ( {senderName ? (
<BusterUserAvatar size={24} name={senderName} src={senderAvatar} useToolTip={true} /> <Avatar size={24} name={senderName} image={senderAvatar || ''} useToolTip={true} />
) : ( ) : (
<BusterAvatar size={24} /> <Avatar size={24} />
)} )}
<div className={cx(className, 'px-1')}>{children}</div> <div className={cx(className, 'px-1')}>{children}</div>
</div> </div>