From c3f13d4e66ca99896c5c17f8c5eceba309a164ca Mon Sep 17 00:00:00 2001 From: Kristaps Fabians Geikins Date: Mon, 22 Jan 2024 11:08:53 +0200 Subject: [PATCH] fix: multiple FE2 and server speed improvements, mainly focusing on the project page (#1975) * introduced app cache & optimized /downloads * added redis cache storage * optimizing latest thread retrieval * more dataloaders * undid debug stuff * deployment changes * minor change to reqTouched * connectorTag parallel resolution * added redis key prefix * gqlgen cleanup * Amend network policy to allow egress to Redis --------- Co-authored-by: Iain Sproat <68657+iainsproat@users.noreply.github.com> --- docker-compose-speckle.yml | 1 + packages/frontend-2/.env.example | 4 +- .../frontend-2/components/connectors/Card.vue | 4 +- .../project/page/latest-items/Comments.vue | 1 - .../page/latest-items/comments/GridItem.vue | 6 +- .../page/latest-items/comments/ListItem.vue | 6 +- .../project/page/models/CardView.vue | 35 ++-- packages/frontend-2/composables/cache.ts | 28 +++ .../lib/common/generated/gql/gql.ts | 9 +- .../lib/common/generated/gql/graphql.ts | 21 +- .../frontend-2/lib/common/helpers/route.ts | 3 + .../frontend-2/lib/viewer/helpers/comments.ts | 10 +- packages/frontend-2/middleware/thread.ts | 50 +++++ packages/frontend-2/nuxt.config.ts | 7 +- packages/frontend-2/package.json | 1 + packages/frontend-2/pages/downloads.vue | 134 +++++++------ .../projects/[id]/threads/[threadId].vue | 13 ++ .../frontend-2/plugins/004-redis.server.ts | 15 ++ packages/frontend-2/plugins/cache.ts | 179 ++++++++++++++++++ .../comments/graph/resolvers/comments.js | 11 +- .../modules/core/graph/resolvers/models.ts | 35 +++- packages/shared/src/observability/index.ts | 2 +- .../template-docker-compose.yml | 1 + .../templates/frontend_2/deployment.yml | 5 + .../frontend_2/networkpolicy.cilium.yml | 2 + .../frontend_2/networkpolicy.kubernetes.yml | 2 + yarn.lock | 1 + 27 files changed, 488 insertions(+), 98 deletions(-) create mode 100644 packages/frontend-2/composables/cache.ts create mode 100644 packages/frontend-2/middleware/thread.ts create mode 100644 packages/frontend-2/pages/projects/[id]/threads/[threadId].vue create mode 100644 packages/frontend-2/plugins/004-redis.server.ts create mode 100644 packages/frontend-2/plugins/cache.ts diff --git a/docker-compose-speckle.yml b/docker-compose-speckle.yml index e49a08de4..a115c90aa 100644 --- a/docker-compose-speckle.yml +++ b/docker-compose-speckle.yml @@ -24,6 +24,7 @@ services: NUXT_PUBLIC_SERVER_NAME: 'local' NUXT_PUBLIC_API_ORIGIN: 'http://127.0.0.1' NUXT_PUBLIC_BACKEND_API_ORIGIN: 'http://speckle-server:3000' + NUXT_REDIS_URL: 'redis://redis' speckle-server: build: diff --git a/packages/frontend-2/.env.example b/packages/frontend-2/.env.example index a9da14203..fd0efdcfb 100644 --- a/packages/frontend-2/.env.example +++ b/packages/frontend-2/.env.example @@ -9,4 +9,6 @@ NUXT_PUBLIC_API_ORIGIN=http://127.0.0.1:3000 NUXT_PUBLIC_MIXPANEL_TOKEN_ID=acd87c5a50b56df91a795e999812a3a4 NUXT_PUBLIC_MIXPANEL_API_HOST=https://analytics.speckle.systems -NUXT_PUBLIC_SERVER_NAME=local \ No newline at end of file +NUXT_PUBLIC_SERVER_NAME=local + +NUXT_REDIS_URL=redis://localhost:6379 diff --git a/packages/frontend-2/components/connectors/Card.vue b/packages/frontend-2/components/connectors/Card.vue index 0f3654fc8..0c9ac2afb 100644 --- a/packages/frontend-2/components/connectors/Card.vue +++ b/packages/frontend-2/components/connectors/Card.vue @@ -48,7 +48,9 @@ Downloads - Tutorials + + Tutorials + diff --git a/packages/frontend-2/components/project/page/latest-items/Comments.vue b/packages/frontend-2/components/project/page/latest-items/Comments.vue index 057bfc9ef..c9232750e 100644 --- a/packages/frontend-2/components/project/page/latest-items/Comments.vue +++ b/packages/frontend-2/components/project/page/latest-items/Comments.vue @@ -62,7 +62,6 @@ graphql(` ...FormUsersSelectItem } } - ...LinkableComment } `) diff --git a/packages/frontend-2/components/project/page/latest-items/comments/GridItem.vue b/packages/frontend-2/components/project/page/latest-items/comments/GridItem.vue index c02ca21fd..48c578008 100644 --- a/packages/frontend-2/components/project/page/latest-items/comments/GridItem.vue +++ b/packages/frontend-2/components/project/page/latest-items/comments/GridItem.vue @@ -52,7 +52,7 @@ import dayjs from 'dayjs' import type { ProjectPageLatestItemsCommentItemFragment } from '~~/lib/common/generated/gql/graphql' import { useCommentScreenshotImage } from '~~/lib/projects/composables/previewImage' import { times } from 'lodash-es' -import { getLinkToThread } from '~~/lib/viewer/helpers/comments' +import { getLightLinkToThread } from '~~/lib/viewer/helpers/comments' import { CheckCircleIcon } from '@heroicons/vue/24/solid' import type { AvatarUserWithId } from '@speckle/ui-components' @@ -90,5 +90,7 @@ const allAvatars = computed((): AvatarUserWithId[] => [ ) ]) -const threadLink = computed(() => getLinkToThread(props.projectId, props.thread)) +const threadLink = computed(() => + getLightLinkToThread(props.projectId, props.thread.id) +) diff --git a/packages/frontend-2/components/project/page/latest-items/comments/ListItem.vue b/packages/frontend-2/components/project/page/latest-items/comments/ListItem.vue index fa7446f43..ea91d5814 100644 --- a/packages/frontend-2/components/project/page/latest-items/comments/ListItem.vue +++ b/packages/frontend-2/components/project/page/latest-items/comments/ListItem.vue @@ -44,7 +44,7 @@ import dayjs from 'dayjs' import { times } from 'lodash-es' import type { ProjectPageLatestItemsCommentItemFragment } from '~~/lib/common/generated/gql/graphql' import { useCommentScreenshotImage } from '~~/lib/projects/composables/previewImage' -import { getLinkToThread } from '~~/lib/viewer/helpers/comments' +import { getLightLinkToThread } from '~~/lib/viewer/helpers/comments' import { CheckCircleIcon } from '@heroicons/vue/24/solid' import type { AvatarUserWithId } from '@speckle/ui-components' @@ -81,5 +81,7 @@ const allAvatars = computed((): AvatarUserWithId[] => [ ) ]) -const threadLink = computed(() => getLinkToThread(props.projectId, props.thread)) +const threadLink = computed(() => + getLightLinkToThread(props.projectId, props.thread.id) +) diff --git a/packages/frontend-2/components/project/page/models/CardView.vue b/packages/frontend-2/components/project/page/models/CardView.vue index 46588d09b..93e878a71 100644 --- a/packages/frontend-2/components/project/page/models/CardView.vue +++ b/packages/frontend-2/components/project/page/models/CardView.vue @@ -81,20 +81,31 @@ const logger = useLogger() const areQueriesLoading = useQueryLoading() const latestModelsQueryVariables = computed( - (): ProjectLatestModelsPaginationQueryVariables => ({ - projectId: props.projectId, - filter: { - search: props.search || null, - excludeIds: props.excludedIds || null, - onlyWithVersions: !!props.excludeEmptyModels, - sourceApps: props.sourceApps?.length - ? props.sourceApps.map((a) => a.searchKey) - : null, - contributors: props.contributors?.length - ? props.contributors.map((c) => c.id) + (): ProjectLatestModelsPaginationQueryVariables => { + const shouldHaveFilter = + props.search?.length || + props.excludedIds?.length || + props.sourceApps?.length || + props.contributors?.length || + !!props.excludeEmptyModels + + return { + projectId: props.projectId, + filter: shouldHaveFilter + ? { + search: props.search || null, + excludeIds: props.excludedIds || null, + onlyWithVersions: !!props.excludeEmptyModels, + sourceApps: props.sourceApps?.length + ? props.sourceApps.map((a) => a.searchKey) + : null, + contributors: props.contributors?.length + ? props.contributors.map((c) => c.id) + : null + } : null } - }) + } ) const infiniteLoaderId = ref('') diff --git a/packages/frontend-2/composables/cache.ts b/packages/frontend-2/composables/cache.ts new file mode 100644 index 000000000..293da96ba --- /dev/null +++ b/packages/frontend-2/composables/cache.ts @@ -0,0 +1,28 @@ +import type { MaybeAsync } from '@speckle/shared' + +/** + * In SSR: Provides a redis cache that is shared across app processes and requests + * In CSR: Provides an in-memory cache that is shared across the app session + */ +export function useAppCache() { + const app = useNuxtApp() + return app.$appCache +} + +/** + * Get value from app cache or resolve and set it + */ +export async function useAppCached( + key: string, + resolver: () => MaybeAsync, + options?: Parameters['set']>['2'] +): Promise { + const cache = useAppCache() + if (await cache.has(key)) { + return (await cache.get(key)) as V + } + + const data = await Promise.resolve(resolver()) + await cache.set(key, data, options) + return data +} diff --git a/packages/frontend-2/lib/common/generated/gql/gql.ts b/packages/frontend-2/lib/common/generated/gql/gql.ts index d047b0255..563b948fa 100644 --- a/packages/frontend-2/lib/common/generated/gql/gql.ts +++ b/packages/frontend-2/lib/common/generated/gql/gql.ts @@ -35,7 +35,7 @@ const documents = { "\n fragment ProjectModelsPageResults_Project on Project {\n ...ProjectPageLatestItemsModels\n }\n": types.ProjectModelsPageResults_ProjectFragmentDoc, "\n fragment ProjectPageProjectHeader on Project {\n id\n role\n name\n description\n visibility\n allowPublicComments\n }\n": types.ProjectPageProjectHeaderFragmentDoc, "\n fragment ProjectPageLatestItemsComments on Project {\n id\n commentThreadCount: commentThreads(limit: 0) {\n totalCount\n }\n }\n": types.ProjectPageLatestItemsCommentsFragmentDoc, - "\n fragment ProjectPageLatestItemsCommentItem on Comment {\n id\n author {\n ...FormUsersSelectItem\n }\n screenshot\n rawText\n createdAt\n updatedAt\n archived\n repliesCount: replies(limit: 0) {\n totalCount\n }\n replyAuthors(limit: 4) {\n totalCount\n items {\n ...FormUsersSelectItem\n }\n }\n ...LinkableComment\n }\n": types.ProjectPageLatestItemsCommentItemFragmentDoc, + "\n fragment ProjectPageLatestItemsCommentItem on Comment {\n id\n author {\n ...FormUsersSelectItem\n }\n screenshot\n rawText\n createdAt\n updatedAt\n archived\n repliesCount: replies(limit: 0) {\n totalCount\n }\n replyAuthors(limit: 4) {\n totalCount\n items {\n ...FormUsersSelectItem\n }\n }\n }\n": types.ProjectPageLatestItemsCommentItemFragmentDoc, "\n fragment ProjectPageLatestItemsModels on Project {\n id\n role\n modelCount: models(limit: 0) {\n totalCount\n }\n }\n": types.ProjectPageLatestItemsModelsFragmentDoc, "\n fragment ProjectPageModelsActions on Model {\n id\n name\n }\n": types.ProjectPageModelsActionsFragmentDoc, "\n fragment ProjectPageModelsCardProject on Project {\n id\n role\n }\n": types.ProjectPageModelsCardProjectFragmentDoc, @@ -163,6 +163,7 @@ const documents = { "\n subscription OnViewerUserActivityBroadcasted(\n $target: ViewerUpdateTrackingTarget!\n $sessionId: String!\n ) {\n viewerUserActivityBroadcasted(target: $target, sessionId: $sessionId) {\n userName\n userId\n user {\n ...LimitedUserAvatar\n }\n state\n status\n sessionId\n }\n }\n": types.OnViewerUserActivityBroadcastedDocument, "\n subscription OnViewerCommentsUpdated($target: ViewerUpdateTrackingTarget!) {\n projectCommentsUpdated(target: $target) {\n id\n type\n comment {\n id\n parent {\n id\n }\n ...ViewerCommentThread\n }\n }\n }\n": types.OnViewerCommentsUpdatedDocument, "\n fragment LinkableComment on Comment {\n id\n viewerResources {\n modelId\n versionId\n objectId\n }\n }\n": types.LinkableCommentFragmentDoc, + "\n query ResolveCommentLink($commentId: String!, $projectId: String!) {\n comment(id: $commentId, streamId: $projectId) {\n ...LinkableComment\n }\n }\n": types.ResolveCommentLinkDocument, "\n fragment ProjectPageProject on Project {\n id\n createdAt\n ...ProjectPageProjectHeader\n ...ProjectPageStatsBlockTeam\n ...ProjectPageTeamDialog\n ...ProjectPageStatsBlockVersions\n ...ProjectPageStatsBlockModels\n ...ProjectPageStatsBlockComments\n ...ProjectPageLatestItemsModels\n ...ProjectPageLatestItemsComments\n }\n": types.ProjectPageProjectFragmentDoc, "\n fragment ModelPageProject on Project {\n id\n createdAt\n name\n }\n": types.ModelPageProjectFragmentDoc, }; @@ -272,7 +273,7 @@ export function graphql(source: "\n fragment ProjectPageLatestItemsComments on /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ -export function graphql(source: "\n fragment ProjectPageLatestItemsCommentItem on Comment {\n id\n author {\n ...FormUsersSelectItem\n }\n screenshot\n rawText\n createdAt\n updatedAt\n archived\n repliesCount: replies(limit: 0) {\n totalCount\n }\n replyAuthors(limit: 4) {\n totalCount\n items {\n ...FormUsersSelectItem\n }\n }\n ...LinkableComment\n }\n"): (typeof documents)["\n fragment ProjectPageLatestItemsCommentItem on Comment {\n id\n author {\n ...FormUsersSelectItem\n }\n screenshot\n rawText\n createdAt\n updatedAt\n archived\n repliesCount: replies(limit: 0) {\n totalCount\n }\n replyAuthors(limit: 4) {\n totalCount\n items {\n ...FormUsersSelectItem\n }\n }\n ...LinkableComment\n }\n"]; +export function graphql(source: "\n fragment ProjectPageLatestItemsCommentItem on Comment {\n id\n author {\n ...FormUsersSelectItem\n }\n screenshot\n rawText\n createdAt\n updatedAt\n archived\n repliesCount: replies(limit: 0) {\n totalCount\n }\n replyAuthors(limit: 4) {\n totalCount\n items {\n ...FormUsersSelectItem\n }\n }\n }\n"): (typeof documents)["\n fragment ProjectPageLatestItemsCommentItem on Comment {\n id\n author {\n ...FormUsersSelectItem\n }\n screenshot\n rawText\n createdAt\n updatedAt\n archived\n repliesCount: replies(limit: 0) {\n totalCount\n }\n replyAuthors(limit: 4) {\n totalCount\n items {\n ...FormUsersSelectItem\n }\n }\n }\n"]; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ @@ -781,6 +782,10 @@ export function graphql(source: "\n subscription OnViewerCommentsUpdated($targe * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ export function graphql(source: "\n fragment LinkableComment on Comment {\n id\n viewerResources {\n modelId\n versionId\n objectId\n }\n }\n"): (typeof documents)["\n fragment LinkableComment on Comment {\n id\n viewerResources {\n modelId\n versionId\n objectId\n }\n }\n"]; +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql(source: "\n query ResolveCommentLink($commentId: String!, $projectId: String!) {\n comment(id: $commentId, streamId: $projectId) {\n ...LinkableComment\n }\n }\n"): (typeof documents)["\n query ResolveCommentLink($commentId: String!, $projectId: String!) {\n comment(id: $commentId, streamId: $projectId) {\n ...LinkableComment\n }\n }\n"]; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ diff --git a/packages/frontend-2/lib/common/generated/gql/graphql.ts b/packages/frontend-2/lib/common/generated/gql/graphql.ts index d14f614a0..ccf8172c2 100644 --- a/packages/frontend-2/lib/common/generated/gql/graphql.ts +++ b/packages/frontend-2/lib/common/generated/gql/graphql.ts @@ -2953,7 +2953,7 @@ export type ProjectPageProjectHeaderFragment = { __typename?: 'Project', id: str export type ProjectPageLatestItemsCommentsFragment = { __typename?: 'Project', id: string, commentThreadCount: { __typename?: 'ProjectCommentCollection', totalCount: number } }; -export type ProjectPageLatestItemsCommentItemFragment = { __typename?: 'Comment', id: string, screenshot?: string | null, rawText: string, createdAt: string, updatedAt: string, archived: boolean, author: { __typename?: 'LimitedUser', id: string, name: string, avatar?: string | null }, repliesCount: { __typename?: 'CommentCollection', totalCount: number }, replyAuthors: { __typename?: 'CommentReplyAuthorCollection', totalCount: number, items: Array<{ __typename?: 'LimitedUser', id: string, name: string, avatar?: string | null }> }, viewerResources: Array<{ __typename?: 'ViewerResourceItem', modelId?: string | null, versionId?: string | null, objectId: string }> }; +export type ProjectPageLatestItemsCommentItemFragment = { __typename?: 'Comment', id: string, screenshot?: string | null, rawText: string, createdAt: string, updatedAt: string, archived: boolean, author: { __typename?: 'LimitedUser', id: string, name: string, avatar?: string | null }, repliesCount: { __typename?: 'CommentCollection', totalCount: number }, replyAuthors: { __typename?: 'CommentReplyAuthorCollection', totalCount: number, items: Array<{ __typename?: 'LimitedUser', id: string, name: string, avatar?: string | null }> } }; export type ProjectPageLatestItemsModelsFragment = { __typename?: 'Project', id: string, role?: string | null, modelCount: { __typename?: 'ModelCollection', totalCount: number } }; @@ -3348,7 +3348,7 @@ export type ProjectLatestCommentThreadsQueryVariables = Exact<{ }>; -export type ProjectLatestCommentThreadsQuery = { __typename?: 'Query', project: { __typename?: 'Project', id: string, commentThreads: { __typename?: 'ProjectCommentCollection', totalCount: number, cursor?: string | null, items: Array<{ __typename?: 'Comment', id: string, screenshot?: string | null, rawText: string, createdAt: string, updatedAt: string, archived: boolean, author: { __typename?: 'LimitedUser', id: string, name: string, avatar?: string | null }, repliesCount: { __typename?: 'CommentCollection', totalCount: number }, replyAuthors: { __typename?: 'CommentReplyAuthorCollection', totalCount: number, items: Array<{ __typename?: 'LimitedUser', id: string, name: string, avatar?: string | null }> }, viewerResources: Array<{ __typename?: 'ViewerResourceItem', modelId?: string | null, versionId?: string | null, objectId: string }> }> } } }; +export type ProjectLatestCommentThreadsQuery = { __typename?: 'Query', project: { __typename?: 'Project', id: string, commentThreads: { __typename?: 'ProjectCommentCollection', totalCount: number, cursor?: string | null, items: Array<{ __typename?: 'Comment', id: string, screenshot?: string | null, rawText: string, createdAt: string, updatedAt: string, archived: boolean, author: { __typename?: 'LimitedUser', id: string, name: string, avatar?: string | null }, repliesCount: { __typename?: 'CommentCollection', totalCount: number }, replyAuthors: { __typename?: 'CommentReplyAuthorCollection', totalCount: number, items: Array<{ __typename?: 'LimitedUser', id: string, name: string, avatar?: string | null }> } }> } } }; export type ProjectInviteQueryVariables = Exact<{ projectId: Scalars['String']; @@ -3682,6 +3682,14 @@ export type OnViewerCommentsUpdatedSubscription = { __typename?: 'Subscription', export type LinkableCommentFragment = { __typename?: 'Comment', id: string, viewerResources: Array<{ __typename?: 'ViewerResourceItem', modelId?: string | null, versionId?: string | null, objectId: string }> }; +export type ResolveCommentLinkQueryVariables = Exact<{ + commentId: Scalars['String']; + projectId: Scalars['String']; +}>; + + +export type ResolveCommentLinkQuery = { __typename?: 'Query', comment?: { __typename?: 'Comment', id: string, viewerResources: Array<{ __typename?: 'ViewerResourceItem', modelId?: string | null, versionId?: string | null, objectId: string }> } | null }; + export type ProjectPageProjectFragment = { __typename?: 'Project', id: string, createdAt: string, role?: string | null, name: string, description?: string | null, visibility: ProjectVisibility, allowPublicComments: boolean, team: Array<{ __typename?: 'ProjectCollaborator', role: string, user: { __typename?: 'LimitedUser', role?: string | null, id: string, name: string, avatar?: string | null } }>, invitedTeam?: Array<{ __typename?: 'PendingStreamCollaborator', id: string, title: string, inviteId: string, role: string, user?: { __typename?: 'LimitedUser', role?: string | null, id: string, name: string, avatar?: string | null } | null }> | null, versionCount: { __typename?: 'VersionCollection', totalCount: number }, modelCount: { __typename?: 'ModelCollection', totalCount: number }, commentThreadCount: { __typename?: 'ProjectCommentCollection', totalCount: number } }; export type ModelPageProjectFragment = { __typename?: 'Project', id: string, createdAt: string, name: string }; @@ -3709,8 +3717,7 @@ export const FormUsersSelectItemFragmentDoc = {"kind":"Document","definitions":[ export const ProjectModelsPageHeader_ProjectFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ProjectModelsPageHeader_Project"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Project"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"sourceApps"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"team"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"FormUsersSelectItem"}}]}}]}}]}}]} as unknown as DocumentNode; export const ProjectPageLatestItemsModelsFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ProjectPageLatestItemsModels"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Project"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","alias":{"kind":"Name","value":"modelCount"},"name":{"kind":"Name","value":"models"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"IntValue","value":"0"}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}}]}}]}}]} as unknown as DocumentNode; export const ProjectModelsPageResults_ProjectFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ProjectModelsPageResults_Project"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Project"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ProjectPageLatestItemsModels"}}]}}]} as unknown as DocumentNode; -export const LinkableCommentFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"LinkableComment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Comment"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"viewerResources"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"modelId"}},{"kind":"Field","name":{"kind":"Name","value":"versionId"}},{"kind":"Field","name":{"kind":"Name","value":"objectId"}}]}}]}}]} as unknown as DocumentNode; -export const ProjectPageLatestItemsCommentItemFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ProjectPageLatestItemsCommentItem"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Comment"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"author"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"FormUsersSelectItem"}}]}},{"kind":"Field","name":{"kind":"Name","value":"screenshot"}},{"kind":"Field","name":{"kind":"Name","value":"rawText"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"archived"}},{"kind":"Field","alias":{"kind":"Name","value":"repliesCount"},"name":{"kind":"Name","value":"replies"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"IntValue","value":"0"}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}}]}},{"kind":"Field","name":{"kind":"Name","value":"replyAuthors"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"IntValue","value":"4"}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"FormUsersSelectItem"}}]}}]}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"LinkableComment"}}]}}]} as unknown as DocumentNode; +export const ProjectPageLatestItemsCommentItemFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ProjectPageLatestItemsCommentItem"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Comment"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"author"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"FormUsersSelectItem"}}]}},{"kind":"Field","name":{"kind":"Name","value":"screenshot"}},{"kind":"Field","name":{"kind":"Name","value":"rawText"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"archived"}},{"kind":"Field","alias":{"kind":"Name","value":"repliesCount"},"name":{"kind":"Name","value":"replies"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"IntValue","value":"0"}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}}]}},{"kind":"Field","name":{"kind":"Name","value":"replyAuthors"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"IntValue","value":"4"}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"FormUsersSelectItem"}}]}}]}}]}}]} as unknown as DocumentNode; export const ModelPreviewFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ModelPreview"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Model"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"previewUrl"}}]}}]} as unknown as DocumentNode; export const ProjectPageModelsCardRenameDialogFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ProjectPageModelsCardRenameDialog"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Model"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}}]}}]} as unknown as DocumentNode; export const ProjectPageModelsCardDeleteDialogFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ProjectPageModelsCardDeleteDialog"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Model"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]} as unknown as DocumentNode; @@ -3737,6 +3744,7 @@ export const ViewerCommentsReplyItemFragmentDoc = {"kind":"Document","definition export const ViewerCommentsListItemFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ViewerCommentsListItem"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Comment"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"rawText"}},{"kind":"Field","name":{"kind":"Name","value":"archived"}},{"kind":"Field","name":{"kind":"Name","value":"author"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"LimitedUserAvatar"}}]}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"viewedAt"}},{"kind":"Field","name":{"kind":"Name","value":"replies"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"cursor"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ViewerCommentsReplyItem"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"replyAuthors"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"IntValue","value":"4"}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"FormUsersSelectItem"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"resources"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"resourceId"}},{"kind":"Field","name":{"kind":"Name","value":"resourceType"}}]}}]}}]} as unknown as DocumentNode; export const ViewerCommentBubblesDataFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ViewerCommentBubblesData"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Comment"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"viewedAt"}},{"kind":"Field","name":{"kind":"Name","value":"viewerState"}}]}}]} as unknown as DocumentNode; export const ViewerCommentThreadFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ViewerCommentThread"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Comment"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ViewerCommentsListItem"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"ViewerCommentBubblesData"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"ViewerCommentsReplyItem"}}]}}]} as unknown as DocumentNode; +export const LinkableCommentFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"LinkableComment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Comment"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"viewerResources"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"modelId"}},{"kind":"Field","name":{"kind":"Name","value":"versionId"}},{"kind":"Field","name":{"kind":"Name","value":"objectId"}}]}}]}}]} as unknown as DocumentNode; export const ProjectPageTeamDialogFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ProjectPageTeamDialog"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Project"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"allowPublicComments"}},{"kind":"Field","name":{"kind":"Name","value":"visibility"}},{"kind":"Field","name":{"kind":"Name","value":"team"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"LimitedUserAvatar"}},{"kind":"Field","name":{"kind":"Name","value":"role"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"invitedTeam"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"inviteId"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"LimitedUserAvatar"}},{"kind":"Field","name":{"kind":"Name","value":"role"}}]}}]}}]}}]} as unknown as DocumentNode; export const ProjectPageStatsBlockTeamFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ProjectPageStatsBlockTeam"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Project"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"team"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"LimitedUserAvatar"}}]}}]}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"ProjectPageTeamDialog"}}]}}]} as unknown as DocumentNode; export const ProjectPageStatsBlockVersionsFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ProjectPageStatsBlockVersions"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Project"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","alias":{"kind":"Name","value":"versionCount"},"name":{"kind":"Name","value":"versions"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"IntValue","value":"0"}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}}]}}]}}]} as unknown as DocumentNode; @@ -3795,7 +3803,7 @@ export const ProjectLatestModelsPaginationDocument = {"kind":"Document","definit export const ProjectModelsTreeTopLevelDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ProjectModelsTreeTopLevel"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filter"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"ProjectModelsTreeFilter"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"project"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"modelsTree"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"cursor"},"value":{"kind":"NullValue"}},{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"IntValue","value":"8"}},{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filter"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"cursor"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SingleLevelModelTreeItem"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"pendingImportedModels"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"PendingFileUpload"}}]}}]}}]}},...SingleLevelModelTreeItemFragmentDoc.definitions,...ProjectPageLatestItemsModelItemFragmentDoc.definitions,...PendingFileUploadFragmentDoc.definitions,...ProjectPageModelsCardRenameDialogFragmentDoc.definitions,...ProjectPageModelsCardDeleteDialogFragmentDoc.definitions,...ProjectPageModelsActionsFragmentDoc.definitions,...ModelCardAutomationStatus_ModelFragmentDoc.definitions,...ModelCardAutomationStatus_AutomationsStatusFragmentDoc.definitions]} as unknown as DocumentNode; export const ProjectModelsTreeTopLevelPaginationDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ProjectModelsTreeTopLevelPagination"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filter"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"ProjectModelsTreeFilter"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"cursor"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}},"defaultValue":{"kind":"NullValue"}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"project"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"modelsTree"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"cursor"},"value":{"kind":"Variable","name":{"kind":"Name","value":"cursor"}}},{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"IntValue","value":"8"}},{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filter"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"cursor"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SingleLevelModelTreeItem"}}]}}]}}]}}]}},...SingleLevelModelTreeItemFragmentDoc.definitions,...ProjectPageLatestItemsModelItemFragmentDoc.definitions,...PendingFileUploadFragmentDoc.definitions,...ProjectPageModelsCardRenameDialogFragmentDoc.definitions,...ProjectPageModelsCardDeleteDialogFragmentDoc.definitions,...ProjectPageModelsActionsFragmentDoc.definitions,...ModelCardAutomationStatus_ModelFragmentDoc.definitions,...ModelCardAutomationStatus_AutomationsStatusFragmentDoc.definitions]} as unknown as DocumentNode; export const ProjectModelChildrenTreeDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ProjectModelChildrenTree"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"parentName"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"project"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"modelChildrenTree"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"fullName"},"value":{"kind":"Variable","name":{"kind":"Name","value":"parentName"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SingleLevelModelTreeItem"}}]}}]}}]}},...SingleLevelModelTreeItemFragmentDoc.definitions,...ProjectPageLatestItemsModelItemFragmentDoc.definitions,...PendingFileUploadFragmentDoc.definitions,...ProjectPageModelsCardRenameDialogFragmentDoc.definitions,...ProjectPageModelsCardDeleteDialogFragmentDoc.definitions,...ProjectPageModelsActionsFragmentDoc.definitions,...ModelCardAutomationStatus_ModelFragmentDoc.definitions,...ModelCardAutomationStatus_AutomationsStatusFragmentDoc.definitions]} as unknown as DocumentNode; -export const ProjectLatestCommentThreadsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ProjectLatestCommentThreads"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"cursor"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}},"defaultValue":{"kind":"NullValue"}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filter"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"ProjectCommentsFilter"}},"defaultValue":{"kind":"NullValue"}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"project"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"commentThreads"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"cursor"},"value":{"kind":"Variable","name":{"kind":"Name","value":"cursor"}}},{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"IntValue","value":"8"}},{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filter"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"cursor"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ProjectPageLatestItemsCommentItem"}}]}}]}}]}}]}},...ProjectPageLatestItemsCommentItemFragmentDoc.definitions,...FormUsersSelectItemFragmentDoc.definitions,...LinkableCommentFragmentDoc.definitions]} as unknown as DocumentNode; +export const ProjectLatestCommentThreadsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ProjectLatestCommentThreads"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"cursor"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}},"defaultValue":{"kind":"NullValue"}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filter"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"ProjectCommentsFilter"}},"defaultValue":{"kind":"NullValue"}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"project"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"commentThreads"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"cursor"},"value":{"kind":"Variable","name":{"kind":"Name","value":"cursor"}}},{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"IntValue","value":"8"}},{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filter"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"cursor"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ProjectPageLatestItemsCommentItem"}}]}}]}}]}}]}},...ProjectPageLatestItemsCommentItemFragmentDoc.definitions,...FormUsersSelectItemFragmentDoc.definitions]} as unknown as DocumentNode; export const ProjectInviteDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ProjectInvite"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"token"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"projectInvite"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"projectId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}}},{"kind":"Argument","name":{"kind":"Name","value":"token"},"value":{"kind":"Variable","name":{"kind":"Name","value":"token"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ProjectsInviteBanner"}}]}}]}},...ProjectsInviteBannerFragmentDoc.definitions,...LimitedUserAvatarFragmentDoc.definitions]} as unknown as DocumentNode; export const ProjectModelCheckDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ProjectModelCheck"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"modelId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"project"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"model"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"modelId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]}}]} as unknown as DocumentNode; export const ProjectModelPageDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ProjectModelPage"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"modelId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"versionsCursor"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"project"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ProjectModelPageHeaderProject"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"ProjectModelPageVersionsProject"}}]}}]}},...ProjectModelPageHeaderProjectFragmentDoc.definitions,...ProjectModelPageVersionsProjectFragmentDoc.definitions,...ProjectPageProjectHeaderFragmentDoc.definitions,...PendingFileUploadFragmentDoc.definitions,...ProjectModelPageVersionsPaginationFragmentDoc.definitions,...ProjectModelPageVersionsCardVersionFragmentDoc.definitions,...LimitedUserAvatarFragmentDoc.definitions,...ProjectModelPageDialogDeleteVersionFragmentDoc.definitions,...ProjectModelPageDialogMoveToVersionFragmentDoc.definitions,...ModelCardAutomationStatus_VersionFragmentDoc.definitions,...ModelCardAutomationStatus_AutomationsStatusFragmentDoc.definitions]} as unknown as DocumentNode; @@ -3837,4 +3845,5 @@ export const ViewerModelVersionsDocument = {"kind":"Document","definitions":[{"k export const ViewerDiffVersionsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ViewerDiffVersions"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"modelId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"versionAId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"versionBId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"project"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"model"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"modelId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","alias":{"kind":"Name","value":"versionA"},"name":{"kind":"Name","value":"version"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"versionAId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ViewerModelVersionCardItem"}}]}},{"kind":"Field","alias":{"kind":"Name","value":"versionB"},"name":{"kind":"Name","value":"version"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"versionBId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ViewerModelVersionCardItem"}}]}}]}}]}}]}},...ViewerModelVersionCardItemFragmentDoc.definitions,...LimitedUserAvatarFragmentDoc.definitions]} as unknown as DocumentNode; export const ViewerLoadedThreadsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ViewerLoadedThreads"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filter"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ProjectCommentsFilter"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"cursor"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"limit"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}},"defaultValue":{"kind":"IntValue","value":"25"}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"project"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"commentThreads"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filter"}}},{"kind":"Argument","name":{"kind":"Name","value":"cursor"},"value":{"kind":"Variable","name":{"kind":"Name","value":"cursor"}}},{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"Variable","name":{"kind":"Name","value":"limit"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"totalArchivedCount"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ViewerCommentThread"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"LinkableComment"}}]}}]}}]}}]}},...ViewerCommentThreadFragmentDoc.definitions,...ViewerCommentsListItemFragmentDoc.definitions,...LimitedUserAvatarFragmentDoc.definitions,...ViewerCommentsReplyItemFragmentDoc.definitions,...ThreadCommentAttachmentFragmentDoc.definitions,...FormUsersSelectItemFragmentDoc.definitions,...ViewerCommentBubblesDataFragmentDoc.definitions,...LinkableCommentFragmentDoc.definitions]} as unknown as DocumentNode; export const OnViewerUserActivityBroadcastedDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"subscription","name":{"kind":"Name","value":"OnViewerUserActivityBroadcasted"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"target"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ViewerUpdateTrackingTarget"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"sessionId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"viewerUserActivityBroadcasted"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"target"},"value":{"kind":"Variable","name":{"kind":"Name","value":"target"}}},{"kind":"Argument","name":{"kind":"Name","value":"sessionId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"sessionId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"userName"}},{"kind":"Field","name":{"kind":"Name","value":"userId"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"LimitedUserAvatar"}}]}},{"kind":"Field","name":{"kind":"Name","value":"state"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"sessionId"}}]}}]}},...LimitedUserAvatarFragmentDoc.definitions]} as unknown as DocumentNode; -export const OnViewerCommentsUpdatedDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"subscription","name":{"kind":"Name","value":"OnViewerCommentsUpdated"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"target"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ViewerUpdateTrackingTarget"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"projectCommentsUpdated"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"target"},"value":{"kind":"Variable","name":{"kind":"Name","value":"target"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"comment"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"parent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"ViewerCommentThread"}}]}}]}}]}},...ViewerCommentThreadFragmentDoc.definitions,...ViewerCommentsListItemFragmentDoc.definitions,...LimitedUserAvatarFragmentDoc.definitions,...ViewerCommentsReplyItemFragmentDoc.definitions,...ThreadCommentAttachmentFragmentDoc.definitions,...FormUsersSelectItemFragmentDoc.definitions,...ViewerCommentBubblesDataFragmentDoc.definitions]} as unknown as DocumentNode; \ No newline at end of file +export const OnViewerCommentsUpdatedDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"subscription","name":{"kind":"Name","value":"OnViewerCommentsUpdated"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"target"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ViewerUpdateTrackingTarget"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"projectCommentsUpdated"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"target"},"value":{"kind":"Variable","name":{"kind":"Name","value":"target"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"comment"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"parent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"ViewerCommentThread"}}]}}]}}]}},...ViewerCommentThreadFragmentDoc.definitions,...ViewerCommentsListItemFragmentDoc.definitions,...LimitedUserAvatarFragmentDoc.definitions,...ViewerCommentsReplyItemFragmentDoc.definitions,...ThreadCommentAttachmentFragmentDoc.definitions,...FormUsersSelectItemFragmentDoc.definitions,...ViewerCommentBubblesDataFragmentDoc.definitions]} as unknown as DocumentNode; +export const ResolveCommentLinkDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ResolveCommentLink"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"commentId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"comment"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"commentId"}}},{"kind":"Argument","name":{"kind":"Name","value":"streamId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"LinkableComment"}}]}}]}},...LinkableCommentFragmentDoc.definitions]} as unknown as DocumentNode; \ No newline at end of file diff --git a/packages/frontend-2/lib/common/helpers/route.ts b/packages/frontend-2/lib/common/helpers/route.ts index 8eadedeba..5a18175ec 100644 --- a/packages/frontend-2/lib/common/helpers/route.ts +++ b/packages/frontend-2/lib/common/helpers/route.ts @@ -30,6 +30,9 @@ export const projectWebhooksRoute = (projectId: string) => export const automationDataPageRoute = (baseUrl: string, automationId: string) => new URL(`/automations/${automationId}`, baseUrl).toString() +export const threadRedirectRoute = (projectId: string, threadId: string) => + `/projects/${projectId}/threads/${threadId}` + /** * TODO: Page doesn't exist */ diff --git a/packages/frontend-2/lib/viewer/helpers/comments.ts b/packages/frontend-2/lib/viewer/helpers/comments.ts index eedec1e51..fd88be6c8 100644 --- a/packages/frontend-2/lib/viewer/helpers/comments.ts +++ b/packages/frontend-2/lib/viewer/helpers/comments.ts @@ -5,7 +5,7 @@ import type { CommentContentInput, LinkableCommentFragment } from '~~/lib/common/generated/gql/graphql' -import { modelRoute } from '~~/lib/common/helpers/route' +import { modelRoute, threadRedirectRoute } from '~~/lib/common/helpers/route' import type { CommentEditorValue } from '~~/lib/viewer/composables/commentManagement' import { ViewerHashStateKeys } from '~~/lib/viewer/composables/setup/urlHashState' @@ -41,6 +41,14 @@ graphql(` } `) +/** + * Resolving the actual full link requires viewerResources which are pretty heavy to fetch. + * This link defers viewerResources resolution to when the link is actually clicked + */ +export function getLightLinkToThread(projectId: string, threadId: string) { + return threadRedirectRoute(projectId, threadId) +} + export function getLinkToThread(projectId: string, thread: LinkableCommentFragment) { if (!thread.viewerResources.length) return undefined const sortedResources = sortBy(thread.viewerResources, (r) => { diff --git a/packages/frontend-2/middleware/thread.ts b/packages/frontend-2/middleware/thread.ts new file mode 100644 index 000000000..bf027c250 --- /dev/null +++ b/packages/frontend-2/middleware/thread.ts @@ -0,0 +1,50 @@ +import { useApolloClientFromNuxt } from '~/lib/common/composables/graphql' +import { graphql } from '~/lib/common/generated/gql' +import { convertThrowIntoFetchResult } from '~/lib/common/helpers/graphql' +import { getLinkToThread } from '~/lib/viewer/helpers/comments' + +const resolveLinkQuery = graphql(` + query ResolveCommentLink($commentId: String!, $projectId: String!) { + comment(id: $commentId, streamId: $projectId) { + ...LinkableComment + } + } +`) + +export default defineNuxtRouteMiddleware(async (to) => { + const client = useApolloClientFromNuxt() + const threadId = to.params.threadId as string + const projectId = to.params.id as string + + const res = await client + .query({ + query: resolveLinkQuery, + variables: { + commentId: threadId, + projectId + } + }) + .catch(convertThrowIntoFetchResult) + + const comment = res.data?.comment + if (!comment) { + return abortNavigation( + createError({ + message: 'Comment thread not found', + statusCode: 404 + }) + ) + } + + const link = getLinkToThread(projectId, comment) + if (!link) { + return abortNavigation( + createError({ + message: 'Comment thread not found', + statusCode: 404 + }) + ) + } + + return navigateTo(link) +}) diff --git a/packages/frontend-2/nuxt.config.ts b/packages/frontend-2/nuxt.config.ts index d0d7f30c4..1d9b2088f 100644 --- a/packages/frontend-2/nuxt.config.ts +++ b/packages/frontend-2/nuxt.config.ts @@ -39,6 +39,7 @@ export default defineNuxtConfig({ '@artmizu/nuxt-prometheus' ], runtimeConfig: { + redisUrl: '', public: { apiOrigin: 'UNDEFINED', backendApiOrigin: '', @@ -105,8 +106,10 @@ export default defineNuxtConfig({ }, // older chrome version for CEF 65 support. all identifiers except the chrome one are default ones. target: ['es2020', 'edge88', 'firefox78', 'chrome65', 'safari14'] - // optionally disable minification for debugging - // minify: false + // // optionally disable minification for debugging + // minify: false, + // // optionally enable sourcemaps for debugging + // sourcemap: 'inline' }, plugins: [ // again - only for CEF 65 diff --git a/packages/frontend-2/package.json b/packages/frontend-2/package.json index fffd7dc81..73c9f0375 100644 --- a/packages/frontend-2/package.json +++ b/packages/frontend-2/package.json @@ -50,6 +50,7 @@ "apollo-upload-client": "^17.0.0", "dayjs": "^1.11.7", "graphql": "^16.6.0", + "ioredis": "^5.3.2", "js-cookie": "^3.0.1", "lodash-es": "^4.17.21", "mitt": "^3.0.0", diff --git a/packages/frontend-2/pages/downloads.vue b/packages/frontend-2/pages/downloads.vue index e301a336f..2d4111afd 100644 --- a/packages/frontend-2/pages/downloads.vue +++ b/packages/frontend-2/pages/downloads.vue @@ -54,7 +54,6 @@ diff --git a/packages/frontend-2/plugins/004-redis.server.ts b/packages/frontend-2/plugins/004-redis.server.ts new file mode 100644 index 000000000..2b4a37838 --- /dev/null +++ b/packages/frontend-2/plugins/004-redis.server.ts @@ -0,0 +1,15 @@ +import { Redis } from 'ioredis' + +/** + * Provide redis (only in SSR) + */ +export default defineNuxtPlugin(() => { + const { redisUrl } = useRuntimeConfig() + const redis = redisUrl?.length ? new Redis(redisUrl) : undefined + + return { + provide: { + redis + } + } +}) diff --git a/packages/frontend-2/plugins/cache.ts b/packages/frontend-2/plugins/cache.ts new file mode 100644 index 000000000..53b16bd06 --- /dev/null +++ b/packages/frontend-2/plugins/cache.ts @@ -0,0 +1,179 @@ +/* eslint-disable @typescript-eslint/require-await */ +import type { Optional } from '@speckle/shared' +import { has as objectHas } from 'lodash-es' +import type { Redis } from 'ioredis' + +type AsyncCacheInterface = { + has(key: string): Promise + get(key: string): Promise + set(key: string, val: V, options?: { expiryMs: number }): Promise + setMultiple( + keyVals: Record, + options?: { expiryMs: number } + ): Promise + getMultiple(keys: string[]): Promise> +} + +let internalCache: Optional = undefined + +const getOrInitInternalCache = async (params: { redis: Optional }) => { + if (internalCache) return internalCache + + if (params.redis) { + const client = params.redis + const redisKeyPrefix = 'fe2-app-cache:' + const finalKey = (key: string) => redisKeyPrefix + key + + internalCache = { + has: async (key) => { + const exists = await client.exists(finalKey(key)) + return !!exists + }, + set: async (key, val, options) => { + if (options?.expiryMs) { + await client.set(finalKey(key), JSON.stringify(val), 'PX', options.expiryMs) + } else { + await client.set(finalKey(key), JSON.stringify(val)) + } + }, + get: async (key: string) => { + const val = await client.get(finalKey(key)) + if (!val) return undefined + + return JSON.parse(val) as V + }, + setMultiple: async (keyVals, options) => { + const entries = Object.entries(keyVals).map(([key, val]) => [ + finalKey(key), + JSON.stringify(val) + ]) + + if (options?.expiryMs) { + await client.mset(...entries.flat(), 'PX', options.expiryMs) + } else { + await client.mset(...entries.flat()) + } + }, + getMultiple: async (keys) => { + if (!keys?.length) return {} + + const finalKeys = keys.map(finalKey) + const vals = await client.mget(...finalKeys) + const keyVals = {} as Record + for (let i = 0; i < keys.length; i++) { + const key = keys[i] + const val = vals[i] + if (!val) continue + + keyVals[key] = JSON.parse(val) + } + + return keyVals + } + } + } else { + const cache: Record = {} + + internalCache = { + has: async (key) => objectHas(cache, key), + set: async (key, val, options) => { + cache[key] = val + + if (options?.expiryMs) { + setTimeout(() => { + delete cache[key] + }, options.expiryMs) + } + }, + get: async (key: string) => { + if (!objectHas(cache, key)) return undefined + + const val = cache[key] as V + return val + }, + setMultiple: async (keyVals, options) => { + Object.assign(cache, keyVals) + + if (options?.expiryMs) { + setTimeout(() => { + for (const key of Object.keys(keyVals)) { + delete cache[key] + } + }, options.expiryMs) + } + }, + getMultiple: async (keys) => { + const keyVals = {} as Record + for (const key of keys) { + if (!objectHas(cache, key)) continue + + keyVals[key] = cache[key] + } + + return keyVals + } + } + } + + return internalCache +} + +/** + * In SSR: Provides a redis cache that is shared across app processes and requests + * In CSR: Provides an in-memory cache that is shared across the app session + */ +export default defineNuxtPlugin(async (nuxtApp) => { + const internalCache = await getOrInitInternalCache({ + redis: nuxtApp.$redis as Redis + }) + const reqTouched: Record = {} + + if (process.server) { + nuxtApp.hook('app:rendered', async () => { + const touchedKeys = Object.keys(reqTouched) + const cacheToSend = await internalCache.getMultiple(touchedKeys) + + nuxtApp.ssrContext!.payload.appCache = cacheToSend + }) + } else if (process.client) { + const restorable = window.__NUXT__?.appCache as Optional> + if (restorable) { + await internalCache.setMultiple(restorable) + } + } + + const finalCache: AsyncCacheInterface = { + has: async (key) => { + const has = await internalCache.has(key) + return has + }, + set: async (key, val, options) => { + await internalCache.set(key, val, options) + reqTouched[key] = true + }, + get: async (key: string) => { + const val = await internalCache.get(key) + reqTouched[key] = true + return val + }, + setMultiple: async (keyVals, options) => { + await internalCache.setMultiple(keyVals, options) + for (const key of Object.keys(keyVals)) { + reqTouched[key] = true + } + }, + getMultiple: async (keys) => { + const keyVals = await internalCache.getMultiple(keys) + for (const key of keys) { + reqTouched[key] = true + } + return keyVals + } + } + + return { + provide: { + appCache: finalCache + } + } +}) diff --git a/packages/server/modules/comments/graph/resolvers/comments.js b/packages/server/modules/comments/graph/resolvers/comments.js index 739642099..469df106d 100644 --- a/packages/server/modules/comments/graph/resolvers/comments.js +++ b/packages/server/modules/comments/graph/resolvers/comments.js @@ -86,7 +86,16 @@ module.exports = { } }, Comment: { - async replies(parent, args) { + async replies(parent, args, ctx) { + // If limit=0, short-cut full execution and use data loader + if (args.limit === 0) { + return { + totalCount: await ctx.loaders.comments.getReplyCount.load(parent.id), + items: [], + cursor: null + } + } + const resources = [{ resourceId: parent.id, resourceType: 'comment' }] return await getComments({ resources, diff --git a/packages/server/modules/core/graph/resolvers/models.ts b/packages/server/modules/core/graph/resolvers/models.ts index 03d9738b5..d229ca2ed 100644 --- a/packages/server/modules/core/graph/resolvers/models.ts +++ b/packages/server/modules/core/graph/resolvers/models.ts @@ -28,7 +28,16 @@ import { CommitNotFoundError } from '@/modules/core/errors/commit' export = { Project: { - async models(parent, args) { + async models(parent, args, ctx) { + // If limit=0 & no filter, short-cut full execution and use data loader + if (args.limit === 0 && !args.filter) { + return { + totalCount: await ctx.loaders.streams.getBranchCount.load(parent.id), + items: [], + cursor: null + } + } + return await getPaginatedProjectModels(parent.id, args) }, async model(_parent, args, ctx) { @@ -58,7 +67,18 @@ export = { loadedVersionsOnly }) }, - async versions(parent, args) { + async versions(parent, args, ctx) { + // If limit=0, short-cut full execution and use data loader + if (args.limit === 0) { + return { + totalCount: await ctx.loaders.streams.getCommitCountWithoutGlobals.load( + parent.id + ), + items: [], + cursor: null + } + } + return await getPaginatedStreamCommits(parent.id, args) } }, @@ -83,7 +103,16 @@ export = { async displayName(parent) { return last(parent.name.split('/')) }, - async versions(parent, args) { + async versions(parent, args, ctx) { + // If limit=0 & no filter, short-cut full execution and use data loader + if (!args.filter && args.limit === 0) { + return { + totalCount: await ctx.loaders.branches.getCommitCount.load(parent.id), + items: [], + cursor: null + } + } + return await getPaginatedBranchCommits({ branchId: parent.id, cursor: args.cursor, diff --git a/packages/shared/src/observability/index.ts b/packages/shared/src/observability/index.ts index 93ca112fd..914f44207 100644 --- a/packages/shared/src/observability/index.ts +++ b/packages/shared/src/observability/index.ts @@ -37,7 +37,7 @@ export function getLogger( if (pretty) { pinoOptions.transport = { - target: '../pinoPrettyTransport.js', + target: '@speckle/shared/pinoPrettyTransport.js', options: { colorize: true, destination: 2, //stderr diff --git a/utils/1click_image_scripts/template-docker-compose.yml b/utils/1click_image_scripts/template-docker-compose.yml index 4ab95219b..754e2700e 100644 --- a/utils/1click_image_scripts/template-docker-compose.yml +++ b/utils/1click_image_scripts/template-docker-compose.yml @@ -45,6 +45,7 @@ services: NUXT_PUBLIC_SERVER_NAME: 'TODO: change' # e.g. 'my-speckle-server' NUXT_PUBLIC_API_ORIGIN: 'TODO: change' # e.g. 'http://127.0.0.1' NUXT_PUBLIC_BACKEND_API_ORIGIN: 'http://speckle-server:3000' + NUXT_REDIS_URL: 'redis://redis' speckle-server: image: speckle/speckle-server:2 diff --git a/utils/helm/speckle-server/templates/frontend_2/deployment.yml b/utils/helm/speckle-server/templates/frontend_2/deployment.yml index ac8b46126..321fdb57c 100644 --- a/utils/helm/speckle-server/templates/frontend_2/deployment.yml +++ b/utils/helm/speckle-server/templates/frontend_2/deployment.yml @@ -79,6 +79,11 @@ spec: value: {{ .Values.frontend_2.logClientApiEndpoint }} - name: NODE_TLS_REJECT_UNAUTHORIZED value: {{ .Values.tlsRejectUnauthorized | quote }} + - name: NUXT_REDIS_URL + valueFrom: + secretKeyRef: + name: {{ default .Values.secretName .Values.redis.connectionString.secretName }} + key: {{ default "redis_url" .Values.redis.connectionString.secretKey }} priorityClassName: high-priority {{- if .Values.frontend_2.affinity }} diff --git a/utils/helm/speckle-server/templates/frontend_2/networkpolicy.cilium.yml b/utils/helm/speckle-server/templates/frontend_2/networkpolicy.cilium.yml index d3895dd2b..e187a5bf0 100644 --- a/utils/helm/speckle-server/templates/frontend_2/networkpolicy.cilium.yml +++ b/utils/helm/speckle-server/templates/frontend_2/networkpolicy.cilium.yml @@ -43,4 +43,6 @@ spec: - ports: - port: {{ printf "%s" ( include "server.port" $ | quote ) }} {{- end -}} + # redis +{{ include "speckle.networkpolicy.egress.redis.cilium" $ | indent 4 }} {{- end -}} diff --git a/utils/helm/speckle-server/templates/frontend_2/networkpolicy.kubernetes.yml b/utils/helm/speckle-server/templates/frontend_2/networkpolicy.kubernetes.yml index f43a82874..0d46180ea 100644 --- a/utils/helm/speckle-server/templates/frontend_2/networkpolicy.kubernetes.yml +++ b/utils/helm/speckle-server/templates/frontend_2/networkpolicy.kubernetes.yml @@ -42,4 +42,6 @@ spec: ports: - port: 3000 {{- end -}} + # redis +{{ include "speckle.networkpolicy.egress.redis" $ | indent 4 }} {{- end -}} diff --git a/yarn.lock b/yarn.lock index 86e33601e..57bcfb3b5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13626,6 +13626,7 @@ __metadata: eslint-plugin-vue: ^9.18.1 eslint-plugin-vuejs-accessibility: ^1.2.0 graphql: ^16.6.0 + ioredis: ^5.3.2 jest: 27 js-cookie: ^3.0.1 lodash-es: ^4.17.21