Skip to content
This repository was archived by the owner on Sep 3, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/dispatch/signal/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,7 @@ class SignalFilterCreate(SignalFilterBase):

class SignalFilterRead(SignalFilterBase):
id: PrimaryKey
signals: list["SignalBase"] | None = []


class SignalFilterPagination(Pagination):
Expand Down Expand Up @@ -411,3 +412,6 @@ class SignalInstanceRead(SignalInstanceBase):

class SignalInstancePagination(Pagination):
items: list[SignalInstanceRead]

# Update forward references
SignalFilterRead.model_rebuild()
2 changes: 1 addition & 1 deletion src/dispatch/static/dispatch/src/router/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,7 @@ export const protectedRoute = [
path: "/:organization/signals",
name: "SignalInstanceTable",
meta: { title: "List" },
component: () => import("@/signal/TableInstance.vue"),
component: () => import("@/signal/instance/TableInstance.vue"),
},
],
},
Expand Down
78 changes: 78 additions & 0 deletions src/dispatch/static/dispatch/src/signal/MultiSignalPopover.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<template>
<div>
<v-menu v-model="menu" origin="overlap">
<template #activator="{ props }">
<v-chip pill size="small" v-bind="props"> {{ signals.length }} Signals </v-chip>
</template>
<v-card width="300">
<v-list dark>
<v-list-item>
<v-list-item-title>{{ signals.length }} Signals</v-list-item-title>

<template #append>
<v-btn icon variant="text" @click="menu = false">
<v-icon>mdi-close-circle</v-icon>
</v-btn>
</template>
</v-list-item>
</v-list>
<v-list>
<v-list-item v-for="(signal, index) in signals" :key="index">
<template #prepend>
<v-avatar color="teal" size="small">
{{ initials(signal.name) }}
</v-avatar>
</template>

<v-list-item-title>{{ signal.name }}</v-list-item-title>
<v-list-item-subtitle>{{ signal.variant }}</v-list-item-subtitle>
</v-list-item>
<v-divider v-if="signals.length > 0" />
<v-list-item v-if="commonOwner">
<template #prepend>
<v-icon>mdi-briefcase</v-icon>
</template>
<v-list-item-subtitle>{{ commonOwner }}</v-list-item-subtitle>
</v-list-item>
</v-list>
</v-card>
</v-menu>
</div>
</template>

<script>
import { initials } from "@/filters"

export default {
name: "MultiSignalPopover",

data: () => ({
menu: false,
}),

setup() {
return { initials }
},

props: {
signals: {
type: Array,
default: function () {
return []
},
},
},

computed: {
commonOwner() {
// Check if all signals have the same owner
if (this.signals.length === 0) return null

const firstOwner = this.signals[0].owner
const allSameOwner = this.signals.every((signal) => signal.owner === firstOwner)

return allSameOwner ? firstOwner : "Multiple Owners"
},
},
}
</script>
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export default {
},

