Skip to content

Commit

Permalink
Merge pull request #221 from lukashornych/dev
Browse files Browse the repository at this point in the history
release: offset date time picker for catalog backups
  • Loading branch information
lukashornych authored Oct 29, 2024
2 parents 4a157ae + 58fc36b commit 674cd11
Show file tree
Hide file tree
Showing 11 changed files with 447 additions and 47 deletions.
56 changes: 29 additions & 27 deletions src/modules/backup-viewer/components/BackupCatalogDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ import { computed, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import VFormDialog from '@/modules/base/component/VFormDialog.vue'
import { DateTime } from 'luxon'
import { Timestamp } from '@bufbuild/protobuf'
import { BackupViewerService, useBackupViewerService } from '@/modules/backup-viewer/service/BackupViewerService'
import { Connection } from '@/modules/connection/model/Connection'
import { Toaster, useToaster } from '@/modules/notification/service/Toaster'
import { CatalogVersionAtResponse } from '@/modules/connection/model/CatalogVersionAtResponse'
import Immutable from 'immutable'
import { Catalog } from '@/modules/connection/model/Catalog'
import VDateTimeInput from '@/modules/base/component/VDateTimeInput.vue'
const backupViewerService: BackupViewerService = useBackupViewerService()
const toaster: Toaster = useToaster()
Expand All @@ -36,20 +36,24 @@ watch(
const availableCatalogs = ref<string[]>([])
const availableCatalogsLoaded = ref<boolean>(false)
const minimalDate = ref<string | undefined>()
const minimalDateLoaded = ref<boolean>(false)
const minDate = ref<DateTime | undefined>()
const minDateLoaded = ref<boolean>(false)
const maxDate = ref<DateTime | undefined>()
const maxDateLoaded = ref<boolean>(false)
const defaultTimeOffset = ref<string>('+00:00')
const defaultTimeOffsetLoaded = ref<boolean>(false)
const catalogName = ref<string | undefined>(undefined)
watch(catalogName, async () => {
minimalDateLoaded.value = false
minDateLoaded.value = false
pastMoment.value = undefined
if (catalogName.value != undefined && catalogName.value.trim().length > 0) {
await loadMinimalDate()
} else {
minimalDate.value = undefined
minDate.value = undefined
}
})
const pastMoment = ref<string>()
const pastMoment = ref<DateTime | undefined>()
const includeWal = ref<boolean>(false)
const changed = computed<boolean>(() =>
Expand Down Expand Up @@ -89,8 +93,15 @@ async function loadMinimalDate(): Promise<void> {
props.connection,
catalogName.value!
)
minimalDate.value = minimalBackupDate.introducedAt.toString()
minimalDateLoaded.value = true
minDate.value = minimalBackupDate.introducedAt.toDateTime()
minDateLoaded.value = true
maxDate.value = DateTime.now()
maxDateLoaded.value = true
defaultTimeOffset.value = minimalBackupDate.introducedAt.offset
defaultTimeOffsetLoaded.value = true
} catch (e: any) {
toaster.error(t(
'backupViewer.backup.notification.couldNotLoadMinimalDate',
Expand All @@ -100,30 +111,20 @@ async function loadMinimalDate(): Promise<void> {
}
function reset(): void {
catalogName.value = ''
pastMoment.value = ''
catalogName.value = undefined
pastMoment.value = undefined
includeWal.value = false
}
function convertPastMoment(): OffsetDateTime | undefined {
if (pastMoment.value === undefined) {
return undefined
}
// todo lho simplify
// todo lho verify date data type
const jsDate = new Date(pastMoment.value!)
const offsetDateTime: DateTime = DateTime.fromJSDate(jsDate)
const timestamp: Timestamp = Timestamp.fromDate(jsDate)
return new OffsetDateTime(timestamp, offsetDateTime.toFormat('ZZ'))
}
async function backup(): Promise<boolean> {
try {
await backupViewerService.backupCatalog(
props.connection,
catalogName.value!,
includeWal.value,
convertPastMoment()
pastMoment.value != undefined
? OffsetDateTime.fromDateTime(pastMoment.value)
: undefined
)
toaster.success(t(
'backupViewer.backup.notification.backupRequested',
Expand Down Expand Up @@ -174,12 +175,13 @@ async function backup(): Promise<boolean> {
:disabled="!availableCatalogsLoaded"
required
/>
<VDateInput
<VDateTimeInput
v-model="pastMoment"
:label="t('backupViewer.backup.form.pastMoment.label')"
:disabled="!minimalDateLoaded"
:min="minimalDate"
:max="new Date().toISOString()"
:disabled="!minDateLoaded || !maxDateLoaded || !defaultTimeOffsetLoaded"
:min="minDate"
:max="maxDate"
:default-time-offset="defaultTimeOffset"
/>
<VCheckbox
v-model="includeWal"
Expand Down
275 changes: 275 additions & 0 deletions src/modules/base/component/VDateTimeInput.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,275 @@
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
import { computed, ref, watch } from 'vue'
import { DateTime } from 'luxon'
import VTimeOffsetPicker from '@/modules/base/component/VTimeOffsetPicker.vue'
import { Toaster, useToaster } from '@/modules/notification/service/Toaster'
enum Step {
Date = 0,
Time = 1,
TimeOffset = 2
}
const toaster: Toaster = useToaster()
const { t } = useI18n()
const props = withDefaults(
defineProps<{
modelValue?: DateTime,
label?: string,
disabled?: boolean,
defaultTimeOffset?: string,
min?: DateTime,
max?: DateTime
}>(),
{
modelValue: undefined,
label: undefined,
disabled: false,
defaultTimeOffset: '+00:00'
}
)
const emit = defineEmits<{
(e: 'update:modelValue', value: DateTime): void
}>()
const showMenu = ref<boolean>(false)
watch(showMenu, (newValue) => {
if (!newValue) {
currentStep.value = Step.Date
}
})
const currentStep = ref<Step>(Step.Date)
const canGoNextStep = computed<boolean>(() => {
switch (currentStep.value) {
case Step.Date: return date.value != undefined
case Step.Time: return time.value != undefined && time.value.length > 0
default: return false
}
})
function goToPreviousStep(): void {
if (currentStep.value > Step.Date) {
currentStep.value--
}
}
function goToNextStep(): void {
if (currentStep.value < Step.TimeOffset) {
currentStep.value++
}
}
const timeOffset = ref<string>(props.defaultTimeOffset)
watch(
() => props.defaultTimeOffset,
() => timeOffset.value = props.defaultTimeOffset,
{ immediate: true }
)
const date = ref<Date>()
const isoDate = computed<string | undefined>(() => {
if (date.value == undefined) {
return undefined
}
return `${date.value.getFullYear()}-${String(date.value.getMonth() + 1).padStart(2, '0')}-${String(date.value.getDate()).padStart(2, '0')}`
})
watch(date, (newValue) => {
if (newValue != undefined) {
currentStep.value = Step.Time
}
})
const minDate = computed<string | undefined>(() => {
if (props.min == undefined) {
return undefined
}
return props.min
.setZone(timeOffset.value) // we need the date in picker's offset, not in the inputted one
.toISODate()!
})
const maxDate = computed<string | undefined>(() => {
if (props.max == undefined) {
return undefined
}
return props.max
.setZone(timeOffset.value) // we need the date in picker's offset, not in the inputted one
.toISODate()!
})
const time = ref<string>('')
watch(time, (newValue) => {
if (newValue != undefined && newValue.length > 0) {
currentStep.value = Step.TimeOffset
}
})
const minTime = computed<string | undefined>(() => {
if (isoDate.value == undefined) {
return undefined
}
if (props.min == undefined) {
return undefined
}
if (isoDate.value !== minDate.value) {
return undefined
}
return props.min
.setZone(timeOffset.value)
.toISOTime({
suppressMilliseconds: true,
includeOffset: false
})!
})
const maxTime = computed<string | undefined>(() => {
if (isoDate.value == undefined) {
return undefined
}
if (props.max == undefined) {
return undefined
}
if (isoDate.value !== maxDate.value) {
return undefined
}
return props.max
.setZone(timeOffset.value)
.toISOTime({
suppressMilliseconds: true,
includeOffset: false
})!
})
const computedOffsetDateTime = computed<DateTime | undefined>(() => {
if (isoDate.value == undefined) {
return undefined
}
if (time.value == undefined || time.value.length === 0) {
return undefined
}
if (timeOffset.value == undefined || timeOffset.value.length === 0) {
return undefined
}
const datePart: string = isoDate.value
const timePart: string = time.value
const timeOffsetPart: string = timeOffset.value
const rawOffsetDateTime: string = `${datePart}T${timePart}${timeOffsetPart}`
return DateTime.fromISO(rawOffsetDateTime)
.setZone(timeOffset.value) // a little bit of hack to not use default device locale
})
const displayedOffsetDateTime = ref<string>('')
function confirm(): void {
if (computedOffsetDateTime.value == undefined) {
throw new Error('Missing offset date time.')
}
const offsetDateTime: DateTime = computedOffsetDateTime.value
if (props.min != undefined && offsetDateTime < props.min) {
toaster.error(t('common.input.dateTime.error.olderThanMin'))
currentStep.value = Step.Date
return
}
if (props.max != undefined && offsetDateTime > props.max) {
toaster.error(t('common.input.dateTime.error.newerThanMax'))
currentStep.value = Step.Date
return
}
displayedOffsetDateTime.value = computedOffsetDateTime.value
.toLocaleString(DateTime.DATETIME_FULL)
showMenu.value = false
emit('update:modelValue', offsetDateTime)
}
</script>

<template>
<VTextField
:model-value="displayedOffsetDateTime"
:active="showMenu"
:focus="showMenu"
:label="label"
:disabled="disabled"
readonly
>
<VMenu
v-model="showMenu"
:close-on-content-click="false"
activator="parent"
min-width="0"
>
<VSheet v-if="showMenu" elevation="6">
<VWindow v-if="showMenu" v-model="currentStep">
<VWindowItem>
<VDatePicker
v-model="date"
:min="minDate"
:max="maxDate"
hide-weekdays
/>
</VWindowItem>
<VWindowItem>
<VTimePicker
v-model="time"
:min="minTime"
:max="maxTime"
format="24hr"
use-seconds
/>
</VWindowItem>
<VWindowItem>
<VTimeOffsetPicker v-model="timeOffset" />
</VWindowItem>
</VWindow>

<div v-if="currentStep < Step.TimeOffset" class="time-offset-info text-disabled">
{{ t('common.input.dateTime.help.timeOffset', { offset: timeOffset }) }}
</div>

<footer class="actions">
<VBtn
v-if="currentStep > Step.Date"
variant="tonal"
prepend-icon="mdi-chevron-left"
@click="goToPreviousStep"
>
{{ t('common.button.previous') }}
</VBtn>

<VSpacer />

<VBtn
v-if="currentStep < Step.TimeOffset"
prepend-icon="mdi-chevron-right"
:disabled="!canGoNextStep"
@click="goToNextStep"
>
{{ t('common.button.next') }}
</VBtn>
<VBtn
v-else-if="currentStep === Step.TimeOffset"
prepend-icon="mdi-check"
@click="confirm"
>
{{ t('common.button.confirm') }}
</VBtn>
</footer>
</VSheet>
</VMenu>
</VTextField>
</template>

<style lang="scss" scoped>
.time-offset-info {
text-align: center;
margin: 0 1rem 1rem;
}
.actions {
display: flex;
padding: 0 1rem 1rem;
gap: 0.5rem;
}
</style>
Loading

0 comments on commit 674cd11

Please sign in to comment.