Skip to main content

Custom HTML/CSS/JS views

XetHub supports rendering custom views that stay up-to-date with your repository files. This allows you to build visualizations of your files and repository that anyone visiting can see and interact with, making it easy to share high-level contextual information. These visualizations can be displayed on file and folder views, as well as commit-level and difference views, to shown how your project has evolved over time.

  • Supported data types: any
  • Supported formats: HTML, with CSS and JavaScript

Not a frontend pro? Develop a view using our in-browser Python editor, or reference our View Gallery for a list of existing views that you can easily use with a single line.


How it works

Custom views can be associated with one or more files in a repository and are shown whenever the associated files are displayed. Each custom view is defined by a YAML configuration file and a HTML file, which live in a directory in .xethub/custom_views. For example, if your view is named "MyView":

  • .xethub/custom_views/MyView/view.yml contains the view configuration.

  • .xethub/custom_views/MyView/index.html contains the view that will be loaded in the user's browser.

  • .xethub/custom_views/MyView/ contains all the rest of the supporting files for index.html.

    Screenshot of custom_views directory structure

The view is loaded as an iframe within the parent window containing the XetHub page for the file being displayed. Views are authored to display data given to the view at runtime in the user's browser. In order to bind to data or configure the view at runtime, the view must use the postMessage API.

Where custom views appear

The "scope" in the custom view configuration tells XetHub where to display the view. This can be an extension, file, or directory path.

  • A scope of "*.xyz" will display the view on pages that render files ending with a .xyz extension.
  • A scope of "/" will display the view at the root of the repo.
  • A scope of "/foo", "foo/", "foo", or "/foo/" will display the view on pages that render the directory /foo/.
  • A scope of "foo/bar.xyz" will display the view on pages that render the file /foo/bar.xyz.
  • All fully qualified (non-wildcard) paths are treated as absolute paths within the repo.

By default, custom views will also be shown on differences within the repo (viewing commits, compares, or pull requests) when applicable.

What data custom views can access

By default, the data for the custom view returned via the postMessage API matches the scope. For a single-file scope, including file extensions with wildcard matching, this implies a single data URL.

To bind to other data files (instead of the default), you can supply a "data" parameter that allows for one or more items following the same pattern as "scope". Note that the "*.xyz" wildcard matching behaves differently for data than for scope: a single instance of the view will be displayed on each matching file for the scope, but all files matching the data parameter will be provided to each instance of the view.

Percent substitution

You can use a % to substitute the current scope (viewed file). For instance, if you want to add a suffix of .summary to find the data file corresponding to the currently viewed file, use %.summary as the data parameter.

Creating a view

View configuration

The view configuration (view.yml) is defined through a YAML file in the custom view folder, dictating where and how the view will be displayed.

The top level options recognized in this file are:

  • uses: a Custom View or template to inherit from. See Inheriting from a View or Template.
  • scope: a list of strings matching extension, file, or directory paths within the repo. See Where custom views appear.
  • data: a list of strings matching extension, file, or directory paths within the repo. See What data custom views can access.
  • override: an optional boolean to tell XetHub whether this view should override the main view of the file:
    • Default/unspecified: show the view above the main view, alongside any other custom views.
    • override: false: same as default/unspecified.
    • override: true: replace the main view for the file with this custom view.
note

There can only be one "override" view for a given file or directory. If more than one view's scope matches the file or directory and both are marked as override, one will be chosen arbitrarily and an error will be shown.

Inheriting from a view or template

You can use another custom view as the basis for your view. This allows you to pick up relevant portions of an existing view (view.yml properties and even the HTML if desired) without having to modify them. Any modifications you do make to your own view will override what is specified in the template or view that you inherit from. Note that other arbitrary files from the inherited view won't get picked up if you override the HTML, so if you copy the index.html file into your view you will also need to copy any other JavaScript or CSS assets provided alongside.

To extend from another existing view, use the uses option in the view configuration:

To inherit from another Custom View with a view.yml:

  • uses: ./.xethub/custom_views/{viewname}/view.yml will inherit from a custom view in the current repository (the latest version in the default branch).
  • uses: {owner}/{repo}/.xethub/custom_views/{viewname}/view.yml will inherit from a custom view in a named repository (the latest version in the default branch).
  • To specify a branch, tag, or commit for either of the paths above, append @{ref} where ref can be a branch name, tag, or commit hash.

To inherit directly from an HTML web app hosted anywhere:

  • uses: ./path/to/index.html will inherit from an index.html file in the current repository (the latest version in the default branch).
  • uses: {owner}/{repo}/.xethub/custom_views/{viewname}/view.yml will inherit from a custom view in a named repository (the latest version in the default branch).
  • To specify a branch, tag, or commit for either of the paths above, append @{ref} where ref can be a branch name, tag, or commit hash.
  • uses: https://path/to/some/existing/view/view.html will inherit from a view at an arbitrary URL.

Note that arbitary web apps are unlikely to work out of the box because the app needs to be aware of the postMessage API used to fetch data and control view parameters. However, adapting an existing web app to use the Custom Views API is possible and can typically be done with as little as 10 lines of code.

postMessage API

Custom views use window.parent.postMessage(https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage) to communicate with the parent window. The structure of the API is:

  • Custom view creates a command with parameters as a JavaScript object like:

    {command: "<Command Goes Here", ...<Optional Parameters Go Here>}
  • Custom view sends commands to the parent window with:

    window.parent.postMessage(command, {targetOrigin: "xethub.com"})
note

