다음을 통해 공유


대시보드 위젯 추가

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

대시보드의 위젯은 확장 프레임워크에서 기여로 구현됩니다. 단일 확장에는 여러 기여가 있을 수 있습니다. 여러 위젯을 기여로 사용하여 확장을 만드는 방법을 알아봅니다.

이 기사는 세 부분으로 나누어져 있으며, 각 부분은 이전 부분을 기반으로 발전합니다. 간단한 위젯으로 시작하고 포괄적인 위젯으로 끝납니다.

Azure DevOps 확장 SDK를 사용한 확장 개발을 위한 최신 문서를 확인해 보세요.

필수 조건

요구 사항 설명
프로그래밍 지식 위젯 개발을 위한 JavaScript, HTML 및 CSS 지식
Azure DevOps 조직 조직 만들기
텍스트 편집기 자습서에 Visual Studio Code 를 사용합니다.
Node.js 최신 버전의 Node.js
플랫폼 간 CLI 확장을 패키징하기 위한 tfx-cli
다음을 사용하여 설치합니다. npm i -g tfx-cli
프로젝트 디렉터리 자습서를 완료한 후 이 구조의 홈 디렉터리:

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

자습서 개요

이 자습서에서는 다음 세 가지 점진적 예제를 통해 위젯 개발을 설명합니다.

부분 포커스 학습 내용
1부: Hello World 기본 위젯 만들기 텍스트를 표시하는 위젯 만들기
2부: REST API 통합 Azure DevOps API 호출 데이터를 가져오고 표시하는 REST API 기능 추가
3부: 위젯 구성 사용자 사용자 지정 위젯에 대한 구성 옵션 구현

자습서를 건너뜁니다. 전체 샘플 확장을 다운로드하고 폴더로 widgets 이동한 다음 6단계 로 이동하여 즉시 사용할 수 있는 세 가지 예제 위젯을 게시합니다.

시작하기 전에 제공하는 기본 위젯 스타일 및 구조 지침을 검토합니다.

1부: 헬로 월드

JavaScript를 사용하여 "Hello World"를 표시하는 기본 위젯을 만듭니다. 이 기초는 핵심 위젯 개발 개념을 보여 줍니다.

샘플 위젯이 있는 개요 대시보드의 스크린샷

1단계: 클라이언트 SDK 설치

VSS SDK를 사용하면 위젯이 Azure DevOps와 통신할 수 있습니다. npm을 사용하여 설치합니다.

npm install vss-web-extension-sdk

VSS.SDK.min.js 파일을 vss-web-extension-sdk/lib에서 home/sdk/scripts 폴더로 복사합니다.

자세한 SDK 설명서는 클라이언트 SDK GitHub 페이지를 참조하세요.

2단계: HTML 구조 만들기

프로젝트 디렉터리에서 hello-world.html를 만드세요. 이 파일은 위젯의 레이아웃과 필요한 스크립트에 대한 참조를 제공합니다.

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

위젯은 iframe에서 실행되므로 프레임워크를 제외한 <script><link> 대부분의 HTML 헤드 요소는 무시됩니다.

3단계: 위젯 JavaScript 추가

위젯 기능을 구현하려면 HTML 파일의 섹션에 다음 스크립트를 <head> 추가합니다.

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

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

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

주요 JavaScript 구성 요소

기능 목적
VSS.init() 위젯과 Azure DevOps 간의 통신을 초기화합니다.
VSS.require() 필요한 SDK 라이브러리 및 위젯 도우미를 로드합니다.
VSS.register() 고유 식별자를 사용하여 위젯을 등록합니다.
WidgetHelpers.IncludeWidgetStyles() 기본 Azure DevOps 스타일 적용
VSS.notifyLoadSucceeded() 로드가 성공적으로 완료되었음을 프레임워크에 알

중요합니다

위젯 이름은 확장 매니페스트의 위젯 이름과 VSS.register() 일치 id 해야 합니다(5단계).

4단계: 확장 이미지 추가

확장에 필요한 이미지를 만듭니다.

  • 확장 로고: 폴더에 img 이름이 지정된 logo.png 98x98 픽셀 이미지
  • 위젯 카탈로그 아이콘: 폴더에 img 명명된 CatalogIcon.png 98x98 픽셀 이미지
  • 위젯 미리 보기: 폴더에 img 명명된 preview.png 330x160 픽셀 이미지

이러한 이미지는 사용자가 사용 가능한 확장을 찾아볼 때 Marketplace 및 위젯 카탈로그에 표시됩니다.

5단계: 확장 매니페스트 만들기

