diff --git a/src/layouts/extraRender.tsx b/src/layouts/extraRender.tsx index 9f6dad01..ce7c6ffa 100644 --- a/src/layouts/extraRender.tsx +++ b/src/layouts/extraRender.tsx @@ -98,7 +98,8 @@ const CustomItem = styled.div` background-color: var(--ant-color-bg-elevated); `; -export const ExtraContent = () => { +export const ExtraContent = (props: { isDarkTheme?: boolean }) => { + const { isDarkTheme } = props; const { saveScrollHeight, restoreScrollHeight } = useBodyScroll(); const [modal, contextHolder] = Modal.useModal(); const [version] = useAtom(GPUStackVersionAtom); @@ -130,6 +131,16 @@ export const ExtraContent = () => { initialState?.currentUser?.is_admin ]); + const avatarStyle = useMemo(() => { + if (isDarkTheme) { + return { + color: 'var(--ant-color-text)', + border: 'none' + }; + } + return {}; + }, [isDarkTheme]); + const showVersion = () => { saveScrollHeight(); modal.info({ @@ -260,6 +271,7 @@ export const ExtraContent = () => { @@ -309,6 +321,7 @@ export const ExtraContent = () => { } /> diff --git a/src/layouts/index.tsx b/src/layouts/index.tsx index da931c6e..40e61288 100644 --- a/src/layouts/index.tsx +++ b/src/layouts/index.tsx @@ -516,7 +516,12 @@ export default (props: any) => { onCollapse={onCollapse} onMenuHeaderClick={onMenuHeaderClick} menuHeaderRender={renderMenuHeader} - extra={[]} + extra={[ + + ]} collapsed={userSettings.collapsed} onPageChange={onPageChange} formatMessage={formatMessage} diff --git a/src/locales/en-US/models.ts b/src/locales/en-US/models.ts index cc803ebe..63242ceb 100644 --- a/src/locales/en-US/models.ts +++ b/src/locales/en-US/models.ts @@ -178,6 +178,10 @@ export default { 'models.table.accessScope': 'Access Scope', 'models.table.accessScope.all': 'All users', 'models.table.userSelection': 'User Selection', + 'models.button.accessSettings.tips': + 'Changes to access settings take effect after one minute.', + 'models.table.userSelection.tips': + 'Administrators always have access to all models.', 'models.table.filterByName': 'Filter by username', 'models.table.admin': 'Admin', 'models.table.noselected': 'No users selected', diff --git a/src/locales/en-US/playground.ts b/src/locales/en-US/playground.ts index 71b1ccd7..8e81c42a 100644 --- a/src/locales/en-US/playground.ts +++ b/src/locales/en-US/playground.ts @@ -90,7 +90,7 @@ export default { 'playground.audio.stoprecord': 'Stop Recording', 'playground.audio.generating.tips': 'Generated text will appear here.', 'playground.audio.uploadfile.tips': - 'Upload an audio file, supported formats: {formats}', + 'Upload an audio file, supported formats: {formats}.', 'playground.input.multiplePaste': 'Batch Input Mode', 'playground.input.multiplePaste.tips': 'When enabled, pasted multi-line text will be automatically split by newline into separate entries in the form.', diff --git a/src/locales/ja-JP/models.ts b/src/locales/ja-JP/models.ts index b1ff4357..0a64cab9 100644 --- a/src/locales/ja-JP/models.ts +++ b/src/locales/ja-JP/models.ts @@ -179,6 +179,10 @@ export default { 'models.table.accessScope': 'Access Scope', 'models.table.accessScope.all': 'All users', 'models.table.userSelection': 'User Selection', + 'models.button.accessSettings.tips': + 'Changes to access settings take effect after one minute.', + 'models.table.userSelection.tips': + 'Administrators always have access to all models.', 'models.table.filterByName': 'Filter by username', 'models.table.admin': 'Admin', 'models.table.noselected': 'No users selected', @@ -338,4 +342,6 @@ export default { // 60. models.form.backend.mindie': 'Only supports Ascend NPUs.', // 61. 'models.form.backend.sglang': 'Built-in support for NVIDIA/AMD GPUs and Ascend NPUs.', // 62. 'models.form.backend_parameters.vllm.tips': 'For more details about {backend} parameters, see here.', +// 63. 'models.button.accessSettings.tips': 'Changes to access settings take effect after one minute.', +// 64. 'models.table.userSelection.tips': 'Administrators always have access to all models.', // ========== End of To-Do List ========== diff --git a/src/locales/ru-RU/models.ts b/src/locales/ru-RU/models.ts index 3b7554f0..9561b05a 100644 --- a/src/locales/ru-RU/models.ts +++ b/src/locales/ru-RU/models.ts @@ -178,6 +178,10 @@ export default { 'models.table.accessScope': 'Область доступа', 'models.table.accessScope.all': 'Все пользователи', 'models.table.userSelection': 'Выбор пользователей', + 'models.button.accessSettings.tips': + 'Changes to access settings take effect after one minute.', + 'models.table.userSelection.tips': + 'Administrators always have access to all models.', 'models.table.filterByName': 'Фильтр по имени пользователя', 'models.table.admin': 'Администратор', 'models.table.noselected': 'Пользователи не выбраны', @@ -266,4 +270,6 @@ export default { // 6. 'models.form.backend.sglang': 'Built-in support for NVIDIA/AMD GPUs and Ascend NPUs.', // 7. 'models.form.gpusAllocationType.auto.tips': 'The system automatically calculates the GPU count per replica, using powers of two by default and capped by the selected GPUs.', // 8. 'models.form.backend_parameters.vllm.tips': 'For more details about {backend} parameters, see here.', +// 9. 'models.button.accessSettings.tips': 'Changes to access settings take effect after one minute.', +// 10. 'models.table.userSelection.tips': 'Administrators always have access to all models.', // ========== End of To-Do List ========== diff --git a/src/locales/zh-CN/models.ts b/src/locales/zh-CN/models.ts index 0ae12fa7..86a5445e 100644 --- a/src/locales/zh-CN/models.ts +++ b/src/locales/zh-CN/models.ts @@ -168,6 +168,8 @@ export default { 'models.table.accessScope': '访问范围', 'models.table.accessScope.all': '所有用户', 'models.table.userSelection': '用户选择', + 'models.button.accessSettings.tips': '访问设置的更改将在一分钟后生效。', + 'models.table.userSelection.tips': '管理员始终拥有对所有模型的访问权限。', 'models.table.filterByName': '按用户名筛选', 'models.table.admin': '管理员', 'models.table.noselected': '未选择用户', diff --git a/src/pages/api-keys/components/add-apikey-modal/allow-models.tsx b/src/pages/api-keys/components/add-apikey-modal/allow-models.tsx index ee6fe365..5869f2c6 100644 --- a/src/pages/api-keys/components/add-apikey-modal/allow-models.tsx +++ b/src/pages/api-keys/components/add-apikey-modal/allow-models.tsx @@ -19,7 +19,8 @@ const Label = styled.div` const AllowModelsForm: React.FC<{ currentData?: Partial | null; action: PageActionType; -}> = ({ currentData, action }) => { + onValuesChange?: (changedValues: any, allValues: any) => void; +}> = ({ currentData, action, onValuesChange }) => { const intl = useIntl(); const { getRuleMessage } = useAppUtils(); const form = Form.useFormInstance(); @@ -102,6 +103,10 @@ const AllowModelsForm: React.FC<{ })} onSelectChange={(selectedKeys) => { form.setFieldsValue({ allowed_model_names: selectedKeys }); + onValuesChange?.( + { allowed_model_names: selectedKeys }, + form.getFieldsValue() + ); }} /> diff --git a/src/pages/api-keys/components/add-apikey-modal/form.tsx b/src/pages/api-keys/components/add-apikey-modal/form.tsx index f9e3cea2..69785114 100644 --- a/src/pages/api-keys/components/add-apikey-modal/form.tsx +++ b/src/pages/api-keys/components/add-apikey-modal/form.tsx @@ -12,7 +12,8 @@ import AllowModelsForm from './allow-models'; const APIKeyForm: React.FC<{ action: PageActionType; currentData?: Partial | null; -}> = ({ action, currentData }) => { + onValuesChange?: (changedValues: any, allValues: any) => void; +}> = ({ action, currentData, onValuesChange }) => { const intl = useIntl(); return ( @@ -70,6 +71,7 @@ const APIKeyForm: React.FC<{ ); diff --git a/src/pages/api-keys/components/add-apikey-modal/index.tsx b/src/pages/api-keys/components/add-apikey-modal/index.tsx index 02abb04e..7b55dd52 100644 --- a/src/pages/api-keys/components/add-apikey-modal/index.tsx +++ b/src/pages/api-keys/components/add-apikey-modal/index.tsx @@ -1,3 +1,4 @@ +import AlertBlockInfo from '@/components/alert-info/block'; import CopyButton from '@/components/copy-button'; import ModalFooter from '@/components/modal-footer'; import GSDrawer from '@/components/scroller-modal/gs-drawer'; @@ -9,7 +10,7 @@ import { useIntl } from '@umijs/max'; import { Form, Tag } from 'antd'; import dayjs from 'dayjs'; import _ from 'lodash'; -import { useEffect, useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { createApisKey, updateApisKey } from '../../apis'; import { expirationOptions } from '../../config'; import { FormData, ListItem } from '../../config/types'; @@ -37,6 +38,11 @@ const AddModal: React.FC = ({ const [showKey, setShowKey] = useState(false); const [apikeyValue, setAPIKeyValue] = useState(''); const [loading, setLoading] = useState(false); + const [isChanged, setIsChanged] = useState(false); + const cacheFormRef = useRef<{ + allowed_type: string; + allowed_model_names: string[]; + }>({} as any); const getExpireValue = (val: number | null) => { const expires_in = val; @@ -128,6 +134,21 @@ const AddModal: React.FC = ({ setShowKey(false); }; + const handleOnValuesChange = async (changedValues: any, allValues: any) => { + const initialValues = cacheFormRef.current; + await new Promise((resolve) => { + setTimeout(resolve, 100); + }); + console.log('initialValues', initialValues, allValues); + if ( + _.isEqual(initialValues, _.pick(allValues, Object.keys(initialValues))) + ) { + setIsChanged(false); + } else { + setIsChanged(true); + } + }; + const initValues = () => { if (action === PageAction.CREATE && open) { form.setFieldsValue({ @@ -146,10 +167,19 @@ const AddModal: React.FC = ({ allowed_model_names: currentData.allowed_model_names || [] }); } + + cacheFormRef.current = { + allowed_type: form.getFieldValue('allowed_type'), + allowed_model_names: form.getFieldValue('allowed_model_names') + }; }; useEffect(() => { initValues(); + if (!open) { + setIsChanged(false); + cacheFormRef.current = {} as any; + } }, [open]); return ( @@ -183,29 +213,45 @@ const AddModal: React.FC = ({ }} footer={ !showKey ? ( - + <> + {isChanged && ( +
+ +
+ )} + + ) : ( - + <> + + ) } > @@ -213,10 +259,15 @@ const AddModal: React.FC = ({ name="addAPIKey" form={form} onFinish={handleOnOk} + onValuesChange={handleOnValuesChange} preserve={false} > {!showKey && ( - + )} {showKey && action === PageAction.CREATE && ( diff --git a/src/pages/cluster-management/cluster-create.tsx b/src/pages/cluster-management/cluster-create.tsx index d074aa5c..75d7872b 100644 --- a/src/pages/cluster-management/cluster-create.tsx +++ b/src/pages/cluster-management/cluster-create.tsx @@ -243,9 +243,6 @@ const ClusterCreate = () => { const info = await queryClusterToken({ id: res.id }); setRegistrationInfo({ ...info, - image: info.container_registry - ? `${info.container_registry}/${info.image}` - : info.image, cluster_id: res.id }); return true; diff --git a/src/pages/cluster-management/components/add-worker/index.tsx b/src/pages/cluster-management/components/add-worker/index.tsx index 940446d4..8432aee8 100644 --- a/src/pages/cluster-management/components/add-worker/index.tsx +++ b/src/pages/cluster-management/components/add-worker/index.tsx @@ -68,9 +68,6 @@ const AddWorker: React.FC = (props) => { firstLoad.current = false; setRegistrationInfo({ ...data, - image: data.container_registry - ? `${data.container_registry}/${data.image}` - : data.image, cluster_id: value }); } catch (error) { diff --git a/src/pages/llmodels/components/access-control-modal/form.tsx b/src/pages/llmodels/components/access-control-modal/form.tsx index d98eaddd..ac9ef96d 100644 --- a/src/pages/llmodels/components/access-control-modal/form.tsx +++ b/src/pages/llmodels/components/access-control-modal/form.tsx @@ -3,14 +3,24 @@ import { PageAction } from '@/config'; import { PageActionType } from '@/config/types'; import TransferInner from '@/pages/_components/transfer'; import { queryUsersList } from '@/pages/users/apis'; -import { DownOutlined } from '@ant-design/icons'; +import { DownOutlined, QuestionCircleOutlined } from '@ant-design/icons'; import { useIntl } from '@umijs/max'; -import { Checkbox, Dropdown, DropdownProps, Empty, Form, Radio } from 'antd'; +import { + Checkbox, + Dropdown, + DropdownProps, + Empty, + Form, + Radio, + RadioChangeEvent, + Tooltip +} from 'antd'; import { forwardRef, useEffect, useImperativeHandle, useMemo, + useRef, useState } from 'react'; import styled from 'styled-components'; @@ -30,10 +40,11 @@ interface AccessControlFormProps { action: PageActionType; currentData?: ListItem | null; onFinish: (values: AccessControlFormData) => void; + onValuesChange?: (changedValues: any, allValues: any) => void; } const AccessControlForm = forwardRef((props: AccessControlFormProps, ref) => { - const { action, currentData, onFinish } = props; + const { action, currentData, onFinish, onValuesChange } = props; const intl = useIntl(); const [form] = Form.useForm(); const accessPolicy = Form.useWatch('access_policy', form); @@ -47,6 +58,7 @@ const AccessControlForm = forwardRef((props: AccessControlFormProps, ref) => { const [queryParams, setQueryParams] = useState({ page: -1 }); + const formDataCacheRef = useRef(null); const dataList = useMemo(() => { if (filterInUsers.size === 0) { @@ -85,7 +97,7 @@ const AccessControlForm = forwardRef((props: AccessControlFormProps, ref) => { } }; - const handleOnChange = ( + const handleOnChange = async ( nextTargetKeys: TransferKey[], direction: string, removeKeys: TransferKey[] @@ -93,6 +105,10 @@ const AccessControlForm = forwardRef((props: AccessControlFormProps, ref) => { setTargetKeys(nextTargetKeys); const users = nextTargetKeys.map((key) => ({ id: key })); form.setFieldsValue({ users }); + await new Promise((resolve) => { + setTimeout(resolve, 100); + }); + onValuesChange?.({ users }, form.getFieldsValue()); }; const onSearch = (dir: 'left' | 'right', value: string) => { @@ -115,6 +131,23 @@ const AccessControlForm = forwardRef((props: AccessControlFormProps, ref) => { } }; + const handleOnPolicyChange = async (e: RadioChangeEvent) => { + console.log('policy changed:', e.target.value); + const policy = e.target.value; + if (policy === 'allowed_users') { + form.setFieldsValue({ users: formDataCacheRef.current?.users || [] }); + } else { + formDataCacheRef.current = { + access_policy: policy, + users: form.getFieldValue('users') || [] + }; + } + await new Promise((resolve) => { + setTimeout(resolve, 100); + }); + onValuesChange?.({ access_policy: policy }, form.getFieldsValue()); + }; + useImperativeHandle(ref, () => ({ submit: () => { form.submit(); @@ -222,16 +255,18 @@ const AccessControlForm = forwardRef((props: AccessControlFormProps, ref) => {
name="access_policy" noStyle> { <> name="users"> = ({ open, title, currentData, action, onCancel }) => { const intl = useIntl(); const form = useRef(null); + const [isChanged, setIsChanged] = useState(false); + const formCacheRef = useRef(null); const handleSumit = () => { form.current?.submit(); @@ -36,6 +40,32 @@ const AccessControlModal: React.FC< } }; + const handleOnValuesChange = async (changedValues: any, allValues: any) => { + console.log('changedValues', changedValues, allValues); + const initialValues = formCacheRef.current; + if (_.isEqual(initialValues, allValues)) { + setIsChanged(false); + } else { + setIsChanged(true); + } + }; + + useEffect(() => { + if (open) { + setIsChanged(false); + setTimeout(() => { + const initialValues = form.current?.getFieldsValue(); + formCacheRef.current = { + access_policy: initialValues.access_policy, + users: initialValues.users || [] + }; + }, 200); + } else { + formCacheRef.current = null; + setIsChanged(false); + } + }, [open, currentData]); + return ( + <> + {isChanged && ( + + )} + + } > ); diff --git a/src/pages/llmodels/forms/schedule-type.tsx b/src/pages/llmodels/forms/schedule-type.tsx index 79456e02..f22dc6c3 100644 --- a/src/pages/llmodels/forms/schedule-type.tsx +++ b/src/pages/llmodels/forms/schedule-type.tsx @@ -110,10 +110,14 @@ const ScheduleTypeForm: React.FC = () => { await new Promise((resolve) => { setTimeout(resolve, 100); }); + if (value === ScheduleValueMap.Auto) { onValuesChange?.({}, form.getFieldsValue()); } else if (value === ScheduleValueMap.Manual) { - form.setFieldValue(['gpu_selector', 'gpus_per_replica'], null); + form.setFieldValue('gpu_selector', { + gpu_ids: [], + gpus_per_replica: null + }); requestAnimationFrame(() => { setContentPaddingBottom(); });