Files
schemas-app/app/pages/index.vue
T

364 lines
10 KiB
Vue
Raw Normal View History

<template>
<div>
<v-row>
<v-col cols="12">
<h1 class="text-h3 mb-4">Dashboard</h1>
</v-col>
</v-row>
<v-row v-if="!auth0?.isAuthenticated?.value">
<v-col cols="12">
<v-alert type="info" variant="tonal">
Please log in to view your schema registry dashboard.
</v-alert>
</v-col>
</v-row>
<v-row v-else-if="organizations.value?.loading?.value">
<v-col cols="12" class="text-center">
<v-progress-circular indeterminate size="64" />
<p class="mt-4">Loading your organizations...</p>
</v-col>
</v-row>
<v-row v-else-if="organizations.value?.error?.value">
<v-col cols="12">
<v-alert type="error" variant="tonal">
Error loading data: {{ organizations.value?.error?.value?.message }}
</v-alert>
</v-col>
</v-row>
<v-row v-else>
<v-col cols="12" md="4">
<v-card>
<v-card-title>
<v-icon icon="mdi-graphql" class="mr-2" />
Total Schemas
</v-card-title>
<v-card-text>
<div class="text-h2">{{ stats.totalSchemas }}</div>
</v-card-text>
</v-card>
</v-col>
<v-col cols="12" md="4">
<v-card>
<v-card-title>
<v-icon icon="mdi-source-branch" class="mr-2" />
Refs
</v-card-title>
<v-card-text>
<div class="text-h2">{{ stats.totalRefs }}</div>
</v-card-text>
</v-card>
</v-col>
<v-col cols="12" md="4">
<v-card>
<v-card-title>
<v-icon icon="mdi-update" class="mr-2" />
Last Updated
</v-card-title>
<v-card-text>
<div class="text-subtitle-1">{{ stats.lastUpdate }}</div>
</v-card-text>
</v-card>
</v-col>
</v-row>
<v-row v-if="auth0?.isAuthenticated?.value && !organizations.value?.loading?.value && !organizations.value?.error?.value" class="mt-4">
<v-col cols="12">
<v-card>
<v-card-title class="d-flex justify-space-between align-center">
<span>Organizations</span>
<v-btn
v-if="isAdmin && !getOrganizationsData()?.length"
color="primary"
prepend-icon="mdi-plus"
@click="createOrgDialog = true"
>
Create Organization
</v-btn>
</v-card-title>
<v-card-text>
<v-list v-if="getOrganizationsData()?.length">
<v-list-item
v-for="org in getOrganizationsData()"
:key="org.id"
:title="org.name"
:subtitle="`${org.apiKeys?.length || 0} API Keys`"
prepend-icon="mdi-domain"
>
<template #append>
<v-chip size="small" class="mr-2">
{{ org.users?.length || 0 }} users
</v-chip>
<v-btn
v-if="isAdmin"
icon="mdi-account-plus"
variant="text"
size="small"
@click="openAddUserDialog(org)"
/>
</template>
</v-list-item>
</v-list>
<v-alert v-else type="info" variant="tonal">
<div class="d-flex flex-column align-center">
<p>You don't have any organizations yet.</p>
<p v-if="isAdmin" class="text-caption">Create one to start managing your GraphQL schemas.</p>
<p v-else class="text-caption">Contact your administrator to get access to an organization.</p>
</div>
</v-alert>
</v-card-text>
</v-card>
</v-col>
</v-row>
<v-dialog v-model="createOrgDialog" max-width="500">
<v-card>
<v-card-title>Create Organization</v-card-title>
<v-card-text>
<v-text-field
v-model="newOrgName"
label="Organization Name"
variant="outlined"
:error-messages="orgNameError"
@keyup.enter="createOrganization"
/>
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn variant="text" @click="createOrgDialog = false">Cancel</v-btn>
<v-btn
color="primary"
variant="flat"
:loading="creatingOrg"
:disabled="!newOrgName"
@click="createOrganization"
>
Create
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<v-dialog v-model="addUserDialog" max-width="500">
<v-card>
<v-card-title>Add User to {{ selectedOrg?.name }}</v-card-title>
<v-card-text>
<v-text-field
v-model="newUserId"
label="User ID (Auth0 Subject)"
variant="outlined"
:error-messages="userIdError"
hint="Enter the Auth0 subject (e.g., auth0|123456)"
persistent-hint
@keyup.enter="addUser"
/>
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn variant="text" @click="addUserDialog = false">Cancel</v-btn>
<v-btn
color="primary"
variant="flat"
:loading="addingUser"
:disabled="!newUserId"
@click="addUser"
>
Add User
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<v-snackbar v-model="snackbar" :timeout="3000" :color="snackbarColor">
{{ snackbarText }}
</v-snackbar>
</div>
</template>
<script setup lang="ts">
import { useAuth0 } from '@auth0/auth0-vue'
import {
useAddOrganizationMutation,
useAddUserToOrganizationMutation,
useAllOrganizationsQuery,
useOrganizationsQuery,
} from '~/graphql/generated'
const auth0 = useAuth0()
// Check if user has admin role
const isAdmin = computed(() => {
if (!auth0?.isAuthenticated?.value || !auth0?.user?.value) return false
const roles = auth0.user.value['https://unbound.se/roles']
return Array.isArray(roles) && roles.includes('admin')
})
// Start with queries paused, then unpause after Auth0 is ready
const pauseQueries = ref(true)
// Wait for Auth0 to be fully ready before unpausing queries
watch(() => auth0?.isAuthenticated?.value, (isAuthenticated) => {
if (isAuthenticated) {
// Small delay to ensure access token is available
setTimeout(() => {
pauseQueries.value = false
}, 100)
} else {
pauseQueries.value = true
}
}, { immediate: true })
// Use generated Apollo composables
const userOrgs = useOrganizationsQuery(() => ({
skip: pauseQueries.value,
}))
const allOrgs = useAllOrganizationsQuery(() => ({
skip: pauseQueries.value,
}))
// Mutations
const { mutate: addOrganizationMutation } = useAddOrganizationMutation({})
const { mutate: addUserMutation } = useAddUserToOrganizationMutation({})
// Select which query to use based on admin status
const organizations = computed(() => {
return isAdmin.value ? allOrgs : userOrgs
})
// Helper function to get organizations data based on admin status
const getOrganizationsData = () => {
if (isAdmin.value) {
return allOrgs.result?.value?.allOrganizations
}
return userOrgs.result?.value?.organizations
}
const stats = computed(() => {
const orgsData = getOrganizationsData()
if (!auth0?.isAuthenticated?.value || !orgsData) {
return {
totalSchemas: 0,
totalRefs: 0,
lastUpdate: 'Not available',
}
}
const allApiKeys = orgsData.flatMap(org => org.apiKeys || [])
const allRefs = new Set(allApiKeys.flatMap(key => key.refs || []))
return {
totalSchemas: allApiKeys.length,
totalRefs: allRefs.size,
lastUpdate: new Date().toLocaleString(),
}
})
// Organization creation
const createOrgDialog = ref(false)
const newOrgName = ref('')
const orgNameError = ref('')
const creatingOrg = ref(false)
const snackbar = ref(false)
const snackbarText = ref('')
const snackbarColor = ref('success')
// Add user to organization
const addUserDialog = ref(false)
const selectedOrg = ref<any>(null)
const newUserId = ref('')
const userIdError = ref('')
const addingUser = ref(false)
const openAddUserDialog = (org: any) => {
selectedOrg.value = org
newUserId.value = ''
userIdError.value = ''
addUserDialog.value = true
}
const addUser = async () => {
if (!newUserId.value) {
userIdError.value = 'User ID is required'
return
}
if (!selectedOrg.value) {
return
}
addingUser.value = true
userIdError.value = ''
try {
const result = await addUserMutation({
organizationId: selectedOrg.value.id,
userId: newUserId.value,
})
if (!result) {
throw new Error('Failed to add user')
}
snackbarText.value = `User added to "${selectedOrg.value.name}" successfully!`
snackbarColor.value = 'success'
snackbar.value = true
addUserDialog.value = false
newUserId.value = ''
// Refetch organizations
await userOrgs.refetch()
await allOrgs.refetch()
} catch (error: any) {
userIdError.value = error.message || 'Failed to add user'
snackbarText.value = 'Failed to add user'
snackbarColor.value = 'error'
snackbar.value = true
} finally {
addingUser.value = false
}
}
const createOrganization = async () => {
if (!newOrgName.value) {
orgNameError.value = 'Organization name is required'
return
}
creatingOrg.value = true
orgNameError.value = ''
try {
const result = await addOrganizationMutation({ name: newOrgName.value })
if (!result) {
throw new Error('Failed to create organization')
}
snackbarText.value = `Organization "${newOrgName.value}" created successfully!`
snackbarColor.value = 'success'
snackbar.value = true
createOrgDialog.value = false
newOrgName.value = ''
// Refetch organizations
await userOrgs.refetch()
await allOrgs.refetch()
} catch (error: any) {
orgNameError.value = error.message || 'Failed to create organization'
snackbarText.value = 'Failed to create organization'
snackbarColor.value = 'error'
snackbar.value = true
} finally {
creatingOrg.value = false
}
}
</script>