|
|
import { useAtom, useAtomValue, useSetAtom } from 'jotai' import { deleteSslAtom, getProvider, KeyTypeEnum, KeyTypes, ProviderTypeEnum, saveOrUpdateSslAtom, sslListAtom, sslPageAtom, sslSearchAtom, uploadSslAtom } from '@/store/websites/ssl.ts' import ListPageLayout from '@/layout/ListPageLayout.tsx' import { BetaSchemaForm, ProColumns, ProFormColumnsType } from '@ant-design/pro-components' import { memo, useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from '@/i18n.ts' import { Button, Form, Popconfirm, Space } from 'antd' import { PlusOutlined } from '@ant-design/icons' import DrawerPicker, { DrawerPickerRef } from '@/components/drawer-picker/DrawerPicker.tsx' import AcmeList from './acme/AcmeList.tsx' import { acmeListAtom, AcmeType, getAcmeAccountTypeName } from '@/store/websites/acme.ts' import { dnsListAtom, getDNSTypeName } from '@/store/websites/dns.ts' import DNSList from './dns/DNSList.tsx' import CAList from './ca/CAList.tsx' import { WebSite } from '@/types' import Switch from '@/components/switch' import { Else, If, Then } from 'react-if' import Action from '@/components/action/Action.tsx' import { Status } from '@/components/status' import SSLDetail from './components/Detail.tsx' import { detailAtom } from './components/store.ts' import Upload from './components/Upload.tsx' import { FormInstance } from 'antd/lib' import Download from '@/components/download/Download.tsx' import { Table as ProTable } from '@/components/table'
const SSL = () => {
const { t } = useTranslation() const [ form ] = Form.useForm() const uploadFormRef = useRef<FormInstance>() const [ page, setPage ] = useAtom(sslPageAtom) const [ search, setSearch ] = useAtom(sslSearchAtom) const { data: acmeData, isLoading: acmeLoading } = useAtomValue(acmeListAtom) const { data: dnsData, isLoading: dnsLoading } = useAtomValue(dnsListAtom) const { data, isLoading, isFetching, refetch } = useAtomValue(sslListAtom) const { mutate: saveOrUpdate, isSuccess, isPending: isSubmitting } = useAtomValue(saveOrUpdateSslAtom) const { mutate: deleteSSL, isPending: isDeleting } = useAtomValue(deleteSslAtom) const { mutate: uploadSSL, isSuccess: isUploadSuccess, isPending: isUploading } = useAtomValue(uploadSslAtom) const updateDetail = useSetAtom(detailAtom) const uploadDrawerRef = useRef<DrawerPickerRef>() const [ open, setOpen ] = useState(false)
const columns = useMemo<ProColumns<WebSite.ISSL>[]>(() => { return [ { title: 'ID', dataIndex: 'id', hideInTable: true, hideInSearch: false, formItemProps: { hidden: true, } }, { title: t('website.ssl.columns.primaryDomain', '域名'), dataIndex: 'primary_domain', formItemProps: { label: t('website.ssl.form.primaryDomain', '主域名'), rules: [ { required: true, message: t('message.required', '主域名') } ] } }, { title: t('website.ssl.columns.otherDomains', '其它域名'), dataIndex: 'domains', }, { title: t('website.ssl.columns.acmeAccountId', 'Acme帐号'), dataIndex: 'acme_account_id', valueType: 'select', fieldProps: { loading: acmeLoading, options: acmeData?.rows?.map(item => ({ label: `${item.email} [${getAcmeAccountTypeName(item.type as AcmeType)}]`, value: item.id })) }, formItemProps: { rules: [ { required: true, message: t('message.required', '请选择') } ] } }, { title: t('website.ssl.columns.status', '状态'), dataIndex: 'status', render: (_, record) => { return <Status status={record.status}/> }, hideInForm: true, }, { title: t('website.ssl.columns.keyType', '密钥算法'), dataIndex: 'key_type', hideInTable: true, valueType: 'select', fieldProps: { options: KeyTypes }, formItemProps: { rules: [ { required: true, message: t('message.required', '请选择') } ] }, }, { title: t('website.ssl.columns.provider', '申请方式'), dataIndex: 'provider', valueType: 'radio', valueEnum: { [ProviderTypeEnum.DnsAccount]: { text: t('website.ssl.providerTypeEnum.DnsAccount', 'DNS帐号'), }, [ProviderTypeEnum.DnsManual]: { text: t('website.ssl.providerTypeEnum.DnsManual', '手动验证'), }, [ProviderTypeEnum.Http]: { text: t('website.ssl.providerTypeEnum.Http', 'HTTP'), } }, dependencies: [ 'provider' ], renderText: (text) => { return getProvider(text) }, formItemProps: (form, config) => { const val = form.getFieldValue(config.dataIndex) const help = { [ProviderTypeEnum.DnsAccount]: t('website.ssl.form.provider_{{v}}', '', { v: val }), [ProviderTypeEnum.DnsManual]: t('website.ssl.form.provider_{{v}}', '手动解析模式需要在创建完之后点击申请按钮获取 DNS 解析值', { v: val }), [ProviderTypeEnum.Http]: t('website.ssl.form.provider_{{v}}', 'HTTP 模式需要安装 OpenResty<br/><span style="color:red;">HTTP 模式无法申请泛域名证书</span>', { v: val }), } return { label: t('website.ssl.form.provider', '验证方式'), help: <span dangerouslySetInnerHTML={{ __html: help[val] }}/>, rules: [ { required: true, message: t('message.required', '请选择') } ] } }, }, { name: [ 'provider' ], valueType: 'dependency', hideInSetting: true, hideInTable: true, columns: ({ provider }) => { if (provider === ProviderTypeEnum.DnsAccount) { return [ { title: t('website.ssl.columns.dnsAccountId', 'DNS帐号'), dataIndex: 'dns_account_id', valueType: 'select', formItemProps: { rules: [ { required: true, message: t('message.required', '请输入DNS帐号') } ] }, fieldProps: { loading: dnsLoading, options: dnsData?.rows?.map(item => ({ label: `${item.name} [${getDNSTypeName(item.type)}]`, value: item.id })) }, } ] } return []
} }, { title: t('website.ssl.columns.autoRenew', '自动续签'), dataIndex: 'auto_renew', valueType: 'switch', render: (_, record) => { return <Switch value={record.auto_renew} size={'small'}/> } }, { title: t('website.ssl.columns.pushDir', '推送证书到本地目录'), dataIndex: 'push_dir', valueType: 'switch', hideInTable: true, hideInSearch: true,
}, { name: [ 'push_dir' ], valueType: 'dependency', hideInSetting: true, hideInTable: true, columns: ({ pushDir }) => { if (pushDir) { return [ { title: t('website.ssl.columns.dir', '目录'), dataIndex: 'dir', formItemProps: { help: t('website.ssl.form.dir_help', '会在此目录下生成两个文件,证书文件:fullchain.pem 密钥文件:privkey.pem'), rules: [ { required: true, message: t('message.required', '请输入目录') } ] } } ] } return []
} }, { title: t('website.ssl.columns.description', '备注'), dataIndex: 'description', }, { title: t('website.ssl.columns.expire_date', '过期时间'), dataIndex: 'expire_date', valueType: 'dateTime', hideInForm: true, }, { title: t('website.ssl.columns.option', '操作'), valueType: 'option', key: 'option', fixed: 'right', width: 300, render: (_, record) => [
<Action key="detail" as={'a'} disabled={record.status === 'init' || record.status === 'error'} onClick={() => { updateDetail({ open: true, record }) }} > {t('actions.detail', '详情')} </Action>, <If condition={() => record.status !== 'manual'}> <Then> <Action key="apply" as={'a'} disabled={record.status === 'applying' || record.status === 'manual'}
onClick={() => {
}} > {t('actions.apply', '申请')} </Action> </Then> <Else> <Action key="update" as={'a'} onClick={() => {
}} > {t('actions.update', '更新')} </Action> </Else>
</If>, <Download key="download" server={async () => { }} > <a>{t('actions.download', '下载')}</a> </Download>, <Popconfirm key={'del_confirm'} disabled={isDeleting} onConfirm={() => { deleteSSL(record.id) }} title={t('message.deleteConfirm')}> <a key="del"> {t('actions.delete', '删除')} </a> </Popconfirm> , ], }, ]
}, [ acmeData, dnsData ])
useEffect(() => { if (isSuccess) { setOpen(false) } }, [ isSuccess ])
return ( <ListPageLayout> <ProTable<WebSite.ISSL> headerTitle={t('website.ssl.title', '证书列表')} search={false} loading={isLoading || isFetching} rowKey={'id'} dataSource={data?.rows ?? []} columns={columns} columnsState={{ defaultValue: { option: { fixed: 'right', disable: true }, }, }} options={{ reload: () => { refetch() }, }} toolbar={{ search: { loading: isFetching && !!search.key, onSearch: (value: string) => { setSearch({ key: value }) }, placeholder: t('website.ssl.search.placeholder', '输入域名') }, actions: [ <DrawerPicker maskClosable={false} title={t('website.ssl.ca.title', '证书颁发机构')} width={1000} target={<Button type={'primary'} ghost={true}> {t('website.ssl.actions.selfSigned', '自签证书')} </Button>} > <CAList/> </DrawerPicker>, <DrawerPicker maskClosable={false} title={t('website.ssl.acme.title', 'Acme帐户')} width={1000} target={<Button type={'primary'} ghost={true}> {t('website.ssl.actions.acme', 'Acme帐户')} </Button>} > <AcmeList/> </DrawerPicker>, <DrawerPicker maskClosable={false} title={t('website.ssl.dns.title', 'DNS帐户')} width={1000} target={<Button type={'primary'} ghost={true}> {t('website.ssl.actions.dns', 'DNS帐户')} </Button>}> <DNSList/> </DrawerPicker>, <Button type={'primary'} onClick={() => { uploadDrawerRef.current?.open() }}> {t('website.ssl.actions.upload', '上传证书')} </Button>, <Button key="button" icon={<PlusOutlined/>} onClick={() => { form.resetFields() form.setFieldsValue({ id: 0, keyType: KeyTypeEnum.EC256, }) setOpen(true) }} type="primary" > {t('actions.sslApply', '申请证书')} </Button>, ] }} scroll={{}} pagination={{ pageSize: page?.pageSize ?? 10, total: data?.total ?? 0, current: page?.page ?? 1, onShowSizeChange: (current: number, size: number) => { setPage({ ...page, pageSize: size, page: current }) }, onChange: (page, pageSize) => { setPage(prev => ({ ...prev, page, pageSize, })) }, }} >
</ProTable> <BetaSchemaForm<WebSite.ISSL> shouldUpdate={false} width={600} form={form} layout={'vertical'} scrollToFirstError={true} title={t(`website.ssl.title_${form.getFieldValue('id') !== 0 ? 'edit' : 'add'}`, form.getFieldValue('id') !== 0 ? '证书编辑' : '证书添加')} // colProps={{ span: 24 }}
labelCol={{ span: 12 }} wrapperCol={{ span: 24 }} layoutType={'DrawerForm'} open={open} drawerProps={{ maskClosable: false, }} onOpenChange={(open) => { setOpen(open) }} loading={isSubmitting} onFinish={async (values) => { // console.log('values', values)
saveOrUpdate(values) }} columns={columns as ProFormColumnsType[]}/> <DrawerPicker id={'upload-drawer-picker'} ref={uploadDrawerRef} maskClosable={false} title={t('website.ssl.upload.title', '上传证书')} width={800} footer={[ <Space wrap={false} style={{ display: 'flex', justifyContent: 'end' }}> <Button onClick={() => uploadDrawerRef.current?.close()}>{t('actions.cancel')}</Button> <Button onClick={() => { uploadFormRef.current?.validateFields?.().then(values => { uploadSSL(values) if (isUploadSuccess) { uploadDrawerRef.current?.close() } }) }} loading={isUploading} type={'primary'}>{t('actions.ok')}</Button> </Space> ]} target={false} > <Upload formRef={uploadFormRef}/> </DrawerPicker> <SSLDetail/> </ListPageLayout> ) }
export default memo(SSL)
|