157 lines
5.2 KiB
TypeScript
157 lines
5.2 KiB
TypeScript
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<RouteItem, AccessControlFormData>
|
|
> = ({ open, title, currentData, action, onCancel }) => {
|
|
const intl = useIntl();
|
|
const form = useRef<any>(null);
|
|
const [isChanged, setIsChanged] = useState(false);
|
|
const formCacheRef = useRef<AccessControlFormData | null>(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 (
|
|
<FormDrawer
|
|
title={title}
|
|
open={open}
|
|
onCancel={onCancel}
|
|
width={700}
|
|
footer={
|
|
<div style={{ paddingInline: 24, paddingBottom: 8 }}>
|
|
{isChanged && (
|
|
<AlertBlockInfo
|
|
type="warning"
|
|
style={{ marginBottom: 16 }}
|
|
contentStyle={{
|
|
paddingInline: 0
|
|
}}
|
|
message={intl.formatMessage({
|
|
id: 'models.button.accessSettings.tips'
|
|
})}
|
|
></AlertBlockInfo>
|
|
)}
|
|
<ModalFooter onOk={handleSumit} onCancel={onCancel}></ModalFooter>
|
|
</div>
|
|
}
|
|
>
|
|
<AccessControlForm
|
|
ref={form}
|
|
currentData={currentData}
|
|
action={action as PageActionType}
|
|
onFinish={handleOnFinish}
|
|
onValuesChange={handleOnValuesChange}
|
|
/>
|
|
</FormDrawer>
|
|
);
|
|
};
|
|
|
|
export default AccessControlModal;
|