diff --git a/package-lock.json b/package-lock.json
index 33cfd68..2af042f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -19,6 +19,7 @@
"@types/node": "^16.18.75",
"@types/react": "^18.2.48",
"@types/react-dom": "^18.2.18",
+ "axios": "^1.6.5",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.21.3",
@@ -5646,6 +5647,29 @@
"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": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.2.1.tgz",
@@ -14900,6 +14924,11 @@
"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": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz",
diff --git a/package.json b/package.json
index ea13e0d..b156e93 100644
--- a/package.json
+++ b/package.json
@@ -14,6 +14,7 @@
"@types/node": "^16.18.75",
"@types/react": "^18.2.48",
"@types/react-dom": "^18.2.18",
+ "axios": "^1.6.5",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.21.3",
diff --git a/public/index.html b/public/index.html
index aa069f2..60ceddd 100644
--- a/public/index.html
+++ b/public/index.html
@@ -1,43 +1,19 @@
-
+
+
-
-
-
+
-
React App
-
-
+
+
+
-
-
-
+
\ No newline at end of file
diff --git a/public/logo192.png b/public/logo192.png
deleted file mode 100644
index fc44b0a..0000000
Binary files a/public/logo192.png and /dev/null differ
diff --git a/public/logo512.png b/public/logo512.png
deleted file mode 100644
index a4e47a6..0000000
Binary files a/public/logo512.png and /dev/null differ
diff --git a/public/manifest.json b/public/manifest.json
index 080d6c7..ccea8eb 100644
--- a/public/manifest.json
+++ b/public/manifest.json
@@ -1,25 +1,15 @@
{
- "short_name": "React App",
- "name": "Create React App Sample",
- "icons": [
- {
- "src": "favicon.ico",
- "sizes": "64x64 32x32 24x24 16x16",
- "type": "image/x-icon"
- },
- {
- "src": "logo192.png",
- "type": "image/png",
- "sizes": "192x192"
- },
- {
- "src": "logo512.png",
- "type": "image/png",
- "sizes": "512x512"
- }
- ],
- "start_url": ".",
- "display": "standalone",
- "theme_color": "#000000",
- "background_color": "#ffffff"
+ "short_name": "React App",
+ "name": "Create React App Sample",
+ "icons": [
+ {
+ "src": "favicon.ico",
+ "sizes": "64x64 32x32 24x24 16x16",
+ "type": "image/x-icon"
+ }
+ ],
+ "start_url": ".",
+ "display": "standalone",
+ "theme_color": "#000000",
+ "background_color": "#ffffff"
}
diff --git a/src/api/common.ts b/src/api/common.ts
new file mode 100644
index 0000000..9c0d03e
--- /dev/null
+++ b/src/api/common.ts
@@ -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,
+ },
+});
diff --git a/src/api/dashboard.ts b/src/api/dashboard.ts
new file mode 100644
index 0000000..f5ee8b2
--- /dev/null
+++ b/src/api/dashboard.ts
@@ -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();
diff --git a/src/api/index.ts b/src/api/index.ts
new file mode 100644
index 0000000..b5c406c
--- /dev/null
+++ b/src/api/index.ts
@@ -0,0 +1 @@
+export { dashboardApi } from "./dashboard";
diff --git a/src/components/Dashboard.tsx b/src/components/Dashboard.tsx
index a0fe12c..96f041f 100644
--- a/src/components/Dashboard.tsx
+++ b/src/components/Dashboard.tsx
@@ -1,11 +1,60 @@
import React from "react";
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() {
- const onSelectEnvTab = (tab: EnvTab): void => {
- console.log("tab changed:", tab);
+ const defaultEnv = ENVIRONMENT_TABS[0];
+
+ const [selectedEnv, setSelectedEnv] = React.useState(defaultEnv);
+ const [loadingEnv, setLoadingEnv] = React.useState(false);
+ const [loadingErr, setLoadingErr] = React.useState(null);
+ const [activeEnvironments, setActiveEnvironments] = React.useState(null);
+
+ React.useEffect(() => {
+ loadEnv(defaultEnv);
+ }, []);
+
+ const onSelectEnvTab = (envTab: EnvTab): void => {
+ loadEnv(envTab);
};
- return ;
+ 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 (
+ <>
+
+
+ {loadingErr && }
+ {activeEnvironments && }
+ >
+ );
}
diff --git a/src/components/EnvTabsContainer.tsx b/src/components/EnvTabsContainer.tsx
index d51d75c..d1385f7 100644
--- a/src/components/EnvTabsContainer.tsx
+++ b/src/components/EnvTabsContainer.tsx
@@ -10,13 +10,12 @@ interface OnSelectTab {
}
interface EnvTabsContainerProps {
+ selectedEnv: EnvTab;
onSelect: OnSelectTab;
}
-export default function EnvTabsContainer({ onSelect }: EnvTabsContainerProps) {
- const defaultTab = ENVIRONMENT_TABS[0];
-
- const [selected, setSelected] = React.useState(defaultTab);
+export default function EnvTabsContainer({ selectedEnv, onSelect }: EnvTabsContainerProps) {
+ const [selected, setSelected] = React.useState(selectedEnv);
const handleChange = (event: React.SyntheticEvent, newValue: EnvTab) => {
setSelected(newValue);
@@ -28,7 +27,7 @@ export default function EnvTabsContainer({ onSelect }: EnvTabsContainerProps) {
});
return (
-
+
{tabs}
diff --git a/src/components/LoadingError.tsx b/src/components/LoadingError.tsx
new file mode 100644
index 0000000..7afb958
--- /dev/null
+++ b/src/components/LoadingError.tsx
@@ -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 (
+
+ {error.message}
+ URL: {error.url}
+
+ );
+}
diff --git a/src/components/LoadingIndicator.tsx b/src/components/LoadingIndicator.tsx
new file mode 100644
index 0000000..6a52215
--- /dev/null
+++ b/src/components/LoadingIndicator.tsx
@@ -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 (
+
+ {active ? : }
+
+ );
+}
diff --git a/src/components/dashboard/EnvironmentList.tsx b/src/components/dashboard/EnvironmentList.tsx
new file mode 100644
index 0000000..391f674
--- /dev/null
+++ b/src/components/dashboard/EnvironmentList.tsx
@@ -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 (
+
+
+ {env.name}
+
+
+
+ );
+ });
+
+ return {envItems};
+}
diff --git a/src/components/dashboard/ServiceList.tsx b/src/components/dashboard/ServiceList.tsx
new file mode 100644
index 0000000..d1a15f8
--- /dev/null
+++ b/src/components/dashboard/ServiceList.tsx
@@ -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 (
+
+
+ {service.name}
+
+
+ );
+ });
+
+ return {serviceItems};
+}
diff --git a/src/components/dashboard/TenantList.tsx b/src/components/dashboard/TenantList.tsx
new file mode 100644
index 0000000..669d701
--- /dev/null
+++ b/src/components/dashboard/TenantList.tsx
@@ -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 (
+
+
+ {tenantName}
+
+
+
+ );
+ });
+
+ return {tenantItems};
+}
diff --git a/src/scss/_app-header.scss b/src/scss/_app-header.scss
index b5f69cd..b885748 100644
--- a/src/scss/_app-header.scss
+++ b/src/scss/_app-header.scss
@@ -3,7 +3,6 @@
color: whitesmoke;
padding: 1rem 0 1rem 0;
box-shadow: 0px 5px 11px 0px rgba(0, 0, 0, 0.31);
- margin-bottom: 1rem;
h1 {
font-size: 2rem;
letter-spacing: 5px;
@@ -12,13 +11,13 @@
color: #08ab08;
animation: rotation 3s infinite linear;
@keyframes rotation {
- from {
- transform: rotate(0deg);
- }
-
- to {
- transform: rotate(359deg);
- }
+ from {
+ transform: rotate(0deg);
}
+
+ to {
+ transform: rotate(359deg);
+ }
+ }
}
}
diff --git a/src/scss/_linear-progress.scss b/src/scss/_linear-progress.scss
new file mode 100644
index 0000000..b3e330b
--- /dev/null
+++ b/src/scss/_linear-progress.scss
@@ -0,0 +1,3 @@
+.linear-progress-placeholder {
+ height: 4px;
+}
diff --git a/src/scss/styles.scss b/src/scss/styles.scss
index 03b53fe..6157728 100644
--- a/src/scss/styles.scss
+++ b/src/scss/styles.scss
@@ -1,2 +1,3 @@
@import "common";
@import "app-header";
+@import "linear-progress";
diff --git a/src/types.tsx b/src/types.tsx
index 1ae164f..37dfd2b 100644
--- a/src/types.tsx
+++ b/src/types.tsx
@@ -3,3 +3,49 @@ export type EnvTab = {
id: 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;
+};