mirror of https://github.com/coder/coder.git
feat(site): add health warning and a health monitor page (#8844)
This commit is contained in:
parent
44f9b0228a
commit
cf35c0dfc5
|
@ -8024,7 +8024,8 @@ const docTemplate = `{
|
||||||
"tailnet_pg_coordinator",
|
"tailnet_pg_coordinator",
|
||||||
"single_tailnet",
|
"single_tailnet",
|
||||||
"template_restart_requirement",
|
"template_restart_requirement",
|
||||||
"template_insights_page"
|
"template_insights_page",
|
||||||
|
"deployment_health_page"
|
||||||
],
|
],
|
||||||
"x-enum-varnames": [
|
"x-enum-varnames": [
|
||||||
"ExperimentMoons",
|
"ExperimentMoons",
|
||||||
|
@ -8032,7 +8033,8 @@ const docTemplate = `{
|
||||||
"ExperimentTailnetPGCoordinator",
|
"ExperimentTailnetPGCoordinator",
|
||||||
"ExperimentSingleTailnet",
|
"ExperimentSingleTailnet",
|
||||||
"ExperimentTemplateRestartRequirement",
|
"ExperimentTemplateRestartRequirement",
|
||||||
"ExperimentTemplateInsightsPage"
|
"ExperimentTemplateInsightsPage",
|
||||||
|
"ExperimentDeploymentHealthPage"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"codersdk.Feature": {
|
"codersdk.Feature": {
|
||||||
|
|
|
@ -7185,7 +7185,8 @@
|
||||||
"tailnet_pg_coordinator",
|
"tailnet_pg_coordinator",
|
||||||
"single_tailnet",
|
"single_tailnet",
|
||||||
"template_restart_requirement",
|
"template_restart_requirement",
|
||||||
"template_insights_page"
|
"template_insights_page",
|
||||||
|
"deployment_health_page"
|
||||||
],
|
],
|
||||||
"x-enum-varnames": [
|
"x-enum-varnames": [
|
||||||
"ExperimentMoons",
|
"ExperimentMoons",
|
||||||
|
@ -7193,7 +7194,8 @@
|
||||||
"ExperimentTailnetPGCoordinator",
|
"ExperimentTailnetPGCoordinator",
|
||||||
"ExperimentSingleTailnet",
|
"ExperimentSingleTailnet",
|
||||||
"ExperimentTemplateRestartRequirement",
|
"ExperimentTemplateRestartRequirement",
|
||||||
"ExperimentTemplateInsightsPage"
|
"ExperimentTemplateInsightsPage",
|
||||||
|
"ExperimentDeploymentHealthPage"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"codersdk.Feature": {
|
"codersdk.Feature": {
|
||||||
|
|
|
@ -1871,6 +1871,9 @@ const (
|
||||||
// Insights page
|
// Insights page
|
||||||
ExperimentTemplateInsightsPage Experiment = "template_insights_page"
|
ExperimentTemplateInsightsPage Experiment = "template_insights_page"
|
||||||
|
|
||||||
|
// Deployment health page
|
||||||
|
ExperimentDeploymentHealthPage Experiment = "deployment_health_page"
|
||||||
|
|
||||||
// Add new experiments here!
|
// Add new experiments here!
|
||||||
// ExperimentExample Experiment = "example"
|
// ExperimentExample Experiment = "example"
|
||||||
)
|
)
|
||||||
|
@ -1881,6 +1884,7 @@ const (
|
||||||
// not be included here and will be essentially hidden.
|
// not be included here and will be essentially hidden.
|
||||||
var ExperimentsAll = Experiments{
|
var ExperimentsAll = Experiments{
|
||||||
ExperimentTemplateInsightsPage,
|
ExperimentTemplateInsightsPage,
|
||||||
|
ExperimentDeploymentHealthPage,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Experiments is a list of experiments that are enabled for the deployment.
|
// Experiments is a list of experiments that are enabled for the deployment.
|
||||||
|
|
|
@ -2677,6 +2677,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
|
||||||
| `single_tailnet` |
|
| `single_tailnet` |
|
||||||
| `template_restart_requirement` |
|
| `template_restart_requirement` |
|
||||||
| `template_insights_page` |
|
| `template_insights_page` |
|
||||||
|
| `deployment_health_page` |
|
||||||
|
|
||||||
## codersdk.Feature
|
## codersdk.Feature
|
||||||
|
|
||||||
|
|
|
@ -183,6 +183,7 @@ const TemplateInsightsPage = lazy(
|
||||||
() =>
|
() =>
|
||||||
import("./pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage"),
|
import("./pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage"),
|
||||||
)
|
)
|
||||||
|
const HealthPage = lazy(() => import("./pages/HealthPage/HealthPage"))
|
||||||
|
|
||||||
export const AppRouter: FC = () => {
|
export const AppRouter: FC = () => {
|
||||||
return (
|
return (
|
||||||
|
@ -197,6 +198,8 @@ export const AppRouter: FC = () => {
|
||||||
<Route element={<DashboardLayout />}>
|
<Route element={<DashboardLayout />}>
|
||||||
<Route index element={<IndexPage />} />
|
<Route index element={<IndexPage />} />
|
||||||
|
|
||||||
|
<Route path="health" element={<HealthPage />} />
|
||||||
|
|
||||||
<Route path="gitauth/:provider" element={<GitAuthPage />} />
|
<Route path="gitauth/:provider" element={<GitAuthPage />} />
|
||||||
|
|
||||||
<Route path="workspaces" element={<WorkspacesPage />} />
|
<Route path="workspaces" element={<WorkspacesPage />} />
|
||||||
|
|
|
@ -1405,3 +1405,15 @@ export const getInsightsTemplate = async (
|
||||||
const response = await axios.get(`/api/v2/insights/templates?${params}`)
|
const response = await axios.get(`/api/v2/insights/templates?${params}`)
|
||||||
return response.data
|
return response.data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getHealth = () => {
|
||||||
|
return axios.get<{
|
||||||
|
healthy: boolean
|
||||||
|
time: string
|
||||||
|
coder_version: string
|
||||||
|
derp: { healthy: boolean }
|
||||||
|
access_url: { healthy: boolean }
|
||||||
|
websocket: { healthy: boolean }
|
||||||
|
database: { healthy: boolean }
|
||||||
|
}>("/api/v2/debug/health")
|
||||||
|
}
|
||||||
|
|
|
@ -1563,6 +1563,7 @@ export const Entitlements: Entitlement[] = [
|
||||||
|
|
||||||
// From codersdk/deployment.go
|
// From codersdk/deployment.go
|
||||||
export type Experiment =
|
export type Experiment =
|
||||||
|
| "deployment_health_page"
|
||||||
| "moons"
|
| "moons"
|
||||||
| "single_tailnet"
|
| "single_tailnet"
|
||||||
| "tailnet_pg_coordinator"
|
| "tailnet_pg_coordinator"
|
||||||
|
@ -1570,6 +1571,7 @@ export type Experiment =
|
||||||
| "template_restart_requirement"
|
| "template_restart_requirement"
|
||||||
| "workspace_actions"
|
| "workspace_actions"
|
||||||
export const Experiments: Experiment[] = [
|
export const Experiments: Experiment[] = [
|
||||||
|
"deployment_health_page",
|
||||||
"moons",
|
"moons",
|
||||||
"single_tailnet",
|
"single_tailnet",
|
||||||
"tailnet_pg_coordinator",
|
"tailnet_pg_coordinator",
|
||||||
|
|
|
@ -16,6 +16,7 @@ import Box from "@mui/material/Box"
|
||||||
import InfoOutlined from "@mui/icons-material/InfoOutlined"
|
import InfoOutlined from "@mui/icons-material/InfoOutlined"
|
||||||
import Button from "@mui/material/Button"
|
import Button from "@mui/material/Button"
|
||||||
import { docs } from "utils/docs"
|
import { docs } from "utils/docs"
|
||||||
|
import { HealthBanner } from "./HealthBanner"
|
||||||
|
|
||||||
export const DashboardLayout: FC = () => {
|
export const DashboardLayout: FC = () => {
|
||||||
const styles = useStyles()
|
const styles = useStyles()
|
||||||
|
@ -30,6 +31,7 @@ export const DashboardLayout: FC = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<HealthBanner />
|
||||||
<ServiceBanner />
|
<ServiceBanner />
|
||||||
{canViewDeployment && <LicenseBanner />}
|
{canViewDeployment && <LicenseBanner />}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,45 @@
|
||||||
|
import { Alert } from "components/Alert/Alert"
|
||||||
|
import { Link as RouterLink } from "react-router-dom"
|
||||||
|
import Link from "@mui/material/Link"
|
||||||
|
import { colors } from "theme/colors"
|
||||||
|
import { useQuery } from "@tanstack/react-query"
|
||||||
|
import { getHealth } from "api/api"
|
||||||
|
import { useDashboard } from "./DashboardProvider"
|
||||||
|
|
||||||
|
export const HealthBanner = () => {
|
||||||
|
const { data: healthStatus } = useQuery({
|
||||||
|
queryKey: ["health"],
|
||||||
|
queryFn: () => getHealth(),
|
||||||
|
})
|
||||||
|
const dashboard = useDashboard()
|
||||||
|
const hasHealthIssues = healthStatus && !healthStatus.data.healthy
|
||||||
|
|
||||||
|
if (
|
||||||
|
dashboard.experiments.includes("deployment_health_page") &&
|
||||||
|
hasHealthIssues
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<Alert
|
||||||
|
severity="error"
|
||||||
|
variant="filled"
|
||||||
|
sx={{
|
||||||
|
border: 0,
|
||||||
|
borderRadius: 0,
|
||||||
|
backgroundColor: colors.red[10],
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
We have detected problems with your Coder deployment. Please{" "}
|
||||||
|
<Link
|
||||||
|
component={RouterLink}
|
||||||
|
to="/health"
|
||||||
|
sx={{ fontWeight: 600, color: "inherit" }}
|
||||||
|
>
|
||||||
|
inspect the health status
|
||||||
|
</Link>
|
||||||
|
.
|
||||||
|
</Alert>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
|
@ -6,6 +6,7 @@ import LockRounded from "@mui/icons-material/LockOutlined"
|
||||||
import Globe from "@mui/icons-material/PublicOutlined"
|
import Globe from "@mui/icons-material/PublicOutlined"
|
||||||
import HubOutlinedIcon from "@mui/icons-material/HubOutlined"
|
import HubOutlinedIcon from "@mui/icons-material/HubOutlined"
|
||||||
import VpnKeyOutlined from "@mui/icons-material/VpnKeyOutlined"
|
import VpnKeyOutlined from "@mui/icons-material/VpnKeyOutlined"
|
||||||
|
import MonitorHeartOutlined from "@mui/icons-material/MonitorHeartOutlined"
|
||||||
import { GitIcon } from "components/Icons/GitIcon"
|
import { GitIcon } from "components/Icons/GitIcon"
|
||||||
import { Stack } from "components/Stack/Stack"
|
import { Stack } from "components/Stack/Stack"
|
||||||
import { ElementType, PropsWithChildren, ReactNode, FC } from "react"
|
import { ElementType, PropsWithChildren, ReactNode, FC } from "react"
|
||||||
|
@ -93,6 +94,14 @@ export const Sidebar: React.FC = () => {
|
||||||
>
|
>
|
||||||
Security
|
Security
|
||||||
</SidebarNavItem>
|
</SidebarNavItem>
|
||||||
|
{dashboard.experiments.includes("deployment_health_page") && (
|
||||||
|
<SidebarNavItem
|
||||||
|
href="/health"
|
||||||
|
icon={<SidebarNavItemIcon icon={MonitorHeartOutlined} />}
|
||||||
|
>
|
||||||
|
Health
|
||||||
|
</SidebarNavItem>
|
||||||
|
)}
|
||||||
</nav>
|
</nav>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,17 @@
|
||||||
import { makeStyles } from "@mui/styles"
|
import { makeStyles } from "@mui/styles"
|
||||||
import { FC, PropsWithChildren } from "react"
|
import { FC, PropsWithChildren } from "react"
|
||||||
|
import { combineClasses } from "utils/combineClasses"
|
||||||
|
|
||||||
export const FullWidthPageHeader: FC<PropsWithChildren> = ({ children }) => {
|
export const FullWidthPageHeader: FC<
|
||||||
|
PropsWithChildren & { sticky?: boolean }
|
||||||
|
> = ({ children, sticky = true }) => {
|
||||||
const styles = useStyles()
|
const styles = useStyles()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className={styles.header} data-testid="header">
|
<header
|
||||||
|
className={combineClasses([styles.header, sticky ? styles.sticky : ""])}
|
||||||
|
data-testid="header"
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</header>
|
</header>
|
||||||
)
|
)
|
||||||
|
@ -35,8 +41,7 @@ const useStyles = makeStyles((theme) => ({
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
gap: theme.spacing(6),
|
gap: theme.spacing(6),
|
||||||
position: "sticky",
|
|
||||||
top: 0,
|
|
||||||
zIndex: 10,
|
zIndex: 10,
|
||||||
flexWrap: "wrap",
|
flexWrap: "wrap",
|
||||||
|
|
||||||
|
@ -48,6 +53,10 @@ const useStyles = makeStyles((theme) => ({
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
sticky: {
|
||||||
|
position: "sticky",
|
||||||
|
top: 0,
|
||||||
|
},
|
||||||
actions: {
|
actions: {
|
||||||
marginLeft: "auto",
|
marginLeft: "auto",
|
||||||
[theme.breakpoints.down("md")]: {
|
[theme.breakpoints.down("md")]: {
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
|
import Box from "@mui/material/Box"
|
||||||
import { makeStyles } from "@mui/styles"
|
import { makeStyles } from "@mui/styles"
|
||||||
import { ComponentProps, FC, PropsWithChildren } from "react"
|
import { ComponentProps, FC, PropsWithChildren } from "react"
|
||||||
import { combineClasses } from "utils/combineClasses"
|
import { combineClasses } from "utils/combineClasses"
|
||||||
|
|
||||||
export const Stats: FC<ComponentProps<"div">> = (props) => {
|
export const Stats: FC<ComponentProps<typeof Box>> = (props) => {
|
||||||
const styles = useStyles()
|
const styles = useStyles()
|
||||||
return (
|
return (
|
||||||
<div
|
<Box
|
||||||
{...props}
|
{...props}
|
||||||
className={combineClasses([styles.stats, props.className])}
|
className={combineClasses([styles.stats, props.className])}
|
||||||
/>
|
/>
|
||||||
|
@ -16,18 +17,18 @@ export const StatsItem: FC<
|
||||||
{
|
{
|
||||||
label: string
|
label: string
|
||||||
value: string | number | JSX.Element
|
value: string | number | JSX.Element
|
||||||
} & ComponentProps<"div">
|
} & ComponentProps<typeof Box>
|
||||||
> = ({ label, value, ...divProps }) => {
|
> = ({ label, value, ...divProps }) => {
|
||||||
const styles = useStyles()
|
const styles = useStyles()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<Box
|
||||||
{...divProps}
|
{...divProps}
|
||||||
className={combineClasses([styles.statItem, divProps.className])}
|
className={combineClasses([styles.statItem, divProps.className])}
|
||||||
>
|
>
|
||||||
<span className={styles.statsLabel}>{label}:</span>
|
<span className={styles.statsLabel}>{label}:</span>
|
||||||
<span className={styles.statsValue}>{value}</span>
|
<span className={styles.statsValue}>{value}</span>
|
||||||
</div>
|
</Box>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { FC } from "react"
|
import { ComponentProps, FC } from "react"
|
||||||
import Editor, { DiffEditor, loader } from "@monaco-editor/react"
|
import Editor, { DiffEditor, loader } from "@monaco-editor/react"
|
||||||
import * as monaco from "monaco-editor"
|
import * as monaco from "monaco-editor"
|
||||||
import { useCoderTheme } from "./coderTheme"
|
import { useCoderTheme } from "./coderTheme"
|
||||||
|
@ -9,8 +9,10 @@ loader.config({ monaco })
|
||||||
export const SyntaxHighlighter: FC<{
|
export const SyntaxHighlighter: FC<{
|
||||||
value: string
|
value: string
|
||||||
language: string
|
language: string
|
||||||
|
editorProps?: ComponentProps<typeof Editor> &
|
||||||
|
ComponentProps<typeof DiffEditor>
|
||||||
compareWith?: string
|
compareWith?: string
|
||||||
}> = ({ value, compareWith, language }) => {
|
}> = ({ value, compareWith, language, editorProps }) => {
|
||||||
const styles = useStyles()
|
const styles = useStyles()
|
||||||
const hasDiff = compareWith && value !== compareWith
|
const hasDiff = compareWith && value !== compareWith
|
||||||
const coderTheme = useCoderTheme()
|
const coderTheme = useCoderTheme()
|
||||||
|
@ -25,6 +27,7 @@ export const SyntaxHighlighter: FC<{
|
||||||
renderSideBySide: true,
|
renderSideBySide: true,
|
||||||
readOnly: true,
|
readOnly: true,
|
||||||
},
|
},
|
||||||
|
...editorProps,
|
||||||
}
|
}
|
||||||
|
|
||||||
if (coderTheme.isLoading) {
|
if (coderTheme.isLoading) {
|
||||||
|
@ -46,5 +49,6 @@ const useStyles = makeStyles((theme) => ({
|
||||||
wrapper: {
|
wrapper: {
|
||||||
padding: theme.spacing(1, 0),
|
padding: theme.spacing(1, 0),
|
||||||
background: theme.palette.background.paper,
|
background: theme.palette.background.paper,
|
||||||
|
height: "100%",
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
import { Meta, StoryObj } from "@storybook/react"
|
||||||
|
import { HealthPageView } from "./HealthPage"
|
||||||
|
import { MockHealth } from "testHelpers/entities"
|
||||||
|
|
||||||
|
const meta: Meta<typeof HealthPageView> = {
|
||||||
|
title: "pages/HealthPageView",
|
||||||
|
component: HealthPageView,
|
||||||
|
args: {
|
||||||
|
tab: {
|
||||||
|
value: "derp",
|
||||||
|
set: () => {},
|
||||||
|
},
|
||||||
|
healthStatus: MockHealth,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default meta
|
||||||
|
type Story = StoryObj<typeof HealthPageView>
|
||||||
|
|
||||||
|
export const HealthPage: Story = {}
|
||||||
|
|
||||||
|
export const UnhealthPage: Story = {
|
||||||
|
args: {
|
||||||
|
healthStatus: {
|
||||||
|
...MockHealth,
|
||||||
|
healthy: false,
|
||||||
|
derp: {
|
||||||
|
...MockHealth.derp,
|
||||||
|
healthy: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
|
@ -0,0 +1,247 @@
|
||||||
|
import Box from "@mui/material/Box"
|
||||||
|
import { useQuery } from "@tanstack/react-query"
|
||||||
|
import { getHealth } from "api/api"
|
||||||
|
import { Loader } from "components/Loader/Loader"
|
||||||
|
import { useTab } from "hooks"
|
||||||
|
import { Helmet } from "react-helmet-async"
|
||||||
|
import { pageTitle } from "utils/page"
|
||||||
|
import { colors } from "theme/colors"
|
||||||
|
import CheckCircleOutlined from "@mui/icons-material/CheckCircleOutlined"
|
||||||
|
import ErrorOutline from "@mui/icons-material/ErrorOutline"
|
||||||
|
import { SyntaxHighlighter } from "components/SyntaxHighlighter/SyntaxHighlighter"
|
||||||
|
import { Stack } from "components/Stack/Stack"
|
||||||
|
import {
|
||||||
|
FullWidthPageHeader,
|
||||||
|
PageHeaderTitle,
|
||||||
|
PageHeaderSubtitle,
|
||||||
|
} from "components/PageHeader/FullWidthPageHeader"
|
||||||
|
import { Stats, StatsItem } from "components/Stats/Stats"
|
||||||
|
import { makeStyles } from "@mui/styles"
|
||||||
|
import { createDayString } from "utils/createDayString"
|
||||||
|
|
||||||
|
const sections = {
|
||||||
|
derp: "DERP",
|
||||||
|
access_url: "Access URL",
|
||||||
|
websocket: "Websocket",
|
||||||
|
database: "Database",
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export default function HealthPage() {
|
||||||
|
const tab = useTab("tab", "derp")
|
||||||
|
const { data: healthStatus } = useQuery({
|
||||||
|
queryKey: ["health"],
|
||||||
|
queryFn: () => getHealth(),
|
||||||
|
refetchInterval: 10_000,
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Helmet>
|
||||||
|
<title>{pageTitle("Health")}</title>
|
||||||
|
</Helmet>
|
||||||
|
|
||||||
|
{healthStatus ? (
|
||||||
|
<HealthPageView healthStatus={healthStatus.data} tab={tab} />
|
||||||
|
) : (
|
||||||
|
<Loader />
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function HealthPageView({
|
||||||
|
healthStatus,
|
||||||
|
tab,
|
||||||
|
}: {
|
||||||
|
healthStatus: Awaited<ReturnType<typeof getHealth>>["data"]
|
||||||
|
tab: ReturnType<typeof useTab>
|
||||||
|
}) {
|
||||||
|
const styles = useStyles()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
height: "calc(100vh - 62px - 36px)",
|
||||||
|
overflow: "hidden",
|
||||||
|
// Remove padding added from dashboard layout (.siteContent)
|
||||||
|
marginBottom: "-48px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FullWidthPageHeader sticky={false}>
|
||||||
|
<Stack direction="row" spacing={2} alignItems="center">
|
||||||
|
{healthStatus.healthy ? (
|
||||||
|
<CheckCircleOutlined
|
||||||
|
sx={{
|
||||||
|
width: 32,
|
||||||
|
height: 32,
|
||||||
|
color: (theme) => theme.palette.success.light,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<ErrorOutline
|
||||||
|
sx={{
|
||||||
|
width: 32,
|
||||||
|
height: 32,
|
||||||
|
color: (theme) => theme.palette.error.main,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<PageHeaderTitle>
|
||||||
|
{healthStatus.healthy ? "Healthy" : "Unhealthy"}
|
||||||
|
</PageHeaderTitle>
|
||||||
|
<PageHeaderSubtitle>
|
||||||
|
{healthStatus.healthy
|
||||||
|
? "All systems operational"
|
||||||
|
: "Some issues have been detected"}
|
||||||
|
</PageHeaderSubtitle>
|
||||||
|
</div>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Stats aria-label="Deployment details" className={styles.stats}>
|
||||||
|
<StatsItem
|
||||||
|
className={styles.statsItem}
|
||||||
|
label="Last check"
|
||||||
|
value={createDayString(healthStatus.time)}
|
||||||
|
/>
|
||||||
|
<StatsItem
|
||||||
|
className={styles.statsItem}
|
||||||
|
label="Coder version"
|
||||||
|
value={healthStatus.coder_version}
|
||||||
|
/>
|
||||||
|
</Stats>
|
||||||
|
</FullWidthPageHeader>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "start",
|
||||||
|
height: "100%",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: (theme) => theme.spacing(32),
|
||||||
|
flexShrink: 0,
|
||||||
|
borderRight: (theme) => `1px solid ${theme.palette.divider}`,
|
||||||
|
height: "100%",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
fontSize: 10,
|
||||||
|
textTransform: "uppercase",
|
||||||
|
fontWeight: 500,
|
||||||
|
color: (theme) => theme.palette.text.secondary,
|
||||||
|
padding: (theme) => theme.spacing(1.5, 3),
|
||||||
|
letterSpacing: "0.5px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Health
|
||||||
|
</Box>
|
||||||
|
<Box component="nav">
|
||||||
|
{Object.keys(sections)
|
||||||
|
.sort()
|
||||||
|
.map((key) => {
|
||||||
|
const label = sections[key as keyof typeof sections]
|
||||||
|
const isActive = tab.value === key
|
||||||
|
const isHealthy =
|
||||||
|
healthStatus[key as keyof typeof sections].healthy
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
component="button"
|
||||||
|
key={key}
|
||||||
|
onClick={() => {
|
||||||
|
tab.set(key)
|
||||||
|
}}
|
||||||
|
sx={{
|
||||||
|
background: isActive ? colors.gray[13] : "none",
|
||||||
|
border: "none",
|
||||||
|
fontSize: 14,
|
||||||
|
width: "100%",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 1,
|
||||||
|
textAlign: "left",
|
||||||
|
height: 36,
|
||||||
|
padding: (theme) => theme.spacing(0, 3),
|
||||||
|
cursor: "pointer",
|
||||||
|
pointerEvents: isActive ? "none" : "auto",
|
||||||
|
color: (theme) =>
|
||||||
|
isActive
|
||||||
|
? theme.palette.text.primary
|
||||||
|
: theme.palette.text.secondary,
|
||||||
|
"&:hover": {
|
||||||
|
background: (theme) => theme.palette.action.hover,
|
||||||
|
color: (theme) => theme.palette.text.primary,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isHealthy ? (
|
||||||
|
<CheckCircleOutlined
|
||||||
|
sx={{
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
color: (theme) => theme.palette.success.light,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<ErrorOutline
|
||||||
|
sx={{
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
color: (theme) => theme.palette.error.main,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{label}
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
{/* 62px - navbar and 36px - the bottom bar */}
|
||||||
|
<Box sx={{ height: "100%", overflowY: "auto", width: "100%" }}>
|
||||||
|
<SyntaxHighlighter
|
||||||
|
language="json"
|
||||||
|
editorProps={{ height: "100%" }}
|
||||||
|
value={JSON.stringify(
|
||||||
|
healthStatus[tab.value as keyof typeof healthStatus],
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme) => ({
|
||||||
|
stats: {
|
||||||
|
padding: 0,
|
||||||
|
border: 0,
|
||||||
|
gap: theme.spacing(6),
|
||||||
|
rowGap: theme.spacing(3),
|
||||||
|
flex: 1,
|
||||||
|
|
||||||
|
[theme.breakpoints.down("md")]: {
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
alignItems: "flex-start",
|
||||||
|
gap: theme.spacing(1),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
statsItem: {
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: 0,
|
||||||
|
padding: 0,
|
||||||
|
|
||||||
|
"& > span:first-of-type": {
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: 500,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}))
|
|
@ -1862,3 +1862,422 @@ export const MockLicenseResponse: GetLicensesResponse[] = [
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
export const MockHealth = {
|
||||||
|
time: "2023-08-01T16:51:03.29792825Z",
|
||||||
|
healthy: true,
|
||||||
|
failing_sections: null,
|
||||||
|
derp: {
|
||||||
|
healthy: true,
|
||||||
|
regions: {
|
||||||
|
"999": {
|
||||||
|
healthy: true,
|
||||||
|
region: {
|
||||||
|
EmbeddedRelay: true,
|
||||||
|
RegionID: 999,
|
||||||
|
RegionCode: "coder",
|
||||||
|
RegionName: "Council Bluffs, Iowa",
|
||||||
|
Nodes: [
|
||||||
|
{
|
||||||
|
Name: "999stun0",
|
||||||
|
RegionID: 999,
|
||||||
|
HostName: "stun.l.google.com",
|
||||||
|
STUNPort: 19302,
|
||||||
|
STUNOnly: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "999b",
|
||||||
|
RegionID: 999,
|
||||||
|
HostName: "dev.coder.com",
|
||||||
|
STUNPort: -1,
|
||||||
|
DERPPort: 443,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
node_reports: [
|
||||||
|
{
|
||||||
|
healthy: true,
|
||||||
|
node: {
|
||||||
|
Name: "999stun0",
|
||||||
|
RegionID: 999,
|
||||||
|
HostName: "stun.l.google.com",
|
||||||
|
STUNPort: 19302,
|
||||||
|
STUNOnly: true,
|
||||||
|
},
|
||||||
|
node_info: {
|
||||||
|
TokenBucketBytesPerSecond: 0,
|
||||||
|
TokenBucketBytesBurst: 0,
|
||||||
|
},
|
||||||
|
can_exchange_messages: false,
|
||||||
|
round_trip_ping: 0,
|
||||||
|
uses_websocket: false,
|
||||||
|
client_logs: [],
|
||||||
|
client_errs: [],
|
||||||
|
error: null,
|
||||||
|
stun: {
|
||||||
|
Enabled: true,
|
||||||
|
CanSTUN: true,
|
||||||
|
Error: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
healthy: true,
|
||||||
|
node: {
|
||||||
|
Name: "999b",
|
||||||
|
RegionID: 999,
|
||||||
|
HostName: "dev.coder.com",
|
||||||
|
STUNPort: -1,
|
||||||
|
DERPPort: 443,
|
||||||
|
},
|
||||||
|
node_info: {
|
||||||
|
TokenBucketBytesPerSecond: 0,
|
||||||
|
TokenBucketBytesBurst: 0,
|
||||||
|
},
|
||||||
|
can_exchange_messages: true,
|
||||||
|
round_trip_ping: 7674330,
|
||||||
|
uses_websocket: false,
|
||||||
|
client_logs: [
|
||||||
|
[
|
||||||
|
"derphttp.Client.Connect: connecting to https://dev.coder.com/derp",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"derphttp.Client.Connect: connecting to https://dev.coder.com/derp",
|
||||||
|
],
|
||||||
|
],
|
||||||
|
client_errs: [[], []],
|
||||||
|
error: null,
|
||||||
|
stun: {
|
||||||
|
Enabled: false,
|
||||||
|
CanSTUN: false,
|
||||||
|
Error: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
error: null,
|
||||||
|
},
|
||||||
|
"10007": {
|
||||||
|
healthy: true,
|
||||||
|
region: {
|
||||||
|
EmbeddedRelay: false,
|
||||||
|
RegionID: 10007,
|
||||||
|
RegionCode: "coder_sydney",
|
||||||
|
RegionName: "sydney",
|
||||||
|
Nodes: [
|
||||||
|
{
|
||||||
|
Name: "10007stun0",
|
||||||
|
RegionID: 10007,
|
||||||
|
HostName: "stun.l.google.com",
|
||||||
|
STUNPort: 19302,
|
||||||
|
STUNOnly: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "10007a",
|
||||||
|
RegionID: 10007,
|
||||||
|
HostName: "sydney.dev.coder.com",
|
||||||
|
STUNPort: -1,
|
||||||
|
DERPPort: 443,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
node_reports: [
|
||||||
|
{
|
||||||
|
healthy: true,
|
||||||
|
node: {
|
||||||
|
Name: "10007stun0",
|
||||||
|
RegionID: 10007,
|
||||||
|
HostName: "stun.l.google.com",
|
||||||
|
STUNPort: 19302,
|
||||||
|
STUNOnly: true,
|
||||||
|
},
|
||||||
|
node_info: {
|
||||||
|
TokenBucketBytesPerSecond: 0,
|
||||||
|
TokenBucketBytesBurst: 0,
|
||||||
|
},
|
||||||
|
can_exchange_messages: false,
|
||||||
|
round_trip_ping: 0,
|
||||||
|
uses_websocket: false,
|
||||||
|
client_logs: [],
|
||||||
|
client_errs: [],
|
||||||
|
error: null,
|
||||||
|
stun: {
|
||||||
|
Enabled: true,
|
||||||
|
CanSTUN: true,
|
||||||
|
Error: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
healthy: true,
|
||||||
|
node: {
|
||||||
|
Name: "10007a",
|
||||||
|
RegionID: 10007,
|
||||||
|
HostName: "sydney.dev.coder.com",
|
||||||
|
STUNPort: -1,
|
||||||
|
DERPPort: 443,
|
||||||
|
},
|
||||||
|
node_info: {
|
||||||
|
TokenBucketBytesPerSecond: 0,
|
||||||
|
TokenBucketBytesBurst: 0,
|
||||||
|
},
|
||||||
|
can_exchange_messages: true,
|
||||||
|
round_trip_ping: 170527034,
|
||||||
|
uses_websocket: false,
|
||||||
|
client_logs: [
|
||||||
|
[
|
||||||
|
"derphttp.Client.Connect: connecting to https://sydney.dev.coder.com/derp",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"derphttp.Client.Connect: connecting to https://sydney.dev.coder.com/derp",
|
||||||
|
],
|
||||||
|
],
|
||||||
|
client_errs: [[], []],
|
||||||
|
error: null,
|
||||||
|
stun: {
|
||||||
|
Enabled: false,
|
||||||
|
CanSTUN: false,
|
||||||
|
Error: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
error: null,
|
||||||
|
},
|
||||||
|
"10008": {
|
||||||
|
healthy: true,
|
||||||
|
region: {
|
||||||
|
EmbeddedRelay: false,
|
||||||
|
RegionID: 10008,
|
||||||
|
RegionCode: "coder_europe-frankfurt",
|
||||||
|
RegionName: "europe-frankfurt",
|
||||||
|
Nodes: [
|
||||||
|
{
|
||||||
|
Name: "10008stun0",
|
||||||
|
RegionID: 10008,
|
||||||
|
HostName: "stun.l.google.com",
|
||||||
|
STUNPort: 19302,
|
||||||
|
STUNOnly: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "10008a",
|
||||||
|
RegionID: 10008,
|
||||||
|
HostName: "europe.dev.coder.com",
|
||||||
|
STUNPort: -1,
|
||||||
|
DERPPort: 443,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
node_reports: [
|
||||||
|
{
|
||||||
|
healthy: true,
|
||||||
|
node: {
|
||||||
|
Name: "10008stun0",
|
||||||
|
RegionID: 10008,
|
||||||
|
HostName: "stun.l.google.com",
|
||||||
|
STUNPort: 19302,
|
||||||
|
STUNOnly: true,
|
||||||
|
},
|
||||||
|
node_info: {
|
||||||
|
TokenBucketBytesPerSecond: 0,
|
||||||
|
TokenBucketBytesBurst: 0,
|
||||||
|
},
|
||||||
|
can_exchange_messages: false,
|
||||||
|
round_trip_ping: 0,
|
||||||
|
uses_websocket: false,
|
||||||
|
client_logs: [],
|
||||||
|
client_errs: [],
|
||||||
|
error: null,
|
||||||
|
stun: {
|
||||||
|
Enabled: true,
|
||||||
|
CanSTUN: true,
|
||||||
|
Error: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
healthy: true,
|
||||||
|
node: {
|
||||||
|
Name: "10008a",
|
||||||
|
RegionID: 10008,
|
||||||
|
HostName: "europe.dev.coder.com",
|
||||||
|
STUNPort: -1,
|
||||||
|
DERPPort: 443,
|
||||||
|
},
|
||||||
|
node_info: {
|
||||||
|
TokenBucketBytesPerSecond: 0,
|
||||||
|
TokenBucketBytesBurst: 0,
|
||||||
|
},
|
||||||
|
can_exchange_messages: true,
|
||||||
|
round_trip_ping: 111329690,
|
||||||
|
uses_websocket: false,
|
||||||
|
client_logs: [
|
||||||
|
[
|
||||||
|
"derphttp.Client.Connect: connecting to https://europe.dev.coder.com/derp",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"derphttp.Client.Connect: connecting to https://europe.dev.coder.com/derp",
|
||||||
|
],
|
||||||
|
],
|
||||||
|
client_errs: [[], []],
|
||||||
|
error: null,
|
||||||
|
stun: {
|
||||||
|
Enabled: false,
|
||||||
|
CanSTUN: false,
|
||||||
|
Error: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
error: null,
|
||||||
|
},
|
||||||
|
"10009": {
|
||||||
|
healthy: true,
|
||||||
|
region: {
|
||||||
|
EmbeddedRelay: false,
|
||||||
|
RegionID: 10009,
|
||||||
|
RegionCode: "coder_brazil-saopaulo",
|
||||||
|
RegionName: "brazil-saopaulo",
|
||||||
|
Nodes: [
|
||||||
|
{
|
||||||
|
Name: "10009stun0",
|
||||||
|
RegionID: 10009,
|
||||||
|
HostName: "stun.l.google.com",
|
||||||
|
STUNPort: 19302,
|
||||||
|
STUNOnly: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "10009a",
|
||||||
|
RegionID: 10009,
|
||||||
|
HostName: "brazil.dev.coder.com",
|
||||||
|
STUNPort: -1,
|
||||||
|
DERPPort: 443,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
node_reports: [
|
||||||
|
{
|
||||||
|
healthy: true,
|
||||||
|
node: {
|
||||||
|
Name: "10009stun0",
|
||||||
|
RegionID: 10009,
|
||||||
|
HostName: "stun.l.google.com",
|
||||||
|
STUNPort: 19302,
|
||||||
|
STUNOnly: true,
|
||||||
|
},
|
||||||
|
node_info: {
|
||||||
|
TokenBucketBytesPerSecond: 0,
|
||||||
|
TokenBucketBytesBurst: 0,
|
||||||
|
},
|
||||||
|
can_exchange_messages: false,
|
||||||
|
round_trip_ping: 0,
|
||||||
|
uses_websocket: false,
|
||||||
|
client_logs: [],
|
||||||
|
client_errs: [],
|
||||||
|
error: null,
|
||||||
|
stun: {
|
||||||
|
Enabled: true,
|
||||||
|
CanSTUN: true,
|
||||||
|
Error: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
healthy: true,
|
||||||
|
node: {
|
||||||
|
Name: "10009a",
|
||||||
|
RegionID: 10009,
|
||||||
|
HostName: "brazil.dev.coder.com",
|
||||||
|
STUNPort: -1,
|
||||||
|
DERPPort: 443,
|
||||||
|
},
|
||||||
|
node_info: {
|
||||||
|
TokenBucketBytesPerSecond: 0,
|
||||||
|
TokenBucketBytesBurst: 0,
|
||||||
|
},
|
||||||
|
can_exchange_messages: true,
|
||||||
|
round_trip_ping: 138185506,
|
||||||
|
uses_websocket: false,
|
||||||
|
client_logs: [
|
||||||
|
[
|
||||||
|
"derphttp.Client.Connect: connecting to https://brazil.dev.coder.com/derp",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"derphttp.Client.Connect: connecting to https://brazil.dev.coder.com/derp",
|
||||||
|
],
|
||||||
|
],
|
||||||
|
client_errs: [[], []],
|
||||||
|
error: null,
|
||||||
|
stun: {
|
||||||
|
Enabled: false,
|
||||||
|
CanSTUN: false,
|
||||||
|
Error: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
error: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
netcheck: {
|
||||||
|
UDP: true,
|
||||||
|
IPv6: false,
|
||||||
|
IPv4: true,
|
||||||
|
IPv6CanSend: false,
|
||||||
|
IPv4CanSend: true,
|
||||||
|
OSHasIPv6: true,
|
||||||
|
ICMPv4: false,
|
||||||
|
MappingVariesByDestIP: false,
|
||||||
|
HairPinning: null,
|
||||||
|
UPnP: false,
|
||||||
|
PMP: false,
|
||||||
|
PCP: false,
|
||||||
|
PreferredDERP: 999,
|
||||||
|
RegionLatency: {
|
||||||
|
"999": 1638180,
|
||||||
|
"10007": 174853022,
|
||||||
|
"10008": 112142029,
|
||||||
|
"10009": 138855606,
|
||||||
|
},
|
||||||
|
RegionV4Latency: {
|
||||||
|
"999": 1638180,
|
||||||
|
"10007": 174853022,
|
||||||
|
"10008": 112142029,
|
||||||
|
"10009": 138855606,
|
||||||
|
},
|
||||||
|
RegionV6Latency: {},
|
||||||
|
GlobalV4: "34.71.26.24:55368",
|
||||||
|
GlobalV6: "",
|
||||||
|
CaptivePortal: null,
|
||||||
|
},
|
||||||
|
netcheck_err: null,
|
||||||
|
netcheck_logs: [
|
||||||
|
"netcheck: netcheck.runProbe: got STUN response for 10007stun0 from 34.71.26.24:55368 (9b07930007da49dd7df79bc7) in 1.791799ms",
|
||||||
|
"netcheck: netcheck.runProbe: got STUN response for 999stun0 from 34.71.26.24:55368 (7397fec097f1d5b01364566b) in 1.791529ms",
|
||||||
|
"netcheck: netcheck.runProbe: got STUN response for 10008stun0 from 34.71.26.24:55368 (1fdaaa016ca386485f097f68) in 2.192899ms",
|
||||||
|
"netcheck: netcheck.runProbe: got STUN response for 10009stun0 from 34.71.26.24:55368 (2596fe60895fbd9542823a76) in 2.146459ms",
|
||||||
|
"netcheck: netcheck.runProbe: got STUN response for 10007stun0 from 34.71.26.24:55368 (19ec320f3b76e8b027b06d3e) in 2.139619ms",
|
||||||
|
"netcheck: netcheck.runProbe: got STUN response for 999stun0 from 34.71.26.24:55368 (a17973bc57c35e606c0f46f5) in 2.131089ms",
|
||||||
|
"netcheck: netcheck.runProbe: got STUN response for 10008stun0 from 34.71.26.24:55368 (c958e15209d139a6e410f13a) in 2.127549ms",
|
||||||
|
"netcheck: netcheck.runProbe: got STUN response for 10009stun0 from 34.71.26.24:55368 (284a1b64dff22f40a3514524) in 2.107549ms",
|
||||||
|
"netcheck: [v1] measureAllICMPLatency: listen ip4:icmp 0.0.0.0: socket: operation not permitted",
|
||||||
|
"netcheck: [v1] report: udp=true v6=false v6os=true mapvarydest=false hair= portmap= v4a=34.71.26.24:55368 derp=999 derpdist=999v4:2ms,10007v4:175ms,10008v4:112ms,10009v4:139ms",
|
||||||
|
],
|
||||||
|
error: null,
|
||||||
|
},
|
||||||
|
access_url: {
|
||||||
|
access_url: "https://dev.coder.com",
|
||||||
|
healthy: true,
|
||||||
|
reachable: true,
|
||||||
|
status_code: 200,
|
||||||
|
healthz_response: "OK",
|
||||||
|
error: null,
|
||||||
|
},
|
||||||
|
websocket: {
|
||||||
|
healthy: true,
|
||||||
|
response: {
|
||||||
|
body: "",
|
||||||
|
code: 101,
|
||||||
|
},
|
||||||
|
error: null,
|
||||||
|
},
|
||||||
|
database: {
|
||||||
|
healthy: true,
|
||||||
|
reachable: true,
|
||||||
|
latency: 92570,
|
||||||
|
error: null,
|
||||||
|
},
|
||||||
|
coder_version: "v0.27.1-devel+c575292",
|
||||||
|
}
|
||||||
|
|
|
@ -387,4 +387,8 @@ export const handlers = [
|
||||||
rest.get("/api/v2/workspaceagents/:agent/logs", (_, res, ctx) => {
|
rest.get("/api/v2/workspaceagents/:agent/logs", (_, res, ctx) => {
|
||||||
return res(ctx.status(200), ctx.json(M.MockWorkspaceAgentLogs))
|
return res(ctx.status(200), ctx.json(M.MockWorkspaceAgentLogs))
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
rest.get("/api/v2/debug/health", (_, res, ctx) => {
|
||||||
|
return res(ctx.status(200), ctx.json(M.MockHealth))
|
||||||
|
}),
|
||||||
]
|
]
|
||||||
|
|
Loading…
Reference in New Issue