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