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.
- package/CHANGELOG.md +45 -6
- package/README.md +1 -0
- package/dist/index.cjs +3 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +585 -0
- package/dist/index.js +485 -261
- package/dist/index.js.map +1 -1
- package/docs/AI-CONTEXT.md +144 -1
- package/examples/vue-alerts-metrics/e2e/auth.spec.ts +8 -1
- package/examples/vue-alerts-metrics/package-lock.json +8 -1
- package/examples/vue-alerts-metrics/package.json +4 -3
- package/examples/vue-alerts-metrics/src/components/AlertsList.vue +567 -16
- package/examples/vue-alerts-metrics/src/components/CameraSelector.vue +16 -6
- package/examples/vue-alerts-metrics/src/components/MetricsChart.vue +23 -9
- package/examples/vue-alerts-metrics/src/components/NotificationsList.vue +579 -17
- package/examples/vue-alerts-metrics/src/components/TimeRangeSelector.vue +197 -12
- package/examples/vue-alerts-metrics/src/composables/useHlsPlayer.ts +285 -0
- package/examples/vue-alerts-metrics/src/views/Dashboard.vue +31 -9
- package/examples/vue-alerts-metrics/src/views/Home.vue +56 -7
- package/examples/vue-event-subscriptions/.env.example +15 -0
- package/examples/vue-event-subscriptions/README.md +103 -0
- package/examples/vue-event-subscriptions/e2e/app.spec.ts +71 -0
- package/examples/vue-event-subscriptions/e2e/auth.spec.ts +290 -0
- package/examples/vue-event-subscriptions/index.html +13 -0
- package/examples/vue-event-subscriptions/package-lock.json +1719 -0
- package/examples/vue-event-subscriptions/package.json +28 -0
- package/examples/vue-event-subscriptions/playwright.config.ts +47 -0
- package/examples/vue-event-subscriptions/src/App.vue +233 -0
- package/examples/vue-event-subscriptions/src/main.ts +25 -0
- package/examples/vue-event-subscriptions/src/router/index.ts +68 -0
- package/examples/vue-event-subscriptions/src/views/Callback.vue +76 -0
- package/examples/vue-event-subscriptions/src/views/Home.vue +192 -0
- package/examples/vue-event-subscriptions/src/views/LiveEvents.vue +640 -0
- package/examples/vue-event-subscriptions/src/views/Login.vue +33 -0
- package/examples/vue-event-subscriptions/src/views/Logout.vue +59 -0
- package/examples/vue-event-subscriptions/src/views/Subscriptions.vue +402 -0
- package/examples/vue-event-subscriptions/src/vite-env.d.ts +12 -0
- package/examples/vue-event-subscriptions/tsconfig.json +21 -0
- package/examples/vue-event-subscriptions/tsconfig.node.json +10 -0
- package/examples/vue-event-subscriptions/vite.config.ts +12 -0
- 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">×</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>
|