Skip to content

Commit 948c98d

Browse files
committed
feat(dashboard): add collapsible navigation sidebar
1 parent e5c8e6c commit 948c98d

11 files changed

Lines changed: 273 additions & 72 deletions

File tree

2.02 KB
Loading

ui/dashboard/src/@locales/en/common.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@
7272
"user-profile": "User profile",
7373
"logout": "Logout",
7474
"back-to-main": "Back to Main",
75+
"collapse": "Collapse sidebar",
76+
"expand": "Expand sidebar",
7577
"no-projects": "No projects found",
7678
"no-organizations": "No organizations found",
7779
"insights": "Insights"

ui/dashboard/src/@locales/ja/common.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@
7272
"user-profile": "ユーザープロフィール",
7373
"logout": "ログアウト",
7474
"back-to-main": "メインに戻る",
75+
"collapse": "サイドバーを折りたたむ",
76+
"expand": "サイドバーを展開",
7577
"no-projects": "プロジェクトが見つかりません",
7678
"no-organizations": "組織が見つかりません",
7779
"insights": "インサイト"

ui/dashboard/src/app/index.tsx

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ import {
5151
setCurrentEnvIdStorage
5252
} from 'storage/environment';
5353
import { getIsLoginFirstTimeStorage } from 'storage/login';
54+
import { getNavigationCollapsedStorage } from 'storage/navigation';
5455
import {
5556
getCurrentProjectEnvironmentStorage,
5657
setCurrentProjectEnvironmentStorage
@@ -61,6 +62,7 @@ import { ConsoleAccount, EnvironmentRole } from '@types';
6162
import { isNotEmpty } from 'utils/data-type';
6263
import { checkEnvironmentEmptyId } from 'utils/function';
6364
import { stringifyParams, useSearchParams } from 'utils/search-params';
65+
import { cn } from 'utils/style';
6466
import AccessDeniedPage from 'pages/access-denied';
6567
import APIKeysPage from 'pages/api-keys';
6668
import AuditLogsPage from 'pages/audit-logs';
@@ -139,6 +141,9 @@ function App() {
139141
export const Root = memo(() => {
140142
const authToken = getTokenStorage();
141143
const [pageKey, setPageKey] = useState<string>(uuid());
144+
const [isNavCollapsed, setIsNavCollapsed] = useState(
145+
getNavigationCollapsedStorage
146+
);
142147
const { isInitialLoading, isLogin, consoleAccount, myOrganizations } =
143148
useAuth();
144149

@@ -158,8 +163,17 @@ export const Root = memo(() => {
158163
return (
159164
<WalkthroughProvider>
160165
<div className="flex flex-row w-full h-full">
161-
<Navigation onClickNavLink={handleChangePageKey} />
162-
<div className="w-full ml-[248px] shadow-lg overflow-y-auto">
166+
<Navigation
167+
onClickNavLink={handleChangePageKey}
168+
isCollapsed={isNavCollapsed}
169+
onToggleCollapsed={setIsNavCollapsed}
170+
/>
171+
<div
172+
className={cn(
173+
'w-full shadow-lg overflow-y-auto transition-all duration-300 ease-in-out',
174+
isNavCollapsed ? 'ml-[60px]' : 'ml-[248px]'
175+
)}
176+
>
163177
<Routes>
164178
{consoleAccount.isSystemAdmin && (
165179
<Route

ui/dashboard/src/components/dropdown/index.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ const DropdownMenuTrigger = forwardRef<
120120
<DropdownMenuPrimitive.Trigger
121121
type="button"
122122
ref={ref}
123+
aria-label={ariaLabel}
123124
className={cn(
124125
triggerVariants({
125126
variant
@@ -363,6 +364,7 @@ interface DropdownProps {
363364
menuContentSide?: 'top' | 'bottom' | 'left' | 'right';
364365
alignContent?: 'center' | 'end' | 'start';
365366
trigger?: ReactNode;
367+
ariaLabel?: string;
366368
additionalElement?: (item: DropdownOption) => ReactNode;
367369
onChange?: (value: DropdownValue | DropdownValue[]) => void;
368370
onChangeAdditional?: (value: DropdownValue | DropdownValue[]) => void; // for additional options
@@ -396,6 +398,7 @@ const Dropdown = ({
396398
className = 'w-full',
397399
alignContent = 'start',
398400
trigger,
401+
ariaLabel,
399402

400403
additionalElement,
401404
sideOffsetContent,
@@ -468,6 +471,7 @@ const Dropdown = ({
468471
}
469472
label={labelCustom ? labelCustom : triggerLabel}
470473
trigger={trigger}
474+
ariaLabel={ariaLabel}
471475
disabled={disabled}
472476
loading={loading}
473477
variant={variant}

ui/dashboard/src/components/navigation/index.tsx

Lines changed: 131 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -8,21 +8,40 @@ import { useToggleOpen } from 'hooks';
88
import { useTranslation } from 'i18n';
99
import compact from 'lodash/compact';
1010
import flatMapDeep from 'lodash/flatMapDeep';
11+
import { setNavigationCollapsedStorage } from 'storage/navigation';
1112
import { cn } from 'utils/style';
1213
import * as IconSystem from '@icons';
1314
import Divider from 'components/divider';
1415
import Icon from 'components/icon';
16+
import { Tooltip } from 'components/tooltip';
1517
import SectionMenu from './menu-section';
1618
import MyProjects from './my-projects';
1719
import SwitchOrganization from './switch-organization';
1820
import UserMenu from './user-menu';
21+
import logoIcon from '/img/bucketeer-logo-icon.png';
1922

20-
const Navigation = ({ onClickNavLink }: { onClickNavLink: () => void }) => {
23+
type NavigationProps = {
24+
onClickNavLink: () => void;
25+
isCollapsed: boolean;
26+
onToggleCollapsed: (value: boolean) => void;
27+
};
28+
29+
const Navigation = ({
30+
onClickNavLink,
31+
isCollapsed,
32+
onToggleCollapsed
33+
}: NavigationProps) => {
2134
const { t } = useTranslation(['common']);
2235
const { pathname } = useLocation();
2336
const navigate = useNavigate();
2437
const { consoleAccount } = useAuth();
2538

39+
const toggleCollapsed = () => {
40+
const next = !isCollapsed;
41+
setNavigationCollapsedStorage(next);
42+
onToggleCollapsed(next);
43+
};
44+
2645
const currentEnvironment = getCurrentEnvironment(consoleAccount!);
2746
const envUrlCode = currentEnvironment.urlCode;
2847

@@ -152,11 +171,58 @@ const Navigation = ({ onClickNavLink }: { onClickNavLink: () => void }) => {
152171
useToggleOpen(false);
153172

154173
return (
155-
<div className="fixed h-screen w-[248px] bg-primary-500 z-50 py-8 px-6">
174+
<div
175+
className={cn(
176+
'fixed h-screen bg-primary-500 z-50 py-8 transition-all duration-300 ease-in-out',
177+
isCollapsed ? 'w-[60px] px-2' : 'w-[248px] px-6'
178+
)}
179+
>
156180
<div className="flex flex-col size-full relative overflow-hidden">
157-
<Link to={ROUTING.PAGE_PATH_ROOT} onClick={onCloseSetting}>
158-
<img src={logo} alt="Bucketer" />
159-
</Link>
181+
<div
182+
className={cn('group relative flex items-center', {
183+
'w-full justify-center': isCollapsed
184+
})}
185+
>
186+
<Link
187+
to={ROUTING.PAGE_PATH_ROOT}
188+
onClick={onCloseSetting}
189+
className={cn(
190+
'overflow-hidden',
191+
isCollapsed && 'flex-center w-full'
192+
)}
193+
>
194+
{isCollapsed ? (
195+
<img
196+
src={logoIcon}
197+
alt="Bucketeer"
198+
className="w-11 h-11 shrink-0"
199+
/>
200+
) : (
201+
<img src={logo} alt="Bucketer" />
202+
)}
203+
</Link>
204+
<button
205+
type="button"
206+
onClick={toggleCollapsed}
207+
className={cn(
208+
'flex-center size-6 rounded-md text-primary-50 shrink-0',
209+
'opacity-0 group-hover:opacity-100 focus-visible:opacity-100 transition-opacity',
210+
isCollapsed
211+
? 'absolute inset-0 mx-auto w-11 h-11 bg-primary-500/80 hover:bg-primary-400 pointer-events-none group-hover:pointer-events-auto focus-visible:pointer-events-auto'
212+
: 'ml-auto hover:bg-primary-400'
213+
)}
214+
aria-label={t(
215+
isCollapsed ? `navigation.expand` : `navigation.collapse`
216+
)}
217+
>
218+
<Icon
219+
icon={IconSystem.IconChevronRight}
220+
color="primary-50"
221+
size="xs"
222+
className={cn({ 'rotate-180': !isCollapsed })}
223+
/>
224+
</button>
225+
</div>
160226

161227
<div className="flex flex-col flex-1 items-center pt-6">
162228
<div
@@ -165,23 +231,39 @@ const Navigation = ({ onClickNavLink }: { onClickNavLink: () => void }) => {
165231
{ 'right-0': isOpenSetting }
166232
)}
167233
>
168-
<button
169-
onClick={() => {
170-
onCloseSetting();
171-
navigate(`/${envUrlCode}${ROUTING.PAGE_PATH_FEATURES}`);
172-
}}
173-
className="flex items-center gap-x-2 text-primary-50"
174-
>
175-
<Icon icon={IconSystem.IconBackspace} />
176-
<span>{t(`navigation.back-to-main`)}</span>
177-
</button>
234+
<Tooltip
235+
hidden={!isCollapsed}
236+
content={t(`navigation.back-to-main`)}
237+
side="right"
238+
trigger={
239+
<button
240+
onClick={() => {
241+
onCloseSetting();
242+
navigate(`/${envUrlCode}${ROUTING.PAGE_PATH_FEATURES}`);
243+
}}
244+
aria-label={
245+
isCollapsed ? t(`navigation.back-to-main`) : undefined
246+
}
247+
className={cn(
248+
'flex items-center gap-x-2 text-primary-50 rounded-lg',
249+
isCollapsed
250+
? 'justify-center w-full py-2 hover:bg-primary-400'
251+
: 'px-3'
252+
)}
253+
>
254+
<Icon icon={IconSystem.IconBackspace} />
255+
{!isCollapsed && <span>{t(`navigation.back-to-main`)}</span>}
256+
</button>
257+
}
258+
/>
178259
<Divider className="my-5 bg-primary-50 opacity-10" />
179260
{settingMenuSections.map((item, index) => (
180261
<SectionMenu
181262
key={index}
182263
className="first:mt-0 mt-4"
183264
title={item.title}
184265
items={item.menus}
266+
isCollapsed={isCollapsed}
185267
/>
186268
))}
187269
</div>
@@ -191,10 +273,12 @@ const Navigation = ({ onClickNavLink }: { onClickNavLink: () => void }) => {
191273
{ 'left-0': !isOpenSetting }
192274
)}
193275
>
194-
<div className="px-3 opacity-80 uppercase typo-head-bold-tiny text-primary-50 mb-3">
195-
{t(`environment`)}
196-
</div>
197-
<MyProjects />
276+
{!isCollapsed && (
277+
<div className="px-3 opacity-80 uppercase typo-head-bold-tiny text-primary-50 mb-3">
278+
{t(`environment`)}
279+
</div>
280+
)}
281+
<MyProjects isCollapsed={isCollapsed} />
198282
<Divider className="my-5 bg-primary-50 opacity-10" />
199283
{mainMenuSections.map((item, index) => (
200284
<SectionMenu
@@ -203,34 +287,48 @@ const Navigation = ({ onClickNavLink }: { onClickNavLink: () => void }) => {
203287
title={item.title}
204288
items={item.menus}
205289
onClickNavLink={onClickNavLink}
290+
isCollapsed={isCollapsed}
206291
/>
207292
))}
208293
</div>
209294
</div>
210295

211296
<Divider className="mb-3 bg-primary-50 opacity-10" />
212297

213-
<div className="flex items-center justify-between">
298+
<div
299+
className={cn('flex items-center justify-between', {
300+
'flex-col gap-y-3': isCollapsed
301+
})}
302+
>
214303
<UserMenu onOpenSwitchOrg={onOpenSwitchOrg} />
215-
<button
216-
type="button"
217-
onClick={() => {
218-
onOpenSetting();
219-
if (consoleAccount?.isSystemAdmin) {
220-
navigate(ROUTING.PAGE_PATH_ORGANIZATIONS);
221-
} else {
222-
navigate(`/${envUrlCode}${ROUTING.PAGE_PATH_SETTINGS}`);
223-
}
224-
}}
225-
>
226-
<Icon icon={IconSystem.IconSetting} color="primary-50" />
227-
</button>
304+
<Tooltip
305+
hidden={!isCollapsed}
306+
content={t(`settings`)}
307+
side="right"
308+
trigger={
309+
<button
310+
type="button"
311+
aria-label={isCollapsed ? t(`settings`) : undefined}
312+
onClick={() => {
313+
onOpenSetting();
314+
if (consoleAccount?.isSystemAdmin) {
315+
navigate(ROUTING.PAGE_PATH_ORGANIZATIONS);
316+
} else {
317+
navigate(`/${envUrlCode}${ROUTING.PAGE_PATH_SETTINGS}`);
318+
}
319+
}}
320+
>
321+
<Icon icon={IconSystem.IconSetting} color="primary-50" />
322+
</button>
323+
}
324+
/>
228325
</div>
229326
</div>
230327
<SwitchOrganization
231328
isOpen={isOpenSwitchOrg}
232329
onCloseSwitchOrg={onCloseSwitchOrg}
233330
onCloseSetting={onCloseSetting}
331+
isCollapsed={isCollapsed}
234332
/>
235333
</div>
236334
);

0 commit comments

Comments
 (0)