mirror of
https://github.com/amir20/dozzle.git
synced 2025-12-21 13:23:07 +01:00
feat: adds table sort to homepage (#2330)
* feat: adds table sort to homepage * feat: adds sorting to dashboard * fixes colors
This commit is contained in:
3
assets/components.d.ts
vendored
3
assets/components.d.ts
vendored
@@ -37,8 +37,6 @@ declare module 'vue' {
|
|||||||
LogViewer: typeof import('./components/LogViewer/LogViewer.vue')['default']
|
LogViewer: typeof import('./components/LogViewer/LogViewer.vue')['default']
|
||||||
LogViewerWithSource: typeof import('./components/LogViewer/LogViewerWithSource.vue')['default']
|
LogViewerWithSource: typeof import('./components/LogViewer/LogViewerWithSource.vue')['default']
|
||||||
'Mdi:arrowUp': typeof import('~icons/mdi/arrow-up')['default']
|
'Mdi:arrowUp': typeof import('~icons/mdi/arrow-up')['default']
|
||||||
'Mdi:chevronLeft': typeof import('~icons/mdi/chevron-left')['default']
|
|
||||||
'Mdi:chevronRight': typeof import('~icons/mdi/chevron-right')['default']
|
|
||||||
'Mdi:dotsVertical': typeof import('~icons/mdi/dots-vertical')['default']
|
'Mdi:dotsVertical': typeof import('~icons/mdi/dots-vertical')['default']
|
||||||
'Mdi:lightChevronDoubleDown': typeof import('~icons/mdi-light/chevron-double-down')['default']
|
'Mdi:lightChevronDoubleDown': typeof import('~icons/mdi-light/chevron-double-down')['default']
|
||||||
'Mdi:lightChevronLeft': typeof import('~icons/mdi-light/chevron-left')['default']
|
'Mdi:lightChevronLeft': typeof import('~icons/mdi-light/chevron-left')['default']
|
||||||
@@ -50,7 +48,6 @@ declare module 'vue' {
|
|||||||
'Octicon:container24': typeof import('~icons/octicon/container24')['default']
|
'Octicon:container24': typeof import('~icons/octicon/container24')['default']
|
||||||
'Octicon:download24': typeof import('~icons/octicon/download24')['default']
|
'Octicon:download24': typeof import('~icons/octicon/download24')['default']
|
||||||
'Octicon:trash24': typeof import('~icons/octicon/trash24')['default']
|
'Octicon:trash24': typeof import('~icons/octicon/trash24')['default']
|
||||||
OrugaIcon: typeof import('./components/OrugaIcon.vue')['default']
|
|
||||||
Popup: typeof import('./components/Popup.vue')['default']
|
Popup: typeof import('./components/Popup.vue')['default']
|
||||||
RouterLink: typeof import('vue-router')['RouterLink']
|
RouterLink: typeof import('vue-router')['RouterLink']
|
||||||
RouterView: typeof import('vue-router')['RouterView']
|
RouterView: typeof import('vue-router')['RouterView']
|
||||||
|
|||||||
@@ -2,12 +2,22 @@
|
|||||||
<template>
|
<template>
|
||||||
<table class="table is-fullwidth">
|
<table class="table is-fullwidth">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr :data-direction="direction > 0 ? 'asc' : 'desc'">
|
||||||
<th>{{ $t("label.container-name") }}</th>
|
<th
|
||||||
<th>{{ $t("label.status") }}</th>
|
v-for="(label, field) in headers"
|
||||||
<th>{{ $t("label.last-started") }}</th>
|
:key="field"
|
||||||
<th>{{ $t("label.avg-cpu") }}</th>
|
@click.prevent="sort(field)"
|
||||||
<th>{{ $t("label.avg-mem") }}</th>
|
:class="{ 'selected-sort': field === sortField }"
|
||||||
|
>
|
||||||
|
<a>
|
||||||
|
<span class="icon-text">
|
||||||
|
<span>{{ $t(label) }}</span>
|
||||||
|
<span class="icon">
|
||||||
|
<mdi:arrow-up />
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -31,13 +41,22 @@
|
|||||||
<nav class="pagination is-right" role="navigation" aria-label="pagination" v-if="isPaginated">
|
<nav class="pagination is-right" role="navigation" aria-label="pagination" v-if="isPaginated">
|
||||||
<ul class="pagination-list">
|
<ul class="pagination-list">
|
||||||
<li v-for="i in totalPages">
|
<li v-for="i in totalPages">
|
||||||
<a class="pagination-link" :class="{ 'is-current': i === currentPage }" @click="currentPage = i">{{ i }}</a>
|
<a class="pagination-link" :class="{ 'is-current': i === currentPage }" @click.prevent="currentPage = i">{{
|
||||||
|
i
|
||||||
|
}}</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
const headers = {
|
||||||
|
name: "label.container-name",
|
||||||
|
state: "label.status",
|
||||||
|
created: "label.last-started",
|
||||||
|
cpu: "label.avg-cpu",
|
||||||
|
mem: "label.avg-mem",
|
||||||
|
};
|
||||||
const { containers, perPage = 15 } = defineProps<{
|
const { containers, perPage = 15 } = defineProps<{
|
||||||
containers: {
|
containers: {
|
||||||
movingAverage: { cpu: number; memory: number };
|
movingAverage: { cpu: number; memory: number };
|
||||||
@@ -48,16 +67,62 @@ const { containers, perPage = 15 } = defineProps<{
|
|||||||
}[];
|
}[];
|
||||||
perPage?: number;
|
perPage?: number;
|
||||||
}>();
|
}>();
|
||||||
|
const sortField: Ref<keyof typeof headers> = ref("created");
|
||||||
|
const direction = ref<1 | -1>(1);
|
||||||
|
const sortedContainers = computedWithControl(
|
||||||
|
() => [containers.length, sortField.value, direction.value],
|
||||||
|
() => {
|
||||||
|
console.log("sorting");
|
||||||
|
return containers.sort((a, b) => {
|
||||||
|
if (sortField.value === "name") {
|
||||||
|
return direction.value * a.name.localeCompare(b.name);
|
||||||
|
} else if (sortField.value === "created") {
|
||||||
|
return direction.value * (a.created.getTime() - b.created.getTime());
|
||||||
|
} else if (sortField.value === "cpu") {
|
||||||
|
return direction.value * (a.movingAverage.cpu - b.movingAverage.cpu);
|
||||||
|
} else if (sortField.value === "mem") {
|
||||||
|
return direction.value * (a.movingAverage.memory - b.movingAverage.memory);
|
||||||
|
} else if (sortField.value === "state") {
|
||||||
|
return direction.value * a.state.localeCompare(b.state);
|
||||||
|
}
|
||||||
|
throw new Error("Invalid sort field");
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const totalPages = computed(() => Math.ceil(containers.length / perPage));
|
const totalPages = computed(() => Math.ceil(sortedContainers.value.length / perPage));
|
||||||
const isPaginated = computed(() => totalPages.value > 1);
|
const isPaginated = computed(() => totalPages.value > 1);
|
||||||
const currentPage = ref(1);
|
const currentPage = ref(1);
|
||||||
const paginated = computed(() => {
|
const paginated = computed(() => {
|
||||||
const start = (currentPage.value - 1) * perPage;
|
const start = (currentPage.value - 1) * perPage;
|
||||||
const end = start + perPage;
|
const end = start + perPage;
|
||||||
|
|
||||||
return containers.slice(start, end);
|
return sortedContainers.value.slice(start, end);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function sort(field: keyof typeof headers) {
|
||||||
|
if (sortField.value === field) {
|
||||||
|
direction.value *= -1;
|
||||||
|
} else {
|
||||||
|
sortField.value = field;
|
||||||
|
direction.value = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped></style>
|
<style lang="scss" scoped>
|
||||||
|
.icon {
|
||||||
|
display: none;
|
||||||
|
transition: transform 0.2s ease-in-out;
|
||||||
|
[data-direction="desc"] & {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.selected-sort {
|
||||||
|
font-weight: bold;
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
.icon {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ const memoryData = computedWithControl(
|
|||||||
value: formatBytes(stat.snapshot.memoryUsage),
|
value: formatBytes(stat.snapshot.memoryUsage),
|
||||||
}));
|
}));
|
||||||
return points;
|
return points;
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
|
|||||||
Reference in New Issue
Block a user