Skip to main content

Customize views with HTML/CSS/JS

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

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.

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.

  • A scope of "*.xyz" will display the view on pages that render files ending with a .xyz extension.
  • Coming soon: Associate custom views with specific files or directories in a repository.

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

Configuring the view

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:

  • scope: a list of strings matching extension, file, or directory paths within the repo. See Where custom views appear.
  • 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.

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.

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 (!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);
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 filesA
    • 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.