添加仪表板小组件

Azure DevOps Services | Azure DevOps Server 2022 - Azure DevOps Server 2019

仪表板上的小组件在扩展框架中作为贡献实现。 单个扩展可以有多个贡献。 了解如何创建包含多个小组件作为贡献的扩展。

本文分为三个部分,每个部分都基于上一部分。 从一个简单的小组件开始,最后创建一个综合的小组件。

提示

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

先决条件

  • 开发小组件需要了解一些 JavaScript、HTML 和 CSS 的知识

  • Azure DevOps 中的组织创建一个组织

  • 文本编辑器。 对于许多教程,我们使用 Visual Studio Code。

  • 最新版本的 Node.js

  • 适用于 Azure DevOps 的跨平台 CLI (tfx-cli) 打包扩展。

    • 可使用 npm(通过运行 npm i -g tfx-cli 安装 Node.js 的一个组件)来安装 tfx-cli
  • 项目的主目录。 在整个教程中,此目录被称为home。完成本文中的步骤后,它应具有以下结构:

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

本教程的内容

注意

如果您赶时间,且想要立即获取代码,可以下载 Azure DevOps 的示例扩展。 下载后,转到 widgets 文件夹,然后直接按照 步骤 6步骤 7 来发布示例扩展,该扩展包含三种复杂程度不同的示例小组件。

开始使用我们的一些现成的小组件基本样式,以及一些关于小组件结构的指导。

第 1 部分:Hello World

第 1 部分展示了使用 JavaScript 打印Hello World的组件。

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

步骤 1:获取客户端 SDK

核心 SDK 脚本 VSS.SDK.min.js使 Web 扩展能够与主机 Azure DevOps 框架通信。 该脚本执行初始化、通知扩展加载或获取当前页上下文等操作。

若要检索 SDK,请使用 npm install 以下命令:

npm install vss-web-extension-sdk

找到位于 vss-web-extension-sdk/lib 文件夹中的客户端 SDK VSS.SDK.min.js 文件,然后将其放在 home/sdk/scripts 文件夹中。

有关详细信息,请参阅客户端 SDK GitHub 页

步骤 2:设置 HTML 页面

创建名为 hello-world.html 的文件。 HTML 页面是将布局组合在一起的粘附,包含对 CSS 和 JavaScript 的引用。 你可以将此文件命名为任何内容。 如果使用不同的文件名,请使用你所用的文件名来更新所有对 hello-world 的引用。

小组件基于 HTML,托管在 iframe 中。 将以下 HTML 复制到 hello-world.html 文件中。 我们将添加对 VSS.SDK.min.js 文件的必需引用,并包含一个 h2 元素,该元素在后续步骤中使用字符串 Hello World 进行更新。

<!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>

即使使用的是 HTML 文件,框架也会忽略除脚本和链接以外的大多数 HTML 头元素。

步骤 3:更新 JavaScript

我们使用 JavaScript 在小组件中呈现内容。 在本文中,我们将所有 JavaScript 代码封装在 HTML 文件中的 <script> 元素内。 可以选择将此代码包含在单独的 JavaScript 文件中,并在 HTML 文件中引用它。

代码呈现内容。 此 JavaScript 代码还初始化了 VSS 软件开发工具包,将小组件的代码映射到小组件名称,并通知扩展框架有关小组件成功或失败的情况。

