Files
cameleer-server/ui/src/api/queries/admin/apps.ts
2026-04-22 22:45:42 +02:00

230 lines
7.3 KiB
TypeScript

import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { config } from '../../../config';
import { useAuthStore } from '../../../auth/auth-store';
export interface App {
id: string;
environmentId: string;
slug: string;
displayName: string;
containerConfig: Record<string, unknown>;
createdAt: string;
updatedAt: string;
}
export interface AppVersion {
id: string;
appId: string;
version: number;
jarPath: string;
jarChecksum: string;
jarFilename: string;
jarSizeBytes: number;
detectedRuntimeType: string | null;
detectedMainClass: string | null;
uploadedAt: string;
}
export interface Deployment {
id: string;
appId: string;
appVersionId: string;
environmentId: string;
status: 'STOPPED' | 'STARTING' | 'RUNNING' | 'DEGRADED' | 'STOPPING' | 'FAILED';
targetState: string;
deploymentStrategy: string;
replicaStates: { index: number; containerId: string; containerName: string; status: string; oomKilled?: boolean }[];
deployStage: string | null;
containerId: string | null;
containerName: string | null;
errorMessage: string | null;
deployedAt: string | null;
stoppedAt: string | null;
createdAt: string;
}
/**
* Authenticated fetch. `path` is relative to apiBaseUrl, must include the
* leading slash. All app/deployment endpoints now live under
* /api/v1/environments/{envSlug}/...
*/
async function apiFetch<T>(path: string, options?: RequestInit): Promise<T> {
const token = useAuthStore.getState().accessToken;
const res = await fetch(`${config.apiBaseUrl}${path}`, {
...options,
headers: {
'Content-Type': 'application/json',
...(token ? { Authorization: `Bearer ${token}` } : {}),
'X-Cameleer-Protocol-Version': '1',
...options?.headers,
},
});
if (res.status === 401 || res.status === 403) {
useAuthStore.getState().logout();
throw new Error('Unauthorized');
}
if (!res.ok) throw new Error(`API error: ${res.status}`);
if (res.status === 204) return undefined as T;
const text = await res.text();
if (!text) return undefined as T;
return JSON.parse(text);
}
function envBase(envSlug: string): string {
return `/environments/${encodeURIComponent(envSlug)}/apps`;
}
// --- Apps ---
export function useApps(envSlug: string | undefined) {
return useQuery({
queryKey: ['apps', envSlug],
queryFn: () => apiFetch<App[]>(envBase(envSlug!)),
enabled: !!envSlug,
});
}
export function useCreateApp() {
const qc = useQueryClient();
return useMutation({
mutationFn: ({ envSlug, slug, displayName }: { envSlug: string; slug: string; displayName: string }) =>
apiFetch<App>(envBase(envSlug), {
method: 'POST',
body: JSON.stringify({ slug, displayName }),
}),
onSuccess: () => qc.invalidateQueries({ queryKey: ['apps'] }),
});
}
export function useDeleteApp() {
const qc = useQueryClient();
return useMutation({
mutationFn: ({ envSlug, appSlug }: { envSlug: string; appSlug: string }) =>
apiFetch<void>(`${envBase(envSlug)}/${encodeURIComponent(appSlug)}`, { method: 'DELETE' }),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['apps'] });
qc.invalidateQueries({ queryKey: ['catalog'] });
},
});
}
export function useUpdateContainerConfig() {
const qc = useQueryClient();
return useMutation({
mutationFn: ({ envSlug, appSlug, config }: { envSlug: string; appSlug: string; config: Record<string, unknown> }) =>
apiFetch<App>(`${envBase(envSlug)}/${encodeURIComponent(appSlug)}/container-config`, {
method: 'PUT',
body: JSON.stringify(config),
}),
onSuccess: () => qc.invalidateQueries({ queryKey: ['apps'] }),
});
}
// --- Versions ---
export function useAppVersions(envSlug: string | undefined, appSlug: string | undefined) {
return useQuery({
queryKey: ['apps', envSlug, appSlug, 'versions'],
queryFn: () => apiFetch<AppVersion[]>(`${envBase(envSlug!)}/${encodeURIComponent(appSlug!)}/versions`),
enabled: !!envSlug && !!appSlug,
});
}
export function useUploadJar() {
const qc = useQueryClient();
return useMutation({
mutationFn: async ({ envSlug, appSlug, file }: { envSlug: string; appSlug: string; file: File }) => {
const token = useAuthStore.getState().accessToken;
const form = new FormData();
form.append('file', file);
const res = await fetch(
`${config.apiBaseUrl}${envBase(envSlug)}/${encodeURIComponent(appSlug)}/versions`, {
method: 'POST',
headers: {
...(token ? { Authorization: `Bearer ${token}` } : {}),
'X-Cameleer-Protocol-Version': '1',
},
body: form,
});
if (!res.ok) throw new Error(`Upload failed: ${res.status}`);
return res.json() as Promise<AppVersion>;
},
onSuccess: (_data, { envSlug, appSlug }) =>
qc.invalidateQueries({ queryKey: ['apps', envSlug, appSlug, 'versions'] }),
});
}
// --- Deployments ---
export function useDeployments(envSlug: string | undefined, appSlug: string | undefined) {
return useQuery({
queryKey: ['apps', envSlug, appSlug, 'deployments'],
queryFn: () => apiFetch<Deployment[]>(`${envBase(envSlug!)}/${encodeURIComponent(appSlug!)}/deployments`),
enabled: !!envSlug && !!appSlug,
refetchInterval: 5000,
});
}
export function useCreateDeployment() {
const qc = useQueryClient();
return useMutation({
mutationFn: ({ envSlug, appSlug, appVersionId }: { envSlug: string; appSlug: string; appVersionId: string }) =>
apiFetch<Deployment>(
`${envBase(envSlug)}/${encodeURIComponent(appSlug)}/deployments`,
{ method: 'POST', body: JSON.stringify({ appVersionId }) },
),
onSuccess: (_data, { envSlug, appSlug }) =>
qc.invalidateQueries({ queryKey: ['apps', envSlug, appSlug, 'deployments'] }),
});
}
export function useStopDeployment() {
const qc = useQueryClient();
return useMutation({
mutationFn: ({ envSlug, appSlug, deploymentId }: { envSlug: string; appSlug: string; deploymentId: string }) =>
apiFetch<Deployment>(
`${envBase(envSlug)}/${encodeURIComponent(appSlug)}/deployments/${deploymentId}/stop`,
{ method: 'POST' },
),
onSuccess: (_data, { envSlug, appSlug }) =>
qc.invalidateQueries({ queryKey: ['apps', envSlug, appSlug, 'deployments'] }),
});
}
export function usePromoteDeployment() {
const qc = useQueryClient();
return useMutation({
mutationFn: ({ envSlug, appSlug, deploymentId, targetEnvironment }:
{ envSlug: string; appSlug: string; deploymentId: string; targetEnvironment: string }) =>
apiFetch<Deployment>(
`${envBase(envSlug)}/${encodeURIComponent(appSlug)}/deployments/${deploymentId}/promote`,
{ method: 'POST', body: JSON.stringify({ targetEnvironment }) },
),
onSuccess: () => qc.invalidateQueries({ queryKey: ['apps'] }),
});
}
// --- Dirty State ---
export interface DirtyStateDifference {
field: string;
staged: string;
deployed: string;
}
export interface DirtyState {
dirty: boolean;
lastSuccessfulDeploymentId: string | null;
differences: DirtyStateDifference[];
}
export function useDirtyState(envSlug: string | undefined, appSlug: string | undefined) {
return useQuery({
queryKey: ['apps', envSlug, appSlug, 'dirty-state'],
queryFn: () => apiFetch<DirtyState>(
`${envBase(envSlug!)}/${encodeURIComponent(appSlug!)}/dirty-state`,
),
enabled: !!envSlug && !!appSlug,
});
}