mirror of
https://github.com/godotengine/godot-commit-artifacts.git
synced 2025-12-31 05:48:27 +03:00
Initial commit
This commit is contained in:
13
.editorconfig
Normal file
13
.editorconfig
Normal file
@@ -0,0 +1,13 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.{css}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
57
.github/workflows/ci.yml
vendored
Normal file
57
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,57 @@
|
||||
name: Continuous integration
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
schedule:
|
||||
# Run every hour at 10 minutes past the hour mark.
|
||||
# The slight offset is there to try and avoid the high load times.
|
||||
- cron: '10 * * * *'
|
||||
|
||||
# Make sure jobs cannot overlap (e.g. one from push and one from schedule).
|
||||
concurrency:
|
||||
group: pages-ci
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build and deploy to GitHub Pages
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Install Node.js 16.x
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16.x
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Build the static content using npm
|
||||
run: npm run build
|
||||
|
||||
- name: Fetch artifact data (master)
|
||||
run: npm run compose-db -- branch:master
|
||||
env:
|
||||
GRAPHQL_TOKEN: ${{ secrets.GRAPHQL_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Archive production artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: web-static
|
||||
path: out
|
||||
|
||||
- name: Deploy to GitHub Pages 🚀
|
||||
uses: JamesIves/github-pages-deploy-action@v4
|
||||
with:
|
||||
branch: gh-pages
|
||||
folder: out
|
||||
# Configure the commit author.
|
||||
git-config-name: 'Godot Organization'
|
||||
git-config-email: '<>'
|
||||
# Don't keep the history.
|
||||
single-commit: true
|
||||
9
.gitignore
vendored
Normal file
9
.gitignore
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
# Project folders.
|
||||
node_modules/
|
||||
out/
|
||||
temp/
|
||||
logs/
|
||||
|
||||
# Development environments.
|
||||
.idea/
|
||||
.vscode/
|
||||
21
LICENSE.md
Normal file
21
LICENSE.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# MIT License
|
||||
|
||||
Copyright © 2023-present Godot Engine contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
43
README.md
Normal file
43
README.md
Normal file
@@ -0,0 +1,43 @@
|
||||
# Godot Commit Artifacts
|
||||
|
||||
This project is provided for Godot Engine users and contributors to
|
||||
easily and reliably get links to the CI build artifacts for the main
|
||||
development branches. While these artifacts are not suitable for
|
||||
production use, they can be used for testing and early feature
|
||||
adoption.
|
||||
|
||||
Live website: https://godotengine.github.io/godot-commit-artifacts/
|
||||
|
||||
## Contributing
|
||||
|
||||
This project is written in JavaScript and is built using Node.JS. HTML and CSS are
|
||||
used for the presentation. The end result of the build process is completely static
|
||||
and can be server from any web server, no Node.JS required.
|
||||
|
||||
Front-end is designed in a reactive manner using industry standard Web Components
|
||||
(powered by `lit-element`). This provides native browser support, and results in a
|
||||
small overhead from the build process.
|
||||
|
||||
To build the project locally you need to have Node.JS installed (12.x and newer
|
||||
should work just fine).
|
||||
|
||||
This project uses GitHub's GraphQL API. To fetch live data you need to generate
|
||||
a [personal OAuth token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token).
|
||||
You can supply your token to the scripts using the `GRAPHQL_TOKEN` environment
|
||||
variable. Note, that if you don't have member access to the organization, you
|
||||
may not be able to access all the information used when generating the database.
|
||||
|
||||
1. Clone or download the project.
|
||||
2. From the project root run `npm install` or `yarn` to install dependencies.
|
||||
3. Run `npm run build` or `yarn run build` to build the pages.
|
||||
4. Run `npm run compose-db` or `yarn run compose-db` to fetch the data from GitHub.
|
||||
5. Serve the `out/` folder with your method of choice (e.g. using Python 3:
|
||||
`python -m http.server 8080 -d ./out`).
|
||||
|
||||
`rollup` is used for browser packing of scripts and copying of static assets. The
|
||||
data fetching script is plain JavaScript with `node-fetch` used to polyfill
|
||||
`fetch()`-like API.
|
||||
|
||||
## License
|
||||
|
||||
This project is provided under the [MIT License](LICENSE.md).
|
||||
41
build/posthtml-include.js
Normal file
41
build/posthtml-include.js
Normal file
@@ -0,0 +1,41 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
import parser from "posthtml-parser";
|
||||
|
||||
export default function(options) {
|
||||
options = options || {};
|
||||
options.root = options.root || './';
|
||||
options.encoding = options.encoding || 'utf-8';
|
||||
|
||||
return function posthtmlInclude(tree) {
|
||||
tree.match({ tag: 'include' }, function(node) {
|
||||
if (!node.attrs.src) {
|
||||
return {
|
||||
tag: false,
|
||||
content: null
|
||||
};
|
||||
}
|
||||
|
||||
const src = path.resolve(options.root, node.attrs.src);
|
||||
const source = fs.readFileSync(src, options.encoding);
|
||||
const subtree = parser(source);
|
||||
subtree.match = tree.match;
|
||||
const content = source.indexOf('include') !== -1? posthtmlInclude(subtree): subtree;
|
||||
|
||||
if (tree.messages) {
|
||||
tree.messages.push({
|
||||
type: "dependency",
|
||||
file: src
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
tag: false,
|
||||
content: content
|
||||
};
|
||||
});
|
||||
|
||||
return tree;
|
||||
};
|
||||
};
|
||||
9
build/res/empty_index.html
Normal file
9
build/res/empty_index.html
Normal file
@@ -0,0 +1,9 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Nothing to see here</title>
|
||||
</head>
|
||||
<body>
|
||||
</body>
|
||||
</html>
|
||||
31
build/rollup-posthtml-template.js
Normal file
31
build/rollup-posthtml-template.js
Normal file
@@ -0,0 +1,31 @@
|
||||
import { promises as fs } from 'fs';
|
||||
|
||||
import posthtml from 'posthtml';
|
||||
import include from './posthtml-include';
|
||||
import { green } from 'colorette';
|
||||
|
||||
export default function(options = {}) {
|
||||
return {
|
||||
name: 'posthtml',
|
||||
buildEnd: async () => {
|
||||
if (!options.src || !options.dest) {
|
||||
return;
|
||||
}
|
||||
const html = await fs.readFile(options.src, { encoding: 'utf-8' });
|
||||
|
||||
const plugins = [
|
||||
include({
|
||||
root: './src'
|
||||
})
|
||||
];
|
||||
const result = await posthtml(plugins).process(html);
|
||||
|
||||
try {
|
||||
await fs.unlink(options.dest);
|
||||
} catch (exc) { }
|
||||
|
||||
await fs.writeFile(options.dest, result.html, { encoding: 'utf-8' });
|
||||
console.log(green(`written html template ${options.dest}`))
|
||||
}
|
||||
};
|
||||
}
|
||||
507
compose-db.js
Normal file
507
compose-db.js
Normal file
@@ -0,0 +1,507 @@
|
||||
const fs = require('fs').promises;
|
||||
const fsConstants = require('fs').constants;
|
||||
const fetch = require('node-fetch');
|
||||
|
||||
const ExitCodes = {
|
||||
"RequestFailure": 1,
|
||||
"ParseFailure": 2,
|
||||
"ExecFailure": 3,
|
||||
"IOFailure": 4,
|
||||
};
|
||||
|
||||
const LogFormat = {
|
||||
"Raw": 0,
|
||||
"JSON": 1,
|
||||
};
|
||||
|
||||
const API_DELAY_MSEC = 1500;
|
||||
const API_MAX_RETRIES = 5;
|
||||
const API_RATE_LIMIT = `
|
||||
rateLimit {
|
||||
limit
|
||||
cost
|
||||
nodeCount
|
||||
remaining
|
||||
resetAt
|
||||
}
|
||||
`;
|
||||
|
||||
class DataFetcher {
|
||||
constructor(data_owner, data_repo) {
|
||||
this.data_owner = data_owner;
|
||||
this.data_repo = data_repo;
|
||||
|
||||
this.repo_ssh_path = `git@github.com:${data_owner}/${data_repo}.git`;
|
||||
this.api_rest_path = `https://api.github.com/repos/${data_owner}/${data_repo}`;
|
||||
this.api_repository_id = `owner:"${data_owner}" name:"${data_repo}"`;
|
||||
}
|
||||
|
||||
async _logResponse(data, name, format = LogFormat.JSON) {
|
||||
try {
|
||||
await ensureDir("./logs");
|
||||
|
||||
let filename = `./logs/${name}`;
|
||||
let fileContent = "" + data;
|
||||
|
||||
if (format === LogFormat.JSON) {
|
||||
filename = `./logs/${name}.json`;
|
||||
fileContent = JSON.stringify(data, null, 4);
|
||||
}
|
||||
|
||||
await fs.writeFile(filename, fileContent, {encoding: "utf-8"});
|
||||
} catch (err) {
|
||||
console.error(" Error saving log file: " + err);
|
||||
}
|
||||
}
|
||||
|
||||
_handleResponseErrors(queryID, res) {
|
||||
console.warn(` Failed to get data from '${queryID}'; server responded with ${res.status} ${res.statusText}`);
|
||||
const retry_header = res.headers.get("Retry-After");
|
||||
if (retry_header) {
|
||||
console.log(` Retry after: ${retry_header}`);
|
||||
}
|
||||
}
|
||||
|
||||
_handleDataErrors(data) {
|
||||
if (typeof data["errors"] === "undefined") {
|
||||
return;
|
||||
}
|
||||
|
||||
console.warn(` Server handled the request, but there were errors:`);
|
||||
data.errors.forEach((item) => {
|
||||
console.log(` [${item.type}] ${item.message}`);
|
||||
});
|
||||
}
|
||||
|
||||
async delay(msec) {
|
||||
return new Promise(resolve => setTimeout(resolve, msec));
|
||||
}
|
||||
|
||||
async fetchGithub(query, retries = 0) {
|
||||
const init = {};
|
||||
init.method = "POST";
|
||||
init.headers = {};
|
||||
init.headers["Content-Type"] = "application/json";
|
||||
if (process.env.GRAPHQL_TOKEN) {
|
||||
init.headers["Authorization"] = `token ${process.env.GRAPHQL_TOKEN}`;
|
||||
} else if (process.env.GITHUB_TOKEN) {
|
||||
init.headers["Authorization"] = `token ${process.env.GITHUB_TOKEN}`;
|
||||
}
|
||||
|
||||
init.body = JSON.stringify({
|
||||
query,
|
||||
});
|
||||
|
||||
let res = await fetch("https://api.github.com/graphql", init);
|
||||
let attempt = 0;
|
||||
while (res.status !== 200 && attempt < retries) {
|
||||
attempt += 1;
|
||||
console.log(` Failed with status ${res.status}, retrying (${attempt}/${retries})...`);
|
||||
|
||||
// GitHub API is flaky, so we add an extra delay to let it calm down a bit.
|
||||
await this.delay(API_DELAY_MSEC);
|
||||
res = await fetch("https://api.github.com/graphql", init);
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
async fetchGithubRest(query) {
|
||||
const init = {};
|
||||
init.method = "GET";
|
||||
init.headers = {};
|
||||
init.headers["Content-Type"] = "application/json";
|
||||
if (process.env.GRAPHQL_TOKEN) {
|
||||
init.headers["Authorization"] = `token ${process.env.GRAPHQL_TOKEN}`;
|
||||
} else if (process.env.GITHUB_TOKEN) {
|
||||
init.headers["Authorization"] = `token ${process.env.GITHUB_TOKEN}`;
|
||||
}
|
||||
|
||||
return await fetch(`${this.api_rest_path}${query}`, init);
|
||||
}
|
||||
|
||||
async checkRates() {
|
||||
try {
|
||||
const query = `
|
||||
query {
|
||||
${API_RATE_LIMIT}
|
||||
}
|
||||
`;
|
||||
|
||||
const res = await this.fetchGithub(query);
|
||||
if (res.status !== 200) {
|
||||
this._handleResponseErrors(this.api_repository_id, res);
|
||||
process.exitCode = ExitCodes.RequestFailure;
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
await this._logResponse(data, "_rate_limit");
|
||||
this._handleDataErrors(data);
|
||||
|
||||
const rate_limit = data.data["rateLimit"];
|
||||
console.log(` [$${rate_limit.cost}][${rate_limit.nodeCount}] Available API calls: ${rate_limit.remaining}/${rate_limit.limit}; resets at ${rate_limit.resetAt}`);
|
||||
} catch (err) {
|
||||
console.error(" Error checking the API rate limits: " + err);
|
||||
process.exitCode = ExitCodes.RequestFailure;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
async fetchRuns(branchName) {
|
||||
try {
|
||||
const query = `
|
||||
query {
|
||||
${API_RATE_LIMIT}
|
||||
|
||||
repository (${this.api_repository_id}) {
|
||||
object (expression: "${branchName}") {
|
||||
... on Commit {
|
||||
history(first: 10) {
|
||||
edges {
|
||||
node {
|
||||
...CommitData
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fragment CommitData on Commit {
|
||||
oid
|
||||
committedDate
|
||||
messageHeadline
|
||||
|
||||
checkSuites(first: 20) {
|
||||
edges {
|
||||
node {
|
||||
...CheckSuiteData
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fragment CheckSuiteData on CheckSuite {
|
||||
databaseId
|
||||
url
|
||||
status
|
||||
conclusion
|
||||
createdAt
|
||||
updatedAt
|
||||
workflowRun {
|
||||
databaseId
|
||||
workflow {
|
||||
databaseId
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
console.log(` Requesting workflow runs data for commits in "${branchName}".`);
|
||||
|
||||
const res = await this.fetchGithub(query, API_MAX_RETRIES);
|
||||
if (res.status !== 200) {
|
||||
this._handleResponseErrors(this.api_repository_id, res);
|
||||
process.exitCode = ExitCodes.RequestFailure;
|
||||
return [];
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
await this._logResponse(data, `data_runs_${branchName}`);
|
||||
this._handleDataErrors(data);
|
||||
|
||||
const repository = data.data["repository"];
|
||||
const run_data = mapNodes(repository.object["history"]);
|
||||
|
||||
const rate_limit = data.data["rateLimit"];
|
||||
console.log(` [$${rate_limit.cost}][${rate_limit.nodeCount}] Retrieved ${run_data.length} commits and their runs.`);
|
||||
console.log(` --`);
|
||||
return run_data;
|
||||
} catch (err) {
|
||||
console.error(" Error fetching workflow runs data: " + err);
|
||||
process.exitCode = ExitCodes.RequestFailure;
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async fetchArtifacts(runId) {
|
||||
try {
|
||||
const query = `/actions/runs/${runId}/artifacts`;
|
||||
|
||||
const res = await this.fetchGithubRest(query);
|
||||
if (res.status !== 200) {
|
||||
this._handleResponseErrors(query, res);
|
||||
process.exitCode = ExitCodes.RequestFailure;
|
||||
return [];
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
await this._logResponse(data, `data_artifacts_${runId}`);
|
||||
this._handleDataErrors(data);
|
||||
|
||||
const artifacts_data = data.artifacts;
|
||||
|
||||
console.log(` [$0] Retrieved ${artifacts_data.length} artifacts for '${runId}'; processing...`);
|
||||
|
||||
return artifacts_data;
|
||||
} catch (err) {
|
||||
console.error(" Error fetching artifact data: " + err);
|
||||
process.exitCode = ExitCodes.RequestFailure;
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class DataProcessor {
|
||||
constructor() {
|
||||
this.commits = [];
|
||||
this.checks = {};
|
||||
this.runs = {};
|
||||
this.artifacts = {};
|
||||
}
|
||||
|
||||
processRuns(runsRaw) {
|
||||
try {
|
||||
runsRaw.forEach((item) => {
|
||||
// Compile basic information about a commit.
|
||||
let commit = {
|
||||
"hash": item.oid,
|
||||
"title": item.messageHeadline,
|
||||
"committed_date": item.committedDate,
|
||||
"checks": [],
|
||||
};
|
||||
|
||||
const checkSuites = mapNodes(item.checkSuites);
|
||||
checkSuites.forEach((checkItem) => {
|
||||
// Compile basic information about a check suite.
|
||||
let check = {
|
||||
"check_id": checkItem.databaseId,
|
||||
"check_url": checkItem.url,
|
||||
"status": checkItem.status,
|
||||
"conclusion": checkItem.conclusion,
|
||||
|
||||
"created_at": checkItem.createdAt,
|
||||
"updated_at": checkItem.updatedAt,
|
||||
|
||||
"workflow": null,
|
||||
};
|
||||
|
||||
if (checkItem.workflowRun) {
|
||||
const runItem = checkItem.workflowRun;
|
||||
let run = {
|
||||
"name": runItem.workflow.name,
|
||||
"workflow_id": runItem.workflow.databaseId,
|
||||
"run_id": runItem.databaseId,
|
||||
|
||||
"artifacts": [],
|
||||
};
|
||||
|
||||
this.runs[run.run_id] = run;
|
||||
check.workflow = run.run_id;
|
||||
}
|
||||
|
||||
this.checks[check.check_id] = check;
|
||||
commit.checks.push(check.check_id);
|
||||
});
|
||||
|
||||
this.commits.push(commit);
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(" Error parsing pull request data: " + err);
|
||||
process.exitCode = ExitCodes.ParseFailure;
|
||||
}
|
||||
}
|
||||
|
||||
processArtifacts(runId, artifactsRaw) {
|
||||
try {
|
||||
artifactsRaw.forEach((item) => {
|
||||
let artifact = {
|
||||
"id": item.id,
|
||||
"name": item.name,
|
||||
"size": item.size_in_bytes,
|
||||
|
||||
"created_at": item.created_at,
|
||||
"updated_at": item.upadted_at,
|
||||
"expires_at": item.expires_at,
|
||||
};
|
||||
|
||||
this.runs[runId].artifacts.push(artifact);
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(" Error parsing artifact data: " + err);
|
||||
process.exitCode = ExitCodes.ParseFailure;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class DataIO {
|
||||
constructor() {
|
||||
// Configurable parameters.
|
||||
this.data_owner = "godotengine";
|
||||
this.data_repo = "godot";
|
||||
this.data_branch = "";
|
||||
}
|
||||
|
||||
parseArgs() {
|
||||
process.argv.forEach((arg) => {
|
||||
if (arg.indexOf("owner:") === 0) {
|
||||
this.data_owner = arg.substring(6);
|
||||
}
|
||||
if (arg.indexOf("repo:") === 0) {
|
||||
this.data_repo = arg.substring(5);
|
||||
}
|
||||
if (arg.indexOf("branch:") === 0) {
|
||||
this.data_branch = arg.substring(7);
|
||||
}
|
||||
});
|
||||
|
||||
if (this.data_owner === "" || this.data_repo === "" || this.data_branch === "") {
|
||||
console.error(" Error reading command-line arguments: owner, repo, and branch cannot be empty.");
|
||||
process.exitCode = ExitCodes.IOFailure;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
async loadData() {
|
||||
try {
|
||||
console.log("[*] Loading existing database from a file.");
|
||||
|
||||
// const dataPath = `./out/data/${this.data_owner}.${this.data_repo}.${this.data_branch}.json`;
|
||||
// await fs.access(dataPath, fsConstants.R_OK);
|
||||
// const existingData = await fs.readFile(dataPath);
|
||||
} catch (err) {
|
||||
console.error(" Error loading existing database file: " + err);
|
||||
process.exitCode = ExitCodes.IOFailure;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
async saveData(output, fileName) {
|
||||
try {
|
||||
console.log("[*] Storing database to a file.");
|
||||
|
||||
await ensureDir("./out");
|
||||
await ensureDir("./out/data");
|
||||
await fs.writeFile(`./out/data/${fileName}`, JSON.stringify(output), {encoding: "utf-8"});
|
||||
} catch (err) {
|
||||
console.error(" Error saving database file: " + err);
|
||||
process.exitCode = ExitCodes.IOFailure;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function mapNodes(object) {
|
||||
return object.edges.map((item) => item["node"])
|
||||
}
|
||||
|
||||
async function ensureDir(dirPath) {
|
||||
try {
|
||||
await fs.access(dirPath, fsConstants.R_OK | fsConstants.W_OK);
|
||||
} catch (err) {
|
||||
await fs.mkdir(dirPath);
|
||||
}
|
||||
}
|
||||
|
||||
async function clearDir(rootPath) {
|
||||
try {
|
||||
const pathStat = await fs.stat(rootPath);
|
||||
if (!pathStat.isDirectory()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const removeDir = async (dirPath) => {
|
||||
const dirFiles = await fs.readdir(dirPath);
|
||||
for (let entryName of dirFiles) {
|
||||
if (entryName === "." || entryName === "..") {
|
||||
continue;
|
||||
}
|
||||
|
||||
const entryPath = `${dirPath}/${entryName}`;
|
||||
const entryStat = await fs.stat(entryPath);
|
||||
if (entryStat.isDirectory()) {
|
||||
await removeDir(entryPath);
|
||||
await fs.rmdir(entryPath);
|
||||
}
|
||||
else if (entryStat.isFile()) {
|
||||
await fs.unlink(entryPath);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
await removeDir(rootPath);
|
||||
} catch (err) {
|
||||
console.error(` Error clearing a folder at ${rootPath}: ` + err);
|
||||
process.exitCode = ExitCodes.IOFailure;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
// Internal utility methods.
|
||||
const checkForExit = () => {
|
||||
if (process.exitCode > 0) {
|
||||
console.log(` Terminating with an exit code ${process.exitCode}.`);
|
||||
process.exit();
|
||||
}
|
||||
};
|
||||
|
||||
console.log("[*] Building local workflow run database.");
|
||||
|
||||
const dataIO = new DataIO();
|
||||
dataIO.parseArgs();
|
||||
checkForExit();
|
||||
|
||||
// await dataIO.loadConfig();
|
||||
// checkForExit();
|
||||
|
||||
console.log(`[*] Configured for the "${dataIO.data_owner}/${dataIO.data_repo}" repository; branch ${dataIO.data_branch}.`);
|
||||
|
||||
const dataFetcher = new DataFetcher(dataIO.data_owner, dataIO.data_repo);
|
||||
const dataProcessor = new DataProcessor();
|
||||
|
||||
console.log("[*] Checking the rate limits before.");
|
||||
await dataFetcher.checkRates();
|
||||
checkForExit();
|
||||
|
||||
console.log("[*] Fetching workflow runs data from GitHub.");
|
||||
const runsRaw = await dataFetcher.fetchRuns(dataIO.data_branch);
|
||||
checkForExit();
|
||||
dataProcessor.processRuns(runsRaw);
|
||||
checkForExit();
|
||||
|
||||
console.log("[*] Fetching artifact data from GitHub.");
|
||||
for (let runId in dataProcessor.runs) {
|
||||
const artifactsRaw = await dataFetcher.fetchArtifacts(runId);
|
||||
checkForExit();
|
||||
dataProcessor.processArtifacts(runId, artifactsRaw);
|
||||
checkForExit();
|
||||
|
||||
// Wait for a bit before proceeding to avoid hitting the secondary rate limit in GitHub API.
|
||||
// See https://docs.github.com/en/rest/guides/best-practices-for-integrators#dealing-with-secondary-rate-limits.
|
||||
await dataFetcher.delay(API_DELAY_MSEC);
|
||||
}
|
||||
|
||||
console.log("[*] Checking the rate limits after.")
|
||||
await dataFetcher.checkRates();
|
||||
checkForExit();
|
||||
|
||||
console.log("[*] Finalizing database.")
|
||||
const output = {
|
||||
"generated_at": Date.now(),
|
||||
"commits": dataProcessor.commits,
|
||||
"checks": dataProcessor.checks,
|
||||
"runs": dataProcessor.runs,
|
||||
"artifacts": dataProcessor.artifacts,
|
||||
};
|
||||
|
||||
await dataIO.saveData(output, `${dataIO.data_owner}.${dataIO.data_repo}.${dataIO.data_branch}.json`);
|
||||
checkForExit();
|
||||
|
||||
console.log("[*] Database built.");
|
||||
}
|
||||
|
||||
main();
|
||||
1161
package-lock.json
generated
Normal file
1161
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
27
package.json
Normal file
27
package.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "godot-commit-artifacts",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"scripts": {
|
||||
"build": "rollup -c",
|
||||
"compose-db": "node ./compose-db.js"
|
||||
},
|
||||
"author": "Yuri Sizov <yuris@humnom.net>",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.6.4",
|
||||
"@babel/plugin-proposal-class-properties": "^7.5.5",
|
||||
"@babel/plugin-proposal-decorators": "^7.6.0",
|
||||
"dompurify": "^2.0.7",
|
||||
"lit-element": "^2.2.1",
|
||||
"marked": "^0.7.0",
|
||||
"node-fetch": "^2.6.1",
|
||||
"posthtml": "^0.12.0",
|
||||
"rollup": "^1.24.0",
|
||||
"rollup-plugin-babel": "^4.3.3",
|
||||
"rollup-plugin-commonjs": "^10.1.0",
|
||||
"rollup-plugin-copy": "^3.4.0",
|
||||
"rollup-plugin-includepaths": "^0.2.3",
|
||||
"rollup-plugin-node-resolve": "^5.2.0"
|
||||
}
|
||||
}
|
||||
116
rollup.config.js
Normal file
116
rollup.config.js
Normal file
@@ -0,0 +1,116 @@
|
||||
import { promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
import nodeResolve from 'rollup-plugin-node-resolve';
|
||||
import includePaths from 'rollup-plugin-includepaths'
|
||||
import commonjs from 'rollup-plugin-commonjs';
|
||||
import copy from 'rollup-plugin-copy';
|
||||
import posthtmlTemplate from './build/rollup-posthtml-template';
|
||||
import babel from 'rollup-plugin-babel';
|
||||
|
||||
const INPUT_ROOT = 'src';
|
||||
const INPUT_PATHS_ROOT = path.join(INPUT_ROOT, 'paths');
|
||||
const INPUT_SHARED_ROOT = path.join(INPUT_ROOT, 'shared');
|
||||
const INPUT_STATIC_ROOT = path.join(INPUT_ROOT, 'static');
|
||||
|
||||
const ENTRY_FILE_NAME = 'entry.js';
|
||||
const TEMPLATE_FILE_NAME = 'template.html';
|
||||
const GLOBAL_FILE_NAME = 'global.js';
|
||||
|
||||
const OUTPUT_ROOT = 'out';
|
||||
const OUTPUT_STYLES = path.join(OUTPUT_ROOT, 'styles');
|
||||
const OUTPUT_STYLES_SHARED = path.join(OUTPUT_STYLES, 'shared');
|
||||
const OUTPUT_SCRIPTS = path.join(OUTPUT_ROOT, 'scripts');
|
||||
const OUTPUT_SCRIPTS_SHARED = path.join(OUTPUT_SCRIPTS, 'shared');
|
||||
|
||||
const generateConfig = async () => {
|
||||
let configs = [];
|
||||
|
||||
getGlobalConfig(configs);
|
||||
await getPathsConfigs(configs);
|
||||
|
||||
return configs;
|
||||
};
|
||||
const getGlobalConfig = (configs) => {
|
||||
const globalScriptPath = path.join(INPUT_SHARED_ROOT, 'scripts', GLOBAL_FILE_NAME);
|
||||
const outputPath = path.join(OUTPUT_SCRIPTS_SHARED, GLOBAL_FILE_NAME);
|
||||
|
||||
const sharedStylesGlob = path.join(INPUT_SHARED_ROOT, 'styles/**/*.css').replace(/\\/g, '/'); // Windows path not supported by copy plugin
|
||||
const staticGlob = path.join(INPUT_STATIC_ROOT, '/**/*.*').replace(/\\/g, '/'); // Windows path not supported by copy plugin
|
||||
|
||||
configs.push({
|
||||
input: globalScriptPath,
|
||||
output: {
|
||||
name: 'global',
|
||||
file: outputPath,
|
||||
format: 'iife'
|
||||
},
|
||||
plugins: [
|
||||
nodeResolve(),
|
||||
copy({
|
||||
targets: [
|
||||
{ src: sharedStylesGlob, dest: OUTPUT_STYLES_SHARED },
|
||||
{ src: staticGlob, dest: OUTPUT_ROOT },
|
||||
],
|
||||
verbose: true
|
||||
})
|
||||
]
|
||||
})
|
||||
|
||||
};
|
||||
const getPathsConfigs = async (configs) => {
|
||||
try {
|
||||
// Collect paths to process
|
||||
const paths = await fs.readdir(INPUT_PATHS_ROOT);
|
||||
|
||||
for (const itemPath of paths) {
|
||||
const itemRoot = path.join(INPUT_PATHS_ROOT, itemPath);
|
||||
const itemFiles = await fs.readdir(itemRoot);
|
||||
|
||||
if (itemFiles.indexOf(ENTRY_FILE_NAME) < 0) {
|
||||
throw Error(`Missing entry script for "${itemPath}" path`);
|
||||
}
|
||||
if (itemFiles.indexOf(TEMPLATE_FILE_NAME) < 0) {
|
||||
throw Error(`Missing HTML template for "${itemPath}" path`);
|
||||
}
|
||||
|
||||
const entryPath = path.join(itemRoot, ENTRY_FILE_NAME);
|
||||
const templatePath = path.join(itemRoot, TEMPLATE_FILE_NAME).replace(/\\/g, '/'); // Windows path not supported by copy plugin
|
||||
const bundlePath = path.join(OUTPUT_ROOT, 'scripts', `${itemPath}.js`);
|
||||
const htmlPath = path.join(OUTPUT_ROOT, `${itemPath}.html`);
|
||||
|
||||
configs.push({
|
||||
input: entryPath,
|
||||
output: {
|
||||
name: itemPath,
|
||||
file: bundlePath,
|
||||
format: 'iife'
|
||||
},
|
||||
plugins: [
|
||||
babel({
|
||||
exclude: 'node_modules/**',
|
||||
plugins: [
|
||||
[ '@babel/plugin-proposal-decorators', { decoratorsBeforeExport: true } ],
|
||||
'@babel/plugin-proposal-class-properties'
|
||||
]
|
||||
}),
|
||||
includePaths({
|
||||
paths: [ './' ]
|
||||
}),
|
||||
nodeResolve(),
|
||||
commonjs({
|
||||
sourceMap: false
|
||||
}),
|
||||
posthtmlTemplate({
|
||||
src: templatePath,
|
||||
dest: htmlPath
|
||||
})
|
||||
]
|
||||
})
|
||||
}
|
||||
} catch (exc) {
|
||||
console.error(exc);
|
||||
}
|
||||
};
|
||||
|
||||
export default generateConfig();
|
||||
85
src/paths/index/components/IndexDescription.js
Normal file
85
src/paths/index/components/IndexDescription.js
Normal file
@@ -0,0 +1,85 @@
|
||||
import { LitElement, html, css, customElement, property } from 'lit-element';
|
||||
|
||||
@customElement('gr-index-description')
|
||||
export default class IndexDescription extends LitElement {
|
||||
static get styles() {
|
||||
return css`
|
||||
/** Colors and variables **/
|
||||
:host {
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:host {
|
||||
}
|
||||
}
|
||||
|
||||
/** Component styling **/
|
||||
:host {
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
:host .header-description {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
color: var(--dimmed-font-color);
|
||||
}
|
||||
|
||||
:host .header-description-column {
|
||||
flex: 2;
|
||||
}
|
||||
:host .header-description-column.header-extra-links {
|
||||
flex: 1;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
:host .header-description a {
|
||||
color: var(--link-font-color);
|
||||
text-decoration: none;
|
||||
}
|
||||
:host .header-description a:hover {
|
||||
color: var(--link-font-color-hover);
|
||||
}
|
||||
|
||||
:host hr {
|
||||
border: none;
|
||||
border-top: 1px solid var(--g-background-extra-color);
|
||||
width: 30%;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 900px) {
|
||||
:host .header-description {
|
||||
padding: 0 8px;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
:host .header-description-column {
|
||||
width: 100%;
|
||||
}
|
||||
:host .header-description-column.header-extra-links {
|
||||
text-align: center;
|
||||
padding-top: 12px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
@property({ type: Date }) generated_at = null;
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div class="header-description">
|
||||
<div class="header-description-column">
|
||||
This page provides links to the latest CI build artifacts for
|
||||
active development branches.
|
||||
<br>
|
||||
<strong>
|
||||
These builds may not be suitable for production use.
|
||||
<br>
|
||||
Please use them for testing purposes only.
|
||||
</strong>
|
||||
</div>
|
||||
<div class="header-description-column header-extra-links">
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
116
src/paths/index/components/IndexHeader.js
Normal file
116
src/paths/index/components/IndexHeader.js
Normal file
@@ -0,0 +1,116 @@
|
||||
import { LitElement, html, css, customElement, property } from 'lit-element';
|
||||
|
||||
@customElement('gr-index-entry')
|
||||
export default class IndexHeader extends LitElement {
|
||||
static get styles() {
|
||||
return css`
|
||||
/** Colors and variables **/
|
||||
:host {
|
||||
--header-meta-color: #98a5b8;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:host {
|
||||
--header-meta-color: #515c6c;
|
||||
}
|
||||
}
|
||||
|
||||
/** Component styling **/
|
||||
:host {
|
||||
}
|
||||
|
||||
:host .header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
:host .header-metadata {
|
||||
color: var(--header-meta-color);
|
||||
text-align: right;
|
||||
}
|
||||
:host .header-metadata a {
|
||||
color: var(--link-font-color);
|
||||
text-decoration: none;
|
||||
}
|
||||
:host .header-metadata a:hover {
|
||||
color: var(--link-font-color-hover);
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 900px) {
|
||||
:host .header {
|
||||
flex-wrap: wrap;
|
||||
text-align: center;
|
||||
}
|
||||
:host .header-title,
|
||||
:host .header-metadata {
|
||||
width: 100%;
|
||||
}
|
||||
:host .header-metadata {
|
||||
padding-bottom: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
@property({ type: Date }) generated_at = null;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
// Auto-refresh about once a minute so that the relative time of generation is always actual.
|
||||
this._refreshTimeout = setTimeout(this._refresh.bind(this), 60 * 1000);
|
||||
}
|
||||
|
||||
_refresh() {
|
||||
this.requestUpdate();
|
||||
|
||||
// Continue updating.
|
||||
this._refreshTimeout = setTimeout(this._refresh.bind(this), 60 * 1000);
|
||||
}
|
||||
|
||||
render() {
|
||||
let generatedAt = "";
|
||||
let generatedRel = "";
|
||||
|
||||
if (this.generated_at) {
|
||||
generatedAt = greports.format.formatTimestamp(this.generated_at);
|
||||
|
||||
let timeValue = (Date.now() - this.generated_at) / (1000 * 60);
|
||||
let timeUnit = "minute";
|
||||
|
||||
if (timeValue < 1) {
|
||||
generatedRel = "just now";
|
||||
} else {
|
||||
if (timeValue > 60) {
|
||||
timeValue = timeValue / 60;
|
||||
timeUnit = "hour";
|
||||
}
|
||||
|
||||
generatedRel = greports.format.formatTimespan(-Math.round(timeValue), timeUnit);
|
||||
}
|
||||
}
|
||||
|
||||
return html`
|
||||
<div class="header">
|
||||
<h1 class="header-title">
|
||||
Godot Commit Artifacts
|
||||
</h1>
|
||||
<div class="header-metadata">
|
||||
${(this.generated_at ? html`
|
||||
<span title="${generatedAt}">
|
||||
data generated ${generatedRel}
|
||||
</span>
|
||||
` : '')}
|
||||
<br/>
|
||||
<a
|
||||
href="https://github.com/godotengine/godot-commit-artifacts"
|
||||
target="_blank"
|
||||
>
|
||||
contribute on GitHub
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
119
src/paths/index/components/branches/BranchItem.js
Normal file
119
src/paths/index/components/branches/BranchItem.js
Normal file
@@ -0,0 +1,119 @@
|
||||
import { LitElement, html, css, customElement, property } from 'lit-element';
|
||||
|
||||
@customElement('gr-branch-item')
|
||||
export default class BranchItem extends LitElement {
|
||||
static get styles() {
|
||||
return css`
|
||||
/** Colors and variables **/
|
||||
:host {
|
||||
--tab-hover-background-color: rgba(0, 0, 0, 0.14);
|
||||
--tab-active-background-color: #d6e6ff;
|
||||
--tab-active-border-color: #397adf;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:host {
|
||||
--tab-hover-background-color: rgba(255, 255, 255, 0.14);
|
||||
--tab-active-background-color: #283446;
|
||||
--tab-active-border-color: #5394f9;
|
||||
}
|
||||
}
|
||||
|
||||
/** Component styling **/
|
||||
:host {
|
||||
max-width: 200px;
|
||||
}
|
||||
|
||||
:host .branch-item {
|
||||
border-left: 5px solid transparent;
|
||||
color: var(--g-font-color);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 6px;
|
||||
padding: 6px 16px;
|
||||
align-items: center;
|
||||
}
|
||||
:host .branch-item:hover {
|
||||
background-color: var(--tab-hover-background-color);
|
||||
}
|
||||
:host .branch-item--active {
|
||||
background-color: var(--tab-active-background-color);
|
||||
border-left: 5px solid var(--tab-active-border-color);
|
||||
}
|
||||
|
||||
:host .branch-title {
|
||||
flex-grow: 1;
|
||||
font-size: 15px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@keyframes loader-rotate {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
:host .branch-loader {
|
||||
background-image: url('loader.svg');
|
||||
background-size: 20px 20px;
|
||||
background-position: 50% 50%;
|
||||
background-repeat: no-repeat;
|
||||
border-radius: 2px;
|
||||
display: inline-block;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
min-width: 20px;
|
||||
animation-name: loader-rotate;
|
||||
animation-duration: 1.25s;
|
||||
animation-timing-function: steps(8);
|
||||
animation-iteration-count: infinite;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
:host .branch-loader {
|
||||
filter: invert(1);
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 900px) {
|
||||
:host .branch-item {
|
||||
padding: 10px 20px;
|
||||
}
|
||||
|
||||
:host .branch-title {
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
@property({ type: String, reflect: true }) name = "";
|
||||
@property({ type: Boolean, reflect: true }) active = false;
|
||||
@property({ type: Boolean, reflect: true }) loading = false;
|
||||
|
||||
render(){
|
||||
const classList = [ "branch-item" ];
|
||||
if (this.active) {
|
||||
classList.push("branch-item--active");
|
||||
}
|
||||
|
||||
return html`
|
||||
<div
|
||||
class="${classList.join(" ")}"
|
||||
title="${this.name}"
|
||||
>
|
||||
<span class="branch-title">
|
||||
${this.name}
|
||||
</span>
|
||||
|
||||
${(this.loading ? html`
|
||||
<div class="branch-loader"></div>
|
||||
` : null)}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
73
src/paths/index/components/branches/BranchList.js
Normal file
73
src/paths/index/components/branches/BranchList.js
Normal file
@@ -0,0 +1,73 @@
|
||||
import { LitElement, html, css, customElement, property } from 'lit-element';
|
||||
|
||||
import BranchItem from "./BranchItem";
|
||||
|
||||
@customElement('gr-branch-list')
|
||||
export default class BranchList extends LitElement {
|
||||
static get styles() {
|
||||
return css`
|
||||
/** Colors and variables **/
|
||||
:host {
|
||||
--branches-background-color: #fcfcfa;
|
||||
--branches-border-color: #515c6c;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:host {
|
||||
--branches-background-color: #0d1117;
|
||||
--branches-border-color: #515c6c;
|
||||
}
|
||||
}
|
||||
|
||||
/** Component styling **/
|
||||
:host {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
:host .branch-list {
|
||||
background-color: var(--branches-background-color);
|
||||
border-right: 2px solid var(--branches-border-color);
|
||||
width: 200px;
|
||||
min-height: 216px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 900px) {
|
||||
:host {
|
||||
width: 100%
|
||||
}
|
||||
|
||||
:host .branch-list {
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
@property({ type: Array }) branches = [];
|
||||
@property({ type: Array }) loadingBranchess = [];
|
||||
@property({ type: String }) selectedBranch = "";
|
||||
|
||||
_onItemClicked(branchName) {
|
||||
this.dispatchEvent(greports.util.createEvent("branchclick", {
|
||||
"branch": branchName,
|
||||
}));
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div class="branch-list">
|
||||
${this.branches.map((item) => {
|
||||
return html`
|
||||
<div class="branch-list-main">
|
||||
<gr-branch-item
|
||||
.name="${item}"
|
||||
?active="${this.selectedBranch === item}"
|
||||
?loading="${this.loadingBranches.includes(item)}"
|
||||
@click="${this._onItemClicked.bind(this, item)}"
|
||||
></gr-branch-item>
|
||||
</div>
|
||||
`;
|
||||
})}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
139
src/paths/index/components/commits/CommitItem.js
Normal file
139
src/paths/index/components/commits/CommitItem.js
Normal file
@@ -0,0 +1,139 @@
|
||||
import { LitElement, html, css, customElement, property } from 'lit-element';
|
||||
|
||||
@customElement('gr-commit-item')
|
||||
export default class CommitItem extends LitElement {
|
||||
static get styles() {
|
||||
return css`
|
||||
/** Colors and variables **/
|
||||
:host {
|
||||
--item-border-color: #fcfcfa;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:host {
|
||||
--item-border-color: #0d1117;
|
||||
}
|
||||
}
|
||||
|
||||
/** Component styling **/
|
||||
:host {
|
||||
border-bottom: 3px solid var(--item-border-color);
|
||||
display: block;
|
||||
padding: 14px 12px 20px 12px;
|
||||
}
|
||||
|
||||
:host a {
|
||||
color: var(--link-font-color);
|
||||
text-decoration: none;
|
||||
}
|
||||
:host a:hover {
|
||||
color: var(--link-font-color-hover);
|
||||
}
|
||||
|
||||
:host .item-title {
|
||||
display: inline-flex;
|
||||
justify-content: space-between;
|
||||
font-size: 20px;
|
||||
margin-top: 6px;
|
||||
margin-bottom: 12px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:host .item-subtitle {
|
||||
color: var(--dimmed-font-color);
|
||||
font-size: 16px;
|
||||
line-height: 20px;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
:host .item-workflows {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
:host .workflow {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 8px;
|
||||
border-bottom: 2px solid var(--g-background-extra-color);
|
||||
padding: 12px 10px;
|
||||
}
|
||||
|
||||
:host .workflow-artifacts {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
color: var(--dimmed-font-color);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
:host .workflow-artifacts a {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 900px) {
|
||||
:host {
|
||||
padding: 14px 0 20px 0;
|
||||
}
|
||||
|
||||
:host .workflow {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 640px) {
|
||||
:host .item-container {
|
||||
padding: 0 10px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
@property({ type: String, reflect: true }) hash = '';
|
||||
@property({ type: String }) title = '';
|
||||
@property({ type: Array }) workflows = [];
|
||||
|
||||
@property({ type: String }) repository = '';
|
||||
|
||||
render(){
|
||||
return html`
|
||||
<div class="item-container">
|
||||
<div class="item-title">
|
||||
<span>${greports.format.formatTimestamp(this.committed_date)}</span>
|
||||
<a
|
||||
href="https://github.com/${this.repository}/commit/${this.hash}"
|
||||
target="_blank"
|
||||
title="Open commit #${this.hash} on GitHub"
|
||||
>
|
||||
#${this.hash.substring(0, 9)}
|
||||
</a>
|
||||
</div>
|
||||
<div class="item-subtitle">${this.title}</div>
|
||||
<div class="item-workflows">
|
||||
${this.workflows.map((item) => {
|
||||
return html`
|
||||
<div class="workflow">
|
||||
<div class="workflow-name">${item.name}</div>
|
||||
<div class="workflow-artifacts">
|
||||
${item.artifacts.map((artifact) => {
|
||||
return html`
|
||||
<span>
|
||||
<a
|
||||
href="https://github.com/godotengine/godot/suites/${item.check_id}/artifacts/${artifact.id}"
|
||||
target="_blank"
|
||||
>
|
||||
${artifact.name}
|
||||
</a>
|
||||
<span>(${greports.format.humanizeBytes(artifact.size)})</span>
|
||||
</span>
|
||||
`;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
122
src/paths/index/components/commits/CommitList.js
Normal file
122
src/paths/index/components/commits/CommitList.js
Normal file
@@ -0,0 +1,122 @@
|
||||
import { LitElement, html, css, customElement, property } from 'lit-element';
|
||||
|
||||
import CommitItem from "./CommitItem";
|
||||
|
||||
@customElement('gr-commit-list')
|
||||
export default class CommitList extends LitElement {
|
||||
static get styles() {
|
||||
return css`
|
||||
/** Colors and variables **/
|
||||
:host {
|
||||
--item-border-color: #fcfcfa;
|
||||
--commits-background-color: #e5edf8;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:host {
|
||||
--item-border-color: #0d1117;
|
||||
--commits-background-color: #191d23;
|
||||
}
|
||||
}
|
||||
|
||||
/** Component styling **/
|
||||
:host {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
:host .branch-commits {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
background-color: var(--commits-background-color);
|
||||
border-radius: 0 4px 4px 0;
|
||||
padding: 8px 12px;
|
||||
max-width: 760px;
|
||||
}
|
||||
@media only screen and (max-width: 900px) {
|
||||
:host .branch-commits {
|
||||
padding: 8px;
|
||||
max-width: 95%;
|
||||
margin: 0px auto;
|
||||
}
|
||||
}
|
||||
|
||||
:host .branch-commits-empty {
|
||||
color: var(--g-font-color);
|
||||
display: inline-block;
|
||||
font-size: 20px;
|
||||
line-height: 24px;
|
||||
margin-top: 6px;
|
||||
margin-bottom: 12px;
|
||||
padding: 14px 12px;
|
||||
word-break: break-word;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
@property({ type: Array }) commits = [];
|
||||
@property({ type: Object }) checks = {};
|
||||
@property({ type: Object }) runs = {};
|
||||
@property({ type: Object }) artifacts = {};
|
||||
|
||||
@property({ type: String }) selectedRepository = "";
|
||||
@property({ type: String }) selectedBranch = "";
|
||||
@property({ type: Boolean, reflect: true }) loading = false;
|
||||
|
||||
render(){
|
||||
if (this.selectedBranch === "") {
|
||||
return html``;
|
||||
}
|
||||
if (this.loading) {
|
||||
return html`
|
||||
<span class="branch-commits-empty">Loading artifacts...</span>
|
||||
`
|
||||
}
|
||||
|
||||
return html`
|
||||
<div class="branch-commits">
|
||||
${this.commits.map((item) => {
|
||||
let workflows = [];
|
||||
|
||||
for (let checkId in this.checks) {
|
||||
const check = this.checks[checkId];
|
||||
if (item.checks.indexOf(check.check_id) < 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (check.workflow == null || typeof this.runs[check.workflow] === "undefined") {
|
||||
continue;
|
||||
}
|
||||
|
||||
const run = this.runs[check.workflow];
|
||||
if (run.artifacts.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
workflows.push({
|
||||
"name": run.name,
|
||||
"name_sanitized": run.name.replace(/([^a-zA-Z0-9_\- ]+)/g, "").trim().toLowerCase(),
|
||||
"check_id": check.check_id,
|
||||
"artifacts": run.artifacts,
|
||||
});
|
||||
}
|
||||
|
||||
workflows.sort((a,b) => {
|
||||
if (a.name_sanitized > b.name_sanitized) return 1;
|
||||
if (a.name_sanitized < b.name_sanitized) return -1;
|
||||
return 0;
|
||||
});
|
||||
|
||||
return html`
|
||||
<gr-commit-item
|
||||
.hash="${item.hash}"
|
||||
.title="${item.title}"
|
||||
.committed_date="${item.committed_date}"
|
||||
.workflows="${workflows}"
|
||||
.repository="${this.selectedRepository}"
|
||||
/>
|
||||
`;
|
||||
})}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
167
src/paths/index/entry.js
Normal file
167
src/paths/index/entry.js
Normal file
@@ -0,0 +1,167 @@
|
||||
import { LitElement, html, css, customElement, property } from 'lit-element';
|
||||
|
||||
import PageContent from 'src/shared/components/PageContent';
|
||||
import SharedNavigation from 'src/shared/components/SharedNavigation';
|
||||
import IndexHeader from "./components/IndexHeader";
|
||||
import IndexDescription from "./components/IndexDescription";
|
||||
|
||||
import BranchList from "./components/branches/BranchList";
|
||||
import CommitList from "./components/commits/CommitList";
|
||||
|
||||
@customElement('entry-component')
|
||||
export default class EntryComponent extends LitElement {
|
||||
static get styles() {
|
||||
return css`
|
||||
/** Colors and variables **/
|
||||
:host {
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:host {
|
||||
}
|
||||
}
|
||||
|
||||
/** Component styling **/
|
||||
:host {
|
||||
}
|
||||
|
||||
:host .branches {
|
||||
display: flex;
|
||||
padding: 24px 0;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 900px) {
|
||||
:host .branches {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this._entryRequested = false;
|
||||
this._isLoading = true;
|
||||
this._loadingBranches = [];
|
||||
|
||||
this._branches = [ "master" ];
|
||||
this._branchData = {};
|
||||
|
||||
this._selectedRepository = "godotengine/godot";
|
||||
this._selectedBranch = "";
|
||||
|
||||
this._restoreUserPreferences();
|
||||
this._requestData();
|
||||
}
|
||||
|
||||
performUpdate() {
|
||||
this._requestData();
|
||||
super.performUpdate();
|
||||
}
|
||||
|
||||
_restoreUserPreferences() {
|
||||
const userPreferences = greports.util.getLocalPreferences();
|
||||
|
||||
// ...
|
||||
}
|
||||
|
||||
_saveUserPreferences() {
|
||||
const currentPreferences = {
|
||||
// ...
|
||||
};
|
||||
|
||||
greports.util.setLocalPreferences(currentPreferences);
|
||||
}
|
||||
|
||||
async _requestData() {
|
||||
if (this._entryRequested) {
|
||||
return;
|
||||
}
|
||||
this._entryRequested = true;
|
||||
this._isLoading = true;
|
||||
|
||||
this._isLoading = false;
|
||||
this.requestUpdate();
|
||||
|
||||
this._branches.forEach((branch) => {
|
||||
this._requestBranchData(branch);
|
||||
});
|
||||
}
|
||||
|
||||
async _requestBranchData(branch) {
|
||||
// Start loading, show the indicator.
|
||||
this._loadingBranches.push(branch);
|
||||
|
||||
const branchData = await greports.api.getBranchData(this._selectedRepository, branch);
|
||||
|
||||
if (branchData) {
|
||||
this._branchData[branch] = branchData;
|
||||
}
|
||||
|
||||
// Finish loading, hide the indicator.
|
||||
const index = this._loadingBranches.indexOf(branch);
|
||||
this._loadingBranches.splice(index, 1);
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
_onBranchClicked(event) {
|
||||
this._selectedBranch = event.detail.branch;
|
||||
this.requestUpdate();
|
||||
|
||||
window.scrollTo(0, 0);
|
||||
}
|
||||
|
||||
render() {
|
||||
// Dereferencing to ensure it triggers an update.
|
||||
const [...branches] = this._branches;
|
||||
const [...loadingBranches] = this._loadingBranches;
|
||||
|
||||
let commits = [];
|
||||
let checks = {};
|
||||
let runs = {};
|
||||
let artifacts = {};
|
||||
|
||||
if (this._selectedBranch !== "" && typeof this._branchData[this._selectedBranch] !== "undefined") {
|
||||
const branchData = this._branchData[this._selectedBranch];
|
||||
|
||||
commits = branchData.commits;
|
||||
checks = branchData.checks;
|
||||
runs = branchData.runs;
|
||||
artifacts = branchData.artifacts;
|
||||
}
|
||||
|
||||
return html`
|
||||
<page-content>
|
||||
<shared-nav></shared-nav>
|
||||
<gr-index-entry></gr-index-entry>
|
||||
<gr-index-description></gr-index-description>
|
||||
|
||||
${(this._isLoading ? html`
|
||||
<h3>Loading...</h3>
|
||||
` : html`
|
||||
<div class="branches">
|
||||
<gr-branch-list
|
||||
.branches="${branches}"
|
||||
.loadingBranches="${loadingBranches}"
|
||||
.selectedBranch="${this._selectedBranch}"
|
||||
@branchclick="${this._onBranchClicked}"
|
||||
></gr-branch-list>
|
||||
|
||||
${(this._selectedBranch !== "" ? html`
|
||||
<gr-commit-list
|
||||
.commits="${commits}"
|
||||
.checks="${checks}"
|
||||
.runs="${runs}"
|
||||
.artifacts="${artifacts}"
|
||||
|
||||
.selectedRepository="${this._selectedRepository}"
|
||||
.selectedBranch="${this._selectedBranch}"
|
||||
?loading="${loadingBranches.indexOf(this._selectedBranch) >= 0}"
|
||||
></gr-commit-list>
|
||||
` : null)}
|
||||
</div>
|
||||
`)}
|
||||
</page-content>
|
||||
`;
|
||||
}
|
||||
}
|
||||
16
src/paths/index/template.html
Normal file
16
src/paths/index/template.html
Normal file
@@ -0,0 +1,16 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<include src="shared/partials/head_content.html"></include>
|
||||
|
||||
<title>Godot Interactive Changelog</title>
|
||||
<meta name="description" content="Godot Engine interactive changelog for each official release of the engine">
|
||||
<meta name="keywords" content="godot, godot engine, gamedev, changelog, project management">
|
||||
|
||||
<script src="scripts/index.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<entry-component></entry-component>
|
||||
<include src="shared/partials/body_content.html"></include>
|
||||
</body>
|
||||
</html>
|
||||
28
src/shared/components/PageContent.js
Normal file
28
src/shared/components/PageContent.js
Normal file
@@ -0,0 +1,28 @@
|
||||
import { LitElement, html, css, customElement } from 'lit-element';
|
||||
|
||||
@customElement('page-content')
|
||||
export default class PageContent extends LitElement {
|
||||
static get styles() {
|
||||
return css`
|
||||
/** Component styling **/
|
||||
:host {
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
padding: 0 12px;
|
||||
max-width: 1024px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 900px) {
|
||||
:host {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
render(){
|
||||
return html`
|
||||
<slot></slot>
|
||||
`;
|
||||
}
|
||||
}
|
||||
119
src/shared/components/SharedNavigation.js
Normal file
119
src/shared/components/SharedNavigation.js
Normal file
@@ -0,0 +1,119 @@
|
||||
import { LitElement, html, css, customElement } from 'lit-element';
|
||||
|
||||
@customElement('shared-nav')
|
||||
export default class SharedNavigation extends LitElement {
|
||||
static get styles() {
|
||||
return css`
|
||||
/** Colors and variables **/
|
||||
:host {
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:host {
|
||||
}
|
||||
}
|
||||
|
||||
/** Component styling **/
|
||||
:host {
|
||||
}
|
||||
|
||||
:host .nav-container a {
|
||||
color: var(--link-font-color);
|
||||
text-decoration: none;
|
||||
}
|
||||
:host .nav-container a:hover {
|
||||
color: var(--link-font-color-hover);
|
||||
}
|
||||
|
||||
:host .nav-container {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 8px;
|
||||
background: var(--g-background-color);
|
||||
}
|
||||
|
||||
:host .nav-item {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
padding: 10px 16px;
|
||||
}
|
||||
:host .nav-item:hover {
|
||||
background-color: var(--g-background-extra2-color);
|
||||
}
|
||||
|
||||
:host .nav-toggler {
|
||||
display: none;
|
||||
background-image: url('hamburger.svg');
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
}
|
||||
:host .nav-toggler:hover {
|
||||
background-color: var(--g-background-extra2-color);
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 640px) {
|
||||
:host .nav-container {
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding-top: 40px;
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
:host .nav-container.nav-active {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
:host .nav-toggler {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this._mobileActive = false;
|
||||
}
|
||||
|
||||
_onMobileToggled() {
|
||||
this._mobileActive = !this._mobileActive;
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
render(){
|
||||
const containerClassList = [ "nav-container" ];
|
||||
if (this._mobileActive) {
|
||||
containerClassList.push("nav-active");
|
||||
}
|
||||
|
||||
return html`
|
||||
<div class="${containerClassList.join(" ")}">
|
||||
<a href="https://godotengine.github.io/doc-status/" target="_blank" class="nav-item">
|
||||
ClassRef Status
|
||||
</a>
|
||||
<a href="https://godot-proposals-viewer.github.io/" target="_blank" class="nav-item">
|
||||
Proposal Viewer
|
||||
</a>
|
||||
<a href="https://godotengine.github.io/godot-team-reports/" target="_blank" class="nav-item">
|
||||
Team Reports
|
||||
</a>
|
||||
<a href="https://godotengine.github.io/godot-prs-by-file/" target="_blank" class="nav-item">
|
||||
PRs by File
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
class="nav-toggler"
|
||||
@click="${this._onMobileToggled}"
|
||||
></div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
0
src/shared/partials/body_content.html
Normal file
0
src/shared/partials/body_content.html
Normal file
9
src/shared/partials/head_content.html
Normal file
9
src/shared/partials/head_content.html
Normal file
@@ -0,0 +1,9 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link href="https://fonts.googleapis.com/css?family=Roboto:400,500i,700,900&display=swap&subset=latin-ext" rel="stylesheet">
|
||||
<link rel="icon" type="image/png" href="favicon.png" />
|
||||
|
||||
<link href="styles/shared/normalize.css" rel="stylesheet">
|
||||
<link href="styles/shared/global.css" rel="stylesheet">
|
||||
|
||||
<script src="scripts/shared/global.js"></script>
|
||||
155
src/shared/scripts/global.js
Normal file
155
src/shared/scripts/global.js
Normal file
@@ -0,0 +1,155 @@
|
||||
const LOCAL_PREFERENCE_PREFIX = "_godot_cmtar"
|
||||
const LOCAL_PREFERENCE_DEFAULTS = {
|
||||
|
||||
};
|
||||
|
||||
// API Interaction
|
||||
const ReportsAPI = {
|
||||
async get(path = '/') {
|
||||
const res = await fetch(`${path}`);
|
||||
if (res.status !== 200) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return await res.json();
|
||||
},
|
||||
|
||||
async getBranchData(repositoryId, branchName) {
|
||||
const idBits = repositoryId.split("/");
|
||||
|
||||
return await this.get(`data/${idBits[0]}.${idBits[1]}.${branchName}.json`);
|
||||
},
|
||||
};
|
||||
|
||||
// Content helpers
|
||||
const ReportsFormatter = {
|
||||
formatDate(dateString) {
|
||||
const options = {
|
||||
year: 'numeric', month: 'long', day: 'numeric',
|
||||
};
|
||||
const dateFormatter = new Intl.DateTimeFormat('en-US', options);
|
||||
|
||||
const date = new Date(dateString);
|
||||
return dateFormatter.format(date);
|
||||
},
|
||||
|
||||
formatTimestamp(timeString) {
|
||||
const options = {
|
||||
year: 'numeric', month: 'long', day: 'numeric',
|
||||
hour: 'numeric', hour12: false, minute: 'numeric',
|
||||
timeZone: 'UTC', timeZoneName: 'short',
|
||||
};
|
||||
const dateFormatter = new Intl.DateTimeFormat('en-US', options);
|
||||
|
||||
const date = new Date(timeString);
|
||||
return dateFormatter.format(date);
|
||||
},
|
||||
|
||||
formatTimespan(timeValue, timeUnit) {
|
||||
const options = {
|
||||
style: 'long',
|
||||
};
|
||||
const timeFormatter = new Intl.RelativeTimeFormat('en-US', options);
|
||||
|
||||
return timeFormatter.format(timeValue, timeUnit);
|
||||
},
|
||||
|
||||
getDaysSince(dateString) {
|
||||
const date = new Date(dateString);
|
||||
const msBetween = (new Date()) - date;
|
||||
const days = Math.floor(msBetween / (1000 * 60 * 60 * 24));
|
||||
|
||||
return days;
|
||||
},
|
||||
|
||||
formatDays(days) {
|
||||
return days + " " + (days !== 1 ? "days" : "day");
|
||||
},
|
||||
|
||||
humanizeBytes(bytes) {
|
||||
if (bytes < 1024) {
|
||||
return `${bytes} B`;
|
||||
}
|
||||
|
||||
bytes = bytes / 1024;
|
||||
if (bytes < 1024) {
|
||||
return `${Math.round(bytes, 2)} KB`;
|
||||
}
|
||||
|
||||
bytes = bytes / 1024;
|
||||
if (bytes < 1024) {
|
||||
return `${Math.round(bytes, 2)} MB`;
|
||||
}
|
||||
|
||||
bytes = bytes / 1024;
|
||||
if (bytes < 1024) {
|
||||
return `${Math.round(bytes, 2)} GB`;
|
||||
}
|
||||
|
||||
bytes = bytes / 1024;
|
||||
return `${Math.round(bytes, 2)} TB`;
|
||||
}
|
||||
};
|
||||
|
||||
const ReportsUtils = {
|
||||
createEvent(name, detail = {}) {
|
||||
return new CustomEvent(name, {
|
||||
detail: detail
|
||||
});
|
||||
},
|
||||
|
||||
getHistoryHash() {
|
||||
let rawHash = window.location.hash;
|
||||
if (rawHash !== "") {
|
||||
return rawHash.substr(1);
|
||||
}
|
||||
|
||||
return "";
|
||||
},
|
||||
|
||||
setHistoryHash(hash) {
|
||||
const url = new URL(window.location);
|
||||
url.hash = hash;
|
||||
window.history.pushState({}, "", url);
|
||||
},
|
||||
|
||||
navigateHistoryHash(hash) {
|
||||
this.setHistoryHash(hash);
|
||||
window.location.reload();
|
||||
},
|
||||
|
||||
getLocalPreferences() {
|
||||
// Always fallback on defaults.
|
||||
const localPreferences = { ...LOCAL_PREFERENCE_DEFAULTS };
|
||||
|
||||
for (let key in localPreferences) {
|
||||
const storedValue = localStorage.getItem(`${LOCAL_PREFERENCE_PREFIX}_${key}`);
|
||||
if (storedValue != null) {
|
||||
localPreferences[key] = JSON.parse(storedValue);
|
||||
}
|
||||
}
|
||||
|
||||
return localPreferences;
|
||||
},
|
||||
|
||||
setLocalPreferences(currentPreferences) {
|
||||
for (let key in currentPreferences) {
|
||||
// Only store known properties.
|
||||
if (key in LOCAL_PREFERENCE_DEFAULTS) {
|
||||
localStorage.setItem(`${LOCAL_PREFERENCE_PREFIX}_${key}`, JSON.stringify(currentPreferences[key]));
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
resetLocalPreferences() {
|
||||
this.setLocalPreferences(LOCAL_PREFERENCE_DEFAULTS);
|
||||
},
|
||||
};
|
||||
|
||||
const ReportsSingleton = {
|
||||
api: ReportsAPI,
|
||||
format: ReportsFormatter,
|
||||
util: ReportsUtils,
|
||||
};
|
||||
|
||||
window.greports = ReportsSingleton;
|
||||
55
src/shared/styles/global.css
Normal file
55
src/shared/styles/global.css
Normal file
@@ -0,0 +1,55 @@
|
||||
/** Colors and variables **/
|
||||
:root {
|
||||
--g-background-color: #fcfcfa;
|
||||
--g-background-extra-color: #98a5b8;
|
||||
--g-background-extra2-color: #cad3e1;
|
||||
--g-font-color: #121314;
|
||||
--g-font-size: 15px;
|
||||
--g-font-weight: 400;
|
||||
--g-line-height: 20px;
|
||||
|
||||
--link-font-color: #1d6dff;
|
||||
--link-font-color-hover: #1051c9;
|
||||
--link-font-color-inactive: #35496f;
|
||||
|
||||
--dimmed-font-color: #535c5f;
|
||||
--light-font-color: #6b7893;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--g-background-color: #0d1117;
|
||||
--g-background-extra-color: #515c6c;
|
||||
--g-background-extra2-color: #22252b;
|
||||
--g-font-color: rgba(228, 228, 232, 0.9);
|
||||
|
||||
--link-font-color: #367df7;
|
||||
--link-font-color-hover: #6391ec;
|
||||
--link-font-color-inactive: #abbdcc;
|
||||
|
||||
--dimmed-font-color: #929da0;
|
||||
--light-font-color: #8491ab;
|
||||
}
|
||||
}
|
||||
|
||||
/** General styling **/
|
||||
html {}
|
||||
|
||||
body {
|
||||
background: var(--g-background-color);
|
||||
color: var(--g-font-color);
|
||||
font-family: 'Roboto', sans-serif;
|
||||
font-size: var(--g-font-size);
|
||||
font-weight: var(--g-font-weight);
|
||||
line-height: var(--g-line-height);
|
||||
min-width: 380px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--link-font-color);
|
||||
font-weight: 700;
|
||||
text-decoration: none;
|
||||
}
|
||||
a:hover {
|
||||
color: var(--link-font-color-hover);
|
||||
}
|
||||
349
src/shared/styles/normalize.css
vendored
Normal file
349
src/shared/styles/normalize.css
vendored
Normal file
@@ -0,0 +1,349 @@
|
||||
/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */
|
||||
|
||||
/* Document
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* 1. Correct the line height in all browsers.
|
||||
* 2. Prevent adjustments of font size after orientation changes in iOS.
|
||||
*/
|
||||
|
||||
html {
|
||||
line-height: 1.15; /* 1 */
|
||||
-webkit-text-size-adjust: 100%; /* 2 */
|
||||
}
|
||||
|
||||
/* Sections
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Remove the margin in all browsers.
|
||||
*/
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the `main` element consistently in IE.
|
||||
*/
|
||||
|
||||
main {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/**
|
||||
* Correct the font size and margin on `h1` elements within `section` and
|
||||
* `article` contexts in Chrome, Firefox, and Safari.
|
||||
*/
|
||||
|
||||
h1 {
|
||||
font-size: 2em;
|
||||
margin: 0.67em 0;
|
||||
}
|
||||
|
||||
/* Grouping content
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* 1. Add the correct box sizing in Firefox.
|
||||
* 2. Show the overflow in Edge and IE.
|
||||
*/
|
||||
|
||||
hr {
|
||||
box-sizing: content-box; /* 1 */
|
||||
height: 0; /* 1 */
|
||||
overflow: visible; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct the inheritance and scaling of font size in all browsers.
|
||||
* 2. Correct the odd `em` font sizing in all browsers.
|
||||
*/
|
||||
|
||||
pre {
|
||||
font-family: monospace, monospace; /* 1 */
|
||||
font-size: 1em; /* 2 */
|
||||
}
|
||||
|
||||
/* Text-level semantics
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Remove the gray background on active links in IE 10.
|
||||
*/
|
||||
|
||||
a {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Remove the bottom border in Chrome 57-
|
||||
* 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
|
||||
*/
|
||||
|
||||
abbr[title] {
|
||||
border-bottom: none; /* 1 */
|
||||
text-decoration: underline; /* 2 */
|
||||
text-decoration: underline dotted; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the correct font weight in Chrome, Edge, and Safari.
|
||||
*/
|
||||
|
||||
b,
|
||||
strong {
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct the inheritance and scaling of font size in all browsers.
|
||||
* 2. Correct the odd `em` font sizing in all browsers.
|
||||
*/
|
||||
|
||||
code,
|
||||
kbd,
|
||||
samp {
|
||||
font-family: monospace, monospace; /* 1 */
|
||||
font-size: 1em; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the correct font size in all browsers.
|
||||
*/
|
||||
|
||||
small {
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevent `sub` and `sup` elements from affecting the line height in
|
||||
* all browsers.
|
||||
*/
|
||||
|
||||
sub,
|
||||
sup {
|
||||
font-size: 75%;
|
||||
line-height: 0;
|
||||
position: relative;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
sub {
|
||||
bottom: -0.25em;
|
||||
}
|
||||
|
||||
sup {
|
||||
top: -0.5em;
|
||||
}
|
||||
|
||||
/* Embedded content
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Remove the border on images inside links in IE 10.
|
||||
*/
|
||||
|
||||
img {
|
||||
border-style: none;
|
||||
}
|
||||
|
||||
/* Forms
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* 1. Change the font styles in all browsers.
|
||||
* 2. Remove the margin in Firefox and Safari.
|
||||
*/
|
||||
|
||||
button,
|
||||
input,
|
||||
optgroup,
|
||||
select,
|
||||
textarea {
|
||||
font-family: inherit; /* 1 */
|
||||
font-size: 100%; /* 1 */
|
||||
line-height: 1.15; /* 1 */
|
||||
margin: 0; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the overflow in IE.
|
||||
* 1. Show the overflow in Edge.
|
||||
*/
|
||||
|
||||
button,
|
||||
input { /* 1 */
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the inheritance of text transform in Edge, Firefox, and IE.
|
||||
* 1. Remove the inheritance of text transform in Firefox.
|
||||
*/
|
||||
|
||||
button,
|
||||
select { /* 1 */
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
/**
|
||||
* Correct the inability to style clickable types in iOS and Safari.
|
||||
*/
|
||||
|
||||
button,
|
||||
[type="button"],
|
||||
[type="reset"],
|
||||
[type="submit"] {
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the inner border and padding in Firefox.
|
||||
*/
|
||||
|
||||
button::-moz-focus-inner,
|
||||
[type="button"]::-moz-focus-inner,
|
||||
[type="reset"]::-moz-focus-inner,
|
||||
[type="submit"]::-moz-focus-inner {
|
||||
border-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore the focus styles unset by the previous rule.
|
||||
*/
|
||||
|
||||
button:-moz-focusring,
|
||||
[type="button"]:-moz-focusring,
|
||||
[type="reset"]:-moz-focusring,
|
||||
[type="submit"]:-moz-focusring {
|
||||
outline: 1px dotted ButtonText;
|
||||
}
|
||||
|
||||
/**
|
||||
* Correct the padding in Firefox.
|
||||
*/
|
||||
|
||||
fieldset {
|
||||
padding: 0.35em 0.75em 0.625em;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct the text wrapping in Edge and IE.
|
||||
* 2. Correct the color inheritance from `fieldset` elements in IE.
|
||||
* 3. Remove the padding so developers are not caught out when they zero out
|
||||
* `fieldset` elements in all browsers.
|
||||
*/
|
||||
|
||||
legend {
|
||||
box-sizing: border-box; /* 1 */
|
||||
color: inherit; /* 2 */
|
||||
display: table; /* 1 */
|
||||
max-width: 100%; /* 1 */
|
||||
padding: 0; /* 3 */
|
||||
white-space: normal; /* 1 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the correct vertical alignment in Chrome, Firefox, and Opera.
|
||||
*/
|
||||
|
||||
progress {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the default vertical scrollbar in IE 10+.
|
||||
*/
|
||||
|
||||
textarea {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Add the correct box sizing in IE 10.
|
||||
* 2. Remove the padding in IE 10.
|
||||
*/
|
||||
|
||||
[type="checkbox"],
|
||||
[type="radio"] {
|
||||
box-sizing: border-box; /* 1 */
|
||||
padding: 0; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Correct the cursor style of increment and decrement buttons in Chrome.
|
||||
*/
|
||||
|
||||
[type="number"]::-webkit-inner-spin-button,
|
||||
[type="number"]::-webkit-outer-spin-button {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct the odd appearance in Chrome and Safari.
|
||||
* 2. Correct the outline style in Safari.
|
||||
*/
|
||||
|
||||
[type="search"] {
|
||||
-webkit-appearance: textfield; /* 1 */
|
||||
outline-offset: -2px; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the inner padding in Chrome and Safari on macOS.
|
||||
*/
|
||||
|
||||
[type="search"]::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct the inability to style clickable types in iOS and Safari.
|
||||
* 2. Change font properties to `inherit` in Safari.
|
||||
*/
|
||||
|
||||
::-webkit-file-upload-button {
|
||||
-webkit-appearance: button; /* 1 */
|
||||
font: inherit; /* 2 */
|
||||
}
|
||||
|
||||
/* Interactive
|
||||
========================================================================== */
|
||||
|
||||
/*
|
||||
* Add the correct display in Edge, IE 10+, and Firefox.
|
||||
*/
|
||||
|
||||
details {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/*
|
||||
* Add the correct display in all browsers.
|
||||
*/
|
||||
|
||||
summary {
|
||||
display: list-item;
|
||||
}
|
||||
|
||||
/* Misc
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Add the correct display in IE 10+.
|
||||
*/
|
||||
|
||||
template {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the correct display in IE 10.
|
||||
*/
|
||||
|
||||
[hidden] {
|
||||
display: none;
|
||||
}
|
||||
BIN
src/static/favicon.png
Normal file
BIN
src/static/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.9 KiB |
1
src/static/icons/loader.svg
Normal file
1
src/static/icons/loader.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><g fill="#e0e0e0"><g fill-opacity=".2"><path d="m10.061 11.466 2.152 2.152c-.924.694-2.023 1.166-3.22 1.336v-3.045c.381-.096.74-.247 1.068-.443z"/><path d="m5.939 11.466c.328.196.687.347 1.068.443v3.045c-1.197-.17-2.297-.642-3.22-1.336z"/><path d="m14.954 8.993c-.17 1.197-.642 2.297-1.336 3.22l-2.152-2.152c.196-.328.347-.687.443-1.068z"/><path d="m1.046 8.993h3.045c.096.381.247.74.443 1.068l-2.152 2.152c-.694-.923-1.166-2.023-1.336-3.22z"/><path d="m2.382 3.787 2.152 2.152c-.196.329-.347.687-.443 1.068h-3.045c.17-1.197.642-2.297 1.336-3.22z"/><path d="m13.618 3.787c.694.923 1.166 2.023 1.336 3.22h-3.045c-.096-.381-.247-.74-.443-1.068z"/><path d="m7.007 1.046v3.045c-.381.096-.74.247-1.068.443l-2.152-2.152c.923-.694 2.023-1.166 3.22-1.336z"/></g><path d="m8.993 1.046c1.197.17 2.297.642 3.22 1.336l-2.152 2.152c-.328-.196-.687-.347-1.068-.443z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 1011 B |
Reference in New Issue
Block a user