在这种情况下,以下代码会在小组件中打印 Hello World。 将此 script 元素添加在 HTML 的 head 部分。

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

    VSS.require("TFS/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>

  • VSS.init 初始化承载小组件的 iframe 与主机帧之间的握手。

  • 传入 explicitNotifyLoaded: true,以便小组件在完成加载时显式通知主机。 此控件允许我们在确保加载依赖模块后通知加载完成。 使用 usePlatformStyles: true,这样小组件就可以使用 Azure DevOps 的 HTML 元素核心样式,如 bodydiv。 如果不想让小组件使用这些样式,请传入 usePlatformStyles: false

  • VSS.require 用于加载所需的 VSS 脚本库。 对此方法的调用会自动加载常规库,例如 JQueryJQueryUI。 在本例中,我们依赖于 WidgetHelpers 库,该库用于将小组件状态传达给小组件框架。 因此,请将相应的模块名称 TFS/Dashboards/WidgetHelpers 和回调传递给 VSS.require。 加载模块后,将调用回调。 回调包含小组件所需的其余 JavaScript 代码。 回调结束时,调用 VSS.notifyLoadSucceeded 以通知加载完成。

  • WidgetHelpers.IncludeWidgetStyles 包括一个样式表,其中包含一些 基本 CSS 来帮助你入门。 若要使用这些样式,请将内容放在 class 为 widget 的 HTML 元素中。

  • VSS.register 用于在 JavaScript 中映射函数,该函数在扩展的不同贡献之间唯一标识小组件。 该名称应与标识你的贡献的id相匹配,如步骤 5中所述。 对于小组件,传递给 VSS.register 的函数应返回一个满足 IWidget 协定的对象。 例如,返回的对象应该有一个 load 属性,其值是另一个具有呈现小组件核心逻辑的函数。 在本例中,需要将元素的文本 h2 更新为 Hello World。 当小组件框架实例化小组件时,就会调用此函数。 我们使用 WidgetStatusHelper 来自 WidgetHelpers 的 将 返回 WidgetStatus 为成功。

    警告

    如果用于注册小组件的名称与清单中贡献的 ID 不匹配,则小组件会意外运行。

用户安装扩展后,徽标将显示在应用市场和小部件目录中。

需要 98x98 像素目录图标。 选择一个图像,将其命名为logo.png,并将其放在img文件夹中。 只要下一步中的扩展名清单更新为所用的名称,你就可以为映像命名。

步骤 5:创建扩展清单

每个 扩展都必须有一个扩展清单文件。 使用以下内容在home目录中创建一个调用vss-extension.json的 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
        }
    ]
}

该文件 vss-extension.json 应始终位于主目录的根目录中。 对于所有其他文件,你可以将它们放在文件夹中任何你想要的结构中,只需确保在 HTML 文件和 vss-extension.json 清单中适当地更新引用即可。

有关所需属性的详细信息,请参阅 扩展清单参考

注意

publisher 更改为发布者名称。 若要创建发布功能,请参阅 包/发布/安装

图标

icons 节指定了扩展图标在清单中的路径。

贡献

每个贡献项都定义 属性

  • 用于标识你的贡献的 id。 此 ID 在扩展中应是唯一的。 此 ID 应与 步骤 3 中用于注册小组件的名称匹配。

  • 贡献的 type。 对于所有小组件,类型应为 ms.vss-dashboards-web.widget

  • 贡献所属的 targets 数组。 对于所有小组件,目标应为 [ms.vss-dashboards-web.widget-catalog]

  • properties这些对象包括贡献类型的属性。 对于小组件,以下属性是必需的:

    属性 说明
    name 要显示在小组件目录中的小组件的名称。
    description 要显示在小组件目录中的小组件的说明。
    catalogIconUrl 步骤 4 中添加的要在小组件目录中显示的目录图标的相对路径。 图像应为 98x98 像素。 如果使用了不同的文件夹结构或其他文件名,请在此处指定相应的相对路径。
    previewImageUrl 步骤 4 中添加的预览图片的相对路径,以显示在小组件目录中。 图像应为 330x160 像素。 如果使用了不同的文件夹结构或其他文件名,请在此处指定相应的相对路径。
    uri 在步骤 1 中添加的 HTML 文件的相对路径。 如果使用了不同的文件夹结构或其他文件名,请在此处指定相应的相对路径。
    supportedSizes 小组件支持的大小数组。 当小组件支持多个大小时,数组中的第一个大小是小组件的默认大小。 widget size为仪表板网格中小组件占用的行和列指定 。 一行/列对应于 160 px。 任何大于 1x1 的尺寸都会多出 10 px,表示小组件之间的装订线。 例如,3x2 小组件宽 160*3+10*2160*2+10*1 高。 支持的最大大小为 4x4。
    supportedScopes 目前只支持团队仪表板。 该值必须为 project_team。 将来的更新可能包括更多仪表板范围选项。

若要了解有关贡献点的详细信息,请参阅 扩展点

文件

