Skip to content

Commit b0b7a25

Browse files
henrychoykeithmanville
authored andcommitted
feat(frontend): add UI element to browse queue history
This commit adds a new UI element that allows the user to browse the history of a queue resource. In the edit queue dialog, click the view history toggle to browse snapshots. The Latest snapshot will be selected by default. Click or tab over to the list, and then you can use up/down arrow keys. Submit button will be disabled when viewing history. feat: simplify snapshot list fix: hide history toggle for drafts fix: form edits should be cleared when toggling on history fix: widen width of history column fix: use snapshotCreatedOn for timestamp fix: getSnapshots should get all results, not just first page
1 parent 26f2095 commit b0b7a25

File tree

3 files changed

+214
-52
lines changed

3 files changed

+214
-52
lines changed

src/frontend/src/dialogs/DialogComponent.vue

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,18 @@
11
<template>
22
<q-dialog v-model="showDialog" aria-labelledby="modalTitle" :persistent="persistent">
3-
<q-card style="width: 95%" flat >
3+
<q-card flat style="min-width: 500px; max-width: 80%;">
44
<q-form @submit="$emit('emitSubmit')">
55
<q-card-section class="bg-primary text-white q-mb-md">
6-
<div class="text-h6">
6+
<div class="text-h6 row justify-between">
77
<slot name="title" />
8+
<q-toggle
9+
v-if="showHistoryToggle"
10+
v-model="history"
11+
color="orange"
12+
left-label
13+
label="View History"
14+
class="text-body2"
15+
/>
816
</div>
917
</q-card-section>
1018
<q-card-section>
@@ -21,15 +29,19 @@
2129
/>
2230
<q-space />
2331
<q-btn color="negative" class="text-white" label="Cancel" @click="$emit('emitCancel')" v-close-popup />
24-
<q-btn color="primary" label="Confirm" type="submit" />
32+
<q-btn color="primary" label="Confirm" type="submit" :disable="disableConfirm" />
2533
</q-card-actions>
2634
</q-form>
2735
</q-card>
2836
</q-dialog>
2937
</template>
3038

