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
1 change: 1 addition & 0 deletions src/dispatch/case/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,7 @@ class CaseRead(CaseBase):
tags: Optional[List[TagRead]] = []
ticket: Optional[TicketRead] = None
triage_at: Optional[datetime] = None
updated_at: Optional[datetime] = None
workflow_instances: Optional[List[WorkflowInstanceRead]] = []


Expand Down
7 changes: 7 additions & 0 deletions src/dispatch/static/dispatch/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,21 +21,28 @@ declare module '@vue/runtime-core' {
DateTimePickerMenu: typeof import('./src/components/DateTimePickerMenu.vue')['default']
DateWindowInput: typeof import('./src/components/DateWindowInput.vue')['default']
DefaultLayout: typeof import('./src/components/layouts/DefaultLayout.vue')['default']
DMenu: typeof import('./src/components/DMenu.vue')['default']
DTooltip: typeof import('./src/components/DTooltip.vue')['default']
InfoWidget: typeof import('./src/components/InfoWidget.vue')['default']
Loading: typeof import('./src/components/Loading.vue')['default']
LockButton: typeof import('./src/components/LockButton.vue')['default']
MonacoEditor: typeof import('./src/components/MonacoEditor.vue')['default']
NotificationSnackbarsWrapper: typeof import('./src/components/NotificationSnackbarsWrapper.vue')['default']
PageHeader: typeof import('./src/components/PageHeader.vue')['default']
ParticipantAutoComplete: typeof import('./src/components/ParticipantAutoComplete.vue')['default']
ParticipantSelect: typeof import('./src/components/ParticipantSelect.vue')['default']
ProjectAutoComplete: typeof import('./src/components/ProjectAutoComplete.vue')['default']
Refresh: typeof import('./src/components/Refresh.vue')['default']
RichEditor: typeof import('./src/components/RichEditor.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
SavingState: typeof import('./src/components/SavingState.vue')['default']
SearchPopover: typeof import('./src/components/SearchPopover.vue')['default']
SettingsBreadcrumbs: typeof import('./src/components/SettingsBreadcrumbs.vue')['default']
ShepherdStep: typeof import('./src/components/ShepherdStep.vue')['default']
ShpherdStep: typeof import('./src/components/ShpherdStep.vue')['default']
StatWidget: typeof import('./src/components/StatWidget.vue')['default']
SubjectLastUpdated: typeof import('./src/components/SubjectLastUpdated.vue')['default']
VAlert: typeof import('vuetify/lib')['VAlert']
VApp: typeof import('vuetify/lib')['VApp']
VAppBar: typeof import('vuetify/lib')['VAppBar']
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import CaseSeveritySearchPopover from "@/case/severity/CaseSeveritySearchPopover
import CaseTypeSearchPopover from "@/case/type/CaseTypeSearchPopover.vue"
import ParticipantSearchPopover from "@/participant/ParticipantSearchPopover.vue"
import ProjectSearchPopover from "@/project/ProjectSearchPopover.vue"
import { useSavingState } from "@/composables/useSavingState"

// Define the props
const props = defineProps({
Expand All @@ -25,7 +26,7 @@ const props = defineProps({
// Define the emits
const emit = defineEmits(["update:modelValue", "update:open"])
const drawerVisible = ref(props.open)
// Create a local state for modelValue
const { setSaving } = useSavingState()
const modelValue = ref({ ...props.modelValue })

watch(
Expand All @@ -52,7 +53,9 @@ const handleResolutionUpdate = (newResolution) => {

const saveCaseDetails = async () => {
try {
setSaving(true)
await CaseApi.update(modelValue.value.id, modelValue.value)
setSaving(false)
} catch (e) {
console.error("Failed to save case details", e)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@ import type { Ref } from "vue"

import CaseApi from "@/case/api"
import SearchPopover from "@/components/SearchPopover.vue"
import { useSavingState } from "@/composables/useSavingState"
import { useStore } from "vuex"

defineProps<{ caseResolution: string }>()

const store = useStore()

const { setSaving } = useSavingState()
const caseResolutions: Ref<string[]> = ref([
"False Positive",
"User Acknowledged",
Expand All @@ -22,7 +23,9 @@ const selectCaseResolution = async (caseResolutionName: string) => {
const caseDetails = store.state.case_management.selected
caseDetails.resolution_reason = caseResolutionName

setSaving(true)
await CaseApi.update(caseDetails.id, caseDetails)
setSaving(false)
}
</script>

Expand Down
6 changes: 6 additions & 0 deletions src/dispatch/static/dispatch/src/case/Page.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
:case-name="caseDetails.name"
:case-visibility="caseDetails.visibility"
:case-status="caseDetails.status"
:case-updated-at="caseDetails.updated_at"
:is-drawer-open="isDrawerOpen"
@toggle-drawer="toggleDrawer"
/>
Expand Down Expand Up @@ -39,6 +40,7 @@ import PageHeader from "@/case//PageHeader.vue"
import CaseTabs from "@/case/CaseTabs.vue"
import RichEditor from "@/components/RichEditor.vue"
import CaseStatusSelectGroup from "@/case/CaseStatusSelectGroup.vue"
import { useSavingState } from "@/composables/useSavingState"

const route = useRoute()
const store = useStore()
Expand Down Expand Up @@ -72,6 +74,7 @@ const caseDefaults = {
tags: [],
ticket: null,
triage_at: null,
updated_at: null,
visibility: "",
conversation: null,
workflow_instances: null,
Expand All @@ -80,6 +83,7 @@ const caseDefaults = {
const caseDetails = ref(caseDefaults)
const loading = ref(false)
const isDrawerOpen = ref(true)
const { setSaving } = useSavingState()

const toggleDrawer = () => {
isDrawerOpen.value = !isDrawerOpen.value
Expand Down Expand Up @@ -115,7 +119,9 @@ const handleDescriptionUpdate = (newDescription) => {

const saveCaseDetails = async () => {
try {
setSaving(true)
await CaseApi.update(caseDetails.value.id, caseDetails.value)
setSaving(false)
} catch (e) {
console.error("Failed to save case details", e)
}
Expand Down
7 changes: 7 additions & 0 deletions src/dispatch/static/dispatch/src/case/PageHeader.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
<v-icon size="x-small" icon="mdi-chevron-right" />
</template>
</v-breadcrumbs>
<SavingState :updatedAt="caseUpdatedAt" />

<template #append>
<DTooltip text="View case participants" :hotkeys="['⌘', '⇧', 'P']">
<template #activator="{ tooltip }">
Expand Down Expand Up @@ -36,6 +38,7 @@ import LockButton from "@/components/LockButton.vue"
import EscalateButton from "@/case/EscalateButton.vue"
import DTooltip from "@/components/DTooltip.vue"
import ParticipantAvatarGroup from "@/participant/ParticipantAvatarGroup.vue"
import SavingState from "@/components/SavingState.vue"
import CaseApi from "@/case/api"

const route = useRoute()
Expand All @@ -53,6 +56,10 @@ const props = defineProps({
type: String,
required: true,
},
caseUpdatedAt: {
type: String,
required: true,
},
isDrawerOpen: {
type: Boolean,
default: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { onMounted, ref } from "vue"
import CasePriorityApi from "@/case/priority/api"
import CaseApi from "@/case/api"
import SearchPopover from "@/components/SearchPopover.vue"
import { useSavingState } from "@/composables/useSavingState"
import type { Ref } from "vue"
import { useStore } from "vuex"

Expand All @@ -13,7 +14,7 @@ type CasePriority = {
defineProps<{ casePriority: string }>()

const store = useStore()

const { setSaving } = useSavingState()
const casePriorities: Ref<CasePriority[]> = ref([])

onMounted(async () => {
Expand All @@ -39,7 +40,9 @@ const selectCasePriority = async (casePriorityName: string) => {
const caseDetails = store.state.case_management.selected
caseDetails.case_priority = caseType

setSaving(true)
await CaseApi.update(caseDetails.id, caseDetails)
setSaving(false)
}
</script>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { onMounted, ref } from "vue"
import CaseSeverityApi from "@/case/severity/api"
import CaseApi from "@/case/api"
import SearchPopover from "@/components/SearchPopover.vue"
import { useSavingState } from "@/composables/useSavingState"
import type { Ref } from "vue"
import { useStore } from "vuex"

Expand All @@ -13,7 +14,7 @@ type CaseSeverity = {
defineProps<{ caseSeverity: string }>()

const store = useStore()

const { setSaving } = useSavingState()
const caseSeveritys: Ref<CaseSeverity[]> = ref([])

onMounted(async () => {
Expand All @@ -39,7 +40,9 @@ const selectCaseSeverity = async (caseSeverityName: string) => {
const caseDetails = store.state.case_management.selected
caseDetails.case_severity = caseSeverity

setSaving(true)
await CaseApi.update(caseDetails.id, caseDetails)
setSaving(false)
}
</script>

Expand Down
6 changes: 6 additions & 0 deletions src/dispatch/static/dispatch/src/case/store.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,15 @@ const getDefaultSelectedState = () => {
reported_at: null,
resolution_reason: null,
resolution: null,
saving: false,
signals: [],
status: null,
storage: null,
tags: [],
ticket: null,
title: null,
triage_at: null,
updated_at: null,
visibility: null,
conversation: null,
workflow_instances: null,
Expand Down Expand Up @@ -93,6 +95,7 @@ const state = {
sortBy: ["reported_at"],
descending: [true],
},
saving: false,
loading: false,
bulkEditLoading: false,
},
Expand Down Expand Up @@ -447,6 +450,9 @@ const mutations = {
SET_SELECTED_LOADING(state, value) {
state.selected.loading = value
},
SET_SELECTED_SAVING(state, value) {
state.selected.saving = value
},
}

export default {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { onMounted, ref } from "vue"
import CaseTypeApi from "@/case/type/api"
import CaseApi from "@/case/api"
import SearchPopover from "@/components/SearchPopover.vue"
import { useSavingState } from "@/composables/useSavingState"
import type { Ref } from "vue"
import { useStore } from "vuex"

Expand All @@ -13,7 +14,7 @@ type CaseType = {
defineProps<{ caseType: string }>()

const store = useStore()

const { setSaving } = useSavingState()
const caseTypes: Ref<CaseType[]> = ref([])

onMounted(async () => {
Expand All @@ -39,7 +40,9 @@ const selectCaseType = async (caseTypeName: string) => {
const caseDetails = store.state.case_management.selected
caseDetails.case_type = caseType

setSaving(true)
await CaseApi.update(caseDetails.id, caseDetails)
setSaving(false)
}
</script>

Expand Down
51 changes: 51 additions & 0 deletions src/dispatch/static/dispatch/src/components/SavingState.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<template>
<div>
<v-progress-circular
v-if="saving"
indeterminate
size="14"
color="grey-lighten-1"
class="pl-6"
/>
<v-icon size="x-small" class="pl-4" v-else>mdi-check</v-icon>
<span class="pl-4 dispatch-text-subtitle">updated {{ formattedUpdatedAt }}</span>
</div>
</template>

<script setup lang="ts">
import { ref, watch, watchEffect } from "vue"
import { formatDistanceToNow, parseISO } from "date-fns"
import { useSavingState } from "@/composables/useSavingState"

const { saving } = useSavingState()
let formattedUpdatedAt = ref("")
let updatedAtRef = ref("")

watchEffect(() => {
if (updatedAtRef.value) {
formattedUpdatedAt.value = formatDistanceToNow(parseISO(updatedAtRef.value)) + " ago"
}
})

watch(saving, (newVal, oldVal) => {
if (oldVal === true && newVal === false) {
updatedAtRef.value = new Date().toISOString()
}
})

const props = defineProps({
updatedAt: {
type: String,
required: true,
},
})

watch(
() => props.updatedAt,
(newVal) => {
if (newVal) {
updatedAtRef.value = newVal
}
}
)
</script>
25 changes: 25 additions & 0 deletions src/dispatch/static/dispatch/src/composables/useSavingState.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { computed, ComputedRef } from "vue"
import { useStore } from "vuex"
import { Store } from "vuex"
import { CaseState } from "@/store/case"

interface UseSavingStateReturns {
saving: ComputedRef<boolean>
// eslint-disable-next-line no-unused-vars
setSaving: (value: boolean) => void
}

export function useSavingState(): UseSavingStateReturns {
const store = useStore<Store<{ case: CaseState }>>()

const saving = computed(() => store.state.case_management.selected.saving)

const setSaving = (value: boolean) => {
store.commit("case_management/SET_SELECTED_SAVING", value)
}

return {
saving,
setSaving,
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<script setup lang="ts">
import { ref, onMounted, watch, computed } from "vue"
import { useSavingState } from "@/composables/useSavingState"
import IndividualApi from "@/individual/api"
import Hotkey from "@/atomics/Hotkey.vue"
import { useHotKey } from "@/composables/useHotkey"
Expand All @@ -24,6 +25,7 @@ const props = withDefaults(
)

const store = useStore()
const { setSaving } = useSavingState()
const menu: Ref<boolean> = ref(false)
const participants: Ref<string[]> = ref([])
const selectedParticipant: Ref<string> = ref("")
Expand Down Expand Up @@ -83,8 +85,13 @@ watch(selectedParticipant, async (newValue: string) => {
caseDetails.reporter.individual = individual
}

// Call the CaseApi.update method to update the case details
await CaseApi.update(caseDetails.id, caseDetails)
setSaving(true)
try {
await CaseApi.update(caseDetails.id, caseDetails)
} catch (error) {
console.error("Error updating case:", error)
}
setSaving(false)
}
})

Expand Down
Loading