chore: model access statement
This commit is contained in:
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -62,7 +62,7 @@ export const ActionList: ActionItem[] = [
|
||||
icon: icons.ApiOutlined
|
||||
},
|
||||
{
|
||||
label: 'Access Control',
|
||||
label: 'Access Settings',
|
||||
key: 'accessControl',
|
||||
icon: icons.Private
|
||||
},
|
||||
|
||||
@@ -24,6 +24,7 @@ export interface ListItem {
|
||||
local_path?: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
public?: boolean;
|
||||
gpu_selector?: {
|
||||
gpu_ids: string[];
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user