添加仪表板小组件

Azure DevOps Services |Azure DevOps Server |Azure DevOps Server 2022 |Azure DevOps Server 2020

小组件作为贡献扩展框架中实现。 单个扩展可以包含多个小组件贡献。 本文介绍如何创建提供一个或多个小组件的扩展。

提示

查看有关使用 Azure DevOps 扩展 SDK 进行扩展开发的最新文档。

提示

如果要启动新的 Azure DevOps 扩展,请先尝试这些维护的示例集合 -- 它们适用于当前产品版本并涵盖新式方案(例如,在拉取请求页上添加选项卡)。

如果某个示例在组织中不起作用,请将其安装到个人或测试组织中,并将扩展清单的目标 ID 和 API 版本与当前文档进行比较。有关参考和 API,请参阅:

先决条件

要求 说明
编程知识 用于小部件开发的 JavaScript、HTML 和 CSS 知识
Azure DevOps 组织 创建组织
文本编辑器 我们将 Visual Studio Code 用于教程
Node.js 最新版本的 Node.js
跨平台 CLI tfx-cli,用于打包扩展
安装使用: npm i -g tfx-cli
项目目录 完成本教程后,主目录的结构为:

|--- README.md
|--- sdk
|--- node_modules
|--- scripts
|--- VSS.SDK.min.js
|--- img
|--- logo.png
|--- scripts
|--- hello-world.html // html page for your widget
|--- vss-extension.json // extension manifest

教程概览

本教程通过三个递进示例教授小部件开发:

部件 焦点 学习内容
第 1 部分:Hello World 创建基本控件 创建显示文本的小组件
第 2 部分:REST API 集成 Azure DevOps API 调用 添加 REST API 功能来提取和显示数据
第 3 部分:窗口小部件配置 用户自定义 实现小组件的配置选项

提示

包含的示例(请参阅上一条注释)显示了一组可以打包和发布的控件,如果希望直接跳转到实际示例。

在开始之前,查看我们提供的基本小组件样式和结构指南。

第 1 部分:Hello World

使用 JavaScript 创建显示“Hello World”的基本控件。 此基础演示了核心小组件开发概念。

带有示例小组件的“概览”仪表板的屏幕截图。

步骤 1:安装客户端 SDK

利用 VSS SDK,小组件可与 Azure DevOps 进行通信。 使用 npm 安装它:

npm install vss-web-extension-sdk

VSS.SDK.min.js 文件从 vss-web-extension-sdk/lib 文件夹复制到文件夹 home/sdk/scripts

有关更多 SDK 文档,请参阅 客户端 SDK GitHub 页

步骤 2:创建 HTML 结构

在项目目录中创建 hello-world.html 。 此文件提供小部件的布局信息和对所需脚本的引用信息。

<!DOCTYPE html>
<html>
    <head>          
        <script src="sdk/scripts/VSS.SDK.min.js"></script>              
    </head>
    <body>
        <div class="widget">
            <h2 class="title"></h2>
        </div>
    </body>
</html>

小组件在 iframe 中运行,因此除 <script><link> 外,大多数 HTML 头元素都会被框架忽略。

步骤 3:添加小组件 JavaScript

若要实现小组件功能,请将此脚本添加到 HTML 文件的 <head> 部分。

<script type="text/javascript">
    VSS.init({                        
        explicitNotifyLoaded: true,
        usePlatformStyles: true
    });

    VSS.require(["AzureDevOps/Dashboards/WidgetHelpers"], function (WidgetHelpers) {
        WidgetHelpers.IncludeWidgetStyles();
        VSS.register("HelloWorldWidget", function () {                
            return {
                load: function (widgetSettings) {
                    var $title = $('h2.title');
                    $title.text('Hello World');

                    return WidgetHelpers.WidgetStatusHelper.Success();
                }
            };
        });
        VSS.notifyLoadSucceeded();
    });
</script>

关键 JavaScript 组件

功能 目的
VSS.init() 初始化小组件与 Azure DevOps 之间的通信
VSS.require() 加载所需的 SDK 库和小部件助手
VSS.register() 使用唯一标识符注册小组件
WidgetHelpers.IncludeWidgetStyles() 应用默认的 Azure DevOps 样式
VSS.notifyLoadSucceeded() 通知框架加载成功完成

重要

VSS.register() 中的小组件名称必须与扩展清单中的 id 匹配(步骤 5)。

步骤 4:添加扩展映像

为你的扩展创建所需的图像:

  • 扩展徽标:98x98 像素图像,命名为 logo.png,位于 img 文件夹中
  • 小组件目录图标CatalogIcon.png 文件夹中名为 img 的 98x98 像素图像
  • 小组件预览preview.png 文件夹中名为 img 的 330x160 像素图像

当用户浏览可用扩展时,这些图像将显示在应用市场和组件目录中。

步骤 5:创建扩展清单

在项目的根目录中创建 vss-extension.json 。 此文件定义扩展的元数据和贡献:

