@wandelbots/wandelbots-js-react-components 2.32.0-pr.feature-robot-precondition-list.372.8bd8d01 → 2.33.0-pr.feature-robot-precondition-list.372.cb78a22

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wandelbots/wandelbots-js-react-components",
3
- "version": "2.32.0-pr.feature-robot-precondition-list.372.8bd8d01",
3
+ "version": "2.33.0-pr.feature-robot-precondition-list.372.cb78a22",
4
4
  "description": "React UI toolkit for building applications on top of the Wandelbots platform",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -0,0 +1,85 @@
1
+ import type { SxProps } from "@mui/material"
2
+ import { observer } from "mobx-react-lite"
3
+ import { useEffect, useMemo, useRef } from "react"
4
+ import { externalizeComponent } from "../externalizeComponent"
5
+ import { LogStore } from "./LogStore"
6
+ import { LogViewer } from "./LogViewer"
7
+
8
+ export type LogPanelProps = {
9
+ /** Log store instance to use, or create one automatically if not provided */
10
+ store?: LogStore
11
+ /** Height of the component */
12
+ height?: string | number
13
+ /** Additional styles */
14
+ sx?: SxProps
15
+ /** Ref to the log store for external access */
16
+ onStoreReady?: (store: LogStore) => void
17
+ }
18
+
19
+ /**
20
+ * A complete log panel component with built-in state management.
21
+ *
22
+ * @example
23
+ * ```tsx
24
+ * // Simple usage with automatic state management
25
+ * function MyComponent() {
26
+ * const [logStore, setLogStore] = useState<LogStore>()
27
+ *
28
+ * return (
29
+ * <LogPanel
30
+ * height={400}
31
+ * onStoreReady={setLogStore}
32
+ * />
33
+ * )
34
+ * }
35
+ *
36
+ * // Then use the store to add messages
37
+ * logStore?.addInfo("Operation completed successfully")
38
+ * logStore?.addError("Something went wrong")
39
+ * logStore?.addWarning("Warning message")
40
+ * ```
41
+ *
42
+ * @example
43
+ * ```tsx
44
+ * // Usage with external store for shared state
45
+ * function MyApp() {
46
+ * const logStore = useMemo(() => new LogStore(), [])
47
+ *
48
+ * return (
49
+ * <div>
50
+ * <LogPanel store={logStore} height={300} />
51
+ * <SomeOtherComponent onError={(msg) => logStore.addError(msg)} />
52
+ * </div>
53
+ * )
54
+ * }
55
+ * ```
56
+ */
57
+ export const LogPanel = externalizeComponent(
58
+ observer((props: LogPanelProps) => {
59
+ const { store: externalStore, onStoreReady, ...logViewerProps } = props
60
+ const onStoreReadyRef = useRef(onStoreReady)
61
+
62
+ // Update ref when callback changes
63
+ useEffect(() => {
64
+ onStoreReadyRef.current = onStoreReady
65
+ }, [onStoreReady])
66
+
67
+ const store = useMemo(() => {
68
+ const logStore = externalStore || new LogStore()
69
+ onStoreReadyRef.current?.(logStore)
70
+ return logStore
71
+ }, [externalStore])
72
+
73
+ const handleClear = () => {
74
+ store.clearMessages()
75
+ }
76
+
77
+ return (
78
+ <LogViewer
79
+ {...logViewerProps}
80
+ messages={store.messages}
81
+ onClear={handleClear}
82
+ />
83
+ )
84
+ }),
85
+ )
@@ -0,0 +1,40 @@
1
+ import { action, makeObservable, observable } from "mobx"
2
+ import type { LogLevel, LogMessage } from "./LogViewer"
3
+
4
+ export class LogStore {
5
+ messages: LogMessage[] = []
6
+
7
+ constructor() {
8
+ makeObservable(this, {
9
+ messages: observable,
10
+ addMessage: action,
11
+ clearMessages: action,
12
+ })
13
+ }
14
+
15
+ addMessage = (message: string, level: LogLevel = "info") => {
16
+ const logMessage: LogMessage = {
17
+ id: Math.random().toString(36).substring(2, 11),
18
+ timestamp: new Date(),
19
+ message,
20
+ level,
21
+ }
22
+ this.messages.push(logMessage)
23
+ }
24
+
25
+ clearMessages = () => {
26
+ this.messages = []
27
+ }
28
+
29
+ addInfo = (message: string) => {
30
+ this.addMessage(message, "info")
31
+ }
32
+
33
+ addWarning = (message: string) => {
34
+ this.addMessage(message, "warning")
35
+ }
36
+
37
+ addError = (message: string) => {
38
+ this.addMessage(message, "error")
39
+ }
40
+ }
@@ -0,0 +1,297 @@
1
+ import {
2
+ ContentCopy,
3
+ DescriptionOutlined as DocumentIcon,
4
+ ExpandLess,
5
+ ExpandMore,
6
+ } from "@mui/icons-material"
7
+ import type { SxProps } from "@mui/material"
8
+ import { Box, Button, IconButton, Paper, Typography } from "@mui/material"
9
+ import { observer } from "mobx-react-lite"
10
+ import { useEffect, useRef, useState } from "react"
11
+ import { externalizeComponent } from "../externalizeComponent"
12
+
13
+ export type LogLevel = "info" | "error" | "warning"
14
+
15
+ export type LogMessage = {
16
+ id: string
17
+ timestamp: Date
18
+ message: string
19
+ level: LogLevel
20
+ }
21
+
22
+ export type LogViewerProps = {
23
+ /** Log messages to display */
24
+ messages: LogMessage[]
25
+ /** Callback when clear button is clicked */
26
+ onClear?: () => void
27
+ /** Height of the component */
28
+ height?: string | number
29
+ /** Additional styles */
30
+ sx?: SxProps
31
+ }
32
+
33
+ /**
34
+ * A log viewer component that displays timestamped log messages with different levels.
35
+ * Features a header with document icon and clear button, and scrollable message area.
36
+ */
37
+ export const LogViewer = externalizeComponent(
38
+ observer((props: LogViewerProps) => {
39
+ const { messages = [], onClear, height = 400, sx } = props
40
+ const scrollContainerRef = useRef<HTMLDivElement>(null)
41
+
42
+ // Auto-scroll to bottom when new messages are added
43
+ useEffect(() => {
44
+ if (messages.length === 0) return
45
+
46
+ const scrollContainer = scrollContainerRef.current
47
+ if (!scrollContainer) return
48
+
49
+ // Use a timeout to scroll after the DOM updates
50
+ const timeoutId = setTimeout(() => {
51
+ // Scroll the container to the bottom, not the entire browser
52
+ scrollContainer.scrollTop = scrollContainer.scrollHeight
53
+ }, 10)
54
+
55
+ return () => clearTimeout(timeoutId)
56
+ }, [messages.length])
57
+
58
+ const formatTimestamp = (timestamp: Date) => {
59
+ return timestamp.toLocaleTimeString("en-US", {
60
+ hour12: false,
61
+ hour: "2-digit",
62
+ minute: "2-digit",
63
+ second: "2-digit",
64
+ })
65
+ }
66
+
67
+ const getMessageColor = (level: LogLevel) => {
68
+ switch (level) {
69
+ case "error":
70
+ return "var(--error-main, #EF5350)"
71
+ case "warning":
72
+ return "var(--warning-main, #FF9800)"
73
+ case "info":
74
+ default:
75
+ return "var(--text-secondary, #FFFFFFB2)"
76
+ }
77
+ }
78
+
79
+ // Component for individual log messages with expand/copy functionality
80
+ const LogMessage = ({ message }: { message: LogMessage }) => {
81
+ const [isExpanded, setIsExpanded] = useState(false)
82
+ const [copyTooltip, setCopyTooltip] = useState(false)
83
+ const [isHovered, setIsHovered] = useState(false)
84
+ const isLongMessage = message.message.length > 150
85
+
86
+ const handleCopy = async () => {
87
+ try {
88
+ await navigator.clipboard.writeText(message.message)
89
+ setCopyTooltip(true)
90
+ setTimeout(() => setCopyTooltip(false), 2000)
91
+ } catch (err) {
92
+ console.error("Failed to copy message:", err)
93
+ }
94
+ }
95
+
96
+ const displayMessage =
97
+ isLongMessage && !isExpanded
98
+ ? message.message.substring(0, 150) + "..."
99
+ : message.message
100
+
101
+ return (
102
+ <Box
103
+ key={message.id}
104
+ onMouseEnter={() => setIsHovered(true)}
105
+ onMouseLeave={() => setIsHovered(false)}
106
+ sx={{
107
+ display: "flex",
108
+ gap: 1,
109
+ fontFamily: "monospace",
110
+ flexDirection: "column",
111
+ "&:hover": {
112
+ backgroundColor: "rgba(255, 255, 255, 0.03)",
113
+ },
114
+ borderRadius: "4px",
115
+ padding: "2px 4px",
116
+ margin: "-2px -4px",
117
+ }}
118
+ >
119
+ <Box sx={{ display: "flex", gap: 1 }}>
120
+ {/* Timestamp */}
121
+ <Typography
122
+ component="span"
123
+ sx={{
124
+ fontWeight: 400,
125
+ fontSize: "12px",
126
+ lineHeight: "18px",
127
+ letterSpacing: "0.4px",
128
+ color: "var(--text-disabled, #FFFFFF61)",
129
+ whiteSpace: "nowrap",
130
+ flexShrink: 0,
131
+ }}
132
+ >
133
+ [{formatTimestamp(message.timestamp)}]
134
+ </Typography>
135
+
136
+ {/* Message */}
137
+ <Typography
138
+ component="span"
139
+ sx={{
140
+ fontWeight: 400,
141
+ fontSize: "12px",
142
+ lineHeight: "18px",
143
+ letterSpacing: "0.4px",
144
+ color: getMessageColor(message.level),
145
+ wordBreak: "break-word",
146
+ overflowWrap: "anywhere",
147
+ hyphens: "auto",
148
+ flex: 1,
149
+ whiteSpace: "pre-wrap",
150
+ }}
151
+ >
152
+ {displayMessage}
153
+ </Typography>
154
+
155
+ {/* Action buttons - only visible on hover */}
156
+ <Box
157
+ sx={{
158
+ display: "flex",
159
+ alignItems: "flex-start",
160
+ gap: 0.5,
161
+ opacity: isHovered ? 1 : 0,
162
+ transition: "opacity 0.2s ease-in-out",
163
+ visibility: isHovered ? "visible" : "hidden",
164
+ }}
165
+ >
166
+ <IconButton
167
+ size="small"
168
+ onClick={handleCopy}
169
+ sx={{
170
+ padding: "2px",
171
+ color: "var(--text-secondary, #FFFFFFB2)",
172
+ "&:hover": {
173
+ backgroundColor: "rgba(255, 255, 255, 0.08)",
174
+ },
175
+ }}
176
+ title={copyTooltip ? "Copied!" : "Copy message"}
177
+ >
178
+ <ContentCopy sx={{ fontSize: 12 }} />
179
+ </IconButton>
180
+
181
+ {isLongMessage && (
182
+ <IconButton
183
+ size="small"
184
+ onClick={() => setIsExpanded(!isExpanded)}
185
+ sx={{
186
+ padding: "2px",
187
+ color: "var(--text-secondary, #FFFFFFB2)",
188
+ "&:hover": {
189
+ backgroundColor: "rgba(255, 255, 255, 0.08)",
190
+ },
191
+ }}
192
+ title={isExpanded ? "Collapse" : "Expand"}
193
+ >
194
+ {isExpanded ? (
195
+ <ExpandLess sx={{ fontSize: 12 }} />
196
+ ) : (
197
+ <ExpandMore sx={{ fontSize: 12 }} />
198
+ )}
199
+ </IconButton>
200
+ )}
201
+ </Box>
202
+ </Box>
203
+ </Box>
204
+ )
205
+ }
206
+
207
+ return (
208
+ <Paper
209
+ sx={{
210
+ background: "var(--background-paper-elevation-2, #171927)",
211
+ height,
212
+ display: "flex",
213
+ flexDirection: "column",
214
+ overflow: "hidden",
215
+ ...sx,
216
+ }}
217
+ >
218
+ {/* Header */}
219
+ <Box
220
+ sx={{
221
+ display: "flex",
222
+ alignItems: "center",
223
+ justifyContent: "space-between",
224
+ padding: "12px 16px",
225
+ }}
226
+ >
227
+ <Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
228
+ <DocumentIcon
229
+ sx={{ fontSize: 16, color: "var(--text-secondary, #FFFFFFB2)" }}
230
+ />
231
+ <Typography
232
+ sx={{
233
+ fontWeight: 500,
234
+ fontSize: "14px",
235
+ lineHeight: "143%",
236
+ letterSpacing: "0.17px",
237
+ color: "var(--text-primary, #FFFFFF)",
238
+ }}
239
+ >
240
+ Log
241
+ </Typography>
242
+ </Box>
243
+ <Button
244
+ onClick={onClear}
245
+ variant="text"
246
+ sx={{
247
+ fontWeight: 500,
248
+ fontSize: "13px",
249
+ lineHeight: "22px",
250
+ letterSpacing: "0.46px",
251
+ color: "var(--primary-main, #8E56FC)",
252
+ textTransform: "none",
253
+ minWidth: "auto",
254
+ padding: "4px 8px",
255
+ "&:hover": {
256
+ backgroundColor: "rgba(142, 86, 252, 0.08)",
257
+ },
258
+ }}
259
+ >
260
+ Clear
261
+ </Button>
262
+ </Box>
263
+
264
+ {/* Messages Container */}
265
+ <Box
266
+ ref={scrollContainerRef}
267
+ sx={{
268
+ flex: 1,
269
+ overflow: "auto",
270
+ padding: "8px 16px",
271
+ display: "flex",
272
+ flexDirection: "column",
273
+ gap: "2px",
274
+ }}
275
+ >
276
+ {messages.length === 0 ? (
277
+ <Typography
278
+ sx={{
279
+ color: "var(--text-disabled, #FFFFFF61)",
280
+ fontSize: "12px",
281
+ fontStyle: "italic",
282
+ textAlign: "center",
283
+ marginTop: 2,
284
+ }}
285
+ >
286
+ No log messages
287
+ </Typography>
288
+ ) : (
289
+ messages.map((message) => (
290
+ <LogMessage key={message.id} message={message} />
291
+ ))
292
+ )}
293
+ </Box>
294
+ </Paper>
295
+ )
296
+ }),
297
+ )
package/src/index.ts CHANGED
@@ -10,6 +10,9 @@ export { JoggingStore } from "./components/jogging/JoggingStore"
10
10
  export * from "./components/jogging/PoseCartesianValues"
11
11
  export * from "./components/jogging/PoseJointValues"
12
12
  export * from "./components/LoadingCover"
13
+ export * from "./components/LogPanel"
14
+ export { LogStore } from "./components/LogStore"
15
+ export * from "./components/LogViewer"
13
16
  export * from "./components/modal/NoMotionGroupModal"
14
17
  export * from "./components/ProgramControl"
15
18
  export * from "./components/ProgramStateIndicator"