Stanza files 指出要包含在包中的文件:HTML 页面、脚本、SDK 脚本和徽标。 true将 设置为 addressable ,除非包含不需要 URL 寻址的其他文件。

注意

有关 扩展清单文件的详细信息(例如其属性及其用途),请查看 扩展清单参考

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

编写扩展后,进入市场的下一步是将所有文件打包在一起。 所有扩展都打包为 VSIX 2.0 兼容的 .vsix 文件。 Microsoft提供用于打包扩展的跨平台命令行接口(CLI)。

获取打包工具

可以使用 npmNode.js 的一个组件)在命令行中安装或更新 tfx-cli。

npm i -g tfx-cli

打包扩展

使用 tfx-cli 后,将扩展名打包到 .vsix 文件中是毫不费力的。 转到扩展的主目录并运行以下命令。

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

注意

每次更新时,都必须递增扩展/集成版本。
更新现有扩展时,要么更新清单中的版本,要么使用 --rev-version 命令行开关。 这会递增你的扩展的补丁版本号,并将新版本保存到清单中。

在 .vsix 文件中打包扩展后,即可将扩展发布到市场。

为扩展创建发布者

所有扩展(包括 Microsoft 的扩展)都标识为由发布者提供。 如果您尚未成为现有发布商的成员,请创建一个。

  1. 登录到 Visual Studio Marketplace 发布门户

  2. 如果你还不是现有发布者的成员,则必须创建一个发布者。 如果您已有发布者,请滚动到相关网站下并选择发布扩展

    • 为发布者指定标识符,例如: mycompany-myteam
      • 标识符用作扩展清单文件中 publisher 属性的值。
    • 为发布者指定显示名称,例如: 我的团队
  3. 查看“市场发布者协议”,然后选择“创建”

现在,你的发布者已定义。 在将来的版本中,可以授予查看和管理发布者扩展的权限。

在通用发布者下发布扩展简化了团队和组织的过程,从而提供了更安全的方法。 此方法无需在多个用户之间分配一组凭据,从而提高安全性。

更新示例中的 vss-extension.json 清单文件,将虚拟发布者 ID fabrikam 替换为发布者 ID。

发布和共享扩展

现在,你现在可以将扩展上传到市场。

选择“上传新扩展名”,转到打包的 .vsix 文件,然后选择“上传”

还可以使用 tfx extension publish 命令(而不是一个步骤打包和发布扩展) tfx extension create 通过命令行上传扩展。 在发布后,您可以选择使用 --share-with 与一个或多个帐户共享您的扩展。 还需要个人访问令牌。

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

步骤 7:从目录中添加小组件

  1. 登录到你的项目 http://dev.azure.com/{Your_Organization}/{Your_Project}

  2. 选择概述>仪表板

  3. 选择“添加小组件”

  4. 突出显示小组件,然后选择“ 添加”。

    小组件将显示在仪表板上。

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

小组件可以调用 Azure DevOps 中的任何 REST API 来与 Azure DevOps 资源交互。

在以下示例中,我们使用 REST API for WorkItemTracking 提取有关现有查询的信息,并在 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               // html page to be used for your widget
|--- hello-world2.html              // renamed copy of hello-world.html
|--- vss-extension.json             // extension's manifest

