diff --git a/layouts/ci/single.html b/layouts/ci/single.html index 36d6fb963..53456cadd 100644 --- a/layouts/ci/single.html +++ b/layouts/ci/single.html @@ -11,12 +11,11 @@
{{ partial "page-toolbar.html" . }} -
-
+

{{- .Title -}}

- {{ .Content }} +

{{ .Content }}

-
+
{{ partial "spinner.html" . }}
@@ -26,13 +25,6 @@

{{- .Title -}}

- -
-
{{ partial "footer.html" . }}
diff --git a/layouts/partials/menu-ci.html b/layouts/partials/menu-ci.html index 908b7ab83..2dbbe936c 100644 --- a/layouts/partials/menu-ci.html +++ b/layouts/partials/menu-ci.html @@ -5,17 +5,7 @@ diff --git a/static/css/custom.css b/static/css/custom.css index 2941ba32d..a245bbc5b 100644 --- a/static/css/custom.css +++ b/static/css/custom.css @@ -1203,7 +1203,224 @@ h6 .anchor::before { content: "\00a7"; } -/* CI Labels */ +/* CI Dashboard */ + +.ci-dashboard { + max-width: 100%; +} + +.ci-dashboard h1 { + margin-bottom: 4px; +} + +.ci-subtitle p { + color: var(--pf-global--Color--200); + margin-bottom: 24px; + font-size: 0.95rem; +} + +.ci-tabs { + display: flex; + gap: 0; + border-bottom: 2px solid var(--pf-global--BorderColor--100, #d2d2d2); + margin-bottom: 16px; +} + +.ci-tab { + padding: 10px 20px; + cursor: pointer; + font-size: 0.9rem; + font-weight: 500; + color: var(--pf-global--Color--200, #6a6e73); + border: none; + background: none; + border-bottom: 3px solid transparent; + margin-bottom: -2px; + transition: color 0.15s, border-color 0.15s; +} + +.ci-tab:hover { + color: var(--pf-global--Color--100, #151515); +} + +.ci-tab.active { + color: var(--pf-global--primary-color--100, #06c); + border-bottom-color: var(--pf-global--primary-color--100, #06c); + font-weight: 600; +} + +.ci-toolbar { + display: flex; + justify-content: flex-end; + align-items: center; + margin-bottom: 12px; + gap: 8px; +} + +.ci-toolbar label { + font-size: 0.85rem; + color: var(--pf-global--Color--200, #6a6e73); +} + +.ci-toolbar select { + font-size: 0.85rem; + padding: 4px 8px; + border: 1px solid var(--pf-global--BorderColor--100, #d2d2d2); + border-radius: 4px; + background: var(--pf-global--BackgroundColor--100, #fff); + color: var(--pf-global--Color--100, #151515); +} + +.ci-table { + width: 100%; + border-collapse: separate; + border-spacing: 0; + font-size: 0.9rem; +} + +.ci-table thead th { + text-align: left; + padding: 10px 16px; + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--pf-global--Color--200, #6a6e73); + border-bottom: 1px solid var(--pf-global--BorderColor--100, #d2d2d2); +} + +.ci-table tbody tr { + transition: background-color 0.1s; +} + +.ci-table tbody tr:hover { + background-color: var(--pf-global--BackgroundColor--200, #f0f0f0); +} + +.ci-table tbody td { + padding: 14px 16px; + border-bottom: 1px solid var(--pf-global--BorderColor--100, #d2d2d2); + vertical-align: middle; +} + +.ci-status-cell { + display: flex; + align-items: center; + gap: 8px; + white-space: nowrap; +} + +.ci-status-dot { + width: 10px; + height: 10px; + border-radius: 50%; + flex-shrink: 0; +} + +.ci-status-dot.green { + background-color: var(--pf-global--success-color--100, #3e8635); +} + +.ci-status-dot.yellow { + background-color: var(--pf-global--palette--gold-500, #f0ab00); +} + +.ci-status-dot.red { + background-color: var(--pf-global--danger-color--100, #c9190b); +} + +.ci-status-text { + font-weight: 500; +} + +.ci-status-text.green { + color: var(--pf-global--success-color--100, #3e8635); +} + +.ci-status-text.yellow { + color: var(--pf-global--palette--gold-500, #f0ab00); +} + +.ci-status-text.red { + color: var(--pf-global--danger-color--100, #c9190b); +} + +.ci-pattern-name { + font-weight: 600; + color: var(--pf-global--Color--100, #151515); +} + +.ci-pattern-name a { + color: inherit; + text-decoration: none; +} + +.ci-pattern-name a:hover { + color: var(--pf-global--primary-color--100, #06c); + text-decoration: underline; +} + +.ci-pattern-branch { + display: block; + font-size: 0.8rem; + color: var(--pf-global--Color--200, #6a6e73); + font-weight: 400; + margin-top: 2px; +} + +.ci-platform-cell { + display: flex; + align-items: center; + gap: 8px; +} + +.ci-platform-icon { + font-size: 1rem; + color: var(--pf-global--Color--200, #6a6e73); + width: 20px; + text-align: center; +} + +.ci-time { + color: var(--pf-global--Color--200, #6a6e73); + white-space: nowrap; +} + +.ci-history-link { + display: block; + text-align: center; + padding: 16px; + font-size: 0.9rem; + color: var(--pf-global--Color--100, #151515); + font-weight: 500; + border-top: 1px solid var(--pf-global--BorderColor--100, #d2d2d2); + margin-top: -1px; + text-decoration: none; +} + +.ci-history-link:hover { + color: var(--pf-global--primary-color--100, #06c); +} + +.ci-section-header { + font-size: 1.1rem; + font-weight: 600; + margin: 24px 0 8px 0; + padding-bottom: 6px; + border-bottom: 2px solid var(--pf-global--BorderColor--100, #d2d2d2); + color: var(--pf-global--Color--100, #151515); +} + +.ci-section-header:first-of-type { + margin-top: 0; +} + +.ci-empty { + text-align: center; + padding: 40px 16px; + color: var(--pf-global--Color--200, #6a6e73); + font-style: italic; +} .ci-label { font-size: 0.8rem; @@ -1248,9 +1465,279 @@ h6 .anchor::before { padding: 0 5px; } -/* CI icon styles - placeholder for future use */ +/* Card-based overview */ + +.ci-stats-bar { + margin-bottom: 20px; + padding: 16px 20px; + background: var(--pf-global--BackgroundColor--200, #f0f0f0); + border-radius: 8px; +} + +.ci-stats-text { + font-size: 0.95rem; + color: var(--pf-global--Color--100, #151515); + margin-bottom: 8px; +} + +.ci-stats-number { + font-weight: 700; + font-size: 1.1rem; + color: var(--pf-global--success-color--100, #3e8635); +} + +.ci-stats-note { + color: var(--pf-global--Color--200, #6a6e73); + font-size: 0.85rem; +} + +.ci-stats-note-warn { + color: var(--pf-global--palette--gold-500, #f0ab00); +} + +.ci-stats-loading-text { + color: var(--pf-global--Color--200, #6a6e73); + font-style: italic; +} + +.ci-stats-progress { + height: 6px; + background: var(--pf-global--BorderColor--100, #d2d2d2); + border-radius: 3px; + overflow: hidden; +} + +.ci-stats-progress-fill { + height: 100%; + background: var(--pf-global--success-color--100, #3e8635); + border-radius: 3px; + transition: width 0.4s ease; +} + +.ci-overview-legend { + font-size: 0.82rem; + color: var(--pf-global--Color--200, #6a6e73); + margin-bottom: 16px; +} + +.ci-legend-bar { + display: inline-block; + width: 4px; + height: 14px; + border-radius: 1px; + vertical-align: middle; + margin-left: 8px; + margin-right: 2px; +} + +.ci-legend-bar.green { + background-color: var(--pf-global--success-color--100, #3e8635); +} + +.ci-legend-bar.gray { + background-color: var(--pf-global--disabled-color--200, #b8bbbe); +} + +.ci-legend-bar.red { + background-color: var(--pf-global--danger-color--100, #c9190b); +} + +.ci-card-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: 16px; + margin-bottom: 24px; +} + +.ci-card { + display: flex; + flex-direction: column; + border: 1px solid var(--pf-global--BorderColor--100, #d2d2d2); + border-radius: 8px; + padding: 20px; + background: var(--pf-global--BackgroundColor--100, #fff); + text-decoration: none; + color: inherit; + transition: box-shadow 0.15s, border-color 0.15s; + cursor: pointer; +} + +.ci-card:hover { + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + border-color: var(--pf-global--Color--200, #6a6e73); + text-decoration: none; + color: inherit; +} + +.ci-card[data-health="green"], +.ci-card[data-health="green-with-infra"] { + border-left: 4px solid var(--pf-global--success-color--100, #3e8635); +} + +.ci-card[data-health="has-failures"] { + border-left: 4px solid var(--pf-global--palette--orange-300, #ec7a08); +} + +.ci-card-header { + display: flex; + align-items: center; + gap: 10px; + margin-bottom: 8px; +} + +.ci-card-health { + flex-shrink: 0; + width: 20px; + height: 20px; + display: flex; + align-items: center; + justify-content: center; +} + +.ci-card-title { + font-size: 1.05rem; + font-weight: 600; + color: var(--pf-global--Color--100, #151515); + line-height: 1.3; +} + +.ci-card-summary { + font-size: 0.85rem; + margin-bottom: 12px; + font-weight: 500; +} + +.ci-card-summary.green, +.ci-card-summary.green-with-infra { + color: var(--pf-global--success-color--100, #3e8635); +} + +.ci-card-summary.has-failures { + color: var(--pf-global--palette--orange-300, #ec7a08); +} + +.ci-card-summary.loading { + color: var(--pf-global--Color--200, #6a6e73); + font-style: italic; +} + +.ci-card-platforms { + display: flex; + flex-wrap: wrap; + gap: 10px; + margin-bottom: 12px; +} + +.ci-card-platform { + display: flex; + align-items: center; + gap: 4px; + font-size: 0.8rem; + color: var(--pf-global--Color--200, #6a6e73); +} + +.ci-card-platform-dot { + width: 8px; + height: 8px; + border-radius: 50%; + flex-shrink: 0; +} + +.ci-card-platform-dot.loading { + background-color: var(--pf-global--BorderColor--100, #d2d2d2); +} + +.ci-card-platform-dot.green { + background-color: var(--pf-global--success-color--100, #3e8635); +} + +.ci-card-platform-dot.yellow { + background-color: var(--pf-global--disabled-color--200, #b8bbbe); +} + +.ci-card-platform-dot.red { + background-color: var(--pf-global--danger-color--100, #c9190b); +} + +.ci-card-platform-label { + white-space: nowrap; +} + +.ci-card-blocks { + display: flex; + gap: 2px; + margin-top: auto; + padding-top: 8px; +} + +.ci-card-block { + width: 4px; + height: 16px; + border-radius: 1px; + flex-shrink: 0; +} + +.ci-card-block.loading { + background-color: var(--pf-global--BorderColor--100, #d2d2d2); +} + +.ci-card-block.green { + background-color: var(--pf-global--success-color--100, #3e8635); +} + +.ci-card-block.yellow { + background-color: var(--pf-global--disabled-color--200, #b8bbbe); +} + +.ci-card-block.red { + background-color: var(--pf-global--danger-color--100, #c9190b); +} + +.ci-card-footer { + font-size: 0.8rem; + color: var(--pf-global--Color--200, #6a6e73); +} + +.ci-back-link { + display: inline-block; + margin-bottom: 12px; + font-size: 0.9rem; + color: var(--pf-global--primary-color--100, #06c); + text-decoration: none; + font-weight: 500; +} + +.ci-back-link:hover { + text-decoration: underline; +} + +.ci-detail-title { + font-size: 1.4rem; + font-weight: 600; + margin-bottom: 12px; + color: var(--pf-global--Color--100, #151515); +} + +.ci-status-key { + display: flex; + flex-wrap: wrap; + gap: 16px; + margin-bottom: 16px; + padding: 12px 16px; + background: var(--pf-global--BackgroundColor--200, #f0f0f0); + border-radius: 6px; + font-size: 0.85rem; + color: var(--pf-global--Color--100, #151515); +} + +.ci-status-key-item { + display: flex; + align-items: center; + gap: 6px; +} + .ci-icon { - display: inline-block; /* placeholder property */ + display: inline-block; } .pf-c-content ul.links-menu { diff --git a/static/data/sample-badges.json b/static/data/sample-badges.json new file mode 100644 index 000000000..da7240fd1 --- /dev/null +++ b/static/data/sample-badges.json @@ -0,0 +1,605 @@ +[ + { + "base": "https://storage.googleapis.com/vp-results", + "key": "industrialedge-aws-4.18-stable-badge.json", + "pattern": "industrialedge", + "platform": "aws", + "operator": "N/A", + "version": "4.18", + "date": "2026-04-16" + }, + { + "base": "https://storage.googleapis.com/vp-results", + "key": "mcgitopshcp-aws-4.20-stable-badge.json", + "pattern": "mcgitopshcp", + "platform": "aws", + "operator": "N/A", + "version": "4.20", + "date": "2026-04-15" + }, + { + "base": "https://storage.googleapis.com/vp-results", + "key": "ragllm-aws-4.21-stable-badge.json", + "pattern": "ragllm", + "platform": "aws", + "operator": "N/A", + "version": "4.21", + "date": "2026-04-15" + }, + { + "base": "https://storage.googleapis.com/vp-results", + "key": "ragllm-azr-4.20-stable-badge.json", + "pattern": "ragllm", + "platform": "azr", + "operator": "N/A", + "version": "4.20", + "date": "2026-04-15" + }, + { + "base": "https://storage.googleapis.com/vp-results", + "key": "aegitops-aws-4.18-stable-badge.json", + "pattern": "aegitops", + "platform": "aws", + "operator": "N/A", + "version": "4.18", + "date": "2026-04-14" + }, + { + "base": "https://storage.googleapis.com/vp-results", + "key": "aegitops-aws-4.21-stable-badge.json", + "pattern": "aegitops", + "platform": "aws", + "operator": "N/A", + "version": "4.21", + "date": "2026-04-14" + }, + { + "base": "https://storage.googleapis.com/vp-results", + "key": "hypershift-aws-4.20-stable-badge.json", + "pattern": "hypershift", + "platform": "aws", + "operator": "N/A", + "version": "4.20", + "date": "2026-04-14" + }, + { + "base": "https://storage.googleapis.com/vp-results", + "key": "medicaldiag-aws-4.18-stable-badge.json", + "pattern": "medicaldiag", + "platform": "aws", + "operator": "N/A", + "version": "4.18", + "date": "2026-04-14" + }, + { + "base": "https://storage.googleapis.com/vp-results", + "key": "hypershift-aws-4.21-stable-badge.json", + "pattern": "hypershift", + "platform": "aws", + "operator": "N/A", + "version": "4.21", + "date": "2026-04-13" + }, + { + "base": "https://storage.googleapis.com/vp-results", + "key": "mcgitops-aws-4.21-stable-badge.json", + "pattern": "mcgitops", + "platform": "aws", + "operator": "N/A", + "version": "4.21", + "date": "2026-04-13" + }, + { + "base": "https://storage.googleapis.com/vp-results", + "key": "medicaldiag-aws-4.21-stable-badge.json", + "pattern": "medicaldiag", + "platform": "aws", + "operator": "N/A", + "version": "4.21", + "date": "2026-04-13" + }, + { + "base": "https://storage.googleapis.com/vp-results", + "key": "medicaldiag-azr-4.20-stable-badge.json", + "pattern": "medicaldiag", + "platform": "azr", + "operator": "N/A", + "version": "4.20", + "date": "2026-04-13" + }, + { + "base": "https://storage.googleapis.com/vp-results", + "key": "openshiftai-intel-4.18-stable-badge.json", + "pattern": "openshiftai", + "platform": "intel", + "operator": "N/A", + "version": "4.18", + "date": "2026-04-12" + }, + { + "base": "https://storage.googleapis.com/vp-results", + "key": "openshiftai-intel-4.20-stable-badge.json", + "pattern": "openshiftai", + "platform": "intel", + "operator": "N/A", + "version": "4.20", + "date": "2026-04-12" + }, + { + "base": "https://storage.googleapis.com/vp-results", + "key": "openshiftai-intel-4.21-stable-badge.json", + "pattern": "openshiftai", + "platform": "intel", + "operator": "N/A", + "version": "4.21", + "date": "2026-04-12" + }, + { + "base": "https://storage.googleapis.com/vp-results", + "key": "mcgitops-intel-4.18-stable-badge.json", + "pattern": "mcgitops", + "platform": "intel", + "operator": "N/A", + "version": "4.18", + "date": "2026-04-11" + }, + { + "base": "https://storage.googleapis.com/vp-results", + "key": "mcgitops-intel-4.20-stable-badge.json", + "pattern": "mcgitops", + "platform": "intel", + "operator": "N/A", + "version": "4.20", + "date": "2026-04-11" + }, + { + "base": "https://storage.googleapis.com/vp-results", + "key": "mcgitops-intel-4.21-stable-badge.json", + "pattern": "mcgitops", + "platform": "intel", + "operator": "N/A", + "version": "4.21", + "date": "2026-04-11" + }, + { + "base": "https://storage.googleapis.com/vp-results", + "key": "mcgitopsstandalone-aws-4.21-stable-badge.json", + "pattern": "mcgitopsstandalone", + "platform": "aws", + "operator": "N/A", + "version": "4.21", + "date": "2026-04-11" + }, + { + "base": "https://storage.googleapis.com/vp-results", + "key": "openshiftai-aws-4.21-stable-badge.json", + "pattern": "openshiftai", + "platform": "aws", + "operator": "N/A", + "version": "4.21", + "date": "2026-04-11" + }, + { + "base": "https://storage.googleapis.com/vp-results", + "key": "patternsoperator-aws-4.18-stable-badge.json", + "pattern": "patternsoperator", + "platform": "aws", + "operator": "N/A", + "version": "4.18", + "date": "2026-04-11" + }, + { + "base": "https://storage.googleapis.com/vp-results", + "key": "travelops-aws-4.21-stable-badge.json", + "pattern": "travelops", + "platform": "aws", + "operator": "N/A", + "version": "4.21", + "date": "2026-04-11" + }, + { + "base": "https://storage.googleapis.com/vp-results", + "key": "coco-azr-4.21-stable-badge.json", + "pattern": "coco", + "platform": "azr", + "operator": "N/A", + "version": "4.21", + "date": "2026-04-10" + }, + { + "base": "https://storage.googleapis.com/vp-results", + "key": "mcgitopshcp-aws-4.21-stable-badge.json", + "pattern": "mcgitopshcp", + "platform": "aws", + "operator": "N/A", + "version": "4.21", + "date": "2026-04-10" + }, + { + "base": "https://storage.googleapis.com/vp-results", + "key": "openshiftai-aws-4.18-stable-badge.json", + "pattern": "openshiftai", + "platform": "aws", + "operator": "N/A", + "version": "4.18", + "date": "2026-04-10" + }, + { + "base": "https://storage.googleapis.com/vp-results", + "key": "hypershift-aws-4.18-stable-badge.json", + "pattern": "hypershift", + "platform": "aws", + "operator": "N/A", + "version": "4.18", + "date": "2026-04-09" + }, + { + "base": "https://storage.googleapis.com/vp-results", + "key": "travelops-aws-4.18-stable-badge.json", + "pattern": "travelops", + "platform": "aws", + "operator": "N/A", + "version": "4.18", + "date": "2026-04-09" + }, + { + "base": "https://storage.googleapis.com/vp-results", + "key": "travelops-aws-4.20-stable-badge.json", + "pattern": "travelops", + "platform": "aws", + "operator": "N/A", + "version": "4.20", + "date": "2026-04-09" + }, + { + "base": "https://storage.googleapis.com/vp-results", + "key": "industrialedge-azr-4.20-stable-badge.json", + "pattern": "industrialedge", + "platform": "azr", + "operator": "N/A", + "version": "4.20", + "date": "2026-04-08" + }, + { + "base": "https://storage.googleapis.com/vp-results", + "key": "ragllm-aws-4.18-stable-badge.json", + "pattern": "ragllm", + "platform": "aws", + "operator": "N/A", + "version": "4.18", + "date": "2026-04-08" + }, + { + "base": "https://storage.googleapis.com/vp-results", + "key": "aegitops-aws-4.20-stable-badge.json", + "pattern": "aegitops", + "platform": "aws", + "operator": "N/A", + "version": "4.20", + "date": "2026-04-07" + }, + { + "base": "https://storage.googleapis.com/vp-results", + "key": "mcgitopsstandalone-aws-4.20-stable-badge.json", + "pattern": "mcgitopsstandalone", + "platform": "aws", + "operator": "N/A", + "version": "4.20", + "date": "2026-04-07" + }, + { + "base": "https://storage.googleapis.com/vp-results", + "key": "coco-azr-4.18-stable-badge.json", + "pattern": "coco", + "platform": "azr", + "operator": "N/A", + "version": "4.18", + "date": "2026-04-06" + }, + { + "base": "https://storage.googleapis.com/vp-results", + "key": "coco-azr-4.20-stable-badge.json", + "pattern": "coco", + "platform": "azr", + "operator": "N/A", + "version": "4.20", + "date": "2026-04-06" + }, + { + "base": "https://storage.googleapis.com/vp-results", + "key": "layeredzerotrust-aws-4.20-stable-badge.json", + "pattern": "layeredzerotrust", + "platform": "aws", + "operator": "N/A", + "version": "4.20", + "date": "2026-04-06" + }, + { + "base": "https://storage.googleapis.com/vp-results", + "key": "mcgitops-aws-4.20-stable-badge.json", + "pattern": "mcgitops", + "platform": "aws", + "operator": "N/A", + "version": "4.20", + "date": "2026-04-06" + }, + { + "base": "https://storage.googleapis.com/vp-results", + "key": "mcgitops-azr-4.18-stable-badge.json", + "pattern": "mcgitops", + "platform": "azr", + "operator": "N/A", + "version": "4.18", + "date": "2026-04-06" + }, + { + "base": "https://storage.googleapis.com/vp-results", + "key": "mcgitopshcp-aws-4.18-stable-badge.json", + "pattern": "mcgitopshcp", + "platform": "aws", + "operator": "N/A", + "version": "4.18", + "date": "2026-04-06" + }, + { + "base": "https://storage.googleapis.com/vp-results", + "key": "mcgitopsstandalone-gcp-4.18-stable-badge.json", + "pattern": "mcgitopsstandalone", + "platform": "gcp", + "operator": "N/A", + "version": "4.18", + "date": "2026-04-06" + }, + { + "base": "https://storage.googleapis.com/vp-results", + "key": "openshiftai-aws-4.20-stable-badge.json", + "pattern": "openshiftai", + "platform": "aws", + "operator": "N/A", + "version": "4.20", + "date": "2026-04-06" + }, + { + "base": "https://storage.googleapis.com/vp-results", + "key": "patternsoperator-aws-4.20-stable-badge.json", + "pattern": "patternsoperator", + "platform": "aws", + "operator": "N/A", + "version": "4.20", + "date": "2026-04-06" + }, + { + "base": "https://storage.googleapis.com/vp-results", + "key": "mcgitopsstandalone-aws-4.18-stable-badge.json", + "pattern": "mcgitopsstandalone", + "platform": "aws", + "operator": "N/A", + "version": "4.18", + "date": "2026-04-05" + }, + { + "base": "https://storage.googleapis.com/vp-results", + "key": "mcgitops-gcp-4.21-stable-badge.json", + "pattern": "mcgitops", + "platform": "gcp", + "operator": "N/A", + "version": "4.21", + "date": "2026-04-01" + }, + { + "base": "https://storage.googleapis.com/vp-results", + "key": "agof-aws-none-stable-badge.json", + "pattern": "agof", + "platform": "aws", + "operator": "N/A", + "version": "none", + "date": "2026-03-06" + }, + { + "base": "https://storage.googleapis.com/vp-results", + "key": "industrialedge-aws-4.21-stable-badge.json", + "pattern": "industrialedge", + "platform": "aws", + "operator": "N/A", + "version": "4.21", + "date": "2026-02-27" + }, + { + "base": "https://storage.googleapis.com/vp-results", + "key": "mcgitops-aws-4.18-stable-badge.json", + "pattern": "mcgitops", + "platform": "aws", + "operator": "N/A", + "version": "4.18", + "date": "2026-02-23" + }, + { + "base": "https://storage.googleapis.com/vp-results", + "key": "aegitops-aws-4.19-stable-badge.json", + "pattern": "aegitops", + "platform": "aws", + "operator": "N/A", + "version": "4.19", + "date": "2026-02-17" + }, + { + "base": "https://storage.googleapis.com/vp-results", + "key": "mcgitops-aws-4.19-stable-badge.json", + "pattern": "mcgitops", + "platform": "aws", + "operator": "N/A", + "version": "4.19", + "date": "2026-02-16" + }, + { + "base": "https://storage.googleapis.com/vp-results", + "key": "mcgitopshcp-aws-4.19-stable-badge.json", + "pattern": "mcgitopshcp", + "platform": "aws", + "operator": "N/A", + "version": "4.19", + "date": "2026-02-16" + }, + { + "base": "https://storage.googleapis.com/vp-results", + "key": "medicaldiag-aws-4.20-stable-badge.json", + "pattern": "medicaldiag", + "platform": "aws", + "operator": "N/A", + "version": "4.20", + "date": "2026-02-16" + }, + { + "base": "https://storage.googleapis.com/vp-results", + "key": "hypershift-aws-4.19-stable-badge.json", + "pattern": "hypershift", + "platform": "aws", + "operator": "N/A", + "version": "4.19", + "date": "2026-02-13" + }, + { + "base": "https://storage.googleapis.com/vp-results", + "key": "ragllm-aws-4.19-stable-badge.json", + "pattern": "ragllm", + "platform": "aws", + "operator": "N/A", + "version": "4.19", + "date": "2026-02-13" + }, + { + "base": "https://storage.googleapis.com/vp-results", + "key": "medicaldiag-azr-4.19-stable-badge.json", + "pattern": "medicaldiag", + "platform": "azr", + "operator": "N/A", + "version": "4.19", + "date": "2026-02-09" + }, + { + "base": "https://storage.googleapis.com/vp-results", + "key": "openshiftai-intel-4.19-stable-badge.json", + "pattern": "openshiftai", + "platform": "intel", + "operator": "N/A", + "version": "4.19", + "date": "2026-02-08" + }, + { + "base": "https://storage.googleapis.com/vp-results", + "key": "mcgitops-intel-4.19-stable-badge.json", + "pattern": "mcgitops", + "platform": "intel", + "operator": "N/A", + "version": "4.19", + "date": "2026-02-07" + }, + { + "base": "https://storage.googleapis.com/vp-results", + "key": "mcgitopsstandalone-aws-4.19-stable-badge.json", + "pattern": "mcgitopsstandalone", + "platform": "aws", + "operator": "N/A", + "version": "4.19", + "date": "2026-02-06" + }, + { + "base": "https://storage.googleapis.com/vp-results", + "key": "openshiftai-aws-4.19-stable-badge.json", + "pattern": "openshiftai", + "platform": "aws", + "operator": "N/A", + "version": "4.19", + "date": "2026-02-06" + }, + { + "base": "https://storage.googleapis.com/vp-results", + "key": "travelops-aws-4.19-stable-badge.json", + "pattern": "travelops", + "platform": "aws", + "operator": "N/A", + "version": "4.19", + "date": "2026-02-05" + }, + { + "base": "https://storage.googleapis.com/vp-results", + "key": "industrialedge-aws-4.19-stable-badge.json", + "pattern": "industrialedge", + "platform": "aws", + "operator": "N/A", + "version": "4.19", + "date": "2026-02-04" + }, + { + "base": "https://storage.googleapis.com/vp-results", + "key": "coco-azr-4.19-stable-badge.json", + "pattern": "coco", + "platform": "azr", + "operator": "N/A", + "version": "4.19", + "date": "2026-02-03" + }, + { + "base": "https://storage.googleapis.com/vp-results", + "key": "patternsoperator-aws-4.19-stable-badge.json", + "pattern": "patternsoperator", + "platform": "aws", + "operator": "N/A", + "version": "4.19", + "date": "2026-02-03" + }, + { + "base": "https://storage.googleapis.com/vp-results", + "key": "mcgitops-gcp-4.19-stable-badge.json", + "pattern": "mcgitops", + "platform": "gcp", + "operator": "N/A", + "version": "4.19", + "date": "2026-02-01" + }, + { + "base": "https://storage.googleapis.com/vp-results", + "key": "layeredzerotrust-aws-4.19-stable-badge.json", + "pattern": "layeredzerotrust", + "platform": "aws", + "operator": "N/A", + "version": "4.19", + "date": "2025-12-22" + }, + { + "base": "https://vp-ntnx-results.s3.amazonaws.com", + "key": "mcgitops-nutanix-4.12-stable-badge.json", + "pattern": "mcgitops", + "platform": "nutanix", + "operator": "N/A", + "version": "4.12", + "date": "2024-04-03" + }, + { + "base": "https://vp-ntnx-results.s3.amazonaws.com", + "key": "mcgitops-nutanix-4.13-stable-badge.json", + "pattern": "mcgitops", + "platform": "nutanix", + "operator": "N/A", + "version": "4.13", + "date": "2024-04-03" + }, + { + "base": "https://vp-ntnx-results.s3.amazonaws.com", + "key": "mcgitops-nutanix-4.14-stable-badge.json", + "pattern": "mcgitops", + "platform": "nutanix", + "operator": "N/A", + "version": "4.14", + "date": "2024-04-03" + }, + { + "base": "https://vp-ntnx-results.s3.amazonaws.com", + "key": "mcgitops-nutanix-4.15-stable-badge.json", + "pattern": "mcgitops", + "platform": "nutanix", + "operator": "N/A", + "version": "4.15", + "date": "2024-04-03" + } +] \ No newline at end of file diff --git a/static/js/dashboard.v2.js b/static/js/dashboard.v2.js index c33f347c8..32ae1710f 100644 --- a/static/js/dashboard.v2.js +++ b/static/js/dashboard.v2.js @@ -1,9 +1,11 @@ +// ============================================ +// Data Classes & Parsing +// ============================================ + class Badge { constructor (base, key, date) { this.base = base - //this.base = '/ci/vp-results' - //this.base = 'https://storage.googleapis.com/vp-results' this.key = key const fields = key.split('-') this.pattern = fields[0] @@ -30,23 +32,14 @@ class Badge { } } +// ============================================ +// Helper Functions +// ============================================ + function getJiraSearch (pattern) { return 'https://issues.redhat.com/issues/?jql=project%3D%22Validated%20Patterns%22%20and%20labels%20in%20(ci-fail)%20and%20component%3D' + jira_component(pattern) + '%20and%20status%20not%20in%20(Closed)' } -function getLabel (field, json_obj) { - if (field == 'pattern') { - return stringForKey(json_obj.infraProvider) + ' ' + json_obj.openshiftVersion - } - if (field == 'platform') { - return stringForKey(json_obj.patternName) + ' - ' + json_obj.openshiftVersion - } - if (field == 'version') { - return stringForKey(json_obj.patternName) + ' : ' + stringForKey(json_obj.infraProvider) - } - return stringForKey(json_obj.patternName) + ' : ' + stringForKey(json_obj.infraProvider) + ' ' + json_obj.openshiftVersion -} - function sleep (ms) { return new Promise(resolve => setTimeout(resolve, ms)) } @@ -70,16 +63,6 @@ function filterBadges (badges, field, value) { return badges } -function rowTitle (field, value) { - if (field === 'pattern') { - return stringForKey(value) - } - if (field === 'platform') { - return stringForKey(value) - } - return value -} - function jira_component (pattern) { if (pattern == 'aegitops') { return 'ansible-edge' @@ -242,6 +225,55 @@ function toTitleCase (str) { ) } +// ============================================ +// Network / JSON Fetching +// ============================================ + +function jsonSuccess() { + this.callback.apply(this, this.arguments); +} + +function jsonError() { + console.error(this.statusText); +} + +function getJSON(url, callback, ...args) { + const jsonRequest = new XMLHttpRequest(); + jsonRequest.callback = callback; + jsonRequest.arguments = args; + jsonRequest.onload = jsonSuccess; + jsonRequest.onerror = jsonError; + jsonRequest.open("GET", url, true); + jsonRequest.send(null); +} + +// ============================================ +// Legacy Badge Rendering (for pattern page embeds) +// ============================================ + +function getLabel (field, json_obj) { + if (field == 'pattern') { + return stringForKey(json_obj.infraProvider) + ' ' + json_obj.openshiftVersion + } + if (field == 'platform') { + return stringForKey(json_obj.patternName) + ' - ' + json_obj.openshiftVersion + } + if (field == 'version') { + return stringForKey(json_obj.patternName) + ' : ' + stringForKey(json_obj.infraProvider) + } + return stringForKey(json_obj.patternName) + ' : ' + stringForKey(json_obj.infraProvider) + ' ' + json_obj.openshiftVersion +} + +function rowTitle (field, value) { + if (field === 'pattern') { + return stringForKey(value) + } + if (field === 'platform') { + return stringForKey(value) + } + return value +} + function renderSetButtons(sets){ var currentURL = new URL(window.location.href) const queryString = window.location.search @@ -266,24 +298,6 @@ function renderSetButtons(sets){ return buttonText } -function jsonSuccess() { - this.callback.apply(this, this.arguments); -} - -function jsonError() { - console.error(this.statusText); -} - -function getJSON(url, callback, ...args) { - const jsonRequest = new XMLHttpRequest(); - jsonRequest.callback = callback; - jsonRequest.arguments = args; - jsonRequest.onload = jsonSuccess; - jsonRequest.onerror = jsonError; - jsonRequest.open("GET", url, true); - jsonRequest.send(null); -} - function renderSingleBadge (key, field, linkType, badge_url) { var json_obj = JSON.parse(this.responseText) var branchLabel = json_obj.patternBranch @@ -372,6 +386,684 @@ function createFilteredHorizontalTable (badges, field, value, titles, links) { return tableText + '' } +function processBadgesLegacy (badges, options) { + const filter_field = options.get('filter_field') + const filter_value = options.get('filter_value') + const links = options.get("links") + + var htmlText = "" + htmlText += renderSetButtons(options.get('sets')) + + if (filter_field === 'date') { + badges.sort(function (a, b) { return -1 * a.date.localeCompare(b.date) }) + if (filter_value != null && filter_value != "all") { + htmlText += renderBadges(badges, filter_field, filter_value, links) + } else if (filter_value == "all") { + htmlText += createFilteredHorizontalTable(badges, filter_field, null, true, links) + } + } else if (filter_field != null) { + if (filter_value != null && filter_value != "all") { + badges = filterBadges(badges, filter_field, filter_value) + } + badges.sort(patternSort) + if (filter_value != null && filter_value != "all") { + htmlText += renderBadges(badges, filter_field, filter_value, links) + } else if (filter_value == "all") { + htmlText += createFilteredHorizontalTable(badges, filter_field, null, true, links) + } + } else { + badges.sort(function (a, b) { return -1 * a.date.localeCompare(b.date) }) + badges.sort(patternVertSort) + htmlText += createFilteredHorizontalTable(badges, 'pattern', null, true, links) + } + + document.getElementById(options.get('target')).innerHTML = htmlText +} + +// ============================================ +// New Dashboard Rendering (for CI status page) +// ============================================ + +function statusLabel (color) { + if (color === 'green') return 'Passed' + if (color === 'yellow') return 'CI infrastructure failure' + if (color === 'red') return 'CI test failure' + return 'Unknown' +} + +function platformDisplayName (platform) { + var names = { + 'aws': 'AWS (us-east-1)', + 'azr': 'Azure', + 'gcp': 'Google Cloud', + 'nutanix': 'Nutanix', + 'intel': 'On-prem (Intel)' + } + return names[platform] || stringForKey(platform) +} + +function platformIconSvg (platform) { + if (platform === 'aws' || platform === 'gcp') { + return '' + } + if (platform === 'azr') { + return '' + } + if (platform === 'intel' || platform === 'nutanix') { + return '' + } + return '' +} + +function timeAgo (dateStr) { + if (!dateStr) return '' + var date = new Date(dateStr + 'T00:00:00') + var now = new Date() + var diffMs = now - date + var diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24)) + + if (diffDays < 0) return dateStr + if (diffDays === 0) return 'Today' + if (diffDays === 1) return 'Yesterday' + if (diffDays < 7) return diffDays + ' days ago' + if (diffDays < 30) { + var weeks = Math.floor(diffDays / 7) + return weeks + (weeks === 1 ? ' week ago' : ' weeks ago') + } + if (diffDays < 365) { + var months = Math.floor(diffDays / 30) + return months + (months === 1 ? ' month ago' : ' months ago') + } + var years = Math.floor(diffDays / 365) + return years + (years === 1 ? ' year ago' : ' years ago') +} + +function sanitizeId (str) { + return str.replace(/\./g, '_').replace(/[^a-zA-Z0-9_-]/g, '-') +} + +function getLatestPerPatternPlatform (badges) { + var seen = {} + var result = [] + badges.forEach(function (b) { + var compositeKey = b.pattern + '-' + b.platform + if (!seen[compositeKey]) { + seen[compositeKey] = true + result.push(b) + } + }) + return result +} + +function getCurrentTab () { + var params = new URLSearchParams(window.location.search) + if (params.get('date') != null) return 'history' + if (params.get('pattern') != null) { + var val = params.get('pattern') + if (val !== 'all') return 'pattern-detail' + return 'patterns' + } + if (params.get('platform') != null) return 'infrastructure' + if (params.get('version') != null) return 'version' + return 'overview' +} + +function buildTabUrl (paramKey) { + var base = window.location.pathname + if (!paramKey) return base + return base + '?' + paramKey + '=all' +} + +function renderTabs (activeTab) { + var tabs = [ + { id: 'overview', label: 'Overview', href: buildTabUrl(null) }, + { id: 'infrastructure', label: 'By Platform', href: buildTabUrl('platform') }, + { id: 'version', label: 'By Version', href: buildTabUrl('version') }, + { id: 'history', label: 'History', href: buildTabUrl('date') } + ] + + var html = '
' + tabs.forEach(function (tab) { + var isActive = tab.id === activeTab || (activeTab === 'pattern-detail' && tab.id === 'overview') + var activeClass = isActive ? ' active' : '' + html += '' + tab.label + '' + }) + html += '
' + return html +} + +function renderSortControl (currentSort) { + var html = '
' + html += '' + html += '' + html += '
' + return html +} + +function renderDashboardTableHeader () { + return '' + + '' + + '' + + '' + + '' + + '' + + '' +} + +function renderDashboardTableRow (badge) { + var rowId = sanitizeId(badge.key) + var patternName = stringForKey(badge.pattern) + var platformName = platformDisplayName(badge.platform) + var version = (badge.version && badge.version !== 'none' && badge.version !== '') ? 'OCP ' + badge.version : '' + var time = timeAgo(badge.date) + + var html = '' + html += '' + + html += '' + + html += '' + + html += '' + + html += '' + html += '' + return html +} + +function updateRowStatus (badgeKey) { + var json_obj = JSON.parse(this.responseText) + var color = json_obj.color || 'green' + var branch = json_obj.patternBranch || '' + var rowId = sanitizeId(badgeKey) + + var statusEl = document.getElementById('ci-status-' + rowId) + if (statusEl) { + statusEl.innerHTML = + '' + + '' + statusLabel(color) + '' + } + + var branchEl = document.getElementById('ci-branch-' + rowId) + if (branchEl && branch) { + branchEl.textContent = '#' + branch + } +} + +function isProductionSite () { + return window.location.hostname === 'validatedpatterns.io' +} + +function sampleColorForKey (badgeKey) { + var colors = ['green', 'green', 'green', 'green', 'green', 'green', 'yellow', 'red'] + var hash = 0 + for (var i = 0; i < badgeKey.length; i++) { + hash = ((hash << 5) - hash) + badgeKey.charCodeAt(i) + hash = hash & hash + } + return colors[Math.abs(hash) % colors.length] +} + +function simulateSampleStatus (badgeKey) { + var rowId = sanitizeId(badgeKey) + var color = sampleColorForKey(badgeKey) + var branch = 'main' + var hash = 0 + for (var i = 0; i < badgeKey.length; i++) { + hash = ((hash << 5) - hash) + badgeKey.charCodeAt(i) + hash = hash & hash + } + + setTimeout(function () { + var statusEl = document.getElementById('ci-status-' + rowId) + if (statusEl) { + statusEl.innerHTML = + '' + + '' + statusLabel(color) + '' + } + var branchEl = document.getElementById('ci-branch-' + rowId) + if (branchEl) { + branchEl.textContent = '#' + branch + } + }, 100 + Math.abs(hash) % 300) +} + +function renderDashboardTableWithBadges (badges) { + var html = renderDashboardTableHeader() + var useSample = !isProductionSite() + badges.forEach(function (b) { + html += renderDashboardTableRow(b) + if (useSample) { + simulateSampleStatus(b.key) + } else { + getJSON(b.getURI(), updateRowStatus, b.key) + } + }) + html += '
StatusPatternPlatformOpenShift VersionTime
' + html += '' + html += '' + html += '
' + html += '' + patternName + '' + html += '
' + platformIconSvg(badge.platform) + ' ' + platformName + '
' + version + '' + time + '
' + return html +} + +function renderGroupedTables (badges, groupField) { + var groups = getUniqueValues(badges, groupField) + var html = '' + + groups.forEach(function (groupValue) { + var groupBadges = filterBadges(badges, groupField, groupValue) + var groupTitle + + if (groupField === 'pattern') { + groupTitle = '' + stringForKey(groupValue) + '' + } else if (groupField === 'platform') { + groupTitle = platformDisplayName(groupValue) + } else if (groupField === 'version') { + groupTitle = (groupValue && groupValue !== 'none') ? 'OCP ' + groupValue : 'Unversioned' + } else { + groupTitle = groupValue + } + + html += '

' + groupTitle + '

' + html += renderDashboardTableWithBadges(groupBadges) + }) + + return html +} + +// ============================================ +// Card-Based Dashboard (Progressive Disclosure) +// ============================================ + +var _cardTracker = {} + +function groupBadgesByPattern (badges) { + var groups = {} + badges.forEach(function (b) { + if (!groups[b.pattern]) { + groups[b.pattern] = [] + } + groups[b.pattern].push(b) + }) + return groups +} + +function getLatestBadgePerPlatform (badges) { + var seen = {} + var result = [] + badges.forEach(function (b) { + if (!seen[b.platform]) { + seen[b.platform] = true + result.push(b) + } + }) + return result +} + +function filterRecentBadges (badges, months) { + var cutoff = new Date() + cutoff.setMonth(cutoff.getMonth() - months) + var cutoffStr = cutoff.toISOString().substr(0, 10) + return badges.filter(function (b) { return b.date >= cutoffStr }) +} + +function getLatestBadgePerCombo (badges) { + var seen = {} + var result = [] + badges.forEach(function (b) { + var combo = b.platform + '-' + b.version + if (!seen[combo]) { + seen[combo] = true + result.push(b) + } + }) + return result +} + +function computeCardHealth (tracker) { + if (tracker.loaded < tracker.total) return 'loading' + var colors = Object.values(tracker.platforms) + var hasRed = colors.indexOf('red') !== -1 + var hasYellow = colors.indexOf('yellow') !== -1 + + if (!hasRed && !hasYellow) return 'green' + if (!hasRed && hasYellow) return 'green-with-infra' + return 'has-failures' +} + +function cardHealthLabel (tracker) { + var colors = Object.values(tracker.platforms) + var total = colors.length + var greenCount = colors.filter(function (c) { return c === 'green' }).length + var yellowCount = colors.filter(function (c) { return c === 'yellow' }).length + var redCount = colors.filter(function (c) { return c === 'red' }).length + + if (greenCount === total) return 'Passing' + if (redCount === 0 && yellowCount > 0) { + return 'Passing (' + yellowCount + ' infra ' + (yellowCount === 1 ? 'issue' : 'issues') + ')' + } + var passing = greenCount + yellowCount + if (redCount > 0) { + return 'Passing on ' + passing + '/' + total + ' platforms' + } + return 'Loading...' +} + +function cardHealthIcon (health) { + if (health === 'green' || health === 'green-with-infra') { + return '' + } + if (health === 'has-failures') { + return '' + } + return '' +} + +function updateCardUI (pattern) { + var tracker = _cardTracker[pattern] + if (!tracker) return + var health = computeCardHealth(tracker) + + Object.keys(tracker.platforms).forEach(function (platform) { + var dotEl = document.getElementById('ci-card-dot-' + sanitizeId(pattern) + '-' + sanitizeId(platform)) + if (dotEl) { + dotEl.className = 'ci-card-platform-dot ' + tracker.platforms[platform] + } + }) + + Object.keys(tracker.tests).forEach(function (comboKey) { + var blockEl = document.getElementById('ci-card-block-' + sanitizeId(pattern) + '-' + comboKey) + if (blockEl) { + blockEl.className = 'ci-card-block ' + tracker.tests[comboKey] + } + }) + + var summaryEl = document.getElementById('ci-card-summary-' + sanitizeId(pattern)) + if (summaryEl) { + summaryEl.textContent = cardHealthLabel(tracker) + summaryEl.className = 'ci-card-summary ' + health + } + + var iconEl = document.getElementById('ci-card-icon-' + sanitizeId(pattern)) + if (iconEl) { + iconEl.innerHTML = cardHealthIcon(health) + } + + var cardEl = document.getElementById('ci-card-' + sanitizeId(pattern)) + if (cardEl) { + cardEl.setAttribute('data-health', health) + } + + updateOverallStats() +} + +function updateOverallStats () { + var statsEl = document.getElementById('ci-overall-stats') + if (!statsEl) return + + var patterns = Object.keys(_cardTracker) + var totalPatterns = patterns.length + var passingCount = 0 + var infraCount = 0 + var failureCount = 0 + var loading = 0 + + patterns.forEach(function (p) { + var health = computeCardHealth(_cardTracker[p]) + if (health === 'green') passingCount++ + else if (health === 'green-with-infra') { passingCount++; infraCount++ } + else if (health === 'has-failures') failureCount++ + else loading++ + }) + + if (loading > 0) { + statsEl.innerHTML = '
Loading test results...
' + return + } + + var pct = Math.round((passingCount / totalPatterns) * 100) + var html = '
' + html += '
' + html += '' + passingCount + ' of ' + totalPatterns + ' patterns passing' + html += '
' + html += '
' + html += '
' + statsEl.innerHTML = html +} + +function cardStatusCallback (badgeKey, pattern, platform, comboKey) { + var json_obj = JSON.parse(this.responseText) + var color = json_obj.color || 'green' + + if (_cardTracker[pattern]) { + _cardTracker[pattern].tests[comboKey] = color + _cardTracker[pattern].loadedTests++ + + // Platform dot uses latest badge only (first one seen, since badges sorted by date desc) + if (!_cardTracker[pattern].platforms[platform]) { + _cardTracker[pattern].platforms[platform] = color + _cardTracker[pattern].loaded++ + } + + updateCardUI(pattern) + } +} + +function renderPatternCard (pattern, platformBadges, comboBadges) { + var patternId = sanitizeId(pattern) + var patternName = stringForKey(pattern) + var latestDate = platformBadges.length > 0 ? platformBadges[0].date : '' + + var html = '' + + html += '
' + html += '' + cardHealthIcon('loading') + '' + html += '' + patternName + '' + html += '
' + + html += '
Loading...
' + + html += '
' + platformBadges.forEach(function (b) { + html += '' + html += '' + html += '' + stringForKey(b.platform) + '' + html += '' + }) + html += '
' + + html += '
' + comboBadges.slice().reverse().forEach(function (b) { + var comboId = sanitizeId(b.platform) + '-' + sanitizeId(b.version) + html += '' + }) + html += '
' + + html += '' + + html += '
' + return html +} + +function renderPatternCards (badges) { + badges.sort(function (a, b) { return -1 * a.date.localeCompare(b.date) }) + + var groups = groupBadgesByPattern(badges) + var patternNames = Object.keys(groups).sort(function (a, b) { + return stringForKey(a).localeCompare(stringForKey(b)) + }) + + _cardTracker = {} + + var html = '
Cards show the latest test per platform. Status bars show all tests from the last 3 months, oldest to newest. Passed Infra issue Test failure
' + html += '
Loading test results...
' + + html += '
' + + var useSample = !isProductionSite() + + patternNames.forEach(function (pattern) { + var patternBadges = groups[pattern] + var latestPerPlatform = getLatestBadgePerPlatform(patternBadges) + var latestPerCombo = getLatestBadgePerCombo(patternBadges) + var recentCombos = getLatestBadgePerCombo(filterRecentBadges(patternBadges, 3)) + + _cardTracker[pattern] = { + total: latestPerPlatform.length, + loaded: 0, + platforms: {}, + tests: {}, + totalTests: latestPerCombo.length, + loadedTests: 0, + recentComboKeys: {} + } + + recentCombos.forEach(function (b) { + _cardTracker[pattern].recentComboKeys[sanitizeId(b.platform) + '-' + sanitizeId(b.version)] = true + }) + + html += renderPatternCard(pattern, latestPerPlatform, recentCombos) + + latestPerCombo.forEach(function (b) { + var comboKey = sanitizeId(b.platform) + '-' + sanitizeId(b.version) + if (useSample) { + var color = sampleColorForKey(b.key) + _cardTracker[pattern].tests[comboKey] = color + + // Platform dot uses latest badge only (first one seen) + if (!_cardTracker[pattern].platforms[b.platform]) { + _cardTracker[pattern].platforms[b.platform] = color + } + _cardTracker[pattern].loadedTests++ + _cardTracker[pattern].loaded = latestPerPlatform.length + } else { + getJSON(b.getURI(), cardStatusCallback, b.key, pattern, b.platform, comboKey) + } + }) + }) + + html += '
' + + if (useSample) { + setTimeout(function () { + patternNames.forEach(function (pattern) { + updateCardUI(pattern) + }) + }, 150) + } + + html += 'View complete history →' + + return html +} + +function renderStatusKey () { + return '
' + + ' Passed: Pattern deployed and tests succeeded' + + ' CI infrastructure failure: Cloud or pipeline problem, not a pattern issue' + + ' CI test failure: Pattern tests did not pass' + + '
' +} + +function handleSort (value) { + var url = new URL(window.location.href) + url.searchParams.set('sort', value) + window.location.href = url.toString() +} + +function renderDashboard (badges, options) { + var filter_field = options.get('filter_field') + var filter_value = options.get('filter_value') + var target = options.get('target') + + var currentTab = getCurrentTab() + var params = new URLSearchParams(window.location.search) + var currentSort = params.get('sort') || 'latest' + + if (filter_value != null && filter_value !== 'all') { + badges = filterBadges(badges, filter_field, filter_value) + } + + badges.sort(function (a, b) { return -1 * a.date.localeCompare(b.date) }) + + if (currentSort === 'pattern') { + badges.sort(patternSort) + } else if (currentSort === 'platform') { + badges.sort(function (a, b) { + if (a.platform !== b.platform) return a.platform.localeCompare(b.platform) + return -1 * a.date.localeCompare(b.date) + }) + } else if (currentSort === 'version') { + badges.sort(function (a, b) { + if (a.version !== b.version) return -1 * a.version.localeCompare(b.version) + return -1 * a.date.localeCompare(b.date) + }) + } + + var html = '' + + // Show tabs for top-level views (not for pattern detail drill-down) + html += renderTabs(currentTab) + + if (currentTab === 'overview') { + // Card-based overview — no sort control + html += renderPatternCards(badges) + } else if (currentTab === 'pattern-detail') { + // Drill-down from a card: show back link + detail table + status key + html += '← Back to overview' + html += '

' + stringForKey(filter_value) + '

' + html += renderStatusKey() + html += renderSortControl(currentSort) + html += renderDashboardTableWithBadges(badges) + } else if (currentTab === 'infrastructure') { + html += renderStatusKey() + html += renderSortControl(currentSort) + if (filter_value != null && filter_value !== 'all') { + html += renderDashboardTableWithBadges(badges) + } else { + html += renderGroupedTables(badges, 'platform') + } + } else if (currentTab === 'version') { + html += renderStatusKey() + html += renderSortControl(currentSort) + if (filter_value != null && filter_value !== 'all') { + html += renderDashboardTableWithBadges(badges) + } else { + html += renderGroupedTables(badges, 'version') + } + } else if (currentTab === 'history') { + html += renderStatusKey() + html += renderSortControl(currentSort) + if (filter_value != null && filter_value !== 'all') { + var dateBadges = filterBadges(badges, 'date', filter_value) + html += renderDashboardTableWithBadges(dateBadges) + } else { + html += renderDashboardTableWithBadges(badges) + } + } else if (currentTab === 'patterns') { + // Legacy route: ?pattern=all + html += renderSortControl(currentSort) + html += renderGroupedTables(badges, 'pattern') + } + + if (badges.length === 0) { + html += '
No CI results found.
' + } + + document.getElementById(target).innerHTML = html +} + +// ============================================ +// Badge Data Processing +// ============================================ + function getBadges (xmlText, bucket_url, badge_set) { parser = new DOMParser() var xmlDoc = parser.parseFromString(xmlText, 'application/xml') @@ -386,7 +1078,6 @@ function getBadges (xmlText, bucket_url, badge_set) { l = entries.length for (i = 0; i < l; i++) { - // entries[i].childNodes[0].nodeValue key = entries[i].childNodes[0].nodeValue if (badge_set == "GA" && key.endsWith("stable-badge.json") ) { badges.push(new Badge(bucket_url, key, getBadgeDate(entries[i]))); @@ -403,49 +1094,18 @@ function getBadges (xmlText, bucket_url, badge_set) { } function processBadges (badges, options) { - const filter_field = options.get('filter_field') - const filter_value = options.get('filter_value') - const links = options.get("links"); - - htmlText = "" - if (options.get("disable_buttons") != true) { - htmlText += renderSetButtons(options.get('sets')) + if (options.get('disable_buttons') === true) { + processBadgesLegacy(badges, options) + return } - if (filter_field === 'date') { - badges.sort(function (a, b) { return -1 * a.date.localeCompare(b.date) }) - if (filter_value != null && filter_value != "all") { - htmlText += renderBadges(badges, filter_field, filter_value) - } else if (filter_value == "all") { - htmlText += createFilteredHorizontalTable(badges, filter_field, null, true, links) - } - } else if (filter_field != null) { - if (filter_value != null && filter_value != "all") { - badges = filterBadges(badges, filter_field, filter_value) - } - badges.sort(patternSort) - numElements = Math.min(Math.floor(window.innerWidth / 140), 6) - if (filter_value != null && filter_value != "all") { - htmlText += renderBadges(badges, filter_field, filter_value) - } else if (filter_value == "all") { - htmlText += createFilteredHorizontalTable(badges, filter_field, null, true, links) - } - } else { - badges.sort(function (a, b) { return -1 * a.date.localeCompare(b.date) }) - badges.sort(patternVertSort) - - htmlText += createFilteredHorizontalTable(badges, 'date', null, true, links) - htmlText += createFilteredHorizontalTable(badges, 'pattern', null, true, links) - htmlText += createFilteredHorizontalTable(badges, 'platform', null, true, links) - htmlText += createFilteredHorizontalTable(badges, 'version', null, true, links) - if ( options.get('sets').includes('all') || options.get('sets').includes('early')) { - htmlText += createFilteredHorizontalTable(badges, 'operator', null, true, links) - } - - } - document.getElementById(options.get('target')).innerHTML = htmlText + renderDashboard(badges, options) } +// ============================================ +// Entry Points +// ============================================ + function getBucketOptions (input) { const options = new Map() const queryString = window.location.search @@ -464,7 +1124,6 @@ function getBucketOptions (input) { if (bucket != null) { buckets.push(bucket) } else { - //buckets.push('/ci/vp-results.xml') buckets.push('https://storage.googleapis.com/vp-results') buckets.push('https://vp-ntnx-results.s3.amazonaws.com') } @@ -478,7 +1137,7 @@ function getBucketOptions (input) { options.set(fields[i], value) } } - const sections = [ 'date', 'version', 'platform', 'pattern', 'operator' ] + const sections = [ 'date', 'version', 'platform', 'pattern' ] filter_field = options.get('filter_field') @@ -515,22 +1174,48 @@ function fetchBucketBadges(bucket, inputs) { }); } +function obtainBadgesFromSample (inputs) { + const options = getBucketOptions(inputs); + + fetch('/data/sample-badges.json') + .then(function (response) { return response.json() }) + .then(function (sampleData) { + var allBadges = sampleData.map(function (item) { + var b = new Badge(item.base, item.key, item.date + 'T00:00:00Z') + b.pattern = item.pattern + b.platform = item.platform + b.operator = item.operator + b.version = item.version + b.date = item.date + b._color = sampleColorForKey(item.key) + return b + }) + console.log('Using sample data:', allBadges.length, 'badges') + processBadges(allBadges, options) + }) + .catch(function (error) { + console.error('Error loading sample data:', error) + }) +} + function obtainBadges (inputs) { + if (!isProductionSite()) { + console.log('Non-production site detected, using sample badge data') + obtainBadgesFromSample(inputs) + return + } + const options = getBucketOptions(inputs); const buckets = options.get('buckets') - // Create an array to store promises for each bucket's badges const badgePromises = []; - // Iterate through each bucket URL and fetch badges asynchronously for (const bucket of buckets) { badgePromises.push(fetchBucketBadges(bucket, inputs)); } - // Wait for all promises to resolve Promise.all(badgePromises) .then((results) => { - // Process the badges from all buckets const allBadges = []; for (const badges of results) { console.log("Got "+badges.length+" badges")