mirror of
https://github.com/graphql-hive/console
synced 2026-04-21 14:37:17 +00:00
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:
parent
c3da2e8666
commit
1cc2a0adca
66 changed files with 3510 additions and 459 deletions
|
|
@ -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'],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
|||
2
.github/workflows/lint.yaml
vendored
2
.github/workflows/lint.yaml
vendored
|
|
@ -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 \
|
||||
|
|
|
|||
2
.github/workflows/typescript-typecheck.yaml
vendored
2
.github/workflows/typescript-typecheck.yaml
vendored
|
|
@ -18,3 +18,5 @@ jobs:
|
|||
|
||||
- name: typecheck
|
||||
run: pnpm typecheck
|
||||
env:
|
||||
NODE_OPTIONS: '--max-old-space-size=4096'
|
||||
|
|
|
|||
3
.vscode/extensions.json
vendored
3
.vscode/extensions.json
vendored
|
|
@ -4,6 +4,7 @@
|
|||
"fabiospampinato.vscode-commands",
|
||||
"esbenp.prettier-vscode",
|
||||
"thebearingedge.vscode-sql-lit",
|
||||
"hashicorp.hcl"
|
||||
"hashicorp.hcl",
|
||||
"GraphQL.vscode-graphql"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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: [],
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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}"]`);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
204
integration-tests/testkit/collections.ts
Normal file
204
integration-tests/testkit/collections.ts
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`);
|
||||
|
|
@ -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 = [],
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}]
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
|
|
@ -0,0 +1 @@
|
|||
raise 'down migration not implemented'
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
12
packages/services/api/src/modules/collection/index.ts
Normal file
12
packages/services/api/src/modules/collection/index.ts
Normal 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],
|
||||
});
|
||||
164
packages/services/api/src/modules/collection/module.graphql.ts
Normal file
164
packages/services/api/src/modules/collection/module.graphql.ts
Normal 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
|
||||
}
|
||||
`;
|
||||
|
|
@ -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 });
|
||||
}
|
||||
}
|
||||
251
packages/services/api/src/modules/collection/resolvers.ts
Normal file
251
packages/services/api/src/modules/collection/resolvers.ts
Normal 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!,
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"main": "./src/index.ts",
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
|
||||
|
|
|
|||
|
|
@ -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} />
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
270
packages/web/app/src/components/v2/modals/create-collection.tsx
Normal file
270
packages/web/app/src/components/v2/modals/create-collection.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
201
packages/web/app/src/components/v2/modals/create-operation.tsx
Normal file
201
packages/web/app/src/components/v2/modals/create-operation.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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`}?
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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!
|
||||
|
|
|
|||
|
|
@ -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!
|
||||
|
|
|
|||
|
|
@ -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!
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
70
packages/web/app/src/lib/hooks/use-collections.ts
Normal file
70
packages/web/app/src/lib/hooks/use-collections.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
|
|
@ -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],
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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.');
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
10
packages/web/app/src/wdyr.ts
Normal file
10
packages/web/app/src/wdyr.ts
Normal 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,
|
||||
});
|
||||
}
|
||||
474
pnpm-lock.yaml
474
pnpm-lock.yaml
|
|
@ -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):
|
||||
|
|
|
|||
Loading…
Reference in a new issue