main
vue 185 lines 4.92 KB
Raw
1 <template>
2 <div>
3 <CardEntity clickable hoverable @click.stop="showDetails = true">
4 <template #headerMain>#{{ asset.asset_id }} - {{ asset.asset_uuid }}</template>
5 <template #default>
6 <div class="flex flex-col gap-1">
7 <div v-html="asset.asset_name"></div>
8 <p v-if="asset.asset_description">
9 <template v-if="isUrlLike(asset.asset_description)">
10 <a
11 :href="asset.asset_description"
12 class="asset-url"
13 target="_blank"
14 alt="asset url"
15 rel="nofollow noopener noreferrer"
16 >
17 <code class="text-primary">
18 <span>
19 {{ asset.asset_description }}
20 </span>
21 <Icon :name="LinkIcon" :size="14" class="relative top-0.5" />
22 </code>
23 </a>
24 </template>
25 <template v-else>
26 {{ excerpt }}
27 </template>
28 </p>
29 </div>
30 </template>
31 <template #mainExtra>
32 <div class="flex flex-wrap items-center gap-3">
33 <Badge type="splitted" color="primary">
34 <template #label>Status</template>
35 <template #value>
36 {{ asset.analysis_status }}
37 </template>
38 </Badge>
39 <Badge type="splitted" color="primary">
40 <template #label>Type</template>
41 <template #value>
42 {{ asset.asset_type }}
43 </template>
44 </Badge>
45 <Badge v-for="tag of tags" :key="tag.key" type="splitted" color="primary">
46 <template #label>
47 {{ tag.key }}
48 </template>
49 <template v-if="tag.value !== undefined" #value>
50 {{ tag.value || "-" }}
51 </template>
52 </Badge>
53 </div>
54 </template>
55 </CardEntity>
56
57 <n-modal
58 v-model:show="showDetails"
59 preset="card"
60 content-class="p-0!"
61 :style="{ maxWidth: 'min(800px, 90vw)', minHeight: 'min(600px, 90vh)', overflow: 'hidden' }"
62 :title="`Assets #${asset.asset_id} - ${asset.asset_uuid}`"
63 :bordered="false"
64 segmented
65 >
66 <n-tabs type="line" animated :tabs-padding="24">
67 <n-tab-pane name="Info" tab="Info" display-directive="show">
68 <div v-if="properties" class="grid-auto-fit-200 grid gap-2 p-6 pt-3">
69 <CardKV v-for="(value, key) of properties" :key>
70 <template #key>
71 {{ key }}
72 </template>
73 <template #value>
74 {{ value || "-" }}
75 </template>
76 </CardKV>
77 </div>
78 </n-tab-pane>
79 <n-tab-pane name="Description" tab="Description" display-directive="show">
80 <div class="p-6 pt-3">
81 <template v-if="isUrlLike(asset.asset_description)">
82 <a
83 :href="asset.asset_description"
84 class="asset-url"
85 target="_blank"
86 alt="asset url"
87 rel="nofollow noopener noreferrer"
88 >
89 {{ asset.asset_description }}
90 </a>
91 </template>
92 <template v-else>
93 <div v-html="descriptionFull"></div>
94 </template>
95 </div>
96 </n-tab-pane>
97 <n-tab-pane name="Link" tab="Link" display-directive="show:lazy">
98 <div v-if="asset.link?.length" class="flex flex-col gap-2 px-4">
99 <SocCaseAssetLink v-for="link of asset.link" :key="`${link.case_id}-${link.asset_id}`" :link />
100 </div>
101 <template v-else>
102 <n-empty description="No items found" class="h-48 justify-center" />
103 </template>
104 </n-tab-pane>
105 </n-tabs>
106 </n-modal>
107 </div>
108 </template>
109
110 <script setup lang="ts">
111 import type { SocCaseAsset } from "@/types/soc/asset.d"
112 import _omit from "lodash/omit"
113 import _split from "lodash/split"
114 import _upperFirst from "lodash/upperFirst"
115 import { NEmpty, NModal, NTabPane, NTabs } from "naive-ui"
116 import { computed, defineAsyncComponent, ref } from "vue"
117 import Badge from "@/components/common/Badge.vue"
118 import CardEntity from "@/components/common/cards/CardEntity.vue"
119 import CardKV from "@/components/common/cards/CardKV.vue"
120 import Icon from "@/components/common/Icon.vue"
121 import { isUrlLike } from "@/utils"
122
123 const { asset } = defineProps<{ asset: SocCaseAsset }>()
124
125 const SocCaseAssetLink = defineAsyncComponent(() => import("./SocCaseAssetLink.vue"))
126
127 const LinkIcon = "carbon:launch"
128
129 const showDetails = ref(false)
130
131 const excerpt = computed(() => {
132 const text = asset.asset_description
133 const truncated = text.split(" ").slice(0, 30).join(" ")
134
135 return truncated + (truncated !== text ? "..." : "")
136 })
137
138 const NEWLINE_REGEX = /\n/g
139
140 const descriptionFull = computed(() => {
141 const text = asset.asset_description
142
143 return text.replace(NEWLINE_REGEX, "<br>") || "Empty"
144 })
145
146 const tags = computed<{ key: string; value?: string }[]>(() => {
147 if (!asset?.asset_tags) {
148 return []
149 }
150
151 return _split(asset.asset_tags, ",")
152 .filter(o => !!o)
153 .map(o => {
154 const chunks = _split(o, ":")
155
156 return {
157 key: _upperFirst(chunks[0]),
158 value: chunks[1] || undefined
159 }
160 })
161 })
162
163 const properties = computed(() => {
164 return _omit(asset, ["asset_description", "link"])
165 })
166 </script>
167
168 <style lang="scss" scoped>
169 .asset-url {
170 display: block;
171
172 code {
173 padding: 8px 12px;
174 display: flex;
175 gap: 10px;
176
177 span {
178 white-space: nowrap;
179 flex-grow: 1;
180 overflow: hidden;
181 text-overflow: ellipsis;
182 }
183 }
184 }
185 </style>