chore: model access statement
This commit is contained in:
@@ -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;
|
||||||
@@ -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,218 +43,239 @@ 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 }> =
|
||||||
const intl = useIntl();
|
forwardRef((props, ref) => {
|
||||||
const { options = [], ...restProps } = props;
|
const intl = useIntl();
|
||||||
|
const { options = [], showTags, ...restProps } = props;
|
||||||
|
|
||||||
const [allSelection, setAllSelection] = React.useState<{
|
const [allSelection, setAllSelection] = React.useState<{
|
||||||
checked: boolean;
|
checked: boolean;
|
||||||
indeterminate: boolean;
|
indeterminate: boolean;
|
||||||
}>({
|
}>({
|
||||||
checked: false,
|
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,
|
|
||||||
indeterminate: 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) {
|
const optionRender = (option: any, info: any) => {
|
||||||
// Select all options
|
const { value, label } = option;
|
||||||
allSelectedValues = Array.from(
|
return (
|
||||||
new Set([...allSelectedValues, ...allValues])
|
<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({
|
setAllSelection({
|
||||||
checked: false,
|
checked: isChecked,
|
||||||
indeterminate: false
|
indeterminate: false
|
||||||
});
|
});
|
||||||
return;
|
|
||||||
}
|
|
||||||
const selectedValues = new Set(restProps.value);
|
|
||||||
const allValues = list?.map((opt: any) => opt.value) || [];
|
|
||||||
|
|
||||||
const isAllSelected = allValues.every((val: any) =>
|
let allSelectedValues = [...(restProps.value || [])];
|
||||||
selectedValues.has(val)
|
|
||||||
);
|
|
||||||
|
|
||||||
const isSomeSelected = allValues.some((val: any) =>
|
if (isChecked) {
|
||||||
selectedValues.has(val)
|
// Select all options
|
||||||
);
|
allSelectedValues = Array.from(
|
||||||
|
new Set([...allSelectedValues, ...allValues])
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Deselect all options
|
||||||
|
allSelectedValues = allSelectedValues.filter(
|
||||||
|
(value) => !allValues.includes(value)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
setAllSelection({
|
restProps.onChange?.(allSelectedValues, optionsList || []);
|
||||||
checked: isAllSelected,
|
};
|
||||||
indeterminate: isSomeSelected && !isAllSelected
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const TagRender = (props: any) => {
|
const dropdownRender = (originPanel: React.ReactNode) => {
|
||||||
const { label } = props;
|
return (
|
||||||
const count = props.isMaxTag ? label.slice(0, -3).slice(1) : label;
|
<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 (
|
const handleOnChange = (value: any, option: any) => {
|
||||||
<TagWrapper
|
const selectedValues = Array.isArray(value) ? value : [value];
|
||||||
bordered={false}
|
const allSelected = optionsList?.map((opt: any) => opt.value) || [];
|
||||||
style={{
|
const isAllSelected = selectedValues.length === allSelected?.length;
|
||||||
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 handleOnSearch = (value: string) => {
|
setAllSelection({
|
||||||
if (restProps.onSearch) {
|
checked: isAllSelected,
|
||||||
restProps.onSearch(value);
|
indeterminate: !isAllSelected && selectedValues.length > 0
|
||||||
} else {
|
});
|
||||||
const filteredOptions = options?.filter((option: any) =>
|
|
||||||
option.label.toLowerCase().includes(value.toLowerCase())
|
|
||||||
) as Global.BaseOption<string | number>[];
|
|
||||||
setOptionsList(filteredOptions || []);
|
|
||||||
checkAllSelection(filteredOptions || []);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleOnBlur = (e: any) => {
|
restProps.onChange?.(selectedValues, option);
|
||||||
restProps.onBlur?.(e);
|
};
|
||||||
};
|
|
||||||
|
|
||||||
const handleOnFocus = (e: any) => {
|
const filterOption = (inputValue: string, option: any) => {
|
||||||
restProps.onFocus?.(e);
|
if (!option || !option.label) return false;
|
||||||
};
|
return option.label.toLowerCase().includes(inputValue.toLowerCase());
|
||||||
|
};
|
||||||
|
|
||||||
const handleOnOpenChange = (open: boolean) => {
|
const checkAllSelection = (list: Global.BaseOption<string | number>[]) => {
|
||||||
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 (
|
if (
|
||||||
event.key === 'Backspace' &&
|
!restProps.value ||
|
||||||
(input as HTMLInputElement).value === ''
|
!Array.isArray(restProps.value) ||
|
||||||
|
list.length === 0
|
||||||
) {
|
) {
|
||||||
event.stopPropagation();
|
setAllSelection({
|
||||||
event.preventDefault();
|
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);
|
const handleOnBlur = (e: any) => {
|
||||||
|
restProps.onBlur?.(e);
|
||||||
return () => {
|
|
||||||
input.removeEventListener('keydown', handler);
|
|
||||||
};
|
};
|
||||||
}, [selectRef.current]);
|
|
||||||
|
|
||||||
return (
|
const handleOnFocus = (e: any) => {
|
||||||
<div ref={selectRef}>
|
restProps.onFocus?.(e);
|
||||||
<BaseSelect
|
};
|
||||||
{...restProps}
|
|
||||||
options={optionsList}
|
const handleOnOpenChange = (open: boolean) => {
|
||||||
maxTagCount={0}
|
if (!open) {
|
||||||
defaultActiveFirstOption={false}
|
checkAllSelection(options as Global.BaseOption<string | number>[]);
|
||||||
popupRender={dropdownRender}
|
setOptionsList(options || []);
|
||||||
optionRender={optionRender}
|
}
|
||||||
menuItemSelectedIcon={false}
|
};
|
||||||
onChange={handleOnChange}
|
|
||||||
tagRender={TagRender}
|
useEffect(() => {
|
||||||
onBlur={handleOnBlur}
|
const input = selectRef.current?.querySelector?.('input');
|
||||||
onFocus={handleOnFocus}
|
|
||||||
onSearch={handleOnSearch}
|
if (!input) return;
|
||||||
filterOption={filterOption}
|
|
||||||
onOpenChange={handleOnOpenChange}
|
const handler = (event: KeyboardEvent) => {
|
||||||
></BaseSelect>
|
if (
|
||||||
</div>
|
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;
|
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 { 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>
|
||||||
}
|
</Form.Item>
|
||||||
></TransferInner>
|
<Form.Item name="allowed_model_names" hidden={allowedType === 'all'}>
|
||||||
</Form.Item>
|
<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,66 +128,115 @@ 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: {
|
||||||
footer={
|
height: 'calc(100vh - 57px)',
|
||||||
!showKey ? (
|
padding: '16px 0',
|
||||||
<ModalFooter
|
overflowX: 'hidden'
|
||||||
onOk={handleSumit}
|
},
|
||||||
onCancel={onCancel}
|
content: {
|
||||||
loading={loading}
|
borderRadius: '6px 0 0 6px'
|
||||||
></ModalFooter>
|
}
|
||||||
) : (
|
}}
|
||||||
<Button type="primary" onClick={handleDone}>
|
width={600}
|
||||||
{intl.formatMessage({ id: 'common.button.done' })}
|
footer={false}
|
||||||
</Button>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<Form name="addAPIKey" form={form} onFinish={handleOnOk} preserve={false}>
|
<ColumnWrapper
|
||||||
{action === PageAction.EDIT && <AllowModelsForm></AllowModelsForm>}
|
styles={{
|
||||||
{!showKey && action === PageAction.CREATE && <APIKeyForm></APIKeyForm>}
|
container: { paddingTop: 0 }
|
||||||
{showKey && action === PageAction.CREATE && (
|
}}
|
||||||
<Form.Item>
|
footer={
|
||||||
<div>
|
!showKey ? (
|
||||||
<Tag
|
<ModalFooter
|
||||||
bordered={false}
|
onOk={handleSumit}
|
||||||
color="error"
|
onCancel={onCancel}
|
||||||
style={{ padding: '6px 8px', marginBottom: 16 }}
|
loading={loading}
|
||||||
>
|
style={{
|
||||||
{intl.formatMessage({ id: 'apikeys.table.save.tips' })}
|
padding: '16px 24px',
|
||||||
</Tag>
|
display: 'flex',
|
||||||
</div>
|
justifyContent: 'flex-end'
|
||||||
<SealInput.Input
|
}}
|
||||||
label={intl.formatMessage({ id: 'apikeys.form.apikey' })}
|
></ModalFooter>
|
||||||
value={apikeyValue}
|
) : (
|
||||||
addAfter={
|
<Button type="primary" onClick={handleDone}>
|
||||||
<CopyButton
|
{intl.formatMessage({ id: 'common.button.done' })}
|
||||||
text={apikeyValue}
|
</Button>
|
||||||
shape="default"
|
)
|
||||||
size="middle"
|
}
|
||||||
type="text"
|
>
|
||||||
></CopyButton>
|
<Form
|
||||||
}
|
name="addAPIKey"
|
||||||
></SealInput.Input>
|
form={form}
|
||||||
</Form.Item>
|
onFinish={handleOnOk}
|
||||||
)}
|
preserve={false}
|
||||||
</Form>
|
>
|
||||||
</ScrollerModal>
|
{!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 {
|
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
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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>
|
></Radio.Group>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
{setPublic && (
|
{!setPublic && (
|
||||||
<>
|
<>
|
||||||
<Label>User Select</Label>
|
<Label>User Selection</Label>
|
||||||
<Form.Item<AccessControlFormData>
|
<Form.Item<AccessControlFormData> name="users">
|
||||||
name="users"
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
message: 'Please select at least one user'
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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[];
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user