een-api-toolkit 0.3.30 → 0.3.35
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/docs-accuracy-reviewer.md +15 -3
- package/.claude/agents/een-auth-agent.md +131 -0
- package/.claude/agents/een-devices-agent.md +10 -7
- package/.claude/agents/een-events-agent.md +98 -0
- package/.claude/agents/een-grouping-agent.md +394 -0
- package/.claude/agents/een-media-agent.md +25 -5
- package/CHANGELOG.md +101 -6
- package/README.md +5 -3
- package/dist/index.cjs +3 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +561 -0
- package/dist/index.js +388 -218
- package/dist/index.js.map +1 -1
- package/docs/AI-CONTEXT.md +13 -1
- package/docs/ai-reference/AI-AUTH.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 +411 -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-alerts-metrics/README.md +2 -0
- package/examples/vue-alerts-metrics/alert-metrics-screenshot.png +0 -0
- package/examples/vue-alerts-metrics/e2e/auth.spec.ts +1 -1
- package/examples/vue-alerts-metrics/package-lock.json +17 -14
- package/examples/vue-alerts-metrics/package.json +1 -1
- package/examples/vue-bridges/package-lock.json +21 -15
- package/examples/vue-bridges/package.json +1 -1
- package/examples/vue-cameras/package-lock.json +21 -15
- package/examples/vue-cameras/package.json +1 -1
- package/examples/vue-event-subscriptions/README.md +2 -0
- package/examples/vue-event-subscriptions/event-subscriptions-screenshot.png +0 -0
- package/examples/vue-event-subscriptions/package-lock.json +17 -14
- package/examples/vue-event-subscriptions/package.json +1 -1
- package/examples/vue-events/events-screenshot.png +0 -0
- package/examples/vue-events/package-lock.json +17 -14
- package/examples/vue-events/package.json +1 -1
- package/examples/vue-feeds/package-lock.json +21 -15
- package/examples/vue-feeds/package.json +1 -1
- package/examples/vue-layouts/.env.example +12 -0
- package/examples/vue-layouts/README.md +320 -0
- package/examples/vue-layouts/e2e/app.spec.ts +76 -0
- package/examples/vue-layouts/e2e/auth.spec.ts +264 -0
- package/examples/vue-layouts/index.html +13 -0
- package/examples/vue-layouts/layouts-screenshot.png +0 -0
- package/examples/vue-layouts/package-lock.json +1722 -0
- package/examples/vue-layouts/package.json +28 -0
- package/examples/vue-layouts/playwright.config.ts +47 -0
- package/examples/vue-layouts/src/App.vue +124 -0
- package/examples/vue-layouts/src/components/LayoutModal.vue +456 -0
- package/examples/vue-layouts/src/main.ts +25 -0
- package/examples/vue-layouts/src/router/index.ts +62 -0
- package/examples/vue-layouts/src/views/Callback.vue +76 -0
- package/examples/vue-layouts/src/views/Home.vue +188 -0
- package/examples/vue-layouts/src/views/Layouts.vue +355 -0
- package/examples/vue-layouts/src/views/Login.vue +33 -0
- package/examples/vue-layouts/src/views/Logout.vue +59 -0
- package/examples/vue-layouts/src/vite-env.d.ts +12 -0
- package/examples/vue-layouts/tsconfig.json +21 -0
- package/examples/vue-layouts/tsconfig.node.json +10 -0
- package/examples/vue-layouts/vite.config.ts +12 -0
- package/examples/vue-media/media-screenshot.png +0 -0
- package/examples/vue-media/package-lock.json +19 -14
- package/examples/vue-media/package.json +1 -1
- package/examples/vue-users/package-lock.json +21 -16
- package/examples/vue-users/package.json +2 -2
- package/package.json +2 -2
- package/scripts/setup-agents.ts +0 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { createRouter, createWebHistory } from 'vue-router'
|
|
2
|
+
import { useAuthStore } from 'een-api-toolkit'
|
|
3
|
+
import Home from '../views/Home.vue'
|
|
4
|
+
import Login from '../views/Login.vue'
|
|
5
|
+
import Callback from '../views/Callback.vue'
|
|
6
|
+
import Layouts from '../views/Layouts.vue'
|
|
7
|
+
import Logout from '../views/Logout.vue'
|
|
8
|
+
|
|
9
|
+
const router = createRouter({
|
|
10
|
+
history: createWebHistory(),
|
|
11
|
+
routes: [
|
|
12
|
+
{
|
|
13
|
+
path: '/',
|
|
14
|
+
name: 'home',
|
|
15
|
+
component: Home
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
path: '/login',
|
|
19
|
+
name: 'login',
|
|
20
|
+
component: Login
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
path: '/callback',
|
|
24
|
+
name: 'callback',
|
|
25
|
+
component: Callback
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
path: '/layouts',
|
|
29
|
+
name: 'layouts',
|
|
30
|
+
component: Layouts,
|
|
31
|
+
meta: { requiresAuth: true }
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
path: '/logout',
|
|
35
|
+
name: 'logout',
|
|
36
|
+
component: Logout
|
|
37
|
+
}
|
|
38
|
+
]
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
// Navigation guard for OAuth callback and protected routes
|
|
42
|
+
// IMPORTANT: OAuth callback check MUST come FIRST, before auth check
|
|
43
|
+
// This ensures the callback is processed even if the root route becomes protected
|
|
44
|
+
router.beforeEach((to, _from, next) => {
|
|
45
|
+
// Handle OAuth callback on root path (EEN IDP redirects to http://127.0.0.1:3333)
|
|
46
|
+
// Check for code and state params which indicate an OAuth callback
|
|
47
|
+
if (to.path === '/' && to.query.code && to.query.state) {
|
|
48
|
+
next({ name: 'callback', query: to.query })
|
|
49
|
+
return
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Check authentication for protected routes
|
|
53
|
+
const authStore = useAuthStore()
|
|
54
|
+
|
|
55
|
+
if (to.meta.requiresAuth && !authStore.isAuthenticated) {
|
|
56
|
+
next({ name: 'login' })
|
|
57
|
+
} else {
|
|
58
|
+
next()
|
|
59
|
+
}
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
export default router
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { onMounted, ref } from 'vue'
|
|
3
|
+
import { useRouter } from 'vue-router'
|
|
4
|
+
import { handleAuthCallback } from 'een-api-toolkit'
|
|
5
|
+
|
|
6
|
+
const router = useRouter()
|
|
7
|
+
const error = ref<string | null>(null)
|
|
8
|
+
const processing = ref(true)
|
|
9
|
+
|
|
10
|
+
onMounted(async () => {
|
|
11
|
+
const url = new URL(window.location.href)
|
|
12
|
+
const code = url.searchParams.get('code')
|
|
13
|
+
const state = url.searchParams.get('state')
|
|
14
|
+
const errorParam = url.searchParams.get('error')
|
|
15
|
+
|
|
16
|
+
if (errorParam) {
|
|
17
|
+
error.value = `OAuth error: ${errorParam}`
|
|
18
|
+
processing.value = false
|
|
19
|
+
return
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (!code || !state) {
|
|
23
|
+
error.value = 'Missing authorization code or state parameter'
|
|
24
|
+
processing.value = false
|
|
25
|
+
return
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const result = await handleAuthCallback(code, state)
|
|
29
|
+
|
|
30
|
+
if (result.error) {
|
|
31
|
+
error.value = result.error.message
|
|
32
|
+
processing.value = false
|
|
33
|
+
return
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Success - redirect to home
|
|
37
|
+
router.push('/')
|
|
38
|
+
})
|
|
39
|
+
</script>
|
|
40
|
+
|
|
41
|
+
<template>
|
|
42
|
+
<div class="callback">
|
|
43
|
+
<div v-if="processing" class="loading">
|
|
44
|
+
<h2>Authenticating...</h2>
|
|
45
|
+
<p>Please wait while we complete the login process.</p>
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
<div v-else-if="error" class="error-state">
|
|
49
|
+
<h2>Authentication Failed</h2>
|
|
50
|
+
<p class="error">{{ error }}</p>
|
|
51
|
+
<router-link to="/login">
|
|
52
|
+
<button>Try Again</button>
|
|
53
|
+
</router-link>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
</template>
|
|
57
|
+
|
|
58
|
+
<style scoped>
|
|
59
|
+
.callback {
|
|
60
|
+
text-align: center;
|
|
61
|
+
max-width: 400px;
|
|
62
|
+
margin: 0 auto;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
h2 {
|
|
66
|
+
margin-bottom: 20px;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.loading p {
|
|
70
|
+
color: #666;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.error-state .error {
|
|
74
|
+
margin-bottom: 20px;
|
|
75
|
+
}
|
|
76
|
+
</style>
|
|
@@ -0,0 +1,188 @@
|
|
|
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 Layouts 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="/layouts">
|
|
84
|
+
<button>View Layouts</button>
|
|
85
|
+
</router-link>
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
|
|
89
|
+
<div class="description">
|
|
90
|
+
<h3>About This Example</h3>
|
|
91
|
+
<p>
|
|
92
|
+
This example demonstrates how to use the <code>getLayouts</code>,
|
|
93
|
+
<code>getLayout</code>, <code>createLayout</code>, <code>updateLayout</code>,
|
|
94
|
+
and <code>deleteLayout</code> functions from the EEN API Toolkit to manage
|
|
95
|
+
camera layouts from the Eagle Eye Networks platform.
|
|
96
|
+
</p>
|
|
97
|
+
<h4>Features</h4>
|
|
98
|
+
<ul>
|
|
99
|
+
<li>List layouts with pagination</li>
|
|
100
|
+
<li>Create new layouts with settings</li>
|
|
101
|
+
<li>Edit layout name and settings</li>
|
|
102
|
+
<li>Add/remove camera panes</li>
|
|
103
|
+
<li>Delete layouts</li>
|
|
104
|
+
<li>OAuth authentication flow</li>
|
|
105
|
+
</ul>
|
|
106
|
+
<p class="storage-note" data-testid="storage-strategy">
|
|
107
|
+
Storage strategy: <strong>{{ storageStrategy }}</strong> ({{ storageDescription }})
|
|
108
|
+
</p>
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
</template>
|
|
112
|
+
|
|
113
|
+
<style scoped>
|
|
114
|
+
.home {
|
|
115
|
+
text-align: center;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
h2 {
|
|
119
|
+
margin-bottom: 30px;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.not-authenticated,
|
|
123
|
+
.authenticated {
|
|
124
|
+
margin-top: 20px;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.user-info {
|
|
128
|
+
background: #f5f5f5;
|
|
129
|
+
padding: 20px;
|
|
130
|
+
border-radius: 8px;
|
|
131
|
+
margin-bottom: 20px;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.user-info h3 {
|
|
135
|
+
margin-bottom: 10px;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.user-info p {
|
|
139
|
+
color: #666;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.actions {
|
|
143
|
+
margin-top: 20px;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.description {
|
|
147
|
+
margin-top: 40px;
|
|
148
|
+
padding: 20px;
|
|
149
|
+
background: #f8f9fa;
|
|
150
|
+
border-radius: 8px;
|
|
151
|
+
text-align: left;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.description h3 {
|
|
155
|
+
margin-bottom: 10px;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.description h4 {
|
|
159
|
+
margin-top: 15px;
|
|
160
|
+
margin-bottom: 10px;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.description p {
|
|
164
|
+
color: #666;
|
|
165
|
+
margin-bottom: 15px;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
.description ul {
|
|
169
|
+
list-style: disc;
|
|
170
|
+
padding-left: 20px;
|
|
171
|
+
color: #666;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
.description code {
|
|
175
|
+
background: #e9ecef;
|
|
176
|
+
padding: 2px 6px;
|
|
177
|
+
border-radius: 3px;
|
|
178
|
+
font-size: 0.9em;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
.storage-note {
|
|
182
|
+
margin-top: 20px;
|
|
183
|
+
padding-top: 15px;
|
|
184
|
+
border-top: 1px solid #ddd;
|
|
185
|
+
font-size: 0.85em;
|
|
186
|
+
color: #888;
|
|
187
|
+
}
|
|
188
|
+
</style>
|
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref, computed, onMounted } from 'vue'
|
|
3
|
+
import {
|
|
4
|
+
getLayouts,
|
|
5
|
+
getCameras,
|
|
6
|
+
createLayout,
|
|
7
|
+
updateLayout,
|
|
8
|
+
deleteLayout,
|
|
9
|
+
type Layout,
|
|
10
|
+
type Camera,
|
|
11
|
+
type EenError,
|
|
12
|
+
type ListLayoutsParams,
|
|
13
|
+
type CreateLayoutParams,
|
|
14
|
+
type UpdateLayoutParams,
|
|
15
|
+
type LayoutSettings
|
|
16
|
+
} from 'een-api-toolkit'
|
|
17
|
+
import LayoutModal from '../components/LayoutModal.vue'
|
|
18
|
+
|
|
19
|
+
// Reactive state
|
|
20
|
+
const layouts = ref<Layout[]>([])
|
|
21
|
+
const cameras = ref<Camera[]>([])
|
|
22
|
+
const loading = ref(false)
|
|
23
|
+
const error = ref<EenError | null>(null)
|
|
24
|
+
const nextPageToken = ref<string | undefined>(undefined)
|
|
25
|
+
|
|
26
|
+
const hasNextPage = computed(() => !!nextPageToken.value)
|
|
27
|
+
|
|
28
|
+
const params = ref<ListLayoutsParams>({
|
|
29
|
+
pageSize: 20,
|
|
30
|
+
include: ['resourceCounts', 'effectivePermissions']
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
// Modal state
|
|
34
|
+
const showModal = ref(false)
|
|
35
|
+
const selectedLayout = ref<Layout | null>(null)
|
|
36
|
+
const modalLoading = ref(false)
|
|
37
|
+
const modalError = ref<string | null>(null)
|
|
38
|
+
|
|
39
|
+
// Default settings for new layouts
|
|
40
|
+
const defaultSettings: LayoutSettings = {
|
|
41
|
+
showCameraBorder: true,
|
|
42
|
+
showCameraName: true,
|
|
43
|
+
cameraAspectRatio: '16x9',
|
|
44
|
+
paneColumns: 3
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function fetchLayouts(fetchParams?: ListLayoutsParams, append = false) {
|
|
48
|
+
loading.value = true
|
|
49
|
+
error.value = null
|
|
50
|
+
|
|
51
|
+
const mergedParams = { ...params.value, ...fetchParams }
|
|
52
|
+
const result = await getLayouts(mergedParams)
|
|
53
|
+
|
|
54
|
+
if (result.error) {
|
|
55
|
+
error.value = result.error
|
|
56
|
+
if (!append) {
|
|
57
|
+
layouts.value = []
|
|
58
|
+
}
|
|
59
|
+
nextPageToken.value = undefined
|
|
60
|
+
} else {
|
|
61
|
+
if (append) {
|
|
62
|
+
layouts.value = [...layouts.value, ...result.data.results]
|
|
63
|
+
} else {
|
|
64
|
+
layouts.value = result.data.results
|
|
65
|
+
}
|
|
66
|
+
nextPageToken.value = result.data.nextPageToken
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
loading.value = false
|
|
70
|
+
return result
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function fetchCameras() {
|
|
74
|
+
const result = await getCameras({ pageSize: 100, include: ['status'] })
|
|
75
|
+
if (result.data) {
|
|
76
|
+
cameras.value = result.data.results
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function refresh() {
|
|
81
|
+
return fetchLayouts()
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function fetchNextPage() {
|
|
85
|
+
if (!nextPageToken.value) return
|
|
86
|
+
// Destructure to explicitly exclude any existing pageToken from params
|
|
87
|
+
const { pageToken: _existingToken, ...restParams } = params.value
|
|
88
|
+
return fetchLayouts({ ...restParams, pageToken: nextPageToken.value }, true)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function openCreateModal() {
|
|
92
|
+
selectedLayout.value = null
|
|
93
|
+
modalError.value = null
|
|
94
|
+
showModal.value = true
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function openEditModal(layout: Layout) {
|
|
98
|
+
selectedLayout.value = layout
|
|
99
|
+
modalError.value = null
|
|
100
|
+
showModal.value = true
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function closeModal() {
|
|
104
|
+
showModal.value = false
|
|
105
|
+
selectedLayout.value = null
|
|
106
|
+
modalError.value = null
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function handleSave(data: { name: string; settings: LayoutSettings; panes: Layout['panes'] }) {
|
|
110
|
+
modalLoading.value = true
|
|
111
|
+
modalError.value = null
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
if (selectedLayout.value) {
|
|
115
|
+
// Update existing layout
|
|
116
|
+
const updateParams: UpdateLayoutParams = {
|
|
117
|
+
name: data.name,
|
|
118
|
+
settings: data.settings,
|
|
119
|
+
panes: data.panes
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const result = await updateLayout(selectedLayout.value.id, updateParams)
|
|
123
|
+
if (result.error) {
|
|
124
|
+
modalError.value = result.error.message
|
|
125
|
+
return
|
|
126
|
+
}
|
|
127
|
+
} else {
|
|
128
|
+
// Create new layout
|
|
129
|
+
const createParams: CreateLayoutParams = {
|
|
130
|
+
name: data.name,
|
|
131
|
+
settings: data.settings,
|
|
132
|
+
panes: data.panes
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const result = await createLayout(createParams)
|
|
136
|
+
if (result.error) {
|
|
137
|
+
modalError.value = result.error.message
|
|
138
|
+
return
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
closeModal()
|
|
143
|
+
await fetchLayouts()
|
|
144
|
+
} catch (err) {
|
|
145
|
+
// Handle unexpected errors (network failures, state mutations, etc.)
|
|
146
|
+
modalError.value = err instanceof Error ? err.message : 'An unexpected error occurred'
|
|
147
|
+
console.error('handleSave error:', err)
|
|
148
|
+
} finally {
|
|
149
|
+
modalLoading.value = false
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function handleDelete(layoutId: string) {
|
|
154
|
+
if (!confirm('Are you sure you want to delete this layout?')) {
|
|
155
|
+
return
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
modalLoading.value = true
|
|
159
|
+
modalError.value = null
|
|
160
|
+
|
|
161
|
+
const result = await deleteLayout(layoutId)
|
|
162
|
+
|
|
163
|
+
if (result.error) {
|
|
164
|
+
modalError.value = result.error.message
|
|
165
|
+
modalLoading.value = false
|
|
166
|
+
return
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
closeModal()
|
|
170
|
+
await fetchLayouts()
|
|
171
|
+
modalLoading.value = false
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
onMounted(async () => {
|
|
175
|
+
await Promise.all([fetchLayouts(), fetchCameras()])
|
|
176
|
+
})
|
|
177
|
+
</script>
|
|
178
|
+
|
|
179
|
+
<template>
|
|
180
|
+
<div class="layouts">
|
|
181
|
+
<div class="header">
|
|
182
|
+
<h2>Layouts</h2>
|
|
183
|
+
<div class="controls">
|
|
184
|
+
<button @click="openCreateModal">Create Layout</button>
|
|
185
|
+
<button class="secondary" @click="refresh" :disabled="loading">
|
|
186
|
+
{{ loading ? 'Loading...' : 'Refresh' }}
|
|
187
|
+
</button>
|
|
188
|
+
</div>
|
|
189
|
+
</div>
|
|
190
|
+
|
|
191
|
+
<div v-if="loading && layouts.length === 0" class="loading">
|
|
192
|
+
Loading layouts...
|
|
193
|
+
</div>
|
|
194
|
+
|
|
195
|
+
<div v-else-if="error" class="error">
|
|
196
|
+
Error: {{ error.message }}
|
|
197
|
+
</div>
|
|
198
|
+
|
|
199
|
+
<div v-else>
|
|
200
|
+
<div v-if="layouts.length > 0" class="layout-grid">
|
|
201
|
+
<div
|
|
202
|
+
v-for="layout in layouts"
|
|
203
|
+
:key="layout.id"
|
|
204
|
+
class="layout-card"
|
|
205
|
+
@click="openEditModal(layout)"
|
|
206
|
+
>
|
|
207
|
+
<div class="layout-header">
|
|
208
|
+
<h3>{{ layout.name }}</h3>
|
|
209
|
+
<span class="pane-count">{{ layout.panes?.length || 0 }} panes</span>
|
|
210
|
+
</div>
|
|
211
|
+
<div class="layout-details">
|
|
212
|
+
<p>
|
|
213
|
+
<strong>Columns:</strong> {{ layout.settings?.paneColumns || 'N/A' }}
|
|
214
|
+
</p>
|
|
215
|
+
<p>
|
|
216
|
+
<strong>Aspect Ratio:</strong> {{ layout.settings?.cameraAspectRatio || 'N/A' }}
|
|
217
|
+
</p>
|
|
218
|
+
<p v-if="layout.resourceCounts?.cameras !== undefined">
|
|
219
|
+
<strong>Cameras:</strong> {{ layout.resourceCounts.cameras }}
|
|
220
|
+
</p>
|
|
221
|
+
<div v-if="layout.effectivePermissions" class="permissions">
|
|
222
|
+
<span v-if="layout.effectivePermissions.edit" class="badge edit">Can Edit</span>
|
|
223
|
+
<span v-if="layout.effectivePermissions.delete" class="badge delete">Can Delete</span>
|
|
224
|
+
</div>
|
|
225
|
+
</div>
|
|
226
|
+
</div>
|
|
227
|
+
</div>
|
|
228
|
+
|
|
229
|
+
<p v-else class="no-layouts">
|
|
230
|
+
No layouts found. Click "Create Layout" to add one.
|
|
231
|
+
</p>
|
|
232
|
+
|
|
233
|
+
<div v-if="hasNextPage" class="pagination">
|
|
234
|
+
<button @click="fetchNextPage" :disabled="loading">
|
|
235
|
+
{{ loading ? 'Loading...' : 'Load More' }}
|
|
236
|
+
</button>
|
|
237
|
+
</div>
|
|
238
|
+
</div>
|
|
239
|
+
|
|
240
|
+
<!-- Layout Modal -->
|
|
241
|
+
<LayoutModal
|
|
242
|
+
v-if="showModal"
|
|
243
|
+
:layout="selectedLayout"
|
|
244
|
+
:cameras="cameras"
|
|
245
|
+
:loading="modalLoading"
|
|
246
|
+
:error="modalError"
|
|
247
|
+
:default-settings="defaultSettings"
|
|
248
|
+
@save="handleSave"
|
|
249
|
+
@delete="handleDelete"
|
|
250
|
+
@close="closeModal"
|
|
251
|
+
/>
|
|
252
|
+
</div>
|
|
253
|
+
</template>
|
|
254
|
+
|
|
255
|
+
<style scoped>
|
|
256
|
+
.layouts {
|
|
257
|
+
max-width: 1000px;
|
|
258
|
+
margin: 0 auto;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
.header {
|
|
262
|
+
display: flex;
|
|
263
|
+
justify-content: space-between;
|
|
264
|
+
align-items: center;
|
|
265
|
+
margin-bottom: 20px;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
.controls {
|
|
269
|
+
display: flex;
|
|
270
|
+
gap: 10px;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
.layout-grid {
|
|
274
|
+
display: grid;
|
|
275
|
+
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
|
276
|
+
gap: 20px;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
.layout-card {
|
|
280
|
+
background: #fff;
|
|
281
|
+
border: 1px solid #e0e0e0;
|
|
282
|
+
border-radius: 8px;
|
|
283
|
+
padding: 16px;
|
|
284
|
+
cursor: pointer;
|
|
285
|
+
transition: box-shadow 0.2s, transform 0.2s;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
.layout-card:hover {
|
|
289
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
|
290
|
+
transform: translateY(-2px);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
.layout-header {
|
|
294
|
+
display: flex;
|
|
295
|
+
justify-content: space-between;
|
|
296
|
+
align-items: flex-start;
|
|
297
|
+
margin-bottom: 12px;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
.layout-header h3 {
|
|
301
|
+
margin: 0;
|
|
302
|
+
font-size: 1.1rem;
|
|
303
|
+
color: #333;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
.pane-count {
|
|
307
|
+
background: #e9ecef;
|
|
308
|
+
padding: 4px 8px;
|
|
309
|
+
border-radius: 4px;
|
|
310
|
+
font-size: 0.85rem;
|
|
311
|
+
color: #666;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
.layout-details p {
|
|
315
|
+
margin: 4px 0;
|
|
316
|
+
font-size: 0.9rem;
|
|
317
|
+
color: #666;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
.permissions {
|
|
321
|
+
margin-top: 8px;
|
|
322
|
+
display: flex;
|
|
323
|
+
gap: 6px;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
.badge {
|
|
327
|
+
padding: 2px 8px;
|
|
328
|
+
border-radius: 4px;
|
|
329
|
+
font-size: 0.75rem;
|
|
330
|
+
font-weight: 500;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
.badge.edit {
|
|
334
|
+
background: #d4edda;
|
|
335
|
+
color: #155724;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
.badge.delete {
|
|
339
|
+
background: #f8d7da;
|
|
340
|
+
color: #721c24;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
.no-layouts {
|
|
344
|
+
text-align: center;
|
|
345
|
+
color: #666;
|
|
346
|
+
padding: 40px 20px;
|
|
347
|
+
background: #f8f9fa;
|
|
348
|
+
border-radius: 8px;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
.pagination {
|
|
352
|
+
margin-top: 20px;
|
|
353
|
+
text-align: center;
|
|
354
|
+
}
|
|
355
|
+
</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>
|