From 46e4ac94f15cf6302e87aad622377f75aa3db47f Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 1 Aug 2026 13:20:46 +0000 Subject: [PATCH] feat(log): add filters to the event log and cap the log store Finding a single error in a stream of RX/TX traffic was not practical with only the origin filters. - add a text filter and an issues only filter - show how many entries are being displayed - add an empty state which distinguishes no activity from no matches - collapse the six origin flags into a single set - cap the log store at 500 entries, it was growing unbounded for the duration of a session - the event log button was labelled Extract, which did not convey that it opens the log in a new window Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01NrhcEgY8mqaxZi8veMPrrx --- apps/client/src/common/stores/logger.ts | 5 +- .../panel/network-panel/NetworkLogExport.tsx | 5 +- apps/client/src/features/log/Log.module.scss | 10 ++ apps/client/src/features/log/Log.tsx | 150 ++++++++---------- 4 files changed, 82 insertions(+), 88 deletions(-) diff --git a/apps/client/src/common/stores/logger.ts b/apps/client/src/common/stores/logger.ts index 13d3b348e..63f5d1c41 100644 --- a/apps/client/src/common/stores/logger.ts +++ b/apps/client/src/common/stores/logger.ts @@ -15,11 +15,14 @@ const logger = createStore(() => ({ logs: [], })); +/** the log is kept in memory, we cap it to avoid it growing through a long show */ +const maxLogEntries = 500; + export const useLogData = () => useStore(logger); export const addLog = (log: Log) => logger.setState((state) => ({ - logs: [log, ...state.logs], + logs: [log, ...state.logs].slice(0, maxLogEntries), })); export const clearLogs = () => logger.setState({ logs: [] }); diff --git a/apps/client/src/features/app-settings/panel/network-panel/NetworkLogExport.tsx b/apps/client/src/features/app-settings/panel/network-panel/NetworkLogExport.tsx index 057e5b119..2f3f288bc 100644 --- a/apps/client/src/features/app-settings/panel/network-panel/NetworkLogExport.tsx +++ b/apps/client/src/features/app-settings/panel/network-panel/NetworkLogExport.tsx @@ -19,10 +19,13 @@ export default function LogExport() { Event log + + Activity in this session. The log is not saved with the project and is cleared when Ontime restarts. + diff --git a/apps/client/src/features/log/Log.module.scss b/apps/client/src/features/log/Log.module.scss index 7e550db34..7a56ff385 100644 --- a/apps/client/src/features/log/Log.module.scss +++ b/apps/client/src/features/log/Log.module.scss @@ -51,3 +51,13 @@ $info-hover: $section-white; flex-wrap: wrap; padding-bottom: 0.5rem; } + +.count { + color: $info-gray; + white-space: nowrap; +} + +.empty { + color: $info-gray; + padding: 0.5rem 0; +} diff --git a/apps/client/src/features/log/Log.tsx b/apps/client/src/features/log/Log.tsx index ae158eb47..e32997e49 100644 --- a/apps/client/src/features/log/Log.tsx +++ b/apps/client/src/features/log/Log.tsx @@ -1,115 +1,90 @@ -import { LogOrigin } from 'ontime-types'; +import { LogLevel, LogOrigin } from 'ontime-types'; import { useCallback, useState } from 'react'; import { IoClose } from 'react-icons/io5'; import Button from '../../common/components/buttons/Button'; +import Input from '../../common/components/input/input/Input'; import { clearLogs, useLogData } from '../../common/stores/logger'; import * as Panel from '../app-settings/panel-utils/PanelUtils'; import style from './Log.module.scss'; +const allOrigins = [ + LogOrigin.User, + LogOrigin.Client, + LogOrigin.Server, + LogOrigin.Playback, + LogOrigin.Rx, + LogOrigin.Tx, +]; + export default function Log() { const { logs: logData } = useLogData(); - const [showClient, setShowClient] = useState(true); - const [showServer, setShowServer] = useState(true); - const [showRx, setShowRx] = useState(true); - const [showTx, setShowTx] = useState(true); - const [showPlayback, setShowPlayback] = useState(true); - const [showUser, setShowUser] = useState(true); + // log entries are not guaranteed to have an origin from the enum + const [origins, setOrigins] = useState>(() => new Set(allOrigins)); + const [onlyProblems, setOnlyProblems] = useState(false); + const [search, setSearch] = useState(''); - const matchers: LogOrigin[] = []; - if (showUser) { - matchers.push(LogOrigin.User); - } - if (showClient) { - matchers.push(LogOrigin.Client); - } - if (showServer) { - matchers.push(LogOrigin.Server); - } - if (showRx) { - matchers.push(LogOrigin.Rx); - } - if (showTx) { - matchers.push(LogOrigin.Tx); - } - if (showPlayback) { - matchers.push(LogOrigin.Playback); - } - - const filteredData = logData.filter((entry) => matchers.some((match) => entry.origin === match)); - - const disableOthers = useCallback((toEnable: LogOrigin) => { - setShowUser(toEnable === LogOrigin.User); - setShowClient(toEnable === LogOrigin.Client); - setShowServer(toEnable === LogOrigin.Server); - setShowRx(toEnable === LogOrigin.Rx); - setShowTx(toEnable === LogOrigin.Tx); - setShowPlayback(toEnable === LogOrigin.Playback); + const toggleOrigin = useCallback((origin: LogOrigin) => { + setOrigins((previous) => { + const newOrigins = new Set(previous); + if (newOrigins.has(origin)) { + newOrigins.delete(origin); + } else { + newOrigins.add(origin); + } + return newOrigins; + }); }, []); + /** middle click on an origin shows only that origin */ + const showOnlyOrigin = useCallback((origin: LogOrigin) => { + setOrigins(new Set([origin])); + }, []); + + const searchTerm = search.toLowerCase(); + const filteredData = logData.filter((entry) => { + if (!origins.has(entry.origin)) return false; + if (onlyProblems && entry.level === LogLevel.Info) return false; + if (searchTerm && !entry.text.toLowerCase().includes(searchTerm)) return false; + return true; + }); + + const isFiltered = filteredData.length !== logData.length; + return ( <> + {allOrigins.map((origin) => ( + + ))} - - - - - + + setSearch(event.target.value)} /> + + {isFiltered ? `${filteredData.length} of ${logData.length}` : logData.length} entries + +
    {filteredData.map((logEntry) => (
  • @@ -118,6 +93,9 @@ export default function Log() { {logEntry.text}
  • ))} + {filteredData.length === 0 && ( +
  • {logData.length === 0 ? 'No activity yet' : 'No entries match the filters'}
  • + )}
);