mirror of
https://github.com/graphql-hive/console
synced 2026-04-21 14:37:17 +00:00
Replace Next with Vite + tanstack/router (#4612)
This commit is contained in:
parent
82f4ff2be9
commit
368f284a97
180 changed files with 6361 additions and 7610 deletions
|
|
@ -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 }],
|
||||
// },
|
||||
|
|
|
|||
18
.github/actions/setup/action.yml
vendored
18
.github/actions/setup/action.yml
vendored
|
|
@ -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' }}
|
||||
|
|
|
|||
1
.github/workflows/apollo-router-updater.yaml
vendored
1
.github/workflows/apollo-router-updater.yaml
vendored
|
|
@ -20,7 +20,6 @@ jobs:
|
|||
with:
|
||||
codegen: false
|
||||
actor: apollo-router-updater
|
||||
cacheNext: false
|
||||
cacheTurbo: false
|
||||
|
||||
- name: Check for updates
|
||||
|
|
|
|||
1
.github/workflows/db-types-diff.yaml
vendored
1
.github/workflows/db-types-diff.yaml
vendored
|
|
@ -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
|
||||
|
|
|
|||
3
.github/workflows/lint.yaml
vendored
3
.github/workflows/lint.yaml
vendored
|
|
@ -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 \
|
||||
|
|
|
|||
1
.github/workflows/prettier.yaml
vendored
1
.github/workflows/prettier.yaml
vendored
|
|
@ -25,7 +25,6 @@ jobs:
|
|||
uses: ./.github/actions/setup
|
||||
with:
|
||||
actor: prettier
|
||||
cacheNext: false
|
||||
cacheTurbo: false
|
||||
|
||||
- name: Cache ESLint and Prettier
|
||||
|
|
|
|||
1
.github/workflows/publish-rust.yaml
vendored
1
.github/workflows/publish-rust.yaml
vendored
|
|
@ -45,7 +45,6 @@ jobs:
|
|||
with:
|
||||
actor: publish-rust
|
||||
codegen: false
|
||||
cacheNext: false
|
||||
cacheTurbo: false
|
||||
|
||||
- name: Prepare MacOS
|
||||
|
|
|
|||
1
.github/workflows/release-alpha.yaml
vendored
1
.github/workflows/release-alpha.yaml
vendored
|
|
@ -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
|
||||
|
|
|
|||
1
.github/workflows/release-stable.yaml
vendored
1
.github/workflows/release-stable.yaml
vendored
|
|
@ -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
|
||||
|
|
|
|||
1
.github/workflows/storybook.yaml
vendored
1
.github/workflows/storybook.yaml
vendored
|
|
@ -28,7 +28,6 @@ jobs:
|
|||
with:
|
||||
codegen: true
|
||||
actor: storybook
|
||||
cacheNext: true
|
||||
cacheTurbo: true
|
||||
|
||||
- uses: the-guild-org/shared-config/website-cf@main
|
||||
|
|
|
|||
1
.github/workflows/tests-db-migrations.yaml
vendored
1
.github/workflows/tests-db-migrations.yaml
vendored
|
|
@ -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
|
||||
|
|
|
|||
1
.github/workflows/tests-e2e.yaml
vendored
1
.github/workflows/tests-e2e.yaml
vendored
|
|
@ -29,7 +29,6 @@ jobs:
|
|||
with:
|
||||
codegen: false
|
||||
actor: test-e2e
|
||||
cacheNext: false
|
||||
cacheTurbo: false
|
||||
|
||||
- name: Install Cypress binary
|
||||
|
|
|
|||
1
.github/workflows/tests-integration.yaml
vendored
1
.github/workflows/tests-integration.yaml
vendored
|
|
@ -45,7 +45,6 @@ jobs:
|
|||
uses: ./.github/actions/setup
|
||||
with:
|
||||
actor: test-integration
|
||||
cacheNext: false
|
||||
cacheTurbo: true
|
||||
|
||||
- name: prepare packages
|
||||
|
|
|
|||
1
.github/workflows/tests-unit.yaml
vendored
1
.github/workflows/tests-unit.yaml
vendored
|
|
@ -14,7 +14,6 @@ jobs:
|
|||
uses: ./.github/actions/setup
|
||||
with:
|
||||
actor: test-unit
|
||||
cacheNext: false
|
||||
cacheTurbo: false
|
||||
|
||||
- name: unit tests
|
||||
|
|
|
|||
1
.github/workflows/typescript-typecheck.yaml
vendored
1
.github/workflows/typescript-typecheck.yaml
vendored
|
|
@ -15,7 +15,6 @@ jobs:
|
|||
uses: ./.github/actions/setup
|
||||
with:
|
||||
actor: typescript-typecheck
|
||||
cacheNext: false
|
||||
cacheTurbo: false
|
||||
|
||||
- name: get cpu count
|
||||
|
|
|
|||
1
.github/workflows/website.yaml
vendored
1
.github/workflows/website.yaml
vendored
|
|
@ -24,7 +24,6 @@ jobs:
|
|||
with:
|
||||
codegen: false
|
||||
actor: website
|
||||
cacheNext: false
|
||||
cacheTurbo: false
|
||||
|
||||
- uses: the-guild-org/shared-config/website-cf@main
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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(' ');
|
||||
|
|
|
|||
|
|
@ -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" ]
|
||||
|
|
@ -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"),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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(_: {
|
||||
|
|
|
|||
|
|
@ -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 => {
|
||||
|
|
|
|||
|
|
@ -32,3 +32,5 @@ INTEGRATION_GITHUB_APP_NAME="<sync>"
|
|||
|
||||
# Stripe
|
||||
STRIPE_PUBLIC_KEY="<sync>"
|
||||
|
||||
LOG_LEVEL=debug
|
||||
|
|
|
|||
3
packages/web/app/.gitignore
vendored
3
packages/web/app/.gitignore
vendored
|
|
@ -30,9 +30,6 @@ npm-debug.log*
|
|||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# next-env-runtime
|
||||
public/__ENV.js
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
16
packages/web/app/index.html
Normal file
16
packages/web/app/index.html
Normal 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>
|
||||
5
packages/web/app/next-env.d.ts
vendored
5
packages/web/app/next-env.d.ts
vendored
|
|
@ -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.
|
||||
|
|
@ -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;
|
||||
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default } from '../checks';
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default } from '../history';
|
||||
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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`);
|
||||
}
|
||||
|
|
@ -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}`);
|
||||
}
|
||||
|
|
@ -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('/');
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
import { NextApiRequest, NextApiResponse } from 'next';
|
||||
|
||||
export default async function health(req: NextApiRequest, res: NextApiResponse) {
|
||||
res.status(200).json({});
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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`);
|
||||
}
|
||||
|
|
@ -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}`);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
54
packages/web/app/src/components/error.tsx
Normal file
54
packages/web/app/src/components/error.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
49
packages/web/app/src/components/not-found.tsx
Normal file
49
packages/web/app/src/components/not-found.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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{' '}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
14
packages/web/app/src/components/organization/stripe.tsx
Normal file
14
packages/web/app/src/components/organization/stripe.tsx
Normal 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}</>;
|
||||
}
|
||||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
});
|
||||
}}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
32
packages/web/app/src/components/ui/meta.tsx
Normal file
32
packages/web/app/src/components/ui/meta.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 ? (
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Reference in a new issue