Skip to main content

 

 

Coupa Success Portal

Create an Embedded Panel App

Introduction

Panel Apps allow customers to display data from external sources within a UI panel on a given Coupa page. This data can be context-specific and can be automatically or manually refreshed. For example, when a Supplier page loads in Coupa, a Panel App on that page can automatically get data from an external source via API that pertains to that particular Supplier and display the data in Coupa.

Create a Panel App by going to Setup > Platform > Installed Apps and click the Create button and then the option: Create New Panel App. The key to creating and configuring a Panel App is through it's Descriptor, a JSON format set of parameters for your App.

Here's an example Panel App on the Coupa Homepage.

Example panel app on the Coupa homepage

You can create a Panel App by going to Setup > Platform > Installed Apps. Click the Create button and select Create New Panel App.

Requirements

Starting in R29, Panel Apps require at least TLS v1.2 to make any outbound connections to 3rd party APIs.

Panel App basics

Panel Apps are built using an app descriptor that uses JSON format. The descriptor consists of two main properties: data and blocks. Data is processed using JQ v1.6.

Property Description
slot

Specifies the page type where the panel will be rendered. Current options are:

  • user.home (Coupa homepage: / and /user/home)
  • suppliers.show (supplier details page: /suppliers/:id/record)
  • contracts.show (contract details page: /contracts/show/:id)
  • requisition_headers.edit (Coupa shopping cart: /requisition_headers/{id}/edit)
{
   "slot": "user.home"
 }

Only one slot can be specified per panel app. To have the app appear on multiple pages, create the app again with the desired slot value.

data

The data attribute describes how to fetch the desired data that is rendered in the panel.  A JSON object that describes what data to fetch to render the panel. The values in this object will be URLs that Coupa will send a request to in order to retrieve data. The responses from all of these data requests will be merged into one Hash, aliased under the key name given to them in this object.

"data": {
    "study_queue": "https://www.wanikani.com/api/user/{user_ID}/study-queue",
    "level_progression": "https://www.wanikani.com/api/user/{user_ID}/level-progression"
  }
}
context


Each slot provides contextual detail about the page on which it is displayed. For example a panel app being displayed on a supplier show page will have access to contextual data about that specific supplier. You can use this contextual information in your data objects, but not in blocks. So when a panel app renders on the show page for “ACME Supplier” the API call can be customized based on the supplier’s name, coupa ID, custom field values, and other properties of that supplier. 

An API call to the NY Times article search for recent articles about a supplier would look like this:

"data": {
    "nyt_data": {
      "uri": "https://api.nytimes.com/svc/search/v2/articlesearch.json?
      q={context.supplier.name}&api-key={properties.api_key}"
  }
}      

Contact Coupa for an updated list of what contextual data is available for each slot.

Parameterize api calls (properties)

An array of properties that a user/administrator has to furnish after enabling your panel app. If your app/API requires a unique API key, or login/password for each Coupa customer that enables it, here's how you provide that option.

An example block would be:

"properties": {
    "username": {
      "type": "string"
    },
    "password": {
      "type": "string"
    }
  }

 You then reference your properties in the data blocks like this:

"data": {
    "my_data": {
      "method": "post",
      "uri": "https://someurl.com/EVToken",
      "body": "grant_type=password&username={{ properties.username }}&
      password={{ properties.password }}"
    }
  }

 When a Coupa admin enables a new panel app, they will be prompted to fill in all “properties” fields specified in your app descriptor.

launch

Adds a Launch Button that can be used when a Panel App does not need to be displayed immediately. Rather than the Panel App automatically reaching out to the 3rd party and rendering the data, it only reaches out if the end user manually clicks the Launch Button.

This JSON object includes description, button_text and help_text. For example:

"launch": {
    "description": "PretendCo Tax Calculator Service",
    "button_text": "Launch Calculator",
    "help_text": "Tax Calculator integrates with Coupa to help you calculate international 
    taxes, right in the cart."
  }

 How to use launch:

  • Use if the data in the Panel App is only relevant for specific business cases within the page
  • Add at the same level as slot, data, etc, but not in blocks
