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
159 changes: 98 additions & 61 deletions tools/release-plan-dashboard/public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -644,76 +644,113 @@ <h2>PM View — Release Plan Health</h2>
</div>

<div id="pm-content">
<section id="section-pm-pp-ready" class="plan-section">
<h3 class="section-header" data-target="list-pm-pp-ready">
<span class="caret">&#9660;</span> 🏁 Ready to Complete Private
Preview Release Plans <span class="section-count"></span>
</h3>
<div id="list-pm-pp-ready" class="plan-list"></div>
</section>
<section class="pm-section-group" aria-labelledby="pm-needs-attention">
<div
class="pm-section-group-header section-header"
data-target="list-pm-needs-attention"
>
<span class="caret">&#9660;</span>
<div>
<h3 id="pm-needs-attention" class="pm-section-group-title">
Needs Attention
</h3>
<p class="pm-section-group-desc">
Priority-ordered groups that require PM follow-up
</p>
</div>
</div>
<div id="list-pm-needs-attention" class="pm-section-group-content">
Comment on lines +647 to +662
<section id="section-pm-pp-ready" class="plan-section">
<h3 class="section-header" data-target="list-pm-pp-ready">
<span class="caret">&#9660;</span> 🏁 Ready to Complete Private
Preview Release Plans <span class="section-count"></span>
</h3>
<div id="list-pm-pp-ready" class="plan-list"></div>
</section>

<section id="section-pm-ns-missing" class="plan-section">
<h3 class="section-header" data-target="list-pm-ns-missing">
<span class="caret">&#9660;</span> ⚠️ Missing Namespace Approval
<span class="section-count"></span>
</h3>
<div id="list-pm-ns-missing" class="plan-list"></div>
</section>
<section id="section-pm-pastdue" class="plan-section">
<h3 class="section-header" data-target="list-pm-pastdue">
<span class="caret">&#9660;</span> 🕐 Past Due Release Plans
<span class="section-count"></span>
</h3>
<div id="list-pm-pastdue" class="plan-list"></div>
</section>

<section id="section-pm-approaching" class="plan-section">
<h3 class="section-header" data-target="list-pm-approaching">
<span class="caret">&#9660;</span> 📅 Approaching SDK Release Target
(This &amp; Next Month) <span class="section-count"></span>
</h3>
<div id="list-pm-approaching" class="plan-list"></div>
</section>
<section id="section-pm-inactive" class="plan-section">
<h3 class="section-header" data-target="list-pm-inactive">
<span class="caret">&#9660;</span> ⚠️ Stale Release Plans (No
activity for 3+ months) <span class="section-count"></span>
</h3>
<div id="list-pm-inactive" class="plan-list"></div>
</section>

<section id="section-pm-pastdue" class="plan-section">
<h3 class="section-header" data-target="list-pm-pastdue">
<span class="caret">&#9660;</span> 🕐 Past Due Release Plans
<span class="section-count"></span>
</h3>
<div id="list-pm-pastdue" class="plan-list"></div>
</section>
<section id="section-pm-tier1" class="plan-section">
<h3 class="section-header" data-target="list-pm-tier1">
<span class="caret">&#9660;</span> 🔴 Release Plans Without SDK
for All Languages <span class="section-count"></span>
</h3>
<div id="list-pm-tier1" class="plan-list"></div>
</section>

<section id="section-pm-inactive" class="plan-section">
<h3 class="section-header" data-target="list-pm-inactive">
<span class="caret">&#9660;</span> ⚠️ Stale Release Plans (No
activity for 3+ months) <span class="section-count"></span>
</h3>
<div id="list-pm-inactive" class="plan-list"></div>
</section>
<section id="section-pm-partial" class="plan-section">
<h3 class="section-header" data-target="list-pm-partial">
<span class="caret">&#9660;</span> 🟡 Partially Released
<span class="section-count"></span>
</h3>
<div id="list-pm-partial" class="plan-list"></div>
</section>

<section id="section-pm-tier1" class="plan-section">
<h3 class="section-header" data-target="list-pm-tier1">
<span class="caret">&#9660;</span> 🔴 Release Plans Without SDK for
All Languages <span class="section-count"></span>
</h3>
<div id="list-pm-tier1" class="plan-list"></div>
</section>
<section id="section-pm-ns-missing" class="plan-section">
<h3 class="section-header" data-target="list-pm-ns-missing">
<span class="caret">&#9660;</span> ⚠️ Missing Namespace Approval
<span class="section-count"></span>
</h3>
<div id="list-pm-ns-missing" class="plan-list"></div>
</section>

<section id="section-pm-partial" class="plan-section">
<h3 class="section-header" data-target="list-pm-partial">
<span class="caret">&#9660;</span> 🟡 Partially Released
<span class="section-count"></span>
</h3>
<div id="list-pm-partial" class="plan-list"></div>
<section id="section-pm-product-missing" class="plan-section">
<h3 class="section-header" data-target="list-pm-product-missing">
<span class="caret">&#9660;</span> 📋 Missing Product Details
<span class="section-count"></span>
</h3>
<div id="list-pm-product-missing" class="plan-list"></div>
</section>
</div>
</section>

