Skip to content

Commit 90e9244

Browse files
committed
feat(FR-1580): Implement custom theme preview mode feature
1 parent 1510583 commit 90e9244

27 files changed

+341
-35
lines changed

react/src/components/BrandingSettingList.tsx

Lines changed: 53 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,38 @@
1+
import BAIAlert from './BAIAlert';
12
import ThemeColorPicker from './BrandingSettingItems/ThemeColorPicker';
23
import SettingList, { SettingGroup } from './SettingList';
3-
import { BAIButton } from 'backend.ai-ui';
4+
import { ExportOutlined } from '@ant-design/icons';
5+
import { App } from 'antd';
6+
import { BAIButton, BAIFlex } from 'backend.ai-ui';
7+
import _ from 'lodash';
48
import { useTranslation } from 'react-i18next';
9+
import { downloadBlob } from 'src/helper/csv-util';
510
import { useBAISettingUserState } from 'src/hooks/useBAISetting';
611

712
interface BrandingSettingListProps {}
813

914
const BrandingSettingList: React.FC<BrandingSettingListProps> = () => {
1015
const { t } = useTranslation();
16+
const { message } = App.useApp();
1117

12-
const [, setUserCustomThemeConfig] = useBAISettingUserState(
13-
'custom_theme_config',
14-
);
18+
const [userCustomThemeConfig, setUserCustomThemeConfig] =
19+
useBAISettingUserState('custom_theme_config');
1520

1621
const settingGroups: Array<SettingGroup> = [
1722
{
1823
'data-testid': 'group-theme-customization',
1924
title: t('userSettings.Theme'),
2025
titleExtra: (
21-
<BAIButton size="small">{t('userSettings.theme.Preview')}</BAIButton>
26+
<BAIButton
27+
size="small"
28+
action={async () => {
29+
const previewWindow = window.open(window.location.origin, '_blank');
30+
previewWindow?.sessionStorage.setItem('isThemePreviewMode', 'true');
31+
previewWindow?.location.reload();
32+
}}
33+
>
34+
{t('userSettings.theme.Preview')}
35+
</BAIButton>
2236
),
2337
settingItems: [
2438
{
@@ -82,7 +96,40 @@ const BrandingSettingList: React.FC<BrandingSettingListProps> = () => {
8296
},
8397
];
8498

85-
return <SettingList settingGroups={settingGroups} showSearchBar />;
99+
return (
100+
<BAIFlex direction="column" gap="md" align="stretch">
101+
<BAIAlert
102+
description={t('userSettings.theme.CustomThemeSettingAlert')}
103+
type="warning"
104+
showIcon
105+
/>
106+
<SettingList
107+
showSearchBar
108+
showResetButton
109+
settingGroups={settingGroups}
110+
primaryButton={
111+
<BAIButton
112+
type="primary"
113+
icon={<ExportOutlined />}
114+
action={async () => {
115+
if (_.isEmpty(userCustomThemeConfig)) {
116+
message.error(t('userSettings.theme.NoChangesMade'));
117+
return;
118+
}
119+
120+
const blob = new Blob(
121+
[JSON.stringify(userCustomThemeConfig, null, 2)],
122+
{ type: 'application/json' },
123+
);
124+
downloadBlob(blob, `theme.json`);
125+
}}
126+
>
127+
{t('theme.button.ExportToJson')}
128+
</BAIButton>
129+
}
130+
/>
131+
</BAIFlex>
132+
);
86133
};
87134

88135
export default BrandingSettingList;

react/src/components/DefaultProviders.tsx

Lines changed: 62 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { ThemeModeProvider, useThemeMode } from '../hooks/useThemeMode';
1313
import indexCss from '../index.css?raw';
1414
import { StyleProvider, createCache } from '@ant-design/cssinjs';
1515
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
16-
import { useUpdateEffect } from 'ahooks';
16+
import { useSessionStorageState, useUpdateEffect } from 'ahooks';
1717
import { App, AppProps, theme, Typography } from 'antd';
1818
import { BAIConfigProvider } from 'backend.ai-ui';
1919
import dayjs from 'dayjs';
@@ -47,17 +47,20 @@ import weekday from 'dayjs/plugin/weekday';
4747
import i18n from 'i18next';
4848
import Backend from 'i18next-http-backend';
4949
import { createStore, Provider as JotaiProvider } from 'jotai';
50+
import _ from 'lodash';
5051
import { GlobeIcon } from 'lucide-react';
5152
import React, {
5253
Suspense,
5354
useEffect,
55+
useEffectEvent,
5456
useLayoutEffect,
5557
useMemo,
5658
useState,
5759
} from 'react';
5860
import { useTranslation, initReactI18next } from 'react-i18next';
5961
import { RelayEnvironmentProvider } from 'react-relay/hooks';
6062
import { BrowserRouter, useLocation, useNavigate } from 'react-router-dom';
63+
import { useBAISettingUserState } from 'src/hooks/useBAISetting';
6164
import { QueryParamProvider } from 'use-query-params';
6265
import { ReactRouter6Adapter } from 'use-query-params/adapters/react-router-6';
6366

@@ -205,7 +208,34 @@ const DefaultProvidersForWebComponent: React.FC<DefaultProvidersProps> = ({
205208
}) => {
206209
const cache = useMemo(() => createCache(), []);
207210
const [lang] = useCurrentLanguage();
211+
212+
const [userCustomThemeConfig] = useBAISettingUserState('custom_theme_config');
213+
const [isThemePreviewMode] = useSessionStorageState('isThemePreviewMode', {
214+
defaultValue: false,
215+
});
208216
const themeConfig = useCustomThemeConfig();
217+
const defaultThemeConfig =
218+
isThemePreviewMode && !_.isEmpty(userCustomThemeConfig)
219+
? userCustomThemeConfig
220+
: themeConfig;
221+
222+
const reloadPreviewWindow = useEffectEvent(() => {
223+
if (!isThemePreviewMode) return;
224+
225+
const handleLocalStorageChange = (e: StorageEvent) => {
226+
if (e.key === 'backendaiwebui.settings.user.custom_theme_config') {
227+
window.location.reload();
228+
}
229+
};
230+
window.addEventListener('storage', handleLocalStorageChange);
231+
return () =>
232+
window.removeEventListener('storage', handleLocalStorageChange);
233+
});
234+
235+
useEffect(() => {
236+
reloadPreviewWindow();
237+
}, []);
238+
209239
const { isDarkMode } = useThemeMode();
210240

211241
const componentValues = useMemo(() => {
@@ -249,8 +279,8 @@ const DefaultProvidersForWebComponent: React.FC<DefaultProvidersProps> = ({
249279
}}
250280
theme={{
251281
...(isDarkMode
252-
? { ...themeConfig?.dark }
253-
: { ...themeConfig?.light }),
282+
? { ...defaultThemeConfig?.dark }
283+
: { ...defaultThemeConfig?.light }),
254284
algorithm: isDarkMode
255285
? theme.darkAlgorithm
256286
: theme.defaultAlgorithm,
@@ -325,9 +355,35 @@ export const DefaultProvidersForReactRoot: React.FC<
325355
Partial<DefaultProvidersProps>
326356
> = ({ children }) => {
327357
const [lang] = useCurrentLanguage();
328-
const themeConfig = useCustomThemeConfig();
329358
const { isDarkMode } = useThemeMode();
330359

360+
const [userCustomThemeConfig] = useBAISettingUserState('custom_theme_config');
361+
const [isThemePreviewMode] = useSessionStorageState('isThemePreviewMode', {
362+
defaultValue: false,
363+
});
364+
const themeConfig = useCustomThemeConfig();
365+
const defaultThemeConfig =
366+
isThemePreviewMode && !_.isEmpty(userCustomThemeConfig)
367+
? userCustomThemeConfig
368+
: themeConfig;
369+
370+
const reloadPreviewWindow = useEffectEvent(() => {
371+
if (!isThemePreviewMode) return;
372+
373+
const handleLocalStorageChange = (e: StorageEvent) => {
374+
if (e.key === 'backendaiwebui.settings.user.custom_theme_config') {
375+
window.location.reload();
376+
}
377+
};
378+
window.addEventListener('storage', handleLocalStorageChange);
379+
return () =>
380+
window.removeEventListener('storage', handleLocalStorageChange);
381+
});
382+
383+
useEffect(() => {
384+
reloadPreviewWindow();
385+
}, []);
386+
331387
return (
332388
<>
333389
<style>{indexCss}</style>
@@ -341,8 +397,8 @@ export const DefaultProvidersForReactRoot: React.FC<
341397
}
342398
theme={{
343399
...(isDarkMode
344-
? { ...themeConfig?.dark }
345-
: { ...themeConfig?.light }),
400+
? { ...defaultThemeConfig?.dark }
401+
: { ...defaultThemeConfig?.light }),
346402
algorithm: isDarkMode
347403
? theme.darkAlgorithm
348404
: theme.defaultAlgorithm,

react/src/components/MainLayout/MainLayout.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import ForceTOTPChecker from '../ForceTOTPChecker';
1010
import NetworkStatusBanner from '../NetworkStatusBanner';
1111
import NoResourceGroupAlert from '../NoResourceGroupAlert';
1212
import PasswordChangeRequestAlert from '../PasswordChangeRequestAlert';
13+
import ThemePreviewModeAlert from '../ThemePreviewModeAlert';
1314
import { DRAWER_WIDTH } from '../WEBUINotificationDrawer';
1415
import WebUIBreadcrumb from '../WebUIBreadcrumb';
1516
import WebUIHeader from './WebUIHeader';
@@ -235,6 +236,7 @@ function MainLayout() {
235236
align="stretch"
236237
className={styles.alertWrapper}
237238
>
239+
<ThemePreviewModeAlert />
238240
<ErrorBoundaryWithNullFallback>
239241
<NoResourceGroupAlert />
240242
</ErrorBoundaryWithNullFallback>

react/src/components/SettingList.tsx

Lines changed: 30 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import { RedoOutlined, SearchOutlined } from '@ant-design/icons';
33
import { useToggle } from 'ahooks';
44
import {
55
Alert,
6-
Button,
76
Checkbox,
87
Divider,
98
Empty,
@@ -13,9 +12,9 @@ import {
1312
theme,
1413
} from 'antd';
1514
import { createStyles } from 'antd-style';
16-
import { BAIModal, BAIFlex } from 'backend.ai-ui';
15+
import { BAIModal, BAIFlex, BAIButton } from 'backend.ai-ui';
1716
import _ from 'lodash';
18-
import { useState, ReactNode } from 'react';
17+
import React, { useState, ReactNode } from 'react';
1918
import { useTranslation } from 'react-i18next';
2019

2120
const useStyles = createStyles(({ css }) => ({
@@ -35,6 +34,7 @@ export type SettingGroup = {
3534
titleExtra?: ReactNode;
3635
description?: ReactNode;
3736
settingItems: SettingItemProps[];
37+
alert?: ReactNode;
3838
};
3939

4040
interface SettingPageProps {
@@ -43,6 +43,8 @@ interface SettingPageProps {
4343
showChangedOptionFilter?: boolean;
4444
showResetButton?: boolean;
4545
showSearchBar?: boolean;
46+
primaryButton?: ReactNode;
47+
extraButton?: ReactNode;
4648
}
4749

4850
const TabTitle: React.FC<{
@@ -108,6 +110,7 @@ const GroupSettingItems: React.FC<
108110
)}
109111
</BAIFlex>
110112
<BAIFlex direction="column" align="stretch" gap={'lg'}>
113+
{group.alert}
111114
{group.settingItems.map((item, idx) => (
112115
<SettingItem key={item.title + idx} {...item} />
113116
))}
@@ -122,6 +125,8 @@ const SettingList: React.FC<SettingPageProps> = ({
122125
showChangedOptionFilter,
123126
showResetButton,
124127
showSearchBar,
128+
primaryButton,
129+
extraButton,
125130
}) => {
126131
'use memo';
127132

@@ -180,14 +185,16 @@ const SettingList: React.FC<SettingPageProps> = ({
180185
{t('settings.ShowOnlyChanged')}
181186
</Checkbox>
182187
)}
188+
{extraButton}
183189
{!!showResetButton && (
184-
<Button
190+
<BAIButton
185191
icon={<RedoOutlined />}
186192
onClick={() => setIsOpenResetChangesModal()}
187193
>
188194
{t('button.Reset')}
189-
</Button>
195+
</BAIButton>
190196
)}
197+
{primaryButton}
191198
</BAIFlex>
192199
<Tabs
193200
activeKey={activeTabKey}
@@ -241,21 +248,24 @@ const SettingList: React.FC<SettingPageProps> = ({
241248
count={group.settingItems.length}
242249
/>
243250
),
244-
children:
245-
group.settingItems.length > 0 ? (
246-
<GroupSettingItems
247-
group={group}
248-
hideEmpty
249-
onReset={() => {
250-
setIsOpenResetChangesModal();
251-
}}
252-
/>
253-
) : (
254-
<Empty
255-
image={Empty.PRESENTED_IMAGE_SIMPLE}
256-
description={t('settings.NoChangesToDisplay')}
257-
/>
258-
),
251+
children: (
252+
<BAIFlex direction="column" align="stretch" gap={'xl'}>
253+
{group.settingItems.length > 0 ? (
254+
<GroupSettingItems
255+
group={group}
256+
hideEmpty
257+
onReset={() => {
258+
setIsOpenResetChangesModal();
259+
}}
260+
/>
261+
) : (
262+
<Empty
263+
image={Empty.PRESENTED_IMAGE_SIMPLE}
264+
description={t('settings.NoChangesToDisplay')}
265+
/>
266+
)}
267+
</BAIFlex>
268+
),
259269
})),
260270
]}
261271
/>
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import BAIAlert, { BAIAlertProps } from './BAIAlert';
2+
import { useSessionStorageState } from 'ahooks';
3+
import { useTranslation } from 'react-i18next';
4+
5+
interface ThemePreviewModeAlertProps extends BAIAlertProps {}
6+
7+
const ThemePreviewModeAlert: React.FC<ThemePreviewModeAlertProps> = () => {
8+
const { t } = useTranslation();
9+
const [isThemePreviewMode] = useSessionStorageState('isThemePreviewMode', {
10+
defaultValue: false,
11+
});
12+
13+
return isThemePreviewMode ? (
14+
<BAIAlert showIcon type="warning" message={t('theme.PreviewModeAlert')} />
15+
) : null;
16+
};
17+
18+
export default ThemePreviewModeAlert;

react/src/pages/ConfigurationsPage.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import ConfigurationsSettingList from '../components/ConfigurationsSettingList';
2+
import { useSessionStorageState } from 'ahooks';
23
import { Card, Skeleton } from 'antd';
4+
import { filterOutEmpty } from 'backend.ai-ui';
35
import { Suspense } from 'react';
46
import { useTranslation } from 'react-i18next';
57
import BAIErrorBoundary from 'src/components/BAIErrorBoundary';
@@ -13,21 +15,24 @@ const tabParam = withDefault(StringParam, 'configurations');
1315
const ConfigurationsPage = () => {
1416
const { t } = useTranslation();
1517
const [curTabKey, setCurTabKey] = useQueryParam('tab', tabParam);
18+
const [isThemePreviewMode] = useSessionStorageState('isThemePreviewMode', {
19+
defaultValue: false,
20+
});
1621

1722
return (
1823
<Card
1924
activeTabKey={curTabKey}
2025
onTabChange={(key) => setCurTabKey(key as TabKey)}
21-
tabList={[
26+
tabList={filterOutEmpty([
2227
{
2328
key: 'configurations',
2429
tab: t('webui.menu.Configurations'),
2530
},
26-
{
31+
!isThemePreviewMode && {
2732
key: 'branding',
2833
tab: t('webui.menu.Branding'),
2934
},
30-
]}
35+
])}
3136
>
3237
<Suspense fallback={<Skeleton active />}>
3338
{curTabKey === 'configurations' && (

0 commit comments

Comments
 (0)