프로젝트 루트 디렉터리에 vss-extension.json를 만드세요. 이 파일은 확장의 메타데이터 및 기여를 정의합니다.

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

중요합니다

"publisher": "fabrikam"을 실제 게시자 이름으로 대체합니다. 게시자를 만드는 방법을 알아봅니다.

필수 매니페스트 속성

섹션 목적
기본 정보 확장 이름, 버전, 설명 및 게시자
아이콘 확장의 시각적 자산에 대한 경로
기여 ID, 형식 및 속성을 포함한 위젯 정의
파일 확장 패키지에 포함할 모든 파일

전체 매니페스트 설명서는 확장 매니페스트 참조를 참조하세요.

6단계: 확장 패키지 및 게시

확장을 패키지하고 Visual Studio Marketplace에 게시합니다.

패키징 도구 설치

npm i -g tfx-cli

확장 패키지 만들기

프로젝트 디렉터리에서 다음을 실행합니다.

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

이 작업은 패키지된 확장자를 포함하는 파일을 만듭니다 .vsix .

게시자 설정

  1. Visual Studio Marketplace 게시 포털로 이동합니다.
  2. 로그인하고 게시자가 없는 경우 게시자를 만듭니다.
  3. 매니페스트 파일에 사용되는 고유한 게시자 식별자를 선택합니다.
  4. vss-extension.json"fabrikam" 대신 게시자 이름을 사용하도록 업데이트합니다.

확장 업로드

  1. 게시 포털에서 새 확장 업로드를 선택합니다.
  2. .vsix 파일을 선택하고 업로드합니다.
  3. Azure DevOps 조직과 확장을 공유합니다.

또는 명령줄을 사용합니다.

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

기존 확장을 업데이트할 때 버전 번호를 자동으로 증가시키는 데 사용합니다 --rev-version .

7단계: 위젯 설치 및 테스트

테스트하려면 대시보드에 위젯을 추가합니다.

  1. Azure DevOps 프로젝트 https://dev.azure.com/{Your_Organization}/{Your_Project}로 이동합니다.
  2. Overview>Dashboards로 이동합니다.
  3. 위젯 추가를 선택합니다.
  4. 카탈로그에서 위젯을 찾고 추가를 선택합니다.

대시보드에 "Hello World" 위젯이 표시되어 구성한 텍스트가 표시됩니다.

다음 단계: 2부 를 계속 진행하여 Azure DevOps REST API를 위젯에 통합하는 방법을 알아봅니다.

2부: Azure DevOps REST API를 사용하여 헬로 월드

REST API를 사용하여 Azure DevOps 데이터와 상호 작용하도록 위젯을 확장합니다. 이 예제에서는 쿼리 정보를 가져와서 위젯에 동적으로 표시하는 방법을 보여 줍니다.

이 부분에서는 작업 항목 추적 REST API 를 사용하여 기존 쿼리에 대한 정보를 검색하고 "Hello World" 텍스트 아래에 쿼리 세부 정보를 표시합니다.

WorkItemTracking용 REST API를 사용하는 샘플 위젯이 있는 개요 대시보드의 스크린샷

1단계: 향상된 HTML 파일 만들기

이전 예제에서 빌드되는 새 위젯 파일을 만듭니다. hello-world.html을(를) 복사하여 hello-world2.html로 이름을 변경하십시오. 이제 프로젝트 구조에 다음이 포함됩니다.

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

위젯 HTML 구조 업데이트

hello-world2.html를 변경하십시오.

  1. 쿼리 데이터에 대한 컨테이너 추가: 쿼리 정보를 표시하는 새 <div> 요소를 포함합니다.
  2. 위젯 식별자 업데이트: 고유 식별을 위해 위젯 이름을 다음으로 HelloWorldWidgetHelloWorldWidget2 변경합니다.
<!DOCTYPE html>
<html>
    <head>
        <script src="sdk/scripts/VSS.SDK.min.js"></script>
        <script type="text/javascript">
            VSS.init({
                explicitNotifyLoaded: true,
                usePlatformStyles: true
            });

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

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

2단계: API 액세스 권한 구성

REST API를 호출하기 전에 확장 매니페스트에서 필요한 권한을 구성합니다.

작업 범위 추가

범위는 vso.work 작업 항목 및 쿼리에 대한 읽기 전용 액세스 권한을 부여합니다. vss-extension.json에 이 범위를 추가합니다.

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

전체 매니페스트 예제

다른 속성이 있는 완전한 매니페스트의 경우 다음과 같이 구성합니다.

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

중요합니다

