Files
Microsoft.SelfService.Porta…/src/pages/DomainsPage.tsx
Torsten Brendgen 1894f740b0 feat: add multi-select functionality across various pages
- 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.
2026-07-09 23:49:10 +02:00

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>
)}
</>
);
}