feat: preflight scripts for laboratory (#5564)

Co-authored-by: Kamil Kisiela <kamil.kisiela@gmail.com>
Co-authored-by: Saihajpreet Singh <saihajpreet.singh@gmail.com>
Co-authored-by: Laurin Quast <laurinquast@googlemail.com>
Co-authored-by: Dotan Simha <dotansimha@gmail.com>
This commit is contained in:
Dimitri POSTOLOV 2024-12-27 18:06:52 +08:00 committed by GitHub
parent 38c14e21d8
commit e0eb3bdb28
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
60 changed files with 2882 additions and 345 deletions

View file

@ -0,0 +1,8 @@
---
'hive': minor
---
Add preflight scripts for laboratory.
It is now possible to add a preflight script within the laboratory that executes before sending a GraphQL request.
[Learn more.](https://the-guild.dev/graphql/hive/product-updates/2024-12-27-preflight-script)

View file

@ -27,7 +27,7 @@ jobs:
- name: setup environment
uses: ./.github/actions/setup
with:
codegen: false
codegen: true
actor: test-e2e
cacheTurbo: false
@ -43,7 +43,7 @@ jobs:
timeout-minutes: 10
run: |
docker compose \
--env-file docker/.end2end.env \
--env-file integration-tests/.env \
-f docker/docker-compose.community.yml \
-f docker/docker-compose.end2end.yml \
up -d --wait
@ -65,7 +65,7 @@ jobs:
docker --version
docker ps --format json | jq .
docker compose \
--env-file docker/.end2end.env \
--env-file integration-tests/.env \
-f docker/docker-compose.community.yml \
-f docker/docker-compose.end2end.yml \
logs

View file

@ -1,19 +1,38 @@
import * as fs from 'node:fs';
// eslint-disable-next-line import/no-extraneous-dependencies -- cypress SHOULD be a dev dependency
import fs from 'node:fs';
import { defineConfig } from 'cypress';
import { initSeed } from './integration-tests/testkit/seed';
if (!process.env.RUN_AGAINST_LOCAL_SERVICES) {
const dotenv = await import('dotenv');
dotenv.config({ path: import.meta.dirname + '/integration-tests/.env' });
}
const isCI = Boolean(process.env.CI);
export const seed = initSeed();
export default defineConfig({
video: isCI,
screenshotOnRunFailure: isCI,
defaultCommandTimeout: 15_000, // sometimes the app takes longer to load, especially in the CI
retries: 2,
env: {
POSTGRES_URL: 'postgresql://postgres:postgres@localhost:5432/registry',
},
e2e: {
setupNodeEvents(on) {
on('task', {
async seedTarget() {
const owner = await seed.createOwner();
const org = await owner.createOrg();
const project = await org.createProject();
const slug = `${org.organization.slug}/${project.project.slug}/${project.target.slug}`;
return {
slug,
refreshToken: owner.ownerRefreshToken,
email: owner.ownerEmail,
};
},
});
on('after:spec', (_, results) => {
if (results && results.video) {
// Do we have failures for any retry attempts?

View file

@ -0,0 +1,196 @@
beforeEach(() => {
cy.clearLocalStorage().then(async () => {
cy.task('seedTarget').then(({ slug, refreshToken }: any) => {
cy.setCookie('sRefreshToken', refreshToken);
cy.visit(`/${slug}/laboratory`);
cy.get('[aria-label*="Preflight Script"]').click();
});
});
});
/** Helper function for setting the text within a monaco editor as typing manually results in flaky tests */
function setMonacoEditorContents(editorCyName: string, text: string) {
// wait for textarea appearing which indicates monaco is loaded
cy.dataCy(editorCyName).find('textarea');
cy.window().then(win => {
// First, check if monaco is available on the main window
const editor = (win as any).monaco.editor
.getEditors()
.find(e => e.getContainerDomNode().parentElement.getAttribute('data-cy') === editorCyName);
// If Monaco instance is found
if (editor) {
editor.setValue(text);
} else {
throw new Error('Monaco editor not found on the window or frames[0]');
}
});
}
function setEditorScript(script: string) {
setMonacoEditorContents('preflight-script-editor', script);
}
describe('Preflight Script', () => {
it('mini script editor is read only', () => {
cy.dataCy('toggle-preflight-script').click();
// Wait loading disappears
cy.dataCy('preflight-script-editor-mini').should('not.contain', 'Loading');
// Click
cy.dataCy('preflight-script-editor-mini').click();
// And type
cy.dataCy('preflight-script-editor-mini').within(() => {
cy.get('textarea').type('🐝', { force: true });
});
cy.dataCy('preflight-script-editor-mini').should(
'have.text',
'Cannot edit in read-only editor',
);
});
});
describe('Preflight Script Modal', () => {
const script = 'console.log("Hello_world")';
const env = '{"foo":123}';
beforeEach(() => {
cy.dataCy('preflight-script-modal-button').click();
setMonacoEditorContents('env-editor', env);
});
it('save script and environment variables when submitting', () => {
setEditorScript(script);
cy.dataCy('preflight-script-modal-submit').click();
cy.dataCy('env-editor-mini').should('have.text', env);
cy.dataCy('toggle-preflight-script').click();
cy.dataCy('preflight-script-editor-mini').should('have.text', script);
cy.reload();
cy.get('[aria-label*="Preflight Script"]').click();
cy.dataCy('env-editor-mini').should('have.text', env);
cy.dataCy('preflight-script-editor-mini').should('have.text', script);
});
it('logs show console/error information', () => {
setEditorScript(script);
cy.dataCy('run-preflight-script').click();
cy.dataCy('console-output').should('contain', 'Log: Hello_world (Line: 1, Column: 1)');
setEditorScript(
`console.info(1)
console.warn(true)
console.error('Fatal')
throw new TypeError('Test')`,
);
cy.dataCy('run-preflight-script').click();
// First log previous log message
cy.dataCy('console-output').should('contain', 'Log: Hello_world (Line: 1, Column: 1)');
// After the new logs
cy.dataCy('console-output').should(
'contain',
[
'Info: 1 (Line: 1, Column: 1)',
'Warn: true (Line: 2, Column: 1)',
'Error: Fatal (Line: 3, Column: 1)',
'TypeError: Test (Line: 4, Column: 7)',
].join(''),
);
});
it('script execution updates environment variables', () => {
setEditorScript(`lab.environment.set('my-test', "TROLOLOL")`);
cy.dataCy('run-preflight-script').click();
cy.dataCy('env-editor').should(
'include.text',
// replace space with &nbsp;
'{ "foo": 123, "my-test": "TROLOLOL"}'.replaceAll(' ', '\xa0'),
);
});
it('`crypto-js` can be used for generating hashes', () => {
setEditorScript('console.log(lab.CryptoJS.SHA256("🐝"))');
cy.dataCy('run-preflight-script').click();
cy.dataCy('console-output').should('contain', 'Info: Using crypto-js version:');
cy.dataCy('console-output').should(
'contain',
'Log: d5b51e79e4be0c4f4d6b9a14e16ca864de96afe68459e60a794e80393a4809e8',
);
});
it('scripts can not use `eval`', () => {
setEditorScript('eval()');
cy.dataCy('preflight-script-modal-submit').click();
cy.get('body').contains('Usage of dangerous statement like eval() or Function("").');
});
it('invalid code is rejected and can not be saved', () => {
setEditorScript('🐝');
cy.dataCy('preflight-script-modal-submit').click();
cy.get('body').contains("[1:1]: Illegal character '}");
});
});
describe('Execution', () => {
it('header placeholders are substituted with environment variables', () => {
cy.dataCy('toggle-preflight-script').click();
cy.get('[data-name="headers"]').click();
cy.get('.graphiql-editor-tool .graphiql-editor:last-child textarea').type(
'{ "__test": "{{foo}} bar {{nonExist}}" }',
{
force: true,
parseSpecialCharSequences: false,
},
);
cy.dataCy('env-editor-mini').within(() => {
cy.get('textarea').type('{"foo":"injected"}', {
force: true,
parseSpecialCharSequences: false,
});
});
cy.intercept('/api/lab/foo/my-new-project/development', req => {
expect(req.headers.__test).to.equal('injected bar {{nonExist}}');
});
cy.get('body').type('{ctrl}{enter}');
});
it('executed script updates update env editor and substitute headers', () => {
cy.dataCy('toggle-preflight-script').click();
cy.get('[data-name="headers"]').click();
cy.get('.graphiql-editor-tool .graphiql-editor:last-child textarea').type(
'{ "__test": "{{foo}}" }',
{
force: true,
parseSpecialCharSequences: false,
},
);
cy.dataCy('preflight-script-modal-button').click();
setMonacoEditorContents('preflight-script-editor', `lab.environment.set('foo', 92)`);
cy.dataCy('preflight-script-modal-submit').click();
cy.intercept('/api/lab/foo/my-new-project/development', req => {
expect(req.headers.__test).to.equal('92');
});
cy.get('.graphiql-execute-button').click();
});
it('disabled script is not executed', () => {
cy.get('[data-name="headers"]').click();
cy.get('.graphiql-editor-tool .graphiql-editor:last-child textarea').type(
'{ "__test": "{{foo}}" }',
{
force: true,
parseSpecialCharSequences: false,
},
);
cy.dataCy('preflight-script-modal-button').click();
setMonacoEditorContents('preflight-script-editor', `lab.environment.set('foo', 92)`);
setMonacoEditorContents('env-editor', `{"foo":10}`);
cy.dataCy('preflight-script-modal-submit').click();
cy.intercept('/api/lab/foo/my-new-project/development', req => {
expect(req.headers.__test).to.equal('10');
});
cy.get('.graphiql-execute-button').click();
});
});

View file

@ -25,7 +25,7 @@ cd ..
docker buildx bake -f docker/docker.hcl build --load
echo "⬆️ Running all local containers..."
docker compose -f ./docker/docker-compose.community.yml -f ./docker/docker-compose.end2end.yml --env-file ./integration-tests/.env --env-file ./docker/.end2end.env up -d --wait
docker compose -f ./docker/docker-compose.community.yml -f ./docker/docker-compose.end2end.yml --env-file ./integration-tests/.env up -d --wait
echo "✅ E2E tests environment is ready. To run tests now, use:"
echo ""

View file

@ -1,8 +1,8 @@
{
"compilerOptions": {
"target": "es5",
"lib": ["es5", "dom"],
"target": "es2021",
"lib": ["es2021", "dom"],
"types": ["node", "cypress"]
},
"include": ["**/*.ts"]
"include": ["**/*.ts", "../integration-tests/testkit/**/*.ts"]
}

View file

@ -335,12 +335,10 @@ deployCloudFlareSecurityTransform({
// Staging
'staging.graphql-hive.com',
'app.staging.graphql-hive.com',
'lab-worker.staging.graphql-hive.com',
'cdn.staging.graphql-hive.com',
// Dev
'dev.graphql-hive.com',
'app.dev.graphql-hive.com',
'lab-worker.dev.graphql-hive.com',
'cdn.dev.graphql-hive.com',
],
});
@ -353,4 +351,4 @@ export const schemaApiServiceId = schema.service.id;
export const webhooksApiServiceId = webhooks.service.id;
export const appId = app.deployment.id;
export const publicIp = proxy!.status.loadBalancer.ingress[0].ip;
export const publicIp = proxy.get()!.status.loadBalancer.ingress[0].ip;

View file

@ -1,3 +1,4 @@
import { randomUUID } from 'crypto';
import * as pulumi from '@pulumi/pulumi';
import { serviceLocalEndpoint } from '../utils/local-endpoint';
import { ServiceDeployment } from '../utils/service-deployment';

View file

@ -34,7 +34,6 @@ export function deployCloudFlareSecurityTransform(options: {
)} } and not http.host in { ${toExpressionList(options.ignoredHosts)} }`;
// TODO: When Preflight PR is merged, we'll need to change this to build this host in a better way.
const labHost = `lab-worker.${options.environment.rootDns}`;
const monacoCdnDynamicBasePath: `https://${string}/` = `https://cdn.jsdelivr.net/npm/monaco-editor@${monacoEditorVersion}/`;
const monacoCdnStaticBasePath: `https://${string}/` = `https://cdn.jsdelivr.net/npm/monaco-editor@0.33.0/`;
const crispHost = 'client.crisp.chat';
@ -44,7 +43,6 @@ export function deployCloudFlareSecurityTransform(options: {
crispHost,
stripeHost,
gtmHost,
labHost,
'settings.crisp.chat',
'*.ingest.sentry.io',
'wss://client.relay.crisp.chat',
@ -57,7 +55,6 @@ export function deployCloudFlareSecurityTransform(options: {
const contentSecurityPolicy = `
default-src 'self';
frame-src ${stripeHost} https://game.crisp.chat;
worker-src 'self' blob: ${labHost};
style-src 'self' 'unsafe-inline' ${crispHost} fonts.googleapis.com rsms.me ${monacoCdnDynamicBasePath} ${monacoCdnStaticBasePath};
script-src 'self' 'unsafe-eval' 'unsafe-inline' {DYNAMIC_HOST_PLACEHOLDER} ${monacoCdnDynamicBasePath} ${monacoCdnStaticBasePath} ${cspHosts};
connect-src 'self' * {DYNAMIC_HOST_PLACEHOLDER} ${cspHosts};

View file

@ -100,6 +100,5 @@ export function deployProxy({
service: usage.service,
retriable: true,
},
])
.get();
]);
}

View file

@ -14,6 +14,67 @@ export class Proxy {
private staticIp?: { address?: string; aksReservedIpResourceGroup?: string },
) {}
registerInternalProxy(
dnsRecord: string,
route: {
path: string;
service: k8s.core.v1.Service;
host: string;
customRewrite: string;
},
) {
const cert = new k8s.apiextensions.CustomResource(`cert-${dnsRecord}`, {
apiVersion: 'cert-manager.io/v1',
kind: 'Certificate',
metadata: {
name: dnsRecord,
},
spec: {
commonName: dnsRecord,
dnsNames: [dnsRecord],
issuerRef: {
name: this.tlsSecretName,
kind: 'ClusterIssuer',
},
secretName: dnsRecord,
},
});
new k8s.apiextensions.CustomResource(
`internal-proxy-${dnsRecord}`,
{
apiVersion: 'projectcontour.io/v1',
kind: 'HTTPProxy',
metadata: {
name: `internal-proxy-metadata-${dnsRecord}`,
},
spec: {
virtualhost: {
fqdn: route.host,
tls: {
secretName: dnsRecord,
},
},
routes: [
{
conditions: [{ prefix: route.path }],
services: [
{
name: route.service.metadata.name,
port: route.service.spec.ports[0].port,
},
],
pathRewritePolicy: {
replacePrefix: [{ prefix: route.path, replacement: route.customRewrite }],
},
},
],
},
},
{ dependsOn: [cert, this.lbService!] },
);
}
registerService(
dns: { record: string; apex?: boolean },
routes: {
@ -29,7 +90,7 @@ export class Proxy {
withWwwDomain?: boolean;
// https://projectcontour.io/docs/1.29/config/rate-limiting/#local-rate-limiting
rateLimit?: {
// Max amount of request allowed with the "unit" paramter.
// Max amount of request allowed with the "unit" parameter.
maxRequests: number;
unit: 'second' | 'minute' | 'hour';
// defining the number of requests above the baseline rate that are allowed in a short period of time.

View file

@ -1,13 +0,0 @@
export HIVE_ENCRYPTION_SECRET=wowverysecuremuchsecret
export HIVE_EMAIL_FROM=no-reply@graphql-hive.com
export HIVE_APP_BASE_URL=http://localhost:8080
export SUPERTOKENS_API_KEY=wowverysecuremuchsecret
export CLICKHOUSE_USER=clickhouse
export CLICKHOUSE_PASSWORD=wowverysecuremuchsecret
export REDIS_PASSWORD=wowverysecuremuchsecret
export POSTGRES_PASSWORD=postgres
export POSTGRES_USER=postgres
export POSTGRES_DB=registry
export MINIO_ROOT_USER=minioadmin
export MINIO_ROOT_PASSWORD=minioadmin
export CDN_AUTH_PRIVATE_KEY=6b4721a99bd2ef6c00ce4328f34d95d7

View file

@ -34,5 +34,9 @@ services:
networks:
- 'stack'
supertokens:
ports:
- '3567:3567'
networks:
stack: {}

View file

@ -68,29 +68,18 @@ To run integration tests locally, from the pre-build Docker image, follow:
e2e Tests are based on Cypress, and matches files that ends with `.cy.ts`. The tests flow runs from
a pre-build Docker image.
#### Running from Source Code
#### Running on built Docker images from source code
To run e2e tests locally, from the local source code, follow:
1. Make sure you have Docker installed. If you are having issues, try to run `docker system prune`
to clean the Docker caches.
2. Install all deps: `pnpm i`
3. Generate types: `pnpm graphql:generate`
4. Build source code: `pnpm build`
5. Set env vars:
```bash
export COMMIT_SHA="local"
export RELEASE="local"
export BRANCH_NAME="local"
export BUILD_TYPE=""
export DOCKER_TAG=":local"
```
6. Compile a local Docker image by running: `docker buildx bake -f docker/docker.hcl build --load`
7. Run the e2e environment, by running:
`docker compose -f ./docker/docker-compose.community.yml -f ./docker/docker-compose.end2end.yml --env-file ./integration-tests/.env up -d --wait`
8. Run Cypress: `pnpm test:e2e`
3. Move into the `cypress` folder (`cd cypress`)
4. Run `./local.sh` for building the project and starting the Docker containers
5. Follow the output instruction from the script for starting the tests
#### Running from Pre-Built Docker Image
#### Running from pre-built Docker image
To run integration tests locally, from the pre-build Docker image, follow:
@ -105,7 +94,13 @@ To run integration tests locally, from the pre-build Docker image, follow:
export DOCKER_TAG=":IMAGE_TAG_HERE"
```
6. Run the e2e environment, by running:
`docker compose -f ./docker/docker-compose.community.yml --env-file ./integration-tests/.env up -d --wait`
```
docker compose \
-f ./docker/docker-compose.community.yml \
-f ./docker/docker-compose.end2end.yml \
--env-file ./integration-tests/.env \
up -d --wait
```
7. Run Cypress: `pnpm test:e2e`
#### Docker Compose Configuration

View file

@ -131,6 +131,7 @@ const createSession = async (
*/
return {
access_token: data.accessToken.token,
refresh_token: data.refreshToken.token,
};
} catch (e) {
console.warn(`Failed to create session:`, e);
@ -148,15 +149,17 @@ const tokenResponsePromise: {
[key: string]: Promise<z.TypeOf<typeof SignUpSignInUserResponseModel>> | null;
} = {};
export function authenticate(email: string): Promise<{ access_token: string }>;
export function authenticate(
email: string,
): Promise<{ access_token: string; refresh_token: string }>;
export function authenticate(
email: string,
oidcIntegrationId?: string,
): Promise<{ access_token: string }>;
): Promise<{ access_token: string; refresh_token: string }>;
export function authenticate(
email: string | string,
oidcIntegrationId?: string,
): Promise<{ access_token: string }> {
): Promise<{ access_token: string; refresh_token: string }> {
if (!tokenResponsePromise[email]) {
tokenResponsePromise[email] = signUpUserViaEmail(email, password);
}

View file

@ -202,3 +202,22 @@ export const DeleteOperationMutation = graphql(`
}
}
`);
export const UpdatePreflightScriptMutation = graphql(`
mutation UpdatePreflightScript($input: UpdatePreflightScriptInput!) {
updatePreflightScript(input: $input) {
ok {
updatedTarget {
id
preflightScript {
id
sourceCode
}
}
}
error {
message
}
}
}
`);

View file

@ -1,5 +1,5 @@
import { RuleInstanceSeverityLevel, SchemaPolicyInput } from 'testkit/gql/graphql';
import { graphql } from './gql';
import { RuleInstanceSeverityLevel, SchemaPolicyInput } from './gql/graphql';
export const OrganizationAndProjectsWithSchemaPolicy = graphql(`
query OrganizationAndProjectsWithSchemaPolicy($organization: String!) {

View file

@ -1,13 +1,5 @@
import { humanId } from 'human-id';
import { createPool, sql } from 'slonik';
import {
OrganizationAccessScope,
ProjectAccessScope,
ProjectType,
RegistryModel,
SchemaPolicyInput,
TargetAccessScope,
} from 'testkit/gql/graphql';
import type { Report } from '../../packages/libraries/core/src/client/usage.js';
import { authenticate, userEmail } from './auth';
import {
@ -17,6 +9,7 @@ import {
DeleteOperationMutation,
UpdateCollectionMutation,
UpdateOperationMutation,
UpdatePreflightScriptMutation,
} from './collections';
import { ensureEnv } from './env';
import {
@ -57,21 +50,29 @@ import {
updateSchemaVersionStatus,
updateTargetValidationSettings,
} from './flow';
import {
OrganizationAccessScope,
ProjectAccessScope,
ProjectType,
RegistryModel,
SchemaPolicyInput,
TargetAccessScope,
} from './gql/graphql';
import { execute } from './graphql';
import { UpdateSchemaPolicyForOrganization, UpdateSchemaPolicyForProject } from './schema-policy';
import { collect, CollectedOperation, legacyCollect } from './usage';
import { generateUnique } from './utils';
export function initSeed() {
const pg = {
user: ensureEnv('POSTGRES_USER'),
password: ensureEnv('POSTGRES_PASSWORD'),
host: ensureEnv('POSTGRES_HOST'),
port: ensureEnv('POSTGRES_PORT'),
db: ensureEnv('POSTGRES_DB'),
};
function createConnectionPool() {
const pg = {
user: ensureEnv('POSTGRES_USER'),
password: ensureEnv('POSTGRES_PASSWORD'),
host: ensureEnv('POSTGRES_HOST'),
port: ensureEnv('POSTGRES_PORT'),
db: ensureEnv('POSTGRES_DB'),
};
return createPool(
`postgres://${pg.user}:${pg.password}@${pg.host}:${pg.port}/${pg.db}?sslmode=disable`,
);
@ -87,15 +88,18 @@ export function initSeed() {
},
};
},
authenticate: authenticate,
authenticate,
generateEmail: () => userEmail(generateUnique()),
async createOwner() {
const ownerEmail = userEmail(generateUnique());
const ownerToken = await authenticate(ownerEmail).then(r => r.access_token);
const auth = await authenticate(ownerEmail);
const ownerRefreshToken = auth.refresh_token;
const ownerToken = auth.access_token;
return {
ownerEmail,
ownerToken,
ownerRefreshToken,
async createOrg() {
const orgSlug = generateUnique();
const orgResult = await createOrganization({ slug: orgSlug }, ownerToken).then(r =>
@ -296,6 +300,30 @@ export function initSeed() {
return result.createDocumentCollection;
},
async updatePreflightScript({
sourceCode,
token = ownerToken,
}: {
sourceCode: string;
token?: string;
}) {
const result = await execute({
document: UpdatePreflightScriptMutation,
variables: {
input: {
selector: {
organizationSlug: organization.slug,
projectSlug: project.slug,
targetSlug: target.slug,
},
sourceCode,
},
},
authToken: token,
}).then(r => r.expectNoGraphQLErrors());
return result.updatePreflightScript;
},
async updateDocumentCollection({
collectionId,
name,

View file

@ -0,0 +1,41 @@
import { ProjectType } from 'testkit/gql/graphql';
import { initSeed } from '../../../testkit/seed';
describe('Preflight Script', () => {
describe('CRUD', () => {
const rawJs = 'console.log("Hello World")';
it.concurrent('Update a Preflight Script', async () => {
const { updatePreflightScript } = await initSeed()
.createOwner()
.then(r => r.createOrg())
.then(r => r.createProject(ProjectType.Single));
const { error, ok } = await updatePreflightScript({ sourceCode: rawJs });
expect(error).toEqual(null);
expect(ok?.updatedTarget.preflightScript?.id).toBeDefined();
expect(ok?.updatedTarget.preflightScript?.sourceCode).toBe(rawJs);
});
describe('Permissions Check', () => {
it('Prevent updating a Preflight Script without the write permission to the target', async () => {
const { updatePreflightScript, createTargetAccessToken } = await initSeed()
.createOwner()
.then(r => r.createOrg())
.then(r => r.createProject(ProjectType.Single));
const { secret: readOnlyToken } = await createTargetAccessToken({ mode: 'readOnly' });
await expect(
updatePreflightScript({ sourceCode: rawJs, token: readOnlyToken }),
).rejects.toEqual(
expect.objectContaining({
message: expect.stringContaining(
`No access (reason: "Missing permission for performing 'laboratory:modifyPreflightScript' on resource")`,
),
}),
);
});
});
});
});

View file

@ -44,7 +44,7 @@
"seed": "tsx scripts/seed-local-env.ts",
"start": "pnpm run local:setup",
"test": "vitest",
"test:e2e": "CYPRESS_BASE_URL=$HIVE_APP_BASE_URL cypress run",
"test:e2e": "CYPRESS_BASE_URL=$HIVE_APP_BASE_URL cypress run --browser chrome",
"test:e2e:open": "CYPRESS_BASE_URL=$HIVE_APP_BASE_URL cypress open",
"test:integration": "cd integration-tests && pnpm test:integration",
"typecheck": "pnpm run -r --filter '!hive' typecheck",
@ -119,9 +119,11 @@
"slonik@30.4.4": "patches/slonik@30.4.4.patch",
"@oclif/core@3.26.6": "patches/@oclif__core@3.26.6.patch",
"oclif@4.13.6": "patches/oclif@4.13.6.patch",
"@graphiql/react@1.0.0-alpha.3": "patches/@graphiql__react@1.0.0-alpha.3.patch",
"graphiql": "patches/graphiql.patch",
"@graphiql/react": "patches/@graphiql__react.patch",
"countup.js": "patches/countup.js.patch",
"@oclif/core@4.0.6": "patches/@oclif__core@4.0.6.patch"
"@oclif/core@4.0.6": "patches/@oclif__core@4.0.6.patch",
"@fastify/vite": "patches/@fastify__vite.patch"
}
}
}

View file

@ -0,0 +1,23 @@
import { type MigrationExecutor } from '../pg-migrator';
export default {
name: '2024.12.27T00.00.00.create-preflight-scripts.ts',
run: ({ sql }) => sql`
CREATE TABLE IF NOT EXISTS "document_preflight_scripts" (
"id" uuid NOT NULL DEFAULT uuid_generate_v4(),
"source_code" text NOT NULL,
"target_id" uuid NOT NULL UNIQUE REFERENCES "targets"("id") ON DELETE CASCADE,
"created_by_user_id" uuid REFERENCES "users"("id") ON DELETE SET NULL,
"created_at" timestamptz NOT NULL DEFAULT now(),
"updated_at" timestamptz NOT NULL DEFAULT now(),
PRIMARY KEY ("id")
);
ALTER TABLE "document_preflight_scripts"
ADD CONSTRAINT "unique_target_id" UNIQUE ("target_id");
CREATE INDEX IF NOT EXISTS "document_preflight_scripts_target" ON "document_preflight_scripts" (
"target_id" ASC
);
`,
} satisfies MigrationExecutor;

View file

@ -149,5 +149,6 @@ export const runPGMigrations = async (args: { slonik: DatabasePool; runTo?: stri
await import('./actions/2024.11.12T00-00-00.supertokens-9.3'),
await import('./actions/2024.12.23T00-00-00.improve-version-index'),
await import('./actions/2024.12.24T00-00-00.improve-version-index-2'),
await import('./actions/2024.12.27T00.00.00.create-preflight-scripts'),
],
});

View file

@ -25,6 +25,8 @@
"@hive/usage-common": "workspace:*",
"@hive/usage-ingestor": "workspace:*",
"@hive/webhooks": "workspace:*",
"@nodesecure/i18n": "^4.0.1",
"@nodesecure/js-x-ray": "8.0.0",
"@octokit/app": "14.1.0",
"@octokit/core": "5.2.0",
"@octokit/plugin-retry": "6.1.0",

View file

@ -321,6 +321,12 @@ export const AuditLogModel = z.union([
updatedFields: z.string(),
}),
}),
z.object({
eventType: z.literal('PREFLIGHT_SCRIPT_CHANGED'),
metadata: z.object({
scriptContents: z.string(),
}),
}),
]);
export type AuditLogSchemaEvent = z.infer<typeof AuditLogModel>;

View file

@ -350,6 +350,7 @@ const actionDefinitions = {
'target:modifySettings': defaultTargetIdentity,
'laboratory:describe': defaultTargetIdentity,
'laboratory:modify': defaultTargetIdentity,
'laboratory:modifyPreflightScript': defaultTargetIdentity,
'appDeployment:describe': defaultTargetIdentity,
'appDeployment:create': defaultAppDeploymentIdentity,
'appDeployment:publish': defaultAppDeploymentIdentity,

View file

@ -307,7 +307,7 @@ function transformOrganizationMemberLegacyScopes(args: {
case TargetAccessScope.SETTINGS: {
policies.push({
effect: 'allow',
action: ['target:modifySettings'],
action: ['target:modifySettings', 'laboratory:modifyPreflightScript'],
resource: [`hrn:${args.organizationId}:organization/${args.organizationId}`],
});
break;

View file

@ -1,4 +1,5 @@
import { createModule } from 'graphql-modules';
import { PreflightScriptProvider } from './providers/preflight-script.provider';
import { resolvers } from './resolvers.generated';
import typeDefs from './module.graphql';
@ -7,5 +8,5 @@ export const labModule = createModule({
dirname: __dirname,
typeDefs,
resolvers,
providers: [],
providers: [PreflightScriptProvider],
});

View file

@ -8,4 +8,41 @@ export default gql`
schema: String!
mocks: JSON
}
type PreflightScript {
id: ID!
sourceCode: String!
createdAt: DateTime!
updatedAt: DateTime!
}
input UpdatePreflightScriptInput {
selector: TargetSelectorInput!
sourceCode: String!
}
extend type Mutation {
updatePreflightScript(input: UpdatePreflightScriptInput!): PreflightScriptResult!
}
"""
@oneOf
"""
type PreflightScriptResult {
ok: PreflightScriptOk
error: PreflightScriptError
}
type PreflightScriptOk {
preflightScript: PreflightScript!
updatedTarget: Target!
}
type PreflightScriptError implements Error {
message: String!
}
extend type Target {
preflightScript: PreflightScript
}
`;

View file

@ -0,0 +1,197 @@
import { Inject, Injectable, Scope } from 'graphql-modules';
import { sql, type DatabasePool } from 'slonik';
import { z } from 'zod';
import { getLocalLang, getTokenSync } from '@nodesecure/i18n';
import * as jsxray from '@nodesecure/js-x-ray';
import type { Target } from '../../../shared/entities';
import { AuditLogRecorder } from '../../audit-logs/providers/audit-log-recorder';
import { Session } from '../../auth/lib/authz';
import { IdTranslator } from '../../shared/providers/id-translator';
import { Logger } from '../../shared/providers/logger';
import { PG_POOL_CONFIG } from '../../shared/providers/pg-pool';
import { Storage } from '../../shared/providers/storage';
const SourceCodeModel = z.string().max(5_000);
const UpdatePreflightScriptModel = z.strictObject({
// Use validation only on insertion
sourceCode: SourceCodeModel.superRefine((val, ctx) => {
try {
const { warnings } = scanner.analyse(val);
for (const warning of warnings) {
const message = getTokenSync(jsxray.warnings[warning.kind].i18n);
throw new Error(message);
}
} catch (error) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: error instanceof Error ? error.message : String(error),
});
}
}),
});
const PreflightScriptModel = z.strictObject({
id: z.string(),
sourceCode: SourceCodeModel,
targetId: z.string(),
createdByUserId: z.union([z.string(), z.null()]),
createdAt: z.string(),
updatedAt: z.string(),
});
type PreflightScript = z.TypeOf<typeof PreflightScriptModel>;
const scanner = new jsxray.AstAnalyser();
await getLocalLang();
@Injectable({
global: true,
scope: Scope.Operation,
})
export class PreflightScriptProvider {
private logger: Logger;
constructor(
logger: Logger,
private storage: Storage,
private session: Session,
private idTranslator: IdTranslator,
private auditLogs: AuditLogRecorder,
@Inject(PG_POOL_CONFIG) private pool: DatabasePool,
) {
this.logger = logger.child({ source: 'PreflightScriptProvider' });
}
async getPreflightScript(targetId: string) {
const result = await this.pool.maybeOne<unknown>(sql`/* getPreflightScript */
SELECT
"id"
, "source_code" AS "sourceCode"
, "target_id" AS "targetId"
, "created_by_user_id" AS "createdByUserId"
, to_json("created_at") AS "createdAt"
, to_json("updated_at") AS "updatedAt"
FROM
"document_preflight_scripts"
WHERE
"target_id" = ${targetId}
`);
if (!result) {
return null;
}
return PreflightScriptModel.parse(result);
}
async updatePreflightScript(args: {
selector: {
organizationSlug: string;
projectSlug: string;
targetSlug: string;
};
sourceCode: string;
}): Promise<
| {
error: { message: string };
ok?: never;
}
| {
error?: never;
ok: {
preflightScript: PreflightScript;
updatedTarget: Target;
};
}
> {
const [organizationId, projectId, targetId] = await Promise.all([
this.idTranslator.translateOrganizationId(args.selector),
this.idTranslator.translateProjectId(args.selector),
this.idTranslator.translateTargetId(args.selector),
]);
await this.session.assertPerformAction({
action: 'laboratory:modifyPreflightScript',
organizationId,
params: {
organizationId,
projectId,
targetId,
},
});
const validationResult = UpdatePreflightScriptModel.safeParse({ sourceCode: args.sourceCode });
if (validationResult.error) {
return {
error: {
message: validationResult.error.errors[0].message,
},
};
}
const currentUser = await this.session.getViewer();
const result = await this.pool.maybeOne(sql`/* createPreflightScript */
INSERT INTO "document_preflight_scripts" (
"source_code"
, "target_id"
, "created_by_user_id")
VALUES (
${validationResult.data.sourceCode}
, ${targetId}
, ${currentUser.id}
)
ON CONFLICT ("target_id")
DO UPDATE
SET
"source_code" = EXCLUDED."source_code"
, "updated_at" = NOW()
RETURNING
"id"
, "source_code" AS "sourceCode"
, "target_id" AS "targetId"
, "created_by_user_id" AS "createdByUserId"
, to_json("created_at") AS "createdAt"
, to_json("updated_at") AS "updatedAt"
`);
if (!result) {
return {
error: {
message: 'No preflight script found',
},
};
}
const { data: preflightScript, error } = PreflightScriptModel.safeParse(result);
if (error) {
return {
error: {
message: error.errors[0].message,
},
};
}
await this.auditLogs.record({
eventType: 'PREFLIGHT_SCRIPT_CHANGED',
organizationId,
metadata: {
scriptContents: preflightScript.sourceCode,
},
});
const updatedTarget = await this.storage.getTarget({
organizationId,
projectId,
targetId,
});
return {
ok: {
preflightScript,
updatedTarget,
},
};
}
}

View file

@ -0,0 +1,23 @@
import { MutationResolvers } from '../../../../__generated__/types';
import { PreflightScriptProvider } from '../../providers/preflight-script.provider';
export const updatePreflightScript: NonNullable<
MutationResolvers['updatePreflightScript']
> = async (_parent, args, { injector }) => {
const result = await injector.get(PreflightScriptProvider).updatePreflightScript({
selector: args.input.selector,
sourceCode: args.input.sourceCode,
});
if (result.error) {
return {
error: result.error,
ok: null,
};
}
return {
ok: result.ok,
error: null,
};
};

View file

@ -0,0 +1,16 @@
import type { TargetResolvers } from '../../../__generated__/types';
import { PreflightScriptProvider } from '../providers/preflight-script.provider';
/*
* Note: This object type is generated because "TargetMapper" is declared. This is to ensure runtime safety.
*
* When a mapper is used, it is possible to hit runtime errors in some scenarios:
* - given a field name, the schema type's field type does not match mapper's field type
* - or a schema type's field does not exist in the mapper's fields
*
* If you want to skip this file generation, remove the mapper or update the pattern in the `resolverGeneration.object` config.
*/
export const Target: Pick<TargetResolvers, 'preflightScript' | '__isTypeOf'> = {
preflightScript: (parent, _args, { injector }) =>
injector.get(PreflightScriptProvider).getPreflightScript(parent.id),
};

View file

@ -240,6 +240,15 @@ export interface DocumentCollection {
updatedAt: string;
}
export interface PreflightScript {
id: string;
sourceCode: string;
targetId: string;
createdByUserId: string | null;
createdAt: string;
updatedAt: string;
}
export type PaginatedDocumentCollections = Readonly<{
edges: ReadonlyArray<{
node: DocumentCollection;

View file

@ -136,6 +136,15 @@ export interface document_collections {
updated_at: Date;
}
export interface document_preflight_scripts {
created_at: Date;
created_by_user_id: string | null;
id: string;
source_code: string;
target_id: string;
updated_at: Date;
}
export interface migration {
date: Date;
hash: string;
@ -417,6 +426,7 @@ export interface DBTables {
contracts: contracts;
document_collection_documents: document_collection_documents;
document_collections: document_collections;
document_preflight_scripts: document_preflight_scripts;
migration: migration;
oidc_integrations: oidc_integrations;
organization_invitations: organization_invitations;

View file

@ -15,9 +15,9 @@
"@date-fns/utc": "2.1.0",
"@fastify/cors": "9.0.1",
"@fastify/static": "7.0.4",
"@fastify/vite": "6.0.7",
"@fastify/vite": "6.0.6",
"@graphiql/plugin-explorer": "4.0.0-alpha.2",
"@graphiql/react": "1.0.0-alpha.3",
"@graphiql/react": "1.0.0-alpha.4",
"@graphiql/toolkit": "0.9.1",
"@graphql-codegen/client-preset-swc-plugin": "0.2.0",
"@graphql-tools/mock": "9.0.6",
@ -66,6 +66,7 @@
"@theguild/editor": "1.2.5",
"@trpc/client": "10.45.2",
"@trpc/server": "10.45.2",
"@types/crypto-js": "^4.2.2",
"@types/dompurify": "3.2.0",
"@types/js-cookie": "3.0.6",
"@types/react": "18.3.18",
@ -81,6 +82,7 @@
"class-variance-authority": "0.7.1",
"clsx": "2.1.1",
"cmdk": "0.2.1",
"crypto-js": "^4.2.0",
"date-fns": "4.1.0",
"dompurify": "3.2.3",
"dotenv": "16.4.7",
@ -88,8 +90,8 @@
"echarts-for-react": "3.0.2",
"fastify": "4.29.0",
"formik": "2.4.6",
"framer-motion": "11.15.0",
"graphiql": "4.0.0-alpha.4",
"framer-motion": "11.11.17",
"graphiql": "4.0.0-alpha.5",
"graphql": "16.9.0",
"graphql-sse": "2.5.3",
"immer": "10.1.1",

View file

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="robots" content="noindex, nofollow" />
</head>
<body>
<a href="https://www.youtube.com/watch?v=CMNry4PE93Y" rel="nofollow">I like turtles</a>
<a href="https://www.youtube.com/watch?v=XOi2jFIhZhA" rel="nofollow">Wheatherboi</a>
<script type="module" src="./src/lib/preflight-sandbox/preflight-worker-embed.ts"></script>
</body>
</html>

View file

@ -104,6 +104,7 @@ export const TargetLaboratoryPageQuery = graphql(`
}
viewerCanViewLaboratory
viewerCanModifyLaboratory
...PreflightScript_TargetFragment
}
...Laboratory_IsCDNEnabledFragment
}
@ -123,11 +124,9 @@ export const operationCollectionsPlugin: GraphiQLPlugin = {
};
export function Content() {
const { organizationSlug, projectSlug, targetSlug } = useParams({ strict: false }) as {
organizationSlug: string;
projectSlug: string;
targetSlug: string;
};
const { organizationSlug, projectSlug, targetSlug } = useParams({
from: '/authenticated/$organizationSlug/$projectSlug/$targetSlug',
});
const [query] = useQuery({
query: TargetLaboratoryPageQuery,
variables: {

View file

@ -0,0 +1,55 @@
/**
* List all variables that we want to allow users to use inside their scripts
*
* initial list comes from https://github.com/postmanlabs/uniscope/blob/develop/lib/allowed-globals.js
*/
export const ALLOWED_GLOBALS = new Set([
'Array',
'Atomics',
'BigInt',
'Boolean',
'DataView',
'Date',
'Error',
'EvalError',
'Infinity',
'JSON',
'Map',
'Math',
'NaN',
'Number',
'Object',
'Promise',
'Proxy',
'RangeError',
'ReferenceError',
'Reflect',
'RegExp',
'Set',
'String',
'Symbol',
'SyntaxError',
'TypeError',
'URIError',
'WeakMap',
'WeakSet',
'decodeURI',
'decodeURIComponent',
'encodeURI',
'encodeURIComponent',
'escape',
'isFinite',
'isNaN',
'parseFloat',
'parseInt',
'undefined',
'unescape',
// More global variables
'btoa',
'atob',
'fetch',
'setTimeout',
// We aren't allowing access to window.console, but we need to "allow" it
// here so a second argument isn't added for it below.
'console',
]);

View file

@ -0,0 +1,672 @@
import {
ComponentPropsWithoutRef,
createContext,
useCallback,
useContext,
useEffect,
useRef,
useState,
} from 'react';
import { clsx } from 'clsx';
import type { editor } from 'monaco-editor';
import { useMutation } from 'urql';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { Subtitle, Title } from '@/components/ui/page';
import { Switch } from '@/components/ui/switch';
import { useToast } from '@/components/ui/use-toast';
import { FragmentType, graphql, useFragment } from '@/gql';
import { useLocalStorage, useToggle } from '@/lib/hooks';
import { GraphiQLPlugin } from '@graphiql/react';
import { Editor as MonacoEditor, OnMount } from '@monaco-editor/react';
import {
Cross2Icon,
CrossCircledIcon,
ExclamationTriangleIcon,
InfoCircledIcon,
Pencil1Icon,
TriangleRightIcon,
} from '@radix-ui/react-icons';
import { useParams } from '@tanstack/react-router';
import type { LogMessage } from './preflight-script-worker';
export const preflightScriptPlugin: GraphiQLPlugin = {
icon: () => (
<svg
viewBox="0 0 256 256"
stroke="currentColor"
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="16"
>
<path d="M136 160h40" />
<path d="m80 96 40 32-40 32" />
<rect width="192" height="160" x="32" y="48" rx="8.5" />
</svg>
),
title: 'Preflight Script',
content: PreflightScriptContent,
};
const classes = {
monaco: clsx('*:bg-[#10151f]'),
monacoMini: clsx('h-32 *:rounded-md *:bg-[#10151f]'),
icon: clsx('absolute -left-5 top-px'),
};
const sharedMonacoProps = {
theme: 'vs-dark',
className: classes.monaco,
options: {
minimap: { enabled: false },
padding: {
top: 10,
},
scrollbar: {
horizontalScrollbarSize: 6,
verticalScrollbarSize: 6,
},
},
} satisfies ComponentPropsWithoutRef<typeof MonacoEditor>;
const monacoProps = {
env: {
...sharedMonacoProps,
defaultLanguage: 'json',
options: {
...sharedMonacoProps.options,
lineNumbers: 'off',
tabSize: 2,
},
},
script: {
...sharedMonacoProps,
theme: 'vs-dark',
defaultLanguage: 'javascript',
options: {
...sharedMonacoProps.options,
},
},
} satisfies Record<'script' | 'env', ComponentPropsWithoutRef<typeof MonacoEditor>>;
type PayloadLog = { type: 'log'; log: string };
type PayloadError = { type: 'error'; error: Error };
type PayloadResult = { type: 'result'; environmentVariables: Record<string, unknown> };
type PayloadReady = { type: 'ready' };
type WorkerMessagePayload = PayloadResult | PayloadLog | PayloadError | PayloadReady;
const UpdatePreflightScriptMutation = graphql(`
mutation UpdatePreflightScript($input: UpdatePreflightScriptInput!) {
updatePreflightScript(input: $input) {
ok {
updatedTarget {
id
preflightScript {
id
sourceCode
}
}
}
error {
message
}
}
}
`);
const PreflightScript_TargetFragment = graphql(`
fragment PreflightScript_TargetFragment on Target {
id
preflightScript {
id
sourceCode
}
}
`);
type LogRecord = LogMessage | { type: 'separator' };
function safeParseJSON(str: string): Record<string, unknown> | null {
try {
return JSON.parse(str);
} catch {
return null;
}
}
const enum PreflightWorkerState {
running,
ready,
}
export function usePreflightScript(args: {
target: FragmentType<typeof PreflightScript_TargetFragment> | null;
}) {
const iframeRef = useRef<HTMLIFrameElement>(null);
const target = useFragment(PreflightScript_TargetFragment, args.target);
const [isPreflightScriptEnabled, setIsPreflightScriptEnabled] = useLocalStorage(
'hive:laboratory:isPreflightScriptEnabled',
false,
);
const [environmentVariables, setEnvironmentVariables] = useLocalStorage(
'hive:laboratory:environment',
'',
);
const latestEnvironmentVariablesRef = useRef(environmentVariables);
useEffect(() => {
latestEnvironmentVariablesRef.current = environmentVariables;
});
const [state, setState] = useState<PreflightWorkerState>(PreflightWorkerState.ready);
const [logs, setLogs] = useState<LogRecord[]>([]);
const currentRun = useRef<null | Function>(null);
async function execute(script = target?.preflightScript?.sourceCode ?? '', isPreview = false) {
if (isPreview === false && !isPreflightScriptEnabled) {
return safeParseJSON(latestEnvironmentVariablesRef.current);
}
const id = crypto.randomUUID();
setState(PreflightWorkerState.running);
const now = Date.now();
setLogs(prev => [...prev, '> Start running script']);
try {
const contentWindow = iframeRef.current?.contentWindow;
if (!contentWindow) {
throw new Error('Could not load iframe embed.');
}
contentWindow.postMessage(
{
type: 'run',
id,
script,
environmentVariables: (environmentVariables && safeParseJSON(environmentVariables)) || {},
},
'*',
);
const isFinishedD = Promise.withResolvers<void>();
// eslint-disable-next-line no-inner-declarations
function eventHandler(ev: MessageEvent<WorkerMessagePayload>) {
if (ev.data.type === 'result') {
const mergedEnvironmentVariables = JSON.stringify(
{
...safeParseJSON(latestEnvironmentVariablesRef.current),
...ev.data.environmentVariables,
},
null,
2,
);
setEnvironmentVariables(mergedEnvironmentVariables);
latestEnvironmentVariablesRef.current = mergedEnvironmentVariables;
setLogs(logs => [
...logs,
`> End running script. Done in ${(Date.now() - now) / 1000}s`,
{
type: 'separator' as const,
},
]);
isFinishedD.resolve();
return;
}
if (ev.data.type === 'error') {
const error = ev.data.error;
setLogs(logs => [
...logs,
error,
'> Preflight script failed',
{
type: 'separator' as const,
},
]);
isFinishedD.resolve();
return;
}
if (ev.data.type === 'log') {
const log = ev.data.log;
setLogs(logs => [...logs, log]);
return;
}
}
window.addEventListener('message', eventHandler);
currentRun.current = () => {
contentWindow.postMessage({
type: 'abort',
id,
});
currentRun.current = null;
};
await isFinishedD.promise;
window.removeEventListener('message', eventHandler);
setState(PreflightWorkerState.ready);
return safeParseJSON(latestEnvironmentVariablesRef.current);
} catch (err) {
if (err instanceof Error) {
setLogs(prev => [
...prev,
err,
'> Preflight script failed',
{
type: 'separator' as const,
},
]);
setState(PreflightWorkerState.ready);
return safeParseJSON(latestEnvironmentVariablesRef.current);
}
throw err;
}
}
function abort() {
currentRun.current?.();
}
// terminate worker when leaving laboratory
useEffect(
() => () => {
currentRun.current?.();
},
[],
);
return {
execute,
abort,
isPreflightScriptEnabled,
setIsPreflightScriptEnabled,
script: target?.preflightScript?.sourceCode ?? '',
environmentVariables,
setEnvironmentVariables,
state,
logs,
clearLogs: () => setLogs([]),
iframeElement: (
<iframe
src="/__preflight-embed"
title="preflight-worker"
className="hidden"
/**
* In DEV we need to use "allow-same-origin", as otherwise the embed can not instantiate the webworker (which is loaded from an URL).
* In PROD the webworker is not
*/
sandbox={import.meta.env.DEV ? 'allow-scripts allow-same-origin' : 'allow-scripts'}
ref={iframeRef}
/>
),
} as const;
}
type PreflightScriptObject = ReturnType<typeof usePreflightScript>;
const PreflightScriptContext = createContext<PreflightScriptObject | null>(null);
export const PreflightScriptProvider = PreflightScriptContext.Provider;
function PreflightScriptContent() {
const preflightScript = useContext(PreflightScriptContext);
if (preflightScript === null) {
throw new Error('PreflightScriptContent used outside PreflightScriptContext.Provider');
}
const [showModal, toggleShowModal] = useToggle();
const params = useParams({
from: '/authenticated/$organizationSlug/$projectSlug/$targetSlug',
});
const [, mutate] = useMutation(UpdatePreflightScriptMutation);
const { toast } = useToast();
const handleScriptChange = useCallback(async (newValue = '') => {
const { data, error } = await mutate({
input: {
selector: params,
sourceCode: newValue,
},
});
const err = error || data?.updatePreflightScript?.error;
if (err) {
toast({
title: 'Error',
description: err.message,
variant: 'destructive',
});
return;
}
toast({
title: 'Update',
description: 'Preflight script has been updated successfully',
variant: 'default',
});
}, []);
return (
<>
<PreflightScriptModal
// to unmount on submit/close
key={String(showModal)}
isOpen={showModal}
toggle={toggleShowModal}
scriptValue={preflightScript.script}
executeScript={value =>
preflightScript.execute(value, true).catch(err => {
console.error(err);
})
}
state={preflightScript.state}
abortScriptRun={preflightScript.abort}
logs={preflightScript.logs}
clearLogs={preflightScript.clearLogs}
onScriptValueChange={handleScriptChange}
envValue={preflightScript.environmentVariables}
onEnvValueChange={preflightScript.setEnvironmentVariables}
/>
<div className="graphiql-doc-explorer-title flex items-center justify-between gap-4">
Preflight Script
<Button
variant="orangeLink"
size="icon-sm"
className="size-auto gap-1"
onClick={toggleShowModal}
data-cy="preflight-script-modal-button"
>
<Pencil1Icon className="shrink-0" />
Edit
</Button>
</div>
<Subtitle>
This script is run before each operation submitted, e.g. for automated authentication.
</Subtitle>
<div className="flex items-center gap-2 text-sm">
<Switch
checked={preflightScript.isPreflightScriptEnabled}
onCheckedChange={v => preflightScript.setIsPreflightScriptEnabled(v)}
className="my-4"
data-cy="toggle-preflight-script"
/>
<span className="w-6">{preflightScript.isPreflightScriptEnabled ? 'ON' : 'OFF'}</span>
</div>
{preflightScript.isPreflightScriptEnabled && (
<MonacoEditor
height={128}
value={preflightScript.script}
{...monacoProps.script}
className={classes.monacoMini}
wrapperProps={{
['data-cy']: 'preflight-script-editor-mini',
}}
options={{
...monacoProps.script.options,
lineNumbers: 'off',
readOnly: true,
}}
/>
)}
<Title className="mt-6 flex items-center gap-2">
Environment variables{' '}
<Badge className="text-xs" variant="outline">
JSON
</Badge>
</Title>
<Subtitle>Define variables to use in your Headers</Subtitle>
<MonacoEditor
height={128}
value={preflightScript.environmentVariables}
onChange={value => preflightScript.setEnvironmentVariables(value ?? '')}
{...monacoProps.env}
className={classes.monacoMini}
wrapperProps={{
['data-cy']: 'env-editor-mini',
}}
/>
</>
);
}
function PreflightScriptModal({
isOpen,
toggle,
scriptValue,
executeScript,
state,
abortScriptRun,
logs,
clearLogs,
onScriptValueChange,
envValue,
onEnvValueChange,
}: {
isOpen: boolean;
toggle: () => void;
scriptValue?: string;
executeScript: (script: string) => void;
state: PreflightWorkerState;
abortScriptRun: () => void;
logs: Array<LogRecord>;
clearLogs: () => void;
onScriptValueChange: (value: string) => void;
envValue: string;
onEnvValueChange: (value: string) => void;
}) {
const scriptEditorRef = useRef<editor.IStandaloneCodeEditor | null>(null);
const envEditorRef = useRef<editor.IStandaloneCodeEditor | null>(null);
const consoleRef = useRef<HTMLElement>(null);
const handleScriptEditorDidMount: OnMount = useCallback(editor => {
scriptEditorRef.current = editor;
}, []);
const handleEnvEditorDidMount: OnMount = useCallback(editor => {
envEditorRef.current = editor;
}, []);
const handleSubmit = useCallback(() => {
onScriptValueChange(scriptEditorRef.current?.getValue() ?? '');
onEnvValueChange(envEditorRef.current?.getValue() ?? '');
toggle();
}, []);
useEffect(() => {
const consoleEl = consoleRef.current;
consoleEl?.scroll({ top: consoleEl.scrollHeight, behavior: 'smooth' });
}, [logs]);
return (
<Dialog
open={isOpen}
onOpenChange={open => {
if (!open) {
abortScriptRun();
}
toggle();
}}
>
<DialogContent
className="w-11/12 max-w-[unset] xl:w-4/5"
onEscapeKeyDown={ev => {
// prevent pressing escape in monaco to close the modal
if (ev.target instanceof HTMLTextAreaElement) {
ev.preventDefault();
}
}}
>
<DialogHeader>
<DialogTitle>Edit your Preflight Script</DialogTitle>
<DialogDescription>
This script will run in each user's browser and be stored in plain text on our servers.
Don't share any secrets here.
<br />
All team members can view the script and toggle it off when they need to.
</DialogDescription>
</DialogHeader>
<div className="grid h-[60vh] grid-cols-2 [&_section]:grow">
<div className="mr-4 flex flex-col">
<div className="flex justify-between p-2">
<Title className="flex gap-2">
Script Editor
<Badge className="text-xs" variant="outline">
JavaScript
</Badge>
</Title>
<Button
variant="orangeLink"
size="icon-sm"
className="size-auto gap-1"
onClick={() => {
if (state === PreflightWorkerState.running) {
abortScriptRun();
return;
}
executeScript(scriptEditorRef.current?.getValue() ?? '');
}}
data-cy="run-preflight-script"
>
{state === PreflightWorkerState.running && (
<>
<Cross2Icon className="shrink-0" />
Stop Script
</>
)}
{state === PreflightWorkerState.ready && (
<>
<TriangleRightIcon className="shrink-0" />
Run Script
</>
)}
</Button>
</div>
<MonacoEditor
value={scriptValue}
onMount={handleScriptEditorDidMount}
{...monacoProps.script}
options={{
...monacoProps.script.options,
wordWrap: 'wordWrapColumn',
}}
wrapperProps={{
['data-cy']: 'preflight-script-editor',
}}
/>
</div>
<div className="flex h-[inherit] flex-col">
<div className="flex justify-between p-2">
<Title>Console Output</Title>
<Button
variant="orangeLink"
size="icon-sm"
className="size-auto gap-1"
onClick={clearLogs}
disabled={state === PreflightWorkerState.running}
>
<Cross2Icon className="shrink-0" height="12" />
Clear Output
</Button>
</div>
<section
ref={consoleRef}
className='h-1/2 overflow-hidden overflow-y-scroll bg-[#10151f] py-2.5 pl-[26px] pr-2.5 font-[Menlo,Monaco,"Courier_New",monospace] text-xs/[18px]'
data-cy="console-output"
>
{logs.map((log, index) => {
let type = '';
if (log instanceof Error) {
type = 'error';
log = `${log.name}: ${log.message}`;
}
if (typeof log === 'string') {
type ||= log.split(':')[0].toLowerCase();
const ComponentToUse = {
error: CrossCircledIcon,
warn: ExclamationTriangleIcon,
info: InfoCircledIcon,
}[type];
return (
<div
key={index}
className={clsx(
'relative',
{
error: 'text-red-500',
warn: 'text-yellow-500',
info: 'text-green-500',
}[type],
)}
>
{ComponentToUse && <ComponentToUse className={classes.icon} />}
{log}
</div>
);
}
return <hr key={index} className="my-2 border-dashed border-current" />;
})}
</section>
<Title className="flex gap-2 p-2">
Environment Variables
<Badge className="text-xs" variant="outline">
JSON
</Badge>
</Title>
<MonacoEditor
value={envValue}
onChange={value => onEnvValueChange(value ?? '')}
onMount={handleEnvEditorDidMount}
{...monacoProps.env}
options={{
...monacoProps.env.options,
wordWrap: 'wordWrapColumn',
}}
wrapperProps={{
['data-cy']: 'env-editor',
}}
/>
</div>
</div>
<DialogFooter className="items-center">
<p className="me-5 flex items-center gap-2 text-sm">
<InfoCircledIcon />
Changes made to this Preflight Script will apply to all users on your team using this
target.
</p>
<Button type="button" onClick={toggle} data-cy="preflight-script-modal-cancel">
Close
</Button>
<Button
type="button"
variant="primary"
onClick={handleSubmit}
data-cy="preflight-script-modal-submit"
>
Save
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View file

@ -0,0 +1,29 @@
export type JSONPrimitive = boolean | null | string | number;
export type JSONObject = { [key: string]: JSONValue };
export type JSONValue = JSONPrimitive | JSONValue[] | JSONObject;
function isJSONValue(value: unknown): value is JSONValue {
return (
(Array.isArray(value) && value.every(isJSONValue)) ||
isJSONObject(value) ||
isJSONPrimitive(value)
);
}
export function isJSONObject(value: unknown): value is JSONObject {
return (
typeof value === 'object' &&
!!value &&
!Array.isArray(value) &&
Object.values(value).every(isJSONValue)
);
}
export function isJSONPrimitive(value: unknown): value is JSONPrimitive {
return (
typeof value === 'boolean' ||
typeof value === 'number' ||
typeof value === 'string' ||
value === null
);
}

View file

@ -0,0 +1,152 @@
import CryptoJS from 'crypto-js';
import CryptoJSPackageJson from 'crypto-js/package.json';
import { ALLOWED_GLOBALS } from './allowed-globals';
import { isJSONPrimitive, JSONPrimitive } from './json';
import { WorkerEvents } from './shared-types';
export type LogMessage = string | Error;
function sendMessage(data: WorkerEvents.Outgoing.EventData) {
postMessage(data);
}
self.onmessage = async event => {
await execute(event.data);
};
self.addEventListener('unhandledrejection', event => {
const error = 'reason' in event ? new Error(event.reason) : event;
sendMessage({ type: WorkerEvents.Outgoing.Event.error, error });
});
async function execute(args: {
environmentVariables: Record<string, JSONPrimitive>;
script: string;
}): Promise<void> {
const { environmentVariables, script } = args;
const inWorker = typeof WorkerGlobalScope !== 'undefined' && self instanceof WorkerGlobalScope;
// Confirm the build pipeline worked and this is running inside a worker and not the main thread
if (!inWorker) {
throw new Error(
'Preflight script must always be run in web workers, this is a problem with laboratory not user input',
);
}
// When running in worker `environmentVariables` will not be a reference to the main thread value
// but sometimes this will be tested outside the worker, so we don't want to mutate the input in that case
const workingEnvironmentVariables = { ...environmentVariables };
// generate list of all in scope variables, we do getOwnPropertyNames and `for in` because each contain slightly different sets of keys
const allGlobalKeys = Object.getOwnPropertyNames(globalThis);
for (const key in globalThis) {
allGlobalKeys.push(key);
}
// filter out allowed global variables and keys that will cause problems
const blockedGlobals = allGlobalKeys.filter(
key =>
// When testing in the main thread this exists on window and is not a valid argument name.
// because global is blocked, even if this was in the worker it's still wouldn't be available because it's not a valid variable name
!key.includes('-') &&
!ALLOWED_GLOBALS.has(key) &&
// window has references as indexes on the globalThis such as `globalThis[0]`, numbers are not valid arguments, so we need to filter these out
Number.isNaN(Number(key)) &&
// @ is not a valid argument name beginning character, so we don't need to block it and including it will cause a syntax error
// only example currently is @wry/context which is a dep of @apollo/client and adds @wry/context:Slot
key.charAt(0) !== '@',
);
// restrict window variable
blockedGlobals.push('window');
const log =
(level: 'log' | 'warn' | 'error' | 'info') =>
(...args: unknown[]) => {
console[level](...args);
let message = `${level.charAt(0).toUpperCase()}${level.slice(1)}: ${args.map(String).join(' ')}`;
message += appendLineAndColumn(new Error(), {
columnOffset: 'console.'.length,
});
// The messages should be streamed to the main thread as they occur not gathered and send to
// the main thread at the end of the execution of the preflight script
postMessage({ type: 'log', message });
};
function getValidEnvVariable(value: unknown) {
if (isJSONPrimitive(value)) {
return value;
}
consoleApi.warn(
'You tried to set a non primitive type in env variables, only string, boolean, number and null are allowed in env variables. The value has been filtered out.',
);
}
const consoleApi = Object.freeze({
log: log('log'),
info: log('info'),
warn: log('warn'),
error: log('error'),
});
let hasLoggedCryptoJSVersion = false;
const labApi = Object.freeze({
get CryptoJS() {
if (!hasLoggedCryptoJSVersion) {
hasLoggedCryptoJSVersion = true;
consoleApi.info(`Using crypto-js version: ${CryptoJSPackageJson.version}`);
}
return CryptoJS;
},
environment: {
get(key: string) {
return Object.freeze(workingEnvironmentVariables[key]);
},
set(key: string, value: unknown) {
const validValue = getValidEnvVariable(value);
if (validValue === undefined) {
delete workingEnvironmentVariables[key];
} else {
workingEnvironmentVariables[key] = validValue;
}
},
},
});
// Wrap the users script in an async IIFE to allow the use of top level await
const rawJs = `return(async()=>{'use strict';
${script}})()`;
try {
await Function(
'lab',
'console',
// spreading all the variables we want to block creates an argument that shadows their names, any attempt to access them will result in `undefined`
...blockedGlobals,
rawJs,
// Bind the function to a null constructor object to prevent `this` leaking scope in
).bind(
// When `this` is `undefined` or `null`, we get [object DedicatedWorkerGlobalScope] in console output
// instead we set as string `'undefined'` so in console, we'll see undefined as well
'undefined',
)(labApi, consoleApi);
} catch (error) {
if (error instanceof Error) {
error.message += appendLineAndColumn(error);
}
sendMessage({ type: WorkerEvents.Outgoing.Event.error, error: error as Error });
return;
}
sendMessage({
type: WorkerEvents.Outgoing.Event.result,
environmentVariables: workingEnvironmentVariables,
});
}
function appendLineAndColumn(error: Error, { columnOffset = 0 } = {}): string {
const regex = /<anonymous>:(?<line>\d+):(?<column>\d+)/; // Regex to match the line and column numbers
const { line, column } = error.stack?.match(regex)?.groups || {};
return ` (Line: ${Number(line) - 3}, Column: ${Number(column) - columnOffset})`;
}
sendMessage({ type: WorkerEvents.Outgoing.Event.ready });

View file

@ -0,0 +1,116 @@
import PreflightWorker from './preflight-script-worker?worker&inline';
import { IFrameEvents, WorkerEvents } from './shared-types';
function postMessage(data: IFrameEvents.Outgoing.EventData) {
window.parent.postMessage(data, '*');
}
const PREFLIGHT_TIMEOUT = 30_000;
const abortSignals = new Map<string, AbortController>();
window.addEventListener('message', (e: IFrameEvents.Incoming.MessageEvent) => {
console.log('received event', e.data);
if (e.data.type === IFrameEvents.Incoming.Event.run) {
handleRunEvent(e.data);
return;
}
if (e.data.type === IFrameEvents.Incoming.Event.abort) {
abortSignals.get(e.data.id)?.abort();
return;
}
});
postMessage({
type: IFrameEvents.Outgoing.Event.ready,
});
function handleRunEvent(data: IFrameEvents.Incoming.RunEventData) {
let worker: Worker;
function terminate() {
if (worker) {
worker.terminate();
}
abortSignals.delete(data.id);
}
const controller = new AbortController();
controller.signal.onabort = terminate;
abortSignals.set(data.id, controller);
try {
worker = new PreflightWorker();
const timeout = setTimeout(() => {
postMessage({
type: IFrameEvents.Outgoing.Event.error,
runId: data.id,
error: new Error(
`Preflight script execution timed out after ${PREFLIGHT_TIMEOUT / 1000} seconds`,
),
});
terminate();
}, PREFLIGHT_TIMEOUT);
worker.addEventListener(
'message',
function eventListener(ev: WorkerEvents.Outgoing.MessageEvent) {
console.log('received event from worker', ev.data);
if (ev.data.type === WorkerEvents.Outgoing.Event.ready) {
worker.postMessage({
script: data.script,
environmentVariables: data.environmentVariables,
} satisfies WorkerEvents.Incoming.MessageData);
return;
}
if (ev.data.type === WorkerEvents.Outgoing.Event.result) {
postMessage({
type: IFrameEvents.Outgoing.Event.result,
runId: data.id,
environmentVariables: ev.data.environmentVariables,
});
clearTimeout(timeout);
terminate();
return;
}
if (ev.data.type === WorkerEvents.Outgoing.Event.log) {
postMessage({
type: IFrameEvents.Outgoing.Event.log,
runId: data.id,
log: ev.data.message,
});
return;
}
if (ev.data.type === WorkerEvents.Outgoing.Event.error) {
postMessage({
type: IFrameEvents.Outgoing.Event.error,
runId: data.id,
error: ev.data.error,
});
clearTimeout(timeout);
terminate();
return;
}
},
);
postMessage({
type: IFrameEvents.Outgoing.Event.start,
runId: data.id,
});
} catch (error) {
console.error(error);
postMessage({
type: IFrameEvents.Outgoing.Event.error,
runId: data.id,
error: error as Error,
});
terminate();
}
}

View file

@ -0,0 +1,101 @@
/* eslint-disable @typescript-eslint/no-namespace */
type _MessageEvent<T> = MessageEvent<T>;
export namespace IFrameEvents {
export namespace Outgoing {
export const enum Event {
ready = 'ready',
start = 'start',
log = 'log',
result = 'result',
error = 'error',
}
type ReadyEventData = {
type: Event.ready;
};
type StartEventData = {
type: Event.start;
runId: string;
};
type LogEventData = {
type: Event.log;
runId: string;
log: string | Error;
};
type ResultEventData = {
type: Event.result;
runId: string;
environmentVariables: Record<string, unknown>;
};
type ErrorEventData = {
type: Event.error;
runId: string;
error: Error;
};
export type EventData =
| ReadyEventData
| StartEventData
| LogEventData
| ResultEventData
| ErrorEventData;
export type MessageEvent = _MessageEvent<EventData>;
}
export namespace Incoming {
export const enum Event {
run = 'run',
abort = 'abort',
}
export type RunEventData = {
type: Event.run;
id: string;
script: string;
environmentVariables: Record<string, unknown>;
};
type AbortEventData = {
type: Event.abort;
id: string;
script: string;
};
type EventData = RunEventData | AbortEventData;
export type MessageEvent = _MessageEvent<EventData>;
}
}
export namespace WorkerEvents {
export namespace Outgoing {
export const enum Event {
ready = 'ready',
log = 'log',
result = 'result',
error = 'error',
}
type LogEventData = { type: Event.log; message: string };
type ErrorEventData = { type: Event.error; error: Error };
type ResultEventData = { type: Event.result; environmentVariables: Record<string, unknown> };
type ReadyEventData = { type: Event.ready };
export type EventData = ResultEventData | LogEventData | ErrorEventData | ReadyEventData;
export type MessageEvent = _MessageEvent<EventData>;
}
export namespace Incoming {
export type MessageData = {
script: string;
environmentVariables: Record<string, unknown>;
};
}
}

View file

@ -33,6 +33,11 @@ import {
import { useSyncOperationState } from '@/lib/hooks/laboratory/use-sync-operation-state';
import { useOperationFromQueryString } from '@/lib/hooks/laboratory/useOperationFromQueryString';
import { useResetState } from '@/lib/hooks/use-reset-state';
import {
preflightScriptPlugin,
PreflightScriptProvider,
usePreflightScript,
} from '@/lib/preflight-sandbox/graphiql-plugin';
import { cn } from '@/lib/utils';
import { explorerPlugin } from '@graphiql/plugin-explorer';
import {
@ -52,7 +57,7 @@ import { useRedirect } from '@/lib/access/common';
const explorer = explorerPlugin();
// Declare outside components, otherwise while clicking on field in explorer operationCollectionsPlugin will be open
const plugins = [explorer, operationCollectionsPlugin];
const plugins = [explorer, operationCollectionsPlugin, preflightScriptPlugin];
function Share(): ReactElement | null {
const label = 'Share query';
@ -243,11 +248,30 @@ function Save(props: {
);
}
function substituteVariablesInHeader(
headers: Record<string, string>,
environmentVariables: Record<string, unknown>,
) {
return Object.fromEntries(
Object.entries(headers).map(([key, value]) => {
if (typeof value === 'string') {
// Replace all occurrences of `{{keyName}}` strings only if key exists in `environmentVariables`
value = value.replaceAll(/{{(?<keyName>.*?)}}/g, (originalString, envKey) => {
return Object.hasOwn(environmentVariables, envKey)
? (environmentVariables[envKey] as string)
: originalString;
});
}
return [key, value];
}),
);
}
function LaboratoryPageContent(props: {
organizationSlug: string;
projectSlug: string;
targetSlug: string;
selectedOperationId: string | undefined;
selectedOperationId?: string;
}) {
const [query] = useQuery({
query: TargetLaboratoryPageQuery,
@ -280,21 +304,58 @@ function LaboratoryPageContent(props: {
);
const mockEndpoint = `${location.origin}/api/lab/${props.organizationSlug}/${props.projectSlug}/${props.targetSlug}`;
const target = query.data?.target;
const preflightScript = usePreflightScript({ target: target ?? null });
const fetcher = useMemo<Fetcher>(() => {
return async (params, opts) => {
let headers = opts?.headers;
const url =
(actualSelectedApiEndpoint === 'linkedApi'
? query.data?.target?.graphqlEndpointUrl
: undefined) ?? mockEndpoint;
(actualSelectedApiEndpoint === 'linkedApi' ? target?.graphqlEndpointUrl : undefined) ??
mockEndpoint;
const _fetcher = createGraphiQLFetcher({ url, fetch });
return new Repeater(async (push, stop) => {
let hasFinishedPreflightScript = false;
// eslint-disable-next-line @typescript-eslint/no-floating-promises
stop.then(() => {
if (!hasFinishedPreflightScript) {
preflightScript.abort();
}
});
try {
const result = await preflightScript.execute();
if (result && headers) {
headers = substituteVariablesInHeader(headers, result);
}
} catch (err: unknown) {
if (err instanceof Error === false) {
throw err;
}
const formatError = JSON.stringify(
{
name: err.name,
message: err.message,
},
null,
2,
);
const error = new Error(`Error during preflight script execution:\n\n${formatError}`);
// We only want to expose the error message, not the whole stack trace.
delete error.stack;
stop(error);
return;
} finally {
hasFinishedPreflightScript = true;
}
const result = await _fetcher(params, opts);
const graphiqlFetcher = createGraphiQLFetcher({ url, fetch });
const result = await graphiqlFetcher(params, {
...opts,
headers,
});
// We only want to expose the error message, not the whole stack trace.
if (isAsyncIterable(result)) {
return new Repeater(async (push, stop) => {
if (isAsyncIterable(result)) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
stop.then(
() => 'return' in result && result.return instanceof Function && result.return(),
@ -306,15 +367,22 @@ function LaboratoryPageContent(props: {
stop();
} catch (err) {
const error = new Error(err instanceof Error ? err.message : 'Unexpected error.');
// We only want to expose the error message, not the whole stack trace.
delete error.stack;
stop(error);
return;
}
});
}
}
return result;
return result;
});
};
}, [query.data?.target?.graphqlEndpointUrl, actualSelectedApiEndpoint]);
}, [
target?.graphqlEndpointUrl,
actualSelectedApiEndpoint,
preflightScript.execute,
preflightScript.isPreflightScriptEnabled,
]);
const FullScreenIcon = isFullScreen ? ExitFullScreenIcon : EnterFullScreenIcon;
@ -335,8 +403,6 @@ function LaboratoryPageContent(props: {
[userOperations],
);
const target = query.data?.target;
useRedirect({
canAccess: target?.viewerCanViewLaboratory === true,
redirectTo: router => {
@ -368,6 +434,7 @@ function LaboratoryPageContent(props: {
return (
<>
{preflightScript.iframeElement}
<div className="flex py-6">
<div className="flex-1">
<Title>Laboratory</Title>
@ -448,7 +515,9 @@ function LaboratoryPageContent(props: {
.graphiql-dialog a {
--color-primary: 40, 89%, 60% !important;
}
.graphiql-container {
overflow: unset; /* remove default overflow */
}
.graphiql-container,
.graphiql-dialog,
.CodeMirror-info {
@ -464,49 +533,49 @@ function LaboratoryPageContent(props: {
}
`}</style>
</Helmet>
{!query.fetching && !query.stale && (
<GraphiQL
fetcher={fetcher}
showPersistHeadersSettings={false}
shouldPersistHeaders={false}
plugins={plugins}
visiblePlugin={operationCollectionsPlugin}
schema={schema}
forcedTheme="dark"
className={isFullScreen ? 'fixed inset-0 bg-[#030711]' : ''}
onTabChange={handleTabChange}
readOnly={!!props.selectedOperationId && target?.viewerCanModifyLaboratory === false}
>
<GraphiQL.Logo>
<Button
onClick={() => setIsFullScreen(prev => !prev)}
variant="orangeLink"
className="gap-2 whitespace-nowrap"
>
<FullScreenIcon className="size-4" />
{isFullScreen ? 'Exit' : 'Enter'} Full Screen
</Button>
</GraphiQL.Logo>
<GraphiQL.Toolbar>
{({ prettify }) => (
<>
{query.data?.target?.viewerCanModifyLaboratory && (
<Save
organizationSlug={props.organizationSlug}
projectSlug={props.projectSlug}
targetSlug={props.targetSlug}
/>
)}
<Share />
{/* if people have no modify access they should still be able to format their own queries. */}
{(query.data?.target?.viewerCanModifyLaboratory === true ||
!props.selectedOperationId) &&
prettify}
</>
)}
</GraphiQL.Toolbar>
</GraphiQL>
<PreflightScriptProvider value={preflightScript}>
<GraphiQL
fetcher={fetcher}
shouldPersistHeaders
plugins={plugins}
visiblePlugin={operationCollectionsPlugin}
schema={schema}
forcedTheme="dark"
className={isFullScreen ? 'fixed inset-0 bg-[#030711]' : ''}
onTabChange={handleTabChange}
readOnly={!!props.selectedOperationId && target?.viewerCanModifyLaboratory === false}
>
<GraphiQL.Logo>
<Button
onClick={() => setIsFullScreen(prev => !prev)}
variant="orangeLink"
className="gap-2 whitespace-nowrap"
>
<FullScreenIcon className="size-4" />
{isFullScreen ? 'Exit' : 'Enter'} Full Screen
</Button>
</GraphiQL.Logo>
<GraphiQL.Toolbar>
{({ prettify }) => (
<>
{query.data?.target?.viewerCanModifyLaboratory && (
<Save
organizationSlug={props.organizationSlug}
projectSlug={props.projectSlug}
targetSlug={props.targetSlug}
/>
)}
<Share />
{/* if people have no modify access they should still be able to format their own queries. */}
{(query.data?.target?.viewerCanModifyLaboratory === true ||
!props.selectedOperationId) &&
prettify}
</>
)}
</GraphiQL.Toolbar>
</GraphiQL>
</PreflightScriptProvider>
)}
<ConnectLabModal
endpoint={mockEndpoint}

View file

@ -22,6 +22,11 @@ const server = Fastify({
},
});
const preflightWorkerEmbed = {
path: '/__preflight-embed',
htmlFile: 'preflight-worker-embed.html',
};
async function main() {
/**
* Why is this necessary?
@ -35,6 +40,17 @@ async function main() {
server.log.info('Running in development mode');
// If in development mode, use Vite to serve the frontend and enable hot module reloading.
const { default: FastifyVite } = await import('@fastify/vite');
// This and a patch of @fastify/vite is necessary to serve the preflight worker embed html file.
// We need to know if the request is for the preflight worker embed or not to determine which html file to serve.
server.decorateRequest('viteHtmlFile', {
getter() {
return this.url.startsWith(preflightWorkerEmbed.path)
? preflightWorkerEmbed.htmlFile
: 'index.html';
},
});
await server.register(FastifyVite, {
// The root directory of @hive/app (where the package.json is located)
// /
@ -85,6 +101,18 @@ async function main() {
connectGithub(server);
connectLab(server);
server.get(preflightWorkerEmbed.path, (_req, reply) => {
if (isDev) {
// If in development mode, return the Vite preflight-worker-embed.html.
return reply.html();
}
// If in production mode, return the static html file.
return reply.sendFile(preflightWorkerEmbed.htmlFile, {
cacheControl: false,
});
});
server.get('*', (_req, reply) => {
if (isDev) {
// If in development mode, return the Vite index.html.

View file

@ -2,7 +2,7 @@
"extends": "../../../tsconfig.json",
"compilerOptions": {
"target": "esnext",
"lib": ["dom", "dom.iterable", "esnext"],
"lib": ["dom", "dom.iterable", "esnext", "webworker"],
"useDefineForClassFields": true,
"allowJs": true,
"strict": true,

View file

@ -1,3 +1,4 @@
import { resolve } from 'node:path';
import type { UserConfig } from 'vite';
import tsconfigPaths from 'vite-tsconfig-paths';
import react from '@vitejs/plugin-react';
@ -7,4 +8,15 @@ const __dirname = new URL('.', import.meta.url).pathname;
export default {
root: __dirname,
plugins: [tsconfigPaths(), react()],
build: {
rollupOptions: {
input: {
index: resolve(__dirname, 'index.html'),
['preflight-worker-embed']: resolve(__dirname, 'preflight-worker-embed.html'),
},
output: {
entryFileNames: '[name].js',
},
},
},
} satisfies UserConfig;

Binary file not shown.

After

Width:  |  Height:  |  Size: 397 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 269 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 309 KiB

View file

@ -1,20 +1,22 @@
import NextImage from 'next/image'
---
title: Overview
---
import { Callout } from '@theguild/components'
import labFormImage from '../../../../public/docs/pages/features/lab-form.png'
import labImage from '../../../../public/docs/pages/features/lab.png'
import { Screenshot } from '../../../../components/screenshot'
# Laboratory
Under your target page, you'll find the **Laboratory** page. The Laboratory allows you to explore
your GraphQL schema and run queries against a mocked version of your GraphQL service.
## Explore your GraphQL schema
## Explore your GraphQL Schema
You can use the full power of [GraphiQL](https://github.com/graphql/graphiql) directly within Hive:
compose your GraphQL operations, explore with different field and variations, and access your
GraphQL schema full documentation.
<NextImage alt="Lab" src={labImage} className="mt-6 max-w-3xl rounded-lg drop-shadow-md" />
<Screenshot>![Laboratory](/docs/pages/features/lab.png)</Screenshot>
## Link a Laboratory Endpoint
@ -49,7 +51,7 @@ To get started with using the Laboratory mock schema externally, create a
Now, click on the **Use Schema Externally** button on the Laboratory page, and follow the
instructions on the form:
<NextImage alt="Lab Form" src={labFormImage} className="mt-6 max-w-xl rounded-lg drop-shadow-md" />
<Screenshot>![Laboratory Form](/docs/pages/features/lab-form.png)</Screenshot>
To test access to your setup, try running a `curl` command to run a simple GraphQL query against
your mocked schema:
@ -60,7 +62,7 @@ curl -X POST -H "X-Hive-Key: HIVE_TOKEN_HERE" -H "Content-Type: application/json
--data-raw '{"query": "{ __typename }"}'
```
### With GraphQL-Code-Generator
### With GraphQL Code Generator
<Callout>
We recommend using the CDN for consuming the GraphQL schema in your project. [See GraphQL Code

Binary file not shown.

After

Width:  |  Height:  |  Size: 476 KiB

View file

@ -0,0 +1,99 @@
---
description:
Useful for handling authentication flows like OAuth, where you may need to refresh an access token
---
# Preflight Scripts
import { Callout } from '@theguild/components'
import { Screenshot } from '../../../../components/screenshot'
export const figcaptionClass = 'text-center text-sm mt-2'
These scripts allow you to automatically run custom authentication processes before executing your
GraphQL operations. They're especially useful for handling authentication flows like OAuth, where
you may need to refresh an access token. Let's explore how it works.
## Configuring Preflight Script
To create a script click on the command line icon (right after Operation Collections plugin icon) in
GraphiQL sidebar section.
<figure className="mt-6">
<Screenshot>![Command line icon](./command-line-icon.png)</Screenshot>
{/* prettier-ignore */}
<figcaption className={figcaptionClass}>The preflight script is accessible by clicking on the Command line icon in the GraphiQL sidebar</figcaption>
</figure>
You will see Script editor (JavaScript language) which is read-only and present for a quick view of
your saved script and Environment variables editor (JSON language) which is persistent in
localStorage.
<figure className="mt-6">
<Screenshot>![](./preflight-script-plugin.png)</Screenshot>
<figcaption className={figcaptionClass}>Preflight script plugin view</figcaption>
</figure>
## Editing Preflight Script
Clicking on the `Edit` button will open Modal where you can edit, test and save your script in
database.
<Callout type="warning">
**Note**: Your script will stored as plain text in our database, don't put any secrets there, use
Environment variables editor for it! The preflight script is accessible to all members of your
organization, but only users with access to target Settings can edit the script code.
</Callout>
You can use any JavaScript syntax (including top-level `await`) in the Script editor. Getting and
Setting environment variables is done by accessing the `environment` property on the `lab` global
variable.
```js
// get myKey variable from the Environment variables editor
lab.environment.get('myKey')
// set myKey variable to the Environment variables editor (persistent in localStorage)
lab.environment.set('myKey', myValue)
```
<figure className="mt-6">
<Screenshot>![](./get-and-set-env-vars.png)</Screenshot>
<figcaption className={figcaptionClass}>Demo how to get and set environment variables</figcaption>
</figure>
## CryptoJS
Additionally, you can access [the CryptoJS library](https://github.com/brix/crypto-js) by accessing
the `CryptoJS` property on the `lab` global variable.
<figure className="mt-6">
<Screenshot>![](./crypto-js.png)</Screenshot>
<figcaption className={figcaptionClass}>CryptoJS</figcaption>
</figure>
## Global Variables and Errors
Access to global variables such as `this`, `window` or `globalThis` is restricted. Errors thrown by
the script will be displayed in Console Output.
<figure className="mt-6">
<Screenshot>![](./unable-to-access-global-variables.png)</Screenshot>
<figcaption className={figcaptionClass}>Demo restricted access to global variables</figcaption>
</figure>
## Using Environment Variables
To use your environment variables in GraphiQL headers editor wraps environment keys with
double-curly braces, e.g.:
```json filename="Headers" /{{myEnvVar}}/
{
"Authorization": "Bearer {{myEnvVar}}"
}
```
<figure className="mt-6">
<Screenshot>![Replace syntax in action](./replace-syntax.png)</Screenshot>
{/* prettier-ignore */}
<figcaption className={figcaptionClass}>Replace syntax is done via double open/closed curly braces, e.g. `{{ myEnvVar }}`</figcaption>
</figure>

Binary file not shown.

After

Width:  |  Height:  |  Size: 334 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 278 KiB

View file

@ -0,0 +1,24 @@
---
title: Preflight Scripts in the Laboratory
description:
Populate headers with environment variables and automate authentication flows for GraphQL
requests.
date: 2024-12-27
authors: [dimitri]
---
import { Callout } from '@theguild/components'
export const figcaptionClass = 'text-center text-sm mt-2'
We've added Preflight Scripts to Laboratory! These scripts allow you to automatically run custom
authentication processes before executing your GraphQL operations. They're especially useful for
handling authentication flows like OAuth, where you may need to refresh an access token.
<figure className="mt-6">
<>![](private-next-pages/docs/dashboard/laboratory/get-and-set-env-vars.png)</>
<figcaption className={figcaptionClass}>Demo of Preflight Scripts</figcaption>
</figure>
Check out [the documentation on Preflight Scripts](/docs/dashboard/laboratory/preflight-scripts) for
information on how to configure, edit, and use them.

View file

@ -0,0 +1,17 @@
diff --git a/mode/development.js b/mode/development.js
index af9de9d75a3689cd4f4b5d2876f2e38bd2674ae4..94ecb29a8e0d2615b1ecd0114dba7f3979dc2b11 100644
--- a/mode/development.js
+++ b/mode/development.js
@@ -79,7 +79,11 @@ async function setup(config) {
}
}
}
- const indexHtmlPath = join(config.vite.root, 'index.html')
+
+ // Request is decorated with viteHtmlFile in: packages/web/app/src/server/index.ts
+ // It is used to render more than one html file
+ const htmlFileName = req.viteHtmlFile ?? 'index.html';
+ const indexHtmlPath = join(config.vite.root,htmlFileName)
const indexHtml = await read(indexHtmlPath, 'utf8')
const transformedHtml = await this.devServer.transformIndexHtml(
req.url,

View file

@ -0,0 +1,423 @@
diff --git a/dist/index.mjs b/dist/index.mjs
index 8ca339a2ba2031f0c1e22f1d099fa9a571492107..1cf3e8c620dc2c3ad4cfc42e2feeb4ca4682163c 100644
--- a/dist/index.mjs
+++ b/dist/index.mjs
@@ -1,6 +1,6 @@
import { jsx, jsxs, Fragment } from "react/jsx-runtime";
import * as React from "react";
-import { createContext, useContext, useRef, useState, useEffect, forwardRef, useCallback, useMemo, useLayoutEffect } from "react";
+import { createContext, useContext, useRef, useState, useEffect, forwardRef, useMemo, useCallback, useLayoutEffect } from "react";
import { clsx } from "clsx";
import { print, astFromValue, isSchema, buildClientSchema, validateSchema, getIntrospectionQuery, isNamedType, isObjectType, isInputObjectType, isScalarType, isEnumType, isInterfaceType, isUnionType, isNonNullType, isListType, isAbstractType, isType, parse, visit } from "graphql";
import { StorageAPI, HistoryStore, formatResult, isObservable, formatError, isAsyncIterable, fetcherReturnToPromise, isPromise, mergeAst, fillLeafs, getSelectedOperationName } from "@graphiql/toolkit";
@@ -40,7 +40,7 @@ function createContextHook(context) {
const StorageContext = createNullableContext("StorageContext");
function StorageContextProvider(props) {
const isInitialRender = useRef(true);
- const [storage, setStorage] = useState(new StorageAPI(props.storage));
+ const [storage, setStorage] = useState(() => new StorageAPI(props.storage));
useEffect(() => {
if (isInitialRender.current) {
isInitialRender.current = false;
@@ -465,68 +465,42 @@ const Tooltip = Object.assign(TooltipRoot, {
Provider: T.Provider
});
const HistoryContext = createNullableContext("HistoryContext");
-function HistoryContextProvider(props) {
- var _a;
+function HistoryContextProvider({
+ maxHistoryLength = DEFAULT_HISTORY_LENGTH,
+ children
+}) {
const storage = useStorageContext();
- const historyStore = useRef(
- new HistoryStore(
+ const [historyStore] = useState(
+ () => (
// Fall back to a noop storage when the StorageContext is empty
- storage || new StorageAPI(null),
- props.maxHistoryLength || DEFAULT_HISTORY_LENGTH
+ new HistoryStore(storage || new StorageAPI(null), maxHistoryLength)
)
);
- const [items, setItems] = useState(((_a = historyStore.current) == null ? void 0 : _a.queries) || []);
- const addToHistory = useCallback(
- (operation) => {
- var _a2;
- (_a2 = historyStore.current) == null ? void 0 : _a2.updateHistory(operation);
- setItems(historyStore.current.queries);
- },
- []
- );
- const editLabel = useCallback(
- (operation, index) => {
- historyStore.current.editLabel(operation, index);
- setItems(historyStore.current.queries);
- },
- []
- );
- const toggleFavorite = useCallback(
- (operation) => {
- historyStore.current.toggleFavorite(operation);
- setItems(historyStore.current.queries);
- },
- []
- );
- const setActive = useCallback(
- (item) => {
- return item;
- },
- []
- );
- const deleteFromHistory = useCallback((item, clearFavorites = false) => {
- historyStore.current.deleteHistory(item, clearFavorites);
- setItems(historyStore.current.queries);
- }, []);
+ const [items, setItems] = useState(() => historyStore.queries || []);
const value = useMemo(
() => ({
- addToHistory,
- editLabel,
+ addToHistory(operation) {
+ historyStore.updateHistory(operation);
+ setItems(historyStore.queries);
+ },
+ editLabel(operation, index) {
+ historyStore.editLabel(operation, index);
+ setItems(historyStore.queries);
+ },
items,
- toggleFavorite,
- setActive,
- deleteFromHistory
+ toggleFavorite(operation) {
+ historyStore.toggleFavorite(operation);
+ setItems(historyStore.queries);
+ },
+ setActive: (item) => item,
+ deleteFromHistory(item, clearFavorites) {
+ historyStore.deleteHistory(item, clearFavorites);
+ setItems(historyStore.queries);
+ }
}),
- [
- addToHistory,
- editLabel,
- items,
- toggleFavorite,
- setActive,
- deleteFromHistory
- ]
+ [items, historyStore]
);
- return /* @__PURE__ */ jsx(HistoryContext.Provider, { value, children: props.children });
+ return /* @__PURE__ */ jsx(HistoryContext.Provider, { value, children });
}
const useHistoryContext = createContextHook(HistoryContext);
const DEFAULT_HISTORY_LENGTH = 20;
@@ -714,7 +688,8 @@ function ExecutionContextProvider({
fetcher,
getDefaultFieldNames,
children,
- operationName
+ operationName,
+ onModifyHeaders
}) {
if (!fetcher) {
throw new TypeError(
@@ -792,6 +767,9 @@ function ExecutionContextProvider({
}
setResponse("");
setIsFetching(true);
+ if (onModifyHeaders) {
+ headers = await onModifyHeaders(headers);
+ }
const opName = operationName ?? queryEditor.operationName ?? void 0;
history == null ? void 0 : history.addToHistory({
query,
@@ -999,9 +977,9 @@ function mergeIncrementalResult(executionResult, incrementalResult) {
}
}
}
+const isMacOs = typeof navigator !== "undefined" && navigator.userAgent.includes("Mac");
const DEFAULT_EDITOR_THEME = "graphiql";
const DEFAULT_KEY_MAP = "sublime";
-const isMacOs = typeof navigator !== "undefined" && navigator.platform.toLowerCase().indexOf("mac") === 0;
const commonKeys = {
// Persistent search box in Query Editor
[isMacOs ? "Cmd-F" : "Ctrl-F"]: "findPersistent",
@@ -1599,7 +1577,7 @@ function Search() {
onFocus: handleFocus,
onBlur: handleFocus,
onChange: (event) => setSearchValue(event.target.value),
- placeholder: "⌘ K",
+ placeholder: `${isMacOs ? "⌘" : "Ctrl"} K`,
ref: inputRef,
value: searchValue,
"data-cy": "doc-explorer-input"
@@ -3063,14 +3041,16 @@ function useSetEditorValues({
);
}
function createTab({
+ id,
+ title,
query = null,
variables = null,
headers = null
-} = {}) {
+}) {
return {
- id: guid(),
+ id: id || guid(),
hash: hashFromTabContents({ query, variables, headers }),
- title: query && fuzzyExtractOperationName(query) || DEFAULT_TITLE,
+ title: title || query && fuzzyExtractOperationName(query) || DEFAULT_TITLE,
query,
variables,
headers,
@@ -3088,8 +3068,7 @@ function setPropertiesInActiveTab(state, partialTab) {
const newTab = { ...tab, ...partialTab };
return {
...newTab,
- hash: hashFromTabContents(newTab),
- title: newTab.operationName || (newTab.query ? fuzzyExtractOperationName(newTab.query) : void 0) || DEFAULT_TITLE
+ hash: hashFromTabContents(newTab)
};
})
};
@@ -3311,32 +3290,36 @@ function EditorContextProvider(props) {
responseEditor,
defaultHeaders
});
- const addTab = useCallback(() => {
- setTabState((current) => {
- const updatedValues = synchronizeActiveTabValues(current);
- const updated = {
- tabs: [
- ...updatedValues.tabs,
- createTab({
- headers: defaultHeaders,
- query: defaultQuery ?? DEFAULT_QUERY
- })
- ],
- activeTabIndex: updatedValues.tabs.length
- };
- storeTabs(updated);
- setEditorValues(updated.tabs[updated.activeTabIndex]);
- onTabChange == null ? void 0 : onTabChange(updated);
- return updated;
- });
- }, [
- defaultHeaders,
- defaultQuery,
- onTabChange,
- setEditorValues,
- storeTabs,
- synchronizeActiveTabValues
- ]);
+ const addTab = useCallback(
+ (_tabState) => {
+ setTabState((current) => {
+ const updatedValues = synchronizeActiveTabValues(current);
+ const updated = {
+ tabs: [
+ ...updatedValues.tabs,
+ createTab({
+ ..._tabState,
+ headers: defaultHeaders,
+ query: defaultQuery ?? DEFAULT_QUERY
+ })
+ ],
+ activeTabIndex: updatedValues.tabs.length
+ };
+ storeTabs(updated);
+ setEditorValues(updated.tabs[updated.activeTabIndex]);
+ onTabChange == null ? void 0 : onTabChange(updated);
+ return updated;
+ });
+ },
+ [
+ defaultHeaders,
+ defaultQuery,
+ onTabChange,
+ setEditorValues,
+ storeTabs,
+ synchronizeActiveTabValues
+ ]
+ );
const changeTab = useCallback(
(index) => {
setTabState((current) => {
@@ -3432,6 +3415,7 @@ function EditorContextProvider(props) {
const value = useMemo(
() => ({
...tabState,
+ setTabState,
addTab,
changeTab,
moveTab,
@@ -3743,9 +3727,10 @@ function GraphiQLProvider({
storage,
validationRules,
variables,
- visiblePlugin
+ visiblePlugin,
+ onModifyHeaders
}) {
- return /* @__PURE__ */ jsx(StorageContextProvider, { storage, children: /* @__PURE__ */ jsx(HistoryContextProvider, { maxHistoryLength, children: /* @__PURE__ */ jsx(
+ return /* @__PURE__ */ jsx(StorageContextProvider, { storage, children: /* @__PURE__ */ jsx(
EditorContextProvider,
{
defaultQuery,
@@ -3776,6 +3761,7 @@ function GraphiQLProvider({
getDefaultFieldNames,
fetcher,
operationName,
+ onModifyHeaders,
children: /* @__PURE__ */ jsx(ExplorerContextProvider, { children: /* @__PURE__ */ jsx(
PluginContextProvider,
{
@@ -3790,7 +3776,7 @@ function GraphiQLProvider({
}
)
}
- ) }) });
+ ) });
}
function useTheme(defaultTheme = null) {
const storageContext = useStorageContext();
@@ -4200,6 +4186,7 @@ export {
TypeLink,
UnStyledButton,
VariableEditor,
+ isMacOs,
useAutoCompleteLeafs,
useCopyQuery,
useDragResize,
diff --git a/dist/types/editor/context.d.ts b/dist/types/editor/context.d.ts
index 199db8a294f8132d46470498870adbdf9fdc83af..d8901fe0d50db17db36a502dcf69d5f69efb84a1 100644
--- a/dist/types/editor/context.d.ts
+++ b/dist/types/editor/context.d.ts
@@ -1,6 +1,6 @@
import { DocumentNode, FragmentDefinitionNode, OperationDefinitionNode, ValidationRule } from 'graphql';
import { VariableToType } from 'graphql-language-service';
-import { ReactNode } from 'react';
+import { Dispatch, ReactNode, SetStateAction } from 'react';
import { TabDefinition, TabsState, TabState } from './tabs';
import { CodeMirrorEditor } from './types';
export declare type CodeMirrorEditorWithOperationFacts = CodeMirrorEditor & {
@@ -10,10 +10,11 @@ export declare type CodeMirrorEditorWithOperationFacts = CodeMirrorEditor & {
variableToType: VariableToType | null;
};
export declare type EditorContextType = TabsState & {
+ setTabState: Dispatch<SetStateAction<TabsState>>;
/**
* Add a new tab.
*/
- addTab(): void;
+ addTab(tabState?: Pick<TabState, 'id' | 'query' | 'variables' | 'headers' | 'title'>): void;
/**
* Switch to a different tab.
* @param index The index of the tab that should be switched to.
@@ -38,7 +39,7 @@ export declare type EditorContextType = TabsState & {
* @param partialTab A partial tab state object that will override the
* current values. The properties `id`, `hash` and `title` cannot be changed.
*/
- updateActiveTabValues(partialTab: Partial<Omit<TabState, 'id' | 'hash' | 'title'>>): void;
+ updateActiveTabValues(partialTab: Partial<Omit<TabState, 'hash'>>): void;
/**
* The CodeMirror editor instance for the headers editor.
*/
diff --git a/dist/types/editor/tabs.d.ts b/dist/types/editor/tabs.d.ts
index 28704a9c1c6e22fa75986de8591759e13035c8c5..5204d2b25198f89da9bba70804656f02799c7df6 100644
--- a/dist/types/editor/tabs.d.ts
+++ b/dist/types/editor/tabs.d.ts
@@ -90,7 +90,7 @@ export declare function useSetEditorValues({ queryEditor, variableEditor, header
headers?: string | null | undefined;
response: string | null;
}) => void;
-export declare function createTab({ query, variables, headers, }?: Partial<TabDefinition>): TabState;
+export declare function createTab({ id, title, query, variables, headers, }: Partial<TabDefinition & Pick<TabState, 'id' | 'title'>>): TabState;
export declare function setPropertiesInActiveTab(state: TabsState, partialTab: Partial<Omit<TabState, 'id' | 'hash' | 'title'>>): TabsState;
export declare function fuzzyExtractOperationName(str: string): string | null;
export declare function clearHeadersFromTabs(storage: StorageAPI | null): void;
diff --git a/dist/types/execution.d.ts b/dist/types/execution.d.ts
index 2d458001265d925ed0323a10aecbefdb7e6d0b4e..eb024cf197f13bfaa67423f5751c7cad7d0664bc 100644
--- a/dist/types/execution.d.ts
+++ b/dist/types/execution.d.ts
@@ -1,4 +1,4 @@
-import { Fetcher } from '@graphiql/toolkit';
+import { Fetcher, MaybePromise } from '@graphiql/toolkit';
import { ReactNode } from 'react';
import { UseAutoCompleteLeafsArgs } from './editor/hooks';
export declare type ExecutionContextType = {
@@ -45,8 +45,13 @@ export declare type ExecutionContextProviderProps = Pick<UseAutoCompleteLeafsArg
* This prop sets the operation name that is passed with a GraphQL request.
*/
operationName?: string;
+ /**
+ * Modify headers before execution
+ * e.g. for interpolating headers values `"myKey": "{{valueToInterpolate}}"`
+ */
+ onModifyHeaders?: (headers?: Record<string, unknown>) => MaybePromise<Record<string, unknown>>;
};
-export declare function ExecutionContextProvider({ fetcher, getDefaultFieldNames, children, operationName, }: ExecutionContextProviderProps): import("react/jsx-runtime").JSX.Element;
+export declare function ExecutionContextProvider({ fetcher, getDefaultFieldNames, children, operationName, onModifyHeaders, }: ExecutionContextProviderProps): import("react/jsx-runtime").JSX.Element;
export declare const useExecutionContext: {
(options: {
nonNull: true;
diff --git a/dist/types/history/context.d.ts b/dist/types/history/context.d.ts
index f2699b344d27806094c0e5d62d914e5618dcf4db..9e6e3c6cdfded41af49c4c15c8d0be100e896bb0 100644
--- a/dist/types/history/context.d.ts
+++ b/dist/types/history/context.d.ts
@@ -76,7 +76,7 @@ export declare type HistoryContextProviderProps = {
* any additional props they added for their needs (i.e., build their own functions that may save
* to a backend instead of localStorage and might need an id property added to the QueryStoreItem)
*/
-export declare function HistoryContextProvider(props: HistoryContextProviderProps): import("react/jsx-runtime").JSX.Element;
+export declare function HistoryContextProvider({ maxHistoryLength, children, }: HistoryContextProviderProps): import("react/jsx-runtime").JSX.Element;
export declare const useHistoryContext: {
(options: {
nonNull: true;
diff --git a/dist/types/index.d.ts b/dist/types/index.d.ts
index 26ef2a2a07dcdf29f868067d32a0f5ff7981d8e6..28d9620636bab2221239ab8b87505425a0468b5f 100644
--- a/dist/types/index.d.ts
+++ b/dist/types/index.d.ts
@@ -8,6 +8,7 @@ export { SchemaContext, SchemaContextProvider, useSchemaContext, } from './schem
export { StorageContext, StorageContextProvider, useStorageContext, } from './storage';
export { useTheme } from './theme';
export { useDragResize } from './utility/resize';
+export { isMacOs } from './utility/is-macos';
export * from './icons';
export * from './ui';
export * from './toolbar';
diff --git a/dist/types/provider.d.ts b/dist/types/provider.d.ts
index e95c73f0b8c7cdfaece528e5f411ffd29862d490..d0d1e80a13da5d22abbcb4d6e052e91323fcc86f 100644
--- a/dist/types/provider.d.ts
+++ b/dist/types/provider.d.ts
@@ -6,4 +6,4 @@ import { PluginContextProviderProps } from './plugin';
import { SchemaContextProviderProps } from './schema';
import { StorageContextProviderProps } from './storage';
export declare type GraphiQLProviderProps = EditorContextProviderProps & ExecutionContextProviderProps & ExplorerContextProviderProps & HistoryContextProviderProps & PluginContextProviderProps & SchemaContextProviderProps & StorageContextProviderProps;
-export declare function GraphiQLProvider({ children, dangerouslyAssumeSchemaIsValid, defaultQuery, defaultHeaders, defaultTabs, externalFragments, fetcher, getDefaultFieldNames, headers, inputValueDeprecation, introspectionQueryName, maxHistoryLength, onEditOperationName, onSchemaChange, onTabChange, onTogglePluginVisibility, operationName, plugins, query, response, schema, schemaDescription, shouldPersistHeaders, storage, validationRules, variables, visiblePlugin, }: GraphiQLProviderProps): import("react/jsx-runtime").JSX.Element;
+export declare function GraphiQLProvider({ children, dangerouslyAssumeSchemaIsValid, defaultQuery, defaultHeaders, defaultTabs, externalFragments, fetcher, getDefaultFieldNames, headers, inputValueDeprecation, introspectionQueryName, maxHistoryLength, onEditOperationName, onSchemaChange, onTabChange, onTogglePluginVisibility, operationName, plugins, query, response, schema, schemaDescription, shouldPersistHeaders, storage, validationRules, variables, visiblePlugin, onModifyHeaders, }: GraphiQLProviderProps): import("react/jsx-runtime").JSX.Element;
diff --git a/dist/types/storage.d.ts b/dist/types/storage.d.ts
index c4c98ab5c3cd32837109d9d20d4808ad6793fd3f..0a1257b6e041d42068bffb5f332855372b89ea88 100644
--- a/dist/types/storage.d.ts
+++ b/dist/types/storage.d.ts
@@ -6,7 +6,7 @@ export declare type StorageContextProviderProps = {
children: ReactNode;
/**
* Provide a custom storage API.
- * @default `localStorage``
+ * @default `localStorage`
* @see {@link https://graphiql-test.netlify.app/typedoc/modules/graphiql_toolkit.html#storage-2|API docs}
* for details on the required interface.
*/
diff --git a/dist/types/utility/is-macos.d.ts b/dist/types/utility/is-macos.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..5f05699dde4723cbd446e914900dd9e7ff41ae70
--- /dev/null
+++ b/dist/types/utility/is-macos.d.ts
@@ -0,0 +1 @@
+export declare const isMacOs: boolean;

View file

@ -1,140 +0,0 @@
diff --git a/dist/index.mjs b/dist/index.mjs
index 567480ba5ccc30619db2eb8e6da8d618ddb5e891..c49c573893a26897062cda2af9896bc69881db2a 100644
--- a/dist/index.mjs
+++ b/dist/index.mjs
@@ -3056,14 +3056,16 @@ function useSetEditorValues({
);
}
function createTab({
+ id,
+ title,
query = null,
variables = null,
headers = null
-} = {}) {
+}) {
return {
- id: guid(),
+ id: id || guid(),
hash: hashFromTabContents({ query, variables, headers }),
- title: query && fuzzyExtractOperationName(query) || DEFAULT_TITLE,
+ title: title || query && fuzzyExtractOperationName(query) || DEFAULT_TITLE,
query,
variables,
headers,
@@ -3081,8 +3083,7 @@ function setPropertiesInActiveTab(state, partialTab) {
const newTab = { ...tab, ...partialTab };
return {
...newTab,
- hash: hashFromTabContents(newTab),
- title: newTab.operationName || (newTab.query ? fuzzyExtractOperationName(newTab.query) : void 0) || DEFAULT_TITLE
+ hash: hashFromTabContents(newTab)
};
})
};
@@ -3304,25 +3305,31 @@ function EditorContextProvider(props) {
responseEditor,
defaultHeaders
});
- const addTab = useCallback(() => {
- setTabState((current) => {
- const updatedValues = synchronizeActiveTabValues(current);
- const updated = {
- tabs: [...updatedValues.tabs, createTab({ headers: defaultHeaders })],
- activeTabIndex: updatedValues.tabs.length
- };
- storeTabs(updated);
- setEditorValues(updated.tabs[updated.activeTabIndex]);
- onTabChange == null ? void 0 : onTabChange(updated);
- return updated;
- });
- }, [
- defaultHeaders,
- onTabChange,
- setEditorValues,
- storeTabs,
- synchronizeActiveTabValues
- ]);
+ const addTab = useCallback(
+ (_tabState) => {
+ setTabState((current) => {
+ const updatedValues = synchronizeActiveTabValues(current);
+ const updated = {
+ tabs: [
+ ...updatedValues.tabs,
+ createTab({ ..._tabState, headers: defaultHeaders })
+ ],
+ activeTabIndex: updatedValues.tabs.length
+ };
+ storeTabs(updated);
+ setEditorValues(updated.tabs[updated.activeTabIndex]);
+ onTabChange == null ? void 0 : onTabChange(updated);
+ return updated;
+ });
+ },
+ [
+ defaultHeaders,
+ onTabChange,
+ setEditorValues,
+ storeTabs,
+ synchronizeActiveTabValues
+ ]
+ );
const changeTab = useCallback(
(index) => {
setTabState((current) => {
@@ -3418,6 +3425,7 @@ function EditorContextProvider(props) {
const value = useMemo(
() => ({
...tabState,
+ setTabState,
addTab,
changeTab,
moveTab,
diff --git a/dist/types/editor/context.d.ts b/dist/types/editor/context.d.ts
index 199db8a294f8132d46470498870adbdf9fdc83af..d8901fe0d50db17db36a502dcf69d5f69efb84a1 100644
--- a/dist/types/editor/context.d.ts
+++ b/dist/types/editor/context.d.ts
@@ -1,6 +1,6 @@
import { DocumentNode, FragmentDefinitionNode, OperationDefinitionNode, ValidationRule } from 'graphql';
import { VariableToType } from 'graphql-language-service';
-import { ReactNode } from 'react';
+import { Dispatch, ReactNode, SetStateAction } from 'react';
import { TabDefinition, TabsState, TabState } from './tabs';
import { CodeMirrorEditor } from './types';
export declare type CodeMirrorEditorWithOperationFacts = CodeMirrorEditor & {
@@ -10,10 +10,11 @@ export declare type CodeMirrorEditorWithOperationFacts = CodeMirrorEditor & {
variableToType: VariableToType | null;
};
export declare type EditorContextType = TabsState & {
+ setTabState: Dispatch<SetStateAction<TabsState>>;
/**
* Add a new tab.
*/
- addTab(): void;
+ addTab(tabState?: Pick<TabState, 'id' | 'query' | 'variables' | 'headers' | 'title'>): void;
/**
* Switch to a different tab.
* @param index The index of the tab that should be switched to.
@@ -38,7 +39,7 @@ export declare type EditorContextType = TabsState & {
* @param partialTab A partial tab state object that will override the
* current values. The properties `id`, `hash` and `title` cannot be changed.
*/
- updateActiveTabValues(partialTab: Partial<Omit<TabState, 'id' | 'hash' | 'title'>>): void;
+ updateActiveTabValues(partialTab: Partial<Omit<TabState, 'hash'>>): void;
/**
* The CodeMirror editor instance for the headers editor.
*/
diff --git a/dist/types/editor/tabs.d.ts b/dist/types/editor/tabs.d.ts
index 28704a9c1c6e22fa75986de8591759e13035c8c5..5204d2b25198f89da9bba70804656f02799c7df6 100644
--- a/dist/types/editor/tabs.d.ts
+++ b/dist/types/editor/tabs.d.ts
@@ -90,7 +90,7 @@ export declare function useSetEditorValues({ queryEditor, variableEditor, header
headers?: string | null | undefined;
response: string | null;
}) => void;
-export declare function createTab({ query, variables, headers, }?: Partial<TabDefinition>): TabState;
+export declare function createTab({ id, title, query, variables, headers, }: Partial<TabDefinition & Pick<TabState, 'id' | 'title'>>): TabState;
export declare function setPropertiesInActiveTab(state: TabsState, partialTab: Partial<Omit<TabState, 'id' | 'hash' | 'title'>>): TabsState;
export declare function fuzzyExtractOperationName(str: string): string | null;
export declare function clearHeadersFromTabs(storage: StorageAPI | null): void;

59
patches/graphiql.patch Normal file
View file

@ -0,0 +1,59 @@
diff --git a/dist/index.d.ts b/dist/index.d.ts
index d0d893ea0caffb6c1c70c5f95aed8ca49bc74701..ba3c02801e958c66fde9b813821e5a608f49b1cf 100644
--- a/dist/index.d.ts
+++ b/dist/index.d.ts
@@ -14,7 +14,7 @@ declare type AddSuffix<Obj extends Record<string, any>, Suffix extends string> =
[Key in keyof Obj as `${string & Key}${Suffix}`]: Obj[Key];
};
-export declare function GraphiQL({ dangerouslyAssumeSchemaIsValid, confirmCloseTab, defaultQuery, defaultTabs, externalFragments, fetcher, getDefaultFieldNames, headers, inputValueDeprecation, introspectionQueryName, maxHistoryLength, onEditOperationName, onSchemaChange, onTabChange, onTogglePluginVisibility, operationName, plugins, query, response, schema, schemaDescription, shouldPersistHeaders, storage, validationRules, variables, visiblePlugin, defaultHeaders, ...props }: GraphiQLProps): JSX_2.Element;
+export declare function GraphiQL({ dangerouslyAssumeSchemaIsValid, confirmCloseTab, defaultQuery, defaultTabs, externalFragments, fetcher, getDefaultFieldNames, headers, inputValueDeprecation, introspectionQueryName, maxHistoryLength, onEditOperationName, onSchemaChange, onTabChange, onTogglePluginVisibility, operationName, plugins, query, response, schema, schemaDescription, shouldPersistHeaders, storage, validationRules, variables, visiblePlugin, defaultHeaders, onModifyHeaders, ...props }: GraphiQLProps): JSX_2.Element;
export declare namespace GraphiQL {
var Logo: typeof GraphiQLLogo;
diff --git a/dist/index.mjs b/dist/index.mjs
index cf1a9036b4b35b7918da09ead6977e1e77724b8a..896dadb9c22b36ceee99776b87684f9e3899023d 100644
--- a/dist/index.mjs
+++ b/dist/index.mjs
@@ -1,4 +1,4 @@
-import { GraphiQLProvider, useEditorContext, useExecutionContext, useSchemaContext, useStorageContext, usePluginContext, useTheme, useDragResize, Tooltip, UnStyledButton, ReloadIcon, KeyboardShortcutIcon, SettingsIcon, Tabs, Tab, PlusIcon, QueryEditor, ExecuteButton, ChevronUpIcon, ChevronDownIcon, VariableEditor, HeaderEditor, Spinner, ResponseEditor, Dialog, ButtonGroup, Button, useCopyQuery, useMergeQuery, usePrettifyEditors, ToolbarButton, PrettifyIcon, MergeIcon, CopyIcon } from "@graphiql/react";
+import { GraphiQLProvider, useEditorContext, useExecutionContext, useSchemaContext, useStorageContext, usePluginContext, useTheme, useDragResize, Tooltip, UnStyledButton, ReloadIcon, KeyboardShortcutIcon, SettingsIcon, Tabs, Tab, PlusIcon, QueryEditor, ExecuteButton, ChevronUpIcon, ChevronDownIcon, VariableEditor, HeaderEditor, Spinner, ResponseEditor, Dialog, ButtonGroup, Button, useCopyQuery, useMergeQuery, usePrettifyEditors, ToolbarButton, PrettifyIcon, MergeIcon, CopyIcon, isMacOs } from "@graphiql/react";
import { GraphiQLProvider as GraphiQLProvider2 } from "@graphiql/react";
import React, { version, useMemo, useEffect, useState, Children, cloneElement, useCallback, Fragment } from "react";
const majorVersion = parseInt(version.slice(0, 2), 10);
@@ -39,6 +39,7 @@ function GraphiQL({
variables,
visiblePlugin,
defaultHeaders,
+ onModifyHeaders,
...props
}) {
var _a, _b;
@@ -85,7 +86,8 @@ function GraphiQL({
shouldPersistHeaders,
storage,
validationRules,
- variables
+ variables,
+ onModifyHeaders
},
/* @__PURE__ */ React.createElement(
GraphiQLInterface,
@@ -398,7 +400,7 @@ function GraphiQLInterface(props) {
{
type: "button",
className: "graphiql-tab-add",
- onClick: handleAddTab,
+ onClick: () => handleAddTab(),
"aria-label": "New tab"
},
/* @__PURE__ */ React.createElement(PlusIcon, { "aria-hidden": "true" })
@@ -602,7 +604,7 @@ function GraphiQLInterface(props) {
)) : null
)));
}
-const modifier = typeof navigator !== "undefined" && navigator.platform.toLowerCase().indexOf("mac") === 0 ? "Cmd" : "Ctrl";
+const modifier = isMacOs ? "⌘" : "Ctrl";
const SHORT_KEYS = Object.entries({
"Search in editor": [modifier, "F"],
"Search in documentation": [modifier, "K"],

View file

@ -16,9 +16,12 @@ patchedDependencies:
'@apollo/federation@0.38.1':
hash: rjgakkkphrejw6qrtph4ar24zq
path: patches/@apollo__federation@0.38.1.patch
'@graphiql/react@1.0.0-alpha.3':
hash: gnbo5nw2wgehkfq2yrmuhrx4im
path: patches/@graphiql__react@1.0.0-alpha.3.patch
'@fastify/vite':
hash: wz23vdqq6qtsz64wb433afnvou
path: patches/@fastify__vite.patch
'@graphiql/react':
hash: bru5she67j343rpipomank3vn4
path: patches/@graphiql__react.patch
'@graphql-eslint/eslint-plugin@3.20.1':
hash: n437g5o7zq7pnxdxldn52uql2q
path: patches/@graphql-eslint__eslint-plugin@3.20.1.patch
@ -40,6 +43,9 @@ patchedDependencies:
got@14.4.5:
hash: b6pwqmrs3qqykctltsasvrfwti
path: patches/got@14.4.5.patch
graphiql:
hash: yjzkcog7ut7wshk4npre67txki
path: patches/graphiql.patch
mjml-core@4.14.0:
hash: zxxsxbqejjmcwuzpigutzzq6wa
path: patches/mjml-core@4.14.0.patch
@ -689,6 +695,12 @@ importers:
'@hive/webhooks':
specifier: workspace:*
version: link:../webhooks
'@nodesecure/i18n':
specifier: ^4.0.1
version: 4.0.1
'@nodesecure/js-x-ray':
specifier: 8.0.0
version: 8.0.0
'@octokit/app':
specifier: 14.1.0
version: 14.1.0
@ -1661,14 +1673,14 @@ importers:
specifier: 7.0.4
version: 7.0.4
'@fastify/vite':
specifier: 6.0.7
version: 6.0.7(@types/node@22.9.3)(less@4.2.0)(lightningcss@1.28.1)(terser@5.36.0)
specifier: 6.0.6
version: 6.0.6(patch_hash=wz23vdqq6qtsz64wb433afnvou)(@types/node@22.9.3)(less@4.2.0)(lightningcss@1.28.1)(terser@5.36.0)
'@graphiql/plugin-explorer':
specifier: 4.0.0-alpha.2
version: 4.0.0-alpha.2(@graphiql/react@1.0.0-alpha.3(patch_hash=gnbo5nw2wgehkfq2yrmuhrx4im)(@codemirror/language@6.10.2)(@types/node@22.9.3)(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(graphql-ws@5.16.0(graphql@16.9.0))(graphql@16.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(graphql@16.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
version: 4.0.0-alpha.2(@graphiql/react@1.0.0-alpha.4(patch_hash=bru5she67j343rpipomank3vn4)(@codemirror/language@6.10.2)(@types/node@22.9.3)(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(graphql-ws@5.16.0(graphql@16.9.0))(graphql@16.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(graphql@16.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@graphiql/react':
specifier: 1.0.0-alpha.3
version: 1.0.0-alpha.3(patch_hash=gnbo5nw2wgehkfq2yrmuhrx4im)(@codemirror/language@6.10.2)(@types/node@22.9.3)(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(graphql-ws@5.16.0(graphql@16.9.0))(graphql@16.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
specifier: 1.0.0-alpha.4
version: 1.0.0-alpha.4(patch_hash=bru5she67j343rpipomank3vn4)(@codemirror/language@6.10.2)(@types/node@22.9.3)(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(graphql-ws@5.16.0(graphql@16.9.0))(graphql@16.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@graphiql/toolkit':
specifier: 0.9.1
version: 0.9.1(@types/node@22.9.3)(graphql-ws@5.16.0(graphql@16.9.0))(graphql@16.9.0)
@ -1813,6 +1825,9 @@ importers:
'@trpc/server':
specifier: 10.45.2
version: 10.45.2
'@types/crypto-js':
specifier: ^4.2.2
version: 4.2.2
'@types/dompurify':
specifier: 3.2.0
version: 3.2.0
@ -1858,6 +1873,9 @@ importers:
cmdk:
specifier: 0.2.1
version: 0.2.1(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
crypto-js:
specifier: ^4.2.0
version: 4.2.0
date-fns:
specifier: 4.1.0
version: 4.1.0
@ -1880,11 +1898,11 @@ importers:
specifier: 2.4.6
version: 2.4.6(react@18.3.1)
framer-motion:
specifier: 11.15.0
version: 11.15.0(@emotion/is-prop-valid@1.2.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
specifier: 11.11.17
version: 11.11.17(@emotion/is-prop-valid@1.2.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
graphiql:
specifier: 4.0.0-alpha.4
version: 4.0.0-alpha.4(@codemirror/language@6.10.2)(@types/node@22.9.3)(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(graphql-ws@5.16.0(graphql@16.9.0))(graphql@16.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
specifier: 4.0.0-alpha.5
version: 4.0.0-alpha.5(patch_hash=yjzkcog7ut7wshk4npre67txki)(@codemirror/language@6.10.2)(@types/node@22.9.3)(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(graphql-ws@5.16.0(graphql@16.9.0))(graphql@16.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
graphql:
specifier: 16.9.0
version: 16.9.0
@ -3940,8 +3958,8 @@ packages:
'@fastify/static@7.0.4':
resolution: {integrity: sha512-p2uKtaf8BMOZWLs6wu+Ihg7bWNBdjNgCwDza4MJtTqg+5ovKmcbgbR9Xs5/smZ1YISfzKOCNYmZV8LaCj+eJ1Q==}
'@fastify/vite@6.0.7':
resolution: {integrity: sha512-+dRo9KUkvmbqdmBskG02SwigWl06Mwkw8SBDK1zTNH6vd4DyXbRvI7RmJEmBkLouSU81KTzy1+OzwHSffqSD6w==}
'@fastify/vite@6.0.6':
resolution: {integrity: sha512-FsWJC92murm5tjeTezTTvMLyZido/ZWy0wYWpVkh/bDe1gAUAabYLB7Vp8hokXGsRE/mOpqYVsRDAKENY2qPUQ==}
bundledDependencies: []
'@floating-ui/core@1.2.6':
@ -3988,8 +4006,8 @@ packages:
react: ^16.8.0 || ^17 || ^18
react-dom: ^16.8.0 || ^17 || ^18
'@graphiql/react@1.0.0-alpha.3':
resolution: {integrity: sha512-9WPfC7I9xO1qC/dKaYwVe3UbPJvdjU+fxoUW2Id0mIljkD7LXnVUnzBQMB1SY4JrRJX3I0nQPbHXzKvAYSpnjw==}
'@graphiql/react@1.0.0-alpha.4':
resolution: {integrity: sha512-psie5qQNVlklXAhNPD8sIRtpNDzJfNzzZ5EH0ofrJ8AeVcj+DYmIqxRWw5zvjDAtNTKyOAJSCpQdCNDCCi2PEQ==}
peerDependencies:
graphql: ^15.5.0 || ^16.0.0 || ^17.0.0-alpha.2
react: ^16.8.0 || ^17 || ^18
@ -5044,6 +5062,20 @@ packages:
resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
engines: {node: '>= 8'}
'@nodesecure/estree-ast-utils@1.5.0':
resolution: {integrity: sha512-uRdPpBQOSvn+iuLWlbtP6NZTI54ALhFZNRc5K+ZaYT8FeYFu0g6HwlNwwwPyyTB1kXyysRRi9j9fkW9bAYSXtw==}
'@nodesecure/i18n@4.0.1':
resolution: {integrity: sha512-i/A8citn5N1i7VBL0PbryAx3zM3sgpFsfVVzIrl5tSTih3cMbQ3QYKCo294U2DMaoncWmI6wO5S71XwctvTHeg==}
engines: {node: '>=18'}
'@nodesecure/js-x-ray@8.0.0':
resolution: {integrity: sha512-RTDrJfYuLIZ1pnIz+KJOCfH+kHaoxkO0Nyr2xo/6eiuFKzO1F0gSnrE1uQfhUGfeBiMiwx2qzOdyeKFgYn5Baw==}
engines: {node: '>=18.0.0'}
'@nodesecure/sec-literal@1.2.0':
resolution: {integrity: sha512-LGgJmBtnIVHwjZ1QA62YyDvPysdYvGcGn6/JADjY23snTNZS+D9JrkxnChggoNDYj3/GtjutbY/cSlLXEcUJRw==}
'@npmcli/agent@2.2.1':
resolution: {integrity: sha512-H4FrOVtNyWC8MUwL3UfjOsAihHvT1Pe8POj3JvjXhSTJipsZMtgUALCT4mGyYZNxymkUfOw3PUj6dE4QPp6osQ==}
engines: {node: ^16.14.0 || >=18.0.0}
@ -7787,6 +7819,9 @@ packages:
'@types/connect@3.4.36':
resolution: {integrity: sha512-P63Zd/JUGq+PdrM1lv0Wv5SBYeA2+CORvbrXbngriYY0jzLUWfQMQQxOhjONEz/wlHOAxOdY7CY65rgQdTjq2w==}
'@types/crypto-js@4.2.2':
resolution: {integrity: sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ==}
'@types/debug@4.1.7':
resolution: {integrity: sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg==}
@ -9556,6 +9591,10 @@ packages:
resolution: {integrity: sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==}
engines: {node: '>=0.10.0'}
deepmerge@4.3.1:
resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==}
engines: {node: '>=0.10.0'}
default-browser-id@3.0.0:
resolution: {integrity: sha512-OZ1y3y0SqSICtE8DE4S8YOE9UZOJ8wO16fKWVP5J1Qz42kV9jcnMVFrEE/noXb/ss3Q4pZIH79kxofzyNNtUNA==}
engines: {node: '>=12'}
@ -9674,6 +9713,10 @@ packages:
resolution: {integrity: sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==}
engines: {node: '>=0.3.1'}
digraph-js@2.2.3:
resolution: {integrity: sha512-btynrARSW6pBmDz9+cwCxkBJ91CGBxIaNQo7V+ul9/rCRr3HddwehpEMnL6Ru2OeC2pKdRteB1v5TgZRrAAYKQ==}
engines: {node: '>=16.0.0'}
dir-glob@3.0.1:
resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==}
engines: {node: '>=8'}
@ -10483,12 +10526,12 @@ packages:
react-dom:
optional: true
framer-motion@11.15.0:
resolution: {integrity: sha512-MLk8IvZntxOMg7lDBLw2qgTHHv664bYoYmnFTmE0Gm/FW67aOJk0WM3ctMcG+Xhcv+vh5uyyXwxvxhSeJzSe+w==}
framer-motion@11.11.17:
resolution: {integrity: sha512-O8QzvoKiuzI5HSAHbcYuL6xU+ZLXbrH7C8Akaato4JzQbX2ULNeniqC2Vo5eiCtFktX9XsJ+7nUhxcl2E2IjpA==}
peerDependencies:
'@emotion/is-prop-valid': '*'
react: ^18.0.0 || ^19.0.0
react-dom: ^18.0.0 || ^19.0.0
react: ^18.0.0
react-dom: ^18.0.0
peerDependenciesMeta:
'@emotion/is-prop-valid':
optional: true
@ -10497,6 +10540,9 @@ packages:
react-dom:
optional: true
frequency-set@1.0.2:
resolution: {integrity: sha512-Qip6vS0fY/et08sZXumws05weoYvj2ZLkBq3xIwFDFLg8v5IMQiRa+P30tXL0CU6DiYUPLuN3HyRcwW6yWPdeA==}
fresh@0.5.2:
resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==}
engines: {node: '>= 0.6'}
@ -10761,8 +10807,8 @@ packages:
react: ^15.6.0 || ^16.0.0
react-dom: ^15.6.0 || ^16.0.0
graphiql@4.0.0-alpha.4:
resolution: {integrity: sha512-mMjUJKqNSuHGDBC6JE3CprtBniB3iaGN7Ifisrd6ucmpH7biqA1uIbz1LgzCeQdtndCTHADLJu6dCGOWAcgP9w==}
graphiql@4.0.0-alpha.5:
resolution: {integrity: sha512-LAxuJ8kwPlT7YbgM2VMr6bn9xWHRgUL4SRZtvA2VTDDkMgsnTLKd9Vro/EZNKJCTaC7SGOBRImAGCOPlpfTTjw==}
peerDependencies:
graphql: ^15.5.0 || ^16.0.0 || ^17.0.0-alpha.2
react: ^16.8.0 || ^17 || ^18
@ -11284,6 +11330,10 @@ packages:
resolution: {integrity: sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA==}
engines: {node: '>= 0.4'}
is-base64@1.1.0:
resolution: {integrity: sha512-Nlhg7Z2dVC4/PTvIFkgVVNvPHSO2eR/Yd0XzhGiXCXEvWnptXlXa/clQ8aePPiMuxEGcWfzWbGw2Fe3d+Y3v1g==}
hasBin: true
is-bigint@1.0.4:
resolution: {integrity: sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==}
@ -11390,6 +11440,9 @@ packages:
is-map@2.0.2:
resolution: {integrity: sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==}
is-minified-code@2.0.0:
resolution: {integrity: sha512-I1BHmOxm7owypunUWnYx2Ggdhg3lzdyJXLepi8NuR/IsvgVgkwjLj+12iYAGUklu0Xvy3nXGcDSKGbE0Q0Nkag==}
is-negative-zero@2.0.2:
resolution: {integrity: sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==}
engines: {node: '>= 0.4'}
@ -11492,6 +11545,10 @@ packages:
resolution: {integrity: sha512-2AT6j+gXe/1ueqbW6fLZJiIw3F8iXGJtt0yDrZaBhAZEG1raiTxKWU+IPqMCzQAXOUCKdA4UDMgacKH25XG2Cw==}
engines: {node: '>=4'}
is-svg@4.4.0:
resolution: {integrity: sha512-v+AgVwiK5DsGtT9ng+m4mClp6zDAmwrW8nZi6Gg15qzvBnRWWdfWA1TGaXyCDnWq5g5asofIgMVl3PjKxvk1ug==}
engines: {node: '>=6'}
is-symbol@1.0.4:
resolution: {integrity: sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==}
engines: {node: '>= 0.4'}
@ -12030,6 +12087,9 @@ packages:
lodash.isarguments@3.1.0:
resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==}
lodash.isequal@4.5.0:
resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==}
lodash.isplainobject@4.0.6:
resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==}
@ -12054,6 +12114,9 @@ packages:
lodash.startcase@4.4.0:
resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==}
lodash.uniqwith@4.5.0:
resolution: {integrity: sha512-7lYL8bLopMoy4CTICbxygAUq6CdRJ36vFc80DucPueUee+d5NBRxz3FdT9Pes/HEx5mPoT9jwnsEJWz1N7uq7Q==}
lodash.xorby@4.7.0:
resolution: {integrity: sha512-gYiD6nvuQy0AEkMoUju+t4f4Rn18fjsLB/7x7YZFqtFT9kmegRLrj/uGEQVyVDy7otTmSrIMXNOk2wwuLcfHCQ==}
@ -12286,6 +12349,10 @@ packages:
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
engines: {node: '>= 8'}
meriyah@5.0.0:
resolution: {integrity: sha512-tNlPDP4AzkH/7cROw7PKJ7mCLe/ZLpa2ja23uqB35vt63+8dgZi2NKLJMrkjxLcxArnLJVvd3Y/7pRl3OLR7yg==}
engines: {node: '>=10.4.0'}
mermaid@11.2.1:
resolution: {integrity: sha512-F8TEaLVVyxTUmvKswVFyOkjPrlJA5h5vNR1f7ZnSWSpqxgEZG1hggtn/QCa7znC28bhlcrNh10qYaIiill7q4A==}
@ -12693,12 +12760,6 @@ packages:
moo@0.5.2:
resolution: {integrity: sha512-iSAJLHYKnX41mKcJKjqvnAN9sf0LMDTXDEvFv+ffuRR9a1MIuXLjMNL6EsnDHSkKLTWNqQQ5uo61P4EbU4NU+Q==}
motion-dom@11.14.3:
resolution: {integrity: sha512-lW+D2wBy5vxLJi6aCP0xyxTxlTfiu+b+zcpVbGVFUxotwThqhdpPRSmX8xztAgtZMPMeU0WGVn/k1w4I+TbPqA==}
motion-utils@11.14.3:
resolution: {integrity: sha512-Xg+8xnqIJTpr0L/cidfTTBFkvRw26ZtGGuIhA94J9PQ2p4mEa06Xx7QVYZH0BP+EpMSaDlu+q0I0mmvwADPsaQ==}
mri@1.2.0:
resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==}
engines: {node: '>=4'}
@ -14380,6 +14441,9 @@ packages:
safe-regex2@3.1.0:
resolution: {integrity: sha512-RAAZAGbap2kBfbVhvmnTFv73NWLMvDGOITFYTZBAaY8eR+Ir4ef7Up/e7amo+y1+AH+3PtLkrt9mvcTsG9LXug==}
safe-regex@2.1.1:
resolution: {integrity: sha512-rx+x8AMzKb5Q5lQ95Zoi6ZbJqwCLkqi3XuJXp5P3rT8OEc6sZCJG5AE5dU3lsgRr/F4Bs31jSlVN+j5KrsGu9A==}
safe-stable-stringify@2.4.2:
resolution: {integrity: sha512-gMxvPJYhP0O9n2pvcfYfIuYgbledAOJFcqRThtPRmjscaipiwcwPPKLytpVzMkG2HAN87Qmo2d4PtGiri1dSLA==}
engines: {node: '>=10'}
@ -15186,6 +15250,9 @@ packages:
'@swc/wasm':
optional: true
ts-pattern@5.5.0:
resolution: {integrity: sha512-jqbIpTsa/KKTJYWgPNsFNbLVpwCgzXfFJ1ukNn4I8hMwyQzHMJnk/BqWzggB0xpkILuKzaO/aMYhS0SkaJyKXg==}
tsconfck@3.0.3:
resolution: {integrity: sha512-4t0noZX9t6GcPTfBAbIbbIU4pfpCwh0ueq3S4O/5qXI1VwK1outmxhe9dOiEWqMz3MW2LKgDTpqWV+37IWuVbA==}
engines: {node: ^18 || >=20}
@ -16449,8 +16516,8 @@ snapshots:
dependencies:
'@aws-crypto/sha256-browser': 3.0.0
'@aws-crypto/sha256-js': 3.0.0
'@aws-sdk/client-sso-oidc': 3.596.0(@aws-sdk/client-sts@3.596.0)
'@aws-sdk/client-sts': 3.596.0
'@aws-sdk/client-sso-oidc': 3.596.0
'@aws-sdk/client-sts': 3.596.0(@aws-sdk/client-sso-oidc@3.596.0)
'@aws-sdk/core': 3.592.0
'@aws-sdk/credential-provider-node': 3.596.0(@aws-sdk/client-sso-oidc@3.596.0)(@aws-sdk/client-sts@3.596.0)
'@aws-sdk/middleware-host-header': 3.577.0
@ -16557,11 +16624,11 @@ snapshots:
transitivePeerDependencies:
- aws-crt
'@aws-sdk/client-sso-oidc@3.596.0(@aws-sdk/client-sts@3.596.0)':
'@aws-sdk/client-sso-oidc@3.596.0':
dependencies:
'@aws-crypto/sha256-browser': 3.0.0
'@aws-crypto/sha256-js': 3.0.0
'@aws-sdk/client-sts': 3.596.0
'@aws-sdk/client-sts': 3.596.0(@aws-sdk/client-sso-oidc@3.596.0)
'@aws-sdk/core': 3.592.0
'@aws-sdk/credential-provider-node': 3.596.0(@aws-sdk/client-sso-oidc@3.596.0)(@aws-sdk/client-sts@3.596.0)
'@aws-sdk/middleware-host-header': 3.577.0
@ -16600,7 +16667,6 @@ snapshots:
'@smithy/util-utf8': 3.0.0
tslib: 2.8.1
transitivePeerDependencies:
- '@aws-sdk/client-sts'
- aws-crt
'@aws-sdk/client-sso-oidc@3.716.0(@aws-sdk/client-sts@3.716.0)':
@ -16734,11 +16800,11 @@ snapshots:
transitivePeerDependencies:
- aws-crt
'@aws-sdk/client-sts@3.596.0':
'@aws-sdk/client-sts@3.596.0(@aws-sdk/client-sso-oidc@3.596.0)':
dependencies:
'@aws-crypto/sha256-browser': 3.0.0
'@aws-crypto/sha256-js': 3.0.0
'@aws-sdk/client-sso-oidc': 3.596.0(@aws-sdk/client-sts@3.596.0)
'@aws-sdk/client-sso-oidc': 3.596.0
'@aws-sdk/core': 3.592.0
'@aws-sdk/credential-provider-node': 3.596.0(@aws-sdk/client-sso-oidc@3.596.0)(@aws-sdk/client-sts@3.596.0)
'@aws-sdk/middleware-host-header': 3.577.0
@ -16777,6 +16843,7 @@ snapshots:
'@smithy/util-utf8': 3.0.0
tslib: 2.8.1
transitivePeerDependencies:
- '@aws-sdk/client-sso-oidc'
- aws-crt
'@aws-sdk/client-sts@3.716.0':
@ -16890,7 +16957,7 @@ snapshots:
'@aws-sdk/credential-provider-ini@3.596.0(@aws-sdk/client-sso-oidc@3.596.0)(@aws-sdk/client-sts@3.596.0)':
dependencies:
'@aws-sdk/client-sts': 3.596.0
'@aws-sdk/client-sts': 3.596.0(@aws-sdk/client-sso-oidc@3.596.0)
'@aws-sdk/credential-provider-env': 3.587.0
'@aws-sdk/credential-provider-http': 3.596.0
'@aws-sdk/credential-provider-process': 3.587.0
@ -17009,7 +17076,7 @@ snapshots:
'@aws-sdk/credential-provider-web-identity@3.587.0(@aws-sdk/client-sts@3.596.0)':
dependencies:
'@aws-sdk/client-sts': 3.596.0
'@aws-sdk/client-sts': 3.596.0(@aws-sdk/client-sso-oidc@3.596.0)
'@aws-sdk/types': 3.577.0
'@smithy/property-provider': 3.1.10
'@smithy/types': 3.7.1
@ -17184,7 +17251,7 @@ snapshots:
'@aws-sdk/token-providers@3.587.0(@aws-sdk/client-sso-oidc@3.596.0)':
dependencies:
'@aws-sdk/client-sso-oidc': 3.596.0(@aws-sdk/client-sts@3.596.0)
'@aws-sdk/client-sso-oidc': 3.596.0
'@aws-sdk/types': 3.577.0
'@smithy/property-provider': 3.1.10
'@smithy/shared-ini-file-loader': 3.1.11
@ -18646,7 +18713,7 @@ snapshots:
fastq: 1.17.1
glob: 10.3.12
'@fastify/vite@6.0.7(@types/node@22.9.3)(less@4.2.0)(lightningcss@1.28.1)(terser@5.36.0)':
'@fastify/vite@6.0.6(patch_hash=wz23vdqq6qtsz64wb433afnvou)(@types/node@22.9.3)(less@4.2.0)(lightningcss@1.28.1)(terser@5.36.0)':
dependencies:
'@fastify/middie': 8.3.1
'@fastify/static': 6.12.0
@ -18702,22 +18769,22 @@ snapshots:
graphql: 16.9.0
typescript: 5.7.2
'@graphiql/plugin-explorer@4.0.0-alpha.2(@graphiql/react@1.0.0-alpha.3(patch_hash=gnbo5nw2wgehkfq2yrmuhrx4im)(@codemirror/language@6.10.2)(@types/node@22.9.3)(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(graphql-ws@5.16.0(graphql@16.9.0))(graphql@16.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(graphql@16.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
'@graphiql/plugin-explorer@4.0.0-alpha.2(@graphiql/react@1.0.0-alpha.4(patch_hash=bru5she67j343rpipomank3vn4)(@codemirror/language@6.10.2)(@types/node@22.9.3)(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(graphql-ws@5.16.0(graphql@16.9.0))(graphql@16.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(graphql@16.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@graphiql/react': 1.0.0-alpha.3(patch_hash=gnbo5nw2wgehkfq2yrmuhrx4im)(@codemirror/language@6.10.2)(@types/node@22.9.3)(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(graphql-ws@5.16.0(graphql@16.9.0))(graphql@16.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@graphiql/react': 1.0.0-alpha.4(patch_hash=bru5she67j343rpipomank3vn4)(@codemirror/language@6.10.2)(@types/node@22.9.3)(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(graphql-ws@5.16.0(graphql@16.9.0))(graphql@16.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
graphiql-explorer: 0.9.0(graphql@16.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
graphql: 16.9.0
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
'@graphiql/react@1.0.0-alpha.3(patch_hash=gnbo5nw2wgehkfq2yrmuhrx4im)(@codemirror/language@6.10.2)(@types/node@22.9.3)(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(graphql-ws@5.16.0(graphql@16.9.0))(graphql@16.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
'@graphiql/react@1.0.0-alpha.4(patch_hash=bru5she67j343rpipomank3vn4)(@codemirror/language@6.10.2)(@types/node@22.9.3)(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(graphql-ws@5.16.0(graphql@16.9.0))(graphql@16.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@graphiql/toolkit': 0.10.0(@types/node@22.9.3)(graphql-ws@5.16.0(graphql@16.9.0))(graphql@16.9.0)
'@headlessui/react': 1.7.19(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-dialog': 1.1.4(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-dropdown-menu': 2.1.4(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-tooltip': 1.1.6(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-visually-hidden': 1.1.0(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-visually-hidden': 1.1.1(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@types/codemirror': 5.60.15
clsx: 1.2.1
codemirror: 5.65.9
@ -20344,6 +20411,35 @@ snapshots:
'@nodelib/fs.scandir': 2.1.5
fastq: 1.17.1
'@nodesecure/estree-ast-utils@1.5.0':
dependencies:
'@nodesecure/sec-literal': 1.2.0
'@nodesecure/i18n@4.0.1':
dependencies:
cacache: 18.0.2
deepmerge: 4.3.1
lodash.get: 4.4.2
'@nodesecure/js-x-ray@8.0.0':
dependencies:
'@nodesecure/estree-ast-utils': 1.5.0
'@nodesecure/sec-literal': 1.2.0
digraph-js: 2.2.3
estree-walker: 3.0.3
frequency-set: 1.0.2
is-minified-code: 2.0.0
meriyah: 5.0.0
safe-regex: 2.1.1
ts-pattern: 5.5.0
'@nodesecure/sec-literal@1.2.0':
dependencies:
frequency-set: 1.0.2
is-base64: 1.1.0
is-svg: 4.4.0
string-width: 5.1.2
'@npmcli/agent@2.2.1':
dependencies:
agent-base: 7.1.0
@ -23962,6 +24058,8 @@ snapshots:
dependencies:
'@types/node': 22.9.3
'@types/crypto-js@4.2.2': {}
'@types/debug@4.1.7':
dependencies:
'@types/ms': 0.7.34
@ -26012,6 +26110,8 @@ snapshots:
deepmerge@4.2.2: {}
deepmerge@4.3.1: {}
default-browser-id@3.0.0:
dependencies:
bplist-parser: 0.2.0
@ -26099,6 +26199,11 @@ snapshots:
diff@5.2.0: {}
digraph-js@2.2.3:
dependencies:
lodash.isequal: 4.5.0
lodash.uniqwith: 4.5.0
dir-glob@3.0.1:
dependencies:
path-type: 4.0.0
@ -27274,16 +27379,16 @@ snapshots:
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
framer-motion@11.15.0(@emotion/is-prop-valid@1.2.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
framer-motion@11.11.17(@emotion/is-prop-valid@1.2.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies:
motion-dom: 11.14.3
motion-utils: 11.14.3
tslib: 2.8.1
optionalDependencies:
'@emotion/is-prop-valid': 1.2.2
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
frequency-set@1.0.2: {}
fresh@0.5.2: {}
fromentries@1.3.2: {}
@ -27606,9 +27711,9 @@ snapshots:
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
graphiql@4.0.0-alpha.4(@codemirror/language@6.10.2)(@types/node@22.9.3)(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(graphql-ws@5.16.0(graphql@16.9.0))(graphql@16.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
graphiql@4.0.0-alpha.5(patch_hash=yjzkcog7ut7wshk4npre67txki)(@codemirror/language@6.10.2)(@types/node@22.9.3)(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(graphql-ws@5.16.0(graphql@16.9.0))(graphql@16.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies:
'@graphiql/react': 1.0.0-alpha.3(patch_hash=gnbo5nw2wgehkfq2yrmuhrx4im)(@codemirror/language@6.10.2)(@types/node@22.9.3)(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(graphql-ws@5.16.0(graphql@16.9.0))(graphql@16.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@graphiql/react': 1.0.0-alpha.4(patch_hash=bru5she67j343rpipomank3vn4)(@codemirror/language@6.10.2)(@types/node@22.9.3)(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(graphql-ws@5.16.0(graphql@16.9.0))(graphql@16.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
graphql: 16.9.0
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
@ -28328,6 +28433,8 @@ snapshots:
dependencies:
has-tostringtag: 1.0.0
is-base64@1.1.0: {}
is-bigint@1.0.4:
dependencies:
has-bigints: 1.0.2
@ -28412,6 +28519,8 @@ snapshots:
is-map@2.0.2: {}
is-minified-code@2.0.0: {}
is-negative-zero@2.0.2: {}
is-network-error@1.0.0: {}
@ -28485,6 +28594,10 @@ snapshots:
dependencies:
better-path-resolve: 1.0.0
is-svg@4.4.0:
dependencies:
fast-xml-parser: 4.4.1
is-symbol@1.0.4:
dependencies:
has-symbols: 1.0.3
@ -29004,6 +29117,8 @@ snapshots:
lodash.isarguments@3.1.0: {}
lodash.isequal@4.5.0: {}
lodash.isplainobject@4.0.6: {}
lodash.lowercase@4.3.0: {}
@ -29020,6 +29135,8 @@ snapshots:
lodash.startcase@4.4.0: {}
lodash.uniqwith@4.5.0: {}
lodash.xorby@4.7.0: {}
lodash@4.17.21: {}
@ -29403,6 +29520,8 @@ snapshots:
merge2@1.4.1: {}
meriyah@5.0.0: {}
mermaid@11.2.1:
dependencies:
'@braintree/sanitize-url': 7.1.0
@ -30176,10 +30295,6 @@ snapshots:
moo@0.5.2: {}
motion-dom@11.14.3: {}
motion-utils@11.14.3: {}
mri@1.2.0: {}
mrmime@2.0.0: {}
@ -32068,6 +32183,10 @@ snapshots:
dependencies:
ret: 0.4.3
safe-regex@2.1.1:
dependencies:
regexp-tree: 0.1.27
safe-stable-stringify@2.4.2: {}
safer-buffer@2.1.2: {}
@ -32971,6 +33090,8 @@ snapshots:
optionalDependencies:
'@swc/core': 1.10.1(@swc/helpers@0.5.11)
ts-pattern@5.5.0: {}
tsconfck@3.0.3(typescript@5.7.2):
optionalDependencies:
typescript: 5.7.2