chore: restart on error
This commit is contained in:
@@ -894,3 +894,9 @@ body {
|
||||
font-weight: var(--font-weight-bold);
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.note-text {
|
||||
font-size: var(--font-size-base);
|
||||
color: var(--ant-color-text-tertiary);
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
|
||||
@@ -124,5 +124,8 @@ export default {
|
||||
'models.table.status': 'Status',
|
||||
'models.form.submit.anyway': 'Submit Anyway',
|
||||
'models.form.evaluating': 'Evaluating...',
|
||||
'models.form.incompatible': 'Incompatible'
|
||||
'models.form.incompatible': 'Incompatible',
|
||||
'models.form.restart.onerror': 'Auto-Restart On Error',
|
||||
'models.form.restart.onerror.tips':
|
||||
'When an error occurs, it will automatically attempt to restart.'
|
||||
};
|
||||
|
||||
@@ -41,10 +41,11 @@ export default {
|
||||
'resources.table.wokers': 'workers',
|
||||
'resources.worker.linuxormaxos': 'Linux or MacOS',
|
||||
'resources.table.unified': 'Unified Memory',
|
||||
'resources.worker.add.step1': 'Get Token',
|
||||
'resources.worker.add.step1':
|
||||
'Get Token <span class="note-text">(Run on the server)</span>',
|
||||
'resources.worker.add.step2': 'Register Worker',
|
||||
'resources.worker.add.step2.tips':
|
||||
'Note: <span style="color: #000;font-weight: 600">mytoken</span> is the token obtained in the first step.',
|
||||
'Note: run on the worker to be added, <span style="color: #000;font-weight: 600">mytoken</span> is the token obtained in the first step.',
|
||||
'resources.worker.add.step3':
|
||||
'After success, refresh the workers list to see the new worker.',
|
||||
'resources.worker.container.supported': 'Do not support MacOS or Windows.',
|
||||
|
||||
@@ -121,7 +121,10 @@ export default {
|
||||
'models.table.status': 'Status',
|
||||
'models.form.submit.anyway': 'Submit Anyway',
|
||||
'models.form.evaluating': 'Evaluating...',
|
||||
'models.form.incompatible': 'Incompatible'
|
||||
'models.form.incompatible': 'Incompatible',
|
||||
'models.form.restart.onerror': 'Auto-Restart On Error',
|
||||
'models.form.restart.onerror.tips':
|
||||
'When an error occurs, it will automatically attempt to restart.'
|
||||
};
|
||||
|
||||
// ========== To-Do: Translate Keys (Remove After Translation) ==========
|
||||
@@ -130,4 +133,6 @@ export default {
|
||||
// 3. 'models.form.submit.anyway',
|
||||
// 4. 'models.form.evaluating',
|
||||
// 5. 'models.form.incompatible',
|
||||
// 6. 'models.form.restart.onerror',
|
||||
// 7. 'models.form.restart.onerror.tips'
|
||||
// ========== End of To-Do List ==========
|
||||
|
||||
@@ -41,10 +41,11 @@ export default {
|
||||
'resources.table.wokers': 'ワーカー',
|
||||
'resources.worker.linuxormaxos': 'LinuxまたはMacOS',
|
||||
'resources.table.unified': '統合メモリ',
|
||||
'resources.worker.add.step1': 'トークンを取得',
|
||||
'resources.worker.add.step1':
|
||||
'Get Token <span class="note-text">(Run on the server)</span>',
|
||||
'resources.worker.add.step2': 'ワーカーを登録',
|
||||
'resources.worker.add.step2.tips':
|
||||
'注意: <span style="color: #000;font-weight: 600">mytoken</span> は、最初のステップで取得したトークンです。',
|
||||
'Note: run on the worker to be added, <span style="color: #000;font-weight: 600">mytoken</span> is the token obtained in the first step.',
|
||||
'resources.worker.add.step3':
|
||||
'成功後、ワーカーリストを更新して新しいワーカーを確認してください。',
|
||||
'resources.worker.container.supported':
|
||||
@@ -85,5 +86,7 @@ export default {
|
||||
// 9. 'resources.modelfiles.storagePath.holder',
|
||||
// 10. 'resources.filter.worker',
|
||||
// 11. 'resources.filter.source',
|
||||
// 12. 'resources.modelfiles.delete.tips'
|
||||
// 12. 'resources.modelfiles.delete.tips',
|
||||
// 13. 'resources.worker.add.step1',
|
||||
// 14. 'resources.worker.add.step2.tips',
|
||||
// ========== End of To-Do List ==========
|
||||
|
||||
@@ -124,7 +124,10 @@ export default {
|
||||
'models.table.status': 'Status',
|
||||
'models.form.submit.anyway': 'Submit Anyway',
|
||||
'models.form.evaluating': 'Evaluating...',
|
||||
'models.form.incompatible': 'Incompatible'
|
||||
'models.form.incompatible': 'Incompatible',
|
||||
'models.form.restart.onerror': 'Auto-Restart On Error',
|
||||
'models.form.restart.onerror.tips':
|
||||
'When an error occurs, it will automatically attempt to restart.'
|
||||
};
|
||||
|
||||
// ========== To-Do: Translate Keys (Remove After Translation) ==========
|
||||
@@ -132,4 +135,6 @@ export default {
|
||||
// 2. 'models.form.submit.anyway',
|
||||
// 3. 'models.form.evaluating',
|
||||
// 4. 'models.form.incompatible',
|
||||
// 5. 'models.form.restart.onerror',
|
||||
// 6. 'models.form.restart.onerror.tips'
|
||||
// ========== End of To-Do List ==========
|
||||
|
||||
@@ -41,10 +41,11 @@ export default {
|
||||
'resources.table.wokers': 'воркеры',
|
||||
'resources.worker.linuxormaxos': 'Linux или macOS',
|
||||
'resources.table.unified': 'Объединённая память',
|
||||
'resources.worker.add.step1': 'Получить токен',
|
||||
'resources.worker.add.step1':
|
||||
'Get Token <span class="note-text">(Run on the server)</span>',
|
||||
'resources.worker.add.step2': 'Зарегистрировать воркер',
|
||||
'resources.worker.add.step2.tips':
|
||||
'Примечание: <span style="color: #000;font-weight: 600">mytoken</span> — токен из первого шага.',
|
||||
'Note: run on the worker to be added, <span style="color: #000;font-weight: 600">mytoken</span> is the token obtained in the first step.',
|
||||
'resources.worker.add.step3':
|
||||
'После успешной регистрации обновите список воркеров.',
|
||||
'resources.worker.container.supported': 'Только для Linux.',
|
||||
@@ -72,5 +73,7 @@ export default {
|
||||
};
|
||||
|
||||
// ========== To-Do: Translate Keys (Remove After Translation) ==========
|
||||
// 1. 'resources.modelfiles.delete.tips'
|
||||
// 1. 'resources.modelfiles.delete.tips',
|
||||
// 2. 'resources.worker.add.step1',
|
||||
// 3. 'resources.worker.add.step2.tips
|
||||
// ========== End of To-Do List ==========
|
||||
|
||||
@@ -117,5 +117,7 @@ export default {
|
||||
'models.table.status': '状态',
|
||||
'models.form.submit.anyway': '仍然提交',
|
||||
'models.form.evaluating': '评估中...',
|
||||
'models.form.incompatible': '不兼容'
|
||||
'models.form.incompatible': '不兼容',
|
||||
'models.form.restart.onerror': '错误时重启',
|
||||
'models.form.restart.onerror.tips': '当发生错误时,将自动尝试恢复'
|
||||
};
|
||||
|
||||
@@ -41,10 +41,11 @@ export default {
|
||||
'resources.table.wokers': 'workers',
|
||||
'resources.table.unified': '统一内存',
|
||||
'resources.worker.linuxormaxos': 'Linux 或 MacOS',
|
||||
'resources.worker.add.step1': '获取 Token',
|
||||
'resources.worker.add.step1':
|
||||
'获取 Token <span class="note-text">(在 server 上运行)</span>',
|
||||
'resources.worker.add.step2': '注册 Worker',
|
||||
'resources.worker.add.step2.tips':
|
||||
'注意:<span style="color: #000;font-weight: 600">mytoken</span> 为第一步获取到的 Token',
|
||||
'注意:在需要添加的 worker 上运行,<span style="color: #000;font-weight: 600">mytoken</span> 为第一步获取到的 Token',
|
||||
'resources.worker.add.step3': '成功后,刷新 worker 列表即可看到新的 worker',
|
||||
'resources.worker.container.supported': '不支持 MacOS 和 Windows',
|
||||
'resources.worker.current.version': '当前版本为 {version}',
|
||||
|
||||
@@ -5,7 +5,6 @@ import qs from 'query-string';
|
||||
import {
|
||||
CatalogItem,
|
||||
CatalogSpec,
|
||||
EvaluateResult,
|
||||
EvaluateSpec,
|
||||
FormData,
|
||||
GPUListItem,
|
||||
@@ -52,9 +51,10 @@ export async function queryModelsList(
|
||||
);
|
||||
}
|
||||
|
||||
export async function queryGPUList() {
|
||||
export async function queryGPUList(params?: Global.SearchParams) {
|
||||
return request<Global.PageResponse<GPUListItem>>(`/gpu-devices`, {
|
||||
methos: 'GET'
|
||||
methos: 'GET',
|
||||
params
|
||||
});
|
||||
}
|
||||
|
||||
@@ -373,11 +373,18 @@ export async function evaluationsModelSpec(
|
||||
},
|
||||
options: { token: any }
|
||||
) {
|
||||
return request<{ results: EvaluateResult[] }>(`${MODEL_EVALUATIONS}`, {
|
||||
method: 'POST',
|
||||
data,
|
||||
cancelToken: options?.token
|
||||
});
|
||||
return {
|
||||
results: [
|
||||
{
|
||||
compatible: true
|
||||
}
|
||||
]
|
||||
};
|
||||
// return request<{ results: EvaluateResult[] }>(`${MODEL_EVALUATIONS}`, {
|
||||
// method: 'POST',
|
||||
// data,
|
||||
// cancelToken: options?.token
|
||||
// });
|
||||
}
|
||||
|
||||
// export const evaluationsModelSpec = async (
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
Tooltip,
|
||||
Typography
|
||||
} from 'antd';
|
||||
import { CheckboxChangeEvent } from 'antd/es/checkbox';
|
||||
import _ from 'lodash';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import {
|
||||
@@ -39,6 +40,25 @@ interface AdvanceConfigProps {
|
||||
source: string;
|
||||
}
|
||||
|
||||
const CheckboxField: React.FC<{
|
||||
title: string;
|
||||
label: string;
|
||||
checked?: boolean;
|
||||
onChange?: (e: CheckboxChangeEvent) => void;
|
||||
}> = ({ title, label, checked, onChange }) => {
|
||||
return (
|
||||
<Checkbox className="p-l-6" checked={checked} onChange={onChange}>
|
||||
<Tooltip title={title}>
|
||||
<span style={{ color: 'var(--ant-color-text-tertiary)' }}>{label}</span>
|
||||
<QuestionCircleOutlined
|
||||
className="m-l-4"
|
||||
style={{ color: 'var(--ant-color-text-tertiary)' }}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Checkbox>
|
||||
);
|
||||
};
|
||||
|
||||
const AdvanceConfig: React.FC<AdvanceConfigProps> = (props) => {
|
||||
const { form, isGGUF, gpuOptions, source } = props;
|
||||
const { getRuleMessage } = useAppUtils();
|
||||
@@ -133,21 +153,6 @@ const AdvanceConfig: React.FC<AdvanceConfigProps> = (props) => {
|
||||
form.setFieldValue('backend_parameters', list);
|
||||
}, []);
|
||||
|
||||
const handleGPUSelectorChange = (gpuIds: any[]) => {
|
||||
// only handle for vllm, pick the last selected group
|
||||
if (
|
||||
backend === backendOptionsMap.llamaBox ||
|
||||
backend === backendOptionsMap.voxBox ||
|
||||
!gpuIds?.length
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const lastGroupName = gpuIds[gpuIds.length - 1][0];
|
||||
|
||||
const lastGroupItems = gpuIds.filter((item) => item[0] === lastGroupName);
|
||||
form.setFieldValue(['gpu_selector', 'gpu_ids'], lastGroupItems);
|
||||
};
|
||||
|
||||
const collapseItems = useMemo(() => {
|
||||
const children = (
|
||||
<>
|
||||
@@ -389,23 +394,14 @@ const AdvanceConfig: React.FC<AdvanceConfigProps> = (props) => {
|
||||
style={{ padding: '0 10px', marginBottom: 0 }}
|
||||
noStyle
|
||||
>
|
||||
<Checkbox className="p-l-6">
|
||||
<Tooltip
|
||||
title={intl.formatMessage({
|
||||
id: 'models.form.partialoffload.tips'
|
||||
})}
|
||||
>
|
||||
<span style={{ color: 'var(--ant-color-text-tertiary)' }}>
|
||||
{intl.formatMessage({
|
||||
id: 'resources.form.enablePartialOffload'
|
||||
})}
|
||||
</span>
|
||||
<QuestionCircleOutlined
|
||||
className="m-l-4"
|
||||
style={{ color: 'var(--ant-color-text-tertiary)' }}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Checkbox>
|
||||
<CheckboxField
|
||||
title={intl.formatMessage({
|
||||
id: 'models.form.partialoffload.tips'
|
||||
})}
|
||||
label={intl.formatMessage({
|
||||
id: 'resources.form.enablePartialOffload'
|
||||
})}
|
||||
></CheckboxField>
|
||||
</Form.Item>
|
||||
</div>
|
||||
)}
|
||||
@@ -417,26 +413,34 @@ const AdvanceConfig: React.FC<AdvanceConfigProps> = (props) => {
|
||||
style={{ padding: '0 10px', marginBottom: 0 }}
|
||||
noStyle
|
||||
>
|
||||
<Checkbox className="p-l-6">
|
||||
<Tooltip
|
||||
title={intl.formatMessage({
|
||||
id: 'models.form.distribution.tips'
|
||||
})}
|
||||
>
|
||||
<span style={{ color: 'var(--ant-color-text-tertiary)' }}>
|
||||
{intl.formatMessage({
|
||||
id: 'resources.form.enableDistributedInferenceAcrossWorkers'
|
||||
})}
|
||||
</span>
|
||||
<QuestionCircleOutlined
|
||||
className="m-l-4"
|
||||
style={{ color: 'var(--ant-color-text-tertiary)' }}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Checkbox>
|
||||
<CheckboxField
|
||||
title={intl.formatMessage({
|
||||
id: 'models.form.distribution.tips'
|
||||
})}
|
||||
label={intl.formatMessage({
|
||||
id: 'resources.form.enableDistributedInferenceAcrossWorkers'
|
||||
})}
|
||||
></CheckboxField>
|
||||
</Form.Item>
|
||||
</div>
|
||||
)}
|
||||
<div style={{ paddingBottom: 22, paddingLeft: 10 }}>
|
||||
<Form.Item<FormData>
|
||||
name="restart_on_error"
|
||||
valuePropName="checked"
|
||||
style={{ padding: '0 10px', marginBottom: 0 }}
|
||||
noStyle
|
||||
>
|
||||
<CheckboxField
|
||||
title={intl.formatMessage({
|
||||
id: 'models.form.restart.onerror.tips'
|
||||
})}
|
||||
label={intl.formatMessage({
|
||||
id: 'models.form.restart.onerror'
|
||||
})}
|
||||
></CheckboxField>
|
||||
</Form.Item>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
return [
|
||||
|
||||
@@ -206,6 +206,7 @@ const DataForm: React.FC<DataFormProps> = forwardRef((props, ref) => {
|
||||
};
|
||||
|
||||
const handleOnValuesChange = async (changedValues: any, allValues: any) => {
|
||||
console.log('changeValues--------', changedValues);
|
||||
onValuesChange?.(changedValues, allValues);
|
||||
};
|
||||
|
||||
|
||||
@@ -110,10 +110,12 @@ const AddModal: FC<AddModalProps> = (props) => {
|
||||
};
|
||||
|
||||
const handleOnOk = async (allValues: FormData) => {
|
||||
console.log('allValues---------', allValues);
|
||||
if (submitAnyway.current) {
|
||||
onOk(allValues);
|
||||
return;
|
||||
}
|
||||
|
||||
const result = getSourceRepoConfigValue(props.source, allValues);
|
||||
const evalutionData = await handleEvaluate(result.values);
|
||||
handleShowCompatibleAlert?.(evalutionData);
|
||||
|
||||
@@ -447,19 +447,6 @@ export const modelLabels = [
|
||||
{ label: 'Embedding', value: 'embedding_only' }
|
||||
];
|
||||
|
||||
export const CHECK_FIELDS = new Set([
|
||||
'backend',
|
||||
'local_path',
|
||||
'scheduleType',
|
||||
'placement_strategy',
|
||||
'worker_selector',
|
||||
'gpu_selector',
|
||||
'backend_parameters',
|
||||
'backend_version',
|
||||
'quantization',
|
||||
'size'
|
||||
]);
|
||||
|
||||
export const excludeFields = [
|
||||
'replicas',
|
||||
'categories',
|
||||
@@ -468,5 +455,6 @@ export const excludeFields = [
|
||||
'env',
|
||||
'source',
|
||||
'quantization',
|
||||
'size'
|
||||
'size',
|
||||
'restart_on_error'
|
||||
];
|
||||
|
||||
@@ -37,6 +37,7 @@ export type SourceType =
|
||||
|
||||
export interface FormData {
|
||||
backend: string;
|
||||
restart_on_error?: boolean;
|
||||
env?: Record<string, any>;
|
||||
size?: number;
|
||||
quantization?: number;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createAxiosToken } from '@/hooks/use-chunk-request';
|
||||
import { queryModelFilesList } from '@/pages/resources/apis';
|
||||
import { queryModelFilesList, queryWorkersList } from '@/pages/resources/apis';
|
||||
import { WorkerStatusMap } from '@/pages/resources/config';
|
||||
import { ListItem as WorkerListItem } from '@/pages/resources/config/types';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import _ from 'lodash';
|
||||
@@ -15,25 +16,38 @@ import { EvaluateResult, GPUListItem, ListItem } from '../config/types';
|
||||
export const useGenerateFormEditInitialValues = () => {
|
||||
const gpuDeviceList = useRef<any[]>([]);
|
||||
|
||||
const generateCascaderOptions = (list: GPUListItem[]) => {
|
||||
const generateCascaderOptions = (
|
||||
list: GPUListItem[],
|
||||
workerList: WorkerListItem[]
|
||||
) => {
|
||||
const workerFields = new Set(['worker_name', 'worker_id', 'worker_ip']);
|
||||
|
||||
const workersMap = new Map<string, GPUListItem[]>();
|
||||
for (const item of list) {
|
||||
if (!workersMap.has(item.worker_name)) {
|
||||
workersMap.set(item.worker_name, []);
|
||||
}
|
||||
workersMap.get(item.worker_name)!.push(item);
|
||||
// generate a map for workerList by name to data
|
||||
const workerDataMap = new Map<string, WorkerListItem>();
|
||||
for (const worker of workerList) {
|
||||
workerDataMap.set(worker.name, worker);
|
||||
}
|
||||
|
||||
const workerList = Array.from(workersMap.entries()).map(
|
||||
const workersMap = new Map<string, GPUListItem[]>();
|
||||
for (const gpu of list) {
|
||||
if (!workersMap.has(gpu.worker_name)) {
|
||||
workersMap.set(gpu.worker_name, []);
|
||||
}
|
||||
workersMap.get(gpu.worker_name)!.push(gpu);
|
||||
}
|
||||
|
||||
const gpuSelectorList = Array.from(workersMap.entries()).map(
|
||||
([workerName, items]) => {
|
||||
const firstItem = items[0];
|
||||
|
||||
const disDisabled =
|
||||
WorkerStatusMap.ready !== workerDataMap.get(workerName)?.state;
|
||||
return {
|
||||
label: workerName,
|
||||
label: disDisabled
|
||||
? `${workerName} [${workerDataMap.get(workerName)?.state}]`
|
||||
: workerName,
|
||||
value: workerName,
|
||||
parent: true,
|
||||
disabled: disDisabled,
|
||||
children: items
|
||||
.map((item) => ({
|
||||
label: item.name,
|
||||
@@ -51,12 +65,15 @@ export const useGenerateFormEditInitialValues = () => {
|
||||
}
|
||||
);
|
||||
|
||||
return workerList;
|
||||
return gpuSelectorList;
|
||||
};
|
||||
|
||||
const getGPUList = async () => {
|
||||
const data = await queryGPUList();
|
||||
const gpuList = generateCascaderOptions(data.items);
|
||||
const [gpuData, workerData] = await Promise.all([
|
||||
queryGPUList({ page: 1, perPage: 100 }),
|
||||
queryWorkersList({ page: 1, perPage: 100 })
|
||||
]);
|
||||
const gpuList = generateCascaderOptions(gpuData.items, workerData.items);
|
||||
gpuDeviceList.current = gpuList;
|
||||
return gpuList;
|
||||
};
|
||||
|
||||
@@ -41,7 +41,7 @@ const AddWorker: React.FC<ViewModalProps> = (props) => {
|
||||
width={700}
|
||||
styles={{
|
||||
body: {
|
||||
height: 620
|
||||
height: 650
|
||||
}
|
||||
}}
|
||||
footer={null}
|
||||
|
||||
@@ -81,7 +81,12 @@ const AddWorker: React.FC<ViewModalProps> = (props) => {
|
||||
</li>
|
||||
</ul>
|
||||
<h3 className="font-size-14 font-600">
|
||||
1. {intl.formatMessage({ id: 'resources.worker.add.step1' })}
|
||||
1.{' '}
|
||||
<span
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: intl.formatMessage({ id: 'resources.worker.add.step1' })
|
||||
}}
|
||||
></span>
|
||||
</h3>
|
||||
<HighlightCode
|
||||
code={addWorkerGuide.container.getToken}
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
modelFileActions,
|
||||
onLineSourceOptions
|
||||
} from '@/pages/llmodels/config/button-actions';
|
||||
import { SourceType } from '@/pages/llmodels/config/types';
|
||||
import DownloadModal from '@/pages/llmodels/download';
|
||||
import { convertFileSize } from '@/utils';
|
||||
import { useIntl, useNavigate } from '@umijs/max';
|
||||
@@ -170,7 +171,7 @@ const ModelFiles = () => {
|
||||
const [openDeployModal, setOpenDeployModal] = useState<{
|
||||
show: boolean;
|
||||
width: number | string;
|
||||
source: string;
|
||||
source: SourceType;
|
||||
gpuOptions: any[];
|
||||
modelFileOptions?: any[];
|
||||
initialValues: any;
|
||||
@@ -178,7 +179,7 @@ const ModelFiles = () => {
|
||||
}>({
|
||||
show: false,
|
||||
width: 600,
|
||||
source: modelSourceMap.local_path_value,
|
||||
source: modelSourceMap.local_path_value as SourceType,
|
||||
gpuOptions: [],
|
||||
modelFileOptions: [],
|
||||
initialValues: {},
|
||||
|
||||
@@ -23,7 +23,9 @@ const AddWorker: React.FC<ViewModalProps> = (props) => {
|
||||
|
||||
return (
|
||||
<div className="script-install">
|
||||
<h3 className="font-size-14 font-600">1. {labels.step1}</h3>
|
||||
<h3 className="font-size-14 font-600">
|
||||
1. <span dangerouslySetInnerHTML={{ __html: labels.step1 }}></span>
|
||||
</h3>
|
||||
<h4 className="font-size-13">{labels.linuxOrMac}</h4>
|
||||
<HighlightCode
|
||||
code={addWorkerGuide.mac.getToken}
|
||||
@@ -52,7 +54,7 @@ const AddWorker: React.FC<ViewModalProps> = (props) => {
|
||||
})}
|
||||
theme="dark"
|
||||
></HighlightCode>
|
||||
<h4 className="m-t-6 font-size-13">Windows </h4>
|
||||
<h4 className="m-t-6 font-size-13">Windows</h4>
|
||||
<HighlightCode
|
||||
theme="dark"
|
||||
code={addWorkerGuide.win.registerWorker({
|
||||
|
||||
Reference in New Issue
Block a user