{
    "manifestVersion": 1,
    "id": "azure-devops-extensions-myExtensions",
    "version": "1.0.0",
    "name": "My First Set of Widgets",
    "description": "Samples containing different widgets extending dashboards",
    "publisher": "fabrikam",
    "categories": ["Azure Boards"],
    "targets": [
        {
            "id": "Microsoft.VisualStudio.Services"
        }
    ],
    "icons": {
        "default": "img/logo.png"
    },
    "contributions": [
        {
            "id": "HelloWorldWidget",
            "type": "ms.vss-dashboards-web.widget",
            "targets": [
                "ms.vss-dashboards-web.widget-catalog"
            ],
            "properties": {
                "name": "Hello World Widget",
                "description": "My first widget",
                "catalogIconUrl": "img/CatalogIcon.png",
                "previewImageUrl": "img/preview.png",
                "uri": "hello-world.html",
                "supportedSizes": [
                    {
                        "rowSpan": 1,
                        "columnSpan": 2
                    }
                ],
                "supportedScopes": ["project_team"]
            }
        }
    ],
    "files": [
        {
            "path": "hello-world.html",
            "addressable": true
        },
        {
            "path": "sdk/scripts",
            "addressable": true
        },
        {
            "path": "img",
            "addressable": true
        }
    ]
}

重要

"publisher": "fabrikam" 替换为您的实际发布者名称。 了解如何 创建发布者

基本清单属性

部分 目的
基本信息 扩展名称、版本、说明和发布者
图标 扩展视觉资产的路径
贡献 小组件定义,包括 ID、类型和属性
文件 要包含在扩展包中的所有文件

有关完整的清单文档,请参阅 扩展清单参考

步骤 6:打包和发布扩展

打包扩展并将其发布到 Visual Studio 市场。

安装打包工具

npm i -g tfx-cli

创建扩展包

从项目目录中运行:

tfx extension create --manifest-globs vss-extension.json

此操作将创建包含已打包扩展的 .vsix 文件。

设置发布者

  1. 转到 Visual Studio Marketplace 发布门户
  2. 如果没有发布者,请登录并创建发布者。
  3. 选择唯一的发布者标识符(在清单文件中使用)。
  4. vss-extension.json 更新为发布者名称,而不是“fabrikam”。

上传扩展

  1. 在发布门户中,选择“ 上传新扩展”。
  2. 选择文件 .vsix 并上传它。
  3. 与 Azure DevOps 组织共享扩展。

或者,使用命令行:

tfx extension publish --manifest-globs vss-extension.json --share-with yourOrganization

提示

用于 --rev-version 在更新现有扩展时自动递增版本号。

步骤 7:安装和测试小组件

若要进行测试,请将小组件添加到仪表板:

  1. 转到 Azure DevOps 项目: https://dev.azure.com/{Your_Organization}/{Your_Project}
  2. 转到概述>仪表板
  3. 选择“添加小组件”
  4. 在目录中找到小组件,然后选择“添加”

“Hello World”小组件显示在仪表板上,其中显示配置的文本。

下一步:继续学习第 2 部分 ,了解如何将 Azure DevOps REST API 集成到小组件中。

第 2 部分:使用 Azure DevOps REST API 实现“你好,世界”

将小组件扩展为使用 REST API 与 Azure DevOps 数据进行交互。 此示例演示如何提取查询信息并在小组件中动态显示信息。

在本部分中,使用 工作项跟踪 REST API 检索有关现有查询的信息,并在“Hello World”文本下方显示查询详细信息。

概览仪表板的屏幕截图,其中包含使用 WorkItemTracking 的 REST API 的示例组件。

步骤 1:创建增强的 HTML 文件

创建一个在上一个示例基础上构建的新挂件文件。 复制 hello-world.html 并将其重命名为 hello-world2.html。 项目结构现在包括:

|--- README.md
|--- node_modules
|--- sdk/
    |--- scripts/
        |--- VSS.SDK.min.js
|--- img/
    |--- logo.png
|--- scripts/
|--- hello-world.html               // Part 1 widget
|--- hello-world2.html              // Part 2 widget (new)
|--- vss-extension.json             // Extension manifest

更新小组件 HTML 结构

hello-world2.html进行这些更改:

  1. 添加查询数据的容器:包括用于显示查询信息的新 <div> 元素。
  2. 更新小组件标识符:将小组件名称从HelloWorldWidget更改为HelloWorldWidget2以便唯一标识。
<!DOCTYPE html>
<html>
    <head>
        <script src="sdk/scripts/VSS.SDK.min.js"></script>
        <script type="text/javascript">
            VSS.init({
                explicitNotifyLoaded: true,
                usePlatformStyles: true
            });

            VSS.require(["AzureDevOps/Dashboards/WidgetHelpers"], function (WidgetHelpers) {
                WidgetHelpers.IncludeWidgetStyles();
                VSS.register("HelloWorldWidget2", function () {
                    return {
                        load: function (widgetSettings) {
                            var $title = $('h2.title');
                            $title.text('Hello World');

                            return WidgetHelpers.WidgetStatusHelper.Success();
                        }
                    }
                });
                VSS.notifyLoadSucceeded();
            });
        </script>
    </head>
    <body>
        <div class="widget">
            <h2 class="title"></h2>
            <div id="query-info-container"></div>
        </div>
    </body>
