Skip to content

Commit

Permalink
Work
Browse files Browse the repository at this point in the history
  • Loading branch information
qligier committed Oct 23, 2024
1 parent bef3433 commit 697c943
Show file tree
Hide file tree
Showing 20 changed files with 280 additions and 40 deletions.
14 changes: 14 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 5 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
},
"scripts": {
"build": "node ./build.js",
"build-js": "tsc -noEmit && esbuild src/ts/index.ts --bundle --minify --tree-shaking=true --target=es2022 --outfile=dist/app.js",
"build-js": "tsc -noEmit && esbuild src/ts/app.ts --bundle --minify --tree-shaking=true --target=es2022 --outfile=dist/app.js",
"build-css": "sass --no-source-map --style=compressed --color src/scss/index.scss dist/style.css",
"build-static": "cp -R ./static/* ./dist"
},
Expand All @@ -29,8 +29,11 @@
"typescript-eslint": "^8.9.0"
},
"type": "module",
"exports": "./dist/index.js",
"exports": "./dist/app.js",
"imports": {
"#package.json": "./package.json"
},
"dependencies": {
"yaml": "^2.6.0"
}
}
2 changes: 1 addition & 1 deletion src/scss/header.scss
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
@import "colors";

header {
$logo-size: 80px;
$logo-size: 100px;

line-height: $logo-size;

Expand Down
12 changes: 10 additions & 2 deletions src/scss/logs.scss
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,7 @@ $block-border-radius: 10px;
line-height: 1.7;
}
.col3 {
width: 250px;
margin-right: $intercol-space;
width: 270px;
line-height: 1.7;
color: $oc-gray-8;

Expand All @@ -100,6 +99,15 @@ $block-border-radius: 10px;
vertical-align: text-bottom;
}
}
.col4 {
width: 120px;
line-height: 1.7;
color: $oc-gray-8;
.icon {
margin-right: 5px;
vertical-align: text-bottom;
}
}
.actions {
width: $summary-height;

Expand Down
108 changes: 96 additions & 12 deletions src/ts/api.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import {Country, repoOwnersToCountries} from "./countries";
import YAML from 'yaml';

const logFileUrl = 'https://build.fhir.org/ig/qas.json';
const qasFileUrl = 'https://build.fhir.org/ig/qas.json';
const buildsFileUrl = 'https://build.fhir.org/ig/builds.json';
const igBuildRequestUrl = 'https://us-central1-fhir-org-starter-project.cloudfunctions.net/ig-commit-trigger';

/**
Expand All @@ -20,7 +22,8 @@ export class IgBuildLog {
public readonly fhirVersion: string,
public readonly repositoryOwner: string,
public readonly repositoryName: string,
public readonly repositoryBranch: string) {
public readonly repositoryBranch: string,
public readonly success: boolean) {
}

get repositoryUrl(): string {
Expand All @@ -35,7 +38,19 @@ export class IgBuildLog {
}

get buildStatus(): string {
return this.errorCount > 0 ? 'error' : 'success';
return this.success ? 'success' : 'error';
}

get baseBuildUrl(): string {
return `https://build.fhir.org/ig/${this.repositoryOwner}/${this.repositoryName}/branches/${this.repositoryBranch}/`;
}

get failureLogsUrl(): string {
return `${this.baseBuildUrl}failure/build.log`;
}

get qaUrl(): string {
return `${this.baseBuildUrl}qa.html`;
}
}

Expand Down Expand Up @@ -64,34 +79,89 @@ interface ApiResponseItem {
}

export async function fetchIgBuildLogs(): Promise<Array<IgBuildLog>> {
const response: Response = await fetch(logFileUrl);
const [succeededBuilds, failedBuilds] = await Promise.all([fetchSucceededBuilds(), fetchFailedBuilds()]);
succeededBuilds.push(...failedBuilds.filter(log => log !== undefined));

// Sort by date descending
succeededBuilds.sort((a: IgBuildLog, b: IgBuildLog): number => b.date.getTime() - a.date.getTime());
return succeededBuilds;
}

async function fetchSucceededBuilds(): Promise<Array<IgBuildLog>> {
const response: Response = await fetch(qasFileUrl);
const data: Array<ApiResponseItem> = await response.json();

const logs = new Array<IgBuildLog>(data.length);
const builds = new Array<IgBuildLog>(data.length);
let i = 0;
for (const row of data) {
const repoParts = row['repo'].split('/');
logs[i++] = new IgBuildLog(
builds[i++] = new IgBuildLog(
row['name'] ?? '',
row['title'] ?? '',
row['description'] ?? '',
row['url'] ?? '',
row['package-id'] ?? '',
row['ig-ver'] ?? '',
parseDate(row['date'] ?? '', row),
parseDate(row['date'] ?? ''),
row['errs'] ?? 0,
row['warnings'] ?? 0,
row['hints'] ?? 0,
row['version'] ?? '',
repoParts[0]!,
repoParts[1]!,
repoParts[3]!,
true
);
}
return builds;
}

// Sort by date descending
logs.sort((a: IgBuildLog, b: IgBuildLog): number => b.date.getTime() - a.date.getTime());
return logs;
/**
* Fetches all the failed builds. This is done in two steps:
* 1. Fetch the list of all log files from the builds.json file. If it contains the word 'failure', it is a failed build.
* Otherwise, we already have its details in the qas.json file.
* 2. Fetch the log file for each failed build, extract some information and return it.
*/
async function fetchFailedBuilds(): Promise<Array<IgBuildLog | undefined>> {
const response: Response = await fetch(buildsFileUrl);
const allIgs = await response.json() as string[];
const failedIgs = allIgs
.filter(ig => ig.includes('failure'))
.map(ig => ig.replace('/build.log', '/sushi-config.yaml'));
const promises = failedIgs.map(async (ig) => {
const response = await fetch(`https://build.fhir.org/ig/${ig}`);
if (!response.ok) {
// Here, we can try to fetch 'publication-request.json', which contains almost the same information.
// Otherwise, we need to fetch 'ig.ini' to find the main IG file, and then fetch it
// ('input/ch.fhir.ig.XXX.xml').
return undefined;
}
const yamlFile = await response.text();
const yaml: SushiConfig = YAML.parse(yamlFile, { uniqueKeys: false, strict: false, stringKeys: true });

const [owner, repo, , branch, ] = ig.split('/');
const lastModified = response.headers.get('last-modified')!;

return new IgBuildLog(
yaml['name'] ?? '',
yaml['title'] ?? '',
yaml['description'] ?? '',
yaml['canonical'] ?? '',
yaml['id'] ?? '',
yaml['version'] ?? '',
parseDate(lastModified),
1, // 1 fatal error
0,
0,
yaml['fhirVersion'] ?? '',
owner!,
repo!,
branch!,
false
);
});

return Promise.all(promises);
}

export async function requestIgBuild(repoOwner: string, repoName: string, branch: string): Promise<void> {
Expand All @@ -106,11 +176,25 @@ export async function requestIgBuild(repoOwner: string, repoName: string, branch
});
}

