@adcops/autocore-react 3.3.45 → 3.3.46

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,191 @@
1
+ /* LogPanel Styles */
2
+
3
+ .log-panel {
4
+ display: flex;
5
+ flex-direction: column;
6
+ height: 250px;
7
+ min-height: 150px;
8
+ max-height: 400px;
9
+ background-color: var(--surface-0, #0d0d0d);
10
+ border-top: 1px solid var(--surface-200, #333);
11
+ }
12
+
13
+ .log-header {
14
+ display: flex;
15
+ justify-content: space-between;
16
+ align-items: center;
17
+ padding: 0.5rem 1rem;
18
+ background-color: var(--surface-50, #1a1a1a);
19
+ border-bottom: 1px solid var(--surface-200, #333);
20
+ font-size: 0.85rem;
21
+ }
22
+
23
+ .log-title {
24
+ display: flex;
25
+ align-items: center;
26
+ gap: 0.5rem;
27
+ font-weight: 600;
28
+ color: #ccc;
29
+ }
30
+
31
+ .log-status {
32
+ font-weight: normal;
33
+ font-size: 0.75rem;
34
+ color: #888;
35
+ }
36
+
37
+ .log-controls {
38
+ display: flex;
39
+ align-items: center;
40
+ gap: 1rem;
41
+ }
42
+
43
+ .log-control-label {
44
+ display: flex;
45
+ align-items: center;
46
+ gap: 0.4rem;
47
+ color: #888;
48
+ font-size: 0.8rem;
49
+ }
50
+
51
+ .log-control-label input[type="checkbox"] {
52
+ margin: 0;
53
+ }
54
+
55
+ .log-level-select {
56
+ padding: 0.2rem 0.4rem;
57
+ background-color: #2a2a2a;
58
+ border: 1px solid #444;
59
+ color: #ddd;
60
+ font-size: 0.8rem;
61
+ border-radius: 2px;
62
+ cursor: pointer;
63
+ }
64
+
65
+ .log-level-select:focus {
66
+ outline: none;
67
+ border-color: var(--primary-color, #6a9fb5);
68
+ }
69
+
70
+ .log-clear-btn {
71
+ padding: 0.2rem 0.5rem;
72
+ font-size: 0.75rem;
73
+ background-color: transparent;
74
+ color: #666;
75
+ border: 1px solid #444;
76
+ border-radius: 2px;
77
+ cursor: pointer;
78
+ }
79
+
80
+ .log-clear-btn:hover {
81
+ background-color: #333;
82
+ color: #aaa;
83
+ }
84
+
85
+ .log-entries {
86
+ flex: 1;
87
+ overflow-y: auto;
88
+ padding: 0.25rem 0.5rem;
89
+ font-family: 'Consolas', 'Monaco', monospace;
90
+ font-size: 0.8rem;
91
+ line-height: 1.4;
92
+ }
93
+
94
+ .log-empty {
95
+ color: #555;
96
+ padding: 1rem;
97
+ text-align: center;
98
+ font-style: italic;
99
+ }
100
+
101
+ .log-entry {
102
+ display: flex;
103
+ gap: 0.5rem;
104
+ padding: 0.15rem 0;
105
+ white-space: nowrap;
106
+ }
107
+
108
+ .log-entry:hover {
109
+ background-color: rgba(255, 255, 255, 0.03);
110
+ }
111
+
112
+ .log-time {
113
+ color: #666;
114
+ flex-shrink: 0;
115
+ }
116
+
117
+ .log-level {
118
+ flex-shrink: 0;
119
+ font-weight: 600;
120
+ min-width: 45px;
121
+ }
122
+
123
+ .log-source {
124
+ color: #6a9fb5;
125
+ flex-shrink: 0;
126
+ }
127
+
128
+ .log-message {
129
+ color: #ccc;
130
+ white-space: pre-wrap;
131
+ word-break: break-word;
132
+ }
133
+
134
+ /* Level-specific colors */
135
+ .log-error {
136
+ color: #f44336;
137
+ }
138
+
139
+ .log-error .log-message {
140
+ color: #ff6b6b;
141
+ }
142
+
143
+ .log-warn {
144
+ color: #ff9800;
145
+ }
146
+
147
+ .log-warn .log-message {
148
+ color: #ffb74d;
149
+ }
150
+
151
+ .log-info {
152
+ color: #4caf50;
153
+ }
154
+
155
+ .log-info .log-message {
156
+ color: #ccc;
157
+ }
158
+
159
+ .log-debug {
160
+ color: #9e9e9e;
161
+ }
162
+
163
+ .log-debug .log-message {
164
+ color: #999;
165
+ }
166
+
167
+ .log-trace {
168
+ color: #666;
169
+ }
170
+
171
+ .log-trace .log-message {
172
+ color: #777;
173
+ }
174
+
175
+ /* Scrollbar styling */
176
+ .log-entries::-webkit-scrollbar {
177
+ width: 8px;
178
+ }
179
+
180
+ .log-entries::-webkit-scrollbar-track {
181
+ background: #1a1a1a;
182
+ }
183
+
184
+ .log-entries::-webkit-scrollbar-thumb {
185
+ background: #444;
186
+ border-radius: 4px;
187
+ }
188
+
189
+ .log-entries::-webkit-scrollbar-thumb:hover {
190
+ background: #555;
191
+ }
@@ -0,0 +1,5 @@
1
+ import React from 'react';
2
+ import './LogPanel.css';
3
+ export declare const LogPanel: React.FC;
4
+ export default LogPanel;
5
+ //# sourceMappingURL=LogPanel.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"LogPanel.d.ts","sourceRoot":"","sources":["../../src/components/LogPanel.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA+D,MAAM,OAAO,CAAC;AAEpF,OAAO,gBAAgB,CAAC;AA+CxB,eAAO,MAAM,QAAQ,EAAE,KAAK,CAAC,EA4J5B,CAAC;AAEF,eAAe,QAAQ,CAAC"}
@@ -0,0 +1 @@
1
+ import{jsx as _jsx,jsxs as _jsxs}from"react/jsx-runtime";import React,{useState,useEffect,useContext,useRef,useCallback}from"react";import{EventEmitterContext}from"../core/EventEmitterContext";import"./LogPanel.css";const LOG_LEVELS=["TRACE","DEBUG","INFO","WARN","ERROR"],MAX_LOGS=500,FLUSH_INTERVAL_MS=100,levelValue=e=>{const t=LOG_LEVELS.indexOf(e.toUpperCase());return t>=0?t:2},formatTime=e=>{const t=new Date(e);return`${t.getHours().toString().padStart(2,"0")}:${t.getMinutes().toString().padStart(2,"0")}:${t.getSeconds().toString().padStart(2,"0")}.${t.getMilliseconds().toString().padStart(3,"0")}`},getLevelClass=e=>{switch(e.toUpperCase()){case"ERROR":return"log-error";case"WARN":return"log-warn";case"INFO":default:return"log-info";case"DEBUG":return"log-debug";case"TRACE":return"log-trace"}};export const LogPanel=()=>{const{subscribe:e,unsubscribe:t,serverSubscribe:s,read:l,isConnected:r}=useContext(EventEmitterContext),[a,c]=useState([]),[n,o]=useState("INFO"),[i,u]=useState(!0),[g,d]=useState(!1),m=useRef(null),h=useRef([]),p=useRef(async()=>{});p.current=useCallback(async()=>{try{await s("log.stream","log.stream");const e=await l("log.get_buffer",{level:"TRACE"});e?.success&&Array.isArray(e.data)&&c(e.data.slice(-500)),d(!0)}catch(e){}},[s,l]),useEffect(()=>{r()&&p.current();const s=e("HUB/connected",()=>{p.current()});return()=>t(s)},[r,e,t]),useEffect(()=>{let s=!1;const l=e("log.stream",e=>{h.current.push(e),s||(s=!0,d(!0))}),r=window.setInterval(()=>{if(0===h.current.length)return;const e=h.current;h.current=[],c(t=>t.length+e.length>500?t.concat(e).slice(-500):t.concat(e))},100);return()=>{t(l),window.clearInterval(r),h.current=[]}},[e,t]),useEffect(()=>{i&&m.current&&(m.current.scrollTop=m.current.scrollHeight)},[a,i]);const x=a.filter(e=>levelValue(e.level)>=levelValue(n));return _jsxs("div",{className:"log-panel",children:[_jsxs("div",{className:"log-header",children:[_jsxs("div",{className:"log-title",children:[_jsx("span",{children:"Logs"}),!g&&_jsx("span",{className:"log-status",children:"(connecting...)"})]}),_jsxs("div",{className:"log-controls",children:[_jsxs("label",{className:"log-control-label",children:["Level:",_jsx("select",{value:n,onChange:e=>o(e.target.value),className:"log-level-select",children:LOG_LEVELS.map(e=>_jsx("option",{value:e,children:e},e))})]}),_jsxs("label",{className:"log-control-label",children:[_jsx("input",{type:"checkbox",checked:i,onChange:e=>u(e.target.checked)}),"Auto-scroll"]}),_jsx("button",{className:"log-clear-btn",onClick:async()=>{h.current=[],c([]);try{await l("log.clear")}catch(e){}},children:"Clear"})]})]}),_jsx("div",{className:"log-entries",ref:m,onScroll:()=>{if(m.current){const{scrollTop:e,scrollHeight:t,clientHeight:s}=m.current;u(t-e-s<50)}},children:0===x.length?_jsx("div",{className:"log-empty",children:"No log entries. Waiting for control program logs..."}):x.map((e,t)=>_jsxs("div",{className:`log-entry ${getLevelClass(e.level)}`,children:[_jsx("span",{className:"log-time",children:formatTime(e.timestamp_ms)}),_jsx("span",{className:`log-level ${getLevelClass(e.level)}`,children:e.level.padEnd(5)}),_jsxs("span",{className:"log-source",children:["[",e.source,"]"]}),_jsx("span",{className:"log-message",children:e.message})]},`${e.timestamp_ms}-${t}`))})]})};export default LogPanel;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adcops/autocore-react",
3
- "version": "3.3.45",
3
+ "version": "3.3.46",
4
4
  "description": "A React component library for industrial user interfaces.",
5
5
  "private": false,
6
6
  "type": "module",
@@ -0,0 +1,191 @@
1
+ /* LogPanel Styles */
2
+
3
+ .log-panel {
4
+ display: flex;
5
+ flex-direction: column;
6
+ height: 250px;
7
+ min-height: 150px;
8
+ max-height: 400px;
9
+ background-color: var(--surface-0, #0d0d0d);
10
+ border-top: 1px solid var(--surface-200, #333);
11
+ }
12
+
13
+ .log-header {
14
+ display: flex;
15
+ justify-content: space-between;
16
+ align-items: center;
17
+ padding: 0.5rem 1rem;
18
+ background-color: var(--surface-50, #1a1a1a);
19
+ border-bottom: 1px solid var(--surface-200, #333);
20
+ font-size: 0.85rem;
21
+ }
22
+
23
+ .log-title {
24
+ display: flex;
25
+ align-items: center;
26
+ gap: 0.5rem;
27
+ font-weight: 600;
28
+ color: #ccc;
29
+ }
30
+
31
+ .log-status {
32
+ font-weight: normal;
33
+ font-size: 0.75rem;
34
+ color: #888;
35
+ }
36
+
37
+ .log-controls {
38
+ display: flex;
39
+ align-items: center;
40
+ gap: 1rem;
41
+ }
42
+
43
+ .log-control-label {
44
+ display: flex;
45
+ align-items: center;
46
+ gap: 0.4rem;
47
+ color: #888;
48
+ font-size: 0.8rem;
49
+ }
50
+
51
+ .log-control-label input[type="checkbox"] {
52
+ margin: 0;
53
+ }
54
+
55
+ .log-level-select {
56
+ padding: 0.2rem 0.4rem;
57
+ background-color: #2a2a2a;
58
+ border: 1px solid #444;
59
+ color: #ddd;
60
+ font-size: 0.8rem;
61
+ border-radius: 2px;
62
+ cursor: pointer;
63
+ }
64
+
65
+ .log-level-select:focus {
66
+ outline: none;
67
+ border-color: var(--primary-color, #6a9fb5);
68
+ }
69
+
70
+ .log-clear-btn {
71
+ padding: 0.2rem 0.5rem;
72
+ font-size: 0.75rem;
73
+ background-color: transparent;
74
+ color: #666;
75
+ border: 1px solid #444;
76
+ border-radius: 2px;
77
+ cursor: pointer;
78
+ }
79
+
80
+ .log-clear-btn:hover {
81
+ background-color: #333;
82
+ color: #aaa;
83
+ }
84
+
85
+ .log-entries {
86
+ flex: 1;
87
+ overflow-y: auto;
88
+ padding: 0.25rem 0.5rem;
89
+ font-family: 'Consolas', 'Monaco', monospace;
90
+ font-size: 0.8rem;
91
+ line-height: 1.4;
92
+ }
93
+
94
+ .log-empty {
95
+ color: #555;
96
+ padding: 1rem;
97
+ text-align: center;
98
+ font-style: italic;
99
+ }
100
+
101
+ .log-entry {
102
+ display: flex;
103
+ gap: 0.5rem;
104
+ padding: 0.15rem 0;
105
+ white-space: nowrap;
106
+ }
107
+
108
+ .log-entry:hover {
109
+ background-color: rgba(255, 255, 255, 0.03);
110
+ }
111
+
112
+ .log-time {
113
+ color: #666;
114
+ flex-shrink: 0;
115
+ }
116
+
117
+ .log-level {
118
+ flex-shrink: 0;
119
+ font-weight: 600;
120
+ min-width: 45px;
121
+ }
122
+
123
+ .log-source {
124
+ color: #6a9fb5;
125
+ flex-shrink: 0;
126
+ }
127
+
128
+ .log-message {
129
+ color: #ccc;
130
+ white-space: pre-wrap;
131
+ word-break: break-word;
132
+ }
133
+
134
+ /* Level-specific colors */
135
+ .log-error {
136
+ color: #f44336;
137
+ }
138
+
139
+ .log-error .log-message {
140
+ color: #ff6b6b;
141
+ }
142
+
143
+ .log-warn {
144
+ color: #ff9800;
145
+ }
146
+
147
+ .log-warn .log-message {
148
+ color: #ffb74d;
149
+ }
150
+
151
+ .log-info {
152
+ color: #4caf50;
153
+ }
154
+
155
+ .log-info .log-message {
156
+ color: #ccc;
157
+ }
158
+
159
+ .log-debug {
160
+ color: #9e9e9e;
161
+ }
162
+
163
+ .log-debug .log-message {
164
+ color: #999;
165
+ }
166
+
167
+ .log-trace {
168
+ color: #666;
169
+ }
170
+
171
+ .log-trace .log-message {
172
+ color: #777;
173
+ }
174
+
175
+ /* Scrollbar styling */
176
+ .log-entries::-webkit-scrollbar {
177
+ width: 8px;
178
+ }
179
+
180
+ .log-entries::-webkit-scrollbar-track {
181
+ background: #1a1a1a;
182
+ }
183
+
184
+ .log-entries::-webkit-scrollbar-thumb {
185
+ background: #444;
186
+ border-radius: 4px;
187
+ }
188
+
189
+ .log-entries::-webkit-scrollbar-thumb:hover {
190
+ background: #555;
191
+ }
@@ -0,0 +1,208 @@
1
+ import React, { useState, useEffect, useContext, useRef, useCallback } from 'react';
2
+ import { EventEmitterContext } from '../core/EventEmitterContext';
3
+ import './LogPanel.css';
4
+
5
+ interface LogEntry {
6
+ timestamp_ms: number;
7
+ level: string;
8
+ source: string;
9
+ target: string;
10
+ message: string;
11
+ }
12
+
13
+ const LOG_LEVELS = ['TRACE', 'DEBUG', 'INFO', 'WARN', 'ERROR'];
14
+
15
+ // Cap on the in-memory ring buffer. Beyond this, the oldest entries drop.
16
+ // 500 keeps React's list small enough to reconcile without jank in Firefox
17
+ // (Chrome tolerates much more, but we'd rather not tune per-browser).
18
+ const MAX_LOGS = 500;
19
+
20
+ // Max cadence at which we push buffered entries into React state. At high log
21
+ // rates this turns N setState/sec into ~10 setState/sec — the difference
22
+ // between "Firefox stops responding" and "scrolls smoothly."
23
+ const FLUSH_INTERVAL_MS = 100;
24
+
25
+ const levelValue = (level: string): number => {
26
+ const idx = LOG_LEVELS.indexOf(level.toUpperCase());
27
+ return idx >= 0 ? idx : 2; // Default to INFO
28
+ };
29
+
30
+ const formatTime = (timestampMs: number): string => {
31
+ const date = new Date(timestampMs);
32
+ const hours = date.getHours().toString().padStart(2, '0');
33
+ const minutes = date.getMinutes().toString().padStart(2, '0');
34
+ const seconds = date.getSeconds().toString().padStart(2, '0');
35
+ const ms = date.getMilliseconds().toString().padStart(3, '0');
36
+ return `${hours}:${minutes}:${seconds}.${ms}`;
37
+ };
38
+
39
+ const getLevelClass = (level: string): string => {
40
+ switch (level.toUpperCase()) {
41
+ case 'ERROR': return 'log-error';
42
+ case 'WARN': return 'log-warn';
43
+ case 'INFO': return 'log-info';
44
+ case 'DEBUG': return 'log-debug';
45
+ case 'TRACE': return 'log-trace';
46
+ default: return 'log-info';
47
+ }
48
+ };
49
+
50
+ export const LogPanel: React.FC = () => {
51
+ const { subscribe, unsubscribe, serverSubscribe, read, isConnected } = useContext(EventEmitterContext);
52
+ const [logs, setLogs] = useState<LogEntry[]>([]);
53
+ const [levelFilter, setLevelFilter] = useState<string>('INFO');
54
+ const [autoScroll, setAutoScroll] = useState(true);
55
+ const [isSubscribed, setIsSubscribed] = useState(false);
56
+ const logContainerRef = useRef<HTMLDivElement>(null);
57
+
58
+ // Staging buffer for incoming entries. We do *not* call setState per entry.
59
+ const pendingRef = useRef<LogEntry[]>([]);
60
+
61
+ // Single init path: attempt serverSubscribe + fetch buffered logs.
62
+ // Called on mount, and again whenever HUB/connected fires in case the
63
+ // hub wasn't ready the first time.
64
+ const initRef = useRef<() => Promise<void>>(async () => {});
65
+ initRef.current = useCallback(async () => {
66
+ try {
67
+ await serverSubscribe('log.stream', 'log.stream');
68
+ const resp = await read('log.get_buffer', { level: 'TRACE' });
69
+ if (resp?.success && Array.isArray(resp.data)) {
70
+ setLogs(resp.data.slice(-MAX_LOGS));
71
+ }
72
+ setIsSubscribed(true);
73
+ } catch (err) {
74
+ console.warn('LogPanel: init deferred until hub reconnects:', err);
75
+ }
76
+ }, [serverSubscribe, read]);
77
+
78
+ useEffect(() => {
79
+ if (isConnected()) {
80
+ initRef.current();
81
+ }
82
+ // Retry once the hub comes up (covers the mount-before-connect race
83
+ // that left "Connecting..." stuck on even after logs started flowing).
84
+ const id = subscribe('HUB/connected', () => {
85
+ initRef.current();
86
+ });
87
+ return () => unsubscribe(id);
88
+ }, [isConnected, subscribe, unsubscribe]);
89
+
90
+ // Receive log entries into a ref; flush to state on an interval.
91
+ // Also flips `isSubscribed` on first entry received, so the header shows
92
+ // the real state even if serverSubscribe didn't resolve cleanly (e.g.,
93
+ // the server was already broadcasting to us from a prior subscription).
94
+ useEffect(() => {
95
+ let sawEntry = false;
96
+
97
+ const subId = subscribe('log.stream', (entry: LogEntry) => {
98
+ pendingRef.current.push(entry);
99
+ if (!sawEntry) {
100
+ sawEntry = true;
101
+ setIsSubscribed(true);
102
+ }
103
+ });
104
+
105
+ const flushTimer = window.setInterval(() => {
106
+ if (pendingRef.current.length === 0) return;
107
+ const batch = pendingRef.current;
108
+ pendingRef.current = [];
109
+ setLogs(prev => {
110
+ const merged = prev.length + batch.length > MAX_LOGS
111
+ ? prev.concat(batch).slice(-MAX_LOGS)
112
+ : prev.concat(batch);
113
+ return merged;
114
+ });
115
+ }, FLUSH_INTERVAL_MS);
116
+
117
+ return () => {
118
+ unsubscribe(subId);
119
+ window.clearInterval(flushTimer);
120
+ pendingRef.current = [];
121
+ };
122
+ }, [subscribe, unsubscribe]);
123
+
124
+ // Auto-scroll to bottom when new logs arrive.
125
+ useEffect(() => {
126
+ if (autoScroll && logContainerRef.current) {
127
+ logContainerRef.current.scrollTop = logContainerRef.current.scrollHeight;
128
+ }
129
+ }, [logs, autoScroll]);
130
+
131
+ // Handle scroll to detect if user has scrolled up.
132
+ const handleScroll = () => {
133
+ if (logContainerRef.current) {
134
+ const { scrollTop, scrollHeight, clientHeight } = logContainerRef.current;
135
+ const isAtBottom = scrollHeight - scrollTop - clientHeight < 50;
136
+ setAutoScroll(isAtBottom);
137
+ }
138
+ };
139
+
140
+ // Clear logs locally and on server.
141
+ const handleClear = async () => {
142
+ pendingRef.current = [];
143
+ setLogs([]);
144
+ try {
145
+ await read('log.clear');
146
+ } catch (err) {
147
+ console.warn('Failed to clear server log buffer:', err);
148
+ }
149
+ };
150
+
151
+ const filteredLogs = logs.filter(log => levelValue(log.level) >= levelValue(levelFilter));
152
+
153
+ return (
154
+ <div className="log-panel">
155
+ <div className="log-header">
156
+ <div className="log-title">
157
+ <span>Logs</span>
158
+ {!isSubscribed && <span className="log-status">(connecting...)</span>}
159
+ </div>
160
+ <div className="log-controls">
161
+ <label className="log-control-label">
162
+ Level:
163
+ <select
164
+ value={levelFilter}
165
+ onChange={(e) => setLevelFilter(e.target.value)}
166
+ className="log-level-select"
167
+ >
168
+ {LOG_LEVELS.map(level => (
169
+ <option key={level} value={level}>{level}</option>
170
+ ))}
171
+ </select>
172
+ </label>
173
+ <label className="log-control-label">
174
+ <input
175
+ type="checkbox"
176
+ checked={autoScroll}
177
+ onChange={(e) => setAutoScroll(e.target.checked)}
178
+ />
179
+ Auto-scroll
180
+ </label>
181
+ <button className="log-clear-btn" onClick={handleClear}>Clear</button>
182
+ </div>
183
+ </div>
184
+ <div
185
+ className="log-entries"
186
+ ref={logContainerRef}
187
+ onScroll={handleScroll}
188
+ >
189
+ {filteredLogs.length === 0 ? (
190
+ <div className="log-empty">
191
+ No log entries. Waiting for control program logs...
192
+ </div>
193
+ ) : (
194
+ filteredLogs.map((log, idx) => (
195
+ <div key={`${log.timestamp_ms}-${idx}`} className={`log-entry ${getLevelClass(log.level)}`}>
196
+ <span className="log-time">{formatTime(log.timestamp_ms)}</span>
197
+ <span className={`log-level ${getLevelClass(log.level)}`}>{log.level.padEnd(5)}</span>
198
+ <span className="log-source">[{log.source}]</span>
199
+ <span className="log-message">{log.message}</span>
200
+ </div>
201
+ ))
202
+ )}
203
+ </div>
204
+ </div>
205
+ );
206
+ };
207
+
208
+ export default LogPanel;