要保存查询信息,请在 h2 下添加一个新的 div 元素。 在调用 VSS.register的行中,将 小组件的名称从 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("TFS/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:访问 Azure DevOps 资源

若要启用对 Azure DevOps 资源的访问,需要在扩展清单中指定 范围 。 我们将范围 vso.work 添加到清单。 此范围指示小组件需要对查询和工作项进行只读访问。 若要查看所有可用的范围,请参阅 “范围”。

在扩展清单末尾添加以下代码:

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

若要包含其他属性,应显式列出它们,例如:

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

警告

目前不支持在发布扩展后添加或更改范围。 如果已上传扩展,请将其从市场中删除。 转到 Visual Studio Marketplace 发布门户,右键单击扩展,然后选择“ 删除”。

步骤 3:进行 REST API 调用

可通过 SDK 访问许多客户端库,以在 Azure DevOps 中进行 REST API 调用。 这些库称为 REST 客户端,是针对所有可用服务器端终结点的 Ajax 调用周围的 JavaScript 包装器。 可以使用这些客户端提供的方法,而不是自行编写 Ajax 调用。 这些方法将 API 响应映射到代码可以使用的对象。

在此步骤中,更新 VSS.require 调用以加载 AzureDevOps/WorkItemTracking/RestClient,后者提供 WorkItemTracking REST 客户端。 可以使用此 REST 客户端获取有关名为Feedback的查询在文件夹Shared Queries中的信息。

在传递给 VSS.register的函数内,创建一个用于保存当前项目 ID 的变量。 需要此变量才能提取查询。 另外,创建一种新方法 getQueryInfo 来使用 REST 客户端。 然后,从加载方法调用此方法。

getClient该方法提供所需的 REST 客户端实例。 方法 getQuery 返回查询,该查询被包装在一个 promise 中。

更新后的VSS.require如下所示:

VSS.require(["AzureDevOps/Dashboards/WidgetHelpers", "AzureDevOps/WorkItemTracking/RestClient"], 
    function (WidgetHelpers, TFS_Wit_WebApi) {
        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 TFS_Wit_WebApi.getClient().getQuery(projectId, "Shared Queries/Feedback")
                    .then(function (query) {
                        // Do something with the query

                        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();
    });

请注意,使用 中的 WidgetStatusHelperFailure 方法。 通过它,可以向小组件框架表明发生了错误,并利用为所有小组件提供的标准错误体验。

如果在Shared Queries文件夹下没有Feedback查询,请将代码中的Shared Queries\Feedback替换为项目中实际存在的查询路径。

步骤 4:显示响应

最后一步是在小组件内呈现查询信息。 函数 getQuery 在 promise 中返回类型的 Contracts.QueryHierarchyItem 对象。 在此示例中,将在 Hello World 文本下显示查询 ID、查询名称和查询创建者的名称。

使用以下代码替换 // Do something with the 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);

你的最终 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, TFS_Wit_WebApi) {
                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 TFS_Wit_WebApi.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:更新扩展清单

在此步骤中,我们将更新扩展清单,以包含第二个小组件的条目。 向 属性中的 contributions 数组添加新贡献,并将新文件 hello-world2.html 添加到 files 属性中的数组。 第二个小组件需要另一个预览图像。 preview2.png将此名称命名为 ,并将其放在 img 文件夹中。

{
    ...,
    "contributions": [
        ...,
        {
            "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"
    ]
}

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

打包、发布和共享你的扩展。 如果已发布扩展,可以重新打包扩展并将其直接更新到市场。

步骤 7:从目录中添加小组件

现在,转到团队仪表板。https:\//dev.azure.com/{Your_Organization}/{Your_Project} 如果此页面已打开,请刷新它。 将鼠标悬停在 “编辑” 上,然后选择“ 添加”。 此时会打开小组件目录,可在其中找到已安装的小组件。 要将其添加到仪表板,请选择小组件并选择“添加”

第 3 部分:配置 Hello World

本指南 的第 2 部分 介绍了如何创建显示硬编码查询的查询信息的小组件。 在此部分中,你将添加配置查询的功能,以取代硬编码的查询。 在配置模式下,用户可以根据更改查看小组件的实时预览。 当用户选择“保存”时,这些更改将保存到仪表板上的组件中。

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

步骤 1:添加 HTML 文件

小组件的实现和小小组件的配置有很多相似之处。 两者在扩展框架中作为贡献实现。 两者都使用相同的 SDK 文件 VSS.SDK.min.js。 两者都基于 HTML、JavaScript 和 CSS。

复制上一示例中的文件 html-world2.html ,并将副本重命名为 hello-world3.html。 添加另一个名为 的 configuration.htmlHTML 文件。

文件夹现在如以下示例所示:

|--- README.md
|--- sdk    
    |--- node_modules           
    |--- scripts
        |--- VSS.SDK.min.js       
|--- img                        
    |--- logo.png                           
|--- scripts          
|--- configuration.html                          
|--- hello-world.html               // html page to be used for your widget  
|--- hello-world2.html              // renamed copy of hello-world.html
|--- hello-world3.html              // renamed copy of hello-world2.html
|--- vss-extension.json             // extension's manifest

configuration.html 中添加以下 HTML 代码。 基本上,你需要为 VSS.SDK.min.js 文件添加强制引用,并为下拉列表添加 select 元素,以便从预设列表中选择查询。

    <!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 在小组件配置中呈现内容,就像本指南第 1 部分 的步骤 3 中针对小组件所做的那样。 此 JavaScript 代码呈现内容、初始化 VSS SDK、将小组件配置的代码映射到配置名称,并将配置设置传递给框架。 在本例中,以下代码会加载小组件配置。

打开文件 configuration.html,并将下面的 <script> 元素添加到 <head> 中。

<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>
  • VSS.initVSS.requireVSS.register 所起的作用与第 1 部分中所描述的小组件相同。 唯一的区别在于,对于组件配置,传递给 VSS.register 的函数应返回满足 IWidgetConfiguration 合约的对象。

  • 协定 loadIWidgetConfiguration 属性应具有函数作为其值。 此函数包含一组用于呈现小组件配置的步骤。 在本例中,它使用现有设置更新下拉列表元素的选定值(如果有)。 当框架实例化你的 widget configuration

  • 协定 onSaveIWidgetConfiguration 属性应具有函数作为其值。 当用户在配置窗格中选择 “保存 ”时,框架将调用此函数。 如果用户输入已准备好保存,则将其序列化为字符串,形成 custom settings 对象,并用于 WidgetConfigurationSave.Valid() 保存用户输入。

在本指南中,我们使用 JSON 将用户输入序列化为字符串。 可以选择任何其他方法将用户输入序列化为字符串。 小组件可通过 WidgetSettings 对象的 customSettings 属性来访问它。 小组件必须进行反序列化,这将在步骤 4 中介绍。

步骤 3:启用实时预览 - JavaScript

若要在用户从下拉列表中选择查询时启用实时预览更新,请将更改事件处理程序附加到按钮。 此处理程序通知框架配置已更改。 它还传递 customSettings 要用于更新预览的 。 若要通知框架, notify 需要调用 上的 widgetConfigurationContext 方法。 它采用两个EventArgs参数:事件的名称(在本例中为 WidgetHelpers.WidgetEvent.ConfigurationChange),以及事件的对象(借助 customSettingsWidgetEvent.Args helper 方法创建)。

在分配给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:在小组件中实现重新加载 - JavaScript

设置小组件配置以存储用户选择的查询路径。 现在,必须更新小组件中的代码,以使用存储的配置,而不是上一个示例中硬编码的 Shared Queries/Feedback

打开 文件hello-world3.html,并在调用 VSS.register的行中将 小组件的名称从 HelloWorldWidget2 更新为 HelloWorldWidget3 。 此操作允许框架在扩展中唯一标识小组件。

通过VSS.register映射到HelloWorldWidget3的函数当前返回一个满足IWidget契约的对象。 由于小组件现在需要配置,因此需要更新此函数以返回满足协定的对象 IConfigurableWidget 。 为此,请根据以下代码更新返回语句,使其包含一个名为 reload 的属性。 此属性的值是一个函数, getQueryInfo 用于再调用方法一次。 每当用户输入更改以显示实时预览时,框架都会调用此重载方法。 保存配置时也会调用此重载方法。

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 应替换为配置的查询路径,该路径可以从传递给 方法的参数 widgetSettings 中提取。

getQueryInfo 方法的开头添加以下代码,并用 settings.queryPath 替换硬编码的查询路径。

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

    return WidgetHelpers.WidgetStatusHelper.Success();
}

此时,小组件已准备好使用配置的设置进行呈现。

loadreload 属性都有类似的函数。 大多数简单控件就是这种情况。 对于复杂的小组件,有些操作无论配置如何变化,都只需运行一次即可。 或者,可能有一些重量较重的操作不需要多次运行。 此类操作属于与 load 属性相对应的函数的一部分,而不是 reload 属性的一部分。

步骤 5:更新扩展清单

打开 vss-extension.json 文件,在 contributions 属性的数组中加入两个新条目:一个用于 HelloWorldWidget3 小组件,另一个用于其配置。 第三个小组件需要另一个预览图像。 :将该项目命名为preview3.png ,并将其放入 img 文件夹中。 更新属性中的 files 数组,以包含在此示例中添加的两个新 HTML 文件。

{
    ...
    "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
                         }
                     ],
                 "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
            }
        ],
        ...     
}