const parseDate = (date: string, object: object): Date => {
const parseDate = (date: string): Date => {
const timestamp = Date.parse(date);
if (isNaN(timestamp)) {
console.log(object);
throw new Error(`Invalid date: ${date}`);
}
return new Date(timestamp);
}

interface SushiConfig {
id: string;
canonical: string;
name: string;
title: string;
description: string;
status: string;
version: string;
fhirVersion: string;
copyrightYear: string;
releaseLabel: string;
license: string;
jurisdiction: string;
}
32 changes: 32 additions & 0 deletions src/ts/app.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import {fetchIgBuildLogs, IgBuildLog} from "./api";
import {rebuildLogsInDom} from "./dom";
import {notifyError} from "./notification";

let allIgBuildLogs: Array<IgBuildLog> = [];
let fetchingData: boolean = false;

const setFetchingData = (value: boolean) => {
fetchingData = value;
// TODO: update DOM to show/hide loading spinner
}

const refreshLogs: () => Promise<void> = async () => {
if (fetchingData) {
return;
}
setFetchingData(true);

try {
allIgBuildLogs = await fetchIgBuildLogs();
// Sort and filter if necessary
rebuildLogsInDom(allIgBuildLogs);
} catch (e: unknown) {
setFetchingData(false);
if (e instanceof Error) {
notifyError('Failed to fetch logs', e).then(() => {});
}
console.error(e);
}
}

refreshLogs().then(() => {});
36 changes: 32 additions & 4 deletions src/ts/dom.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {IgBuildLog} from "./api";
import {IgBuildLog, requestIgBuild} from "./api";
import packageJson from '#package.json' assert {type: 'json'};
import {dayNameFormatter, mediumDateFormatter, timeFormatter} from "./browser";

Expand All @@ -17,18 +17,31 @@ if (!domTemplateLog) {

document.querySelector("#shoebill-version")!.textContent = packageJson.version;

const requestRebuild = (targetLog: HTMLElement): void => {
const dataset = targetLog.dataset;
if (!dataset["repoOwner"] || !dataset["repoName"] || !dataset["repoBranch"]) {
throw new Error("Missing repository information");
}
requestIgBuild(dataset["repoOwner"], dataset["repoName"], dataset["repoBranch"]).then(() => {});
}

domNodeLogWrapper.addEventListener('click', (event: MouseEvent) => {
if (event.target instanceof SVGElement) {
if (event.target.matches('.switchy')) {
// Toggle the details
event.target.closest('.log')!.classList.toggle('switchy-open');
}
if (event.target.matches('.request-rebuild svg')) {
requestRebuild(event.target.closest('.log')!);
}
return;
}
if (event.target instanceof HTMLElement) {
return;
if (event.target.matches('.request-rebuild')) {
requestRebuild(event.target.closest('.log')!);
}
}
})
});

