main
vue 299 lines 8.65 KB
Raw
1 <template>
2 <div class="flex flex-col gap-4">
3 <n-form ref="formRef" :model="formData" :rules label-placement="top" :disabled="saving">
4 <n-divider title-placement="left" class="mt-2!">Basic Settings</n-divider>
5
6 <n-form-item label="Customer" path="customer_code">
7 <n-select
8 v-model:value="formData.customer_code"
9 placeholder="Select customer"
10 :options="customerOptions"
11 :disabled="isEdit"
12 filterable
13 />
14 </n-form-item>
15
16 <n-form-item label="GitHub Organization" path="organization">
17 <n-input v-model:value="formData.organization" placeholder="e.g., my-org" />
18 </n-form-item>
19
20 <n-form-item label="GitHub Token" path="github_token">
21 <n-input
22 v-model:value="formData.github_token"
23 type="password"
24 show-password-on="click"
25 :placeholder="isEdit ? 'Leave blank to keep existing token' : 'ghp_xxxxxxxxxxxx'"
26 />
27 </n-form-item>
28
29 <n-form-item label="Token Type" path="token_type">
30 <n-radio-group v-model:value="formData.token_type">
31 <n-radio value="pat">Personal Access Token</n-radio>
32 <n-radio value="app">GitHub App</n-radio>
33 </n-radio-group>
34 </n-form-item>
35
36 <n-form-item label="Enabled" :show-feedback="false">
37 <n-switch v-model:value="formData.enabled" />
38 </n-form-item>
39
40 <n-divider title-placement="left">Audit Scope</n-divider>
41
42 <n-grid :cols="2" :x-gap="16">
43 <n-gi>
44 <n-form-item label="Include Repositories">
45 <n-switch v-model:value="formData.include_repos" />
46 </n-form-item>
47 </n-gi>
48 <n-gi>
49 <n-form-item label="Include Workflows">
50 <n-switch v-model:value="formData.include_workflows" />
51 </n-form-item>
52 </n-gi>
53 <n-gi>
54 <n-form-item label="Include Members">
55 <n-switch v-model:value="formData.include_members" />
56 </n-form-item>
57 </n-gi>
58 <n-gi>
59 <n-form-item label="Include Archived Repos">
60 <n-switch v-model:value="formData.include_archived_repos" />
61 </n-form-item>
62 </n-gi>
63 </n-grid>
64
65 <n-form-item label="Repository Filter Mode" :show-feedback="false">
66 <n-radio-group v-model:value="formData.repo_filter_mode">
67 <n-radio value="all">All Repositories</n-radio>
68 <n-radio value="include">Include Only</n-radio>
69 <n-radio value="exclude">Exclude</n-radio>
70 </n-radio-group>
71 </n-form-item>
72
73 <n-form-item v-if="formData.repo_filter_mode !== 'all'" label="Repository List">
74 <n-dynamic-tags v-model:value="formData.repo_filter_list" />
75 <template #feedback>Enter repository names to {{ formData.repo_filter_mode }}</template>
76 </n-form-item>
77
78 <n-divider title-placement="left">Schedule</n-divider>
79
80 <n-form-item label="Enable Scheduled Audits" :show-feedback="false">
81 <n-switch v-model:value="formData.auto_audit_enabled" />
82 </n-form-item>
83
84 <n-form-item v-if="formData.auto_audit_enabled" label="Schedule (Cron)" path="audit_schedule_cron">
85 <n-input v-model:value="formData.audit_schedule_cron" placeholder="0 0 * * 1 (Weekly on Monday)" />
86 <template #feedback>
87 <n-text depth="3">Use cron format. Example: "0 0 * * 1" for weekly on Monday at midnight</n-text>
88 </template>
89 </n-form-item>
90
91 <n-divider title-placement="left">Notifications</n-divider>
92
93 <n-grid :cols="2" :x-gap="16">
94 <n-gi>
95 <n-form-item label="Notify on Critical">
96 <n-switch v-model:value="formData.notify_on_critical" />
97 </n-form-item>
98 </n-gi>
99 <n-gi>
100 <n-form-item label="Notify on High">
101 <n-switch v-model:value="formData.notify_on_high" />
102 </n-form-item>
103 </n-gi>
104 </n-grid>
105
106 <n-form-item label="Notification Webhook URL">
107 <n-input v-model:value="formData.notification_webhook_url" placeholder="https://..." />
108 </n-form-item>
109
110 <n-form-item label="Notification Email" :show-feedback="false">
111 <n-input v-model:value="formData.notification_email" placeholder="security@example.com" />
112 </n-form-item>
113
114 <n-divider title-placement="left">Thresholds</n-divider>
115
116 <n-form-item label="Minimum Passing Score" :show-feedback="false">
117 <n-slider v-model:value="formData.minimum_passing_score" :min="0" :max="100" :step="5" />
118 <n-input-number
119 v-model:value="formData.minimum_passing_score"
120 :min="0"
121 :max="100"
122 style="width: 100px; margin-left: 16px"
123 />
124 </n-form-item>
125 </n-form>
126
127 <div class="flex justify-end gap-3">
128 <n-button @click="emit('cancel')">Cancel</n-button>
129 <n-button type="primary" :loading="saving" @click="handleSubmit">
130 {{ isEdit ? "Update" : "Create" }}
131 </n-button>
132 </div>
133 </div>
134 </template>
135
136 <script setup lang="ts">
137 import type { FormInst, FormRules } from "naive-ui"
138 import type { GitHubAuditConfig, GitHubAuditConfigCreate, GitHubAuditConfigUpdate } from "@/types/githubAudit.d"
139 import {
140 NButton,
141 NDivider,
142 NDynamicTags,
143 NForm,
144 NFormItem,
145 NGi,
146 NGrid,
147 NInput,
148 NInputNumber,
149 NRadio,
150 NRadioGroup,
151 NSelect,
152 NSlider,
153 NSwitch,
154 NText,
155 useMessage
156 } from "naive-ui"
157 import { computed, onBeforeMount, reactive, ref, watch } from "vue"
158 import Api from "@/api"
159
160 interface GitHubAuditConfigFormData extends Omit<
161 GitHubAuditConfigCreate,
162 "customer_code" | "github_token" | "organization"
163 > {
164 customer_code: string | null
165 github_token: string | null
166 organization: string | null
167 }
168
169 const props = defineProps<{
170 config?: GitHubAuditConfig | null
171 }>()
172
173 const emit = defineEmits<{
174 (e: "saved"): void
175 (e: "cancel"): void
176 }>()
177
178 const message = useMessage()
179 const formRef = ref<FormInst | null>(null)
180 const saving = ref(false)
181 const customerOptions = ref<{ label: string; value: string }[]>([])
182
183 const isEdit = computed(() => !!props.config)
184
185 function defaultFormData(): GitHubAuditConfigFormData {
186 return {
187 customer_code: null,
188 github_token: null,
189 organization: null,
190 token_type: "pat",
191 enabled: true,
192 auto_audit_enabled: false,
193 audit_schedule_cron: null,
194 include_repos: true,
195 include_workflows: true,
196 include_members: true,
197 include_archived_repos: false,
198 repo_filter_mode: "all",
199 repo_filter_list: [],
200 notify_on_critical: true,
201 notify_on_high: false,
202 notification_webhook_url: null,
203 notification_email: null,
204 minimum_passing_score: 70
205 }
206 }
207
208 const formData = reactive<GitHubAuditConfigFormData>(defaultFormData())
209
210 const rules: FormRules = {
211 customer_code: { required: true, message: "Customer is required", trigger: "blur" },
212 organization: { required: true, message: "Organization is required", trigger: "blur" },
213 github_token: {
214 required: !isEdit.value,
215 message: "GitHub token is required",
216 trigger: "blur"
217 }
218 }
219
220 watch(
221 () => props.config,
222 config => {
223 if (config) {
224 Object.assign(formData, {
225 customer_code: config.customer_code,
226 github_token: "",
227 organization: config.organization,
228 token_type: config.token_type,
229 enabled: config.enabled,
230 auto_audit_enabled: config.auto_audit_enabled,
231 audit_schedule_cron: config.audit_schedule_cron,
232 include_repos: config.include_repos,
233 include_workflows: config.include_workflows,
234 include_members: config.include_members,
235 include_archived_repos: config.include_archived_repos,
236 repo_filter_mode: config.repo_filter_mode,
237 repo_filter_list: config.repo_filter_list || [],
238 notify_on_critical: config.notify_on_critical,
239 notify_on_high: config.notify_on_high,
240 notification_webhook_url: config.notification_webhook_url,
241 notification_email: config.notification_email,
242 minimum_passing_score: config.minimum_passing_score
243 })
244 } else {
245 Object.assign(formData, defaultFormData())
246 }
247 },
248 { immediate: true }
249 )
250
251 async function handleSubmit() {
252 try {
253 await formRef.value?.validate()
254 } catch {
255 return
256 }
257
258 saving.value = true
259
260 try {
261 if (isEdit.value && props.config) {
262 const updateData: GitHubAuditConfigUpdate = { ...formData }
263 if (!updateData.github_token) {
264 delete updateData.github_token
265 }
266 await Api.githubAudit.updateConfig(props.config.id, updateData)
267 message.success("Configuration updated successfully")
268 } else {
269 const createData: GitHubAuditConfigCreate = {
270 ...formData,
271 customer_code: formData.customer_code ?? "",
272 github_token: formData.github_token ?? "",
273 organization: formData.organization ?? ""
274 }
275 await Api.githubAudit.createConfig(createData)
276 message.success("Configuration created successfully")
277 }
278 emit("saved")
279 } catch (error: any) {
280 message.error(error.response?.data?.detail || "Failed to save configuration")
281 } finally {
282 saving.value = false
283 }
284 }
285
286 onBeforeMount(async () => {
287 try {
288 const response = await Api.customers.getCustomers()
289 if (response.data.customers) {
290 customerOptions.value = response.data.customers.map((c: any) => ({
291 label: `${c.customer_name} (${c.customer_code})`,
292 value: c.customer_code
293 }))
294 }
295 } catch (error) {
296 console.error("Failed to load customers:", error)
297 }
298 })
299 </script>