allow_refresh

Adds a Refresh Button that can be used when data on the page changes and affects the data rendered in the Panel App. This is handy for your Cart Panel App when a Req line is updated. The user can click Refresh Button to update the data in the Panel App. 

This is boolean option is used to display a refresh button on the Panel App in the app descriptor: 

"allow_refresh": true

How to use allow_refresh:

  • Use if the data required by the app can be updated by the user and the app needs the new data

When clicked, the button refreshes the contents on that app. This field should be added on the same level as “slot”, “data”, “launch” etc, but not in blocks. The refresh button is shown at the top of panel application.
 

blocks

The blocks attribute describes how to render data in the panel. It is an array of JSON objects ("blocks") of various types that each describe a separate visualization. Blocks have access to the data that was previously sent and use it to create visualizations.

error_blocks The error blocks attribute works similarly to the blocks attribute but is used to create error messages when there's a problem getting or rendering the data.

Fetching Data

We will allow apps to make up to 5 separate HTTP calls in order to fetch data for the app. These will be specified using the data attribute in the descriptor. API calls should return either JSON (preferred) or XML.

Panel App block types

Panel App renders that data on an existing Coupa page using different types of UI elements known as blocks. Coupa supports the following block types:

  • Rich text (including images)
  • Fields (essentially key-value pairs)
  • Number
  • Money
  • Table
  • Pie chart
  • Line/Bar/Column graph
  • Launch
  • Refresh

App configuration

Attribute Description

type

Identifies the type of block. For example, fields, text, or bars. This will determine how the block is rendered in the panel on the UI, and also the required format for the data.

data

This JSON object will have different properties depending on strategy.

JQ

  • type must equal "jq"

  • jq is a string that contains a JQ script. The JQ script is fed the merged data structure from the data attribute as its input and must produce a JSON structure that fits the required format for the block type.

Processing data with JQ

For example, the following JQ script could be used to extract several specific pieces of data and repackage them in another JSON object:

{
   "username": ".study_queue.user_information.username",
   "radicals": ".level_progression.requested_information.radicals_progress",
   "kanji": ".level_progression.requested_information.kanji_progress"
}

This would result in the following JSON object being produced:

{
   "username": "example_user",
   "radicals": 0,
   "kanji": 0
}

More information:

other properties

Different blocks types may have other properties specific to that block type.

Panel App block types

Text block

Attribute Description

type

Must equal text

template

The text block type uses a Liquid Markdown template. The template is first parsed as Liquid to interpolate the data that's returned from the requests. Then, the template is parsed again as Markdown to generate the HTML.

Markdown provides a safe method of generating HTML, and for security reasons, Coupa blocks inline HTML in the template. There's no character limit other than the max limit of the data column that stores the block's configuration.

Markdown allows for headers, inline emphasis formatting, lists, images, links, and blockquotes. Coupa also supports syntax highlighting and tables. 

For detailed info about working with Liquid and Markdown, see Liquid template language and Mastering Markdown.

data

The data strategy must produce a single JSON object. All of the keys in the object will be available to the Liquid template as variables.

{
   "foo": "bar",
   "fizz": {
      "buzz": "buzz"
   }
}

Fields block 

Attribute Description

type

Must equal field

data

The data strategy must produce an array of JSON objects with label and value attributes. When rendering, the label will be used as the field label and the value will be used as the field value.

[
   {
      "label": "Foobar",
      "value": "fizzbuzz"
   },
   {
      "label": "Lorem Ipsum",
      "value": 42
   }
]
title An optional title that will appear above the list of fields.

Table block

Attribute Description

type

Must equal table

data

The data strategy must produce an array of arrays.

[
   ["col1", "col2", "col3"],
   [1, 2, 3],
   [4, 5, 6]
]
headers A boolean, defaulting to false. If true, the first row of data will be treated as headers for the table and will receive special styling.
title An optional title that will appear above the table.
description An optional description that will appear below the field in smaller text.

Bar/Line block 

Attribute Description

type

