Skip to content

Commit

Permalink
feat(frontend): add panel to browse experiment history
Browse files Browse the repository at this point in the history
This commit adds the ability for user to browse Experiment history in the view/edit form. Clicking
the toggle will bring out the right drawer containing the list of snapshots. Selecting a row loads
the values into the form. Up/down keyboard navigation works once focus is on the table. The table
can be sorted. The Queue dialog was also updated to use this SnapshotList component.
  • Loading branch information
henrychoy authored and keithmanville committed Jan 29, 2025
1 parent dbd1f3e commit 693317a
Show file tree
Hide file tree
Showing 11 changed files with 350 additions and 198 deletions.
46 changes: 41 additions & 5 deletions src/frontend/src/App.vue
Original file line number Diff line number Diff line change
@@ -1,22 +1,49 @@
<template>
<NavBar class="fixed-top" style="z-index: 999;" />
<main :class="isMobile ? 'q-ma-md' : 'q-ma-xl'" style="margin-top: 75px;">
<q-layout view="hHh lpR fFf">

<q-header>
<NavBar />
</q-header>

<q-drawer v-model="store.showRightDrawer" side="right" bordered :width="240">
<SnapshotList />
</q-drawer>

<q-page-container>
<div
:class="isMobile ? 'q-ma-md' : 'q-ma-xl'"
:style="{ 'margin-top': isMobile ? '10px' : '25px', height: '100' }"
>
<router-view />
</div>
</q-page-container>

</q-layout>

<!-- <main :class="isMobile ? 'q-ma-md' : 'q-ma-xl'" style="margin-top: 75px;">
<RouterView />
</main>
<!-- <AccessibilityTest /> -->
</main> -->
</template>

<script setup lang="ts">
import { RouterView, useRoute } from 'vue-router'
import NavBar from '@/components/NavBar.vue'
import AccessibilityTest from '@/components/AccessibilityTest.vue'
import { useQuasar } from 'quasar'
import { computed, provide } from 'vue'
import { computed, provide, watch } from 'vue'
import { useLoginStore } from '@/stores/LoginStore'
import SnapshotList from './components/SnapshotList.vue'
const store = useLoginStore()
const route = useRoute()
const $q = useQuasar()
const isExtraSmall = computed(() => {
return $q.screen.xs
})
const isMobile = computed(() => {
return $q.screen.sm || $q.screen.xs
})
Expand All @@ -27,5 +54,14 @@
provide('isMobile', isMobile)
provide('isMedium', isMedium)
provide('isExtraSmall', isExtraSmall)
watch(route, (to) => {
// on every route change, close snapshot drawer if open
if(store.showRightDrawer) {
store.showRightDrawer = false
store.selectedSnapshot = null
}
})
</script>
3 changes: 2 additions & 1 deletion src/frontend/src/assets/main.css
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ h1 {
font-size: clamp(1rem, 10vw, 4rem) !important;
line-height: 1 !important;
font-weight: 400 !important;
margin-top: 0;
}

