main
vue 360 lines 10.4 KB
Raw
1 <template>
2 <div class="page page-wrapped page-mobile-full page-without-footer flex flex-col">
3 <SegmentedPage
4 main-content-class="p-0! overflow-hidden grow flex h-full"
5 :use-main-scroll="false"
6 padding="18px"
7 enable-resize
8 toolbar-height="54px"
9 >
10 <template #sidebar-header>Customers</template>
11 <template #sidebar-content>
12 <n-spin :show="loadingList">
13 <template v-if="customers.length">
14 <div class="flex flex-col gap-4">
15 <CardEntity
16 v-for="customer of customers"
17 :key="customer"
18 hoverable
19 clickable
20 :highlighted="customer === currentConfig?.customer_code"
21 @click.stop="loadConfig(customer)"
22 >
23 <div class="flex items-center justify-between">
24 <div>{{ customer }}</div>
25 <n-button text @click.stop="routeCustomer({ code: customer }).navigate()">
26 <template #icon>
27 <Icon :size="14" :name="LinkIcon" />
28 </template>
29 </n-button>
30 </div>
31 </CardEntity>
32 </div>
33 </template>
34 <template v-else>
35 <n-empty v-if="!loadingList" description="No items found" class="h-48 justify-center" />
36 </template>
37 <n-dropdown
38 v-if="hasCustomersAvailable"
39 placement="bottom-start"
40 trigger="click"
41 :options="customersOptions"
42 @select="newConfig($event)"
43 >
44 <n-button secondary class="mt-4! w-full!" size="large">
45 <template #icon>
46 <Icon :size="18" :name="NewConfigIcon" />
47 </template>
48 <span class="ml-1.5 truncate">Add new Configuration</span>
49 </n-button>
50 </n-dropdown>
51 </n-spin>
52 </template>
53 <template #main-toolbar>
54 <div v-if="currentConfig" class="@container flex items-center justify-between">
55 <div class="flex items-center gap-2 md:gap-3">
56 <n-button
57 v-if="xmlEditorCTX"
58 size="small"
59 :disabled="!xmlEditorCTX.canUndo()"
60 @click="xmlEditorCTX.undo"
61 >
62 <div class="flex items-center gap-2">
63 <Icon :name="UndoIcon" />
64 <span class="hidden @sm:flex">Undo</span>
65 </div>
66 </n-button>
67 <n-button
68 v-if="xmlEditorCTX"
69 size="small"
70 :disabled="!xmlEditorCTX.canRedo()"
71 @click="xmlEditorCTX.redo"
72 >
73 <div class="flex items-center gap-2">
74 <span class="hidden @sm:flex">Redo</span>
75 <Icon :name="RedoIcon" />
76 </div>
77 </n-button>
78 </div>
79 <div class="flex items-center gap-2 md:gap-3">
80 <n-popover v-if="xmlErrors.length && xmlEditorCTX" class="p-1!">
81 <template #trigger>
82 <div class="flex items-center justify-end gap-2">
83 <Icon
84 name="carbon:warning-alt"
85 :size="20"
86 class="text-warning animate-fade cursor-help"
87 />
88 <span class="text-warning hidden font-mono text-xs @lg:flex">Errors detected</span>
89 </div>
90 </template>
91
92 <n-scrollbar class="max-h-100">
93 <div class="flex max-w-80 flex-col gap-1">
94 <div
95 v-for="item of xmlErrors"
96 :key="JSON.stringify(item)"
97 class="bg-secondary hover:bg-body flex cursor-pointer flex-col gap-0.5 rounded-sm p-1 font-mono"
98 @click="xmlEditorCTX.scrollToLine(item.line)"
99 >
100 <div class="text-secondary text-[8px]">line: {{ item.line }}</div>
101 <div class="text-xs">{{ item.message }}</div>
102 </div>
103 </div>
104 </n-scrollbar>
105 </n-popover>
106
107 <n-button
108 :loading="uploadingConfig"
109 size="small"
110 type="primary"
111 :disabled="!isDirty"
112 @click="uploadConfigFile()"
113 >
114 <div class="flex items-center gap-2">
115 <Icon :name="UploadIcon" />
116 <span class="hidden @xs:flex">Upload</span>
117 </div>
118 </n-button>
119 <n-button
120 :loading="deployingConfig"
121 size="small"
122 type="success"
123 :disabled="!currentConfig.config_content"
124 @click="deployConfig()"
125 >
126 <div class="flex items-center gap-2">
127 <Icon :name="DeployIcon" />
128 <span class="hidden @xs:flex">Deploy</span>
129 </div>
130 </n-button>
131 </div>
132 </div>
133 </template>
134 <template #main-content>
135 <n-spin
136 :show="loadingConfig || uploadingConfig || deployingConfig"
137 class="flex h-full w-full overflow-hidden"
138 content-class="flex h-full grow flex-col justify-center overflow-hidden"
139 >
140 <template v-if="currentConfig">
141 <XMLEditor
142 v-model="currentConfig.config_content"
143 class="scrollbar-styled text-sm"
144 @errors="xmlErrors = $event"
145 @mounted="xmlEditorCTX = $event"
146 />
147 </template>
148 <template v-else>
149 <n-empty v-if="!loadingConfig" description="Select a customer" class="h-48 justify-center" />
150 </template>
151 </n-spin>
152 </template>
153 </SegmentedPage>
154 </div>
155 </template>
156
157 <script setup lang="ts">
158 import type { DropdownMixedOption } from "naive-ui/es/dropdown/src/interface"
159 import type { XMLEditorCtx, XMLError } from "@/components/common/XMLEditor.vue"
160 import type { Customer } from "@/types/customers"
161 import type { ConfigContent } from "@/types/sysmonConfig.d"
162 import _clone from "lodash/cloneDeep"
163 import { NButton, NDropdown, NEmpty, NPopover, NScrollbar, NSpin, useMessage } from "naive-ui"
164 import { computed, h, onBeforeMount, ref } from "vue"
165 import Api from "@/api"
166 import CardEntity from "@/components/common/cards/CardEntity.vue"
167 import Icon from "@/components/common/Icon.vue"
168 import SegmentedPage from "@/components/common/SegmentedPage.vue"
169 import XMLEditor from "@/components/common/XMLEditor.vue"
170 import { useNavigation } from "@/composables/useNavigation"
171
172 const message = useMessage()
173 const { routeCustomer } = useNavigation()
174 const loadingList = ref(false)
175 const loadingConfig = ref(false)
176 const uploadingConfig = ref(false)
177 const deployingConfig = ref(false)
178 const customers = ref<string[]>([])
179 const currentConfig = ref<ConfigContent | null>(null)
180 const backupConfig = ref<ConfigContent | null>(null)
181 const xmlEditorCTX = ref<XMLEditorCtx | null>(null)
182 const UndoIcon = "carbon:undo"
183 const RedoIcon = "carbon:redo"
184 const LinkIcon = "carbon:launch"
185 const DeployIcon = "carbon:deploy"
186 const UploadIcon = "carbon:cloud-upload"
187 const NewConfigIcon = "carbon:document-add"
188
189 const isDirty = computed(() => currentConfig.value?.config_content !== backupConfig.value?.config_content)
190 const xmlErrors = ref<XMLError[]>([])
191
192 const loadingCustomersList = ref(false)
193 const customersList = ref<Customer[]>([])
194 const hasCustomersAvailable = computed<boolean>(
195 () => !!customersList.value.filter(o => !customers.value.includes(o.customer_code)).length
196 )
197
198 const customersOptions = computed(() => {
199 const options: DropdownMixedOption[] = []
200
201 options.push({
202 label: () => h("div", { class: "pl-2" }, `Select a Customer${loadingCustomersList.value ? "..." : ""}`),
203 type: "group",
204 children: customersList.value
205 .filter(o => !customers.value.includes(o.customer_code))
206 .map(o => ({
207 label: `#${o.customer_code} - ${o.customer_name}`,
208 key: o.customer_code
209 }))
210 })
211
212 return options
213 })
214
215 function getCustomers() {
216 loadingCustomersList.value = true
217
218 Api.customers
219 .getCustomers()
220 .then(res => {
221 if (res.data.success) {
222 customersList.value = res.data?.customers || []
223 } else {
224 message.warning(res.data?.message || "An error occurred. Please try again later.")
225 }
226 })
227 .catch(err => {
228 message.error(err.response?.data?.message || "An error occurred. Please try again later.")
229 })
230 .finally(() => {
231 loadingCustomersList.value = false
232 })
233 }
234
235 function newConfig(customerCode: string) {
236 customers.value.push(customerCode)
237 currentConfig.value = {
238 customer_code: customerCode,
239 config_content: `<Sysmon schemaversion="4.60">
240 <EventFiltering>
241 <RuleGroup groupRelation="or"></RuleGroup>
242 </EventFiltering>
243 </Sysmon>`
244 }
245 backupConfig.value = {
246 customer_code: customerCode,
247 config_content: `<Sysmon schemaversion="4.60">
248 <EventFiltering>
249 <RuleGroup groupRelation="or"></RuleGroup>
250 </EventFiltering>
251 </Sysmon>`
252 }
253 uploadConfigFile()
254 }
255
256 function loadConfig(customerCode: string) {
257 if (customerCode !== currentConfig.value?.customer_code) {
258 getConfigContent(customerCode)
259 }
260 }
261
262 function getList() {
263 loadingList.value = true
264
265 Api.sysmonConfig
266 .getAll()
267 .then(res => {
268 if (res.data.success) {
269 customers.value = res.data.customer_codes || []
270 } else {
271 message.error(res.data?.message || "An error occurred. Please try again later.")
272 }
273 })
274 .catch(err => {
275 message.error(err.response?.data?.message || "An error occurred. Please try again later.")
276 })
277 .finally(() => {
278 loadingList.value = false
279 })
280 }
281
282 function getConfigContent(customerCode: string) {
283 loadingConfig.value = true
284
285 Api.sysmonConfig
286 .getConfigContent(customerCode)
287 .then(res => {
288 if (res.data.success) {
289 currentConfig.value = _clone(res.data)
290 backupConfig.value = _clone(res.data)
291 } else {
292 message.error(res.data?.message || "An error occurred. Please try again later.")
293 }
294 })
295 .catch(err => {
296 message.error(err.response?.data?.message || "An error occurred. Please try again later.")
297 })
298 .finally(() => {
299 loadingConfig.value = false
300 })
301 }
302
303 function uploadConfigFile() {
304 if (currentConfig.value) {
305 uploadingConfig.value = true
306
307 Api.sysmonConfig
308 .uploadConfigFile(
309 currentConfig.value.customer_code,
310 new File(
311 [currentConfig.value.config_content],
312 `sysmon_config-${currentConfig.value.customer_code}.xml`,
313 { type: "text/xml;charset=utf-8" }
314 )
315 )
316 .then(res => {
317 if (res.data.success) {
318 currentConfig.value = _clone(currentConfig.value)
319 backupConfig.value = _clone(currentConfig.value)
320 message.success("Sysmon Config uploaded Successfully")
321 } else {
322 message.error("An error occurred. Please try again later.")
323 }
324 })
325 .catch(err => {
326 message.error(err.response?.data?.message || "An error occurred. Please try again later.")
327 })
328 .finally(() => {
329 uploadingConfig.value = false
330 })
331 }
332 }
333
334 function deployConfig() {
335 if (currentConfig.value) {
336 deployingConfig.value = true
337
338 Api.sysmonConfig
339 .deployConfig(currentConfig.value.customer_code)
340 .then(res => {
341 if (res.data.success) {
342 message.success("Sysmon Config deployed successfully")
343 } else {
344 message.error("An error occurred. Please try again later.")
345 }
346 })
347 .catch(err => {
348 message.error(err.response?.data?.message || "An error occurred. Please try again later.")
349 })
350 .finally(() => {
351 deployingConfig.value = false
352 })
353 }
354 }
355
356 onBeforeMount(() => {
357 getList()
358 getCustomers()
359 })
360 </script>