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
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"""Adds timestamps to the SearchFilter model

Revision ID: 9ad021045e45
Revises: 930eb80028d2
Create Date: 2023-05-17 11:30:33.542458

"""
from alembic import op
import sqlalchemy as sa

# revision identifiers, used by Alembic.
revision = "9ad021045e45"
down_revision = "930eb80028d2"
branch_labels = None
depends_on = None


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column("search_filter", sa.Column("enabled", sa.Boolean(), nullable=True))
op.add_column("search_filter", sa.Column("updated_at", sa.DateTime(), nullable=True))
op.add_column("search_filter", sa.Column("created_at", sa.DateTime(), nullable=True))
# ### end Alembic commands ###


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column("search_filter", "created_at")
op.drop_column("search_filter", "updated_at")
op.drop_column("search_filter", "enabled")
# ### end Alembic commands ###
37 changes: 32 additions & 5 deletions src/dispatch/search_filter/models.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
from datetime import datetime
from typing import List, Optional

from pydantic import Field
from sqlalchemy import Column, ForeignKey, Integer, String

from sqlalchemy import Column, ForeignKey, Integer, String, Boolean
from sqlalchemy.orm import relationship
from sqlalchemy.sql.schema import UniqueConstraint
from sqlalchemy.sql.sqltypes import JSON
from sqlalchemy_utils import TSVectorType

from dispatch.enums import DispatchEnum
from dispatch.auth.models import DispatchUser, UserRead
from dispatch.database.core import Base
from dispatch.models import DispatchBase, NameStr, PrimaryKey, ProjectMixin
from dispatch.enums import DispatchEnum
from dispatch.models import DispatchBase, NameStr, PrimaryKey, ProjectMixin, TimeStampMixin
from dispatch.project.models import ProjectRead


Expand All @@ -19,7 +21,7 @@ class SearchFilterSubject(DispatchEnum):
incident = "incident"


class SearchFilter(Base, ProjectMixin):
class SearchFilter(Base, ProjectMixin, TimeStampMixin):
__table_args__ = (UniqueConstraint("name", "project_id"),)

id = Column(Integer, primary_key=True)
Expand All @@ -29,18 +31,36 @@ class SearchFilter(Base, ProjectMixin):
creator_id = Column(Integer, ForeignKey(DispatchUser.id))
creator = relationship("DispatchUser", backref="search_filters")
subject = Column(String, default="incident")
enabled = Column(Boolean, default=True)

search_vector = Column(
TSVectorType("name", "description", weights={"name": "A", "description": "B"})
)


# Pydantic models...
class IndividualContactRead(DispatchBase):
id: Optional[PrimaryKey]


class TeamRead(DispatchBase):
id: Optional[PrimaryKey]


class ServiceRead(DispatchBase):
id: Optional[PrimaryKey]


class NotificationRead(DispatchBase):
id: Optional[PrimaryKey]


class SearchFilterBase(DispatchBase):
description: Optional[str] = Field(None, nullable=True)
enabled: Optional[bool]
expression: List[dict]
name: NameStr
subject: SearchFilterSubject = SearchFilterSubject.incident
description: Optional[str] = Field(None, nullable=True)


class SearchFilterCreate(SearchFilterBase):
Expand All @@ -53,7 +73,14 @@ class SearchFilterUpdate(SearchFilterBase):

class SearchFilterRead(SearchFilterBase):
id: PrimaryKey
created_at: Optional[datetime] = None
updated_at: Optional[datetime] = None
project: Optional[ProjectRead]
creator: Optional[UserRead]
individuals: Optional[List[IndividualContactRead]] = []
notifications: Optional[List[NotificationRead]] = []
services: Optional[List[ServiceRead]] = []
teams: Optional[List[TeamRead]] = []


class SearchFilterPagination(DispatchBase):
Expand Down
16 changes: 11 additions & 5 deletions src/dispatch/search_filter/views.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
from fastapi import APIRouter, HTTPException, status
from fastapi import APIRouter, HTTPException, status, Depends
from pydantic.error_wrappers import ErrorWrapper, ValidationError

from sqlalchemy.exc import IntegrityError

