Initial commit
This commit is contained in:
2
.env.example
Normal file
2
.env.example
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
VITE_API_BASE_URL=/api
|
||||||
|
VITE_API_PROXY_TARGET=https://localhost:7260
|
||||||
9
.gitignore
vendored
Normal file
9
.gitignore
vendored
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
44
README.md
Normal file
44
README.md
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
# Microsoft Self Service Portal Web
|
||||||
|
|
||||||
|
Separate React/TypeScript frontend for `Microsoft.SelfService.Portal.Core.API`.
|
||||||
|
|
||||||
|
## Stack
|
||||||
|
|
||||||
|
- React
|
||||||
|
- TypeScript
|
||||||
|
- Vite
|
||||||
|
- Fluent UI React
|
||||||
|
- TanStack Query
|
||||||
|
- React Router
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
Start the API first:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
cd F:\Projekte\Coding\.Net\Microsoft.SelfService.Portal.Core.API
|
||||||
|
$env:ASPNETCORE_ENVIRONMENT="Development"
|
||||||
|
dotnet run --launch-profile https
|
||||||
|
```
|
||||||
|
|
||||||
|
Then start the frontend:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
npm install
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
The development server proxies `/api` calls to `https://localhost:7260`.
|
||||||
|
Adjust `VITE_API_PROXY_TARGET` in `.env.local` if the API runs on a different port.
|
||||||
|
|
||||||
|
## Project Layout
|
||||||
|
|
||||||
|
```text
|
||||||
|
src/
|
||||||
|
api/ HTTP client and API resource services
|
||||||
|
components/ Shared UI components
|
||||||
|
layout/ App shell and navigation
|
||||||
|
pages/ Route pages
|
||||||
|
styles/ Global styles
|
||||||
|
types/ Shared TypeScript types
|
||||||
|
```
|
||||||
25
eslint.config.js
Normal file
25
eslint.config.js
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import js from "@eslint/js";
|
||||||
|
import globals from "globals";
|
||||||
|
import reactHooks from "eslint-plugin-react-hooks";
|
||||||
|
import reactRefresh from "eslint-plugin-react-refresh";
|
||||||
|
import tseslint from "typescript-eslint";
|
||||||
|
|
||||||
|
export default tseslint.config(
|
||||||
|
{ ignores: ["dist"] },
|
||||||
|
{
|
||||||
|
extends: [js.configs.recommended, ...tseslint.configs.recommended],
|
||||||
|
files: ["**/*.{ts,tsx}"],
|
||||||
|
languageOptions: {
|
||||||
|
ecmaVersion: 2020,
|
||||||
|
globals: globals.browser,
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
"react-hooks": reactHooks,
|
||||||
|
"react-refresh": reactRefresh,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
...reactHooks.configs.recommended.rules,
|
||||||
|
"react-refresh/only-export-components": ["warn", { allowConstantExport: true }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
12
index.html
Normal file
12
index.html
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Self Service Portal</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
5049
package-lock.json
generated
Normal file
5049
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
34
package.json
Normal file
34
package.json
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"name": "microsoft-selfservice-portal-web",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.1.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc -b && vite build",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@fluentui/react-components": "^9.56.0",
|
||||||
|
"@fluentui/react-icons": "^2.0.245",
|
||||||
|
"@tanstack/react-query": "^5.59.16",
|
||||||
|
"react": "^18.3.1",
|
||||||
|
"react-dom": "^18.3.1",
|
||||||
|
"react-router-dom": "^6.27.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^9.13.0",
|
||||||
|
"@types/node": "^25.8.0",
|
||||||
|
"@types/react": "^18.3.12",
|
||||||
|
"@types/react-dom": "^18.3.1",
|
||||||
|
"@vitejs/plugin-react": "^4.3.3",
|
||||||
|
"eslint": "^9.13.0",
|
||||||
|
"eslint-plugin-react-hooks": "^5.0.0",
|
||||||
|
"eslint-plugin-react-refresh": "^0.4.14",
|
||||||
|
"globals": "^15.11.0",
|
||||||
|
"typescript": "^5.6.3",
|
||||||
|
"typescript-eslint": "^8.11.0",
|
||||||
|
"vite": "^5.4.10"
|
||||||
|
}
|
||||||
|
}
|
||||||
58
src/api/httpClient.ts
Normal file
58
src/api/httpClient.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
const apiBaseUrl = import.meta.env.VITE_API_BASE_URL ?? "/api";
|
||||||
|
|
||||||
|
export class ApiError extends Error {
|
||||||
|
constructor(
|
||||||
|
message: string,
|
||||||
|
public readonly status: number,
|
||||||
|
) {
|
||||||
|
super(message);
|
||||||
|
this.name = "ApiError";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getJson<T>(path: string, signal?: AbortSignal): Promise<T> {
|
||||||
|
const response = await fetch(`${apiBaseUrl}${path}`, {
|
||||||
|
credentials: "include",
|
||||||
|
headers: {
|
||||||
|
Accept: "application/json",
|
||||||
|
},
|
||||||
|
signal,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new ApiError(`API request failed: ${response.status} ${response.statusText}`, response.status);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json() as Promise<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function postJson<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: "POST",
|
||||||
|
});
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
33
src/api/portalApi.ts
Normal file
33
src/api/portalApi.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { getJson, postJson } from "./httpClient";
|
||||||
|
import type {
|
||||||
|
AddDeployment,
|
||||||
|
AddDeploymentGroup,
|
||||||
|
AddDomain,
|
||||||
|
AddEnvironment,
|
||||||
|
AddRunbook,
|
||||||
|
Deployment,
|
||||||
|
DeploymentGroup,
|
||||||
|
Domain,
|
||||||
|
EnvironmentItem,
|
||||||
|
Runbook,
|
||||||
|
ServiceItem,
|
||||||
|
Template,
|
||||||
|
} from "../types/portal";
|
||||||
|
|
||||||
|
export const portalApi = {
|
||||||
|
getDeployments: (signal?: AbortSignal) => getJson<Deployment[]>("/Deployment", signal),
|
||||||
|
addDeployment: (deployment: AddDeployment) => postJson<AddDeployment, string>("/Deployment", deployment),
|
||||||
|
getDeploymentGroups: (signal?: AbortSignal) =>
|
||||||
|
getJson<DeploymentGroup[]>("/DeploymentGroup", signal),
|
||||||
|
addDeploymentGroup: (deploymentGroup: AddDeploymentGroup) =>
|
||||||
|
postJson<AddDeploymentGroup, string>("/DeploymentGroup", deploymentGroup),
|
||||||
|
getDomains: (signal?: AbortSignal) => getJson<Domain[]>("/Domain", signal),
|
||||||
|
addDomain: (domain: AddDomain) => postJson<AddDomain, string>("/Domain", domain),
|
||||||
|
getEnvironments: (signal?: AbortSignal) => getJson<EnvironmentItem[]>("/Environment", signal),
|
||||||
|
addEnvironment: (environment: AddEnvironment) =>
|
||||||
|
postJson<AddEnvironment, string>("/Environment", environment),
|
||||||
|
getRunbooks: (signal?: AbortSignal) => getJson<Runbook[]>("/Runbook", signal),
|
||||||
|
addRunbook: (runbook: AddRunbook) => postJson<AddRunbook, string>("/Runbook", runbook),
|
||||||
|
getTemplates: (signal?: AbortSignal) => getJson<Template[]>("/Template", signal),
|
||||||
|
getServices: (signal?: AbortSignal) => getJson<ServiceItem[]>("/Service", signal),
|
||||||
|
};
|
||||||
23
src/components/DataState.tsx
Normal file
23
src/components/DataState.tsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { MessageBar, MessageBarBody, Spinner } from "@fluentui/react-components";
|
||||||
|
|
||||||
|
type DataStateProps = {
|
||||||
|
isLoading: boolean;
|
||||||
|
error: unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function DataState({ isLoading, error }: DataStateProps) {
|
||||||
|
if (isLoading) {
|
||||||
|
return <Spinner label="Daten werden geladen" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
const message = error instanceof Error ? error.message : "Unbekannter Fehler";
|
||||||
|
return (
|
||||||
|
<MessageBar intent="error">
|
||||||
|
<MessageBarBody>{message}</MessageBarBody>
|
||||||
|
</MessageBar>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
60
src/components/FormSection.tsx
Normal file
60
src/components/FormSection.tsx
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import { makeStyles, shorthands, tokens } from "@fluentui/react-components";
|
||||||
|
import type { PropsWithChildren } from "react";
|
||||||
|
|
||||||
|
const useStyles = makeStyles({
|
||||||
|
root: {
|
||||||
|
backgroundColor: tokens.colorNeutralBackground1,
|
||||||
|
display: "grid",
|
||||||
|
gap: "14px",
|
||||||
|
marginBottom: "22px",
|
||||||
|
maxWidth: "760px",
|
||||||
|
...shorthands.border("1px", "solid", tokens.colorNeutralStroke2),
|
||||||
|
...shorthands.borderRadius("8px"),
|
||||||
|
...shorthands.padding("18px"),
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
display: "grid",
|
||||||
|
gap: "14px",
|
||||||
|
gridTemplateColumns: "repeat(2, minmax(220px, 1fr))",
|
||||||
|
},
|
||||||
|
wide: {
|
||||||
|
gridColumn: "1 / -1",
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
display: "flex",
|
||||||
|
gap: "10px",
|
||||||
|
justifyContent: "flex-start",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
type FormSectionProps = PropsWithChildren<{
|
||||||
|
onSubmit: React.FormEventHandler<HTMLFormElement>;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export function FormSection({ children, onSubmit }: FormSectionProps) {
|
||||||
|
const styles = useStyles();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form className={styles.root} onSubmit={onSubmit}>
|
||||||
|
{children}
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FormGrid({ children }: PropsWithChildren) {
|
||||||
|
const styles = useStyles();
|
||||||
|
|
||||||
|
return <div className={styles.grid}>{children}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FormWide({ children }: PropsWithChildren) {
|
||||||
|
const styles = useStyles();
|
||||||
|
|
||||||
|
return <div className={styles.wide}>{children}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FormActions({ children }: PropsWithChildren) {
|
||||||
|
const styles = useStyles();
|
||||||
|
|
||||||
|
return <div className={styles.actions}>{children}</div>;
|
||||||
|
}
|
||||||
25
src/components/PageHeader.tsx
Normal file
25
src/components/PageHeader.tsx
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { makeStyles, Text, Title1 } from "@fluentui/react-components";
|
||||||
|
|
||||||
|
const useStyles = makeStyles({
|
||||||
|
root: {
|
||||||
|
display: "grid",
|
||||||
|
gap: "6px",
|
||||||
|
marginBottom: "24px",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
type PageHeaderProps = {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function PageHeader({ title, description }: PageHeaderProps) {
|
||||||
|
const styles = useStyles();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.root}>
|
||||||
|
<Title1>{title}</Title1>
|
||||||
|
<Text>{description}</Text>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
120
src/layout/AppShell.tsx
Normal file
120
src/layout/AppShell.tsx
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
import { Outlet, NavLink } from "react-router-dom";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
makeStyles,
|
||||||
|
shorthands,
|
||||||
|
Text,
|
||||||
|
Title2,
|
||||||
|
tokens,
|
||||||
|
} from "@fluentui/react-components";
|
||||||
|
import {
|
||||||
|
AppsListDetail24Regular,
|
||||||
|
BoxMultiple24Regular,
|
||||||
|
CloudFlow24Regular,
|
||||||
|
DatabasePlugConnectedRegular,
|
||||||
|
Globe24Regular,
|
||||||
|
Home24Regular,
|
||||||
|
PlayCircle24Regular,
|
||||||
|
ServerMultipleRegular,
|
||||||
|
} from "@fluentui/react-icons";
|
||||||
|
|
||||||
|
const useStyles = makeStyles({
|
||||||
|
root: {
|
||||||
|
minHeight: "100vh",
|
||||||
|
display: "grid",
|
||||||
|
gridTemplateColumns: "280px minmax(0, 1fr)",
|
||||||
|
backgroundColor: tokens.colorNeutralBackground2,
|
||||||
|
color: tokens.colorNeutralForeground1,
|
||||||
|
},
|
||||||
|
sidebar: {
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: "20px",
|
||||||
|
backgroundColor: tokens.colorNeutralBackground1,
|
||||||
|
...shorthands.borderRight("1px", "solid", tokens.colorNeutralStroke2),
|
||||||
|
...shorthands.padding("24px", "18px"),
|
||||||
|
},
|
||||||
|
brand: {
|
||||||
|
display: "grid",
|
||||||
|
gap: "4px",
|
||||||
|
...shorthands.padding("0", "6px"),
|
||||||
|
},
|
||||||
|
nav: {
|
||||||
|
display: "grid",
|
||||||
|
gap: "6px",
|
||||||
|
},
|
||||||
|
navLink: {
|
||||||
|
textDecorationLine: "none",
|
||||||
|
},
|
||||||
|
navButton: {
|
||||||
|
width: "100%",
|
||||||
|
justifyContent: "flex-start",
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
minWidth: 0,
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
},
|
||||||
|
topbar: {
|
||||||
|
height: "64px",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
backgroundColor: tokens.colorNeutralBackground1,
|
||||||
|
...shorthands.borderBottom("1px", "solid", tokens.colorNeutralStroke2),
|
||||||
|
...shorthands.padding("0", "28px"),
|
||||||
|
},
|
||||||
|
main: {
|
||||||
|
...shorthands.padding("28px"),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const links = [
|
||||||
|
{ to: "/", label: "Dashboard", icon: <Home24Regular /> },
|
||||||
|
{ to: "/deployments", label: "Deployments", icon: <CloudFlow24Regular /> },
|
||||||
|
{ to: "/deployment-groups", label: "Deployment Groups", icon: <ServerMultipleRegular /> },
|
||||||
|
{ to: "/domains", label: "Domains", icon: <Globe24Regular /> },
|
||||||
|
{ to: "/environments", label: "Environments", icon: <DatabasePlugConnectedRegular /> },
|
||||||
|
{ to: "/runbooks", label: "Runbooks", icon: <PlayCircle24Regular /> },
|
||||||
|
{ to: "/templates", label: "Templates", icon: <BoxMultiple24Regular /> },
|
||||||
|
{ to: "/services", label: "Services", icon: <AppsListDetail24Regular /> },
|
||||||
|
];
|
||||||
|
|
||||||
|
export function AppShell() {
|
||||||
|
const styles = useStyles();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.root}>
|
||||||
|
<aside className={styles.sidebar}>
|
||||||
|
<div className={styles.brand}>
|
||||||
|
<Title2>Self Service Portal</Title2>
|
||||||
|
<Text size={200}>Core API Frontend</Text>
|
||||||
|
</div>
|
||||||
|
<nav className={styles.nav}>
|
||||||
|
{links.map((link) => (
|
||||||
|
<NavLink className={styles.navLink} end={link.to === "/"} key={link.to} to={link.to}>
|
||||||
|
{({ isActive }) => (
|
||||||
|
<Button
|
||||||
|
appearance={isActive ? "primary" : "subtle"}
|
||||||
|
className={styles.navButton}
|
||||||
|
icon={link.icon}
|
||||||
|
>
|
||||||
|
{link.label}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</NavLink>
|
||||||
|
))}
|
||||||
|
</nav>
|
||||||
|
</aside>
|
||||||
|
<section className={styles.content}>
|
||||||
|
<header className={styles.topbar}>
|
||||||
|
<Text weight="semibold">Portal workspace</Text>
|
||||||
|
<Text size={200}>API: {import.meta.env.VITE_API_BASE_URL ?? "/api"}</Text>
|
||||||
|
</header>
|
||||||
|
<main className={styles.main}>
|
||||||
|
<Outlet />
|
||||||
|
</main>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
51
src/main.tsx
Normal file
51
src/main.tsx
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import React from "react";
|
||||||
|
import ReactDOM from "react-dom/client";
|
||||||
|
import { FluentProvider, webLightTheme } from "@fluentui/react-components";
|
||||||
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
|
import { createBrowserRouter, RouterProvider } from "react-router-dom";
|
||||||
|
import { AppShell } from "./layout/AppShell";
|
||||||
|
import { DashboardPage } from "./pages/DashboardPage";
|
||||||
|
import { DeploymentsPage } from "./pages/DeploymentsPage";
|
||||||
|
import { DeploymentGroupsPage } from "./pages/DeploymentGroupsPage";
|
||||||
|
import { DomainsPage } from "./pages/DomainsPage";
|
||||||
|
import { EnvironmentsPage } from "./pages/EnvironmentsPage";
|
||||||
|
import { RunbooksPage } from "./pages/RunbooksPage";
|
||||||
|
import { TemplatesPage } from "./pages/TemplatesPage";
|
||||||
|
import { ServicesPage } from "./pages/ServicesPage";
|
||||||
|
import "./styles/global.css";
|
||||||
|
|
||||||
|
const queryClient = new QueryClient({
|
||||||
|
defaultOptions: {
|
||||||
|
queries: {
|
||||||
|
staleTime: 30_000,
|
||||||
|
retry: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const router = createBrowserRouter([
|
||||||
|
{
|
||||||
|
path: "/",
|
||||||
|
element: <AppShell />,
|
||||||
|
children: [
|
||||||
|
{ index: true, element: <DashboardPage /> },
|
||||||
|
{ path: "deployments", element: <DeploymentsPage /> },
|
||||||
|
{ path: "deployment-groups", element: <DeploymentGroupsPage /> },
|
||||||
|
{ path: "domains", element: <DomainsPage /> },
|
||||||
|
{ path: "environments", element: <EnvironmentsPage /> },
|
||||||
|
{ path: "runbooks", element: <RunbooksPage /> },
|
||||||
|
{ path: "templates", element: <TemplatesPage /> },
|
||||||
|
{ path: "services", element: <ServicesPage /> },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<FluentProvider theme={webLightTheme}>
|
||||||
|
<QueryClientProvider client={queryClient}>
|
||||||
|
<RouterProvider router={router} />
|
||||||
|
</QueryClientProvider>
|
||||||
|
</FluentProvider>
|
||||||
|
</React.StrictMode>,
|
||||||
|
);
|
||||||
74
src/pages/DashboardPage.tsx
Normal file
74
src/pages/DashboardPage.tsx
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardHeader,
|
||||||
|
makeStyles,
|
||||||
|
Text,
|
||||||
|
tokens,
|
||||||
|
} from "@fluentui/react-components";
|
||||||
|
import { portalApi } from "../api/portalApi";
|
||||||
|
import { DataState } from "../components/DataState";
|
||||||
|
import { PageHeader } from "../components/PageHeader";
|
||||||
|
|
||||||
|
const useStyles = makeStyles({
|
||||||
|
grid: {
|
||||||
|
display: "grid",
|
||||||
|
gridTemplateColumns: "repeat(4, minmax(160px, 1fr))",
|
||||||
|
gap: "16px",
|
||||||
|
},
|
||||||
|
metric: {
|
||||||
|
fontSize: "32px",
|
||||||
|
fontWeight: tokens.fontWeightSemibold,
|
||||||
|
lineHeight: "40px",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export function DashboardPage() {
|
||||||
|
const styles = useStyles();
|
||||||
|
const deployments = useQuery({
|
||||||
|
queryKey: ["deployments"],
|
||||||
|
queryFn: ({ signal }) => portalApi.getDeployments(signal),
|
||||||
|
});
|
||||||
|
const environments = useQuery({
|
||||||
|
queryKey: ["environments"],
|
||||||
|
queryFn: ({ signal }) => portalApi.getEnvironments(signal),
|
||||||
|
});
|
||||||
|
const templates = useQuery({
|
||||||
|
queryKey: ["templates"],
|
||||||
|
queryFn: ({ signal }) => portalApi.getTemplates(signal),
|
||||||
|
});
|
||||||
|
const services = useQuery({
|
||||||
|
queryKey: ["services"],
|
||||||
|
queryFn: ({ signal }) => portalApi.getServices(signal),
|
||||||
|
});
|
||||||
|
|
||||||
|
const error = deployments.error ?? environments.error ?? templates.error ?? services.error;
|
||||||
|
const isLoading =
|
||||||
|
deployments.isLoading || environments.isLoading || templates.isLoading || services.isLoading;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PageHeader title="Dashboard" description="Uebersicht ueber die wichtigsten Portalobjekte." />
|
||||||
|
<DataState isLoading={isLoading} error={error} />
|
||||||
|
{!isLoading && !error && (
|
||||||
|
<div className={styles.grid}>
|
||||||
|
<MetricCard label="Deployments" value={deployments.data?.length ?? 0} />
|
||||||
|
<MetricCard label="Environments" value={environments.data?.length ?? 0} />
|
||||||
|
<MetricCard label="Templates" value={templates.data?.length ?? 0} />
|
||||||
|
<MetricCard label="Services" value={services.data?.length ?? 0} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function MetricCard({ label, value }: { label: string; value: number }) {
|
||||||
|
const styles = useStyles();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader header={<Text weight="semibold">{label}</Text>} />
|
||||||
|
<Text className={styles.metric}>{value}</Text>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
84
src/pages/DeploymentGroupsPage.tsx
Normal file
84
src/pages/DeploymentGroupsPage.tsx
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Field,
|
||||||
|
Input,
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHeader,
|
||||||
|
TableHeaderCell,
|
||||||
|
TableRow,
|
||||||
|
} from "@fluentui/react-components";
|
||||||
|
import { 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 DeploymentGroupsPage() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const [templateId, setTemplateId] = useState("");
|
||||||
|
const [status, setStatus] = useState("New");
|
||||||
|
const { data, error, isLoading } = useQuery({
|
||||||
|
queryKey: ["deploymentGroups"],
|
||||||
|
queryFn: ({ signal }) => portalApi.getDeploymentGroups(signal),
|
||||||
|
});
|
||||||
|
const addDeploymentGroup = useMutation({
|
||||||
|
mutationFn: portalApi.addDeploymentGroup,
|
||||||
|
onSuccess: async () => {
|
||||||
|
setTemplateId("");
|
||||||
|
setStatus("New");
|
||||||
|
await queryClient.invalidateQueries({ queryKey: ["deploymentGroups"] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PageHeader title="Deployment Groups" description="Gruppen von Bereitstellungen je Template." />
|
||||||
|
<FormSection
|
||||||
|
onSubmit={(event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
addDeploymentGroup.mutate({ status, templateId });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FormGrid>
|
||||||
|
<Field label="Template Id" required>
|
||||||
|
<Input value={templateId} onChange={(_, data) => setTemplateId(data.value)} />
|
||||||
|
</Field>
|
||||||
|
<Field label="Status" required validationMessage={addDeploymentGroup.error?.message}>
|
||||||
|
<Input value={status} onChange={(_, data) => setStatus(data.value)} />
|
||||||
|
</Field>
|
||||||
|
</FormGrid>
|
||||||
|
<FormActions>
|
||||||
|
<Button
|
||||||
|
appearance="primary"
|
||||||
|
disabled={!templateId || !status || addDeploymentGroup.isPending}
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
Add deployment group
|
||||||
|
</Button>
|
||||||
|
</FormActions>
|
||||||
|
</FormSection>
|
||||||
|
<DataState isLoading={isLoading} error={error} />
|
||||||
|
{data && (
|
||||||
|
<Table aria-label="Deployment groups">
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHeaderCell>Status</TableHeaderCell>
|
||||||
|
<TableHeaderCell>Id</TableHeaderCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{data.map((deploymentGroup) => (
|
||||||
|
<TableRow key={deploymentGroup.id}>
|
||||||
|
<TableCell>{deploymentGroup.status ?? "Unknown"}</TableCell>
|
||||||
|
<TableCell>{deploymentGroup.id}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
98
src/pages/DeploymentsPage.tsx
Normal file
98
src/pages/DeploymentsPage.tsx
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Field,
|
||||||
|
Input,
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHeader,
|
||||||
|
TableHeaderCell,
|
||||||
|
TableRow,
|
||||||
|
Textarea,
|
||||||
|
} from "@fluentui/react-components";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { portalApi } from "../api/portalApi";
|
||||||
|
import { DataState } from "../components/DataState";
|
||||||
|
import { FormActions, FormGrid, FormSection, FormWide } from "../components/FormSection";
|
||||||
|
import { PageHeader } from "../components/PageHeader";
|
||||||
|
|
||||||
|
export function DeploymentsPage() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const [deploymentGroupId, setDeploymentGroupId] = useState("");
|
||||||
|
const [virtualMachineId, setVirtualMachineId] = useState("");
|
||||||
|
const [status, setStatus] = useState("New");
|
||||||
|
const [jsonData, setJsonData] = useState("{}");
|
||||||
|
const { data, error, isLoading } = useQuery({
|
||||||
|
queryKey: ["deployments"],
|
||||||
|
queryFn: ({ signal }) => portalApi.getDeployments(signal),
|
||||||
|
});
|
||||||
|
const addDeployment = useMutation({
|
||||||
|
mutationFn: portalApi.addDeployment,
|
||||||
|
onSuccess: async () => {
|
||||||
|
setDeploymentGroupId("");
|
||||||
|
setVirtualMachineId("");
|
||||||
|
setStatus("New");
|
||||||
|
setJsonData("{}");
|
||||||
|
await queryClient.invalidateQueries({ queryKey: ["deployments"] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PageHeader title="Deployments" description="Aktuelle Bereitstellungen aus der Core API." />
|
||||||
|
<FormSection
|
||||||
|
onSubmit={(event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
addDeployment.mutate({ deploymentGroupId, jsonData, status, virtualMachineId });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FormGrid>
|
||||||
|
<Field label="Deployment Group Id" required>
|
||||||
|
<Input value={deploymentGroupId} onChange={(_, data) => setDeploymentGroupId(data.value)} />
|
||||||
|
</Field>
|
||||||
|
<Field label="Virtual Machine Id" required>
|
||||||
|
<Input value={virtualMachineId} onChange={(_, data) => setVirtualMachineId(data.value)} />
|
||||||
|
</Field>
|
||||||
|
<Field label="Status" required validationMessage={addDeployment.error?.message}>
|
||||||
|
<Input value={status} onChange={(_, data) => setStatus(data.value)} />
|
||||||
|
</Field>
|
||||||
|
<FormWide>
|
||||||
|
<Field label="JSON data" required>
|
||||||
|
<Textarea value={jsonData} onChange={(_, data) => setJsonData(data.value)} />
|
||||||
|
</Field>
|
||||||
|
</FormWide>
|
||||||
|
</FormGrid>
|
||||||
|
<FormActions>
|
||||||
|
<Button
|
||||||
|
appearance="primary"
|
||||||
|
disabled={!deploymentGroupId || !virtualMachineId || !status || addDeployment.isPending}
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
Add deployment
|
||||||
|
</Button>
|
||||||
|
</FormActions>
|
||||||
|
</FormSection>
|
||||||
|
<DataState isLoading={isLoading} error={error} />
|
||||||
|
{data && (
|
||||||
|
<Table aria-label="Deployments">
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHeaderCell>Status</TableHeaderCell>
|
||||||
|
<TableHeaderCell>Deployment Id</TableHeaderCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{data.map((deployment) => (
|
||||||
|
<TableRow key={deployment.id}>
|
||||||
|
<TableCell>{deployment.status ?? "Unknown"}</TableCell>
|
||||||
|
<TableCell>{deployment.id}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
93
src/pages/DomainsPage.tsx
Normal file
93
src/pages/DomainsPage.tsx
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Field,
|
||||||
|
Input,
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHeader,
|
||||||
|
TableHeaderCell,
|
||||||
|
TableRow,
|
||||||
|
} from "@fluentui/react-components";
|
||||||
|
import { 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 DomainsPage() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const [name, setName] = useState("");
|
||||||
|
const [fqdn, setFqdn] = useState("");
|
||||||
|
const [netBIOS, setNetBIOS] = useState("");
|
||||||
|
const { data, error, isLoading } = useQuery({
|
||||||
|
queryKey: ["domains"],
|
||||||
|
queryFn: ({ signal }) => portalApi.getDomains(signal),
|
||||||
|
});
|
||||||
|
const addDomain = useMutation({
|
||||||
|
mutationFn: portalApi.addDomain,
|
||||||
|
onSuccess: async () => {
|
||||||
|
setName("");
|
||||||
|
setFqdn("");
|
||||||
|
setNetBIOS("");
|
||||||
|
await queryClient.invalidateQueries({ queryKey: ["domains"] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PageHeader title="Domains" description="Verfuegbare Active Directory Domaenen." />
|
||||||
|
<FormSection
|
||||||
|
onSubmit={(event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
addDomain.mutate({ fqdn, name, netBIOS });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FormGrid>
|
||||||
|
<Field label="Name" required>
|
||||||
|
<Input value={name} onChange={(_, data) => setName(data.value)} />
|
||||||
|
</Field>
|
||||||
|
<Field label="FQDN" required>
|
||||||
|
<Input value={fqdn} onChange={(_, data) => setFqdn(data.value)} />
|
||||||
|
</Field>
|
||||||
|
<Field label="NetBIOS" required validationMessage={addDomain.error?.message}>
|
||||||
|
<Input value={netBIOS} onChange={(_, data) => setNetBIOS(data.value)} />
|
||||||
|
</Field>
|
||||||
|
</FormGrid>
|
||||||
|
<FormActions>
|
||||||
|
<Button
|
||||||
|
appearance="primary"
|
||||||
|
disabled={!name || !fqdn || !netBIOS || addDomain.isPending}
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
Add domain
|
||||||
|
</Button>
|
||||||
|
</FormActions>
|
||||||
|
</FormSection>
|
||||||
|
<DataState isLoading={isLoading} error={error} />
|
||||||
|
{data && (
|
||||||
|
<Table aria-label="Domains">
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHeaderCell>Name</TableHeaderCell>
|
||||||
|
<TableHeaderCell>FQDN</TableHeaderCell>
|
||||||
|
<TableHeaderCell>NetBIOS</TableHeaderCell>
|
||||||
|
<TableHeaderCell>Id</TableHeaderCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{data.map((domain) => (
|
||||||
|
<TableRow key={domain.id}>
|
||||||
|
<TableCell>{domain.name}</TableCell>
|
||||||
|
<TableCell>{domain.fqdn}</TableCell>
|
||||||
|
<TableCell>{domain.netBIOS}</TableCell>
|
||||||
|
<TableCell>{domain.id}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
76
src/pages/EnvironmentsPage.tsx
Normal file
76
src/pages/EnvironmentsPage.tsx
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Field,
|
||||||
|
Input,
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHeader,
|
||||||
|
TableHeaderCell,
|
||||||
|
TableRow,
|
||||||
|
} from "@fluentui/react-components";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { portalApi } from "../api/portalApi";
|
||||||
|
import { DataState } from "../components/DataState";
|
||||||
|
import { FormActions, FormGrid, FormSection } from "../components/FormSection";
|
||||||
|
import { PageHeader } from "../components/PageHeader";
|
||||||
|
|
||||||
|
export function EnvironmentsPage() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const [name, setName] = useState("");
|
||||||
|
const { data, error, isLoading } = useQuery({
|
||||||
|
queryKey: ["environments"],
|
||||||
|
queryFn: ({ signal }) => portalApi.getEnvironments(signal),
|
||||||
|
});
|
||||||
|
const addEnvironment = useMutation({
|
||||||
|
mutationFn: portalApi.addEnvironment,
|
||||||
|
onSuccess: async () => {
|
||||||
|
setName("");
|
||||||
|
await queryClient.invalidateQueries({ queryKey: ["environments"] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PageHeader title="Environments" description="Umgebungen und Cloud-Faehigkeit." />
|
||||||
|
<FormSection
|
||||||
|
onSubmit={(event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
addEnvironment.mutate({ name });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FormGrid>
|
||||||
|
<Field label="Name" required validationMessage={addEnvironment.error?.message}>
|
||||||
|
<Input value={name} onChange={(_, data) => setName(data.value)} />
|
||||||
|
</Field>
|
||||||
|
</FormGrid>
|
||||||
|
<FormActions>
|
||||||
|
<Button appearance="primary" disabled={!name || addEnvironment.isPending} type="submit">
|
||||||
|
Add environment
|
||||||
|
</Button>
|
||||||
|
</FormActions>
|
||||||
|
</FormSection>
|
||||||
|
<DataState isLoading={isLoading} error={error} />
|
||||||
|
{data && (
|
||||||
|
<Table aria-label="Environments">
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHeaderCell>Name</TableHeaderCell>
|
||||||
|
<TableHeaderCell>Id</TableHeaderCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{data.map((environment) => (
|
||||||
|
<TableRow key={environment.id}>
|
||||||
|
<TableCell>{environment.name}</TableCell>
|
||||||
|
<TableCell>{environment.id}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
89
src/pages/RunbooksPage.tsx
Normal file
89
src/pages/RunbooksPage.tsx
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Field,
|
||||||
|
Input,
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHeader,
|
||||||
|
TableHeaderCell,
|
||||||
|
TableRow,
|
||||||
|
Textarea,
|
||||||
|
} from "@fluentui/react-components";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { portalApi } from "../api/portalApi";
|
||||||
|
import { DataState } from "../components/DataState";
|
||||||
|
import { FormActions, FormGrid, FormSection, FormWide } from "../components/FormSection";
|
||||||
|
import { PageHeader } from "../components/PageHeader";
|
||||||
|
|
||||||
|
export function RunbooksPage() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const [name, setName] = useState("");
|
||||||
|
const [description, setDescription] = useState("");
|
||||||
|
const { data, error, isLoading } = useQuery({
|
||||||
|
queryKey: ["runbooks"],
|
||||||
|
queryFn: ({ signal }) => portalApi.getRunbooks(signal),
|
||||||
|
});
|
||||||
|
const addRunbook = useMutation({
|
||||||
|
mutationFn: portalApi.addRunbook,
|
||||||
|
onSuccess: async () => {
|
||||||
|
setName("");
|
||||||
|
setDescription("");
|
||||||
|
await queryClient.invalidateQueries({ queryKey: ["runbooks"] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PageHeader title="Runbooks" description="Automatisierungen fuer Portalereignisse." />
|
||||||
|
<FormSection
|
||||||
|
onSubmit={(event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
addRunbook.mutate({ description, name });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FormGrid>
|
||||||
|
<Field label="Name" required>
|
||||||
|
<Input value={name} onChange={(_, data) => setName(data.value)} />
|
||||||
|
</Field>
|
||||||
|
<FormWide>
|
||||||
|
<Field label="Description" required validationMessage={addRunbook.error?.message}>
|
||||||
|
<Textarea value={description} onChange={(_, data) => setDescription(data.value)} />
|
||||||
|
</Field>
|
||||||
|
</FormWide>
|
||||||
|
</FormGrid>
|
||||||
|
<FormActions>
|
||||||
|
<Button
|
||||||
|
appearance="primary"
|
||||||
|
disabled={!name || !description || addRunbook.isPending}
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
Add runbook
|
||||||
|
</Button>
|
||||||
|
</FormActions>
|
||||||
|
</FormSection>
|
||||||
|
<DataState isLoading={isLoading} error={error} />
|
||||||
|
{data && (
|
||||||
|
<Table aria-label="Runbooks">
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHeaderCell>Name</TableHeaderCell>
|
||||||
|
<TableHeaderCell>Description</TableHeaderCell>
|
||||||
|
<TableHeaderCell>Id</TableHeaderCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{data.map((runbook) => (
|
||||||
|
<TableRow key={runbook.id}>
|
||||||
|
<TableCell>{runbook.name}</TableCell>
|
||||||
|
<TableCell>{runbook.decription ?? "-"}</TableCell>
|
||||||
|
<TableCell>{runbook.id}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
44
src/pages/ServicesPage.tsx
Normal file
44
src/pages/ServicesPage.tsx
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHeader,
|
||||||
|
TableHeaderCell,
|
||||||
|
TableRow,
|
||||||
|
} from "@fluentui/react-components";
|
||||||
|
import { portalApi } from "../api/portalApi";
|
||||||
|
import { DataState } from "../components/DataState";
|
||||||
|
import { PageHeader } from "../components/PageHeader";
|
||||||
|
|
||||||
|
export function ServicesPage() {
|
||||||
|
const { data, error, isLoading } = useQuery({
|
||||||
|
queryKey: ["services"],
|
||||||
|
queryFn: ({ signal }) => portalApi.getServices(signal),
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PageHeader title="Services" description="Service-Katalog aus der Core API." />
|
||||||
|
<DataState isLoading={isLoading} error={error} />
|
||||||
|
{data && (
|
||||||
|
<Table aria-label="Services">
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHeaderCell>Name</TableHeaderCell>
|
||||||
|
<TableHeaderCell>Id</TableHeaderCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{data.map((service) => (
|
||||||
|
<TableRow key={service.id}>
|
||||||
|
<TableCell>{service.name}</TableCell>
|
||||||
|
<TableCell>{service.id}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
44
src/pages/TemplatesPage.tsx
Normal file
44
src/pages/TemplatesPage.tsx
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHeader,
|
||||||
|
TableHeaderCell,
|
||||||
|
TableRow,
|
||||||
|
} from "@fluentui/react-components";
|
||||||
|
import { portalApi } from "../api/portalApi";
|
||||||
|
import { DataState } from "../components/DataState";
|
||||||
|
import { PageHeader } from "../components/PageHeader";
|
||||||
|
|
||||||
|
export function TemplatesPage() {
|
||||||
|
const { data, error, isLoading } = useQuery({
|
||||||
|
queryKey: ["templates"],
|
||||||
|
queryFn: ({ signal }) => portalApi.getTemplates(signal),
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PageHeader title="Templates" description="Vorlagen fuer Portal-Bereitstellungen." />
|
||||||
|
<DataState isLoading={isLoading} error={error} />
|
||||||
|
{data && (
|
||||||
|
<Table aria-label="Templates">
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHeaderCell>Name</TableHeaderCell>
|
||||||
|
<TableHeaderCell>Id</TableHeaderCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{data.map((template) => (
|
||||||
|
<TableRow key={template.id}>
|
||||||
|
<TableCell>{template.name}</TableCell>
|
||||||
|
<TableCell>{template.id}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
20
src/styles/global.css
Normal file
20
src/styles/global.css
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-family:
|
||||||
|
"Segoe UI",
|
||||||
|
system-ui,
|
||||||
|
-apple-system,
|
||||||
|
BlinkMacSystemFont,
|
||||||
|
sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
button,
|
||||||
|
input,
|
||||||
|
textarea,
|
||||||
|
select {
|
||||||
|
font: inherit;
|
||||||
|
}
|
||||||
64
src/types/portal.ts
Normal file
64
src/types/portal.ts
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
export type Deployment = {
|
||||||
|
id: string;
|
||||||
|
status?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AddDeployment = {
|
||||||
|
deploymentGroupId: string;
|
||||||
|
virtualMachineId: string;
|
||||||
|
status: string;
|
||||||
|
jsonData: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DeploymentGroup = {
|
||||||
|
id: string;
|
||||||
|
status?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AddDeploymentGroup = {
|
||||||
|
templateId: string;
|
||||||
|
status: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Domain = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
fqdn: string;
|
||||||
|
netBIOS: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AddDomain = {
|
||||||
|
name: string;
|
||||||
|
fqdn: string;
|
||||||
|
netBIOS: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type EnvironmentItem = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AddEnvironment = {
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Runbook = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
decription?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AddRunbook = {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Template = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ServiceItem = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
1
src/vite-env.d.ts
vendored
Normal file
1
src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
22
tsconfig.app.json
Normal file
22
tsconfig.app.json
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||||
|
"target": "ES2020",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
|
"allowJs": false,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"strict": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Bundler",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx"
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
11
tsconfig.json
Normal file
11
tsconfig.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.app.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.node.json"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
12
tsconfig.node.json
Normal file
12
tsconfig.node.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Bundler",
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"strict": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts", "eslint.config.js"]
|
||||||
|
}
|
||||||
2
vite.config.d.ts
vendored
Normal file
2
vite.config.d.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
declare const _default: import("vite").UserConfigFnObject;
|
||||||
|
export default _default;
|
||||||
21
vite.config.js
Normal file
21
vite.config.js
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { defineConfig, loadEnv } from "vite";
|
||||||
|
import react from "@vitejs/plugin-react";
|
||||||
|
export default defineConfig(function (_a) {
|
||||||
|
var _b;
|
||||||
|
var mode = _a.mode;
|
||||||
|
var env = loadEnv(mode, process.cwd(), "");
|
||||||
|
var apiTarget = (_b = env.VITE_API_PROXY_TARGET) !== null && _b !== void 0 ? _b : "https://localhost:7260";
|
||||||
|
return {
|
||||||
|
plugins: [react()],
|
||||||
|
server: {
|
||||||
|
port: 5173,
|
||||||
|
proxy: {
|
||||||
|
"/api": {
|
||||||
|
target: apiTarget,
|
||||||
|
changeOrigin: true,
|
||||||
|
secure: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
21
vite.config.ts
Normal file
21
vite.config.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { defineConfig, loadEnv } from "vite";
|
||||||
|
import react from "@vitejs/plugin-react";
|
||||||
|
|
||||||
|
export default defineConfig(({ mode }) => {
|
||||||
|
const env = loadEnv(mode, process.cwd(), "");
|
||||||
|
const apiTarget = env.VITE_API_PROXY_TARGET ?? "https://localhost:7260";
|
||||||
|
|
||||||
|
return {
|
||||||
|
plugins: [react()],
|
||||||
|
server: {
|
||||||
|
port: 5173,
|
||||||
|
proxy: {
|
||||||
|
"/api": {
|
||||||
|
target: apiTarget,
|
||||||
|
changeOrigin: true,
|
||||||
|
secure: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user