chore: model access statement

This commit is contained in:
jialin
2025-10-13 15:21:17 +08:00
parent 663b6c7b4e
commit bb7c8e9c85
18 changed files with 976 additions and 357 deletions
+1
View File
@@ -29,6 +29,7 @@ interface AutoTooltipProps extends Omit<TagProps, 'title'> {
} }
const StyledTag = styled(Tag)` const StyledTag = styled(Tag)`
margin: 0;
&.tag-filled { &.tag-filled {
border: none; border: none;
background-color: var(--ant-color-fill-secondary); background-color: var(--ant-color-fill-secondary);
@@ -0,0 +1,121 @@
import { isNotEmptyValue } from '@/utils/index';
import { useIntl } from '@umijs/max';
import type { SelectProps } from 'antd';
import { Form } from 'antd';
import { cloneDeep } from 'lodash';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import SimpleSelect from './simple-select';
import { SealFormItemProps } from './types';
import Wrapper from './wrapper';
import SelectWrapper from './wrapper/select';
const SealSelect: React.FC<
SelectProps & SealFormItemProps & { showTags?: boolean }
> = (props) => {
const {
label,
placeholder,
children,
required,
description,
options,
allowNull,
isInFormItems = true,
notFoundContent = null,
...rest
} = props;
const intl = useIntl();
const [isFocus, setIsFocus] = useState(false);
const inputRef = useRef<any>(null);
let status = '';
// the status can be controlled by Form.Item
if (isInFormItems) {
const statusData = Form?.Item?.useStatus?.();
status = statusData?.status || '';
} else {
status = props.status || '';
}
const _options = useMemo(() => {
if (!options?.length) {
return [];
}
const list = cloneDeep(options);
return list.map((item: any) => {
if (item.locale) {
item.label = intl.formatMessage({ id: item.label as string });
}
return item;
});
}, [options, intl]);
useEffect(() => {
if (
isNotEmptyValue(props.value) ||
(allowNull && (props.value === null || props.value === undefined))
) {
setIsFocus(true);
}
}, [props.value, allowNull]);
const handleClickWrapper = () => {
if (!props.disabled && !isFocus) {
inputRef.current?.focus?.();
setIsFocus(true);
}
};
const handleChange = (val: any, options: any) => {
if (isNotEmptyValue(val) || (allowNull && val === null)) {
setIsFocus(true);
} else {
setIsFocus(false);
}
props.onChange?.(val, options);
};
const handleOnFocus = (e: any) => {
setIsFocus(true);
props.onFocus?.(e);
};
const handleOnBlur = (e: any) => {
if (allowNull && props.value === null) {
setIsFocus(true);
} else if (!props.value) {
setIsFocus(false);
}
props.onBlur?.(e);
};
return (
<SelectWrapper>
<Wrapper
className="seal-select-wrapper"
status={status}
label={label}
isFocus={isFocus}
required={required}
description={description}
disabled={props.disabled}
onClick={handleClickWrapper}
>
<SimpleSelect
{...rest}
ref={inputRef}
options={children ? null : _options}
onFocus={handleOnFocus}
onBlur={handleOnBlur}
onChange={handleChange}
notFoundContent={notFoundContent}
>
{children}
</SimpleSelect>
</Wrapper>
</SelectWrapper>
);
};
export default SealSelect;
+30 -9
View File
@@ -2,7 +2,7 @@ import { useIntl } from '@umijs/max';
import type { SelectProps } from 'antd'; import type { SelectProps } from 'antd';
import { Checkbox, Tag } from 'antd'; import { Checkbox, Tag } from 'antd';
import { CheckboxChangeEvent } from 'antd/es/checkbox'; import { CheckboxChangeEvent } from 'antd/es/checkbox';
import React, { useEffect } from 'react'; import React, { forwardRef, useEffect, useImperativeHandle } from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import AutoTooltip from '../auto-tooltip'; import AutoTooltip from '../auto-tooltip';
import BaseSelect from './base/select'; import BaseSelect from './base/select';
@@ -43,9 +43,10 @@ const TagWrapper = styled(Tag)`
border-radius: 12px; border-radius: 12px;
`; `;
const SimpleSelect: React.FC<SelectProps> = (props) => { const SimpleSelect: React.FC<SelectProps & { ref?: any; showTags?: boolean }> =
forwardRef((props, ref) => {
const intl = useIntl(); const intl = useIntl();
const { options = [], ...restProps } = props; const { options = [], showTags, ...restProps } = props;
const [allSelection, setAllSelection] = React.useState<{ const [allSelection, setAllSelection] = React.useState<{
checked: boolean; checked: boolean;
@@ -56,6 +57,7 @@ const SimpleSelect: React.FC<SelectProps> = (props) => {
}); });
const [optionsList, setOptionsList] = React.useState<any[]>(options || []); const [optionsList, setOptionsList] = React.useState<any[]>(options || []);
const selectRef = React.useRef<any>(null); const selectRef = React.useRef<any>(null);
const selectorRef = React.useRef<any>(null);
useEffect(() => { useEffect(() => {
setOptionsList(options || []); setOptionsList(options || []);
@@ -65,7 +67,7 @@ const SimpleSelect: React.FC<SelectProps> = (props) => {
const { value, label } = option; const { value, label } = option;
return ( return (
<OptionWrapper> <OptionWrapper>
{restProps.value?.includes(value) ? ( {restProps.value?.includes?.(value) ? (
<Checkbox checked></Checkbox> <Checkbox checked></Checkbox>
) : ( ) : (
<Checkbox></Checkbox> <Checkbox></Checkbox>
@@ -174,6 +176,8 @@ const SimpleSelect: React.FC<SelectProps> = (props) => {
return ( return (
<TagWrapper <TagWrapper
bordered={false} bordered={false}
closable={props.closable}
onClose={props.onClose}
style={{ style={{
height: 24, height: 24,
backgroundColor: 'var(--ant-color-fill-tertiary)', backgroundColor: 'var(--ant-color-fill-tertiary)',
@@ -181,7 +185,12 @@ const SimpleSelect: React.FC<SelectProps> = (props) => {
}} }}
className="flex-center" className="flex-center"
> >
{intl.formatMessage({ id: 'common.select.count' }, { count: count })} {showTags
? label
: intl.formatMessage(
{ id: 'common.select.count' },
{ count: count }
)}
</TagWrapper> </TagWrapper>
); );
}; };
@@ -235,12 +244,22 @@ const SimpleSelect: React.FC<SelectProps> = (props) => {
}; };
}, [selectRef.current]); }, [selectRef.current]);
useImperativeHandle(ref, () => ({
focus: () => {
selectorRef.current?.focus();
},
blur: () => {
selectorRef.current?.blur();
}
}));
return ( return (
<div ref={selectRef}> <div ref={selectRef} style={{ width: 'inherit' }}>
<BaseSelect <BaseSelect
{...restProps} {...restProps}
ref={selectorRef}
options={optionsList} options={optionsList}
maxTagCount={0} maxTagCount={restProps.maxTagCount || 0}
defaultActiveFirstOption={false} defaultActiveFirstOption={false}
popupRender={dropdownRender} popupRender={dropdownRender}
optionRender={optionRender} optionRender={optionRender}
@@ -252,9 +271,11 @@ const SimpleSelect: React.FC<SelectProps> = (props) => {
onSearch={handleOnSearch} onSearch={handleOnSearch}
filterOption={filterOption} filterOption={filterOption}
onOpenChange={handleOnOpenChange} onOpenChange={handleOnOpenChange}
></BaseSelect> >
{props.children}
</BaseSelect>
</div> </div>
); );
}; });
export default SimpleSelect; export default SimpleSelect;
@@ -0,0 +1,203 @@
import { SearchOutlined } from '@ant-design/icons';
import { Button, Checkbox, Empty, Input } from 'antd';
import React, { useEffect, useMemo, useState } from 'react';
import styled from 'styled-components';
import List from './list';
import SelectedList from './selected-list';
const PanelWrapper = styled.div<{ $maxHeight?: number; $leftWidth?: number }>`
border: 1px solid var(--ant-color-border);
border-radius: var(--ant-border-radius);
overflow-y: auto;
max-height: ${({ $maxHeight }) =>
$maxHeight ? `${$maxHeight + 2}px` : 'auto'};
`;
const Left = styled.div`
padding: 0;
`;
const Right = styled.div``;
const Header = styled.div`
padding: 8px 12px 8px;
display: flex;
gap: 8px;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid var(--ant-color-split);
background-color: var(--ant-color-fill-alter);
`;
interface SelectPanelProps {
searchPlaceholder?: string;
height?: number;
leftWidth?: number;
options: Array<{ key: string; title: string }>;
selectedKeys: string[];
onSelectChange: (selectedKeys: string[]) => void;
}
const SelectPanel: React.FC<SelectPanelProps> = ({
height = 300,
leftWidth = 260,
options,
selectedKeys,
searchPlaceholder,
onSelectChange
}) => {
const [indeterminate, setIndeterminate] = React.useState(false);
const [checkAll, setCheckAll] = React.useState(false);
const [searchText, setSearchText] = useState('');
const showOptions = useMemo(() => {
return options.filter((item) =>
item.title.toLowerCase().includes(searchText.toLowerCase())
);
}, [options, searchText]);
const handleSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
setSearchText(e.target.value);
};
const handleOnUnselect = (
key: string,
newSelectedKeys: { key: string; title: string }[]
) => {
onSelectChange(newSelectedKeys.map((item) => item.key));
};
const handleCheckAllChange = (e: any) => {
const checked = e.target.checked;
setCheckAll(checked);
setIndeterminate(false);
if (checked) {
const allKeys = options
.filter((item) =>
item.title.toLowerCase().includes(searchText.toLowerCase())
)
.map((item) => item.key);
onSelectChange(Array.from(new Set([...selectedKeys, ...allKeys])));
} else {
const filteredKeys = options
.filter((item) =>
item.title.toLowerCase().includes(searchText.toLowerCase())
)
.map((item) => item.key);
const newSelectedKeys = selectedKeys.filter(
(key) => !filteredKeys.includes(key)
);
onSelectChange(newSelectedKeys);
}
};
const updateCheckStatus = (newSelectedKeys: string[]) => {
if (options.length === 0) {
setIndeterminate(false);
setCheckAll(false);
return;
}
const filteredOptions = options.filter((item) =>
item.title.toLowerCase().includes(searchText.toLowerCase())
);
const filteredKeys = filteredOptions.map((item) => item.key);
const selectedFilteredKeys = newSelectedKeys.filter((key) =>
filteredKeys.includes(key)
);
setIndeterminate(
selectedFilteredKeys.length > 0 &&
selectedFilteredKeys.length < filteredKeys.length
);
setCheckAll(selectedFilteredKeys.length === filteredKeys.length);
};
const handleSelectChange = (newSelectedKeys: string[]) => {
onSelectChange(newSelectedKeys);
updateCheckStatus(newSelectedKeys);
};
const handleClearSelection = () => {
onSelectChange([]);
setCheckAll(false);
setIndeterminate(false);
};
useEffect(() => {
updateCheckStatus(selectedKeys);
}, [selectedKeys, options]);
const renderRight = () => {
return (
<Right>
<Header>
<span>({selectedKeys.length}) selected</span>
<Button type="text" size="small" onClick={handleClearSelection}>
Clear
</Button>
</Header>
<SelectedList
maxHeight={height - 40}
selectedList={options.filter((item) =>
selectedKeys.includes(item.key)
)}
onUnselect={handleOnUnselect}
/>
</Right>
);
};
return (
<PanelWrapper $maxHeight={height} $leftWidth={leftWidth}>
<Left>
<Header>
<span
style={{
display: 'flex',
alignItems: 'center',
gap: 8,
color: 'var(--ant-color-text-tertiary)'
}}
>
<Checkbox
checked={checkAll}
indeterminate={indeterminate}
onChange={handleCheckAllChange}
></Checkbox>
<span> {selectedKeys.length} selected</span>
</span>
<Input
prefix={
<SearchOutlined
style={{ color: 'var(--ant-color-text-quaternary)' }}
/>
}
size="small"
allowClear
placeholder={searchPlaceholder}
style={{
width: 300,
height: 32,
borderRadius: 4,
backgroundColor: 'var(--ant-color-bg-container) !important'
}}
onChange={handleSearch}
/>
</Header>
{showOptions.length > 0 ? (
<List
maxHeight={height - 40}
dataList={showOptions}
selectedKeys={selectedKeys}
onSelectChange={handleSelectChange}
/>
) : (
<Empty
image={Empty.PRESENTED_IMAGE_SIMPLE}
description="No models found"
></Empty>
)}
</Left>
</PanelWrapper>
);
};
export default SelectPanel;
@@ -0,0 +1,70 @@
import { OverlayScroller } from '@/components/overlay-scroller';
import { Checkbox } from 'antd';
import React from 'react';
import styled from 'styled-components';
import AutoTooltip from '../../../components/auto-tooltip';
interface ListProps {
maxHeight?: number;
dataList: Array<{ key: string; title: string }>;
selectedKeys: string[];
renderTitle?: (item: { key: string; title: string }) => React.ReactNode;
onSelectChange: (selectedKeys: string[]) => void;
}
const UL = styled.ul`
list-style: none;
margin: 0;
padding: 0;
`;
const LI = styled.li<{ selected: boolean }>`
display: flex;
align-items: center;
padding: 5px 12px;
cursor: pointer;
border-radius: 2px;
gap: 8px;
&:hover {
background-color: var(--ant-control-item-bg-hover);
}
`;
const List: React.FC<ListProps> = ({
maxHeight,
dataList,
selectedKeys,
onSelectChange,
renderTitle
}) => {
const handleClickItem = (item: { key: string; title: string }) => {
const itemKey = item.key;
const newSelectedKeys = selectedKeys.includes(itemKey)
? selectedKeys.filter((key) => key !== itemKey)
: [...selectedKeys, itemKey];
onSelectChange(newSelectedKeys);
};
return (
<OverlayScroller style={{ paddingInline: 0 }} maxHeight={maxHeight}>
<UL>
{dataList.map((item) => (
<LI
key={item.key}
selected={selectedKeys.includes(item.key)}
onClick={() => handleClickItem(item)}
>
<Checkbox checked={selectedKeys.includes(item.key)}></Checkbox>
{renderTitle ? (
renderTitle(item)
) : (
<AutoTooltip ghost>{item.title}</AutoTooltip>
)}
</LI>
))}
</UL>
</OverlayScroller>
);
};
export default List;
@@ -0,0 +1,60 @@
import AutoTooltip from '@/components/auto-tooltip';
import { OverlayScroller } from '@/components/overlay-scroller';
import { Tag } from 'antd';
import React from 'react';
import styled from 'styled-components';
const TagInner = styled(Tag)`
border-radius: 12px;
margin: 0;
`;
const Content = styled.div`
display: flex;
flex-wrap: wrap;
gap: 8px;
padding: 8px 0;
`;
interface SelectedProps {
maxHeight?: number;
selectedList: { key: string; title: string }[];
onUnselect: (
key: string,
newSelectedKeys: { key: string; title: string }[]
) => void;
}
const SelectedList: React.FC<SelectedProps> = ({
maxHeight,
selectedList,
onUnselect
}) => {
const handleOnUnselect = (key: string) => {
const newSelectedKeys = selectedList.filter((item) => item.key !== key);
onUnselect(key, newSelectedKeys);
};
return (
<OverlayScroller maxHeight={maxHeight}>
<Content>
{selectedList.map((item) => (
<AutoTooltip
title={item.title}
key={item.key}
maxWidth={200}
onClose={(e) => {
e.preventDefault();
handleOnUnselect(item.key);
}}
closable
>
{item.title}
</AutoTooltip>
))}
</Content>
</OverlayScroller>
);
};
export default SelectedList;
+50 -29
View File
@@ -1,20 +1,51 @@
import { MoreOutlined } from '@ant-design/icons'; import { MoreOutlined } from '@ant-design/icons';
import { Pagination, Transfer, TransferProps } from 'antd'; import { Transfer, TransferProps } from 'antd';
import { useState } from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
type TransferKey = string | number | bigint; type TransferKey = string | number | bigint;
const PaginationWrapper = styled.div`
padding: 4px 16px;
`;
const TransferWrap = styled.div` const TransferWrap = styled.div`
.ant-transfer-list { .ant-transfer-list {
width: 100%; width: 100%;
height: 360px; height: 300px;
.ant-transfer-list-header-dropdown {
display: none;
}
.ant-input-outlined {
height: 32px;
padding-block: 4px;
border-radius: 4px;
}
}
.ant-transfer-operation {
margin: 0 16px;
gap: 12px;
.ant-btn-icon-only {
width: 32px;
height: 32px;
border-radius: 50%;
}
} }
.ant-transfer-list-content { .ant-transfer-list-content {
&::-webkit-scrollbar {
width: var(--scrollbar-size);
}
&::-webkit-scrollbar-thumb {
background-color: transparent;
border-radius: 4px;
}
&::-webkit-scrollbar-track {
background-color: transparent;
}
&:hover {
&::-webkit-scrollbar-thumb {
background-color: var(--color-scrollbar-thumb);
border-radius: 4px;
}
}
.ant-transfer-list-content-item { .ant-transfer-list-content-item {
&:hover { &:hover {
background-color: var(--ant-control-item-bg-hover); background-color: var(--ant-control-item-bg-hover);
@@ -39,38 +70,28 @@ interface TransferInnerProps extends TransferProps {
dataSource?: Array<{ key: TransferKey; title: string }>; dataSource?: Array<{ key: TransferKey; title: string }>;
targetKeys?: TransferKey[]; targetKeys?: TransferKey[];
} }
const TransferInner: React.FC<TransferInnerProps> = (props) => { const TransferInner: React.FC<TransferInnerProps> = (props) => {
const [page, setPage] = useState(1); const renderAllLabels = (info: {
const { onPageChange, total, perPage = 30 } = props; selectedCount: number;
totalCount: number;
const handleOnPageChange = (page: number, perPage?: number) => { }) => {
setPage(page); if (info.selectedCount) {
onPageChange?.(page, perPage);
};
const renderFooter = (TransferProps: any, { direction }: any) => {
if (direction === 'left' && total && total > perPage!) {
return ( return (
<PaginationWrapper> <span style={{ color: 'var(--ant-color-text-secondary)' }}>
<Pagination {info.selectedCount} selected
simple={{ readOnly: true }} </span>
size="small"
total={total}
onChange={handleOnPageChange}
pageSize={perPage}
current={page}
showSizeChanger={false}
/>
</PaginationWrapper>
); );
} }
return null; return null;
}; };
return ( return (
<TransferWrap> <TransferWrap>
<Transfer <Transfer
{...props} {...props}
selectAllLabels={
props.selectAllLabels || [renderAllLabels, renderAllLabels]
}
selectionsIcon={ selectionsIcon={
<MoreOutlined style={{ fontSize: 14, marginBottom: 3 }} /> <MoreOutlined style={{ fontSize: 14, marginBottom: 3 }} />
} }
@@ -1,11 +1,29 @@
import TransferInner from '@/pages/_components/transfer'; import { PageAction } from '@/config';
import { PageActionType } from '@/config/types';
import SelectPanel from '@/pages/_components/select-panel';
import { queryModelsList } from '@/pages/llmodels/apis'; import { queryModelsList } from '@/pages/llmodels/apis';
import { Form } from 'antd'; import { Divider, Form, Radio } from 'antd';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import styled from 'styled-components';
import { ListItem } from '../../config/types';
const AllowModelsForm: React.FC = () => { const Label = styled.div`
font-weight: 500;
margin-block: -8px 12px;
font-size: 14px;
margin-left: 4px;
`;
const AllowModelsForm: React.FC<{
currentData?: Partial<ListItem> | null;
action: PageActionType;
}> = ({ currentData, action }) => {
const form = Form.useFormInstance(); const form = Form.useFormInstance();
const targetKeys = Form.useWatch('allowed_model_names', form); const allowedModelNames = Form.useWatch(
'allowed_model_names',
form
) as string[];
const allowedType = Form.useWatch('allowed_type', form);
const [modelList, setModelList] = useState<{ key: string; title: string }[]>( const [modelList, setModelList] = useState<{ key: string; title: string }[]>(
[] []
); );
@@ -17,36 +35,53 @@ const AllowModelsForm: React.FC = () => {
const getModelList = async () => { const getModelList = async () => {
try { try {
const res = await queryModelsList(queryParams); const res = await queryModelsList(queryParams);
const options = res.items.map((item) => ({ const options = res.items.map((item) => item.name);
title: item.name, if (action === PageAction.EDIT && currentData) {
key: item.name const list = new Set([
})); ...options,
setModelList(options); ...(currentData.allowed_model_names?.map((item) => item) || [])
]);
setModelList(
Array.from(list).map((item) => ({ key: item, title: item }))
);
} else {
setModelList(options.map((item) => ({ key: item, title: item })));
}
} catch (error) {} } catch (error) {}
}; };
useEffect(() => { useEffect(() => {
getModelList(); getModelList();
}, []); }, [action, currentData]);
return ( return (
<Form.Item name="allowed_model_names"> <div>
<TransferInner <Divider></Divider>
dataSource={modelList} <Label>Model Access</Label>
targetKeys={targetKeys} <Form.Item
onChange={(nextTargetKeys) => { name="allowed_type"
form.setFieldsValue({ allowed_model_names: nextTargetKeys }); initialValue="all"
}} style={{ marginBottom: 8 }}
render={(item) => item.title} >
titles={['Available Models', 'Allowed Models']} <Radio.Group
showSearch={{ options={[
placeholder: 'Filter by model name' { label: 'All models', value: 'all' },
}} { label: 'Selected models', value: 'custom' }
filterOption={(inputValue, item) => ]}
item.title.toLowerCase().includes(inputValue.toLowerCase()) ></Radio.Group>
}
></TransferInner>
</Form.Item> </Form.Item>
<Form.Item name="allowed_model_names" hidden={allowedType === 'all'}>
<SelectPanel
height={300}
searchPlaceholder="Search by model name"
options={modelList}
selectedKeys={allowedModelNames || []}
onSelectChange={(selectedKeys) => {
form.setFieldsValue({ allowed_model_names: selectedKeys });
}}
/>
</Form.Item>
</div>
); );
}; };
@@ -1,13 +1,18 @@
import SealInput from '@/components/seal-form/seal-input'; import SealInput from '@/components/seal-form/seal-input';
import SealSelect from '@/components/seal-form/seal-select'; import SealSelect from '@/components/seal-form/seal-select';
import { PageAction } from '@/config';
import { PageActionType } from '@/config/types';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import { Form } from 'antd'; import { Form } from 'antd';
import React from 'react'; import React from 'react';
import { expirationOptions } from '../../config'; import { expirationOptions } from '../../config';
import { FormData } from '../../config/types'; import { FormData, ListItem } from '../../config/types';
import AllowModelsForm from './allow-models'; import AllowModelsForm from './allow-models';
const APIKeyForm: React.FC = () => { const APIKeyForm: React.FC<{
action: PageActionType;
currentData?: Partial<ListItem> | null;
}> = ({ action, currentData }) => {
const intl = useIntl(); const intl = useIntl();
return ( return (
@@ -27,6 +32,8 @@ const APIKeyForm: React.FC = () => {
]} ]}
> >
<SealInput.Input <SealInput.Input
trim
disabled={action === PageAction.EDIT}
label={intl.formatMessage({ id: 'common.table.name' })} label={intl.formatMessage({ id: 'common.table.name' })}
required required
></SealInput.Input> ></SealInput.Input>
@@ -48,18 +55,22 @@ const APIKeyForm: React.FC = () => {
]} ]}
> >
<SealSelect <SealSelect
disabled={action === PageAction.EDIT}
options={expirationOptions} options={expirationOptions}
label={intl.formatMessage({ id: 'apikeys.form.expiretime' })} label={intl.formatMessage({ id: 'apikeys.form.expiretime' })}
required required
></SealSelect> ></SealSelect>
</Form.Item> </Form.Item>
<AllowModelsForm></AllowModelsForm>
<Form.Item<FormData> name="description" rules={[{ required: false }]}> <Form.Item<FormData> name="description" rules={[{ required: false }]}>
<SealInput.TextArea <SealInput.TextArea
scaleSize={true} scaleSize={true}
label={intl.formatMessage({ id: 'common.table.description' })} label={intl.formatMessage({ id: 'common.table.description' })}
></SealInput.TextArea> ></SealInput.TextArea>
</Form.Item> </Form.Item>
<AllowModelsForm
currentData={currentData}
action={action}
></AllowModelsForm>
</> </>
); );
}; };
@@ -1,17 +1,18 @@
import CopyButton from '@/components/copy-button'; import CopyButton from '@/components/copy-button';
import ModalFooter from '@/components/modal-footer'; import ModalFooter from '@/components/modal-footer';
import ScrollerModal from '@/components/scroller-modal'; import GSDrawer from '@/components/scroller-modal/gs-drawer';
import SealInput from '@/components/seal-form/seal-input'; import SealInput from '@/components/seal-form/seal-input';
import { PageAction } from '@/config'; import { PageAction } from '@/config';
import { PageActionType } from '@/config/types'; import { PageActionType } from '@/config/types';
import ColumnWrapper from '@/pages/_components/column-wrapper';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import { Button, Form, Tag } from 'antd'; import { Button, Form, Tag } from 'antd';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import _ from 'lodash';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { createApisKey, updateApisKey } from '../../apis'; import { createApisKey, updateApisKey } from '../../apis';
import { expirationOptions } from '../../config'; import { expirationOptions } from '../../config';
import { FormData, ListItem } from '../../config/types'; import { FormData, ListItem } from '../../config/types';
import AllowModelsForm from './allow-models';
import APIKeyForm from './form'; import APIKeyForm from './form';
type AddModalProps = { type AddModalProps = {
@@ -37,25 +38,6 @@ const AddModal: React.FC<AddModalProps> = ({
const [apikeyValue, setAPIKeyValue] = useState(''); const [apikeyValue, setAPIKeyValue] = useState('');
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const initValues = () => {
if (action === PageAction.CREATE && open) {
form.setFieldsValue({
expires_in: 1
});
}
if (action === PageAction.EDIT && currentData && open) {
form.setFieldsValue({
name: currentData.name,
description: currentData.description,
allowed_model_names: currentData.allowed_model_names || []
});
}
};
useEffect(() => {
initValues();
}, [open]);
const getExpireValue = (val: number | null) => { const getExpireValue = (val: number | null) => {
const expires_in = val; const expires_in = val;
if (expires_in === -1) { if (expires_in === -1) {
@@ -74,6 +56,28 @@ const AddModal: React.FC<AddModalProps> = ({
return res; return res;
}; };
// 7d, 1m, 6m, -1
const parseExpireValue = (data: ListItem) => {
const createdAt = dayjs(data.created_at);
const expiresAt = dayjs(data.expires_at);
if (!data.expires_at) {
return -1;
}
const diffInDays = expiresAt.diff(createdAt, 'day');
if (diffInDays < 10) {
return 7;
}
if (diffInDays < 60) {
return 1;
}
return 6;
};
const createAPIKey = async (data: FormData) => { const createAPIKey = async (data: FormData) => {
const params = { const params = {
...data, ...data,
@@ -89,13 +93,22 @@ const AddModal: React.FC<AddModalProps> = ({
onOk(); onOk();
}; };
const handleOnOk = async (data: FormData) => { const handleOnOk = async (formdata: FormData) => {
try { try {
setLoading(true); setLoading(true);
const data = {
..._.omit(formdata, ['allowed_type']),
allowed_model_names:
formdata.allowed_type === 'all'
? []
: formdata.allowed_model_names || []
};
if (action === PageAction.CREATE) { if (action === PageAction.CREATE) {
await createAPIKey(data); await createAPIKey(data);
} else if (action === PageAction.EDIT && currentData?.id) { } else if (action === PageAction.EDIT && currentData?.id) {
await updateAPIKey(data); await updateAPIKey({
..._.omit(data, ['expires_in'])
});
} }
setLoading(false); setLoading(false);
} catch (error) { } catch (error) {
@@ -115,28 +128,70 @@ const AddModal: React.FC<AddModalProps> = ({
setShowKey(false); setShowKey(false);
}; };
const initValues = () => {
if (action === PageAction.CREATE && open) {
form.setFieldsValue({
expires_in: 1
});
}
if (action === PageAction.EDIT && currentData && open) {
parseExpireValue(currentData as ListItem);
form.setFieldsValue({
name: currentData.name,
description: currentData.description,
allowed_type: currentData.allowed_model_names?.length
? 'custom'
: 'all',
expires_in: parseExpireValue(currentData as ListItem),
allowed_model_names: currentData.allowed_model_names || []
});
}
};
useEffect(() => {
initValues();
}, [open]);
return ( return (
<ScrollerModal <GSDrawer
title={ title={
!showKey ? title : intl.formatMessage({ id: 'apikeys.title.save' }) !showKey ? title : intl.formatMessage({ id: 'apikeys.title.save' })
} }
open={open} open={open}
centered={true} onClose={onCancel}
onOk={handleSumit}
onCancel={onCancel}
afterOpenChange={handleAfterOpenChange} afterOpenChange={handleAfterOpenChange}
destroyOnHidden={true} destroyOnHidden={true}
closeIcon={false} closeIcon={false}
maskClosable={false} maskClosable={false}
keyboard={false} keyboard={false}
width={700} styles={{
styles={{}} body: {
height: 'calc(100vh - 57px)',
padding: '16px 0',
overflowX: 'hidden'
},
content: {
borderRadius: '6px 0 0 6px'
}
}}
width={600}
footer={false}
>
<ColumnWrapper
styles={{
container: { paddingTop: 0 }
}}
footer={ footer={
!showKey ? ( !showKey ? (
<ModalFooter <ModalFooter
onOk={handleSumit} onOk={handleSumit}
onCancel={onCancel} onCancel={onCancel}
loading={loading} loading={loading}
style={{
padding: '16px 24px',
display: 'flex',
justifyContent: 'flex-end'
}}
></ModalFooter> ></ModalFooter>
) : ( ) : (
<Button type="primary" onClick={handleDone}> <Button type="primary" onClick={handleDone}>
@@ -145,9 +200,15 @@ const AddModal: React.FC<AddModalProps> = ({
) )
} }
> >
<Form name="addAPIKey" form={form} onFinish={handleOnOk} preserve={false}> <Form
{action === PageAction.EDIT && <AllowModelsForm></AllowModelsForm>} name="addAPIKey"
{!showKey && action === PageAction.CREATE && <APIKeyForm></APIKeyForm>} form={form}
onFinish={handleOnOk}
preserve={false}
>
{!showKey && (
<APIKeyForm action={action} currentData={currentData}></APIKeyForm>
)}
{showKey && action === PageAction.CREATE && ( {showKey && action === PageAction.CREATE && (
<Form.Item> <Form.Item>
<div> <div>
@@ -174,7 +235,8 @@ const AddModal: React.FC<AddModalProps> = ({
</Form.Item> </Form.Item>
)} )}
</Form> </Form>
</ScrollerModal> </ColumnWrapper>
</GSDrawer>
); );
}; };
+1
View File
@@ -11,6 +11,7 @@ export interface ListItem {
export interface FormData { export interface FormData {
name: string; name: string;
allowed_type: 'all' | 'custom';
description: string; description: string;
allowed_model_names: string[]; allowed_model_names: string[];
expires_in: number | null; expires_in: number | null;
@@ -16,7 +16,7 @@ interface ColumnsHookProps {
const actionList: Global.ActionItem[] = [ const actionList: Global.ActionItem[] = [
{ {
label: 'Edit Allowed Models', label: 'common.button.edit',
key: 'edit', key: 'edit',
icon: icons.EditOutlined icon: icons.EditOutlined
}, },
+1 -1
View File
@@ -58,7 +58,7 @@ const APIKeys: React.FC = () => {
const handleEditKey = (record: ListItem) => { const handleEditKey = (record: ListItem) => {
setOpenAddModal({ setOpenAddModal({
open: true, open: true,
title: 'Edit Allowed Models', title: 'Edit API Key',
action: PageAction.EDIT, action: PageAction.EDIT,
currentData: record currentData: record
}); });
@@ -1,7 +1,6 @@
import CheckboxField from '@/components/seal-form/checkbox-field';
import TransferInner from '@/pages/_components/transfer'; import TransferInner from '@/pages/_components/transfer';
import { queryUsersList } from '@/pages/users/apis'; import { queryUsersList } from '@/pages/users/apis';
import { Form } from 'antd'; import { Empty, Form, Radio } from 'antd';
import { forwardRef, useEffect, useImperativeHandle, useState } from 'react'; import { forwardRef, useEffect, useImperativeHandle, useState } from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import { queryModelAccessUserList } from '../../apis'; import { queryModelAccessUserList } from '../../apis';
@@ -11,9 +10,9 @@ type TransferKey = string | number | bigint;
const Label = styled.div` const Label = styled.div`
font-weight: 500; font-weight: 500;
margin-bottom: 16px; margin-block: 8px 12px;
margin-top: 16px;
font-size: 14px; font-size: 14px;
color: var(--ant-color-text-tertiary);
`; `;
interface AccessControlFormProps { interface AccessControlFormProps {
@@ -40,7 +39,8 @@ const AccessControlForm = forwardRef((props: AccessControlFormProps, ref) => {
const res = await queryUsersList(query); const res = await queryUsersList(query);
const options = res.items.map((item) => ({ const options = res.items.map((item) => ({
title: item.username, title: item.username,
key: item.id key: item.id,
is_admin: item.is_admin
})); }));
console.log('options', options); console.log('options', options);
setTotalPages(res.pagination.totalPage); setTotalPages(res.pagination.totalPage);
@@ -86,7 +86,7 @@ const AccessControlForm = forwardRef((props: AccessControlFormProps, ref) => {
const keys = res.items.map((item) => item.id); const keys = res.items.map((item) => item.id);
setTargetKeys(keys); setTargetKeys(keys);
form.setFieldsValue({ form.setFieldsValue({
set_public: res.items.length > 0, set_public: currentData.public,
users: res.items.map((item) => ({ id: item.id })) users: res.items.map((item) => ({ id: item.id }))
}); });
}); });
@@ -105,43 +105,56 @@ const AccessControlForm = forwardRef((props: AccessControlFormProps, ref) => {
clearOnDestroy={true} clearOnDestroy={true}
scrollToFirstError={true} scrollToFirstError={true}
initialValues={{ initialValues={{
public: true set_public: true
}} }}
> >
<Form.Item<AccessControlFormData> <Label>Access Scope</Label>
valuePropName="checked" <Form.Item<AccessControlFormData> name="set_public" noStyle>
name="set_public" <Radio.Group
style={{ marginBottom: 24, paddingLeft: 6 }} style={{ marginBottom: 12 }}
> options={[
<CheckboxField { label: 'All users', value: true },
description="Only authorized users can access" { label: 'Selected users', value: false }
label={'Restricted'}
></CheckboxField>
</Form.Item>
{setPublic && (
<>
<Label>User Select</Label>
<Form.Item<AccessControlFormData>
name="users"
rules={[
{
required: true,
message: 'Please select at least one user'
}
]} ]}
> ></Radio.Group>
</Form.Item>
{!setPublic && (
<>
<Label>User Selection</Label>
<Form.Item<AccessControlFormData> name="users">
<TransferInner <TransferInner
total={100}
dataSource={userList} dataSource={userList}
targetKeys={targetKeys} targetKeys={targetKeys}
showSelectAll
pagination={false} pagination={false}
titles={['Available Users', 'Users with Access']} titles={['All Users', 'Selected Users']}
locale={{
notFoundContent: [
<Empty
key="all"
description="No users found"
image={Empty.PRESENTED_IMAGE_SIMPLE}
/>,
<Empty
key="selected"
description="No users selected"
image={Empty.PRESENTED_IMAGE_SIMPLE}
/>
]
}}
showSearch={{ showSearch={{
placeholder: 'Filter by username' placeholder: 'Filter by username'
}} }}
render={(item) => item.title} filterOption={(inputValue, item) =>
selectAllLabels={[]} item.title.toLowerCase().includes(inputValue.toLowerCase())
}
render={(item) => (
<span className="flex-center gap-8">
<span>{item.title}</span>
<span className="text-tertiary">
{item.is_admin ? '(Admin)' : ''}
</span>
</span>
)}
onSearch={onSearch} onSearch={onSearch}
onChange={handleOnChange} onChange={handleOnChange}
/> />
@@ -18,11 +18,10 @@ const AccessControlModal: React.FC<
}; };
const handleOnFinish = async (values: AccessControlFormData) => { const handleOnFinish = async (values: AccessControlFormData) => {
console.log('onFinish', values);
try { try {
const data = { const data = {
set_public: !values.set_public, set_public: values.set_public,
users: values.users || [] users: values.set_public ? [] : values.users || []
}; };
await updateModelAccessUser({ await updateModelAccessUser({
id: currentData?.id as number, id: currentData?.id as number,
+1 -1
View File
@@ -467,7 +467,7 @@ const Models: React.FC<ModelsProps> = ({
if (val === 'accessControl') { if (val === 'accessControl') {
setOpenAccessControlModal({ setOpenAccessControlModal({
title: 'Edit Access Control', title: 'Access Settings',
action: PageAction.EDIT, action: PageAction.EDIT,
currentData: row, currentData: row,
open: true open: true
+1 -1
View File
@@ -62,7 +62,7 @@ export const ActionList: ActionItem[] = [
icon: icons.ApiOutlined icon: icons.ApiOutlined
}, },
{ {
label: 'Access Control', label: 'Access Settings',
key: 'accessControl', key: 'accessControl',
icon: icons.Private icon: icons.Private
}, },
+1
View File
@@ -24,6 +24,7 @@ export interface ListItem {
local_path?: string; local_path?: string;
created_at: string; created_at: string;
updated_at: string; updated_at: string;
public?: boolean;
gpu_selector?: { gpu_selector?: {
gpu_ids: string[]; gpu_ids: string[];
}; };