mirror of https://github.com/buster-so/buster.git
sidebar footer
This commit is contained in:
parent
6aefea9cb1
commit
a46f74589b
|
@ -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'
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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 });
|
||||
|
|
|
@ -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 <Sidebar content={content} header={<SidebarSettingsHeader />} activeItem={currentRoute} />;
|
||||
return (
|
||||
<Sidebar
|
||||
content={content}
|
||||
header={<SidebarSettingsHeader />}
|
||||
activeItem={currentRoute}
|
||||
footer={<SidebarUserFooter />}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
SidebarSettings.displayName = 'SidebarSettings';
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { SidebarUserFooterComponent } from './SidebarUserFooter';
|
||||
|
||||
const meta: Meta<typeof SidebarUserFooterComponent> = {
|
||||
title: 'Features/Sidebars/SidebarUserFooter',
|
||||
component: SidebarUserFooterComponent,
|
||||
parameters: {
|
||||
layout: 'centered'
|
||||
},
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="bg-background-secondary h-screen w-[300px] p-4">
|
||||
<Story />
|
||||
</div>
|
||||
)
|
||||
],
|
||||
tags: ['autodocs']
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof SidebarUserFooterComponent>;
|
||||
|
||||
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'
|
||||
}
|
||||
};
|
|
@ -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 (
|
||||
<SidebarUserFooterComponent
|
||||
signOut={signOutServer}
|
||||
name={user?.name}
|
||||
avatarUrl={''}
|
||||
email={user?.email}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const SidebarUserFooterComponent: React.FC<{
|
||||
name: string;
|
||||
avatarUrl?: string;
|
||||
email: string;
|
||||
signOut: () => Promise<{ error: string }>;
|
||||
}> = React.memo(({ name, avatarUrl, email, signOut }) => {
|
||||
return (
|
||||
<SidebarUserDropdown signOut={signOut}>
|
||||
<div className="flex w-full">
|
||||
<AvatarUserButton username={name} avatarUrl={avatarUrl} email={email} />
|
||||
</div>
|
||||
</SidebarUserDropdown>
|
||||
);
|
||||
});
|
||||
|
||||
SidebarUserFooterComponent.displayName = 'SidebarUserFooterComponent';
|
||||
|
||||
const topItems: DropdownProps['items'] = [
|
||||
{
|
||||
label: 'Settings',
|
||||
value: 'setting',
|
||||
icon: <Gear />,
|
||||
link: createBusterRoute({
|
||||
route: BusterRoutes.APP_SETTINGS_PROFILE
|
||||
})
|
||||
},
|
||||
{
|
||||
label: 'Datasources',
|
||||
value: 'datasources',
|
||||
link: createBusterRoute({
|
||||
route: BusterRoutes.APP_SETTINGS_DATASOURCES
|
||||
}),
|
||||
icon: <Database />
|
||||
},
|
||||
{
|
||||
label: 'Invite & manage members',
|
||||
value: 'invite-manage-members',
|
||||
icon: <UserGroup />,
|
||||
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: <Book2 />
|
||||
},
|
||||
{
|
||||
label: 'Contact support',
|
||||
value: 'contact-support',
|
||||
icon: <Message />
|
||||
},
|
||||
{
|
||||
label: 'Leave feedback',
|
||||
value: 'leave-feedback',
|
||||
icon: <Flag />
|
||||
},
|
||||
{ type: 'divider' },
|
||||
{
|
||||
label: 'Logout',
|
||||
value: 'logout',
|
||||
onClick: signOut,
|
||||
icon: <ArrowRightFromLine />
|
||||
}
|
||||
];
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Dropdown align="end" side="right" items={allItems}>
|
||||
{children}
|
||||
</Dropdown>
|
||||
);
|
||||
});
|
|
@ -0,0 +1,3 @@
|
|||
export * from './SidebarUserFooter/SidebarUserComponent';
|
||||
export * from './SidebarSettings';
|
||||
export * from './SidebarPrimary';
|
|
@ -27,7 +27,7 @@ export const Avatar: React.FC<AvatarProps> = React.memo(
|
|||
height: size
|
||||
}}>
|
||||
{image && <AvatarImage src={image} />}
|
||||
<AvatarFallback className={cn(!hasName && 'border bg-white')}>
|
||||
<AvatarFallback className={cn(!hasName && !image && 'border bg-white')}>
|
||||
{nameLetters || <BusterAvatarFallback />}
|
||||
</AvatarFallback>
|
||||
</AvatarBase>
|
||||
|
|
|
@ -35,7 +35,7 @@ const AvatarFallback = React.forwardRef<
|
|||
<AvatarPrimitive.Fallback
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'bg-gray-light/70 text-background flex h-full w-full items-center justify-center rounded-full text-base',
|
||||
'bg-gray-light/60 text-background flex h-full w-full items-center justify-center rounded-full text-base',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { AvatarUserButton } from './AvatarUserButton';
|
||||
|
||||
const defaultUser = {
|
||||
username: 'John Doe',
|
||||
avatarUrl: 'https://i.pravatar.cc/200',
|
||||
email: 'john.doe@example.com'
|
||||
};
|
||||
|
||||
const meta: Meta<typeof AvatarUserButton> = {
|
||||
title: 'UI/Avatar/AvatarUserButton',
|
||||
component: AvatarUserButton,
|
||||
parameters: {
|
||||
layout: 'fullscreen'
|
||||
},
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="bg-background-secondary h-screen w-[300px] p-4">
|
||||
<Story />
|
||||
</div>
|
||||
)
|
||||
],
|
||||
tags: ['autodocs']
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof AvatarUserButton>;
|
||||
|
||||
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!')
|
||||
}
|
||||
};
|
|
@ -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 (
|
||||
<div
|
||||
ref={ref}
|
||||
className="hover:bg-item-hover active:bg-item-active flex w-full cursor-pointer items-center gap-x-2 rounded-md p-2">
|
||||
<Avatar size={32} image={avatarUrl} name={username} />
|
||||
<div className="flex flex-col gap-y-0.5">
|
||||
<Text className="flex-grow">{username}</Text>
|
||||
<Text size={'sm'} variant={'secondary'}>
|
||||
{email}
|
||||
</Text>
|
||||
</div>
|
||||
<div className="ml-auto">
|
||||
<ChevronExpandY />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
AvatarUserButton.displayName = 'AvatarUserButton';
|
|
@ -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<DropdownProps> = 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<DropdownProps> = React.memo(
|
|||
const dropdownItems = selectType === 'multiple' ? unselectedItems : filteredItems;
|
||||
|
||||
return (
|
||||
<DropdownMenu open={open} defaultOpen={open} onOpenChange={onOpenChange} {...props}>
|
||||
<DropdownMenu
|
||||
open={open}
|
||||
defaultOpen={open}
|
||||
onOpenChange={onOpenChange}
|
||||
dir={dir}
|
||||
modal={modal}>
|
||||
<DropdownMenuTrigger asChild>{children}</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
className={cn('max-w-72 min-w-44', className)}
|
||||
|
@ -246,9 +253,12 @@ const DropdownItem: React.FC<
|
|||
});
|
||||
|
||||
const isSubItem = items && items.length > 0;
|
||||
const isSelectable = !!selectType && selectType !== 'none';
|
||||
const NodeWrapper = isSelectable && link ? Link : React.Fragment;
|
||||
const nodeProps = isSelectable && link ? { href: link! } : ({} as any);
|
||||
|
||||
const content = (
|
||||
<>
|
||||
<NodeWrapper {...nodeProps}>
|
||||
{showIndex && <span className="text-gray-light">{index}</span>}
|
||||
{icon && !loading && <span className="text-icon-color">{icon}</span>}
|
||||
{loading && <CircleSpinnerLoader size={9} />}
|
||||
|
@ -264,7 +274,7 @@ const DropdownItem: React.FC<
|
|||
linkIcon={linkIcon}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
</NodeWrapper>
|
||||
);
|
||||
|
||||
if (isSubItem) {
|
||||
|
|
|
@ -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 (
|
||||
<DropdownMenuPrimitive.Portal>
|
||||
|
@ -80,7 +81,7 @@ const DropdownMenuContent = React.forwardRef<
|
|||
sideOffset={sideOffset}
|
||||
className={cn(baseContentClass, 'shadow', footerContent && 'p-0', className)}
|
||||
{...props}>
|
||||
<NodeWrapper className={cn(footerContent && 'p-2')}>{children}</NodeWrapper>
|
||||
<NodeWrapper {...nodeWrapperProps}>{children}</NodeWrapper>
|
||||
{footerContent && <div className="border-t p-2">{footerContent}</div>}
|
||||
</DropdownMenuPrimitive.Content>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
|
|
|
@ -15,7 +15,7 @@ export const Sidebar: React.FC<SidebarProps> = React.memo(
|
|||
))}
|
||||
</div>
|
||||
</div>
|
||||
{footer && <div className="mt-auto bg-red-200 pt-5">{footer}</div>}
|
||||
{footer && <div className="mt-auto mb-2 pt-5">{footer}</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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 <code className={cx(styles.codeInlineWrapper, 'px-1')}>{children}</code>;
|
||||
return <code className={cn('bg-item-active rounded-sm border text-sm', 'px-1')}>{children}</code>;
|
||||
};
|
||||
|
|
|
@ -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 (
|
||||
<div className={cx(styles.container, 'max-h-fit')}>
|
||||
<div className={cx(styles.containerHeader, 'flex items-center justify-between')}>
|
||||
<div className={cn('overflow-hidden rounded border', 'max-h-fit')}>
|
||||
<div
|
||||
className={cn(
|
||||
'bg-item-active border-border max-h-[32px] min-h-[32px] border-b p-1',
|
||||
'flex items-center justify-between'
|
||||
)}>
|
||||
<Text className="pl-2">{title || language}</Text>
|
||||
<div className="flex items-center space-x-1">
|
||||
{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'
|
||||
}
|
||||
}));
|
||||
|
|
|
@ -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 (
|
||||
<HeadingTag>
|
||||
{children}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
})
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
export const BUSTER_HOME_PAGE = 'https://buster.so';
|
||||
export const BUSTER_DOCS_URL = 'https://docs.buster.so';
|
||||
|
|
Loading…
Reference in New Issue