범위 제한 사항: 게시 후 범위를 추가하거나 변경하는 것은 지원되지 않습니다. 확장을 이미 게시한 경우 먼저 Marketplace에서 확장을 제거해야 합니다. Visual Studio Marketplace 게시 포털로 이동하여 확장을 찾은 다음 제거를 선택합니다.

3단계: REST API 통합 구현

Azure DevOps는 SDK를 통해 JavaScript REST 클라이언트 라이브러리를 제공합니다. 이러한 라이브러리는 AJAX 호출을 래핑하고 API 응답을 사용 가능한 개체에 매핑합니다.

위젯 JavaScript 업데이트

VSS.require 호출을 hello-world2.html에서 작업 항목 추적 REST 클라이언트를 포함하도록 바꿉니다.

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

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

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

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

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

주요 구현 세부 정보

구성 요소 목적
WorkItemTrackingRestClient.getClient() 작업 항목 추적 REST 클라이언트의 인스턴스를 가져옵니다.
getQuery() 프라미스 객체로 감싸진 쿼리 정보를 검색합니다.
WidgetStatusHelper.Failure() 위젯 오류에 대한 일관된 오류 처리를 제공합니다.
projectId API 호출에 필요한 현재 프로젝트 컨텍스트

사용자 지정 쿼리 경로: "공유 쿼리"에 "피드백" 쿼리가 없는 경우 프로젝트에 있는 모든 쿼리의 경로로 바꿉 "Shared Queries/Feedback" 니다.

4단계: API 응답 데이터 표시

REST API 응답을 처리하여 위젯에서 쿼리 정보를 렌더링합니다.

쿼리 데이터 렌더링 추가

주석을 // Process query data 다음 구현으로 바꿉다.

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

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

메서드는 쿼리 메타데이터에 대한 속성이 있는 Contracts.QueryHierarchyItem 개체를 getQuery() 반환합니다. 다음은 "Hello World" 텍스트 아래에 세 가지 주요 정보를 표시하는 예제입니다.

전체 작업 예제

최종 hello-world2.html 파일은 다음과 같습니다.

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

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

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

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

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

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

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

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

5단계: 확장 매니페스트 업데이트

위젯 카탈로그에서 사용할 수 있도록 하려면 확장 매니페스트에 새 위젯을 추가합니다.

두 번째 위젯 기여 추가

vss-extension.json를 REST API를 활성화한 위젯을 포함하도록 업데이트하세요. 배열에 이 기여를 추가합니다.contributions

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

미리 보기 이미지: 이미지(330x160픽셀)를 만들고 preview2.png 폴더에 img 배치하여 사용자에게 위젯이 카탈로그의 모양을 표시합니다.

6단계: 패키지, 게시 및 공유

확장을 패키지화하고 게시하며 공유하십시오. 확장을 이미 게시한 경우 Marketplace에서 직접 다시 패키지하고 업데이트할 수 있습니다.

7단계: REST API 위젯 테스트

작동 중인 REST API 통합을 보려면 대시보드에 새 위젯을 추가합니다.

  1. Azure DevOps 프로젝트 https://dev.azure.com/{Your_Organization}/{Your_Project}로 이동합니다.
  2. 개요>대시보드를 선택합니다.
  3. 위젯 추가를 선택합니다.
  4. "Hello World 위젯 2(API 포함)"를 찾아 추가를 선택합니다.

향상된 위젯은 Azure DevOps 프로젝트의 "Hello World" 텍스트와 라이브 쿼리 정보를 모두 표시합니다.

다음 단계: 3부 를 계속 진행하여 사용자가 표시할 쿼리를 사용자 지정할 수 있는 구성 옵션을 추가합니다.

3부: 헬로 월드 구성

위젯에 사용자 구성 기능을 추가하여 2부 를 빌드합니다. 쿼리 경로를 하드 코딩하는 대신 사용자가 라이브 미리 보기 기능을 사용하여 표시할 쿼리를 선택할 수 있는 구성 인터페이스를 만듭니다.

이 부분에서는 구성 중에 실시간 피드백을 제공하면서 사용자가 특정 요구 사항에 맞게 사용자 지정할 수 있는 구성 가능한 위젯을 만드는 방법을 보여 줍니다.

변경 내용에 따라 위젯의 개요 대시보드 라이브 미리 보기 스크린샷

1단계: 구성 파일 만들기

위젯 구성은 위젯 자체와 많은 유사점을 공유합니다. 둘 다 동일한 SDK, HTML 구조 및 JavaScript 패턴을 사용하지만 확장 프레임워크 내에서는 서로 다른 용도로 사용됩니다.

프로젝트 구조 설정

