feat: nodes list
This commit is contained in:
+3
-1
@@ -13,10 +13,12 @@
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^5.3.7",
|
||||
"@ant-design/pro-components": "^2.7.1",
|
||||
"@types/lodash": "^4.17.4",
|
||||
"@umijs/max": "^4.2.1",
|
||||
"antd": "^5.17.0",
|
||||
"antd-style": "^3.6.2",
|
||||
"classnames": "^2.5.1"
|
||||
"classnames": "^2.5.1",
|
||||
"lodash": "^4.17.21"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.3.1",
|
||||
|
||||
Generated
+10
@@ -11,6 +11,9 @@ dependencies:
|
||||
'@ant-design/pro-components':
|
||||
specifier: ^2.7.1
|
||||
version: 2.7.1(antd@5.17.0)(rc-field-form@1.44.0)(react-dom@18.3.1)(react@18.3.1)
|
||||
'@types/lodash':
|
||||
specifier: ^4.17.4
|
||||
version: 4.17.4
|
||||
'@umijs/max':
|
||||
specifier: ^4.2.1
|
||||
version: 4.2.1(@babel/core@7.24.5)(@types/react-dom@18.3.0)(@types/react@18.3.1)(dva@2.5.0-beta.2)(prettier@3.2.5)(rc-field-form@1.44.0)(react-dom@18.3.1)(react@18.3.1)(typescript@5.4.5)(webpack@5.91.0)
|
||||
@@ -23,6 +26,9 @@ dependencies:
|
||||
classnames:
|
||||
specifier: ^2.5.1
|
||||
version: 2.5.1
|
||||
lodash:
|
||||
specifier: ^4.17.21
|
||||
version: 4.17.21
|
||||
|
||||
devDependencies:
|
||||
'@types/react':
|
||||
@@ -2611,6 +2617,10 @@ packages:
|
||||
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
|
||||
dev: false
|
||||
|
||||
/@types/lodash@4.17.4:
|
||||
resolution: {integrity: sha512-wYCP26ZLxaT3R39kiN2+HcJ4kTd3U1waI/cY7ivWYqFP6pW3ZNpvi6Wd6PHZx7T/t8z0vlkXMg3QYLa7DZ/IJQ==}
|
||||
dev: false
|
||||
|
||||
/@types/minimist@1.2.5:
|
||||
resolution: {integrity: sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==}
|
||||
dev: false
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@
|
||||
// 全局初始化数据配置,用于 Layout 用户信息和权限初始化
|
||||
// 更多信息见文档:https://umijs.org/docs/api/runtime-config#getinitialstate
|
||||
export async function getInitialState(): Promise<{ name: string }> {
|
||||
return { name: 'admin' };
|
||||
return { name: 'admin', logout: true };
|
||||
}
|
||||
|
||||
// export const layout: RunTimeLayoutConfig = () => {
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
.status-tag {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 2px 10px;
|
||||
border-radius: 20px;
|
||||
width: fit-content;
|
||||
font-size: var(--font-size-base);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { StatusColorMap } from '@/config';
|
||||
import { StatusType } from '@/config/types';
|
||||
import { useEffect, useState } from 'react';
|
||||
import styles from './index.less';
|
||||
|
||||
export const StatusMaps = {
|
||||
running: 'blue',
|
||||
error: 'red',
|
||||
warning: 'orange',
|
||||
success: 'success'
|
||||
};
|
||||
|
||||
type StatusTagProps = {
|
||||
statusValue: {
|
||||
status: StatusType;
|
||||
text: string;
|
||||
};
|
||||
};
|
||||
|
||||
const StatusTag: React.FC<StatusTagProps> = ({ statusValue }) => {
|
||||
const { text, status } = statusValue;
|
||||
const [statusColor, setStatusColor] = useState<{ text: string; bg: string }>({
|
||||
text: '',
|
||||
bg: ''
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setStatusColor(StatusColorMap[status]);
|
||||
}, [status]);
|
||||
|
||||
return (
|
||||
<span
|
||||
className={styles['status-tag']}
|
||||
style={{
|
||||
color: statusColor.text,
|
||||
border: `1px solid ${statusColor.text}`
|
||||
}}
|
||||
>
|
||||
{text}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
export default StatusTag;
|
||||
+25
-1
@@ -1,7 +1,31 @@
|
||||
import { PageActionType } from './types';
|
||||
import { PageActionType, StatusType } from './types';
|
||||
|
||||
export const PageAction: Record<string, PageActionType> = {
|
||||
CREATE: 'create',
|
||||
UPDATE: 'update',
|
||||
VIEW: 'view'
|
||||
};
|
||||
|
||||
export const StatusColorMap: Record<StatusType, { text: string; bg: string }> =
|
||||
{
|
||||
error: {
|
||||
text: `var(--ant-red-6)`,
|
||||
bg: `var(--ant-red-1)`
|
||||
},
|
||||
warning: {
|
||||
text: `var(--ant-orange-6)`,
|
||||
bg: `var(--ant-orange-1)`
|
||||
},
|
||||
transitioning: {
|
||||
text: `var(--ant-blue-6)`,
|
||||
bg: `var(--ant-blue-1)`
|
||||
},
|
||||
success: {
|
||||
text: `var(--ant-color-primary)`,
|
||||
bg: `var(--ant-green-1)`
|
||||
},
|
||||
inactive: {
|
||||
text: `var(--ant-color-border)`,
|
||||
bg: `var(--ant-color-fill)`
|
||||
}
|
||||
};
|
||||
|
||||
@@ -7,3 +7,10 @@ export interface DropDownItem {
|
||||
}
|
||||
|
||||
export type PageActionType = 'create' | 'update' | 'view';
|
||||
|
||||
export type StatusType =
|
||||
| 'error'
|
||||
| 'warning'
|
||||
| 'transitioning'
|
||||
| 'success'
|
||||
| 'inactive';
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@ html {
|
||||
--color-white-1: #fff;
|
||||
--font-weight-normal: 500;
|
||||
--font-weight-bold: 700;
|
||||
--color-text-1: #1d2129;
|
||||
--color-text-1: var(--ant-color-text);
|
||||
--color-bg-light-1: #f0fff6;
|
||||
--font-size-base: 12px;
|
||||
--ant-color-fill-secondary: rgba(0, 0, 0, 0.06);
|
||||
|
||||
+17
-23
@@ -12,19 +12,19 @@ import {
|
||||
useAppData,
|
||||
useLocation,
|
||||
useNavigate,
|
||||
type IRoute,
|
||||
type IRoute
|
||||
} from '@umijs/max';
|
||||
import { useMemo } from 'react';
|
||||
import Exception from './Exception';
|
||||
import './Layout.css';
|
||||
import Logo from './Logo';
|
||||
import { getRightRenderContent } from './rightRender';
|
||||
import { renderMenuIcon,patchRoutes } from './runtime';
|
||||
import { patchRoutes } from './runtime';
|
||||
|
||||
// 过滤出需要显示的路由, 这里的filterFn 指 不希望显示的层级
|
||||
const filterRoutes = (
|
||||
routes: IRoute[],
|
||||
filterFn: (route: IRoute) => boolean,
|
||||
filterFn: (route: IRoute) => boolean
|
||||
) => {
|
||||
if (routes.length === 0) {
|
||||
return [];
|
||||
@@ -78,34 +78,33 @@ export default (props: any) => {
|
||||
const navigate = useNavigate();
|
||||
const { clientRoutes, pluginManager } = useAppData();
|
||||
|
||||
console.log('pluginManager===========', pluginManager);
|
||||
|
||||
const initialInfo = (useModel && useModel('@@initialState')) || {
|
||||
initialState: undefined,
|
||||
loading: false,
|
||||
setInitialState: null,
|
||||
setInitialState: null
|
||||
};
|
||||
const { initialState, loading, setInitialState } = initialInfo;
|
||||
|
||||
const userConfig = {
|
||||
title: '',
|
||||
layout: 'mix',
|
||||
layout: 'mix'
|
||||
};
|
||||
|
||||
const formatMessage = undefined;
|
||||
const runtimeConfig = pluginManager.applyPlugins({
|
||||
key: 'layout',
|
||||
type: 'modify',
|
||||
logout: true,
|
||||
initialValue: {
|
||||
...initialInfo,
|
||||
notFound: <div>not found</div>
|
||||
},
|
||||
}
|
||||
});
|
||||
console.log(
|
||||
'clientRoute===========',
|
||||
clientRoutes,
|
||||
runtimeConfig,
|
||||
initialInfo,
|
||||
initialInfo
|
||||
);
|
||||
|
||||
// 现在的 layout 及 wrapper 实现是通过父路由的形式实现的, 会导致路由数据多了冗余层级, proLayout 消费时, 无法正确展示菜单, 这里对冗余数据进行过滤操作
|
||||
@@ -116,18 +115,17 @@ export default (props: any) => {
|
||||
(!!route.isLayout && route.id !== '@@/global-layout') ||
|
||||
!!route.isWrapper
|
||||
);
|
||||
},
|
||||
}
|
||||
);
|
||||
console.log('clientRoutes===========', clientRoutes, newRoutes)
|
||||
console.log('clientRoutes===========', clientRoutes, newRoutes);
|
||||
const [route] = useAccessMarkedRoutes(mapRoutes(newRoutes));
|
||||
patchRoutes({ routes: route?.children || [] });
|
||||
|
||||
|
||||
const matchedRoute = useMemo(
|
||||
() => matchRoutes(route?.children || [], location.pathname)?.pop?.()?.route,
|
||||
[location.pathname],
|
||||
[location.pathname]
|
||||
);
|
||||
console.log('route===========', route)
|
||||
console.log('route===========', route);
|
||||
return (
|
||||
<ProLayout
|
||||
route={route}
|
||||
@@ -147,7 +145,7 @@ console.log('route===========', route)
|
||||
menu={{ locale: userConfig.locale }}
|
||||
logo={Logo}
|
||||
menuItemRender={(menuItemProps, defaultDom) => {
|
||||
console.log('meurender=========',{ defaultDom})
|
||||
console.log('meurender=========', { defaultDom });
|
||||
if (menuItemProps.isUrl || menuItemProps.children) {
|
||||
return defaultDom;
|
||||
}
|
||||
@@ -158,15 +156,11 @@ console.log('route===========', route)
|
||||
to={menuItemProps.path.replace('/*', '')}
|
||||
target={menuItemProps.target}
|
||||
>
|
||||
{defaultDom}
|
||||
{defaultDom}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
{defaultDom}
|
||||
</>
|
||||
);
|
||||
return <>{defaultDom}</>;
|
||||
}}
|
||||
itemRender={(route, _, routes) => {
|
||||
const { breadcrumbName, title, path } = route;
|
||||
@@ -190,7 +184,7 @@ console.log('route===========', route)
|
||||
runtimeConfig,
|
||||
loading,
|
||||
initialState,
|
||||
setInitialState,
|
||||
setInitialState
|
||||
});
|
||||
if (runtimeConfig.rightContentRender) {
|
||||
return runtimeConfig.rightContentRender(layoutProps, dom, {
|
||||
@@ -199,7 +193,7 @@ console.log('route===========', route)
|
||||
runtimeConfig,
|
||||
loading,
|
||||
initialState,
|
||||
setInitialState,
|
||||
setInitialState
|
||||
});
|
||||
}
|
||||
return dom;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// @ts-nocheck
|
||||
// This file is generated by Umi automatically
|
||||
// DO NOT CHANGE IT MANUALLY!
|
||||
import avatarImg from '@/assets/images/avatar.png';
|
||||
import { LogoutOutlined } from '@ant-design/icons';
|
||||
import { Avatar, Dropdown, Menu, Spin, version } from 'antd';
|
||||
|
||||
@@ -14,7 +15,7 @@ export function getRightRenderContent(opts: {
|
||||
return opts.runtimeConfig.rightRender(
|
||||
opts.initialState,
|
||||
opts.setInitialState,
|
||||
opts.runtimeConfig,
|
||||
opts.runtimeConfig
|
||||
);
|
||||
}
|
||||
|
||||
@@ -32,10 +33,7 @@ export function getRightRenderContent(opts: {
|
||||
<Avatar
|
||||
size="small"
|
||||
className="umi-plugin-layout-avatar"
|
||||
src={
|
||||
opts.initialState?.avatar ||
|
||||
'https://gw.alipayobjects.com/zos/antfincdn/XAosXuNZyF/BiazfanxmamNRoxxVxka.png'
|
||||
}
|
||||
src={opts.initialState?.avatar || avatarImg}
|
||||
alt="avatar"
|
||||
/>
|
||||
) : null}
|
||||
@@ -68,9 +66,9 @@ export function getRightRenderContent(opts: {
|
||||
),
|
||||
onClick: () => {
|
||||
opts?.runtimeConfig?.logout?.(opts.initialState);
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
// antd@5 和 4.24 之后推荐使用 menu,性能更好
|
||||
let dropdownProps;
|
||||
@@ -86,7 +84,7 @@ export function getRightRenderContent(opts: {
|
||||
</Menu.Item>
|
||||
))}
|
||||
</Menu>
|
||||
),
|
||||
)
|
||||
};
|
||||
} else {
|
||||
// 需要 antd 4.20.0 以上版本
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
export interface Capacity {
|
||||
cpu: number;
|
||||
gpu: number;
|
||||
memory: string;
|
||||
gram: string;
|
||||
}
|
||||
|
||||
export interface NodeItem {
|
||||
id: number;
|
||||
name: string;
|
||||
address: string;
|
||||
hostname: string;
|
||||
labels: object;
|
||||
resources: {
|
||||
capacity: Capacity;
|
||||
allocable: Capacity;
|
||||
};
|
||||
state: string;
|
||||
}
|
||||
+271
-10
@@ -1,16 +1,277 @@
|
||||
import PageTools from '@/components/page-tools';
|
||||
import StatusTag from '@/components/status-tag';
|
||||
import useTableRowSelection from '@/hooks/use-table-row-selection';
|
||||
import useTableSort from '@/hooks/use-table-sort';
|
||||
import { SyncOutlined } from '@ant-design/icons';
|
||||
import { PageContainer } from '@ant-design/pro-components';
|
||||
import { Button, Input, Progress, Space, Table } from 'antd';
|
||||
import _ from 'lodash';
|
||||
import { memo, useMemo, useState } from 'react';
|
||||
import { NodeItem } from './config/types';
|
||||
const { Column } = Table;
|
||||
|
||||
const dataSource: NodeItem[] = [
|
||||
{
|
||||
id: 1,
|
||||
name: '192.168.1.2',
|
||||
address: '192.168.1.2',
|
||||
hostname: 'node-1',
|
||||
labels: {},
|
||||
resources: {
|
||||
capacity: {
|
||||
cpu: 4,
|
||||
gpu: 2,
|
||||
memory: '64 GiB',
|
||||
gram: '24 Gib'
|
||||
},
|
||||
allocable: {
|
||||
cpu: 2.5,
|
||||
gpu: 1.6,
|
||||
memory: '64',
|
||||
gram: '24 Gib'
|
||||
}
|
||||
},
|
||||
state: 'ALIVE'
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
name: '192.168.1.2',
|
||||
address: '192.168.1.5',
|
||||
hostname: 'node-2',
|
||||
labels: {},
|
||||
resources: {
|
||||
capacity: {
|
||||
cpu: 4,
|
||||
gpu: 2,
|
||||
memory: '64 GiB',
|
||||
gram: '24 Gib'
|
||||
},
|
||||
allocable: {
|
||||
cpu: 2,
|
||||
gpu: 1.5,
|
||||
memory: '32 GiB',
|
||||
gram: '12 Gib'
|
||||
}
|
||||
},
|
||||
state: 'ALIVE'
|
||||
}
|
||||
];
|
||||
|
||||
const Models: React.FC = () => {
|
||||
const rowSelection = useTableRowSelection();
|
||||
const { sortOrder, setSortOrder } = useTableSort({
|
||||
defaultSortOrder: 'descend'
|
||||
});
|
||||
const [total, setTotal] = useState(100);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [queryParams, setQueryParams] = useState({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
name: ''
|
||||
});
|
||||
const handleShowSizeChange = (current: number, size: number) => {
|
||||
console.log(current, size);
|
||||
};
|
||||
|
||||
const handlePageChange = (page: number, pageSize: number | undefined) => {
|
||||
console.log(page, pageSize);
|
||||
};
|
||||
|
||||
const handleTableChange = (pagination: any, filters: any, sorter: any) => {
|
||||
setSortOrder(sorter.order);
|
||||
};
|
||||
|
||||
const fetchData = async () => {
|
||||
console.log('fetchData');
|
||||
};
|
||||
const handleSearch = (e: any) => {
|
||||
fetchData();
|
||||
};
|
||||
|
||||
const handleNameChange = (e: any) => {
|
||||
setQueryParams({
|
||||
...queryParams,
|
||||
name: e.target.value
|
||||
});
|
||||
};
|
||||
|
||||
const handleClickMenu = (e: any) => {
|
||||
console.log('click', e);
|
||||
};
|
||||
|
||||
const RenderProgress = memo(
|
||||
(props: { record: NodeItem; dataIndex: string }) => {
|
||||
const { record, dataIndex } = props;
|
||||
const value1 = useMemo(() => {
|
||||
let value = _.get(record, ['resources', 'allocable', dataIndex]);
|
||||
if (['gram', 'memory'].includes(dataIndex)) {
|
||||
value = _.toNumber(value.replace(/GiB|Gib/, ''));
|
||||
}
|
||||
return value;
|
||||
}, [record, dataIndex]);
|
||||
|
||||
const value2 = useMemo(() => {
|
||||
let value = _.get(record, ['resources', 'capacity', dataIndex]);
|
||||
if (['gram', 'memory'].includes(dataIndex)) {
|
||||
value = _.toNumber(value.replace(/GiB|Gib/, ''));
|
||||
}
|
||||
return value;
|
||||
}, [record, dataIndex]);
|
||||
|
||||
if (!value1 || !value2) {
|
||||
return <Progress percent={0} strokeColor="var(--ant-color-primary)" />;
|
||||
}
|
||||
const percent = _.round(value1 / value2, 2) * 100;
|
||||
const strokeColor = useMemo(() => {
|
||||
if (percent <= 50) {
|
||||
return 'var(--ant-color-primary)';
|
||||
}
|
||||
if (percent <= 80) {
|
||||
return 'var(--ant-color-warning)';
|
||||
}
|
||||
return 'var(--ant-color-error)';
|
||||
}, [percent]);
|
||||
return (
|
||||
<Progress
|
||||
steps={5}
|
||||
format={() => {
|
||||
return (
|
||||
<span style={{ color: 'var(--ant-color-text)' }}>{percent}%</span>
|
||||
);
|
||||
}}
|
||||
percent={percent}
|
||||
strokeColor={strokeColor}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
const Dashboard: React.FC = () => {
|
||||
return (
|
||||
<PageContainer
|
||||
ghost
|
||||
header={{
|
||||
title: 'Dashboard',
|
||||
}}
|
||||
>
|
||||
<div>Nodes</div>
|
||||
</PageContainer>
|
||||
<>
|
||||
<PageContainer
|
||||
ghost
|
||||
header={{
|
||||
title: 'Nodes'
|
||||
}}
|
||||
extra={[]}
|
||||
>
|
||||
<PageTools
|
||||
marginBottom={22}
|
||||
left={
|
||||
<Space>
|
||||
<Input
|
||||
placeholder="按名称查询"
|
||||
style={{ width: 300 }}
|
||||
onChange={handleNameChange}
|
||||
></Input>
|
||||
<Button
|
||||
type="text"
|
||||
style={{ color: 'var(--ant-color-primary)' }}
|
||||
onClick={handleSearch}
|
||||
icon={<SyncOutlined></SyncOutlined>}
|
||||
></Button>
|
||||
</Space>
|
||||
}
|
||||
></PageTools>
|
||||
<Table
|
||||
dataSource={dataSource}
|
||||
rowSelection={rowSelection}
|
||||
loading={loading}
|
||||
onChange={handleTableChange}
|
||||
pagination={{
|
||||
showSizeChanger: true,
|
||||
pageSize: 10,
|
||||
current: 2,
|
||||
total: total,
|
||||
hideOnSinglePage: true,
|
||||
onShowSizeChange: handleShowSizeChange,
|
||||
onChange: handlePageChange
|
||||
}}
|
||||
>
|
||||
<Column title="Host Name" dataIndex="hostname" key="hostname" />
|
||||
<Column
|
||||
title="State"
|
||||
dataIndex="state"
|
||||
key="state"
|
||||
render={(text, record) => {
|
||||
return (
|
||||
<StatusTag
|
||||
statusValue={{
|
||||
status: 'success',
|
||||
text: 'ALIVE'
|
||||
}}
|
||||
></StatusTag>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Column title="IP / PID" dataIndex="address" key="address" />
|
||||
<Column
|
||||
title="CPU"
|
||||
dataIndex="CPU"
|
||||
key="CPU"
|
||||
render={(text, record: NodeItem) => {
|
||||
return (
|
||||
<RenderProgress
|
||||
record={record}
|
||||
dataIndex="cpu"
|
||||
></RenderProgress>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Column
|
||||
title="Memory"
|
||||
dataIndex="memory"
|
||||
key="Memory"
|
||||
render={(text, record: NodeItem) => {
|
||||
return (
|
||||
<RenderProgress
|
||||
record={record}
|
||||
dataIndex="memory"
|
||||
></RenderProgress>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Column
|
||||
title="GPU"
|
||||
dataIndex="GPU"
|
||||
key="GPU"
|
||||
render={(text, record: NodeItem) => {
|
||||
return (
|
||||
<RenderProgress
|
||||
record={record}
|
||||
dataIndex="gpu"
|
||||
></RenderProgress>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Column
|
||||
title="GRAM"
|
||||
dataIndex="GRAM"
|
||||
key="GRAM"
|
||||
render={(text, record: NodeItem) => {
|
||||
return (
|
||||
<RenderProgress
|
||||
record={record}
|
||||
dataIndex="gram"
|
||||
></RenderProgress>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Column
|
||||
title="Operation"
|
||||
key="operation"
|
||||
render={(text, record) => {
|
||||
return (
|
||||
<Space>
|
||||
<Button size="middle">Logs</Button>
|
||||
</Space>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Table>
|
||||
</PageContainer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Dashboard;
|
||||
export default Models;
|
||||
|
||||
Vendored
+1
@@ -14,5 +14,6 @@ declare module 'omit.js';
|
||||
declare module 'numeral';
|
||||
declare module 'mockjs';
|
||||
declare module 'react-fittext';
|
||||
declare module 'lodash';
|
||||
|
||||
declare const REACT_APP_ENV: 'test' | 'dev' | 'pre' | false;
|
||||
|
||||
Reference in New Issue
Block a user