072e1b10f1
- Add Nuxt 4 application with Vuetify UI framework - Implement GraphQL schema registry management interface - Add Apollo Client integration with Auth0 authentication - Create organization and API key management - Add schema and ref browsing capabilities - Implement organization switcher for multi-org users - Add delete functionality for organizations and API keys - Create Kubernetes deployment descriptors - Add Docker configuration with nginx Features: - Dashboard with organization overview - Schema browsing by ref with supergraph viewing - Ref management with schema details - Settings page for organizations and API keys - User list per organization with provider icons - Admin-only organization creation - Delete confirmations with warnings 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
257 lines
7.4 KiB
Vue
257 lines
7.4 KiB
Vue
<template>
|
|
<div>
|
|
<v-row>
|
|
<v-col cols="12">
|
|
<h1 class="text-h3 mb-4">Schema Refs</h1>
|
|
<p class="text-subtitle-1 text-grey mb-4">
|
|
View and manage different versions of your federated GraphQL schemas
|
|
</p>
|
|
</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 schema refs.
|
|
</v-alert>
|
|
</v-col>
|
|
</v-row>
|
|
|
|
<v-row v-else-if="loading?.value">
|
|
<v-col cols="12" class="text-center">
|
|
<v-progress-circular indeterminate size="64" />
|
|
<p class="mt-4">Loading refs...</p>
|
|
</v-col>
|
|
</v-row>
|
|
|
|
<v-row v-else-if="error?.value">
|
|
<v-col cols="12">
|
|
<v-alert type="error" variant="tonal">
|
|
Error loading refs: {{ error?.value?.message }}
|
|
</v-alert>
|
|
</v-col>
|
|
</v-row>
|
|
|
|
<v-row v-else-if="refs.length === 0">
|
|
<v-col cols="12" class="text-center py-12">
|
|
<v-icon icon="mdi-source-branch" size="64" class="mb-4 text-grey" />
|
|
<p class="text-h6 text-grey">No refs found</p>
|
|
<p class="text-grey">Create an API key with refs to get started</p>
|
|
</v-col>
|
|
</v-row>
|
|
|
|
<v-row v-else>
|
|
<v-col
|
|
v-for="ref in refs"
|
|
:key="ref.name"
|
|
cols="12"
|
|
md="6"
|
|
lg="4"
|
|
>
|
|
<v-card hover>
|
|
<v-card-title class="d-flex align-center">
|
|
<v-icon icon="mdi-source-branch" class="mr-2" />
|
|
{{ ref.name }}
|
|
</v-card-title>
|
|
<v-card-text>
|
|
<v-list density="compact">
|
|
<v-list-item>
|
|
<v-list-item-title>Subgraphs</v-list-item-title>
|
|
<v-list-item-subtitle>{{ ref.subgraphCount }}</v-list-item-subtitle>
|
|
</v-list-item>
|
|
<v-list-item>
|
|
<v-list-item-title>Last Updated</v-list-item-title>
|
|
<v-list-item-subtitle>{{ ref.lastUpdate }}</v-list-item-subtitle>
|
|
</v-list-item>
|
|
<v-list-item>
|
|
<v-list-item-title>Status</v-list-item-title>
|
|
<template #append>
|
|
<v-chip :color="ref.status === 'healthy' ? 'success' : 'warning'" size="small">
|
|
{{ ref.status }}
|
|
</v-chip>
|
|
</template>
|
|
</v-list-item>
|
|
</v-list>
|
|
</v-card-text>
|
|
<v-card-actions>
|
|
<v-btn variant="text" color="primary" @click="viewSupergraph(ref.name)">
|
|
View Supergraph
|
|
</v-btn>
|
|
<v-spacer />
|
|
<v-btn icon="mdi-download" variant="text" @click="downloadSupergraph(ref.name)" />
|
|
</v-card-actions>
|
|
</v-card>
|
|
</v-col>
|
|
</v-row>
|
|
|
|
<v-dialog v-model="dialog" max-width="900">
|
|
<v-card>
|
|
<v-card-title class="d-flex justify-space-between align-center">
|
|
<span>Supergraph - {{ selectedRef }}</span>
|
|
<v-btn icon="mdi-close" variant="text" @click="dialog = false" />
|
|
</v-card-title>
|
|
<v-card-text>
|
|
<pre class="sdl-viewer"><code>{{ supergraph }}</code></pre>
|
|
</v-card-text>
|
|
<v-card-actions>
|
|
<v-spacer />
|
|
<v-btn
|
|
prepend-icon="mdi-content-copy"
|
|
variant="text"
|
|
@click="copySupergraph"
|
|
>
|
|
Copy
|
|
</v-btn>
|
|
<v-btn
|
|
prepend-icon="mdi-download"
|
|
variant="text"
|
|
color="primary"
|
|
@click="downloadSupergraph(selectedRef)"
|
|
>
|
|
Download
|
|
</v-btn>
|
|
</v-card-actions>
|
|
</v-card>
|
|
</v-dialog>
|
|
|
|
<v-snackbar v-model="snackbar" :timeout="2000">
|
|
{{ snackbarText }}
|
|
</v-snackbar>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { useAuth0 } from '@auth0/auth0-vue'
|
|
|
|
import { useOrganizationSelector } from '~/composables/useOrganizationSelector'
|
|
import {
|
|
useLatestSchemaQuery,
|
|
useSupergraphQuery,
|
|
} from '~/graphql/generated'
|
|
|
|
const auth0 = useAuth0()
|
|
const { selectedOrganization, loading, error } = useOrganizationSelector()
|
|
|
|
const dialog = ref(false)
|
|
const selectedRef = ref<string | null>(null)
|
|
const supergraph = ref('')
|
|
const snackbar = ref(false)
|
|
const snackbarText = ref('')
|
|
|
|
// Supergraph query - only runs when selectedRef is set
|
|
const supergraphQuery = useSupergraphQuery(() => ({
|
|
ref: selectedRef.value || '',
|
|
isAfter: null,
|
|
}), () => ({
|
|
skip: !selectedRef.value,
|
|
}))
|
|
|
|
// Watch for supergraph query results
|
|
watch(() => supergraphQuery.result.value, (data) => {
|
|
if (data?.supergraph) {
|
|
if (data.supergraph.__typename === 'SubGraphs') {
|
|
supergraph.value = data.supergraph.sdl || '# No supergraph available'
|
|
} else if (data.supergraph.__typename === 'Unchanged') {
|
|
supergraph.value = `# Supergraph unchanged (ID: ${data.supergraph.id})\n# Please retry after ${data.supergraph.minDelaySeconds} seconds`
|
|
}
|
|
}
|
|
})
|
|
|
|
// Watch for supergraph query errors
|
|
watch(() => supergraphQuery.error.value, (err) => {
|
|
if (err) {
|
|
supergraph.value = `# Error loading supergraph\n# ${err.message || 'Unknown error'}`
|
|
snackbarText.value = 'Failed to load supergraph'
|
|
snackbar.value = true
|
|
}
|
|
})
|
|
|
|
// Get available refs from the selected organization
|
|
const refNames = computed(() => {
|
|
if (!selectedOrganization.value) return []
|
|
|
|
const allRefs = new Set<string>()
|
|
selectedOrganization.value.apiKeys?.forEach(key => {
|
|
key.refs?.forEach(ref => allRefs.add(ref))
|
|
})
|
|
|
|
return Array.from(allRefs)
|
|
})
|
|
|
|
// Create reactive queries for each ref
|
|
const refQueries = computed(() => {
|
|
if (!auth0.isAuthenticated.value) return {}
|
|
|
|
const queries: Record<string, any> = {}
|
|
refNames.value.forEach(ref => {
|
|
queries[ref] = useLatestSchemaQuery(() => ({
|
|
ref,
|
|
}), () => ({
|
|
skip: !auth0.isAuthenticated.value,
|
|
}))
|
|
})
|
|
return queries
|
|
})
|
|
|
|
const refs = computed(() => {
|
|
return refNames.value.map(refName => {
|
|
const query = refQueries.value[refName]
|
|
const latestSchema = query?.result?.value?.latestSchema
|
|
|
|
return {
|
|
name: refName,
|
|
subgraphCount: latestSchema?.subGraphs?.length || 0,
|
|
lastUpdate: latestSchema?.subGraphs?.[0]?.changedAt
|
|
? new Date(latestSchema.subGraphs[0].changedAt).toLocaleString()
|
|
: 'N/A',
|
|
status: query?.loading?.value ? 'loading' : (query?.error?.value ? 'error' : 'healthy'),
|
|
}
|
|
})
|
|
})
|
|
|
|
const viewSupergraph = (refName: string) => {
|
|
selectedRef.value = refName
|
|
supergraph.value = '# Loading...'
|
|
dialog.value = true
|
|
}
|
|
|
|
const downloadSupergraph = (refName: string) => {
|
|
const blob = new Blob([supergraph.value || '# Loading...'], { type: 'text/plain' })
|
|
const url = URL.createObjectURL(blob)
|
|
const a = document.createElement('a')
|
|
a.href = url
|
|
a.download = `supergraph-${refName}.graphql`
|
|
a.click()
|
|
URL.revokeObjectURL(url)
|
|
|
|
snackbarText.value = 'Supergraph downloaded'
|
|
snackbar.value = true
|
|
}
|
|
|
|
const copySupergraph = async () => {
|
|
try {
|
|
await navigator.clipboard.writeText(supergraph.value)
|
|
snackbarText.value = 'Supergraph copied to clipboard'
|
|
snackbar.value = true
|
|
} catch (_err) {
|
|
snackbarText.value = 'Failed to copy'
|
|
snackbar.value = true
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.sdl-viewer {
|
|
background: #f5f5f5;
|
|
padding: 16px;
|
|
border-radius: 4px;
|
|
overflow-x: auto;
|
|
font-family: Monaco, Menlo, 'Ubuntu Mono', monospace;
|
|
font-size: 14px;
|
|
line-height: 1.5;
|
|
max-height: 60vh;
|
|
white-space: pre-wrap;
|
|
overflow-wrap: break-word;
|
|
}
|
|
</style>
|