mirror of
https://github.com/graphql-hive/console
synced 2026-04-21 14:37:17 +00:00
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:
parent
38c14e21d8
commit
e0eb3bdb28
60 changed files with 2882 additions and 345 deletions
8
.changeset/dull-seas-remember.md
Normal file
8
.changeset/dull-seas-remember.md
Normal 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)
|
||||
6
.github/workflows/tests-e2e.yaml
vendored
6
.github/workflows/tests-e2e.yaml
vendored
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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?
|
||||
|
|
|
|||
196
cypress/e2e/preflight-script.cy.ts
Normal file
196
cypress/e2e/preflight-script.cy.ts
Normal 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
|
||||
'{ "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();
|
||||
});
|
||||
});
|
||||
|
|
@ -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 ""
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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};
|
||||
|
|
|
|||
|
|
@ -100,6 +100,5 @@ export function deployProxy({
|
|||
service: usage.service,
|
||||
retriable: true,
|
||||
},
|
||||
])
|
||||
.get();
|
||||
]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -34,5 +34,9 @@ services:
|
|||
networks:
|
||||
- 'stack'
|
||||
|
||||
supertokens:
|
||||
ports:
|
||||
- '3567:3567'
|
||||
|
||||
networks:
|
||||
stack: {}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
`);
|
||||
|
|
|
|||
|
|
@ -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!) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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")`,
|
||||
),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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'),
|
||||
],
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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],
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
16
packages/services/api/src/modules/lab/resolvers/Target.ts
Normal file
16
packages/services/api/src/modules/lab/resolvers/Target.ts
Normal 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),
|
||||
};
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
13
packages/web/app/preflight-worker-embed.html
Normal file
13
packages/web/app/preflight-worker-embed.html
Normal 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>
|
||||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
]);
|
||||
672
packages/web/app/src/lib/preflight-sandbox/graphiql-plugin.tsx
Normal file
672
packages/web/app/src/lib/preflight-sandbox/graphiql-plugin.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
29
packages/web/app/src/lib/preflight-sandbox/json.ts
Normal file
29
packages/web/app/src/lib/preflight-sandbox/json.ts
Normal 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
|
||||
);
|
||||
}
|
||||
|
|
@ -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 });
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
101
packages/web/app/src/lib/preflight-sandbox/shared-types.ts
Normal file
101
packages/web/app/src/lib/preflight-sandbox/shared-types.ts
Normal 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>;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 |
|
|
@ -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></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></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 |
|
|
@ -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></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></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></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></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></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></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 |
|
|
@ -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">
|
||||
<></>
|
||||
<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.
|
||||
17
patches/@fastify__vite.patch
Normal file
17
patches/@fastify__vite.patch
Normal 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,
|
||||
423
patches/@graphiql__react.patch
Normal file
423
patches/@graphiql__react.patch
Normal 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;
|
||||
|
|
@ -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
59
patches/graphiql.patch
Normal 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"],
|
||||
225
pnpm-lock.yaml
225
pnpm-lock.yaml
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue