From 406e7d49b5794ffaa2c16a6f140cf3d82b0e6b22 Mon Sep 17 00:00:00 2001
From: Andrea Cordoba
Date: Thu, 5 Dec 2024 12:48:54 +0100
Subject: [PATCH] feat: add group page design
---
client/src/components/EntityWatermark.tsx | 43 +++
client/src/components/PageNav.tsx | 56 ++++
.../components/DataConnectorsBox.tsx | 48 ++-
.../DataConnectorsBoxListDisplay.tsx | 61 +++-
.../features/groupsV2/LazyGroupContainer.tsx | 30 ++
...roupV2Show.tsx => LazyGroupV2Overview.tsx} | 6 +-
.../members/GroupV2MemberListDisplay.tsx | 72 ++---
.../settings/GroupSettingsMetadata.tsx | 151 ++++++---
.../groupsV2/settings/GroupV2Settings.tsx | 62 ++--
.../groupsV2/show/GroupPageContainer.tsx | 144 +++++++++
.../groupsV2/show/GroupV2Information.tsx | 92 ++++++
.../features/groupsV2/show/GroupV2Show.tsx | 165 ++--------
.../projectsV2/list/ProjectV2ListDisplay.tsx | 289 +++++++++++-------
client/src/features/rootV2/RootV2.tsx | 15 +-
.../src/features/usersV2/show/UserAvatar.tsx | 25 ++
client/src/features/usersV2/show/UserShow.tsx | 4 +-
tests/cypress/e2e/groupV2.spec.ts | 13 +-
17 files changed, 866 insertions(+), 410 deletions(-)
create mode 100644 client/src/components/EntityWatermark.tsx
create mode 100644 client/src/components/PageNav.tsx
create mode 100644 client/src/features/groupsV2/LazyGroupContainer.tsx
rename client/src/features/groupsV2/{LazyGroupV2Show.tsx => LazyGroupV2Overview.tsx} (86%)
create mode 100644 client/src/features/groupsV2/show/GroupPageContainer.tsx
create mode 100644 client/src/features/groupsV2/show/GroupV2Information.tsx
diff --git a/client/src/components/EntityWatermark.tsx b/client/src/components/EntityWatermark.tsx
new file mode 100644
index 000000000..dd3c190d0
--- /dev/null
+++ b/client/src/components/EntityWatermark.tsx
@@ -0,0 +1,43 @@
+/*!
+ * 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 { CSSProperties } from "react";
+import { Folder, People, Person } from "react-bootstrap-icons";
+
+interface EntityWatermarkProps {
+ type: "project" | "user" | "group";
+}
+export function EntityWatermark({ type }: EntityWatermarkProps) {
+ const watermarkStyles: CSSProperties = {
+ right: "0",
+ fontSize: "150px",
+ lineHeight: "0",
+ color: "rgba(0, 0, 0, 0.1)",
+ };
+ return (
+
+ {type === "group" &&
}
+ {type === "user" &&
}
+ {type === "project" &&
}
+
+ );
+}
diff --git a/client/src/components/PageNav.tsx b/client/src/components/PageNav.tsx
new file mode 100644
index 000000000..fb80614e1
--- /dev/null
+++ b/client/src/components/PageNav.tsx
@@ -0,0 +1,56 @@
+/*!
+ * 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 { Eye, Sliders } from "react-bootstrap-icons";
+import { Nav, NavItem } from "reactstrap";
+import RenkuNavLinkV2 from "./RenkuNavLinkV2";
+
+export interface PageNavOptions {
+ overviewUrl: string;
+ settingsUrl: string;
+}
+export default function PageNav({ options }: { options: PageNavOptions }) {
+ return (
+ <>
+
+
+
+
+ Overview
+
+
+
+
+
+ Settings
+
+
+
+ >
+ );
+}
diff --git a/client/src/features/dataConnectorsV2/components/DataConnectorsBox.tsx b/client/src/features/dataConnectorsV2/components/DataConnectorsBox.tsx
index 8b1ed9286..e67a53f56 100644
--- a/client/src/features/dataConnectorsV2/components/DataConnectorsBox.tsx
+++ b/client/src/features/dataConnectorsV2/components/DataConnectorsBox.tsx
@@ -212,16 +212,20 @@ function DataConnectorBoxContent({
totalConnectors={data.total}
/>
- {data.total === 0 && (
-
- Add published datasets from data repositories, and connect to
- cloud storage to read and write custom data.
-
+ {data.total === 0 && namespaceKind === "group" && (
+
+ )}
+ {data.total === 0 && namespaceKind === "user" && (
+
)}
{data.total > 0 && (
{data.dataConnectors?.map((dc) => (
-
+
))}
)}
@@ -320,3 +324,35 @@ function DataConnectorLoadingBoxContent() {
);
}
+
+function AddEmptyListForGroupNamespace({ namespace }: { namespace: string }) {
+ const { permissions } = useGroupPermissions({ groupSlug: namespace });
+
+ return (
+ This group has no visible data connectors.
}
+ enabled={
+
+ Add published datasets from data repositories, and connect to cloud
+ storage to read and write custom data.
+
+ }
+ requestedPermission="write"
+ userPermissions={permissions}
+ />
+ );
+}
+
+function AddEmptyListForUserNamespace({ namespace }: { namespace: string }) {
+ const { data: currentUser } = useGetUserQuery();
+
+ if (currentUser?.isLoggedIn && currentUser.username === namespace) {
+ return (
+
+ Add published datasets from data repositories, and connect to cloud
+ storage to read and write custom data.
+
+ );
+ }
+ return This user has no visible data connectors.
;
+}
diff --git a/client/src/features/dataConnectorsV2/components/DataConnectorsBoxListDisplay.tsx b/client/src/features/dataConnectorsV2/components/DataConnectorsBoxListDisplay.tsx
index 181338226..6a1fcf91d 100644
--- a/client/src/features/dataConnectorsV2/components/DataConnectorsBoxListDisplay.tsx
+++ b/client/src/features/dataConnectorsV2/components/DataConnectorsBoxListDisplay.tsx
@@ -18,11 +18,12 @@
import cx from "classnames";
import { useCallback, useState } from "react";
-import { Globe2, Lock } from "react-bootstrap-icons";
+import { EyeFill, Globe2, Lock, Pencil } from "react-bootstrap-icons";
import { Col, ListGroupItem, Row } from "reactstrap";
import ClampedParagraph from "../../../components/clamped/ClampedParagraph";
import { TimeCaption } from "../../../components/TimeCaption";
+import UserAvatar, { UserAvatarSize } from "../../usersV2/show/UserAvatar.tsx";
import type {
DataConnector,
DataConnectorToProjectLink,
@@ -33,16 +34,20 @@ import DataConnectorView from "./DataConnectorView";
interface DataConnectorBoxListDisplayProps {
dataConnector: DataConnector;
dataConnectorLink?: DataConnectorToProjectLink;
+ extendedPreview?: boolean;
}
export default function DataConnectorBoxListDisplay({
dataConnector,
dataConnectorLink,
+ extendedPreview,
}: DataConnectorBoxListDisplayProps) {
const {
name,
description,
visibility,
creation_date: creationDate,
+ storage,
+ namespace,
} = dataConnector;
const [showDetails, setShowDetails] = useState(false);
@@ -50,6 +55,21 @@ export default function DataConnectorBoxListDisplay({
setShowDetails((open) => !open);
}, []);
+ const type = `${storage?.configuration?.type?.toString() ?? ""} ${
+ storage?.configuration?.provider?.toString() ?? ""
+ }`;
+ const readOnly = storage?.readonly ? (
+
+
+ Read only
+
+ ) : (
+
+ );
+
return (
<>
-
+
{name}
+
{description && {description} }
+ {extendedPreview && {type}
}
-
+
{visibility.toLowerCase() === "private" ? (
- <>
+
Private
- >
+
) : (
- <>
+
Public
- >
+
)}
+ {extendedPreview && readOnly}
import("./show/GroupPageContainer"));
+
+export default function LazyGroupContainer() {
+ return (
+ }>
+
+
+ );
+}
diff --git a/client/src/features/groupsV2/LazyGroupV2Show.tsx b/client/src/features/groupsV2/LazyGroupV2Overview.tsx
similarity index 86%
rename from client/src/features/groupsV2/LazyGroupV2Show.tsx
rename to client/src/features/groupsV2/LazyGroupV2Overview.tsx
index f666626d6..0f2adabcb 100644
--- a/client/src/features/groupsV2/LazyGroupV2Show.tsx
+++ b/client/src/features/groupsV2/LazyGroupV2Overview.tsx
@@ -19,12 +19,12 @@
import { Suspense, lazy } from "react";
import PageLoader from "../../components/PageLoader";
-const GroupV2Show = lazy(() => import("./show/GroupV2Show"));
+const GroupV2Overview = lazy(() => import("./show/GroupV2Show"));
-export default function LazyGroupV2Show() {
+export default function LazyGroupV2Overview() {
return (
}>
-
+
);
}
diff --git a/client/src/features/groupsV2/members/GroupV2MemberListDisplay.tsx b/client/src/features/groupsV2/members/GroupV2MemberListDisplay.tsx
index c0a5e7372..b87f9366a 100644
--- a/client/src/features/groupsV2/members/GroupV2MemberListDisplay.tsx
+++ b/client/src/features/groupsV2/members/GroupV2MemberListDisplay.tsx
@@ -19,8 +19,9 @@
import cx from "classnames";
import { capitalize } from "lodash-es";
import { useMemo } from "react";
+import { People } from "react-bootstrap-icons";
import { Link, generatePath } from "react-router-dom-v5-compat";
-import { ListGroup } from "reactstrap";
+import { Badge } from "reactstrap";
import { Loader } from "../../../components/Loader";
import { RtkOrNotebooksError } from "../../../components/errors/RtkErrorAlert";
@@ -28,7 +29,7 @@ import { ABSOLUTE_ROUTES } from "../../../routing/routes.constants";
import { toSortedMembers } from "../../ProjectPageV2/utils/roleUtils";
import type { ProjectMemberResponse } from "../../projectsV2/api/projectV2.api";
import { useGetGroupsByGroupSlugMembersQuery } from "../../projectsV2/api/projectV2.enhanced-api";
-import UserAvatar from "../../usersV2/show/UserAvatar";
+import { GroupInformationBox } from "../show/GroupV2Information";
interface GroupV2MemberListDisplayProps {
group: string;
@@ -48,30 +49,33 @@ export default function GroupV2MemberListDisplay({
[members]
);
- if (isLoading)
- return (
-
-
-
-
Retrieving group members...
-
-
- );
-
if (error || sortedMembers == null) {
return ;
}
- if (!sortedMembers.length) {
- return There are no members in this group.
;
- }
-
return (
-
+ }
+ title={
+ <>
+ Members
+ {sortedMembers.length ?? 0}
+ >
+ }
+ >
+ {!sortedMembers.length && There are no members in this group.
}
+ {isLoading && (
+
+
+
+
Retrieving group members...
+
+
+ )}
{sortedMembers?.map((member) => (
))}
-
+
);
}
@@ -95,32 +99,22 @@ function GroupV2Member({ member }: GroupV2MemberProps) {
return (
<>
-
+
-
-
- {name ?? "Unknown user"} {" "}
-
-
{`@${username}`}
+
+ {name ?? "Unknown user"} ({capitalize(role)})
+
-
{capitalize(role)}
>
diff --git a/client/src/features/groupsV2/settings/GroupSettingsMetadata.tsx b/client/src/features/groupsV2/settings/GroupSettingsMetadata.tsx
index 5095e2650..97275710b 100644
--- a/client/src/features/groupsV2/settings/GroupSettingsMetadata.tsx
+++ b/client/src/features/groupsV2/settings/GroupSettingsMetadata.tsx
@@ -17,7 +17,7 @@
*/
import cx from "classnames";
-import { useCallback, useEffect, useState } from "react";
+import { useCallback, useContext, useEffect, useState } from "react";
import { CheckLg, Pencil, XLg } from "react-bootstrap-icons";
import { useForm } from "react-hook-form";
import { generatePath, useNavigate } from "react-router-dom-v5-compat";
@@ -25,6 +25,7 @@ import {
Button,
Form,
Input,
+ Label,
Modal,
ModalBody,
ModalFooter,
@@ -33,6 +34,8 @@ import {
import { RtkOrNotebooksError } from "../../../components/errors/RtkErrorAlert";
import { Loader } from "../../../components/Loader";
import { ABSOLUTE_ROUTES } from "../../../routing/routes.constants";
+import AppContext from "../../../utils/context/appContext.ts";
+import PermissionsGuard from "../../permissionsV2/PermissionsGuard.tsx";
import type {
GroupPatchRequest,
GroupResponse,
@@ -44,6 +47,7 @@ import {
import DescriptionFormField from "../../projectsV2/fields/DescriptionFormField";
import NameFormField from "../../projectsV2/fields/NameFormField";
import SlugFormField from "../../projectsV2/fields/SlugFormField";
+import useGroupPermissions from "../utils/useGroupPermissions.hook.ts";
type GroupMetadata = Omit
;
@@ -58,7 +62,9 @@ function GroupDeleteConfirmation({
toggle,
group,
}: GroupDeleteConfirmationProps) {
+ const navigate = useNavigate();
const [deleteGroup, result] = useDeleteGroupsByGroupSlugMutation();
+ const { notifications } = useContext(AppContext);
const onDelete = useCallback(() => {
deleteGroup({ groupSlug: group.slug });
}, [deleteGroup, group.slug]);
@@ -71,10 +77,15 @@ function GroupDeleteConfirmation({
);
useEffect(() => {
- if (result.isSuccess || result.isError) {
- toggle();
+ if (result.isError)
+ notifications?.addError(`Error deleting the group ${group.name}`);
+ if (result.isSuccess) {
+ notifications?.addSuccess(
+ `Group ${group.name} has been successfully deleted.`
+ );
+ navigate(generatePath(ABSOLUTE_ROUTES.v2.root));
}
- }, [result.isError, result.isSuccess, toggle]);
+ }, [result.isError, result.isSuccess, notifications, group.name, navigate]);
return (
@@ -131,6 +142,7 @@ export default function GroupMetadataForm({ group }: GroupMetadataFormProps) {
slug: group.slug ?? "",
},
});
+ const permissions = useGroupPermissions({ groupSlug: group.slug });
const [updateGroup, updateGroupResult] = usePatchGroupsByGroupSlugMutation();
@@ -165,42 +177,113 @@ export default function GroupMetadataForm({ group }: GroupMetadataFormProps) {
)}
);
}
+function GroupReadOnlyField({
+ title,
+ value,
+}: {
+ title: string;
+ value: string;
+}) {
+ return (
+
+
+ {title}
+
+
+
+ );
+}
diff --git a/client/src/features/groupsV2/settings/GroupV2Settings.tsx b/client/src/features/groupsV2/settings/GroupV2Settings.tsx
index d9f324d98..7f16e99af 100644
--- a/client/src/features/groupsV2/settings/GroupV2Settings.tsx
+++ b/client/src/features/groupsV2/settings/GroupV2Settings.tsx
@@ -18,14 +18,12 @@
import { skipToken } from "@reduxjs/toolkit/query";
import cx from "classnames";
-import { ArrowLeft, Sliders } from "react-bootstrap-icons";
-import { Link, generatePath, useParams } from "react-router-dom-v5-compat";
+import { Sliders } from "react-bootstrap-icons";
+import { useParams } from "react-router-dom-v5-compat";
import { Card, CardBody, CardHeader } from "reactstrap";
import { Loader } from "../../../components/Loader";
-import ContainerWrap from "../../../components/container/ContainerWrap";
import LazyNotFound from "../../../not-found/LazyNotFound";
-import { ABSOLUTE_ROUTES } from "../../../routing/routes.constants";
import { useGetGroupsByGroupSlugQuery } from "../../projectsV2/api/projectV2.enhanced-api";
import GroupNotFound from "../../projectsV2/notFound/GroupNotFound";
import GroupSettingsMembers from "./GroupSettingsMembers";
@@ -53,42 +51,26 @@ export default function GroupV2Settings() {
}
return (
-
-
-
-
{group.name ?? "Unknown group"}
-
-
+
+
+
+
+
+
+ General settings
+
+
+
+
+
+
+
-
-
-
-
-
- General settings
-
-
-
-
-
-
-
-
-
-
-
+
+
);
}
diff --git a/client/src/features/groupsV2/show/GroupPageContainer.tsx b/client/src/features/groupsV2/show/GroupPageContainer.tsx
new file mode 100644
index 000000000..35380c579
--- /dev/null
+++ b/client/src/features/groupsV2/show/GroupPageContainer.tsx
@@ -0,0 +1,144 @@
+/*!
+ * 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 { skipToken } from "@reduxjs/toolkit/query";
+import cx from "classnames";
+import { useEffect } from "react";
+import {
+ generatePath,
+ Outlet,
+ useNavigate,
+ useOutletContext,
+ useParams,
+} from "react-router-dom-v5-compat";
+import { Col, Row } from "reactstrap";
+import ContainerWrap from "../../../components/container/ContainerWrap.tsx";
+import { EntityWatermark } from "../../../components/EntityWatermark.tsx";
+import { Loader } from "../../../components/Loader.tsx";
+import PageNav, { PageNavOptions } from "../../../components/PageNav.tsx";
+import LazyNotFound from "../../../not-found/LazyNotFound.tsx";
+import { ABSOLUTE_ROUTES } from "../../../routing/routes.constants.ts";
+import { GroupResponse } from "../../projectsV2/api/namespace.api.ts";
+import {
+ useGetGroupsByGroupSlugQuery,
+ useGetNamespacesByNamespaceSlugQuery,
+} from "../../projectsV2/api/projectV2.enhanced-api.ts";
+import GroupNotFound from "../../projectsV2/notFound/GroupNotFound.tsx";
+import UserAvatar, {
+ AvatarTypeWrap,
+ UserAvatarSize,
+} from "../../usersV2/show/UserAvatar.tsx";
+
+export default function GroupPageContainer() {
+ const { slug } = useParams<{ slug: string }>();
+
+ const navigate = useNavigate();
+
+ const {
+ data: namespace,
+ isLoading: isLoadingNamespace,
+ error: namespaceError,
+ } = useGetNamespacesByNamespaceSlugQuery(
+ slug ? { namespaceSlug: slug } : skipToken
+ );
+ const {
+ data: group,
+ isLoading: isLoadingGroup,
+ error: groupError,
+ } = useGetGroupsByGroupSlugQuery(slug ? { groupSlug: slug } : skipToken);
+
+ const isLoading = isLoadingNamespace || isLoadingGroup;
+ const error = namespaceError ?? groupError;
+
+ useEffect(() => {
+ if (slug && namespace?.namespace_kind === "user") {
+ navigate(
+ generatePath(ABSOLUTE_ROUTES.v2.users.show, { username: slug }),
+ {
+ replace: true,
+ }
+ );
+ }
+ }, [namespace?.namespace_kind, navigate, slug]);
+
+ if (!slug) {
+ return ;
+ }
+
+ if (isLoading) {
+ return ;
+ }
+
+ if (error || !namespace || !group) {
+ return ;
+ }
+
+ const options: PageNavOptions = {
+ overviewUrl: generatePath(ABSOLUTE_ROUTES.v2.groups.show.root, {
+ slug: group.slug,
+ }),
+ settingsUrl: generatePath(ABSOLUTE_ROUTES.v2.groups.show.settings, {
+ slug: group.slug,
+ }),
+ };
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+type ContextType = { group: GroupResponse };
+export function useGroup() {
+ return useOutletContext();
+}
+
+function GroupHeader({ group, slug }: { group: GroupResponse; slug: string }) {
+ return (
+
+
+
+
+
+
+
{group.name ?? "Unknown group"}
+ {group.description && (
+
+ )}
+
+
+
+ );
+}
diff --git a/client/src/features/groupsV2/show/GroupV2Information.tsx b/client/src/features/groupsV2/show/GroupV2Information.tsx
new file mode 100644
index 000000000..50ee0f611
--- /dev/null
+++ b/client/src/features/groupsV2/show/GroupV2Information.tsx
@@ -0,0 +1,92 @@
+/*!
+ * 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 { Clock, InfoCircle, JournalAlbum } from "react-bootstrap-icons";
+import { Card, CardBody, CardHeader } from "reactstrap";
+import { TimeCaption } from "../../../components/TimeCaption.tsx";
+import GroupV2MemberListDisplay from "../members/GroupV2MemberListDisplay.tsx";
+import { useGroup } from "./GroupPageContainer.tsx";
+
+interface GroupInformationProps {
+ output?: "plain" | "card";
+}
+export default function GroupInformation({
+ output = "plain",
+}: GroupInformationProps) {
+ const { group } = useGroup();
+
+ const information = (
+
+
}
+ title="Identifier:"
+ >
+
@{group.slug}
+
+
} title="Created:">
+
+
+
+
+
+
+ );
+ return output === "plain" ? (
+ information
+ ) : (
+
+
+
+
+
+ Info
+
+
+
+ {information}
+
+ );
+}
+
+interface GroupInformationBoxProps {
+ children: React.ReactNode;
+ icon: React.ReactNode;
+ title: React.ReactNode;
+}
+export function GroupInformationBox({
+ children,
+ icon,
+ title,
+}: GroupInformationBoxProps) {
+ return (
+
+
+ {icon}
+ {title}
+
+
{children}
+
+ );
+}
diff --git a/client/src/features/groupsV2/show/GroupV2Show.tsx b/client/src/features/groupsV2/show/GroupV2Show.tsx
index 5205e7d80..cebb1d3f9 100644
--- a/client/src/features/groupsV2/show/GroupV2Show.tsx
+++ b/client/src/features/groupsV2/show/GroupV2Show.tsx
@@ -16,163 +16,38 @@
* limitations under the License.
*/
-import { skipToken } from "@reduxjs/toolkit/query";
-import cx from "classnames";
-import { useEffect } from "react";
-import { Pencil } from "react-bootstrap-icons";
-import {
- Link,
- generatePath,
- useNavigate,
- useParams,
-} from "react-router-dom-v5-compat";
import { Col, Row } from "reactstrap";
-
-import { Loader } from "../../../components/Loader";
-import ContainerWrap from "../../../components/container/ContainerWrap";
-import LazyNotFound from "../../../not-found/LazyNotFound";
-import { ABSOLUTE_ROUTES } from "../../../routing/routes.constants";
-
import DataConnectorsBox from "../../dataConnectorsV2/components/DataConnectorsBox";
-import PermissionsGuard from "../../permissionsV2/PermissionsGuard";
-import type { GroupResponse } from "../../projectsV2/api/namespace.api";
-import {
- useGetGroupsByGroupSlugQuery,
- useGetNamespacesByNamespaceSlugQuery,
-} from "../../projectsV2/api/projectV2.enhanced-api";
import ProjectV2ListDisplay from "../../projectsV2/list/ProjectV2ListDisplay";
-import GroupNotFound from "../../projectsV2/notFound/GroupNotFound";
-import { EntityPill } from "../../searchV2/components/SearchV2Results";
-import UserAvatar, { UserAvatarSize } from "../../usersV2/show/UserAvatar";
-import GroupV2MemberListDisplay from "../members/GroupV2MemberListDisplay";
-import useGroupPermissions from "../utils/useGroupPermissions.hook";
+import { useGroup } from "./GroupPageContainer.tsx";
+import GroupInformation from "./GroupV2Information.tsx";
export default function GroupV2Show() {
- const { slug } = useParams<{ slug: string }>();
-
- const navigate = useNavigate();
-
- const {
- data: namespace,
- isLoading: isLoadingNamespace,
- error: namespaceError,
- } = useGetNamespacesByNamespaceSlugQuery(
- slug ? { namespaceSlug: slug } : skipToken
- );
- const {
- data: group,
- isLoading: isLoadingGroup,
- error: groupError,
- } = useGetGroupsByGroupSlugQuery(slug ? { groupSlug: slug } : skipToken);
-
- const isLoading = isLoadingNamespace || isLoadingGroup;
- const error = namespaceError ?? groupError;
-
- useEffect(() => {
- if (slug && namespace?.namespace_kind === "user") {
- navigate(
- generatePath(ABSOLUTE_ROUTES.v2.users.show, { username: slug }),
- {
- replace: true,
- }
- );
- }
- }, [namespace?.namespace_kind, navigate, slug]);
-
- if (!slug) {
- return ;
- }
-
- if (isLoading) {
- return ;
- }
-
- if (error || !namespace || !group) {
- return ;
- }
+ const { group } = useGroup();
return (
-
-
-
-
-
-
-
{group.name ?? "Unknown group"}
-
-
-
-
-
-
{`@${slug}`}
-
-
-
-
-
-
- {group.description && (
-
- )}
-
-
-
- Group Projects
- No visible projects.}
- />
-
-
-
-
- );
-}
-
-interface GroupSettingsButtonProps {
- group: GroupResponse;
-}
-
-function GroupSettingsButton({ group }: GroupSettingsButtonProps) {
- const { permissions } = useGroupPermissions({ groupSlug: group.slug });
-
- return (
-
-
- Edit settings
-
- }
- disabled={null}
- requestedPermission="write"
- userPermissions={permissions}
- />
+
+
+
+
+
);
}
diff --git a/client/src/features/projectsV2/list/ProjectV2ListDisplay.tsx b/client/src/features/projectsV2/list/ProjectV2ListDisplay.tsx
index 7787793e5..ea4b36d34 100644
--- a/client/src/features/projectsV2/list/ProjectV2ListDisplay.tsx
+++ b/client/src/features/projectsV2/list/ProjectV2ListDisplay.tsx
@@ -17,42 +17,36 @@
*/
import cx from "classnames";
-import { ReactNode, useCallback, useEffect, useMemo } from "react";
-import { Globe2, Lock } from "react-bootstrap-icons";
-import {
- Link,
- generatePath,
- useSearchParams,
-} from "react-router-dom-v5-compat";
-import { Card, CardBody, Col, Row } from "reactstrap";
+import { useCallback, useEffect, useMemo } from "react";
+import { Folder, PlusLg } from "react-bootstrap-icons";
+import { Link, useSearchParams } from "react-router-dom-v5-compat";
+import { Badge, Card, CardBody, CardHeader, ListGroup } from "reactstrap";
import { Loader } from "../../../components/Loader";
import Pagination from "../../../components/Pagination";
-import { TimeCaption } from "../../../components/TimeCaption";
import { RtkOrNotebooksError } from "../../../components/errors/RtkErrorAlert";
-import { ABSOLUTE_ROUTES } from "../../../routing/routes.constants";
-import type { Project } from "../api/projectV2.api";
-import {
- useGetNamespacesByNamespaceSlugQuery,
- useGetProjectsQuery,
-} from "../api/projectV2.enhanced-api";
-import ClampedParagraph from "../../../components/clamped/ClampedParagraph";
-
-const DEFAULT_PER_PAGE = 12;
+import useGroupPermissions from "../../groupsV2/utils/useGroupPermissions.hook.ts";
+import PermissionsGuard from "../../permissionsV2/PermissionsGuard.tsx";
+import { useGetUserQuery } from "../../usersV2/api/users.api.ts";
+import { NamespaceKind } from "../api/namespace.api.ts";
+import { useGetProjectsQuery } from "../api/projectV2.enhanced-api";
+import ProjectShortHandDisplay from "../show/ProjectShortHandDisplay.tsx";
+
+const DEFAULT_PER_PAGE = 5;
const DEFAULT_PAGE_PARAM = "page";
interface ProjectListDisplayProps {
namespace?: string;
pageParam?: string;
perPage?: number;
- emptyListElement?: ReactNode;
+ namespaceKind: NamespaceKind;
}
export default function ProjectListDisplay({
namespace: ns,
pageParam: pageParam_,
perPage: perPage_,
- emptyListElement,
+ namespaceKind,
}: ProjectListDisplayProps) {
const pageParam = useMemo(
() => (pageParam_ ? pageParam_ : DEFAULT_PAGE_PARAM),
@@ -115,120 +109,179 @@ export default function ProjectListDisplay({
}
}, [data?.totalPages, page, pageParam, setSearchParams]);
- if (isLoading)
- return (
-
-
-
-
Retrieving projects...
-
-
- );
-
if (error || data == null) {
return ;
}
- if (!data.total) {
- return emptyListElement ?? The project list is empty.
;
- }
+ const emptyListElement =
+ namespaceKind === "group" ? (
+
+ ) : (
+
+ );
return (
-
- {data.projects?.map((project) => (
-
- ))}
-
-
+
+
+ {isLoading && (
+
+
+
+
Retrieving projects...
+
+
+ )}
+ {!data.total && emptyListElement}
+ {data.projects.length > 0 && (
+ <>
+
+
+ {data?.projects?.map((project) => (
+
+ ))}
+
+
+
+ >
+ )}
+
+
+
+ );
+}
+
+interface ProjectBoxHeaderProps {
+ totalProjects: number;
+ namespace: string;
+ namespaceKind: NamespaceKind;
+}
+function ProjectBoxHeader({
+ totalProjects,
+ namespaceKind,
+ namespace,
+}: ProjectBoxHeaderProps) {
+ return (
+
+
-
+ >
+
+
+
+ Projects
+
+ {totalProjects}
+
+ {namespaceKind === "group" && (
+
+ )}
+ {namespaceKind === "user" && (
+
+ )}
+
+
);
}
-interface ProjectV2ListProjectProps {
- project: Project;
+function AddButtonForGroupNamespace({ namespace }: { namespace: string }) {
+ const { permissions } = useGroupPermissions({ groupSlug: namespace });
+
+ return (
+
+
+
+ }
+ requestedPermission="write"
+ userPermissions={permissions}
+ />
+ );
}
-function ProjectV2ListProject({ project }: ProjectV2ListProjectProps) {
- const { data: namespaceData } = useGetNamespacesByNamespaceSlugQuery({
- namespaceSlug: project.namespace,
- });
- const {
- name,
- namespace,
- description,
- visibility,
- creation_date: creationDate,
- } = project;
-
- const projectUrl = generatePath(ABSOLUTE_ROUTES.v2.projects.show.root, {
- namespace: project.namespace,
- slug: project.slug,
- });
- const namespaceUrl =
- namespaceData && namespaceData.namespace_kind === "group"
- ? generatePath(ABSOLUTE_ROUTES.v2.groups.show.root, { slug: namespace })
- : generatePath(ABSOLUTE_ROUTES.v2.users.show, {
- username: project.namespace,
- });
+function AddButtonForUserNamespace({ namespace }: { namespace: string }) {
+ const { data: currentUser } = useGetUserQuery();
+
+ if (currentUser?.isLoggedIn && currentUser.username === namespace) {
+ return (
+
+
+
+ );
+ }
+ return null;
+}
+
+function AddEmptyListForGroupNamespace({ namespace }: { namespace: string }) {
+ const { permissions } = useGroupPermissions({ groupSlug: namespace });
return (
-
-
-
-
- {name}
-
-
-
- {"@"}
- {namespace}
-
-
- {description && {description} }
-
-
- {visibility.toLowerCase() === "private" ? (
- <>
-
- Private
- >
- ) : (
- <>
-
- Public
- >
- )}
-
-
-
-
-
-
+ This group has no visible projects.}
+ enabled={
+
+ Collaborate on projects with anyone, with data, code, and compute
+ together in one place.
+
+ }
+ requestedPermission="write"
+ userPermissions={permissions}
+ />
);
}
+
+function AddEmptyListForUserNamespace({ namespace }: { namespace: string }) {
+ const { data: currentUser } = useGetUserQuery();
+
+ if (currentUser?.isLoggedIn && currentUser.username === namespace) {
+ return (
+
+ Collaborate on projects with anyone, with data, code, and compute
+ together in one place.
+
+ );
+ }
+ return This user has no visible personal projects.
;
+}
diff --git a/client/src/features/rootV2/RootV2.tsx b/client/src/features/rootV2/RootV2.tsx
index 19b8a5620..617ac28ab 100644
--- a/client/src/features/rootV2/RootV2.tsx
+++ b/client/src/features/rootV2/RootV2.tsx
@@ -26,6 +26,7 @@ import { RELATIVE_ROUTES } from "../../routing/routes.constants";
import useAppDispatch from "../../utils/customHooks/useAppDispatch.hook";
import useAppSelector from "../../utils/customHooks/useAppSelector.hook";
import { setFlag } from "../../utils/feature-flags/featureFlags.slice";
+import LazyGroupContainer from "../groupsV2/LazyGroupContainer.tsx";
import LazyProjectPageV2Show from "../ProjectPageV2/LazyProjectPageV2Show";
import LazyProjectPageOverview from "../ProjectPageV2/ProjectPageContent/LazyProjectPageOverview";
@@ -33,7 +34,7 @@ import LazyProjectPageSettings from "../ProjectPageV2/ProjectPageContent/LazyPro
import LazyConnectedServicesPage from "../connectedServices/LazyConnectedServicesPage";
import LazyDashboardV2 from "../dashboardV2/LazyDashboardV2";
import LazyHelpV2 from "../dashboardV2/LazyHelpV2";
-import LazyGroupV2Show from "../groupsV2/LazyGroupV2Show";
+import LazyGroupV2Overview from "../groupsV2/LazyGroupV2Overview.tsx";
import LazyGroupV2New from "../projectsV2/LazyGroupNew";
import LazyProjectV2New from "../projectsV2/LazyProjectV2New";
import LazyProjectV2ShowByProjectId from "../projectsV2/LazyProjectV2ShowByProjectId";
@@ -143,11 +144,13 @@ function GroupsV2Routes() {
element={ }
/>
- } />
- }
- />
+ }>
+ } />
+ }
+ />
+
acc + char.charCodeAt(0), 0);
}
+
+interface AvatarType {
+ type: "User" | "Group";
+ children: ReactNode;
+}
+export function AvatarTypeWrap({ type, children }: AvatarType) {
+ const styles: CSSProperties = {
+ width: "75px",
+ height: "65px",
+ };
+
+ return (
+
+ );
+}
diff --git a/client/src/features/usersV2/show/UserShow.tsx b/client/src/features/usersV2/show/UserShow.tsx
index 9801e88c2..3c9cd67e6 100644
--- a/client/src/features/usersV2/show/UserShow.tsx
+++ b/client/src/features/usersV2/show/UserShow.tsx
@@ -128,9 +128,7 @@ export default function UserShow() {
{name ?? username} has no visible personal projects.
- }
+ namespaceKind="user"
/>
diff --git a/tests/cypress/e2e/groupV2.spec.ts b/tests/cypress/e2e/groupV2.spec.ts
index d98ae4c3d..bafd9b08d 100644
--- a/tests/cypress/e2e/groupV2.spec.ts
+++ b/tests/cypress/e2e/groupV2.spec.ts
@@ -122,8 +122,7 @@ describe("Edit v2 group", () => {
cy.contains("test 2 group-v2").should("be.visible").click();
cy.wait("@readGroupV2");
cy.contains("test 2 group-v2").should("be.visible");
- cy.wait("@getGroupV2Permissions");
- cy.contains("Edit settings").should("be.visible").click();
+ cy.getDataCy("nav-link-settings").should("be.visible").click();
cy.getDataCy("group-name-input").clear().type("new name");
cy.getDataCy("group-slug-input").clear().type("new-slug");
cy.getDataCy("group-description-input").clear().type("new description");
@@ -159,8 +158,7 @@ describe("Edit v2 group", () => {
cy.contains("test 2 group-v2").should("be.visible").click();
cy.wait("@readGroupV2");
cy.contains("test 2 group-v2").should("be.visible");
- cy.wait("@getGroupV2Permissions");
- cy.contains("Edit settings").should("be.visible").click();
+ cy.getDataCy("nav-link-settings").should("be.visible").click();
cy.contains("@user1").should("be.visible");
cy.contains("user3-uuid").should("be.visible");
fixtures
@@ -206,8 +204,7 @@ describe("Edit v2 group", () => {
cy.contains("test 2 group-v2").should("be.visible").click();
cy.wait("@readGroupV2");
cy.contains("test 2 group-v2").should("be.visible");
- cy.wait("@getGroupV2Permissions");
- cy.contains("Edit settings").should("be.visible").click();
+ cy.getDataCy("nav-link-settings").should("be.visible").click();
cy.getDataCy("group-description-input").clear().type("new description");
cy.get("button").contains("Delete").should("be.visible").click();
cy.get("button")
@@ -225,7 +222,9 @@ describe("Edit v2 group", () => {
fixture: "groupV2/list-groupV2-post-delete.json",
name: "listGroupV2PostDelete",
});
- cy.contains("Group with slug test-2-group-v2 does not exist");
+ cy.contains("Group test 2 group-v2 has been successfully deleted.").should(
+ "be.visible"
+ );
});
});