Files
schemas-app/app/pages/refs.vue
T
argoyle 072e1b10f1 feat: initial schemas-app implementation
- 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>
2025-11-22 17:10:10 +01:00

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>