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, Text, tokens, Tooltip, } from "@fluentui/react-components"; import { AddRegular, DeleteRegular, OpenRegular } from "@fluentui/react-icons"; import { ChevronDownRegular, ChevronRightRegular } from "@fluentui/react-icons"; import { Fragment, useEffect, useMemo, 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 { DeploymentBatch } from "../types/portal"; const statusOrder = ["New", "Pending", "Running", "Failed", "Completed", "Unknown"]; const useStyles = makeStyles({ pageActions: { display: "flex", gap: "10px", marginBottom: "16px", }, groupRow: { cursor: "pointer", }, groupHeader: { display: "flex", alignItems: "center", gap: "8px", }, nestedLevel2Cell: { paddingLeft: "48px", }, actions: { display: "flex", gap: "4px", ...shorthands.padding("2px", "0"), }, muted: { color: tokens.colorNeutralForeground3, }, idText: { color: tokens.colorNeutralForeground3, fontFamily: "Consolas, monospace", fontSize: "12px", }, emptyState: { backgroundColor: tokens.colorNeutralBackground1, color: tokens.colorNeutralForeground3, ...shorthands.border("1px", "solid", tokens.colorNeutralStroke2), ...shorthands.borderRadius(tokens.borderRadiusMedium), ...shorthands.padding("24px"), }, dialogIntro: { color: tokens.colorNeutralForeground3, marginBottom: "16px", }, targetPicker: { display: "grid", gap: "10px", }, }); export function DeploymentGroupsPage() { const styles = useStyles(); const queryClient = useQueryClient(); const [serviceId, setServiceId] = useState(""); const [templateId, setTemplateId] = useState(""); const [templateVersionId, setTemplateVersionId] = useState(""); const [deploymentRuleId, setDeploymentRuleId] = useState(""); const [targetIds, setTargetIds] = useState([]); const [targetSearch, setTargetSearch] = useState(""); const [dialogOpen, setDialogOpen] = useState(false); const [deleteCandidate, setDeleteCandidate] = useState(null); const [selectedIds, setSelectedIds] = useState([]); const [expandedStatusGroups, setExpandedStatusGroups] = useState>({}); const { data, error, isLoading } = useQuery({ queryKey: ["deployment-batches"], queryFn: ({ signal }) => portalApi.getDeploymentBatches(signal), }); const { data: services } = useQuery({ queryKey: ["services"], queryFn: ({ signal }) => portalApi.getServices(signal), }); const { data: templates } = useQuery({ queryKey: ["templates"], queryFn: ({ signal }) => portalApi.getTemplates(signal), }); const { data: templateCategories } = useQuery({ queryKey: ["template-categories"], queryFn: ({ signal }) => portalApi.getTemplateCategories(signal), }); const { data: targets } = useQuery({ queryKey: ["targets"], queryFn: ({ signal }) => portalApi.getTargets(signal), }); const { data: templateVersions } = useQuery({ queryKey: ["template-versions", templateId], queryFn: ({ signal }) => portalApi.getTemplateVersions(templateId, signal), enabled: Boolean(templateId), }); const { data: deploymentRules } = useQuery({ queryKey: ["deployment-rules"], queryFn: ({ signal }) => portalApi.getDeploymentRules(signal), }); const addDeploymentBatch = useMutation({ mutationFn: portalApi.addDeploymentBatch, onSuccess: async () => { setServiceId(""); setTemplateId(""); setTemplateVersionId(""); setDeploymentRuleId(""); setTargetIds([]); setTargetSearch(""); setDialogOpen(false); await queryClient.invalidateQueries({ queryKey: ["deployment-batches"] }); await queryClient.invalidateQueries({ queryKey: ["deployments"] }); }, }); const deleteDeploymentBatch = useMutation({ mutationFn: portalApi.deleteDeploymentBatch, onSuccess: async () => { setDeleteCandidate(null); await queryClient.invalidateQueries({ queryKey: ["deployment-batches"] }); await queryClient.invalidateQueries({ queryKey: ["deployments"] }); }, }); const deleteSelectedDeploymentBatches = useMutation({ mutationFn: async (ids: string[]) => { await Promise.all(ids.map((id) => portalApi.deleteDeploymentBatch(id))); }, onSuccess: async () => { setSelectedIds([]); await queryClient.invalidateQueries({ queryKey: ["deployment-batches"] }); await queryClient.invalidateQueries({ queryKey: ["deployments"] }); }, }); 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)); const groupedBatches = useMemo(() => { const groups = (data ?? []).reduce>((acc, deploymentBatch) => { const status = deploymentBatch.status || "Unknown"; acc[status] = [...(acc[status] ?? []), deploymentBatch]; return acc; }, {}); return Object.entries(groups).sort(([left], [right]) => { const leftIndex = statusOrder.indexOf(left); const rightIndex = statusOrder.indexOf(right); return (leftIndex === -1 ? statusOrder.length : leftIndex) - (rightIndex === -1 ? statusOrder.length : rightIndex); }); }, [data]); const allSelected = Boolean(data?.length) && data!.every((deploymentBatch) => selectedIds.includes(deploymentBatch.id)); const toggleSelected = (id: string, checked: boolean) => { setSelectedIds((current) => (checked ? [...new Set([...current, id])] : current.filter((entry) => entry !== id))); }; useEffect(() => { if (!templateVersions || templateVersions.length === 0 || templateVersionId) { return; } const publishedVersion = templateVersions.find((version) => version.isPublished); setTemplateVersionId((publishedVersion ?? templateVersions[0]).id); }, [templateVersionId, templateVersions]); const getServiceName = (deploymentBatch: DeploymentBatch) => { 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 ?? "-"; }; const getPrimaryTemplateLabel = (deploymentBatch: DeploymentBatch) => { const fallbackTemplate = (templates ?? []).find((entry) => entry.id === deploymentBatch.templateId); const templateName = deploymentBatch.primaryTemplateName ?? fallbackTemplate?.name ?? "-"; const templateVersion = deploymentBatch.primaryTemplateVersion ? ` ${deploymentBatch.primaryTemplateVersion}` : ""; const additionalTemplates = deploymentBatch.templateSelectionCount > 1 ? ` +${deploymentBatch.templateSelectionCount - 1}` : ""; return `${templateName}${templateVersion}${additionalTemplates}`; }; const toggleStatusGroup = (status: string) => { setExpandedStatusGroups((current) => ({ ...current, [status]: !current[status], })); }; return ( <>
{!isLoading && !error && groupedBatches.length === 0 && (
Keine Deployment Batches vorhanden.
)} {!isLoading && !error && groupedBatches.length > 0 && ( setSelectedIds(checkboxData.checked ? (data ?? []).map((deploymentBatch) => deploymentBatch.id) : []) } /> Service Primary Template Templates Targets Rule Created Batch Actions {groupedBatches.map(([status, deploymentBatches]) => ( toggleStatusGroup(status)}>
{expandedStatusGroups[status] !== false ? : } {status}
{expandedStatusGroups[status] !== false && deploymentBatches.map((deploymentBatch) => ( toggleSelected(deploymentBatch.id, Boolean(checkboxData.checked))} /> {getServiceName(deploymentBatch)} {getPrimaryTemplateLabel(deploymentBatch)} {deploymentBatch.templateSelectionCount ?? 0} {deploymentBatch.targetAssignmentCount ?? 0} {(deploymentRules ?? []).find((entry) => entry.id === deploymentBatch.deploymentRuleId)?.name ?? "-"} {deploymentBatch.created ? new Date(deploymentBatch.created).toLocaleString() : "-"} {deploymentBatch.id.slice(0, 8)}
))}
))}
)} setDialogOpen(data.open)}> Neuer Deployment Batch Erstellt eine neue Deployment Composition mit einer initialen Template Selection. service.id === serviceId)?.name ?? ""} onOptionSelect={(_, data) => { const nextServiceId = data.optionValue ?? ""; setServiceId(nextServiceId); setTemplateId(""); setTemplateVersionId(""); setTargetIds([]); }} > {(services ?? []).map((service) => ( ))} template.id === templateId)?.name ?? ""} onOptionSelect={(_, data) => { setTemplateId(data.optionValue ?? ""); setTemplateVersionId(""); }} > {(templates ?? []) .filter((template) => { const category = (templateCategories ?? []).find((entry) => entry.id === template.templateCategoryId); return category?.serviceId === serviceId; }) .map((template) => ( ))} version.id === templateVersionId)?.version ?? ""} onOptionSelect={(_, data) => setTemplateVersionId(data.optionValue ?? "")} > {(templateVersions ?? []).map((version) => ( ))} rule.id === deploymentRuleId)?.name ?? ""} onOptionSelect={(_, data) => setDeploymentRuleId(data.optionValue ?? "")} > {(deploymentRules ?? []).map((rule) => ( ))} {isOnPremService && (
setTargetSearch(data.value)} /> { if (data.checked) { setTargetIds((prev) => Array.from(new Set([...prev, ...filteredTargets.map((target) => target.id)])), ); } else { const visibleIds = new Set(filteredTargets.map((target) => target.id)); setTargetIds((prev) => prev.filter((id) => !visibleIds.has(id))); } }} /> Name Provider {filteredTargets.map((target) => ( { if (data.checked) { setTargetIds((prev) => [...prev, target.id]); } else { setTargetIds((prev) => prev.filter((entry) => entry !== target.id)); } }} /> {target.name} {target.providerType} ))}
)}
!data.open && setDeleteCandidate(null)}> Deployment Batch loeschen? Der Batch {deleteCandidate?.id.slice(0, 8)} und seine Composition werden geloescht. ); }