- 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.
479 lines
18 KiB
TypeScript
479 lines
18 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,
|
|
Textarea,
|
|
Tooltip,
|
|
} from "@fluentui/react-components";
|
|
import { AddRegular, DeleteRegular, EditRegular, LinkDismissRegular, LinkRegular, 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 { Domain, Target } from "../types/portal";
|
|
import { Link } from "react-router-dom";
|
|
|
|
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"),
|
|
},
|
|
value: {
|
|
overflowWrap: "anywhere",
|
|
},
|
|
});
|
|
|
|
type DialogMode = "add" | "edit" | null;
|
|
|
|
export function TargetsPage() {
|
|
const styles = useStyles();
|
|
const queryClient = useQueryClient();
|
|
const [dialogMode, setDialogMode] = useState<DialogMode>(null);
|
|
const [selectedTarget, setSelectedTarget] = useState<Target | null>(null);
|
|
const [domainID, setDomainID] = useState<string | undefined>(undefined);
|
|
const [linkTargetId, setLinkTargetId] = useState("");
|
|
const [linkDomainId, setLinkDomainId] = useState("");
|
|
const [unlinkTargetId, setUnlinkTargetId] = useState("");
|
|
const [name, setName] = useState("");
|
|
const [targetType, setTargetType] = useState("VirtualMachine");
|
|
const [providerType, setProviderType] = useState("OnPrem");
|
|
const [externalId, setExternalId] = useState("");
|
|
const [metadataJson, setMetadataJson] = useState("");
|
|
const [selectedIds, setSelectedIds] = useState<string[]>([]);
|
|
|
|
const { data, error, isLoading } = useQuery({
|
|
queryKey: ["targets"],
|
|
queryFn: ({ signal }) => portalApi.getTargets(signal),
|
|
});
|
|
const { data: domains, error: domainsError, isLoading: domainsLoading } = useQuery({
|
|
queryKey: ["domains"],
|
|
queryFn: ({ signal }) => portalApi.getDomains(signal),
|
|
});
|
|
|
|
const closeDialog = () => {
|
|
setDialogMode(null);
|
|
setSelectedTarget(null);
|
|
setDomainID(undefined);
|
|
setName("");
|
|
setTargetType("VirtualMachine");
|
|
setProviderType("OnPrem");
|
|
setExternalId("");
|
|
setMetadataJson("");
|
|
};
|
|
|
|
const openAddDialog = () => {
|
|
setSelectedTarget(null);
|
|
setDomainID(undefined);
|
|
setName("");
|
|
setTargetType("VirtualMachine");
|
|
setProviderType("OnPrem");
|
|
setExternalId("");
|
|
setMetadataJson("");
|
|
setDialogMode("add");
|
|
};
|
|
|
|
const openTargetDialog = (mode: "edit", target: Target) => {
|
|
setSelectedTarget(target);
|
|
setDomainID(target.domainID);
|
|
setName(target.name);
|
|
setTargetType(target.targetType ?? "VirtualMachine");
|
|
setProviderType(target.providerType ?? "OnPrem");
|
|
setExternalId(target.externalId ?? "");
|
|
setMetadataJson(target.metadataJson ?? "");
|
|
setDialogMode(mode);
|
|
};
|
|
|
|
const addTarget = useMutation({
|
|
mutationFn: portalApi.addTarget,
|
|
onSuccess: async () => {
|
|
closeDialog();
|
|
await queryClient.invalidateQueries({ queryKey: ["targets"] });
|
|
},
|
|
});
|
|
|
|
const updateTarget = useMutation({
|
|
mutationFn: ({ id, target }: { id: string; target: { name: string; targetType: string; providerType: string; externalId?: string; metadataJson?: string } }) =>
|
|
portalApi.updateTarget(id, target),
|
|
onSuccess: async () => {
|
|
closeDialog();
|
|
await queryClient.invalidateQueries({ queryKey: ["targets"] });
|
|
},
|
|
});
|
|
|
|
const deleteTarget = useMutation({
|
|
mutationFn: portalApi.deleteTarget,
|
|
onSuccess: async () => {
|
|
await queryClient.invalidateQueries({ queryKey: ["targets"] });
|
|
},
|
|
});
|
|
const deleteSelectedTargets = useMutation({
|
|
mutationFn: async (ids: string[]) => {
|
|
await Promise.all(ids.map((id) => portalApi.deleteTarget(id)));
|
|
},
|
|
onSuccess: async () => {
|
|
setSelectedIds([]);
|
|
await queryClient.invalidateQueries({ queryKey: ["targets"] });
|
|
},
|
|
});
|
|
|
|
const linkTargetToDomain = useMutation({
|
|
mutationFn: ({ targetId, targetDomainId }: { targetId: string; targetDomainId: string }) =>
|
|
portalApi.linkTargetToDomain(targetId, targetDomainId),
|
|
onSuccess: async () => {
|
|
setLinkTargetId("");
|
|
setLinkDomainId("");
|
|
await queryClient.invalidateQueries({ queryKey: ["targets"] });
|
|
},
|
|
});
|
|
|
|
const unlinkTargetFromDomain = useMutation({
|
|
mutationFn: (targetId: string) => portalApi.unlinkTargetFromDomain(targetId),
|
|
onSuccess: async () => {
|
|
setUnlinkTargetId("");
|
|
await queryClient.invalidateQueries({ queryKey: ["targets"] });
|
|
},
|
|
});
|
|
|
|
const submitTarget = (event: React.FormEvent<HTMLFormElement>) => {
|
|
event.preventDefault();
|
|
const target = { domainID, name, targetType, providerType, externalId, metadataJson };
|
|
|
|
if (dialogMode === "edit" && selectedTarget) {
|
|
updateTarget.mutate({ id: selectedTarget.id, target: { name, targetType, providerType, externalId, metadataJson } });
|
|
return;
|
|
}
|
|
|
|
addTarget.mutate(target);
|
|
};
|
|
|
|
const formError =
|
|
addTarget.error?.message ??
|
|
updateTarget.error?.message ??
|
|
linkTargetToDomain.error?.message ??
|
|
unlinkTargetFromDomain.error?.message;
|
|
const isSaving = addTarget.isPending || updateTarget.isPending;
|
|
const domainNameById = new Map((domains ?? []).map((domain: Domain) => [domain.id, domain.name]));
|
|
const linkedTargets = (data ?? []).filter((target) => Boolean(target.domainID));
|
|
const unlinkedTargets = (data ?? []).filter((target) => !target.domainID);
|
|
const allSelected = Boolean(data?.length) && data!.every((target) => selectedIds.includes(target.id));
|
|
const toggleSelected = (id: string, checked: boolean) => {
|
|
setSelectedIds((current) => (checked ? [...new Set([...current, id])] : current.filter((entry) => entry !== id)));
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<PageHeader title="Targets" description="Generische Ziele fuer On-Prem-, Hybrid- und Cloud-Deployments verwalten." />
|
|
<div className={styles.toolbar}>
|
|
<Button appearance="primary" icon={<AddRegular />} onClick={openAddDialog}>
|
|
Target hinzufuegen
|
|
</Button>
|
|
<Button
|
|
appearance="secondary"
|
|
icon={<LinkRegular />}
|
|
onClick={() => {
|
|
setLinkTargetId(unlinkedTargets[0]?.id ?? "");
|
|
setLinkDomainId(domains?.[0]?.id ?? "");
|
|
}}
|
|
disabled={!unlinkedTargets.length || !domains?.length}
|
|
style={{ marginLeft: "10px" }}
|
|
>
|
|
Link to Domain
|
|
</Button>
|
|
<Button
|
|
appearance="secondary"
|
|
icon={<LinkDismissRegular />}
|
|
onClick={() => {
|
|
setUnlinkTargetId(linkedTargets[0]?.id ?? "");
|
|
}}
|
|
disabled={!linkedTargets.length}
|
|
style={{ marginLeft: "10px" }}
|
|
>
|
|
Unlink Domain
|
|
</Button>
|
|
<Button
|
|
appearance="secondary"
|
|
disabled={selectedIds.length === 0 || deleteSelectedTargets.isPending}
|
|
icon={<DeleteRegular />}
|
|
onClick={() => {
|
|
if (window.confirm(`${selectedIds.length} Targets wirklich loeschen?`)) {
|
|
deleteSelectedTargets.mutate(selectedIds);
|
|
}
|
|
}}
|
|
style={{ marginLeft: "10px" }}
|
|
>
|
|
Auswahl loeschen
|
|
</Button>
|
|
</div>
|
|
|
|
<Dialog open={dialogMode !== null} onOpenChange={(_, dialogData) => !dialogData.open && closeDialog()}>
|
|
<DialogSurface>
|
|
<form onSubmit={submitTarget}>
|
|
<DialogBody>
|
|
<DialogTitle>
|
|
{dialogMode === "edit"
|
|
? "Target aendern"
|
|
: "Target hinzufuegen"}
|
|
</DialogTitle>
|
|
<DialogContent className={styles.form}>
|
|
<Field label="Domain (optional)" validationMessage={formError}>
|
|
<Combobox
|
|
disabled={domainsLoading}
|
|
placeholder="Domain waehlen"
|
|
value={domainID ? (domainNameById.get(domainID) ?? "") : ""}
|
|
onOptionSelect={(_, optionData) => setDomainID(optionData.optionValue || undefined)}
|
|
>
|
|
{(domains ?? []).map((domain) => (
|
|
<Option key={domain.id} text={domain.name} value={domain.id}>
|
|
{domain.name}
|
|
</Option>
|
|
))}
|
|
</Combobox>
|
|
</Field>
|
|
<Field label="Name" required>
|
|
<Input value={name} onChange={(_, inputData) => setName(inputData.value)} />
|
|
</Field>
|
|
<Field label="Target Type" required>
|
|
<Combobox
|
|
value={targetType}
|
|
onOptionSelect={(_, optionData) => setTargetType(optionData.optionValue ?? "VirtualMachine")}
|
|
>
|
|
{["VirtualMachine", "Tenant", "Subscription", "ResourceGroup", "User", "Group", "Site", "PolicyScope"].map((type) => (
|
|
<Option key={type} text={type} value={type}>
|
|
{type}
|
|
</Option>
|
|
))}
|
|
</Combobox>
|
|
</Field>
|
|
<Field label="Provider Type" required>
|
|
<Combobox
|
|
value={providerType}
|
|
onOptionSelect={(_, optionData) => setProviderType(optionData.optionValue ?? "OnPrem")}
|
|
>
|
|
{["OnPrem", "Azure", "Microsoft365", "ExchangeOnline", "Teams"].map((provider) => (
|
|
<Option key={provider} text={provider} value={provider}>
|
|
{provider}
|
|
</Option>
|
|
))}
|
|
</Combobox>
|
|
</Field>
|
|
<Field label="External Id">
|
|
<Input
|
|
value={externalId}
|
|
onChange={(_, inputData) => setExternalId(inputData.value)}
|
|
/>
|
|
</Field>
|
|
<Field label="Metadata JSON">
|
|
<Textarea
|
|
resize="vertical"
|
|
value={metadataJson}
|
|
onChange={(_, inputData) => setMetadataJson(inputData.value)}
|
|
/>
|
|
</Field>
|
|
{selectedTarget && (
|
|
<Field label="Id">
|
|
<div className={styles.value}>{selectedTarget.id}</div>
|
|
</Field>
|
|
)}
|
|
</DialogContent>
|
|
<DialogActions>
|
|
<Button appearance="secondary" onClick={closeDialog}>
|
|
Abbrechen
|
|
</Button>
|
|
<Button appearance="primary" disabled={!name || isSaving} type="submit">
|
|
Speichern
|
|
</Button>
|
|
</DialogActions>
|
|
</DialogBody>
|
|
</form>
|
|
</DialogSurface>
|
|
</Dialog>
|
|
|
|
<Dialog open={Boolean(linkTargetId)} onOpenChange={(_, dialogData) => !dialogData.open && setLinkTargetId("")}>
|
|
<DialogSurface>
|
|
<DialogBody>
|
|
<DialogTitle>Link Target to Domain</DialogTitle>
|
|
<DialogContent className={styles.form}>
|
|
<Field label="Target" required>
|
|
<Combobox
|
|
placeholder="Target waehlen"
|
|
value={(data ?? []).find((target) => target.id === linkTargetId)?.name ?? ""}
|
|
onOptionSelect={(_, optionData) => setLinkTargetId(optionData.optionValue ?? "")}
|
|
>
|
|
{unlinkedTargets.map((target) => (
|
|
<Option key={target.id} text={target.name} value={target.id}>
|
|
{target.name}
|
|
</Option>
|
|
))}
|
|
</Combobox>
|
|
</Field>
|
|
<Field label="Domain" required>
|
|
<Combobox
|
|
placeholder="Domain waehlen"
|
|
value={domains?.find((domain) => domain.id === linkDomainId)?.name ?? ""}
|
|
onOptionSelect={(_, optionData) => setLinkDomainId(optionData.optionValue ?? "")}
|
|
>
|
|
{(domains ?? []).map((domain) => (
|
|
<Option key={domain.id} text={domain.name} value={domain.id}>
|
|
{domain.name}
|
|
</Option>
|
|
))}
|
|
</Combobox>
|
|
</Field>
|
|
</DialogContent>
|
|
<DialogActions>
|
|
<Button appearance="secondary" onClick={() => setLinkTargetId("")}>
|
|
Abbrechen
|
|
</Button>
|
|
<Button
|
|
appearance="primary"
|
|
disabled={!linkTargetId || !linkDomainId || linkTargetToDomain.isPending}
|
|
onClick={() => linkTargetToDomain.mutate({ targetId: linkTargetId, targetDomainId: linkDomainId })}
|
|
>
|
|
Link
|
|
</Button>
|
|
</DialogActions>
|
|
</DialogBody>
|
|
</DialogSurface>
|
|
</Dialog>
|
|
|
|
<Dialog open={Boolean(unlinkTargetId)} onOpenChange={(_, dialogData) => !dialogData.open && setUnlinkTargetId("")}>
|
|
<DialogSurface>
|
|
<DialogBody>
|
|
<DialogTitle>Unlink Target from Domain</DialogTitle>
|
|
<DialogContent className={styles.form}>
|
|
<Field label="Target" required>
|
|
<Combobox
|
|
placeholder="Target waehlen"
|
|
value={(data ?? []).find((target) => target.id === unlinkTargetId)?.name ?? ""}
|
|
onOptionSelect={(_, optionData) => setUnlinkTargetId(optionData.optionValue ?? "")}
|
|
>
|
|
{linkedTargets.map((target) => (
|
|
<Option key={target.id} text={target.name} value={target.id}>
|
|
{target.name}
|
|
</Option>
|
|
))}
|
|
</Combobox>
|
|
</Field>
|
|
</DialogContent>
|
|
<DialogActions>
|
|
<Button appearance="secondary" onClick={() => setUnlinkTargetId("")}>
|
|
Abbrechen
|
|
</Button>
|
|
<Button
|
|
appearance="primary"
|
|
disabled={!unlinkTargetId || unlinkTargetFromDomain.isPending}
|
|
onClick={() => unlinkTargetFromDomain.mutate(unlinkTargetId)}
|
|
>
|
|
Unlink
|
|
</Button>
|
|
</DialogActions>
|
|
</DialogBody>
|
|
</DialogSurface>
|
|
</Dialog>
|
|
|
|
<DataState
|
|
isLoading={isLoading || domainsLoading}
|
|
error={error ?? domainsError ?? deleteTarget.error ?? deleteSelectedTargets.error ?? linkTargetToDomain.error ?? unlinkTargetFromDomain.error}
|
|
/>
|
|
{data && (
|
|
<Table aria-label="Targets">
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHeaderCell>
|
|
<Checkbox
|
|
checked={allSelected}
|
|
onChange={(_, checkboxData) => setSelectedIds(checkboxData.checked ? data.map((target) => target.id) : [])}
|
|
/>
|
|
</TableHeaderCell>
|
|
<TableHeaderCell>Name</TableHeaderCell>
|
|
<TableHeaderCell>Type</TableHeaderCell>
|
|
<TableHeaderCell>Provider</TableHeaderCell>
|
|
<TableHeaderCell>Domain</TableHeaderCell>
|
|
<TableHeaderCell>External Id</TableHeaderCell>
|
|
<TableHeaderCell>Id</TableHeaderCell>
|
|
<TableHeaderCell>Aktionen</TableHeaderCell>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{data.map((target) => (
|
|
<TableRow key={target.id}>
|
|
<TableCell>
|
|
<Checkbox
|
|
checked={selectedIds.includes(target.id)}
|
|
onChange={(_, checkboxData) => toggleSelected(target.id, Boolean(checkboxData.checked))}
|
|
/>
|
|
</TableCell>
|
|
<TableCell>{target.name}</TableCell>
|
|
<TableCell>{target.targetType}</TableCell>
|
|
<TableCell>{target.providerType}</TableCell>
|
|
<TableCell>{target.domainID ? (domainNameById.get(target.domainID) ?? target.domainID) : "-"}</TableCell>
|
|
<TableCell>{target.externalId}</TableCell>
|
|
<TableCell>{target.id}</TableCell>
|
|
<TableCell>
|
|
<div className={styles.actions}>
|
|
<Tooltip content="Details" relationship="label">
|
|
<Link to={`/targets/${target.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={() => openTargetDialog("edit", target)}
|
|
/>
|
|
</Tooltip>
|
|
<Tooltip content="Loeschen" relationship="label">
|
|
<Button
|
|
appearance="subtle"
|
|
aria-label="Loeschen"
|
|
disabled={deleteTarget.isPending}
|
|
icon={<DeleteRegular />}
|
|
onClick={() => {
|
|
if (window.confirm(`Target "${target.name}" wirklich loeschen?`)) {
|
|
deleteTarget.mutate(target.id);
|
|
}
|
|
}}
|
|
/>
|
|
</Tooltip>
|
|
</div>
|
|
</TableCell>
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
)}
|
|
</>
|
|
);
|
|
}
|
|
|