위젯 구성을 지원하려면 다음 두 개의 새 파일을 만듭니다.

  1. hello-world2.html을 복사하고 구성 가능한 위젯인 hello-world3.html으로 이름을 바꾸십시오.
  2. 구성 인터페이스를 처리하는 새 configuration.html파일을 만듭니다.

이제 프로젝트 구조에 다음이 포함됩니다.

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

구성 인터페이스 만들기

이 HTML 구조를 configuration.html에 추가하면 쿼리를 선택하기 위한 드롭다운 선택기가 생성됩니다.

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

2단계: JavaScript 구성 구현

구성 JavaScript는 위젯과 동일한 초기화 패턴을 따르지만 기본 IWidget 계약 대신 IWidgetConfiguration 계약을 구현합니다.

구성 논리 추가

이 스크립트를 <head>configuration.html 섹션에 삽입합니다.

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

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

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

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

구성 계약 세부 정보

계약에는 IWidgetConfiguration 다음과 같은 주요 함수가 필요합니다.

기능 목적 호출 시
load() 기존 설정을 사용하여 구성 UI 초기화 구성 대화 상자가 열리는 경우
onSave() 사용자 입력 직렬화 및 설정 유효성 검사 사용자가 저장을 선택하는 경우

데이터 serialization: 이 예제에서는 JSON을 사용하여 설정을 직렬화합니다. 위젯은 widgetSettings.customSettings.data을(를) 통해 이러한 설정에 액세스 및 그에 따라 역직렬화를 수행해야 합니다.

3단계: 라이브 미리 보기 기능 사용

라이브 미리 보기를 사용하면 사용자가 구성 설정을 수정할 때 위젯 변경 내용을 즉시 볼 수 있으며 저장하기 전에 즉각적인 피드백을 제공할 수 있습니다.

변경 알림 구현

라이브 미리 보기를 사용하도록 설정하려면 함수 내에 다음 이벤트 처리기를 추가합니다 load .

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

구성 파일 완료

마지막 configuration.html 은 다음과 같습니다.

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

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

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

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

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

중요합니다

저장 단추 사용: 저장 단추를 사용하도록 설정하려면 프레임워크에 하나 이상의 구성 변경 알림이 필요합니다 . 변경 이벤트 처리기는 사용자가 옵션을 선택할 때 이 작업이 발생하도록 합니다.

4단계: 위젯을 구성할 수 있도록 설정

하드 코딩된 값 대신 구성 데이터를 사용하도록 2부에서 위젯을 변환합니다. 이 단계를 수행하려면 IConfigurableWidget 계약을 구현해야 합니다.

위젯 등록 업데이트

hello-world3.html에서 다음과 같이 변경하십시오.

  1. 위젯 ID 업데이트: HelloWorldWidget2에서 HelloWorldWidget3로 변경합니다.
  2. 다시 로드 함수 추가: 계약을 구현합니다 IConfigurableWidget .
