1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
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;
|