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:
Torsten Brendgen
2026-07-07 23:30:08 +02:00
parent 08bcd60746
commit 3083e81452
27 changed files with 1492 additions and 399 deletions

View File

@@ -1,9 +1,16 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import {
Button,
Checkbox,
Combobox,
Dialog,
DialogActions,
DialogBody,
DialogContent,
DialogSurface,
DialogTitle,
Field,
Input,
makeStyles,
Option,
Table,
@@ -14,12 +21,11 @@ import {
TableRow,
Tooltip,
} from "@fluentui/react-components";
import { OpenRegular } from "@fluentui/react-icons";
import { useState } from "react";
import { AddRegular, DeleteRegular, OpenRegular } from "@fluentui/react-icons";
import { useMemo, useState } from "react";
import { Link } from "react-router-dom";
import { portalApi } from "../api/portalApi";
import { DataState } from "../components/DataState";
import { FormActions, FormGrid, FormSection } from "../components/FormSection";
import { PageHeader } from "../components/PageHeader";
const useStyles = makeStyles({
@@ -34,8 +40,10 @@ export function DeploymentGroupsPage() {
const queryClient = useQueryClient();
const [serviceId, setServiceId] = useState("");
const [templateId, setTemplateId] = useState("");
const [virtualMachineIds, setVirtualMachineIds] = useState<string[]>([]);
const [status, setStatus] = useState("New");
const [deploymentRuleId, setDeploymentRuleId] = useState("");
const [targetIds, setTargetIds] = useState<string[]>([]);
const [targetSearch, setTargetSearch] = useState("");
const [dialogOpen, setDialogOpen] = useState(false);
const { data, error, isLoading } = useQuery({
queryKey: ["deployment-batches"],
queryFn: ({ signal }) => portalApi.getDeploymentBatches(signal),
@@ -52,17 +60,29 @@ export function DeploymentGroupsPage() {
queryKey: ["template-categories"],
queryFn: ({ signal }) => portalApi.getTemplateCategories(signal),
});
const { data: virtualMachines } = useQuery({
queryKey: ["virtual-machines"],
queryFn: ({ signal }) => portalApi.getVirtualMachines(signal),
const { data: targets } = useQuery({
queryKey: ["targets"],
queryFn: ({ signal }) => portalApi.getTargets(signal),
});
const { data: deploymentRules } = useQuery({
queryKey: ["deployment-rules"],
queryFn: ({ signal }) => portalApi.getDeploymentRules(signal),
});
const addDeploymentBatch = useMutation({
mutationFn: portalApi.addDeploymentBatch,
onSuccess: async () => {
setServiceId("");
setTemplateId("");
setVirtualMachineIds([]);
setStatus("New");
setDeploymentRuleId("");
setTargetIds([]);
setDialogOpen(false);
await queryClient.invalidateQueries({ queryKey: ["deployment-batches"] });
await queryClient.invalidateQueries({ queryKey: ["deployments"] });
},
});
const deleteDeploymentBatch = useMutation({
mutationFn: portalApi.deleteDeploymentBatch,
onSuccess: async () => {
await queryClient.invalidateQueries({ queryKey: ["deployment-batches"] });
await queryClient.invalidateQueries({ queryKey: ["deployments"] });
},
@@ -70,104 +90,39 @@ export function DeploymentGroupsPage() {
const selectedService = (services ?? []).find((service) => service.id === serviceId);
const isOnPremService = selectedService ? !selectedService.isCloudService : false;
const filteredTargets = useMemo(() => {
const search = targetSearch.trim().toLowerCase();
if (!search) {
return targets ?? [];
}
return (targets ?? []).filter((target) => {
const name = target.name?.toLowerCase() ?? "";
const id = target.id?.toLowerCase() ?? "";
return name.includes(search) || id.includes(search);
});
}, [targetSearch, targets]);
const allVisibleSelected =
filteredTargets.length > 0 &&
filteredTargets.every((target) => targetIds.includes(target.id));
return (
<>
<PageHeader title="Deployments" description="Deployment Batches und deren Ausfuehrungen." />
<FormSection
onSubmit={(event) => {
event.preventDefault();
addDeploymentBatch.mutate({
status,
templateId,
virtualMachineIds: isOnPremService ? virtualMachineIds : undefined,
});
}}
>
<FormGrid>
<Field label="Service" required>
<Combobox
placeholder="Service waehlen"
value={services?.find((service) => service.id === serviceId)?.name ?? ""}
onOptionSelect={(_, data) => {
const nextServiceId = data.optionValue ?? "";
setServiceId(nextServiceId);
setTemplateId("");
setVirtualMachineIds([]);
}}
>
{(services ?? []).map((service) => (
<Option key={service.id} text={service.name} value={service.id}>
{service.name}
</Option>
))}
</Combobox>
</Field>
<Field label="Template" required>
<Combobox
placeholder={serviceId ? "Template waehlen" : "Erst Service waehlen"}
disabled={!serviceId}
value={templates?.find((template) => template.id === templateId)?.name ?? ""}
onOptionSelect={(_, data) => setTemplateId(data.optionValue ?? "")}
>
{(templates ?? [])
.filter((template) => {
const category = (templateCategories ?? []).find((entry) => entry.id === template.templateCategoryId);
return category?.serviceId === serviceId;
})
.map((template) => (
<Option key={template.id} text={template.name} value={template.id}>
{template.name}
</Option>
))}
</Combobox>
</Field>
<Field label="Status" required validationMessage={addDeploymentBatch.error?.message}>
<Combobox value={status} onOptionSelect={(_, data) => setStatus(data.optionValue ?? "New")}>
<Option value="New">New</Option>
<Option value="Pending">Pending</Option>
<Option value="Running">Running</Option>
<Option value="Succeeded">Succeeded</Option>
<Option value="Failed">Failed</Option>
</Combobox>
</Field>
{isOnPremService && (
<Field label="Virtual Machines" required>
<div>
{(virtualMachines ?? []).map((virtualMachine) => (
<Checkbox
key={virtualMachine.id}
label={virtualMachine.name}
checked={virtualMachineIds.includes(virtualMachine.id)}
onChange={(_, data) => {
if (data.checked) {
setVirtualMachineIds((prev) => [...prev, virtualMachine.id]);
} else {
setVirtualMachineIds((prev) => prev.filter((id) => id !== virtualMachine.id));
}
}}
/>
))}
</div>
</Field>
)}
</FormGrid>
<FormActions>
<Button
appearance="primary"
disabled={!templateId || !status || (isOnPremService && virtualMachineIds.length === 0) || addDeploymentBatch.isPending}
type="submit"
>
Add deployment batch
</Button>
</FormActions>
</FormSection>
<Button appearance="primary" icon={<AddRegular />} onClick={() => setDialogOpen(true)}>
Neues Deployment
</Button>
<DataState isLoading={isLoading} error={error} />
{data && (
<Table aria-label="Deployment batches">
<TableHeader>
<TableRow>
<TableHeaderCell>Service</TableHeaderCell>
<TableHeaderCell>Template</TableHeaderCell>
<TableHeaderCell>Rule</TableHeaderCell>
<TableHeaderCell>Status</TableHeaderCell>
<TableHeaderCell>Created</TableHeaderCell>
<TableHeaderCell>Modified</TableHeaderCell>
<TableHeaderCell>Id</TableHeaderCell>
<TableHeaderCell>Actions</TableHeaderCell>
</TableRow>
@@ -175,7 +130,21 @@ export function DeploymentGroupsPage() {
<TableBody>
{data.map((deploymentBatch) => (
<TableRow key={deploymentBatch.id}>
<TableCell>
{(() => {
const template = (templates ?? []).find((entry) => entry.id === deploymentBatch.templateId);
const category = (templateCategories ?? []).find((entry) => entry.id === template?.templateCategoryId);
const service = (services ?? []).find((entry) => entry.id === category?.serviceId);
return service?.name ?? "-";
})()}
</TableCell>
<TableCell>{(templates ?? []).find((entry) => entry.id === deploymentBatch.templateId)?.name ?? "-"}</TableCell>
<TableCell>
{(deploymentRules ?? []).find((entry) => entry.id === deploymentBatch.deploymentRuleId)?.name ?? "-"}
</TableCell>
<TableCell>{deploymentBatch.status ?? "Unknown"}</TableCell>
<TableCell>{deploymentBatch.created ? new Date(deploymentBatch.created).toLocaleString() : "-"}</TableCell>
<TableCell>{deploymentBatch.modified ? new Date(deploymentBatch.modified).toLocaleString() : "-"}</TableCell>
<TableCell>{deploymentBatch.id}</TableCell>
<TableCell>
<div className={styles.actions}>
@@ -184,6 +153,14 @@ export function DeploymentGroupsPage() {
<Button appearance="subtle" aria-label="Details" icon={<OpenRegular />} />
</Link>
</Tooltip>
<Tooltip content="Delete" relationship="label">
<Button
appearance="subtle"
aria-label="Delete"
icon={<DeleteRegular />}
onClick={() => deleteDeploymentBatch.mutate(deploymentBatch.id)}
/>
</Tooltip>
</div>
</TableCell>
</TableRow>
@@ -191,6 +168,140 @@ export function DeploymentGroupsPage() {
</TableBody>
</Table>
)}
<Dialog open={dialogOpen} onOpenChange={(_, data) => setDialogOpen(data.open)}>
<DialogSurface>
<DialogBody>
<DialogTitle>Neues Deployment</DialogTitle>
<DialogContent>
<Field label="Service" required>
<Combobox
placeholder="Service waehlen"
value={services?.find((service) => service.id === serviceId)?.name ?? ""}
onOptionSelect={(_, data) => {
const nextServiceId = data.optionValue ?? "";
setServiceId(nextServiceId);
setTemplateId("");
setTargetIds([]);
}}
>
{(services ?? []).map((service) => (
<Option key={service.id} text={service.name} value={service.id}>
{service.name}
</Option>
))}
</Combobox>
</Field>
<Field label="Template" required>
<Combobox
placeholder={serviceId ? "Template waehlen" : "Erst Service waehlen"}
disabled={!serviceId}
value={templates?.find((template) => template.id === templateId)?.name ?? ""}
onOptionSelect={(_, data) => setTemplateId(data.optionValue ?? "")}
>
{(templates ?? [])
.filter((template) => {
const category = (templateCategories ?? []).find((entry) => entry.id === template.templateCategoryId);
return category?.serviceId === serviceId;
})
.map((template) => (
<Option key={template.id} text={template.name} value={template.id}>
{template.name}
</Option>
))}
</Combobox>
</Field>
<Field label="Deployment Rule">
<Combobox
placeholder="Rule waehlen (optional)"
value={deploymentRules?.find((rule) => rule.id === deploymentRuleId)?.name ?? ""}
onOptionSelect={(_, data) => setDeploymentRuleId(data.optionValue ?? "")}
>
{(deploymentRules ?? []).map((rule) => (
<Option key={rule.id} text={rule.name} value={rule.id}>
{rule.name}
</Option>
))}
</Combobox>
</Field>
{isOnPremService && (
<Field label="Targets" required>
<div>
<Input
placeholder="VM suchen (Name oder Id)"
value={targetSearch}
onChange={(_, data) => setTargetSearch(data.value)}
/>
<Table aria-label="Targets selection">
<TableHeader>
<TableRow>
<TableHeaderCell>
<Checkbox
checked={allVisibleSelected}
onChange={(_, data) => {
if (data.checked) {
setTargetIds((prev) =>
Array.from(new Set([...prev, ...filteredTargets.map((vm) => vm.id)])),
);
} else {
const visibleIds = new Set(filteredTargets.map((vm) => vm.id));
setTargetIds((prev) => prev.filter((id) => !visibleIds.has(id)));
}
}}
/>
</TableHeaderCell>
<TableHeaderCell>Name</TableHeaderCell>
<TableHeaderCell>Id</TableHeaderCell>
</TableRow>
</TableHeader>
<TableBody>
{filteredTargets.map((target) => (
<TableRow key={target.id}>
<TableCell>
<Checkbox
checked={targetIds.includes(target.id)}
onChange={(_, data) => {
if (data.checked) {
setTargetIds((prev) => [...prev, target.id]);
} else {
setTargetIds((prev) => prev.filter((entry) => entry !== target.id));
}
}}
/>
</TableCell>
<TableCell>{target.name}</TableCell>
<TableCell>{target.id}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</Field>
)}
</DialogContent>
<DialogActions>
<Button appearance="secondary" onClick={() => setDialogOpen(false)}>
Abbrechen
</Button>
<Button
appearance="primary"
disabled={!templateId || (isOnPremService && targetIds.length === 0) || addDeploymentBatch.isPending}
onClick={() =>
addDeploymentBatch.mutate({
status: "New",
templateId,
deploymentRuleId: deploymentRuleId || undefined,
targetIds: isOnPremService ? targetIds : undefined,
})
}
>
Deployment anlegen
</Button>
</DialogActions>
</DialogBody>
</DialogSurface>
</Dialog>
</>
);
}