diff --git a/client/scripts/update_api_spec.js b/client/scripts/update_api_spec.js index 0676c0f19c..5dfe64c3a9 100644 --- a/client/scripts/update_api_spec.js +++ b/client/scripts/update_api_spec.js @@ -24,7 +24,8 @@ import { parseDocument } from "yaml"; const GH_BASE_URL = "https://raw.githubusercontent.com"; const DATA_SERVICES_REPO = "SwissDataScienceCenter/renku-data-services"; -const DATA_SERVICES_RELEASE = "main"; +const DATA_SERVICES_RELEASE = + "build/pagination-for-get-data-connector-project-links"; async function main() { argv.forEach((arg) => { diff --git a/client/src/features/dataConnectorsV2/api/data-connectors.api.ts b/client/src/features/dataConnectorsV2/api/data-connectors.api.ts index d44d6d1f20..1f70d3c202 100644 --- a/client/src/features/dataConnectorsV2/api/data-connectors.api.ts +++ b/client/src/features/dataConnectorsV2/api/data-connectors.api.ts @@ -31,6 +31,15 @@ const injectedRtkApi = api.injectEndpoints({ body: queryArg.globalDataConnectorPost, }), }), + getDataConnectorsSearch: build.query< + GetDataConnectorsSearchApiResponse, + GetDataConnectorsSearchApiArg + >({ + query: (queryArg) => ({ + url: `/data_connectors/search`, + params: { doi: queryArg.doi }, + }), + }), getDataConnectorsByDataConnectorId: build.query< GetDataConnectorsByDataConnectorIdApiResponse, GetDataConnectorsByDataConnectorIdApiArg @@ -97,6 +106,7 @@ const injectedRtkApi = api.injectEndpoints({ >({ query: (queryArg) => ({ url: `/data_connectors/${queryArg.dataConnectorId}/project_links`, + params: { params: queryArg.params }, }), }), postDataConnectorsByDataConnectorIdProjectLinks: build.mutation< @@ -183,6 +193,12 @@ export type PostDataConnectorsGlobalApiResponse = export type PostDataConnectorsGlobalApiArg = { globalDataConnectorPost: GlobalDataConnectorPost; }; +export type GetDataConnectorsSearchApiResponse = + /** status 200 The data connector */ DataConnectorRead; +export type GetDataConnectorsSearchApiArg = { + /** The DOI of the data connector */ + doi: Doi; +}; export type GetDataConnectorsByDataConnectorIdApiResponse = /** status 200 The data connector */ DataConnectorRead; export type GetDataConnectorsByDataConnectorIdApiArg = { @@ -232,13 +248,15 @@ export type GetDataConnectorsByDataConnectorIdPermissionsApiArg = { export type GetDataConnectorsByDataConnectorIdProjectLinksApiResponse = /** status 200 List of data connector to project links */ DataConnectorToProjectLinksList; export type GetDataConnectorsByDataConnectorIdProjectLinksApiArg = { - /** the ID of the data connector */ + /** the ID of the data connector that can be ULID or DOI */ dataConnectorId: Ulid; + /** query parameters */ + params?: PaginationRequest; }; export type PostDataConnectorsByDataConnectorIdProjectLinksApiResponse = /** status 201 The data connector was connected to a project */ DataConnectorToProjectLink; export type PostDataConnectorsByDataConnectorIdProjectLinksApiArg = { - /** the ID of the data connector */ + /** the ID of the data connector that can be ULID or DOI */ dataConnectorId: Ulid; dataConnectorToProjectLinkPost: DataConnectorToProjectLinkPost; }; @@ -502,10 +520,12 @@ export type DataConnectorPermissions = { /** The user can manage data connector members */ change_membership?: boolean; }; +export type ProjectPath = string; export type DataConnectorToProjectLink = { id: Ulid; data_connector_id: Ulid; project_id: Ulid; + project_path: ProjectPath; creation_date: CreationDate; created_by: UserId; }; @@ -533,6 +553,7 @@ export const { useGetDataConnectorsQuery, usePostDataConnectorsMutation, usePostDataConnectorsGlobalMutation, + useGetDataConnectorsSearchQuery, useGetDataConnectorsByDataConnectorIdQuery, usePatchDataConnectorsByDataConnectorIdMutation, useDeleteDataConnectorsByDataConnectorIdMutation, diff --git a/client/src/features/dataConnectorsV2/api/data-connectors.enhanced-api.ts b/client/src/features/dataConnectorsV2/api/data-connectors.enhanced-api.ts index 1e31358dc0..cf8cba30a4 100644 --- a/client/src/features/dataConnectorsV2/api/data-connectors.enhanced-api.ts +++ b/client/src/features/dataConnectorsV2/api/data-connectors.enhanced-api.ts @@ -1,20 +1,20 @@ -import { processPaginationHeaders } from "../../../utils/helpers/kgPagination.utils"; -import { AbstractKgPaginatedResponse } from "../../../utils/types/pagination.types"; +import { processApiPaginationHeaders } from "~/utils/helpers/pagination.utils"; +import { + DataConnectorsPaginated, + DataConnectorToProjectLinksPaginated, +} from "../dataConnectors.types"; import type { GetDataConnectorsApiArg, - GetDataConnectorsApiResponse as GetDataConnectorsApiResponseOrig, + GetDataConnectorsApiResponse, GetDataConnectorsByDataConnectorIdApiArg, GetDataConnectorsByDataConnectorIdApiResponse, + GetDataConnectorsByDataConnectorIdProjectLinksApiArg, + GetDataConnectorsByDataConnectorIdProjectLinksApiResponse, GetDataConnectorsByDataConnectorIdSecretsApiArg, GetDataConnectorsByDataConnectorIdSecretsApiResponse, } from "./data-connectors.api"; import { dataConnectorsApi as api } from "./data-connectors.api"; -export interface GetDataConnectorsApiResponse - extends AbstractKgPaginatedResponse { - dataConnectors: GetDataConnectorsApiResponseOrig; -} - interface GetDataConnectorsListByDataConnectorIdsApiArg { dataConnectorIds: GetDataConnectorsByDataConnectorIdApiArg["dataConnectorId"][]; } @@ -33,40 +33,8 @@ type GetDataConnectorListSecretsApiResponse = Record< GetDataConnectorsByDataConnectorIdSecretsApiResponse >; -const injectedApi = api.injectEndpoints({ +const withNewEndpoints = api.injectEndpoints({ endpoints: (builder) => ({ - getDataConnectorsPaged: builder.query< - GetDataConnectorsApiResponse, - GetDataConnectorsApiArg - >({ - query: (queryArg) => ({ - url: "/data_connectors", - params: { - namespace: queryArg.params?.namespace, - page: queryArg.params?.page, - per_page: queryArg.params?.per_page, - }, - }), - transformResponse: (response, meta, queryArg) => { - const dataConnectors = response as GetDataConnectorsApiResponseOrig; - const headers = meta?.response?.headers; - const headerResponse = processPaginationHeaders( - headers, - queryArg.params == null - ? {} - : { page: queryArg.params.page, perPage: queryArg.params.per_page }, - dataConnectors - ); - - return { - dataConnectors, - page: headerResponse.page, - perPage: headerResponse.perPage, - total: headerResponse.total, - totalPages: headerResponse.totalPages, - }; - }, - }), getDataConnectorsListByDataConnectorIds: builder.query< GetDataConnectorsListByDataConnectorIdsApiResponse, GetDataConnectorsListByDataConnectorIdsApiArg @@ -112,7 +80,57 @@ const injectedApi = api.injectEndpoints({ }), }); -const enhancedApi = injectedApi.enhanceEndpoints({ +const withPagination = withNewEndpoints.injectEndpoints({ + endpoints: (builder) => ({ + getDataConnectors: builder.query< + DataConnectorsPaginated, + GetDataConnectorsApiArg + >({ + query: (queryArg) => ({ + url: "/data_connectors", + params: { + namespace: queryArg.params?.namespace, + page: queryArg.params?.page, + per_page: queryArg.params?.per_page, + }, + }), + transformResponse: (data: GetDataConnectorsApiResponse, meta) => { + const headers = meta?.response?.headers; + const pagination = processApiPaginationHeaders(headers); + return { + data, + pagination, + }; + }, + }), + getDataConnectorsByDataConnectorIdProjectLinks: builder.query< + DataConnectorToProjectLinksPaginated, + GetDataConnectorsByDataConnectorIdProjectLinksApiArg + >({ + query: ({ dataConnectorId, params }) => ({ + url: `/data_connectors/${dataConnectorId}/project_links`, + params: { + page: params?.page, + per_page: params?.per_page, + }, + }), + transformResponse: ( + data: GetDataConnectorsByDataConnectorIdProjectLinksApiResponse, + meta + ) => { + const headers = meta?.response?.headers; + const pagination = processApiPaginationHeaders(headers); + return { + data, + pagination, + }; + }, + }), + }), + overrideExisting: true, +}); + +const enhancedApi = withPagination.enhanceEndpoints({ addTagTypes: [ "DataConnectors", "DataConnectorsProjectLinks", @@ -128,7 +146,7 @@ const enhancedApi = injectedApi.enhanceEndpoints({ deleteDataConnectorsByDataConnectorIdSecrets: { invalidatesTags: ["DataConnectorSecrets"], }, - getDataConnectorsPaged: { + getDataConnectors: { providesTags: ["DataConnectors"], }, getDataConnectorsByDataConnectorId: { @@ -179,7 +197,7 @@ export const { useDeleteDataConnectorsByDataConnectorIdMutation, useDeleteDataConnectorsByDataConnectorIdProjectLinksAndLinkIdMutation, useDeleteDataConnectorsByDataConnectorIdSecretsMutation, - useGetDataConnectorsPagedQuery: useGetDataConnectorsQuery, + useGetDataConnectorsQuery, useGetDataConnectorsByDataConnectorIdQuery, useGetDataConnectorsByDataConnectorIdProjectLinksQuery, useGetDataConnectorsByDataConnectorIdSecretsQuery, diff --git a/client/src/features/dataConnectorsV2/api/data-connectors.openapi.json b/client/src/features/dataConnectorsV2/api/data-connectors.openapi.json index ae00f2d29c..06871e269e 100644 --- a/client/src/features/dataConnectorsV2/api/data-connectors.openapi.json +++ b/client/src/features/dataConnectorsV2/api/data-connectors.openapi.json @@ -144,6 +144,48 @@ "tags": ["data_connectors"] } }, + "/data_connectors/search": { + "get": { + "summary": "Get data connector details by DOI", + "parameters": [ + { + "in": "query", + "name": "doi", + "required": true, + "schema": { + "$ref": "#/components/schemas/DOI" + }, + "description": "The DOI of the data connector" + } + ], + "responses": { + "200": { + "description": "The data connector", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DataConnector" + } + } + } + }, + "404": { + "description": "The data connector with the given DOI does not exist or user does not have access to it", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "default": { + "$ref": "#/components/responses/Error" + } + }, + "tags": ["data_connectors"] + } + }, "/data_connectors/{data_connector_id}": { "parameters": [ { @@ -440,11 +482,23 @@ "schema": { "$ref": "#/components/schemas/Ulid" }, - "description": "the ID of the data connector" + "description": "the ID of the data connector that can be ULID or DOI" } ], "get": { "summary": "Get all links from a given data connector to projects", + "parameters": [ + { + "in": "query", + "description": "query parameters", + "name": "params", + "style": "form", + "explode": true, + "schema": { + "$ref": "#/components/schemas/PaginationRequest" + } + } + ], "responses": { "200": { "description": "List of data connector to project links", @@ -454,6 +508,36 @@ "$ref": "#/components/schemas/DataConnectorToProjectLinksList" } } + }, + "headers": { + "page": { + "description": "The index of the current page (starting at 1).", + "required": true, + "schema": { + "type": "integer" + } + }, + "per-page": { + "description": "The number of items per page.", + "required": true, + "schema": { + "type": "integer" + } + }, + "total": { + "description": "The total number of items.", + "required": true, + "schema": { + "type": "integer" + } + }, + "total-pages": { + "description": "The total number of pages.", + "required": true, + "schema": { + "type": "integer" + } + } } }, "default": { @@ -955,6 +1039,11 @@ "$ref": "#/components/schemas/DataConnectorToProjectLink" } }, + "ProjectPath": { + "description": "The path to the project page", + "type": "string", + "example": "namespace/project-slug" + }, "DataConnectorToProjectLink": { "description": "A link from a data connector to a project in Renku 2.0", "type": "object", @@ -969,6 +1058,9 @@ "project_id": { "$ref": "#/components/schemas/Ulid" }, + "project_path": { + "$ref": "#/components/schemas/ProjectPath" + }, "creation_date": { "$ref": "#/components/schemas/CreationDate" }, @@ -980,6 +1072,7 @@ "id", "data_connector_id", "project_id", + "project_path", "creation_date", "created_by" ] diff --git a/client/src/features/dataConnectorsV2/components/DataConnectorActions.tsx b/client/src/features/dataConnectorsV2/components/DataConnectorActions.tsx index 094d07552b..26b251a1cf 100644 --- a/client/src/features/dataConnectorsV2/components/DataConnectorActions.tsx +++ b/client/src/features/dataConnectorsV2/components/DataConnectorActions.tsx @@ -71,7 +71,7 @@ function DataConnectorRemoveDeleteModal({ useDataConnectorPermissions({ dataConnectorId: dataConnector.id }); const { - data: dataConnectorLinks, + data: dataConnectorLinksPaginated, isLoading: isLoadingLinks, isError: isLoadingLinksError, } = useGetDataConnectorsByDataConnectorIdProjectLinksQuery({ @@ -120,7 +120,7 @@ function DataConnectorRemoveDeleteModal({ } enabled={ - dataConnectorLinks == null || isLoadingLinksError ? ( + dataConnectorLinksPaginated == null || isLoadingLinksError ? (

@@ -143,12 +143,13 @@ function DataConnectorRemoveDeleteModal({

Are you sure you want to delete this data connector?{" "} - {dataConnectorLinks.length < 1 ? ( + {dataConnectorLinksPaginated.pagination.totalItems < 1 ? ( <> It is not used in any projects that are visible to you, but it will affect any projects where it is used. - ) : dataConnectorLinks.length === 1 ? ( + ) : dataConnectorLinksPaginated.pagination.totalItems === + 1 ? ( <> It will affect at least 1 project that uses it. @@ -156,7 +157,8 @@ function DataConnectorRemoveDeleteModal({ <> It will affect at least{" "} - {dataConnectorLinks.length} projects that use it + {dataConnectorLinksPaginated.pagination.totalItems}{" "} + projects that use it . diff --git a/client/src/features/dataConnectorsV2/components/DataConnectorView.tsx b/client/src/features/dataConnectorsV2/components/DataConnectorView.tsx index 384a28102a..2a2afa47d0 100644 --- a/client/src/features/dataConnectorsV2/components/DataConnectorView.tsx +++ b/client/src/features/dataConnectorsV2/components/DataConnectorView.tsx @@ -31,7 +31,7 @@ import { Pencil, PersonBadge, } from "react-bootstrap-icons"; -import { generatePath, Link } from "react-router"; +import { generatePath, Link, useSearchParams } from "react-router"; import { Button, Offcanvas, @@ -42,6 +42,7 @@ import { import KeywordBadge from "~/components/keywords/KeywordBadge"; import KeywordContainer from "~/components/keywords/KeywordContainer"; import LazyMarkdown from "~/components/markdown/LazyMarkdown"; +import Pagination from "~/components/Pagination"; import { IntegrationAlert } from "~/features/cloudStorage/AddOrEditCloudStorage"; import { useGetStorageSchemaQuery } from "~/features/cloudStorage/api/projectCloudStorage.api"; import { @@ -419,10 +420,33 @@ function DataConnectorViewHeader({ ); } +const LINKED_PROJECTS_PER_PAGE = 10; +const LINKED_PROJECTS_PAGE_PARAM = "linked_projects_page"; + function DataConnectorViewProjects({ dataConnector, }: Pick) { - const { projects, isLoading } = useDataConnectorProjects({ dataConnector }); + const [searchParams] = useSearchParams(); + + const page = useMemo(() => { + const pageRaw = searchParams.get(LINKED_PROJECTS_PAGE_PARAM); + if (!pageRaw) { + return 1; + } + try { + const page = parseInt(pageRaw, 10); + return page > 0 ? page : 1; + } catch { + return 1; + } + }, [searchParams]); + + const { projectsPaginated, isFetching } = useDataConnectorProjects({ + dataConnector, + page, + perPage: LINKED_PROJECTS_PER_PAGE, + }); + return (

- {isLoading &&

Retrieving projects...

} - {!isLoading && projects.length === 0 &&

None

} - - - {projects.map((project) => { - if (!project) return null; - - const projectUrl = generatePath( - ABSOLUTE_ROUTES.v2.projects.show.root, - { - namespace: project.namespace, - slug: project.slug, - } - ); - return ( - - - - - ); - })} - -
- - {project.namespace}/{project.slug} - - {project.description}
+ {isFetching ? ( +

Retrieving projects...

+ ) : projectsPaginated.data.length == 0 ? ( +

None

+ ) : ( + <> + + + {projectsPaginated.data.map((project) => { + if (!project) return null; + + const projectUrl = generatePath( + ABSOLUTE_ROUTES.v2.projects.show.root, + { + namespace: project.namespace, + slug: project.slug, + } + ); + return ( + + + + + ); + })} + +
+ + {project.namespace}/{project.slug} + + {project.description}
+ + + )}
); diff --git a/client/src/features/dataConnectorsV2/components/DataConnectorsBox.tsx b/client/src/features/dataConnectorsV2/components/DataConnectorsBox.tsx index b0947224f9..00db05cfef 100644 --- a/client/src/features/dataConnectorsV2/components/DataConnectorsBox.tsx +++ b/client/src/features/dataConnectorsV2/components/DataConnectorsBox.tsx @@ -37,10 +37,8 @@ import Pagination from "../../../components/Pagination"; import useGroupPermissions from "../../groupsV2/utils/useGroupPermissions.hook"; import PermissionsGuard from "../../permissionsV2/PermissionsGuard"; import type { NamespaceKind } from "../../projectsV2/api/namespace.api"; -import { - useGetDataConnectorsQuery, - type GetDataConnectorsApiResponse, -} from "../api/data-connectors.enhanced-api"; +import { useGetDataConnectorsQuery } from "../api/data-connectors.enhanced-api"; +import type { DataConnectorsPaginated } from "../dataConnectors.types"; import DataConnectorModal from "./DataConnectorModal"; import DataConnectorBoxListDisplay, { DataConnectorBoxListDisplayPlaceholder, @@ -146,20 +144,20 @@ export default function DataConnectorsBox({ }); useEffect(() => { - if (data?.totalPages && page > data.totalPages) { + if (data?.pagination.totalPages && page > data.pagination.totalPages) { setSearchParams( (prevParams) => { - if (data.totalPages == 1) { + if (data.pagination.totalPages == 1) { prevParams.delete(pageParam); } else { - prevParams.set(pageParam, `${data.totalPages}`); + prevParams.set(pageParam, `${data.pagination.totalPages}`); } return prevParams; }, { replace: true } ); } - }, [data?.totalPages, page, pageParam, setSearchParams]); + }, [data?.pagination.totalPages, page, pageParam, setSearchParams]); if (isLoading) return ; @@ -184,7 +182,7 @@ export default function DataConnectorsBox({ interface DataConnectorBoxContentProps { children?: React.ReactNode; - data: GetDataConnectorsApiResponse; + data: DataConnectorsPaginated; isLoading: boolean; limit?: number; namespace: string; @@ -213,18 +211,18 @@ function DataConnectorBoxContent({ toggleOpen={toggleOpen} namespace={namespace} namespaceKind={namespaceKind} - totalConnectors={data.total} + totalConnectors={data.pagination.totalItems} /> - {data.total === 0 && namespaceKind === "group" && ( + {data.pagination.totalItems === 0 && namespaceKind === "group" && ( )} - {data.total === 0 && namespaceKind === "user" && ( + {data.pagination.totalItems === 0 && namespaceKind === "user" && ( )} - {data.total > 0 && ( + {data.pagination.totalItems > 0 && ( - {data.dataConnectors?.map((dc) => + {data.data?.map((dc) => isLoading ? ( ) : ( @@ -235,9 +233,9 @@ function DataConnectorBoxContent({ /> ) )} - {limit && data.total > limit && ( + {limit && data.pagination.totalItems > limit && ( - And {data.total - data.dataConnectors.length} more... + And {data.pagination.totalItems - data.data.length} more... )} @@ -245,10 +243,10 @@ function DataConnectorBoxContent({ {!limit && ( )} {children} diff --git a/client/src/features/dataConnectorsV2/components/useDataConnectorProjects.hook.ts b/client/src/features/dataConnectorsV2/components/useDataConnectorProjects.hook.ts index ec68e3c81f..8a4a53f9c6 100644 --- a/client/src/features/dataConnectorsV2/components/useDataConnectorProjects.hook.ts +++ b/client/src/features/dataConnectorsV2/components/useDataConnectorProjects.hook.ts @@ -19,8 +19,13 @@ import { skipToken } from "@reduxjs/toolkit/query"; import { useMemo } from "react"; -import { useGetProjectsByProjectIdsQuery } from "../../projectsV2/api/projectV2.enhanced-api"; -import type { SessionStartDataConnectorConfiguration } from "../../sessionsV2/startSessionOptionsV2.types"; +import type { Project } from "~/features/projectsV2/api/projectV2.api"; +import { useGetProjectsByProjectIdsQuery } from "~/features/projectsV2/api/projectV2.enhanced-api"; +import type { SessionStartDataConnectorConfiguration } from "~/features/sessionsV2/startSessionOptionsV2.types"; +import type { + ApiPagination, + PaginatedResponse, +} from "~/utils/types/pagination.types"; import type { DataConnectorRead } from "../api/data-connectors.api"; import { useGetDataConnectorsByDataConnectorIdProjectLinksQuery } from "../api/data-connectors.enhanced-api"; @@ -31,38 +36,52 @@ export interface DataConnectorConfiguration interface UseDataSourceConfigurationArgs { dataConnector: DataConnectorRead | undefined; + page: number; + perPage: number; } export default function useDataConnectorProjects({ dataConnector, + page, + perPage, }: UseDataSourceConfigurationArgs) { - const { data: projectLinks, isLoading: isLoadingLinks } = + const { currentData: projectLinksPaginated, isFetching: isFetchingLinks } = useGetDataConnectorsByDataConnectorIdProjectLinksQuery( dataConnector ? { dataConnectorId: dataConnector.id, + params: { page, per_page: perPage }, } : skipToken ); - const { data: projectsMap, isLoading: isLoadingProjects } = + + const { currentData: projectsMap, isFetching: isFetchingProjects } = useGetProjectsByProjectIdsQuery({ - projectIds: projectLinks?.map((pl) => pl.project_id) ?? [], + projectIds: projectLinksPaginated?.data.map((pl) => pl.project_id) ?? [], }); - const projects = useMemo(() => { - return ( - projectLinks - ?.map((pl) => projectsMap?.[pl.project_id]) - .filter((p) => p != null) ?? [] - ); - }, [projectLinks, projectsMap]); + const projectsPaginated = useMemo(() => { + if (projectLinksPaginated == null || projectsMap == null) { + return undefined; + } + return { + data: projectLinksPaginated.data + .map(({ project_id }) => projectsMap[project_id]) + .filter((p) => p != null), + pagination: projectLinksPaginated.pagination, + } satisfies ProjectsPaginated; + }, [projectLinksPaginated, projectsMap]); - const projectMapLength = Object.keys(projectsMap ?? {}).length; + if (isFetchingLinks || isFetchingProjects || projectsPaginated == null) { + return { + projectsPaginated, + isFetching: true as const, + }; + } return { - projects, - isLoading: - isLoadingLinks || - isLoadingProjects || - projectMapLength < (projectLinks?.length ?? 0), + projectsPaginated, + isFetching: false as const, }; } + +type ProjectsPaginated = PaginatedResponse; diff --git a/client/src/features/dataConnectorsV2/dataConnectors.types.ts b/client/src/features/dataConnectorsV2/dataConnectors.types.ts index d3d6f39a21..87ca8c056f 100644 --- a/client/src/features/dataConnectorsV2/dataConnectors.types.ts +++ b/client/src/features/dataConnectorsV2/dataConnectors.types.ts @@ -16,10 +16,27 @@ * limitations under the License. */ -import type { DataConnector } from "./api/data-connectors.api"; +import type { + ApiPagination, + PaginatedResponse, +} from "~/utils/types/pagination.types"; +import type { + DataConnector, + DataConnectorToProjectLink, +} from "./api/data-connectors.api"; export type DataConnectorScope = "global" | "namespace" | "project"; export type DataConnectorWithScope = DataConnector & { scope: DataConnectorScope; }; + +export type DataConnectorsPaginated = PaginatedResponse< + DataConnector, + ApiPagination +>; + +export type DataConnectorToProjectLinksPaginated = PaginatedResponse< + DataConnectorToProjectLink, + ApiPagination +>; diff --git a/client/src/features/projectsV2/api/projectV2.openapi.json b/client/src/features/projectsV2/api/projectV2.openapi.json index b8f0d4c3d1..56b52fbe92 100644 --- a/client/src/features/projectsV2/api/projectV2.openapi.json +++ b/client/src/features/projectsV2/api/projectV2.openapi.json @@ -1415,42 +1415,6 @@ "description": "Entity Tag", "example": "9EE498F9D565D0C41E511377425F32F3" }, - "DataConnectorToProjectLinksList": { - "description": "A list of links from a data connector to a project", - "type": "array", - "items": { - "$ref": "#/components/schemas/DataConnectorToProjectLink" - } - }, - "DataConnectorToProjectLink": { - "description": "A link from a data connector to a project in Renku 2.0", - "type": "object", - "additionalProperties": false, - "properties": { - "id": { - "$ref": "#/components/schemas/Ulid" - }, - "data_connector_id": { - "$ref": "#/components/schemas/Ulid" - }, - "project_id": { - "$ref": "#/components/schemas/Ulid" - }, - "creation_date": { - "$ref": "#/components/schemas/CreationDate" - }, - "created_by": { - "$ref": "#/components/schemas/UserId" - } - }, - "required": [ - "id", - "data_connector_id", - "project_id", - "creation_date", - "created_by" - ] - }, "ProjectGetQuery": { "description": "Query params for project get request", "allOf": [ diff --git a/client/src/utils/helpers/pagination.constants.ts b/client/src/utils/helpers/pagination.constants.ts new file mode 100644 index 0000000000..9fe13ee811 --- /dev/null +++ b/client/src/utils/helpers/pagination.constants.ts @@ -0,0 +1,23 @@ +/*! + * Copyright 2026 - Swiss Data Science Center (SDSC) + * A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and + * Eidgenössische Technische Hochschule Zürich (ETHZ). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** Default current page used in pagination (page 1). */ +export const DEFAULT_PAGE = 1; + +/** Default amount of items per page (this is taken from the default value used in renku-data-services). */ +export const DEFAULT_PER_PAGE = 20; diff --git a/client/src/utils/helpers/pagination.utils.ts b/client/src/utils/helpers/pagination.utils.ts index 85fee40352..b9ec3c84cf 100644 --- a/client/src/utils/helpers/pagination.utils.ts +++ b/client/src/utils/helpers/pagination.utils.ts @@ -17,10 +17,24 @@ */ import processPaginationHeadersJs from "../../api-client/pagination"; -import { Pagination } from "../types/pagination.types"; +import type { ApiPagination, Pagination } from "../types/pagination.types"; +import { DEFAULT_PAGE, DEFAULT_PER_PAGE } from "./pagination.constants"; const processPaginationHeaders = processPaginationHeadersJs as ( headers: Headers | undefined | null ) => Pagination; export default processPaginationHeaders; + +export function processApiPaginationHeaders( + headers: Headers | undefined | null +): ApiPagination { + const pagination_ = processPaginationHeaders(headers); + return { + ...pagination_, + currentPage: pagination_.currentPage ?? DEFAULT_PAGE, + perPage: pagination_.perPage ?? DEFAULT_PER_PAGE, + totalItems: pagination_.totalItems ?? 0, + totalPages: pagination_.totalPages ?? 1, + }; +} diff --git a/client/src/utils/types/pagination.types.ts b/client/src/utils/types/pagination.types.ts index 52480b6574..b7dd2cc618 100644 --- a/client/src/utils/types/pagination.types.ts +++ b/client/src/utils/types/pagination.types.ts @@ -44,3 +44,10 @@ export interface PaginatedResponse { data: T[]; pagination: P; } + +/** Pagination used by renku-data-services. */ +export type ApiPagination = Pagination & + Pick< + Required, + "currentPage" | "perPage" | "totalItems" | "totalPages" + >;