Skip to content

Commit 0dfebe2

Browse files
authored
Merge pull request #1 from CompassSecurity/feat/frontend/ui-improvements
- Add warning for unsaved changes when leaving an activity - Add autocomplete for asset properties key and value fields (frontend only - no API endpoint) - Improved Blue and Spectator Read-only behavior - Improved markdown edit and preview layout (tabs instead of toggle)
2 parents 45733b9 + 83fb536 commit 0dfebe2

9 files changed

Lines changed: 728 additions & 243 deletions

frontend/src/components/assessment/ActivityDetectionSection.vue

Lines changed: 104 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import { ChevronDown } from 'lucide-vue-next';
33
import { computed } from 'vue';
44
import ActivityAssetsManager from '@/components/assessment/ActivityAssetsManager.vue';
5+
import { Badge } from '@/components/ui/badge';
56
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
67
import {
78
Collapsible,
@@ -19,7 +20,9 @@ import {
1920
SelectValue,
2021
} from '@/components/ui/select';
2122
import { Switch } from '@/components/ui/switch';
23+
import { usePreferencesStore } from '@/stores/preferences';
2224
import type { ActivityRead, AssetRead } from '@/types/utils';
25+
import { formatDateTime } from '@/utils/dateFormatter';
2326
import { schemas } from '@/types/zod';
2427
2528
const props = defineProps<{
@@ -37,6 +40,17 @@ const formData = defineModel<Partial<ActivityRead>>('formData', {
3740
});
3841
3942
const severityOptions = schemas.ActivitySeverity.options;
43+
const preferencesStore = usePreferencesStore();
44+
45+
// Formatted date strings for readonly display
46+
function readonlyDate(value: string | null | undefined): string {
47+
return formatDateTime(
48+
value,
49+
preferencesStore.effectiveTimezone,
50+
preferencesStore.dateFormat,
51+
preferencesStore.timeFormat,
52+
);
53+
}
4054
4155
// Writable computed properties for detection toggles
4256
const logged = computed({
@@ -139,17 +153,26 @@ const stakeholderNotificationCreated = computed({
139153
<div class="flex items-center gap-3">
140154
<Label class="text-sm font-semibold" for="switch_logged">Activity Logged</Label>
141155
</div>
142-
<Switch id="switch_logged" v-model="logged" :disabled="readonly" />
156+
<template v-if="readonly">
157+
<Badge :class="logged ? 'bg-green-600 text-white' : 'bg-muted text-muted-foreground'">{{ logged ? 'Yes' : 'No' }}</Badge>
158+
</template>
159+
<template v-else>
160+
<Switch id="switch_logged" v-model="logged" />
161+
</template>
143162
</div>
144163

145164
<div v-if="logged" class="space-y-4 pt-2 border-t">
146165
<div class="space-y-2 w-full sm:w-1/2 pr-2">
147166
<Label class="text-sm font-medium">Log Time</Label>
148-
<DateTimePicker
149-
:model-value="formData.log_time ?? undefined"
150-
@update:model-value="formData.log_time = $event ?? null"
151-
:disabled="readonly"
152-
/>
167+
<template v-if="readonly">
168+
<div class="text-sm px-3 py-2 rounded-md border bg-muted/30 min-h-[36px] flex items-center">{{ readonlyDate(formData.log_time) }}</div>
169+
</template>
170+
<template v-else>
171+
<DateTimePicker
172+
:model-value="formData.log_time ?? undefined"
173+
@update:model-value="formData.log_time = $event ?? null"
174+
/>
175+
</template>
153176
</div>
154177
<ActivityAssetsManager
155178
:sources="formData.log_sources ?? []"
@@ -185,17 +208,26 @@ const stakeholderNotificationCreated = computed({
185208
<div class="flex items-center gap-3">
186209
<Label class="text-sm font-semibold" for="switch_prevented">Activity Prevented</Label>
187210
</div>
188-
<Switch id="switch_prevented" v-model="prevented" :disabled="readonly" />
211+
<template v-if="readonly">
212+
<Badge :class="prevented ? 'bg-green-600 text-white' : 'bg-muted text-muted-foreground'">{{ prevented ? 'Yes' : 'No' }}</Badge>
213+
</template>
214+
<template v-else>
215+
<Switch id="switch_prevented" v-model="prevented" />
216+
</template>
189217
</div>
190218

191219
<div v-if="prevented" class="space-y-4 pt-2 border-t">
192220
<div class="space-y-2 w-full sm:w-1/2 pr-2">
193221
<Label class="text-sm font-medium">Prevention Time</Label>
194-
<DateTimePicker
195-
:model-value="formData.prevent_time ?? undefined"
196-
@update:model-value="formData.prevent_time = $event ?? null"
197-
:disabled="readonly"
198-
/>
222+
<template v-if="readonly">
223+
<div class="text-sm px-3 py-2 rounded-md border bg-muted/30 min-h-[36px] flex items-center">{{ readonlyDate(formData.prevent_time) }}</div>
224+
</template>
225+
<template v-else>
226+
<DateTimePicker
227+
:model-value="formData.prevent_time ?? undefined"
228+
@update:model-value="formData.prevent_time = $event ?? null"
229+
/>
230+
</template>
199231
</div>
200232
<ActivityAssetsManager
201233
:sources="formData.prevention_sources ?? []"
@@ -231,31 +263,45 @@ const stakeholderNotificationCreated = computed({
231263
<div class="flex items-center gap-3">
232264
<Label class="text-sm font-semibold" for="switch_alerted">Alert Generated</Label>
233265
</div>
234-
<Switch id="switch_alerted" v-model="alerted" :disabled="readonly" />
266+
<template v-if="readonly">
267+
<Badge :class="alerted ? 'bg-green-600 text-white' : 'bg-muted text-muted-foreground'">{{ alerted ? 'Yes' : 'No' }}</Badge>
268+
</template>
269+
<template v-else>
270+
<Switch id="switch_alerted" v-model="alerted" />
271+
</template>
235272
</div>
236273

237274
<div v-if="alerted" class="space-y-4 pt-2 border-t">
238275
<div class="space-y-4">
239276
<div class="space-y-2 w-full sm:w-1/2 pr-2">
240277
<Label class="text-sm font-medium">Alert Time</Label>
241-
<DateTimePicker
242-
:model-value="formData.alert_time ?? undefined"
243-
@update:model-value="formData.alert_time = $event ?? null"
244-
:disabled="readonly"
245-
/>
278+
<template v-if="readonly">
279+
<div class="text-sm px-3 py-2 rounded-md border bg-muted/30 min-h-[36px] flex items-center">{{ readonlyDate(formData.alert_time) }}</div>
280+
</template>
281+
<template v-else>
282+
<DateTimePicker
283+
:model-value="formData.alert_time ?? undefined"
284+
@update:model-value="formData.alert_time = $event ?? null"
285+
/>
286+
</template>
246287
</div>
247288
<div class="space-y-2 w-full sm:w-1/2 pr-2">
248289
<Label class="text-sm font-medium">Alert Severity</Label>
249-
<Select v-model="formData.alert_severity" :disabled="readonly">
250-
<SelectTrigger class="w-full">
251-
<SelectValue :placeholder="formData.alert_severity || '\xa0'" />
252-
</SelectTrigger>
253-
<SelectContent>
254-
<SelectItem v-for="opt in severityOptions" :key="opt" :value="opt">
255-
{{ opt }}
256-
</SelectItem>
257-
</SelectContent>
258-
</Select>
290+
<template v-if="readonly">
291+
<div class="text-sm px-3 py-2 rounded-md border bg-muted/30 min-h-[36px] flex items-center">{{ formData.alert_severity || '—' }}</div>
292+
</template>
293+
<template v-else>
294+
<Select v-model="formData.alert_severity">
295+
<SelectTrigger class="w-full">
296+
<SelectValue :placeholder="formData.alert_severity || '\xa0'" />
297+
</SelectTrigger>
298+
<SelectContent>
299+
<SelectItem v-for="opt in severityOptions" :key="opt" :value="opt">
300+
{{ opt }}
301+
</SelectItem>
302+
</SelectContent>
303+
</Select>
304+
</template>
259305
</div>
260306
</div>
261307
<ActivityAssetsManager
@@ -292,31 +338,45 @@ const stakeholderNotificationCreated = computed({
292338
<div class="flex items-center gap-3">
293339
<Label class="text-sm font-semibold" for="switch_stakeholder">Stakeholder Notification Created</Label>
294340
</div>
295-
<Switch id="switch_stakeholder" v-model="stakeholderNotificationCreated" :disabled="readonly" />
341+
<template v-if="readonly">
342+
<Badge :class="stakeholderNotificationCreated ? 'bg-green-600 text-white' : 'bg-muted text-muted-foreground'">{{ stakeholderNotificationCreated ? 'Yes' : 'No' }}</Badge>
343+
</template>
344+
<template v-else>
345+
<Switch id="switch_stakeholder" v-model="stakeholderNotificationCreated" />
346+
</template>
296347
</div>
297348

298349
<div v-if="stakeholderNotificationCreated" class="space-y-4 pt-2 border-t">
299350
<div class="space-y-4">
300351
<div class="space-y-2 w-full sm:w-1/2 pr-2">
301352
<Label class="text-sm font-medium">Notification Time</Label>
302-
<DateTimePicker
303-
:model-value="formData.stakeholder_notification_time ?? undefined"
304-
@update:model-value="formData.stakeholder_notification_time = $event ?? null"
305-
:disabled="readonly"
306-
/>
353+
<template v-if="readonly">
354+
<div class="text-sm px-3 py-2 rounded-md border bg-muted/30 min-h-[36px] flex items-center">{{ readonlyDate(formData.stakeholder_notification_time) }}</div>
355+
</template>
356+
<template v-else>
357+
<DateTimePicker
358+
:model-value="formData.stakeholder_notification_time ?? undefined"
359+
@update:model-value="formData.stakeholder_notification_time = $event ?? null"
360+
/>
361+
</template>
307362
</div>
308363
<div class="space-y-2 w-full sm:w-1/2 pr-2">
309364
<Label class="text-sm font-medium">Notification Severity</Label>
310-
<Select v-model="formData.stakeholder_notification_severity" :disabled="readonly">
311-
<SelectTrigger class="w-full">
312-
<SelectValue :placeholder="formData.stakeholder_notification_severity || '\xa0'" />
313-
</SelectTrigger>
314-
<SelectContent>
315-
<SelectItem v-for="opt in severityOptions" :key="opt" :value="opt">
316-
{{ opt }}
317-
</SelectItem>
318-
</SelectContent>
319-
</Select>
365+
<template v-if="readonly">
366+
<div class="text-sm px-3 py-2 rounded-md border bg-muted/30 min-h-[36px] flex items-center">{{ formData.stakeholder_notification_severity || '—' }}</div>
367+
</template>
368+
<template v-else>
369+
<Select v-model="formData.stakeholder_notification_severity">
370+
<SelectTrigger class="w-full">
371+
<SelectValue :placeholder="formData.stakeholder_notification_severity || '\xa0'" />
372+
</SelectTrigger>
373+
<SelectContent>
374+
<SelectItem v-for="opt in severityOptions" :key="opt" :value="opt">
375+
{{ opt }}
376+
</SelectItem>
377+
</SelectContent>
378+
</Select>
379+
</template>
320380
</div>
321381
</div>
322382
<ActivityAssetsManager

frontend/src/components/assessment/ActivityEvaluation.vue

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -183,9 +183,9 @@ async function handleDynamicQuestionsUpdated() {
183183
<Badge :class="evalBadgeClass(eventToAlertEvalStatus)">{{ eventToAlertEvalStatus }}</Badge>
184184
</div>
185185
<EvalResultToggle
186+
v-if="!readonly"
186187
:model-value="(formData.evaluation as any)?.event_to_alert_evaluation_result || 'n/a'"
187188
@update:model-value="formData.evaluation = { ...formData.evaluation!, ['event_to_alert_evaluation_result']: $event as any }"
188-
:disabled="readonly"
189189
/>
190190
</div>
191191
<MarkdownEditor :on-upload="uploadImage" :resolve-image-url="resolveImageUrl"
@@ -204,9 +204,9 @@ async function handleDynamicQuestionsUpdated() {
204204
<Badge :class="evalBadgeClass(alertToStakeholderEvalStatus)">{{ alertToStakeholderEvalStatus }}</Badge>
205205
</div>
206206
<EvalResultToggle
207+
v-if="!readonly"
207208
:model-value="(formData.evaluation as any)?.alert_to_stakeholder_evaluation_result || 'n/a'"
208209
@update:model-value="formData.evaluation = { ...formData.evaluation!, ['alert_to_stakeholder_evaluation_result']: $event as any }"
209-
:disabled="readonly"
210210
/>
211211
</div>
212212
<MarkdownEditor :on-upload="uploadImage" :resolve-image-url="resolveImageUrl"
@@ -225,9 +225,9 @@ async function handleDynamicQuestionsUpdated() {
225225
<Badge :class="evalBadgeClass(alertSeverityEvalStatus)">{{ alertSeverityEvalStatus }}</Badge>
226226
</div>
227227
<EvalResultToggle
228+
v-if="!readonly"
228229
:model-value="(formData.evaluation as any)?.alert_severity_evaluation_result || 'n/a'"
229230
@update:model-value="formData.evaluation = { ...formData.evaluation!, ['alert_severity_evaluation_result']: $event as any }"
230-
:disabled="readonly"
231231
/>
232232
</div>
233233
<MarkdownEditor :on-upload="uploadImage" :resolve-image-url="resolveImageUrl"
@@ -246,9 +246,9 @@ async function handleDynamicQuestionsUpdated() {
246246
<Badge :class="evalBadgeClass(stakeholderSeverityEvalStatus)">{{ stakeholderSeverityEvalStatus }}</Badge>
247247
</div>
248248
<EvalResultToggle
249+
v-if="!readonly"
249250
:model-value="(formData.evaluation as any)?.stakeholder_notification_severity_evaluation_result || 'n/a'"
250251
@update:model-value="formData.evaluation = { ...formData.evaluation!, ['stakeholder_notification_severity_evaluation_result']: $event as any }"
251-
:disabled="readonly"
252252
/>
253253
</div>
254254
<MarkdownEditor :on-upload="uploadImage" :resolve-image-url="resolveImageUrl"
@@ -290,9 +290,9 @@ async function handleDynamicQuestionsUpdated() {
290290
</Badge>
291291
</div>
292292
<EvalResultToggle
293+
v-if="!readonly"
293294
:model-value="question.evaluation_result || 'n/a'"
294295
@update:model-value="updateDynamicQuestion(question.evaluation_template_id, 'evaluation_result', $event)"
295-
:disabled="readonly"
296296
/>
297297
</div>
298298
<p v-if="evaluationTemplates[question.evaluation_template_id]?.description" class="text-xs text-muted-foreground">

0 commit comments

Comments
 (0)