diff --git a/airbyte-webapp/src/components/InitialBadge/InitialBadge.module.scss b/airbyte-webapp/src/components/InitialBadge/InitialBadge.module.scss new file mode 100644 index 00000000000..5df450252b8 --- /dev/null +++ b/airbyte-webapp/src/components/InitialBadge/InitialBadge.module.scss @@ -0,0 +1,51 @@ +@use "scss/colors"; +@use "scss/variables"; + +.green900 { + background-color: colors.$green-900; +} + +.green600 { + background-color: colors.$green-600; +} + +.green300 { + background-color: colors.$green-300; +} + +.darkBlue200 { + background-color: colors.$dark-blue-200; +} + +.blue900 { + background-color: colors.$blue-900; +} + +.orange900 { + background-color: colors.$orange-900; +} + +.orange300 { + background-color: colors.$orange-300; +} + +.blue200 { + background-color: colors.$blue-200; +} + +.blue400 { + background-color: colors.$blue-400; +} + +.darkBlue400 { + background-color: colors.$dark-blue-400; +} + +.initialBadge__container { + display: flex; + justify-content: center; + align-items: center; + width: 24px; + height: 24px; + border-radius: variables.$border-radius-xs; +} diff --git a/airbyte-webapp/src/components/InitialBadge/InitialBadge.stories.tsx b/airbyte-webapp/src/components/InitialBadge/InitialBadge.stories.tsx new file mode 100644 index 00000000000..c5c2f79c1e1 --- /dev/null +++ b/airbyte-webapp/src/components/InitialBadge/InitialBadge.stories.tsx @@ -0,0 +1,22 @@ +import { StoryObj } from "@storybook/react"; + +import { InitialBadge } from "./InitialBadge"; + +export default { + title: "ui/InitialBadge", + component: InitialBadge, + argTypes: { + inputString: { + control: { + type: "text", + }, + }, + }, +} as StoryObj; + +export const Default: StoryObj = { + args: { + inputString: "Octavia Squidington", + }, + render: (args) => , +}; diff --git a/airbyte-webapp/src/components/InitialBadge/InitialBadge.tsx b/airbyte-webapp/src/components/InitialBadge/InitialBadge.tsx new file mode 100644 index 00000000000..a01641ae488 --- /dev/null +++ b/airbyte-webapp/src/components/InitialBadge/InitialBadge.tsx @@ -0,0 +1,68 @@ +import classNames from "classnames"; + +import { Text } from "components/ui/Text"; + +import styles from "./InitialBadge.module.scss"; + +interface InitialBadgeProps { + inputString: string; + hashingString: string; +} +type ColorOptions = + | "green900" + | "green600" + | "green300" + | "darkBlue200" + | "blue900" + | "orange900" + | "orange300" + | "blue200" + | "blue400" + | "darkBlue400"; + +export const InitialBadge: React.FC = ({ inputString, hashingString: additionalString }) => { + const initials = inputString + .split(" ") + .filter(Boolean) + .map((word) => word[0].toLocaleUpperCase()) + .slice(0, 2) + .join("\u200b"); // zero-width character to prevent characters from forming ligatures in certain scripts + + const colorOptions: ColorOptions[] = [ + "green900", + "green600", + "green300", + "darkBlue200", + "blue900", + "orange900", + "orange300", + "blue200", + "blue400", + "darkBlue400", + ]; + const colorStyles = { + green900: styles.green900, + green600: styles.green600, + green300: styles.green300, + darkBlue200: styles.darkBlue200, + blue900: styles.blue900, + orange900: styles.orange900, + orange300: styles.orange300, + blue200: styles.blue200, + blue400: styles.blue400, + darkBlue400: styles.darkBlue400, + }; + + const randomColor = + colorOptions[ + (additionalString ?? "").split("").reduce((sum, char) => sum + char.charCodeAt(0), 0) % colorOptions.length + ]; + + return ( + + + {initials} + + + ); +}; diff --git a/airbyte-webapp/src/locales/en.json b/airbyte-webapp/src/locales/en.json index b8b6cf5a05b..17caa33742b 100644 --- a/airbyte-webapp/src/locales/en.json +++ b/airbyte-webapp/src/locales/en.json @@ -828,13 +828,14 @@ "settings.defaultDataResidencyDescription": "Choose the default preferred data processing location for all of your connections. The default data residency setting only affects new connections. Existing connections will retain their data residency setting. Learn more.", "settings.defaultDataResidencyUpdateError": "There was an error updating the default data residency for this workspace.", "settings.defaultDataResidencyUpdateSuccess": "Data residency preference has been updated!", - "settings.accessManagement": "Access management", + "settings.accessManagement": "Access Management", "settings.accessManagement.resource.description": "The following users have access to {resourceName}:", "settings.accessManagement.noUsers": "No users have this level of permission", "settings.accessManagement.organization": "Organization users", "settings.accessManagement.workspace": "Workspace users", "settings.accessManagement.instance": "Instance users", "settings.accessManagement.table.column.fullname": "Full Name", + "settings.accessManagement.table.column.member": "Member", "settings.accessManagement.table.column.email": "Email", "settings.accessManagement.table.column.role": "Role", "settings.accessManagement.table.column.action": "Action", @@ -1541,7 +1542,6 @@ "settings.integrationSettings.dbtCloudSettings.form.description": "To use the dbt Cloud integration, enter your service token here. Learn more.", "settings.generalSettings.changeWorkspace": "Change Workspace", - "settings.accessManagementSettings": "Access Management", "userSettings.table.title": "Current users", "userSettings.table.column.fullname": "Full Name", "userSettings.table.column.email": "Email", diff --git a/airbyte-webapp/src/packages/cloud/cloudRoutes.tsx b/airbyte-webapp/src/packages/cloud/cloudRoutes.tsx index 91a2dda6a9d..4b0b14cb416 100644 --- a/airbyte-webapp/src/packages/cloud/cloudRoutes.tsx +++ b/airbyte-webapp/src/packages/cloud/cloudRoutes.tsx @@ -68,12 +68,19 @@ const SourceConnectionsPage = React.lazy(() => import("pages/source/SourceConnec const SourceSettingsPage = React.lazy(() => import("pages/source/SourceSettingsPage")); const CloudDefaultView = React.lazy(() => import("./views/CloudDefaultView")); const CloudSettingsPage = React.lazy(() => import("./views/settings/CloudSettingsPage")); +const NextOrganizationAccessManagementPage = React.lazy( + () => import("pages/SettingsPage/pages/AccessManagementPage/NextOrganizationAccessManagementPage") +); +const NextWorkspaceAccessManagementPage = React.lazy( + () => import("pages/SettingsPage/pages/AccessManagementPage/NextWorkspaceAccessManagementPage") +); const MainRoutes: React.FC = () => { const workspace = useCurrentWorkspace(); const organization = useCurrentOrganizationInfo(); const isSsoEnabled = organization?.sso; const isTokenManagementEnabled = useExperiment("settings.token-management-ui", false); + const isUpdatedOrganizationsUi = useExperiment("settings.organizationsUpdates", false); const analyticsContext = useMemo( () => ({ @@ -122,7 +129,17 @@ const MainRoutes: React.FC = () => { } /> : } + element={ + isSsoEnabled ? ( + isUpdatedOrganizationsUi ? ( + + ) : ( + + ) + ) : ( + + ) + } /> } /> {supportsCloudDbtIntegration && ( @@ -134,7 +151,13 @@ const MainRoutes: React.FC = () => { {isSsoEnabled && ( } + element={ + isUpdatedOrganizationsUi ? ( + + ) : ( + + ) + } /> )} diff --git a/airbyte-webapp/src/packages/cloud/views/settings/CloudSettingsPage.tsx b/airbyte-webapp/src/packages/cloud/views/settings/CloudSettingsPage.tsx index 8709f4d036c..ee546af5f87 100644 --- a/airbyte-webapp/src/packages/cloud/views/settings/CloudSettingsPage.tsx +++ b/airbyte-webapp/src/packages/cloud/views/settings/CloudSettingsPage.tsx @@ -2,9 +2,8 @@ import React, { Suspense } from "react"; import { FormattedMessage, useIntl } from "react-intl"; import { Outlet } from "react-router-dom"; -import { LoadingPage } from "components"; +import { LoadingPage, MainPageWithScroll } from "components"; import { HeadTitle } from "components/common/HeadTitle"; -import { MainPageWithScroll } from "components/common/MainPageWithScroll"; import { SettingsButton, SettingsLink, @@ -95,7 +94,7 @@ export const CloudSettingsPage: React.FC = () => { /> {isSsoEnabled && ( )} diff --git a/airbyte-webapp/src/pages/SettingsPage/pages/AccessManagementPage/NextOrganizationAccessManagementPage.tsx b/airbyte-webapp/src/pages/SettingsPage/pages/AccessManagementPage/NextOrganizationAccessManagementPage.tsx new file mode 100644 index 00000000000..1719db6977f --- /dev/null +++ b/airbyte-webapp/src/pages/SettingsPage/pages/AccessManagementPage/NextOrganizationAccessManagementPage.tsx @@ -0,0 +1,38 @@ +import React from "react"; +import { FormattedMessage } from "react-intl"; + +import { Box } from "components/ui/Box"; +import { FlexContainer } from "components/ui/Flex"; +import { Heading } from "components/ui/Heading"; +import { Text } from "components/ui/Text"; + +import { useCurrentWorkspace, useListUsersInOrganization } from "core/api"; +import { useTrackPage, PageTrackingCodes } from "core/services/analytics"; + +import { OrganizationUsersTable } from "./OrganizationUsersTable"; + +const NextOrganizationAccessManagementPage: React.FC = () => { + useTrackPage(PageTrackingCodes.SETTINGS_ORGANIZATION_ACCESS_MANAGEMENT); + const workspace = useCurrentWorkspace(); + const organizationUsers = useListUsersInOrganization(workspace.organizationId ?? "").users; + + return ( + + + + + + + {organizationUsers && organizationUsers.length > 0 ? ( + + ) : ( + + + + + + )} + + ); +}; +export default NextOrganizationAccessManagementPage; diff --git a/airbyte-webapp/src/pages/SettingsPage/pages/AccessManagementPage/NextWorkspaceAccessManagementPage.tsx b/airbyte-webapp/src/pages/SettingsPage/pages/AccessManagementPage/NextWorkspaceAccessManagementPage.tsx new file mode 100644 index 00000000000..285fcbd321c --- /dev/null +++ b/airbyte-webapp/src/pages/SettingsPage/pages/AccessManagementPage/NextWorkspaceAccessManagementPage.tsx @@ -0,0 +1,46 @@ +import React from "react"; +import { FormattedMessage } from "react-intl"; + +import { Box } from "components/ui/Box"; +import { FlexContainer } from "components/ui/Flex"; +import { Heading } from "components/ui/Heading"; +import { Text } from "components/ui/Text"; + +import { useTrackPage, PageTrackingCodes } from "core/services/analytics"; + +import { AddUserControl } from "./components/AddUserControl"; +import { useNextGetWorkspaceAccessUsers } from "./components/useGetAccessManagementData"; +import { WorkspaceUsersTable } from "./WorkspaceUsersTable"; + +const NextWorkspaceAccessManagementPage: React.FC = () => { + useTrackPage(PageTrackingCodes.SETTINGS_WORKSPACE_ACCESS_MANAGEMENT); + const accessData = useNextGetWorkspaceAccessUsers(); + const usersWithAccess = accessData.workspace?.users ?? []; + const usersToAdd = accessData.workspace?.usersToAdd ?? []; + + const showAddUsersButton = usersToAdd && usersToAdd.length > 0; + + return ( + + + + + + + + {showAddUsersButton && } + + {usersWithAccess && usersWithAccess.length > 0 ? ( + + ) : ( + + + + + + )} + + ); +}; + +export default NextWorkspaceAccessManagementPage; diff --git a/airbyte-webapp/src/pages/SettingsPage/pages/AccessManagementPage/OrganizationUsersTable.tsx b/airbyte-webapp/src/pages/SettingsPage/pages/AccessManagementPage/OrganizationUsersTable.tsx new file mode 100644 index 00000000000..cc6c4c62451 --- /dev/null +++ b/airbyte-webapp/src/pages/SettingsPage/pages/AccessManagementPage/OrganizationUsersTable.tsx @@ -0,0 +1,79 @@ +import { createColumnHelper } from "@tanstack/react-table"; +import { useMemo } from "react"; +import { FormattedMessage } from "react-intl"; + +import { Table } from "components/ui/Table"; + +import { OrganizationUserRead } from "core/api/types/AirbyteClient"; +import { useCurrentUser } from "core/services/auth"; +import { RbacRoleHierarchy, partitionPermissionType } from "core/utils/rbac/rbacPermissionsQuery"; + +import { UserCell } from "./components/UserCell"; +import { RoleManagementMenu } from "./next/RoleManagementMenu"; + +export const OrganizationUsersTable: React.FC<{ + users: OrganizationUserRead[]; +}> = ({ users }) => { + const { userId: currentUserId } = useCurrentUser(); + const columnHelper = createColumnHelper(); + + const columns = useMemo( + () => [ + columnHelper.accessor("name", { + header: () => , + cell: (props) => { + return ( + + ); + }, + sortingFn: "alphanumeric", + meta: { responsive: true }, + }), + columnHelper.accessor("permissionType", { + id: "permissionType", + header: () => ( + <> + {" "} + + + ), + meta: { responsive: true }, + cell: (props) => { + const user = { + name: props.row.original.name ?? "", + userId: props.row.original.userId, + email: props.row.original.email, + organizationPermission: { + permissionType: props.row.original.permissionType, + organizationId: props.row.original.organizationId, + permissionId: props.row.original.permissionId, + userId: props.row.original.userId, + }, + }; + + return ; + }, + sortingFn: (a, b, order) => { + const aRole = partitionPermissionType(a.original.permissionType)[1]; + const bRole = partitionPermissionType(b.original.permissionType)[1]; + + const aValue = RbacRoleHierarchy.indexOf(aRole); + const bValue = RbacRoleHierarchy.indexOf(bRole); + + if (order === "asc") { + return aValue - bValue; + } + return bValue - aValue; + }, + }), + ], + [columnHelper, currentUserId] + ); + + return ; +}; diff --git a/airbyte-webapp/src/pages/SettingsPage/pages/AccessManagementPage/WorkspaceUsersTable.tsx b/airbyte-webapp/src/pages/SettingsPage/pages/AccessManagementPage/WorkspaceUsersTable.tsx new file mode 100644 index 00000000000..9005f6f7683 --- /dev/null +++ b/airbyte-webapp/src/pages/SettingsPage/pages/AccessManagementPage/WorkspaceUsersTable.tsx @@ -0,0 +1,72 @@ +import { createColumnHelper } from "@tanstack/react-table"; +import { useMemo } from "react"; +import { FormattedMessage } from "react-intl"; + +import { Table } from "components/ui/Table"; + +import { useCurrentUser } from "core/services/auth"; +import { RbacRoleHierarchy } from "core/utils/rbac/rbacPermissionsQuery"; + +import { NextAccessUserRead, getWorkspaceAccessLevel } from "./components/useGetAccessManagementData"; +import { UserCell } from "./components/UserCell"; +import { RoleManagementMenu } from "./next/RoleManagementMenu"; + +export const WorkspaceUsersTable: React.FC<{ + users: NextAccessUserRead[]; +}> = ({ users }) => { + const { userId: currentUserId } = useCurrentUser(); + const columnHelper = createColumnHelper(); + + const columns = useMemo( + () => [ + columnHelper.accessor("name", { + header: () => , + cell: (props) => { + return ( + + ); + }, + sortingFn: "alphanumeric", + meta: { responsive: true }, + }), + columnHelper.accessor( + (row) => { + return getWorkspaceAccessLevel(row); + }, + { + id: "permissionType", + header: () => ( + <> + {" "} + + + ), + meta: { responsive: true }, + cell: (props) => { + return ; + }, + sortingFn: (a, b, order) => { + const aHighestRole = getWorkspaceAccessLevel(a.original); + const bHighestRole = getWorkspaceAccessLevel(b.original); + + const aValue = RbacRoleHierarchy.indexOf(aHighestRole); + const bValue = RbacRoleHierarchy.indexOf(bHighestRole); + + if (order === "asc") { + return aValue - bValue; + } + return bValue - aValue; + }, + } + ), + ], + [columnHelper, currentUserId] + ); + + return
; +}; diff --git a/airbyte-webapp/src/pages/SettingsPage/pages/AccessManagementPage/components/AccessManagementPageContent.tsx b/airbyte-webapp/src/pages/SettingsPage/pages/AccessManagementPage/components/AccessManagementPageContent.tsx index 9ba7898b6d7..f1a73315040 100644 --- a/airbyte-webapp/src/pages/SettingsPage/pages/AccessManagementPage/components/AccessManagementPageContent.tsx +++ b/airbyte-webapp/src/pages/SettingsPage/pages/AccessManagementPage/components/AccessManagementPageContent.tsx @@ -19,6 +19,11 @@ interface AccessManagementContentProps { accessUsers: AccessUsers; pageResourceType: ResourceType; } + +/** + * @deprecated will be removed when RBAC UI v2 is turned on. Use NextOrganizationAccessManagementPage or NextWorkspaceAccessManagementPage instead. + */ + export const AccessManagementPageContent: React.FC = ({ resourceName, accessUsers, diff --git a/airbyte-webapp/src/pages/SettingsPage/pages/AccessManagementPage/components/AccessManagementSection.tsx b/airbyte-webapp/src/pages/SettingsPage/pages/AccessManagementPage/components/AccessManagementSection.tsx index 993ba155a0c..f278f4dde89 100644 --- a/airbyte-webapp/src/pages/SettingsPage/pages/AccessManagementPage/components/AccessManagementSection.tsx +++ b/airbyte-webapp/src/pages/SettingsPage/pages/AccessManagementPage/components/AccessManagementSection.tsx @@ -6,12 +6,10 @@ import { Heading } from "components/ui/Heading"; import { Text } from "components/ui/Text"; import { OrganizationUserRead, WorkspaceUserRead } from "core/api/types/AirbyteClient"; -import { useExperiment } from "hooks/services/Experiment"; import { AccessManagementTable } from "./AccessManagementTable"; import { AddUserControl } from "./AddUserControl"; -import { NextAccessManagementTable } from "./NextAccessManagementTable"; -import { ResourceType, tableTitleDictionary, useNextGetWorkspaceAccessUsers } from "./useGetAccessManagementData"; +import { ResourceType, tableTitleDictionary } from "./useGetAccessManagementData"; interface AccessManagementSectionProps { users?: WorkspaceUserRead[] | OrganizationUserRead[]; @@ -20,6 +18,10 @@ interface AccessManagementSectionProps { pageResourceType: ResourceType; pageResourceName: string; } + +/** + * @deprecated will be removed when RBAC UI v2 is turned on + */ export const AccessManagementSection: React.FC = ({ users, usersToAdd, @@ -27,13 +29,7 @@ export const AccessManagementSection: React.FC = ( pageResourceType, pageResourceName, }) => { - const updatedOrganizationsUI = useExperiment("settings.organizationsUpdates", false); - const nextAccessData = useNextGetWorkspaceAccessUsers(); - const nextAccessUsers = nextAccessData.workspace?.users ?? []; - const nextUsersToAdd = nextAccessData.workspace?.usersToAdd ?? []; - - const showAddUsersButton = - (usersToAdd && usersToAdd.length > 0) || (updatedOrganizationsUI && nextUsersToAdd && nextUsersToAdd.length > 0); + const showAddUsersButton = usersToAdd && usersToAdd.length > 0; return ( @@ -45,17 +41,10 @@ export const AccessManagementSection: React.FC = ( {showAddUsersButton && ( // the empty array is not a possible state, but is required to prevent a type error... will be cleaner when types are moved over in full - + )} - {updatedOrganizationsUI === true && nextAccessUsers.length > 0 && pageResourceType === "workspace" ? ( - - ) : users && users.length > 0 ? ( + {users && users.length > 0 ? ( = ({ users, tableResourceType }) => { - const { userId: currentUserId } = useCurrentUser(); - const columnHelper = createColumnHelper(); - - const columns = useMemo( - () => [ - columnHelper.accessor("name", { - header: () => , - cell: (props) => { - return ( - - {props.cell.getValue()} - {props.row.original.userId === currentUserId && ( - - - - )} - - ); - }, - sortingFn: "alphanumeric", - meta: { responsive: true }, - }), - columnHelper.accessor("email", { - header: () => , - cell: (props) => props.cell.getValue(), - sortingFn: "alphanumeric", - meta: { responsive: true }, - }), - columnHelper.accessor( - (row) => { - return getHighestPermissionType(row, tableResourceType); - }, - { - id: "permissionType", - header: () => ( - <> - {" "} - - - ), - meta: { responsive: true }, - cell: (props) => { - return ; - }, - sortingFn: (a, b, order) => { - const roleOrder = ["admin", "editor", "reader", "member", undefined]; - const aHighestRole = getHighestPermissionType(a.original, tableResourceType); - const bHighestRole = getHighestPermissionType(b.original, tableResourceType); - - const aValue = aHighestRole === undefined ? -1 : roleOrder.indexOf(aHighestRole); - const bValue = bHighestRole === undefined ? -1 : roleOrder.indexOf(bHighestRole); - - if (order === "asc") { - return aValue - bValue; - } - return bValue - aValue; - }, - } - ), - ], - [columnHelper, tableResourceType, currentUserId] - ); - - return
; -}; diff --git a/airbyte-webapp/src/pages/SettingsPage/pages/AccessManagementPage/components/RoleManagementControl.tsx b/airbyte-webapp/src/pages/SettingsPage/pages/AccessManagementPage/components/RoleManagementControl.tsx index 20c71125b54..720b34a6618 100644 --- a/airbyte-webapp/src/pages/SettingsPage/pages/AccessManagementPage/components/RoleManagementControl.tsx +++ b/airbyte-webapp/src/pages/SettingsPage/pages/AccessManagementPage/components/RoleManagementControl.tsx @@ -31,7 +31,10 @@ const roleManagementFormSchema = yup.object().shape({ permissionType: yup.mixed().oneOf(Object.values(PermissionType)).required(), permissionId: yup.string().required(), }); - +/** + * + * @deprecated this component will be removed when RBAC UI v2 is turned on. Use RoleManagementMenu instead. + */ export const RoleManagementControl: React.FC = ({ permission, pageResourceType, diff --git a/airbyte-webapp/src/pages/SettingsPage/pages/AccessManagementPage/components/RoleToolTip.tsx b/airbyte-webapp/src/pages/SettingsPage/pages/AccessManagementPage/components/RoleToolTip.tsx index 06ea76ec93f..bbe80f64ac6 100644 --- a/airbyte-webapp/src/pages/SettingsPage/pages/AccessManagementPage/components/RoleToolTip.tsx +++ b/airbyte-webapp/src/pages/SettingsPage/pages/AccessManagementPage/components/RoleToolTip.tsx @@ -11,7 +11,9 @@ import { permissionStringDictionary, permissionsByResourceType, } from "./useGetAccessManagementData"; - +/** + * @deprecated this component will be removed when RBAC UI v2 is turned on. Role descriptions will live in the RoleManagementMenu instead. + */ export const RoleToolTip: React.FC<{ resourceType: ResourceType }> = ({ resourceType }) => { return ( diff --git a/airbyte-webapp/src/pages/SettingsPage/pages/AccessManagementPage/components/UserCell.tsx b/airbyte-webapp/src/pages/SettingsPage/pages/AccessManagementPage/components/UserCell.tsx new file mode 100644 index 00000000000..fc2ac6cc9d3 --- /dev/null +++ b/airbyte-webapp/src/pages/SettingsPage/pages/AccessManagementPage/components/UserCell.tsx @@ -0,0 +1,33 @@ +import { FormattedMessage } from "react-intl"; + +import { InitialBadge } from "components/InitialBadge/InitialBadge"; +import { Badge } from "components/ui/Badge"; +import { FlexContainer } from "components/ui/Flex"; +import { Text } from "components/ui/Text"; +export const UserCell: React.FC<{ name?: string; email: string; isCurrentUser: boolean; userId: string }> = ({ + name, + email, + userId, + isCurrentUser, +}) => { + return ( + + + + + {name ?? email} + {isCurrentUser && ( + + + + )} + + {name && ( + + {email} + + )} + + + ); +}; diff --git a/airbyte-webapp/src/pages/SettingsPage/pages/AccessManagementPage/components/UserRoleText.tsx b/airbyte-webapp/src/pages/SettingsPage/pages/AccessManagementPage/components/UserRoleText.tsx index a1677c06ab5..e956d1aceb6 100644 --- a/airbyte-webapp/src/pages/SettingsPage/pages/AccessManagementPage/components/UserRoleText.tsx +++ b/airbyte-webapp/src/pages/SettingsPage/pages/AccessManagementPage/components/UserRoleText.tsx @@ -2,19 +2,19 @@ import { FormattedMessage } from "react-intl"; import { Text } from "components/ui/Text"; -export const UserRoleText: React.FC<{ highestPermissionType?: "admin" | "editor" | "reader" | "member" }> = ({ - highestPermissionType, -}) => { +import { RbacRole } from "core/utils/rbac/rbacPermissionsQuery"; + +export const UserRoleText: React.FC<{ highestPermissionType?: RbacRole }> = ({ highestPermissionType }) => { if (!highestPermissionType) { return null; } const roleId = - highestPermissionType === "admin" + highestPermissionType === "ADMIN" ? "role.admin" - : highestPermissionType === "editor" + : highestPermissionType === "EDITOR" ? "role.editor" - : highestPermissionType === "reader" + : highestPermissionType === "READER" ? "role.reader" : "role.member"; diff --git a/airbyte-webapp/src/pages/SettingsPage/pages/AccessManagementPage/components/useGetAccessManagementData.tsx b/airbyte-webapp/src/pages/SettingsPage/pages/AccessManagementPage/components/useGetAccessManagementData.tsx index db58d611e42..5686724baa3 100644 --- a/airbyte-webapp/src/pages/SettingsPage/pages/AccessManagementPage/components/useGetAccessManagementData.tsx +++ b/airbyte-webapp/src/pages/SettingsPage/pages/AccessManagementPage/components/useGetAccessManagementData.tsx @@ -1,7 +1,7 @@ import { useCurrentWorkspace, useListUsersInOrganization, useListUsersInWorkspace } from "core/api"; import { OrganizationUserRead, PermissionRead, PermissionType, WorkspaceUserRead } from "core/api/types/AirbyteClient"; import { useIntent } from "core/utils/rbac"; - +import { RbacRole, RbacRoleHierarchy, partitionPermissionType } from "core/utils/rbac/rbacPermissionsQuery"; export type ResourceType = "workspace" | "organization" | "instance"; export const permissionStringDictionary: Record> = { @@ -67,7 +67,10 @@ export interface AccessUsers { export interface NextAccessUsers { workspace?: { users: NextAccessUserRead[]; usersToAdd: OrganizationUserRead[] }; } - +/** + * + * @deprecated this will be removed with RBAC UI v2. For now, use useNextGetWorkspaceAccessUsers instead. However, that will be, at least in part, replaced by a new endpoint instead. + */ export const useGetWorkspaceAccessUsers = (): AccessUsers => { const workspace = useCurrentWorkspace(); const canListOrganizationUsers = useIntent("ListOrganizationMembers", { organizationId: workspace.organizationId }); @@ -167,37 +170,19 @@ export const useGetOrganizationAccessUsers = (): AccessUsers => { }; }; -export const getHighestPermissionType = ( - user: NextAccessUserRead, - resourceType: "workspace" | "organization" | "instance" -) => { - const orgPermissionType = user.organizationPermission ? user.organizationPermission.permissionType : undefined; - const workspacePermissionType = user.workspacePermission ? user.workspacePermission.permissionType : undefined; - - switch (resourceType) { - case "instance": - return undefined; - case "organization": - switch (orgPermissionType) { - case "organization_admin": - return "admin"; - case "organization_editor": - return "editor"; - case "organization_reader": - return "reader"; - default: - return "member"; - } - default: - switch (true) { - case workspacePermissionType === "workspace_admin" || orgPermissionType === "organization_admin": - return "admin"; - case workspacePermissionType === "workspace_editor" || orgPermissionType === "organization_editor": - return "editor"; - case workspacePermissionType === "workspace_reader" || orgPermissionType === "organization_reader": - return "reader"; - default: - return "member"; - } - } +export const getWorkspaceAccessLevel = (user: NextAccessUserRead): RbacRole => { + const orgPermissionType = user.organizationPermission?.permissionType; + const workspacePermissionType = user.workspacePermission?.permissionType; + + const orgRole = orgPermissionType ? partitionPermissionType(orgPermissionType)[1] : undefined; + const workspaceRole = workspacePermissionType ? partitionPermissionType(workspacePermissionType)[1] : undefined; + + // return whatever is the "highest" role ie the lowest index greater than -1. + // the reason we set the index to the length of the array is so that if there is not a given type of role, it will not be the lowest index. + return RbacRoleHierarchy[ + Math.min( + orgRole ? RbacRoleHierarchy.indexOf(orgRole) : RbacRoleHierarchy.length, + workspaceRole ? RbacRoleHierarchy.indexOf(workspaceRole) : RbacRoleHierarchy.length + ) + ]; }; diff --git a/airbyte-webapp/src/pages/SettingsPage/pages/AccessManagementPage/next/ChangeRoleMenuItem.tsx b/airbyte-webapp/src/pages/SettingsPage/pages/AccessManagementPage/next/ChangeRoleMenuItem.tsx index 5d6ade63ec5..5aadbbfdac7 100644 --- a/airbyte-webapp/src/pages/SettingsPage/pages/AccessManagementPage/next/ChangeRoleMenuItem.tsx +++ b/airbyte-webapp/src/pages/SettingsPage/pages/AccessManagementPage/next/ChangeRoleMenuItem.tsx @@ -106,34 +106,32 @@ export const ChangeRoleMenuItem: React.FC = ({ user, permissi const roleIsInvalid = disallowedRoles(user, resourceType, isCurrentUser).includes(permissionType); return ( - - - + ); }; diff --git a/airbyte-webapp/src/pages/SettingsPage/pages/AccessManagementPage/next/RemoveRoleMenuItem.tsx b/airbyte-webapp/src/pages/SettingsPage/pages/AccessManagementPage/next/RemoveRoleMenuItem.tsx index c89fe07f079..9e5d0825ae5 100644 --- a/airbyte-webapp/src/pages/SettingsPage/pages/AccessManagementPage/next/RemoveRoleMenuItem.tsx +++ b/airbyte-webapp/src/pages/SettingsPage/pages/AccessManagementPage/next/RemoveRoleMenuItem.tsx @@ -4,6 +4,7 @@ import { Box } from "components/ui/Box"; import { Text } from "components/ui/Text"; import { useCurrentOrganizationInfo, useCurrentWorkspace, useDeletePermissions } from "core/api"; +import { useCurrentUser } from "core/services/auth"; import { useConfirmationModalService } from "hooks/services/ConfirmationModal"; import styles from "./RemoveRoleMenuItem.module.scss"; @@ -28,6 +29,7 @@ export const RemoveRoleMenuItem: React.FC = ({ user, re const organizationName = useCurrentOrganizationInfo()?.organizationName; const { name: workspaceName } = useCurrentWorkspace(); const { formatMessage } = useIntl(); + const { userId: currentUserId } = useCurrentUser(); const resourceName = resourceType === "organization" ? organizationName : workspaceName; const { mutateAsync: removePermission } = useDeletePermissions(); @@ -48,9 +50,13 @@ export const RemoveRoleMenuItem: React.FC = ({ user, re }); return ( -