</html>

步骤 2:配置 API 访问权限

在进行 REST API 调用之前,请在扩展清单中配置所需的权限。

添加工作范围

vso.work 范围授予对工作项和查询的只读访问权限。 将此范围添加到vss-extension.json中。

{
    "scopes": [
        "vso.work"
    ]
}

完整清单示例

对于具有其他属性的完整清单,请按如下所示对其进行结构:

{
    "name": "example-widget",
    "publisher": "example-publisher", 
    "version": "1.0.0",
    "scopes": [
        "vso.work"
    ]
}

重要

范围限制:不支持在发布后添加或更改范围。 如果已发布扩展,必须先将其从市场中删除。 转到 Visual Studio Marketplace 发布门户,找到扩展,然后选择“ 删除”。

步骤 3:实现 REST API 集成

Azure DevOps 通过 SDK 提供 JavaScript REST 客户端库。 这些库包装 AJAX 调用并将 API 响应映射到可用对象。

更新小组件 JavaScript

替换 VSS.require 中的 hello-world2.html 调用,以包含工作项跟踪 REST 客户端:

VSS.require(["AzureDevOps/Dashboards/WidgetHelpers", "AzureDevOps/WorkItemTracking/RestClient"], 
    function (WidgetHelpers, WorkItemTrackingRestClient) {
        WidgetHelpers.IncludeWidgetStyles();
        VSS.register("HelloWorldWidget2", function () { 
            var projectId = VSS.getWebContext().project.id;

            var getQueryInfo = function (widgetSettings) {
                // Get a WIT client to make REST calls to Azure DevOps Services
                return WorkItemTrackingRestClient.getClient().getQuery(projectId, "Shared Queries/Feedback")
                    .then(function (query) {
                        // Process query data (implemented in Step 4)

                        return WidgetHelpers.WidgetStatusHelper.Success();
                    }, function (error) {                            
                        return WidgetHelpers.WidgetStatusHelper.Failure(error.message);
                    });
            }

            return {
                load: function (widgetSettings) {
                    // Set your title
                    var $title = $('h2.title');
                    $title.text('Hello World');

                    return getQueryInfo(widgetSettings);
                }
            }
        });
        VSS.notifyLoadSucceeded();
    });

关键实现详细信息

组件 目的
WorkItemTrackingRestClient.getClient() 获取工作项跟踪 REST 客户端的实例
getQuery() 检索承诺中包装的查询信息
WidgetStatusHelper.Failure() 为小组件故障提供一致的错误处理
projectId API 调用所需的当前项目上下文

提示

自定义查询路径:如果在“共享查询”中没有“反馈”查询,请将 "Shared Queries/Feedback" 替换为项目中任何现有查询的路径。

步骤 4:显示 API 响应数据

通过处理 REST API 响应,在小组件中呈现查询信息。

添加查询数据呈现功能

用此实现替换 // Process query data 注释:

// Create a list with query details                                
var $list = $('<ul>');                                
$list.append($('<li>').text("Query ID: " + query.id));
$list.append($('<li>').text("Query Name: " + query.name));
$list.append($('<li>').text("Created By: " + (query.createdBy ? query.createdBy.displayName : "<unknown>")));

// Append the list to the query-info-container
var $container = $('#query-info-container');
$container.empty();
$container.append($list);

该方法 getQuery() 返回一个 Contracts.QueryHierarchyItem 对象,其中包含查询元数据的属性。 本示例显示“Hello World”文本下方的三个关键信息片段。

完整工作示例

最终 hello-world2.html 文件应如下所示:

<!DOCTYPE html>
<html>
<head>    
    <script src="sdk/scripts/VSS.SDK.min.js"></script>
    <script type="text/javascript">
        VSS.init({
            explicitNotifyLoaded: true,
            usePlatformStyles: true
        });

        VSS.require(["AzureDevOps/Dashboards/WidgetHelpers", "AzureDevOps/WorkItemTracking/RestClient"], 
            function (WidgetHelpers, WorkItemTrackingRestClient) {
                WidgetHelpers.IncludeWidgetStyles();
                VSS.register("HelloWorldWidget2", function () {                
                    var projectId = VSS.getWebContext().project.id;

                    var getQueryInfo = function (widgetSettings) {
                        // Get a WIT client to make REST calls to Azure DevOps Services
                        return WorkItemTrackingRestClient.getClient().getQuery(projectId, "Shared Queries/Feedback")
                            .then(function (query) {
                                // Create a list with query details                                
                                var $list = $('<ul>');
                                $list.append($('<li>').text("Query ID: " + query.id));
                                $list.append($('<li>').text("Query Name: " + query.name));
                                $list.append($('<li>').text("Created By: " + (query.createdBy ? query.createdBy.displayName : "<unknown>")));

                                // Append the list to the query-info-container
                                var $container = $('#query-info-container');
                                $container.empty();
                                $container.append($list);

                                // Use the widget helper and return success as Widget Status
                                return WidgetHelpers.WidgetStatusHelper.Success();
                            }, function (error) {
                                // Use the widget helper and return failure as Widget Status
                                return WidgetHelpers.WidgetStatusHelper.Failure(error.message);
                            });
                    }

                    return {
                        load: function (widgetSettings) {
                            // Set your title
                            var $title = $('h2.title');
                            $title.text('Hello World');

                            return getQueryInfo(widgetSettings);
                        }
                    }
                });
            VSS.notifyLoadSucceeded();
        });       
    </script>

