MPM marketplace and UI updates

This commit is contained in:
leon
2026-10-08 21:13:57 +02:00
parent 7b121fa908
commit fd9de823ea
66 changed files with 5187 additions and 749 deletions

View File

@@ -11,6 +11,8 @@ export class ApiError extends Error {
public readonly status: number,
message: string,
public readonly details?: Record<string, string | string[]>,
public readonly code?: string,
public readonly diagnostic?: string,
) {
super(message);
this.name = 'ApiError';
@@ -78,10 +80,14 @@ export async function apiRequest<TResponse>(
if (!response.ok) {
let message = 'Ein unerwarteter Fehler ist aufgetreten.';
let details: Record<string, string[]> | undefined;
let code: string | undefined;
let diagnostic: string | undefined;
try {
const errorBody = (await response.json()) as {
message?: string | string[];
details?: Record<string, string[]>;
code?: string;
diagnostic?: string;
};
if (typeof errorBody.message === 'string') {
message = errorBody.message;
@@ -89,10 +95,12 @@ export async function apiRequest<TResponse>(
message = errorBody.message.join(', ');
}
details = errorBody.details;
code = errorBody.code;
diagnostic = errorBody.diagnostic;
} catch {
// Antwort enthält kein JSON – Standardmeldung verwenden.
}
throw new ApiError(response.status, message, details);
throw new ApiError(response.status, message, details, code, diagnostic);
}
return (await response.json()) as TResponse;

View File

@@ -3,9 +3,11 @@ import {
accessibleModuleSchema,
modulePermissionSchema,
moduleSchema,
moduleConfigurationSchema,
type AccessibleModule,
type Module,
type ModulePermission,
type ModuleConfiguration,
} from './schemas';
/** Typsichere API-Funktionen für die Modul-Verwaltung. */
@@ -35,10 +37,14 @@ export async function installModule(file: File): Promise<Module> {
if (!response.ok) {
let message = 'Installation fehlgeschlagen';
let details: Record<string, string | string[]> | undefined;
let code: string | undefined;
let diagnostic: string | undefined;
try {
const errorBody = (await response.json()) as {
message?: string | string[];
details?: Record<string, string | string[]>;
code?: string;
diagnostic?: string;
};
if (typeof errorBody.message === 'string') {
message = errorBody.message;
@@ -46,10 +52,12 @@ export async function installModule(file: File): Promise<Module> {
message = errorBody.message.join(', ');
}
details = errorBody.details;
code = errorBody.code;
diagnostic = errorBody.diagnostic;
} catch {
// Kein JSON in der Antwort – Standardmeldung verwenden.
}
throw new ApiError(response.status, message, details);
throw new ApiError(response.status, message, details, code, diagnostic);
}
const response_ = (await response.json()) as { module: unknown };
@@ -85,8 +93,21 @@ export async function setModuleEnabled(id: string, enabled: boolean): Promise<Mo
return moduleSchema.parse(response.module);
}
export async function removeModule(id: string): Promise<void> {
await apiRequest(`/api/v1/modules/${id}`, { method: 'DELETE' });
export async function removeModule(id: string): Promise<{ cleanupWarning?: string }> {
return apiRequest<{ cleanupWarning?: string }>(`/api/v1/modules/${id}`, { method: 'DELETE' });
}
export async function fetchModuleConfiguration(id: string): Promise<ModuleConfiguration> {
return moduleConfigurationSchema.parse(await apiRequest(`/api/v1/modules/${id}/configuration`));
}
export async function saveModuleConfiguration(
id: string,
input: { values: Record<string, string>; clearKeys: string[] },
): Promise<ModuleConfiguration> {
return moduleConfigurationSchema.parse(await apiRequest(`/api/v1/modules/${id}/configuration`, {
method: 'PATCH', body: input,
}));
}
export async function checkModuleHealth(
@@ -166,6 +187,24 @@ export async function installMarketplaceRepository(
return moduleSchema.parse(response.module);
}
export interface MarketplaceUpdateState {
installedBranch: string;
installedCommit: string | null;
branches: Array<{ name: string; commit: string }>;
}
export async function fetchMarketplaceUpdates(moduleId: string): Promise<MarketplaceUpdateState> {
return apiRequest<MarketplaceUpdateState>(`/api/v1/marketplace/modules/${encodeURIComponent(moduleId)}/updates`);
}
export async function updateMarketplaceModule(moduleId: string, branch: string): Promise<Module> {
const response = await apiRequest<{ module: unknown }>(
`/api/v1/marketplace/modules/${encodeURIComponent(moduleId)}/update`,
{ method: 'POST', body: { branch } },
);
return moduleSchema.parse(response.module);
}
export async function fetchMarketplaceProviders(): Promise<MarketplaceProvider[]> {
return apiRequest<MarketplaceProvider[]>('/api/v1/marketplace/providers');
}

View File

@@ -134,9 +134,25 @@ export const moduleSchema = z.object({
healthcheckUrl: z.string(),
enabled: z.boolean(),
createdAt: z.string(),
configuration: z.array(z.object({
key: z.string(), label: z.string(), description: z.string(),
type: z.enum(['text', 'url', 'boolean']), secret: z.boolean(), required: z.boolean(),
defaultValue: z.string().optional(), services: z.array(z.string()),
})),
configurationReady: z.boolean(),
});
export type Module = z.infer<typeof moduleSchema>;
export const moduleConfigurationSchema = z.object({
ready: z.boolean(),
fields: z.array(z.object({
key: z.string(), label: z.string(), description: z.string(),
type: z.enum(['text', 'url', 'boolean']), secret: z.boolean(), required: z.boolean(),
services: z.array(z.string()), isSet: z.boolean(), value: z.string().optional(),
})),
});
export type ModuleConfiguration = z.infer<typeof moduleConfigurationSchema>;
/** Modul-Kachel für das Dashboard (/api/v1/profile/modules). */
export const accessibleModuleSchema = z.object({
id: z.string(),
@@ -155,4 +171,4 @@ export const modulePermissionSchema = z.object({
moduleName: z.string(),
permission: z.enum(['GRANTED', 'DENIED']),
});
export type ModulePermission = z.infer<typeof modulePermissionSchema>;
export type ModulePermission = z.infer<typeof modulePermissionSchema>;

View File

@@ -0,0 +1,6 @@
import { clsx, type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]): string {
return twMerge(clsx(inputs));
}