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)`
margin: 0;
&.tag-filled {
border: none;
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;
+208 -187
View File
@@ -2,7 +2,7 @@ import { useIntl } from '@umijs/max';
import type { SelectProps } from 'antd';
import { Checkbox, Tag } from 'antd';
import { CheckboxChangeEvent } from 'antd/es/checkbox';
import React, { useEffect } from 'react';
import React, { forwardRef, useEffect, useImperativeHandle } from 'react';
import styled from 'styled-components';
import AutoTooltip from '../auto-tooltip';
import BaseSelect from './base/select';
@@ -43,218 +43,239 @@ const TagWrapper = styled(Tag)`
border-radius: 12px;
`;
const SimpleSelect: React.FC<SelectProps> = (props) => {
const intl = useIntl();
const { options = [], ...restProps } = props;
const SimpleSelect: React.FC<SelectProps & { ref?: any; showTags?: boolean }> =
forwardRef((props, ref) => {
const intl = useIntl();
const { options = [], showTags, ...restProps } = props;
const [allSelection, setAllSelection] = React.useState<{
checked: boolean;
indeterminate: boolean;
}>({
checked: false,
indeterminate: false
});
const [optionsList, setOptionsList] = React.useState<any[]>(options || []);
const selectRef = React.useRef<any>(null);
useEffect(() => {
setOptionsList(options || []);
}, [options]);
const optionRender = (option: any, info: any) => {
const { value, label } = option;
return (
<OptionWrapper>
{restProps.value?.includes(value) ? (
<Checkbox checked></Checkbox>
) : (
<Checkbox></Checkbox>
)}
<AutoTooltip ghost>{label}</AutoTooltip>
</OptionWrapper>
);
};
const handleOnCheckboxChange = (e: CheckboxChangeEvent) => {
const isChecked = e.target.checked;
const allValues = optionsList?.map((opt: any) => opt.value) || [];
setAllSelection({
checked: isChecked,
const [allSelection, setAllSelection] = React.useState<{
checked: boolean;
indeterminate: boolean;
}>({
checked: false,
indeterminate: false
});
const [optionsList, setOptionsList] = React.useState<any[]>(options || []);
const selectRef = React.useRef<any>(null);
const selectorRef = React.useRef<any>(null);
let allSelectedValues = [...(restProps.value || [])];
useEffect(() => {
setOptionsList(options || []);
}, [options]);
if (isChecked) {
// Select all options
allSelectedValues = Array.from(
new Set([...allSelectedValues, ...allValues])
const optionRender = (option: any, info: any) => {
const { value, label } = option;
return (
<OptionWrapper>
{restProps.value?.includes?.(value) ? (
<Checkbox checked></Checkbox>
) : (
<Checkbox></Checkbox>
)}
<AutoTooltip ghost>{label}</AutoTooltip>
</OptionWrapper>
);
} else {
// Deselect all options
allSelectedValues = allSelectedValues.filter(
(value) => !allValues.includes(value)
);
}
};
restProps.onChange?.(allSelectedValues, optionsList || []);
};
const handleOnCheckboxChange = (e: CheckboxChangeEvent) => {
const isChecked = e.target.checked;
const allValues = optionsList?.map((opt: any) => opt.value) || [];
const dropdownRender = (originPanel: React.ReactNode) => {
return (
<DropdownWrapper>
{restProps.mode === 'multiple' && (
<SelectAllWrapper>
<Checkbox
checked={allSelection.checked}
indeterminate={allSelection.indeterminate}
onChange={handleOnCheckboxChange}
>
{intl.formatMessage({ id: 'common.checbox.all' })}
</Checkbox>
</SelectAllWrapper>
)}
{originPanel}
</DropdownWrapper>
);
};
const handleOnChange = (value: any, option: any) => {
const selectedValues = Array.isArray(value) ? value : [value];
const allSelected = optionsList?.map((opt: any) => opt.value) || [];
const isAllSelected = selectedValues.length === allSelected?.length;
setAllSelection({
checked: isAllSelected,
indeterminate: !isAllSelected && selectedValues.length > 0
});
restProps.onChange?.(selectedValues, option);
};
const filterOption = (inputValue: string, option: any) => {
if (!option || !option.label) return false;
return option.label.toLowerCase().includes(inputValue.toLowerCase());
};
const checkAllSelection = (list: Global.BaseOption<string | number>[]) => {
if (
!restProps.value ||
!Array.isArray(restProps.value) ||
list.length === 0
) {
setAllSelection({
checked: false,
checked: isChecked,
indeterminate: false
});
return;
}
const selectedValues = new Set(restProps.value);
const allValues = list?.map((opt: any) => opt.value) || [];
const isAllSelected = allValues.every((val: any) =>
selectedValues.has(val)
);
let allSelectedValues = [...(restProps.value || [])];
const isSomeSelected = allValues.some((val: any) =>
selectedValues.has(val)
);
if (isChecked) {
// Select all options
allSelectedValues = Array.from(
new Set([...allSelectedValues, ...allValues])
);
} else {
// Deselect all options
allSelectedValues = allSelectedValues.filter(
(value) => !allValues.includes(value)
);
}
setAllSelection({
checked: isAllSelected,
indeterminate: isSomeSelected && !isAllSelected
});
};
restProps.onChange?.(allSelectedValues, optionsList || []);
};
const TagRender = (props: any) => {
const { label } = props;
const count = props.isMaxTag ? label.slice(0, -3).slice(1) : label;
const dropdownRender = (originPanel: React.ReactNode) => {
return (
<DropdownWrapper>
{restProps.mode === 'multiple' && (
<SelectAllWrapper>
<Checkbox
checked={allSelection.checked}
indeterminate={allSelection.indeterminate}
onChange={handleOnCheckboxChange}
>
{intl.formatMessage({ id: 'common.checbox.all' })}
</Checkbox>
</SelectAllWrapper>
)}
{originPanel}
</DropdownWrapper>
);
};
return (
<TagWrapper
bordered={false}
style={{
height: 24,
backgroundColor: 'var(--ant-color-fill-tertiary)',
fontSize: 'var(--ant-font-size)'
}}
className="flex-center"
>
{intl.formatMessage({ id: 'common.select.count' }, { count: count })}
</TagWrapper>
);
};
const handleOnChange = (value: any, option: any) => {
const selectedValues = Array.isArray(value) ? value : [value];
const allSelected = optionsList?.map((opt: any) => opt.value) || [];
const isAllSelected = selectedValues.length === allSelected?.length;
const handleOnSearch = (value: string) => {
if (restProps.onSearch) {
restProps.onSearch(value);
} else {
const filteredOptions = options?.filter((option: any) =>
option.label.toLowerCase().includes(value.toLowerCase())
) as Global.BaseOption<string | number>[];
setOptionsList(filteredOptions || []);
checkAllSelection(filteredOptions || []);
}
};
setAllSelection({
checked: isAllSelected,
indeterminate: !isAllSelected && selectedValues.length > 0
});
const handleOnBlur = (e: any) => {
restProps.onBlur?.(e);
};
restProps.onChange?.(selectedValues, option);
};
const handleOnFocus = (e: any) => {
restProps.onFocus?.(e);
};
const filterOption = (inputValue: string, option: any) => {
if (!option || !option.label) return false;
return option.label.toLowerCase().includes(inputValue.toLowerCase());
};
const handleOnOpenChange = (open: boolean) => {
if (!open) {
checkAllSelection(options as Global.BaseOption<string | number>[]);
setOptionsList(options || []);
}
};
useEffect(() => {
const input = selectRef.current?.querySelector?.('input');
if (!input) return;
const handler = (event: KeyboardEvent) => {
const checkAllSelection = (list: Global.BaseOption<string | number>[]) => {
if (
event.key === 'Backspace' &&
(input as HTMLInputElement).value === ''
!restProps.value ||
!Array.isArray(restProps.value) ||
list.length === 0
) {
event.stopPropagation();
event.preventDefault();
setAllSelection({
checked: false,
indeterminate: false
});
return;
}
const selectedValues = new Set(restProps.value);
const allValues = list?.map((opt: any) => opt.value) || [];
const isAllSelected = allValues.every((val: any) =>
selectedValues.has(val)
);
const isSomeSelected = allValues.some((val: any) =>
selectedValues.has(val)
);
setAllSelection({
checked: isAllSelected,
indeterminate: isSomeSelected && !isAllSelected
});
};
const TagRender = (props: any) => {
const { label } = props;
const count = props.isMaxTag ? label.slice(0, -3).slice(1) : label;
return (
<TagWrapper
bordered={false}
closable={props.closable}
onClose={props.onClose}
style={{
height: 24,
backgroundColor: 'var(--ant-color-fill-tertiary)',
fontSize: 'var(--ant-font-size)'
}}
className="flex-center"
>
{showTags
? label
: intl.formatMessage(
{ id: 'common.select.count' },
{ count: count }
)}
</TagWrapper>
);
};
const handleOnSearch = (value: string) => {
if (restProps.onSearch) {
restProps.onSearch(value);
} else {
const filteredOptions = options?.filter((option: any) =>
option.label.toLowerCase().includes(value.toLowerCase())
) as Global.BaseOption<string | number>[];
setOptionsList(filteredOptions || []);
checkAllSelection(filteredOptions || []);
}
};
input.addEventListener('keydown', handler);
return () => {
input.removeEventListener('keydown', handler);
const handleOnBlur = (e: any) => {
restProps.onBlur?.(e);
};
}, [selectRef.current]);
return (
<div ref={selectRef}>
<BaseSelect
{...restProps}
options={optionsList}
maxTagCount={0}
defaultActiveFirstOption={false}
popupRender={dropdownRender}
optionRender={optionRender}
menuItemSelectedIcon={false}
onChange={handleOnChange}
tagRender={TagRender}
onBlur={handleOnBlur}
onFocus={handleOnFocus}
onSearch={handleOnSearch}
filterOption={filterOption}
onOpenChange={handleOnOpenChange}
></BaseSelect>
</div>
);
};
const handleOnFocus = (e: any) => {
restProps.onFocus?.(e);
};
const handleOnOpenChange = (open: boolean) => {
if (!open) {
checkAllSelection(options as Global.BaseOption<string | number>[]);
setOptionsList(options || []);
}
};
useEffect(() => {
const input = selectRef.current?.querySelector?.('input');
if (!input) return;
const handler = (event: KeyboardEvent) => {
if (
event.key === 'Backspace' &&
(input as HTMLInputElement).value === ''
) {
event.stopPropagation();
event.preventDefault();
}
};
input.addEventListener('keydown', handler);
return () => {
input.removeEventListener('keydown', handler);
};
}, [selectRef.current]);
useImperativeHandle(ref, () => ({
focus: () => {
selectorRef.current?.focus();
},
blur: () => {
selectorRef.current?.blur();
}
}));
return (
<div ref={selectRef} style={{ width: 'inherit' }}>
<BaseSelect
{...restProps}
ref={selectorRef}
options={optionsList}
maxTagCount={restProps.maxTagCount || 0}
defaultActiveFirstOption={false}
popupRender={dropdownRender}
optionRender={optionRender}
menuItemSelectedIcon={false}
onChange={handleOnChange}
tagRender={TagRender}
onBlur={handleOnBlur}
onFocus={handleOnFocus}
onSearch={handleOnSearch}
filterOption={filterOption}
onOpenChange={handleOnOpenChange}
>
{props.children}
</BaseSelect>
</div>
);
});
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 { Pagination, Transfer, TransferProps } from 'antd';
import { useState } from 'react';
import { Transfer, TransferProps } from 'antd';
import styled from 'styled-components';
type TransferKey = string | number | bigint;
const PaginationWrapper = styled.div`
padding: 4px 16px;
`;
const TransferWrap = styled.div`
.ant-transfer-list {
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 {
&::-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 {
&:hover {
background-color: var(--ant-control-item-bg-hover);
@@ -39,38 +70,28 @@ interface TransferInnerProps extends TransferProps {
dataSource?: Array<{ key: TransferKey; title: string }>;
targetKeys?: TransferKey[];
}
const TransferInner: React.FC<TransferInnerProps> = (props) => {
const [page, setPage] = useState(1);
const { onPageChange, total, perPage = 30 } = props;
const handleOnPageChange = (page: number, perPage?: number) => {
setPage(page);
onPageChange?.(page, perPage);
};
const renderFooter = (TransferProps: any, { direction }: any) => {
if (direction === 'left' && total && total > perPage!) {
const renderAllLabels = (info: {
selectedCount: number;
totalCount: number;
}) => {
if (info.selectedCount) {
return (
<PaginationWrapper>
<Pagination
simple={{ readOnly: true }}
size="small"
total={total}
onChange={handleOnPageChange}
pageSize={perPage}
current={page}
showSizeChanger={false}
/>
</PaginationWrapper>
<span style={{ color: 'var(--ant-color-text-secondary)' }}>
{info.selectedCount} selected
</span>
);
}
return null;
};
return (
<TransferWrap>
<Transfer
{...props}
selectAllLabels={
props.selectAllLabels || [renderAllLabels, renderAllLabels]
}
selectionsIcon={
<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 { Form } from 'antd';
import { Divider, Form, Radio } from 'antd';
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 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 }[]>(
[]
);
@@ -17,36 +35,53 @@ const AllowModelsForm: React.FC = () => {
const getModelList = async () => {
try {
const res = await queryModelsList(queryParams);
const options = res.items.map((item) => ({
title: item.name,
key: item.name
}));
setModelList(options);
const options = res.items.map((item) => item.name);
if (action === PageAction.EDIT && currentData) {
const list = new Set([
...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) {}
};
useEffect(() => {
getModelList();
}, []);
}, [action, currentData]);
return (
<Form.Item name="allowed_model_names">
<TransferInner
dataSource={modelList}
targetKeys={targetKeys}
onChange={(nextTargetKeys) => {
form.setFieldsValue({ allowed_model_names: nextTargetKeys });
}}
render={(item) => item.title}
titles={['Available Models', 'Allowed Models']}
showSearch={{
placeholder: 'Filter by model name'
}}
filterOption={(inputValue, item) =>
item.title.toLowerCase().includes(inputValue.toLowerCase())
}
></TransferInner>
</Form.Item>
<div>
<Divider></Divider>
<Label>Model Access</Label>
<Form.Item
name="allowed_type"
initialValue="all"
style={{ marginBottom: 8 }}
>
<Radio.Group
options={[
{ label: 'All models', value: 'all' },
{ label: 'Selected models', value: 'custom' }
]}
></Radio.Group>
</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 SealSelect from '@/components/seal-form/seal-select';
import { PageAction } from '@/config';
import { PageActionType } from '@/config/types';
import { useIntl } from '@umijs/max';
import { Form } from 'antd';
import React from 'react';
import { expirationOptions } from '../../config';
import { FormData } from '../../config/types';
import { FormData, ListItem } from '../../config/types';
import AllowModelsForm from './allow-models';
const APIKeyForm: React.FC = () => {
const APIKeyForm: React.FC<{
action: PageActionType;
currentData?: Partial<ListItem> | null;
}> = ({ action, currentData }) => {
const intl = useIntl();
return (
@@ -27,6 +32,8 @@ const APIKeyForm: React.FC = () => {
]}
>
<SealInput.Input
trim
disabled={action === PageAction.EDIT}
label={intl.formatMessage({ id: 'common.table.name' })}
required
></SealInput.Input>
@@ -48,18 +55,22 @@ const APIKeyForm: React.FC = () => {
]}
>
<SealSelect
disabled={action === PageAction.EDIT}
options={expirationOptions}
label={intl.formatMessage({ id: 'apikeys.form.expiretime' })}
required
></SealSelect>
</Form.Item>
<AllowModelsForm></AllowModelsForm>
<Form.Item<FormData> name="description" rules={[{ required: false }]}>
<SealInput.TextArea
scaleSize={true}
label={intl.formatMessage({ id: 'common.table.description' })}
></SealInput.TextArea>
</Form.Item>
<AllowModelsForm
currentData={currentData}
action={action}
></AllowModelsForm>
</>
);
};
@@ -1,17 +1,18 @@
import CopyButton from '@/components/copy-button';
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 { PageAction } from '@/config';
import { PageActionType } from '@/config/types';
import ColumnWrapper from '@/pages/_components/column-wrapper';
import { useIntl } from '@umijs/max';
import { Button, Form, Tag } from 'antd';
import dayjs from 'dayjs';
import _ from 'lodash';
import { useEffect, useState } from 'react';
import { createApisKey, updateApisKey } from '../../apis';
import { expirationOptions } from '../../config';
import { FormData, ListItem } from '../../config/types';
import AllowModelsForm from './allow-models';
import APIKeyForm from './form';
type AddModalProps = {
@@ -37,25 +38,6 @@ const AddModal: React.FC<AddModalProps> = ({
const [apikeyValue, setAPIKeyValue] = useState('');
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 expires_in = val;
if (expires_in === -1) {
@@ -74,6 +56,28 @@ const AddModal: React.FC<AddModalProps> = ({
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 params = {
...data,
@@ -89,13 +93,22 @@ const AddModal: React.FC<AddModalProps> = ({
onOk();
};
const handleOnOk = async (data: FormData) => {
const handleOnOk = async (formdata: FormData) => {
try {
setLoading(true);
const data = {
..._.omit(formdata, ['allowed_type']),
allowed_model_names:
formdata.allowed_type === 'all'
? []
: formdata.allowed_model_names || []
};
if (action === PageAction.CREATE) {
await createAPIKey(data);
} else if (action === PageAction.EDIT && currentData?.id) {
await updateAPIKey(data);
await updateAPIKey({
..._.omit(data, ['expires_in'])
});
}
setLoading(false);
} catch (error) {
@@ -115,66 +128,115 @@ const AddModal: React.FC<AddModalProps> = ({
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 (
<ScrollerModal
<GSDrawer
title={
!showKey ? title : intl.formatMessage({ id: 'apikeys.title.save' })
}
open={open}
centered={true}
onOk={handleSumit}
onCancel={onCancel}
onClose={onCancel}
afterOpenChange={handleAfterOpenChange}
destroyOnHidden={true}
closeIcon={false}
maskClosable={false}
keyboard={false}
width={700}
styles={{}}
footer={
!showKey ? (
<ModalFooter
onOk={handleSumit}
onCancel={onCancel}
loading={loading}
></ModalFooter>
) : (
<Button type="primary" onClick={handleDone}>
{intl.formatMessage({ id: 'common.button.done' })}
</Button>
)
}
styles={{
body: {
height: 'calc(100vh - 57px)',
padding: '16px 0',
overflowX: 'hidden'
},
content: {
borderRadius: '6px 0 0 6px'
}
}}
width={600}
footer={false}
>
<Form name="addAPIKey" form={form} onFinish={handleOnOk} preserve={false}>
{action === PageAction.EDIT && <AllowModelsForm></AllowModelsForm>}
{!showKey && action === PageAction.CREATE && <APIKeyForm></APIKeyForm>}
{showKey && action === PageAction.CREATE && (
<Form.Item>
<div>
<Tag
bordered={false}
color="error"
style={{ padding: '6px 8px', marginBottom: 16 }}
>
{intl.formatMessage({ id: 'apikeys.table.save.tips' })}
</Tag>
</div>
<SealInput.Input
label={intl.formatMessage({ id: 'apikeys.form.apikey' })}
value={apikeyValue}
addAfter={
<CopyButton
text={apikeyValue}
shape="default"
size="middle"
type="text"
></CopyButton>
}
></SealInput.Input>
</Form.Item>
)}
</Form>
</ScrollerModal>
<ColumnWrapper
styles={{
container: { paddingTop: 0 }
}}
footer={
!showKey ? (
<ModalFooter
onOk={handleSumit}
onCancel={onCancel}
loading={loading}
style={{
padding: '16px 24px',
display: 'flex',
justifyContent: 'flex-end'
}}
></ModalFooter>
) : (
<Button type="primary" onClick={handleDone}>
{intl.formatMessage({ id: 'common.button.done' })}
</Button>
)
}
>
<Form
name="addAPIKey"
form={form}
onFinish={handleOnOk}
preserve={false}
>
{!showKey && (
<APIKeyForm action={action} currentData={currentData}></APIKeyForm>
)}
{showKey && action === PageAction.CREATE && (
<Form.Item>
<div>
<Tag
bordered={false}
color="error"
style={{ padding: '6px 8px', marginBottom: 16 }}
>
{intl.formatMessage({ id: 'apikeys.table.save.tips' })}
</Tag>
</div>
<SealInput.Input
label={intl.formatMessage({ id: 'apikeys.form.apikey' })}
value={apikeyValue}
addAfter={
<CopyButton
text={apikeyValue}
shape="default"
size="middle"
type="text"
></CopyButton>
}
></SealInput.Input>
</Form.Item>
)}
</Form>
</ColumnWrapper>
</GSDrawer>
);
};
+1
View File
@@ -11,6 +11,7 @@ export interface ListItem {
export interface FormData {
name: string;
allowed_type: 'all' | 'custom';
description: string;
allowed_model_names: string[];
expires_in: number | null;
@@ -16,7 +16,7 @@ interface ColumnsHookProps {
const actionList: Global.ActionItem[] = [
{
label: 'Edit Allowed Models',
label: 'common.button.edit',
key: 'edit',
icon: icons.EditOutlined
},
+1 -1
View File
@@ -58,7 +58,7 @@ const APIKeys: React.FC = () => {
const handleEditKey = (record: ListItem) => {
setOpenAddModal({
open: true,
title: 'Edit Allowed Models',
title: 'Edit API Key',
action: PageAction.EDIT,
currentData: record
});
@@ -1,7 +1,6 @@
import CheckboxField from '@/components/seal-form/checkbox-field';
import TransferInner from '@/pages/_components/transfer';
import { queryUsersList } from '@/pages/users/apis';
import { Form } from 'antd';
import { Empty, Form, Radio } from 'antd';
import { forwardRef, useEffect, useImperativeHandle, useState } from 'react';
import styled from 'styled-components';
import { queryModelAccessUserList } from '../../apis';
@@ -11,9 +10,9 @@ type TransferKey = string | number | bigint;
const Label = styled.div`
font-weight: 500;
margin-bottom: 16px;
margin-top: 16px;
margin-block: 8px 12px;
font-size: 14px;
color: var(--ant-color-text-tertiary);
`;
interface AccessControlFormProps {
@@ -40,7 +39,8 @@ const AccessControlForm = forwardRef((props: AccessControlFormProps, ref) => {
const res = await queryUsersList(query);
const options = res.items.map((item) => ({
title: item.username,
key: item.id
key: item.id,
is_admin: item.is_admin
}));
console.log('options', options);
setTotalPages(res.pagination.totalPage);
@@ -86,7 +86,7 @@ const AccessControlForm = forwardRef((props: AccessControlFormProps, ref) => {
const keys = res.items.map((item) => item.id);
setTargetKeys(keys);
form.setFieldsValue({
set_public: res.items.length > 0,
set_public: currentData.public,
users: res.items.map((item) => ({ id: item.id }))
});
});
@@ -105,43 +105,56 @@ const AccessControlForm = forwardRef((props: AccessControlFormProps, ref) => {
clearOnDestroy={true}
scrollToFirstError={true}
initialValues={{
public: true
set_public: true
}}
>
<Form.Item<AccessControlFormData>
valuePropName="checked"
name="set_public"
style={{ marginBottom: 24, paddingLeft: 6 }}
>
<CheckboxField
description="Only authorized users can access"
label={'Restricted'}
></CheckboxField>
<Label>Access Scope</Label>
<Form.Item<AccessControlFormData> name="set_public" noStyle>
<Radio.Group
style={{ marginBottom: 12 }}
options={[
{ label: 'All users', value: true },
{ label: 'Selected users', value: false }
]}
></Radio.Group>
</Form.Item>
{setPublic && (
{!setPublic && (
<>
<Label>User Select</Label>
<Form.Item<AccessControlFormData>
name="users"
rules={[
{
required: true,
message: 'Please select at least one user'
}
]}
>
<Label>User Selection</Label>
<Form.Item<AccessControlFormData> name="users">
<TransferInner
total={100}
dataSource={userList}
targetKeys={targetKeys}
showSelectAll
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={{
placeholder: 'Filter by username'
}}
render={(item) => item.title}
selectAllLabels={[]}
filterOption={(inputValue, item) =>
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}
onChange={handleOnChange}
/>
@@ -18,11 +18,10 @@ const AccessControlModal: React.FC<
};
const handleOnFinish = async (values: AccessControlFormData) => {
console.log('onFinish', values);
try {
const data = {
set_public: !values.set_public,
users: values.users || []
set_public: values.set_public,
users: values.set_public ? [] : values.users || []
};
await updateModelAccessUser({
id: currentData?.id as number,
+1 -1
View File
@@ -467,7 +467,7 @@ const Models: React.FC<ModelsProps> = ({
if (val === 'accessControl') {
setOpenAccessControlModal({
title: 'Edit Access Control',
title: 'Access Settings',
action: PageAction.EDIT,
currentData: row,
open: true
+1 -1
View File
@@ -62,7 +62,7 @@ export const ActionList: ActionItem[] = [
icon: icons.ApiOutlined
},
{
label: 'Access Control',
label: 'Access Settings',
key: 'accessControl',
icon: icons.Private
},
+1
View File
@@ -24,6 +24,7 @@ export interface ListItem {
local_path?: string;
created_at: string;
updated_at: string;
public?: boolean;
gpu_selector?: {
gpu_ids: string[];
};