From d0b528c202ae55e1b51565b30d53a4e56fc853a1 Mon Sep 17 00:00:00 2001 From: Chuck Driesler Date: Wed, 8 Oct 2025 11:07:28 +0100 Subject: [PATCH] feat(dashboards): query dashboards by/on project (#5704) --- .../dashboards/typedefs/dashboards.graphql | 33 +++++++++++++++---- .../modules/core/graph/generated/graphql.ts | 23 +++++++++++++ .../modules/dashboards/domain/operations.ts | 10 +++++- .../dashboards/graph/resolvers/dashboards.ts | 30 ++++++++++++++++- .../dashboards/repositories/management.ts | 28 +++++++++++++--- .../modules/dashboards/services/management.ts | 17 ++++++++-- 6 files changed, 126 insertions(+), 15 deletions(-) diff --git a/packages/server/assets/dashboards/typedefs/dashboards.graphql b/packages/server/assets/dashboards/typedefs/dashboards.graphql index ccc8812cf..4c633def7 100644 --- a/packages/server/assets/dashboards/typedefs/dashboards.graphql +++ b/packages/server/assets/dashboards/typedefs/dashboards.graphql @@ -6,6 +6,28 @@ extend type Mutation { dashboardMutations: DashboardMutations! @hasServerRole(role: SERVER_GUEST) } +type DashboardMutations { + create(workspace: WorkspaceIdentifier!, input: DashboardCreateInput!): Dashboard! + delete(id: String!): Boolean! + update(input: DashboardUpdateInput!): Dashboard! +} + +extend type Workspace { + dashboards( + limit: Int! = 50 + cursor: String + filter: WorkspaceDashboardsFilter + ): DashboardCollection! +} + +extend type Project { + dashboards( + limit: Int! = 50 + cursor: String + filter: ProjectDashboardsFilter + ): DashboardCollection! +} + type Dashboard { id: String! name: String! @@ -25,14 +47,13 @@ type DashboardCollection { totalCount: Int! } -extend type Workspace { - dashboards(limit: Int! = 50, cursor: String): DashboardCollection! +input WorkspaceDashboardsFilter { + projectIds: [String!] + search: String } -type DashboardMutations { - create(workspace: WorkspaceIdentifier!, input: DashboardCreateInput!): Dashboard! - delete(id: String!): Boolean! - update(input: DashboardUpdateInput!): Dashboard! +input ProjectDashboardsFilter { + search: String } input DashboardCreateInput { diff --git a/packages/server/modules/core/graph/generated/graphql.ts b/packages/server/modules/core/graph/generated/graphql.ts index 6a4b0afc8..7a5f44df0 100644 --- a/packages/server/modules/core/graph/generated/graphql.ts +++ b/packages/server/modules/core/graph/generated/graphql.ts @@ -2656,6 +2656,7 @@ export type Project = { commentThreads: ProjectCommentCollection; createdAt: Scalars['DateTime']['output']; dashboardTokens: DashboardTokenCollection; + dashboards: DashboardCollection; description?: Maybe; /** Public project-level configuration for embedded viewer */ embedOptions: ProjectEmbedOptions; @@ -2774,6 +2775,13 @@ export type ProjectDashboardTokensArgs = { }; +export type ProjectDashboardsArgs = { + cursor?: InputMaybe; + filter?: InputMaybe; + limit?: Scalars['Int']['input']; +}; + + export type ProjectEmbedTokensArgs = { cursor?: InputMaybe; limit?: InputMaybe; @@ -3091,6 +3099,10 @@ export type ProjectCreateInput = { visibility?: InputMaybe; }; +export type ProjectDashboardsFilter = { + search?: InputMaybe; +}; + export type ProjectEmbedOptions = { __typename?: 'ProjectEmbedOptions'; hideSpeckleBranding: Scalars['Boolean']['output']; @@ -5679,6 +5691,7 @@ export type WorkspaceAutomateFunctionsArgs = { export type WorkspaceDashboardsArgs = { cursor?: InputMaybe; + filter?: InputMaybe; limit?: Scalars['Int']['input']; }; @@ -5777,6 +5790,11 @@ export type WorkspaceCreationStateInput = { workspaceId: Scalars['ID']['input']; }; +export type WorkspaceDashboardsFilter = { + projectIds?: InputMaybe>; + search?: InputMaybe; +}; + export type WorkspaceDismissInput = { workspaceId: Scalars['ID']['input']; }; @@ -6623,6 +6641,7 @@ export type ResolversTypes = { ProjectCommentsUpdatedMessage: ResolverTypeWrapper & { comment?: Maybe }>; ProjectCommentsUpdatedMessageType: ProjectCommentsUpdatedMessageType; ProjectCreateInput: ProjectCreateInput; + ProjectDashboardsFilter: ProjectDashboardsFilter; ProjectEmbedOptions: ResolverTypeWrapper; ProjectFileImportUpdatedMessage: ResolverTypeWrapper & { upload: ResolversTypes['FileUpload'] }>; ProjectFileImportUpdatedMessageType: ProjectFileImportUpdatedMessageType; @@ -6776,6 +6795,7 @@ export type ResolversTypes = { WorkspaceCreateInput: WorkspaceCreateInput; WorkspaceCreationState: ResolverTypeWrapper; WorkspaceCreationStateInput: WorkspaceCreationStateInput; + WorkspaceDashboardsFilter: WorkspaceDashboardsFilter; WorkspaceDismissInput: WorkspaceDismissInput; WorkspaceDomain: ResolverTypeWrapper; WorkspaceDomainDeleteInput: WorkspaceDomainDeleteInput; @@ -7033,6 +7053,7 @@ export type ResolversParentTypes = { ProjectCommentsFilter: ProjectCommentsFilter; ProjectCommentsUpdatedMessage: Omit & { comment?: Maybe }; ProjectCreateInput: ProjectCreateInput; + ProjectDashboardsFilter: ProjectDashboardsFilter; ProjectEmbedOptions: ProjectEmbedOptions; ProjectFileImportUpdatedMessage: Omit & { upload: ResolversParentTypes['FileUpload'] }; ProjectInviteCreateInput: ProjectInviteCreateInput; @@ -7167,6 +7188,7 @@ export type ResolversParentTypes = { WorkspaceCreateInput: WorkspaceCreateInput; WorkspaceCreationState: WorkspaceCreationState; WorkspaceCreationStateInput: WorkspaceCreationStateInput; + WorkspaceDashboardsFilter: WorkspaceDashboardsFilter; WorkspaceDismissInput: WorkspaceDismissInput; WorkspaceDomain: WorkspaceDomain; WorkspaceDomainDeleteInput: WorkspaceDomainDeleteInput; @@ -8277,6 +8299,7 @@ export type ProjectResolvers>; createdAt?: Resolver; dashboardTokens?: Resolver>; + dashboards?: Resolver>; description?: Resolver, ParentType, ContextType>; embedOptions?: Resolver; embedTokens?: Resolver>; diff --git a/packages/server/modules/dashboards/domain/operations.ts b/packages/server/modules/dashboards/domain/operations.ts index 75d7a5035..1994a097b 100644 --- a/packages/server/modules/dashboards/domain/operations.ts +++ b/packages/server/modules/dashboards/domain/operations.ts @@ -14,9 +14,17 @@ export type UpsertDashboardRecord = >( export type ListDashboardRecords = (args: { workspaceId: string filter?: { + projectIds: string[] | null + search: string | null updatedBefore: string | null limit: number | null } }) => Promise -export type CountDashboardRecords = (args: { workspaceId: string }) => Promise +export type CountDashboardRecords = (args: { + workspaceId: string + filter?: { + projectIds: string[] | null + search: string | null + } +}) => Promise diff --git a/packages/server/modules/dashboards/graph/resolvers/dashboards.ts b/packages/server/modules/dashboards/graph/resolvers/dashboards.ts index 141c130b2..a7e941745 100644 --- a/packages/server/modules/dashboards/graph/resolvers/dashboards.ts +++ b/packages/server/modules/dashboards/graph/resolvers/dashboards.ts @@ -82,7 +82,35 @@ const resolvers: Resolvers = { workspaceId: parent.id, filter: { limit: args.limit, - cursor: args.cursor ?? null + cursor: args.cursor ?? null, + projectIds: args.filter?.projectIds ?? [], + search: args.filter?.search ?? null + } + }) + } + }, + Project: { + dashboards: async (parent, args, context) => { + const authResult = await context.authPolicies.workspace.canListDashboards({ + userId: context.userId, + workspaceId: parent.id + }) + throwIfAuthNotOk(authResult) + + if (!parent.workspaceId) { + throw new WorkspaceNotFoundError() + } + + return await getPaginatedDashboardsFactory({ + listDashboards: listDashboardsFactory({ db }), + countDashboards: countDashboardsFactory({ db }) + })({ + workspaceId: parent.workspaceId, + filter: { + limit: args.limit, + cursor: args.cursor ?? null, + projectIds: [parent.id], + search: args.filter?.search ?? null } }) } diff --git a/packages/server/modules/dashboards/repositories/management.ts b/packages/server/modules/dashboards/repositories/management.ts index f577946cf..72a1831b3 100644 --- a/packages/server/modules/dashboards/repositories/management.ts +++ b/packages/server/modules/dashboards/repositories/management.ts @@ -61,15 +61,33 @@ export const listDashboardsFactory = q.andWhere(Dashboards.col.updatedAt, '<', filter.updatedBefore) } + if (filter?.projectIds?.length) { + q.andWhereRaw('?? && ?', [Dashboards.col.projectIds, filter.projectIds]) + } + + if (filter?.search) { + q.andWhereILike(Dashboards.col.name, filter.search) + } + return await q } export const countDashboardsFactory = (deps: { db: Knex }): CountDashboardRecords => - async ({ workspaceId }) => { - const [{ count }] = await tables - .dashboards(deps.db) - .where(Dashboards.col.workspaceId, workspaceId) - .count() + async ({ workspaceId, filter = {} }) => { + const { projectIds, search } = filter + + const q = tables.dashboards(deps.db).where(Dashboards.col.workspaceId, workspaceId) + + if (projectIds?.length) { + q.andWhereRaw('?? && ?', [Dashboards.col.projectIds, projectIds]) + } + + if (search) { + q.andWhereILike(Dashboards.col.name, search) + } + + const [{ count }] = await q.count() + return Number.parseInt(count as string) } diff --git a/packages/server/modules/dashboards/services/management.ts b/packages/server/modules/dashboards/services/management.ts index 6880259f2..e2d7720f4 100644 --- a/packages/server/modules/dashboards/services/management.ts +++ b/packages/server/modules/dashboards/services/management.ts @@ -19,6 +19,7 @@ import type { StoreTokenResourceAccessDefinitions } from '@/modules/core/domain/tokens/operations' import { TokenResourceIdentifierType } from '@/modules/core/domain/tokens/types' +import { clamp } from 'lodash-es' export type CreateDashboard = (params: { name: string @@ -150,6 +151,8 @@ export type GetPaginatedDashboards = (params: { filter?: { limit: number | null cursor: string | null + projectIds: string[] | null + search: string | null } }) => Promise> @@ -160,16 +163,26 @@ export const getPaginatedDashboardsFactory = }): GetPaginatedDashboards => async ({ workspaceId, filter }) => { const cursor = filter?.cursor ? decodeIsoDateCursor(filter.cursor) : null + const projectIds = filter?.projectIds ?? [] + const search = filter?.search ?? null const [items, totalCount] = await Promise.all([ deps.listDashboards({ workspaceId, filter: { updatedBefore: cursor, - limit: filter?.limit ?? null + limit: clamp(filter?.limit ?? 50, 1, 200), + projectIds, + search } }), - deps.countDashboards({ workspaceId }) + deps.countDashboards({ + workspaceId, + filter: { + projectIds, + search + } + }) ]) const lastItem = items.at(-1)