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 {
|
export class ApiError extends Error {
|
||||||
constructor(
|
constructor(
|
||||||
@@ -116,3 +116,4 @@ export async function deleteJson<TResult = unknown>(path: string): Promise<TResu
|
|||||||
return text as TResult;
|
return text as TResult;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
import { deleteJson, getJson, postJson, putJson } from "./httpClient";
|
import { deleteJson, getJson, postJson, putJson } from "./httpClient";
|
||||||
import type {
|
import type {
|
||||||
AddDeploymentExecution,
|
AddDeploymentExecution,
|
||||||
|
AddDeploymentParameterValue,
|
||||||
AddDeploymentRequest,
|
AddDeploymentRequest,
|
||||||
AddDeploymentBatch,
|
AddDeploymentBatch,
|
||||||
|
AddDeploymentTargetAssignment,
|
||||||
|
AddDeploymentTemplateSelection,
|
||||||
AddDomain,
|
AddDomain,
|
||||||
AddEnvironment,
|
AddEnvironment,
|
||||||
AddRunbook,
|
AddRunbook,
|
||||||
@@ -15,7 +18,7 @@ import type {
|
|||||||
DeploymentBatchDetails,
|
DeploymentBatchDetails,
|
||||||
Domain,
|
Domain,
|
||||||
DomainWithEnvironments,
|
DomainWithEnvironments,
|
||||||
DomainWithVirtualMachines,
|
DomainWithTargets,
|
||||||
EnvironmentItem,
|
EnvironmentItem,
|
||||||
EnvironmentWithDomains,
|
EnvironmentWithDomains,
|
||||||
Runbook,
|
Runbook,
|
||||||
@@ -23,10 +26,13 @@ import type {
|
|||||||
ServiceRoleDefinition,
|
ServiceRoleDefinition,
|
||||||
TemplateCategory,
|
TemplateCategory,
|
||||||
Template,
|
Template,
|
||||||
VirtualMachine,
|
TemplateVersion,
|
||||||
AddVirtualMachine,
|
Target,
|
||||||
|
AddTarget,
|
||||||
DeploymentJob,
|
DeploymentJob,
|
||||||
DeploymentJobDetails,
|
DeploymentJobDetails,
|
||||||
|
DeploymentRule,
|
||||||
|
AddDeploymentRule,
|
||||||
} from "../types/portal";
|
} from "../types/portal";
|
||||||
|
|
||||||
export const portalApi = {
|
export const portalApi = {
|
||||||
@@ -41,17 +47,38 @@ export const portalApi = {
|
|||||||
postJson<{ comment?: string }, void>(`/Deployment/QueueJobs/Steps/${stepId}/Approve`, { comment }),
|
postJson<{ comment?: string }, void>(`/Deployment/QueueJobs/Steps/${stepId}/Approve`, { comment }),
|
||||||
rejectDeploymentJobStep: (stepId: string, comment?: string) =>
|
rejectDeploymentJobStep: (stepId: string, comment?: string) =>
|
||||||
postJson<{ comment?: string }, void>(`/Deployment/QueueJobs/Steps/${stepId}/Reject`, { comment }),
|
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) =>
|
getDeploymentBatchById: (deploymentBatchId: string, signal?: AbortSignal) =>
|
||||||
getJson<DeploymentBatchDetails>(`/DeploymentGroup/${deploymentBatchId}`, signal),
|
getJson<DeploymentBatchDetails>(`/deployment-batches/${deploymentBatchId}`, signal),
|
||||||
addDeploymentBatch: (deploymentBatch: AddDeploymentBatch) => postJson<AddDeploymentBatch, string>("/DeploymentGroup", deploymentBatch),
|
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),
|
getDomains: (signal?: AbortSignal) => getJson<Domain[]>("/Domain", signal),
|
||||||
getDomainEnvironments: (domainId: string, signal?: AbortSignal) => getJson<DomainWithEnvironments>(`/Domain/${domainId}/Environments`, 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),
|
addDomain: (domain: AddDomain) => postJson<AddDomain, string>("/Domain", domain),
|
||||||
updateDomain: (domainId: string, domain: AddDomain) => putJson<AddDomain, void>(`/Domain/${domainId}`, domain),
|
updateDomain: (domainId: string, domain: AddDomain) => putJson<AddDomain, void>(`/Domain/${domainId}`, domain),
|
||||||
deleteDomain: (domainId: string) => deleteJson<void>(`/Domain/${domainId}`),
|
deleteDomain: (domainId: string) => deleteJson<void>(`/Domain/${domainId}`),
|
||||||
linkDomainToEnvironment: (domainId: string, environmentId: string) => postJson<undefined, string>(`/Domain/${domainId}/Environment/${environmentId}`, undefined),
|
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),
|
getEnvironments: (signal?: AbortSignal) => getJson<EnvironmentItem[]>("/Environment", signal),
|
||||||
getEnvironmentDomains: (environmentId: string, signal?: AbortSignal) => getJson<EnvironmentWithDomains>(`/Environment/${environmentId}/Domains`, signal),
|
getEnvironmentDomains: (environmentId: string, signal?: AbortSignal) => getJson<EnvironmentWithDomains>(`/Environment/${environmentId}/Domains`, signal),
|
||||||
addEnvironment: (environment: AddEnvironment) => postJson<AddEnvironment, string>("/Environment", environment),
|
addEnvironment: (environment: AddEnvironment) => postJson<AddEnvironment, string>("/Environment", environment),
|
||||||
@@ -60,6 +87,7 @@ export const portalApi = {
|
|||||||
getRunbooks: (signal?: AbortSignal) => getJson<Runbook[]>("/Runbook", signal),
|
getRunbooks: (signal?: AbortSignal) => getJson<Runbook[]>("/Runbook", signal),
|
||||||
addRunbook: (runbook: AddRunbook) => postJson<AddRunbook, string>("/Runbook", runbook),
|
addRunbook: (runbook: AddRunbook) => postJson<AddRunbook, string>("/Runbook", runbook),
|
||||||
getTemplates: (signal?: AbortSignal) => getJson<Template[]>("/Template", signal),
|
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),
|
addTemplate: (template: AddTemplate) => postJson<AddTemplate, string>("/Template", template),
|
||||||
updateTemplate: (templateId: string, template: AddTemplate) => putJson<AddTemplate, void>(`/Template/${templateId}`, template),
|
updateTemplate: (templateId: string, template: AddTemplate) => putJson<AddTemplate, void>(`/Template/${templateId}`, template),
|
||||||
deleteTemplate: (templateId: string) => deleteJson<void>(`/Template/${templateId}`),
|
deleteTemplate: (templateId: string) => deleteJson<void>(`/Template/${templateId}`),
|
||||||
@@ -80,13 +108,14 @@ export const portalApi = {
|
|||||||
updateTemplateCategory: (templateCategoryId: string, templateCategory: AddTemplateCategory) =>
|
updateTemplateCategory: (templateCategoryId: string, templateCategory: AddTemplateCategory) =>
|
||||||
putJson<AddTemplateCategory, void>(`/TemplateCategory/${templateCategoryId}`, templateCategory),
|
putJson<AddTemplateCategory, void>(`/TemplateCategory/${templateCategoryId}`, templateCategory),
|
||||||
deleteTemplateCategory: (templateCategoryId: string) => deleteJson<void>(`/TemplateCategory/${templateCategoryId}`),
|
deleteTemplateCategory: (templateCategoryId: string) => deleteJson<void>(`/TemplateCategory/${templateCategoryId}`),
|
||||||
getVirtualMachines: (signal?: AbortSignal) => getJson<VirtualMachine[]>("/VirtualMachine", signal),
|
getTargets: (signal?: AbortSignal) => getJson<Target[]>("/Target", signal),
|
||||||
getVirtualMachineById: (virtualMachineId: string, signal?: AbortSignal) => getJson<VirtualMachine>(`/VirtualMachine/${virtualMachineId}`, signal),
|
getTargetById: (targetId: string, signal?: AbortSignal) => getJson<Target>(`/Target/${targetId}`, signal),
|
||||||
addVirtualMachine: (virtualMachine: AddVirtualMachine) => postJson<AddVirtualMachine, string>("/VirtualMachine", virtualMachine),
|
addTarget: (target: AddTarget) => postJson<AddTarget, string>("/Target", target),
|
||||||
updateVirtualMachine: (virtualMachineId: string, virtualMachine: AddVirtualMachine) =>
|
updateTarget: (targetId: string, target: AddTarget) =>
|
||||||
putJson<AddVirtualMachine, void>(`/VirtualMachine/${virtualMachineId}`, virtualMachine),
|
putJson<AddTarget, void>(`/Target/${targetId}`, target),
|
||||||
deleteVirtualMachine: (virtualMachineId: string) => deleteJson<void>(`/VirtualMachine/${virtualMachineId}`),
|
deleteTarget: (targetId: string) => deleteJson<void>(`/Target/${targetId}`),
|
||||||
linkVirtualMachineToDomain: (virtualMachineId: string, domainId: string) =>
|
linkTargetToDomain: (targetId: string, domainId: string) =>
|
||||||
postJson<undefined, void>(`/VirtualMachine/${virtualMachineId}/Domain/${domainId}`, undefined),
|
postJson<undefined, void>(`/Target/${targetId}/Domain/${domainId}`, undefined),
|
||||||
unlinkVirtualMachineFromDomain: (virtualMachineId: string) => deleteJson<void>(`/VirtualMachine/${virtualMachineId}/Domain`),
|
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 = {
|
type DataStateProps = {
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
@@ -21,3 +21,4 @@ export function DataState({ isLoading, error }: DataStateProps) {
|
|||||||
|
|
||||||
return null;
|
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";
|
import type { PropsWithChildren } from "react";
|
||||||
|
|
||||||
const useStyles = makeStyles({
|
const useStyles = makeStyles({
|
||||||
@@ -58,3 +58,4 @@ export function FormActions({ children }: PropsWithChildren) {
|
|||||||
|
|
||||||
return <div className={styles.actions}>{children}</div>;
|
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({
|
const useStyles = makeStyles({
|
||||||
root: {
|
root: {
|
||||||
@@ -23,3 +23,4 @@ export function PageHeader({ title, description }: PageHeaderProps) {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Outlet, NavLink } from "react-router-dom";
|
import { Outlet, NavLink } from "react-router-dom";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
makeStyles,
|
makeStyles,
|
||||||
@@ -71,6 +71,7 @@ const useStyles = makeStyles({
|
|||||||
const links = [
|
const links = [
|
||||||
{ to: "/", label: "Dashboard", icon: <Home24Regular /> },
|
{ to: "/", label: "Dashboard", icon: <Home24Regular /> },
|
||||||
{ to: "/deployments", label: "Deployments", icon: <BoxMultiple24Regular /> },
|
{ to: "/deployments", label: "Deployments", icon: <BoxMultiple24Regular /> },
|
||||||
|
{ to: "/deployment-rules", label: "Deployment Rules", icon: <AppsListDetail24Regular /> },
|
||||||
{ to: "/worker-jobs", label: "Worker Jobs", icon: <AppsListDetail24Regular /> },
|
{ to: "/worker-jobs", label: "Worker Jobs", icon: <AppsListDetail24Regular /> },
|
||||||
{ to: "/domains", label: "Domains", icon: <Globe24Regular /> },
|
{ to: "/domains", label: "Domains", icon: <Globe24Regular /> },
|
||||||
{ to: "/environments", label: "Environments", icon: <DatabasePlugConnectedRegular /> },
|
{ to: "/environments", label: "Environments", icon: <DatabasePlugConnectedRegular /> },
|
||||||
@@ -78,7 +79,7 @@ const links = [
|
|||||||
{ to: "/templates", label: "Templates", icon: <BoxMultiple24Regular /> },
|
{ to: "/templates", label: "Templates", icon: <BoxMultiple24Regular /> },
|
||||||
{ to: "/template-categories", label: "Template Categories", icon: <TagMultiple24Regular /> },
|
{ to: "/template-categories", label: "Template Categories", icon: <TagMultiple24Regular /> },
|
||||||
{ to: "/services", label: "Services", icon: <AppsListDetail24Regular /> },
|
{ to: "/services", label: "Services", icon: <AppsListDetail24Regular /> },
|
||||||
{ to: "/virtual-machines", label: "Virtual Machines", icon: <Desktop24Regular /> },
|
{ to: "/targets", label: "Targets", icon: <Desktop24Regular /> },
|
||||||
];
|
];
|
||||||
|
|
||||||
export function AppShell() {
|
export function AppShell() {
|
||||||
@@ -119,3 +120,4 @@ export function AppShell() {
|
|||||||
</div>
|
</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 ReactDOM from "react-dom/client";
|
||||||
import { FluentProvider, webLightTheme } from "@fluentui/react-components";
|
import { FluentProvider, webLightTheme } from "@fluentui/react-components";
|
||||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
@@ -9,6 +9,7 @@ import { DeploymentBatchDetailsPage } from "./pages/DeploymentBatchDetailsPage";
|
|||||||
import { DeploymentJobDetailsPage } from "./pages/DeploymentJobDetailsPage";
|
import { DeploymentJobDetailsPage } from "./pages/DeploymentJobDetailsPage";
|
||||||
import { DeploymentJobsPage } from "./pages/DeploymentJobsPage";
|
import { DeploymentJobsPage } from "./pages/DeploymentJobsPage";
|
||||||
import { DeploymentGroupsPage } from "./pages/DeploymentGroupsPage";
|
import { DeploymentGroupsPage } from "./pages/DeploymentGroupsPage";
|
||||||
|
import { DeploymentRulesPage } from "./pages/DeploymentRulesPage";
|
||||||
import { DomainDetailsPage } from "./pages/DomainDetailsPage";
|
import { DomainDetailsPage } from "./pages/DomainDetailsPage";
|
||||||
import { DomainsPage } from "./pages/DomainsPage";
|
import { DomainsPage } from "./pages/DomainsPage";
|
||||||
import { EnvironmentDetailsPage } from "./pages/EnvironmentDetailsPage";
|
import { EnvironmentDetailsPage } from "./pages/EnvironmentDetailsPage";
|
||||||
@@ -16,8 +17,8 @@ import { EnvironmentsPage } from "./pages/EnvironmentsPage";
|
|||||||
import { TemplatesPage } from "./pages/TemplatesPage";
|
import { TemplatesPage } from "./pages/TemplatesPage";
|
||||||
import { ServicesPage } from "./pages/ServicesPage";
|
import { ServicesPage } from "./pages/ServicesPage";
|
||||||
import { TemplateCategoriesPage } from "./pages/TemplateCategoriesPage";
|
import { TemplateCategoriesPage } from "./pages/TemplateCategoriesPage";
|
||||||
import { VirtualMachineDetailsPage } from "./pages/VirtualMachineDetailsPage";
|
import { TargetDetailsPage } from "./pages/TargetDetailsPage";
|
||||||
import { VirtualMachinesPage } from "./pages/VirtualMachinesPage";
|
import { TargetsPage } from "./pages/TargetsPage";
|
||||||
import "./styles/global.css";
|
import "./styles/global.css";
|
||||||
|
|
||||||
const queryClient = new QueryClient({
|
const queryClient = new QueryClient({
|
||||||
@@ -36,6 +37,7 @@ const router = createBrowserRouter([
|
|||||||
children: [
|
children: [
|
||||||
{ index: true, element: <DashboardPage /> },
|
{ index: true, element: <DashboardPage /> },
|
||||||
{ path: "deployments", element: <DeploymentGroupsPage /> },
|
{ path: "deployments", element: <DeploymentGroupsPage /> },
|
||||||
|
{ path: "deployment-rules", element: <DeploymentRulesPage /> },
|
||||||
{ path: "deployments/:id", element: <DeploymentBatchDetailsPage /> },
|
{ path: "deployments/:id", element: <DeploymentBatchDetailsPage /> },
|
||||||
{ path: "worker-jobs", element: <DeploymentJobsPage /> },
|
{ path: "worker-jobs", element: <DeploymentJobsPage /> },
|
||||||
{ path: "worker-jobs/:id", element: <DeploymentJobDetailsPage /> },
|
{ path: "worker-jobs/:id", element: <DeploymentJobDetailsPage /> },
|
||||||
@@ -46,8 +48,8 @@ const router = createBrowserRouter([
|
|||||||
{ path: "templates", element: <TemplatesPage /> },
|
{ path: "templates", element: <TemplatesPage /> },
|
||||||
{ path: "template-categories", element: <TemplateCategoriesPage /> },
|
{ path: "template-categories", element: <TemplateCategoriesPage /> },
|
||||||
{ path: "services", element: <ServicesPage /> },
|
{ path: "services", element: <ServicesPage /> },
|
||||||
{ path: "virtual-machines", element: <VirtualMachinesPage /> },
|
{ path: "targets", element: <TargetsPage /> },
|
||||||
{ path: "virtual-machines/:id", element: <VirtualMachineDetailsPage /> },
|
{ path: "targets/:id", element: <TargetDetailsPage /> },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
@@ -61,3 +63,4 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
|
|||||||
</FluentProvider>
|
</FluentProvider>
|
||||||
</React.StrictMode>,
|
</React.StrictMode>,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardHeader,
|
CardHeader,
|
||||||
@@ -78,9 +78,9 @@ export function DashboardPage() {
|
|||||||
queryKey: ["queue-jobs"],
|
queryKey: ["queue-jobs"],
|
||||||
queryFn: ({ signal }) => portalApi.getDeploymentJobs(signal),
|
queryFn: ({ signal }) => portalApi.getDeploymentJobs(signal),
|
||||||
});
|
});
|
||||||
const virtualMachines = useQuery({
|
const targets = useQuery({
|
||||||
queryKey: ["virtual-machines"],
|
queryKey: ["targets"],
|
||||||
queryFn: ({ signal }) => portalApi.getVirtualMachines(signal),
|
queryFn: ({ signal }) => portalApi.getTargets(signal),
|
||||||
});
|
});
|
||||||
|
|
||||||
const error =
|
const error =
|
||||||
@@ -89,14 +89,14 @@ export function DashboardPage() {
|
|||||||
templates.error ??
|
templates.error ??
|
||||||
services.error ??
|
services.error ??
|
||||||
queueJobs.error ??
|
queueJobs.error ??
|
||||||
virtualMachines.error;
|
targets.error;
|
||||||
const isLoading =
|
const isLoading =
|
||||||
domains.isLoading ||
|
domains.isLoading ||
|
||||||
environments.isLoading ||
|
environments.isLoading ||
|
||||||
templates.isLoading ||
|
templates.isLoading ||
|
||||||
services.isLoading ||
|
services.isLoading ||
|
||||||
queueJobs.isLoading ||
|
queueJobs.isLoading ||
|
||||||
virtualMachines.isLoading;
|
targets.isLoading;
|
||||||
|
|
||||||
const queueByStatus = (queueJobs.data ?? []).reduce<Record<string, number>>((acc, job) => {
|
const queueByStatus = (queueJobs.data ?? []).reduce<Record<string, number>>((acc, job) => {
|
||||||
const key = job.status || "Unknown";
|
const key = job.status || "Unknown";
|
||||||
@@ -104,7 +104,7 @@ export function DashboardPage() {
|
|||||||
return acc;
|
return acc;
|
||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
const vmCompliance = (virtualMachines.data ?? []).reduce(
|
const vmCompliance = (targets.data ?? []).reduce(
|
||||||
(acc, vm) => {
|
(acc, vm) => {
|
||||||
const compliance = getVmCompliance(vm.metadataJson);
|
const compliance = getVmCompliance(vm.metadataJson);
|
||||||
if (compliance === true) acc.compliant += 1;
|
if (compliance === true) acc.compliant += 1;
|
||||||
@@ -222,3 +222,4 @@ function getVmCompliance(metadataJson?: string): boolean | undefined {
|
|||||||
|
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
|
Combobox,
|
||||||
Field,
|
Field,
|
||||||
|
Input,
|
||||||
|
makeStyles,
|
||||||
|
Option,
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
TableCell,
|
TableCell,
|
||||||
@@ -10,109 +14,458 @@ import {
|
|||||||
TableHeaderCell,
|
TableHeaderCell,
|
||||||
TableRow,
|
TableRow,
|
||||||
Textarea,
|
Textarea,
|
||||||
|
Tooltip,
|
||||||
} from "@fluentui/react-components";
|
} from "@fluentui/react-components";
|
||||||
import { ArrowLeftRegular } from "@fluentui/react-icons";
|
import { AddRegular, ArrowLeftRegular, DeleteRegular } from "@fluentui/react-icons";
|
||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import { Link, useParams } from "react-router-dom";
|
import { Link, useParams } from "react-router-dom";
|
||||||
import { portalApi } from "../api/portalApi";
|
import { portalApi } from "../api/portalApi";
|
||||||
import { DataState } from "../components/DataState";
|
import { DataState } from "../components/DataState";
|
||||||
import { FormActions, FormGrid, FormSection, FormWide } from "../components/FormSection";
|
|
||||||
import { PageHeader } from "../components/PageHeader";
|
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() {
|
export function DeploymentBatchDetailsPage() {
|
||||||
|
const styles = useStyles();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const { id } = useParams();
|
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({
|
const { data: deploymentBatch, error, isLoading } = useQuery({
|
||||||
queryKey: ["deployment-batches", id],
|
queryKey: ["deployment-batches", id],
|
||||||
queryFn: ({ signal }) => portalApi.getDeploymentBatchById(id!, signal),
|
queryFn: ({ signal }) => portalApi.getDeploymentBatchById(id!, signal),
|
||||||
enabled: Boolean(id),
|
enabled: Boolean(id),
|
||||||
});
|
});
|
||||||
const { data: virtualMachines } = useQuery({
|
const { data: allDeployments } = useQuery({
|
||||||
queryKey: ["virtual-machines"],
|
queryKey: ["deployments"],
|
||||||
queryFn: ({ signal }) => portalApi.getVirtualMachines(signal),
|
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({
|
const invalidateBatch = async () => {
|
||||||
mutationFn: portalApi.addDeploymentRequest,
|
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 () => {
|
onSuccess: async () => {
|
||||||
setSelectedVirtualMachineIds([]);
|
setTemplateId("");
|
||||||
setJsonData("{}");
|
setTemplateVersionId("");
|
||||||
await queryClient.invalidateQueries({ queryKey: ["deployments"] });
|
setTemplateRole("Service");
|
||||||
await queryClient.invalidateQueries({ queryKey: ["deployment-batches", id] });
|
setTemplateAlias("");
|
||||||
await queryClient.invalidateQueries({ queryKey: ["deployment-jobs"] });
|
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 (
|
return (
|
||||||
<>
|
<div className={styles.layout}>
|
||||||
<Link to="/deployments">
|
<Link to="/deployments">
|
||||||
<Button appearance="subtle" icon={<ArrowLeftRegular />}>
|
<Button appearance="subtle" icon={<ArrowLeftRegular />}>
|
||||||
Deployments
|
Deployments
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
<PageHeader title={id ? `Deployment Batch ${id}` : "Deployment Batch"} description="Executions und Start neuer Deployments fuer diesen Batch." />
|
<PageHeader title={id ? `Deployment Batch ${id}` : "Deployment Batch"} description="Composition, Targets und Executions fuer diesen Batch." />
|
||||||
<FormSection
|
<DataState
|
||||||
onSubmit={(event) => {
|
isLoading={isLoading}
|
||||||
event.preventDefault();
|
error={error ?? templateSelectionError ?? parameterError ?? targetError}
|
||||||
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>
|
|
||||||
|
|
||||||
<DataState isLoading={isLoading} error={error} />
|
<section className={styles.section}>
|
||||||
<Table aria-label="Deployment executions">
|
<div className={styles.sectionHeader}>
|
||||||
<TableHeader>
|
<div className={styles.sectionTitle}>Template Selections</div>
|
||||||
<TableRow>
|
<Button
|
||||||
<TableHeaderCell>Status</TableHeaderCell>
|
appearance="primary"
|
||||||
<TableHeaderCell>Virtual Machine</TableHeaderCell>
|
icon={<AddRegular />}
|
||||||
<TableHeaderCell>Execution Id</TableHeaderCell>
|
disabled={!id || !templateVersionId || addTemplateSelection.isPending}
|
||||||
</TableRow>
|
onClick={() => addTemplateSelection.mutate()}
|
||||||
</TableHeader>
|
>
|
||||||
<TableBody>
|
Add
|
||||||
{filteredExecutions.map((execution) => (
|
</Button>
|
||||||
<TableRow key={execution.id}>
|
</div>
|
||||||
<TableCell>{execution.status ?? "Unknown"}</TableCell>
|
<div className={styles.formGrid}>
|
||||||
<TableCell>{execution.virtualMachineId ?? "-"}</TableCell>
|
<Field label="Template">
|
||||||
<TableCell>{execution.id}</TableCell>
|
<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>
|
</TableRow>
|
||||||
))}
|
</TableHeader>
|
||||||
</TableBody>
|
<TableBody>
|
||||||
</Table>
|
{(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 {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
Combobox,
|
Combobox,
|
||||||
|
Dialog,
|
||||||
|
DialogActions,
|
||||||
|
DialogBody,
|
||||||
|
DialogContent,
|
||||||
|
DialogSurface,
|
||||||
|
DialogTitle,
|
||||||
Field,
|
Field,
|
||||||
|
Input,
|
||||||
makeStyles,
|
makeStyles,
|
||||||
Option,
|
Option,
|
||||||
Table,
|
Table,
|
||||||
@@ -14,12 +21,11 @@ import {
|
|||||||
TableRow,
|
TableRow,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
} from "@fluentui/react-components";
|
} from "@fluentui/react-components";
|
||||||
import { OpenRegular } from "@fluentui/react-icons";
|
import { AddRegular, DeleteRegular, OpenRegular } from "@fluentui/react-icons";
|
||||||
import { useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { portalApi } from "../api/portalApi";
|
import { portalApi } from "../api/portalApi";
|
||||||
import { DataState } from "../components/DataState";
|
import { DataState } from "../components/DataState";
|
||||||
import { FormActions, FormGrid, FormSection } from "../components/FormSection";
|
|
||||||
import { PageHeader } from "../components/PageHeader";
|
import { PageHeader } from "../components/PageHeader";
|
||||||
|
|
||||||
const useStyles = makeStyles({
|
const useStyles = makeStyles({
|
||||||
@@ -34,8 +40,10 @@ export function DeploymentGroupsPage() {
|
|||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const [serviceId, setServiceId] = useState("");
|
const [serviceId, setServiceId] = useState("");
|
||||||
const [templateId, setTemplateId] = useState("");
|
const [templateId, setTemplateId] = useState("");
|
||||||
const [virtualMachineIds, setVirtualMachineIds] = useState<string[]>([]);
|
const [deploymentRuleId, setDeploymentRuleId] = useState("");
|
||||||
const [status, setStatus] = useState("New");
|
const [targetIds, setTargetIds] = useState<string[]>([]);
|
||||||
|
const [targetSearch, setTargetSearch] = useState("");
|
||||||
|
const [dialogOpen, setDialogOpen] = useState(false);
|
||||||
const { data, error, isLoading } = useQuery({
|
const { data, error, isLoading } = useQuery({
|
||||||
queryKey: ["deployment-batches"],
|
queryKey: ["deployment-batches"],
|
||||||
queryFn: ({ signal }) => portalApi.getDeploymentBatches(signal),
|
queryFn: ({ signal }) => portalApi.getDeploymentBatches(signal),
|
||||||
@@ -52,17 +60,29 @@ export function DeploymentGroupsPage() {
|
|||||||
queryKey: ["template-categories"],
|
queryKey: ["template-categories"],
|
||||||
queryFn: ({ signal }) => portalApi.getTemplateCategories(signal),
|
queryFn: ({ signal }) => portalApi.getTemplateCategories(signal),
|
||||||
});
|
});
|
||||||
const { data: virtualMachines } = useQuery({
|
const { data: targets } = useQuery({
|
||||||
queryKey: ["virtual-machines"],
|
queryKey: ["targets"],
|
||||||
queryFn: ({ signal }) => portalApi.getVirtualMachines(signal),
|
queryFn: ({ signal }) => portalApi.getTargets(signal),
|
||||||
|
});
|
||||||
|
const { data: deploymentRules } = useQuery({
|
||||||
|
queryKey: ["deployment-rules"],
|
||||||
|
queryFn: ({ signal }) => portalApi.getDeploymentRules(signal),
|
||||||
});
|
});
|
||||||
const addDeploymentBatch = useMutation({
|
const addDeploymentBatch = useMutation({
|
||||||
mutationFn: portalApi.addDeploymentBatch,
|
mutationFn: portalApi.addDeploymentBatch,
|
||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
setServiceId("");
|
setServiceId("");
|
||||||
setTemplateId("");
|
setTemplateId("");
|
||||||
setVirtualMachineIds([]);
|
setDeploymentRuleId("");
|
||||||
setStatus("New");
|
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: ["deployment-batches"] });
|
||||||
await queryClient.invalidateQueries({ queryKey: ["deployments"] });
|
await queryClient.invalidateQueries({ queryKey: ["deployments"] });
|
||||||
},
|
},
|
||||||
@@ -70,104 +90,39 @@ export function DeploymentGroupsPage() {
|
|||||||
|
|
||||||
const selectedService = (services ?? []).find((service) => service.id === serviceId);
|
const selectedService = (services ?? []).find((service) => service.id === serviceId);
|
||||||
const isOnPremService = selectedService ? !selectedService.isCloudService : false;
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageHeader title="Deployments" description="Deployment Batches und deren Ausfuehrungen." />
|
<PageHeader title="Deployments" description="Deployment Batches und deren Ausfuehrungen." />
|
||||||
<FormSection
|
<Button appearance="primary" icon={<AddRegular />} onClick={() => setDialogOpen(true)}>
|
||||||
onSubmit={(event) => {
|
Neues Deployment
|
||||||
event.preventDefault();
|
</Button>
|
||||||
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>
|
|
||||||
<DataState isLoading={isLoading} error={error} />
|
<DataState isLoading={isLoading} error={error} />
|
||||||
{data && (
|
{data && (
|
||||||
<Table aria-label="Deployment batches">
|
<Table aria-label="Deployment batches">
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
|
<TableHeaderCell>Service</TableHeaderCell>
|
||||||
|
<TableHeaderCell>Template</TableHeaderCell>
|
||||||
|
<TableHeaderCell>Rule</TableHeaderCell>
|
||||||
<TableHeaderCell>Status</TableHeaderCell>
|
<TableHeaderCell>Status</TableHeaderCell>
|
||||||
|
<TableHeaderCell>Created</TableHeaderCell>
|
||||||
|
<TableHeaderCell>Modified</TableHeaderCell>
|
||||||
<TableHeaderCell>Id</TableHeaderCell>
|
<TableHeaderCell>Id</TableHeaderCell>
|
||||||
<TableHeaderCell>Actions</TableHeaderCell>
|
<TableHeaderCell>Actions</TableHeaderCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
@@ -175,7 +130,21 @@ export function DeploymentGroupsPage() {
|
|||||||
<TableBody>
|
<TableBody>
|
||||||
{data.map((deploymentBatch) => (
|
{data.map((deploymentBatch) => (
|
||||||
<TableRow key={deploymentBatch.id}>
|
<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.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>{deploymentBatch.id}</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<div className={styles.actions}>
|
<div className={styles.actions}>
|
||||||
@@ -184,6 +153,14 @@ export function DeploymentGroupsPage() {
|
|||||||
<Button appearance="subtle" aria-label="Details" icon={<OpenRegular />} />
|
<Button appearance="subtle" aria-label="Details" icon={<OpenRegular />} />
|
||||||
</Link>
|
</Link>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
<Tooltip content="Delete" relationship="label">
|
||||||
|
<Button
|
||||||
|
appearance="subtle"
|
||||||
|
aria-label="Delete"
|
||||||
|
icon={<DeleteRegular />}
|
||||||
|
onClick={() => deleteDeploymentBatch.mutate(deploymentBatch.id)}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
@@ -191,6 +168,140 @@ export function DeploymentGroupsPage() {
|
|||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</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 {
|
import {
|
||||||
Badge,
|
Badge,
|
||||||
Button,
|
Button,
|
||||||
@@ -202,7 +202,7 @@ export function DeploymentJobDetailsPage() {
|
|||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHeaderCell>Status</TableHeaderCell>
|
<TableHeaderCell>Status</TableHeaderCell>
|
||||||
<TableHeaderCell>Virtual Machine Id</TableHeaderCell>
|
<TableHeaderCell>Target Id</TableHeaderCell>
|
||||||
<TableHeaderCell>Deployment Batch Id</TableHeaderCell>
|
<TableHeaderCell>Deployment Batch Id</TableHeaderCell>
|
||||||
<TableHeaderCell>Template Id</TableHeaderCell>
|
<TableHeaderCell>Template Id</TableHeaderCell>
|
||||||
<TableHeaderCell>Attempts</TableHeaderCell>
|
<TableHeaderCell>Attempts</TableHeaderCell>
|
||||||
@@ -213,7 +213,7 @@ export function DeploymentJobDetailsPage() {
|
|||||||
{data.targets.map((target) => (
|
{data.targets.map((target) => (
|
||||||
<TableRow key={target.id}>
|
<TableRow key={target.id}>
|
||||||
<TableCell>{target.status}</TableCell>
|
<TableCell>{target.status}</TableCell>
|
||||||
<TableCell>{target.virtualMachineId}</TableCell>
|
<TableCell>{target.targetId}</TableCell>
|
||||||
<TableCell>{target.deploymentBatchId}</TableCell>
|
<TableCell>{target.deploymentBatchId}</TableCell>
|
||||||
<TableCell>{target.templateId}</TableCell>
|
<TableCell>{target.templateId}</TableCell>
|
||||||
<TableCell>{target.attempts}</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 {
|
import {
|
||||||
Badge,
|
Badge,
|
||||||
Button,
|
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 {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
@@ -23,7 +23,7 @@ import { PageHeader } from "../components/PageHeader";
|
|||||||
export function DeploymentsPage() {
|
export function DeploymentsPage() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const [deploymentBatchId, setDeploymentBatchId] = useState("");
|
const [deploymentBatchId, setDeploymentBatchId] = useState("");
|
||||||
const [selectedVirtualMachineIds, setSelectedVirtualMachineIds] = useState<string[]>([]);
|
const [selectedTargetIds, setSelectedTargetIds] = useState<string[]>([]);
|
||||||
const [jsonData, setJsonData] = useState("{}");
|
const [jsonData, setJsonData] = useState("{}");
|
||||||
const { data, error, isLoading } = useQuery({
|
const { data, error, isLoading } = useQuery({
|
||||||
queryKey: ["deployments"],
|
queryKey: ["deployments"],
|
||||||
@@ -33,9 +33,9 @@ export function DeploymentsPage() {
|
|||||||
queryKey: ["deployment-batches"],
|
queryKey: ["deployment-batches"],
|
||||||
queryFn: ({ signal }) => portalApi.getDeploymentBatches(signal),
|
queryFn: ({ signal }) => portalApi.getDeploymentBatches(signal),
|
||||||
});
|
});
|
||||||
const { data: virtualMachines } = useQuery({
|
const { data: targets } = useQuery({
|
||||||
queryKey: ["virtual-machines"],
|
queryKey: ["targets"],
|
||||||
queryFn: ({ signal }) => portalApi.getVirtualMachines(signal),
|
queryFn: ({ signal }) => portalApi.getTargets(signal),
|
||||||
});
|
});
|
||||||
const { data: deploymentJobs, error: deploymentJobsError, isLoading: deploymentJobsLoading } = useQuery({
|
const { data: deploymentJobs, error: deploymentJobsError, isLoading: deploymentJobsLoading } = useQuery({
|
||||||
queryKey: ["deployment-jobs"],
|
queryKey: ["deployment-jobs"],
|
||||||
@@ -46,7 +46,7 @@ export function DeploymentsPage() {
|
|||||||
mutationFn: portalApi.addDeploymentRequest,
|
mutationFn: portalApi.addDeploymentRequest,
|
||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
setDeploymentBatchId("");
|
setDeploymentBatchId("");
|
||||||
setSelectedVirtualMachineIds([]);
|
setSelectedTargetIds([]);
|
||||||
setJsonData("{}");
|
setJsonData("{}");
|
||||||
await queryClient.invalidateQueries({ queryKey: ["deployments"] });
|
await queryClient.invalidateQueries({ queryKey: ["deployments"] });
|
||||||
await queryClient.invalidateQueries({ queryKey: ["deployment-jobs"] });
|
await queryClient.invalidateQueries({ queryKey: ["deployment-jobs"] });
|
||||||
@@ -65,7 +65,7 @@ export function DeploymentsPage() {
|
|||||||
<FormSection
|
<FormSection
|
||||||
onSubmit={(event) => {
|
onSubmit={(event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
addDeploymentRequest.mutate({ deploymentBatchId, jsonData, virtualMachineIds: selectedVirtualMachineIds });
|
addDeploymentRequest.mutate({ deploymentBatchId, jsonData, targetIds: selectedTargetIds });
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<FormGrid>
|
<FormGrid>
|
||||||
@@ -82,18 +82,18 @@ export function DeploymentsPage() {
|
|||||||
))}
|
))}
|
||||||
</Combobox>
|
</Combobox>
|
||||||
</Field>
|
</Field>
|
||||||
<Field label="Targets (Virtual Machines)" required>
|
<Field label="Targets (Targets)" required>
|
||||||
<div>
|
<div>
|
||||||
{(virtualMachines ?? []).map((virtualMachine) => (
|
{(targets ?? []).map((target) => (
|
||||||
<Checkbox
|
<Checkbox
|
||||||
key={virtualMachine.id}
|
key={target.id}
|
||||||
label={virtualMachine.name}
|
label={target.name}
|
||||||
checked={selectedVirtualMachineIds.includes(virtualMachine.id)}
|
checked={selectedTargetIds.includes(target.id)}
|
||||||
onChange={(_, data) => {
|
onChange={(_, data) => {
|
||||||
if (data.checked) {
|
if (data.checked) {
|
||||||
setSelectedVirtualMachineIds((previous) => [...previous, virtualMachine.id]);
|
setSelectedTargetIds((previous) => [...previous, target.id]);
|
||||||
} else {
|
} 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>
|
<FormActions>
|
||||||
<Button
|
<Button
|
||||||
appearance="primary"
|
appearance="primary"
|
||||||
disabled={!deploymentBatchId || selectedVirtualMachineIds.length === 0 || addDeploymentRequest.isPending}
|
disabled={!deploymentBatchId || selectedTargetIds.length === 0 || addDeploymentRequest.isPending}
|
||||||
type="submit"
|
type="submit"
|
||||||
>
|
>
|
||||||
Start deployment
|
Start deployment
|
||||||
@@ -158,7 +158,7 @@ export function DeploymentsPage() {
|
|||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHeaderCell>Status</TableHeaderCell>
|
<TableHeaderCell>Status</TableHeaderCell>
|
||||||
<TableHeaderCell>Deployment Batch</TableHeaderCell>
|
<TableHeaderCell>Deployment Batch</TableHeaderCell>
|
||||||
<TableHeaderCell>Virtual Machine</TableHeaderCell>
|
<TableHeaderCell>Target</TableHeaderCell>
|
||||||
<TableHeaderCell>Execution Id</TableHeaderCell>
|
<TableHeaderCell>Execution Id</TableHeaderCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
@@ -167,7 +167,7 @@ export function DeploymentsPage() {
|
|||||||
<TableRow key={deployment.id}>
|
<TableRow key={deployment.id}>
|
||||||
<TableCell>{deployment.status ?? "Unknown"}</TableCell>
|
<TableCell>{deployment.status ?? "Unknown"}</TableCell>
|
||||||
<TableCell>{deployment.deploymentBatchId ?? "-"}</TableCell>
|
<TableCell>{deployment.deploymentBatchId ?? "-"}</TableCell>
|
||||||
<TableCell>{deployment.virtualMachineId ?? "-"}</TableCell>
|
<TableCell>{deployment.targetId ?? "-"}</TableCell>
|
||||||
<TableCell>{deployment.id}</TableCell>
|
<TableCell>{deployment.id}</TableCell>
|
||||||
</TableRow>
|
</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 {
|
import {
|
||||||
Badge,
|
Badge,
|
||||||
Button,
|
Button,
|
||||||
@@ -73,21 +73,21 @@ export function DomainDetailsPage() {
|
|||||||
queryFn: ({ signal }) => portalApi.getDomainEnvironments(id!, signal),
|
queryFn: ({ signal }) => portalApi.getDomainEnvironments(id!, signal),
|
||||||
});
|
});
|
||||||
const {
|
const {
|
||||||
data: virtualMachineData,
|
data: targetData,
|
||||||
error: virtualMachinesError,
|
error: targetsError,
|
||||||
isLoading: virtualMachinesLoading,
|
isLoading: targetsLoading,
|
||||||
} = useQuery({
|
} = useQuery({
|
||||||
enabled: Boolean(id),
|
enabled: Boolean(id),
|
||||||
queryKey: ["domain", id, "virtual-machines"],
|
queryKey: ["domain", id, "targets"],
|
||||||
queryFn: ({ signal }) => portalApi.getDomainVirtualMachines(id!, signal),
|
queryFn: ({ signal }) => portalApi.getDomainTargets(id!, signal),
|
||||||
});
|
});
|
||||||
const links = data?.environmentDomains?.filter((link) => link.environment) ?? [];
|
const links = data?.environmentDomains?.filter((link) => link.environment) ?? [];
|
||||||
const filteredVirtualMachines = useMemo(() => {
|
const filteredTargets = useMemo(() => {
|
||||||
const items = [...(virtualMachineData?.virtualMachines ?? [])];
|
const items = [...(targetData?.targets ?? [])];
|
||||||
const searchValue = search.trim().toLowerCase();
|
const searchValue = search.trim().toLowerCase();
|
||||||
const filteredItems = searchValue.length
|
const filteredItems = searchValue.length
|
||||||
? items.filter((virtualMachine) =>
|
? items.filter((target) =>
|
||||||
[virtualMachine.name, virtualMachine.externalId, virtualMachine.id]
|
[target.name, target.externalId, target.id]
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.some((value) => value!.toLowerCase().includes(searchValue)),
|
.some((value) => value!.toLowerCase().includes(searchValue)),
|
||||||
)
|
)
|
||||||
@@ -101,7 +101,7 @@ export function DomainDetailsPage() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return filteredItems;
|
return filteredItems;
|
||||||
}, [virtualMachineData?.virtualMachines, search, sortBy, sortOrder]);
|
}, [targetData?.targets, search, sortBy, sortOrder]);
|
||||||
|
|
||||||
const toggleSort = (column: "name" | "externalId" | "id") => {
|
const toggleSort = (column: "name" | "externalId" | "id") => {
|
||||||
if (sortBy === column) {
|
if (sortBy === column) {
|
||||||
@@ -115,7 +115,7 @@ export function DomainDetailsPage() {
|
|||||||
|
|
||||||
const sortIndicator = (column: "name" | "externalId" | "id") => {
|
const sortIndicator = (column: "name" | "externalId" | "id") => {
|
||||||
if (sortBy !== column) return "";
|
if (sortBy !== column) return "";
|
||||||
return sortOrder === "asc" ? " ↑" : " ↓";
|
return sortOrder === "asc" ? " ↑" : " ↓";
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -126,7 +126,7 @@ export function DomainDetailsPage() {
|
|||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
<PageHeader title={data?.name ?? "Domain"} description="Details und verknuepfte Environments." />
|
<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 && (
|
{data && (
|
||||||
<>
|
<>
|
||||||
<section className={styles.details} aria-label="Domain details">
|
<section className={styles.details} aria-label="Domain details">
|
||||||
@@ -196,7 +196,7 @@ export function DomainDetailsPage() {
|
|||||||
</Table>
|
</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" }}>
|
<div style={{ display: "grid", gap: "12px", gridTemplateColumns: "minmax(220px, 1fr)", marginBottom: "12px" }}>
|
||||||
<Field label="Search">
|
<Field label="Search">
|
||||||
<Input
|
<Input
|
||||||
@@ -206,12 +206,12 @@ export function DomainDetailsPage() {
|
|||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
</div>
|
</div>
|
||||||
{filteredVirtualMachines.length === 0 ? (
|
{filteredTargets.length === 0 ? (
|
||||||
<MessageBar>
|
<MessageBar>
|
||||||
<MessageBarBody>Keine Virtual Machines fuer diese Domain gefunden.</MessageBarBody>
|
<MessageBarBody>Keine Targets fuer diese Domain gefunden.</MessageBarBody>
|
||||||
</MessageBar>
|
</MessageBar>
|
||||||
) : (
|
) : (
|
||||||
<Table aria-label="Domain virtual machines">
|
<Table aria-label="Domain targets">
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHeaderCell onClick={() => toggleSort("name")} style={{ cursor: "pointer" }}>
|
<TableHeaderCell onClick={() => toggleSort("name")} style={{ cursor: "pointer" }}>
|
||||||
@@ -226,13 +226,13 @@ export function DomainDetailsPage() {
|
|||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{filteredVirtualMachines.map((virtualMachine) => (
|
{filteredTargets.map((target) => (
|
||||||
<TableRow key={virtualMachine.id}>
|
<TableRow key={target.id}>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Link to={`/virtual-machines/${virtualMachine.id}`}>{virtualMachine.name}</Link>
|
<Link to={`/targets/${target.id}`}>{target.name}</Link>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>{virtualMachine.externalId ?? "-"}</TableCell>
|
<TableCell>{target.externalId ?? "-"}</TableCell>
|
||||||
<TableCell>{virtualMachine.id}</TableCell>
|
<TableCell>{target.id}</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))}
|
||||||
</TableBody>
|
</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 {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Combobox,
|
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 {
|
import {
|
||||||
Badge,
|
Badge,
|
||||||
Button,
|
Button,
|
||||||
@@ -17,7 +17,7 @@ import {
|
|||||||
tokens,
|
tokens,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
} from "@fluentui/react-components";
|
} 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 { Link, useParams } from "react-router-dom";
|
||||||
import { portalApi } from "../api/portalApi";
|
import { portalApi } from "../api/portalApi";
|
||||||
import { DataState } from "../components/DataState";
|
import { DataState } from "../components/DataState";
|
||||||
@@ -59,6 +59,7 @@ const useStyles = makeStyles({
|
|||||||
|
|
||||||
export function EnvironmentDetailsPage() {
|
export function EnvironmentDetailsPage() {
|
||||||
const styles = useStyles();
|
const styles = useStyles();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
const { data, error, isLoading } = useQuery({
|
const { data, error, isLoading } = useQuery({
|
||||||
enabled: Boolean(id),
|
enabled: Boolean(id),
|
||||||
@@ -66,6 +67,15 @@ export function EnvironmentDetailsPage() {
|
|||||||
queryFn: ({ signal }) => portalApi.getEnvironmentDomains(id!, signal),
|
queryFn: ({ signal }) => portalApi.getEnvironmentDomains(id!, signal),
|
||||||
});
|
});
|
||||||
const links = data?.environmentDomains?.filter((link) => link.domain) ?? [];
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -92,24 +102,30 @@ export function EnvironmentDetailsPage() {
|
|||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.field}>
|
<div className={styles.field}>
|
||||||
<Text size={200}>Environment Type</Text>
|
<Text size={200}>Environment Stage</Text>
|
||||||
<Text className={styles.value} weight="semibold">
|
<Text className={styles.value} weight="semibold">
|
||||||
{data.environmentType}
|
{data.environmentType}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</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}>
|
<div className={styles.field}>
|
||||||
<Text size={200}>Cloud Enabled</Text>
|
<Text size={200}>Cloud Enabled</Text>
|
||||||
<Text className={styles.value} weight="semibold">
|
<Text className={styles.value} weight="semibold">
|
||||||
{data.environmentType === "OnPrem" ? "Nein" : "Ja"}
|
{data.hostingType === "OnPrem" ? "Nein" : "Ja"}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.field}>
|
<div className={styles.field}>
|
||||||
<Text size={200}>Provider Type</Text>
|
<Text size={200}>Provider Type</Text>
|
||||||
<Text className={styles.value} weight="semibold">
|
<Text className={styles.value} weight="semibold">
|
||||||
{data.environmentType === "OnPrem" ? data.providerType : ""}
|
{data.hostingType === "OnPrem" ? data.providerType : ""}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
{data.environmentType !== "OnPrem" && (
|
{data.hostingType !== "OnPrem" && (
|
||||||
<div className={styles.field}>
|
<div className={styles.field}>
|
||||||
<Text size={200}>Tenant Id</Text>
|
<Text size={200}>Tenant Id</Text>
|
||||||
<Text className={styles.value} weight="semibold">
|
<Text className={styles.value} weight="semibold">
|
||||||
@@ -117,7 +133,7 @@ export function EnvironmentDetailsPage() {
|
|||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{data.environmentType === "AzureTenant" && (
|
{data.hostingType === "AzureTenant" && (
|
||||||
<div className={styles.field}>
|
<div className={styles.field}>
|
||||||
<Text size={200}>Subscription Id</Text>
|
<Text size={200}>Subscription Id</Text>
|
||||||
<Text className={styles.value} weight="semibold">
|
<Text className={styles.value} weight="semibold">
|
||||||
@@ -140,7 +156,7 @@ export function EnvironmentDetailsPage() {
|
|||||||
<TableHeaderCell>FQDN</TableHeaderCell>
|
<TableHeaderCell>FQDN</TableHeaderCell>
|
||||||
<TableHeaderCell>NetBIOS</TableHeaderCell>
|
<TableHeaderCell>NetBIOS</TableHeaderCell>
|
||||||
<TableHeaderCell>Status</TableHeaderCell>
|
<TableHeaderCell>Status</TableHeaderCell>
|
||||||
<TableHeaderCell>Details</TableHeaderCell>
|
<TableHeaderCell>Actions</TableHeaderCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
@@ -161,6 +177,21 @@ export function EnvironmentDetailsPage() {
|
|||||||
<Button appearance="subtle" aria-label="Details" icon={<OpenRegular />} />
|
<Button appearance="subtle" aria-label="Details" icon={<OpenRegular />} />
|
||||||
</Link>
|
</Link>
|
||||||
</Tooltip>
|
</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>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</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 {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Combobox,
|
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 {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Combobox,
|
Combobox,
|
||||||
@@ -56,7 +56,8 @@ export function EnvironmentsPage() {
|
|||||||
const [dialogMode, setDialogMode] = useState<DialogMode>(null);
|
const [dialogMode, setDialogMode] = useState<DialogMode>(null);
|
||||||
const [selectedEnvironment, setSelectedEnvironment] = useState<EnvironmentItem | null>(null);
|
const [selectedEnvironment, setSelectedEnvironment] = useState<EnvironmentItem | null>(null);
|
||||||
const [name, setName] = useState("");
|
const [name, setName] = useState("");
|
||||||
const [environmentType, setEnvironmentType] = useState("OnPrem");
|
const [environmentType, setEnvironmentType] = useState("Test");
|
||||||
|
const [hostingType, setHostingType] = useState("OnPrem");
|
||||||
const [providerType, setProviderType] = useState("");
|
const [providerType, setProviderType] = useState("");
|
||||||
const [tenantId, setTenantId] = useState("");
|
const [tenantId, setTenantId] = useState("");
|
||||||
const [subscriptionId, setSubscriptionId] = useState("");
|
const [subscriptionId, setSubscriptionId] = useState("");
|
||||||
@@ -76,7 +77,8 @@ export function EnvironmentsPage() {
|
|||||||
setDialogMode(null);
|
setDialogMode(null);
|
||||||
setSelectedEnvironment(null);
|
setSelectedEnvironment(null);
|
||||||
setName("");
|
setName("");
|
||||||
setEnvironmentType("OnPrem");
|
setEnvironmentType("Test");
|
||||||
|
setHostingType("OnPrem");
|
||||||
setProviderType("");
|
setProviderType("");
|
||||||
setTenantId("");
|
setTenantId("");
|
||||||
setSubscriptionId("");
|
setSubscriptionId("");
|
||||||
@@ -86,7 +88,8 @@ export function EnvironmentsPage() {
|
|||||||
const openAddDialog = () => {
|
const openAddDialog = () => {
|
||||||
setSelectedEnvironment(null);
|
setSelectedEnvironment(null);
|
||||||
setName("");
|
setName("");
|
||||||
setEnvironmentType("OnPrem");
|
setEnvironmentType("Test");
|
||||||
|
setHostingType("OnPrem");
|
||||||
setProviderType("");
|
setProviderType("");
|
||||||
setTenantId("");
|
setTenantId("");
|
||||||
setSubscriptionId("");
|
setSubscriptionId("");
|
||||||
@@ -97,7 +100,8 @@ export function EnvironmentsPage() {
|
|||||||
const openEditDialog = (environment: EnvironmentItem) => {
|
const openEditDialog = (environment: EnvironmentItem) => {
|
||||||
setSelectedEnvironment(environment);
|
setSelectedEnvironment(environment);
|
||||||
setName(environment.name);
|
setName(environment.name);
|
||||||
setEnvironmentType(environment.environmentType ?? "OnPrem");
|
setEnvironmentType(environment.environmentType ?? "Test");
|
||||||
|
setHostingType(environment.hostingType ?? "OnPrem");
|
||||||
setProviderType(environment.providerType ?? "");
|
setProviderType(environment.providerType ?? "");
|
||||||
setTenantId(environment.tenantId ?? "");
|
setTenantId(environment.tenantId ?? "");
|
||||||
setSubscriptionId(environment.subscriptionId ?? "");
|
setSubscriptionId(environment.subscriptionId ?? "");
|
||||||
@@ -121,6 +125,7 @@ export function EnvironmentsPage() {
|
|||||||
mutationFn: ({ id, environment }: { id: string; environment: {
|
mutationFn: ({ id, environment }: { id: string; environment: {
|
||||||
name: string;
|
name: string;
|
||||||
environmentType: string;
|
environmentType: string;
|
||||||
|
hostingType: string;
|
||||||
providerType?: string;
|
providerType?: string;
|
||||||
tenantId?: string;
|
tenantId?: string;
|
||||||
subscriptionId?: string;
|
subscriptionId?: string;
|
||||||
@@ -155,6 +160,7 @@ export function EnvironmentsPage() {
|
|||||||
const environment = {
|
const environment = {
|
||||||
name,
|
name,
|
||||||
environmentType,
|
environmentType,
|
||||||
|
hostingType,
|
||||||
providerType,
|
providerType,
|
||||||
tenantId,
|
tenantId,
|
||||||
subscriptionId,
|
subscriptionId,
|
||||||
@@ -171,9 +177,9 @@ export function EnvironmentsPage() {
|
|||||||
|
|
||||||
const formError = addEnvironment.error?.message ?? updateEnvironment.error?.message;
|
const formError = addEnvironment.error?.message ?? updateEnvironment.error?.message;
|
||||||
const isSaving = addEnvironment.isPending || updateEnvironment.isPending;
|
const isSaving = addEnvironment.isPending || updateEnvironment.isPending;
|
||||||
const isOnPrem = environmentType === "OnPrem";
|
const isOnPrem = hostingType === "OnPrem";
|
||||||
const isAzureTenant = environmentType === "AzureTenant";
|
const isAzureTenant = hostingType === "AzureTenant";
|
||||||
const isM365Tenant = environmentType === "M365Tenant";
|
const isM365Tenant = hostingType === "M365Tenant";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -198,12 +204,28 @@ export function EnvironmentsPage() {
|
|||||||
<Field label="Name" required validationMessage={formError}>
|
<Field label="Name" required validationMessage={formError}>
|
||||||
<Input value={name} onChange={(_, data) => setName(data.value)} />
|
<Input value={name} onChange={(_, data) => setName(data.value)} />
|
||||||
</Field>
|
</Field>
|
||||||
<Field label="Environment Type" required>
|
<Field label="Environment Stage" required>
|
||||||
<Combobox
|
<Combobox value={environmentType} onOptionSelect={(_, data) => setEnvironmentType(data.optionValue ?? "Test")}>
|
||||||
value={environmentType}
|
<Option text="Development" value="Development">
|
||||||
onOptionSelect={(_, data) => {
|
Development
|
||||||
const nextType = data.optionValue ?? "OnPrem";
|
</Option>
|
||||||
setEnvironmentType(nextType);
|
<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") {
|
if (nextType === "OnPrem") {
|
||||||
setTenantId("");
|
setTenantId("");
|
||||||
@@ -352,7 +374,8 @@ export function EnvironmentsPage() {
|
|||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHeaderCell>Name</TableHeaderCell>
|
<TableHeaderCell>Name</TableHeaderCell>
|
||||||
<TableHeaderCell>Type</TableHeaderCell>
|
<TableHeaderCell>Stage</TableHeaderCell>
|
||||||
|
<TableHeaderCell>Hosting</TableHeaderCell>
|
||||||
<TableHeaderCell>Cloud</TableHeaderCell>
|
<TableHeaderCell>Cloud</TableHeaderCell>
|
||||||
<TableHeaderCell>Provider</TableHeaderCell>
|
<TableHeaderCell>Provider</TableHeaderCell>
|
||||||
<TableHeaderCell>Tenant</TableHeaderCell>
|
<TableHeaderCell>Tenant</TableHeaderCell>
|
||||||
@@ -366,10 +389,11 @@ export function EnvironmentsPage() {
|
|||||||
<TableRow key={environment.id}>
|
<TableRow key={environment.id}>
|
||||||
<TableCell>{environment.name}</TableCell>
|
<TableCell>{environment.name}</TableCell>
|
||||||
<TableCell>{environment.environmentType}</TableCell>
|
<TableCell>{environment.environmentType}</TableCell>
|
||||||
<TableCell>{environment.environmentType === "OnPrem" ? "Nein" : "Ja"}</TableCell>
|
<TableCell>{environment.hostingType}</TableCell>
|
||||||
<TableCell>{environment.environmentType === "OnPrem" ? environment.providerType : ""}</TableCell>
|
<TableCell>{environment.hostingType === "OnPrem" ? "Nein" : "Ja"}</TableCell>
|
||||||
<TableCell>{environment.environmentType !== "OnPrem" ? environment.tenantId : ""}</TableCell>
|
<TableCell>{environment.hostingType === "OnPrem" ? environment.providerType : ""}</TableCell>
|
||||||
<TableCell>{environment.environmentType === "AzureTenant" ? environment.subscriptionId : ""}</TableCell>
|
<TableCell>{environment.hostingType !== "OnPrem" ? environment.tenantId : ""}</TableCell>
|
||||||
|
<TableCell>{environment.hostingType === "AzureTenant" ? environment.subscriptionId : ""}</TableCell>
|
||||||
<TableCell>{environment.id}</TableCell>
|
<TableCell>{environment.id}</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<div className={styles.actions}>
|
<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 {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Field,
|
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 {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Checkbox,
|
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 {
|
import {
|
||||||
Badge,
|
Badge,
|
||||||
Button,
|
Button,
|
||||||
@@ -39,32 +39,44 @@ const useStyles = makeStyles({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export function VirtualMachineDetailsPage() {
|
export function TargetDetailsPage() {
|
||||||
const styles = useStyles();
|
const styles = useStyles();
|
||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
const { data, error, isLoading } = useQuery({
|
const { data, error, isLoading } = useQuery({
|
||||||
enabled: Boolean(id),
|
enabled: Boolean(id),
|
||||||
queryKey: ["virtual-machine", id],
|
queryKey: ["target", id],
|
||||||
queryFn: ({ signal }) => portalApi.getVirtualMachineById(id!, signal),
|
queryFn: ({ signal }) => portalApi.getTargetById(id!, signal),
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Link className={styles.backLink} to="/virtual-machines">
|
<Link className={styles.backLink} to="/targets">
|
||||||
<Button appearance="subtle" icon={<ArrowLeftRegular />}>
|
<Button appearance="subtle" icon={<ArrowLeftRegular />}>
|
||||||
Virtual Machines
|
Targets
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</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} />
|
<DataState isLoading={isLoading} error={error} />
|
||||||
{data && (
|
{data && (
|
||||||
<section className={styles.details} aria-label="Virtual machine details">
|
<section className={styles.details} aria-label="Target details">
|
||||||
<div className={styles.field}>
|
<div className={styles.field}>
|
||||||
<Text size={200}>Name</Text>
|
<Text size={200}>Name</Text>
|
||||||
<Text className={styles.value} weight="semibold">
|
<Text className={styles.value} weight="semibold">
|
||||||
{data.name}
|
{data.name}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</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}>
|
<div className={styles.field}>
|
||||||
<Text size={200}>Domain Link Status</Text>
|
<Text size={200}>Domain Link Status</Text>
|
||||||
<Badge appearance={data.domainID ? "filled" : "tint"} color={data.domainID ? "success" : "informative"}>
|
<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 {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Combobox,
|
Combobox,
|
||||||
@@ -27,7 +27,7 @@ import { useState } from "react";
|
|||||||
import { portalApi } from "../api/portalApi";
|
import { portalApi } from "../api/portalApi";
|
||||||
import { DataState } from "../components/DataState";
|
import { DataState } from "../components/DataState";
|
||||||
import { PageHeader } from "../components/PageHeader";
|
import { PageHeader } from "../components/PageHeader";
|
||||||
import type { Domain, VirtualMachine } from "../types/portal";
|
import type { Domain, Target } from "../types/portal";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
const useStyles = makeStyles({
|
const useStyles = makeStyles({
|
||||||
@@ -52,22 +52,24 @@ const useStyles = makeStyles({
|
|||||||
|
|
||||||
type DialogMode = "add" | "edit" | null;
|
type DialogMode = "add" | "edit" | null;
|
||||||
|
|
||||||
export function VirtualMachinesPage() {
|
export function TargetsPage() {
|
||||||
const styles = useStyles();
|
const styles = useStyles();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const [dialogMode, setDialogMode] = useState<DialogMode>(null);
|
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 [domainID, setDomainID] = useState<string | undefined>(undefined);
|
||||||
const [linkVirtualMachineId, setLinkVirtualMachineId] = useState("");
|
const [linkTargetId, setLinkTargetId] = useState("");
|
||||||
const [linkDomainId, setLinkDomainId] = useState("");
|
const [linkDomainId, setLinkDomainId] = useState("");
|
||||||
const [unlinkVirtualMachineId, setUnlinkVirtualMachineId] = useState("");
|
const [unlinkTargetId, setUnlinkTargetId] = useState("");
|
||||||
const [name, setName] = useState("");
|
const [name, setName] = useState("");
|
||||||
|
const [targetType, setTargetType] = useState("VirtualMachine");
|
||||||
|
const [providerType, setProviderType] = useState("OnPrem");
|
||||||
const [externalId, setExternalId] = useState("");
|
const [externalId, setExternalId] = useState("");
|
||||||
const [metadataJson, setMetadataJson] = useState("");
|
const [metadataJson, setMetadataJson] = useState("");
|
||||||
|
|
||||||
const { data, error, isLoading } = useQuery({
|
const { data, error, isLoading } = useQuery({
|
||||||
queryKey: ["virtual-machines"],
|
queryKey: ["targets"],
|
||||||
queryFn: ({ signal }) => portalApi.getVirtualMachines(signal),
|
queryFn: ({ signal }) => portalApi.getTargets(signal),
|
||||||
});
|
});
|
||||||
const { data: domains, error: domainsError, isLoading: domainsLoading } = useQuery({
|
const { data: domains, error: domainsError, isLoading: domainsLoading } = useQuery({
|
||||||
queryKey: ["domains"],
|
queryKey: ["domains"],
|
||||||
@@ -76,110 +78,116 @@ export function VirtualMachinesPage() {
|
|||||||
|
|
||||||
const closeDialog = () => {
|
const closeDialog = () => {
|
||||||
setDialogMode(null);
|
setDialogMode(null);
|
||||||
setSelectedVirtualMachine(null);
|
setSelectedTarget(null);
|
||||||
setDomainID(undefined);
|
setDomainID(undefined);
|
||||||
setName("");
|
setName("");
|
||||||
|
setTargetType("VirtualMachine");
|
||||||
|
setProviderType("OnPrem");
|
||||||
setExternalId("");
|
setExternalId("");
|
||||||
setMetadataJson("");
|
setMetadataJson("");
|
||||||
};
|
};
|
||||||
|
|
||||||
const openAddDialog = () => {
|
const openAddDialog = () => {
|
||||||
setSelectedVirtualMachine(null);
|
setSelectedTarget(null);
|
||||||
setDomainID(undefined);
|
setDomainID(undefined);
|
||||||
setName("");
|
setName("");
|
||||||
|
setTargetType("VirtualMachine");
|
||||||
|
setProviderType("OnPrem");
|
||||||
setExternalId("");
|
setExternalId("");
|
||||||
setMetadataJson("");
|
setMetadataJson("");
|
||||||
setDialogMode("add");
|
setDialogMode("add");
|
||||||
};
|
};
|
||||||
|
|
||||||
const openVirtualMachineDialog = (mode: "edit", virtualMachine: VirtualMachine) => {
|
const openTargetDialog = (mode: "edit", target: Target) => {
|
||||||
setSelectedVirtualMachine(virtualMachine);
|
setSelectedTarget(target);
|
||||||
setDomainID(virtualMachine.domainID);
|
setDomainID(target.domainID);
|
||||||
setName(virtualMachine.name);
|
setName(target.name);
|
||||||
setExternalId(virtualMachine.externalId ?? "");
|
setTargetType(target.targetType ?? "VirtualMachine");
|
||||||
setMetadataJson(virtualMachine.metadataJson ?? "");
|
setProviderType(target.providerType ?? "OnPrem");
|
||||||
|
setExternalId(target.externalId ?? "");
|
||||||
|
setMetadataJson(target.metadataJson ?? "");
|
||||||
setDialogMode(mode);
|
setDialogMode(mode);
|
||||||
};
|
};
|
||||||
|
|
||||||
const addVirtualMachine = useMutation({
|
const addTarget = useMutation({
|
||||||
mutationFn: portalApi.addVirtualMachine,
|
mutationFn: portalApi.addTarget,
|
||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
closeDialog();
|
closeDialog();
|
||||||
await queryClient.invalidateQueries({ queryKey: ["virtual-machines"] });
|
await queryClient.invalidateQueries({ queryKey: ["targets"] });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const updateVirtualMachine = useMutation({
|
const updateTarget = useMutation({
|
||||||
mutationFn: ({ id, virtualMachine }: { id: string; virtualMachine: { name: string; externalId?: string; metadataJson?: string } }) =>
|
mutationFn: ({ id, target }: { id: string; target: { name: string; targetType: string; providerType: string; externalId?: string; metadataJson?: string } }) =>
|
||||||
portalApi.updateVirtualMachine(id, virtualMachine),
|
portalApi.updateTarget(id, target),
|
||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
closeDialog();
|
closeDialog();
|
||||||
await queryClient.invalidateQueries({ queryKey: ["virtual-machines"] });
|
await queryClient.invalidateQueries({ queryKey: ["targets"] });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const deleteVirtualMachine = useMutation({
|
const deleteTarget = useMutation({
|
||||||
mutationFn: portalApi.deleteVirtualMachine,
|
mutationFn: portalApi.deleteTarget,
|
||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
await queryClient.invalidateQueries({ queryKey: ["virtual-machines"] });
|
await queryClient.invalidateQueries({ queryKey: ["targets"] });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const linkVirtualMachineToDomain = useMutation({
|
const linkTargetToDomain = useMutation({
|
||||||
mutationFn: ({ virtualMachineId, targetDomainId }: { virtualMachineId: string; targetDomainId: string }) =>
|
mutationFn: ({ targetId, targetDomainId }: { targetId: string; targetDomainId: string }) =>
|
||||||
portalApi.linkVirtualMachineToDomain(virtualMachineId, targetDomainId),
|
portalApi.linkTargetToDomain(targetId, targetDomainId),
|
||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
setLinkVirtualMachineId("");
|
setLinkTargetId("");
|
||||||
setLinkDomainId("");
|
setLinkDomainId("");
|
||||||
await queryClient.invalidateQueries({ queryKey: ["virtual-machines"] });
|
await queryClient.invalidateQueries({ queryKey: ["targets"] });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const unlinkVirtualMachineFromDomain = useMutation({
|
const unlinkTargetFromDomain = useMutation({
|
||||||
mutationFn: (virtualMachineId: string) => portalApi.unlinkVirtualMachineFromDomain(virtualMachineId),
|
mutationFn: (targetId: string) => portalApi.unlinkTargetFromDomain(targetId),
|
||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
setUnlinkVirtualMachineId("");
|
setUnlinkTargetId("");
|
||||||
await queryClient.invalidateQueries({ queryKey: ["virtual-machines"] });
|
await queryClient.invalidateQueries({ queryKey: ["targets"] });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const submitVirtualMachine = (event: React.FormEvent<HTMLFormElement>) => {
|
const submitTarget = (event: React.FormEvent<HTMLFormElement>) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const virtualMachine = { domainID, name, externalId, metadataJson };
|
const target = { domainID, name, targetType, providerType, externalId, metadataJson };
|
||||||
|
|
||||||
if (dialogMode === "edit" && selectedVirtualMachine) {
|
if (dialogMode === "edit" && selectedTarget) {
|
||||||
updateVirtualMachine.mutate({ id: selectedVirtualMachine.id, virtualMachine: { name, externalId, metadataJson } });
|
updateTarget.mutate({ id: selectedTarget.id, target: { name, targetType, providerType, externalId, metadataJson } });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
addVirtualMachine.mutate(virtualMachine);
|
addTarget.mutate(target);
|
||||||
};
|
};
|
||||||
|
|
||||||
const formError =
|
const formError =
|
||||||
addVirtualMachine.error?.message ??
|
addTarget.error?.message ??
|
||||||
updateVirtualMachine.error?.message ??
|
updateTarget.error?.message ??
|
||||||
linkVirtualMachineToDomain.error?.message ??
|
linkTargetToDomain.error?.message ??
|
||||||
unlinkVirtualMachineFromDomain.error?.message;
|
unlinkTargetFromDomain.error?.message;
|
||||||
const isSaving = addVirtualMachine.isPending || updateVirtualMachine.isPending;
|
const isSaving = addTarget.isPending || updateTarget.isPending;
|
||||||
const domainNameById = new Map((domains ?? []).map((domain: Domain) => [domain.id, domain.name]));
|
const domainNameById = new Map((domains ?? []).map((domain: Domain) => [domain.id, domain.name]));
|
||||||
const linkedVirtualMachines = (data ?? []).filter((virtualMachine) => Boolean(virtualMachine.domainID));
|
const linkedTargets = (data ?? []).filter((target) => Boolean(target.domainID));
|
||||||
const unlinkedVirtualMachines = (data ?? []).filter((virtualMachine) => !virtualMachine.domainID);
|
const unlinkedTargets = (data ?? []).filter((target) => !target.domainID);
|
||||||
|
|
||||||
return (
|
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}>
|
<div className={styles.toolbar}>
|
||||||
<Button appearance="primary" icon={<AddRegular />} onClick={openAddDialog}>
|
<Button appearance="primary" icon={<AddRegular />} onClick={openAddDialog}>
|
||||||
Virtual Machine hinzufuegen
|
Target hinzufuegen
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
appearance="secondary"
|
appearance="secondary"
|
||||||
icon={<LinkRegular />}
|
icon={<LinkRegular />}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setLinkVirtualMachineId(unlinkedVirtualMachines[0]?.id ?? "");
|
setLinkTargetId(unlinkedTargets[0]?.id ?? "");
|
||||||
setLinkDomainId(domains?.[0]?.id ?? "");
|
setLinkDomainId(domains?.[0]?.id ?? "");
|
||||||
}}
|
}}
|
||||||
disabled={!unlinkedVirtualMachines.length || !domains?.length}
|
disabled={!unlinkedTargets.length || !domains?.length}
|
||||||
style={{ marginLeft: "10px" }}
|
style={{ marginLeft: "10px" }}
|
||||||
>
|
>
|
||||||
Link to Domain
|
Link to Domain
|
||||||
@@ -188,9 +196,9 @@ export function VirtualMachinesPage() {
|
|||||||
appearance="secondary"
|
appearance="secondary"
|
||||||
icon={<LinkDismissRegular />}
|
icon={<LinkDismissRegular />}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setUnlinkVirtualMachineId(linkedVirtualMachines[0]?.id ?? "");
|
setUnlinkTargetId(linkedTargets[0]?.id ?? "");
|
||||||
}}
|
}}
|
||||||
disabled={!linkedVirtualMachines.length}
|
disabled={!linkedTargets.length}
|
||||||
style={{ marginLeft: "10px" }}
|
style={{ marginLeft: "10px" }}
|
||||||
>
|
>
|
||||||
Unlink Domain
|
Unlink Domain
|
||||||
@@ -199,12 +207,12 @@ export function VirtualMachinesPage() {
|
|||||||
|
|
||||||
<Dialog open={dialogMode !== null} onOpenChange={(_, dialogData) => !dialogData.open && closeDialog()}>
|
<Dialog open={dialogMode !== null} onOpenChange={(_, dialogData) => !dialogData.open && closeDialog()}>
|
||||||
<DialogSurface>
|
<DialogSurface>
|
||||||
<form onSubmit={submitVirtualMachine}>
|
<form onSubmit={submitTarget}>
|
||||||
<DialogBody>
|
<DialogBody>
|
||||||
<DialogTitle>
|
<DialogTitle>
|
||||||
{dialogMode === "edit"
|
{dialogMode === "edit"
|
||||||
? "Virtual Machine aendern"
|
? "Target aendern"
|
||||||
: "Virtual Machine hinzufuegen"}
|
: "Target hinzufuegen"}
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogContent className={styles.form}>
|
<DialogContent className={styles.form}>
|
||||||
<Field label="Domain (optional)" validationMessage={formError}>
|
<Field label="Domain (optional)" validationMessage={formError}>
|
||||||
@@ -224,6 +232,30 @@ export function VirtualMachinesPage() {
|
|||||||
<Field label="Name" required>
|
<Field label="Name" required>
|
||||||
<Input value={name} onChange={(_, inputData) => setName(inputData.value)} />
|
<Input value={name} onChange={(_, inputData) => setName(inputData.value)} />
|
||||||
</Field>
|
</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">
|
<Field label="External Id">
|
||||||
<Input
|
<Input
|
||||||
value={externalId}
|
value={externalId}
|
||||||
@@ -237,9 +269,9 @@ export function VirtualMachinesPage() {
|
|||||||
onChange={(_, inputData) => setMetadataJson(inputData.value)}
|
onChange={(_, inputData) => setMetadataJson(inputData.value)}
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
{selectedVirtualMachine && (
|
{selectedTarget && (
|
||||||
<Field label="Id">
|
<Field label="Id">
|
||||||
<div className={styles.value}>{selectedVirtualMachine.id}</div>
|
<div className={styles.value}>{selectedTarget.id}</div>
|
||||||
</Field>
|
</Field>
|
||||||
)}
|
)}
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
@@ -256,20 +288,20 @@ export function VirtualMachinesPage() {
|
|||||||
</DialogSurface>
|
</DialogSurface>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
<Dialog open={Boolean(linkVirtualMachineId)} onOpenChange={(_, dialogData) => !dialogData.open && setLinkVirtualMachineId("")}>
|
<Dialog open={Boolean(linkTargetId)} onOpenChange={(_, dialogData) => !dialogData.open && setLinkTargetId("")}>
|
||||||
<DialogSurface>
|
<DialogSurface>
|
||||||
<DialogBody>
|
<DialogBody>
|
||||||
<DialogTitle>Link Virtual Machine to Domain</DialogTitle>
|
<DialogTitle>Link Target to Domain</DialogTitle>
|
||||||
<DialogContent className={styles.form}>
|
<DialogContent className={styles.form}>
|
||||||
<Field label="Virtual Machine" required>
|
<Field label="Target" required>
|
||||||
<Combobox
|
<Combobox
|
||||||
placeholder="Virtual Machine waehlen"
|
placeholder="Target waehlen"
|
||||||
value={(data ?? []).find((virtualMachine) => virtualMachine.id === linkVirtualMachineId)?.name ?? ""}
|
value={(data ?? []).find((target) => target.id === linkTargetId)?.name ?? ""}
|
||||||
onOptionSelect={(_, optionData) => setLinkVirtualMachineId(optionData.optionValue ?? "")}
|
onOptionSelect={(_, optionData) => setLinkTargetId(optionData.optionValue ?? "")}
|
||||||
>
|
>
|
||||||
{unlinkedVirtualMachines.map((virtualMachine) => (
|
{unlinkedTargets.map((target) => (
|
||||||
<Option key={virtualMachine.id} text={virtualMachine.name} value={virtualMachine.id}>
|
<Option key={target.id} text={target.name} value={target.id}>
|
||||||
{virtualMachine.name}
|
{target.name}
|
||||||
</Option>
|
</Option>
|
||||||
))}
|
))}
|
||||||
</Combobox>
|
</Combobox>
|
||||||
@@ -289,13 +321,13 @@ export function VirtualMachinesPage() {
|
|||||||
</Field>
|
</Field>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button appearance="secondary" onClick={() => setLinkVirtualMachineId("")}>
|
<Button appearance="secondary" onClick={() => setLinkTargetId("")}>
|
||||||
Abbrechen
|
Abbrechen
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
appearance="primary"
|
appearance="primary"
|
||||||
disabled={!linkVirtualMachineId || !linkDomainId || linkVirtualMachineToDomain.isPending}
|
disabled={!linkTargetId || !linkDomainId || linkTargetToDomain.isPending}
|
||||||
onClick={() => linkVirtualMachineToDomain.mutate({ virtualMachineId: linkVirtualMachineId, targetDomainId: linkDomainId })}
|
onClick={() => linkTargetToDomain.mutate({ targetId: linkTargetId, targetDomainId: linkDomainId })}
|
||||||
>
|
>
|
||||||
Link
|
Link
|
||||||
</Button>
|
</Button>
|
||||||
@@ -304,33 +336,33 @@ export function VirtualMachinesPage() {
|
|||||||
</DialogSurface>
|
</DialogSurface>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
<Dialog open={Boolean(unlinkVirtualMachineId)} onOpenChange={(_, dialogData) => !dialogData.open && setUnlinkVirtualMachineId("")}>
|
<Dialog open={Boolean(unlinkTargetId)} onOpenChange={(_, dialogData) => !dialogData.open && setUnlinkTargetId("")}>
|
||||||
<DialogSurface>
|
<DialogSurface>
|
||||||
<DialogBody>
|
<DialogBody>
|
||||||
<DialogTitle>Unlink Virtual Machine from Domain</DialogTitle>
|
<DialogTitle>Unlink Target from Domain</DialogTitle>
|
||||||
<DialogContent className={styles.form}>
|
<DialogContent className={styles.form}>
|
||||||
<Field label="Virtual Machine" required>
|
<Field label="Target" required>
|
||||||
<Combobox
|
<Combobox
|
||||||
placeholder="Virtual Machine waehlen"
|
placeholder="Target waehlen"
|
||||||
value={(data ?? []).find((virtualMachine) => virtualMachine.id === unlinkVirtualMachineId)?.name ?? ""}
|
value={(data ?? []).find((target) => target.id === unlinkTargetId)?.name ?? ""}
|
||||||
onOptionSelect={(_, optionData) => setUnlinkVirtualMachineId(optionData.optionValue ?? "")}
|
onOptionSelect={(_, optionData) => setUnlinkTargetId(optionData.optionValue ?? "")}
|
||||||
>
|
>
|
||||||
{linkedVirtualMachines.map((virtualMachine) => (
|
{linkedTargets.map((target) => (
|
||||||
<Option key={virtualMachine.id} text={virtualMachine.name} value={virtualMachine.id}>
|
<Option key={target.id} text={target.name} value={target.id}>
|
||||||
{virtualMachine.name}
|
{target.name}
|
||||||
</Option>
|
</Option>
|
||||||
))}
|
))}
|
||||||
</Combobox>
|
</Combobox>
|
||||||
</Field>
|
</Field>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button appearance="secondary" onClick={() => setUnlinkVirtualMachineId("")}>
|
<Button appearance="secondary" onClick={() => setUnlinkTargetId("")}>
|
||||||
Abbrechen
|
Abbrechen
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
appearance="primary"
|
appearance="primary"
|
||||||
disabled={!unlinkVirtualMachineId || unlinkVirtualMachineFromDomain.isPending}
|
disabled={!unlinkTargetId || unlinkTargetFromDomain.isPending}
|
||||||
onClick={() => unlinkVirtualMachineFromDomain.mutate(unlinkVirtualMachineId)}
|
onClick={() => unlinkTargetFromDomain.mutate(unlinkTargetId)}
|
||||||
>
|
>
|
||||||
Unlink
|
Unlink
|
||||||
</Button>
|
</Button>
|
||||||
@@ -341,13 +373,15 @@ export function VirtualMachinesPage() {
|
|||||||
|
|
||||||
<DataState
|
<DataState
|
||||||
isLoading={isLoading || domainsLoading}
|
isLoading={isLoading || domainsLoading}
|
||||||
error={error ?? domainsError ?? deleteVirtualMachine.error ?? linkVirtualMachineToDomain.error ?? unlinkVirtualMachineFromDomain.error}
|
error={error ?? domainsError ?? deleteTarget.error ?? linkTargetToDomain.error ?? unlinkTargetFromDomain.error}
|
||||||
/>
|
/>
|
||||||
{data && (
|
{data && (
|
||||||
<Table aria-label="Virtual Machines">
|
<Table aria-label="Targets">
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHeaderCell>Name</TableHeaderCell>
|
<TableHeaderCell>Name</TableHeaderCell>
|
||||||
|
<TableHeaderCell>Type</TableHeaderCell>
|
||||||
|
<TableHeaderCell>Provider</TableHeaderCell>
|
||||||
<TableHeaderCell>Domain</TableHeaderCell>
|
<TableHeaderCell>Domain</TableHeaderCell>
|
||||||
<TableHeaderCell>External Id</TableHeaderCell>
|
<TableHeaderCell>External Id</TableHeaderCell>
|
||||||
<TableHeaderCell>Id</TableHeaderCell>
|
<TableHeaderCell>Id</TableHeaderCell>
|
||||||
@@ -355,16 +389,18 @@ export function VirtualMachinesPage() {
|
|||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{data.map((virtualMachine) => (
|
{data.map((target) => (
|
||||||
<TableRow key={virtualMachine.id}>
|
<TableRow key={target.id}>
|
||||||
<TableCell>{virtualMachine.name}</TableCell>
|
<TableCell>{target.name}</TableCell>
|
||||||
<TableCell>{virtualMachine.domainID ? (domainNameById.get(virtualMachine.domainID) ?? virtualMachine.domainID) : "-"}</TableCell>
|
<TableCell>{target.targetType}</TableCell>
|
||||||
<TableCell>{virtualMachine.externalId}</TableCell>
|
<TableCell>{target.providerType}</TableCell>
|
||||||
<TableCell>{virtualMachine.id}</TableCell>
|
<TableCell>{target.domainID ? (domainNameById.get(target.domainID) ?? target.domainID) : "-"}</TableCell>
|
||||||
|
<TableCell>{target.externalId}</TableCell>
|
||||||
|
<TableCell>{target.id}</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<div className={styles.actions}>
|
<div className={styles.actions}>
|
||||||
<Tooltip content="Details" relationship="label">
|
<Tooltip content="Details" relationship="label">
|
||||||
<Link to={`/virtual-machines/${virtualMachine.id}`}>
|
<Link to={`/targets/${target.id}`}>
|
||||||
<Button appearance="subtle" aria-label="Details" icon={<OpenRegular />} />
|
<Button appearance="subtle" aria-label="Details" icon={<OpenRegular />} />
|
||||||
</Link>
|
</Link>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@@ -373,18 +409,18 @@ export function VirtualMachinesPage() {
|
|||||||
appearance="subtle"
|
appearance="subtle"
|
||||||
aria-label="Aendern"
|
aria-label="Aendern"
|
||||||
icon={<EditRegular />}
|
icon={<EditRegular />}
|
||||||
onClick={() => openVirtualMachineDialog("edit", virtualMachine)}
|
onClick={() => openTargetDialog("edit", target)}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip content="Loeschen" relationship="label">
|
<Tooltip content="Loeschen" relationship="label">
|
||||||
<Button
|
<Button
|
||||||
appearance="subtle"
|
appearance="subtle"
|
||||||
aria-label="Loeschen"
|
aria-label="Loeschen"
|
||||||
disabled={deleteVirtualMachine.isPending}
|
disabled={deleteTarget.isPending}
|
||||||
icon={<DeleteRegular />}
|
icon={<DeleteRegular />}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (window.confirm(`Virtual Machine "${virtualMachine.name}" wirklich loeschen?`)) {
|
if (window.confirm(`Target "${target.name}" wirklich loeschen?`)) {
|
||||||
deleteVirtualMachine.mutate(virtualMachine.id);
|
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 {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Checkbox,
|
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 {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Combobox,
|
Combobox,
|
||||||
@@ -582,3 +582,4 @@ export function TemplatesPage() {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,22 +1,29 @@
|
|||||||
export type DeploymentExecution = {
|
export type DeploymentExecution = {
|
||||||
id: string;
|
id: string;
|
||||||
deploymentGroupId?: string;
|
deploymentGroupId?: string;
|
||||||
deploymentBatchId?: string;
|
deploymentBatchId?: string;
|
||||||
virtualMachineId?: string;
|
targetId?: string;
|
||||||
status?: string;
|
status?: string;
|
||||||
jsonData?: string;
|
jsonData?: string;
|
||||||
|
created?: string;
|
||||||
|
modified?: string;
|
||||||
|
target?: {
|
||||||
|
id: string;
|
||||||
|
name?: string;
|
||||||
|
domainID?: string;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AddDeploymentExecution = {
|
export type AddDeploymentExecution = {
|
||||||
deploymentBatchId: string;
|
deploymentBatchId: string;
|
||||||
virtualMachineId: string;
|
targetId: string;
|
||||||
status: string;
|
status: string;
|
||||||
jsonData: string;
|
jsonData: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AddDeploymentRequest = {
|
export type AddDeploymentRequest = {
|
||||||
deploymentBatchId: string;
|
deploymentBatchId: string;
|
||||||
virtualMachineIds: string[];
|
targetIds: string[];
|
||||||
jsonData: string;
|
jsonData: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -36,7 +43,7 @@ export type DeploymentJob = {
|
|||||||
|
|
||||||
export type DeploymentJobTarget = {
|
export type DeploymentJobTarget = {
|
||||||
id: string;
|
id: string;
|
||||||
virtualMachineId: string;
|
targetId: string;
|
||||||
deploymentBatchId: string;
|
deploymentBatchId: string;
|
||||||
templateId: string;
|
templateId: string;
|
||||||
status: string;
|
status: string;
|
||||||
@@ -73,17 +80,117 @@ export type DeploymentJobDetails = {
|
|||||||
|
|
||||||
export type DeploymentBatch = {
|
export type DeploymentBatch = {
|
||||||
id: string;
|
id: string;
|
||||||
|
templateId?: string;
|
||||||
|
deploymentRuleId?: string;
|
||||||
status?: string;
|
status?: string;
|
||||||
|
created?: string;
|
||||||
|
createdBy?: string;
|
||||||
|
modified?: string;
|
||||||
|
modifiedBy?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DeploymentBatchDetails = DeploymentBatch & {
|
export type DeploymentBatchDetails = DeploymentBatch & {
|
||||||
deployments?: DeploymentExecution[];
|
deployments?: DeploymentExecution[];
|
||||||
|
templateSelections?: DeploymentTemplateSelection[];
|
||||||
|
parameterValues?: DeploymentParameterValue[];
|
||||||
|
targetAssignments?: DeploymentTargetAssignment[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AddDeploymentBatch = {
|
export type AddDeploymentBatch = {
|
||||||
templateId: string;
|
templateId: string;
|
||||||
|
deploymentRuleId?: string;
|
||||||
status: 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 = {
|
export type Domain = {
|
||||||
@@ -99,8 +206,8 @@ export type DomainWithEnvironments = Domain & {
|
|||||||
}>;
|
}>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DomainWithVirtualMachines = Domain & {
|
export type DomainWithTargets = Domain & {
|
||||||
virtualMachines?: VirtualMachine[];
|
targets?: Target[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AddDomain = {
|
export type AddDomain = {
|
||||||
@@ -113,6 +220,7 @@ export type EnvironmentItem = {
|
|||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
environmentType?: string;
|
environmentType?: string;
|
||||||
|
hostingType?: string;
|
||||||
providerType?: string;
|
providerType?: string;
|
||||||
tenantId?: string;
|
tenantId?: string;
|
||||||
subscriptionId?: string;
|
subscriptionId?: string;
|
||||||
@@ -128,6 +236,7 @@ export type EnvironmentWithDomains = EnvironmentItem & {
|
|||||||
export type AddEnvironment = {
|
export type AddEnvironment = {
|
||||||
name: string;
|
name: string;
|
||||||
environmentType: string;
|
environmentType: string;
|
||||||
|
hostingType: string;
|
||||||
providerType?: string;
|
providerType?: string;
|
||||||
tenantId?: string;
|
tenantId?: string;
|
||||||
subscriptionId?: string;
|
subscriptionId?: string;
|
||||||
@@ -209,17 +318,22 @@ export type AddTemplateCategory = {
|
|||||||
color?: string;
|
color?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type VirtualMachine = {
|
export type Target = {
|
||||||
id: string;
|
id: string;
|
||||||
domainID?: string;
|
domainID?: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
targetType: string;
|
||||||
|
providerType: string;
|
||||||
externalId?: string;
|
externalId?: string;
|
||||||
metadataJson?: string;
|
metadataJson?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AddVirtualMachine = {
|
export type AddTarget = {
|
||||||
domainID?: string;
|
domainID?: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
targetType: string;
|
||||||
|
providerType: string;
|
||||||
externalId?: string;
|
externalId?: string;
|
||||||
metadataJson?: 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