|
@@ -26,6 +26,7 @@ import { toast } from 'vue-sonner'
|
|
|
import { api } from '@/api/client'
|
|
import { api } from '@/api/client'
|
|
|
import type { DnsRecord, Zone } from '@/api/types'
|
|
import type { DnsRecord, Zone } from '@/api/types'
|
|
|
import { ref, watch, onMounted } from 'vue'
|
|
import { ref, watch, onMounted } from 'vue'
|
|
|
|
|
+import { Locate, Loader2 } from 'lucide-vue-next'
|
|
|
import SpinnerLoader from '@/components/SpinnerLoader.vue'
|
|
import SpinnerLoader from '@/components/SpinnerLoader.vue'
|
|
|
|
|
|
|
|
const props = defineProps<{
|
|
const props = defineProps<{
|
|
@@ -39,13 +40,26 @@ const emit = defineEmits<{
|
|
|
}>()
|
|
}>()
|
|
|
|
|
|
|
|
const submitting = ref(false)
|
|
const submitting = ref(false)
|
|
|
|
|
+const fetchingIp = ref(false)
|
|
|
const zones = ref<Zone[]>([])
|
|
const zones = ref<Zone[]>([])
|
|
|
const isEdit = () => !!props.record
|
|
const isEdit = () => !!props.record
|
|
|
|
|
|
|
|
|
|
+const useCurrentIp = async () => {
|
|
|
|
|
+ fetchingIp.value = true
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await api.get<{ ip: string }>('/api/ip')
|
|
|
|
|
+ setFieldValue('content', res.ip)
|
|
|
|
|
+ } catch {
|
|
|
|
|
+ toast.error('Failed to fetch current IP')
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ fetchingIp.value = false
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
const schema = toTypedSchema(
|
|
const schema = toTypedSchema(
|
|
|
z.object({
|
|
z.object({
|
|
|
zone_id: z.number({ required_error: 'Zone is required' }).min(1, 'Zone is required'),
|
|
zone_id: z.number({ required_error: 'Zone is required' }).min(1, 'Zone is required'),
|
|
|
- cf_record_id: z.string().min(1, 'Cloudflare Record ID is required'),
|
|
|
|
|
|
|
+ cf_record_id: z.string().optional(),
|
|
|
name: z.string().min(1, 'Name is required'),
|
|
name: z.string().min(1, 'Name is required'),
|
|
|
type: z.string().min(1, 'Type is required'),
|
|
type: z.string().min(1, 'Type is required'),
|
|
|
content: z.string().min(1, 'Content is required'),
|
|
content: z.string().min(1, 'Content is required'),
|
|
@@ -54,7 +68,7 @@ const schema = toTypedSchema(
|
|
|
}),
|
|
}),
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
-const { handleSubmit, resetForm, setValues } = useForm({
|
|
|
|
|
|
|
+const { handleSubmit, resetForm, setValues, setFieldValue } = useForm({
|
|
|
validationSchema: schema,
|
|
validationSchema: schema,
|
|
|
initialValues: {
|
|
initialValues: {
|
|
|
type: 'A',
|
|
type: 'A',
|
|
@@ -79,6 +93,9 @@ watch(
|
|
|
})
|
|
})
|
|
|
} else {
|
|
} else {
|
|
|
resetForm()
|
|
resetForm()
|
|
|
|
|
+ if (zones.value.length === 1) {
|
|
|
|
|
+ setFieldValue('zone_id', zones.value[0].id)
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
},
|
|
},
|
|
|
{ flush: 'post' },
|
|
{ flush: 'post' },
|
|
@@ -128,53 +145,42 @@ const onSubmit = handleSubmit(async (values) => {
|
|
|
</DialogDescription>
|
|
</DialogDescription>
|
|
|
</DialogHeader>
|
|
</DialogHeader>
|
|
|
<form @submit="onSubmit" class="space-y-4">
|
|
<form @submit="onSubmit" class="space-y-4">
|
|
|
- <FormField name="zone_id" v-slot="{ componentField }">
|
|
|
|
|
|
|
+ <FormField v-if="isEdit()" name="cf_record_id" v-slot="{ field }">
|
|
|
<FormItem>
|
|
<FormItem>
|
|
|
- <FormLabel>Zone</FormLabel>
|
|
|
|
|
- <Select v-bind="componentField" :disabled="isEdit()">
|
|
|
|
|
- <FormControl>
|
|
|
|
|
- <SelectTrigger>
|
|
|
|
|
- <SelectValue placeholder="Select a zone" />
|
|
|
|
|
- </SelectTrigger>
|
|
|
|
|
- </FormControl>
|
|
|
|
|
- <SelectContent>
|
|
|
|
|
- <SelectItem v-for="zone in zones" :key="zone.id" :value="zone.id">
|
|
|
|
|
- {{ zone.name }}
|
|
|
|
|
- </SelectItem>
|
|
|
|
|
- </SelectContent>
|
|
|
|
|
- </Select>
|
|
|
|
|
- <FormMessage />
|
|
|
|
|
- </FormItem>
|
|
|
|
|
- </FormField>
|
|
|
|
|
-
|
|
|
|
|
- <FormField name="cf_record_id" v-slot="{ field }">
|
|
|
|
|
- <FormItem>
|
|
|
|
|
- <FormLabel>Cloudflare Record ID</FormLabel>
|
|
|
|
|
|
|
+ <FormLabel class="text-muted-foreground text-xs">Cloudflare Record ID</FormLabel>
|
|
|
<FormControl>
|
|
<FormControl>
|
|
|
- <Input v-bind="field" placeholder="CF record ID" :disabled="isEdit()" />
|
|
|
|
|
|
|
+ <Input v-bind="field" disabled class="font-mono text-xs text-muted-foreground" />
|
|
|
</FormControl>
|
|
</FormControl>
|
|
|
- <FormMessage />
|
|
|
|
|
</FormItem>
|
|
</FormItem>
|
|
|
</FormField>
|
|
</FormField>
|
|
|
|
|
|
|
|
- <FormField name="name" v-slot="{ field }">
|
|
|
|
|
- <FormItem>
|
|
|
|
|
- <FormLabel>Name</FormLabel>
|
|
|
|
|
- <FormControl>
|
|
|
|
|
- <Input v-bind="field" placeholder="subdomain.example.com" />
|
|
|
|
|
- </FormControl>
|
|
|
|
|
- <FormMessage />
|
|
|
|
|
- </FormItem>
|
|
|
|
|
- </FormField>
|
|
|
|
|
|
|
+ <div class="grid grid-cols-[1fr_auto] gap-4">
|
|
|
|
|
+ <FormField name="zone_id" v-slot="{ componentField }">
|
|
|
|
|
+ <FormItem>
|
|
|
|
|
+ <FormLabel>Zone</FormLabel>
|
|
|
|
|
+ <Select v-bind="componentField" :disabled="isEdit()">
|
|
|
|
|
+ <FormControl>
|
|
|
|
|
+ <SelectTrigger>
|
|
|
|
|
+ <SelectValue placeholder="Select a zone" />
|
|
|
|
|
+ </SelectTrigger>
|
|
|
|
|
+ </FormControl>
|
|
|
|
|
+ <SelectContent>
|
|
|
|
|
+ <SelectItem v-for="zone in zones" :key="zone.id" :value="zone.id">
|
|
|
|
|
+ {{ zone.name }}
|
|
|
|
|
+ </SelectItem>
|
|
|
|
|
+ </SelectContent>
|
|
|
|
|
+ </Select>
|
|
|
|
|
+ <FormMessage />
|
|
|
|
|
+ </FormItem>
|
|
|
|
|
+ </FormField>
|
|
|
|
|
|
|
|
- <div class="grid grid-cols-2 gap-4">
|
|
|
|
|
<FormField name="type" v-slot="{ componentField }">
|
|
<FormField name="type" v-slot="{ componentField }">
|
|
|
<FormItem>
|
|
<FormItem>
|
|
|
<FormLabel>Type</FormLabel>
|
|
<FormLabel>Type</FormLabel>
|
|
|
<Select v-bind="componentField">
|
|
<Select v-bind="componentField">
|
|
|
<FormControl>
|
|
<FormControl>
|
|
|
- <SelectTrigger>
|
|
|
|
|
- <SelectValue placeholder="Record type" />
|
|
|
|
|
|
|
+ <SelectTrigger class="w-24">
|
|
|
|
|
+ <SelectValue placeholder="Type" />
|
|
|
</SelectTrigger>
|
|
</SelectTrigger>
|
|
|
</FormControl>
|
|
</FormControl>
|
|
|
<SelectContent>
|
|
<SelectContent>
|
|
@@ -186,17 +192,39 @@ const onSubmit = handleSubmit(async (values) => {
|
|
|
<FormMessage />
|
|
<FormMessage />
|
|
|
</FormItem>
|
|
</FormItem>
|
|
|
</FormField>
|
|
</FormField>
|
|
|
|
|
+ </div>
|
|
|
|
|
|
|
|
- <FormField name="content" v-slot="{ field }">
|
|
|
|
|
- <FormItem>
|
|
|
|
|
|
|
+ <FormField name="name" v-slot="{ field }">
|
|
|
|
|
+ <FormItem>
|
|
|
|
|
+ <FormLabel>Name</FormLabel>
|
|
|
|
|
+ <FormControl>
|
|
|
|
|
+ <Input v-bind="field" placeholder="subdomain.example.com" />
|
|
|
|
|
+ </FormControl>
|
|
|
|
|
+ <FormMessage />
|
|
|
|
|
+ </FormItem>
|
|
|
|
|
+ </FormField>
|
|
|
|
|
+
|
|
|
|
|
+ <FormField name="content" v-slot="{ field }">
|
|
|
|
|
+ <FormItem>
|
|
|
|
|
+ <div class="flex items-center justify-between">
|
|
|
<FormLabel>Content</FormLabel>
|
|
<FormLabel>Content</FormLabel>
|
|
|
- <FormControl>
|
|
|
|
|
- <Input v-bind="field" placeholder="IP or target" />
|
|
|
|
|
- </FormControl>
|
|
|
|
|
- <FormMessage />
|
|
|
|
|
- </FormItem>
|
|
|
|
|
- </FormField>
|
|
|
|
|
- </div>
|
|
|
|
|
|
|
+ <button
|
|
|
|
|
+ type="button"
|
|
|
|
|
+ class="flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground disabled:opacity-50"
|
|
|
|
|
+ :disabled="fetchingIp"
|
|
|
|
|
+ @click="useCurrentIp"
|
|
|
|
|
+ >
|
|
|
|
|
+ <Loader2 v-if="fetchingIp" class="h-3 w-3 animate-spin" />
|
|
|
|
|
+ <Locate v-else class="h-3 w-3" />
|
|
|
|
|
+ {{ fetchingIp ? 'Fetching…' : 'Use current IP' }}
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <FormControl>
|
|
|
|
|
+ <Input v-bind="field" placeholder="IP or target" />
|
|
|
|
|
+ </FormControl>
|
|
|
|
|
+ <FormMessage />
|
|
|
|
|
+ </FormItem>
|
|
|
|
|
+ </FormField>
|
|
|
|
|
|
|
|
<div class="flex items-center gap-6">
|
|
<div class="flex items-center gap-6">
|
|
|
<FormField name="proxied" v-slot="{ value, handleChange }">
|
|
<FormField name="proxied" v-slot="{ value, handleChange }">
|