小组件配置的贡献模式与小组件本身略有不同。 小组件配置的贡献项具有:

  • 用于标识你的贡献的 id。 ID 在扩展中应是唯一的。
  • 贡献的 type。 对于所有小组件配置,它都应为 ms.vss-dashboards-web.widget-configuration
  • 贡献所属的 targets 数组。 对于所有小组件配置,它都只有一个条目:ms.vss-dashboards-web.widget-configuration
  • properties包含一组属性,其中包括用于配置的 HTML 文件的名称、说明和 URI。

若要支持配置,还需要更改小组件贡献。 需要更新小组件的 targets 数组,以包含以下形式的配置 ID:

<publisher>.<id for the extension>.<id for the configuration contribution>

在本例中,示例为:

fabrikam.vsts-extensions-myExtensions.HelloWorldWidget.Configuration

警告

如果可配置小组件的贡献项未按前面所述使用正确的发布者和扩展名称以配置为目标,则不会显示小组件的“配置”按钮。

在本部分结束时,清单文件应包含三个小组件和一个配置。 有关完整的示例清单文件,请参阅 vss-extension.json

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

如果未发布扩展,请参阅 步骤 6:包、发布和共享。 如果已发布扩展,可以重新打包扩展并将其直接更新到市场。

步骤 7:从目录中添加小组件

