HTML Report Viewer — RequirementsOverviewVersion 2 of Extension Identity
Three Contribution Points1. Work Hub — HTML Report Viewer
2. Admin Configuration Page
3. Pipeline Build Task —
|
| Input | Type | Required | Default | Description |
|---|---|---|---|---|
reportName |
string | ✅ | — | Logical name / report title |
tabName |
string | ❌ | HTML-Report |
Label for this tab |
htmlDirectory |
filePath | ✅ | — | Directory containing HTML file(s) |
folderPath |
string | ❌ | / |
Target folder path in viewer |
Data Model (Extension Data Service, project-scoped)
FolderEntry — collection: hrv-folders
interface FolderEntry {
id: string; // UUID
name: string;
parentId: string | null; // null = root
isShared: boolean; // true = visible to all; false = private to creator
createdBy: string; // identity descriptor
createdAt: string; // ISO 8601
}
ReportEntry — collection: hrv-reports
interface ReportEntry {
id: string; // UUID
name: string;
folderId: string; // "root" or FolderEntry.id
tabs: ReportTab[];
createdBy: string;
createdAt: string;
updatedAt: string;
}
ReportTab
interface ReportTab {
id: string;
label: string; // display name on the tab
chunkIds: string[]; // ordered list of chunk document IDs
totalSize: number; // bytes (original HTML)
addedAt: string;
addedBy: string;
}
Chunk documents — collection: hrv-chunks
interface ChunkDoc {
id: string; // referenced by ReportTab.chunkIds
data: string; // base64-encoded ~800 KB slice of HTML
}
Chunking rule: HTML is UTF-8 → base64 → split every 800 000 chars → one document per chunk.
On read: fetch all chunk docs in order → concat → atob() → full HTML string → inject into <iframe srcdoc>.
Hub UI Layout
┌─ HTML Reports (hub group) ────────────────────────────────────────────────┐
│ │
│ [+ New Folder] [+ Upload Report] 🔍 Search reports... │
│ │
│ ┌──────────────┐ ┌───────────────────────────────────────────────────┐ │
│ │ 📁 Root │ │ Name │ Tabs │ Created By │ Date │ │
│ │ ├ 📁 Alice │ │ ─────────────│──────│────────────│──────────── │ │
│ │ ├ 📁 Bob │ │ HTML-Report │ 3 │ pipeline │ 2026-05-24 │ │
│ │ └ 📁 QA │ │ Coverage │ 1 │ alice │ 2026-05-23 │ │
│ │ └ 🔒 Mine │ │ │ │
│ └──────────────┘ └───────────────────────────────────────────────────┘ │
│ │
│ ┌─ Report Viewer (expands on row click) ─────────────────────────────┐ │
│ │ ◀ [Tab1] [Tab2] [Tab3] [Tab4] [Tab5] [Tab6] ... ▶ [⛶ Full] │ │
│ │ ──────────────────────────────────────────────────────────────────│ │
│ │ │ │
│ │ <iframe srcdoc={assembled HTML} sandbox="allow-scripts" /> │ │
│ │ │ │
│ └──────────────────────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────────────────────┘
Hub Features
- Folder tree (left panel, collapsible)
- Top-level folders: shared (all users see them)
- Sub-folders: private by default (🔒 icon), "Share" context-menu action to make public
- Create / Rename / Delete (with confirmation)
- Expansion state persisted in localStorage
- Report list (right panel, filtered by selected folder)
- Columns: Name, Tab count, Created By, Date — all sortable
- Search/filter bar (filters by name)
- Click row → expand viewer panel below
- Delete report (confirmation dialog)
- Tab bar (inside viewer)
- Horizontal scrollable with ◀ ▶ arrow buttons when tabs overflow
- Active tab highlighted
- "➕ Add Tab" button — file picker → uploads new tab to existing report
- Iframe viewer
sandbox="allow-scripts allow-same-origin"attribute- Full-screen toggle button ⛶
- Loading spinner while assembling chunks
Configuration / Admin Page
Sections
Manual Upload
- Fields: Report Name, Folder (dropdown), Tab Name, HTML file (multi-select)
- Progress bar per file
- Success / error toast notifications
Manage Reports
- Full table across all folders
- Actions: Rename, Move to folder, Delete
Manage Folders
- Create / Rename / Delete / Toggle shared
Storage Stats
- Total reports count
- Total chunk documents
- Estimated storage used (KB/MB)
Pipeline Task Logic
- Scan
htmlDirectoryfor all*.htmlfiles - For each file:
- Read as UTF-8 string
- Base64-encode full content
- Split into 800 000-char chunks
- POST each chunk as separate extension data document → collect
chunkIds - Build
ReportTabobject
- Check if
ReportEntrywithname == reportNameinfolderPathalready exists- Exists → append new tab(s), update
updatedAt - New → create
ReportEntrywith new UUID
- Exists → append new tab(s), update
- Save
ReportEntrydocument - Log:
✅ Published "${reportName}" (${tabs} tab(s)) — view at {hubUrl}
Project Structure
HtmlReportViewer/
├── vss-extension.json
├── package.json
├── tsconfig.json
├── webpack.config.js
├── bump-version.js
├── images/
│ └── logo.png
├── src/
│ ├── Hub/
│ │ ├── Hub.html
│ │ ├── Hub.scss
│ │ └── Hub.tsx
│ ├── Config/
│ │ ├── Config.html
│ │ ├── Config.scss
│ │ └── Config.tsx
│ ├── components/
│ │ ├── FolderTree.tsx
│ │ ├── ReportList.tsx
│ │ ├── ReportViewer.tsx
│ │ ├── TabBar.tsx
│ │ └── UploadModal.tsx
│ ├── services/
│ │ ├── StorageService.ts
│ │ └── UploadService.ts
│ └── types/
│ └── index.ts
└── task/
├── task.json
├── package.json
└── index.ts
Tech Stack
- React 16 + TypeScript 5
- azure-devops-extension-sdk ^4
- azure-devops-extension-api ^1
- azure-devops-ui ^2 (Table, Tree, Dialog, Panel, Pill, Spinner)
- Webpack 5 + sass-loader
- tfx-cli for packaging
Required Scopes
["vso.work", "vso.build", "vso.extension.data", "vso.extension.data_write"]
Access Control Rules
Folder Creation
- Admin only — only users with the "Project Administrator" role can create, rename, or delete folders
- Non-admins see the folder tree in read-only mode (no + button)
Folder Permissions
- Visibility (read):
- 🌐 Shared folders: visible to all project members
- 🔒 Private folders: visible only to the creator
- Admins always see all folders regardless of visibility
- Write access (upload/delete reports):
- Per-folder write permission list, managed by admin
- Admin adds/removes users or groups who may upload to that folder
- Pipeline service identity must be granted write access to target folder by admin
- Toggle: Admin can switch any folder between Shared ↔ Private at any time
Configuration Page
- The admin configuration page (
/settings) is restricted to Project Administrators only - If a non-admin navigates to it, they see an "Access Denied" message
- The admin check is done on page load via
SDK.getPageContext()→webAccessLevelor viaIVssIdentityService
Delete Cascade
- Deleting a report is a full cascade delete from the database:
- Delete every chunk document (
hrv-chunkscollection) referenced by every tab in the report - Delete the
ReportEntrydocument (hrv-reportscollection)
- Delete every chunk document (
- A confirmation dialog shows the user exactly what will be removed (report name, tab count, chunk count)
- The old extension left orphaned chunk data — the new extension fully cleans up
Version & Build
| Setting | Value |
|---|---|
| Starting version | 2.0.0 |
| Bump strategy | Patch auto-increment on every npm run build |
| Script | bump-version.js (run via "prebuild" in package.json scripts) |
| Files updated | package.json + vss-extension.json on each build |
Same bump-version.js pattern as test-analysis-v2:
- Reads current version from
package.json - Increments patch:
2.0.0→2.0.1→2.0.2… - Writes back to both
package.jsonandvss-extension.json
Pipeline Migration
Old task: AviHadad/ADOStoragePublisher-build-release-task → writes to its own extension data namespace (not visible to the new hub)
New task: PublishHtmlReport (bundled in AviHadad/Extend-Htmls-Hub) → writes directly to the hub's data namespace
Once a pipeline is updated to use the new task, all new runs write to the correct storage immediately. The hub sees the data without any migration.
Steps to migrate a pipeline
Replace the old task step with the new one:
Old (stop using):
- task: AviHadad.ADOStoragePublisher-build-release-task.publish-html-task.PublishHtml@1
inputs:
reportName: 'My Report'
...
New:
- task: PublishHtmlReport@2
inputs:
reportName: 'My Report'
tabName: 'HTML-Report'
htmlDirectory: '$(Build.ArtifactStagingDirectory)/reports'
folderPath: '/'
env:
SYSTEM_ACCESSTOKEN: $(System.AccessToken)
⚠️ Enable "Allow scripts to access the OAuth token" in the pipeline's Agent Job settings, or the task cannot authenticate to write extension data.
Historical data
Reports published by the old task before migration still sit in the old extension's namespace. Use the "Import from v1" button in Project Settings → Stored Html Configuration to import them into the new format.
The old extension stored its HTML data under a different extension ID:
- Old publisher/ext:
AviHadad / ADOStoragePublisher-build-release-task - Old key:
StoredHtmlList→ array of{ name, storedKey } - Old content: each
storedKey→ base64-encoded[{ PSChildName, value, ... }]
The new extension will NOT automatically see v1 data — it uses its own data namespace.
A "Import from v1" button in the Config page will:
- Read
StoredHtmlListfrom the old extension's data scope - For each entry: fetch the HTML content array by
storedKey - Decode base64 → extract HTML string per
PSChildName - Store as new
ReportEntry+ chunks inhrv-reports/hrv-chunks - Show progress and a summary of what was imported
| Old field | Maps to |
|---|---|
StoredHtml.name |
ReportEntry.name |
StoredHtml.storedKey |
(used to fetch content, then discarded) |
PSChildName |
ReportTab.label |
value (HTML string) |
encoded → ChunkDoc.data |
Old Extension Storage Keys
| Old key | Old format | New equivalent |
|---|---|---|
StoredHtmlList |
StoredHtml[] (name, storedKey) |
hrv-reports collection |
{storedKey} |
[{ PSChildName, value }] base64-JSON |
hrv-chunks + hrv-reports |
| Extension ID | AviHadad/ADOStoragePublisher-build-release-task |
AviHadad/Extend-Htmls-Hub |