|
@@ -0,0 +1,135 @@
|
|
|
|
|
+import { SvelteMap, SvelteSet } from 'svelte/reactivity';
|
|
|
|
|
+
|
|
|
|
|
+import { extractMessage } from '$lib/api/types';
|
|
|
|
|
+import { notificationsStore } from '$lib/stores/notifications.svelte';
|
|
|
|
|
+import type { ResourceEvent } from '$lib/types/events';
|
|
|
|
|
+
|
|
|
|
|
+import { eventsStore } from './events.svelte';
|
|
|
|
|
+
|
|
|
|
|
+type Resource = {
|
|
|
|
|
+ group: string;
|
|
|
|
|
+ version: string;
|
|
|
|
|
+ resources: string;
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+abstract class ResourceStore<T extends { id: string; name: string }, CRStatus = unknown> {
|
|
|
|
|
+ #resourceDisplayName: string;
|
|
|
|
|
+ #resources: Set<string>;
|
|
|
|
|
+ protected workspaceId = 'default';
|
|
|
|
|
+ list = $state<T[]>([]);
|
|
|
|
|
+ status = $state<Map<string, CRStatus | undefined>>(new SvelteMap());
|
|
|
|
|
+ creating = $state(0);
|
|
|
|
|
+ deleting = $state(0);
|
|
|
|
|
+
|
|
|
|
|
+ constructor(resourceName: string, resources: Resource[]) {
|
|
|
|
|
+ this.#resourceDisplayName = resourceName;
|
|
|
|
|
+ this.#resources = new SvelteSet(resources.map((r) => `${r.group}/${r.version}/${r.resources}`));
|
|
|
|
|
+ eventsStore.subscribe((event) => this.onResourceEvent(event as ResourceEvent<CRStatus>));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ protected abstract resourceHref(id: string): string | undefined;
|
|
|
|
|
+ protected abstract apiGet(workspaceId: string, id: string): Promise<T>;
|
|
|
|
|
+ protected abstract apiGetStatus(workspaceId: string, id: string): Promise<CRStatus>;
|
|
|
|
|
+ protected abstract apiCreate(workspaceId: string, item: T): Promise<T>;
|
|
|
|
|
+ protected abstract apiUpdate(workspaceId: string, item: T): Promise<T>;
|
|
|
|
|
+ protected abstract apiDelete(workspaceId: string, id: string): Promise<void>;
|
|
|
|
|
+ protected abstract onStatusChange(id: string, status: CRStatus, notify?: boolean): void;
|
|
|
|
|
+
|
|
|
|
|
+ setWorkspaceId(id: string): void {
|
|
|
|
|
+ this.workspaceId = id;
|
|
|
|
|
+ this.list = [];
|
|
|
|
|
+ this.creating = 0;
|
|
|
|
|
+ this.deleting = 0;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ hydrate(items: T[]): void {
|
|
|
|
|
+ this.list = items;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ upsert(item: T, status?: CRStatus): T {
|
|
|
|
|
+ const existing = this.list.findIndex((i) => i.id === item.id);
|
|
|
|
|
+ let reactiveItem: T;
|
|
|
|
|
+ if (existing !== -1) {
|
|
|
|
|
+ this.list[existing] = item;
|
|
|
|
|
+ reactiveItem = this.list[existing];
|
|
|
|
|
+ } else {
|
|
|
|
|
+ this.list.push(item);
|
|
|
|
|
+ reactiveItem = this.list[this.list.length - 1];
|
|
|
|
|
+ }
|
|
|
|
|
+ if (status !== undefined) {
|
|
|
|
|
+ this.onStatusChange(item.id, status, false);
|
|
|
|
|
+ }
|
|
|
|
|
+ return reactiveItem;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ get(id: string): T | undefined {
|
|
|
|
|
+ return this.list.find((item) => item.id === id);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ getStatus(id: string): CRStatus | undefined {
|
|
|
|
|
+ return this.status.get(id);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ async refresh(id: string): Promise<T | undefined> {
|
|
|
|
|
+ const item = await this.apiGet(this.workspaceId, id);
|
|
|
|
|
+ return this.upsert(item);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ async refreshStatus(id: string): Promise<void> {
|
|
|
|
|
+ const status = await this.apiGetStatus(this.workspaceId, id);
|
|
|
|
|
+ if (status === undefined) return;
|
|
|
|
|
+ this.status.set(id, status);
|
|
|
|
|
+ this.onStatusChange(id, status, false);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ async create(item: T): Promise<T> {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const createdItem = await this.apiCreate(this.workspaceId, item);
|
|
|
|
|
+ notificationsStore.push({
|
|
|
|
|
+ type: 'info',
|
|
|
|
|
+ message: `${this.#resourceDisplayName} created - deploying...`,
|
|
|
|
|
+ href: this.resourceHref(createdItem.id),
|
|
|
|
|
+ });
|
|
|
|
|
+ return this.upsert(createdItem);
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ notificationsStore.push({ type: 'error', message: extractMessage(err) });
|
|
|
|
|
+ throw err;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ async update(item: T): Promise<T> {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const updatedItem = await this.apiUpdate(this.workspaceId, item);
|
|
|
|
|
+ notificationsStore.push({ type: 'success', message: `${this.#resourceDisplayName} updated` });
|
|
|
|
|
+ return this.upsert(updatedItem);
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ notificationsStore.push({ type: 'error', message: extractMessage(err) });
|
|
|
|
|
+ throw err;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ async remove(id: string): Promise<void> {
|
|
|
|
|
+ try {
|
|
|
|
|
+ await this.apiDelete(this.workspaceId, id);
|
|
|
|
|
+ notificationsStore.push({
|
|
|
|
|
+ type: 'success',
|
|
|
|
|
+ message: `${this.#resourceDisplayName} deleted`,
|
|
|
|
|
+ });
|
|
|
|
|
+ const idx = this.list.findIndex((i) => i.id === id);
|
|
|
|
|
+ if (idx !== -1) this.list.splice(idx, 1);
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ notificationsStore.push({ type: 'error', message: extractMessage(err) });
|
|
|
|
|
+ throw err;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ onResourceEvent(event: ResourceEvent<CRStatus>): void {
|
|
|
|
|
+ if (event.namespace) return;
|
|
|
|
|
+ if (!this.#resources.has(`${event.group}/${event.version}/${event.resource}`)) return;
|
|
|
|
|
+ if (this.list.every((item) => item.id !== event.name)) return;
|
|
|
|
|
+ this.status.set(event.name, event.status);
|
|
|
|
|
+ this.onStatusChange(event.name, event.status, true);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+export { ResourceStore };
|