| // Copyright 2022 The Pigweed Authors |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); you may not |
| // use this file except in compliance with the License. You may obtain a copy of |
| // the License at |
| // |
| // https://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| // License for the specific language governing permissions and limitations under |
| // the License. |
| |
| import {useEffect, useRef, useState} from "react"; |
| import {pw_tokenizer, Device} from "pigweedjs"; |
| import {AutoSizer, Table, Column} from 'react-virtualized'; |
| import {listenToDefaultLogService} from "../common/logService"; |
| import 'react-virtualized/styles.css'; |
| import styles from "../styles/log.module.css"; |
| |
| type Detokenizer = pw_tokenizer.Detokenizer; |
| |
| interface LogProps { |
| device: Device | undefined, |
| tokenDB: string | undefined |
| } |
| |
| interface LogEntry { |
| msg: string, |
| timestamp: number, |
| humanTime: string, |
| module: string, |
| file: string |
| } |
| |
| function parseLogMsg(msg: string): LogEntry { |
| const pairs = msg.split("■").slice(1).map(pair => pair.split("♦")); |
| |
| // Not a valid message, print as-is. |
| if (pairs.length === 0) { |
| return { |
| msg, |
| module: "", |
| file: "", |
| timestamp: Date.now(), |
| humanTime: new Date(Date.now()).toLocaleTimeString("en-US") |
| } |
| } |
| |
| let map: any = {}; |
| pairs.forEach(pair => map[pair[0]] = pair[1]) |
| return { |
| msg: map.msg, |
| module: map.module, |
| file: map.file, |
| timestamp: Date.now(), |
| humanTime: new Date(Date.now()).toLocaleTimeString("en-US") |
| } |
| } |
| |
| const keyToDisplayName: {[key: string]: string} = { |
| "msg": "Message", |
| "humanTime": "Time", |
| "module": "Module", |
| "file": "File" |
| } |
| |
| export default function Log({device, tokenDB}: LogProps) { |
| const [logs, setLogs] = useState<LogEntry[]>([]); |
| const [detokenizer, setDetokenizer] = useState<Detokenizer | null>(null); |
| const logTable = useRef<Table | null>(null); |
| const _headerRenderer = ({dataKey, sortBy, sortDirection}: any) => { |
| return ( |
| <div> |
| {keyToDisplayName[dataKey]} |
| </div> |
| ); |
| } |
| |
| const processFrame = (frame: Uint8Array) => { |
| if (detokenizer) { |
| const detokenized = detokenizer.detokenizeUint8Array(frame); |
| setLogs(oldLogs => [...oldLogs, parseLogMsg(detokenized)]); |
| } |
| else { |
| const decoded = new TextDecoder().decode(frame); |
| setLogs(oldLogs => [...oldLogs, parseLogMsg(decoded)]); |
| } |
| setTimeout(() => { |
| logTable.current!.scrollToRow(logs.length - 1); |
| }, 100); |
| } |
| |
| useEffect(() => { |
| if (device) { |
| let cleanupFn: () => void; |
| listenToDefaultLogService(device, processFrame).then((unsub) => cleanupFn = unsub); |
| return () => { |
| if (cleanupFn) cleanupFn(); |
| } |
| } |
| }, [device, detokenizer]); |
| |
| useEffect(() => { |
| if (tokenDB && tokenDB.length > 0) { |
| const detokenizer = new pw_tokenizer.Detokenizer(tokenDB); |
| setDetokenizer(detokenizer); |
| } |
| }, [tokenDB]) |
| |
| return ( |
| <> |
| {/* @ts-ignore */} |
| <AutoSizer> |
| {({height, width}) => ( |
| <> |
| {/* @ts-ignore */} |
| <Table |
| className={styles.logsContainer} |
| headerHeight={40} |
| height={height} |
| rowCount={logs.length} |
| rowGetter={({index}) => logs[index]} |
| rowHeight={30} |
| ref={logTable} |
| width={width} |
| > |
| {/* @ts-ignore */} |
| <Column |
| dataKey="humanTime" |
| width={190} |
| headerRenderer={_headerRenderer} |
| /> |
| {/* @ts-ignore */} |
| <Column |
| dataKey="msg" |
| flexGrow={1} |
| width={290} |
| headerRenderer={_headerRenderer} |
| /> |
| {/* @ts-ignore */} |
| <Column |
| dataKey="module" |
| width={190} |
| headerRenderer={_headerRenderer} |
| /> |
| {/* @ts-ignore */} |
| <Column |
| dataKey="file" |
| flexGrow={1} |
| width={190} |
| headerRenderer={_headerRenderer} |
| /> |
| </Table> |
| </> |
| )} |
| </AutoSizer> |
| </> |
| ) |
| } |