import { PageActionType } from '@/config/types'; import { RouteItem } from '@/pages/model-routes/config/types'; import { getGPUStackPlugin } from '@/plugins'; import { AlertBlockInfo, FormDrawer, ModalFooter } from '@gpustack/core-ui'; import { useIntl } from '@umijs/max'; import { message } from 'antd'; import _ from 'lodash'; import { useEffect, useRef, useState } from 'react'; import { updateModelAccessUser } from '../../apis'; import { AccessControlFormData } from '../../config/types'; import AccessControlForm, { ALLOWED_PRINCIPALS_POLICY } from './form'; const AccessControlModal: React.FC< Global.ScrollerModalProps > = ({ 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(); }; const handleOnFinish = async (values: AccessControlFormData) => { try { // Everything saves through the single `/access` POST as // `principals` (one request, one success toast): // * principal-based override active → its staged full grant set. // * user picker → selected users mapped to USER-kind grants, // plus any non-user grants preserved from the GET snapshot // (so a user-only UI never wipes org/group grants it can't // see — though in practice the picker only runs where none // exist). // * otherwise (authed/public) → send neither, so the server // leaves existing grants untouched. const override = getGPUStackPlugin()?.accessControl?.allowedUsersOverride; const usesPrincipals = !!override && values.access_policy === override.policyValue; const usesUserList = !override && values.access_policy === ALLOWED_PRINCIPALS_POLICY; // Guard the load race: on an existing route, `principals` is // undefined until the GET seeds it. Saving in that window would // submit an empty grant set and wipe existing grants — bail out // (the modal stays open; the user can retry once loaded). if ( currentData?.id && (usesPrincipals || usesUserList) && !values.principals ) { return; } let data: AccessControlFormData; if (usesPrincipals) { data = { access_policy: values.access_policy, principals: values.principals || [] }; } else if (usesUserList) { const preserved = (values.principals || []).filter( (p) => p.principal_type !== 'user' ); const userGrants = (values.users || []).map((u) => ({ principal_type: 'user', principal_id: u.id })); data = { access_policy: values.access_policy, principals: [...preserved, ...userGrants] }; } else { data = { access_policy: values.access_policy }; } await updateModelAccessUser({ id: currentData?.id as number, data: data }); message.success(intl.formatMessage({ id: 'common.message.success' })); onCancel?.(); } catch (error) { // do nothing, error message handled in request layer } }; const customEqual = (objValue: any, othValue: any) => { if (_.isArray(objValue) && _.isArray(othValue)) { const objIds = objValue.map((item: any) => item.id).sort(); const othIds = othValue.map((item: any) => item.id).sort(); return _.isEqual(objIds, othIds); } return undefined; }; const handleOnValuesChange = async (changedValues: any, allValues: any) => { const initialValues = formCacheRef.current; if (_.isEqualWith(initialValues, allValues, customEqual)) { 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 && ( )} } > ); }; export default AccessControlModal;