Must equal bar or line

data

The data strategy must produce an array of arrays. Unlike the Table Block, the data for bar/line graphs will be column-oriented.

Each array represents a single series of data that will be displayed. The first element is the name of the series, then the remaining elements are the points in the series.

[{
    "name": "Year 1800",
    "data": [107, 31, 635, 203, 2]
 }, {
    "name": "Year 1900",
    "data": [133, 156, 947, 408, 6]
 }, {
    "name": "Year 2000",
    "data": [814, 841, 3714, 727, 31]
 }, {
    "name": "Year 2016",
    "data": [1216, 1001, 4436, 738, 40]
}]
axis Allows configuration of the labels on the x and y axes.
title An optional title that will appear above the graph.
description An options description that will appear below the graph in smaller text.

Pie block

Attribute Description

type

Must equal pie

data

The data strategy must produce a column-oriented array of arrays as described in the Bar/Line Block section.


[{
    name: 'Chrome',
    y: 20.41,
    sliced: true,
    selected: true
  }, {
    name: 'Internet Explorer',
    y: 11.84
  }, {
    name: 'Firefox',
    y: 10.85
  }, {
    name: 'Edge',
    y: 4.67
  }, {
    name: 'Safari',
    y: 4.18
  }, {
    name: 'Sogou Explorer',
    y: 1.64
  }, {
    name: 'Opera',
    y: 2.8
  }, {
    name: 'Other',
    y: 2.61
}]
title An optional title that will appear above the chart.
description An options description that will appear below the chart in smaller text.

Money block

Attribute Description

type

Must equal money

data The data strategy must produce a JSON object with the following structure:
  • Amount a JSON Float to determine the amount of currency.
  • Currency a string containing the ISO 4217 currency code of the currency.
{
  "amount": 42.0,
  "currency": "USD"
}
Title An optional title that will appear above the number.
Description An optional description that will appear below the number in smaller text.

Numbers block

Attribute Description

Type

Must equal numbers

Data

The data strategy must produce a single JSON integer or Float.

42.0
Decimal The number of decimals to display after the decimal point. Defaults to 0.
Title An optional title that will appear above the number.
Description An optional description that will appear below the number in smaller text.Example Panel Apps

API details

Authentication

Coupa supports the following authentication types:

Method Example
Standard API key
"data": {
    "nyt_data": {
      "uri": "https://api.nytimes.com/svc/search/v2/articlesearch.json?q={context.supplier.name}
            &api-key={properties.api_key}"
    }
  }
Basic Auth
"data": {
    "trip_data": {
      "uri": "https://someurl.net/trips",
      "headers": {"Authorization":"basic YOUR_BASE64_ENCODED_TOKEN_HERE"}
    }
  }
Request for short lived token using “before_data” request
"before_data": {
    "auth": {
      "method": "post",
      "uri": "https://someurl.com/MYToken",
      "body": "grant_type=password&username={{ properties.username }}&password={{ properties.password }}"
    }
  },
  "data": {
    "risk_data": {
      "uri": "https://someurl.com/MYData?integration_id=%22ID{context.supplier.id}%22",
      "headers": {
        "Authorization": "bearer {{ before_data.auth.access_token }}"
      }
    }
  }

API URLs

  • URLs must be HTTPS
  • URLs must be accessible by Coupa servers (i.e. a HEAD request must succeed)
  • URLs must successfully parse as valid Liquid templates. This is to enable interpolation

API request details

  • When interpolating data into the URLs using Liquid, the result must be a valid URL
  • Response code must be 200 or 204
  • Remote server must respond within a timeout period of 5 minutes
  • Response Content-Type must be either application/json or application/xml

Buttons

You can configure a Launch Button (launch)and a Refresh Button (allow_refresh) for your panel apps. While the functionality looks and feels like a block, technically buttons are not blocks. You can find details on buttons in the Panel App basics section of this article. for more info.

Panel App payloads

