een-api-toolkit 0.3.47 → 0.3.49
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/.claude/agents/een-jobs-agent.md +676 -0
- package/CHANGELOG.md +7 -8
- package/dist/index.cjs +3 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +1172 -28
- package/dist/index.js +796 -333
- package/dist/index.js.map +1 -1
- package/docs/AI-CONTEXT.md +22 -1
- package/docs/ai-reference/AI-AUTH.md +1 -1
- package/docs/ai-reference/AI-AUTOMATIONS.md +1 -1
- package/docs/ai-reference/AI-DEVICES.md +1 -1
- package/docs/ai-reference/AI-EVENTS.md +1 -1
- package/docs/ai-reference/AI-GROUPING.md +1 -1
- package/docs/ai-reference/AI-JOBS.md +1084 -0
- package/docs/ai-reference/AI-MEDIA.md +1 -1
- package/docs/ai-reference/AI-SETUP.md +1 -1
- package/docs/ai-reference/AI-USERS.md +1 -1
- package/examples/vue-jobs/.env.example +11 -0
- package/examples/vue-jobs/README.md +245 -0
- package/examples/vue-jobs/e2e/app.spec.ts +79 -0
- package/examples/vue-jobs/e2e/auth.spec.ts +382 -0
- package/examples/vue-jobs/e2e/delete-features.spec.ts +564 -0
- package/examples/vue-jobs/e2e/timelapse.spec.ts +361 -0
- package/examples/vue-jobs/index.html +13 -0
- package/examples/vue-jobs/package-lock.json +1722 -0
- package/examples/vue-jobs/package.json +28 -0
- package/examples/vue-jobs/playwright.config.ts +47 -0
- package/examples/vue-jobs/src/App.vue +154 -0
- package/examples/vue-jobs/src/main.ts +25 -0
- package/examples/vue-jobs/src/router/index.ts +82 -0
- package/examples/vue-jobs/src/views/Callback.vue +76 -0
- package/examples/vue-jobs/src/views/CreateExport.vue +284 -0
- package/examples/vue-jobs/src/views/Files.vue +424 -0
- package/examples/vue-jobs/src/views/Home.vue +195 -0
- package/examples/vue-jobs/src/views/JobDetail.vue +392 -0
- package/examples/vue-jobs/src/views/Jobs.vue +297 -0
- package/examples/vue-jobs/src/views/Login.vue +33 -0
- package/examples/vue-jobs/src/views/Logout.vue +59 -0
- package/examples/vue-jobs/src/vite-env.d.ts +1 -0
- package/examples/vue-jobs/tsconfig.json +25 -0
- package/examples/vue-jobs/vite.config.ts +12 -0
- package/package.json +1 -1
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref, computed, onMounted } from 'vue'
|
|
3
|
+
import { listFiles, downloadFile, deleteFile, type EenFile, type EenError, type ListFilesParams, type FileIncludeField } from 'een-api-toolkit'
|
|
4
|
+
|
|
5
|
+
// Reactive state
|
|
6
|
+
const files = ref<EenFile[]>([])
|
|
7
|
+
const loading = ref(false)
|
|
8
|
+
const error = ref<EenError | null>(null)
|
|
9
|
+
const nextPageToken = ref<string | undefined>(undefined)
|
|
10
|
+
const downloadingId = ref<string | null>(null)
|
|
11
|
+
const deletingId = ref<string | null>(null)
|
|
12
|
+
const selectedFile = ref<EenFile | null>(null)
|
|
13
|
+
|
|
14
|
+
const hasNextPage = computed(() => !!nextPageToken.value)
|
|
15
|
+
|
|
16
|
+
// Request size and createTimestamp by default for better display
|
|
17
|
+
const defaultInclude: FileIncludeField[] = ['size', 'createTimestamp']
|
|
18
|
+
|
|
19
|
+
const params = ref<ListFilesParams>({
|
|
20
|
+
pageSize: 20,
|
|
21
|
+
include: defaultInclude,
|
|
22
|
+
sort: ['-createTimestamp']
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
async function fetchFiles(fetchParams?: ListFilesParams, append = false) {
|
|
26
|
+
loading.value = true
|
|
27
|
+
error.value = null
|
|
28
|
+
|
|
29
|
+
const mergedParams = { ...params.value, ...fetchParams }
|
|
30
|
+
const result = await listFiles(mergedParams)
|
|
31
|
+
|
|
32
|
+
if (result.error) {
|
|
33
|
+
error.value = result.error
|
|
34
|
+
if (!append) {
|
|
35
|
+
files.value = []
|
|
36
|
+
}
|
|
37
|
+
nextPageToken.value = undefined
|
|
38
|
+
} else {
|
|
39
|
+
if (append) {
|
|
40
|
+
files.value = [...files.value, ...result.data.results]
|
|
41
|
+
} else {
|
|
42
|
+
files.value = result.data.results
|
|
43
|
+
}
|
|
44
|
+
nextPageToken.value = result.data.nextPageToken
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
loading.value = false
|
|
48
|
+
return result
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function refresh() {
|
|
52
|
+
return fetchFiles()
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function fetchNextPage() {
|
|
56
|
+
if (!nextPageToken.value) return
|
|
57
|
+
return fetchFiles({ ...params.value, pageToken: nextPageToken.value }, true)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function handleDownload(file: EenFile) {
|
|
61
|
+
downloadingId.value = file.id
|
|
62
|
+
const result = await downloadFile(file.id)
|
|
63
|
+
|
|
64
|
+
if (result.error) {
|
|
65
|
+
error.value = result.error
|
|
66
|
+
} else {
|
|
67
|
+
// Create download link
|
|
68
|
+
const url = URL.createObjectURL(result.data.blob)
|
|
69
|
+
const a = document.createElement('a')
|
|
70
|
+
a.href = url
|
|
71
|
+
a.download = result.data.filename || file.name
|
|
72
|
+
a.click()
|
|
73
|
+
URL.revokeObjectURL(url)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
downloadingId.value = null
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function handleDelete(file: EenFile) {
|
|
80
|
+
if (!confirm(`Are you sure you want to delete "${file.name}"? This will move it to the recycle bin.`)) {
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
deletingId.value = file.id
|
|
85
|
+
const result = await deleteFile(file.id)
|
|
86
|
+
|
|
87
|
+
if (result.error) {
|
|
88
|
+
error.value = result.error
|
|
89
|
+
} else {
|
|
90
|
+
// Remove the file from the list
|
|
91
|
+
files.value = files.value.filter(f => f.id !== file.id)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
deletingId.value = null
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function formatDate(timestamp: string | undefined) {
|
|
98
|
+
if (!timestamp) return '-'
|
|
99
|
+
return new Date(timestamp).toLocaleString()
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function getFileType(file: EenFile) {
|
|
103
|
+
// Use type if available, otherwise derive from mimeType
|
|
104
|
+
if (file.type) return file.type
|
|
105
|
+
if (file.mimeType) {
|
|
106
|
+
if (file.mimeType.startsWith('video/')) return 'video'
|
|
107
|
+
if (file.mimeType.startsWith('image/')) return 'image'
|
|
108
|
+
if (file.mimeType === 'application/directory') return 'folder'
|
|
109
|
+
return file.mimeType.split('/')[1] || file.mimeType
|
|
110
|
+
}
|
|
111
|
+
return '-'
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function formatSize(bytes: number | undefined) {
|
|
115
|
+
if (bytes === undefined || bytes === null) return '-'
|
|
116
|
+
if (bytes === 0) return '0 B'
|
|
117
|
+
if (bytes < 1024) return `${bytes} B`
|
|
118
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
|
|
119
|
+
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
|
|
120
|
+
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function openFileDetails(file: EenFile) {
|
|
124
|
+
selectedFile.value = file
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function closeModal() {
|
|
128
|
+
selectedFile.value = null
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
onMounted(() => {
|
|
132
|
+
fetchFiles()
|
|
133
|
+
})
|
|
134
|
+
</script>
|
|
135
|
+
|
|
136
|
+
<template>
|
|
137
|
+
<div class="files">
|
|
138
|
+
<div class="header">
|
|
139
|
+
<h2>Files</h2>
|
|
140
|
+
<button @click="refresh" :disabled="loading">
|
|
141
|
+
{{ loading ? 'Loading...' : 'Refresh' }}
|
|
142
|
+
</button>
|
|
143
|
+
</div>
|
|
144
|
+
|
|
145
|
+
<div v-if="loading && files.length === 0" class="loading">
|
|
146
|
+
Loading files...
|
|
147
|
+
</div>
|
|
148
|
+
|
|
149
|
+
<div v-else-if="error" class="error">
|
|
150
|
+
Error: {{ error.message }}
|
|
151
|
+
</div>
|
|
152
|
+
|
|
153
|
+
<div v-else>
|
|
154
|
+
<table v-if="files.length > 0">
|
|
155
|
+
<thead>
|
|
156
|
+
<tr>
|
|
157
|
+
<th>Name</th>
|
|
158
|
+
<th>Type</th>
|
|
159
|
+
<th>Size</th>
|
|
160
|
+
<th>Created</th>
|
|
161
|
+
<th>Actions</th>
|
|
162
|
+
</tr>
|
|
163
|
+
</thead>
|
|
164
|
+
<tbody>
|
|
165
|
+
<tr v-for="file in files" :key="file.id">
|
|
166
|
+
<td><a href="#" class="file-link" @click.prevent="openFileDetails(file)">{{ file.name }}</a></td>
|
|
167
|
+
<td>{{ getFileType(file) }}</td>
|
|
168
|
+
<td>{{ formatSize(file.size) }}</td>
|
|
169
|
+
<td>{{ formatDate(file.createTimestamp) }}</td>
|
|
170
|
+
<td class="actions">
|
|
171
|
+
<button
|
|
172
|
+
class="btn-small"
|
|
173
|
+
@click="handleDownload(file)"
|
|
174
|
+
:disabled="downloadingId === file.id || deletingId === file.id"
|
|
175
|
+
>
|
|
176
|
+
{{ downloadingId === file.id ? 'Downloading...' : 'Download' }}
|
|
177
|
+
</button>
|
|
178
|
+
<button
|
|
179
|
+
class="btn-small btn-danger"
|
|
180
|
+
@click="handleDelete(file)"
|
|
181
|
+
:disabled="downloadingId === file.id || deletingId === file.id"
|
|
182
|
+
>
|
|
183
|
+
{{ deletingId === file.id ? 'Deleting...' : 'Delete' }}
|
|
184
|
+
</button>
|
|
185
|
+
</td>
|
|
186
|
+
</tr>
|
|
187
|
+
</tbody>
|
|
188
|
+
</table>
|
|
189
|
+
|
|
190
|
+
<p v-else>No files found.</p>
|
|
191
|
+
|
|
192
|
+
<div v-if="hasNextPage" class="pagination">
|
|
193
|
+
<button @click="fetchNextPage" :disabled="loading">
|
|
194
|
+
{{ loading ? 'Loading...' : 'Load More' }}
|
|
195
|
+
</button>
|
|
196
|
+
</div>
|
|
197
|
+
</div>
|
|
198
|
+
|
|
199
|
+
<!-- File Details Modal -->
|
|
200
|
+
<div v-if="selectedFile" class="modal-overlay" @click.self="closeModal">
|
|
201
|
+
<div class="modal">
|
|
202
|
+
<div class="modal-header">
|
|
203
|
+
<h3>File Details</h3>
|
|
204
|
+
<button class="modal-close" @click="closeModal">×</button>
|
|
205
|
+
</div>
|
|
206
|
+
<div class="modal-body">
|
|
207
|
+
<div class="detail-row">
|
|
208
|
+
<span class="detail-label">Name:</span>
|
|
209
|
+
<span class="detail-value">{{ selectedFile.name }}</span>
|
|
210
|
+
</div>
|
|
211
|
+
<div class="detail-row">
|
|
212
|
+
<span class="detail-label">ID:</span>
|
|
213
|
+
<span class="detail-value">{{ selectedFile.id }}</span>
|
|
214
|
+
</div>
|
|
215
|
+
<div class="detail-row">
|
|
216
|
+
<span class="detail-label">Type:</span>
|
|
217
|
+
<span class="detail-value">{{ getFileType(selectedFile) }}</span>
|
|
218
|
+
</div>
|
|
219
|
+
<div v-if="selectedFile.mimeType" class="detail-row">
|
|
220
|
+
<span class="detail-label">MIME Type:</span>
|
|
221
|
+
<span class="detail-value">{{ selectedFile.mimeType }}</span>
|
|
222
|
+
</div>
|
|
223
|
+
<div class="detail-row">
|
|
224
|
+
<span class="detail-label">Size:</span>
|
|
225
|
+
<span class="detail-value">{{ formatSize(selectedFile.size) }}</span>
|
|
226
|
+
</div>
|
|
227
|
+
<div v-if="selectedFile.directory" class="detail-row">
|
|
228
|
+
<span class="detail-label">Directory:</span>
|
|
229
|
+
<span class="detail-value">{{ selectedFile.directory }}</span>
|
|
230
|
+
</div>
|
|
231
|
+
<div class="detail-row">
|
|
232
|
+
<span class="detail-label">Created:</span>
|
|
233
|
+
<span class="detail-value">{{ formatDate(selectedFile.createTimestamp) }}</span>
|
|
234
|
+
</div>
|
|
235
|
+
<div v-if="selectedFile.updateTimestamp" class="detail-row">
|
|
236
|
+
<span class="detail-label">Updated:</span>
|
|
237
|
+
<span class="detail-value">{{ formatDate(selectedFile.updateTimestamp) }}</span>
|
|
238
|
+
</div>
|
|
239
|
+
<div v-if="selectedFile.accountId" class="detail-row">
|
|
240
|
+
<span class="detail-label">Account ID:</span>
|
|
241
|
+
<span class="detail-value">{{ selectedFile.accountId }}</span>
|
|
242
|
+
</div>
|
|
243
|
+
<div v-if="selectedFile.tags && selectedFile.tags.length > 0" class="detail-row">
|
|
244
|
+
<span class="detail-label">Tags:</span>
|
|
245
|
+
<span class="detail-value">{{ selectedFile.tags.join(', ') }}</span>
|
|
246
|
+
</div>
|
|
247
|
+
<div v-if="selectedFile.notes" class="detail-row">
|
|
248
|
+
<span class="detail-label">Notes:</span>
|
|
249
|
+
<span class="detail-value">{{ selectedFile.notes }}</span>
|
|
250
|
+
</div>
|
|
251
|
+
</div>
|
|
252
|
+
<div class="modal-footer">
|
|
253
|
+
<button @click="handleDownload(selectedFile)" :disabled="downloadingId === selectedFile.id">
|
|
254
|
+
{{ downloadingId === selectedFile.id ? 'Downloading...' : 'Download' }}
|
|
255
|
+
</button>
|
|
256
|
+
<button class="btn-secondary" @click="closeModal">Close</button>
|
|
257
|
+
</div>
|
|
258
|
+
</div>
|
|
259
|
+
</div>
|
|
260
|
+
</div>
|
|
261
|
+
</template>
|
|
262
|
+
|
|
263
|
+
<style scoped>
|
|
264
|
+
.files {
|
|
265
|
+
width: 80vw;
|
|
266
|
+
min-width: 900px;
|
|
267
|
+
margin: 0 auto;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
.header {
|
|
271
|
+
display: flex;
|
|
272
|
+
justify-content: space-between;
|
|
273
|
+
align-items: center;
|
|
274
|
+
margin-bottom: 20px;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
table {
|
|
278
|
+
width: 100%;
|
|
279
|
+
border-collapse: collapse;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
th,
|
|
283
|
+
td {
|
|
284
|
+
padding: 12px;
|
|
285
|
+
text-align: left;
|
|
286
|
+
border-bottom: 1px solid #eee;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
th {
|
|
290
|
+
background: #f5f5f5;
|
|
291
|
+
font-weight: 600;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
.actions {
|
|
295
|
+
display: flex;
|
|
296
|
+
gap: 8px;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
.btn-small {
|
|
300
|
+
padding: 5px 10px;
|
|
301
|
+
font-size: 0.85rem;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
.btn-danger {
|
|
305
|
+
background-color: #e74c3c;
|
|
306
|
+
color: white;
|
|
307
|
+
border: none;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
.btn-danger:hover:not(:disabled) {
|
|
311
|
+
background-color: #c0392b;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
.btn-danger:disabled {
|
|
315
|
+
background-color: #f5b7b1;
|
|
316
|
+
cursor: not-allowed;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
.pagination {
|
|
320
|
+
margin-top: 20px;
|
|
321
|
+
text-align: center;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
.file-link {
|
|
325
|
+
color: #42b883;
|
|
326
|
+
text-decoration: none;
|
|
327
|
+
cursor: pointer;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
.file-link:hover {
|
|
331
|
+
text-decoration: underline;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/* Modal styles */
|
|
335
|
+
.modal-overlay {
|
|
336
|
+
position: fixed;
|
|
337
|
+
top: 0;
|
|
338
|
+
left: 0;
|
|
339
|
+
right: 0;
|
|
340
|
+
bottom: 0;
|
|
341
|
+
background: rgba(0, 0, 0, 0.5);
|
|
342
|
+
display: flex;
|
|
343
|
+
align-items: center;
|
|
344
|
+
justify-content: center;
|
|
345
|
+
z-index: 1000;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
.modal {
|
|
349
|
+
background: white;
|
|
350
|
+
border-radius: 8px;
|
|
351
|
+
width: 70vw;
|
|
352
|
+
max-height: 80vh;
|
|
353
|
+
overflow-y: auto;
|
|
354
|
+
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
.modal-header {
|
|
358
|
+
display: flex;
|
|
359
|
+
justify-content: space-between;
|
|
360
|
+
align-items: center;
|
|
361
|
+
padding: 15px 20px;
|
|
362
|
+
border-bottom: 1px solid #eee;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
.modal-header h3 {
|
|
366
|
+
margin: 0;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
.modal-close {
|
|
370
|
+
background: none;
|
|
371
|
+
border: none;
|
|
372
|
+
font-size: 24px;
|
|
373
|
+
cursor: pointer;
|
|
374
|
+
color: #666;
|
|
375
|
+
padding: 0;
|
|
376
|
+
line-height: 1;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
.modal-close:hover {
|
|
380
|
+
color: #333;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
.modal-body {
|
|
384
|
+
padding: 20px;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
.detail-row {
|
|
388
|
+
display: flex;
|
|
389
|
+
margin-bottom: 12px;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
.detail-row:last-child {
|
|
393
|
+
margin-bottom: 0;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
.detail-label {
|
|
397
|
+
font-weight: 600;
|
|
398
|
+
width: 30%;
|
|
399
|
+
flex-shrink: 0;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
.detail-value {
|
|
403
|
+
color: #666;
|
|
404
|
+
word-break: break-all;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
.modal-footer {
|
|
408
|
+
display: flex;
|
|
409
|
+
justify-content: flex-end;
|
|
410
|
+
gap: 10px;
|
|
411
|
+
padding: 15px 20px;
|
|
412
|
+
border-top: 1px solid #eee;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
.btn-secondary {
|
|
416
|
+
background: #6c757d;
|
|
417
|
+
color: white;
|
|
418
|
+
border: none;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
.btn-secondary:hover {
|
|
422
|
+
background: #5a6268;
|
|
423
|
+
}
|
|
424
|
+
</style>
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { useAuthStore, getCurrentUser, getAuthUrl, getStorageStrategy, STORAGE_STRATEGY_DESCRIPTIONS, type UserProfile, type EenError } from 'een-api-toolkit'
|
|
3
|
+
import { computed, ref, watch } from 'vue'
|
|
4
|
+
|
|
5
|
+
const authStore = useAuthStore()
|
|
6
|
+
const isAuthenticated = computed(() => authStore.isAuthenticated)
|
|
7
|
+
const loginError = ref<string | null>(null)
|
|
8
|
+
|
|
9
|
+
const storageStrategy = getStorageStrategy()
|
|
10
|
+
const storageDescription = STORAGE_STRATEGY_DESCRIPTIONS[storageStrategy]
|
|
11
|
+
|
|
12
|
+
function login() {
|
|
13
|
+
try {
|
|
14
|
+
loginError.value = null
|
|
15
|
+
const authUrl = getAuthUrl()
|
|
16
|
+
window.location.href = authUrl
|
|
17
|
+
} catch (err) {
|
|
18
|
+
loginError.value = err instanceof Error ? err.message : 'Failed to initiate login'
|
|
19
|
+
console.error('Login error:', err)
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Reactive state for current user
|
|
24
|
+
const user = ref<UserProfile | null>(null)
|
|
25
|
+
const loading = ref(false)
|
|
26
|
+
const error = ref<EenError | null>(null)
|
|
27
|
+
|
|
28
|
+
// Guard to prevent concurrent fetch calls
|
|
29
|
+
let fetchInProgress = false
|
|
30
|
+
|
|
31
|
+
async function fetchUser() {
|
|
32
|
+
if (fetchInProgress) return
|
|
33
|
+
fetchInProgress = true
|
|
34
|
+
loading.value = true
|
|
35
|
+
error.value = null
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
const result = await getCurrentUser()
|
|
39
|
+
if (result.error) {
|
|
40
|
+
error.value = result.error
|
|
41
|
+
user.value = null
|
|
42
|
+
} else {
|
|
43
|
+
user.value = result.data
|
|
44
|
+
}
|
|
45
|
+
} finally {
|
|
46
|
+
loading.value = false
|
|
47
|
+
fetchInProgress = false
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Fetch user when authentication state changes
|
|
52
|
+
watch(
|
|
53
|
+
isAuthenticated,
|
|
54
|
+
async (isAuth) => {
|
|
55
|
+
if (isAuth && !user.value && !fetchInProgress) {
|
|
56
|
+
await fetchUser()
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
{ immediate: true }
|
|
60
|
+
)
|
|
61
|
+
</script>
|
|
62
|
+
|
|
63
|
+
<template>
|
|
64
|
+
<div class="home">
|
|
65
|
+
<h2>Welcome to the EEN Jobs Example</h2>
|
|
66
|
+
|
|
67
|
+
<div v-if="!isAuthenticated" class="not-authenticated" data-testid="not-authenticated">
|
|
68
|
+
<p data-testid="not-authenticated-message">You are not logged in.</p>
|
|
69
|
+
<p v-if="loginError" class="error" data-testid="login-error">{{ loginError }}</p>
|
|
70
|
+
<button data-testid="login-button" @click="login">Login with Eagle Eye Networks</button>
|
|
71
|
+
</div>
|
|
72
|
+
|
|
73
|
+
<div v-else class="authenticated">
|
|
74
|
+
<div v-if="loading" class="loading">Loading user profile...</div>
|
|
75
|
+
<div v-else-if="error" class="error">Error: {{ error.message }}</div>
|
|
76
|
+
<div v-else-if="user" class="user-info">
|
|
77
|
+
<h3>Hello, {{ user.firstName }} {{ user.lastName }}!</h3>
|
|
78
|
+
<p>Email: {{ user.email }}</p>
|
|
79
|
+
<p>Account ID: {{ user.accountId }}</p>
|
|
80
|
+
</div>
|
|
81
|
+
|
|
82
|
+
<div class="actions">
|
|
83
|
+
<router-link to="/jobs">
|
|
84
|
+
<button>View Jobs</button>
|
|
85
|
+
</router-link>
|
|
86
|
+
<router-link to="/files">
|
|
87
|
+
<button>View Files</button>
|
|
88
|
+
</router-link>
|
|
89
|
+
<router-link to="/create-export">
|
|
90
|
+
<button>Create Export</button>
|
|
91
|
+
</router-link>
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
|
|
95
|
+
<div class="description">
|
|
96
|
+
<h3>About This Example</h3>
|
|
97
|
+
<p>
|
|
98
|
+
This example demonstrates how to use the Jobs, Files, and Exports APIs from
|
|
99
|
+
the EEN API Toolkit to manage video exports and downloadable files from the
|
|
100
|
+
Eagle Eye Networks platform.
|
|
101
|
+
</p>
|
|
102
|
+
<h4>Features</h4>
|
|
103
|
+
<ul>
|
|
104
|
+
<li>List jobs with state filtering</li>
|
|
105
|
+
<li>View job progress and poll for completion</li>
|
|
106
|
+
<li>Create video exports from cameras</li>
|
|
107
|
+
<li>List and download files</li>
|
|
108
|
+
<li>OAuth authentication flow</li>
|
|
109
|
+
</ul>
|
|
110
|
+
<p class="storage-note" data-testid="storage-strategy">
|
|
111
|
+
Storage strategy: <strong>{{ storageStrategy }}</strong> ({{ storageDescription }})
|
|
112
|
+
</p>
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
</template>
|
|
116
|
+
|
|
117
|
+
<style scoped>
|
|
118
|
+
.home {
|
|
119
|
+
text-align: center;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
h2 {
|
|
123
|
+
margin-bottom: 30px;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.not-authenticated,
|
|
127
|
+
.authenticated {
|
|
128
|
+
margin-top: 20px;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.user-info {
|
|
132
|
+
background: #f5f5f5;
|
|
133
|
+
padding: 20px;
|
|
134
|
+
border-radius: 8px;
|
|
135
|
+
margin-bottom: 20px;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.user-info h3 {
|
|
139
|
+
margin-bottom: 10px;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.user-info p {
|
|
143
|
+
color: #666;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.actions {
|
|
147
|
+
margin-top: 20px;
|
|
148
|
+
display: flex;
|
|
149
|
+
gap: 10px;
|
|
150
|
+
justify-content: center;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.description {
|
|
154
|
+
margin-top: 40px;
|
|
155
|
+
padding: 20px;
|
|
156
|
+
background: #f8f9fa;
|
|
157
|
+
border-radius: 8px;
|
|
158
|
+
text-align: left;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
.description h3 {
|
|
162
|
+
margin-bottom: 10px;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
.description h4 {
|
|
166
|
+
margin-top: 15px;
|
|
167
|
+
margin-bottom: 10px;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
.description p {
|
|
171
|
+
color: #666;
|
|
172
|
+
margin-bottom: 15px;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
.description ul {
|
|
176
|
+
list-style: disc;
|
|
177
|
+
padding-left: 20px;
|
|
178
|
+
color: #666;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
.description code {
|
|
182
|
+
background: #e9ecef;
|
|
183
|
+
padding: 2px 6px;
|
|
184
|
+
border-radius: 3px;
|
|
185
|
+
font-size: 0.9em;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
.storage-note {
|
|
189
|
+
margin-top: 20px;
|
|
190
|
+
padding-top: 15px;
|
|
191
|
+
border-top: 1px solid #ddd;
|
|
192
|
+
font-size: 0.85em;
|
|
193
|
+
color: #888;
|
|
194
|
+
}
|
|
195
|
+
</style>
|