feat: Refactor deployment types and add new deployment rules management
- Updated DeploymentExecution and related types to use targetId instead of virtualMachineId. - Introduced new types for DeploymentRule, DeploymentRuleStep, and related entities. - Added DeploymentRulesPage for managing deployment rules with CRUD operations. - Created TargetDetailsPage and TargetsPage for managing targets with linking and unlinking functionality. - Enhanced UI components with Fluent UI for better user experience. - Implemented data fetching and state management using React Query.
This commit is contained in:
438
src/pages/TargetsPage.tsx
Normal file
438
src/pages/TargetsPage.tsx
Normal file
@@ -0,0 +1,438 @@
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
Button,
|
||||
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 { 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 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);
|
||||
|
||||
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>
|
||||
</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 ?? linkTargetToDomain.error ?? unlinkTargetFromDomain.error}
|
||||
/>
|
||||
{data && (
|
||||
<Table aria-label="Targets">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<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>{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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user