Skip to content
| Marketplace
Sign in
Azure DevOps>Azure Pipelines>Extend Htmls Hub View Control
Extend Htmls Hub View Control

Extend Htmls Hub View Control

Avi Hadad

|
3 installs
| (0) | Free
HTML Report Viewer — publish, organise and view HTML reports inside Azure DevOps. Supports folders, tabbed reports, pipeline publishing, and admin permissions.
Get it free

HTML Report Viewer — Requirements

Overview

Version 2 of AviHadad.Extend-Htmls-Hub — published under the same Publisher + ID so it updates the installed extension in-place. Users and pipelines require no changes.


Extension Identity

Field Value
Publisher AviHadad
ID Extend-Htmls-Hub ← same as v1, enables in-place update
Name Extend Htmls Hub View Control
Version 2.0.0 (starting point)
Auto-bump Patch version incremented on every npm run build via prebuild script

Three Contribution Points

1. Work Hub — HTML Report Viewer

  • Custom hub group: "HTML Reports" (appears in left project nav)
  • Viewer hub lives inside this group
  • Entry point: dist/Hub/Hub.html

2. Admin Configuration Page

  • Appears in Project Settings
  • Entry point: dist/Config/Config.html

3. Pipeline Build Task — PublishHtmlReport

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

  1. Manual Upload

    • Fields: Report Name, Folder (dropdown), Tab Name, HTML file (multi-select)
    • Progress bar per file
    • Success / error toast notifications
  2. Manage Reports

    • Full table across all folders
    • Actions: Rename, Move to folder, Delete
  3. Manage Folders

    • Create / Rename / Delete / Toggle shared
  4. Storage Stats

    • Total reports count
    • Total chunk documents
    • Estimated storage used (KB/MB)

Pipeline Task Logic

  1. Scan htmlDirectory for all *.html files
  2. 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 ReportTab object
  3. Check if ReportEntry with name == reportName in folderPath already exists
    • Exists → append new tab(s), update updatedAt
    • New → create ReportEntry with new UUID
  4. Save ReportEntry document
  5. 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() → webAccessLevel or via IVssIdentityService

Delete Cascade

  • Deleting a report is a full cascade delete from the database:
    1. Delete every chunk document (hrv-chunks collection) referenced by every tab in the report
    2. Delete the ReportEntry document (hrv-reports collection)
  • 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.json and vss-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:

  1. Read StoredHtmlList from the old extension's data scope
  2. For each entry: fetch the HTML content array by storedKey
  3. Decode base64 → extract HTML string per PSChildName
  4. Store as new ReportEntry + chunks in hrv-reports / hrv-chunks
  5. 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
  • Contact us
  • Jobs
  • Privacy
  • Manage cookies
  • Terms of use
  • Trademarks
© 2026 Microsoft