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>
This commit is contained in:
@@ -0,0 +1,15 @@
|
|||||||
|
node_modules
|
||||||
|
.nuxt
|
||||||
|
.output
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
*.md
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
coverage
|
||||||
|
.idea
|
||||||
|
.vscode
|
||||||
|
*.log
|
||||||
|
dist
|
||||||
|
.DS_Store
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
end_of_line = lf
|
||||||
|
charset = utf-8
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
insert_final_newline = true
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
trim_trailing_whitespace = false
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# GraphQL Schema Registry API URL (optional - auto-detected based on hostname)
|
||||||
|
# NUXT_PUBLIC_API_BASE=http://localhost:8080
|
||||||
|
|
||||||
|
# Auth0 Configuration
|
||||||
|
NUXT_PUBLIC_AUTH0_DOMAIN=unbound.eu.auth0.com
|
||||||
|
NUXT_PUBLIC_AUTH0_CLIENT_ID=7k3rZ4HRxRTkTCI3ytpF8ji0o531Zq3M
|
||||||
|
NUXT_PUBLIC_AUTH0_AUDIENCE=https://schemas.unbound.se
|
||||||
+24
@@ -0,0 +1,24 @@
|
|||||||
|
# Nuxt dev/build outputs
|
||||||
|
.output
|
||||||
|
.data
|
||||||
|
.nuxt
|
||||||
|
.nitro
|
||||||
|
.cache
|
||||||
|
dist
|
||||||
|
|
||||||
|
# Node dependencies
|
||||||
|
node_modules
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Misc
|
||||||
|
.DS_Store
|
||||||
|
.fleet
|
||||||
|
.idea
|
||||||
|
|
||||||
|
# Local env files
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
include:
|
||||||
|
- template: 'Workflows/MergeRequest-Pipelines.gitlab-ci.yml'
|
||||||
|
- project: unboundsoftware/ci-templates
|
||||||
|
file: Defaults.gitlab-ci.yml
|
||||||
|
- project: unboundsoftware/ci-templates
|
||||||
|
file: Pre-Commit-Node.gitlab-ci.yml
|
||||||
|
|
||||||
|
stages:
|
||||||
|
- build
|
||||||
|
- deploy-prod
|
||||||
|
|
||||||
|
image: buildtool/build-tools:${BUILDTOOLS_VERSION}
|
||||||
|
|
||||||
|
build:
|
||||||
|
stage: build
|
||||||
|
script:
|
||||||
|
- build
|
||||||
|
- push
|
||||||
|
artifacts:
|
||||||
|
paths:
|
||||||
|
- k8s
|
||||||
|
|
||||||
|
deploy-to-prod:
|
||||||
|
stage: deploy-prod
|
||||||
|
script:
|
||||||
|
- echo Deploy Schemas App to PROD.
|
||||||
|
- deploy prod
|
||||||
|
environment:
|
||||||
|
name: prod
|
||||||
|
resource_group: prod
|
||||||
|
rules:
|
||||||
|
- if: $CI_DEFAULT_BRANCH == $CI_COMMIT_BRANCH
|
||||||
|
when: manual
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
rules "~MD013", "~MD024", "~MD029", "~MD005", "~MD007", "~MD033", "~MD036"
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
24
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
# See https://pre-commit.com for more information
|
||||||
|
# See https://pre-commit.com/hooks.html for more hooks
|
||||||
|
repos:
|
||||||
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
|
rev: v6.0.0
|
||||||
|
hooks:
|
||||||
|
- id: trailing-whitespace
|
||||||
|
- id: end-of-file-fixer
|
||||||
|
- id: check-yaml
|
||||||
|
args:
|
||||||
|
- --allow-multiple-documents
|
||||||
|
- id: check-added-large-files
|
||||||
|
- repo: https://github.com/markdownlint/markdownlint
|
||||||
|
rev: v0.13.0
|
||||||
|
hooks:
|
||||||
|
- id: markdownlint
|
||||||
|
- repo: https://gitlab.com/devopshq/gitlab-ci-linter
|
||||||
|
rev: v1.0.6
|
||||||
|
hooks:
|
||||||
|
- id: gitlab-ci-linter
|
||||||
|
args:
|
||||||
|
- --project
|
||||||
|
- unboundsoftware/schemas-app
|
||||||
|
- repo: https://github.com/alessandrojcm/commitlint-pre-commit-hook
|
||||||
|
rev: v9.23.0
|
||||||
|
hooks:
|
||||||
|
- id: commitlint
|
||||||
|
stages: [ commit-msg ]
|
||||||
|
additional_dependencies: [ '@commitlint/config-conventional' ]
|
||||||
|
- repo: https://github.com/pre-commit/mirrors-eslint
|
||||||
|
rev: v9.39.1
|
||||||
|
hooks:
|
||||||
|
- id: eslint
|
||||||
|
types: [ file ]
|
||||||
|
types_or: [ javascript, ts, vue ]
|
||||||
|
- repo: https://github.com/gitleaks/gitleaks
|
||||||
|
rev: v8.29.1
|
||||||
|
hooks:
|
||||||
|
- id: gitleaks
|
||||||
|
exclude: '^app/graphql/generated\.ts$'
|
||||||
+17
@@ -0,0 +1,17 @@
|
|||||||
|
FROM cypress/base:25.0.0@sha256:43bc659f825c1a9e4aa540fcdf35065de3e20e0b463589bfccd1ecb717cfc0fb as builder
|
||||||
|
WORKDIR /build
|
||||||
|
|
||||||
|
RUN npm install --global node-gyp && \
|
||||||
|
git config --global --add safe.directory /build
|
||||||
|
|
||||||
|
COPY ./package.json ./package-lock.json ./
|
||||||
|
RUN npm ci --no-progress
|
||||||
|
COPY . /build
|
||||||
|
RUN npm run generate
|
||||||
|
|
||||||
|
FROM amd64/nginx:1.29.3@sha256:4fd8a65a560a906addb9930f2cd5a3d33ff5b8e8b50e983bce275c9c78151a96
|
||||||
|
MAINTAINER Joakim Olsson <joakim@unbound.se>
|
||||||
|
|
||||||
|
COPY ./nginx/nginx.conf /etc/nginx/nginx.conf
|
||||||
|
COPY ./nginx/security_headers.conf /etc/nginx/security_headers.conf
|
||||||
|
COPY --from=builder /build/.output/public/ /usr/share/nginx/html/
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
# Unbound Schemas - Web UI
|
||||||
|
|
||||||
|
A Nuxt 4 application with Vuetify for viewing and managing federated GraphQL schemas.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- 📊 Dashboard with schema statistics and recent updates
|
||||||
|
- 📝 View all published schemas by ref (production, staging, etc.)
|
||||||
|
- 🔍 Search and filter schemas
|
||||||
|
- 📄 View schema SDL (Schema Definition Language)
|
||||||
|
- 📥 Download and copy schemas
|
||||||
|
- 🌳 View merged supergraphs for each ref
|
||||||
|
- 🎨 Modern Material Design UI with Vuetify
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
Create a `.env` file in the root directory:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
NUXT_PUBLIC_API_BASE=http://localhost:8080
|
||||||
|
```
|
||||||
|
|
||||||
|
The API base URL points to your GraphQL Schema Registry backend service.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
Make sure to install dependencies:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# npm
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# pnpm
|
||||||
|
pnpm install
|
||||||
|
|
||||||
|
# yarn
|
||||||
|
yarn install
|
||||||
|
|
||||||
|
# bun
|
||||||
|
bun install
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development Server
|
||||||
|
|
||||||
|
Start the development server on `http://localhost:3500`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# npm
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# pnpm
|
||||||
|
pnpm dev
|
||||||
|
|
||||||
|
# yarn
|
||||||
|
yarn dev
|
||||||
|
|
||||||
|
# bun
|
||||||
|
bun run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## Production
|
||||||
|
|
||||||
|
Build the application for production:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# npm
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
# pnpm
|
||||||
|
pnpm build
|
||||||
|
|
||||||
|
# yarn
|
||||||
|
yarn build
|
||||||
|
|
||||||
|
# bun
|
||||||
|
bun run build
|
||||||
|
```
|
||||||
|
|
||||||
|
Locally preview production build:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# npm
|
||||||
|
npm run preview
|
||||||
|
|
||||||
|
# pnpm
|
||||||
|
pnpm preview
|
||||||
|
|
||||||
|
# yarn
|
||||||
|
yarn preview
|
||||||
|
|
||||||
|
# bun
|
||||||
|
bun run preview
|
||||||
|
```
|
||||||
|
|
||||||
|
Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information.
|
||||||
+82
@@ -0,0 +1,82 @@
|
|||||||
|
<template>
|
||||||
|
<v-app>
|
||||||
|
<v-app-bar color="primary" prominent>
|
||||||
|
<v-app-bar-nav-icon variant="text" @click.stop="drawer = !drawer" />
|
||||||
|
<v-toolbar-title>Unbound Schemas</v-toolbar-title>
|
||||||
|
<v-spacer />
|
||||||
|
|
||||||
|
<template v-if="auth0?.isLoading?.value">
|
||||||
|
<v-progress-circular indeterminate size="24" width="2" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else-if="auth0?.isAuthenticated?.value">
|
||||||
|
<OrganizationSwitcher class="mr-4" />
|
||||||
|
|
||||||
|
<v-menu>
|
||||||
|
<template #activator="{ props }">
|
||||||
|
<v-btn v-bind="props" variant="text">
|
||||||
|
<v-avatar size="32" class="mr-2">
|
||||||
|
<v-img v-if="auth0?.user?.value?.picture" :src="auth0.user.value.picture" />
|
||||||
|
<v-icon v-else>mdi-account-circle</v-icon>
|
||||||
|
</v-avatar>
|
||||||
|
{{ auth0?.user?.value?.name || auth0?.user?.value?.email }}
|
||||||
|
</v-btn>
|
||||||
|
</template>
|
||||||
|
<v-list>
|
||||||
|
<v-list-item>
|
||||||
|
<v-list-item-title>{{ auth0?.user?.value?.email }}</v-list-item-title>
|
||||||
|
<v-list-item-subtitle>{{ auth0?.user?.value?.name }}</v-list-item-subtitle>
|
||||||
|
</v-list-item>
|
||||||
|
<v-divider />
|
||||||
|
<v-list-item prepend-icon="mdi-cog" title="Settings" to="/settings" />
|
||||||
|
<v-list-item prepend-icon="mdi-logout" title="Logout" @click="auth0?.logout({ logoutParams: { returnTo: window.location.origin } })" />
|
||||||
|
</v-list>
|
||||||
|
</v-menu>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<v-btn variant="outlined" @click="auth0?.loginWithRedirect()">
|
||||||
|
Login
|
||||||
|
</v-btn>
|
||||||
|
</template>
|
||||||
|
</v-app-bar>
|
||||||
|
|
||||||
|
<v-navigation-drawer v-model="drawer" temporary>
|
||||||
|
<v-list>
|
||||||
|
<v-list-item
|
||||||
|
prepend-icon="mdi-view-dashboard"
|
||||||
|
title="Dashboard"
|
||||||
|
to="/"
|
||||||
|
/>
|
||||||
|
<v-list-item
|
||||||
|
prepend-icon="mdi-graphql"
|
||||||
|
title="Schemas"
|
||||||
|
to="/schemas"
|
||||||
|
/>
|
||||||
|
<v-list-item
|
||||||
|
prepend-icon="mdi-source-branch"
|
||||||
|
title="Refs"
|
||||||
|
to="/refs"
|
||||||
|
/>
|
||||||
|
<v-list-item
|
||||||
|
prepend-icon="mdi-cog"
|
||||||
|
title="Settings"
|
||||||
|
to="/settings"
|
||||||
|
/>
|
||||||
|
</v-list>
|
||||||
|
</v-navigation-drawer>
|
||||||
|
|
||||||
|
<v-main>
|
||||||
|
<v-container fluid>
|
||||||
|
<NuxtPage />
|
||||||
|
</v-container>
|
||||||
|
</v-main>
|
||||||
|
</v-app>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useAuth0 } from '@auth0/auth0-vue'
|
||||||
|
|
||||||
|
const drawer = ref(false)
|
||||||
|
const auth0 = useAuth0()
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
<template>
|
||||||
|
<v-menu v-if="organizations.length > 1">
|
||||||
|
<template #activator="{ props }">
|
||||||
|
<v-btn
|
||||||
|
v-bind="props"
|
||||||
|
variant="outlined"
|
||||||
|
prepend-icon="mdi-domain"
|
||||||
|
class="text-none"
|
||||||
|
>
|
||||||
|
{{ selectedOrganization?.name || 'Select Organization' }}
|
||||||
|
<v-icon end>mdi-menu-down</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</template>
|
||||||
|
<v-list>
|
||||||
|
<v-list-item
|
||||||
|
v-for="org in organizations"
|
||||||
|
:key="org.id"
|
||||||
|
:active="org.id === selectedOrgId"
|
||||||
|
@click="selectOrganization(org.id)"
|
||||||
|
>
|
||||||
|
<v-list-item-title>
|
||||||
|
<v-icon v-if="org.id === selectedOrgId" start size="small">mdi-check</v-icon>
|
||||||
|
{{ org.name }}
|
||||||
|
</v-list-item-title>
|
||||||
|
<template #append>
|
||||||
|
<v-chip size="small" variant="tonal">
|
||||||
|
{{ org.apiKeys?.length || 0 }} keys
|
||||||
|
</v-chip>
|
||||||
|
</template>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
</v-menu>
|
||||||
|
<v-chip v-else-if="organizations.length === 1" variant="outlined" prepend-icon="mdi-domain">
|
||||||
|
{{ organizations[0].name }}
|
||||||
|
</v-chip>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useOrganizationSelector } from '~/composables/useOrganizationSelector'
|
||||||
|
|
||||||
|
const { organizations, selectedOrganization, selectedOrgId, selectOrganization } = useOrganizationSelector()
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import { useAuth0 } from '@auth0/auth0-vue'
|
||||||
|
import { computed, ref, watch } from 'vue'
|
||||||
|
|
||||||
|
import { useOrganizationsQuery } from '~/graphql/generated'
|
||||||
|
|
||||||
|
const selectedOrgId = ref<string | null>(null)
|
||||||
|
|
||||||
|
export const useOrganizationSelector = () => {
|
||||||
|
const auth0 = useAuth0()
|
||||||
|
|
||||||
|
// Fetch user's organizations
|
||||||
|
const { result, loading, error, refetch } = useOrganizationsQuery(() => ({
|
||||||
|
skip: !auth0.isAuthenticated.value,
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Get list of organizations
|
||||||
|
const organizations = computed(() => {
|
||||||
|
return result.value?.organizations || []
|
||||||
|
})
|
||||||
|
|
||||||
|
// Get currently selected organization
|
||||||
|
const selectedOrganization = computed(() => {
|
||||||
|
if (!selectedOrgId.value) return null
|
||||||
|
return organizations.value.find(org => org.id === selectedOrgId.value) || null
|
||||||
|
})
|
||||||
|
|
||||||
|
// Auto-select first organization if none selected
|
||||||
|
watch(organizations, (orgs) => {
|
||||||
|
if (orgs.length > 0 && !selectedOrgId.value) {
|
||||||
|
// Try to restore from localStorage
|
||||||
|
const saved = localStorage.getItem('selectedOrgId')
|
||||||
|
if (saved && orgs.find(o => o.id === saved)) {
|
||||||
|
selectedOrgId.value = saved
|
||||||
|
} else {
|
||||||
|
selectedOrgId.value = orgs[0].id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, { immediate: true })
|
||||||
|
|
||||||
|
// Save selection to localStorage
|
||||||
|
watch(selectedOrgId, (newId) => {
|
||||||
|
if (newId) {
|
||||||
|
localStorage.setItem('selectedOrgId', newId)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const selectOrganization = (orgId: string) => {
|
||||||
|
selectedOrgId.value = orgId
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
organizations,
|
||||||
|
selectedOrganization,
|
||||||
|
selectedOrgId: computed(() => selectedOrgId.value),
|
||||||
|
selectOrganization,
|
||||||
|
loading: loading || ref(false),
|
||||||
|
error: error || ref(null),
|
||||||
|
refetch,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,584 @@
|
|||||||
|
import * as VueApolloComposable from '@vue/apollo-composable'
|
||||||
|
import gql from 'graphql-tag'
|
||||||
|
import type * as VueCompositionApi from 'vue'
|
||||||
|
export type Maybe<T> = T | null
|
||||||
|
export type InputMaybe<T> = Maybe<T>
|
||||||
|
export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] }
|
||||||
|
export type MakeOptional<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]?: Maybe<T[SubKey]> }
|
||||||
|
export type MakeMaybe<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]: Maybe<T[SubKey]> }
|
||||||
|
export type MakeEmpty<T extends { [key: string]: unknown }, K extends keyof T> = { [_ in K]?: never }
|
||||||
|
export type Incremental<T> = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never }
|
||||||
|
export type ReactiveFunction<TParam> = () => TParam
|
||||||
|
/** All built-in and custom scalars, mapped to their actual values */
|
||||||
|
export type Scalars = {
|
||||||
|
ID: { input: string; output: string }
|
||||||
|
String: { input: string; output: string }
|
||||||
|
Boolean: { input: boolean; output: boolean }
|
||||||
|
Int: { input: number; output: number }
|
||||||
|
Float: { input: number; output: number }
|
||||||
|
Time: { input: any; output: any }
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ApiKey = {
|
||||||
|
__typename?: 'APIKey'
|
||||||
|
id: Scalars['ID']['output']
|
||||||
|
key?: Maybe<Scalars['String']['output']>
|
||||||
|
name: Scalars['String']['output']
|
||||||
|
organization: Organization
|
||||||
|
publish: Scalars['Boolean']['output']
|
||||||
|
read: Scalars['Boolean']['output']
|
||||||
|
refs: Array<Scalars['String']['output']>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type InputApiKey = {
|
||||||
|
name: Scalars['String']['input']
|
||||||
|
organizationId: Scalars['ID']['input']
|
||||||
|
publish: Scalars['Boolean']['input']
|
||||||
|
read: Scalars['Boolean']['input']
|
||||||
|
refs: Array<Scalars['String']['input']>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type InputSubGraph = {
|
||||||
|
ref: Scalars['String']['input']
|
||||||
|
sdl: Scalars['String']['input']
|
||||||
|
service: Scalars['String']['input']
|
||||||
|
url?: InputMaybe<Scalars['String']['input']>
|
||||||
|
wsUrl?: InputMaybe<Scalars['String']['input']>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Mutation = {
|
||||||
|
__typename?: 'Mutation'
|
||||||
|
addAPIKey: ApiKey
|
||||||
|
addOrganization: Organization
|
||||||
|
addUserToOrganization: Organization
|
||||||
|
removeAPIKey: Organization
|
||||||
|
removeOrganization: Scalars['Boolean']['output']
|
||||||
|
updateSubGraph: SubGraph
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export type MutationAddApiKeyArgs = {
|
||||||
|
input?: InputMaybe<InputApiKey>
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export type MutationAddOrganizationArgs = {
|
||||||
|
name: Scalars['String']['input']
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export type MutationAddUserToOrganizationArgs = {
|
||||||
|
organizationId: Scalars['ID']['input']
|
||||||
|
userId: Scalars['String']['input']
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export type MutationRemoveApiKeyArgs = {
|
||||||
|
keyName: Scalars['String']['input']
|
||||||
|
organizationId: Scalars['ID']['input']
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export type MutationRemoveOrganizationArgs = {
|
||||||
|
organizationId: Scalars['ID']['input']
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export type MutationUpdateSubGraphArgs = {
|
||||||
|
input: InputSubGraph
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Organization = {
|
||||||
|
__typename?: 'Organization'
|
||||||
|
apiKeys: Array<ApiKey>
|
||||||
|
id: Scalars['ID']['output']
|
||||||
|
name: Scalars['String']['output']
|
||||||
|
users: Array<User>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Query = {
|
||||||
|
__typename?: 'Query'
|
||||||
|
allOrganizations: Array<Organization>
|
||||||
|
latestSchema: SchemaUpdate
|
||||||
|
organizations: Array<Organization>
|
||||||
|
supergraph: Supergraph
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export type QueryLatestSchemaArgs = {
|
||||||
|
ref: Scalars['String']['input']
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export type QuerySupergraphArgs = {
|
||||||
|
isAfter?: InputMaybe<Scalars['String']['input']>
|
||||||
|
ref: Scalars['String']['input']
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SchemaUpdate = {
|
||||||
|
__typename?: 'SchemaUpdate'
|
||||||
|
cosmoRouterConfig?: Maybe<Scalars['String']['output']>
|
||||||
|
id: Scalars['ID']['output']
|
||||||
|
ref: Scalars['String']['output']
|
||||||
|
subGraphs: Array<SubGraph>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SubGraph = {
|
||||||
|
__typename?: 'SubGraph'
|
||||||
|
changedAt: Scalars['Time']['output']
|
||||||
|
changedBy: Scalars['String']['output']
|
||||||
|
id: Scalars['ID']['output']
|
||||||
|
sdl: Scalars['String']['output']
|
||||||
|
service: Scalars['String']['output']
|
||||||
|
url?: Maybe<Scalars['String']['output']>
|
||||||
|
wsUrl?: Maybe<Scalars['String']['output']>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SubGraphs = {
|
||||||
|
__typename?: 'SubGraphs'
|
||||||
|
id: Scalars['ID']['output']
|
||||||
|
minDelaySeconds: Scalars['Int']['output']
|
||||||
|
sdl: Scalars['String']['output']
|
||||||
|
subGraphs: Array<SubGraph>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Subscription = {
|
||||||
|
__typename?: 'Subscription'
|
||||||
|
schemaUpdates: SchemaUpdate
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export type SubscriptionSchemaUpdatesArgs = {
|
||||||
|
ref: Scalars['String']['input']
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Supergraph = SubGraphs | Unchanged
|
||||||
|
|
||||||
|
export type Unchanged = {
|
||||||
|
__typename?: 'Unchanged'
|
||||||
|
id: Scalars['ID']['output']
|
||||||
|
minDelaySeconds: Scalars['Int']['output']
|
||||||
|
}
|
||||||
|
|
||||||
|
export type User = {
|
||||||
|
__typename?: 'User'
|
||||||
|
id: Scalars['String']['output']
|
||||||
|
}
|
||||||
|
|
||||||
|
export type OrganizationsQueryVariables = Exact<{ [key: string]: never }>
|
||||||
|
|
||||||
|
|
||||||
|
export type OrganizationsQuery = { __typename?: 'Query'; organizations: Array<{ __typename?: 'Organization'; id: string; name: string; users: Array<{ __typename?: 'User'; id: string }>; apiKeys: Array<{ __typename?: 'APIKey'; id: string; name: string; refs: Array<string>; read: boolean; publish: boolean }> }> }
|
||||||
|
|
||||||
|
export type AllOrganizationsQueryVariables = Exact<{ [key: string]: never }>
|
||||||
|
|
||||||
|
|
||||||
|
export type AllOrganizationsQuery = { __typename?: 'Query'; allOrganizations: Array<{ __typename?: 'Organization'; id: string; name: string; users: Array<{ __typename?: 'User'; id: string }>; apiKeys: Array<{ __typename?: 'APIKey'; id: string; name: string; refs: Array<string>; read: boolean; publish: boolean }> }> }
|
||||||
|
|
||||||
|
export type LatestSchemaQueryVariables = Exact<{
|
||||||
|
ref: Scalars['String']['input']
|
||||||
|
}>
|
||||||
|
|
||||||
|
|
||||||
|
export type LatestSchemaQuery = { __typename?: 'Query'; latestSchema: { __typename?: 'SchemaUpdate'; ref: string; id: string; cosmoRouterConfig?: string | null; subGraphs: Array<{ __typename?: 'SubGraph'; id: string; service: string; url?: string | null; wsUrl?: string | null; sdl: string; changedBy: string; changedAt: any }> } }
|
||||||
|
|
||||||
|
export type SupergraphQueryVariables = Exact<{
|
||||||
|
ref: Scalars['String']['input']
|
||||||
|
isAfter?: InputMaybe<Scalars['String']['input']>
|
||||||
|
}>
|
||||||
|
|
||||||
|
|
||||||
|
export type SupergraphQuery = { __typename?: 'Query', supergraph:
|
||||||
|
| { __typename?: 'SubGraphs'; id: string; sdl: string; subGraphs: Array<{ __typename?: 'SubGraph'; id: string; service: string; url?: string | null; wsUrl?: string | null; sdl: string; changedBy: string; changedAt: any }> }
|
||||||
|
| { __typename?: 'Unchanged'; id: string; minDelaySeconds: number }
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AddOrganizationMutationVariables = Exact<{
|
||||||
|
name: Scalars['String']['input']
|
||||||
|
}>
|
||||||
|
|
||||||
|
|
||||||
|
export type AddOrganizationMutation = { __typename?: 'Mutation'; addOrganization: { __typename?: 'Organization'; id: string; name: string; users: Array<{ __typename?: 'User'; id: string }>; apiKeys: Array<{ __typename?: 'APIKey'; id: string; name: string; refs: Array<string>; read: boolean; publish: boolean }> } }
|
||||||
|
|
||||||
|
export type AddUserToOrganizationMutationVariables = Exact<{
|
||||||
|
organizationId: Scalars['ID']['input']
|
||||||
|
userId: Scalars['String']['input']
|
||||||
|
}>
|
||||||
|
|
||||||
|
|
||||||
|
export type AddUserToOrganizationMutation = { __typename?: 'Mutation'; addUserToOrganization: { __typename?: 'Organization'; id: string; name: string; users: Array<{ __typename?: 'User'; id: string }>; apiKeys: Array<{ __typename?: 'APIKey'; id: string; name: string; refs: Array<string>; read: boolean; publish: boolean }> } }
|
||||||
|
|
||||||
|
export type AddApiKeyMutationVariables = Exact<{
|
||||||
|
input: InputApiKey
|
||||||
|
}>
|
||||||
|
|
||||||
|
|
||||||
|
export type AddApiKeyMutation = { __typename?: 'Mutation'; addAPIKey: { __typename?: 'APIKey'; id: string; name: string; key?: string | null; refs: Array<string>; read: boolean; publish: boolean; organization: { __typename?: 'Organization'; id: string; name: string } } }
|
||||||
|
|
||||||
|
export type RemoveApiKeyMutationVariables = Exact<{
|
||||||
|
organizationId: Scalars['ID']['input']
|
||||||
|
keyName: Scalars['String']['input']
|
||||||
|
}>
|
||||||
|
|
||||||
|
|
||||||
|
export type RemoveApiKeyMutation = { __typename?: 'Mutation'; removeAPIKey: { __typename?: 'Organization'; id: string; name: string; users: Array<{ __typename?: 'User'; id: string }>; apiKeys: Array<{ __typename?: 'APIKey'; id: string; name: string; refs: Array<string>; read: boolean; publish: boolean }> } }
|
||||||
|
|
||||||
|
export type RemoveOrganizationMutationVariables = Exact<{
|
||||||
|
organizationId: Scalars['ID']['input']
|
||||||
|
}>
|
||||||
|
|
||||||
|
|
||||||
|
export type RemoveOrganizationMutation = { __typename?: 'Mutation'; removeOrganization: boolean }
|
||||||
|
|
||||||
|
|
||||||
|
export const OrganizationsDocument = gql`
|
||||||
|
query Organizations {
|
||||||
|
organizations {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
users {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
apiKeys {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
refs
|
||||||
|
read
|
||||||
|
publish
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
/**
|
||||||
|
* __useOrganizationsQuery__
|
||||||
|
*
|
||||||
|
* To run a query within a Vue component, call `useOrganizationsQuery` and pass it any options that fit your needs.
|
||||||
|
* When your component renders, `useOrganizationsQuery` returns an object from Apollo Client that contains result, loading and error properties
|
||||||
|
* you can use to render your UI.
|
||||||
|
*
|
||||||
|
* @param options that will be passed into the query, supported options are listed on: https://v4.apollo.vuejs.org/guide-composable/query.html#options;
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const { result, loading, error } = useOrganizationsQuery();
|
||||||
|
*/
|
||||||
|
export function useOrganizationsQuery(options: VueApolloComposable.UseQueryOptions<OrganizationsQuery, OrganizationsQueryVariables> | VueCompositionApi.Ref<VueApolloComposable.UseQueryOptions<OrganizationsQuery, OrganizationsQueryVariables>> | ReactiveFunction<VueApolloComposable.UseQueryOptions<OrganizationsQuery, OrganizationsQueryVariables>> = {}) {
|
||||||
|
return VueApolloComposable.useQuery<OrganizationsQuery, OrganizationsQueryVariables>(OrganizationsDocument, {}, options)
|
||||||
|
}
|
||||||
|
export function useOrganizationsLazyQuery(options: VueApolloComposable.UseQueryOptions<OrganizationsQuery, OrganizationsQueryVariables> | VueCompositionApi.Ref<VueApolloComposable.UseQueryOptions<OrganizationsQuery, OrganizationsQueryVariables>> | ReactiveFunction<VueApolloComposable.UseQueryOptions<OrganizationsQuery, OrganizationsQueryVariables>> = {}) {
|
||||||
|
return VueApolloComposable.useLazyQuery<OrganizationsQuery, OrganizationsQueryVariables>(OrganizationsDocument, {}, options)
|
||||||
|
}
|
||||||
|
export type OrganizationsQueryCompositionFunctionResult = VueApolloComposable.UseQueryReturn<OrganizationsQuery, OrganizationsQueryVariables>
|
||||||
|
export const AllOrganizationsDocument = gql`
|
||||||
|
query AllOrganizations {
|
||||||
|
allOrganizations {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
users {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
apiKeys {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
refs
|
||||||
|
read
|
||||||
|
publish
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
/**
|
||||||
|
* __useAllOrganizationsQuery__
|
||||||
|
*
|
||||||
|
* To run a query within a Vue component, call `useAllOrganizationsQuery` and pass it any options that fit your needs.
|
||||||
|
* When your component renders, `useAllOrganizationsQuery` returns an object from Apollo Client that contains result, loading and error properties
|
||||||
|
* you can use to render your UI.
|
||||||
|
*
|
||||||
|
* @param options that will be passed into the query, supported options are listed on: https://v4.apollo.vuejs.org/guide-composable/query.html#options;
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const { result, loading, error } = useAllOrganizationsQuery();
|
||||||
|
*/
|
||||||
|
export function useAllOrganizationsQuery(options: VueApolloComposable.UseQueryOptions<AllOrganizationsQuery, AllOrganizationsQueryVariables> | VueCompositionApi.Ref<VueApolloComposable.UseQueryOptions<AllOrganizationsQuery, AllOrganizationsQueryVariables>> | ReactiveFunction<VueApolloComposable.UseQueryOptions<AllOrganizationsQuery, AllOrganizationsQueryVariables>> = {}) {
|
||||||
|
return VueApolloComposable.useQuery<AllOrganizationsQuery, AllOrganizationsQueryVariables>(AllOrganizationsDocument, {}, options)
|
||||||
|
}
|
||||||
|
export function useAllOrganizationsLazyQuery(options: VueApolloComposable.UseQueryOptions<AllOrganizationsQuery, AllOrganizationsQueryVariables> | VueCompositionApi.Ref<VueApolloComposable.UseQueryOptions<AllOrganizationsQuery, AllOrganizationsQueryVariables>> | ReactiveFunction<VueApolloComposable.UseQueryOptions<AllOrganizationsQuery, AllOrganizationsQueryVariables>> = {}) {
|
||||||
|
return VueApolloComposable.useLazyQuery<AllOrganizationsQuery, AllOrganizationsQueryVariables>(AllOrganizationsDocument, {}, options)
|
||||||
|
}
|
||||||
|
export type AllOrganizationsQueryCompositionFunctionResult = VueApolloComposable.UseQueryReturn<AllOrganizationsQuery, AllOrganizationsQueryVariables>
|
||||||
|
export const LatestSchemaDocument = gql`
|
||||||
|
query LatestSchema($ref: String!) {
|
||||||
|
latestSchema(ref: $ref) {
|
||||||
|
ref
|
||||||
|
id
|
||||||
|
subGraphs {
|
||||||
|
id
|
||||||
|
service
|
||||||
|
url
|
||||||
|
wsUrl
|
||||||
|
sdl
|
||||||
|
changedBy
|
||||||
|
changedAt
|
||||||
|
}
|
||||||
|
cosmoRouterConfig
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
/**
|
||||||
|
* __useLatestSchemaQuery__
|
||||||
|
*
|
||||||
|
* To run a query within a Vue component, call `useLatestSchemaQuery` and pass it any options that fit your needs.
|
||||||
|
* When your component renders, `useLatestSchemaQuery` returns an object from Apollo Client that contains result, loading and error properties
|
||||||
|
* you can use to render your UI.
|
||||||
|
*
|
||||||
|
* @param variables that will be passed into the query
|
||||||
|
* @param options that will be passed into the query, supported options are listed on: https://v4.apollo.vuejs.org/guide-composable/query.html#options;
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const { result, loading, error } = useLatestSchemaQuery({
|
||||||
|
* ref: // value for 'ref'
|
||||||
|
* });
|
||||||
|
*/
|
||||||
|
export function useLatestSchemaQuery(variables: LatestSchemaQueryVariables | VueCompositionApi.Ref<LatestSchemaQueryVariables> | ReactiveFunction<LatestSchemaQueryVariables>, options: VueApolloComposable.UseQueryOptions<LatestSchemaQuery, LatestSchemaQueryVariables> | VueCompositionApi.Ref<VueApolloComposable.UseQueryOptions<LatestSchemaQuery, LatestSchemaQueryVariables>> | ReactiveFunction<VueApolloComposable.UseQueryOptions<LatestSchemaQuery, LatestSchemaQueryVariables>> = {}) {
|
||||||
|
return VueApolloComposable.useQuery<LatestSchemaQuery, LatestSchemaQueryVariables>(LatestSchemaDocument, variables, options)
|
||||||
|
}
|
||||||
|
export function useLatestSchemaLazyQuery(variables?: LatestSchemaQueryVariables | VueCompositionApi.Ref<LatestSchemaQueryVariables> | ReactiveFunction<LatestSchemaQueryVariables>, options: VueApolloComposable.UseQueryOptions<LatestSchemaQuery, LatestSchemaQueryVariables> | VueCompositionApi.Ref<VueApolloComposable.UseQueryOptions<LatestSchemaQuery, LatestSchemaQueryVariables>> | ReactiveFunction<VueApolloComposable.UseQueryOptions<LatestSchemaQuery, LatestSchemaQueryVariables>> = {}) {
|
||||||
|
return VueApolloComposable.useLazyQuery<LatestSchemaQuery, LatestSchemaQueryVariables>(LatestSchemaDocument, variables, options)
|
||||||
|
}
|
||||||
|
export type LatestSchemaQueryCompositionFunctionResult = VueApolloComposable.UseQueryReturn<LatestSchemaQuery, LatestSchemaQueryVariables>
|
||||||
|
export const SupergraphDocument = gql`
|
||||||
|
query Supergraph($ref: String!, $isAfter: String) {
|
||||||
|
supergraph(ref: $ref, isAfter: $isAfter) {
|
||||||
|
... on SubGraphs {
|
||||||
|
id
|
||||||
|
sdl
|
||||||
|
subGraphs {
|
||||||
|
id
|
||||||
|
service
|
||||||
|
url
|
||||||
|
wsUrl
|
||||||
|
sdl
|
||||||
|
changedBy
|
||||||
|
changedAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
... on Unchanged {
|
||||||
|
id
|
||||||
|
minDelaySeconds
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
/**
|
||||||
|
* __useSupergraphQuery__
|
||||||
|
*
|
||||||
|
* To run a query within a Vue component, call `useSupergraphQuery` and pass it any options that fit your needs.
|
||||||
|
* When your component renders, `useSupergraphQuery` returns an object from Apollo Client that contains result, loading and error properties
|
||||||
|
* you can use to render your UI.
|
||||||
|
*
|
||||||
|
* @param variables that will be passed into the query
|
||||||
|
* @param options that will be passed into the query, supported options are listed on: https://v4.apollo.vuejs.org/guide-composable/query.html#options;
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const { result, loading, error } = useSupergraphQuery({
|
||||||
|
* ref: // value for 'ref'
|
||||||
|
* isAfter: // value for 'isAfter'
|
||||||
|
* });
|
||||||
|
*/
|
||||||
|
export function useSupergraphQuery(variables: SupergraphQueryVariables | VueCompositionApi.Ref<SupergraphQueryVariables> | ReactiveFunction<SupergraphQueryVariables>, options: VueApolloComposable.UseQueryOptions<SupergraphQuery, SupergraphQueryVariables> | VueCompositionApi.Ref<VueApolloComposable.UseQueryOptions<SupergraphQuery, SupergraphQueryVariables>> | ReactiveFunction<VueApolloComposable.UseQueryOptions<SupergraphQuery, SupergraphQueryVariables>> = {}) {
|
||||||
|
return VueApolloComposable.useQuery<SupergraphQuery, SupergraphQueryVariables>(SupergraphDocument, variables, options)
|
||||||
|
}
|
||||||
|
export function useSupergraphLazyQuery(variables?: SupergraphQueryVariables | VueCompositionApi.Ref<SupergraphQueryVariables> | ReactiveFunction<SupergraphQueryVariables>, options: VueApolloComposable.UseQueryOptions<SupergraphQuery, SupergraphQueryVariables> | VueCompositionApi.Ref<VueApolloComposable.UseQueryOptions<SupergraphQuery, SupergraphQueryVariables>> | ReactiveFunction<VueApolloComposable.UseQueryOptions<SupergraphQuery, SupergraphQueryVariables>> = {}) {
|
||||||
|
return VueApolloComposable.useLazyQuery<SupergraphQuery, SupergraphQueryVariables>(SupergraphDocument, variables, options)
|
||||||
|
}
|
||||||
|
export type SupergraphQueryCompositionFunctionResult = VueApolloComposable.UseQueryReturn<SupergraphQuery, SupergraphQueryVariables>
|
||||||
|
export const AddOrganizationDocument = gql`
|
||||||
|
mutation AddOrganization($name: String!) {
|
||||||
|
addOrganization(name: $name) {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
users {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
apiKeys {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
refs
|
||||||
|
read
|
||||||
|
publish
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
/**
|
||||||
|
* __useAddOrganizationMutation__
|
||||||
|
*
|
||||||
|
* To run a mutation, you first call `useAddOrganizationMutation` within a Vue component and pass it any options that fit your needs.
|
||||||
|
* When your component renders, `useAddOrganizationMutation` returns an object that includes:
|
||||||
|
* - A mutate function that you can call at any time to execute the mutation
|
||||||
|
* - Several other properties: https://v4.apollo.vuejs.org/api/use-mutation.html#return
|
||||||
|
*
|
||||||
|
* @param options that will be passed into the mutation, supported options are listed on: https://v4.apollo.vuejs.org/guide-composable/mutation.html#options;
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const { mutate, loading, error, onDone } = useAddOrganizationMutation({
|
||||||
|
* variables: {
|
||||||
|
* name: // value for 'name'
|
||||||
|
* },
|
||||||
|
* });
|
||||||
|
*/
|
||||||
|
export function useAddOrganizationMutation(options: VueApolloComposable.UseMutationOptions<AddOrganizationMutation, AddOrganizationMutationVariables> | ReactiveFunction<VueApolloComposable.UseMutationOptions<AddOrganizationMutation, AddOrganizationMutationVariables>> = {}) {
|
||||||
|
return VueApolloComposable.useMutation<AddOrganizationMutation, AddOrganizationMutationVariables>(AddOrganizationDocument, options)
|
||||||
|
}
|
||||||
|
export type AddOrganizationMutationCompositionFunctionResult = VueApolloComposable.UseMutationReturn<AddOrganizationMutation, AddOrganizationMutationVariables>
|
||||||
|
export const AddUserToOrganizationDocument = gql`
|
||||||
|
mutation AddUserToOrganization($organizationId: ID!, $userId: String!) {
|
||||||
|
addUserToOrganization(organizationId: $organizationId, userId: $userId) {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
users {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
apiKeys {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
refs
|
||||||
|
read
|
||||||
|
publish
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
/**
|
||||||
|
* __useAddUserToOrganizationMutation__
|
||||||
|
*
|
||||||
|
* To run a mutation, you first call `useAddUserToOrganizationMutation` within a Vue component and pass it any options that fit your needs.
|
||||||
|
* When your component renders, `useAddUserToOrganizationMutation` returns an object that includes:
|
||||||
|
* - A mutate function that you can call at any time to execute the mutation
|
||||||
|
* - Several other properties: https://v4.apollo.vuejs.org/api/use-mutation.html#return
|
||||||
|
*
|
||||||
|
* @param options that will be passed into the mutation, supported options are listed on: https://v4.apollo.vuejs.org/guide-composable/mutation.html#options;
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const { mutate, loading, error, onDone } = useAddUserToOrganizationMutation({
|
||||||
|
* variables: {
|
||||||
|
* organizationId: // value for 'organizationId'
|
||||||
|
* userId: // value for 'userId'
|
||||||
|
* },
|
||||||
|
* });
|
||||||
|
*/
|
||||||
|
export function useAddUserToOrganizationMutation(options: VueApolloComposable.UseMutationOptions<AddUserToOrganizationMutation, AddUserToOrganizationMutationVariables> | ReactiveFunction<VueApolloComposable.UseMutationOptions<AddUserToOrganizationMutation, AddUserToOrganizationMutationVariables>> = {}) {
|
||||||
|
return VueApolloComposable.useMutation<AddUserToOrganizationMutation, AddUserToOrganizationMutationVariables>(AddUserToOrganizationDocument, options)
|
||||||
|
}
|
||||||
|
export type AddUserToOrganizationMutationCompositionFunctionResult = VueApolloComposable.UseMutationReturn<AddUserToOrganizationMutation, AddUserToOrganizationMutationVariables>
|
||||||
|
export const AddApiKeyDocument = gql`
|
||||||
|
mutation AddAPIKey($input: InputAPIKey!) {
|
||||||
|
addAPIKey(input: $input) {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
key
|
||||||
|
organization {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
}
|
||||||
|
refs
|
||||||
|
read
|
||||||
|
publish
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
/**
|
||||||
|
* __useAddApiKeyMutation__
|
||||||
|
*
|
||||||
|
* To run a mutation, you first call `useAddApiKeyMutation` within a Vue component and pass it any options that fit your needs.
|
||||||
|
* When your component renders, `useAddApiKeyMutation` returns an object that includes:
|
||||||
|
* - A mutate function that you can call at any time to execute the mutation
|
||||||
|
* - Several other properties: https://v4.apollo.vuejs.org/api/use-mutation.html#return
|
||||||
|
*
|
||||||
|
* @param options that will be passed into the mutation, supported options are listed on: https://v4.apollo.vuejs.org/guide-composable/mutation.html#options;
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const { mutate, loading, error, onDone } = useAddApiKeyMutation({
|
||||||
|
* variables: {
|
||||||
|
* input: // value for 'input'
|
||||||
|
* },
|
||||||
|
* });
|
||||||
|
*/
|
||||||
|
export function useAddApiKeyMutation(options: VueApolloComposable.UseMutationOptions<AddApiKeyMutation, AddApiKeyMutationVariables> | ReactiveFunction<VueApolloComposable.UseMutationOptions<AddApiKeyMutation, AddApiKeyMutationVariables>> = {}) {
|
||||||
|
return VueApolloComposable.useMutation<AddApiKeyMutation, AddApiKeyMutationVariables>(AddApiKeyDocument, options)
|
||||||
|
}
|
||||||
|
export type AddApiKeyMutationCompositionFunctionResult = VueApolloComposable.UseMutationReturn<AddApiKeyMutation, AddApiKeyMutationVariables>
|
||||||
|
export const RemoveApiKeyDocument = gql`
|
||||||
|
mutation RemoveAPIKey($organizationId: ID!, $keyName: String!) {
|
||||||
|
removeAPIKey(organizationId: $organizationId, keyName: $keyName) {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
users {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
apiKeys {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
refs
|
||||||
|
read
|
||||||
|
publish
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
/**
|
||||||
|
* __useRemoveApiKeyMutation__
|
||||||
|
*
|
||||||
|
* To run a mutation, you first call `useRemoveApiKeyMutation` within a Vue component and pass it any options that fit your needs.
|
||||||
|
* When your component renders, `useRemoveApiKeyMutation` returns an object that includes:
|
||||||
|
* - A mutate function that you can call at any time to execute the mutation
|
||||||
|
* - Several other properties: https://v4.apollo.vuejs.org/api/use-mutation.html#return
|
||||||
|
*
|
||||||
|
* @param options that will be passed into the mutation, supported options are listed on: https://v4.apollo.vuejs.org/guide-composable/mutation.html#options;
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const { mutate, loading, error, onDone } = useRemoveApiKeyMutation({
|
||||||
|
* variables: {
|
||||||
|
* organizationId: // value for 'organizationId'
|
||||||
|
* keyName: // value for 'keyName'
|
||||||
|
* },
|
||||||
|
* });
|
||||||
|
*/
|
||||||
|
export function useRemoveApiKeyMutation(options: VueApolloComposable.UseMutationOptions<RemoveApiKeyMutation, RemoveApiKeyMutationVariables> | ReactiveFunction<VueApolloComposable.UseMutationOptions<RemoveApiKeyMutation, RemoveApiKeyMutationVariables>> = {}) {
|
||||||
|
return VueApolloComposable.useMutation<RemoveApiKeyMutation, RemoveApiKeyMutationVariables>(RemoveApiKeyDocument, options)
|
||||||
|
}
|
||||||
|
export type RemoveApiKeyMutationCompositionFunctionResult = VueApolloComposable.UseMutationReturn<RemoveApiKeyMutation, RemoveApiKeyMutationVariables>
|
||||||
|
export const RemoveOrganizationDocument = gql`
|
||||||
|
mutation RemoveOrganization($organizationId: ID!) {
|
||||||
|
removeOrganization(organizationId: $organizationId)
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
/**
|
||||||
|
* __useRemoveOrganizationMutation__
|
||||||
|
*
|
||||||
|
* To run a mutation, you first call `useRemoveOrganizationMutation` within a Vue component and pass it any options that fit your needs.
|
||||||
|
* When your component renders, `useRemoveOrganizationMutation` returns an object that includes:
|
||||||
|
* - A mutate function that you can call at any time to execute the mutation
|
||||||
|
* - Several other properties: https://v4.apollo.vuejs.org/api/use-mutation.html#return
|
||||||
|
*
|
||||||
|
* @param options that will be passed into the mutation, supported options are listed on: https://v4.apollo.vuejs.org/guide-composable/mutation.html#options;
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const { mutate, loading, error, onDone } = useRemoveOrganizationMutation({
|
||||||
|
* variables: {
|
||||||
|
* organizationId: // value for 'organizationId'
|
||||||
|
* },
|
||||||
|
* });
|
||||||
|
*/
|
||||||
|
export function useRemoveOrganizationMutation(options: VueApolloComposable.UseMutationOptions<RemoveOrganizationMutation, RemoveOrganizationMutationVariables> | ReactiveFunction<VueApolloComposable.UseMutationOptions<RemoveOrganizationMutation, RemoveOrganizationMutationVariables>> = {}) {
|
||||||
|
return VueApolloComposable.useMutation<RemoveOrganizationMutation, RemoveOrganizationMutationVariables>(RemoveOrganizationDocument, options)
|
||||||
|
}
|
||||||
|
export type RemoveOrganizationMutationCompositionFunctionResult = VueApolloComposable.UseMutationReturn<RemoveOrganizationMutation, RemoveOrganizationMutationVariables>
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
query Organizations {
|
||||||
|
organizations {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
users {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
apiKeys {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
refs
|
||||||
|
read
|
||||||
|
publish
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
query AllOrganizations {
|
||||||
|
allOrganizations {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
users {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
apiKeys {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
refs
|
||||||
|
read
|
||||||
|
publish
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
query LatestSchema($ref: String!) {
|
||||||
|
latestSchema(ref: $ref) {
|
||||||
|
ref
|
||||||
|
id
|
||||||
|
subGraphs {
|
||||||
|
id
|
||||||
|
service
|
||||||
|
url
|
||||||
|
wsUrl
|
||||||
|
sdl
|
||||||
|
changedBy
|
||||||
|
changedAt
|
||||||
|
}
|
||||||
|
cosmoRouterConfig
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
query Supergraph($ref: String!, $isAfter: String) {
|
||||||
|
supergraph(ref: $ref, isAfter: $isAfter) {
|
||||||
|
... on SubGraphs {
|
||||||
|
id
|
||||||
|
sdl
|
||||||
|
subGraphs {
|
||||||
|
id
|
||||||
|
service
|
||||||
|
url
|
||||||
|
wsUrl
|
||||||
|
sdl
|
||||||
|
changedBy
|
||||||
|
changedAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
... on Unchanged {
|
||||||
|
id
|
||||||
|
minDelaySeconds
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mutation AddOrganization($name: String!) {
|
||||||
|
addOrganization(name: $name) {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
users {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
apiKeys {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
refs
|
||||||
|
read
|
||||||
|
publish
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mutation AddUserToOrganization($organizationId: ID!, $userId: String!) {
|
||||||
|
addUserToOrganization(organizationId: $organizationId, userId: $userId) {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
users {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
apiKeys {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
refs
|
||||||
|
read
|
||||||
|
publish
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mutation AddAPIKey($input: InputAPIKey!) {
|
||||||
|
addAPIKey(input: $input) {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
key
|
||||||
|
organization {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
}
|
||||||
|
refs
|
||||||
|
read
|
||||||
|
publish
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mutation RemoveAPIKey($organizationId: ID!, $keyName: String!) {
|
||||||
|
removeAPIKey(organizationId: $organizationId, keyName: $keyName) {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
users {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
apiKeys {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
refs
|
||||||
|
read
|
||||||
|
publish
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mutation RemoveOrganization($organizationId: ID!) {
|
||||||
|
removeOrganization(organizationId: $organizationId)
|
||||||
|
}
|
||||||
@@ -0,0 +1,363 @@
|
|||||||
|
<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>
|
||||||
@@ -0,0 +1,256 @@
|
|||||||
|
<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>
|
||||||
@@ -0,0 +1,219 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-btn
|
||||||
|
prepend-icon="mdi-arrow-left"
|
||||||
|
variant="text"
|
||||||
|
@click="navigateTo('/schemas')"
|
||||||
|
>
|
||||||
|
Back to Schemas
|
||||||
|
</v-btn>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-row v-if="schema">
|
||||||
|
<v-col cols="12">
|
||||||
|
<h1 class="text-h3 mb-2">{{ schema.service }}</h1>
|
||||||
|
<v-chip class="mr-2">{{ schema.ref }}</v-chip>
|
||||||
|
<v-chip color="success">Active</v-chip>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-row v-if="schema">
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-card>
|
||||||
|
<v-card-title>Details</v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
<v-list>
|
||||||
|
<v-list-item>
|
||||||
|
<v-list-item-title>Service</v-list-item-title>
|
||||||
|
<v-list-item-subtitle>{{ schema.service }}</v-list-item-subtitle>
|
||||||
|
</v-list-item>
|
||||||
|
<v-list-item>
|
||||||
|
<v-list-item-title>Ref</v-list-item-title>
|
||||||
|
<v-list-item-subtitle>{{ schema.ref }}</v-list-item-subtitle>
|
||||||
|
</v-list-item>
|
||||||
|
<v-list-item>
|
||||||
|
<v-list-item-title>URL</v-list-item-title>
|
||||||
|
<v-list-item-subtitle>{{ schema.url }}</v-list-item-subtitle>
|
||||||
|
</v-list-item>
|
||||||
|
<v-list-item v-if="schema.wsUrl">
|
||||||
|
<v-list-item-title>WebSocket URL</v-list-item-title>
|
||||||
|
<v-list-item-subtitle>{{ schema.wsUrl }}</v-list-item-subtitle>
|
||||||
|
</v-list-item>
|
||||||
|
<v-list-item>
|
||||||
|
<v-list-item-title>Last Updated</v-list-item-title>
|
||||||
|
<v-list-item-subtitle>{{ schema.updatedAt }}</v-list-item-subtitle>
|
||||||
|
</v-list-item>
|
||||||
|
<v-list-item>
|
||||||
|
<v-list-item-title>Updated By</v-list-item-title>
|
||||||
|
<v-list-item-subtitle>{{ schema.updatedBy }}</v-list-item-subtitle>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-card>
|
||||||
|
<v-card-title>Actions</v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
<v-btn
|
||||||
|
block
|
||||||
|
prepend-icon="mdi-download"
|
||||||
|
variant="outlined"
|
||||||
|
class="mb-2"
|
||||||
|
@click="downloadSchema"
|
||||||
|
>
|
||||||
|
Download SDL
|
||||||
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
block
|
||||||
|
prepend-icon="mdi-content-copy"
|
||||||
|
variant="outlined"
|
||||||
|
class="mb-2"
|
||||||
|
@click="copyToClipboard"
|
||||||
|
>
|
||||||
|
Copy SDL
|
||||||
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
block
|
||||||
|
prepend-icon="mdi-graph"
|
||||||
|
variant="outlined"
|
||||||
|
color="primary"
|
||||||
|
>
|
||||||
|
View Federation Graph
|
||||||
|
</v-btn>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-row v-if="schema">
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-card>
|
||||||
|
<v-card-title class="d-flex justify-space-between align-center">
|
||||||
|
<span>Schema Definition (SDL)</span>
|
||||||
|
<v-btn
|
||||||
|
icon="mdi-content-copy"
|
||||||
|
variant="text"
|
||||||
|
size="small"
|
||||||
|
@click="copyToClipboard"
|
||||||
|
/>
|
||||||
|
</v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
<pre class="sdl-viewer"><code>{{ schema.sdl }}</code></pre>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-row v-if="!schema && !loading">
|
||||||
|
<v-col cols="12" class="text-center py-12">
|
||||||
|
<v-icon icon="mdi-alert-circle" size="64" class="mb-4 text-warning" />
|
||||||
|
<p class="text-h6">Schema not found</p>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-snackbar v-model="snackbar" :timeout="2000">
|
||||||
|
{{ snackbarText }}
|
||||||
|
</v-snackbar>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const route = useRoute()
|
||||||
|
const _config = useRuntimeConfig()
|
||||||
|
|
||||||
|
const schema = ref<any>(null)
|
||||||
|
const loading = ref(true)
|
||||||
|
const snackbar = ref(false)
|
||||||
|
const snackbarText = ref('')
|
||||||
|
|
||||||
|
const downloadSchema = () => {
|
||||||
|
if (!schema.value) return
|
||||||
|
|
||||||
|
const blob = new Blob([schema.value.sdl], { type: 'text/plain' })
|
||||||
|
const url = URL.createObjectURL(blob)
|
||||||
|
const a = document.createElement('a')
|
||||||
|
a.href = url
|
||||||
|
a.download = `${schema.value.service}-${schema.value.ref}.graphql`
|
||||||
|
a.click()
|
||||||
|
URL.revokeObjectURL(url)
|
||||||
|
|
||||||
|
snackbarText.value = 'Schema downloaded'
|
||||||
|
snackbar.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const copyToClipboard = async () => {
|
||||||
|
if (!schema.value) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(schema.value.sdl)
|
||||||
|
snackbarText.value = 'SDL copied to clipboard'
|
||||||
|
snackbar.value = true
|
||||||
|
} catch (_err) {
|
||||||
|
snackbarText.value = 'Failed to copy'
|
||||||
|
snackbar.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Fetch actual data from the GraphQL API
|
||||||
|
onMounted(() => {
|
||||||
|
// Mock data for now
|
||||||
|
setTimeout(() => {
|
||||||
|
schema.value = {
|
||||||
|
id: route.params.id,
|
||||||
|
service: 'users-service',
|
||||||
|
ref: 'production',
|
||||||
|
url: 'http://users.example.com/graphql',
|
||||||
|
wsUrl: 'ws://users.example.com/graphql',
|
||||||
|
updatedAt: '2024-11-21 19:30:00',
|
||||||
|
updatedBy: 'john.doe@example.com',
|
||||||
|
sdl: `type User @key(fields: "id") {
|
||||||
|
id: ID!
|
||||||
|
username: String!
|
||||||
|
email: String!
|
||||||
|
createdAt: DateTime!
|
||||||
|
}
|
||||||
|
|
||||||
|
type Query {
|
||||||
|
user(id: ID!): User
|
||||||
|
users(limit: Int, offset: Int): [User!]!
|
||||||
|
}
|
||||||
|
|
||||||
|
type Mutation {
|
||||||
|
createUser(input: CreateUserInput!): User!
|
||||||
|
updateUser(id: ID!, input: UpdateUserInput!): User!
|
||||||
|
deleteUser(id: ID!): Boolean!
|
||||||
|
}
|
||||||
|
|
||||||
|
input CreateUserInput {
|
||||||
|
username: String!
|
||||||
|
email: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
input UpdateUserInput {
|
||||||
|
username: String
|
||||||
|
email: String
|
||||||
|
}
|
||||||
|
|
||||||
|
scalar DateTime`,
|
||||||
|
}
|
||||||
|
loading.value = false
|
||||||
|
}, 500)
|
||||||
|
})
|
||||||
|
</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;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,163 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12">
|
||||||
|
<h1 class="text-h3 mb-4">Published Schemas</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 published schemas.
|
||||||
|
</v-alert>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-row v-else>
|
||||||
|
<v-col cols="12" md="3">
|
||||||
|
<v-select
|
||||||
|
v-model="selectedRef"
|
||||||
|
:items="refs"
|
||||||
|
label="Select Ref"
|
||||||
|
prepend-icon="mdi-source-branch"
|
||||||
|
variant="outlined"
|
||||||
|
:disabled="!selectedOrganization"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="9">
|
||||||
|
<v-text-field
|
||||||
|
v-model="search"
|
||||||
|
label="Search schemas"
|
||||||
|
prepend-icon="mdi-magnify"
|
||||||
|
variant="outlined"
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-row v-if="auth0.isAuthenticated.value && latestSchema.loading.value">
|
||||||
|
<v-col cols="12" class="text-center">
|
||||||
|
<v-progress-circular indeterminate size="64" />
|
||||||
|
<p class="mt-4">Loading schemas for {{ selectedRef }}...</p>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-row v-else-if="auth0.isAuthenticated.value && latestSchema.error.value">
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-alert type="error" variant="tonal">
|
||||||
|
Error loading schemas: {{ latestSchema.error.value.message }}
|
||||||
|
</v-alert>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-row v-else-if="auth0.isAuthenticated.value">
|
||||||
|
<v-col
|
||||||
|
v-for="schema in filteredSchemas"
|
||||||
|
:key="schema.id"
|
||||||
|
cols="12"
|
||||||
|
md="6"
|
||||||
|
lg="4"
|
||||||
|
>
|
||||||
|
<v-card hover @click="navigateTo(`/schemas/${schema.id}`)">
|
||||||
|
<v-card-title>
|
||||||
|
<v-icon icon="mdi-graphql" class="mr-2" />
|
||||||
|
{{ schema.service }}
|
||||||
|
</v-card-title>
|
||||||
|
<v-card-subtitle>{{ schema.ref }}</v-card-subtitle>
|
||||||
|
<v-card-text>
|
||||||
|
<div class="mb-2">
|
||||||
|
<strong>URL:</strong> {{ schema.url }}
|
||||||
|
</div>
|
||||||
|
<div class="text-caption text-grey">
|
||||||
|
Updated: {{ schema.updatedAt }}
|
||||||
|
</div>
|
||||||
|
</v-card-text>
|
||||||
|
<v-card-actions>
|
||||||
|
<v-btn variant="text" color="primary">
|
||||||
|
View Schema
|
||||||
|
</v-btn>
|
||||||
|
<v-spacer />
|
||||||
|
<v-chip size="small" color="success">Active</v-chip>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
|
||||||
|
<v-col v-if="filteredSchemas.length === 0" cols="12" class="text-center py-12">
|
||||||
|
<v-icon icon="mdi-graphql" size="64" class="mb-4 text-grey" />
|
||||||
|
<p class="text-h6 text-grey">No schemas found</p>
|
||||||
|
<p class="text-grey">{{ search ? 'Try a different search term' : 'Publish your first schema to get started' }}</p>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useAuth0 } from '@auth0/auth0-vue'
|
||||||
|
import { watch } from 'vue'
|
||||||
|
|
||||||
|
import { useOrganizationSelector } from '~/composables/useOrganizationSelector'
|
||||||
|
import {
|
||||||
|
useLatestSchemaQuery,
|
||||||
|
} from '~/graphql/generated'
|
||||||
|
|
||||||
|
const auth0 = useAuth0()
|
||||||
|
const { selectedOrganization } = useOrganizationSelector()
|
||||||
|
|
||||||
|
const selectedRef = ref('production')
|
||||||
|
const search = ref('')
|
||||||
|
|
||||||
|
// Get available refs from the selected organization
|
||||||
|
const refs = computed(() => {
|
||||||
|
if (!selectedOrganization.value) return ['production', 'staging', 'development']
|
||||||
|
|
||||||
|
const allRefs = new Set<string>()
|
||||||
|
selectedOrganization.value.apiKeys?.forEach(key => {
|
||||||
|
key.refs?.forEach(ref => allRefs.add(ref))
|
||||||
|
})
|
||||||
|
|
||||||
|
return Array.from(allRefs).length > 0 ? Array.from(allRefs) : ['production', 'staging', 'development']
|
||||||
|
})
|
||||||
|
|
||||||
|
// Fetch schema for selected ref
|
||||||
|
const latestSchema = useLatestSchemaQuery(() => ({
|
||||||
|
ref: selectedRef.value,
|
||||||
|
}), () => ({
|
||||||
|
skip: !auth0.isAuthenticated.value || !selectedRef.value,
|
||||||
|
}))
|
||||||
|
|
||||||
|
const schemas = computed(() => {
|
||||||
|
if (!latestSchema.result.value?.latestSchema?.subGraphs) return []
|
||||||
|
|
||||||
|
return latestSchema.result.value.latestSchema.subGraphs.map(subgraph => ({
|
||||||
|
id: subgraph.id,
|
||||||
|
service: subgraph.service,
|
||||||
|
ref: selectedRef.value,
|
||||||
|
url: subgraph.url || 'N/A',
|
||||||
|
wsUrl: subgraph.wsUrl,
|
||||||
|
updatedAt: new Date(subgraph.changedAt).toLocaleString(),
|
||||||
|
changedBy: subgraph.changedBy,
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
const filteredSchemas = computed(() => {
|
||||||
|
let filtered = schemas.value
|
||||||
|
|
||||||
|
if (search.value) {
|
||||||
|
const searchLower = search.value.toLowerCase()
|
||||||
|
filtered = filtered.filter(s =>
|
||||||
|
s.service.toLowerCase().includes(searchLower) ||
|
||||||
|
s.url.toLowerCase().includes(searchLower),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return filtered
|
||||||
|
})
|
||||||
|
|
||||||
|
// Watch for ref changes and ensure the first ref is selected
|
||||||
|
watch(refs, (newRefs) => {
|
||||||
|
if (newRefs.length > 0 && !newRefs.includes(selectedRef.value)) {
|
||||||
|
selectedRef.value = newRefs[0]
|
||||||
|
}
|
||||||
|
}, { immediate: true })
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,716 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12">
|
||||||
|
<h1 class="text-h3 mb-4">Settings</h1>
|
||||||
|
<p class="text-subtitle-1 text-grey mb-4">
|
||||||
|
Manage API keys and organization settings
|
||||||
|
</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 manage settings.
|
||||||
|
</v-alert>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-row v-else-if="!selectedOrganization">
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-alert type="info" variant="tonal">
|
||||||
|
Please select an organization to manage API keys.
|
||||||
|
</v-alert>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-row v-else>
|
||||||
|
<!-- Organizations Section -->
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-card class="mb-4">
|
||||||
|
<v-card-title class="d-flex justify-space-between align-center">
|
||||||
|
<span>Organizations</span>
|
||||||
|
<v-btn
|
||||||
|
v-if="isAdmin"
|
||||||
|
color="primary"
|
||||||
|
prepend-icon="mdi-plus"
|
||||||
|
@click="openAddOrgDialog"
|
||||||
|
>
|
||||||
|
Create Organization
|
||||||
|
</v-btn>
|
||||||
|
</v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
<v-list v-if="organizations.length > 0">
|
||||||
|
<v-list-group
|
||||||
|
v-for="org in organizations"
|
||||||
|
:key="org.id"
|
||||||
|
class="mb-2"
|
||||||
|
>
|
||||||
|
<template #activator="{ props }">
|
||||||
|
<v-list-item
|
||||||
|
v-bind="props"
|
||||||
|
:active="org.id === selectedOrganization?.id"
|
||||||
|
border
|
||||||
|
>
|
||||||
|
<template #prepend>
|
||||||
|
<v-icon icon="mdi-domain" class="mr-2" />
|
||||||
|
</template>
|
||||||
|
<v-list-item-title class="font-weight-bold">
|
||||||
|
{{ org.name }}
|
||||||
|
</v-list-item-title>
|
||||||
|
<v-list-item-subtitle>
|
||||||
|
<v-chip size="small" class="mr-2" variant="tonal">
|
||||||
|
{{ org.apiKeys?.length || 0 }} API keys
|
||||||
|
</v-chip>
|
||||||
|
<v-chip size="small" variant="tonal">
|
||||||
|
{{ org.users?.length || 0 }} users
|
||||||
|
</v-chip>
|
||||||
|
</v-list-item-subtitle>
|
||||||
|
<template v-if="isAdmin" #append>
|
||||||
|
<v-btn
|
||||||
|
icon="mdi-delete"
|
||||||
|
variant="text"
|
||||||
|
color="error"
|
||||||
|
size="small"
|
||||||
|
@click.stop="openDeleteOrgDialog(org)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</v-list-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Expandable content showing users -->
|
||||||
|
<v-list-item>
|
||||||
|
<v-list-item-title class="text-subtitle-2 mb-2">
|
||||||
|
<v-icon icon="mdi-account-multiple" size="small" class="mr-1" />
|
||||||
|
Users
|
||||||
|
</v-list-item-title>
|
||||||
|
<v-list density="compact" class="ml-4">
|
||||||
|
<v-list-item
|
||||||
|
v-for="user in org.users"
|
||||||
|
:key="user.id"
|
||||||
|
class="text-caption"
|
||||||
|
>
|
||||||
|
<template #prepend>
|
||||||
|
<v-icon :icon="getUserProviderIcon(user.id)" size="small" class="mr-2" />
|
||||||
|
</template>
|
||||||
|
<v-list-item-title class="text-body-2">
|
||||||
|
<v-chip v-if="getUserProvider(user.id)" size="x-small" variant="tonal" class="mr-2">
|
||||||
|
{{ getUserProvider(user.id) }}
|
||||||
|
</v-chip>
|
||||||
|
{{ getUserId(user.id) }}
|
||||||
|
</v-list-item-title>
|
||||||
|
<v-list-item-subtitle class="text-caption">
|
||||||
|
<span class="text-grey">Full ID: {{ user.id }}</span>
|
||||||
|
</v-list-item-subtitle>
|
||||||
|
</v-list-item>
|
||||||
|
<v-list-item v-if="!org.users || org.users.length === 0">
|
||||||
|
<v-list-item-title class="text-caption text-grey">
|
||||||
|
No users
|
||||||
|
</v-list-item-title>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list-group>
|
||||||
|
</v-list>
|
||||||
|
<v-alert v-else type="info" variant="tonal">
|
||||||
|
<div class="d-flex flex-column align-center">
|
||||||
|
<p>You don't belong to any organizations yet.</p>
|
||||||
|
<p v-if="isAdmin" class="text-caption">Create one to get started.</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>
|
||||||
|
|
||||||
|
<!-- API Keys Section -->
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-card>
|
||||||
|
<v-card-title class="d-flex justify-space-between align-center">
|
||||||
|
<span>API Keys - {{ selectedOrganization.name }}</span>
|
||||||
|
<v-btn
|
||||||
|
color="primary"
|
||||||
|
prepend-icon="mdi-plus"
|
||||||
|
@click="openAddKeyDialog"
|
||||||
|
>
|
||||||
|
Add API Key
|
||||||
|
</v-btn>
|
||||||
|
</v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
<v-list v-if="selectedOrganization.apiKeys && selectedOrganization.apiKeys.length > 0">
|
||||||
|
<v-list-item
|
||||||
|
v-for="apiKey in selectedOrganization.apiKeys"
|
||||||
|
:key="apiKey.id"
|
||||||
|
class="mb-2"
|
||||||
|
border
|
||||||
|
>
|
||||||
|
<template #prepend>
|
||||||
|
<v-icon icon="mdi-key" class="mr-2" />
|
||||||
|
</template>
|
||||||
|
<v-list-item-title class="font-weight-bold">
|
||||||
|
{{ apiKey.name }}
|
||||||
|
</v-list-item-title>
|
||||||
|
<v-list-item-subtitle>
|
||||||
|
<div class="mt-2">
|
||||||
|
<v-chip size="small" class="mr-2" variant="tonal">
|
||||||
|
{{ apiKey.refs?.length || 0 }} refs
|
||||||
|
</v-chip>
|
||||||
|
<v-chip
|
||||||
|
v-if="apiKey.read"
|
||||||
|
size="small"
|
||||||
|
class="mr-2"
|
||||||
|
color="blue"
|
||||||
|
variant="tonal"
|
||||||
|
>
|
||||||
|
Read
|
||||||
|
</v-chip>
|
||||||
|
<v-chip
|
||||||
|
v-if="apiKey.publish"
|
||||||
|
size="small"
|
||||||
|
color="green"
|
||||||
|
variant="tonal"
|
||||||
|
>
|
||||||
|
Publish
|
||||||
|
</v-chip>
|
||||||
|
</div>
|
||||||
|
<div v-if="apiKey.refs && apiKey.refs.length > 0" class="mt-2">
|
||||||
|
<strong>Refs:</strong> {{ apiKey.refs.join(', ') }}
|
||||||
|
</div>
|
||||||
|
</v-list-item-subtitle>
|
||||||
|
<template #append>
|
||||||
|
<v-btn
|
||||||
|
icon="mdi-delete"
|
||||||
|
variant="text"
|
||||||
|
color="error"
|
||||||
|
size="small"
|
||||||
|
@click.stop="openDeleteKeyDialog(apiKey)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
<v-alert v-else type="info" variant="tonal">
|
||||||
|
No API keys yet. Create one to get started.
|
||||||
|
</v-alert>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<!-- Create Organization Dialog -->
|
||||||
|
<v-dialog v-model="addOrgDialog" 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"
|
||||||
|
hint="A unique name for your organization"
|
||||||
|
persistent-hint
|
||||||
|
@keyup.enter="createOrganization"
|
||||||
|
/>
|
||||||
|
</v-card-text>
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer />
|
||||||
|
<v-btn variant="text" @click="addOrgDialog = 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>
|
||||||
|
|
||||||
|
<!-- Add API Key Dialog -->
|
||||||
|
<v-dialog v-model="addKeyDialog" max-width="600">
|
||||||
|
<v-card>
|
||||||
|
<v-card-title>Create API Key</v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
<v-text-field
|
||||||
|
v-model="newKeyName"
|
||||||
|
label="Key Name"
|
||||||
|
variant="outlined"
|
||||||
|
:error-messages="keyNameError"
|
||||||
|
class="mb-4"
|
||||||
|
hint="A descriptive name for this API key"
|
||||||
|
persistent-hint
|
||||||
|
/>
|
||||||
|
|
||||||
|
<v-combobox
|
||||||
|
v-model="newKeyRefs"
|
||||||
|
label="Refs"
|
||||||
|
variant="outlined"
|
||||||
|
multiple
|
||||||
|
chips
|
||||||
|
:error-messages="refsError"
|
||||||
|
class="mb-4"
|
||||||
|
hint="Which refs this key can access (e.g., production, staging)"
|
||||||
|
persistent-hint
|
||||||
|
/>
|
||||||
|
|
||||||
|
<v-checkbox
|
||||||
|
v-model="newKeyRead"
|
||||||
|
label="Read Access"
|
||||||
|
hint="Allow reading schemas and supergraphs"
|
||||||
|
persistent-hint
|
||||||
|
/>
|
||||||
|
|
||||||
|
<v-checkbox
|
||||||
|
v-model="newKeyPublish"
|
||||||
|
label="Publish Access"
|
||||||
|
hint="Allow publishing/updating subgraph schemas"
|
||||||
|
persistent-hint
|
||||||
|
/>
|
||||||
|
</v-card-text>
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer />
|
||||||
|
<v-btn variant="text" @click="addKeyDialog = false">Cancel</v-btn>
|
||||||
|
<v-btn
|
||||||
|
color="primary"
|
||||||
|
variant="flat"
|
||||||
|
:loading="creatingKey"
|
||||||
|
:disabled="!canCreateKey"
|
||||||
|
@click="createAPIKey"
|
||||||
|
>
|
||||||
|
Create Key
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
|
||||||
|
<!-- Show API Key Dialog (only shown once after creation) -->
|
||||||
|
<v-dialog v-model="showKeyDialog" max-width="600" persistent>
|
||||||
|
<v-card>
|
||||||
|
<v-card-title class="d-flex align-center">
|
||||||
|
<v-icon icon="mdi-alert" color="warning" class="mr-2" />
|
||||||
|
Save Your API Key
|
||||||
|
</v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
<v-alert type="warning" variant="tonal" class="mb-4">
|
||||||
|
This is the only time you'll see this key. Please copy it now and store it securely.
|
||||||
|
</v-alert>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<strong>Key Name:</strong> {{ createdKey?.name }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<v-text-field
|
||||||
|
:model-value="createdKey?.key"
|
||||||
|
label="API Key"
|
||||||
|
variant="outlined"
|
||||||
|
readonly
|
||||||
|
class="mb-2"
|
||||||
|
>
|
||||||
|
<template #append-inner>
|
||||||
|
<v-btn
|
||||||
|
icon="mdi-content-copy"
|
||||||
|
variant="text"
|
||||||
|
size="small"
|
||||||
|
@click="copyKey"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</v-text-field>
|
||||||
|
|
||||||
|
<div class="text-caption text-grey">
|
||||||
|
Use this key in the X-API-Key header when making requests to the schema registry.
|
||||||
|
</div>
|
||||||
|
</v-card-text>
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer />
|
||||||
|
<v-btn
|
||||||
|
color="primary"
|
||||||
|
variant="flat"
|
||||||
|
@click="closeKeyDialog"
|
||||||
|
>
|
||||||
|
I've Saved the Key
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
|
||||||
|
<!-- Delete Organization Confirmation Dialog -->
|
||||||
|
<v-dialog v-model="deleteOrgDialog" max-width="500">
|
||||||
|
<v-card>
|
||||||
|
<v-card-title class="text-error">
|
||||||
|
<v-icon icon="mdi-alert" color="error" class="mr-2" />
|
||||||
|
Delete Organization
|
||||||
|
</v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
<v-alert type="warning" variant="tonal" class="mb-4">
|
||||||
|
This action cannot be undone. All API keys and data for this organization will be permanently removed.
|
||||||
|
</v-alert>
|
||||||
|
<p>Are you sure you want to delete <strong>{{ orgToDelete?.name }}</strong>?</p>
|
||||||
|
</v-card-text>
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer />
|
||||||
|
<v-btn variant="text" @click="deleteOrgDialog = false">Cancel</v-btn>
|
||||||
|
<v-btn
|
||||||
|
color="error"
|
||||||
|
variant="flat"
|
||||||
|
:loading="deletingOrg"
|
||||||
|
@click="deleteOrganization"
|
||||||
|
>
|
||||||
|
Delete Organization
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
|
||||||
|
<!-- Delete API Key Confirmation Dialog -->
|
||||||
|
<v-dialog v-model="deleteKeyDialog" max-width="500">
|
||||||
|
<v-card>
|
||||||
|
<v-card-title class="text-error">
|
||||||
|
<v-icon icon="mdi-alert" color="error" class="mr-2" />
|
||||||
|
Delete API Key
|
||||||
|
</v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
<v-alert type="warning" variant="tonal" class="mb-4">
|
||||||
|
This action cannot be undone. Services using this key will lose access.
|
||||||
|
</v-alert>
|
||||||
|
<p>Are you sure you want to delete the API key <strong>{{ keyToDelete?.name }}</strong>?</p>
|
||||||
|
</v-card-text>
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer />
|
||||||
|
<v-btn variant="text" @click="deleteKeyDialog = false">Cancel</v-btn>
|
||||||
|
<v-btn
|
||||||
|
color="error"
|
||||||
|
variant="flat"
|
||||||
|
:loading="deletingKey"
|
||||||
|
@click="deleteAPIKey"
|
||||||
|
>
|
||||||
|
Delete Key
|
||||||
|
</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 { useOrganizationSelector } from '~/composables/useOrganizationSelector'
|
||||||
|
import {
|
||||||
|
useAddApiKeyMutation,
|
||||||
|
useAddOrganizationMutation,
|
||||||
|
useRemoveApiKeyMutation,
|
||||||
|
useRemoveOrganizationMutation,
|
||||||
|
} from '~/graphql/generated'
|
||||||
|
|
||||||
|
const auth0 = useAuth0()
|
||||||
|
const { selectedOrganization, organizations, refetch, selectOrganization } = useOrganizationSelector()
|
||||||
|
|
||||||
|
// 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')
|
||||||
|
})
|
||||||
|
|
||||||
|
// Create Organization Dialog
|
||||||
|
const addOrgDialog = ref(false)
|
||||||
|
const newOrgName = ref('')
|
||||||
|
const orgNameError = ref('')
|
||||||
|
const creatingOrg = ref(false)
|
||||||
|
|
||||||
|
// Add API Key Dialog
|
||||||
|
const addKeyDialog = ref(false)
|
||||||
|
const newKeyName = ref('')
|
||||||
|
const newKeyRefs = ref<string[]>([])
|
||||||
|
const newKeyRead = ref(true)
|
||||||
|
const newKeyPublish = ref(false)
|
||||||
|
const keyNameError = ref('')
|
||||||
|
const refsError = ref('')
|
||||||
|
const creatingKey = ref(false)
|
||||||
|
|
||||||
|
// Computed property for button state to ensure reactivity
|
||||||
|
const canCreateKey = computed(() => {
|
||||||
|
return newKeyName.value.trim() !== '' && newKeyRefs.value.length > 0
|
||||||
|
})
|
||||||
|
|
||||||
|
// Show created key dialog
|
||||||
|
const showKeyDialog = ref(false)
|
||||||
|
const createdKey = ref<any>(null)
|
||||||
|
|
||||||
|
// Snackbar
|
||||||
|
const snackbar = ref(false)
|
||||||
|
const snackbarText = ref('')
|
||||||
|
const snackbarColor = ref('success')
|
||||||
|
|
||||||
|
// Delete Organization Dialog
|
||||||
|
const deleteOrgDialog = ref(false)
|
||||||
|
const orgToDelete = ref<any>(null)
|
||||||
|
const deletingOrg = ref(false)
|
||||||
|
|
||||||
|
// Delete API Key Dialog
|
||||||
|
const deleteKeyDialog = ref(false)
|
||||||
|
const keyToDelete = ref<any>(null)
|
||||||
|
const deletingKey = ref(false)
|
||||||
|
|
||||||
|
// Mutations
|
||||||
|
const { mutate: addAPIKeyMutation } = useAddApiKeyMutation({})
|
||||||
|
const { mutate: addOrganizationMutation } = useAddOrganizationMutation({})
|
||||||
|
const { mutate: removeAPIKeyMutation } = useRemoveApiKeyMutation({})
|
||||||
|
const { mutate: removeOrganizationMutation } = useRemoveOrganizationMutation({})
|
||||||
|
|
||||||
|
const openAddOrgDialog = () => {
|
||||||
|
newOrgName.value = ''
|
||||||
|
orgNameError.value = ''
|
||||||
|
addOrgDialog.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
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?.data?.addOrganization) {
|
||||||
|
throw new Error('Failed to create organization')
|
||||||
|
}
|
||||||
|
|
||||||
|
snackbarText.value = `Organization "${newOrgName.value}" created successfully!`
|
||||||
|
snackbarColor.value = 'success'
|
||||||
|
snackbar.value = true
|
||||||
|
addOrgDialog.value = false
|
||||||
|
newOrgName.value = ''
|
||||||
|
|
||||||
|
// Refresh organizations and select the new one
|
||||||
|
await refetch()
|
||||||
|
selectOrganization(result.data.addOrganization.id)
|
||||||
|
} 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const openAddKeyDialog = () => {
|
||||||
|
newKeyName.value = ''
|
||||||
|
newKeyRefs.value = []
|
||||||
|
newKeyRead.value = true
|
||||||
|
newKeyPublish.value = false
|
||||||
|
keyNameError.value = ''
|
||||||
|
refsError.value = ''
|
||||||
|
addKeyDialog.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const createAPIKey = async () => {
|
||||||
|
if (!newKeyName.value) {
|
||||||
|
keyNameError.value = 'Key name is required'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newKeyRefs.value.length === 0) {
|
||||||
|
refsError.value = 'At least one ref is required'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!selectedOrganization.value) {
|
||||||
|
snackbarText.value = 'No organization selected'
|
||||||
|
snackbarColor.value = 'error'
|
||||||
|
snackbar.value = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
creatingKey.value = true
|
||||||
|
keyNameError.value = ''
|
||||||
|
refsError.value = ''
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await addAPIKeyMutation({
|
||||||
|
input: {
|
||||||
|
name: newKeyName.value,
|
||||||
|
organizationId: selectedOrganization.value.id,
|
||||||
|
refs: newKeyRefs.value,
|
||||||
|
read: newKeyRead.value,
|
||||||
|
publish: newKeyPublish.value,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!result?.data?.addAPIKey) {
|
||||||
|
throw new Error('Failed to create API key')
|
||||||
|
}
|
||||||
|
|
||||||
|
createdKey.value = result.data.addAPIKey
|
||||||
|
addKeyDialog.value = false
|
||||||
|
showKeyDialog.value = true
|
||||||
|
|
||||||
|
// Refresh organizations to show new key
|
||||||
|
await refetch()
|
||||||
|
} catch (error: any) {
|
||||||
|
keyNameError.value = error.message || 'Failed to create API key'
|
||||||
|
snackbarText.value = 'Failed to create API key'
|
||||||
|
snackbarColor.value = 'error'
|
||||||
|
snackbar.value = true
|
||||||
|
} finally {
|
||||||
|
creatingKey.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const copyKey = async () => {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(createdKey.value?.key || '')
|
||||||
|
snackbarText.value = 'API key copied to clipboard'
|
||||||
|
snackbarColor.value = 'success'
|
||||||
|
snackbar.value = true
|
||||||
|
} catch (_err) {
|
||||||
|
snackbarText.value = 'Failed to copy'
|
||||||
|
snackbarColor.value = 'error'
|
||||||
|
snackbar.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeKeyDialog = () => {
|
||||||
|
showKeyDialog.value = false
|
||||||
|
createdKey.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper functions to parse user IDs
|
||||||
|
const getUserProvider = (userId: string) => {
|
||||||
|
if (!userId) return ''
|
||||||
|
const parts = userId.split('|')
|
||||||
|
if (parts.length === 2) {
|
||||||
|
// Map provider names to more readable formats
|
||||||
|
const providerMap: Record<string, string> = {
|
||||||
|
'google-oauth2': 'Google',
|
||||||
|
'auth0': 'Email/Password',
|
||||||
|
'github': 'GitHub',
|
||||||
|
'twitter': 'Twitter',
|
||||||
|
'facebook': 'Facebook',
|
||||||
|
'windowslive': 'Microsoft',
|
||||||
|
'linkedin': 'LinkedIn',
|
||||||
|
}
|
||||||
|
return providerMap[parts[0]] || parts[0]
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const getUserId = (userId: string) => {
|
||||||
|
if (!userId) return ''
|
||||||
|
const parts = userId.split('|')
|
||||||
|
if (parts.length === 2) {
|
||||||
|
return parts[1]
|
||||||
|
}
|
||||||
|
return userId
|
||||||
|
}
|
||||||
|
|
||||||
|
const getUserProviderIcon = (userId: string) => {
|
||||||
|
if (!userId) return 'mdi-account'
|
||||||
|
const parts = userId.split('|')
|
||||||
|
if (parts.length === 2) {
|
||||||
|
const iconMap: Record<string, string> = {
|
||||||
|
'google-oauth2': 'mdi-google',
|
||||||
|
'auth0': 'mdi-email',
|
||||||
|
'github': 'mdi-github',
|
||||||
|
'twitter': 'mdi-twitter',
|
||||||
|
'facebook': 'mdi-facebook',
|
||||||
|
'windowslive': 'mdi-microsoft',
|
||||||
|
'linkedin': 'mdi-linkedin',
|
||||||
|
}
|
||||||
|
return iconMap[parts[0]] || 'mdi-account'
|
||||||
|
}
|
||||||
|
return 'mdi-account'
|
||||||
|
}
|
||||||
|
|
||||||
|
const openDeleteOrgDialog = (org: any) => {
|
||||||
|
orgToDelete.value = org
|
||||||
|
deleteOrgDialog.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteOrganization = async () => {
|
||||||
|
if (!orgToDelete.value) return
|
||||||
|
|
||||||
|
deletingOrg.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await removeOrganizationMutation({
|
||||||
|
organizationId: orgToDelete.value.id,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!result?.data?.removeOrganization) {
|
||||||
|
throw new Error('Failed to delete organization')
|
||||||
|
}
|
||||||
|
|
||||||
|
snackbarText.value = `Organization "${orgToDelete.value.name}" deleted successfully`
|
||||||
|
snackbarColor.value = 'success'
|
||||||
|
snackbar.value = true
|
||||||
|
deleteOrgDialog.value = false
|
||||||
|
orgToDelete.value = null
|
||||||
|
|
||||||
|
// Refresh organizations
|
||||||
|
await refetch()
|
||||||
|
|
||||||
|
// If deleted org was selected, select another one
|
||||||
|
if (organizations.value.length > 0) {
|
||||||
|
selectOrganization(organizations.value[0].id)
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
snackbarText.value = error.message || 'Failed to delete organization'
|
||||||
|
snackbarColor.value = 'error'
|
||||||
|
snackbar.value = true
|
||||||
|
} finally {
|
||||||
|
deletingOrg.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const openDeleteKeyDialog = (apiKey: any) => {
|
||||||
|
keyToDelete.value = apiKey
|
||||||
|
deleteKeyDialog.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteAPIKey = async () => {
|
||||||
|
if (!keyToDelete.value || !selectedOrganization.value) return
|
||||||
|
|
||||||
|
deletingKey.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await removeAPIKeyMutation({
|
||||||
|
organizationId: selectedOrganization.value.id,
|
||||||
|
keyName: keyToDelete.value.name,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!result?.data?.removeAPIKey) {
|
||||||
|
throw new Error('Failed to delete API key')
|
||||||
|
}
|
||||||
|
|
||||||
|
snackbarText.value = `API key "${keyToDelete.value.name}" deleted successfully`
|
||||||
|
snackbarColor.value = 'success'
|
||||||
|
snackbar.value = true
|
||||||
|
deleteKeyDialog.value = false
|
||||||
|
keyToDelete.value = null
|
||||||
|
|
||||||
|
// Refresh organizations
|
||||||
|
await refetch()
|
||||||
|
} catch (error: any) {
|
||||||
|
snackbarText.value = error.message || 'Failed to delete API key'
|
||||||
|
snackbarColor.value = 'error'
|
||||||
|
snackbar.value = true
|
||||||
|
} finally {
|
||||||
|
deletingKey.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import { ApolloClient, from, HttpLink, InMemoryCache } from '@apollo/client/core'
|
||||||
|
import { setContext } from '@apollo/client/link/context'
|
||||||
|
import { DefaultApolloClient } from '@vue/apollo-composable'
|
||||||
|
|
||||||
|
export default defineNuxtPlugin((nuxtApp) => {
|
||||||
|
const config = useRuntimeConfig()
|
||||||
|
|
||||||
|
// Determine API URL based on current host
|
||||||
|
const getApiUrl = () => {
|
||||||
|
if (typeof window === 'undefined') {
|
||||||
|
return config.public.apiBase
|
||||||
|
}
|
||||||
|
|
||||||
|
const hostname = window.location.hostname
|
||||||
|
|
||||||
|
// If running on localhost, use localhost:8080
|
||||||
|
if (hostname === 'localhost' || hostname === '127.0.0.1') {
|
||||||
|
return 'http://localhost:8080'
|
||||||
|
}
|
||||||
|
|
||||||
|
// If running on schemas.unbound.se, use the same domain
|
||||||
|
if (hostname === 'schemas.unbound.se') {
|
||||||
|
return 'https://schemas.unbound.se'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to config or construct from current location
|
||||||
|
return config.public.apiBase || `${window.location.protocol}//${window.location.host}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTTP link for GraphQL endpoint
|
||||||
|
const httpLink = new HttpLink({
|
||||||
|
uri: `${getApiUrl()}/query`,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Auth link to add authorization header
|
||||||
|
const authLink = setContext(async (_, { headers }) => {
|
||||||
|
try {
|
||||||
|
// Access Auth0 from the global app instance
|
||||||
|
const auth0Instance = (nuxtApp.vueApp.config.globalProperties as any).$auth0
|
||||||
|
|
||||||
|
if (auth0Instance?.isAuthenticated?.value) {
|
||||||
|
// Get the access token for API calls
|
||||||
|
const token = await auth0Instance.getAccessTokenSilently()
|
||||||
|
|
||||||
|
if (token) {
|
||||||
|
return {
|
||||||
|
headers: {
|
||||||
|
...headers,
|
||||||
|
authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Apollo] Failed to get Auth0 token:', error)
|
||||||
|
}
|
||||||
|
|
||||||
|
return { headers }
|
||||||
|
})
|
||||||
|
|
||||||
|
// Create Apollo client
|
||||||
|
const apolloClient = new ApolloClient({
|
||||||
|
link: from([authLink, httpLink]),
|
||||||
|
cache: new InMemoryCache(),
|
||||||
|
})
|
||||||
|
|
||||||
|
// Provide Apollo client to the app
|
||||||
|
nuxtApp.vueApp.provide(DefaultApolloClient, apolloClient)
|
||||||
|
|
||||||
|
return {
|
||||||
|
provide: {
|
||||||
|
apolloClient,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { createAuth0 } from '@auth0/auth0-vue'
|
||||||
|
|
||||||
|
export default defineNuxtPlugin((nuxtApp) => {
|
||||||
|
const config = useRuntimeConfig()
|
||||||
|
|
||||||
|
const auth0 = createAuth0({
|
||||||
|
domain: config.public.auth0.domain,
|
||||||
|
clientId: config.public.auth0.clientId,
|
||||||
|
authorizationParams: {
|
||||||
|
redirect_uri: window.location.origin,
|
||||||
|
audience: config.public.auth0.audience,
|
||||||
|
},
|
||||||
|
cacheLocation: 'localstorage',
|
||||||
|
})
|
||||||
|
|
||||||
|
nuxtApp.vueApp.use(auth0)
|
||||||
|
})
|
||||||
+13
@@ -0,0 +1,13 @@
|
|||||||
|
schema: http://localhost:8080/query
|
||||||
|
documents: 'app/**/*.graphql'
|
||||||
|
generates:
|
||||||
|
app/graphql/generated.ts:
|
||||||
|
plugins:
|
||||||
|
- typescript
|
||||||
|
- typescript-operations
|
||||||
|
- typescript-vue-apollo
|
||||||
|
config:
|
||||||
|
withCompositionFunctions: true
|
||||||
|
vueCompositionApiImportFrom: vue
|
||||||
|
dedupeFragments: true
|
||||||
|
skipTypename: false
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
// @ts-check
|
||||||
|
import stylistic from '@stylistic/eslint-plugin'
|
||||||
|
import parserVue from 'vue-eslint-parser'
|
||||||
|
import simpleImportSort from "eslint-plugin-simple-import-sort"
|
||||||
|
import withNuxt from './.nuxt/eslint.config.mjs'
|
||||||
|
|
||||||
|
export default withNuxt(
|
||||||
|
// Ignore generated files
|
||||||
|
{
|
||||||
|
ignores: ['**/app/graphql/generated.ts'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ['**/*.vue', '**/*.ts', '**/*.js'],
|
||||||
|
plugins: {
|
||||||
|
'@stylistic': stylistic,
|
||||||
|
"simple-import-sort": simpleImportSort,
|
||||||
|
},
|
||||||
|
languageOptions: {
|
||||||
|
parser: parserVue,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
"simple-import-sort/imports": "error",
|
||||||
|
"simple-import-sort/exports": "error",
|
||||||
|
'@stylistic/no-multiple-empty-lines': 'error',
|
||||||
|
'@stylistic/comma-dangle': ['error', {
|
||||||
|
arrays: 'always-multiline',
|
||||||
|
objects: 'always-multiline',
|
||||||
|
imports: 'always-multiline',
|
||||||
|
exports: 'never',
|
||||||
|
functions: 'always-multiline',
|
||||||
|
}],
|
||||||
|
'@stylistic/block-spacing': ['error'],
|
||||||
|
'@stylistic/object-curly-spacing': ['error', 'always'],
|
||||||
|
'@stylistic/indent': ['error', 2],
|
||||||
|
'@stylistic/quotes': ['error', 'single'],
|
||||||
|
'@stylistic/semi': ['error', 'never'],
|
||||||
|
'@stylistic/member-delimiter-style': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
multiline: {
|
||||||
|
delimiter: 'none',
|
||||||
|
requireLast: true,
|
||||||
|
},
|
||||||
|
singleline: {
|
||||||
|
delimiter: 'semi',
|
||||||
|
requireLast: false,
|
||||||
|
},
|
||||||
|
multilineDetection: 'brackets',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'@typescript-eslint/no-unused-vars': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
args: 'all',
|
||||||
|
argsIgnorePattern: '^_',
|
||||||
|
caughtErrors: 'all',
|
||||||
|
caughtErrorsIgnorePattern: '^_',
|
||||||
|
destructuredArrayIgnorePattern: '^_',
|
||||||
|
varsIgnorePattern: '^_',
|
||||||
|
ignoreRestSiblings: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
// Allow any types for now - can be fixed incrementally
|
||||||
|
'@typescript-eslint/no-explicit-any': 'warn',
|
||||||
|
// Disable bare strings rule since this app doesn't use i18n yet
|
||||||
|
"vue/no-bare-strings-in-template": "off",
|
||||||
|
"vue/no-useless-v-bind": ["error", {
|
||||||
|
"ignoreIncludesComment": false,
|
||||||
|
"ignoreStringEscape": false
|
||||||
|
}],
|
||||||
|
"vue/no-useless-mustaches": ["error", {
|
||||||
|
"ignoreIncludesComment": false,
|
||||||
|
"ignoreStringEscape": false
|
||||||
|
}],
|
||||||
|
"vue/html-closing-bracket-spacing": ["error", {
|
||||||
|
"startTag": "never",
|
||||||
|
"endTag": "never",
|
||||||
|
"selfClosingTag": "always"
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
apiVersion: autoscaling/v2
|
||||||
|
kind: HorizontalPodAutoscaler
|
||||||
|
metadata:
|
||||||
|
name: schemas-app
|
||||||
|
spec:
|
||||||
|
scaleTargetRef:
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
name: schemas-app
|
||||||
|
minReplicas: 2
|
||||||
|
maxReplicas: 10
|
||||||
|
metrics:
|
||||||
|
- type: Resource
|
||||||
|
resource:
|
||||||
|
name: cpu
|
||||||
|
target:
|
||||||
|
type: Utilization
|
||||||
|
averageUtilization: 80
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: schemas-app
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: schemas-app
|
||||||
|
annotations:
|
||||||
|
kubernetes.io/change-cause: "${TIMESTAMP} Deployed commit id: ${COMMIT}"
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
strategy:
|
||||||
|
type: RollingUpdate
|
||||||
|
rollingUpdate:
|
||||||
|
maxSurge: 1
|
||||||
|
maxUnavailable: 1
|
||||||
|
minReadySeconds: 30
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app.kubernetes.io/name: schemas-app
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: schemas-app
|
||||||
|
app.kubernetes.io/instance: schemas
|
||||||
|
spec:
|
||||||
|
affinity:
|
||||||
|
podAntiAffinity:
|
||||||
|
preferredDuringSchedulingIgnoredDuringExecution:
|
||||||
|
- weight: 100
|
||||||
|
podAffinityTerm:
|
||||||
|
labelSelector:
|
||||||
|
matchExpressions:
|
||||||
|
- key: "app.kubernetes.io/name"
|
||||||
|
operator: In
|
||||||
|
values:
|
||||||
|
- schemas-app
|
||||||
|
topologyKey: kubernetes.io/hostname
|
||||||
|
containers:
|
||||||
|
- name: schemas-app
|
||||||
|
image: registry.gitlab.com/unboundsoftware/schemas-app:${COMMIT}
|
||||||
|
imagePullPolicy: IfNotPresent
|
||||||
|
ports:
|
||||||
|
- containerPort: 80
|
||||||
|
readinessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /
|
||||||
|
port: 80
|
||||||
|
initialDelaySeconds: 5
|
||||||
|
periodSeconds: 5
|
||||||
|
timeoutSeconds: 5
|
||||||
|
livenessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /
|
||||||
|
port: 80
|
||||||
|
initialDelaySeconds: 10
|
||||||
|
periodSeconds: 10
|
||||||
|
timeoutSeconds: 5
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 10m
|
||||||
|
memory: 10Mi
|
||||||
|
limits:
|
||||||
|
cpu: 300m
|
||||||
|
memory: 300Mi
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: schemas-app
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: schemas-app
|
||||||
|
spec:
|
||||||
|
type: NodePort
|
||||||
|
selector:
|
||||||
|
app.kubernetes.io/name: schemas-app
|
||||||
|
ports:
|
||||||
|
- port: 80
|
||||||
|
targetPort: 80
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: schemas-app-ingress
|
||||||
|
annotations:
|
||||||
|
nginx.ingress.kubernetes.io/rewrite-target: /
|
||||||
|
spec:
|
||||||
|
ingressClassName: nginx
|
||||||
|
rules:
|
||||||
|
- host: "schemas.local"
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- path: /
|
||||||
|
pathType: Prefix
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: schemas-app
|
||||||
|
port:
|
||||||
|
number: 80
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: schemas-app-ingress
|
||||||
|
annotations:
|
||||||
|
alb.ingress.kubernetes.io/group.name: "default"
|
||||||
|
alb.ingress.kubernetes.io/group.order: "15"
|
||||||
|
alb.ingress.kubernetes.io/scheme: internet-facing
|
||||||
|
alb.ingress.kubernetes.io/target-type: instance
|
||||||
|
alb.ingress.kubernetes.io/listen-ports: '[{"HTTP": 80},{"HTTPS": 443}]'
|
||||||
|
alb.ingress.kubernetes.io/ssl-redirect: "443"
|
||||||
|
alb.ingress.kubernetes.io/healthcheck-path: '/'
|
||||||
|
spec:
|
||||||
|
ingressClassName: "alb"
|
||||||
|
rules:
|
||||||
|
- host: "schemas.unbound.se"
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- path: /*
|
||||||
|
pathType: ImplementationSpecific
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: schemas-app
|
||||||
|
port:
|
||||||
|
number: 80
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
apiVersion: policy/v1
|
||||||
|
kind: PodDisruptionBudget
|
||||||
|
metadata:
|
||||||
|
name: schemas-app
|
||||||
|
spec:
|
||||||
|
minAvailable: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app.kubernetes.io/name: schemas-app
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
user nginx;
|
||||||
|
worker_processes auto;
|
||||||
|
|
||||||
|
error_log /var/log/nginx/error.log notice;
|
||||||
|
pid /var/run/nginx.pid;
|
||||||
|
|
||||||
|
events {
|
||||||
|
worker_connections 1024;
|
||||||
|
}
|
||||||
|
|
||||||
|
http {
|
||||||
|
include /etc/nginx/mime.types;
|
||||||
|
default_type application/octet-stream;
|
||||||
|
types {
|
||||||
|
application/manifest+json webmanifest;
|
||||||
|
}
|
||||||
|
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||||
|
'$status $body_bytes_sent "$http_referer" '
|
||||||
|
'"$http_user_agent" "$http_x_forwarded_for"';
|
||||||
|
|
||||||
|
sendfile on;
|
||||||
|
keepalive_timeout 65;
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name localhost;
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html index.htm;
|
||||||
|
access_log off;
|
||||||
|
|
||||||
|
# Security headers
|
||||||
|
include /etc/nginx/security_headers.conf;
|
||||||
|
|
||||||
|
# Health check endpoint for Kubernetes
|
||||||
|
location = /health {
|
||||||
|
return 200 '{"status":"UP"}';
|
||||||
|
}
|
||||||
|
|
||||||
|
# all assets contain hash in filename, cache forever
|
||||||
|
location ^~ /assets/ {
|
||||||
|
include /etc/nginx/security_headers.conf;
|
||||||
|
add_header Cache-Control "public, max-age=31536000, s-maxage=31536000, immutable";
|
||||||
|
try_files $uri =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Nuxt generated files
|
||||||
|
location /_nuxt/ {
|
||||||
|
access_log /var/log/nginx/access.log main;
|
||||||
|
include /etc/nginx/security_headers.conf;
|
||||||
|
autoindex off;
|
||||||
|
expires off;
|
||||||
|
add_header Cache-Control "public, max-age=31536000, s-maxage=31536000, immutable";
|
||||||
|
}
|
||||||
|
|
||||||
|
# Main application - SPA mode
|
||||||
|
location / {
|
||||||
|
access_log /var/log/nginx/access.log main;
|
||||||
|
include /etc/nginx/security_headers.conf;
|
||||||
|
autoindex off;
|
||||||
|
expires off;
|
||||||
|
add_header Cache-Control "public, max-age=0, s-maxage=0, must-revalidate" always;
|
||||||
|
try_files $uri $uri/ /index.html =404;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
add_header Strict-Transport-Security 'max-age=63072000; includeSubDomains; preload';
|
||||||
|
add_header Content-Security-Policy "frame-ancestors 'none'";
|
||||||
|
add_header X-XSS-Protection "0";
|
||||||
|
add_header X-Frame-Options "SAMEORIGIN";
|
||||||
|
add_header Referrer-Policy strict-origin-when-cross-origin;
|
||||||
|
add_header X-Content-Type-Options nosniff;
|
||||||
|
add_header X-Permitted-Cross-Domain-Policies master-only;
|
||||||
|
add_header Feature-Policy '*=*';
|
||||||
|
add_header Permissions-Policy '*=*';
|
||||||
|
add_header Document-Policy 'unsized-media=?0, document-write=?0, max-image-bpp=2.0, frame-loading=lazy';
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||||
|
export default defineNuxtConfig({
|
||||||
|
compatibilityDate: '2024-11-21',
|
||||||
|
|
||||||
|
devtools: { enabled: true },
|
||||||
|
|
||||||
|
ssr: false,
|
||||||
|
|
||||||
|
devServer: {
|
||||||
|
port: 3500,
|
||||||
|
},
|
||||||
|
|
||||||
|
modules: [
|
||||||
|
'vuetify-nuxt-module',
|
||||||
|
'@nuxt/eslint',
|
||||||
|
'@nuxtjs/stylelint-module',
|
||||||
|
],
|
||||||
|
|
||||||
|
eslint: {
|
||||||
|
checker: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
stylelint: {
|
||||||
|
lintOnStart: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
vuetify: {
|
||||||
|
moduleOptions: {
|
||||||
|
/* module specific options */
|
||||||
|
},
|
||||||
|
vuetifyOptions: {
|
||||||
|
theme: {
|
||||||
|
defaultTheme: 'light',
|
||||||
|
themes: {
|
||||||
|
light: {
|
||||||
|
colors: {
|
||||||
|
primary: '#1976D2',
|
||||||
|
secondary: '#424242',
|
||||||
|
accent: '#82B1FF',
|
||||||
|
error: '#FF5252',
|
||||||
|
info: '#2196F3',
|
||||||
|
success: '#4CAF50',
|
||||||
|
warning: '#FB8C00',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
app: {
|
||||||
|
head: {
|
||||||
|
title: 'Unbound Schemas',
|
||||||
|
meta: [
|
||||||
|
{ charset: 'utf-8' },
|
||||||
|
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
|
||||||
|
{ name: 'description', content: 'View and manage your federated GraphQL schemas' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
runtimeConfig: {
|
||||||
|
public: {
|
||||||
|
apiBase: process.env.NUXT_PUBLIC_API_BASE || 'http://localhost:8080',
|
||||||
|
auth0: {
|
||||||
|
domain: process.env.NUXT_PUBLIC_AUTH0_DOMAIN || '',
|
||||||
|
clientId: process.env.NUXT_PUBLIC_AUTH0_CLIENT_ID || '',
|
||||||
|
audience: process.env.NUXT_PUBLIC_AUTH0_AUDIENCE || 'https://schemas.unbound.se',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
Generated
+21949
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,68 @@
|
|||||||
|
{
|
||||||
|
"name": "schemas-app",
|
||||||
|
"type": "module",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"build": "nuxt build",
|
||||||
|
"dev": "nuxt dev",
|
||||||
|
"generate": "nuxt generate",
|
||||||
|
"preview": "nuxt preview",
|
||||||
|
"postinstall": "nuxt prepare",
|
||||||
|
"codegen": "graphql-codegen --config codegen.yml",
|
||||||
|
"test": "vitest run",
|
||||||
|
"test:watch": "vitest",
|
||||||
|
"test:coverage": "vitest run --coverage",
|
||||||
|
"lint:js": "eslint .",
|
||||||
|
"lint:style": "stylelint \"**/*.{css,scss,sass,html,vue}\" --ignore-path .gitignore",
|
||||||
|
"lint": "npm run lint:js && npm run lint:style",
|
||||||
|
"lintfix": "eslint --fix . && npm run lint:style --fix"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@apollo/client": "^3.14.0",
|
||||||
|
"@auth0/auth0-vue": "^2.4.0",
|
||||||
|
"@vue/apollo-composable": "^4.2.2",
|
||||||
|
"graphql": "^16.12.0",
|
||||||
|
"graphql-ws": "^6.0.6",
|
||||||
|
"nuxt": "^4.2.1",
|
||||||
|
"vue": "^3.5.24",
|
||||||
|
"vue-router": "^4.6.3",
|
||||||
|
"vuetify-nuxt-module": "^0.18.4"
|
||||||
|
},
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Look at the [Nuxt documentation](https://nuxt.com/docs/getting-started/introduction) to learn more.",
|
||||||
|
"main": "index.js",
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"devDependencies": {
|
||||||
|
"@commitlint/cli": "^20.1.0",
|
||||||
|
"@commitlint/config-conventional": "^20.0.0",
|
||||||
|
"@graphql-codegen/cli": "^6.1.0",
|
||||||
|
"@graphql-codegen/typed-document-node": "^6.1.3",
|
||||||
|
"@graphql-codegen/typescript": "^5.0.5",
|
||||||
|
"@graphql-codegen/typescript-operations": "^5.0.5",
|
||||||
|
"@graphql-codegen/typescript-vue-apollo": "^4.1.2",
|
||||||
|
"@nuxt/devtools": "^3.1.0",
|
||||||
|
"@nuxt/eslint": "^1.10.0",
|
||||||
|
"@nuxtjs/eslint-module": "^4.1.0",
|
||||||
|
"@nuxtjs/stylelint-module": "^5.2.1",
|
||||||
|
"@stylistic/eslint-plugin": "^5.6.1",
|
||||||
|
"@vitejs/plugin-vue": "^6.0.2",
|
||||||
|
"@vitest/coverage-v8": "^4.0.13",
|
||||||
|
"@vue/test-utils": "^2.4.6",
|
||||||
|
"eslint": "^9.39.1",
|
||||||
|
"eslint-plugin-nuxt": "^4.0.0",
|
||||||
|
"eslint-plugin-simple-import-sort": "^12.1.1",
|
||||||
|
"eslint-plugin-vue": "^10.6.0",
|
||||||
|
"jsdom": "^27.2.0",
|
||||||
|
"postcss-html": "^1.8.0",
|
||||||
|
"stylelint": "^16.26.0",
|
||||||
|
"stylelint-config-recommended-vue": "^1.6.1",
|
||||||
|
"stylelint-config-standard": "^39.0.1",
|
||||||
|
"typescript": "^5.9.3",
|
||||||
|
"typescript-eslint": "^8.47.0",
|
||||||
|
"vite-plugin-eslint2": "^5.0.4",
|
||||||
|
"vitest": "^4.0.13",
|
||||||
|
"vue-eslint-parser": "^10.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
@@ -0,0 +1,2 @@
|
|||||||
|
User-Agent: *
|
||||||
|
Disallow:
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||||
|
"extends": [
|
||||||
|
"config:recommended"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
module.exports = {
|
||||||
|
customSyntax: 'postcss-html',
|
||||||
|
extends: [
|
||||||
|
'stylelint-config-standard',
|
||||||
|
'stylelint-config-recommended-vue',
|
||||||
|
],
|
||||||
|
// add your custom config here
|
||||||
|
// https://stylelint.io/user-guide/configuration
|
||||||
|
rules: {
|
||||||
|
"no-invalid-position-declaration": null
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
import { config } from '@vue/test-utils'
|
||||||
|
import { vi } from 'vitest'
|
||||||
|
import * as Vue from 'vue'
|
||||||
|
|
||||||
|
// Mock localStorage
|
||||||
|
const localStorageMock = {
|
||||||
|
getItem: vi.fn(),
|
||||||
|
setItem: vi.fn(),
|
||||||
|
removeItem: vi.fn(),
|
||||||
|
clear: vi.fn(),
|
||||||
|
key: vi.fn(),
|
||||||
|
length: 0,
|
||||||
|
}
|
||||||
|
global.localStorage = localStorageMock as unknown as Storage
|
||||||
|
|
||||||
|
// Make Vue composables globally available
|
||||||
|
global.ref = Vue.ref
|
||||||
|
global.computed = Vue.computed
|
||||||
|
global.reactive = Vue.reactive
|
||||||
|
global.watch = Vue.watch
|
||||||
|
global.watchEffect = Vue.watchEffect
|
||||||
|
global.defineModel = Vue.defineModel
|
||||||
|
global.onMounted = Vue.onMounted
|
||||||
|
global.onUnmounted = Vue.onUnmounted
|
||||||
|
global.nextTick = Vue.nextTick
|
||||||
|
|
||||||
|
// Mock Nuxt composables
|
||||||
|
global.useRuntimeConfig = vi.fn(() => ({
|
||||||
|
public: {},
|
||||||
|
}))
|
||||||
|
|
||||||
|
global.useNuxtApp = vi.fn(() => ({
|
||||||
|
$apollo: {},
|
||||||
|
}))
|
||||||
|
|
||||||
|
global.navigateTo = vi.fn()
|
||||||
|
global.definePageMeta = vi.fn()
|
||||||
|
|
||||||
|
// Mock Vuetify components globally for tests
|
||||||
|
config.global.stubs = {
|
||||||
|
VAlert: {
|
||||||
|
name: 'v-alert',
|
||||||
|
template: '<div class="v-alert" :type="type" :variant="variant" :color="color"><slot /></div>',
|
||||||
|
props: ['color', 'type', 'variant'],
|
||||||
|
},
|
||||||
|
VTextField: {
|
||||||
|
template: '<div class="v-text-field"><input :readonly="readonly || disabled" :disabled="disabled" :error="error" :value="modelValue" @input="$emit(\'update:model-value\', $event.target.value)" @blur="$emit(\'blur\')" /></div>',
|
||||||
|
props: ['modelValue', 'readonly', 'disabled', 'active', 'focused', 'variant', 'error', 'errorMessages', 'label'],
|
||||||
|
emits: ['update:model-value', 'blur'],
|
||||||
|
},
|
||||||
|
VBtn: {
|
||||||
|
name: 'v-btn',
|
||||||
|
template: '<button :disabled="disabled" :color="color" :data-testid="$attrs[\'data-testid\']" @click="$emit(\'click\')"><slot /></button>',
|
||||||
|
props: ['disabled', 'variant', 'icon', 'color', 'flat', 'depressed', 'xLarge'],
|
||||||
|
emits: ['click'],
|
||||||
|
},
|
||||||
|
VDialog: {
|
||||||
|
name: 'v-dialog',
|
||||||
|
template: '<div class="v-dialog"><slot name="activator" /><div class="v-dialog__content"><slot /></div></div>',
|
||||||
|
props: ['modelValue', 'width', 'maxWidth', 'persistent'],
|
||||||
|
},
|
||||||
|
VForm: {
|
||||||
|
name: 'v-form',
|
||||||
|
template: '<form @submit="$emit(\'submit\', $event)"><slot /></form>',
|
||||||
|
emits: ['submit'],
|
||||||
|
},
|
||||||
|
VCard: {
|
||||||
|
name: 'v-card',
|
||||||
|
template: '<div class="v-card" :variant="variant" :rounded="rounded" :loading="loading"><div v-if="title" class="v-card-title">{{ title }}</div><slot /></div>',
|
||||||
|
props: ['variant', 'rounded', 'loading', 'title'],
|
||||||
|
},
|
||||||
|
VCardTitle: {
|
||||||
|
template: '<div class="v-card-title"><slot /></div>',
|
||||||
|
},
|
||||||
|
VCardText: {
|
||||||
|
name: 'v-card-text',
|
||||||
|
template: '<div class="v-card-text"><slot /></div>',
|
||||||
|
},
|
||||||
|
VCardActions: {
|
||||||
|
name: 'v-card-actions',
|
||||||
|
template: '<div class="v-card-actions"><slot /></div>',
|
||||||
|
},
|
||||||
|
VSpacer: {
|
||||||
|
name: 'v-spacer',
|
||||||
|
template: '<div class="v-spacer"></div>',
|
||||||
|
},
|
||||||
|
VIcon: {
|
||||||
|
name: 'v-icon',
|
||||||
|
template: '<i class="v-icon" :class="icon" @click="$emit(\'click\')"></i>',
|
||||||
|
props: ['icon', 'large'],
|
||||||
|
emits: ['click'],
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
// https://nuxt.com/docs/guide/concepts/typescript
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "./.nuxt/tsconfig.app.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./.nuxt/tsconfig.server.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./.nuxt/tsconfig.shared.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./.nuxt/tsconfig.node.json"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import vue from '@vitejs/plugin-vue'
|
||||||
|
import { dirname,resolve } from 'path'
|
||||||
|
import { fileURLToPath } from 'url'
|
||||||
|
import { defineConfig } from 'vitest/config'
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url)
|
||||||
|
const __dirname = dirname(__filename)
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [vue()],
|
||||||
|
test: {
|
||||||
|
environment: 'jsdom',
|
||||||
|
globals: true,
|
||||||
|
setupFiles: ['./test/setup.ts'],
|
||||||
|
coverage: {
|
||||||
|
provider: 'v8',
|
||||||
|
reporter: ['text', 'json', 'html'],
|
||||||
|
include: [
|
||||||
|
'app/utils/**/*.{ts,js}',
|
||||||
|
'app/components/**/*.vue',
|
||||||
|
'app/composables/**/*.{ts,js}',
|
||||||
|
'app/hooks/**/*.{ts,js}',
|
||||||
|
'app/store/**/*.{ts,js}',
|
||||||
|
],
|
||||||
|
exclude: [
|
||||||
|
'app/graphql/generated.ts', // GraphQL generated code
|
||||||
|
'**/*.d.ts',
|
||||||
|
'**/*.config.*',
|
||||||
|
'**/node_modules/**',
|
||||||
|
'**/test/**',
|
||||||
|
'**/__tests__/**',
|
||||||
|
'**/*.spec.ts',
|
||||||
|
'**/*.test.ts',
|
||||||
|
],
|
||||||
|
// TODO: Re-enable coverage thresholds once we have more test coverage
|
||||||
|
// thresholds: {
|
||||||
|
// statements: 70,
|
||||||
|
// branches: 70,
|
||||||
|
// functions: 70,
|
||||||
|
// lines: 70,
|
||||||
|
// },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'~': resolve(__dirname, './app'),
|
||||||
|
'~~': resolve(__dirname, '.'),
|
||||||
|
'@': resolve(__dirname, './app'),
|
||||||
|
},
|
||||||
|
extensions: ['.js', '.ts', '.jsx', '.tsx', '.json', '.vue'],
|
||||||
|
},
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user