chore: playground hotkey
This commit is contained in:
@@ -27,6 +27,7 @@
|
|||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"numeral": "^2.0.6",
|
"numeral": "^2.0.6",
|
||||||
"query-string": "^9.0.0",
|
"query-string": "^9.0.0",
|
||||||
|
"react-hotkeys-hook": "^4.5.0",
|
||||||
"umi-presets-pro": "^2.0.3"
|
"umi-presets-pro": "^2.0.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
Generated
+13
@@ -53,6 +53,9 @@ dependencies:
|
|||||||
query-string:
|
query-string:
|
||||||
specifier: ^9.0.0
|
specifier: ^9.0.0
|
||||||
version: 9.0.0
|
version: 9.0.0
|
||||||
|
react-hotkeys-hook:
|
||||||
|
specifier: ^4.5.0
|
||||||
|
version: 4.5.0(react-dom@18.3.1)(react@18.3.1)
|
||||||
umi-presets-pro:
|
umi-presets-pro:
|
||||||
specifier: ^2.0.3
|
specifier: ^2.0.3
|
||||||
version: 2.0.3(@babel/core@7.24.5)(@types/react-dom@18.3.0)(@types/react@18.3.1)(antd@5.17.0)(dva@2.5.0-beta.2)(rc-field-form@1.44.0)(react-dom@18.3.1)(react@18.3.1)(umi@4.2.1)
|
version: 2.0.3(@babel/core@7.24.5)(@types/react-dom@18.3.0)(@types/react@18.3.1)(antd@5.17.0)(dva@2.5.0-beta.2)(rc-field-form@1.44.0)(react-dom@18.3.1)(react@18.3.1)(umi@4.2.1)
|
||||||
@@ -14203,6 +14206,16 @@ packages:
|
|||||||
shallowequal: 1.1.0
|
shallowequal: 1.1.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/react-hotkeys-hook@4.5.0(react-dom@18.3.1)(react@18.3.1):
|
||||||
|
resolution: {integrity: sha512-Samb85GSgAWFQNvVt3PS90LPPGSf9mkH/r4au81ZP1yOIFayLC3QAvqTgGtJ8YEDMXtPmaVBs6NgipHO6h4Mug==, tarball: https://registry.npmjs.org/react-hotkeys-hook/-/react-hotkeys-hook-4.5.0.tgz}
|
||||||
|
peerDependencies:
|
||||||
|
react: '>=16.8.1'
|
||||||
|
react-dom: '>=16.8.1'
|
||||||
|
dependencies:
|
||||||
|
react: 18.3.1
|
||||||
|
react-dom: 18.3.1(react@18.3.1)
|
||||||
|
dev: false
|
||||||
|
|
||||||
/react-intl@3.12.1(react@18.3.1):
|
/react-intl@3.12.1(react@18.3.1):
|
||||||
resolution: {integrity: sha512-cgumW29mwROIqyp8NXStYsoIm27+8FqnxykiLSawWjOxGIBeLuN/+p2srei5SRIumcJefOkOIHP+NDck05RgHg==}
|
resolution: {integrity: sha512-cgumW29mwROIqyp8NXStYsoIm27+8FqnxykiLSawWjOxGIBeLuN/+p2srei5SRIumcJefOkOIHP+NDck05RgHg==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
|
|||||||
@@ -27,3 +27,11 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.opct-7 {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opct-8 {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|||||||
+12
-11
@@ -1,13 +1,14 @@
|
|||||||
export default {
|
export default {
|
||||||
Create: ['ctrl+n', 'cmd+n'],
|
CREATE: ['ctrl+n', 'meta+n'],
|
||||||
Save: ['ctrl+s', 'cmd+s'],
|
SAVE: ['ctrl+s', 'meta+s'],
|
||||||
SaveAs: ['ctrl+shift+s', 'cmd+shift+s'],
|
SUBMIT: ['ctrl+enter', 'meta+enter'],
|
||||||
Open: ['ctrl+o', 'cmd+o'],
|
SAVEAS: ['ctrl+shift+s', 'meta+shift+s'],
|
||||||
Cancel: ['ctrl+w', 'cmd+w'],
|
OPEN: ['ctrl+o', 'meta+o'],
|
||||||
Delete: ['delete'],
|
CANCEL: ['ctrl+w', 'meta+w'],
|
||||||
Copy: ['ctrl+c', 'cmd+c'],
|
DELETE: ['delete'],
|
||||||
Refresh: ['ctrl+r', 'cmd+r'],
|
COPY: ['ctrl+c', 'meta+c'],
|
||||||
Edit: ['ctrl+e', 'cmd+e'],
|
REFRESH: ['ctrl+r', 'meta+r'],
|
||||||
Search: ['ctrl+f', 'cmd+f'],
|
EDIT: ['ctrl+e', 'meta+e'],
|
||||||
Reset: ['ctrl+shift+r', 'cmd+shift+r']
|
SEARCH: ['ctrl+f', 'meta+f'],
|
||||||
|
RESET: ['ctrl+shift+r', 'meta+shift+r']
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -26,8 +26,8 @@ export interface ModelInstanceListItem {
|
|||||||
huggingface_repo_id: string;
|
huggingface_repo_id: string;
|
||||||
huggingface_filename: string;
|
huggingface_filename: string;
|
||||||
s3_address: string;
|
s3_address: string;
|
||||||
node_id: number;
|
worker_id: number;
|
||||||
node_ip: string;
|
worker_ip: string;
|
||||||
pid: number;
|
pid: number;
|
||||||
port: number;
|
port: number;
|
||||||
state: string;
|
state: string;
|
||||||
|
|||||||
@@ -338,7 +338,7 @@ const Models: React.FC = () => {
|
|||||||
<RowChildren>
|
<RowChildren>
|
||||||
<Row style={{ width: '100%' }} align="middle">
|
<Row style={{ width: '100%' }} align="middle">
|
||||||
<Col span={4}>
|
<Col span={4}>
|
||||||
{item.node_ip}:{item.port}
|
{item.worker_ip}:{item.port}
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={5}>
|
<Col span={5}>
|
||||||
<span>{item.huggingface_filename}</span>
|
<span>{item.huggingface_filename}</span>
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
|
import HotKeys from '@/config/hotkeys';
|
||||||
import {
|
import {
|
||||||
CodeOutlined,
|
CodeOutlined,
|
||||||
DeleteOutlined,
|
DeleteOutlined,
|
||||||
PlusOutlined,
|
EnterOutlined,
|
||||||
SaveOutlined
|
MacCommandOutlined,
|
||||||
|
PlusOutlined
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import { useIntl } from '@umijs/max';
|
import { useIntl } from '@umijs/max';
|
||||||
import { Button, Col, Row, Space } from 'antd';
|
import { Button, Col, Row, Space } from 'antd';
|
||||||
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
import '../style/chat-footer.less';
|
import '../style/chat-footer.less';
|
||||||
|
|
||||||
interface ChatFooterProps {
|
interface ChatFooterProps {
|
||||||
@@ -20,6 +23,14 @@ interface ChatFooterProps {
|
|||||||
const ChatFooter: React.FC<ChatFooterProps> = (props) => {
|
const ChatFooter: React.FC<ChatFooterProps> = (props) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const { onSubmit, onClear, onNewMessage, onView, feedback, disabled } = props;
|
const { onSubmit, onClear, onNewMessage, onView, feedback, disabled } = props;
|
||||||
|
useHotkeys(
|
||||||
|
HotKeys.SUBMIT.join(','),
|
||||||
|
() => {
|
||||||
|
onSubmit();
|
||||||
|
},
|
||||||
|
{ enabled: !disabled }
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="chat-footer">
|
<div className="chat-footer">
|
||||||
<Row style={{ width: '100%' }}>
|
<Row style={{ width: '100%' }}>
|
||||||
@@ -52,13 +63,11 @@ const ChatFooter: React.FC<ChatFooterProps> = (props) => {
|
|||||||
>
|
>
|
||||||
{intl.formatMessage({ id: 'playground.viewcode' })}
|
{intl.formatMessage({ id: 'playground.viewcode' })}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button disabled={disabled} type="primary" onClick={onSubmit}>
|
||||||
disabled={disabled}
|
|
||||||
type="primary"
|
|
||||||
icon={<SaveOutlined></SaveOutlined>}
|
|
||||||
onClick={onSubmit}
|
|
||||||
>
|
|
||||||
{intl.formatMessage({ id: 'common.button.submit' })}
|
{intl.formatMessage({ id: 'common.button.submit' })}
|
||||||
|
<span className="m-l-5 opct-7">
|
||||||
|
<MacCommandOutlined /> + <EnterOutlined />
|
||||||
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
</Space>
|
</Space>
|
||||||
</Col>
|
</Col>
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import TransitionWrapper from '@/components/transition';
|
import TransitionWrapper from '@/components/transition';
|
||||||
|
import HotKeys from '@/config/hotkeys';
|
||||||
import { EyeInvisibleOutlined } from '@ant-design/icons';
|
import { EyeInvisibleOutlined } from '@ant-design/icons';
|
||||||
import { PageContainer } from '@ant-design/pro-components';
|
import { PageContainer } from '@ant-design/pro-components';
|
||||||
import { useIntl } from '@umijs/max';
|
import { useIntl } from '@umijs/max';
|
||||||
import { Button, Input, Spin } from 'antd';
|
import { Button, Input, Spin } from 'antd';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { useRef, useState } from 'react';
|
import { useRef, useState } from 'react';
|
||||||
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
import { fetchChatStream, receiveChatStream } from '../apis';
|
import { fetchChatStream, receiveChatStream } from '../apis';
|
||||||
import { Roles } from '../config';
|
import { Roles } from '../config';
|
||||||
import '../style/ground-left.less';
|
import '../style/ground-left.less';
|
||||||
@@ -13,6 +15,7 @@ import ChatFooter from './chat-footer';
|
|||||||
import MessageItem from './message-item';
|
import MessageItem from './message-item';
|
||||||
import ReferenceParams from './reference-params';
|
import ReferenceParams from './reference-params';
|
||||||
import ViewCodeModal from './view-code-modal';
|
import ViewCodeModal from './view-code-modal';
|
||||||
|
|
||||||
interface MessageProps {
|
interface MessageProps {
|
||||||
parameters: any;
|
parameters: any;
|
||||||
}
|
}
|
||||||
@@ -40,6 +43,7 @@ const MessageList: React.FC<MessageProps> = (props) => {
|
|||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [activeIndex, setActiveIndex] = useState(-1);
|
const [activeIndex, setActiveIndex] = useState(-1);
|
||||||
const [tokenResult, setTokenResult] = useState<any>(null);
|
const [tokenResult, setTokenResult] = useState<any>(null);
|
||||||
|
const [currentIsFocus, setCurrentIsFocus] = useState(false);
|
||||||
const systemRef = useRef<any>(null);
|
const systemRef = useRef<any>(null);
|
||||||
const contentRef = useRef<any>('');
|
const contentRef = useRef<any>('');
|
||||||
|
|
||||||
@@ -153,6 +157,27 @@ const MessageList: React.FC<MessageProps> = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleFocus = () => {
|
||||||
|
setCurrentIsFocus(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBlur = () => {
|
||||||
|
setCurrentIsFocus(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
useHotkeys(
|
||||||
|
HotKeys.SUBMIT,
|
||||||
|
() => {
|
||||||
|
handleSubmit();
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: currentIsFocus && !loading,
|
||||||
|
enableOnFormTags: currentIsFocus && !loading,
|
||||||
|
preventDefault: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="ground-left">
|
<div className="ground-left">
|
||||||
<PageContainer title={false} className="message-list-wrap">
|
<PageContainer title={false} className="message-list-wrap">
|
||||||
@@ -166,6 +191,8 @@ const MessageList: React.FC<MessageProps> = (props) => {
|
|||||||
value={systemMessage}
|
value={systemMessage}
|
||||||
variant="filled"
|
variant="filled"
|
||||||
autoSize={true}
|
autoSize={true}
|
||||||
|
onFocus={handleFocus}
|
||||||
|
onBlur={handleBlur}
|
||||||
placeholder={intl.formatMessage({ id: 'playground.system.tips' })}
|
placeholder={intl.formatMessage({ id: 'playground.system.tips' })}
|
||||||
onChange={handleSystemMessageChange}
|
onChange={handleSystemMessageChange}
|
||||||
></Input.TextArea>
|
></Input.TextArea>
|
||||||
@@ -184,6 +211,7 @@ const MessageList: React.FC<MessageProps> = (props) => {
|
|||||||
updateMessage={(message: MessageItemProps) =>
|
updateMessage={(message: MessageItemProps) =>
|
||||||
handleUpdateMessage(index, message)
|
handleUpdateMessage(index, message)
|
||||||
}
|
}
|
||||||
|
onSubmit={handleSubmit}
|
||||||
message={item}
|
message={item}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
|
import HotKeys from '@/config/hotkeys';
|
||||||
import { MinusCircleOutlined } from '@ant-design/icons';
|
import { MinusCircleOutlined } from '@ant-design/icons';
|
||||||
import { useIntl } from '@umijs/max';
|
import { useIntl } from '@umijs/max';
|
||||||
import { Button, Input } from 'antd';
|
import { Button, Input } from 'antd';
|
||||||
import { memo, useEffect, useRef, useState } from 'react';
|
import { memo, useEffect, useRef, useState } from 'react';
|
||||||
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
import { Roles } from '../config';
|
import { Roles } from '../config';
|
||||||
import '../style/message-item.less';
|
import '../style/message-item.less';
|
||||||
interface MessageItemProps {
|
interface MessageItemProps {
|
||||||
@@ -14,18 +16,32 @@ const MessageItem: React.FC<{
|
|||||||
message: MessageItemProps;
|
message: MessageItemProps;
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
islast?: boolean;
|
islast?: boolean;
|
||||||
|
onSubmit: () => void;
|
||||||
updateMessage: (message: MessageItemProps) => void;
|
updateMessage: (message: MessageItemProps) => void;
|
||||||
isFocus: boolean;
|
isFocus: boolean;
|
||||||
onDelete: () => void;
|
onDelete: () => void;
|
||||||
}> = ({ message, isFocus, onDelete, updateMessage }) => {
|
}> = ({ message, isFocus, onDelete, updateMessage, onSubmit, loading }) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const [roleType, setRoleType] = useState(message.role);
|
const [roleType, setRoleType] = useState(message.role);
|
||||||
const [isTyping, setIsTyping] = useState(false);
|
const [isTyping, setIsTyping] = useState(false);
|
||||||
const [messageContent, setMessageContent] = useState(message.content);
|
const [messageContent, setMessageContent] = useState(message.content);
|
||||||
const isInitialRender = useRef(true);
|
const isInitialRender = useRef(true);
|
||||||
const [isAnimating, setIsAnimating] = useState(false);
|
const [isAnimating, setIsAnimating] = useState(false);
|
||||||
|
const [currentIsFocus, setCurrentIsFocus] = useState(isFocus);
|
||||||
const inputRef = useRef<any>(null);
|
const inputRef = useRef<any>(null);
|
||||||
|
|
||||||
|
useHotkeys(
|
||||||
|
HotKeys.SUBMIT,
|
||||||
|
() => {
|
||||||
|
onSubmit();
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: currentIsFocus && !loading,
|
||||||
|
enableOnFormTags: currentIsFocus && !loading,
|
||||||
|
preventDefault: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (inputRef.current && isFocus) {
|
if (inputRef.current && isFocus) {
|
||||||
inputRef.current.focus();
|
inputRef.current.focus();
|
||||||
@@ -75,6 +91,11 @@ const MessageItem: React.FC<{
|
|||||||
|
|
||||||
const handleBlur = () => {
|
const handleBlur = () => {
|
||||||
setIsTyping(true);
|
setIsTyping(true);
|
||||||
|
setCurrentIsFocus(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFocus = () => {
|
||||||
|
setCurrentIsFocus(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRoleChange = () => {
|
const handleRoleChange = () => {
|
||||||
@@ -103,6 +124,7 @@ const MessageItem: React.FC<{
|
|||||||
autoSize={true}
|
autoSize={true}
|
||||||
variant="filled"
|
variant="filled"
|
||||||
onChange={handleMessageChange}
|
onChange={handleMessageChange}
|
||||||
|
onFocus={handleFocus}
|
||||||
onBlur={handleBlur}
|
onBlur={handleBlur}
|
||||||
></Input.TextArea>
|
></Input.TextArea>
|
||||||
</div>
|
</div>
|
||||||
@@ -110,6 +132,7 @@ const MessageItem: React.FC<{
|
|||||||
<Button
|
<Button
|
||||||
type="text"
|
type="text"
|
||||||
shape="circle"
|
shape="circle"
|
||||||
|
size="small"
|
||||||
style={{ color: 'var(--ant-color-primary)' }}
|
style={{ color: 'var(--ant-color-primary)' }}
|
||||||
onClick={handleDelete}
|
onClick={handleDelete}
|
||||||
icon={<MinusCircleOutlined />}
|
icon={<MinusCircleOutlined />}
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { request } from '@umijs/max';
|
import { request } from '@umijs/max';
|
||||||
import { ListItem } from '../config/types';
|
import { ListItem } from '../config/types';
|
||||||
|
|
||||||
export const NODES_API = '/nodes';
|
export const WORKERS_API = '/workers';
|
||||||
|
|
||||||
export async function queryNodesList(
|
export async function queryWorkersList(
|
||||||
params: Global.Pagination & { query?: string }
|
params: Global.Pagination & { query?: string }
|
||||||
) {
|
) {
|
||||||
return request<Global.PageResponse<ListItem>>(`${NODES_API}`, {
|
return request<Global.PageResponse<ListItem>>(`${WORKERS_API}`, {
|
||||||
methos: 'GET',
|
methos: 'GET',
|
||||||
params
|
params
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { useIntl } from '@umijs/max';
|
|||||||
import { Button, Input, Space, Table } from 'antd';
|
import { Button, Input, Space, Table } from 'antd';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { queryNodesList } from '../apis';
|
import { queryWorkersList } from '../apis';
|
||||||
import { ListItem } from '../config/types';
|
import { ListItem } from '../config/types';
|
||||||
const { Column } = Table;
|
const { Column } = Table;
|
||||||
|
|
||||||
@@ -33,8 +33,8 @@ const Models: React.FC = () => {
|
|||||||
const params = {
|
const params = {
|
||||||
..._.pickBy(queryParams, (val: any) => !!val)
|
..._.pickBy(queryParams, (val: any) => !!val)
|
||||||
};
|
};
|
||||||
const res = await queryNodesList(params);
|
const res = await queryWorkersList(params);
|
||||||
console.log('res=======', res);
|
|
||||||
setDataSource(res.items);
|
setDataSource(res.items);
|
||||||
setTotal(res.pagination.total);
|
setTotal(res.pagination.total);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -131,15 +131,16 @@ const Models: React.FC = () => {
|
|||||||
return (
|
return (
|
||||||
<StatusTag
|
<StatusTag
|
||||||
statusValue={{
|
statusValue={{
|
||||||
status:
|
status: ['Inactive', 'unknown'].includes(record.state)
|
||||||
record.status?.state === 'active' ? 'success' : 'error',
|
? 'inactive'
|
||||||
text: record.status?.state
|
: 'success',
|
||||||
|
text: record.state
|
||||||
}}
|
}}
|
||||||
></StatusTag>
|
></StatusTag>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Column title="IP" dataIndex="address" key="address" />
|
<Column title="IP" dataIndex="ip" key="address" />
|
||||||
<Column
|
<Column
|
||||||
title="CPU"
|
title="CPU"
|
||||||
dataIndex="CPU"
|
dataIndex="CPU"
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ export interface ListItem {
|
|||||||
hostname: string;
|
hostname: string;
|
||||||
address: string;
|
address: string;
|
||||||
labels: object;
|
labels: object;
|
||||||
|
state: string;
|
||||||
|
ip: string;
|
||||||
status: {
|
status: {
|
||||||
cpu: {
|
cpu: {
|
||||||
total: number;
|
total: number;
|
||||||
@@ -60,7 +62,6 @@ export interface ListItem {
|
|||||||
uptime: number;
|
uptime: number;
|
||||||
boot_time: string;
|
boot_time: string;
|
||||||
};
|
};
|
||||||
state: string;
|
|
||||||
};
|
};
|
||||||
id: number;
|
id: number;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ import Nodes from './components/nodes';
|
|||||||
|
|
||||||
const items: TabsProps['items'] = [
|
const items: TabsProps['items'] = [
|
||||||
{
|
{
|
||||||
key: 'nodes',
|
key: 'workers',
|
||||||
label: 'Nodes',
|
label: 'Workers',
|
||||||
children: <Nodes />
|
children: <Nodes />
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -38,7 +38,7 @@ const Resources = () => {
|
|||||||
<div style={{ marginTop: 70 }}>
|
<div style={{ marginTop: 70 }}>
|
||||||
<Tabs
|
<Tabs
|
||||||
type="card"
|
type="card"
|
||||||
defaultActiveKey="nodes"
|
defaultActiveKey="workers"
|
||||||
items={items}
|
items={items}
|
||||||
accessKey={activeKey}
|
accessKey={activeKey}
|
||||||
onChange={handleChangeTab}
|
onChange={handleChangeTab}
|
||||||
|
|||||||
Reference in New Issue
Block a user