all tsx used, no vue SFC
This commit is contained in:
parent
731504ae82
commit
01765c4e7f
32
web/src/App.tsx
Normal file
32
web/src/App.tsx
Normal file
@ -0,0 +1,32 @@
|
||||
import {
|
||||
NNotificationProvider,
|
||||
NConfigProvider,
|
||||
NGlobalStyle,
|
||||
useOsTheme,
|
||||
darkTheme,
|
||||
lightTheme,
|
||||
} from "naive-ui";
|
||||
|
||||
import { zhCN, dateZhCN, enUS, dateEnUS } from 'naive-ui'
|
||||
import { RouterView } from "vue-router";
|
||||
|
||||
const osThemeRef = useOsTheme()
|
||||
const theme = osThemeRef.value === 'dark' ? darkTheme : lightTheme
|
||||
const locale = navigator.language === "zh-CN" ? zhCN : enUS
|
||||
const dateLocale = navigator.language === "zh-CN" ? dateZhCN : dateEnUS
|
||||
|
||||
function App() {
|
||||
document.title = 'reCoreD-UI'
|
||||
return (
|
||||
<NConfigProvider theme={theme} locale={locale} date-locale={dateLocale}>
|
||||
<NGlobalStyle />
|
||||
<NNotificationProvider max={3}>
|
||||
<RouterView />
|
||||
</NNotificationProvider>
|
||||
</NConfigProvider>
|
||||
)
|
||||
}
|
||||
|
||||
App.displayName = 'App'
|
||||
|
||||
export default App
|
@ -1,32 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
NNotificationProvider,
|
||||
NConfigProvider,
|
||||
NGlobalStyle,
|
||||
useOsTheme,
|
||||
darkTheme,
|
||||
lightTheme,
|
||||
type GlobalTheme
|
||||
} from "naive-ui";
|
||||
import { zhCN, dateZhCN, enUS, dateEnUS, type NLocale, type NDateLocale } from 'naive-ui'
|
||||
import { RouterView } from "vue-router";
|
||||
import { onMounted } from "vue";
|
||||
|
||||
const osThemeRef = useOsTheme()
|
||||
const theme = osThemeRef.value === 'dark' ? darkTheme : lightTheme
|
||||
const locale = navigator.language === "zh-CN" ? zhCN : enUS
|
||||
const dateLocale = navigator.language === "zh-CN" ? dateZhCN : dateEnUS
|
||||
|
||||
onMounted(() => {
|
||||
document.title = 'reCoreD-UI'
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NConfigProvider :theme="theme" :locale="locale" :date-locale="dateLocale">
|
||||
<NGlobalStyle />
|
||||
<NNotificationProvider :max="3">
|
||||
<RouterView />
|
||||
</NNotificationProvider>
|
||||
</NConfigProvider>
|
||||
</template>
|
@ -76,4 +76,15 @@ function DomainOps({ domain }: Props, { emit }: SetupContext<Events>) {
|
||||
)
|
||||
}
|
||||
|
||||
DomainOps.props = {
|
||||
domain: {
|
||||
required: true
|
||||
}
|
||||
}
|
||||
|
||||
DomainOps.emits = {
|
||||
removeDomain: (d:Domain) => d,
|
||||
editDomain: (d:Domain) => d
|
||||
} as Events
|
||||
|
||||
export default DomainOps
|
459
web/src/components/records/RecordEditModal.tsx
Normal file
459
web/src/components/records/RecordEditModal.tsx
Normal file
@ -0,0 +1,459 @@
|
||||
import {
|
||||
NModal,
|
||||
NCard,
|
||||
NForm,
|
||||
NFormItem,
|
||||
NFlex,
|
||||
NButton,
|
||||
NInput,
|
||||
NInputNumber,
|
||||
NInputGroup,
|
||||
NSelect,
|
||||
NIcon,
|
||||
type FormRules,
|
||||
type SelectOption,
|
||||
createDiscreteApi,
|
||||
type FormItemRule,
|
||||
} from 'naive-ui'
|
||||
import { getErrorInfo } from '@/apis/api';
|
||||
import {
|
||||
useRecordStore,
|
||||
RecordTypes,
|
||||
type Record,
|
||||
type ARecord,
|
||||
type AAAARecord,
|
||||
type CAARecord,
|
||||
type CNAMERecord,
|
||||
type NSRecord,
|
||||
type SRVRecord,
|
||||
type TXTRecord,
|
||||
type MXRecord,
|
||||
type RecordT,
|
||||
} from '@/stores/records';
|
||||
import { Check, Times } from '@vicons/fa';
|
||||
import { ref, type SetupContext } from 'vue';
|
||||
import i18n from '@/locale/i18n';
|
||||
|
||||
const { t } = i18n.global
|
||||
|
||||
const enum validationFlags {
|
||||
name = 1,
|
||||
content = name << 1
|
||||
}
|
||||
|
||||
type Props = {
|
||||
record: Record
|
||||
domain: string
|
||||
show: boolean
|
||||
}
|
||||
|
||||
type Events = {
|
||||
reloadRecords(): void
|
||||
'update:show': (v: boolean) => void
|
||||
}
|
||||
|
||||
const invalidData = ref(validationFlags.content)
|
||||
const loading = ref(false)
|
||||
const recordStore = useRecordStore()
|
||||
const { notification } = createDiscreteApi(['notification'])
|
||||
const recordTypeOptions = Object.entries(RecordTypes).filter(
|
||||
e => e[1] !== RecordTypes.RecordTypeSOA
|
||||
).map(e => {
|
||||
return {
|
||||
label: e[1],
|
||||
value: e[1]
|
||||
} as SelectOption
|
||||
})
|
||||
|
||||
function validateName(_rule: FormItemRule, value: string): boolean | Error {
|
||||
invalidData.value |= validationFlags.name
|
||||
if (!value || value === '') {
|
||||
invalidData.value &= ~validationFlags.name
|
||||
return new Error(t('common.mandatory'))
|
||||
}
|
||||
|
||||
if (value.includes(' ')) {
|
||||
invalidData.value &= ~validationFlags.name
|
||||
return new Error(t('records.errors.hasSpace'))
|
||||
}
|
||||
|
||||
if (value.startsWith('.') || value.endsWith('.')) {
|
||||
invalidData.value &= ~validationFlags.name
|
||||
return new Error(t('records.errors.badName.dotAndMinus'))
|
||||
}
|
||||
|
||||
if (value.startsWith('-') || value.endsWith('-')) {
|
||||
invalidData.value &= ~validationFlags.name
|
||||
return new Error(t('records.errors.badName.dotAndMinus'))
|
||||
}
|
||||
|
||||
if (value.includes('..')) {
|
||||
invalidData.value &= ~validationFlags.name
|
||||
return new Error(t('records.errors.badName.doubleDots'))
|
||||
}
|
||||
|
||||
if (value.split('.').filter(e => e.length > 63).length > 0) {
|
||||
invalidData.value &= ~validationFlags.name
|
||||
return new Error(t('records.errors.badName.longerThan63'))
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
function validateTXTRecord(record: Record) {
|
||||
return () => {
|
||||
invalidData.value |= validationFlags.content
|
||||
if (record.record_type !== RecordTypes.RecordTypeTXT) return true
|
||||
|
||||
const r = (record.content as TXTRecord)
|
||||
if (!r || !r.text || r.text === '') {
|
||||
invalidData.value &= ~validationFlags.content
|
||||
return new Error(t('common.mandatory'))
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
function validateHostRecord(record: Record) {
|
||||
return () => {
|
||||
invalidData.value |= validationFlags.content
|
||||
if ([RecordTypes.RecordTypeCNAME, RecordTypes.RecordTypeNS].indexOf(record.record_type) === -1) return true
|
||||
|
||||
const r = (record.content as CNAMERecord | NSRecord)
|
||||
if (!r || !r.host || r.host === '') {
|
||||
invalidData.value &= ~validationFlags.content
|
||||
return new Error(t('common.mandatory'))
|
||||
}
|
||||
|
||||
if (r.host.includes(' ')) {
|
||||
invalidData.value &= ~validationFlags.content
|
||||
return new Error(t('records.errors.hasSpace'))
|
||||
}
|
||||
|
||||
if (!r.host.endsWith('.')) {
|
||||
invalidData.value &= ~validationFlags.content
|
||||
return new Error(t('records.errors.endWithDot'))
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
function validateIPRecord(record: Record) {
|
||||
return () => {
|
||||
invalidData.value |= validationFlags.content
|
||||
if ([RecordTypes.RecordTypeA, RecordTypes.RecordTypeAAAA].indexOf(record.record_type) === -1) return true
|
||||
const r = (record.content as AAAARecord | ARecord)
|
||||
if (!r || !r.ip || r.ip === '') {
|
||||
invalidData.value &= ~validationFlags.content
|
||||
return new Error(t('common.mandatory'))
|
||||
}
|
||||
|
||||
switch (record.record_type) {
|
||||
case RecordTypes.RecordTypeA:
|
||||
if (!/^((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])$/.test(r.ip)) {
|
||||
invalidData.value &= ~validationFlags.content
|
||||
return new Error(t('records.errors.badIPv4'))
|
||||
}
|
||||
|
||||
break
|
||||
case RecordTypes.RecordTypeAAAA:
|
||||
if (!/^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$/.test(r.ip)) {
|
||||
invalidData.value &= ~validationFlags.content
|
||||
return new Error(t('records.errors.badIPv6'))
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
function validateMXRecord(record: Record) {
|
||||
return () => {
|
||||
invalidData.value |= validationFlags.content
|
||||
if (record.record_type !== RecordTypes.RecordTypeMX) return true
|
||||
const r = (record.content as MXRecord)
|
||||
|
||||
if (!r || !r.host || !r.preference || r.host === '') {
|
||||
invalidData.value &= ~validationFlags.content
|
||||
return new Error(t('common.mandatory'))
|
||||
}
|
||||
|
||||
if (r.host.includes(' ')) {
|
||||
invalidData.value &= ~validationFlags.content
|
||||
return new Error(t('records.errors.hasSpace'))
|
||||
}
|
||||
|
||||
if (!r.host.endsWith('.')) {
|
||||
invalidData.value &= ~validationFlags.content
|
||||
return new Error(t('records.errors.endWithDot'))
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
function validateSRVRecord(record: Record) {
|
||||
return () => {
|
||||
invalidData.value |= validationFlags.content
|
||||
if (record.record_type !== RecordTypes.RecordTypeSRV) return true
|
||||
const r = (record.content as SRVRecord)
|
||||
|
||||
if (!r || !r.port || !r.priority || !r.weight || !r.target || r.target === '') {
|
||||
invalidData.value &= ~validationFlags.content
|
||||
return new Error(t('common.mandatory'))
|
||||
}
|
||||
|
||||
if (r.target.includes(' ')) {
|
||||
invalidData.value &= ~validationFlags.content
|
||||
return new Error(t('records.errors.hasSpace'))
|
||||
}
|
||||
|
||||
if (!r.target.endsWith('.')) {
|
||||
invalidData.value &= ~validationFlags.content
|
||||
return new Error(t('records.errors.endWithDot'))
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
function validateCAARecord(record: Record) {
|
||||
return () => {
|
||||
invalidData.value |= validationFlags.content
|
||||
if (record.record_type !== RecordTypes.RecordTypeCAA) return true
|
||||
const r = (record.content as CAARecord)
|
||||
|
||||
if (!r || !r.flag || !r.tag || r.tag === '' || !r.value || r.value === '') {
|
||||
invalidData.value &= ~validationFlags.content
|
||||
return new Error(t('common.mandatory'))
|
||||
}
|
||||
|
||||
if (r.tag.includes(' ')) {
|
||||
invalidData.value &= ~validationFlags.content
|
||||
return new Error(t('records.errors.hasSpace'))
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
function buildRules(record: Record): FormRules {
|
||||
return {
|
||||
name: {
|
||||
trigger: 'blur',
|
||||
validator: validateName
|
||||
},
|
||||
txt: {
|
||||
trigger: 'blur',
|
||||
validator: validateTXTRecord(record)
|
||||
},
|
||||
host: {
|
||||
trigger: 'blur',
|
||||
validator: validateHostRecord(record)
|
||||
},
|
||||
ip: {
|
||||
trigger: 'blur',
|
||||
validator: validateIPRecord(record)
|
||||
},
|
||||
mx: {
|
||||
trigger: 'blur',
|
||||
validator: validateMXRecord(record)
|
||||
},
|
||||
srv: {
|
||||
trigger: 'blur',
|
||||
validator: validateSRVRecord(record)
|
||||
},
|
||||
caa: {
|
||||
trigger: 'blur',
|
||||
validator: validateCAARecord(record)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function confirm({ record, domain }: Props) {
|
||||
loading.value = true;
|
||||
try {
|
||||
if (!record.id || record.id < 1) {
|
||||
await recordStore.addRecord(domain, record)
|
||||
} else {
|
||||
await recordStore.updateRecord(domain, record)
|
||||
}
|
||||
} catch (e) {
|
||||
const msg = getErrorInfo(e)
|
||||
notification.error(msg)
|
||||
console.error(e)
|
||||
}
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
function modalHeader({ record }: Props) {
|
||||
return (
|
||||
<>
|
||||
{(!record || !record.id || record.id < 1) ? <span>{t('common.new')}</span> : <span> t('common.edit')</span>}
|
||||
<span>{t('records._')}</span>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function modalActions({ record, domain }: Props, { emit }: SetupContext<Events>) {
|
||||
return (
|
||||
<NFlex justify='end'>
|
||||
<NButton size='small' onClick={() => emit('update:show', false)}>
|
||||
{{
|
||||
icon: () => <NIcon component={Times} />,
|
||||
default: () => t('common.cancel')
|
||||
}}
|
||||
</NButton>
|
||||
|
||||
<NButton size='small' type='primary' loading={loading.value} attrType='submit'
|
||||
disabled={invalidData.value !== (validationFlags.content | validationFlags.name)}
|
||||
onClick={() => confirm({ record, domain, show: false }).then(() => { emit('reloadRecords'); emit('update:show', false) })}>
|
||||
{{
|
||||
icon: () => <NIcon component={Check} />,
|
||||
default: () => t('common.confirm')
|
||||
}}
|
||||
</NButton>
|
||||
</NFlex>
|
||||
)
|
||||
}
|
||||
|
||||
function modalBody({ record }: Props) {
|
||||
const rules = buildRules(record)
|
||||
return (
|
||||
<>
|
||||
<NForm model={record} rules={rules} inline>
|
||||
<NFormItem label={t('records.recordType')}>
|
||||
<NSelect value={record.record_type}
|
||||
onUpdate:value={(v) => { record.record_type = v; record.content = {} as RecordT }}
|
||||
options={recordTypeOptions} style={{ width: '8vw' }} />
|
||||
</NFormItem>
|
||||
<NFormItem label={t('records.name')} path='name'>
|
||||
<NInput value={record.name} onUpdate:value={v => record.name = v} />
|
||||
</NFormItem>
|
||||
<NFormItem label='TTL' path='ttl'>
|
||||
<NInputNumber value={record.ttl} onUpdate:value={v => v ? record.ttl = v : null} showButton={false} >
|
||||
{{
|
||||
suffix: () => t('common.unitForSecond')
|
||||
}}
|
||||
</NInputNumber>
|
||||
</NFormItem>
|
||||
</NForm>
|
||||
<NForm model={record} rules={rules}>
|
||||
<modalBodyContent type={record.record_type} record={record} />
|
||||
</NForm>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const IPRecordE = ({ record }: Props) => (
|
||||
<NFormItem label={t('records.content')} path='ip'>
|
||||
<NInput value={(record.content as ARecord | AAAARecord).ip} onUpdate:value={v => (record.content as ARecord | AAAARecord).ip = v} placeholder='IP' />
|
||||
</NFormItem>
|
||||
)
|
||||
|
||||
const HostRecordE = ({ record }: Props) => (
|
||||
<NFormItem label={t('records.content')} path='host'>
|
||||
<NInput value={(record.content as CNAMERecord | NSRecord).host} onUpdate:value={v => (record.content as CNAMERecord | NSRecord).host = v} placeholder={t('records.form.host')} />
|
||||
</NFormItem>
|
||||
)
|
||||
|
||||
const TXTRecordE = ({ record }: Props) => (
|
||||
<NFormItem label={t('records.content')} path='txt'>
|
||||
<NInput value={(record.content as TXTRecord).text} onUpdateValue={v => (record.content as TXTRecord).text = v} placeholder={t('records.form.text')} />
|
||||
</NFormItem>
|
||||
)
|
||||
|
||||
const MXRecordE = ({ record }: Props) => (
|
||||
<NFormItem label={t('records.content')} path='mx'>
|
||||
<NInputGroup>
|
||||
<NInput placeholder={t('records.form.host')}
|
||||
value={(record.content as MXRecord).host}
|
||||
onUpdate:value={v => (record.content as MXRecord).host = v}
|
||||
style={{ width: '75%' }} />
|
||||
<NInputNumber placeholder={t('records.form.preference')}
|
||||
value={(record.content as MXRecord).preference}
|
||||
onUpdate:value={v => v ? (record.content as MXRecord).preference = v : null}
|
||||
style={{ width: '25%' }} show-button={false} />
|
||||
</NInputGroup>
|
||||
</NFormItem>
|
||||
)
|
||||
|
||||
const CAARecordE = ({ record }: Props) => (
|
||||
<NFormItem label={t('records.content')} path='caa'>
|
||||
<NInputGroup>
|
||||
<NInputNumber placeholder={t('records.form.flag')}
|
||||
value={(record.content as CAARecord).flag} style={{ width: '20%' }}
|
||||
onUpdate:value={v => v ? (record.content as CAARecord).flag = v : null}
|
||||
show-button={false} />
|
||||
<NInput placeholder={t('records.form.tag')}
|
||||
value={(record.content as CAARecord).tag}
|
||||
onUpdate:value={v => v ? (record.content as CAARecord).tag = v : null}
|
||||
style={{ width: '40%' }} />
|
||||
<NInput placeholder={t('records.form.value')}
|
||||
value={(record.content as CAARecord).value}
|
||||
onUpdate:value={v => v ? (record.content as CAARecord).value = v : null}
|
||||
style={{ width: '40%' }} />
|
||||
</NInputGroup>
|
||||
</NFormItem>
|
||||
)
|
||||
|
||||
const SRVRecordE = ({ record }: Props) => (
|
||||
<NFormItem label={t('records.content')} path='srv'>
|
||||
<NInputGroup>
|
||||
<NInputNumber placeholder={t('records.form.priority')}
|
||||
value={(record.content as SRVRecord).priority} style={{ width: '15%' }}
|
||||
onUpdateValue={v => v ? (record.content as SRVRecord).priority = v : null}
|
||||
show-button={false} />
|
||||
<NInputNumber placeholder={t('records.form.weight')}
|
||||
value={(record.content as SRVRecord).weight} style={{ width: '15%' }}
|
||||
onUpdate:value={v => v ? (record.content as SRVRecord).weight = v : null}
|
||||
show-button={false} />
|
||||
<NInputNumber placeholder={t('records.form.port')}
|
||||
value={(record.content as SRVRecord).port} style={{ width: '15%' }} min={0} max={65535}
|
||||
onUpdate:value={v => v ? (record.content as SRVRecord).port = v : null}
|
||||
show-button={false} />
|
||||
<NInput placeholder={t('records.form.target')}
|
||||
value={(record.content as SRVRecord).target} style={{ width: '55%' }}
|
||||
onUpdate:value={v => (record.content as SRVRecord).target = v}
|
||||
/>
|
||||
</NInputGroup>
|
||||
</NFormItem>
|
||||
)
|
||||
|
||||
const modalBodyContent = ({ type, record }: { type: RecordTypes, record: Record }) => {
|
||||
const e = {
|
||||
'A': IPRecordE,
|
||||
'AAAA': IPRecordE,
|
||||
'CNAME': HostRecordE,
|
||||
'NS': HostRecordE,
|
||||
'TXT': TXTRecordE,
|
||||
'MX': MXRecordE,
|
||||
'SRV': SRVRecordE,
|
||||
'CAA': CAARecordE,
|
||||
'SOA': ({ }: Props) => <></>
|
||||
}[type]
|
||||
return <e record={record} />
|
||||
}
|
||||
|
||||
function RecordEditModal(
|
||||
{ domain, show, record }: Props,
|
||||
{ emit }: SetupContext<Events>) {
|
||||
return (
|
||||
<NModal maskClosable={false} show={show}>
|
||||
<NCard style={{ width: '640px' }} role='dialog'>
|
||||
{{
|
||||
header: () => <modalHeader record={record} />,
|
||||
default: () => <modalBody record={record} />,
|
||||
action: () => <modalActions record={record} domain={domain}
|
||||
onUpdate:show={(v: boolean) => emit('update:show', v)}
|
||||
onReloadRecords={() => emit('reloadRecords')} />
|
||||
}}
|
||||
</NCard>
|
||||
</NModal>
|
||||
)
|
||||
}
|
||||
|
||||
export default RecordEditModal
|
@ -1,380 +0,0 @@
|
||||
<template>
|
||||
<NModal :mask-closable="false" :show="show">
|
||||
<NCard style="width: 640px" role="dialog" aria-modal="true">
|
||||
<template #header>
|
||||
<span v-if="!record || !record.id || record.id < 1">{{ t('common.new') }}</span><span v-else>{{
|
||||
t('common.edit') }}</span><span>{{ t('records._') }}</span>
|
||||
</template>
|
||||
|
||||
<NForm :model="record" inline :rules="rules">
|
||||
<NFormItem :label="t('records.recordType')">
|
||||
<NSelect v-model:value="record.record_type" :options="recordTypeOptions"
|
||||
@update:value="clearRecordContent" style="width: 8vw;" />
|
||||
</NFormItem>
|
||||
<NFormItem :label="t('records.name')" path="name">
|
||||
<NInput v-model:value="record.name" />
|
||||
</NFormItem>
|
||||
<NFormItem label="TTL" path="ttl">
|
||||
<NInputNumber v-model:value="record.ttl" :show-button="false">
|
||||
<template #suffix>
|
||||
{{ t('common.unitForSecond') }}
|
||||
</template>
|
||||
</NInputNumber>
|
||||
</NFormItem>
|
||||
</NForm>
|
||||
<NForm :model="record" inline :rules="rules">
|
||||
<!-- A or AAAA -->
|
||||
<NFormItem :label="t('records.content')"
|
||||
v-if="[RecordTypes.RecordTypeA, RecordTypes.RecordTypeAAAA].indexOf(record.record_type) > -1"
|
||||
path="ip">
|
||||
<NInput v-model:value="(record.content as ARecord | AAAARecord).ip" placeholder="IP" />
|
||||
</NFormItem>
|
||||
|
||||
<!-- CNAME or NS -->
|
||||
<NFormItem :label="t('records.content')"
|
||||
v-if="[RecordTypes.RecordTypeCNAME, RecordTypes.RecordTypeNS].indexOf(record.record_type) > -1"
|
||||
path="host">
|
||||
<NInput v-model:value="(record.content as CNAMERecord | NSRecord).host"
|
||||
:placeholder="t('records.form.host')" />
|
||||
</NFormItem>
|
||||
|
||||
<!-- TXT -->
|
||||
<NFormItem :label="t('records.content')" v-if="RecordTypes.RecordTypeTXT === record.record_type"
|
||||
path="txt">
|
||||
<NInput v-model:value="(record.content as TXTRecord).text" :placeholder="t('records.form.text')" />
|
||||
</NFormItem>
|
||||
|
||||
<!-- MX -->
|
||||
<NFormItem :label="t('records.content')" v-if="RecordTypes.RecordTypeMX === record.record_type"
|
||||
path="mx">
|
||||
<NInputGroup>
|
||||
<NInput :placeholder="t('records.form.host')" v-model:value="(record.content as MXRecord).host"
|
||||
style="width: 75%;" />
|
||||
<NInputNumber :placeholder="t('records.form.preference')"
|
||||
v-model:value="(record.content as MXRecord).preference" style="width: 25%;"
|
||||
:show-button="false" />
|
||||
</NInputGroup>
|
||||
</NFormItem>
|
||||
|
||||
<!-- SRV -->
|
||||
<NFormItem :label="t('records.content')" v-if="RecordTypes.RecordTypeSRV === record.record_type"
|
||||
path="srv">
|
||||
<NInputGroup>
|
||||
<NInputNumber :placeholder="t('records.form.priority')"
|
||||
v-model:value="(record.content as SRVRecord).priority" style="width: 15%;"
|
||||
:show-button="false" />
|
||||
<NInputNumber :placeholder="t('records.form.weight')"
|
||||
v-model:value="(record.content as SRVRecord).weight" style="width: 15%;"
|
||||
:show-button="false" />
|
||||
<NInputNumber :placeholder="t('records.form.port')"
|
||||
v-model:value="(record.content as SRVRecord).port" style="width: 15%;" :min="0" :max="65535"
|
||||
:show-button="false" />
|
||||
<NInput :placeholder="t('records.form.target')"
|
||||
v-model:value="(record.content as SRVRecord).target" style="width: 55%;" />
|
||||
</NInputGroup>
|
||||
</NFormItem>
|
||||
|
||||
<!-- CAA -->
|
||||
<NFormItem :label="t('records.content')" v-if="RecordTypes.RecordTypeCAA === record.record_type"
|
||||
path="caa">
|
||||
<NInputGroup>
|
||||
<NInputNumber :placeholder="t('records.form.flag')"
|
||||
v-model:value="(record.content as CAARecord).flag" style="width: 20%;"
|
||||
:show-button="false" />
|
||||
<NInput :placeholder="t('records.form.tag')" v-model:value="(record.content as CAARecord).tag"
|
||||
style="width: 40%;" />
|
||||
<NInput :placeholder="t('records.form.value')"
|
||||
v-model:value="(record.content as CAARecord).value" style="width: 40%;" />
|
||||
</NInputGroup>
|
||||
</NFormItem>
|
||||
</NForm>
|
||||
|
||||
<template #action>
|
||||
<NFlex justify="end">
|
||||
<NButton size="small" @click="show = false">
|
||||
<template #icon>
|
||||
<NIcon>
|
||||
<Times />
|
||||
</NIcon>
|
||||
</template>
|
||||
{{ t('common.cancel') }}
|
||||
</NButton>
|
||||
|
||||
<NButton size="small" type="primary" :loading="loading" :disabled="invalidData !== (validationFlags.content | validationFlags.name)" @click="confirm" attr-type="submit">
|
||||
<template #icon>
|
||||
<NIcon>
|
||||
<Check />
|
||||
</NIcon>
|
||||
</template>
|
||||
{{ t('common.confirm') }}
|
||||
</NButton>
|
||||
</NFlex>
|
||||
</template>
|
||||
</NCard>
|
||||
</NModal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
NModal,
|
||||
NCard,
|
||||
NForm,
|
||||
NFormItem,
|
||||
NFlex,
|
||||
NButton,
|
||||
NInput,
|
||||
NInputNumber,
|
||||
NInputGroup,
|
||||
NSelect,
|
||||
NIcon,
|
||||
useNotification,
|
||||
type FormRules,
|
||||
type SelectOption,
|
||||
type FormItemRule,
|
||||
} from 'naive-ui'
|
||||
import { getErrorInfo } from '@/apis/api';
|
||||
import {
|
||||
useRecordStore,
|
||||
RecordTypes,
|
||||
type Record,
|
||||
type ARecord,
|
||||
type AAAARecord,
|
||||
type CAARecord,
|
||||
type CNAMERecord,
|
||||
type NSRecord,
|
||||
type SRVRecord,
|
||||
type TXTRecord,
|
||||
type MXRecord,
|
||||
type RecordT,
|
||||
} from '@/stores/records';
|
||||
import { Check, Times } from '@vicons/fa';
|
||||
import { ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const enum validationFlags {
|
||||
name = 1,
|
||||
content = name << 1
|
||||
}
|
||||
|
||||
const { t } = useI18n()
|
||||
const props = defineProps<{
|
||||
record: Record,
|
||||
domain: string,
|
||||
}>()
|
||||
const emit = defineEmits(['reload-records'])
|
||||
|
||||
const invalidData = ref(validationFlags.content)
|
||||
const show = defineModel<boolean>('show', { default: false })
|
||||
const loading = ref(false)
|
||||
const notification = useNotification()
|
||||
const recordStore = useRecordStore()
|
||||
const recordTypeOptions = Object.entries(RecordTypes).filter(
|
||||
e => e[1] !== RecordTypes.RecordTypeSOA
|
||||
).map(e => {
|
||||
return {
|
||||
label: e[1],
|
||||
value: e[1]
|
||||
} as SelectOption
|
||||
})
|
||||
const rules = {
|
||||
name: {
|
||||
trigger: 'blur',
|
||||
validator() {
|
||||
invalidData.value |= validationFlags.name
|
||||
if (!props.record.name || props.record.name === '') {
|
||||
invalidData.value &= ~validationFlags.name
|
||||
return new Error(t('common.mandatory'))
|
||||
}
|
||||
|
||||
if (props.record.name.includes(' ')) {
|
||||
invalidData.value &= ~validationFlags.name
|
||||
return new Error(t('records.errors.hasSpace'))
|
||||
}
|
||||
|
||||
if (props.record.name.startsWith('.') || props.record.name.endsWith('.')) {
|
||||
invalidData.value &= ~validationFlags.name
|
||||
return new Error(t('records.errors.badName.dotAndMinus'))
|
||||
}
|
||||
|
||||
if (props.record.name.startsWith('-') || props.record.name.endsWith('-')) {
|
||||
invalidData.value &= ~validationFlags.name
|
||||
return new Error(t('records.errors.badName.dotAndMinus'))
|
||||
}
|
||||
|
||||
if (props.record.name.includes('..')) {
|
||||
invalidData.value &= ~validationFlags.name
|
||||
return new Error(t('records.errors.badName.doubleDots'))
|
||||
}
|
||||
|
||||
if (props.record.name.split('.').filter(e => e.length > 63).length > 0) {
|
||||
invalidData.value &= ~validationFlags.name
|
||||
return new Error(t('records.errors.badName.longerThan63'))
|
||||
}
|
||||
return true
|
||||
}
|
||||
},
|
||||
txt : {
|
||||
trigger: 'blur' ,
|
||||
validator() {
|
||||
invalidData.value |= validationFlags.content
|
||||
if (props.record.record_type !== RecordTypes.RecordTypeTXT) return true
|
||||
|
||||
const r = (props.record.content as TXTRecord)
|
||||
if (!r || !r.text || r.text === '') {
|
||||
invalidData.value &= ~validationFlags.content
|
||||
return new Error(t('common.mandatory'))
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
},
|
||||
host: {
|
||||
trigger: 'blur',
|
||||
validator() {
|
||||
invalidData.value |= validationFlags.content
|
||||
if ([RecordTypes.RecordTypeCNAME, RecordTypes.RecordTypeNS].indexOf(props.record.record_type) === -1) return true
|
||||
|
||||
const r = (props.record.content as CNAMERecord | NSRecord)
|
||||
if (!r || !r.host || r.host === '') {
|
||||
invalidData.value &= ~validationFlags.content
|
||||
return new Error(t('common.mandatory'))
|
||||
}
|
||||
|
||||
if (r.host.includes(' ')) {
|
||||
invalidData.value &= ~validationFlags.content
|
||||
return new Error(t('records.errors.hasSpace'))
|
||||
}
|
||||
|
||||
if (!r.host.endsWith('.')) {
|
||||
invalidData.value &= ~validationFlags.content
|
||||
return new Error(t('records.errors.endWithDot'))
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
},
|
||||
ip: {
|
||||
trigger: 'blur',
|
||||
validator() {
|
||||
invalidData.value |= validationFlags.content
|
||||
if ([RecordTypes.RecordTypeA, RecordTypes.RecordTypeAAAA].indexOf(props.record.record_type) === -1) return true
|
||||
const r = (props.record.content as AAAARecord | ARecord)
|
||||
if (!r || !r.ip || r.ip === '') {
|
||||
invalidData.value &= ~validationFlags.content
|
||||
return new Error(t('common.mandatory'))
|
||||
}
|
||||
|
||||
switch (props.record.record_type) {
|
||||
case RecordTypes.RecordTypeA:
|
||||
if (!/^((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])$/.test(r.ip)) {
|
||||
invalidData.value &= ~validationFlags.content
|
||||
return new Error(t('records.errors.badIPv4'))
|
||||
}
|
||||
|
||||
break
|
||||
case RecordTypes.RecordTypeAAAA:
|
||||
if (!/^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$/.test(r.ip)) {
|
||||
invalidData.value &= ~validationFlags.content
|
||||
return new Error(t('records.errors.badIPv6'))
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
},
|
||||
mx: {
|
||||
trigger: 'blur',
|
||||
validator() {
|
||||
invalidData.value |= validationFlags.content
|
||||
if (props.record.record_type !== RecordTypes.RecordTypeMX) return true
|
||||
const r = (props.record.content as MXRecord)
|
||||
|
||||
if (!r || !r.host || !r.preference || r.host === '') {
|
||||
invalidData.value &= ~validationFlags.content
|
||||
return new Error(t('common.mandatory'))
|
||||
}
|
||||
|
||||
if (r.host.includes(' ')) {
|
||||
invalidData.value &= ~validationFlags.content
|
||||
return new Error(t('records.errors.hasSpace'))
|
||||
}
|
||||
|
||||
if (!r.host.endsWith('.')) {
|
||||
invalidData.value &= ~validationFlags.content
|
||||
return new Error(t('records.errors.endWithDot'))
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
},
|
||||
srv: {
|
||||
trigger: 'blur',
|
||||
validator() {
|
||||
invalidData.value |= validationFlags.content
|
||||
if (props.record.record_type !== RecordTypes.RecordTypeSRV) return true
|
||||
const r = (props.record.content as SRVRecord)
|
||||
|
||||
if (!r || !r.port || !r.priority || !r.weight || !r.target || r.target === '') {
|
||||
invalidData.value &= ~validationFlags.content
|
||||
return new Error(t('common.mandatory'))
|
||||
}
|
||||
|
||||
if (r.target.includes(' ')) {
|
||||
invalidData.value &= ~validationFlags.content
|
||||
return new Error(t('records.errors.hasSpace'))
|
||||
}
|
||||
|
||||
if (!r.target.endsWith('.')) {
|
||||
invalidData.value &= ~validationFlags.content
|
||||
return new Error(t('records.errors.endWithDot'))
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
},
|
||||
caa: {
|
||||
trigger: 'blur',
|
||||
validator() {
|
||||
invalidData.value |= validationFlags.content
|
||||
if (props.record.record_type !== RecordTypes.RecordTypeCAA) return true
|
||||
const r = (props.record.content as CAARecord)
|
||||
|
||||
if (!r || !r.flag || !r.tag || r.tag === '' || !r.value || r.value === '') {
|
||||
invalidData.value &= ~validationFlags.content
|
||||
return new Error(t('common.mandatory'))
|
||||
}
|
||||
|
||||
if (r.tag.includes(' ')) {
|
||||
invalidData.value &= ~validationFlags.content
|
||||
return new Error(t('records.errors.hasSpace'))
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
} as FormRules
|
||||
|
||||
function clearRecordContent() {
|
||||
props.record.content = {} as RecordT
|
||||
}
|
||||
|
||||
async function confirm() {
|
||||
loading.value = true;
|
||||
try {
|
||||
if (!props.record.id || props.record.id < 1) {
|
||||
await recordStore.addRecord(props.domain, props.record)
|
||||
} else {
|
||||
await recordStore.updateRecord(props.domain, props.record)
|
||||
}
|
||||
emit('reload-records')
|
||||
show.value = false
|
||||
} catch (e) {
|
||||
const msg = getErrorInfo(e)
|
||||
notification.error(msg)
|
||||
console.error(e)
|
||||
}
|
||||
loading.value = false;
|
||||
}
|
||||
</script>
|
@ -3,7 +3,7 @@ import './assets/main.css'
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
|
||||
import App from './App.vue'
|
||||
import App from './App'
|
||||
import router from './router'
|
||||
import i18n from './locale/i18n'
|
||||
|
||||
|
@ -1,23 +1,27 @@
|
||||
import { createRouter, createWebHashHistory } from 'vue-router'
|
||||
import HomeView from '../views/HomeView.vue'
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHashHistory(),
|
||||
routes: [
|
||||
{
|
||||
path: '/',
|
||||
name: 'home',
|
||||
component: HomeView
|
||||
redirect: '/domains'
|
||||
},
|
||||
{
|
||||
path: '/domains',
|
||||
name: 'domains',
|
||||
component: () => import('../views/DomainsView.vue')
|
||||
meta: {
|
||||
type: 'domains'
|
||||
},
|
||||
component: () => import('@/views/DomainsView')
|
||||
},
|
||||
{
|
||||
path: '/records/:domain',
|
||||
name: 'records',
|
||||
component: () => import('../views/RecordsView.vue'),
|
||||
meta: {
|
||||
type: 'records'
|
||||
},
|
||||
component: () => import('@/views/RecordsView'),
|
||||
props: true
|
||||
}
|
||||
]
|
||||
|
3
web/src/views/DomainsView.css
Normal file
3
web/src/views/DomainsView.css
Normal file
@ -0,0 +1,3 @@
|
||||
.n-card {
|
||||
width: 32vw;
|
||||
}
|
87
web/src/views/DomainsView.tsx
Normal file
87
web/src/views/DomainsView.tsx
Normal file
@ -0,0 +1,87 @@
|
||||
import './DomainsView.css'
|
||||
|
||||
import { NSpin, NFlex, NCard, NButton, NIcon, NModalProvider, createDiscreteApi } from 'naive-ui'
|
||||
import { PlusSquare } from "@vicons/fa"
|
||||
import { type Domain, useDomainStore } from '@/stores/domains'
|
||||
import { getErrorInfo } from '@/apis/api'
|
||||
import DomainInfo from '@/components/domains/DomainInfo'
|
||||
import DomainOps from '@/components/domains/DomainOps'
|
||||
import DomainRemoveModal from '@/components/domains/DomainRemoveModal'
|
||||
import DomainEditModal from '@/components/domains/DomainEditModal'
|
||||
import { ref } from 'vue'
|
||||
|
||||
const domainStore = useDomainStore()
|
||||
const { notification } = createDiscreteApi(['notification'])
|
||||
|
||||
const loading = ref(true);
|
||||
const removeModalShow = ref(false);
|
||||
const editModalShow = ref(false);
|
||||
const operationDomain = ref({} as Domain)
|
||||
|
||||
function showRemoveModal(domain: Domain) {
|
||||
operationDomain.value = domain
|
||||
removeModalShow.value = true
|
||||
}
|
||||
|
||||
function showEditModal(domain: Domain) {
|
||||
operationDomain.value = domain
|
||||
editModalShow.value = true
|
||||
}
|
||||
|
||||
function addDomain() {
|
||||
const domain = {
|
||||
refresh_interval: 86400,
|
||||
retry_interval: 7200,
|
||||
expiry_period: 3600000,
|
||||
negative_ttl: 86400,
|
||||
serial_number: 1,
|
||||
} as Domain
|
||||
showEditModal(domain)
|
||||
}
|
||||
|
||||
function DomainsView() {
|
||||
try {
|
||||
domainStore.loadDomains()
|
||||
loading.value = false
|
||||
} catch (e) {
|
||||
const msg = getErrorInfo(e)
|
||||
notification.error(msg)
|
||||
console.error(e)
|
||||
}
|
||||
return (
|
||||
<>
|
||||
{
|
||||
loading.value ? <NSpin size="large" /> :
|
||||
<NModalProvider>
|
||||
<NFlex vertical>
|
||||
{
|
||||
domainStore.domains.map((domain: Domain) => (
|
||||
<NCard title={domain.domain_name} key={domain.id} size='large' hoverable>
|
||||
{{
|
||||
default: () => <DomainInfo domain={domain} />,
|
||||
action: () => <DomainOps domain={domain} onRemoveDomain={showRemoveModal} onEditDomain={showEditModal} />
|
||||
}}
|
||||
|
||||
</NCard>
|
||||
))
|
||||
}
|
||||
|
||||
<NCard hoverable>
|
||||
<NButton block quaternary size="large" onClick={addDomain}>
|
||||
{{
|
||||
icon: () => <NIcon component={PlusSquare} depth={5} />
|
||||
}}
|
||||
</NButton>
|
||||
</NCard>
|
||||
</NFlex>
|
||||
<DomainRemoveModal show={removeModalShow.value} domain={operationDomain.value} onUpdate:show={(v: boolean) => removeModalShow.value = v} />
|
||||
<DomainEditModal show={editModalShow.value} domain={operationDomain.value} onUpdate:show={(v: boolean) => editModalShow.value = v} />
|
||||
</NModalProvider>
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
DomainsView.displayName = 'DomainsView'
|
||||
|
||||
export default DomainsView
|
@ -1,83 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { NSpin, NFlex, NCard, NButton, NIcon, useNotification, NModalProvider } from 'naive-ui'
|
||||
import { PlusSquare } from "@vicons/fa"
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { type Domain, useDomainStore } from '@/stores/domains'
|
||||
import { getErrorInfo } from '@/apis/api'
|
||||
import DomainInfo from '@/components/domains/DomainInfo'
|
||||
import DomainOps from '@/components/domains/DomainOps'
|
||||
import DomainRemoveModal from '@/components/domains/DomainRemoveModal'
|
||||
import DomainEditModal from '@/components/domains/DomainEditModal'
|
||||
|
||||
const domainStore = useDomainStore()
|
||||
const notification = useNotification()
|
||||
|
||||
const loading = ref(true);
|
||||
const removeModalShow = ref(false);
|
||||
const editModalShow = ref(false);
|
||||
const operationDomain = ref({} as Domain)
|
||||
|
||||
onMounted(() => {
|
||||
try {
|
||||
domainStore.loadDomains()
|
||||
loading.value = false
|
||||
} catch (error) {
|
||||
const msg = getErrorInfo(error)
|
||||
notification.error(msg)
|
||||
console.error(error)
|
||||
}
|
||||
})
|
||||
|
||||
function showRemoveModal(domain: Domain) {
|
||||
operationDomain.value = domain
|
||||
removeModalShow.value = true
|
||||
}
|
||||
|
||||
function showEditModal(domain: Domain) {
|
||||
operationDomain.value = domain
|
||||
editModalShow.value = true
|
||||
}
|
||||
|
||||
function addDomain() {
|
||||
const domain = {
|
||||
refresh_interval: 86400,
|
||||
retry_interval: 7200,
|
||||
expiry_period: 3600000,
|
||||
negative_ttl: 86400,
|
||||
serial_number: 1,
|
||||
} as Domain
|
||||
showEditModal(domain)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<NSpin size="large" v-if="loading" />
|
||||
<NModalProvider v-else>
|
||||
<NFlex id="domains" vertical>
|
||||
<NCard v-for="domain in domainStore.domains" :title="domain.domain_name" v-bind:key="domain.id"
|
||||
size="large" hoverable>
|
||||
<DomainInfo :domain="domain" />
|
||||
<template #action>
|
||||
<DomainOps :domain="domain" @remove-domain="showRemoveModal" @edit-domain="showEditModal" />
|
||||
</template>
|
||||
</NCard>
|
||||
<NCard hoverable>
|
||||
<NButton block quaternary size="large" @click="addDomain">
|
||||
<template #icon>
|
||||
<NIcon :component="PlusSquare" :depth="5" />
|
||||
</template>
|
||||
</NButton>
|
||||
</NCard>
|
||||
</NFlex>
|
||||
<DomainRemoveModal v-model:show="removeModalShow" :domain="operationDomain" />
|
||||
<DomainEditModal v-model:show="editModalShow" :domain="operationDomain" />
|
||||
</NModalProvider>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.n-card {
|
||||
width: 32vw;
|
||||
}
|
||||
</style>
|
@ -1,6 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import router from '@/router';
|
||||
router.push("/domains")
|
||||
</script>
|
||||
|
||||
<template></template>
|
7
web/src/views/RecordsView.css
Normal file
7
web/src/views/RecordsView.css
Normal file
@ -0,0 +1,7 @@
|
||||
div#records {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
padding: 1.5rem;
|
||||
}
|
250
web/src/views/RecordsView.tsx
Normal file
250
web/src/views/RecordsView.tsx
Normal file
@ -0,0 +1,250 @@
|
||||
import './RecordsView.css'
|
||||
|
||||
import {
|
||||
NSpin, NPageHeader,
|
||||
NFlex, NButton, NIcon, NGrid, NGi,
|
||||
NStatistic, NDataTable, NInput,
|
||||
NModalProvider,
|
||||
createDiscreteApi
|
||||
} from 'naive-ui'
|
||||
import type { DataTableColumns } from 'naive-ui'
|
||||
import { useRecordStore, type Record, type SOARecord, RecordTypes, type ARecord } from '@/stores/records'
|
||||
import { getErrorInfo } from '@/apis/api'
|
||||
import { PlusSquare, RedoAlt, CheckCircle, Clock, Cogs, Search } from '@vicons/fa'
|
||||
import router from '@/router';
|
||||
import RecordOps from '@/components/records/RecordOps'
|
||||
import RecordEditModal from '@/components/records/RecordEditModal'
|
||||
import i18n from '@/locale/i18n'
|
||||
import { ref } from 'vue'
|
||||
|
||||
const { t } = i18n.global
|
||||
|
||||
type Props = {
|
||||
domain: string
|
||||
}
|
||||
|
||||
const recordStore = useRecordStore()
|
||||
const { notification } = createDiscreteApi(['notification'])
|
||||
const editModalShow = ref(false)
|
||||
const editingRecord = ref<Record>({} as Record)
|
||||
const loading = ref(true);
|
||||
const records = ref<Record[] | undefined>([] as Record[]);
|
||||
const soa = ref<SOARecord>({} as SOARecord)
|
||||
const reloadRecords = () => records.value = recordStore.records?.filter(e => e.record_type !== RecordTypes.RecordTypeSOA)
|
||||
|
||||
async function refreshRecords(domain: string) {
|
||||
try {
|
||||
await recordStore.loadRecords(domain)
|
||||
reloadRecords()
|
||||
soa.value = recordStore.records?.find(e => e.record_type === RecordTypes.RecordTypeSOA)?.content as SOARecord
|
||||
} catch (err) {
|
||||
const msg = getErrorInfo(err)
|
||||
notification.error(msg)
|
||||
console.error(err)
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
router.push('/domains')
|
||||
}
|
||||
|
||||
function searchRecord(value: string) {
|
||||
if (value.length > 0) {
|
||||
records.value = recordStore.records?.
|
||||
filter(e => e.record_type !== RecordTypes.RecordTypeSOA).
|
||||
filter(e => !!~e.name.indexOf(value))
|
||||
} else {
|
||||
records.value = recordStore.records?.
|
||||
filter(e => e.record_type !== RecordTypes.RecordTypeSOA)
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteRecord(domain: string, record: Record) {
|
||||
try {
|
||||
await recordStore.removeRecord(domain, record)
|
||||
reloadRecords()
|
||||
} catch (err) {
|
||||
const msg = getErrorInfo(err)
|
||||
notification.error(msg)
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
|
||||
function showEditing(domain: string, record: Record) {
|
||||
editModalShow.value = true
|
||||
editingRecord.value = record
|
||||
}
|
||||
|
||||
function newRecord(domain: string) {
|
||||
showEditing(domain, {
|
||||
zone: `${domain}.`,
|
||||
ttl: 500,
|
||||
record_type: RecordTypes.RecordTypeA,
|
||||
content: {
|
||||
ip: ''
|
||||
} as ARecord
|
||||
} as Record)
|
||||
}
|
||||
|
||||
const generateColumns = (domain: string) => [
|
||||
{
|
||||
key: 'no',
|
||||
title: '#',
|
||||
render(_, index) {
|
||||
return index + 1
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'name',
|
||||
title: t("records.name"),
|
||||
},
|
||||
{
|
||||
key: 'record_type',
|
||||
title: t('records.recordType')
|
||||
},
|
||||
{
|
||||
key: 'content',
|
||||
title: t('records.content'),
|
||||
render(row: Record) {
|
||||
return Object.entries(row.content).map(i => i[1]).join(" ")
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'ttl',
|
||||
title: 'TTL (s)'
|
||||
},
|
||||
{
|
||||
key: '',
|
||||
render(row: Record) {
|
||||
return <RecordOps record={row} domain={domain} onRecord-delete={deleteRecord} onEdit-record={showEditing} />
|
||||
}
|
||||
}
|
||||
] as DataTableColumns<Record>
|
||||
|
||||
const statRefresh = () => (
|
||||
<NGi>
|
||||
<NStatistic value={soa.value.refresh}>
|
||||
{{
|
||||
suffix: () => 's',
|
||||
label: () => (
|
||||
<>
|
||||
<NIcon component={RedoAlt} style={{ transform: 'translateY(2px)' }} />
|
||||
<span>{t('records.refresh')}</span>
|
||||
</>
|
||||
)
|
||||
}}
|
||||
</NStatistic>
|
||||
</NGi>
|
||||
)
|
||||
|
||||
const statRetry = () => (
|
||||
<NGi>
|
||||
<NStatistic value={soa.value.retry}>
|
||||
{{
|
||||
suffix: () => 's',
|
||||
label: () => (
|
||||
<>
|
||||
<NIcon component={CheckCircle} style={{ transform: 'translateY(2px)' }} />
|
||||
<span>{t('records.retry')}</span>
|
||||
</>
|
||||
)
|
||||
}}
|
||||
</NStatistic>
|
||||
</NGi>
|
||||
)
|
||||
|
||||
const statExpire = () => (
|
||||
<NGi>
|
||||
<NStatistic value={soa.value.expire}>
|
||||
{{
|
||||
suffix: () => 's',
|
||||
label: () => (
|
||||
<>
|
||||
<NIcon component={Clock} style={{ transform: 'translateY(2px)' }} />
|
||||
<span>{t('records.expire')}</span>
|
||||
</>
|
||||
)
|
||||
}}
|
||||
</NStatistic>
|
||||
</NGi>
|
||||
)
|
||||
|
||||
const statTTL = () => (
|
||||
<NGi>
|
||||
<NStatistic value={soa.value.minttl}>
|
||||
{{
|
||||
suffix: () => 's',
|
||||
label: () => (
|
||||
<>
|
||||
<NIcon component={Cogs} style={{ transform: 'translateY(2px)' }} />
|
||||
<span>{t('records.ttl')}</span>
|
||||
</>
|
||||
)
|
||||
}}
|
||||
</NStatistic>
|
||||
</NGi>
|
||||
)
|
||||
|
||||
function recordsViewBodyHeaderExtra() {
|
||||
return (
|
||||
<NGrid cols={4} >
|
||||
<statRefresh />
|
||||
<statRetry />
|
||||
<statExpire />
|
||||
<statTTL />
|
||||
</NGrid>
|
||||
)
|
||||
}
|
||||
|
||||
function recordsViewBody({ domain }: Props) {
|
||||
const columns = generateColumns(domain)
|
||||
return (
|
||||
<NModalProvider>
|
||||
<NPageHeader title={t('domains.dnsRecord')} subtitle={domain} onBack={goBack}>
|
||||
{{
|
||||
extra: () => (
|
||||
<NFlex wrap={false} justify="end" inline>
|
||||
<NButton type="primary" onClick={() => newRecord(domain)}>
|
||||
{{
|
||||
icon: () => <NIcon component={PlusSquare} />,
|
||||
default: () => t('common.add')
|
||||
}}
|
||||
</NButton>
|
||||
<NInput placeholder={t('records.search')} onUpdate:value={searchRecord} clearable>
|
||||
{{
|
||||
prefix: () => <NIcon component={Search} />
|
||||
}}
|
||||
</NInput>
|
||||
</NFlex>
|
||||
),
|
||||
default: () => <recordsViewBodyHeaderExtra />
|
||||
}}
|
||||
</NPageHeader>
|
||||
<br />
|
||||
<NDataTable data={records.value} columns={columns} pagination={{ pageSize: 20 }} />
|
||||
</NModalProvider>
|
||||
)
|
||||
}
|
||||
|
||||
function RecordsView({ domain }: Props) {
|
||||
try {
|
||||
refreshRecords(domain)
|
||||
} catch (err) {
|
||||
const msg = getErrorInfo(err)
|
||||
notification.error(msg)
|
||||
console.error(err)
|
||||
}
|
||||
return (
|
||||
<div id='records'>
|
||||
<RecordEditModal show={editModalShow.value} domain={domain} record={editingRecord.value} onReloadRecords={reloadRecords} />
|
||||
{
|
||||
loading.value ? <NSpin size='large' /> : <recordsViewBody domain={domain} />
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
RecordsView.displayName = 'RecordsView'
|
||||
export default RecordsView
|
@ -1,231 +0,0 @@
|
||||
<script setup lang="tsx">
|
||||
import {
|
||||
NSpin, NPageHeader, useNotification,
|
||||
NFlex, NButton, NIcon, NGrid, NGi,
|
||||
NStatistic, NDataTable, NInput,
|
||||
NModalProvider
|
||||
} from 'naive-ui'
|
||||
import type { DataTableColumns } from 'naive-ui'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useRecordStore, type Record, type SOARecord, RecordTypes } from '@/stores/records'
|
||||
import { getErrorInfo } from '@/apis/api'
|
||||
import { PlusSquare, RedoAlt, CheckCircle, Clock, Cogs, Search } from '@vicons/fa'
|
||||
import router from '@/router';
|
||||
import RecordOps from '@/components/records/RecordOps'
|
||||
import RecordEditModal from '@/components/records/RecordEditModal.vue'
|
||||
import { useI18n } from 'vue-i18n';
|
||||
const { t } = useI18n()
|
||||
|
||||
const props = defineProps<{
|
||||
domain: string
|
||||
}>()
|
||||
|
||||
const columns = [
|
||||
{
|
||||
key: 'no',
|
||||
title: '#',
|
||||
render(_, index) {
|
||||
return index + 1
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'name',
|
||||
title: t("records.name"),
|
||||
},
|
||||
{
|
||||
key: 'record_type',
|
||||
title: t('records.recordType')
|
||||
},
|
||||
{
|
||||
key: 'content',
|
||||
title: t('records.content'),
|
||||
render(row: Record) {
|
||||
return Object.entries(row.content).map(i => i[1]).join(" ")
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'ttl',
|
||||
title: 'TTL (s)'
|
||||
},
|
||||
{
|
||||
key: '',
|
||||
render(row: Record) {
|
||||
return <RecordOps record={row} domain={props.domain} onRecord-delete={deleteRecord} onEdit-record={showEditing} />
|
||||
}
|
||||
}
|
||||
] as DataTableColumns<Record>
|
||||
|
||||
const recordStore = useRecordStore()
|
||||
const notification = useNotification()
|
||||
|
||||
const records = ref<Record[] | undefined>([] as Record[]);
|
||||
const soa = ref<SOARecord>({} as SOARecord)
|
||||
const editModalShow = ref(false)
|
||||
const editingRecord = ref<Record>({} as Record)
|
||||
const loading = ref(true);
|
||||
onMounted(() => {
|
||||
try {
|
||||
refreshRecords()
|
||||
} catch (err) {
|
||||
const msg = getErrorInfo(err)
|
||||
notification.error(msg)
|
||||
console.error(err)
|
||||
}
|
||||
})
|
||||
|
||||
const reloadRecords = () => records.value = recordStore.records?.filter(e => e.record_type !== RecordTypes.RecordTypeSOA)
|
||||
|
||||
|
||||
async function refreshRecords() {
|
||||
try {
|
||||
await recordStore.loadRecords(props.domain)
|
||||
reloadRecords()
|
||||
soa.value = recordStore.records?.find(e => e.record_type === RecordTypes.RecordTypeSOA)?.content as SOARecord
|
||||
} catch (err) {
|
||||
const msg = getErrorInfo(err)
|
||||
notification.error(msg)
|
||||
console.error(err)
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
router.push('/domains')
|
||||
}
|
||||
|
||||
function searchRecord(value: string) {
|
||||
if (value.length > 0) {
|
||||
records.value = recordStore.records?.
|
||||
filter(e => e.record_type !== RecordTypes.RecordTypeSOA).
|
||||
filter(e => !!~e.name.indexOf(value))
|
||||
} else {
|
||||
records.value = recordStore.records?.
|
||||
filter(e => e.record_type !== RecordTypes.RecordTypeSOA)
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteRecord(domain: string, record: Record) {
|
||||
try {
|
||||
await recordStore.removeRecord(domain, record)
|
||||
reloadRecords()
|
||||
} catch (err) {
|
||||
const msg = getErrorInfo(err)
|
||||
notification.error(msg)
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
|
||||
function showEditing(domain: string, record: Record) {
|
||||
editModalShow.value = true
|
||||
editingRecord.value = record
|
||||
}
|
||||
|
||||
function newRecord() {
|
||||
showEditing(props.domain, {
|
||||
zone: `${props.domain}.`,
|
||||
ttl: 500
|
||||
} as Record)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div id="records">
|
||||
<RecordEditModal v-model:show="editModalShow" :domain="domain" :record="editingRecord"
|
||||
@reload-records="reloadRecords" />
|
||||
<NSpin size="large" v-if="loading" />
|
||||
<div v-else class="content">
|
||||
<NModalProvider>
|
||||
<NPageHeader :title="t('domains.dnsRecord')" :subtitle="domain" @back="goBack">
|
||||
<template #extra>
|
||||
<NFlex :wrap="false" justify="end" inline>
|
||||
<NButton type="primary" @click="newRecord">
|
||||
<template #icon>
|
||||
<NIcon>
|
||||
<PlusSquare />
|
||||
</NIcon>
|
||||
</template>
|
||||
{{ t('common.add') }}
|
||||
</NButton>
|
||||
<NInput :placeholder="t('records.search')" @update:value="searchRecord" clearable>
|
||||
<template #prefix>
|
||||
<NIcon :component="Search" />
|
||||
</template>
|
||||
</NInput>
|
||||
</NFlex>
|
||||
</template>
|
||||
<NGrid :cols="4">
|
||||
<NGi>
|
||||
<NStatistic :value="soa?.refresh">
|
||||
<template #suffix>
|
||||
s
|
||||
</template>
|
||||
<template #label>
|
||||
<NIcon class="icon">
|
||||
<RedoAlt />
|
||||
</NIcon>
|
||||
{{ t('records.refresh') }}
|
||||
</template>
|
||||
</NStatistic>
|
||||
</NGi>
|
||||
<NGi>
|
||||
<NStatistic :value="soa?.retry">
|
||||
<template #suffix>
|
||||
s
|
||||
</template>
|
||||
<template #label>
|
||||
<NIcon class="icon">
|
||||
<CheckCircle />
|
||||
</NIcon>
|
||||
{{ t('records.retry') }}
|
||||
</template>
|
||||
</NStatistic>
|
||||
</NGi>
|
||||
<NGi>
|
||||
<NStatistic :value="soa?.expire">
|
||||
<template #suffix>
|
||||
s
|
||||
</template>
|
||||
<template #label>
|
||||
<NIcon class="icon">
|
||||
<Clock />
|
||||
</NIcon>
|
||||
{{ t('records.expire') }}
|
||||
</template>
|
||||
</NStatistic>
|
||||
</NGi>
|
||||
<NGi>
|
||||
<NStatistic :value="soa?.minttl">
|
||||
<template #suffix>
|
||||
s
|
||||
</template>
|
||||
<template #label>
|
||||
<NIcon class="icon">
|
||||
<Cogs />
|
||||
</NIcon>
|
||||
{{ t('records.ttl') }}
|
||||
</template>
|
||||
</NStatistic>
|
||||
</NGi>
|
||||
</NGrid>
|
||||
</NPageHeader>
|
||||
<br />
|
||||
<NDataTable :data="records" :columns="columns" :pagination="{ pageSize: 20 }" />
|
||||
</NModalProvider>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.icon {
|
||||
transform: translateY(2px);
|
||||
}
|
||||
|
||||
div#records {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
</style>
|
@ -7,5 +7,10 @@
|
||||
{
|
||||
"path": "./tsconfig.app.json"
|
||||
}
|
||||
]
|
||||
],
|
||||
"compilerOptions": {
|
||||
"jsx": "preserve",
|
||||
"jsxFactory": "h",
|
||||
"jsxFragmentFactory": "Fragment",
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user