3139
<script setup>
32-
const showDialog = defineModel()
40+
import { ref } from 'vue'
41+
const showDialog = defineModel('showDialog')
3342
defineEmits(['emitSubmit', 'emitCancel', 'emitSaveDraft'])
34-
const props = defineProps(['hideDraftBtn', 'persistent'])
43+
const props = defineProps(['hideDraftBtn', 'persistent', 'showHistoryToggle', 'disableConfirm'])
44+
45+
const history = defineModel('history')
46+
3547
</script>
Lines changed: 179 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,68 +1,137 @@
11
<template>
22
<DialogComponent
3-
v-model="showDialog"
3+
v-model:showDialog="showDialog"
4+
v-model:history="history"
45
@emitSubmit="emitAddOrEdit"
56
@emitSaveDraft="saveDraft"
67
:hideDraftBtn="editQueue ? true : false"
8+
:showHistoryToggle="editQueue && !Object.hasOwn(editQueue, 'payload')"
9+
:disableConfirm="history"
710
>
811
<template #title>
912
<label id="modalTitle">
1013
{{editQueue ? 'Edit Queue' : 'Register Queue'}}
1114
</label>
1215
</template>
13-
<div class="row items-center">
14-
<label class="col-3 q-mb-lg" id="queueName">
15-
Queue Name:
16-
</label>
17-
<q-input
18-
class="col q-mb-xs"
19-
outlined
20-
dense
21-
v-model.trim="name"
22-
autofocus
23-
:rules="[requiredRule]"
24-
aria-labelledby="queueName"
25-
aria-required="true"
26-
/>
27-
</div>
28-
<div class="row items-center q-mb-xs">
29-
<label class="col-3 q-mb-lg" id="pluginGroup">
30-
Group:
31-
</label>
32-
<q-select
33-
class="col"
34-
outlined
35-
v-model="group"
36-
:options="store.groups"
37-
option-label="name"
38-
option-value="id"
39-
emit-value
40-
map-options
41-
dense
42-
:rules="[requiredRule]"
43-
aria-labelledby="pluginGroup"
44-
aria-required="true"
45-
/>
46-
</div>
47-
<div class="row items-center">
48-
<label class="col-3" id="description">
49-
Description:
50-
</label>
51-
<q-input
52-
class="col"
53-
v-model.trim="description"
54-
outlined
55-
type="textarea"
56-
aria-labelledby="description"
57-
/>
16+
<div class="row no-wrap">
17+
<!-- this is the form col -->
18+
<div
19+
style="width: 500px;"
20+
:style="{
21+
pointerEvents: history ? 'none' : 'auto',
22+
opacity: history ? .65 : 1,
23+
cursor: history ? 'not-allowed' : 'default'
24+
}"
25+
>
26+
<div class="row items-center">
27+
<label class="col-3 q-mb-lg" id="queueName">
28+
Queue Name:
29+
</label>
30+
<q-input
31+
class="col q-mb-xs"
32+
outlined
33+
dense
34+
v-model.trim="name"
35+
autofocus
36+
:rules="[requiredRule]"
37+
aria-labelledby="queueName"
38+
aria-required="true"
39+
/>
40+
</div>
41+
<div class="row items-center q-mb-xs">
42+
<label class="col-3 q-mb-lg" id="pluginGroup">
43+
Group:
44+
</label>
45+
<q-select
46+
class="col"
47+
outlined
48+
v-model="group"
49+
:options="store.groups"
50+
option-label="name"
51+
option-value="id"
52+
emit-value
53+
map-options
54+
dense
55+
:rules="[requiredRule]"
56+
aria-labelledby="pluginGroup"
57+
aria-required="true"
58+
/>
59+
</div>
60+
<div class="row items-center">
61+
<label class="col-3" id="description">
62+
Description:
63+
</label>
64+
<q-input
65+
class="col"
66+
v-model.trim="description"
67+
outlined
68+
type="textarea"
69+
aria-labelledby="description"
70+
/>
71+
</div>
72+
<!-- <q-inner-loading :showing="history" size="0px" /> -->
73+
</div>
74+
<!-- this is the history col -->
75+
<q-card
76+
v-if="history"
77+
style="width: 240px; max-height: 263px; overflow: auto;"
78+
flat
79+
bordered
80+
class="q-ml-sm col"
81+
>
82+
<q-list bordered separator>
83+
<q-item
84+
v-for="(snapshot, i) in snapshots"
85+
tag="label"
86+
v-ripple
87+
dense
88+
clickable
89+
@click="loadSnapshot(snapshot, i)"
90+
@keydown="keydown"
91+
:class="`${getSelectedColor(selectedSnapshotIndex === i)} cursor-pointer` "
92+
>
93+
<!-- <q-item-section avatar>
94+
<q-radio
95+
v-model="selectedSnapshot"
96+
:val="snapshot"
97+
@update:model-value="loadSnapshot(snapshot)"
98+
/>
99+
</q-item-section> -->
100+
<q-item-section>
101+
<q-item-label>
102+
{{
103+
new Intl.DateTimeFormat('en-US', {
104+
year: '2-digit',
105+
month: '2-digit',
106+
day: '2-digit',
107+
hour: 'numeric',
108+
minute: 'numeric',
109+
hour12: true
110+
}).format(new Date(snapshot.snapshotCreatedOn))
111+
}}
112+
<q-chip
113+
v-if="snapshot.latestSnapshot"
114+
label="latest"
115+
size="md"
116+
dense
117+
color="orange"
118+
text-color="white"
119+
/>
120+
</q-item-label>
121+
</q-item-section>
122+
</q-item>
123+
</q-list>
124+
</q-card>
58125
</div>
59126
</DialogComponent>
60127
</template>
61128

