Files
dozzle/assets/stores/container.ts
T
Amir Raminfar de368a1d97 refactor(ui): tighten TypeScript inference, drop redundant casts (#4779)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 15:25:31 +00:00

216 lines
6.2 KiB
TypeScript

import { acceptHMRUpdate, defineStore } from "pinia";
import { Ref, UnwrapNestedRefs } from "vue";
import type { ContainerHealth, ContainerJson, ContainerStat } from "@/types/Container";
import { Container } from "@/models/Container";
import i18n from "@/modules/i18n";
import { parseEventData } from "@/utils/events";
import { Host } from "./hosts";
const { showToast, removeToast } = useToast();
const { updateHost } = useHosts();
// @ts-ignore
const { t } = i18n.global;
export const useContainerStore = defineStore("container", () => {
const containers: Ref<Container[]> = ref([]);
let es: EventSource | null = null;
const ready = ref(false);
const allContainersById = computed(() =>
containers.value.reduce(
(acc, container) => {
acc[container.id] = container;
return acc;
},
{} as Record<string, Container>,
),
);
const visibleContainers = computed(() => {
const filter = showAllContainers.value ? () => true : (c: Container) => c.state === "running";
return containers.value.filter(filter);
});
let errorTimer: ReturnType<typeof setTimeout> | null = null;
function connect() {
es?.close();
ready.value = false;
es = new EventSource(withBase("/api/events/stream"));
es.addEventListener("error", (e) => {
if (es?.readyState === EventSource.CONNECTING && errorTimer === null) {
errorTimer = setTimeout(() => {
errorTimer = null;
showToast(
{
id: "events-stream",
message: t("error.events-stream.message"),
title: t("error.events-stream.title"),
type: "error",
},
{ once: true },
);
}, 5000);
}
});
es.addEventListener("containers-changed", (e) => updateContainers(parseEventData<ContainerJson[]>(e)));
es.addEventListener("container-stat", (e) => {
const stat = parseEventData<ContainerStat>(e);
const container = allContainersById.value[stat.id] as unknown as UnwrapNestedRefs<Container>;
if (container) {
const { id, ...rest } = stat;
container.updateStat(rest);
}
});
es.addEventListener("container-event", (e) => {
const event = parseEventData<{ actorId: string; name: string; time: string }>(e);
const container = allContainersById.value[event.actorId];
if (container) {
switch (event.name) {
case "die":
container.state = "exited";
container.finishedAt = new Date(event.time);
break;
case "destroy":
container.state = "deleted";
break;
case "pause":
container.state = "paused";
break;
case "unpause":
container.state = "running";
break;
}
}
});
es.addEventListener("container-updated", (e) => {
const container = parseEventData<ContainerJson>(e);
const existing = allContainersById.value[container.id];
if (existing) {
existing.name = container.name;
existing.state = container.state;
existing.health = container.health;
existing.startedAt = new Date(container.startedAt);
existing.finishedAt = new Date(container.finishedAt);
if (container.mountStats) {
existing.updateMountStats(container.mountStats);
}
}
});
es.addEventListener("update-host", (e) => {
const host = parseEventData<Host>(e);
updateHost(host);
});
es.addEventListener("container-health", (e) => {
const event = parseEventData<{ actorId: string; health: ContainerHealth }>(e);
const container = allContainersById.value[event.actorId];
if (container) {
container.health = event.health;
}
});
es.onopen = () => {
if (errorTimer !== null) {
clearTimeout(errorTimer);
errorTimer = null;
}
removeToast("events-stream");
if (containers.value.length > 0) {
containers.value = [];
}
};
watchOnce(containers, () => (ready.value = true));
}
connect();
(async function () {
try {
await until(ready).toBe(true, { timeout: 8000, throwOnTimeout: true });
} catch (e) {
showToast(
{
id: "events-timeout",
message: t("error.events-timeout.message"),
title: t("error.events-timeout.title"),
type: "error",
},
{ once: true },
);
}
})();
const updateContainers = (containersPayload: ContainerJson[]) => {
const existingContainers = containersPayload.filter((c) => allContainersById.value[c.id]);
const newContainers = containersPayload.filter((c) => !allContainersById.value[c.id]);
existingContainers.forEach((c) => {
const existing = allContainersById.value[c.id];
if (ready.value && existing.state !== "running" && c.state === "running") {
existing.isNew = true;
}
existing.state = c.state;
existing.health = c.health;
existing.name = c.name;
});
const mapped = newContainers.map((c) => {
const container = Container.fromJSON(c);
if (ready.value) {
container.isNew = true;
}
return container;
});
containers.value = [...containers.value, ...mapped];
};
const currentContainer = (id: Ref<string>) => computed(() => allContainersById.value[id.value]);
const containerNames = computed(() =>
containers.value.reduce(
(acc, container) => {
acc[container.id] = container.name;
return acc;
},
{} as Record<string, string>,
),
);
const findContainerById = (id: string) => allContainersById.value[id];
const containersByHost = computed(() =>
containers.value.reduce(
(acc, container) => {
if (!acc[container.host]) {
acc[container.host] = [];
}
acc[container.host].push(container);
return acc;
},
{} as Record<string, Container[]>,
),
);
return {
containers,
allContainersById,
containersByHost,
visibleContainers,
currentContainer,
findContainerById,
containerNames,
ready,
};
});
if (import.meta.hot) {
import.meta.hot.accept(acceptHMRUpdate(useContainerStore, import.meta.hot));
}