<section id="section-pm-product-missing" class="plan-section">
<h3 class="section-header" data-target="list-pm-product-missing">
<span class="caret">&#9660;</span> 📋 Missing Product Details
<span class="section-count"></span>
</h3>
<div id="list-pm-product-missing" class="plan-list"></div>
</section>
<section class="pm-section-group" aria-labelledby="pm-informational">
<div
class="pm-section-group-header section-header"
data-target="list-pm-informational"
>
<span class="caret">&#9660;</span>
<div>
<h3 id="pm-informational" class="pm-section-group-title">
Informational
</h3>
<p class="pm-section-group-desc">
No immediate PM action required
</p>
</div>
</div>
<div id="list-pm-informational" class="pm-section-group-content">
<section id="section-pm-finished" class="plan-section">
<h3 class="section-header" data-target="list-pm-finished">
<span class="caret">&#9660;</span> ✅ Recently Finished (Last 2
Months) <span class="section-count"></span>
</h3>
<div id="list-pm-finished" class="plan-list"></div>
</section>

<section id="section-pm-finished" class="plan-section">
<h3 class="section-header" data-target="list-pm-finished">
<span class="caret">&#9660;</span> ✅ Recently Finished (Last 2
Months) <span class="section-count"></span>
</h3>
<div id="list-pm-finished" class="plan-list"></div>
<section id="section-pm-approaching" class="plan-section">
<h3 class="section-header" data-target="list-pm-approaching">
<span class="caret">&#9660;</span> 📅 Approaching SDK Release
Target (This &amp; Next Month)
<span class="section-count"></span>
</h3>
<div id="list-pm-approaching" class="plan-list"></div>
</section>
</div>
</section>
</div>
</div>
Expand Down
41 changes: 41 additions & 0 deletions tools/release-plan-dashboard/public/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -452,6 +452,47 @@ main {
opacity: 0.8;
}

.pm-section-group {
border: 1px solid #e1dfdd;
border-radius: var(--radius);
background: #faf9f8;
padding: 16px 18px 8px;
margin-bottom: 20px;
}

.pm-section-group-header {
margin-bottom: 8px;
}

.pm-section-group-header.section-header {
padding-top: 0;
}

.pm-section-group-header.section-header > div {
min-width: 0;
}

.pm-section-group-header.section-header:hover .pm-section-group-title {
color: var(--azure-blue);
}

.pm-section-group-title {
margin: 0;
font-size: 1.1rem;
font-weight: 700;
color: var(--azure-dark);
}

.pm-section-group-desc {
margin: 4px 0 0;
color: var(--gray);
font-size: 0.9rem;
}

.pm-section-group.collapsed > .pm-section-group-content {
display: none !important;
}

.section-header {
cursor: pointer;
user-select: none;
Expand Down
74 changes: 74 additions & 0 deletions tools/release-plan-dashboard/tests/pm-view-layout.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { describe, expect, test } from "vitest";
import { readFileSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const indexHtml = readFileSync(
path.join(__dirname, "../public/index.html"),
"utf8",
);

function expectOrdered(block, labels) {
let lastIndex = -1;
for (const label of labels) {
const index = block.indexOf(label);
expect(index, `Expected to find "${label}"`).toBeGreaterThanOrEqual(0);
expect(index, `Expected "${label}" after prior sections`).toBeGreaterThan(
lastIndex,
);
lastIndex = index;
}
}

describe("PM View Attention Required layout", () => {
test("groups informational and action sections in the expected order", () => {
const informationalIndex = indexHtml.indexOf('id="pm-informational"');
const needsAttentionIndex = indexHtml.indexOf('id="pm-needs-attention"');

expect(needsAttentionIndex).toBeGreaterThanOrEqual(0);
expect(indexHtml).toMatch(
/aria-labelledby="pm-needs-attention"[\s\S]*class="pm-section-group-header section-header"[\s\S]*data-target="list-pm-needs-attention"[\s\S]*id="pm-needs-attention"[\s\S]*>\s*Needs Attention\s*</,
);
expect(informationalIndex).toBeGreaterThanOrEqual(0);
expect(indexHtml).toMatch(
/aria-labelledby="pm-informational"[\s\S]*class="pm-section-group-header section-header"[\s\S]*data-target="list-pm-informational"[\s\S]*id="pm-informational"[\s\S]*>\s*Informational\s*</,
);
expect(informationalIndex).toBeGreaterThan(needsAttentionIndex);

expectOrdered(indexHtml.slice(needsAttentionIndex, informationalIndex), [
"Ready to Complete Private",
"Past Due Release Plans",
"Stale Release Plans",
"Release Plans Without SDK",
"Partially Released",
"Missing Namespace Approval",
"Missing Product Details",
]);

expectOrdered(indexHtml.slice(informationalIndex), [
"Recently Finished (Last 2",
"Approaching SDK Release",
]);
});

test("keeps each PM section present exactly once", () => {
const sectionIds = [
"section-pm-finished",
"section-pm-approaching",
"section-pm-pp-ready",
"section-pm-pastdue",
"section-pm-inactive",
"section-pm-tier1",
"section-pm-partial",
"section-pm-ns-missing",
"section-pm-product-missing",
];

for (const sectionId of sectionIds) {
const matches =
indexHtml.match(new RegExp(`id="${sectionId}"`, "g")) || [];
expect(matches).toHaveLength(1);
}
});
});