🔨 Add home assistant backup list

This commit is contained in:
SebClem 2023-02-16 16:12:45 +01:00
parent d47ae5d5cc
commit 8f51d5d837
Signed by: sebclem
GPG Key ID: 5A4308F6A359EA50
10 changed files with 350 additions and 20 deletions

View File

@ -7,7 +7,19 @@ homeAssistantRouter.get("/backups/", (req, res, next) => {
haOsService haOsService
.getBackups() .getBackups()
.then((value) => { .then((value) => {
res.json(value.body.data.backups); res.json(value.body.data.backups.sort((a, b)=> Date.parse(b.date) - Date.parse(a.date)));
})
.catch((reason) => {
res.status(500);
res.json(reason);
});
});
homeAssistantRouter.get("/backup/:slug", (req, res, next) => {
haOsService
.getBackupInfo(req.params.slug)
.then((value) => {
res.json(value.body.data);
}) })
.catch((reason) => { .catch((reason) => {
res.status(500); res.status(500);
@ -29,6 +41,6 @@ homeAssistantRouter.get("/addons", (req, res, next) => {
homeAssistantRouter.get("/folders", (req, res, next) => { homeAssistantRouter.get("/folders", (req, res, next) => {
res.json(haOsService.getFolderList()); res.json(haOsService.getFolderList());
}) });
export default homeAssistantRouter; export default homeAssistantRouter;

View File

@ -185,7 +185,7 @@ function delSnap(id: string) {
); );
} }
function checkSnap(id: string) { function getBackupInfo(id: string) {
const option: OptionsOfJSONResponseBody = { const option: OptionsOfJSONResponseBody = {
headers: { authorization: `Bearer ${token}` }, headers: { authorization: `Bearer ${token}` },
responseType: "json", responseType: "json",
@ -269,7 +269,7 @@ function clean(backups: BackupModel[]) {
return; return;
} }
backups.sort((a, b) => { backups.sort((a, b) => {
return a.date < b.date ? 1 : -1; return Date.parse(b.date) - Date.parse(a.date);
}); });
const toDel = backups.slice(limit); const toDel = backups.slice(limit);
for (const i of toDel) { for (const i of toDel) {
@ -511,4 +511,5 @@ export {
startAddons, startAddons,
clean, clean,
publish_state, publish_state,
getBackupInfo
}; };

View File

@ -51,7 +51,7 @@ export interface BackupModel {
slug: string; slug: string;
date: string; date: string;
name: string; name: string;
type: string; type: "full" | "partial";
protected: boolean; protected: boolean;
content: BackupContent; content: BackupContent;
compressed: boolean; compressed: boolean;
@ -65,7 +65,7 @@ export interface BackupContent {
export interface BackupDetailModel { export interface BackupDetailModel {
slug: string; slug: string;
type: string; type: "full" | "partial";
name: string; name: string;
date: string; date: string;
size: string; size: string;
@ -76,7 +76,7 @@ export interface BackupDetailModel {
name: string; name: string;
version: string; version: string;
size: number; size: number;
}; }[];
repositories: string[]; repositories: string[];
folders: string[]; folders: string[];
} }

View File

@ -7,7 +7,10 @@
<alert-manager></alert-manager> <alert-manager></alert-manager>
<v-main class="mx-12"> <v-main class="mx-12">
<v-row> <v-row>
<v-col cols="6" offset="6"> <v-col cols="6">
<ha-list></ha-list>
</v-col>
<v-col cols="6">
<cloud-list></cloud-list> <cloud-list></cloud-list>
</v-col> </v-col>
</v-row> </v-row>
@ -18,6 +21,7 @@
<script setup lang="ts"> <script setup lang="ts">
import AlertManager from "./components/AlertManager.vue"; import AlertManager from "./components/AlertManager.vue";
import CloudList from "./components/cloud/CloudList.vue"; import CloudList from "./components/cloud/CloudList.vue";
import HaList from "./components/homeAssistant/HaList.vue";
import MessageBar from "./components/MessageBar.vue"; import MessageBar from "./components/MessageBar.vue";
import NavbarComponent from "./components/NavbarComponent.vue"; import NavbarComponent from "./components/NavbarComponent.vue";
import BackupConfigMenu from "./components/settings/BackupConfigMenu.vue"; import BackupConfigMenu from "./components/settings/BackupConfigMenu.vue";

View File

@ -3,7 +3,7 @@
<v-dialog <v-dialog
v-model="dialog" v-model="dialog"
:width="width" :width="width"
:fullscreen="xs" :fullscreen="isFullScreen"
:persistent="loading" :persistent="loading"
> >
<v-card> <v-card>
@ -30,25 +30,17 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useMenuSize } from "@/composable/menuSize";
import { deleteWebdabBackup } from "@/services/webdavService"; import { deleteWebdabBackup } from "@/services/webdavService";
import type { WebdavBackup } from "@/types/webdav"; import type { WebdavBackup } from "@/types/webdav";
import { computed, ref } from "vue"; import { computed, ref } from "vue";
import { useDisplay } from "vuetify/dist/vuetify"; import { useDisplay } from "vuetify/dist/vuetify";
const { xs, mdAndDown } = useDisplay();
const dialog = ref(false); const dialog = ref(false);
const loading = ref(false); const loading = ref(false);
const item = ref<WebdavBackup | null>(null); const item = ref<WebdavBackup | null>(null);
const width = computed(() => { const { width, isFullScreen } = useMenuSize();
if (xs.value) {
return undefined;
} else if (mdAndDown.value) {
return "80%";
} else {
return "50%";
}
});
function confirm() { function confirm() {
loading.value = true; loading.value = true;

View File

@ -0,0 +1,61 @@
<template>
<div>
<v-card elevation="10" class="mt-10" border>
<v-card-title class="text-center"> Home Assistant </v-card-title>
<v-divider></v-divider>
<v-card-text>
<v-row>
<v-col>
<v-card>
<v-card-title
class="text-center text-white bg-light-blue-darken-4"
>Backups</v-card-title
>
<v-divider></v-divider>
<v-card-text class="pa-0">
<v-list class="pa-0">
<v-list-item
v-if="backups.length == 0"
class="text-center text-subtitle-2 text-disabled"
>No backup in Home Assistant</v-list-item
>
<ha-list-item
v-for="(item, index) in backups"
:key="item.slug"
:item="item"
:index="index"
>
</ha-list-item>
</v-list>
</v-card-text>
</v-card>
</v-col>
</v-row>
</v-card-text>
</v-card>
</div>
</template>
<script lang="ts" setup>
import type { BackupModel } from "@/types/homeAssistant";
import { ref, onBeforeUnmount } from "vue";
import { getBackups } from "@/services/homeAssistantService";
import HaListItem from "./HaListItem.vue";
const backups = ref<BackupModel[]>([]);
function refreshBackup() {
getBackups().then((value) => {
backups.value = value;
});
}
refreshBackup();
const interval = setInterval(refreshBackup, 2000);
onBeforeUnmount(() => {
clearInterval(interval);
});
// TODO Manage delete
</script>

View File

@ -0,0 +1,197 @@
<template>
<v-divider v-if="index != 0" color="grey-darken-3"></v-divider>
<v-list-item class="bg-grey-darken-3">
<v-list-item-title>{{ item.name }}</v-list-item-title>
<template v-slot:append>
<v-scroll-x-transition>
<div v-if="!detail">
<v-chip color="primary" variant="flat" size="small" class="mr-2">
{{ item.type == "partial" ? "P" : "F" }}
</v-chip>
<v-chip color="primary" variant="flat" size="small" class="mr-1">
{{
DateTime.fromISO(item.date).toLocaleString(DateTime.DATETIME_MED)
}}
</v-chip>
</div>
</v-scroll-x-transition>
<v-btn variant="text" icon color="success" @click="detail = !detail">
<v-icon>{{ detail ? "mdi-chevron-up" : "mdi-information" }}</v-icon>
</v-btn>
</template>
</v-list-item>
<v-expand-transition>
<v-card v-show="detail" variant="tonal" color="secondary" rounded="0">
<v-card-text>
<v-row v-if="!detailData">
<v-col class="text-center">
<v-progress-circular indeterminate></v-progress-circular>
</v-col>
</v-row>
<template v-if="detailData">
<v-row>
<v-col class="d-flex justify-center">
<v-tooltip text="Creation" location="top">
<template v-slot:activator="{ props }">
<v-chip
color="primary"
variant="flat"
class="mr-2"
label
v-bind="props"
>
<v-icon start icon="mdi-folder-plus"></v-icon>
{{
DateTime.fromISO(detailData.date).toLocaleString(
DateTime.DATETIME_MED
)
}}
</v-chip>
</template>
</v-tooltip>
<v-tooltip text="Home Assistant Version" location="top">
<template v-slot:activator="{ props }">
<v-chip color="success" variant="flat" label v-bind="props">
<v-icon start icon="mdi-home-assistant"></v-icon>
{{ detailData.homeassistant }}
</v-chip>
</template>
</v-tooltip>
</v-col>
</v-row>
<v-row dense>
<v-col class="d-flex justify-center">
<v-tooltip text="Password protection" location="top">
<template v-slot:activator="{ props }">
<v-chip
:color="detailData.protected ? 'success' : 'primary'"
variant="flat"
class="mr-2"
label
v-bind="props"
>
<v-icon
start
:icon="
detailData.protected ? 'mdi-lock' : 'mdi-lock-open'
"
></v-icon>
{{ detailData.protected ? "Protected" : "Unprotected" }}
</v-chip>
</template>
</v-tooltip>
<v-tooltip text="Size" location="top">
<template v-slot:activator="{ props }">
<v-chip
color="success"
variant="flat"
class="mr-2"
label
v-bind="props"
>
<v-icon start icon="mdi-database"></v-icon>
{{ detailData.size }} MB
</v-chip>
</template>
</v-tooltip>
</v-col>
</v-row>
<v-row dense>
<v-col class="d-flex justify-center">
<v-tooltip text="Backup Type" location="top">
<template v-slot:activator="{ props }">
<v-chip color="success" variant="flat" label v-bind="props">
<v-icon
start
:icon="
detailData.type == 'full'
? 'mdi-alpha-f-box'
: 'mdi-alpha-p-box'
"
></v-icon>
{{ detailData.type }}
</v-chip>
</template>
</v-tooltip>
</v-col>
</v-row>
<v-row>
<v-col class="d-flex justify-center text-white">
<h3>Content</h3>
</v-col>
</v-row>
<v-divider class="mt-2"></v-divider>
<v-row>
<v-col cols="12" lg="6">
<div class="text-center text-white mt-2">
<h3>Folders</h3>
</div>
<v-list density="compact" variant="tonal">
<ha-list-item-content
v-for="item of detailData.folders.sort()"
:name="item"
v-bind:key="item"
></ha-list-item-content>
</v-list>
</v-col>
<v-col>
<div class="text-center text-white mt-2">
<h3>Addons</h3>
</div>
<v-list density="compact" variant="tonal">
<ha-list-item-content
v-for="item of detailData.addons.sort((a, b) =>
a.name.localeCompare(b.name)
)"
:name="item.name"
v-bind:key="item.slug"
:version="item.version"
></ha-list-item-content>
</v-list>
</v-col>
</v-row>
</template>
</v-card-text>
<v-divider class="mx-4"></v-divider>
<v-card-actions class="justify-center">
<v-tooltip text="Upload to Cloud" location="bottom">
<template v-slot:activator="{ props }">
<v-btn variant="outlined" color="success" v-bind="props">
<v-icon>mdi-cloud-upload</v-icon>
</v-btn>
</template>
</v-tooltip>
<v-btn variant="outlined" color="red" @click="emits('delete', item)">
<v-icon>mdi-trash-can</v-icon>
</v-btn>
</v-card-actions>
</v-card>
</v-expand-transition>
</template>
<script setup lang="ts">
import { getBackupDetail } from "@/services/homeAssistantService";
import type { BackupDetailModel, BackupModel } from "@/types/homeAssistant";
import { DateTime } from "luxon";
import { ref, watch } from "vue";
import HaListItemContent from "./HaListItemContent.vue";
const detail = ref(false);
const detailData = ref<BackupDetailModel | null>(null);
const props = defineProps<{
item: BackupModel;
index: number;
}>();
watch(detail, (value) => {
if (value) {
getBackupDetail(props.item.slug).then((response) => {
detailData.value = response;
});
}
});
const emits = defineEmits<{
(e: "delete", item: BackupModel): void;
}>();
</script>

View File

@ -0,0 +1,16 @@
<template>
<v-list-item :title="name" rounded>
<template v-slot:append v-if="version">
<div class="text-secondary">{{ version }}</div>
</template>
</v-list-item>
<v-divider></v-divider>
</template>
<script setup lang="ts">
const props = defineProps<{
name: string;
version?: string;
}>();
</script>

View File

@ -1,4 +1,9 @@
import type { AddonData, Folder } from "@/types/homeAssistant"; import type {
BackupDetailModel,
AddonData,
BackupModel,
Folder,
} from "@/types/homeAssistant";
import kyClient from "./kyClient"; import kyClient from "./kyClient";
export function getFolders() { export function getFolders() {
@ -8,3 +13,11 @@ export function getFolders() {
export function getAddons() { export function getAddons() {
return kyClient.get("homeAssistant/addons").json<AddonData>(); return kyClient.get("homeAssistant/addons").json<AddonData>();
} }
export function getBackups() {
return kyClient.get("homeAssistant/backups").json<BackupModel[]>();
}
export function getBackupDetail(slug: string) {
return kyClient.get(`homeAssistant/backup/${slug}`).json<BackupDetailModel>();
}

View File

@ -22,3 +22,37 @@ export interface AddonModel {
logo: boolean; logo: boolean;
state: string; state: string;
} }
export interface BackupModel {
slug: string;
date: string;
name: string;
type: "full" | "partial";
protected: boolean;
content: BackupContent;
compressed: boolean;
}
export interface BackupContent {
homeassistant: boolean;
addons: string[];
folders: string[];
}
export interface BackupDetailModel {
slug: string;
type: "full" | "partial";
name: string;
date: string;
size: string;
protected: boolean;
homeassistant: string;
addons: {
slug: string;
name: string;
version: string;
size: number;
}[];
repositories: string[];
folders: string[];
}