From 0eebdcb5f8ff98cbf3dfd67f74a21dadcb05f2b3 Mon Sep 17 00:00:00 2001 From: Andrea Cordoba <43388408+andre-code@users.noreply.github.com> Date: Tue, 15 Oct 2024 14:25:07 +0200 Subject: [PATCH] feat: add playful 404 and error pages (#3248) fix #3235 --- .../error-boundary/ErrorBoundary.module.scss | 13 --- client/src/error-boundary/ErrorBoundary.tsx | 79 ++++++++++++------- .../src/features/projectsV2/new/GroupNew.tsx | 10 ++- .../features/projectsV2/new/ProjectV2New.tsx | 15 +++- .../projectsV2/notFound/GroupNotFound.tsx | 58 ++++++-------- .../projectsV2/notFound/ProjectNotFound.tsx | 63 +++++++-------- .../projectsV2/notFound/UserNotFound.tsx | 62 +++++++-------- client/src/index.jsx | 2 +- client/src/namespace/Namespace.present.jsx | 42 +++++++--- client/src/not-found/NotFound.tsx | 53 ++++++++----- client/src/styles/assets/not-found.svg | 69 ++++++++++++++++ client/src/styles/assets/not-foundV2.svg | 69 ++++++++++++++++ client/src/styles/assets/oops.svg | 28 +++++++ client/src/styles/assets/oopsV2.svg | 28 +++++++ tests/cypress/e2e/adminPage.spec.ts | 4 - tests/cypress/e2e/groupV2.spec.ts | 5 +- tests/cypress/e2e/project.spec.ts | 12 --- tests/cypress/e2e/projectV2.spec.ts | 4 +- 18 files changed, 418 insertions(+), 198 deletions(-) delete mode 100644 client/src/error-boundary/ErrorBoundary.module.scss create mode 100644 client/src/styles/assets/not-found.svg create mode 100644 client/src/styles/assets/not-foundV2.svg create mode 100644 client/src/styles/assets/oops.svg create mode 100644 client/src/styles/assets/oopsV2.svg diff --git a/client/src/error-boundary/ErrorBoundary.module.scss b/client/src/error-boundary/ErrorBoundary.module.scss deleted file mode 100644 index 76794e9655..0000000000 --- a/client/src/error-boundary/ErrorBoundary.module.scss +++ /dev/null @@ -1,13 +0,0 @@ -.error { - background-image: url("/src/styles/assets/error-background@0.50x.png"); - background-position: center; - background-size: cover; - color: var(--bs-rk-pink); - height: calc(100vh); - overflow: auto; - width: calc(100vw); - - @media (min-width: 1500px) { - background-image: url("/src/styles/assets/error-background@1.00x.png"); - } -} diff --git a/client/src/error-boundary/ErrorBoundary.tsx b/client/src/error-boundary/ErrorBoundary.tsx index 302ae9e78b..8e64f0b97b 100644 --- a/client/src/error-boundary/ErrorBoundary.tsx +++ b/client/src/error-boundary/ErrorBoundary.tsx @@ -19,10 +19,11 @@ import * as Sentry from "@sentry/react"; import cx from "classnames"; import { ReactNode, useCallback } from "react"; - -import styles from "./ErrorBoundary.module.scss"; -import v2Styles from "../styles/renku_bootstrap.scss?inline"; -import { Helmet } from "react-helmet"; +import { ArrowLeft } from "react-bootstrap-icons"; +import { StyleHandler } from "../index"; +import rkOopsImg from "../styles/assets/oops.svg"; +import rkOopsV2Img from "../styles/assets/oopsV2.svg"; +import useLegacySelector from "../utils/customHooks/useLegacySelector.hook"; interface AppErrorBoundaryProps { children?: ReactNode; @@ -44,41 +45,59 @@ export function AppErrorBoundary({ children }: AppErrorBoundaryProps) { }, []); return ( - + }> {children} ); } function ErrorPage() { + const isV2 = location.pathname.startsWith("/v2"); + const logged = useLegacySelector((state) => state.stateModel.user.logged); return ( <> - - - -
-
-
-

Application Error

-

- Ooops! It looks like we are having some issues! -

+ +
+
+ +

+ It looks like we are having some issues. +

-

- You can try to{" "} - window.location.reload()} - > - reload the page - {" "} - or go to the{" "} - - Renku home page - -

