From a46f74589b9467393ddd80d050f8b94a8b3c7f31 Mon Sep 17 00:00:00 2001 From: Nate Kelley Date: Sat, 1 Mar 2025 21:00:19 -0700 Subject: [PATCH] sidebar footer --- web/.storybook/main.ts | 1 - web/package.json | 1 - web/src/app/app/layout.tsx | 1 - .../features/sidebars/SidebarSettings.tsx | 10 +- .../SidebarUserFooter.stories.tsx | 42 +++++++ .../SidebarUserFooter/SidebarUserFooter.tsx | 118 ++++++++++++++++++ web/src/components/features/sidebars/index.ts | 3 + web/src/components/ui/avatar/Avatar.tsx | 2 +- web/src/components/ui/avatar/AvatarBase.tsx | 2 +- .../ui/avatar/AvatarUserButton.stories.tsx | 53 ++++++++ .../components/ui/avatar/AvatarUserButton.tsx | 32 +++++ web/src/components/ui/dropdown/Dropdown.tsx | 18 ++- .../components/ui/dropdown/DropdownBase.tsx | 3 +- web/src/components/ui/sidebar/Sidebar.tsx | 2 +- .../typography/AppCodeBlock/AppCodeBlock.tsx | 15 +-- .../AppCodeBlock/AppCodeBlockWrapper.tsx | 28 ++--- .../AppMarkdown/AppMarkdownCommon.tsx | 4 +- .../BusterReactQuery/getQueryClient.ts | 3 +- web/src/hooks/useBusterSupabaseAuthMethods.ts | 26 ++++ web/src/routes/externalRoutes.ts | 1 + 20 files changed, 316 insertions(+), 49 deletions(-) create mode 100644 web/src/components/features/sidebars/SidebarUserFooter/SidebarUserFooter.stories.tsx create mode 100644 web/src/components/features/sidebars/SidebarUserFooter/SidebarUserFooter.tsx create mode 100644 web/src/components/features/sidebars/index.ts create mode 100644 web/src/components/ui/avatar/AvatarUserButton.stories.tsx create mode 100644 web/src/components/ui/avatar/AvatarUserButton.tsx diff --git a/web/.storybook/main.ts b/web/.storybook/main.ts index 9ca9392b3..a494a8fa6 100644 --- a/web/.storybook/main.ts +++ b/web/.storybook/main.ts @@ -3,7 +3,6 @@ import type { StorybookConfig } from '@storybook/nextjs'; const config: StorybookConfig = { stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'], addons: [ - '@storybook/addon-onboarding', '@storybook/addon-essentials', '@chromatic-com/storybook', '@storybook/addon-interactions' diff --git a/web/package.json b/web/package.json index 039656e0e..4311d7288 100644 --- a/web/package.json +++ b/web/package.json @@ -120,7 +120,6 @@ "@chromatic-com/storybook": "^3.2.5", "@storybook/addon-essentials": "^8.6.2", "@storybook/addon-interactions": "^8.6.2", - "@storybook/addon-onboarding": "^8.6.2", "@storybook/blocks": "^8.6.2", "@storybook/nextjs": "^8.6.2", "@storybook/react": "^8.6.2", diff --git a/web/src/app/app/layout.tsx b/web/src/app/app/layout.tsx index 22c0794c9..81ca5e300 100644 --- a/web/src/app/app/layout.tsx +++ b/web/src/app/app/layout.tsx @@ -22,7 +22,6 @@ export default async function Layout({ jwtToken: accessToken }); - // const { signOut } = useBusterSupabaseAuthMethods(); const pathname = headersList.get('x-next-pathname') as string; const cookiePathname = cookies().get('x-next-pathname')?.value; const newUserRoute = createBusterRoute({ route: BusterAppRoutes.NEW_USER }); diff --git a/web/src/components/features/sidebars/SidebarSettings.tsx b/web/src/components/features/sidebars/SidebarSettings.tsx index 6343d93b9..82db0ae66 100644 --- a/web/src/components/features/sidebars/SidebarSettings.tsx +++ b/web/src/components/features/sidebars/SidebarSettings.tsx @@ -7,6 +7,7 @@ import { BusterRoutes, createBusterRoute } from '@/routes'; import React, { useMemo } from 'react'; import { useUserConfigContextSelector } from '@/context/Users'; import { useAppLayoutContextSelector } from '@/context/BusterAppLayout'; +import { SidebarUserFooter } from './SidebarUserFooter/SidebarUserFooter'; const accountItems: ISidebarGroup = { label: 'Account', @@ -70,7 +71,14 @@ export const SidebarSettings: React.FC<{}> = React.memo(({}) => { return items; }, [isAdmin]); - return } activeItem={currentRoute} />; + return ( + } + activeItem={currentRoute} + footer={} + /> + ); }); SidebarSettings.displayName = 'SidebarSettings'; diff --git a/web/src/components/features/sidebars/SidebarUserFooter/SidebarUserFooter.stories.tsx b/web/src/components/features/sidebars/SidebarUserFooter/SidebarUserFooter.stories.tsx new file mode 100644 index 000000000..c9faed0fe --- /dev/null +++ b/web/src/components/features/sidebars/SidebarUserFooter/SidebarUserFooter.stories.tsx @@ -0,0 +1,42 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { SidebarUserFooterComponent } from './SidebarUserFooter'; + +const meta: Meta = { + title: 'Features/Sidebars/SidebarUserFooter', + component: SidebarUserFooterComponent, + parameters: { + layout: 'centered' + }, + decorators: [ + (Story) => ( +
+ +
+ ) + ], + tags: ['autodocs'] +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + name: 'John Doe', + email: 'john.doe@example.com' + } +}; + +export const WithoutAvatar: Story = { + args: { + name: 'Jane Smith', + email: 'jane.smith@example.com' + } +}; + +export const LongName: Story = { + args: { + name: 'Alexander Bartholomew Christopherson III', + email: 'alexander@example.com' + } +}; diff --git a/web/src/components/features/sidebars/SidebarUserFooter/SidebarUserFooter.tsx b/web/src/components/features/sidebars/SidebarUserFooter/SidebarUserFooter.tsx new file mode 100644 index 000000000..ea5ad3b5f --- /dev/null +++ b/web/src/components/features/sidebars/SidebarUserFooter/SidebarUserFooter.tsx @@ -0,0 +1,118 @@ +'use client'; + +import React, { useMemo } from 'react'; +import { BusterRoutes, createBusterRoute } from '@/routes'; +import { + Gear, + Database, + UserGroup, + Book2, + Message, + Flag, + ArrowRightFromLine +} from '@/components/ui/icons/NucleoIconOutlined'; +import { BUSTER_DOCS_URL } from '@/routes/externalRoutes'; +import { type DropdownProps, Dropdown } from '@/components/ui/dropdown/Dropdown'; +import { AvatarUserButton } from '@/components/ui/avatar/AvatarUserButton'; +import { useUserConfigContextSelector } from '@/context/Users'; +import { signOut } from '@/hooks/useBusterSupabaseAuthMethods'; + +export const SidebarUserFooter: React.FC<{}> = () => { + const user = useUserConfigContextSelector((x) => x.user); + if (!user) return null; + + const signOutServer = signOut; + return ( + + ); +}; + +export const SidebarUserFooterComponent: React.FC<{ + name: string; + avatarUrl?: string; + email: string; + signOut: () => Promise<{ error: string }>; +}> = React.memo(({ name, avatarUrl, email, signOut }) => { + return ( + +
+ +
+
+ ); +}); + +SidebarUserFooterComponent.displayName = 'SidebarUserFooterComponent'; + +const topItems: DropdownProps['items'] = [ + { + label: 'Settings', + value: 'setting', + icon: , + link: createBusterRoute({ + route: BusterRoutes.APP_SETTINGS_PROFILE + }) + }, + { + label: 'Datasources', + value: 'datasources', + link: createBusterRoute({ + route: BusterRoutes.APP_SETTINGS_DATASOURCES + }), + icon: + }, + { + label: 'Invite & manage members', + value: 'invite-manage-members', + icon: , + link: createBusterRoute({ + route: BusterRoutes.APP_SETTINGS_USERS + }) + } +]; + +const SidebarUserDropdown: React.FC<{ + children: React.ReactNode; + signOut: () => void; +}> = React.memo(({ children, signOut }) => { + const allItems: DropdownProps['items'] = useMemo(() => { + return [ + ...topItems, + { type: 'divider' }, + { + label: 'Docs', + value: 'docs', + link: BUSTER_DOCS_URL, + icon: + }, + { + label: 'Contact support', + value: 'contact-support', + icon: + }, + { + label: 'Leave feedback', + value: 'leave-feedback', + icon: + }, + { type: 'divider' }, + { + label: 'Logout', + value: 'logout', + onClick: signOut, + icon: + } + ]; + }, []); + + return ( + + {children} + + ); +}); diff --git a/web/src/components/features/sidebars/index.ts b/web/src/components/features/sidebars/index.ts new file mode 100644 index 000000000..d8eee916c --- /dev/null +++ b/web/src/components/features/sidebars/index.ts @@ -0,0 +1,3 @@ +export * from './SidebarUserFooter/SidebarUserComponent'; +export * from './SidebarSettings'; +export * from './SidebarPrimary'; diff --git a/web/src/components/ui/avatar/Avatar.tsx b/web/src/components/ui/avatar/Avatar.tsx index 2cbfc88ad..76c516f3b 100644 --- a/web/src/components/ui/avatar/Avatar.tsx +++ b/web/src/components/ui/avatar/Avatar.tsx @@ -27,7 +27,7 @@ export const Avatar: React.FC = React.memo( height: size }}> {image && } - + {nameLetters || } diff --git a/web/src/components/ui/avatar/AvatarBase.tsx b/web/src/components/ui/avatar/AvatarBase.tsx index cbc98dc0c..5f3248864 100644 --- a/web/src/components/ui/avatar/AvatarBase.tsx +++ b/web/src/components/ui/avatar/AvatarBase.tsx @@ -35,7 +35,7 @@ const AvatarFallback = React.forwardRef< = { + title: 'UI/Avatar/AvatarUserButton', + component: AvatarUserButton, + parameters: { + layout: 'fullscreen' + }, + decorators: [ + (Story) => ( +
+ +
+ ) + ], + tags: ['autodocs'] +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + ...defaultUser + } +}; + +export const WithCustomUser: Story = { + args: { + ...defaultUser + } +}; + +export const WithOnlyUsername: Story = { + args: { + ...defaultUser, + avatarUrl: undefined + } +}; + +export const WithClickHandler: Story = { + args: { + username: 'Click Me', + onClick: () => alert('User component clicked!') + } +}; diff --git a/web/src/components/ui/avatar/AvatarUserButton.tsx b/web/src/components/ui/avatar/AvatarUserButton.tsx new file mode 100644 index 000000000..04548878e --- /dev/null +++ b/web/src/components/ui/avatar/AvatarUserButton.tsx @@ -0,0 +1,32 @@ +import React from 'react'; +import { Avatar } from './Avatar'; +import { Text } from '../typography/Text'; +import { ChevronExpandY } from '../icons'; + +export const AvatarUserButton = React.forwardRef< + HTMLDivElement, + { + username?: string; + avatarUrl?: string; + email?: string; + } +>(({ username, avatarUrl, email }, ref) => { + return ( +
+ +
+ {username} + + {email} + +
+
+ +
+
+ ); +}); + +AvatarUserButton.displayName = 'AvatarUserButton'; diff --git a/web/src/components/ui/dropdown/Dropdown.tsx b/web/src/components/ui/dropdown/Dropdown.tsx index b76b991e0..6e43acf85 100644 --- a/web/src/components/ui/dropdown/Dropdown.tsx +++ b/web/src/components/ui/dropdown/Dropdown.tsx @@ -22,6 +22,7 @@ import { useMemoizedFn } from 'ahooks'; import { cn } from '@/lib/classMerge'; import { Input } from '../inputs/Input'; import { useDebounceSearch } from '@/hooks'; +import Link from 'next/link'; export interface DropdownItem { label: React.ReactNode | string; @@ -82,7 +83,8 @@ export const Dropdown: React.FC = React.memo( emptyStateText = 'No items found', className, footerContent, - ...props + dir, + modal }) => { const { filteredItems, searchText, handleSearchChange } = useDebounceSearch({ items, @@ -126,7 +128,12 @@ export const Dropdown: React.FC = React.memo( const dropdownItems = selectType === 'multiple' ? unselectedItems : filteredItems; return ( - + {children} 0; + const isSelectable = !!selectType && selectType !== 'none'; + const NodeWrapper = isSelectable && link ? Link : React.Fragment; + const nodeProps = isSelectable && link ? { href: link! } : ({} as any); const content = ( - <> + {showIndex && {index}} {icon && !loading && {icon}} {loading && } @@ -264,7 +274,7 @@ const DropdownItem: React.FC< linkIcon={linkIcon} /> )} - + ); if (isSubItem) { diff --git a/web/src/components/ui/dropdown/DropdownBase.tsx b/web/src/components/ui/dropdown/DropdownBase.tsx index 91f9040c2..14c4482e1 100644 --- a/web/src/components/ui/dropdown/DropdownBase.tsx +++ b/web/src/components/ui/dropdown/DropdownBase.tsx @@ -72,6 +72,7 @@ const DropdownMenuContent = React.forwardRef< } >(({ className, children, sideOffset = 4, footerContent, ...props }, ref) => { const NodeWrapper = footerContent ? 'div' : React.Fragment; + const nodeWrapperProps = footerContent ? { className: 'p-2' } : {}; return ( @@ -80,7 +81,7 @@ const DropdownMenuContent = React.forwardRef< sideOffset={sideOffset} className={cn(baseContentClass, 'shadow', footerContent && 'p-0', className)} {...props}> - {children} + {children} {footerContent &&
{footerContent}
}
diff --git a/web/src/components/ui/sidebar/Sidebar.tsx b/web/src/components/ui/sidebar/Sidebar.tsx index 3960f4077..ad7e9f415 100644 --- a/web/src/components/ui/sidebar/Sidebar.tsx +++ b/web/src/components/ui/sidebar/Sidebar.tsx @@ -15,7 +15,7 @@ export const Sidebar: React.FC = React.memo( ))} - {footer &&
{footer}
} + {footer &&
{footer}
} ); } diff --git a/web/src/components/ui/typography/AppCodeBlock/AppCodeBlock.tsx b/web/src/components/ui/typography/AppCodeBlock/AppCodeBlock.tsx index ce5fa41f6..71d63acf5 100644 --- a/web/src/components/ui/typography/AppCodeBlock/AppCodeBlock.tsx +++ b/web/src/components/ui/typography/AppCodeBlock/AppCodeBlock.tsx @@ -1,10 +1,9 @@ import React, { useState } from 'react'; import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; -import { createStyles } from 'antd-style'; -import darkTheme from 'react-syntax-highlighter/dist/cjs/styles/prism/vsc-dark-plus'; import { TextPulseLoader } from '../..'; import lightTheme from './light'; import { AppCodeBlockWrapper } from './AppCodeBlockWrapper'; +import { cn } from '@/lib/classMerge'; export const AppCodeBlock: React.FC<{ language?: string; @@ -57,18 +56,8 @@ export const AppCodeBlock: React.FC<{ }); AppCodeBlock.displayName = 'AppCodeBlock'; -const useStyles = createStyles(({ token }) => ({ - codeInlineWrapper: { - backgroundColor: token.controlItemBgActive, - borderRadius: token.borderRadiusSM, - border: `0.5px solid ${token.colorBorder}`, - fontSize: token.fontSize - 1 - } -})); - const CodeInlineWrapper: React.FC<{ children: React.ReactNode; }> = ({ children }) => { - const { cx, styles } = useStyles(); - return {children}; + return {children}; }; diff --git a/web/src/components/ui/typography/AppCodeBlock/AppCodeBlockWrapper.tsx b/web/src/components/ui/typography/AppCodeBlock/AppCodeBlockWrapper.tsx index 546da3ca4..a565d91d4 100644 --- a/web/src/components/ui/typography/AppCodeBlock/AppCodeBlockWrapper.tsx +++ b/web/src/components/ui/typography/AppCodeBlock/AppCodeBlockWrapper.tsx @@ -5,6 +5,7 @@ import { Text } from '@/components/ui/typography'; import { useMemoizedFn } from 'ahooks'; import { Button } from '@/components/ui/buttons'; import { Copy } from '@/components/ui/icons'; +import { cn } from '@/lib/classMerge'; export const AppCodeBlockWrapper: React.FC<{ children: React.ReactNode; @@ -14,7 +15,6 @@ export const AppCodeBlockWrapper: React.FC<{ buttons?: React.ReactNode; title?: string | React.ReactNode; }> = React.memo(({ children, code, showCopyButton = true, language, buttons, title }) => { - const { cx, styles } = useStyles(); const { openSuccessMessage } = useBusterNotifications(); const copyCode = useMemoizedFn(() => { @@ -24,8 +24,12 @@ export const AppCodeBlockWrapper: React.FC<{ }); return ( -
-
+
+
{title || language}
{showCopyButton && ( @@ -49,21 +53,3 @@ export const AppCodeBlockWrapper: React.FC<{ ); }); AppCodeBlockWrapper.displayName = 'CodeBlockWrapper'; - -const useStyles = createStyles(({ token }) => ({ - container: { - backgroundColor: token.colorBgBase, - margin: `0px 0px`, - border: `0.5px solid ${token.colorBorder}`, - borderRadius: `${token.borderRadiusLG}px`, - overflow: 'hidden' - }, - containerHeader: { - borderBottom: `0.5px solid ${token.colorBorder}`, - padding: '4px', - backgroundColor: token.controlItemBgActive, - height: '32px', - minHeight: '32px', - maxHeight: '32px' - } -})); diff --git a/web/src/components/ui/typography/AppMarkdown/AppMarkdownCommon.tsx b/web/src/components/ui/typography/AppMarkdown/AppMarkdownCommon.tsx index b7b0841b4..89fe8b414 100644 --- a/web/src/components/ui/typography/AppMarkdown/AppMarkdownCommon.tsx +++ b/web/src/components/ui/typography/AppMarkdown/AppMarkdownCommon.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { JSX } from 'react'; import { ExtraProps } from 'react-markdown'; import { AppCodeBlock } from '../AppCodeBlock/AppCodeBlock'; import { TextPulseLoader } from '@/components/ui/loaders'; @@ -95,7 +95,7 @@ export const CustomHeading: React.FC< numberOfLineMarkdown: number; } & ExtraPropsExtra > = ({ level, children, markdown, ...rest }) => { - const HeadingTag = `h${level}` as keyof JSX.IntrinsicElements; + const HeadingTag = `h${level}` as any; return ( {children} diff --git a/web/src/context/BusterReactQuery/getQueryClient.ts b/web/src/context/BusterReactQuery/getQueryClient.ts index ec15e7bbe..91cb5392d 100644 --- a/web/src/context/BusterReactQuery/getQueryClient.ts +++ b/web/src/context/BusterReactQuery/getQueryClient.ts @@ -5,7 +5,8 @@ function makeQueryClient() { return new QueryClient({ defaultOptions: { queries: { - staleTime: PREFETCH_STALE_TIME + staleTime: PREFETCH_STALE_TIME, + queryFn: () => Promise.resolve() }, dehydrate: { // include pending queries in dehydration diff --git a/web/src/hooks/useBusterSupabaseAuthMethods.ts b/web/src/hooks/useBusterSupabaseAuthMethods.ts index 10b9e3f23..602aee5f7 100644 --- a/web/src/hooks/useBusterSupabaseAuthMethods.ts +++ b/web/src/hooks/useBusterSupabaseAuthMethods.ts @@ -1,3 +1,5 @@ +'use server'; + import { createClient } from '@/context/Supabase/server'; import { redirect } from 'next/navigation'; import { BusterRoutes, createBusterRoute } from '@/routes/busterRoutes/busterRoutes'; @@ -205,3 +207,27 @@ export const useBusterSupabaseAuthMethods = () => { resetPasswordEmailSend }; }; + +export const signOut = async () => { + const supabase = await createClient(); + const queryClient = new QueryClient(); + + const { error } = await supabase.auth.signOut(); + + if (error) { + return { error: error.message }; + } + + setTimeout(() => { + Object.keys(Cookies.get()).forEach((cookieName) => { + Cookies.remove(cookieName); + }); + queryClient.clear(); + }, 650); + + return redirect( + createBusterRoute({ + route: BusterRoutes.AUTH_LOGIN + }) + ); +}; diff --git a/web/src/routes/externalRoutes.ts b/web/src/routes/externalRoutes.ts index e4b2a0fc0..2514f5cbc 100644 --- a/web/src/routes/externalRoutes.ts +++ b/web/src/routes/externalRoutes.ts @@ -1 +1,2 @@ export const BUSTER_HOME_PAGE = 'https://buster.so'; +export const BUSTER_DOCS_URL = 'https://docs.buster.so';