- {{ event.source }}
+
+ {{
+ sourceIconMap[event.source].icon
+ }}
+
+
+
+
+
+
+ {{ sourceIconMap[event.source]?.sourceName || event.source }}
+
+ {{ descriptionMap[event.description] || event.description }} ·
+ {{ formatRelativeDate(event.started_at) }}
+
-
-
-
- {{
- formatToUTC(event.started_at)
- }}
-
- {{ formatToTimeZones(event.started_at) }}
-
-
+
+
+ (times in UTC)
+
-
No timeline data available.
+
-
-
-
+
diff --git a/src/dispatch/static/dispatch/src/case/api.js b/src/dispatch/static/dispatch/src/case/api.js
index 2fc1a01f27e3..2097db6ed170 100644
--- a/src/dispatch/static/dispatch/src/case/api.js
+++ b/src/dispatch/static/dispatch/src/case/api.js
@@ -13,6 +13,12 @@ export default {
return API.get(`/${resource}/${caseId}`)
},
+ getParticipants(caseId, minimal = true) {
+ return API.get(`/${resource}/${caseId}/participants`, {
+ params: { minimal },
+ })
+ },
+
create(payload) {
return API.post(`/${resource}`, payload)
},
diff --git a/src/dispatch/static/dispatch/src/case/priority/CasePrioritySearchPopover.vue b/src/dispatch/static/dispatch/src/case/priority/CasePrioritySearchPopover.vue
new file mode 100644
index 000000000000..3ccf87dd8617
--- /dev/null
+++ b/src/dispatch/static/dispatch/src/case/priority/CasePrioritySearchPopover.vue
@@ -0,0 +1,55 @@
+
+
+
+
+
+
diff --git a/src/dispatch/static/dispatch/src/case/severity/CaseSeveritySearchPopover.vue b/src/dispatch/static/dispatch/src/case/severity/CaseSeveritySearchPopover.vue
new file mode 100644
index 000000000000..de78a2918e3b
--- /dev/null
+++ b/src/dispatch/static/dispatch/src/case/severity/CaseSeveritySearchPopover.vue
@@ -0,0 +1,55 @@
+
+
+
+
+
+
diff --git a/src/dispatch/static/dispatch/src/case/type/CaseTypeSearchPopover.vue b/src/dispatch/static/dispatch/src/case/type/CaseTypeSearchPopover.vue
new file mode 100644
index 000000000000..ac6c2f4ab491
--- /dev/null
+++ b/src/dispatch/static/dispatch/src/case/type/CaseTypeSearchPopover.vue
@@ -0,0 +1,62 @@
+
+
+
+
+
+ {{ item }}
+
+
+
+
+
diff --git a/src/dispatch/static/dispatch/src/components/DMenu.vue b/src/dispatch/static/dispatch/src/components/DMenu.vue
new file mode 100644
index 000000000000..79038406894d
--- /dev/null
+++ b/src/dispatch/static/dispatch/src/components/DMenu.vue
@@ -0,0 +1,53 @@
+
+
+
+
+
+ mdi-dots-horizontal
+
+
+
+
+
+
+
+ {{ option }}
+
+
+
+
+
+
+
+
+
+
diff --git a/src/dispatch/static/dispatch/src/components/DTooltip.vue b/src/dispatch/static/dispatch/src/components/DTooltip.vue
new file mode 100644
index 000000000000..e9fd3c4c21b6
--- /dev/null
+++ b/src/dispatch/static/dispatch/src/components/DTooltip.vue
@@ -0,0 +1,71 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ text }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/dispatch/static/dispatch/src/components/LockButton.vue b/src/dispatch/static/dispatch/src/components/LockButton.vue
new file mode 100644
index 000000000000..203daecbe1bf
--- /dev/null
+++ b/src/dispatch/static/dispatch/src/components/LockButton.vue
@@ -0,0 +1,85 @@
+
+
+
+ {{ lockIcon }}
+
+
+
+
+ Change {{ subjectTitle }} Visibility
+
+ You are about to change the {{ subjectTitle.toLowerCase() }} visibility from
+ {{ visibility }} to {{ toggleVisibility }}.
+
+
+
+ Confirm
+
+
+
+
+
+
+
diff --git a/src/dispatch/static/dispatch/src/components/RichEditor.vue b/src/dispatch/static/dispatch/src/components/RichEditor.vue
new file mode 100644
index 000000000000..187fa88c55e4
--- /dev/null
+++ b/src/dispatch/static/dispatch/src/components/RichEditor.vue
@@ -0,0 +1,88 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/dispatch/static/dispatch/src/components/SearchPopover.vue b/src/dispatch/static/dispatch/src/components/SearchPopover.vue
new file mode 100644
index 000000000000..d60d64c05e2c
--- /dev/null
+++ b/src/dispatch/static/dispatch/src/components/SearchPopover.vue
@@ -0,0 +1,139 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ props.label }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ item }}
+
+
+
+
+
+
+
+
+
+
diff --git a/src/dispatch/static/dispatch/src/composables/useEventListener.ts b/src/dispatch/static/dispatch/src/composables/useEventListener.ts
new file mode 100644
index 000000000000..76f925ece9af
--- /dev/null
+++ b/src/dispatch/static/dispatch/src/composables/useEventListener.ts
@@ -0,0 +1,27 @@
+import { onMounted, onUnmounted } from "vue"
+
+/**
+ * A composable function to handle event listeners.
+ *
+ * @param target - The target to attach the event listener to.
+ * @param event - The event to listen for.
+ * @param callback - A function to call when the event is triggered.
+ *
+ * Usage:
+ * ```
+ * import { useEventListener } from '@/composables/useEventListener'
+ *
+ * // In your setup function, call useEventListener with your target, event, and callback function
+ * // Example for listening to 'mousemove' event on window
+ * useEventListener(window, 'mousemove', (event) => {
+ * // Do something when 'mousemove' event is triggered
+ * })
+ * ```
+ */
+export function useEventListener(target: Window, event: string, callback: (e: Event) => void) {
+ // On component mount, add the event listener to the target
+ onMounted(() => target.addEventListener(event, callback))
+
+ // On component unmount, remove the event listener from the target
+ onUnmounted(() => target.removeEventListener(event, callback))
+}
diff --git a/src/dispatch/static/dispatch/src/composables/useHotkey.ts b/src/dispatch/static/dispatch/src/composables/useHotkey.ts
new file mode 100644
index 000000000000..2a68f31bf32f
--- /dev/null
+++ b/src/dispatch/static/dispatch/src/composables/useHotkey.ts
@@ -0,0 +1,71 @@
+import { Ref, ref } from "vue"
+import { useEventListener } from "@/composables/useEventListener"
+
+type Key = keyof typeof KeyboardEvent.prototype
+
+/**
+ * A composable function to handle hotkeys.
+ *
+ * @param keyCombination - An array of keys that form the hotkey.
+ * @param callback - A function to call when the hotkey is pressed.
+ * @param allowFocusSteal - A boolean to allow the hotkeys to steal focus from an active element.
+ *
+ * Usage:
+ * ```
+ * import { useHotKey } from './useHotKey'
+ *
+ * // In your setup function, call useHotKey with your desired key combination and callback function
+ * // Example for single key 'a'
+ * useHotKey(["a"], (event) => {
+ * // Do something when 'a' is pressed
+ * })
+ *
+ * // Example for combination of keys 'Meta' + 'Shift' + 'p'
+ * useHotKey(["Meta", "Shift", "p"], (event) => {
+ * // Do something when 'Meta' + 'Shift' + 'p' is pressed
+ * })
+ * ```
+ */
+export function useHotKey(
+ keyCombination: Key[],
+ callback: (event: KeyboardEvent) => void,
+ allowFocusSteal: boolean = false
+) {
+ // Define a ref to keep track of keys that are currently pressed
+ // This is a record where the keys are Key types and the values are booleans
+ let keysPressed: Ref
> = ref({} as Record)
+
+ const handleKeyDown = (event: KeyboardEvent) => {
+ // Check if the user wants to ignore keys pressed when an element is focused
+ if (document.activeElement !== document.body && !allowFocusSteal) {
+ return
+ }
+
+ // When a key is pressed, add it to the keysPressed record
+ keysPressed.value[event.key] = true
+
+ // Check if all keys in the keyCombination array are currently pressed
+ // and that the last key pressed is the final key in the keyCombination array
+ // If so, call the provided callback function
+ if (
+ keyCombination.every((key) => keysPressed.value[key]) &&
+ event.key === keyCombination[keyCombination.length - 1] &&
+ Object.keys(keysPressed.value).length === keyCombination.length
+ ) {
+ callback(event)
+ }
+ }
+
+ const handleKeyUp = (event: KeyboardEvent) => {
+ // Check if the user wants to ignore keys released when an element is focused
+ if (document.activeElement !== document.body && !allowFocusSteal) {
+ return
+ }
+
+ // When a key is released, remove it from the keysPressed record
+ delete keysPressed.value[event.key]
+ }
+
+ useEventListener(window, "keydown", handleKeyDown)
+ useEventListener(window, "keyup", handleKeyUp)
+}
diff --git a/src/dispatch/static/dispatch/src/participant/ParticipantAvatarGroup.vue b/src/dispatch/static/dispatch/src/participant/ParticipantAvatarGroup.vue
new file mode 100644
index 000000000000..2d01332ec786
--- /dev/null
+++ b/src/dispatch/static/dispatch/src/participant/ParticipantAvatarGroup.vue
@@ -0,0 +1,197 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ participant.individual.name }}
+
+
+
+
+
+
+
+
+
diff --git a/src/dispatch/static/dispatch/src/participant/ParticipantSearchPopover.vue b/src/dispatch/static/dispatch/src/participant/ParticipantSearchPopover.vue
new file mode 100644
index 000000000000..0d128256f04a
--- /dev/null
+++ b/src/dispatch/static/dispatch/src/participant/ParticipantSearchPopover.vue
@@ -0,0 +1,264 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ props.label }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ participant }}
+
+
+ mdi-check
+
+
+
+
+
+
+
+
+
+
diff --git a/src/dispatch/static/dispatch/src/project/ProjectSearchPopover.vue b/src/dispatch/static/dispatch/src/project/ProjectSearchPopover.vue
new file mode 100644
index 000000000000..d309fa037dd8
--- /dev/null
+++ b/src/dispatch/static/dispatch/src/project/ProjectSearchPopover.vue
@@ -0,0 +1,57 @@
+
+
+
+
+
+
diff --git a/src/dispatch/static/dispatch/src/router/config.js b/src/dispatch/static/dispatch/src/router/config.js
index df77f94e7bea..ebce1af4e6cd 100644
--- a/src/dispatch/static/dispatch/src/router/config.js
+++ b/src/dispatch/static/dispatch/src/router/config.js
@@ -46,7 +46,7 @@ export const publicRoute = [
},
{
path: "/implicit/callback",
- name: "PKCEImplicityCallback",
+ name: "PKCEImplicitlyCallback",
meta: { requiresAuth: true },
},
{
@@ -194,7 +194,7 @@ export const protectedRoute = [
component: () => import("@/case/Table.vue"),
children: [
{
- path: "/:organization/cases/:name",
+ path: "/:organization/cases/:name/edit",
name: "CaseTableEdit",
component: () => import("@/case/EditSheet.vue"),
props: true,
@@ -204,6 +204,20 @@ export const protectedRoute = [
},
],
},
+ {
+ path: "/:organization/cases/:id",
+ name: "CasePage",
+ meta: { title: "Page" },
+ component: () => import("@/case/Page.vue"),
+ children: [
+ {
+ path: "signal/:signal_id",
+ name: "SignalDetails",
+ component: () => import("@/case/Page.vue"), // Use the same component to avoid re-render
+ props: true,
+ },
+ ],
+ },
],
},
{
diff --git a/src/dispatch/static/dispatch/src/signal/NewRawSignalViewer.vue b/src/dispatch/static/dispatch/src/signal/NewRawSignalViewer.vue
new file mode 100644
index 000000000000..0ebb7c4da33c
--- /dev/null
+++ b/src/dispatch/static/dispatch/src/signal/NewRawSignalViewer.vue
@@ -0,0 +1,78 @@
+
+
+
+
+
+
+
diff --git a/src/dispatch/static/dispatch/src/signal/SignalInstanceNode.vue b/src/dispatch/static/dispatch/src/signal/SignalInstanceNode.vue
new file mode 100644
index 000000000000..119954c53e43
--- /dev/null
+++ b/src/dispatch/static/dispatch/src/signal/SignalInstanceNode.vue
@@ -0,0 +1,23 @@
+
+
+
+
+
diff --git a/src/dispatch/static/dispatch/src/styles/index.scss b/src/dispatch/static/dispatch/src/styles/index.scss
index 39de3e324c24..2cf83b2a8758 100644
--- a/src/dispatch/static/dispatch/src/styles/index.scss
+++ b/src/dispatch/static/dispatch/src/styles/index.scss
@@ -1,7 +1,52 @@
.v-data-table {
- font-size: 0.875rem;
+ font-size: 0.875rem;
}
.v-card--variant-outlined {
- border-color: rgb(210, 210, 210);
+ border-color: rgb(210, 210, 210);
+}
+
+.dispatch-side-card {
+ backdrop-filter: blur(12px) saturate(190%) contrast(50%) brightness(130%) !important;
+ border: 0.5px solid rgb(216, 216, 216) !important;
+ border-radius: 8px !important;
+ box-shadow: rgba(0, 0, 0, 0.09) 0px 3px 12px !important;
+ color: rgb(60, 65, 73) !important;
+ opacity: 2 !important;
+}
+
+.dispatch-text-paragraph {
+ font-size: 0.75rem !important;
+}
+
+.dispatch-text-subtitle {
+ font-size: 0.8125rem !important;
+ font-weight: 500 !important;
+ color: rgb(60, 65, 73) !important;
+}
+
+.dispatch-text-title {
+ font-size: 13px !important;
+}
+
+.dispatch-button {
+ text-transform: none !important;
+ display: inline-flex !important;
+ align-items: center !important;
+ justify-content: center !important;
+ margin: 0px !important;
+ font-weight: 500px !important;
+ line-height: normal !important;
+ transition-property: border, background-color, color, opacity !important;
+ transition-duration: 0.15s !important;
+ user-select: none !important;
+ box-shadow: rgba(0, 0, 0, 0.09) 0px 1px 4px !important;
+ background-color: rgb(255, 255, 255) !important;
+ border: 1px solid rgb(223, 225, 228) !important;
+ border-radius: 4px !important;
+ color: rgb(60, 65, 73) !important;
+ min-width: 28px !important;
+ height: 28px !important;
+ padding: 0px 14px !important;
+ font-size: 0.75rem !important;
}