| 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> |