Many nice things

This commit is contained in:
Eden Kirin
2024-01-24 21:58:39 +01:00
parent 76b1a2d4e9
commit 490f2bdde6
20 changed files with 298 additions and 72 deletions

29
package-lock.json generated
View File

@ -19,6 +19,7 @@
"@types/node": "^16.18.75", "@types/node": "^16.18.75",
"@types/react": "^18.2.48", "@types/react": "^18.2.48",
"@types/react-dom": "^18.2.18", "@types/react-dom": "^18.2.18",
"axios": "^1.6.5",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-router-dom": "^6.21.3", "react-router-dom": "^6.21.3",
@ -5646,6 +5647,29 @@
"node": ">=4" "node": ">=4"
} }
}, },
"node_modules/axios": {
"version": "1.6.5",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.6.5.tgz",
"integrity": "sha512-Ii012v05KEVuUoFWmMW/UQv9aRIc3ZwkWDcM+h5Il8izZCtRVpDUfwpoFf7eOtajT3QiGR4yDUx7lPqHJULgbg==",
"dependencies": {
"follow-redirects": "^1.15.4",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/axios/node_modules/form-data": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/axobject-query": { "node_modules/axobject-query": {
"version": "3.2.1", "version": "3.2.1",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.2.1.tgz", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.2.1.tgz",
@ -14900,6 +14924,11 @@
"node": ">= 0.10" "node": ">= 0.10"
} }
}, },
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
},
"node_modules/psl": { "node_modules/psl": {
"version": "1.9.0", "version": "1.9.0",
"resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz",

View File

@ -14,6 +14,7 @@
"@types/node": "^16.18.75", "@types/node": "^16.18.75",
"@types/react": "^18.2.48", "@types/react": "^18.2.48",
"@types/react-dom": "^18.2.18", "@types/react-dom": "^18.2.18",
"axios": "^1.6.5",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-router-dom": "^6.21.3", "react-router-dom": "^6.21.3",

View File

@ -1,43 +1,19 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head>
<head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" /> <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" /> <meta name="theme-color" content="#000000" />
<meta <meta name="description" content="Web site created using create-react-app" />
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" /> <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title> <title>React App</title>
</head> </head>
<body>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript> <noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div> <div id="root"></div>
<!-- </body>
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html> </html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

View File

@ -6,16 +6,6 @@
"src": "favicon.ico", "src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16", "sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon" "type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
} }
], ],
"start_url": ".", "start_url": ".",

17
src/api/common.ts Normal file
View File

@ -0,0 +1,17 @@
import axios from "axios";
export class ApiBase {}
const commonHeaders = {
"Content-Type": "application/json",
Accept: "application/json",
"Access-Control-Allow-Origin": "*",
"Access-Control-Max-Age": 60,
};
export const axiosInstance = axios.create({
timeout: 5000,
headers: {
...commonHeaders,
},
});

12
src/api/dashboard.ts Normal file
View File

@ -0,0 +1,12 @@
import { axiosInstance, ApiBase } from "./common";
class DashboardApi extends ApiBase {
get = async (endpoint: string) => {
return axiosInstance.get(endpoint, {});
};
getFake = async (endpoint: string) => {
return axiosInstance.get("http://localhost:8002", {});
};
}
export const dashboardApi = new DashboardApi();

1
src/api/index.ts Normal file
View File

@ -0,0 +1 @@
export { dashboardApi } from "./dashboard";

View File

@ -1,11 +1,60 @@
import React from "react"; import React from "react";
import EnvTabsContainer from "./EnvTabsContainer"; import EnvTabsContainer from "./EnvTabsContainer";
import { EnvTab } from "../types"; import LoadingIndicator from "./LoadingIndicator";
import { DashboardLoadError, DashboardResponseContent, EnvTab, Environment } from "../types";
import { ENVIRONMENT_TABS } from "../const";
import { dashboardApi } from "../api";
import { AxiosError } from "axios";
import LoadingError from "./LoadingError";
import EnvironmentList from "./dashboard/EnvironmentList";
export default function Dashboard() { export default function Dashboard() {
const onSelectEnvTab = (tab: EnvTab): void => { const defaultEnv = ENVIRONMENT_TABS[0];
console.log("tab changed:", tab);
const [selectedEnv, setSelectedEnv] = React.useState(defaultEnv);
const [loadingEnv, setLoadingEnv] = React.useState(false);
const [loadingErr, setLoadingErr] = React.useState<DashboardLoadError | null>(null);
const [activeEnvironments, setActiveEnvironments] = React.useState<Environment[] | null>(null);
React.useEffect(() => {
loadEnv(defaultEnv);
}, []);
const onSelectEnvTab = (envTab: EnvTab): void => {
loadEnv(envTab);
}; };
return <EnvTabsContainer onSelect={onSelectEnvTab} />; const loadEnv = (envTab: EnvTab) => {
console.log("loading env:", envTab);
setLoadingErr(null);
setLoadingEnv(true);
dashboardApi
.getFake(envTab.dashboardEndpoint)
.then((response) => {
return response.data.content;
})
.then((data: DashboardResponseContent) => {
setActiveEnvironments(data.environments);
})
.catch((error: AxiosError) => {
console.error(error);
setLoadingErr({
message: error.message,
url: envTab.dashboardEndpoint,
});
})
.finally(() => {
setLoadingEnv(false);
});
};
return (
<>
<LoadingIndicator active={loadingEnv} />
<EnvTabsContainer selectedEnv={selectedEnv} onSelect={onSelectEnvTab} />
{loadingErr && <LoadingError error={loadingErr} />}
{activeEnvironments && <EnvironmentList environments={activeEnvironments} />}
</>
);
} }

View File

