diff options
Diffstat (limited to 'ofborg/ofborg-viewer/src/app.js')
| -rw-r--r-- | ofborg/ofborg-viewer/src/app.js | 201 |
1 files changed, 201 insertions, 0 deletions
diff --git a/ofborg/ofborg-viewer/src/app.js b/ofborg/ofborg-viewer/src/app.js new file mode 100644 index 0000000000..cdfc3b4e0d --- /dev/null +++ b/ofborg/ofborg-viewer/src/app.js @@ -0,0 +1,201 @@ +import ready from "./lib/ready"; +import Listener from "./listener"; +import Gui from "./gui"; +import Backlog from "./backlog"; +import State from "./state"; +import {WELL_KNOWN} from "./config"; + +const MAN = ` +tickborg-viewer(web) tickborg web interface tickborg-viewer(web) + +NAME + tickborg-viewer — Build logs web interface + +DESCRIPTION + tickborg-viewer is a web interface that aggregates the currently in-progress + build logs made by tickborg. + + New logs for the given build will be added to the discovered logs at the + top of the interface. Clicking them or activating them through the keyboard + shortcuts of your browser will show them. + + The log will autoscroll when the logger interface is scrolled at the + bottom. Scrolling up will stop the autoscroll until scrolled back down. + +`; + +/** + * The logger app. + */ +class App { + constructor() { + // Only "boot" the app when the DOM is ready. + ready(() => this.boot()); + + // To use as event listener targets. + this.handle_select = this.handle_select.bind(this); + } + + /** + * Hooks and starts the app. + * + * This means: + * * Starts the GUI. + * * Reads parameters. + * * Starts the Listener. + */ + boot() { + window.document.title = "Log viewer starting..."; + this.gui = new Gui(); + + this.gui.addEventListener("select", this.handle_select); + + this.log("$ tickborg-viewer --version", null, {tag: "tickborg"}); + this.log(`tickborg-viewer, version ${VERSION}${GIT_REVISION && ` (${GIT_REVISION})`}`, null, {tag: "tickborg"}); + this.log("$ man tickborg-viewer", null, {tag: "tickborg"}); + this.log(MAN, null, {tag: "man"}); + + this.log("→ logger starting", null, {tag: "tickborg"}); + window.document.title = "Log viewer started..."; + + this.state = new State(); + this.state.on_state_change = (s) => this.handle_state_change(s); + this.handle_state_change(this.state.params); + } + + handle_select(selected) { + this.state.set_state({attempt_id: selected["name"]}); + } + + handle_state_change(new_state) { + const {attempt_id, key} = new_state; + const {logs} = this.gui; + + // Loading parameters + if (!key) { + this.log("!! No key parameter... stopping now.", "tickborg"); + return; + } + + // This will allow some app parts to log more information. + if (new_state["debug"]) { + window.DEBUG = true; + } + + window.document.title = `Log viewer [${key}]`; + + // This is set only once in the lifetime of the app and is expected + // never to change. + // FIXME : Allow key to change live. + if (!this.key) { + this.key = key; + + // Pings the logger API for existing logs. + // Those logs can be either live or complete. + this.load_logs(() => { + // Selects the log if loaded async. + if (logs[attempt_id]) { + logs[attempt_id].select(); + } + }); + } + + // Attempts to select the log. + if (logs[attempt_id]) { + logs[attempt_id].select(); + } + + if (!this.listener) { + // Starts the listener. + this.listener = new Listener({ + key: new_state["key"], + logger: (msg, tag) => this.log(msg, null, {tag}), + fn: (...msg) => this.from_listener(...msg), + }); + } + } + + load_logs(callback) { + this.log(`→ fetching existing attempts for ${this.key}`, null, {tag: "tickborg"}); + return fetch(`${WELL_KNOWN}/${this.key}`, {mode: "cors"}) + .then((response) => response.json()) + .then(({attempts}) => Object.keys(attempts).forEach((attempt_id) => { + this.log(`→ fetching log for ${attempt_id}`, null, {tag: "tickborg"}); + const attempt = attempts[attempt_id]; + const log = this.gui.addLog(attempt_id, attempt["metadata"]); + const {log_url} = attempt; + // Loads backlog only when needed. + const handler = () => { + log.backlog_loading(); + fetch(log_url, {mode: "cors"}) + .then((response) => response.text()) + .then((txt) => { + const lines = txt.split("\n"); + log.backlog(lines, log_url); + this.log(`→ added log for ${attempt_id}`, null, {tag: "tickborg"}); + }) + ; + // Removes self from events. + log.removeEventListener("select", handler); + }; + log.addEventListener("select", handler); + })) + .then(() => callback()) + ; + } + + from_listener(message, routing_key) { + const {output, attempt_id, line_number} = message; + + // Probably a build-start message. + if (!output && output !== "") { + this.gui.addLog(attempt_id, message); + return; + } + + // Opening a new log? + // It should already have been created, but just in case. + if (Object.keys(this.gui.logs).indexOf(attempt_id) === -1) { + const log = this.gui.addLog(attempt_id); + + // Assumes if there was no log open for attempt, it needs to fetch backlog. + if (line_number > 1) { + // FIXME : Loop backlog fetching until all lines are found up to line_number. + log.backlog_loading(); + const log_url = Backlog.get_url(routing_key, attempt_id); + + return fetch(log_url, {mode: "cors"}) + .then((response) => response.text()) + .then((txt) => { + const lines = txt.split("\n").slice(0, line_number - 1); + log.backlog(lines, log_url); + }) + .catch((err) => { + log.backlog_error(err); + }) + ; + } + } + + return this.log(output, attempt_id, { + tag: "stdout", + title: `#${line_number}`, + }); + } + + /** + * Logs to the console. + * + * This can receive a class for some more styling. + */ + log(msg, log, {tag, title} = {}) { + this.gui.log({ + msg, + log, + tag, + title, + }); + } +} + +export default App; |
