fix: custom logos

This commit is contained in:
jialin
2026-04-27 21:10:34 +08:00
committed by jialin
parent 519020077a
commit e4992e8313
20 changed files with 12081 additions and 3320 deletions
+1
View File
@@ -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
View File
@@ -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",
+11741 -3223
View File
File diff suppressed because it is too large Load Diff
+14 -4
View File
@@ -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);
} }
+19
View File
@@ -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 };
+8 -4
View File
@@ -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 };
+2
View File
@@ -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
View File
@@ -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
+5 -1
View File
@@ -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}.'
}; };
+5 -1
View File
@@ -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) ==========
+5 -1
View File
@@ -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) ==========
+5 -1
View File
@@ -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}.'
}; };
+5 -1
View File
@@ -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}。'
}; };
+25
View File
@@ -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
View File
@@ -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>
); );
}; };
+27
View File
@@ -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();
};
+1 -1
View File
@@ -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);
} }
+5 -4
View File
@@ -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
View File
@@ -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