return {
    load: function (widgetSettings) {
        // Set your title
        var $title = $('h2.title');
        $title.text('Hello World');

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

구성 데이터 처리

getQueryInfo 하드 코딩된 쿼리 경로 대신 구성 설정을 사용하도록 함수를 업데이트합니다.

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

    return WidgetHelpers.WidgetStatusHelper.Success();
}

위젯 수명 주기 차이

기능 목적 사용 지침
load() 초기 위젯 렌더링 및 일회성 설정 많은 작업, 리소스 초기화
reload() 새 구성으로 위젯 업데이트 간단한 업데이트, 데이터 새로 고침

성능 최적화: 한 번만 실행해야 하는 비용이 많이 드는 작업과 reload() 구성이 변경되면 빠른 업데이트에 사용합니다load().

(선택 사항) 자세한 정보를 위한 라이트박스 추가

대시보드 위젯은 공간이 제한되어 포괄적인 정보를 표시하기가 어렵습니다. 라이트박스는 대시보드에서 벗어나지 않고 모달 오버레이에 자세한 데이터를 표시하여 세련된 솔루션을 제공합니다.

위젯에 라이트박스를 사용하는 이유는 무엇인가요?

이익 설명
공간 효율성 자세한 보기를 제공하면서 위젯을 간결하게 유지
사용자 환경 추가 정보를 표시하는 동안 대시보드 컨텍스트 유지 관리
점진적 공개 위젯에 요약 데이터 표시, 요청 시 세부 정보
반응형 디자인 다양한 화면 크기 및 위젯 구성에 맞게 조정

클릭 가능한 요소 구현

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

Lightbox 기능 만들기

위젯 JavaScript에 다음 lightbox 구현을 추가합니다.

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

lightbox 스타일 추가

위젯 HTML <head> 섹션에 라이트박스에 대한 CSS 스타일을 포함합니다.

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

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

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

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

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

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

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

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

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

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

향상된 위젯 구현

라이트박스 기능이 포함된 완전한 향상된 위젯:

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

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

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

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

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

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

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

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

접근성 고려 사항: 라이트박스가 키보드에 액세스할 수 있고 화면 읽기 프로그램을 위한 적절한 레이블이 포함되어 있는지 확인합니다. Azure DevOps의 기본 제공 접근성 기능을 사용하여 테스트합니다.

중요합니다

성능: Lightbox는 빠르게 로드되어야 합니다. 모든 데이터를 처음부터 가져오는 대신 라이트박스가 열릴 때만 자세한 데이터를 지연 로드하는 것을 고려하십시오.

5단계: 확장 매니페스트 구성

구성 가능한 위젯과 해당 구성 인터페이스를 모두 확장 매니페스트에 등록합니다.

위젯 및 구성 기여 추가

다음 두 가지 새 기여를 포함하도록 업데이트 vss-extension.json 합니다.

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

구성 기여 요구 사항

속성 목적 필수 값
type 기여를 위젯 구성으로 식별 ms.vss-dashboards-web.widget-configuration
targets 구성이 표시되는 위치 ms.vss-dashboards-web.widget-configuration
uri 구성 HTML 파일 경로 구성 파일 경로

위젯 대상 지정 패턴

구성 가능한 위젯의 경우 배열에 targets 구성에 대한 참조가 포함되어야 합니다.

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

경고

구성 단추 표시 유형: 위젯이 구성 기여를 제대로 대상으로 지정하지 않으면 구성 단추가 나타나지 않습니다. 게시자 및 확장 이름이 매니페스트와 정확히 일치하는지 확인합니다.

6단계: 패키지, 게시 및 공유

구성 기능을 사용하여 향상된 확장을 배포합니다.

첫 번째 게시인 경우 6단계: 패키지, 게시 및 공유를 수행합니다. 기존 확장의 경우 Marketplace에서 직접 다시 패키징하고 업데이트합니다.

7단계: 구성 가능한 위젯 테스트

위젯을 추가하고 구성하여 전체 구성 워크플로를 경험하세요.

대시보드에 위젯 추가

  1. https://dev.azure.com/{Your_Organization}/{Your_Project}로 이동합니다.
  2. Overview>Dashboards로 이동합니다.
  3. 위젯 추가를 선택합니다.
  4. "Hello World 위젯 3(구성 포함)"을 찾고 추가를 선택합니다.

위젯에 설치가 필요하므로 구성 프롬프트가 표시됩니다.

카탈로그의 샘플 위젯이 있는 개요 대시보드의 스크린샷.

위젯 구성

다음 방법 중 하나를 통해 구성에 액세스합니다.

  • 위젯 메뉴: 위젯을 마우스로 가리키고 줄임표(⋯)를 선택한 다음 구성
  • 대시보드 편집 모드: 대시보드에서 편집 을 선택한 다음 위젯의 구성 단추

중앙에 라이브 미리 보기가 있는 구성 패널이 열립니다. 드롭다운에서 쿼리를 선택하여 즉시 업데이트를 확인하고 저장 을 선택하여 변경 내용을 적용합니다.

8단계: 고급 구성 옵션 추가

사용자 지정 이름 및 크기와 같은 더 많은 기본 제공 구성 기능을 사용하여 위젯을 확장합니다.

이름 및 크기 설정 활성화

Azure DevOps는 다음과 같은 두 가지 구성 가능한 기능을 제공합니다.

특징 매니페스트 속성 목적
사용자 지정 이름 isNameConfigurable: true 사용자는 기본 위젯 이름을 재정의할 수 있습니다.
여러 크기 여러 supportedSizes 항목 사용자는 위젯의 크기를 조정할 수 있습니다.

향상된 매니페스트 예제

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

구성된 이름 표시

사용자 지정 위젯 이름을 표시하려면 다음을 사용하도록 widgetSettings.name위젯을 업데이트합니다.

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

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

        return getQueryInfo(widgetSettings);
    }
}

확장을 업데이트한 후 위젯 이름과 크기를 모두 구성할 수 있습니다.

위젯 이름 및 크기를 구성할 수 있는 위치를 보여 주는 스크린샷

이러한 고급 구성 옵션을 사용하도록 확장을 다시 패키지하고 업데이트합니다.

축하합니다! 라이브 미리 보기 기능 및 사용자 지정 옵션을 사용하여 구성 가능한 완전한 Azure DevOps 대시보드 위젯을 만들었습니다.