1// Copyright 2024 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 15import { WebSocketRPCReplKernel, WebSocketRPCClient } from './repl-kernel'; 16import { createLogViewer } from './createLogViewer'; 17import { LogSource } from './log-source'; 18import { Severity } from './shared/interfaces'; 19import { Repl } from './components/repl/repl'; 20 21const logLevelToSeverity = { 22 10: Severity.DEBUG, 23 20: Severity.INFO, 24 21: Severity.INFO, 25 30: Severity.WARNING, 26 40: Severity.ERROR, 27 50: Severity.CRITICAL, 28 70: Severity.CRITICAL, 29}; 30 31const nonAdditionalDataFields = [ 32 '_hosttime', 33 'levelname', 34 'levelno', 35 'args', 36 'fields', 37 'message', 38 'time', 39]; 40 41// Format a date in the standard pw_cli style YYYY-mm-dd HH:MM:SS 42function formatDate(dt: Date) { 43 function pad2(n: number) { 44 return (n < 10 ? '0' : '') + n; 45 } 46 47 return ( 48 dt.getFullYear() + 49 pad2(dt.getMonth() + 1) + 50 pad2(dt.getDate()) + 51 ' ' + 52 pad2(dt.getHours()) + 53 ':' + 54 pad2(dt.getMinutes()) + 55 ':' + 56 pad2(dt.getSeconds()) 57 ); 58} 59 60// New LogSource to consume pw-console log json messages 61class PwConsoleLogSource extends LogSource { 62 // @ts-ignore 63 appendLog(data) { 64 const fields = [ 65 // @ts-ignore 66 { key: 'severity', value: logLevelToSeverity[data.levelno] }, 67 { key: 'time', value: data.time }, 68 ]; 69 Object.keys(data.fields || {}).forEach((columnName) => { 70 if (nonAdditionalDataFields.indexOf(columnName) === -1) { 71 fields.push({ key: columnName, value: data.fields[columnName] }); 72 } 73 }); 74 fields.push({ key: 'message', value: data.message }); 75 fields.push({ key: 'py_file', value: data.py_file }); 76 fields.push({ key: 'py_logger', value: data.py_logger }); 77 this.publishLogEntry({ 78 // @ts-ignore 79 severity: logLevelToSeverity[data.levelno], 80 timestamp: new Date(), 81 fields: fields, 82 }); 83 } 84} 85 86export async function renderPWConsole(containerEl: HTMLElement, wsUrl = '/ws') { 87 const replContainerEl = document.createElement('div'); 88 replContainerEl.id = 'repl-container'; 89 const logsContainerEl = document.createElement('div'); 90 logsContainerEl.id = 'logs-container'; 91 createSplitPanel(replContainerEl, logsContainerEl, containerEl, 40); 92 93 const ws = new WebSocket(wsUrl); 94 const kernel = new WebSocketRPCClient(ws); 95 const allLogSourceNames: string[] = await kernel.call('log_source_list', { 96 filter: '', 97 }); 98 const logSources = allLogSourceNames.map( 99 (name) => new PwConsoleLogSource(name), 100 ); 101 logSources.forEach((source, index) => { 102 kernel.openStream( 103 'log_source_subscribe', 104 { name: allLogSourceNames[index] }, 105 (data) => { 106 if (data.log_line) 107 source.appendLog({ ...data.log_line, time: formatDate(new Date()) }); 108 }, 109 ); 110 }); 111 112 const unsubscribe = createLogViewer(logSources, logsContainerEl); 113 114 const rpc = new WebSocketRPCReplKernel(kernel); 115 const repl = new Repl(rpc, 'Python Shell'); 116 replContainerEl.appendChild(repl); 117 (window as any).rpc = rpc; 118 const resizeObserver = new ResizeObserver((entries) => { 119 for (const entry of entries) { 120 const splitPanel = document.querySelector('sl-split-panel'); 121 if (splitPanel) { 122 // Stack vertically for smaller viewport sizes 123 splitPanel.vertical = entry.contentRect.width < 800; 124 } 125 } 126 }); 127 resizeObserver.observe(document.body); 128 return () => { 129 unsubscribe(); 130 }; 131} 132 133export function createSplitPanel( 134 startEl: HTMLElement, 135 endEl: HTMLElement, 136 containerEl: HTMLElement, 137 initialPosition = 50, 138) { 139 const splitPanel = document.createElement('sl-split-panel'); 140 141 startEl.setAttribute('slot', 'start'); 142 endEl.setAttribute('slot', 'end'); 143 splitPanel.setAttribute('position', `${initialPosition}`); 144 145 splitPanel.appendChild(startEl); 146 splitPanel.appendChild(endEl); 147 containerEl.appendChild(splitPanel); 148} 149