Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 8 additions & 6 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -195,12 +195,14 @@ Follow additional instructions when working in the `@mui/internal-docs-infra` (`
- **7.2** Use `async/await` for asynchronous code. Avoid using `.then()` and `.catch()`.
- **7.3** Use `import { ... } from '...'` syntax for imports. Avoid using `require()`.
- **7.4** Use ES modules and `import`/`export` syntax.
- **7.5** This package is ESM only. Do not add any CJS code.
- **7.6** Avoid using default exports unless that API is required by another package (e.g. webpack). Use named exports for all functions, types, and constants.
- **7.7** Always try to parallelize asynchronous operations using `Promise.all()` or similar techniques. If the result of an async operation is not needed for subsequent operations, it should be started as early as possible and awaited later.
- **7.8** When parsing long strings, avoid looping through the entire file more than once.
- **7.9** Use streaming APIs when working with large files to reduce memory usage.
- **7.10** Avoid using regex when string methods can achieve the same result more clearly and efficiently.
- **7.5** When importing from React, always use namespace imports: `import * as React from 'react'`. Access React exports via the namespace (e.g., `React.Suspense`, `React.useState`). Do not use named imports like `import { Suspense } from 'react'`.
- **7.6** This package is ESM only. Do not add any CJS code.
- **7.7** Avoid using default exports unless that API is required by another package (e.g. webpack). Use named exports for all functions, types, and constants.
- **7.8** Always try to parallelize asynchronous operations using `Promise.all()` or similar techniques. If the result of an async operation is not needed for subsequent operations, it should be started as early as possible and awaited later.
- **7.9** When parsing long strings, avoid looping through the entire file more than once.
- **7.10** Use streaming APIs when working with large files to reduce memory usage.
- **7.11** Avoid using regex when string methods can achieve the same result more clearly and efficiently.
- **7.12** When building skeleton/loading UI, use a single presentational component with a `loading` prop that renders skeletons internally, rather than creating separate skeleton components. This keeps the component API simple and ensures the skeleton matches the actual layout.

### Dependencies, Debugging & Performance

Expand Down
68 changes: 68 additions & 0 deletions apps/code-infra-dashboard/app/(dashboard)/kpis/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import * as React from 'react';
import Box from '@mui/material/Box';
import Card from '@mui/material/Card';
import CardContent from '@mui/material/CardContent';
import Grid from '@mui/material/Grid';
import Heading from '@/components/Heading';
import KpiCard from '@/components/KpiCard';
import LinkCardActionArea from '@/components/LinkCardActionArea';
import { kpiRegistry, type KpiConfig } from '@/lib/kpis';

export const revalidate = 3600; // 1 hour ISR

export const metadata = {
title: 'KPIs Overview',
description: 'Key Performance Indicators dashboard',
};

// Async component that fetches data for a single KPI
async function KpiCardAsync({ kpi }: { kpi: KpiConfig }) {
const result = await kpi.fetch();
return <KpiCard kpi={kpi} result={result} />;
}

// Group KPIs by data source for display
const kpisBySource = kpiRegistry.reduce((acc, kpi) => {
const group = acc.get(kpi.dataSource) || [];
group.push(kpi);
acc.set(kpi.dataSource, group);
return acc;
}, new Map<string, KpiConfig[]>());

const sourceLabels: Record<string, string> = {
github: 'GitHub',
zendesk: 'Zendesk',
ossInsight: 'OSS Insight',
circleCI: 'CircleCI',
hibob: 'HiBob',
store: 'Store',
};

export default function KpisPage() {
return (
<Box sx={{ mt: 4 }}>
<Heading level={1}>KPIs Dashboard</Heading>

{Array.from(kpisBySource.entries()).map(([source, kpis]) => (
<Box key={source} sx={{ mt: 4 }}>
<Heading level={2}>{sourceLabels[source] || source} KPIs</Heading>
<Grid container spacing={3} sx={{ mt: 1 }}>
{kpis.map((kpi) => (
<Grid size={{ xs: 12, sm: 6, md: 4 }} key={kpi.id}>
<Card sx={{ height: '100%' }}>
<LinkCardActionArea href={`/kpis/${kpi.id}`}>
<CardContent sx={{ flexGrow: 1 }}>
<React.Suspense fallback={<KpiCard kpi={kpi} loading />}>
<KpiCardAsync kpi={kpi} />
</React.Suspense>
</CardContent>
</LinkCardActionArea>
</Card>
</Grid>
))}
</Grid>
</Box>
))}
</Box>
);
}
41 changes: 41 additions & 0 deletions apps/code-infra-dashboard/app/kpis/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import * as React from 'react';
import { notFound } from 'next/navigation';
import Box from '@mui/material/Box';
import { getKpiById, getAllKpiIds } from '@/lib/kpis';
import KpiDetail from '@/views/KpiDetail';

export async function generateStaticParams() {
return getAllKpiIds().map((id) => ({ id }));
}

export const revalidate = 3600; // 1 hour ISR

interface PageProps {
params: Promise<{ id: string }>;
}

export default async function KpiPage({ params }: PageProps) {
const { id } = await params;
const kpi = getKpiById(id);

if (!kpi) {
notFound();
}

const result = await kpi.fetch();

return (
<Box sx={{ p: 2 }}>
<KpiDetail kpi={kpi} result={result} />
</Box>
);
}

export async function generateMetadata({ params }: PageProps) {
const { id } = await params;
const kpi = getKpiById(id);
return {
title: kpi?.title ?? 'KPI',
description: kpi?.description,
};
}
1 change: 1 addition & 0 deletions apps/code-infra-dashboard/next.config.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
transpilePackages: ['@mui/internal-bundle-size-checker'],
serverExternalPackages: ['@heroku/socksv5'],
};

export default nextConfig;
5 changes: 4 additions & 1 deletion apps/code-infra-dashboard/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,13 @@
"dayjs": "^1.11.19",
"diff": "^8.0.3",
"etag": "^1.8.1",
"mysql2": "^3.15.3",
"next": "^16.1.6",
"pako": "^2.1.0",
"react": "^19.2.3",
"react-dom": "^19.2.3",
"semver": "^7.7.3"
"semver": "^7.7.3",
"ssh2-promise": "^1.0.3"
},
"devDependencies": {
"@types/etag": "1.8.4",
Expand All @@ -32,6 +34,7 @@
"@types/react": "19.2.8",
"@types/react-dom": "19.2.3",
"@types/semver": "7.7.1",
"baseline-browser-mapping": "^2.9.13",
"typescript": "5.9.3"
},
"scripts": {
Expand Down
113 changes: 113 additions & 0 deletions apps/code-infra-dashboard/src/components/HealthBadge.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import * as React from 'react';
import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography';
import Stack from '@mui/material/Stack';
import Skeleton from '@mui/material/Skeleton';

type HealthLevel = 'ok' | 'warning' | 'problem' | 'unknown';

const levelsInfo: Record<HealthLevel, { label: string; backgroundColor: string }> = {
ok: { label: 'Ok', backgroundColor: 'green' },
warning: { label: 'Warning', backgroundColor: 'orange' },
problem: { label: 'Problem', backgroundColor: 'red' },
unknown: { label: 'Unknown', backgroundColor: 'grey' },
};

interface HealthBadgeLabelProps {
level: HealthLevel;
}

function HealthBadgeLabel({ level }: HealthBadgeLabelProps): React.ReactElement {
const info = levelsInfo[level];
return (
<Box
sx={{
backgroundColor: info.backgroundColor,
color: 'white',
borderRadius: '0.2em',
fontWeight: 700,
fontSize: 14,
display: 'flex',
alignItems: 'center',
padding: '2px 12px',
textAlign: 'center',
}}
>
{info.label}
</Box>
);
}

function computeLevel(
value: number | null,
warning: number,
problem: number,
lowerIsBetter: boolean,
): HealthLevel {
if (value == null) {
return 'unknown';
}

if (lowerIsBetter) {
if (value > problem) {
return 'problem';
}
if (value > warning) {
return 'warning';
}
return 'ok';
}
if (value < problem) {
return 'problem';
}
if (value < warning) {
return 'warning';
}
return 'ok';
}

export interface HealthBadgeProps {
value: number | null;
warning: number;
problem: number;
unit: string;
lowerIsBetter?: boolean;
loading?: boolean;
metadata?: string;
}

export default function HealthBadge({
value,
warning,
problem,
unit,
lowerIsBetter = false,
loading = false,
metadata,
}: HealthBadgeProps): React.ReactElement {
const level = loading ? 'unknown' : computeLevel(value, warning, problem, lowerIsBetter);

return (
<Stack spacing={0.5}>
<Stack direction="row" spacing={1} alignItems="center">
<Typography sx={{ width: 100 }}>Health:</Typography>
{loading ? (
<Skeleton variant="rounded" width={80} height={24} />
) : (
<HealthBadgeLabel level={level} />
)}
</Stack>
<Stack direction="row" spacing={1} alignItems="center">
<Typography sx={{ width: 100 }}>Value:</Typography>
<Typography sx={{ flex: 1 }}>
{loading ? <Skeleton variant="text" width={100} /> : `${value ?? 'N/A'}${unit}`}
</Typography>
</Stack>
{metadata && (
<Typography variant="body2" color="text.secondary">
{metadata}
</Typography>
)}
</Stack>
);
}
55 changes: 55 additions & 0 deletions apps/code-infra-dashboard/src/components/KpiCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
'use client';

import * as React from 'react';
import Typography from '@mui/material/Typography';
import Skeleton from '@mui/material/Skeleton';
import Stack from '@mui/material/Stack';
import Tooltip from '@mui/material/Tooltip';
import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline';
import HealthBadge from './HealthBadge';
import type { KpiConfig, KpiResult } from '../lib/kpis';

interface KpiCardProps {
kpi: KpiConfig;
result?: KpiResult;
loading?: boolean;
}

export default function KpiCard({
kpi,
result,
loading = false,
}: KpiCardProps): React.ReactElement {
return (
<React.Fragment>
{loading ? (
<Skeleton variant="text" width={150} height={32} />
) : (
<Stack direction="row" alignItems="center" spacing={1}>
<Typography variant="h6" component="h3">
{kpi.title}
</Typography>
{result?.error && (
<Tooltip title={result.error}>
<ErrorOutlineIcon color="error" fontSize="small" />
</Tooltip>
)}
</Stack>
)}
{kpi.description && (
<Typography variant="body2" color="text.secondary" sx={{ mb: 1 }}>
{kpi.description}
</Typography>
)}
<HealthBadge
value={result?.value ?? null}
warning={kpi.thresholds.warning}
problem={kpi.thresholds.problem}
unit={kpi.unit}
lowerIsBetter={kpi.thresholds.lowerIsBetter}
loading={loading}
metadata={result?.metadata}
/>
</React.Fragment>
);
}
30 changes: 30 additions & 0 deletions apps/code-infra-dashboard/src/components/LinkCardActionArea.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
'use client';

import * as React from 'react';
import CardActionArea from '@mui/material/CardActionArea';
import NextLink from 'next/link';

interface LinkCardActionAreaProps {
href: string;
children: React.ReactNode;
}

export default function LinkCardActionArea({
href,
children,
}: LinkCardActionAreaProps): React.ReactElement {
return (
<CardActionArea
component={NextLink}
href={href}
sx={{
height: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'stretch',
}}
>
{children}
</CardActionArea>
);
}
24 changes: 24 additions & 0 deletions apps/code-infra-dashboard/src/lib/kpis/fetchers/circleCI.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import type { KpiResult } from '../types';
import { checkHttpError, errorResult, successResult } from './utils';

export async function fetchCompletionTime(repository: string): Promise<KpiResult> {
const response = await fetch(
`https://circleci.com/api/v2/insights/github/mui/${repository}/workflows/pipeline/summary?analytics-segmentation=web-ui-insights&reporting-window=last-7-days&workflow-name=pipeline`,
{ next: { revalidate: 3600 } },
);

const httpError = checkHttpError(response);
if (httpError) {
return httpError;
}

const data = await response.json();

if (!data.metrics?.duration_metrics?.median) {
return errorResult('No duration metrics available');
}

const medianMinutes = Math.round((data.metrics.duration_metrics.median / 60) * 100) / 100;

return successResult(medianMinutes, `Based on the last 7 days (${data.metrics.total_runs} runs)`);
}
Loading
Loading