| 1 | <template> |
| 2 | <div class="@container flex flex-col gap-8"> |
| 3 | <div class="flex w-full flex-col gap-4"> |
| 4 | <div class="flex flex-col justify-between gap-4 @2xl:flex-row @2xl:items-center"> |
| 5 | <h2 class="text-xl font-semibold">GitHub Audit Configurations</h2> |
| 6 | <div class="flex flex-wrap items-center gap-x-6 gap-y-2"> |
| 7 | <n-button text @click="showInfo = true"> |
| 8 | <template #icon> |
| 9 | <Icon :name="InfoIcon" /> |
| 10 | </template> |
| 11 | Reference Guide |
| 12 | </n-button> |
| 13 | <n-button type="primary" @click="openCreateForm"> |
| 14 | <template #icon> |
| 15 | <Icon :name="AddIcon" /> |
| 16 | </template> |
| 17 | New Configuration |
| 18 | </n-button> |
| 19 | </div> |
| 20 | </div> |
| 21 | |
| 22 | <div class="flex flex-wrap items-center gap-2"> |
| 23 | <n-select |
| 24 | v-model:value="filterCustomerCode" |
| 25 | placeholder="Filter by Customer" |
| 26 | clearable |
| 27 | :options="customerOptions" |
| 28 | :loading="loadingCustomers" |
| 29 | class="min-w-50 flex-1" |
| 30 | @update:value="loadConfigs" |
| 31 | /> |
| 32 | <n-select |
| 33 | v-model:value="filterStatus" |
| 34 | placeholder="Filter by Status" |
| 35 | clearable |
| 36 | :options="statusOptions" |
| 37 | class="min-w-50 flex-1" |
| 38 | @update:value="loadConfigs" |
| 39 | /> |
| 40 | <n-input |
| 41 | v-model:value="filterOrganization" |
| 42 | placeholder="Search organization..." |
| 43 | clearable |
| 44 | class="min-w-80 flex-1" |
| 45 | @keyup.enter="loadConfigs" |
| 46 | @clear="loadConfigs" |
| 47 | > |
| 48 | <template #prefix> |
| 49 | <Icon :name="SearchIcon" /> |
| 50 | </template> |
| 51 | </n-input> |
| 52 | </div> |
| 53 | </div> |
| 54 | |
| 55 | <n-spin :show="loading"> |
| 56 | <div v-if="configs.length === 0 && !loading" class="py-8 text-center"> |
| 57 | <n-empty description="No configurations found"> |
| 58 | <template #extra> |
| 59 | <n-button type="primary" @click="openCreateForm">Create your first configuration</n-button> |
| 60 | </template> |
| 61 | </n-empty> |
| 62 | </div> |
| 63 | |
| 64 | <div v-else class="grid grid-cols-1 gap-4 @4xl:grid-cols-2"> |
| 65 | <GitHubAuditCard |
| 66 | v-for="config in configs" |
| 67 | :key="config.id" |
| 68 | :config |
| 69 | @click="openDetail(config)" |
| 70 | @edit="openEditForm" |
| 71 | @audit-complete="loadConfigs" |
| 72 | /> |
| 73 | </div> |
| 74 | </n-spin> |
| 75 | |
| 76 | <n-drawer v-model:show="showForm" :width="600" placement="right" class="max-w-[98vw]"> |
| 77 | <n-drawer-content |
| 78 | :title="selectedConfig ? 'Edit Configuration' : 'New GitHub Audit Configuration'" |
| 79 | closable |
| 80 | :native-scrollbar="false" |
| 81 | > |
| 82 | <GitHubAuditConfigForm |
| 83 | v-if="showForm" |
| 84 | :config="selectedConfig" |
| 85 | @saved="onConfigSaved" |
| 86 | @cancel="showForm = false" |
| 87 | /> |
| 88 | </n-drawer-content> |
| 89 | </n-drawer> |
| 90 | |
| 91 | <n-drawer v-model:show="showDetail" :width="800" placement="right" class="max-w-[98vw]"> |
| 92 | <n-drawer-content v-if="selectedConfig" closable :native-scrollbar="false"> |
| 93 | <template #header> |
| 94 | <div class="flex items-center gap-3"> |
| 95 | <Icon name="codicon:organization" :size="24" /> |
| 96 | <span>{{ selectedConfig.organization }}</span> |
| 97 | <n-tag v-if="!selectedConfig.enabled" type="warning" size="small">Disabled</n-tag> |
| 98 | </div> |
| 99 | </template> |
| 100 | |
| 101 | <GitHubAuditDetail |
| 102 | :config="selectedConfig" |
| 103 | @updated="loadConfigs" |
| 104 | @edit="openEditForm" |
| 105 | @close="showDetail = false" |
| 106 | /> |
| 107 | </n-drawer-content> |
| 108 | </n-drawer> |
| 109 | |
| 110 | <n-drawer v-model:show="showInfo" :width="700" placement="right" class="max-w-[98vw]"> |
| 111 | <n-drawer-content closable :native-scrollbar="false"> |
| 112 | <template #header> |
| 113 | <div class="flex items-center gap-3"> |
| 114 | <Icon :name="InfoIcon" :size="24" /> |
| 115 | <span>GitHub Audit Reference Guide</span> |
| 116 | </div> |
| 117 | </template> |
| 118 | |
| 119 | <GitHubAuditInfo /> |
| 120 | </n-drawer-content> |
| 121 | </n-drawer> |
| 122 | </div> |
| 123 | </template> |
| 124 | |
| 125 | <script setup lang="ts"> |
| 126 | import type { GitHubAuditConfig } from "@/types/githubAudit.d" |
| 127 | import { NButton, NDrawer, NDrawerContent, NEmpty, NInput, NSelect, NSpin, NTag, useMessage } from "naive-ui" |
| 128 | import { onBeforeMount, ref } from "vue" |
| 129 | import Api from "@/api" |
| 130 | import Icon from "@/components/common/Icon.vue" |
| 131 | import GitHubAuditCard from "./GitHubAuditCard.vue" |
| 132 | import GitHubAuditConfigForm from "./GitHubAuditConfigForm.vue" |
| 133 | import GitHubAuditDetail from "./GitHubAuditDetail.vue" |
| 134 | import GitHubAuditInfo from "./GitHubAuditInfo.vue" |
| 135 | |
| 136 | const AddIcon = "ion:add" |
| 137 | const SearchIcon = "ion:search-outline" |
| 138 | const InfoIcon = "ion:information-circle-outline" |
| 139 | |
| 140 | const message = useMessage() |
| 141 | const loading = ref(false) |
| 142 | const configs = ref<GitHubAuditConfig[]>([]) |
| 143 | const showForm = ref(false) |
| 144 | const showDetail = ref(false) |
| 145 | const showInfo = ref(false) |
| 146 | const selectedConfig = ref<GitHubAuditConfig | null>(null) |
| 147 | |
| 148 | // Filters |
| 149 | const filterCustomerCode = ref<string | null>(null) |
| 150 | const filterStatus = ref<string | null>(null) |
| 151 | const filterOrganization = ref<string | null>(null) |
| 152 | const customerOptions = ref<{ label: string; value: string }[]>([]) |
| 153 | const loadingCustomers = ref(false) |
| 154 | |
| 155 | const statusOptions = [ |
| 156 | { label: "Enabled", value: "enabled" }, |
| 157 | { label: "Disabled", value: "disabled" } |
| 158 | ] |
| 159 | |
| 160 | async function loadCustomers() { |
| 161 | if (loadingCustomers.value) return |
| 162 | |
| 163 | loadingCustomers.value = true |
| 164 | try { |
| 165 | const response = await Api.customers.getCustomers() |
| 166 | if (response.data.customers) { |
| 167 | customerOptions.value = response.data.customers.map((c: any) => ({ |
| 168 | label: `${c.customer_name} (${c.customer_code})`, |
| 169 | value: c.customer_code |
| 170 | })) |
| 171 | } |
| 172 | } catch (error) { |
| 173 | console.error("Failed to load customers:", error) |
| 174 | } finally { |
| 175 | loadingCustomers.value = false |
| 176 | } |
| 177 | } |
| 178 | |
| 179 | async function loadConfigs() { |
| 180 | if (loading.value) return |
| 181 | |
| 182 | loading.value = true |
| 183 | try { |
| 184 | const response = await Api.githubAudit.getConfigs(filterCustomerCode.value || undefined) |
| 185 | if (response.data.configs) { |
| 186 | let filteredConfigs = response.data.configs |
| 187 | |
| 188 | if (filterStatus.value) { |
| 189 | const isEnabled = filterStatus.value === "enabled" |
| 190 | filteredConfigs = filteredConfigs.filter((c: GitHubAuditConfig) => c.enabled === isEnabled) |
| 191 | } |
| 192 | |
| 193 | if (filterOrganization.value) { |
| 194 | const searchTerm = filterOrganization.value.toLowerCase() |
| 195 | filteredConfigs = filteredConfigs.filter((c: GitHubAuditConfig) => |
| 196 | c.organization.toLowerCase().includes(searchTerm) |
| 197 | ) |
| 198 | } |
| 199 | |
| 200 | configs.value = filteredConfigs |
| 201 | } |
| 202 | } catch (error: any) { |
| 203 | message.error(error.response?.data?.detail || "Failed to load configurations") |
| 204 | configs.value = [] |
| 205 | } finally { |
| 206 | loading.value = false |
| 207 | } |
| 208 | } |
| 209 | |
| 210 | function openCreateForm() { |
| 211 | selectedConfig.value = null |
| 212 | showForm.value = true |
| 213 | } |
| 214 | |
| 215 | function openEditForm(config: GitHubAuditConfig) { |
| 216 | selectedConfig.value = config |
| 217 | showDetail.value = false |
| 218 | showForm.value = true |
| 219 | } |
| 220 | |
| 221 | function openDetail(config: GitHubAuditConfig) { |
| 222 | selectedConfig.value = config |
| 223 | showDetail.value = true |
| 224 | } |
| 225 | |
| 226 | function onConfigSaved() { |
| 227 | showForm.value = false |
| 228 | loadConfigs() |
| 229 | } |
| 230 | |
| 231 | onBeforeMount(() => { |
| 232 | loadCustomers() |
| 233 | loadConfigs() |
| 234 | }) |
| 235 | </script> |