Compartilhar via


Adicionar um widget de painel

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

Os widgets são implementados como contribuições na estrutura de extensão. Uma única extensão pode incluir várias contribuições de widget. Este artigo mostra como criar uma extensão que fornece um ou mais widgets.

Dica

Confira a nossa documentação mais nova sobre desenvolvimento de extensões usando o Azure DevOps Extension SDK.

Dica

Se você estiver iniciando uma nova extensão do Azure DevOps, experimente essas coleções de exemplo mantidas primeiro— elas funcionam com builds de produtos atuais e abrangem cenários modernos (por exemplo, adicionando guias em páginas de solicitação pull).

Se um exemplo não funcionar em sua organização, instale-o em uma organização pessoal ou de teste e compare as IDs de destino do manifesto de extensão e as versões da API com as documentações atuais. Para referência e APIs, consulte:

Pré-requisitos

Requisito Descrição
Conhecimento de programação Conhecimento de JavaScript, HTML e CSS para desenvolvimento de widget
Organização do Azure DevOps Criar uma organização
Editor de texto Usamos o Visual Studio Code para tutoriais
Node.js Versão mais recente do Node.js
CLI multiplataforma tfx-cli para empacotar extensões
Instale usando: npm i -g tfx-cli
Diretório do projeto Diretório inicial com essa estrutura depois de concluir o tutorial:

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

Visão geral dos tutoriais

Este tutorial ensina o desenvolvimento de widget por meio de três exemplos progressivos:

Parte Foco O que você aprenderá
Parte 1: Olá, Mundo Criação básica de widget Criar um widget que exibe texto
Parte 2: integração da API REST Chamadas à API do Azure DevOps Adicionar funcionalidade da API REST para buscar e exibir dados
Parte 3: Configuração do Widget Personalização do usuário Implementar opções de configuração para o widget

Dica

Se você preferir ir direto para exemplos de trabalho, os exemplos incluídos (consulte a nota anterior) mostram um conjunto de widgets que você pode empacotar e publicar.

Antes de começar, examine os estilos básicos de widget e as diretrizes estruturais que fornecemos.

Parte 1: Olá, Mundo

Crie um widget básico que exibe "Olá, Mundo" usando JavaScript. Esta fundação demonstra os principais conceitos de desenvolvimento de widget.

Captura de tela do painel visão geral com um widget de exemplo.

Etapa 1: Instalar o SDK do cliente

O SDK do VSS permite que seu widget se comunique com o Azure DevOps. Instale-o usando o npm:

npm install vss-web-extension-sdk

Copie o arquivo VSS.SDK.min.js de vss-web-extension-sdk/lib para a sua pasta home/sdk/scripts.

Para obter mais documentação do SDK, consulte a página do GitHub do SDK do Cliente.

Etapa 2: Criar a estrutura HTML

Crie hello-world.html no diretório do projeto. Esse arquivo fornece o layout do widget e as referências aos scripts necessários.

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

Os widgets são executados em iframes, portanto, a maioria dos elementos de cabeçalho HTML exceto <script> e <link> são ignorados pela estrutura.

Etapa 3: Adicionar o widget JavaScript

Para implementar a funcionalidade do widget, adicione este script à <head> seção do arquivo HTML:

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

Principais componentes javaScript

Função Propósito
VSS.init() Inicializa a comunicação entre o widget e o Azure DevOps
VSS.require() Carrega bibliotecas de SDK necessárias e auxiliares de widget
VSS.register() Registra seu widget com um identificador exclusivo
WidgetHelpers.IncludeWidgetStyles() Aplica o estilo padrão do Azure DevOps
VSS.notifyLoadSucceeded() Notifica a estrutura que o carregamento foi concluído com êxito

Importante

O nome do widget em VSS.register() deve corresponder ao id no manifesto da sua extensão (Etapa 5).

Etapa 4: Adicionar imagens de extensão