@ -10,13 +10,12 @@ interface OnSelectTab {
} }
interface EnvTabsContainerProps { interface EnvTabsContainerProps {
selectedEnv: EnvTab;
onSelect: OnSelectTab; onSelect: OnSelectTab;
} }
export default function EnvTabsContainer({ onSelect }: EnvTabsContainerProps) { export default function EnvTabsContainer({ selectedEnv, onSelect }: EnvTabsContainerProps) {
const defaultTab = ENVIRONMENT_TABS[0]; const [selected, setSelected] = React.useState(selectedEnv);
const [selected, setSelected] = React.useState(defaultTab);
const handleChange = (event: React.SyntheticEvent, newValue: EnvTab) => { const handleChange = (event: React.SyntheticEvent, newValue: EnvTab) => {
setSelected(newValue); setSelected(newValue);
@ -28,7 +27,7 @@ export default function EnvTabsContainer({ onSelect }: EnvTabsContainerProps) {
}); });
return ( return (
<Box sx={{ width: "100%" }}> <Box sx={{ width: "100%", marginBottom: "2rem" }}>
<Box sx={{ borderBottom: 1, borderColor: "divider" }}> <Box sx={{ borderBottom: 1, borderColor: "divider" }}>
<Tabs value={selected} onChange={handleChange}> <Tabs value={selected} onChange={handleChange}>
{tabs} {tabs}

View File

@ -0,0 +1,17 @@
import React from "react";
import { DashboardLoadError } from "../types";
import Alert from "@mui/material/Alert";
import AlertTitle from "@mui/material/AlertTitle";
interface LoadingErrorProps {
error: DashboardLoadError;
}
export default function LoadingError({ error }: LoadingErrorProps) {
return (
<Alert severity="error">
<AlertTitle>{error.message}</AlertTitle>
<p>URL: {error.url}</p>
</Alert>
);
}

View File

@ -0,0 +1,15 @@
import React from "react";
import Box from "@mui/material/Box";
import LinearProgress from "@mui/material/LinearProgress";
interface LoadingIndicatorProps {
active: boolean;
}
export default function LoadingIndicator({ active }: LoadingIndicatorProps) {
return (
<Box sx={{ width: "100%" }}>
{active ? <LinearProgress /> : <Box className="linear-progress-placeholder" />}
</Box>
);
}

View File

@ -0,0 +1,24 @@
import React from "react";
import { Environment } from "../../types";
import TenantList from "./TenantList";
import Typography from "@mui/material/Typography";
import Box from "@mui/material/Box";
interface EnvironmentListProps {
environments: Environment[];
}
export default function EnvironmentList({ environments }: EnvironmentListProps) {
const envItems = environments.map((env) => {
return (
<Box className="environment">
<Typography component={"h1"} key={env.name} className="title">
{env.name}
</Typography>
<TenantList tenants={env.tenants} />
</Box>
);
});
return <Box className="environment-list">{envItems}</Box>;
}

View File

@ -0,0 +1,22 @@
import React from "react";
import { Service } from "../../types";
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";
interface ServiceListProps {
services: Service[];
}
export default function ServiceList({ services }: ServiceListProps) {
const serviceItems = services.map((service) => {
return (
<Box className="service-card">
<Typography component={"h1"} key={service.name} className="service-name">
{service.name}
</Typography>
</Box>
);
});
return <Box className="service-list">{serviceItems}</Box>;
}

View File

@ -0,0 +1,25 @@
import React from "react";
import { Tenant } from "../../types";
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";
import ServiceList from "./ServiceList";
interface TenantListProps {
tenants: Tenant[];
}
export default function TenantList({ tenants: tenants }: TenantListProps) {
const tenantItems = tenants.map((tenant) => {
const tenantName = tenant.name !== "default" ? `Tenant: ${tenant.name}` : "Multitenant";
return (
<Box className="tenant">
<Typography component={"h1"} key={tenant.name} className="tenant-name">
{tenantName}
</Typography>
<ServiceList services={tenant.services} />
</Box>
);
});
return <Box className="tenant-list">{tenantItems}</Box>;
}

View File

@ -3,7 +3,6 @@
color: whitesmoke; color: whitesmoke;
padding: 1rem 0 1rem 0; padding: 1rem 0 1rem 0;
box-shadow: 0px 5px 11px 0px rgba(0, 0, 0, 0.31); box-shadow: 0px 5px 11px 0px rgba(0, 0, 0, 0.31);
margin-bottom: 1rem;
h1 { h1 {
font-size: 2rem; font-size: 2rem;
letter-spacing: 5px; letter-spacing: 5px;

View File

@ -0,0 +1,3 @@
.linear-progress-placeholder {
height: 4px;
}

View File

@ -1,2 +1,3 @@
@import "common"; @import "common";
@import "app-header"; @import "app-header";
@import "linear-progress";

View File

@ -3,3 +3,49 @@ export type EnvTab = {
id: string; id: string;
dashboardEndpoint: string; dashboardEndpoint: string;
}; };
type StatusPerTenant = { [tenantId: string]: boolean };
type HealthCheckStatus = {
status_ok: boolean;
message: string;
status_per_tenant: StatusPerTenant | null;
};
type AppDetails = {
app_name: string;
app_version: string;
template_name: string | null;
template_version: string | null;
};
type Node = {
name: string;
url: string;
app_details: AppDetails | null;
health_check_status: HealthCheckStatus;
};
export type Service = {
name: string;
nodes: Node[];
};
export type Tenant = {
name: string;
services: Service[];
};
export type Environment = {
name: string;
tenants: Tenant[];
};
export type DashboardResponseContent = {
environments: Environment[];
};
export type DashboardLoadError = {
url: string;
message: string;
};