- Implemented checkbox selection for multiple items in DeploymentRulesPage, DomainsPage, EnvironmentsPage, ServicesPage, TargetsPage, TemplateCategoriesPage, and TemplatesPage. - Added delete functionality for selected items with confirmation prompts. - Updated state management to handle selected IDs for batch operations. - Enhanced UI with a toolbar for actions related to selected items. - Adjusted API calls to accommodate changes in data structure, including deployment requests and job details.
354 lines
12 KiB
TypeScript
354 lines
12 KiB
TypeScript
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
import {
|
|
Button,
|
|
Checkbox,
|
|
Combobox,
|
|
Dialog,
|
|
DialogActions,
|
|
DialogBody,
|
|
DialogContent,
|
|
DialogSurface,
|
|
DialogTitle,
|
|
Field,
|
|
Input,
|
|
makeStyles,
|
|
Option,
|
|
shorthands,
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableHeader,
|
|
TableHeaderCell,
|
|
TableRow,
|
|
Tooltip,
|
|
} from "@fluentui/react-components";
|
|
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";
|
|
import { DataState } from "../components/DataState";
|
|
import { PageHeader } from "../components/PageHeader";
|
|
import type { Domain } from "../types/portal";
|
|
|
|
const useStyles = makeStyles({
|
|
toolbar: {
|
|
display: "flex",
|
|
justifyContent: "flex-start",
|
|
gap: "10px",
|
|
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("");
|
|
const [linkDomainId, setLinkDomainId] = useState("");
|
|
const [environmentId, setEnvironmentId] = useState("");
|
|
const [selectedIds, setSelectedIds] = useState<string[]>([]);
|
|
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);
|
|
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 openLinkDialog = () => {
|
|
setLinkDomainId(data?.[0]?.id ?? "");
|
|
setEnvironmentId(environments?.[0]?.id ?? "");
|
|
};
|
|
|
|
const addDomain = useMutation({
|
|
mutationFn: portalApi.addDomain,
|
|
onSuccess: async () => {
|
|
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 deleteSelectedDomains = useMutation({
|
|
mutationFn: async (ids: string[]) => {
|
|
await Promise.all(ids.map((id) => portalApi.deleteDomain(id)));
|
|
},
|
|
onSuccess: async () => {
|
|
setSelectedIds([]);
|
|
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();
|
|
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;
|
|
const allSelected = Boolean(data?.length) && data!.every((domain) => selectedIds.includes(domain.id));
|
|
const toggleSelected = (id: string, checked: boolean) => {
|
|
setSelectedIds((current) => (checked ? [...new Set([...current, id])] : current.filter((entry) => entry !== id)));
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<PageHeader title="Domains" description="Verfuegbare Active Directory Domaenen." />
|
|
<div className={styles.toolbar}>
|
|
<Button appearance="primary" icon={<AddRegular />} onClick={openAddDialog}>
|
|
Domain hinzufuegen
|
|
</Button>
|
|
<Button appearance="secondary" icon={<LinkMultiple24Regular />} onClick={openLinkDialog}>
|
|
Link to Environment
|
|
</Button>
|
|
<Button
|
|
appearance="secondary"
|
|
disabled={selectedIds.length === 0 || deleteSelectedDomains.isPending}
|
|
icon={<DeleteRegular />}
|
|
onClick={() => {
|
|
if (window.confirm(`${selectedIds.length} Domains wirklich loeschen?`)) {
|
|
deleteSelectedDomains.mutate(selectedIds);
|
|
}
|
|
}}
|
|
>
|
|
Auswahl loeschen
|
|
</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>
|
|
<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 || environmentsLoading}
|
|
error={error ?? environmentsError ?? deleteDomain.error ?? deleteSelectedDomains.error ?? linkDomainToEnvironment.error}
|
|
/>
|
|
{data && (
|
|
<Table aria-label="Domains">
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHeaderCell>
|
|
<Checkbox
|
|
checked={allSelected}
|
|
onChange={(_, checkboxData) => setSelectedIds(checkboxData.checked ? data.map((domain) => domain.id) : [])}
|
|
/>
|
|
</TableHeaderCell>
|
|
<TableHeaderCell>Name</TableHeaderCell>
|
|
<TableHeaderCell>FQDN</TableHeaderCell>
|
|
<TableHeaderCell>NetBIOS</TableHeaderCell>
|
|
<TableHeaderCell>Id</TableHeaderCell>
|
|
<TableHeaderCell>Aktionen</TableHeaderCell>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{data.map((domain) => (
|
|
<TableRow key={domain.id}>
|
|
<TableCell>
|
|
<Checkbox
|
|
checked={selectedIds.includes(domain.id)}
|
|
onChange={(_, checkboxData) => toggleSelected(domain.id, Boolean(checkboxData.checked))}
|
|
/>
|
|
</TableCell>
|
|
<TableCell>{domain.name}</TableCell>
|
|
<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>
|
|
</Table>
|
|
)}
|
|
</>
|
|
);
|
|
}
|
|
|