-
+

+ You can try to{" "} + window.location.reload()} + > + Reload the page + {" "} + or{" "} + + + {logged ? "Return to the dashboard" : "Return to home page"} + +

diff --git a/client/src/features/projectsV2/new/GroupNew.tsx b/client/src/features/projectsV2/new/GroupNew.tsx index d664670acf..8018058383 100644 --- a/client/src/features/projectsV2/new/GroupNew.tsx +++ b/client/src/features/projectsV2/new/GroupNew.tsx @@ -30,6 +30,7 @@ import useLegacySelector from "../../../utils/customHooks/useLegacySelector.hook import { slugFromTitle } from "../../../utils/helpers/HelperFunctions"; import { RtkOrNotebooksError } from "../../../components/errors/RtkErrorAlert"; +import LoginAlert from "../../../components/loginAlert/LoginAlert"; import type { GroupPostRequest } from "../api/namespace.api"; import { usePostGroupsMutation } from "../api/projectV2.enhanced-api"; import DescriptionFormField from "../fields/DescriptionFormField"; @@ -164,9 +165,16 @@ function GroupMetadataForm() { export default function GroupNew() { const user = useLegacySelector((state) => state.stateModel.user); if (!user.logged) { + const textIntro = "Only authenticated users can create new groups."; + const textPost = "to create a new group."; return ( -

Please log in to create a group.

+

New group

+
); } diff --git a/client/src/features/projectsV2/new/ProjectV2New.tsx b/client/src/features/projectsV2/new/ProjectV2New.tsx index 4cc9321cce..f5895b7331 100644 --- a/client/src/features/projectsV2/new/ProjectV2New.tsx +++ b/client/src/features/projectsV2/new/ProjectV2New.tsx @@ -16,6 +16,7 @@ * limitations under the License. */ +import cx from "classnames"; import { FormEvent, useCallback, useEffect } from "react"; import { useDispatch } from "react-redux"; import { generatePath, useNavigate } from "react-router-dom-v5-compat"; @@ -30,6 +31,7 @@ import useLegacySelector from "../../../utils/customHooks/useLegacySelector.hook import type { ProjectPost } from "../api/projectV2.api"; import { usePostProjectsMutation } from "../api/projectV2.enhanced-api"; +import LoginAlert from "../../../components/loginAlert/LoginAlert"; import WipBadge from "../shared/WipBadge"; import { ProjectV2DescriptionAndRepositories } from "../show/ProjectV2DescriptionAndRepositories"; import ProjectFormSubmitGroup from "./ProjectV2FormSubmitGroup"; @@ -168,7 +170,18 @@ export default function ProjectV2New() { }, [dispatch]); const { currentStep } = useAppSelector((state) => state.newProjectV2); if (!user.logged) { - return

Please log in to create a project.