computed: {
...mapFields("signal", ["instanceTable.options.filters.signal"]),
...mapFields("signalInstance", ["instanceTable.options.filters.signal"]),
numFilters: function () {
return sum([this.signal.length])
},
Expand Down
12 changes: 12 additions & 0 deletions src/dispatch/static/dispatch/src/signal/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,16 @@ export default {
getInstance(signalId, instanceId) {
return API.get(`${resource}/${signalId}/${instanceId}`)
},

getStats(entity_type_id, entity_value, num_days = null) {
let days_filter
if (num_days != null) {
days_filter = `&num_days=${num_days}`
} else {
days_filter = ""
}
return API.get(
`${resource}/stats?entity_type_id=${entity_type_id}&entity_value="${entity_value}"${days_filter}`
)
},
}
98 changes: 98 additions & 0 deletions src/dispatch/static/dispatch/src/signal/instance/TableInstance.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
<template>
<v-container fluid>
<v-row no-gutters>
<v-col>
<div class="text-h5">Signals</div>
</v-col>
</v-row>
<v-row no-gutters class="pb-3" align="center">
<v-col cols="auto" class="pr-4"><div class="text">Group by:</div></v-col>
<v-col cols="auto" class="pr-4">
<v-btn
:class="activeView === 'triggers' ? 'selectedViewButton' : 'viewButton'"
@click="setActiveView('triggers')"
>
<v-icon>mdi-broadcast</v-icon>
<div class="pl-1">Triggers</div>
</v-btn>
</v-col>
<!-- todo (amats) we should have routes to allow navigation to a certain tab via link-->
<v-col cols="auto" class="pr-4">
<v-btn
:class="activeView === 'entities' ? 'selectedViewButton' : 'viewButton'"
@click="setActiveView('entities')"
>
<v-icon>mdi-cube-outline</v-icon>
<div class="pl-1">Entities</div>
</v-btn>
</v-col>
<v-col cols="auto" class="pr-4">
<v-btn
:class="activeView === 'snoozes' ? 'selectedViewButton' : 'viewButton'"
@click="setActiveView('snoozes')"
>
<v-icon>mdi-bell-off</v-icon>
<div class="pl-1">Snoozes</div>
</v-btn>
</v-col>
<v-col class="text-right" style="padding-right: 4px">
<table-filter-dialog />
</v-col>
</v-row>
<table-instance-triggers v-if="activeView === 'triggers'" />
<table-instance-entities v-if="activeView === 'entities'" />
<table-instance-snoozes v-if="activeView === 'snoozes'" />
</v-container>
</template>

<script>
import TableFilterDialog from "@/signal/TableFilterDialog.vue"
import TableInstanceTriggers from "@/signal/instance/TableInstanceTriggers.vue"
import TableInstanceEntities from "@/signal/instance/TableInstanceEntities.vue"
import TableInstanceSnoozes from "@/signal/instance/TableInstanceSnoozes.vue"

export default {
name: "SignalInstanceTable",

components: {
TableFilterDialog,
TableInstanceTriggers,
TableInstanceEntities,
TableInstanceSnoozes,
},

data() {
return {
activeView: "triggers",
}
},

computed: {},

methods: {
/**
* Set the active view and update the UI accordingly.
* @param view: The view to set as active ('triggers', 'entities', or 'snoozes').
*/
setActiveView(view) {
this.activeView = view
},
},
}
</script>

<style>
@import "@/styles/index.scss";

.viewButton {
background-color: rgb(var(--v-theme-background2));
color: rgb(var(--v-theme-anchor));
box-shadow: 0 0 0 0;
}

.selectedViewButton {
background-color: rgb(var(--v-theme-gray7));
color: rgb(var(--v-theme-gray0));
box-shadow: 0 0 0 0;
}
</style>
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
<template>
<v-data-table-server
:headers="headers"
:items="items"
:items-length="total || 0"
v-model:page="page"
v-model:items-per-page="itemsPerPage"
:footer-props="{
'items-per-page-options': [10, 25, 50, 100],
}"
v-model:sort-by="sortBy"
v-model:sort-desc="descending"
:loading="loading"
loading-text="Loading... Please wait"
>
<template #item.project.display_name="{ item, value }">
<v-chip size="small" :color="item.project.color">
{{ value }}
</v-chip>
</template>
<template #item.created_at="{ value }">
<v-tooltip location="bottom">
<template #activator="{ props }">
<span v-bind="props">{{ formatRelativeDate(value) }}</span>
</template>
<span>{{ formatDate(value) }}</span>
</v-tooltip>
</template>
<template #item.instanceStats="{ value }">
<span>
<v-chip color="red-darken-1" size="small">
{{ value.num_signal_instances_alerted }} events
</v-chip>
<v-chip color="blue-accent-4" size="small">
{{ value.num_signal_instances_snoozed }} filtered
</v-chip>
</span>
</template>
<template #item.snoozeStats="{ value }">
<span>
<v-chip size="small">{{ value.num_snoozes_active }} Active</v-chip>
<v-chip size="small">{{ value.num_snoozes_expired }} Expired</v-chip>
</span>
</template>
</v-data-table-server>
</template>

<script>
import { mapFields } from "vuex-map-fields"
import { mapActions } from "vuex"
import { formatRelativeDate, formatDate } from "@/filters"

import RouterUtils from "@/router/utils"

export default {
name: "TableInstanceEntities",

data() {
return {
headers: [
{ title: "Type", value: "entity_type.name", sortable: true },
{ title: "Value", value: "value", sortable: true },
{ title: "Description", value: "entity_type.description", sortable: false },
{ title: "Signal Triggers", value: "instanceStats", sortable: false },
{ title: "Snooze Filters", value: "snoozeStats", sortable: false },
{ title: "Project", value: "project.display_name", sortable: true },
],
}
},

setup() {
return { formatRelativeDate, formatDate }
},

computed: {
...mapFields("signalInstance", [
"entityTable.loading",
"entityTable.options.descending",
"entityTable.options.filters",
"entityTable.options.itemsPerPage",
"entityTable.options.page",
"entityTable.options.sortBy",
"entityTable.rows.items",
"entityTable.rows.total",
]),
...mapFields("auth", ["currentUser.projects"]),

defaultUserProjects: {
get() {
let d = null
if (this.projects) {
d = this.projects.filter((v) => v.default === true)
return d.map((v) => v.project)
}
return d
},
},
},

methods: {
...mapActions("signalInstance", ["getAllEntities"]),
},

created() {
this.filters = {
...this.filters,
...RouterUtils.deserializeFilters(this.$route.query),
project: this.defaultUserProjects,
}

this.getAllEntities()

this.$watch(
(vm) => [vm.page],
() => {
this.getAllEntities()
}
)

this.$watch(
(vm) => [vm.sortBy, vm.itemsPerPage, vm.descending, vm.created_at, vm.project],
() => {
this.page = 1
RouterUtils.updateURLFilters(this.filters)
this.getAllEntities()
}
)
},
}
</script>
Loading
Loading