</head>
<body>
    <div class="widget">
        <h2 class="title"></h2>
        <div id="query-info-container"></div>
    </div>
</body>
</html>

步骤 5:更新扩展清单

若要使其在小组件目录中可用,请将新小组件添加到扩展清单。

添加第二个小组件贡献

vss-extension.json 更新为包含启用 REST API 的小组件。 将此贡献添加到 contributions 数组:

{
    "contributions": [
        // ...existing HelloWorldWidget contribution...,
        {
            "id": "HelloWorldWidget2",
            "type": "ms.vss-dashboards-web.widget",
            "targets": [
                "ms.vss-dashboards-web.widget-catalog"
            ],
            "properties": {
                "name": "Hello World Widget 2 (with API)",
                "description": "My second widget",
                "previewImageUrl": "img/preview2.png",
                "uri": "hello-world2.html",
                "supportedSizes": [
                    {
                        "rowSpan": 1,
                        "columnSpan": 2
                    }
                ],
                "supportedScopes": ["project_team"]
            }
        }
    ],
    "files": [
        {
            "path": "hello-world.html",
            "addressable": true
        },
        {
            "path": "hello-world2.html",
            "addressable": true
        },
        {
            "path": "sdk/scripts",
            "addressable": true
        },
        {
            "path": "img",
            "addressable": true
        }
    ],
    "scopes": [
        "vso.work"
    ]
}

提示

预览图像:创建 preview2.png 图像(330x160 像素),并将其 img 放置在文件夹中,向用户显示小组件在目录中的外观。

步骤 6:打包、发布和共享

打包、发布和共享你的扩展。 如果已发布扩展,则可以在市场中直接重新打包和更新它。

步骤 7:测试您的 REST API 组件

为了查看 REST API 集成的实际效果,请将新部件添加到您的仪表板中。

  1. 转到 Azure DevOps 项目: https://dev.azure.com/{Your_Organization}/{Your_Project}
  2. 选择概述>仪表板
  3. 选择“添加小组件”
  4. 找到“Hello World 小工具 2(包含 API)”,然后选择 添加

您的增强窗口小部件显示了 "Hello World" 文本以及来自您的 Azure DevOps 项目的实时查询信息。

后续步骤:继续执行 第 3 部分 以添加配置选项,让用户自定义要显示的查询。

第 3 部分:配置 Hello World

通过将用户配置功能添加到小组件,利用第 2 部分。 无需对查询路径进行硬编码,而是创建一个配置界面,让用户选择要显示的查询,并具有实时预览功能。

本部分演示如何创建可配置的小组件,用户可以根据特定需求进行自定义,同时在配置期间提供实时反馈。

基于更改的小组件“概述”仪表板实时预览的屏幕截图。

步骤 1:创建配置文件

小组件配置与小组件本身共享许多相似之处-它们都使用相同的 SDK、HTML 结构和 JavaScript 模式,但在扩展框架中提供不同的用途。

设置项目结构

若要支持小组件配置,请创建两个新文件:

  1. 复制 hello-world2.html 并将其重命名为 hello-world3.html,即可配置的小组件。
  2. 创建名为 configuration.html 的新文件,用于处理配置接口。

项目结构现在包括:

|--- README.md
|--- sdk/    
    |--- node_modules           
    |--- scripts/
        |--- VSS.SDK.min.js       
|--- img/                        
    |--- logo.png                           
|--- scripts/          
|--- configuration.html              // New: Configuration interface
|--- hello-world.html               // Part 1: Basic widget  
|--- hello-world2.html              // Part 2: REST API widget
|--- hello-world3.html              // Part 3: Configurable widget (new)
|--- vss-extension.json             // Extension manifest

创建配置接口

将此 HTML 结构添加到 configuration.html其中,这将创建用于选择查询的下拉列表选择器:

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
    <head>                          
        <script src="sdk/scripts/VSS.SDK.min.js"></script>              
    </head>
    <body>
        <div class="container">
            <fieldset>
                <label class="label">Query: </label>
                <select id="query-path-dropdown" style="margin-top:10px">
                    <option value="" selected disabled hidden>Please select a query</option>
                    <option value="Shared Queries/Feedback">Shared Queries/Feedback</option>
                    <option value="Shared Queries/My Bugs">Shared Queries/My Bugs</option>
                    <option value="Shared Queries/My Tasks">Shared Queries/My Tasks</option>                        
                </select>
            </fieldset>             
        </div>
    </body>
