From 1fd1fe6994f3dd596eb649c59a2b107cd6026e64 Mon Sep 17 00:00:00 2001 From: Andrew Zelenets Date: Wed, 11 Mar 2026 16:01:12 +0100 Subject: [PATCH 1/7] WIP --- package-lock.json | 84 +++ package.json | 1 + src/App.test.tsx | 19 +- src/App.tsx | 208 ++++---- src/components/ConnectionPanel.test.tsx | 2 +- src/components/ConnectionPanel.tsx | 153 +++--- src/components/MessageRow.tsx | 61 ++- src/components/NamespaceFilter.tsx | 31 +- src/components/SchemaGuardModal.test.tsx | 16 +- src/components/SchemaGuardModal.tsx | 91 ++-- src/components/TimelinePanel.test.tsx | 24 +- src/components/TimelinePanel.tsx | 169 +++--- src/components/TransmitPanel.test.tsx | 18 +- src/components/TransmitPanel.tsx | 92 ++-- src/main.tsx | 2 + src/styles.css | 647 ++++------------------- vitest.setup.ts | 14 + 17 files changed, 746 insertions(+), 886 deletions(-) diff --git a/package-lock.json b/package-lock.json index f5b57f9..c04bbc4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "signal-trace", "version": "0.1.0", "dependencies": { + "@azelenets/aegis-design-system": "^0.1.3", "react": "^19.2.4", "react-dom": "^19.2.4" }, @@ -103,6 +104,20 @@ "dev": true, "license": "MIT" }, + "node_modules/@azelenets/aegis-design-system": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@azelenets/aegis-design-system/-/aegis-design-system-0.1.3.tgz", + "integrity": "sha512-6m9n0+Lr83KqDJ/brMvThshFl4+11y3mOliW/WgP9qc//6TOn6AVX61ckFUHrdVONmRF0efYT9wWEIwLgFqYvg==", + "license": "MIT", + "dependencies": { + "@material-symbols-svg/react": "^0.1.23", + "leaflet": "^1.9.4" + }, + "peerDependencies": { + "react": "^19.2.0", + "react-dom": "^19.2.0" + } + }, "node_modules/@babel/code-frame": { "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", @@ -1259,6 +1274,69 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@material-symbols-svg/react": { + "version": "0.1.23", + "resolved": "https://registry.npmjs.org/@material-symbols-svg/react/-/react-0.1.23.tgz", + "integrity": "sha512-wCHfGI1tflEtradIBLtEuFfd+9PtJIsZFVl+/Xm/h8cS8+cd/2WFFFSigPTCl3ES8kpRYAIyCQppFQDFs8xFFw==", + "license": "Apache-2.0", + "dependencies": { + "@material-symbols/svg-100": "^0.40.2", + "@material-symbols/svg-200": "^0.40.2", + "@material-symbols/svg-300": "^0.40.2", + "@material-symbols/svg-400": "^0.40.2", + "@material-symbols/svg-500": "^0.40.2", + "@material-symbols/svg-600": "^0.40.2", + "@material-symbols/svg-700": "^0.40.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@material-symbols/svg-100": { + "version": "0.40.2", + "resolved": "https://registry.npmjs.org/@material-symbols/svg-100/-/svg-100-0.40.2.tgz", + "integrity": "sha512-x8x/F+4D7FYe9ObdRVuKWy/ErsdUwX5W4J3HB/vLFiGUV2q0OmlKq+w36Dr8cFg0nijfAUqEB/gnLjp8mVScXw==", + "license": "Apache-2.0" + }, + "node_modules/@material-symbols/svg-200": { + "version": "0.40.2", + "resolved": "https://registry.npmjs.org/@material-symbols/svg-200/-/svg-200-0.40.2.tgz", + "integrity": "sha512-9jAlkUJV6TdTEnRw0kOHZlDlpQYRXz8mdao/v+P9xzmmi3ATMa4QfRJ0oqBIgkqmMHYon2NwSl6PHpMWHBY34A==", + "license": "Apache-2.0" + }, + "node_modules/@material-symbols/svg-300": { + "version": "0.40.2", + "resolved": "https://registry.npmjs.org/@material-symbols/svg-300/-/svg-300-0.40.2.tgz", + "integrity": "sha512-d7lvHBSAHFaaduIkNCpuQ16dEv+bd5B/Qiizf/NkoOXyHxqDcnesObeh28s4weQcRlg95XfXqzvZSs11UB2wsw==", + "license": "Apache-2.0" + }, + "node_modules/@material-symbols/svg-400": { + "version": "0.40.2", + "resolved": "https://registry.npmjs.org/@material-symbols/svg-400/-/svg-400-0.40.2.tgz", + "integrity": "sha512-e2yEgZW/OveVT1sGaZW1kkRWTPVghjsJYWy+vIea3q08Fv2o7FCYv23PESMyr5D4AaAXdM5dKWkF1e6yIm4swA==", + "license": "Apache-2.0" + }, + "node_modules/@material-symbols/svg-500": { + "version": "0.40.2", + "resolved": "https://registry.npmjs.org/@material-symbols/svg-500/-/svg-500-0.40.2.tgz", + "integrity": "sha512-eTNax14JaRlGKpnGTq22c/yQaRBoSOhwHN2yUZmIrmxhz1E1CPydxuRpPxDsUzTjy5GQhtczey/fg480tEMkXQ==", + "license": "Apache-2.0" + }, + "node_modules/@material-symbols/svg-600": { + "version": "0.40.2", + "resolved": "https://registry.npmjs.org/@material-symbols/svg-600/-/svg-600-0.40.2.tgz", + "integrity": "sha512-S9Db52VquiR8f+KWjWXGXIExZ3GmLi9nH8PPbfoPFsXS9zUXJil/FJ8YRLJJ9lXE3CEzyYIezTQOvhQS7Itdpw==", + "license": "Apache-2.0" + }, + "node_modules/@material-symbols/svg-700": { + "version": "0.40.2", + "resolved": "https://registry.npmjs.org/@material-symbols/svg-700/-/svg-700-0.40.2.tgz", + "integrity": "sha512-jnFwf0t1E+/eAjk9Ssqsy3sc/3af/IDopY2d2yHKOpgEjar2pPFqW2fFG9aztshxKlhPoli765NavZp4w9cWhg==", + "license": "Apache-2.0" + }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-rc.3", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz", @@ -3419,6 +3497,12 @@ "json-buffer": "3.0.1" } }, + "node_modules/leaflet": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz", + "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==", + "license": "BSD-2-Clause" + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", diff --git a/package.json b/package.json index 0b18041..bdc25ef 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "test:run": "vitest run" }, "dependencies": { + "@azelenets/aegis-design-system": "^0.1.3", "react": "^19.2.4", "react-dom": "^19.2.4" }, diff --git a/src/App.test.tsx b/src/App.test.tsx index d929842..30fb6df 100644 --- a/src/App.test.tsx +++ b/src/App.test.tsx @@ -41,6 +41,11 @@ class MockWebSocket { describe('App', () => { const originalWebSocket = globalThis.WebSocket; + const selectProtocolMode = async (user: ReturnType) => { + await user.click(screen.getAllByRole('combobox', { name: 'Protocol Decode' })[0]); + await user.click(screen.getByRole('option', { name: 'SOCKET.IO' })); + }; + beforeEach(() => { MockWebSocket.instances.length = 0; globalThis.WebSocket = MockWebSocket as unknown as typeof WebSocket; @@ -55,10 +60,9 @@ describe('App', () => { const user = userEvent.setup(); render(); - await user.click(screen.getByText('Schema Guard')); await user.click(screen.getByRole('button', { name: 'Open Schema Builder' })); - expect(screen.getByRole('dialog', { name: 'Schema Guard Builder' })).toBeInTheDocument(); + expect(screen.getByRole('dialog')).toBeInTheDocument(); expect(screen.getByText('Schema Guard Builder')).toBeInTheDocument(); }); @@ -67,7 +71,6 @@ describe('App', () => { render(); // Open schema builder and add a required field - await user.click(screen.getByText('Schema Guard')); await user.click(screen.getByRole('button', { name: 'Open Schema Builder' })); const dialog = screen.getByRole('dialog'); await user.click(within(dialog).getByRole('button', { name: 'Add Property' })); @@ -78,6 +81,7 @@ describe('App', () => { // Send a payload that is missing the required field const payloadInput = screen.getAllByLabelText('JSON Payload')[0]; fireEvent.change(payloadInput, { target: { value: '{"action":"ping"}' } }); + await user.click(screen.getByRole('button', { name: 'Demo' })); await user.click(screen.getByRole('button', { name: 'Send Frame' })); expect(screen.getByText('schema_violation')).toBeInTheDocument(); @@ -87,8 +91,9 @@ describe('App', () => { const user = userEvent.setup(); render(); - await user.selectOptions(screen.getAllByLabelText('Protocol Decode')[0], 'socketio'); + await selectProtocolMode(user); await user.click(screen.getAllByLabelText('Socket.IO Handshake')[0]); + await user.click(screen.getByRole('button', { name: 'Connect' })); await user.click(screen.getByRole('button', { name: 'Send Frame' })); expect(screen.getByText('socketio_not_ready')).toBeInTheDocument(); @@ -98,7 +103,7 @@ describe('App', () => { const user = userEvent.setup(); render(); - await user.selectOptions(screen.getAllByLabelText('Protocol Decode')[0], 'socketio'); + await selectProtocolMode(user); await user.click(screen.getAllByLabelText('Socket.IO Handshake')[0]); await user.clear(screen.getAllByLabelText('Socket.IO Namespace')[0]); await user.type(screen.getAllByLabelText('Socket.IO Namespace')[0], '/devices'); @@ -108,14 +113,14 @@ describe('App', () => { expect(ws).toBeDefined(); ws.emitOpen(); - expect(screen.getByText('CONNECTING')).toBeInTheDocument(); + expect(screen.getAllByText('CONNECTING').length).toBeGreaterThan(0); ws.emitMessage('0{"sid":"abc"}'); expect(ws.send).toHaveBeenCalledWith('40/devices'); ws.emitMessage('40/devices'); await waitFor(() => { - expect(screen.getByText('CONNECTED')).toBeInTheDocument(); + expect(screen.getAllByText('CONNECTED').length).toBeGreaterThan(0); }); ws.emitMessage('2'); diff --git a/src/App.tsx b/src/App.tsx index 57680bf..0e95daf 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,4 +1,16 @@ import { useCallback } from 'react'; +import { + Accordion, + Badge, + Card, + CardBody, + Container, + Grid, + GridItem, + PageHeader, + ThemeProvider, + ThemeToggle, +} from '@azelenets/aegis-design-system'; import { extractCorrelationId, safeJson, @@ -10,7 +22,6 @@ import { useTimeline } from './hooks/useTimeline'; import { useConnection } from './hooks/useConnection'; import { useSchemaGuard } from './hooks/useSchemaGuard'; import { ConnectionPanel } from './components/ConnectionPanel'; -import { NamespaceFilter } from './components/NamespaceFilter'; import { TransmitPanel } from './components/TransmitPanel'; import { SchemaGuardModal } from './components/SchemaGuardModal'; import { TimelinePanel } from './components/TimelinePanel'; @@ -132,98 +143,113 @@ const App = () => { }, [connection, schema.parsedSchema, timeline]); return ( -
-
-
-
-

