230 lines
7.3 KiB
TypeScript
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,
|
|
});
|
|
}
|