main
vue 138 lines 4.33 KB
Raw
1 <template>
2 <n-spin :show="loadingDetails" content-class="min-h-40">
3 <div v-if="tacticDetails" class="flex flex-col gap-4 md:flex-row">
4 <div class="flex grow flex-col gap-3">
5 <code class="self-start">
6 {{ tacticDetails.id ?? "" }}
7 </code>
8
9 <CardKV>
10 <template #key>name</template>
11 <template #value>
12 <span class="whitespace-pre-wrap">
13 {{ tacticDetails.name ?? "" }}
14 </span>
15 </template>
16 </CardKV>
17 <CardKV v-if="tacticDetails.description" class="[&_p]:text-white">
18 <template #key>description</template>
19 <template #value>
20 <Markdown :source="tacticDetails.description" />
21 </template>
22 </CardKV>
23
24 <CardKV v-if="tacticDetails.references?.length">
25 <template #key>references</template>
26 <template #value>
27 <References :references="tacticDetails.references" />
28 </template>
29 </CardKV>
30 </div>
31 <div ref="sidebarRef" class="shrink-0 basis-1/3 md:max-w-70">
32 <div ref="sidebarCardRef" class="flex flex-col gap-2 will-change-transform">
33 <n-card content-class="bg-secondary flex flex-col gap-3 rounded-lg" size="small">
34 <div class="flex flex-col gap-0.5 text-sm">
35 <div class="text-secondary font-mono text-xs">external_id</div>
36 <div>{{ tacticDetails.external_id }}</div>
37 </div>
38 <div class="flex flex-col gap-0.5 text-sm">
39 <div class="text-secondary font-mono text-xs">short_name</div>
40 <div>{{ tacticDetails.short_name }}</div>
41 </div>
42 <div class="flex flex-col gap-0.5 text-sm">
43 <div class="text-secondary font-mono text-xs">created_time</div>
44 <div>{{ formatDate(tacticDetails.created_time, dFormats.datetime) }}</div>
45 </div>
46 <div class="flex flex-col gap-0.5 text-sm">
47 <div class="text-secondary font-mono text-xs">modified_time</div>
48 <div>{{ formatDate(tacticDetails.modified_time, dFormats.datetime) }}</div>
49 </div>
50 <div class="flex flex-col gap-0.5 text-sm">
51 <div class="text-secondary font-mono text-xs">url</div>
52 <div>
53 <a :href="tacticDetails.url" target="_blank" rel="nofollow noopener noreferrer">
54 {{ tacticDetails.url }}
55 </a>
56 </div>
57 </div>
58 <div class="flex flex-col gap-0.5 text-sm">
59 <div class="text-secondary font-mono text-xs">source</div>
60 <div>{{ tacticDetails.source }}</div>
61 </div>
62 </n-card>
63 </div>
64 </div>
65 </div>
66 </n-spin>
67 </template>
68
69 <script setup lang="ts">
70 import type { MitreTacticDetails } from "@/types/mitre.d"
71 import { useElementBounding, useRafFn } from "@vueuse/core"
72 import { useMotionProperties } from "@vueuse/motion"
73 import { NCard, NSpin, useMessage } from "naive-ui"
74 import { defineAsyncComponent, onBeforeMount, ref, watch } from "vue"
75 import Api from "@/api"
76 import CardKV from "@/components/common/cards/CardKV.vue"
77 import { useSettingsStore } from "@/stores/settings"
78 import { formatDate } from "@/utils/format"
79 import References from "../common/References.vue"
80
81 const { externalId, entity } = defineProps<{
82 externalId?: string
83 entity?: MitreTacticDetails
84 }>()
85
86 const Markdown = defineAsyncComponent(() => import("@/components/common/Markdown.vue"))
87
88 const dFormats = useSettingsStore().dateFormat
89 const message = useMessage()
90 const loadingDetails = ref(false)
91 const tacticDetails = ref<MitreTacticDetails | null>(null)
92
93 const sidebarRef = ref(null)
94 const sidebarCardRef = ref(null)
95 const { top: sidebarTop } = useElementBounding(sidebarRef)
96 const { transform: styleCardTransform } = useMotionProperties(sidebarCardRef)
97
98 const { resume } = useRafFn(
99 () => {
100 const targetY = sidebarTop.value <= 50 ? sidebarTop.value * -1 + 50 : 0
101 styleCardTransform.translateY = `${targetY}px`
102 },
103 { immediate: false }
104 )
105
106 watch(sidebarTop, () => {
107 resume()
108 })
109
110 function getDetails(id: string) {
111 loadingDetails.value = true
112
113 Api.wazuh.mitre
114 .getMitreTactics({ id })
115 .then(res => {
116 if (res.data.success) {
117 tacticDetails.value = res.data.results?.[0] || null
118 } else {
119 message.warning(res.data?.message || "An error occurred. Please try again later.")
120 }
121 })
122 .catch(err => {
123 message.error(err.response?.data?.message || "An error occurred. Please try again later.")
124 })
125 .finally(() => {
126 loadingDetails.value = false
127 })
128 }
129
130 onBeforeMount(() => {
131 if (externalId) {
132 getDetails(externalId)
133 }
134 if (entity) {
135 tacticDetails.value = entity
136 }
137 })
138 </script>