mirror of https://github.com/buster-so/buster.git
sidebar primary
This commit is contained in:
parent
9c956f7d5b
commit
a6ce9c8c4d
|
@ -0,0 +1,37 @@
|
|||
import { ShareAssetType } from '@/api/asset_interfaces/share';
|
||||
import { Messages, SquareChart, Grid, Folder5 } from '@/components/ui/icons';
|
||||
import { BusterRoutes, createBusterRoute } from '@/routes/busterRoutes';
|
||||
|
||||
export const ASSET_ICONS = {
|
||||
metrics: SquareChart,
|
||||
chats: Messages,
|
||||
dashboards: Grid,
|
||||
collections: Folder5
|
||||
};
|
||||
|
||||
export const assetTypeToIcon = (assetType: ShareAssetType) => {
|
||||
switch (assetType) {
|
||||
case ShareAssetType.METRIC:
|
||||
return ASSET_ICONS.metrics;
|
||||
case ShareAssetType.DASHBOARD:
|
||||
return ASSET_ICONS.dashboards;
|
||||
case ShareAssetType.COLLECTION:
|
||||
return ASSET_ICONS.collections;
|
||||
default:
|
||||
const _result: unknown = assetType;
|
||||
return ASSET_ICONS.metrics;
|
||||
}
|
||||
};
|
||||
|
||||
export const assetTypeToRoute = (assetType: ShareAssetType, assetId: string) => {
|
||||
switch (assetType) {
|
||||
case ShareAssetType.METRIC:
|
||||
return createBusterRoute({ route: BusterRoutes.APP_METRIC_ID, metricId: assetId });
|
||||
case ShareAssetType.DASHBOARD:
|
||||
return createBusterRoute({ route: BusterRoutes.APP_DASHBOARD_ID, dashboardId: assetId });
|
||||
case ShareAssetType.COLLECTION:
|
||||
return createBusterRoute({ route: BusterRoutes.APP_COLLECTIONS_ID, collectionId: assetId });
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
};
|
|
@ -0,0 +1,105 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { SidebarPrimary } from './SidebarPrimary';
|
||||
import { BusterRoutes, createBusterRoute } from '@/routes';
|
||||
import { ShareAssetType } from '@/api/asset_interfaces/share/shareInterfaces';
|
||||
|
||||
const meta: Meta<typeof SidebarPrimary> = {
|
||||
title: 'Features/Sidebars/SidebarPrimary',
|
||||
component: SidebarPrimary,
|
||||
parameters: {
|
||||
layout: 'fullscreen'
|
||||
},
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="bg-background-secondary h-screen w-[300px]">
|
||||
<Story />
|
||||
</div>
|
||||
)
|
||||
]
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof SidebarPrimary>;
|
||||
|
||||
const mockFavorites = [
|
||||
{
|
||||
id: '123',
|
||||
name: 'Favorite Dashboard',
|
||||
asset_type: ShareAssetType.DASHBOARD,
|
||||
asset_id: '123',
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
route: createBusterRoute({ route: BusterRoutes.APP_DASHBOARD_ID, dashboardId: '123' })
|
||||
},
|
||||
{
|
||||
id: '456',
|
||||
name: 'Important Metrics',
|
||||
route: createBusterRoute({ route: BusterRoutes.APP_METRIC_ID, metricId: '456' }),
|
||||
asset_type: ShareAssetType.METRIC,
|
||||
asset_id: '456',
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString()
|
||||
},
|
||||
{
|
||||
id: '789',
|
||||
name: 'Favorite Metric 3',
|
||||
route: createBusterRoute({ route: BusterRoutes.APP_METRIC_ID, metricId: '789' }),
|
||||
asset_type: ShareAssetType.METRIC,
|
||||
asset_id: '789',
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString()
|
||||
}
|
||||
];
|
||||
|
||||
export const AdminUser: Story = {
|
||||
args: {
|
||||
isAdmin: true,
|
||||
activeRoute: BusterRoutes.APP_HOME,
|
||||
activePage: 'home',
|
||||
favorites: mockFavorites,
|
||||
onClickInvitePeople: () => alert('Invite people clicked'),
|
||||
onClickLeaveFeedback: () => alert('Leave feedback clicked')
|
||||
}
|
||||
};
|
||||
|
||||
export const RegularUser: Story = {
|
||||
args: {
|
||||
isAdmin: false,
|
||||
activeRoute: BusterRoutes.APP_HOME,
|
||||
activePage: 'home',
|
||||
favorites: mockFavorites,
|
||||
onClickInvitePeople: () => alert('Invite people clicked'),
|
||||
onClickLeaveFeedback: () => alert('Leave feedback clicked')
|
||||
}
|
||||
};
|
||||
|
||||
export const NoFavorites: Story = {
|
||||
args: {
|
||||
isAdmin: true,
|
||||
activeRoute: BusterRoutes.APP_HOME,
|
||||
activePage: 'home',
|
||||
favorites: null,
|
||||
onClickInvitePeople: () => alert('Invite people clicked'),
|
||||
onClickLeaveFeedback: () => alert('Leave feedback clicked')
|
||||
}
|
||||
};
|
||||
|
||||
export const DifferentActiveRoute: Story = {
|
||||
args: {
|
||||
isAdmin: true,
|
||||
activeRoute: BusterRoutes.APP_CHAT,
|
||||
activePage: 'chat',
|
||||
favorites: mockFavorites,
|
||||
onClickInvitePeople: () => alert('Invite people clicked'),
|
||||
onClickLeaveFeedback: () => alert('Leave feedback clicked')
|
||||
}
|
||||
};
|
||||
|
||||
export const FavoritesActiveRoute: Story = {
|
||||
args: {
|
||||
isAdmin: true,
|
||||
favorites: mockFavorites,
|
||||
activeRoute: BusterRoutes.APP_METRIC,
|
||||
activePage: createBusterRoute({ route: BusterRoutes.APP_METRIC_ID, metricId: '456' })
|
||||
}
|
||||
};
|
|
@ -0,0 +1,176 @@
|
|||
import React, { useMemo } from 'react';
|
||||
import { Sidebar } from '@/components/ui/sidebar/Sidebar';
|
||||
import { BusterLogoWithText } from '@/assets/svg/BusterLogoWithText';
|
||||
import { BusterRoutes, createBusterRoute } from '@/routes';
|
||||
import type { ISidebarGroup, ISidebarList, SidebarProps } from '@/components/ui/sidebar/interfaces';
|
||||
import { BookOpen4, Flag, Gear, House4, Table, UnorderedList2, Plus } from '@/components/ui/icons';
|
||||
import { ASSET_ICONS, assetTypeToIcon, assetTypeToRoute } from '../config/assetIcons';
|
||||
import type { BusterUserFavorite } from '@/api/asset_interfaces/users';
|
||||
import { Button } from '@/components/ui/buttons';
|
||||
import { Tooltip } from '@/components/ui/tooltip/Tooltip';
|
||||
import Link from 'next/link';
|
||||
import { PencilSquareIcon } from '@/components/ui/icons/customIcons/Pencil_Square';
|
||||
|
||||
const topItems: ISidebarList = {
|
||||
items: [
|
||||
{
|
||||
label: 'Home',
|
||||
icon: <House4 />,
|
||||
route: BusterRoutes.APP_HOME,
|
||||
id: BusterRoutes.APP_HOME
|
||||
},
|
||||
{
|
||||
label: 'Chat history',
|
||||
icon: <ASSET_ICONS.chats />,
|
||||
route: BusterRoutes.APP_CHAT,
|
||||
id: BusterRoutes.APP_CHAT
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const yourStuff: ISidebarGroup = {
|
||||
label: 'Your stuffs',
|
||||
items: [
|
||||
{
|
||||
label: 'Metrics',
|
||||
icon: <ASSET_ICONS.metrics />,
|
||||
route: BusterRoutes.APP_METRIC,
|
||||
id: BusterRoutes.APP_METRIC
|
||||
},
|
||||
{
|
||||
label: 'Dashboards',
|
||||
icon: <ASSET_ICONS.dashboards />,
|
||||
route: BusterRoutes.APP_DASHBOARDS,
|
||||
id: BusterRoutes.APP_DASHBOARDS
|
||||
},
|
||||
{
|
||||
label: 'Collections',
|
||||
icon: <ASSET_ICONS.collections />,
|
||||
route: BusterRoutes.APP_COLLECTIONS,
|
||||
id: BusterRoutes.APP_COLLECTIONS
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const adminTools: ISidebarGroup = {
|
||||
label: 'Admin tools',
|
||||
items: [
|
||||
{
|
||||
label: 'Logs',
|
||||
icon: <UnorderedList2 />,
|
||||
route: BusterRoutes.APP_LOGS,
|
||||
id: BusterRoutes.APP_LOGS
|
||||
},
|
||||
{
|
||||
label: 'Terms & Definitions',
|
||||
icon: <BookOpen4 />,
|
||||
route: BusterRoutes.APP_TERMS,
|
||||
id: BusterRoutes.APP_TERMS
|
||||
},
|
||||
{
|
||||
label: 'Datasets',
|
||||
icon: <Table />,
|
||||
route: BusterRoutes.APP_DATASETS,
|
||||
id: BusterRoutes.APP_DATASETS
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const tryGroup = (onClickInvitePeople: () => void, onClickLeaveFeedback: () => void) => ({
|
||||
label: 'Try',
|
||||
items: [
|
||||
{
|
||||
label: 'Invite people',
|
||||
icon: <Plus />,
|
||||
route: null,
|
||||
id: 'invite-people',
|
||||
onClick: onClickInvitePeople
|
||||
},
|
||||
{
|
||||
label: 'Leave feedback',
|
||||
icon: <Flag />,
|
||||
route: null,
|
||||
id: 'leave-feedback',
|
||||
onClick: onClickLeaveFeedback
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
export const SidebarPrimary: React.FC<{
|
||||
isAdmin: boolean;
|
||||
activeRoute: BusterRoutes;
|
||||
activePage: string;
|
||||
favorites: BusterUserFavorite[] | null;
|
||||
onClickInvitePeople: () => void;
|
||||
onClickLeaveFeedback: () => void;
|
||||
}> = React.memo(
|
||||
({ isAdmin, activeRoute, activePage, favorites, onClickInvitePeople, onClickLeaveFeedback }) => {
|
||||
const sidebarItems: SidebarProps['content'] = useMemo(() => {
|
||||
const items = [topItems];
|
||||
|
||||
if (isAdmin) {
|
||||
items.push(adminTools);
|
||||
}
|
||||
|
||||
items.push(yourStuff);
|
||||
|
||||
if (favorites && favorites.length > 0) {
|
||||
items.push(favoritesDropdown(favorites));
|
||||
}
|
||||
|
||||
items.push(tryGroup(onClickInvitePeople, onClickLeaveFeedback));
|
||||
|
||||
return items;
|
||||
}, [isAdmin, activePage]);
|
||||
|
||||
return (
|
||||
<Sidebar content={sidebarItems} header={<SidebarPrimaryHeader />} activeItem={activePage} />
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
SidebarPrimary.displayName = 'SidebarPrimary';
|
||||
|
||||
const SidebarPrimaryHeader = React.memo(() => {
|
||||
return (
|
||||
<div className="flex items-center justify-between">
|
||||
<BusterLogoWithText />
|
||||
<div className="flex items-center gap-2">
|
||||
<Tooltip title="Settings">
|
||||
<Link href={createBusterRoute({ route: BusterRoutes.SETTINGS_GENERAL })}>
|
||||
<Button prefix={<Gear />} variant="ghost" />
|
||||
</Link>
|
||||
</Tooltip>
|
||||
<Tooltip title="Start a chat">
|
||||
<Link href={createBusterRoute({ route: BusterRoutes.SETTINGS_GENERAL })}>
|
||||
<Button
|
||||
size="tall"
|
||||
rounding={'large'}
|
||||
prefix={
|
||||
<div className="translate-x-[-1px] translate-y-[-1px]">
|
||||
<PencilSquareIcon />
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</Link>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const favoritesDropdown = (favorites: BusterUserFavorite[]): ISidebarGroup => {
|
||||
return {
|
||||
label: 'Favorites',
|
||||
items: favorites.map((favorite) => {
|
||||
const Icon = assetTypeToIcon(favorite.asset_type);
|
||||
const route = assetTypeToRoute(favorite.asset_type, favorite.id);
|
||||
return {
|
||||
label: favorite.name,
|
||||
icon: <Icon />,
|
||||
route,
|
||||
id: route
|
||||
};
|
||||
})
|
||||
};
|
||||
};
|
|
@ -2,7 +2,7 @@ import React, { PropsWithChildren } from 'react';
|
|||
import { cn } from '@/lib/utils';
|
||||
import { cva, type VariantProps } from 'class-variance-authority';
|
||||
|
||||
const contentVariants = cva('max-h-[100%]', {
|
||||
const contentVariants = cva('max-h-[100%] bg-background', {
|
||||
variants: {
|
||||
scrollable: {
|
||||
true: 'overflow-y-auto'
|
||||
|
|
|
@ -50,7 +50,11 @@ export const SidebarCollapsible: React.FC<ISidebarGroup & { activeItem?: string
|
|||
<CollapsibleContent className="data-[state=open]:animate-collapsible-down data-[state=closed]:animate-collapsible-up pl-0">
|
||||
<div className="space-y-0.5">
|
||||
{items.map((item) => (
|
||||
<SidebarItem key={item.id} {...item} active={activeItem === item.id || item.active} />
|
||||
<SidebarItem
|
||||
key={item.id + item.route}
|
||||
{...item}
|
||||
active={activeItem === item.id || item.active}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
|
|
|
@ -5,7 +5,7 @@ import { type ISidebarItem } from './interfaces';
|
|||
import { cva, VariantProps } from 'class-variance-authority';
|
||||
|
||||
const itemVariants = cva(
|
||||
'flex items-center gap-2 rounded px-1.5 py-1.5 text-base transition-colors',
|
||||
'flex items-center gap-2 rounded px-1.5 py-1.5 text-base transition-colors cursor-pointer',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
|
@ -58,10 +58,10 @@ const itemVariants = cva(
|
|||
|
||||
export const SidebarItem: React.FC<ISidebarItem & VariantProps<typeof itemVariants>> = React.memo(
|
||||
({ label, icon, route, id, disabled = false, active = false, variant = 'default' }) => {
|
||||
const ItemNode = disabled ? 'div' : Link;
|
||||
const ItemNode = disabled || !route ? 'div' : Link;
|
||||
|
||||
return (
|
||||
<ItemNode href={route} className={cn(itemVariants({ active, disabled, variant }))}>
|
||||
<ItemNode href={route || ''} className={cn(itemVariants({ active, disabled, variant }))}>
|
||||
<span className={cn('text-icon-size! text-icon-color', { 'text-text-disabled': disabled })}>
|
||||
{icon}
|
||||
</span>
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
export * from './Sidebar';
|
||||
|
||||
export { type SidebarProps, type ISidebarItem, type ISidebarGroup } from './interfaces';
|
|
@ -1,9 +1,8 @@
|
|||
import { BusterRoutes } from '@/routes';
|
||||
import React from 'react';
|
||||
export interface ISidebarItem {
|
||||
label: string;
|
||||
icon: React.ReactNode;
|
||||
route: BusterRoutes;
|
||||
route: string | null;
|
||||
id: string;
|
||||
disabled?: boolean;
|
||||
active?: boolean;
|
||||
|
|
|
@ -18,7 +18,7 @@ export interface TooltipProps
|
|||
}
|
||||
|
||||
export const Tooltip = React.memo<TooltipProps>(
|
||||
({ children, title, shortcut, delayDuration, skipDelayDuration, align, side, open }) => {
|
||||
({ children, title, shortcut, delayDuration = 0, skipDelayDuration, align, side, open }) => {
|
||||
if (!title && !shortcut?.length) return children;
|
||||
|
||||
return (
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
export * from './AppTooltip';
|
||||
export * from './AppPopover';
|
||||
export * from './AppPopoverMenu';
|
||||
export * from './Tooltip';
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
export enum BusterAppRoutes {
|
||||
APP_ROOT = '/app',
|
||||
APP_HOME = '/app/home',
|
||||
APP_COLLECTIONS = '/app/collections',
|
||||
APP_COLLECTIONS_ID = '/app/collections/:collectionId',
|
||||
APP_COLLECTIONS_ID_METRICS_ID = '/app/collections/:collectionId/metrics/:metricId',
|
||||
|
@ -72,6 +73,7 @@ export enum BusterAppRoutes {
|
|||
|
||||
export type BusterAppRoutesWithArgs = {
|
||||
[BusterAppRoutes.APP_ROOT]: { route: BusterAppRoutes.APP_ROOT };
|
||||
[BusterAppRoutes.APP_HOME]: { route: BusterAppRoutes.APP_HOME };
|
||||
[BusterAppRoutes.APP_COLLECTIONS]: { route: BusterAppRoutes.APP_COLLECTIONS };
|
||||
[BusterAppRoutes.APP_COLLECTIONS_ID]: {
|
||||
route: BusterAppRoutes.APP_COLLECTIONS_ID;
|
||||
|
|
|
@ -59,6 +59,7 @@
|
|||
/* base color */
|
||||
--color-background: #ffffff;
|
||||
--color-foreground: #000000;
|
||||
--color-background-secondary: '#f3f3f3';
|
||||
--color-foreground-hover: #393939;
|
||||
--color-primary: #7c3aed;
|
||||
--color-primary-light: #a26cff;
|
||||
|
@ -109,6 +110,6 @@
|
|||
@apply border-border outline-ring/50;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground text-base;
|
||||
@apply bg-background-secondary text-foreground text-base;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue