feat: Enhance Domains, Environments, Services, and Templates management

- Implemented dialog-based forms for adding and editing Domains and Environments.
- Added delete functionality for Domains and Environments with confirmation prompts.
- Introduced EnvironmentDetailsPage to display details of selected environments and their linked domains.
- Created EnvironmentDomainsPage for linking domains to environments.
- Enhanced ServicesPage with dialog support for adding, editing, and viewing service details.
- Updated TemplatesPage to manage templates with comprehensive form fields and validation.
- Improved type definitions in portal.ts to support new features and ensure type safety.
This commit is contained in:
Torsten Brendgen
2026-05-15 00:05:09 +02:00
parent fdf294cac0
commit 7080d659ef
13 changed files with 1283 additions and 86 deletions

View File

@@ -1,39 +1,305 @@
import { useQuery } from "@tanstack/react-query";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import {
Button,
Checkbox,
Dialog,
DialogActions,
DialogBody,
DialogContent,
DialogSurface,
DialogTitle,
Field,
Input,
makeStyles,
shorthands,
Table,
TableBody,
TableCell,
TableHeader,
TableHeaderCell,
TableRow,
Textarea,
Tooltip,
} from "@fluentui/react-components";
import { AddRegular, DeleteRegular, EditRegular, OpenRegular } from "@fluentui/react-icons";
import { useState } from "react";
import { portalApi } from "../api/portalApi";
import { DataState } from "../components/DataState";
import { PageHeader } from "../components/PageHeader";
import type { Template } from "../types/portal";
const useStyles = makeStyles({
toolbar: {
display: "flex",
justifyContent: "flex-start",
marginBottom: "18px",
},
form: {
display: "grid",
gap: "14px",
},
grid: {
display: "grid",
gap: "14px",
gridTemplateColumns: "repeat(2, minmax(180px, 1fr))",
},
wide: {
gridColumn: "1 / -1",
},
actions: {
display: "flex",
gap: "4px",
...shorthands.padding("2px", "0"),
},
value: {
overflowWrap: "anywhere",
},
});
type DialogMode = "add" | "edit" | "details" | null;
function getTemplateJsonData(template: Template) {
return template.jsonData ?? template.jSONData ?? "";
}
export function TemplatesPage() {
const styles = useStyles();
const queryClient = useQueryClient();
const [dialogMode, setDialogMode] = useState<DialogMode>(null);
const [selectedTemplate, setSelectedTemplate] = useState<Template | null>(null);
const [templateCategoryId, setTemplateCategoryId] = useState("");
const [name, setName] = useState("");
const [cloudTemplate, setCloudTemplate] = useState(false);
const [version, setVersion] = useState("");
const [description, setDescription] = useState("");
const [jsonData, setJsonData] = useState("");
const { data, error, isLoading } = useQuery({
queryKey: ["templates"],
queryFn: ({ signal }) => portalApi.getTemplates(signal),
});
const closeDialog = () => {
setDialogMode(null);
setSelectedTemplate(null);
setTemplateCategoryId("");
setName("");
setCloudTemplate(false);
setVersion("");
setDescription("");
setJsonData("");
};
const openAddDialog = () => {
closeDialog();
setDialogMode("add");
};
const openTemplateDialog = (mode: "edit" | "details", template: Template) => {
setSelectedTemplate(template);
setTemplateCategoryId(template.templateCategoryId ?? "");
setName(template.name);
setCloudTemplate(Boolean(template.cloudTemplate));
setVersion(template.version ?? "");
setDescription(template.description ?? "");
setJsonData(getTemplateJsonData(template));
setDialogMode(mode);
};
const addTemplate = useMutation({
mutationFn: portalApi.addTemplate,
onSuccess: async () => {
closeDialog();
await queryClient.invalidateQueries({ queryKey: ["templates"] });
},
});
const updateTemplate = useMutation({
mutationFn: ({
id,
template,
}: {
id: string;
template: {
templateCategoryId: string;
name: string;
cloudTemplate: boolean;
version: string;
description: string;
jsonData: string;
};
}) => portalApi.updateTemplate(id, template),
onSuccess: async () => {
closeDialog();
await queryClient.invalidateQueries({ queryKey: ["templates"] });
},
});
const deleteTemplate = useMutation({
mutationFn: portalApi.deleteTemplate,
onSuccess: async () => {
await queryClient.invalidateQueries({ queryKey: ["templates"] });
},
});
const submitTemplate = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
const template = { cloudTemplate, description, jsonData, name, templateCategoryId, version };
if (dialogMode === "edit" && selectedTemplate) {
updateTemplate.mutate({ id: selectedTemplate.id, template });
return;
}
addTemplate.mutate(template);
};
const formError = addTemplate.error?.message ?? updateTemplate.error?.message;
const isSaving = addTemplate.isPending || updateTemplate.isPending;
const isDetails = dialogMode === "details";
return (
<>
<PageHeader title="Templates" description="Vorlagen fuer Portal-Bereitstellungen." />
<DataState isLoading={isLoading} error={error} />
<div className={styles.toolbar}>
<Button appearance="primary" icon={<AddRegular />} onClick={openAddDialog}>
Template hinzufuegen
</Button>
</div>
<Dialog open={dialogMode !== null} onOpenChange={(_, data) => !data.open && closeDialog()}>
<DialogSurface>
<form onSubmit={submitTemplate}>
<DialogBody>
<DialogTitle>
{dialogMode === "edit"
? "Template aendern"
: dialogMode === "details"
? "Template Details"
: "Template hinzufuegen"}
</DialogTitle>
<DialogContent className={styles.form}>
<div className={styles.grid}>
<Field label="Name" required>
<Input disabled={isDetails} value={name} onChange={(_, data) => setName(data.value)} />
</Field>
<Field label="Version" required>
<Input
disabled={isDetails}
value={version}
onChange={(_, data) => setVersion(data.value)}
/>
</Field>
<Field className={styles.wide} label="TemplateCategoryId" required validationMessage={formError}>
<Input
disabled={isDetails}
value={templateCategoryId}
onChange={(_, data) => setTemplateCategoryId(data.value)}
/>
</Field>
<Field className={styles.wide}>
<Checkbox
checked={cloudTemplate}
disabled={isDetails}
label="Cloud Template"
onChange={(_, data) => setCloudTemplate(Boolean(data.checked))}
/>
</Field>
<Field className={styles.wide} label="Description">
<Textarea
disabled={isDetails}
resize="vertical"
value={description}
onChange={(_, data) => setDescription(data.value)}
/>
</Field>
<Field className={styles.wide} label="JSONData" required>
<Textarea
disabled={isDetails}
resize="vertical"
value={jsonData}
onChange={(_, data) => setJsonData(data.value)}
/>
</Field>
{selectedTemplate && (
<Field className={styles.wide} label="Id">
<div className={styles.value}>{selectedTemplate.id}</div>
</Field>
)}
</div>
</DialogContent>
<DialogActions>
<Button appearance="secondary" onClick={closeDialog}>
{isDetails ? "Schliessen" : "Abbrechen"}
</Button>
{!isDetails && (
<Button
appearance="primary"
disabled={!name || !templateCategoryId || !version || !jsonData || isSaving}
type="submit"
>
Speichern
</Button>
)}
</DialogActions>
</DialogBody>
</form>
</DialogSurface>
</Dialog>
<DataState isLoading={isLoading} error={error ?? deleteTemplate.error} />
{data && (
<Table aria-label="Templates">
<TableHeader>
<TableRow>
<TableHeaderCell>Name</TableHeaderCell>
<TableHeaderCell>Id</TableHeaderCell>
</TableRow>
<TableRow>
<TableHeaderCell>Name</TableHeaderCell>
<TableHeaderCell>Version</TableHeaderCell>
<TableHeaderCell>Cloud</TableHeaderCell>
<TableHeaderCell>TemplateCategoryId</TableHeaderCell>
<TableHeaderCell>Id</TableHeaderCell>
<TableHeaderCell>Aktionen</TableHeaderCell>
</TableRow>
</TableHeader>
<TableBody>
{data.map((template) => (
<TableRow key={template.id}>
<TableCell>{template.name}</TableCell>
<TableCell>{template.version}</TableCell>
<TableCell>{template.cloudTemplate ? "Ja" : "Nein"}</TableCell>
<TableCell>{template.templateCategoryId}</TableCell>
<TableCell>{template.id}</TableCell>
<TableCell>
<div className={styles.actions}>
<Tooltip content="Details" relationship="label">
<Button
appearance="subtle"
aria-label="Details"
icon={<OpenRegular />}
onClick={() => openTemplateDialog("details", template)}
/>
</Tooltip>
<Tooltip content="Aendern" relationship="label">
<Button
appearance="subtle"
aria-label="Aendern"
icon={<EditRegular />}
onClick={() => openTemplateDialog("edit", template)}
/>
</Tooltip>
<Tooltip content="Loeschen" relationship="label">
<Button
appearance="subtle"
aria-label="Loeschen"
disabled={deleteTemplate.isPending}
icon={<DeleteRegular />}
onClick={() => {
if (window.confirm(`Template "${template.name}" wirklich loeschen?`)) {
deleteTemplate.mutate(template.id);
}
}}
/>
</Tooltip>
</div>
</TableCell>
</TableRow>
))}
</TableBody>