Nota:
El acceso a esta página requiere autorización. Puede intentar iniciar sesión o cambiar directorios.
El acceso a esta página requiere autorización. Puede intentar cambiar los directorios.
Azure DevOps Services | Azure DevOps Server | Azure DevOps Server 2022
Los widgets se implementan como contribuciones en el marco de extensión. Una sola extensión puede incluir varias contribuciones de widget. En este artículo se muestra cómo crear una extensión que proporcione uno o varios widgets.
Sugerencia
Consulte nuestra documentación más reciente sobre el desarrollo de extensiones mediante el SDK de extensión de Azure DevOps.
Sugerencia
Si va a iniciar una nueva extensión de Azure DevOps, pruebe primero estas colecciones de ejemplo mantenidas: funcionan con compilaciones de productos actuales y cubren escenarios modernos (por ejemplo, agregar pestañas en páginas de solicitud de incorporación de cambios).
- Ejemplo de extensión de Azure DevOps (GitHub): un ejemplo de inicio compacto que muestra patrones de extensión comunes: https://github.com/microsoft/azure-devops-extension-sample
- Ejemplos de extensión de Azure DevOps (guía de colecciones y contribuciones heredadas): instale para inspeccionar los destinos de la interfaz de usuario o vea el origen: https://marketplace.visualstudio.com/items/ms-samples.samples-contributions-guide y https://github.com/Microsoft/vso-extension-samples/tree/master/contributions-guide
- Ejemplos de Microsoft Learn (examinar ejemplos de Azure DevOps)—ejemplos curados y actualizados en Microsoft Docs: /samples/browse/?terms=azure%20devops%20extension
Si un ejemplo no funciona en su organización, instálelo en una organización personal o de prueba y compare los identificadores de destino y las versiones de API del manifiesto de extensión con los documentos actuales. Para obtener referencia y API, consulte:
Requisitos previos
| Requisito | Descripción |
|---|---|
| Conocimientos de programación | Conocimientos de JavaScript, HTML y CSS para el desarrollo de widgets |
| Organización de Azure DevOps | Cree una organización |
| Editor de texto | Usamos Visual Studio Code para tutoriales |
| Node.js | Última versión de Node.js |
| CLI multiplataforma |
tfx-cli para empaquetar extensiones Instalación mediante: npm i -g tfx-cli |
| Directorio del proyecto | Directorio principal con esta estructura después de completar el 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 |
Información general del tutorial
En este tutorial se enseña el desarrollo de widgets a través de tres ejemplos progresivos:
| Parte | Enfoque | Conocimientos que adquirirá |
|---|---|---|
| Parte 1: Hola mundo | Creación básica de widgets | Crear un widget que muestre texto |
| Parte 2: Integración de la API REST | Llamadas API de Azure DevOps | Adición de la funcionalidad de la API REST para capturar y mostrar datos |
| Parte 3: Configuración del widget | Personalización del usuario | Implementación de opciones de configuración para el widget |
Sugerencia
Si prefiere saltar directamente a los ejemplos de trabajo, los ejemplos incluidos (vea la nota anterior) muestran un conjunto de widgets que puede empaquetar y publicar.
Antes de comenzar, revise los estilos básicos de widget y las instrucciones estructurales que proporcionamos.
Parte 1: Hola mundo
Cree un widget básico que muestre "Hola mundo" mediante JavaScript. Esta base muestra los conceptos básicos de desarrollo de widgets.
Paso 1: Instalación del SDK de cliente
El SDK de VSS permite que el widget se comunique con Azure DevOps. Instálelo mediante npm:
npm install vss-web-extension-sdk
Copie el VSS.SDK.min.js archivo de vss-web-extension-sdk/lib a la home/sdk/scripts carpeta.
Para más documentación del SDK, consulte la página gitHub del SDK de cliente.
Paso 2: Crear la estructura HTML
Cree hello-world.html en el directorio del proyecto. Este archivo proporciona el diseño del widget y las referencias a los scripts necesarios.
<!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>
Los widgets se ejecutan en iframes, por lo que la mayoría de los elementos principales HTML excepto <script> y <link> se omiten en el marco de trabajo.
Paso 3: Adición de JavaScript widget
Para implementar la funcionalidad del widget, agregue este script a la <head> sección del archivo 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>
Componentes clave de JavaScript
| Función | Propósito |
|---|---|
VSS.init() |
Inicializa la comunicación entre el widget y Azure DevOps. |
VSS.require() |
Carga las bibliotecas de SDK necesarias y los asistentes de widgets |
VSS.register() |
Registra el widget con un identificador único |
WidgetHelpers.IncludeWidgetStyles() |
Aplica el estilo predeterminado de Azure DevOps. |
VSS.notifyLoadSucceeded() |
Notifica al framework que la carga se completó correctamente. |
Importante
El nombre del widget en VSS.register() debe coincidir con el id en el manifiesto de tu extensión (paso 5).
Paso 4: Agregar imágenes de extensión
Cree las imágenes necesarias para la extensión:
-
Logotipo de extensión: imagen de píxeles de 98 x 98 píxeles denominada
logo.pngen laimgcarpeta -
Icono del catálogo de widgets: imagen de píxeles de 98x98 denominada
CatalogIcon.pngen laimgcarpeta -
Vista previa del widget: imagen de 330 x 160 píxeles denominada
preview.pngen laimgcarpeta
Estas imágenes se muestran en Marketplace y el catálogo de widgets cuando los usuarios examinan las extensiones disponibles.
Paso 5: Crear el manifiesto de extensión
Cree vss-extension.json en el directorio raíz del proyecto. Este archivo define los metadatos y las contribuciones de la extensión:
{
"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
Reemplace por "publisher": "fabrikam" el nombre real del publicador. Aprenda a crear un publicador.
Propiedades esenciales del manifiesto
| Sección | Propósito |
|---|---|
| Información básica | Nombre de extensión, versión, descripción y publicador |
| Iconos | Rutas de acceso a los recursos visuales de la extensión |
| Contribuciones | Definiciones de widgets, como el identificador, el tipo y las propiedades |
| Archivos | Todos los archivos que se van a incluir en el paquete de extensión |
Para obtener documentación completa del manifiesto, consulte Referencia del manifiesto de extensión.
Paso 6: Empaquetar y publicar la extensión
Empaquete la extensión y publíquela en Visual Studio Marketplace.
Instalación de la herramienta de empaquetado
npm i -g tfx-cli
Creación del paquete de extensión
En el directorio del proyecto, ejecute:
tfx extension create --manifest-globs vss-extension.json
Esta acción crea un .vsix archivo que contiene la extensión empaquetada.
Configuración de un publicador
- Vaya al Portal de publicación de Visual Studio Marketplace.
- Inicie sesión y cree un publicador si no tiene uno.
- Elija un identificador de publicador único (usado en el archivo de manifiesto).
- Actualice su
vss-extension.jsonpara utilizar su nombre de editor en lugar de "fabrikam".
Cargue su extensión
- En el Portal de publicación, seleccione Cargar nueva extensión.
- Elija el
.vsixarchivo y cárguelo. - Comparta la extensión con su organización de Azure DevOps.
Como alternativa, use la línea de comandos:
tfx extension publish --manifest-globs vss-extension.json --share-with yourOrganization
Sugerencia
Use --rev-version para incrementar automáticamente el número de versión al actualizar una extensión existente.
Paso 7: Instalar y probar el widget
Para probarlo, agregue el widget a un panel:
- Vaya al proyecto de Azure DevOps:
https://dev.azure.com/{Your_Organization}/{Your_Project}. - Vaya a Información general>Paneles.
- Selecciona Agregar un widget.
- Busque el widget en el catálogo y seleccione Agregar.
El widget "Hola mundo" aparece en el panel, mostrando el texto que configuró.
Paso siguiente: continúe con la parte 2 para aprender a integrar las API rest de Azure DevOps en el widget.
Parte 2: Hola mundo con la API REST de Azure DevOps
Amplíe el widget para interactuar con los datos de Azure DevOps mediante las API REST. En este ejemplo se muestra cómo capturar información de consulta y mostrarla dinámicamente en el widget.
En esta parte, use la API REST de seguimiento de elementos de trabajo para recuperar información sobre una consulta existente y mostrar los detalles de la consulta debajo del texto "Hola mundo".
Paso 1: Crear el archivo HTML mejorado
Cree un nuevo archivo de widget que se basa en el ejemplo anterior. Copie hello-world.html y cámbiele el nombre a hello-world2.html. La estructura del proyecto ahora incluye:
|--- 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
Actualización de la estructura HTML del widget
Realice estos cambios en hello-world2.html:
-
Agregar un contenedor para los datos de consulta: incluya un nuevo
<div>elemento para mostrar información de consulta. -
Actualice el identificador del widget: cambie el nombre del widget de
HelloWorldWidgetaHelloWorldWidget2para la identificación ú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>
Paso 2: Configuración de permisos de acceso de API
Antes de realizar llamadas a la API REST, configure los permisos necesarios en el manifiesto de extensión.
Adición del ámbito de trabajo
El ámbito vso.work concede acceso de solo lectura a los elementos de trabajo y las consultas. Agregue este ámbito a vss-extension.json:
{
"scopes": [
"vso.work"
]
}
Ejemplo de manifiesto completo
Para obtener un manifiesto completo con otras propiedades, estructurelo de la siguiente manera:
{
"name": "example-widget",
"publisher": "example-publisher",
"version": "1.0.0",
"scopes": [
"vso.work"
]
}
Importante
Limitaciones de ámbito: no se admite agregar o cambiar ámbitos después de publicar. Si ya ha publicado la extensión, primero debe quitarla de Marketplace. Vaya al Portal de publicación de Visual Studio Marketplace, busque la extensión y seleccione Quitar.
Paso 3: Implementación de la integración de la API REST
Azure DevOps proporciona bibliotecas cliente REST de JavaScript a través del SDK. Estas bibliotecas encapsulan las llamadas AJAX y asignan respuestas de API a objetos utilizables.
Actualización del widget de JavaScript
Sustituya la llamada VSS.require en su hello-world2.html para incluir el cliente REST de seguimiento de elementos de trabajo:
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();
});
Detalles clave de implementación
| Componente | Propósito |
|---|---|
WorkItemTrackingRestClient.getClient() |
Obtenga una instancia del cliente REST de seguimiento de elementos de trabajo |
getQuery() |
Recupera la información de consulta encapsulada en una promesa. |
WidgetStatusHelper.Failure() |
Proporciona una gestión de errores coherente para fallos de widget |
projectId |
Contexto actual del proyecto necesario para las llamadas API |
Sugerencia
Rutas de acceso de consulta personalizadas: si no tiene una consulta de "Comentarios" en "Consultas compartidas", reemplace por "Shared Queries/Feedback" la ruta de acceso a cualquier consulta que exista en el proyecto.
Paso 4: Mostrar datos de respuesta de API
Represente la información de consulta en el widget mediante el procesamiento de la respuesta de la API REST.
Agregar representación de datos de consulta
Reemplace el // Process query data comentario por esta implementación:
// 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);
El getQuery() método devuelve un Contracts.QueryHierarchyItem objeto con propiedades para los metadatos de consulta. En este ejemplo se muestran tres fragmentos clave de información debajo del texto "Hola mundo".
Ejemplo de trabajo completo
El archivo final hello-world2.html debe tener este aspecto:
<!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>
Paso 5: Actualizar el manifiesto de extensión
Para que esté disponible en el catálogo de widgets, agregue el nuevo widget al manifiesto de extensión.
Adición de la segunda contribución del widget
Actualice vss-extension.json para incluir el widget habilitado para la API REST. Agregue esta contribución a la 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"
]
}
Sugerencia
Imagen de vista previa: cree una preview2.png imagen (330 x 160 píxeles) y colóquela en la img carpeta para mostrar a los usuarios el aspecto del widget en el catálogo.
Paso 6: Empaquetar, publicar y compartir
Empaquete, publique y comparta su extensión. Si ya ha publicado la extensión, puede volver a empaquetar y actualizarla directamente en Marketplace.
Paso 7: Probar el widget de la API REST
Para ver la integración de la API REST en acción, agregue el nuevo widget al panel:
- Vaya al proyecto de Azure DevOps:
https://dev.azure.com/{Your_Organization}/{Your_Project}. - Seleccione Overview>Dashboards.
- Selecciona Agregar un widget.
- Busque "Hello World Widget 2 (con API)" y seleccione Agregar.
El widget mejorado muestra tanto el texto "Hola mundo" como la información de consulta dinámica del proyecto de Azure DevOps.
Pasos siguientes: continúe con la parte 3 para agregar opciones de configuración que permiten a los usuarios personalizar qué consulta mostrar.
Parte 3: Configurar Hola mundo
Amplíe Parte 2 añadiendo capacidades de configuración de usuario a su widget. En lugar de codificar de forma rígida la ruta de acceso de consulta, cree una interfaz de configuración que permita a los usuarios seleccionar qué consulta mostrar, con la funcionalidad de vista previa activa.
En esta parte se muestra cómo crear widgets configurables que los usuarios pueden personalizar para sus necesidades específicas al proporcionar comentarios en tiempo real durante la configuración.
Paso 1: Crear archivos de configuración
Las configuraciones de widget comparten muchas similitudes con los propios widgets, tanto usan el mismo SDK, estructura HTML y patrones de JavaScript, pero sirven para propósitos diferentes dentro del marco de extensión.
Configuración de la estructura del proyecto
Para admitir la configuración del widget, cree dos nuevos archivos:
- Copie
hello-world2.htmly cámbiele el nombre ahello-world3.html, el widget configurable. - Cree un archivo denominado
configuration.html, que controla la interfaz de configuración.
La estructura del proyecto ahora incluye:
|--- 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
Creación de la interfaz de configuración
Agregue esta estructura HTML a configuration.html, que crea un selector desplegable para elegir 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>
Paso 2: Implementación de la configuración de JavaScript
La configuración de JavaScript sigue el mismo patrón de inicialización que los widgets, pero implementa el IWidgetConfiguration contrato en lugar del contrato básico IWidget .
Agrega lógica de configuración
Inserte este script en la <head> sección 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>
Detalles del contrato de configuración
El IWidgetConfiguration contrato requiere estas funciones clave:
| Función | Propósito | Cuándo se llama |
|---|---|---|
load() |
Inicialización de la interfaz de usuario de configuración con las opciones existentes | Cuando se abre el cuadro de diálogo de configuración |
onSave() |
Serializar la entrada del usuario y validar la configuración | Cuando el usuario selecciona Guardar |
Sugerencia
Serialización de datos: en este ejemplo se usa JSON para serializar la configuración. El widget accede a esta configuración a través de widgetSettings.customSettings.data y debe deserializarla en consecuencia.
Paso 3: Habilitación de la funcionalidad de vista previa dinámica
La versión preliminar activa permite a los usuarios ver los cambios de widget inmediatamente a medida que modifican las opciones de configuración, proporcionando comentarios instantáneos antes de guardarlos.
Implementación de notificaciones de cambios
Para habilitar la versión preliminar en directo, agregue este controlador de eventos dentro de la load función :
$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);
});
Archivo de configuración completo
El resultado final configuration.html debería tener este aspecto:
<!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
Habilitar el botón Guardar: el marco requiere al menos una notificación de cambio de configuración para habilitar el botón Guardar . El controlador de eventos de cambio garantiza que esta acción se produce cuando los usuarios seleccionan una opción.
Paso 4: Hacer que el widget sea configurable
Transforme el widget de la parte 2 para usar datos de configuración en lugar de valores codificados de forma rígida. Este paso requiere implementar el IConfigurableWidget contrato.
Actualización del registro de widgets
En hello-world3.html, realice estos cambios:
-
Actualizar el identificador del widget: cambie de
HelloWorldWidget2aHelloWorldWidget3. -
Añadir función de recarga: Implementa el 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);
}
}
Control de los datos de configuración
Actualice la getQueryInfo función para usar opciones de configuración en lugar de rutas de consulta codificadas de forma rígida:
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();
}
Diferencias de ciclo de vida del widget
| Función | Propósito | Directrices de uso |
|---|---|---|
load() |
Representación inicial de widgets y configuración única | Operaciones intensivas, inicialización de recursos |
reload() |
Actualizar widget con nueva configuración | Actualizaciones ligeras, actualización de datos |
Sugerencia
Optimización del rendimiento: use load() para operaciones costosas que solo necesiten ejecutarse una vez y reload() para actualizaciones rápidas cuando cambie la configuración.
(Opcional) Agregar una caja de luz para obtener información detallada
Los widgets de panel tienen un espacio limitado, por lo que resulta difícil mostrar información completa. Una caja de luz proporciona una solución elegante mostrando datos detallados en una superposición modal sin salir del panel.
¿Por qué usar una caja de luz en widgets?
| Ventajas | Descripción |
|---|---|
| Ahorro de espacio | Mantener el widget compacto al ofrecer vistas detalladas |
| Experiencia del usuario | Mantener el contexto del panel al mostrar más información |
| Divulgación progresiva | Mostrar datos de resumen en widget, detalles a petición |
| Diseño con capacidad de respuesta | Adaptarse a diferentes tamaños de pantalla y configuraciones de widgets |
Implementar elementos en los que se pueden hacer clic
Actualice la representación de datos de consulta para incluir elementos en los que se pueden hacer clic que desencadenan la caja 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);
Creación de la funcionalidad lightbox
Agregue esta implementación de lightbox al 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">×</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');
}
});
}
Añada estilo lightbox
Incluya estilos CSS para lightbox en la sección HTML <head> del 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>
Implementación mejorada del widget
Su widget mejorado completo con funcionalidad 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>
Consideraciones de accesibilidad: asegúrese de que la caja de luz sea accesible con el teclado e incluya etiquetas adecuadas para los lectores de pantalla. Pruebe con las características de accesibilidad integradas de Azure DevOps.
Importante
Rendimiento: las cajas de luz deben cargarse rápidamente. Considere la posibilidad de cargar los datos detallados solo cuando se abra la caja de luz, en lugar de obtener todo por adelantado.
Paso 5: Configurar el manifiesto de extensión
Registre tanto el widget configurable como su interfaz de configuración en el manifiesto de extensión.
Incorporación de contribuciones de widget y configuración
Actualice vss-extension.json para incluir dos nuevas contribuciones:
{
"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 la contribución a la configuración
| Propiedad | Propósito | Valor obligatorio |
|---|---|---|
type |
Identifique la contribución como configuración del widget | ms.vss-dashboards-web.widget-configuration |
targets |
Dónde aparece la configuración | ms.vss-dashboards-web.widget-configuration |
uri |
Ruta de acceso al archivo HTML de configuración | Su ruta del archivo de configuración |
Patrón de destino de widgets
Para widgets configurables, la targets matriz debe incluir una referencia a la configuración:
<publisher>.<extension-id>.<configuration-id>
Advertencia
Visibilidad del botón de configuración: si el widget no tiene como destino correctamente su contribución de configuración, el botón Configurar no aparece. Verifique que los nombres de publicador y de extensión coincidan exactamente con su manifiesto.
Paso 6: Empaquetar, publicar y compartir
Implemente la extensión mejorada con funcionalidades de configuración.
Si es su primera publicación, siga paso 6: Empaquetar, publicar y compartir. Para las extensiones existentes, vuelva a empaquetar y actualice directamente en el Marketplace.
Paso 7: Probar el widget configurable
Experimente el flujo de trabajo de configuración completo agregando y configurando el widget.
Añada el widget a su panel
- Ir a
https://dev.azure.com/{Your_Organization}/{Your_Project}. - Vaya a Información general>Paneles.
- Selecciona Agregar un widget.
- Busque "Hello World Widget 3 (con configuración)" y seleccione Agregar.
Se muestra un mensaje de configuración, ya que el widget requiere configuración:
Configuración del widget
Acceda a la configuración a través de cualquiera de los métodos:
- Menú widget: mantenga el puntero sobre el widget, seleccione los puntos suspensivos (⋯) y, a continuación, Configurar.
- Modo de edición del panel: seleccione Editar en el panel y, a continuación, el botón Configurar del widget.
El panel de configuración se abre con una vista previa en tiempo real en el centro. Seleccione una consulta en la lista desplegable para ver las actualizaciones inmediatas y, a continuación, seleccione Guardar para aplicar los cambios.
Paso 8: Agregar opciones de configuración avanzadas
Amplíe el widget con características de configuración más integradas, como nombres y tamaños personalizados.
Habilitación de la configuración de nombre y tamaño
Azure DevOps proporciona dos características configurables integradas:
| Característica | Propiedad de manifiesto | Propósito |
|---|---|---|
| Nombres personalizados | isNameConfigurable: true |
Los usuarios pueden invalidar el nombre del widget predeterminado |
| Varios tamaños | Entradas múltiples supportedSizes |
Los usuarios pueden cambiar el tamaño de los widgets |
Ejemplo de manifiesto mejorado
{
"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"]
}
}
]
}
Mostrar nombres configurados
Para mostrar nombres de widget personalizados, actualice el 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);
}
}
Después de actualizar la extensión, puede configurar el nombre y el tamaño del widget:
Vuelva a empaquetar y actualice la extensión para habilitar estas opciones de configuración avanzadas.
¡Felicidades! Ha creado un widget de panel de Azure DevOps completo y configurable con funcionalidades de vista previa dinámica y opciones de personalización de usuario.