feat: add Deployment Jobs, Template Categories, Virtual Machine details and management pages

- Implemented DeploymentJobsPage for managing deployment jobs with filtering and retry functionality.
- Created TemplateCategoriesPage for managing template categories with add, edit, and delete capabilities.
- Developed VirtualMachineDetailsPage to display detailed information about a specific virtual machine.
- Added VirtualMachinesPage for listing, adding, editing, and deleting virtual machines, including linking and unlinking to domains.
This commit is contained in:
Torsten Brendgen
2026-05-16 16:44:38 +02:00
parent 7080d659ef
commit 08bcd60746
19 changed files with 3265 additions and 208 deletions

View File

@@ -1,6 +1,7 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import {
Button,
Combobox,
Dialog,
DialogActions,
DialogBody,
@@ -10,6 +11,7 @@ import {
Field,
Input,
makeStyles,
Option,
shorthands,
Table,
TableBody,
@@ -19,7 +21,7 @@ import {
TableRow,
Tooltip,
} from "@fluentui/react-components";
import { AddRegular, DeleteRegular, EditRegular, OpenRegular } from "@fluentui/react-icons";
import { AddRegular, DeleteRegular, EditRegular, OpenRegular, LinkMultiple24Regular } from "@fluentui/react-icons";
import { useState } from "react";
import { Link } from "react-router-dom";
import { portalApi } from "../api/portalApi";
@@ -31,6 +33,7 @@ const useStyles = makeStyles({
toolbar: {
display: "flex",
justifyContent: "flex-start",
gap: "10px",
marginBottom: "18px",
},
form: {
@@ -54,10 +57,16 @@ export function DomainsPage() {
const [name, setName] = useState("");
const [fqdn, setFqdn] = useState("");
const [netBIOS, setNetBIOS] = useState("");
const [linkDomainId, setLinkDomainId] = useState("");
const [environmentId, setEnvironmentId] = useState("");
const { data, error, isLoading } = useQuery({
queryKey: ["domains"],
queryFn: ({ signal }) => portalApi.getDomains(signal),
});
const { data: environments, error: environmentsError, isLoading: environmentsLoading } = useQuery({
queryKey: ["environments"],
queryFn: ({ signal }) => portalApi.getEnvironments(signal),
});
const closeDialog = () => {
setDialogMode(null);
@@ -83,6 +92,11 @@ export function DomainsPage() {
setDialogMode("edit");
};
const openLinkDialog = () => {
setLinkDomainId(data?.[0]?.id ?? "");
setEnvironmentId(environments?.[0]?.id ?? "");
};
const addDomain = useMutation({
mutationFn: portalApi.addDomain,
onSuccess: async () => {
@@ -106,6 +120,16 @@ export function DomainsPage() {
await queryClient.invalidateQueries({ queryKey: ["domains"] });
},
});
const linkDomainToEnvironment = useMutation({
mutationFn: ({ domainId, environmentId: targetEnvironmentId }: { domainId: string; environmentId: string }) =>
portalApi.linkDomainToEnvironment(domainId, targetEnvironmentId),
onSuccess: async () => {
setLinkDomainId("");
setEnvironmentId("");
await queryClient.invalidateQueries({ queryKey: ["domains"] });
await queryClient.invalidateQueries({ queryKey: ["environments"] });
},
});
const submitDomain = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
@@ -129,6 +153,9 @@ export function DomainsPage() {
<Button appearance="primary" icon={<AddRegular />} onClick={openAddDialog}>
Domain hinzufuegen
</Button>
<Button appearance="secondary" icon={<LinkMultiple24Regular />} onClick={openLinkDialog}>
Link to Environment
</Button>
</div>
<Dialog open={dialogMode !== null} onOpenChange={(_, data) => !data.open && closeDialog()}>
@@ -159,8 +186,72 @@ export function DomainsPage() {
</form>
</DialogSurface>
</Dialog>
<Dialog
open={Boolean(linkDomainId || environmentId)}
onOpenChange={(_, dialogData) => {
if (!dialogData.open) {
setLinkDomainId("");
setEnvironmentId("");
}
}}
>
<DialogSurface>
<DialogBody>
<DialogTitle>Link to Environment</DialogTitle>
<DialogContent className={styles.form}>
<Field label="Domain" required>
<Combobox
placeholder="Domain waehlen"
value={data?.find((domain) => domain.id === linkDomainId)?.name ?? ""}
onOptionSelect={(_, optionData) => setLinkDomainId(optionData.optionValue ?? "")}
>
{(data ?? []).map((domain) => (
<Option key={domain.id} text={domain.name} value={domain.id}>
{domain.name}
</Option>
))}
</Combobox>
</Field>
<Field label="Environment" required validationMessage={linkDomainToEnvironment.error?.message}>
<Combobox
disabled={environmentsLoading}
placeholder="Environment waehlen"
value={environments?.find((environment) => environment.id === environmentId)?.name ?? ""}
onOptionSelect={(_, data) => setEnvironmentId(data.optionValue ?? "")}
>
{(environments ?? []).map((environment) => (
<Option key={environment.id} text={environment.name} value={environment.id}>
{environment.name}
</Option>
))}
</Combobox>
</Field>
</DialogContent>
<DialogActions>
<Button
appearance="secondary"
onClick={() => {
setLinkDomainId("");
setEnvironmentId("");
}}
>
Abbrechen
</Button>
<Button
appearance="primary"
disabled={!linkDomainId || !environmentId || linkDomainToEnvironment.isPending}
onClick={() => {
linkDomainToEnvironment.mutate({ domainId: linkDomainId, environmentId });
}}
>
Link
</Button>
</DialogActions>
</DialogBody>
</DialogSurface>
</Dialog>
<DataState isLoading={isLoading} error={error ?? deleteDomain.error} />
<DataState isLoading={isLoading || environmentsLoading} error={error ?? environmentsError ?? deleteDomain.error ?? linkDomainToEnvironment.error} />
{data && (
<Table aria-label="Domains">
<TableHeader>