From c598db5122b14d795e3732d94d9a1a1eaf6dc6a0 Mon Sep 17 00:00:00 2001 From: Flora Thiebaut Date: Wed, 10 Jul 2024 09:31:21 +0200 Subject: [PATCH] feat: add user and group pages in Renku 2.0 (#3198) Closes #3197. New features for Renku 2.0: add user pages, update group pages, update project info, update search page. Changes: - feat: add user pages in Renku 2.0 (#3200) - feat: update group pages (#3206) - feat: update the search page in Renku 2.0 (#3208) **BREAKING CHANGE**: requires `renku-data-services >= 0.17.0` **BREAKING CHANGE**: requires `renku-search >= 0.4.0` --------- Co-authored-by: Chandrasekhar Ramakrishnan --- client/package-lock.json | 12 +- client/package.json | 9 +- client/src/components/List.jsx | 5 +- client/src/components/Pagination.jsx | 73 -- client/src/components/Pagination.tsx | 94 +++ .../SearchResultsContent.tsx | 12 +- .../CodeRepositoryDisplay.tsx | 19 +- .../ProjectInformation/ProjectInformation.tsx | 53 +- .../src/features/dashboardV2/DashboardV2.tsx | 2 +- .../features/groupsV2/LazyGroupV2Settings.tsx | 30 + .../LazyGroupV2Show.tsx} | 13 +- .../members/GroupV2MemberListDisplay.tsx | 134 ++++ .../groupsV2/settings/GroupV2Settings.tsx | 80 +++ .../features/groupsV2/show/GroupV2Show.tsx | 189 ++++++ .../projectsV2/api/namespace.api-config.ts | 2 +- .../features/projectsV2/api/namespace.api.ts | 26 +- .../projectsV2/api/namespace.openapi.json | 43 +- .../features/projectsV2/api/projectV2.api.ts | 8 +- .../projectsV2/api/projectV2.enhanced-api.ts | 14 +- .../projectsV2/api/projectV2.openapi.json | 28 +- .../fields/DescriptionFormField.tsx | 2 +- .../fields/ProjectNamespaceFormField.tsx | 3 +- .../fields/RemoveProjectMemberModal.tsx | 8 +- .../features/projectsV2/list/GroupList.tsx | 4 +- .../projectsV2/list/ProjectV2List.tsx | 90 +-- .../projectsV2/list/ProjectV2ListDisplay.tsx | 234 +++++++ .../src/features/projectsV2/new/GroupNew.tsx | 2 +- .../projectsV2/notFound/UserNotFound.tsx | 87 +++ .../features/projectsV2/show/GroupShow.tsx | 174 ----- .../projectsV2/show/groupEditForms.tsx | 69 +- client/src/features/rootV2/RootV2.tsx | 24 +- .../features/searchV2/api/search.openapi.json | 633 ++++++++++++++++++ .../searchV2/api/searchV2-empty.api.ts | 26 + .../searchV2/api/searchV2.api-config.ts | 32 + .../features/searchV2/api/searchV2Api.api.ts | 132 ++++ .../searchV2/components/SearchV2Header.tsx | 8 +- .../searchV2/components/SearchV2Results.tsx | 220 ++++-- client/src/features/searchV2/searchV2.api.ts | 50 -- .../src/features/searchV2/searchV2.types.ts | 47 -- .../src/features/searchV2/searchV2.utils.ts | 5 +- .../features/searchV2/useStartSearch.hook.ts | 7 +- .../dataServicesUser.api.ts | 4 +- .../dataServicesUser.openapi.json | 134 +++- .../src/features/usersV2/LazyUserRedirect.tsx | 30 + .../LazyUserShow.tsx} | 9 +- .../usersV2/show/UserAvatar.module.scss | 24 + .../src/features/usersV2/show/UserAvatar.tsx | 61 ++ .../features/usersV2/show/UserRedirect.tsx | 108 +++ client/src/features/usersV2/show/UserShow.tsx | 177 +++++ .../{userV2 => usersV2}/userV2.types.ts | 0 .../overview/ProjectOverview.present.jsx | 2 +- client/src/routing/routes.constants.ts | 18 +- client/src/styleguide/ListsGuide.jsx | 2 +- client/src/utils/helpers/EnhancedState.ts | 2 +- client/src/utils/helpers/safeNewUrl.utils.ts | 32 + tests/cypress/e2e/groupV2.spec.ts | 58 +- tests/cypress/e2e/projectV2.spec.ts | 19 +- tests/cypress/e2e/projectV2setup.spec.ts | 2 +- .../groupV2/list-groupV2-members.json | 6 +- .../groupV2/read-groupV2-namespace.json | 8 + .../groupV2/update-groupV2-namespace.json | 8 + .../list-projectV2-members-many.json | 2 +- .../projectV2/list-projectV2-members.json | 2 +- .../update-projectV2-one-repository.json | 1 + .../support/renkulab-fixtures/namespaceV2.ts | 21 +- .../support/renkulab-fixtures/searchV2.ts | 2 +- 66 files changed, 2741 insertions(+), 694 deletions(-) delete mode 100644 client/src/components/Pagination.jsx create mode 100644 client/src/components/Pagination.tsx create mode 100644 client/src/features/groupsV2/LazyGroupV2Settings.tsx rename client/src/features/{projectsV2/show/groupShow.types.ts => groupsV2/LazyGroupV2Show.tsx} (71%) create mode 100644 client/src/features/groupsV2/members/GroupV2MemberListDisplay.tsx create mode 100644 client/src/features/groupsV2/settings/GroupV2Settings.tsx create mode 100644 client/src/features/groupsV2/show/GroupV2Show.tsx create mode 100644 client/src/features/projectsV2/list/ProjectV2ListDisplay.tsx create mode 100644 client/src/features/projectsV2/notFound/UserNotFound.tsx delete mode 100644 client/src/features/projectsV2/show/GroupShow.tsx create mode 100644 client/src/features/searchV2/api/search.openapi.json create mode 100644 client/src/features/searchV2/api/searchV2-empty.api.ts create mode 100644 client/src/features/searchV2/api/searchV2.api-config.ts create mode 100644 client/src/features/searchV2/api/searchV2Api.api.ts delete mode 100644 client/src/features/searchV2/searchV2.api.ts create mode 100644 client/src/features/usersV2/LazyUserRedirect.tsx rename client/src/features/{projectsV2/LazyGroupShow.tsx => usersV2/LazyUserShow.tsx} (85%) create mode 100644 client/src/features/usersV2/show/UserAvatar.module.scss create mode 100644 client/src/features/usersV2/show/UserAvatar.tsx create mode 100644 client/src/features/usersV2/show/UserRedirect.tsx create mode 100644 client/src/features/usersV2/show/UserShow.tsx rename client/src/features/{userV2 => usersV2}/userV2.types.ts (100%) create mode 100644 client/src/utils/helpers/safeNewUrl.utils.ts create mode 100644 tests/cypress/fixtures/groupV2/read-groupV2-namespace.json create mode 100644 tests/cypress/fixtures/groupV2/update-groupV2-namespace.json 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 (