Operation Collections in Lab (#1610)

Co-authored-by: Laurin Quast <laurinquast@googlemail.com>
Co-authored-by: Dotan Simha <dotansimha@gmail.com>
This commit is contained in:
Dimitri POSTOLOV 2023-06-12 16:56:27 +02:00 committed by GitHub
parent c3da2e8666
commit 1cc2a0adca
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
66 changed files with 3510 additions and 459 deletions

View file

@ -144,7 +144,7 @@ module.exports = {
},
tailwindcss: {
config: 'packages/web/app/tailwind.config.cjs',
whitelist: ['drag-none'],
whitelist: ['drag-none', 'graphiql-toolbar-icon', 'graphiql-toolbar-button'],
},
},
},

View file

@ -62,7 +62,7 @@ jobs:
- name: Operation Check
run: |
npx graphql-inspector validate \
"packages/web/app/**/*.graphql|packages/libraries/cli/**/*.graphql|packages/web/app/**/*.tsx|packages/web/app/src/lib/**/*.ts" \
"packages/web/app/{src,pages}/**/*.{graphql,tsx}|packages/libraries/cli/**/*.graphql|packages/web/app/src/lib/**/*.ts" \
"packages/**/module.graphql.ts" \
--maxDepth=20 \
--maxAliasCount=20 \

View file

@ -18,3 +18,5 @@ jobs:
- name: typecheck
run: pnpm typecheck
env:
NODE_OPTIONS: '--max-old-space-size=4096'

View file

@ -4,6 +4,7 @@
"fabiospampinato.vscode-commands",
"esbenp.prettier-vscode",
"thebearingedge.vscode-sql-lit",
"hashicorp.hcl"
"hashicorp.hcl",
"GraphQL.vscode-graphql"
]
}

View file

@ -112,6 +112,12 @@ const config = {
SchemaPolicy: '../shared/entities#SchemaPolicy as SchemaPolicyMapper',
SchemaPolicyRule: '../shared/entities#SchemaPolicyAvailableRuleObject',
SchemaCoordinateUsage: '../shared/mappers#SchemaCoordinateUsageTypeMapper',
DocumentCollection: '../shared/entities#DocumentCollection as DocumentCollectionEntity',
DocumentCollectionOperation:
'../shared/entities#DocumentCollectionOperation as DocumentCollectionOperationEntity',
DocumentCollectionConnection: '../shared/entities#PaginatedDocumentCollections',
DocumentCollectionOperationsConnection:
'../shared/entities#PaginatedDocumentCollectionOperations',
},
},
},
@ -168,7 +174,7 @@ const config = {
},
// Integration tests
'./integration-tests/testkit/gql/': {
documents: './integration-tests/(testkit|tests)/**/*.ts',
documents: ['./integration-tests/(testkit|tests)/**/*.ts'],
preset: 'client',
plugins: [],
},

View file

@ -1,11 +1,34 @@
// eslint-disable-next-line import/no-extraneous-dependencies -- cypress SHOULD be a dev dependency
import { defineConfig } from 'cypress';
// eslint-disable-next-line import/no-extraneous-dependencies
import pg from 'pg';
export default defineConfig({
video: false, // TODO: can it be useful for CI?
screenshotOnRunFailure: false, // TODO: can it be useful for CI?
defaultCommandTimeout: 8000, // sometimes the app takes longer to load, especially in the CI
env: {
POSTGRES_URL: 'postgresql://postgres:postgres@localhost:5432/registry',
},
e2e: {
// defaults
setupNodeEvents(on, config) {
on('task', {
async connectDB(query: string) {
const dbUrl = new URL(config.env.POSTGRES_URL);
const client = new pg.Client({
user: dbUrl.username,
password: dbUrl.password,
host: dbUrl.hostname,
database: dbUrl.pathname.slice(1),
port: Number(dbUrl.port),
ssl: false,
});
await client.connect();
const res = await client.query(query);
await client.end();
return res.rows;
},
});
},
},
});

View file

@ -1,8 +1,14 @@
namespace Cypress {
export interface Chainable {
fillSupertokensFormAndSubmit(user: { email: string; password: string }): Chainable;
signup(user: { email: string; password: string }): Chainable;
login(user: { email: string; password: string }): Chainable;
fillSupertokensFormAndSubmit(data: { email: string; password: string }): Chainable;
signup(data: { email: string; password: string }): Chainable;
login(data: { email: string; password: string }): Chainable;
loginAndSetCookie(data: { email: string; password: string }): Chainable;
dataCy(name: string): Chainable<JQuery<HTMLElement>>;
}
}
@ -32,3 +38,32 @@ Cypress.Commands.add('login', user => {
cy.contains('Create Organization');
});
Cypress.Commands.add('loginAndSetCookie', ({ email, password }) => {
cy.request({
method: 'POST',
url: '/api/auth/signin',
body: {
formFields: [
{ id: 'email', value: email },
{ id: 'password', value: password },
],
},
}).then(response => {
const { status, headers, body } = response;
if (status !== 200) {
throw new Error(`Create session failed. ${status}.\n${JSON.stringify(body)}`);
}
const frontToken = headers['front-token'] as string;
const accessToken = headers['st-access-token'] as string;
const timeJoined = String(body.user.timeJoined);
cy.setCookie('sAccessToken', accessToken);
cy.setCookie('sFrontToken', frontToken);
cy.setCookie('st-last-access-token-update', timeJoined);
});
});
Cypress.Commands.add('dataCy', value => {
return cy.get(`[data-cy="${value}"]`);
});

View file

@ -19,7 +19,8 @@ Integration tests are based pre-built Docker images, so you can run it in 2 mode
#### Running from Source Code
**TL;DR**: Use `pnpm integration:prepare` command to setup the complete environment from locally
running integration tests.
running integration tests. You can ignore the rest of the commands in this section, if this script
worked for you, and just run `pnpm test:integration` to run the actual tests.
To run integration tests locally, from the local source code, you need to build a valid Docker
image.

View file

@ -0,0 +1,204 @@
import { graphql } from './gql';
export const FindCollectionQuery = graphql(`
query Collection($selector: TargetSelectorInput!, $id: ID!) {
target(selector: $selector) {
id
documentCollection(id: $id) {
id
name
description
}
}
}
`);
export const CreateCollectionMutation = graphql(`
mutation CreateCollection(
$selector: TargetSelectorInput!
$input: CreateDocumentCollectionInput!
) {
createDocumentCollection(selector: $selector, input: $input) {
error {
message
}
ok {
updatedTarget {
id
documentCollections {
edges {
cursor
node {
id
name
}
}
}
}
collection {
id
name
operations(first: 100) {
edges {
cursor
node {
id
name
}
cursor
}
}
}
}
}
}
`);
export const UpdateCollectionMutation = graphql(`
mutation UpdateCollection(
$selector: TargetSelectorInput!
$input: UpdateDocumentCollectionInput!
) {
updateDocumentCollection(selector: $selector, input: $input) {
error {
message
}
ok {
updatedTarget {
id
documentCollections {
edges {
node {
id
name
}
cursor
}
}
}
collection {
id
name
description
operations(first: 100) {
edges {
cursor
node {
id
name
}
}
}
}
}
}
}
`);
export const DeleteCollectionMutation = graphql(`
mutation DeleteCollection($selector: TargetSelectorInput!, $id: ID!) {
deleteDocumentCollection(selector: $selector, id: $id) {
error {
message
}
ok {
deletedId
updatedTarget {
id
documentCollections {
edges {
cursor
node {
id
}
}
}
}
}
}
}
`);
export const CreateOperationMutation = graphql(`
mutation CreateOperation(
$selector: TargetSelectorInput!
$input: CreateDocumentCollectionOperationInput!
) {
createOperationInDocumentCollection(selector: $selector, input: $input) {
error {
message
}
ok {
operation {
id
name
}
collection {
id
operations {
edges {
cursor
node {
id
}
}
}
}
}
}
}
`);
export const UpdateOperationMutation = graphql(`
mutation UpdateOperation(
$selector: TargetSelectorInput!
$input: UpdateDocumentCollectionOperationInput!
) {
updateOperationInDocumentCollection(selector: $selector, input: $input) {
error {
message
}
ok {
operation {
id
name
query
variables
headers
}
}
}
}
`);
export const DeleteOperationMutation = graphql(`
mutation DeleteOperation($selector: TargetSelectorInput!, $id: ID!) {
deleteOperationInDocumentCollection(selector: $selector, id: $id) {
error {
message
}
ok {
deletedId
updatedTarget {
id
documentCollections {
edges {
cursor
node {
id
operations {
edges {
node {
id
}
cursor
}
}
}
}
}
}
}
}
}
`);

View file

