diff --git a/client/package-lock.json b/client/package-lock.json index 3e044c1513..5fa7cc7a97 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -48,7 +48,7 @@ "react-helmet": "^6.1.0", "react-hook-form": "^7.45.2", "react-ipynb-renderer": "^2.1.2", - "react-js-pagination": "^3.0.2", + "react-js-pagination": "^3.0.3", "react-katex": "^3.0.1", "react-markdown": "^8.0.7", "react-masonry-css": "^1.0.16", @@ -105,6 +105,7 @@ "@types/react-avatar-editor": "^13.0.2", "@types/react-dom": "^18.2.17", "@types/react-helmet": "^6.1.9", + "@types/react-js-pagination": "^3.0.7", "@types/react-router-dom": "^5.3.3", "@types/react-router-hash-link": "^2.4.9", "@types/react-test-renderer": "^18.0.7", @@ -17462,6 +17463,15 @@ "@types/react": "*" } }, + "node_modules/@types/react-js-pagination": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/react-js-pagination/-/react-js-pagination-3.0.7.tgz", + "integrity": "sha512-h16F5eFcVaTO5LTT5jJrvK8SxTlxkuv03ZKt/e6L3GPng/0TZTqhEKEyD8F5XksLeKBalsS1tTN2foJLWv/6mA==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/react-router": { "version": "5.1.18", "dev": true, diff --git a/client/package.json b/client/package.json index 45dfa5f463..940aa56ff7 100644 --- a/client/package.json +++ b/client/package.json @@ -22,9 +22,11 @@ "storybook-wait-server": "wait-on http://127.0.0.1:6006", "storybook-test": "test-storybook", "storybook-compile-and-test": "concurrently -k -s first -n 'BUILD,TEST' -c 'magenta,blue' 'npm run storybook-build && npm run storybook-start-server' 'npm run storybook-wait-server && npm run storybook-test'", - "generate-api": "npm run generate-api:dataServicesUser && npm run generate-api:projectV2", + "generate-api": "npm run generate-api:dataServicesUser && npm run generate-api:namespaceV2 && npm run generate-api:projectV2 && npm run generate-api:searchV2", "generate-api:dataServicesUser": "rtk-query-codegen-openapi src/features/user/dataServicesUser.api/dataServicesUser.api-config.ts", - "generate-api:projectV2": "rtk-query-codegen-openapi src/features/projectsV2/api/projectV2.api-config.ts" + "generate-api:namespaceV2": "rtk-query-codegen-openapi src/features/projectsV2/api/namespace.api-config.ts", + "generate-api:projectV2": "rtk-query-codegen-openapi src/features/projectsV2/api/projectV2.api-config.ts", + "generate-api:searchV2": "rtk-query-codegen-openapi src/features/searchV2/api/searchV2.api-config.ts" }, "type": "module", "dependencies": { @@ -68,7 +70,7 @@ "react-helmet": "^6.1.0", "react-hook-form": "^7.45.2", "react-ipynb-renderer": "^2.1.2", - "react-js-pagination": "^3.0.2", + "react-js-pagination": "^3.0.3", "react-katex": "^3.0.1", "react-markdown": "^8.0.7", "react-masonry-css": "^1.0.16", @@ -125,6 +127,7 @@ "@types/react-avatar-editor": "^13.0.2", "@types/react-dom": "^18.2.17", "@types/react-helmet": "^6.1.9", + "@types/react-js-pagination": "^3.0.7", "@types/react-router-dom": "^5.3.3", "@types/react-router-hash-link": "^2.4.9", "@types/react-test-renderer": "^18.0.7", diff --git a/client/src/components/List.jsx b/client/src/components/List.jsx index 78c24130da..59536a3355 100644 --- a/client/src/components/List.jsx +++ b/client/src/components/List.jsx @@ -17,9 +17,10 @@ */ import Masonry from "react-masonry-css"; -import { Pagination } from "./Pagination"; -import ListCard from "./list/ListCard"; + +import Pagination from "./Pagination"; import ListBar from "./list/ListBar"; +import ListCard from "./list/ListCard"; /** * This class receives a list of "items" and displays them either in a grid or in classic list. diff --git a/client/src/components/Pagination.jsx b/client/src/components/Pagination.jsx deleted file mode 100644 index 151804420b..0000000000 --- a/client/src/components/Pagination.jsx +++ /dev/null @@ -1,73 +0,0 @@ -/*! - * Copyright 2022 - 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. - */ -import { Component } from "react"; -import ReactPagination from "react-js-pagination"; - -/** - * renku-ui - * - * Pagination.js - * Pagination code and presentation. - */ - -class Pagination extends Component { - render() { - // We do not display the pagination footer when there are no pages or only one page - if ( - this.props.totalItems == null || - this.props.totalItems < 1 || - this.props.totalItems <= this.props.perPage - ) - return null; - - let extraInfoPagination = null; - if (this.props.showDescription && this.props.totalInPage) { - const initialValue = - this.props.currentPage * this.props.perPage - (this.props.perPage - 1); - const lastValue = initialValue + this.props.totalInPage - 1; - extraInfoPagination = ( -
- {initialValue} - {lastValue} of {this.props.totalItems} results -
- ); - } - - const className = `pagination ${ - this.props.className ? this.props.className : null - }`; - return ( -
- - {extraInfoPagination} -
- ); - } -} -export { Pagination }; diff --git a/client/src/components/Pagination.tsx b/client/src/components/Pagination.tsx new file mode 100644 index 0000000000..c1d612d6a2 --- /dev/null +++ b/client/src/components/Pagination.tsx @@ -0,0 +1,94 @@ +/*! + * Copyright 2024 - 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. + */ + +import cx from "classnames"; +import ReactPagination from "react-js-pagination"; + +interface PaginationProps { + className?: string; + currentPage: number; + onPageChange: (pageNumber: number) => void; + perPage: number; + showDescription?: boolean; + totalInPage?: number; + totalItems?: number; +} + +export default function Pagination({ + className: className_, + currentPage, + onPageChange, + perPage, + showDescription, + totalInPage, + totalItems, +}: PaginationProps) { + // We do not display the pagination footer when there are no pages or only one page + if (totalItems == null || totalItems < 1 || totalItems <= perPage) { + return null; + } + + const className = cx("pagination", className_); + + return ( +
+ + {showDescription && totalInPage && ( + + )} +
+ ); +} + +interface ExtraInfoPaginationProps { + currentPage: number; + perPage: number; + totalInPage: number; + totalItems: number; +} +function ExtraInfoPagination({ + currentPage, + perPage, + totalInPage, + totalItems, +}: ExtraInfoPaginationProps) { + const initialValue = currentPage * perPage - (perPage - 1); + const lastValue = initialValue + totalInPage - 1; + return ( +
+ {initialValue} - {lastValue} of {totalItems} results +
+ ); +} diff --git a/client/src/components/searchResultsContent/SearchResultsContent.tsx b/client/src/components/searchResultsContent/SearchResultsContent.tsx index 2b8c191470..009047644f 100644 --- a/client/src/components/searchResultsContent/SearchResultsContent.tsx +++ b/client/src/components/searchResultsContent/SearchResultsContent.tsx @@ -16,26 +16,26 @@ * limitations under the License. */ -import { Button } from "reactstrap"; -import Masonry from "react-masonry-css"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faSadCry } from "@fortawesome/free-solid-svg-icons"; -import { FetchBaseQueryError } from "@reduxjs/toolkit/query"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { SerializedError } from "@reduxjs/toolkit"; +import { FetchBaseQueryError } from "@reduxjs/toolkit/query"; +import Masonry from "react-masonry-css"; +import { Button } from "reactstrap"; import { KgSearchResult, ListResponse, } from "../../features/kgSearch/KgSearch.types"; +import { useKgSearchContext } from "../../features/kgSearch/KgSearchContext"; import { FiltersProperties, hasInitialFilterValues, mapSearchResultToEntity, } from "../../utils/helpers/KgSearchFunctions"; import { Loader } from "../Loader"; +import Pagination from "../Pagination"; import ListCard from "../list/ListCard"; -import { Pagination } from "../Pagination"; -import { useKgSearchContext } from "../../features/kgSearch/KgSearchContext"; interface SearchResultProps { data?: ListResponse; diff --git a/client/src/features/ProjectPageV2/ProjectPageContent/CodeRepositories/CodeRepositoryDisplay.tsx b/client/src/features/ProjectPageV2/ProjectPageContent/CodeRepositories/CodeRepositoryDisplay.tsx index ce9bf0f632..fc9b6b5e25 100644 --- a/client/src/features/ProjectPageV2/ProjectPageContent/CodeRepositories/CodeRepositoryDisplay.tsx +++ b/client/src/features/ProjectPageV2/ProjectPageContent/CodeRepositories/CodeRepositoryDisplay.tsx @@ -50,6 +50,7 @@ import { import { Loader } from "../../../../components/Loader"; import { RtkOrNotebooksError } from "../../../../components/errors/RtkErrorAlert"; import RenkuFrogIcon from "../../../../components/icons/RenkuIcon"; +import { safeNewUrl } from "../../../../utils/helpers/safeNewUrl.utils"; import { Project } from "../../../projectsV2/api/projectV2.api"; import { usePatchProjectsByProjectIdMutation } from "../../../projectsV2/api/projectV2.enhanced-api"; @@ -386,24 +387,18 @@ export function RepositoryItem({ showMenu = true, }: RepositoryItemProps) { const canonicalUrlStr = useMemo(() => `${url.replace(/.git$/i, "")}`, [url]); - const canonicalUrl = useMemo(() => { - try { - return new URL(canonicalUrlStr); - } catch (error) { - if (error instanceof TypeError) { - return null; - } - throw error; - } - }, [canonicalUrlStr]); + const canonicalUrl = useMemo( + () => safeNewUrl(canonicalUrlStr), + [canonicalUrlStr] + ); - const title = canonicalUrl?.pathname.split("/").pop() || canonicalUrlStr; + const title = canonicalUrl?.pathname.split("/").pop() ?? canonicalUrlStr; const urlDisplay = (
{canonicalUrl?.hostname && ( diff --git a/client/src/features/ProjectPageV2/ProjectPageContent/ProjectInformation/ProjectInformation.tsx b/client/src/features/ProjectPageV2/ProjectPageContent/ProjectInformation/ProjectInformation.tsx index a041bf086e..e847cf2ec0 100644 --- a/client/src/features/ProjectPageV2/ProjectPageContent/ProjectInformation/ProjectInformation.tsx +++ b/client/src/features/ProjectPageV2/ProjectPageContent/ProjectInformation/ProjectInformation.tsx @@ -17,7 +17,7 @@ */ import cx from "classnames"; -import { generatePath } from "react-router-dom-v5-compat"; +import { Link, generatePath } from "react-router-dom-v5-compat"; import { TimeCaption } from "../../../../components/TimeCaption"; import { @@ -30,7 +30,11 @@ import type { ProjectMemberListResponse, ProjectMemberResponse, } from "../../../projectsV2/api/projectV2.api"; -import { useGetProjectsByProjectIdMembersQuery } from "../../../projectsV2/api/projectV2.enhanced-api"; +import { + useGetNamespacesByNamespaceSlugQuery, + useGetProjectsByProjectIdMembersQuery, +} from "../../../projectsV2/api/projectV2.enhanced-api"; +import { useGetUsersByUserIdQuery } from "../../../user/dataServicesUser.api"; import { useProject } from "../../ProjectPageContainer/ProjectPageContainer"; import MembershipGuard from "../../utils/MembershipGuard"; import { toSortedMembers } from "../../utils/roleUtils"; @@ -38,6 +42,8 @@ import { toSortedMembers } from "../../utils/roleUtils"; import projectPreviewImg from "../../../../styles/assets/projectImagePreview.svg"; import styles from "./ProjectInformation.module.scss"; +import UserAvatar from "../../../usersV2/show/UserAvatar"; +import { useMemo } from "react"; export function ProjectImageView() { return ( @@ -56,6 +62,8 @@ function ProjectInformationMember({ }: { member: ProjectMemberResponse; }) { + const { data: memberData } = useGetUsersByUserIdQuery({ userId: member.id }); + const displayName = member.first_name && member.last_name ? `${member.first_name} ${member.last_name}` @@ -65,6 +73,26 @@ function ProjectInformationMember({ ? member.email : member.id; + if (memberData?.username) { + return ( +
+
+ +
+ + {displayName} + +
+ ); + } + return
{displayName}
; } @@ -127,6 +155,25 @@ export default function ProjectInformation() { }); const membersUrl = `${settingsUrl}#members`; + const { data: namespace } = useGetNamespacesByNamespaceSlugQuery({ + namespaceSlug: project.namespace, + }); + const namespaceName = useMemo( + () => namespace?.name ?? project.namespace, + [namespace?.name, project.namespace] + ); + const namespaceUrl = useMemo( + () => + namespace?.namespace_kind === "group" + ? generatePath(ABSOLUTE_ROUTES.v2.groups.show.root, { + slug: project.namespace, + }) + : generatePath(ABSOLUTE_ROUTES.v2.users.show, { + username: project.namespace, + }), + [namespace?.namespace_kind, project.namespace] + ); + return (