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 React, { memo, useMemo, useRef, useState } from 'react';
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 { useMemoizedFn } from 'ahooks';
import { BusterListColumn, BusterListRow } from '@/components/ui/list';
@ -180,7 +181,7 @@ TitleCell.displayName = 'TitleCell';
const OwnerCell = memo<{ name: string; image: string | undefined }>(({ name, image }) => (
<div className="flex pl-0">
<BusterUserAvatar image={image} name={name} size={18} />
<Avatar image={image} name={name} className="h-[18px] w-[18px]" />
</div>
));
OwnerCell.displayName = 'OwnerCell';

View File

@ -1,5 +1,6 @@
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 { Button } from 'antd';
@ -17,7 +18,7 @@ UserHeader.displayName = 'UserHeader';
const UserInfo: React.FC<{ user: OrganizationUser }> = ({ user }) => {
return (
<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">
<Title level={4}>{user.name}</Title>
<Text size="sm" type="secondary">

View File

@ -6,7 +6,7 @@ import { useUserConfigContextSelector } from '@/context/Users';
import { createStyles } from 'antd-style';
import { formatDate } from '@/lib/date';
import { Text, Title } from '@/components/ui';
import { BusterUserAvatar } from '@/components/ui';
import { Avatar } from '@/components/ui/avatar';
import { Card } from 'antd';
const useStyles = createStyles(({ token, css }) => ({
@ -41,7 +41,7 @@ export default function ProfilePage() {
body: 'flex flex-col space-y-3'
}}>
<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>
</div>
<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 { Dropdown, type DropdownItem, type DropdownProps } from '@/components/ui/dropdown';
import { AppTooltip } from '@/components/ui/tooltip';
import { useAppLayoutContextSelector } from '@/context/BusterAppLayout';
import { useBusterCollectionListContextSelector } from '@/context/Collections';
@ -21,12 +21,13 @@ export const SaveToCollectionsDropdown: React.FC<{
const [openCollectionModal, setOpenCollectionModal] = 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 {
key: collection.id,
value: collection.id,
label: collection.name,
selected: selectedCollections.some((id) => id === collection.id),
onClick: () => onClickItem(collection),
link: createBusterRoute({
route: BusterRoutes.APP_COLLECTIONS_ID,
@ -34,7 +35,7 @@ export const SaveToCollectionsDropdown: React.FC<{
})
};
}),
[collectionsList]
[collectionsList, selectedCollections]
);
const onClickItem = useMemoizedFn((collection: BusterCollectionListItem) => {
@ -84,22 +85,16 @@ export const SaveToCollectionsDropdown: React.FC<{
return (
<>
<AppDropdownSelect
trigger={['click']}
placement="bottomRight"
className="flex! h-fit! items-center"
headerContent={'Save to a collection'}
<Dropdown
side="bottom"
align="start"
menuHeader={'Save to a collection'}
open={showDropdown}
onOpenChange={onOpenChange}
footerContent={memoizedButton}
items={items}
selectedItems={selectedCollections}>
{showDropdown ? (
<>{children}</>
) : (
<AppTooltip title={showDropdown ? '' : 'Save to collection'}>{children} </AppTooltip>
)}
</AppDropdownSelect>
items={items}>
<AppTooltip title={showDropdown ? '' : 'Save to collection'}>{children} </AppTooltip>
</Dropdown>
<NewCollectionModal
open={openCollectionModal}

View File

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

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';
export const ListUserItem = React.memo(({ name, email }: { name: string; email: string }) => {
return (
<div className="flex w-full items-center space-x-1.5">
<div className="flex items-center space-x-2">
<BusterUserAvatar size={18} name={name} />
<Avatar className="h-[18px] w-[18px]" name={name} />
</div>
<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 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 h-full items-center space-x-2">
<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 className="flex flex-col space-y-0">
<Text className="">{name || email}</Text>

View File

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

View File

@ -2,7 +2,6 @@
import * as React from 'react';
import * as AvatarPrimitive from '@radix-ui/react-avatar';
import { cn } from '@/lib/utils';
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 { AppCodeEditor } from '../inputs/AppCodeEditor';
import { useMemoizedFn } from 'ahooks';
@ -29,8 +28,6 @@ export const CodeCard: React.FC<{
onChange,
readOnly = false
}) => {
const { styles, cx } = useStyles();
const ShownButtons = buttons === true ? <CardButtons fileName={fileName} code={code} /> : buttons;
return (
@ -42,7 +39,7 @@ export const CodeCard: React.FC<{
</div>
</CardHeader>
<CardContent className={cn('bg-background overflow-hidden p-0', bodyClassName)}>
<div className={cx(styles.containerBody, bodyClassName)}>
<div className={cn('bg-background', bodyClassName)}>
<AppCodeEditor
language={language}
value={code}
@ -102,18 +99,3 @@ const CardButtons: React.FC<{
);
});
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
selectType="multiple"
items={items}
menuHeader={{ placeholder: 'Search items...' }}
menuHeader={'Search items...'}
onSelect={handleSelect}
children={<Button>Selection Menu</Button>}
/>
@ -345,9 +345,7 @@ export const WithSecondaryLabel: Story = {
// Example with search header
export const WithSearchHeader: Story = {
args: {
menuHeader: {
placeholder: 'Search items...'
},
menuHeader: 'Search items...',
items: [
{
value: '1',
@ -393,9 +391,7 @@ export const WithSearchHeader: Story = {
// Example with long text to test truncation
export const WithLongText: Story = {
args: {
menuHeader: {
placeholder: 'Search items...'
},
menuHeader: 'Search items...',
items: [
...Array.from({ length: 100 }).map(() => {
const label = faker.commerce.product();
@ -508,10 +504,38 @@ export const WithLinksAndMultipleSelection: Story = {
open
selectType="multiple"
items={items}
menuHeader={{ placeholder: 'Search documentation...' }}
menuHeader="Search documentation..."
onSelect={handleSelect}
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 interface DropdownProps extends DropdownMenuProps {
items?: DropdownItems;
items: DropdownItems;
selectType?: 'single' | 'multiple' | 'none';
menuHeader?: string | React.ReactNode | { placeholder: string };
menuHeader?: string | React.ReactNode; //if string it will render a search box
minWidth?: number;
maxWidth?: number;
closeOnSelect?: boolean;
@ -59,6 +59,7 @@ export interface DropdownProps extends DropdownMenuProps {
side?: 'top' | 'right' | 'bottom' | 'left';
emptyStateText?: string;
className?: string;
footerContent?: React.ReactNode;
}
const dropdownItemKey = (item: DropdownItems[number], index: number) => {
@ -84,6 +85,7 @@ export const Dropdown: React.FC<DropdownProps> = React.memo(
onOpenChange,
emptyStateText = 'No items found',
className,
footerContent,
...props
}) => {
const { filteredItems, searchText, handleSearchChange } = useDebounceSearch({
@ -133,7 +135,8 @@ export const Dropdown: React.FC<DropdownProps> = React.memo(
<DropdownMenuContent
className={cn('max-w-72 min-w-44', className)}
align={align}
side={side}>
side={side}
footerContent={footerContent}>
{menuHeader && (
<>
<DropdownMenuHeaderSelector
@ -357,17 +360,11 @@ const DropdownMenuHeaderSelector: React.FC<{
onChange: (text: string) => void;
text: string;
}> = React.memo(({ menuHeader, onChange, text }) => {
// if (typeof menuHeader === 'string') {
// return <DropdownMenuLabel>{menuHeader}</DropdownMenuLabel>;
// }
if (typeof menuHeader === 'string') {
return <DropdownMenuLabel>{menuHeader}</DropdownMenuLabel>;
}
if (typeof menuHeader === 'object' && 'placeholder' in menuHeader) {
return (
<DropdownMenuHeaderSearch
placeholder={menuHeader.placeholder}
onChange={onChange}
text={text}
/>
);
return <DropdownMenuHeaderSearch placeholder={menuHeader} onChange={onChange} text={text} />;
}
return menuHeader;
});

View File

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

View File

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

View File

@ -23,14 +23,14 @@ export const Default: Story = {
// Error state
export const WithError: Story = {
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: {
error: new Error('Simulated error for story')
error: new Error(`Simulated error for story`)
},
render: (args) => {
const ErrorTrigger = () => {
throw new Error('Simulated error for story');
throw new Error(`Simulated error for story`);
};
return (

View File

@ -3,10 +3,8 @@
import React, { useMemo, useRef, useState } from 'react';
import { BusterResizeableGridRow } from './interfaces';
import { BusterResizeColumns } from './BusterResizeColumns';
import clsx from 'clsx';
import { BusterNewItemDropzone } from './_BusterBusterNewItemDropzone';
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 { useDebounceFn, useMemoizedFn, useUpdateLayoutEffect } from 'ahooks';
import { useDroppable } from '@dnd-kit/core';
@ -58,7 +56,7 @@ export const BusterResizeRows: React.FC<{
return (
<div
ref={ref}
className={clsx(
className={cn(
className,
'buster-resize-row relative',
'mb-10 flex h-full w-full flex-col space-y-3 transition',
@ -120,7 +118,6 @@ const ResizeRowHandle: React.FC<{
hideDropzone?: boolean;
}> = React.memo(
({ hideDropzone, top, id, active, allowEdit, setIsDraggingResizeId, index, sizes, onResize }) => {
const { styles } = useStyles();
const { setNodeRef, isOver, over } = useDroppable({
id: `${NEW_ROW_ID}_${id}}`,
disabled: !allowEdit,
@ -193,24 +190,3 @@ const ResizeRowHandle: React.FC<{
);
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 { BusterResizeableGrid } from './BusterResizeableGrid';
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 { DataSourceTypes } from '@/api/asset_interfaces';
import { AppMaterialIcons } from './AppMaterialIcons';
import { Database } from './NucleoIconOutlined';
const IconRecord: Record<DataSourceTypes, any> = {
[DataSourceTypes.postgres]: PostgresIcon,
@ -36,10 +37,8 @@ export const AppDataSourceIcon: React.FC<{
}> = ({ type, ...props }) => {
const ChosenIcon = IconRecord[type];
console;
if (!ChosenIcon) {
return <AppMaterialIcons {...props} icon="database" />;
return <Database />;
}
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 './inputs';
export * from './tooltip';
export * from './image';
export * from './select';
export * from './segmented';
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 type { editor } from 'monaco-editor/esm/vs/editor/editor.api';
import { CircleSpinnerLoaderContainer } from '../../loaders/CircleSpinnerLoaderContainer';
import { createStyles } from 'antd-style';
import { useMemoizedFn } from 'ahooks';
import { cn } from '@/lib/classMerge';
import './MonacoWebWorker';
import { configureMonacoToUseYaml } from './yamlHelper';
@ -19,19 +19,6 @@ import { configureMonacoToUseYaml } from './yamlHelper';
import { Editor as DynamicEditor } from '@monaco-editor/react';
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 {
className?: string;
onChangeEditorHeight?: (height: number) => void;
@ -75,7 +62,7 @@ export const AppCodeEditor = forwardRef<AppCodeEditorHandle, AppCodeEditorProps>
},
ref
) => {
const { cx, styles } = useStyles();
// const { cx, styles } = useStyles();
const isDarkModeContext = useTheme()?.theme === 'dark';
const useDarkMode = isDarkMode ?? isDarkModeContext;
@ -148,9 +135,11 @@ export const AppCodeEditor = forwardRef<AppCodeEditorHandle, AppCodeEditorProps>
return (
<div
className={cx('app-code-editor relative h-full w-full border', className, styles.code, {
[styles.bordered]: variant === 'bordered'
})}
className={cn(
'app-code-editor relative h-full w-full border',
variant === 'bordered' && 'overflow-hidden border',
className
)}
style={style}>
<DynamicEditor
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 './SearchInput';
export * from './Input';

View File

@ -1,11 +1,9 @@
'use client';
import {
useBusterCollectionIndividualContextSelector,
useCollectionIndividual
} from '@/context/Collections';
import { useBusterCollectionIndividualContextSelector } from '@/context/Collections';
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 { formatDate } from '@/lib';
import {
@ -93,11 +91,7 @@ const columns: BusterListColumn[] = [
width: 50,
render: (created_by: BusterCollectionListItem['owner']) => {
return (
<BusterUserAvatar
image={created_by?.avatar_url || undefined}
name={created_by?.name}
size={18}
/>
<Avatar image={created_by?.avatar_url || undefined} name={created_by?.name} size={18} />
);
}
}

View File

@ -1,7 +1,8 @@
'use client';
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 { BusterRoutes, createBusterRoute } from '@/routes';
import { useBusterCollectionListContextSelector } from '@/context/Collections';
@ -74,9 +75,7 @@ const columns: BusterListColumn[] = [
title: 'Owner',
width: 50,
render: (owner: BusterCollectionListItem['owner']) => {
return (
<BusterUserAvatar image={owner?.avatar_url || undefined} name={owner?.name} size={18} />
);
return <Avatar image={owner?.avatar_url || undefined} name={owner?.name} size={18} />;
}
}
];

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
import React from 'react';
import { useBusterTermsIndividualContextSelector, useBusterTermsIndividual } from '@/context/Terms';
import { BusterUserAvatar } from '@/components/ui';
import { Avatar } from '@/components/ui/avatar';
import { formatDate } from '@/lib';
import { Text } from '@/components/ui';
import { DatasetList } from './TermDatasetSelect';
@ -44,7 +44,7 @@ export const TermIndividualContentSider: React.FC<{ termId: string }> = ({ termI
</Text>
<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 type="secondary">
(

View File

@ -2,7 +2,7 @@
import React, { useMemo, useState } from 'react';
import { AppContent } from '@/components/ui/layout/AppContent';
import { BusterUserAvatar } from '@/components/ui';
import { Avatar } from '@/components/ui/avatar';
import { formatDate } from '@/lib';
import {
ListEmptyStateWithButton,
@ -38,9 +38,7 @@ const columns: BusterListColumn[] = [
dataIndex: 'owner',
title: 'Owner',
width: 60,
render: (_, data: BusterTermListItem) => (
<BusterUserAvatar name={data.created_by.name} size={18} />
)
render: (_, data: BusterTermListItem) => <Avatar 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 React from 'react';
@ -13,9 +13,9 @@ export const MessageContainer: React.FC<{
return (
<div className={cx('flex w-full space-x-2 overflow-hidden')}>
{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>