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:
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user