11<script lang="ts" setup>
2- import {computed , ref } from " vue" ;
2+ import {computed , watch , ref , onMounted } from " vue" ;
33import {useWorkspaceStore } from " @/stores/WorkspaceStore" ;
4+ import {getAvailablePipelines } from " @/lib/sigma/worker/workerApi" ;
45import {
5- Select ,
6- SelectContent ,
7- SelectGroup ,
8- SelectItem ,
9- SelectLabel ,
10- SelectTrigger ,
11- SelectValue
12- } from " @/components/ui/select" ;
13- import {Badge } from " @/components/ui/badge" ;
14- import {Plus , X } from " lucide-vue-next" ;
6+ DropdownMenu ,
7+ DropdownMenuContent ,
8+ DropdownMenuCheckboxItem ,
9+ DropdownMenuLabel ,
10+ DropdownMenuSeparator ,
11+ DropdownMenuTrigger ,
12+ } from " @/components/ui/dropdown-menu" ;
1513import {Button } from " @/components/ui/button" ;
14+ import {ScrollArea } from " @/components/ui/scroll-area" ;
15+ import {Filter , Check } from " lucide-vue-next" ;
1616import type {FileItem } from " @/types/types" ;
1717
18+ const isDropdownOpen = ref (false );
19+
1820const workspace = useWorkspaceStore ();
1921const fs = computed (() => workspace .currentWorkspace ?.fileStore ());
2022const sigma = computed (() => workspace .currentWorkspace ?.sigmaStore ());
2123
22- // Default built-in pipelines
23- const defaultPipelines = [
24- ' splunk_windows' ,
25- ' splunk_linux' ,
26- ' splunk_web' ,
27- ' splunk_network' ,
28- ' elasticsearch_windows' ,
29- ' elasticsearch_linux' ,
30- ' elasticsearch_web' ,
31- ' elasticsearch_network'
32- ];
33-
34- // Get available pipelines from pipeline files
24+ // All available pipelines from plugins
25+ const allPipelines = ref <string []>([]);
26+
27+ // Load available pipelines from worker on mount
28+ onMounted (async () => {
29+ try {
30+ console .log (' Fetching available pipelines from worker...' );
31+ const result = await getAvailablePipelines ();
32+ console .log (' Pipeline fetch result:' , result );
33+ if (result .success && result .pipelines ) {
34+ allPipelines .value = result .pipelines ;
35+ console .log (' Loaded pipelines:' , allPipelines .value );
36+ } else {
37+ console .warn (' Failed to get pipelines:' , result .error );
38+ }
39+ } catch (error ) {
40+ console .error (' Failed to load available pipelines:' , error );
41+ }
42+ });
43+
44+ // Filter pipelines by backend prefix
45+ function getPipelinesForBackend(backend : string ): string [] {
46+ if (! backend || allPipelines .value .length === 0 ) {
47+ return allPipelines .value ;
48+ }
49+
50+ // Map backend names to their pipeline prefixes
51+ const backendPrefixes: Record <string , string []> = {
52+ ' splunk' : [' splunk' ],
53+ ' esql' : [' ecs' , ' elasticsearch' ],
54+ ' lucene' : [' ecs' , ' elasticsearch' ],
55+ ' eql' : [' ecs' , ' elasticsearch' ],
56+ ' loki' : [' loki' ],
57+ ' kusto' : [' azure' , ' microsoft' ],
58+ ' qradar' : [' qradar' ],
59+ ' carbonblack' : [' carbonblack' ],
60+ ' crowdstrike' : [' crowdstrike' ],
61+ ' sentinel' : [' azure' , ' microsoft' ],
62+ };
63+
64+ const prefixes = backendPrefixes [backend ] || [];
65+
66+ // Filter pipelines that start with any of the backend's prefixes
67+ return allPipelines .value .filter (pipeline =>
68+ prefixes .some (prefix => pipeline .startsWith (prefix ))
69+ );
70+ }
71+
72+ // Get available pipelines filtered by selected backend
3573const availablePipelines = computed (() => {
36- const pipelines: string [] = [... defaultPipelines ];
74+ const selectedBackend = sigma .value ?.selected_siem ;
75+
76+ // Start with backend-specific pipelines if available, otherwise all pipelines
77+ const basePipelines = selectedBackend
78+ ? getPipelinesForBackend (selectedBackend )
79+ : allPipelines .value ;
80+
81+ const pipelines: string [] = [... basePipelines ];
3782
3883 // Add custom pipelines from files
3984 const pipelineFiles = fs .value ?.files .filter ((f : FileItem ) => f .type === " pipeline" ) || [];
@@ -43,7 +88,6 @@ const availablePipelines = computed(() => {
4388 const content = file .content || ' ' ;
4489
4590 // Look for name patterns in YAML
46- // This is a simple approach - the backend uses pipeline_resolver.py for proper parsing
4791 const pipelines_in_file = [
4892 // Look for name: value format
4993 ... content .split (' \n ' )
@@ -63,70 +107,124 @@ const availablePipelines = computed(() => {
63107 return [... new Set (pipelines )].sort (); // Remove duplicates and sort
64108});
65109
66- // Handle pipeline selection
67- const selectedPipeline = ref (' ' );
110+ const selectedPipelines = computed (() => sigma .value ?.selected_pipelines || []);
68111
69- function addPipeline() {
70- if (! selectedPipeline .value ||
71- sigma .value .selected_pipelines .includes (selectedPipeline .value )) {
72- return ;
112+ // Toggle pipeline selection
113+ function togglePipeline(pipeline : string , checked : boolean ) {
114+ if (! sigma .value ) return ;
115+
116+ const currentPipelines = sigma .value .selected_pipelines || [];
117+
118+ if (checked ) {
119+ // Add pipeline if not already selected
120+ if (! currentPipelines .includes (pipeline )) {
121+ const newPipelines = [... currentPipelines , pipeline ];
122+ sigma .value .updateSelectedPipelines (newPipelines );
123+ }
124+ } else {
125+ // Remove pipeline
126+ const newPipelines = currentPipelines .filter (p => p !== pipeline );
127+ sigma .value .updateSelectedPipelines (newPipelines );
73128 }
74-
75- const newPipelines = [... sigma .value .selected_pipelines , selectedPipeline .value ];
76- sigma .value .updateSelectedPipelines (newPipelines );
77- selectedPipeline .value = ' ' ;
78129}
79130
80- function removePipeline(pipeline : string ) {
81- const newPipelines = sigma .value .selected_pipelines .filter (p => p !== pipeline );
82- sigma .value .updateSelectedPipelines (newPipelines );
83- }
131+ // Watch for backend changes and clear incompatible pipelines
132+ watch (() => sigma .value ?.selected_siem , (newBackend ) => {
133+ if (! newBackend || ! sigma .value ) return ;
84134
85- const selectedPipelines = computed (() => sigma .value ?.selected_pipelines || []);
135+ const compatiblePipelines = getPipelinesForBackend (newBackend );
136+ const currentPipelines = sigma .value .selected_pipelines || [];
137+
138+ // Remove pipelines that are not compatible with the new backend
139+ const filteredPipelines = currentPipelines .filter (pipeline => {
140+ // Keep pipelines that are compatible with the backend
141+ return compatiblePipelines .includes (pipeline );
142+ });
143+
144+ // Update if pipelines were filtered out
145+ if (filteredPipelines .length !== currentPipelines .length ) {
146+ sigma .value .updateSelectedPipelines (filteredPipelines );
147+ }
148+ });
149+
150+ // Computed label for the button
151+ const buttonLabel = computed (() => {
152+ const count = selectedPipelines .value .length ;
153+ if (count === 0 ) return ' Pipelines' ;
154+ if (count === 1 ) return ' 1 Pipeline' ;
155+ return ` ${count } Pipelines ` ;
156+ });
157+
158+ // Convert to Title Case, minus the underscores
159+ function toTitleCase(str : string ): string {
160+ return str
161+ .toLowerCase ()
162+ .split (/ [_\s ] + / )
163+ .map (word => {
164+ if (word === ' ecs' ) return ' ECS' ;
165+ return word .charAt (0 ).toUpperCase () + word .slice (1 );
166+ })
167+ .join (' ' );
168+ }
86169 </script >
87170
88171<template >
89- <div class =" flex flex-col gap-2" >
90- <div class =" flex gap-2 items-center" >
91- <Select v-model =" selectedPipeline" >
92- <SelectTrigger class =" h-8 w-[200px]" >
93- <SelectValue placeholder =" Select pipeline" />
94- </SelectTrigger >
95- <SelectContent >
96- <SelectLabel class =" m-0 pt-2 pb-0 px-3" >Pipelines</SelectLabel >
97- <SelectGroup >
98- <div class =" grid grid-cols-1 gap-1 p-2" >
99- <template v-for =" pipeline in availablePipelines " :key =" pipeline " >
100- <SelectItem :value =" pipeline" >
101- {{ pipeline }}
102- </SelectItem >
103- </template >
104- </div >
105- </SelectGroup >
106- </SelectContent >
107- </Select >
108-
172+ <DropdownMenu v-model:open =" isDropdownOpen" >
173+ <DropdownMenuTrigger as-child >
109174 <Button
110- class =" h-8 w-8"
111- size =" icon"
112175 variant =" outline"
113- @click =" addPipeline" >
114- <Plus class =" h-4 w-4" />
115- </Button >
116- </div >
117-
118- <div class =" flex flex-wrap gap-2 mt-1" >
119- <Badge
120- v-for =" pipeline in selectedPipelines"
121- :key =" pipeline"
122- class =" flex items-center gap-1"
123- variant =" secondary"
176+ size =" sm"
177+ class =" h-8 gap-2"
178+ :class =" selectedPipelines.length > 0 ? 'border-primary' : ''"
124179 >
125- {{ pipeline }}
126- <button class =" ml-1 p-0" @click =" removePipeline(pipeline)" >
127- <X class =" h-3 w-3" />
128- </button >
129- </Badge >
130- </div >
131- </div >
132- </template >
180+ <Filter class =" h-3.5 w-3.5" />
181+ <span >{{ toTitleCase(buttonLabel) }}</span >
182+ <Check v-if =" selectedPipelines.length > 0" class =" h-3.5 w-3.5 text-primary" />
183+ </Button >
184+ </DropdownMenuTrigger >
185+ <DropdownMenuContent class =" w-[280px]" align =" start" >
186+ <DropdownMenuLabel >
187+ <div class =" flex flex-col gap-1" >
188+ <span >Pipelines</span >
189+ <span class =" text-xs font-normal text-muted-foreground" >
190+ Compatible with {{ sigma?.selected_siem || 'current backend' }}
191+ </span >
192+ </div >
193+ </DropdownMenuLabel >
194+ <DropdownMenuSeparator />
195+
196+ <ScrollArea class =" h-[280px]" >
197+ <div class =" px-1" >
198+ <DropdownMenuCheckboxItem
199+ v-for =" pipeline in availablePipelines"
200+ :key =" pipeline"
201+ :checked =" selectedPipelines.includes(pipeline)"
202+ @select.prevent
203+ @update:checked =" (checked) => togglePipeline(pipeline, checked)"
204+ >
205+ {{ toTitleCase(pipeline) }}
206+ </DropdownMenuCheckboxItem >
207+
208+ <div v-if =" availablePipelines.length === 0" class =" px-2 py-6 text-center text-sm text-muted-foreground" >
209+ No pipelines available for this backend
210+ </div >
211+ </div >
212+ </ScrollArea >
213+
214+ <template v-if =" selectedPipelines .length > 0 " >
215+ <DropdownMenuSeparator />
216+ <div class =" px-2 py-2 flex items-center justify-between" >
217+ <span class =" text-xs text-muted-foreground" >{{ selectedPipelines.length }} selected</span >
218+ <Button
219+ variant =" ghost"
220+ size =" sm"
221+ class =" h-6 px-2 text-xs"
222+ @click =" sigma?.updateSelectedPipelines([]); isDropdownOpen = false"
223+ >
224+ Clear all
225+ </Button >
226+ </div >
227+ </template >
228+ </DropdownMenuContent >
229+ </DropdownMenu >
230+ </template >
0 commit comments