As of R29, you can build Panel Apps for the following pages:

  • Homepage: / and /user/home
  • Suppliers page: /suppliers/:id/record
  • Contracts page: /contracts/show/:id
  • Cart: /requisition_headers/{id}/edit
  • Projects home page: projects.show
  • Sourcing event Settings page: quotes/requests.show

Each Panel App type has its own respective context payload.  Below are the fields that will be provided in the Panel Apps API payload, which are used as context to match and find data.

Page Available fields
All panel apps user_instance (i.e. [domain name].coupahost.com) is included in the payload.
Homepage id, email, fullname, login, employee_number
Supplier page

id, name, display_name, duns, number, tax_id, website, status,

custom_fields: object.supplier.custom_field_values_hash

Contract Page id, parent_contract_id, name, number, version, supplier_id, start_date, status
Cart The Cart Panel App payload will contain the same fields as the Requisition header API payload
Projects All Project system fields and custom fields
Sourcing event

id, event_name, event_commodity, project_id, supplier_id, and any custom fields and tags

Panel App examples

Coupa Cats

This simple app shows a random image and fact related to cats.

The Coupa Cats Panel App

This is the code that drives the app.

{
  "properties": {},
  "slot": "user.home",
  "before_data": {},
  "data": {
    "cat_data": {
      "uri": "https://aws.random.cat/meow"
    },
    "fact_data": {
      "uri": "https://cat-fact.herokuapp.com/facts/random"
    }
  },
  "blocks": [
    {
      "type": "text",
      "data": {
        "type": "jq",
        "jq": ".cat_data"
      },
      "text": "![random cat pic from aws.random.cat]({{ file }}){:height=\"450px\"}"
    },
    {
      "type": "text",
      "data": {
        "type": "jq",
        "jq": ".fact_data"
      },
      "text": "## Here's an interesting fact about cats:\n\n{{ text }}"
    }
  ]
} 

New York Times article search

This app adds the recent news about the supplier from the New York Times on the supplier's page. You'll need to get an NYT API key to make this one work.

nyt-article-search.png

This is the code that drives the app.

{
  "properties": {
    "api_key": {
      "type": "string"
    }
  },
  "slot": "suppliers.show",
  "before_data": {
  },
  "data": {
    "nyt_data": {
      "uri": "https://api.nytimes.com/svc/search/v2/articlesearch.json?q={context.supplier.name}&api-key={properties.api_key}"
    }
  },
  "blocks": [
    {
      "type": "text",
      "data": {
        "type": "jq",
        "jq": "{ \"docs\": .nyt_data | .response | .docs }"
      },
      "text": "![nyt logo](http://www.transervice.com/wp-content/uploads/2018/06/the-new-york-times-4.png){:height=\"100px\"}"
    },
    {
      "type": "text",
      "data": {
        "type": "jq",
        "jq": "{ \"docs\": .nyt_data | .response | .docs }"
      },
      "text": "1. [{{ docs[0].headline.main }}]({{ docs[0].web_url }}) \n2. [{{ docs[1].headline.main }}]
                  ({{ docs[1].web_url }})\n3. [{{ docs[2].headline.main }}]({{ docs[2].web_url }})\n4. 
                  [{{ docs[3].headline.main }}]({{ docs[2].web_url }})\n5. [{{ docs[2].headline.main }}]
                  ({{ docs[4].web_url }})\n"
    }
  ]
} 

Weather Panel App

This example app shows the graphical representation of temperature aspects of a location. You'll need to get an API key to make this one work.

weather-app-example.png

This is the code that drives the app.


