diff --git a/src/components/dynamic-form/components/field-item.tsx b/src/components/dynamic-form/components/field-item.tsx new file mode 100644 index 00000000..24966ab4 --- /dev/null +++ b/src/components/dynamic-form/components/field-item.tsx @@ -0,0 +1,19 @@ +import ComponentsMap from '@/components/seal-form/config/components'; +import { SealFormItemProps } from '@/components/seal-form/types'; +import { Form } from 'antd'; +import React from 'react'; + +interface FieldItemProps extends SealFormItemProps { + widget: keyof typeof ComponentsMap; + name: string; +} + +const FieldItem: React.FC = (props) => { + const { name, widget, required = [], ...rest } = props; + + const Component = ComponentsMap[widget]; + + return ; +}; + +export default FieldItem; diff --git a/src/components/dynamic-form/components/form-widget.tsx b/src/components/dynamic-form/components/form-widget.tsx new file mode 100644 index 00000000..09548ca9 --- /dev/null +++ b/src/components/dynamic-form/components/form-widget.tsx @@ -0,0 +1,45 @@ +import ComponentsMap from '@/components/seal-form/config/components'; +import { FormWidgetProps } from '../config/types'; + +const FormWidget: React.FC< + FormWidgetProps & { + onChange?: (data: any) => void; + } +> = ({ + widget, + title: label, + required, + placeholder, + options, + description, + enum: enumValues, + style, + value, + min, + max, + checked, + onChange +}) => { + const Component = ComponentsMap[widget]; + + const optionList = enumValues?.map((item: string | number) => ({ + label: item, + value: item + })); + + return Component ? ( + + ) : null; +}; + +export default FormWidget; diff --git a/src/components/dynamic-form/components/list-map.tsx b/src/components/dynamic-form/components/list-map.tsx new file mode 100644 index 00000000..2961e49f --- /dev/null +++ b/src/components/dynamic-form/components/list-map.tsx @@ -0,0 +1,136 @@ +import Wrapper from '@/components/label-selector/wrapper'; +import { MinusOutlined } from '@ant-design/icons'; +import { Button } from 'antd'; +import React, { useEffect, useMemo } from 'react'; +import styled from 'styled-components'; +import FormWidget from './form-widget'; + +interface ListMapProps { + dataList: any[]; + label?: React.ReactNode; + btnText?: string; + properties: Record; + onChange?: (data: any) => void; +} + +interface ListItemProps { + schemaList: any[]; + data: Record; + onChange?: (data: any) => void; +} + +const RowWrapper = styled.div` + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 8px; +`; + +const WidgetBox = styled.div` + display: flex; + align-items: center; + gap: 8px; + width: 100%; +`; + +const ListItem: React.FC = ({ schemaList, data, onChange }) => { + const handleValueChange = (name: string, target: any) => { + if (target?.target?.type === 'checkbox') { + const checked = target.target?.checked; + onChange?.({ [name]: checked }); + } else { + const value = target?.target ? target.target.value : target; + onChange?.({ [name]: value }); + } + }; + + return ( + <> + {schemaList.map((schema: any) => ( + handleValueChange(schema.name, target)} + /> + ))} + + ); +}; + +const ListMap: React.FC = ({ + dataList = [], + label, + btnText, + properties = {}, + onChange +}) => { + const [items, setItems] = React.useState(dataList || []); + + const schemaList = useMemo(() => { + const list = Object.entries(properties).map(([key, value]) => ({ + ...value, + name: key + })); + return list; + }, [properties]); + + const handleOnAdd = () => { + const keys = Object.keys(properties); + setItems([ + ...items, + { ...keys.reduce((acc, key) => ({ ...acc, [key]: '' }), {}) } + ]); + }; + + const handleDelete = (index: number) => { + const newItems = items.filter((_, i) => i !== index); + setItems(newItems); + onChange?.(newItems); + }; + + const handleItemChange = (index: number, data: { [key: string]: any }) => { + const newItems = [...items]; + newItems[index] = { ...newItems[index], ...data }; + setItems(newItems); + onChange?.(newItems); + }; + + useEffect(() => { + if (!dataList.length) { + handleOnAdd(); + } + }, []); + + return ( + + {items.map((item, index) => ( + + + handleItemChange(index, value)} + /> + + - ); }; -export default React.memo(Inner); +export default Inner; diff --git a/src/components/label-selector/styles/wrapper.less b/src/components/label-selector/styles/wrapper.less deleted file mode 100644 index ad82f3ec..00000000 --- a/src/components/label-selector/styles/wrapper.less +++ /dev/null @@ -1,20 +0,0 @@ -.wrapper { - position: relative; - padding: 14px; - padding-top: 34px; - border: 1px solid var(--ant-color-border); - border-radius: var(--border-radius-base); - display: flex; - width: 100%; - flex-direction: column; - - :global { - .label { - position: absolute; - left: 16px; - line-height: 1; - top: 12px; - color: var(--ant-color-text-tertiary); - } - } -} diff --git a/src/components/label-selector/wrapper.tsx b/src/components/label-selector/wrapper.tsx index ddc9a8e2..0176fd9a 100644 --- a/src/components/label-selector/wrapper.tsx +++ b/src/components/label-selector/wrapper.tsx @@ -1,15 +1,54 @@ import LabelInfo from '@/components/seal-form/components/label-info'; +import { PlusOutlined } from '@ant-design/icons'; +import { useIntl } from '@umijs/max'; +import { Button } from 'antd'; import React from 'react'; -import styles from './styles/wrapper.less'; +import styled from 'styled-components'; -const Wrapper: React.FC<{ +interface WrapperProps { label?: React.ReactNode; description?: React.ReactNode; labelExtra?: React.ReactNode; children: React.ReactNode; -}> = ({ children, label, description, labelExtra, ...rest }) => { + btnText?: string; + onAdd?: () => void; + button?: React.ReactNode; +} + +const Container = styled.div` + position: relative; + padding: 14px; + padding-top: 34px; + border: 1px solid var(--ant-color-border); + border-radius: var(--border-radius-base); + display: flex; + width: 100%; + flex-direction: column; + .label { + position: absolute; + left: 16px; + line-height: 1; + top: 12px; + color: var(--ant-color-text-tertiary); + } +`; + +const ButtonWrapper = styled.div` + margin-top: 16px; +`; + +const Wrapper: React.FC = ({ + children, + label, + description, + labelExtra, + onAdd, + btnText, + button +}) => { + const intl = useIntl(); return ( -
+ {label && ( )} - {React.isValidElement(children) - ? React.cloneElement(children, { ...rest }) - : children} -
+ {children} + + {button || ( + + )} + + ); }; diff --git a/src/components/list-input/index.tsx b/src/components/list-input/index.tsx index bf909daf..3375aa2b 100644 --- a/src/components/list-input/index.tsx +++ b/src/components/list-input/index.tsx @@ -1,6 +1,4 @@ -import { PlusOutlined } from '@ant-design/icons'; import { useIntl } from '@umijs/max'; -import { Button } from 'antd'; import _ from 'lodash'; import React from 'react'; import Wrapper from '../label-selector/wrapper'; @@ -34,7 +32,6 @@ const ListInput: React.FC = (props) => { } = props; const [list, setList] = React.useState<{ value: string; uid: number }[]>([]); const countRef = React.useRef(0); - const buttonRef = React.useRef(null); const updateCountRef = () => { countRef.current = countRef.current + 1; @@ -65,9 +62,6 @@ const ListInput: React.FC = (props) => { uid: countRef.current }); setList(values); - // setTimeout(() => { - // buttonRef.current?.scrollIntoView?.({ behavior: 'smooth' }); - // }, 100); }; React.useEffect(() => { @@ -85,7 +79,13 @@ const ListInput: React.FC = (props) => { }, [dataList]); return ( - + <> {_.map(list, (item: any, index: number) => { return ( @@ -100,26 +100,9 @@ const ListInput: React.FC = (props) => { /> ); })} -
- -
); }; -export default React.memo(ListInput); +export default ListInput; diff --git a/src/components/page-tools/index.tsx b/src/components/page-tools/index.tsx index e54c2322..1e1bfe5e 100644 --- a/src/components/page-tools/index.tsx +++ b/src/components/page-tools/index.tsx @@ -85,7 +85,7 @@ export const FilterBar: React.FC = (props) => { handleDeleteByBatch, handleClickPrimary, rowSelection, - actionItems, + actionItems = [], selectOptions, showSelect, buttonText, diff --git a/src/components/seal-form/seal-input.tsx b/src/components/seal-form/seal-input.tsx index 442442d8..485d8309 100644 --- a/src/components/seal-form/seal-input.tsx +++ b/src/components/seal-form/seal-input.tsx @@ -22,6 +22,7 @@ const SealInput: React.FC = (props) => { trim = true, loading, labelExtra, + style, ...rest } = props; const [isFocus, setIsFocus] = useState(false); @@ -73,7 +74,7 @@ const SealInput: React.FC = (props) => { }; return ( - + { data: formdata, clusterId: addPoolStatus.clusterId }); + setAddPoolStatus({ + ...addPoolStatus, + open: false + }); message.success(intl.formatMessage({ id: 'common.message.success' })); } catch (error) { // error } - setAddPoolStatus({ - ...addPoolStatus, - open: false - }); }; useEffect(() => { diff --git a/src/pages/cluster-management/components/add-pool.tsx b/src/pages/cluster-management/components/add-pool.tsx index 96cf8f83..eea585f6 100644 --- a/src/pages/cluster-management/components/add-pool.tsx +++ b/src/pages/cluster-management/components/add-pool.tsx @@ -1,19 +1,12 @@ -import LabelSelector from '@/components/label-selector'; import ModalFooter from '@/components/modal-footer'; import ScrollerModal from '@/components/scroller-modal'; -import SealInputNumber from '@/components/seal-form/input-number'; -import SealInput from '@/components/seal-form/seal-input'; -import { PageAction } from '@/config'; import { PageActionType } from '@/config/types'; -import useAppUtils from '@/hooks/use-app-utils'; -import { useIntl } from '@umijs/max'; -import { Form } from 'antd'; -import _ from 'lodash'; -import React, { useEffect } from 'react'; +import React, { useRef } from 'react'; import { NodePoolFormData as FormData, NodePoolListItem as ListItem } from '../config/types'; +import PoolForm from './pool-form'; type AddModalProps = { title: string; @@ -24,7 +17,7 @@ type AddModalProps = { onOk: (values: FormData) => void; onCancel: () => void; }; -const AddCluster: React.FC = ({ +const AddPool: React.FC = ({ title, action, open, @@ -33,45 +26,21 @@ const AddCluster: React.FC = ({ currentData, onCancel }) => { - const [form] = Form.useForm(); - const intl = useIntl(); - const { getRuleMessage } = useAppUtils(); + const formRef = useRef(null); - const handleSumit = () => { - form.submit(); + const handleSubmit = () => { + formRef.current?.submit?.(); }; - const handleOnOk = async (data: FormData) => { - const { volumes, ...rest } = data; - - await onOk({ - ...rest, - cloud_options: !_.isEmpty(volumes) - ? { - volumes: [ - { - ...volumes - } - ] - } - : {} - }); + const handleOnFinish = async (data: FormData) => { + onOk(data); }; const handleCancel = () => { - form.resetFields(); + formRef.current?.reset?.(); onCancel(); }; - useEffect(() => { - if (currentData) { - form.setFieldsValue({ - ...currentData, - volumes: currentData.cloud_options?.volumes?.[0] || {} - }); - } - }, [currentData]); - return ( = ({ keyboard={false} width={600} footer={ - + } > -
- - name="instance_type" - rules={[ - { - required: true, - message: getRuleMessage( - 'input', - 'clusters.workerpool.instanceType' - ) - } - ]} - > - - - - name="replicas" - rules={[ - { - required: true, - message: getRuleMessage('input', 'clusters.workerpool.replicas') - } - ]} - > - - - - name="batch_size" - rules={[ - { - required: true, - message: getRuleMessage('input', 'clusters.workerpool.batchSize') - } - ]} - > - - - - name="os_image" - rules={[ - { - required: true, - message: getRuleMessage('input', 'clusters.workerpool.osImage') - } - ]} - > - - - - - name="labels" - rules={[ - ({ getFieldValue }) => ({ - validator(rule, value) { - if (_.keys(value).length > 0) { - if (_.some(_.keys(value), (k: string) => !value[k])) { - return Promise.reject( - intl.formatMessage( - { - id: 'common.validate.value' - }, - { - name: 'labels' - } - ) - ); - } - } - return Promise.resolve(); - } - }) - ]} - > - - - - name="volumes" - rules={[ - ({ getFieldValue }) => ({ - validator(rule, value) { - if (_.keys(value).length > 0) { - if (_.some(_.keys(value), (k: string) => !value[k])) { - return Promise.reject( - intl.formatMessage( - { - id: 'common.validate.value' - }, - { - name: 'Volumes' - } - ) - ); - } - } - return Promise.resolve(); - } - }) - ]} - > - - - +
); }; -export default AddCluster; +export default AddPool; diff --git a/src/pages/cluster-management/components/cloud-options.tsx b/src/pages/cluster-management/components/cloud-options.tsx new file mode 100644 index 00000000..4fe4fa26 --- /dev/null +++ b/src/pages/cluster-management/components/cloud-options.tsx @@ -0,0 +1,128 @@ +import DropDownActions from '@/components/drop-down-actions'; +import ListMap from '@/components/dynamic-form/components/list-map'; +import { FieldSchema } from '@/components/dynamic-form/config/types'; +import { PlusOutlined } from '@ant-design/icons'; +import { useMemoizedFn } from 'ahooks'; +import { Button, Form } from 'antd'; +import _ from 'lodash'; +import React, { forwardRef, useImperativeHandle, useMemo } from 'react'; +import styled from 'styled-components'; +import { CloudOptionItems } from '../config'; +import { fieldConfig } from '../config/cloud-options-config'; + +const Title = styled.div` + display: flex; + height: 40px; + align-items: center; + gap: 16px; + // background-color: var(--ant-color-fill-secondary); + border-radius: var(--ant-border-radius); + margin-bottom: 22px; + font-weight: 600; +`; + +const ButtonWrapper = styled.span` + display: flex; + align-items: center; + height: 100%; + font-weight: 400; + cursor: pointer; + gap: 8px; + &:hover { + color: var(--ant-color-text-secondary); + } +`; + +const CloudOptions: React.FC<{ + ref?: any; +}> = forwardRef((props, ref) => { + // form instance + const form = Form.useFormInstance(); + const [selectedOptions, setSelectedOptions] = React.useState>( + new Set() + ); + const [fieldList, setFieldList] = React.useState([]); + + const items = useMemo(() => { + return CloudOptionItems.map((item) => ({ + ...item, + disabled: selectedOptions.has(item.key) + })); + }, [selectedOptions]); + + const handleAddOption = useMemoizedFn((item: { key: string }) => { + const field = fieldConfig[item.key]; + setFieldList((prev) => [...prev, { ...field, name: item.key }]); + setSelectedOptions((prev) => new Set(prev).add(item.key)); + }); + + const menu = useMemo(() => { + return { + items: items, + onClick: handleAddOption + }; + }, [items, handleAddOption]); + + const handleOnChange = (name: string, value: any) => { + form.setFieldValue(['cloud_options', name], value); + if (_.isEmpty(value)) { + setFieldList((prev) => prev.filter((field) => field.name !== name)); + setSelectedOptions((prev) => { + const newSelected = new Set(prev); + newSelected.delete(name); + return newSelected; + }); + } + }; + + // init field list by form data + const initFieldList = () => { + const cloudOptions = form.getFieldValue('cloud_options'); + if (cloudOptions) { + const fields = Object.entries(cloudOptions) + .filter(([, value]) => { + return !_.isEmpty(value); + }) + .map(([key, value]) => { + const field = fieldConfig[key]; + return { ...field, name: key }; + }); + setFieldList(fields); + setSelectedOptions(new Set(Object.keys(cloudOptions))); + } + }; + + useImperativeHandle(ref, () => ({ + initFieldList + })); + + return ( + <> + + <DropDownActions menu={menu}> + <Button variant="filled" color="default"> + <PlusOutlined /> + <span>Add Cloud Options</span> + </Button> + </DropDownActions> + + {fieldList.length > 0 && + fieldList.map((field) => ( + + handleOnChange(field.name, value)} + /> + + ))} + + ); +}); + +export default CloudOptions; diff --git a/src/pages/cluster-management/components/pool-form.tsx b/src/pages/cluster-management/components/pool-form.tsx new file mode 100644 index 00000000..948d61ce --- /dev/null +++ b/src/pages/cluster-management/components/pool-form.tsx @@ -0,0 +1,167 @@ +import LabelSelector from '@/components/label-selector'; +import SealInputNumber from '@/components/seal-form/input-number'; +import SealInput from '@/components/seal-form/seal-input'; +import { PageAction } from '@/config'; +import { PageActionType } from '@/config/types'; +import useAppUtils from '@/hooks/use-app-utils'; +import { useIntl } from '@umijs/max'; +import { Form } from 'antd'; +import _ from 'lodash'; +import React, { + forwardRef, + useEffect, + useImperativeHandle, + useRef +} from 'react'; +import { + NodePoolFormData as FormData, + NodePoolListItem as ListItem +} from '../config/types'; +import CloudOptions from './cloud-options'; + +type AddModalProps = { + ref: any; + action: PageActionType; + provider: string; // 'kubernetes' | 'custom' | 'digitalocean'; + currentData?: ListItem | null; + onFinish: (values: FormData) => void; +}; +const PoolForm: React.FC = forwardRef( + ({ action, onFinish, currentData }, ref) => { + const cloudOptionsRef = useRef(null); + const [form] = Form.useForm(); + const intl = useIntl(); + const { getRuleMessage } = useAppUtils(); + + useEffect(() => { + if (currentData) { + form.setFieldsValue({ + ...currentData, + volumes: currentData.cloud_options?.volumes?.[0] || {} + }); + cloudOptionsRef.current?.initFieldList(); + } + }, [currentData]); + + useImperativeHandle(ref, () => ({ + reset: () => { + form.resetFields(); + }, + submit: () => { + form.submit(); + }, + validateFields: async () => { + return await form.validateFields(); + } + })); + + return ( +
+ + name="instance_type" + rules={[ + { + required: true, + message: getRuleMessage( + 'input', + 'clusters.workerpool.instanceType' + ) + } + ]} + > + + + + name="replicas" + rules={[ + { + required: true, + message: getRuleMessage('input', 'clusters.workerpool.replicas') + } + ]} + > + + + + name="batch_size" + rules={[ + { + required: true, + message: getRuleMessage('input', 'clusters.workerpool.batchSize') + } + ]} + > + + + + name="os_image" + rules={[ + { + required: true, + message: getRuleMessage('input', 'clusters.workerpool.osImage') + } + ]} + > + + + + + name="labels" + rules={[ + ({ getFieldValue }) => ({ + validator(rule, value) { + if (_.keys(value).length > 0) { + if (_.some(_.keys(value), (k: string) => !value[k])) { + return Promise.reject( + intl.formatMessage( + { + id: 'common.validate.value' + }, + { + name: 'labels' + } + ) + ); + } + } + return Promise.resolve(); + } + }) + ]} + > + + + + + ); + } +); + +export default PoolForm; diff --git a/src/pages/cluster-management/components/worker-pools.tsx b/src/pages/cluster-management/components/worker-pools.tsx index b55f1bef..bb392b2b 100644 --- a/src/pages/cluster-management/components/worker-pools.tsx +++ b/src/pages/cluster-management/components/worker-pools.tsx @@ -116,15 +116,15 @@ const WorkerPools = () => { id: addPoolStatus.currentData!.id }); } + setAddPoolStatus({ + ...addPoolStatus, + open: false + }); message.success(intl.formatMessage({ id: 'common.message.success' })); handleSearch(); } catch (error) { // error } - setAddPoolStatus({ - ...addPoolStatus, - open: false - }); }; const columns = usePoolsColumns(sortOrder, onSelect); diff --git a/src/pages/cluster-management/config/cloud-options-config.ts b/src/pages/cluster-management/config/cloud-options-config.ts new file mode 100644 index 00000000..4e47c340 --- /dev/null +++ b/src/pages/cluster-management/config/cloud-options-config.ts @@ -0,0 +1,49 @@ +import { FieldSchema } from '@/components/dynamic-form/config/types'; + +export const fields = { + volumes: { + type: 'array', + minItems: 1, + items: { + type: 'object', + properties: { + name: { type: 'string' }, + size_gb: { type: 'number', unit: 'GB' }, + format: { type: 'string' } + }, + required: ['name', 'size_gb', 'format'] + } + } +}; + +export const fieldConfig: Record = { + volumes: { + type: 'array', + title: 'Volumes', + name: 'volumes', + properties: { + name: { + name: 'name', + type: 'string', + title: 'Name', + widget: 'Input' + }, + size_gb: { + name: 'size_gb', + type: 'number', + title: 'Size (GB)', + widget: 'InputNumber', + min: 0, + style: { width: 120 } + }, + format: { + name: 'format', + type: 'string', + title: 'Format', + widget: 'Select', + enum: ['ext4', 'xfs', 'btrfs'], + style: { width: 150 } + } + } + } +}; diff --git a/src/pages/cluster-management/config/index.ts b/src/pages/cluster-management/config/index.ts index 4e124de5..71d09e9e 100644 --- a/src/pages/cluster-management/config/index.ts +++ b/src/pages/cluster-management/config/index.ts @@ -155,3 +155,10 @@ export const regionList: { { label: 'Frankfurt', datacenter: 'Datacenter 1', value: 'fra1', icon: '🇩🇪' }, { label: 'Sydney', datacenter: 'Datacenter 1', value: 'syd1', icon: '🇦🇺' } ]; + +export const CloudOptionItems = [ + { + label: 'Volumes', + key: 'volumes' + } +]; diff --git a/src/pages/llmodels/components/advance-config.tsx b/src/pages/llmodels/components/advance-config.tsx index 90d799e2..56b38306 100644 --- a/src/pages/llmodels/components/advance-config.tsx +++ b/src/pages/llmodels/components/advance-config.tsx @@ -347,7 +347,7 @@ const AdvanceConfig: React.FC = (props) => { }) : '' } - btnText="common.button.addParams" + btnText={intl.formatMessage({ id: 'common.button.addParams' })} label={intl.formatMessage({ id: 'models.form.backend_parameters' })} @@ -395,7 +395,7 @@ const AdvanceConfig: React.FC = (props) => { id: 'models.form.env' })} labels={EnviromentVars} - btnText="common.button.vars" + btnText={intl.formatMessage({ id: 'common.button.vars' })} onBlur={handleEnvSelectorOnBlur} onDelete={handleDeleteEnvSelector} onChange={handleEnviromentVarsChange} diff --git a/src/pages/resources/apis/index.ts b/src/pages/resources/apis/index.ts index 616381b1..948ff736 100644 --- a/src/pages/resources/apis/index.ts +++ b/src/pages/resources/apis/index.ts @@ -1,14 +1,29 @@ +import { downloadFile } from '@/utils/download-stream'; import { request } from '@umijs/max'; +import { message } from 'antd'; import { GPUDeviceItem, ListItem, ModelFile } from '../config/types'; export const WORKERS_API = '/workers'; export const GPU_DEVICES_API = '/gpu-devices'; export const MODEL_FILES_API = '/model-files'; -export async function downloadWorkerPrivateKey(id: string | number) { - return request(`${WORKERS_API}/${id}/privatekey`, { - method: 'GET' - }); +// download stream data and save as a csv file +export async function downloadWorkerPrivateKey({ + id, + name +}: { + id: string | number; + name?: string; +}) { + try { + const res = await fetch(`/v1${WORKERS_API}/${id}/privatekey`); + if (res.ok) { + const blob = await res.blob(); + downloadFile(blob, `${name}-privatekey.csv`); + } + } catch (error) { + message.error('Download failed'); + } } export async function queryWorkersList>( diff --git a/src/pages/resources/components/update-labels.tsx b/src/pages/resources/components/update-labels.tsx index 8a7a058c..e47e6102 100644 --- a/src/pages/resources/components/update-labels.tsx +++ b/src/pages/resources/components/update-labels.tsx @@ -102,7 +102,7 @@ const UpdateLabels: React.FC = (props) => { id: 'resources.table.labels' })} labels={labels} - btnText="common.button.addLabel" + btnText={intl.formatMessage({ id: 'common.button.addLabel' })} onChange={handleLabelsChange} > diff --git a/src/pages/resources/components/workers.tsx b/src/pages/resources/components/workers.tsx index 7d54d8ab..3a31fdc4 100644 --- a/src/pages/resources/components/workers.tsx +++ b/src/pages/resources/components/workers.tsx @@ -146,7 +146,10 @@ const Workers: React.FC = () => { handleViewDetail(record); } if (val === 'download_ssh_key') { - downloadWorkerPrivateKey(record.id); + downloadWorkerPrivateKey({ + id: record.id, + name: record.name + }); } }); diff --git a/src/pages/resources/config/index.ts b/src/pages/resources/config/index.ts index 4e2c9636..7567f1d3 100644 --- a/src/pages/resources/config/index.ts +++ b/src/pages/resources/config/index.ts @@ -23,7 +23,7 @@ export const status: any = { [WorkerStatusMap.not_ready]: StatusMaps.error, [WorkerStatusMap.unreachable]: StatusMaps.error, [WorkerStatusMap.provisioning]: StatusMaps.transitioning, - [WorkerStatusMap.deleting]: StatusMaps.warning, + [WorkerStatusMap.deleting]: StatusMaps.transitioning, [WorkerStatusMap.error]: StatusMaps.error }; diff --git a/src/utils/download-stream.ts b/src/utils/download-stream.ts new file mode 100644 index 00000000..f35f9e77 --- /dev/null +++ b/src/utils/download-stream.ts @@ -0,0 +1,5 @@ +import { saveAs } from 'file-saver'; + +export const downloadFile = (blob: Blob, filename: string) => { + saveAs(blob, filename); +};