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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user