| 1 | <template> |
| 2 | <div class="flex flex-col gap-4"> |
| 3 | <!-- Search Filters --> |
| 4 | <div class="flex items-center gap-2"> |
| 5 | <n-input v-model:value="searchQuery" size="small" placeholder="Search rules..." class="grow" clearable> |
| 6 | <template #prefix> |
| 7 | <Icon :name="SearchIcon" /> |
| 8 | </template> |
| 9 | </n-input> |
| 10 | |
| 11 | <n-select |
| 12 | v-model:value="selectedPlatform" |
| 13 | :options="platformOptions" |
| 14 | size="small" |
| 15 | placeholder="All Platforms" |
| 16 | class="max-w-35" |
| 17 | clearable |
| 18 | :consistent-menu-width="false" |
| 19 | /> |
| 20 | </div> |
| 21 | |
| 22 | <n-card content-class="p-0!"> |
| 23 | <!-- Rules List --> |
| 24 | <n-spin :show="loadingRules" class="min-h-50"> |
| 25 | <n-scrollbar |
| 26 | v-if="filteredRules.length" |
| 27 | class="max-h-90 p-3!" |
| 28 | trigger="none" |
| 29 | :theme-overrides="{ |
| 30 | railInsetVerticalRight: `4px 4px 4px auto` |
| 31 | }" |
| 32 | > |
| 33 | <div class="flex flex-col gap-3"> |
| 34 | <CardEntity |
| 35 | v-for="rule in filteredRules" |
| 36 | :key="rule.id" |
| 37 | size="small" |
| 38 | hoverable |
| 39 | embedded |
| 40 | clickable |
| 41 | :highlighted="selectedRule?.id === rule.id" |
| 42 | @click="selectRule(rule)" |
| 43 | > |
| 44 | <template #headerMain> |
| 45 | <PlatformBadge :platform="rule.platform" class="text-default" /> |
| 46 | </template> |
| 47 | <template #headerExtra> |
| 48 | <SeverityBadge :severity="rule.severity" /> |
| 49 | </template> |
| 50 | <template #mainExtra> |
| 51 | <div class="flex flex-col gap-1"> |
| 52 | <span class="font-medium">{{ rule.name }}</span> |
| 53 | <span class="line-clamp-2 text-sm opacity-70">{{ rule.description }}</span> |
| 54 | </div> |
| 55 | </template> |
| 56 | </CardEntity> |
| 57 | </div> |
| 58 | </n-scrollbar> |
| 59 | |
| 60 | <n-empty v-else-if="!loadingRules" description="No rules found" class="py-20" /> |
| 61 | </n-spin> |
| 62 | </n-card> |
| 63 | |
| 64 | <n-modal |
| 65 | :show="!!selectedRule" |
| 66 | :mask-closable="false" |
| 67 | :title="selectedRule?.name" |
| 68 | segmented |
| 69 | preset="card" |
| 70 | :style="{ maxWidth: 'min(550px, 90vw)', minHeight: 'min(300px, 90vh)', overflow: 'hidden' }" |
| 71 | @close="selectedRule = null" |
| 72 | > |
| 73 | <ExecuteSearchForm v-if="selectedRule" :rule-summary="selectedRule" :asset /> |
| 74 | </n-modal> |
| 75 | </div> |
| 76 | </template> |
| 77 | |
| 78 | <script setup lang="ts"> |
| 79 | import type { PlatformFilter, RuleSummary } from "@/types/copilotSearches.d" |
| 80 | import type { AlertAsset } from "@/types/incidentManagement/alerts.d" |
| 81 | import { NCard, NEmpty, NInput, NModal, NScrollbar, NSelect, NSpin, useMessage } from "naive-ui" |
| 82 | import { computed, onBeforeMount, ref, watch } from "vue" |
| 83 | import Api from "@/api" |
| 84 | import CardEntity from "@/components/common/cards/CardEntity.vue" |
| 85 | import Icon from "@/components/common/Icon.vue" |
| 86 | import PlatformBadge from "@/components/common/PlatformBadge.vue" |
| 87 | import ExecuteSearchForm from "./ExecuteSearchForm.vue" |
| 88 | import SeverityBadge from "./SeverityBadge.vue" |
| 89 | |
| 90 | const { asset } = defineProps<{ |
| 91 | asset: AlertAsset |
| 92 | }>() |
| 93 | |
| 94 | const message = useMessage() |
| 95 | const SearchIcon = "carbon:search" |
| 96 | |
| 97 | // State |
| 98 | const loadingRules = ref(false) |
| 99 | const rules = ref<RuleSummary[]>([]) |
| 100 | const selectedRule = ref<RuleSummary | null>(null) |
| 101 | |
| 102 | // Filters |
| 103 | const selectedPlatform = ref<PlatformFilter | null>(null) |
| 104 | const searchQuery = ref<string | null>(null) |
| 105 | |
| 106 | const platformOptions = [ |
| 107 | { label: "Linux", value: "linux" }, |
| 108 | { label: "Windows", value: "windows" } |
| 109 | ] |
| 110 | |
| 111 | // Computed |
| 112 | const filteredRules = computed(() => { |
| 113 | let result = rules.value |
| 114 | |
| 115 | if (selectedPlatform.value) { |
| 116 | result = result.filter(r => r.platform === selectedPlatform.value) |
| 117 | } |
| 118 | |
| 119 | if (searchQuery.value) { |
| 120 | const query = searchQuery.value.toLowerCase() |
| 121 | result = result.filter(r => r.name.toLowerCase().includes(query) || r.description.toLowerCase().includes(query)) |
| 122 | } |
| 123 | |
| 124 | return result |
| 125 | }) |
| 126 | |
| 127 | // Methods |
| 128 | async function loadRules() { |
| 129 | loadingRules.value = true |
| 130 | |
| 131 | try { |
| 132 | const res = await Api.copilotSearches.getRules({ limit: 100 }) |
| 133 | if (res.data.success) { |
| 134 | rules.value = res.data.rules |
| 135 | } |
| 136 | } catch (err: any) { |
| 137 | message.error(err.response?.data?.message || "Failed to load rules") |
| 138 | } finally { |
| 139 | loadingRules.value = false |
| 140 | } |
| 141 | } |
| 142 | |
| 143 | function selectRule(rule: RuleSummary) { |
| 144 | selectedRule.value = rule |
| 145 | } |
| 146 | |
| 147 | // Lifecycle |
| 148 | onBeforeMount(() => { |
| 149 | loadRules() |
| 150 | }) |
| 151 | |
| 152 | // Watch for platform changes to reload |
| 153 | watch(selectedPlatform, () => { |
| 154 | selectedRule.value = null |
| 155 | }) |
| 156 | </script> |