een-api-toolkit 0.3.15 → 0.3.20

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.
Files changed (41) hide show
  1. package/CHANGELOG.md +45 -6
  2. package/README.md +1 -0
  3. package/dist/index.cjs +3 -1
  4. package/dist/index.cjs.map +1 -1
  5. package/dist/index.d.ts +585 -0
  6. package/dist/index.js +485 -261
  7. package/dist/index.js.map +1 -1
  8. package/docs/AI-CONTEXT.md +144 -1
  9. package/examples/vue-alerts-metrics/e2e/auth.spec.ts +8 -1
  10. package/examples/vue-alerts-metrics/package-lock.json +8 -1
  11. package/examples/vue-alerts-metrics/package.json +4 -3
  12. package/examples/vue-alerts-metrics/src/components/AlertsList.vue +567 -16
  13. package/examples/vue-alerts-metrics/src/components/CameraSelector.vue +16 -6
  14. package/examples/vue-alerts-metrics/src/components/MetricsChart.vue +23 -9
  15. package/examples/vue-alerts-metrics/src/components/NotificationsList.vue +579 -17
  16. package/examples/vue-alerts-metrics/src/components/TimeRangeSelector.vue +197 -12
  17. package/examples/vue-alerts-metrics/src/composables/useHlsPlayer.ts +285 -0
  18. package/examples/vue-alerts-metrics/src/views/Dashboard.vue +31 -9
  19. package/examples/vue-alerts-metrics/src/views/Home.vue +56 -7
  20. package/examples/vue-event-subscriptions/.env.example +15 -0
  21. package/examples/vue-event-subscriptions/README.md +103 -0
  22. package/examples/vue-event-subscriptions/e2e/app.spec.ts +71 -0
  23. package/examples/vue-event-subscriptions/e2e/auth.spec.ts +290 -0
  24. package/examples/vue-event-subscriptions/index.html +13 -0
  25. package/examples/vue-event-subscriptions/package-lock.json +1719 -0
  26. package/examples/vue-event-subscriptions/package.json +28 -0
  27. package/examples/vue-event-subscriptions/playwright.config.ts +47 -0
  28. package/examples/vue-event-subscriptions/src/App.vue +233 -0
  29. package/examples/vue-event-subscriptions/src/main.ts +25 -0
  30. package/examples/vue-event-subscriptions/src/router/index.ts +68 -0
  31. package/examples/vue-event-subscriptions/src/views/Callback.vue +76 -0
  32. package/examples/vue-event-subscriptions/src/views/Home.vue +192 -0
  33. package/examples/vue-event-subscriptions/src/views/LiveEvents.vue +640 -0
  34. package/examples/vue-event-subscriptions/src/views/Login.vue +33 -0
  35. package/examples/vue-event-subscriptions/src/views/Logout.vue +59 -0
  36. package/examples/vue-event-subscriptions/src/views/Subscriptions.vue +402 -0
  37. package/examples/vue-event-subscriptions/src/vite-env.d.ts +12 -0
  38. package/examples/vue-event-subscriptions/tsconfig.json +21 -0
  39. package/examples/vue-event-subscriptions/tsconfig.node.json +10 -0
  40. package/examples/vue-event-subscriptions/vite.config.ts +12 -0
  41. package/package.json +1 -1
