diff --git a/apps/compliance-portal/src/pages/subprocessors/SubprocessorsPage.tsx b/apps/compliance-portal/src/pages/subprocessors/SubprocessorsPage.tsx
index c599e12a9..319916fa2 100644
--- a/apps/compliance-portal/src/pages/subprocessors/SubprocessorsPage.tsx
+++ b/apps/compliance-portal/src/pages/subprocessors/SubprocessorsPage.tsx
@@ -19,16 +19,16 @@ import { graphql, usePreloadedQuery, useRefetchableFragment } from "react-relay"
import { PageHeader } from "#/components/PageHeader/PageHeader";
-import { SubprocessorCategorySection } from "./_components/SubprocessorCategorySection";
+import type { SubprocessorsPage_query$key } from "./__generated__/SubprocessorsPage_query.graphql";
+import type { SubprocessorsPageQuery } from "./__generated__/SubprocessorsPageQuery.graphql";
+import type { SubprocessorsPageRefetchQuery } from "./__generated__/SubprocessorsPageRefetchQuery.graphql";
import type { SubprocessorNode } from "./_components/SubprocessorCategorySection";
+import { SubprocessorCategorySection } from "./_components/SubprocessorCategorySection";
import { SubprocessorsEmpty } from "./_components/SubprocessorsEmpty";
import { SubprocessorsToolbar } from "./_components/SubprocessorsToolbar";
import { groupByCategory } from "./_lib/groupByCategory";
import { toQueryVariables } from "./_lib/toQueryVariables";
import { useSubprocessorFilters } from "./_lib/useSubprocessorFilters";
-import type { SubprocessorsPageQuery } from "./__generated__/SubprocessorsPageQuery.graphql";
-import type { SubprocessorsPageRefetchQuery } from "./__generated__/SubprocessorsPageRefetchQuery.graphql";
-import type { SubprocessorsPage_query$key } from "./__generated__/SubprocessorsPage_query.graphql";
export const subprocessorsPageQuery = graphql`
query SubprocessorsPageQuery($query: String, $category: SubprocessorCategory, $country: CountryCode) {
diff --git a/apps/compliance-portal/src/pages/subprocessors/SubprocessorsPageLoader.tsx b/apps/compliance-portal/src/pages/subprocessors/SubprocessorsPageLoader.tsx
index fcb9947c3..460c3cdae 100644
--- a/apps/compliance-portal/src/pages/subprocessors/SubprocessorsPageLoader.tsx
+++ b/apps/compliance-portal/src/pages/subprocessors/SubprocessorsPageLoader.tsx
@@ -15,11 +15,11 @@
import { useEffect, useRef } from "react";
import { useQueryLoader } from "react-relay";
-import { SubprocessorsPage, subprocessorsPageQuery } from "./SubprocessorsPage";
-import { SubprocessorsPageSkeleton } from "./SubprocessorsPageSkeleton";
+import type { SubprocessorsPageQuery } from "./__generated__/SubprocessorsPageQuery.graphql";
import { toQueryVariables } from "./_lib/toQueryVariables";
import { useSubprocessorFilters } from "./_lib/useSubprocessorFilters";
-import type { SubprocessorsPageQuery } from "./__generated__/SubprocessorsPageQuery.graphql";
+import { SubprocessorsPage, subprocessorsPageQuery } from "./SubprocessorsPage";
+import { SubprocessorsPageSkeleton } from "./SubprocessorsPageSkeleton";
export default function SubprocessorsPageLoader() {
const filters = useSubprocessorFilters();
diff --git a/apps/compliance-portal/src/pages/subprocessors/_components/SubprocessorsToolbar.tsx b/apps/compliance-portal/src/pages/subprocessors/_components/SubprocessorsToolbar.tsx
index 0c8ff9d23..117e72860 100644
--- a/apps/compliance-portal/src/pages/subprocessors/_components/SubprocessorsToolbar.tsx
+++ b/apps/compliance-portal/src/pages/subprocessors/_components/SubprocessorsToolbar.tsx
@@ -12,11 +12,11 @@
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
+import { TextField } from "@probo/ui/src/v2/form/TextField";
import { Select } from "@probo/ui/src/v2/Select/Select";
import { SelectItem } from "@probo/ui/src/v2/Select/SelectItem";
import { SelectPopup } from "@probo/ui/src/v2/Select/SelectPopup";
import { SelectTrigger } from "@probo/ui/src/v2/Select/SelectTrigger";
-import { TextField } from "@probo/ui/src/v2/form/TextField";
import { useMemo } from "react";
import { useTranslation } from "react-i18next";
import { graphql, useFragment } from "react-relay";
@@ -27,21 +27,14 @@ import { useSubprocessorSearch } from "../_lib/useSubprocessorSearch";
import type { SubprocessorsToolbar_query$key } from "./__generated__/SubprocessorsToolbar_query.graphql";
-// Unfiltered facet data: the distinct categories and countries actually present,
-// used to populate the filter dropdowns (aliased so it does not collide with the
-// filtered list selection on the same trust center).
+// Facet data: the distinct categories and countries actually present across the
+// trust center's published subprocessors, used to populate the filter dropdowns
+// (server-computed so the options never dead-end on an empty result).
const subprocessorsToolbarFragment = graphql`
fragment SubprocessorsToolbar_query on Query {
currentTrustCenter @required(action: THROW) {
- allSubprocessors: subprocessors(first: 250) {
- edges {
- node {
- id
- category
- countries
- }
- }
- }
+ subprocessorCategories
+ subprocessorCountries
}
}
`;
@@ -59,17 +52,15 @@ export function SubprocessorsToolbar({ queryKey }: SubprocessorsToolbarProps) {
const { category, country, setCategory, setCountry } = useSubprocessorFilters();
const [queryInput, setQueryInput] = useSubprocessorSearch();
- const nodes = data.currentTrustCenter.allSubprocessors.edges.map(edge => edge.node);
+ const { subprocessorCategories, subprocessorCountries } = data.currentTrustCenter;
const categoryOptions = useMemo(() => {
- const present = [...new Set(nodes.map(node => node.category))];
- return present.sort((a, b) => t(`categories.${a}.label`).localeCompare(t(`categories.${b}.label`)));
- }, [nodes, t]);
+ return [...subprocessorCategories].sort((a, b) => t(`categories.${a}.label`).localeCompare(t(`categories.${b}.label`)));
+ }, [subprocessorCategories, t]);
const countryOptions = useMemo(() => {
- const present = [...new Set(nodes.flatMap(node => node.countries))];
- return present.sort((a, b) => countryLabel(a).localeCompare(countryLabel(b)));
- }, [nodes, countryLabel]);
+ return [...subprocessorCountries].sort((a, b) => countryLabel(a).localeCompare(countryLabel(b)));
+ }, [subprocessorCountries, countryLabel]);
return (
diff --git a/pkg/coredata/third_party.go b/pkg/coredata/third_party.go
index e4c924932..8a333350b 100644
--- a/pkg/coredata/third_party.go
+++ b/pkg/coredata/third_party.go
@@ -780,6 +780,78 @@ WHERE
return count, nil
}
+func (v *ThirdParties) LoadDistinctTrustCenterCategoriesByOrganizationID(
+ ctx context.Context,
+ conn pg.Querier,
+ scope Scoper,
+ organizationID gid.GID,
+) ([]ThirdPartyCategory, error) {
+ q := `
+SELECT DISTINCT
+ category
+FROM
+ third_parties
+WHERE
+ %s
+ AND organization_id = @organization_id
+ AND show_on_trust_center = true
+ORDER BY
+ category ASC
+`
+ q = fmt.Sprintf(q, scope.SQLFragment())
+
+ args := pgx.NamedArgs{"organization_id": organizationID}
+ maps.Copy(args, scope.SQLArguments())
+
+ rows, err := conn.Query(ctx, q, args)
+ if err != nil {
+ return nil, fmt.Errorf("cannot query thirdParty categories: %w", err)
+ }
+
+ categories, err := pgx.CollectRows(rows, pgx.RowTo[ThirdPartyCategory])
+ if err != nil {
+ return nil, fmt.Errorf("cannot collect thirdParty categories: %w", err)
+ }
+
+ return categories, nil
+}
+
+func (v *ThirdParties) LoadDistinctTrustCenterCountriesByOrganizationID(
+ ctx context.Context,
+ conn pg.Querier,
+ scope Scoper,
+ organizationID gid.GID,
+) ([]CountryCode, error) {
+ q := `
+SELECT DISTINCT
+ unnest(countries) AS country
+FROM
+ third_parties
+WHERE
+ %s
+ AND organization_id = @organization_id
+ AND show_on_trust_center = true
+ORDER BY
+ country ASC
+`
+ q = fmt.Sprintf(q, scope.SQLFragment())
+
+ args := pgx.NamedArgs{"organization_id": organizationID}
+ maps.Copy(args, scope.SQLArguments())
+
+ rows, err := conn.Query(ctx, q, args)
+ if err != nil {
+ return nil, fmt.Errorf("cannot query thirdParty countries: %w", err)
+ }
+
+ countries, err := pgx.CollectRows(rows, pgx.RowTo[CountryCode])
+ if err != nil {
+ return nil, fmt.Errorf("cannot collect thirdParty countries: %w", err)
+ }
+
+ return countries, nil
+}
+
func (v *ThirdParties) LoadByOrganizationID(
ctx context.Context,
conn pg.Querier,
diff --git a/pkg/server/api/trust/v1/graphql/trust_center.graphql b/pkg/server/api/trust/v1/graphql/trust_center.graphql
index 749b5e2de..f3d9b4705 100644
--- a/pkg/server/api/trust/v1/graphql/trust_center.graphql
+++ b/pkg/server/api/trust/v1/graphql/trust_center.graphql
@@ -33,6 +33,11 @@ type TrustCenter implements Node {
filter: SubprocessorFilter
): SubprocessorConnection! @goField(forceResolver: true)
+ subprocessorCategories: [SubprocessorCategory!]!
+ @goField(forceResolver: true)
+
+ subprocessorCountries: [CountryCode!]! @goField(forceResolver: true)
+
references(
first: Int
after: CursorKey
diff --git a/pkg/server/api/trust/v1/trust_center_resolvers.go b/pkg/server/api/trust/v1/trust_center_resolvers.go
index 48a10f1ba..349333071 100644
--- a/pkg/server/api/trust/v1/trust_center_resolvers.go
+++ b/pkg/server/api/trust/v1/trust_center_resolvers.go
@@ -831,6 +831,34 @@ func (r *trustCenterResolver) Subprocessors(ctx context.Context, obj *types.Trus
return types.NewSubprocessorConnection(thirdPartyPage, r, obj.ID, thirdPartyFilter), nil
}
+// SubprocessorCategories is the resolver for the subprocessorCategories field.
+func (r *trustCenterResolver) SubprocessorCategories(ctx context.Context, obj *types.TrustCenter) ([]coredata.ThirdPartyCategory, error) {
+ compliancePage := compliancepage.CompliancePageFromContext(ctx)
+ scope := coredata.NewScopeFromObjectID(compliancePage.OrganizationID)
+
+ categories, err := r.trust.ThirdParties.ListDistinctTrustCenterCategoriesForOrganizationID(ctx, scope, obj.Organization.ID)
+ if err != nil {
+ r.logger.ErrorCtx(ctx, "cannot list subprocessor categories", log.Error(err))
+ return nil, gqlutils.Internal(ctx)
+ }
+
+ return categories, nil
+}
+
+// SubprocessorCountries is the resolver for the subprocessorCountries field.
+func (r *trustCenterResolver) SubprocessorCountries(ctx context.Context, obj *types.TrustCenter) ([]coredata.CountryCode, error) {
+ compliancePage := compliancepage.CompliancePageFromContext(ctx)
+ scope := coredata.NewScopeFromObjectID(compliancePage.OrganizationID)
+
+ countries, err := r.trust.ThirdParties.ListDistinctTrustCenterCountriesForOrganizationID(ctx, scope, obj.Organization.ID)
+ if err != nil {
+ r.logger.ErrorCtx(ctx, "cannot list subprocessor countries", log.Error(err))
+ return nil, gqlutils.Internal(ctx)
+ }
+
+ return countries, nil
+}
+
// References is the resolver for the references field.
func (r *trustCenterResolver) References(ctx context.Context, obj *types.TrustCenter, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.TrustCenterReferenceConnection, error) {
compliancePage := compliancepage.CompliancePageFromContext(ctx)
diff --git a/pkg/trust/third_party_service.go b/pkg/trust/third_party_service.go
index e0b175fa5..016eeb3b0 100644
--- a/pkg/trust/third_party_service.go
+++ b/pkg/trust/third_party_service.go
@@ -80,6 +80,64 @@ func (s ThirdPartyService) ListForOrganizationId(
return page.NewPage(thirdParties, cursor), nil
}
+func (s ThirdPartyService) ListDistinctTrustCenterCategoriesForOrganizationID(
+ ctx context.Context,
+ scope coredata.Scoper,
+ organizationID gid.GID,
+) ([]coredata.ThirdPartyCategory, error) {
+ var categories []coredata.ThirdPartyCategory
+
+ err := s.svc.pg.WithConn(
+ ctx,
+ func(ctx context.Context, conn pg.Querier) error {
+ thirdParties := &coredata.ThirdParties{}
+
+ result, err := thirdParties.LoadDistinctTrustCenterCategoriesByOrganizationID(ctx, conn, scope, organizationID)
+ if err != nil {
+ return fmt.Errorf("cannot load thirdParty categories: %w", err)
+ }
+
+ categories = result
+
+ return nil
+ },
+ )
+ if err != nil {
+ return nil, err
+ }
+
+ return categories, nil
+}
+
+func (s ThirdPartyService) ListDistinctTrustCenterCountriesForOrganizationID(
+ ctx context.Context,
+ scope coredata.Scoper,
+ organizationID gid.GID,
+) ([]coredata.CountryCode, error) {
+ var countries []coredata.CountryCode
+
+ err := s.svc.pg.WithConn(
+ ctx,
+ func(ctx context.Context, conn pg.Querier) error {
+ thirdParties := &coredata.ThirdParties{}
+
+ result, err := thirdParties.LoadDistinctTrustCenterCountriesByOrganizationID(ctx, conn, scope, organizationID)
+ if err != nil {
+ return fmt.Errorf("cannot load thirdParty countries: %w", err)
+ }
+
+ countries = result
+
+ return nil
+ },
+ )
+ if err != nil {
+ return nil, err
+ }
+
+ return countries, nil
+}
+
func (s ThirdPartyService) CountForTrustCenterId(
ctx context.Context,
scope coredata.Scoper,