Crie as imagens necessárias para sua extensão:

  • Logotipo da extensão: imagem de 98 x 98 pixels nomeada logo.png na img pasta
  • Ícone do catálogo do Widget: imagem de 98 x 98 pixels nomeada CatalogIcon.png na img pasta
  • Visualização do widget: imagem de 330 x 160 pixels nomeada preview.png na img pasta

Essas imagens são exibidas no catálogo do Marketplace e do widget quando os usuários navegam por extensões disponíveis.

Etapa 5: Criar o manifesto da extensão

Crie vss-extension.json no diretório raiz do projeto. Este arquivo define os metadados e as contribuições da extensão:

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

Importante

Substitua "publisher": "fabrikam" pelo nome real do editor. Saiba como criar um publicador.

Propriedades essenciais do manifesto

Seção Propósito
Informações básicas Nome da extensão, versão, descrição e editor
Ícones Caminhos para os ativos visuais da sua extensão
Contribuições Definições de widget, incluindo ID, tipo e propriedades
Arquivos Todos os arquivos a serem incluídos no pacote de extensão

Para ver a documentação completa do manifesto, consulte Referência do manifesto da extensão.

Etapa 6: Empacotar e publicar sua extensão

Empacote sua extensão e publique-a no Visual Studio Marketplace.

Instalar a ferramenta de empacotamento

npm i -g tfx-cli

Criar seu pacote de extensão

No diretório do projeto, execute:

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

Essa ação cria um .vsix arquivo que contém sua extensão empacotada.

Configurar um publicador

  1. Acesse o Portal de Publicação do Visual Studio Marketplace.
  2. Entre e crie um publicador se você não tiver um.
  3. Escolha um identificador exclusivo do editor (usado no arquivo de manifesto).
  4. Atualize vss-extension.json para usar o nome do seu editor em vez de "fabrikam".

Carregar sua extensão

  1. No Portal de Publicação, selecione Carregar nova extensão.
  2. Escolha seu .vsix arquivo e carregue-o.
  3. Compartilhe a extensão com sua organização do Azure DevOps.

Como alternativa, use a linha de comando:

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

Dica

Use --rev-version para incrementar automaticamente o número de versão ao atualizar uma extensão existente.

Etapa 7: Instalar e testar seu widget

Para testar, adicione seu widget a um painel:

  1. Acesse seu projeto do Azure DevOps: https://dev.azure.com/{Your_Organization}/{Your_Project}.
  2. Vá para Visão Geral>Painéis.
  3. Selecione Adicionar um widget.
  4. Localize o widget no catálogo e selecione Adicionar.

O widget "Olá, Mundo" aparece no painel, exibindo o texto configurado.

Próxima etapa: Continue para a Parte 2 para saber como integrar as APIs REST do Azure DevOps ao seu widget.

Parte 2: Olá, Mundo com a API REST do Azure DevOps

Estenda o widget para interagir com os dados do Azure DevOps usando APIs REST. Este exemplo demonstra como buscar informações de consulta e exibi-la dinamicamente em seu widget.

Nesta parte, use a API REST de Acompanhamento de Item de Trabalho para recuperar informações sobre uma consulta existente e exibir os detalhes da consulta abaixo do texto "Olá, Mundo".

Captura de tela do painel visão geral com um widget de exemplo usando a API REST para WorkItemTracking.

Etapa 1: Criar o arquivo HTML aprimorado

Crie um novo arquivo de widget que se baseia no exemplo anterior. Copie hello-world.html e renomeie-o para hello-world2.html. A estrutura do projeto agora inclui:

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

Atualizar a estrutura HTML do widget

Faça estas alterações em hello-world2.html:

  1. Adicionar um contêiner para dados de consulta: inclua um novo <div> elemento para exibir informações de consulta.
  2. Atualize o identificador do widget: mude o nome do widget de HelloWorldWidget para HelloWorldWidget2 para identificação única.
<!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>

Etapa 2: Configurar permissões de acesso à API

Antes de fazer chamadas à API REST, configure as permissões necessárias no manifesto da extensão.

Adicionar o escopo de trabalho

O escopo vso.work concede acesso somente leitura a itens de trabalho e consultas. Adicione este escopo a vss-extension.json:

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

