diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 78886f601..af6e404a3 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -1,5 +1,7 @@ /* eslint-env node */ +// eslint-disable-next-line @typescript-eslint/no-var-requires const guildConfig = require('@theguild/eslint-config/base'); +// eslint-disable-next-line @typescript-eslint/no-var-requires const { REACT_RESTRICTED_SYNTAX, RESTRICTED_SYNTAX } = require('@theguild/eslint-config/constants'); const rulesToExtends = Object.fromEntries( @@ -40,6 +42,8 @@ module.exports = { 'packages/web/app/src/graphql/index.ts', 'packages/libraries/cli/src/sdk.ts', 'packages/services/storage/src/db/types.ts', + 'codegen.cjs', + 'packages/web/app/src/gql/**/*', ], parserOptions: { ecmaVersion: 2020, diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index 80c4a34ea..532c22d92 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -48,7 +48,7 @@ jobs: - name: Operation Check run: | npx graphql-inspector validate \ - "packages/web/app/**/*.graphql|packages/libraries/cli/**/*.graphql|packages/web/app/**/*.ts(x)" \ + "packages/web/app/**/*.graphql|packages/libraries/cli/**/*.graphql|packages/web/app/**/*.tsx|packages/web/app/src/lib/**/*.ts" \ "packages/**/module.graphql.ts" \ --maxDepth=20 \ --maxAliasCount=20 \ diff --git a/.prettierignore b/.prettierignore index b9d9642c9..a695d8263 100644 --- a/.prettierignore +++ b/.prettierignore @@ -9,8 +9,7 @@ __generated__/ /integration-tests/testkit/gql/ /packages/services/storage/src/db/types.ts /packages/libraries/cli/src/sdk.ts -/packages/web/app/src/gql/gql.d.ts -/packages/web/app/src/gql/graphql.ts +/packages/web/app/src/gql/** /packages/web/app/src/graphql/index.ts /packages/web/app/next.config.mjs diff --git a/codegen.cjs b/codegen.cjs index 0d7b237a6..052266a30 100644 --- a/codegen.cjs +++ b/codegen.cjs @@ -133,14 +133,8 @@ const config = { './packages/web/app/src/graphql', '!./packages/web/app/pages/api/github/setup-callback.ts', ], - preset: 'gql-tag-operations-preset', + preset: 'client', plugins: [], - config: { - dedupeFragments: true, - }, - presetConfig: { - augmentedModuleName: '@urql/core', - }, }, // CLI 'packages/libraries/cli/src/sdk.ts': { @@ -162,7 +156,7 @@ const config = { // Integration tests './integration-tests/testkit/gql/': { documents: './integration-tests/(testkit|tests)/**/*.ts', - preset: 'client-preset', + preset: 'client', plugins: [], }, }, diff --git a/integration-tests/testkit/flow.ts b/integration-tests/testkit/flow.ts index e83f5a70c..beea6bd3b 100644 --- a/integration-tests/testkit/flow.ts +++ b/integration-tests/testkit/flow.ts @@ -39,7 +39,7 @@ export function waitFor(ms: number) { export function createOrganization(input: CreateOrganizationInput, authToken: string) { return execute({ - document: graphql(/* GraphQL */ ` + document: graphql(` mutation createOrganization($input: CreateOrganizationInput!) { createOrganization(input: $input) { ok { @@ -75,7 +75,7 @@ export function createOrganization(input: CreateOrganizationInput, authToken: st export function getOrganization(organizationId: string, authToken: string) { return execute({ - document: graphql(/* GraphQL */ ` + document: graphql(` query getOrganization($organizationId: ID!) { organization(selector: { organization: $organizationId }) { organization { @@ -104,7 +104,7 @@ export function getOrganization(organizationId: string, authToken: string) { export function inviteToOrganization(input: InviteToOrganizationByEmailInput, authToken: string) { return execute({ - document: graphql(/* GraphQL */ ` + document: graphql(` mutation inviteToOrganization($input: InviteToOrganizationByEmailInput!) { inviteToOrganizationByEmail(input: $input) { ok { @@ -129,7 +129,7 @@ export function inviteToOrganization(input: InviteToOrganizationByEmailInput, au export function renameOrganization(input: UpdateOrganizationNameInput, authToken: string) { return execute({ - document: graphql(/* GraphQL */ ` + document: graphql(` mutation updateOrganizationName($input: UpdateOrganizationNameInput!) { updateOrganizationName(input: $input) { ok { @@ -159,7 +159,7 @@ export function renameOrganization(input: UpdateOrganizationNameInput, authToken export function joinOrganization(code: string, authToken: string) { return execute({ - document: graphql(/* GraphQL */ ` + document: graphql(` mutation joinOrganization($code: String!) { joinOrganization(code: $code) { __typename @@ -191,7 +191,7 @@ export function joinOrganization(code: string, authToken: string) { export function getOrganizationMembers(selector: OrganizationSelectorInput, authToken: string) { return execute({ - document: graphql(/* GraphQL */ ` + document: graphql(` query getOrganizationMembers($selector: OrganizationSelectorInput!) { organization(selector: $selector) { organization { @@ -222,7 +222,7 @@ export function getOrganizationTransferRequest( authToken: string, ) { return execute({ - document: graphql(/* GraphQL */ ` + document: graphql(` query getOrganizationTransferRequest($selector: OrganizationTransferRequestSelector!) { organizationTransferRequest(selector: $selector) { organization { @@ -243,7 +243,7 @@ export function requestOrganizationTransfer( authToken: string, ) { return execute({ - document: graphql(/* GraphQL */ ` + document: graphql(` mutation requestOrganizationTransfer($input: RequestOrganizationTransferInput!) { requestOrganizationTransfer(input: $input) { ok { @@ -268,7 +268,7 @@ export function answerOrganizationTransferRequest( authToken: string, ) { return execute({ - document: graphql(/* GraphQL */ ` + document: graphql(` mutation answerOrganizationTransferRequest($input: AnswerOrganizationTransferRequestInput!) { answerOrganizationTransferRequest(input: $input) { ok { @@ -289,7 +289,7 @@ export function answerOrganizationTransferRequest( export function createProject(input: CreateProjectInput, authToken: string) { return execute({ - document: graphql(/* GraphQL */ ` + document: graphql(` mutation createProject($input: CreateProjectInput!) { createProject(input: $input) { ok { @@ -315,7 +315,7 @@ export function createProject(input: CreateProjectInput, authToken: string) { export function renameProject(input: UpdateProjectNameInput, authToken: string) { return execute({ - document: graphql(/* GraphQL */ ` + document: graphql(` mutation updateProjectName($input: UpdateProjectNameInput!) { updateProjectName(input: $input) { ok { @@ -344,7 +344,7 @@ export function renameProject(input: UpdateProjectNameInput, authToken: string) export function updateRegistryModel(input: UpdateProjectRegistryModelInput, authToken: string) { return execute({ - document: graphql(/* GraphQL */ ` + document: graphql(` mutation updateRegistryModel($input: UpdateProjectRegistryModelInput!) { updateProjectRegistryModel(input: $input) { ok { @@ -367,7 +367,7 @@ export function updateRegistryModel(input: UpdateProjectRegistryModelInput, auth export function createTarget(input: CreateTargetInput, authToken: string) { return execute({ - document: graphql(/* GraphQL */ ` + document: graphql(` mutation createTarget($input: CreateTargetInput!) { createTarget(input: $input) { ok { @@ -391,7 +391,7 @@ export function createTarget(input: CreateTargetInput, authToken: string) { export function renameTarget(input: UpdateTargetNameInput, authToken: string) { return execute({ - document: graphql(/* GraphQL */ ` + document: graphql(` mutation updateTargetName($input: UpdateTargetNameInput!) { updateTargetName(input: $input) { ok { @@ -421,7 +421,7 @@ export function renameTarget(input: UpdateTargetNameInput, authToken: string) { export function createToken(input: CreateTokenInput, authToken: string) { return execute({ - document: graphql(/* GraphQL */ ` + document: graphql(` mutation createToken($input: CreateTokenInput!) { createToken(input: $input) { ok { @@ -442,7 +442,7 @@ export function createToken(input: CreateTokenInput, authToken: string) { export function deleteTokens(input: DeleteTokensInput, authToken: string) { return execute({ - document: graphql(/* GraphQL */ ` + document: graphql(` mutation deleteTokens($input: DeleteTokensInput!) { deleteTokens(input: $input) { deletedTokens @@ -458,7 +458,7 @@ export function deleteTokens(input: DeleteTokensInput, authToken: string) { export function readTokenInfo(token: string) { return execute({ - document: graphql(/* GraphQL */ ` + document: graphql(` query readTokenInfo { tokenInfo { __typename @@ -494,7 +494,7 @@ export function readTokenInfo(token: string) { export function updateMemberAccess(input: OrganizationMemberAccessInput, authToken: string) { return execute({ - document: graphql(/* GraphQL */ ` + document: graphql(` mutation updateOrganizationMemberAccess($input: OrganizationMemberAccessInput!) { updateOrganizationMemberAccess(input: $input) { organization { @@ -527,7 +527,7 @@ export function publishSchema( authHeader?: 'x-api-token' | 'authorization', ) { return execute({ - document: graphql(/* GraphQL */ ` + document: graphql(` mutation schemaPublish($input: SchemaPublishInput!) { schemaPublish(input: $input) { __typename @@ -574,7 +574,7 @@ export function publishSchema( export function checkSchema(input: SchemaCheckInput, token: string) { return execute({ - document: graphql(/* GraphQL */ ` + document: graphql(` mutation schemaCheck($input: SchemaCheckInput!) { schemaCheck(input: $input) { ... on SchemaCheckSuccess { @@ -621,7 +621,7 @@ export function deleteSchema( authHeader?: 'x-api-token' | 'authorization', ) { return execute({ - document: graphql(/* GraphQL */ ` + document: graphql(` mutation schemaDelete($input: SchemaDeleteInput!) { schemaDelete(input: $input) { __typename @@ -647,7 +647,7 @@ export function setTargetValidation( }, ) { return execute({ - document: graphql(/* GraphQL */ ` + document: graphql(` mutation setTargetValidation($input: SetTargetValidationInput!) { setTargetValidation(input: $input) { enabled @@ -675,7 +675,7 @@ export function updateTargetValidationSettings( }, ) { return execute({ - document: graphql(/* GraphQL */ ` + document: graphql(` mutation updateTargetValidationSettings($input: UpdateTargetValidationSettingsInput!) { updateTargetValidationSettings(input: $input) { ok { @@ -709,7 +709,7 @@ export function updateTargetValidationSettings( export function updateBaseSchema(input: UpdateBaseSchemaInput, token: string) { return execute({ - document: graphql(/* GraphQL */ ` + document: graphql(` mutation updateBaseSchema($input: UpdateBaseSchemaInput!) { updateBaseSchema(input: $input) { __typename @@ -725,7 +725,7 @@ export function updateBaseSchema(input: UpdateBaseSchemaInput, token: string) { export function readOperationsStats(input: OperationsStatsSelectorInput, token: string) { return execute({ - document: graphql(/* GraphQL */ ` + document: graphql(` query readOperationsStats($input: OperationsStatsSelectorInput!) { operationsStats(selector: $input) { totalOperations @@ -757,7 +757,7 @@ export function readOperationsStats(input: OperationsStatsSelectorInput, token: export function readOperationBody(selector: OperationBodyByHashInput, token: string) { return execute({ - document: graphql(/* GraphQL */ ` + document: graphql(` query readOperationBody($selector: OperationBodyByHashInput!) { operationBodyByHash(selector: $selector) } @@ -771,7 +771,7 @@ export function readOperationBody(selector: OperationBodyByHashInput, token: str export function fetchLatestSchema(token: string) { return execute({ - document: graphql(/* GraphQL */ ` + document: graphql(` query latestVersion { latestVersion { baseSchema @@ -809,7 +809,7 @@ export function fetchLatestSchema(token: string) { export function fetchLatestValidSchema(token: string) { return execute({ - document: graphql(/* GraphQL */ ` + document: graphql(` query latestValidVersion { latestValidVersion { id @@ -850,7 +850,7 @@ export function fetchLatestValidSchema(token: string) { export function fetchVersions(selector: SchemaVersionsInput, limit: number, token: string) { return execute({ - document: graphql(/* GraphQL */ ` + document: graphql(` query schemaVersions($limit: Int!, $selector: SchemaVersionsInput!) { schemaVersions(selector: $selector, limit: $limit) { nodes { @@ -898,7 +898,7 @@ export function fetchVersions(selector: SchemaVersionsInput, limit: number, toke export function publishPersistedOperations(input: PublishPersistedOperationInput[], token: string) { return execute({ - document: graphql(/* GraphQL */ ` + document: graphql(` mutation publishPersistedOperations($input: [PublishPersistedOperationInput!]!) { publishPersistedOperations(input: $input) { summary { @@ -924,7 +924,7 @@ export function publishPersistedOperations(input: PublishPersistedOperationInput export function updateSchemaVersionStatus(input: SchemaVersionUpdateInput, token: string) { return execute({ - document: graphql(/* GraphQL */ ` + document: graphql(` mutation updateSchemaVersionStatus($input: SchemaVersionUpdateInput!) { updateSchemaVersionStatus(input: $input) { id @@ -953,7 +953,7 @@ export function updateSchemaVersionStatus(input: SchemaVersionUpdateInput, token export function createCdnAccess(selector: TargetSelectorInput, token: string) { return execute({ - document: graphql(/* GraphQL */ ` + document: graphql(` mutation createCdnAccessToken($input: CreateCdnAccessTokenInput!) { createCdnAccessToken(input: $input) { ok { @@ -1041,7 +1041,7 @@ export async function updateOrgRateLimit( authToken: string, ) { return execute({ - document: graphql(/* GraphQL */ ` + document: graphql(` mutation updateOrgRateLimit( $selector: OrganizationSelectorInput! $monthlyLimits: RateLimitInput! @@ -1064,7 +1064,7 @@ export async function enableExternalSchemaComposition( token: string, ) { return execute({ - document: graphql(/* GraphQL */ ` + document: graphql(` mutation enableExternalSchemaComposition($input: EnableExternalSchemaCompositionInput!) { enableExternalSchemaComposition(input: $input) { ok { diff --git a/integration-tests/testkit/seed.ts b/integration-tests/testkit/seed.ts index d36446b60..af023fd9d 100644 --- a/integration-tests/testkit/seed.ts +++ b/integration-tests/testkit/seed.ts @@ -53,7 +53,7 @@ export function initSeed() { ownerToken, async createPersonalProject(projectType: ProjectType) { const orgs = await execute({ - document: graphql(/* GraphQL */ ` + document: graphql(` query myOrganizations { organizations { total diff --git a/integration-tests/tests/api/artifacts-cdn.spec.ts b/integration-tests/tests/api/artifacts-cdn.spec.ts index 11fe218d1..d82ee8984 100644 --- a/integration-tests/tests/api/artifacts-cdn.spec.ts +++ b/integration-tests/tests/api/artifacts-cdn.spec.ts @@ -446,7 +446,7 @@ runArtifactsCDNTests('API Mirror', { service: 'server', port: 8082, path: '/arti // runArtifactsCDNTests('Local CDN Mock', 'http://127.0.0.1:3004/artifacts/v1/'); describe('CDN token', () => { - const TargetCDNAccessTokensQuery = graphql(/* GraphQL */ ` + const TargetCDNAccessTokensQuery = graphql(` query TargetCDNAccessTokens($selector: TargetSelectorInput!, $after: String, $first: Int = 2) { target(selector: $selector) { cdnAccessTokens(first: $first, after: $after) { @@ -468,7 +468,7 @@ describe('CDN token', () => { } `); - const DeleteCDNAccessTokenMutation = graphql(/* GraphQL */ ` + const DeleteCDNAccessTokenMutation = graphql(` mutation DeleteCDNAccessToken($input: DeleteCdnAccessTokenInput!) { deleteCdnAccessToken(input: $input) { error { diff --git a/integration-tests/tests/api/oidc-integrations/crud.spec.ts b/integration-tests/tests/api/oidc-integrations/crud.spec.ts index d3389f1e1..f26864cb7 100644 --- a/integration-tests/tests/api/oidc-integrations/crud.spec.ts +++ b/integration-tests/tests/api/oidc-integrations/crud.spec.ts @@ -2,7 +2,7 @@ import { graphql } from '../../../testkit/gql'; import { execute } from '../../../testkit/graphql'; import { initSeed } from '../../../testkit/seed'; -const OrganizationWithOIDCIntegration = graphql(/* GraphQL */ ` +const OrganizationWithOIDCIntegration = graphql(` query OrganizationWithOIDCIntegration($organizationId: ID!) { organization(selector: { organization: $organizationId }) { organization { @@ -15,7 +15,7 @@ const OrganizationWithOIDCIntegration = graphql(/* GraphQL */ ` } `); -const CreateOIDCIntegrationMutation = graphql(/* GraphQL */ ` +const CreateOIDCIntegrationMutation = graphql(` mutation CreateOIDCIntegrationMutation($input: CreateOIDCIntegrationInput!) { createOIDCIntegration(input: $input) { ok { @@ -393,7 +393,7 @@ describe('create', () => { }); }); -const DeleteOIDCIntegrationMutation = graphql(/* GraphQL */ ` +const DeleteOIDCIntegrationMutation = graphql(` mutation DeleteOIDCIntegrationMutation($input: DeleteOIDCIntegrationInput!) { deleteOIDCIntegration(input: $input) { ok { @@ -560,7 +560,7 @@ describe('delete', () => { const oidcIntegrationId = createResult.createOIDCIntegration.ok!.createdOIDCIntegration.id; - const MeQuery = graphql(/* GraphQL */ ` + const MeQuery = graphql(` query Me { me { id @@ -619,7 +619,7 @@ describe('delete', () => { }); }); -const UpdateOIDCIntegrationMutation = graphql(/* GraphQL */ ` +const UpdateOIDCIntegrationMutation = graphql(` mutation UpdateOIDCIntegrationMutation($input: UpdateOIDCIntegrationInput!) { updateOIDCIntegration(input: $input) { ok { diff --git a/package.json b/package.json index 0c361c4df..b42084449 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,6 @@ "@graphql-codegen/add": "4.0.1", "@graphql-codegen/cli": "3.2.1", "@graphql-codegen/client-preset": "2.1.0", - "@graphql-codegen/gql-tag-operations-preset": "2.1.0", "@graphql-codegen/graphql-modules-preset": "3.1.0", "@graphql-codegen/typed-document-node": "3.0.1", "@graphql-codegen/typescript": "3.0.1", diff --git a/packages/web/app/pages/[orgId]/[projectId]/[targetId]/explorer.tsx b/packages/web/app/pages/[orgId]/[projectId]/[targetId]/explorer.tsx index 1ba6d4d9b..9fd43c86e 100644 --- a/packages/web/app/pages/[orgId]/[projectId]/[targetId]/explorer.tsx +++ b/packages/web/app/pages/[orgId]/[projectId]/[targetId]/explorer.tsx @@ -1,5 +1,5 @@ import { ReactElement } from 'react'; -import { gql, useQuery } from 'urql'; +import { useQuery } from 'urql'; import { authenticated } from '@/components/authenticated-container'; import { TargetLayout } from '@/components/layouts'; import { SchemaExplorerFilter } from '@/components/target/explorer/filter'; @@ -9,10 +9,10 @@ import { useSchemaExplorerContext, } from '@/components/target/explorer/provider'; import { DataWrapper, noSchema, Title } from '@/components/v2'; -import { OrganizationFieldsFragment, ProjectFieldsFragment, TargetFieldsFragment } from '@/graphql'; +import { graphql } from '@/gql'; import { withSessionProtection } from '@/lib/supertokens/guard'; -const SchemaView_SchemaExplorer = gql(/* GraphQL */ ` +const SchemaView_SchemaExplorer = graphql(` query SchemaView_SchemaExplorer( $organization: ID! $project: ID! @@ -48,21 +48,21 @@ const SchemaView_SchemaExplorer = gql(/* GraphQL */ ` `); function SchemaView({ - organization, - project, - target, + organizationCleanId, + projectCleanId, + targetCleanId, }: { - organization: OrganizationFieldsFragment; - project: ProjectFieldsFragment; - target: TargetFieldsFragment; + organizationCleanId: string; + projectCleanId: string; + targetCleanId: string; }): ReactElement | null { const { period } = useSchemaExplorerContext(); const [query] = useQuery({ query: SchemaView_SchemaExplorer, variables: { - organization: organization.cleanId, - project: project.cleanId, - target: target.cleanId, + organization: organizationCleanId, + project: projectCleanId, + target: targetCleanId, period, }, requestPolicy: 'cache-first', @@ -85,9 +85,9 @@ function SchemaView({
{query ? ( @@ -115,18 +115,48 @@ function SchemaView({ ); } +const TargetExplorerPageQuery = graphql(` + query TargetExplorerPageQuery($organizationId: ID!, $projectId: ID!, $targetId: ID!) { + organization(selector: { organization: $organizationId }) { + organization { + ...TargetLayout_OrganizationFragment + rateLimit { + retentionInDays + } + cleanId + } + } + project(selector: { organization: $organizationId, project: $projectId }) { + ...TargetLayout_ProjectFragment + cleanId + } + targets(selector: { organization: $organizationId, project: $projectId }) { + ...TargetLayout_TargetConnectionFragment + } + target(selector: { organization: $organizationId, project: $projectId, target: $targetId }) { + cleanId + } + } +`); + function ExplorerPage(): ReactElement { return ( <> - <TargetLayout value="explorer"> - {props => ( - <SchemaExplorerProvider - dataRetentionInDays={props.organization.rateLimit.retentionInDays} - > - <SchemaView {...props} /> - </SchemaExplorerProvider> - )} + <TargetLayout value="explorer" query={TargetExplorerPageQuery}> + {props => + props.organization && props.project && props.target ? ( + <SchemaExplorerProvider + dataRetentionInDays={props.organization.organization.rateLimit.retentionInDays} + > + <SchemaView + organizationCleanId={props.organization.organization.cleanId} + projectCleanId={props.project.cleanId} + targetCleanId={props.target.cleanId} + /> + </SchemaExplorerProvider> + ) : null + } </TargetLayout> </> ); diff --git a/packages/web/app/pages/[orgId]/[projectId]/[targetId]/explorer/[typename].tsx b/packages/web/app/pages/[orgId]/[projectId]/[targetId]/explorer/[typename].tsx index 57d30e101..4972a8580 100644 --- a/packages/web/app/pages/[orgId]/[projectId]/[targetId]/explorer/[typename].tsx +++ b/packages/web/app/pages/[orgId]/[projectId]/[targetId]/explorer/[typename].tsx @@ -1,42 +1,24 @@ import { ReactElement } from 'react'; -import { DocumentType, gql, useQuery } from 'urql'; +import { useQuery } from 'urql'; import { authenticated } from '@/components/authenticated-container'; import { TargetLayout } from '@/components/layouts'; -import { - GraphQLEnumTypeComponent, - GraphQLEnumTypeComponent_TypeFragment, -} from '@/components/target/explorer/enum-type'; +import { GraphQLEnumTypeComponent } from '@/components/target/explorer/enum-type'; import { SchemaExplorerFilter } from '@/components/target/explorer/filter'; -import { - GraphQLInputObjectTypeComponent, - GraphQLInputObjectTypeComponent_TypeFragment, -} from '@/components/target/explorer/input-object-type'; -import { - GraphQLInterfaceTypeComponent, - GraphQLInterfaceTypeComponent_TypeFragment, -} from '@/components/target/explorer/interface-type'; -import { - GraphQLObjectTypeComponent, - GraphQLObjectTypeComponent_TypeFragment, -} from '@/components/target/explorer/object-type'; +import { GraphQLInputObjectTypeComponent } from '@/components/target/explorer/input-object-type'; +import { GraphQLInterfaceTypeComponent } from '@/components/target/explorer/interface-type'; +import { GraphQLObjectTypeComponent } from '@/components/target/explorer/object-type'; import { SchemaExplorerProvider, useSchemaExplorerContext, } from '@/components/target/explorer/provider'; -import { - GraphQLScalarTypeComponent, - GraphQLScalarTypeComponent_TypeFragment, -} from '@/components/target/explorer/scalar-type'; -import { - GraphQLUnionTypeComponent, - GraphQLUnionTypeComponent_TypeFragment, -} from '@/components/target/explorer/union-type'; +import { GraphQLScalarTypeComponent } from '@/components/target/explorer/scalar-type'; +import { GraphQLUnionTypeComponent } from '@/components/target/explorer/union-type'; import { DataWrapper, noSchema, Title } from '@/components/v2'; -import { OrganizationFieldsFragment, ProjectFieldsFragment, TargetFieldsFragment } from '@/graphql'; +import { FragmentType, graphql, useFragment } from '@/gql'; import { useRouteSelector } from '@/lib/hooks/use-route-selector'; import { withSessionProtection } from '@/lib/supertokens/guard'; -const SchemaTypeExplorer_Type = gql(/* GraphQL */ ` +const SchemaTypeExplorer_Type = graphql(` query SchemaTypeExplorer_Type( $organization: ID! $project: ID! @@ -53,13 +35,7 @@ const SchemaTypeExplorer_Type = gql(/* GraphQL */ ` valid explorer(usage: { period: $period }) { type(name: $typename) { - __typename - ...GraphQLObjectTypeComponent_TypeFragment - ...GraphQLInterfaceTypeComponent_TypeFragment - ...GraphQLUnionTypeComponent_TypeFragment - ...GraphQLEnumTypeComponent_TypeFragment - ...GraphQLInputObjectTypeComponent_TypeFragment - ...GraphQLScalarTypeComponent_TypeFragment + ...TypeRenderFragment } } } @@ -72,51 +48,59 @@ const SchemaTypeExplorer_Type = gql(/* GraphQL */ ` } `); -type GraphQLNamedType = - | DocumentType<typeof GraphQLEnumTypeComponent_TypeFragment> - | DocumentType<typeof GraphQLInputObjectTypeComponent_TypeFragment> - | DocumentType<typeof GraphQLInterfaceTypeComponent_TypeFragment> - | DocumentType<typeof GraphQLObjectTypeComponent_TypeFragment> - | DocumentType<typeof GraphQLScalarTypeComponent_TypeFragment> - | DocumentType<typeof GraphQLUnionTypeComponent_TypeFragment>; +const TypeRenderFragment = graphql(` + fragment TypeRenderFragment on GraphQLNamedType { + __typename + ...GraphQLObjectTypeComponent_TypeFragment + ...GraphQLInterfaceTypeComponent_TypeFragment + ...GraphQLUnionTypeComponent_TypeFragment + ...GraphQLEnumTypeComponent_TypeFragment + ...GraphQLInputObjectTypeComponent_TypeFragment + ...GraphQLScalarTypeComponent_TypeFragment + } +`); -function TypeRenderer({ type, totalRequests }: { type: GraphQLNamedType; totalRequests: number }) { - switch (type.__typename) { +function TypeRenderer(props: { + type: FragmentType<typeof TypeRenderFragment>; + totalRequests: number; +}) { + const ttype = useFragment(TypeRenderFragment, props.type); + switch (ttype.__typename) { case 'GraphQLObjectType': - return <GraphQLObjectTypeComponent type={type} totalRequests={totalRequests} />; + return <GraphQLObjectTypeComponent type={ttype} totalRequests={props.totalRequests} />; case 'GraphQLInterfaceType': - return <GraphQLInterfaceTypeComponent type={type} totalRequests={totalRequests} />; + return <GraphQLInterfaceTypeComponent type={ttype} totalRequests={props.totalRequests} />; case 'GraphQLUnionType': - return <GraphQLUnionTypeComponent type={type} totalRequests={totalRequests} />; + return <GraphQLUnionTypeComponent type={ttype} totalRequests={props.totalRequests} />; case 'GraphQLEnumType': - return <GraphQLEnumTypeComponent type={type} totalRequests={totalRequests} />; + return <GraphQLEnumTypeComponent type={ttype} totalRequests={props.totalRequests} />; case 'GraphQLInputObjectType': - return <GraphQLInputObjectTypeComponent type={type} totalRequests={totalRequests} />; + return <GraphQLInputObjectTypeComponent type={ttype} totalRequests={props.totalRequests} />; case 'GraphQLScalarType': - return <GraphQLScalarTypeComponent type={type} totalRequests={totalRequests} />; + return <GraphQLScalarTypeComponent type={ttype} totalRequests={props.totalRequests} />; default: - return <div>Unknown type: {type.__typename}</div>; + return <div>Unknown type: {(ttype as any).__typename}</div>; } } function SchemaTypeExplorer({ - organization, - project, - target, + organizationCleanId, + projectCleanId, + targetCleanId, typename, }: { - organization: OrganizationFieldsFragment; - project: ProjectFieldsFragment; - target: TargetFieldsFragment; + organizationCleanId: string; + projectCleanId: string; + targetCleanId: string; typename: string; }): ReactElement | null { const { period } = useSchemaExplorerContext(); const [query] = useQuery({ query: SchemaTypeExplorer_Type, variables: { - organization: organization.cleanId, - project: project.cleanId, - target: target.cleanId, + organization: organizationCleanId, + project: projectCleanId, + target: targetCleanId, period, typename, }, @@ -140,9 +124,9 @@ function SchemaTypeExplorer({ return ( <div className="space-y-4"> <SchemaExplorerFilter - organization={organization} - project={project} - target={target} + organization={{ cleanId: organizationCleanId }} + project={{ cleanId: projectCleanId }} + target={{ cleanId: targetCleanId }} period={period} typename={typename} /> @@ -154,6 +138,30 @@ function SchemaTypeExplorer({ ); } +const TargetExplorerTypenamePageQuery = graphql(` + query TargetExplorerTypenamePageQuery($organizationId: ID!, $projectId: ID!, $targetId: ID!) { + organization(selector: { organization: $organizationId }) { + organization { + ...TargetLayout_OrganizationFragment + cleanId + rateLimit { + retentionInDays + } + } + } + project(selector: { organization: $organizationId, project: $projectId }) { + ...TargetLayout_ProjectFragment + cleanId + } + targets(selector: { organization: $organizationId, project: $projectId }) { + ...TargetLayout_TargetConnectionFragment + } + target(selector: { organization: $organizationId, project: $projectId, target: $targetId }) { + cleanId + } + } +`); + function ExplorerPage(): ReactElement | null { const router = useRouteSelector(); const { typename } = router.query; @@ -165,12 +173,17 @@ function ExplorerPage(): ReactElement | null { return ( <> <Title title={`Type ${typename}`} /> - <TargetLayout value="explorer"> + <TargetLayout value="explorer" query={TargetExplorerTypenamePageQuery}> {props => ( <SchemaExplorerProvider - dataRetentionInDays={props.organization.rateLimit.retentionInDays} + dataRetentionInDays={props.organization?.organization.rateLimit.retentionInDays ?? 0} > - <SchemaTypeExplorer {...props} typename={typename} /> + <SchemaTypeExplorer + organizationCleanId={props.organization?.organization.cleanId ?? ''} + projectCleanId={props.project?.cleanId ?? ''} + targetCleanId={props.target?.cleanId ?? ''} + typename={typename} + /> </SchemaExplorerProvider> )} </TargetLayout> diff --git a/packages/web/app/pages/[orgId]/[projectId]/[targetId]/history.tsx b/packages/web/app/pages/[orgId]/[projectId]/[targetId]/history.tsx index 94a0376e5..7dca197a0 100644 --- a/packages/web/app/pages/[orgId]/[projectId]/[targetId]/history.tsx +++ b/packages/web/app/pages/[orgId]/[projectId]/[targetId]/history.tsx @@ -18,10 +18,10 @@ import { ToggleGroupItem, } from '@/components/v2'; import { DiffIcon } from '@/components/v2/icon'; +import { graphql } from '@/gql'; import { CompareDocument, CriticalityLevel, - LatestSchemaDocument, SchemaChangeFieldsFragment, VersionsDocument, } from '@/graphql'; @@ -292,29 +292,43 @@ function Page({ versionId }: { versionId: string }) { ); } +const TargetHistoryPageQuery = graphql(` + query TargetHistoryPageQuery($organizationId: ID!, $projectId: ID!, $targetId: ID!) { + organization(selector: { organization: $organizationId }) { + organization { + ...TargetLayout_OrganizationFragment + } + } + project(selector: { organization: $organizationId, project: $projectId }) { + ...TargetLayout_ProjectFragment + } + targets(selector: { organization: $organizationId, project: $projectId }) { + ...TargetLayout_TargetConnectionFragment + } + target(selector: { organization: $organizationId, project: $projectId, target: $targetId }) { + id + latestSchemaVersion { + id + } + } + } +`); + function HistoryPage(): ReactElement { const router = useRouteSelector(); - const [latestSchemaQuery] = useQuery({ - query: LatestSchemaDocument, - variables: { - selector: { - organization: router.organizationId, - project: router.projectId, - target: router.targetId, - }, - }, - requestPolicy: 'cache-and-network', - }); - const versionId = router.versionId ?? latestSchemaQuery.data?.target?.latestSchemaVersion?.id; return ( <> <Title title="History" /> <TargetLayout value="history" - className={versionId ? 'flex h-full items-stretch gap-x-5' : ''} + className={router.versionId ? 'flex h-full items-stretch gap-x-5' : ''} + query={TargetHistoryPageQuery} > - {() => (versionId ? <Page versionId={versionId} /> : noSchema)} + {({ target }) => { + const versionId = router.versionId ?? target?.latestSchemaVersion?.id; + return versionId ? <Page versionId={versionId} /> : noSchema; + }} </TargetLayout> </> ); diff --git a/packages/web/app/pages/[orgId]/[projectId]/[targetId]/index.tsx b/packages/web/app/pages/[orgId]/[projectId]/[targetId]/index.tsx index 096633967..7a9d462b5 100644 --- a/packages/web/app/pages/[orgId]/[projectId]/[targetId]/index.tsx +++ b/packages/web/app/pages/[orgId]/[projectId]/[targetId]/index.tsx @@ -5,15 +5,9 @@ import { authenticated } from '@/components/authenticated-container'; import { TargetLayout } from '@/components/layouts'; import { MarkAsValid } from '@/components/target/history/MarkAsValid'; import { DataWrapper, GraphQLBlock, Input, noSchema, Title } from '@/components/v2'; +import { FragmentType, graphql, useFragment } from '@/gql'; import { CompositeSchemaFieldsFragment, SingleSchemaFieldsFragment } from '@/gql/graphql'; -import { - LatestSchemaDocument, - OrganizationFieldsFragment, - ProjectFieldsFragment, - ProjectType, - RegistryModel, - TargetFieldsFragment, -} from '@/graphql'; +import { LatestSchemaDocument, ProjectType, RegistryModel } from '@/graphql'; import { TargetAccessScope, useTargetAccess } from '@/lib/access/target'; import { withSessionProtection } from '@/lib/supertokens/guard'; @@ -23,15 +17,22 @@ function isCompositeSchema( return schema.__typename === 'CompositeSchema'; } +const Schemas_ProjectFragment = graphql(` + fragment Schemas_ProjectFragment on Project { + type + } +`); + function Schemas({ - project, filterService, schemas = [], + ...props }: { - project: ProjectFieldsFragment; + project: FragmentType<typeof Schemas_ProjectFragment>; schemas: Array<SingleSchemaFieldsFragment | CompositeSchemaFieldsFragment>; filterService?: string; }): ReactElement { + const project = useFragment(Schemas_ProjectFragment, props.project); if (project.type === ProjectType.Single) { const [schema] = schemas; return ( @@ -66,15 +67,45 @@ function Schemas({ ); } -function SchemaView({ - organization, - project, - target, -}: { - organization: OrganizationFieldsFragment; - project: ProjectFieldsFragment; - target: TargetFieldsFragment; +const SchemaView_OrganizationFragment = graphql(` + fragment SchemaView_OrganizationFragment on Organization { + cleanId + me { + ...CanAccessTarget_MemberFragment + } + } +`); + +const SchemaView_ProjectFragment = graphql(` + fragment SchemaView_ProjectFragment on Project { + cleanId + type + registryModel + ...Schemas_ProjectFragment + } +`); + +const SchemaView_TargetFragment = graphql(` + fragment SchemaView_TargetFragment on Target { + cleanId + latestSchemaVersion { + schemas { + nodes { + __typename + } + } + } + } +`); + +function SchemaView(props: { + organization: FragmentType<typeof SchemaView_OrganizationFragment>; + project: FragmentType<typeof SchemaView_ProjectFragment>; + target: FragmentType<typeof SchemaView_TargetFragment>; }): ReactElement | null { + const organization = useFragment(SchemaView_OrganizationFragment, props.organization); + const project = useFragment(SchemaView_ProjectFragment, props.project); + const target = useFragment(SchemaView_TargetFragment, props.target); const [filterService, setFilterService] = useState(''); const [term, setTerm] = useState(''); const debouncedFilter = useDebouncedCallback((value: string) => { @@ -153,11 +184,40 @@ function SchemaView({ ); } +const TargetSchemaPageQuery = graphql(` + query TargetSchemaPageQuery($organizationId: ID!, $projectId: ID!, $targetId: ID!) { + organization(selector: { organization: $organizationId }) { + organization { + ...TargetLayout_OrganizationFragment + ...SchemaView_OrganizationFragment + } + } + project(selector: { organization: $organizationId, project: $projectId }) { + ...TargetLayout_ProjectFragment + ...SchemaView_ProjectFragment + } + targets(selector: { organization: $organizationId, project: $projectId }) { + ...TargetLayout_TargetConnectionFragment + } + target(selector: { organization: $organizationId, project: $projectId, target: $targetId }) { + ...SchemaView_TargetFragment + } + } +`); + function SchemaPage(): ReactElement { return ( <> <Title title="Schema" /> - <TargetLayout value="schema">{props => <SchemaView {...props} />}</TargetLayout> + <TargetLayout value="schema" query={TargetSchemaPageQuery}> + {props => ( + <SchemaView + target={props.target!} + organization={props.organization!.organization} + project={props.project!} + /> + )} + </TargetLayout> </> ); } diff --git a/packages/web/app/pages/[orgId]/[projectId]/[targetId]/laboratory.tsx b/packages/web/app/pages/[orgId]/[projectId]/[targetId]/laboratory.tsx index 3c437262a..9287ec8c9 100644 --- a/packages/web/app/pages/[orgId]/[projectId]/[targetId]/laboratory.tsx +++ b/packages/web/app/pages/[orgId]/[projectId]/[targetId]/laboratory.tsx @@ -5,6 +5,7 @@ import { TargetLayout } from '@/components/layouts'; import { Button, 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 { withSessionProtection } from '@/lib/supertokens/guard'; import { createGraphiQLFetcher } from '@graphiql/toolkit'; @@ -31,6 +32,25 @@ const Page = ({ endpoint }: { endpoint: string }): ReactElement => { ); }; +const TargetLaboratoryPageQuery = graphql(` + query TargetLaboratoryPageQuery($organizationId: ID!, $projectId: ID!, $targetId: ID!) { + organization(selector: { organization: $organizationId }) { + organization { + ...TargetLayout_OrganizationFragment + } + } + project(selector: { organization: $organizationId, project: $projectId }) { + ...TargetLayout_ProjectFragment + } + targets(selector: { organization: $organizationId, project: $projectId }) { + ...TargetLayout_TargetConnectionFragment + } + target(selector: { organization: $organizationId, project: $projectId, target: $targetId }) { + id + } + } +`); + function LaboratoryPage(): ReactElement { const [isModalOpen, toggleModalOpen] = useToggle(); const router = useRouteSelector(); @@ -40,6 +60,7 @@ function LaboratoryPage(): ReactElement { <> <Title title="Schema laboratory" /> <TargetLayout + query={TargetLaboratoryPageQuery} value="laboratory" className="flex h-full flex-col" connect={ diff --git a/packages/web/app/pages/[orgId]/[projectId]/[targetId]/operations.tsx b/packages/web/app/pages/[orgId]/[projectId]/[targetId]/operations.tsx index fbad02a08..025a0ef23 100644 --- a/packages/web/app/pages/[orgId]/[projectId]/[targetId]/operations.tsx +++ b/packages/web/app/pages/[orgId]/[projectId]/[targetId]/operations.tsx @@ -1,19 +1,13 @@ import { ReactElement, useCallback, useMemo, useState } from 'react'; import { useRouter } from 'next/router'; import { formatISO, subDays, subHours, subMinutes } from 'date-fns'; -import { useQuery } from 'urql'; import { authenticated } from '@/components/authenticated-container'; import { TargetLayout } from '@/components/layouts'; import { OperationsFilterTrigger } from '@/components/target/operations/Filters'; import { OperationsList } from '@/components/target/operations/List'; import { OperationsStats } from '@/components/target/operations/Stats'; -import { DataWrapper, EmptyList, RadixSelect, Title } from '@/components/v2'; -import { - HasCollectedOperationsDocument, - OrganizationFieldsFragment, - ProjectFieldsFragment, - TargetFieldsFragment, -} from '@/graphql'; +import { EmptyList, RadixSelect, Title } from '@/components/v2'; +import { graphql } from '@/gql'; import { getDocsUrl } from '@/lib/docs-url'; import { withSessionProtection } from '@/lib/supertokens/guard'; @@ -33,13 +27,13 @@ const DateRange = { type PeriodKey = keyof typeof DateRange; function OperationsView({ - organization, - project, - target, + organizationCleanId, + projectCleanId, + targetCleanId, }: { - organization: OrganizationFieldsFragment; - project: ProjectFieldsFragment; - target: TargetFieldsFragment; + organizationCleanId: string; + projectCleanId: string; + targetCleanId: string; }): ReactElement { const router = useRouter(); const [href, periodParam] = router.asPath.split('?'); @@ -86,74 +80,75 @@ function OperationsView({ /> </div> <OperationsStats - organization={organization.cleanId} - project={project.cleanId} - target={target.cleanId} + organization={organizationCleanId} + project={projectCleanId} + target={targetCleanId} period={period} operationsFilter={selectedOperations} /> <OperationsList className="mt-12" period={period} - organization={organization.cleanId} - project={project.cleanId} - target={target.cleanId} + organization={organizationCleanId} + project={projectCleanId} + target={targetCleanId} operationsFilter={selectedOperations} /> </> ); } -function OperationsViewGate({ - organization, - project, - target, -}: { - organization: OrganizationFieldsFragment; - project: ProjectFieldsFragment; - target: TargetFieldsFragment; -}): ReactElement { - const [query] = useQuery({ - query: HasCollectedOperationsDocument, - variables: { - selector: { - organization: organization.cleanId, - project: project.cleanId, - target: target.cleanId, - }, - }, - }); - - return ( - <DataWrapper query={query}> - {result => - result.data.hasCollectedOperations ? ( - <OperationsView organization={organization} project={project} target={target} /> - ) : ( - <EmptyList - title="Hive is waiting for your first collected operation" - description="You can collect usage of your GraphQL API with Hive Client" - docsUrl={getDocsUrl('/features/monitoring')} - /> - ) +const TargetOperationsPageQuery = graphql(` + query TargetOperationsPageQuery($organizationId: ID!, $projectId: ID!, $targetId: ID!) { + organization(selector: { organization: $organizationId }) { + organization { + ...TargetLayout_OrganizationFragment + cleanId } - </DataWrapper> - ); -} + } + project(selector: { organization: $organizationId, project: $projectId }) { + ...TargetLayout_ProjectFragment + cleanId + } + targets(selector: { organization: $organizationId, project: $projectId }) { + ...TargetLayout_TargetConnectionFragment + } + target(selector: { organization: $organizationId, project: $projectId, target: $targetId }) { + cleanId + } + hasCollectedOperations( + selector: { organization: $organizationId, project: $projectId, target: $targetId } + ) + } +`); function OperationsPage(): ReactElement { return ( <> <Title title="Operations" /> - <TargetLayout value="operations"> - {({ organization, project, target }) => ( - <div className="relative"> - <p className="mb-5 font-light text-gray-500"> - Data collected based on operation executed against your GraphQL schema. - </p> - <OperationsViewGate organization={organization} project={project} target={target} /> - </div> - )} + <TargetLayout value="operations" query={TargetOperationsPageQuery}> + {({ organization, project, target, hasCollectedOperations }) => + organization && project && target ? ( + <div className="relative"> + <p className="mb-5 font-light text-gray-500"> + Data collected based on operation executed against your GraphQL schema. + </p> + {hasCollectedOperations ? ( + <OperationsView + organizationCleanId={organization.organization.cleanId} + projectCleanId={project.cleanId} + targetCleanId={target.cleanId} + /> + ) : ( + <EmptyList + title="Hive is waiting for your first collected operation" + description="You can collect usage of your GraphQL API with Hive Client" + docsUrl={getDocsUrl('/features/monitoring')} + /> + )} + </div> + ) : null + } </TargetLayout> </> ); diff --git a/packages/web/app/pages/[orgId]/[projectId]/[targetId]/settings.tsx b/packages/web/app/pages/[orgId]/[projectId]/[targetId]/settings.tsx index 579277ec9..199c9c0ec 100644 --- a/packages/web/app/pages/[orgId]/[projectId]/[targetId]/settings.tsx +++ b/packages/web/app/pages/[orgId]/[projectId]/[targetId]/settings.tsx @@ -2,7 +2,7 @@ import React, { ReactElement, useCallback, useState } from 'react'; import clsx from 'clsx'; import { formatISO, subDays } from 'date-fns'; import { useFormik } from 'formik'; -import { gql, useMutation, useQuery } from 'urql'; +import { useMutation, useQuery } from 'urql'; import * as Yup from 'yup'; import { authenticated } from '@/components/authenticated-container'; import { TargetLayout } from '@/components/layouts'; @@ -27,19 +27,22 @@ import { import { Combobox } from '@/components/v2/combobox'; import { AlertTriangleIcon } from '@/components/v2/icon'; import { CreateAccessTokenModal, DeleteTargetModal } from '@/components/v2/modals'; -import { - DeleteTokensDocument, - MemberFieldsFragment, - OrganizationFieldsFragment, - SetTargetValidationDocument, - TargetFieldsFragment, - TokensDocument, -} from '@/graphql'; +import { FragmentType, graphql, useFragment } from '@/gql'; +import { DeleteTokensDocument, SetTargetValidationDocument, TokensDocument } from '@/graphql'; import { canAccessTarget, TargetAccessScope } from '@/lib/access/target'; import { useRouteSelector, useToggle } from '@/lib/hooks'; import { withSessionProtection } from '@/lib/supertokens/guard'; -function RegistryAccessTokens({ me }: { me: MemberFieldsFragment }): ReactElement { +const RegistryAccessTokens_MeFragment = graphql(` + fragment RegistryAccessTokens_MeFragment on Member { + ...CanAccessTarget_MemberFragment + } +`); + +function RegistryAccessTokens(props: { + me: FragmentType<typeof RegistryAccessTokens_MeFragment>; +}): ReactElement { + const me = useFragment(RegistryAccessTokens_MeFragment, props.me); const router = useRouteSelector(); const [{ fetching: deleting }, mutate] = useMutation(DeleteTokensDocument); const [checked, setChecked] = useState<string[]>([]); @@ -134,7 +137,7 @@ function RegistryAccessTokens({ me }: { me: MemberFieldsFragment }): ReactElemen ); } -const Settings_UpdateBaseSchemaMutation = gql(/* GraphQL */ ` +const Settings_UpdateBaseSchemaMutation = graphql(` mutation Settings_UpdateBaseSchema($input: UpdateBaseSchemaInput!) { updateBaseSchema(input: $input) { ok { @@ -212,7 +215,7 @@ const ExtendBaseSchema = (props: { baseSchema: string }): ReactElement => { ); }; -const ClientExclusion_AvailableClientNamesQuery = gql(/* GraphQL */ ` +const ClientExclusion_AvailableClientNamesQuery = graphql(` query ClientExclusion_AvailableClientNamesQuery($selector: ClientStatsByTargetsInput!) { clientStatsByTargets(selector: $selector) { nodes { @@ -274,18 +277,27 @@ function ClientExclusion( ); } -const Settings_TargetSettingsQuery = gql(/* GraphQL */ ` +const Settings_TargetSettingsQuery = graphql(` query Settings_TargetSettingsQuery( $selector: TargetSelectorInput! $targetsSelector: ProjectSelectorInput! $organizationSelector: OrganizationSelectorInput! ) { targetSettings(selector: $selector) { - ...TargetSettingsFields + validation { + enabled + percentage + period + targets { + id + } + excludedClients + } } targets(selector: $targetsSelector) { nodes { - ...TargetEssentials + id + name } } organization(selector: $organizationSelector) { @@ -299,7 +311,7 @@ const Settings_TargetSettingsQuery = gql(/* GraphQL */ ` } `); -const Settings_UpdateTargetValidationSettingsMutation = gql(/* GraphQL */ ` +const Settings_UpdateTargetValidationSettingsMutation = graphql(` mutation Settings_UpdateTargetValidationSettings($input: UpdateTargetValidationSettingsInput!) { updateTargetValidationSettings(input: $input) { ok { @@ -554,7 +566,7 @@ const ConditionalBreakingChanges = (): ReactElement => { ); }; -const Settings_UpdateTargetNameMutation = gql(/* GraphQL */ ` +const Settings_UpdateTargetNameMutation = graphql(` mutation Settings_UpdateTargetName($input: UpdateTargetNameInput!) { updateTargetName(input: $input) { ok { @@ -565,6 +577,7 @@ const Settings_UpdateTargetNameMutation = gql(/* GraphQL */ ` } updatedTarget { ...TargetFields + cleanId } } error { @@ -577,13 +590,29 @@ const Settings_UpdateTargetNameMutation = gql(/* GraphQL */ ` } `); -const Page = ({ - target, - organization, -}: { - target: TargetFieldsFragment; - organization: OrganizationFieldsFragment; +const TargetSettingsPage_TargetFragment = graphql(` + fragment TargetSettingsPage_TargetFragment on Target { + name + baseSchema + } +`); + +const TargetSettingsPage_OrganizationFragment = graphql(` + fragment TargetSettingsPage_OrganizationFragment on Organization { + me { + ...CanAccessTarget_MemberFragment + ...RegistryAccessTokens_MeFragment + ...CDNAccessTokens_MeFragment + } + } +`); + +const Page = (props: { + target: FragmentType<typeof TargetSettingsPage_TargetFragment>; + organization: FragmentType<typeof TargetSettingsPage_OrganizationFragment>; }) => { + const target = useFragment(TargetSettingsPage_TargetFragment, props.target); + const organization = useFragment(TargetSettingsPage_OrganizationFragment, props.organization); const router = useRouteSelector(); const [isModalOpen, toggleModalOpen] = useToggle(); @@ -694,12 +723,41 @@ const Page = ({ ); }; +const TargetSettingsPageQuery = graphql(` + query TargetSettingsPageQuery($organizationId: ID!, $projectId: ID!, $targetId: ID!) { + organization(selector: { organization: $organizationId }) { + organization { + ...TargetLayout_OrganizationFragment + ...TargetSettingsPage_OrganizationFragment + } + } + project(selector: { organization: $organizationId, project: $projectId }) { + ...TargetLayout_ProjectFragment + } + targets(selector: { organization: $organizationId, project: $projectId }) { + ...TargetLayout_TargetConnectionFragment + } + target(selector: { organization: $organizationId, project: $projectId, target: $targetId }) { + id + ...TargetSettingsPage_TargetFragment + } + } +`); + function SettingsPage(): ReactElement { return ( <> <Title title="Settings" /> - <TargetLayout value="settings" className="flex flex-col gap-16"> - {props => <Page target={props.target} organization={props.organization} />} + <TargetLayout + value="settings" + className="flex flex-col gap-16" + query={TargetSettingsPageQuery} + > + {props => + props.organization ? ( + <Page target={props.target!} organization={props.organization.organization} /> + ) : null + } </TargetLayout> </> ); diff --git a/packages/web/app/pages/[orgId]/[projectId]/index.tsx b/packages/web/app/pages/[orgId]/[projectId]/index.tsx index 044a6329d..6fd345c6a 100644 --- a/packages/web/app/pages/[orgId]/[projectId]/index.tsx +++ b/packages/web/app/pages/[orgId]/[projectId]/index.tsx @@ -21,6 +21,7 @@ import { DropdownMenuTrigger, } from '@/components/v2/dropdown'; import { LinkIcon, MoreIcon, SettingsIcon } from '@/components/v2/icon'; +import { graphql } from '@/gql'; import { TargetQuery, TargetsDocument, VersionsDocument } from '@/graphql'; import { getDocsUrl } from '@/lib/docs-url'; import { useClipboard } from '@/lib/hooks/use-clipboard'; @@ -140,11 +141,24 @@ const Page = () => { ); }; +const ProjectOverviewPageQuery = graphql(` + query ProjectOverviewPageQuery($organizationId: ID!, $projectId: ID!) { + organization(selector: { organization: $organizationId }) { + organization { + ...ProjectLayout_OrganizationFragment + } + } + project(selector: { organization: $organizationId, project: $projectId }) { + ...ProjectLayout_ProjectFragment + } + } +`); + function ProjectsPage(): ReactElement { return ( <> <Title title="Targets" /> - <ProjectLayout value="targets" className="flex gap-x-5"> + <ProjectLayout value="targets" className="flex gap-x-5" query={ProjectOverviewPageQuery}> {() => <Page />} </ProjectLayout> </> diff --git a/packages/web/app/pages/[orgId]/[projectId]/view/alerts.tsx b/packages/web/app/pages/[orgId]/[projectId]/view/alerts.tsx index 479385302..9369b355a 100644 --- a/packages/web/app/pages/[orgId]/[projectId]/view/alerts.tsx +++ b/packages/web/app/pages/[orgId]/[projectId]/view/alerts.tsx @@ -4,14 +4,13 @@ import { authenticated } from '@/components/authenticated-container'; import { ProjectLayout } from '@/components/layouts'; import { Button, Card, Checkbox, Heading, Table, Tag, TBody, Td, Title, Tr } from '@/components/v2'; import { CreateAlertModal, CreateChannelModal } from '@/components/v2/modals'; +import { FragmentType, graphql, useFragment } from '@/gql'; import { AlertChannelsDocument, AlertChannelType, AlertsDocument, DeleteAlertChannelsDocument, DeleteAlertsDocument, - OrganizationFieldsFragment, - ProjectFieldsFragment, } from '@/graphql'; import { ProjectAccessScope, useProjectAccess } from '@/lib/access/project'; import { useRouteSelector, useToggle } from '@/lib/hooks'; @@ -94,13 +93,30 @@ function Channels(): ReactElement { ); } +const AlertsPage_OrganizationFragment = graphql(` + fragment AlertsPage_OrganizationFragment on Organization { + cleanId + me { + ...CanAccessProject_MemberFragment + } + } +`); + +const AlertsPage_ProjectFragment = graphql(` + fragment AlertsPage_ProjectFragment on Project { + cleanId + } +`); + const Page = (props: { - organization: OrganizationFieldsFragment; - project: ProjectFieldsFragment; + organization: FragmentType<typeof AlertsPage_OrganizationFragment>; + project: FragmentType<typeof AlertsPage_ProjectFragment>; }) => { + const organization = useFragment(AlertsPage_OrganizationFragment, props.organization); + const project = useFragment(AlertsPage_ProjectFragment, props.project); useProjectAccess({ scope: ProjectAccessScope.Alerts, - member: props.organization.me, + member: organization.me, redirect: true, }); const [checked, setChecked] = useState<string[]>([]); @@ -111,8 +127,8 @@ const Page = (props: { query: AlertsDocument, variables: { selector: { - organization: props.organization.cleanId, - project: props.project.cleanId, + organization: organization.cleanId, + project: project.cleanId, }, }, requestPolicy: 'cache-and-network', @@ -180,11 +196,30 @@ const Page = (props: { ); }; +const ProjectAlertsPageQuery = graphql(` + query ProjectAlertsPageQuery($organizationId: ID!, $projectId: ID!) { + organization(selector: { organization: $organizationId }) { + organization { + ...ProjectLayout_OrganizationFragment + ...AlertsPage_OrganizationFragment + } + } + project(selector: { organization: $organizationId, project: $projectId }) { + ...ProjectLayout_ProjectFragment + ...AlertsPage_ProjectFragment + } + } +`); + function AlertsPage(): ReactElement { return ( <> <Title title="Alerts" /> - <ProjectLayout value="alerts" className="flex flex-col gap-y-10"> + <ProjectLayout + value="alerts" + className="flex flex-col gap-y-10" + query={ProjectAlertsPageQuery} + > {props => <Page organization={props.organization} project={props.project} />} </ProjectLayout> </> diff --git a/packages/web/app/pages/[orgId]/[projectId]/view/settings.tsx b/packages/web/app/pages/[orgId]/[projectId]/view/settings.tsx index 08b4b37c3..44797f931 100644 --- a/packages/web/app/pages/[orgId]/[projectId]/view/settings.tsx +++ b/packages/web/app/pages/[orgId]/[projectId]/view/settings.tsx @@ -1,6 +1,6 @@ import { ReactElement } from 'react'; import { useFormik } from 'formik'; -import { gql, useMutation, useQuery } from 'urql'; +import { useMutation, useQuery } from 'urql'; import * as Yup from 'yup'; import { authenticated } from '@/components/authenticated-container'; import { ProjectLayout } from '@/components/layouts'; @@ -9,17 +9,13 @@ import { ModelMigrationSettings } from '@/components/project/settings/model-migr import { Button, Card, Heading, Input, Link, Select, Tag, Title } from '@/components/v2'; import { AlertTriangleIcon } from '@/components/v2/icon'; import { DeleteProjectModal } from '@/components/v2/modals'; -import { - GetGitHubIntegrationDetailsDocument, - OrganizationFieldsFragment, - ProjectFieldsFragment, - ProjectType, -} from '@/graphql'; +import { FragmentType, graphql, useFragment } from '@/gql'; +import { GetGitHubIntegrationDetailsDocument, ProjectType } from '@/graphql'; import { canAccessProject, ProjectAccessScope, useProjectAccess } from '@/lib/access/project'; import { useRouteSelector, useToggle } from '@/lib/hooks'; import { withSessionProtection } from '@/lib/supertokens/guard'; -const Settings_UpdateProjectGitRepositoryMutation = gql(/* GraphQL */ ` +const Settings_UpdateProjectGitRepositoryMutation = graphql(` mutation Settings_UpdateProjectGitRepository($input: UpdateProjectGitRepositoryInput!) { updateProjectGitRepository(input: $input) { ok { @@ -131,7 +127,7 @@ function GitHubIntegration({ ); } -const Settings_UpdateProjectNameMutation = gql(/* GraphQL */ ` +const Settings_UpdateProjectNameMutation = graphql(` mutation Settings_UpdateProjectName($input: UpdateProjectNameInput!) { updateProjectName(input: $input) { ok { @@ -141,6 +137,7 @@ const Settings_UpdateProjectNameMutation = gql(/* GraphQL */ ` } updatedProject { ...ProjectFields + cleanId } } error { @@ -150,13 +147,32 @@ const Settings_UpdateProjectNameMutation = gql(/* GraphQL */ ` } `); -const Page = ({ - organization, - project, -}: { - organization: OrganizationFieldsFragment; - project: ProjectFieldsFragment; +const SettingsPage_OrganizationFragment = graphql(` + fragment SettingsPage_OrganizationFragment on Organization { + cleanId + me { + ...CanAccessProject_MemberFragment + } + ...ExternalCompositionSettings_OrganizationFragment + } +`); + +const SettingsPage_ProjectFragment = graphql(` + fragment SettingsPage_ProjectFragment on Project { + name + gitRepository + type + ...ModelMigrationSettings_ProjectFragment + ...ExternalCompositionSettings_ProjectFragment + } +`); + +const Page = (props: { + organization: FragmentType<typeof SettingsPage_OrganizationFragment>; + project: FragmentType<typeof SettingsPage_ProjectFragment>; }) => { + const organization = useFragment(SettingsPage_OrganizationFragment, props.organization); + const project = useFragment(SettingsPage_ProjectFragment, props.project); useProjectAccess({ scope: ProjectAccessScope.Settings, member: organization.me, @@ -193,9 +209,7 @@ const Page = ({ return ( <> - {/* {project.registryModel === 'LEGACY' ? ( */} <ModelMigrationSettings project={project} organizationId={organization.cleanId} /> - {/* ) : null} */} <Card> <Heading className="mb-2">Project Name</Heading> <p className="mb-3 font-light text-gray-300"> @@ -272,11 +286,30 @@ const Page = ({ ); }; +const ProjectSettingsPageQuery = graphql(` + query ProjectSettingsPageQuery($organizationId: ID!, $projectId: ID!) { + organization(selector: { organization: $organizationId }) { + organization { + ...SettingsPage_OrganizationFragment + ...ProjectLayout_OrganizationFragment + } + } + project(selector: { organization: $organizationId, project: $projectId }) { + ...ProjectLayout_ProjectFragment + ...SettingsPage_ProjectFragment + } + } +`); + function SettingsPage(): ReactElement { return ( <> <Title title="Project settings" /> - <ProjectLayout value="settings" className="flex flex-col gap-y-10"> + <ProjectLayout + value="settings" + className="flex flex-col gap-y-10" + query={ProjectSettingsPageQuery} + > {props => <Page {...props} />} </ProjectLayout> </> diff --git a/packages/web/app/pages/[orgId]/index.tsx b/packages/web/app/pages/[orgId]/index.tsx index e0455ac70..674fb39e5 100644 --- a/packages/web/app/pages/[orgId]/index.tsx +++ b/packages/web/app/pages/[orgId]/index.tsx @@ -22,11 +22,8 @@ import { DropdownMenuTrigger, } from '@/components/v2/dropdown'; import { LinkIcon, MoreIcon, SettingsIcon } from '@/components/v2/icon'; -import { - ProjectActivitiesDocument, - ProjectsWithTargetsDocument, - ProjectsWithTargetsQuery, -} from '@/graphql'; +import { FragmentType, graphql, useFragment } from '@/gql'; +import { ProjectActivitiesDocument } from '@/graphql'; import { writeLastVisitedOrganization } from '@/lib/cookies'; import { getDocsUrl } from '@/lib/docs-url'; import { fixDuplicatedFragments } from '@/lib/graphql'; @@ -36,11 +33,19 @@ import { withSessionProtection } from '@/lib/supertokens/guard'; const projectActivitiesDocument = fixDuplicatedFragments(ProjectActivitiesDocument); -const ProjectCard = ({ - project, -}: { - project: ProjectsWithTargetsQuery['projects']['nodes'][number]; +const ProjectCard_ProjectFragment = graphql(` + fragment ProjectCard_ProjectFragment on Project { + cleanId + id + type + name + } +`); + +const ProjectCard = (props: { + project: FragmentType<typeof ProjectCard_ProjectFragment>; }): ReactElement => { + const project = useFragment(ProjectCard_ProjectFragment, props.project); const copyToClipboard = useClipboard(); const router = useRouteSelector(); const [projectActivitiesQuery] = useQuery({ @@ -109,27 +114,37 @@ const ProjectCard = ({ ); }; +const OrganizationProjectsPageQuery = graphql(` + query OrganizationProjectsPageQuery($selector: OrganizationSelectorInput!) { + organization(selector: $selector) { + organization { + ...OrganizationLayout_OrganizationFragment + } + } + projects(selector: $selector) { + total + nodes { + id + ...ProjectCard_ProjectFragment + } + } + } +`); + function ProjectsPage(): ReactElement { - const router = useRouteSelector(); - - const [projectsWithTargetsQuery] = useQuery({ - query: ProjectsWithTargetsDocument, - variables: { - selector: { - organization: router.organizationId, - }, - }, - }); - return ( <> <Title title="Projects" /> - <OrganizationLayout value="overview" className="flex justify-between gap-5"> - {() => ( + <OrganizationLayout + value="overview" + className="flex justify-between gap-5" + query={OrganizationProjectsPageQuery} + > + {({ projects }) => ( <> <div className="grow"> <Heading className="mb-4">Active Projects</Heading> - {projectsWithTargetsQuery.data?.projects.total === 0 ? ( + {projects.total === 0 ? ( <EmptyList title="Hive is waiting for your first project" description='You can create a project by clicking the "Create Project" button' @@ -137,7 +152,8 @@ function ProjectsPage(): ReactElement { /> ) : ( <div className="grid grid-cols-2 gap-5 items-stretch"> - {projectsWithTargetsQuery.fetching + {/** TODO: use defer here :) */} + {projects === null ? [1, 2].map(key => ( <Card key={key}> <div className="flex gap-x-2"> @@ -151,7 +167,7 @@ function ProjectsPage(): ReactElement { <Skeleton visible className="h-7" /> </Card> )) - : projectsWithTargetsQuery.data?.projects.nodes.map(project => ( + : projects.nodes.map(project => ( <ProjectCard key={project.id} project={project} /> ))} </div> diff --git a/packages/web/app/pages/[orgId]/view/members.tsx b/packages/web/app/pages/[orgId]/view/members.tsx index b6be8c216..8e8d7d92d 100644 --- a/packages/web/app/pages/[orgId]/view/members.tsx +++ b/packages/web/app/pages/[orgId]/view/members.tsx @@ -1,6 +1,6 @@ import { ReactElement, useCallback, useEffect, useState } from 'react'; import { useFormik } from 'formik'; -import { DocumentType, gql, useMutation, useQuery } from 'urql'; +import { useMutation, useQuery } from 'urql'; import * as Yup from 'yup'; import { authenticated } from '@/components/authenticated-container'; import { OrganizationLayout } from '@/components/layouts'; @@ -13,6 +13,7 @@ import { } from '@/components/v2/dropdown'; import { CopyIcon, KeyIcon, MoreIcon, SettingsIcon, TrashIcon } from '@/components/v2/icon'; import { ChangePermissionsModal, DeleteMembersModal } from '@/components/v2/modals'; +import { FragmentType, graphql, useFragment } from '@/gql'; import { MeDocument, OrganizationFieldsFragment, OrganizationType } from '@/graphql'; import { OrganizationAccessScope, useOrganizationAccess } from '@/lib/access/organization'; import { useClipboard } from '@/lib/hooks/use-clipboard'; @@ -27,7 +28,7 @@ export const DateFormatter = Intl.DateTimeFormat('en', { day: 'numeric', }); -const Members_Invitation = gql(/* GraphQL */ ` +const Members_Invitation = graphql(` fragment Members_Invitation on OrganizationInvitation { id createdAt @@ -37,11 +38,13 @@ const Members_Invitation = gql(/* GraphQL */ ` } `); -export const MemberInvitationForm_InviteByEmail = gql(/* GraphQL */ ` +const MemberInvitationForm_InviteByEmail = graphql(` mutation MemberInvitationForm_InviteByEmail($input: InviteToOrganizationByEmailInput!) { inviteToOrganizationByEmail(input: $input) { ok { ...Members_Invitation + email + id } error { message @@ -53,7 +56,7 @@ export const MemberInvitationForm_InviteByEmail = gql(/* GraphQL */ ` } `); -export const InvitationDeleteButton_DeleteInvitation = gql(/* GraphQL */ ` +const InvitationDeleteButton_DeleteInvitation = graphql(` mutation InvitationDeleteButton_DeleteInvitation($input: DeleteOrganizationInvitationInput!) { deleteOrganizationInvitation(input: $input) { ok { @@ -99,7 +102,7 @@ const MemberInvitationForm = ({ if (result.data?.inviteToOrganizationByEmail?.ok?.email) { notify(`Invited ${result.data.inviteToOrganizationByEmail.ok.email}`, 'success'); - resetForm(); + globalThis.window?.location.reload(); } }, }); @@ -160,6 +163,7 @@ function InvitationDeleteButton({ email, }, }); + globalThis.window?.location.reload(); }} > <TrashIcon /> Remove @@ -167,37 +171,11 @@ function InvitationDeleteButton({ ); } -export const Members_OrganizationMembers = gql(/* GraphQL */ ` - query Members_OrganizationMembers($selector: OrganizationSelectorInput!) { - organization(selector: $selector) { - organization { - ...OrganizationFields - owner { - ...MemberFields - } - members { - nodes { - ...MemberFields - } - total - } - invitations { - nodes { - ...Members_Invitation - } - } - } - } - } -`); - -const Invitation = ({ - invitation, - organizationCleanId, -}: { - invitation: DocumentType<typeof Members_Invitation>; +const Invitation = (props: { + invitation: FragmentType<typeof Members_Invitation>; organizationCleanId: string; -}) => { +}): ReactElement => { + const invitation = useFragment(Members_Invitation, props.invitation); const copyToClipboard = useClipboard(); const copyLink = useCallback(async () => { await copyToClipboard(`${window.location.origin}/join/${invitation.code}`); @@ -223,7 +201,7 @@ const Invitation = ({ Copy invite link </DropdownMenuItem> <InvitationDeleteButton - organizationCleanId={organizationCleanId} + organizationCleanId={props.organizationCleanId} email={invitation.email} /> </DropdownMenuContent> @@ -232,7 +210,64 @@ const Invitation = ({ ); }; -function Page({ organization }: { organization: OrganizationFieldsFragment }) { +const Page_OrganizationFragment = graphql(` + fragment Page_OrganizationFragment on Organization { + me { + ...CanAccessOrganization_MemberFragment + ...ChangePermissionsModal_MemberFragment + } + cleanId + type + owner { + id + } + members { + nodes { + id + ...ChangePermissionsModal_MemberFragment + user { + provider + displayName + email + } + } + total + } + ...OrganizationInvitations_OrganizationFragment + ...ChangePermissionsModal_OrganizationFragment + } +`); + +const OrganizationInvitations_OrganizationFragment = graphql(` + fragment OrganizationInvitations_OrganizationFragment on Organization { + cleanId + invitations { + nodes { + id + ...Members_Invitation + } + } + } +`); + +const OrganizationInvitations = (props: { + organization: FragmentType<typeof OrganizationInvitations_OrganizationFragment>; +}): ReactElement | null => { + const org = useFragment(OrganizationInvitations_OrganizationFragment, props.organization); + + return org.invitations.nodes.length ? ( + <div className="pt-3"> + <div className="border-t-4 border-solid pb-6" /> + {org.invitations.nodes.map(node => ( + <Invitation key={node.id} invitation={node} organizationCleanId={org.cleanId} /> + ))} + </div> + ) : null; +}; + +function Page(props: { organization: FragmentType<typeof Page_OrganizationFragment> }) { + const organization = useFragment(Page_OrganizationFragment, props.organization); + useOrganizationAccess({ scope: OrganizationAccessScope.Members, redirect: true, @@ -246,19 +281,10 @@ function Page({ organization }: { organization: OrganizationFieldsFragment }) { const [meQuery] = useQuery({ query: MeDocument }); const router = useRouteSelector(); - const [organizationMembersQuery] = useQuery({ - query: Members_OrganizationMembers, - variables: { - selector: { - organization: router.organizationId, - }, - }, - }); - const org = organizationMembersQuery.data?.organization?.organization; + const org = organization; const isPersonal = org?.type === OrganizationType.Personal; const members = org?.members.nodes; - const invitations = org?.invitations.nodes; useEffect(() => { if (isPersonal) { @@ -360,24 +386,34 @@ function Page({ organization }: { organization: OrganizationFieldsFragment }) { </Card> ); })} - {invitations?.length ? ( - <div className="pt-3"> - <div className="border-t-4 border-solid pb-6" /> - {invitations.map(node => ( - <Invitation key={node.id} invitation={node} organizationCleanId={org.cleanId} /> - ))} - </div> - ) : null} + <OrganizationInvitations organization={org} /> </> ); } -function MembersPage(): ReactElement { +const OrganizationMembersPageQuery = graphql(` + query OrganizationMembersPageQuery($selector: OrganizationSelectorInput!) { + organization(selector: $selector) { + organization { + ...OrganizationLayout_OrganizationFragment + ...Page_OrganizationFragment + } + } + } +`); + +function OrganizationMembersPage(): ReactElement { return ( <> <Title title="Members" /> - <OrganizationLayout value="members" className="flex w-4/5 flex-col gap-4"> - {({ organization }) => <Page organization={organization} />} + <OrganizationLayout + value="members" + className="flex w-4/5 flex-col gap-4" + query={OrganizationMembersPageQuery} + > + {({ organization }) => + organization ? <Page organization={organization.organization} /> : null + } </OrganizationLayout> </> ); @@ -385,4 +421,4 @@ function MembersPage(): ReactElement { export const getServerSideProps = withSessionProtection(); -export default authenticated(MembersPage); +export default authenticated(OrganizationMembersPage); diff --git a/packages/web/app/pages/[orgId]/view/settings.tsx b/packages/web/app/pages/[orgId]/view/settings.tsx index 355497aed..6aa8fd79b 100644 --- a/packages/web/app/pages/[orgId]/view/settings.tsx +++ b/packages/web/app/pages/[orgId]/view/settings.tsx @@ -1,6 +1,6 @@ import { ReactElement } from 'react'; import { useFormik } from 'formik'; -import { gql, useMutation, useQuery } from 'urql'; +import { useMutation, useQuery } from 'urql'; import * as Yup from 'yup'; import { authenticated } from '@/components/authenticated-container'; import { OrganizationLayout } from '@/components/layouts'; @@ -12,11 +12,11 @@ import { TransferOrganizationOwnershipModal, } from '@/components/v2/modals'; import { env } from '@/env/frontend'; +import { FragmentType, graphql, useFragment } from '@/gql'; import { CheckIntegrationsDocument, DeleteGitHubIntegrationDocument, DeleteSlackIntegrationDocument, - OrganizationFieldsFragment, OrganizationType, } from '@/graphql'; import { @@ -122,7 +122,7 @@ function Integrations(): ReactElement | null { ); } -const UpdateOrganizationNameMutation = gql(/* GraphQL */ ` +const UpdateOrganizationNameMutation = graphql(` mutation Settings_UpdateOrganizationName($input: UpdateOrganizationNameInput!) { updateOrganizationName(input: $input) { ok { @@ -142,7 +142,23 @@ const UpdateOrganizationNameMutation = gql(/* GraphQL */ ` } `); -const Page = ({ organization }: { organization: OrganizationFieldsFragment }) => { +const SettingsPageRenderer_OrganizationFragment = graphql(` + fragment SettingsPageRenderer_OrganizationFragment on Organization { + type + name + me { + ...CanAccessOrganization_MemberFragment + isOwner + } + ...DeleteOrganizationModal_OrganizationFragment + ...TransferOrganizationOwnershipModal_OrganizationFragment + } +`); + +const SettingsPageRenderer = (props: { + organization: FragmentType<typeof SettingsPageRenderer_OrganizationFragment>; +}) => { + const organization = useFragment(SettingsPageRenderer_OrganizationFragment, props.organization); useOrganizationAccess({ scope: OrganizationAccessScope.Settings, member: organization.me, @@ -294,12 +310,31 @@ const Page = ({ organization }: { organization: OrganizationFieldsFragment }) => ); }; -function SettingsPage(): ReactElement { +const OrganizationSettingsPageQuery = graphql(` + query OrganizationSettingsPageQuery($selector: OrganizationSelectorInput!) { + organization(selector: $selector) { + organization { + ...OrganizationLayout_OrganizationFragment + ...SettingsPageRenderer_OrganizationFragment + } + } + } +`); + +function OrganizationSettingsPage(): ReactElement { return ( <> <Title title="Organization settings" /> - <OrganizationLayout value="settings" className="flex flex-col gap-y-10"> - {props => <Page {...props} />} + <OrganizationLayout + value="settings" + className="flex flex-col gap-y-10" + query={OrganizationSettingsPageQuery} + > + {props => + props.organization ? ( + <SettingsPageRenderer organization={props.organization.organization} /> + ) : null + } </OrganizationLayout> </> ); @@ -307,4 +342,4 @@ function SettingsPage(): ReactElement { export const getServerSideProps = withSessionProtection(); -export default authenticated(SettingsPage); +export default authenticated(OrganizationSettingsPage); diff --git a/packages/web/app/pages/[orgId]/view/subscription/index.tsx b/packages/web/app/pages/[orgId]/view/subscription/index.tsx index a194e9dc9..8792b1f00 100644 --- a/packages/web/app/pages/[orgId]/view/subscription/index.tsx +++ b/packages/web/app/pages/[orgId]/view/subscription/index.tsx @@ -9,11 +9,7 @@ import { InvoicesList } from '@/components/organization/billing/InvoicesList'; import { RateLimitWarn } from '@/components/organization/billing/RateLimitWarn'; import { OrganizationUsageEstimationView } from '@/components/organization/Usage'; import { Card, Heading, Stat, Tabs, Title } from '@/components/v2'; -import { - OrganizationFieldsFragment, - OrgBillingInfoFieldsFragment, - OrgRateLimitFieldsFragment, -} from '@/graphql'; +import { FragmentType, graphql, useFragment } from '@/gql'; import { OrganizationAccessScope, useOrganizationAccess } from '@/lib/access/organization'; import { getIsStripeEnabled } from '@/lib/billing/stripe-public-key'; import { withSessionProtection } from '@/lib/supertokens/guard'; @@ -26,13 +22,39 @@ const DateFormatter = Intl.DateTimeFormat('en-US', { const ManagePage = dynamic(() => import('./manage')); -function Page({ - organization, -}: { - organization: OrganizationFieldsFragment & - OrgBillingInfoFieldsFragment & - OrgRateLimitFieldsFragment; +const SubscriptionPage_OrganizationFragment = graphql(` + fragment SubscriptionPage_OrganizationFragment on Organization { + me { + ...CanAccessOrganization_MemberFragment + } + billingConfiguration { + invoices { + id + } + upcomingInvoice { + amount + date + } + } + ...RateLimitWarn_OrganizationFragment + ...OrganizationInvoicesList_OrganizationFragment + ...BillingView_OrganizationFragment + ...OrganizationUsageEstimationView_OrganizationFragment + } +`); + +const SubscriptionPage_QueryFragment = graphql(` + fragment SubscriptionPage_QueryFragment on Query { + ...BillingView_QueryFragment + } +`); + +function Page(props: { + organization: FragmentType<typeof SubscriptionPage_OrganizationFragment>; + query: FragmentType<typeof SubscriptionPage_QueryFragment>; }): ReactElement | null { + const organization = useFragment(SubscriptionPage_OrganizationFragment, props.organization); + const query = useFragment(SubscriptionPage_QueryFragment, props.query); const canAccess = useOrganizationAccess({ scope: OrganizationAccessScope.Settings, member: organization?.me, @@ -65,7 +87,7 @@ function Page({ <Card className="mt-8"> <Heading className="mb-2">Plan and Reserved Volume</Heading> <div> - <BillingView organization={organization}> + <BillingView organization={organization} query={query}> {organization.billingConfiguration?.upcomingInvoice && ( <Stat> <Stat.Label>Next Invoice</Stat.Label> @@ -109,12 +131,28 @@ function Page({ ); } +const SubscriptionPageQuery = graphql(` + query SubscriptionPageQuery($selector: OrganizationSelectorInput!) { + organization(selector: $selector) { + organization { + ...OrganizationLayout_OrganizationFragment + ...SubscriptionPage_OrganizationFragment + } + } + ...SubscriptionPage_QueryFragment + } +`); + function SubscriptionPage(): ReactElement { return ( <> <Title title="Subscription & Usage" /> - <OrganizationLayout value="subscription" includeBilling includeRateLimit> - {({ organization }) => <Page organization={organization} />} + <OrganizationLayout value="subscription" query={SubscriptionPageQuery}> + {props => + props.organization ? ( + <Page organization={props.organization.organization} query={props} /> + ) : null + } </OrganizationLayout> </> ); diff --git a/packages/web/app/pages/[orgId]/view/subscription/manage.tsx b/packages/web/app/pages/[orgId]/view/subscription/manage.tsx index 3cab9afe9..fc9b21cdd 100644 --- a/packages/web/app/pages/[orgId]/view/subscription/manage.tsx +++ b/packages/web/app/pages/[orgId]/view/subscription/manage.tsx @@ -6,13 +6,12 @@ import { OrganizationLayout } from '@/components/layouts'; import { BillingPaymentMethod } from '@/components/organization/billing/BillingPaymentMethod'; import { BillingPlanPicker } from '@/components/organization/billing/BillingPlanPicker'; import { PlanSummary } from '@/components/organization/billing/PlanSummary'; -import { Button, Card, DataWrapper, Heading, Input, Slider, Stat, Title } from '@/components/v2'; +import { Button, Card, Heading, Input, Slider, Stat, Title } from '@/components/v2'; +import { FragmentType, graphql, useFragment } from '@/gql'; import { BillingPlanType } from '@/gql/graphql'; import { BillingPlansDocument, DowngradeToHobbyDocument, - OrganizationFieldsFragment, - OrgBillingInfoFieldsFragment, UpdateOrgRateLimitDocument, UpgradeToProDocument, } from '@/graphql'; @@ -20,11 +19,42 @@ import { OrganizationAccessScope, useOrganizationAccess } from '@/lib/access/org import { openChatSupport } from '@/utils'; import { CardElement, useElements, useStripe } from '@stripe/react-stripe-js'; -function Inner({ - organization, -}: { - organization: OrganizationFieldsFragment & OrgBillingInfoFieldsFragment; +const ManageSubscriptionInner_OrganizationFragment = graphql(` + fragment ManageSubscriptionInner_OrganizationFragment on Organization { + cleanId + me { + ...CanAccessOrganization_MemberFragment + } + billingConfiguration { + paymentMethod { + __typename + } + } + plan + rateLimit { + operations + } + ...BillingPaymentMethod_OrganizationFragment + } +`); + +const ManageSubscriptionInner_BillingPlansFragment = graphql(` + fragment ManageSubscriptionInner_BillingPlansFragment on BillingPlan { + ...BillingPlanPicker_PlanFragment + planType + retentionInDays + ...PlanSummary_PlanFragment + } +`); + +function Inner(props: { + organization: FragmentType<typeof ManageSubscriptionInner_OrganizationFragment>; + billingPlans: Array<FragmentType<typeof ManageSubscriptionInner_BillingPlansFragment>>; }): ReactElement | null { + const organization = useFragment( + ManageSubscriptionInner_OrganizationFragment, + props.organization, + ); const stripe = useStripe(); const elements = useElements(); const canAccess = useOrganizationAccess({ @@ -43,9 +73,7 @@ function Inner({ const updateOrgRateLimitMutation = useMutation(UpdateOrgRateLimitDocument); const planSummaryRef = useRef<HTMLDivElement>(null); - const [plan, setPlan] = useState<BillingPlanType>( - (organization?.plan || 'HOBBY') as BillingPlanType, - ); + const [plan, setPlan] = useState<BillingPlanType>(organization?.plan || 'HOBBY'); const onPlan = useCallback( (plan: BillingPlanType) => { setPlan(plan); @@ -192,116 +220,131 @@ function Inner({ downgradeToHobbyMutation[0].error || updateOrgRateLimitMutation[0].error; + const billingPlans = useFragment( + ManageSubscriptionInner_BillingPlansFragment, + props.billingPlans, + ); + + // TODO: this is also not safe as billingPlans might be an empty list. + const selectedPlan = billingPlans.find(v => v.planType === plan) ?? billingPlans[0]; + return ( - <DataWrapper query={query}> - {result => { - // TODO: this is also not safe as billingPlans might be an empty list. - const selectedPlan = - result.data.billingPlans.find(v => v.planType === plan) ?? result.data.billingPlans[0]; + <div className="flex w-full flex-col gap-5"> + <Card className="w-full"> + <Heading className="mb-4">Choose Your Plan</Heading> + <BillingPlanPicker + activePlan={organization.plan} + value={plan} + plans={billingPlans} + onPlanChange={onPlan} + /> + </Card> + <Card className="w-full self-start" ref={planSummaryRef}> + <Heading className="mb-2">Plan Summary</Heading> + <div> + {error && <QueryError showError error={error} />} - return ( - <div className="flex w-full flex-col gap-5"> - <Card className="w-full"> - <Heading className="mb-4">Choose Your Plan</Heading> - <BillingPlanPicker - activePlan={organization.plan} - value={plan} - plans={result.data.billingPlans} - onPlanChange={onPlan} - /> - </Card> - <Card className="w-full self-start" ref={planSummaryRef}> - <Heading className="mb-2">Plan Summary</Heading> - <div> - {error && <QueryError showError error={error} />} + <div className="flex flex-col"> + <div> + <PlanSummary plan={selectedPlan} operationsRateLimit={operationsRateLimit}> + {selectedPlan.planType === BillingPlanType.Pro && ( + <Stat> + <Stat.Label>Free Trial</Stat.Label> + <Stat.Number>30</Stat.Number> + <Stat.HelpText>days</Stat.HelpText> + </Stat> + )} + </PlanSummary> + </div> - <div className="flex flex-col"> - <div> - <PlanSummary - plan={selectedPlan} - retentionInDays={selectedPlan.retentionInDays} - operationsRateLimit={operationsRateLimit} - > - {selectedPlan.planType === BillingPlanType.Pro && ( - <Stat> - <Stat.Label>Free Trial</Stat.Label> - <Stat.Number>30</Stat.Number> - <Stat.HelpText>days</Stat.HelpText> - </Stat> - )} - </PlanSummary> + {plan === BillingPlanType.Pro && ( + <div className="my-8 w-1/2"> + <Heading>Define your reserved volume</Heading> + <p className="text-sm text-gray-500"> + Pro plan requires to defined quota of reported operations. + </p> + <p className="text-sm text-gray-500"> + Pick a volume a little higher than you think you'll need to avoid being rate + limited. + </p> + <p className="text-sm text-gray-500"> + Don't worry, you can always adjust it later. + </p> + <div className="mt-5 pl-2.5"> + <Slider + min={1} + max={300} + value={[operationsRateLimit]} + onValueChange={onOperationsRateLimitChange} + /> + <div className="flex justify-between"> + <span>1M</span> + <span>100M</span> + <span>200M</span> + <span>300M</span> </div> - - {plan === BillingPlanType.Pro && ( - <div className="my-8 w-1/2"> - <Heading>Define your reserved volume</Heading> - <p className="text-sm text-gray-500"> - Pro plan requires to defined quota of reported operations. - </p> - <p className="text-sm text-gray-500"> - Pick a volume a little higher than you think you'll need to avoid being rate - limited. - </p> - <p className="text-sm text-gray-500"> - Don't worry, you can always adjust it later. - </p> - <div className="mt-5 pl-2.5"> - <Slider - min={1} - max={300} - value={[operationsRateLimit]} - onValueChange={onOperationsRateLimitChange} - /> - <div className="flex justify-between"> - <span>1M</span> - <span>100M</span> - <span>200M</span> - <span>300M</span> - </div> - </div> - </div> - )} - - <div className="my-8 flex flex-row gap-6"> - <BillingPaymentMethod - className="w-1/2" - plan={selectedPlan.planType} - organizationBilling={organization} - onValidationChange={setPaymentDetailsValid} - /> - <div className="w-1/2"> - {plan === BillingPlanType.Pro && plan !== organization.plan ? ( - <div> - <Heading className="mb-3">Discount</Heading> - <Input - className="w-full" - size="medium" - value={couponCode ?? ''} - onChange={e => setCouponCode(e.target.value)} - placeholder="Code" - /> - </div> - ) : null} - </div> - </div> - - <div>{renderActions()}</div> </div> </div> - </Card> + )} + + <div className="my-8 flex flex-row gap-6"> + <BillingPaymentMethod + className="w-1/2" + plan={selectedPlan.planType} + organization={organization} + onValidationChange={setPaymentDetailsValid} + /> + <div className="w-1/2"> + {plan === BillingPlanType.Pro && plan !== organization.plan ? ( + <div> + <Heading className="mb-3">Discount</Heading> + <Input + className="w-full" + size="medium" + value={couponCode ?? ''} + onChange={e => setCouponCode(e.target.value)} + placeholder="Code" + /> + </div> + ) : null} + </div> + </div> + + <div>{renderActions()}</div> </div> - ); - }} - </DataWrapper> + </div> + </Card> + </div> ); } +const ManageSubscriptionPageQuery = graphql(` + query ManageSubscriptionPageQuery($selector: OrganizationSelectorInput!) { + organization(selector: $selector) { + organization { + ...OrganizationLayout_OrganizationFragment + ...ManageSubscriptionInner_OrganizationFragment + } + } + billingPlans { + ...ManageSubscriptionInner_BillingPlansFragment + } + } +`); + export default function ManageSubscriptionPage(): ReactElement { return ( <> <Title title="Manage Subscription" /> - <OrganizationLayout includeBilling includeRateLimit> - {({ organization }) => <Inner organization={organization} />} + <OrganizationLayout query={ManageSubscriptionPageQuery}> + {props => + props.organization ? ( + <Inner + organization={props.organization.organization} + billingPlans={props.billingPlans} + /> + ) : null + } </OrganizationLayout> </> ); diff --git a/packages/web/app/pages/action/transfer/[orgId]/[code].tsx b/packages/web/app/pages/action/transfer/[orgId]/[code].tsx index 6961d440f..65c98ec4f 100644 --- a/packages/web/app/pages/action/transfer/[orgId]/[code].tsx +++ b/packages/web/app/pages/action/transfer/[orgId]/[code].tsx @@ -1,9 +1,10 @@ import { useCallback } from 'react'; import { clsx } from 'clsx'; -import { gql, useMutation, useQuery } from 'urql'; +import { useMutation, useQuery } from 'urql'; import { authenticated } from '@/components/authenticated-container'; import { Title } from '@/components/common'; import { Button, DataWrapper } from '@/components/v2'; +import { graphql } from '@/gql'; import { useNotifications } from '@/lib/hooks/use-notifications'; import { useRouteSelector } from '@/lib/hooks/use-route-selector'; import { withSessionProtection } from '@/lib/supertokens/guard'; @@ -14,7 +15,7 @@ const classes = { actions: clsx('flex flex-row gap-2 items-center justify-center'), }; -const OrganizationTransferPage_GetRequest = gql(` +const OrganizationTransferPage_GetRequest = graphql(` query OrganizationTransferPage_GetRequest($selector: OrganizationTransferRequestSelector!) { organizationTransferRequest(selector: $selector) { organization { @@ -33,7 +34,7 @@ const OrganizationTransferPage_GetRequest = gql(` } `); -const OrganizationTransferPage_AnswerRequest = gql(` +const OrganizationTransferPage_AnswerRequest = graphql(` mutation OrganizationTransferPage_AnswerRequest($input: AnswerOrganizationTransferRequestInput!) { answerOrganizationTransferRequest(input: $input) { ok { diff --git a/packages/web/app/pages/settings.tsx b/packages/web/app/pages/settings.tsx index 34b80124f..2a8044bd4 100644 --- a/packages/web/app/pages/settings.tsx +++ b/packages/web/app/pages/settings.tsx @@ -1,26 +1,21 @@ import { ReactElement } from 'react'; import { useFormik } from 'formik'; -import { gql, useMutation, useQuery } from 'urql'; +import { useMutation, useQuery } from 'urql'; import * as Yup from 'yup'; import { authenticated } from '@/components/authenticated-container'; import { Avatar, Button, Heading, Input, SubHeader, Tabs, Title } from '@/components/v2'; +import { graphql } from '@/gql'; import { MeDocument } from '@/graphql'; import { withSessionProtection } from '@/lib/supertokens/guard'; -gql(/* GraphQL */ ` - fragment UpdateMeFragment on User { - id - fullName - displayName - } -`); - -const UpdateMeMutation = gql(/* GraphQL */ ` +const UpdateMeMutation = graphql(` mutation updateMe($input: UpdateMeInput!) { updateMe(input: $input) { ok { updatedUser { - ...UpdateMeFragment + id + fullName + displayName } } error { diff --git a/packages/web/app/src/components/admin/AdminStats.tsx b/packages/web/app/src/components/admin/AdminStats.tsx index cf1b1956d..f25b63667 100644 --- a/packages/web/app/src/components/admin/AdminStats.tsx +++ b/packages/web/app/src/components/admin/AdminStats.tsx @@ -11,10 +11,11 @@ import { clsx } from 'clsx'; import { formatISO } from 'date-fns'; import ReactECharts from 'echarts-for-react'; import AutoSizer from 'react-virtualized-auto-sizer'; -import { DocumentType, gql, useQuery } from 'urql'; +import { useQuery } from 'urql'; import { Button, DataWrapper, Stat, Table, TBody, Td, Th, THead, Tr } from '@/components/v2'; import { CHART_PRIMARY_COLOR } from '@/constants'; import { env } from '@/env/frontend'; +import { DocumentType, FragmentType, graphql, useFragment } from '@/gql'; import { OrganizationType } from '@/graphql'; import { theme } from '@/lib/charts'; import { useChartStyles } from '@/utils'; @@ -65,26 +66,25 @@ export type Filters = Partial<{ 'with-collected': boolean; }>; -const CollectedOperationsOverTime_OperationFragment = gql(/* GraphQL */ ` +const CollectedOperationsOverTime_OperationFragment = graphql(` fragment CollectedOperationsOverTime_OperationFragment on AdminOperationPoint { count date } `); -function CollectedOperationsOverTime({ - dateRange, - operations, -}: { +function CollectedOperationsOverTime(props: { dateRange: { from: Date; to: Date; }; - operations: DocumentType<typeof CollectedOperationsOverTime_OperationFragment>[]; + operations: Array<FragmentType<typeof CollectedOperationsOverTime_OperationFragment>>; }): ReactElement { + const operations = useFragment(CollectedOperationsOverTime_OperationFragment, props.operations); const dataRef = useRef<[string, number][]>(); dataRef.current ||= operations.map(node => [node.date, node.count]); const data = dataRef.current; + return ( <AutoSizer disableHeight> {size => ( @@ -107,8 +107,8 @@ function CollectedOperationsOverTime({ { type: 'time', boundaryGap: false, - min: dateRange.from, - max: dateRange.to, + min: props.dateRange.from, + max: props.dateRange.to, }, ], yAxis: [{ type: 'value', min: 0 }], @@ -143,7 +143,7 @@ function OverallStat({ label, value }: { label: string; value: number }): ReactE ); } -const AdminStatsQuery = gql(/* GraphQL */ ` +const AdminStatsQuery = graphql(` query adminStats($period: DateRangeInput!) { admin { stats(period: $period) { diff --git a/packages/web/app/src/components/get-started/wizard.tsx b/packages/web/app/src/components/get-started/wizard.tsx index 5326ba9dd..d47654f30 100644 --- a/packages/web/app/src/components/get-started/wizard.tsx +++ b/packages/web/app/src/components/get-started/wizard.tsx @@ -1,13 +1,13 @@ import { ReactElement, ReactNode } from 'react'; import clsx from 'clsx'; -import { DocumentType, gql } from 'urql'; import { Drawer } from '@/components/v2'; +import { DocumentType, graphql } from '@/gql'; import { OrganizationType } from '@/graphql'; import { getDocsUrl } from '@/lib/docs-url'; import { useToggle } from '@/lib/hooks'; import { CheckCircledIcon } from '@radix-ui/react-icons'; -const GetStartedWizard_GetStartedProgress = gql(/* GraphQL */ ` +const GetStartedWizard_GetStartedProgress = graphql(` fragment GetStartedWizard_GetStartedProgress on OrganizationGetStarted { creatingProject publishingSchema diff --git a/packages/web/app/src/components/layouts/organization.tsx b/packages/web/app/src/components/layouts/organization.tsx index c6f0eaceb..61ab22b77 100644 --- a/packages/web/app/src/components/layouts/organization.tsx +++ b/packages/web/app/src/components/layouts/organization.tsx @@ -2,18 +2,13 @@ import { ReactElement, ReactNode, useEffect } from 'react'; import NextLink from 'next/link'; import { useRouter } from 'next/router'; import cookies from 'js-cookie'; -import { useQuery } from 'urql'; +import { TypedDocumentNode, useQuery } from 'urql'; import { Button, Heading, SubHeader, Tabs } from '@/components/v2'; import { PlusIcon } from '@/components/v2/icon'; import { CreateProjectModal } from '@/components/v2/modals'; import { LAST_VISITED_ORG_KEY } from '@/constants'; -import { - OrganizationDocument, - OrganizationFieldsFragment, - OrganizationType, - OrgBillingInfoFieldsFragment, - OrgRateLimitFieldsFragment, -} from '@/graphql'; +import { FragmentType, graphql, useFragment } from '@/gql'; +import { Exact, OrganizationType } from '@/graphql'; import { canAccessOrganization, OrganizationAccessScope, @@ -29,38 +24,43 @@ enum TabValue { Subscription = 'subscription', } -type OrganizationLayout<T, P> = { - children(props: { organization: OrganizationFieldsFragment & T }): ReactNode; - value?: 'overview' | 'members' | 'settings' | 'subscription'; - className?: string; -} & P; +const OrganizationLayout_OrganizationFragment = graphql(` + fragment OrganizationLayout_OrganizationFragment on Organization { + type + name + me { + ...CanAccessOrganization_MemberFragment + } + } +`); -export function OrganizationLayout( - props: OrganizationLayout<OrgBillingInfoFieldsFragment, { includeBilling: true }>, -): ReactElement; -export function OrganizationLayout( - props: OrganizationLayout<OrgRateLimitFieldsFragment, { includeRateLimit: true }>, -): ReactElement; -export function OrganizationLayout( - props: OrganizationLayout< - OrgBillingInfoFieldsFragment & OrgRateLimitFieldsFragment, - { includeBilling: true; includeRateLimit: true } - >, -): ReactElement; -export function OrganizationLayout(props: OrganizationLayout<{}, {}>): ReactElement; -export function OrganizationLayout({ +export function OrganizationLayout< + TSatisfiesType extends { + organization?: + | { + organization?: FragmentType<typeof OrganizationLayout_OrganizationFragment> | null; + } + | null + | undefined; + }, +>({ children, value, + query, className, - includeBilling, - includeRateLimit, -}: OrganizationLayout< - {}, - { - includeBilling?: boolean; - includeRateLimit?: boolean; - } ->): ReactElement | null { +}: { + children(props: TSatisfiesType): ReactNode; + value?: 'overview' | 'members' | 'settings' | 'subscription'; + className?: string; + query: TypedDocumentNode< + TSatisfiesType, + Exact<{ + selector: { + organization: string; + }; + }> + >; +}): ReactElement | null { const router = useRouteSelector(); const { push } = useRouter(); const [isModalOpen, toggleModalOpen] = useToggle(); @@ -68,16 +68,19 @@ export function OrganizationLayout({ const orgId = router.organizationId; const [organizationQuery] = useQuery({ - query: OrganizationDocument, + query, variables: { selector: { organization: orgId, }, - includeBilling: includeBilling ?? false, - includeRateLimit: includeRateLimit ?? false, }, }); + const organization = useFragment( + OrganizationLayout_OrganizationFragment, + organizationQuery.data?.organization?.organization, + ); + useEffect(() => { if (organizationQuery.error) { cookies.remove(LAST_VISITED_ORG_KEY); @@ -87,7 +90,7 @@ export function OrganizationLayout({ }, [organizationQuery.error, router]); useOrganizationAccess({ - member: organizationQuery.data?.organization?.organization?.me, + member: organization?.me ?? null, scope: OrganizationAccessScope.Read, redirect: true, }); @@ -96,7 +99,6 @@ export function OrganizationLayout({ return null; } - const organization = organizationQuery.data?.organization?.organization; const me = organization?.me; const isRegularOrg = !organization || organization.type === OrganizationType.Regular; @@ -105,7 +107,7 @@ export function OrganizationLayout({ } if (!value) { - return <>{children({ organization })}</>; + return <>{children(organizationQuery.data!)}</>; } return ( @@ -148,7 +150,7 @@ export function OrganizationLayout({ )} </Tabs.List> <Tabs.Content value={value} className={className}> - {children({ organization })} + {children(organizationQuery.data!)} </Tabs.Content> </Tabs> </> diff --git a/packages/web/app/src/components/layouts/project.tsx b/packages/web/app/src/components/layouts/project.tsx index 0c24b7a66..939cdb5a8 100644 --- a/packages/web/app/src/components/layouts/project.tsx +++ b/packages/web/app/src/components/layouts/project.tsx @@ -1,6 +1,6 @@ import { ReactElement, ReactNode, useEffect } from 'react'; import NextLink from 'next/link'; -import { useQuery } from 'urql'; +import { TypedDocumentNode, useQuery } from 'urql'; import { Button, Heading, Link, SubHeader, Tabs } from '@/components/v2'; import { DropdownMenu, @@ -10,12 +10,8 @@ import { } from '@/components/v2/dropdown'; import { ArrowDownIcon, TargetIcon } from '@/components/v2/icon'; import { CreateTargetModal } from '@/components/v2/modals'; -import { - OrganizationFieldsFragment, - ProjectDocument, - ProjectFieldsFragment, - ProjectsDocument, -} from '@/graphql'; +import { FragmentType, graphql, useFragment } from '@/gql'; +import { Exact, ProjectsDocument } from '@/graphql'; import { canAccessProject, ProjectAccessScope, useProjectAccess } from '@/lib/access/project'; import { useRouteSelector, useToggle } from '@/lib/hooks'; import { ProjectMigrationToast } from '../project/migration-toast'; @@ -26,25 +22,65 @@ enum TabValue { Settings = 'settings', } -export const ProjectLayout = ({ +const ProjectLayout_OrganizationFragment = graphql(` + fragment ProjectLayout_OrganizationFragment on Organization { + name + me { + ...CanAccessProject_MemberFragment + } + } +`); + +const ProjectLayout_ProjectFragment = graphql(` + fragment ProjectLayout_ProjectFragment on Project { + name + type + registryModel + } +`); + +export function ProjectLayout< + /** + * LOL fire me for this. + * Because of this kind of abstraction in place I invented this complicated generic satisfaction thing. + * I'm not sure if it's worth it, but it's the only way I could think of to make it work without removing this component. + */ + TSatisfiesType extends { + organization?: + | { + organization?: FragmentType<typeof ProjectLayout_OrganizationFragment> | null; + } + | null + | undefined; + project?: FragmentType<typeof ProjectLayout_ProjectFragment> | null; + }, +>({ children, value, className, + query, }: { children(props: { - project: ProjectFieldsFragment; - organization: OrganizationFieldsFragment; + project: Exclude<TSatisfiesType['project'], null | undefined>; + organization: Exclude<TSatisfiesType['organization'], null | undefined>['organization']; }): ReactNode; value: 'targets' | 'alerts' | 'settings'; className?: string; -}): ReactElement | null => { + query: TypedDocumentNode< + TSatisfiesType, + Exact<{ + organizationId: string; + projectId: string; + }> + >; +}): ReactElement | null { const router = useRouteSelector(); const [isModalOpen, toggleModalOpen] = useToggle(); const { organizationId: orgId, projectId } = router; const [projectQuery] = useQuery({ - query: ProjectDocument, + query, variables: { organizationId: orgId, projectId, @@ -67,9 +103,14 @@ export const ProjectLayout = ({ }, }); + const organization = useFragment( + ProjectLayout_OrganizationFragment, + projectQuery.data?.organization?.organization, + ); + useProjectAccess({ scope: ProjectAccessScope.Read, - member: projectQuery.data?.organization?.organization.me, + member: organization?.me ?? null, redirect: true, }); @@ -77,11 +118,10 @@ export const ProjectLayout = ({ return null; } - const project = projectQuery.data?.project; - const org = projectQuery.data?.organization?.organization; + const project = useFragment(ProjectLayout_ProjectFragment, projectQuery.data?.project); const projects = projectsQuery.data?.projects; - if (!org || !project) { + if (!organization || !project) { return null; } @@ -90,12 +130,12 @@ export const ProjectLayout = ({ <SubHeader> <div className="container flex items-center pb-4"> <div> - {org && ( + {organization && ( <Link href={`/${orgId}`} className="line-clamp-1 flex max-w-[250px] items-center text-xs font-medium text-gray-500" > - {org.name} + {organization.name} </Link> )} <div className="flex items-center gap-2.5"> @@ -150,21 +190,30 @@ export const ProjectLayout = ({ <Tabs.Trigger value={TabValue.Targets} asChild> <NextLink href={`/${orgId}/${projectId}`}>Targets</NextLink> </Tabs.Trigger> - {canAccessProject(ProjectAccessScope.Alerts, org.me) && ( + {canAccessProject(ProjectAccessScope.Alerts, organization.me) && ( <Tabs.Trigger value={TabValue.Alerts} asChild> <NextLink href={`/${orgId}/${projectId}/view/alerts`}>Alerts</NextLink> </Tabs.Trigger> )} - {canAccessProject(ProjectAccessScope.Settings, org.me) && ( + {canAccessProject(ProjectAccessScope.Settings, organization.me) && ( <Tabs.Trigger value={TabValue.Settings} asChild> <NextLink href={`/${orgId}/${projectId}/view/settings`}>Settings</NextLink> </Tabs.Trigger> )} </Tabs.List> <Tabs.Content value={value} className={className}> - {children({ project, organization: org })} + {children({ + project: projectQuery.data?.project as Exclude< + TSatisfiesType['project'], + null | undefined + >, + organization: projectQuery.data?.organization as Exclude< + TSatisfiesType['organization'], + null | undefined + >['organization'], + })} </Tabs.Content> </Tabs> </> ); -}; +} diff --git a/packages/web/app/src/components/layouts/target.tsx b/packages/web/app/src/components/layouts/target.tsx index aacc11ea4..a0eabd54f 100644 --- a/packages/web/app/src/components/layouts/target.tsx +++ b/packages/web/app/src/components/layouts/target.tsx @@ -1,6 +1,6 @@ import { ReactElement, ReactNode, useEffect } from 'react'; import NextLink from 'next/link'; -import { gql, useQuery } from 'urql'; +import { TypedDocumentNode, useQuery } from 'urql'; import { Button, Heading, Link, SubHeader, Tabs } from '@/components/v2'; import { DropdownMenu, @@ -10,13 +10,7 @@ import { } from '@/components/v2/dropdown'; import { ArrowDownIcon, Link2Icon } from '@/components/v2/icon'; import { ConnectSchemaModal } from '@/components/v2/modals'; -import { - OrganizationFieldsFragment, - ProjectDocument, - ProjectFieldsFragment, - TargetFieldsFragment, - TargetsDocument, -} from '@/graphql'; +import { FragmentType, graphql, useFragment } from '@/gql'; import { canAccessTarget, TargetAccessScope, useTargetAccess } from '@/lib/access/target'; import { useRouteSelector, useToggle } from '@/lib/hooks'; import { QueryError } from '../common/DataWrapper'; @@ -31,100 +25,127 @@ enum TabValue { Settings = 'settings', } -const IsCDNEnabledQuery = gql(/* GraphQL */ ` - query IsCDNEnabledQuery { +const TargetLayout_OrganizationFragment = graphql(` + fragment TargetLayout_OrganizationFragment on Organization { + me { + ...CanAccessTarget_MemberFragment + } + name + } +`); + +const TargetLayout_ProjectFragment = graphql(` + fragment TargetLayout_ProjectFragment on Project { + type + name + registryModel + } +`); + +const TargetLayout_TargetConnectionFragment = graphql(` + fragment TargetLayout_TargetConnectionFragment on TargetConnection { + total + nodes { + cleanId + name + } + } +`); + +const TargetLayout_IsCDNEnabledFragment = graphql(` + fragment TargetLayout_IsCDNEnabledFragment on Query { isCDNEnabled } `); -export const TargetLayout = ({ +export const TargetLayout = < + TSatisfies extends { + organization?: + | { + organization?: FragmentType<typeof TargetLayout_OrganizationFragment> | null; + } + | null + | undefined; + project?: FragmentType<typeof TargetLayout_ProjectFragment> | null; + targets: FragmentType<typeof TargetLayout_TargetConnectionFragment>; + } & FragmentType<typeof TargetLayout_IsCDNEnabledFragment>, +>({ children, connect, value, className, + query, }: { - children(props: { - target: TargetFieldsFragment; - project: ProjectFieldsFragment; - organization: OrganizationFieldsFragment; - }): ReactNode; + children(props: TSatisfies): ReactNode; value: 'schema' | 'explorer' | 'history' | 'operations' | 'laboratory' | 'settings'; className?: string; connect?: ReactNode; + query: TypedDocumentNode< + TSatisfies, + { + organizationId: string; + projectId: string; + targetId: string; + } + >; }): ReactElement | null => { const [isModalOpen, toggleModalOpen] = useToggle(); const router = useRouteSelector(); - const { organizationId: orgId, projectId, targetId } = router; - const [targetsQuery] = useQuery({ - query: TargetsDocument, - variables: { - selector: { - organization: orgId, - project: projectId, - }, - }, - requestPolicy: 'cache-and-network', - }); - - const [projectQuery] = useQuery({ - query: ProjectDocument, + const [data] = useQuery({ + query, variables: { organizationId: orgId, projectId, + targetId, }, requestPolicy: 'cache-and-network', }); - const [isCdnEnabledQuery] = useQuery({ query: IsCDNEnabledQuery }); - - const targets = targetsQuery.data?.targets; + const isCDNEnabled = useFragment(TargetLayout_IsCDNEnabledFragment, data.data); + const targets = useFragment(TargetLayout_TargetConnectionFragment, data.data?.targets); const target = targets?.nodes.find(node => node.cleanId === targetId); - const org = projectQuery.data?.organization?.organization; - const project = projectQuery.data?.project; + const org = useFragment(TargetLayout_OrganizationFragment, data.data?.organization?.organization); + const project = useFragment(TargetLayout_ProjectFragment, data.data?.project); const me = org?.me; useEffect(() => { - if (!targetsQuery.fetching && !target) { + if (!data.fetching && !target) { // url with # provoke error Maximum update depth exceeded router.push('/404', router.asPath.replace(/#.*/, '')); } - }, [router, target, targetsQuery.fetching]); + }, [router, target, data.fetching]); useEffect(() => { - if (!projectQuery.fetching && !project) { + if (!data.fetching && !project) { // url with # provoke error Maximum update depth exceeded router.push('/404', router.asPath.replace(/#.*/, '')); } - }, [router, project, projectQuery.fetching]); + }, [router, project, data.fetching]); useTargetAccess({ scope: TargetAccessScope.Read, - member: me, + member: me ?? null, redirect: true, }); - const canAccessSchema = canAccessTarget(TargetAccessScope.RegistryRead, me); - const canAccessSettings = canAccessTarget(TargetAccessScope.Settings, me); + const canAccessSchema = canAccessTarget(TargetAccessScope.RegistryRead, me ?? null); + const canAccessSettings = canAccessTarget(TargetAccessScope.Settings, me ?? null); - if (projectQuery.fetching || targetsQuery.fetching) { + if (data.fetching) { return null; } - if (projectQuery.error || targetsQuery.error) { - return <QueryError error={projectQuery.error || targetsQuery.error} />; + if (data.error) { + return <QueryError error={data.error} />; } if (!org || !project || !target) { return null; } - if (!isCdnEnabledQuery.data) { - return null; - } - return ( <> <SubHeader> @@ -170,7 +191,7 @@ export const TargetLayout = ({ <div className="mb-10 text-xs font-bold text-[#34eab9]">{project?.type}</div> </div> {connect ?? - (isCdnEnabledQuery.data.isCDNEnabled ? ( + (isCDNEnabled?.isCDNEnabled ? ( <> <Button size="large" @@ -224,11 +245,7 @@ export const TargetLayout = ({ </Tabs.List> <Tabs.Content value={value} className={className}> - {children({ - target, - project, - organization: org, - })} + {children(data.data as TSatisfies)} </Tabs.Content> </Tabs> </> diff --git a/packages/web/app/src/components/organization/Permissions.tsx b/packages/web/app/src/components/organization/Permissions.tsx index e87a15a2a..5a7afb2ab 100644 --- a/packages/web/app/src/components/organization/Permissions.tsx +++ b/packages/web/app/src/components/organization/Permissions.tsx @@ -1,10 +1,9 @@ import { FormEventHandler, memo, ReactElement, useCallback, useState } from 'react'; import { useMutation } from 'urql'; import { Accordion, RadixSelect } from '@/components/v2'; +import { FragmentType, graphql, useFragment } from '@/gql'; import { - MemberFieldsFragment, OrganizationAccessScope, - OrganizationFieldsFragment, ProjectAccessScope, TargetAccessScope, UpdateOrganizationMemberAccessDocument, @@ -149,17 +148,38 @@ export const PermissionsSpace = memo( PermissionsSpaceInner, ) as unknown as typeof PermissionsSpaceInner; +const UsePermissionManager_OrganizationFragment = graphql(` + fragment UsePermissionManager_OrganizationFragment on Organization { + cleanId + me { + ...CanAccessOrganization_MemberFragment + ...CanAccessProject_MemberFragment + ...CanAccessTarget_MemberFragment + } + } +`); + +const UsePermissionManager_MemberFragment = graphql(` + fragment UsePermissionManager_MemberFragment on Member { + id + targetAccessScopes + projectAccessScopes + organizationAccessScopes + } +`); + export function usePermissionsManager({ - organization, - member, onSuccess, passMemberScopes, + ...props }: { - organization: OrganizationFieldsFragment; - member: MemberFieldsFragment; + organization: FragmentType<typeof UsePermissionManager_OrganizationFragment>; + member: FragmentType<typeof UsePermissionManager_MemberFragment>; passMemberScopes: boolean; onSuccess(): void; }) { + const member = useFragment(UsePermissionManager_MemberFragment, props.member); + const organization = useFragment(UsePermissionManager_OrganizationFragment, props.organization); const [state, setState] = useState<'LOADING' | 'IDLE'>('IDLE'); const notify = useNotifications(); const [, mutate] = useMutation(UpdateOrganizationMemberAccessDocument); diff --git a/packages/web/app/src/components/organization/Usage.tsx b/packages/web/app/src/components/organization/Usage.tsx index a3d549ff5..35ffe06f6 100644 --- a/packages/web/app/src/components/organization/Usage.tsx +++ b/packages/web/app/src/components/organization/Usage.tsx @@ -1,21 +1,29 @@ import { ReactElement } from 'react'; import { useQuery } from 'urql'; import { DataWrapper, Table, TBody, Td, Th, THead, Tr } from '@/components/v2'; -import { - OrganizationFieldsFragment, - OrgBillingInfoFieldsFragment, - UsageEstimationDocument, -} from '@/graphql'; +import { FragmentType, graphql, useFragment } from '@/gql'; +import { UsageEstimationDocument } from '@/graphql'; import { Scale } from '../common'; import { calculatePeriod } from '../common/TimeFilter'; const NumericFormatter = Intl.NumberFormat('en', { notation: 'standard' }); -export function OrganizationUsageEstimationView({ - organization, -}: { - organization: OrganizationFieldsFragment & OrgBillingInfoFieldsFragment; +const OrganizationUsageEstimationView_OrganizationFragment = graphql(` + fragment OrganizationUsageEstimationView_OrganizationFragment on Organization { + cleanId + rateLimit { + operations + } + } +`); + +export function OrganizationUsageEstimationView(props: { + organization: FragmentType<typeof OrganizationUsageEstimationView_OrganizationFragment>; }): ReactElement { + const organization = useFragment( + OrganizationUsageEstimationView_OrganizationFragment, + props.organization, + ); const period = calculatePeriod('month'); const [query] = useQuery({ diff --git a/packages/web/app/src/components/organization/billing/Billing.tsx b/packages/web/app/src/components/organization/billing/Billing.tsx index 7652b6aad..8e1f9676e 100644 --- a/packages/web/app/src/components/organization/billing/Billing.tsx +++ b/packages/web/app/src/components/organization/billing/Billing.tsx @@ -1,41 +1,45 @@ import { ReactNode } from 'react'; -import { useQuery } from 'urql'; -import { DataWrapper } from '@/components/v2'; -import { - BillingPlansDocument, - OrganizationFieldsFragment, - OrgBillingInfoFieldsFragment, -} from '@/graphql'; +import { FragmentType, graphql, useFragment } from '@/gql'; import { PlanSummary } from './PlanSummary'; -export function BillingView({ - organization, - children, -}: { +const BillingView_OrganizationFragment = graphql(` + fragment BillingView_OrganizationFragment on Organization { + plan + rateLimit { + retentionInDays + operations + } + } +`); + +const BillingView_QueryFragment = graphql(` + fragment BillingView_QueryFragment on Query { + billingPlans { + planType + ...PlanSummary_PlanFragment + } + } +`); + +export function BillingView(props: { children: ReactNode; - organization: OrganizationFieldsFragment & OrgBillingInfoFieldsFragment; + organization: FragmentType<typeof BillingView_OrganizationFragment>; + query: FragmentType<typeof BillingView_QueryFragment>; }) { - const [query] = useQuery({ query: BillingPlansDocument }); + const organization = useFragment(BillingView_OrganizationFragment, props.organization); + const query = useFragment(BillingView_QueryFragment, props.query); + const plan = query.billingPlans.find(v => v.planType === organization.plan); + + if (plan == null) { + return null; + } return ( - <DataWrapper query={query}> - {result => { - const plan = result.data.billingPlans.find(v => v.planType === organization.plan); - - if (plan == null) { - return null; - } - - return ( - <PlanSummary - retentionInDays={organization.rateLimit.retentionInDays} - operationsRateLimit={Math.floor(organization.rateLimit.operations / 1_000_000)} - plan={plan} - > - {children} - </PlanSummary> - ); - }} - </DataWrapper> + <PlanSummary + operationsRateLimit={Math.floor(organization.rateLimit.operations / 1_000_000)} + plan={plan} + > + {props.children} + </PlanSummary> ); } diff --git a/packages/web/app/src/components/organization/billing/BillingPaymentMethod.tsx b/packages/web/app/src/components/organization/billing/BillingPaymentMethod.tsx index 46e8cae37..391b3b6ea 100644 --- a/packages/web/app/src/components/organization/billing/BillingPaymentMethod.tsx +++ b/packages/web/app/src/components/organization/billing/BillingPaymentMethod.tsx @@ -2,25 +2,41 @@ import { ReactElement } from 'react'; import clsx from 'clsx'; import { Section } from '@/components/common'; import { Heading, Link } from '@/components/v2'; -import { BillingPlanType, OrgBillingInfoFieldsFragment } from '@/graphql'; +import { FragmentType, graphql, useFragment } from '@/gql'; +import { BillingPlanType } from '@/graphql'; import { CardElement } from '@stripe/react-stripe-js'; +const BillingPaymentMethod_OrganizationFragment = graphql(` + fragment BillingPaymentMethod_OrganizationFragment on Organization { + billingConfiguration { + paymentMethod { + brand + last4 + expMonth + expYear + } + } + } +`); + export const BillingPaymentMethod = ({ plan, - organizationBilling, onValidationChange, className, + ...props }: { plan: BillingPlanType; className?: string; - organizationBilling: OrgBillingInfoFieldsFragment; + organization: FragmentType<typeof BillingPaymentMethod_OrganizationFragment>; onValidationChange?: (isValid: boolean) => void; }): ReactElement | null => { if (plan !== 'PRO') { return null; } - if (!organizationBilling.billingConfiguration?.paymentMethod) { + const organization = useFragment(BillingPaymentMethod_OrganizationFragment, props.organization); + + if (!organization.billingConfiguration?.paymentMethod) { return ( <div className={clsx('flex flex-col gap-6', className)}> <Heading>Payment Method</Heading> @@ -50,7 +66,7 @@ export const BillingPaymentMethod = ({ </div> ); } - const info = organizationBilling.billingConfiguration.paymentMethod; + const info = organization.billingConfiguration.paymentMethod; return ( <> diff --git a/packages/web/app/src/components/organization/billing/BillingPlanPicker.tsx b/packages/web/app/src/components/organization/billing/BillingPlanPicker.tsx index fa45187f1..0d3ad32ec 100644 --- a/packages/web/app/src/components/organization/billing/BillingPlanPicker.tsx +++ b/packages/web/app/src/components/organization/billing/BillingPlanPicker.tsx @@ -1,7 +1,8 @@ import { ReactElement, ReactNode } from 'react'; import { Label, Section } from '@/components/common'; import { Link, Radio, RadioGroup } from '@/components/v2'; -import { BillingPlansQuery, BillingPlanType } from '@/graphql'; +import { FragmentType, graphql, useFragment } from '@/gql'; +import { BillingPlanType } from '@/graphql'; import { CheckIcon } from '@radix-ui/react-icons'; const planCollection: { @@ -103,17 +104,27 @@ const billingPlanLookUpMap = { [BillingPlanType.Hobby]: 'Free', } as Record<BillingPlanType, string | undefined>; +const BillingPlanPicker_PlanFragment = graphql(` + fragment BillingPlanPicker_PlanFragment on BillingPlan { + planType + id + name + basePrice + } +`); + export function BillingPlanPicker({ value, - activePlan, - plans, onPlanChange, + activePlan, + ...props }: { value: BillingPlanType; activePlan: BillingPlanType; - plans: BillingPlansQuery['billingPlans']; + plans: ReadonlyArray<FragmentType<typeof BillingPlanPicker_PlanFragment>>; onPlanChange: (plan: BillingPlanType) => void; }): ReactElement { + const plans = useFragment(BillingPlanPicker_PlanFragment, props.plans); return ( <RadioGroup value={value} onValueChange={onPlanChange} className="flex gap-4 md:!flex-row"> {plans.map(plan => ( diff --git a/packages/web/app/src/components/organization/billing/InvoicesList.tsx b/packages/web/app/src/components/organization/billing/InvoicesList.tsx index 02d6230a8..cefd00a06 100644 --- a/packages/web/app/src/components/organization/billing/InvoicesList.tsx +++ b/packages/web/app/src/components/organization/billing/InvoicesList.tsx @@ -1,13 +1,30 @@ import { ReactElement } from 'react'; import { Link, Table, TBody, Td, Th, THead, Tr } from '@/components/v2'; -import { OrganizationFieldsFragment, OrgBillingInfoFieldsFragment } from '@/graphql'; +import { FragmentType, graphql, useFragment } from '@/gql'; import { CurrencyFormatter, DateFormatter } from './helpers'; -export function InvoicesList({ - organization, -}: { - organization: OrganizationFieldsFragment & OrgBillingInfoFieldsFragment; +const OrganizationInvoicesList_OrganizationFragment = graphql(` + fragment OrganizationInvoicesList_OrganizationFragment on Organization { + billingConfiguration { + invoices { + id + date + amount + periodStart + periodEnd + pdfLink + } + } + } +`); + +export function InvoicesList(props: { + organization: FragmentType<typeof OrganizationInvoicesList_OrganizationFragment>; }): ReactElement | null { + const organization = useFragment( + OrganizationInvoicesList_OrganizationFragment, + props.organization, + ); if (!organization.billingConfiguration?.invoices?.length) { return null; } diff --git a/packages/web/app/src/components/organization/billing/PlanSummary.tsx b/packages/web/app/src/components/organization/billing/PlanSummary.tsx index 0b542ed1e..a718d550c 100644 --- a/packages/web/app/src/components/organization/billing/PlanSummary.tsx +++ b/packages/web/app/src/components/organization/billing/PlanSummary.tsx @@ -1,17 +1,28 @@ import { ReactElement, ReactNode } from 'react'; import { Stat, Table, TBody, Td, TFoot, Th, THead, Tr } from '@/components/v2'; -import { BillingPlansQuery, BillingPlanType } from '@/graphql'; +import { FragmentType, graphql, useFragment } from '@/gql'; +import { BillingPlanType } from '@/graphql'; import { CurrencyFormatter } from './helpers'; -function PriceEstimationTable({ - plan, - operationsRateLimit, -}: { - plan: BillingPlansQuery['billingPlans'][number]; +const PriceEstimationTable_PlanFragment = graphql(` + fragment PriceEstimationTable_PlanFragment on BillingPlan { + includedOperationsLimit + pricePerOperationsUnit + basePrice + planType + } +`); + +function PriceEstimationTable(props: { + plan: FragmentType<typeof PriceEstimationTable_PlanFragment>; operationsRateLimit: number; }): ReactElement { + const plan = useFragment(PriceEstimationTable_PlanFragment, props.plan); const includedOperationsInMillions = (plan.includedOperationsLimit ?? 0) / 1_000_000; - const additionalOperations = Math.max(0, operationsRateLimit - includedOperationsInMillions); + const additionalOperations = Math.max( + 0, + props.operationsRateLimit - includedOperationsInMillions, + ); const operationsTotal = (plan.pricePerOperationsUnit ?? 0) * additionalOperations; const total = (plan.basePrice ?? 0) + operationsTotal; @@ -59,17 +70,24 @@ function PriceEstimationTable({ ); } +const PlanSummary_PlanFragment = graphql(` + fragment PlanSummary_PlanFragment on BillingPlan { + planType + retentionInDays + ...PriceEstimationTable_PlanFragment + } +`); + export function PlanSummary({ - plan, operationsRateLimit, - retentionInDays, children, + ...props }: { - plan: BillingPlansQuery['billingPlans'][number]; + plan: FragmentType<typeof PlanSummary_PlanFragment>; operationsRateLimit: number; - retentionInDays: number; children: ReactNode; }): ReactElement { + const plan = useFragment(PlanSummary_PlanFragment, props.plan); if (plan.planType === BillingPlanType.Enterprise) { return ( <Stat> @@ -104,7 +122,7 @@ export function PlanSummary({ <Stat className="mb-8"> <Stat.Label>Retention</Stat.Label> <Stat.HelpText>usage reports</Stat.HelpText> - <Stat.Number>{retentionInDays} days</Stat.Number> + <Stat.Number>{plan.retentionInDays} days</Stat.Number> </Stat> </div> <PriceEstimationTable plan={plan} operationsRateLimit={operationsRateLimit} /> diff --git a/packages/web/app/src/components/organization/billing/RateLimitWarn.tsx b/packages/web/app/src/components/organization/billing/RateLimitWarn.tsx index 137e63893..eacc97526 100644 --- a/packages/web/app/src/components/organization/billing/RateLimitWarn.tsx +++ b/packages/web/app/src/components/organization/billing/RateLimitWarn.tsx @@ -1,12 +1,20 @@ import { ReactElement } from 'react'; import { Callout } from '@/components/v2'; -import { OrgRateLimitFieldsFragment } from '@/graphql'; +import { FragmentType, graphql, useFragment } from '@/gql'; -export function RateLimitWarn({ - organization, -}: { - organization: OrgRateLimitFieldsFragment; +const RateLimitWarn_OrganizationFragment = graphql(` + fragment RateLimitWarn_OrganizationFragment on Organization { + name + rateLimit { + limitedForOperations + } + } +`); + +export function RateLimitWarn(props: { + organization: FragmentType<typeof RateLimitWarn_OrganizationFragment>; }): ReactElement | null { + const organization = useFragment(RateLimitWarn_OrganizationFragment, props.organization); if (!organization.rateLimit.limitedForOperations) { return null; } diff --git a/packages/web/app/src/components/organization/settings/oidc-integration-section.tsx b/packages/web/app/src/components/organization/settings/oidc-integration-section.tsx index 5d52ac26a..b1e4d0ff8 100644 --- a/packages/web/app/src/components/organization/settings/oidc-integration-section.tsx +++ b/packages/web/app/src/components/organization/settings/oidc-integration-section.tsx @@ -2,23 +2,25 @@ import { ReactElement } from 'react'; import { useRouter } from 'next/router'; import { clsx } from 'clsx'; import { useFormik } from 'formik'; -import { DocumentType, gql, useMutation } from 'urql'; +import { useMutation } from 'urql'; import { Button, Heading, Input, Modal, Tag } from '@/components/v2'; import { AlertTriangleIcon, KeyIcon } from '@/components/v2/icon'; import { InlineCode } from '@/components/v2/inline-code'; import { env } from '@/env/frontend'; +import { DocumentType, FragmentType, graphql, useFragment } from '@/gql'; const classes = { container: clsx('flex flex-col items-stretch gap-5'), modal: clsx('w-[550px]'), }; -const OIDCIntegrationSection_OrganizationFragment = gql(/* GraphQL */ ` +const OIDCIntegrationSection_OrganizationFragment = graphql(` fragment OIDCIntegrationSection_OrganizationFragment on Organization { id oidcIntegration { id ...UpdateOIDCIntegration_OIDCIntegrationFragment + authorizationEndpoint } } `); @@ -120,7 +122,7 @@ export function OIDCIntegrationSection(props: { ); } -const CreateOIDCIntegrationModal_CreateOIDCIntegrationMutation = gql(/* GraphQL */ ` +const CreateOIDCIntegrationModal_CreateOIDCIntegrationMutation = graphql(` mutation CreateOIDCIntegrationModal_CreateOIDCIntegrationMutation( $input: CreateOIDCIntegrationInput! ) { @@ -319,10 +321,15 @@ function CreateOIDCIntegrationForm(props: { function ManageOIDCIntegrationModal(props: { isOpen: boolean; close: () => void; - oidcIntegration: DocumentType<typeof UpdateOIDCIntegration_OIDCIntegrationFragment> | null; + oidcIntegration: FragmentType<typeof UpdateOIDCIntegration_OIDCIntegrationFragment> | null; openCreateModalLink: string; }): ReactElement { - return props.oidcIntegration === null ? ( + const oidcIntegration = useFragment( + UpdateOIDCIntegration_OIDCIntegrationFragment, + props.oidcIntegration, + ); + + return oidcIntegration == null ? ( <Modal open={props.isOpen} onOpenChange={props.close} className={classes.modal}> <div className={classes.container}> <Heading>Manage OpenID Connect Integration</Heading> @@ -350,13 +357,13 @@ function ManageOIDCIntegrationModal(props: { <UpdateOIDCIntegrationForm close={props.close} isOpen={props.isOpen} - key={props.oidcIntegration.id} - oidcIntegration={props.oidcIntegration} + key={oidcIntegration.id} + oidcIntegration={oidcIntegration} /> ); } -const UpdateOIDCIntegration_OIDCIntegrationFragment = gql(/* GraphQL */ ` +const UpdateOIDCIntegration_OIDCIntegrationFragment = graphql(` fragment UpdateOIDCIntegration_OIDCIntegrationFragment on OIDCIntegration { id tokenEndpoint @@ -367,7 +374,7 @@ const UpdateOIDCIntegration_OIDCIntegrationFragment = gql(/* GraphQL */ ` } `); -const UpdateOIDCIntegrationForm_UpdateOIDCIntegrationMutation = gql(/* GraphQL */ ` +const UpdateOIDCIntegrationForm_UpdateOIDCIntegrationMutation = graphql(` mutation UpdateOIDCIntegrationForm_UpdateOIDCIntegrationMutation( $input: UpdateOIDCIntegrationInput! ) { @@ -549,7 +556,7 @@ function UpdateOIDCIntegrationForm(props: { ); } -const RemoveOIDCIntegrationForm_DeleteOIDCIntegrationMutation = gql(/* GraphQL */ ` +const RemoveOIDCIntegrationForm_DeleteOIDCIntegrationMutation = graphql(` mutation RemoveOIDCIntegrationForm_DeleteOIDCIntegrationMutation( $input: DeleteOIDCIntegrationInput! ) { diff --git a/packages/web/app/src/components/project/settings/external-composition.tsx b/packages/web/app/src/components/project/settings/external-composition.tsx index 14118d117..59f7f4d38 100644 --- a/packages/web/app/src/components/project/settings/external-composition.tsx +++ b/packages/web/app/src/components/project/settings/external-composition.tsx @@ -1,12 +1,12 @@ import { useCallback, useState } from 'react'; import { useFormik } from 'formik'; -import { gql, useMutation, useQuery } from 'urql'; +import { useMutation, useQuery } from 'urql'; import * as Yup from 'yup'; import { Button, Card, Heading, Input, Spinner, Switch } from '@/components/v2'; -import { OrganizationFieldsFragment, ProjectFieldsFragment } from '@/graphql'; +import { FragmentType, graphql, useFragment } from '@/gql'; import { useNotifications } from '@/lib/hooks'; -export const ExternalCompositionForm_EnableMutation = gql(` +export const ExternalCompositionForm_EnableMutation = graphql(` mutation ExternalCompositionForm_EnableMutation($input: EnableExternalSchemaCompositionInput!) { enableExternalSchemaComposition(input: $input) { ok { @@ -23,15 +23,31 @@ export const ExternalCompositionForm_EnableMutation = gql(` } `); +const ExternalCompositionForm_OrganizationFragment = graphql(` + fragment ExternalCompositionForm_OrganizationFragment on Organization { + cleanId + } +`); + +const ExternalCompositionForm_ProjectFragment = graphql(` + fragment ExternalCompositionForm_ProjectFragment on Project { + cleanId + } +`); + const ExternalCompositionForm = ({ - project, - organization, endpoint, + ...props }: { - project: ProjectFieldsFragment; - organization: OrganizationFieldsFragment; + project: FragmentType<typeof ExternalCompositionForm_ProjectFragment>; + organization: FragmentType<typeof ExternalCompositionForm_OrganizationFragment>; endpoint?: string; }) => { + const project = useFragment(ExternalCompositionForm_ProjectFragment, props.project); + const organization = useFragment( + ExternalCompositionForm_OrganizationFragment, + props.organization, + ); const notify = useNotifications(); const [mutation, enable] = useMutation(ExternalCompositionForm_EnableMutation); const { handleSubmit, values, handleChange, handleBlur, isSubmitting, errors, touched } = @@ -122,7 +138,7 @@ const ExternalCompositionForm = ({ ); }; -export const ExternalComposition_DisableMutation = gql(` +export const ExternalComposition_DisableMutation = graphql(` mutation ExternalComposition_DisableMutation($input: DisableExternalSchemaCompositionInput!) { disableExternalSchemaComposition(input: $input) { ok @@ -131,7 +147,7 @@ export const ExternalComposition_DisableMutation = gql(` } `); -export const ExternalComposition_ProjectConfigurationQuery = gql(` +export const ExternalComposition_ProjectConfigurationQuery = graphql(` query ExternalComposition_ProjectConfigurationQuery($selector: ProjectSelectorInput!) { project(selector: $selector) { id @@ -143,13 +159,29 @@ export const ExternalComposition_ProjectConfigurationQuery = gql(` } `); -export const ExternalCompositionSettings = ({ - project, - organization, -}: { - project: ProjectFieldsFragment; - organization: OrganizationFieldsFragment; +const ExternalCompositionSettings_OrganizationFragment = graphql(` + fragment ExternalCompositionSettings_OrganizationFragment on Organization { + cleanId + ...ExternalCompositionForm_OrganizationFragment + } +`); + +const ExternalCompositionSettings_ProjectFragment = graphql(` + fragment ExternalCompositionSettings_ProjectFragment on Project { + cleanId + ...ExternalCompositionForm_ProjectFragment + } +`); + +export const ExternalCompositionSettings = (props: { + project: FragmentType<typeof ExternalCompositionSettings_ProjectFragment>; + organization: FragmentType<typeof ExternalCompositionSettings_OrganizationFragment>; }) => { + const project = useFragment(ExternalCompositionSettings_ProjectFragment, props.project); + const organization = useFragment( + ExternalCompositionSettings_OrganizationFragment, + props.organization, + ); const [enabled, setEnabled] = useState<boolean>(); const [mutation, disableComposition] = useMutation(ExternalComposition_DisableMutation); const notify = useNotifications(); diff --git a/packages/web/app/src/components/project/settings/model-migration.tsx b/packages/web/app/src/components/project/settings/model-migration.tsx index 35db04e0a..a41a597f2 100644 --- a/packages/web/app/src/components/project/settings/model-migration.tsx +++ b/packages/web/app/src/components/project/settings/model-migration.tsx @@ -1,7 +1,8 @@ import { ReactElement, ReactNode, useCallback } from 'react'; -import { gql, useMutation } from 'urql'; +import { useMutation } from 'urql'; import { Button, Card, Heading, Tooltip } from '@/components/v2'; -import { ProjectFieldsFragment, RegistryModel } from '@/graphql'; +import { FragmentType, graphql, useFragment } from '@/gql'; +import { RegistryModel } from '@/graphql'; import { useNotifications } from '@/lib/hooks'; import { openChatSupport } from '@/utils'; import { CheckIcon, Cross2Icon, QuestionMarkCircledIcon } from '@radix-ui/react-icons'; @@ -106,7 +107,7 @@ const available = ( const notAvailable = <Tooltip content="Not available">{crossIcon}</Tooltip>; -const ModelMigrationSettings_upgradeProjectRegistryModelMutation = gql(/* GraphQL */ ` +const ModelMigrationSettings_upgradeProjectRegistryModelMutation = graphql(` mutation ModelMigrationSettings_upgradeProjectRegistryModelMutation( $input: UpdateProjectRegistryModelInput! ) { @@ -121,13 +122,19 @@ const ModelMigrationSettings_upgradeProjectRegistryModelMutation = gql(/* GraphQ } `); -export function ModelMigrationSettings({ - project, - organizationId, -}: { - project: ProjectFieldsFragment; +const ModelMigrationSettings_ProjectFragment = graphql(` + fragment ModelMigrationSettings_ProjectFragment on Project { + type + cleanId + registryModel + } +`); + +export function ModelMigrationSettings(props: { + project: FragmentType<typeof ModelMigrationSettings_ProjectFragment>; organizationId: string; }): ReactElement | null { + const project = useFragment(ModelMigrationSettings_ProjectFragment, props.project); const isStitching = project.type === 'STITCHING'; const isComposite = isStitching || project.type === 'FEDERATION'; const notify = useNotifications(); @@ -140,7 +147,7 @@ export function ModelMigrationSettings({ const result = await upgradeMutation({ input: { project: project.cleanId, - organization: organizationId, + organization: props.organizationId, model: RegistryModel.Modern, }, }); diff --git a/packages/web/app/src/components/target/explorer/common.tsx b/packages/web/app/src/components/target/explorer/common.tsx index 4d9d0efe2..dd999e75f 100644 --- a/packages/web/app/src/components/target/explorer/common.tsx +++ b/packages/web/app/src/components/target/explorer/common.tsx @@ -1,9 +1,9 @@ import React, { ReactElement, ReactNode } from 'react'; import { clsx } from 'clsx'; -import { DocumentType, gql } from 'urql'; import { PulseIcon } from '@/components/v2/icon'; import { Link } from '@/components/v2/link'; import { Markdown } from '@/components/v2/markdown'; +import { FragmentType, graphql, useFragment } from '@/gql'; import { formatNumber, useRouteSelector } from '@/lib/hooks'; import { ChatBubbleIcon } from '@radix-ui/react-icons'; import * as P from '@radix-ui/react-popover'; @@ -11,7 +11,7 @@ import { useArgumentListToggle } from './provider'; const noop = () => {}; -function useCollapsibleList<T>(list: T[], max: number, defaultValue: boolean) { +function useCollapsibleList<T>(list: ReadonlyArray<T>, max: number, defaultValue: boolean) { const [collapsed, setCollapsed] = React.useState(defaultValue === true && list.length > max); const expand = React.useCallback(() => { setCollapsed(false); @@ -44,7 +44,7 @@ function Description(props: { description: string }) { ); } -const SchemaExplorerUsageStats_UsageFragment = gql(/* GraphQL */ ` +const SchemaExplorerUsageStats_UsageFragment = graphql(` fragment SchemaExplorerUsageStats_UsageFragment on SchemaCoordinateUsage { total isUsed @@ -52,10 +52,11 @@ const SchemaExplorerUsageStats_UsageFragment = gql(/* GraphQL */ ` `); export function SchemaExplorerUsageStats(props: { - usage: DocumentType<typeof SchemaExplorerUsageStats_UsageFragment>; + usage: FragmentType<typeof SchemaExplorerUsageStats_UsageFragment>; totalRequests: number; }) { - const percentage = props.totalRequests ? (props.usage.total / props.totalRequests) * 100 : 0; + const usage = useFragment(SchemaExplorerUsageStats_UsageFragment, props.usage); + const percentage = props.totalRequests ? (usage.total / props.totalRequests) * 100 : 0; return ( <div className="flex flex-row items-center gap-2 text-xs"> @@ -63,8 +64,8 @@ export function SchemaExplorerUsageStats(props: { <PulseIcon className="h-6 w-auto" /> </div> <div className="grow"> - <div className="text-center" title={`${props.usage.total} requests`}> - {formatNumber(props.usage.total)} + <div className="text-center" title={`${usage.total} requests`}> + {formatNumber(usage.total)} </div> <div title={`${percentage.toFixed(2)}% of all requests`} @@ -78,7 +79,7 @@ export function SchemaExplorerUsageStats(props: { ); } -const GraphQLFields_FieldFragment = gql(/* GraphQL */ ` +const GraphQLFields_FieldFragment = graphql(` fragment GraphQLFields_FieldFragment on GraphQLField { name description @@ -94,7 +95,7 @@ const GraphQLFields_FieldFragment = gql(/* GraphQL */ ` } `); -const GraphQLArguments_ArgumentFragment = gql(/* GraphQL */ ` +const GraphQLArguments_ArgumentFragment = graphql(` fragment GraphQLArguments_ArgumentFragment on GraphQLArgument { name description @@ -107,7 +108,7 @@ const GraphQLArguments_ArgumentFragment = gql(/* GraphQL */ ` } `); -const GraphQLInputFields_InputFieldFragment = gql(/* GraphQL */ ` +const GraphQLInputFields_InputFieldFragment = graphql(` fragment GraphQLInputFields_InputFieldFragment on GraphQLInputField { name description @@ -127,7 +128,7 @@ export function GraphQLTypeCard( description?: string | null; implements?: string[]; totalRequests?: number; - usage?: DocumentType<typeof SchemaExplorerUsageStats_UsageFragment>; + usage?: FragmentType<typeof SchemaExplorerUsageStats_UsageFragment>; }>, ): ReactElement { return ( @@ -160,9 +161,9 @@ export function GraphQLTypeCard( } function GraphQLArguments(props: { - args: DocumentType<typeof GraphQLArguments_ArgumentFragment>[]; + args: FragmentType<typeof GraphQLArguments_ArgumentFragment>[]; }) { - const { args } = props; + const args = useFragment(GraphQLArguments_ArgumentFragment, props.args); const [isCollapsedGlobally] = useArgumentListToggle(); const [collapsed, setCollapsed] = React.useState(isCollapsedGlobally); const hasMoreThanTwo = args.length > 2; @@ -177,7 +178,7 @@ function GraphQLArguments(props: { <span className="ml-1"> <span className="text-gray-400">(</span> <div className="pl-4"> - {props.args.map(arg => { + {args.map(arg => { return ( <div key={arg.name}> {arg.name} @@ -197,13 +198,15 @@ function GraphQLArguments(props: { <span className="ml-1"> <span className="text-gray-400">(</span> <span className="space-x-2"> - {props.args.slice(0, 2).map(arg => ( - <span key={arg.name}> - {arg.name} - {': '} - <GraphQLTypeAsLink type={arg.type} /> - </span> - ))} + {args.slice(0, 2).map(arg => { + return ( + <span key={arg.name}> + {arg.name} + {': '} + <GraphQLTypeAsLink type={arg.type} /> + </span> + ); + })} {hasMoreThanTwo ? ( <span className="cursor-pointer rounded bg-gray-900 p-1 text-xs text-gray-300 hover:bg-gray-700 hover:text-white" @@ -239,12 +242,16 @@ export function GraphQLTypeCardListItem(props: { } export function GraphQLFields(props: { - fields: DocumentType<typeof GraphQLFields_FieldFragment>[]; + fields: Array<FragmentType<typeof GraphQLFields_FieldFragment>>; totalRequests: number; collapsed?: boolean; }) { const { totalRequests } = props; - const [fields, collapsed, expand] = useCollapsibleList(props.fields, 5, props.collapsed ?? false); + const [fields, collapsed, expand] = useCollapsibleList( + useFragment(GraphQLFields_FieldFragment, props.fields), + 5, + props.collapsed ?? false, + ); return ( <div className="flex flex-col"> @@ -274,13 +281,11 @@ export function GraphQLFields(props: { ); } -export function GraphQLInputFields({ - fields, - totalRequests, -}: { - fields: DocumentType<typeof GraphQLInputFields_InputFieldFragment>[]; +export function GraphQLInputFields(props: { + fields: FragmentType<typeof GraphQLInputFields_InputFieldFragment>[]; totalRequests: number; }): ReactElement { + const fields = useFragment(GraphQLInputFields_InputFieldFragment, props.fields); return ( <div className="flex flex-col"> {fields.map((field, i) => { @@ -291,7 +296,7 @@ export function GraphQLInputFields({ <span className="mr-1">:</span> <GraphQLTypeAsLink type={field.type} /> </div> - <SchemaExplorerUsageStats totalRequests={totalRequests} usage={field.usage} /> + <SchemaExplorerUsageStats totalRequests={props.totalRequests} usage={field.usage} /> </GraphQLTypeCardListItem> ); })} diff --git a/packages/web/app/src/components/target/explorer/enum-type.tsx b/packages/web/app/src/components/target/explorer/enum-type.tsx index a663d8b30..bec67ccb5 100644 --- a/packages/web/app/src/components/target/explorer/enum-type.tsx +++ b/packages/web/app/src/components/target/explorer/enum-type.tsx @@ -1,7 +1,7 @@ -import { DocumentType, gql } from 'urql'; +import { FragmentType, graphql, useFragment } from '@/gql'; import { GraphQLTypeCard, GraphQLTypeCardListItem, SchemaExplorerUsageStats } from './common'; -export const GraphQLEnumTypeComponent_TypeFragment = gql(/* GraphQL */ ` +export const GraphQLEnumTypeComponent_TypeFragment = graphql(` fragment GraphQLEnumTypeComponent_TypeFragment on GraphQLEnumType { name description @@ -21,13 +21,14 @@ export const GraphQLEnumTypeComponent_TypeFragment = gql(/* GraphQL */ ` `); export function GraphQLEnumTypeComponent(props: { - type: DocumentType<typeof GraphQLEnumTypeComponent_TypeFragment>; + type: FragmentType<typeof GraphQLEnumTypeComponent_TypeFragment>; totalRequests: number; }) { + const ttype = useFragment(GraphQLEnumTypeComponent_TypeFragment, props.type); return ( - <GraphQLTypeCard name={props.type.name} kind="enum" description={props.type.description}> + <GraphQLTypeCard name={ttype.name} kind="enum" description={ttype.description}> <div className="flex flex-col"> - {props.type.values.map((value, i) => ( + {ttype.values.map((value, i) => ( <GraphQLTypeCardListItem index={i}> <div>{value.name}</div> <SchemaExplorerUsageStats totalRequests={props.totalRequests} usage={value.usage} /> diff --git a/packages/web/app/src/components/target/explorer/filter.tsx b/packages/web/app/src/components/target/explorer/filter.tsx index d56f9f519..3021b5bcd 100644 --- a/packages/web/app/src/components/target/explorer/filter.tsx +++ b/packages/web/app/src/components/target/explorer/filter.tsx @@ -1,10 +1,11 @@ import { useMemo } from 'react'; import { useRouter } from 'next/router'; -import { gql, useQuery } from 'urql'; +import { useQuery } from 'urql'; import { Autocomplete, RadixSelect, SelectOption, Switch } from '@/components/v2'; +import { graphql } from '@/gql'; import { useArgumentListToggle, usePeriodSelector } from './provider'; -const SchemaExplorerFilter_AllTypes = gql(/* GraphQL */ ` +const SchemaExplorerFilter_AllTypes = graphql(` query SchemaExplorerFilter_AllTypes( $organization: ID! $project: ID! diff --git a/packages/web/app/src/components/target/explorer/input-object-type.tsx b/packages/web/app/src/components/target/explorer/input-object-type.tsx index 32601aa3a..31a0dd980 100644 --- a/packages/web/app/src/components/target/explorer/input-object-type.tsx +++ b/packages/web/app/src/components/target/explorer/input-object-type.tsx @@ -1,7 +1,7 @@ -import { DocumentType, gql } from 'urql'; +import { FragmentType, graphql, useFragment } from '@/gql'; import { GraphQLInputFields, GraphQLTypeCard } from './common'; -export const GraphQLInputObjectTypeComponent_TypeFragment = gql(/* GraphQL */ ` +export const GraphQLInputObjectTypeComponent_TypeFragment = graphql(` fragment GraphQLInputObjectTypeComponent_TypeFragment on GraphQLInputObjectType { name description @@ -15,18 +15,19 @@ export const GraphQLInputObjectTypeComponent_TypeFragment = gql(/* GraphQL */ ` `); export function GraphQLInputObjectTypeComponent(props: { - type: DocumentType<typeof GraphQLInputObjectTypeComponent_TypeFragment>; + type: FragmentType<typeof GraphQLInputObjectTypeComponent_TypeFragment>; totalRequests: number; }) { + const ttype = useFragment(GraphQLInputObjectTypeComponent_TypeFragment, props.type); return ( <GraphQLTypeCard kind="input" - name={props.type.name} - description={props.type.description} + name={ttype.name} + description={ttype.description} totalRequests={props.totalRequests} - usage={props.type.usage} + usage={ttype.usage} > - <GraphQLInputFields fields={props.type.fields} totalRequests={props.totalRequests} /> + <GraphQLInputFields fields={ttype.fields} totalRequests={props.totalRequests} /> </GraphQLTypeCard> ); } diff --git a/packages/web/app/src/components/target/explorer/interface-type.tsx b/packages/web/app/src/components/target/explorer/interface-type.tsx index 36843e161..fae7e04d1 100644 --- a/packages/web/app/src/components/target/explorer/interface-type.tsx +++ b/packages/web/app/src/components/target/explorer/interface-type.tsx @@ -1,7 +1,7 @@ -import { DocumentType, gql } from 'urql'; +import { FragmentType, graphql, useFragment } from '@/gql'; import { GraphQLFields, GraphQLTypeCard } from './common'; -export const GraphQLInterfaceTypeComponent_TypeFragment = gql(/* GraphQL */ ` +export const GraphQLInterfaceTypeComponent_TypeFragment = graphql(` fragment GraphQLInterfaceTypeComponent_TypeFragment on GraphQLInterfaceType { name description @@ -16,17 +16,18 @@ export const GraphQLInterfaceTypeComponent_TypeFragment = gql(/* GraphQL */ ` `); export function GraphQLInterfaceTypeComponent(props: { - type: DocumentType<typeof GraphQLInterfaceTypeComponent_TypeFragment>; + type: FragmentType<typeof GraphQLInterfaceTypeComponent_TypeFragment>; totalRequests: number; }) { + const ttype = useFragment(GraphQLInterfaceTypeComponent_TypeFragment, props.type); return ( <GraphQLTypeCard kind="interface" - name={props.type.name} - description={props.type.description} - implements={props.type.interfaces} + name={ttype.name} + description={ttype.description} + implements={ttype.interfaces} > - <GraphQLFields fields={props.type.fields} totalRequests={props.totalRequests} /> + <GraphQLFields fields={ttype.fields} totalRequests={props.totalRequests} /> </GraphQLTypeCard> ); } diff --git a/packages/web/app/src/components/target/explorer/object-type.tsx b/packages/web/app/src/components/target/explorer/object-type.tsx index 1d219ec5b..668f1ea1f 100644 --- a/packages/web/app/src/components/target/explorer/object-type.tsx +++ b/packages/web/app/src/components/target/explorer/object-type.tsx @@ -1,7 +1,7 @@ -import { DocumentType, gql } from 'urql'; +import { FragmentType, graphql, useFragment } from '@/gql'; import { GraphQLFields, GraphQLTypeCard } from './common'; -export const GraphQLObjectTypeComponent_TypeFragment = gql(/* GraphQL */ ` +export const GraphQLObjectTypeComponent_TypeFragment = graphql(` fragment GraphQLObjectTypeComponent_TypeFragment on GraphQLObjectType { name description @@ -16,19 +16,20 @@ export const GraphQLObjectTypeComponent_TypeFragment = gql(/* GraphQL */ ` `); export function GraphQLObjectTypeComponent(props: { - type: DocumentType<typeof GraphQLObjectTypeComponent_TypeFragment>; + type: FragmentType<typeof GraphQLObjectTypeComponent_TypeFragment>; totalRequests: number; collapsed?: boolean; }) { + const ttype = useFragment(GraphQLObjectTypeComponent_TypeFragment, props.type); return ( <GraphQLTypeCard kind="type" - name={props.type.name} - description={props.type.description} - implements={props.type.interfaces} + name={ttype.name} + description={ttype.description} + implements={ttype.interfaces} > <GraphQLFields - fields={props.type.fields} + fields={ttype.fields} totalRequests={props.totalRequests} collapsed={props.collapsed} /> diff --git a/packages/web/app/src/components/target/explorer/scalar-type.tsx b/packages/web/app/src/components/target/explorer/scalar-type.tsx index 9ea90033d..95fb5191d 100644 --- a/packages/web/app/src/components/target/explorer/scalar-type.tsx +++ b/packages/web/app/src/components/target/explorer/scalar-type.tsx @@ -1,8 +1,8 @@ -import { DocumentType, gql } from 'urql'; import { Markdown } from '@/components/v2/markdown'; +import { FragmentType, graphql, useFragment } from '@/gql'; import { GraphQLTypeCard, SchemaExplorerUsageStats } from './common'; -export const GraphQLScalarTypeComponent_TypeFragment = gql(/* GraphQL */ ` +export const GraphQLScalarTypeComponent_TypeFragment = graphql(` fragment GraphQLScalarTypeComponent_TypeFragment on GraphQLScalarType { name description @@ -13,18 +13,17 @@ export const GraphQLScalarTypeComponent_TypeFragment = gql(/* GraphQL */ ` `); export function GraphQLScalarTypeComponent(props: { - type: DocumentType<typeof GraphQLScalarTypeComponent_TypeFragment>; + type: FragmentType<typeof GraphQLScalarTypeComponent_TypeFragment>; totalRequests: number; }) { + const ttype = useFragment(GraphQLScalarTypeComponent_TypeFragment, props.type); return ( - <GraphQLTypeCard name={props.type.name} kind="scalar"> + <GraphQLTypeCard name={ttype.name} kind="scalar"> <div className="flex flex-row gap-4 p-4"> <div className="grow text-sm"> - {typeof props.type.description === 'string' ? ( - <Markdown content={props.type.description} /> - ) : null} + {typeof ttype.description === 'string' ? <Markdown content={ttype.description} /> : null} </div> - <SchemaExplorerUsageStats totalRequests={props.totalRequests} usage={props.type.usage} /> + <SchemaExplorerUsageStats totalRequests={props.totalRequests} usage={ttype.usage} /> </div> </GraphQLTypeCard> ); diff --git a/packages/web/app/src/components/target/explorer/union-type.tsx b/packages/web/app/src/components/target/explorer/union-type.tsx index b8c4f8f08..97c88932c 100644 --- a/packages/web/app/src/components/target/explorer/union-type.tsx +++ b/packages/web/app/src/components/target/explorer/union-type.tsx @@ -1,7 +1,7 @@ -import { DocumentType, gql } from 'urql'; +import { FragmentType, graphql, useFragment } from '@/gql'; import { GraphQLTypeCard, GraphQLTypeCardListItem, SchemaExplorerUsageStats } from './common'; -export const GraphQLUnionTypeComponent_TypeFragment = gql(/* GraphQL */ ` +export const GraphQLUnionTypeComponent_TypeFragment = graphql(` fragment GraphQLUnionTypeComponent_TypeFragment on GraphQLUnionType { name description @@ -18,13 +18,14 @@ export const GraphQLUnionTypeComponent_TypeFragment = gql(/* GraphQL */ ` `); export function GraphQLUnionTypeComponent(props: { - type: DocumentType<typeof GraphQLUnionTypeComponent_TypeFragment>; + type: FragmentType<typeof GraphQLUnionTypeComponent_TypeFragment>; totalRequests: number; }) { + const ttype = useFragment(GraphQLUnionTypeComponent_TypeFragment, props.type); return ( - <GraphQLTypeCard name={props.type.name} kind="union" description={props.type.description}> + <GraphQLTypeCard name={ttype.name} kind="union" description={ttype.description}> <div className="flex flex-col"> - {props.type.members.map((member, i) => ( + {ttype.members.map((member, i) => ( <GraphQLTypeCardListItem index={i}> <div>{member.name}</div> <SchemaExplorerUsageStats totalRequests={props.totalRequests} usage={member.usage} /> diff --git a/packages/web/app/src/components/target/operations/List.tsx b/packages/web/app/src/components/target/operations/List.tsx index 9eef8f740..e30a549a5 100644 --- a/packages/web/app/src/components/target/operations/List.tsx +++ b/packages/web/app/src/components/target/operations/List.tsx @@ -1,11 +1,12 @@ import { ReactElement, SetStateAction, useCallback, useMemo, useState } from 'react'; -import { clsx } from 'clsx'; -import { gql, useQuery } from 'urql'; +import clsx from 'clsx'; +import { useQuery } from 'urql'; import { useDebouncedCallback } from 'use-debounce'; import { Scale, Section } from '@/components/common'; import { GraphQLHighlight } from '@/components/common/GraphQLSDLBlock'; import { Button, Drawer, Input, Table, TBody, Td, Th, THead, Tooltip, Tr } from '@/components/v2'; import { env } from '@/env/frontend'; +import { graphql } from '@/gql'; import { DateRangeInput, OperationsStatsDocument, OperationStatsFieldsFragment } from '@/graphql'; import { useDecimal, useFormattedDuration, useFormattedNumber, useToggle } from '@/lib/hooks'; import { ChevronUpIcon, ExclamationTriangleIcon } from '@radix-ui/react-icons'; @@ -34,7 +35,7 @@ interface Operation { hash: string; } -const GraphQLOperationBody_GetOperationBodyQuery = gql(/* GraphQL */ ` +const GraphQLOperationBody_GetOperationBodyQuery = graphql(` query GraphQLOperationBody_GetOperationBodyQuery($selector: OperationBodyByHashInput!) { operationBodyByHash(selector: $selector) } diff --git a/packages/web/app/src/components/target/settings/cdn-access-tokens.tsx b/packages/web/app/src/components/target/settings/cdn-access-tokens.tsx index 8682ff166..b98890e96 100644 --- a/packages/web/app/src/components/target/settings/cdn-access-tokens.tsx +++ b/packages/web/app/src/components/target/settings/cdn-access-tokens.tsx @@ -1,7 +1,7 @@ import { ReactElement, useEffect, useMemo, useState } from 'react'; import { useRouter } from 'next/router'; import { useFormik } from 'formik'; -import { gql, useMutation, useQuery } from 'urql'; +import { useMutation, useQuery } from 'urql'; import * as Yup from 'yup'; import { Button, @@ -18,14 +18,12 @@ import { } from '@/components/v2'; import { AlertTriangleIcon, TrashIcon } from '@/components/v2/icon'; import { InlineCode } from '@/components/v2/inline-code'; +import { FragmentType, graphql, useFragment } from '@/gql'; import { TargetAccessScope } from '@/gql/graphql'; -import { MemberFieldsFragment } from '@/graphql'; import { canAccessTarget } from '@/lib/access/target'; import { useRouteSelector } from '@/lib/hooks'; -// Note: this will be used in the future :) -// eslint-disable-next-line @typescript-eslint/no-unused-vars -const CDNAccessTokeRowFragment = gql(/* GraphQL */ ` +const CDNAccessTokeRowFragment = graphql(` fragment CDNAccessTokens_CdnAccessTokenRowFragment on CdnAccessToken { id firstCharacters @@ -35,7 +33,7 @@ const CDNAccessTokeRowFragment = gql(/* GraphQL */ ` } `); -const CDNAccessTokenCreateMutation = gql(/* GraphQL */ ` +const CDNAccessTokenCreateMutation = graphql(` mutation CDNAccessTokens_CDNAccessTokenCreateMutation($input: CreateCdnAccessTokenInput!) { createCdnAccessToken(input: $input) { error { @@ -43,6 +41,7 @@ const CDNAccessTokenCreateMutation = gql(/* GraphQL */ ` } ok { createdCdnAccessToken { + id ...CDNAccessTokens_CdnAccessTokenRowFragment } secretAccessToken @@ -180,7 +179,7 @@ function CreateCDNAccessTokenModal(props: { ); } -const CDNAccessTokenDeleteMutation = gql(/* GraphQL */ ` +const CDNAccessTokenDeleteMutation = graphql(` mutation CDNAccessTokens_DeleteCDNAccessToken($input: DeleteCdnAccessTokenInput!) { deleteCdnAccessToken(input: $input) { error { @@ -300,7 +299,7 @@ function DeleteCDNAccessTokenModal(props: { ); } -const CDNAccessTokensQuery = gql(/* GraphQL */ ` +const CDNAccessTokensQuery = graphql(` query CDNAccessTokensQuery($selector: TargetSelectorInput!, $first: Int!, $after: String) { target(selector: $selector) { id @@ -332,7 +331,16 @@ const isDeleteCDNAccessTokenModalPath = (path: string): null | string => { return result[1]; }; -export function CDNAccessTokens(props: { me: MemberFieldsFragment }): React.ReactElement { +const CDNAccessTokens_MeFragment = graphql(` + fragment CDNAccessTokens_MeFragment on Member { + ...CanAccessTarget_MemberFragment + } +`); + +export function CDNAccessTokens(props: { + me: FragmentType<typeof CDNAccessTokens_MeFragment>; +}): React.ReactElement { + const me = useFragment(CDNAccessTokens_MeFragment, props.me); const routerSelector = useRouteSelector(); const router = useRouter(); @@ -365,7 +373,7 @@ export function CDNAccessTokens(props: { me: MemberFieldsFragment }): React.Reac requestPolicy: 'cache-and-network', }); - const canManage = canAccessTarget(TargetAccessScope.Settings, props.me); + const canManage = canAccessTarget(TargetAccessScope.Settings, me); return ( <Card> @@ -394,31 +402,36 @@ export function CDNAccessTokens(props: { me: MemberFieldsFragment }): React.Reac )} <Table> <TBody> - {target?.data?.target?.cdnAccessTokens.edges?.map(edge => ( - <Tr key={edge.node.id}> - <Td> - {edge.node.firstCharacters + - new Array(10).fill('•').join('') + - edge.node.lastCharacters} - </Td> - <Td>{edge.node.alias}</Td> - <Td align="right"> - created <TimeAgo date={edge.node.createdAt} /> - </Td> - <Td align="right"> - <Button - className="hover:text-red-500" - onClick={() => { - void router.push(`${router.asPath}#delete-cdn-access-token?id=${edge.node.id}`); - }} - > - <TrashIcon /> - </Button> - </Td> - </Tr> - ))} + {target?.data?.target?.cdnAccessTokens.edges?.map(edge => { + const node = useFragment(CDNAccessTokeRowFragment, edge.node); + + return ( + <Tr key={node.id}> + <Td> + {node.firstCharacters + new Array(10).fill('•').join('') + node.lastCharacters} + </Td> + <Td>{node.alias}</Td> + <Td align="right"> + created <TimeAgo date={node.createdAt} /> + </Td> + <Td align="right"> + <Button + className="hover:text-red-500" + onClick={() => { + void router.push( + `${router.asPath}#delete-cdn-access-token?id=${edge.node.id}`, + ); + }} + > + <TrashIcon /> + </Button> + </Td> + </Tr> + ); + })} </TBody> </Table> + <div className="my-3.5 flex justify-end"> {target.data?.target?.cdnAccessTokens.pageInfo.hasPreviousPage ? ( <Button diff --git a/packages/web/app/src/components/v2/modals/change-permissions.tsx b/packages/web/app/src/components/v2/modals/change-permissions.tsx index 58c337b6d..c3193265c 100644 --- a/packages/web/app/src/components/v2/modals/change-permissions.tsx +++ b/packages/web/app/src/components/v2/modals/change-permissions.tsx @@ -1,20 +1,33 @@ import { ReactElement } from 'react'; import { PermissionsSpace, usePermissionsManager } from '@/components/organization/Permissions'; import { Accordion, Button, Heading, Modal } from '@/components/v2'; -import { MemberFieldsFragment, OrganizationFieldsFragment } from '@/graphql'; +import { FragmentType, graphql, useFragment } from '@/gql'; import { scopes } from '@/lib/access/common'; +const ChangePermissionsModal_OrganizationFragment = graphql(` + fragment ChangePermissionsModal_OrganizationFragment on Organization { + ...UsePermissionManager_OrganizationFragment + } +`); + +const ChangePermissionsModal_MemberFragment = graphql(` + fragment ChangePermissionsModal_MemberFragment on Member { + ...UsePermissionManager_MemberFragment + } +`); + export function ChangePermissionsModal({ isOpen, toggleModalOpen, - organization, - member, + ...props }: { isOpen: boolean; toggleModalOpen: () => void; - organization: OrganizationFieldsFragment; - member: MemberFieldsFragment; + organization: FragmentType<typeof ChangePermissionsModal_OrganizationFragment>; + member: FragmentType<typeof ChangePermissionsModal_MemberFragment>; }): ReactElement { + const organization = useFragment(ChangePermissionsModal_OrganizationFragment, props.organization); + const member = useFragment(ChangePermissionsModal_MemberFragment, props.member); const manager = usePermissionsManager({ onSuccess: toggleModalOpen, organization, diff --git a/packages/web/app/src/components/v2/modals/connect-schema.tsx b/packages/web/app/src/components/v2/modals/connect-schema.tsx index bb153adf8..9d9698c2a 100644 --- a/packages/web/app/src/components/v2/modals/connect-schema.tsx +++ b/packages/web/app/src/components/v2/modals/connect-schema.tsx @@ -1,10 +1,11 @@ import { ReactElement } from 'react'; -import { gql, useQuery } from 'urql'; +import { useQuery } from 'urql'; import { Button, CopyValue, Heading, Link, Modal, Tag } from '@/components/v2'; +import { graphql } from '@/gql'; import { getDocsUrl } from '@/lib/docs-url'; import { useRouteSelector } from '@/lib/hooks'; -const ConnectSchemaModalQuery = gql(/* GraphQL */ ` +const ConnectSchemaModalQuery = graphql(` query ConnectSchemaModal($targetSelector: TargetSelectorInput!) { target(selector: $targetSelector) { id diff --git a/packages/web/app/src/components/v2/modals/create-access-token.tsx b/packages/web/app/src/components/v2/modals/create-access-token.tsx index a58b383f0..122b01937 100644 --- a/packages/web/app/src/components/v2/modals/create-access-token.tsx +++ b/packages/web/app/src/components/v2/modals/create-access-token.tsx @@ -1,14 +1,14 @@ import { ReactElement } from 'react'; import { useFormik } from 'formik'; -import { gql, useMutation, useQuery } from 'urql'; +import { useMutation, useQuery } from 'urql'; import * as Yup from 'yup'; import { PermissionsSpace, usePermissionsManager } from '@/components/organization/Permissions'; import { Accordion, Button, CopyValue, Heading, Input, Modal, Tag } from '@/components/v2'; -import { OrganizationDocument, OrganizationQuery } from '@/graphql'; +import { FragmentType, graphql, useFragment } from '@/gql'; import { scopes } from '@/lib/access/common'; import { useRouteSelector } from '@/lib/hooks'; -const CreateAccessToken_CreateTokenMutation = gql(/* GraphQL */ ` +const CreateAccessToken_CreateTokenMutation = graphql(` mutation CreateAccessToken_CreateToken($input: CreateTokenInput!) { createToken(input: $input) { ok { @@ -29,6 +29,16 @@ const CreateAccessToken_CreateTokenMutation = gql(/* GraphQL */ ` } `); +const CreateAccessTokenModalQuery = graphql(` + query CreateAccessTokenModalQuery($organizationId: ID!) { + organization(selector: { organization: $organizationId }) { + organization { + ...CreateAccessTokenModalContent_OrganizationFragment + } + } + } +`); + export function CreateAccessTokenModal({ isOpen, toggleModalOpen, @@ -38,11 +48,9 @@ export function CreateAccessTokenModal({ }): ReactElement { const router = useRouteSelector(); const [organizationQuery] = useQuery({ - query: OrganizationDocument, + query: CreateAccessTokenModalQuery, variables: { - selector: { - organization: router.organizationId, - }, + organizationId: router.organizationId, }, }); @@ -63,13 +71,27 @@ export function CreateAccessTokenModal({ ); } +const CreateAccessTokenModalContent_OrganizationFragment = graphql(` + fragment CreateAccessTokenModalContent_OrganizationFragment on Organization { + id + ...UsePermissionManager_OrganizationFragment + me { + ...UsePermissionManager_MemberFragment + } + } +`); + function ModalContent(props: { - organization: Exclude<OrganizationQuery['organization'], null | undefined>['organization']; + organization: FragmentType<typeof CreateAccessTokenModalContent_OrganizationFragment>; organizationId: string; projectId: string; targetId: string; toggleModalOpen: () => void; }): ReactElement { + const organization = useFragment( + CreateAccessTokenModalContent_OrganizationFragment, + props.organization, + ); const [mutation, mutate] = useMutation(CreateAccessToken_CreateTokenMutation); const { handleSubmit, values, handleChange, handleBlur, isSubmitting, errors, touched } = @@ -95,8 +117,8 @@ function ModalContent(props: { const manager = usePermissionsManager({ onSuccess() {}, - organization: props.organization, - member: props.organization.me, + organization, + member: organization.me, passMemberScopes: false, }); diff --git a/packages/web/app/src/components/v2/modals/create-channel.tsx b/packages/web/app/src/components/v2/modals/create-channel.tsx index 2eb04dc3a..e21a175f2 100644 --- a/packages/web/app/src/components/v2/modals/create-channel.tsx +++ b/packages/web/app/src/components/v2/modals/create-channel.tsx @@ -1,12 +1,13 @@ import { ReactElement } from 'react'; import { useFormik } from 'formik'; -import { gql, useMutation } from 'urql'; +import { useMutation } from 'urql'; import * as Yup from 'yup'; import { Button, Heading, Input, Modal, Select, Tag } from '@/components/v2'; +import { graphql } from '@/gql'; import { AlertChannelType } from '@/graphql'; import { useRouteSelector } from '@/lib/hooks'; -const CreateChannel_AddAlertChannelMutation = gql(/* GraphQL */ ` +const CreateChannel_AddAlertChannelMutation = graphql(` mutation CreateChannel_AddAlertChannel($input: AddAlertChannelInput!) { addAlertChannel(input: $input) { ok { diff --git a/packages/web/app/src/components/v2/modals/create-organization.tsx b/packages/web/app/src/components/v2/modals/create-organization.tsx index 928662f25..1a0e49879 100644 --- a/packages/web/app/src/components/v2/modals/create-organization.tsx +++ b/packages/web/app/src/components/v2/modals/create-organization.tsx @@ -3,9 +3,9 @@ import { useFormik } from 'formik'; import { useMutation } from 'urql'; import * as Yup from 'yup'; import { Button, Heading, Input } from '@/components/v2'; -import { gql } from '@urql/core'; +import { graphql } from '@/gql'; -const CreateOrganizationMutation = gql(/* GraphQL */ ` +const CreateOrganizationMutation = graphql(` mutation CreateOrganizationMutation($input: CreateOrganizationInput!) { createOrganization(input: $input) { ok { @@ -14,6 +14,7 @@ const CreateOrganizationMutation = gql(/* GraphQL */ ` organization } organization { + cleanId ...OrganizationFields } } diff --git a/packages/web/app/src/components/v2/modals/create-project.tsx b/packages/web/app/src/components/v2/modals/create-project.tsx index 5189b470a..b925ed6f5 100644 --- a/packages/web/app/src/components/v2/modals/create-project.tsx +++ b/packages/web/app/src/components/v2/modals/create-project.tsx @@ -1,13 +1,14 @@ import { ReactElement } from 'react'; import { useRouter } from 'next/router'; import { useFormik } from 'formik'; -import { gql, useMutation } from 'urql'; +import { useMutation } from 'urql'; import * as Yup from 'yup'; import { Button, Heading, Input, Modal, ProjectTypes } from '@/components/v2'; +import { graphql } from '@/gql'; import { ProjectType } from '@/graphql'; import { useRouteSelector } from '@/lib/hooks'; -const CreateProjectMutation = gql(/* GraphQL */ ` +const CreateProjectMutation = graphql(` mutation CreateProject_CreateProject($input: CreateProjectInput!) { createProject(input: $input) { ok { @@ -16,6 +17,7 @@ const CreateProjectMutation = gql(/* GraphQL */ ` project } createdProject { + cleanId ...ProjectFields } createdTargets { diff --git a/packages/web/app/src/components/v2/modals/create-target.tsx b/packages/web/app/src/components/v2/modals/create-target.tsx index 66ae2d3a2..202c5c75e 100644 --- a/packages/web/app/src/components/v2/modals/create-target.tsx +++ b/packages/web/app/src/components/v2/modals/create-target.tsx @@ -1,12 +1,13 @@ import { ReactElement } from 'react'; import { useRouter } from 'next/router'; import { useFormik } from 'formik'; -import { gql, useMutation } from 'urql'; +import { useMutation } from 'urql'; import * as Yup from 'yup'; import { Button, Heading, Input, Modal } from '@/components/v2'; +import { graphql } from '@/gql'; import { useRouteSelector } from '@/lib/hooks'; -const CreateTarget_CreateTargetMutation = gql(/* GraphQL */ ` +const CreateTarget_CreateTargetMutation = graphql(` mutation CreateTarget_CreateTarget($input: CreateTargetInput!) { createTarget(input: $input) { ok { @@ -16,6 +17,7 @@ const CreateTarget_CreateTargetMutation = gql(/* GraphQL */ ` target } createdTarget { + cleanId ...TargetFields } } diff --git a/packages/web/app/src/components/v2/modals/delete-organization.tsx b/packages/web/app/src/components/v2/modals/delete-organization.tsx index 3059cfb34..c2da7fd74 100644 --- a/packages/web/app/src/components/v2/modals/delete-organization.tsx +++ b/packages/web/app/src/components/v2/modals/delete-organization.tsx @@ -3,18 +3,29 @@ import { useRouter } from 'next/router'; import { useMutation } from 'urql'; import { Button, Heading, Modal } from '@/components/v2'; import { TrashIcon } from '@/components/v2/icon'; -import { DeleteOrganizationDocument, OrganizationFieldsFragment } from '@/graphql'; +import { FragmentType, graphql, useFragment } from '@/gql'; +import { DeleteOrganizationDocument } from '@/graphql'; import { useRouteSelector } from '@/lib/hooks'; +const DeleteOrganizationModal_OrganizationFragment = graphql(` + fragment DeleteOrganizationModal_OrganizationFragment on Organization { + cleanId + } +`); + export const DeleteOrganizationModal = ({ isOpen, toggleModalOpen, - organization, + ...props }: { isOpen: boolean; toggleModalOpen: () => void; - organization: OrganizationFieldsFragment; + organization: FragmentType<typeof DeleteOrganizationModal_OrganizationFragment>; }): ReactElement => { + const organization = useFragment( + DeleteOrganizationModal_OrganizationFragment, + props.organization, + ); const [, mutate] = useMutation(DeleteOrganizationDocument); const router = useRouteSelector(); const { replace } = useRouter(); diff --git a/packages/web/app/src/components/v2/modals/transfer-organization-ownership.tsx b/packages/web/app/src/components/v2/modals/transfer-organization-ownership.tsx index 08702c293..197a57a5f 100644 --- a/packages/web/app/src/components/v2/modals/transfer-organization-ownership.tsx +++ b/packages/web/app/src/components/v2/modals/transfer-organization-ownership.tsx @@ -1,18 +1,19 @@ import { Fragment, ReactElement, useCallback, useState } from 'react'; import clsx from 'clsx'; import { useFormik } from 'formik'; -import { gql, useMutation, useQuery } from 'urql'; +import { useMutation, useQuery } from 'urql'; import * as Yup from 'yup'; import { Button, Heading, Input, Modal } from '@/components/v2'; import { ArrowDownIcon, CheckIcon } from '@/components/v2/icon'; -import { MemberFieldsFragment, OrganizationFieldsFragment } from '@/graphql'; +import { FragmentType, graphql, useFragment } from '@/gql'; +import { MemberFieldsFragment } from '@/graphql'; import { useNotifications } from '@/lib/hooks'; import { Combobox as HeadlessCombobox, Transition as HeadlessTransition } from '@headlessui/react'; const Combobox = HeadlessCombobox as any; const Transition = HeadlessTransition as any; -const TransferOrganizationOwnership_Request = gql(/* GraphQL */ ` +const TransferOrganizationOwnership_Request = graphql(` mutation TransferOrganizationOwnership_Request($input: RequestOrganizationTransferInput!) { requestOrganizationTransfer(input: $input) { ok { @@ -25,7 +26,7 @@ const TransferOrganizationOwnership_Request = gql(/* GraphQL */ ` } `); -const TransferOrganizationOwnership_Members = gql(/* GraphQL */ ` +const TransferOrganizationOwnership_Members = graphql(` query TransferOrganizationOwnership_Members($selector: OrganizationSelectorInput!) { organization(selector: $selector) { organization { @@ -35,7 +36,14 @@ const TransferOrganizationOwnership_Members = gql(/* GraphQL */ ` type members { nodes { + isOwner ...MemberFields + user { + id + fullName + displayName + email + } } total } @@ -46,15 +54,25 @@ const TransferOrganizationOwnership_Members = gql(/* GraphQL */ ` type Member = MemberFieldsFragment; +const TransferOrganizationOwnershipModal_OrganizationFragment = graphql(` + fragment TransferOrganizationOwnershipModal_OrganizationFragment on Organization { + cleanId + } +`); + export const TransferOrganizationOwnershipModal = ({ isOpen, toggleModalOpen, - organization, + ...props }: { isOpen: boolean; toggleModalOpen: () => void; - organization: OrganizationFieldsFragment; + organization: FragmentType<typeof TransferOrganizationOwnershipModal_OrganizationFragment>; }): ReactElement => { + const organization = useFragment( + TransferOrganizationOwnershipModal_OrganizationFragment, + props.organization, + ); const notify = useNotifications(); const [, mutate] = useMutation(TransferOrganizationOwnership_Request); const [query] = useQuery({ diff --git a/packages/web/app/src/lib/access/organization.ts b/packages/web/app/src/lib/access/organization.ts index 0f9bf6a7f..7d34afadf 100644 --- a/packages/web/app/src/lib/access/organization.ts +++ b/packages/web/app/src/lib/access/organization.ts @@ -1,12 +1,20 @@ -import { MemberFieldsFragment, OrganizationAccessScope } from '../../graphql'; +import { FragmentType, graphql, useFragment } from '@/gql'; +import { OrganizationAccessScope } from '../../graphql'; import { useRedirect } from './common'; export { OrganizationAccessScope } from '../../graphql'; +const CanAccessOrganization_MemberFragment = graphql(` + fragment CanAccessOrganization_MemberFragment on Member { + organizationAccessScopes + } +`); + export function canAccessOrganization( scope: OrganizationAccessScope, - member: Pick<MemberFieldsFragment, 'organizationAccessScopes'> | null | undefined, + mmember: null | FragmentType<typeof CanAccessOrganization_MemberFragment>, ) { + const member = useFragment(CanAccessOrganization_MemberFragment, mmember); if (!member) { return false; } @@ -16,14 +24,16 @@ export function canAccessOrganization( export function useOrganizationAccess({ scope, - member, + member: mmember, redirect = false, }: { scope: OrganizationAccessScope; - member: Pick<MemberFieldsFragment, 'organizationAccessScopes'> | null | undefined; + member: null | FragmentType<typeof CanAccessOrganization_MemberFragment>; redirect?: boolean; }) { - const canAccess = canAccessOrganization(scope, member); + const member = useFragment(CanAccessOrganization_MemberFragment, mmember); + const canAccess = canAccessOrganization(scope, mmember); + useRedirect({ canAccess, redirectTo: redirect diff --git a/packages/web/app/src/lib/access/project.ts b/packages/web/app/src/lib/access/project.ts index 73b4c9083..6ddc188e2 100644 --- a/packages/web/app/src/lib/access/project.ts +++ b/packages/web/app/src/lib/access/project.ts @@ -1,12 +1,20 @@ -import { MemberFieldsFragment, ProjectAccessScope } from '../../graphql'; +import { FragmentType, graphql, useFragment } from '@/gql'; +import { ProjectAccessScope } from '../../graphql'; import { useRedirect } from './common'; export { ProjectAccessScope } from '../../graphql'; +const CanAccessProject_MemberFragment = graphql(` + fragment CanAccessProject_MemberFragment on Member { + projectAccessScopes + } +`); + export function canAccessProject( scope: ProjectAccessScope, - member: Pick<MemberFieldsFragment, 'projectAccessScopes'> | null | undefined, + mmember: null | FragmentType<typeof CanAccessProject_MemberFragment>, ) { + const member = useFragment(CanAccessProject_MemberFragment, mmember); if (!member) { return false; } @@ -16,14 +24,16 @@ export function canAccessProject( export function useProjectAccess({ scope, - member, + member: mmember, redirect = false, }: { scope: ProjectAccessScope; - member: Pick<MemberFieldsFragment, 'projectAccessScopes'> | null | undefined; + member: null | FragmentType<typeof CanAccessProject_MemberFragment>; redirect?: boolean; }) { - const canAccess = canAccessProject(scope, member); + const member = useFragment(CanAccessProject_MemberFragment, mmember); + + const canAccess = canAccessProject(scope, mmember); useRedirect({ canAccess, redirectTo: redirect diff --git a/packages/web/app/src/lib/access/target.ts b/packages/web/app/src/lib/access/target.ts index 1d56f5087..41b2f78f1 100644 --- a/packages/web/app/src/lib/access/target.ts +++ b/packages/web/app/src/lib/access/target.ts @@ -1,12 +1,21 @@ -import { MemberFieldsFragment, TargetAccessScope } from '../../graphql'; +import { FragmentType, graphql, useFragment } from '@/gql'; +import { TargetAccessScope } from '../../graphql'; import { useRedirect } from './common'; export { TargetAccessScope } from '../../graphql'; +const CanAccessTarget_MemberFragment = graphql(` + fragment CanAccessTarget_MemberFragment on Member { + targetAccessScopes + } +`); + export function canAccessTarget( scope: TargetAccessScope, - member: Pick<MemberFieldsFragment, 'targetAccessScopes'> | null | undefined, + mmember: null | FragmentType<typeof CanAccessTarget_MemberFragment>, ) { + const member = useFragment(CanAccessTarget_MemberFragment, mmember); + if (!member) { return false; } @@ -16,14 +25,15 @@ export function canAccessTarget( export function useTargetAccess({ scope, - member, + member: mmember, redirect = false, }: { scope: TargetAccessScope; - member: Pick<MemberFieldsFragment, 'targetAccessScopes'> | null | undefined; + member: null | FragmentType<typeof CanAccessTarget_MemberFragment>; redirect?: boolean; }) { - const canAccess = canAccessTarget(scope, member); + const member = useFragment(CanAccessTarget_MemberFragment, mmember); + const canAccess = canAccessTarget(scope, mmember); useRedirect({ canAccess, redirectTo: redirect diff --git a/packages/web/app/src/lib/urql-cache.ts b/packages/web/app/src/lib/urql-cache.ts index 0d40b3868..f685c037c 100644 --- a/packages/web/app/src/lib/urql-cache.ts +++ b/packages/web/app/src/lib/urql-cache.ts @@ -8,11 +8,6 @@ import { } from '@/components/project/settings/external-composition'; import { ResultOf, VariablesOf } from '@graphql-typed-document-node/core'; import { Cache, QueryInput, UpdateResolver } from '@urql/exchange-graphcache'; -import { - InvitationDeleteButton_DeleteInvitation, - MemberInvitationForm_InviteByEmail, - Members_OrganizationMembers, -} from '../../pages/[orgId]/view/members'; import { AddAlertChannelDocument, AddAlertDocument, @@ -361,72 +356,6 @@ const deleteGitHubIntegration: TypedDocumentNodeUpdateResolver< ); }; -const inviteToOrganizationByEmail: TypedDocumentNodeUpdateResolver< - typeof MemberInvitationForm_InviteByEmail -> = ({ inviteToOrganizationByEmail }, args, cache) => { - if (inviteToOrganizationByEmail.ok) { - cache.updateQuery( - { - query: Members_OrganizationMembers, - variables: { - selector: { - organization: args.input.organization, - }, - }, - }, - data => { - if (data === null) { - return null; - } - - const invitation = inviteToOrganizationByEmail.ok; - - if (invitation) { - data.organization?.organization?.invitations.nodes.push({ - ...invitation, - __typename: 'OrganizationInvitation', - }); - } - - return data; - }, - ); - } -}; - -const deleteOrganizationInvitation: TypedDocumentNodeUpdateResolver< - typeof InvitationDeleteButton_DeleteInvitation -> = ({ deleteOrganizationInvitation }, args, cache) => { - if (deleteOrganizationInvitation.ok) { - cache.updateQuery( - { - query: Members_OrganizationMembers, - variables: { - selector: { - organization: args.input.organization, - }, - }, - }, - data => { - if (data === null) { - return null; - } - - const invitation = deleteOrganizationInvitation.ok; - - if (invitation && data.organization?.organization?.invitations.nodes) { - data.organization.organization.invitations.nodes = - data.organization.organization.invitations.nodes.filter( - node => node.id !== invitation.id, - ); - } - - return data; - }, - ); - } -}; - const enableExternalSchemaComposition: TypedDocumentNodeUpdateResolver< typeof ExternalCompositionForm_EnableMutation > = ({ enableExternalSchemaComposition }, args, cache) => { @@ -509,8 +438,6 @@ export const Mutation = { deleteAlertChannels, addAlert, deletePersistedOperation, - inviteToOrganizationByEmail, - deleteOrganizationInvitation, enableExternalSchemaComposition, disableExternalSchemaComposition, }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b30d5a2cd..63724954e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,21 +5,24 @@ overrides: tsup: 6.5.0 patchedDependencies: - '@tgriesser/schemats@7.0.1': - hash: u3kbucfchakklx3sci2vh6wjau - path: patches/@tgriesser__schemats@7.0.1.patch - slonik@30.1.2: - hash: wg2hxbo7txnklmvja4aeqnygfi - path: patches/slonik@30.1.2.patch - '@oclif/core@1.23.0': - hash: zhte2hlj7lfobytjpalcwwo474 - path: patches/@oclif__core@1.23.0.patch '@theguild/buddy@0.1.0': hash: ryylgra5xglhidfoiaxehn22hq path: patches/@theguild__buddy@0.1.0.patch + slonik@30.1.2: + hash: wg2hxbo7txnklmvja4aeqnygfi + path: patches/slonik@30.1.2.patch + '@tgriesser/schemats@7.0.1': + hash: u3kbucfchakklx3sci2vh6wjau + path: patches/@tgriesser__schemats@7.0.1.patch + '@oclif/core@1.23.0': + hash: zhte2hlj7lfobytjpalcwwo474 + path: patches/@oclif__core@1.23.0.patch bullmq@3.7.2: hash: xbmkiyyfti7h6orsfs6pdmi4s4 path: patches/bullmq@3.7.2.patch + mjml-core@4.13.0: + hash: zxxsxbqejjmcwuzpigutzzq6wa + path: patches/mjml-core@4.13.0.patch oclif@3.7.0: hash: rxmtqiusuyruv7tkwux5gvotbm path: patches/oclif@3.7.0.patch @@ -29,15 +32,12 @@ patchedDependencies: '@graphql-inspector/core@4.0.0': hash: wf35oaq7brzyeva5aoncxac66a path: patches/@graphql-inspector__core@4.0.0.patch - '@apollo/federation@0.38.1': - hash: rjgakkkphrejw6qrtph4ar24zq - path: patches/@apollo__federation@0.38.1.patch - mjml-core@4.13.0: - hash: zxxsxbqejjmcwuzpigutzzq6wa - path: patches/mjml-core@4.13.0.patch '@octokit/webhooks-methods@3.0.1': hash: ckboo4crezw7uvstxm24ozngtq path: patches/@octokit__webhooks-methods@3.0.1.patch + '@apollo/federation@0.38.1': + hash: rjgakkkphrejw6qrtph4ar24zq + path: patches/@apollo__federation@0.38.1.patch importers: @@ -48,7 +48,6 @@ importers: '@graphql-codegen/add': 4.0.1 '@graphql-codegen/cli': 3.2.1 '@graphql-codegen/client-preset': 2.1.0 - '@graphql-codegen/gql-tag-operations-preset': 2.1.0 '@graphql-codegen/graphql-modules-preset': 3.1.0 '@graphql-codegen/typed-document-node': 3.0.1 '@graphql-codegen/typescript': 3.0.1 @@ -89,7 +88,6 @@ importers: '@graphql-codegen/add': 4.0.1_graphql@16.6.0 '@graphql-codegen/cli': 3.2.1_3duxb27ep3oxt5ktrrpxolj5jm '@graphql-codegen/client-preset': 2.1.0_graphql@16.6.0 - '@graphql-codegen/gql-tag-operations-preset': 2.1.0_graphql@16.6.0 '@graphql-codegen/graphql-modules-preset': 3.1.0_graphql@16.6.0 '@graphql-codegen/typed-document-node': 3.0.1_graphql@16.6.0 '@graphql-codegen/typescript': 3.0.1_graphql@16.6.0 @@ -3075,9 +3073,9 @@ packages: '@babel/helper-annotate-as-pure': 7.18.6 '@babel/helper-environment-visitor': 7.18.9 '@babel/helper-function-name': 7.19.0 - '@babel/helper-member-expression-to-functions': 7.18.9 + '@babel/helper-member-expression-to-functions': 7.20.7 '@babel/helper-optimise-call-expression': 7.18.6 - '@babel/helper-replace-supers': 7.19.1 + '@babel/helper-replace-supers': 7.20.7 '@babel/helper-split-export-declaration': 7.18.6 transitivePeerDependencies: - supports-color @@ -3100,13 +3098,6 @@ packages: dependencies: '@babel/types': 7.20.7 - /@babel/helper-member-expression-to-functions/7.18.9: - resolution: {integrity: sha512-RxifAh2ZoVU67PyKIO4AMi1wTenGfMR/O/ae0CCRqwgBAt5v7xjdtRw7UoSbsreKrQn5t7r89eruK/9JjYHuDg==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/types': 7.20.7 - dev: true - /@babel/helper-member-expression-to-functions/7.20.7: resolution: {integrity: sha512-9J0CxJLq315fEdi4s7xK5TQaNYjZw+nDVpVqr1axNGKzdrdwYBD5b4uKv3n75aABG0rCCTK8Im8Ww7eYfMrZgw==} engines: {node: '>=6.9.0'} @@ -3147,19 +3138,6 @@ packages: resolution: {integrity: sha512-8RvlJG2mj4huQ4pZ+rU9lqKi9ZKiRmuvGuM2HlWmkmgOhbs6zEAw6IEiJ5cQqGbDzGZOhwuOQNtZMi/ENLjZoQ==} engines: {node: '>=6.9.0'} - /@babel/helper-replace-supers/7.19.1: - resolution: {integrity: sha512-T7ahH7wV0Hfs46SFh5Jz3s0B6+o8g3c+7TMxu7xKfmHikg7EAZ3I2Qk9LFhjxXq8sL7UkP5JflezNwoZa8WvWw==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/helper-environment-visitor': 7.18.9 - '@babel/helper-member-expression-to-functions': 7.20.7 - '@babel/helper-optimise-call-expression': 7.18.6 - '@babel/traverse': 7.20.13 - '@babel/types': 7.20.7 - transitivePeerDependencies: - - supports-color - dev: true - /@babel/helper-replace-supers/7.20.7: resolution: {integrity: sha512-vujDMtB6LVfNW13jhlCrp48QNslK6JXi7lQG736HVbHz/mbf4Dc7tIRh1Xf5C0rF7BP8iiSxGMCmY6Ci1ven3A==} engines: {node: '>=6.9.0'} @@ -6544,10 +6522,6 @@ packages: resolution: {integrity: sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==} dev: true - /@iarna/toml/2.2.5: - resolution: {integrity: sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg==} - dev: false - /@ioredis/as-callback/3.0.0: resolution: {integrity: sha512-Kqv1rZ3WbgOrS+hgzJ5xG5WQuhvzzSTRYvNeyPMLOAM78MHSnuKI20JeJGbpuAt//LCuP0vsexZcorqW7kWhJg==} dev: true @@ -9565,13 +9539,12 @@ packages: react: 18.2.0 react-dom: 18.2.0_react@18.2.0 transitivePeerDependencies: - - '@swc/core' - - '@swc/wasm' - '@types/node' - bufferutil + - cosmiconfig-toml-loader + - cosmiconfig-typescript-loader - encoding - graphql - - typescript - utf-8-validate dev: false @@ -12264,25 +12237,6 @@ packages: vary: 1.1.2 dev: false - /cosmiconfig-toml-loader/1.0.0: - resolution: {integrity: sha512-H/2gurFWVi7xXvCyvsWRLCMekl4tITJcX0QEsDMpzxtuxDyM59xLatYNg4s/k9AA/HdtCYfj2su8mgA0GSDLDA==} - dependencies: - '@iarna/toml': 2.2.5 - dev: false - - /cosmiconfig-typescript-loader/4.3.0_hguxidgmm6hcsx2ec4lzz7uqxu: - resolution: {integrity: sha512-NTxV1MFfZDLPiBMjxbHRwSh5LaLcPMwNdCutmnHJCKoVnlvldPWlllonKwrsRJ5pYZBIBGRWWU2tfvzxgeSW5Q==} - engines: {node: '>=12', npm: '>=6'} - peerDependencies: - '@types/node': '*' - cosmiconfig: '>=7' - ts-node: '>=10' - typescript: '>=3' - dependencies: - cosmiconfig: 7.0.1 - ts-node: 10.9.1 - dev: false - /cosmiconfig-typescript-loader/4.3.0_tynoeqfilgwodzrdv3jpzpskvq: resolution: {integrity: sha512-NTxV1MFfZDLPiBMjxbHRwSh5LaLcPMwNdCutmnHJCKoVnlvldPWlllonKwrsRJ5pYZBIBGRWWU2tfvzxgeSW5Q==} engines: {node: '>=12', npm: '>=6'} @@ -12316,7 +12270,6 @@ packages: js-yaml: 4.1.0 parse-json: 5.2.0 path-type: 4.0.0 - dev: true /cpu-features/0.0.4: resolution: {integrity: sha512-fKiZ/zp1mUwQbnzb9IghXtHtDoTMtNeb8oYGx6kX2SYfhnG0HNdBEBIzB9b5KlXu5DQPhfy3mInbBxFcgwAr3A==} @@ -15291,33 +15244,34 @@ packages: - react-is dev: false - /graphql-config/4.3.6_graphql@16.6.0: - resolution: {integrity: sha512-i7mAPwc0LAZPnYu2bI8B6yXU5820Wy/ArvmOseDLZIu0OU1UTULEuexHo6ZcHXeT9NvGGaUPQZm8NV3z79YydA==} + /graphql-config/4.4.1_graphql@16.6.0: + resolution: {integrity: sha512-B8wlvfBHZ5WnI4IiuQZRqql6s+CKz7S+xpUeTb28Z8nRBi8tH9ChEBgT5FnTyE05PUhHlrS2jK9ICJ4YBl9OtQ==} engines: {node: '>= 10.0.0'} peerDependencies: + cosmiconfig-toml-loader: ^1.0.0 + cosmiconfig-typescript-loader: ^4.0.0 graphql: ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 + peerDependenciesMeta: + cosmiconfig-toml-loader: + optional: true + cosmiconfig-typescript-loader: + optional: true dependencies: '@graphql-tools/graphql-file-loader': 7.5.5_graphql@16.6.0 '@graphql-tools/json-file-loader': 7.4.6_graphql@16.6.0 '@graphql-tools/load': 7.8.0_graphql@16.6.0 '@graphql-tools/merge': 8.3.18_graphql@16.6.0 '@graphql-tools/url-loader': 7.17.1_graphql@16.6.0 - '@graphql-tools/utils': 8.13.1_graphql@16.6.0 - cosmiconfig: 7.0.1 - cosmiconfig-toml-loader: 1.0.0 - cosmiconfig-typescript-loader: 4.3.0_hguxidgmm6hcsx2ec4lzz7uqxu + '@graphql-tools/utils': 9.2.1_graphql@16.6.0 + cosmiconfig: 8.0.0 graphql: 16.6.0 minimatch: 4.2.1 string-env-interpolation: 1.0.1 - ts-node: 10.9.1 tslib: 2.5.0 transitivePeerDependencies: - - '@swc/core' - - '@swc/wasm' - '@types/node' - bufferutil - encoding - - typescript - utf-8-validate dev: false @@ -15359,18 +15313,17 @@ packages: graphql: ^15.5.0 || ^16.0.0 dependencies: graphql: 16.6.0 - graphql-config: 4.3.6_graphql@16.6.0 + graphql-config: 4.4.1_graphql@16.6.0 graphql-language-service-parser: 1.10.4_graphql@16.6.0 graphql-language-service-types: 1.8.7_graphql@16.6.0 graphql-language-service-utils: 2.7.1_graphql@16.6.0 vscode-languageserver-types: 3.17.2 transitivePeerDependencies: - - '@swc/core' - - '@swc/wasm' - '@types/node' - bufferutil + - cosmiconfig-toml-loader + - cosmiconfig-typescript-loader - encoding - - typescript - utf-8-validate dev: false @@ -15382,12 +15335,11 @@ packages: graphql: 16.6.0 graphql-language-service-types: 1.8.7_graphql@16.6.0 transitivePeerDependencies: - - '@swc/core' - - '@swc/wasm' - '@types/node' - bufferutil + - cosmiconfig-toml-loader + - cosmiconfig-typescript-loader - encoding - - typescript - utf-8-validate dev: false @@ -15397,15 +15349,14 @@ packages: graphql: ^15.5.0 || ^16.0.0 dependencies: graphql: 16.6.0 - graphql-config: 4.3.6_graphql@16.6.0 + graphql-config: 4.4.1_graphql@16.6.0 vscode-languageserver-types: 3.17.2 transitivePeerDependencies: - - '@swc/core' - - '@swc/wasm' - '@types/node' - bufferutil + - cosmiconfig-toml-loader + - cosmiconfig-typescript-loader - encoding - - typescript - utf-8-validate dev: false @@ -15419,12 +15370,11 @@ packages: graphql-language-service-types: 1.8.7_graphql@16.6.0 nullthrows: 1.1.1 transitivePeerDependencies: - - '@swc/core' - - '@swc/wasm' - '@types/node' - bufferutil + - cosmiconfig-toml-loader + - cosmiconfig-typescript-loader - encoding - - typescript - utf-8-validate dev: false @@ -15440,12 +15390,11 @@ packages: graphql-language-service-types: 1.8.7_graphql@16.6.0 graphql-language-service-utils: 2.7.1_graphql@16.6.0 transitivePeerDependencies: - - '@swc/core' - - '@swc/wasm' - '@types/node' - bufferutil + - cosmiconfig-toml-loader + - cosmiconfig-typescript-loader - encoding - - typescript - utf-8-validate dev: false