feat: add multi-select functionality across various pages
- 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.
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
import { Outlet, NavLink } from "react-router-dom";
|
import { Outlet, NavLink } from "react-router-dom";
|
||||||
import {
|
import {
|
||||||
|
Badge,
|
||||||
Button,
|
Button,
|
||||||
makeStyles,
|
makeStyles,
|
||||||
shorthands,
|
shorthands,
|
||||||
@@ -16,6 +17,8 @@ import {
|
|||||||
TagMultiple24Regular,
|
TagMultiple24Regular,
|
||||||
Desktop24Regular,
|
Desktop24Regular,
|
||||||
} from "@fluentui/react-icons";
|
} from "@fluentui/react-icons";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { portalApi } from "../api/portalApi";
|
||||||
|
|
||||||
const useStyles = makeStyles({
|
const useStyles = makeStyles({
|
||||||
root: {
|
root: {
|
||||||
@@ -63,6 +66,11 @@ const useStyles = makeStyles({
|
|||||||
...shorthands.borderBottom("1px", "solid", tokens.colorNeutralStroke2),
|
...shorthands.borderBottom("1px", "solid", tokens.colorNeutralStroke2),
|
||||||
...shorthands.padding("0", "28px"),
|
...shorthands.padding("0", "28px"),
|
||||||
},
|
},
|
||||||
|
apiStatus: {
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: "10px",
|
||||||
|
},
|
||||||
main: {
|
main: {
|
||||||
...shorthands.padding("28px"),
|
...shorthands.padding("28px"),
|
||||||
},
|
},
|
||||||
@@ -84,6 +92,19 @@ const links = [
|
|||||||
|
|
||||||
export function AppShell() {
|
export function AppShell() {
|
||||||
const styles = useStyles();
|
const styles = useStyles();
|
||||||
|
const apiStatus = useQuery({
|
||||||
|
queryKey: ["api-status"],
|
||||||
|
queryFn: ({ signal }) => portalApi.getServices(signal),
|
||||||
|
retry: 1,
|
||||||
|
staleTime: 60_000,
|
||||||
|
});
|
||||||
|
const apiBaseUrl = import.meta.env.VITE_API_BASE_URL ?? "/api";
|
||||||
|
const apiBadge =
|
||||||
|
apiStatus.isLoading
|
||||||
|
? { appearance: "tint" as const, color: "subtle" as const, label: "API pruefen" }
|
||||||
|
: apiStatus.error
|
||||||
|
? { appearance: "filled" as const, color: "danger" as const, label: "API Fehler" }
|
||||||
|
: { appearance: "filled" as const, color: "success" as const, label: "API verbunden" };
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.root}>
|
<div className={styles.root}>
|
||||||
@@ -111,7 +132,12 @@ export function AppShell() {
|
|||||||
<section className={styles.content}>
|
<section className={styles.content}>
|
||||||
<header className={styles.topbar}>
|
<header className={styles.topbar}>
|
||||||
<Text weight="semibold">Portal workspace</Text>
|
<Text weight="semibold">Portal workspace</Text>
|
||||||
<Text size={200}>API: {import.meta.env.VITE_API_BASE_URL ?? "/api"}</Text>
|
<div className={styles.apiStatus}>
|
||||||
|
<Badge appearance={apiBadge.appearance} color={apiBadge.color}>
|
||||||
|
{apiBadge.label}
|
||||||
|
</Badge>
|
||||||
|
<Text size={200}>{apiBaseUrl}</Text>
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<main className={styles.main}>
|
<main className={styles.main}>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
|
|||||||
@@ -4,9 +4,11 @@ import {
|
|||||||
CardHeader,
|
CardHeader,
|
||||||
Divider,
|
Divider,
|
||||||
makeStyles,
|
makeStyles,
|
||||||
|
shorthands,
|
||||||
Text,
|
Text,
|
||||||
tokens,
|
tokens,
|
||||||
} from "@fluentui/react-components";
|
} from "@fluentui/react-components";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
import { portalApi } from "../api/portalApi";
|
import { portalApi } from "../api/portalApi";
|
||||||
import { DataState } from "../components/DataState";
|
import { DataState } from "../components/DataState";
|
||||||
import { PageHeader } from "../components/PageHeader";
|
import { PageHeader } from "../components/PageHeader";
|
||||||
@@ -28,6 +30,28 @@ const useStyles = makeStyles({
|
|||||||
fontWeight: tokens.fontWeightSemibold,
|
fontWeight: tokens.fontWeightSemibold,
|
||||||
lineHeight: "40px",
|
lineHeight: "40px",
|
||||||
},
|
},
|
||||||
|
metricLink: {
|
||||||
|
color: "inherit",
|
||||||
|
display: "block",
|
||||||
|
textDecorationLine: "none",
|
||||||
|
...shorthands.borderRadius(tokens.borderRadiusMedium),
|
||||||
|
":focus-visible": {
|
||||||
|
outlineStyle: "solid",
|
||||||
|
outlineWidth: "2px",
|
||||||
|
outlineColor: tokens.colorStrokeFocus2,
|
||||||
|
outlineOffset: "2px",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
metricCard: {
|
||||||
|
transitionProperty: "box-shadow, transform, border-color",
|
||||||
|
transitionDuration: tokens.durationFaster,
|
||||||
|
transitionTimingFunction: tokens.curveEasyEase,
|
||||||
|
":hover": {
|
||||||
|
boxShadow: tokens.shadow8,
|
||||||
|
transform: "translateY(-1px)",
|
||||||
|
...shorthands.borderColor(tokens.colorNeutralStroke1Hover),
|
||||||
|
},
|
||||||
|
},
|
||||||
donutWrap: {
|
donutWrap: {
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
@@ -54,6 +78,12 @@ const useStyles = makeStyles({
|
|||||||
borderRadius: "50%",
|
borderRadius: "50%",
|
||||||
display: "inline-block",
|
display: "inline-block",
|
||||||
},
|
},
|
||||||
|
emptyState: {
|
||||||
|
minHeight: "120px",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
color: tokens.colorNeutralForeground3,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export function DashboardPage() {
|
export function DashboardPage() {
|
||||||
@@ -121,16 +151,17 @@ export function DashboardPage() {
|
|||||||
<DataState isLoading={isLoading} error={error} />
|
<DataState isLoading={isLoading} error={error} />
|
||||||
{!isLoading && !error && (
|
{!isLoading && !error && (
|
||||||
<div className={styles.grid}>
|
<div className={styles.grid}>
|
||||||
<MetricCard label="Domains" value={domains.data?.length ?? 0} />
|
<MetricCard label="Domains" value={domains.data?.length ?? 0} to="/domains" />
|
||||||
<MetricCard label="Environments" value={environments.data?.length ?? 0} />
|
<MetricCard label="Environments" value={environments.data?.length ?? 0} to="/environments" />
|
||||||
<MetricCard label="Templates" value={templates.data?.length ?? 0} />
|
<MetricCard label="Templates" value={templates.data?.length ?? 0} to="/templates" />
|
||||||
<MetricCard label="Services" value={services.data?.length ?? 0} />
|
<MetricCard label="Services" value={services.data?.length ?? 0} to="/services" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!isLoading && !error && (
|
{!isLoading && !error && (
|
||||||
<div className={styles.chartGrid}>
|
<div className={styles.chartGrid}>
|
||||||
<DonutCard
|
<DonutCard
|
||||||
title="Jobs"
|
title="Jobs"
|
||||||
|
emptyText="Keine Worker Jobs vorhanden."
|
||||||
items={Object.entries(queueByStatus).map(([label, value], index) => ({
|
items={Object.entries(queueByStatus).map(([label, value], index) => ({
|
||||||
label,
|
label,
|
||||||
value,
|
value,
|
||||||
@@ -139,6 +170,7 @@ export function DashboardPage() {
|
|||||||
/>
|
/>
|
||||||
<DonutCard
|
<DonutCard
|
||||||
title="VM DSC Compliance"
|
title="VM DSC Compliance"
|
||||||
|
emptyText="Keine Compliance-Daten gemeldet."
|
||||||
items={[
|
items={[
|
||||||
{ label: "Compliant", value: vmCompliance.compliant, color: "#107C10" },
|
{ label: "Compliant", value: vmCompliance.compliant, color: "#107C10" },
|
||||||
{ label: "Non-Compliant", value: vmCompliance.nonCompliant, color: "#D13438" },
|
{ label: "Non-Compliant", value: vmCompliance.nonCompliant, color: "#D13438" },
|
||||||
@@ -151,23 +183,27 @@ export function DashboardPage() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function MetricCard({ label, value }: { label: string; value: number }) {
|
function MetricCard({ label, value, to }: { label: string; value: number; to: string }) {
|
||||||
const styles = useStyles();
|
const styles = useStyles();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Link className={styles.metricLink} to={to}>
|
||||||
<CardHeader header={<Text weight="semibold">{label}</Text>} />
|
<Card className={styles.metricCard}>
|
||||||
<Text className={styles.metric}>{value}</Text>
|
<CardHeader header={<Text weight="semibold">{label}</Text>} />
|
||||||
</Card>
|
<Text className={styles.metric}>{value}</Text>
|
||||||
|
</Card>
|
||||||
|
</Link>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const chartColors = ["#0F6CBD", "#107C10", "#D13438", "#8764B8", "#CA5010", "#605E5C"];
|
const chartColors = ["#0F6CBD", "#107C10", "#D13438", "#8764B8", "#CA5010", "#605E5C"];
|
||||||
|
|
||||||
function DonutCard({
|
function DonutCard({
|
||||||
|
emptyText,
|
||||||
title,
|
title,
|
||||||
items,
|
items,
|
||||||
}: {
|
}: {
|
||||||
|
emptyText: string;
|
||||||
title: string;
|
title: string;
|
||||||
items: Array<{ label: string; value: number; color: string }>;
|
items: Array<{ label: string; value: number; color: string }>;
|
||||||
}) {
|
}) {
|
||||||
@@ -183,20 +219,26 @@ function DonutCard({
|
|||||||
<Card>
|
<Card>
|
||||||
<CardHeader header={<Text weight="semibold">{title}</Text>} />
|
<CardHeader header={<Text weight="semibold">{title}</Text>} />
|
||||||
<Divider />
|
<Divider />
|
||||||
<div className={styles.donutWrap}>
|
{total === 0 ? (
|
||||||
<div className={styles.donut} style={{ background: `conic-gradient(${segments})` }} />
|
<div className={styles.emptyState}>
|
||||||
<div className={styles.legend}>
|
<Text>{emptyText}</Text>
|
||||||
{items.map((item) => (
|
|
||||||
<div key={item.label} className={styles.legendRow}>
|
|
||||||
<span className={styles.swatch} style={{ backgroundColor: item.color }} />
|
|
||||||
<Text>
|
|
||||||
{item.label}: {item.value}
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
<Text>Total: {total}</Text>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
) : (
|
||||||
|
<div className={styles.donutWrap}>
|
||||||
|
<div className={styles.donut} style={{ background: `conic-gradient(${segments})` }} />
|
||||||
|
<div className={styles.legend}>
|
||||||
|
{items.map((item) => (
|
||||||
|
<div key={item.label} className={styles.legendRow}>
|
||||||
|
<span className={styles.swatch} style={{ backgroundColor: item.color }} />
|
||||||
|
<Text>
|
||||||
|
{item.label}: {item.value}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<Text>Total: {total}</Text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import {
|
|||||||
Tooltip,
|
Tooltip,
|
||||||
} from "@fluentui/react-components";
|
} from "@fluentui/react-components";
|
||||||
import { AddRegular, ArrowLeftRegular, DeleteRegular } from "@fluentui/react-icons";
|
import { AddRegular, ArrowLeftRegular, DeleteRegular } from "@fluentui/react-icons";
|
||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState, type SetStateAction } from "react";
|
||||||
import { Link, useParams } from "react-router-dom";
|
import { Link, useParams } from "react-router-dom";
|
||||||
import { portalApi } from "../api/portalApi";
|
import { portalApi } from "../api/portalApi";
|
||||||
import { DataState } from "../components/DataState";
|
import { DataState } from "../components/DataState";
|
||||||
@@ -42,6 +42,12 @@ const useStyles = makeStyles({
|
|||||||
fontSize: "18px",
|
fontSize: "18px",
|
||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
},
|
},
|
||||||
|
headerActions: {
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
gap: "12px",
|
||||||
|
},
|
||||||
formGrid: {
|
formGrid: {
|
||||||
display: "grid",
|
display: "grid",
|
||||||
gridTemplateColumns: "repeat(4, minmax(160px, 1fr))",
|
gridTemplateColumns: "repeat(4, minmax(160px, 1fr))",
|
||||||
@@ -79,6 +85,9 @@ export function DeploymentBatchDetailsPage() {
|
|||||||
const [targetTargetId, setTargetTargetId] = useState("");
|
const [targetTargetId, setTargetTargetId] = useState("");
|
||||||
const [targetRoleKey, setTargetRoleKey] = useState("Node");
|
const [targetRoleKey, setTargetRoleKey] = useState("Node");
|
||||||
const [targetNodeDataJson, setTargetNodeDataJson] = useState("{}");
|
const [targetNodeDataJson, setTargetNodeDataJson] = useState("{}");
|
||||||
|
const [selectedTemplateSelectionIds, setSelectedTemplateSelectionIds] = useState<string[]>([]);
|
||||||
|
const [selectedParameterValueIds, setSelectedParameterValueIds] = useState<string[]>([]);
|
||||||
|
const [selectedTargetAssignmentIds, setSelectedTargetAssignmentIds] = useState<string[]>([]);
|
||||||
|
|
||||||
const { data: deploymentBatch, error, isLoading } = useQuery({
|
const { data: deploymentBatch, error, isLoading } = useQuery({
|
||||||
queryKey: ["deployment-batches", id],
|
queryKey: ["deployment-batches", id],
|
||||||
@@ -127,6 +136,15 @@ export function DeploymentBatchDetailsPage() {
|
|||||||
mutationFn: (selectionId: string) => portalApi.deleteDeploymentTemplateSelection(id!, selectionId),
|
mutationFn: (selectionId: string) => portalApi.deleteDeploymentTemplateSelection(id!, selectionId),
|
||||||
onSuccess: invalidateBatch,
|
onSuccess: invalidateBatch,
|
||||||
});
|
});
|
||||||
|
const deleteSelectedTemplateSelections = useMutation({
|
||||||
|
mutationFn: async (selectionIds: string[]) => {
|
||||||
|
await Promise.all(selectionIds.map((selectionId) => portalApi.deleteDeploymentTemplateSelection(id!, selectionId)));
|
||||||
|
},
|
||||||
|
onSuccess: async () => {
|
||||||
|
setSelectedTemplateSelectionIds([]);
|
||||||
|
await invalidateBatch();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const addParameterValue = useMutation({
|
const addParameterValue = useMutation({
|
||||||
mutationFn: () =>
|
mutationFn: () =>
|
||||||
@@ -149,6 +167,15 @@ export function DeploymentBatchDetailsPage() {
|
|||||||
mutationFn: (parameterValueId: string) => portalApi.deleteDeploymentParameterValue(id!, parameterValueId),
|
mutationFn: (parameterValueId: string) => portalApi.deleteDeploymentParameterValue(id!, parameterValueId),
|
||||||
onSuccess: invalidateBatch,
|
onSuccess: invalidateBatch,
|
||||||
});
|
});
|
||||||
|
const deleteSelectedParameterValues = useMutation({
|
||||||
|
mutationFn: async (parameterValueIds: string[]) => {
|
||||||
|
await Promise.all(parameterValueIds.map((parameterValueId) => portalApi.deleteDeploymentParameterValue(id!, parameterValueId)));
|
||||||
|
},
|
||||||
|
onSuccess: async () => {
|
||||||
|
setSelectedParameterValueIds([]);
|
||||||
|
await invalidateBatch();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const addTargetAssignment = useMutation({
|
const addTargetAssignment = useMutation({
|
||||||
mutationFn: () =>
|
mutationFn: () =>
|
||||||
@@ -168,6 +195,29 @@ export function DeploymentBatchDetailsPage() {
|
|||||||
mutationFn: (targetAssignmentId: string) => portalApi.deleteDeploymentTargetAssignment(id!, targetAssignmentId),
|
mutationFn: (targetAssignmentId: string) => portalApi.deleteDeploymentTargetAssignment(id!, targetAssignmentId),
|
||||||
onSuccess: invalidateBatch,
|
onSuccess: invalidateBatch,
|
||||||
});
|
});
|
||||||
|
const deleteSelectedTargetAssignments = useMutation({
|
||||||
|
mutationFn: async (targetAssignmentIds: string[]) => {
|
||||||
|
await Promise.all(targetAssignmentIds.map((targetAssignmentId) => portalApi.deleteDeploymentTargetAssignment(id!, targetAssignmentId)));
|
||||||
|
},
|
||||||
|
onSuccess: async () => {
|
||||||
|
setSelectedTargetAssignmentIds([]);
|
||||||
|
await invalidateBatch();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const queueDeployment = useMutation({
|
||||||
|
mutationFn: () =>
|
||||||
|
portalApi.addDeploymentRequest({
|
||||||
|
deploymentGroupId: id!,
|
||||||
|
targetIds: [],
|
||||||
|
jsonData: "{}",
|
||||||
|
}),
|
||||||
|
onSuccess: async () => {
|
||||||
|
await queryClient.invalidateQueries({ queryKey: ["deployment-jobs"] });
|
||||||
|
await queryClient.invalidateQueries({ queryKey: ["queue-jobs"] });
|
||||||
|
await queryClient.invalidateQueries({ queryKey: ["deployments"] });
|
||||||
|
await invalidateBatch();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const filteredExecutions = useMemo(() => {
|
const filteredExecutions = useMemo(() => {
|
||||||
if (deploymentBatch?.deployments && deploymentBatch.deployments.length > 0) {
|
if (deploymentBatch?.deployments && deploymentBatch.deployments.length > 0) {
|
||||||
@@ -182,18 +232,53 @@ export function DeploymentBatchDetailsPage() {
|
|||||||
const templateSelectionError = addTemplateSelection.error;
|
const templateSelectionError = addTemplateSelection.error;
|
||||||
const parameterError = addParameterValue.error;
|
const parameterError = addParameterValue.error;
|
||||||
const targetError = addTargetAssignment.error;
|
const targetError = addTargetAssignment.error;
|
||||||
|
const queueError = queueDeployment.error;
|
||||||
|
const templateSelections = deploymentBatch?.templateSelections ?? [];
|
||||||
|
const parameterValues = deploymentBatch?.parameterValues ?? [];
|
||||||
|
const targetAssignments = deploymentBatch?.targetAssignments ?? [];
|
||||||
|
const allTemplateSelectionsSelected =
|
||||||
|
templateSelections.length > 0 && templateSelections.every((selection) => selectedTemplateSelectionIds.includes(selection.id));
|
||||||
|
const allParameterValuesSelected =
|
||||||
|
parameterValues.length > 0 && parameterValues.every((parameterValue) => selectedParameterValueIds.includes(parameterValue.id));
|
||||||
|
const allTargetAssignmentsSelected =
|
||||||
|
targetAssignments.length > 0 && targetAssignments.every((targetAssignment) => selectedTargetAssignmentIds.includes(targetAssignment.id));
|
||||||
|
const toggleSelected = (ids: string[], setIds: (value: SetStateAction<string[]>) => void, id: string, checked: boolean) => {
|
||||||
|
setIds(checked ? [...new Set([...ids, id])] : ids.filter((entry) => entry !== id));
|
||||||
|
};
|
||||||
|
const canQueueDeployment =
|
||||||
|
Boolean(id) &&
|
||||||
|
((deploymentBatch?.targetAssignments?.length ?? 0) > 0 || filteredExecutions.some((execution) => Boolean(execution.targetId)));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.layout}>
|
<div className={styles.layout}>
|
||||||
<Link to="/deployments">
|
<div className={styles.headerActions}>
|
||||||
<Button appearance="subtle" icon={<ArrowLeftRegular />}>
|
<Link to="/deployments">
|
||||||
Deployments
|
<Button appearance="subtle" icon={<ArrowLeftRegular />}>
|
||||||
|
Deployments
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
<Button
|
||||||
|
appearance="primary"
|
||||||
|
disabled={!canQueueDeployment || queueDeployment.isPending}
|
||||||
|
icon={<AddRegular />}
|
||||||
|
onClick={() => queueDeployment.mutate()}
|
||||||
|
>
|
||||||
|
Queue Deployment
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</div>
|
||||||
<PageHeader title={id ? `Deployment Batch ${id}` : "Deployment Batch"} description="Composition, Targets und Executions fuer diesen Batch." />
|
<PageHeader title={id ? `Deployment Batch ${id}` : "Deployment Batch"} description="Composition, Targets und Executions fuer diesen Batch." />
|
||||||
<DataState
|
<DataState
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
error={error ?? templateSelectionError ?? parameterError ?? targetError}
|
error={
|
||||||
|
error ??
|
||||||
|
templateSelectionError ??
|
||||||
|
deleteSelectedTemplateSelections.error ??
|
||||||
|
parameterError ??
|
||||||
|
deleteSelectedParameterValues.error ??
|
||||||
|
targetError ??
|
||||||
|
deleteSelectedTargetAssignments.error ??
|
||||||
|
queueError
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<section className={styles.section}>
|
<section className={styles.section}>
|
||||||
@@ -207,6 +292,18 @@ export function DeploymentBatchDetailsPage() {
|
|||||||
>
|
>
|
||||||
Add
|
Add
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
appearance="secondary"
|
||||||
|
icon={<DeleteRegular />}
|
||||||
|
disabled={selectedTemplateSelectionIds.length === 0 || deleteSelectedTemplateSelections.isPending}
|
||||||
|
onClick={() => {
|
||||||
|
if (window.confirm(`${selectedTemplateSelectionIds.length} Template Selections wirklich loeschen?`)) {
|
||||||
|
deleteSelectedTemplateSelections.mutate(selectedTemplateSelectionIds);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Auswahl loeschen
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.formGrid}>
|
<div className={styles.formGrid}>
|
||||||
<Field label="Template">
|
<Field label="Template">
|
||||||
@@ -249,6 +346,14 @@ export function DeploymentBatchDetailsPage() {
|
|||||||
<Table aria-label="Template selections">
|
<Table aria-label="Template selections">
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
|
<TableHeaderCell>
|
||||||
|
<Checkbox
|
||||||
|
checked={allTemplateSelectionsSelected}
|
||||||
|
onChange={(_, checkboxData) =>
|
||||||
|
setSelectedTemplateSelectionIds(checkboxData.checked ? templateSelections.map((selection) => selection.id) : [])
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</TableHeaderCell>
|
||||||
<TableHeaderCell>Order</TableHeaderCell>
|
<TableHeaderCell>Order</TableHeaderCell>
|
||||||
<TableHeaderCell>Role</TableHeaderCell>
|
<TableHeaderCell>Role</TableHeaderCell>
|
||||||
<TableHeaderCell>Alias</TableHeaderCell>
|
<TableHeaderCell>Alias</TableHeaderCell>
|
||||||
@@ -259,8 +364,16 @@ export function DeploymentBatchDetailsPage() {
|
|||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{(deploymentBatch?.templateSelections ?? []).map((selection) => (
|
{templateSelections.map((selection) => (
|
||||||
<TableRow key={selection.id}>
|
<TableRow key={selection.id}>
|
||||||
|
<TableCell>
|
||||||
|
<Checkbox
|
||||||
|
checked={selectedTemplateSelectionIds.includes(selection.id)}
|
||||||
|
onChange={(_, checkboxData) =>
|
||||||
|
toggleSelected(selectedTemplateSelectionIds, setSelectedTemplateSelectionIds, selection.id, Boolean(checkboxData.checked))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
<TableCell>{selection.sortOrder}</TableCell>
|
<TableCell>{selection.sortOrder}</TableCell>
|
||||||
<TableCell>{selection.templateRole}</TableCell>
|
<TableCell>{selection.templateRole}</TableCell>
|
||||||
<TableCell>{selection.alias ?? "-"}</TableCell>
|
<TableCell>{selection.alias ?? "-"}</TableCell>
|
||||||
@@ -295,6 +408,18 @@ export function DeploymentBatchDetailsPage() {
|
|||||||
>
|
>
|
||||||
Add
|
Add
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
appearance="secondary"
|
||||||
|
icon={<DeleteRegular />}
|
||||||
|
disabled={selectedParameterValueIds.length === 0 || deleteSelectedParameterValues.isPending}
|
||||||
|
onClick={() => {
|
||||||
|
if (window.confirm(`${selectedParameterValueIds.length} Parameter Values wirklich loeschen?`)) {
|
||||||
|
deleteSelectedParameterValues.mutate(selectedParameterValueIds);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Auswahl loeschen
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.formGrid}>
|
<div className={styles.formGrid}>
|
||||||
<Field label="Scope">
|
<Field label="Scope">
|
||||||
@@ -329,6 +454,14 @@ export function DeploymentBatchDetailsPage() {
|
|||||||
<Table aria-label="Parameter values">
|
<Table aria-label="Parameter values">
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
|
<TableHeaderCell>
|
||||||
|
<Checkbox
|
||||||
|
checked={allParameterValuesSelected}
|
||||||
|
onChange={(_, checkboxData) =>
|
||||||
|
setSelectedParameterValueIds(checkboxData.checked ? parameterValues.map((parameterValue) => parameterValue.id) : [])
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</TableHeaderCell>
|
||||||
<TableHeaderCell>Scope</TableHeaderCell>
|
<TableHeaderCell>Scope</TableHeaderCell>
|
||||||
<TableHeaderCell>Name</TableHeaderCell>
|
<TableHeaderCell>Name</TableHeaderCell>
|
||||||
<TableHeaderCell>Secret</TableHeaderCell>
|
<TableHeaderCell>Secret</TableHeaderCell>
|
||||||
@@ -337,13 +470,21 @@ export function DeploymentBatchDetailsPage() {
|
|||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{(deploymentBatch?.parameterValues ?? []).map((parameterValue) => {
|
{parameterValues.map((parameterValue) => {
|
||||||
const selection = (deploymentBatch?.templateSelections ?? []).find(
|
const selection = templateSelections.find(
|
||||||
(entry) => entry.id === parameterValue.deploymentTemplateSelectionId,
|
(entry) => entry.id === parameterValue.deploymentTemplateSelectionId,
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableRow key={parameterValue.id}>
|
<TableRow key={parameterValue.id}>
|
||||||
|
<TableCell>
|
||||||
|
<Checkbox
|
||||||
|
checked={selectedParameterValueIds.includes(parameterValue.id)}
|
||||||
|
onChange={(_, checkboxData) =>
|
||||||
|
toggleSelected(selectedParameterValueIds, setSelectedParameterValueIds, parameterValue.id, Boolean(checkboxData.checked))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
<TableCell>{selection?.alias ?? selection?.templateRole ?? "Global"}</TableCell>
|
<TableCell>{selection?.alias ?? selection?.templateRole ?? "Global"}</TableCell>
|
||||||
<TableCell>{parameterValue.name}</TableCell>
|
<TableCell>{parameterValue.name}</TableCell>
|
||||||
<TableCell>{parameterValue.isSecretReference ? "Yes" : "No"}</TableCell>
|
<TableCell>{parameterValue.isSecretReference ? "Yes" : "No"}</TableCell>
|
||||||
@@ -377,6 +518,18 @@ export function DeploymentBatchDetailsPage() {
|
|||||||
>
|
>
|
||||||
Add
|
Add
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
appearance="secondary"
|
||||||
|
icon={<DeleteRegular />}
|
||||||
|
disabled={selectedTargetAssignmentIds.length === 0 || deleteSelectedTargetAssignments.isPending}
|
||||||
|
onClick={() => {
|
||||||
|
if (window.confirm(`${selectedTargetAssignmentIds.length} Target Assignments wirklich loeschen?`)) {
|
||||||
|
deleteSelectedTargetAssignments.mutate(selectedTargetAssignmentIds);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Auswahl loeschen
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.formGrid}>
|
<div className={styles.formGrid}>
|
||||||
<Field label="Target">
|
<Field label="Target">
|
||||||
@@ -402,6 +555,14 @@ export function DeploymentBatchDetailsPage() {
|
|||||||
<Table aria-label="Target assignments">
|
<Table aria-label="Target assignments">
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
|
<TableHeaderCell>
|
||||||
|
<Checkbox
|
||||||
|
checked={allTargetAssignmentsSelected}
|
||||||
|
onChange={(_, checkboxData) =>
|
||||||
|
setSelectedTargetAssignmentIds(checkboxData.checked ? targetAssignments.map((targetAssignment) => targetAssignment.id) : [])
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</TableHeaderCell>
|
||||||
<TableHeaderCell>Order</TableHeaderCell>
|
<TableHeaderCell>Order</TableHeaderCell>
|
||||||
<TableHeaderCell>Target</TableHeaderCell>
|
<TableHeaderCell>Target</TableHeaderCell>
|
||||||
<TableHeaderCell>Role</TableHeaderCell>
|
<TableHeaderCell>Role</TableHeaderCell>
|
||||||
@@ -410,8 +571,16 @@ export function DeploymentBatchDetailsPage() {
|
|||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{(deploymentBatch?.targetAssignments ?? []).map((targetAssignment) => (
|
{targetAssignments.map((targetAssignment) => (
|
||||||
<TableRow key={targetAssignment.id}>
|
<TableRow key={targetAssignment.id}>
|
||||||
|
<TableCell>
|
||||||
|
<Checkbox
|
||||||
|
checked={selectedTargetAssignmentIds.includes(targetAssignment.id)}
|
||||||
|
onChange={(_, checkboxData) =>
|
||||||
|
toggleSelected(selectedTargetAssignmentIds, setSelectedTargetAssignmentIds, targetAssignment.id, Boolean(checkboxData.checked))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
<TableCell>{targetAssignment.sortOrder}</TableCell>
|
<TableCell>{targetAssignment.sortOrder}</TableCell>
|
||||||
<TableCell>{targetAssignment.target?.name ?? targetAssignment.targetId}</TableCell>
|
<TableCell>{targetAssignment.target?.name ?? targetAssignment.targetId}</TableCell>
|
||||||
<TableCell>{targetAssignment.roleKey}</TableCell>
|
<TableCell>{targetAssignment.roleKey}</TableCell>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
@@ -13,25 +13,72 @@ import {
|
|||||||
Input,
|
Input,
|
||||||
makeStyles,
|
makeStyles,
|
||||||
Option,
|
Option,
|
||||||
|
shorthands,
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
TableCell,
|
TableCell,
|
||||||
TableHeader,
|
TableHeader,
|
||||||
TableHeaderCell,
|
TableHeaderCell,
|
||||||
TableRow,
|
TableRow,
|
||||||
|
Text,
|
||||||
|
tokens,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
} from "@fluentui/react-components";
|
} from "@fluentui/react-components";
|
||||||
import { AddRegular, DeleteRegular, OpenRegular } from "@fluentui/react-icons";
|
import { AddRegular, DeleteRegular, OpenRegular } from "@fluentui/react-icons";
|
||||||
import { useMemo, useState } from "react";
|
import { ChevronDownRegular, ChevronRightRegular } from "@fluentui/react-icons";
|
||||||
|
import { Fragment, useEffect, useMemo, useState } from "react";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { portalApi } from "../api/portalApi";
|
import { portalApi } from "../api/portalApi";
|
||||||
import { DataState } from "../components/DataState";
|
import { DataState } from "../components/DataState";
|
||||||
import { PageHeader } from "../components/PageHeader";
|
import { PageHeader } from "../components/PageHeader";
|
||||||
|
import type { DeploymentBatch } from "../types/portal";
|
||||||
|
|
||||||
|
const statusOrder = ["New", "Pending", "Running", "Failed", "Completed", "Unknown"];
|
||||||
|
|
||||||
const useStyles = makeStyles({
|
const useStyles = makeStyles({
|
||||||
|
pageActions: {
|
||||||
|
display: "flex",
|
||||||
|
gap: "10px",
|
||||||
|
marginBottom: "16px",
|
||||||
|
},
|
||||||
|
groupRow: {
|
||||||
|
cursor: "pointer",
|
||||||
|
},
|
||||||
|
groupHeader: {
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: "8px",
|
||||||
|
},
|
||||||
|
nestedLevel2Cell: {
|
||||||
|
paddingLeft: "48px",
|
||||||
|
},
|
||||||
actions: {
|
actions: {
|
||||||
display: "flex",
|
display: "flex",
|
||||||
gap: "6px",
|
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",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -40,10 +87,15 @@ export function DeploymentGroupsPage() {
|
|||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const [serviceId, setServiceId] = useState("");
|
const [serviceId, setServiceId] = useState("");
|
||||||
const [templateId, setTemplateId] = useState("");
|
const [templateId, setTemplateId] = useState("");
|
||||||
|
const [templateVersionId, setTemplateVersionId] = useState("");
|
||||||
const [deploymentRuleId, setDeploymentRuleId] = useState("");
|
const [deploymentRuleId, setDeploymentRuleId] = useState("");
|
||||||
const [targetIds, setTargetIds] = useState<string[]>([]);
|
const [targetIds, setTargetIds] = useState<string[]>([]);
|
||||||
const [targetSearch, setTargetSearch] = useState("");
|
const [targetSearch, setTargetSearch] = useState("");
|
||||||
const [dialogOpen, setDialogOpen] = useState(false);
|
const [dialogOpen, setDialogOpen] = useState(false);
|
||||||
|
const [deleteCandidate, setDeleteCandidate] = useState<DeploymentBatch | null>(null);
|
||||||
|
const [selectedIds, setSelectedIds] = useState<string[]>([]);
|
||||||
|
const [expandedStatusGroups, setExpandedStatusGroups] = useState<Record<string, boolean>>({});
|
||||||
|
|
||||||
const { data, error, isLoading } = useQuery({
|
const { data, error, isLoading } = useQuery({
|
||||||
queryKey: ["deployment-batches"],
|
queryKey: ["deployment-batches"],
|
||||||
queryFn: ({ signal }) => portalApi.getDeploymentBatches(signal),
|
queryFn: ({ signal }) => portalApi.getDeploymentBatches(signal),
|
||||||
@@ -64,17 +116,25 @@ export function DeploymentGroupsPage() {
|
|||||||
queryKey: ["targets"],
|
queryKey: ["targets"],
|
||||||
queryFn: ({ signal }) => portalApi.getTargets(signal),
|
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({
|
const { data: deploymentRules } = useQuery({
|
||||||
queryKey: ["deployment-rules"],
|
queryKey: ["deployment-rules"],
|
||||||
queryFn: ({ signal }) => portalApi.getDeploymentRules(signal),
|
queryFn: ({ signal }) => portalApi.getDeploymentRules(signal),
|
||||||
});
|
});
|
||||||
|
|
||||||
const addDeploymentBatch = useMutation({
|
const addDeploymentBatch = useMutation({
|
||||||
mutationFn: portalApi.addDeploymentBatch,
|
mutationFn: portalApi.addDeploymentBatch,
|
||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
setServiceId("");
|
setServiceId("");
|
||||||
setTemplateId("");
|
setTemplateId("");
|
||||||
|
setTemplateVersionId("");
|
||||||
setDeploymentRuleId("");
|
setDeploymentRuleId("");
|
||||||
setTargetIds([]);
|
setTargetIds([]);
|
||||||
|
setTargetSearch("");
|
||||||
setDialogOpen(false);
|
setDialogOpen(false);
|
||||||
await queryClient.invalidateQueries({ queryKey: ["deployment-batches"] });
|
await queryClient.invalidateQueries({ queryKey: ["deployment-batches"] });
|
||||||
await queryClient.invalidateQueries({ queryKey: ["deployments"] });
|
await queryClient.invalidateQueries({ queryKey: ["deployments"] });
|
||||||
@@ -83,6 +143,17 @@ export function DeploymentGroupsPage() {
|
|||||||
const deleteDeploymentBatch = useMutation({
|
const deleteDeploymentBatch = useMutation({
|
||||||
mutationFn: portalApi.deleteDeploymentBatch,
|
mutationFn: portalApi.deleteDeploymentBatch,
|
||||||
onSuccess: async () => {
|
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: ["deployment-batches"] });
|
||||||
await queryClient.invalidateQueries({ queryKey: ["deployments"] });
|
await queryClient.invalidateQueries({ queryKey: ["deployments"] });
|
||||||
},
|
},
|
||||||
@@ -106,73 +177,184 @@ export function DeploymentGroupsPage() {
|
|||||||
filteredTargets.length > 0 &&
|
filteredTargets.length > 0 &&
|
||||||
filteredTargets.every((target) => targetIds.includes(target.id));
|
filteredTargets.every((target) => targetIds.includes(target.id));
|
||||||
|
|
||||||
|
const groupedBatches = useMemo(() => {
|
||||||
|
const groups = (data ?? []).reduce<Record<string, DeploymentBatch[]>>((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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageHeader title="Deployments" description="Deployment Batches und deren Ausfuehrungen." />
|
<PageHeader title="Deployments" description="Deployment Batches und Compositionen nach Status." />
|
||||||
<Button appearance="primary" icon={<AddRegular />} onClick={() => setDialogOpen(true)}>
|
<div className={styles.pageActions}>
|
||||||
Neues Deployment
|
<Button appearance="primary" icon={<AddRegular />} onClick={() => setDialogOpen(true)}>
|
||||||
</Button>
|
Neuer Deployment Batch
|
||||||
<DataState isLoading={isLoading} error={error} />
|
</Button>
|
||||||
{data && (
|
<Button
|
||||||
|
appearance="secondary"
|
||||||
|
disabled={selectedIds.length === 0 || deleteSelectedDeploymentBatches.isPending}
|
||||||
|
icon={<DeleteRegular />}
|
||||||
|
onClick={() => {
|
||||||
|
if (window.confirm(`${selectedIds.length} Deployment Batches wirklich loeschen?`)) {
|
||||||
|
deleteSelectedDeploymentBatches.mutate(selectedIds);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Auswahl loeschen
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<DataState isLoading={isLoading} error={error ?? deleteSelectedDeploymentBatches.error} />
|
||||||
|
{!isLoading && !error && groupedBatches.length === 0 && (
|
||||||
|
<div className={styles.emptyState}>Keine Deployment Batches vorhanden.</div>
|
||||||
|
)}
|
||||||
|
{!isLoading && !error && groupedBatches.length > 0 && (
|
||||||
<Table aria-label="Deployment batches">
|
<Table aria-label="Deployment batches">
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHeaderCell>Service</TableHeaderCell>
|
<TableHeaderCell>
|
||||||
<TableHeaderCell>Template</TableHeaderCell>
|
<Checkbox
|
||||||
<TableHeaderCell>Rule</TableHeaderCell>
|
checked={allSelected}
|
||||||
<TableHeaderCell>Status</TableHeaderCell>
|
onChange={(_, checkboxData) =>
|
||||||
<TableHeaderCell>Created</TableHeaderCell>
|
setSelectedIds(checkboxData.checked ? (data ?? []).map((deploymentBatch) => deploymentBatch.id) : [])
|
||||||
<TableHeaderCell>Modified</TableHeaderCell>
|
}
|
||||||
<TableHeaderCell>Id</TableHeaderCell>
|
/>
|
||||||
<TableHeaderCell>Actions</TableHeaderCell>
|
</TableHeaderCell>
|
||||||
|
<TableHeaderCell>
|
||||||
|
<Text weight="semibold">Service</Text>
|
||||||
|
</TableHeaderCell>
|
||||||
|
<TableHeaderCell>
|
||||||
|
<Text weight="semibold">Primary Template</Text>
|
||||||
|
</TableHeaderCell>
|
||||||
|
<TableHeaderCell>
|
||||||
|
<Text weight="semibold">Templates</Text>
|
||||||
|
</TableHeaderCell>
|
||||||
|
<TableHeaderCell>
|
||||||
|
<Text weight="semibold">Targets</Text>
|
||||||
|
</TableHeaderCell>
|
||||||
|
<TableHeaderCell>
|
||||||
|
<Text weight="semibold">Rule</Text>
|
||||||
|
</TableHeaderCell>
|
||||||
|
<TableHeaderCell>
|
||||||
|
<Text weight="semibold">Created</Text>
|
||||||
|
</TableHeaderCell>
|
||||||
|
<TableHeaderCell>
|
||||||
|
<Text weight="semibold">Batch</Text>
|
||||||
|
</TableHeaderCell>
|
||||||
|
<TableHeaderCell>
|
||||||
|
<Text weight="semibold">Actions</Text>
|
||||||
|
</TableHeaderCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{data.map((deploymentBatch) => (
|
{groupedBatches.map(([status, deploymentBatches]) => (
|
||||||
<TableRow key={deploymentBatch.id}>
|
<Fragment key={`status-${status}`}>
|
||||||
<TableCell>
|
<TableRow className={styles.groupRow} onClick={() => toggleStatusGroup(status)}>
|
||||||
{(() => {
|
<TableCell colSpan={9}>
|
||||||
const template = (templates ?? []).find((entry) => entry.id === deploymentBatch.templateId);
|
<div className={styles.groupHeader}>
|
||||||
const category = (templateCategories ?? []).find((entry) => entry.id === template?.templateCategoryId);
|
{expandedStatusGroups[status] !== false ? <ChevronDownRegular /> : <ChevronRightRegular />}
|
||||||
const service = (services ?? []).find((entry) => entry.id === category?.serviceId);
|
<strong>{status}</strong>
|
||||||
return service?.name ?? "-";
|
</div>
|
||||||
})()}
|
</TableCell>
|
||||||
</TableCell>
|
</TableRow>
|
||||||
<TableCell>{(templates ?? []).find((entry) => entry.id === deploymentBatch.templateId)?.name ?? "-"}</TableCell>
|
{expandedStatusGroups[status] !== false &&
|
||||||
<TableCell>
|
deploymentBatches.map((deploymentBatch) => (
|
||||||
{(deploymentRules ?? []).find((entry) => entry.id === deploymentBatch.deploymentRuleId)?.name ?? "-"}
|
<TableRow key={deploymentBatch.id}>
|
||||||
</TableCell>
|
<TableCell>
|
||||||
<TableCell>{deploymentBatch.status ?? "Unknown"}</TableCell>
|
<Checkbox
|
||||||
<TableCell>{deploymentBatch.created ? new Date(deploymentBatch.created).toLocaleString() : "-"}</TableCell>
|
checked={selectedIds.includes(deploymentBatch.id)}
|
||||||
<TableCell>{deploymentBatch.modified ? new Date(deploymentBatch.modified).toLocaleString() : "-"}</TableCell>
|
onChange={(_, checkboxData) => toggleSelected(deploymentBatch.id, Boolean(checkboxData.checked))}
|
||||||
<TableCell>{deploymentBatch.id}</TableCell>
|
/>
|
||||||
<TableCell>
|
</TableCell>
|
||||||
<div className={styles.actions}>
|
<TableCell className={styles.nestedLevel2Cell}>{getServiceName(deploymentBatch)}</TableCell>
|
||||||
<Tooltip content="Details" relationship="label">
|
<TableCell>{getPrimaryTemplateLabel(deploymentBatch)}</TableCell>
|
||||||
<Link to={`/deployments/${deploymentBatch.id}`}>
|
<TableCell>{deploymentBatch.templateSelectionCount ?? 0}</TableCell>
|
||||||
<Button appearance="subtle" aria-label="Details" icon={<OpenRegular />} />
|
<TableCell>{deploymentBatch.targetAssignmentCount ?? 0}</TableCell>
|
||||||
</Link>
|
<TableCell>
|
||||||
</Tooltip>
|
{(deploymentRules ?? []).find((entry) => entry.id === deploymentBatch.deploymentRuleId)?.name ?? "-"}
|
||||||
<Tooltip content="Delete" relationship="label">
|
</TableCell>
|
||||||
<Button
|
<TableCell>{deploymentBatch.created ? new Date(deploymentBatch.created).toLocaleString() : "-"}</TableCell>
|
||||||
appearance="subtle"
|
<TableCell>
|
||||||
aria-label="Delete"
|
<Tooltip content={deploymentBatch.id} relationship="label">
|
||||||
icon={<DeleteRegular />}
|
<span className={styles.idText}>{deploymentBatch.id.slice(0, 8)}</span>
|
||||||
onClick={() => deleteDeploymentBatch.mutate(deploymentBatch.id)}
|
</Tooltip>
|
||||||
/>
|
</TableCell>
|
||||||
</Tooltip>
|
<TableCell>
|
||||||
</div>
|
<div className={styles.actions}>
|
||||||
</TableCell>
|
<Tooltip content="Details" relationship="label">
|
||||||
</TableRow>
|
<Link to={`/deployments/${deploymentBatch.id}`}>
|
||||||
|
<Button appearance="subtle" aria-label="Details" icon={<OpenRegular />} />
|
||||||
|
</Link>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip content="Delete" relationship="label">
|
||||||
|
<Button
|
||||||
|
appearance="subtle"
|
||||||
|
aria-label="Delete"
|
||||||
|
icon={<DeleteRegular />}
|
||||||
|
onClick={() => setDeleteCandidate(deploymentBatch)}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</Fragment>
|
||||||
))}
|
))}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Dialog open={dialogOpen} onOpenChange={(_, data) => setDialogOpen(data.open)}>
|
<Dialog open={dialogOpen} onOpenChange={(_, data) => setDialogOpen(data.open)}>
|
||||||
<DialogSurface>
|
<DialogSurface>
|
||||||
<DialogBody>
|
<DialogBody>
|
||||||
<DialogTitle>Neues Deployment</DialogTitle>
|
<DialogTitle>Neuer Deployment Batch</DialogTitle>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
|
<Text className={styles.dialogIntro}>
|
||||||
|
Erstellt eine neue Deployment Composition mit einer initialen Template Selection.
|
||||||
|
</Text>
|
||||||
<Field label="Service" required>
|
<Field label="Service" required>
|
||||||
<Combobox
|
<Combobox
|
||||||
placeholder="Service waehlen"
|
placeholder="Service waehlen"
|
||||||
@@ -181,6 +363,7 @@ export function DeploymentGroupsPage() {
|
|||||||
const nextServiceId = data.optionValue ?? "";
|
const nextServiceId = data.optionValue ?? "";
|
||||||
setServiceId(nextServiceId);
|
setServiceId(nextServiceId);
|
||||||
setTemplateId("");
|
setTemplateId("");
|
||||||
|
setTemplateVersionId("");
|
||||||
setTargetIds([]);
|
setTargetIds([]);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -191,12 +374,15 @@ export function DeploymentGroupsPage() {
|
|||||||
))}
|
))}
|
||||||
</Combobox>
|
</Combobox>
|
||||||
</Field>
|
</Field>
|
||||||
<Field label="Template" required>
|
<Field label="Initial Template" required>
|
||||||
<Combobox
|
<Combobox
|
||||||
placeholder={serviceId ? "Template waehlen" : "Erst Service waehlen"}
|
placeholder={serviceId ? "Template waehlen" : "Erst Service waehlen"}
|
||||||
disabled={!serviceId}
|
disabled={!serviceId}
|
||||||
value={templates?.find((template) => template.id === templateId)?.name ?? ""}
|
value={templates?.find((template) => template.id === templateId)?.name ?? ""}
|
||||||
onOptionSelect={(_, data) => setTemplateId(data.optionValue ?? "")}
|
onOptionSelect={(_, data) => {
|
||||||
|
setTemplateId(data.optionValue ?? "");
|
||||||
|
setTemplateVersionId("");
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{(templates ?? [])
|
{(templates ?? [])
|
||||||
.filter((template) => {
|
.filter((template) => {
|
||||||
@@ -210,6 +396,21 @@ export function DeploymentGroupsPage() {
|
|||||||
))}
|
))}
|
||||||
</Combobox>
|
</Combobox>
|
||||||
</Field>
|
</Field>
|
||||||
|
<Field label="Version" required>
|
||||||
|
<Combobox
|
||||||
|
placeholder={templateId ? "Version waehlen" : "Erst Template waehlen"}
|
||||||
|
disabled={!templateId}
|
||||||
|
value={templateVersions?.find((version) => version.id === templateVersionId)?.version ?? ""}
|
||||||
|
onOptionSelect={(_, data) => setTemplateVersionId(data.optionValue ?? "")}
|
||||||
|
>
|
||||||
|
{(templateVersions ?? []).map((version) => (
|
||||||
|
<Option key={version.id} text={version.version} value={version.id}>
|
||||||
|
{version.version}
|
||||||
|
{version.isPublished ? " published" : ""}
|
||||||
|
</Option>
|
||||||
|
))}
|
||||||
|
</Combobox>
|
||||||
|
</Field>
|
||||||
<Field label="Deployment Rule">
|
<Field label="Deployment Rule">
|
||||||
<Combobox
|
<Combobox
|
||||||
placeholder="Rule waehlen (optional)"
|
placeholder="Rule waehlen (optional)"
|
||||||
@@ -224,10 +425,10 @@ export function DeploymentGroupsPage() {
|
|||||||
</Combobox>
|
</Combobox>
|
||||||
</Field>
|
</Field>
|
||||||
{isOnPremService && (
|
{isOnPremService && (
|
||||||
<Field label="Targets" required>
|
<Field label="Initial Targets" required>
|
||||||
<div>
|
<div className={styles.targetPicker}>
|
||||||
<Input
|
<Input
|
||||||
placeholder="VM suchen (Name oder Id)"
|
placeholder="Target suchen (Name oder Id)"
|
||||||
value={targetSearch}
|
value={targetSearch}
|
||||||
onChange={(_, data) => setTargetSearch(data.value)}
|
onChange={(_, data) => setTargetSearch(data.value)}
|
||||||
/>
|
/>
|
||||||
@@ -240,17 +441,17 @@ export function DeploymentGroupsPage() {
|
|||||||
onChange={(_, data) => {
|
onChange={(_, data) => {
|
||||||
if (data.checked) {
|
if (data.checked) {
|
||||||
setTargetIds((prev) =>
|
setTargetIds((prev) =>
|
||||||
Array.from(new Set([...prev, ...filteredTargets.map((vm) => vm.id)])),
|
Array.from(new Set([...prev, ...filteredTargets.map((target) => target.id)])),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
const visibleIds = new Set(filteredTargets.map((vm) => vm.id));
|
const visibleIds = new Set(filteredTargets.map((target) => target.id));
|
||||||
setTargetIds((prev) => prev.filter((id) => !visibleIds.has(id)));
|
setTargetIds((prev) => prev.filter((id) => !visibleIds.has(id)));
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</TableHeaderCell>
|
</TableHeaderCell>
|
||||||
<TableHeaderCell>Name</TableHeaderCell>
|
<TableHeaderCell>Name</TableHeaderCell>
|
||||||
<TableHeaderCell>Id</TableHeaderCell>
|
<TableHeaderCell>Provider</TableHeaderCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
@@ -269,7 +470,7 @@ export function DeploymentGroupsPage() {
|
|||||||
/>
|
/>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>{target.name}</TableCell>
|
<TableCell>{target.name}</TableCell>
|
||||||
<TableCell>{target.id}</TableCell>
|
<TableCell>{target.providerType}</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
@@ -284,17 +485,44 @@ export function DeploymentGroupsPage() {
|
|||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
appearance="primary"
|
appearance="primary"
|
||||||
disabled={!templateId || (isOnPremService && targetIds.length === 0) || addDeploymentBatch.isPending}
|
disabled={!templateId || !templateVersionId || (isOnPremService && targetIds.length === 0) || addDeploymentBatch.isPending}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
addDeploymentBatch.mutate({
|
addDeploymentBatch.mutate({
|
||||||
status: "New",
|
status: "New",
|
||||||
templateId,
|
templateId,
|
||||||
|
templateVersionId,
|
||||||
deploymentRuleId: deploymentRuleId || undefined,
|
deploymentRuleId: deploymentRuleId || undefined,
|
||||||
targetIds: isOnPremService ? targetIds : undefined,
|
targetIds: isOnPremService ? targetIds : undefined,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
Deployment anlegen
|
Batch anlegen
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</DialogBody>
|
||||||
|
</DialogSurface>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
<Dialog open={Boolean(deleteCandidate)} onOpenChange={(_, data) => !data.open && setDeleteCandidate(null)}>
|
||||||
|
<DialogSurface>
|
||||||
|
<DialogBody>
|
||||||
|
<DialogTitle>Deployment Batch loeschen?</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<Text>
|
||||||
|
Der Batch {deleteCandidate?.id.slice(0, 8)} und seine Composition werden geloescht.
|
||||||
|
</Text>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button appearance="secondary" onClick={() => setDeleteCandidate(null)}>
|
||||||
|
Abbrechen
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
appearance="primary"
|
||||||
|
disabled={!deleteCandidate || deleteDeploymentBatch.isPending}
|
||||||
|
icon={<DeleteRegular />}
|
||||||
|
onClick={() => deleteCandidate && deleteDeploymentBatch.mutate(deleteCandidate.id)}
|
||||||
|
>
|
||||||
|
Loeschen
|
||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</DialogBody>
|
</DialogBody>
|
||||||
@@ -303,5 +531,3 @@ export function DeploymentGroupsPage() {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -71,6 +71,11 @@ const useStyles = makeStyles({
|
|||||||
addStepButton: {
|
addStepButton: {
|
||||||
marginTop: "12px",
|
marginTop: "12px",
|
||||||
},
|
},
|
||||||
|
toolbar: {
|
||||||
|
display: "flex",
|
||||||
|
gap: "10px",
|
||||||
|
marginBottom: "16px",
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
function createDefaultStep(sortOrder: number): DeploymentRuleStep {
|
function createDefaultStep(sortOrder: number): DeploymentRuleStep {
|
||||||
@@ -92,6 +97,7 @@ export function DeploymentRulesPage() {
|
|||||||
const [description, setDescription] = useState("");
|
const [description, setDescription] = useState("");
|
||||||
const [isActive, setIsActive] = useState(true);
|
const [isActive, setIsActive] = useState(true);
|
||||||
const [steps, setSteps] = useState<DeploymentRuleStep[]>([createDefaultStep(1)]);
|
const [steps, setSteps] = useState<DeploymentRuleStep[]>([createDefaultStep(1)]);
|
||||||
|
const [selectedIds, setSelectedIds] = useState<string[]>([]);
|
||||||
|
|
||||||
const { data, error, isLoading } = useQuery({
|
const { data, error, isLoading } = useQuery({
|
||||||
queryKey: ["deployment-rules"],
|
queryKey: ["deployment-rules"],
|
||||||
@@ -121,6 +127,15 @@ export function DeploymentRulesPage() {
|
|||||||
await queryClient.invalidateQueries({ queryKey: ["deployment-rules"] });
|
await queryClient.invalidateQueries({ queryKey: ["deployment-rules"] });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
const deleteSelectedRules = useMutation({
|
||||||
|
mutationFn: async (ids: string[]) => {
|
||||||
|
await Promise.all(ids.map((id) => portalApi.deleteDeploymentRule(id)));
|
||||||
|
},
|
||||||
|
onSuccess: async () => {
|
||||||
|
setSelectedIds([]);
|
||||||
|
await queryClient.invalidateQueries({ queryKey: ["deployment-rules"] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const openAddDialog = () => {
|
const openAddDialog = () => {
|
||||||
setDialogMode("add");
|
setDialogMode("add");
|
||||||
@@ -164,18 +179,42 @@ export function DeploymentRulesPage() {
|
|||||||
isActive,
|
isActive,
|
||||||
steps: normalizedSteps,
|
steps: normalizedSteps,
|
||||||
};
|
};
|
||||||
|
const allSelected = Boolean(data?.length) && data!.every((rule) => selectedIds.includes(rule.id));
|
||||||
|
const toggleSelected = (id: string, checked: boolean) => {
|
||||||
|
setSelectedIds((current) => (checked ? [...new Set([...current, id])] : current.filter((entry) => entry !== id)));
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageHeader title="Deployment Rules" description="Low-Code Regeln fuer Deployment Pipelines gestalten." />
|
<PageHeader title="Deployment Rules" description="Low-Code Regeln fuer Deployment Pipelines gestalten." />
|
||||||
<Button appearance="primary" icon={<AddRegular />} onClick={openAddDialog}>
|
<div className={styles.toolbar}>
|
||||||
Neue Rule
|
<Button appearance="primary" icon={<AddRegular />} onClick={openAddDialog}>
|
||||||
</Button>
|
Neue Rule
|
||||||
<DataState isLoading={isLoading} error={error} />
|
</Button>
|
||||||
|
<Button
|
||||||
|
appearance="secondary"
|
||||||
|
disabled={selectedIds.length === 0 || deleteSelectedRules.isPending}
|
||||||
|
icon={<DeleteRegular />}
|
||||||
|
onClick={() => {
|
||||||
|
if (window.confirm(`${selectedIds.length} Deployment Rules wirklich loeschen?`)) {
|
||||||
|
deleteSelectedRules.mutate(selectedIds);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Auswahl loeschen
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<DataState isLoading={isLoading} error={error ?? deleteRule.error ?? deleteSelectedRules.error} />
|
||||||
{data && (
|
{data && (
|
||||||
<Table aria-label="Deployment rules">
|
<Table aria-label="Deployment rules">
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
|
<TableHeaderCell>
|
||||||
|
<Checkbox
|
||||||
|
checked={allSelected}
|
||||||
|
onChange={(_, checkboxData) => setSelectedIds(checkboxData.checked ? data.map((rule) => rule.id) : [])}
|
||||||
|
/>
|
||||||
|
</TableHeaderCell>
|
||||||
<TableHeaderCell>Name</TableHeaderCell>
|
<TableHeaderCell>Name</TableHeaderCell>
|
||||||
<TableHeaderCell>Status</TableHeaderCell>
|
<TableHeaderCell>Status</TableHeaderCell>
|
||||||
<TableHeaderCell>Steps</TableHeaderCell>
|
<TableHeaderCell>Steps</TableHeaderCell>
|
||||||
@@ -186,6 +225,12 @@ export function DeploymentRulesPage() {
|
|||||||
<TableBody>
|
<TableBody>
|
||||||
{data.map((rule) => (
|
{data.map((rule) => (
|
||||||
<TableRow key={rule.id}>
|
<TableRow key={rule.id}>
|
||||||
|
<TableCell>
|
||||||
|
<Checkbox
|
||||||
|
checked={selectedIds.includes(rule.id)}
|
||||||
|
onChange={(_, checkboxData) => toggleSelected(rule.id, Boolean(checkboxData.checked))}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
<TableCell>{rule.name}</TableCell>
|
<TableCell>{rule.name}</TableCell>
|
||||||
<TableCell>{rule.isActive ? "Active" : "Inactive"}</TableCell>
|
<TableCell>{rule.isActive ? "Active" : "Inactive"}</TableCell>
|
||||||
<TableCell>{rule.steps?.length ?? 0}</TableCell>
|
<TableCell>{rule.steps?.length ?? 0}</TableCell>
|
||||||
|
|||||||
@@ -58,6 +58,8 @@ export function DeploymentsPage() {
|
|||||||
await queryClient.invalidateQueries({ queryKey: ["deployment-jobs"] });
|
await queryClient.invalidateQueries({ queryKey: ["deployment-jobs"] });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
const selectedDeploymentBatch = (deploymentBatches ?? []).find((batch) => batch.id === deploymentBatchId);
|
||||||
|
const canUseCompositionTargets = Boolean(selectedDeploymentBatch && selectedDeploymentBatch.targetAssignmentCount > 0);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -65,7 +67,7 @@ export function DeploymentsPage() {
|
|||||||
<FormSection
|
<FormSection
|
||||||
onSubmit={(event) => {
|
onSubmit={(event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
addDeploymentRequest.mutate({ deploymentBatchId, jsonData, targetIds: selectedTargetIds });
|
addDeploymentRequest.mutate({ deploymentGroupId: deploymentBatchId, jsonData, targetIds: selectedTargetIds });
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<FormGrid>
|
<FormGrid>
|
||||||
@@ -109,7 +111,7 @@ export function DeploymentsPage() {
|
|||||||
<FormActions>
|
<FormActions>
|
||||||
<Button
|
<Button
|
||||||
appearance="primary"
|
appearance="primary"
|
||||||
disabled={!deploymentBatchId || selectedTargetIds.length === 0 || addDeploymentRequest.isPending}
|
disabled={!deploymentBatchId || (!canUseCompositionTargets && selectedTargetIds.length === 0) || addDeploymentRequest.isPending}
|
||||||
type="submit"
|
type="submit"
|
||||||
>
|
>
|
||||||
Start deployment
|
Start deployment
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
|
Checkbox,
|
||||||
Combobox,
|
Combobox,
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogActions,
|
DialogActions,
|
||||||
@@ -59,6 +60,7 @@ export function DomainsPage() {
|
|||||||
const [netBIOS, setNetBIOS] = useState("");
|
const [netBIOS, setNetBIOS] = useState("");
|
||||||
const [linkDomainId, setLinkDomainId] = useState("");
|
const [linkDomainId, setLinkDomainId] = useState("");
|
||||||
const [environmentId, setEnvironmentId] = useState("");
|
const [environmentId, setEnvironmentId] = useState("");
|
||||||
|
const [selectedIds, setSelectedIds] = useState<string[]>([]);
|
||||||
const { data, error, isLoading } = useQuery({
|
const { data, error, isLoading } = useQuery({
|
||||||
queryKey: ["domains"],
|
queryKey: ["domains"],
|
||||||
queryFn: ({ signal }) => portalApi.getDomains(signal),
|
queryFn: ({ signal }) => portalApi.getDomains(signal),
|
||||||
@@ -120,6 +122,15 @@ export function DomainsPage() {
|
|||||||
await queryClient.invalidateQueries({ queryKey: ["domains"] });
|
await queryClient.invalidateQueries({ queryKey: ["domains"] });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
const deleteSelectedDomains = useMutation({
|
||||||
|
mutationFn: async (ids: string[]) => {
|
||||||
|
await Promise.all(ids.map((id) => portalApi.deleteDomain(id)));
|
||||||
|
},
|
||||||
|
onSuccess: async () => {
|
||||||
|
setSelectedIds([]);
|
||||||
|
await queryClient.invalidateQueries({ queryKey: ["domains"] });
|
||||||
|
},
|
||||||
|
});
|
||||||
const linkDomainToEnvironment = useMutation({
|
const linkDomainToEnvironment = useMutation({
|
||||||
mutationFn: ({ domainId, environmentId: targetEnvironmentId }: { domainId: string; environmentId: string }) =>
|
mutationFn: ({ domainId, environmentId: targetEnvironmentId }: { domainId: string; environmentId: string }) =>
|
||||||
portalApi.linkDomainToEnvironment(domainId, targetEnvironmentId),
|
portalApi.linkDomainToEnvironment(domainId, targetEnvironmentId),
|
||||||
@@ -145,6 +156,10 @@ export function DomainsPage() {
|
|||||||
|
|
||||||
const formError = addDomain.error?.message ?? updateDomain.error?.message;
|
const formError = addDomain.error?.message ?? updateDomain.error?.message;
|
||||||
const isSaving = addDomain.isPending || updateDomain.isPending;
|
const isSaving = addDomain.isPending || updateDomain.isPending;
|
||||||
|
const allSelected = Boolean(data?.length) && data!.every((domain) => selectedIds.includes(domain.id));
|
||||||
|
const toggleSelected = (id: string, checked: boolean) => {
|
||||||
|
setSelectedIds((current) => (checked ? [...new Set([...current, id])] : current.filter((entry) => entry !== id)));
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -156,6 +171,18 @@ export function DomainsPage() {
|
|||||||
<Button appearance="secondary" icon={<LinkMultiple24Regular />} onClick={openLinkDialog}>
|
<Button appearance="secondary" icon={<LinkMultiple24Regular />} onClick={openLinkDialog}>
|
||||||
Link to Environment
|
Link to Environment
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
appearance="secondary"
|
||||||
|
disabled={selectedIds.length === 0 || deleteSelectedDomains.isPending}
|
||||||
|
icon={<DeleteRegular />}
|
||||||
|
onClick={() => {
|
||||||
|
if (window.confirm(`${selectedIds.length} Domains wirklich loeschen?`)) {
|
||||||
|
deleteSelectedDomains.mutate(selectedIds);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Auswahl loeschen
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Dialog open={dialogMode !== null} onOpenChange={(_, data) => !data.open && closeDialog()}>
|
<Dialog open={dialogMode !== null} onOpenChange={(_, data) => !data.open && closeDialog()}>
|
||||||
@@ -251,11 +278,20 @@ export function DomainsPage() {
|
|||||||
</DialogSurface>
|
</DialogSurface>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
<DataState isLoading={isLoading || environmentsLoading} error={error ?? environmentsError ?? deleteDomain.error ?? linkDomainToEnvironment.error} />
|
<DataState
|
||||||
|
isLoading={isLoading || environmentsLoading}
|
||||||
|
error={error ?? environmentsError ?? deleteDomain.error ?? deleteSelectedDomains.error ?? linkDomainToEnvironment.error}
|
||||||
|
/>
|
||||||
{data && (
|
{data && (
|
||||||
<Table aria-label="Domains">
|
<Table aria-label="Domains">
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
|
<TableHeaderCell>
|
||||||
|
<Checkbox
|
||||||
|
checked={allSelected}
|
||||||
|
onChange={(_, checkboxData) => setSelectedIds(checkboxData.checked ? data.map((domain) => domain.id) : [])}
|
||||||
|
/>
|
||||||
|
</TableHeaderCell>
|
||||||
<TableHeaderCell>Name</TableHeaderCell>
|
<TableHeaderCell>Name</TableHeaderCell>
|
||||||
<TableHeaderCell>FQDN</TableHeaderCell>
|
<TableHeaderCell>FQDN</TableHeaderCell>
|
||||||
<TableHeaderCell>NetBIOS</TableHeaderCell>
|
<TableHeaderCell>NetBIOS</TableHeaderCell>
|
||||||
@@ -266,6 +302,12 @@ export function DomainsPage() {
|
|||||||
<TableBody>
|
<TableBody>
|
||||||
{data.map((domain) => (
|
{data.map((domain) => (
|
||||||
<TableRow key={domain.id}>
|
<TableRow key={domain.id}>
|
||||||
|
<TableCell>
|
||||||
|
<Checkbox
|
||||||
|
checked={selectedIds.includes(domain.id)}
|
||||||
|
onChange={(_, checkboxData) => toggleSelected(domain.id, Boolean(checkboxData.checked))}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
<TableCell>{domain.name}</TableCell>
|
<TableCell>{domain.name}</TableCell>
|
||||||
<TableCell>{domain.fqdn}</TableCell>
|
<TableCell>{domain.fqdn}</TableCell>
|
||||||
<TableCell>{domain.netBIOS}</TableCell>
|
<TableCell>{domain.netBIOS}</TableCell>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
|
Checkbox,
|
||||||
Combobox,
|
Combobox,
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogActions,
|
DialogActions,
|
||||||
@@ -64,6 +65,7 @@ export function EnvironmentsPage() {
|
|||||||
const [metadataJson, setMetadataJson] = useState("");
|
const [metadataJson, setMetadataJson] = useState("");
|
||||||
const [linkEnvironmentId, setLinkEnvironmentId] = useState("");
|
const [linkEnvironmentId, setLinkEnvironmentId] = useState("");
|
||||||
const [domainId, setDomainId] = useState("");
|
const [domainId, setDomainId] = useState("");
|
||||||
|
const [selectedIds, setSelectedIds] = useState<string[]>([]);
|
||||||
const { data, error, isLoading } = useQuery({
|
const { data, error, isLoading } = useQuery({
|
||||||
queryKey: ["environments"],
|
queryKey: ["environments"],
|
||||||
queryFn: ({ signal }) => portalApi.getEnvironments(signal),
|
queryFn: ({ signal }) => portalApi.getEnvironments(signal),
|
||||||
@@ -144,6 +146,15 @@ export function EnvironmentsPage() {
|
|||||||
await queryClient.invalidateQueries({ queryKey: ["environments"] });
|
await queryClient.invalidateQueries({ queryKey: ["environments"] });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
const deleteSelectedEnvironments = useMutation({
|
||||||
|
mutationFn: async (ids: string[]) => {
|
||||||
|
await Promise.all(ids.map((id) => portalApi.deleteEnvironment(id)));
|
||||||
|
},
|
||||||
|
onSuccess: async () => {
|
||||||
|
setSelectedIds([]);
|
||||||
|
await queryClient.invalidateQueries({ queryKey: ["environments"] });
|
||||||
|
},
|
||||||
|
});
|
||||||
const linkDomainToEnvironment = useMutation({
|
const linkDomainToEnvironment = useMutation({
|
||||||
mutationFn: ({ targetDomainId, environmentId }: { targetDomainId: string; environmentId: string }) =>
|
mutationFn: ({ targetDomainId, environmentId }: { targetDomainId: string; environmentId: string }) =>
|
||||||
portalApi.linkDomainToEnvironment(targetDomainId, environmentId),
|
portalApi.linkDomainToEnvironment(targetDomainId, environmentId),
|
||||||
@@ -180,6 +191,10 @@ export function EnvironmentsPage() {
|
|||||||
const isOnPrem = hostingType === "OnPrem";
|
const isOnPrem = hostingType === "OnPrem";
|
||||||
const isAzureTenant = hostingType === "AzureTenant";
|
const isAzureTenant = hostingType === "AzureTenant";
|
||||||
const isM365Tenant = hostingType === "M365Tenant";
|
const isM365Tenant = hostingType === "M365Tenant";
|
||||||
|
const allSelected = Boolean(data?.length) && data!.every((environment) => selectedIds.includes(environment.id));
|
||||||
|
const toggleSelected = (id: string, checked: boolean) => {
|
||||||
|
setSelectedIds((current) => (checked ? [...new Set([...current, id])] : current.filter((entry) => entry !== id)));
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -191,6 +206,18 @@ export function EnvironmentsPage() {
|
|||||||
<Button appearance="secondary" icon={<LinkMultiple24Regular />} onClick={openLinkDialog}>
|
<Button appearance="secondary" icon={<LinkMultiple24Regular />} onClick={openLinkDialog}>
|
||||||
Link to Domain
|
Link to Domain
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
appearance="secondary"
|
||||||
|
disabled={selectedIds.length === 0 || deleteSelectedEnvironments.isPending}
|
||||||
|
icon={<DeleteRegular />}
|
||||||
|
onClick={() => {
|
||||||
|
if (window.confirm(`${selectedIds.length} Environments wirklich loeschen?`)) {
|
||||||
|
deleteSelectedEnvironments.mutate(selectedIds);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Auswahl loeschen
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Dialog open={dialogMode !== null} onOpenChange={(_, data) => !data.open && closeDialog()}>
|
<Dialog open={dialogMode !== null} onOpenChange={(_, data) => !data.open && closeDialog()}>
|
||||||
@@ -368,11 +395,22 @@ export function EnvironmentsPage() {
|
|||||||
</DialogSurface>
|
</DialogSurface>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
<DataState isLoading={isLoading || domainsLoading} error={error ?? domainsError ?? deleteEnvironment.error ?? linkDomainToEnvironment.error} />
|
<DataState
|
||||||
|
isLoading={isLoading || domainsLoading}
|
||||||
|
error={error ?? domainsError ?? deleteEnvironment.error ?? deleteSelectedEnvironments.error ?? linkDomainToEnvironment.error}
|
||||||
|
/>
|
||||||
{data && (
|
{data && (
|
||||||
<Table aria-label="Environments">
|
<Table aria-label="Environments">
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
|
<TableHeaderCell>
|
||||||
|
<Checkbox
|
||||||
|
checked={allSelected}
|
||||||
|
onChange={(_, checkboxData) =>
|
||||||
|
setSelectedIds(checkboxData.checked ? data.map((environment) => environment.id) : [])
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</TableHeaderCell>
|
||||||
<TableHeaderCell>Name</TableHeaderCell>
|
<TableHeaderCell>Name</TableHeaderCell>
|
||||||
<TableHeaderCell>Stage</TableHeaderCell>
|
<TableHeaderCell>Stage</TableHeaderCell>
|
||||||
<TableHeaderCell>Hosting</TableHeaderCell>
|
<TableHeaderCell>Hosting</TableHeaderCell>
|
||||||
@@ -387,6 +425,12 @@ export function EnvironmentsPage() {
|
|||||||
<TableBody>
|
<TableBody>
|
||||||
{data.map((environment) => (
|
{data.map((environment) => (
|
||||||
<TableRow key={environment.id}>
|
<TableRow key={environment.id}>
|
||||||
|
<TableCell>
|
||||||
|
<Checkbox
|
||||||
|
checked={selectedIds.includes(environment.id)}
|
||||||
|
onChange={(_, checkboxData) => toggleSelected(environment.id, Boolean(checkboxData.checked))}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
<TableCell>{environment.name}</TableCell>
|
<TableCell>{environment.name}</TableCell>
|
||||||
<TableCell>{environment.environmentType}</TableCell>
|
<TableCell>{environment.environmentType}</TableCell>
|
||||||
<TableCell>{environment.hostingType}</TableCell>
|
<TableCell>{environment.hostingType}</TableCell>
|
||||||
|
|||||||
@@ -125,6 +125,7 @@ export function ServicesPage() {
|
|||||||
const [roleName, setRoleName] = useState("");
|
const [roleName, setRoleName] = useState("");
|
||||||
const [roleDescription, setRoleDescription] = useState("");
|
const [roleDescription, setRoleDescription] = useState("");
|
||||||
const [serviceFilter, setServiceFilter] = useState<"all" | "cloud" | "onprem">("all");
|
const [serviceFilter, setServiceFilter] = useState<"all" | "cloud" | "onprem">("all");
|
||||||
|
const [selectedIds, setSelectedIds] = useState<string[]>([]);
|
||||||
const [expandedGroups, setExpandedGroups] = useState<Record<string, boolean>>({
|
const [expandedGroups, setExpandedGroups] = useState<Record<string, boolean>>({
|
||||||
onprem: true,
|
onprem: true,
|
||||||
cloud: true,
|
cloud: true,
|
||||||
@@ -192,6 +193,15 @@ export function ServicesPage() {
|
|||||||
await queryClient.invalidateQueries({ queryKey: ["services"] });
|
await queryClient.invalidateQueries({ queryKey: ["services"] });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
const deleteSelectedServices = useMutation({
|
||||||
|
mutationFn: async (ids: string[]) => {
|
||||||
|
await Promise.all(ids.map((id) => portalApi.deleteService(id)));
|
||||||
|
},
|
||||||
|
onSuccess: async () => {
|
||||||
|
setSelectedIds([]);
|
||||||
|
await queryClient.invalidateQueries({ queryKey: ["services"] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const addRoleDefinition = useMutation({
|
const addRoleDefinition = useMutation({
|
||||||
mutationFn: ({ serviceId, roleDefinition }: { serviceId: string; roleDefinition: AddServiceRoleDefinition }) =>
|
mutationFn: ({ serviceId, roleDefinition }: { serviceId: string; roleDefinition: AddServiceRoleDefinition }) =>
|
||||||
@@ -294,6 +304,11 @@ export function ServicesPage() {
|
|||||||
items: filteredServices.filter((service) => Boolean(service.isCloudService)),
|
items: filteredServices.filter((service) => Boolean(service.isCloudService)),
|
||||||
},
|
},
|
||||||
].filter((group) => group.items.length > 0);
|
].filter((group) => group.items.length > 0);
|
||||||
|
const allSelected =
|
||||||
|
filteredServices.length > 0 && filteredServices.every((service) => selectedIds.includes(service.id));
|
||||||
|
const toggleSelected = (id: string, checked: boolean) => {
|
||||||
|
setSelectedIds((current) => (checked ? [...new Set([...current, id])] : current.filter((entry) => entry !== id)));
|
||||||
|
};
|
||||||
|
|
||||||
const toggleGroup = (groupKey: string) => {
|
const toggleGroup = (groupKey: string) => {
|
||||||
setExpandedGroups((current) => ({
|
setExpandedGroups((current) => ({
|
||||||
@@ -310,6 +325,18 @@ export function ServicesPage() {
|
|||||||
<Button appearance="primary" icon={<AddRegular />} onClick={openAddDialog}>
|
<Button appearance="primary" icon={<AddRegular />} onClick={openAddDialog}>
|
||||||
Service hinzufuegen
|
Service hinzufuegen
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
appearance="secondary"
|
||||||
|
disabled={selectedIds.length === 0 || deleteSelectedServices.isPending}
|
||||||
|
icon={<DeleteRegular />}
|
||||||
|
onClick={() => {
|
||||||
|
if (window.confirm(`${selectedIds.length} Services wirklich loeschen?`)) {
|
||||||
|
deleteSelectedServices.mutate(selectedIds);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Auswahl loeschen
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<Field className={styles.filter} label="Service Filter">
|
<Field className={styles.filter} label="Service Filter">
|
||||||
<Combobox
|
<Combobox
|
||||||
@@ -498,11 +525,19 @@ export function ServicesPage() {
|
|||||||
</DialogSurface>
|
</DialogSurface>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
<DataState isLoading={isLoading} error={error ?? deleteService.error} />
|
<DataState isLoading={isLoading} error={error ?? deleteService.error ?? deleteSelectedServices.error} />
|
||||||
{data && (
|
{data && (
|
||||||
<Table aria-label="Services">
|
<Table aria-label="Services">
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
|
<TableHeaderCell>
|
||||||
|
<Checkbox
|
||||||
|
checked={allSelected}
|
||||||
|
onChange={(_, checkboxData) =>
|
||||||
|
setSelectedIds(checkboxData.checked ? filteredServices.map((service) => service.id) : [])
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</TableHeaderCell>
|
||||||
<TableHeaderCell>Name</TableHeaderCell>
|
<TableHeaderCell>Name</TableHeaderCell>
|
||||||
<TableHeaderCell>Description</TableHeaderCell>
|
<TableHeaderCell>Description</TableHeaderCell>
|
||||||
<TableHeaderCell>Id</TableHeaderCell>
|
<TableHeaderCell>Id</TableHeaderCell>
|
||||||
@@ -513,7 +548,7 @@ export function ServicesPage() {
|
|||||||
{groupedServices.map((group) => (
|
{groupedServices.map((group) => (
|
||||||
<Fragment key={group.key}>
|
<Fragment key={group.key}>
|
||||||
<TableRow className={styles.groupRow} onClick={() => toggleGroup(group.key)}>
|
<TableRow className={styles.groupRow} onClick={() => toggleGroup(group.key)}>
|
||||||
<TableCell colSpan={4}>
|
<TableCell colSpan={5}>
|
||||||
<div className={styles.groupHeader}>
|
<div className={styles.groupHeader}>
|
||||||
{expandedGroups[group.key] !== false ? <ChevronDownRegular /> : <ChevronRightRegular />}
|
{expandedGroups[group.key] !== false ? <ChevronDownRegular /> : <ChevronRightRegular />}
|
||||||
<strong>{group.label}</strong>
|
<strong>{group.label}</strong>
|
||||||
@@ -522,6 +557,12 @@ export function ServicesPage() {
|
|||||||
</TableRow>
|
</TableRow>
|
||||||
{expandedGroups[group.key] !== false && group.items.map((service) => (
|
{expandedGroups[group.key] !== false && group.items.map((service) => (
|
||||||
<TableRow key={service.id}>
|
<TableRow key={service.id}>
|
||||||
|
<TableCell>
|
||||||
|
<Checkbox
|
||||||
|
checked={selectedIds.includes(service.id)}
|
||||||
|
onChange={(_, checkboxData) => toggleSelected(service.id, Boolean(checkboxData.checked))}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
<TableCell className={styles.nestedCell}>
|
<TableCell className={styles.nestedCell}>
|
||||||
<div className={styles.nameWithIcon}>
|
<div className={styles.nameWithIcon}>
|
||||||
{getServiceIcon(service.iconKey)}
|
{getServiceIcon(service.iconKey)}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
|
Checkbox,
|
||||||
Combobox,
|
Combobox,
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogActions,
|
DialogActions,
|
||||||
@@ -66,6 +67,7 @@ export function TargetsPage() {
|
|||||||
const [providerType, setProviderType] = useState("OnPrem");
|
const [providerType, setProviderType] = useState("OnPrem");
|
||||||
const [externalId, setExternalId] = useState("");
|
const [externalId, setExternalId] = useState("");
|
||||||
const [metadataJson, setMetadataJson] = useState("");
|
const [metadataJson, setMetadataJson] = useState("");
|
||||||
|
const [selectedIds, setSelectedIds] = useState<string[]>([]);
|
||||||
|
|
||||||
const { data, error, isLoading } = useQuery({
|
const { data, error, isLoading } = useQuery({
|
||||||
queryKey: ["targets"],
|
queryKey: ["targets"],
|
||||||
@@ -132,6 +134,15 @@ export function TargetsPage() {
|
|||||||
await queryClient.invalidateQueries({ queryKey: ["targets"] });
|
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({
|
const linkTargetToDomain = useMutation({
|
||||||
mutationFn: ({ targetId, targetDomainId }: { targetId: string; targetDomainId: string }) =>
|
mutationFn: ({ targetId, targetDomainId }: { targetId: string; targetDomainId: string }) =>
|
||||||
@@ -172,6 +183,10 @@ export function TargetsPage() {
|
|||||||
const domainNameById = new Map((domains ?? []).map((domain: Domain) => [domain.id, domain.name]));
|
const domainNameById = new Map((domains ?? []).map((domain: Domain) => [domain.id, domain.name]));
|
||||||
const linkedTargets = (data ?? []).filter((target) => Boolean(target.domainID));
|
const linkedTargets = (data ?? []).filter((target) => Boolean(target.domainID));
|
||||||
const unlinkedTargets = (data ?? []).filter((target) => !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 (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -203,6 +218,19 @@ export function TargetsPage() {
|
|||||||
>
|
>
|
||||||
Unlink Domain
|
Unlink Domain
|
||||||
</Button>
|
</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>
|
</div>
|
||||||
|
|
||||||
<Dialog open={dialogMode !== null} onOpenChange={(_, dialogData) => !dialogData.open && closeDialog()}>
|
<Dialog open={dialogMode !== null} onOpenChange={(_, dialogData) => !dialogData.open && closeDialog()}>
|
||||||
@@ -373,12 +401,18 @@ export function TargetsPage() {
|
|||||||
|
|
||||||
<DataState
|
<DataState
|
||||||
isLoading={isLoading || domainsLoading}
|
isLoading={isLoading || domainsLoading}
|
||||||
error={error ?? domainsError ?? deleteTarget.error ?? linkTargetToDomain.error ?? unlinkTargetFromDomain.error}
|
error={error ?? domainsError ?? deleteTarget.error ?? deleteSelectedTargets.error ?? linkTargetToDomain.error ?? unlinkTargetFromDomain.error}
|
||||||
/>
|
/>
|
||||||
{data && (
|
{data && (
|
||||||
<Table aria-label="Targets">
|
<Table aria-label="Targets">
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
|
<TableHeaderCell>
|
||||||
|
<Checkbox
|
||||||
|
checked={allSelected}
|
||||||
|
onChange={(_, checkboxData) => setSelectedIds(checkboxData.checked ? data.map((target) => target.id) : [])}
|
||||||
|
/>
|
||||||
|
</TableHeaderCell>
|
||||||
<TableHeaderCell>Name</TableHeaderCell>
|
<TableHeaderCell>Name</TableHeaderCell>
|
||||||
<TableHeaderCell>Type</TableHeaderCell>
|
<TableHeaderCell>Type</TableHeaderCell>
|
||||||
<TableHeaderCell>Provider</TableHeaderCell>
|
<TableHeaderCell>Provider</TableHeaderCell>
|
||||||
@@ -391,6 +425,12 @@ export function TargetsPage() {
|
|||||||
<TableBody>
|
<TableBody>
|
||||||
{data.map((target) => (
|
{data.map((target) => (
|
||||||
<TableRow key={target.id}>
|
<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.name}</TableCell>
|
||||||
<TableCell>{target.targetType}</TableCell>
|
<TableCell>{target.targetType}</TableCell>
|
||||||
<TableCell>{target.providerType}</TableCell>
|
<TableCell>{target.providerType}</TableCell>
|
||||||
|
|||||||
@@ -108,6 +108,7 @@ export function TemplateCategoriesPage() {
|
|||||||
const [isActive, setIsActive] = useState(true);
|
const [isActive, setIsActive] = useState(true);
|
||||||
const [color, setColor] = useState("");
|
const [color, setColor] = useState("");
|
||||||
const [serviceFilterId, setServiceFilterId] = useState("all");
|
const [serviceFilterId, setServiceFilterId] = useState("all");
|
||||||
|
const [selectedIds, setSelectedIds] = useState<string[]>([]);
|
||||||
const [expandedGroups, setExpandedGroups] = useState<Record<string, boolean>>({});
|
const [expandedGroups, setExpandedGroups] = useState<Record<string, boolean>>({});
|
||||||
const { data, error, isLoading } = useQuery({
|
const { data, error, isLoading } = useQuery({
|
||||||
queryKey: ["template-categories"],
|
queryKey: ["template-categories"],
|
||||||
@@ -176,6 +177,15 @@ export function TemplateCategoriesPage() {
|
|||||||
await queryClient.invalidateQueries({ queryKey: ["template-categories"] });
|
await queryClient.invalidateQueries({ queryKey: ["template-categories"] });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
const deleteSelectedTemplateCategories = useMutation({
|
||||||
|
mutationFn: async (ids: string[]) => {
|
||||||
|
await Promise.all(ids.map((id) => portalApi.deleteTemplateCategory(id)));
|
||||||
|
},
|
||||||
|
onSuccess: async () => {
|
||||||
|
setSelectedIds([]);
|
||||||
|
await queryClient.invalidateQueries({ queryKey: ["template-categories"] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const submitTemplateCategory = (event: React.FormEvent<HTMLFormElement>) => {
|
const submitTemplateCategory = (event: React.FormEvent<HTMLFormElement>) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@@ -212,6 +222,12 @@ export function TemplateCategoriesPage() {
|
|||||||
.sort((a, b) => a.name.localeCompare(b.name)),
|
.sort((a, b) => a.name.localeCompare(b.name)),
|
||||||
}))
|
}))
|
||||||
.filter((group) => group.items.length > 0);
|
.filter((group) => group.items.length > 0);
|
||||||
|
const allSelected =
|
||||||
|
filteredTemplateCategories.length > 0 &&
|
||||||
|
filteredTemplateCategories.every((templateCategory) => selectedIds.includes(templateCategory.id));
|
||||||
|
const toggleSelected = (id: string, checked: boolean) => {
|
||||||
|
setSelectedIds((current) => (checked ? [...new Set([...current, id])] : current.filter((entry) => entry !== id)));
|
||||||
|
};
|
||||||
|
|
||||||
const toggleGroup = (groupId: string) => {
|
const toggleGroup = (groupId: string) => {
|
||||||
setExpandedGroups((current) => ({
|
setExpandedGroups((current) => ({
|
||||||
@@ -228,6 +244,18 @@ export function TemplateCategoriesPage() {
|
|||||||
<Button appearance="primary" icon={<AddRegular />} onClick={openAddDialog}>
|
<Button appearance="primary" icon={<AddRegular />} onClick={openAddDialog}>
|
||||||
Template Category hinzufuegen
|
Template Category hinzufuegen
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
appearance="secondary"
|
||||||
|
disabled={selectedIds.length === 0 || deleteSelectedTemplateCategories.isPending}
|
||||||
|
icon={<DeleteRegular />}
|
||||||
|
onClick={() => {
|
||||||
|
if (window.confirm(`${selectedIds.length} Template Categories wirklich loeschen?`)) {
|
||||||
|
deleteSelectedTemplateCategories.mutate(selectedIds);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Auswahl loeschen
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<Field className={styles.filter} label="Service Filter">
|
<Field className={styles.filter} label="Service Filter">
|
||||||
<Combobox
|
<Combobox
|
||||||
@@ -316,11 +344,24 @@ export function TemplateCategoriesPage() {
|
|||||||
</DialogSurface>
|
</DialogSurface>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
<DataState isLoading={isLoading || servicesLoading} error={error ?? servicesError ?? deleteTemplateCategory.error} />
|
<DataState
|
||||||
|
isLoading={isLoading || servicesLoading}
|
||||||
|
error={error ?? servicesError ?? deleteTemplateCategory.error ?? deleteSelectedTemplateCategories.error}
|
||||||
|
/>
|
||||||
{data && (
|
{data && (
|
||||||
<Table aria-label="Template Categories">
|
<Table aria-label="Template Categories">
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
|
<TableHeaderCell>
|
||||||
|
<Checkbox
|
||||||
|
checked={allSelected}
|
||||||
|
onChange={(_, checkboxData) =>
|
||||||
|
setSelectedIds(
|
||||||
|
checkboxData.checked ? filteredTemplateCategories.map((templateCategory) => templateCategory.id) : [],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</TableHeaderCell>
|
||||||
<TableHeaderCell>Name</TableHeaderCell>
|
<TableHeaderCell>Name</TableHeaderCell>
|
||||||
<TableHeaderCell>Active</TableHeaderCell>
|
<TableHeaderCell>Active</TableHeaderCell>
|
||||||
<TableHeaderCell>Id</TableHeaderCell>
|
<TableHeaderCell>Id</TableHeaderCell>
|
||||||
@@ -335,7 +376,7 @@ export function TemplateCategoriesPage() {
|
|||||||
key={`group-${group.serviceId}`}
|
key={`group-${group.serviceId}`}
|
||||||
onClick={() => toggleGroup(group.serviceId)}
|
onClick={() => toggleGroup(group.serviceId)}
|
||||||
>
|
>
|
||||||
<TableCell colSpan={4}>
|
<TableCell colSpan={5}>
|
||||||
<div className={styles.groupHeader}>
|
<div className={styles.groupHeader}>
|
||||||
{expandedGroups[group.serviceId] !== false ? <ChevronDownRegular /> : <ChevronRightRegular />}
|
{expandedGroups[group.serviceId] !== false ? <ChevronDownRegular /> : <ChevronRightRegular />}
|
||||||
<strong>{group.serviceName}</strong>
|
<strong>{group.serviceName}</strong>
|
||||||
@@ -344,6 +385,12 @@ export function TemplateCategoriesPage() {
|
|||||||
</TableRow>
|
</TableRow>
|
||||||
{expandedGroups[group.serviceId] !== false && group.items.map((templateCategory) => (
|
{expandedGroups[group.serviceId] !== false && group.items.map((templateCategory) => (
|
||||||
<TableRow key={templateCategory.id}>
|
<TableRow key={templateCategory.id}>
|
||||||
|
<TableCell>
|
||||||
|
<Checkbox
|
||||||
|
checked={selectedIds.includes(templateCategory.id)}
|
||||||
|
onChange={(_, checkboxData) => toggleSelected(templateCategory.id, Boolean(checkboxData.checked))}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
<TableCell className={styles.nestedCell}>{templateCategory.name}</TableCell>
|
<TableCell className={styles.nestedCell}>{templateCategory.name}</TableCell>
|
||||||
<TableCell>{templateCategory.isActive ? "Ja" : "Nein"}</TableCell>
|
<TableCell>{templateCategory.isActive ? "Ja" : "Nein"}</TableCell>
|
||||||
<TableCell>{templateCategory.id}</TableCell>
|
<TableCell>{templateCategory.id}</TableCell>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
|
Checkbox,
|
||||||
Combobox,
|
Combobox,
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogActions,
|
DialogActions,
|
||||||
@@ -161,6 +162,7 @@ export function TemplatesPage() {
|
|||||||
const [jsonResources, setJsonResources] = useState("[]");
|
const [jsonResources, setJsonResources] = useState("[]");
|
||||||
const [editorTab, setEditorTab] = useState<TemplateEditorTab>("parameters");
|
const [editorTab, setEditorTab] = useState<TemplateEditorTab>("parameters");
|
||||||
const [serviceFilterId, setServiceFilterId] = useState("all");
|
const [serviceFilterId, setServiceFilterId] = useState("all");
|
||||||
|
const [selectedIds, setSelectedIds] = useState<string[]>([]);
|
||||||
const [expandedServiceGroups, setExpandedServiceGroups] = useState<Record<string, boolean>>({});
|
const [expandedServiceGroups, setExpandedServiceGroups] = useState<Record<string, boolean>>({});
|
||||||
const [expandedCategoryGroups, setExpandedCategoryGroups] = useState<Record<string, boolean>>({});
|
const [expandedCategoryGroups, setExpandedCategoryGroups] = useState<Record<string, boolean>>({});
|
||||||
const { data, error, isLoading } = useQuery({
|
const { data, error, isLoading } = useQuery({
|
||||||
@@ -255,6 +257,15 @@ export function TemplatesPage() {
|
|||||||
await queryClient.invalidateQueries({ queryKey: ["templates"] });
|
await queryClient.invalidateQueries({ queryKey: ["templates"] });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
const deleteSelectedTemplates = useMutation({
|
||||||
|
mutationFn: async (ids: string[]) => {
|
||||||
|
await Promise.all(ids.map((id) => portalApi.deleteTemplate(id)));
|
||||||
|
},
|
||||||
|
onSuccess: async () => {
|
||||||
|
setSelectedIds([]);
|
||||||
|
await queryClient.invalidateQueries({ queryKey: ["templates"] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const submitTemplate = (event: React.FormEvent<HTMLFormElement>) => {
|
const submitTemplate = (event: React.FormEvent<HTMLFormElement>) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@@ -326,6 +337,11 @@ export function TemplatesPage() {
|
|||||||
.sort((a, b) => a.categoryName.localeCompare(b.categoryName)),
|
.sort((a, b) => a.categoryName.localeCompare(b.categoryName)),
|
||||||
}))
|
}))
|
||||||
.filter((group) => group.categories.length > 0);
|
.filter((group) => group.categories.length > 0);
|
||||||
|
const allSelected =
|
||||||
|
filteredTemplates.length > 0 && filteredTemplates.every((template) => selectedIds.includes(template.id));
|
||||||
|
const toggleSelected = (id: string, checked: boolean) => {
|
||||||
|
setSelectedIds((current) => (checked ? [...new Set([...current, id])] : current.filter((entry) => entry !== id)));
|
||||||
|
};
|
||||||
|
|
||||||
const toggleServiceGroup = (serviceId: string) => {
|
const toggleServiceGroup = (serviceId: string) => {
|
||||||
setExpandedServiceGroups((current) => ({
|
setExpandedServiceGroups((current) => ({
|
||||||
@@ -350,6 +366,18 @@ export function TemplatesPage() {
|
|||||||
<Button appearance="primary" icon={<AddRegular />} onClick={openAddDialog}>
|
<Button appearance="primary" icon={<AddRegular />} onClick={openAddDialog}>
|
||||||
Template hinzufuegen
|
Template hinzufuegen
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
appearance="secondary"
|
||||||
|
disabled={selectedIds.length === 0 || deleteSelectedTemplates.isPending}
|
||||||
|
icon={<DeleteRegular />}
|
||||||
|
onClick={() => {
|
||||||
|
if (window.confirm(`${selectedIds.length} Templates wirklich loeschen?`)) {
|
||||||
|
deleteSelectedTemplates.mutate(selectedIds);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Auswahl loeschen
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<Field className={styles.filter} label="Service Filter">
|
<Field className={styles.filter} label="Service Filter">
|
||||||
<Combobox
|
<Combobox
|
||||||
@@ -487,12 +515,20 @@ export function TemplatesPage() {
|
|||||||
|
|
||||||
<DataState
|
<DataState
|
||||||
isLoading={isLoading || templateCategoriesLoading || servicesLoading}
|
isLoading={isLoading || templateCategoriesLoading || servicesLoading}
|
||||||
error={error ?? templateCategoriesError ?? servicesError ?? deleteTemplate.error}
|
error={error ?? templateCategoriesError ?? servicesError ?? deleteTemplate.error ?? deleteSelectedTemplates.error}
|
||||||
/>
|
/>
|
||||||
{data && (
|
{data && (
|
||||||
<Table aria-label="Templates">
|
<Table aria-label="Templates">
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
|
<TableHeaderCell>
|
||||||
|
<Checkbox
|
||||||
|
checked={allSelected}
|
||||||
|
onChange={(_, checkboxData) =>
|
||||||
|
setSelectedIds(checkboxData.checked ? filteredTemplates.map((template) => template.id) : [])
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</TableHeaderCell>
|
||||||
<TableHeaderCell>Name</TableHeaderCell>
|
<TableHeaderCell>Name</TableHeaderCell>
|
||||||
<TableHeaderCell>Version</TableHeaderCell>
|
<TableHeaderCell>Version</TableHeaderCell>
|
||||||
<TableHeaderCell>Template Category</TableHeaderCell>
|
<TableHeaderCell>Template Category</TableHeaderCell>
|
||||||
@@ -504,7 +540,7 @@ export function TemplatesPage() {
|
|||||||
{groupedTemplates.map((group) => (
|
{groupedTemplates.map((group) => (
|
||||||
<Fragment key={`group-${group.serviceId}`}>
|
<Fragment key={`group-${group.serviceId}`}>
|
||||||
<TableRow className={styles.groupRow} onClick={() => toggleServiceGroup(group.serviceId)}>
|
<TableRow className={styles.groupRow} onClick={() => toggleServiceGroup(group.serviceId)}>
|
||||||
<TableCell colSpan={5}>
|
<TableCell colSpan={6}>
|
||||||
<div className={styles.groupHeader}>
|
<div className={styles.groupHeader}>
|
||||||
{expandedServiceGroups[group.serviceId] !== false ? <ChevronDownRegular /> : <ChevronRightRegular />}
|
{expandedServiceGroups[group.serviceId] !== false ? <ChevronDownRegular /> : <ChevronRightRegular />}
|
||||||
<strong>{group.serviceName}</strong>
|
<strong>{group.serviceName}</strong>
|
||||||
@@ -517,7 +553,7 @@ export function TemplatesPage() {
|
|||||||
className={styles.groupRow}
|
className={styles.groupRow}
|
||||||
onClick={() => toggleCategoryGroup(group.serviceId, categoryGroup.categoryId)}
|
onClick={() => toggleCategoryGroup(group.serviceId, categoryGroup.categoryId)}
|
||||||
>
|
>
|
||||||
<TableCell className={styles.categoryHeaderCell} colSpan={5}>
|
<TableCell className={styles.categoryHeaderCell} colSpan={6}>
|
||||||
<div className={styles.groupHeader}>
|
<div className={styles.groupHeader}>
|
||||||
{expandedCategoryGroups[`${group.serviceId}:${categoryGroup.categoryId}`] !== false ? (
|
{expandedCategoryGroups[`${group.serviceId}:${categoryGroup.categoryId}`] !== false ? (
|
||||||
<ChevronDownRegular />
|
<ChevronDownRegular />
|
||||||
@@ -531,6 +567,12 @@ export function TemplatesPage() {
|
|||||||
{expandedCategoryGroups[`${group.serviceId}:${categoryGroup.categoryId}`] !== false &&
|
{expandedCategoryGroups[`${group.serviceId}:${categoryGroup.categoryId}`] !== false &&
|
||||||
categoryGroup.items.map((template) => (
|
categoryGroup.items.map((template) => (
|
||||||
<TableRow key={template.id}>
|
<TableRow key={template.id}>
|
||||||
|
<TableCell>
|
||||||
|
<Checkbox
|
||||||
|
checked={selectedIds.includes(template.id)}
|
||||||
|
onChange={(_, checkboxData) => toggleSelected(template.id, Boolean(checkboxData.checked))}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
<TableCell className={styles.nestedLevel2Cell}>{template.name}</TableCell>
|
<TableCell className={styles.nestedLevel2Cell}>{template.name}</TableCell>
|
||||||
<TableCell>{template.version}</TableCell>
|
<TableCell>{template.version}</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
|
|||||||
@@ -22,19 +22,24 @@ export type AddDeploymentExecution = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type AddDeploymentRequest = {
|
export type AddDeploymentRequest = {
|
||||||
deploymentBatchId: string;
|
deploymentGroupId: string;
|
||||||
targetIds: string[];
|
targetIds: string[];
|
||||||
jsonData: string;
|
jsonData: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DeploymentJob = {
|
export type DeploymentJob = {
|
||||||
id: string;
|
id: string;
|
||||||
|
correlationId: string;
|
||||||
type: string;
|
type: string;
|
||||||
status: string;
|
status: string;
|
||||||
|
priority: number;
|
||||||
|
scheduledAt?: string;
|
||||||
attempts: number;
|
attempts: number;
|
||||||
maxAttempts: number;
|
maxAttempts: number;
|
||||||
started?: string;
|
started?: string;
|
||||||
finished?: string;
|
finished?: string;
|
||||||
|
heartbeatAt?: string;
|
||||||
|
workerName?: string;
|
||||||
errorMessage?: string;
|
errorMessage?: string;
|
||||||
targetCount: number;
|
targetCount: number;
|
||||||
succeededTargetCount: number;
|
succeededTargetCount: number;
|
||||||
@@ -48,7 +53,10 @@ export type DeploymentJobTarget = {
|
|||||||
templateId: string;
|
templateId: string;
|
||||||
status: string;
|
status: string;
|
||||||
attempts: number;
|
attempts: number;
|
||||||
|
started?: string;
|
||||||
|
finished?: string;
|
||||||
errorMessage?: string;
|
errorMessage?: string;
|
||||||
|
outputMetadataJson?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DeploymentJobStep = {
|
export type DeploymentJobStep = {
|
||||||
@@ -59,6 +67,10 @@ export type DeploymentJobStep = {
|
|||||||
stepType: string;
|
stepType: string;
|
||||||
status: string;
|
status: string;
|
||||||
metadataJson?: string;
|
metadataJson?: string;
|
||||||
|
started?: string;
|
||||||
|
finished?: string;
|
||||||
|
outputMetadataJson?: string;
|
||||||
|
errorMessage?: string;
|
||||||
approvedAt?: string;
|
approvedAt?: string;
|
||||||
approvedBy?: string;
|
approvedBy?: string;
|
||||||
approvalComment?: string;
|
approvalComment?: string;
|
||||||
@@ -66,13 +78,20 @@ export type DeploymentJobStep = {
|
|||||||
|
|
||||||
export type DeploymentJobDetails = {
|
export type DeploymentJobDetails = {
|
||||||
id: string;
|
id: string;
|
||||||
|
correlationId: string;
|
||||||
type: string;
|
type: string;
|
||||||
status: string;
|
status: string;
|
||||||
payloadJson: string;
|
payloadJson: string;
|
||||||
|
priority: number;
|
||||||
|
scheduledAt?: string;
|
||||||
attempts: number;
|
attempts: number;
|
||||||
maxAttempts: number;
|
maxAttempts: number;
|
||||||
started?: string;
|
started?: string;
|
||||||
finished?: string;
|
finished?: string;
|
||||||
|
heartbeatAt?: string;
|
||||||
|
workerName?: string;
|
||||||
|
lockedBy?: string;
|
||||||
|
lockedUntil?: string;
|
||||||
errorMessage?: string;
|
errorMessage?: string;
|
||||||
targets: DeploymentJobTarget[];
|
targets: DeploymentJobTarget[];
|
||||||
steps: DeploymentJobStep[];
|
steps: DeploymentJobStep[];
|
||||||
@@ -83,6 +102,11 @@ export type DeploymentBatch = {
|
|||||||
templateId?: string;
|
templateId?: string;
|
||||||
deploymentRuleId?: string;
|
deploymentRuleId?: string;
|
||||||
status?: string;
|
status?: string;
|
||||||
|
primaryTemplateVersionId?: string;
|
||||||
|
primaryTemplateName?: string;
|
||||||
|
primaryTemplateVersion?: string;
|
||||||
|
templateSelectionCount: number;
|
||||||
|
targetAssignmentCount: number;
|
||||||
created?: string;
|
created?: string;
|
||||||
createdBy?: string;
|
createdBy?: string;
|
||||||
modified?: string;
|
modified?: string;
|
||||||
@@ -97,10 +121,13 @@ export type DeploymentBatchDetails = DeploymentBatch & {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type AddDeploymentBatch = {
|
export type AddDeploymentBatch = {
|
||||||
templateId: string;
|
templateId?: string;
|
||||||
|
templateVersionId?: string;
|
||||||
deploymentRuleId?: string;
|
deploymentRuleId?: string;
|
||||||
status: string;
|
status: string;
|
||||||
targetIds?: string[];
|
targetIds?: string[];
|
||||||
|
templateSelections?: AddDeploymentTemplateSelection[];
|
||||||
|
targetAssignments?: AddDeploymentTargetAssignment[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DeploymentRuleStep = {
|
export type DeploymentRuleStep = {
|
||||||
|
|||||||
Reference in New Issue
Block a user