Skip to content

Commit a3adf64

Browse files
committed
chore(docs): add classes to module coverage tracking
1 parent 0c0a2a2 commit a3adf64

6 files changed

Lines changed: 388 additions & 192 deletions

File tree

docs/.vitepress/theme/components/ModulesCoverageTable.vue

Lines changed: 166 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<script setup>
2-
import { computed, onMounted, ref } from 'vue';
2+
import { computed, onBeforeUnmount, onMounted, ref } from 'vue';
33
import featuresData from '../../../roadmap/features.json';
44
import { getModuleCoverage } from '../utils';
55
@@ -10,21 +10,20 @@ const statusFilters = [
1010
{ key: 'all', label: 'All' },
1111
{ key: 'available', label: 'Available' },
1212
{ key: 'partially-implemented', label: 'Partial' },
13-
{ key: 'not-serialized', label: 'Not serialized' },
14-
{ key: 'na', label: 'NA' },
13+
{ key: 'na', label: 'Unavailable' },
1514
{ key: 'other', label: 'Other' },
1615
];
1716
18-
const serializedModuleRows = computed(() => {
19-
const entries = Object.entries(featuresData.moduleSerialization?.serializedModules || {})
17+
const availableModules = computed(() => {
18+
const entries = Object.entries(featuresData.moduleStatus?.available || {})
2019
return entries
2120
.map(([module, item]) => ({ module, ...item }))
2221
.sort((a, b) => a.module.localeCompare(b.module))
2322
});
2423
2524
function getRowStatusKey(row) {
26-
if (row?.status === 'not-serialized') {
27-
return 'not-serialized';
25+
if (row?.status === 'na') {
26+
return 'na';
2827
}
2928
if (row?.status === 'available') {
3029
return 'available';
@@ -51,14 +50,13 @@ const statusCounts = computed(() => {
5150
});
5251
5352
const allModuleRows = computed(() => {
54-
const serializedRows = serializedModuleRows.value;
55-
const notSerialized = (featuresData.moduleSerialization?.notSerializedModules || []).map((module) => ({
53+
const unavailableRows = (featuresData.moduleStatus?.unavailable || []).map((module) => ({
5654
module,
57-
status: 'not-serialized',
55+
status: 'na',
5856
version: '',
5957
}));
6058
61-
return [...serializedRows, ...notSerialized].sort((a, b) => a.module.localeCompare(b.module));
59+
return [...availableModules.value, ...unavailableRows].sort((a, b) => a.module.localeCompare(b.module));
6260
});
6361
6462
const filteredModuleRows = computed(() => {
@@ -82,6 +80,7 @@ function getModuleGroupName(moduleName) {
8280
if (!moduleId) {
8381
return 'Other';
8482
}
83+
// These two have an uppercase character in the group name, so we need to check them first
8584
if (moduleId.startsWith('GeoVis')) {
8685
return 'GeoVis';
8786
}
@@ -110,8 +109,49 @@ const moduleGroups = computed(() => {
110109
});
111110
112111
const moduleCoverage = ref(new Map());
112+
const openMissingPopoverFor = ref(null);
113+
114+
function getMissingClasses(moduleName) {
115+
const moduleStats = moduleCoverage.value?.get(moduleName);
116+
return Array.isArray(moduleStats?.missingClasses) ? moduleStats.missingClasses : [];
117+
}
118+
119+
function getMissingCount(moduleName) {
120+
return getMissingClasses(moduleName).length;
121+
}
122+
123+
function getCoveragePercent(moduleName) {
124+
const rawCoverage = moduleCoverage.value?.get(moduleName)?.coverage;
125+
if (typeof rawCoverage !== 'number' || Number.isNaN(rawCoverage)) {
126+
return 0;
127+
}
128+
return Math.min(100, Math.max(0, rawCoverage));
129+
}
130+
131+
function getMissingPopoverId(moduleName) {
132+
return `missing-classes-${String(moduleName || '').replace(/[^a-zA-Z0-9_-]/g, '-')}`;
133+
}
134+
135+
function toggleMissingPopover(moduleName) {
136+
openMissingPopoverFor.value = openMissingPopoverFor.value === moduleName ? null : moduleName;
137+
}
138+
139+
function isMissingPopoverOpen(moduleName) {
140+
return openMissingPopoverFor.value === moduleName;
141+
}
142+
143+
function closeMissingPopover() {
144+
openMissingPopoverFor.value = null;
145+
}
146+
147+
function onDocumentClick(event) {
148+
if (!event.target.closest('.missing-badge-container')) {
149+
closeMissingPopover();
150+
}
151+
}
113152
114153
onMounted(async () => {
154+
document.addEventListener('click', onDocumentClick);
115155
try {
116156
moduleCoverage.value = await getModuleCoverage();
117157
} catch (error) {
@@ -120,6 +160,10 @@ onMounted(async () => {
120160
}
121161
});
122162
163+
onBeforeUnmount(() => {
164+
document.removeEventListener('click', onDocumentClick);
165+
});
166+
123167
function formatCoverage(value) {
124168
if (value === undefined || value === null) {
125169
return '';
@@ -217,14 +261,43 @@ function getHighlightParts(text) {
217261
</template>
218262
</td>
219263
<td>
220-
<div>
221-
<template v-if="row.status === 'not-serialized'">Not serialized</template>
264+
<div class="status-content">
265+
<template v-if="row.status === 'na'">Unavailable</template>
222266
<template v-else-if="row.status === 'available'">
223267
<div></div>
224268
<small>{{ row.version || '' }}</small>
225269
</template>
226270
<template v-else-if="isNotApplicable(row.version) || row.status === 'na'"></template>
227-
<template v-else-if="row.status === 'partially-implemented'">{{ formatCoverage(moduleCoverage?.get(row.module)?.coverage) }}<div>completed</div></template>
271+
<template v-else-if="row.status === 'partially-implemented'">
272+
<div v-if="getMissingCount(row.module) > 0" class="missing-badge-container">
273+
<button
274+
type="button"
275+
class="missing-badge"
276+
:aria-expanded="isMissingPopoverOpen(row.module)"
277+
:aria-controls="getMissingPopoverId(row.module)"
278+
:style="{ '--missing-progress': `${getCoveragePercent(row.module)}%` }"
279+
@click.stop="toggleMissingPopover(row.module)"
280+
>
281+
<span class="missing-badge-fill" aria-hidden="true" />
282+
<span class="missing-badge-label">{{ getMissingCount(row.module) }} missing · {{ formatCoverage(getCoveragePercent(row.module)) }}</span>
283+
</button>
284+
<div
285+
v-if="isMissingPopoverOpen(row.module)"
286+
:id="getMissingPopoverId(row.module)"
287+
class="missing-popover"
288+
role="dialog"
289+
aria-label="Missing class names"
290+
@click.stop
291+
>
292+
<div class="missing-popover-title">Missing classes</div>
293+
<ul>
294+
<li v-for="className in getMissingClasses(row.module)" :key="`${row.module}-${className}`">
295+
<a :href="`https://vtk.org/doc/nightly/html/class${className}.html`" target="_blank" rel="noopener noreferrer">{{ className }}</a>
296+
</li>
297+
</ul>
298+
</div>
299+
</div>
300+
</template>
228301
<template v-else></template>
229302
</div>
230303
</td>
@@ -265,8 +338,8 @@ function getHighlightParts(text) {
265338
}
266339
267340
.status-chip.active {
268-
border-color: var(--vp-c-brand-1);
269-
color: var(--vp-c-brand-1);
341+
border-color: var(--vp-c-brand-2);
342+
color: var(--vp-c-brand-2);
270343
font-weight: 600;
271344
}
272345
@@ -293,4 +366,81 @@ function getHighlightParts(text) {
293366
.module-table th {
294367
text-align: center;
295368
}
369+
370+
.status-content {
371+
position: relative;
372+
display: inline-flex;
373+
flex-direction: column;
374+
align-items: center;
375+
gap: 0.1rem;
376+
}
377+
378+
.missing-badge-container {
379+
position: relative;
380+
margin-top: 0.1rem;
381+
}
382+
383+
.missing-badge {
384+
border: 2px solid var(--vp-c-divider);
385+
background: var(--vp-c-bg-soft);
386+
color: var(--vp-c-text-1);
387+
border-radius: 999px;
388+
padding: 0.1rem 0.5rem;
389+
font-size: 0.72rem;
390+
cursor: pointer;
391+
position: relative;
392+
overflow: hidden;
393+
}
394+
395+
.missing-badge:hover {
396+
border-color: var(--vp-c-brand-1);
397+
color: var(--vp-c-brand-1);
398+
}
399+
400+
.missing-badge-fill {
401+
position: absolute;
402+
inset: 0;
403+
width: var(--missing-progress, 0%);
404+
background: var(--vp-c-brand-2);
405+
pointer-events: none;
406+
}
407+
408+
.missing-badge-label {
409+
position: relative;
410+
z-index: 1;
411+
}
412+
413+
.missing-popover {
414+
position: absolute;
415+
top: calc(100% + 0.25rem);
416+
left: 50%;
417+
transform: translateX(-50%);
418+
z-index: 20;
419+
min-width: 16rem;
420+
max-width: 24rem;
421+
max-height: 14rem;
422+
overflow: auto;
423+
padding: 0.45rem 0.6rem;
424+
border: 1px solid var(--vp-c-divider);
425+
border-radius: 0.4rem;
426+
background: var(--vp-c-bg);
427+
text-align: left;
428+
box-shadow: var(--vp-shadow-2);
429+
}
430+
431+
.missing-popover-title {
432+
font-size: 0.78rem;
433+
font-weight: 600;
434+
margin-bottom: 0.2rem;
435+
}
436+
437+
.missing-popover ul {
438+
margin: 0;
439+
padding-left: 1rem;
440+
}
441+
442+
.missing-popover li {
443+
font-size: 0.75rem;
444+
line-height: 1.35;
445+
}
296446
</style>

docs/.vitepress/theme/utils.js

Lines changed: 58 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,52 @@
1+
function normalizeHeaderList(headerList) {
2+
return headerList
3+
.map((headerFileName) => headerFileName.trim())
4+
.filter((headerFileName) => headerFileName !== "" && !headerFileName.startsWith("#"));
5+
}
6+
7+
function getModuleNameFromHeaderFileName(headerFileName) {
8+
return `VTK::${headerFileName.split("/").slice(0, 2).join("")}`;
9+
}
10+
11+
function getClassNameFromHeaderFileName(headerFileName) {
12+
const classToken = headerFileName.split("/").at(-1) || "";
13+
return classToken.replace(/\.[^/.]+$/, "");
14+
}
15+
116
function getClassCountPerModule(headerList) {
217
const classCountMap = new Map();
3-
for (const headerFileName of headerList) {
4-
if (headerFileName.trim() === "") continue;
5-
const moduleName = `VTK::${headerFileName.split("/").slice(0, 2).join("")}`;
18+
for (const headerFileName of normalizeHeaderList(headerList)) {
19+
const moduleName = getModuleNameFromHeaderFileName(headerFileName);
620
classCountMap.set(moduleName, (classCountMap.get(moduleName) || 0) + 1);
721
}
822
return classCountMap;
923
}
1024

25+
function getClassNamesPerModule(headerList) {
26+
const classNamesMap = new Map();
27+
for (const headerFileName of normalizeHeaderList(headerList)) {
28+
const moduleName = getModuleNameFromHeaderFileName(headerFileName);
29+
if (!classNamesMap.has(moduleName)) {
30+
classNamesMap.set(moduleName, []);
31+
}
32+
classNamesMap.get(moduleName).push(getClassNameFromHeaderFileName(headerFileName));
33+
}
34+
35+
for (const [moduleName, classNames] of classNamesMap.entries()) {
36+
const deduped = [...new Set(classNames)].sort((leftName, rightName) => leftName.localeCompare(rightName));
37+
classNamesMap.set(moduleName, deduped);
38+
}
39+
40+
return classNamesMap;
41+
}
42+
1143
class ModuleCoverageStatistics {
12-
constructor(moduleName, autoCount, ignoreCount, manualCount) {
44+
constructor(moduleName, autoCount, ignoreCount, manualCount, missingClasses = []) {
1345
this.moduleName = moduleName;
1446
this.autoCount = autoCount;
1547
this.ignoreCount = ignoreCount;
1648
this.manualCount = manualCount;
49+
this.missingClasses = missingClasses;
1750
}
1851

1952
get totalCount() {
@@ -25,24 +58,37 @@ class ModuleCoverageStatistics {
2558
}
2659
}
2760

28-
async function fetchAndCount(url) {
61+
async function fetchHeaderList(url) {
2962
const response = await fetch(url);
30-
return getClassCountPerModule((await response.text()).split("\n"));
63+
return normalizeHeaderList((await response.text()).split("\n"));
3164
}
3265

3366
export async function getModuleCoverage() {
34-
const [autoMap, ignoreMap, manualMap] = await Promise.all([
67+
const [autoHeaderList, ignoreHeaderList, manualHeaderList] = await Promise.all([
3568
// use github because gitlab.kitware.com raw url needs cors proxy which over-complicates the code.
36-
fetchAndCount("https://raw.githubusercontent.com/Kitware/VTK/refs/heads/master/Utilities/Marshalling/VTK_MARSHALAUTO.txt"),
37-
fetchAndCount("https://raw.githubusercontent.com/Kitware/VTK/refs/heads/master/Utilities/Marshalling/ignore.txt"),
38-
fetchAndCount("https://raw.githubusercontent.com/Kitware/VTK/refs/heads/master/Utilities/Marshalling/VTK_MARSHALMANUAL.txt"),
69+
fetchHeaderList("https://raw.githubusercontent.com/Kitware/VTK/refs/heads/master/Utilities/Marshalling/VTK_MARSHALAUTO.txt"),
70+
fetchHeaderList("https://raw.githubusercontent.com/Kitware/VTK/refs/heads/master/Utilities/Marshalling/ignore.txt"),
71+
fetchHeaderList("https://raw.githubusercontent.com/Kitware/VTK/refs/heads/master/Utilities/Marshalling/VTK_MARSHALMANUAL.txt"),
3972
]);
4073

74+
const autoMap = getClassCountPerModule(autoHeaderList);
75+
const ignoreMap = getClassCountPerModule(ignoreHeaderList);
76+
const manualMap = getClassCountPerModule(manualHeaderList);
77+
const missingClassesMap = getClassNamesPerModule(ignoreHeaderList);
78+
79+
const allModules = new Set([...autoMap.keys(), ...ignoreMap.keys(), ...manualMap.keys()]);
80+
4181
const coverageMap = new Map();
42-
for (const [moduleName, autoCount] of autoMap.entries()) {
82+
for (const moduleName of allModules.values()) {
4383
coverageMap.set(
4484
moduleName,
45-
new ModuleCoverageStatistics(moduleName, autoCount, ignoreMap.get(moduleName) || 0, manualMap.get(moduleName) || 0)
85+
new ModuleCoverageStatistics(
86+
moduleName,
87+
autoMap.get(moduleName) || 0,
88+
ignoreMap.get(moduleName) || 0,
89+
manualMap.get(moduleName) || 0,
90+
missingClassesMap.get(moduleName) || []
91+
)
4692
);
4793
}
4894
return coverageMap;

0 commit comments

Comments
 (0)