Partilhar via


Adicionar um widget de painel

Serviços de DevOps do Azure | 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 widgets. Este artigo mostra como criar uma extensão que fornece um ou mais widgets.

Gorjeta

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

Gorjeta

Se você estiver iniciando uma nova extensão do Azure DevOps, experimente essas coleções de exemplo mantidas primeiro — elas funcionam com compilações 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 e as versões de API do manifesto de extensão com os documentos 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 widgets
Organização do Azure DevOps Criar uma organização
Editor de texto Usamos o Visual Studio Code para tutoriais
Node.js Última versão do Node.js
CLI multiplataforma TFX-CLI para empacotar extensões
Instale usando: npm i -g tfx-cli
Diretório de projetos Diretório inicial com esta 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

Descrição geral dos tutoriais

Este tutorial ensina o desenvolvimento de widgets através de três exemplos progressivos:

Parte Foco O que você aprende
Parte 1: Olá Mundo Criação básica de widgets Criar um widget que exiba texto
Parte 2: Integração da API REST Chamadas da 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 seu widget

Gorjeta

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

Antes de começar, revise os estilos básicos de widgets e as orientações estruturais que fornecemos.

Parte 1: Olá Mundo

Crie um widget básico que exiba "Hello World" usando JavaScript. Esta base demonstra os principais conceitos de desenvolvimento de widgets.

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

Etapa 1: Instalar o SDK do cliente

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

npm install vss-web-extension-sdk

Copie o ficheiro 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 GitHub do SDK do cliente.

Etapa 2: Criar a estrutura HTML

Crie hello-world.html no diretório do seu projeto. Este arquivo fornece o layout do widget e 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 JavaScript widget

Para implementar a funcionalidade do widget, adicione este script à <head> seção do seu 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 as bibliotecas SDK necessárias e os auxiliares de widgets
VSS.register() Registra seu widget com um identificador exclusivo
WidgetHelpers.IncludeWidgetStyles() Aplica o estilo padrão do Azure DevOps
VSS.notifyLoadSucceeded() Notifica o framework de 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 a sua extensão:

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

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

Etapa 5: Criar o manifesto da extensão

Crie vss-extension.json no diretório raiz do seu projeto. Este arquivo define os metadados e contribuições da sua 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ção básica 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
Ficheiros Todos os arquivos a serem incluídos no pacote de extensão

Para obter a documentação completa do manifesto, consulte Referência do manifesto de 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

Crie seu pacote de extensão

No diretório do projeto, execute:

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

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

Configurar um publicador

  1. Vá para o Portal de Publicação do Visual Studio Marketplace.
  2. Inicie sessão e crie um publicador, se não tiver um.
  3. Escolha um identificador de editor exclusivo (usado no seu arquivo de manifesto).
  4. Atualize o seu vss-extension.json para substituir "fabrikam" pelo nome do seu editor.

Carregue a sua extensão

  1. No Portal de Publicação, selecione Carregar nova extensão.
  2. Escolha o seu .vsix ficheiro 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

Gorjeta

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

Etapa 7: Instalar e testar seu widget

Para testar, adicione seu widget a um painel:

  1. Vá para seu projeto de DevOps do Azure: https://dev.azure.com/{Your_Organization}/{Your_Project}.
  2. Vá para Visão geral>Dashboards.
  3. Selecione Adicionar um widget.
  4. Encontre seu widget no catálogo e selecione Adicionar.

Seu widget "Hello World" aparece no painel, exibindo o texto que você configurou.

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 seu widget para interagir com os dados do Azure DevOps usando APIs REST. Este exemplo demonstra como buscar informações de consulta e exibi-las dinamicamente em seu widget.

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

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 seu 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. Atualizar o identificador do widget: altere o nome do widget de HelloWorldWidget para para HelloWorldWidget2 para identificação exclusiva.
<!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 do trabalho

O vso.work escopo concede acesso somente leitura a itens de trabalho e consultas. Adicione este âmbito ao seu 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á publicou sua extensão, você deve removê-la do Marketplace primeiro. Vá para 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 JavaScript por meio do SDK. Essas bibliotecas encapsulam chamadas AJAX e mapeiam respostas de API para objetos utilizáveis.

Atualizar o JavaScript do widget

Substitua a VSS.require chamada em seu hello-world2.html para incluir o cliente REST de rastreamento de item 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 da implementação

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

Gorjeta

Caminhos de consulta personalizados: Se você não tiver uma consulta "Feedback" em "Consultas compartilhadas", substitua "Shared Queries/Feedback" pelo caminho para qualquer consulta existente 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 informações principais abaixo do texto "Hello World".

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 de widgets, adicione seu novo widget ao manifesto da extensão.

Adicionar a segunda contribuição do widget

Atualize vss-extension.json para incluir seu widget habilitado para API REST. Adicione esta 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"
    ]
}

Gorjeta

