| 1 | <template> |
| 2 | <n-tabs type="line" animated :tabs-padding="24"> |
| 3 | <n-tab-pane name="Overview" tab="Overview" display-directive="show"> |
| 4 | <div class="flex flex-col gap-6 p-7 pt-2"> |
| 5 | <div class="grid-auto-fit-200 grid gap-2"> |
| 6 | <CardKV> |
| 7 | <template #key>Policy ID</template> |
| 8 | <template #value> |
| 9 | {{ sca.policy_id }} |
| 10 | </template> |
| 11 | </CardKV> |
| 12 | |
| 13 | <CardKV> |
| 14 | <template #key>Agent</template> |
| 15 | <template #value> |
| 16 | {{ sca.agent_name }} |
| 17 | </template> |
| 18 | </CardKV> |
| 19 | |
| 20 | <CardKV v-if="sca.customer_code"> |
| 21 | <template #key>Customer</template> |
| 22 | <template #value> |
| 23 | <code |
| 24 | class="text-primary cursor-pointer" |
| 25 | @click.stop="routeCustomer({ code: sca.customer_code }).navigate()" |
| 26 | > |
| 27 | #{{ sca.customer_code }} |
| 28 | <Icon :name="LinkIcon" :size="13" class="relative top-0.5" /> |
| 29 | </code> |
| 30 | </template> |
| 31 | </CardKV> |
| 32 | |
| 33 | <CardKV> |
| 34 | <template #key>Compliance Level</template> |
| 35 | <template #value> |
| 36 | {{ getComplianceLevel(sca.score) }} |
| 37 | </template> |
| 38 | </CardKV> |
| 39 | |
| 40 | <CardKV> |
| 41 | <template #key>Score</template> |
| 42 | <template #value>{{ sca.score }}%</template> |
| 43 | </CardKV> |
| 44 | |
| 45 | <CardKV> |
| 46 | <template #key>Scan Date</template> |
| 47 | <template #value> |
| 48 | {{ formatDate(sca.end_scan, dFormats.datetime) }} |
| 49 | </template> |
| 50 | </CardKV> |
| 51 | </div> |
| 52 | |
| 53 | <CardKV> |
| 54 | <template #key>Description</template> |
| 55 | <template #value> |
| 56 | {{ sca.description }} |
| 57 | </template> |
| 58 | </CardKV> |
| 59 | |
| 60 | <CardKV v-if="sca.references"> |
| 61 | <template #key>References</template> |
| 62 | <template #value> |
| 63 | <a :href="sca.references" target="_blank" tabindex="-1" class="outline-none"> |
| 64 | {{ sca.references }} |
| 65 | </a> |
| 66 | </template> |
| 67 | </CardKV> |
| 68 | </div> |
| 69 | </n-tab-pane> |
| 70 | <n-tab-pane name="Statistics & Progress" tab="Statistics & Progress" display-directive="show"> |
| 71 | <div class="flex flex-col gap-4 p-7 pt-2"> |
| 72 | <div class="flex items-center gap-2 text-sm"> |
| 73 | <Icon :name="InfoIcon" :size="14" /> |
| 74 | Total: |
| 75 | <code>{{ sca.total_checks }}</code> |
| 76 | </div> |
| 77 | |
| 78 | <div class="grid-auto-fit-200 grid gap-2"> |
| 79 | <n-card content-class="flex flex-col gap-2" size="small"> |
| 80 | <div class="flex items-center justify-between gap-2 whitespace-nowrap"> |
| 81 | <div class="flex items-center gap-2"> |
| 82 | <Icon :name="PassIcon" :size="20" class="text-success" /> |
| 83 | <span class="text-lg">Passed</span> |
| 84 | </div> |
| 85 | <div class="font-mono text-lg font-bold">{{ sca.pass }}</div> |
| 86 | </div> |
| 87 | <n-progress |
| 88 | type="line" |
| 89 | indicator-placement="inside" |
| 90 | :percentage="getPercentage(sca.pass)" |
| 91 | color="var(--success-color)" |
| 92 | class="custom-progress" |
| 93 | /> |
| 94 | </n-card> |
| 95 | |
| 96 | <n-card content-class="flex flex-col gap-2" size="small"> |
| 97 | <div class="flex items-center justify-between gap-2 whitespace-nowrap"> |
| 98 | <div class="flex items-center gap-2"> |
| 99 | <Icon :name="FailIcon" :size="20" class="text-error" /> |
| 100 | <span class="text-lg">Failed</span> |
| 101 | </div> |
| 102 | <div class="font-mono text-lg font-bold">{{ sca.fail }}</div> |
| 103 | </div> |
| 104 | <n-progress |
| 105 | type="line" |
| 106 | indicator-placement="inside" |
| 107 | :percentage="getPercentage(sca.fail)" |
| 108 | color="var(--error-color)" |
| 109 | class="custom-progress" |
| 110 | /> |
| 111 | </n-card> |
| 112 | |
| 113 | <n-card v-if="sca.invalid > 0" content-class="flex flex-col gap-2" size="small"> |
| 114 | <div class="flex items-center justify-between gap-2 whitespace-nowrap"> |
| 115 | <div class="flex items-center gap-2"> |
| 116 | <Icon :name="InvalidIcon" :size="20" class="text-warning" /> |
| 117 | <span class="text-lg">Invalid</span> |
| 118 | </div> |
| 119 | <div class="font-mono text-lg font-bold">{{ sca.invalid }}</div> |
| 120 | </div> |
| 121 | <n-progress |
| 122 | type="line" |
| 123 | indicator-placement="inside" |
| 124 | :percentage="getPercentage(sca.invalid)" |
| 125 | color="var(--warning-color)" |
| 126 | class="custom-progress" |
| 127 | /> |
| 128 | </n-card> |
| 129 | </div> |
| 130 | |
| 131 | <n-card class="bg-secondary! overflow-hidden" title="Progress"> |
| 132 | <n-progress |
| 133 | type="line" |
| 134 | indicator-placement="inside" |
| 135 | :percentage="sca.score" |
| 136 | color="var(--success-color)" |
| 137 | class="custom-progress" |
| 138 | /> |
| 139 | </n-card> |
| 140 | </div> |
| 141 | </n-tab-pane> |
| 142 | <n-tab-pane name="Scan Information" tab="Scan Information" display-directive="show"> |
| 143 | <div class="flex flex-col gap-4 p-7 pt-2"> |
| 144 | <n-card class="bg-secondary! overflow-hidden"> |
| 145 | <div class="flex flex-wrap justify-between gap-8"> |
| 146 | <n-statistic |
| 147 | label="Scan Started" |
| 148 | :value="`${formatDate(sca.start_scan, dFormats.datetime)}`" |
| 149 | tabular-nums |
| 150 | /> |
| 151 | <n-statistic |
| 152 | label="Scan Completed" |
| 153 | :value="`${formatDate(sca.end_scan, dFormats.datetime)}`" |
| 154 | tabular-nums |
| 155 | /> |
| 156 | <n-statistic label="Scan Duration" :value="`${getScanDuration()}`" tabular-nums /> |
| 157 | </div> |
| 158 | </n-card> |
| 159 | |
| 160 | <CardKV v-if="sca.hash_file"> |
| 161 | <template #key>Hash File</template> |
| 162 | <template #value> |
| 163 | {{ sca.hash_file }} |
| 164 | </template> |
| 165 | </CardKV> |
| 166 | </div> |
| 167 | </n-tab-pane> |
| 168 | </n-tabs> |
| 169 | </template> |
| 170 | |
| 171 | <script setup lang="ts"> |
| 172 | import type { AgentScaOverviewItem } from "@/types/sca.d" |
| 173 | import _toNumber from "lodash/toNumber" |
| 174 | import { NCard, NProgress, NStatistic, NTabPane, NTabs } from "naive-ui" |
| 175 | import CardKV from "@/components/common/cards/CardKV.vue" |
| 176 | import Icon from "@/components/common/Icon.vue" |
| 177 | import { useNavigation } from "@/composables/useNavigation" |
| 178 | import { useSettingsStore } from "@/stores/settings" |
| 179 | import { formatDate } from "@/utils/format" |
| 180 | import { getComplianceLevel } from "./utils" |
| 181 | |
| 182 | const { sca } = defineProps<{ sca: AgentScaOverviewItem }>() |
| 183 | |
| 184 | const dFormats = useSettingsStore().dateFormat |
| 185 | const { routeCustomer } = useNavigation() |
| 186 | |
| 187 | const InfoIcon = "carbon:information" |
| 188 | const LinkIcon = "carbon:launch" |
| 189 | const PassIcon = "carbon:checkmark-filled" |
| 190 | const FailIcon = "carbon:close-filled" |
| 191 | const InvalidIcon = "carbon:warning-alt" |
| 192 | |
| 193 | function getPercentage(count: number): number { |
| 194 | if (sca.total_checks === 0) return 0 |
| 195 | return _toNumber(((count / sca.total_checks) * 100).toFixed(1)) |
| 196 | } |
| 197 | |
| 198 | function getScanDuration(): string { |
| 199 | const start = new Date(sca.start_scan) |
| 200 | const end = new Date(sca.end_scan) |
| 201 | const durationMs = end.getTime() - start.getTime() |
| 202 | const durationMinutes = Math.floor(durationMs / 60000) |
| 203 | const durationSeconds = Math.floor((durationMs % 60000) / 1000) |
| 204 | |
| 205 | if (durationMinutes > 0) { |
| 206 | return `${durationMinutes}m ${durationSeconds}s` |
| 207 | } |
| 208 | return `${durationSeconds}s` |
| 209 | } |
| 210 | </script> |
| 211 | |
| 212 | <style lang="scss" scoped> |
| 213 | .custom-progress { |
| 214 | :deep() { |
| 215 | .n-progress-graph-line-indicator { |
| 216 | text-shadow: |
| 217 | 0px 0px 1px black, |
| 218 | 0px 0px 2px black, |
| 219 | 0px 0px 3px black; |
| 220 | font-weight: bold; |
| 221 | color: white !important; |
| 222 | } |
| 223 | } |
| 224 | } |
| 225 | </style> |