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,23 +1,56 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import {
Button,
Dialog,
DialogActions,
DialogBody,
DialogContent,
DialogSurface,
DialogTitle,
Field,
Input,
makeStyles,
shorthands,
Table,
TableBody,
TableCell,
TableHeader,
TableHeaderCell,
TableRow,
Tooltip,
} from "@fluentui/react-components";
import { AddRegular, DeleteRegular, EditRegular, OpenRegular } from "@fluentui/react-icons";
import { useState } from "react";
import { Link } from "react-router-dom";
import { portalApi } from "../api/portalApi";
import { DataState } from "../components/DataState";
import { FormActions, FormGrid, FormSection } from "../components/FormSection";
import { PageHeader } from "../components/PageHeader";
import type { Domain } from "../types/portal";
const useStyles = makeStyles({
toolbar: {
display: "flex",
justifyContent: "flex-start",
marginBottom: "18px",
},
form: {
display: "grid",
gap: "14px",
},
actions: {
display: "flex",
gap: "4px",
...shorthands.padding("2px", "0"),
},
});
type DialogMode = "add" | "edit" | null;
export function DomainsPage() {
const styles = useStyles();
const queryClient = useQueryClient();
const [dialogMode, setDialogMode] = useState<DialogMode>(null);
const [selectedDomain, setSelectedDomain] = useState<Domain | null>(null);
const [name, setName] = useState("");
const [fqdn, setFqdn] = useState("");
const [netBIOS, setNetBIOS] = useState("");
@@ -25,47 +58,109 @@ export function DomainsPage() {
queryKey: ["domains"],
queryFn: ({ signal }) => portalApi.getDomains(signal),
});
const closeDialog = () => {
setDialogMode(null);
setSelectedDomain(null);
setName("");
setFqdn("");
setNetBIOS("");
};
const openAddDialog = () => {
setSelectedDomain(null);
setName("");
setFqdn("");
setNetBIOS("");
setDialogMode("add");
};
const openEditDialog = (domain: Domain) => {
setSelectedDomain(domain);
setName(domain.name);
setFqdn(domain.fqdn);
setNetBIOS(domain.netBIOS);
setDialogMode("edit");
};
const addDomain = useMutation({
mutationFn: portalApi.addDomain,
onSuccess: async () => {
setName("");
setFqdn("");
setNetBIOS("");
closeDialog();
await queryClient.invalidateQueries({ queryKey: ["domains"] });
},
});
const updateDomain = useMutation({
mutationFn: ({ id, domain }: { id: string; domain: { name: string; fqdn: string; netBIOS: string } }) =>
portalApi.updateDomain(id, domain),
onSuccess: async () => {
closeDialog();
await queryClient.invalidateQueries({ queryKey: ["domains"] });
},
});
const deleteDomain = useMutation({
mutationFn: portalApi.deleteDomain,
onSuccess: async () => {
await queryClient.invalidateQueries({ queryKey: ["domains"] });
},
});
const submitDomain = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
const domain = { fqdn, name, netBIOS };
if (dialogMode === "edit" && selectedDomain) {
updateDomain.mutate({ id: selectedDomain.id, domain });
return;
}
addDomain.mutate(domain);
};
const formError = addDomain.error?.message ?? updateDomain.error?.message;
const isSaving = addDomain.isPending || updateDomain.isPending;
return (
<>
<PageHeader title="Domains" description="Verfuegbare Active Directory Domaenen." />
<FormSection
onSubmit={(event) => {
event.preventDefault();
addDomain.mutate({ fqdn, name, netBIOS });
}}
>
<FormGrid>
<Field label="Name" required>
<Input value={name} onChange={(_, data) => setName(data.value)} />
</Field>
<Field label="FQDN" required>
<Input value={fqdn} onChange={(_, data) => setFqdn(data.value)} />
</Field>
<Field label="NetBIOS" required validationMessage={addDomain.error?.message}>
<Input value={netBIOS} onChange={(_, data) => setNetBIOS(data.value)} />
</Field>
</FormGrid>
<FormActions>
<Button
appearance="primary"
disabled={!name || !fqdn || !netBIOS || addDomain.isPending}
type="submit"
>
Add domain
</Button>
</FormActions>
</FormSection>
<DataState isLoading={isLoading} error={error} />
<div className={styles.toolbar}>
<Button appearance="primary" icon={<AddRegular />} onClick={openAddDialog}>
Domain hinzufuegen
</Button>
</div>
<Dialog open={dialogMode !== null} onOpenChange={(_, data) => !data.open && closeDialog()}>
<DialogSurface>
<form onSubmit={submitDomain}>
<DialogBody>
<DialogTitle>{dialogMode === "edit" ? "Domain aendern" : "Domain hinzufuegen"}</DialogTitle>
<DialogContent className={styles.form}>
<Field label="Name" required>
<Input value={name} onChange={(_, data) => setName(data.value)} />
</Field>
<Field label="FQDN" required>
<Input value={fqdn} onChange={(_, data) => setFqdn(data.value)} />
</Field>
<Field label="NetBIOS" required validationMessage={formError}>
<Input value={netBIOS} onChange={(_, data) => setNetBIOS(data.value)} />
</Field>
</DialogContent>
<DialogActions>
<Button appearance="secondary" onClick={closeDialog}>
Abbrechen
</Button>
<Button appearance="primary" disabled={!name || !fqdn || !netBIOS || isSaving} type="submit">
Speichern
</Button>
</DialogActions>
</DialogBody>
</form>
</DialogSurface>
</Dialog>
<DataState isLoading={isLoading} error={error ?? deleteDomain.error} />
{data && (
<Table aria-label="Domains">
<TableHeader>
@@ -74,6 +169,7 @@ export function DomainsPage() {
<TableHeaderCell>FQDN</TableHeaderCell>
<TableHeaderCell>NetBIOS</TableHeaderCell>
<TableHeaderCell>Id</TableHeaderCell>
<TableHeaderCell>Aktionen</TableHeaderCell>
</TableRow>
</TableHeader>
<TableBody>
@@ -83,6 +179,36 @@ export function DomainsPage() {
<TableCell>{domain.fqdn}</TableCell>
<TableCell>{domain.netBIOS}</TableCell>
<TableCell>{domain.id}</TableCell>
<TableCell>
<div className={styles.actions}>
<Tooltip content="Details" relationship="label">
<Link to={`/domains/${domain.id}`}>
<Button appearance="subtle" aria-label="Details" icon={<OpenRegular />} />
</Link>
</Tooltip>
<Tooltip content="Aendern" relationship="label">
<Button
appearance="subtle"
aria-label="Aendern"
icon={<EditRegular />}
onClick={() => openEditDialog(domain)}
/>
</Tooltip>
<Tooltip content="Loeschen" relationship="label">
<Button
appearance="subtle"
aria-label="Loeschen"
disabled={deleteDomain.isPending}
icon={<DeleteRegular />}
onClick={() => {
if (window.confirm(`Domain "${domain.name}" wirklich loeschen?`)) {
deleteDomain.mutate(domain.id);
}
}}
/>
</Tooltip>
</div>
</TableCell>
</TableRow>
))}
</TableBody>