feat: Refactor deployment types and add new deployment rules management
- Updated DeploymentExecution and related types to use targetId instead of virtualMachineId. - Introduced new types for DeploymentRule, DeploymentRuleStep, and related entities. - Added DeploymentRulesPage for managing deployment rules with CRUD operations. - Created TargetDetailsPage and TargetsPage for managing targets with linking and unlinking functionality. - Enhanced UI components with Fluent UI for better user experience. - Implemented data fetching and state management using React Query.
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
const apiBaseUrl = import.meta.env.VITE_API_BASE_URL ?? "/api";
|
||||
const apiBaseUrl = import.meta.env.VITE_API_BASE_URL ?? "/api";
|
||||
|
||||
export class ApiError extends Error {
|
||||
constructor(
|
||||
@@ -116,3 +116,4 @@ export async function deleteJson<TResult = unknown>(path: string): Promise<TResu
|
||||
return text as TResult;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { deleteJson, getJson, postJson, putJson } from "./httpClient";
|
||||
import { deleteJson, getJson, postJson, putJson } from "./httpClient";
|
||||
import type {
|
||||
AddDeploymentExecution,
|
||||
AddDeploymentParameterValue,
|
||||
AddDeploymentRequest,
|
||||
AddDeploymentBatch,
|
||||
AddDeploymentTargetAssignment,
|
||||
AddDeploymentTemplateSelection,
|
||||
AddDomain,
|
||||
AddEnvironment,
|
||||
AddRunbook,
|
||||
@@ -15,7 +18,7 @@ import type {
|
||||
DeploymentBatchDetails,
|
||||
Domain,
|
||||
DomainWithEnvironments,
|
||||
DomainWithVirtualMachines,
|
||||
DomainWithTargets,
|
||||
EnvironmentItem,
|
||||
EnvironmentWithDomains,
|
||||
Runbook,
|
||||
@@ -23,10 +26,13 @@ import type {
|
||||
ServiceRoleDefinition,
|
||||
TemplateCategory,
|
||||
Template,
|
||||
VirtualMachine,
|
||||
AddVirtualMachine,
|
||||
TemplateVersion,
|
||||
Target,
|
||||
AddTarget,
|
||||
DeploymentJob,
|
||||
DeploymentJobDetails,
|
||||
DeploymentRule,
|
||||
AddDeploymentRule,
|
||||
} from "../types/portal";
|
||||
|
||||
export const portalApi = {
|
||||
@@ -41,17 +47,38 @@ export const portalApi = {
|
||||
postJson<{ comment?: string }, void>(`/Deployment/QueueJobs/Steps/${stepId}/Approve`, { comment }),
|
||||
rejectDeploymentJobStep: (stepId: string, comment?: string) =>
|
||||
postJson<{ comment?: string }, void>(`/Deployment/QueueJobs/Steps/${stepId}/Reject`, { comment }),
|
||||
getDeploymentBatches: (signal?: AbortSignal) => getJson<DeploymentBatch[]>("/DeploymentGroup", signal),
|
||||
getDeploymentBatches: (signal?: AbortSignal) => getJson<DeploymentBatch[]>("/deployment-batches", signal),
|
||||
getDeploymentBatchById: (deploymentBatchId: string, signal?: AbortSignal) =>
|
||||
getJson<DeploymentBatchDetails>(`/DeploymentGroup/${deploymentBatchId}`, signal),
|
||||
addDeploymentBatch: (deploymentBatch: AddDeploymentBatch) => postJson<AddDeploymentBatch, string>("/DeploymentGroup", deploymentBatch),
|
||||
getJson<DeploymentBatchDetails>(`/deployment-batches/${deploymentBatchId}`, signal),
|
||||
addDeploymentBatch: (deploymentBatch: AddDeploymentBatch) => postJson<AddDeploymentBatch, string>("/deployment-batches", deploymentBatch),
|
||||
deleteDeploymentBatch: (deploymentBatchId: string) => deleteJson<void>(`/deployment-batches/${deploymentBatchId}`),
|
||||
addDeploymentTemplateSelection: (deploymentBatchId: string, templateSelection: AddDeploymentTemplateSelection) =>
|
||||
postJson<AddDeploymentTemplateSelection, string>(`/deployment-batches/${deploymentBatchId}/template-selections`, templateSelection),
|
||||
deleteDeploymentTemplateSelection: (deploymentBatchId: string, templateSelectionId: string) =>
|
||||
deleteJson<void>(`/deployment-batches/${deploymentBatchId}/template-selections/${templateSelectionId}`),
|
||||
addDeploymentParameterValue: (deploymentBatchId: string, parameterValue: AddDeploymentParameterValue) =>
|
||||
postJson<AddDeploymentParameterValue, string>(`/deployment-batches/${deploymentBatchId}/parameter-values`, parameterValue),
|
||||
deleteDeploymentParameterValue: (deploymentBatchId: string, parameterValueId: string) =>
|
||||
deleteJson<void>(`/deployment-batches/${deploymentBatchId}/parameter-values/${parameterValueId}`),
|
||||
addDeploymentTargetAssignment: (deploymentBatchId: string, targetAssignment: AddDeploymentTargetAssignment) =>
|
||||
postJson<AddDeploymentTargetAssignment, string>(`/deployment-batches/${deploymentBatchId}/target-assignments`, targetAssignment),
|
||||
deleteDeploymentTargetAssignment: (deploymentBatchId: string, targetAssignmentId: string) =>
|
||||
deleteJson<void>(`/deployment-batches/${deploymentBatchId}/target-assignments/${targetAssignmentId}`),
|
||||
deleteDeploymentExecution: (deploymentBatchId: string, targetId: string) =>
|
||||
deleteJson<void>(`/Deployment/Batch/${deploymentBatchId}/Target/${targetId}`),
|
||||
getDeploymentRules: (signal?: AbortSignal) => getJson<DeploymentRule[]>("/deployment-rules", signal),
|
||||
addDeploymentRule: (rule: AddDeploymentRule) => postJson<AddDeploymentRule, string>("/deployment-rules", rule),
|
||||
updateDeploymentRule: (ruleId: string, rule: AddDeploymentRule) => putJson<AddDeploymentRule, void>(`/deployment-rules/${ruleId}`, rule),
|
||||
deleteDeploymentRule: (ruleId: string) => deleteJson<void>(`/deployment-rules/${ruleId}`),
|
||||
getDomains: (signal?: AbortSignal) => getJson<Domain[]>("/Domain", signal),
|
||||
getDomainEnvironments: (domainId: string, signal?: AbortSignal) => getJson<DomainWithEnvironments>(`/Domain/${domainId}/Environments`, signal),
|
||||
getDomainVirtualMachines: (domainId: string, signal?: AbortSignal) => getJson<DomainWithVirtualMachines>(`/Domain/${domainId}/VirtualMachines`, signal),
|
||||
getDomainTargets: (domainId: string, signal?: AbortSignal) => getJson<DomainWithTargets>(`/Domain/${domainId}/Targets`, signal),
|
||||
addDomain: (domain: AddDomain) => postJson<AddDomain, string>("/Domain", domain),
|
||||
updateDomain: (domainId: string, domain: AddDomain) => putJson<AddDomain, void>(`/Domain/${domainId}`, domain),
|
||||
deleteDomain: (domainId: string) => deleteJson<void>(`/Domain/${domainId}`),
|
||||
linkDomainToEnvironment: (domainId: string, environmentId: string) => postJson<undefined, string>(`/Domain/${domainId}/Environment/${environmentId}`, undefined),
|
||||
unlinkDomainFromEnvironment: (domainId: string, environmentId: string) =>
|
||||
deleteJson<void>(`/Domain/${domainId}/Environment/${environmentId}`),
|
||||
getEnvironments: (signal?: AbortSignal) => getJson<EnvironmentItem[]>("/Environment", signal),
|
||||
getEnvironmentDomains: (environmentId: string, signal?: AbortSignal) => getJson<EnvironmentWithDomains>(`/Environment/${environmentId}/Domains`, signal),
|
||||
addEnvironment: (environment: AddEnvironment) => postJson<AddEnvironment, string>("/Environment", environment),
|
||||
@@ -60,6 +87,7 @@ export const portalApi = {
|
||||
getRunbooks: (signal?: AbortSignal) => getJson<Runbook[]>("/Runbook", signal),
|
||||
addRunbook: (runbook: AddRunbook) => postJson<AddRunbook, string>("/Runbook", runbook),
|
||||
getTemplates: (signal?: AbortSignal) => getJson<Template[]>("/Template", signal),
|
||||
getTemplateVersions: (templateId: string, signal?: AbortSignal) => getJson<TemplateVersion[]>(`/Template/${templateId}/Versions`, signal),
|
||||
addTemplate: (template: AddTemplate) => postJson<AddTemplate, string>("/Template", template),
|
||||
updateTemplate: (templateId: string, template: AddTemplate) => putJson<AddTemplate, void>(`/Template/${templateId}`, template),
|
||||
deleteTemplate: (templateId: string) => deleteJson<void>(`/Template/${templateId}`),
|
||||
@@ -80,13 +108,14 @@ export const portalApi = {
|
||||
updateTemplateCategory: (templateCategoryId: string, templateCategory: AddTemplateCategory) =>
|
||||
putJson<AddTemplateCategory, void>(`/TemplateCategory/${templateCategoryId}`, templateCategory),
|
||||
deleteTemplateCategory: (templateCategoryId: string) => deleteJson<void>(`/TemplateCategory/${templateCategoryId}`),
|
||||
getVirtualMachines: (signal?: AbortSignal) => getJson<VirtualMachine[]>("/VirtualMachine", signal),
|
||||
getVirtualMachineById: (virtualMachineId: string, signal?: AbortSignal) => getJson<VirtualMachine>(`/VirtualMachine/${virtualMachineId}`, signal),
|
||||
addVirtualMachine: (virtualMachine: AddVirtualMachine) => postJson<AddVirtualMachine, string>("/VirtualMachine", virtualMachine),
|
||||
updateVirtualMachine: (virtualMachineId: string, virtualMachine: AddVirtualMachine) =>
|
||||
putJson<AddVirtualMachine, void>(`/VirtualMachine/${virtualMachineId}`, virtualMachine),
|
||||
deleteVirtualMachine: (virtualMachineId: string) => deleteJson<void>(`/VirtualMachine/${virtualMachineId}`),
|
||||
linkVirtualMachineToDomain: (virtualMachineId: string, domainId: string) =>
|
||||
postJson<undefined, void>(`/VirtualMachine/${virtualMachineId}/Domain/${domainId}`, undefined),
|
||||
unlinkVirtualMachineFromDomain: (virtualMachineId: string) => deleteJson<void>(`/VirtualMachine/${virtualMachineId}/Domain`),
|
||||
getTargets: (signal?: AbortSignal) => getJson<Target[]>("/Target", signal),
|
||||
getTargetById: (targetId: string, signal?: AbortSignal) => getJson<Target>(`/Target/${targetId}`, signal),
|
||||
addTarget: (target: AddTarget) => postJson<AddTarget, string>("/Target", target),
|
||||
updateTarget: (targetId: string, target: AddTarget) =>
|
||||
putJson<AddTarget, void>(`/Target/${targetId}`, target),
|
||||
deleteTarget: (targetId: string) => deleteJson<void>(`/Target/${targetId}`),
|
||||
linkTargetToDomain: (targetId: string, domainId: string) =>
|
||||
postJson<undefined, void>(`/Target/${targetId}/Domain/${domainId}`, undefined),
|
||||
unlinkTargetFromDomain: (targetId: string) => deleteJson<void>(`/Target/${targetId}/Domain`),
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { MessageBar, MessageBarBody, Spinner } from "@fluentui/react-components";
|
||||
import { MessageBar, MessageBarBody, Spinner } from "@fluentui/react-components";
|
||||
|
||||
type DataStateProps = {
|
||||
isLoading: boolean;
|
||||
@@ -21,3 +21,4 @@ export function DataState({ isLoading, error }: DataStateProps) {
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { makeStyles, shorthands, tokens } from "@fluentui/react-components";
|
||||
import { makeStyles, shorthands, tokens } from "@fluentui/react-components";
|
||||
import type { PropsWithChildren } from "react";
|
||||
|
||||
const useStyles = makeStyles({
|
||||
@@ -58,3 +58,4 @@ export function FormActions({ children }: PropsWithChildren) {
|
||||
|
||||
return <div className={styles.actions}>{children}</div>;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { makeStyles, Text, Title1 } from "@fluentui/react-components";
|
||||
import { makeStyles, Text, Title1 } from "@fluentui/react-components";
|
||||
|
||||
const useStyles = makeStyles({
|
||||
root: {
|
||||
@@ -23,3 +23,4 @@ export function PageHeader({ title, description }: PageHeaderProps) {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Outlet, NavLink } from "react-router-dom";
|
||||
import { Outlet, NavLink } from "react-router-dom";
|
||||
import {
|
||||
Button,
|
||||
makeStyles,
|
||||
@@ -71,6 +71,7 @@ const useStyles = makeStyles({
|
||||
const links = [
|
||||
{ to: "/", label: "Dashboard", icon: <Home24Regular /> },
|
||||
{ to: "/deployments", label: "Deployments", icon: <BoxMultiple24Regular /> },
|
||||
{ to: "/deployment-rules", label: "Deployment Rules", icon: <AppsListDetail24Regular /> },
|
||||
{ to: "/worker-jobs", label: "Worker Jobs", icon: <AppsListDetail24Regular /> },
|
||||
{ to: "/domains", label: "Domains", icon: <Globe24Regular /> },
|
||||
{ to: "/environments", label: "Environments", icon: <DatabasePlugConnectedRegular /> },
|
||||
@@ -78,7 +79,7 @@ const links = [
|
||||
{ to: "/templates", label: "Templates", icon: <BoxMultiple24Regular /> },
|
||||
{ to: "/template-categories", label: "Template Categories", icon: <TagMultiple24Regular /> },
|
||||
{ to: "/services", label: "Services", icon: <AppsListDetail24Regular /> },
|
||||
{ to: "/virtual-machines", label: "Virtual Machines", icon: <Desktop24Regular /> },
|
||||
{ to: "/targets", label: "Targets", icon: <Desktop24Regular /> },
|
||||
];
|
||||
|
||||
export function AppShell() {
|
||||
@@ -119,3 +120,4 @@ export function AppShell() {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
13
src/main.tsx
13
src/main.tsx
@@ -1,4 +1,4 @@
|
||||
import React from "react";
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import { FluentProvider, webLightTheme } from "@fluentui/react-components";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
@@ -9,6 +9,7 @@ import { DeploymentBatchDetailsPage } from "./pages/DeploymentBatchDetailsPage";
|
||||
import { DeploymentJobDetailsPage } from "./pages/DeploymentJobDetailsPage";
|
||||
import { DeploymentJobsPage } from "./pages/DeploymentJobsPage";
|
||||
import { DeploymentGroupsPage } from "./pages/DeploymentGroupsPage";
|
||||
import { DeploymentRulesPage } from "./pages/DeploymentRulesPage";
|
||||
import { DomainDetailsPage } from "./pages/DomainDetailsPage";
|
||||
import { DomainsPage } from "./pages/DomainsPage";
|
||||
import { EnvironmentDetailsPage } from "./pages/EnvironmentDetailsPage";
|
||||
@@ -16,8 +17,8 @@ import { EnvironmentsPage } from "./pages/EnvironmentsPage";
|
||||
import { TemplatesPage } from "./pages/TemplatesPage";
|
||||
import { ServicesPage } from "./pages/ServicesPage";
|
||||
import { TemplateCategoriesPage } from "./pages/TemplateCategoriesPage";
|
||||
import { VirtualMachineDetailsPage } from "./pages/VirtualMachineDetailsPage";
|
||||
import { VirtualMachinesPage } from "./pages/VirtualMachinesPage";
|
||||
import { TargetDetailsPage } from "./pages/TargetDetailsPage";
|
||||
import { TargetsPage } from "./pages/TargetsPage";
|
||||
import "./styles/global.css";
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
@@ -36,6 +37,7 @@ const router = createBrowserRouter([
|
||||
children: [
|
||||
{ index: true, element: <DashboardPage /> },
|
||||
{ path: "deployments", element: <DeploymentGroupsPage /> },
|
||||
{ path: "deployment-rules", element: <DeploymentRulesPage /> },
|
||||
{ path: "deployments/:id", element: <DeploymentBatchDetailsPage /> },
|
||||
{ path: "worker-jobs", element: <DeploymentJobsPage /> },
|
||||
{ path: "worker-jobs/:id", element: <DeploymentJobDetailsPage /> },
|
||||
@@ -46,8 +48,8 @@ const router = createBrowserRouter([
|
||||
{ path: "templates", element: <TemplatesPage /> },
|
||||
{ path: "template-categories", element: <TemplateCategoriesPage /> },
|
||||
{ path: "services", element: <ServicesPage /> },
|
||||
{ path: "virtual-machines", element: <VirtualMachinesPage /> },
|
||||
{ path: "virtual-machines/:id", element: <VirtualMachineDetailsPage /> },
|
||||
{ path: "targets", element: <TargetsPage /> },
|
||||
{ path: "targets/:id", element: <TargetDetailsPage /> },
|
||||
],
|
||||
},
|
||||
]);
|
||||
@@ -61,3 +63,4 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||
</FluentProvider>
|
||||
</React.StrictMode>,
|
||||
);
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
Card,
|
||||
CardHeader,
|
||||
@@ -78,9 +78,9 @@ export function DashboardPage() {
|
||||
queryKey: ["queue-jobs"],
|
||||
queryFn: ({ signal }) => portalApi.getDeploymentJobs(signal),
|
||||
});
|
||||
const virtualMachines = useQuery({
|
||||
queryKey: ["virtual-machines"],
|
||||
queryFn: ({ signal }) => portalApi.getVirtualMachines(signal),
|
||||
const targets = useQuery({
|
||||
queryKey: ["targets"],
|
||||
queryFn: ({ signal }) => portalApi.getTargets(signal),
|
||||
});
|
||||
|
||||
const error =
|
||||
@@ -89,14 +89,14 @@ export function DashboardPage() {
|
||||
templates.error ??
|
||||
services.error ??
|
||||
queueJobs.error ??
|
||||
virtualMachines.error;
|
||||
targets.error;
|
||||
const isLoading =
|
||||
domains.isLoading ||
|
||||
environments.isLoading ||
|
||||
templates.isLoading ||
|
||||
services.isLoading ||
|
||||
queueJobs.isLoading ||
|
||||
virtualMachines.isLoading;
|
||||
targets.isLoading;
|
||||
|
||||
const queueByStatus = (queueJobs.data ?? []).reduce<Record<string, number>>((acc, job) => {
|
||||
const key = job.status || "Unknown";
|
||||
@@ -104,7 +104,7 @@ export function DashboardPage() {
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
const vmCompliance = (virtualMachines.data ?? []).reduce(
|
||||
const vmCompliance = (targets.data ?? []).reduce(
|
||||
(acc, vm) => {
|
||||
const compliance = getVmCompliance(vm.metadataJson);
|
||||
if (compliance === true) acc.compliant += 1;
|
||||
@@ -222,3 +222,4 @@ function getVmCompliance(metadataJson?: string): boolean | undefined {
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
Combobox,
|
||||
Field,
|
||||
Input,
|
||||
makeStyles,
|
||||
Option,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
@@ -10,109 +14,458 @@ import {
|
||||
TableHeaderCell,
|
||||
TableRow,
|
||||
Textarea,
|
||||
Tooltip,
|
||||
} from "@fluentui/react-components";
|
||||
import { ArrowLeftRegular } from "@fluentui/react-icons";
|
||||
import { AddRegular, ArrowLeftRegular, DeleteRegular } from "@fluentui/react-icons";
|
||||
import { useMemo, useState } from "react";
|
||||
import { Link, useParams } from "react-router-dom";
|
||||
import { portalApi } from "../api/portalApi";
|
||||
import { DataState } from "../components/DataState";
|
||||
import { FormActions, FormGrid, FormSection, FormWide } from "../components/FormSection";
|
||||
import { PageHeader } from "../components/PageHeader";
|
||||
|
||||
const useStyles = makeStyles({
|
||||
layout: {
|
||||
display: "grid",
|
||||
gap: "24px",
|
||||
},
|
||||
section: {
|
||||
display: "grid",
|
||||
gap: "12px",
|
||||
},
|
||||
sectionHeader: {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
gap: "12px",
|
||||
},
|
||||
sectionTitle: {
|
||||
fontSize: "18px",
|
||||
fontWeight: 600,
|
||||
},
|
||||
formGrid: {
|
||||
display: "grid",
|
||||
gridTemplateColumns: "repeat(4, minmax(160px, 1fr))",
|
||||
gap: "12px",
|
||||
},
|
||||
wide: {
|
||||
gridColumn: "span 2",
|
||||
},
|
||||
actions: {
|
||||
display: "flex",
|
||||
gap: "6px",
|
||||
},
|
||||
monospace: {
|
||||
fontFamily: "Consolas, monospace",
|
||||
fontSize: "12px",
|
||||
wordBreak: "break-word",
|
||||
},
|
||||
});
|
||||
|
||||
export function DeploymentBatchDetailsPage() {
|
||||
const styles = useStyles();
|
||||
const queryClient = useQueryClient();
|
||||
const { id } = useParams();
|
||||
const [selectedVirtualMachineIds, setSelectedVirtualMachineIds] = useState<string[]>([]);
|
||||
const [jsonData, setJsonData] = useState("{}");
|
||||
|
||||
const [templateId, setTemplateId] = useState("");
|
||||
const [templateVersionId, setTemplateVersionId] = useState("");
|
||||
const [templateRole, setTemplateRole] = useState("Service");
|
||||
const [templateAlias, setTemplateAlias] = useState("");
|
||||
|
||||
const [parameterSelectionId, setParameterSelectionId] = useState("");
|
||||
const [parameterName, setParameterName] = useState("");
|
||||
const [parameterValueJson, setParameterValueJson] = useState('{"value":""}');
|
||||
const [parameterIsSecret, setParameterIsSecret] = useState(false);
|
||||
|
||||
const [targetTargetId, setTargetTargetId] = useState("");
|
||||
const [targetRoleKey, setTargetRoleKey] = useState("Node");
|
||||
const [targetNodeDataJson, setTargetNodeDataJson] = useState("{}");
|
||||
|
||||
const { data: deploymentBatch, error, isLoading } = useQuery({
|
||||
queryKey: ["deployment-batches", id],
|
||||
queryFn: ({ signal }) => portalApi.getDeploymentBatchById(id!, signal),
|
||||
enabled: Boolean(id),
|
||||
});
|
||||
const { data: virtualMachines } = useQuery({
|
||||
queryKey: ["virtual-machines"],
|
||||
queryFn: ({ signal }) => portalApi.getVirtualMachines(signal),
|
||||
const { data: allDeployments } = useQuery({
|
||||
queryKey: ["deployments"],
|
||||
queryFn: ({ signal }) => portalApi.getDeployments(signal),
|
||||
});
|
||||
const { data: templates } = useQuery({
|
||||
queryKey: ["templates"],
|
||||
queryFn: ({ signal }) => portalApi.getTemplates(signal),
|
||||
});
|
||||
const { data: templateVersions } = useQuery({
|
||||
queryKey: ["template-versions", templateId],
|
||||
queryFn: ({ signal }) => portalApi.getTemplateVersions(templateId, signal),
|
||||
enabled: Boolean(templateId),
|
||||
});
|
||||
const { data: targets } = useQuery({
|
||||
queryKey: ["targets"],
|
||||
queryFn: ({ signal }) => portalApi.getTargets(signal),
|
||||
});
|
||||
|
||||
const addDeploymentRequest = useMutation({
|
||||
mutationFn: portalApi.addDeploymentRequest,
|
||||
const invalidateBatch = async () => {
|
||||
await queryClient.invalidateQueries({ queryKey: ["deployment-batches", id] });
|
||||
await queryClient.invalidateQueries({ queryKey: ["deployment-batches"] });
|
||||
};
|
||||
|
||||
const addTemplateSelection = useMutation({
|
||||
mutationFn: () =>
|
||||
portalApi.addDeploymentTemplateSelection(id!, {
|
||||
templateVersionId,
|
||||
templateRole,
|
||||
alias: templateAlias || undefined,
|
||||
}),
|
||||
onSuccess: async () => {
|
||||
setSelectedVirtualMachineIds([]);
|
||||
setJsonData("{}");
|
||||
await queryClient.invalidateQueries({ queryKey: ["deployments"] });
|
||||
await queryClient.invalidateQueries({ queryKey: ["deployment-batches", id] });
|
||||
await queryClient.invalidateQueries({ queryKey: ["deployment-jobs"] });
|
||||
setTemplateId("");
|
||||
setTemplateVersionId("");
|
||||
setTemplateRole("Service");
|
||||
setTemplateAlias("");
|
||||
await invalidateBatch();
|
||||
},
|
||||
});
|
||||
const filteredExecutions = useMemo(() => deploymentBatch?.deployments ?? [], [deploymentBatch?.deployments]);
|
||||
const deleteTemplateSelection = useMutation({
|
||||
mutationFn: (selectionId: string) => portalApi.deleteDeploymentTemplateSelection(id!, selectionId),
|
||||
onSuccess: invalidateBatch,
|
||||
});
|
||||
|
||||
const addParameterValue = useMutation({
|
||||
mutationFn: () =>
|
||||
portalApi.addDeploymentParameterValue(id!, {
|
||||
deploymentTemplateSelectionId: parameterSelectionId || undefined,
|
||||
name: parameterName,
|
||||
valueJson: parameterValueJson,
|
||||
isSecretReference: parameterIsSecret,
|
||||
isOverride: true,
|
||||
}),
|
||||
onSuccess: async () => {
|
||||
setParameterSelectionId("");
|
||||
setParameterName("");
|
||||
setParameterValueJson('{"value":""}');
|
||||
setParameterIsSecret(false);
|
||||
await invalidateBatch();
|
||||
},
|
||||
});
|
||||
const deleteParameterValue = useMutation({
|
||||
mutationFn: (parameterValueId: string) => portalApi.deleteDeploymentParameterValue(id!, parameterValueId),
|
||||
onSuccess: invalidateBatch,
|
||||
});
|
||||
|
||||
const addTargetAssignment = useMutation({
|
||||
mutationFn: () =>
|
||||
portalApi.addDeploymentTargetAssignment(id!, {
|
||||
targetId: targetTargetId,
|
||||
roleKey: targetRoleKey,
|
||||
nodeDataJson: targetNodeDataJson,
|
||||
}),
|
||||
onSuccess: async () => {
|
||||
setTargetTargetId("");
|
||||
setTargetRoleKey("Node");
|
||||
setTargetNodeDataJson("{}");
|
||||
await invalidateBatch();
|
||||
},
|
||||
});
|
||||
const deleteTargetAssignment = useMutation({
|
||||
mutationFn: (targetAssignmentId: string) => portalApi.deleteDeploymentTargetAssignment(id!, targetAssignmentId),
|
||||
onSuccess: invalidateBatch,
|
||||
});
|
||||
|
||||
const filteredExecutions = useMemo(() => {
|
||||
if (deploymentBatch?.deployments && deploymentBatch.deployments.length > 0) {
|
||||
return deploymentBatch.deployments;
|
||||
}
|
||||
|
||||
return (allDeployments ?? []).filter(
|
||||
(execution) => execution.deploymentGroupId === id || execution.deploymentBatchId === id,
|
||||
);
|
||||
}, [allDeployments, deploymentBatch?.deployments, id]);
|
||||
|
||||
const templateSelectionError = addTemplateSelection.error;
|
||||
const parameterError = addParameterValue.error;
|
||||
const targetError = addTargetAssignment.error;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.layout}>
|
||||
<Link to="/deployments">
|
||||
<Button appearance="subtle" icon={<ArrowLeftRegular />}>
|
||||
Deployments
|
||||
</Button>
|
||||
</Link>
|
||||
<PageHeader title={id ? `Deployment Batch ${id}` : "Deployment Batch"} description="Executions und Start neuer Deployments fuer diesen Batch." />
|
||||
<FormSection
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault();
|
||||
if (!id) return;
|
||||
addDeploymentRequest.mutate({ deploymentBatchId: id, jsonData, virtualMachineIds: selectedVirtualMachineIds });
|
||||
}}
|
||||
>
|
||||
<FormGrid>
|
||||
<Field label="Targets (Virtual Machines)" required>
|
||||
<div>
|
||||
{(virtualMachines ?? []).map((virtualMachine) => (
|
||||
<Checkbox
|
||||
key={virtualMachine.id}
|
||||
label={virtualMachine.name}
|
||||
checked={selectedVirtualMachineIds.includes(virtualMachine.id)}
|
||||
onChange={(_, data) => {
|
||||
if (data.checked) {
|
||||
setSelectedVirtualMachineIds((previous) => [...previous, virtualMachine.id]);
|
||||
} else {
|
||||
setSelectedVirtualMachineIds((previous) => previous.filter((vmId) => vmId !== virtualMachine.id));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</Field>
|
||||
<FormWide>
|
||||
<Field label="JSON data" required validationMessage={addDeploymentRequest.error?.message}>
|
||||
<Textarea value={jsonData} onChange={(_, data) => setJsonData(data.value)} />
|
||||
</Field>
|
||||
</FormWide>
|
||||
</FormGrid>
|
||||
<FormActions>
|
||||
<Button appearance="primary" disabled={!id || selectedVirtualMachineIds.length === 0 || addDeploymentRequest.isPending} type="submit">
|
||||
Start deployment
|
||||
</Button>
|
||||
</FormActions>
|
||||
</FormSection>
|
||||
<PageHeader title={id ? `Deployment Batch ${id}` : "Deployment Batch"} description="Composition, Targets und Executions fuer diesen Batch." />
|
||||
<DataState
|
||||
isLoading={isLoading}
|
||||
error={error ?? templateSelectionError ?? parameterError ?? targetError}
|
||||
/>
|
||||
|
||||
<DataState isLoading={isLoading} error={error} />
|
||||
<Table aria-label="Deployment executions">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHeaderCell>Status</TableHeaderCell>
|
||||
<TableHeaderCell>Virtual Machine</TableHeaderCell>
|
||||
<TableHeaderCell>Execution Id</TableHeaderCell>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filteredExecutions.map((execution) => (
|
||||
<TableRow key={execution.id}>
|
||||
<TableCell>{execution.status ?? "Unknown"}</TableCell>
|
||||
<TableCell>{execution.virtualMachineId ?? "-"}</TableCell>
|
||||
<TableCell>{execution.id}</TableCell>
|
||||
<section className={styles.section}>
|
||||
<div className={styles.sectionHeader}>
|
||||
<div className={styles.sectionTitle}>Template Selections</div>
|
||||
<Button
|
||||
appearance="primary"
|
||||
icon={<AddRegular />}
|
||||
disabled={!id || !templateVersionId || addTemplateSelection.isPending}
|
||||
onClick={() => addTemplateSelection.mutate()}
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
</div>
|
||||
<div className={styles.formGrid}>
|
||||
<Field label="Template">
|
||||
<Combobox
|
||||
placeholder="Template"
|
||||
value={(templates ?? []).find((template) => template.id === templateId)?.name ?? ""}
|
||||
onOptionSelect={(_, data) => {
|
||||
setTemplateId(data.optionValue ?? "");
|
||||
setTemplateVersionId("");
|
||||
}}
|
||||
>
|
||||
{(templates ?? []).map((template) => (
|
||||
<Option key={template.id} text={template.name} value={template.id}>
|
||||
{template.name}
|
||||
</Option>
|
||||
))}
|
||||
</Combobox>
|
||||
</Field>
|
||||
<Field label="Version">
|
||||
<Combobox
|
||||
disabled={!templateId}
|
||||
placeholder="Version"
|
||||
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="Role">
|
||||
<Input value={templateRole} onChange={(_, data) => setTemplateRole(data.value)} />
|
||||
</Field>
|
||||
<Field label="Alias">
|
||||
<Input value={templateAlias} onChange={(_, data) => setTemplateAlias(data.value)} />
|
||||
</Field>
|
||||
</div>
|
||||
<Table aria-label="Template selections">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHeaderCell>Order</TableHeaderCell>
|
||||
<TableHeaderCell>Role</TableHeaderCell>
|
||||
<TableHeaderCell>Alias</TableHeaderCell>
|
||||
<TableHeaderCell>Version</TableHeaderCell>
|
||||
<TableHeaderCell>Schema</TableHeaderCell>
|
||||
<TableHeaderCell>Hash</TableHeaderCell>
|
||||
<TableHeaderCell>Actions</TableHeaderCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{(deploymentBatch?.templateSelections ?? []).map((selection) => (
|
||||
<TableRow key={selection.id}>
|
||||
<TableCell>{selection.sortOrder}</TableCell>
|
||||
<TableCell>{selection.templateRole}</TableCell>
|
||||
<TableCell>{selection.alias ?? "-"}</TableCell>
|
||||
<TableCell>{selection.templateVersion?.version ?? selection.templateVersionId}</TableCell>
|
||||
<TableCell>{selection.templateVersion?.schemaVersion ?? "-"}</TableCell>
|
||||
<TableCell className={styles.monospace}>{selection.templateVersion?.jsonHash?.slice(0, 12) ?? "-"}</TableCell>
|
||||
<TableCell>
|
||||
<Tooltip content="Delete selection" relationship="label">
|
||||
<Button
|
||||
appearance="subtle"
|
||||
aria-label="Delete selection"
|
||||
icon={<DeleteRegular />}
|
||||
disabled={deleteTemplateSelection.isPending}
|
||||
onClick={() => deleteTemplateSelection.mutate(selection.id)}
|
||||
/>
|
||||
</Tooltip>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</section>
|
||||
|
||||
<section className={styles.section}>
|
||||
<div className={styles.sectionHeader}>
|
||||
<div className={styles.sectionTitle}>Parameter Values</div>
|
||||
<Button
|
||||
appearance="primary"
|
||||
icon={<AddRegular />}
|
||||
disabled={!id || !parameterName || addParameterValue.isPending}
|
||||
onClick={() => addParameterValue.mutate()}
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
</div>
|
||||
<div className={styles.formGrid}>
|
||||
<Field label="Scope">
|
||||
<Combobox
|
||||
placeholder="Global"
|
||||
value={
|
||||
(deploymentBatch?.templateSelections ?? []).find((selection) => selection.id === parameterSelectionId)?.alias ??
|
||||
(parameterSelectionId ? parameterSelectionId : "Global")
|
||||
}
|
||||
onOptionSelect={(_, data) => setParameterSelectionId(data.optionValue ?? "")}
|
||||
>
|
||||
<Option text="Global" value="">
|
||||
Global
|
||||
</Option>
|
||||
{(deploymentBatch?.templateSelections ?? []).map((selection) => (
|
||||
<Option key={selection.id} text={selection.alias ?? selection.templateRole} value={selection.id}>
|
||||
{selection.alias ?? selection.templateRole}
|
||||
</Option>
|
||||
))}
|
||||
</Combobox>
|
||||
</Field>
|
||||
<Field label="Name">
|
||||
<Input value={parameterName} onChange={(_, data) => setParameterName(data.value)} />
|
||||
</Field>
|
||||
<Field label="Secret">
|
||||
<Checkbox checked={parameterIsSecret} onChange={(_, data) => setParameterIsSecret(Boolean(data.checked))} />
|
||||
</Field>
|
||||
<Field className={styles.wide} label="Value JSON">
|
||||
<Textarea value={parameterValueJson} onChange={(_, data) => setParameterValueJson(data.value)} resize="vertical" />
|
||||
</Field>
|
||||
</div>
|
||||
<Table aria-label="Parameter values">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHeaderCell>Scope</TableHeaderCell>
|
||||
<TableHeaderCell>Name</TableHeaderCell>
|
||||
<TableHeaderCell>Secret</TableHeaderCell>
|
||||
<TableHeaderCell>Value JSON</TableHeaderCell>
|
||||
<TableHeaderCell>Actions</TableHeaderCell>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{(deploymentBatch?.parameterValues ?? []).map((parameterValue) => {
|
||||
const selection = (deploymentBatch?.templateSelections ?? []).find(
|
||||
(entry) => entry.id === parameterValue.deploymentTemplateSelectionId,
|
||||
);
|
||||
|
||||
return (
|
||||
<TableRow key={parameterValue.id}>
|
||||
<TableCell>{selection?.alias ?? selection?.templateRole ?? "Global"}</TableCell>
|
||||
<TableCell>{parameterValue.name}</TableCell>
|
||||
<TableCell>{parameterValue.isSecretReference ? "Yes" : "No"}</TableCell>
|
||||
<TableCell className={styles.monospace}>{parameterValue.valueJson}</TableCell>
|
||||
<TableCell>
|
||||
<Tooltip content="Delete parameter" relationship="label">
|
||||
<Button
|
||||
appearance="subtle"
|
||||
aria-label="Delete parameter"
|
||||
icon={<DeleteRegular />}
|
||||
disabled={deleteParameterValue.isPending}
|
||||
onClick={() => deleteParameterValue.mutate(parameterValue.id)}
|
||||
/>
|
||||
</Tooltip>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</section>
|
||||
|
||||
<section className={styles.section}>
|
||||
<div className={styles.sectionHeader}>
|
||||
<div className={styles.sectionTitle}>Target Assignments</div>
|
||||
<Button
|
||||
appearance="primary"
|
||||
icon={<AddRegular />}
|
||||
disabled={!id || !targetTargetId || !targetRoleKey || addTargetAssignment.isPending}
|
||||
onClick={() => addTargetAssignment.mutate()}
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
</div>
|
||||
<div className={styles.formGrid}>
|
||||
<Field label="Target">
|
||||
<Combobox
|
||||
placeholder="Target"
|
||||
value={(targets ?? []).find((target) => target.id === targetTargetId)?.name ?? ""}
|
||||
onOptionSelect={(_, data) => setTargetTargetId(data.optionValue ?? "")}
|
||||
>
|
||||
{(targets ?? []).map((target) => (
|
||||
<Option key={target.id} text={target.name} value={target.id}>
|
||||
{target.name}
|
||||
</Option>
|
||||
))}
|
||||
</Combobox>
|
||||
</Field>
|
||||
<Field label="Role">
|
||||
<Input value={targetRoleKey} onChange={(_, data) => setTargetRoleKey(data.value)} />
|
||||
</Field>
|
||||
<Field className={styles.wide} label="Node Data JSON">
|
||||
<Textarea value={targetNodeDataJson} onChange={(_, data) => setTargetNodeDataJson(data.value)} resize="vertical" />
|
||||
</Field>
|
||||
</div>
|
||||
<Table aria-label="Target assignments">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHeaderCell>Order</TableHeaderCell>
|
||||
<TableHeaderCell>Target</TableHeaderCell>
|
||||
<TableHeaderCell>Role</TableHeaderCell>
|
||||
<TableHeaderCell>Node Data JSON</TableHeaderCell>
|
||||
<TableHeaderCell>Actions</TableHeaderCell>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{(deploymentBatch?.targetAssignments ?? []).map((targetAssignment) => (
|
||||
<TableRow key={targetAssignment.id}>
|
||||
<TableCell>{targetAssignment.sortOrder}</TableCell>
|
||||
<TableCell>{targetAssignment.target?.name ?? targetAssignment.targetId}</TableCell>
|
||||
<TableCell>{targetAssignment.roleKey}</TableCell>
|
||||
<TableCell className={styles.monospace}>{targetAssignment.nodeDataJson}</TableCell>
|
||||
<TableCell>
|
||||
<Tooltip content="Delete target" relationship="label">
|
||||
<Button
|
||||
appearance="subtle"
|
||||
aria-label="Delete target"
|
||||
icon={<DeleteRegular />}
|
||||
disabled={deleteTargetAssignment.isPending}
|
||||
onClick={() => deleteTargetAssignment.mutate(targetAssignment.id)}
|
||||
/>
|
||||
</Tooltip>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</section>
|
||||
|
||||
<section className={styles.section}>
|
||||
<div className={styles.sectionTitle}>Executions</div>
|
||||
<Table aria-label="Deployment executions">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHeaderCell>Status</TableHeaderCell>
|
||||
<TableHeaderCell>VM Name</TableHeaderCell>
|
||||
<TableHeaderCell>VM Id</TableHeaderCell>
|
||||
<TableHeaderCell>Domain Id</TableHeaderCell>
|
||||
<TableHeaderCell>JSON Data</TableHeaderCell>
|
||||
<TableHeaderCell>Created</TableHeaderCell>
|
||||
<TableHeaderCell>Modified</TableHeaderCell>
|
||||
<TableHeaderCell>Execution Id</TableHeaderCell>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filteredExecutions.map((execution) => (
|
||||
<TableRow key={execution.id}>
|
||||
<TableCell>{execution.status ?? "Unknown"}</TableCell>
|
||||
<TableCell>{execution.target?.name ?? "-"}</TableCell>
|
||||
<TableCell>{execution.targetId ?? "-"}</TableCell>
|
||||
<TableCell>{execution.target?.domainID ?? "-"}</TableCell>
|
||||
<TableCell className={styles.monospace}>{execution.jsonData ? execution.jsonData.slice(0, 80) : "-"}</TableCell>
|
||||
<TableCell>{execution.created ? new Date(execution.created).toLocaleString() : "-"}</TableCell>
|
||||
<TableCell>{execution.modified ? new Date(execution.modified).toLocaleString() : "-"}</TableCell>
|
||||
<TableCell>{execution.id}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
Combobox,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogBody,
|
||||
DialogContent,
|
||||
DialogSurface,
|
||||
DialogTitle,
|
||||
Field,
|
||||
Input,
|
||||
makeStyles,
|
||||
Option,
|
||||
Table,
|
||||
@@ -14,12 +21,11 @@ import {
|
||||
TableRow,
|
||||
Tooltip,
|
||||
} from "@fluentui/react-components";
|
||||
import { OpenRegular } from "@fluentui/react-icons";
|
||||
import { useState } from "react";
|
||||
import { AddRegular, DeleteRegular, OpenRegular } from "@fluentui/react-icons";
|
||||
import { useMemo, useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { portalApi } from "../api/portalApi";
|
||||
import { DataState } from "../components/DataState";
|
||||
import { FormActions, FormGrid, FormSection } from "../components/FormSection";
|
||||
import { PageHeader } from "../components/PageHeader";
|
||||
|
||||
const useStyles = makeStyles({
|
||||
@@ -34,8 +40,10 @@ export function DeploymentGroupsPage() {
|
||||
const queryClient = useQueryClient();
|
||||
const [serviceId, setServiceId] = useState("");
|
||||
const [templateId, setTemplateId] = useState("");
|
||||
const [virtualMachineIds, setVirtualMachineIds] = useState<string[]>([]);
|
||||
const [status, setStatus] = useState("New");
|
||||
const [deploymentRuleId, setDeploymentRuleId] = useState("");
|
||||
const [targetIds, setTargetIds] = useState<string[]>([]);
|
||||
const [targetSearch, setTargetSearch] = useState("");
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const { data, error, isLoading } = useQuery({
|
||||
queryKey: ["deployment-batches"],
|
||||
queryFn: ({ signal }) => portalApi.getDeploymentBatches(signal),
|
||||
@@ -52,17 +60,29 @@ export function DeploymentGroupsPage() {
|
||||
queryKey: ["template-categories"],
|
||||
queryFn: ({ signal }) => portalApi.getTemplateCategories(signal),
|
||||
});
|
||||
const { data: virtualMachines } = useQuery({
|
||||
queryKey: ["virtual-machines"],
|
||||
queryFn: ({ signal }) => portalApi.getVirtualMachines(signal),
|
||||
const { data: targets } = useQuery({
|
||||
queryKey: ["targets"],
|
||||
queryFn: ({ signal }) => portalApi.getTargets(signal),
|
||||
});
|
||||
const { data: deploymentRules } = useQuery({
|
||||
queryKey: ["deployment-rules"],
|
||||
queryFn: ({ signal }) => portalApi.getDeploymentRules(signal),
|
||||
});
|
||||
const addDeploymentBatch = useMutation({
|
||||
mutationFn: portalApi.addDeploymentBatch,
|
||||
onSuccess: async () => {
|
||||
setServiceId("");
|
||||
setTemplateId("");
|
||||
setVirtualMachineIds([]);
|
||||
setStatus("New");
|
||||
setDeploymentRuleId("");
|
||||
setTargetIds([]);
|
||||
setDialogOpen(false);
|
||||
await queryClient.invalidateQueries({ queryKey: ["deployment-batches"] });
|
||||
await queryClient.invalidateQueries({ queryKey: ["deployments"] });
|
||||
},
|
||||
});
|
||||
const deleteDeploymentBatch = useMutation({
|
||||
mutationFn: portalApi.deleteDeploymentBatch,
|
||||
onSuccess: async () => {
|
||||
await queryClient.invalidateQueries({ queryKey: ["deployment-batches"] });
|
||||
await queryClient.invalidateQueries({ queryKey: ["deployments"] });
|
||||
},
|
||||
@@ -70,104 +90,39 @@ export function DeploymentGroupsPage() {
|
||||
|
||||
const selectedService = (services ?? []).find((service) => service.id === serviceId);
|
||||
const isOnPremService = selectedService ? !selectedService.isCloudService : false;
|
||||
const filteredTargets = useMemo(() => {
|
||||
const search = targetSearch.trim().toLowerCase();
|
||||
if (!search) {
|
||||
return targets ?? [];
|
||||
}
|
||||
|
||||
return (targets ?? []).filter((target) => {
|
||||
const name = target.name?.toLowerCase() ?? "";
|
||||
const id = target.id?.toLowerCase() ?? "";
|
||||
return name.includes(search) || id.includes(search);
|
||||
});
|
||||
}, [targetSearch, targets]);
|
||||
const allVisibleSelected =
|
||||
filteredTargets.length > 0 &&
|
||||
filteredTargets.every((target) => targetIds.includes(target.id));
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHeader title="Deployments" description="Deployment Batches und deren Ausfuehrungen." />
|
||||
<FormSection
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault();
|
||||
addDeploymentBatch.mutate({
|
||||
status,
|
||||
templateId,
|
||||
virtualMachineIds: isOnPremService ? virtualMachineIds : undefined,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<FormGrid>
|
||||
<Field label="Service" required>
|
||||
<Combobox
|
||||
placeholder="Service waehlen"
|
||||
value={services?.find((service) => service.id === serviceId)?.name ?? ""}
|
||||
onOptionSelect={(_, data) => {
|
||||
const nextServiceId = data.optionValue ?? "";
|
||||
setServiceId(nextServiceId);
|
||||
setTemplateId("");
|
||||
setVirtualMachineIds([]);
|
||||
}}
|
||||
>
|
||||
{(services ?? []).map((service) => (
|
||||
<Option key={service.id} text={service.name} value={service.id}>
|
||||
{service.name}
|
||||
</Option>
|
||||
))}
|
||||
</Combobox>
|
||||
</Field>
|
||||
<Field label="Template" required>
|
||||
<Combobox
|
||||
placeholder={serviceId ? "Template waehlen" : "Erst Service waehlen"}
|
||||
disabled={!serviceId}
|
||||
value={templates?.find((template) => template.id === templateId)?.name ?? ""}
|
||||
onOptionSelect={(_, data) => setTemplateId(data.optionValue ?? "")}
|
||||
>
|
||||
{(templates ?? [])
|
||||
.filter((template) => {
|
||||
const category = (templateCategories ?? []).find((entry) => entry.id === template.templateCategoryId);
|
||||
return category?.serviceId === serviceId;
|
||||
})
|
||||
.map((template) => (
|
||||
<Option key={template.id} text={template.name} value={template.id}>
|
||||
{template.name}
|
||||
</Option>
|
||||
))}
|
||||
</Combobox>
|
||||
</Field>
|
||||
<Field label="Status" required validationMessage={addDeploymentBatch.error?.message}>
|
||||
<Combobox value={status} onOptionSelect={(_, data) => setStatus(data.optionValue ?? "New")}>
|
||||
<Option value="New">New</Option>
|
||||
<Option value="Pending">Pending</Option>
|
||||
<Option value="Running">Running</Option>
|
||||
<Option value="Succeeded">Succeeded</Option>
|
||||
<Option value="Failed">Failed</Option>
|
||||
</Combobox>
|
||||
</Field>
|
||||
{isOnPremService && (
|
||||
<Field label="Virtual Machines" required>
|
||||
<div>
|
||||
{(virtualMachines ?? []).map((virtualMachine) => (
|
||||
<Checkbox
|
||||
key={virtualMachine.id}
|
||||
label={virtualMachine.name}
|
||||
checked={virtualMachineIds.includes(virtualMachine.id)}
|
||||
onChange={(_, data) => {
|
||||
if (data.checked) {
|
||||
setVirtualMachineIds((prev) => [...prev, virtualMachine.id]);
|
||||
} else {
|
||||
setVirtualMachineIds((prev) => prev.filter((id) => id !== virtualMachine.id));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</Field>
|
||||
)}
|
||||
</FormGrid>
|
||||
<FormActions>
|
||||
<Button
|
||||
appearance="primary"
|
||||
disabled={!templateId || !status || (isOnPremService && virtualMachineIds.length === 0) || addDeploymentBatch.isPending}
|
||||
type="submit"
|
||||
>
|
||||
Add deployment batch
|
||||
</Button>
|
||||
</FormActions>
|
||||
</FormSection>
|
||||
<Button appearance="primary" icon={<AddRegular />} onClick={() => setDialogOpen(true)}>
|
||||
Neues Deployment
|
||||
</Button>
|
||||
<DataState isLoading={isLoading} error={error} />
|
||||
{data && (
|
||||
<Table aria-label="Deployment batches">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHeaderCell>Service</TableHeaderCell>
|
||||
<TableHeaderCell>Template</TableHeaderCell>
|
||||
<TableHeaderCell>Rule</TableHeaderCell>
|
||||
<TableHeaderCell>Status</TableHeaderCell>
|
||||
<TableHeaderCell>Created</TableHeaderCell>
|
||||
<TableHeaderCell>Modified</TableHeaderCell>
|
||||
<TableHeaderCell>Id</TableHeaderCell>
|
||||
<TableHeaderCell>Actions</TableHeaderCell>
|
||||
</TableRow>
|
||||
@@ -175,7 +130,21 @@ export function DeploymentGroupsPage() {
|
||||
<TableBody>
|
||||
{data.map((deploymentBatch) => (
|
||||
<TableRow key={deploymentBatch.id}>
|
||||
<TableCell>
|
||||
{(() => {
|
||||
const template = (templates ?? []).find((entry) => entry.id === deploymentBatch.templateId);
|
||||
const category = (templateCategories ?? []).find((entry) => entry.id === template?.templateCategoryId);
|
||||
const service = (services ?? []).find((entry) => entry.id === category?.serviceId);
|
||||
return service?.name ?? "-";
|
||||
})()}
|
||||
</TableCell>
|
||||
<TableCell>{(templates ?? []).find((entry) => entry.id === deploymentBatch.templateId)?.name ?? "-"}</TableCell>
|
||||
<TableCell>
|
||||
{(deploymentRules ?? []).find((entry) => entry.id === deploymentBatch.deploymentRuleId)?.name ?? "-"}
|
||||
</TableCell>
|
||||
<TableCell>{deploymentBatch.status ?? "Unknown"}</TableCell>
|
||||
<TableCell>{deploymentBatch.created ? new Date(deploymentBatch.created).toLocaleString() : "-"}</TableCell>
|
||||
<TableCell>{deploymentBatch.modified ? new Date(deploymentBatch.modified).toLocaleString() : "-"}</TableCell>
|
||||
<TableCell>{deploymentBatch.id}</TableCell>
|
||||
<TableCell>
|
||||
<div className={styles.actions}>
|
||||
@@ -184,6 +153,14 @@ export function DeploymentGroupsPage() {
|
||||
<Button appearance="subtle" aria-label="Details" icon={<OpenRegular />} />
|
||||
</Link>
|
||||
</Tooltip>
|
||||
<Tooltip content="Delete" relationship="label">
|
||||
<Button
|
||||
appearance="subtle"
|
||||
aria-label="Delete"
|
||||
icon={<DeleteRegular />}
|
||||
onClick={() => deleteDeploymentBatch.mutate(deploymentBatch.id)}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
@@ -191,6 +168,140 @@ export function DeploymentGroupsPage() {
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
<Dialog open={dialogOpen} onOpenChange={(_, data) => setDialogOpen(data.open)}>
|
||||
<DialogSurface>
|
||||
<DialogBody>
|
||||
<DialogTitle>Neues Deployment</DialogTitle>
|
||||
<DialogContent>
|
||||
<Field label="Service" required>
|
||||
<Combobox
|
||||
placeholder="Service waehlen"
|
||||
value={services?.find((service) => service.id === serviceId)?.name ?? ""}
|
||||
onOptionSelect={(_, data) => {
|
||||
const nextServiceId = data.optionValue ?? "";
|
||||
setServiceId(nextServiceId);
|
||||
setTemplateId("");
|
||||
setTargetIds([]);
|
||||
}}
|
||||
>
|
||||
{(services ?? []).map((service) => (
|
||||
<Option key={service.id} text={service.name} value={service.id}>
|
||||
{service.name}
|
||||
</Option>
|
||||
))}
|
||||
</Combobox>
|
||||
</Field>
|
||||
<Field label="Template" required>
|
||||
<Combobox
|
||||
placeholder={serviceId ? "Template waehlen" : "Erst Service waehlen"}
|
||||
disabled={!serviceId}
|
||||
value={templates?.find((template) => template.id === templateId)?.name ?? ""}
|
||||
onOptionSelect={(_, data) => setTemplateId(data.optionValue ?? "")}
|
||||
>
|
||||
{(templates ?? [])
|
||||
.filter((template) => {
|
||||
const category = (templateCategories ?? []).find((entry) => entry.id === template.templateCategoryId);
|
||||
return category?.serviceId === serviceId;
|
||||
})
|
||||
.map((template) => (
|
||||
<Option key={template.id} text={template.name} value={template.id}>
|
||||
{template.name}
|
||||
</Option>
|
||||
))}
|
||||
</Combobox>
|
||||
</Field>
|
||||
<Field label="Deployment Rule">
|
||||
<Combobox
|
||||
placeholder="Rule waehlen (optional)"
|
||||
value={deploymentRules?.find((rule) => rule.id === deploymentRuleId)?.name ?? ""}
|
||||
onOptionSelect={(_, data) => setDeploymentRuleId(data.optionValue ?? "")}
|
||||
>
|
||||
{(deploymentRules ?? []).map((rule) => (
|
||||
<Option key={rule.id} text={rule.name} value={rule.id}>
|
||||
{rule.name}
|
||||
</Option>
|
||||
))}
|
||||
</Combobox>
|
||||
</Field>
|
||||
{isOnPremService && (
|
||||
<Field label="Targets" required>
|
||||
<div>
|
||||
<Input
|
||||
placeholder="VM suchen (Name oder Id)"
|
||||
value={targetSearch}
|
||||
onChange={(_, data) => setTargetSearch(data.value)}
|
||||
/>
|
||||
<Table aria-label="Targets selection">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHeaderCell>
|
||||
<Checkbox
|
||||
checked={allVisibleSelected}
|
||||
onChange={(_, data) => {
|
||||
if (data.checked) {
|
||||
setTargetIds((prev) =>
|
||||
Array.from(new Set([...prev, ...filteredTargets.map((vm) => vm.id)])),
|
||||
);
|
||||
} else {
|
||||
const visibleIds = new Set(filteredTargets.map((vm) => vm.id));
|
||||
setTargetIds((prev) => prev.filter((id) => !visibleIds.has(id)));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</TableHeaderCell>
|
||||
<TableHeaderCell>Name</TableHeaderCell>
|
||||
<TableHeaderCell>Id</TableHeaderCell>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filteredTargets.map((target) => (
|
||||
<TableRow key={target.id}>
|
||||
<TableCell>
|
||||
<Checkbox
|
||||
checked={targetIds.includes(target.id)}
|
||||
onChange={(_, data) => {
|
||||
if (data.checked) {
|
||||
setTargetIds((prev) => [...prev, target.id]);
|
||||
} else {
|
||||
setTargetIds((prev) => prev.filter((entry) => entry !== target.id));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>{target.name}</TableCell>
|
||||
<TableCell>{target.id}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</Field>
|
||||
)}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button appearance="secondary" onClick={() => setDialogOpen(false)}>
|
||||
Abbrechen
|
||||
</Button>
|
||||
<Button
|
||||
appearance="primary"
|
||||
disabled={!templateId || (isOnPremService && targetIds.length === 0) || addDeploymentBatch.isPending}
|
||||
onClick={() =>
|
||||
addDeploymentBatch.mutate({
|
||||
status: "New",
|
||||
templateId,
|
||||
deploymentRuleId: deploymentRuleId || undefined,
|
||||
targetIds: isOnPremService ? targetIds : undefined,
|
||||
})
|
||||
}
|
||||
>
|
||||
Deployment anlegen
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</DialogBody>
|
||||
</DialogSurface>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
@@ -202,7 +202,7 @@ export function DeploymentJobDetailsPage() {
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHeaderCell>Status</TableHeaderCell>
|
||||
<TableHeaderCell>Virtual Machine Id</TableHeaderCell>
|
||||
<TableHeaderCell>Target Id</TableHeaderCell>
|
||||
<TableHeaderCell>Deployment Batch Id</TableHeaderCell>
|
||||
<TableHeaderCell>Template Id</TableHeaderCell>
|
||||
<TableHeaderCell>Attempts</TableHeaderCell>
|
||||
@@ -213,7 +213,7 @@ export function DeploymentJobDetailsPage() {
|
||||
{data.targets.map((target) => (
|
||||
<TableRow key={target.id}>
|
||||
<TableCell>{target.status}</TableCell>
|
||||
<TableCell>{target.virtualMachineId}</TableCell>
|
||||
<TableCell>{target.targetId}</TableCell>
|
||||
<TableCell>{target.deploymentBatchId}</TableCell>
|
||||
<TableCell>{target.templateId}</TableCell>
|
||||
<TableCell>{target.attempts}</TableCell>
|
||||
@@ -227,3 +227,5 @@ export function DeploymentJobDetailsPage() {
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
@@ -227,3 +227,4 @@ export function DeploymentJobsPage() {
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
355
src/pages/DeploymentRulesPage.tsx
Normal file
355
src/pages/DeploymentRulesPage.tsx
Normal file
@@ -0,0 +1,355 @@
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
Combobox,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogBody,
|
||||
DialogContent,
|
||||
DialogSurface,
|
||||
DialogTitle,
|
||||
Field,
|
||||
Input,
|
||||
makeStyles,
|
||||
Option,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHeader,
|
||||
TableHeaderCell,
|
||||
TableRow,
|
||||
Textarea,
|
||||
Tooltip,
|
||||
} from "@fluentui/react-components";
|
||||
import { AddRegular, ArrowDownRegular, ArrowUpRegular, DeleteRegular, EditRegular } from "@fluentui/react-icons";
|
||||
import { useMemo, useState } from "react";
|
||||
import { portalApi } from "../api/portalApi";
|
||||
import { DataState } from "../components/DataState";
|
||||
import { PageHeader } from "../components/PageHeader";
|
||||
import type { AddDeploymentRule, DeploymentRule, DeploymentRuleStep } from "../types/portal";
|
||||
|
||||
type DialogMode = "add" | "edit" | null;
|
||||
|
||||
const useStyles = makeStyles({
|
||||
dialogSurface: {
|
||||
width: "min(1200px, 96vw)",
|
||||
maxWidth: "1200px",
|
||||
},
|
||||
stepActions: {
|
||||
display: "flex",
|
||||
gap: "4px",
|
||||
alignItems: "center",
|
||||
},
|
||||
stepsTable: {
|
||||
tableLayout: "fixed",
|
||||
width: "100%",
|
||||
},
|
||||
stepCellNarrow: {
|
||||
width: "52px",
|
||||
},
|
||||
stepCellAction: {
|
||||
width: "120px",
|
||||
borderLeft: "1px solid #e0e0e0",
|
||||
},
|
||||
stepCellName: {
|
||||
width: "240px",
|
||||
},
|
||||
stepCellType: {
|
||||
width: "140px",
|
||||
},
|
||||
stepCellMetadata: {
|
||||
width: "420px",
|
||||
},
|
||||
fullWidthInput: {
|
||||
width: "100%",
|
||||
minWidth: 0,
|
||||
},
|
||||
fullWidthControl: {
|
||||
width: "100%",
|
||||
},
|
||||
addStepButton: {
|
||||
marginTop: "12px",
|
||||
},
|
||||
});
|
||||
|
||||
function createDefaultStep(sortOrder: number): DeploymentRuleStep {
|
||||
return {
|
||||
sortOrder,
|
||||
name: `Step ${sortOrder}`,
|
||||
stepType: "Deploy",
|
||||
requiresApproval: false,
|
||||
metadataJson: "",
|
||||
};
|
||||
}
|
||||
|
||||
export function DeploymentRulesPage() {
|
||||
const styles = useStyles();
|
||||
const queryClient = useQueryClient();
|
||||
const [dialogMode, setDialogMode] = useState<DialogMode>(null);
|
||||
const [selectedRuleId, setSelectedRuleId] = useState("");
|
||||
const [name, setName] = useState("");
|
||||
const [description, setDescription] = useState("");
|
||||
const [isActive, setIsActive] = useState(true);
|
||||
const [steps, setSteps] = useState<DeploymentRuleStep[]>([createDefaultStep(1)]);
|
||||
|
||||
const { data, error, isLoading } = useQuery({
|
||||
queryKey: ["deployment-rules"],
|
||||
queryFn: ({ signal }) => portalApi.getDeploymentRules(signal),
|
||||
});
|
||||
|
||||
const addRule = useMutation({
|
||||
mutationFn: portalApi.addDeploymentRule,
|
||||
onSuccess: async () => {
|
||||
closeDialog();
|
||||
await queryClient.invalidateQueries({ queryKey: ["deployment-rules"] });
|
||||
},
|
||||
});
|
||||
|
||||
const updateRule = useMutation({
|
||||
mutationFn: ({ id, payload }: { id: string; payload: AddDeploymentRule }) =>
|
||||
portalApi.updateDeploymentRule(id, payload),
|
||||
onSuccess: async () => {
|
||||
closeDialog();
|
||||
await queryClient.invalidateQueries({ queryKey: ["deployment-rules"] });
|
||||
},
|
||||
});
|
||||
|
||||
const deleteRule = useMutation({
|
||||
mutationFn: portalApi.deleteDeploymentRule,
|
||||
onSuccess: async () => {
|
||||
await queryClient.invalidateQueries({ queryKey: ["deployment-rules"] });
|
||||
},
|
||||
});
|
||||
|
||||
const openAddDialog = () => {
|
||||
setDialogMode("add");
|
||||
setSelectedRuleId("");
|
||||
setName("");
|
||||
setDescription("");
|
||||
setIsActive(true);
|
||||
setSteps([createDefaultStep(1)]);
|
||||
};
|
||||
|
||||
const openEditDialog = (rule: DeploymentRule) => {
|
||||
setDialogMode("edit");
|
||||
setSelectedRuleId(rule.id);
|
||||
setName(rule.name);
|
||||
setDescription(rule.description ?? "");
|
||||
setIsActive(rule.isActive);
|
||||
setSteps(
|
||||
(rule.steps?.length ? rule.steps : [createDefaultStep(1)]).map((step, index) => ({
|
||||
...step,
|
||||
sortOrder: index + 1,
|
||||
})),
|
||||
);
|
||||
};
|
||||
|
||||
const closeDialog = () => {
|
||||
setDialogMode(null);
|
||||
};
|
||||
|
||||
const normalizedSteps = useMemo(
|
||||
() =>
|
||||
steps.map((step, index) => ({
|
||||
...step,
|
||||
sortOrder: index + 1,
|
||||
})),
|
||||
[steps],
|
||||
);
|
||||
|
||||
const payload: AddDeploymentRule = {
|
||||
name,
|
||||
description,
|
||||
isActive,
|
||||
steps: normalizedSteps,
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHeader title="Deployment Rules" description="Low-Code Regeln fuer Deployment Pipelines gestalten." />
|
||||
<Button appearance="primary" icon={<AddRegular />} onClick={openAddDialog}>
|
||||
Neue Rule
|
||||
</Button>
|
||||
<DataState isLoading={isLoading} error={error} />
|
||||
{data && (
|
||||
<Table aria-label="Deployment rules">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHeaderCell>Name</TableHeaderCell>
|
||||
<TableHeaderCell>Status</TableHeaderCell>
|
||||
<TableHeaderCell>Steps</TableHeaderCell>
|
||||
<TableHeaderCell>Modified</TableHeaderCell>
|
||||
<TableHeaderCell>Actions</TableHeaderCell>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{data.map((rule) => (
|
||||
<TableRow key={rule.id}>
|
||||
<TableCell>{rule.name}</TableCell>
|
||||
<TableCell>{rule.isActive ? "Active" : "Inactive"}</TableCell>
|
||||
<TableCell>{rule.steps?.length ?? 0}</TableCell>
|
||||
<TableCell>{rule.modified ? new Date(rule.modified).toLocaleString() : "-"}</TableCell>
|
||||
<TableCell>
|
||||
<Tooltip content="Edit" relationship="label">
|
||||
<Button appearance="subtle" icon={<EditRegular />} onClick={() => openEditDialog(rule)} />
|
||||
</Tooltip>
|
||||
<Tooltip content="Delete" relationship="label">
|
||||
<Button appearance="subtle" icon={<DeleteRegular />} onClick={() => deleteRule.mutate(rule.id)} />
|
||||
</Tooltip>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
|
||||
<Dialog open={dialogMode !== null} onOpenChange={(_, data) => !data.open && closeDialog()}>
|
||||
<DialogSurface className={styles.dialogSurface}>
|
||||
<DialogBody>
|
||||
<DialogTitle>{dialogMode === "edit" ? "Rule bearbeiten" : "Neue Rule"}</DialogTitle>
|
||||
<DialogContent>
|
||||
<Field label="Name" required>
|
||||
<Input value={name} onChange={(_, data) => setName(data.value)} />
|
||||
</Field>
|
||||
<Field label="Description">
|
||||
<Textarea value={description} onChange={(_, data) => setDescription(data.value)} />
|
||||
</Field>
|
||||
<Field label="Active">
|
||||
<Checkbox checked={isActive} onChange={(_, data) => setIsActive(Boolean(data.checked))} />
|
||||
</Field>
|
||||
<Field label="Pipeline Steps">
|
||||
<Table aria-label="Rule steps" className={styles.stepsTable}>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHeaderCell className={styles.stepCellNarrow}>#</TableHeaderCell>
|
||||
<TableHeaderCell className={styles.stepCellName}>Name</TableHeaderCell>
|
||||
<TableHeaderCell className={styles.stepCellType}>Action</TableHeaderCell>
|
||||
<TableHeaderCell className={styles.stepCellMetadata}>Metadata</TableHeaderCell>
|
||||
<TableHeaderCell className={styles.stepCellAction}>Actions</TableHeaderCell>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{steps.map((step, index) => (
|
||||
<TableRow key={`${index}-${step.name}`}>
|
||||
<TableCell className={styles.stepCellNarrow}>{index + 1}</TableCell>
|
||||
<TableCell className={styles.stepCellName}>
|
||||
<Input
|
||||
className={styles.fullWidthControl}
|
||||
value={step.name}
|
||||
onChange={(_, data) =>
|
||||
setSteps((prev) => prev.map((entry, i) => (i === index ? { ...entry, name: data.value } : entry)))
|
||||
}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className={styles.stepCellType}>
|
||||
<Combobox
|
||||
className={styles.fullWidthControl}
|
||||
value={step.stepType}
|
||||
onOptionSelect={(_, data) =>
|
||||
setSteps((prev) => prev.map((entry, i) => (i === index ? { ...entry, stepType: data.optionValue ?? "Deploy" } : entry)))
|
||||
}
|
||||
>
|
||||
<Option value="Deploy">Deploy</Option>
|
||||
<Option value="Validate">Validate</Option>
|
||||
<Option value="Approval">Approval</Option>
|
||||
<Option value="Notify">Notify</Option>
|
||||
</Combobox>
|
||||
</TableCell>
|
||||
<TableCell className={styles.stepCellMetadata}>
|
||||
<Input
|
||||
placeholder={
|
||||
step.stepType === "Approval"
|
||||
? "z.B. approverUser / approverMail"
|
||||
: step.stepType === "Deploy" || step.stepType === "Validate"
|
||||
? "z.B. target=Test|Prod oder scope=VM|Domain"
|
||||
: "Schluessel/Wert JSON oder Text"
|
||||
}
|
||||
value={step.metadataJson ?? ""}
|
||||
onChange={(_, data) =>
|
||||
setSteps((prev) => prev.map((entry, i) => (i === index ? { ...entry, metadataJson: data.value } : entry)))
|
||||
}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className={styles.stepCellAction}>
|
||||
<div className={styles.stepActions}>
|
||||
<Tooltip content="Nach oben" relationship="label">
|
||||
<Button
|
||||
appearance="subtle"
|
||||
size="small"
|
||||
icon={<ArrowUpRegular />}
|
||||
disabled={index === 0}
|
||||
onClick={() =>
|
||||
setSteps((prev) => {
|
||||
const clone = [...prev];
|
||||
[clone[index - 1], clone[index]] = [clone[index], clone[index - 1]];
|
||||
return clone;
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip content="Nach unten" relationship="label">
|
||||
<Button
|
||||
appearance="subtle"
|
||||
size="small"
|
||||
icon={<ArrowDownRegular />}
|
||||
disabled={index === steps.length - 1}
|
||||
onClick={() =>
|
||||
setSteps((prev) => {
|
||||
const clone = [...prev];
|
||||
[clone[index + 1], clone[index]] = [clone[index], clone[index + 1]];
|
||||
return clone;
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip content="Step löschen" relationship="label">
|
||||
<Button
|
||||
appearance="subtle"
|
||||
size="small"
|
||||
icon={<DeleteRegular />}
|
||||
disabled={steps.length === 1}
|
||||
onClick={() => setSteps((prev) => prev.filter((_, i) => i !== index))}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<Button
|
||||
appearance="secondary"
|
||||
className={styles.addStepButton}
|
||||
icon={<AddRegular />}
|
||||
onClick={() => setSteps((prev) => [...prev, createDefaultStep(prev.length + 1)])}
|
||||
>
|
||||
Step hinzufügen
|
||||
</Button>
|
||||
</Field>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button appearance="secondary" onClick={closeDialog}>
|
||||
Abbrechen
|
||||
</Button>
|
||||
<Button
|
||||
appearance="primary"
|
||||
disabled={!name.trim()}
|
||||
onClick={() => {
|
||||
if (dialogMode === "edit" && selectedRuleId) {
|
||||
updateRule.mutate({ id: selectedRuleId, payload });
|
||||
return;
|
||||
}
|
||||
addRule.mutate(payload);
|
||||
}}
|
||||
>
|
||||
Speichern
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</DialogBody>
|
||||
</DialogSurface>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
@@ -23,7 +23,7 @@ import { PageHeader } from "../components/PageHeader";
|
||||
export function DeploymentsPage() {
|
||||
const queryClient = useQueryClient();
|
||||
const [deploymentBatchId, setDeploymentBatchId] = useState("");
|
||||
const [selectedVirtualMachineIds, setSelectedVirtualMachineIds] = useState<string[]>([]);
|
||||
const [selectedTargetIds, setSelectedTargetIds] = useState<string[]>([]);
|
||||
const [jsonData, setJsonData] = useState("{}");
|
||||
const { data, error, isLoading } = useQuery({
|
||||
queryKey: ["deployments"],
|
||||
@@ -33,9 +33,9 @@ export function DeploymentsPage() {
|
||||
queryKey: ["deployment-batches"],
|
||||
queryFn: ({ signal }) => portalApi.getDeploymentBatches(signal),
|
||||
});
|
||||
const { data: virtualMachines } = useQuery({
|
||||
queryKey: ["virtual-machines"],
|
||||
queryFn: ({ signal }) => portalApi.getVirtualMachines(signal),
|
||||
const { data: targets } = useQuery({
|
||||
queryKey: ["targets"],
|
||||
queryFn: ({ signal }) => portalApi.getTargets(signal),
|
||||
});
|
||||
const { data: deploymentJobs, error: deploymentJobsError, isLoading: deploymentJobsLoading } = useQuery({
|
||||
queryKey: ["deployment-jobs"],
|
||||
@@ -46,7 +46,7 @@ export function DeploymentsPage() {
|
||||
mutationFn: portalApi.addDeploymentRequest,
|
||||
onSuccess: async () => {
|
||||
setDeploymentBatchId("");
|
||||
setSelectedVirtualMachineIds([]);
|
||||
setSelectedTargetIds([]);
|
||||
setJsonData("{}");
|
||||
await queryClient.invalidateQueries({ queryKey: ["deployments"] });
|
||||
await queryClient.invalidateQueries({ queryKey: ["deployment-jobs"] });
|
||||
@@ -65,7 +65,7 @@ export function DeploymentsPage() {
|
||||
<FormSection
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault();
|
||||
addDeploymentRequest.mutate({ deploymentBatchId, jsonData, virtualMachineIds: selectedVirtualMachineIds });
|
||||
addDeploymentRequest.mutate({ deploymentBatchId, jsonData, targetIds: selectedTargetIds });
|
||||
}}
|
||||
>
|
||||
<FormGrid>
|
||||
@@ -82,18 +82,18 @@ export function DeploymentsPage() {
|
||||
))}
|
||||
</Combobox>
|
||||
</Field>
|
||||
<Field label="Targets (Virtual Machines)" required>
|
||||
<Field label="Targets (Targets)" required>
|
||||
<div>
|
||||
{(virtualMachines ?? []).map((virtualMachine) => (
|
||||
{(targets ?? []).map((target) => (
|
||||
<Checkbox
|
||||
key={virtualMachine.id}
|
||||
label={virtualMachine.name}
|
||||
checked={selectedVirtualMachineIds.includes(virtualMachine.id)}
|
||||
key={target.id}
|
||||
label={target.name}
|
||||
checked={selectedTargetIds.includes(target.id)}
|
||||
onChange={(_, data) => {
|
||||
if (data.checked) {
|
||||
setSelectedVirtualMachineIds((previous) => [...previous, virtualMachine.id]);
|
||||
setSelectedTargetIds((previous) => [...previous, target.id]);
|
||||
} else {
|
||||
setSelectedVirtualMachineIds((previous) => previous.filter((id) => id !== virtualMachine.id));
|
||||
setSelectedTargetIds((previous) => previous.filter((id) => id !== target.id));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
@@ -109,7 +109,7 @@ export function DeploymentsPage() {
|
||||
<FormActions>
|
||||
<Button
|
||||
appearance="primary"
|
||||
disabled={!deploymentBatchId || selectedVirtualMachineIds.length === 0 || addDeploymentRequest.isPending}
|
||||
disabled={!deploymentBatchId || selectedTargetIds.length === 0 || addDeploymentRequest.isPending}
|
||||
type="submit"
|
||||
>
|
||||
Start deployment
|
||||
@@ -158,7 +158,7 @@ export function DeploymentsPage() {
|
||||
<TableRow>
|
||||
<TableHeaderCell>Status</TableHeaderCell>
|
||||
<TableHeaderCell>Deployment Batch</TableHeaderCell>
|
||||
<TableHeaderCell>Virtual Machine</TableHeaderCell>
|
||||
<TableHeaderCell>Target</TableHeaderCell>
|
||||
<TableHeaderCell>Execution Id</TableHeaderCell>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
@@ -167,7 +167,7 @@ export function DeploymentsPage() {
|
||||
<TableRow key={deployment.id}>
|
||||
<TableCell>{deployment.status ?? "Unknown"}</TableCell>
|
||||
<TableCell>{deployment.deploymentBatchId ?? "-"}</TableCell>
|
||||
<TableCell>{deployment.virtualMachineId ?? "-"}</TableCell>
|
||||
<TableCell>{deployment.targetId ?? "-"}</TableCell>
|
||||
<TableCell>{deployment.id}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
@@ -177,3 +177,5 @@ export function DeploymentsPage() {
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
@@ -73,21 +73,21 @@ export function DomainDetailsPage() {
|
||||
queryFn: ({ signal }) => portalApi.getDomainEnvironments(id!, signal),
|
||||
});
|
||||
const {
|
||||
data: virtualMachineData,
|
||||
error: virtualMachinesError,
|
||||
isLoading: virtualMachinesLoading,
|
||||
data: targetData,
|
||||
error: targetsError,
|
||||
isLoading: targetsLoading,
|
||||
} = useQuery({
|
||||
enabled: Boolean(id),
|
||||
queryKey: ["domain", id, "virtual-machines"],
|
||||
queryFn: ({ signal }) => portalApi.getDomainVirtualMachines(id!, signal),
|
||||
queryKey: ["domain", id, "targets"],
|
||||
queryFn: ({ signal }) => portalApi.getDomainTargets(id!, signal),
|
||||
});
|
||||
const links = data?.environmentDomains?.filter((link) => link.environment) ?? [];
|
||||
const filteredVirtualMachines = useMemo(() => {
|
||||
const items = [...(virtualMachineData?.virtualMachines ?? [])];
|
||||
const filteredTargets = useMemo(() => {
|
||||
const items = [...(targetData?.targets ?? [])];
|
||||
const searchValue = search.trim().toLowerCase();
|
||||
const filteredItems = searchValue.length
|
||||
? items.filter((virtualMachine) =>
|
||||
[virtualMachine.name, virtualMachine.externalId, virtualMachine.id]
|
||||
? items.filter((target) =>
|
||||
[target.name, target.externalId, target.id]
|
||||
.filter(Boolean)
|
||||
.some((value) => value!.toLowerCase().includes(searchValue)),
|
||||
)
|
||||
@@ -101,7 +101,7 @@ export function DomainDetailsPage() {
|
||||
});
|
||||
|
||||
return filteredItems;
|
||||
}, [virtualMachineData?.virtualMachines, search, sortBy, sortOrder]);
|
||||
}, [targetData?.targets, search, sortBy, sortOrder]);
|
||||
|
||||
const toggleSort = (column: "name" | "externalId" | "id") => {
|
||||
if (sortBy === column) {
|
||||
@@ -115,7 +115,7 @@ export function DomainDetailsPage() {
|
||||
|
||||
const sortIndicator = (column: "name" | "externalId" | "id") => {
|
||||
if (sortBy !== column) return "";
|
||||
return sortOrder === "asc" ? " ↑" : " ↓";
|
||||
return sortOrder === "asc" ? " ↑" : " ↓";
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -126,7 +126,7 @@ export function DomainDetailsPage() {
|
||||
</Button>
|
||||
</Link>
|
||||
<PageHeader title={data?.name ?? "Domain"} description="Details und verknuepfte Environments." />
|
||||
<DataState isLoading={isLoading || virtualMachinesLoading} error={error ?? virtualMachinesError} />
|
||||
<DataState isLoading={isLoading || targetsLoading} error={error ?? targetsError} />
|
||||
{data && (
|
||||
<>
|
||||
<section className={styles.details} aria-label="Domain details">
|
||||
@@ -196,7 +196,7 @@ export function DomainDetailsPage() {
|
||||
</Table>
|
||||
)}
|
||||
|
||||
<Title3 className={styles.sectionTitle}>Virtual Machines</Title3>
|
||||
<Title3 className={styles.sectionTitle}>Targets</Title3>
|
||||
<div style={{ display: "grid", gap: "12px", gridTemplateColumns: "minmax(220px, 1fr)", marginBottom: "12px" }}>
|
||||
<Field label="Search">
|
||||
<Input
|
||||
@@ -206,12 +206,12 @@ export function DomainDetailsPage() {
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
{filteredVirtualMachines.length === 0 ? (
|
||||
{filteredTargets.length === 0 ? (
|
||||
<MessageBar>
|
||||
<MessageBarBody>Keine Virtual Machines fuer diese Domain gefunden.</MessageBarBody>
|
||||
<MessageBarBody>Keine Targets fuer diese Domain gefunden.</MessageBarBody>
|
||||
</MessageBar>
|
||||
) : (
|
||||
<Table aria-label="Domain virtual machines">
|
||||
<Table aria-label="Domain targets">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHeaderCell onClick={() => toggleSort("name")} style={{ cursor: "pointer" }}>
|
||||
@@ -226,13 +226,13 @@ export function DomainDetailsPage() {
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filteredVirtualMachines.map((virtualMachine) => (
|
||||
<TableRow key={virtualMachine.id}>
|
||||
{filteredTargets.map((target) => (
|
||||
<TableRow key={target.id}>
|
||||
<TableCell>
|
||||
<Link to={`/virtual-machines/${virtualMachine.id}`}>{virtualMachine.name}</Link>
|
||||
<Link to={`/targets/${target.id}`}>{target.name}</Link>
|
||||
</TableCell>
|
||||
<TableCell>{virtualMachine.externalId ?? "-"}</TableCell>
|
||||
<TableCell>{virtualMachine.id}</TableCell>
|
||||
<TableCell>{target.externalId ?? "-"}</TableCell>
|
||||
<TableCell>{target.id}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
@@ -243,3 +243,5 @@ export function DomainDetailsPage() {
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
Button,
|
||||
Combobox,
|
||||
@@ -308,3 +308,4 @@ export function DomainsPage() {
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
tokens,
|
||||
Tooltip,
|
||||
} from "@fluentui/react-components";
|
||||
import { ArrowLeftRegular, OpenRegular } from "@fluentui/react-icons";
|
||||
import { ArrowLeftRegular, DeleteRegular, OpenRegular } from "@fluentui/react-icons";
|
||||
import { Link, useParams } from "react-router-dom";
|
||||
import { portalApi } from "../api/portalApi";
|
||||
import { DataState } from "../components/DataState";
|
||||
@@ -59,6 +59,7 @@ const useStyles = makeStyles({
|
||||
|
||||
export function EnvironmentDetailsPage() {
|
||||
const styles = useStyles();
|
||||
const queryClient = useQueryClient();
|
||||
const { id } = useParams();
|
||||
const { data, error, isLoading } = useQuery({
|
||||
enabled: Boolean(id),
|
||||
@@ -66,6 +67,15 @@ export function EnvironmentDetailsPage() {
|
||||
queryFn: ({ signal }) => portalApi.getEnvironmentDomains(id!, signal),
|
||||
});
|
||||
const links = data?.environmentDomains?.filter((link) => link.domain) ?? [];
|
||||
const unlinkDomainFromEnvironment = useMutation({
|
||||
mutationFn: ({ domainId, environmentId }: { domainId: string; environmentId: string }) =>
|
||||
portalApi.unlinkDomainFromEnvironment(domainId, environmentId),
|
||||
onSuccess: async () => {
|
||||
await queryClient.invalidateQueries({ queryKey: ["environment", id, "domains"] });
|
||||
await queryClient.invalidateQueries({ queryKey: ["domains"] });
|
||||
await queryClient.invalidateQueries({ queryKey: ["environments"] });
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -92,24 +102,30 @@ export function EnvironmentDetailsPage() {
|
||||
</Text>
|
||||
</div>
|
||||
<div className={styles.field}>
|
||||
<Text size={200}>Environment Type</Text>
|
||||
<Text size={200}>Environment Stage</Text>
|
||||
<Text className={styles.value} weight="semibold">
|
||||
{data.environmentType}
|
||||
</Text>
|
||||
</div>
|
||||
<div className={styles.field}>
|
||||
<Text size={200}>Hosting Type</Text>
|
||||
<Text className={styles.value} weight="semibold">
|
||||
{data.hostingType}
|
||||
</Text>
|
||||
</div>
|
||||
<div className={styles.field}>
|
||||
<Text size={200}>Cloud Enabled</Text>
|
||||
<Text className={styles.value} weight="semibold">
|
||||
{data.environmentType === "OnPrem" ? "Nein" : "Ja"}
|
||||
{data.hostingType === "OnPrem" ? "Nein" : "Ja"}
|
||||
</Text>
|
||||
</div>
|
||||
<div className={styles.field}>
|
||||
<Text size={200}>Provider Type</Text>
|
||||
<Text className={styles.value} weight="semibold">
|
||||
{data.environmentType === "OnPrem" ? data.providerType : ""}
|
||||
{data.hostingType === "OnPrem" ? data.providerType : ""}
|
||||
</Text>
|
||||
</div>
|
||||
{data.environmentType !== "OnPrem" && (
|
||||
{data.hostingType !== "OnPrem" && (
|
||||
<div className={styles.field}>
|
||||
<Text size={200}>Tenant Id</Text>
|
||||
<Text className={styles.value} weight="semibold">
|
||||
@@ -117,7 +133,7 @@ export function EnvironmentDetailsPage() {
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
{data.environmentType === "AzureTenant" && (
|
||||
{data.hostingType === "AzureTenant" && (
|
||||
<div className={styles.field}>
|
||||
<Text size={200}>Subscription Id</Text>
|
||||
<Text className={styles.value} weight="semibold">
|
||||
@@ -140,7 +156,7 @@ export function EnvironmentDetailsPage() {
|
||||
<TableHeaderCell>FQDN</TableHeaderCell>
|
||||
<TableHeaderCell>NetBIOS</TableHeaderCell>
|
||||
<TableHeaderCell>Status</TableHeaderCell>
|
||||
<TableHeaderCell>Details</TableHeaderCell>
|
||||
<TableHeaderCell>Actions</TableHeaderCell>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
@@ -161,6 +177,21 @@ export function EnvironmentDetailsPage() {
|
||||
<Button appearance="subtle" aria-label="Details" icon={<OpenRegular />} />
|
||||
</Link>
|
||||
</Tooltip>
|
||||
<Tooltip content="Unlink" relationship="label">
|
||||
<Button
|
||||
appearance="subtle"
|
||||
aria-label="Unlink"
|
||||
disabled={unlinkDomainFromEnvironment.isPending}
|
||||
icon={<DeleteRegular />}
|
||||
onClick={() => {
|
||||
if (!id) return;
|
||||
unlinkDomainFromEnvironment.mutate({
|
||||
domainId: link.domain!.id,
|
||||
environmentId: id,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
@@ -173,3 +204,4 @@ export function EnvironmentDetailsPage() {
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
Button,
|
||||
Combobox,
|
||||
@@ -102,3 +102,4 @@ export function EnvironmentDomainsPage() {
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
Button,
|
||||
Combobox,
|
||||
@@ -56,7 +56,8 @@ export function EnvironmentsPage() {
|
||||
const [dialogMode, setDialogMode] = useState<DialogMode>(null);
|
||||
const [selectedEnvironment, setSelectedEnvironment] = useState<EnvironmentItem | null>(null);
|
||||
const [name, setName] = useState("");
|
||||
const [environmentType, setEnvironmentType] = useState("OnPrem");
|
||||
const [environmentType, setEnvironmentType] = useState("Test");
|
||||
const [hostingType, setHostingType] = useState("OnPrem");
|
||||
const [providerType, setProviderType] = useState("");
|
||||
const [tenantId, setTenantId] = useState("");
|
||||
const [subscriptionId, setSubscriptionId] = useState("");
|
||||
@@ -76,7 +77,8 @@ export function EnvironmentsPage() {
|
||||
setDialogMode(null);
|
||||
setSelectedEnvironment(null);
|
||||
setName("");
|
||||
setEnvironmentType("OnPrem");
|
||||
setEnvironmentType("Test");
|
||||
setHostingType("OnPrem");
|
||||
setProviderType("");
|
||||
setTenantId("");
|
||||
setSubscriptionId("");
|
||||
@@ -86,7 +88,8 @@ export function EnvironmentsPage() {
|
||||
const openAddDialog = () => {
|
||||
setSelectedEnvironment(null);
|
||||
setName("");
|
||||
setEnvironmentType("OnPrem");
|
||||
setEnvironmentType("Test");
|
||||
setHostingType("OnPrem");
|
||||
setProviderType("");
|
||||
setTenantId("");
|
||||
setSubscriptionId("");
|
||||
@@ -97,7 +100,8 @@ export function EnvironmentsPage() {
|
||||
const openEditDialog = (environment: EnvironmentItem) => {
|
||||
setSelectedEnvironment(environment);
|
||||
setName(environment.name);
|
||||
setEnvironmentType(environment.environmentType ?? "OnPrem");
|
||||
setEnvironmentType(environment.environmentType ?? "Test");
|
||||
setHostingType(environment.hostingType ?? "OnPrem");
|
||||
setProviderType(environment.providerType ?? "");
|
||||
setTenantId(environment.tenantId ?? "");
|
||||
setSubscriptionId(environment.subscriptionId ?? "");
|
||||
@@ -121,6 +125,7 @@ export function EnvironmentsPage() {
|
||||
mutationFn: ({ id, environment }: { id: string; environment: {
|
||||
name: string;
|
||||
environmentType: string;
|
||||
hostingType: string;
|
||||
providerType?: string;
|
||||
tenantId?: string;
|
||||
subscriptionId?: string;
|
||||
@@ -155,6 +160,7 @@ export function EnvironmentsPage() {
|
||||
const environment = {
|
||||
name,
|
||||
environmentType,
|
||||
hostingType,
|
||||
providerType,
|
||||
tenantId,
|
||||
subscriptionId,
|
||||
@@ -171,9 +177,9 @@ export function EnvironmentsPage() {
|
||||
|
||||
const formError = addEnvironment.error?.message ?? updateEnvironment.error?.message;
|
||||
const isSaving = addEnvironment.isPending || updateEnvironment.isPending;
|
||||
const isOnPrem = environmentType === "OnPrem";
|
||||
const isAzureTenant = environmentType === "AzureTenant";
|
||||
const isM365Tenant = environmentType === "M365Tenant";
|
||||
const isOnPrem = hostingType === "OnPrem";
|
||||
const isAzureTenant = hostingType === "AzureTenant";
|
||||
const isM365Tenant = hostingType === "M365Tenant";
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -198,12 +204,28 @@ export function EnvironmentsPage() {
|
||||
<Field label="Name" required validationMessage={formError}>
|
||||
<Input value={name} onChange={(_, data) => setName(data.value)} />
|
||||
</Field>
|
||||
<Field label="Environment Type" required>
|
||||
<Combobox
|
||||
value={environmentType}
|
||||
onOptionSelect={(_, data) => {
|
||||
const nextType = data.optionValue ?? "OnPrem";
|
||||
setEnvironmentType(nextType);
|
||||
<Field label="Environment Stage" required>
|
||||
<Combobox value={environmentType} onOptionSelect={(_, data) => setEnvironmentType(data.optionValue ?? "Test")}>
|
||||
<Option text="Development" value="Development">
|
||||
Development
|
||||
</Option>
|
||||
<Option text="Test" value="Test">
|
||||
Test
|
||||
</Option>
|
||||
<Option text="QA" value="QA">
|
||||
QA
|
||||
</Option>
|
||||
<Option text="Production" value="Production">
|
||||
Production
|
||||
</Option>
|
||||
</Combobox>
|
||||
</Field>
|
||||
<Field label="Hosting Type" required>
|
||||
<Combobox
|
||||
value={hostingType}
|
||||
onOptionSelect={(_, data) => {
|
||||
const nextType = data.optionValue ?? "OnPrem";
|
||||
setHostingType(nextType);
|
||||
|
||||
if (nextType === "OnPrem") {
|
||||
setTenantId("");
|
||||
@@ -352,7 +374,8 @@ export function EnvironmentsPage() {
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHeaderCell>Name</TableHeaderCell>
|
||||
<TableHeaderCell>Type</TableHeaderCell>
|
||||
<TableHeaderCell>Stage</TableHeaderCell>
|
||||
<TableHeaderCell>Hosting</TableHeaderCell>
|
||||
<TableHeaderCell>Cloud</TableHeaderCell>
|
||||
<TableHeaderCell>Provider</TableHeaderCell>
|
||||
<TableHeaderCell>Tenant</TableHeaderCell>
|
||||
@@ -366,10 +389,11 @@ export function EnvironmentsPage() {
|
||||
<TableRow key={environment.id}>
|
||||
<TableCell>{environment.name}</TableCell>
|
||||
<TableCell>{environment.environmentType}</TableCell>
|
||||
<TableCell>{environment.environmentType === "OnPrem" ? "Nein" : "Ja"}</TableCell>
|
||||
<TableCell>{environment.environmentType === "OnPrem" ? environment.providerType : ""}</TableCell>
|
||||
<TableCell>{environment.environmentType !== "OnPrem" ? environment.tenantId : ""}</TableCell>
|
||||
<TableCell>{environment.environmentType === "AzureTenant" ? environment.subscriptionId : ""}</TableCell>
|
||||
<TableCell>{environment.hostingType}</TableCell>
|
||||
<TableCell>{environment.hostingType === "OnPrem" ? "Nein" : "Ja"}</TableCell>
|
||||
<TableCell>{environment.hostingType === "OnPrem" ? environment.providerType : ""}</TableCell>
|
||||
<TableCell>{environment.hostingType !== "OnPrem" ? environment.tenantId : ""}</TableCell>
|
||||
<TableCell>{environment.hostingType === "AzureTenant" ? environment.subscriptionId : ""}</TableCell>
|
||||
<TableCell>{environment.id}</TableCell>
|
||||
<TableCell>
|
||||
<div className={styles.actions}>
|
||||
@@ -409,3 +433,4 @@ export function EnvironmentsPage() {
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
Button,
|
||||
Field,
|
||||
@@ -87,3 +87,4 @@ export function RunbooksPage() {
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
@@ -573,3 +573,4 @@ export function ServicesPage() {
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
@@ -39,32 +39,44 @@ const useStyles = makeStyles({
|
||||
},
|
||||
});
|
||||
|
||||
export function VirtualMachineDetailsPage() {
|
||||
export function TargetDetailsPage() {
|
||||
const styles = useStyles();
|
||||
const { id } = useParams();
|
||||
const { data, error, isLoading } = useQuery({
|
||||
enabled: Boolean(id),
|
||||
queryKey: ["virtual-machine", id],
|
||||
queryFn: ({ signal }) => portalApi.getVirtualMachineById(id!, signal),
|
||||
queryKey: ["target", id],
|
||||
queryFn: ({ signal }) => portalApi.getTargetById(id!, signal),
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Link className={styles.backLink} to="/virtual-machines">
|
||||
<Link className={styles.backLink} to="/targets">
|
||||
<Button appearance="subtle" icon={<ArrowLeftRegular />}>
|
||||
Virtual Machines
|
||||
Targets
|
||||
</Button>
|
||||
</Link>
|
||||
<PageHeader title={data?.name ?? "Virtual Machine"} description="Details zur Virtual Machine." />
|
||||
<PageHeader title={data?.name ?? "Target"} description="Details zum Deployment-Ziel." />
|
||||
<DataState isLoading={isLoading} error={error} />
|
||||
{data && (
|
||||
<section className={styles.details} aria-label="Virtual machine details">
|
||||
<section className={styles.details} aria-label="Target details">
|
||||
<div className={styles.field}>
|
||||
<Text size={200}>Name</Text>
|
||||
<Text className={styles.value} weight="semibold">
|
||||
{data.name}
|
||||
</Text>
|
||||
</div>
|
||||
<div className={styles.field}>
|
||||
<Text size={200}>Target Type</Text>
|
||||
<Text className={styles.value} weight="semibold">
|
||||
{data.targetType}
|
||||
</Text>
|
||||
</div>
|
||||
<div className={styles.field}>
|
||||
<Text size={200}>Provider Type</Text>
|
||||
<Text className={styles.value} weight="semibold">
|
||||
{data.providerType}
|
||||
</Text>
|
||||
</div>
|
||||
<div className={styles.field}>
|
||||
<Text size={200}>Domain Link Status</Text>
|
||||
<Badge appearance={data.domainID ? "filled" : "tint"} color={data.domainID ? "success" : "informative"}>
|
||||
@@ -100,3 +112,4 @@ export function VirtualMachineDetailsPage() {
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
Button,
|
||||
Combobox,
|
||||
@@ -27,7 +27,7 @@ import { useState } from "react";
|
||||
import { portalApi } from "../api/portalApi";
|
||||
import { DataState } from "../components/DataState";
|
||||
import { PageHeader } from "../components/PageHeader";
|
||||
import type { Domain, VirtualMachine } from "../types/portal";
|
||||
import type { Domain, Target } from "../types/portal";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
const useStyles = makeStyles({
|
||||
@@ -52,22 +52,24 @@ const useStyles = makeStyles({
|
||||
|
||||
type DialogMode = "add" | "edit" | null;
|
||||
|
||||
export function VirtualMachinesPage() {
|
||||
export function TargetsPage() {
|
||||
const styles = useStyles();
|
||||
const queryClient = useQueryClient();
|
||||
const [dialogMode, setDialogMode] = useState<DialogMode>(null);
|
||||
const [selectedVirtualMachine, setSelectedVirtualMachine] = useState<VirtualMachine | null>(null);
|
||||
const [selectedTarget, setSelectedTarget] = useState<Target | null>(null);
|
||||
const [domainID, setDomainID] = useState<string | undefined>(undefined);
|
||||
const [linkVirtualMachineId, setLinkVirtualMachineId] = useState("");
|
||||
const [linkTargetId, setLinkTargetId] = useState("");
|
||||
const [linkDomainId, setLinkDomainId] = useState("");
|
||||
const [unlinkVirtualMachineId, setUnlinkVirtualMachineId] = useState("");
|
||||
const [unlinkTargetId, setUnlinkTargetId] = useState("");
|
||||
const [name, setName] = useState("");
|
||||
const [targetType, setTargetType] = useState("VirtualMachine");
|
||||
const [providerType, setProviderType] = useState("OnPrem");
|
||||
const [externalId, setExternalId] = useState("");
|
||||
const [metadataJson, setMetadataJson] = useState("");
|
||||
|
||||
const { data, error, isLoading } = useQuery({
|
||||
queryKey: ["virtual-machines"],
|
||||
queryFn: ({ signal }) => portalApi.getVirtualMachines(signal),
|
||||
queryKey: ["targets"],
|
||||
queryFn: ({ signal }) => portalApi.getTargets(signal),
|
||||
});
|
||||
const { data: domains, error: domainsError, isLoading: domainsLoading } = useQuery({
|
||||
queryKey: ["domains"],
|
||||
@@ -76,110 +78,116 @@ export function VirtualMachinesPage() {
|
||||
|
||||
const closeDialog = () => {
|
||||
setDialogMode(null);
|
||||
setSelectedVirtualMachine(null);
|
||||
setSelectedTarget(null);
|
||||
setDomainID(undefined);
|
||||
setName("");
|
||||
setTargetType("VirtualMachine");
|
||||
setProviderType("OnPrem");
|
||||
setExternalId("");
|
||||
setMetadataJson("");
|
||||
};
|
||||
|
||||
const openAddDialog = () => {
|
||||
setSelectedVirtualMachine(null);
|
||||
setSelectedTarget(null);
|
||||
setDomainID(undefined);
|
||||
setName("");
|
||||
setTargetType("VirtualMachine");
|
||||
setProviderType("OnPrem");
|
||||
setExternalId("");
|
||||
setMetadataJson("");
|
||||
setDialogMode("add");
|
||||
};
|
||||
|
||||
const openVirtualMachineDialog = (mode: "edit", virtualMachine: VirtualMachine) => {
|
||||
setSelectedVirtualMachine(virtualMachine);
|
||||
setDomainID(virtualMachine.domainID);
|
||||
setName(virtualMachine.name);
|
||||
setExternalId(virtualMachine.externalId ?? "");
|
||||
setMetadataJson(virtualMachine.metadataJson ?? "");
|
||||
const openTargetDialog = (mode: "edit", target: Target) => {
|
||||
setSelectedTarget(target);
|
||||
setDomainID(target.domainID);
|
||||
setName(target.name);
|
||||
setTargetType(target.targetType ?? "VirtualMachine");
|
||||
setProviderType(target.providerType ?? "OnPrem");
|
||||
setExternalId(target.externalId ?? "");
|
||||
setMetadataJson(target.metadataJson ?? "");
|
||||
setDialogMode(mode);
|
||||
};
|
||||
|
||||
const addVirtualMachine = useMutation({
|
||||
mutationFn: portalApi.addVirtualMachine,
|
||||
const addTarget = useMutation({
|
||||
mutationFn: portalApi.addTarget,
|
||||
onSuccess: async () => {
|
||||
closeDialog();
|
||||
await queryClient.invalidateQueries({ queryKey: ["virtual-machines"] });
|
||||
await queryClient.invalidateQueries({ queryKey: ["targets"] });
|
||||
},
|
||||
});
|
||||
|
||||
const updateVirtualMachine = useMutation({
|
||||
mutationFn: ({ id, virtualMachine }: { id: string; virtualMachine: { name: string; externalId?: string; metadataJson?: string } }) =>
|
||||
portalApi.updateVirtualMachine(id, virtualMachine),
|
||||
const updateTarget = useMutation({
|
||||
mutationFn: ({ id, target }: { id: string; target: { name: string; targetType: string; providerType: string; externalId?: string; metadataJson?: string } }) =>
|
||||
portalApi.updateTarget(id, target),
|
||||
onSuccess: async () => {
|
||||
closeDialog();
|
||||
await queryClient.invalidateQueries({ queryKey: ["virtual-machines"] });
|
||||
await queryClient.invalidateQueries({ queryKey: ["targets"] });
|
||||
},
|
||||
});
|
||||
|
||||
const deleteVirtualMachine = useMutation({
|
||||
mutationFn: portalApi.deleteVirtualMachine,
|
||||
const deleteTarget = useMutation({
|
||||
mutationFn: portalApi.deleteTarget,
|
||||
onSuccess: async () => {
|
||||
await queryClient.invalidateQueries({ queryKey: ["virtual-machines"] });
|
||||
await queryClient.invalidateQueries({ queryKey: ["targets"] });
|
||||
},
|
||||
});
|
||||
|
||||
const linkVirtualMachineToDomain = useMutation({
|
||||
mutationFn: ({ virtualMachineId, targetDomainId }: { virtualMachineId: string; targetDomainId: string }) =>
|
||||
portalApi.linkVirtualMachineToDomain(virtualMachineId, targetDomainId),
|
||||
const linkTargetToDomain = useMutation({
|
||||
mutationFn: ({ targetId, targetDomainId }: { targetId: string; targetDomainId: string }) =>
|
||||
portalApi.linkTargetToDomain(targetId, targetDomainId),
|
||||
onSuccess: async () => {
|
||||
setLinkVirtualMachineId("");
|
||||
setLinkTargetId("");
|
||||
setLinkDomainId("");
|
||||
await queryClient.invalidateQueries({ queryKey: ["virtual-machines"] });
|
||||
await queryClient.invalidateQueries({ queryKey: ["targets"] });
|
||||
},
|
||||
});
|
||||
|
||||
const unlinkVirtualMachineFromDomain = useMutation({
|
||||
mutationFn: (virtualMachineId: string) => portalApi.unlinkVirtualMachineFromDomain(virtualMachineId),
|
||||
const unlinkTargetFromDomain = useMutation({
|
||||
mutationFn: (targetId: string) => portalApi.unlinkTargetFromDomain(targetId),
|
||||
onSuccess: async () => {
|
||||
setUnlinkVirtualMachineId("");
|
||||
await queryClient.invalidateQueries({ queryKey: ["virtual-machines"] });
|
||||
setUnlinkTargetId("");
|
||||
await queryClient.invalidateQueries({ queryKey: ["targets"] });
|
||||
},
|
||||
});
|
||||
|
||||
const submitVirtualMachine = (event: React.FormEvent<HTMLFormElement>) => {
|
||||
const submitTarget = (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
const virtualMachine = { domainID, name, externalId, metadataJson };
|
||||
const target = { domainID, name, targetType, providerType, externalId, metadataJson };
|
||||
|
||||
if (dialogMode === "edit" && selectedVirtualMachine) {
|
||||
updateVirtualMachine.mutate({ id: selectedVirtualMachine.id, virtualMachine: { name, externalId, metadataJson } });
|
||||
if (dialogMode === "edit" && selectedTarget) {
|
||||
updateTarget.mutate({ id: selectedTarget.id, target: { name, targetType, providerType, externalId, metadataJson } });
|
||||
return;
|
||||
}
|
||||
|
||||
addVirtualMachine.mutate(virtualMachine);
|
||||
addTarget.mutate(target);
|
||||
};
|
||||
|
||||
const formError =
|
||||
addVirtualMachine.error?.message ??
|
||||
updateVirtualMachine.error?.message ??
|
||||
linkVirtualMachineToDomain.error?.message ??
|
||||
unlinkVirtualMachineFromDomain.error?.message;
|
||||
const isSaving = addVirtualMachine.isPending || updateVirtualMachine.isPending;
|
||||
addTarget.error?.message ??
|
||||
updateTarget.error?.message ??
|
||||
linkTargetToDomain.error?.message ??
|
||||
unlinkTargetFromDomain.error?.message;
|
||||
const isSaving = addTarget.isPending || updateTarget.isPending;
|
||||
const domainNameById = new Map((domains ?? []).map((domain: Domain) => [domain.id, domain.name]));
|
||||
const linkedVirtualMachines = (data ?? []).filter((virtualMachine) => Boolean(virtualMachine.domainID));
|
||||
const unlinkedVirtualMachines = (data ?? []).filter((virtualMachine) => !virtualMachine.domainID);
|
||||
const linkedTargets = (data ?? []).filter((target) => Boolean(target.domainID));
|
||||
const unlinkedTargets = (data ?? []).filter((target) => !target.domainID);
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHeader title="Virtual Machines" description="Ziele fuer Deployments verwalten." />
|
||||
<PageHeader title="Targets" description="Generische Ziele fuer On-Prem-, Hybrid- und Cloud-Deployments verwalten." />
|
||||
<div className={styles.toolbar}>
|
||||
<Button appearance="primary" icon={<AddRegular />} onClick={openAddDialog}>
|
||||
Virtual Machine hinzufuegen
|
||||
Target hinzufuegen
|
||||
</Button>
|
||||
<Button
|
||||
appearance="secondary"
|
||||
icon={<LinkRegular />}
|
||||
onClick={() => {
|
||||
setLinkVirtualMachineId(unlinkedVirtualMachines[0]?.id ?? "");
|
||||
setLinkTargetId(unlinkedTargets[0]?.id ?? "");
|
||||
setLinkDomainId(domains?.[0]?.id ?? "");
|
||||
}}
|
||||
disabled={!unlinkedVirtualMachines.length || !domains?.length}
|
||||
disabled={!unlinkedTargets.length || !domains?.length}
|
||||
style={{ marginLeft: "10px" }}
|
||||
>
|
||||
Link to Domain
|
||||
@@ -188,9 +196,9 @@ export function VirtualMachinesPage() {
|
||||
appearance="secondary"
|
||||
icon={<LinkDismissRegular />}
|
||||
onClick={() => {
|
||||
setUnlinkVirtualMachineId(linkedVirtualMachines[0]?.id ?? "");
|
||||
setUnlinkTargetId(linkedTargets[0]?.id ?? "");
|
||||
}}
|
||||
disabled={!linkedVirtualMachines.length}
|
||||
disabled={!linkedTargets.length}
|
||||
style={{ marginLeft: "10px" }}
|
||||
>
|
||||
Unlink Domain
|
||||
@@ -199,12 +207,12 @@ export function VirtualMachinesPage() {
|
||||
|
||||
<Dialog open={dialogMode !== null} onOpenChange={(_, dialogData) => !dialogData.open && closeDialog()}>
|
||||
<DialogSurface>
|
||||
<form onSubmit={submitVirtualMachine}>
|
||||
<form onSubmit={submitTarget}>
|
||||
<DialogBody>
|
||||
<DialogTitle>
|
||||
{dialogMode === "edit"
|
||||
? "Virtual Machine aendern"
|
||||
: "Virtual Machine hinzufuegen"}
|
||||
? "Target aendern"
|
||||
: "Target hinzufuegen"}
|
||||
</DialogTitle>
|
||||
<DialogContent className={styles.form}>
|
||||
<Field label="Domain (optional)" validationMessage={formError}>
|
||||
@@ -224,6 +232,30 @@ export function VirtualMachinesPage() {
|
||||
<Field label="Name" required>
|
||||
<Input value={name} onChange={(_, inputData) => setName(inputData.value)} />
|
||||
</Field>
|
||||
<Field label="Target Type" required>
|
||||
<Combobox
|
||||
value={targetType}
|
||||
onOptionSelect={(_, optionData) => setTargetType(optionData.optionValue ?? "VirtualMachine")}
|
||||
>
|
||||
{["VirtualMachine", "Tenant", "Subscription", "ResourceGroup", "User", "Group", "Site", "PolicyScope"].map((type) => (
|
||||
<Option key={type} text={type} value={type}>
|
||||
{type}
|
||||
</Option>
|
||||
))}
|
||||
</Combobox>
|
||||
</Field>
|
||||
<Field label="Provider Type" required>
|
||||
<Combobox
|
||||
value={providerType}
|
||||
onOptionSelect={(_, optionData) => setProviderType(optionData.optionValue ?? "OnPrem")}
|
||||
>
|
||||
{["OnPrem", "Azure", "Microsoft365", "ExchangeOnline", "Teams"].map((provider) => (
|
||||
<Option key={provider} text={provider} value={provider}>
|
||||
{provider}
|
||||
</Option>
|
||||
))}
|
||||
</Combobox>
|
||||
</Field>
|
||||
<Field label="External Id">
|
||||
<Input
|
||||
value={externalId}
|
||||
@@ -237,9 +269,9 @@ export function VirtualMachinesPage() {
|
||||
onChange={(_, inputData) => setMetadataJson(inputData.value)}
|
||||
/>
|
||||
</Field>
|
||||
{selectedVirtualMachine && (
|
||||
{selectedTarget && (
|
||||
<Field label="Id">
|
||||
<div className={styles.value}>{selectedVirtualMachine.id}</div>
|
||||
<div className={styles.value}>{selectedTarget.id}</div>
|
||||
</Field>
|
||||
)}
|
||||
</DialogContent>
|
||||
@@ -256,20 +288,20 @@ export function VirtualMachinesPage() {
|
||||
</DialogSurface>
|
||||
</Dialog>
|
||||
|
||||
<Dialog open={Boolean(linkVirtualMachineId)} onOpenChange={(_, dialogData) => !dialogData.open && setLinkVirtualMachineId("")}>
|
||||
<Dialog open={Boolean(linkTargetId)} onOpenChange={(_, dialogData) => !dialogData.open && setLinkTargetId("")}>
|
||||
<DialogSurface>
|
||||
<DialogBody>
|
||||
<DialogTitle>Link Virtual Machine to Domain</DialogTitle>
|
||||
<DialogTitle>Link Target to Domain</DialogTitle>
|
||||
<DialogContent className={styles.form}>
|
||||
<Field label="Virtual Machine" required>
|
||||
<Field label="Target" required>
|
||||
<Combobox
|
||||
placeholder="Virtual Machine waehlen"
|
||||
value={(data ?? []).find((virtualMachine) => virtualMachine.id === linkVirtualMachineId)?.name ?? ""}
|
||||
onOptionSelect={(_, optionData) => setLinkVirtualMachineId(optionData.optionValue ?? "")}
|
||||
placeholder="Target waehlen"
|
||||
value={(data ?? []).find((target) => target.id === linkTargetId)?.name ?? ""}
|
||||
onOptionSelect={(_, optionData) => setLinkTargetId(optionData.optionValue ?? "")}
|
||||
>
|
||||
{unlinkedVirtualMachines.map((virtualMachine) => (
|
||||
<Option key={virtualMachine.id} text={virtualMachine.name} value={virtualMachine.id}>
|
||||
{virtualMachine.name}
|
||||
{unlinkedTargets.map((target) => (
|
||||
<Option key={target.id} text={target.name} value={target.id}>
|
||||
{target.name}
|
||||
</Option>
|
||||
))}
|
||||
</Combobox>
|
||||
@@ -289,13 +321,13 @@ export function VirtualMachinesPage() {
|
||||
</Field>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button appearance="secondary" onClick={() => setLinkVirtualMachineId("")}>
|
||||
<Button appearance="secondary" onClick={() => setLinkTargetId("")}>
|
||||
Abbrechen
|
||||
</Button>
|
||||
<Button
|
||||
appearance="primary"
|
||||
disabled={!linkVirtualMachineId || !linkDomainId || linkVirtualMachineToDomain.isPending}
|
||||
onClick={() => linkVirtualMachineToDomain.mutate({ virtualMachineId: linkVirtualMachineId, targetDomainId: linkDomainId })}
|
||||
disabled={!linkTargetId || !linkDomainId || linkTargetToDomain.isPending}
|
||||
onClick={() => linkTargetToDomain.mutate({ targetId: linkTargetId, targetDomainId: linkDomainId })}
|
||||
>
|
||||
Link
|
||||
</Button>
|
||||
@@ -304,33 +336,33 @@ export function VirtualMachinesPage() {
|
||||
</DialogSurface>
|
||||
</Dialog>
|
||||
|
||||
<Dialog open={Boolean(unlinkVirtualMachineId)} onOpenChange={(_, dialogData) => !dialogData.open && setUnlinkVirtualMachineId("")}>
|
||||
<Dialog open={Boolean(unlinkTargetId)} onOpenChange={(_, dialogData) => !dialogData.open && setUnlinkTargetId("")}>
|
||||
<DialogSurface>
|
||||
<DialogBody>
|
||||
<DialogTitle>Unlink Virtual Machine from Domain</DialogTitle>
|
||||
<DialogTitle>Unlink Target from Domain</DialogTitle>
|
||||
<DialogContent className={styles.form}>
|
||||
<Field label="Virtual Machine" required>
|
||||
<Field label="Target" required>
|
||||
<Combobox
|
||||
placeholder="Virtual Machine waehlen"
|
||||
value={(data ?? []).find((virtualMachine) => virtualMachine.id === unlinkVirtualMachineId)?.name ?? ""}
|
||||
onOptionSelect={(_, optionData) => setUnlinkVirtualMachineId(optionData.optionValue ?? "")}
|
||||
placeholder="Target waehlen"
|
||||
value={(data ?? []).find((target) => target.id === unlinkTargetId)?.name ?? ""}
|
||||
onOptionSelect={(_, optionData) => setUnlinkTargetId(optionData.optionValue ?? "")}
|
||||
>
|
||||
{linkedVirtualMachines.map((virtualMachine) => (
|
||||
<Option key={virtualMachine.id} text={virtualMachine.name} value={virtualMachine.id}>
|
||||
{virtualMachine.name}
|
||||
{linkedTargets.map((target) => (
|
||||
<Option key={target.id} text={target.name} value={target.id}>
|
||||
{target.name}
|
||||
</Option>
|
||||
))}
|
||||
</Combobox>
|
||||
</Field>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button appearance="secondary" onClick={() => setUnlinkVirtualMachineId("")}>
|
||||
<Button appearance="secondary" onClick={() => setUnlinkTargetId("")}>
|
||||
Abbrechen
|
||||
</Button>
|
||||
<Button
|
||||
appearance="primary"
|
||||
disabled={!unlinkVirtualMachineId || unlinkVirtualMachineFromDomain.isPending}
|
||||
onClick={() => unlinkVirtualMachineFromDomain.mutate(unlinkVirtualMachineId)}
|
||||
disabled={!unlinkTargetId || unlinkTargetFromDomain.isPending}
|
||||
onClick={() => unlinkTargetFromDomain.mutate(unlinkTargetId)}
|
||||
>
|
||||
Unlink
|
||||
</Button>
|
||||
@@ -341,13 +373,15 @@ export function VirtualMachinesPage() {
|
||||
|
||||
<DataState
|
||||
isLoading={isLoading || domainsLoading}
|
||||
error={error ?? domainsError ?? deleteVirtualMachine.error ?? linkVirtualMachineToDomain.error ?? unlinkVirtualMachineFromDomain.error}
|
||||
error={error ?? domainsError ?? deleteTarget.error ?? linkTargetToDomain.error ?? unlinkTargetFromDomain.error}
|
||||
/>
|
||||
{data && (
|
||||
<Table aria-label="Virtual Machines">
|
||||
<Table aria-label="Targets">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHeaderCell>Name</TableHeaderCell>
|
||||
<TableHeaderCell>Type</TableHeaderCell>
|
||||
<TableHeaderCell>Provider</TableHeaderCell>
|
||||
<TableHeaderCell>Domain</TableHeaderCell>
|
||||
<TableHeaderCell>External Id</TableHeaderCell>
|
||||
<TableHeaderCell>Id</TableHeaderCell>
|
||||
@@ -355,16 +389,18 @@ export function VirtualMachinesPage() {
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{data.map((virtualMachine) => (
|
||||
<TableRow key={virtualMachine.id}>
|
||||
<TableCell>{virtualMachine.name}</TableCell>
|
||||
<TableCell>{virtualMachine.domainID ? (domainNameById.get(virtualMachine.domainID) ?? virtualMachine.domainID) : "-"}</TableCell>
|
||||
<TableCell>{virtualMachine.externalId}</TableCell>
|
||||
<TableCell>{virtualMachine.id}</TableCell>
|
||||
{data.map((target) => (
|
||||
<TableRow key={target.id}>
|
||||
<TableCell>{target.name}</TableCell>
|
||||
<TableCell>{target.targetType}</TableCell>
|
||||
<TableCell>{target.providerType}</TableCell>
|
||||
<TableCell>{target.domainID ? (domainNameById.get(target.domainID) ?? target.domainID) : "-"}</TableCell>
|
||||
<TableCell>{target.externalId}</TableCell>
|
||||
<TableCell>{target.id}</TableCell>
|
||||
<TableCell>
|
||||
<div className={styles.actions}>
|
||||
<Tooltip content="Details" relationship="label">
|
||||
<Link to={`/virtual-machines/${virtualMachine.id}`}>
|
||||
<Link to={`/targets/${target.id}`}>
|
||||
<Button appearance="subtle" aria-label="Details" icon={<OpenRegular />} />
|
||||
</Link>
|
||||
</Tooltip>
|
||||
@@ -373,18 +409,18 @@ export function VirtualMachinesPage() {
|
||||
appearance="subtle"
|
||||
aria-label="Aendern"
|
||||
icon={<EditRegular />}
|
||||
onClick={() => openVirtualMachineDialog("edit", virtualMachine)}
|
||||
onClick={() => openTargetDialog("edit", target)}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip content="Loeschen" relationship="label">
|
||||
<Button
|
||||
appearance="subtle"
|
||||
aria-label="Loeschen"
|
||||
disabled={deleteVirtualMachine.isPending}
|
||||
disabled={deleteTarget.isPending}
|
||||
icon={<DeleteRegular />}
|
||||
onClick={() => {
|
||||
if (window.confirm(`Virtual Machine "${virtualMachine.name}" wirklich loeschen?`)) {
|
||||
deleteVirtualMachine.mutate(virtualMachine.id);
|
||||
if (window.confirm(`Target "${target.name}" wirklich loeschen?`)) {
|
||||
deleteTarget.mutate(target.id);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
@@ -399,3 +435,4 @@ export function VirtualMachinesPage() {
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
@@ -390,3 +390,4 @@ export function TemplateCategoriesPage() {
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
Button,
|
||||
Combobox,
|
||||
@@ -582,3 +582,4 @@ export function TemplatesPage() {
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,22 +1,29 @@
|
||||
export type DeploymentExecution = {
|
||||
export type DeploymentExecution = {
|
||||
id: string;
|
||||
deploymentGroupId?: string;
|
||||
deploymentBatchId?: string;
|
||||
virtualMachineId?: string;
|
||||
targetId?: string;
|
||||
status?: string;
|
||||
jsonData?: string;
|
||||
created?: string;
|
||||
modified?: string;
|
||||
target?: {
|
||||
id: string;
|
||||
name?: string;
|
||||
domainID?: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type AddDeploymentExecution = {
|
||||
deploymentBatchId: string;
|
||||
virtualMachineId: string;
|
||||
targetId: string;
|
||||
status: string;
|
||||
jsonData: string;
|
||||
};
|
||||
|
||||
export type AddDeploymentRequest = {
|
||||
deploymentBatchId: string;
|
||||
virtualMachineIds: string[];
|
||||
targetIds: string[];
|
||||
jsonData: string;
|
||||
};
|
||||
|
||||
@@ -36,7 +43,7 @@ export type DeploymentJob = {
|
||||
|
||||
export type DeploymentJobTarget = {
|
||||
id: string;
|
||||
virtualMachineId: string;
|
||||
targetId: string;
|
||||
deploymentBatchId: string;
|
||||
templateId: string;
|
||||
status: string;
|
||||
@@ -73,17 +80,117 @@ export type DeploymentJobDetails = {
|
||||
|
||||
export type DeploymentBatch = {
|
||||
id: string;
|
||||
templateId?: string;
|
||||
deploymentRuleId?: string;
|
||||
status?: string;
|
||||
created?: string;
|
||||
createdBy?: string;
|
||||
modified?: string;
|
||||
modifiedBy?: string;
|
||||
};
|
||||
|
||||
export type DeploymentBatchDetails = DeploymentBatch & {
|
||||
deployments?: DeploymentExecution[];
|
||||
templateSelections?: DeploymentTemplateSelection[];
|
||||
parameterValues?: DeploymentParameterValue[];
|
||||
targetAssignments?: DeploymentTargetAssignment[];
|
||||
};
|
||||
|
||||
export type AddDeploymentBatch = {
|
||||
templateId: string;
|
||||
deploymentRuleId?: string;
|
||||
status: string;
|
||||
virtualMachineIds?: string[];
|
||||
targetIds?: string[];
|
||||
};
|
||||
|
||||
export type DeploymentRuleStep = {
|
||||
id?: string;
|
||||
sortOrder: number;
|
||||
name: string;
|
||||
stepType: string;
|
||||
requiresApproval: boolean;
|
||||
metadataJson?: string;
|
||||
};
|
||||
|
||||
export type DeploymentRule = {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
isActive: boolean;
|
||||
created?: string;
|
||||
modified?: string;
|
||||
steps: DeploymentRuleStep[];
|
||||
};
|
||||
|
||||
export type TemplateVersion = {
|
||||
id: string;
|
||||
templateId: string;
|
||||
version: string;
|
||||
jsonData: string;
|
||||
jsonHash: string;
|
||||
schemaVersion: string;
|
||||
isPublished: boolean;
|
||||
publishedAt?: string;
|
||||
publishedBy?: string;
|
||||
};
|
||||
|
||||
export type DeploymentTemplateSelection = {
|
||||
id: string;
|
||||
deploymentGroupId: string;
|
||||
templateVersionId: string;
|
||||
templateRole: string;
|
||||
sortOrder: number;
|
||||
alias?: string;
|
||||
templateVersion?: TemplateVersion;
|
||||
};
|
||||
|
||||
export type AddDeploymentTemplateSelection = {
|
||||
templateVersionId: string;
|
||||
templateRole: string;
|
||||
sortOrder?: number;
|
||||
alias?: string;
|
||||
};
|
||||
|
||||
export type DeploymentParameterValue = {
|
||||
id: string;
|
||||
deploymentGroupId: string;
|
||||
deploymentTemplateSelectionId?: string;
|
||||
name: string;
|
||||
valueJson: string;
|
||||
isSecretReference: boolean;
|
||||
isOverride: boolean;
|
||||
};
|
||||
|
||||
export type AddDeploymentParameterValue = {
|
||||
deploymentTemplateSelectionId?: string;
|
||||
name: string;
|
||||
valueJson: string;
|
||||
isSecretReference: boolean;
|
||||
isOverride: boolean;
|
||||
};
|
||||
|
||||
export type DeploymentTargetAssignment = {
|
||||
id: string;
|
||||
deploymentGroupId: string;
|
||||
targetId: string;
|
||||
roleKey: string;
|
||||
sortOrder: number;
|
||||
nodeDataJson: string;
|
||||
target?: Target;
|
||||
};
|
||||
|
||||
export type AddDeploymentTargetAssignment = {
|
||||
targetId: string;
|
||||
roleKey: string;
|
||||
sortOrder?: number;
|
||||
nodeDataJson: string;
|
||||
};
|
||||
|
||||
export type AddDeploymentRule = {
|
||||
name: string;
|
||||
description?: string;
|
||||
isActive: boolean;
|
||||
steps: DeploymentRuleStep[];
|
||||
};
|
||||
|
||||
export type Domain = {
|
||||
@@ -99,8 +206,8 @@ export type DomainWithEnvironments = Domain & {
|
||||
}>;
|
||||
};
|
||||
|
||||
export type DomainWithVirtualMachines = Domain & {
|
||||
virtualMachines?: VirtualMachine[];
|
||||
export type DomainWithTargets = Domain & {
|
||||
targets?: Target[];
|
||||
};
|
||||
|
||||
export type AddDomain = {
|
||||
@@ -113,6 +220,7 @@ export type EnvironmentItem = {
|
||||
id: string;
|
||||
name: string;
|
||||
environmentType?: string;
|
||||
hostingType?: string;
|
||||
providerType?: string;
|
||||
tenantId?: string;
|
||||
subscriptionId?: string;
|
||||
@@ -128,6 +236,7 @@ export type EnvironmentWithDomains = EnvironmentItem & {
|
||||
export type AddEnvironment = {
|
||||
name: string;
|
||||
environmentType: string;
|
||||
hostingType: string;
|
||||
providerType?: string;
|
||||
tenantId?: string;
|
||||
subscriptionId?: string;
|
||||
@@ -209,17 +318,22 @@ export type AddTemplateCategory = {
|
||||
color?: string;
|
||||
};
|
||||
|
||||
export type VirtualMachine = {
|
||||
export type Target = {
|
||||
id: string;
|
||||
domainID?: string;
|
||||
name: string;
|
||||
targetType: string;
|
||||
providerType: string;
|
||||
externalId?: string;
|
||||
metadataJson?: string;
|
||||
};
|
||||
|
||||
export type AddVirtualMachine = {
|
||||
export type AddTarget = {
|
||||
domainID?: string;
|
||||
name: string;
|
||||
targetType: string;
|
||||
providerType: string;
|
||||
externalId?: string;
|
||||
metadataJson?: string;
|
||||
};
|
||||
|
||||
|
||||
3
src/vite-env.d.ts
vendored
3
src/vite-env.d.ts
vendored
@@ -1 +1,2 @@
|
||||
/// <reference types="vite/client" />
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
|
||||
Reference in New Issue
Block a user