- Apps tab visible to OPERATOR+ (hidden for VIEWER), scoped by sidebar app selection and environment filter - List view: DataTable with name, environment, updated, created columns - Detail view: deployments across all envs, version upload with per-env deploy target, container config form (resources, ports, custom env vars) with explicit Save - Memory reserve field disabled for non-production environments with info hint - Admin sidebar sorted alphabetically, Applications entry removed - Old admin AppsPage deleted Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
169 lines
5.0 KiB
TypeScript
169 lines
5.0 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;
|
|
uploadedAt: string;
|
|
}
|
|
|
|
export interface Deployment {
|
|
id: string;
|
|
appId: string;
|
|
appVersionId: string;
|
|
environmentId: string;
|
|
status: 'STARTING' | 'RUNNING' | 'FAILED' | 'STOPPED';
|
|
containerId: string | null;
|
|
containerName: string | null;
|
|
errorMessage: string | null;
|
|
deployedAt: string | null;
|
|
stoppedAt: string | null;
|
|
createdAt: string;
|
|
}
|
|
|
|
async function appFetch<T>(path: string, options?: RequestInit): Promise<T> {
|
|
const token = useAuthStore.getState().accessToken;
|
|
const res = await fetch(`${config.apiBaseUrl}/apps${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);
|
|
}
|
|
|
|
// --- Apps ---
|
|
|
|
export function useAllApps() {
|
|
return useQuery({
|
|
queryKey: ['apps', 'all'],
|
|
queryFn: () => appFetch<App[]>(''),
|
|
});
|
|
}
|
|
|
|
export function useApps(environmentId: string | undefined) {
|
|
return useQuery({
|
|
queryKey: ['apps', environmentId],
|
|
queryFn: () => appFetch<App[]>(`?environmentId=${environmentId}`),
|
|
enabled: !!environmentId,
|
|
});
|
|
}
|
|
|
|
export function useCreateApp() {
|
|
const qc = useQueryClient();
|
|
return useMutation({
|
|
mutationFn: (req: { environmentId: string; slug: string; displayName: string }) =>
|
|
appFetch<App>('', { method: 'POST', body: JSON.stringify(req) }),
|
|
onSuccess: () => qc.invalidateQueries({ queryKey: ['apps'] }),
|
|
});
|
|
}
|
|
|
|
export function useDeleteApp() {
|
|
const qc = useQueryClient();
|
|
return useMutation({
|
|
mutationFn: (id: string) =>
|
|
appFetch<void>(`/${id}`, { method: 'DELETE' }),
|
|
onSuccess: () => qc.invalidateQueries({ queryKey: ['apps'] }),
|
|
});
|
|
}
|
|
|
|
export function useUpdateContainerConfig() {
|
|
const qc = useQueryClient();
|
|
return useMutation({
|
|
mutationFn: ({ appId, config }: { appId: string; config: Record<string, unknown> }) =>
|
|
appFetch<App>(`/${appId}/container-config`, { method: 'PUT', body: JSON.stringify(config) }),
|
|
onSuccess: () => qc.invalidateQueries({ queryKey: ['apps'] }),
|
|
});
|
|
}
|
|
|
|
// --- Versions ---
|
|
|
|
export function useAppVersions(appId: string | undefined) {
|
|
return useQuery({
|
|
queryKey: ['apps', appId, 'versions'],
|
|
queryFn: () => appFetch<AppVersion[]>(`/${appId}/versions`),
|
|
enabled: !!appId,
|
|
});
|
|
}
|
|
|
|
export function useUploadJar() {
|
|
const qc = useQueryClient();
|
|
return useMutation({
|
|
mutationFn: async ({ appId, file }: { appId: string; file: File }) => {
|
|
const token = useAuthStore.getState().accessToken;
|
|
const form = new FormData();
|
|
form.append('file', file);
|
|
const res = await fetch(`${config.apiBaseUrl}/apps/${appId}/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, { appId }) =>
|
|
qc.invalidateQueries({ queryKey: ['apps', appId, 'versions'] }),
|
|
});
|
|
}
|
|
|
|
// --- Deployments ---
|
|
|
|
export function useDeployments(appId: string | undefined) {
|
|
return useQuery({
|
|
queryKey: ['apps', appId, 'deployments'],
|
|
queryFn: () => appFetch<Deployment[]>(`/${appId}/deployments`),
|
|
enabled: !!appId,
|
|
refetchInterval: 5000,
|
|
});
|
|
}
|
|
|
|
export function useCreateDeployment() {
|
|
const qc = useQueryClient();
|
|
return useMutation({
|
|
mutationFn: ({ appId, ...req }: { appId: string; appVersionId: string; environmentId: string }) =>
|
|
appFetch<Deployment>(`/${appId}/deployments`, { method: 'POST', body: JSON.stringify(req) }),
|
|
onSuccess: (_data, { appId }) =>
|
|
qc.invalidateQueries({ queryKey: ['apps', appId, 'deployments'] }),
|
|
});
|
|
}
|
|
|
|
export function useStopDeployment() {
|
|
const qc = useQueryClient();
|
|
return useMutation({
|
|
mutationFn: ({ appId, deploymentId }: { appId: string; deploymentId: string }) =>
|
|
appFetch<Deployment>(`/${appId}/deployments/${deploymentId}/stop`, { method: 'POST' }),
|
|
onSuccess: (_data, { appId }) =>
|
|
qc.invalidateQueries({ queryKey: ['apps', appId, 'deployments'] }),
|
|
});
|
|
}
|