dark
7 months ago
22 changed files with 693 additions and 149 deletions
-
1package.json
-
23src/components/action/Action.tsx
-
1src/components/action/index.ts
-
17src/components/action/style.ts
-
52src/components/drawer-picker/DrawerPicker.tsx
-
59src/components/status/Status.tsx
-
1src/components/status/index.ts
-
1src/locales/lang/en-US.ts
-
41src/locales/lang/status/zh-CN.ts
-
3src/locales/lang/zh-CN.ts
-
11src/pages/websites/ssl/ca/Detail.tsx
-
127src/pages/websites/ssl/components/Detail.tsx
-
77src/pages/websites/ssl/components/Upload.tsx
-
23src/pages/websites/ssl/components/store.ts
-
23src/pages/websites/ssl/components/style.ts
-
111src/pages/websites/ssl/index.tsx
-
7src/service/websites.ts
-
14src/store/websites/ssl.ts
-
2src/types/index.d.ts
-
10src/types/website/ssl.d.ts
-
1vite.config.ts
-
5yarn.lock
@ -0,0 +1,23 @@ |
|||||
|
import { Button, ButtonProps } from 'antd' |
||||
|
import { useStyle } from './style' |
||||
|
|
||||
|
export interface ActionProps extends ButtonProps { |
||||
|
as?: string |
||||
|
} |
||||
|
|
||||
|
const Action = ({ title, as, children, ...props }: ActionProps) => { |
||||
|
|
||||
|
const { styles } = useStyle() |
||||
|
|
||||
|
const isLink = as === 'a' || props.type === 'link' |
||||
|
|
||||
|
return ( |
||||
|
<span className={styles.container}> |
||||
|
<Button {...props} |
||||
|
type={isLink ? 'link' : props.type} |
||||
|
className={as === 'a' ? styles.actionA : ''}>{title ?? children}</Button> |
||||
|
</span> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
export default Action |
@ -0,0 +1 @@ |
|||||
|
export * from './Action' |
@ -0,0 +1,17 @@ |
|||||
|
import { createStyles } from '@/theme' |
||||
|
|
||||
|
export const useStyle = createStyles(({ token, css, cx, prefixCls }, props: any) => { |
||||
|
const prefix = `${prefixCls}-${token?.proPrefix}-action` |
||||
|
|
||||
|
const container = css`
|
||||
|
|
||||
|
`
|
||||
|
const actionA = css`
|
||||
|
padding: 0 2px; |
||||
|
`
|
||||
|
|
||||
|
return { |
||||
|
container: cx(prefix, props?.className, container), |
||||
|
actionA, |
||||
|
} |
||||
|
}) |
@ -1,42 +1,60 @@ |
|||||
import { Button, Drawer, DrawerProps } from 'antd' |
import { Button, Drawer, DrawerProps } from 'antd' |
||||
import React, { useState } from 'react' |
|
||||
|
import React, { forwardRef, useState, useImperativeHandle, memo } from 'react' |
||||
import { useStyle } from './style' |
import { useStyle } from './style' |
||||
import { generateUUID } from '@/utils/uuid.ts' |
|
||||
|
|
||||
export interface DrawerPickerProps extends DrawerProps { |
export interface DrawerPickerProps extends DrawerProps { |
||||
target?: React.ReactNode |
|
||||
|
target?: React.ReactNode | boolean |
||||
children?: React.ReactNode |
children?: React.ReactNode |
||||
key?: string |
|
||||
|
id?: string |
||||
foreRender?: boolean |
foreRender?: boolean |
||||
} |
} |
||||
|
|
||||
const DrawerPicker = ({ children, target, foreRender, ...props }: DrawerPickerProps) => { |
|
||||
|
export interface DrawerPickerRef { |
||||
|
open: () => void, |
||||
|
close: () => void |
||||
|
} |
||||
|
|
||||
const { styles } = useStyle() |
|
||||
|
const DrawerPicker = forwardRef<DrawerPickerRef | undefined, DrawerPickerProps>(( |
||||
|
{ |
||||
|
children, |
||||
|
target, |
||||
|
foreRender, |
||||
|
...props |
||||
|
}: DrawerPickerProps, ref) => { |
||||
|
|
||||
const [ open, setOpen ] = useState(false) |
|
||||
|
const { styles, cx } = useStyle() |
||||
|
|
||||
const getTarget = () => { |
|
||||
const def = <Button>{props.title ?? 'Target'}</Button> |
|
||||
return <span className={styles.target}>{target ?? def}</span> |
|
||||
|
const [ open, setOpen ] = useState(() => false) |
||||
|
|
||||
|
useImperativeHandle(ref, () => ({ |
||||
|
open: () => { |
||||
|
setOpen(true) |
||||
|
}, |
||||
|
close: () => { |
||||
|
setOpen(false) |
||||
} |
} |
||||
|
}), []) |
||||
|
|
||||
return ( |
|
||||
<div className={styles.container} key={props.key ?? generateUUID()}> |
|
||||
<div className={styles.target} onClick={() => { |
|
||||
|
const getTarget = () => { |
||||
|
if (target === false) return null |
||||
|
const def = <Button>{props.title ?? 'Target'}</Button> |
||||
|
return <div className={styles.target} onClick={() => { |
||||
setOpen(true) |
setOpen(true) |
||||
}}> |
}}> |
||||
{getTarget()} |
|
||||
|
<span className={styles.target}>{target ?? def}</span> |
||||
</div> |
</div> |
||||
|
} |
||||
|
return ( |
||||
|
<div className={cx(styles.container, props.id)}> |
||||
|
{getTarget()} |
||||
{ |
{ |
||||
(foreRender || open) && <Drawer {...props} |
(foreRender || open) && <Drawer {...props} |
||||
open={open} |
open={open} |
||||
onClose={() => setOpen(false)} |
onClose={() => setOpen(false)} |
||||
>{children}</Drawer> |
>{children}</Drawer> |
||||
} |
} |
||||
|
|
||||
</div> |
</div> |
||||
) |
) |
||||
} |
|
||||
|
}) |
||||
|
|
||||
export default DrawerPicker |
|
||||
|
export default memo(DrawerPicker) |
@ -0,0 +1,59 @@ |
|||||
|
import { Tag, TagProps } from 'antd' |
||||
|
import { useTranslation } from '@/i18n.ts' |
||||
|
import { SyncOutlined } from '@ant-design/icons' |
||||
|
|
||||
|
export interface StatusProps extends TagProps { |
||||
|
status: string |
||||
|
} |
||||
|
|
||||
|
|
||||
|
const getColor = (status: string) => { |
||||
|
if (status.includes('error') || status.includes('err')) { |
||||
|
return 'danger' |
||||
|
} |
||||
|
switch (status) { |
||||
|
case 'running': |
||||
|
return 'success' |
||||
|
case 'stopped': |
||||
|
return 'danger' |
||||
|
case 'unhealthy': |
||||
|
case 'paused': |
||||
|
case 'exited': |
||||
|
case 'dead': |
||||
|
case 'removing': |
||||
|
return 'warning' |
||||
|
default: |
||||
|
return 'primary' |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
const loadingStatus = [ |
||||
|
'installing', |
||||
|
'building', |
||||
|
'restarting', |
||||
|
'upgrading', |
||||
|
'rebuilding', |
||||
|
'recreating', |
||||
|
'creating', |
||||
|
'starting', |
||||
|
'removing', |
||||
|
'applying', |
||||
|
] |
||||
|
|
||||
|
const loadingIcon = (status: string): boolean => { |
||||
|
return loadingStatus.indexOf(status) > -1 |
||||
|
} |
||||
|
export const Status = ({ status = 'running', ...props }: StatusProps) => { |
||||
|
|
||||
|
const { t } = useTranslation() |
||||
|
const icon = loadingIcon(status) ? <SyncOutlined spin/> : null |
||||
|
return ( |
||||
|
<> |
||||
|
<Tag {...props} |
||||
|
color={getColor(status)} |
||||
|
icon={icon}>{t(`status.${status}`, status)}</Tag> |
||||
|
</> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
export default Status |
@ -0,0 +1 @@ |
|||||
|
export * from './Status.tsx' |
@ -0,0 +1,41 @@ |
|||||
|
export default { |
||||
|
running: '已启动', |
||||
|
done: '已完成', |
||||
|
success: '成功', |
||||
|
waiting: '执行中', |
||||
|
waiting1: '等待中', |
||||
|
failed: '失败', |
||||
|
stopped: '已停止', |
||||
|
error: '失败', |
||||
|
created: '已创建', |
||||
|
restarting: '重启中', |
||||
|
uploading: '上传中', |
||||
|
unhealthy: '异常', |
||||
|
removing: '移除中', |
||||
|
paused: '已暂停', |
||||
|
exited: '已停止', |
||||
|
dead: '已结束', |
||||
|
installing: '安装中', |
||||
|
enabled: '已启用', |
||||
|
disabled: '已停止', |
||||
|
normal: '正常', |
||||
|
building: '制作镜像中', |
||||
|
downloaderr: '下载失败', |
||||
|
upgrading: '升级中', |
||||
|
upgradeerr: '升级失败', |
||||
|
pullerr: '镜像拉取失败', |
||||
|
rebuilding: '重建中', |
||||
|
deny: '已屏蔽', |
||||
|
accept: '已放行', |
||||
|
used: '已使用', |
||||
|
unUsed: '未使用', |
||||
|
starting: '启动中', |
||||
|
recreating: '重建中', |
||||
|
creating: '创建中', |
||||
|
systemrestart: '中断', |
||||
|
init: '等待申请', |
||||
|
ready: '正常', |
||||
|
applying: '申请中', |
||||
|
applyerror: '失败', |
||||
|
syncerr: '失败', |
||||
|
} |
@ -0,0 +1,127 @@ |
|||||
|
import { Segmented, Drawer, DrawerProps, Input, Button, Flex, message, Descriptions } from 'antd' |
||||
|
import { useEffect, useMemo, useState } from 'react' |
||||
|
import { useTranslation } from '@/i18n.ts' |
||||
|
import { useAtom } from 'jotai' |
||||
|
import { detailAtom } from './store.ts' |
||||
|
import { useStyle } from './style.ts' |
||||
|
import { useCopyToClipboard } from 'react-use' |
||||
|
import { getProvider } from '@/store/websites/ssl.ts' |
||||
|
|
||||
|
const Detail = (props: DrawerProps) => { |
||||
|
|
||||
|
|
||||
|
const prefix = 'website.ssl.detail' |
||||
|
const { t } = useTranslation() |
||||
|
const { styles } = useStyle() |
||||
|
const [ key, setKey ] = useState('base') |
||||
|
const [ ui, setUI ] = useAtom(detailAtom) |
||||
|
const [ copyState, setCopy ] = useCopyToClipboard() |
||||
|
|
||||
|
const options = useMemo(() => { |
||||
|
return [ |
||||
|
{ label: t(`${prefix}.base`, '证书信息'), value: 'base' }, |
||||
|
{ label: t(`${prefix}.pem`, '证书'), value: 'pem' }, |
||||
|
{ label: t(`${prefix}.private_key`, '私钥'), value: 'private_key' }, |
||||
|
] |
||||
|
}, []) |
||||
|
|
||||
|
useEffect(() => { |
||||
|
if (copyState.error) { |
||||
|
message.error(t('message.copyError', '复制失败')) |
||||
|
} else if (copyState.value) { |
||||
|
message.success(t('message.copySuccess', '复制成功')) |
||||
|
} |
||||
|
}, [ copyState ]) |
||||
|
|
||||
|
useEffect(() => { |
||||
|
return () => { |
||||
|
setUI({ |
||||
|
open: false, |
||||
|
record: {}, |
||||
|
}) |
||||
|
} |
||||
|
}, []) |
||||
|
|
||||
|
const render = useMemo(() => { |
||||
|
switch (key) { |
||||
|
case 'base': |
||||
|
return <div> |
||||
|
<Descriptions bordered={true} column={1}> |
||||
|
<Descriptions.Item label={t(`${prefix}.primary_domain`, '主域名')}> |
||||
|
{ui.record?.primary_domain} |
||||
|
</Descriptions.Item> |
||||
|
<Descriptions.Item label={t(`${prefix}.domains`, '其它域名')}> |
||||
|
{ui.record?.domains} |
||||
|
</Descriptions.Item> |
||||
|
<Descriptions.Item label={t(`${prefix}.type`, '证书主体名称(CN)')}> |
||||
|
{ui.record?.type} |
||||
|
</Descriptions.Item> |
||||
|
<Descriptions.Item label={t(`${prefix}.organization`, '颁发组织')}> |
||||
|
{ui.record?.organization} |
||||
|
</Descriptions.Item> |
||||
|
<Descriptions.Item label={t(`${prefix}.start_date`, '生效时间')}> |
||||
|
{ui.record?.start_date} |
||||
|
</Descriptions.Item> |
||||
|
<Descriptions.Item label={t(`${prefix}.expire_date`, '过期时间')}> |
||||
|
{ui.record?.expire_date} |
||||
|
</Descriptions.Item> |
||||
|
<Descriptions.Item label={t(`${prefix}.provider`, '申请方式')}> |
||||
|
{getProvider(ui.record?.provider)} |
||||
|
</Descriptions.Item> |
||||
|
</Descriptions> |
||||
|
</div> |
||||
|
case 'pem': |
||||
|
return <Flex gap={15} vertical={true}> |
||||
|
<Input.TextArea |
||||
|
rows={20} |
||||
|
value={ui.record?.pem} |
||||
|
/> |
||||
|
<span> |
||||
|
<Button type={'primary'} onClick={() => { |
||||
|
setCopy(ui.record?.pem) |
||||
|
}}> {t('actions.copy', '复制')}</Button> |
||||
|
</span> |
||||
|
</Flex> |
||||
|
case 'private_key': |
||||
|
return <Flex gap={15} vertical={true}> |
||||
|
<Input.TextArea |
||||
|
rows={20} |
||||
|
value={ui.record?.private_key} |
||||
|
/> |
||||
|
<span> |
||||
|
<Button type={'primary'} onClick={() => { |
||||
|
setCopy(ui.record?.private_key) |
||||
|
}}> {t('actions.copy', '复制')}</Button> |
||||
|
</span> |
||||
|
</Flex> |
||||
|
default: |
||||
|
return null |
||||
|
} |
||||
|
}, [ key, ui.record ]) |
||||
|
|
||||
|
return ( |
||||
|
<Drawer {...props} |
||||
|
destroyOnClose={true} |
||||
|
title={t(`${prefix}.title`, '详情')} |
||||
|
width={800} |
||||
|
open={ui.open} |
||||
|
onClose={() => |
||||
|
setUI(prev => ({ |
||||
|
...prev, |
||||
|
open: false |
||||
|
})) |
||||
|
} |
||||
|
> |
||||
|
<Segmented |
||||
|
value={key} |
||||
|
options={options} |
||||
|
onChange={setKey} |
||||
|
/> |
||||
|
<div className={styles.content}> |
||||
|
{render} |
||||
|
</div> |
||||
|
</Drawer> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
export default Detail |
@ -0,0 +1,77 @@ |
|||||
|
import { Select, Form, Input } from 'antd' |
||||
|
import { useTranslation } from '@/i18n.ts' |
||||
|
import { useAtom } from 'jotai' |
||||
|
import { uploadAtom, uploadType } from './store.ts' |
||||
|
import { Else, If, Then } from 'react-if' |
||||
|
import { MutableRefObject, useEffect } from 'react' |
||||
|
import { FormInstance } from 'antd/lib' |
||||
|
|
||||
|
export interface UploadProps { |
||||
|
form?: FormInstance |
||||
|
formRef?: MutableRefObject<FormInstance | undefined> |
||||
|
} |
||||
|
|
||||
|
const Upload = (props: UploadProps) => { |
||||
|
const { t } = useTranslation() |
||||
|
const prefix = 'website.ssl.upload' |
||||
|
const [ form ] = Form.useForm() |
||||
|
const [ dto, updateDto ] = useAtom(uploadAtom) |
||||
|
|
||||
|
useEffect(() => { |
||||
|
form.setFieldsValue(dto) |
||||
|
}, []) |
||||
|
|
||||
|
props.formRef!.current = props.form ?? form |
||||
|
|
||||
|
return ( |
||||
|
<Form |
||||
|
form={props.form ?? form} |
||||
|
layout={'vertical'} |
||||
|
onValuesChange={(_change) => { |
||||
|
updateDto(prev => ({ |
||||
|
...prev, |
||||
|
..._change |
||||
|
})) |
||||
|
}} |
||||
|
scrollToFirstError={true} |
||||
|
> |
||||
|
<Form.Item name={'type'} label={t(`${prefix}.type`, '导入方式')} |
||||
|
rules={[ { required: true } ]} |
||||
|
> |
||||
|
<Select options={uploadType}/> |
||||
|
</Form.Item> |
||||
|
|
||||
|
<If condition={dto.type === 'paste'}> |
||||
|
<Then> |
||||
|
<Form.Item name={'private_key'} label={t(`${prefix}.private_key`, '私钥(KEY)')} |
||||
|
rules={[ { required: true } ]} |
||||
|
> |
||||
|
<Input.TextArea rows={10}/> |
||||
|
</Form.Item> |
||||
|
<Form.Item name={'pem'} label={t(`${prefix}.pem`, '证书(PEM格式)')} |
||||
|
rules={[ { required: true } ]} |
||||
|
> |
||||
|
<Input.TextArea rows={10}/> |
||||
|
</Form.Item> |
||||
|
</Then> |
||||
|
<Else> |
||||
|
<Form.Item name={'private_key_path'} label={t(`${prefix}.private_key_path`, '私钥文件')} |
||||
|
rules={[ { required: true } ]} |
||||
|
> |
||||
|
<Input/> |
||||
|
</Form.Item> <Form.Item name={'pem_path'} label={t(`${prefix}.pem_path`, '证书文件')} |
||||
|
rules={[ { required: true } ]} |
||||
|
> |
||||
|
<Input/> |
||||
|
</Form.Item> |
||||
|
</Else> |
||||
|
</If> |
||||
|
|
||||
|
<Form.Item name={'description'} label={t(`${prefix}.description`, '备注')}> |
||||
|
<Input/> |
||||
|
</Form.Item> |
||||
|
</Form> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
export default Upload |
@ -0,0 +1,23 @@ |
|||||
|
import { atom } from 'jotai' |
||||
|
import { WebSite } from '@/types' |
||||
|
import { t } from 'i18next' |
||||
|
|
||||
|
type DetailUI = { |
||||
|
open: boolean, |
||||
|
record?: WebSite.ISSL, |
||||
|
|
||||
|
} |
||||
|
|
||||
|
export const detailAtom = atom<DetailUI>({ |
||||
|
open: false, |
||||
|
}) |
||||
|
|
||||
|
export const uploadType = [ |
||||
|
{ label: t('website.ssl.upload.type_paste', '粘贴代码'), value: 'paste' }, |
||||
|
{ label: t('website.ssl.upload.type_local', '选择服务器文件'), value: 'local' }, |
||||
|
] |
||||
|
|
||||
|
|
||||
|
export const uploadAtom = atom<WebSite.SSLUploadDto>({ |
||||
|
type: 'paste' |
||||
|
}) |
@ -0,0 +1,23 @@ |
|||||
|
import { createStyles } from '@/theme' |
||||
|
|
||||
|
export const useStyle = createStyles(({ token, css, cx, prefixCls }, props: any) => { |
||||
|
const prefix = `${prefixCls}-${token?.proPrefix}-ssl-detail` |
||||
|
|
||||
|
const container = css`
|
||||
|
|
||||
|
`
|
||||
|
|
||||
|
const content = css`
|
||||
|
padding: 20px 0; |
||||
|
|
||||
|
.ant-descriptions-item-label{ |
||||
|
font-weight: bold; |
||||
|
width: 50%; |
||||
|
} |
||||
|
`
|
||||
|
|
||||
|
return { |
||||
|
container: cx(prefix, props?.className, container), |
||||
|
content, |
||||
|
} |
||||
|
}) |
Write
Preview
Loading…
Cancel
Save
Reference in new issue