mirror of
https://github.com/godotengine/godot-prs-by-file.git
synced 2025-12-31 21:48:29 +03:00
Initial commit
This commit is contained in:
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 15 minutes of every hour, starting at 5 minutes (5m, 20m, 35m, 50m).
|
||||||
|
# The slight offset is there to try and avoid the high load times.
|
||||||
|
- cron: '5/15 * * * *'
|
||||||
|
|
||||||
|
# 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 pull request data
|
||||||
|
run: npm run compose-db
|
||||||
|
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
|
||||||
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# Project folders.
|
||||||
|
node_modules/
|
||||||
|
out/
|
||||||
|
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.
|
||||||
42
README.md
Normal file
42
README.md
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
# Godot PRs by File
|
||||||
|
|
||||||
|
This project is provided for Godot Engine contributors to quickly find open
|
||||||
|
PRs editing a specific file or folder. With the amount of work that goes into
|
||||||
|
Godot it becomes tricky to keep in mind every PR that touches every file, and
|
||||||
|
identify conflicts or duplicates. This project aims to help with that.
|
||||||
|
|
||||||
|
Live website: https://godotengine.github.io/godot-prs-by-file/
|
||||||
|
|
||||||
|
## 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}`))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
456
compose-db.js
Normal file
456
compose-db.js
Normal file
@@ -0,0 +1,456 @@
|
|||||||
|
const fs = require('fs').promises;
|
||||||
|
const fsConstants = require('fs').constants;
|
||||||
|
const path = require('path');
|
||||||
|
const fetch = require('node-fetch');
|
||||||
|
|
||||||
|
const PULLS_PER_PAGE = 100;
|
||||||
|
let page_count = 1;
|
||||||
|
let last_cursor = "";
|
||||||
|
|
||||||
|
const ExitCodes = {
|
||||||
|
"RequestFailure": 1,
|
||||||
|
"ParseFailure": 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
const API_REST_PATH = `https://api.github.com/repos/godotengine/godot`;
|
||||||
|
const API_REPOSITORY_ID = `owner:"godotengine" name:"godot"`;
|
||||||
|
const API_RATE_LIMIT = `
|
||||||
|
rateLimit {
|
||||||
|
limit
|
||||||
|
cost
|
||||||
|
remaining
|
||||||
|
resetAt
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
class DataFetcher {
|
||||||
|
async _logResponse(data, name) {
|
||||||
|
try {
|
||||||
|
try {
|
||||||
|
await fs.access("logs", fsConstants.R_OK | fsConstants.W_OK);
|
||||||
|
} catch (err) {
|
||||||
|
await fs.mkdir("logs");
|
||||||
|
}
|
||||||
|
|
||||||
|
await fs.writeFile(`logs/${name}.json`, JSON.stringify(data, null, 4), {encoding: "utf-8"});
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error saving log file: " + err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_handleResponseErrors(res) {
|
||||||
|
console.warn(` Failed to get pull requests for '${API_REPOSITORY_ID}'; 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 fetchGithub(query) {
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
|
||||||
|
return await fetch("https://api.github.com/graphql", init);
|
||||||
|
}
|
||||||
|
|
||||||
|
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(`${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(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}] 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 fetchPulls(page) {
|
||||||
|
try {
|
||||||
|
let after_cursor = "";
|
||||||
|
let after_text = "initial";
|
||||||
|
if (last_cursor !== "") {
|
||||||
|
after_cursor = `after: "${last_cursor}"`;
|
||||||
|
after_text = after_cursor;
|
||||||
|
}
|
||||||
|
|
||||||
|
const query = `
|
||||||
|
query {
|
||||||
|
${API_RATE_LIMIT}
|
||||||
|
repository(${API_REPOSITORY_ID}) {
|
||||||
|
pullRequests(first:${PULLS_PER_PAGE} ${after_cursor} states: OPEN) {
|
||||||
|
totalCount
|
||||||
|
pageInfo {
|
||||||
|
endCursor
|
||||||
|
hasNextPage
|
||||||
|
}
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
number
|
||||||
|
url
|
||||||
|
title
|
||||||
|
state
|
||||||
|
isDraft
|
||||||
|
|
||||||
|
createdAt
|
||||||
|
updatedAt
|
||||||
|
|
||||||
|
baseRef {
|
||||||
|
name
|
||||||
|
}
|
||||||
|
|
||||||
|
author {
|
||||||
|
login
|
||||||
|
avatarUrl
|
||||||
|
url
|
||||||
|
|
||||||
|
... on User {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
milestone {
|
||||||
|
id
|
||||||
|
title
|
||||||
|
url
|
||||||
|
}
|
||||||
|
|
||||||
|
labels (first: 100) {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
color
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
files (first: 100) {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
path
|
||||||
|
changeType
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
let page_text = page;
|
||||||
|
if (page_count > 1) {
|
||||||
|
page_text = `${page}/${page_count}`;
|
||||||
|
}
|
||||||
|
console.log(` Requesting page ${page_text} of pull request data (${after_text}).`);
|
||||||
|
|
||||||
|
const res = await this.fetchGithub(query);
|
||||||
|
if (res.status !== 200) {
|
||||||
|
this._handleResponseErrors(res);
|
||||||
|
process.exitCode = ExitCodes.RequestFailure;
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await res.json();
|
||||||
|
await this._logResponse(data, `data_page_${page}`);
|
||||||
|
this._handleDataErrors(data);
|
||||||
|
|
||||||
|
const rate_limit = data.data["rateLimit"];
|
||||||
|
const repository = data.data["repository"];
|
||||||
|
const pulls_data = mapNodes(repository.pullRequests);
|
||||||
|
|
||||||
|
console.log(` [$${rate_limit.cost}] Retrieved ${pulls_data.length} pull requests; processing...`);
|
||||||
|
|
||||||
|
last_cursor = repository.pullRequests.pageInfo.endCursor;
|
||||||
|
page_count = Math.ceil(repository.pullRequests.totalCount / PULLS_PER_PAGE);
|
||||||
|
|
||||||
|
return pulls_data;
|
||||||
|
} catch (err) {
|
||||||
|
console.error(" Error fetching pull request data: " + err);
|
||||||
|
process.exitCode = ExitCodes.RequestFailure;
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchFiles(branch) {
|
||||||
|
try {
|
||||||
|
const query = `/git/trees/${branch}?recursive=1`;
|
||||||
|
|
||||||
|
const res = await this.fetchGithubRest(query);
|
||||||
|
if (res.status !== 200) {
|
||||||
|
this._handleResponseErrors(res);
|
||||||
|
process.exitCode = ExitCodes.RequestFailure;
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await res.json();
|
||||||
|
await this._logResponse(data, `data_files_${branch}`);
|
||||||
|
this._handleDataErrors(data);
|
||||||
|
|
||||||
|
const files_data = data.tree;
|
||||||
|
|
||||||
|
console.log(` [$0] Retrieved ${files_data.length} file system entries in '${branch}'; processing...`);
|
||||||
|
|
||||||
|
return files_data;
|
||||||
|
} catch (err) {
|
||||||
|
console.error(" Error fetching pull request data: " + err);
|
||||||
|
process.exitCode = ExitCodes.RequestFailure;
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DataProcessor {
|
||||||
|
constructor() {
|
||||||
|
this.authors = {};
|
||||||
|
this.pulls = [];
|
||||||
|
this.files = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
_explainFileType(type) {
|
||||||
|
switch(type) {
|
||||||
|
case "blob":
|
||||||
|
return "file";
|
||||||
|
case "tree":
|
||||||
|
return "folder";
|
||||||
|
default:
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
processPulls(pullsRaw) {
|
||||||
|
try {
|
||||||
|
pullsRaw.forEach((item) => {
|
||||||
|
// Compile basic information about a PR.
|
||||||
|
let pr = {
|
||||||
|
"id": item.id,
|
||||||
|
"public_id": item.number,
|
||||||
|
"url": item.url,
|
||||||
|
"diff_url": `${item.url}.diff`,
|
||||||
|
"patch_url": `${item.url}.patch`,
|
||||||
|
|
||||||
|
"title": item.title,
|
||||||
|
"state": item.state,
|
||||||
|
"is_draft": item.isDraft,
|
||||||
|
"authored_by": null,
|
||||||
|
"created_at": item.createdAt,
|
||||||
|
"updated_at": item.updatedAt,
|
||||||
|
|
||||||
|
"target_branch": item.baseRef.name,
|
||||||
|
|
||||||
|
"labels": [],
|
||||||
|
"milestone": null,
|
||||||
|
|
||||||
|
"files": [],
|
||||||
|
};
|
||||||
|
|
||||||
|
// Compose and link author information.
|
||||||
|
const author = {
|
||||||
|
"id": "",
|
||||||
|
"user": "ghost",
|
||||||
|
"avatar": "https://avatars.githubusercontent.com/u/10137?v=4",
|
||||||
|
"url": "https://github.com/ghost",
|
||||||
|
"pull_count": 0,
|
||||||
|
};
|
||||||
|
if (item.author != null) {
|
||||||
|
author["id"] = item.author.id;
|
||||||
|
author["user"] = item.author.login;
|
||||||
|
author["avatar"] = item.author.avatarUrl;
|
||||||
|
author["url"] = item.author.url;
|
||||||
|
}
|
||||||
|
pr.authored_by = author.id;
|
||||||
|
|
||||||
|
// Store the author if they haven't been stored.
|
||||||
|
if (typeof this.authors[author.id] === "undefined") {
|
||||||
|
this.authors[author.id] = author;
|
||||||
|
}
|
||||||
|
this.authors[author.id].pull_count++;
|
||||||
|
|
||||||
|
// Add the milestone, if available.
|
||||||
|
if (item.milestone) {
|
||||||
|
pr.milestone = {
|
||||||
|
"id": item.milestone.id,
|
||||||
|
"title": item.milestone.title,
|
||||||
|
"url": item.milestone.url,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add labels, if available.
|
||||||
|
let labels = mapNodes(item.labels);
|
||||||
|
labels.forEach((labelItem) => {
|
||||||
|
pr.labels.push({
|
||||||
|
"id": labelItem.id,
|
||||||
|
"name": labelItem.name,
|
||||||
|
"color": "#" + labelItem.color,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
pr.labels.sort((a, b) => {
|
||||||
|
if (a.name > b.name) return 1;
|
||||||
|
if (a.name < b.name) return -1;
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add changed files.
|
||||||
|
let files = mapNodes(item.files);
|
||||||
|
files.forEach((fileItem) => {
|
||||||
|
pr.files.push({
|
||||||
|
"path": fileItem.path,
|
||||||
|
"changeType": fileItem.changeType,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
pr.files.sort((a, b) => {
|
||||||
|
if (a.name > b.name) return 1;
|
||||||
|
if (a.name < b.name) return -1;
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.pulls.push(pr);
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(" Error parsing pull request data: " + err);
|
||||||
|
process.exitCode = ExitCodes.ParseFailure;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
processFiles(filesRaw) {
|
||||||
|
try {
|
||||||
|
filesRaw.forEach((item) => {
|
||||||
|
let file = {
|
||||||
|
"type": this._explainFileType(item.type),
|
||||||
|
"name": item.path.split("/").pop(),
|
||||||
|
"path": item.path,
|
||||||
|
"parent": "",
|
||||||
|
};
|
||||||
|
|
||||||
|
let parentPath = item.path.split("/");
|
||||||
|
parentPath.pop();
|
||||||
|
if (parentPath.length > 0) {
|
||||||
|
file.parent = parentPath.join("/");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.files.push(file);
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(" Error parsing repository file system: " + err);
|
||||||
|
process.exitCode = ExitCodes.ParseFailure;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapNodes(object) {
|
||||||
|
return object.edges.map((item) => item["node"])
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
// Internal utility methods.
|
||||||
|
const checkForExit = () => {
|
||||||
|
if (process.exitCode > 0) {
|
||||||
|
process.exit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const delay = async (msec) => {
|
||||||
|
return new Promise(resolve => setTimeout(resolve, msec));
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("[*] Building local pull request database.");
|
||||||
|
const dataFetcher = new DataFetcher();
|
||||||
|
const dataProcessor = new DataProcessor();
|
||||||
|
|
||||||
|
console.log("[*] Checking the rate limits before.");
|
||||||
|
await dataFetcher.checkRates();
|
||||||
|
checkForExit();
|
||||||
|
|
||||||
|
// console.log("[*] Fetching pull request data from GitHub.");
|
||||||
|
// // Pages are starting with 1 for better presentation.
|
||||||
|
// let page = 1;
|
||||||
|
// while (page <= page_count) {
|
||||||
|
// const pullsRaw = await dataFetcher.fetchPulls(page);
|
||||||
|
// dataProcessor.processPulls(pullsRaw);
|
||||||
|
// checkForExit();
|
||||||
|
// page++;
|
||||||
|
|
||||||
|
// // 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 delay(1500);
|
||||||
|
// }
|
||||||
|
|
||||||
|
console.log("[*] Fetching repository file system from GitHub.");
|
||||||
|
const filesRaw = await dataFetcher.fetchFiles("master");
|
||||||
|
dataProcessor.processFiles(filesRaw);
|
||||||
|
checkForExit();
|
||||||
|
|
||||||
|
console.log("[*] Checking the rate limits after.")
|
||||||
|
await dataFetcher.checkRates();
|
||||||
|
checkForExit();
|
||||||
|
|
||||||
|
console.log("[*] Finalizing database.")
|
||||||
|
const output = {
|
||||||
|
"generated_at": Date.now(),
|
||||||
|
"authors": dataProcessor.authors,
|
||||||
|
"pulls": dataProcessor.pulls,
|
||||||
|
"files": dataProcessor.files,
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
console.log("[*] Storing database to file.")
|
||||||
|
await fs.writeFile("out/data.json", JSON.stringify(output), {encoding: "utf-8"});
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error saving database file: " + err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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-prs-by-file",
|
||||||
|
"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();
|
||||||
83
src/paths/index/components/IndexDescription.js
Normal file
83
src/paths/index/components/IndexDescription.js
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
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 lists all open pull-requests (PRs) associated with the selected file
|
||||||
|
or folder. The goal here is to help contributors and maintainers identify possible
|
||||||
|
conflicts and duplication.
|
||||||
|
</div>
|
||||||
|
<div class="header-description-column header-extra-links">
|
||||||
|
See also:
|
||||||
|
<br />
|
||||||
|
<a href="https://godotengine.github.io/godot-team-reports/" target="_blank">Godot Team Reports</a>
|
||||||
|
</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 PRs by File
|
||||||
|
</h1>
|
||||||
|
<div class="header-metadata">
|
||||||
|
${(this.generated_at ? html`
|
||||||
|
<span title="${generatedAt}">
|
||||||
|
data generated ${generatedRel}
|
||||||
|
</span>
|
||||||
|
` : '')}
|
||||||
|
<br/>
|
||||||
|
<a
|
||||||
|
href="https://github.com/godotengine/godot-prs-by-file"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
contribute on GitHub
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
121
src/paths/index/components/files/FileItem.js
Normal file
121
src/paths/index/components/files/FileItem.js
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
import { LitElement, html, css, customElement, property } from 'lit-element';
|
||||||
|
|
||||||
|
@customElement('gr-file-item')
|
||||||
|
export default class FileItem 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: #2c3c55;
|
||||||
|
--tab-active-border-color: #397adf;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Component styling **/
|
||||||
|
:host {
|
||||||
|
max-width: 240px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host .file-item {
|
||||||
|
border-left: 5px solid transparent;
|
||||||
|
color: var(--g-font-color);
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 3px 12px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
:host .file-item:hover {
|
||||||
|
background-color: var(--tab-hover-background-color);
|
||||||
|
}
|
||||||
|
:host .file-item--active {
|
||||||
|
background-color: var(--tab-active-background-color);
|
||||||
|
border-left: 5px solid var(--tab-active-border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
:host .file-icon {
|
||||||
|
background-size: cover;
|
||||||
|
border-radius: 2px;
|
||||||
|
display: inline-block;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
min-width: 16px;
|
||||||
|
}
|
||||||
|
:host .file-icon--folder {
|
||||||
|
background-image: url('/folder.svg');
|
||||||
|
}
|
||||||
|
:host .file-icon--file {
|
||||||
|
background-image: url('/file.svg');
|
||||||
|
filter: brightness(0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
:host .file-title {
|
||||||
|
font-size: 13px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host .file-pull-count {
|
||||||
|
color: var(--dimmed-font-color);
|
||||||
|
flex-grow: 1;
|
||||||
|
font-size: 13px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
:host .file-pull-count--hot {
|
||||||
|
color: var(--g-font-color);
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 900px) {
|
||||||
|
:host .file-item {
|
||||||
|
padding: 6px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host .file-title,
|
||||||
|
:host .file-pull-count {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
@property({ type: String }) path = "";
|
||||||
|
@property({ type: String, reflect: true }) name = "";
|
||||||
|
@property({ type: String, reflect: true }) type = "";
|
||||||
|
@property({ type: Boolean, reflect: true }) active = false;
|
||||||
|
@property({ type: Number }) pull_count = 0;
|
||||||
|
|
||||||
|
render(){
|
||||||
|
const classList = [ "file-item" ];
|
||||||
|
if (this.active) {
|
||||||
|
classList.push("file-item--active");
|
||||||
|
}
|
||||||
|
|
||||||
|
const iconClassList = [ "file-icon", "file-icon--" + this.type ];
|
||||||
|
|
||||||
|
const countClassList = [ "file-pull-count" ];
|
||||||
|
if (this.pull_count > 50) {
|
||||||
|
countClassList.push("file-pull-count--hot");
|
||||||
|
}
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<div class="${classList.join(" ")}">
|
||||||
|
<div class="${iconClassList.join(" ")}"></div>
|
||||||
|
<span class="file-title">
|
||||||
|
${this.path}
|
||||||
|
</span>
|
||||||
|
<span class="${countClassList.join(" ")}">
|
||||||
|
${this.pull_count}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
76
src/paths/index/components/files/FileList.js
Normal file
76
src/paths/index/components/files/FileList.js
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import { LitElement, html, css, customElement, property } from 'lit-element';
|
||||||
|
|
||||||
|
import FileItem from "./FileItem";
|
||||||
|
|
||||||
|
@customElement('gr-file-list')
|
||||||
|
export default class FileList extends LitElement {
|
||||||
|
static get styles() {
|
||||||
|
return css`
|
||||||
|
/** Colors and variables **/
|
||||||
|
:host {
|
||||||
|
--files-background-color: #fcfcfa;
|
||||||
|
--files-border-color: #515c6c;
|
||||||
|
}
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:host {
|
||||||
|
--files-background-color: #0d1117;
|
||||||
|
--files-border-color: #515c6c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Component styling **/
|
||||||
|
:host {
|
||||||
|
}
|
||||||
|
|
||||||
|
:host .file-list {
|
||||||
|
background-color: var(--files-background-color);
|
||||||
|
border-right: 2px solid var(--files-border-color);
|
||||||
|
width: 320px;
|
||||||
|
min-height: 216px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 900px) {
|
||||||
|
:host {
|
||||||
|
width: 100%
|
||||||
|
}
|
||||||
|
|
||||||
|
:host .file-list {
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
@property({ type: Object }) files = {};
|
||||||
|
@property({ type: String }) selected = "";
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const topLevel = this.files[""] || [];
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<div class="file-list">
|
||||||
|
<div class="file-list-section">
|
||||||
|
${(topLevel.length > 0) ?
|
||||||
|
topLevel.map((item) => {
|
||||||
|
return html`
|
||||||
|
<gr-file-item
|
||||||
|
.path="${item.path}"
|
||||||
|
.name="${item.name}"
|
||||||
|
.type="${item.type}"
|
||||||
|
.pull_count="${item.pull_count}"
|
||||||
|
?active="${false}"
|
||||||
|
/>
|
||||||
|
`;
|
||||||
|
}) : html`
|
||||||
|
<span>There are no files</span>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
110
src/paths/index/entry.js
Normal file
110
src/paths/index/entry.js
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
import { LitElement, html, css, customElement, property } from 'lit-element';
|
||||||
|
|
||||||
|
import PageContent from 'src/shared/components/PageContent';
|
||||||
|
import IndexHeader from "./components/IndexHeader";
|
||||||
|
import IndexDescription from "./components/IndexDescription";
|
||||||
|
|
||||||
|
import FileList from "./components/files/FileList";
|
||||||
|
|
||||||
|
@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 {
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this._entryRequested = false;
|
||||||
|
this._isLoading = true;
|
||||||
|
this._generatedAt = null;
|
||||||
|
|
||||||
|
this._files = {};
|
||||||
|
|
||||||
|
this._requestData();
|
||||||
|
}
|
||||||
|
|
||||||
|
performUpdate() {
|
||||||
|
this._requestData();
|
||||||
|
super.performUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
async _requestData() {
|
||||||
|
if (this._entryRequested) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._entryRequested = true;
|
||||||
|
this._isLoading = true;
|
||||||
|
const data = await greports.api.getData();
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
this._generatedAt = data.generated_at;
|
||||||
|
|
||||||
|
data.files.forEach((file) => {
|
||||||
|
if (file.type === "file" || file.type === "folder") {
|
||||||
|
if (typeof this._files[file.parent] === "undefined") {
|
||||||
|
this._files[file.parent] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
this._files[file.parent].push(file);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
for (let folderName in this._files) {
|
||||||
|
this._files[folderName].sort((a, b) => {
|
||||||
|
if (a.type === "folder" && b.type !== "folder") {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (b.type === "folder" && a.type !== "folder") {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const a_name = a.path.toLowerCase();
|
||||||
|
const b_name = b.path.toLowerCase();
|
||||||
|
|
||||||
|
if (a_name > b_name) return 1;
|
||||||
|
if (a_name < b_name) return -1;
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this._generatedAt = null;
|
||||||
|
|
||||||
|
this._files = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
this._isLoading = false;
|
||||||
|
this.requestUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
render(){
|
||||||
|
return html`
|
||||||
|
<page-content>
|
||||||
|
<gr-index-entry .generated_at="${this._generatedAt}"></gr-index-entry>
|
||||||
|
<gr-index-description></gr-index-description>
|
||||||
|
|
||||||
|
${(this._isLoading ? html`
|
||||||
|
<h3>Loading...</h3>
|
||||||
|
` : html`
|
||||||
|
<div class="files">
|
||||||
|
<gr-file-list
|
||||||
|
.files="${this._files}"
|
||||||
|
></gr-file-list>
|
||||||
|
</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 PRs by File</title>
|
||||||
|
<meta name="description" content="Godot Engine PRs grouped by individual source files they affect">
|
||||||
|
<meta name="keywords" content="godot, godot engine, gamedev, 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>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
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>
|
||||||
124
src/shared/scripts/global.js
Normal file
124
src/shared/scripts/global.js
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
const LOCAL_PREFERENCE_PREFIX = "_godot_prbf"
|
||||||
|
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 getData() {
|
||||||
|
return await this.get("data.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");
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
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);
|
||||||
|
},
|
||||||
|
|
||||||
|
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;
|
||||||
53
src/shared/styles/global.css
Normal file
53
src/shared/styles/global.css
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
/** Colors and variables **/
|
||||||
|
:root {
|
||||||
|
--g-background-color: #fcfcfa;
|
||||||
|
--g-background-extra-color: #98a5b8;
|
||||||
|
--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-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/file.svg
Normal file
1
src/static/icons/file.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg height="16" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg"><path d="m2 1v14h12v-9h-5v-5zm8 0v4h4z" fill="#e0e0e0" transform="translate(0 -.000017)"/></svg>
|
||||||
|
After Width: | Height: | Size: 180 B |
1
src/static/icons/folder.svg
Normal file
1
src/static/icons/folder.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg height="16" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg"><path d="m2 2a1 1 0 0 0 -1 1v2 6 2a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-7a1 1 0 0 0 -1-1h-4a1 1 0 0 1 -1-1v-1a1 1 0 0 0 -1-1z" fill="#e0e0e0"/></svg>
|
||||||
|
After Width: | Height: | Size: 228 B |
Reference in New Issue
Block a user