Files
cameleer-server/ui/src/api/queries/admin/apps.ts
hsiegeln de4ca10fa5
Some checks failed
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m20s
CI / docker (push) Successful in 1m8s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Failing after 2m16s
feat: move Apps from admin to main tab bar with container config
- 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>
2026-04-08 16:23:30 +02:00

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'] }),
});
}