Exemplo de manifesto completo

Para um manifesto completo com outras propriedades, estruture-o assim:

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

Importante

Limitações de escopo: não há suporte para adicionar ou alterar escopos após a publicação. Se você já tiver publicado sua extensão, deverá removê-la do Marketplace primeiro. Acesse o Portal de Publicação do Visual Studio Marketplace, localize sua extensão e selecione Remover.

Etapa 3: Implementar a integração da API REST

O Azure DevOps fornece bibliotecas de cliente REST do JavaScript por meio do SDK. Essas bibliotecas encapsulam chamadas AJAX e mapeiam respostas de API para objetos utilizáveis.

Atualizar o widget JavaScript

Substitua a chamada VSS.require em hello-world2.html para incluir o cliente REST de Rastreamento de Itens de Trabalho:

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

Principais detalhes de implementação

Componente Propósito
WorkItemTrackingRestClient.getClient() Obtém uma instância do cliente REST de Rastreamento de Itens de Trabalho
getQuery() Recupera informações de consulta encapsuladas em uma promessa
WidgetStatusHelper.Failure() Fornece tratamento de erros consistente para falhas de widget
projectId Contexto de projeto atual necessário para chamadas à API

Dica

Caminhos de consulta personalizados: se você não tiver uma consulta "Comentários" em "Consultas Compartilhadas", substitua "Shared Queries/Feedback" pelo caminho para qualquer consulta que exista em seu projeto.

Etapa 4: Exibir dados de resposta da API

Renderize as informações de consulta em seu widget processando a resposta da API REST.

Adicionar renderização de dados de consulta

Substitua o // Process query data comentário por esta implementação:

// 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);

O getQuery() método retorna um Contracts.QueryHierarchyItem objeto com propriedades para metadados de consulta. Este exemplo exibe três partes principais de informações abaixo do texto "Olá, Mundo".

Exemplo de trabalho completo

Seu arquivo final hello-world2.html deve ter esta aparência:

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

Etapa 5: Atualizar o manifesto da extensão

Para disponibilizá-lo no catálogo do widget, adicione seu novo widget ao manifesto da extensão.

Adicionar a segunda contribuição de widget

Atualize vss-extension.json para incluir o widget habilitado para API REST. Adicione essa contribuição à contributions matriz:

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

Dica

Imagem de visualização: crie uma preview2.png imagem (330 x 160 pixels) e coloque-a img na pasta para mostrar aos usuários a aparência do widget no catálogo.

Etapa 6: Empacotar, publicar e compartilhar

Empacotar, publicar e compartilhar sua extensão. Se você já tiver publicado a extensão, poderá reempacotá-la e atualizá-la diretamente no Marketplace.

Etapa 7: Testar o widget da API REST

Para exibir a integração da API REST em ação, adicione o novo widget ao painel:

  1. Acesse seu projeto do Azure DevOps: https://dev.azure.com/{Your_Organization}/{Your_Project}.
  2. Selecione Visão Geral>Painéis.
  3. Selecione Adicionar um widget.
  4. Encontre "Hello World Widget 2 (com API)" e selecione Adicionar.

O widget aprimorado exibe o texto "Olá, Mundo" e as informações de consulta ao vivo do seu projeto do Azure DevOps.

Próximas etapas: Continue para a Parte 3 para adicionar opções de configuração que permitem aos usuários personalizar qual consulta exibir.

Parte 3: Configurar Hello World

Crie com base na Parte 2 adicionando funcionalidades de configuração do usuário ao widget. Em vez de codificar o caminho da consulta, crie uma interface de configuração que permita que os usuários selecionem qual consulta exibir, com funcionalidade de visualização ao vivo.

Essa parte demonstra como criar widgets configuráveis que os usuários podem personalizar para suas necessidades específicas, fornecendo comentários em tempo real durante a configuração.

Captura de tela da pré-visualização em tempo real do painel de visão geral do widget com base nas alterações.

Etapa 1: Criar arquivos de configuração

