Skip to content

Commit faba45f

Browse files
authored
feat(ui/dashboard): add collapsible navigation sidebar (#2776)
* feat(dashboard): add collapsible navigation sidebar * fix(ui/dashboard): replace collapsed nav logo with SVG and fix sizing
1 parent e1f410e commit faba45f

11 files changed

Lines changed: 283 additions & 73 deletions

File tree

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

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

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

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

ui/dashboard/src/app/index.tsx

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ import {
5252
setCurrentEnvIdStorage
5353
} from 'storage/environment';
5454
import { getIsLoginFirstTimeStorage } from 'storage/login';
55+
import { getNavigationCollapsedStorage } from 'storage/navigation';
5556
import {
5657
getCurrentProjectEnvironmentStorage,
5758
setCurrentProjectEnvironmentStorage
@@ -62,6 +63,7 @@ import { ConsoleAccount, EnvironmentRole } from '@types';
6263
import { isNotEmpty } from 'utils/data-type';
6364
import { checkEnvironmentEmptyId } from 'utils/function';
6465
import { stringifyParams, useSearchParams } from 'utils/search-params';
66+
import { cn } from 'utils/style';
6567
import AccessDeniedPage from 'pages/access-denied';
6668
import APIKeysPage from 'pages/api-keys';
6769
import AuditLogsPage from 'pages/audit-logs';
@@ -141,6 +143,9 @@ function App() {
141143
export const Root = memo(() => {
142144
const authToken = getTokenStorage();
143145
const [pageKey, setPageKey] = useState<string>(uuid());
146+
const [isNavCollapsed, setIsNavCollapsed] = useState(
147+
getNavigationCollapsedStorage
148+
);
144149
const { isInitialLoading, isLogin, consoleAccount, myOrganizations } =
145150
useAuth();
146151

@@ -160,8 +165,17 @@ export const Root = memo(() => {
160165
return (
161166
<WalkthroughProvider>
162167
<div className="flex flex-row w-full h-full">
163-
<Navigation onClickNavLink={handleChangePageKey} />
164-
<div className="w-full ml-[248px] shadow-lg overflow-y-auto">
168+
<Navigation
169+
onClickNavLink={handleChangePageKey}
170+
isCollapsed={isNavCollapsed}
171+
onToggleCollapsed={setIsNavCollapsed}
172+
/>
173+
<div
174+
className={cn(
175+
'w-full shadow-lg overflow-y-auto transition-all duration-300 ease-in-out',
176+
isNavCollapsed ? 'ml-[60px]' : 'ml-[248px]'
177+
)}
178+
>
165179
<Routes>
166180
{consoleAccount.isSystemAdmin && (
167181
<Route
Lines changed: 4 additions & 0 deletions
Loading

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: 137 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { useEffect } from 'react';
22
import { Link, useLocation, useNavigate } from 'react-router';
3+
import logoIcon from 'assets/logos/logo-icon.svg';
34
import logo from 'assets/logos/logo-white.svg';
45
import { useAuth, getCurrentEnvironment } from 'auth';
56
import * as ROUTING from 'constants/routing';
@@ -8,22 +9,40 @@ import { useToggleOpen } from 'hooks';
89
import { useTranslation } from 'i18n';
910
import compact from 'lodash/compact';
1011
import flatMapDeep from 'lodash/flatMapDeep';
12+
import { setNavigationCollapsedStorage } from 'storage/navigation';
1113
import { cn } from 'utils/style';
1214
import * as IconSystem from '@icons';
1315
import Divider from 'components/divider';
1416
import Icon from 'components/icon';
17+
import { Tooltip } from 'components/tooltip';
1518
import SectionMenu from './menu-section';
1619
import MyProjects from './my-projects';
1720
import NotificationBell from './notification-bell';
1821
import SwitchOrganization from './switch-organization';
1922
import UserMenu from './user-menu';
2023

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

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

@@ -161,11 +180,59 @@ const Navigation = ({ onClickNavLink }: { onClickNavLink: () => void }) => {
161180
useToggleOpen(false);
162181

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

170237
<div className="flex flex-col flex-1 items-center pt-6">
171238
<div
@@ -174,23 +241,39 @@ const Navigation = ({ onClickNavLink }: { onClickNavLink: () => void }) => {
174241
{ 'right-0': isOpenSetting }
175242
)}
176243
>
177-
<button
178-
onClick={() => {
179-
onCloseSetting();
180-
navigate(`/${envUrlCode}${ROUTING.PAGE_PATH_FEATURES}`);
181-
}}
182-
className="flex items-center gap-x-2 text-primary-50"
183-
>
184-
<Icon icon={IconSystem.IconBackspace} />
185-
<span>{t(`navigation.back-to-main`)}</span>
186-
</button>
244+
<Tooltip
245+
hidden={!isCollapsed}
246+
content={t(`navigation.back-to-main`)}
247+
side="right"
248+
trigger={
249+
<button
250+
onClick={() => {
251+
onCloseSetting();
252+
navigate(`/${envUrlCode}${ROUTING.PAGE_PATH_FEATURES}`);
253+
}}
254+
aria-label={
255+
isCollapsed ? t(`navigation.back-to-main`) : undefined
256+
}
257+
className={cn(
258+
'flex items-center gap-x-2 text-primary-50 rounded-lg',
259+
isCollapsed
260+
? 'justify-center w-full py-2 hover:bg-primary-400'
261+
: 'px-3'
262+
)}
263+
>
264+
<Icon icon={IconSystem.IconBackspace} />
265+
{!isCollapsed && <span>{t(`navigation.back-to-main`)}</span>}
266+
</button>
267+
}
268+
/>
187269
<Divider className="my-5 bg-primary-50 opacity-10" />
188270
{settingMenuSections.map((item, index) => (
189271
<SectionMenu
190272
key={index}
191273
className="first:mt-0 mt-4"
192274
title={item.title}
193275
items={item.menus}
276+
isCollapsed={isCollapsed}
194277
/>
195278
))}
196279
</div>
@@ -200,10 +283,12 @@ const Navigation = ({ onClickNavLink }: { onClickNavLink: () => void }) => {
200283
{ 'left-0': !isOpenSetting }
201284
)}
202285
>
203-
<div className="px-3 opacity-80 uppercase typo-head-bold-tiny text-primary-50 mb-3">
204-
{t(`environment`)}
205-
</div>
206-
<MyProjects />
286+
{!isCollapsed && (
287+
<div className="px-3 opacity-80 uppercase typo-head-bold-tiny text-primary-50 mb-3">
288+
{t(`environment`)}
289+
</div>
290+
)}
291+
<MyProjects isCollapsed={isCollapsed} />
207292
<Divider className="my-5 bg-primary-50 opacity-10" />
208293
{mainMenuSections.map((item, index) => (
209294
<SectionMenu
@@ -212,37 +297,55 @@ const Navigation = ({ onClickNavLink }: { onClickNavLink: () => void }) => {
212297
title={item.title}
213298
items={item.menus}
214299
onClickNavLink={onClickNavLink}
300+
isCollapsed={isCollapsed}
215301
/>
216302
))}
217303
</div>
218304
</div>
219305

220306
<Divider className="mb-3 bg-primary-50 opacity-10" />
221307

222-
<div className="flex items-center justify-between">
308+
<div
309+
className={cn('flex items-center justify-between', {
310+
'flex-col gap-y-3': isCollapsed
311+
})}
312+
>
223313
<UserMenu onOpenSwitchOrg={onOpenSwitchOrg} />
224-
<div className="flex items-center justify-center gap-2">
314+
<div
315+
className={cn('flex items-center justify-center gap-2', {
316+
'flex-col': isCollapsed
317+
})}
318+
>
225319
<NotificationBell envUrlCode={envUrlCode} />
226-
<button
227-
type="button"
228-
onClick={() => {
229-
onOpenSetting();
230-
if (consoleAccount?.isSystemAdmin) {
231-
navigate(ROUTING.PAGE_PATH_ORGANIZATIONS);
232-
} else {
233-
navigate(`/${envUrlCode}${ROUTING.PAGE_PATH_SETTINGS}`);
234-
}
235-
}}
236-
>
237-
<Icon icon={IconSystem.IconSetting} color="primary-50" />
238-
</button>
320+
<Tooltip
321+
hidden={!isCollapsed}
322+
content={t(`settings`)}
323+
side="right"
324+
trigger={
325+
<button
326+
type="button"
327+
aria-label={isCollapsed ? t(`settings`) : undefined}
328+
onClick={() => {
329+
onOpenSetting();
330+
if (consoleAccount?.isSystemAdmin) {
331+
navigate(ROUTING.PAGE_PATH_ORGANIZATIONS);
332+
} else {
333+
navigate(`/${envUrlCode}${ROUTING.PAGE_PATH_SETTINGS}`);
334+
}
335+
}}
336+
>
337+
<Icon icon={IconSystem.IconSetting} color="primary-50" />
338+
</button>
339+
}
340+
/>
239341
</div>
240342
</div>
241343
</div>
242344
<SwitchOrganization
243345
isOpen={isOpenSwitchOrg}
244346
onCloseSwitchOrg={onCloseSwitchOrg}
245347
onCloseSetting={onCloseSetting}
348+
isCollapsed={isCollapsed}
246349
/>
247350
</div>
248351
);

0 commit comments

Comments
 (0)