{
  "properties": {
    "api_key":{
      "type": "string"
    }
  },
  "slot": "user.home",
  "data": {
    "chicago_data": {
      "method": "get",
      "uri": "https://api.openweathermap.org/data/2.5/weather?q=Chicago&appid={properties.api_key}&units=imperial"
    }
  },
  "blocks": [
    {
      "type": "bar",
      "title": "Here's the weather in Chicago",
      "data": {
        "type": "jq",
        "jq": "[{name:\"Temp\",data:[.chicago_data.main.temp]},{name:\"WindSpeed\",data:[.chicago_data.wind.speed]},
               {name:\"Humidity\",data:[.chicago_data.main.humidity]}]"
      },
      "axes": {
        "xAxis": {
          "labels": {
            "enabled": false
          }
        },
        "yAxis": {
          "title": {
            "text": ""
          },
          "max": 100
        }
      }
    },
    {
      "type": "line",
      "title": "Here's the weather in Chicago",
      "data": {
        "type": "jq",
        "jq": "[{name:\"Minimum, Current, MaxTemp\", data:[[.chicago_data.main.temp_min],[.chicago_data.main.temp],
               [.chicago_data.main.temp_max]]}]"
      },
      "axes": {
        "xAxis": {
          "labels": {
            "enabled": false
          }
        },
        "yAxis": {
          "title": {
            "text": ""
          },
          "max": 70
        }
      }
    }
  ]
}

Panel App API example

Below is the App descriptor used for an automated test. We stub out the API response to return exactly this:

[  
  {  
    "data_title":"Title",
    "data_number":42,
    "second_score":88
  }
]
 
{
  # Here is where you'd setup client specific fields. Like login/password. The customer would be prompted to fill in
  # those values once, when activating the application
  "properties": {},
  # This is the page where the app gets displayed. See the "slot" property for possible values.  
  "slot": "suppliers.show",   
  # Some APIs require a bearer token, or a 2-step process, this is where you handle that.
  "before_data": {}, 
  "data": {
    "coupa_data": {
      # You can add context specific values to this API call, {{context.contract.supplier_name}} for example
      "uri": "http://fake.test", 
      # This works intuitively, just specify your API headers here, Bearer token, params, etc.
      "headers": {} 
    }
  },
  "blocks": [
    {
      "type": "number",
      # This is hard-coded title for this block, cant use api data, or contextual data here  
      "title": "Title for number block",
      # "coupa_data" was the API response above, .[0] returns the stuff in the {}, ".data_number" gets the value, 
      # in this case "42"
      "data": {
        "type": "jq",
        "jq": ".coupa_data | .[0] | .data_number"  
      }
    }, 
      # So this block renders a specially formatted number block, with the number "42" in large bold font, 
      # and a text label/title of "Title for number block"
    {
      # Fields block has some of the strictest requirements for data, It displays all data in key -> value pairs
      "type": "fields",
      "data": {
        "type": "jq",
        "jq": ".coupa_data | .[] | to_entries | map({ label: .key, value: .value })" 
      }
    },
    {
      # For this block, we are just going to pull the hash out from the [] and use the values inside it
      "type": "text",
      "data": {
        "type": "jq",
        "jq": " .coupa_data | .[0]"
      },
      # Text blocks gives you a lot of flexibility for displaying data from the API response in context.
      # The text can be formatted just like markup, so you can create links, tables, render/resize images, etc.   
      "text": "{{ data_number }}  is the data_number" 
    },
    {
      # Setting up the bar graph is pretty tricky, maybe work with us if you want to go this route.
      # But this example creates a bar graph with values/labels bar sizes based on the values in the API response
      "type": "bar",
      "title": "Bar graph with fake data",
      "data": {
        "type": "jq",
        "jq": "[{name: \"Overall Score\", data: [.coupa_data | .[0].data_number]}, 
                {name: \"Environment score\", data: [.coupa_data | .[0].second_score]}]" 
      },
      "axes": {"xAxis": {"labels": { "enabled": false}}, "yAxis": {"title": { "text": ""}, "max": 100} }
    }
  ]
}

Test in the partner's instance

To test this in Coupa, you will need to be on version R25.0 or higher.  In the end, your App will be a part of the App Directory and not a custom Panel App, but using these steps you can test on your own before Coupa officially adds the App to the App Directory.

  1. Go to Setup
  2. Under Platform, click 'Installed Apps'
  3. Click the 'Create' button, then select 'Create New Panel App'
  4. Create the App 
  5. Add a name
  6. In the descriptor section, include the code listed above

Once complete, if no errors a Panel App will be displayed on each Supplier page.

  • Was this article helpful?