from dispatch.auth.permissions import SensitiveProjectActionPermission, PermissionsDependency
from dispatch.auth.service import CurrentUser
from dispatch.database.core import DbSession
from dispatch.database.service import CommonParameters, search_filter_sort_paginate
from dispatch.exceptions import ExistsError
from dispatch.models import PrimaryKey
from dispatch.auth.service import CurrentUser

from .models import (
SearchFilterCreate,
SearchFilterUpdate,
SearchFilterRead,
SearchFilterPagination,
SearchFilterRead,
SearchFilterUpdate,
)
from .service import create, delete, get, update

Expand Down Expand Up @@ -77,7 +79,11 @@ def update_search_filter(
return search_filter


@router.delete("/{search_filter_id}", response_model=None)
@router.delete(
"/{search_filter_id}",
response_model=None,
dependencies=[Depends(PermissionsDependency([SensitiveProjectActionPermission]))],
)
def delete_filter(db_session: DbSession, search_filter_id: PrimaryKey):
"""Delete a search filter."""
search_filter = get(db_session=db_session, search_filter_id=search_filter_id)
Expand Down
1 change: 1 addition & 0 deletions src/dispatch/static/dispatch/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ declare module 'vue' {
VBtn: typeof import('vuetify/lib')['VBtn']
VCard: typeof import('vuetify/lib')['VCard']
VCardActions: typeof import('vuetify/lib')['VCardActions']
VCardSubtitle: typeof import('vuetify/lib')['VCardSubtitle']
VCardText: typeof import('vuetify/lib')['VCardText']
VCardTitle: typeof import('vuetify/lib')['VCardTitle']
VCheckbox: typeof import('vuetify/lib')['VCheckbox']
Expand Down
6 changes: 6 additions & 0 deletions src/dispatch/static/dispatch/src/router/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,12 @@ export const protectedRoute = [
meta: { title: "Notifications", subMenu: "project", group: "general" },
component: () => import("@/notification/Table.vue"),
},
{
path: "searchFilters",
name: "SearchFilterTable",
meta: { title: "Search Filters", subMenu: "project", group: "general" },
component: () => import("@/search/Table.vue"),
},
{
path: "workflows",
name: "WorkflowTable",
Expand Down
40 changes: 40 additions & 0 deletions src/dispatch/static/dispatch/src/search/DeleteDialog.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<template>
<v-dialog v-model="showRemove" persistent max-width="800px">
<v-card>
<v-card-title>
<span class="headline">Delete Search Filter?</span>
</v-card-title>
<v-card-text>
<v-container grid-list-md>
<v-layout wrap> Are you sure you would like to delete this search filter? </v-layout>
</v-container>
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn color="blue en-1" text @click="closeRemove()"> Cancel </v-btn>
<v-btn color="red en-1" text @click="remove()"> Delete </v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>

<script>
import { mapActions } from "vuex"
import { mapFields } from "vuex-map-fields"

export default {
name: "SearchFilterDeleteDialog",

data() {
return {}
},

computed: {
...mapFields("search", ["dialogs.showRemove"]),
},

methods: {
...mapActions("search", ["remove", "closeRemove"]),
},
}
</script>
171 changes: 171 additions & 0 deletions src/dispatch/static/dispatch/src/search/Table.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
<template>
<v-container fluid>
<!-- <new-edit-dialog /> -->
<delete-dialog />
<v-row no-gutters>
<v-col>
<v-alert dismissible icon="mdi-school" prominent text type="info">
Search filters enable you to define under which conditions an individual, oncall service,
or team need to be engage in an incident.
</v-alert>
</v-col>
</v-row>
<v-row align="center" justify="space-between" no-gutters>
<v-col cols="8">
<settings-breadcrumbs v-model="project" />
</v-col>
<!-- <v-col class="text-right"> -->
<!-- <v-btn color="info" class="ml-2" @click="createEditShow()"> New </v-btn> -->
<!-- </v-col> -->
</v-row>
<v-row no-gutters>
<v-col>
<v-card elevation="0">
<v-card-title>
<v-text-field
v-model="q"
append-icon="search"
label="Search"
single-line
hide-details
clearable
/>
</v-card-title>
<v-data-table
:headers="headers"
:items="items"
:server-items-length="total"
:page.sync="page"
:items-per-page.sync="itemsPerPage"
:sort-by.sync="sortBy"
:sort-desc.sync="descending"
:loading="loading"
loading-text="Loading... Please wait"
>
<!-- TODO(mvilanova): Allow to view the list of individuals, teams, services, and notifications upon clicking on the chip -->
<template #[`item.individuals`]="{ item }">
<v-chip small color="info" text-color="white">{{ item.individuals.length }}</v-chip>
</template>
<template #[`item.teams`]="{ item }">
<v-chip small color="info" text-color="white">{{ item.teams.length }}</v-chip>
</template>
<template #[`item.services`]="{ item }">
<v-chip small color="info" text-color="white">{{ item.services.length }}</v-chip>
</template>
<template #[`item.notifications`]="{ item }">
<v-chip small color="info" text-color="white">{{ item.notifications.length }}</v-chip>
</template>
<template #[`item.enabled`]="{ item }">
<v-simple-checkbox v-model="item.enabled" disabled />
</template>
<template #[`item.created_at`]="{ item }">
<v-tooltip bottom>
<template #activator="{ on, attrs }">
<span v-bind="attrs" v-on="on">{{ item.created_at | formatRelativeDate }}</span>
</template>
<span>{{ item.created_at | formatDate }}</span>
</v-tooltip>
</template>
<template #[`item.updated_at`]="{ item }">
<v-tooltip bottom>
<template #activator="{ on, attrs }">
<span v-bind="attrs" v-on="on">{{ item.updated_at | formatRelativeDate }}</span>
</template>
<span>{{ item.updated_at | formatDate }}</span>
</v-tooltip>
</template>
<template #[`item.data-table-actions`]="{ item }">
<v-menu bottom left>
<template #activator="{ on }">
<v-btn icon v-on="on">
<v-icon>mdi-dots-vertical</v-icon>
</v-btn>
</template>
<v-list>
<!-- <v-list-item @click="createEditShow(item)"> -->
<!-- <v-list-item-title>View / Edit</v-list-item-title> -->
<!-- </v-list-item> -->
<v-list-item @click="removeShow(item)">
<v-list-item-title>Delete</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
</template>
</v-data-table>
</v-card>
</v-col>
</v-row>
</v-container>
</template>

<script>
import { mapFields } from "vuex-map-fields"
import { mapActions } from "vuex"

import DeleteDialog from "@/search/DeleteDialog.vue"

export default {
name: "SearchFilterTable",

components: { DeleteDialog },

data() {
return {
headers: [
{ text: "Name", value: "name", align: "left", width: "10%" },
{ text: "Description", value: "description", sortable: false },
{ text: "Individuals", value: "individuals" },
{ text: "Teams", value: "teams" },
{ text: "Services", value: "services" },
{ text: "Notifications", value: "notifications" },
{ text: "Creator", value: "creator.email" },
{ text: "Created At", value: "created_at" },
{ text: "Updated At", value: "updated_at" },
{ text: "Enabled", value: "enabled" },
{ text: "", value: "data-table-actions", sortable: false, align: "end" },
],
showEditSheet: false,
}
},

computed: {
...mapFields("search", [
"table.loading",
"table.options.descending",
"table.options.filters.project",
"table.options.itemsPerPage",
"table.options.page",
"table.options.q",
"table.options.sortBy",
"table.rows.items",
"table.rows.total",
]),
...mapFields("route", ["query"]),
},

methods: {
...mapActions("search", ["getAll", "createEditShow", "removeShow"]),
},

created() {
this.project = [{ name: this.query.project }]
this.getAll()

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

this.$watch(
(vm) => [vm.q, vm.itemsPerPage, vm.sortBy, vm.descending, vm.project],
() => {
this.page = 1
this.$router.push({ query: { project: this.project[0].name } })
this.getAll()
}
)
},
}
</script>
1 change: 1 addition & 0 deletions src/dispatch/static/dispatch/src/search/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export default {
search(query, type) {
return API.get(`${resource}`, { params: { q: query, type: type } })
},

getAllFilters(options) {
return API.get(`${resource}/filters`, { params: { ...options } })
},
Expand Down
Loading