export const rebuildLogsInDom = (logs: Array<IgBuildLog>) => {
const fragment = document.createDocumentFragment();
Expand All @@ -50,7 +63,7 @@ export const rebuildLogsInDom = (logs: Array<IgBuildLog>) => {
const template = domTemplateLog.cloneNode(true) as HTMLTemplateElement;

template.content.querySelector('.status')!.classList.add(log.buildStatus);
template.content.querySelector('.status')!.setAttribute('title', log.buildStatus === 'error' ? 'Error' : 'Success');

template.content.querySelector('.time')!.textContent = timeFormatter.format(log.date);
template.content.querySelector('.name')!.textContent = log.name;
template.content.querySelector('.title')!.textContent = log.title;
Expand All @@ -64,13 +77,28 @@ export const rebuildLogsInDom = (logs: Array<IgBuildLog>) => {
template.content.querySelector('.ig-version')!.appendChild(document.createTextNode(log.igVersion));
template.content.querySelector('.fhir-version')!.appendChild(document.createTextNode(log.fhirVersion));

if (log.buildStatus === 'error') {
template.content.querySelector('.status')!.setAttribute('title', 'The build has failed');
template.content.querySelector('.link-failure-logs a')!.setAttribute('href', log.failureLogsUrl);
template.content.querySelector('.link-preview')!.remove();
} else {
template.content.querySelector('.status')!.setAttribute('title', 'The build has succeeded');
template.content.querySelector('.link-failure-logs')!.remove();
template.content.querySelector('.link-preview a')!.setAttribute('href', log.baseBuildUrl);
}

if (log.country) {
const img = document.createElement('img');
img.src = `images/flags/${log.country}.svg`;
img.alt = `Country: log.country`;
template.content.querySelector('.country')!.appendChild(img);
}

const dataset = (template.content.querySelector('.log') as HTMLElement).dataset;
dataset['repoOwner'] = log.repositoryOwner;
dataset['repoName'] = log.repositoryName;
dataset['repoBranch'] = log.repositoryBranch;

currentDay.appendChild(template.content);
}
if (currentDay) {
Expand Down
12 changes: 0 additions & 12 deletions src/ts/index.ts

This file was deleted.

4 changes: 4 additions & 0 deletions src/ts/notification.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export async function notifyError(title: string, error: Error): Promise<void> {
title;
error;
}
Binary file added static/images/apple-touch-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added static/images/favicon-48x48.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added static/images/favicon.ico
Binary file not shown.
1 change: 1 addition & 0 deletions static/images/favicon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit 697c943

Please sign in to comment.