feat: custom api key

This commit is contained in:
jialin
2026-04-09 21:27:50 +08:00
committed by jialin
parent b947e3a8b9
commit f04b5cdc27
13 changed files with 174 additions and 19 deletions
+12
View File
@@ -368,3 +368,15 @@ textarea:hover {
.line-6 {
line-height: 24px;
}
.align-right {
text-align: right;
}
.align-left {
text-align: left;
}
.align-center {
text-align: center;
}
+10 -3
View File
@@ -4,7 +4,6 @@ import styled from 'styled-components';
import './index.less';
const HeaderWrapper = styled.div<{ $height?: number }>`
height: ${(props) => (props.$height ? `${props.$height}px` : 'auto')};
display: flex;
padding-block: 0;
justify-content: space-between;
@@ -12,7 +11,7 @@ const HeaderWrapper = styled.div<{ $height?: number }>`
`;
const Wrapper = styled.div`
border-radius: var(--border-radius-mini);
border-radius: var(--border-radius);
overflow: hidden;
&.bordered {
@@ -57,7 +56,15 @@ const EditorWrap: React.FC<EditorwrapProps> = ({
borderless: variant === 'borderless'
})}
>
{header && <HeaderWrapper $height={headerHeight}>{header}</HeaderWrapper>}
{header && (
<HeaderWrapper
style={{
height: headerHeight || 'auto'
}}
>
{header}
</HeaderWrapper>
)}
<div>{children}</div>
</Wrapper>
);
+5 -1
View File
@@ -17,5 +17,9 @@ export default {
'apikeys.table.bindModels': 'Model Access',
'apikeys.models.all': 'All models',
'apikeys.models.selected': 'Allowed models',
'apikeys.models.noModelsFound': 'No models found'
'apikeys.models.noModelsFound': 'No models found',
'apikeys.accessScope.all': 'All',
'apikeys.accessScope.management': 'Management APIs',
'apikeys.accessScope.inference': 'Inference APIs',
'apikeys.access.permissions': 'Access Permissions'
};
+7 -2
View File
@@ -17,12 +17,17 @@ export default {
'apikeys.table.bindModels': 'Model Access',
'apikeys.models.all': 'All models',
'apikeys.models.selected': 'Allowed models',
'apikeys.models.noModelsFound': 'No models found'
'apikeys.models.noModelsFound': 'No models found',
'apikeys.accessScope.all': 'All',
'apikeys.accessScope.management': 'Management APIs',
'apikeys.accessScope.inference': 'Inference APIs',
'apikeys.access.permissions': 'Access Permissions'
};
// ========== To-Do: Translate Keys (Remove After Translation) ==========
// 1. 'apikeys.table.bindModels': 'Model Access',
// 2. 'apikeys.models.all': 'All models',
// 3. 'apikeys.models.selected': 'Allowed models',
// 4. 'apikeys.models.noModelsFound': 'No models found'
// 4. 'apikeys.models.noModelsFound': 'No models found',
// 5. 'apikeys.access.permissions': 'Access Permissions'
// ========== End of To-Do List ==========
+5 -1
View File
@@ -17,7 +17,11 @@ export default {
'apikeys.table.bindModels': 'Доступ к моделям',
'apikeys.models.all': 'Все модели',
'apikeys.models.selected': 'Разрешенные модели',
'apikeys.models.noModelsFound': 'Модели не найдены'
'apikeys.models.noModelsFound': 'Модели не найдены',
'apikeys.accessScope.all': 'All',
'apikeys.accessScope.management': 'Management APIs',
'apikeys.accessScope.inference': 'Inference APIs',
'apikeys.access.permissions': 'Access Permissions'
};
// ========== To-Do: Translate Keys (Remove After Translation) ==========
+5 -1
View File
@@ -17,5 +17,9 @@ export default {
'apikeys.table.bindModels': 'Model Erişimi',
'apikeys.models.all': 'Tüm modeller',
'apikeys.models.selected': 'İzin verilen modeller',
'apikeys.models.noModelsFound': 'Model bulunamadı'
'apikeys.models.noModelsFound': 'Model bulunamadı',
'apikeys.accessScope.all': 'All',
'apikeys.accessScope.management': 'Management APIs',
'apikeys.accessScope.inference': 'Inference APIs',
'apikeys.access.permissions': 'Access Permissions'
};
+5 -1
View File
@@ -16,5 +16,9 @@ export default {
'apikeys.table.bindModels': '模型访问',
'apikeys.models.all': '所有模型',
'apikeys.models.selected': '指定模型',
'apikeys.models.noModelsFound': '未找到模型'
'apikeys.models.noModelsFound': '未找到模型',
'apikeys.accessScope.all': '全部',
'apikeys.accessScope.management': '管理接口',
'apikeys.accessScope.inference': '推理接口',
'apikeys.access.permissions': '访问权限'
};
@@ -22,6 +22,7 @@ const Container = styled.div`
position: relative;
border: 1px solid var(--ant-color-border);
border-radius: var(--ant-border-radius);
overflow: hidden;
.monaco-editor .scroll-decoration {
box-shadow: none;
}
@@ -1,3 +1,4 @@
import Password from '@/components/seal-form/password';
import SealInput from '@/components/seal-form/seal-input';
import SealSelect from '@/components/seal-form/seal-select';
import { PageAction } from '@/config';
@@ -5,7 +6,7 @@ import { PageActionType } from '@/config/types';
import { useIntl } from '@umijs/max';
import { Form } from 'antd';
import React from 'react';
import { expirationOptions } from '../../config';
import { accessScopeOptions, expirationOptions } from '../../config';
import { FormData, ListItem } from '../../config/types';
import AllowModelsForm from './allow-models';
@@ -16,6 +17,17 @@ const APIKeyForm: React.FC<{
}> = ({ action, currentData, onValuesChange }) => {
const intl = useIntl();
const optionRender = (option: any) => {
return (
<span className="flex-center gap-4">
{option.label}
{option.data.description && (
<span className="text-tertiary">[{option.data.description}]</span>
)}
</span>
);
};
return (
<>
<Form.Item<FormData>
@@ -39,6 +51,28 @@ const APIKeyForm: React.FC<{
required
></SealInput.Input>
</Form.Item>
{action === PageAction.CREATE && (
<Form.Item<FormData> name="custom">
<Password
trim
autoComplete="new-password"
label={intl.formatMessage({ id: 'playground.params.custom' })}
></Password>
</Form.Item>
)}
<Form.Item<FormData>
name="scope"
normalize={(value) => (value ? [value] : [])}
getValueProps={(value) => ({
value: Array.isArray(value) ? value[0] : value
})}
>
<SealSelect
optionRender={optionRender}
options={accessScopeOptions}
label={intl.formatMessage({ id: 'models.table.accessScope' })}
></SealSelect>
</Form.Item>
<Form.Item<FormData>
name="expires_in"
rules={[
@@ -96,6 +96,12 @@ const AddModal: React.FC<AddModalProps> = ({
expires_in: getExpireValue(data.expires_in)
};
const res = await createApisKey({ data: params });
// if custom value
if (data.custom) {
return onOk();
}
setAPIKeyValue(res.value);
setShowKey(true);
};
@@ -107,6 +113,7 @@ const AddModal: React.FC<AddModalProps> = ({
const handleOnOk = async (formdata: FormData) => {
try {
console.log('formdata=========', formdata);
setLoading(true);
const data = {
..._.omit(formdata, ['allowed_type']),
@@ -261,6 +268,9 @@ const AddModal: React.FC<AddModalProps> = ({
form={form}
onFinish={handleOnOk}
preserve={false}
initialValues={{
scope: ['*']
}}
>
{!showKey && (
<APIKeyForm
+20
View File
@@ -24,3 +24,23 @@ export const expirationOptions = [
locale: true
}
];
export const accessScopeOptions = [
{
label: 'apikeys.accessScope.all',
value: '*',
locale: true
},
{
label: 'apikeys.accessScope.management',
value: 'management',
description: 'v2/',
locale: true
},
{
label: 'apikeys.accessScope.inference',
value: 'inference',
description: 'v1/',
locale: true
}
];
+4
View File
@@ -7,6 +7,8 @@ export interface ListItem {
updated_at: string;
expires_at: string;
allowed_model_names: string[];
custom?: string;
scope?: string[];
}
export interface FormData {
@@ -15,4 +17,6 @@ export interface FormData {
description: string;
allowed_model_names: string[];
expires_in: number | null;
custom?: string;
scope?: string[];
}
+55 -9
View File
@@ -4,9 +4,11 @@ import DropdownButtons from '@/components/drop-down-buttons';
import icons from '@/components/icon-font/icons';
import { tableSorter } from '@/config/settings';
import { useIntl } from '@umijs/max';
import { Tag } from 'antd';
import { ColumnsType } from 'antd/lib/table';
import dayjs from 'dayjs';
import { useMemo } from 'react';
import { accessScopeOptions } from '../config';
import { ListItem } from '../config/types';
interface ColumnsHookProps {
@@ -34,6 +36,20 @@ const useModelsColumns = ({
}: ColumnsHookProps): ColumnsType<ListItem> => {
const intl = useIntl();
const renderAPIs = (record: ListItem) => {
const option = accessScopeOptions.find(
(item) => item.value === record.scope?.[0]
);
return (
<span className="flex-center gap-4">
{intl.formatMessage({
id: option?.label || ''
})}
<span>[{option?.description}]</span>
</span>
);
};
return useMemo(() => {
return [
{
@@ -42,9 +58,23 @@ const useModelsColumns = ({
key: 'name',
sorter: tableSorter(1),
render: (text: string, record: ListItem) => (
<AutoTooltip ghost style={{ maxWidth: 400 }}>
{text}
</AutoTooltip>
<span className="flex items-center">
<AutoTooltip ghost style={{ maxWidth: 400 }}>
<span className="text-primary">{text}</span>
</AutoTooltip>
<Tag
style={{
marginLeft: 8,
borderRadius: 12,
color: 'var(--ant-color-text-tertiary)',
borderColor: 'var(--ant-color-split)',
backgroundColor: 'transparent'
}}
variant="outlined"
>
{intl.formatMessage({ id: 'playground.params.custom' })}
</Tag>
</span>
)
},
{
@@ -73,18 +103,34 @@ const useModelsColumns = ({
)
},
{
title: intl.formatMessage({ id: 'apikeys.table.bindModels' }),
title: intl.formatMessage({ id: 'apikeys.access.permissions' }),
dataIndex: 'allowed_model_names',
key: 'allowed_model_names',
ellipsis: {
showTitle: false
},
render: (text: string[], record: ListItem) => (
<AutoTooltip ghost>
{text?.length
? text?.join(', ')
: intl.formatMessage({ id: 'common.select.option' })}
</AutoTooltip>
<div
style={{
display: 'grid',
gridTemplateColumns: 'max-content 1fr',
gap: 8,
alignItems: 'center'
}}
>
<span className="text-primary">APIs:</span>
<AutoTooltip ghost>{renderAPIs(record)}</AutoTooltip>
<span className="text-primary">
{intl.formatMessage({ id: 'models.title' })}:
</span>
<AutoTooltip ghost>
<span>
{text?.length
? text?.join(', ')
: intl.formatMessage({ id: 'common.select.option' })}
</span>
</AutoTooltip>
</div>
)
},
{