feat: adds exponential average and a new dashboard showing all containers (#2317)
* chore: updates modules * feat: adds support for exponential moving average * feat: add expoentital moving avg * adds index page * cleans up table * fixes typecheck * adds bar chart * updates dashboard to orgua table * wip * cleans up ui * remove screenshot tests for playwright * adds more tests * fixes icon * fixes default sort * removes unused var * adds vscode settings
1
.gitignore
vendored
@@ -6,7 +6,6 @@ static
|
|||||||
dozzle
|
dozzle
|
||||||
coverage
|
coverage
|
||||||
.pnpm-debug.log
|
.pnpm-debug.log
|
||||||
.vscode
|
|
||||||
coverage.out
|
coverage.out
|
||||||
.netlify
|
.netlify
|
||||||
/test-results/
|
/test-results/
|
||||||
|
|||||||
6
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"i18n-ally.localesPaths": ["locales"],
|
||||||
|
"i18n-ally.keystyle": "nested",
|
||||||
|
"cSpell.words": ["healthcheck"],
|
||||||
|
"editor.formatOnSave": true
|
||||||
|
}
|
||||||
3
assets/auto-imports.d.ts
vendored
@@ -204,6 +204,7 @@ declare global {
|
|||||||
const useEventBus: typeof import('@vueuse/core')['useEventBus']
|
const useEventBus: typeof import('@vueuse/core')['useEventBus']
|
||||||
const useEventListener: typeof import('@vueuse/core')['useEventListener']
|
const useEventListener: typeof import('@vueuse/core')['useEventListener']
|
||||||
const useEventSource: typeof import('@vueuse/core')['useEventSource']
|
const useEventSource: typeof import('@vueuse/core')['useEventSource']
|
||||||
|
const useExponentialMovingAverage: typeof import('./utils/index')['useExponentialMovingAverage']
|
||||||
const useEyeDropper: typeof import('@vueuse/core')['useEyeDropper']
|
const useEyeDropper: typeof import('@vueuse/core')['useEyeDropper']
|
||||||
const useFavicon: typeof import('@vueuse/core')['useFavicon']
|
const useFavicon: typeof import('@vueuse/core')['useFavicon']
|
||||||
const useFetch: typeof import('@vueuse/core')['useFetch']
|
const useFetch: typeof import('@vueuse/core')['useFetch']
|
||||||
@@ -540,6 +541,7 @@ declare module 'vue' {
|
|||||||
readonly useEventBus: UnwrapRef<typeof import('@vueuse/core')['useEventBus']>
|
readonly useEventBus: UnwrapRef<typeof import('@vueuse/core')['useEventBus']>
|
||||||
readonly useEventListener: UnwrapRef<typeof import('@vueuse/core')['useEventListener']>
|
readonly useEventListener: UnwrapRef<typeof import('@vueuse/core')['useEventListener']>
|
||||||
readonly useEventSource: UnwrapRef<typeof import('@vueuse/core')['useEventSource']>
|
readonly useEventSource: UnwrapRef<typeof import('@vueuse/core')['useEventSource']>
|
||||||
|
readonly useExponentialMovingAverage: UnwrapRef<typeof import('./utils/index')['useExponentialMovingAverage']>
|
||||||
readonly useEyeDropper: UnwrapRef<typeof import('@vueuse/core')['useEyeDropper']>
|
readonly useEyeDropper: UnwrapRef<typeof import('@vueuse/core')['useEyeDropper']>
|
||||||
readonly useFavicon: UnwrapRef<typeof import('@vueuse/core')['useFavicon']>
|
readonly useFavicon: UnwrapRef<typeof import('@vueuse/core')['useFavicon']>
|
||||||
readonly useFetch: UnwrapRef<typeof import('@vueuse/core')['useFetch']>
|
readonly useFetch: UnwrapRef<typeof import('@vueuse/core')['useFetch']>
|
||||||
@@ -870,6 +872,7 @@ declare module '@vue/runtime-core' {
|
|||||||
readonly useEventBus: UnwrapRef<typeof import('@vueuse/core')['useEventBus']>
|
readonly useEventBus: UnwrapRef<typeof import('@vueuse/core')['useEventBus']>
|
||||||
readonly useEventListener: UnwrapRef<typeof import('@vueuse/core')['useEventListener']>
|
readonly useEventListener: UnwrapRef<typeof import('@vueuse/core')['useEventListener']>
|
||||||
readonly useEventSource: UnwrapRef<typeof import('@vueuse/core')['useEventSource']>
|
readonly useEventSource: UnwrapRef<typeof import('@vueuse/core')['useEventSource']>
|
||||||
|
readonly useExponentialMovingAverage: UnwrapRef<typeof import('./utils/index')['useExponentialMovingAverage']>
|
||||||
readonly useEyeDropper: UnwrapRef<typeof import('@vueuse/core')['useEyeDropper']>
|
readonly useEyeDropper: UnwrapRef<typeof import('@vueuse/core')['useEyeDropper']>
|
||||||
readonly useFavicon: UnwrapRef<typeof import('@vueuse/core')['useFavicon']>
|
readonly useFavicon: UnwrapRef<typeof import('@vueuse/core')['useFavicon']>
|
||||||
readonly useFetch: UnwrapRef<typeof import('@vueuse/core')['useFetch']>
|
readonly useFetch: UnwrapRef<typeof import('@vueuse/core')['useFetch']>
|
||||||
|
|||||||
3
assets/components.d.ts
vendored
@@ -7,6 +7,7 @@ export {}
|
|||||||
|
|
||||||
declare module 'vue' {
|
declare module 'vue' {
|
||||||
export interface GlobalComponents {
|
export interface GlobalComponents {
|
||||||
|
BarChart: typeof import('./components/BarChart.vue')['default']
|
||||||
'Carbon:caretDown': typeof import('~icons/carbon/caret-down')['default']
|
'Carbon:caretDown': typeof import('~icons/carbon/caret-down')['default']
|
||||||
'Carbon:circleSolid': typeof import('~icons/carbon/circle-solid')['default']
|
'Carbon:circleSolid': typeof import('~icons/carbon/circle-solid')['default']
|
||||||
'Cil:checkCircle': typeof import('~icons/cil/check-circle')['default']
|
'Cil:checkCircle': typeof import('~icons/cil/check-circle')['default']
|
||||||
@@ -34,6 +35,7 @@ declare module 'vue' {
|
|||||||
LogStd: typeof import('./components/LogViewer/LogStd.vue')['default']
|
LogStd: typeof import('./components/LogViewer/LogStd.vue')['default']
|
||||||
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: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']
|
||||||
@@ -45,6 +47,7 @@ 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']
|
||||||
|
|||||||
24
assets/components/BarChart.vue
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<template>
|
||||||
|
<div class="is-relative">
|
||||||
|
<div class="bar"></div>
|
||||||
|
<div class="is-overlay">
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
const { value } = defineProps<{ value: number }>();
|
||||||
|
|
||||||
|
const minValue = computed(() => Math.min(value, 1));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.bar {
|
||||||
|
height: 100%;
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
transform-origin: left;
|
||||||
|
transform: scaleX(v-bind(minValue));
|
||||||
|
transition: transform 0.2s ease-out;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -44,7 +44,7 @@ const memoryData = computedWithControl(
|
|||||||
value: formatBytes(stat.snapshot.memoryUsage),
|
value: formatBytes(stat.snapshot.memoryUsage),
|
||||||
}));
|
}));
|
||||||
return points;
|
return points;
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
11
assets/components/OrugaIcon.vue
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<mdi:arrow-up v-if="icon[1] == 'arrow-up'" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
const { icon } = defineProps<{ icon: string[] }>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
@@ -54,7 +54,7 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</transition>
|
</transition>
|
||||||
</div>
|
</div>
|
||||||
<ul class="menu-list is-hidden-mobile loading" v-else>
|
<ul class="menu-list is-hidden-mobile has-light-opacity" v-else>
|
||||||
<li v-for="index in 7" class="my-4"><o-skeleton animated size="large" :key="index"></o-skeleton></li>
|
<li v-for="index in 7" class="my-4"><o-skeleton animated size="large" :key="index"></o-skeleton></li>
|
||||||
</ul>
|
</ul>
|
||||||
</template>
|
</template>
|
||||||
@@ -110,7 +110,7 @@ const activeContainersById = computed(() =>
|
|||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.loading {
|
.has-light-opacity {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,9 @@ const {
|
|||||||
}>();
|
}>();
|
||||||
|
|
||||||
const text = ref<string>();
|
const text = ref<string>();
|
||||||
|
|
||||||
|
watch($$(date), updateFromNow, { immediate: true });
|
||||||
|
|
||||||
function updateFromNow() {
|
function updateFromNow() {
|
||||||
const fn = strict ? formatDistanceToNowStrict : formatDistanceToNow;
|
const fn = strict ? formatDistanceToNowStrict : formatDistanceToNow;
|
||||||
text.value = fn(date, {
|
text.value = fn(date, {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { ContainerHealth, ContainerStat, ContainerState } from "@/types/Container";
|
import type { ContainerHealth, ContainerStat, ContainerState } from "@/types/Container";
|
||||||
import type { UseThrottledRefHistoryReturn } from "@vueuse/core";
|
import type { UseThrottledRefHistoryReturn } from "@vueuse/core";
|
||||||
|
import { useExponentialMovingAverage } from "@/utils";
|
||||||
import { Ref } from "vue";
|
import { Ref } from "vue";
|
||||||
|
|
||||||
type Stat = Omit<ContainerStat, "id">;
|
type Stat = Omit<ContainerStat, "id">;
|
||||||
@@ -11,6 +12,7 @@ export class Container {
|
|||||||
private readonly throttledStatHistory: UseThrottledRefHistoryReturn<Stat, Stat>;
|
private readonly throttledStatHistory: UseThrottledRefHistoryReturn<Stat, Stat>;
|
||||||
public readonly swarmId: string | null = null;
|
public readonly swarmId: string | null = null;
|
||||||
public readonly isSwarm: boolean = false;
|
public readonly isSwarm: boolean = false;
|
||||||
|
public readonly movingAverageStat: Ref<Stat>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public readonly id: string,
|
public readonly id: string,
|
||||||
@@ -25,6 +27,7 @@ export class Container {
|
|||||||
) {
|
) {
|
||||||
this.stat = ref({ cpu: 0, memory: 0, memoryUsage: 0 });
|
this.stat = ref({ cpu: 0, memory: 0, memoryUsage: 0 });
|
||||||
this.throttledStatHistory = useThrottledRefHistory(this.stat, { capacity: 300, deep: true, throttle: 1000 });
|
this.throttledStatHistory = useThrottledRefHistory(this.stat, { capacity: 300, deep: true, throttle: 1000 });
|
||||||
|
this.movingAverageStat = useExponentialMovingAverage(this.stat, 0.2);
|
||||||
|
|
||||||
const match = name.match(SWARM_ID_REGEX);
|
const match = name.match(SWARM_ID_REGEX);
|
||||||
if (match) {
|
if (match) {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { type App } from "vue";
|
import { type App } from "vue";
|
||||||
import { Autocomplete, Button, Dropdown, Switch, Skeleton, Field, Modal, Config } from "@oruga-ui/oruga-next";
|
import { Autocomplete, Button, Dropdown, Switch, Skeleton, Field, Table, Modal, Config } from "@oruga-ui/oruga-next";
|
||||||
import { bulmaConfig } from "@oruga-ui/theme-bulma";
|
import { bulmaConfig } from "@oruga-ui/theme-bulma";
|
||||||
|
import OrugaIcon from "@/components/OrugaIcon.vue";
|
||||||
export const install = (app: App) => {
|
export const install = (app: App) => {
|
||||||
app
|
app
|
||||||
.use(Autocomplete)
|
.use(Autocomplete)
|
||||||
@@ -11,5 +11,7 @@ export const install = (app: App) => {
|
|||||||
.use(Modal)
|
.use(Modal)
|
||||||
.use(Field)
|
.use(Field)
|
||||||
.use(Skeleton)
|
.use(Skeleton)
|
||||||
.use(Config, bulmaConfig);
|
.use(Table)
|
||||||
|
.component("oruga-icon", OrugaIcon)
|
||||||
|
.use(Config, { ...bulmaConfig, iconComponent: "oruga-icon", iconPack: "" });
|
||||||
};
|
};
|
||||||
@@ -1,123 +1,88 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div class="section tile is-ancestor">
|
||||||
<section class="level section pb-0-is-mobile">
|
<div class="tile is-parent">
|
||||||
|
<div class="tile is-child box">
|
||||||
<div class="level-item has-text-centered">
|
<div class="level-item has-text-centered">
|
||||||
<div>
|
<div>
|
||||||
<p class="title">{{ containers.length }}</p>
|
<p class="title">{{ containers.length }}</p>
|
||||||
<p class="heading">{{ $t("label.total-containers") }}</p>
|
<p class="heading">{{ $t("label.total-containers") }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="level-item has-text-centered">
|
|
||||||
<div>
|
|
||||||
<p class="title">{{ runningContainers.length }}</p>
|
|
||||||
<p class="heading">{{ $t("label.running") }}</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="tile is-parent">
|
||||||
|
<div class="tile is-child box">
|
||||||
<div class="level-item has-text-centered" data-ci-skip>
|
<div class="level-item has-text-centered" data-ci-skip>
|
||||||
<div>
|
<div>
|
||||||
<p class="title">{{ totalCpu }}%</p>
|
<p class="title">{{ totalCpu }}%</p>
|
||||||
<p class="heading">{{ $t("label.total-cpu-usage") }}</p>
|
<p class="heading">{{ $t("label.total-cpu-usage") }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="tile is-parent">
|
||||||
|
<div class="tile is-child box">
|
||||||
<div class="level-item has-text-centered" data-ci-skip>
|
<div class="level-item has-text-centered" data-ci-skip>
|
||||||
<div>
|
<div>
|
||||||
<p class="title">{{ formatBytes(totalMem) }}</p>
|
<p class="title">{{ formatBytes(totalMem) }}</p>
|
||||||
<p class="heading">{{ $t("label.total-mem-usage") }}</p>
|
<p class="heading">{{ $t("label.total-mem-usage") }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="tile is-parent">
|
||||||
|
<div class="tile is-child box">
|
||||||
<div class="level-item has-text-centered">
|
<div class="level-item has-text-centered">
|
||||||
<div>
|
<div>
|
||||||
<p class="title">{{ version }}</p>
|
<p class="title">{{ version }}</p>
|
||||||
<p class="heading">{{ $t("label.dozzle-version") }}</p>
|
<p class="heading">{{ $t("label.dozzle-version") }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="columns is-centered section is-marginless pt-0-is-mobile">
|
|
||||||
<div class="column is-12-mobile is-6-tablet is-5-desktop is-4-fullhd">
|
|
||||||
<div class="panel">
|
|
||||||
<p class="panel-heading">{{ $t("label.containers") }}</p>
|
|
||||||
<div class="panel-block">
|
|
||||||
<p class="control has-icons-left">
|
|
||||||
<input
|
|
||||||
class="input"
|
|
||||||
type="text"
|
|
||||||
:placeholder="$t('placeholder.search-containers')"
|
|
||||||
v-model="query"
|
|
||||||
@keyup.esc="query = ''"
|
|
||||||
@keyup.enter="onEnter()"
|
|
||||||
/>
|
|
||||||
<span class="icon is-left">
|
|
||||||
<mdi:light-magnify />
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<p class="panel-tabs" v-if="query === ''">
|
|
||||||
<a :class="{ 'is-active': sort === 'running' }" @click="sort = 'running'">{{ $t("label.running") }}</a>
|
|
||||||
<a :class="{ 'is-active': sort === 'all' }" @click="sort = 'all'">{{ $t("label.all") }}</a>
|
|
||||||
</p>
|
|
||||||
<router-link
|
|
||||||
:to="{ name: 'container-id', params: { id: item.id } }"
|
|
||||||
v-for="item in data.slice(0, 10)"
|
|
||||||
:key="item.id"
|
|
||||||
class="panel-block"
|
|
||||||
>
|
|
||||||
<span class="name">{{ item.name }}</span>
|
|
||||||
|
|
||||||
<div class="subtitle is-7 status">
|
|
||||||
<distance-time :date="item.created"></distance-time>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section class="section table-container">
|
||||||
|
<div class="box" data-ci-skip>
|
||||||
|
<o-table :data="runningContainers" :defaultSort="['created', 'desc']">
|
||||||
|
<o-table-column #default="{ row: container }" label="Container Name" sortable field="name">
|
||||||
|
<router-link :to="{ name: 'container-id', params: { id: container.id } }" :title="container.name">
|
||||||
|
{{ container.name }}
|
||||||
</router-link>
|
</router-link>
|
||||||
|
</o-table-column>
|
||||||
|
<o-table-column #default="{ row: container }" label="State" sortable field="state">
|
||||||
|
{{ container.state }}
|
||||||
|
</o-table-column>
|
||||||
|
<o-table-column #default="{ row: container }" label="Running" sortable field="created">
|
||||||
|
<distance-time :date="container.created" strict :suffix="false"></distance-time>
|
||||||
|
</o-table-column>
|
||||||
|
<o-table-column #default="{ row: container }" label="Avg. CPU" sortable field="movingAverageStat.cpu">
|
||||||
|
<bar-chart :value="container.movingAverageStat.cpu / 100" class="bar-chart">
|
||||||
|
<div class="bar-text">
|
||||||
|
{{ (container.movingAverageStat.cpu / 100).toLocaleString(undefined, { style: "percent" }) }}
|
||||||
</div>
|
</div>
|
||||||
|
</bar-chart>
|
||||||
|
</o-table-column>
|
||||||
|
<o-table-column #default="{ row: container }" label="Avg. Memory" sortable field="movingAverageStat.memory">
|
||||||
|
<bar-chart :value="container.movingAverageStat.memory / 100" class="bar-chart">
|
||||||
|
<div class="bar-text">
|
||||||
|
{{ formatBytes(container.movingAverageStat.memoryUsage) }}
|
||||||
|
</div>
|
||||||
|
</bar-chart>
|
||||||
|
</o-table-column>
|
||||||
|
</o-table>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { useFuse } from "@vueuse/integrations/useFuse";
|
|
||||||
|
|
||||||
const { version } = config;
|
const { version } = config;
|
||||||
const containerStore = useContainerStore();
|
const containerStore = useContainerStore();
|
||||||
const { containers } = storeToRefs(containerStore);
|
const { containers } = storeToRefs(containerStore);
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const sort = $ref("running");
|
|
||||||
const query = ref("");
|
|
||||||
|
|
||||||
const mostRecentContainers = $computed(() => [...containers.value].sort((a, b) => +b.created - +a.created));
|
const mostRecentContainers = $computed(() => [...containers.value].sort((a, b) => +b.created - +a.created));
|
||||||
const runningContainers = $computed(() => mostRecentContainers.filter((c) => c.state === "running"));
|
const runningContainers = $computed(() => mostRecentContainers.filter((c) => c.state === "running"));
|
||||||
|
|
||||||
const list = computed(() => {
|
|
||||||
return containers.value.map(({ id, created, name, state }) => {
|
|
||||||
return {
|
|
||||||
id,
|
|
||||||
created,
|
|
||||||
name,
|
|
||||||
state,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const { results } = useFuse(query, list, {
|
|
||||||
fuseOptions: { keys: ["name"] },
|
|
||||||
matchAllWhenSearchEmpty: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = computed(() => {
|
|
||||||
if (results.value.length) {
|
|
||||||
return results.value.map(({ item }) => item);
|
|
||||||
}
|
|
||||||
switch (sort) {
|
|
||||||
case "all":
|
|
||||||
return mostRecentContainers;
|
|
||||||
case "running":
|
|
||||||
return runningContainers;
|
|
||||||
default:
|
|
||||||
throw `Invalid sort order: ${sort}`;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let totalCpu = $ref(0);
|
let totalCpu = $ref(0);
|
||||||
useIntervalFn(
|
useIntervalFn(
|
||||||
() => {
|
() => {
|
||||||
@@ -135,13 +100,6 @@ useIntervalFn(
|
|||||||
1000,
|
1000,
|
||||||
{ immediate: true },
|
{ immediate: true },
|
||||||
);
|
);
|
||||||
|
|
||||||
function onEnter() {
|
|
||||||
if (data.value.length > 0) {
|
|
||||||
const item = data.value[0];
|
|
||||||
router.push({ name: "container-id", params: { id: item.id } });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.panel {
|
.panel {
|
||||||
@@ -181,4 +139,17 @@ function onEnter() {
|
|||||||
.icon {
|
.icon {
|
||||||
padding: 10px 3px;
|
padding: 10px 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bar-chart {
|
||||||
|
height: 1.5em;
|
||||||
|
.bar-text {
|
||||||
|
font-size: 0.9em;
|
||||||
|
padding: 0 0.5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(tr td) {
|
||||||
|
padding-top: 1em;
|
||||||
|
padding-bottom: 1em;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -28,14 +28,8 @@ $link-active: $grey-dark;
|
|||||||
$dark-toolbar-color: rgba($black-bis, 0.7);
|
$dark-toolbar-color: rgba($black-bis, 0.7);
|
||||||
$light-toolbar-color: rgba($grey-darker, 0.7);
|
$light-toolbar-color: rgba($grey-darker, 0.7);
|
||||||
|
|
||||||
@import "bulma/bulma.sass";
|
@import "bulma/bulma";
|
||||||
@import "@oruga-ui/theme-bulma/dist/scss/components/utils/all.scss";
|
@import "@oruga-ui/theme-bulma/dist/scss/bulma";
|
||||||
@import "@oruga-ui/theme-bulma/dist/scss/components/autocomplete.scss";
|
|
||||||
@import "@oruga-ui/theme-bulma/dist/scss/components/button.scss";
|
|
||||||
@import "@oruga-ui/theme-bulma/dist/scss/components/modal.scss";
|
|
||||||
@import "@oruga-ui/theme-bulma/dist/scss/components/switch.scss";
|
|
||||||
@import "@oruga-ui/theme-bulma/dist/scss/components/dropdown.scss";
|
|
||||||
@import "@oruga-ui/theme-bulma/dist/scss/components/skeleton.scss";
|
|
||||||
@import "splitpanes/dist/splitpanes.css";
|
@import "splitpanes/dist/splitpanes.css";
|
||||||
|
|
||||||
@mixin dark {
|
@mixin dark {
|
||||||
@@ -218,7 +212,9 @@ html.has-custom-scrollbars {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.has-boxshadow {
|
.has-boxshadow {
|
||||||
box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
|
box-shadow:
|
||||||
|
0 1px 3px 0 rgb(0 0 0 / 0.1),
|
||||||
|
0 1px 2px -1px rgb(0 0 0 / 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
mark {
|
mark {
|
||||||
|
|||||||
@@ -37,3 +37,17 @@ export function stripVersion(label: string) {
|
|||||||
const [name, _] = label.split(":");
|
const [name, _] = label.split(":");
|
||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useExponentialMovingAverage<T extends Record<string, number>>(source: Ref<T>, alpha: number = 0.2) {
|
||||||
|
const ema = ref<T>(source.value) as Ref<T>;
|
||||||
|
|
||||||
|
watch(source, (value) => {
|
||||||
|
const newValue = {} as Record<string, number>;
|
||||||
|
for (const key in value) {
|
||||||
|
newValue[key] = alpha * value[key] + (1 - alpha) * ema.value[key];
|
||||||
|
}
|
||||||
|
ema.value = newValue as T;
|
||||||
|
});
|
||||||
|
|
||||||
|
return ema;
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,6 +8,13 @@ test("has right title", async ({ page }) => {
|
|||||||
await expect(page).toHaveTitle(/.* - Dozzle/);
|
await expect(page).toHaveTitle(/.* - Dozzle/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("has dashboard text", async ({ page }) => {
|
||||||
|
await expect(page.getByText("Total Containers")).toBeVisible();
|
||||||
|
await expect(page.getByText("Total CPU Usage")).toBeVisible();
|
||||||
|
await expect(page.getByText("Total Mem Usage")).toBeVisible();
|
||||||
|
await expect(page.getByText("Dozzle Version")).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
test("click on settings button", async ({ page }) => {
|
test("click on settings button", async ({ page }) => {
|
||||||
await page.getByRole("link", { name: "Settings" }).click();
|
await page.getByRole("link", { name: "Settings" }).click();
|
||||||
await expect(page.getByRole("heading", { name: "About" })).toBeVisible();
|
await expect(page.getByRole("heading", { name: "About" })).toBeVisible();
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
import { test, expect } from "@playwright/test";
|
|
||||||
|
|
||||||
test.beforeEach(async ({ page }) => {
|
|
||||||
await page.goto("http://dozzle:8080/");
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe("default", () => {
|
|
||||||
test("homepage", async ({ page }) => {
|
|
||||||
await page.addStyleTag({ content: `[data-ci-skip] { visibility: hidden; }` });
|
|
||||||
await expect(page).toHaveScreenshot({});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe("dark", () => {
|
|
||||||
test.use({ colorScheme: "dark" });
|
|
||||||
test("homepage", async ({ page }) => {
|
|
||||||
await page.addStyleTag({ content: `[data-ci-skip] { visibility: hidden; }` });
|
|
||||||
await expect(page).toHaveScreenshot({});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 27 KiB |