现在,转到团队仪表板。https://dev.azure.com/{Your_Organization}/{Your_Project} 如果此页面已打开,请刷新它。 将鼠标悬停在 “编辑” 上,然后选择“ 添加”。 此操作应打开小组件目录,在其中找到已安装的小组件。 要将小组件添加到仪表板,请选择小组件并选择“添加”

会出现类似下面的消息,要求对小组件进行配置。

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

可以通过两种方式配置控件。 一个方法是将鼠标悬停在小组件上,选择右上角出现的省略号,然后选择“配置”。 另一种方法是选择右下角的“编辑”按钮,然后选择小组件右上角显示的“配置”按钮。 两者在右侧打开配置体验,并在中心预览小组件。

继续从下拉列表中选择一个查询。 实时预览显示更新的结果。 选择“保存”,小组件将显示更新的结果。

步骤 8:配置更多(可选)

您可以在 configuration.html 位置根据需要添加任意数量的 HTML 表单元素,以实现更多配置。 现成配置功能有两个:小组件名称和小组件大小。

默认情况下,在扩展清单中为小组件提供的名称将存储为曾经添加到仪表板的小组件的每个实例的小组件名称。 可以允许用户进行配置,这样他们就可以在自己的小组件实例中添加任何想要的名称。 若要允许此类配置,请在扩展清单中为小组件添加 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"]
             }
         },
         ...
    ]
}

重新打包更新 扩展。 使用 config) ) 刷新具有此小组件的仪表板 (Hello World小组件 3 (。 打开小组件的配置模式,现在应该可以看到更改小组件名称和大小的选项。

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

从下拉列表中选择不同的大小。 你会看到实时预览被调整大小。 保存更改,仪表板上的小组件也会调整大小。

更改小组件的名称不会导致小组件的任何可见更改,因为我们的示例小组件不会在任何位置显示小组件名称。

修改示例代码以显示小组件名称,而不是硬编码文本 Hello World 。在设置 h2 元素文本的那一行,把硬编码文本 Hello World 替换为 widgetSettings.name。 此操作可确保每次刷新页面时加载小组件时都会显示小组件名称。 由于希望每次配置更改时更新实时预览,因此还应在 reload 代码的一部分添加相同的代码。 中的 hello-world3.html 最终返回语句如下所示:

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

        return getQueryInfo(widgetSettings);
    },
    reload: function (widgetSettings) {
        // Set your title
        var $title = $('h2.title');
        $title.text(widgetSettings.name);

        return getQueryInfo(widgetSettings);
    }
}

重新打包并再次更新扩展。 刷新具有此小组件的仪表板。

在配置模式下,对小组件名称所做的任何更改,立即更新小组件标题。