import hljs from 'highlight.js/lib/core'; import { StrictMode, useEffect, useMemo, useRef, useState } from 'react'; import { createRoot } from 'react-dom/client'; import 'highlight.js/styles/hybrid.css'; import styles from './api.module.css'; hljs.registerLanguage('json', require('highlight.js/lib/languages/json')); hljs.registerLanguage( 'plaintext', require('highlight.js/lib/languages/plaintext'), ); const CodeBlock = ({ value, language, originalValue, }: { value: string; language?: string | null; originalValue?: string }) => { const codeRef = useRef(null); const langClass = language ? `language-${language}` : undefined; // biome-ignore lint/correctness/useExhaustiveDependencies: useEffect(() => { if (codeRef.current && !codeRef.current.dataset.highlighted) { hljs.highlightElement(codeRef.current); } }, [value]); const copyToClipboard = () => { navigator.clipboard.writeText(originalValue || value).catch((e) => { console.error('Failed to copy text', e); }); }; return (
      
); }; type ProjectsResponse = { projects: ProjectResponse[]; }; type ProjectResponse = { id: string; owner: string; repo: string; name: string | null; short_name: string | null; platform: string | null; default_version: string; report_versions: string[]; report_categories: CategoryResponse[]; }; type CategoryResponse = { id: string; name: string; }; const fetchProjects = async () => { const url = new URL('/projects', window.location.origin); url.searchParams.append('sort', 'name'); const response = await fetch(url, { headers: { Accept: 'application/json' }, }); if (!response.ok) { throw new Error('Failed to fetch projects'); } return (await response.json()) as ProjectsResponse; }; const fetchProjectById = async (id: string, version: string | null) => { const url = new URL(`/projects/${id}`, window.location.origin); if (version) { url.searchParams.append('version', version); } const response = await fetch(url, { headers: { Accept: 'application/json' }, }); if (!response.ok) { throw new Error('Failed to fetch projects'); } return (await response.json()) as ProjectResponse; }; const fetchApiUrl = async (url: string) => { const response = await fetch(url, { headers: { Accept: 'application/json' }, }); if (!response.ok) { throw new Error('Failed to fetch projects'); } return await response.text(); }; const ProjectForm = () => { const [projects, setProjects] = useState([]); const [loadingProjects, setLoadingProjects] = useState(true); const [loadingProject, setLoadingProject] = useState(true); type Selected = { project: string | null; version: string | null; category: string | null; subcategory: string | null; }; const [selected, setSelected] = useState({ project: null, version: null, category: null, subcategory: null, }); const [mode, setMode] = useState('overview'); const [format, setFormat] = useState('json'); const [formatOptions, setFormatOptions] = useState({}); const [useProjectId, setUseProjectId] = useState(false); const [currentProject, setCurrentProject] = useState( null, ); const categories = useMemo(() => { const result: Record< string, { name: string; subcategories: CategoryResponse[] } > = {}; currentProject?.report_categories.forEach((c) => { const [top, sub] = c.id.split('.', 2); if (!result[top]) { result[top] = { name: c.id === top ? c.name : top, subcategories: [] }; } if (sub) { result[top].subcategories.push({ id: sub, name: c.name }); } else { result[top].name = c.name; } }); return result; }, [currentProject]); useEffect(() => { setLoadingProjects(true); fetchProjects() .then((response) => { setProjects(response.projects); const url = new URL(document.location.href); const deferredProject = url.searchParams.get('project'); if (deferredProject) { const project = response.projects.find( (p) => p.id.toString() === deferredProject, ); if (project) { setCurrentProject(project); setSelected({ project: project.id, version: null, category: null, subcategory: null, }); } } setLoadingProjects(false); }) .catch((error) => { console.error('Error fetching projects:', error); setProjects([]); setLoadingProjects(false); }); }, []); useEffect(() => { if (selected.project === null) { return; } setLoadingProject(true); fetchProjectById(selected.project, selected.version) .then((response) => { setCurrentProject(response); setLoadingProject(false); }) .catch((error) => { console.error('Error fetching project by ID:', error); setCurrentProject(null); setLoadingProject(false); }); }, [selected.project, selected.version]); const url = new URL('/', window.location.origin); url.searchParams.append('mode', mode); if (currentProject) { if (useProjectId) { url.pathname = `/projects/${currentProject.id}`; if (selected.version) { url.searchParams.append('version', selected.version); } } else { url.pathname = `/${currentProject.owner}/${currentProject.repo}`; if (selected.version) { url.pathname += `/${selected.version || currentProject.default_version}`; } } if (selected.category) { const cat = selected.subcategory ? `${selected.category}.${selected.subcategory}` : selected.category; url.searchParams.append('category', cat); } } url.pathname += `.${format}`; if (mode === 'shield') { if (formatOptions.label) { url.searchParams.append('label', formatOptions.label); } if (formatOptions.labelColor) { url.searchParams.append('labelColor', formatOptions.labelColor); } if (formatOptions.color) { url.searchParams.append('color', formatOptions.color); } if (formatOptions.style) { url.searchParams.append('style', formatOptions.style); } if (formatOptions.measure) { url.searchParams.append('measure', formatOptions.measure); } } const updateMode = (newMode: string) => { setMode(newMode); setFormat(formatsByMode[newMode][0]); setFormatOptions({}); }; const updateFormat = (newFormat: string) => { setFormat(newFormat); }; let modeOptions = null; if (mode === 'shield') { modeOptions = ( <>
); } return (
{selected.category && categories[selected.category]?.subcategories.length > 0 && (
)}

{modeOptions} {currentProject && }
); }; const formatsByMode: Record = { overview: ['json', 'svg', 'png'], measures: ['json', 'binpb'], report: ['json', 'binpb'], shield: ['svg', 'png', 'json'], history: ['json'], }; const prettifyJson = (data: string): string => { let parsed: any; try { parsed = JSON.parse(data); } catch (error) { // Return the original data if parsing fails return data; } return JSON.stringify(parsed, null, 2); }; const ApiPreview = ({ url, format, }: { url: string | null; format: string }) => { type ResponseData = { data: string; format: string; }; const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [response, setResponse] = useState(null); // biome-ignore lint/correctness/useExhaustiveDependencies: useEffect(() => { setResponse(null); }, [url]); const fetchData = async () => { if (!url) { return; } setLoading(true); setError(null); try { const response = await fetchApiUrl(url); setResponse({ data: response, format }); } catch (error) { console.error('Error fetching API URL:', error); setError('Failed to fetch data'); } finally { setLoading(false); } }; const stringified = useMemo(() => { if (!response) { return ''; } const maxLength = 250000; // 250 KB let stringified: string; if (response.format === 'json') { stringified = prettifyJson(response.data); } else if (response.format === 'binpb') { stringified = `// Binary protobuf size ${formatBytes(response.data.length)}`; } else { stringified = response.data; } if (stringified.length > maxLength) { return `// Response was ${formatBytes(response.data.length)}, preview truncated to ${formatBytes(maxLength, 0)}\n${stringified.slice(0, maxLength)}`; } return stringified; }, [response]); if (!url) { return null; } let preview = null; if (response) { if (response.format === 'json' || response.format === 'binpb') { preview = ( ); } } if (error) { preview = ; } if (format === 'svg' || format === 'png') { preview = Preview; } else if (format === 'json' || format === 'binpb') { preview = ( <>

{preview} ); } return ( <> {preview} ); }; function formatBytes(bytes: number, decimals = 2): string { if (bytes === 0) return '0 B'; const k = 1000; const dm = decimals < 0 ? 0 : decimals; const sizes = ['B', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; const sign = bytes < 0 ? '-' : ''; const absoluteBytes = Math.abs(bytes); const i = Math.min( Math.floor(Math.log10(absoluteBytes) / Math.log10(k)), sizes.length - 1, ); const value = absoluteBytes / k ** i; return `${sign}${value.toFixed(dm)} ${sizes[i]}`; } const rootNode = document.getElementById('root'); if (rootNode) { const root = createRoot(rootNode); root.render( , ); }