Skip to content
This repository was archived by the owner on Sep 3, 2025. It is now read-only.

Commit 2a90ae1

Browse files
authored
feat(ui): allow filtering by dedicated channel cases (#6206)
1 parent 3aafd4e commit 2a90ae1

File tree

5 files changed

+80
-4
lines changed

5 files changed

+80
-4
lines changed

src/dispatch/database/service.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from pydantic import Json
1111
from six import string_types
1212
from sortedcontainers import SortedSet
13-
from sqlalchemy import Table, and_, desc, func, not_, or_, orm
13+
from sqlalchemy import Table, and_, desc, func, not_, or_, orm, exists
1414
from sqlalchemy.exc import InvalidRequestError, ProgrammingError
1515
from sqlalchemy.orm import mapperlib, Query as SQLAlchemyQuery
1616
from sqlalchemy_filters import apply_pagination, apply_sort
@@ -591,6 +591,7 @@ def common_parameters(
591591
sort_by: list[str] = Query([], alias="sortBy[]"),
592592
descending: list[bool] = Query([], alias="descending[]"),
593593
role: UserRoles = Depends(get_current_role),
594+
security_event_only: bool = Query(None, alias="security_event_only"),
594595
):
595596
return {
596597
"db_session": db_session,
@@ -602,11 +603,15 @@ def common_parameters(
602603
"descending": descending,
603604
"current_user": current_user,
604605
"role": role,
606+
"security_event_only": security_event_only,
605607
}
606608

607609

608610
CommonParameters = Annotated[
609-
dict[str, int | CurrentUser | DbSession | QueryStr | Json | list[str] | list[bool] | UserRoles],
611+
dict[
612+
str,
613+
int | CurrentUser | DbSession | QueryStr | Json | list[str] | list[bool] | UserRoles | bool,
614+
],
610615
Depends(common_parameters),
611616
]
612617

@@ -676,6 +681,7 @@ def search_filter_sort_paginate(
676681
descending: list[bool] = None,
677682
current_user: DispatchUser = None,
678683
role: UserRoles = UserRoles.member,
684+
security_event_only: bool = None,
679685
):
680686
"""Common functionality for searching, filtering, sorting, and pagination."""
681687
model_cls = get_class_by_tablename(model)
@@ -712,6 +718,11 @@ def search_filter_sort_paginate(
712718
else:
713719
query = apply_filters(query, filter_spec, model_cls)
714720

721+
# Handle security_event_only filter for Case model
722+
if model == "Case" and security_event_only:
723+
# Use NOT EXISTS to find cases that do NOT have signal instances
724+
query = query.filter(~exists().where(SignalInstance.case_id == Case.id))
725+
715726
# Apply tag_all filters using intersect only when necessary
716727
for filter in tag_all_filters:
717728
query = query.intersect(filter)

src/dispatch/static/dispatch/src/case/TableFilterDialog.vue

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,18 @@
8080
/>
8181
</v-card>
8282
</v-list-item>
83+
<v-list-item>
84+
<v-card class="mx-auto">
85+
<v-card-title>Security Events</v-card-title>
86+
<v-card-subtitle>Show only cases with a dedicated channel</v-card-subtitle>
87+
<v-checkbox
88+
class="ml-10 mr-5"
89+
v-model="local_security_event_only"
90+
label="Show only Security Event cases"
91+
hint="Show only cases that have a dedicated channel"
92+
/>
93+
</v-card>
94+
</v-list-item>
8395
</v-list>
8496
<v-card-actions>
8597
<v-spacer />
@@ -93,7 +105,7 @@
93105
</template>
94106

95107
<script setup>
96-
import { ref, computed } from "vue"
108+
import { ref, computed, watch } from "vue"
97109
import { useStore } from "vuex"
98110
import { sum } from "lodash"
99111
@@ -133,6 +145,7 @@ const local_tag = ref([])
133145
const local_tag_type = ref([])
134146
const local_participant = ref(null)
135147
const local_participant_is_assignee = ref(false)
148+
const local_security_event_only = ref(false)
136149
137150
const case_priority = computed(
138151
() => store.state.case_management.table.options.filters.case_priority
@@ -144,6 +157,26 @@ const project = computed(() => store.state.case_management.table.options.filters
144157
const status = computed(() => store.state.case_management.table.options.filters.status)
145158
const tag = computed(() => store.state.case_management.table.options.filters.tag)
146159
const tag_type = computed(() => store.state.case_management.table.options.filters.tag_type)
160+
const security_event_only = computed(
161+
() => store.state.case_management.table.options.filters.security_event_only
162+
)
163+
const case_type = computed(() => store.state.case_management.table.options.filters.case_type)
164+
165+
// Initialize local variables when dialog opens
166+
watch(display, (newValue) => {
167+
if (newValue) {
168+
local_case_priority.value = case_priority.value || []
169+
local_case_severity.value = case_severity.value || []
170+
local_selected_case_types.value = case_type.value || []
171+
local_project.value = project.value || []
172+
local_status.value = status.value || []
173+
local_tag.value = tag.value || []
174+
local_tag_type.value = tag_type.value || []
175+
local_participant.value = null
176+
local_participant_is_assignee.value = false
177+
local_security_event_only.value = security_event_only.value || false
178+
}
179+
})
147180
148181
const numFilters = computed(() => {
149182
return sum([
@@ -155,6 +188,7 @@ const numFilters = computed(() => {
155188
tag.value?.length || 0,
156189
tag_type.value?.length || 0,
157190
local_participant.value == null ? 0 : 1,
191+
local_security_event_only.value ? 1 : 0,
158192
])
159193
})
160194
@@ -182,6 +216,7 @@ const applyFilters = () => {
182216
tag_type: local_tag_type.value,
183217
participant: filtered_participant,
184218
assignee: filtered_assignee,
219+
security_event_only: local_security_event_only.value,
185220
}
186221
187222
// Commit the mutation to update the filters in the Vuex store

src/dispatch/static/dispatch/src/case/store.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ const state = {
134134
end: null,
135135
},
136136
participant: null,
137+
security_event_only: false,
137138
},
138139
q: "",
139140
page: 1,

src/dispatch/static/dispatch/src/router/utils.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,13 @@ export default {
1313
flatFilters = { ...flatFilters, ...{ [startKey]: value.start, [endKey]: value.end } }
1414
return
1515
}
16+
17+
// handle boolean values (like security_event_only)
18+
if (typeof value === "boolean") {
19+
flatFilters[key] = value
20+
return
21+
}
22+
1623
each(value, function (item) {
1724
if (["commander", "participant", "assignee"].includes(key)) {
1825
if (has(flatFilters, key)) {
@@ -71,6 +78,12 @@ export default {
7178
}
7279
return
7380
}
81+
82+
// handle boolean values (like security_event_only)
83+
if (typeof value === "boolean" || value === "true" || value === "false") {
84+
filters[key] = value === "true" || value === true
85+
return
86+
}
7487
if (["status"].includes(key)) {
7588
if (typeof value === "string" || value instanceof String) {
7689
if (has(filters, key)) {

src/dispatch/static/dispatch/src/search/utils.js

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,15 @@ export default {
2929
createParametersFromTableOptions(options, model, rawFilters) {
3030
let [sortBy, descending] = this.createSortExpression(options.sortBy, options.descending)
3131
let expression = this.createFilterExpression(options.filters, model)
32+
33+
// Extract boolean filters (like security_event_only) and pass them as separate parameters
34+
let booleanFilters = {}
35+
forEach(options.filters, function (value, key) {
36+
if (typeof value === "boolean") {
37+
booleanFilters[key] = value
38+
}
39+
})
40+
3241
delete options.filters
3342
delete options.sortBy
3443

@@ -37,12 +46,13 @@ export default {
3746
expression = { and: [...rawFilters] }
3847
return {
3948
...options,
49+
...booleanFilters,
4050
sortBy: sortBy,
4151
descending: descending,
4252
filter: JSON.stringify(expression),
4353
}
4454
} else {
45-
return { ...options, sortBy: sortBy, descending: descending }
55+
return { ...options, ...booleanFilters, sortBy: sortBy, descending: descending }
4656
}
4757
}
4858

@@ -54,6 +64,7 @@ export default {
5464

5565
return {
5666
...options,
67+
...booleanFilters,
5768
sortBy: sortBy,
5869
descending: descending,
5970
filter: JSON.stringify(expression),
@@ -131,6 +142,11 @@ export default {
131142
],
132143
})
133144
}
145+
} else if (typeof value === "boolean") {
146+
// Handle boolean values (like security_event_only)
147+
// For boolean filters, we'll pass them as raw filters to be handled by the backend
148+
// This is a special case that bypasses the normal filter expression creation
149+
return
134150
} else {
135151
each(value, function (value) {
136152
// filter null/undefined values but allow false

0 commit comments

Comments
 (0)