</html>

步骤 2:实现配置 JavaScript

配置 JavaScript 遵循与小组件相同的初始化模式,但实现 IWidgetConfiguration 合同,而不是基本 IWidget 合同。

添加配置逻辑

将此脚本插入到 <head>configuration.html 之间的部分中:

<script type="text/javascript">
    VSS.init({                        
        explicitNotifyLoaded: true,
        usePlatformStyles: true
    });

    VSS.require(["AzureDevOps/Dashboards/WidgetHelpers"], function (WidgetHelpers) {
        VSS.register("HelloWorldWidget.Configuration", function () {   
            var $queryDropdown = $("#query-path-dropdown"); 

            return {
                load: function (widgetSettings, widgetConfigurationContext) {
                    var settings = JSON.parse(widgetSettings.customSettings.data);
                    if (settings && settings.queryPath) {
                         $queryDropdown.val(settings.queryPath);
                     }

                    return WidgetHelpers.WidgetStatusHelper.Success();
                },
                onSave: function() {
                    var customSettings = {
                        data: JSON.stringify({
                                queryPath: $queryDropdown.val()
                            })
                    };
                    return WidgetHelpers.WidgetConfigurationSave.Valid(customSettings); 
                }
            }
        });
        VSS.notifyLoadSucceeded();
    });
</script>

配置协定详细信息

协定 IWidgetConfiguration 需要以下关键功能:

功能 目的 在调用时
load() 使用现有设置初始化配置 UI “配置”对话框打开时
onSave() 序列化用户输入和验证设置 当用户选择“保存”

提示

数据序列化:此示例使用 JSON 序列化设置。 小组件通过 widgetSettings.customSettings.data 访问这些设置,并且必须相应地对这些设置进行反序列化。

步骤 3:启用实时预览功能

实时预览允许用户在修改配置设置时立即查看小组件更改,在保存之前提供即时反馈。

实现更改通知

若要启用实时预览,请在 load 函数中添加此事件处理程序:

$queryDropdown.on("change", function () {
    var customSettings = {
       data: JSON.stringify({
               queryPath: $queryDropdown.val()
           })
    };
    var eventName = WidgetHelpers.WidgetEvent.ConfigurationChange;
    var eventArgs = WidgetHelpers.WidgetEvent.Args(customSettings);
    widgetConfigurationContext.notify(eventName, eventArgs);
});

完成配置文件

最终的 configuration.html 应该是这样的:

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
    <head>                          
        <script src="sdk/scripts/VSS.SDK.min.js"></script>      
        <script type="text/javascript">
            VSS.init({                        
                explicitNotifyLoaded: true,
                usePlatformStyles: true
            });

            VSS.require(["AzureDevOps/Dashboards/WidgetHelpers"], function (WidgetHelpers) {
                VSS.register("HelloWorldWidget.Configuration", function () {   
                    var $queryDropdown = $("#query-path-dropdown");

                    return {
                        load: function (widgetSettings, widgetConfigurationContext) {
                            var settings = JSON.parse(widgetSettings.customSettings.data);
                            if (settings && settings.queryPath) {
                                 $queryDropdown.val(settings.queryPath);
                             }

                             $queryDropdown.on("change", function () {
                                 var customSettings = {data: JSON.stringify({queryPath: $queryDropdown.val()})};
                                 var eventName = WidgetHelpers.WidgetEvent.ConfigurationChange;
                                 var eventArgs = WidgetHelpers.WidgetEvent.Args(customSettings);
                                 widgetConfigurationContext.notify(eventName, eventArgs);
                             });

                            return WidgetHelpers.WidgetStatusHelper.Success();
                        },
                        onSave: function() {
                            var customSettings = {data: JSON.stringify({queryPath: $queryDropdown.val()})};
                            return WidgetHelpers.WidgetConfigurationSave.Valid(customSettings); 
                        }
                    }
                });
                VSS.notifyLoadSucceeded();
            });
        </script>       
    </head>
    <body>
        <div class="container">
            <fieldset>
                <label class="label">Query: </label>
                <select id="query-path-dropdown" style="margin-top:10px">
                    <option value="" selected disabled hidden>Please select a query</option>
                    <option value="Shared Queries/Feedback">Shared Queries/Feedback</option>
                    <option value="Shared Queries/My Bugs">Shared Queries/My Bugs</option>
                    <option value="Shared Queries/My Tasks">Shared Queries/My Tasks</option>                        
                </select>
            </fieldset>     
        </div>
    </body>
</html>

重要

“启用保存”按钮:框架至少需要一个配置更改通知才能启用 “保存 ”按钮。 更改事件处理程序可确保当用户选择选项时执行此作。

步骤 4:将小组件设为可配置

将第 2 部分中的小组件转换为使用配置数据,而不是硬编码值。 此步骤需要实现 IConfigurableWidget 协定。

更新小组件注册

hello-world3.html中进行以下更改:

  1. 更新小组件 ID:从 HelloWorldWidget2 更改为 HelloWorldWidget3
  2. 添加重载函数:实现 IConfigurableWidget 协定。