h2 {
Expand All @@ -120,7 +121,7 @@ h2 {

.field-label {
width: 100px;
font-size: 1rem;
font-size: .6em;
color: black;
}

Expand Down
106 changes: 106 additions & 0 deletions src/frontend/src/components/SnapshotList.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
<template>
<TableComponent
:rows="snapshots"
:columns="columns"
v-model:selected="selected"
:title="props.maxHeight ? '' : 'Snapshots'"
:hideCreateBtn="true"
:hideDeleteBtn="true"
:hideEditBtn="true"
:hideSearch="true"
:disableRadio="true"
rowKey="snapshot"
:showAll="true"
:style="{
marginTop: '0',
maxHeight: props.maxHeight ? props.maxHeight + 'px' : '',
height: props.maxHeight ? '' : 'calc(100vh - 50px)'
}"
>
<template #body-cell-timestamp="props">
{{
new Intl.DateTimeFormat('en-US', {
year: '2-digit',
month: '2-digit',
day: '2-digit',
hour: 'numeric',
minute: 'numeric',
hour12: true
}).format(new Date(props.row.snapshotCreatedOn))
}}
<q-chip
v-if="props.row.latestSnapshot"
label="latest"
size="md"
dense
color="orange"
text-color="white"
/>
</template>
</TableComponent>
</template>

<script setup>
import { useLoginStore } from '@/stores/LoginStore'
import { useRoute } from 'vue-router'
import TableComponent from '@/components/TableComponent.vue'
import { ref, watch } from 'vue'
import * as api from '@/services/dataApi'
const props = defineProps(['showDialogHistory', 'type', 'id', 'maxHeight'])
const store = useLoginStore()
const route = useRoute()
const snapshots = ref([])
const selected = ref([])
async function getSnapshots() {
try {
const res = await api.getSnapshots(route.meta.type, route.params.id)
snapshots.value = res.data.data.reverse()
console.log('snapshots = ', snapshots.value)
} catch(err) {
console.warn(err)
}
}
async function getDialogSnapshots() {
try {
const res = await api.getSnapshots(props.type, props.id)
snapshots.value = res.data.data.reverse()
console.log('snapshots = ', snapshots.value)
} catch(err) {
console.warn(err)
}
}
watch(() => store.showRightDrawer, (history) => {
if(history) {
getSnapshots()
} else {
store.selectedSnapshot = null
}
})
watch(() => props.showDialogHistory, (history) => {
if(history) {
getDialogSnapshots()
} else {
store.selectedSnapshot = null
}
})
watch(selected, (newVal) => {
if(newVal.length > 0) {
store.selectedSnapshot = newVal[0]
}
})
const columns = [
{ name: 'timestamp', label: 'Created On', align: 'left', field: 'snapshotCreatedOn', sortable: true, },
]
</script>
87 changes: 65 additions & 22 deletions src/frontend/src/components/TableComponent.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,16 @@
:filter="filter"
selection="single"
v-model:selected="selected"
row-key="id"
:class="`q-mt-lg ${isMobile ? '' : '' }`"
:row-key="props.rowKey"
:class="'q-mt-lg'"
flat
bordered
dense
v-model:pagination="pagination"
@request="onRequest"
:rows-per-page-options="[5,10,15,20,25,50,0]"
:tabindex="props.disableSelect ? '' : '0'"
@keydown="keydown"
:rows-per-page-options="props.showAll ? [0] : [5,10,15,20,25,50,0]"
>
<template v-slot:header="props">
<q-tr :props="props">
Expand All @@ -29,11 +31,12 @@
:class="`${getSelectedColor(props.selected)} cursor-pointer` "
:props="props"
@click="handleClick(props)"
style="padding-left: 50px;"
>
<q-td v-for="col in props.cols" :key="col.name" :props="props">
<q-radio
v-model="radioSelected"
:val="props.row.id"
:val="props.row[props.rowKey || 'id']"
v-if="col.name === 'radio'"
@click="handleClick(props)"
/>
Expand Down Expand Up @@ -165,19 +168,25 @@
const isMobile = inject('isMobile')
const props = defineProps([
'columns',
'rows',
'title',
'showExpand',
'hideCreateBtn',
'hideEditBtn',
'hideDeleteBtn',
'showToggleDraft',
'hideSearch',
'disableSelect',
'rightCaption'
])
const props = defineProps({
columns: Array,
rows: Array,
title: String,
showExpand: Boolean,
hideCreateBtn: Boolean,
hideEditBtn: Boolean,
hideDeleteBtn: Boolean,
showToggleDraft: Boolean,
hideSearch: Boolean,
disableSelect: Boolean,
rightCaption: String,
showAll: Boolean,
disableRadio: Boolean,
rowKey: {
type: String,
default: 'id'
},
})
const emit = defineEmits([
'edit',
'delete',
Expand All @@ -189,7 +198,7 @@
const finalColumns = computed(() => {
let defaultColumns = [ ...props.columns ]
if(!props.disableSelect) {
if(!props.disableSelect && !props.disableRadio) {
defaultColumns.unshift({ name: 'radio', align: 'center', sortable: false, label: 'Select', headerStyle: 'width: 100px' })
}
if(props.showExpand) {
Expand Down Expand Up @@ -221,26 +230,36 @@
function handleClick(tableProps) {
if(props.disableSelect) return
tableProps.selected = !tableProps.selected
radioSelected.value = tableProps.row.id
// tableProps.selected = !tableProps.selected
tableProps.selected = true
radioSelected.value = tableProps.row[props.rowKey]
}
watch(selected, (newVal) => {
if(newVal.length === 0) radioSelected.value = ''
})
watch(() => props.rows, (newVal) => {
// when viewing history, auto select first (latest) row
if (newVal.length > 0 && props.rowKey === 'snapshot') {
if (tableRef.value && newVal[0]) {
selected.value = [newVal[0]]
}
}
}, { deep: true })
watch(showDrafts, (newVal, oldVal) => {
if(newVal !== oldVal) selected.value = []
})
function getSelectedColor(selected) {
if(darkMode.value && selected) return 'bg-deep-purple-10'
else if(selected) return 'bg-blue-grey-1'
// else if(selected) return 'bg-blue-grey-1'
}
const pagination = ref({
page: 1,
rowsPerPage: 15,
rowsPerPage: props.showAll ? 0 : 15,
sortBy: '',
descending: false,
})
Expand Down Expand Up @@ -281,4 +300,28 @@
return new Date(dateString).toLocaleString('en-US', options)
}
function keydown(event) {
// Ensure there are rows to navigate
if (!props.rows || props.rows.length === 0) return;
// Get the current index of the selected row
const currentIndex = props.rows.findIndex(row => row[props.rowKey] === selected.value[0]?.[props.rowKey])
if (event.key === 'ArrowUp') {
// Navigate to the previous row (if not at the first row)
if (currentIndex > 0) {
const prevRow = props.rows[currentIndex - 1]
selected.value = [prevRow]
radioSelected.value = prevRow[props.rowKey]
}
} else if (event.key === 'ArrowDown') {
// Navigate to the next row (if not at the last row)
if (currentIndex < props.rows.length - 1) {
const nextRow = props.rows[currentIndex + 1]
selected.value = [nextRow]
radioSelected.value = nextRow[props.rowKey]
}
}
}
</script>
6 changes: 4 additions & 2 deletions src/frontend/src/dialogs/DialogComponent.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<template>
<q-dialog v-model="showDialog" aria-labelledby="modalTitle" :persistent="persistent">
<q-card flat style="min-width: 500px; max-width: 80%;">
<q-card flat :style="{ 'min-width': isMedium ? '50%' : '30%' }">
<q-form @submit="$emit('emitSubmit')">
<q-card-section class="bg-primary text-white q-mb-md">
<div class="text-h6 row justify-between">
Expand Down Expand Up @@ -37,11 +37,13 @@
</template>

<script setup>
import { ref } from 'vue'
import { inject } from 'vue'
const showDialog = defineModel('showDialog')
defineEmits(['emitSubmit', 'emitCancel', 'emitSaveDraft'])
const props = defineProps(['hideDraftBtn', 'persistent', 'showHistoryToggle', 'disableConfirm'])
const history = defineModel('history')
const isMedium = inject('isMedium')
</script>
4 changes: 3 additions & 1 deletion src/frontend/src/dialogs/LeaveFormDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
@emitSubmit="$emit('leaveForm')"
:hideDraftBtn="true"
>
<template #title>Leave <span class="text-capitalize">{{ type }}</span> Form?</template>
<template #title>
<span class="text-capitalize">Leave {{ type }} Form?</span>
</template>
<q-card-section class="q-pt-none">
You are about to leave the {{ type }} form and have unsaved changes. <br>
All changes will be lost. Continue?
Expand Down
Loading

0 comments on commit 693317a

Please sign in to comment.