Files
Amir Raminfar abac4fdf7e feat: welcome modal after cloud linking (#4624)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 21:20:59 +00:00

157 lines
5.1 KiB
TypeScript

import { Container } from "@/models/Container";
import type { ContainerJson } from "@/types/Container";
import type { Dispatcher, NotificationRule, PreviewResult } from "@/types/notifications";
import { createContainerHints } from "@/composable/exprEditor";
export interface AlertFormOptions {
close?: () => void;
onCreated?: () => void;
alert?: NotificationRule;
prefill?: {
name?: string;
containerExpression?: string;
logExpression?: string;
metricExpression?: string;
eventExpression?: string;
dispatcherId?: number;
};
}
export interface ContainerResult {
error?: string;
containers?: Container[];
}
export function useAlertForm(options: AlertFormOptions) {
const isEditing = computed(() => !!options.alert);
const alertName = ref(options.alert?.name ?? options.prefill?.name ?? "");
const containerExpression = ref(options.alert?.containerExpression ?? options.prefill?.containerExpression ?? "");
const dispatcherId = ref(options.alert?.dispatcher?.id ?? options.prefill?.dispatcherId ?? -1);
const isSaving = ref(false);
const saveError = ref<string | null>(null);
// Destinations (cloud dispatcher with id=0 is included by the backend when configured)
const destinations = ref<Dispatcher[]>([]);
onMounted(async () => {
const res = await fetch(withBase("/api/notifications/dispatchers"));
destinations.value = await res.json();
});
const selectedDestination = computed(() => destinations.value.find((d) => d.id === dispatcherId.value));
// Container store for autocomplete
const containerStore = useContainerStore();
const { containers } = storeToRefs(containerStore);
const containerNames = computed(() => [
...new Set(containers.value.filter((c) => c.state === "running").map((c) => c.name)),
]);
const imageNames = computed(() => [...new Set(containers.value.map((c) => c.image))]);
const hostNames = computed(() => [...new Set(containers.value.map((c) => c.host))]);
// Container validation
const containerResult = ref<ContainerResult | null>(null);
const isLoading = ref(false);
const baseCanSave = computed(
() =>
alertName.value.trim() &&
containerExpression.value.trim() &&
dispatcherId.value >= 0 &&
!containerResult.value?.error &&
!isSaving.value,
);
function setupContainerEditor(editorRef: Ref<HTMLElement | undefined>) {
useExprEditorField(editorRef, {
placeholder: 'name contains "api"',
initialValue: options.alert?.containerExpression ?? options.prefill?.containerExpression ?? "",
getHints: () => createContainerHints(containerNames.value, imageNames.value, hostNames.value),
onChange: (v) => (containerExpression.value = v),
});
}
async function saveAlert(typeSpecificFields: Record<string, unknown>) {
isSaving.value = true;
saveError.value = null;
try {
const input = {
name: alertName.value.trim(),
containerExpression: containerExpression.value,
dispatcherId: dispatcherId.value,
enabled: true,
...typeSpecificFields,
};
const url = isEditing.value
? withBase(`/api/notifications/rules/${options.alert!.id}`)
: withBase("/api/notifications/rules");
const res = await fetch(url, {
method: isEditing.value ? "PUT" : "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(input),
});
if (!res.ok) {
const data = await res.json();
throw new Error(data.error || "Failed to save alert");
}
options.onCreated?.();
options.close?.();
} catch (e) {
saveError.value = e instanceof Error ? e.message : "Failed to save alert";
} finally {
isSaving.value = false;
}
}
async function validatePreview(extraFields: Record<string, unknown> = {}) {
if (!containerExpression.value && !Object.values(extraFields).some(Boolean)) {
containerResult.value = null;
return { data: null };
}
isLoading.value = true;
try {
const res = await fetch(withBase("/api/notifications/preview"), {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
containerExpression: containerExpression.value,
...extraFields,
}),
});
if (!res.ok) {
const errData = await res.json();
throw new Error(errData.error || "Preview failed");
}
const data: PreviewResult = await res.json();
containerResult.value = containerExpression.value
? {
error: data.containerError ?? undefined,
containers: data.matchedContainers?.map((c) => Container.fromJSON(c as ContainerJson)),
}
: null;
return { data };
} catch (e) {
containerResult.value = { error: e instanceof Error ? e.message : "Unknown error" };
return { data: null };
} finally {
isLoading.value = false;
}
}
return {
isEditing,
alertName,
containerExpression,
dispatcherId,
destinations,
selectedDestination,
containerResult,
isLoading,
isSaving,
saveError,
baseCanSave,
setupContainerEditor,
saveAlert,
validatePreview,
};
}