feat: Enhance Domains, Environments, Services, and Templates management
- Implemented dialog-based forms for adding and editing Domains and Environments. - Added delete functionality for Domains and Environments with confirmation prompts. - Introduced EnvironmentDetailsPage to display details of selected environments and their linked domains. - Created EnvironmentDomainsPage for linking domains to environments. - Enhanced ServicesPage with dialog support for adding, editing, and viewing service details. - Updated TemplatesPage to manage templates with comprehensive form fields and validation. - Improved type definitions in portal.ts to support new features and ensure type safety.
This commit is contained in:
@@ -56,3 +56,63 @@ export async function postJson<TBody, TResult = unknown>(path: string, body: TBo
|
|||||||
return text as TResult;
|
return text as TResult;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function putJson<TBody, TResult = unknown>(path: string, body: TBody): Promise<TResult> {
|
||||||
|
const response = await fetch(`${apiBaseUrl}${path}`, {
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
credentials: "include",
|
||||||
|
headers: {
|
||||||
|
Accept: "application/json",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
method: "PUT",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const message = await response.text();
|
||||||
|
throw new ApiError(
|
||||||
|
message || `API request failed: ${response.status} ${response.statusText}`,
|
||||||
|
response.status,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = await response.text();
|
||||||
|
if (!text) {
|
||||||
|
return undefined as TResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return JSON.parse(text) as TResult;
|
||||||
|
} catch {
|
||||||
|
return text as TResult;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteJson<TResult = unknown>(path: string): Promise<TResult> {
|
||||||
|
const response = await fetch(`${apiBaseUrl}${path}`, {
|
||||||
|
credentials: "include",
|
||||||
|
headers: {
|
||||||
|
Accept: "application/json",
|
||||||
|
},
|
||||||
|
method: "DELETE",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const message = await response.text();
|
||||||
|
throw new ApiError(
|
||||||
|
message || `API request failed: ${response.status} ${response.statusText}`,
|
||||||
|
response.status,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = await response.text();
|
||||||
|
if (!text) {
|
||||||
|
return undefined as TResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return JSON.parse(text) as TResult;
|
||||||
|
} catch {
|
||||||
|
return text as TResult;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,14 +1,18 @@
|
|||||||
import { getJson, postJson } from "./httpClient";
|
import { deleteJson, getJson, postJson, putJson } from "./httpClient";
|
||||||
import type {
|
import type {
|
||||||
AddDeployment,
|
AddDeployment,
|
||||||
AddDeploymentGroup,
|
AddDeploymentGroup,
|
||||||
AddDomain,
|
AddDomain,
|
||||||
AddEnvironment,
|
AddEnvironment,
|
||||||
AddRunbook,
|
AddRunbook,
|
||||||
|
AddService,
|
||||||
|
AddTemplate,
|
||||||
Deployment,
|
Deployment,
|
||||||
DeploymentGroup,
|
DeploymentGroup,
|
||||||
Domain,
|
Domain,
|
||||||
|
DomainWithEnvironments,
|
||||||
EnvironmentItem,
|
EnvironmentItem,
|
||||||
|
EnvironmentWithDomains,
|
||||||
Runbook,
|
Runbook,
|
||||||
ServiceItem,
|
ServiceItem,
|
||||||
Template,
|
Template,
|
||||||
@@ -17,17 +21,27 @@ import type {
|
|||||||
export const portalApi = {
|
export const portalApi = {
|
||||||
getDeployments: (signal?: AbortSignal) => getJson<Deployment[]>("/Deployment", signal),
|
getDeployments: (signal?: AbortSignal) => getJson<Deployment[]>("/Deployment", signal),
|
||||||
addDeployment: (deployment: AddDeployment) => postJson<AddDeployment, string>("/Deployment", deployment),
|
addDeployment: (deployment: AddDeployment) => postJson<AddDeployment, string>("/Deployment", deployment),
|
||||||
getDeploymentGroups: (signal?: AbortSignal) =>
|
getDeploymentGroups: (signal?: AbortSignal) => getJson<DeploymentGroup[]>("/DeploymentGroup", signal),
|
||||||
getJson<DeploymentGroup[]>("/DeploymentGroup", signal),
|
addDeploymentGroup: (deploymentGroup: AddDeploymentGroup) => postJson<AddDeploymentGroup, string>("/DeploymentGroup", deploymentGroup),
|
||||||
addDeploymentGroup: (deploymentGroup: AddDeploymentGroup) =>
|
|
||||||
postJson<AddDeploymentGroup, string>("/DeploymentGroup", deploymentGroup),
|
|
||||||
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),
|
||||||
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),
|
||||||
|
deleteDomain: (domainId: string) => deleteJson<void>(`/Domain/${domainId}`),
|
||||||
|
linkDomainToEnvironment: (domainId: string, environmentId: string) => postJson<undefined, string>(`/Domain/${domainId}/Environment/${environmentId}`, undefined),
|
||||||
getEnvironments: (signal?: AbortSignal) => getJson<EnvironmentItem[]>("/Environment", signal),
|
getEnvironments: (signal?: AbortSignal) => getJson<EnvironmentItem[]>("/Environment", signal),
|
||||||
addEnvironment: (environment: AddEnvironment) =>
|
getEnvironmentDomains: (environmentId: string, signal?: AbortSignal) => getJson<EnvironmentWithDomains>(`/Environment/${environmentId}/Domains`, signal),
|
||||||
postJson<AddEnvironment, string>("/Environment", environment),
|
addEnvironment: (environment: AddEnvironment) => postJson<AddEnvironment, string>("/Environment", environment),
|
||||||
|
updateEnvironment: (environmentId: string, environment: AddEnvironment) => putJson<AddEnvironment, void>(`/Environment/${environmentId}`, environment),
|
||||||
|
deleteEnvironment: (environmentId: string) => deleteJson<void>(`/Environment/${environmentId}`),
|
||||||
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),
|
||||||
|
addTemplate: (template: AddTemplate) => postJson<AddTemplate, string>("/Template", template),
|
||||||
|
updateTemplate: (templateId: string, template: AddTemplate) => putJson<AddTemplate, void>(`/Template/${templateId}`, template),
|
||||||
|
deleteTemplate: (templateId: string) => deleteJson<void>(`/Template/${templateId}`),
|
||||||
getServices: (signal?: AbortSignal) => getJson<ServiceItem[]>("/Service", signal),
|
getServices: (signal?: AbortSignal) => getJson<ServiceItem[]>("/Service", signal),
|
||||||
|
addService: (service: AddService) => postJson<AddService, string>("/Service", service),
|
||||||
|
updateService: (serviceId: string, service: AddService) => putJson<AddService, void>(`/Service/${serviceId}`, service),
|
||||||
|
deleteService: (serviceId: string) => deleteJson<void>(`/Service/${serviceId}`),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
BoxMultiple24Regular,
|
BoxMultiple24Regular,
|
||||||
CloudFlow24Regular,
|
CloudFlow24Regular,
|
||||||
DatabasePlugConnectedRegular,
|
DatabasePlugConnectedRegular,
|
||||||
|
LinkMultiple24Regular,
|
||||||
Globe24Regular,
|
Globe24Regular,
|
||||||
Home24Regular,
|
Home24Regular,
|
||||||
PlayCircle24Regular,
|
PlayCircle24Regular,
|
||||||
@@ -71,11 +72,12 @@ const useStyles = makeStyles({
|
|||||||
|
|
||||||
const links = [
|
const links = [
|
||||||
{ to: "/", label: "Dashboard", icon: <Home24Regular /> },
|
{ to: "/", label: "Dashboard", icon: <Home24Regular /> },
|
||||||
{ to: "/deployments", label: "Deployments", icon: <CloudFlow24Regular /> },
|
//{ to: "/deployments", label: "Deployments", icon: <CloudFlow24Regular /> },
|
||||||
{ to: "/deployment-groups", label: "Deployment Groups", icon: <ServerMultipleRegular /> },
|
//{ to: "/deployment-groups", label: "Deployment Groups", icon: <ServerMultipleRegular /> },
|
||||||
{ to: "/domains", label: "Domains", icon: <Globe24Regular /> },
|
{ to: "/domains", label: "Domains", icon: <Globe24Regular /> },
|
||||||
|
{ to: "/environment-domains", label: "Environment Domains", icon: <LinkMultiple24Regular /> },
|
||||||
{ to: "/environments", label: "Environments", icon: <DatabasePlugConnectedRegular /> },
|
{ to: "/environments", label: "Environments", icon: <DatabasePlugConnectedRegular /> },
|
||||||
{ to: "/runbooks", label: "Runbooks", icon: <PlayCircle24Regular /> },
|
//{ to: "/runbooks", label: "Runbooks", icon: <PlayCircle24Regular /> },
|
||||||
{ to: "/templates", label: "Templates", icon: <BoxMultiple24Regular /> },
|
{ to: "/templates", label: "Templates", icon: <BoxMultiple24Regular /> },
|
||||||
{ to: "/services", label: "Services", icon: <AppsListDetail24Regular /> },
|
{ to: "/services", label: "Services", icon: <AppsListDetail24Regular /> },
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -7,7 +7,10 @@ import { AppShell } from "./layout/AppShell";
|
|||||||
import { DashboardPage } from "./pages/DashboardPage";
|
import { DashboardPage } from "./pages/DashboardPage";
|
||||||
import { DeploymentsPage } from "./pages/DeploymentsPage";
|
import { DeploymentsPage } from "./pages/DeploymentsPage";
|
||||||
import { DeploymentGroupsPage } from "./pages/DeploymentGroupsPage";
|
import { DeploymentGroupsPage } from "./pages/DeploymentGroupsPage";
|
||||||
|
import { DomainDetailsPage } from "./pages/DomainDetailsPage";
|
||||||
import { DomainsPage } from "./pages/DomainsPage";
|
import { DomainsPage } from "./pages/DomainsPage";
|
||||||
|
import { EnvironmentDomainsPage } from "./pages/EnvironmentDomainsPage";
|
||||||
|
import { EnvironmentDetailsPage } from "./pages/EnvironmentDetailsPage";
|
||||||
import { EnvironmentsPage } from "./pages/EnvironmentsPage";
|
import { EnvironmentsPage } from "./pages/EnvironmentsPage";
|
||||||
import { RunbooksPage } from "./pages/RunbooksPage";
|
import { RunbooksPage } from "./pages/RunbooksPage";
|
||||||
import { TemplatesPage } from "./pages/TemplatesPage";
|
import { TemplatesPage } from "./pages/TemplatesPage";
|
||||||
@@ -32,7 +35,10 @@ const router = createBrowserRouter([
|
|||||||
{ path: "deployments", element: <DeploymentsPage /> },
|
{ path: "deployments", element: <DeploymentsPage /> },
|
||||||
{ path: "deployment-groups", element: <DeploymentGroupsPage /> },
|
{ path: "deployment-groups", element: <DeploymentGroupsPage /> },
|
||||||
{ path: "domains", element: <DomainsPage /> },
|
{ path: "domains", element: <DomainsPage /> },
|
||||||
|
{ path: "domains/:id", element: <DomainDetailsPage /> },
|
||||||
|
{ path: "environment-domains", element: <EnvironmentDomainsPage /> },
|
||||||
{ path: "environments", element: <EnvironmentsPage /> },
|
{ path: "environments", element: <EnvironmentsPage /> },
|
||||||
|
{ path: "environments/:id", element: <EnvironmentDetailsPage /> },
|
||||||
{ path: "runbooks", element: <RunbooksPage /> },
|
{ path: "runbooks", element: <RunbooksPage /> },
|
||||||
{ path: "templates", element: <TemplatesPage /> },
|
{ path: "templates", element: <TemplatesPage /> },
|
||||||
{ path: "services", element: <ServicesPage /> },
|
{ path: "services", element: <ServicesPage /> },
|
||||||
|
|||||||
@@ -25,9 +25,9 @@ const useStyles = makeStyles({
|
|||||||
|
|
||||||
export function DashboardPage() {
|
export function DashboardPage() {
|
||||||
const styles = useStyles();
|
const styles = useStyles();
|
||||||
const deployments = useQuery({
|
const domains = useQuery({
|
||||||
queryKey: ["deployments"],
|
queryKey: ["domains"],
|
||||||
queryFn: ({ signal }) => portalApi.getDeployments(signal),
|
queryFn: ({ signal }) => portalApi.getDomains(signal),
|
||||||
});
|
});
|
||||||
const environments = useQuery({
|
const environments = useQuery({
|
||||||
queryKey: ["environments"],
|
queryKey: ["environments"],
|
||||||
@@ -42,9 +42,9 @@ export function DashboardPage() {
|
|||||||
queryFn: ({ signal }) => portalApi.getServices(signal),
|
queryFn: ({ signal }) => portalApi.getServices(signal),
|
||||||
});
|
});
|
||||||
|
|
||||||
const error = deployments.error ?? environments.error ?? templates.error ?? services.error;
|
const error = domains.error ?? environments.error ?? templates.error ?? services.error;
|
||||||
const isLoading =
|
const isLoading =
|
||||||
deployments.isLoading || environments.isLoading || templates.isLoading || services.isLoading;
|
domains.isLoading || environments.isLoading || templates.isLoading || services.isLoading;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -52,7 +52,7 @@ export function DashboardPage() {
|
|||||||
<DataState isLoading={isLoading} error={error} />
|
<DataState isLoading={isLoading} error={error} />
|
||||||
{!isLoading && !error && (
|
{!isLoading && !error && (
|
||||||
<div className={styles.grid}>
|
<div className={styles.grid}>
|
||||||
<MetricCard label="Deployments" value={deployments.data?.length ?? 0} />
|
<MetricCard label="Domains" value={domains.data?.length ?? 0} />
|
||||||
<MetricCard label="Environments" value={environments.data?.length ?? 0} />
|
<MetricCard label="Environments" value={environments.data?.length ?? 0} />
|
||||||
<MetricCard label="Templates" value={templates.data?.length ?? 0} />
|
<MetricCard label="Templates" value={templates.data?.length ?? 0} />
|
||||||
<MetricCard label="Services" value={services.data?.length ?? 0} />
|
<MetricCard label="Services" value={services.data?.length ?? 0} />
|
||||||
|
|||||||
139
src/pages/DomainDetailsPage.tsx
Normal file
139
src/pages/DomainDetailsPage.tsx
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import {
|
||||||
|
Badge,
|
||||||
|
Button,
|
||||||
|
makeStyles,
|
||||||
|
MessageBar,
|
||||||
|
MessageBarBody,
|
||||||
|
shorthands,
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHeader,
|
||||||
|
TableHeaderCell,
|
||||||
|
TableRow,
|
||||||
|
Text,
|
||||||
|
Title3,
|
||||||
|
tokens,
|
||||||
|
} from "@fluentui/react-components";
|
||||||
|
import { ArrowLeftRegular } from "@fluentui/react-icons";
|
||||||
|
import { Link, useParams } from "react-router-dom";
|
||||||
|
import { portalApi } from "../api/portalApi";
|
||||||
|
import { DataState } from "../components/DataState";
|
||||||
|
import { PageHeader } from "../components/PageHeader";
|
||||||
|
|
||||||
|
const useStyles = makeStyles({
|
||||||
|
backLink: {
|
||||||
|
display: "inline-flex",
|
||||||
|
marginBottom: "18px",
|
||||||
|
textDecorationLine: "none",
|
||||||
|
},
|
||||||
|
details: {
|
||||||
|
display: "grid",
|
||||||
|
gap: "14px",
|
||||||
|
gridTemplateColumns: "repeat(4, minmax(160px, 1fr))",
|
||||||
|
marginBottom: "24px",
|
||||||
|
maxWidth: "980px",
|
||||||
|
...shorthands.border("1px", "solid", tokens.colorNeutralStroke2),
|
||||||
|
...shorthands.borderRadius("8px"),
|
||||||
|
...shorthands.padding("18px"),
|
||||||
|
},
|
||||||
|
field: {
|
||||||
|
display: "grid",
|
||||||
|
gap: "4px",
|
||||||
|
minWidth: 0,
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
overflowWrap: "anywhere",
|
||||||
|
},
|
||||||
|
sectionTitle: {
|
||||||
|
marginBottom: "12px",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export function DomainDetailsPage() {
|
||||||
|
const styles = useStyles();
|
||||||
|
const { id } = useParams();
|
||||||
|
const { data, error, isLoading } = useQuery({
|
||||||
|
enabled: Boolean(id),
|
||||||
|
queryKey: ["domain", id, "environments"],
|
||||||
|
queryFn: ({ signal }) => portalApi.getDomainEnvironments(id!, signal),
|
||||||
|
});
|
||||||
|
const links = data?.environmentDomains?.filter((link) => link.environment) ?? [];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Link className={styles.backLink} to="/domains">
|
||||||
|
<Button appearance="subtle" icon={<ArrowLeftRegular />}>
|
||||||
|
Domains
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
<PageHeader title={data?.name ?? "Domain"} description="Details und verknuepfte Environments." />
|
||||||
|
<DataState isLoading={isLoading} error={error} />
|
||||||
|
{data && (
|
||||||
|
<>
|
||||||
|
<section className={styles.details} aria-label="Domain details">
|
||||||
|
<div className={styles.field}>
|
||||||
|
<Text size={200}>Name</Text>
|
||||||
|
<Text className={styles.value} weight="semibold">
|
||||||
|
{data.name}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<div className={styles.field}>
|
||||||
|
<Text size={200}>FQDN</Text>
|
||||||
|
<Text className={styles.value} weight="semibold">
|
||||||
|
{data.fqdn}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<div className={styles.field}>
|
||||||
|
<Text size={200}>NetBIOS</Text>
|
||||||
|
<Text className={styles.value} weight="semibold">
|
||||||
|
{data.netBIOS}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<div className={styles.field}>
|
||||||
|
<Text size={200}>Id</Text>
|
||||||
|
<Text className={styles.value} weight="semibold">
|
||||||
|
{data.id}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<Title3 className={styles.sectionTitle}>Linked Environments</Title3>
|
||||||
|
{links.length === 0 ? (
|
||||||
|
<MessageBar>
|
||||||
|
<MessageBarBody>Diese Domain ist noch mit keinem Environment verknuepft.</MessageBarBody>
|
||||||
|
</MessageBar>
|
||||||
|
) : (
|
||||||
|
<Table aria-label="Linked environments">
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHeaderCell>Name</TableHeaderCell>
|
||||||
|
<TableHeaderCell>Id</TableHeaderCell>
|
||||||
|
<TableHeaderCell>Status</TableHeaderCell>
|
||||||
|
<TableHeaderCell>Details</TableHeaderCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{links.map((link) => (
|
||||||
|
<TableRow key={link.environment!.id}>
|
||||||
|
<TableCell>{link.environment!.name}</TableCell>
|
||||||
|
<TableCell>{link.environment!.id}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Badge appearance="filled" color="success">
|
||||||
|
Linked
|
||||||
|
</Badge>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Link to={`/environments/${link.environment!.id}`}>Oeffnen</Link>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,23 +1,56 @@
|
|||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
|
Dialog,
|
||||||
|
DialogActions,
|
||||||
|
DialogBody,
|
||||||
|
DialogContent,
|
||||||
|
DialogSurface,
|
||||||
|
DialogTitle,
|
||||||
Field,
|
Field,
|
||||||
Input,
|
Input,
|
||||||
|
makeStyles,
|
||||||
|
shorthands,
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
TableCell,
|
TableCell,
|
||||||
TableHeader,
|
TableHeader,
|
||||||
TableHeaderCell,
|
TableHeaderCell,
|
||||||
TableRow,
|
TableRow,
|
||||||
|
Tooltip,
|
||||||
} from "@fluentui/react-components";
|
} from "@fluentui/react-components";
|
||||||
|
import { AddRegular, DeleteRegular, EditRegular, OpenRegular } from "@fluentui/react-icons";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
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";
|
||||||
|
import type { Domain } from "../types/portal";
|
||||||
|
|
||||||
|
const useStyles = makeStyles({
|
||||||
|
toolbar: {
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "flex-start",
|
||||||
|
marginBottom: "18px",
|
||||||
|
},
|
||||||
|
form: {
|
||||||
|
display: "grid",
|
||||||
|
gap: "14px",
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
display: "flex",
|
||||||
|
gap: "4px",
|
||||||
|
...shorthands.padding("2px", "0"),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
type DialogMode = "add" | "edit" | null;
|
||||||
|
|
||||||
export function DomainsPage() {
|
export function DomainsPage() {
|
||||||
|
const styles = useStyles();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
const [dialogMode, setDialogMode] = useState<DialogMode>(null);
|
||||||
|
const [selectedDomain, setSelectedDomain] = useState<Domain | null>(null);
|
||||||
const [name, setName] = useState("");
|
const [name, setName] = useState("");
|
||||||
const [fqdn, setFqdn] = useState("");
|
const [fqdn, setFqdn] = useState("");
|
||||||
const [netBIOS, setNetBIOS] = useState("");
|
const [netBIOS, setNetBIOS] = useState("");
|
||||||
@@ -25,47 +58,109 @@ export function DomainsPage() {
|
|||||||
queryKey: ["domains"],
|
queryKey: ["domains"],
|
||||||
queryFn: ({ signal }) => portalApi.getDomains(signal),
|
queryFn: ({ signal }) => portalApi.getDomains(signal),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const closeDialog = () => {
|
||||||
|
setDialogMode(null);
|
||||||
|
setSelectedDomain(null);
|
||||||
|
setName("");
|
||||||
|
setFqdn("");
|
||||||
|
setNetBIOS("");
|
||||||
|
};
|
||||||
|
|
||||||
|
const openAddDialog = () => {
|
||||||
|
setSelectedDomain(null);
|
||||||
|
setName("");
|
||||||
|
setFqdn("");
|
||||||
|
setNetBIOS("");
|
||||||
|
setDialogMode("add");
|
||||||
|
};
|
||||||
|
|
||||||
|
const openEditDialog = (domain: Domain) => {
|
||||||
|
setSelectedDomain(domain);
|
||||||
|
setName(domain.name);
|
||||||
|
setFqdn(domain.fqdn);
|
||||||
|
setNetBIOS(domain.netBIOS);
|
||||||
|
setDialogMode("edit");
|
||||||
|
};
|
||||||
|
|
||||||
const addDomain = useMutation({
|
const addDomain = useMutation({
|
||||||
mutationFn: portalApi.addDomain,
|
mutationFn: portalApi.addDomain,
|
||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
setName("");
|
closeDialog();
|
||||||
setFqdn("");
|
|
||||||
setNetBIOS("");
|
|
||||||
await queryClient.invalidateQueries({ queryKey: ["domains"] });
|
await queryClient.invalidateQueries({ queryKey: ["domains"] });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const updateDomain = useMutation({
|
||||||
|
mutationFn: ({ id, domain }: { id: string; domain: { name: string; fqdn: string; netBIOS: string } }) =>
|
||||||
|
portalApi.updateDomain(id, domain),
|
||||||
|
onSuccess: async () => {
|
||||||
|
closeDialog();
|
||||||
|
await queryClient.invalidateQueries({ queryKey: ["domains"] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const deleteDomain = useMutation({
|
||||||
|
mutationFn: portalApi.deleteDomain,
|
||||||
|
onSuccess: async () => {
|
||||||
|
await queryClient.invalidateQueries({ queryKey: ["domains"] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const submitDomain = (event: React.FormEvent<HTMLFormElement>) => {
|
||||||
|
event.preventDefault();
|
||||||
|
const domain = { fqdn, name, netBIOS };
|
||||||
|
|
||||||
|
if (dialogMode === "edit" && selectedDomain) {
|
||||||
|
updateDomain.mutate({ id: selectedDomain.id, domain });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
addDomain.mutate(domain);
|
||||||
|
};
|
||||||
|
|
||||||
|
const formError = addDomain.error?.message ?? updateDomain.error?.message;
|
||||||
|
const isSaving = addDomain.isPending || updateDomain.isPending;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageHeader title="Domains" description="Verfuegbare Active Directory Domaenen." />
|
<PageHeader title="Domains" description="Verfuegbare Active Directory Domaenen." />
|
||||||
<FormSection
|
<div className={styles.toolbar}>
|
||||||
onSubmit={(event) => {
|
<Button appearance="primary" icon={<AddRegular />} onClick={openAddDialog}>
|
||||||
event.preventDefault();
|
Domain hinzufuegen
|
||||||
addDomain.mutate({ fqdn, name, netBIOS });
|
</Button>
|
||||||
}}
|
</div>
|
||||||
>
|
|
||||||
<FormGrid>
|
<Dialog open={dialogMode !== null} onOpenChange={(_, data) => !data.open && closeDialog()}>
|
||||||
<Field label="Name" required>
|
<DialogSurface>
|
||||||
<Input value={name} onChange={(_, data) => setName(data.value)} />
|
<form onSubmit={submitDomain}>
|
||||||
</Field>
|
<DialogBody>
|
||||||
<Field label="FQDN" required>
|
<DialogTitle>{dialogMode === "edit" ? "Domain aendern" : "Domain hinzufuegen"}</DialogTitle>
|
||||||
<Input value={fqdn} onChange={(_, data) => setFqdn(data.value)} />
|
<DialogContent className={styles.form}>
|
||||||
</Field>
|
<Field label="Name" required>
|
||||||
<Field label="NetBIOS" required validationMessage={addDomain.error?.message}>
|
<Input value={name} onChange={(_, data) => setName(data.value)} />
|
||||||
<Input value={netBIOS} onChange={(_, data) => setNetBIOS(data.value)} />
|
</Field>
|
||||||
</Field>
|
<Field label="FQDN" required>
|
||||||
</FormGrid>
|
<Input value={fqdn} onChange={(_, data) => setFqdn(data.value)} />
|
||||||
<FormActions>
|
</Field>
|
||||||
<Button
|
<Field label="NetBIOS" required validationMessage={formError}>
|
||||||
appearance="primary"
|
<Input value={netBIOS} onChange={(_, data) => setNetBIOS(data.value)} />
|
||||||
disabled={!name || !fqdn || !netBIOS || addDomain.isPending}
|
</Field>
|
||||||
type="submit"
|
</DialogContent>
|
||||||
>
|
<DialogActions>
|
||||||
Add domain
|
<Button appearance="secondary" onClick={closeDialog}>
|
||||||
</Button>
|
Abbrechen
|
||||||
</FormActions>
|
</Button>
|
||||||
</FormSection>
|
<Button appearance="primary" disabled={!name || !fqdn || !netBIOS || isSaving} type="submit">
|
||||||
<DataState isLoading={isLoading} error={error} />
|
Speichern
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</DialogBody>
|
||||||
|
</form>
|
||||||
|
</DialogSurface>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
<DataState isLoading={isLoading} error={error ?? deleteDomain.error} />
|
||||||
{data && (
|
{data && (
|
||||||
<Table aria-label="Domains">
|
<Table aria-label="Domains">
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
@@ -74,6 +169,7 @@ export function DomainsPage() {
|
|||||||
<TableHeaderCell>FQDN</TableHeaderCell>
|
<TableHeaderCell>FQDN</TableHeaderCell>
|
||||||
<TableHeaderCell>NetBIOS</TableHeaderCell>
|
<TableHeaderCell>NetBIOS</TableHeaderCell>
|
||||||
<TableHeaderCell>Id</TableHeaderCell>
|
<TableHeaderCell>Id</TableHeaderCell>
|
||||||
|
<TableHeaderCell>Aktionen</TableHeaderCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
@@ -83,6 +179,36 @@ export function DomainsPage() {
|
|||||||
<TableCell>{domain.fqdn}</TableCell>
|
<TableCell>{domain.fqdn}</TableCell>
|
||||||
<TableCell>{domain.netBIOS}</TableCell>
|
<TableCell>{domain.netBIOS}</TableCell>
|
||||||
<TableCell>{domain.id}</TableCell>
|
<TableCell>{domain.id}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<div className={styles.actions}>
|
||||||
|
<Tooltip content="Details" relationship="label">
|
||||||
|
<Link to={`/domains/${domain.id}`}>
|
||||||
|
<Button appearance="subtle" aria-label="Details" icon={<OpenRegular />} />
|
||||||
|
</Link>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip content="Aendern" relationship="label">
|
||||||
|
<Button
|
||||||
|
appearance="subtle"
|
||||||
|
aria-label="Aendern"
|
||||||
|
icon={<EditRegular />}
|
||||||
|
onClick={() => openEditDialog(domain)}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip content="Loeschen" relationship="label">
|
||||||
|
<Button
|
||||||
|
appearance="subtle"
|
||||||
|
aria-label="Loeschen"
|
||||||
|
disabled={deleteDomain.isPending}
|
||||||
|
icon={<DeleteRegular />}
|
||||||
|
onClick={() => {
|
||||||
|
if (window.confirm(`Domain "${domain.name}" wirklich loeschen?`)) {
|
||||||
|
deleteDomain.mutate(domain.id);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
|
|||||||
129
src/pages/EnvironmentDetailsPage.tsx
Normal file
129
src/pages/EnvironmentDetailsPage.tsx
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import {
|
||||||
|
Badge,
|
||||||
|
Button,
|
||||||
|
makeStyles,
|
||||||
|
MessageBar,
|
||||||
|
MessageBarBody,
|
||||||
|
shorthands,
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHeader,
|
||||||
|
TableHeaderCell,
|
||||||
|
TableRow,
|
||||||
|
Text,
|
||||||
|
Title3,
|
||||||
|
tokens,
|
||||||
|
} from "@fluentui/react-components";
|
||||||
|
import { ArrowLeftRegular } from "@fluentui/react-icons";
|
||||||
|
import { Link, useParams } from "react-router-dom";
|
||||||
|
import { portalApi } from "../api/portalApi";
|
||||||
|
import { DataState } from "../components/DataState";
|
||||||
|
import { PageHeader } from "../components/PageHeader";
|
||||||
|
|
||||||
|
const useStyles = makeStyles({
|
||||||
|
backLink: {
|
||||||
|
display: "inline-flex",
|
||||||
|
marginBottom: "18px",
|
||||||
|
textDecorationLine: "none",
|
||||||
|
},
|
||||||
|
details: {
|
||||||
|
display: "grid",
|
||||||
|
gap: "14px",
|
||||||
|
gridTemplateColumns: "repeat(2, minmax(180px, 1fr))",
|
||||||
|
marginBottom: "24px",
|
||||||
|
maxWidth: "720px",
|
||||||
|
...shorthands.border("1px", "solid", tokens.colorNeutralStroke2),
|
||||||
|
...shorthands.borderRadius("8px"),
|
||||||
|
...shorthands.padding("18px"),
|
||||||
|
},
|
||||||
|
field: {
|
||||||
|
display: "grid",
|
||||||
|
gap: "4px",
|
||||||
|
minWidth: 0,
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
overflowWrap: "anywhere",
|
||||||
|
},
|
||||||
|
sectionTitle: {
|
||||||
|
marginBottom: "12px",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export function EnvironmentDetailsPage() {
|
||||||
|
const styles = useStyles();
|
||||||
|
const { id } = useParams();
|
||||||
|
const { data, error, isLoading } = useQuery({
|
||||||
|
enabled: Boolean(id),
|
||||||
|
queryKey: ["environment", id, "domains"],
|
||||||
|
queryFn: ({ signal }) => portalApi.getEnvironmentDomains(id!, signal),
|
||||||
|
});
|
||||||
|
const links = data?.environmentDomains?.filter((link) => link.domain) ?? [];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Link className={styles.backLink} to="/environments">
|
||||||
|
<Button appearance="subtle" icon={<ArrowLeftRegular />}>
|
||||||
|
Environments
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
<PageHeader title={data?.name ?? "Environment"} description="Details und verknuepfte Domains." />
|
||||||
|
<DataState isLoading={isLoading} error={error} />
|
||||||
|
{data && (
|
||||||
|
<>
|
||||||
|
<section className={styles.details} aria-label="Environment details">
|
||||||
|
<div className={styles.field}>
|
||||||
|
<Text size={200}>Name</Text>
|
||||||
|
<Text className={styles.value} weight="semibold">
|
||||||
|
{data.name}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<div className={styles.field}>
|
||||||
|
<Text size={200}>Id</Text>
|
||||||
|
<Text className={styles.value} weight="semibold">
|
||||||
|
{data.id}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<Title3 className={styles.sectionTitle}>Linked Domains</Title3>
|
||||||
|
{links.length === 0 ? (
|
||||||
|
<MessageBar>
|
||||||
|
<MessageBarBody>Dieses Environment ist noch mit keiner Domain verknuepft.</MessageBarBody>
|
||||||
|
</MessageBar>
|
||||||
|
) : (
|
||||||
|
<Table aria-label="Linked domains">
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHeaderCell>Name</TableHeaderCell>
|
||||||
|
<TableHeaderCell>FQDN</TableHeaderCell>
|
||||||
|
<TableHeaderCell>NetBIOS</TableHeaderCell>
|
||||||
|
<TableHeaderCell>Status</TableHeaderCell>
|
||||||
|
<TableHeaderCell>Details</TableHeaderCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{links.map((link) => (
|
||||||
|
<TableRow key={link.domain!.id}>
|
||||||
|
<TableCell>{link.domain!.name}</TableCell>
|
||||||
|
<TableCell>{link.domain!.fqdn}</TableCell>
|
||||||
|
<TableCell>{link.domain!.netBIOS}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Badge appearance="filled" color="success">
|
||||||
|
Linked
|
||||||
|
</Badge>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Link to={`/domains/${link.domain!.id}`}>Oeffnen</Link>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
104
src/pages/EnvironmentDomainsPage.tsx
Normal file
104
src/pages/EnvironmentDomainsPage.tsx
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Combobox,
|
||||||
|
Field,
|
||||||
|
MessageBar,
|
||||||
|
MessageBarBody,
|
||||||
|
Option,
|
||||||
|
} from "@fluentui/react-components";
|
||||||
|
import { useMemo, useState } from "react";
|
||||||
|
import { portalApi } from "../api/portalApi";
|
||||||
|
import { DataState } from "../components/DataState";
|
||||||
|
import { FormActions, FormGrid, FormSection } from "../components/FormSection";
|
||||||
|
import { PageHeader } from "../components/PageHeader";
|
||||||
|
|
||||||
|
export function EnvironmentDomainsPage() {
|
||||||
|
const [domainId, setDomainId] = useState("");
|
||||||
|
const [environmentId, setEnvironmentId] = useState("");
|
||||||
|
|
||||||
|
const domains = useQuery({
|
||||||
|
queryKey: ["domains"],
|
||||||
|
queryFn: ({ signal }) => portalApi.getDomains(signal),
|
||||||
|
});
|
||||||
|
const environments = useQuery({
|
||||||
|
queryKey: ["environments"],
|
||||||
|
queryFn: ({ signal }) => portalApi.getEnvironments(signal),
|
||||||
|
});
|
||||||
|
const linkMutation = useMutation({
|
||||||
|
mutationFn: () => portalApi.linkDomainToEnvironment(domainId, environmentId),
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectedDomain = useMemo(
|
||||||
|
() => domains.data?.find((domain) => domain.id === domainId),
|
||||||
|
[domainId, domains.data],
|
||||||
|
);
|
||||||
|
const selectedEnvironment = useMemo(
|
||||||
|
() => environments.data?.find((environment) => environment.id === environmentId),
|
||||||
|
[environmentId, environments.data],
|
||||||
|
);
|
||||||
|
|
||||||
|
const isLoading = domains.isLoading || environments.isLoading;
|
||||||
|
const error = domains.error ?? environments.error;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PageHeader
|
||||||
|
title="Environment Domains"
|
||||||
|
description="Domains mit Environments verknuepfen."
|
||||||
|
/>
|
||||||
|
<FormSection
|
||||||
|
onSubmit={(event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
linkMutation.mutate();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FormGrid>
|
||||||
|
<Field label="Environment" required>
|
||||||
|
<Combobox
|
||||||
|
disabled={!environments.data?.length}
|
||||||
|
onOptionSelect={(_, data) => setEnvironmentId(data.optionValue ?? "")}
|
||||||
|
placeholder="Select environment"
|
||||||
|
value={selectedEnvironment?.name ?? ""}
|
||||||
|
>
|
||||||
|
{environments.data?.map((environment) => (
|
||||||
|
<Option key={environment.id} text={environment.name} value={environment.id}>
|
||||||
|
{environment.name}
|
||||||
|
</Option>
|
||||||
|
))}
|
||||||
|
</Combobox>
|
||||||
|
</Field>
|
||||||
|
<Field label="Domain" required validationMessage={linkMutation.error?.message}>
|
||||||
|
<Combobox
|
||||||
|
disabled={!domains.data?.length}
|
||||||
|
onOptionSelect={(_, data) => setDomainId(data.optionValue ?? "")}
|
||||||
|
placeholder="Select domain"
|
||||||
|
value={selectedDomain?.name ?? ""}
|
||||||
|
>
|
||||||
|
{domains.data?.map((domain) => (
|
||||||
|
<Option key={domain.id} text={domain.name} value={domain.id}>
|
||||||
|
{domain.name} ({domain.fqdn})
|
||||||
|
</Option>
|
||||||
|
))}
|
||||||
|
</Combobox>
|
||||||
|
</Field>
|
||||||
|
</FormGrid>
|
||||||
|
<FormActions>
|
||||||
|
<Button
|
||||||
|
appearance="primary"
|
||||||
|
disabled={!domainId || !environmentId || linkMutation.isPending}
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
Link domain
|
||||||
|
</Button>
|
||||||
|
</FormActions>
|
||||||
|
</FormSection>
|
||||||
|
<DataState isLoading={isLoading} error={error} />
|
||||||
|
{linkMutation.isSuccess && (
|
||||||
|
<MessageBar intent="success">
|
||||||
|
<MessageBarBody>Domain wurde mit dem Environment verknuepft.</MessageBarBody>
|
||||||
|
</MessageBar>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,71 +1,198 @@
|
|||||||
import { useQuery } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
|
Dialog,
|
||||||
|
DialogActions,
|
||||||
|
DialogBody,
|
||||||
|
DialogContent,
|
||||||
|
DialogSurface,
|
||||||
|
DialogTitle,
|
||||||
Field,
|
Field,
|
||||||
Input,
|
Input,
|
||||||
|
makeStyles,
|
||||||
|
shorthands,
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
TableCell,
|
TableCell,
|
||||||
TableHeader,
|
TableHeader,
|
||||||
TableHeaderCell,
|
TableHeaderCell,
|
||||||
TableRow,
|
TableRow,
|
||||||
|
Tooltip,
|
||||||
} from "@fluentui/react-components";
|
} from "@fluentui/react-components";
|
||||||
|
import { AddRegular, DeleteRegular, EditRegular, OpenRegular } from "@fluentui/react-icons";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
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";
|
||||||
|
import type { EnvironmentItem } from "../types/portal";
|
||||||
|
|
||||||
|
const useStyles = makeStyles({
|
||||||
|
toolbar: {
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "flex-start",
|
||||||
|
marginBottom: "18px",
|
||||||
|
},
|
||||||
|
form: {
|
||||||
|
display: "grid",
|
||||||
|
gap: "14px",
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
display: "flex",
|
||||||
|
gap: "4px",
|
||||||
|
...shorthands.padding("2px", "0"),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
type DialogMode = "add" | "edit" | null;
|
||||||
|
|
||||||
export function EnvironmentsPage() {
|
export function EnvironmentsPage() {
|
||||||
|
const styles = useStyles();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
const [dialogMode, setDialogMode] = useState<DialogMode>(null);
|
||||||
|
const [selectedEnvironment, setSelectedEnvironment] = useState<EnvironmentItem | null>(null);
|
||||||
const [name, setName] = useState("");
|
const [name, setName] = useState("");
|
||||||
const { data, error, isLoading } = useQuery({
|
const { data, error, isLoading } = useQuery({
|
||||||
queryKey: ["environments"],
|
queryKey: ["environments"],
|
||||||
queryFn: ({ signal }) => portalApi.getEnvironments(signal),
|
queryFn: ({ signal }) => portalApi.getEnvironments(signal),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const closeDialog = () => {
|
||||||
|
setDialogMode(null);
|
||||||
|
setSelectedEnvironment(null);
|
||||||
|
setName("");
|
||||||
|
};
|
||||||
|
|
||||||
|
const openAddDialog = () => {
|
||||||
|
setSelectedEnvironment(null);
|
||||||
|
setName("");
|
||||||
|
setDialogMode("add");
|
||||||
|
};
|
||||||
|
|
||||||
|
const openEditDialog = (environment: EnvironmentItem) => {
|
||||||
|
setSelectedEnvironment(environment);
|
||||||
|
setName(environment.name);
|
||||||
|
setDialogMode("edit");
|
||||||
|
};
|
||||||
|
|
||||||
const addEnvironment = useMutation({
|
const addEnvironment = useMutation({
|
||||||
mutationFn: portalApi.addEnvironment,
|
mutationFn: portalApi.addEnvironment,
|
||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
setName("");
|
closeDialog();
|
||||||
await queryClient.invalidateQueries({ queryKey: ["environments"] });
|
await queryClient.invalidateQueries({ queryKey: ["environments"] });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const updateEnvironment = useMutation({
|
||||||
|
mutationFn: ({ id, environment }: { id: string; environment: { name: string } }) =>
|
||||||
|
portalApi.updateEnvironment(id, environment),
|
||||||
|
onSuccess: async () => {
|
||||||
|
closeDialog();
|
||||||
|
await queryClient.invalidateQueries({ queryKey: ["environments"] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const deleteEnvironment = useMutation({
|
||||||
|
mutationFn: portalApi.deleteEnvironment,
|
||||||
|
onSuccess: async () => {
|
||||||
|
await queryClient.invalidateQueries({ queryKey: ["environments"] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const submitEnvironment = (event: React.FormEvent<HTMLFormElement>) => {
|
||||||
|
event.preventDefault();
|
||||||
|
const environment = { name };
|
||||||
|
|
||||||
|
if (dialogMode === "edit" && selectedEnvironment) {
|
||||||
|
updateEnvironment.mutate({ id: selectedEnvironment.id, environment });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
addEnvironment.mutate(environment);
|
||||||
|
};
|
||||||
|
|
||||||
|
const formError = addEnvironment.error?.message ?? updateEnvironment.error?.message;
|
||||||
|
const isSaving = addEnvironment.isPending || updateEnvironment.isPending;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageHeader title="Environments" description="Umgebungen und Cloud-Faehigkeit." />
|
<PageHeader title="Environments" description="Umgebungen und Cloud-Faehigkeit." />
|
||||||
<FormSection
|
<div className={styles.toolbar}>
|
||||||
onSubmit={(event) => {
|
<Button appearance="primary" icon={<AddRegular />} onClick={openAddDialog}>
|
||||||
event.preventDefault();
|
Environment hinzufuegen
|
||||||
addEnvironment.mutate({ name });
|
</Button>
|
||||||
}}
|
</div>
|
||||||
>
|
|
||||||
<FormGrid>
|
<Dialog open={dialogMode !== null} onOpenChange={(_, data) => !data.open && closeDialog()}>
|
||||||
<Field label="Name" required validationMessage={addEnvironment.error?.message}>
|
<DialogSurface>
|
||||||
<Input value={name} onChange={(_, data) => setName(data.value)} />
|
<form onSubmit={submitEnvironment}>
|
||||||
</Field>
|
<DialogBody>
|
||||||
</FormGrid>
|
<DialogTitle>
|
||||||
<FormActions>
|
{dialogMode === "edit" ? "Environment aendern" : "Environment hinzufuegen"}
|
||||||
<Button appearance="primary" disabled={!name || addEnvironment.isPending} type="submit">
|
</DialogTitle>
|
||||||
Add environment
|
<DialogContent className={styles.form}>
|
||||||
</Button>
|
<Field label="Name" required validationMessage={formError}>
|
||||||
</FormActions>
|
<Input value={name} onChange={(_, data) => setName(data.value)} />
|
||||||
</FormSection>
|
</Field>
|
||||||
<DataState isLoading={isLoading} error={error} />
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button appearance="secondary" onClick={closeDialog}>
|
||||||
|
Abbrechen
|
||||||
|
</Button>
|
||||||
|
<Button appearance="primary" disabled={!name || isSaving} type="submit">
|
||||||
|
Speichern
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</DialogBody>
|
||||||
|
</form>
|
||||||
|
</DialogSurface>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
<DataState isLoading={isLoading} error={error ?? deleteEnvironment.error} />
|
||||||
{data && (
|
{data && (
|
||||||
<Table aria-label="Environments">
|
<Table aria-label="Environments">
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHeaderCell>Name</TableHeaderCell>
|
<TableHeaderCell>Name</TableHeaderCell>
|
||||||
<TableHeaderCell>Id</TableHeaderCell>
|
<TableHeaderCell>Id</TableHeaderCell>
|
||||||
</TableRow>
|
<TableHeaderCell>Aktionen</TableHeaderCell>
|
||||||
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{data.map((environment) => (
|
{data.map((environment) => (
|
||||||
<TableRow key={environment.id}>
|
<TableRow key={environment.id}>
|
||||||
<TableCell>{environment.name}</TableCell>
|
<TableCell>{environment.name}</TableCell>
|
||||||
<TableCell>{environment.id}</TableCell>
|
<TableCell>{environment.id}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<div className={styles.actions}>
|
||||||
|
<Tooltip content="Details" relationship="label">
|
||||||
|
<Link to={`/environments/${environment.id}`}>
|
||||||
|
<Button appearance="subtle" aria-label="Details" icon={<OpenRegular />} />
|
||||||
|
</Link>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip content="Aendern" relationship="label">
|
||||||
|
<Button
|
||||||
|
appearance="subtle"
|
||||||
|
aria-label="Aendern"
|
||||||
|
icon={<EditRegular />}
|
||||||
|
onClick={() => openEditDialog(environment)}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip content="Loeschen" relationship="label">
|
||||||
|
<Button
|
||||||
|
appearance="subtle"
|
||||||
|
aria-label="Loeschen"
|
||||||
|
disabled={deleteEnvironment.isPending}
|
||||||
|
icon={<DeleteRegular />}
|
||||||
|
onClick={() => {
|
||||||
|
if (window.confirm(`Environment "${environment.name}" wirklich loeschen?`)) {
|
||||||
|
deleteEnvironment.mutate(environment.id);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
|
|||||||
@@ -1,39 +1,230 @@
|
|||||||
import { useQuery } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import {
|
import {
|
||||||
|
Button,
|
||||||
|
Dialog,
|
||||||
|
DialogActions,
|
||||||
|
DialogBody,
|
||||||
|
DialogContent,
|
||||||
|
DialogSurface,
|
||||||
|
DialogTitle,
|
||||||
|
Field,
|
||||||
|
Input,
|
||||||
|
makeStyles,
|
||||||
|
shorthands,
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
TableCell,
|
TableCell,
|
||||||
TableHeader,
|
TableHeader,
|
||||||
TableHeaderCell,
|
TableHeaderCell,
|
||||||
TableRow,
|
TableRow,
|
||||||
|
Textarea,
|
||||||
|
Tooltip,
|
||||||
} from "@fluentui/react-components";
|
} from "@fluentui/react-components";
|
||||||
|
import { AddRegular, DeleteRegular, EditRegular, OpenRegular } from "@fluentui/react-icons";
|
||||||
|
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 { ServiceItem } from "../types/portal";
|
||||||
|
|
||||||
|
const useStyles = makeStyles({
|
||||||
|
toolbar: {
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "flex-start",
|
||||||
|
marginBottom: "18px",
|
||||||
|
},
|
||||||
|
form: {
|
||||||
|
display: "grid",
|
||||||
|
gap: "14px",
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
display: "flex",
|
||||||
|
gap: "4px",
|
||||||
|
...shorthands.padding("2px", "0"),
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
overflowWrap: "anywhere",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
type DialogMode = "add" | "edit" | "details" | null;
|
||||||
|
|
||||||
export function ServicesPage() {
|
export function ServicesPage() {
|
||||||
|
const styles = useStyles();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const [dialogMode, setDialogMode] = useState<DialogMode>(null);
|
||||||
|
const [selectedService, setSelectedService] = useState<ServiceItem | null>(null);
|
||||||
|
const [name, setName] = useState("");
|
||||||
|
const [description, setDescription] = useState("");
|
||||||
const { data, error, isLoading } = useQuery({
|
const { data, error, isLoading } = useQuery({
|
||||||
queryKey: ["services"],
|
queryKey: ["services"],
|
||||||
queryFn: ({ signal }) => portalApi.getServices(signal),
|
queryFn: ({ signal }) => portalApi.getServices(signal),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const closeDialog = () => {
|
||||||
|
setDialogMode(null);
|
||||||
|
setSelectedService(null);
|
||||||
|
setName("");
|
||||||
|
setDescription("");
|
||||||
|
};
|
||||||
|
|
||||||
|
const openAddDialog = () => {
|
||||||
|
setName("");
|
||||||
|
setDescription("");
|
||||||
|
setSelectedService(null);
|
||||||
|
setDialogMode("add");
|
||||||
|
};
|
||||||
|
|
||||||
|
const openServiceDialog = (mode: "edit" | "details", service: ServiceItem) => {
|
||||||
|
setSelectedService(service);
|
||||||
|
setName(service.name);
|
||||||
|
setDescription(service.description ?? "");
|
||||||
|
setDialogMode(mode);
|
||||||
|
};
|
||||||
|
|
||||||
|
const addService = useMutation({
|
||||||
|
mutationFn: portalApi.addService,
|
||||||
|
onSuccess: async () => {
|
||||||
|
closeDialog();
|
||||||
|
await queryClient.invalidateQueries({ queryKey: ["services"] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateService = useMutation({
|
||||||
|
mutationFn: ({ id, service }: { id: string; service: { name: string; description: string } }) =>
|
||||||
|
portalApi.updateService(id, service),
|
||||||
|
onSuccess: async () => {
|
||||||
|
closeDialog();
|
||||||
|
await queryClient.invalidateQueries({ queryKey: ["services"] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const deleteService = useMutation({
|
||||||
|
mutationFn: portalApi.deleteService,
|
||||||
|
onSuccess: async () => {
|
||||||
|
await queryClient.invalidateQueries({ queryKey: ["services"] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const submitService = (event: React.FormEvent<HTMLFormElement>) => {
|
||||||
|
event.preventDefault();
|
||||||
|
const service = { description, name };
|
||||||
|
|
||||||
|
if (dialogMode === "edit" && selectedService) {
|
||||||
|
updateService.mutate({ id: selectedService.id, service });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
addService.mutate(service);
|
||||||
|
};
|
||||||
|
|
||||||
|
const formError = addService.error?.message ?? updateService.error?.message;
|
||||||
|
const isSaving = addService.isPending || updateService.isPending;
|
||||||
|
const isDetails = dialogMode === "details";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageHeader title="Services" description="Service-Katalog aus der Core API." />
|
<PageHeader title="Services" description="Service-Katalog aus der Core API." />
|
||||||
<DataState isLoading={isLoading} error={error} />
|
<div className={styles.toolbar}>
|
||||||
|
<Button appearance="primary" icon={<AddRegular />} onClick={openAddDialog}>
|
||||||
|
Service hinzufuegen
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Dialog open={dialogMode !== null} onOpenChange={(_, data) => !data.open && closeDialog()}>
|
||||||
|
<DialogSurface>
|
||||||
|
<form onSubmit={submitService}>
|
||||||
|
<DialogBody>
|
||||||
|
<DialogTitle>
|
||||||
|
{dialogMode === "edit"
|
||||||
|
? "Service aendern"
|
||||||
|
: dialogMode === "details"
|
||||||
|
? "Service Details"
|
||||||
|
: "Service hinzufuegen"}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogContent className={styles.form}>
|
||||||
|
<Field label="Name" required>
|
||||||
|
<Input disabled={isDetails} value={name} onChange={(_, data) => setName(data.value)} />
|
||||||
|
</Field>
|
||||||
|
<Field label="Description" validationMessage={formError}>
|
||||||
|
<Textarea
|
||||||
|
disabled={isDetails}
|
||||||
|
resize="vertical"
|
||||||
|
value={description}
|
||||||
|
onChange={(_, data) => setDescription(data.value)}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
{selectedService && (
|
||||||
|
<Field label="Id">
|
||||||
|
<div className={styles.value}>{selectedService.id}</div>
|
||||||
|
</Field>
|
||||||
|
)}
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button appearance="secondary" onClick={closeDialog}>
|
||||||
|
{isDetails ? "Schliessen" : "Abbrechen"}
|
||||||
|
</Button>
|
||||||
|
{!isDetails && (
|
||||||
|
<Button appearance="primary" disabled={!name || isSaving} type="submit">
|
||||||
|
Speichern
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</DialogActions>
|
||||||
|
</DialogBody>
|
||||||
|
</form>
|
||||||
|
</DialogSurface>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
<DataState isLoading={isLoading} error={error ?? deleteService.error} />
|
||||||
{data && (
|
{data && (
|
||||||
<Table aria-label="Services">
|
<Table aria-label="Services">
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHeaderCell>Name</TableHeaderCell>
|
<TableHeaderCell>Name</TableHeaderCell>
|
||||||
<TableHeaderCell>Id</TableHeaderCell>
|
<TableHeaderCell>Description</TableHeaderCell>
|
||||||
</TableRow>
|
<TableHeaderCell>Id</TableHeaderCell>
|
||||||
|
<TableHeaderCell>Aktionen</TableHeaderCell>
|
||||||
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{data.map((service) => (
|
{data.map((service) => (
|
||||||
<TableRow key={service.id}>
|
<TableRow key={service.id}>
|
||||||
<TableCell>{service.name}</TableCell>
|
<TableCell>{service.name}</TableCell>
|
||||||
|
<TableCell>{service.description}</TableCell>
|
||||||
<TableCell>{service.id}</TableCell>
|
<TableCell>{service.id}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<div className={styles.actions}>
|
||||||
|
<Tooltip content="Details" relationship="label">
|
||||||
|
<Button
|
||||||
|
appearance="subtle"
|
||||||
|
aria-label="Details"
|
||||||
|
icon={<OpenRegular />}
|
||||||
|
onClick={() => openServiceDialog("details", service)}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip content="Aendern" relationship="label">
|
||||||
|
<Button
|
||||||
|
appearance="subtle"
|
||||||
|
aria-label="Aendern"
|
||||||
|
icon={<EditRegular />}
|
||||||
|
onClick={() => openServiceDialog("edit", service)}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip content="Loeschen" relationship="label">
|
||||||
|
<Button
|
||||||
|
appearance="subtle"
|
||||||
|
aria-label="Loeschen"
|
||||||
|
disabled={deleteService.isPending}
|
||||||
|
icon={<DeleteRegular />}
|
||||||
|
onClick={() => {
|
||||||
|
if (window.confirm(`Service "${service.name}" wirklich loeschen?`)) {
|
||||||
|
deleteService.mutate(service.id);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
|
|||||||
@@ -1,39 +1,305 @@
|
|||||||
import { useQuery } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import {
|
import {
|
||||||
|
Button,
|
||||||
|
Checkbox,
|
||||||
|
Dialog,
|
||||||
|
DialogActions,
|
||||||
|
DialogBody,
|
||||||
|
DialogContent,
|
||||||
|
DialogSurface,
|
||||||
|
DialogTitle,
|
||||||
|
Field,
|
||||||
|
Input,
|
||||||
|
makeStyles,
|
||||||
|
shorthands,
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
TableCell,
|
TableCell,
|
||||||
TableHeader,
|
TableHeader,
|
||||||
TableHeaderCell,
|
TableHeaderCell,
|
||||||
TableRow,
|
TableRow,
|
||||||
|
Textarea,
|
||||||
|
Tooltip,
|
||||||
} from "@fluentui/react-components";
|
} from "@fluentui/react-components";
|
||||||
|
import { AddRegular, DeleteRegular, EditRegular, OpenRegular } from "@fluentui/react-icons";
|
||||||
|
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 { Template } from "../types/portal";
|
||||||
|
|
||||||
|
const useStyles = makeStyles({
|
||||||
|
toolbar: {
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "flex-start",
|
||||||
|
marginBottom: "18px",
|
||||||
|
},
|
||||||
|
form: {
|
||||||
|
display: "grid",
|
||||||
|
gap: "14px",
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
display: "grid",
|
||||||
|
gap: "14px",
|
||||||
|
gridTemplateColumns: "repeat(2, minmax(180px, 1fr))",
|
||||||
|
},
|
||||||
|
wide: {
|
||||||
|
gridColumn: "1 / -1",
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
display: "flex",
|
||||||
|
gap: "4px",
|
||||||
|
...shorthands.padding("2px", "0"),
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
overflowWrap: "anywhere",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
type DialogMode = "add" | "edit" | "details" | null;
|
||||||
|
|
||||||
|
function getTemplateJsonData(template: Template) {
|
||||||
|
return template.jsonData ?? template.jSONData ?? "";
|
||||||
|
}
|
||||||
|
|
||||||
export function TemplatesPage() {
|
export function TemplatesPage() {
|
||||||
|
const styles = useStyles();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const [dialogMode, setDialogMode] = useState<DialogMode>(null);
|
||||||
|
const [selectedTemplate, setSelectedTemplate] = useState<Template | null>(null);
|
||||||
|
const [templateCategoryId, setTemplateCategoryId] = useState("");
|
||||||
|
const [name, setName] = useState("");
|
||||||
|
const [cloudTemplate, setCloudTemplate] = useState(false);
|
||||||
|
const [version, setVersion] = useState("");
|
||||||
|
const [description, setDescription] = useState("");
|
||||||
|
const [jsonData, setJsonData] = useState("");
|
||||||
const { data, error, isLoading } = useQuery({
|
const { data, error, isLoading } = useQuery({
|
||||||
queryKey: ["templates"],
|
queryKey: ["templates"],
|
||||||
queryFn: ({ signal }) => portalApi.getTemplates(signal),
|
queryFn: ({ signal }) => portalApi.getTemplates(signal),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const closeDialog = () => {
|
||||||
|
setDialogMode(null);
|
||||||
|
setSelectedTemplate(null);
|
||||||
|
setTemplateCategoryId("");
|
||||||
|
setName("");
|
||||||
|
setCloudTemplate(false);
|
||||||
|
setVersion("");
|
||||||
|
setDescription("");
|
||||||
|
setJsonData("");
|
||||||
|
};
|
||||||
|
|
||||||
|
const openAddDialog = () => {
|
||||||
|
closeDialog();
|
||||||
|
setDialogMode("add");
|
||||||
|
};
|
||||||
|
|
||||||
|
const openTemplateDialog = (mode: "edit" | "details", template: Template) => {
|
||||||
|
setSelectedTemplate(template);
|
||||||
|
setTemplateCategoryId(template.templateCategoryId ?? "");
|
||||||
|
setName(template.name);
|
||||||
|
setCloudTemplate(Boolean(template.cloudTemplate));
|
||||||
|
setVersion(template.version ?? "");
|
||||||
|
setDescription(template.description ?? "");
|
||||||
|
setJsonData(getTemplateJsonData(template));
|
||||||
|
setDialogMode(mode);
|
||||||
|
};
|
||||||
|
|
||||||
|
const addTemplate = useMutation({
|
||||||
|
mutationFn: portalApi.addTemplate,
|
||||||
|
onSuccess: async () => {
|
||||||
|
closeDialog();
|
||||||
|
await queryClient.invalidateQueries({ queryKey: ["templates"] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateTemplate = useMutation({
|
||||||
|
mutationFn: ({
|
||||||
|
id,
|
||||||
|
template,
|
||||||
|
}: {
|
||||||
|
id: string;
|
||||||
|
template: {
|
||||||
|
templateCategoryId: string;
|
||||||
|
name: string;
|
||||||
|
cloudTemplate: boolean;
|
||||||
|
version: string;
|
||||||
|
description: string;
|
||||||
|
jsonData: string;
|
||||||
|
};
|
||||||
|
}) => portalApi.updateTemplate(id, template),
|
||||||
|
onSuccess: async () => {
|
||||||
|
closeDialog();
|
||||||
|
await queryClient.invalidateQueries({ queryKey: ["templates"] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const deleteTemplate = useMutation({
|
||||||
|
mutationFn: portalApi.deleteTemplate,
|
||||||
|
onSuccess: async () => {
|
||||||
|
await queryClient.invalidateQueries({ queryKey: ["templates"] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const submitTemplate = (event: React.FormEvent<HTMLFormElement>) => {
|
||||||
|
event.preventDefault();
|
||||||
|
const template = { cloudTemplate, description, jsonData, name, templateCategoryId, version };
|
||||||
|
|
||||||
|
if (dialogMode === "edit" && selectedTemplate) {
|
||||||
|
updateTemplate.mutate({ id: selectedTemplate.id, template });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
addTemplate.mutate(template);
|
||||||
|
};
|
||||||
|
|
||||||
|
const formError = addTemplate.error?.message ?? updateTemplate.error?.message;
|
||||||
|
const isSaving = addTemplate.isPending || updateTemplate.isPending;
|
||||||
|
const isDetails = dialogMode === "details";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageHeader title="Templates" description="Vorlagen fuer Portal-Bereitstellungen." />
|
<PageHeader title="Templates" description="Vorlagen fuer Portal-Bereitstellungen." />
|
||||||
<DataState isLoading={isLoading} error={error} />
|
<div className={styles.toolbar}>
|
||||||
|
<Button appearance="primary" icon={<AddRegular />} onClick={openAddDialog}>
|
||||||
|
Template hinzufuegen
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Dialog open={dialogMode !== null} onOpenChange={(_, data) => !data.open && closeDialog()}>
|
||||||
|
<DialogSurface>
|
||||||
|
<form onSubmit={submitTemplate}>
|
||||||
|
<DialogBody>
|
||||||
|
<DialogTitle>
|
||||||
|
{dialogMode === "edit"
|
||||||
|
? "Template aendern"
|
||||||
|
: dialogMode === "details"
|
||||||
|
? "Template Details"
|
||||||
|
: "Template hinzufuegen"}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogContent className={styles.form}>
|
||||||
|
<div className={styles.grid}>
|
||||||
|
<Field label="Name" required>
|
||||||
|
<Input disabled={isDetails} value={name} onChange={(_, data) => setName(data.value)} />
|
||||||
|
</Field>
|
||||||
|
<Field label="Version" required>
|
||||||
|
<Input
|
||||||
|
disabled={isDetails}
|
||||||
|
value={version}
|
||||||
|
onChange={(_, data) => setVersion(data.value)}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
<Field className={styles.wide} label="TemplateCategoryId" required validationMessage={formError}>
|
||||||
|
<Input
|
||||||
|
disabled={isDetails}
|
||||||
|
value={templateCategoryId}
|
||||||
|
onChange={(_, data) => setTemplateCategoryId(data.value)}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
<Field className={styles.wide}>
|
||||||
|
<Checkbox
|
||||||
|
checked={cloudTemplate}
|
||||||
|
disabled={isDetails}
|
||||||
|
label="Cloud Template"
|
||||||
|
onChange={(_, data) => setCloudTemplate(Boolean(data.checked))}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
<Field className={styles.wide} label="Description">
|
||||||
|
<Textarea
|
||||||
|
disabled={isDetails}
|
||||||
|
resize="vertical"
|
||||||
|
value={description}
|
||||||
|
onChange={(_, data) => setDescription(data.value)}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
<Field className={styles.wide} label="JSONData" required>
|
||||||
|
<Textarea
|
||||||
|
disabled={isDetails}
|
||||||
|
resize="vertical"
|
||||||
|
value={jsonData}
|
||||||
|
onChange={(_, data) => setJsonData(data.value)}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
{selectedTemplate && (
|
||||||
|
<Field className={styles.wide} label="Id">
|
||||||
|
<div className={styles.value}>{selectedTemplate.id}</div>
|
||||||
|
</Field>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button appearance="secondary" onClick={closeDialog}>
|
||||||
|
{isDetails ? "Schliessen" : "Abbrechen"}
|
||||||
|
</Button>
|
||||||
|
{!isDetails && (
|
||||||
|
<Button
|
||||||
|
appearance="primary"
|
||||||
|
disabled={!name || !templateCategoryId || !version || !jsonData || isSaving}
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
Speichern
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</DialogActions>
|
||||||
|
</DialogBody>
|
||||||
|
</form>
|
||||||
|
</DialogSurface>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
<DataState isLoading={isLoading} error={error ?? deleteTemplate.error} />
|
||||||
{data && (
|
{data && (
|
||||||
<Table aria-label="Templates">
|
<Table aria-label="Templates">
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHeaderCell>Name</TableHeaderCell>
|
<TableHeaderCell>Name</TableHeaderCell>
|
||||||
<TableHeaderCell>Id</TableHeaderCell>
|
<TableHeaderCell>Version</TableHeaderCell>
|
||||||
</TableRow>
|
<TableHeaderCell>Cloud</TableHeaderCell>
|
||||||
|
<TableHeaderCell>TemplateCategoryId</TableHeaderCell>
|
||||||
|
<TableHeaderCell>Id</TableHeaderCell>
|
||||||
|
<TableHeaderCell>Aktionen</TableHeaderCell>
|
||||||
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{data.map((template) => (
|
{data.map((template) => (
|
||||||
<TableRow key={template.id}>
|
<TableRow key={template.id}>
|
||||||
<TableCell>{template.name}</TableCell>
|
<TableCell>{template.name}</TableCell>
|
||||||
|
<TableCell>{template.version}</TableCell>
|
||||||
|
<TableCell>{template.cloudTemplate ? "Ja" : "Nein"}</TableCell>
|
||||||
|
<TableCell>{template.templateCategoryId}</TableCell>
|
||||||
<TableCell>{template.id}</TableCell>
|
<TableCell>{template.id}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<div className={styles.actions}>
|
||||||
|
<Tooltip content="Details" relationship="label">
|
||||||
|
<Button
|
||||||
|
appearance="subtle"
|
||||||
|
aria-label="Details"
|
||||||
|
icon={<OpenRegular />}
|
||||||
|
onClick={() => openTemplateDialog("details", template)}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip content="Aendern" relationship="label">
|
||||||
|
<Button
|
||||||
|
appearance="subtle"
|
||||||
|
aria-label="Aendern"
|
||||||
|
icon={<EditRegular />}
|
||||||
|
onClick={() => openTemplateDialog("edit", template)}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip content="Loeschen" relationship="label">
|
||||||
|
<Button
|
||||||
|
appearance="subtle"
|
||||||
|
aria-label="Loeschen"
|
||||||
|
disabled={deleteTemplate.isPending}
|
||||||
|
icon={<DeleteRegular />}
|
||||||
|
onClick={() => {
|
||||||
|
if (window.confirm(`Template "${template.name}" wirklich loeschen?`)) {
|
||||||
|
deleteTemplate.mutate(template.id);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
|
|||||||
@@ -27,6 +27,12 @@ export type Domain = {
|
|||||||
netBIOS: string;
|
netBIOS: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type DomainWithEnvironments = Domain & {
|
||||||
|
environmentDomains?: Array<{
|
||||||
|
environment?: EnvironmentItem;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
|
||||||
export type AddDomain = {
|
export type AddDomain = {
|
||||||
name: string;
|
name: string;
|
||||||
fqdn: string;
|
fqdn: string;
|
||||||
@@ -38,6 +44,12 @@ export type EnvironmentItem = {
|
|||||||
name: string;
|
name: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type EnvironmentWithDomains = EnvironmentItem & {
|
||||||
|
environmentDomains?: Array<{
|
||||||
|
domain?: Domain;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
|
||||||
export type AddEnvironment = {
|
export type AddEnvironment = {
|
||||||
name: string;
|
name: string;
|
||||||
};
|
};
|
||||||
@@ -56,9 +68,30 @@ export type AddRunbook = {
|
|||||||
export type Template = {
|
export type Template = {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
templateCategoryId?: string;
|
||||||
|
cloudTemplate?: boolean;
|
||||||
|
version?: string;
|
||||||
|
description?: string;
|
||||||
|
jsonData?: string;
|
||||||
|
jSONData?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AddTemplate = {
|
||||||
|
templateCategoryId: string;
|
||||||
|
name: string;
|
||||||
|
cloudTemplate: boolean;
|
||||||
|
version: string;
|
||||||
|
description: string;
|
||||||
|
jsonData: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ServiceItem = {
|
export type ServiceItem = {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AddService = {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user