xref: /aosp_15_r20/external/pigweed/pw_console/py/pw_console/html/main.js (revision 61c4878ac05f98d0ceed94b57d316916de578985)
1// Copyright 2022 The Pigweed Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License"); you may not
4// use this file except in compliance with the License. You may obtain a copy of
5// the License at
6//
7//     https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12// License for the specific language governing permissions and limitations under
13// the License.
14
15// eslint-disable-next-line no-undef
16const { createLogViewer, LogSource, LogEntry, Severity } = PigweedLogging;
17
18let currentTheme = {};
19let defaultLogStyleRule = 'color: #ffffff;';
20let columnStyleRules = {};
21let defaultColumnStyles = [];
22let logLevelStyles = {};
23
24const logLevelToString = {
25  10: 'DBG',
26  20: 'INF',
27  21: 'OUT',
28  30: 'WRN',
29  40: 'ERR',
30  50: 'CRT',
31  70: 'FTL',
32};
33
34const logLevelToSeverity = {
35  10: Severity.DEBUG,
36  20: Severity.INFO,
37  21: Severity.INFO,
38  30: Severity.WARNING,
39  40: Severity.ERROR,
40  50: Severity.CRITICAL,
41  70: Severity.CRITICAL,
42};
43
44let nonAdditionalDataFields = [
45  '_hosttime',
46  'levelname',
47  'levelno',
48  'args',
49  'fields',
50  'message',
51  'time',
52];
53let additionalHeaders = [];
54
55// New LogSource to consume pw-console log json messages
56class PwConsoleLogSource extends LogSource {
57  constructor() {
58    super();
59  }
60  append_log(data) {
61    var fields = [
62      { key: 'severity', value: logLevelToSeverity[data.levelno] },
63      { key: 'time', value: data.time },
64    ];
65    Object.keys(data.fields).forEach((columnName) => {
66      if (
67        nonAdditionalDataFields.indexOf(columnName) === -1 &&
68        additionalHeaders.indexOf(columnName) === -1
69      ) {
70        fields.push({ key: columnName, value: data.fields[columnName] });
71      }
72    });
73    fields.push({ key: 'message', value: data.message });
74    fields.push({ key: 'py_file', value: data.py_file });
75    fields.push({ key: 'py_logger', value: data.py_logger });
76    this.publishLogEntry({
77      severity: logLevelToSeverity[data.levelno],
78      timestamp: new Date(),
79      fields: fields,
80    });
81  }
82}
83
84// Setup the pigweedjs log-viewer
85const logSource = new PwConsoleLogSource();
86const containerEl = document.querySelector('#log-viewer-container');
87let unsubscribe = createLogViewer(logSource, containerEl);
88
89// Format a date in the standard pw_cli style YYYY-mm-dd HH:MM:SS
90function formatDate(dt) {
91  function pad2(n) {
92    return (n < 10 ? '0' : '') + n;
93  }
94
95  return (
96    dt.getFullYear() +
97    pad2(dt.getMonth() + 1) +
98    pad2(dt.getDate()) +
99    ' ' +
100    pad2(dt.getHours()) +
101    ':' +
102    pad2(dt.getMinutes()) +
103    ':' +
104    pad2(dt.getSeconds())
105  );
106}
107
108// Return the value for the given # parameter name.
109function getUrlHashParameter(param) {
110  var params = getUrlHashParameters();
111  return params[param];
112}
113
114// Capture all # parameters from the current URL.
115function getUrlHashParameters() {
116  var sPageURL = window.location.hash;
117  if (sPageURL) sPageURL = sPageURL.split('#')[1];
118  var pairs = sPageURL.split('&');
119  var object = {};
120  pairs.forEach(function (pair, i) {
121    pair = pair.split('=');
122    if (pair[0] !== '') object[pair[0]] = pair[1];
123  });
124  return object;
125}
126
127// Update web page CSS styles based on a pw-console color json log message.
128function setCurrentTheme(newTheme) {
129  currentTheme = newTheme;
130  defaultLogStyleRule = parsePromptToolkitStyle(newTheme.default);
131  // Set body background color
132  // document.querySelector('body').setAttribute('style', defaultLogStyleRule);
133
134  // Apply default font styles to columns
135  let styles = [];
136  Object.keys(newTheme).forEach((key) => {
137    if (key.startsWith('log-table-column-')) {
138      styles.push(newTheme[key]);
139    }
140    if (key.startsWith('log-level-')) {
141      logLevelStyles[parseInt(key.replace('log-level-', ''))] =
142        parsePromptToolkitStyle(newTheme[key]);
143    }
144  });
145  defaultColumnStyles = styles;
146}
147
148// Convert prompt_toolkit color format strings to CSS.
149// 'bg:#BG-HEX #FG-HEX STYLE' where STYLE is either 'bold' or 'underline'
150function parsePromptToolkitStyle(rule) {
151  const ruleList = rule.split(' ');
152  let outputStyle = ruleList.map((fragment) => {
153    if (fragment.startsWith('bg:')) {
154      return `background-color: ${fragment.replace('bg:', '')}`;
155    } else if (fragment === 'bold') {
156      return `font-weight: bold`;
157    } else if (fragment === 'underline') {
158      return `text-decoration: underline`;
159    } else if (fragment.startsWith('#')) {
160      return `color: ${fragment}`;
161    }
162  });
163  return outputStyle.join(';');
164}
165
166// Inject styled spans into the log message column values.
167function applyStyling(data, applyColors = false) {
168  let colIndex = 0;
169  Object.keys(data).forEach((key) => {
170    if (columnStyleRules[key] && typeof data[key] === 'string') {
171      Object.keys(columnStyleRules[key]).forEach((token) => {
172        data[key] = data[key].replaceAll(
173          token,
174          `<span
175              style="${defaultLogStyleRule};${
176                applyColors
177                  ? defaultColumnStyles[colIndex % defaultColumnStyles.length]
178                  : ''
179              };${parsePromptToolkitStyle(columnStyleRules[key][token])};">
180                ${token}
181            </span>`,
182        );
183      });
184    } else if (key === 'fields') {
185      data[key] = applyStyling(data.fields, true);
186    }
187    if (applyColors) {
188      data[key] = `<span
189      style="${parsePromptToolkitStyle(
190        defaultColumnStyles[colIndex % defaultColumnStyles.length],
191      )}">
192        ${data[key]}
193      </span>`;
194    }
195    colIndex++;
196  });
197  return data;
198}
199
200// Connect to the pw-console websocket and start emitting logs.
201(function () {
202  const container = document.querySelector('.log-container');
203  const height = window.innerHeight - 50;
204  let follow = true;
205
206  const port = getUrlHashParameter('ws');
207  const hostname = location.hostname || '127.0.0.1';
208  var ws = new WebSocket(`ws://${hostname}:${port}/`);
209  ws.onmessage = function (event) {
210    let dataObj;
211    try {
212      dataObj = JSON.parse(event.data);
213    } catch (e) {
214      // empty
215    }
216    if (!dataObj) return;
217
218    if (dataObj.__pw_console_colors) {
219      // If this is a color theme message, update themes.
220      const colors = dataObj.__pw_console_colors;
221      setCurrentTheme(colors.classes);
222      if (colors.column_values) {
223        columnStyleRules = { ...colors.column_values };
224      }
225    } else {
226      // Normal log message.
227      const currentData = { ...dataObj, time: formatDate(new Date()) };
228      logSource.append_log(currentData);
229    }
230  };
231})();
232