Skip to content

Commit

Permalink
feat(timed-events): add administration interface for new timed events (
Browse files Browse the repository at this point in the history
…#19)

* feat: add view for managing all timed events in Aurora

* feat: improve event labels
  • Loading branch information
Yoronex authored Jan 5, 2025
1 parent f757443 commit 856d4e4
Show file tree
Hide file tree
Showing 18 changed files with 739 additions and 12 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"@hey-api/client-fetch": "^0.4.2",
"@primevue/themes": "^4.2.1",
"@vee-validate/yup": "^4.14.6",
"cronstrue": "^2.52.0",
"dotenv": "^16.4.5",
"pinia": "^2.2.5",
"primeicons": "^7.0.0",
Expand Down
34 changes: 34 additions & 0 deletions src/components/timed-events/TimedEventDeleteButton.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<template>
<Button icon="pi pi-times" severity="danger" @click="confirmRef?.confirmDialog" />
<ConfirmWrapper
ref="confirmRef"
:loading="loading"
message="Are you sure you want to delete this timed event?"
:on-accept="
() => {
loading = true;
store.deleteTimedEvent(props.timedEvent.id).finally(() => {
loading = false;
});
}
"
/>
</template>

<script setup lang="ts">
import { ref } from 'vue';
import type { TimedEventResponse } from '@/api';
import { useTimedEventsStore } from '@/stores/timed-events.store';
import ConfirmWrapper from '@/components/prime/ConfirmWrapper.vue';
const store = useTimedEventsStore();
const props = defineProps<{
timedEvent: TimedEventResponse;
}>();
const confirmRef = ref();
const loading = ref<boolean>(false);
</script>

<style scoped></style>
165 changes: 165 additions & 0 deletions src/components/timed-events/TimedEventDialog.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
<template>
<Dialog
closable
close-on-escape
dismissable-mask
header="Create new timed event"
:keep-in-view-port="false"
modal
:visible="visible"
@update:visible="(v) => $emit('update:visible', v)"
>
<div class="flex flex-col gap-4">
<div class="flex flex-col gap-2">
<label for="cronExpression">Cron expression</label>
<InputText
id="cronExpression"
v-model="cronExpression"
:invalid="cronInvalid"
placeholder="39 5 * * *"
type="text"
/>
<Message v-if="cronInvalid" severity="error" size="small" variant="simple"
>Please type a valid cron expression</Message
>
<Message v-else severity="secondary" size="small" variant="simple">{{
cronstrue.toString(cronExpression)
}}</Message>
</div>
<div class="flex flex-col gap-2">
<label for="event-type">Event type</label>
<Select
id="event-type"
v-model="selectedType"
option-label="label"
option-value="name"
:options="possibleTypes"
placeholder="Choose a type"
/>
</div>
<div v-if="originalTimedEvent" class="flex flex-col gap-2">
<label for="skip-next">Skip next?</label>
<ToggleSwitch v-model="skipNext" />
</div>
<TimedEventParamsHandlerAudio
v-if="selectedType === 'switch-handler-audio'"
:cron-expression="cronExpression"
:cron-valid="!cronInvalid"
:on-save="onSaveWrapper"
:original-event-spec-params="
originalTimedEvent?.eventSpec.type === 'switch-handler-audio'
? originalTimedEvent?.eventSpec.params
: undefined
"
:skip-next="skipNext"
/>
<TimedEventParamsHandlerLights
v-else-if="selectedType === 'switch-handler-lights'"
:cron-expression="cronExpression"
:cron-valid="!cronInvalid"
:on-save="onSaveWrapper"
:original-event-spec-params="
originalTimedEvent?.eventSpec.type === 'switch-handler-lights'
? originalTimedEvent?.eventSpec.params
: undefined
"
:skip-next="skipNext"
/>
<TimedEventParamsHandlerScreen
v-else-if="selectedType === 'switch-handler-screen'"
:cron-expression="cronExpression"
:cron-valid="!cronInvalid"
:on-save="onSaveWrapper"
:original-event-spec-params="
originalTimedEvent?.eventSpec.type === 'switch-handler-screen'
? originalTimedEvent?.eventSpec.params
: undefined
"
:skip-next="skipNext"
/>
<TimedEventDialogSaveButton
v-else
:disabled="loading || cronInvalid || selectedType === ''"
:loading="loading"
@click="saveEvent"
>
Save changes
</TimedEventDialogSaveButton>
</div>
</Dialog>
</template>

<script setup lang="ts">
import { computed, type ComputedRef, type Ref, ref } from 'vue';
import cronstrue from 'cronstrue';
import type { CreateTimedEventRequest, EventSpec, TimedEventResponse } from '@/api';
import TimedEventParamsHandlerScreen from '@/components/timed-events/types/TimedEventParamsHandlerScreen.vue';
import TimedEventParamsHandlerAudio from '@/components/timed-events/types/TimedEventParamsHandlerAudio.vue';
import TimedEventParamsHandlerLights from '@/components/timed-events/types/TimedEventParamsHandlerLights.vue';
import TimedEventDialogSaveButton from '@/components/timed-events/types/TimedEventDialogSaveButton.vue';
const props = defineProps<{
originalTimedEvent?: TimedEventResponse;
visible: boolean;
onSave: (params: CreateTimedEventRequest, skipNext?: boolean) => Promise<void>;
}>();
const emit = defineEmits<{
'update:visible': [visible: boolean];
}>();
const loading = ref<boolean>(false);
const cronExpression = ref<string>(props.originalTimedEvent?.cronExpression ?? '');
const cronInvalid: ComputedRef<boolean> = computed(() => {
try {
cronstrue.toString(cronExpression.value);
return false;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (error: unknown) {
return true;
}
});
const selectedType = ref<EventSpec['type'] | ''>(props.originalTimedEvent?.eventSpec.type ?? '');
const possibleTypes: Ref<{ name: EventSpec['type']; label: string }[]> = ref([
{ name: 'system-reset', label: 'System reset' },
{ name: 'clean-audit-logs', label: 'Clean audit logs' },
{ name: 'switch-handler-audio', label: 'Switch handler audio' },
{ name: 'switch-handler-lights', label: 'Switch handler lights group' },
{ name: 'switch-handler-screen', label: 'Switch handler screen' },
] as { name: EventSpec['type']; label: string }[]);
const skipNext = ref<boolean>(false);
const onSaveWrapper = async (params: CreateTimedEventRequest, skipNext?: boolean) => {
await props.onSave(params, skipNext);
emit('update:visible', false as never);
};
const saveEvent = () => {
if (selectedType.value === '') return;
loading.value = true;
switch (selectedType.value) {
case 'system-reset':
onSaveWrapper({ cronExpression: cronExpression.value, eventSpec: { type: 'system-reset' } }, skipNext.value).then(
() => {
loading.value = false;
},
);
return;
case 'clean-audit-logs':
onSaveWrapper(
{ cronExpression: cronExpression.value, eventSpec: { type: 'clean-audit-logs' } },
skipNext.value,
).then(() => {
loading.value = false;
});
return;
}
};
</script>

<style scoped></style>
21 changes: 21 additions & 0 deletions src/components/timed-events/TimedEventDialogCreate.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<template>
<Button icon="pi pi-plus" label="Create new event" severity="success" @click="open = true" />
<TimedEventDialog v-model:visible="open" :on-save="onSave" />
</template>

<script setup lang="ts">
import { ref } from 'vue';
import TimedEventDialog from '@/components/timed-events/TimedEventDialog.vue';
import type { CreateTimedEventRequest } from '@/api';
import { useTimedEventsStore } from '@/stores/timed-events.store';
const open = ref<boolean>(false);
const store = useTimedEventsStore();
const onSave = async (params: CreateTimedEventRequest) => {
return store.createTimedEvent(params);
};
</script>

<style scoped></style>
25 changes: 25 additions & 0 deletions src/components/timed-events/TimedEventDialogUpdate.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<template>
<Button icon="pi pi-pen-to-square" severity="secondary" @click="open = true" />
<TimedEventDialog v-model:visible="open" :on-save="onSave" :original-timed-event="timedEvent" />
</template>

<script setup lang="ts">
import { ref } from 'vue';
import TimedEventDialog from '@/components/timed-events/TimedEventDialog.vue';
import type { CreateTimedEventRequest, TimedEventResponse } from '@/api';
import { useTimedEventsStore } from '@/stores/timed-events.store';
const props = defineProps<{
timedEvent: TimedEventResponse;
}>();
const open = ref<boolean>(false);
const store = useTimedEventsStore();
const onSave = async (params: CreateTimedEventRequest, skipNext?: boolean) => {
if (!skipNext) throw new Error('"skipNext" is required.');
return store.updateTimedEvent(props.timedEvent.id, { ...params, skipNext });
};
</script>
<style scoped></style>
72 changes: 72 additions & 0 deletions src/components/timed-events/TimedEventSpec.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<template>
<TimedEventTag
v-if="props.eventSpec.type === 'system-reset'"
description="Reset system to initial state."
label="System reset"
/>
<TimedEventTag
v-else-if="props.eventSpec.type === 'clean-audit-logs'"
description="Remove all expired audit logs from the database."
label="Clean audit logs"
/>
<TimedEventTag
v-else-if="props.eventSpec.type === 'switch-handler-audio' && props.eventSpec.params.handler !== ''"
description=""
:label="`Switch handler for audio '${getAudioName(props.eventSpec.params.id)}' to ${props.eventSpec.params.handler}`"
/>
<TimedEventTag
v-else-if="props.eventSpec.type === 'switch-handler-audio'"
description=""
:label="`Remove handler for audio '${getAudioName(props.eventSpec.params.id)}'`"
/>
<TimedEventTag
v-else-if="props.eventSpec.type === 'switch-handler-lights' && props.eventSpec.params.handler !== ''"
description=""
:label="`Switch handler for lights group '${getLightsGroupName(props.eventSpec.params.id)}' to ${props.eventSpec.params.handler}`"
/>
<TimedEventTag
v-else-if="props.eventSpec.type === 'switch-handler-lights'"
description=""
:label="`Remove handler for lights group '${getLightsGroupName(props.eventSpec.params.id)}'`"
/>
<TimedEventTag
v-else-if="props.eventSpec.type === 'switch-handler-screen' && props.eventSpec.params.handler !== ''"
description=""
:label="`Switch handler for screen '${getScreenName(props.eventSpec.params.id)}' to ${props.eventSpec.params.handler}`"
/>
<TimedEventTag
v-else-if="props.eventSpec.type === 'switch-handler-screen'"
description=""
:label="`Remove handler for screen '${getScreenName(props.eventSpec.params.id)}'`"
/>
</template>

<script setup lang="ts">
import type { EventSpec } from '@/api';
import TimedEventTag from '@/components/timed-events/TimedEventTag.vue';
import { useSubscriberStore } from '@/stores/subscriber.store';
const subscriberStore = useSubscriberStore();
const getAudioName = (id: number) => {
const match = subscriberStore.audios.find((audio) => audio.id === id);
if (match) return match.name;
return id.toString();
};
const getLightsGroupName = (id: number) => {
const match = subscriberStore.lightsGroups.find((lightsGroup) => lightsGroup.id === id);
if (match) return match.name;
return id.toString();
};
const getScreenName = (id: number) => {
const match = subscriberStore.screens.find((screen) => screen.id === id);
if (match) return match.name;
return id.toString();
};
const props = defineProps<{
eventSpec: EventSpec;
}>();
</script>

<style scoped></style>
12 changes: 12 additions & 0 deletions src/components/timed-events/TimedEventTag.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<template>
<Tag v-tooltip.bottom="{ value: description }" severity="secondary">{{ props.label }}</Tag>
</template>

<script setup lang="ts">
const props = defineProps<{
label: string;
description: string;
}>();
</script>

<style scoped></style>
18 changes: 18 additions & 0 deletions src/components/timed-events/types/TimedEventDialogSaveButton.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<template>
<div>
<Button :disabled="disabled" :loading="loading" severity="success" @click="$emit('click')"> Save changes </Button>
</div>
</template>

<script setup lang="ts">
defineProps<{
disabled: boolean;
loading: boolean;
}>();
defineEmits<{
click: [];
}>();
</script>

<style scoped></style>
Loading

0 comments on commit 856d4e4

Please sign in to comment.