Skip to content

Commit fa1a720

Browse files
a1icjasprt
authored andcommitted
coco: Create CoCo page
This creates a page for CoCo tests at /coco. Signed-off-by: Alicja Mahr <[email protected]> Signed-off-by: Aurelien Bombo <[email protected]>
1 parent 541dc0d commit fa1a720

File tree

4 files changed

+352
-338
lines changed

4 files changed

+352
-338
lines changed

components/Dashboard.js

Lines changed: 339 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,339 @@
1+
import React, { useEffect, useState, useRef } from "react";
2+
import { DataTable } from "primereact/datatable";
3+
import { Column } from "primereact/column";
4+
import Head from "next/head";
5+
import { weatherTemplate, getWeatherIndex } from "./weatherTemplate";
6+
import { OverlayPanel } from "primereact/overlaypanel";
7+
import MaintainerMapping from "../maintainers.yml";
8+
9+
export default function Dashboard({ coco = false }) {
10+
const [loading, setLoading] = useState(true);
11+
const [jobs, setJobs] = useState([]);
12+
const [rows, setRows] = useState([]);
13+
const [expandedRows, setExpandedRows] = useState([]);
14+
const [requiredFilter, setRequiredFilter] = useState(false);
15+
16+
useEffect(() => {
17+
const fetchData = async () => {
18+
let data = {};
19+
20+
if (process.env.NODE_ENV === "development") {
21+
data = (await import("../localData/job_stats.json")).default;
22+
} else {
23+
const response = await fetch(
24+
"https://raw.githubusercontent.com/kata-containers/kata-containers.github.io" +
25+
"/refs/heads/latest-dashboard-data/data/job_stats.json"
26+
);
27+
data = await response.json();
28+
}
29+
30+
try {
31+
let jobData = Object.keys(data).map((key) => {
32+
const job = data[key];
33+
return { name: key, ...job };
34+
});
35+
36+
if (coco) jobData = jobData.filter((job) => job.name.includes("coco"));
37+
38+
setJobs(jobData);
39+
} catch (error) {
40+
// TODO: Add pop-up/toast message for error
41+
console.error("Error fetching data:", error);
42+
} finally {
43+
setLoading(false);
44+
}
45+
};
46+
47+
fetchData();
48+
}, []);
49+
50+
// Filter based on required tag.
51+
const filterRequired = (filteredJobs) => {
52+
if (requiredFilter) {
53+
filteredJobs = filteredJobs.filter((job) => job.required);
54+
}
55+
return filteredJobs;
56+
};
57+
58+
useEffect(() => {
59+
setLoading(true);
60+
61+
// Filter based on required tag.
62+
let filteredJobs = filterRequired(jobs);
63+
64+
//Set the rows for the table.
65+
setRows(
66+
filteredJobs.map((job) => ({
67+
name: job.name,
68+
runs: job.runs,
69+
fails: job.fails,
70+
skips: job.skips,
71+
required: job.required,
72+
weather: getWeatherIndex(job),
73+
}))
74+
);
75+
setLoading(false);
76+
}, [jobs, requiredFilter]);
77+
78+
const toggleRow = (rowData) => {
79+
const isRowExpanded = expandedRows.includes(rowData);
80+
81+
let updatedExpandedRows;
82+
if (isRowExpanded) {
83+
updatedExpandedRows = expandedRows.filter((r) => r !== rowData);
84+
} else {
85+
updatedExpandedRows = [...expandedRows, rowData];
86+
}
87+
88+
setExpandedRows(updatedExpandedRows);
89+
};
90+
91+
const buttonClass = (active) => `tab md:px-4 px-2 py-2 border-2
92+
${
93+
active
94+
? "border-blue-500 bg-blue-500 text-white"
95+
: "border-gray-300 bg-white hover:bg-gray-100"
96+
}`;
97+
98+
// Template for rendering the Name column as a clickable item
99+
const nameTemplate = (rowData) => {
100+
return (
101+
<span onClick={() => toggleRow(rowData)} style={{ cursor: "pointer" }}>
102+
{rowData.name}
103+
</span>
104+
);
105+
};
106+
107+
const maintainRefs = useRef([]);
108+
109+
const rowExpansionTemplate = (data) => {
110+
const job = jobs.find((job) => job.name === data.name);
111+
112+
// Prepare run data
113+
const runs = [];
114+
for (let i = 0; i < job.runs; i++) {
115+
runs.push({
116+
run_num: job.run_nums[i],
117+
result: job.results[i],
118+
url: job.urls[i],
119+
});
120+
}
121+
122+
// Find maintainers for the given job
123+
const maintainerData = MaintainerMapping.mappings
124+
.filter(({ regex }) => new RegExp(regex).test(job.name))
125+
.flatMap((match) =>
126+
match.owners.map((owner) => ({
127+
...owner,
128+
group: match.group,
129+
}))
130+
);
131+
132+
// Group maintainers by their group name
133+
const groupedMaintainers = maintainerData.reduce((acc, owner) => {
134+
if (!acc[owner.group]) {
135+
acc[owner.group] = [];
136+
}
137+
acc[owner.group].push(owner);
138+
return acc;
139+
}, {});
140+
141+
return (
142+
<div key={`${job.name}-runs`} className="p-3 bg-gray-100">
143+
{/* Display last 10 runs */}
144+
<div className="flex flex-wrap gap-4">
145+
{runs.length > 0 ? (
146+
runs.map((run) => {
147+
const emoji =
148+
run.result === "Pass"
149+
? "✅"
150+
: run.result === "Fail"
151+
? "❌"
152+
: "⚠️";
153+
return (
154+
<span key={`${job.name}-runs-${run.run_num}`}>
155+
<a href={run.url}>
156+
{emoji} {run.run_num}
157+
</a>
158+
&nbsp;&nbsp;&nbsp;&nbsp;
159+
</span>
160+
);
161+
})
162+
) : (
163+
<div>No Nightly Runs associated with this job</div>
164+
)}
165+
</div>
166+
167+
{/* Display Maintainers, if there's any */}
168+
<div className="mt-4 p-2 bg-gray-300 w-full">
169+
{Object.keys(groupedMaintainers).length > 0 ? (
170+
<div className="grid grid-cols-2 p-2 gap-6">
171+
{Object.entries(groupedMaintainers).map(
172+
([group, owners], groupIndex) => (
173+
<div key={groupIndex} className="flex flex-col max-w-xs">
174+
{/* List the group name */}
175+
<strong className="pl-2">{group}:</strong>
176+
<div>
177+
{/* List all maintainers for the group */}
178+
{owners.map((owner, ownerIndex) => {
179+
const badgeMaintain = `maintain-${owner.github}`;
180+
maintainRefs.current[badgeMaintain] =
181+
maintainRefs.current[badgeMaintain] ||
182+
React.createRef();
183+
184+
return (
185+
// Create the OverlayPanel with contact information.
186+
<span key={ownerIndex}>
187+
<span
188+
onMouseEnter={(e) =>
189+
maintainRefs.current[
190+
badgeMaintain
191+
].current.toggle(e)
192+
}
193+
>
194+
<a
195+
href={`https://github.com/${owner.github}`}
196+
target="_blank"
197+
rel="noopener noreferrer"
198+
className="text-blue-500 underline pl-2 whitespace-nowrap"
199+
>
200+
{owner.fullname}
201+
</a>
202+
{ownerIndex < owners.length - 1 && ", "}
203+
</span>
204+
<OverlayPanel
205+
ref={maintainRefs.current[badgeMaintain]}
206+
dismissable
207+
onMouseLeave={(e) =>
208+
maintainRefs.current[
209+
badgeMaintain
210+
].current.toggle(e)
211+
}
212+
>
213+
<ul className="bg-white border rounded shadow-lg p-2">
214+
<li className="p-2 hover:bg-gray-200">
215+
<span className="font-bold mr-4">Email:</span>{" "}
216+
{owner.email}
217+
</li>
218+
<a
219+
href={`https://github.com/${owner.github}`}
220+
target="_blank"
221+
rel="noopener noreferrer"
222+
>
223+
<li className="p-2 hover:bg-gray-200 flex justify-between">
224+
<span className="font-bold mr-4">
225+
GitHub:
226+
</span>
227+
<span className="text-right">
228+
{owner.github}
229+
</span>
230+
</li>
231+
</a>
232+
<a
233+
href={`${owner.slackurl}`}
234+
target="_blank"
235+
rel="noopener noreferrer"
236+
>
237+
<li className="p-2 hover:bg-gray-200 flex justify-between">
238+
<span className="font-bold mr-4">
239+
Slack:
240+
</span>
241+
<span className="text-right">
242+
@{owner.slack}
243+
</span>
244+
</li>
245+
</a>
246+
</ul>
247+
</OverlayPanel>
248+
</span>
249+
);
250+
})}
251+
</div>
252+
</div>
253+
)
254+
)}
255+
</div>
256+
) : (
257+
<div>No Maintainer Information Available</div>
258+
)}
259+
</div>
260+
</div>
261+
);
262+
};
263+
264+
const renderTable = () => (
265+
<DataTable
266+
value={rows}
267+
expandedRows={expandedRows}
268+
stripedRows
269+
rowExpansionTemplate={rowExpansionTemplate}
270+
onRowToggle={(e) => setExpandedRows(e.data)}
271+
loading={loading}
272+
emptyMessage="No results found."
273+
sortField="fails"
274+
sortOrder={-1}
275+
>
276+
<Column expander style={{ width: "5rem" }} />
277+
<Column
278+
field="name"
279+
header="Name"
280+
body={nameTemplate}
281+
filter
282+
sortable
283+
maxConstraints={4}
284+
filterHeader="Filter by Name"
285+
filterPlaceholder="Search..."
286+
/>
287+
<Column field="required" header="Required" sortable />
288+
<Column field="runs" header="Runs" sortable />
289+
<Column field="fails" header="Fails" sortable />
290+
<Column field="skips" header="Skips" sortable />
291+
<Column
292+
field="weather"
293+
header="Weather"
294+
body={weatherTemplate}
295+
sortable
296+
/>
297+
</DataTable>
298+
);
299+
300+
return (
301+
<div className="text-center">
302+
<Head>
303+
<title>Kata CI Dashboard</title>
304+
</Head>
305+
306+
<h1
307+
className={
308+
"text-4xl mt-4 mb-0 underline text-inherit hover:text-blue-500"
309+
}
310+
>
311+
<a
312+
href={
313+
"https://github.com/kata-containers/kata-containers/" +
314+
"actions/workflows/ci-nightly.yaml"
315+
}
316+
target="_blank"
317+
rel="noopener noreferrer"
318+
>
319+
Kata CI Dashboard
320+
</a>
321+
</h1>
322+
323+
<main
324+
className={
325+
"m-0 h-full p-4 overflow-x-hidden overflow-y-auto bg-surface-ground font-normal text-text-color antialiased select-text"
326+
}
327+
>
328+
<button
329+
className={buttonClass(requiredFilter)}
330+
onClick={() => setRequiredFilter(!requiredFilter)}
331+
>
332+
Required Jobs Only
333+
</button>
334+
<div className="mt-4 text-lg">Total Rows: {rows.length}</div>
335+
<div>{renderTable()}</div>
336+
</main>
337+
</div>
338+
);
339+
}

package-lock.json

Lines changed: 0 additions & 15 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pages/coco.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import Dashboard from "../components/Dashboard";
2+
3+
export default function MainDashboard() {
4+
return (
5+
<div>
6+
<Dashboard coco={true} />
7+
</div>
8+
);
9+
}

0 commit comments

Comments
 (0)