R&D // EXPERIMENTAL LAB

-

SIGNALTRACE

-

Browser-based WebSocket traffic inspector for real-time IoT debugging.

-
- -
- - - + + + + + + + ), + }, + { + id: 'transmit', + trigger: 'Transmit', + content: ( + schema.setSchemaModalOpen(true)} + onSend={handleSend} + onSystemLog={timeline.appendSystem} + /> + ), + }, + ]} + /> + + + + + + + + + {schema.schemaModalOpen && ( + schema.setSchemaModalOpen(false)} + onAdd={schema.addSchemaProperty} + onRemove={schema.removeSchemaProperty} + onUpdateField={schema.updateSchemaPropertyField} + onUpdateType={schema.updateSchemaPropertyType} + onUpdateRequired={schema.updateSchemaPropertyRequired} /> -
-
- - {schema.schemaModalOpen && ( - schema.setSchemaModalOpen(false)} - onAdd={schema.addSchemaProperty} - onRemove={schema.removeSchemaProperty} - onUpdateField={schema.updateSchemaPropertyField} - onUpdateType={schema.updateSchemaPropertyType} - onUpdateRequired={schema.updateSchemaPropertyRequired} - /> - )} -
+ )} +
+ ); }; diff --git a/src/components/ConnectionPanel.test.tsx b/src/components/ConnectionPanel.test.tsx index 6cc7d80..c76b2da 100644 --- a/src/components/ConnectionPanel.test.tsx +++ b/src/components/ConnectionPanel.test.tsx @@ -37,7 +37,7 @@ describe('ConnectionPanel', () => { it('shows connection status dot and state label', () => { render(); expect(screen.getByText('CONNECTED')).toBeInTheDocument(); - expect(document.querySelector('.dot.connected')).toBeInTheDocument(); + expect(screen.getByText('CONNECTED').closest('span')).toBeInTheDocument(); }); it('hides Socket.IO fields when handshake is off', () => { diff --git a/src/components/ConnectionPanel.tsx b/src/components/ConnectionPanel.tsx index 9eb52a5..0acb34c 100644 --- a/src/components/ConnectionPanel.tsx +++ b/src/components/ConnectionPanel.tsx @@ -1,4 +1,17 @@ import { memo } from 'react'; +import { + Badge, + Button, + Card, + CardBody, + CardHeader, + FormRow, + FormSection, + Input, + Select, + Textarea, + Toggle, +} from '@azelenets/aegis-design-system'; import type { LinkState } from '../types'; import type { ProtocolMode } from '../lib/trace-utils'; @@ -41,99 +54,105 @@ export const ConnectionPanel = memo(function ConnectionPanel({ onDisconnect, onToggleDemo, }: ConnectionPanelProps) { + const statusVariant = (() => { + switch (connState) { + case 'CONNECTED': + return 'success'; + case 'CONNECTING': + return 'hazard'; + case 'ERROR': + return 'alert'; + default: + return 'ghost'; + } + })(); + return ( -
- - Connection - - - {connState} - - -
- - - - {socketIoHandshake && ( - <> - - -