From a6ce9c8c4d0d263eaa439b90ad58f6c9c81f7dd3 Mon Sep 17 00:00:00 2001 From: Nate Kelley Date: Fri, 28 Feb 2025 15:02:54 -0700 Subject: [PATCH] sidebar primary --- .../components/features/config/assetIcons.tsx | 37 ++++ .../sidebars/SidebarPrimary.stories.tsx | 105 +++++++++++ .../features/sidebars/SidebarPrimary.tsx | 176 ++++++++++++++++++ .../components/ui/layouts/AppContentPage.tsx | 2 +- .../ui/sidebar/SidebarCollapsible.tsx | 6 +- web/src/components/ui/sidebar/SidebarItem.tsx | 6 +- web/src/components/ui/sidebar/index.ts | 3 + web/src/components/ui/sidebar/interfaces.ts | 3 +- web/src/components/ui/tooltip/Tooltip.tsx | 2 +- web/src/components/ui/tooltip/index.ts | 1 + .../routes/busterRoutes/busterAppRoutes.ts | 2 + web/src/styles/tailwind.css | 3 +- 12 files changed, 337 insertions(+), 9 deletions(-) create mode 100644 web/src/components/features/config/assetIcons.tsx create mode 100644 web/src/components/features/sidebars/SidebarPrimary.stories.tsx create mode 100644 web/src/components/features/sidebars/SidebarPrimary.tsx create mode 100644 web/src/components/ui/sidebar/index.ts diff --git a/web/src/components/features/config/assetIcons.tsx b/web/src/components/features/config/assetIcons.tsx new file mode 100644 index 000000000..4b0bc28de --- /dev/null +++ b/web/src/components/features/config/assetIcons.tsx @@ -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 ''; + } +}; diff --git a/web/src/components/features/sidebars/SidebarPrimary.stories.tsx b/web/src/components/features/sidebars/SidebarPrimary.stories.tsx new file mode 100644 index 000000000..da2efe611 --- /dev/null +++ b/web/src/components/features/sidebars/SidebarPrimary.stories.tsx @@ -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 = { + title: 'Features/Sidebars/SidebarPrimary', + component: SidebarPrimary, + parameters: { + layout: 'fullscreen' + }, + decorators: [ + (Story) => ( +
+ +
+ ) + ] +}; + +export default meta; +type Story = StoryObj; + +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' }) + } +}; diff --git a/web/src/components/features/sidebars/SidebarPrimary.tsx b/web/src/components/features/sidebars/SidebarPrimary.tsx new file mode 100644 index 000000000..f5450deb8 --- /dev/null +++ b/web/src/components/features/sidebars/SidebarPrimary.tsx @@ -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: , + route: BusterRoutes.APP_HOME, + id: BusterRoutes.APP_HOME + }, + { + label: 'Chat history', + icon: , + route: BusterRoutes.APP_CHAT, + id: BusterRoutes.APP_CHAT + } + ] +}; + +const yourStuff: ISidebarGroup = { + label: 'Your stuffs', + items: [ + { + label: 'Metrics', + icon: , + route: BusterRoutes.APP_METRIC, + id: BusterRoutes.APP_METRIC + }, + { + label: 'Dashboards', + icon: , + route: BusterRoutes.APP_DASHBOARDS, + id: BusterRoutes.APP_DASHBOARDS + }, + { + label: 'Collections', + icon: , + route: BusterRoutes.APP_COLLECTIONS, + id: BusterRoutes.APP_COLLECTIONS + } + ] +}; + +const adminTools: ISidebarGroup = { + label: 'Admin tools', + items: [ + { + label: 'Logs', + icon: , + route: BusterRoutes.APP_LOGS, + id: BusterRoutes.APP_LOGS + }, + { + label: 'Terms & Definitions', + icon: , + route: BusterRoutes.APP_TERMS, + id: BusterRoutes.APP_TERMS + }, + { + label: 'Datasets', + icon: , + route: BusterRoutes.APP_DATASETS, + id: BusterRoutes.APP_DATASETS + } + ] +}; + +const tryGroup = (onClickInvitePeople: () => void, onClickLeaveFeedback: () => void) => ({ + label: 'Try', + items: [ + { + label: 'Invite people', + icon: , + route: null, + id: 'invite-people', + onClick: onClickInvitePeople + }, + { + label: 'Leave feedback', + icon: , + 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 ( + } activeItem={activePage} /> + ); + } +); + +SidebarPrimary.displayName = 'SidebarPrimary'; + +const SidebarPrimaryHeader = React.memo(() => { + return ( +
+ +
+ + +
+ } + /> + + +
+ + ); +}); + +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: , + route, + id: route + }; + }) + }; +}; diff --git a/web/src/components/ui/layouts/AppContentPage.tsx b/web/src/components/ui/layouts/AppContentPage.tsx index 63fb17ca1..61c88e6f5 100644 --- a/web/src/components/ui/layouts/AppContentPage.tsx +++ b/web/src/components/ui/layouts/AppContentPage.tsx @@ -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' diff --git a/web/src/components/ui/sidebar/SidebarCollapsible.tsx b/web/src/components/ui/sidebar/SidebarCollapsible.tsx index d9c69f70d..ebd5f43ea 100644 --- a/web/src/components/ui/sidebar/SidebarCollapsible.tsx +++ b/web/src/components/ui/sidebar/SidebarCollapsible.tsx @@ -50,7 +50,11 @@ export const SidebarCollapsible: React.FC
{items.map((item) => ( - + ))}
diff --git a/web/src/components/ui/sidebar/SidebarItem.tsx b/web/src/components/ui/sidebar/SidebarItem.tsx index 31a8c10b0..2c3b83031 100644 --- a/web/src/components/ui/sidebar/SidebarItem.tsx +++ b/web/src/components/ui/sidebar/SidebarItem.tsx @@ -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> = React.memo( ({ label, icon, route, id, disabled = false, active = false, variant = 'default' }) => { - const ItemNode = disabled ? 'div' : Link; + const ItemNode = disabled || !route ? 'div' : Link; return ( - + {icon} diff --git a/web/src/components/ui/sidebar/index.ts b/web/src/components/ui/sidebar/index.ts new file mode 100644 index 000000000..a1546e09f --- /dev/null +++ b/web/src/components/ui/sidebar/index.ts @@ -0,0 +1,3 @@ +export * from './Sidebar'; + +export { type SidebarProps, type ISidebarItem, type ISidebarGroup } from './interfaces'; diff --git a/web/src/components/ui/sidebar/interfaces.ts b/web/src/components/ui/sidebar/interfaces.ts index 53377ed51..604214a99 100644 --- a/web/src/components/ui/sidebar/interfaces.ts +++ b/web/src/components/ui/sidebar/interfaces.ts @@ -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; diff --git a/web/src/components/ui/tooltip/Tooltip.tsx b/web/src/components/ui/tooltip/Tooltip.tsx index 8c2cd175e..8e917c8f3 100644 --- a/web/src/components/ui/tooltip/Tooltip.tsx +++ b/web/src/components/ui/tooltip/Tooltip.tsx @@ -18,7 +18,7 @@ export interface TooltipProps } export const Tooltip = React.memo( - ({ children, title, shortcut, delayDuration, skipDelayDuration, align, side, open }) => { + ({ children, title, shortcut, delayDuration = 0, skipDelayDuration, align, side, open }) => { if (!title && !shortcut?.length) return children; return ( diff --git a/web/src/components/ui/tooltip/index.ts b/web/src/components/ui/tooltip/index.ts index e2e9159d6..879d56979 100644 --- a/web/src/components/ui/tooltip/index.ts +++ b/web/src/components/ui/tooltip/index.ts @@ -1,3 +1,4 @@ export * from './AppTooltip'; export * from './AppPopover'; export * from './AppPopoverMenu'; +export * from './Tooltip'; diff --git a/web/src/routes/busterRoutes/busterAppRoutes.ts b/web/src/routes/busterRoutes/busterAppRoutes.ts index 8cd4e5a6a..61e4ad272 100644 --- a/web/src/routes/busterRoutes/busterAppRoutes.ts +++ b/web/src/routes/busterRoutes/busterAppRoutes.ts @@ -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; diff --git a/web/src/styles/tailwind.css b/web/src/styles/tailwind.css index 9b904a200..8cbb5ddb1 100644 --- a/web/src/styles/tailwind.css +++ b/web/src/styles/tailwind.css @@ -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; } }