@@ -0,0 +1,640 @@
1
+ <script setup lang="ts">
2
+ import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
3
+ import { useRoute } from 'vue-router'
4
+ import {
5
+ listEventSubscriptions,
6
+ connectToEventSubscription,
7
+ type EventSubscription,
8
+ type SSEConnection,
9
+ type SSEConnectionStatus,
10
+ type SSEEvent,
11
+ type EenError
12
+ } from 'een-api-toolkit'
13
+
14
+ const route = useRoute()
15
+
16
+ // Subscriptions state
17
+ const subscriptions = ref<EventSubscription[]>([])
18
+ const selectedSubscriptionId = ref<string | null>(null)
19
+ const loadingSubscriptions = ref(false)
20
+
21
+ // Connection state
22
+ const connection = ref<SSEConnection | null>(null)
23
+ const connectionStatus = ref<SSEConnectionStatus>('disconnected')
24
+ const connectionError = ref<EenError | null>(null)
25
+
26
+ // Events state
27
+ const events = ref<SSEEvent[]>([])
28
+ const maxEvents = 100
29
+
30
+ // Modal state
31
+ const selectedEvent = ref<SSEEvent | null>(null)
32
+ const showModal = ref(false)
33
+
34
+ const selectedSubscription = computed(() => {
35
+ return subscriptions.value.find(s => s.id === selectedSubscriptionId.value)
36
+ })
37
+
38
+ const canConnect = computed(() => {
39
+ if (!selectedSubscription.value) return false
40
+ if (selectedSubscription.value.deliveryConfig.type !== 'serverSentEvents.v1') return false
41
+ return !!selectedSubscription.value.deliveryConfig.sseUrl
42
+ })
43
+
44
+ const isConnected = computed(() => connectionStatus.value === 'connected')
45
+ const isConnecting = computed(() => connectionStatus.value === 'connecting')
46
+
47
+ async function loadSubscriptions() {
48
+ loadingSubscriptions.value = true
49
+ const result = await listEventSubscriptions({ pageSize: 100 })
50
+
51
+ if (!result.error) {
52
+ // Filter to only SSE subscriptions
53
+ subscriptions.value = result.data.results.filter(
54
+ s => s.deliveryConfig.type === 'serverSentEvents.v1'
55
+ )
56
+ }
57
+
58
+ loadingSubscriptions.value = false
59
+ }
60
+
61
+ function connect() {
62
+ if (!canConnect.value || !selectedSubscription.value) return
63
+
64
+ const sseUrl = selectedSubscription.value.deliveryConfig.type === 'serverSentEvents.v1'
65
+ ? selectedSubscription.value.deliveryConfig.sseUrl
66
+ : undefined
67
+
68
+ if (!sseUrl) return
69
+
70
+ connectionError.value = null
71
+ events.value = []
72
+
73
+ const result = connectToEventSubscription(sseUrl, {
74
+ onEvent: (event) => {
75
+ // Add new event at the beginning, limit to maxEvents
76
+ // Using unshift + pop is more efficient than spread operator for large arrays
77
+ events.value.unshift(event)
78
+ if (events.value.length > maxEvents) {
79
+ events.value.pop()
80
+ }
81
+ },
82
+ onError: (error) => {
83
+ connectionError.value = { code: 'NETWORK_ERROR', message: error.message }
84
+ },
85
+ onStatusChange: (status) => {
86
+ connectionStatus.value = status
87
+ }
88
+ })
89
+
90
+ if (result.error) {
91
+ connectionError.value = result.error
92
+ } else {
93
+ connection.value = result.data
94
+ }
95
+ }
96
+
97
+ function disconnect() {
98
+ if (connection.value) {
99
+ connection.value.close()
100
+ connection.value = null
101
+ }
102
+ }
103
+
104
+ function clearEvents() {
105
+ events.value = []
106
+ }
107
+
108
+ function formatTimestamp(timestamp: string): string {
109
+ return new Date(timestamp).toLocaleString()
110
+ }
111
+
112
+ function formatEventType(type: string): string {
113
+ // Extract the short name from the full event type ID
114
+ // e.g., "een.motionDetectionEvent.v1" -> "Motion Detection"
115
+ const parts = type.split('.')
116
+ if (parts.length >= 2) {
117
+ const name = parts[1]
118
+ .replace('Event', '')
119
+ .replace(/([A-Z])/g, ' $1')
120
+ .trim()
121
+ return name.charAt(0).toUpperCase() + name.slice(1)
122
+ }
123
+ return type
124
+ }
125
+
126
+ function openEventModal(event: SSEEvent) {
127
+ selectedEvent.value = event
128
+ showModal.value = true
129
+ // Add keyboard listener for Escape key
130
+ document.addEventListener('keydown', handleKeyDown)
131
+ }
132
+
133
+ function closeModal() {
134
+ showModal.value = false
135
+ selectedEvent.value = null
136
+ // Remove keyboard listener
137
+ document.removeEventListener('keydown', handleKeyDown)
138
+ }
139
+
140
+ function handleKeyDown(e: KeyboardEvent) {
141
+ if (e.key === 'Escape' && showModal.value) {
142
+ closeModal()
143
+ }
144
+ }
145
+
146
+ function handleModalBackdropClick(e: MouseEvent) {
147
+ if ((e.target as HTMLElement).classList.contains('modal-backdrop')) {
148
+ closeModal()
149
+ }
150
+ }
151
+
152
+ // Load subscriptions on mount
153
+ onMounted(async () => {
154
+ await loadSubscriptions()
155
+
156
+ // Check for subscriptionId in query params
157
+ const queryId = route.query.subscriptionId as string | undefined
158
+ if (queryId) {
159
+ selectedSubscriptionId.value = queryId
160
+ }
161
+ })
162
+
163
+ // Clean up connection on unmount
164
+ onUnmounted(() => {
165
+ disconnect()
166
+ })
167
+
168
+ // Auto-disconnect when changing subscription
169
+ watch(selectedSubscriptionId, () => {
170
+ if (connection.value) {
171
+ disconnect()
172
+ }
173
+ events.value = []
174
+ })
175
+ </script>
176
+
177
+ <template>
178
+ <div class="live-events">
179
+ <h2>Live Events</h2>
180
+
181
+ <!-- Subscription Selector -->
182
+ <div class="selector-section">
183
+ <label>Select Subscription:</label>
184
+ <select
185
+ v-model="selectedSubscriptionId"
186
+ :disabled="loadingSubscriptions || isConnected"
187
+ data-testid="subscription-select"
188
+ >
189
+ <option :value="null">-- Select a subscription --</option>
190
+ <option v-for="sub in subscriptions" :key="sub.id" :value="sub.id">
191
+ {{ sub.id.slice(0, 16) }}... ({{ sub.subscriptionConfig?.lifeCycle || 'unknown' }})
192
+ </option>
193
+ </select>
194
+
195
+ <div class="connection-controls">
196
+ <button
197
+ v-if="!isConnected"
198
+ @click="connect"
199
+ :disabled="!canConnect || isConnecting"
200
+ data-testid="connect-button"
201
+ >
202
+ {{ isConnecting ? 'Connecting...' : 'Connect' }}
203
+ </button>
204
+ <button
205
+ v-else
206
+ @click="disconnect"
207
+ class="danger"
208
+ data-testid="disconnect-button"
209
+ >
210
+ Disconnect
211
+ </button>
212
+ <button
213
+ @click="clearEvents"
214
+ class="secondary"
215
+ :disabled="events.length === 0"
216
+ >
217
+ Clear Events
218
+ </button>
219
+ </div>
220
+ </div>
221
+
222
+ <!-- Connection Status -->
223
+ <div class="status-section">
224
+ <span class="status-label">Status:</span>
225
+ <span
226
+ class="status-indicator"
227
+ :class="{
228
+ 'connected': isConnected,
229
+ 'connecting': isConnecting,
230
+ 'disconnected': connectionStatus === 'disconnected',
231
+ 'error': connectionStatus === 'error'
232
+ }"
233
+ data-testid="connection-status"
234
+ >
235
+ {{ connectionStatus }}
236
+ </span>
237
+ </div>
238
+
239
+ <div v-if="connectionError" class="error">
240
+ Connection Error: {{ connectionError.message }}
241
+ </div>
242
+
243
+ <!-- Events List -->
244
+ <div class="events-section">
245
+ <h3>Events ({{ events.length }})</h3>
246
+
247
+ <div v-if="events.length === 0" class="no-events">
248
+ <p v-if="isConnected">Waiting for events...</p>
249
+ <p v-else>Connect to a subscription to start receiving events.</p>
250
+ </div>
251
+
252
+ <div v-else class="events-list" data-testid="events-list">
253
+ <div
254
+ v-for="(event, index) in events"
255
+ :key="`${event.id}-${index}`"
256
+ class="event-card clickable"
257
+ @click="openEventModal(event)"
258
+ >
259
+ <div class="event-header">
260
+ <span class="event-type">{{ formatEventType(event.type) }}</span>
261
+ <span class="event-time">{{ formatTimestamp(event.startTimestamp) }}</span>
262
+ </div>
263
+ <div class="event-details">
264
+ <span class="detail">
265
+ <strong>Actor:</strong> {{ event.actorId }}
266
+ </span>
267
+ <span class="detail">
268
+ <strong>Type:</strong> {{ event.type }}
269
+ </span>
270
+ <span v-if="event.span !== undefined" class="detail">
271
+ <strong>Span:</strong> {{ event.span ? 'Yes' : 'No' }}
272
+ </span>
273
+ </div>
274
+ </div>
275
+ </div>
276
+ </div>
277
+
278
+ <div class="help-section">
279
+ <h4>Tips</h4>
280
+ <ul>
281
+ <li>Select a subscription from the dropdown to connect</li>
282
+ <li>Events will appear in real-time as they occur</li>
283
+ <li>Maximum {{ maxEvents }} events are displayed (oldest removed first)</li>
284
+ <li>Click on an event card to view detailed information</li>
285
+ </ul>
286
+ </div>
287
+
288
+ <!-- Event Details Modal -->
289
+ <div
290
+ v-if="showModal && selectedEvent"
291
+ class="modal-backdrop"
292
+ role="dialog"
293
+ aria-modal="true"
294
+ aria-labelledby="modal-title"
295
+ @click="handleModalBackdropClick"
296
+ >
297
+ <div class="modal-content" role="document">
298
+ <div class="modal-header">
299
+ <h3 id="modal-title">{{ formatEventType(selectedEvent.type) }}</h3>
300
+ <button class="modal-close" @click="closeModal" aria-label="Close modal">&times;</button>
301
+ </div>
302
+ <div class="modal-body">
303
+ <div class="modal-section">
304
+ <h4>Event Information</h4>
305
+ <div class="modal-field">
306
+ <strong>Event ID:</strong>
307
+ <span class="mono">{{ selectedEvent.id }}</span>
308
+ </div>
309
+ <div class="modal-field">
310
+ <strong>Type:</strong>
311
+ <span class="mono">{{ selectedEvent.type }}</span>
312
+ </div>
313
+ <div class="modal-field">
314
+ <strong>Actor:</strong>
315
+ <span class="mono">{{ selectedEvent.actorId }}</span>
316
+ </div>
317
+ <div class="modal-field">
318
+ <strong>Start Time:</strong>
319
+ <span>{{ formatTimestamp(selectedEvent.startTimestamp) }}</span>
320
+ </div>
321
+ <div v-if="selectedEvent.endTimestamp" class="modal-field">
322
+ <strong>End Time:</strong>
323
+ <span>{{ formatTimestamp(selectedEvent.endTimestamp) }}</span>
324
+ </div>
325
+ <div v-if="selectedEvent.span !== undefined" class="modal-field">
326
+ <strong>Span Event:</strong>
327
+ <span>{{ selectedEvent.span ? 'Yes' : 'No' }}</span>
328
+ </div>
329
+ </div>
330
+
331
+ <div v-if="selectedEvent.data && selectedEvent.data.length > 0" class="modal-section">
332
+ <h4>Event Data ({{ selectedEvent.data.length }} items)</h4>
333
+ <pre class="modal-json">{{ JSON.stringify(selectedEvent.data, null, 2) }}</pre>
334
+ </div>
335
+
336
+ <div v-else class="modal-section">
337
+ <h4>Event Data</h4>
338
+ <p class="no-data-text">No additional data available for this event.</p>
339
+ </div>
340
+ </div>
341
+ </div>
342
+ </div>
343
+ </div>
344
+ </template>
345
+
346
+ <style scoped>
347
+ .live-events {
348
+ max-width: 900px;
349
+ margin: 0 auto;
350
+ }
351
+
352
+ h2 {
353
+ margin-bottom: 20px;
354
+ }
355
+
356
+ .selector-section {
357
+ background: #f8f9fa;
358
+ padding: 20px;
359
+ border-radius: 8px;
360
+ margin-bottom: 20px;
361
+ }
362
+
363
+ .selector-section label {
364
+ display: block;
365
+ margin-bottom: 8px;
366
+ font-weight: 500;
367
+ }
368
+
369
+ .selector-section select {
370
+ width: 100%;
371
+ padding: 10px;
372
+ border: 1px solid #ddd;
373
+ border-radius: 4px;
374
+ font-size: 14px;
375
+ margin-bottom: 15px;
376
+ }
377
+
378
+ .connection-controls {
379
+ display: flex;
380
+ gap: 10px;
381
+ }
382
+
383
+ .status-section {
384
+ display: flex;
385
+ align-items: center;
386
+ gap: 10px;
387
+ margin-bottom: 15px;
388
+ padding: 10px;
389
+ background: #f5f5f5;
390
+ border-radius: 4px;
391
+ }
392
+
393
+ .status-label {
394
+ font-weight: 500;
395
+ }
396
+
397
+ .status-indicator {
398
+ padding: 4px 12px;
399
+ border-radius: 20px;
400
+ font-size: 12px;
401
+ font-weight: 500;
402
+ text-transform: uppercase;
403
+ }
404
+
405
+ .status-indicator.connected {
406
+ background: #d4edda;
407
+ color: #155724;
408
+ }
409
+
410
+ .status-indicator.connecting {
411
+ background: #fff3cd;
412
+ color: #856404;
413
+ }
414
+
415
+ .status-indicator.disconnected {
416
+ background: #e2e3e5;
417
+ color: #383d41;
418
+ }
419
+
420
+ .status-indicator.error {
421
+ background: #f8d7da;
422
+ color: #721c24;
423
+ }
424
+
425
+ .events-section {
426
+ margin-top: 20px;
427
+ }
428
+
429
+ .events-section h3 {
430
+ margin-bottom: 15px;
431
+ }
432
+
433
+ .no-events {
434
+ text-align: center;
435
+ color: #666;
436
+ padding: 40px;
437
+ background: #f8f9fa;
438
+ border-radius: 8px;
439
+ }
440
+
441
+ .events-list {
442
+ max-height: 500px;
443
+ overflow-y: auto;
444
+ border: 1px solid #e2e3e5;
445
+ border-radius: 8px;
446
+ }
447
+
448
+ .event-card {
449
+ padding: 15px;
450
+ border-bottom: 1px solid #e2e3e5;
451
+ background: white;
452
+ }
453
+
454
+ .event-card:last-child {
455
+ border-bottom: none;
456
+ }
457
+
458
+ .event-card.clickable {
459
+ cursor: pointer;
460
+ transition: background-color 0.15s ease, box-shadow 0.15s ease;
461
+ }
462
+
463
+ .event-card.clickable:hover {
464
+ background: #f0f7f4;
465
+ box-shadow: inset 3px 0 0 #42b883;
466
+ }
467
+
468
+ .event-header {
469
+ display: flex;
470
+ justify-content: space-between;
471
+ align-items: center;
472
+ margin-bottom: 8px;
473
+ }
474
+
475
+ .event-type {
476
+ font-weight: 600;
477
+ color: #42b883;
478
+ }
479
+
480
+ .event-time {
481
+ font-size: 12px;
482
+ color: #666;
483
+ }
484
+
485
+ .event-details {
486
+ display: flex;
487
+ flex-wrap: wrap;
488
+ gap: 15px;
489
+ font-size: 13px;
490
+ }
491
+
492
+ .detail strong {
493
+ color: #555;
494
+ }
495
+
496
+ /* Modal styles */
497
+ .modal-backdrop {
498
+ position: fixed;
499
+ top: 0;
500
+ left: 0;
501
+ width: 100%;
502
+ height: 100%;
503
+ background: rgba(0, 0, 0, 0.5);
504
+ display: flex;
505
+ align-items: center;
506
+ justify-content: center;
507
+ z-index: 1000;
508
+ }
509
+
510
+ .modal-content {
511
+ background: white;
512
+ border-radius: 12px;
513
+ width: 90%;
514
+ max-width: 600px;
515
+ max-height: 80vh;
516
+ overflow: hidden;
517
+ display: flex;
518
+ flex-direction: column;
519
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
520
+ }
521
+
522
+ .modal-header {
523
+ display: flex;
524
+ justify-content: space-between;
525
+ align-items: center;
526
+ padding: 20px;
527
+ border-bottom: 1px solid #e2e3e5;
528
+ background: #f8f9fa;
529
+ }
530
+
531
+ .modal-header h3 {
532
+ margin: 0;
533
+ color: #42b883;
534
+ }
535
+
536
+ .modal-close {
537
+ background: none;
538
+ border: none;
539
+ font-size: 28px;
540
+ cursor: pointer;
541
+ color: #666;
542
+ padding: 0;
543
+ line-height: 1;
544
+ width: 36px;
545
+ height: 36px;
546
+ display: flex;
547
+ align-items: center;
548
+ justify-content: center;
549
+ border-radius: 50%;
550
+ transition: background-color 0.15s ease;
551
+ }
552
+
553
+ .modal-close:hover {
554
+ background: #e2e3e5;
555
+ color: #333;
556
+ }
557
+
558
+ .modal-body {
559
+ padding: 20px;
560
+ overflow-y: auto;
561
+ }
562
+
563
+ .modal-section {
564
+ margin-bottom: 20px;
565
+ }
566
+
567
+ .modal-section:last-child {
568
+ margin-bottom: 0;
569
+ }
570
+
571
+ .modal-section h4 {
572
+ margin: 0 0 12px 0;
573
+ font-size: 14px;
574
+ color: #555;
575
+ text-transform: uppercase;
576
+ letter-spacing: 0.5px;
577
+ }
578
+
579
+ .modal-field {
580
+ display: flex;
581
+ gap: 10px;
582
+ padding: 8px 0;
583
+ border-bottom: 1px solid #f0f0f0;
584
+ }
585
+
586
+ .modal-field:last-child {
587
+ border-bottom: none;
588
+ }
589
+
590
+ .modal-field strong {
591
+ min-width: 100px;
592
+ color: #666;
593
+ font-weight: 500;
594
+ }
595
+
596
+ .modal-field .mono {
597
+ font-family: monospace;
598
+ font-size: 13px;
599
+ word-break: break-all;
600
+ }
601
+
602
+ .modal-json {
603
+ background: #f5f5f5;
604
+ padding: 15px;
605
+ border-radius: 8px;
606
+ font-size: 12px;
607
+ overflow-x: auto;
608
+ margin: 0;
609
+ max-height: 300px;
610
+ overflow-y: auto;
611
+ }
612
+
613
+ .no-data-text {
614
+ color: #888;
615
+ font-style: italic;
616
+ margin: 0;
617
+ }
618
+
619
+ .help-section {
620
+ margin-top: 30px;
621
+ padding: 15px;
622
+ background: #e9ecef;
623
+ border-radius: 8px;
624
+ }
625
+
626
+ .help-section h4 {
627
+ margin-bottom: 10px;
628
+ }
629
+
630
+ .help-section ul {
631
+ margin: 0;
632
+ padding-left: 20px;
633
+ color: #666;
634
+ font-size: 14px;
635
+ }
636
+
637
+ .help-section li {
638
+ margin-bottom: 5px;
639
+ }
640
+ </style>
@@ -0,0 +1,33 @@
1
+ <script setup lang="ts">
2
+ import { getAuthUrl } from 'een-api-toolkit'
3
+
4
+ function login() {
5
+ // Redirect to EEN OAuth login
6
+ window.location.href = getAuthUrl()
7
+ }
8
+ </script>
9
+
10
+ <template>
11
+ <div class="login">
12
+ <h2 data-testid="login-title">Login</h2>
13
+ <p>Click the button below to login with your Eagle Eye Networks account.</p>
14
+ <button data-testid="login-button" @click="login">Login with Eagle Eye Networks</button>
15
+ </div>
16
+ </template>
17
+
18
+ <style scoped>
19
+ .login {
20
+ text-align: center;
21
+ max-width: 400px;
22
+ margin: 0 auto;
23
+ }
24
+
25
+ h2 {
26
+ margin-bottom: 20px;
27
+ }
28
+
29
+ p {
30
+ margin-bottom: 20px;
31
+ color: #666;
32
+ }
33
+ </style>