diff --git a/src/api/httpClient.ts b/src/api/httpClient.ts index 0bb3c91..1c76bc1 100644 --- a/src/api/httpClient.ts +++ b/src/api/httpClient.ts @@ -56,3 +56,63 @@ export async function postJson(path: string, body: TBo return text as TResult; } } + +export async function putJson(path: string, body: TBody): Promise { + 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(path: string): Promise { + 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; + } +} diff --git a/src/api/portalApi.ts b/src/api/portalApi.ts index cb0118e..40a3c59 100644 --- a/src/api/portalApi.ts +++ b/src/api/portalApi.ts @@ -1,14 +1,18 @@ -import { getJson, postJson } from "./httpClient"; +import { deleteJson, getJson, postJson, putJson } from "./httpClient"; import type { AddDeployment, AddDeploymentGroup, AddDomain, AddEnvironment, AddRunbook, + AddService, + AddTemplate, Deployment, DeploymentGroup, Domain, + DomainWithEnvironments, EnvironmentItem, + EnvironmentWithDomains, Runbook, ServiceItem, Template, @@ -17,17 +21,27 @@ import type { export const portalApi = { getDeployments: (signal?: AbortSignal) => getJson("/Deployment", signal), addDeployment: (deployment: AddDeployment) => postJson("/Deployment", deployment), - getDeploymentGroups: (signal?: AbortSignal) => - getJson("/DeploymentGroup", signal), - addDeploymentGroup: (deploymentGroup: AddDeploymentGroup) => - postJson("/DeploymentGroup", deploymentGroup), + getDeploymentGroups: (signal?: AbortSignal) => getJson("/DeploymentGroup", signal), + addDeploymentGroup: (deploymentGroup: AddDeploymentGroup) => postJson("/DeploymentGroup", deploymentGroup), getDomains: (signal?: AbortSignal) => getJson("/Domain", signal), + getDomainEnvironments: (domainId: string, signal?: AbortSignal) => getJson(`/Domain/${domainId}/Environments`, signal), addDomain: (domain: AddDomain) => postJson("/Domain", domain), + updateDomain: (domainId: string, domain: AddDomain) => putJson(`/Domain/${domainId}`, domain), + deleteDomain: (domainId: string) => deleteJson(`/Domain/${domainId}`), + linkDomainToEnvironment: (domainId: string, environmentId: string) => postJson(`/Domain/${domainId}/Environment/${environmentId}`, undefined), getEnvironments: (signal?: AbortSignal) => getJson("/Environment", signal), - addEnvironment: (environment: AddEnvironment) => - postJson("/Environment", environment), + getEnvironmentDomains: (environmentId: string, signal?: AbortSignal) => getJson(`/Environment/${environmentId}/Domains`, signal), + addEnvironment: (environment: AddEnvironment) => postJson("/Environment", environment), + updateEnvironment: (environmentId: string, environment: AddEnvironment) => putJson(`/Environment/${environmentId}`, environment), + deleteEnvironment: (environmentId: string) => deleteJson(`/Environment/${environmentId}`), getRunbooks: (signal?: AbortSignal) => getJson("/Runbook", signal), addRunbook: (runbook: AddRunbook) => postJson("/Runbook", runbook), getTemplates: (signal?: AbortSignal) => getJson("/Template", signal), + addTemplate: (template: AddTemplate) => postJson("/Template", template), + updateTemplate: (templateId: string, template: AddTemplate) => putJson(`/Template/${templateId}`, template), + deleteTemplate: (templateId: string) => deleteJson(`/Template/${templateId}`), getServices: (signal?: AbortSignal) => getJson("/Service", signal), + addService: (service: AddService) => postJson("/Service", service), + updateService: (serviceId: string, service: AddService) => putJson(`/Service/${serviceId}`, service), + deleteService: (serviceId: string) => deleteJson(`/Service/${serviceId}`), }; diff --git a/src/layout/AppShell.tsx b/src/layout/AppShell.tsx index ed11369..1cea4ce 100644 --- a/src/layout/AppShell.tsx +++ b/src/layout/AppShell.tsx @@ -12,6 +12,7 @@ import { BoxMultiple24Regular, CloudFlow24Regular, DatabasePlugConnectedRegular, + LinkMultiple24Regular, Globe24Regular, Home24Regular, PlayCircle24Regular, @@ -71,11 +72,12 @@ const useStyles = makeStyles({ const links = [ { to: "/", label: "Dashboard", icon: }, - { to: "/deployments", label: "Deployments", icon: }, - { to: "/deployment-groups", label: "Deployment Groups", icon: }, + //{ to: "/deployments", label: "Deployments", icon: }, + //{ to: "/deployment-groups", label: "Deployment Groups", icon: }, { to: "/domains", label: "Domains", icon: }, + { to: "/environment-domains", label: "Environment Domains", icon: }, { to: "/environments", label: "Environments", icon: }, - { to: "/runbooks", label: "Runbooks", icon: }, + //{ to: "/runbooks", label: "Runbooks", icon: }, { to: "/templates", label: "Templates", icon: }, { to: "/services", label: "Services", icon: }, ]; diff --git a/src/main.tsx b/src/main.tsx index abb363e..c71e3fa 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -7,7 +7,10 @@ import { AppShell } from "./layout/AppShell"; import { DashboardPage } from "./pages/DashboardPage"; import { DeploymentsPage } from "./pages/DeploymentsPage"; import { DeploymentGroupsPage } from "./pages/DeploymentGroupsPage"; +import { DomainDetailsPage } from "./pages/DomainDetailsPage"; import { DomainsPage } from "./pages/DomainsPage"; +import { EnvironmentDomainsPage } from "./pages/EnvironmentDomainsPage"; +import { EnvironmentDetailsPage } from "./pages/EnvironmentDetailsPage"; import { EnvironmentsPage } from "./pages/EnvironmentsPage"; import { RunbooksPage } from "./pages/RunbooksPage"; import { TemplatesPage } from "./pages/TemplatesPage"; @@ -32,7 +35,10 @@ const router = createBrowserRouter([ { path: "deployments", element: }, { path: "deployment-groups", element: }, { path: "domains", element: }, + { path: "domains/:id", element: }, + { path: "environment-domains", element: }, { path: "environments", element: }, + { path: "environments/:id", element: }, { path: "runbooks", element: }, { path: "templates", element: }, { path: "services", element: }, diff --git a/src/pages/DashboardPage.tsx b/src/pages/DashboardPage.tsx index 16e5601..7968a09 100644 --- a/src/pages/DashboardPage.tsx +++ b/src/pages/DashboardPage.tsx @@ -25,9 +25,9 @@ const useStyles = makeStyles({ export function DashboardPage() { const styles = useStyles(); - const deployments = useQuery({ - queryKey: ["deployments"], - queryFn: ({ signal }) => portalApi.getDeployments(signal), + const domains = useQuery({ + queryKey: ["domains"], + queryFn: ({ signal }) => portalApi.getDomains(signal), }); const environments = useQuery({ queryKey: ["environments"], @@ -42,9 +42,9 @@ export function DashboardPage() { 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 = - deployments.isLoading || environments.isLoading || templates.isLoading || services.isLoading; + domains.isLoading || environments.isLoading || templates.isLoading || services.isLoading; return ( <> @@ -52,7 +52,7 @@ export function DashboardPage() { {!isLoading && !error && (
- + diff --git a/src/pages/DomainDetailsPage.tsx b/src/pages/DomainDetailsPage.tsx new file mode 100644 index 0000000..7b362d7 --- /dev/null +++ b/src/pages/DomainDetailsPage.tsx @@ -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 ( + <> + + + + + + {data && ( + <> +
+
+ Name + + {data.name} + +
+
+ FQDN + + {data.fqdn} + +
+
+ NetBIOS + + {data.netBIOS} + +
+
+ Id + + {data.id} + +
+
+ + Linked Environments + {links.length === 0 ? ( + + Diese Domain ist noch mit keinem Environment verknuepft. + + ) : ( + + + + Name + Id + Status + Details + + + + {links.map((link) => ( + + {link.environment!.name} + {link.environment!.id} + + + Linked + + + + Oeffnen + + + ))} + +
+ )} + + )} + + ); +} diff --git a/src/pages/DomainsPage.tsx b/src/pages/DomainsPage.tsx index 6427391..cac46c1 100644 --- a/src/pages/DomainsPage.tsx +++ b/src/pages/DomainsPage.tsx @@ -1,23 +1,56 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { Button, + Dialog, + DialogActions, + DialogBody, + DialogContent, + DialogSurface, + DialogTitle, Field, Input, + makeStyles, + shorthands, Table, TableBody, TableCell, TableHeader, TableHeaderCell, TableRow, + Tooltip, } from "@fluentui/react-components"; +import { AddRegular, DeleteRegular, EditRegular, OpenRegular } from "@fluentui/react-icons"; import { useState } from "react"; +import { Link } from "react-router-dom"; import { portalApi } from "../api/portalApi"; import { DataState } from "../components/DataState"; -import { FormActions, FormGrid, FormSection } from "../components/FormSection"; import { PageHeader } from "../components/PageHeader"; +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() { + const styles = useStyles(); const queryClient = useQueryClient(); + const [dialogMode, setDialogMode] = useState(null); + const [selectedDomain, setSelectedDomain] = useState(null); const [name, setName] = useState(""); const [fqdn, setFqdn] = useState(""); const [netBIOS, setNetBIOS] = useState(""); @@ -25,47 +58,109 @@ export function DomainsPage() { queryKey: ["domains"], 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({ mutationFn: portalApi.addDomain, onSuccess: async () => { - setName(""); - setFqdn(""); - setNetBIOS(""); + closeDialog(); 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) => { + 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 ( <> - { - event.preventDefault(); - addDomain.mutate({ fqdn, name, netBIOS }); - }} - > - - - setName(data.value)} /> - - - setFqdn(data.value)} /> - - - setNetBIOS(data.value)} /> - - - - - - - +
+ +
+ + !data.open && closeDialog()}> + +
+ + {dialogMode === "edit" ? "Domain aendern" : "Domain hinzufuegen"} + + + setName(data.value)} /> + + + setFqdn(data.value)} /> + + + setNetBIOS(data.value)} /> + + + + + + + +
+
+
+ + {data && ( @@ -74,6 +169,7 @@ export function DomainsPage() { FQDN NetBIOS Id + Aktionen @@ -83,6 +179,36 @@ export function DomainsPage() { {domain.fqdn} {domain.netBIOS} {domain.id} + +
+ + +
+
))}
diff --git a/src/pages/EnvironmentDetailsPage.tsx b/src/pages/EnvironmentDetailsPage.tsx new file mode 100644 index 0000000..6af8640 --- /dev/null +++ b/src/pages/EnvironmentDetailsPage.tsx @@ -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 ( + <> + + + + + + {data && ( + <> +
+
+ Name + + {data.name} + +
+
+ Id + + {data.id} + +
+
+ + Linked Domains + {links.length === 0 ? ( + + Dieses Environment ist noch mit keiner Domain verknuepft. + + ) : ( +
+ + + Name + FQDN + NetBIOS + Status + Details + + + + {links.map((link) => ( + + {link.domain!.name} + {link.domain!.fqdn} + {link.domain!.netBIOS} + + + Linked + + + + Oeffnen + + + ))} + +
+ )} + + )} + + ); +} diff --git a/src/pages/EnvironmentDomainsPage.tsx b/src/pages/EnvironmentDomainsPage.tsx new file mode 100644 index 0000000..a144720 --- /dev/null +++ b/src/pages/EnvironmentDomainsPage.tsx @@ -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 ( + <> + + { + event.preventDefault(); + linkMutation.mutate(); + }} + > + + + setEnvironmentId(data.optionValue ?? "")} + placeholder="Select environment" + value={selectedEnvironment?.name ?? ""} + > + {environments.data?.map((environment) => ( + + ))} + + + + setDomainId(data.optionValue ?? "")} + placeholder="Select domain" + value={selectedDomain?.name ?? ""} + > + {domains.data?.map((domain) => ( + + ))} + + + + + + + + + {linkMutation.isSuccess && ( + + Domain wurde mit dem Environment verknuepft. + + )} + + ); +} diff --git a/src/pages/EnvironmentsPage.tsx b/src/pages/EnvironmentsPage.tsx index 2440890..86e20d4 100644 --- a/src/pages/EnvironmentsPage.tsx +++ b/src/pages/EnvironmentsPage.tsx @@ -1,71 +1,198 @@ -import { useQuery } from "@tanstack/react-query"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { Button, + Dialog, + DialogActions, + DialogBody, + DialogContent, + DialogSurface, + DialogTitle, Field, Input, + makeStyles, + shorthands, Table, TableBody, TableCell, TableHeader, TableHeaderCell, TableRow, + Tooltip, } from "@fluentui/react-components"; +import { AddRegular, DeleteRegular, EditRegular, OpenRegular } from "@fluentui/react-icons"; import { useState } from "react"; -import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { Link } from "react-router-dom"; import { portalApi } from "../api/portalApi"; import { DataState } from "../components/DataState"; -import { FormActions, FormGrid, FormSection } from "../components/FormSection"; import { PageHeader } from "../components/PageHeader"; +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() { + const styles = useStyles(); const queryClient = useQueryClient(); + const [dialogMode, setDialogMode] = useState(null); + const [selectedEnvironment, setSelectedEnvironment] = useState(null); const [name, setName] = useState(""); const { data, error, isLoading } = useQuery({ queryKey: ["environments"], 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({ mutationFn: portalApi.addEnvironment, onSuccess: async () => { - setName(""); + closeDialog(); 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) => { + 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 ( <> - { - event.preventDefault(); - addEnvironment.mutate({ name }); - }} - > - - - setName(data.value)} /> - - - - - - - +
+ +
+ + !data.open && closeDialog()}> + +
+ + + {dialogMode === "edit" ? "Environment aendern" : "Environment hinzufuegen"} + + + + setName(data.value)} /> + + + + + + + +
+
+
+ + {data && ( - - Name - Id - + + Name + Id + Aktionen + {data.map((environment) => ( {environment.name} {environment.id} + +
+ + +
+
))}
diff --git a/src/pages/ServicesPage.tsx b/src/pages/ServicesPage.tsx index 0e56b70..3bd70af 100644 --- a/src/pages/ServicesPage.tsx +++ b/src/pages/ServicesPage.tsx @@ -1,39 +1,230 @@ -import { useQuery } from "@tanstack/react-query"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { + Button, + Dialog, + DialogActions, + DialogBody, + DialogContent, + DialogSurface, + DialogTitle, + Field, + Input, + makeStyles, + shorthands, Table, TableBody, TableCell, TableHeader, TableHeaderCell, TableRow, + Textarea, + Tooltip, } from "@fluentui/react-components"; +import { AddRegular, DeleteRegular, EditRegular, OpenRegular } from "@fluentui/react-icons"; +import { useState } from "react"; import { portalApi } from "../api/portalApi"; import { DataState } from "../components/DataState"; 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() { + const styles = useStyles(); + const queryClient = useQueryClient(); + const [dialogMode, setDialogMode] = useState(null); + const [selectedService, setSelectedService] = useState(null); + const [name, setName] = useState(""); + const [description, setDescription] = useState(""); const { data, error, isLoading } = useQuery({ queryKey: ["services"], 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) => { + 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 ( <> - +
+ +
+ + !data.open && closeDialog()}> + +
+ + + {dialogMode === "edit" + ? "Service aendern" + : dialogMode === "details" + ? "Service Details" + : "Service hinzufuegen"} + + + + setName(data.value)} /> + + +