event-storage-ui 1.0.0-alpha.1 → 1.0.0-alpha.2

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/README.md CHANGED
@@ -10,15 +10,23 @@ This is an admin dashboard for inspecting a running [node-event-storage](https:/
10
10
 
11
11
  ![Event Stream](https://raw.githubusercontent.com/albe/node-event-storage-ui/main/public/screenshots/event-stream.png)
12
12
 
13
+ ### Event commit
14
+
15
+ ![Commit Events](https://raw.githubusercontent.com/albe/node-event-storage-ui/main/public/screenshots/write-events-filled.png)
16
+
13
17
  ### Consumers (create and list)
14
18
 
15
19
  ![Consumers](https://raw.githubusercontent.com/albe/node-event-storage-ui/main/public/screenshots/consumers-preview-executed.png)
16
20
 
17
- ### Event commit/write
21
+ ### Consumer Browser
22
+
23
+ ![Consumer Browser](https://raw.githubusercontent.com/albe/node-event-storage-ui/main/public/screenshots/consumers-list.png)
18
24
 
19
- ![Write Events](https://raw.githubusercontent.com/albe/node-event-storage-ui/main/public/screenshots/write-events-filled.png)
25
+ ### Consumer Detail
20
26
 
21
- ## Usage
27
+ ![Consumer Detail](https://raw.githubusercontent.com/albe/node-event-storage-ui/main/public/screenshots/consumers-detail.png)
28
+
29
+ ### Event commit/write
22
30
 
23
31
  ```
24
32
  git clone https://github.com/albe/node-event-storage-ui.git
@@ -51,3 +59,9 @@ Configuration is loaded at server startup, so restart the app after changing `ev
51
59
  }
52
60
  }
53
61
  ```
62
+
63
+ ## Cypress test suites
64
+
65
+ - Functional tests (used in PR CI): `npm run cypress:functional`
66
+ - Screenshot tests (README images only, includes automatic sync): `npm run cypress:screenshots:readme`
67
+ - Manual sync only (optional): `npm run screenshots:sync`
@@ -5,7 +5,7 @@ import 'react18-json-view/src/dark.css';
5
5
  const JsonView = JsonViewModule.default ?? JsonViewModule;
6
6
 
7
7
  export default function Json({ data, collapsed = true, style = undefined, className = '' }) {
8
- const normalizedCollapsed = collapsed === false ? 1 : collapsed === true ? true : collapsed;
8
+ const normalizedCollapsed = collapsed === true ? true : collapsed === false ? false : collapsed;
9
9
 
10
10
  return (
11
11
  <JsonView
@@ -116,6 +116,11 @@ function replayConsumer({ stream, consumerLogic, initialState }) {
116
116
  return state;
117
117
  }
118
118
 
119
+ function buildConsumerRouteIdentifier(streamName, consumerName) {
120
+ const indexName = streamName === '_all' ? '_all' : `stream-${streamName}`;
121
+ return `${indexName}.${consumerName}`;
122
+ }
123
+
119
124
  export async function previewConsumerState(
120
125
  { streamNames: streamNamesInput, consumerLogic, initialState = {} },
121
126
  storeNameOverride
@@ -188,7 +193,7 @@ export async function createConsumer(
188
193
  });
189
194
 
190
195
  return {
191
- consumerIdentifier: `${normalizedStreamName}.${normalizedConsumerName}`,
196
+ consumerIdentifier: buildConsumerRouteIdentifier(normalizedStreamName, normalizedConsumerName),
192
197
  streamName: normalizedStreamName,
193
198
  consumerName: normalizedConsumerName
194
199
  };
package/app/root.jsx CHANGED
@@ -72,15 +72,18 @@ export default function App() {
72
72
  <body>
73
73
  <div className="shell">
74
74
  <aside className="d-sidebar">
75
- <div className="brand brand--text-only">
75
+ <div className="brand">
76
76
  <div className="brand-text">
77
77
  <div className="brand-name">event-storage-ui</div>
78
78
  <div className="brand-tag">node-event-storage</div>
79
79
  </div>
80
+ <div className="brand-logo brand-logo-transparent">
81
+ <img alt="" src="/icon-hero-variant-3.svg" />
82
+ </div>
80
83
  </div>
81
84
 
82
85
  <nav className="nav-section">
83
- <div className="nav-label">Workspace</div>
86
+ <div className="nav-label">Workspace</div>
84
87
  <NavLink
85
88
  to={`/${storeSearch}`}
86
89
  className={({ isActive }) => `nav-link${isActive ? ' is-active' : ''}`}
@@ -97,28 +100,43 @@ export default function App() {
97
100
  <span>Stream Browser</span>
98
101
  </NavLink>
99
102
  <NavLink
100
- to={`/consumers${storeSearch}`}
103
+ to={`/query${storeSearch}`}
101
104
  className={({ isActive }) => `nav-link${isActive ? ' is-active' : ''}`}
102
105
  >
103
- <i className="material-icons">restore_page</i>
104
- <span>Consumers</span>
106
+ <i className="material-icons">search</i>
107
+ <span>Query</span>
105
108
  </NavLink>
106
109
  {!storeLocked && (
107
110
  <NavLink
108
- to={`/write-events${storeSearch}`}
111
+ to={`/commit-events${storeSearch}`}
109
112
  className={({ isActive }) => `nav-link${isActive ? ' is-active' : ''}`}
110
113
  >
111
114
  <i className="material-icons">edit</i>
112
- <span>Write Events</span>
115
+ <span>Commit Events</span>
113
116
  </NavLink>
114
117
  )}
118
+ <NavLink
119
+ to={`/consumers${storeSearch}`}
120
+ className={({ isActive }) => `nav-link${isActive ? ' is-active' : ''}`}
121
+ end
122
+ >
123
+ <i className="material-icons">manage_search</i>
124
+ <span>Consumer Browser</span>
125
+ </NavLink>
126
+ <NavLink
127
+ to={`/consumers/create${storeSearch}`}
128
+ className={({ isActive }) => `nav-link${isActive ? ' is-active' : ''}`}
129
+ >
130
+ <i className="material-icons">playlist_add</i>
131
+ <span>Create Consumer</span>
132
+ </NavLink>
115
133
  </nav>
116
134
  </aside>
117
135
 
118
136
  <div className="main">
119
137
  <header className="d-topbar">
120
138
  <div className="crumbs">
121
- <img src="/logo_white.png" className="topbar-logo" alt="* event-storage" />
139
+ <img src="/logo_white.svg" className="topbar-logo" alt="* event-storage" />
122
140
  </div>
123
141
  <div className="topbar-actions">
124
142
  <ul className="nav-right">
@@ -14,28 +14,24 @@ export async function loader({ request }) {
14
14
  const storeNameOverride = url.searchParams.get('store') || undefined;
15
15
  const { eventstore, storageStats } = await getEventStore({ readOnly: true }, storeNameOverride);
16
16
 
17
- try {
18
- const consumers = await new Promise((resolve, reject) => {
19
- eventstore.scanConsumers((err, scannedConsumers) => {
20
- if (err) {
21
- reject(err);
22
- return;
23
- }
24
- resolve(scannedConsumers);
25
- });
17
+ const consumers = await new Promise((resolve, reject) => {
18
+ eventstore.scanConsumers((err, scannedConsumers) => {
19
+ if (err) {
20
+ reject(err);
21
+ return;
22
+ }
23
+ resolve(scannedConsumers);
26
24
  });
25
+ });
27
26
 
28
- return {
29
- storeName: eventstore.storeName,
30
- storageDirectory: eventstore.storageDirectory,
31
- streamsCount: Object.keys(eventstore.streams).length,
32
- eventsCount: eventstore.length,
33
- consumersCount: consumers.length,
34
- stats: storageStats ?? {}
35
- };
36
- } finally {
37
- eventstore.close();
38
- }
27
+ return {
28
+ storeName: eventstore.storeName,
29
+ storageDirectory: eventstore.storageDirectory,
30
+ streamsCount: Object.keys(eventstore.streams).length,
31
+ eventsCount: eventstore.length,
32
+ consumersCount: consumers.length,
33
+ stats: storageStats ?? {}
34
+ };
39
35
  }
40
36
 
41
37
  const COLORS = { primary: '#00bcd4', success: '#32d48e', warning: '#f59e0b' };
@@ -5,7 +5,7 @@ import { Form } from 'react-router';
5
5
  import { getStoreLockStatus, commitToEventStore } from '../../eventstore';
6
6
  import Json from '../components/json';
7
7
 
8
- export const meta = () => [{ title: 'event-storage: Write Events' }];
8
+ export const meta = () => [{ title: 'event-storage: Commit Events' }];
9
9
 
10
10
  export async function loader({ request }) {
11
11
  const url = new URL(request.url);
@@ -97,10 +97,10 @@ export default function WriteEvents() {
97
97
  <div className="page-stack">
98
98
  <section className="page-hero hero">
99
99
  <div className="hero-text">
100
- <div className="page-eyebrow eyebrow">Writer</div>
101
- <h2 className="page-title hero-title">Write Events ({storeName})</h2>
100
+ <div className="page-eyebrow eyebrow">Committer</div>
101
+ <h2 className="page-title hero-title">Commit Events ({storeName})</h2>
102
102
  <p className="page-subtitle hero-sub">
103
- Writing is disabled while this store is locked by an external process.
103
+ Committing is disabled while this store is locked by an external process.
104
104
  </p>
105
105
  </div>
106
106
  <div className="page-actions hero-actions">
@@ -116,7 +116,7 @@ export default function WriteEvents() {
116
116
  <div className="status-banner" role="alert">
117
117
  <span className="status-banner__icon">❗</span>
118
118
  <div className="status-banner__text">
119
- This Eventstore is currently locked by an external process. Writing is not possible while the
119
+ This Eventstore is currently locked by an external process. Committing is not possible while the
120
120
  store is locked.
121
121
  </div>
122
122
  </div>
@@ -130,8 +130,8 @@ export default function WriteEvents() {
130
130
  <div className="page-stack">
131
131
  <section className="page-hero hero">
132
132
  <div className="hero-text">
133
- <div className="page-eyebrow eyebrow">Writer</div>
134
- <h2 className="page-title hero-title">Write Events ({storeName})</h2>
133
+ <div className="page-eyebrow eyebrow">Committer</div>
134
+ <h2 className="page-title hero-title">Commit Events ({storeName})</h2>
135
135
  <p className="page-subtitle hero-sub">
136
136
  Compose new event payloads, preview parsed JSON, and optionally attach metadata before committing.
137
137
  </p>
@@ -6,29 +6,46 @@ export const meta = ({ params }) => [
6
6
  { title: `event-storage: Consumer ${params.consumerIdentifier}` }
7
7
  ];
8
8
 
9
+ function parseStreamFromIndexName(indexName) {
10
+ if (indexName === '_all') {
11
+ return '_all';
12
+ }
13
+ if (indexName.startsWith('stream-')) {
14
+ return indexName.slice(7);
15
+ }
16
+ return indexName;
17
+ }
18
+
19
+ function parseConsumerRouteIdentifier(consumerIdentifier) {
20
+ const splitIndex = consumerIdentifier.lastIndexOf('.');
21
+ if (splitIndex < 0) {
22
+ return { indexName: consumerIdentifier, consumerName: consumerIdentifier };
23
+ }
24
+ const indexName = consumerIdentifier.slice(0, splitIndex);
25
+ const consumerName = consumerIdentifier.slice(splitIndex + 1);
26
+ return { indexName, consumerName };
27
+ }
28
+
9
29
  export async function loader({ params, request }) {
10
30
  const consumerIdentifier = params.consumerIdentifier;
11
31
  const url = new URL(request.url);
12
32
  const storeNameOverride = url.searchParams.get('store') || undefined;
13
33
  const { eventstore } = await getEventStore({ readOnly: true }, storeNameOverride);
14
34
 
15
- try {
16
- const [indexName, consumerName] = consumerIdentifier.split('.', 2);
17
- const consumer = eventstore.getConsumer(indexName, consumerName);
18
- const consumerPosition = consumer.position;
19
- const consumerState = consumer.state;
20
- const indexLength = consumer.index.length;
35
+ const parsed = parseConsumerRouteIdentifier(consumerIdentifier);
36
+ const streamName = parseStreamFromIndexName(parsed.indexName);
37
+ const consumer = eventstore.getConsumer(streamName, parsed.consumerName);
38
+ const consumerPosition = consumer.position;
39
+ const consumerState = consumer.state;
40
+ const indexLength = consumer.index.length;
21
41
 
22
- return {
23
- indexName,
24
- indexLength,
25
- consumerName,
26
- consumerPosition,
27
- consumerState
28
- };
29
- } finally {
30
- eventstore.close();
31
- }
42
+ return {
43
+ indexName: streamName,
44
+ indexLength,
45
+ consumerName: parsed.consumerName,
46
+ consumerPosition,
47
+ consumerState
48
+ };
32
49
  }
33
50
 
34
51
  export default function Consumer() {
@@ -88,7 +105,7 @@ export default function Consumer() {
88
105
  <div className="meta-list__item">
89
106
  <div className="meta-list__label">State</div>
90
107
  <div className="json-surface json-surface--short">
91
- <Json data={consumerState} />
108
+ <Json data={consumerState} collapsed={3} />
92
109
  </div>
93
110
  </div>
94
111
  </div>