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; +};