Many nice things
This commit is contained in:
29
package-lock.json
generated
29
package-lock.json
generated
@ -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",
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -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 |
@ -1,25 +1,15 @@
|
|||||||
{
|
{
|
||||||
"short_name": "React App",
|
"short_name": "React App",
|
||||||
"name": "Create React App Sample",
|
"name": "Create React App Sample",
|
||||||
"icons": [
|
"icons": [
|
||||||
{
|
{
|
||||||
"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",
|
"start_url": ".",
|
||||||
"type": "image/png",
|
"display": "standalone",
|
||||||
"sizes": "192x192"
|
"theme_color": "#000000",
|
||||||
},
|
"background_color": "#ffffff"
|
||||||
{
|
|
||||||
"src": "logo512.png",
|
|
||||||
"type": "image/png",
|
|
||||||
"sizes": "512x512"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"start_url": ".",
|
|
||||||
"display": "standalone",
|
|
||||||
"theme_color": "#000000",
|
|
||||||
"background_color": "#ffffff"
|
|
||||||
}
|
}
|
||||||
|
|||||||
17
src/api/common.ts
Normal file
17
src/api/common.ts
Normal 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
12
src/api/dashboard.ts
Normal 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
1
src/api/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { dashboardApi } from "./dashboard";
|
||||||
@ -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} />}
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
17
src/components/LoadingError.tsx
Normal file
17
src/components/LoadingError.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
15
src/components/LoadingIndicator.tsx
Normal file
15
src/components/LoadingIndicator.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
24
src/components/dashboard/EnvironmentList.tsx
Normal file
24
src/components/dashboard/EnvironmentList.tsx
Normal 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>;
|
||||||
|
}
|
||||||
22
src/components/dashboard/ServiceList.tsx
Normal file
22
src/components/dashboard/ServiceList.tsx
Normal 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>;
|
||||||
|
}
|
||||||
25
src/components/dashboard/TenantList.tsx
Normal file
25
src/components/dashboard/TenantList.tsx
Normal 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>;
|
||||||
|
}
|
||||||
@ -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;
|
||||||
@ -12,13 +11,13 @@
|
|||||||
color: #08ab08;
|
color: #08ab08;
|
||||||
animation: rotation 3s infinite linear;
|
animation: rotation 3s infinite linear;
|
||||||
@keyframes rotation {
|
@keyframes rotation {
|
||||||
from {
|
from {
|
||||||
transform: rotate(0deg);
|
transform: rotate(0deg);
|
||||||
}
|
|
||||||
|
|
||||||
to {
|
|
||||||
transform: rotate(359deg);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
transform: rotate(359deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
3
src/scss/_linear-progress.scss
Normal file
3
src/scss/_linear-progress.scss
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
.linear-progress-placeholder {
|
||||||
|
height: 4px;
|
||||||
|
}
|
||||||
@ -1,2 +1,3 @@
|
|||||||
@import "common";
|
@import "common";
|
||||||
@import "app-header";
|
@import "app-header";
|
||||||
|
@import "linear-progress";
|
||||||
|
|||||||
@ -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;
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user