Using Azure Devops Service Connections in dashboard widgets

Using queryServiceEndpoint

Intro

So, this is the reason for creating this blog. My frustration with not being able to find any documentation on how to use this feature. Checking the Azure DevOps REST API documentation, I can see this option is available, but there didn't seem to be anyone on the internet who had a working implementation. And the documents from Microsoft didn't help either.
So in the end, after some trial and error, I figured this out and decided it might be useful for others to have an example to work from.

Why this setup is needed

Normally, you could connect the widget, which is running in the browser, directly to a REST API, have it consume the information and then show the values you extracted. However, this means you expose that REST connection and all details necessary to access that API in source code that is run in the browser. If this can be safely done, the party you are consuming your data from will have to set your domain as a valid location to consume their REST API. This is set up using CORS.
If this is not applicable (e.g. your connection to the API uses authentication which you will not want to expose to the browser), you can instead use a Service Connection which will act as a proxy between the widget and the actual REST API you want to consume. Authentication between the widget and the Service Connection is handled through the VSS module from the SDK, so you don't need to expose any sensitive information to the browser.

Software architecture

The example here works on the following basis:
  • I want to display information available on a REST API in a dashboard widget
  • The REST API isn't directly accessible to the widget because CORS isn't implemented
  • The REST API is however accessible through a Service Connection with an authorized account
So the solution is, Widget calls Endpoint Proxy, Endpoint calls REST service. And the returned data is rendered on the widget.

Calling the Service Connection (Endpoint)

In Azure DevOps, the Service Endpoints have been renamed to Service Connection. Since the actual widget was built for TFS 2018, you'll see the word endpoint in the code samples.
Provided you are using the VSS SDK, you need two references:
VSS.require(["TFS/DistributedTask/TaskAgentRestClient"], function (taskRestClient)
and
var $context = VSS.getWebContext();
These give you access to the Azure DevOps rest-client and retrieve the context under which the Widget is being executed.

Using the rest-client, the following code can be executed:
    taskRestClient.queryServiceEndpoint({
        endpointUrl: "{{endpoint.url}},
        resultSelector: "jsonpath:$.result",
        requestVerb: "POST",
        endpointId: endpointId
    }, $context.project.id).then(
        function (resultArray) {
            // Do something with the result
        },
        function (error) {
            console.error(error);
            // Handle the error
        }
    );
As should be clear, you need the endpointId and projectId. The latter comes from the context, the first should be part of the configuration.
Executing a query against the Service Connection is done in the form of a POST, the body must contain the resultSelector, endpointUrl (which can be adapted here), the requestVerb (POST) and endpointId.

Retrieving the available Service Connections can be done in the widget configuration with this call
taskRestClient.getServiceEndpoints($context.project.id)

Full implementation

The following is a full example of this code, minus the HTML for the widgets.
Based on the Service Connection, the REST API is called for Entity with the configured ID (entityId). So we are calling <ServiceConnectionURL>/Entity/<EntityId>

Configuration

VSS.init({
    explicitNotifyLoaded: true,
    usePlatformStyles: true
});

VSS.require(["TFS/Dashboards/WidgetHelpers", "TFS/DistributedTask/TaskAgentRestClient"]
            , function (WidgetHelpers,  taskRestClient) {
    WidgetHelpers.IncludeWidgetConfigurationStyles();
    VSS.register("MyWidget.Configuration", function () {
        var $endpointDropdown = $("#selectEndpoint");
        var $entityText = $("#entityID");
        var $context = VSS.getWebContext();

        var endpointClient = taskRestClient.getClient();
        endpointClient.getServiceEndpoints($context.project.id).then(
            function (endpoints) {
                $endpointDropdown.empty();
                $.each(endpoints, function (i, endpoint) {
                    $endpointDropdown.append($('

Widget

VSS.init({
    explicitNotifyLoaded: true,
    usePlatformStyles: true
});

VSS.require(["TFS/Dashboards/WidgetHelpers", "TFS/DistributedTask/TaskAgentRestClient"], function (WidgetHelpers, taskRestClient) {
    WidgetHelpers.IncludeWidgetStyles();
    VSS.register("MyWidget", function () {
        var endpointClient = taskRestClient.getClient();

        return {
            load: function (widgetSettings) {
                var settings = JSON.parse(widgetSettings.customSettings.data);
                if (settings && settings.endpoint && settings.entityId) {
                    GetIncidentCountAsync(settings.endpoint, settings.entityId, endpointClient);
                }
                return WidgetHelpers.WidgetStatusHelper.Success();
            },
            reload: function (widgetSettings) {
                var settings = JSON.parse(widgetSettings.customSettings.data);
                if (settings && settings.endpoint && settings.entityId) {
                    GetIncidentCountAsync(settings.endpoint, settings.entityId, endpointClient);
                }
                return WidgetHelpers.WidgetStatusHelper.Success();
            }
       }
    });
    VSS.notifyLoadSucceeded();
});

async function GetIncidentCountAsync(endpointId, entityId, restClient) {
    var $context = VSS.getWebContext();
    var $incidentCount = $('div.big-count');
    restClient.queryServiceEndpoint({
        endpointUrl: "{{endpoint.url}}/Entity/" + entityId,
        resultSelector: "jsonpath:$.result[0].rowCount",
        requestVerb: "POST",
        endpointId: endpointId
    }, $context.project.id).then(
        function (resultArray) {
            $incidentCount.text(resultArray[0]);
        },
        function (error) {
            console.error(error);
            $incidentCount.text("ERROR");
        }
    );
}

Comments

  1. This comment has been removed by the author.

    ReplyDelete
  2. This comment has been removed by the author.

    ReplyDelete
  3. Hello !
    Im on a issue.
    I can't set the dropDown.val with stored setting:
    $endpointDropdown.val(settings.endpoint) not worked for me.
    I checked the settings.endpoint variable, and it is ok so the issue is with the select element.
    Did you got some issuse with this?
    Thanks

    ReplyDelete
    Replies
    1. I think you mean that when you (re-)open the configuration of the widget, it doesn't display the correct setting?
      Yes, I had an issue here as well, although I do not fully remember how it was resolved.
      My guess is the issue is caused by the population of the dropdown (which is handled async on calling 'getServiceEndpoints' ) completes -after- the load of the settings happens.

      Delete
    2. This comment has been removed by the author.

      Delete
    3. Yes, when the configuration re-opens it doesn't display correct setting to the endopoints dropdown item, the rest is ok.
      You are right, this happens because getServiceEndpoints returns a Promise.
      I'm still trying to figure it out a solution. If you'll remember how you did to solve this, ill be very thankful.
      By the way, thanks for your time and your reply

      Delete
  4. Hello team, Connections in dashboard widgets with the help of Azure DevOps was a useful content and your article was very easy to understand and the code snipped was very to execute the program and I have faced a bit of difficulty in the drop down list! Great blog thank you for the effort!!! For getting DevOps Training in Chennai contact us...

    ReplyDelete
  5. This comment has been removed by the author.

    ReplyDelete
  6. This comment has been removed by the author.

    ReplyDelete
  7. This comment has been removed by a blog administrator.

    ReplyDelete

Post a Comment

Popular posts from this blog

Running Azure DevOps container agents on OpenShift

NuGet Release and Pre-Release pipeline