62129
<script setup>
63-
import { ref, watch } from 'vue'
130+
import { ref, watch, computed } from 'vue'
64131
import DialogComponent from './DialogComponent.vue'
65132
import { useLoginStore } from '@/stores/LoginStore.ts'
133+
import * as api from '@/services/dataApi'
134+
import { useQuasar } from 'quasar'
66135
67136
const store = useLoginStore()
68137
@@ -79,6 +148,13 @@
79148
const group = ref('')
80149
const description = ref('')
81150
151+
function loadSnapshot(snapshot, index) {
152+
selectedSnapshotIndex.value = index
153+
name.value = snapshot.name
154+
group.value = snapshot.group
155+
description.value = snapshot.description
156+
}
157+
82158
watch(showDialog, (newVal) => {
83159
if(newVal) {
84160
name.value = props.editQueue.name
@@ -88,6 +164,8 @@
88164
else {
89165
name.value = ''
90166
description.value = ''
167+
history.value = false
168+
snapshots.value = []
91169
}
92170
})
93171
@@ -109,5 +187,59 @@
109187
emit('saveDraft', name.value, description.value, props.editQueue.id)
110188
}
111189
190+
const history = ref(false)
191+
192+
const snapshots = ref([])
193+
const selectedSnapshot = ref()
194+
const selectedSnapshotIndex = ref()
195+
196+
async function getSnapshots() {
197+
try {
198+
const res = await api.getSnapshots('queues', props.editQueue.id)
199+
snapshots.value = res.data.data.reverse()
200+
selectedSnapshot.value = snapshots.value[0]
201+
selectedSnapshotIndex.value = 0
202+
loadSnapshot(snapshots.value[0], 0)
203+
} catch(err) {
204+
console.warn(err)
205+
}
206+
}
207+
208+
watch(history, (newVal) => {
209+
if(newVal) {
210+
getSnapshots()
211+
} else {
212+
selectedSnapshotIndex.value = 0
213+
name.value = props.editQueue.name
214+
description.value = props.editQueue.description
215+
group.value = props.editQueue.group
216+
}
217+
})
218+
219+
const $q = useQuasar()
220+
221+
const darkMode = computed(() => {
222+
if($q.dark.mode === 'auto') {
223+
return window.matchMedia('(prefers-color-scheme: dark)').matches
224+
}
225+
return $q.dark.mode
226+
})
227+
228+
function getSelectedColor(selected) {
229+
if(darkMode.value && selected) return 'bg-deep-purple-10'
230+
else if(selected) return 'bg-blue-grey-2'
231+
}
232+
233+
function keydown(event) {
234+
if(event.key === 'ArrowUp' && selectedSnapshotIndex.value > 0) {
235+
const newIndex = selectedSnapshotIndex.value - 1
236+
loadSnapshot(snapshots.value[newIndex], newIndex)
237+
}
238+
else if(event.key === 'ArrowDown' && selectedSnapshotIndex.value < snapshots.value.length - 1) {
239+
const newIndex = selectedSnapshotIndex.value + 1
240+
loadSnapshot(snapshots.value[newIndex], newIndex)
241+
}
242+
}
243+
112244
113245
</script>

src/frontend/src/services/dataApi.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,24 @@ export async function getData<T extends ItemType>(type: T, pagination: Paginatio
155155
return res
156156
}
157157

158+
export async function getSnapshots<T extends ItemType>(type: T, id: number) {
159+
const res = await axios.get(`/api/${type}/${id}/snapshots`, {
160+
params: {
161+
pageLength: 100
162+
}
163+
})
164+
165+
if(res.data.next) {
166+
let nextUrl = res.data.next.replace("/v1", "")
167+
while (nextUrl) {
168+
const response = await axios.get(nextUrl)
169+
res.data.data.push(...response.data.data)
170+
nextUrl = response.data.next ? response.data.next.replace("/v1", "") : null
171+
}
172+
}
173+
return res
174+
}
175+
158176
export async function getJobs(id: number, pagination: Pagination) {
159177
const res = await axios.get(`/api/experiments/${id}/jobs`, {
160178
params: {

0 commit comments

Comments
 (0)