fix: custom logos
This commit is contained in:
@@ -76,6 +76,7 @@ export default defineConfig({
|
|||||||
antd: {
|
antd: {
|
||||||
style: 'less'
|
style: 'less'
|
||||||
},
|
},
|
||||||
|
title: 'GPUStack',
|
||||||
hash: true,
|
hash: true,
|
||||||
access: {},
|
access: {},
|
||||||
model: {},
|
model: {},
|
||||||
|
|||||||
+1
-1
@@ -22,7 +22,7 @@
|
|||||||
"@ant-design/icons": "^6.1.0",
|
"@ant-design/icons": "^6.1.0",
|
||||||
"@ant-design/pro-components": "3.1.0-0",
|
"@ant-design/pro-components": "3.1.0-0",
|
||||||
"@braintree/sanitize-url": "^7.1.1",
|
"@braintree/sanitize-url": "^7.1.1",
|
||||||
"@gpustack/core-ui": "^1.0.2",
|
"@gpustack/core-ui": "^1.0.3",
|
||||||
"@huggingface/gguf": "^0.1.7",
|
"@huggingface/gguf": "^0.1.7",
|
||||||
"@huggingface/hub": "^0.15.1",
|
"@huggingface/hub": "^0.15.1",
|
||||||
"@huggingface/tasks": "^0.11.6",
|
"@huggingface/tasks": "^0.11.6",
|
||||||
|
|||||||
Generated
+11741
-3223
File diff suppressed because it is too large
Load Diff
+14
-4
@@ -1,7 +1,9 @@
|
|||||||
|
import { userSettingsHelperAtom } from '@/atoms/settings';
|
||||||
import { GPUStackVersionAtom, UpdateCheckAtom } from '@/atoms/user';
|
import { GPUStackVersionAtom, UpdateCheckAtom } from '@/atoms/user';
|
||||||
import { setAtomStorage } from '@/atoms/utils';
|
import { setAtomStorage } from '@/atoms/utils';
|
||||||
import { DEFAULT_ENTER_PAGE, GPUSTACK_API_BASE_URL } from '@/config/settings';
|
import { DEFAULT_ENTER_PAGE, GPUSTACK_API_BASE_URL } from '@/config/settings';
|
||||||
import { getGPUStackPlugin } from '@/plugins';
|
import { COLOR_PRIMARY } from '@/config/theme/constants';
|
||||||
|
import { enterprisePluginReady } from '@/plugins/enterprise-ready';
|
||||||
import { GPUStackPluginManager } from '@/plugins/manager';
|
import { GPUStackPluginManager } from '@/plugins/manager';
|
||||||
import { mergeEnterpriseRoutes } from '@/plugins/route-merger';
|
import { mergeEnterpriseRoutes } from '@/plugins/route-merger';
|
||||||
import { requestConfig } from '@/request-config';
|
import { requestConfig } from '@/request-config';
|
||||||
@@ -18,7 +20,7 @@ import {
|
|||||||
writeState
|
writeState
|
||||||
} from '@/utils/localstore/index';
|
} from '@/utils/localstore/index';
|
||||||
import '@gpustack/core-ui/style.css';
|
import '@gpustack/core-ui/style.css';
|
||||||
import { RequestConfig, history } from '@umijs/max';
|
import { RequestConfig, history, request as umiRequest } from '@umijs/max';
|
||||||
import { message } from 'antd';
|
import { message } from 'antd';
|
||||||
|
|
||||||
// only for the first login and access from http://localhost
|
// only for the first login and access from http://localhost
|
||||||
@@ -40,12 +42,20 @@ export async function getInitialState(): Promise<{
|
|||||||
pluginData?: Record<string, any>;
|
pluginData?: Record<string, any>;
|
||||||
}> {
|
}> {
|
||||||
const { location } = history;
|
const { location } = history;
|
||||||
const enterprisePlugin = getGPUStackPlugin();
|
|
||||||
|
// In open-source builds the promise resolves immediately.
|
||||||
|
await enterprisePluginReady;
|
||||||
|
|
||||||
// initialize plugins and merge enterprise locales
|
// initialize plugins and merge enterprise locales
|
||||||
let pluginData = {};
|
let pluginData = {};
|
||||||
try {
|
try {
|
||||||
pluginData = await GPUStackPluginManager.initialize();
|
pluginData = await GPUStackPluginManager.initialize({
|
||||||
|
request: umiRequest,
|
||||||
|
setUserSettings: (value) => setAtomStorage(userSettingsHelperAtom, value),
|
||||||
|
setStorageUserSettings: (value) =>
|
||||||
|
setAtomStorage(userSettingsHelperAtom, value),
|
||||||
|
defaultColorPrimary: COLOR_PRIMARY
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to initialize plugins:', error);
|
console.error('Failed to initialize plugins:', error);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import GpustackLogo from '@/assets/images/gpustack-logo.png';
|
||||||
|
import SmallLogo from '@/assets/images/small-logo-200x200.png';
|
||||||
|
import useUserSettings from '@/hooks/use-user-settings';
|
||||||
|
import { getGPUStackPlugin } from '@/plugins';
|
||||||
|
|
||||||
|
const useLogo = () => {
|
||||||
|
const { isDarkTheme, userSettings } = useUserSettings();
|
||||||
|
|
||||||
|
const enterprisePlugin = getGPUStackPlugin();
|
||||||
|
const resolved =
|
||||||
|
enterprisePlugin?.branding?.resolveLogos?.(userSettings, isDarkTheme) ?? {};
|
||||||
|
|
||||||
|
return {
|
||||||
|
sidebarLogo: resolved.sidebarLogo || GpustackLogo,
|
||||||
|
miniLogo: resolved.miniLogo || SmallLogo
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export { useLogo };
|
||||||
@@ -1,13 +1,17 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
import GpustackLogo from '@/assets/images/gpustack-logo.png';
|
import { useLogo } from '@/hooks/use-logo';
|
||||||
import SmallLogo from '@/assets/images/small-logo-200x200.png';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
const LogoIcon: React.FC = () => {
|
const LogoIcon: React.FC = () => {
|
||||||
return <img src={GpustackLogo} alt="logo" style={{ height: 24 }} />;
|
const { sidebarLogo } = useLogo();
|
||||||
|
|
||||||
|
return <img src={sidebarLogo} alt="logo" style={{ height: 24 }} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
const SLogoIcon: React.FC = () => {
|
const SLogoIcon: React.FC = () => {
|
||||||
return <img src={SmallLogo} alt="logo" style={{ height: 24 }} />;
|
const { miniLogo } = useLogo();
|
||||||
|
|
||||||
|
return <img src={miniLogo} alt="logo" style={{ height: 24 }} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
export { LogoIcon, SLogoIcon };
|
export { LogoIcon, SLogoIcon };
|
||||||
|
|||||||
@@ -119,6 +119,7 @@ export const ExtraContent = (props: { isDarkTheme?: boolean }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const { initialState } = initialInfo;
|
const { initialState } = initialInfo;
|
||||||
|
const GlobalSettings = pluginManager?.components?.GlobalSettings;
|
||||||
console.log('plugin+++++++++', pluginManager);
|
console.log('plugin+++++++++', pluginManager);
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -303,6 +304,7 @@ export const ExtraContent = (props: { isDarkTheme?: boolean }) => {
|
|||||||
/>
|
/>
|
||||||
</IconWrapper>
|
</IconWrapper>
|
||||||
</DropdownActions>
|
</DropdownActions>
|
||||||
|
{GlobalSettings && <GlobalSettings></GlobalSettings>}
|
||||||
<DropdownActions menu={{ ...userMenu }} popupRender={userPopupRender}>
|
<DropdownActions menu={{ ...userMenu }} popupRender={userPopupRender}>
|
||||||
<IconWrapper>
|
<IconWrapper>
|
||||||
<Avatar
|
<Avatar
|
||||||
|
|||||||
+1
-28
@@ -39,7 +39,7 @@ import {
|
|||||||
import { Button, ConfigProvider, Modal, theme } from 'antd';
|
import { Button, ConfigProvider, Modal, theme } from 'antd';
|
||||||
import { useAtom } from 'jotai';
|
import { useAtom } from 'jotai';
|
||||||
import 'overlayscrollbars/overlayscrollbars.css';
|
import 'overlayscrollbars/overlayscrollbars.css';
|
||||||
import { useEffect, useMemo, useRef } from 'react';
|
import { useMemo, useRef } from 'react';
|
||||||
import { PageContainerInner } from '../pages/_components/page-box';
|
import { PageContainerInner } from '../pages/_components/page-box';
|
||||||
import Exception from './Exception';
|
import Exception from './Exception';
|
||||||
import './Layout.css';
|
import './Layout.css';
|
||||||
@@ -227,32 +227,6 @@ export default (props: any) => {
|
|||||||
return NO_CONTAINER_PAGES.includes(matchedRoute?.name as string);
|
return NO_CONTAINER_PAGES.includes(matchedRoute?.name as string);
|
||||||
}, [matchedRoute]);
|
}, [matchedRoute]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const initializeScroller = () => {
|
|
||||||
// const body = document.querySelector('.ant-pro-grid-content-children');
|
|
||||||
// if (body) {
|
|
||||||
// const ins = initialize(body);
|
|
||||||
// window.__GPUSTACK_BODY_SCROLLER__ = ins;
|
|
||||||
// return true;
|
|
||||||
// }
|
|
||||||
// return false;
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Try to initialize immediately
|
|
||||||
if (!initializeScroller()) {
|
|
||||||
// If element not found, retry with requestAnimationFrame
|
|
||||||
const rafId = requestAnimationFrame(() => {
|
|
||||||
if (!initializeScroller()) {
|
|
||||||
// If still not found, retry after a short delay
|
|
||||||
const timeoutId = setTimeout(initializeScroller, 100);
|
|
||||||
return () => clearTimeout(timeoutId);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return () => cancelAnimationFrame(rafId);
|
|
||||||
}
|
|
||||||
}, [initialize]);
|
|
||||||
|
|
||||||
const collapsed = useMemo(() => {
|
const collapsed = useMemo(() => {
|
||||||
return userSettings.collapsed || false;
|
return userSettings.collapsed || false;
|
||||||
}, [userSettings.collapsed]);
|
}, [userSettings.collapsed]);
|
||||||
@@ -346,7 +320,6 @@ export default (props: any) => {
|
|||||||
return (
|
return (
|
||||||
<ConfigProvider
|
<ConfigProvider
|
||||||
componentSize="large"
|
componentSize="large"
|
||||||
key={userSettings.colorPrimary}
|
|
||||||
theme={{
|
theme={{
|
||||||
algorithm: userSettings.isDarkTheme
|
algorithm: userSettings.isDarkTheme
|
||||||
? theme.darkAlgorithm
|
? theme.darkAlgorithm
|
||||||
|
|||||||
@@ -276,5 +276,9 @@ export default {
|
|||||||
'common.status.enabled': 'Enabled',
|
'common.status.enabled': 'Enabled',
|
||||||
'common.status.disabled': 'Disabled',
|
'common.status.disabled': 'Disabled',
|
||||||
'common.button.duplicate': 'Duplicate',
|
'common.button.duplicate': 'Duplicate',
|
||||||
'common.option.other': 'Other'
|
'common.option.other': 'Other',
|
||||||
|
'common.file.size.limit': 'File size must not exceed {size}.',
|
||||||
|
'common.file.format.limit': 'Invalid file format. Allowed: {formats}.',
|
||||||
|
'common.image.limit.width': 'Image width must be {width}.',
|
||||||
|
'common.image.limit.height': 'Image height must be {height}.'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -276,7 +276,11 @@ export default {
|
|||||||
'common.status.enabled': 'Enabled',
|
'common.status.enabled': 'Enabled',
|
||||||
'common.status.disabled': 'Disabled',
|
'common.status.disabled': 'Disabled',
|
||||||
'common.button.duplicate': 'Duplicate',
|
'common.button.duplicate': 'Duplicate',
|
||||||
'common.option.other': 'Other'
|
'common.option.other': 'Other',
|
||||||
|
'common.file.size.limit': 'File size must not exceed {size}.',
|
||||||
|
'common.file.format.limit': 'Invalid file format. Allowed: {formats}.',
|
||||||
|
'common.image.limit.width': 'Image width must be {width}.',
|
||||||
|
'common.image.limit.height': 'Image height must be {height}.'
|
||||||
};
|
};
|
||||||
|
|
||||||
// ========== To-Do: Translate Keys (Remove After Translation) ==========
|
// ========== To-Do: Translate Keys (Remove After Translation) ==========
|
||||||
|
|||||||
@@ -275,7 +275,11 @@ export default {
|
|||||||
'common.status.enabled': 'Enabled',
|
'common.status.enabled': 'Enabled',
|
||||||
'common.status.disabled': 'Disabled',
|
'common.status.disabled': 'Disabled',
|
||||||
'common.button.duplicate': 'Duplicate',
|
'common.button.duplicate': 'Duplicate',
|
||||||
'common.option.other': 'Другое'
|
'common.option.other': 'Другое',
|
||||||
|
'common.file.size.limit': 'File size must not exceed {size}.',
|
||||||
|
'common.file.format.limit': 'Invalid file format. Allowed: {formats}.',
|
||||||
|
'common.image.limit.width': 'Image width must be {width}.',
|
||||||
|
'common.image.limit.height': 'Image height must be {height}.'
|
||||||
};
|
};
|
||||||
|
|
||||||
// ========== To-Do: Translate Keys (Remove After Translation) ==========
|
// ========== To-Do: Translate Keys (Remove After Translation) ==========
|
||||||
|
|||||||
@@ -279,5 +279,9 @@ export default {
|
|||||||
'common.status.enabled': 'Etkin',
|
'common.status.enabled': 'Etkin',
|
||||||
'common.status.disabled': 'Devre dışı',
|
'common.status.disabled': 'Devre dışı',
|
||||||
'common.button.duplicate': 'Çoğalt',
|
'common.button.duplicate': 'Çoğalt',
|
||||||
'common.option.other': 'Diğer'
|
'common.option.other': 'Diğer',
|
||||||
|
'common.file.size.limit': 'File size must not exceed {size}.',
|
||||||
|
'common.file.format.limit': 'Invalid file format. Allowed: {formats}.',
|
||||||
|
'common.image.limit.width': 'Image width must be {width}.',
|
||||||
|
'common.image.limit.height': 'Image height must be {height}.'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -268,5 +268,9 @@ export default {
|
|||||||
'common.status.enabled': '已启用',
|
'common.status.enabled': '已启用',
|
||||||
'common.status.disabled': '未启用',
|
'common.status.disabled': '未启用',
|
||||||
'common.button.duplicate': '复制',
|
'common.button.duplicate': '复制',
|
||||||
'common.option.other': '其它'
|
'common.option.other': '其它',
|
||||||
|
'common.file.size.limit': '文件大小不超过{size}。',
|
||||||
|
'common.file.format.limit': '文件格式不正确,仅支持{formats}。',
|
||||||
|
'common.image.limit.width': '图片宽度须为{width}。',
|
||||||
|
'common.image.limit.height': '图片高度须为{height}。'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import Bg2 from '@/assets/images/bg-2.png';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const Background: React.FC<{ isDarkTheme: boolean }> = ({ isDarkTheme }) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: 'fixed',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
bottom: 0,
|
||||||
|
right: 0,
|
||||||
|
minHeight: '100vh',
|
||||||
|
zIndex: -1,
|
||||||
|
background: isDarkTheme
|
||||||
|
? `radial-gradient(at 50% 20%, #383838 0%, #292929 40%, #000 100%)`
|
||||||
|
: `url(${Bg2}) center center no-repeat`,
|
||||||
|
backgroundSize: isDarkTheme ? 'contain' : 'cover',
|
||||||
|
opacity: isDarkTheme ? 1 : 0.6
|
||||||
|
}}
|
||||||
|
></div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Background;
|
||||||
@@ -98,7 +98,6 @@ const LocalUserForm: React.FC<LocalUserFormProps> = (props) => {
|
|||||||
prefix={<UserOutlined />}
|
prefix={<UserOutlined />}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name="password"
|
name="password"
|
||||||
rules={[
|
rules={[
|
||||||
|
|||||||
+155
-46
@@ -1,20 +1,48 @@
|
|||||||
import Bg2 from '@/assets/images/bg-2.png';
|
import LogoIcon from '@/assets/images/gpustack-logo.png';
|
||||||
import { userAtom } from '@/atoms/user';
|
import { initialPasswordAtom, userAtom } from '@/atoms/user';
|
||||||
|
import { resetStorageUserSettings } from '@/atoms/utils';
|
||||||
import DarkMask from '@/components/dark-mask';
|
import DarkMask from '@/components/dark-mask';
|
||||||
import Footer from '@/components/footer';
|
import Footer from '@/components/footer';
|
||||||
import LangSelect from '@/components/lang-select';
|
import LangSelect from '@/components/lang-select';
|
||||||
import ThemeDropActions from '@/components/theme-toggle/theme-drop-actions';
|
import ThemeDropActions from '@/components/theme-toggle/theme-drop-actions';
|
||||||
|
import { PasswordReg } from '@/config';
|
||||||
|
import { GPUSTACK_API_BASE_URL } from '@/config/settings';
|
||||||
|
import { COLOR_PRIMARY } from '@/config/theme/constants';
|
||||||
|
import externalLinks from '@/constants/external-links';
|
||||||
import useUserSettings from '@/hooks/use-user-settings';
|
import useUserSettings from '@/hooks/use-user-settings';
|
||||||
import { useModel } from '@umijs/max';
|
import useUserSettingsStorage from '@/hooks/use-user-settings-storage';
|
||||||
|
import { getGPUStackPlugin } from '@/plugins';
|
||||||
|
import {
|
||||||
|
CRYPT_TEXT,
|
||||||
|
IS_FIRST_LOGIN,
|
||||||
|
writeState
|
||||||
|
} from '@/utils/localstore/index';
|
||||||
|
import { CoreUIProvider } from '@gpustack/core-ui';
|
||||||
|
import {
|
||||||
|
getAllLocales,
|
||||||
|
request,
|
||||||
|
setLocale,
|
||||||
|
useIntl,
|
||||||
|
useModel,
|
||||||
|
useNavigate
|
||||||
|
} from '@umijs/max';
|
||||||
import { ConfigProvider, theme } from 'antd';
|
import { ConfigProvider, theme } from 'antd';
|
||||||
import { createStyles } from 'antd-style';
|
import { createStyles } from 'antd-style';
|
||||||
|
import CryptoJS from 'crypto-js';
|
||||||
import { useAtom } from 'jotai';
|
import { useAtom } from 'jotai';
|
||||||
import { useEffect } from 'react';
|
import { useCallback, useEffect, useMemo } from 'react';
|
||||||
|
import { flushSync } from 'react-dom';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
import { updatePassword as updatePasswordApi } from './apis';
|
||||||
|
import Background from './components/background';
|
||||||
import LoginForm from './components/login-form';
|
import LoginForm from './components/login-form';
|
||||||
import PasswordForm from './components/password-form';
|
import PasswordForm from './components/password-form';
|
||||||
|
import { useLocalAuth } from './hooks/use-local-auth';
|
||||||
|
import { useSSOAuth } from './hooks/use-sso-auth';
|
||||||
import { checkDefaultPage } from './utils';
|
import { checkDefaultPage } from './utils';
|
||||||
|
|
||||||
|
const COLOR_LINK = '#1677ff';
|
||||||
|
|
||||||
const useStyles = createStyles(({ token, css }) => ({
|
const useStyles = createStyles(({ token, css }) => ({
|
||||||
header: css`
|
header: css`
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -25,6 +53,7 @@ const useStyles = createStyles(({ token, css }) => ({
|
|||||||
top: 0;
|
top: 0;
|
||||||
height: 60px;
|
height: 60px;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
|
z-index: 100;
|
||||||
.anticon-global {
|
.anticon-global {
|
||||||
color: ${token.colorText};
|
color: ${token.colorText};
|
||||||
}
|
}
|
||||||
@@ -38,23 +67,6 @@ const useStyles = createStyles(({ token, css }) => ({
|
|||||||
`
|
`
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const Wrapper = styled.div<{ $isDarkTheme: boolean }>`
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
bottom: 0;
|
|
||||||
right: 0;
|
|
||||||
min-height: 100vh;
|
|
||||||
z-index: -1;
|
|
||||||
background: ${({ $isDarkTheme }) =>
|
|
||||||
$isDarkTheme
|
|
||||||
? `radial-gradient(at 50% 20%, #383838 0%, #292929 40%, #000 100%)`
|
|
||||||
: `url(${Bg2}) center center no-repeat`};
|
|
||||||
background-size: ${({ $isDarkTheme }) =>
|
|
||||||
$isDarkTheme ? 'contain' : 'cover'};
|
|
||||||
opacity: ${({ $isDarkTheme }) => ($isDarkTheme ? 1 : 0.6)};
|
|
||||||
`;
|
|
||||||
|
|
||||||
const Box = styled.div`
|
const Box = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -85,22 +97,43 @@ const FormWrapper = styled.div`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const decryptInitialPassword = (password: string) => {
|
||||||
|
const bytes = CryptoJS.AES?.decrypt?.(password, CRYPT_TEXT);
|
||||||
|
return bytes.toString(CryptoJS.enc.Utf8);
|
||||||
|
};
|
||||||
|
|
||||||
const Login = () => {
|
const Login = () => {
|
||||||
const { styles } = useStyles();
|
const { styles } = useStyles();
|
||||||
|
const intl = useIntl();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const userSettingsStorage = useUserSettingsStorage();
|
||||||
const { themeData, userSettings, isDarkTheme } = useUserSettings();
|
const { themeData, userSettings, isDarkTheme } = useUserSettings();
|
||||||
const [userInfo, setUserInfo] = useAtom(userAtom);
|
const [userInfo, setUserInfo] = useAtom(userAtom);
|
||||||
|
const [initialPassword, setInitialPassword] = useAtom(initialPasswordAtom);
|
||||||
const { initialState, setInitialState } = useModel('@@initialState') || {};
|
const { initialState, setInitialState } = useModel('@@initialState') || {};
|
||||||
|
|
||||||
const gotoDefaultPage = async (userInfo: any) => {
|
const enterprisePlugin = getGPUStackPlugin();
|
||||||
if (!userInfo || userInfo?.require_password_change) {
|
|
||||||
|
const CustomLoginComponent = enterprisePlugin?.login?.CustomLoginComponent;
|
||||||
|
const shouldUseCustomLogin = enterprisePlugin?.login?.shouldUseCustomLogin;
|
||||||
|
|
||||||
|
const useCustomLogin = useMemo(
|
||||||
|
() => shouldUseCustomLogin?.(userSettings),
|
||||||
|
[shouldUseCustomLogin, userSettings]
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log('useCustomLogin', useCustomLogin, userSettings);
|
||||||
|
|
||||||
|
const gotoDefaultPage = async (info: any) => {
|
||||||
|
if (!info || info?.require_password_change) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
checkDefaultPage(userInfo, true);
|
checkDefaultPage(info, true);
|
||||||
if (!initialState?.currentUser) {
|
if (!initialState?.currentUser) {
|
||||||
setInitialState((preState: any) => ({
|
setInitialState((preState: any) => ({
|
||||||
...preState,
|
...preState,
|
||||||
currentUser: userInfo
|
currentUser: info
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -109,39 +142,115 @@ const Login = () => {
|
|||||||
gotoDefaultPage(userInfo);
|
gotoDefaultPage(userInfo);
|
||||||
}, [userInfo]);
|
}, [userInfo]);
|
||||||
|
|
||||||
useEffect(() => {
|
const fetchUserInfo = useCallback(async () => {
|
||||||
document.title = 'GPUStack';
|
const info = await initialState?.fetchUserInfo?.();
|
||||||
|
if (info) {
|
||||||
|
flushSync(() => {
|
||||||
|
setInitialState((s: any) => ({
|
||||||
|
...s,
|
||||||
|
currentUser: info
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return info;
|
||||||
|
}, [initialState, setInitialState]);
|
||||||
|
|
||||||
|
const onPasswordChanged = useCallback(() => {
|
||||||
|
resetStorageUserSettings();
|
||||||
|
writeState(IS_FIRST_LOGIN, null);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const kit = useMemo(
|
||||||
|
() => ({
|
||||||
|
useLocalAuth,
|
||||||
|
useSSOAuth,
|
||||||
|
userInfo,
|
||||||
|
setUserInfo,
|
||||||
|
fetchUserInfo,
|
||||||
|
checkDefaultPage,
|
||||||
|
initialPassword,
|
||||||
|
setInitialPassword,
|
||||||
|
decryptInitialPassword,
|
||||||
|
updatePassword: updatePasswordApi,
|
||||||
|
passwordReg: PasswordReg,
|
||||||
|
onPasswordChanged,
|
||||||
|
resetPasswordUrl: externalLinks.resetPassword,
|
||||||
|
formLogoUrl: LogoIcon
|
||||||
|
}),
|
||||||
|
[
|
||||||
|
userInfo,
|
||||||
|
setUserInfo,
|
||||||
|
fetchUserInfo,
|
||||||
|
initialPassword,
|
||||||
|
setInitialPassword,
|
||||||
|
onPasswordChanged
|
||||||
|
]
|
||||||
|
);
|
||||||
return (
|
return (
|
||||||
<ConfigProvider
|
<ConfigProvider
|
||||||
componentSize="large"
|
componentSize="large"
|
||||||
key={userSettings.colorPrimary}
|
|
||||||
theme={{
|
theme={{
|
||||||
algorithm: userSettings.isDarkTheme
|
algorithm: userSettings.isDarkTheme
|
||||||
? theme.darkAlgorithm
|
? theme.darkAlgorithm
|
||||||
: theme.defaultAlgorithm,
|
: theme.defaultAlgorithm,
|
||||||
...themeData
|
...themeData,
|
||||||
|
token: {
|
||||||
|
...themeData?.token,
|
||||||
|
colorPrimary:
|
||||||
|
enterprisePlugin?.getPrimaryColor?.(userSettings)?.colorPrimary ||
|
||||||
|
COLOR_PRIMARY
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className={styles.header}>
|
<CoreUIProvider
|
||||||
<ThemeDropActions></ThemeDropActions>
|
config={{
|
||||||
<LangSelect />
|
apiBaseUrl: GPUSTACK_API_BASE_URL,
|
||||||
</div>
|
theme: userSettings.theme,
|
||||||
<DarkMask></DarkMask>
|
isDarkTheme: userSettings.isDarkTheme,
|
||||||
<Wrapper $isDarkTheme={isDarkTheme}></Wrapper>
|
defaultColorPrimary: COLOR_PRIMARY
|
||||||
<Box>
|
}}
|
||||||
<div className={styles.formContainer}>
|
hooks={{
|
||||||
<FormWrapper>
|
useUserSettings: useUserSettings as any,
|
||||||
{userInfo?.require_password_change ? (
|
useUserSettingsStorage: () => userSettingsStorage,
|
||||||
<PasswordForm />
|
useIntl: useIntl
|
||||||
) : (
|
}}
|
||||||
<LoginForm />
|
i18n={intl}
|
||||||
)}
|
locale={{
|
||||||
</FormWrapper>
|
getAllLocales: getAllLocales,
|
||||||
|
setLocale: setLocale
|
||||||
|
}}
|
||||||
|
services={{
|
||||||
|
request: request,
|
||||||
|
router: {
|
||||||
|
push: (path: string) => navigate(path),
|
||||||
|
replace: (path: string) => navigate(path, { replace: true }),
|
||||||
|
goBack: () => navigate(-1)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className={styles.header}>
|
||||||
|
<ThemeDropActions></ThemeDropActions>
|
||||||
|
<LangSelect />
|
||||||
</div>
|
</div>
|
||||||
<Footer />
|
<DarkMask></DarkMask>
|
||||||
</Box>
|
<Background isDarkTheme={isDarkTheme} />
|
||||||
|
{useCustomLogin && CustomLoginComponent ? (
|
||||||
|
<CustomLoginComponent kit={kit} />
|
||||||
|
) : (
|
||||||
|
<Box>
|
||||||
|
<div className={styles.formContainer}>
|
||||||
|
<FormWrapper>
|
||||||
|
{userInfo?.require_password_change ? (
|
||||||
|
<PasswordForm />
|
||||||
|
) : (
|
||||||
|
<LoginForm />
|
||||||
|
)}
|
||||||
|
</FormWrapper>
|
||||||
|
</div>
|
||||||
|
<Footer />
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</CoreUIProvider>
|
||||||
</ConfigProvider>
|
</ConfigProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
// Cross-module signaling for enterprise plugin registration.
|
||||||
|
//
|
||||||
|
// The enterprise plugin is loaded asynchronously by the block injected into
|
||||||
|
// global.tsx during enterprise builds. getInitialState() must wait for that
|
||||||
|
// registration before calling GPUStackPluginManager.initialize, otherwise
|
||||||
|
// initialize runs with zero plugins.
|
||||||
|
//
|
||||||
|
// In open-source builds the block isn't injected, so the promise resolves
|
||||||
|
// immediately and getInitialState proceeds without delay.
|
||||||
|
|
||||||
|
const isEnterpriseMode = process.env.ENABLE_ENTERPRISE === 'true';
|
||||||
|
|
||||||
|
let resolveReady: () => void = () => {};
|
||||||
|
|
||||||
|
export const enterprisePluginReady: Promise<void> = new Promise<void>(
|
||||||
|
(resolve) => {
|
||||||
|
if (!isEnterpriseMode) {
|
||||||
|
resolve();
|
||||||
|
} else {
|
||||||
|
resolveReady = resolve;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const markEnterprisePluginReady = (): void => {
|
||||||
|
resolveReady();
|
||||||
|
};
|
||||||
@@ -26,7 +26,7 @@ export function mergeEnterpriseLocales(locales: Record<string, any> = {}) {
|
|||||||
antd: locale
|
antd: locale
|
||||||
});
|
});
|
||||||
mergedLocales.add(locale);
|
mergedLocales.add(locale);
|
||||||
console.log(`✓ Merged enterprise locale: ${locale}`);
|
console.log(`✓ Merged enterprise locale: ${locale}`, messages);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Failed to merge enterprise locale ${locale}:`, error);
|
console.error(`Failed to merge enterprise locale ${locale}:`, error);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { AppPlugin, IPluginManager } from './types';
|
import { AppPlugin, AppPluginContext, IPluginManager } from './types';
|
||||||
|
|
||||||
class PluginManager implements IPluginManager {
|
class PluginManager implements IPluginManager {
|
||||||
private plugins = new Map<string, AppPlugin>();
|
private plugins = new Map<string, AppPlugin>();
|
||||||
@@ -38,7 +38,7 @@ class PluginManager implements IPluginManager {
|
|||||||
/**
|
/**
|
||||||
* initialize all plugins
|
* initialize all plugins
|
||||||
*/
|
*/
|
||||||
async initialize(): Promise<Record<string, any>> {
|
async initialize(context: AppPluginContext): Promise<Record<string, any>> {
|
||||||
if (this.initialized) {
|
if (this.initialized) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
@@ -46,9 +46,10 @@ class PluginManager implements IPluginManager {
|
|||||||
const initData: Record<string, any> = {};
|
const initData: Record<string, any> = {};
|
||||||
|
|
||||||
for (const [name, plugin] of this.plugins.entries()) {
|
for (const [name, plugin] of this.plugins.entries()) {
|
||||||
|
console.log(`Initializing plugin "${name}"...`);
|
||||||
if (plugin.onAppInit) {
|
if (plugin.onAppInit) {
|
||||||
try {
|
try {
|
||||||
const data = await plugin.onAppInit();
|
const data = await plugin.onAppInit(context);
|
||||||
if (data) {
|
if (data) {
|
||||||
initData[name] = data;
|
initData[name] = data;
|
||||||
}
|
}
|
||||||
@@ -63,7 +64,7 @@ class PluginManager implements IPluginManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 通知所有插件应用已就绪
|
* notify all plugins that the app is ready
|
||||||
*/
|
*/
|
||||||
async notifyReady(): Promise<void> {
|
async notifyReady(): Promise<void> {
|
||||||
for (const [name, plugin] of this.plugins.entries()) {
|
for (const [name, plugin] of this.plugins.entries()) {
|
||||||
|
|||||||
+56
-3
@@ -10,12 +10,52 @@ export interface RouteConfig {
|
|||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* dependency kit passed from host to plugin's custom login component
|
||||||
|
*/
|
||||||
|
export interface LoginKit {
|
||||||
|
useLocalAuth: (opts: any) => {
|
||||||
|
handleLogin: (values: any) => void;
|
||||||
|
submitLoading: boolean;
|
||||||
|
};
|
||||||
|
useSSOAuth: (opts: any) => {
|
||||||
|
options: {
|
||||||
|
saml: boolean;
|
||||||
|
oidc: boolean;
|
||||||
|
first_time_setup: boolean;
|
||||||
|
get_initial_password_command: string;
|
||||||
|
};
|
||||||
|
loginWithOIDC: () => void;
|
||||||
|
loginWithSAML: () => void;
|
||||||
|
};
|
||||||
|
userInfo: any;
|
||||||
|
setUserInfo: (info: any) => void;
|
||||||
|
fetchUserInfo: () => Promise<any>;
|
||||||
|
checkDefaultPage: (userInfo: any, replace: boolean) => Promise<void>;
|
||||||
|
initialPassword: string;
|
||||||
|
setInitialPassword: (val: string) => void;
|
||||||
|
decryptInitialPassword: (encrypted: string) => string;
|
||||||
|
updatePassword: (data: any) => Promise<any>;
|
||||||
|
passwordReg: RegExp;
|
||||||
|
onPasswordChanged: () => void;
|
||||||
|
resetPasswordUrl: string;
|
||||||
|
formLogoUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* login module configuration
|
* login module configuration
|
||||||
*/
|
*/
|
||||||
export interface LoginPlugin {
|
export interface LoginPlugin {
|
||||||
shouldUseCustomLogin?: () => boolean;
|
shouldUseCustomLogin?: (enterpriseSettings: any) => boolean;
|
||||||
CustomLoginComponent?: ComponentType;
|
CustomLoginComponent?: ComponentType<{ kit: LoginKit }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* resolved logo URLs for sidebar and collapsed-state mini icon
|
||||||
|
*/
|
||||||
|
export interface LogoSet {
|
||||||
|
sidebarLogo?: string;
|
||||||
|
miniLogo?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -23,6 +63,7 @@ export interface LoginPlugin {
|
|||||||
*/
|
*/
|
||||||
export interface BrandingPlugin {
|
export interface BrandingPlugin {
|
||||||
ConfigPage?: ComponentType;
|
ConfigPage?: ComponentType;
|
||||||
|
resolveLogos?: (userSettings: any, isDarkTheme: boolean) => LogoSet;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -32,6 +73,16 @@ export interface LocalesConfig {
|
|||||||
[locale: string]: Record<string, any>;
|
[locale: string]: Record<string, any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* runtime context passed to plugin lifecycle hooks
|
||||||
|
*/
|
||||||
|
export interface AppPluginContext {
|
||||||
|
request: <T = any>(url: string, options?: Record<string, any>) => Promise<T>;
|
||||||
|
setUserSettings?: (value: Record<string, any>) => void;
|
||||||
|
setStorageUserSettings?: (value: Record<string, any>) => void;
|
||||||
|
defaultColorPrimary?: string;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* application plugin interface
|
* application plugin interface
|
||||||
*/
|
*/
|
||||||
@@ -40,7 +91,9 @@ export interface AppPlugin {
|
|||||||
* application initialization hook
|
* application initialization hook
|
||||||
* called when the application starts, can return initialization data
|
* called when the application starts, can return initialization data
|
||||||
*/
|
*/
|
||||||
onAppInit?: () => Promise<Record<string, any>> | Record<string, any>;
|
onAppInit?: (
|
||||||
|
context: AppPluginContext
|
||||||
|
) => Promise<Record<string, any>> | Record<string, any>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* application ready hook
|
* application ready hook
|
||||||
|
|||||||
Reference in New Issue
Block a user