Visualizar imagem: crie uma preview2.png imagem (330x160 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

Empacote, publique e partilhe a sua extensão. Se você já publicou a extensão, pode reempacotá-la e atualizá-la diretamente no Marketplace.

Etapa 7: Testar o widget da API REST

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

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

Seu widget aprimorado exibe o texto "Hello World" 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 permitam aos usuários personalizar qual consulta exibir.

Parte 3: Configurar o Hello World

Desenvolva a Parte 2 adicionando recursos de configuração do usuário ao seu widget. Em vez de codificar o caminho da consulta, crie uma interface de configuração que permita aos usuários selecionar qual consulta exibir, com a funcionalidade de visualização ao vivo.

Esta parte demonstra como criar widgets configuráveis que os usuários podem personalizar de acordo com suas necessidades específicas, fornecendo feedback em tempo real durante a configuração.

Captura de tela da pré-visualização ao vivo do painel Visão geral com base nas alterações do widget.

Etapa 1: Criar arquivos de configuração

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

Configurar a estrutura do projeto

Para suportar a configuração do widget, crie dois novos arquivos:

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

A estrutura do seu 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 esta estrutura HTML ao configuration.html, que cria um menu suspenso para seleção 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 JavaScript de configuração

O JavaScript de configuração segue o mesmo padrão de inicialização dos 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 exige as seguintes funções essenciais:

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 configurações Quando o usuário seleciona Salvar

Gorjeta

Serialização de dados: Este exemplo usa JSON para serializar configurações. O widget acede a estas configurações através de widgetSettings.customSettings.data e deve desserializá-las de acordo.

Etapa 3: Ativar 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 definições de configuração, fornecendo feedback instantâneo 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);
});

Arquivo de configuração completo

O seu configuration.html final deve ter o seguinte aspeto:

<!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 Ativar 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 change garante que essa ação ocorra quando os usuários selecionam uma opção.

Etapa 4: Tornar o widget configurável

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

Atualizar registro do widget

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

  1. Atualizar ID do widget: Mude de HelloWorldWidget2 para HelloWorldWidget3.
  2. Adicionar função de recarga: Implemente o IConfigurableWidget contrato.
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 getQueryInfo função para usar definições de configuração em vez de caminhos de consulta codificados:

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 nova configuração Atualizações leves, atualização de dados

Gorjeta

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 lightbox para obter informações detalhadas

Os widgets do painel têm espaço limitado, o que dificulta a exibição de informações abrangentes. Uma lightbox fornece uma solução elegante, mostrando dados detalhados em uma sobreposição modal sem sair do dashboard.

Por que usar uma lightbox em widgets?

Benefício Descrição
Eficiência do espaço Mantenha o widget compacto enquanto oferece vistas detalhadas
Experiência do utilizador Manter o contexto do painel enquanto mostra mais informações
Divulgação progressiva Mostrar dados de resumo no widget, detalhes sob demanda
Design responsivo Adapte-se a diferentes tamanhos de tela e configurações de widgets

Implementar elementos clicáveis

Atualize a renderização de dados de consulta para incluir elementos clicáveis que acionam a lightbox:

// 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 de visualização em destaque (lightbox)

Adicione esta implementação 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 lightbox

Inclua estilos CSS para a lightbox 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 aprimorada de widgets

Seu widget aprimorado completo com funcionalidade 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 sobre acessibilidade: Certifique-se de que a sua lightbox é acessível pelo teclado e inclui etiquetas adequadas para leitores de ecrã. Teste com os recursos de acessibilidade internos do Azure DevOps.

Importante

Desempenho: As caixas de luz devem ser carregadas rapidamente. Considere carregar os dados detalhados usando lazy loading apenas quando a lightbox é 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 em seu manifesto de extensão.

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

Atualização 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 de colaboração para configuração

Propriedade Propósito Valor obrigatório
type Identifica a contribuição como configuração do widget ms.vss-dashboards-web.widget-configuration
targets Onde a configuração aparece ms.vss-dashboards-web.widget-configuration
uri Caminho para o arquivo HTML de configuração O caminho do arquivo de configuração

Padrão de segmentação do 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 aparecerá. Verifique se os nomes do editor e da extensão correspondem exatamente ao seu manifesto.

Etapa 6: Empacotar, publicar e compartilhar

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

Se for a sua primeira publicação, siga o Passo 6: Empacotar, publicar e partilhar. 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.

Adicione o widget ao seu painel

  1. Aceda a https://dev.azure.com/{Your_Organization}/{Your_Project}.
  2. Vá para Visão geral>Dashboards.
  3. Selecione Adicionar um widget.
  4. Encontre "Hello World Widget 3 (with config)" e selecione Adicionar.

Um prompt de configuração é exibido, já que o widget requer configuração:

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

Configurar o widget

Configuração de acesso através de qualquer um dos métodos:

  • Menu do widget: passe o cursor sobre o widget, selecione as reticências (⋯) e, em seguida, configure
  • 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-se com uma visualização ao vivo na parte central. Selecione uma consulta na lista suspensa para ver atualizações imediatas e, em seguida, selecione Salvar para aplicar as alterações.

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

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

Ativar configuração de nome e tamanho

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

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

Exemplo de manifesto melhorado

{
    "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 widgets personalizados, atualize seu 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 completo e configurável do painel do Azure DevOps com recursos de visualização ao vivo e opções de personalização do usuário.