@ -8,6 +8,14 @@ import {
TargetAccessScope,
} from '@app/gql/graphql';
import { authenticate, userEmail } from './auth';
import {
CreateCollectionMutation,
CreateOperationMutation,
DeleteCollectionMutation,
DeleteOperationMutation,
UpdateCollectionMutation,
UpdateOperationMutation,
} from './collections';
import { ensureEnv } from './env';
import {
checkSchema,
@ -199,6 +207,161 @@ export function initSeed() {
.then(r => r.expectNoGraphQLErrors())
.then(r => r.deleteTokens.deletedTokens);
},
async createDocumentCollection({
name,
description,
token = ownerToken,
}: {
name: string;
description: string;
token?: string;
}) {
const result = await execute({
document: CreateCollectionMutation,
variables: {
input: {
name,
description,
},
selector: {
organization: organization.cleanId,
project: project.cleanId,
target: target.cleanId,
},
},
authToken: token,
}).then(r => r.expectNoGraphQLErrors());
return result.createDocumentCollection;
},
async updateDocumentCollection({
collectionId,
name,
description,
token = ownerToken,
}: {
collectionId: string;
name: string;
description: string;
token?: string;
}) {
const result = await execute({
document: UpdateCollectionMutation,
variables: {
input: {
collectionId,
name,
description,
},
selector: {
organization: organization.cleanId,
project: project.cleanId,
target: target.cleanId,
},
},
authToken: token,
}).then(r => r.expectNoGraphQLErrors());
return result.updateDocumentCollection;
},
async deleteDocumentCollection({
collectionId,
token = ownerToken,
}: {
collectionId: string;
token?: string;
}) {
const result = await execute({
document: DeleteCollectionMutation,
variables: {
id: collectionId,
selector: {
organization: organization.cleanId,
project: project.cleanId,
target: target.cleanId,
},
},
authToken: token,
}).then(r => r.expectNoGraphQLErrors());
return result.deleteDocumentCollection;
},
async createOperationInCollection(input: {
collectionId: string;
name: string;
query: string;
variables?: string;
headers?: string;
token?: string;
}) {
const result = await execute({
document: CreateOperationMutation,
variables: {
input: {
collectionId: input.collectionId,
name: input.name,
query: input.query,
headers: input.headers,
variables: input.variables,
},
selector: {
organization: organization.cleanId,
project: project.cleanId,
target: target.cleanId,
},
},
authToken: input.token || ownerToken,
}).then(r => r.expectNoGraphQLErrors());
return result.createOperationInDocumentCollection;
},
async deleteOperationInCollection(input: { operationId: string; token?: string }) {
const result = await execute({
document: DeleteOperationMutation,
variables: {
id: input.operationId,
selector: {
organization: organization.cleanId,
project: project.cleanId,
target: target.cleanId,
},
},
authToken: input.token || ownerToken,
}).then(r => r.expectNoGraphQLErrors());
return result.deleteOperationInDocumentCollection;
},
async updateOperationInCollection(input: {
operationId: string;
collectionId: string;
name: string;
query: string;
variables?: string;
headers?: string;
token?: string;
}) {
const result = await execute({
document: UpdateOperationMutation,
variables: {
input: {
operationId: input.operationId,
collectionId: input.collectionId,
name: input.name,
query: input.query,
headers: input.headers,
variables: input.variables,
},
selector: {
organization: organization.cleanId,
project: project.cleanId,
target: target.cleanId,
},
},
authToken: input.token || ownerToken,
}).then(r => r.expectNoGraphQLErrors());
return result.updateOperationInDocumentCollection;
},
async createToken({
targetScopes = [TargetAccessScope.RegistryRead, TargetAccessScope.RegistryWrite],
projectScopes = [],

View file

@ -0,0 +1,358 @@
/* eslint-disable @typescript-eslint/no-non-null-asserted-optional-chain */
import { ProjectType, TargetAccessScope } from '@app/gql/graphql';
import { initSeed } from '../../../testkit/seed';
describe('Document Collections', () => {
describe('CRUD', () => {
it.concurrent('Create, update and delete a Collection', async () => {
const { createDocumentCollection, updateDocumentCollection, deleteDocumentCollection } =
await initSeed()
.createOwner()
.then(r => r.createOrg())
.then(r => r.createProject(ProjectType.Single));
// Create a collection
const createDocumentCollectionResult = await createDocumentCollection({
name: 'My Collection',
description: 'My favorite queries',
});
expect(createDocumentCollectionResult.error).toBeNull();
expect(createDocumentCollectionResult.ok?.collection.id).toBeDefined();
expect(
createDocumentCollectionResult.ok?.updatedTarget.documentCollections.edges.length,
).toBe(1);
// Update the collection
const updateDocumentCollectionResult = await updateDocumentCollection({
collectionId: createDocumentCollectionResult.ok?.collection.id!,
name: 'Best Queries #3',
description: 'My favorite queries updated',
});
expect(updateDocumentCollectionResult.error).toBeNull();
expect(updateDocumentCollectionResult.ok?.collection.id).toBeDefined();
expect(updateDocumentCollectionResult.ok?.collection.name).toBe('Best Queries #3');
expect(updateDocumentCollectionResult.ok?.collection.description).toBe(
'My favorite queries updated',
);
expect(
updateDocumentCollectionResult.ok?.updatedTarget.documentCollections.edges.length,
).toBe(1);
// Delete the collection
const deleteDocumentCollectionResult = await deleteDocumentCollection({
collectionId: createDocumentCollectionResult.ok?.collection.id!,
});
expect(deleteDocumentCollectionResult.error).toBeNull();
expect(deleteDocumentCollectionResult.ok?.deletedId).toBe(
updateDocumentCollectionResult.ok?.collection.id,
);
expect(
deleteDocumentCollectionResult.ok?.updatedTarget.documentCollections.edges.length,
).toBe(0);
});
it.concurrent('Create, update and delete an operation inside a collection', async () => {
const {
createDocumentCollection,
createOperationInCollection,
updateOperationInCollection,
deleteOperationInCollection,
} = await initSeed()
.createOwner()
.then(r => r.createOrg())
.then(r => r.createProject(ProjectType.Single));
const createDocumentCollectionResult = await createDocumentCollection({
name: 'My Collection',
description: 'My favorite queries',
});
expect(createDocumentCollectionResult.error).toBeNull();
const collectionId = createDocumentCollectionResult.ok?.collection.id!;
expect(collectionId).toBeDefined();
const createResult = await createOperationInCollection({
collectionId,
name: 'My Operation',
query: 'query { hello }',
});
expect(createResult.error).toBeNull();
const operationId = createResult.ok?.operation?.id!;
expect(operationId).toBeDefined();
expect(createResult.ok?.operation?.name).toBe('My Operation');
expect(createResult.ok?.collection.operations.edges.length).toBe(1);
const updateResult = await updateOperationInCollection({
collectionId,
operationId,
name: 'My Updated Operation',
query: 'query { hello world }',
variables: JSON.stringify({
id: '1',
}),
headers: JSON.stringify({
Key: '3',
}),
});
expect(updateResult.error).toBeNull();
expect(updateResult.ok?.operation?.id).toBeDefined();
expect(updateResult.ok?.operation?.name).toBe('My Updated Operation');
expect(updateResult.ok?.operation?.query).toBe('query { hello world }');
expect(updateResult.ok?.operation?.headers).toBe(
JSON.stringify({
Key: '3',
}),
);
expect(updateResult.ok?.operation?.variables).toBe(
JSON.stringify({
id: '1',
}),
);
const deleteResult = await deleteOperationInCollection({
operationId,
});
expect(deleteResult.error).toBeNull();
expect(deleteResult.ok?.deletedId).toBe(operationId);
expect(deleteResult.ok?.updatedTarget.documentCollections.edges.length).toBe(1);
expect(
deleteResult.ok?.updatedTarget.documentCollections.edges[0].node.operations.edges.length,
).toBe(0);
});
describe('Permissions Check', () => {
it('Prevent creating collection without the write permission to the target', async () => {
const { createDocumentCollection, createToken } = await initSeed()
.createOwner()
.then(r => r.createOrg())
.then(r => r.createProject(ProjectType.Single));
const { secret: readOnlyToken } = await createToken({
targetScopes: [TargetAccessScope.Read],
organizationScopes: [],
projectScopes: [],
});
// Create a collection
await expect(
createDocumentCollection({
name: 'My Collection',
description: 'My favorite queries',
token: readOnlyToken,
}),
).rejects.toMatchInlineSnapshot(`
[Error: Expected GraphQL response to have no errors, but got 1 errors:
No access (reason: "Missing target:settings permission")
endpoint: http://localhost:8082/graphql
query:
mutation CreateCollection($selector: TargetSelectorInput!, $input: CreateDocumentCollectionInput!) {
createDocumentCollection(selector: $selector, input: $input) {
error {
message
}
ok {
updatedTarget {
id
documentCollections {
edges {
cursor
node {
id
name
}
}
}
}
collection {
id
name
operations(first: 100) {
edges {
cursor
node {
id
name
}
cursor
}
}
}
}
}
}
body:
{
"errors": [
{
"message": "No access (reason: \\"Missing target:settings permission\\")",
"locations": [
{
"line": 2,
"column": 3
}
],
"path": [
"createDocumentCollection"
]
}
],
"data": null
}]
`);
});
it('Prevent updating collection without the write permission to the target', async () => {
const { createDocumentCollection, updateDocumentCollection, createToken } = await initSeed()
.createOwner()
.then(r => r.createOrg())
.then(r => r.createProject(ProjectType.Single));
const createResult = await createDocumentCollection({
name: 'My Collection',
description: 'My favorite queries',
});
const { secret: readOnlyToken } = await createToken({
targetScopes: [TargetAccessScope.Read],
organizationScopes: [],
projectScopes: [],
});
await expect(
updateDocumentCollection({
collectionId: createResult.ok?.collection.id!,
token: readOnlyToken,
name: 'My Collection',
description: 'My favorite queries',
}),
).rejects.toMatchInlineSnapshot(`
[Error: Expected GraphQL response to have no errors, but got 1 errors:
No access (reason: "Missing target:settings permission")
endpoint: http://localhost:8082/graphql
query:
mutation UpdateCollection($selector: TargetSelectorInput!, $input: UpdateDocumentCollectionInput!) {
updateDocumentCollection(selector: $selector, input: $input) {
error {
message
}
ok {
updatedTarget {
id
documentCollections {
edges {
node {
id
name
}
cursor
}
}
}
collection {
id
name
description
operations(first: 100) {
edges {
cursor
node {
id
name
}
}
}
}
}
}
}
body:
{
"errors": [
{
"message": "No access (reason: \\"Missing target:settings permission\\")",
"locations": [
{
"line": 2,
"column": 3
}
],
"path": [
"updateDocumentCollection"
]
}
],
"data": null
}]
`);
});
it('Prevent deleting collection without the write permission to the target', async () => {
const { createDocumentCollection, deleteDocumentCollection, createToken } = await initSeed()
.createOwner()
.then(r => r.createOrg())
.then(r => r.createProject(ProjectType.Single));
const createResult = await createDocumentCollection({
name: 'My Collection',
description: 'My favorite queries',
});
const { secret: readOnlyToken } = await createToken({
targetScopes: [TargetAccessScope.Read],
organizationScopes: [],
projectScopes: [],
});
await expect(
deleteDocumentCollection({
collectionId: createResult.ok?.collection.id!,
token: readOnlyToken,
}),
).rejects.toMatchInlineSnapshot(`
[Error: Expected GraphQL response to have no errors, but got 1 errors:
No access (reason: "Missing target:settings permission")
endpoint: http://localhost:8082/graphql
query:
mutation DeleteCollection($selector: TargetSelectorInput!, $id: ID!) {
deleteDocumentCollection(selector: $selector, id: $id) {
error {
message
}
ok {
deletedId
updatedTarget {
id
documentCollections {
edges {
cursor
node {
id
}
}
}
}
}
}
}
body:
{
"errors": [
{
"message": "No access (reason: \\"Missing target:settings permission\\")",
"locations": [
{
"line": 2,
"column": 3
}
],
"path": [
"deleteDocumentCollection"
]
}
],
"data": null
}]
`);
});
});
});
});

View file

@ -79,6 +79,7 @@
"husky": "8.0.3",
"jest-snapshot-serializer-raw": "1.2.0",
"lint-staged": "13.2.2",
"pg": "^8.10.0",
"prettier": "2.8.8",
"prettier-plugin-sql": "0.14.0",
"prettier-plugin-tailwindcss": "0.3.0",

View file

@ -0,0 +1,35 @@
CREATE TABLE public."document_collections" (
"id" uuid NOT NULL DEFAULT uuid_generate_v4(),
"title" text NOT NULL,
"description" text,
"target_id" uuid NOT NULL REFERENCES public."targets"("id") ON DELETE CASCADE,
"created_by_user_id" uuid REFERENCES public."users"("id") ON DELETE SET NULL,
"created_at" timestamptz NOT NULL DEFAULT now(),
"updated_at" timestamptz NOT NULL DEFAULT now(),
PRIMARY KEY ("id")
);
CREATE INDEX "document_collections_connection_pagination" ON "document_collections" (
"target_id" ASC,
"created_at" DESC,
"id" DESC
);
CREATE TABLE public."document_collection_documents" (
"id" uuid NOT NULL DEFAULT uuid_generate_v4(),
"title" text NOT NULL,
"contents" text NOT NULL,
"variables" text,
"headers" text,
"created_by_user_id" uuid REFERENCES public."users"("id") ON DELETE SET NULL,
"document_collection_id" uuid NOT NULL REFERENCES public."document_collections"("id") ON DELETE CASCADE,
"created_at" timestamptz NOT NULL DEFAULT now(),
"updated_at" timestamptz NOT NULL DEFAULT now(),
PRIMARY KEY ("id")
);
CREATE INDEX "document_collection_documents_connection_pagination" ON "document_collection_documents" (
"document_collection_id" ASC,
"created_at" DESC,
"id" DESC
);

View file

@ -0,0 +1 @@
raise 'down migration not implemented'

View file

@ -42,7 +42,8 @@
"prom-client": "14.2.0",
"redlock": "5.0.0-beta.2",
"supertokens-node": "13.4.2",
"zod": "3.21.4"
"zod": "3.21.4",
"zod-validation-error": "1.3.0"
},
"devDependencies": {
"@graphql-hive/core": "0.2.3",

View file

@ -9,6 +9,7 @@ import { billingModule } from './modules/billing';
import { BILLING_CONFIG, BillingConfig } from './modules/billing/providers/tokens';
import { cdnModule } from './modules/cdn';
import { CDN_CONFIG, CDNConfig } from './modules/cdn/providers/tokens';
import { collectionModule } from './modules/collection';
import { feedbackModule } from './modules/feedback';
import { FEEDBACK_SLACK_CHANNEL, FEEDBACK_SLACK_TOKEN } from './modules/feedback/providers/tokens';
import { integrationsModule } from './modules/integrations';
@ -84,6 +85,7 @@ const modules = [
billingModule,
oidcIntegrationsModule,
schemaPolicyModule,
collectionModule,
];
export function createRegistry({

View file

@ -33,9 +33,7 @@ export class AlertsManager {
private projectManager: ProjectManager,
private storage: Storage,
) {
this.logger = logger.child({
source: 'AlertsManager',
});
this.logger = logger.child({ source: 'AlertsManager' });
}
async addChannel(input: AlertsModule.AddAlertChannelInput): Promise<AlertChannel> {
@ -174,14 +172,8 @@ export class AlertsManager {
});
const [channels, alerts] = await Promise.all([
this.getChannels({
organization,
project,
}),
this.getAlerts({
organization,
project,
}),
this.getChannels({ organization, project }),
this.getAlerts({ organization, project }),
]);
const matchingAlerts = alerts.filter(

View file

@ -0,0 +1,12 @@
import { createModule } from 'graphql-modules';
import { CollectionProvider } from './providers/collection.provider';
import { resolvers } from './resolvers';
import { typeDefs } from './module.graphql';
export const collectionModule = createModule({
id: 'collection',
dirname: __dirname,
typeDefs,
resolvers,
providers: [CollectionProvider],
});

View file

@ -0,0 +1,164 @@
import { gql } from 'graphql-modules';
export const typeDefs = gql`
type DocumentCollection {
id: ID!
name: String!
description: String
createdAt: DateTime!
updatedAt: DateTime!
createdBy: User!
operations(first: Int = 100, after: String = null): DocumentCollectionOperationsConnection!
}
type DocumentCollectionEdge {
node: DocumentCollection!
cursor: String!
}
type DocumentCollectionConnection {
edges: [DocumentCollectionEdge!]!
pageInfo: PageInfo!
}
type DocumentCollectionOperationsConnection {
edges: [DocumentCollectionOperationEdge!]!
pageInfo: PageInfo!
}
type DocumentCollectionOperation {
id: ID!
name: String!
query: String!
variables: String
headers: String
createdAt: DateTime!
updatedAt: DateTime!
collection: DocumentCollection!
}
type DocumentCollectionOperationEdge {
node: DocumentCollectionOperation!
cursor: String!
}
input CreateDocumentCollectionInput {
name: String!
description: String
}
input UpdateDocumentCollectionInput {
collectionId: ID!
name: String!
description: String
}
input CreateDocumentCollectionOperationInput {
collectionId: ID!
name: String!
query: String!
variables: String
headers: String
}
input UpdateDocumentCollectionOperationInput {
operationId: ID!
collectionId: ID!
name: String!
query: String!
variables: String
headers: String
}
extend type Mutation {
createOperationInDocumentCollection(
selector: TargetSelectorInput!
input: CreateDocumentCollectionOperationInput!
): ModifyDocumentCollectionOperationResult!
updateOperationInDocumentCollection(
selector: TargetSelectorInput!
input: UpdateDocumentCollectionOperationInput!
): ModifyDocumentCollectionOperationResult!
deleteOperationInDocumentCollection(
selector: TargetSelectorInput!
id: ID!
): DeleteDocumentCollectionOperationResult!
createDocumentCollection(
selector: TargetSelectorInput!
input: CreateDocumentCollectionInput!
): ModifyDocumentCollectionResult!
updateDocumentCollection(
selector: TargetSelectorInput!
input: UpdateDocumentCollectionInput!
): ModifyDocumentCollectionResult!
deleteDocumentCollection(
selector: TargetSelectorInput!
id: ID!
): DeleteDocumentCollectionResult!
}
type ModifyDocumentCollectionError implements Error {
message: String!
}
"""
@oneOf
"""
type DeleteDocumentCollectionResult {
ok: DeleteDocumentCollectionOkPayload
error: ModifyDocumentCollectionError
}
type DeleteDocumentCollectionOkPayload {
updatedTarget: Target!
deletedId: ID!
}
"""
@oneOf
"""
type DeleteDocumentCollectionOperationResult {
ok: DeleteDocumentCollectionOperationOkPayload
error: ModifyDocumentCollectionError
}
type DeleteDocumentCollectionOperationOkPayload {
updatedTarget: Target!
updatedCollection: DocumentCollection!
deletedId: ID!
}
"""
@oneOf
"""
type ModifyDocumentCollectionResult {
ok: ModifyDocumentCollectionOkPayload
error: ModifyDocumentCollectionError
}
type ModifyDocumentCollectionOkPayload {
collection: DocumentCollection!
updatedTarget: Target!
}
"""
@oneOf
"""
type ModifyDocumentCollectionOperationResult {
ok: ModifyDocumentCollectionOperationOkPayload
error: ModifyDocumentCollectionError
}
type ModifyDocumentCollectionOperationOkPayload {
operation: DocumentCollectionOperation!
collection: DocumentCollection!
updatedTarget: Target!
}
extend type Target {
documentCollection(id: ID!): DocumentCollection
documentCollections(first: Int = 100, after: String = null): DocumentCollectionConnection!
documentCollectionOperation(id: ID!): DocumentCollectionOperation
}
`;

View file

@ -0,0 +1,99 @@
import { Injectable, Scope } from 'graphql-modules';
import {
CreateDocumentCollectionInput,
CreateDocumentCollectionOperationInput,
UpdateDocumentCollectionInput,
UpdateDocumentCollectionOperationInput,
} from '@/graphql';
import { AuthManager } from '../../auth/providers/auth-manager';
import { Logger } from '../../shared/providers/logger';
import { Storage } from '../../shared/providers/storage';
@Injectable({
global: true,
scope: Scope.Operation,
})
export class CollectionProvider {
private logger: Logger;
constructor(logger: Logger, private storage: Storage, private authManager: AuthManager) {
this.logger = logger.child({ source: 'CollectionProvider' });
}
getCollections(targetId: string, first: number, cursor: string | null) {
return this.storage.getPaginatedDocumentCollectionsForTarget({
targetId,
first,
cursor,
});
}
getCollection(id: string) {
return this.storage.getDocumentCollection({ id });
}
getOperations(documentCollectionId: string, first: number, cursor: string | null) {
return this.storage.getPaginatedDocumentsForDocumentCollection({
documentCollectionId,
first,
cursor,
});
}
getOperation(id: string) {
return this.storage.getDocumentCollectionDocument({ id });
}
async createCollection(
targetId: string,
{ name, description }: Pick<CreateDocumentCollectionInput, 'description' | 'name'>,
) {
const currentUser = await this.authManager.getCurrentUser();
return this.storage.createDocumentCollection({
createdByUserId: currentUser.id,
title: name,
description: description || '',
targetId,
});
}
deleteCollection(id: string) {
return this.storage.deleteDocumentCollection({ documentCollectionId: id });
}
async createOperation(input: CreateDocumentCollectionOperationInput) {
const currentUser = await this.authManager.getCurrentUser();
return this.storage.createDocumentCollectionDocument({
documentCollectionId: input.collectionId,
title: input.name,
contents: input.query,
variables: input.variables ?? null,
headers: input.headers ?? null,
createdByUserId: currentUser.id,
});
}
updateOperation(input: UpdateDocumentCollectionOperationInput) {
return this.storage.updateDocumentCollectionDocument({
documentCollectionDocumentId: input.operationId,
title: input.name,
contents: input.query,
variables: input.variables ?? null,
headers: input.headers ?? null,
});
}
updateCollection(input: UpdateDocumentCollectionInput) {
return this.storage.updateDocumentCollection({
documentCollectionId: input.collectionId,
description: input.description || null,
title: input.name,
});
}
deleteOperation(id: string) {
return this.storage.deleteDocumentCollectionDocument({ documentCollectionDocumentId: id });
}
}

View file

@ -0,0 +1,251 @@
import { Injector } from 'graphql-modules';
import * as zod from 'zod';
import { fromZodError } from 'zod-validation-error';
import { TargetSelectorInput } from '../../__generated__/types';
import { AuthManager } from '../auth/providers/auth-manager';
import { TargetAccessScope } from '../auth/providers/scopes';
import { IdTranslator } from '../shared/providers/id-translator';
import { Storage } from '../shared/providers/storage';
import { CollectionModule } from './__generated__/types';
import { CollectionProvider } from './providers/collection.provider';
const MAX_INPUT_LENGTH = 5000;
// The following validates the length and the validity of the JSON object incoming as string.
const inputObjectSchema = zod
.string()
.max(MAX_INPUT_LENGTH)
.optional()
.nullable()
.refine(v => {
if (!v) {
return true;
}
try {
JSON.parse(v);
return true;
} catch {
return false;
}
});
const OperationValidationInputModel = zod
.object({
collectionId: zod.string(),
name: zod.string().min(1).max(100),
query: zod.string().min(1).max(MAX_INPUT_LENGTH),
variables: inputObjectSchema,
headers: inputObjectSchema,
})
.partial()
.passthrough();
async function validateTargetAccess(
injector: Injector,
selector: TargetSelectorInput,
scope: TargetAccessScope = TargetAccessScope.REGISTRY_READ,
) {
const translator = injector.get(IdTranslator);
const [organization, project, target] = await Promise.all([
translator.translateOrganizationId(selector),
translator.translateProjectId(selector),
translator.translateTargetId(selector),
]);
await injector.get(AuthManager).ensureTargetAccess({
organization,
project,
target,
scope,
});
return await injector.get(Storage).getTarget({ target, organization, project });
}
export const resolvers: CollectionModule.Resolvers = {
DocumentCollection: {
id: root => root.id,
name: root => root.title,
description: root => root.description,
operations: (root, args, { injector }) =>
injector.get(CollectionProvider).getOperations(root.id, args.first, args.after),
},
DocumentCollectionOperation: {
name: op => op.title,
query: op => op.contents,
async collection(op, args, { injector }) {
const collection = await injector
.get(CollectionProvider)
.getCollection(op.documentCollectionId);
// This should not happen, but we do want to flag this as an unexpected error.
if (!collection) {
throw new Error('Collection not found');
}
return collection;
},
},
Target: {
documentCollections: (target, args, { injector }) =>
injector.get(CollectionProvider).getCollections(target.id, args.first, args.after),
documentCollectionOperation: (_, args, { injector }) =>
injector.get(CollectionProvider).getOperation(args.id),
documentCollection: (_, args, { injector }) =>
injector.get(CollectionProvider).getCollection(args.id),
},
Mutation: {
async createDocumentCollection(_, { selector, input }, { injector }) {
const target = await validateTargetAccess(injector, selector, TargetAccessScope.SETTINGS);
const result = await injector.get(CollectionProvider).createCollection(target.id, input);
return {
ok: {
__typename: 'ModifyDocumentCollectionOkPayload',
collection: result,
updatedTarget: target,
},
};
},
async updateDocumentCollection(_, { selector, input }, { injector }) {
const target = await validateTargetAccess(injector, selector, TargetAccessScope.SETTINGS);
const result = await injector.get(CollectionProvider).updateCollection(input);
if (!result) {
return {
error: {
__typename: 'ModifyDocumentCollectionError',
message: 'Failed to locate a document collection',
},
};
}
return {
ok: {
__typename: 'ModifyDocumentCollectionOkPayload',
collection: result,
updatedTarget: target,
},
};
},
async deleteDocumentCollection(_, { selector, id }, { injector }) {
const target = await validateTargetAccess(injector, selector, TargetAccessScope.SETTINGS);
await injector.get(CollectionProvider).deleteCollection(id);
return {
ok: {
__typename: 'DeleteDocumentCollectionOkPayload',
deletedId: id,
updatedTarget: target,
},
};
},
async createOperationInDocumentCollection(_, { selector, input }, { injector }) {
try {
OperationValidationInputModel.parse(input);
const target = await validateTargetAccess(injector, selector, TargetAccessScope.SETTINGS);
const result = await injector.get(CollectionProvider).createOperation(input);
const collection = await injector
.get(CollectionProvider)
.getCollection(result.documentCollectionId);
if (!result || !collection) {
return {
error: {
__typename: 'ModifyDocumentCollectionError',
message: 'Failed to locate a document collection',
},
};
}
return {
ok: {
__typename: 'ModifyDocumentCollectionOperationOkPayload',
operation: result,
updatedTarget: target,
collection,
},
};
} catch (e) {
if (e instanceof zod.ZodError) {
return {
error: {
__typename: 'ModifyDocumentCollectionError',
message: fromZodError(e).message,
},
};
}
throw e;
}
},
async updateOperationInDocumentCollection(_, { selector, input }, { injector }) {
try {
OperationValidationInputModel.parse(input);
const target = await validateTargetAccess(injector, selector, TargetAccessScope.SETTINGS);
const result = await injector.get(CollectionProvider).updateOperation(input);
if (!result) {
return {
error: {
__typename: 'ModifyDocumentCollectionError',
message: 'Failed to locate a document collection',
},
};
}
const collection = await injector
.get(CollectionProvider)
.getCollection(result.documentCollectionId);
return {
ok: {
__typename: 'ModifyDocumentCollectionOperationOkPayload',
operation: result,
updatedTarget: target,
collection: collection!,
},
};
} catch (e) {
if (e instanceof zod.ZodError) {
return {
error: {
__typename: 'ModifyDocumentCollectionError',
message: fromZodError(e).message,
},
};
}
throw e;
}
},
async deleteOperationInDocumentCollection(_, { selector, id }, { injector }) {
const target = await validateTargetAccess(injector, selector, TargetAccessScope.SETTINGS);
const operation = await injector.get(CollectionProvider).getOperation(id);
if (!operation) {
return {
error: {
__typename: 'ModifyDocumentCollectionError',
message: 'Failed to locate a operation',
},
};
}
const collection = await injector
.get(CollectionProvider)
.getCollection(operation.documentCollectionId);
await injector.get(CollectionProvider).deleteOperation(id);
return {
ok: {
__typename: 'DeleteDocumentCollectionOperationOkPayload',
deletedId: id,
updatedTarget: target,
updatedCollection: collection!,
},
};
},
},
};

View file

@ -207,6 +207,7 @@ export class OperationsManager {
percentage: total === 0 ? 0 : (totalField / total) * 100,
};
}
async readFieldListStats({
fields,
period,
@ -575,9 +576,7 @@ export class OperationsManager {
return false;
}
const total = await this.reader.countOperationsForTargets({
targets,
});
const total = await this.reader.countOperationsForTargets({ targets });
if (total > 0) {
await this.storage.completeGetStartedStep({

View file

@ -12,11 +12,15 @@ import type {
AlertChannel,
CDNAccessToken,
DeletedCompositeSchema,
DocumentCollection,
DocumentCollectionOperation,
Member,
OIDCIntegration,
Organization,
OrganizationBilling,
OrganizationInvitation,
PaginatedDocumentCollectionOperations,
PaginatedDocumentCollections,
PersistedOperation,
Project,
Schema,
@ -94,27 +98,34 @@ export interface Storage {
reservedNames: string[];
},
): Promise<Organization | never>;
deleteOrganization(_: OrganizationSelector): Promise<
| (Organization & {
tokens: string[];
})
| never
>;
updateOrganizationName(
_: OrganizationSelector & Pick<Organization, 'name' | 'cleanId'> & { user: string },
): Promise<Organization | never>;
updateOrganizationPlan(
_: OrganizationSelector & Pick<Organization, 'billingPlan'>,
): Promise<Organization | never>;
updateOrganizationRateLimits(
_: OrganizationSelector & Pick<Organization, 'monthlyRateLimit'>,
): Promise<Organization | never>;
createOrganizationInvitation(
_: OrganizationSelector & { email: string },
): Promise<OrganizationInvitation | never>;
deleteOrganizationInvitationByEmail(
_: OrganizationSelector & { email: string },
): Promise<OrganizationInvitation | null>;
createOrganizationTransferRequest(
_: OrganizationSelector & {
user: string;
@ -122,6 +133,7 @@ export interface Storage {
): Promise<{
code: string;
}>;
getOrganizationTransferRequest(
_: OrganizationSelector & {
code: string;
@ -130,6 +142,7 @@ export interface Storage {
): Promise<{
code: string;
} | null>;
answerOrganizationTransferRequest(
_: OrganizationSelector & {
code: string;
@ -142,21 +155,29 @@ export interface Storage {
): Promise<void>;
getOrganizationMembers(_: OrganizationSelector): Promise<readonly Member[] | never>;
getOrganizationInvitations(_: OrganizationSelector): Promise<readonly OrganizationInvitation[]>;
getOrganizationOwnerId(_: OrganizationSelector): Promise<string | null>;
getOrganizationOwner(_: OrganizationSelector): Promise<Member | never>;
getOrganizationMember(_: OrganizationSelector & { user: string }): Promise<Member | null>;
getOrganizationMemberAccessPairs(
_: readonly (OrganizationSelector & { user: string })[],
): Promise<
ReadonlyArray<ReadonlyArray<OrganizationAccessScope | ProjectAccessScope | TargetAccessScope>>
>;
hasOrganizationMemberPairs(
_: readonly (OrganizationSelector & { user: string })[],
): Promise<readonly boolean[]>;
hasOrganizationProjectMemberPairs(
_: readonly (ProjectSelector & { user: string })[],
): Promise<readonly boolean[]>;
addOrganizationMemberViaInvitationCode(
_: OrganizationSelector & {
code: string;
@ -164,7 +185,9 @@ export interface Storage {
scopes: ReadonlyArray<OrganizationAccessScope | ProjectAccessScope | TargetAccessScope>;
},
): Promise<void>;
deleteOrganizationMembers(_: OrganizationSelector & { users: readonly string[] }): Promise<void>;
updateOrganizationMemberAccess(
_: OrganizationSelector & {
user: string;
@ -175,31 +198,41 @@ export interface Storage {
getPersistedOperationId(_: PersistedOperationSelector): Promise<string | never>;
getProject(_: ProjectSelector): Promise<Project | never>;
getProjectId(_: ProjectSelector): Promise<string | never>;
getProjectByCleanId(_: { cleanId: string } & OrganizationSelector): Promise<Project | null>;
getProjects(_: OrganizationSelector): Promise<Project[] | never>;
createProject(
_: Pick<Project, 'name' | 'cleanId' | 'type'> & OrganizationSelector,
): Promise<Project | never>;
deleteProject(_: ProjectSelector): Promise<
| (Project & {
tokens: string[];
})
| never
>;
updateProjectName(
_: ProjectSelector & Pick<Project, 'name' | 'cleanId'> & { user: string },
): Promise<Project | never>;
updateProjectGitRepository(
_: ProjectSelector & Pick<Project, 'gitRepository'>,
): Promise<Project | never>;
enableExternalSchemaComposition(
_: ProjectSelector & {
endpoint: string;
encryptedSecret: string;
},
): Promise<Project>;
disableExternalSchemaComposition(_: ProjectSelector): Promise<Project>;
updateProjectRegistryModel(
_: ProjectSelector & {
model: RegistryModel;
@ -207,33 +240,44 @@ export interface Storage {
): Promise<Project>;
getTargetId(_: TargetSelector & { useIds?: boolean }): Promise<string | never>;
getTargetByCleanId(
_: {
cleanId: string;
} & ProjectSelector,
): Promise<Target | null>;
createTarget(_: Pick<Target, 'cleanId' | 'name'> & ProjectSelector): Promise<Target | never>;
updateTargetName(
_: TargetSelector & Pick<Project, 'name' | 'cleanId'> & { user: string },
): Promise<Target | never>;
deleteTarget(_: TargetSelector): Promise<
| (Target & {
tokens: string[];
})
| never
>;
getTarget(_: TargetSelector): Promise<Target | never>;
getTargets(_: ProjectSelector): Promise<readonly Target[]>;
getTargetIdsOfOrganization(_: OrganizationSelector): Promise<readonly string[]>;
getTargetSettings(_: TargetSelector): Promise<TargetSettings | never>;
setTargetValidation(
_: TargetSelector & { enabled: boolean },
): Promise<TargetSettings['validation'] | never>;
updateTargetValidationSettings(
_: TargetSelector & Omit<TargetSettings['validation'], 'enabled'>,
): Promise<TargetSettings['validation'] | never>;
hasSchema(_: TargetSelector): Promise<boolean>;
getLatestSchemas(
_: {
onlyComposable?: boolean;
@ -243,9 +287,13 @@ export interface Storage {
version: string;
valid: boolean;
} | null>;
getLatestValidVersion(_: TargetSelector): Promise<SchemaVersion | never>;
getMaybeLatestValidVersion(_: TargetSelector): Promise<SchemaVersion | null | never>;
getLatestVersion(_: TargetSelector): Promise<SchemaVersion | never>;
getMaybeLatestVersion(_: TargetSelector): Promise<SchemaVersion | null>;
getMatchingServiceSchemaOfVersions(versions: {
@ -281,6 +329,7 @@ export interface Storage {
}
| never
>;
getVersion(_: TargetSelector & { version: string }): Promise<SchemaVersion | never>;
deleteSchema(
_: {
@ -389,27 +438,35 @@ export interface Storage {
deletePersistedOperation(_: PersistedOperationSelector): Promise<PersistedOperation | never>;
addSlackIntegration(_: OrganizationSelector & { token: string }): Promise<void>;
deleteSlackIntegration(_: OrganizationSelector): Promise<void>;
getSlackIntegrationToken(_: OrganizationSelector): Promise<string | null | undefined>;
addGitHubIntegration(_: OrganizationSelector & { installationId: string }): Promise<void>;
deleteGitHubIntegration(_: OrganizationSelector): Promise<void>;
getGitHubIntegrationInstallationId(_: OrganizationSelector): Promise<string | null | undefined>;
addAlertChannel(_: AddAlertChannelInput): Promise<AlertChannel>;
deleteAlertChannels(
_: ProjectSelector & {
channels: readonly string[];
},
): Promise<readonly AlertChannel[]>;
getAlertChannels(_: ProjectSelector): Promise<readonly AlertChannel[]>;
addAlert(_: AddAlertInput): Promise<Alert>;
deleteAlerts(
_: ProjectSelector & {
alerts: readonly string[];
},
): Promise<readonly Alert[]>;
getAlerts(_: ProjectSelector): Promise<readonly Alert[]>;
adminGetStats(period: { from: Date; to: Date }): Promise<
@ -448,12 +505,15 @@ export interface Storage {
>;
getBillingParticipants(): Promise<ReadonlyArray<OrganizationBilling>>;
getOrganizationBilling(_: OrganizationSelector): Promise<OrganizationBilling | null>;
deleteOrganizationBilling(_: OrganizationSelector): Promise<void>;
createOrganizationBilling(_: OrganizationBilling): Promise<OrganizationBilling>;
getBaseSchema(_: TargetSelector): Promise<string | null>;
updateBaseSchema(_: TargetSelector, base: string | null): Promise<void>;
completeGetStartedStep(
@ -463,7 +523,9 @@ export interface Storage {
): Promise<void>;
getOIDCIntegrationForOrganization(_: { organizationId: string }): Promise<OIDCIntegration | null>;
getOIDCIntegrationById(_: { oidcIntegrationId: string }): Promise<OIDCIntegration | null>;
createOIDCIntegrationForOrganization(_: {
organizationId: string;
clientId: string;
@ -472,6 +534,7 @@ export interface Storage {
userinfoEndpoint: string;
authorizationEndpoint: string;
}): Promise<{ type: 'ok'; oidcIntegration: OIDCIntegration } | { type: 'error'; reason: string }>;
updateOIDCIntegration(_: {
oidcIntegrationId: string;
clientId: string | null;
@ -480,6 +543,7 @@ export interface Storage {
userinfoEndpoint: string | null;
authorizationEndpoint: string | null;
}): Promise<OIDCIntegration>;
deleteOIDCIntegration(_: { oidcIntegrationId: string }): Promise<void>;
createCDNAccessToken(_: {
@ -527,6 +591,73 @@ export interface Storage {
findInheritedPolicies(selector: ProjectSelector): Promise<SchemaPolicy[]>;
getSchemaPolicyForOrganization(organizationId: string): Promise<SchemaPolicy | null>;
getSchemaPolicyForProject(projectId: string): Promise<SchemaPolicy | null>;
/** Document Collections */
getPaginatedDocumentCollectionsForTarget(_: {
targetId: string;
first: number | null;
cursor: null | string;
}): Promise<PaginatedDocumentCollections>;
createDocumentCollection(_: {
targetId: string;
title: string;
description: string;
createdByUserId: string | null;
}): Promise<DocumentCollection>;
/**
* Returns null if the document collection does not exist (did not get deleted).
* Returns the id of the deleted document collection if it got deleted
*/
deleteDocumentCollection(_: { documentCollectionId: string }): Promise<string | null>;
/**
* Returns null if the document collection does not exist (did not get updated).
*/
updateDocumentCollection(_: {
documentCollectionId: string;
title: string | null;
description: string | null;
}): Promise<DocumentCollection | null>;
getDocumentCollection(_: { id: string }): Promise<DocumentCollection | null>;
getPaginatedDocumentsForDocumentCollection(_: {
documentCollectionId: string;
first: number | null;
cursor: null | string;
}): Promise<PaginatedDocumentCollectionOperations>;
createDocumentCollectionDocument(_: {
documentCollectionId: string;
title: string;
contents: string;
variables: string | null;
headers: string | null;
createdByUserId: string | null;
}): Promise<DocumentCollectionOperation>;
/**
* Returns null if the document collection document does not exist (did not get deleted).
* Returns the id of the deleted document collection document if it got deleted
*/
deleteDocumentCollectionDocument(_: {
documentCollectionDocumentId: string;
}): Promise<string | null>;
/**
* Returns null if the document collection document does not exist (did not get updated).
*/
updateDocumentCollectionDocument(_: {
documentCollectionDocumentId: string;
title: string | null;
contents: string | null;
variables: string | null;
headers: string | null;
}): Promise<DocumentCollectionOperation | null>;
getDocumentCollectionDocument(_: { id: string }): Promise<DocumentCollectionOperation | null>;
}
@Injectable()

View file

@ -126,11 +126,11 @@ export class TargetManager {
return this.storage.getTargets(selector);
}
async getTarget(selector: TargetSelector): Promise<Target> {
async getTarget(selector: TargetSelector, scope = TargetAccessScope.READ): Promise<Target> {
this.logger.debug('Fetching target (selector=%o)', selector);
await this.authManager.ensureTargetAccess({
...selector,
scope: TargetAccessScope.READ,
scope,
});
return this.storage.getTarget(selector);
}

View file

@ -194,6 +194,54 @@ export interface CDNAccessToken {
readonly createdAt: string;
}
export interface DocumentCollection {
id: string;
title: string;
description: string | null;
targetId: string;
createdByUserId: string | null;
createdAt: string;
updatedAt: string;
}
export type PaginatedDocumentCollections = Readonly<{
edges: ReadonlyArray<{
node: DocumentCollection;
cursor: string;
}>;
pageInfo: Readonly<{
hasNextPage: boolean;
hasPreviousPage: boolean;
startCursor: string;
endCursor: string;
}>;
}>;
export interface DocumentCollectionOperation {
id: string;
title: string;
contents: string;
variables: string | null;
headers: string | null;
createdByUserId: string | null;
documentCollectionId: string;
createdAt: string;
updatedAt: string;
}
export type PaginatedDocumentCollectionOperations = Readonly<{
edges: ReadonlyArray<{
node: DocumentCollectionOperation;
cursor: string;
}>;
pageInfo: Readonly<{
hasNextPage: boolean;
hasPreviousPage: boolean;
startCursor: string;
endCursor: string;
}>;
}>;
export interface Project {
id: string;
cleanId: string;

View file

@ -25,6 +25,8 @@ import type {
ActivityObject,
DateRange,
DeletedCompositeSchema as DeletedCompositeSchemaEntity,
DocumentCollection,
DocumentCollectionOperation,
Member,
Organization,
PersistedOperation,
@ -231,3 +233,6 @@ export type SchemaCoordinateUsageTypeMapper = {
total: number;
usedByClients: PromiseOrValue<Array<string> | null>;
};
export type DocumentCollectionConnection = ReadonlyArray<DocumentCollection>;
export type DocumentCollectionOperationsConnection = ReadonlyArray<DocumentCollectionOperation>;

View file

@ -170,7 +170,7 @@ export const graphqlHandler = (options: GraphQLHandlerOptions): RouteHandlerMeth
if (authHeaderParts.length === 2 && authHeaderParts[0] === 'Bearer') {
const accessToken = authHeaderParts[1];
// The token issued by Hive is always 32 characters long.
// Everything longer should be treated as an supertokens token (JWT).
// Everything longer should be treated as a supertokens token (JWT).
if (accessToken.length > 32) {
return await verifySuperTokensSession(
options.supertokens.connectionUri,

View file

@ -7,6 +7,7 @@
"engines": {
"node": ">=12"
},
"main": "./src/index.ts",
"exports": {
".": "./src/index.ts"
},

View file

@ -53,6 +53,28 @@ export interface cdn_access_tokens {
target_id: string;
}
export interface document_collection_documents {
contents: string;
created_at: Date;
created_by_user_id: string | null;
document_collection_id: string;
headers: string | null;
id: string;
title: string;
updated_at: Date;
variables: string | null;
}
export interface document_collections {
created_at: Date;
created_by_user_id: string | null;
description: string | null;
id: string;
target_id: string;
title: string;
updated_at: Date;
}
export interface migration {
date: Date;
hash: string;
@ -257,6 +279,8 @@ export interface DBTables {
alert_channels: alert_channels;
alerts: alerts;
cdn_access_tokens: cdn_access_tokens;
document_collection_documents: document_collection_documents;
document_collections: document_collections;
migration: migration;
oidc_integrations: oidc_integrations;
organization_invitations: organization_invitations;

View file

@ -2871,7 +2871,7 @@ export async function createStorage(connection: string, maximumPoolSize: number)
const limit = args.first ? (args.first > 0 ? Math.min(args.first, 20) : 20) : 20;
if (args.cursor) {
cursor = decodeCDNAccessTokenCursor(args.cursor);
cursor = decodeCreatedAtAndUUIDIdBasedCursor(args.cursor);
}
const result = await pool.any(sql`
@ -2913,7 +2913,7 @@ export async function createStorage(connection: string, maximumPoolSize: number)
return {
node,
get cursor() {
return encodeCDNAccessTokenCursor(node);
return encodeCreatedAtAndUUIDIdBasedCursor(node);
},
};
});
@ -3008,21 +3008,373 @@ export async function createStorage(connection: string, maximumPoolSize: number)
return result ? transformSchemaPolicy(result) : null;
},
async getPaginatedDocumentCollectionsForTarget(args) {
let cursor: null | {
createdAt: string;
id: string;
} = null;
const limit = args.first ? (args.first > 0 ? Math.min(args.first, 20) : 20) : 20;
if (args.cursor) {
cursor = decodeCreatedAtAndUUIDIdBasedCursor(args.cursor);
}
const result = await pool.any(sql`
SELECT
"id"
, "title"
, "description"
, "target_id" as "targetId"
, "created_by_user_id" as "createdByUserId"
, to_json("created_at") as "createdAt"
, to_json("updated_at") as "updatedAt"
FROM
"public"."document_collections"
WHERE
"target_id" = ${args.targetId}
${
cursor
? sql`
AND (
(
"created_at" = ${cursor.createdAt}
AND "id" < ${cursor.id}
)
OR "created_at" < ${cursor.createdAt}
)
`
: sql``
}
ORDER BY
"target_id" ASC
, "created_at" DESC
, "id" DESC
LIMIT ${limit + 1}
`);
let items = result.map(row => {
const node = DocumentCollectionModel.parse(row);
return {
node,
get cursor() {
return encodeCreatedAtAndUUIDIdBasedCursor(node);
},
};
});
const hasNextPage = items.length > limit;
items = items.slice(0, limit);
return {
edges: items,
pageInfo: {
hasNextPage,
hasPreviousPage: cursor !== null,
get endCursor() {
return items[items.length - 1]?.cursor ?? '';
},
get startCursor() {
return items[0]?.cursor ?? '';
},
},
};
},
async createDocumentCollection(args) {
const result = await pool.maybeOne(sql`
INSERT INTO "public"."document_collections" (
"title"
, "description"
, "target_id"
, "created_by_user_id"
)
VALUES (
${args.title},
${args.description},
${args.targetId},
${args.createdByUserId}
)
RETURNING
"id"
, "title"
, "description"
, "target_id" as "targetId"
, "created_by_user_id" as "createdByUserId"
, to_json("created_at") as "createdAt"
, to_json("updated_at") as "updatedAt"
`);
return DocumentCollectionModel.parse(result);
},
async deleteDocumentCollection(args) {
const result = await pool.maybeOneFirst(sql`
DELETE
FROM
"public"."document_collections"
WHERE
"id" = ${args.documentCollectionId}
RETURNING
"id"
`);
if (result == null) {
return null;
}
return zod.string().parse(result);
},
async updateDocumentCollection(args) {
const result = await pool.maybeOne(sql`
UPDATE
"public"."document_collections"
SET
"title" = COALESCE(${args.title}, "title")
, "description" = COALESCE(${args.description}, "description")
, "updated_at" = NOW()
WHERE
"id" = ${args.documentCollectionId}
RETURNING
"id"
, "title"
, "description"
, "target_id" as "targetId"
, "created_by_user_id" as "createdByUserId"
, to_json("created_at") as "createdAt"
, to_json("updated_at") as "updatedAt"
`);
if (result == null) {
return null;
}
return DocumentCollectionModel.parse(result);
},
async getPaginatedDocumentsForDocumentCollection(args) {
let cursor: null | {
createdAt: string;
id: string;
} = null;
const limit = args.first ? (args.first > 0 ? Math.min(args.first, 20) : 20) : 20;
if (args.cursor) {
cursor = decodeCreatedAtAndUUIDIdBasedCursor(args.cursor);
}
const result = await pool.any(sql`
SELECT
"id"
, "title"
, "contents"
, "variables"
, "headers"
, "created_by_user_id" as "createdByUserId"
, "document_collection_id" as "documentCollectionId"
, to_json("created_at") as "createdAt"
, to_json("updated_at") as "updatedAt"
FROM
"public"."document_collection_documents"
WHERE
"document_collection_id" = ${args.documentCollectionId}
${
cursor
? sql`
AND (
(
"created_at" = ${cursor.createdAt}
AND "id" < ${cursor.id}
)
OR "created_at" < ${cursor.createdAt}
)
`
: sql``
}
ORDER BY
"document_collection_id" ASC
, "created_at" DESC
, "id" DESC
LIMIT ${limit + 1}
`);
let items = result.map(row => {
const node = DocumentCollectionDocumentModel.parse(row);
return {
node,
get cursor() {
return encodeCreatedAtAndUUIDIdBasedCursor(node);
},
};
});
const hasNextPage = items.length > limit;
items = items.slice(0, limit);
return {
edges: items,
pageInfo: {
hasNextPage,
hasPreviousPage: cursor !== null,
get endCursor() {
return items[items.length - 1]?.cursor ?? '';
},
get startCursor() {
return items[0]?.cursor ?? '';
},
},
};
},
async createDocumentCollectionDocument(args) {
const result = await pool.one(sql`
INSERT INTO "public"."document_collection_documents" (
"title"
, "contents"
, "variables"
, "headers"
, "created_by_user_id"
, "document_collection_id"
)
VALUES (
${args.title}
, ${args.contents}
, ${args.variables}
, ${args.headers}
, ${args.createdByUserId}
, ${args.documentCollectionId}
)
RETURNING
"id"
, "title"
, "contents"
, "variables"
, "headers"
, "created_by_user_id" as "createdByUserId"
, "document_collection_id" as "documentCollectionId"
, to_json("created_at") as "createdAt"
, to_json("updated_at") as "updatedAt"
`);
return DocumentCollectionDocumentModel.parse(result);
},
async deleteDocumentCollectionDocument(args) {
const result = await pool.maybeOneFirst(sql`
DELETE
FROM
"public"."document_collection_documents"
WHERE
"id" = ${args.documentCollectionDocumentId}
RETURNING
"id"
`);
if (result == null) {
return null;
}
return zod.string().parse(result);
},
async getDocumentCollectionDocument(args) {
const result = await pool.maybeOne(sql`
SELECT
"id"
, "title"
, "contents"
, "variables"
, "headers"
, "created_by_user_id" as "createdByUserId"
, "document_collection_id" as "documentCollectionId"
, to_json("created_at") as "createdAt"
, to_json("updated_at") as "updatedAt"
FROM
"public"."document_collection_documents"
WHERE
"id" = ${args.id}
`);
if (result === null) {
return null;
}
return DocumentCollectionDocumentModel.parse(result);
},
async getDocumentCollection(args) {
const result = await pool.maybeOne(sql`
SELECT
"id"
, "title"
, "description"
, "target_id" as "targetId"
, "created_by_user_id" as "createdByUserId"
, to_json("created_at") as "createdAt"
, to_json("updated_at") as "updatedAt"
FROM
"public"."document_collections"
WHERE
"id" = ${args.id}
`);
if (result === null) {
return null;
}
return DocumentCollectionModel.parse(result);
},
async updateDocumentCollectionDocument(args) {
const result = await pool.maybeOne(sql`
UPDATE
"public"."document_collection_documents"
SET
"title" = COALESCE(${args.title}, "title")
, "contents" = COALESCE(${args.contents}, "contents")
, "variables" = COALESCE(${args.variables}, "variables")
, "headers" = COALESCE(${args.headers}, "headers")
, "updated_at" = NOW()
WHERE
"id" = ${args.documentCollectionDocumentId}
RETURNING
"id"
, "title"
, "contents"
, "variables"
, "headers"
, "created_by_user_id" as "createdByUserId"
, "document_collection_id" as "documentCollectionId"
, to_json("created_at") as "createdAt"
, to_json("updated_at") as "updatedAt"
`);
if (result === null) {
return null;
}
return DocumentCollectionDocumentModel.parse(result);
},
};
return storage;
}
function encodeCDNAccessTokenCursor(cursor: { createdAt: string; id: string }) {
function encodeCreatedAtAndUUIDIdBasedCursor(cursor: { createdAt: string; id: string }) {
return Buffer.from(`${cursor.createdAt}|${cursor.id}`).toString('base64');
}
function decodeCDNAccessTokenCursor(cursor: string) {
function decodeCreatedAtAndUUIDIdBasedCursor(cursor: string) {
const [createdAt, id] = Buffer.from(cursor, 'base64').toString('utf8').split('|');
if (
Number.isNaN(Date.parse(createdAt)) ||
id === undefined ||
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(id) === false
!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(id)
) {
throw new Error('Invalid cursor');
}
@ -3159,6 +3511,28 @@ const SchemaVersionModel = zod
schemaCompositionErrors: value.schema_composition_errors,
}));
const DocumentCollectionModel = zod.object({
id: zod.string(),
title: zod.string(),
description: zod.union([zod.string(), zod.null()]),
targetId: zod.string(),
createdByUserId: zod.union([zod.string(), zod.null()]),
createdAt: zod.string(),
updatedAt: zod.string(),
});
const DocumentCollectionDocumentModel = zod.object({
id: zod.string(),
title: zod.string(),
contents: zod.string(),
variables: zod.string().nullable(),
headers: zod.string().nullable(),
createdByUserId: zod.union([zod.string(), zod.null()]),
documentCollectionId: zod.string(),
createdAt: zod.string(),
updatedAt: zod.string(),
});
/**
* Insert a schema version changes into the database.
*/

View file

@ -14,6 +14,7 @@
"typecheck": "tsc"
},
"dependencies": {
"@graphiql/react": "0.18.0-alpha.0",
"@graphiql/toolkit": "0.8.4",
"@graphql-tools/mock": "9.0.0",
"@headlessui/react": "1.7.15",
@ -42,9 +43,9 @@
"@theguild/editor": "1.2.5",
"@trpc/client": "10.29.1",
"@trpc/server": "10.29.1",
"@urql/core": "3.1.1",
"@urql/core": "4.0.10",
"@urql/devtools": "2.0.3",
"@urql/exchange-graphcache": "5.0.9",
"@urql/exchange-graphcache": "6.1.1",
"@whatwg-node/fetch": "0.9.3",
"clsx": "1.2.1",
"cookies": "0.8.0",
@ -53,7 +54,7 @@
"echarts": "5.4.2",
"echarts-for-react": "3.0.2",
"formik": "2.2.9",
"graphiql": "2.4.7",
"graphiql": "3.0.0-alpha.0",
"graphql": "16.6.0",
"hyperid": "3.1.1",
"immer": "10.0.2",
@ -81,7 +82,7 @@
"supertokens-node": "13.4.2",
"supertokens-web-js": "0.5.0",
"tslib": "2.5.3",
"urql": "3.0.3",
"urql": "4.0.3",
"use-debounce": "9.0.4",
"valtio": "1.10.5",
"wonka": "6.3.2",

View file

@ -1,17 +1,425 @@
import { ReactElement } from 'react';
import { ReactElement, useEffect, useRef, useState } from 'react';
import { useRouter } from 'next/router';
import clsx from 'clsx';
import { GraphiQL } from 'graphiql';
import { useMutation, useQuery } from 'urql';
import { authenticated } from '@/components/authenticated-container';
import { TargetLayout } from '@/components/layouts';
import { Button, DocsLink, DocsNote, Title } from '@/components/v2';
import { HiveLogo, Link2Icon } from '@/components/v2/icon';
import { ConnectLabModal } from '@/components/v2/modals/connect-lab';
import { graphql } from '@/gql';
import { useRouteSelector, useToggle } from '@/lib/hooks';
import { TargetLayout_OrganizationFragment } from '@/components/layouts/target';
import {
Accordion,
Button,
DocsLink,
DocsNote,
EmptyList,
Heading,
Link,
Spinner,
Title,
} from '@/components/v2';
import { HiveLogo, SaveIcon } from '@/components/v2/icon';
import {
ConnectLabModal,
CreateCollectionModal,
CreateOperationModal,
DeleteCollectionModal,
DeleteOperationModal,
} from '@/components/v2/modals';
import { FragmentType, graphql, useFragment } from '@/gql';
import { TargetAccessScope } from '@/gql/graphql';
import { canAccessTarget, CanAccessTarget_MemberFragment } from '@/lib/access/target';
import { useClipboard, useNotifications, useRouteSelector, useToggle } from '@/lib/hooks';
import { useCollections } from '@/lib/hooks/use-collections';
import { withSessionProtection } from '@/lib/supertokens/guard';
import { DropdownMenu, GraphiQLPlugin, Tooltip, useEditorContext } from '@graphiql/react';
import { createGraphiQLFetcher } from '@graphiql/toolkit';
import { BookmarkIcon, DotsVerticalIcon, Link1Icon, Share2Icon } from '@radix-ui/react-icons';
import 'graphiql/graphiql.css';
const Page = ({ endpoint }: { endpoint: string }): ReactElement => {
function Share(): ReactElement {
const label = 'Share query';
const copyToClipboard = useClipboard();
const { href } = window.location;
const router = useRouter();
return (
<Tooltip label={label}>
<Button
className="graphiql-toolbar-button"
aria-label={label}
disabled={!router.query.operation}
onClick={async () => {
await copyToClipboard(href);
}}
>
<Share2Icon className="graphiql-toolbar-icon" />
</Button>
</Tooltip>
);
}
const OperationQuery = graphql(`
query Operation($selector: TargetSelectorInput!, $id: ID!) {
target(selector: $selector) {
id
documentCollectionOperation(id: $id) {
id
name
query
headers
variables
collection {
id
}
}
}
}
`);
function useCurrentOperation() {
const router = useRouteSelector();
const operationId = router.query.operation as string;
const [{ data }] = useQuery({
query: OperationQuery,
variables: {
selector: {
target: router.targetId,
project: router.projectId,
organization: router.organizationId,
},
id: operationId,
},
pause: !operationId,
});
// if operationId is undefined `data` could contain previous state
return operationId ? data?.target?.documentCollectionOperation : null;
}
function useOperationCollectionsPlugin(props: {
meRef: FragmentType<typeof CanAccessTarget_MemberFragment>;
}) {
const propsRef = useRef(props);
propsRef.current = props;
const pluginRef = useRef<GraphiQLPlugin>();
pluginRef.current ||= {
title: 'Operation Collections',
icon: BookmarkIcon,
content: function Content() {
const [isCollectionModalOpen, toggleCollectionModal] = useToggle();
const { collections, loading } = useCollections();
const [collectionId, setCollectionId] = useState('');
const [operationId, setOperationId] = useState('');
const [isDeleteCollectionModalOpen, toggleDeleteCollectionModalOpen] = useToggle();
const [isDeleteOperationModalOpen, toggleDeleteOperationModalOpen] = useToggle();
const copyToClipboard = useClipboard();
const router = useRouteSelector();
const currentOperation = useCurrentOperation();
const editorContext = useEditorContext({ nonNull: true });
const hasAllEditors = !!(
editorContext.queryEditor &&
editorContext.variableEditor &&
editorContext.headerEditor
);
const queryParamsOperationId = router.query.operation as string;
const tabsCount = editorContext.tabs.length;
useEffect(() => {
if (tabsCount !== 1) {
for (let index = 1; index < tabsCount; index++) {
// Workaround to close opened tabs from end, to avoid bug when tabs are still opened
editorContext.closeTab(tabsCount - index);
}
const { operation: _paramToRemove, ...query } = router.query;
void router.push({ query });
}
}, [tabsCount]);
useEffect(() => {
if (!hasAllEditors) return;
if (queryParamsOperationId && currentOperation) {
// Set selected operation in editors
editorContext.queryEditor.setValue(currentOperation.query);
editorContext.variableEditor.setValue(currentOperation.variables);
editorContext.headerEditor.setValue(currentOperation.headers);
} else {
// Clear editors if operation not selected
editorContext.queryEditor.setValue('');
editorContext.variableEditor.setValue('');
editorContext.headerEditor.setValue('');
}
}, [hasAllEditors, queryParamsOperationId, currentOperation]);
const canEdit = canAccessTarget(TargetAccessScope.Settings, props.meRef);
const canDelete = canAccessTarget(TargetAccessScope.Delete, props.meRef);
const shouldShowMenu = canEdit || canDelete;
const initialSelectedCollection =
currentOperation?.id &&
collections?.find(c =>
c.operations.edges.some(({ node }) => node.id === currentOperation.id),
)?.id;
return (
<>
<div className="flex justify-between">
<Heading>Operation Collections</Heading>
{canEdit ? (
<Button
variant="link"
onClick={() => {
if (collectionId) setCollectionId('');
toggleCollectionModal();
}}
data-cy="create-collection"
>
+ Create
</Button>
) : null}
</div>
<p className="mb-3 font-light text-gray-300">Shared across your organization</p>
{loading ? (
<Spinner />
) : (
<Accordion defaultValue={initialSelectedCollection}>
<CreateCollectionModal
isOpen={isCollectionModalOpen}
toggleModalOpen={toggleCollectionModal}
collectionId={collectionId}
/>
<DeleteCollectionModal
isOpen={isDeleteCollectionModalOpen}
toggleModalOpen={toggleDeleteCollectionModalOpen}
collectionId={collectionId}
/>
<DeleteOperationModal
isOpen={isDeleteOperationModalOpen}
toggleModalOpen={toggleDeleteOperationModalOpen}
operationId={operationId}
/>
{collections?.length ? (
collections.map(collection => (
<Accordion.Item key={collection.id} value={collection.id}>
<div className="flex">
<Accordion.Header>{collection.name}</Accordion.Header>
{shouldShowMenu ? (
<DropdownMenu
// https://github.com/radix-ui/primitives/issues/1241#issuecomment-1580887090
modal={false}
>
<DropdownMenu.Button
className="graphiql-toolbar-button !shrink-0"
aria-label="More"
data-cy="collection-3-dots"
>
<DotsVerticalIcon />
</DropdownMenu.Button>
<DropdownMenu.Content>
<DropdownMenu.Item
onSelect={() => {
setCollectionId(collection.id);
toggleCollectionModal();
}}
data-cy="collection-edit"
>
Edit
</DropdownMenu.Item>
<DropdownMenu.Item
onSelect={() => {
setCollectionId(collection.id);
toggleDeleteCollectionModalOpen();
}}
className="!text-red-500"
data-cy="collection-delete"
>
Delete
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu>
) : null}
</div>
<Accordion.Content className="pr-0">
{collection.operations.edges.length
? collection.operations.edges.map(({ node }) => (
<div key={node.id} className="flex justify-between items-center">
<Link
href={{
query: {
operation: node.id,
orgId: router.organizationId,
projectId: router.projectId,
targetId: router.targetId,
},
}}
className={clsx(
'hover:bg-gray-100/10 w-full rounded p-2 !text-gray-300',
router.query.operation === node.id && 'bg-gray-100/10',
)}
>
{node.name}
</Link>
<DropdownMenu
// https://github.com/radix-ui/primitives/issues/1241#issuecomment-1580887090
modal={false}
>
<DropdownMenu.Button
className="graphiql-toolbar-button opacity-0 [div:hover>&]:opacity-100 transition"
aria-label="More"
data-cy="operation-3-dots"
>
<DotsVerticalIcon />
</DropdownMenu.Button>
<DropdownMenu.Content>
<DropdownMenu.Item
onSelect={async () => {
const url = new URL(window.location.href);
await copyToClipboard(
`${url.origin}${url.pathname}?operation=${node.id}`,
);
}}
>
Copy link to operation
</DropdownMenu.Item>
{canDelete ? (
<DropdownMenu.Item
onSelect={() => {
setOperationId(node.id);
toggleDeleteOperationModalOpen();
}}
className="!text-red-500"
data-cy="remove-operation"
>
Delete
</DropdownMenu.Item>
) : null}
</DropdownMenu.Content>
</DropdownMenu>
</div>
))
: 'No operations yet. Use the editor to create an operation, and click Save to store and share it.'}
</Accordion.Content>
</Accordion.Item>
))
) : (
<EmptyList
title="Add your first collection"
description="Collections shared across organization"
/>
)}
</Accordion>
)}
</>
);
},
};
return pluginRef.current;
}
const UpdateOperationMutation = graphql(`
mutation UpdateOperation(
$selector: TargetSelectorInput!
$input: UpdateDocumentCollectionOperationInput!
) {
updateOperationInDocumentCollection(selector: $selector, input: $input) {
error {
message
}
ok {
operation {
id
name
query
variables
headers
}
}
}
}
`);
function Save(): ReactElement {
const [isOpen, toggle] = useToggle();
const { collections } = useCollections();
const notify = useNotifications();
const routeSelector = useRouteSelector();
const currentOperation = useCurrentOperation();
const [, mutateUpdate] = useMutation(UpdateOperationMutation);
const { queryEditor, variableEditor, headerEditor } = useEditorContext()!;
const isSame =
!!currentOperation &&
currentOperation.query === queryEditor?.getValue() &&
currentOperation.variables === variableEditor?.getValue() &&
currentOperation.headers === headerEditor?.getValue();
const operationId = currentOperation?.id;
const label = isSame ? undefined : operationId ? 'Update saved operation' : 'Save operation';
const button = (
<Button
className="graphiql-toolbar-button"
data-cy="save-collection"
aria-label={label}
disabled={isSame}
onClick={async () => {
if (!collections?.length) {
notify('You must create collection first!', 'warning');
return;
}
if (!operationId) {
toggle();
return;
}
const { error, data } = await mutateUpdate({
selector: {
target: routeSelector.targetId,
organization: routeSelector.organizationId,
project: routeSelector.projectId,
},
input: {
name: currentOperation.name,
collectionId: currentOperation.collection.id,
query: queryEditor?.getValue(),
variables: variableEditor?.getValue(),
headers: headerEditor?.getValue(),
operationId,
},
});
if (data) {
notify('Updated!', 'success');
}
if (error) {
notify(error.message, 'error');
}
}}
>
<SaveIcon className="graphiql-toolbar-icon !h-5 w-auto" />
</Button>
);
return (
<>
{label ? <Tooltip label={label}>{button}</Tooltip> : button}
{isOpen ? <CreateOperationModal isOpen={isOpen} toggleModalOpen={toggle} /> : null}
</>
);
}
// Save.whyDidYouRender = true;
function Page({
endpoint,
organizationRef,
}: {
endpoint: string;
organizationRef: FragmentType<typeof TargetLayout_OrganizationFragment>;
}): ReactElement {
const { me } = useFragment(TargetLayout_OrganizationFragment, organizationRef);
const operationCollectionsPlugin = useOperationCollectionsPlugin({ meRef: me });
return (
<>
<DocsNote>
@ -22,16 +430,29 @@ const Page = ({ endpoint }: { endpoint: string }): ReactElement => {
.graphiql-container {
--color-base: transparent !important;
--color-primary: 40, 89%, 60% !important;
min-height: 600px;
}
`}</style>
<GraphiQL fetcher={createGraphiQLFetcher({ url: endpoint })}>
<GraphiQL
fetcher={createGraphiQLFetcher({ url: endpoint })}
toolbar={{
additionalContent: (
<>
<Save />
<Share />
</>
),
}}
plugins={[operationCollectionsPlugin]}
visiblePlugin={operationCollectionsPlugin}
>
<GraphiQL.Logo>
<HiveLogo className="h-6 w-6" />
<HiveLogo className="h-6 w-auto" />
</GraphiQL.Logo>
</GraphiQL>
</>
);
};
}
const TargetLaboratoryPageQuery = graphql(`
query TargetLaboratoryPageQuery($organizationId: ID!, $projectId: ID!, $targetId: ID!) {
@ -56,7 +477,7 @@ const TargetLaboratoryPageQuery = graphql(`
function LaboratoryPage(): ReactElement {
const [isModalOpen, toggleModalOpen] = useToggle();
const router = useRouteSelector();
const endpoint = `${location.origin}/api/lab/${router.organizationId}/${router.projectId}/${router.targetId}`;
const endpoint = `${window.location.origin}/api/lab/${router.organizationId}/${router.projectId}/${router.targetId}`;
return (
<>
@ -69,7 +490,7 @@ function LaboratoryPage(): ReactElement {
<>
<Button size="large" variant="primary" onClick={toggleModalOpen} className="ml-auto">
Use Schema Externally
<Link2Icon className="ml-8 h-4 w-4" />
<Link1Icon className="ml-8 h-6 w-auto" />
</Button>
<ConnectLabModal
isOpen={isModalOpen}
@ -79,7 +500,9 @@ function LaboratoryPage(): ReactElement {
</>
}
>
{() => <Page endpoint={endpoint} />}
{({ organization }) => (
<Page organizationRef={organization!.organization} endpoint={endpoint} />
)}
</TargetLayout>
</>
);

View file

@ -87,23 +87,19 @@ function RegistryAccessTokens(props: {
targets/projects. In most cases, this token is used from the Hive CLI.
<br />
<DocsLink href="/management/targets#registry-access-tokens">
Learn more about Registry Access Token
Learn more about Registry Access Tokens
</DocsLink>
</DocsNote>
{canManage && (
<div className="my-3.5 flex justify-between">
<Button variant="secondary" onClick={toggleModalOpen} size="large" className="px-5">
Generate new token
</Button>
<Button
size="large"
danger
disabled={checked.length === 0 || deleting}
className="px-9"
onClick={deleteTokens}
>
Delete {checked.length || null}
<Button variant="primary" onClick={toggleModalOpen} size="large" className="px-5">
Create new registry token
</Button>
{checked.length === 0 ? null : (
<Button size="large" danger disabled={deleting} className="px-9" onClick={deleteTokens}>
Delete {checked.length || null}
</Button>
)}
</div>
)}
<Table>

View file

@ -14,6 +14,7 @@ import { env } from '@/env/frontend';
import * as gtag from '@/lib/gtag';
import { urqlClient } from '@/lib/urql';
import { configureScope, init } from '@sentry/nextjs';
import '../src/wdyr';
import '../public/styles.css';
import 'react-toastify/dist/ReactToastify.css';

View file

@ -76,7 +76,7 @@ function OrganizationTransferPage() {
if (accept) {
notify('The organization is now yours!', 'success');
}
router.visitOrganization({
void router.visitOrganization({
organizationId: orgId,
});
} else {
@ -95,7 +95,7 @@ function OrganizationTransferPage() {
const reject = useCallback(() => answer(false), [answer]);
const goBack = useCallback(() => {
router.visitHome();
void router.visitHome();
}, [router]);
return (

View file

@ -77,7 +77,7 @@ function Home(): ReactElement {
const org = query.data.organizations.nodes[0];
if (org) {
router.visitOrganization({ organizationId: org.cleanId });
void router.visitOrganization({ organizationId: org.cleanId });
}
}
}, [router, query.data]);

View file

@ -32,13 +32,13 @@ function OrganizationPage() {
} else {
const org = result.data.joinOrganization.organization;
notify(`You joined "${org.name}" organization`, 'success');
router.visitOrganization({ organizationId: org.cleanId });
void router.visitOrganization({ organizationId: org.cleanId });
}
}
}, [mutate, code, router, notify]);
const goBack = useCallback(() => {
router.visitHome();
void router.visitHome();
}, [router]);
return (

View file

@ -97,7 +97,7 @@ export function ProjectLayout<
useEffect(() => {
if (projectQuery.error) {
// url with # provoke error Maximum update depth exceeded
router.push('/404', router.asPath.replace(/#.*/, ''));
void router.push('/404', router.asPath.replace(/#.*/, ''));
}
}, [projectQuery.error, router]);

View file

@ -8,11 +8,12 @@ import {
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/v2/dropdown';
import { ArrowDownIcon, Link2Icon } from '@/components/v2/icon';
import { ArrowDownIcon } from '@/components/v2/icon';
import { ConnectSchemaModal } from '@/components/v2/modals';
import { FragmentType, graphql, useFragment } from '@/gql';
import { canAccessTarget, TargetAccessScope, useTargetAccess } from '@/lib/access/target';
import { useRouteSelector, useToggle } from '@/lib/hooks';
import { Link1Icon } from '@radix-ui/react-icons';
import { QueryError } from '../common/DataWrapper';
import { ProjectMigrationToast } from '../project/migration-toast';
@ -25,7 +26,7 @@ enum TabValue {
Settings = 'settings',
}
const TargetLayout_OrganizationFragment = graphql(`
export const TargetLayout_OrganizationFragment = graphql(`
fragment TargetLayout_OrganizationFragment on Organization {
me {
...CanAccessTarget_MemberFragment
@ -114,14 +115,14 @@ export const TargetLayout = <
useEffect(() => {
if (!data.fetching && !target) {
// url with # provoke error Maximum update depth exceeded
router.push('/404', router.asPath.replace(/#.*/, ''));
void router.push('/404', router.asPath.replace(/#.*/, ''));
}
}, [router, target, data.fetching]);
useEffect(() => {
if (!data.fetching && !project) {
// url with # provoke error Maximum update depth exceeded
router.push('/404', router.asPath.replace(/#.*/, ''));
void router.push('/404', router.asPath.replace(/#.*/, ''));
}
}, [router, project, data.fetching]);
@ -200,7 +201,7 @@ export const TargetLayout = <
className="ml-auto"
>
Connect to CDN
<Link2Icon className="ml-8 h-4 w-4" />
<Link1Icon className="ml-8 h-6 w-auto" />
</Button>
<ConnectSchemaModal isOpen={isModalOpen} toggleModalOpen={toggleModalOpen} />
</>

View file

@ -277,6 +277,7 @@ export function usePermissionsManager({
projectScopes,
targetScopes,
state,
noneSelected: !organizationScopes.length && !projectScopes.length && !targetScopes.length,
// Methods
canAccessOrganization: useCallback(
(scope: OrganizationAccessScope) => canAccessOrganization(scope, organization.me),

View file

@ -387,7 +387,7 @@ export function CDNAccessTokens(props: {
artifacts.
<br />
<DocsLink href="/management/targets#cdn-access-tokens">
Learn more about CDN Access Token
Learn more about CDN Access Tokens
</DocsLink>
</DocsNote>
{canManage && (
@ -395,7 +395,7 @@ export function CDNAccessTokens(props: {
<Button
as="a"
href={openCreateCDNAccessTokensModalLink}
variant="secondary"
variant="primary"
onClick={ev => {
ev.preventDefault();
void router.push(openCreateCDNAccessTokensModalLink);
@ -403,7 +403,7 @@ export function CDNAccessTokens(props: {
size="large"
className="px-5"
>
Create new CDN Token
Create new CDN token
</Button>
</div>
)}
@ -457,22 +457,23 @@ export function CDNAccessTokens(props: {
Previous Page
</Button>
) : null}
<Button
variant="secondary"
size="large"
className="px-5"
disabled={!target.data?.target?.cdnAccessTokens.pageInfo.hasNextPage}
onClick={() => {
setEndCursors(cursors => {
if (!target.data?.target?.cdnAccessTokens.pageInfo.endCursor) {
return cursors;
}
return [...cursors, target.data?.target?.cdnAccessTokens.pageInfo.endCursor];
});
}}
>
Next Page
</Button>
{target.data?.target?.cdnAccessTokens.pageInfo.hasNextPage ? (
<Button
variant="secondary"
size="large"
className="px-5"
onClick={() => {
setEndCursors(cursors => {
if (!target.data?.target?.cdnAccessTokens.pageInfo.endCursor) {
return cursors;
}
return [...cursors, target.data?.target?.cdnAccessTokens.pageInfo.endCursor];
});
}}
>
Next Page
</Button>
) : null}
</div>
{isCreateCDNAccessTokensModalOpen ? (
<CreateCDNAccessTokenModal

View file

@ -28,6 +28,7 @@ function Wrapper({
defaultValue={defaultValue}
collapsible
className="space-y-4 w-full"
data-cy="accordion"
>
{children}
</A.Root>
@ -51,9 +52,15 @@ function Item({
);
}
function Header({ children }: { children: ReactNode }): ReactElement {
function Header({
children,
className,
}: {
children: ReactNode;
className?: string;
}): ReactElement {
return (
<A.Header className="w-full">
<A.Header className={clsx('w-full', className)}>
<A.Trigger
className={clsx(
'group',
@ -74,9 +81,15 @@ function Header({ children }: { children: ReactNode }): ReactElement {
);
}
function Content({ children }: { children: ReactNode }): ReactElement {
function Content({
children,
className,
}: {
children: ReactNode;
className?: string;
}): ReactElement {
return (
<A.Content className="pt-1 w-full rounded-b-lg px-4 pb-3">
<A.Content className={clsx('pt-1 w-full rounded-b-lg px-4 pb-3', className)}>
<div className="text-sm text-gray-700 dark:text-gray-400">{children}</div>
</A.Content>
);

View file

@ -10,10 +10,10 @@ export const EmptyList = ({
}: {
title: string;
description: string;
docsUrl: string | null;
docsUrl?: string;
}): ReactElement => {
return (
<Card className="flex grow flex-col items-center gap-y-2">
<Card className="flex grow flex-col items-center gap-y-2" data-cy="empty-list">
<Image
src={magnifier}
alt="Magnifier illustration"
@ -23,9 +23,7 @@ export const EmptyList = ({
/>
<Heading>{title}</Heading>
<span className="text-center text-sm font-medium text-gray-500">{description}</span>
{docsUrl === null ? null : (
<DocsLink href={docsUrl}>Read about it in the documentation</DocsLink>
)}
{docsUrl && <DocsLink href={docsUrl}>Read about it in the documentation</DocsLink>}
</Card>
);
};

View file

@ -28,6 +28,15 @@ export const GraphQLIcon = ({ className }: IconProps): ReactElement => (
</svg>
);
export const SaveIcon = ({ className }: IconProps): ReactElement => (
<svg viewBox="0 0 24 24" className={className}>
<g fill="none" stroke="currentColor" strokeWidth="1">
<path d="M21.75 23.25H2.25a1.5 1.5 0 0 1-1.5-1.5V7.243a3 3 0 0 1 .879-2.121l3.492-3.493A3 3 0 0 1 7.243.75H21.75a1.5 1.5 0 0 1 1.5 1.5v19.5a1.5 1.5 0 0 1-1.5 1.5z" />
<path d="M8.25.75v6a1.5 1.5 0 0 0 1.5 1.5h7.5a1.5 1.5 0 0 0 1.5-1.5v-6M15.75 3.75v1.5M17.25 12.75H6.75a1.5 1.5 0 0 0-1.5 1.5v9h13.5v-9a1.5 1.5 0 0 0-1.5-1.5zM8.25 15.75h4.5M8.25 18.75h7.5" />
</g>
</svg>
);
export const TrendingUpIcon = ({ className }: IconProps): ReactElement => (
<svg
width="24"
@ -439,17 +448,6 @@ export const SlackIcon = ({ className }: IconProps): ReactElement => (
</svg>
);
export const Link2Icon = ({ className }: IconProps): ReactElement => (
<svg
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
className={clsx('h-6 w-6 fill-current stroke-current', className)}
>
<path d="M15 6C14.4477 6 14 6.44772 14 7C14 7.55228 14.4477 8 15 8V6ZM18 7V6V7ZM15 16C14.4477 16 14 16.4477 14 17C14 17.5523 14.4477 18 15 18V16ZM9 18C9.55229 18 10 17.5523 10 17C10 16.4477 9.55229 16 9 16V18ZM9 8C9.55229 8 10 7.55228 10 7C10 6.44772 9.55229 6 9 6V8ZM15 8H18V6H15V8ZM18 8C19.0609 8 20.0783 8.42143 20.8284 9.17157L22.2426 7.75736C21.1174 6.63214 19.5913 6 18 6V8ZM20.8284 9.17157C21.5786 9.92172 22 10.9391 22 12H24C24 10.4087 23.3679 8.88258 22.2426 7.75736L20.8284 9.17157ZM22 12C22 14.2091 20.2091 16 18 16V18C21.3137 18 24 15.3137 24 12H22ZM18 16H15V18H18V16ZM9 16H6V18H9V16ZM6 16C4.93913 16 3.92172 15.5786 3.17157 14.8284L1.75736 16.2426C2.88258 17.3679 4.4087 18 6 18V16ZM3.17157 14.8284C2.42143 14.0783 2 13.0609 2 12H0C0 13.5913 0.632141 15.1174 1.75736 16.2426L3.17157 14.8284ZM2 12C2 9.79086 3.79086 8 6 8V6C2.68629 6 0 8.68629 0 12H2ZM6 8H9V6H6V8Z" />
<path d="M8 12H16" {...DEFAULT_PATH_PROPS} />
</svg>
);
export const HiveLogo = ({ className }: IconProps): ReactElement => (
<svg
width="42"

View file

@ -17,9 +17,9 @@ import {
import { Provider as TooltipProvider } from '@radix-ui/react-tooltip';
const widthBySize = {
sm: 'w-[450px]',
md: 'w-[600px]',
lg: 'w-[800px]',
sm: clsx('w-[450px]'),
md: clsx('w-[600px]'),
lg: clsx('w-[800px]'),
};
export const ModalTooltipContext = createContext<HTMLDivElement | null>(null);

View file

@ -0,0 +1,270 @@
import { ReactElement, useEffect } from 'react';
import { useFormik } from 'formik';
import { useMutation, useQuery } from 'urql';
import * as Yup from 'yup';
import { Button, Heading, Input, Modal } from '@/components/v2';
import { graphql } from '@/gql';
import { TargetDocument } from '@/graphql';
import { useRouteSelector } from '@/lib/hooks';
const CollectionQuery = graphql(`
query Collection($selector: TargetSelectorInput!, $id: ID!) {
target(selector: $selector) {
id
documentCollection(id: $id) {
id
name
description
}
}
}
`);
export const CreateCollectionMutation = graphql(`
mutation CreateCollection(
$selector: TargetSelectorInput!
$input: CreateDocumentCollectionInput!
) {
createDocumentCollection(selector: $selector, input: $input) {
error {
message
}
ok {
updatedTarget {
id
documentCollections {
edges {
cursor
node {
id
name
}
}
}
}
collection {
id
name
description
operations(first: 100) {
edges {
cursor
node {
id
name
}
cursor
}
}
}
}
}
}
`);
const UpdateCollectionMutation = graphql(`
mutation UpdateCollection(
$selector: TargetSelectorInput!
$input: UpdateDocumentCollectionInput!
) {
updateDocumentCollection(selector: $selector, input: $input) {
error {
message
}
ok {
updatedTarget {
id
documentCollections {
edges {
node {
id
name
}
cursor
}
}
}
collection {
id
name
description
operations(first: 100) {
edges {
cursor
node {
id
name
}
}
}
}
}
}
}
`);
export function CreateCollectionModal({
isOpen,
toggleModalOpen,
collectionId,
}: {
isOpen: boolean;
toggleModalOpen: () => void;
collectionId?: string;
}): ReactElement {
const router = useRouteSelector();
const [mutationCreate, mutateCreate] = useMutation(CreateCollectionMutation);
const [mutationUpdate, mutateUpdate] = useMutation(UpdateCollectionMutation);
const [result] = useQuery({
query: TargetDocument,
variables: {
targetId: router.targetId,
organizationId: router.organizationId,
projectId: router.projectId,
},
});
const [{ data, error: collectionError, fetching: loadingCollection }] = useQuery({
query: CollectionQuery,
variables: {
id: collectionId!,
selector: {
target: router.targetId,
organization: router.organizationId,
project: router.projectId,
},
},
pause: !collectionId,
});
const error = mutationCreate.error || result.error || collectionError || mutationUpdate.error;
const fetching = loadingCollection || result.fetching;
useEffect(() => {
if (!collectionId) {
resetForm();
} else if (data) {
const { documentCollection } = data.target!;
if (documentCollection) {
void setValues({
name: documentCollection.name,
description: documentCollection.description || '',
});
}
}
}, [data, collectionId]);
const {
handleSubmit,
values,
handleChange,
errors,
touched,
isSubmitting,
setValues,
resetForm,
} = useFormik({
initialValues: {
name: '',
description: '',
},
validationSchema: Yup.object().shape({
name: Yup.string().required(),
description: Yup.string(),
}),
async onSubmit(values) {
const { error } = collectionId
? await mutateUpdate({
selector: {
target: router.targetId,
organization: router.organizationId,
project: router.projectId,
},
input: {
collectionId,
name: values.name,
description: values.description,
},
})
: await mutateCreate({
selector: {
target: router.targetId,
organization: router.organizationId,
project: router.projectId,
},
input: values,
});
if (!error) {
resetForm();
toggleModalOpen();
}
},
});
return (
<Modal open={isOpen} onOpenChange={toggleModalOpen}>
{!fetching && (
<form className="flex flex-col gap-8" onSubmit={handleSubmit}>
<Heading className="text-center">
{collectionId ? 'Update' : 'Create'} Shared Collection
</Heading>
<div className="flex flex-col gap-4">
<label className="text-sm font-semibold" htmlFor="name">
Collection Name
</label>
<Input
data-cy="input.name"
name="name"
placeholder="My Collection"
value={values.name}
onChange={handleChange}
isInvalid={!!(touched.name && errors.name)}
/>
{touched.name && errors.name && (
<div className="text-sm text-red-500">{errors.name}</div>
)}
</div>
<div className="flex flex-col gap-4">
<label className="text-sm font-semibold" htmlFor="description">
Collection Description
</label>
<Input
data-cy="input.description"
name="description"
value={values.description}
onChange={handleChange}
isInvalid={!!(touched.description && errors.description)}
/>
{touched.description && errors.description && (
<div className="text-sm text-red-500">{errors.description}</div>
)}
</div>
{error && <div className="text-sm text-red-500">{error.message}</div>}
<div className="flex w-full gap-2">
<Button type="button" size="large" block onClick={toggleModalOpen}>
Cancel
</Button>
<Button
type="submit"
size="large"
block
variant="primary"
disabled={isSubmitting}
data-cy="confirm"
>
{collectionId ? 'Update' : 'Add'}
</Button>
</div>
</form>
)}
</Modal>
);
}

View file

@ -0,0 +1,201 @@
import { ReactElement } from 'react';
import { useFormik } from 'formik';
import { useMutation } from 'urql';
import * as Yup from 'yup';
import { Button, Heading, Input, Modal, Select } from '@/components/v2';
import { graphql } from '@/gql';
import { useRouteSelector } from '@/lib/hooks';
import { useCollections } from '@/lib/hooks/use-collections';
import { useEditorContext } from '@graphiql/react';
const CreateOperationMutation = graphql(`
mutation CreateOperation(
$selector: TargetSelectorInput!
$input: CreateDocumentCollectionOperationInput!
) {
createOperationInDocumentCollection(selector: $selector, input: $input) {
error {
message
}
ok {
operation {
id
name
}
updatedTarget {
id
documentCollections {
edges {
cursor
node {
id
operations {
edges {
node {
id
}
cursor
}
}
}
}
}
}
}
}
}
`);
export type CreateOperationMutationType = typeof CreateOperationMutation;
export function CreateOperationModal({
isOpen,
toggleModalOpen,
}: {
isOpen: boolean;
toggleModalOpen: () => void;
}): ReactElement {
const router = useRouteSelector();
const [mutationCreate, mutateCreate] = useMutation(CreateOperationMutation);
const { collections, loading } = useCollections();
const { queryEditor, variableEditor, headerEditor } = useEditorContext({
nonNull: true,
});
const { error } = mutationCreate;
const fetching = loading;
const {
handleSubmit,
values,
handleChange,
handleBlur,
errors,
isValid,
touched,
isSubmitting,
resetForm,
} = useFormik({
initialValues: {
name: '',
collectionId: '',
},
validationSchema: Yup.object().shape({
name: Yup.string().min(3).required(),
collectionId: Yup.string().required('Collection is a required field'),
}),
async onSubmit(values) {
const response = await mutateCreate({
selector: {
target: router.targetId,
organization: router.organizationId,
project: router.projectId,
},
input: {
name: values.name,
collectionId: values.collectionId,
query: queryEditor?.getValue(),
variables: variableEditor?.getValue(),
headers: headerEditor?.getValue(),
},
});
const result = response.data;
const error = response.error || response.data?.createOperationInDocumentCollection.error;
if (!error) {
if (result) {
const data = result.createOperationInDocumentCollection;
void router.push({
query: {
...router.query,
operation: data.ok?.operation.id,
},
});
}
resetForm();
toggleModalOpen();
}
},
});
return (
<Modal open={isOpen} onOpenChange={toggleModalOpen}>
{!fetching && (
<form className="flex flex-col gap-8" onSubmit={handleSubmit}>
<Heading className="text-center">Create Operation</Heading>
<div className="flex flex-col gap-4">
<label className="text-sm font-semibold" htmlFor="name">
Operation Name
</label>
<Input
name="name"
placeholder="Your Operation Name"
value={values.name}
onChange={handleChange}
onBlur={handleBlur}
isInvalid={!!(touched.name && errors.name)}
data-cy="input.name"
/>
{touched.name && errors.name && (
<div className="text-sm text-red-500">{errors.name}</div>
)}
</div>
<div className="flex flex-col gap-4">
<label className="text-sm font-semibold" htmlFor="name">
Which collection would you like to save this operation to?
</label>
<Select
name="collectionId"
placeholder="Select collection"
options={collections?.map(c => ({
value: c.id,
name: c.name,
}))}
value={values.collectionId}
onChange={handleChange}
onBlur={handleBlur}
isInvalid={!!(touched.collectionId && errors.collectionId)}
data-cy="select.collectionId"
/>
{touched.collectionId && errors.collectionId && (
<div className="text-sm text-red-500">{errors.collectionId}</div>
)}
</div>
{error && <div className="text-sm text-red-500">{error.message}</div>}
<div className="flex w-full gap-2">
<Button
type="button"
size="large"
block
onClick={() => {
resetForm();
toggleModalOpen();
}}
>
Cancel
</Button>
<Button
type="submit"
size="large"
block
variant="primary"
disabled={isSubmitting || !isValid || values.collectionId === ''}
data-cy="confirm"
>
Add Operation
</Button>
</div>
</form>
)}
</Modal>
);
}

View file

@ -0,0 +1,83 @@
import { ReactElement } from 'react';
import { useMutation } from 'urql';
import { Button, Heading, Modal } from '@/components/v2';
import { graphql } from '@/gql';
import { useRouteSelector } from '@/lib/hooks';
import { TrashIcon } from '@radix-ui/react-icons';
const DeleteCollectionMutation = graphql(`
mutation DeleteCollection($selector: TargetSelectorInput!, $id: ID!) {
deleteDocumentCollection(selector: $selector, id: $id) {
error {
message
}
ok {
deletedId
updatedTarget {
id
documentCollections {
edges {
cursor
node {
id
}
}
}
}
}
}
}
`);
export type DeleteCollectionMutationType = typeof DeleteCollectionMutation;
export function DeleteCollectionModal({
isOpen,
toggleModalOpen,
collectionId,
}: {
isOpen: boolean;
toggleModalOpen: () => void;
collectionId: string;
}): ReactElement {
const router = useRouteSelector();
const [, mutate] = useMutation(DeleteCollectionMutation);
return (
<Modal
open={isOpen}
onOpenChange={toggleModalOpen}
className="flex flex-col items-center gap-5"
>
<TrashIcon className="h-16 w-auto text-red-500 opacity-70" />
<Heading>Delete Collection</Heading>
<p className="text-sm text-gray-500">
Are you sure you wish to delete this collection? This action is irreversible!
</p>
<div className="flex w-full gap-2">
<Button type="button" size="large" block onClick={toggleModalOpen}>
Cancel
</Button>
<Button
size="large"
block
danger
onClick={async () => {
await mutate({
id: collectionId,
selector: {
target: router.targetId,
organization: router.organizationId,
project: router.projectId,
},
});
toggleModalOpen();
}}
data-cy="confirm"
>
Delete
</Button>
</div>
</Modal>
);
}

View file

@ -1,9 +1,9 @@
import { ReactElement } from 'react';
import { useMutation } from 'urql';
import { Button, Heading, Modal } from '@/components/v2';
import { TrashIcon } from '@/components/v2/icon';
import { DeleteOrganizationMembersDocument } from '@/graphql';
import { useRouteSelector } from '@/lib/hooks';
import { TrashIcon } from '@radix-ui/react-icons';
export const DeleteMembersModal = ({
isOpen,
@ -24,7 +24,7 @@ export const DeleteMembersModal = ({
onOpenChange={toggleModalOpen}
className="flex flex-col items-center gap-5"
>
<TrashIcon className="h-24 w-24 text-red-500 opacity-70" />
<TrashIcon className="h-16 w-auto text-red-500 opacity-70" />
<Heading>Delete member{isSingle ? '' : 's'}</Heading>
<p className="text-sm text-gray-500">
Are you sure you wish to delete {isSingle ? 'this user' : `${memberIds.length} users`}?

View file

@ -0,0 +1,95 @@
import { ReactElement } from 'react';
import { useMutation } from 'urql';
import { Button, Heading, Modal } from '@/components/v2';
import { graphql } from '@/gql';
import { useNotifications, useRouteSelector } from '@/lib/hooks';
import { TrashIcon } from '@radix-ui/react-icons';
const DeleteOperationMutation = graphql(`
mutation DeleteOperation($selector: TargetSelectorInput!, $id: ID!) {
deleteOperationInDocumentCollection(selector: $selector, id: $id) {
error {
message
}
ok {
deletedId
updatedTarget {
id
documentCollections {
edges {
cursor
node {
id
operations {
edges {
node {
id
}
cursor
}
}
}
}
}
}
}
}
}
`);
export type DeleteOperationMutationType = typeof DeleteOperationMutation;
export function DeleteOperationModal({
isOpen,
toggleModalOpen,
operationId,
}: {
isOpen: boolean;
toggleModalOpen: () => void;
operationId: string;
}): ReactElement {
const route = useRouteSelector();
const [, mutate] = useMutation(DeleteOperationMutation);
const notify = useNotifications();
return (
<Modal
open={isOpen}
onOpenChange={toggleModalOpen}
className="flex flex-col items-center gap-5"
>
<TrashIcon className="h-16 w-auto text-red-500 opacity-70" />
<Heading>Delete Operation</Heading>
<p className="text-sm text-gray-500">
Are you sure you wish to delete this operation? This action is irreversible!
</p>
<div className="flex w-full gap-2">
<Button type="button" size="large" block onClick={toggleModalOpen}>
Cancel
</Button>
<Button
size="large"
block
danger
onClick={async () => {
const { error } = await mutate({
id: operationId,
selector: {
target: route.targetId,
organization: route.organizationId,
project: route.projectId,
},
});
if (error) {
notify(error.message, 'error');
}
toggleModalOpen();
}}
data-cy="confirm"
>
Delete
</Button>
</div>
</Modal>
);
}

View file

@ -2,10 +2,10 @@ import { ReactElement } from 'react';
import { useRouter } from 'next/router';
import { useMutation } from 'urql';
import { Button, Heading, Modal } from '@/components/v2';
import { TrashIcon } from '@/components/v2/icon';
import { FragmentType, graphql, useFragment } from '@/gql';
import { DeleteOrganizationDocument } from '@/graphql';
import { useRouteSelector } from '@/lib/hooks';
import { TrashIcon } from '@radix-ui/react-icons';
const DeleteOrganizationModal_OrganizationFragment = graphql(`
fragment DeleteOrganizationModal_OrganizationFragment on Organization {
@ -36,7 +36,7 @@ export const DeleteOrganizationModal = ({
onOpenChange={toggleModalOpen}
className="flex flex-col items-center gap-5"
>
<TrashIcon className="h-24 w-24 text-red-500 opacity-70" />
<TrashIcon className="h-16 w-auto text-red-500 opacity-70" />
<Heading>Delete organization</Heading>
<p className="text-sm text-gray-500">
Are you sure you wish to delete this organization? This action is irreversible!

View file

@ -2,9 +2,9 @@ import { ReactElement } from 'react';
import { useRouter } from 'next/router';
import { useMutation } from 'urql';
import { Button, Heading, Modal } from '@/components/v2';
import { TrashIcon } from '@/components/v2/icon';
import { DeleteProjectDocument } from '@/graphql';
import { useRouteSelector } from '@/lib/hooks';
import { TrashIcon } from '@radix-ui/react-icons';
export const DeleteProjectModal = ({
isOpen,
@ -23,7 +23,7 @@ export const DeleteProjectModal = ({
onOpenChange={toggleModalOpen}
className="flex flex-col items-center gap-5"
>
<TrashIcon className="h-24 w-24 text-red-500 opacity-70" />
<TrashIcon className="h-16 w-auto text-red-500 opacity-70" />
<Heading>Delete project</Heading>
<p className="text-sm text-gray-500">
Are you sure you wish to delete this project? This action is irreversible!

View file

@ -2,9 +2,9 @@ import { ReactElement } from 'react';
import { useRouter } from 'next/router';
import { useMutation } from 'urql';
import { Button, Heading, Modal } from '@/components/v2';
import { TrashIcon } from '@/components/v2/icon';
import { DeleteTargetDocument } from '@/graphql';
import { useRouteSelector } from '@/lib/hooks';
import { TrashIcon } from '@radix-ui/react-icons';
export const DeleteTargetModal = ({
isOpen,
@ -23,7 +23,7 @@ export const DeleteTargetModal = ({
onOpenChange={toggleModalOpen}
className="flex flex-col items-center gap-5"
>
<TrashIcon className="h-24 w-24 text-red-500 opacity-70" />
<TrashIcon className="h-16 w-auto text-red-500 opacity-70" />
<Heading>Delete target</Heading>
<p className="text-sm text-gray-500">
Are you sure you wish to delete this target? This action is irreversible!

View file

@ -1,11 +1,16 @@
export { ChangePermissionsModal } from './change-permissions';
export { ConnectLabModal } from './connect-lab';
export { ConnectSchemaModal } from './connect-schema';
export { CreateAccessTokenModal } from './create-access-token';
export { CreateAlertModal } from './create-alert';
export { CreateChannelModal } from './create-channel';
export { CreateCollectionModal } from './create-collection';
export { CreateOperationModal } from './create-operation';
export { CreateProjectModal } from './create-project';
export { CreateTargetModal } from './create-target';
export { DeleteCollectionModal } from './delete-collection';
export { DeleteMembersModal } from './delete-members';
export { DeleteOperationModal } from './delete-operation';
export { DeleteOrganizationModal } from './delete-organization';
export { DeleteProjectModal } from './delete-project';
export { DeleteTargetModal } from './delete-target';

View file

@ -9,7 +9,7 @@ export function Select({
isInvalid,
...props
}: ComponentProps<'select'> & {
options: { name: string; value: string }[];
options?: { name: string; value: string }[];
isInvalid?: boolean;
}): ReactElement {
return (

View file

@ -139,7 +139,7 @@ export function useRedirect({
redirectRef.current = true;
const route = redirectTo(router);
if (route) {
router.push(route.route, route.as);
void router.push(route.route, route.as);
}
}
}, [router, canAccess, redirectRef, redirectTo]);

View file

@ -4,7 +4,7 @@ import { useRedirect } from './common';
export { TargetAccessScope };
const CanAccessTarget_MemberFragment = graphql(`
export const CanAccessTarget_MemberFragment = graphql(`
fragment CanAccessTarget_MemberFragment on Member {
targetAccessScopes
}

View file

@ -1,4 +1,5 @@
export { useClipboard } from './use-clipboard';
export { useCollections } from './use-collections';
export { toDecimal, useDecimal } from './use-decimal';
export { formatDuration, useFormattedDuration } from './use-formatted-duration';
export { formatNumber, useFormattedNumber } from './use-formatted-number';

View file

@ -0,0 +1,70 @@
import { useEffect } from 'react';
import { useQuery } from 'urql';
import { graphql } from '@/gql';
import { TargetDocument } from '@/graphql';
import { useNotifications } from '@/lib/hooks/use-notifications';
import { useRouteSelector } from '@/lib/hooks/use-route-selector';
export const CollectionsQuery = graphql(`
query Collections($selector: TargetSelectorInput!) {
target(selector: $selector) {
id
documentCollections {
edges {
cursor
node {
id
name
operations(first: 100) {
edges {
node {
id
name
}
cursor
}
}
}
}
}
}
}
`);
export function useCollections() {
const router = useRouteSelector();
const [result] = useQuery({
query: TargetDocument,
variables: {
targetId: router.targetId,
organizationId: router.organizationId,
projectId: router.projectId,
},
});
const targetId = result.data?.target?.id as string;
const [{ data, error, fetching }] = useQuery({
query: CollectionsQuery,
variables: {
selector: {
target: router.targetId,
organization: router.organizationId,
project: router.projectId,
},
},
pause: !targetId,
});
const notify = useNotifications();
useEffect(() => {
if (error) {
notify(error.message, 'error');
}
}, [error]);
return {
collections: data?.target?.documentCollections.edges.map(v => v.node) || [],
loading: result.fetching || fetching,
};
}

View file

@ -6,34 +6,18 @@ export type Router = ReturnType<typeof useRouteSelector>;
export function useRouteSelector() {
const router = useRouter();
const push = useCallback(
(
route: string,
as: string,
options?: {
shallow?: boolean;
},
) => {
void router.push(route, as, options);
},
[router],
);
const { push } = router;
const visitHome = useCallback(() => {
push('/', '/');
}, [push]);
const visitHome = useCallback(() => push('/', '/'), [push]);
const visitOrganization = useCallback(
({ organizationId }: { organizationId: string }) => {
push('/[orgId]', `/${organizationId}`);
},
({ organizationId }: { organizationId: string }) => push('/[orgId]', `/${organizationId}`),
[push],
);
const visitProject = useCallback(
({ organizationId, projectId }: { organizationId: string; projectId: string }) => {
push('/[orgId]/[projectId]', `/${organizationId}/${projectId}`);
},
({ organizationId, projectId }: { organizationId: string; projectId: string }) =>
push('/[orgId]/[projectId]', `/${organizationId}/${projectId}`),
[push],
);
@ -46,9 +30,7 @@ export function useRouteSelector() {
organizationId: string;
projectId: string;
targetId: string;
}) => {
push('/[orgId]/[projectId]/[targetId]', `/${organizationId}/${projectId}/${targetId}`);
},
}) => push('/[orgId]/[projectId]/[targetId]', `/${organizationId}/${projectId}/${targetId}`),
[push],
);
@ -75,7 +57,7 @@ export function useRouteSelector() {
router.route.replace(/\[([a-z]+)\]/gi, (_, param) => router.query[param] as string) +
attributesPath;
push(router.route + attributesPath, route, { shallow: true });
return push(router.route + attributesPath, route, { shallow: true });
},
[router, push],
);

View file

@ -24,9 +24,7 @@ const createOIDCSuperTokensProvider = (oidcConfig: {
}): ThirdPartyEmailPasswordNode.TypeProvider => ({
id: 'oidc',
get: (redirectURI, authCodeFromRequest) => ({
getClientId: () => {
return oidcConfig.clientId;
},
getClientId: () => oidcConfig.clientId,
getProfileInfo: async (rawTokenAPIResponse: unknown) => {
const tokenResponse = OIDCTokenSchema.parse(rawTokenAPIResponse);
const rawData: unknown = await fetch(oidcConfig.userinfoEndpoint, {
@ -132,7 +130,7 @@ export const getOIDCThirdPartyEmailPasswordNodeOverrides = (args: {
export const createOIDCSuperTokensNoopProvider = () => ({
id: 'oidc',
get: () => {
get() {
throw new Error('Provider implementation was not provided via overrides.');
},
});

View file

@ -1,6 +1,10 @@
/* eslint-disable import/no-extraneous-dependencies */
import { DocumentNode, Kind } from 'graphql';
import { produce } from 'immer';
import { getOperationName, TypedDocumentNode } from 'urql';
import { TypedDocumentNode } from 'urql';
import type { CreateOperationMutationType } from '@/components/v2/modals/create-operation';
import type { DeleteCollectionMutationType } from '@/components/v2/modals/delete-collection';
import type { DeleteOperationMutationType } from '@/components/v2/modals/delete-operation';
import { ResultOf, VariablesOf } from '@graphql-typed-document-node/core';
import { Cache, QueryInput, UpdateResolver } from '@urql/exchange-graphcache';
import {
@ -27,6 +31,15 @@ import {
TargetsDocument,
TokensDocument,
} from '../graphql';
import { CollectionsQuery } from './hooks/use-collections';
export const getOperationName = (query: DocumentNode): string | void => {
for (const node of query.definitions) {
if (node.kind === Kind.OPERATION_DEFINITION) {
return node.name?.value;
}
}
};
function updateQuery<T, V>(cache: Cache, input: QueryInput<T, V>, recipe: (obj: T) => void) {
return cache.updateQuery(input, (data: T | null) => {
@ -351,6 +364,89 @@ const deleteGitHubIntegration: TypedDocumentNodeUpdateResolver<
);
};
const deleteDocumentCollection: TypedDocumentNodeUpdateResolver<DeleteCollectionMutationType> = (
mutation,
args,
cache,
) => {
cache.updateQuery(
{
query: CollectionsQuery,
variables: {
selector: args.selector,
},
},
data => {
if (data === null) {
return null;
}
return {
...data,
target: Object.assign(
{},
data.target,
mutation.deleteDocumentCollection.ok?.updatedTarget || {},
),
};
},
);
};
const deleteOperationInDocumentCollection: TypedDocumentNodeUpdateResolver<
DeleteOperationMutationType
> = (mutation, args, cache) => {
cache.updateQuery(
{
query: CollectionsQuery,
variables: {
selector: args.selector,
},
},
data => {
if (data === null) {
return null;
}
return {
...data,
target: Object.assign(
{},
data.target,
mutation.deleteOperationInDocumentCollection.ok?.updatedTarget || {},
),
};
},
);
};
const createOperationInDocumentCollection: TypedDocumentNodeUpdateResolver<
CreateOperationMutationType
> = (mutation, args, cache) => {
cache.updateQuery(
{
query: CollectionsQuery,
variables: {
selector: args.selector,
},
},
data => {
if (data === null) {
return null;
}
return {
...data,
target: Object.assign(
{},
data.target,
mutation.createOperationInDocumentCollection.ok?.updatedTarget || {},
),
};
},
);
};
// UpdateResolver
export const Mutation = {
createOrganization,
@ -368,4 +464,7 @@ export const Mutation = {
deleteAlertChannels,
addAlert,
deletePersistedOperation,
deleteDocumentCollection,
deleteOperationInDocumentCollection,
createOperationInDocumentCollection,
};

View file

@ -1,4 +1,4 @@
import { createClient, dedupExchange, errorExchange, fetchExchange } from 'urql';
import { createClient, errorExchange, fetchExchange } from 'urql';
import { cacheExchange } from '@urql/exchange-graphcache';
import { Mutation } from './urql-cache';
import { networkStatusExchange } from './urql-exchanges/state';
@ -10,7 +10,6 @@ const SERVER_BASE_PATH = '/api/proxy';
export const urqlClient = createClient({
url: SERVER_BASE_PATH,
exchanges: [
dedupExchange,
cacheExchange({
updates: {
Mutation,

View file

@ -1,5 +1,5 @@
import type { Meta, StoryObj } from '@storybook/react';
import { Callout } from '../components/v2/callout';
import { Callout } from '../components/v2';
const meta: Meta<typeof Callout> = {
title: 'Callout',

View file

@ -0,0 +1,10 @@
import React from 'react';
// eslint-disable-next-line no-process-env
if (process.env.NODE_ENV === 'development') {
// eslint-disable-next-line @typescript-eslint/no-var-requires, import/no-extraneous-dependencies
const whyDidYouRender = require('@welldone-software/why-did-you-render');
whyDidYouRender(React, {
trackAllPureComponents: true,
});
}

View file

@ -136,6 +136,9 @@ importers:
lint-staged:
specifier: 13.2.2
version: 13.2.2
pg:
specifier: ^8.10.0
version: 8.11.0
prettier:
specifier: 2.8.8
version: 2.8.8
@ -620,6 +623,9 @@ importers:
zod:
specifier: 3.21.4
version: 3.21.4
zod-validation-error:
specifier: 1.3.0
version: 1.3.0(zod@3.21.4)
devDependencies:
'@graphql-hive/core':
specifier: 0.2.3
@ -1447,6 +1453,9 @@ importers:
packages/web/app:
dependencies:
'@graphiql/react':
specifier: 0.18.0-alpha.0
version: 0.18.0-alpha.0(@codemirror/language@6.0.0)(@types/node@18.16.16)(@types/react-dom@18.2.4)(@types/react@18.2.8)(graphql@16.6.0)(react-dom@18.2.0)(react@18.2.0)
'@graphiql/toolkit':
specifier: 0.8.4
version: 0.8.4(@types/node@18.16.16)(graphql@16.6.0)
@ -1532,14 +1541,14 @@ importers:
specifier: 10.29.1
version: 10.29.1
'@urql/core':
specifier: 3.1.1
version: 3.1.1(graphql@16.6.0)
specifier: 4.0.10
version: 4.0.10(graphql@16.6.0)
'@urql/devtools':
specifier: 2.0.3
version: 2.0.3(@urql/core@3.1.1)(graphql@16.6.0)
version: 2.0.3(@urql/core@4.0.10)(graphql@16.6.0)
'@urql/exchange-graphcache':
specifier: 5.0.9
version: 5.0.9(graphql@16.6.0)
specifier: 6.1.1
version: 6.1.1(graphql@16.6.0)
'@whatwg-node/fetch':
specifier: 0.9.3
version: 0.9.3
@ -1565,8 +1574,8 @@ importers:
specifier: 2.2.9
version: 2.2.9(react@18.2.0)
graphiql:
specifier: 2.4.7
version: 2.4.7(@codemirror/language@6.0.0)(@types/node@18.16.16)(@types/react@18.2.8)(graphql@16.6.0)(react-dom@18.2.0)(react-is@17.0.2)(react@18.2.0)
specifier: 3.0.0-alpha.0
version: 3.0.0-alpha.0(@codemirror/language@6.0.0)(@types/node@18.16.16)(@types/react-dom@18.2.4)(@types/react@18.2.8)(graphql@16.6.0)(react-dom@18.2.0)(react@18.2.0)
graphql:
specifier: 16.6.0
version: 16.6.0
@ -1649,8 +1658,8 @@ importers:
specifier: 2.5.3
version: 2.5.3
urql:
specifier: 3.0.3
version: 3.0.3(graphql@16.6.0)(react@18.2.0)
specifier: 4.0.3
version: 4.0.3(graphql@16.6.0)(react@18.2.0)
use-debounce:
specifier: 9.0.4
version: 9.0.4(react@18.2.0)
@ -1809,6 +1818,17 @@ importers:
packages:
/@0no-co/graphql.web@1.0.1(graphql@16.6.0):
resolution: {integrity: sha512-6Yaxyv6rOwRkLIvFaL0NrLDgfNqC/Ng9QOPmTmlqW4mORXMEKmh5NYGkIvvt5Yw8fZesnMAqkj8cIqTj8f40cQ==}
peerDependencies:
graphql: ^14.0.0 || ^15.0.0 || ^16.0.0
peerDependenciesMeta:
graphql:
optional: true
dependencies:
graphql: 16.6.0
dev: false
/@algolia/autocomplete-core@1.9.2(@algolia/client-search@4.17.1)(algoliasearch@4.17.1)(search-insights@2.6.0):
resolution: {integrity: sha512-hkG80c9kx9ClVAEcUJbTd2ziVC713x9Bji9Ty4XJfKXlxlsx3iXsoNhAwfeR4ulzIUg7OE5gez0UU1zVDdG7kg==}
dependencies:
@ -2189,6 +2209,7 @@ packages:
/@apollo/server@4.7.2(graphql@16.6.0):
resolution: {integrity: sha512-GCGgdrz+1+9fNRuj64J4H+iyYNm6C+Uph5kNbYgSEUz8ujjs3P4FKKMbWov2N6z2kany3y+QjOsHXksrhqrkgQ==}
engines: {node: '>=14.16.0'}
requiresBuild: true
peerDependencies:
graphql: ^16.6.0
dependencies:
@ -5088,6 +5109,19 @@ packages:
resolution: {integrity: sha512-14FtKiHhy2QoPIzdTcvh//8OyBlknNs2nXRwIhG904opCby3l+9Xaf/wuPvICBF0rc1ZCNBd3nKe9cd2mecVkQ==}
dev: false
/@emotion/is-prop-valid@0.8.8:
resolution: {integrity: sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==}
requiresBuild: true
dependencies:
'@emotion/memoize': 0.7.4
dev: false
optional: true
/@emotion/memoize@0.7.4:
resolution: {integrity: sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==}
dev: false
optional: true
/@emotion/memoize@0.8.0:
resolution: {integrity: sha512-G/YwXTkv7Den9mXDO7AhLWkE3q+I92B+VqAE+dYG4NGPaHZGvt3G8Q0p9vmE+sq7rTGphUbAvmQ9YpbfMQGGlA==}
dev: false
@ -5830,24 +5864,24 @@ packages:
react-dom: 18.2.0(react@18.2.0)
dev: false
/@graphiql/react@0.17.6(@codemirror/language@6.0.0)(@types/node@18.16.16)(@types/react@18.2.8)(graphql@16.6.0)(react-dom@18.2.0)(react-is@17.0.2)(react@18.2.0):
resolution: {integrity: sha512-3k1paSRbRwVNxr2U80xnRhkws8tSErWlETJvEQBmqRcWbt0+WmwFJorkLnG1n3Wj0Ho6k4a2BAiTfJ6F4SPrLg==}
/@graphiql/react@0.18.0-alpha.0(@codemirror/language@6.0.0)(@types/node@18.16.16)(@types/react-dom@18.2.4)(@types/react@18.2.8)(graphql@16.6.0)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-d+Ra22v9bkyJFhgqZseemBqg+7WFASskerX3Wg5QJ3t3XUSu05teH6MP7R6NtlQhue0VUKjxccp7rkTC48nkMg==}
peerDependencies:
graphql: ^15.5.0 || ^16.0.0
react: ^16.8.0 || ^17.0.0 || ^18.0.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
react: ^16.8.0 || ^17 || ^18
react-dom: ^16.8.0 || ^17 || ^18
dependencies:
'@graphiql/toolkit': 0.8.4(@types/node@18.16.16)(graphql@16.6.0)
'@reach/combobox': 0.17.0(react-dom@18.2.0)(react@18.2.0)
'@reach/dialog': 0.17.0(@types/react@18.2.8)(react-dom@18.2.0)(react@18.2.0)
'@reach/listbox': 0.17.0(react-dom@18.2.0)(react@18.2.0)
'@reach/menu-button': 0.17.0(react-dom@18.2.0)(react-is@17.0.2)(react@18.2.0)
'@reach/tooltip': 0.17.0(react-dom@18.2.0)(react@18.2.0)
'@reach/visually-hidden': 0.17.0(react-dom@18.2.0)(react@18.2.0)
'@headlessui/react': 1.7.15(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-dialog': 1.0.4(@types/react-dom@18.2.4)(@types/react@18.2.8)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-dropdown-menu': 2.0.5(@types/react-dom@18.2.4)(@types/react@18.2.8)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-tooltip': 1.0.6(@types/react-dom@18.2.4)(@types/react@18.2.8)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-visually-hidden': 1.0.3(@types/react-dom@18.2.4)(@types/react@18.2.8)(react-dom@18.2.0)(react@18.2.0)
clsx: 1.2.1
codemirror: 5.65.9
codemirror-graphql: 2.0.8(@codemirror/language@6.0.0)(codemirror@5.65.9)(graphql@16.6.0)
codemirror-graphql: 2.0.9-alpha.0(@codemirror/language@6.0.0)(codemirror@5.65.9)(graphql@16.6.0)
copy-to-clipboard: 3.3.3
framer-motion: 6.5.1(react-dom@18.2.0)(react@18.2.0)
graphql: 16.6.0
graphql-language-service: 5.1.6(graphql@16.6.0)
markdown-it: 12.3.2
@ -5858,8 +5892,8 @@ packages:
- '@codemirror/language'
- '@types/node'
- '@types/react'
- '@types/react-dom'
- graphql-ws
- react-is
dev: false
/@graphiql/toolkit@0.8.4(@types/node@18.16.16)(graphql@16.6.0):
@ -7901,6 +7935,53 @@ packages:
react-dom: 18.2.0(react@18.2.0)
dev: false
/@motionone/animation@10.15.1:
resolution: {integrity: sha512-mZcJxLjHor+bhcPuIFErMDNyrdb2vJur8lSfMCsuCB4UyV8ILZLvK+t+pg56erv8ud9xQGK/1OGPt10agPrCyQ==}
dependencies:
'@motionone/easing': 10.15.1
'@motionone/types': 10.15.1
'@motionone/utils': 10.15.1
tslib: 2.5.3
dev: false
/@motionone/dom@10.12.0:
resolution: {integrity: sha512-UdPTtLMAktHiqV0atOczNYyDd/d8Cf5fFsd1tua03PqTwwCe/6lwhLSQ8a7TbnQ5SN0gm44N1slBfj+ORIhrqw==}
dependencies:
'@motionone/animation': 10.15.1
'@motionone/generators': 10.15.1
'@motionone/types': 10.15.1
'@motionone/utils': 10.15.1
hey-listen: 1.0.8
tslib: 2.5.3
dev: false
/@motionone/easing@10.15.1:
resolution: {integrity: sha512-6hIHBSV+ZVehf9dcKZLT7p5PEKHGhDwky2k8RKkmOvUoYP3S+dXsKupyZpqx5apjd9f+php4vXk4LuS+ADsrWw==}
dependencies:
'@motionone/utils': 10.15.1
tslib: 2.5.3
dev: false
/@motionone/generators@10.15.1:
resolution: {integrity: sha512-67HLsvHJbw6cIbLA/o+gsm7h+6D4Sn7AUrB/GPxvujse1cGZ38F5H7DzoH7PhX+sjvtDnt2IhFYF2Zp1QTMKWQ==}
dependencies:
'@motionone/types': 10.15.1
'@motionone/utils': 10.15.1
tslib: 2.5.3
dev: false
/@motionone/types@10.15.1:
resolution: {integrity: sha512-iIUd/EgUsRZGrvW0jqdst8st7zKTzS9EsKkP+6c6n4MPZoQHwiHuVtTQLD6Kp0bsBLhNzKIBlHXponn/SDT4hA==}
dev: false
/@motionone/utils@10.15.1:
resolution: {integrity: sha512-p0YncgU+iklvYr/Dq4NobTRdAPv9PveRDUXabPEeOjBLSO/1FNB2phNTZxOxpi1/GZwYpAoECEa0Wam+nsmhSw==}
dependencies:
'@motionone/types': 10.15.1
hey-listen: 1.0.8
tslib: 2.5.3
dev: false
/@msgpackr-extract/msgpackr-extract-darwin-arm64@2.1.2:
resolution: {integrity: sha512-TyVLn3S/+ikMDsh0gbKv2YydKClN8HaJDDpONlaZR+LVJmsxLFUgA+O7zu59h9+f9gX1aj/ahw9wqa6rosmrYQ==}
cpu: [arm64]
@ -10358,217 +10439,6 @@ packages:
'@babel/runtime': 7.21.0
dev: false
/@reach/auto-id@0.17.0(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-ud8iPwF52RVzEmkHq1twuqGuPA+moreumUHdtgvU3sr3/15BNhwp3KyDLrKKSz0LP1r3V4pSdyF9MbYM8BoSjA==}
peerDependencies:
react: ^16.8.0 || 17.x
react-dom: ^16.8.0 || 17.x
dependencies:
'@reach/utils': 0.17.0(react-dom@18.2.0)(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
tslib: 2.5.3
dev: false
/@reach/combobox@0.17.0(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-2mYvU5agOBCQBMdlM4cri+P1BbNwp05P1OuDyc33xJSNiBG7BMy4+ZSHJ0X4fyle6rHwSgCAOCLOeWV1XUYjoQ==}
peerDependencies:
react: ^16.8.0 || 17.x
react-dom: ^16.8.0 || 17.x
dependencies:
'@reach/auto-id': 0.17.0(react-dom@18.2.0)(react@18.2.0)
'@reach/descendants': 0.17.0(react-dom@18.2.0)(react@18.2.0)
'@reach/popover': 0.17.0(react-dom@18.2.0)(react@18.2.0)
'@reach/portal': 0.17.0(react-dom@18.2.0)(react@18.2.0)
'@reach/utils': 0.17.0(react-dom@18.2.0)(react@18.2.0)
prop-types: 15.8.1
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
tiny-warning: 1.0.3
tslib: 2.5.3
dev: false
/@reach/descendants@0.17.0(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-c7lUaBfjgcmKFZiAWqhG+VnXDMEhPkI4kAav/82XKZD6NVvFjsQOTH+v3tUkskrAPV44Yuch0mFW/u5Ntifr7Q==}
peerDependencies:
react: ^16.8.0 || 17.x
react-dom: ^16.8.0 || 17.x
dependencies:
'@reach/utils': 0.17.0(react-dom@18.2.0)(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
tslib: 2.5.3
dev: false
/@reach/dialog@0.17.0(@types/react@18.2.8)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-AnfKXugqDTGbeG3c8xDcrQDE4h9b/vnc27Sa118oQSquz52fneUeX9MeFb5ZEiBJK8T5NJpv7QUTBIKnFCAH5A==}
peerDependencies:
react: ^16.8.0 || 17.x
react-dom: ^16.8.0 || 17.x
dependencies:
'@reach/portal': 0.17.0(react-dom@18.2.0)(react@18.2.0)
'@reach/utils': 0.17.0(react-dom@18.2.0)(react@18.2.0)
prop-types: 15.8.1
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
react-focus-lock: 2.9.2(@types/react@18.2.8)(react@18.2.0)
react-remove-scroll: 2.5.5(@types/react@18.2.8)(react@18.2.0)
tslib: 2.5.3
transitivePeerDependencies:
- '@types/react'
dev: false
/@reach/dropdown@0.17.0(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-qBTIGInhxtPHtdj4Pl2XZgZMz3e37liydh0xR3qc48syu7g71sL4nqyKjOzThykyfhA3Pb3/wFgsFJKGTSdaig==}
peerDependencies:
react: ^16.8.0 || 17.x
react-dom: ^16.8.0 || 17.x
dependencies:
'@reach/auto-id': 0.17.0(react-dom@18.2.0)(react@18.2.0)
'@reach/descendants': 0.17.0(react-dom@18.2.0)(react@18.2.0)
'@reach/popover': 0.17.0(react-dom@18.2.0)(react@18.2.0)
'@reach/utils': 0.17.0(react-dom@18.2.0)(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
tslib: 2.5.3
dev: false
/@reach/listbox@0.17.0(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-AMnH1P6/3VKy2V/nPb4Es441arYR+t4YRdh9jdcFVrCOD6y7CQrlmxsYjeg9Ocdz08XpdoEBHM3PKLJqNAUr7A==}
peerDependencies:
react: ^16.8.0 || 17.x
react-dom: ^16.8.0 || 17.x
dependencies:
'@reach/auto-id': 0.17.0(react-dom@18.2.0)(react@18.2.0)
'@reach/descendants': 0.17.0(react-dom@18.2.0)(react@18.2.0)
'@reach/machine': 0.17.0(react-dom@18.2.0)(react@18.2.0)
'@reach/popover': 0.17.0(react-dom@18.2.0)(react@18.2.0)
'@reach/utils': 0.17.0(react-dom@18.2.0)(react@18.2.0)
prop-types: 15.8.1
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: false
/@reach/machine@0.17.0(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-9EHnuPgXzkbRENvRUzJvVvYt+C2jp7PGN0xon7ffmKoK8rTO6eA/bb7P0xgloyDDQtu88TBUXKzW0uASqhTXGA==}
peerDependencies:
react: ^16.8.0 || 17.x
react-dom: ^16.8.0 || 17.x
dependencies:
'@reach/utils': 0.17.0(react-dom@18.2.0)(react@18.2.0)
'@xstate/fsm': 1.4.0
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
tslib: 2.5.3
dev: false
/@reach/menu-button@0.17.0(react-dom@18.2.0)(react-is@17.0.2)(react@18.2.0):
resolution: {integrity: sha512-YyuYVyMZKamPtivoEI6D0UEILYH3qZtg4kJzEAuzPmoR/aHN66NZO75Fx0gtjG1S6fZfbiARaCOZJC0VEiDOtQ==}
peerDependencies:
react: ^16.8.0 || 17.x
react-dom: ^16.8.0 || 17.x
react-is: ^16.8.0 || 17.x
dependencies:
'@reach/dropdown': 0.17.0(react-dom@18.2.0)(react@18.2.0)
'@reach/popover': 0.17.0(react-dom@18.2.0)(react@18.2.0)
'@reach/utils': 0.17.0(react-dom@18.2.0)(react@18.2.0)
prop-types: 15.8.1
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
react-is: 17.0.2
tiny-warning: 1.0.3
tslib: 2.5.3
dev: false
/@reach/observe-rect@1.2.0:
resolution: {integrity: sha512-Ba7HmkFgfQxZqqaeIWWkNK0rEhpxVQHIoVyW1YDSkGsGIXzcaW4deC8B0pZrNSSyLTdIk7y+5olKt5+g0GmFIQ==}
dev: false
/@reach/popover@0.17.0(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-yYbBF4fMz4Ml4LB3agobZjcZ/oPtPsNv70ZAd7lEC2h7cvhF453pA+zOBGYTPGupKaeBvgAnrMjj7RnxDU5hoQ==}
peerDependencies:
react: ^16.8.0 || 17.x
react-dom: ^16.8.0 || 17.x
dependencies:
'@reach/portal': 0.17.0(react-dom@18.2.0)(react@18.2.0)
'@reach/rect': 0.17.0(react-dom@18.2.0)(react@18.2.0)
'@reach/utils': 0.17.0(react-dom@18.2.0)(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
tabbable: 4.0.0
tslib: 2.5.3
dev: false
/@reach/portal@0.17.0(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-+IxsgVycOj+WOeNPL2NdgooUdHPSY285wCtj/iWID6akyr4FgGUK7sMhRM9aGFyrGpx2vzr+eggbUmAVZwOz+A==}
peerDependencies:
react: ^16.8.0 || 17.x
react-dom: ^16.8.0 || 17.x
dependencies:
'@reach/utils': 0.17.0(react-dom@18.2.0)(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
tiny-warning: 1.0.3
tslib: 2.5.3
dev: false
/@reach/rect@0.17.0(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-3YB7KA5cLjbLc20bmPkJ06DIfXSK06Cb5BbD2dHgKXjUkT9WjZaLYIbYCO8dVjwcyO3GCNfOmPxy62VsPmZwYA==}
peerDependencies:
react: ^16.8.0 || 17.x
react-dom: ^16.8.0 || 17.x
dependencies:
'@reach/observe-rect': 1.2.0
'@reach/utils': 0.17.0(react-dom@18.2.0)(react@18.2.0)
prop-types: 15.8.1
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
tiny-warning: 1.0.3
tslib: 2.5.3
dev: false
/@reach/tooltip@0.17.0(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-HP8Blordzqb/Cxg+jnhGmWQfKgypamcYLBPlcx6jconyV5iLJ5m93qipr1giK7MqKT2wlsKWy44ZcOrJ+Wrf8w==}
peerDependencies:
react: ^16.8.0 || 17.x
react-dom: ^16.8.0 || 17.x
dependencies:
'@reach/auto-id': 0.17.0(react-dom@18.2.0)(react@18.2.0)
'@reach/portal': 0.17.0(react-dom@18.2.0)(react@18.2.0)
'@reach/rect': 0.17.0(react-dom@18.2.0)(react@18.2.0)
'@reach/utils': 0.17.0(react-dom@18.2.0)(react@18.2.0)
'@reach/visually-hidden': 0.17.0(react-dom@18.2.0)(react@18.2.0)
prop-types: 15.8.1
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
tiny-warning: 1.0.3
tslib: 2.5.3
dev: false
/@reach/utils@0.17.0(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-M5y8fCBbrWeIsxedgcSw6oDlAMQDkl5uv3VnMVJ7guwpf4E48Xlh1v66z/1BgN/WYe2y8mB/ilFD2nysEfdGeA==}
peerDependencies:
react: ^16.8.0 || 17.x
react-dom: ^16.8.0 || 17.x
dependencies:
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
tiny-warning: 1.0.3
tslib: 2.5.3
dev: false
/@reach/visually-hidden@0.17.0(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-T6xF3Nv8vVnjVkGU6cm0+kWtvliLqPAo8PcZ+WxkKacZsaHTjaZb4v1PaCcyQHmuTNT/vtTVNOJLG0SjQOIb7g==}
peerDependencies:
react: ^16.8.0 || 17.x
react-dom: ^16.8.0 || 17.x
dependencies:
prop-types: 15.8.1
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
tslib: 2.5.3
dev: false
/@repeaterjs/repeater@3.0.4:
resolution: {integrity: sha512-AW8PKd6iX3vAZ0vA43nOUOnbq/X5ihgU+mSXXqunMkeQADGiqw/PY0JNeYtD5sr0PAy51YPgAPbDoeapv9r8WA==}
@ -13249,34 +13119,34 @@ packages:
eslint-visitor-keys: 3.4.1
dev: true
/@urql/core@3.1.1(graphql@16.6.0):
resolution: {integrity: sha512-Mnxtq4I4QeFJsgs7Iytw+HyhiGxISR6qtyk66c9tipozLZ6QVxrCiUPF2HY4BxNIabaxcp+rivadvm8NAnXj4Q==}
peerDependencies:
graphql: ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0
/@urql/core@4.0.10(graphql@16.6.0):
resolution: {integrity: sha512-Vs3nOSAnYftqOCg034Ostp/uSqWlQg5ryLIzcOrm8+O43s4M+Ew4GQAuemIH7ZDB8dek6h61zzWI3ujd8FH3NA==}
dependencies:
graphql: 16.6.0
'@0no-co/graphql.web': 1.0.1(graphql@16.6.0)
wonka: 6.3.2
transitivePeerDependencies:
- graphql
dev: false
/@urql/devtools@2.0.3(@urql/core@3.1.1)(graphql@16.6.0):
/@urql/devtools@2.0.3(@urql/core@4.0.10)(graphql@16.6.0):
resolution: {integrity: sha512-TktPLiBS9LcBPHD6qcnb8wqOVcg3Bx0iCtvQ80uPpfofwwBGJmqnQTjUdEFU6kwaLOFZULQ9+Uo4831G823mQw==}
peerDependencies:
'@urql/core': '>= 1.14.0'
graphql: '>= 0.11.0'
dependencies:
'@urql/core': 3.1.1(graphql@16.6.0)
'@urql/core': 4.0.10(graphql@16.6.0)
graphql: 16.6.0
wonka: 6.3.2
dev: false
/@urql/exchange-graphcache@5.0.9(graphql@16.6.0):
resolution: {integrity: sha512-GYhN4YDL+/alF6ci1OFOaL0Ze0GinmuYWrjQjpjl8kP/9ZbXY2LLN/3ZlRG9cCks1nxyea/9h4IS0mk5LS57lA==}
peerDependencies:
graphql: ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0
/@urql/exchange-graphcache@6.1.1(graphql@16.6.0):
resolution: {integrity: sha512-R4BOZLPSfSWVMhfJMRIGY6ktbnIa0EkmMGeXa78SyfLq/g8RgA+8zk9znVVFI0q8LadKCMoPpxIbsx7wJxx7RA==}
dependencies:
'@urql/core': 3.1.1(graphql@16.6.0)
graphql: 16.6.0
'@0no-co/graphql.web': 1.0.1(graphql@16.6.0)
'@urql/core': 4.0.10(graphql@16.6.0)
wonka: 6.3.2
transitivePeerDependencies:
- graphql
dev: false
/@vercel/ncc@0.36.1:
@ -13666,10 +13536,6 @@ packages:
'@whatwg-node/fetch': 0.9.3
tslib: 2.5.3
/@xstate/fsm@1.4.0:
resolution: {integrity: sha512-uTHDeu2xI5E1IFwf37JFQM31RrH7mY7877RqPBS4ZqSNUwoLDuct8AhBWaXGnVizBAYyimVwgCyGa9z/NiRhXA==}
dev: false
/@xtuc/ieee754@1.2.0:
resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==}
@ -15734,8 +15600,8 @@ packages:
resolution: {integrity: sha512-RpAVKQA5T63xEj6/giIbUEtZwJ4UFIc3ZtvEkiaUERylqe8xb5IvqcgOurZLahv93CLKfxcw5YI+DZcUBRyLXA==}
engines: {node: '>=0.10.0'}
/codemirror-graphql@2.0.8(@codemirror/language@6.0.0)(codemirror@5.65.9)(graphql@16.6.0):
resolution: {integrity: sha512-EU+pXsSKZJAFVdF8j5hbB5gqXsDDjsBiJoohQq09yhsr69pzaI8ZrXjmpuR4CMyf9jgqcz5KK7rsTmxDHmeJPQ==}
/codemirror-graphql@2.0.9-alpha.0(@codemirror/language@6.0.0)(codemirror@5.65.9)(graphql@16.6.0):
resolution: {integrity: sha512-52UDU1rHCLkju/4tQQXkG/3fmYH+leeKPV9DaRERRQK+/HIbfSxeunslKSCL5qjaQa18heZCI4J0lKl5IAXx5Q==}
peerDependencies:
'@codemirror/language': 6.0.0
codemirror: ^5.65.3
@ -18756,13 +18622,6 @@ packages:
engines: {node: '>=8'}
dev: false
/focus-lock@0.11.4:
resolution: {integrity: sha512-LzZWJcOBIcHslQ46N3SUu/760iLPSrUtp8omM4gh9du438V2CQdks8TcOu1yvmu2C68nVOBnl1WFiKGPbQ8L6g==}
engines: {node: '>=10'}
dependencies:
tslib: 2.5.3
dev: false
/focus-trap-react@10.1.4(prop-types@15.8.1)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-vLUQRXI6SUJD8YLYTBa1DlCYRmTKFDxRvc4TEe2nq8S1aj+YKsucuNxqZUOf0+RZ01Yoiwtk/6rD9xqSvawIvQ==}
peerDependencies:
@ -18938,6 +18797,30 @@ packages:
map-cache: 0.2.2
dev: false
/framer-motion@6.5.1(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-o1BGqqposwi7cgDrtg0dNONhkmPsUFDaLcKXigzuTFC5x58mE8iyTazxSudFzmT6MEyJKfjjU8ItoMe3W+3fiw==}
peerDependencies:
react: '>=16.8 || ^17.0.0 || ^18.0.0'
react-dom: '>=16.8 || ^17.0.0 || ^18.0.0'
dependencies:
'@motionone/dom': 10.12.0
framesync: 6.0.1
hey-listen: 1.0.8
popmotion: 11.0.3
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
style-value-types: 5.0.0
tslib: 2.5.3
optionalDependencies:
'@emotion/is-prop-valid': 0.8.8
dev: false
/framesync@6.0.1:
resolution: {integrity: sha512-fUY88kXvGiIItgNC7wcTOl0SNRCVXMKSWW2Yzfmn7EKNc+MpCzcz9DhdHcdjbrtN3c6R4H5dTY2jiCpPdysEjA==}
dependencies:
tslib: 2.5.3
dev: false
/fresh@0.5.2:
resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==}
engines: {node: '>= 0.6'}
@ -19476,14 +19359,14 @@ packages:
/graphemer@1.4.0:
resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==}
/graphiql@2.4.7(@codemirror/language@6.0.0)(@types/node@18.16.16)(@types/react@18.2.8)(graphql@16.6.0)(react-dom@18.2.0)(react-is@17.0.2)(react@18.2.0):
resolution: {integrity: sha512-Fm3fVI65EPyXy+PdbeQUyODTwl2NhpZ47msGnGwpDvdEzYdgF7pPrxL96xCfF31KIauS4+ceEJ+ZwEe5iLWiQw==}
/graphiql@3.0.0-alpha.0(@codemirror/language@6.0.0)(@types/node@18.16.16)(@types/react-dom@18.2.4)(@types/react@18.2.8)(graphql@16.6.0)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-7674fsWb7kFgHbTzNqi1VDB4UJb2ZY3MBmS+agUCZWWCPNmxh2e/4+PftdRDwkH8FaOme8jHTWsqNZYXK+L9uw==}
peerDependencies:
graphql: ^15.5.0 || ^16.0.0
react: ^16.8.0 || ^17.0.0 || ^18.0.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
react: ^16.8.0 || ^17 || ^18
react-dom: ^16.8.0 || ^17 || ^18
dependencies:
'@graphiql/react': 0.17.6(@codemirror/language@6.0.0)(@types/node@18.16.16)(@types/react@18.2.8)(graphql@16.6.0)(react-dom@18.2.0)(react-is@17.0.2)(react@18.2.0)
'@graphiql/react': 0.18.0-alpha.0(@codemirror/language@6.0.0)(@types/node@18.16.16)(@types/react-dom@18.2.4)(@types/react@18.2.8)(graphql@16.6.0)(react-dom@18.2.0)(react@18.2.0)
'@graphiql/toolkit': 0.8.4(@types/node@18.16.16)(graphql@16.6.0)
graphql: 16.6.0
graphql-language-service: 5.1.6(graphql@16.6.0)
@ -19494,8 +19377,8 @@ packages:
- '@codemirror/language'
- '@types/node'
- '@types/react'
- '@types/react-dom'
- graphql-ws
- react-is
dev: false
/graphql-config@4.5.0(@types/node@18.16.16)(graphql@16.6.0):
@ -20013,6 +19896,10 @@ packages:
readable-stream: 3.6.0
dev: true
/hey-listen@1.0.8:
resolution: {integrity: sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q==}
dev: false
/highlight-words-core@1.2.2:
resolution: {integrity: sha512-BXUKIkUuh6cmmxzi5OIbUJxrG8OAk2MqoL1DtO3Wo9D2faJg2ph5ntyuQeLqaHJmzER6H5tllCDA9ZnNe9BVGg==}
dev: false
@ -25557,6 +25444,15 @@ packages:
engines: {node: '>=12.0.0'}
dev: false
/popmotion@11.0.3:
resolution: {integrity: sha512-Y55FLdj3UxkR7Vl3s7Qr4e9m0onSnP8W7d/xQLsoJM40vs6UKHFdygs6SWryasTZYqugMjm3BepCF4CWXDiHgA==}
dependencies:
framesync: 6.0.1
hey-listen: 1.0.8
style-value-types: 5.0.0
tslib: 2.5.3
dev: false
/posix-character-classes@0.1.1:
resolution: {integrity: sha512-xTgYBc3fuo7Yt7JbiuFxSYGToMoz8fLoE6TC9Wx1P/u+LfeThMOAqmuyECnlBaaJb+u1m9hHiXUEtwW4OzfUJg==}
engines: {node: '>=0.10.0'}
@ -26496,15 +26392,6 @@ packages:
react: 18.2.0
dev: false
/react-clientside-effect@1.2.6(react@18.2.0):
resolution: {integrity: sha512-XGGGRQAKY+q25Lz9a/4EPqom7WRjz3z9R2k4jhVKA/puQFH/5Nt27vFZYql4m4NVNdUvX8PS3O7r/Zzm7cjUlg==}
peerDependencies:
react: ^15.3.0 || ^16.0.0 || ^17.0.0 || ^18.0.0
dependencies:
'@babel/runtime': 7.21.0
react: 18.2.0
dev: false
/react-colorful@5.6.1(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-1exovf0uGTGyq5mXQT0zgQ80uvj2PCwvF8zY1RN9/vbJVSjSo3fsB/4L3ObbF7u70NduSiK4xu4Y6q1MHoUGEw==}
peerDependencies:
@ -26586,25 +26473,6 @@ packages:
resolution: {integrity: sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA==}
dev: false
/react-focus-lock@2.9.2(@types/react@18.2.8)(react@18.2.0):
resolution: {integrity: sha512-5JfrsOKyA5Zn3h958mk7bAcfphr24jPoMoznJ8vaJF6fUrPQ8zrtEd3ILLOK8P5jvGxdMd96OxWNjDzATfR2qw==}
peerDependencies:
'@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0
react: ^16.8.0 || ^17.0.0 || ^18.0.0
peerDependenciesMeta:
'@types/react':
optional: true
dependencies:
'@babel/runtime': 7.21.0
'@types/react': 18.2.8
focus-lock: 0.11.4
prop-types: 15.8.1
react: 18.2.0
react-clientside-effect: 1.2.6(react@18.2.0)
use-callback-ref: 1.3.0(@types/react@18.2.8)(react@18.2.0)
use-sidecar: 1.1.2(@types/react@18.2.8)(react@18.2.0)
dev: false
/react-highlight-words@0.20.0(react@18.2.0):
resolution: {integrity: sha512-asCxy+jCehDVhusNmCBoxDf2mm1AJ//D+EzDx1m5K7EqsMBIHdZ5G4LdwbSEXqZq1Ros0G0UySWmAtntSph7XA==}
peerDependencies:
@ -26669,6 +26537,7 @@ packages:
/react-is@17.0.2:
resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==}
dev: true
/react-is@18.1.0:
resolution: {integrity: sha512-Fl7FuabXsJnV5Q1qIOQwx/sagGF18kogb4gpfcG4gjLBWO0WDiiz1ko/ExayuxE7InyQkBLkxRFG5oxY6Uu3Kg==}
@ -28708,6 +28577,13 @@ packages:
inline-style-parser: 0.1.1
dev: false
/style-value-types@5.0.0:
resolution: {integrity: sha512-08yq36Ikn4kx4YU6RD7jWEv27v4V+PUsOGa4n/as8Et3CuODMJQ00ENeAVXAeydX4Z2j1XHZF1K2sX4mGl18fA==}
dependencies:
hey-listen: 1.0.8
tslib: 2.5.3
dev: false
/styled-jsx@5.1.1(@babel/core@7.21.5)(react@18.2.0):
resolution: {integrity: sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==}
engines: {node: '>= 12.0.0'}
@ -28892,10 +28768,6 @@ packages:
tslib: 2.5.3
dev: true
/tabbable@4.0.0:
resolution: {integrity: sha512-H1XoH1URcBOa/rZZWxLxHCtOdVUEev+9vo5YdYhC9tCY4wnybX+VQrCYuy9ubkg69fCBxCONJOSLGfw0DWMffQ==}
dev: false
/tabbable@6.1.2:
resolution: {integrity: sha512-qCN98uP7i9z0fIS4amQ5zbGBOq+OSigYeGvPy7NDk8Y9yncqDZ9pRPgfsc2PJIVM9RrJj7GIfuRgmjoUU9zTHQ==}
dev: false
@ -30080,16 +29952,16 @@ packages:
/urlpattern-polyfill@9.0.0:
resolution: {integrity: sha512-WHN8KDQblxd32odxeIgo83rdVDE2bvdkb86it7bMhYZwWKJz0+O0RK/eZiHYnM+zgt/U7hAHOlCQGfjjvSkw2g==}
/urql@3.0.3(graphql@16.6.0)(react@18.2.0):
resolution: {integrity: sha512-aVUAMRLdc5AOk239DxgXt6ZxTl/fEmjr7oyU5OGo8uvpqu42FkeJErzd2qBzhAQ3DyusoZIbqbBLPlnKo/yy2A==}
/urql@4.0.3(graphql@16.6.0)(react@18.2.0):
resolution: {integrity: sha512-ynwez59f5uo31RO9/OD1RveasLGsaRC2mEm5nNGuPiQbpIeuZLQJTya61hr6I9c24YWIHf3Ld29xZy6JW3scvQ==}
peerDependencies:
graphql: ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0
react: '>= 16.8.0'
dependencies:
'@urql/core': 3.1.1(graphql@16.6.0)
graphql: 16.6.0
'@urql/core': 4.0.10(graphql@16.6.0)
react: 18.2.0
wonka: 6.3.2
transitivePeerDependencies:
- graphql
dev: false
/use-callback-ref@1.3.0(@types/react@18.2.8)(react@18.2.0):