Replace Next with Vite + tanstack/router (#4612)

This commit is contained in:
Kamil Kisiela 2024-05-17 14:30:10 +02:00 committed by GitHub
parent 82f4ff2be9
commit 368f284a97
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
180 changed files with 6361 additions and 7610 deletions

View file

@ -107,6 +107,8 @@ module.exports = {
'packages/services/storage/tools/*.js',
'packages/services/**',
'packages/migrations/**',
// We bundle it all anyway, so there are no node_modules
'packages/web/app/**',
'**/*.spec.ts',
'**/*.test.ts',
],
@ -190,9 +192,6 @@ module.exports = {
{
files: ['packages/web/app/**'],
settings: {
next: {
rootDir: 'packages/web/app',
},
tailwindcss: {
config: 'packages/web/app/tailwind.config.cjs',
whitelist: ['drag-none', 'graphiql-toolbar-icon', 'graphiql-toolbar-button'],
@ -201,7 +200,7 @@ module.exports = {
},
// {
// files: ['packages/web/app/**'],
// excludedFiles: ['packages/web/app/pages/**'],
// excludedFiles: ['packages/web/app/src/pages/**'],
// rules: {
// 'import/no-unused-modules': ['error', { unusedExports: true }],
// },

View file

@ -18,9 +18,6 @@ inputs:
description: Name of the workflow that called this action
required: true
type: string
cacheNext:
description: Should cache Next?
default: 'true'
cacheTurbo:
description: Should cache Turbo?
default: 'true'
@ -56,7 +53,7 @@ runs:
- name: Cache Turbo
uses: actions/cache@v4
if: ${{ inputs.cacheNext == 'true' }}
if: ${{ inputs.cacheTurbo == 'true' }}
with:
path: node_modules/.cache/turbo
key: turbo-cache-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ inputs.actor }}-${{ github.sha }}
@ -64,19 +61,6 @@ runs:
turbo-cache-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ inputs.actor }}-
turbo-cache-${{ hashFiles('**/pnpm-lock.yaml') }}-
- uses: actions/cache@v4
name: Cache Next.js
if: ${{ inputs.cacheNext == 'true' }}
with:
path: |
packages/web/app/.next/cache
packages/web/docs/.next/cache
key:
nextjs-cache-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ inputs.actor }}-${{ github.sha }}
restore-keys: |
nextjs-cache-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ inputs.actor }}-
nextjs-cache-${{ hashFiles('**/pnpm-lock.yaml') }}-
- name: pnpm install
shell: bash
if: ${{ inputs.installDependencies == 'true' }}

View file

@ -20,7 +20,6 @@ jobs:
with:
codegen: false
actor: apollo-router-updater
cacheNext: false
cacheTurbo: false
- name: Check for updates

View file

@ -33,7 +33,6 @@ jobs:
with:
codegen: false # no need to run codegen in this case, we can skip
actor: db-types-diff
cacheNext: false
cacheTurbo: false
- name: create database

View file

@ -15,7 +15,6 @@ jobs:
uses: ./.github/actions/setup
with:
actor: lint
cacheNext: false
cacheTurbo: false
- name: lint .env.template files
@ -64,7 +63,7 @@ jobs:
- name: Operation Check
run: |
npx graphql-inspector validate \
"packages/web/app/{src,pages}/**/*.{graphql,tsx}|packages/libraries/cli/**/*.graphql|packages/web/app/src/lib/**/*.ts" \
"packages/web/app/src/**/*.{graphql,tsx}|packages/libraries/cli/**/*.graphql|packages/web/app/src/lib/**/*.ts" \
"packages/**/module.graphql.ts" \
--maxDepth=20 \
--maxAliasCount=20 \

View file

@ -25,7 +25,6 @@ jobs:
uses: ./.github/actions/setup
with:
actor: prettier
cacheNext: false
cacheTurbo: false
- name: Cache ESLint and Prettier

View file

@ -45,7 +45,6 @@ jobs:
with:
actor: publish-rust
codegen: false
cacheNext: false
cacheTurbo: false
- name: Prepare MacOS

View file

@ -29,7 +29,6 @@ jobs:
with:
codegen: false # no need to run because release script will run it anyway
actor: release-alpha
cacheNext: false
cacheTurbo: true
- name: build libraries

View file

@ -28,7 +28,6 @@ jobs:
with:
codegen: false # no need to run because release script will run it anyway
actor: release-stable
cacheNext: false
cacheTurbo: true
- name: prepare npm credentials

View file

@ -28,7 +28,6 @@ jobs:
with:
codegen: true
actor: storybook
cacheNext: true
cacheTurbo: true
- uses: the-guild-org/shared-config/website-cf@main

View file

@ -35,7 +35,6 @@ jobs:
with:
codegen: false # no need to run codegen in this case, we can skip
actor: migrations-test
cacheNext: false
cacheTurbo: false
- name: migrations tests

View file

@ -29,7 +29,6 @@ jobs:
with:
codegen: false
actor: test-e2e
cacheNext: false
cacheTurbo: false
- name: Install Cypress binary

View file

@ -45,7 +45,6 @@ jobs:
uses: ./.github/actions/setup
with:
actor: test-integration
cacheNext: false
cacheTurbo: true
- name: prepare packages

View file

@ -14,7 +14,6 @@ jobs:
uses: ./.github/actions/setup
with:
actor: test-unit
cacheNext: false
cacheTurbo: false
- name: unit tests

View file

@ -15,7 +15,6 @@ jobs:
uses: ./.github/actions/setup
with:
actor: typescript-typecheck
cacheNext: false
cacheTurbo: false
- name: get cpu count

View file

@ -24,7 +24,6 @@ jobs:
with:
codegen: false
actor: website
cacheNext: false
cacheTurbo: false
- uses: the-guild-org/shared-config/website-cf@main

View file

@ -145,11 +145,9 @@ const config: CodegenConfig = {
},
'./packages/web/app/src/gql/': {
documents: [
'./packages/web/app/src/(components|lib)/**/*.ts(x)?',
'./packages/web/app/pages/v2/**/*.ts(x)?',
'./packages/web/app/pages/**/*.ts(x)?',
'./packages/web/app/src/(components|lib|pages)/**/*.ts(x)?',
'./packages/web/app/src/graphql',
'!./packages/web/app/pages/api/github/setup-callback.ts',
'!./packages/web/app/src/server/**/*.ts',
],
preset: 'client',
config: {

View file

@ -48,13 +48,7 @@ export function deployApp({
imagePullSecret: docker.secret,
readinessProbe: '/api/health',
livenessProbe: '/api/health',
startupProbe: {
endpoint: '/api/health',
initialDelaySeconds: 60,
failureThreshold: 10,
periodSeconds: 30,
timeoutSeconds: 15,
},
startupProbe: '/api/health',
availabilityOnEveryNode: true,
env: {
...environment.envVars,
@ -75,6 +69,7 @@ export function deployApp({
AUTH_REQUIRE_EMAIL_VERIFICATION: '1',
AUTH_ORGANIZATION_OIDC: '1',
MEMBER_ROLES_DEADLINE: appEnv.MEMBER_ROLES_DEADLINE,
PORT: '3000',
},
port: 3000,
},

View file

@ -7,7 +7,7 @@ const webAppPkgJsonFilepath = join(__dirname, '../../packages/web/app/package.js
const webAppPkg = JSON.parse(readFileSync(webAppPkgJsonFilepath, 'utf8'));
const cfConfig = new pulumi.Config('cloudflareCustom');
const monacoEditorVersion = webAppPkg.dependencies['monaco-editor'];
const monacoEditorVersion = webAppPkg.devDependencies['monaco-editor'];
function toExpressionList(items: string[]): string {
return items.map(v => `"${v}"`).join(' ');

View file

@ -1,24 +0,0 @@
FROM node:21.7.3-slim
RUN apt-get update && apt-get install -y ca-certificates
ENV NODE_ENV production
WORKDIR /usr/src/app
COPY --from=dist . /usr/src/app/
COPY --from=shared . /
LABEL org.opencontainers.image.title=$IMAGE_TITLE
LABEL org.opencontainers.image.version=$RELEASE
LABEL org.opencontainers.image.description=$IMAGE_DESCRIPTION
LABEL org.opencontainers.image.authors="The Guild"
LABEL org.opencontainers.image.vendor="Kamil Kisiela"
LABEL org.opencontainers.image.url="https://github.com/kamilkisiela/graphql-hive"
LABEL org.opencontainers.image.source="https://github.com/kamilkisiela/graphql-hive"
ENV ENVIRONMENT production
ENV RELEASE $RELEASE
ENV PORT $PORT
ENTRYPOINT [ "/entrypoint.sh" ]

View file

@ -62,13 +62,6 @@ target "service-base" {
}
}
target "app-base" {
dockerfile = "${PWD}/docker/app.dockerfile"
args = {
RELEASE = "${RELEASE}"
}
}
target "router-base" {
dockerfile = "${PWD}/docker/router.dockerfile"
args = {
@ -367,15 +360,17 @@ target "composition-federation-2" {
}
target "app" {
inherits = ["app-base", get_target()]
inherits = ["service-base", get_target()]
contexts = {
dist = "${PWD}/packages/web/app/dist"
shared = "${PWD}/docker/shared"
}
args = {
SERVICE_DIR_NAME = "@hive/app"
IMAGE_TITLE = "graphql-hive/app"
PORT = "3000"
IMAGE_DESCRIPTION = "The app of the GraphQL Hive project."
HEALTHCHECK_CMD = "wget --spider -q http://127.0.0.1:$${PORT}/api/health"
}
tags = [
local_image_tag("app"),

View file

@ -1,6 +1,6 @@
FROM node:21.7.3-slim
RUN apt-get update && apt-get install -y wget && rm -rf /var/lib/apt/lists/*
RUN apt-get update && apt-get install -y wget ca-certificates && rm -rf /var/lib/apt/lists/*
ARG SERVICE_DIR_NAME
WORKDIR /usr/src/app/$SERVICE_DIR_NAME

View file

@ -85,21 +85,23 @@ export const resolvers: OrganizationModule.Resolvers = {
// This is the organization that got stored as an cookie
// We make sure it actually exists before directing to it.
if (previouslyVisitedOrganizationId) {
const orgId = await injector.get(IdTranslator).translateOrganizationId({
const orgId = await injector.get(IdTranslator).translateOrganizationIdSafe({
organization: previouslyVisitedOrganizationId,
});
const org = await organizationManager.getOrganization({
organization: orgId,
});
if (orgId) {
const org = await organizationManager.getOrganization({
organization: orgId,
});
if (org) {
return {
selector: {
organization: org.cleanId,
},
organization: org,
};
if (org) {
return {
selector: {
organization: org.cleanId,
},
organization: org,
};
}
}
}

View file

@ -17,7 +17,22 @@ export class IdTranslator {
}
@cache<OrganizationSelector>(selector => selector.organization)
translateOrganizationId(selector: OrganizationSelector) {
async translateOrganizationId(selector: OrganizationSelector) {
this.logger.debug(
'Translating Organization Clean ID (selector=%o)',
filterSelector('organization', selector),
);
const organizationId = await this.storage.getOrganizationId(selector);
if (!organizationId) {
throw new Error('Organization not found');
}
return organizationId;
}
@cache<OrganizationSelector>(selector => selector.organization)
translateOrganizationIdSafe(selector: OrganizationSelector) {
this.logger.debug(
'Translating Organization Clean ID (selector=%o)',
filterSelector('organization', selector),

View file

@ -87,7 +87,7 @@ export interface Storage {
updateUser(_: { id: string; fullName: string; displayName: string }): Promise<User | never>;
getOrganizationId(_: OrganizationSelector): Promise<string | never>;
getOrganizationId(_: OrganizationSelector): Promise<string | null>;
getOrganizationByInviteCode(_: { inviteCode: string }): Promise<Organization | null>;
getOrganizationByCleanId(_: { cleanId: string }): Promise<Organization | null>;
getOrganizationByGitHubInstallationId(_: {

View file

@ -898,12 +898,16 @@ export async function createStorage(
},
async getOrganizationId({ organization }) {
// Based on clean_id, resolve id
const result = await pool.one<Pick<organizations, 'id'>>(
const result = await pool.maybeOne<Pick<organizations, 'id'>>(
sql`/* getOrganizationId */
SELECT id FROM organizations WHERE clean_id = ${organization} LIMIT 1
`,
);
if (!result) {
return null;
}
return result.id;
},
getOrganizationOwnerId: batch(async selectors => {

View file

@ -32,3 +32,5 @@ INTEGRATION_GITHUB_APP_NAME="<sync>"
# Stripe
STRIPE_PUBLIC_KEY="<sync>"
LOG_LEVEL=debug

View file

@ -30,9 +30,6 @@ npm-debug.log*
.env.test.local
.env.production.local
# next-env-runtime
public/__ENV.js
# vercel
.vercel

View file

@ -1,5 +1,3 @@
import postcss from 'postcss';
/** @type { import('@storybook/nextjs').StorybookConfig } */
const config = {
stories: ['../src/**/*.stories.@(js|jsx|ts|tsx)'],
@ -7,12 +5,8 @@ const config = {
'@storybook/addon-links',
'@storybook/addon-essentials',
'@storybook/addon-interactions',
'@storybook/addon-styling',
],
framework: {
name: '@storybook/nextjs',
options: {},
},
framework: '@storybook/react-vite',
docs: {
autodocs: 'tag',
},

View file

@ -1,8 +1,6 @@
import '../public/styles.css';
import '../src/index.css';
console.log('[dark]');
if (!document.body.className.includes('dark')) {
console.log('[dark] added');
document.body.className += ' ' + 'dark';
}

View file

@ -1,33 +0,0 @@
import fs from 'node:fs';
import path from 'node:path';
import { ALLOWED_ENVIRONMENT_VARIABLES } from './src/env/frontend-public-variables';
//
// Runtime environment in Next.js
//
configureRuntimeEnv(ALLOWED_ENVIRONMENT_VARIABLES);
// Writes the environment variables to public/__ENV.js file and make them accessible under `window.__ENV`
function configureRuntimeEnv(publicEnvVars: readonly string[]) {
const envObject: Record<string, unknown> = {};
// eslint-disable-next-line no-process-env
const processEnv = process.env;
for (const key in processEnv) {
if (publicEnvVars.includes(key)) {
envObject[key] = processEnv[key];
}
}
const base = fs.realpathSync(process.cwd());
const file = `${base}/public/__ENV.js`;
const content = `window.__ENV = ${JSON.stringify(envObject)};`;
const dirname = path.dirname(file);
if (!fs.existsSync(dirname)) {
fs.mkdirSync(dirname, { recursive: true });
}
fs.writeFileSync(file, content);
}

View file

@ -0,0 +1,16 @@
<!doctype html>
<html lang="en" class="dark">
<head>
<meta charset="UTF-8" />
<link rel="preconnect" href="https://rsms.me/" />
<link rel="stylesheet" href="https://rsms.me/inter/inter.css" />
<link rel="icon" href="/just-logo.svg" type="image/svg+xml" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>GraphQL Hive</title>
<script src="/__env.js"></script>
</head>
<body>
<div class="bg-transparent font-sans text-white" id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View file

@ -1,5 +0,0 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.

View file

@ -1,23 +0,0 @@
import type { NextConfig } from 'next';
import './environment';
export default {
productionBrowserSourceMaps: true,
poweredByHeader: false,
eslint: {
ignoreDuringBuilds: true,
},
typescript: {
// next doesn't need to check because typecheck command will
// also Next.js report false positives (try it...)
ignoreBuildErrors: true,
},
redirects: async () => [
// Redirect organization routes
{
source: '/:organizationId/view/subscription/manage',
destination: '/:organizationId/view/subscription',
permanent: true,
},
],
} satisfies NextConfig;

View file

@ -1,25 +1,30 @@
{
"name": "@hive/app",
"version": "0.0.0",
"type": "module",
"private": true,
"scripts": {
"build": "pnpm generate-changelog && pnpm build:config && next build && tsx ../../../scripts/runify.ts",
"build": "tsx ../../../scripts/runify.ts src/server/index.ts && vite build --outDir dist/client",
"build-storybook": "storybook build",
"build:config": "tsup-node --no-splitting --out-dir . --loader \".mts=ts\" --format esm --target node21 next.config.mts",
"dev": "pnpm generate-changelog && pnpm build:config && next dev --turbo | pino-pretty",
"dev": "node --env-file=.env --watch-path src/server --import tsx src/server/index.ts --dev",
"generate-changelog": "node ../../../scripts/generate-changelog.js",
"postbuild": "rimraf deploy-tmp/web && pnpm --filter @hive/app deploy --prod --no-optional deploy-tmp/web && rimraf dist/node_modules && mv deploy-tmp/web/node_modules dist && rimraf deploy-tmp/web",
"postinstall": "pnpm generate-changelog",
"start": "node dist/index.js",
"storybook": "storybook dev -p 6006",
"typecheck": "tsc"
},
"dependencies": {
"devDependencies": {
"@date-fns/utc": "1.2.0",
"@fastify/cors": "9.0.1",
"@fastify/static": "7.0.4",
"@fastify/vite": "6.0.6",
"@graphiql/react": "0.18.0-alpha.0",
"@graphiql/toolkit": "0.9.1",
"@graphql-codegen/client-preset-swc-plugin": "0.2.0",
"@graphql-tools/mock": "9.0.2",
"@graphql-typed-document-node/core": "3.2.0",
"@headlessui/react": "1.7.17",
"@hive/emails": "workspace:*",
"@hive/server": "workspace:*",
"@hookform/resolvers": "3.3.4",
"@monaco-editor/react": "4.6.0",
"@n1ru4l/react-time-ago": "1.1.0",
@ -45,17 +50,36 @@
"@radix-ui/react-toolbar": "1.0.4",
"@radix-ui/react-tooltip": "1.0.7",
"@repeaterjs/repeater": "3.0.6",
"@sentry/nextjs": "7.114.0",
"@sentry/node": "7.114.0",
"@sentry/react": "7.114.0",
"@sentry/types": "7.114.0",
"@storybook/addon-essentials": "8.1.1",
"@storybook/addon-interactions": "8.1.1",
"@storybook/addon-links": "8.1.1",
"@storybook/blocks": "8.1.1",
"@storybook/react": "8.1.1",
"@storybook/react-vite": "8.1.1",
"@stripe/react-stripe-js": "2.7.1",
"@stripe/stripe-js": "3.4.0",
"@tanstack/react-router": "1.32.13",
"@tanstack/react-table": "8.0.0-beta.8",
"@tanstack/router-devtools": "1.32.13",
"@theguild/editor": "1.2.5",
"@trpc/client": "10.45.2",
"@trpc/server": "10.45.2",
"@types/dompurify": "3.0.5",
"@types/js-cookie": "3.0.6",
"@types/react": "18.3.2",
"@types/react-dom": "18.3.0",
"@types/react-highlight-words": "0.16.7",
"@types/react-virtualized-auto-sizer": "1.0.4",
"@types/react-window": "1.8.8",
"@urql/core": "4.1.4",
"@urql/exchange-auth": "2.1.6",
"@urql/exchange-graphcache": "6.3.3",
"@urql/exchange-persisted": "4.2.0",
"@vitejs/plugin-react": "4.2.1",
"autoprefixer": "10.4.19",
"class-variance-authority": "0.7.0",
"clsx": "2.1.1",
"cmdk": "0.2.1",
@ -63,6 +87,7 @@
"dompurify": "3.1.3",
"echarts": "5.5.0",
"echarts-for-react": "3.0.2",
"fastify": "4.27.0",
"formik": "2.4.6",
"framer-motion": "11.2.3",
"graphiql": "3.0.0-alpha.0",
@ -76,12 +101,13 @@
"mini-svg-data-uri": "1.4.4",
"monaco-editor": "0.48.0",
"monaco-themes": "0.4.4",
"next": "14.2.3",
"pino": "9.1.0",
"pino-http": "9.0.0",
"pino-pretty": "11.0.0",
"react": "18.3.1",
"react-day-picker": "8.10.1",
"react-dom": "18.3.1",
"react-helmet-async": "2.0.5",
"react-highlight-words": "0.20.0",
"react-hook-form": "7.51.4",
"react-icons": "5.2.1",
@ -92,51 +118,28 @@
"react-virtuoso": "4.7.10",
"react-window": "1.8.10",
"regenerator-runtime": "0.14.1",
"rimraf": "4.4.1",
"snarkdown": "2.0.0",
"storybook": "8.1.1",
"supertokens-auth-react": "0.35.6",
"supertokens-web-js": "0.8.0",
"tailwind-merge": "2.3.0",
"tailwindcss": "3.4.3",
"tailwindcss-animate": "1.0.7",
"tailwindcss-radix": "3.0.3",
"tslib": "2.6.2",
"urql": "4.0.5",
"use-debounce": "10.0.0",
"valtio": "1.13.2",
"vite": "5.2.10",
"vite-tsconfig-paths": "4.3.2",
"wonka": "6.3.4",
"yup": "1.4.0",
"zod": "3.23.8"
},
"devDependencies": {
"@graphql-codegen/client-preset-swc-plugin": "0.2.0",
"@graphql-typed-document-node/core": "3.2.0",
"@hive/emails": "workspace:*",
"@hive/server": "workspace:*",
"@sentry/types": "7.114.0",
"@storybook/addon-essentials": "8.1.1",
"@storybook/addon-interactions": "8.1.1",
"@storybook/addon-links": "8.1.1",
"@storybook/addon-styling": "1.3.7",
"@storybook/blocks": "8.1.1",
"@storybook/nextjs": "8.1.1",
"@storybook/react": "8.1.1",
"@types/dompurify": "3.0.5",
"@types/js-cookie": "3.0.6",
"@types/react": "18.3.2",
"@types/react-dom": "18.3.0",
"@types/react-highlight-words": "0.16.7",
"@types/react-virtualized-auto-sizer": "1.0.4",
"@types/react-window": "1.8.8",
"autoprefixer": "10.4.19",
"pino-pretty": "11.0.0",
"postcss": "8.4.38",
"postcss-loader": "8.1.1",
"rimraf": "4.4.1",
"storybook": "8.1.1",
"tailwindcss": "3.4.3",
"tailwindcss-animate": "1.0.7",
"tailwindcss-radix": "3.0.3"
},
"buildOptions": {
"next": {
"header": "./environment.ts"
}
"external": [
"vite"
]
}
}

View file

@ -1,45 +0,0 @@
import Image from 'next/image';
import Router from 'next/router';
import { Button } from '@/components/v2';
import ghost from '../public/images/figures/ghost.svg';
const NotFoundPage = () => {
return (
<>
<div className="flex h-screen flex-col items-center justify-center gap-2.5">
<Image
src={ghost}
alt="Ghost illustration"
width="200"
height="200"
className="drag-none"
/>
<h2 className="text-5xl font-bold">404</h2>
<h3 className="text-xl font-semibold">Page Not Found</h3>
<Button variant="link" onClick={Router.back}>
Go back
</Button>
<style jsx global>{`
html {
background:
url(/images/bg-top-shine.svg) no-repeat left top,
url(/images/bg-bottom-shine.svg) no-repeat right bottom,
#0b0d11;
}
body {
background: transparent !important;
color: #fcfcfc !important;
}
#__next {
color: inherit;
}
`}</style>
</div>
</>
);
};
export default NotFoundPage;

View file

@ -1 +0,0 @@
export { default } from '../checks';

View file

@ -1 +0,0 @@
export { default } from '../history';

View file

@ -1,95 +0,0 @@
import { ReactElement, useEffect } from 'react';
import { AppProps } from 'next/app';
import Router from 'next/router';
import Script from 'next/script';
import { ToastContainer } from 'react-toastify';
import SuperTokens, { SuperTokensWrapper } from 'supertokens-auth-react';
import Session from 'supertokens-auth-react/recipe/session';
import { Provider as UrqlProvider } from 'urql';
import { LoadingAPIIndicator } from '@/components/common/LoadingAPI';
import { frontendConfig } from '@/config/supertokens/frontend';
import { env } from '@/env/frontend';
import * as gtag from '@/lib/gtag';
import { urqlClient } from '@/lib/urql';
import { configureScope, init } from '@sentry/nextjs';
import '../public/styles.css';
import 'react-toastify/dist/ReactToastify.css';
import { Toaster } from '@/components/ui/toaster';
function identifyOnSentry(userId: string, email: string): void {
configureScope(scope => {
scope.setUser({ id: userId, email });
});
}
export default function App({ Component, pageProps }: AppProps): ReactElement {
useEffect(() => {
const handleRouteChange = (url: string) => {
gtag.pageview(url);
};
Router.events.on('routeChangeComplete', handleRouteChange);
Router.events.on('hashChangeComplete', handleRouteChange);
return () => {
Router.events.off('routeChangeComplete', handleRouteChange);
Router.events.off('hashChangeComplete', handleRouteChange);
};
}, []);
useEffect(() => {
void Session.doesSessionExist().then(async doesExist => {
if (!doesExist) {
return;
}
const payload = await Session.getAccessTokenPayloadSecurely();
identifyOnSentry(payload.superTokensUserId, payload.email);
});
}, []);
return (
<>
{env.analytics.googleAnalyticsTrackingId && (
<>
<Script
strategy="afterInteractive"
src={`https://www.googletagmanager.com/gtag/js?id=${env.analytics.googleAnalyticsTrackingId}`}
/>
<Script
id="gtag-init"
strategy="afterInteractive"
dangerouslySetInnerHTML={{
__html: `
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', '${env.analytics.googleAnalyticsTrackingId}', {
page_path: window.location.pathname,
});`,
}}
/>
</>
)}
<SuperTokensWrapper>
<UrqlProvider value={urqlClient}>
<LoadingAPIIndicator />
<Component {...pageProps} />
</UrqlProvider>
</SuperTokensWrapper>
<Toaster />
<ToastContainer hideProgressBar />
</>
);
}
if (globalThis.window) {
SuperTokens.init(frontendConfig());
if (env.sentry) {
init({
dsn: env.sentry.dsn,
enabled: true,
dist: 'webapp',
release: env.release,
environment: env.environment,
});
}
}

View file

@ -1,42 +0,0 @@
import Document, { DocumentContext, Head, Html, Main, NextScript } from 'next/document';
import 'regenerator-runtime/runtime';
export default class MyDocument extends Document<{
ids: Array<string>;
css: string;
}> {
static async getInitialProps(ctx: DocumentContext) {
const initialProps = await Document.getInitialProps(ctx);
const page = await ctx.renderPage();
return {
...initialProps,
...page,
};
}
render() {
return (
<Html className="dark">
<Head>
<style
dangerouslySetInnerHTML={{
__html:
// we setup background via style tag to prevent white flash on initial page loading
'html {background: #030711}',
}}
/>
<link rel="preconnect" href="https://rsms.me/" />
<link rel="stylesheet" href="https://rsms.me/inter/inter.css" />
<link rel="icon" href="/just-logo.svg" type="image/svg+xml" />
{/* eslint-disable-next-line @next/next/no-sync-scripts -- if it's not sync, then env variables are not present) */}
<script src="/__ENV.js" />
</Head>
<body className="bg-transparent font-sans text-white">
<Main />
<NextScript />
</body>
</Html>
);
}
}

View file

@ -1,72 +0,0 @@
import { NextPageContext } from 'next';
import NextErrorComponent from 'next/error';
import { getLogger } from '@/server-logger';
import { captureException, flush } from '@sentry/nextjs';
const MyError = ({
statusCode,
hasGetInitialPropsRun,
err,
}: {
statusCode: number;
hasGetInitialPropsRun: boolean;
err: Error;
}) => {
if (!hasGetInitialPropsRun && err) {
// getInitialProps is not called in case of
// https://github.com/vercel/next.js/issues/8592. As a workaround, we pass
// err via _app.js so it can be captured
captureException(err);
// Flushing is not required in this case as it only happens on the client
}
return <NextErrorComponent statusCode={statusCode} />;
};
MyError.getInitialProps = async (props: NextPageContext) => {
const logger = getLogger(props.req);
const errorInitialProps = await NextErrorComponent.getInitialProps({
res: props.res,
err: props.err,
} as any);
const { err, asPath } = props;
// Workaround for https://github.com/vercel/next.js/issues/8592, mark when
// getInitialProps has run
(errorInitialProps as any).hasGetInitialPropsRun = true;
// Running on the server, the response object (`res`) is available.
//
// Next.js will pass an err on the server if a page's data fetching methods
// threw or returned a Promise that rejected
//
// Running on the client (browser), Next.js will provide an err if:
//
// - a page's `getInitialProps` threw or returned a Promise that rejected
// - an exception was thrown somewhere in the React lifecycle (render,
// componentDidMount, etc.) that was caught by Next.js' React Error
// Boundary. Read more about what types of exceptions are caught by Error
// Boundaries: https://reactjs.org/docs/error-boundaries.html
if (err) {
captureException(err);
logger.error(err);
// Flushing before returning is necessary if deploying to Vercel, see
// https://vercel.com/docs/platform/limits#streaming-responses
await flush(2000);
return errorInitialProps;
}
// If this point is reached, getInitialProps was called without any
// information about what the error might be. This is unexpected and may
// indicate a bug introduced in Next.js, so record it in Sentry
captureException(new Error(`_error.tsx getInitialProps missing data at path: ${asPath}`));
logger.error(`_error.tsx getInitialProps missing data at path: ${asPath}`);
await flush(2000);
return errorInitialProps;
};
export default MyError;

View file

@ -1,150 +0,0 @@
import { ReactElement, useState } from 'react';
import clsx from 'clsx';
import { Button, Heading, HiveLink, Input, Link, ShineBackground } from '@/components/v2';
import { GitHubIcon, GoogleIcon, LinkedInIcon } from '@/components/v2/icon';
const IndexPage = (): ReactElement => {
const [isLoginPage, setIsLoginPage] = useState(true);
return (
<div className="flex">
<ShineBackground />
<div
className="
flex h-screen
w-1/3
flex-col
bg-gray-800/10 pb-[50px] pl-[30px] pr-[50px] pt-[30px]
"
>
<GuildLink textClassName="flex-col" />
<div className="mt-[50px] grow rounded-[30px] bg-gray-800/30" />
<Heading size="2xl" className="mb-[5px] mt-[22px]">
Hive
</Heading>
<p className="font-light text-gray-500">Open GraphQL Platform</p>
</div>
<div className="grow px-9 pb-9 pt-11">
<div className="mx-auto flex w-[500px] flex-col">
<HiveLink />
<h2 className="mb-1 mt-20 text-2xl font-light text-white">
{isLoginPage ? 'Log In' : 'Create an account'}
</h2>
<p className="font-light text-[#9b9b9b]">
{isLoginPage ? "Don't Have An Account" : 'Already A Member'}?{' '}
<Button
variant="link"
onClick={() => setIsLoginPage(prev => !prev)}
className="font-light"
>
{isLoginPage ? 'Create Account' : 'Log In'}
</Button>
</p>
<div className="my-5 flex gap-x-3.5">
<Button size="large" block variant="secondary" title="Log in with GitHub">
<GitHubIcon />
<span className="grow">Continue with GitHub</span>
</Button>
<Button size="large" variant="secondary" title="Log in with Google">
<GoogleIcon />
</Button>
<Button size="large" variant="secondary" title="Log in with LinkedIn">
<LinkedInIcon />
</Button>
</div>
<div className="mb-5 flex items-center">
<hr className="grow border-gray-800" />
<span className="px-5 text-sm font-medium text-gray-500">Or</span>
<hr className="grow border-gray-800" />
</div>
<Input placeholder="Email" />
<Input placeholder="Password" type="password" className="mb-1 mt-7" />
<Link
variant="primary"
href="#"
className={clsx('mb-11 ml-auto', !isLoginPage && 'invisible')}
>
Forgot password?
</Link>
<Button size="large" variant="primary" block>
{isLoginPage ? 'Log In' : 'Create Account'}
</Button>
<GuildLink className="mx-auto mb-6 mt-20 opacity-10 transition hover:opacity-100" />
<p className={clsx('mb-4 text-xs text-[#9b9b9b]', isLoginPage && 'invisible')}>
Creating an account means you're okay with our{' '}
<Link variant="primary" href="#">
Terms of Service
</Link>
,{' '}
<Link variant="primary" href="#">
Privacy Policy
</Link>
, and our default Notification Settings.
</p>
<p className="text-xs text-gray-800">
This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service
apple
</p>
</div>
</div>
</div>
);
};
const GuildLink = ({
className,
textClassName,
}: {
className?: string;
textClassName?: string;
}): ReactElement => (
<a
href="https://the-guild.dev"
target="_blank"
rel="noreferrer"
title="The Guild website homepage"
className={clsx('flex items-center', className)}
>
<svg
width="34"
height="37"
viewBox="0 0 34 37"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M1.38036 13.5726C1.79761 13.7147 2.24265 13.7954 2.70718 13.7954C3.12443 13.7954 3.52689 13.7314 3.90696 13.6158V24.6009C3.90696 24.9891 4.11414 25.3507 4.44837 25.5448L14.9547 31.6536C15.4517 31.0695 16.1877 30.6977 17.0096 30.6977C17.904 30.6977 18.6955 31.1382 19.1889 31.8132C19.1976 31.8256 19.207 31.8372 19.2157 31.8492C19.2503 31.8986 19.2828 31.9492 19.3142 32.0011L19.3503 32.0604C19.3781 32.1084 19.4037 32.1571 19.4282 32.2065C19.4416 32.233 19.4542 32.2599 19.4669 32.2868C19.4878 32.3326 19.5077 32.3792 19.5264 32.4264C19.5398 32.4602 19.5517 32.494 19.5636 32.5282C19.5788 32.5714 19.5936 32.6151 19.6062 32.659C19.6185 32.7008 19.6282 32.743 19.6387 32.7852L19.6657 32.904C19.6762 32.9567 19.6831 33.0102 19.6903 33.064C19.6943 33.096 19.7004 33.1272 19.7033 33.1592C19.7116 33.2464 19.7166 33.3344 19.7166 33.4238C19.7166 33.5663 19.7026 33.7048 19.6813 33.8415L19.6737 33.8924C19.4524 35.1729 18.3433 36.15 17.0096 36.15C15.8203 36.15 14.81 35.3725 14.4472 34.2966L3.18507 27.7487C2.06795 27.0995 1.38036 25.8992 1.38036 24.6009V13.5726ZM30.8608 6.88911C32.3536 6.88911 33.5678 8.11188 33.5678 9.61527C33.5678 10.4331 33.2069 11.1659 32.6388 11.6661V24.6009C32.6388 25.8992 31.9512 27.0995 30.8341 27.7487L21.1579 33.3744C21.1471 32.454 20.8381 31.6067 20.3256 30.9201L29.5708 25.5448C29.905 25.3507 30.1122 24.9891 30.1122 24.6009V12.2331C28.9828 11.9053 28.1537 10.857 28.1537 9.61527C28.1537 9.01079 28.3526 8.45356 28.6846 8.00102C28.6897 7.99375 28.6951 7.98684 28.7002 7.97957C28.763 7.89597 28.8301 7.81528 28.9016 7.73931L28.9113 7.72877C29.0597 7.57319 29.2257 7.43616 29.4069 7.31948C29.4257 7.30712 29.4455 7.29585 29.4646 7.28385C29.5365 7.2406 29.6097 7.19989 29.6855 7.16318C29.7076 7.15263 29.7296 7.141 29.752 7.13083C29.8469 7.08793 29.9436 7.04831 30.044 7.01669C30.0443 7.01669 30.0443 7.01669 30.0443 7.01633L30.2408 6.96179C30.44 6.91446 30.6475 6.88911 30.8608 6.88911ZM27.1611 12.7668V21.7828C27.1611 22.8507 26.5905 23.846 25.6722 24.3799V24.3803L18.4364 28.5826L17.7687 28.9512L17.7766 28.1798V24.9833L23.7322 21.5207V18.3496L18.6627 16.8968L27.1611 12.7668ZM6.85803 12.7614L10.287 14.4553V21.5204L16.1523 24.9307V28.9182L8.34692 24.3804C7.42868 23.846 6.85803 22.8508 6.85803 21.7829V12.7614ZM15.5206 6.67294C16.4248 6.1477 17.5942 6.1477 18.4984 6.67294L26.4366 11.3107L25.6822 11.6702L22.7373 13.1252L17.0095 9.79495L11.2817 13.1252L7.59429 11.3031L8.30282 10.8734C8.30931 10.8676 8.32772 10.8552 8.34757 10.844L15.5206 6.67294ZM17.0095 0C17.6328 0 18.2558 0.162116 18.8142 0.486712L28.5477 6.14624C27.8313 6.63259 27.2769 7.33958 26.9719 8.16761L17.5509 2.69055C17.3866 2.59459 17.1993 2.54443 17.0095 2.54443C16.8196 2.54443 16.6326 2.59459 16.468 2.69055L5.37084 9.14285C5.39755 9.2966 5.41415 9.45363 5.41415 9.61538C5.41415 10.7884 4.6735 11.7876 3.63939 12.1714C3.63145 12.1743 3.62315 12.1776 3.61521 12.1802C3.53327 12.2096 3.45026 12.2351 3.36508 12.2565L3.31418 12.2696C3.23622 12.2878 3.15645 12.3019 3.07632 12.3132L3.01099 12.323C2.91101 12.3343 2.80994 12.3416 2.70708 12.3416C2.59699 12.3416 2.4887 12.3328 2.3815 12.3197C2.35263 12.3161 2.32448 12.311 2.29596 12.3067C2.21367 12.2939 2.13281 12.2776 2.05305 12.2576C2.0285 12.2514 2.00396 12.2456 1.97941 12.2387C1.76826 12.1787 1.56758 12.0955 1.38025 11.989L1.21935 11.8898C0.485782 11.4019 0 10.5653 0 9.61538C0 8.11199 1.21457 6.88921 2.70708 6.88921C3.09437 6.88921 3.46181 6.97282 3.79496 7.12076L15.2047 0.486712C15.7631 0.162116 16.3865 0 17.0095 0Z" />
</svg>
<div className={clsx('ml-2 flex gap-1', textClassName)}>
<svg
width="31"
height="11"
viewBox="0 0 31 11"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M0.313477 2.77294H3.57946V10.6541H6.26751V2.77294H9.53349V0.163818H0.313477V2.77294Z" />
<path d="M17.8588 0.163818V4.23889H13.5848V0.163818H10.9102V10.6541H13.5848V6.75386H17.8588V10.6541H20.5468V0.163818H17.8588Z" />
<path d="M22.568 10.6541H30.6187V8.05842H25.2561V6.71352H29.6645V4.27923H25.2561V2.77294H30.6187V0.163818H22.568V10.6541Z" />
</svg>
<svg
width="47"
height="11"
viewBox="0 0 47 11"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M5.53497 6.91933H8.05247V7.20426C7.55963 7.90361 6.76042 8.35689 5.82801 8.35689C4.25624 8.35689 3.00414 7.1395 3.00414 5.61129C3.00414 4.08308 4.25624 2.86568 5.82801 2.86568C6.73378 2.86568 7.53299 3.26716 8.05247 3.90176L10.2237 2.47716C9.22464 1.20796 7.61291 0.36615 5.82801 0.36615C2.81766 0.36615 0.313477 2.72323 0.313477 5.61129C0.313477 8.49935 2.81766 10.8564 5.82801 10.8564C6.89362 10.8564 7.94591 10.4679 8.45208 9.71674V10.6622H10.5433V4.76948H5.53497V6.91933Z" />
<path d="M19.0352 0.560414V6.09047C19.0352 7.55393 18.3026 8.35689 16.904 8.35689C15.5187 8.35689 14.7994 7.55393 14.7994 6.09047V0.560414H12.1354V6.24588C12.1354 8.84903 13.7871 10.8564 16.904 10.8564C20.0076 10.8564 21.6859 8.84903 21.6859 6.24588V0.560414H19.0352Z" />
<path d="M23.5364 0.560414V10.6622H26.2004V0.560414H23.5364Z" />
<path d="M28.1958 10.6622H35.8283V8.16263H30.8465V0.560414H28.1958V10.6622Z" />
<path d="M37.1999 10.6622H42.0218C45.2719 10.6622 46.937 8.36984 46.937 5.61129C46.937 2.86569 45.2719 0.560414 42.0218 0.560414H37.1999V10.6622ZM41.822 3.0729C43.4071 3.0729 44.2463 4.09603 44.2463 5.61129C44.2463 7.12655 43.4071 8.16263 41.822 8.16263H39.864V3.0729H41.822Z" />
</svg>
</div>
</a>
);
export default IndexPage;

View file

@ -1,45 +0,0 @@
import { NextApiRequest, NextApiResponse } from 'next';
import { env } from '@/env/backend';
import { graphql } from '@/lib/api/utils';
export async function ensureGithubIntegration(
req: NextApiRequest,
input: {
installationId: string;
orgId: string;
},
) {
const { orgId, installationId } = input;
await graphql({
url: env.graphqlPublicEndpoint,
headers: {
...req.headers,
'content-type': 'application/json',
'graphql-client-name': 'Hive App',
'graphql-client-version': env.release,
},
operationName: 'addGitHubIntegration',
query: /* GraphQL */ `
mutation addGitHubIntegration($input: AddGitHubIntegrationInput!) {
addGitHubIntegration(input: $input)
}
`,
variables: {
input: {
organization: orgId,
installationId,
},
},
});
}
export default async function githubCallback(req: NextApiRequest, res: NextApiResponse) {
const installationId = req.query.installation_id as string;
const orgId = req.query.state as string;
await ensureGithubIntegration(req, {
installationId,
orgId,
});
res.redirect(`/${orgId}/view/settings`);
}

View file

@ -1,20 +0,0 @@
import { NextApiRequest, NextApiResponse } from 'next';
import { env } from '@/env/backend';
import { getLogger } from '@/server-logger';
export default async function githubConnectOrg(req: NextApiRequest, res: NextApiResponse) {
const logger = getLogger(req);
if (!env.github) {
logger.error('GitHub is not set up.');
throw new Error('GitHub is not set up.');
}
const { organizationId } = req.query;
logger.info('Connect to GitHub (orgId=%s)', organizationId);
const url = `https://github.com/apps/${env.github.appName}/installations/new`;
const redirectUrl = `${env.appBaseUrl}/api/github/callback`;
res.redirect(`${url}?state=${organizationId}&redirect_url=${redirectUrl}`);
}

View file

@ -1,54 +0,0 @@
import { NextApiRequest, NextApiResponse } from 'next';
import { env } from '@/env/backend';
import { graphql } from '@/lib/api/utils';
import { getLogger } from '@/server-logger';
import { ensureGithubIntegration } from './callback';
export default async function githubSetupCallback(req: NextApiRequest, res: NextApiResponse) {
const logger = getLogger(req);
const installationId = req.query.installation_id as string;
let orgId = req.query.state as string | undefined;
logger.info('GitHub setup callback (installationId=%s, orgId=%s)', installationId, orgId);
if (orgId) {
await ensureGithubIntegration(req, {
installationId,
orgId,
});
} else {
const result = await graphql<{
organizationByGitHubInstallationId?: {
cleanId: string;
};
}>({
url: env.graphqlPublicEndpoint,
headers: {
...req.headers,
'content-type': 'application/json',
'graphql-client-name': 'Hive App',
'graphql-client-version': env.release,
},
operationName: 'getOrganizationByGitHubInstallationId',
query: /* GraphQL */ `
query getOrganizationByGitHubInstallationId($installation: ID!) {
organizationByGitHubInstallationId(input: $input) {
id
cleanId
}
}
`,
variables: {
installation: installationId,
},
});
orgId = result.data?.organizationByGitHubInstallationId?.cleanId;
}
if (orgId) {
res.redirect(`/${orgId}/view/settings`);
} else {
res.redirect('/');
}
}

View file

@ -1,5 +0,0 @@
import { NextApiRequest, NextApiResponse } from 'next';
export default async function health(req: NextApiRequest, res: NextApiResponse) {
res.status(200).json({});
}

View file

@ -1,104 +0,0 @@
import { NextApiRequest, NextApiResponse } from 'next';
import { buildSchema, execute, GraphQLError, parse } from 'graphql';
import { env } from '@/env/backend';
import { getLogger } from '@/server-logger';
import { addMocksToSchema } from '@graphql-tools/mock';
// TODO: check if lab is working
async function lab(req: NextApiRequest, res: NextApiResponse) {
const logger = getLogger(req);
const url = env.graphqlPublicEndpoint;
const labParams = req.query.lab || [];
if (labParams.length < 3) {
res.status(400).json({
error: 'Missing Lab Params',
});
return;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [organization, project, target, mock] = labParams as string[];
const headers: Record<string, string> = {};
if (req.headers['x-hive-key']) {
headers['Authorization'] = `Bearer ${req.headers['x-hive-key'] as string}`;
} else {
headers['Cookie'] = req.headers.cookie as string;
}
const body = {
operationName: 'lab',
query: /* GraphQL */ `
query lab($selector: TargetSelectorInput!) {
lab(selector: $selector) {
schema
mocks
}
}
`,
variables: {
selector: {
organization,
project,
target,
},
},
};
if (req['headers']['x-request-id']) {
headers['x-request-id'] = req['headers']['x-request-id'] as string;
}
const response = await fetch(url, {
headers: {
'content-type': 'application/json',
'graphql-client-name': 'Hive App',
'graphql-client-version': env.release,
...headers,
},
credentials: 'include',
method: 'POST',
body: JSON.stringify(body),
});
const parsedData = await response.json();
if (!parsedData.data?.lab?.schema) {
res.status(200).json({
errors: [new GraphQLError('Please publish your first schema to Hive')],
});
return;
}
if (parsedData.data?.errors?.length > 0) {
res.status(200).json(parsedData.data);
}
try {
const rawSchema = buildSchema(parsedData.data.lab?.schema);
const document = parse(req.body.query);
const mockedSchema = addMocksToSchema({
schema: rawSchema,
preserveResolvers: false,
});
const result = await execute({
schema: mockedSchema,
document,
variableValues: req.body.variables || {},
contextValue: {},
});
res.status(200).json(result);
} catch (e) {
logger.error(e);
res.status(200).json({
errors: [e],
});
}
}
export default lab;

View file

@ -1,73 +0,0 @@
import { stringify } from 'node:querystring';
import { NextApiRequest, NextApiResponse } from 'next';
import { env } from '@/env/backend';
import { graphql } from '@/lib/api/utils';
import { getLogger } from '@/server-logger';
async function fetchData({
url,
headers,
body,
}: {
url: string;
headers: Record<string, any>;
body: string;
}) {
const response = await fetch(url, {
headers,
method: 'POST',
body,
} as any);
return response.json();
}
export default async function slackCallback(req: NextApiRequest, res: NextApiResponse) {
const logger = getLogger(req);
if (env.slack === null) {
throw new Error('The Slack integration is not enabled.');
}
const { code } = req.query;
const orgId = req.query.state;
logger.info('Fetching data from Slack API (orgId=%s)', orgId);
const slackResponse = await fetchData({
url: 'https://slack.com/api/oauth.v2.access',
headers: {
'content-type': 'application/x-www-form-urlencoded',
},
body: stringify({
client_id: env.slack.clientId,
client_secret: env.slack.clientSecret,
code,
}),
});
const token = slackResponse.access_token;
await graphql({
url: env.graphqlPublicEndpoint,
headers: {
...req.headers,
'content-type': 'application/json',
'graphql-client-name': 'Hive App',
'graphql-client-version': env.release,
},
operationName: 'addSlackIntegration',
query: /* GraphQL */ `
mutation addSlackIntegration($input: AddSlackIntegrationInput!) {
addSlackIntegration(input: $input)
}
`,
variables: {
input: {
organization: orgId,
token,
},
},
});
res.redirect(`/${orgId}/view/settings`);
}

View file

@ -1,19 +0,0 @@
import { NextApiRequest, NextApiResponse } from 'next';
import { env } from '@/env/backend';
import { getLogger } from '@/server-logger';
export default async function slackConnectOrg(req: NextApiRequest, res: NextApiResponse) {
const logger = getLogger(req);
logger.info('Connect to Slack');
if (env.slack === null) {
logger.error('The Slack integration is not enabled.');
throw new Error('The Slack integration is not enabled.');
}
const { organizationId } = req.query;
logger.info('Connect organization to Slack (id=%s)', organizationId);
const slackUrl = `https://slack.com/oauth/v2/authorize?scope=incoming-webhook,chat:write,chat:write.public,commands&client_id=${env.slack.clientId}`;
const redirectUrl = `${env.appBaseUrl}/api/slack/callback`;
res.redirect(`${slackUrl}&state=${organizationId}&redirect_uri=${redirectUrl}`);
}

View file

@ -1,36 +0,0 @@
import { GraphiQL } from 'graphiql';
import { HiveLogo } from '@/components/v2/icon';
import { createGraphiQLFetcher } from '@graphiql/toolkit';
import 'graphiql/graphiql.css';
import { env } from '@/env/frontend';
import { useBrowser } from '@/lib/hooks/use-browser';
export default function DevPage() {
return (
<div className="mt-20 size-full">
<style global jsx>{`
body.graphiql-dark .graphiql-container {
--color-base: transparent;
--color-primary: 40, 89%, 60%;
}
`}</style>
<Editor />
</div>
);
}
function Editor() {
const isBrowser = useBrowser();
if (!isBrowser) {
return null;
}
return (
<GraphiQL fetcher={createGraphiQLFetcher({ url: env.graphqlPublicEndpoint })}>
<GraphiQL.Logo>
<HiveLogo className="size-6" />
</GraphiQL.Logo>
</GraphiQL>
);
}

View file

@ -1,14 +0,0 @@
import React from 'react';
import { useRouter } from 'next/router';
import { signOut } from 'supertokens-auth-react/recipe/thirdpartyemailpassword';
export default function LogOutPage() {
const router = useRouter();
React.useEffect(() => {
void signOut().then(() => {
void router.replace('/');
});
});
return null;
}

View file

@ -437,7 +437,7 @@ export function AdminStats({
);
return (
<DataWrapper query={query}>
<DataWrapper query={query} organizationId={null}>
{({ data }) => (
<div className="flex flex-col gap-6">
<div className="flex justify-between rounded-md border border-gray-800 bg-gray-900/50 p-5">

View file

@ -1,18 +1,10 @@
import { ComponentProps, ReactElement, ReactNode } from 'react';
import Head from 'next/head';
import { clsx } from 'clsx';
export const Title = ({ title }: { title: string }): ReactElement => (
<Head>
<title>{title} - GraphQL Hive</title>
<meta property="og:title" content={`${title} - GraphQL Hive`} key="title" />
</Head>
);
import { cn } from '@/lib/utils';
export function Label({ className, children, ...props }: ComponentProps<'span'>): ReactElement {
return (
<span
className={clsx(
className={cn(
`
inline-block
rounded bg-yellow-50
@ -47,7 +39,7 @@ export const Page = ({
className?: string;
}): ReactElement => {
return (
<div className={clsx('relative flex h-full flex-col', className)}>
<div className={cn('relative flex h-full flex-col', className)}>
<div className="flex shrink-0 flex-row items-center justify-between p-4">
<div>
<h2 className="text-xl font-bold text-black dark:text-white">{title}</h2>
@ -59,7 +51,7 @@ export const Page = ({
children
) : (
<div
className={clsx(
className={cn(
'px-4 pb-4 dark:text-white',
scrollable ? 'grow overflow-y-auto' : 'h-full',
)}
@ -73,17 +65,17 @@ export const Page = ({
export const Section = {
Title: ({ className, children, ...props }: ComponentProps<'h3'>): ReactElement => (
<h3 className={clsx('text-base font-bold text-black dark:text-white', className)} {...props}>
<h3 className={cn('text-base font-bold text-black dark:text-white', className)} {...props}>
{children}
</h3>
),
BigTitle: ({ className, children, ...props }: ComponentProps<'h2'>): ReactElement => (
<h2 className={clsx('text-base font-bold text-black dark:text-white', className)} {...props}>
<h2 className={cn('text-base font-bold text-black dark:text-white', className)} {...props}>
{children}
</h2>
),
Subtitle: ({ className, children, ...props }: ComponentProps<'div'>): ReactElement => (
<div className={clsx('text-sm text-gray-600 dark:text-gray-300', className)} {...props}>
<div className={cn('text-sm text-gray-600 dark:text-gray-300', className)} {...props}>
{children}
</div>
),
@ -101,11 +93,11 @@ export function Scale({
className?: string;
}): ReactElement {
return (
<div className={clsx('flex grow-0 flex-row space-x-1', className)}>
<div className={cn('flex grow-0 flex-row space-x-1', className)}>
{new Array(size).fill(null).map((_, i) => (
<div
key={i}
className={clsx('h-4 w-1', value >= i * (max / size) ? 'bg-emerald-400' : 'bg-gray-200')}
className={cn('h-4 w-1', value >= i * (max / size) ? 'bg-emerald-400' : 'bg-gray-200')}
/>
))}
</div>

View file

@ -0,0 +1,54 @@
import { useEffect } from 'react';
import { LogOutIcon } from 'lucide-react';
import { useSessionContext } from 'supertokens-auth-react/recipe/session';
import { Button } from '@/components/ui/button';
import { captureException, flush } from '@sentry/react';
import { useRouter } from '@tanstack/react-router';
export function ErrorComponent(props: { error: any }) {
const router = useRouter();
const session = useSessionContext();
useEffect(() => {
captureException(props.error);
void flush(2000);
}, []);
const isLoggedIn = (session.loading === false && session?.doesSessionExist === true) || false;
return (
<div className="flex size-full items-center justify-center">
{isLoggedIn ? (
<Button
variant="outline"
onClick={() =>
router.navigate({
to: '/logout',
})
}
className="absolute right-6 top-6"
>
<LogOutIcon className="mr-2 size-4" /> Sign out
</Button>
) : null}
<div className="flex max-w-[960px] flex-col items-center gap-x-6 sm:flex-row">
<img src="/images/figures/connection.svg" alt="Ghost" className="block size-[200px]" />
<div className="grow text-center sm:text-left">
<h1 className="text-xl font-semibold">Oops, something went wrong.</h1>
<div className="mt-2">
<div className="text-sm">
<p>Don't worry, our technical support got this error reported automatically.</p>
<p>
If you wish to track it later or share more details with us,{' '}
<Button variant="link" className="h-auto p-0 text-orange-500" asChild>
<a href="emailto:support@graphql-hive.com">you can use the support</a>
</Button>
.
</p>
</div>
</div>
</div>
</div>
</div>
);
}

View file

@ -1,5 +1,4 @@
import { ReactElement, ReactNode } from 'react';
import NextLink from 'next/link';
import { Button } from '@/components/ui/button';
import { Select, SelectContent, SelectItem, SelectTrigger } from '@/components/ui/select';
import { UserMenu } from '@/components/ui/user-menu';
@ -14,8 +13,9 @@ import {
useOrganizationAccess,
} from '@/lib/access/organization';
import { getIsStripeEnabled } from '@/lib/billing/stripe-public-key';
import { useRouteSelector, useToggle } from '@/lib/hooks';
import { useToggle } from '@/lib/hooks';
import { useLastVisitedOrganizationWriter } from '@/lib/last-visited-org';
import { Link, useRouter } from '@tanstack/react-router';
import { ProPlanBilling } from '../organization/billing/ProPlanBillingWarm';
import { RateLimitWarn } from '../organization/billing/RateLimitWarn';
@ -69,11 +69,12 @@ export function OrganizationLayout({
page?: Page;
className?: string;
me: FragmentType<typeof OrganizationLayout_MeFragment> | null;
organizationId: string;
currentOrganization: FragmentType<typeof OrganizationLayout_CurrentOrganizationFragment> | null;
organizations: FragmentType<typeof OrganizationLayout_OrganizationConnectionFragment> | null;
children: ReactNode;
}): ReactElement | null {
const router = useRouteSelector();
const router = useRouter();
const [isModalOpen, toggleModalOpen] = useToggle();
const currentOrganization = useFragment(
@ -85,6 +86,7 @@ export function OrganizationLayout({
member: currentOrganization?.me ?? null,
scope: OrganizationAccessScope.Read,
redirect: true,
organizationId: props.organizationId,
});
useLastVisitedOrganizationWriter(currentOrganization?.cleanId);
@ -107,8 +109,11 @@ export function OrganizationLayout({
<Select
defaultValue={currentOrganization.cleanId}
onValueChange={id => {
router.visitOrganization({
organizationId: id,
void router.navigate({
to: '/$organizationId',
params: {
organizationId: id,
},
});
}}
>
@ -144,85 +149,64 @@ export function OrganizationLayout({
<Tabs value={page}>
<Tabs.List>
<Tabs.Trigger value={Page.Overview} asChild>
<NextLink
href={{
pathname: '/[organizationId]',
query: { organizationId: currentOrganization.cleanId },
}}
<Link
to="/$organizationId"
params={{ organizationId: currentOrganization.cleanId }}
>
Overview
</NextLink>
</Link>
</Tabs.Trigger>
{canAccessOrganization(OrganizationAccessScope.Members, meInCurrentOrg) && (
<Tabs.Trigger value={Page.Members} asChild>
<NextLink
href={{
pathname: `/[organizationId]/view/${Page.Members}`,
query: {
organizationId: currentOrganization.cleanId,
},
}}
<Link
to="/$organizationId/view/members"
params={{ organizationId: currentOrganization.cleanId }}
search={{ page: 'list' }}
>
Members
</NextLink>
</Link>
</Tabs.Trigger>
)}
{canAccessOrganization(OrganizationAccessScope.Settings, meInCurrentOrg) && (
<>
<Tabs.Trigger value={Page.Policy} asChild>
<NextLink
href={{
pathname: `/[organizationId]/view/${Page.Policy}`,
query: {
organizationId: currentOrganization.cleanId,
},
}}
<Link
to="/$organizationId/view/policy"
params={{ organizationId: currentOrganization.cleanId }}
>
Policy
</NextLink>
</Link>
</Tabs.Trigger>
<Tabs.Trigger value={Page.Settings} asChild>
<NextLink
href={{
pathname: `/[organizationId]/view/${Page.Settings}`,
query: {
organizationId: currentOrganization.cleanId,
},
}}
<Link
to="/$organizationId/view/settings"
params={{ organizationId: currentOrganization.cleanId }}
>
Settings
</NextLink>
</Link>
</Tabs.Trigger>
</>
)}
{canAccessOrganization(OrganizationAccessScope.Read, meInCurrentOrg) &&
env.zendeskSupport && (
<Tabs.Trigger value={Page.Support} asChild>
<NextLink
href={{
pathname: `/[organizationId]/view/${Page.Support}`,
query: {
organizationId: currentOrganization.cleanId,
},
}}
<Link
to="/$organizationId/view/support"
params={{ organizationId: currentOrganization.cleanId }}
>
Support
</NextLink>
</Link>
</Tabs.Trigger>
)}
{getIsStripeEnabled() &&
canAccessOrganization(OrganizationAccessScope.Settings, meInCurrentOrg) && (
<Tabs.Trigger value={Page.Subscription} asChild>
<NextLink
href={{
pathname: `/[organizationId]/view/${Page.Subscription}`,
query: {
organizationId: currentOrganization.cleanId,
},
}}
<Link
to="/$organizationId/view/subscription"
params={{ organizationId: currentOrganization.cleanId }}
>
Subscription
</NextLink>
</Link>
</Tabs.Trigger>
)}
</Tabs.List>
@ -240,7 +224,11 @@ export function OrganizationLayout({
<PlusIcon size={16} className="mr-2" />
New project
</Button>
<CreateProjectModal isOpen={isModalOpen} toggleModalOpen={toggleModalOpen} />
<CreateProjectModal
organizationId={props.organizationId}
isOpen={isModalOpen}
toggleModalOpen={toggleModalOpen}
/>
</>
) : null}
</div>

View file

@ -1,5 +1,4 @@
import { ReactElement, ReactNode } from 'react';
import NextLink from 'next/link';
import { Button } from '@/components/ui/button';
import { Select, SelectContent, SelectItem, SelectTrigger } from '@/components/ui/select';
import { UserMenu } from '@/components/ui/user-menu';
@ -8,8 +7,9 @@ import { PlusIcon } from '@/components/v2/icon';
import { CreateTargetModal } from '@/components/v2/modals';
import { FragmentType, graphql, useFragment } from '@/gql';
import { canAccessProject, ProjectAccessScope, useProjectAccess } from '@/lib/access/project';
import { useRouteSelector, useToggle } from '@/lib/hooks';
import { useToggle } from '@/lib/hooks';
import { useLastVisitedOrganizationWriter } from '@/lib/last-visited-org';
import { Link, useRouter } from '@tanstack/react-router';
import { ProjectMigrationToast } from '../project/migration-toast';
export enum Page {
@ -78,6 +78,8 @@ export function ProjectLayout({
...props
}: {
page: Page;
organizationId: string;
projectId: string;
className?: string;
me: FragmentType<typeof ProjectLayout_MeFragment> | null;
currentOrganization: FragmentType<typeof ProjectLayout_CurrentOrganizationFragment> | null;
@ -85,11 +87,9 @@ export function ProjectLayout({
organizations: FragmentType<typeof ProjectLayout_OrganizationConnectionFragment> | null;
children: ReactNode;
}): ReactElement | null {
const router = useRouteSelector();
const router = useRouter();
const [isModalOpen, toggleModalOpen] = useToggle();
const { organizationId: orgId } = router;
const currentOrganization = useFragment(
ProjectLayout_CurrentOrganizationFragment,
props.currentOrganization,
@ -100,6 +100,8 @@ export function ProjectLayout({
scope: ProjectAccessScope.Read,
member: currentOrganization?.me ?? null,
redirect: true,
organizationId: props.organizationId,
projectId: props.projectId,
});
useLastVisitedOrganizationWriter(currentOrganization?.cleanId);
@ -122,15 +124,13 @@ export function ProjectLayout({
<div className="flex flex-row items-center gap-4">
<HiveLink className="size-8" />
{currentOrganization ? (
<NextLink
href={{
pathname: '/[organizationId]',
query: { organizationId: currentOrganization.cleanId },
}}
<Link
to="/$organizationId"
params={{ organizationId: currentOrganization.cleanId }}
className="max-w-[200px] shrink-0 truncate font-medium"
>
{currentOrganization.name}
</NextLink>
</Link>
) : (
<div className="h-5 w-48 max-w-[200px] animate-pulse rounded-full bg-gray-800" />
)}
@ -140,9 +140,12 @@ export function ProjectLayout({
<Select
defaultValue={currentProject.cleanId}
onValueChange={id => {
router.visitProject({
organizationId: orgId,
projectId: id,
void router.navigate({
to: '/$organizationId/$projectId',
params: {
organizationId: props.organizationId,
projectId: id,
},
});
}}
>
@ -173,7 +176,7 @@ export function ProjectLayout({
</header>
{page === Page.Settings || currentProject?.registryModel !== 'LEGACY' ? null : (
<ProjectMigrationToast orgId={orgId} projectId={currentProject.cleanId} />
<ProjectMigrationToast orgId={props.organizationId} projectId={currentProject.cleanId} />
)}
<div className="relative border-b border-gray-800">
@ -182,60 +185,52 @@ export function ProjectLayout({
<Tabs value={page}>
<Tabs.List>
<Tabs.Trigger value={Page.Targets} asChild>
<NextLink
href={{
pathname: '/[organizationId]/[projectId]',
query: {
organizationId: currentOrganization.cleanId,
projectId: currentProject.cleanId,
},
<Link
to="/$organizationId/$projectId"
params={{
organizationId: currentOrganization.cleanId,
projectId: currentProject.cleanId,
}}
>
Targets
</NextLink>
</Link>
</Tabs.Trigger>
{canAccessProject(ProjectAccessScope.Alerts, currentOrganization.me) && (
<Tabs.Trigger value={Page.Alerts} asChild>
<NextLink
href={{
pathname: `/[organizationId]/[projectId]/view/${Page.Alerts}`,
query: {
organizationId: currentOrganization.cleanId,
projectId: currentProject.cleanId,
},
<Link
to="/$organizationId/$projectId/view/alerts"
params={{
organizationId: currentOrganization.cleanId,
projectId: currentProject.cleanId,
}}
>
Alerts
</NextLink>
</Link>
</Tabs.Trigger>
)}
{canAccessProject(ProjectAccessScope.Settings, currentOrganization.me) && (
<>
<Tabs.Trigger value={Page.Policy} asChild>
<NextLink
href={{
pathname: `/[organizationId]/[projectId]/view/${Page.Policy}`,
query: {
organizationId: currentOrganization.cleanId,
projectId: currentProject.cleanId,
},
<Link
to="/$organizationId/$projectId/view/policy"
params={{
organizationId: currentOrganization.cleanId,
projectId: currentProject.cleanId,
}}
>
Policy
</NextLink>
</Link>
</Tabs.Trigger>
<Tabs.Trigger value={Page.Settings} asChild>
<NextLink
href={{
pathname: `/[organizationId]/[projectId]/view/${Page.Settings}`,
query: {
organizationId: currentOrganization.cleanId,
projectId: currentProject.cleanId,
},
<Link
to="/$organizationId/$projectId/view/settings"
params={{
organizationId: currentOrganization.cleanId,
projectId: currentProject.cleanId,
}}
>
Settings
</NextLink>
</Link>
</Tabs.Trigger>
</>
)}
@ -254,7 +249,12 @@ export function ProjectLayout({
New target
</Button>
) : null}
<CreateTargetModal isOpen={isModalOpen} toggleModalOpen={toggleModalOpen} />
<CreateTargetModal
organizationId={props.organizationId}
projectId={props.projectId}
isOpen={isModalOpen}
toggleModalOpen={toggleModalOpen}
/>
</div>
</div>
<div className="container h-full pb-7">

View file

@ -1,6 +1,5 @@
import { ReactElement, ReactNode } from 'react';
import NextLink from 'next/link';
import { Link } from 'lucide-react';
import { LinkIcon } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Select, SelectContent, SelectItem, SelectTrigger } from '@/components/ui/select';
import { UserMenu } from '@/components/ui/user-menu';
@ -8,9 +7,10 @@ import { HiveLink, Tabs } from '@/components/v2';
import { ConnectSchemaModal } from '@/components/v2/modals';
import { FragmentType, graphql, useFragment } from '@/gql';
import { canAccessTarget, TargetAccessScope, useTargetAccess } from '@/lib/access/target';
import { useRouteSelector, useToggle } from '@/lib/hooks';
import { useToggle } from '@/lib/hooks';
import { useLastVisitedOrganizationWriter } from '@/lib/last-visited-org';
import { cn } from '@/lib/utils';
import { Link, useRouter } from '@tanstack/react-router';
import { ProjectMigrationToast } from '../project/migration-toast';
export enum Page {
@ -93,6 +93,9 @@ export const TargetLayout = ({
...props
}: {
page: Page;
organizationId: string;
projectId: string;
targetId: string;
className?: string;
children: ReactNode;
connect?: ReactNode;
@ -102,10 +105,10 @@ export const TargetLayout = ({
organizations: FragmentType<typeof TargetLayout_OrganizationConnectionFragment> | null;
isCDNEnabled: FragmentType<typeof TargetLayout_IsCDNEnabledFragment> | null;
}): ReactElement | null => {
const router = useRouteSelector();
const router = useRouter();
const [isModalOpen, toggleModalOpen] = useToggle();
const { organizationId: orgId, projectId } = router;
const { organizationId: orgId, projectId } = props;
const currentOrganization = useFragment(
TargetLayout_CurrentOrganizationFragment,
@ -123,13 +126,16 @@ export const TargetLayout = ({
currentProject?.targets,
);
const targets = targetConnection?.nodes;
const currentTarget = targets?.find(target => target.cleanId === router.targetId);
const currentTarget = targets?.find(target => target.cleanId === props.targetId);
const isCDNEnabled = useFragment(TargetLayout_IsCDNEnabledFragment, props.isCDNEnabled);
useTargetAccess({
scope: TargetAccessScope.Read,
member: currentOrganization?.me ?? null,
redirect: true,
targetId: props.targetId,
projectId,
organizationId: orgId,
});
useLastVisitedOrganizationWriter(currentOrganization?.cleanId);
@ -150,34 +156,30 @@ export const TargetLayout = ({
<div className="flex flex-row items-center gap-4">
<HiveLink className="size-8" />
{currentOrganization ? (
<NextLink
href={{
pathname: '/[organizationId]',
query: {
organizationId: currentOrganization.cleanId,
},
<Link
to="/$organizationId"
params={{
organizationId: currentOrganization.cleanId,
}}
className="max-w-[200px] shrink-0 truncate font-medium"
>
{currentOrganization.name}
</NextLink>
</Link>
) : (
<div className="h-5 w-48 max-w-[200px] animate-pulse rounded-full bg-gray-800" />
)}
<div className="italic text-gray-500">/</div>
{currentOrganization && currentProject ? (
<NextLink
href={{
pathname: '/[organizationId]/[projectId]',
query: {
organizationId: currentOrganization.cleanId,
projectId: currentProject.cleanId,
},
<Link
to="/$organizationId/$projectId"
params={{
organizationId: currentOrganization.cleanId,
projectId: currentProject.cleanId,
}}
className="max-w-[200px] shrink-0 truncate font-medium"
>
{currentProject.name}
</NextLink>
</Link>
) : (
<div className="h-5 w-48 max-w-[200px] animate-pulse rounded-full bg-gray-800" />
)}
@ -187,10 +189,13 @@ export const TargetLayout = ({
<Select
defaultValue={currentTarget.cleanId}
onValueChange={id => {
router.visitTarget({
organizationId: currentOrganization.cleanId,
projectId: currentProject.cleanId,
targetId: id,
void router.navigate({
to: '/$organizationId/$projectId/$targetId',
params: {
organizationId: currentOrganization.cleanId,
projectId: currentProject.cleanId,
targetId: id,
},
});
}}
>
@ -232,105 +237,91 @@ export const TargetLayout = ({
{canAccessSchema && (
<>
<Tabs.Trigger value={Page.Schema} asChild>
<NextLink
href={{
pathname: '/[organizationId]/[projectId]/[targetId]',
query: {
organizationId: currentOrganization.cleanId,
projectId: currentProject.cleanId,
targetId: currentTarget.cleanId,
},
<Link
to="/$organizationId/$projectId/$targetId"
params={{
organizationId: currentOrganization.cleanId,
projectId: currentProject.cleanId,
targetId: currentTarget.cleanId,
}}
>
Schema
</NextLink>
</Link>
</Tabs.Trigger>
<Tabs.Trigger value={Page.Checks} asChild>
<NextLink
href={{
pathname: '/[organizationId]/[projectId]/[targetId]/checks',
query: {
organizationId: currentOrganization.cleanId,
projectId: currentProject.cleanId,
targetId: currentTarget.cleanId,
},
<Link
to="/$organizationId/$projectId/$targetId/checks"
params={{
organizationId: currentOrganization.cleanId,
projectId: currentProject.cleanId,
targetId: currentTarget.cleanId,
}}
>
Checks
</NextLink>
</Link>
</Tabs.Trigger>
<Tabs.Trigger value={Page.Explorer} asChild>
<NextLink
href={{
pathname: '/[organizationId]/[projectId]/[targetId]/explorer',
query: {
organizationId: currentOrganization.cleanId,
projectId: currentProject.cleanId,
targetId: currentTarget.cleanId,
},
<Link
to="/$organizationId/$projectId/$targetId/explorer"
params={{
organizationId: currentOrganization.cleanId,
projectId: currentProject.cleanId,
targetId: currentTarget.cleanId,
}}
>
Explorer
</NextLink>
</Link>
</Tabs.Trigger>
<Tabs.Trigger value={Page.History} asChild>
<NextLink
href={{
pathname: '/[organizationId]/[projectId]/[targetId]/history',
query: {
organizationId: currentOrganization.cleanId,
projectId: currentProject.cleanId,
targetId: currentTarget.cleanId,
},
<Link
to="/$organizationId/$projectId/$targetId/history"
params={{
organizationId: currentOrganization.cleanId,
projectId: currentProject.cleanId,
targetId: currentTarget.cleanId,
}}
>
History
</NextLink>
</Link>
</Tabs.Trigger>
<Tabs.Trigger value={Page.Insights} asChild>
<NextLink
href={{
pathname: '/[organizationId]/[projectId]/[targetId]/insights',
query: {
organizationId: currentOrganization.cleanId,
projectId: currentProject.cleanId,
targetId: currentTarget.cleanId,
},
<Link
to="/$organizationId/$projectId/$targetId/insights"
params={{
organizationId: currentOrganization.cleanId,
projectId: currentProject.cleanId,
targetId: currentTarget.cleanId,
}}
>
Insights
</NextLink>
</Link>
</Tabs.Trigger>
<Tabs.Trigger value={Page.Laboratory} asChild>
<NextLink
href={{
pathname: '/[organizationId]/[projectId]/[targetId]/laboratory',
query: {
organizationId: currentOrganization.cleanId,
projectId: currentProject.cleanId,
targetId: currentTarget.cleanId,
},
<Link
to="/$organizationId/$projectId/$targetId/laboratory"
params={{
organizationId: currentOrganization.cleanId,
projectId: currentProject.cleanId,
targetId: currentTarget.cleanId,
}}
>
Laboratory
</NextLink>
</Link>
</Tabs.Trigger>
</>
)}
{canAccessSettings && (
<Tabs.Trigger value={Page.Settings} asChild>
<NextLink
href={{
pathname: '/[organizationId]/[projectId]/[targetId]/settings',
query: {
organizationId: currentOrganization.cleanId,
projectId: currentProject.cleanId,
targetId: currentTarget.cleanId,
},
<Link
to="/$organizationId/$projectId/$targetId/settings"
params={{
organizationId: currentOrganization.cleanId,
projectId: currentProject.cleanId,
targetId: currentTarget.cleanId,
}}
>
Settings
</NextLink>
</Link>
</Tabs.Trigger>
)}
</Tabs.List>
@ -348,10 +339,16 @@ export const TargetLayout = ({
) : isCDNEnabled?.isCDNEnabled ? (
<>
<Button onClick={toggleModalOpen} variant="link" className="text-orange-500">
<Link size={16} className="mr-2" />
<LinkIcon size={16} className="mr-2" />
Connect to CDN
</Button>
<ConnectSchemaModal isOpen={isModalOpen} toggleModalOpen={toggleModalOpen} />
<ConnectSchemaModal
organizationId={props.organizationId}
projectId={props.projectId}
targetId={props.targetId}
isOpen={isModalOpen}
toggleModalOpen={toggleModalOpen}
/>
</>
) : null
) : null}

View file

@ -0,0 +1,49 @@
import ghost from '../../public/images/figures/ghost.svg?url';
import { Helmet } from 'react-helmet-async';
import { Button } from '@/components/ui/button';
import { captureMessage } from '@sentry/react';
import { useRouter } from '@tanstack/react-router';
export function NotFound() {
const router = useRouter();
captureMessage('404 Not Found', {
level: 'warning',
extra: {
href1: router.history.location.href,
href2: window.location.href,
href3: router.latestLocation.href,
},
});
return (
<>
<div className="flex h-screen flex-col items-center justify-center gap-2.5">
<img src={ghost} alt="Ghost illustration" width="200" height="200" className="drag-none" />
<h2 className="text-5xl font-bold">404</h2>
<h3 className="text-xl font-semibold">Page Not Found</h3>
<Button variant="secondary" onClick={router.history.back}>
Go back
</Button>
<Helmet>
<style key="not-found-styles">
{`
html {
background:
url(/images/bg-top-shine.svg) no-repeat left top,
url(/images/bg-bottom-shine.svg) no-repeat right bottom,
#0b0d11;
}
body {
background: transparent !important;
color: #fcfcfc !important;
}
`}
</style>
</Helmet>
</div>
</>
);
}

View file

@ -44,7 +44,7 @@ export function OrganizationUsageEstimationView(props: {
return (
<div className="right-4 top-7">
<DataWrapper query={query}>
<DataWrapper query={query} organizationId={organization.cleanId}>
{result => (
<Table>
<THead>

View file

@ -1,5 +1,4 @@
import { ReactElement } from 'react';
import { useRouter } from 'next/router';
import clsx from 'clsx';
import { useMutation } from 'urql';
import { Section } from '@/components/common';
@ -8,6 +7,7 @@ import { FragmentType, graphql, useFragment } from '@/gql';
import { BillingPlanType } from '@/gql/graphql';
import { ExternalLinkIcon } from '@radix-ui/react-icons';
import { CardElement } from '@stripe/react-stripe-js';
import { useRouter } from '@tanstack/react-router';
const GenerateStripeLinkMutation = graphql(`
mutation GenerateStripeLinkMutation($selector: OrganizationSelectorInput!) {
@ -67,7 +67,10 @@ export const ManagePaymentMethod = (props: {
},
}).then(result => {
if (result.data?.generateStripePortalLink) {
void router.push(result.data.generateStripePortalLink);
void router.navigate({
to: result.data.generateStripePortalLink,
});
// void router.push(result.data.generateStripePortalLink);
}
});
}}
@ -115,7 +118,7 @@ export const BillingPaymentMethodForm = ({
/>
<Section.Subtitle>
All payments and subscriptions are processed securely by{' '}
<Link variant="primary" href="https://stripe.com" target="_blank" rel="noreferrer">
<Link as="a" variant="primary" href="https://stripe.com" target="_blank" rel="noreferrer">
Stripe
</Link>
</Section.Subtitle>

View file

@ -45,7 +45,7 @@ const planCollection: {
'Change your plan at any time',
'Improved pricing as you scale',
'12 months of usage data retention',
<span className="gap-1">
<span key="enterprise" className="gap-1">
GraphQL/APIs support and guidance
<br />
from{' '}

View file

@ -1,5 +1,6 @@
import { ReactElement } from 'react';
import { Link, Table, TBody, Td, Th, THead, Tr } from '@/components/v2';
import { Button } from '@/components/ui/button';
import { Table, TBody, Td, Th, THead, Tr } from '@/components/v2';
import { FragmentType, graphql, useFragment } from '@/gql';
import { CurrencyFormatter, DateFormatter } from './helpers';
@ -51,9 +52,11 @@ export function InvoicesList(props: {
<Td>{DateFormatter.format(new Date(invoice.periodEnd))}</Td>
<Td>
{invoice.pdfLink && (
<Link variant="primary" href={invoice.pdfLink} target="_blank" rel="noreferrer">
Download
</Link>
<Button variant="orangeLink" asChild>
<a href={invoice.pdfLink} target="_blank" rel="noreferrer">
Download
</a>
</Button>
)}
</Td>
</Tr>

View file

@ -1,6 +1,4 @@
import { useCallback, useRef, useState } from 'react';
import Link from 'next/link';
import { useRouter } from 'next/router';
import { differenceInCalendarDays } from 'date-fns';
import { InfoIcon, LightbulbIcon, PartyPopperIcon } from 'lucide-react';
import { useForm } from 'react-hook-form';
@ -42,6 +40,7 @@ import { FragmentType, graphql, useFragment } from '@/gql';
import { OrganizationAccessScope, ProjectAccessScope, TargetAccessScope } from '@/gql/graphql';
import { Scope, scopes } from '@/lib/access/common';
import { zodResolver } from '@hookform/resolvers/zod';
import { Link, useRouter } from '@tanstack/react-router';
import { PermissionsSpace } from '../Permissions';
import { RoleSelector } from './common';
import { authProviderToIconAndTextMap } from './list';
@ -79,7 +78,11 @@ export function MemberRoleMigrationStickyNote(props: {
: 0;
}
const isMembersView = router.route.startsWith('/[organizationId]/view/members');
const isMembersView =
// @ts-expect-error - it's missing the `search` property, but it's fine, it works correctly
router.matchRoute({
to: '/$organizationId/view/members',
}) !== false;
if (
// Only admins can perform migration
@ -103,12 +106,12 @@ export function MemberRoleMigrationStickyNote(props: {
{daysLeft.current} {daysLeft.current > 1 ? 'days' : 'day'} left to{' '}
<Link
className="underline underline-offset-4"
href={{
pathname: '/[organizationId]/view/members',
query: {
organizationId: organization.cleanId,
page: 'migration',
},
to="/$organizationId/view/members"
params={{
organizationId: organization.cleanId,
}}
search={{
page: 'migration',
}}
>
assign roles
@ -124,10 +127,6 @@ function SimilarRoleScopes<T>(props: {
definitions: readonly Scope<T>[];
scopes: readonly T[];
}) {
if (props.scopes.length === 0) {
return null;
}
const groupedScopes = useRef<
{
name: string;
@ -138,6 +137,10 @@ function SimilarRoleScopes<T>(props: {
}[]
>();
if (props.scopes.length === 0) {
return null;
}
if (!groupedScopes.current) {
groupedScopes.current = [];
for (const def of props.definitions) {

View file

@ -1,7 +1,4 @@
import { ReactElement, useEffect, useRef } from 'react';
import Link from 'next/link';
import { useRouter } from 'next/router';
import { clsx } from 'clsx';
import { format } from 'date-fns';
import { useFormik } from 'formik';
import { Virtuoso, VirtuosoHandle } from 'react-virtuoso';
@ -15,16 +12,18 @@ import {
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { Heading, Input, Button as LegacyButton, Modal, Tag } from '@/components/v2';
import { Input, Tag } from '@/components/v2';
import { AlertTriangleIcon, KeyIcon } from '@/components/v2/icon';
import { InlineCode } from '@/components/v2/inline-code';
import { env } from '@/env/frontend';
import { DocumentType, FragmentType, graphql, useFragment } from '@/gql';
import { useResetState } from '@/lib/hooks/use-reset-state';
import { cn } from '@/lib/utils';
import { Link, useRouter } from '@tanstack/react-router';
const classes = {
container: clsx('flex flex-col items-stretch gap-2'),
modal: clsx('w-[550px]'),
container: cn('flex flex-col items-stretch gap-2'),
modal: cn('w-[550px]'),
};
const OIDCIntegrationSection_OrganizationFragment = graphql(`
@ -49,74 +48,52 @@ export function OIDCIntegrationSection(props: {
const router = useRouter();
const organization = useFragment(OIDCIntegrationSection_OrganizationFragment, props.organization);
const isCreateOIDCIntegrationModalOpen = router.asPath.endsWith('#create-oidc-integration');
const isUpdateOIDCIntegrationModalOpen = router.asPath.endsWith('#manage-oidc-integration');
const isDeleteOIDCIntegrationModalOpen = router.asPath.endsWith('#remove-oidc-integration');
const isDebugOIDCIntegrationModalOpen = router.asPath.endsWith('#debug-oidc-integration');
const hash = router.latestLocation.hash;
const openCreateModalHash = 'create-oidc-integration';
const openEditModalHash = 'manage-oidc-integration';
const openDeleteModalHash = 'remove-oidc-integration';
const openDebugModalHash = 'debug-oidc-integration';
const isCreateOIDCIntegrationModalOpen = hash.endsWith(openCreateModalHash);
const isUpdateOIDCIntegrationModalOpen = hash.endsWith(openEditModalHash);
const isDeleteOIDCIntegrationModalOpen = hash.endsWith(openDeleteModalHash);
const isDebugOIDCIntegrationModalOpen = hash.endsWith(openDebugModalHash);
const closeModal = () => {
void router.push(router.asPath.split('#')[0], undefined, {
shallow: true,
scroll: false,
void router.navigate({
hash: undefined,
});
};
const openCreateModalLink = `${router.asPath}#create-oidc-integration`;
const openEditModalLink = `${router.asPath}#manage-oidc-integration`;
const openDeleteModalLink = `${router.asPath}#remove-oidc-integration`;
const openDebugModalLink = `${router.asPath}#debug-oidc-integration`;
return (
<>
<div className="flex items-center gap-x-2">
{organization.oidcIntegration ? (
<>
<Link
className={buttonVariants({ variant: 'default' })}
href={openEditModalLink}
onClick={ev => {
ev.preventDefault();
void router.push(openEditModalLink);
}}
>
<Link className={buttonVariants({ variant: 'default' })} hash={openEditModalHash}>
<KeyIcon className="mr-2 size-4" />
Manage OIDC Provider (
{extractDomain(organization.oidcIntegration.authorizationEndpoint)})
</Link>
<Link
className={clsx(buttonVariants({ variant: 'default' }), 'px-5')}
href={openDebugModalLink}
onClick={ev => {
ev.preventDefault();
void router.push(openDebugModalLink);
}}
className={cn(buttonVariants({ variant: 'default' }), 'px-5')}
hash={openDebugModalHash}
>
Show Debug Logs
</Link>
<Link
className={clsx(buttonVariants({ variant: 'destructive' }), 'px-5')}
href={openDeleteModalLink}
onClick={ev => {
ev.preventDefault();
void router.push(openDeleteModalLink);
}}
className={cn(buttonVariants({ variant: 'destructive' }), 'px-5')}
hash={openDeleteModalHash}
>
Remove
</Link>
</>
) : (
<LegacyButton
size="large"
as="a"
href={openCreateModalLink}
onClick={ev => {
ev.preventDefault();
void router.push(openCreateModalLink);
}}
>
<KeyIcon className="mr-2" />
Connect Open ID Connect Provider
</LegacyButton>
<Button asChild>
<Link hash={openCreateModalHash}>
<KeyIcon className="mr-2" />
Connect Open ID Connect Provider
</Link>
</Button>
)}
</div>
<CreateOIDCIntegrationModal
@ -124,9 +101,12 @@ export function OIDCIntegrationSection(props: {
close={closeModal}
hasOIDCIntegration={!!organization.oidcIntegration}
organizationId={organization.id}
openEditModalLink={openEditModalLink}
openEditModalHash={openEditModalHash}
transitionToManageScreen={() => {
void router.replace(openEditModalLink);
// TODO(router)
void router.navigate({
hash: 'manage-oidc-integration',
});
}}
/>
<ManageOIDCIntegrationModal
@ -134,7 +114,7 @@ export function OIDCIntegrationSection(props: {
oidcIntegration={organization.oidcIntegration ?? null}
isOpen={isUpdateOIDCIntegrationModalOpen}
close={closeModal}
openCreateModalLink={openCreateModalLink}
openCreateModalHash={openCreateModalHash}
/>
<RemoveOIDCIntegrationModal
isOpen={isDeleteOIDCIntegrationModalOpen}
@ -181,42 +161,40 @@ function CreateOIDCIntegrationModal(props: {
close: () => void;
hasOIDCIntegration: boolean;
organizationId: string;
openEditModalLink: string;
openEditModalHash: string;
transitionToManageScreen: () => void;
}): ReactElement {
return (
<Modal open={props.isOpen} onOpenChange={props.close} className={classes.modal}>
{props.hasOIDCIntegration ? (
<div className={classes.container}>
<Heading>Connect OpenID Connect Provider</Heading>
<p>
You are trying to create an OpenID Connect integration for an organization that already
has a provider attached. Please instead configure the existing provider.
</p>
<div className="flex w-full gap-2">
<LegacyButton type="button" size="large" block onClick={props.close}>
Close
</LegacyButton>
<LegacyButton
type="submit"
size="large"
block
variant="primary"
href={props.openEditModalLink}
>
Edit OIDC Integration
</LegacyButton>
</div>
</div>
) : (
<CreateOIDCIntegrationForm
organizationId={props.organizationId}
close={props.close}
key={props.organizationId}
transitionToManageScreen={props.transitionToManageScreen}
/>
)}
</Modal>
<Dialog open={props.isOpen} onOpenChange={props.close}>
<DialogContent className={classes.modal}>
{props.hasOIDCIntegration ? (
<>
<DialogHeader>
<DialogTitle>Connect OpenID Connect Provider</DialogTitle>
<DialogDescription>
You are trying to create an OpenID Connect integration for an organization that
already has a provider attached. Please configure the existing provider instead.
</DialogDescription>
</DialogHeader>
<DialogFooter className="space-x-2 text-right">
<Button variant="outline" onClick={props.close}>
Close
</Button>
<Button asChild>
<Link hash={props.openEditModalHash}>Edit OIDC Integration</Link>
</Button>
</DialogFooter>
</>
) : (
<CreateOIDCIntegrationForm
organizationId={props.organizationId}
close={props.close}
key={props.organizationId}
transitionToManageScreen={props.transitionToManageScreen}
/>
)}
</DialogContent>
</Dialog>
);
}
@ -261,94 +239,102 @@ function CreateOIDCIntegrationForm(props: {
return (
<form className={classes.container} onSubmit={formik.handleSubmit}>
<Heading>Connect OpenID Connect Provider</Heading>
<p>
Connecting an OIDC provider to this organization allows users to automatically log in and be
part of this organization.
</p>
<p>
Use Okta, Auth0, Google Workspaces or any other OAuth2 Open ID Connect compatible provider.
</p>
<DialogHeader>
<DialogTitle>Connect OpenID Connect Provider</DialogTitle>
<DialogDescription>
Connecting an OIDC provider to this organization allows users to automatically log in and
be part of this organization.
</DialogDescription>
<DialogDescription>
Use Okta, Auth0, Google Workspaces or any other OAuth2 Open ID Connect compatible
provider.
</DialogDescription>
</DialogHeader>
<div className="space-y-2 pt-4">
<div>
<label className="text-sm font-semibold" htmlFor="tokenEndpoint">
Token Endpoint
</label>
<label className="text-sm font-semibold" htmlFor="tokenEndpoint">
Token Endpoint
</label>
<Input
placeholder="OAuth Token Endpoint API"
id="tokenEndpoint"
name="tokenEndpoint"
onChange={formik.handleChange}
value={formik.values.tokenEndpoint}
isInvalid={!!mutation.data?.createOIDCIntegration.error?.details.tokenEndpoint}
/>
<div>{mutation.data?.createOIDCIntegration.error?.details.tokenEndpoint}</div>
</div>
<Input
placeholder="OAuth Token Endpoint API"
id="tokenEndpoint"
name="tokenEndpoint"
onChange={formik.handleChange}
value={formik.values.tokenEndpoint}
isInvalid={!!mutation.data?.createOIDCIntegration.error?.details.tokenEndpoint}
/>
<div>{mutation.data?.createOIDCIntegration.error?.details.tokenEndpoint}</div>
<div>
<label className="text-sm font-semibold" htmlFor="userinfoEndpoint">
User Info Endpoint
</label>
<Input
placeholder="OAuth User Info Endpoint API"
id="userinfoEndpoint"
name="userinfoEndpoint"
onChange={formik.handleChange}
value={formik.values.userinfoEndpoint}
isInvalid={!!mutation.data?.createOIDCIntegration.error?.details.userinfoEndpoint}
/>
<div>{mutation.data?.createOIDCIntegration.error?.details.userinfoEndpoint}</div>
</div>
<label className="text-sm font-semibold" htmlFor="userinfoEndpoint">
User Info Endpoint
</label>
<Input
placeholder="OAuth User Info Endpoint API"
id="userinfoEndpoint"
name="userinfoEndpoint"
onChange={formik.handleChange}
value={formik.values.userinfoEndpoint}
isInvalid={!!mutation.data?.createOIDCIntegration.error?.details.userinfoEndpoint}
/>
<div>{mutation.data?.createOIDCIntegration.error?.details.userinfoEndpoint}</div>
<div>
<label className="text-sm font-semibold" htmlFor="authorizationEndpoint">
Authorization Endpoint
</label>
<Input
placeholder="OAuth Authorization Endpoint API"
id="authorizationEndpoint"
name="authorizationEndpoint"
onChange={formik.handleChange}
value={formik.values.authorizationEndpoint}
isInvalid={!!mutation.data?.createOIDCIntegration.error?.details.authorizationEndpoint}
/>
<div>{mutation.data?.createOIDCIntegration.error?.details.authorizationEndpoint}</div>
</div>
<label className="text-sm font-semibold" htmlFor="authorizationEndpoint">
Authorization Endpoint
</label>
<Input
placeholder="OAuth Authorization Endpoint API"
id="authorizationEndpoint"
name="authorizationEndpoint"
onChange={formik.handleChange}
value={formik.values.authorizationEndpoint}
isInvalid={!!mutation.data?.createOIDCIntegration.error?.details.authorizationEndpoint}
/>
<div>{mutation.data?.createOIDCIntegration.error?.details.authorizationEndpoint}</div>
<div>
<label className="text-sm font-semibold" htmlFor="clientId">
Client ID
</label>
<Input
placeholder="Client ID"
id="clientId"
name="clientId"
onChange={formik.handleChange}
value={formik.values.clientId}
isInvalid={!!mutation.data?.createOIDCIntegration.error?.details.clientId}
/>
<div>{mutation.data?.createOIDCIntegration.error?.details.clientId}</div>
</div>
<label className="text-sm font-semibold" htmlFor="clientId">
Client ID
</label>
<Input
placeholder="Client ID"
id="clientId"
name="clientId"
onChange={formik.handleChange}
value={formik.values.clientId}
isInvalid={!!mutation.data?.createOIDCIntegration.error?.details.clientId}
/>
<div>{mutation.data?.createOIDCIntegration.error?.details.clientId}</div>
<div>
<label className="text-sm font-semibold" htmlFor="clientSecret">
Client Secret
</label>
<Input
placeholder="Client Secret"
id="clientSecret"
name="clientSecret"
onChange={formik.handleChange}
value={formik.values.clientSecret}
isInvalid={!!mutation.data?.createOIDCIntegration.error?.details.clientSecret}
/>
<div>{mutation.data?.createOIDCIntegration.error?.details.clientSecret}</div>
</div>
<label className="text-sm font-semibold" htmlFor="clientSecret">
Client Secret
</label>
<Input
placeholder="Client Secret"
id="clientSecret"
name="clientSecret"
onChange={formik.handleChange}
value={formik.values.clientSecret}
isInvalid={!!mutation.data?.createOIDCIntegration.error?.details.clientSecret}
/>
<div>{mutation.data?.createOIDCIntegration.error?.details.clientSecret}</div>
<div className="flex w-full gap-2">
<LegacyButton type="button" size="large" block onClick={props.close}>
Cancel
</LegacyButton>
<LegacyButton
type="submit"
size="large"
block
variant="primary"
disabled={mutation.fetching}
>
Connect OIDC Provider
</LegacyButton>
<div className="flex w-full justify-end gap-x-2">
<Button variant="outline" disabled={mutation.fetching} onClick={props.close}>
Cancel
</Button>
<Button type="submit" disabled={mutation.fetching}>
Connect OIDC Provider
</Button>
</div>
</div>
</form>
);
@ -358,7 +344,7 @@ function ManageOIDCIntegrationModal(props: {
isOpen: boolean;
close: () => void;
oidcIntegration: FragmentType<typeof UpdateOIDCIntegration_OIDCIntegrationFragment> | null;
openCreateModalLink: string;
openCreateModalHash: string;
}): ReactElement {
const oidcIntegration = useFragment(
UpdateOIDCIntegration_OIDCIntegrationFragment,
@ -366,29 +352,25 @@ function ManageOIDCIntegrationModal(props: {
);
return oidcIntegration == null ? (
<Modal open={props.isOpen} onOpenChange={props.close} className={classes.modal}>
<div className={classes.container}>
<Heading>Manage OpenID Connect Integration</Heading>
<p>
You are trying to update an OpenID Connect integration for an organization that has no
integration.
</p>
<div className="flex w-full gap-2">
<LegacyButton type="button" size="large" block onClick={props.close}>
<Dialog open={props.isOpen} onOpenChange={props.close}>
<DialogContent className={classes.modal}>
<DialogHeader>
<DialogTitle>Manage OpenID Connect Integration</DialogTitle>
<DialogDescription>
You are trying to update an OpenID Connect integration for an organization that has no
integration.
</DialogDescription>
</DialogHeader>
<DialogFooter className="space-x-2 text-right">
<Button variant="outline" onClick={props.close}>
Close
</LegacyButton>
<LegacyButton
type="submit"
size="large"
block
variant="primary"
href={props.openCreateModalLink}
>
Connect OIDC Provider
</LegacyButton>
</div>
</div>
</Modal>
</Button>
<Button asChild>
<Link hash={props.openCreateModalHash}>Connect OIDC Provider</Link>
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
) : (
<UpdateOIDCIntegrationForm
close={props.close}
@ -479,122 +461,121 @@ function UpdateOIDCIntegrationForm(props: {
});
return (
<Modal open={props.isOpen} onOpenChange={props.close} className="flex min-h-[600px] w-[960px]">
<form className={clsx(classes.container, 'flex-1 gap-12')} onSubmit={formik.handleSubmit}>
<Heading>Manage OpenID Connect Integration</Heading>
<div className="flex">
<div className={clsx(classes.container, 'flex flex-1 flex-col pr-5')}>
<Heading size="lg">OIDC Provider Instructions</Heading>
<ul className="flex flex-col gap-5">
<li>
<div className="pb-1"> Set your OIDC Provider Sign-in redirect URI to </div>
<InlineCode content={`${env.appBaseUrl}/auth/callback/oidc`} />
</li>
<li>
<div className="pb-1"> Set your OIDC Provider Sign-out redirect URI to </div>
<InlineCode content={`${env.appBaseUrl}/logout`} />
</li>
<li>
<div className="pb-1">Your users can login to the organization via </div>
<InlineCode
content={`${env.appBaseUrl}/auth/oidc?id=${props.oidcIntegration.id}`}
/>
</li>
</ul>
<Dialog open={props.isOpen} onOpenChange={props.close}>
<DialogContent className="flex min-h-[600px] w-[960px] max-w-none">
<form className={cn(classes.container, 'flex-1 gap-12')} onSubmit={formik.handleSubmit}>
<DialogHeader>
<DialogTitle>Manage OpenID Connect Integration</DialogTitle>
</DialogHeader>
<div className="flex">
<div className={cn(classes.container, 'flex flex-1 flex-col pr-5')}>
<DialogTitle>OIDC Provider Instructions</DialogTitle>
<ul className="flex flex-col gap-5">
<li>
<div className="pb-1"> Set your OIDC Provider Sign-in redirect URI to </div>
<InlineCode content={`${env.appBaseUrl}/auth/callback/oidc`} />
</li>
<li>
<div className="pb-1"> Set your OIDC Provider Sign-out redirect URI to </div>
<InlineCode content={`${env.appBaseUrl}/logout`} />
</li>
<li>
<div className="pb-1">Your users can login to the organization via </div>
<InlineCode
content={`${env.appBaseUrl}/auth/oidc?id=${props.oidcIntegration.id}`}
/>
</li>
</ul>
</div>
<div className={cn(classes.container, 'flex-1 pl-5')}>
<DialogTitle>Properties</DialogTitle>
<label className="text-sm font-semibold" htmlFor="tokenEndpoint">
Token Endpoint
</label>
<Input
placeholder="OAuth Token Endpoint API"
id="tokenEndpoint"
name="tokenEndpoint"
onChange={formik.handleChange}
value={formik.values.tokenEndpoint}
isInvalid={!!mutation.data?.updateOIDCIntegration.error?.details.tokenEndpoint}
/>
<div>{mutation.data?.updateOIDCIntegration.error?.details.tokenEndpoint}</div>
<label className="text-sm font-semibold" htmlFor="userinfoEndpoint">
User Info Endpoint
</label>
<Input
placeholder="OAuth User Info Endpoint API"
id="userinfoEndpoint"
name="userinfoEndpoint"
onChange={formik.handleChange}
value={formik.values.userinfoEndpoint}
isInvalid={!!mutation.data?.updateOIDCIntegration.error?.details.userinfoEndpoint}
/>
<div>{mutation.data?.updateOIDCIntegration.error?.details.userinfoEndpoint}</div>
<label className="text-sm font-semibold" htmlFor="authorizationEndpoint">
Authorization Endpoint
</label>
<Input
placeholder="OAuth Authorization Endpoint API"
id="authorizationEndpoint"
name="authorizationEndpoint"
onChange={formik.handleChange}
value={formik.values.authorizationEndpoint}
isInvalid={
!!mutation.data?.updateOIDCIntegration.error?.details.authorizationEndpoint
}
/>
<div>{mutation.data?.updateOIDCIntegration.error?.details.authorizationEndpoint}</div>
<label className="text-sm font-semibold" htmlFor="clientId">
Client ID
</label>
<Input
placeholder="Client ID"
id="clientId"
name="clientId"
onChange={formik.handleChange}
value={formik.values.clientId}
isInvalid={!!mutation.data?.updateOIDCIntegration.error?.details.clientId}
/>
<div>{mutation.data?.updateOIDCIntegration.error?.details.clientId}</div>
<label className="text-sm font-semibold" htmlFor="clientSecret">
Client Secret
</label>
<Input
placeholder={
'Keep old value. (Ending with ' +
props.oidcIntegration.clientSecretPreview.substring(
props.oidcIntegration.clientSecretPreview.length - 4,
) +
')'
}
id="clientSecret"
name="clientSecret"
onChange={formik.handleChange}
value={formik.values.clientSecret}
isInvalid={!!mutation.data?.updateOIDCIntegration.error?.details.clientSecret}
/>
<div>{mutation.data?.updateOIDCIntegration.error?.details.clientSecret}</div>
</div>
</div>
<div className={clsx(classes.container, 'flex-1 pl-5')}>
<Heading size="lg">Properties</Heading>
<label className="text-sm font-semibold" htmlFor="tokenEndpoint">
Token Endpoint
</label>
<Input
placeholder="OAuth Token Endpoint API"
id="tokenEndpoint"
name="tokenEndpoint"
onChange={formik.handleChange}
value={formik.values.tokenEndpoint}
isInvalid={!!mutation.data?.updateOIDCIntegration.error?.details.tokenEndpoint}
/>
<div>{mutation.data?.updateOIDCIntegration.error?.details.tokenEndpoint}</div>
<label className="text-sm font-semibold" htmlFor="userinfoEndpoint">
User Info Endpoint
</label>
<Input
placeholder="OAuth User Info Endpoint API"
id="userinfoEndpoint"
name="userinfoEndpoint"
onChange={formik.handleChange}
value={formik.values.userinfoEndpoint}
isInvalid={!!mutation.data?.updateOIDCIntegration.error?.details.userinfoEndpoint}
/>
<div>{mutation.data?.updateOIDCIntegration.error?.details.userinfoEndpoint}</div>
<label className="text-sm font-semibold" htmlFor="authorizationEndpoint">
Authorization Endpoint
</label>
<Input
placeholder="OAuth Authorization Endpoint API"
id="authorizationEndpoint"
name="authorizationEndpoint"
onChange={formik.handleChange}
value={formik.values.authorizationEndpoint}
isInvalid={
!!mutation.data?.updateOIDCIntegration.error?.details.authorizationEndpoint
}
/>
<div>{mutation.data?.updateOIDCIntegration.error?.details.authorizationEndpoint}</div>
<label className="text-sm font-semibold" htmlFor="clientId">
Client ID
</label>
<Input
placeholder="Client ID"
id="clientId"
name="clientId"
onChange={formik.handleChange}
value={formik.values.clientId}
isInvalid={!!mutation.data?.updateOIDCIntegration.error?.details.clientId}
/>
<div>{mutation.data?.updateOIDCIntegration.error?.details.clientId}</div>
<label className="text-sm font-semibold" htmlFor="clientSecret">
Client Secret
</label>
<Input
placeholder={
'Keep old value. (Ending with ' +
props.oidcIntegration.clientSecretPreview.substring(
props.oidcIntegration.clientSecretPreview.length - 4,
) +
')'
}
id="clientSecret"
name="clientSecret"
onChange={formik.handleChange}
value={formik.values.clientSecret}
isInvalid={!!mutation.data?.updateOIDCIntegration.error?.details.clientSecret}
/>
<div>{mutation.data?.updateOIDCIntegration.error?.details.clientSecret}</div>
</div>
</div>
<div className="mt-4 flex w-full gap-2 self-end">
<LegacyButton type="button" size="large" block onClick={props.close} tabIndex={0}>
Close
</LegacyButton>
<LegacyButton
type="submit"
size="large"
block
variant="primary"
disabled={mutation.fetching}
>
Save
</LegacyButton>
</div>
</form>
</Modal>
<DialogFooter className="space-x-2 text-right">
<Button variant="outline" onClick={props.close} tabIndex={0}>
Close
</Button>
<Button type="submit" disabled={mutation.fetching}>
Save
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
);
}
@ -624,25 +605,23 @@ function RemoveOIDCIntegrationModal(props: {
const { oidcIntegrationId } = props;
return (
<Modal open={props.isOpen} onOpenChange={props.close} className={classes.modal}>
<div className={classes.container}>
<Heading>Remove OpenID Connect Integration</Heading>
<Dialog open={props.isOpen} onOpenChange={props.close}>
<DialogContent className={classes.modal}>
<DialogHeader>
<DialogTitle>Remove OpenID Connect Integration</DialogTitle>
</DialogHeader>
{mutation.data?.deleteOIDCIntegration.ok ? (
<>
<p>The OIDC integration has been removed successfully.</p>
<div className="flex w-full gap-2">
<LegacyButton type="button" size="large" block onClick={props.close}>
Close
</LegacyButton>
<div className="text-right">
<Button onClick={props.close}>Close</Button>
</div>
</>
) : oidcIntegrationId === null ? (
<>
<p>This organization does not have an OIDC integration.</p>
<div className="flex w-full gap-2">
<LegacyButton type="button" size="large" block onClick={props.close}>
Close
</LegacyButton>
<div className="text-right">
<Button onClick={props.close}>Close</Button>
</div>
</>
) : (
@ -656,26 +635,24 @@ function RemoveOIDCIntegrationModal(props: {
</Tag>
<p>Do you really want to proceed?</p>
<div className="flex w-full gap-2">
<LegacyButton type="button" size="large" block onClick={props.close}>
<div className="space-x-2 text-right">
<Button variant="outline" onClick={props.close}>
Close
</LegacyButton>
<LegacyButton
size="large"
block
danger
</Button>
<Button
variant="destructive"
disabled={mutation.fetching}
onClick={async () => {
await mutate({ input: { oidcIntegrationId } });
}}
>
Delete
</LegacyButton>
</Button>
</div>
</>
)}
</div>
</Modal>
</DialogContent>
</Dialog>
);
}

View file

@ -0,0 +1,14 @@
import { ReactNode } from 'react';
import { getIsStripeEnabled } from '@/lib/billing/stripe-public-key';
import { Navigate } from '@tanstack/react-router';
export function RenderIfStripeAvailable(props: { children: ReactNode; organizationId: string }) {
/**
* If Stripe is not enabled we redirect the user to the organization.
*/
if (!getIsStripeEnabled()) {
return <Navigate to="/$organizationId" params={{ organizationId: props.organizationId }} />;
}
return <>{props.children}</>;
}

View file

@ -163,7 +163,7 @@ export function PolicySettings({
const activePolicy = useFragment(PolicySettings_SchemaPolicyFragment, currentState);
return (
<DataWrapper query={availableRules}>
<DataWrapper query={availableRules} organizationId={null}>
{query => (
<PolicySettingsListForm
saving={saving}

View file

@ -5,7 +5,6 @@ import * as Yup from 'yup';
import { Button, Heading, Modal, Select } from '@/components/v2';
import { FragmentType, graphql, useFragment } from '@/gql';
import { AlertType } from '@/gql/graphql';
import { useRouteSelector } from '@/lib/hooks';
export const CreateAlertModal_AddAlertMutation = graphql(`
mutation CreateAlertModal_AddAlertMutation($input: AddAlertInput!) {
@ -45,12 +44,13 @@ export const CreateAlertModal = (props: {
toggleModalOpen: () => void;
targets: FragmentType<typeof CreateAlertModal_TargetFragment>[];
channels: FragmentType<typeof CreateAlertModal_AlertChannelFragment>[];
organizationId: string;
projectId: string;
}): ReactElement => {
const { isOpen, toggleModalOpen } = props;
const targets = useFragment(CreateAlertModal_TargetFragment, props.targets);
const channels = useFragment(CreateAlertModal_AlertChannelFragment, props.channels);
const [mutation, mutate] = useMutation(CreateAlertModal_AddAlertMutation);
const router = useRouteSelector();
const { handleSubmit, values, handleChange, errors, touched, isSubmitting } = useFormik({
initialValues: {
@ -76,8 +76,8 @@ export const CreateAlertModal = (props: {
async onSubmit(values) {
const { error, data } = await mutate({
input: {
organization: router.organizationId,
project: router.projectId,
organization: props.organizationId,
project: props.projectId,
target: values.target,
channel: values.channel,
type: values.type,

View file

@ -5,7 +5,6 @@ import * as Yup from 'yup';
import { Button, Heading, Input, Modal, Select, Tag } from '@/components/v2';
import { graphql } from '@/gql';
import { AlertChannelType } from '@/gql/graphql';
import { useRouteSelector } from '@/lib/hooks';
export const CreateChannel_AddAlertChannelMutation = graphql(`
mutation CreateChannel_AddAlertChannel($input: AddAlertChannelInput!) {
@ -33,11 +32,14 @@ export const CreateChannel_AddAlertChannelMutation = graphql(`
export const CreateChannelModal = ({
isOpen,
toggleModalOpen,
organizationId,
projectId,
}: {
isOpen: boolean;
toggleModalOpen: () => void;
organizationId: string;
projectId: string;
}): ReactElement => {
const router = useRouteSelector();
const [mutation, mutate] = useMutation(CreateChannel_AddAlertChannelMutation);
const { errors, values, touched, handleChange, handleBlur, handleSubmit, isSubmitting } =
useFormik({
@ -64,8 +66,8 @@ export const CreateChannelModal = ({
async onSubmit(values) {
const { data, error } = await mutate({
input: {
organization: router.organizationId,
project: router.projectId,
organization: organizationId,
project: projectId,
name: values.name,
type: values.type,
slack: values.type === AlertChannelType.Slack ? { channel: values.slackChannel } : null,

View file

@ -3,8 +3,9 @@ import { useMutation } from 'urql';
import { Button, Card, Heading, Tooltip } from '@/components/v2';
import { FragmentType, graphql, useFragment } from '@/gql';
import { RegistryModel } from '@/gql/graphql';
import { useNotifications, useRouteSelector } from '@/lib/hooks';
import { useNotifications } from '@/lib/hooks';
import { CheckIcon, Cross2Icon, QuestionMarkCircledIcon } from '@radix-ui/react-icons';
import { useRouter } from '@tanstack/react-router';
const divider = <div className="mt-4 border-b border-gray-900" />;
@ -138,7 +139,7 @@ export function ModelMigrationSettings(props: {
const isStitching = project.type === 'STITCHING';
const isComposite = isStitching || project.type === 'FEDERATION';
const notify = useNotifications();
const router = useRouteSelector();
const router = useRouter();
const [{ fetching }, upgradeMutation] = useMutation(
ModelMigrationSettings_upgradeProjectRegistryModelMutation,
@ -191,9 +192,9 @@ export function ModelMigrationSettings(props: {
variant="secondary"
size="large"
onClick={() => {
void router.push({
href: '/[organizationId]/view/support',
query: { organizationId: props.organizationId },
void router.navigate({
to: '/$organizationId/view/support',
params: { organizationId: props.organizationId },
});
}}
>
@ -329,9 +330,9 @@ export function ModelMigrationSettings(props: {
<Button
variant="link"
onClick={() => {
void router.push({
href: '/[organizationId]/view/support',
query: { organizationId: props.organizationId },
void router.navigate({
to: '/$organizationId/view/support',
params: { organizationId: props.organizationId },
});
}}
>

View file

@ -1,5 +1,4 @@
import { useCallback } from 'react';
import Link from 'next/link';
import { FlaskConicalIcon, HeartCrackIcon, PartyPopperIcon, RefreshCcwIcon } from 'lucide-react';
import { useMutation, useQuery } from 'urql';
import { Button } from '@/components/ui/button';
@ -195,12 +194,12 @@ export function NativeCompositionSettings(props: {
NativeFederationCompatibilityStatus.Compatible ? (
<>
Subgraphs of this project are composed and validated correctly by our{' '}
<Link
<a
className="text-muted-foreground font-semibold underline-offset-4 hover:underline"
href="https://github.com/the-guild-org/federation"
>
Open Source composition library
</Link>{' '}
</a>{' '}
for Apollo Federation.
</>
) : null}
@ -208,12 +207,12 @@ export function NativeCompositionSettings(props: {
NativeFederationCompatibilityStatus.Incompatible ? (
<>
Our{' '}
<Link
<a
className="text-muted-foreground font-semibold underline-offset-4 hover:underline"
href="https://github.com/the-guild-org/federation"
>
Open Source composition library
</Link>{' '}
</a>{' '}
is not yet compatible with subgraphs of your project. We're working on it!
<br />
Please reach out to us to explore solutions for addressing this issue.
@ -224,12 +223,12 @@ export function NativeCompositionSettings(props: {
<>
Your project appears to lack any subgraphs at the moment, making it impossible
for us to assess compatibility with our{' '}
<Link
<a
className="text-muted-foreground font-semibold underline-offset-4 hover:underline"
href="https://github.com/the-guild-org/federation"
>
Open Source composition library
</Link>
</a>
.
</>
) : null}
@ -259,9 +258,9 @@ export function NativeCompositionSettings(props: {
</Button>
<div>
<Button variant="link" className="text-orange-500" asChild>
<Link href="https://github.com/the-guild-org/federation?tab=readme-ov-file#compatibility">
<a href="https://github.com/the-guild-org/federation?tab=readme-ov-file#compatibility">
Learn more about risks and compatibility with Apollo Composition
</Link>
</a>
</Button>
</div>
</div>

View file

@ -1,4 +1,4 @@
import dynamic from 'next/dynamic';
import { lazy } from 'react';
import {
loader,
DiffEditor as MonacoDiffEditor,
@ -8,22 +8,19 @@ import pkg from '../../package.json' assert { type: 'json' };
loader.config({
paths: {
vs: `https://cdn.jsdelivr.net/npm/monaco-editor@${pkg.dependencies['monaco-editor']}/min/vs`,
vs: `https://cdn.jsdelivr.net/npm/monaco-editor@${pkg.devDependencies['monaco-editor']}/min/vs`,
},
});
export { MonacoDiffEditor };
export { MonacoEditor };
export const SchemaEditor = dynamic({
async loader() {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
await import('regenerator-runtime/runtime');
const { SchemaEditor } = await import('@theguild/editor');
return SchemaEditor;
},
ssr: false,
export const SchemaEditor = lazy(async () => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
await import('regenerator-runtime/runtime');
const { SchemaEditor } = await import('@theguild/editor');
return { default: SchemaEditor };
});
export type { SchemaEditorProps } from '@theguild/editor';

View file

@ -1,14 +1,14 @@
import React, { ReactElement, ReactNode, useMemo } from 'react';
import NextLink from 'next/link';
import { clsx } from 'clsx';
import { Popover, PopoverArrow, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
import { PulseIcon, UsersIcon } from '@/components/v2/icon';
import { Markdown } from '@/components/v2/markdown';
import { FragmentType, graphql, useFragment } from '@/gql';
import { formatNumber, toDecimal, useRouteSelector } from '@/lib/hooks';
import { formatNumber, toDecimal } from '@/lib/hooks';
import { cn } from '@/lib/utils';
import { ChatBubbleIcon } from '@radix-ui/react-icons';
import { Link as NextLink, useRouter } from '@tanstack/react-router';
import { useArgumentListToggle } from './provider';
import { SupergraphMetadataList } from './super-graph-metadata';
@ -118,16 +118,13 @@ export function SchemaExplorerUsageStats(props: {
<td className="px-2 pl-0 text-left">
<NextLink
className="text-orange-500 hover:text-orange-500 hover:underline hover:underline-offset-2"
href={{
pathname:
'/[organizationId]/[projectId]/[targetId]/insights/[operationName]/[operationHash]',
query: {
organizationId: props.organizationCleanId,
projectId: props.projectCleanId,
targetId: props.targetCleanId,
operationName: `${op.hash.substring(0, 4)}_${op.name}`,
operationHash: op.hash,
},
to="/$organizationId/$projectId/$targetId/insights/$operationName/$operationHash"
params={{
organizationId: props.organizationCleanId,
projectId: props.projectCleanId,
targetId: props.targetCleanId,
operationName: `${op.hash.substring(0, 4)}_${op.name}`,
operationHash: op.hash,
}}
>
{op.hash.substring(0, 4)}_{op.name}
@ -166,15 +163,12 @@ export function SchemaExplorerUsageStats(props: {
<li key={clientName} className="font-bold">
<NextLink
className="text-orange-500 hover:text-orange-500 hover:underline hover:underline-offset-2"
href={{
pathname:
'/[organizationId]/[projectId]/[targetId]/insights/client/[name]',
query: {
organizationId: props.organizationCleanId,
projectId: props.projectCleanId,
targetId: props.targetCleanId,
name: clientName,
},
to="/$organizationId/$projectId/$targetId/insights/client/$name"
params={{
organizationId: props.organizationCleanId,
projectId: props.projectCleanId,
targetId: props.targetCleanId,
name: clientName,
}}
>
{clientName}
@ -299,7 +293,12 @@ export function GraphQLTypeCard(props: {
<div className="flex flex-row items-center gap-2">
<div className="font-normal text-gray-500">{props.kind}</div>
<div className="font-semibold">
<GraphQLTypeAsLink type={props.name} />
<GraphQLTypeAsLink
organizationId={props.organizationCleanId}
projectId={props.projectCleanId}
targetId={props.targetCleanId}
type={props.name}
/>
</div>
{props.description ? <Description description={props.description} /> : null}
</div>
@ -309,7 +308,13 @@ export function GraphQLTypeCard(props: {
<div className="mr-2">implements</div>
<div className="flex flex-row gap-2">
{props.implements.map(t => (
<GraphQLTypeAsLink key={t} type={t} />
<GraphQLTypeAsLink
organizationId={props.organizationCleanId}
projectId={props.projectCleanId}
targetId={props.targetCleanId}
key={t}
type={t}
/>
))}
</div>
</div>
@ -324,7 +329,12 @@ export function GraphQLTypeCard(props: {
/>
) : null}
{supergraphMetadata ? (
<SupergraphMetadataList supergraphMetadata={supergraphMetadata} />
<SupergraphMetadataList
targetId={props.targetCleanId}
projectId={props.projectCleanId}
organizationId={props.organizationCleanId}
supergraphMetadata={supergraphMetadata}
/>
) : null}
</div>
<div>{props.children}</div>
@ -336,6 +346,9 @@ function GraphQLArguments(props: {
parentCoordinate: string;
args: FragmentType<typeof GraphQLArguments_ArgumentFragment>[];
styleDeprecated: boolean;
organizationCleanId: string;
projectCleanId: string;
targetCleanId: string;
}) {
const args = useFragment(GraphQLArguments_ArgumentFragment, props.args);
const [isCollapsedGlobally] = useArgumentListToggle();
@ -360,10 +373,22 @@ function GraphQLArguments(props: {
styleDeprecated={props.styleDeprecated}
deprecationReason={arg.deprecationReason}
>
<LinkToCoordinatePage coordinate={coordinate}>{arg.name}</LinkToCoordinatePage>
<LinkToCoordinatePage
organizationId={props.organizationCleanId}
projectId={props.projectCleanId}
targetId={props.targetCleanId}
coordinate={coordinate}
>
{arg.name}
</LinkToCoordinatePage>
</DeprecationNote>
{': '}
<GraphQLTypeAsLink type={arg.type} />
<GraphQLTypeAsLink
organizationId={props.organizationCleanId}
projectId={props.projectCleanId}
targetId={props.targetCleanId}
type={arg.type}
/>
{arg.description ? <Description description={arg.description} /> : null}
</div>
);
@ -386,10 +411,22 @@ function GraphQLArguments(props: {
styleDeprecated={props.styleDeprecated}
deprecationReason={arg.deprecationReason}
>
<LinkToCoordinatePage coordinate={coordinate}>{arg.name}</LinkToCoordinatePage>
<LinkToCoordinatePage
organizationId={props.organizationCleanId}
projectId={props.projectCleanId}
targetId={props.targetCleanId}
coordinate={coordinate}
>
{arg.name}
</LinkToCoordinatePage>
</DeprecationNote>
{': '}
<GraphQLTypeAsLink type={arg.type} />
<GraphQLTypeAsLink
organizationId={props.organizationCleanId}
projectId={props.projectCleanId}
targetId={props.targetCleanId}
type={arg.type}
/>
</span>
);
})}
@ -498,24 +535,44 @@ export function GraphQLFields(props: {
styleDeprecated={props.styleDeprecated}
deprecationReason={field.deprecationReason}
>
<LinkToCoordinatePage coordinate={coordinate} className="font-semibold">
<LinkToCoordinatePage
organizationId={props.organizationCleanId}
projectId={props.projectCleanId}
targetId={props.targetCleanId}
coordinate={coordinate}
className="font-semibold"
>
{field.name}
</LinkToCoordinatePage>
</DeprecationNote>
{field.args.length > 0 ? (
<GraphQLArguments
organizationCleanId={props.organizationCleanId}
projectCleanId={props.projectCleanId}
targetCleanId={props.targetCleanId}
styleDeprecated={props.styleDeprecated}
parentCoordinate={coordinate}
args={field.args}
/>
) : null}
<span className="mr-1">:</span>
<GraphQLTypeAsLink className="font-semibold text-gray-400" type={field.type} />
<GraphQLTypeAsLink
organizationId={props.organizationCleanId}
projectId={props.projectCleanId}
targetId={props.targetCleanId}
className="font-semibold text-gray-400"
type={field.type}
/>
</div>
<div className="flex flex-row items-center">
{field.supergraphMetadata ? (
<div className="ml-1">
<SupergraphMetadataList supergraphMetadata={field.supergraphMetadata} />
<SupergraphMetadataList
targetId={props.targetCleanId}
projectId={props.projectCleanId}
organizationId={props.organizationCleanId}
supergraphMetadata={field.supergraphMetadata}
/>
</div>
) : null}
{typeof totalRequests === 'number' ? (
@ -567,12 +624,24 @@ export function GraphQLInputFields(props: {
styleDeprecated={props.styleDeprecated}
deprecationReason={field.deprecationReason}
>
<LinkToCoordinatePage coordinate={coordinate} className="font-semibold text-white">
<LinkToCoordinatePage
organizationId={props.organizationCleanId}
projectId={props.projectCleanId}
targetId={props.targetCleanId}
coordinate={coordinate}
className="font-semibold text-white"
>
{field.name}
</LinkToCoordinatePage>
</DeprecationNote>
<span className="mr-1">:</span>
<GraphQLTypeAsLink className="font-semibold" type={field.type} />
<GraphQLTypeAsLink
organizationId={props.organizationCleanId}
projectId={props.projectCleanId}
targetId={props.targetCleanId}
className="font-semibold"
type={field.type}
/>
</div>
{typeof props.totalRequests === 'number' ? (
<SchemaExplorerUsageStats
@ -590,8 +659,14 @@ export function GraphQLInputFields(props: {
);
}
function GraphQLTypeAsLink(props: { type: string; className?: string }): ReactElement {
const router = useRouteSelector();
function GraphQLTypeAsLink(props: {
type: string;
className?: string;
organizationId: string;
projectId: string;
targetId: string;
}): ReactElement {
const router = useRouter();
const typename = props.type.replace(/[[\]!]+/g, '');
return (
@ -604,16 +679,14 @@ function GraphQLTypeAsLink(props: { type: string; className?: string }): ReactEl
<p>
<NextLink
className="text-sm font-normal hover:underline hover:underline-offset-2"
href={{
pathname: '/[organizationId]/[projectId]/[targetId]/explorer/[typename]',
query: {
organizationId: router.organizationId,
projectId: router.projectId,
targetId: router.targetId,
typename,
...(router.query.period ? { period: router.query.period } : {}),
},
to="/$organizationId/$projectId/$targetId/explorer/$typename"
params={{
organizationId: props.organizationId,
projectId: props.projectId,
targetId: props.targetId,
typename,
}}
search={router.latestLocation.search}
>
Visit in <span className="font-bold">Explorer</span>
</NextLink>
@ -622,17 +695,14 @@ function GraphQLTypeAsLink(props: { type: string; className?: string }): ReactEl
<p>
<NextLink
className="text-sm font-normal hover:underline hover:underline-offset-2"
href={{
pathname:
'/[organizationId]/[projectId]/[targetId]/insights/schema-coordinate/[typename]',
query: {
organizationId: router.organizationId,
projectId: router.projectId,
targetId: router.targetId,
typename,
...(router.query.period ? { period: router.query.period } : {}),
},
to="/$organizationId/$projectId/$targetId/insights/schema-coordinate/$coordinate"
params={{
organizationId: props.organizationId,
projectId: props.projectId,
targetId: props.targetId,
coordinate: typename,
}}
search={router.latestLocation.search}
>
Visit in <span className="font-bold">Insights</span>
</NextLink>
@ -650,26 +720,26 @@ export const LinkToCoordinatePage = React.forwardRef<
{
coordinate: string;
children: ReactNode;
organizationId: string;
projectId: string;
targetId: string;
className?: string;
}
>((props, ref) => {
const router = useRouteSelector();
const router = useRouter();
return (
<NextLink
ref={ref}
className={cn('hover:underline hover:underline-offset-2', props.className)}
href={{
pathname:
'/[organizationId]/[projectId]/[targetId]/insights/schema-coordinate/[coordinate]',
query: {
organizationId: router.organizationId,
projectId: router.projectId,
targetId: router.targetId,
coordinate: props.coordinate,
...(router.query.period ? { period: router.query.period } : {}),
},
to="/$organizationId/$projectId/$targetId/insights/schema-coordinate/$coordinate"
params={{
organizationId: props.organizationId,
projectId: props.projectId,
targetId: props.targetId,
coordinate: props.coordinate,
}}
search={router.latestLocation.search}
>
{props.children}
</NextLink>

View file

@ -60,13 +60,23 @@ export function GraphQLEnumTypeComponent(props: {
styleDeprecated={props.styleDeprecated}
deprecationReason={value.deprecationReason}
>
<LinkToCoordinatePage coordinate={`${ttype.name}.${value.name}`}>
<LinkToCoordinatePage
organizationId={props.organizationCleanId}
projectId={props.projectCleanId}
targetId={props.targetCleanId}
coordinate={`${ttype.name}.${value.name}`}
>
{value.name}
</LinkToCoordinatePage>
</DeprecationNote>
</div>
{value.supergraphMetadata ? (
<SupergraphMetadataList supergraphMetadata={value.supergraphMetadata} />
<SupergraphMetadataList
targetId={props.targetCleanId}
projectId={props.projectCleanId}
organizationId={props.organizationCleanId}
supergraphMetadata={value.supergraphMetadata}
/>
) : null}
{typeof props.totalRequests === 'number' ? (
<SchemaExplorerUsageStats

View file

@ -1,6 +1,4 @@
import { useMemo } from 'react';
import Link from 'next/link';
import { useRouter } from 'next/router';
import { useQuery } from 'urql';
import { DateRangePicker } from '@/components/ui/date-range-picker';
import { Input } from '@/components/ui/input';
@ -10,6 +8,13 @@ import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
import { Autocomplete } from '@/components/v2';
import { graphql } from '@/gql';
import {
Link,
RegisteredRouter,
RoutePaths,
ToPathOption,
useRouter,
} from '@tanstack/react-router';
import { useArgumentListToggle, usePeriodSelector } from './provider';
const TypeFilter_AllTypes = graphql(`
@ -93,9 +98,15 @@ export function TypeFilter(props: {
defaultValue={props.typename ? { value: props.typename, label: props.typename } : null}
options={types}
onChange={option => {
void router.push(
`/${props.organizationId}/${props.projectId}/${props.targetId}/explorer/${option.value}`,
);
void router.navigate({
to: '/$organizationId/$projectId/$targetId/explorer/$typename',
params: {
organizationId: props.organizationId,
projectId: props.projectId,
targetId: props.targetId,
typename: option.value,
},
});
}}
loading={query.fetching}
/>
@ -110,25 +121,18 @@ export function FieldByNameFilter() {
className="w-[200px] grow cursor-text"
placeholder="Filter by field name"
onChange={e => {
if (e.target.value === '') {
const routerQuery = router.query;
delete routerQuery.search;
void router.push({ query: routerQuery }, undefined, { shallow: true });
return;
}
void router.push(
{
query: {
...router.query,
search: e.target.value === '' ? undefined : e.target.value,
},
void router.navigate({
search: {
search: e.target.value === '' ? undefined : e.target.value,
},
undefined,
{ shallow: true },
);
});
}}
value={typeof router.query.search === 'string' ? router.query.search : ''}
value={
'search' in router.latestLocation.search &&
typeof router.latestLocation.search.search === 'string'
? router.latestLocation.search.search
: ''
}
/>
);
}
@ -154,7 +158,7 @@ export function ArgumentVisibilityFilter() {
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger>
<TooltipTrigger asChild>
<div className="bg-secondary flex h-[40px] flex-row items-center gap-x-4 rounded-md border px-3">
<div>
<Label htmlFor="filter-toggle-arguments" className="text-sm font-normal">
@ -177,23 +181,28 @@ export function ArgumentVisibilityFilter() {
);
}
const variants = [
const variants: Array<{
value: 'all' | 'unused' | 'deprecated';
label: string;
pathname: ToPathOption<RegisteredRouter, RoutePaths<RegisteredRouter['routeTree']>, ''>;
tooltip: string;
}> = [
{
value: 'all',
label: 'All',
pathname: '/[organizationId]/[projectId]/[targetId]/explorer',
pathname: '/$organizationId/$projectId/$targetId/explorer',
tooltip: 'Shows all types, including unused and deprecated ones',
},
{
value: 'unused',
label: 'Unused',
pathname: '/[organizationId]/[projectId]/[targetId]/explorer/unused',
pathname: '/$organizationId/$projectId/$targetId/explorer/unused',
tooltip: 'Shows only types that are not used in any operation',
},
{
value: 'deprecated',
label: 'Deprecated',
pathname: '/[organizationId]/[projectId]/[targetId]/explorer/deprecated',
pathname: '/$organizationId/$projectId/$targetId/explorer/deprecated',
tooltip: 'Shows only types that are marked as deprecated',
},
];
@ -210,19 +219,19 @@ export function SchemaVariantFilter(props: {
<TabsList>
{variants.map(variant => (
<Tooltip key={variant.value}>
<TooltipTrigger>
<TooltipTrigger asChild>
{props.variant === variant.value ? (
<TabsTrigger value={variant.value}>{variant.label}</TabsTrigger>
<div>
<TabsTrigger value={variant.value}>{variant.label}</TabsTrigger>
</div>
) : (
<TabsTrigger value={variant.value} asChild>
<Link
href={{
pathname: variant.pathname,
query: {
organizationId: props.organizationId,
projectId: props.projectId,
targetId: props.targetId,
},
to={variant.pathname}
params={{
organizationId: props.organizationId,
projectId: props.projectId,
targetId: props.targetId,
}}
>
{variant.label}

View file

@ -1,5 +1,5 @@
import { useRouter } from 'next/router';
import { FragmentType, graphql, useFragment } from '@/gql';
import { useRouter } from '@tanstack/react-router';
import { GraphQLFields, GraphQLTypeCard } from './common';
export const GraphQLObjectTypeComponent_TypeFragment = graphql(`
@ -32,6 +32,9 @@ export function GraphQLObjectTypeComponent(props: {
}) {
const ttype = useFragment(GraphQLObjectTypeComponent_TypeFragment, props.type);
const router = useRouter();
const searchObj = router.latestLocation.search;
const search =
'search' in searchObj && typeof searchObj.search === 'string' ? searchObj.search : undefined;
return (
<GraphQLTypeCard
@ -47,7 +50,7 @@ export function GraphQLObjectTypeComponent(props: {
<GraphQLFields
typeName={ttype.name}
fields={ttype.fields}
filterValue={typeof router.query.search === 'string' ? router.query.search : undefined}
filterValue={search}
totalRequests={props.totalRequests}
collapsed={props.collapsed}
targetCleanId={props.targetCleanId}

View file

@ -1,10 +1,9 @@
import { useMemo } from 'react';
import Link from 'next/link';
import { Tooltip } from '@/components/v2';
import { PackageIcon } from '@/components/v2/icon';
import { FragmentType, graphql, useFragment } from '@/gql';
import { useRouteSelector } from '@/lib/hooks';
import { TooltipProvider } from '@radix-ui/react-tooltip';
import { Link } from '@tanstack/react-router';
function stringToHslColor(str: string, s = 30, l = 80) {
let hash = 0;
@ -16,20 +15,23 @@ function stringToHslColor(str: string, s = 30, l = 80) {
return 'hsl(' + h + ', ' + s + '%, ' + l + '%)';
}
function SubgraphChip(props: { text: string; tooltip: boolean }): React.ReactElement {
const router = useRouteSelector();
function SubgraphChip(props: {
text: string;
tooltip: boolean;
organizationId: string;
projectId: string;
targetId: string;
}): React.ReactElement {
const inner = (
<Link
href={{
pathname: '/[organizationId]/[projectId]/[targetId]',
query: {
organizationId: router.organizationId,
projectId: router.projectId,
targetId: router.targetId,
},
hash: `service-${props.text}`,
to="/$organizationId/$projectId/$targetId"
params={{
organizationId: props.organizationId,
projectId: props.projectId,
targetId: props.targetId,
}}
// TODO(router)
hash={`service-${props.text}`}
style={{ backgroundColor: stringToHslColor(props.text) }}
className="my-[2px] ml-[6px] inline-block h-[22px] max-w-[100px] cursor-pointer items-center justify-between truncate rounded-[16px] py-0 pl-[8px] pr-[6px] text-[10px] font-normal normal-case leading-loose text-[#4f4f4f] drop-shadow-md"
>
@ -65,6 +67,9 @@ const tooltipColor = 'rgb(36, 39, 46)';
const previewThreshold = 3;
export function SupergraphMetadataList(props: {
organizationId: string;
projectId: string;
targetId: string;
supergraphMetadata: FragmentType<typeof SupergraphMetadataList_SupergraphMetadataFragment>;
}) {
const supergraphMetadata = useFragment(
@ -80,7 +85,14 @@ export function SupergraphMetadataList(props: {
if (supergraphMetadata.ownedByServiceNames.length <= previewThreshold) {
return [
supergraphMetadata.ownedByServiceNames.map((serviceName, index) => (
<SubgraphChip key={`${serviceName}-${index}`} text={serviceName} tooltip />
<SubgraphChip
organizationId={props.organizationId}
projectId={props.projectId}
targetId={props.targetId}
key={`${serviceName}-${index}`}
text={serviceName}
tooltip
/>
)),
null,
] as const;
@ -90,10 +102,24 @@ export function SupergraphMetadataList(props: {
supergraphMetadata.ownedByServiceNames
.slice(0, previewThreshold)
.map((serviceName, index) => (
<SubgraphChip key={`${serviceName}-${index}`} text={serviceName} tooltip />
<SubgraphChip
organizationId={props.organizationId}
projectId={props.projectId}
targetId={props.targetId}
key={`${serviceName}-${index}`}
text={serviceName}
tooltip
/>
)),
supergraphMetadata.ownedByServiceNames.map((serviceName, index) => (
<SubgraphChip key={`${serviceName}-${index}`} text={serviceName} tooltip={false} />
<SubgraphChip
organizationId={props.organizationId}
projectId={props.projectId}
targetId={props.targetId}
key={`${serviceName}-${index}`}
text={serviceName}
tooltip={false}
/>
)),
] as const;
}, [supergraphMetadata.ownedByServiceNames]);

View file

@ -57,7 +57,12 @@ export function GraphQLUnionTypeComponent(props: {
/>
) : null}
{member.supergraphMetadata ? (
<SupergraphMetadataList supergraphMetadata={member.supergraphMetadata} />
<SupergraphMetadataList
targetId={props.targetCleanId}
projectId={props.projectCleanId}
organizationId={props.organizationCleanId}
supergraphMetadata={member.supergraphMetadata}
/>
) : null}
</GraphQLTypeCardListItem>
))}

View file

@ -2,7 +2,6 @@ import { ReactElement, useCallback } from 'react';
import { useMutation } from 'urql';
import { Button, Tooltip } from '@/components/v2';
import { FragmentType, graphql, useFragment } from '@/gql';
import { useRouteSelector } from '@/lib/hooks';
const UpdateSchemaVersionStatusMutation = graphql(`
mutation updateSchemaVersionStatus($input: SchemaVersionUpdateInput!) {
@ -36,21 +35,23 @@ const MarkAsValid_SchemaVersionFragment = graphql(`
export function MarkAsValid(props: {
version: FragmentType<typeof MarkAsValid_SchemaVersionFragment>;
organizationId: string;
projectId: string;
targetId: string;
}): ReactElement | null {
const router = useRouteSelector();
const version = useFragment(MarkAsValid_SchemaVersionFragment, props.version);
const [mutation, mutate] = useMutation(UpdateSchemaVersionStatusMutation);
const markAsValid = useCallback(async () => {
await mutate({
input: {
organization: router.organizationId,
project: router.projectId,
target: router.targetId,
organization: props.organizationId,
project: props.projectId,
target: props.targetId,
version: version.id,
valid: true,
},
});
}, [mutate, version, router]);
}, [mutate, version]);
if (version?.valid) {
return null;

View file

@ -1,5 +1,4 @@
import { ReactElement } from 'react';
import Link from 'next/link';
import { clsx } from 'clsx';
import { format } from 'date-fns';
import { CheckIcon } from 'lucide-react';
@ -27,8 +26,8 @@ import { Heading } from '@/components/v2';
import { PulseIcon } from '@/components/v2/icon';
import { FragmentType, graphql, useFragment } from '@/gql';
import { CriticalityLevel } from '@/gql/graphql';
import { useRouteSelector } from '@/lib/hooks/use-route-selector';
import { CheckCircledIcon, InfoCircledIcon } from '@radix-ui/react-icons';
import { Link } from '@tanstack/react-router';
export function labelize(message: string) {
// Turn " into '
@ -112,6 +111,10 @@ export function ChangesBlock(
props: {
title: string | React.ReactElement;
criticality: CriticalityLevel;
organizationId: string;
projectId: string;
targetId: string;
schemaCheckId: string;
conditionBreakingChangeMetadata?: FragmentType<
typeof ChangesBlock_SchemaCheckConditionalBreakingChangeMetadataFragment
> | null;
@ -134,6 +137,10 @@ export function ChangesBlock(
<div className="list-inside list-disc space-y-2 text-sm leading-relaxed">
{changes.map((change, key) => (
<ChangeItem
organizationId={props.organizationId}
projectId={props.projectId}
targetId={props.targetId}
schemaCheckId={props.schemaCheckId}
key={key}
change={change}
conditionBreakingChangeMetadata={props.conditionBreakingChangeMetadata ?? null}
@ -162,8 +169,11 @@ function ChangeItem(props: {
conditionBreakingChangeMetadata: FragmentType<
typeof ChangesBlock_SchemaCheckConditionalBreakingChangeMetadataFragment
> | null;
organizationId: string;
projectId: string;
targetId: string;
schemaCheckId: string;
}) {
const router = useRouteSelector();
const change = isChangesBlock_SchemaChangeWithUsageFragment(props.change)
? useFragment(ChangesBlock_SchemaChangeWithUsageFragment, props.change)
: useFragment(ChangesBlock_SchemaChangeFragment, props.change);
@ -215,7 +225,15 @@ function ChangeItem(props: {
</div>
</AccordionTrigger>
<AccordionContent className="pb-8 pt-4">
{change.approval && <SchemaChangeApproval approval={change.approval} />}
{change.approval && (
<SchemaChangeApproval
organizationId={props.organizationId}
projectId={props.projectId}
targetId={props.targetId}
schemaCheckId={props.schemaCheckId}
approval={change.approval}
/>
)}
{'usageStatistics' in change && change.usageStatistics && metadata ? (
<div>
<div className="flex space-x-4">
@ -240,21 +258,18 @@ function ChangeItem(props: {
<PopoverContent side="right">
<div className="flex flex-col gap-y-2 text-sm">
View live usage on
{metadata.settings.targets.map(target =>
{metadata.settings.targets.map((target, i) =>
target.target ? (
<p>
<p key={i}>
<Link
className="text-orange-500 hover:text-orange-500"
href={{
pathname:
'/[organizationId]/[projectId]/[targetId]/insights/[operationName]/[operationHash]',
query: {
organizationId: router.organizationId,
projectId: router.projectId,
targetId: target.target.cleanId,
operationName: `${hash.substring(0, 4)}_${name}`,
operationHash: hash,
},
to="/$organizationId/$projectId/$targetId/insights/$operationName/$operationHash"
params={{
organizationId: props.organizationId,
projectId: props.projectId,
targetId: target.target.cleanId,
operationName: `${hash.substring(0, 4)}_${name}`,
operationHash: hash,
}}
target="_blank"
>
@ -315,15 +330,12 @@ function ChangeItem(props: {
<Link
key={index}
className="text-orange-500 hover:text-orange-500 "
href={{
pathname:
'/[organizationId]/[projectId]/[targetId]/insights/schema-coordinate/[coordinate]',
query: {
organizationId: router.organizationId,
projectId: router.projectId,
targetId: target.target.cleanId,
coordinate: change.path?.join('.'),
},
to="/$organizationId/$projectId/$targetId/insights/schema-coordinate/$coordinate"
params={{
organizationId: props.organizationId,
projectId: props.projectId,
targetId: target.target.cleanId,
coordinate: change.path!.join('.'),
}}
target="_blank"
>
@ -368,29 +380,32 @@ function ApprovedByBadge(props: {
function SchemaChangeApproval(props: {
approval: FragmentType<typeof ChangesBlock_SchemaChangeApprovalFragment>;
organizationId: string;
projectId: string;
targetId: string;
schemaCheckId: string;
}) {
const approval = useFragment(ChangesBlock_SchemaChangeApprovalFragment, props.approval);
const approvalName = approval.approvedBy?.displayName ?? '<unknown>';
const approvalDate = format(new Date(approval.approvedAt), 'do MMMM yyyy');
const route = useRouteSelector();
const schemaCheckPath =
'/' +
[route.organizationId, route.projectId, route.targetId, 'checks', approval.schemaCheckId].join(
[props.organizationId, props.projectId, props.targetId, 'checks', approval.schemaCheckId].join(
'/',
);
return (
<div className="mb-3">
This breaking change was manually{' '}
{approval.schemaCheckId === route.schemaCheckId ? (
{approval.schemaCheckId === props.schemaCheckId ? (
<>
{' '}
approved by {approvalName} in this schema check on {approvalDate}.
</>
) : (
<Link href={schemaCheckPath} className="text-orange-500 hover:underline">
<a href={schemaCheckPath} className="text-orange-500 hover:underline">
approved by {approvalName} on {approvalDate}.
</Link>
</a>
)}
</div>
);

View file

@ -9,7 +9,7 @@ import { Sheet, SheetContent, SheetHeader, SheetTitle } from '@/components/ui/sh
import { Checkbox, Input, Button as LegacyButton, Spinner } from '@/components/v2';
import { FragmentType, graphql, useFragment } from '@/gql';
import { DateRangeInput } from '@/gql/graphql';
import { useFormattedNumber, useRouteSelector, useToggle } from '@/lib/hooks';
import { useFormattedNumber, useToggle } from '@/lib/hooks';
const OperationsFilter_OperationStatsValuesConnectionFragment = graphql(`
fragment OperationsFilter_OperationStatsValuesConnectionFragment on OperationStatsValuesConnection {
@ -195,21 +195,26 @@ function OperationsFilterContainer({
onClose,
onFilter,
selected,
organizationId,
projectId,
targetId,
}: {
onFilter(keys: string[]): void;
onClose(): void;
isOpen: boolean;
period: DateRangeInput;
selected?: string[];
organizationId: string;
projectId: string;
targetId: string;
}): ReactElement | null {
const router = useRouteSelector();
const [query, refresh] = useQuery({
query: OperationsFilterContainer_OperationStatsQuery,
variables: {
selector: {
organization: router.organizationId,
project: router.projectId,
target: router.targetId,
organization: organizationId,
project: projectId,
target: targetId,
period,
operations: [],
},
@ -290,10 +295,16 @@ export function OperationsFilterTrigger({
period,
onFilter,
selected,
organizationId,
projectId,
targetId,
}: {
period: DateRangeInput;
onFilter(keys: string[]): void;
selected?: string[];
organizationId: string;
projectId: string;
targetId: string;
}): ReactElement {
const [isOpen, toggle] = useToggle();
@ -304,6 +315,9 @@ export function OperationsFilterTrigger({
<FilterIcon className="ml-2 size-4" />
</Button>
<OperationsFilterContainer
organizationId={organizationId}
projectId={projectId}
targetId={targetId}
isOpen={isOpen}
onClose={toggle}
period={period}
@ -530,21 +544,26 @@ function ClientsFilterContainer({
onClose,
onFilter,
selected,
organizationId,
projectId,
targetId,
}: {
onFilter(keys: string[]): void;
onClose(): void;
isOpen: boolean;
period: DateRangeInput;
selected?: string[];
organizationId: string;
projectId: string;
targetId: string;
}): ReactElement | null {
const router = useRouteSelector();
const [query, refresh] = useQuery({
query: ClientsFilterContainer_ClientStatsQuery,
variables: {
selector: {
organization: router.organizationId,
project: router.projectId,
target: router.targetId,
organization: organizationId,
project: projectId,
target: targetId,
period,
operations: [],
},
@ -584,10 +603,16 @@ export function ClientsFilterTrigger({
period,
onFilter,
selected,
organizationId,
projectId,
targetId,
}: {
period: DateRangeInput;
onFilter(keys: string[]): void;
selected?: string[];
organizationId: string;
projectId: string;
targetId: string;
}): ReactElement {
const [isOpen, toggle] = useToggle();
@ -598,6 +623,9 @@ export function ClientsFilterTrigger({
<FilterIcon className="ml-2 size-4" />
</Button>
<ClientsFilterContainer
organizationId={organizationId}
projectId={projectId}
targetId={targetId}
isOpen={isOpen}
onClose={toggle}
period={period}

View file

@ -1,27 +1,17 @@
import { ReactElement, SetStateAction, useCallback, useEffect, useMemo, useState } from 'react';
import Link from 'next/link';
import clsx from 'clsx';
import { useQuery } from 'urql';
import { useDebouncedCallback } from 'use-debounce';
import { Scale, Section } from '@/components/common';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import {
Button as OldButton,
Sortable,
Table,
TBody,
Td,
Th,
THead,
Tooltip,
Tr,
} from '@/components/v2';
import { Sortable, Table, TBody, Td, Th, THead, Tooltip, Tr } from '@/components/v2';
import { env } from '@/env/frontend';
import { FragmentType, graphql, useFragment } from '@/gql';
import { DateRangeInput } from '@/gql/graphql';
import { useDecimal, useFormattedDuration, useFormattedNumber } from '@/lib/hooks';
import { ChevronUpIcon, ExclamationTriangleIcon } from '@radix-ui/react-icons';
import { Link } from '@tanstack/react-router';
import {
createTable,
getCoreRowModel,
@ -72,26 +62,25 @@ function OperationRow({
<Tr>
<Td className="font-medium">
<div className="flex items-center gap-2">
<Link
href={{
pathname:
'/[organizationId]/[projectId]/[targetId]/insights/[operationName]/[operationHash]',
query: {
<Button variant="orangeLink" className="h-auto p-0" asChild>
<Link
className="block max-w-[300px] truncate"
to="/$organizationId/$projectId/$targetId/insights/$operationName/$operationHash"
params={{
organizationId: organization,
projectId: project,
targetId: target,
operationName: operation.name,
operationHash: operation.hash,
}}
search={{
from: selectedPeriod?.from ? encodeURIComponent(selectedPeriod.from) : undefined,
to: selectedPeriod?.to ? encodeURIComponent(selectedPeriod.to) : undefined,
},
}}
passHref
>
<OldButton variant="link" as="a" className="block max-w-[300px] truncate">
}}
>
{operation.name}
</OldButton>
</Link>
</Link>
</Button>
{operation.name === 'anonymous' && (
<Tooltip.Provider delayDuration={200}>
<Tooltip content="Anonymous operation detected. Naming your operations is a recommended practice">

View file

@ -26,9 +26,9 @@ import {
useFormattedDuration,
useFormattedNumber,
useFormattedThroughput,
useRouteSelector,
} from '@/lib/hooks';
import { useChartStyles } from '@/utils';
import { useRouter } from '@tanstack/react-router';
import { OperationsFallback } from './Fallback';
import { resolutionToMilliseconds } from './utils';
@ -421,8 +421,11 @@ function getLevelOption() {
function ClientsStats(props: {
operationStats: FragmentType<typeof ClientsStats_OperationsStatsFragment> | null;
organizationId: string;
projectId: string;
targetId: string;
}): ReactElement {
const router = useRouteSelector();
const router = useRouter();
const styles = useChartStyles();
const operationStats = useFragment(ClientsStats_OperationsStatsFragment, props.operationStats);
const sortedClients = useMemo(() => {
@ -555,12 +558,12 @@ function ClientsStats(props: {
return;
}
void router.push({
pathname: '/[organizationId]/[projectId]/[targetId]/insights/client/[name]',
query: {
organizationId: router.organizationId,
projectId: router.projectId,
targetId: router.targetId,
void router.navigate({
to: '/$organizationId/$projectId/$targetId/insights/client/$name',
params: {
organizationId: props.organizationId,
projectId: props.projectId,
targetId: props.targetId,
name: ev.value,
},
});
@ -1120,7 +1123,12 @@ export function OperationsStats({
</OperationsFallback>
<div>
<OperationsFallback state={state} refetch={refetch}>
<ClientsStats operationStats={operationsStats ?? null} />
<ClientsStats
operationStats={operationsStats ?? null}
organizationId={organization}
projectId={project}
targetId={target}
/>
</OperationsFallback>
</div>
<div>

View file

@ -42,14 +42,21 @@ export const ConnectLabModal = (props: {
To authenticate, use the following HTTP headers, with a token that has `target:read` scope:
</span>
<Tag>
X-Hive-Key:{' '}
<Link variant="secondary" target="_blank" rel="noreferrer" href={docsUrl}>
X-Hive-Key:
<Link
as="a"
variant="secondary"
target="_blank"
className="underline underline-offset-2"
rel="noreferrer"
href={docsUrl}
>
YOUR_TOKEN_HERE
</Link>
</Tag>
<p className="text-sm text-gray-500">
Read the{' '}
<Link variant="primary" target="_blank" rel="noreferrer" href={docsUrl}>
<Link as="a" variant="primary" target="_blank" rel="noreferrer" href={docsUrl}>
Managing Tokens
</Link>{' '}
chapter in our documentation to create a Registry Access Token.

View file

@ -4,7 +4,6 @@ import { useMutation, useQuery } from 'urql';
import * as Yup from 'yup';
import { Button, Heading, Input, Modal } from '@/components/v2';
import { graphql } from '@/gql';
import { useRouteSelector } from '@/lib/hooks';
const CollectionQuery = graphql(`
query Collection($selector: TargetSelectorInput!, $id: ID!) {
@ -102,16 +101,15 @@ const UpdateCollectionMutation = graphql(`
}
`);
export function CreateCollectionModal({
isOpen,
toggleModalOpen,
collectionId,
}: {
export function CreateCollectionModal(props: {
isOpen: boolean;
toggleModalOpen: () => void;
collectionId?: string;
organizationId: string;
projectId: string;
targetId: string;
}): ReactElement {
const router = useRouteSelector();
const { isOpen, toggleModalOpen, collectionId } = props;
const [mutationCreate, mutateCreate] = useMutation(CreateCollectionMutation);
const [mutationUpdate, mutateUpdate] = useMutation(UpdateCollectionMutation);
@ -120,9 +118,9 @@ export function CreateCollectionModal({
variables: {
id: collectionId!,
selector: {
target: router.targetId,
organization: router.organizationId,
project: router.projectId,
target: props.targetId,
organization: props.organizationId,
project: props.projectId,
},
},
pause: !collectionId,
@ -168,9 +166,9 @@ export function CreateCollectionModal({
const { error } = collectionId
? await mutateUpdate({
selector: {
target: router.targetId,
organization: router.organizationId,
project: router.projectId,
target: props.targetId,
organization: props.organizationId,
project: props.projectId,
},
input: {
collectionId,
@ -181,9 +179,9 @@ export function CreateCollectionModal({
})
: await mutateCreate({
selector: {
target: router.targetId,
organization: router.organizationId,
project: router.projectId,
target: props.targetId,
organization: props.organizationId,
project: props.projectId,
},
input: values,
});

View file

@ -4,9 +4,8 @@ import { useMutation } from 'urql';
import * as Yup from 'yup';
import { Button, Heading, Input, Modal, Select } from '@/components/v2';
import { graphql } from '@/gql';
import { useRouteSelector } from '@/lib/hooks';
import { useCollections } from '@/pages/target-laboratory';
import { useEditorContext } from '@graphiql/react';
import { useCollections } from '../../../../pages/[organizationId]/[projectId]/[targetId]/laboratory';
const CreateOperationMutation = graphql(`
mutation CreateOperation(
@ -48,19 +47,22 @@ const CreateOperationMutation = graphql(`
export type CreateOperationMutationType = typeof CreateOperationMutation;
export function CreateOperationModal({
isOpen,
close,
onSaveSuccess,
}: {
export function CreateOperationModal(props: {
isOpen: boolean;
close: () => void;
onSaveSuccess?: () => void;
onSaveSuccess?: (operationId?: string) => void;
organizationId: string;
projectId: string;
targetId: string;
}): ReactElement {
const router = useRouteSelector();
const { isOpen, close, onSaveSuccess } = props;
const [mutationCreate, mutateCreate] = useMutation(CreateOperationMutation);
const { collections, fetching } = useCollections();
const { collections, fetching } = useCollections({
organizationId: props.organizationId,
projectId: props.projectId,
targetId: props.targetId,
});
const { queryEditor, variableEditor, headerEditor } = useEditorContext({
nonNull: true,
@ -88,9 +90,9 @@ export function CreateOperationModal({
async onSubmit(values) {
const response = await mutateCreate({
selector: {
target: router.targetId,
organization: router.organizationId,
project: router.projectId,
target: props.targetId,
organization: props.organizationId,
project: props.projectId,
},
input: {
name: values.name,
@ -104,18 +106,7 @@ export function CreateOperationModal({
const error = response.error || response.data?.createOperationInDocumentCollection.error;
if (!error) {
if (result) {
const data = result.createOperationInDocumentCollection;
void router.push({
query: {
...router.query,
operation: data.ok?.operation.id,
},
});
}
onSaveSuccess?.();
onSaveSuccess?.(result?.createOperationInDocumentCollection.ok?.operation.id);
resetForm();
close();
}

View file

@ -2,7 +2,6 @@ import { ReactElement } from 'react';
import { useMutation } from 'urql';
import { Button, Heading, Modal } from '@/components/v2';
import { graphql } from '@/gql';
import { useRouteSelector } from '@/lib/hooks';
import { TrashIcon } from '@radix-ui/react-icons';
const DeleteCollectionMutation = graphql(`
@ -31,16 +30,15 @@ const DeleteCollectionMutation = graphql(`
export type DeleteCollectionMutationType = typeof DeleteCollectionMutation;
export function DeleteCollectionModal({
isOpen,
toggleModalOpen,
collectionId,
}: {
export function DeleteCollectionModal(props: {
isOpen: boolean;
toggleModalOpen: () => void;
collectionId: string;
organizationId: string;
projectId: string;
targetId: string;
}): ReactElement {
const router = useRouteSelector();
const { isOpen, toggleModalOpen, collectionId } = props;
const [, mutate] = useMutation(DeleteCollectionMutation);
return (
@ -66,9 +64,9 @@ export function DeleteCollectionModal({
await mutate({
id: collectionId,
selector: {
target: router.targetId,
organization: router.organizationId,
project: router.projectId,
target: props.targetId,
organization: props.organizationId,
project: props.projectId,
},
});
toggleModalOpen();

View file

@ -2,7 +2,7 @@ import { ReactElement } from 'react';
import { useMutation } from 'urql';
import { Button, Heading, Modal } from '@/components/v2';
import { graphql } from '@/gql';
import { useNotifications, useRouteSelector } from '@/lib/hooks';
import { useNotifications } from '@/lib/hooks';
import { TrashIcon } from '@radix-ui/react-icons';
const DeleteOperationMutation = graphql(`
@ -39,14 +39,14 @@ const DeleteOperationMutation = graphql(`
export type DeleteOperationMutationType = typeof DeleteOperationMutation;
export function DeleteOperationModal({
close,
operationId,
}: {
export function DeleteOperationModal(props: {
close: () => void;
operationId: string;
organizationId: string;
projectId: string;
targetId: string;
}): ReactElement {
const route = useRouteSelector();
const { close, operationId } = props;
const [, mutate] = useMutation(DeleteOperationMutation);
const notify = useNotifications();
@ -69,9 +69,9 @@ export function DeleteOperationModal({
const { error } = await mutate({
id: operationId,
selector: {
target: route.targetId,
organization: route.organizationId,
project: route.projectId,
target: props.targetId,
organization: props.organizationId,
project: props.projectId,
},
});

View file

@ -1,11 +1,10 @@
import { ReactElement, useMemo } from 'react';
import { useFormik } from 'formik';
import { useCollections } from 'packages/web/app/pages/[organizationId]/[projectId]/[targetId]/laboratory';
import { useMutation } from 'urql';
import * as Yup from 'yup';
import { Button, Heading, Input, Modal } from '@/components/v2';
import { graphql } from '@/gql';
import { useRouteSelector } from '@/lib/hooks';
import { useCollections } from '../../../pages/target-laboratory';
const UpdateOperationNameMutation = graphql(`
mutation UpdateOperation(
@ -32,10 +31,16 @@ const UpdateOperationNameMutation = graphql(`
export const EditOperationModal = (props: {
operationId: string;
close: () => void;
organizationId: string;
projectId: string;
targetId: string;
}): ReactElement => {
const router = useRouteSelector();
const [updateOperationNameState, mutate] = useMutation(UpdateOperationNameMutation);
const { collections } = useCollections();
const { collections } = useCollections({
organizationId: props.organizationId,
projectId: props.projectId,
targetId: props.targetId,
});
const [collection, operation] = useMemo(() => {
for (const collection of collections) {
@ -61,9 +66,9 @@ export const EditOperationModal = (props: {
async onSubmit(values) {
const response = await mutate({
selector: {
target: router.targetId,
organization: router.organizationId,
project: router.projectId,
target: props.targetId,
organization: props.organizationId,
project: props.projectId,
},
input: {
collectionId: values.collectionId,

View file

@ -1,9 +1,8 @@
import { ReactElement, useEffect, useMemo, useState } from 'react';
import NextLink from 'next/link';
import { useRouter } from 'next/router';
import { ReactElement, useEffect, useState } from 'react';
import { useFormik } from 'formik';
import { useMutation, useQuery } from 'urql';
import * as Yup from 'yup';
import { z } from 'zod';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import {
@ -23,7 +22,7 @@ import { InlineCode } from '@/components/v2/inline-code';
import { FragmentType, graphql, useFragment } from '@/gql';
import { TargetAccessScope } from '@/gql/graphql';
import { canAccessTarget } from '@/lib/access/target';
import { useRouteSelector } from '@/lib/hooks';
import { Link, useRouter } from '@tanstack/react-router';
const CDNAccessTokeRowFragment = graphql(`
fragment CDNAccessTokens_CdnAccessTokenRowFragment on CdnAccessToken {
@ -55,8 +54,10 @@ const CDNAccessTokenCreateMutation = graphql(`
function CreateCDNAccessTokenModal(props: {
onCreateCDNAccessToken: () => void;
onClose: () => void;
organizationId: string;
projectId: string;
targetId: string;
}): ReactElement {
const router = useRouteSelector();
const [createCdnAccessToken, mutate] = useMutation(CDNAccessTokenCreateMutation);
const form = useFormik({
@ -71,9 +72,9 @@ function CreateCDNAccessTokenModal(props: {
await mutate({
input: {
selector: {
organization: router.organizationId,
project: router.projectId,
target: router.targetId,
organization: props.organizationId,
project: props.projectId,
target: props.targetId,
},
alias: values.alias,
},
@ -193,8 +194,10 @@ function DeleteCDNAccessTokenModal(props: {
cdnAccessTokenId: string;
onDeletedAccessTokenId: (deletedAccessTokenId: string) => void;
onClose: () => void;
organizationId: string;
projectId: string;
targetId: string;
}): ReactElement {
const router = useRouteSelector();
const [deleteCdnAccessToken, mutate] = useMutation(CDNAccessTokenDeleteMutation);
useEffect(() => {
@ -230,9 +233,9 @@ function DeleteCDNAccessTokenModal(props: {
mutate({
input: {
selector: {
organization: router.organizationId,
project: router.projectId,
target: router.targetId,
organization: props.organizationId,
project: props.projectId,
target: props.targetId,
},
cdnAccessTokenId: props.cdnAccessTokenId,
},
@ -315,42 +318,43 @@ const CDNAccessTokensQuery = graphql(`
}
`);
const isDeleteCDNAccessTokenModalPath = (path: string): null | string => {
const pattern = /#delete-cdn-access-token\?id=(([a-zA-Z-\d]*))$/;
const result = path.match(pattern);
if (result === null) {
return null;
}
return result[1];
};
const CDNAccessTokens_MeFragment = graphql(`
fragment CDNAccessTokens_MeFragment on Member {
...CanAccessTarget_MemberFragment
}
`);
const CDNSearchParams = z.discriminatedUnion('cdn', [
z.object({
cdn: z.literal('create').optional(),
}),
z.object({
cdn: z.literal('delete'),
id: z.string(),
}),
]);
export function CDNAccessTokens(props: {
me: FragmentType<typeof CDNAccessTokens_MeFragment>;
organizationId: string;
projectId: string;
targetId: string;
}): React.ReactElement {
const me = useFragment(CDNAccessTokens_MeFragment, props.me);
const routerSelector = useRouteSelector();
const router = useRouter();
const [endCursors, setEndCursors] = useState<Array<string>>([]);
const router = useRouter();
const searchParamsResult = CDNSearchParams.safeParse(router.latestLocation.search);
const openCreateCDNAccessTokensModalLink = `${router.asPath}#create-cdn-access-token`;
const isCreateCDNAccessTokensModalOpen = router.asPath.endsWith('#create-cdn-access-token');
if (!searchParamsResult.success) {
console.error('Invalid search params', searchParamsResult.error);
}
const deleteCDNAccessTokenId = useMemo(() => {
return isDeleteCDNAccessTokenModalPath(router.asPath);
}, [router.asPath]);
const searchParams = searchParamsResult.data ?? { cdn: undefined };
const closeModal = () => {
void router.push(router.asPath.split('#')[0], undefined, {
scroll: false,
void router.navigate({
search: {},
});
};
@ -358,9 +362,9 @@ export function CDNAccessTokens(props: {
query: CDNAccessTokensQuery,
variables: {
selector: {
organization: routerSelector.organizationId,
project: routerSelector.projectId,
target: routerSelector.targetId,
organization: props.organizationId,
project: props.projectId,
target: props.targetId,
},
first: 10,
after: endCursors[endCursors.length - 1] ?? null,
@ -391,7 +395,13 @@ export function CDNAccessTokens(props: {
{canManage && (
<div className="my-3.5 flex justify-between">
<Button asChild>
<NextLink href={openCreateCDNAccessTokensModalLink}>Create new CDN token</NextLink>
<Link
search={{
cdn: 'create',
}}
>
Create new CDN token
</Link>
</Button>
</div>
)}
@ -414,9 +424,12 @@ export function CDNAccessTokens(props: {
className="hover:text-red-500"
variant="ghost"
onClick={() => {
void router.push(
`${router.asPath}#delete-cdn-access-token?id=${edge.node.id}`,
);
void router.navigate({
search: {
cdn: 'delete',
id: edge.node.id,
},
});
}}
>
<TrashIcon />
@ -463,21 +476,27 @@ export function CDNAccessTokens(props: {
) : null}
</div>
</CardContent>
{isCreateCDNAccessTokensModalOpen ? (
{searchParams.cdn === 'create' ? (
<CreateCDNAccessTokenModal
onCreateCDNAccessToken={() => {
reexecuteQuery({ requestPolicy: 'network-only' });
}}
onClose={closeModal}
organizationId={props.organizationId}
projectId={props.projectId}
targetId={props.targetId}
/>
) : null}
{deleteCDNAccessTokenId ? (
{searchParams.cdn === 'delete' ? (
<DeleteCDNAccessTokenModal
cdnAccessTokenId={deleteCDNAccessTokenId}
cdnAccessTokenId={searchParams.id}
onDeletedAccessTokenId={() => {
reexecuteQuery({ requestPolicy: 'network-only' });
}}
onClose={closeModal}
organizationId={props.organizationId}
projectId={props.projectId}
targetId={props.targetId}
/>
) : null}
</Card>

View file

@ -39,7 +39,6 @@ import { TimeAgo } from '@/components/ui/time-ago';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
import { DocsLink, Heading } from '@/components/v2';
import { FragmentType, graphql, useFragment } from '@/gql';
import { useRouteSelector } from '@/lib/hooks';
import { cn } from '@/lib/utils';
import { InfoCircledIcon } from '@radix-ui/react-icons';
@ -138,17 +137,20 @@ function DisableContractDialog(props: { contractId: string; onClose: () => void
);
}
export function SchemaContracts() {
const router = useRouteSelector();
export function SchemaContracts(props: {
organizationId: string;
projectId: string;
targetId: string;
}) {
const [disabledContractId, setDisabledContractId] = useState<string | null>(null);
const [schemaContractsQuery, reexecuteQuery] = useQuery({
query: SchemaContractsQuery,
variables: {
selector: {
organization: router.organizationId,
project: router.projectId,
target: router.targetId,
organization: props.organizationId,
project: props.projectId,
target: props.targetId,
},
},
});

View file

@ -14,6 +14,7 @@ const buttonVariants = cva(
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
ghost: 'hover:bg-accent hover:text-accent-foreground',
link: 'underline-offset-4 hover:underline text-primary',
orangeLink: 'h-auto p-0 underline-offset-4 hover:underline text-orange-500',
},
size: {
default: 'h-10 py-2 px-4',

View file

@ -1,5 +1,4 @@
import { ReactElement, useCallback, useEffect } from 'react';
import Link from 'next/link';
import { format } from 'date-fns/format';
import { Button } from '@/components/ui/button';
import { Popover, PopoverArrow, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
@ -50,7 +49,7 @@ function ChangelogPopover(props: { changes: Changelog[] }) {
return (
<Popover open={isOpen} onOpenChange={toggle}>
{props.changes.length > 0 ? (
<PopoverTrigger>
<PopoverTrigger asChild>
<Button variant="outline" className="relative text-sm">
Latest changes
{displayDot ? (
@ -86,14 +85,14 @@ function ChangelogPopover(props: { changes: Changelog[] }) {
{format(new Date(change.date), 'do MMMM yyyy')}
</time>
<h3 className="text-pretty text-base font-semibold text-white hover:underline">
<Link
<a
target="_blank"
rel="noopener"
rel="noreferrer"
onClick={() => handleChangelogClick(change)}
href={change.href}
>
{change.title}
</Link>
</a>
</h3>
<div className="mb-4 mt-1 text-pretty text-sm font-normal text-white/80">
{change.description}
@ -104,13 +103,13 @@ function ChangelogPopover(props: { changes: Changelog[] }) {
</div>
<div className="flex flex-row items-center justify-center">
<Button variant="link" asChild className="text-left text-sm">
<Link
<a
rel="noopener noreferrer"
href="https://the-guild.dev/graphql/hive/product-updates"
target="_blank"
>
View all updates
</Link>
</a>
</Button>
</div>
</PopoverContent>

View file

@ -0,0 +1,32 @@
import { Helmet } from 'react-helmet-async';
const defaultDescription =
'GraphQL Hive is Open GraphQL Platform to help you prevent breaking changes, monitor performance of your GraphQL API, and manage your API gateway (Federation, Stitching) with the Schema Registry. GraphQL Hive is 100% open source and can be self-hosted.';
const defaultSuffix = 'GraphQL Hive';
export function Meta({
title,
description = defaultDescription,
suffix = defaultSuffix,
}: {
title: string;
description?: string;
suffix?: string;
}) {
const fullTitle = suffix ? `${title} | ${suffix}` : title;
return (
<Helmet>
<title>{fullTitle}</title>
<meta property="og:title" content={fullTitle} key="title" />
<meta name="description" content={description} key="description" />
<meta property="og:url" key="og:url" content="https://app.graphql-hive.com" />
<meta property="og:type" key="og:type" content="website" />
<meta
property="og:image"
key="og:image"
content="https://og-image.the-guild.dev/?product=HIVE&title=Open%20GraphQL%20Platform&extra=Prevent breaking changes, monitor performance and manage your gateway (Federation, Stitching)"
/>
</Helmet>
);
}

View file

@ -1,7 +1,6 @@
import Image from 'next/image';
import ghost from '../../../public/images/figures/ghost.svg?url';
import { Card, Heading } from '@/components/v2/index';
import { cn } from '@/lib/utils';
import ghost from '../../../public/images/figures/ghost.svg';
export const NotFound = ({
title,
@ -17,7 +16,7 @@ export const NotFound = ({
className={cn('flex grow cursor-default flex-col items-center gap-y-2', className)}
data-cy="empty-list"
>
<Image src={ghost} alt="Ghost illustration" width="200" height="200" className="drag-none" />
<img src={ghost} alt="Ghost illustration" width="200" height="200" className="drag-none" />
<Heading className="text-center">{title}</Heading>
<span className="text-center text-sm font-medium text-gray-500">{description}</span>
</Card>

View file

@ -1,18 +1,19 @@
import { ReactElement } from 'react';
import Link from 'next/link';
import { useRouter } from 'next/router';
import cookies from 'js-cookie';
import { LogOutIcon } from 'lucide-react';
import { CombinedError } from 'urql';
import { Button } from '@/components/ui/button';
import { LAST_VISITED_ORG_KEY } from '@/constants';
import { Link, useRouter } from '@tanstack/react-router';
export function QueryError({
error,
showError,
organizationId,
}: {
error: CombinedError;
showError?: boolean;
organizationId: string | null;
}): ReactElement {
const router = useRouter();
const requestId =
@ -26,14 +27,16 @@ export function QueryError({
const isNetworkError = !!error.networkError;
const isExpectedError = !isNetworkError && !containsUnexpectedError;
const shouldShowError = typeof showError === 'boolean' ? showError : isExpectedError;
const organizationId =
typeof router.query.organizationId === 'string' ? router.query.organizationId : null;
return (
<div className="flex size-full items-center justify-center">
<Button
variant="outline"
onClick={() => router.push('/logout')}
onClick={() =>
router.navigate({
to: '/logout',
})
}
className="absolute right-6 top-6"
>
<LogOutIcon className="mr-2 size-4" /> Sign out
@ -52,12 +55,7 @@ export function QueryError({
If you wish to track it later or share more details with us,{' '}
{organizationId ? (
<Button variant="link" className="h-auto p-0 text-orange-500" asChild>
<Link
href={{
pathname: '/[organizationId]/view/support',
query: { organizationId },
}}
>
<Link to="/$organizationId/view/support" params={{ organizationId }}>
you can use the support
</Link>
</Button>

View file

@ -1,4 +1,3 @@
import NextLink from 'next/link';
import { LifeBuoyIcon } from 'lucide-react';
import { FaGithub, FaGoogle, FaKey, FaUsersSlash } from 'react-icons/fa';
import {
@ -31,6 +30,7 @@ import { AuthProvider } from '@/gql/graphql';
import { getDocsUrl } from '@/lib/docs-url';
import { useToggle } from '@/lib/hooks';
import { cn } from '@/lib/utils';
import { Link } from '@tanstack/react-router';
import { GetStartedProgress } from '../get-started/trigger';
import { MemberRoleMigrationStickyNote } from '../organization/members/migration';
import { UserSettingsModal } from '../user/settings';
@ -163,25 +163,25 @@ export function UserMenu(props: {
) : null}
<DropdownMenuSeparator />
{organizations.nodes.map(org => (
<NextLink
href={{
pathname: '/[organizationId]',
query: { organizationId: org.cleanId },
<Link
to="/$organizationId"
params={{
organizationId: org.cleanId,
}}
key={org.cleanId}
>
<DropdownMenuItem active={currentOrganization?.cleanId === org.cleanId}>
{org.name}
</DropdownMenuItem>
</NextLink>
</Link>
))}
<DropdownMenuSeparator />
<NextLink href="/org/new">
<Link to="/org/new">
<DropdownMenuItem>
Create organization
<PlusIcon className="ml-2 size-4" />
</DropdownMenuItem>
</NextLink>
</Link>
</DropdownMenuSubContent>
</DropdownMenuSub>
<DropdownMenuItem asChild>
@ -211,17 +211,17 @@ export function UserMenu(props: {
</a>
</DropdownMenuItem>
{currentOrganization && env.zendeskSupport ? (
<NextLink
href={{
pathname: '/[organizationId]/view/support',
query: { organizationId: currentOrganization.cleanId },
<Link
to="/$organizationId/view/support"
params={{
organizationId: currentOrganization.cleanId,
}}
>
<DropdownMenuItem>
<LifeBuoyIcon className="mr-2 size-4" />
Support
</DropdownMenuItem>
</NextLink>
</Link>
) : null}
<DropdownMenuItem asChild>
<a href="https://status.graphql-hive.com" target="_blank" rel="noreferrer">
@ -230,20 +230,20 @@ export function UserMenu(props: {
</a>
</DropdownMenuItem>
{me.isAdmin === true && (
<NextLink href="/manage">
<Link to="/manage">
<DropdownMenuItem>
<TrendingUpIcon className="mr-2 size-4" />
Manage Instance
</DropdownMenuItem>
</NextLink>
</Link>
)}
{env.nodeEnv === 'development' && (
<NextLink href="/dev">
<Link to="/dev">
<DropdownMenuItem>
<GraphQLIcon className="mr-2 size-4" />
Dev GraphiQL
</DropdownMenuItem>
</NextLink>
</Link>
)}
<DropdownMenuSeparator />
{canLeaveOrganization ? (

View file

@ -3,7 +3,6 @@ import { useQuery } from 'urql';
import { Link, TimeAgo } from '@/components/v2';
import { EditIcon, PlusIcon, TrashIcon, UserPlusMinusIcon } from '@/components/v2/icon';
import { DocumentType, graphql, useFragment } from '@/gql';
import { useRouteSelector } from '@/lib/hooks';
import { Subtitle, Title } from '../ui/page';
const Activities_OrganizationActivitiesQuery = graphql(`
@ -279,35 +278,30 @@ export const getActivity = (
project && organization ? (
<Link
variant="primary"
href={{
pathname: '/[organizationId]/[projectId]',
query: {
organizationId: organization.cleanId,
projectId: project.cleanId,
},
to="/$organizationId/$projectId"
params={{
organizationId: organization.cleanId,
projectId: project.cleanId,
}}
>
{project.name}
</Link>
) : null;
const targetHref =
target && project && organization
? {
pathname: '/[organizationId]/[projectId]/[targetId]',
query: {
organizationId: organization.cleanId,
projectId: project.cleanId,
targetId: target.cleanId,
},
}
: '#';
const targetLink = target ? (
<Link variant="primary" href={targetHref}>
{target.name}
</Link>
) : null;
const targetLink =
target && project && organization ? (
<Link
variant="primary"
to="/$organizationId/$projectId/$targetId"
params={{
organizationId: organization.cleanId,
projectId: project.cleanId,
targetId: target.cleanId,
}}
>
{target.name}
</Link>
) : null;
switch (type) {
/* Organization */
@ -433,9 +427,21 @@ export const getActivity = (
content: (
<>
{userDisplayName} changed{' '}
<Link variant="primary" href={targetHref}>
{activity.value}
</Link>{' '}
{organization && project && target ? (
<Link
variant="primary"
to="/$organizationId/$projectId/$targetId"
params={{
organizationId: organization.cleanId,
projectId: project.cleanId,
targetId: target.cleanId,
}}
>
{activity.value}
</Link>
) : (
activity.value
)}{' '}
target name in {projectLink} project
</>
),
@ -456,13 +462,12 @@ export const getActivity = (
}
};
export const Activities = (): ReactElement => {
const router = useRouteSelector();
export const Activities = (props: { organizationId: string }): ReactElement => {
const [organizationActivitiesQuery] = useQuery({
query: Activities_OrganizationActivitiesQuery,
variables: {
selector: {
organization: router.organizationId,
organization: props.organizationId,
limit: 5,
},
},

View file

@ -1,33 +1,27 @@
import { forwardRef, ReactNode } from 'react';
import NextLink, { LinkProps } from 'next/link';
import { forwardRef, HTMLAttributes } from 'react';
import { cn } from '@/lib/utils';
import { Slot } from '@radix-ui/react-slot';
type CardProps = (
| {
as?: never;
href?: never;
}
| ({
as: typeof NextLink;
} & LinkProps)
) & {
children?: ReactNode;
className?: string;
};
export interface CardProps extends HTMLAttributes<HTMLDivElement> {
asChild?: boolean;
}
export const Card = forwardRef<HTMLDivElement, CardProps>(
({ children, className, as, ...props }, forwardedRef) => {
const TagToUse = as || 'div';
const Card = forwardRef<HTMLDivElement, CardProps>(
({ children, className, asChild = false, ...props }, forwardedRef) => {
const Comp = asChild ? Slot : 'div';
return (
<TagToUse
// @ts-expect-error TODO: figure out what's wrong with ref here
<Comp
ref={forwardedRef}
className={cn('rounded-md border border-gray-800 p-5', className)}
{...props}
>
{children}
</TagToUse>
</Comp>
);
},
);
Card.displayName = 'Card';
export { Card };

View file

@ -5,6 +5,7 @@ import { QueryError } from '@/components/ui/query-error';
export class DataWrapper<TData, TVariables extends AnyVariables> extends Component<{
query: UseQueryState<TData, TVariables>;
showStale?: boolean;
organizationId: string | null;
children(props: { data: TData }): ReactNode;
spinnerComponent?: ReactNode;
}> {
@ -16,7 +17,7 @@ export class DataWrapper<TData, TVariables extends AnyVariables> extends Compone
}
if (error) {
return <QueryError error={error} />;
return <QueryError organizationId={this.props.organizationId} error={error} />;
}
if (!data) {

View file

@ -1,11 +1,9 @@
import { ReactElement } from 'react';
import NextLink from 'next/link';
import { Book, Megaphone } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { getDocsUrl, getProductUpdatesUrl } from '@/lib/docs-url';
import { cn } from '@/lib/utils';
import { ExternalLinkIcon } from '@radix-ui/react-icons';
import { Link } from './link';
export const DocsNote = ({ children, warn }: { warn?: boolean; children: React.ReactNode }) => {
return (
@ -39,11 +37,11 @@ export const DocsLink = ({
return (
<Button variant="link" className={cn('p-0 text-orange-500', className)} asChild>
<Link href={fullUrl} target="_blank" rel="noreferrer">
<a href={fullUrl} target="_blank" rel="noreferrer">
{icon ?? <Book className="mr-2 size-4" />}
{children}
<ExternalLinkIcon className="inline pl-1" />
</Link>
</a>
</Button>
);
};
@ -69,17 +67,16 @@ export const ProductUpdatesLink = ({
return (
<Button variant="link" className={cn('p-0 text-blue-500', className)} asChild>
<NextLink
<a
href={fullUrl}
target={isExternal ? '_blank' : undefined}
rel="noreferrer"
className="font-medium transition-colors hover:underline"
scroll={false}
>
{icon ?? <Megaphone className="mr-2 size-4" />}
{children}
{isExternal ? <ExternalLinkIcon className="inline pl-1" /> : null}
</NextLink>
</a>
</Button>
);
};

Some files were not shown because too many files have changed in this diff Show more