diff --git a/dashboard/src/main/Main.tsx b/dashboard/src/main/Main.tsx index fd300562f0..fcf8252370 100644 --- a/dashboard/src/main/Main.tsx +++ b/dashboard/src/main/Main.tsx @@ -16,6 +16,8 @@ import VerifyEmail from "./auth/VerifyEmail"; import CurrentError from "./CurrentError"; import Home from "./home/Home"; +import StatusPage from "./status/StatusPage"; + type PropsType = {}; type StateType = { @@ -233,6 +235,17 @@ export default class Main extends Component { return ; } }} + /> + { + if (!this.state.isLoggedIn) { + return ; + } else if (!this.context.user?.email?.includes("@porter.run")) { + return ; + } + return ; + }} /> = ({ cluster, projectId }) => { + const [statusData, setStatusData] = useState({} as StatusData); + + useEffect(() => { + if (!projectId || !cluster) { + return; + } + + api + .systemStatusHistory( + "", + {}, + { + projectId, + clusterId: cluster.id, + } + ) + .then(({ data }) => { + console.log(data); + setStatusData({ + cluster_health_histories: data.cluster_status_histories, + service_health_histories_grouped: {}, + }); + }) + .catch((err) => { + console.error(err); + }); + }, [projectId, cluster]); + return ( + <> + + {cluster.name} + + Operational + + } + preExpanded={true} + > + { + statusData?.cluster_health_histories && + Object.keys(statusData?.cluster_health_histories).map((key) => { + return ( + + {key} + + + {Array.from({ length: 90 }).map((_, i) => { + const status = + statusData?.cluster_health_histories[key][89 - i] ? "failure" : "healthy"; + return ( + + ); + })} + + + + ); + })} + + + + + + + + ); +} + + +export default ClusterStatusSection; + +const getBackgroundGradient = (status: string): string => { + switch (status) { + case "healthy": + return "linear-gradient(#01a05d, #0f2527)"; + case "failure": + return "linear-gradient(#E1322E, #25100f)"; + case "partial_failure": + return "linear-gradient(#E49621, #25270f)"; + default: + return "linear-gradient(#76767644, #76767622)"; // Default or unknown status + } +} + +const Bar = styled.div<{ isFirst: boolean; isLast: boolean; status: string }>` + height: 20px; + display: flex; + flex: 1; + border-top-left-radius: ${(props) => (props.isFirst ? "5px" : "0")}; + border-bottom-left-radius: ${(props) => (props.isFirst ? "5px" : "0")}; + border-top-right-radius: ${(props) => (props.isLast ? "5px" : "0")}; + border-bottom-right-radius: ${(props) => (props.isLast ? "5px" : "0")}; + background: ${(props) => getBackgroundGradient(props.status)}; +`; + +const StatusBars = styled.div` + width: 100%; + display: flex; + justify-content: space-between; + gap: 2px; +`; diff --git a/dashboard/src/main/status/ProjectStatusSection.tsx b/dashboard/src/main/status/ProjectStatusSection.tsx new file mode 100644 index 0000000000..70eaa43aef --- /dev/null +++ b/dashboard/src/main/status/ProjectStatusSection.tsx @@ -0,0 +1,73 @@ +import React, { useContext, useEffect, useState } from "react"; + +import { ProjectListType, ClusterType } from "shared/types"; + +import styled from "styled-components"; +import Container from "components/porter/Container"; +import Expandable from "components/porter/Expandable"; +import Image from "components/porter/Image"; +import Spacer from "components/porter/Spacer"; +import logo from "assets/logo.png"; + +import Back from "components/porter/Back"; +import Text from "components/porter/Text"; +import { Context } from "shared/Context"; + +import midnight from "shared/themes/midnight"; +import gradient from "assets/gradient.png"; + +import api from "shared/api"; + +import ClusterStatusSection from "./ClusterStatusSection"; + +type Props = { project: ProjectListType }; + +const ProjectStatusSection: React.FC = ({ project }) => { + const [clusters, setClusters] = useState([]); + + useEffect(() => { + if (!project || !project.id) { + console.log("project undefined") + return; + } + api. + getClusters( + "", + {}, + { id: project.id }, + ) + .then((res) => res.data as ClusterType[]) + .then((clustersList) => { + console.log(clustersList); + setClusters(clustersList); + }) + .catch((err) => { + console.log(err); + }); + }, [project]); + + return ( + <> + + {project.name} + + Operational + + } + > + {clusters.map((cluster, _) => ( + <> + + + + ))} + + + ); +} + +export default ProjectStatusSection; diff --git a/dashboard/src/main/status/StatusPage.tsx b/dashboard/src/main/status/StatusPage.tsx new file mode 100644 index 0000000000..3e3c276c54 --- /dev/null +++ b/dashboard/src/main/status/StatusPage.tsx @@ -0,0 +1,89 @@ +import React, { useContext, useEffect, useState } from "react"; +import { withRouter, type RouteComponentProps } from "react-router"; +import styled, { ThemeProvider } from "styled-components"; +import { z } from "zod"; + +import Back from "components/porter/Back"; +import Container from "components/porter/Container"; +import Expandable from "components/porter/Expandable"; +import Image from "components/porter/Image"; +import Spacer from "components/porter/Spacer"; +import Text from "components/porter/Text"; +import { Context } from "shared/Context"; + +import midnight from "shared/themes/midnight"; +import gradient from "assets/gradient.png"; +import logo from "assets/logo.png"; + +import { ProjectListType, ClusterType } from "shared/types"; +import api from "shared/api"; +import ProjectStatusSection from "./ProjectStatusSection"; + + + +type Props = RouteComponentProps; + + +const StatusPage: React.FC = () => { + const {user} = useContext(Context); + + const [projects, setProjects] = useState([{id: 0, name: "default"}]); + + useEffect(() => { + if (user === undefined || user.userId === 0) { + console.log("no user defined") + return; + } + api + .getProjects( + "", + {}, + {id: user.userId}, + ) + .then((res) => res.data as ProjectListType[]) + .then((projectList) => { + console.log(projectList); + setProjects(projectList); + }) + .catch((err) => { + console.log(err); + }); + }, [user]); + + return ( + + + + + + <> + {projects.map((project, _) => ( + <> + + + + ))} + + + + + ); +}; + +export default withRouter(StatusPage); + +const StyledStatusPage = styled.div` + width: 100vw; + height: 100vh; + overflow: auto; + padding-top: 50px; + display: flex; + align-items: center; + flex-direction: column; +`; + +const StatusSection = styled.div` + width: calc(100% - 40px); + padding-bottom: 50px; + max-width: 1000px; +`; diff --git a/dashboard/src/shared/types.tsx b/dashboard/src/shared/types.tsx index 30a80a2ade..3a7ccfaa95 100644 --- a/dashboard/src/shared/types.tsx +++ b/dashboard/src/shared/types.tsx @@ -781,3 +781,39 @@ export type AppEventWebhook = { app_event_status: string; payload_encryption_key: string; }; + +export type StatusData = { + cluster_health_histories: Record>; + service_health_histories_grouped: Record; +}; + +export type SystemService = { + name: string; + namespace: string; + involved_object_type: string; +}; + +export type HealthStatus = { + start_time: string; + end_time: string; + status: "failure" | "healthy" | "partial_failure" | "undefined"; + description: string; +}; + +export type DailyHealthStatus = { + status_percentages: Record; + health_statuses: HealthStatus[]; +} + +export type ServiceStatusHistory = { + system_service: SystemService; + daily_health_history: Record; +}; + +// If you're also grouping services by namespace and want a type for the grouped structure: +export type GroupedService = { + system_service: SystemService; + daily_health_history: Record; +}; + +export type GroupedServices = Record; \ No newline at end of file