If you are using a XetHub deployment other than xethub.com, change targetOrigin above to the URL of the deployment you're using, or "*" to allow any to work.

  • The parent window will then respond with an acknowledgement and any return values (if the command results in any). The response is sent as a postMessage the other direction, so the custom view also needs to have a handler set up to receive these responses:
window.addEventListener("message", async (event) => {
const data = event.data;
const command = data.command;
const value = data.value;
if (!value) {
console.log(`Got empty postMessage response to command ${command}`)
} else {
console.log(`Got postMessage response to command ${command} with value with length ${value.length}`)
}
switch (command) {
case "GetDataURL":
const dataUrl = new URL(value);
const resp = await fetch(dataUrl.href);

// Do something with the response here. For instance, you could get it as a JSON object,
// if the data is expected to be valid JSON:
const json = await resp.json();
break;
default:
throw "Unrecognized postMessage command: " + command;
}
});

Supported commands for custom views:

  • GetDataURL: use this command to bind data to your View; retrieve the contents of the URL and display it however you want.
    • Parameters: none.
    • Returns: the URL to the associated file in the repo, as a string.
  • SetFrameHeight:
    • Parameters:
      • height: a string (i.e. "400px") for the desired height of the view container.
    • Returns: none.

Authentication

If your custom view is in a private repository, or a public repository on an enterprise deployment requiring login for all repositories, then an auth token must be provided in order to access repository contents. This token is passed on the query string as "token". For most requests, this is automatic and no modification to the URL is needed:

  • XetHub will automatically put the auth token in the URL when loading the custom view itself (index.html).
  • XetHub will automatically put the auth token in the URL for the data (GetDataURL over postMessage).

For other requests, you will need to add a token to the URL:

  • URLs referenced within the index.html page, like CSS and JS files that come from the repo, can specify ?token={{ token }}, and the placeholder will automatically be replaced with a real token during page load.
  • URLs generated at runtime (i.e. via string manipulation in JavaScript) will need to have the token appended by the runtime logic. A valid token can be obtained from the window.location.search of the current page.

Examples

Minimal end-to-end example

Here's a minimal example of a custom view which counts the words in a .txt file and shows the word count.

The .xethub/custom_views/minimal/view.yml looks like this:

scope:
- "*.txt"

This YAML sets the scope of the view to "*.txt", meaning the view will show up on any .txt file in the repo.

The .xethub/custom_views/minimal/index.html looks like this:

<!DOCTYPE html>
<html>
<head>
<style>
body {
color: black;
}
@media (prefers-color-scheme: dark) {
body {
color: white;
}
}
</style>
<title>Minimal custom view example</title>
</head>
<body>
<div>
Word count: <span id="word_count"></span>
</div>
<script>
window.addEventListener("message", async (event) => {
const data = event.data;
const command = data.command;
const value = data.value;
if (data.error) {
throw "Got error over postMessage: " + data.error;
}
switch (command) {
case "GetDataURL":
const dataUrl = new URL(value);
const resp = await fetch(dataUrl.href);
const text = await resp.text();
const wordCount = text.trim().split(/\s+/).length;
document.querySelector("#word_count").innerText = String(wordCount);
break;
default:
throw "Unrecognized postMessage command: " + command;
}
});
window.parent.postMessage({command: "GetDataURL"}, {targetOrigin: "*"});
window.parent.postMessage({command: "SetFrameHeight", height: "40px"}, {targetOrigin: "*"});
</script>
</body>
</html>

For simplicity, all styling (CSS) and logic (JavaScript) is included in the HTML document, but those can also be split out into separate files as desired. The typical pattern would be to put .css and .js files alongside the HTML in the same directory, and reference them using auth tokens in the query string to authenticate against private repositories as needed.

Try out this minimal example here!

A more complex example

To do something more than count words requires a bit more complexity. Check out the Tabular Data Summarizer example, which combines Actions-generated summaries with custom views to show a per-column summary of the data for CSV, TSV, or Parquet data:

  • main.yml defines the Actions workflow to compute summaries for tabular data.
    • run.sh provides the entry point for the script to run on each Action run.
    • summarize.py provides the logic in Python, using DuckDB to summarize each column and produce a JSON file to store the summary results.
  • view.yml defines the Custom View for .tsv, .csv, and .parquet files.
    • index.html provides the custom view implementation (including logic and some styling) to display the data from the .summary file alongside any .csv/.tsv/.parquet file.

Several examples of custom HTML/CSS/JS views are provided in our View Gallery. These can be used as inheritable templates that can be added with one line of code, or as references for how to do more complex work with this feature.

To add any of these views to an existing repository:

  1. Create a .xethub/custom_views/myView folder at the top level of your repository, where myView is what you'd like to call your view.
  2. Inherit the view by creating a view.yml file within your view folder with the uses: syntax referenced in each template.
  3. By default, the view will inherit the scope of the template repository. You can optionally override this by editing view.yml.

Upon navigation to a file within the defined scope, the custom view will appear.

Try these templates out for yourself to get the functionality in your repo:

  • Tabular Data Summary for csv/tsv/parquet per-column summaries.
    • Put uses: zach/CustomView-TabularDataSummary/.xethub/custom_views/parquet/view.yml in your view.yml.
  • Pyodide Example - Word Cloud to render a word cloud in Python in the browser.
    • Put uses: XetHub/CustomView-Pyodide/.xethub/custom_views/pyodide/view.yml in your view.yml.

Use these built-in views as inspiration for your own Custom Views:

Use these bespoke examples as inspiration for your own Custom Views:

  • Decision Trees combines Actions and Views to summarize model output as data changes over time.