As configurações de widget compartilham muitas semelhanças com os próprios widgets , ambos usam os mesmos padrões SDK, HTML e JavaScript, mas servem a diferentes finalidades dentro da estrutura de extensão.

Configurar a estrutura do projeto

Para dar suporte à configuração de widget, crie dois novos arquivos:

  1. Copie hello-world2.html e renomeie-o para hello-world3.htmlo widget configurável.
  2. Crie um novo arquivo chamado configuration.html, que manipula a interface de configuração.

A estrutura do projeto agora inclui:

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

Criar a interface de configuração

Adicione essa estrutura HTML ao configuration.html, que cria um menu suspenso para a escolha de consultas.

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

Etapa 2: Implementar o JavaScript de configuração

O JavaScript de configuração segue o mesmo padrão de inicialização que os widgets, mas implementa o IWidgetConfiguration contrato em vez do contrato básico IWidget .

Adicionar lógica de configuração

Insira este script na <head> seção de 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>

Detalhes do contrato de configuração

O IWidgetConfiguration contrato requer estas funções principais:

Função Propósito Quando chamado
load() Inicializar a interface do usuário de configuração com as configurações existentes Quando a caixa de diálogo de configuração é aberta
onSave() Serializar a entrada do usuário e validar as configurações Quando o usuário seleciona Salvar

Dica

Serialização de dados: este exemplo usa JSON para serializar as configurações. O widget acessa essas configurações via widgetSettings.customSettings.data e deve desserializá-las adequadamente.

Etapa 3: Habilitar a funcionalidade de visualização ao vivo

A visualização ao vivo permite que os usuários vejam as alterações do widget imediatamente à medida que modificam as configurações, fornecendo comentários instantâneos antes de salvar.

Implementar notificações de alteração

Para habilitar a visualização ao vivo, adicione este manipulador de eventos dentro da load função:

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

Concluir o arquivo de configuração

Sua final configuration.html deve ter esta aparência:

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

Importante

Botão Habilitar Salvar: a estrutura requer pelo menos uma notificação de alteração de configuração para habilitar o botão Salvar . O manipulador de eventos de alteração garante que essa ação ocorra quando os usuários selecionam uma opção.

Etapa 4: tornar o widget configurável

Transforme o widget da Parte 2 para usar dados de configuração em vez de valores embutidos em código. Esta etapa requer a implementação do IConfigurableWidget contrato.

Atualizar o registro do widget

In hello-world3.html, faça estas alterações:

  1. Atualizar iD do widget: alterar de HelloWorldWidget2 para HelloWorldWidget3.
  2. Adicionar função de recarregamento: implemente o contrato 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);
    }
}

Manipular dados de configuração

Atualize a função getQueryInfo para usar as configurações em vez de caminhos de consulta pré-definidos.

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

Diferenças no ciclo de vida do widget

Função Propósito Diretrizes de uso
load() Renderização inicial do widget e configuração única Operações pesadas, inicialização de recursos
reload() Atualizar o widget com a nova configuração Atualizações leves, atualização de dados

Dica

Otimização de desempenho: use load() para operações caras que só precisam ser executadas uma vez e reload() para atualizações rápidas quando a configuração for alterada.

(Opcional) Adicionar uma caixa de luz para obter informações detalhadas

Os widgets de painel têm espaço limitado, tornando desafiador exibir informações abrangentes. Uma lightbox oferece uma solução elegante ao exibir dados detalhados em uma sobreposição modal, sem a necessidade de navegar para fora do painel.

Por que usar uma caixa de luz em widgets?

Benefício Descrição
Eficiência de espaço Manter o widget compacto ao oferecer exibições detalhadas
Experiência do usuário Manter o contexto do painel enquanto mostra mais informações
Divulgação progressiva Mostrar dados de resumo no widget, detalhes sob demanda
Design responsivo Adaptar-se a diferentes tamanhos de tela e configurações de widget

Implementar elementos clicáveis

Atualize a renderização de dados de consulta para incluir elementos clicáveis que disparam a caixa de luz:

// 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);

