chore(fe2): project page load speed optimization (#1974)

* merging queries for faster load

* preload plugin

* latest threads/models query optimization
This commit is contained in:
Kristaps Fabians Geikins
2024-01-18 11:00:48 +02:00
committed by GitHub
parent c2085d6b13
commit 86b535d751
20 changed files with 154 additions and 80 deletions
@@ -142,19 +142,18 @@ import {
CloudArrowDownIcon,
ChatBubbleLeftRightIcon
} from '@heroicons/vue/24/outline'
import { useQuery } from '@vue/apollo-composable'
import { Roles } from '@speckle/shared'
import type { Optional } from '@speckle/shared'
import { useActiveUser } from '~~/lib/auth/composables/activeUser'
import { useAuthManager } from '~~/lib/auth/composables/auth'
import { loginRoute } from '~~/lib/common/helpers/route'
import { useTheme, AppTheme } from '~~/lib/core/composables/theme'
import { serverVersionInfoQuery } from '~~/lib/core/graphql/queries'
import { useServerInfo } from '~/lib/core/composables/server'
const { logout } = useAuthManager()
const { activeUser, isGuest } = useActiveUser()
const { isDarkTheme, setTheme } = useTheme()
const { result } = useQuery(serverVersionInfoQuery)
const { serverInfo } = useServerInfo()
const route = useRoute()
const router = useRouter()
@@ -163,7 +162,7 @@ const showProfileEditDialog = ref(false)
const token = computed(() => route.query.token as Optional<string>)
const Icon = computed(() => (isDarkTheme.value ? SunIcon : MoonIcon))
const version = computed(() => result.value?.serverInfo.version)
const version = computed(() => serverInfo.value?.version)
const isAdmin = computed(() => activeUser.value?.role === Roles.Server.Admin)
@@ -5,6 +5,7 @@
v-if="gridOrList === GridListToggleValue.List"
:search="finalSearch"
:project="project"
:project-id="project.id"
:source-apps="sourceApps"
:contributors="contributors"
@update:loading="finalLoading = $event"
@@ -14,6 +15,7 @@
v-if="gridOrList === GridListToggleValue.Grid"
:search="finalSearch"
:project="project"
:project-id="project.id"
:source-apps="sourceApps"
:contributors="contributors"
@update:loading="finalLoading = $event"
@@ -1,8 +1,8 @@
<template>
<ProjectPageLatestItems
:count="project.commentThreadCount.totalCount"
:count="project?.commentThreadCount.totalCount || 0"
:hide-filters="showCommentsIntro"
:see-all-url="projectDiscussionsRoute(project.id)"
:see-all-url="projectDiscussionsRoute(projectId)"
title="Discussions"
>
<template #default="{ gridOrList }">
@@ -31,6 +31,7 @@ import { GridListToggleValue } from '~~/lib/layout/helpers/components'
import { useQuery } from '@vue/apollo-composable'
import { latestCommentThreadsQuery } from '~~/lib/projects/graphql/queries'
import { projectDiscussionsRoute } from '~~/lib/common/helpers/route'
import type { Optional } from '@speckle/shared'
graphql(`
fragment ProjectPageLatestItemsComments on Project {
@@ -66,14 +67,15 @@ graphql(`
`)
const props = defineProps<{
project: ProjectPageLatestItemsCommentsFragment
projectId: string
project: Optional<ProjectPageLatestItemsCommentsFragment>
}>()
const { result: latestCommentsResult } = useQuery(latestCommentThreadsQuery, () => ({
projectId: props.project?.id
projectId: props.projectId
}))
const showCommentsIntro = computed(
() => props.project.commentThreadCount.totalCount < 1
const showCommentsIntro = computed(() =>
props.project ? props.project.commentThreadCount.totalCount < 1 : false
)
</script>
@@ -1,8 +1,8 @@
<template>
<ProjectPageLatestItems
:count="project.modelCount.totalCount"
:count="project?.modelCount.totalCount || 0"
:title="title"
:see-all-url="allProjectModelsRoute(project.id)"
:see-all-url="allProjectModelsRoute(projectId)"
hide-heading-bottom-margin
>
<template #default>
@@ -12,6 +12,7 @@
v-if="gridOrList === GridListToggleValue.List"
:search="debouncedSearch"
:project="project"
:project-id="projectId"
disable-pagination
@update:loading="queryLoading = $event"
@clear-search=";(search = ''), updateSearchImmediately()"
@@ -20,6 +21,7 @@
v-if="gridOrList === GridListToggleValue.Grid"
:search="debouncedSearch"
:project="project"
:project-id="projectId"
disable-pagination
@update:loading="queryLoading = $event"
@clear-search=";(search = ''), updateSearchImmediately()"
@@ -27,7 +29,7 @@
</div>
<ProjectPageModelsNewDialog
v-model:open="showNewDialog"
:project-id="project.id"
:project-id="projectId"
/>
</template>
<template #filters>
@@ -86,7 +88,7 @@ import { debounce } from 'lodash-es'
import { PlusIcon } from '@heroicons/vue/24/solid'
import { CubeIcon } from '@heroicons/vue/24/outline'
import { allProjectModelsRoute, modelRoute } from '~~/lib/common/helpers/route'
import { SpeckleViewer } from '@speckle/shared'
import { SpeckleViewer, type Optional } from '@speckle/shared'
import { useMixpanel } from '~~/lib/core/composables/mp'
graphql(`
@@ -100,7 +102,8 @@ graphql(`
`)
const props = defineProps<{
project: ProjectPageLatestItemsModelsFragment
projectId: string
project: Optional<ProjectPageLatestItemsModelsFragment>
}>()
const mp = useMixpanel()
@@ -121,12 +124,14 @@ const title = ref('Models')
const gridOrList = useProjectPageItemViewType(title.value)
const canContribute = computed(() => canModifyModels(props.project))
const canContribute = computed(() =>
props.project ? canModifyModels(props.project) : false
)
const allModelsRoute = computed(() => {
const resourceIdString = SpeckleViewer.ViewerRoute.resourceBuilder()
.addAllModels()
.toString()
return modelRoute(props.project.id, resourceIdString)
return modelRoute(props.projectId, resourceIdString)
})
const updateDebouncedSearch = debounce(() => {
@@ -34,7 +34,7 @@
>
<ProjectCardImportFileArea
ref="importArea"
:project-id="project.id"
:project-id="projectId"
:model-name="model.name"
class="h-full w-full"
/>
@@ -69,7 +69,7 @@
rounded
size="xs"
:icon-left="ArrowPathRoundedSquareIcon"
:to="modelVersionsRoute(project.id, model.id)"
:to="modelVersionsRoute(projectId, model.id)"
:class="`transition ${
hovered ? 'inline-block opacity-100' : 'sm:hidden sm:opacity-0'
}`"
@@ -80,7 +80,7 @@
v-if="showActions && !isPendingModelFragment(model)"
v-model:open="showActionsMenu"
:model="model"
:project-id="project.id"
:project-id="projectId"
:can-edit="canEdit"
@click.stop.prevent
@upload-version="triggerVersionUpload"
@@ -103,7 +103,7 @@
class="absolute top-0 left-0 p-2"
>
<ProjectPageModelsCardAutomationStatusRefactor
:project-id="project.id"
:project-id="projectId"
:model-or-version="{
...model,
automationStatus: model.automationStatus
@@ -129,7 +129,7 @@ import { modelRoute, modelVersionsRoute } from '~~/lib/common/helpers/route'
import { graphql } from '~~/lib/common/generated/gql'
import { canModifyModels } from '~~/lib/projects/helpers/permissions'
import { isPendingModelFragment } from '~~/lib/projects/helpers/models'
import type { Nullable } from '@speckle/shared'
import type { Nullable, Optional } from '@speckle/shared'
import { keyboardClick } from '@speckle/ui-components'
graphql(`
@@ -145,8 +145,9 @@ const emit = defineEmits<{
const props = withDefaults(
defineProps<{
projectId: string
model: ProjectPageLatestItemsModelItemFragment | PendingFileUploadFragment
project: ProjectPageModelsCardProjectFragment
project: Optional<ProjectPageModelsCardProjectFragment>
showVersions?: boolean
showActions?: boolean
disableDefaultLink?: boolean
@@ -159,7 +160,8 @@ const props = withDefaults(
}
)
provide('projectId', props.project.id)
// TODO: Get rid of this, its not reactive. Is it even necessary?
provide('projectId', props.projectId)
const importArea = ref(
null as Nullable<{
@@ -206,7 +208,7 @@ const updatedAt = computed(() => {
const finalShowVersions = computed(
() => props.showVersions && !isPendingModelFragment(props.model)
)
const canEdit = computed(() => canModifyModels(props.project))
const canEdit = computed(() => (props.project ? canModifyModels(props.project) : false))
const versionCount = computed(() => {
return isPendingModelFragment(props.model) ? 0 : props.model.versionCount.totalCount
})
@@ -218,7 +220,7 @@ const pendingVersion = computed(() => {
})
const finalModelUrl = computed(() =>
defaultLinkDisabled.value ? undefined : modelRoute(props.project.id, props.model.id)
defaultLinkDisabled.value ? undefined : modelRoute(props.projectId, props.model.id)
)
const triggerVersionUpload = () => {
@@ -6,6 +6,7 @@
v-for="(item, i) in items"
:key="item.id"
:model="item"
:project-id="projectId"
:project="project"
:show-actions="showActions"
:show-versions="showVersions"
@@ -17,7 +18,7 @@
<FormButtonSecondaryViewAll
v-if="showViewAll"
class="mt-4"
:to="allProjectModelsRoute(project.id)"
:to="allProjectModelsRoute(projectId)"
/>
</template>
<template v-else-if="!areQueriesLoading">
@@ -26,7 +27,7 @@
@clear-search="() => $emit('clear-search')"
/>
<div v-else>
<ProjectCardImportFileArea :project-id="project.id" class="h-36 col-span-4" />
<ProjectCardImportFileArea :project-id="projectId" class="h-36 col-span-4" />
</div>
</template>
<InfiniteLoading
@@ -46,7 +47,7 @@ import {
latestModelsPaginationQuery,
latestModelsQuery
} from '~~/lib/projects/graphql/queries'
import type { Nullable, SourceAppDefinition } from '@speckle/shared'
import type { Nullable, Optional, SourceAppDefinition } from '@speckle/shared'
import type { InfiniteLoaderState } from '~~/lib/global/helpers/components'
import { allProjectModelsRoute } from '~~/lib/common/helpers/route'
@@ -58,7 +59,8 @@ const emit = defineEmits<{
const props = withDefaults(
defineProps<{
project: ProjectPageLatestItemsModelsFragment
projectId: string
project: Optional<ProjectPageLatestItemsModelsFragment>
search?: string
showActions?: boolean
showVersions?: boolean
@@ -80,7 +82,7 @@ const areQueriesLoading = useQueryLoading()
const latestModelsQueryVariables = computed(
(): ProjectLatestModelsPaginationQueryVariables => ({
projectId: props.project.id,
projectId: props.projectId,
filter: {
search: props.search || null,
excludeIds: props.excludedIds || null,
@@ -12,7 +12,7 @@
</div>
<FormButtonSecondaryViewAll
v-if="showViewAll"
:to="allProjectModelsRoute(project.id)"
:to="allProjectModelsRoute(projectId)"
/>
</div>
<template v-else-if="!areQueriesLoading">
@@ -25,7 +25,7 @@
@clear-search="$emit('clear-search')"
/>
<div v-else>
<ProjectCardImportFileArea :project-id="project.id" class="h-36 col-span-4" />
<ProjectCardImportFileArea :project-id="projectId" class="h-36 col-span-4" />
</div>
</template>
<InfiniteLoading
@@ -35,7 +35,7 @@
/>
<ProjectPageModelsNewDialog
v-model:open="showNewDialog"
:project-id="project.id"
:project-id="projectId"
:parent-model-name="newSubmodelParent || undefined"
/>
</template>
@@ -62,7 +62,8 @@ const emit = defineEmits<{
}>()
const props = defineProps<{
project: ProjectPageLatestItemsModelsFragment
projectId: string
project?: ProjectPageLatestItemsModelsFragment
search?: string
disablePagination?: boolean
sourceApps?: SourceAppDefinition[]
@@ -84,11 +85,10 @@ const showNewDialog = computed({
const evictModelFields = useEvictProjectModelFields()
const areQueriesLoading = useQueryLoading()
const projectId = computed(() => props.project.id)
const baseQueryVariables = computed(
(): ProjectModelsTreeTopLevelQueryVariables => ({
projectId: projectId.value,
projectId: props.projectId,
filter: {
search: props.search || null,
sourceApps: props.sourceApps?.length
@@ -148,7 +148,9 @@ const topLevelItems = computed(
props.disablePagination ? 8 : undefined
)
)
const canContribute = computed(() => canModifyModels(props.project))
const canContribute = computed(() =>
props.project ? canModifyModels(props.project) : false
)
const isUsingSearch = computed(() => !!resultVariables.value?.filter?.search)
const moreToLoad = computed(
() =>
@@ -160,7 +162,7 @@ const showViewAll = computed(() => moreToLoad.value && props.disablePagination)
const onModelUpdated = () => {
// Evict model data
evictModelFields(props.project.id)
evictModelFields(props.projectId)
// Reset pagination
infiniteLoadCacheBuster.value++
@@ -43,6 +43,7 @@
:key="pendingModel.id"
:model="pendingModel"
:project="project"
:project-id="project.id"
height="h-52"
/>
<ProjectPageModelsCard
@@ -52,6 +53,7 @@
:project="project"
:show-versions="false"
:show-actions="false"
:project-id="project.id"
height="h-52"
/>
<ProjectCardImportFileArea
@@ -20,9 +20,8 @@
</LayoutDialog>
</template>
<script setup lang="ts">
import { useQuery } from '@vue/apollo-composable'
import { CodeBracketIcon, ChevronRightIcon } from '@heroicons/vue/24/outline'
import { profileEditDialogQuery } from '~~/lib/user/graphql/queries'
import { useActiveUser } from '~/lib/auth/composables/activeUser'
type FormButtonColor =
| 'default'
@@ -42,9 +41,7 @@ const props = defineProps<{
open: boolean
}>()
const { result } = useQuery(profileEditDialogQuery)
const user = computed(() => result.value?.activeUser)
const { activeUser: user } = useActiveUser()
const isOpen = computed({
get: () => !!(props.open && user.value),
@@ -19,6 +19,7 @@
v-if="project"
:search="debouncedSearch"
:project="project"
:project-id="project.id"
:excluded-ids="alreadyLoadedModelIds"
:show-actions="false"
:show-versions="false"
@@ -8,12 +8,15 @@ export const activeUserQuery = graphql(`
activeUser {
id
email
company
bio
name
role
avatar
isOnboardingFinished
createdAt
verified
notificationPreferences
}
}
`)
@@ -1,3 +1,6 @@
import type { QueryOptions } from '@apollo/client/core'
import { convertThrowIntoFetchResult } from '~/lib/common/helpers/graphql'
export const useApolloClientIfAvailable = () => {
const nuxt = useNuxtApp()
const getClient = () => (nuxt.$apollo?.default ? nuxt.$apollo.default : undefined)
@@ -13,3 +16,15 @@ export const useApolloClientFromNuxt = () => {
return client
}
export const usePreloadApolloQueries = () => {
const client = useApolloClientFromNuxt()
return async (params: { queries: QueryOptions[] }) => {
const { queries } = params
const promises = queries.map((q) =>
client.query(q).catch(convertThrowIntoFetchResult)
)
return await Promise.all(promises)
}
}
@@ -62,7 +62,7 @@ const documents = {
"\n fragment ThreadCommentAttachment on Comment {\n text {\n attachments {\n id\n fileName\n fileType\n fileSize\n }\n }\n }\n": types.ThreadCommentAttachmentFragmentDoc,
"\n fragment ViewerCommentsListItem on Comment {\n id\n rawText\n archived\n author {\n ...LimitedUserAvatar\n }\n createdAt\n viewedAt\n replies {\n totalCount\n cursor\n items {\n ...ViewerCommentsReplyItem\n }\n }\n replyAuthors(limit: 4) {\n totalCount\n items {\n ...FormUsersSelectItem\n }\n }\n resources {\n resourceId\n resourceType\n }\n }\n": types.ViewerCommentsListItemFragmentDoc,
"\n fragment ViewerModelVersionCardItem on Version {\n id\n message\n referencedObject\n sourceApplication\n createdAt\n previewUrl\n authorUser {\n ...LimitedUserAvatar\n }\n }\n": types.ViewerModelVersionCardItemFragmentDoc,
"\n query ActiveUserMainMetadata {\n activeUser {\n id\n email\n name\n role\n avatar\n isOnboardingFinished\n createdAt\n verified\n }\n }\n": types.ActiveUserMainMetadataDocument,
"\n query ActiveUserMainMetadata {\n activeUser {\n id\n email\n company\n bio\n name\n role\n avatar\n isOnboardingFinished\n createdAt\n verified\n notificationPreferences\n }\n }\n": types.ActiveUserMainMetadataDocument,
"\n mutation CreateOnboardingProject {\n projectMutations {\n createForOnboarding {\n ...ProjectPageProject\n ...ProjectDashboardItem\n }\n }\n }\n ": types.CreateOnboardingProjectDocument,
"\n mutation FinishOnboarding {\n activeUserMutations {\n finishOnboarding\n }\n }\n": types.FinishOnboardingDocument,
"\n mutation RequestVerificationByEmail($email: String!) {\n requestVerificationByEmail(email: $email)\n }\n": types.RequestVerificationByEmailDocument,
@@ -75,7 +75,6 @@ const documents = {
"\n query ServerInfoAllScopes {\n serverInfo {\n scopes {\n name\n description\n }\n }\n }\n": types.ServerInfoAllScopesDocument,
"\n query ProjectModelsSelectorValues($projectId: String!, $cursor: String) {\n project(id: $projectId) {\n id\n models(limit: 100, cursor: $cursor) {\n cursor\n totalCount\n items {\n ...CommonModelSelectorModel\n }\n }\n }\n }\n": types.ProjectModelsSelectorValuesDocument,
"\n query MainServerInfoData {\n serverInfo {\n adminContact\n blobSizeLimitBytes\n canonicalUrl\n company\n description\n guestModeEnabled\n inviteOnly\n name\n termsOfService\n version\n automateUrl\n }\n }\n": types.MainServerInfoDataDocument,
"\n query ServerVersionInfo {\n serverInfo {\n version\n }\n }\n": types.ServerVersionInfoDocument,
"\n mutation DeleteAccessToken($token: String!) {\n apiTokenRevoke(token: $token)\n }\n": types.DeleteAccessTokenDocument,
"\n mutation CreateAccessToken($token: ApiTokenCreateInput!) {\n apiTokenCreate(token: $token)\n }\n": types.CreateAccessTokenDocument,
"\n mutation DeleteApplication($appId: String!) {\n appDelete(appId: $appId)\n }\n": types.DeleteApplicationDocument,
@@ -381,7 +380,7 @@ export function graphql(source: "\n fragment ViewerModelVersionCardItem on Vers
/**
* 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 ActiveUserMainMetadata {\n activeUser {\n id\n email\n name\n role\n avatar\n isOnboardingFinished\n createdAt\n verified\n }\n }\n"): (typeof documents)["\n query ActiveUserMainMetadata {\n activeUser {\n id\n email\n name\n role\n avatar\n isOnboardingFinished\n createdAt\n verified\n }\n }\n"];
export function graphql(source: "\n query ActiveUserMainMetadata {\n activeUser {\n id\n email\n company\n bio\n name\n role\n avatar\n isOnboardingFinished\n createdAt\n verified\n notificationPreferences\n }\n }\n"): (typeof documents)["\n query ActiveUserMainMetadata {\n activeUser {\n id\n email\n company\n bio\n name\n role\n avatar\n isOnboardingFinished\n createdAt\n verified\n notificationPreferences\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
@@ -430,10 +429,6 @@ export function graphql(source: "\n query ProjectModelsSelectorValues($projectI
* 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 MainServerInfoData {\n serverInfo {\n adminContact\n blobSizeLimitBytes\n canonicalUrl\n company\n description\n guestModeEnabled\n inviteOnly\n name\n termsOfService\n version\n automateUrl\n }\n }\n"): (typeof documents)["\n query MainServerInfoData {\n serverInfo {\n adminContact\n blobSizeLimitBytes\n canonicalUrl\n company\n description\n guestModeEnabled\n inviteOnly\n name\n termsOfService\n version\n automateUrl\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 ServerVersionInfo {\n serverInfo {\n version\n }\n }\n"): (typeof documents)["\n query ServerVersionInfo {\n serverInfo {\n version\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
@@ -3013,7 +3013,7 @@ export type ViewerModelVersionCardItemFragment = { __typename?: 'Version', id: s
export type ActiveUserMainMetadataQueryVariables = Exact<{ [key: string]: never; }>;
export type ActiveUserMainMetadataQuery = { __typename?: 'Query', activeUser?: { __typename?: 'User', id: string, email?: string | null, name: string, role?: string | null, avatar?: string | null, isOnboardingFinished?: boolean | null, createdAt?: string | null, verified?: boolean | null } | null };
export type ActiveUserMainMetadataQuery = { __typename?: 'Query', activeUser?: { __typename?: 'User', id: string, email?: string | null, company?: string | null, bio?: string | null, name: string, role?: string | null, avatar?: string | null, isOnboardingFinished?: boolean | null, createdAt?: string | null, verified?: boolean | null, notificationPreferences: {} } | null };
export type CreateOnboardingProjectMutationVariables = Exact<{ [key: string]: never; }>;
@@ -3092,11 +3092,6 @@ export type MainServerInfoDataQueryVariables = Exact<{ [key: string]: never; }>;
export type MainServerInfoDataQuery = { __typename?: 'Query', serverInfo: { __typename?: 'ServerInfo', adminContact?: string | null, blobSizeLimitBytes: number, canonicalUrl?: string | null, company?: string | null, description?: string | null, guestModeEnabled: boolean, inviteOnly?: boolean | null, name: string, termsOfService?: string | null, version?: string | null, automateUrl?: string | null } };
export type ServerVersionInfoQueryVariables = Exact<{ [key: string]: never; }>;
export type ServerVersionInfoQuery = { __typename?: 'Query', serverInfo: { __typename?: 'ServerInfo', version?: string | null } };
export type DeleteAccessTokenMutationVariables = Exact<{
token: Scalars['String'];
}>;
@@ -3754,7 +3749,7 @@ export const RegisterPanelServerInviteDocument = {"kind":"Document","definitions
export const EmailVerificationBannerStateDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"EmailVerificationBannerState"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"activeUser"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"verified"}},{"kind":"Field","name":{"kind":"Name","value":"hasPendingVerification"}}]}}]}}]} as unknown as DocumentNode<EmailVerificationBannerStateQuery, EmailVerificationBannerStateQueryVariables>;
export const RequestVerificationDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"RequestVerification"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"requestVerification"}}]}}]} as unknown as DocumentNode<RequestVerificationMutation, RequestVerificationMutationVariables>;
export const OnUserProjectsUpdateDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"subscription","name":{"kind":"Name","value":"OnUserProjectsUpdate"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"userProjectsUpdated"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"project"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ProjectDashboardItem"}}]}}]}}]}},...ProjectDashboardItemFragmentDoc.definitions,...ProjectDashboardItemNoModelsFragmentDoc.definitions,...ProjectPageModelsCardProjectFragmentDoc.definitions,...ProjectPageLatestItemsModelItemFragmentDoc.definitions,...PendingFileUploadFragmentDoc.definitions,...ProjectPageModelsCardRenameDialogFragmentDoc.definitions,...ProjectPageModelsCardDeleteDialogFragmentDoc.definitions,...ProjectPageModelsActionsFragmentDoc.definitions,...ModelCardAutomationStatus_ModelFragmentDoc.definitions,...ModelCardAutomationStatus_AutomationsStatusFragmentDoc.definitions]} as unknown as DocumentNode<OnUserProjectsUpdateSubscription, OnUserProjectsUpdateSubscriptionVariables>;
export const ActiveUserMainMetadataDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ActiveUserMainMetadata"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"activeUser"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"avatar"}},{"kind":"Field","name":{"kind":"Name","value":"isOnboardingFinished"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"verified"}}]}}]}}]} as unknown as DocumentNode<ActiveUserMainMetadataQuery, ActiveUserMainMetadataQueryVariables>;
export const ActiveUserMainMetadataDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ActiveUserMainMetadata"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"activeUser"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"company"}},{"kind":"Field","name":{"kind":"Name","value":"bio"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"avatar"}},{"kind":"Field","name":{"kind":"Name","value":"isOnboardingFinished"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"verified"}},{"kind":"Field","name":{"kind":"Name","value":"notificationPreferences"}}]}}]}}]} as unknown as DocumentNode<ActiveUserMainMetadataQuery, ActiveUserMainMetadataQueryVariables>;
export const CreateOnboardingProjectDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateOnboardingProject"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"projectMutations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createForOnboarding"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ProjectPageProject"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"ProjectDashboardItem"}}]}}]}}]}},...ProjectPageProjectFragmentDoc.definitions,...ProjectPageProjectHeaderFragmentDoc.definitions,...ProjectPageStatsBlockTeamFragmentDoc.definitions,...LimitedUserAvatarFragmentDoc.definitions,...ProjectPageTeamDialogFragmentDoc.definitions,...ProjectPageStatsBlockVersionsFragmentDoc.definitions,...ProjectPageStatsBlockModelsFragmentDoc.definitions,...ProjectPageStatsBlockCommentsFragmentDoc.definitions,...ProjectPageLatestItemsModelsFragmentDoc.definitions,...ProjectPageLatestItemsCommentsFragmentDoc.definitions,...ProjectDashboardItemFragmentDoc.definitions,...ProjectDashboardItemNoModelsFragmentDoc.definitions,...ProjectPageModelsCardProjectFragmentDoc.definitions,...ProjectPageLatestItemsModelItemFragmentDoc.definitions,...PendingFileUploadFragmentDoc.definitions,...ProjectPageModelsCardRenameDialogFragmentDoc.definitions,...ProjectPageModelsCardDeleteDialogFragmentDoc.definitions,...ProjectPageModelsActionsFragmentDoc.definitions,...ModelCardAutomationStatus_ModelFragmentDoc.definitions,...ModelCardAutomationStatus_AutomationsStatusFragmentDoc.definitions]} as unknown as DocumentNode<CreateOnboardingProjectMutation, CreateOnboardingProjectMutationVariables>;
export const FinishOnboardingDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"FinishOnboarding"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"activeUserMutations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"finishOnboarding"}}]}}]}}]} as unknown as DocumentNode<FinishOnboardingMutation, FinishOnboardingMutationVariables>;
export const RequestVerificationByEmailDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"RequestVerificationByEmail"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"email"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"requestVerificationByEmail"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"email"},"value":{"kind":"Variable","name":{"kind":"Name","value":"email"}}}]}]}}]} as unknown as DocumentNode<RequestVerificationByEmailMutation, RequestVerificationByEmailMutationVariables>;
@@ -3767,7 +3762,6 @@ export const ServerInfoBlobSizeLimitDocument = {"kind":"Document","definitions":
export const ServerInfoAllScopesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ServerInfoAllScopes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"serverInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"scopes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}}]}}]}}]}}]} as unknown as DocumentNode<ServerInfoAllScopesQuery, ServerInfoAllScopesQueryVariables>;
export const ProjectModelsSelectorValuesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ProjectModelsSelectorValues"},"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"}}}],"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":"models"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"IntValue","value":"100"}},{"kind":"Argument","name":{"kind":"Name","value":"cursor"},"value":{"kind":"Variable","name":{"kind":"Name","value":"cursor"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"cursor"}},{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"CommonModelSelectorModel"}}]}}]}}]}}]}},...CommonModelSelectorModelFragmentDoc.definitions]} as unknown as DocumentNode<ProjectModelsSelectorValuesQuery, ProjectModelsSelectorValuesQueryVariables>;
export const MainServerInfoDataDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"MainServerInfoData"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"serverInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"adminContact"}},{"kind":"Field","name":{"kind":"Name","value":"blobSizeLimitBytes"}},{"kind":"Field","name":{"kind":"Name","value":"canonicalUrl"}},{"kind":"Field","name":{"kind":"Name","value":"company"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"guestModeEnabled"}},{"kind":"Field","name":{"kind":"Name","value":"inviteOnly"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"termsOfService"}},{"kind":"Field","name":{"kind":"Name","value":"version"}},{"kind":"Field","name":{"kind":"Name","value":"automateUrl"}}]}}]}}]} as unknown as DocumentNode<MainServerInfoDataQuery, MainServerInfoDataQueryVariables>;
export const ServerVersionInfoDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ServerVersionInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"serverInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"version"}}]}}]}}]} as unknown as DocumentNode<ServerVersionInfoQuery, ServerVersionInfoQueryVariables>;
export const DeleteAccessTokenDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteAccessToken"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"token"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"apiTokenRevoke"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"token"},"value":{"kind":"Variable","name":{"kind":"Name","value":"token"}}}]}]}}]} as unknown as DocumentNode<DeleteAccessTokenMutation, DeleteAccessTokenMutationVariables>;
export const CreateAccessTokenDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateAccessToken"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"token"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ApiTokenCreateInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"apiTokenCreate"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"token"},"value":{"kind":"Variable","name":{"kind":"Name","value":"token"}}}]}]}}]} as unknown as DocumentNode<CreateAccessTokenMutation, CreateAccessTokenMutationVariables>;
export const DeleteApplicationDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteApplication"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"appId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"appDelete"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"appId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"appId"}}}]}]}}]} as unknown as DocumentNode<DeleteApplicationMutation, DeleteApplicationMutationVariables>;
@@ -3,7 +3,7 @@ import { nanoid } from 'nanoid'
import { graphql } from '~~/lib/common/generated/gql'
import type { H3Event } from 'h3'
const serverInfoQuery = graphql(`
export const mainServerInfoDataQuery = graphql(`
query MainServerInfoData {
serverInfo {
adminContact
@@ -22,7 +22,7 @@ const serverInfoQuery = graphql(`
`)
export function useServerInfo() {
const { result } = useQuery(serverInfoQuery)
const { result } = useQuery(mainServerInfoDataQuery)
const serverInfo = computed(() => result.value?.serverInfo)
@@ -381,9 +381,14 @@ function createLink(params: {
// SSR req logging link
const loggerLink = new ApolloLink((operation, forward) => {
const startTime = Date.now()
const name = operation.operationName
nuxtApp.$logger.debug(
{ operation: name },
`Apollo operation {operation} started...`
)
return forward(operation).map((result) => {
const elapsed = new Date().getTime() - startTime
const name = operation.operationName
const success = !!(result.data && !result.errors?.length)
nuxtApp.$logger.info(
@@ -1,9 +0,0 @@
import { graphql } from '~~/lib/common/generated/gql'
export const serverVersionInfoQuery = graphql(`
query ServerVersionInfo {
serverInfo {
version
}
}
`)
@@ -1,6 +1,6 @@
<template>
<div>
<div v-if="project">
<template v-if="project">
<ProjectsInviteBanner v-if="invite" :invite="invite" :show-stream-name="false" />
<!-- Heading text w/ actions -->
<ProjectPageHeader :project="project" class="mb-8" />
@@ -17,14 +17,15 @@
<ProjectPageStatsBlockComments :project="project" />
</div>
</div>
<div class="flex flex-col space-y-8 sm:space-y-14">
<!-- Latest models -->
<ProjectPageLatestItemsModels :project="project" />
<!-- Latest comments -->
<ProjectPageLatestItemsComments :project="project" />
<!-- More actions -->
<!-- <ProjectPageMoreActions /> -->
</div>
</template>
<!-- No v-if=project to ensure internal queries trigger ASAP -->
<div v-show="project" class="flex flex-col space-y-8 sm:space-y-14">
<!-- Latest models -->
<ProjectPageLatestItemsModels :project="project" :project-id="projectId" />
<!-- Latest comments -->
<ProjectPageLatestItemsComments :project="project" :project-id="projectId" />
<!-- More actions -->
<!-- <ProjectPageMoreActions /> -->
</div>
</div>
</template>
@@ -13,6 +13,14 @@ export default defineNuxtPlugin((ctx) => {
path: `${String(name)}: ${path}`
}
logger.debug(
{
routeName: name,
routePath: path
},
'{routePath} SSR render started...'
)
ctx.hook('app:rendered', () => {
const endTime = Date.now() - state.start
logger.info(
@@ -0,0 +1,48 @@
import type { Optional } from '@speckle/shared'
import { activeUserQuery } from '~/lib/auth/composables/activeUser'
import { usePreloadApolloQueries } from '~/lib/common/composables/graphql'
import { mainServerInfoDataQuery } from '~/lib/core/composables/server'
import { projectAccessCheckQuery } from '~/lib/projects/graphql/queries'
/**
* Prefetches data for specific routes to avoid the problem of serial API requests
* (e.g. in the case of multiple middlewares)
*/
export default defineNuxtPlugin(async (ctx) => {
const logger = useLogger()
const route = ctx._route
const preload = usePreloadApolloQueries()
if (!route) {
logger.info('No route obj found, skipping data preload...')
return
}
const path = route.path
const idParam = route.params.id as Optional<string>
const promises: Promise<unknown>[] = []
// Standard/global
promises.push(
preload({
queries: [{ query: activeUserQuery }, { query: mainServerInfoDataQuery }]
})
)
// Preload project data
if (idParam && path.startsWith('/projects/')) {
promises.push(
preload({
queries: [
{
query: projectAccessCheckQuery,
variables: { id: idParam },
context: { skipLoggingErrors: true }
}
]
})
)
}
await Promise.all(promises)
})