implementing snapshots

This commit is contained in:
Emily
2024-07-31 15:34:35 +02:00
parent 4bede171fa
commit 7cb10f5aa1
9 changed files with 196 additions and 31 deletions

View File

@@ -17,7 +17,7 @@ const { showDialog, closeDialog, dialogComponent, dialogParams, dialogStyle, dia
<div class="fixed top-4 right-8 z-[999] flex flex-col gap-2" v-if="alerts.length > 0">
<div v-for="alert of alerts"
class="w-[30vw] min-w-[20rem] relative bg-[#151515] border-solid border-[2px] border-[#262626] rounded-lg p-6 drop-shadow-lg">
class="w-[30vw] min-w-[20rem] relative bg-[#151515] overflow-hidden border-solid border-[2px] border-[#262626] rounded-lg p-6 drop-shadow-lg">
<div class="flex items-start gap-4">
<div> <i :class="alert.icon"></i> </div>
<div class="grow">
@@ -30,7 +30,8 @@ const { showDialog, closeDialog, dialogComponent, dialogParams, dialogStyle, dia
<i @click="closeAlert(alert.id)" class="fas fa-close hover:text-[#CCCCCC] cursor-pointer"></i>
</div>
</div>
<div :style="`width: ${Math.floor(100 / alert.ms * alert.remaining)}%; ${alert.transitionStyle}`"
class="absolute bottom-0 left-0 h-1 bg-lyx-primary z-100 alert-bar"></div>
</div>
</div>
@@ -62,3 +63,4 @@ const { showDialog, closeDialog, dialogComponent, dialogParams, dialogStyle, dia
</div>
</template>

View File

@@ -1,4 +1,5 @@
<script lang="ts" setup>
import CreateSnapshot from './dialog/CreateSnapshot.vue';
export type Entry = {
@@ -29,7 +30,7 @@ const debugMode = process.dev;
const { isOpen, close } = useMenu();
const { snapshots, snapshot } = useSnapshot();
const { snapshots, snapshot, updateSnapshots } = useSnapshot();
const snapshotsItems = computed(() => {
if (!snapshots.value) return []
@@ -41,12 +42,28 @@ const { openDialogEx } = useCustomDialog();
function openSnapshotDialog() {
openDialogEx(CreateSnapshot, {
width: "20rem",
width: "24rem",
height: "16rem",
closable: false
});
}
const { createAlert } = useAlert()
async function deleteSnapshot(close: () => any) {
await $fetch("/api/snapshot/delete", {
method: 'DELETE',
...signHeaders({ 'Content-Type': 'application/json' }),
body: JSON.stringify({
id: snapshot.value._id.toString(),
})
});
await updateSnapshots();
snapshot.value = snapshots.value[1];
createAlert('Snapshot deleted','Snapshot deleted successfully', 'far fa-circle-check', 5000);
close();
}
</script>
<template>
@@ -102,11 +119,31 @@ function openSnapshotDialog() {
<div v-if="snapshot" class="flex flex-col text-[.8rem] mt-2 px-2">
<div class="flex">
<div class="grow poppins"> From:</div>
<div class="poppins"> {{ new Date(snapshot.from).toLocaleString('it-IT') }} </div>
<div class="poppins"> {{ new Date(snapshot.from).toLocaleString('it-IT').split(',')[0].trim() }} </div>
</div>
<div class="flex">
<div class="grow poppins"> To:</div>
<div class="poppins"> {{ new Date(snapshot.to).toLocaleString('it-IT') }}</div>
<div class="poppins"> {{ new Date(snapshot.to).toLocaleString('it-IT').split(',')[0].trim() }}</div>
</div>
<div class="mt-4" v-if="snapshot._id.toString().startsWith('default') === false">
<UPopover placement="bottom">
<LyxUiButton type="danger" class="w-full">
Delete current snapshot
</LyxUiButton>
<template #panel="{ close }">
<div class="p-4 bg-lyx-widget">
<div class="poppins text-center font-medium">
Are you sure?
</div>
<div class="flex gap-2 mt-4">
<LyxUiButton @click="close()" type="secondary"> Cancel </LyxUiButton>
<LyxUiButton type="danger" @click="deleteSnapshot(close)"> Delete </LyxUiButton>
</div>
</div>
</template>
</UPopover>
</div>
</div>
</div>

View File

@@ -12,7 +12,7 @@ const ranges = [
{ label: 'Last 6 months', duration: { months: 6 } },
{ label: 'Last year', duration: { years: 1 } }
]
const selected = ref({ start: sub(new Date(), { days: 14 }), end: new Date() })
const selected = ref<{ start: Date, end: Date }>({ start: sub(new Date(), { days: 14 }), end: new Date() })
function isRangeSelected(duration: Duration) {
return isSameDay(selected.value.start, sub(new Date(), duration)) && isSameDay(selected.value.end, new Date())
@@ -22,7 +22,7 @@ function selectRange(duration: Duration) {
selected.value = { start: sub(new Date(), duration), end: new Date() }
}
const currentColor = ref<string>("");
const currentColor = ref<string>("#5680F8");
const colorpicker = ref<HTMLInputElement | null>(null);
@@ -34,29 +34,55 @@ function onColorChange() {
currentColor.value = colorpicker.value?.value || '#000000';
}
const snapshotName = ref<string>("");
const { updateSnapshots } = useSnapshot();
const { createAlert } = useAlert()
async function confirmSnapshot() {
await $fetch("/api/snapshot/create", {
method: 'POST',
...signHeaders({ 'Content-Type': 'application/json' }),
body: JSON.stringify({
name: snapshotName.value,
color: currentColor.value,
from: selected.value.start.toISOString(),
to: selected.value.end.toISOString()
})
});
await updateSnapshots();
closeDialog();
createAlert('Snapshot created','Snapshot created successfully', 'far fa-circle-check', 5000);
}
</script>
<template>
<div class="w-full h-full flex flex-col">
<div class="w-full h-full flex flex-col">
<div class="poppins text-center">
Create a snapshot
</div>
<div class="mt-10 flex items-center gap-2">
<div :style="`background-color: ${currentColor};`" @click="showColorPicker" class="w-6 h-6 rounded-full aspect-[1/1] relative">
<div :style="`background-color: ${currentColor};`" @click="showColorPicker"
class="w-6 h-6 rounded-full aspect-[1/1] relative cursor-pointer">
<input @input="onColorChange" ref="colorpicker" class="relative w-0 h-0 z-[-100]" type="color">
</div>
<div class="grow">
<input placeholder="Snapshot name" class="px-4 py-2 w-full rounded-lg bg-lyx-widget-light" type="text">
<LyxUiInput placeholder="Snapshot name" v-model="snapshotName" class="px-4 py-1 w-full"></LyxUiInput>
</div>
</div>
<div class="mt-4 justify-center flex w-full">
<UPopover :popper="{ placement: 'bottom' }">
<UButton icon="i-heroicons-calendar-days-20-solid">
{{ selected.start.toLocaleDateString() }} - {{ selected.end.toLocaleDateString() }}
<UPopover class="w-full" :popper="{ placement: 'bottom' }">
<UButton class="w-full" color="primary" variant="solid">
<div class="flex items-center justify-center w-full gap-2">
<i class="i-heroicons-calendar-days-20-solid"></i>
{{ selected.start.toLocaleDateString() }} - {{ selected.end.toLocaleDateString() }}
</div>
</UButton>
<template #panel="{ close }">
<div class="flex items-center sm:divide-x divide-gray-200 dark:divide-gray-800">
@@ -76,13 +102,14 @@ function onColorChange() {
</div>
<div class="grow"></div>
<div class="flex items-center justify-around">
<div @click="closeDialog()">
<div class="flex items-center justify-around gap-4">
<LyxUiButton @click="closeDialog()" type="secondary" class="w-full text-center">
Cancel
</div>
<div @click="closeDialog()">
</LyxUiButton>
<LyxUiButton @click="confirmSnapshot()" type="primary" class="w-full text-center"
:disabled="snapshotName.length == 0">
Confirm
</div>
</LyxUiButton>
</div>
</div>

View File

@@ -2,7 +2,7 @@
export type ButtonType = 'primary' | 'secondary' | 'outline' | 'danger';
const props = defineProps<{ type: ButtonType, }>();
const props = defineProps<{ type: ButtonType, disabled?: boolean }>();
</script>
@@ -12,6 +12,7 @@ const props = defineProps<{ type: ButtonType, }>();
'bg-lyx-widget-lighter outline-lyx-widget-lighter hover:bg-lyx-widget-light': type === 'secondary',
'bg-lyx-transparent outline-lyx-widget-lighter hover:bg-lyx-widget-light': type === 'outline',
'bg-lyx-danger-dark outline-lyx-danger hover:bg-lyx-danger': type === 'danger',
'bg-lyx-widget-lighter outline-lyx-widget-lighter hover:bg-lyx-widget-lighter !cursor-not-allowed !text-lyx-text-darker': disabled === true
}">
<slot></slot>
</div>

View File

@@ -5,7 +5,9 @@ export type Alert = {
text: string,
icon: string,
ms: number,
id: number
id: number,
remaining: number,
transitionStyle: string
}
const alerts = ref<Alert[]>([]);
@@ -18,11 +20,18 @@ const idPool = {
}
function createAlert(title: string, text: string, icon: string, ms: number) {
const alert: Alert = { title, text, icon, ms, id: idPool.getId() }
const alert = reactive<Alert>({
title, text, icon, ms, id: idPool.getId(), remaining: ms,
transitionStyle: 'transition: all 250ms linear;'
});
alerts.value.push(alert);
setTimeout(() => {
closeAlert(alert.id);
}, ms)
const timeout = setInterval(() => {
alert.remaining -= 250;
if (alert.remaining <= 0) {
closeAlert(alert.id);
clearInterval(timeout);
}
}, 250)
}
function closeAlert(id: number) {

View File

@@ -5,6 +5,10 @@ const remoteSnapshots = useFetch<TProjectSnapshot[]>('/api/project/snapshots', {
immediate: false
});
const activeProject = useActiveProject();
watch(activeProject, () => {
remoteSnapshots.refresh();
});
const snapshots = computed(() => {
@@ -13,7 +17,7 @@ const snapshots = computed(() => {
const getDefaultSnapshots: () => TProjectSnapshot[] = () => [
{
project_id: activeProject.value?._id as any,
_id: 'deafult0' as any,
_id: 'default0' as any,
name: 'All',
from: new Date(activeProject.value?.created_at || 0),
to: new Date(Date.now()),
@@ -21,7 +25,7 @@ const snapshots = computed(() => {
},
{
project_id: activeProject.value?._id as any,
_id: 'deafult1' as any,
_id: 'default1' as any,
name: 'Last month',
from: new Date(Date.now() - 1000 * 60 * 60 * 24 * 30),
to: new Date(Date.now()),
@@ -29,7 +33,7 @@ const snapshots = computed(() => {
},
{
project_id: activeProject.value?._id as any,
_id: 'deafult2' as any,
_id: 'default2' as any,
name: 'Last week',
from: new Date(Date.now() - 1000 * 60 * 60 * 24 * 7),
to: new Date(Date.now()),
@@ -37,7 +41,7 @@ const snapshots = computed(() => {
},
{
project_id: activeProject.value?._id as any,
_id: 'deafult3' as any,
_id: 'default3' as any,
name: 'Last day',
from: new Date(Date.now() - 1000 * 60 * 60 * 24),
to: new Date(Date.now()),
@@ -51,7 +55,7 @@ const snapshots = computed(() => {
];
})
const snapshot = ref<TProjectSnapshot>(snapshots.value[0]);
const snapshot = ref<TProjectSnapshot>(snapshots.value[1]);
// watch(remoteSnapshots.data, () => {
// if (!remoteSnapshots.data.value) return;
@@ -64,9 +68,13 @@ const safeSnapshotDates = computed(() => {
return { from, to }
})
function updateSnapshots() {
remoteSnapshots.refresh();
}
export function useSnapshot() {
if (remoteSnapshots.status.value === 'idle') {
remoteSnapshots.execute();
}
return { snapshot, snapshots, safeSnapshotDates }
return { snapshot, snapshots, safeSnapshotDates, updateSnapshots }
}

View File

@@ -70,6 +70,9 @@ const selectLabels = [
// { label: 'Month', value: 'month' },
];
function testAlert() {
createAlert('test', 'test', 'fas fa-home', 40000);
}
</script>

View File

@@ -0,0 +1,43 @@
import { ProjectModel } from "@schema/ProjectSchema";
import { ProjectSnapshotModel } from "@schema/ProjectSnapshot";
import { UserSettingsModel } from "@schema/UserSettings";
export default defineEventHandler(async event => {
const body = await readBody(event);
const { name: newSnapshotName, from, to, color: snapshotColor } = body;
if (!newSnapshotName) return setResponseStatus(event, 400, 'SnapshotName too short');
if (newSnapshotName.length == 0) return setResponseStatus(event, 400, 'SnapshotName too short');
if (!from) return setResponseStatus(event, 400, 'from is required');
if (!to) return setResponseStatus(event, 400, 'to is required');
if (!snapshotColor) return setResponseStatus(event, 400, 'color is required');
const userData = getRequestUser(event);
if (!userData?.logged) return setResponseStatus(event, 400, 'NotLogged');
const userSettings = await UserSettingsModel.findOne({ user_id: userData.id }, { active_project_id: 1 });
if (!userSettings) return setResponseStatus(event, 500, 'Unkwnown error');
const currentProjectId = userSettings.active_project_id;
const project = await ProjectModel.findById(currentProjectId);
if (!project) return setResponseStatus(event, 400, 'Project not found');
const newSnapshot = await ProjectSnapshotModel.create({
name: newSnapshotName,
from: new Date(from),
to: new Date(to),
color: snapshotColor,
project_id: currentProjectId
});
return newSnapshot.id;
});

View File

@@ -0,0 +1,35 @@
import { ProjectModel } from "@schema/ProjectSchema";
import { ProjectSnapshotModel } from "@schema/ProjectSnapshot";
import { UserSettingsModel } from "@schema/UserSettings";
export default defineEventHandler(async event => {
const body = await readBody(event);
const { id: snapshotId } = body;
if (!snapshotId) return setResponseStatus(event, 400, 'id is required');
const userData = getRequestUser(event);
if (!userData?.logged) return setResponseStatus(event, 400, 'NotLogged');
const userSettings = await UserSettingsModel.findOne({ user_id: userData.id }, { active_project_id: 1 });
if (!userSettings) return setResponseStatus(event, 500, 'Unkwnown error');
const currentProjectId = userSettings.active_project_id;
const project = await ProjectModel.findById(currentProjectId);
if (!project) return setResponseStatus(event, 400, 'Project not found');
const deletation = await ProjectSnapshotModel.deleteOne({
project_id: currentProjectId,
_id: snapshotId
});
return { ok: deletation.acknowledged };
});