Criar a funcionalidade do lightbox

Adicione esta implementação de lightbox ao seu widget 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');
        }
    });
}

Adicionar estilo de caixa de luz

Inclua estilos CSS para a caixa de luz na seção HTML <head> do widget:

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

Implementação avançada do widget

Seu widget aprimorado completo com funcionalidade de lightbox:

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

Considerações de acessibilidade: verifique se sua caixa de luz está acessível para teclado e inclui rótulos adequados para leitores de tela. Teste com os recursos de acessibilidade internos do Azure DevOps.

Importante

Desempenho: as lightboxes devem carregar rapidamente. Considere carregar os dados detalhados sob demanda quando a lightbox for aberta, em vez de buscar tudo antecipadamente.

Etapa 5: Configurar o manifesto da extensão

Registre o widget configurável e sua interface de configuração no manifesto da extensão.

Adicionar widget e contribuições de configuração

Atualize vss-extension.json para incluir duas novas contribuições:

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

Requisitos para contribuição de configuração

Propriedade Propósito Valor necessário
type Identifica a contribuição como configuração de widget ms.vss-dashboards-web.widget-configuration
targets Onde a configuração é exibida ms.vss-dashboards-web.widget-configuration
uri Caminho para o arquivo HTML de configuração Seu caminho de arquivo de configuração

Padrão de direcionamento de widget

Para widgets configuráveis, a targets matriz deve incluir uma referência à configuração:

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

Aviso

Visibilidade do botão de configuração: se o widget não direcionar corretamente sua contribuição de configuração, o botão Configurar não será exibido. Verifique se os nomes do editor e da extensão correspondem exatamente aos do seu manifesto.

Etapa 6: Empacotar, publicar e compartilhar

Implante sua extensão aprimorada com recursos de configuração.

Se for sua primeira publicação, siga a Etapa 6: Empacotar, publicar e compartilhar. Para extensões existentes, reempacote e atualize diretamente no Marketplace.

Etapa 7: Testar o widget configurável

Experimente o fluxo de trabalho de configuração completo adicionando e configurando seu widget.

Adicionar o widget ao painel

  1. Acesse https://dev.azure.com/{Your_Organization}/{Your_Project}.
  2. Vá para Visão Geral>Painéis.
  3. Selecione Adicionar um widget.
  4. Localize "Hello World Widget 3 (com configuração)" e selecione Adicionar.

Um prompt de configuração é exibido, pois o widget requer configuração:

Captura de tela do painel visão geral com um widget de exemplo do catálogo.

Configurar o widget

Acessar a configuração por meio de qualquer método:

  • Menu do widget: passe o cursor sobre o widget, selecione a reticência (⋯) e depois clique em Configurar
  • Modo de edição do painel: Selecione Editar no painel e, em seguida, o botão configurar no widget

O painel de configuração abre com uma pré-visualização em tempo real no centro. Selecione uma consulta no menu suspenso para ver atualizações imediatas, depois selecione Salvar para aplicar suas alterações.

Etapa 8: Adicionar opções de configuração avançadas

Estenda o widget com recursos de configuração mais internos, como nomes e tamanhos personalizados.

Habilitar configuração de nome e tamanho

O Azure DevOps fornece dois recursos configuráveis prontos para uso:

Característica Propriedade de manifesto Propósito
Nomes personalizados isNameConfigurable: true Os usuários podem substituir o nome do widget padrão
Vários tamanhos supportedSizes Várias entradas Os usuários podem redimensionar widgets

Exemplo de manifesto aprimorado

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

Exibir nomes configurados

Para mostrar nomes de widget personalizados, atualize o widget para usar 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);
    }
}

Depois de atualizar sua extensão, você pode configurar o nome e o tamanho do widget:

Captura de tela mostrando onde o nome e o tamanho do widget podem ser configurados.

Reempacote e atualize sua extensão para habilitar essas opções de configuração avançadas.

Parabéns! Você criou um widget de painel completo e configurável do Azure DevOps com recursos de visualização ao vivo e opções de personalização do usuário.