return {
    load: function (widgetSettings) {
        // Set your title
        var $title = $('h2.title');
        $title.text('Hello World');

        return getQueryInfo(widgetSettings);
    },
    reload: function (widgetSettings) {
        return getQueryInfo(widgetSettings);
    }
}

处理配置数据

更新函数 getQueryInfo 以使用配置设置,而不是硬编码的查询路径:

var settings = JSON.parse(widgetSettings.customSettings.data);
if (!settings || !settings.queryPath) {
    var $container = $('#query-info-container');
    $container.empty();
    $container.text("Please configure a query path to display data.");

    return WidgetHelpers.WidgetStatusHelper.Success();
}

小组件生命周期差异

功能 目的 使用指南
load() 初始小组件呈现和一次性设置 大量操作,资源初始化
reload() 使用新配置更新小组件 轻量级更新,数据刷新

提示

性能优化:对于只需运行一次的昂贵操作,使用load(),而对于配置变更时的快速更新,使用reload()

(可选)添加灯箱以获取详细信息

仪表板小组件的空间有限,因此很难显示全面的信息。 灯箱通过在模态叠加层中显示详细数据,而无需离开仪表板,提供一种优雅的解决方案。

为什么在小组件中使用光盒?

益处 说明
空间效率 保持小组件紧凑,同时提供详细视图
用户体验 显示详细信息时保持仪表板上下文
渐进式披露 在组件中显示摘要数据,详细信息可按需获取
响应式设计 根据不同的屏幕大小和小组件配置进行调整

实现可单击元素

更新查询数据呈现,以包含可单击的元素来触发灯箱:

// Create a list with clickable query details
var $list = $('<ul class="query-summary">');                                
$list.append($('<li>').text("Query ID: " + query.id));
$list.append($('<li>').text("Query Name: " + query.name));
$list.append($('<li>').text("Created By: " + (query.createdBy ? query.createdBy.displayName : "<unknown>"));

// Add a clickable element to open detailed view
var $detailsLink = $('<button class="details-link">View Details</button>');
$detailsLink.on('click', function() {
    showQueryDetails(query);
});

// Append to the container
var $container = $('#query-info-container');
$container.empty();
$container.append($list);
$container.append($detailsLink);

创建灯箱功能

将此光盒实现添加到小组件 JavaScript:

function showQueryDetails(query) {
    // Create lightbox overlay
    var $overlay = $('<div class="lightbox-overlay">');
    var $lightbox = $('<div class="lightbox-content">');
    
    // Add close button
    var $closeBtn = $('<button class="lightbox-close">&times;</button>');
    $closeBtn.on('click', function() {
        $overlay.remove();
    });
    
    // Create detailed content
    var $content = $('<div class="query-details">');
    $content.append($('<h3>').text(query.name || 'Query Details'));
    $content.append($('<p>').html('<strong>ID:</strong> ' + query.id));
    $content.append($('<p>').html('<strong>Path:</strong> ' + query.path));
    $content.append($('<p>').html('<strong>Created:</strong> ' + (query.createdDate ? new Date(query.createdDate).toLocaleDateString() : 'Unknown')));
    $content.append($('<p>').html('<strong>Modified:</strong> ' + (query.lastModifiedDate ? new Date(query.lastModifiedDate).toLocaleDateString() : 'Unknown')));
    $content.append($('<p>').html('<strong>Created By:</strong> ' + (query.createdBy ? query.createdBy.displayName : 'Unknown')));
    $content.append($('<p>').html('<strong>Modified By:</strong> ' + (query.lastModifiedBy ? query.lastModifiedBy.displayName : 'Unknown')));
    
    if (query.queryType) {
        $content.append($('<p>').html('<strong>Type:</strong> ' + query.queryType));
    }
    
    // Assemble lightbox
    $lightbox.append($closeBtn);
    $lightbox.append($content);
    $overlay.append($lightbox);
    
    // Add to document and show
    $('body').append($overlay);
    
    // Close on overlay click
    $overlay.on('click', function(e) {
        if (e.target === $overlay[0]) {
            $overlay.remove();
        }
    });
    
    // Close on Escape key
    $(document).on('keydown.lightbox', function(e) {
        if (e.keyCode === 27) { // Escape key
            $overlay.remove();
            $(document).off('keydown.lightbox');
        }
    });
}

添加灯箱样式

在小组件 HTML <head> 部分中包含光盒的 CSS 样式:

<style>
.query-summary {
    list-style: none;
    padding: 0;
    margin: 10px 0;
}

.query-summary li {
    padding: 2px 0;
    font-size: 12px;
}

.details-link {
    background: #0078d4;
    color: white;
    border: none;
    padding: 4px 8px;
    font-size: 11px;
    cursor: pointer;
    border-radius: 2px;
    margin-top: 8px;
}

.details-link:hover {
    background: #106ebe;
}

.lightbox-overlay {
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background: rgba(0, 0, 0, 0.7);
    z-index: 10000;
    display: flex;
    align-items: center;
    justify-content: center;
}

.lightbox-content {
    background: white;
    border-radius: 4px;
    padding: 20px;
    max-width: 500px;
    max-height: 80vh;
    overflow-y: auto;
    position: relative;
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}

.lightbox-close {
    position: absolute;
    top: 10px;
    right: 15px;
    background: none;
    border: none;
    font-size: 24px;
    cursor: pointer;
    color: #666;
    line-height: 1;
}

.lightbox-close:hover {
    color: #000;
}

.query-details h3 {
    margin-top: 0;
    color: #323130;
}

.query-details p {
    margin: 8px 0;
    font-size: 14px;
    line-height: 1.4;
}
</style>

增强的小组件实现

具有光盒功能的全面增强小组件:

<!DOCTYPE html>
<html>
<head>    
    <script src="sdk/scripts/VSS.SDK.min.js"></script>
    <style>
        /* Lightbox styles from above */
        .query-summary {
            list-style: none;
            padding: 0;
            margin: 10px 0;
        }
        
        .query-summary li {
            padding: 2px 0;
            font-size: 12px;
        }
        
        .details-link {
            background: #0078d4;
            color: white;
            border: none;
            padding: 4px 8px;
            font-size: 11px;
            cursor: pointer;
            border-radius: 2px;
            margin-top: 8px;
        }
        
        .details-link:hover {
            background: #106ebe;
        }
        
        .lightbox-overlay {
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background: rgba(0, 0, 0, 0.7);
            z-index: 10000;
            display: flex;
            align-items: center;
            justify-content: center;
        }
        
        .lightbox-content {
            background: white;
            border-radius: 4px;
            padding: 20px;
            max-width: 500px;
            max-height: 80vh;
            overflow-y: auto;
            position: relative;
            box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
        }
        
        .lightbox-close {
            position: absolute;
            top: 10px;
            right: 15px;
            background: none;
            border: none;
            font-size: 24px;
            cursor: pointer;
            color: #666;
            line-height: 1;
        }
        
        .lightbox-close:hover {
            color: #000;
        }
        
        .query-details h3 {
            margin-top: 0;
            color: #323130;
        }
        
        .query-details p {
            margin: 8px 0;
            font-size: 14px;
            line-height: 1.4;
        }
    </style>
    <script type="text/javascript">
        VSS.init({
            explicitNotifyLoaded: true,
            usePlatformStyles: true
        });

        VSS.require(["AzureDevOps/Dashboards/WidgetHelpers", "AzureDevOps/WorkItemTracking/RestClient"], 
            function (WidgetHelpers, WorkItemTrackingRestClient) {
                WidgetHelpers.IncludeWidgetStyles();
                
                function showQueryDetails(query) {
                    // Lightbox implementation from above
                }
                
                VSS.register("HelloWorldWidget2", function () {                
                    var projectId = VSS.getWebContext().project.id;

                    var getQueryInfo = function (widgetSettings) {
                        return WorkItemTrackingRestClient.getClient().getQuery(projectId, "Shared Queries/Feedback")
                            .then(function (query) {
                                // Enhanced display with lightbox trigger
                                var $list = $('<ul class="query-summary">');                                
                                $list.append($('<li>').text("Query ID: " + query.id));
                                $list.append($('<li>').text("Query Name: " + query.name));
                                $list.append($('<li>').text("Created By: " + (query.createdBy ? query.createdBy.displayName : "<unknown>")));

                                var $detailsLink = $('<button class="details-link">View Details</button>');
                                $detailsLink.on('click', function() {
                                    showQueryDetails(query);
                                });

                                var $container = $('#query-info-container');
                                $container.empty();
                                $container.append($list);
                                $container.append($detailsLink);

                                return WidgetHelpers.WidgetStatusHelper.Success();
                            }, function (error) {
                                return WidgetHelpers.WidgetStatusHelper.Failure(error.message);
                            });
                    }

                    return {
                        load: function (widgetSettings) {
                            // Set your title
                            var $title = $('h2.title');
                            $title.text('Hello World');

                            return getQueryInfo(widgetSettings);
                        }
                    }
                });
            VSS.notifyLoadSucceeded();
        });       
    </script>
</head>
<body>
    <div class="widget">
        <h2 class="title"></h2>
        <div id="query-info-container"></div>
    </div>
</body>
</html>

辅助功能注意事项:确保灯箱易于键盘访问,并为屏幕阅读器提供适当的标签。 使用 Azure DevOps 的内置辅助功能进行测试。

重要

性能:灯箱应快速加载。 考虑仅在光盒打开时才延迟加载详细数据,而不是提前提取所有数据。

步骤 5:配置扩展清单

在扩展清单中注册可配置组件及其配置界面。

添加小组件和配置贡献

更新 vss-extension.json 以包括两个新贡献:

{
    "contributions": [
        {
             "id": "HelloWorldWidget3",
             "type": "ms.vss-dashboards-web.widget",
             "targets": [
                 "ms.vss-dashboards-web.widget-catalog",  
                 "fabrikam.azuredevops-extensions-myExtensions.HelloWorldWidget.Configuration"
             ],
             "properties": {
                 "name": "Hello World Widget 3 (with config)",
                 "description": "My third widget",
                 "previewImageUrl": "img/preview3.png",                       
                 "uri": "hello-world3.html",
                 "supportedSizes": [
                    {
                        "rowSpan": 1,
                        "columnSpan": 2
                    },
                    {
                        "rowSpan": 2,
                        "columnSpan": 2
                    }
                 ],
                 "supportedScopes": ["project_team"]
             }
         },
         {
             "id": "HelloWorldWidget.Configuration",
             "type": "ms.vss-dashboards-web.widget-configuration",
             "targets": [ "ms.vss-dashboards-web.widget-configuration" ],
             "properties": {
                 "name": "HelloWorldWidget Configuration",
                 "description": "Configures HelloWorldWidget",
                 "uri": "configuration.html"
             }
         }
    ],
    "files": [
        {
            "path": "hello-world.html", "addressable": true
        },
        {
            "path": "hello-world2.html", "addressable": true
        },
        {
            "path": "hello-world3.html", "addressable": true
        },
        {
            "path": "configuration.html", "addressable": true
        },
        {
            "path": "sdk/scripts", "addressable": true
        },
        {
            "path": "img", "addressable": true
        }
    ]
}

配置贡献要求

属性 目的 所需的值
type 将贡献识别为小组件配置 ms.vss-dashboards-web.widget-configuration
targets 显示配置的位置 ms.vss-dashboards-web.widget-configuration
uri 配置 HTML 文件的路径 配置文件路径

小组件定位模式

对于可配置的小组件,targets 数组必须包含配置引用:

<publisher>.<extension-id>.<configuration-id>

警告

配置按钮可见性:如果小组件未正确定位其配置贡献,则不会显示 “配置 ”按钮。 验证发布者和扩展名称是否与清单完全匹配。

步骤 6:打包、发布和共享

使用配置功能部署增强型扩展。

如果是第一个发布,请按照 步骤 6:打包、发布和共享。 对于现有扩展,请直接在市场中重新打包和更新。

步骤 7:测试可配置小组件

通过添加和配置微件来体验完整的配置流程。

将小组件添加到仪表板

  1. 转到 https://dev.azure.com/{Your_Organization}/{Your_Project}
  2. 转到概述>仪表板
  3. 选择“添加小组件”
  4. 找到“Hello World 组件 3(带配置)”,然后选择添加

因为小组件需要设置,所以此时会显示配置提示:

概览仪表板的屏幕截图,其中包含一个来自目录的示例组件。

配置小组件

通过任一方法访问配置:

  • 小组件菜单:将鼠标悬停在小组件上,选择省略号 (?),然后选择 “配置”
  • 仪表板编辑模式:在仪表板上选择编辑,然后在微件上选择配置按钮

此时会打开配置面板,并在中心内提供实时预览。 从下拉列表中选择一个查询以查看即时更新,然后选择“ 保存” 以应用更改。

步骤 8:添加高级配置选项

使用更多内置配置功能(如自定义名称和大小)扩展小组件。

启用名称和大小配置选项

Azure DevOps 提供两个现装的可配置功能:

功能 / 特点 清单属性 目的
自定义名称 isNameConfigurable: true 用户可以替代默认小组件名称
多种尺寸 多个 supportedSizes 条目 用户可以调整小组件的大小

增强的清单示例

{
    "contributions": [
        {
             "id": "HelloWorldWidget3",
             "type": "ms.vss-dashboards-web.widget",
             "targets": [
                 "ms.vss-dashboards-web.widget-catalog",  
                 "fabrikam.azuredevops-extensions-myExtensions.HelloWorldWidget.Configuration"
             ],
             "properties": {
                 "name": "Hello World Widget 3 (with config)",
                 "description": "My third widget",
                 "previewImageUrl": "img/preview3.png",                       
                 "uri": "hello-world3.html",
                 "isNameConfigurable": true,
                 "supportedSizes": [
                    {
                        "rowSpan": 1,
                        "columnSpan": 2
                    },
                    {
                        "rowSpan": 2,
                        "columnSpan": 2
                    }
                 ],
                 "supportedScopes": ["project_team"]
             }
         }
    ]
}

显示配置的名称

若要显示自定义小组件名称,请更新小组件以使用 widgetSettings.name

return {
    load: function (widgetSettings) {
        // Display configured name instead of hard-coded text
        var $title = $('h2.title');
        $title.text(widgetSettings.name);

        return getQueryInfo(widgetSettings);
    },
    reload: function (widgetSettings) {
        // Update name during configuration changes
        var $title = $('h2.title');
        $title.text(widgetSettings.name);

        return getQueryInfo(widgetSettings);
    }
}

更新扩展后,可以配置小组件名称和大小:

显示了可配置小组件名称和大小的位置的屏幕截图。

重新打包更新 扩展以启用这些高级配置选项。

祝贺! 你创建了一个完整的可配置 Azure DevOps 仪表板小组件,其中包含实时预览功能和用户自定义选项。