; + const textIntro = "Only authenticated users can create new projects."; + const textPost = "to create a new project."; + return ( +
+

New project

+ +
+ ); } return ( (); - const [detailsOpen, setDetailsOpen] = useState(false); - const onClickDetails = useCallback(() => { - setDetailsOpen((open) => !open); - }, []); - const notFoundText = groupSlug ? ( <> We could not find the group{" "} @@ -51,38 +44,35 @@ export default function GroupNotFound({ error }: GroupNotFoundProps) { return ( - - -

Error 404

-

Group not found

- -

{notFoundText}

-

It is possible that the group has been deleted by its owner.

- -
+
+
+

+ + Group not found +

+
+

{notFoundText}

+

It is possible that the group has been deleted by its owner.

+ {error && } - Return to the dashboard + Return to the groups list
- - {error && ( - <> -
- -
- - - - - )} - - +
+
); } diff --git a/client/src/features/projectsV2/notFound/ProjectNotFound.tsx b/client/src/features/projectsV2/notFound/ProjectNotFound.tsx index 3d179800cc..c8b145ff90 100644 --- a/client/src/features/projectsV2/notFound/ProjectNotFound.tsx +++ b/client/src/features/projectsV2/notFound/ProjectNotFound.tsx @@ -19,14 +19,13 @@ import { SerializedError } from "@reduxjs/toolkit"; import { FetchBaseQueryError } from "@reduxjs/toolkit/query"; import cx from "classnames"; -import { useCallback, useState } from "react"; import { ArrowLeft } from "react-bootstrap-icons"; import { Link, useParams } from "react-router-dom-v5-compat"; -import { Button, Col, Collapse, Row } from "reactstrap"; import ContainerWrap from "../../../components/container/ContainerWrap"; import { RtkOrNotebooksError } from "../../../components/errors/RtkErrorAlert"; import { ABSOLUTE_ROUTES } from "../../../routing/routes.constants"; +import rkNotFoundImgV2 from "../../../styles/assets/not-foundV2.svg"; interface ProjectNotFoundProps { error?: FetchBaseQueryError | SerializedError | undefined | null; @@ -43,11 +42,6 @@ export default function ProjectNotFound({ error }: ProjectNotFoundProps) { slug: string; }>(); - const [detailsOpen, setDetailsOpen] = useState(false); - const onClickDetails = useCallback(() => { - setDetailsOpen((open) => !open); - }, []); - const notFoundText = namespace && slug ? ( <> @@ -69,41 +63,38 @@ export default function ProjectNotFound({ error }: ProjectNotFoundProps) { return ( - - -

Error 404

-

Project not found

- -

{notFoundText}

-

- It is possible that the project has been deleted by its owner or you - do not have permission to access it. -

- -
+
+
+

+ + Project not found +

+
+

{notFoundText}

+

+ It is possible that the project has been deleted by its owner or + you do not have permission to access it. +

+ {error && } - Return to the dashboard + Return to the projects list
- - {error && ( - <> -
- -
- - - - - )} - - +
+
); } diff --git a/client/src/features/projectsV2/notFound/UserNotFound.tsx b/client/src/features/projectsV2/notFound/UserNotFound.tsx index 54c354ec54..6781066418 100644 --- a/client/src/features/projectsV2/notFound/UserNotFound.tsx +++ b/client/src/features/projectsV2/notFound/UserNotFound.tsx @@ -19,14 +19,14 @@ import { SerializedError } from "@reduxjs/toolkit"; import { FetchBaseQueryError } from "@reduxjs/toolkit/query"; import cx from "classnames"; -import { useCallback, useState } from "react"; import { ArrowLeft } from "react-bootstrap-icons"; import { Link, useParams } from "react-router-dom-v5-compat"; -import { Button, Col, Collapse, Row } from "reactstrap"; import ContainerWrap from "../../../components/container/ContainerWrap"; import { RtkOrNotebooksError } from "../../../components/errors/RtkErrorAlert"; import { ABSOLUTE_ROUTES } from "../../../routing/routes.constants"; +import rkNotFoundImgV2 from "../../../styles/assets/not-foundV2.svg"; +import useLegacySelector from "../../../utils/customHooks/useLegacySelector.hook.ts"; interface UserNotFoundProps { error?: FetchBaseQueryError | SerializedError | undefined | null; @@ -34,11 +34,7 @@ interface UserNotFoundProps { export default function UserNotFound({ error }: UserNotFoundProps) { const { username } = useParams<{ username: string }>(); - - const [detailsOpen, setDetailsOpen] = useState(false); - const onClickDetails = useCallback(() => { - setDetailsOpen((open) => !open); - }, []); + const logged = useLegacySelector((state) => state.stateModel.user.logged); const notFoundText = username ? ( <> @@ -50,38 +46,36 @@ export default function UserNotFound({ error }: UserNotFoundProps) { ); return ( - - - -

Error 404

-

User not found

- -

{notFoundText}

- -
+ +
+
+

+ + User not found +

+
+

{notFoundText}

+

It is possible that the user has been deleted.

+ {error && } - - Return to the home page + + {logged ? "Return to the dashboard" : "Return to home page"}
- - {error && ( - <> -
- -
- - - - - )} - - +
+
); } diff --git a/client/src/index.jsx b/client/src/index.jsx index 97912c0702..b70ac5c624 100644 --- a/client/src/index.jsx +++ b/client/src/index.jsx @@ -138,7 +138,7 @@ function FeatureFlagHandler() { return null; } -function StyleHandler() { +export function StyleHandler() { return ( diff --git a/client/src/namespace/Namespace.present.jsx b/client/src/namespace/Namespace.present.jsx index 679af09487..602319fda5 100644 --- a/client/src/namespace/Namespace.present.jsx +++ b/client/src/namespace/Namespace.present.jsx @@ -23,13 +23,24 @@ * Namespace presentational components. */ +import { createMemoryHistory } from "history"; import { Link } from "react-router-dom"; import { Col, Row } from "reactstrap"; +import cx from "classnames"; import { ExternalLink } from "../components/ExternalLinks"; import { Loader } from "../components/Loader"; import NotFound from "../not-found/NotFound"; +const fakeHistory = createMemoryHistory({ + initialEntries: ["/"], + initialIndex: 0, +}); +fakeHistory.push({ + pathname: "/projects", + search: "?page=1", +}); + const NamespaceProjects = (props) => { const { namespace } = props; // TODO: I should get the URLs from the redux store: #779 @@ -96,16 +107,27 @@ const NamespaceProjects = (props) => { } return ( - - -

- {userOrGroup} {props.namespace} -

-
 
- {checking} - {outcome} - -
+
+ + +

+ {userOrGroup} {props.namespace} +

+
 
+ {checking} + {outcome} + +
+
); }; diff --git a/client/src/not-found/NotFound.tsx b/client/src/not-found/NotFound.tsx index 8d22bf204e..a55d38defd 100644 --- a/client/src/not-found/NotFound.tsx +++ b/client/src/not-found/NotFound.tsx @@ -25,10 +25,11 @@ import cx from "classnames"; import { ReactNode } from "react"; import { Link } from "react-router-dom"; -import { Button } from "reactstrap"; - -import { HouseFill } from "react-bootstrap-icons"; +import { ArrowLeft } from "react-bootstrap-icons"; +import ContainerWrap from "../components/container/ContainerWrap"; +import rkNotFoundImg from "../styles/assets/not-found.svg"; +import rkNotFoundImgV2 from "../styles/assets/not-foundV2.svg"; import "./NotFound.css"; interface NotFoundProps { @@ -42,6 +43,7 @@ export default function NotFound({ description: description_, children, }: NotFoundProps) { + const isV2 = location.pathname?.startsWith("/v2"); const title = title_ ?? "Page not found"; const description = description_ ?? @@ -53,30 +55,43 @@ export default function NotFound({ descriptionType === "boolean" ? "p" : "div"; + return ( -
-
-
-

404

-

+ +
+
+

+ {title}

{description}
- - + + + Return to home
+ {children == null ? null : ( +
+ {children} +
+ )}
- {children == null ? null : ( -
- {children} -
- )}
-

+ ); } diff --git a/client/src/styles/assets/not-found.svg b/client/src/styles/assets/not-found.svg new file mode 100644 index 0000000000..cbe5c9bd6e --- /dev/null +++ b/client/src/styles/assets/not-found.svg @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/client/src/styles/assets/not-foundV2.svg b/client/src/styles/assets/not-foundV2.svg new file mode 100644 index 0000000000..ff4a369dc5 --- /dev/null +++ b/client/src/styles/assets/not-foundV2.svg @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/client/src/styles/assets/oops.svg b/client/src/styles/assets/oops.svg new file mode 100644 index 0000000000..d2b064c951 --- /dev/null +++ b/client/src/styles/assets/oops.svg @@ -0,0 +1,28 @@ + + + + + + + + + + diff --git a/client/src/styles/assets/oopsV2.svg b/client/src/styles/assets/oopsV2.svg new file mode 100644 index 0000000000..6bb9fbaae0 --- /dev/null +++ b/client/src/styles/assets/oopsV2.svg @@ -0,0 +1,28 @@ + + + + + + + + + + diff --git a/tests/cypress/e2e/adminPage.spec.ts b/tests/cypress/e2e/adminPage.spec.ts index 552b90be57..36c5c44284 100644 --- a/tests/cypress/e2e/adminPage.spec.ts +++ b/tests/cypress/e2e/adminPage.spec.ts @@ -29,8 +29,6 @@ describe("admin page", () => { cy.wait("@getUser"); cy.visit("/admin"); - - cy.contains("404").should("be.visible"); cy.contains("Page not found").should("be.visible"); }); @@ -41,8 +39,6 @@ describe("admin page", () => { cy.wait("@getKeycloakUser"); cy.visit("/admin"); - - cy.contains("404").should("be.visible"); cy.contains("Page not found").should("be.visible"); }); diff --git a/tests/cypress/e2e/groupV2.spec.ts b/tests/cypress/e2e/groupV2.spec.ts index 47772ce80e..3526ef849a 100644 --- a/tests/cypress/e2e/groupV2.spec.ts +++ b/tests/cypress/e2e/groupV2.spec.ts @@ -52,7 +52,9 @@ describe("Add new group -- not logged in", () => { }); it("create a new group", () => { - cy.contains("Please log in to create a group").should("be.visible"); + cy.contains("Only authenticated users can create new groups.").should( + "be.visible" + ); }); }); @@ -198,6 +200,5 @@ describe("Edit v2 group", () => { name: "listGroupV2PostDelete", }); cy.contains("Group with slug test-2-group-v2 does not exist"); - cy.contains("Return to the dashboard").click(); }); }); diff --git a/tests/cypress/e2e/project.spec.ts b/tests/cypress/e2e/project.spec.ts index 392f1e8a85..34920501df 100644 --- a/tests/cypress/e2e/project.spec.ts +++ b/tests/cypress/e2e/project.spec.ts @@ -30,9 +30,6 @@ describe("display a project - not found", () => { cy.visit("/projects/e2e/not-found-test-project"); cy.getDataCy("not-found-title") - .should("be.visible") - .should("contain.text", "404"); - cy.getDataCy("not-found-subtitle") .should("be.visible") .should("contain.text", "Project not found"); cy.getDataCy("not-found-description") @@ -56,9 +53,6 @@ describe("display a project - not found", () => { cy.visit("/projects/e2e/not-found-test-project"); cy.getDataCy("not-found-title") - .should("be.visible") - .should("contain.text", "404"); - cy.getDataCy("not-found-subtitle") .should("be.visible") .should("contain.text", "Project not found"); cy.getDataCy("not-found-description") @@ -80,9 +74,6 @@ describe("display a project - not found", () => { cy.visit("/projects/12345"); cy.getDataCy("not-found-title") - .should("be.visible") - .should("contain.text", "404"); - cy.getDataCy("not-found-subtitle") .should("be.visible") .should("contain.text", "Project not found"); cy.getDataCy("not-found-description") @@ -104,9 +95,6 @@ describe("display a project - not found", () => { cy.visit("/projects/12345"); cy.getDataCy("not-found-title") - .should("be.visible") - .should("contain.text", "404"); - cy.getDataCy("not-found-subtitle") .should("be.visible") .should("contain.text", "Project not found"); cy.getDataCy("not-found-description") diff --git a/tests/cypress/e2e/projectV2.spec.ts b/tests/cypress/e2e/projectV2.spec.ts index 54bf74d2bd..09246db67c 100644 --- a/tests/cypress/e2e/projectV2.spec.ts +++ b/tests/cypress/e2e/projectV2.spec.ts @@ -114,7 +114,9 @@ describe("Add new v2 project -- not logged in", () => { }); it("create a new project", () => { - cy.contains("Please log in to create a project").should("be.visible"); + cy.contains("Only authenticated users can create new projects.").should( + "be.visible" + ); }); });