From fc56e8fbbd37a05dd431495b30c0f3c272e836a1 Mon Sep 17 00:00:00 2001 From: Ayush Pahwa Date: Tue, 7 Nov 2023 13:34:47 +0700 Subject: [PATCH] feat: manage env code split (#28215) Coed split PR for custom environments [EE PR](https://github.com/appsmithorg/appsmith-ee/pull/2207) --- .../WorkspaceSettingsTabs/index.tsx | 168 +++++++++++++++++ app/client/src/ce/configs/types.ts | 7 + app/client/src/ce/constants/messages.ts | 1 + .../Applications/ManageEnvironmentsMenu.tsx | 6 + .../ce/pages/Applications/WorkspaceMenu.tsx | 5 + .../src/ce/pages/Applications/index.tsx | 12 +- app/client/src/ce/pages/workspace/Members.tsx | 4 +- .../src/ce/selectors/environmentSelectors.tsx | 2 + .../src/ce/selectors/workspaceSelectors.tsx | 8 + app/client/src/ce/utils/permissionHelpers.tsx | 4 + .../WorkspaceSettingsTabs/index.tsx | 1 + .../Applications/ManageEnvironmentsMenu.tsx | 1 + .../pages/Editor/DataSourceEditor/index.tsx | 4 + .../Editor/DatasourceInfo/DatasorceTabs.tsx | 2 +- app/client/src/pages/workspace/General.tsx | 167 +++++++++-------- .../workspace/__tests__/settings.test.tsx | 3 +- app/client/src/pages/workspace/settings.tsx | 169 +++--------------- app/client/src/sagas/DatasourcesSagas.ts | 4 +- 18 files changed, 342 insertions(+), 226 deletions(-) create mode 100644 app/client/src/ce/components/WorkspaceSettingsTabs/index.tsx create mode 100644 app/client/src/ce/pages/Applications/ManageEnvironmentsMenu.tsx create mode 100644 app/client/src/ee/components/WorkspaceSettingsTabs/index.tsx create mode 100644 app/client/src/ee/pages/Applications/ManageEnvironmentsMenu.tsx diff --git a/app/client/src/ce/components/WorkspaceSettingsTabs/index.tsx b/app/client/src/ce/components/WorkspaceSettingsTabs/index.tsx new file mode 100644 index 0000000000..c8907d0105 --- /dev/null +++ b/app/client/src/ce/components/WorkspaceSettingsTabs/index.tsx @@ -0,0 +1,168 @@ +import React, { useCallback, useEffect, useMemo } from "react"; +import { + useRouteMatch, + Route, + useLocation, + useHistory, +} from "react-router-dom"; +import MemberSettings from "@appsmith/pages/workspace/Members"; +import { GeneralSettings } from "pages/workspace/General"; +import { Tabs, Tab, TabsList, TabPanel } from "design-system"; +import { navigateToTab } from "@appsmith/pages/workspace/helpers"; +import styled from "styled-components"; + +import * as Sentry from "@sentry/react"; +import { APPLICATIONS_URL } from "constants/routes/baseRoutes"; +export const SentryRoute = Sentry.withSentryRouting(Route); + +export const TabsWrapper = styled.div` + padding-top: var(--ads-v2-spaces-4); + + .ads-v2-tabs { + height: 100%; + overflow: hidden; + + .tab-panel { + height: calc(100% - 46px); + } + } +`; + +interface TabProp { + key: string; + title: string; + count?: number; + panelComponent?: JSX.Element; +} + +export interface WorkspaceSettingsTabsProps { + currentTab: string | undefined; + isMemberofTheWorkspace: boolean; + hasManageWorkspacePermissions: boolean; + searchValue: string; + setTabArrLen: (tabArrLen: number) => void; + workspacePermissions?: string[]; + // EE Tab Props + addTabComponent?: () => TabProp; + eeTabRedirect?: boolean; +} + +enum TABS { + GENERAL = "general", + MEMBERS = "members", +} + +export const WorkspaceSettingsTabs = ({ + addTabComponent, + currentTab, + eeTabRedirect, + hasManageWorkspacePermissions, + isMemberofTheWorkspace, + searchValue, + setTabArrLen, + workspacePermissions, +}: WorkspaceSettingsTabsProps) => { + const { path } = useRouteMatch(); + const location = useLocation(); + const history = useHistory(); + + const shouldRedirect = useMemo(() => { + // If the permissions are not yet fetched, don't redirect + if (!workspacePermissions) { + return false; + } + // If user doesn't have manage workspace permissions & is on settings page, redirect to applications + if (currentTab === TABS.GENERAL && !hasManageWorkspacePermissions) + return true; + // If user doesn't have manage members permissions & is on members page, redirect to applications + if (currentTab === TABS.MEMBERS && !isMemberofTheWorkspace) return true; + // If the redirect flag is set to true by EE application, redirect to applications + if (eeTabRedirect) return true; + return false; + }, [ + workspacePermissions, + isMemberofTheWorkspace, + hasManageWorkspacePermissions, + currentTab, + eeTabRedirect, + ]); + + useEffect(() => { + if (shouldRedirect) { + history.replace(APPLICATIONS_URL); + } + }, [shouldRedirect]); + + const GeneralSettingsComponent = ( + + ); + + const MemberSettingsComponent = ( + ( + + ), + [location, searchValue], + )} + location={location} + path={`${path}/members`} + /> + ); + + const tabArr: TabProp[] = [ + hasManageWorkspacePermissions && { + key: "general", + title: "General Settings", + panelComponent: GeneralSettingsComponent, + }, + isMemberofTheWorkspace && { + key: "members", + title: "Members", + panelComponent: MemberSettingsComponent, + }, + addTabComponent && addTabComponent(), + ].filter(Boolean) as TabProp[]; + + useEffect(() => { + setTabArrLen(tabArr.length); + }, [tabArr.length, setTabArrLen]); + + return ( + + navigateToTab(key, location, history)} + value={currentTab} + > + + {tabArr.map((tab) => { + return ( + +
{tab.title}
+
+ ); + })} +
+ {tabArr.map((tab) => { + return ( + + {tab.panelComponent} + + ); + })} +
+
+ ); +}; diff --git a/app/client/src/ce/configs/types.ts b/app/client/src/ce/configs/types.ts index 8c3cd53cba..50f3ff5848 100644 --- a/app/client/src/ce/configs/types.ts +++ b/app/client/src/ce/configs/types.ts @@ -72,11 +72,18 @@ export interface AppsmithUIConfigs { customerPortalUrl: string; } +export interface DatasourceMeta { + configuredDatasources: number; + totalDatasources: number; +} + // Type for one environment export interface EnvironmentType { id: string; name: string; workspaceId: string; isDefault?: boolean; + isLocked: boolean; // Whether the environment is locked (disables editing and deleting of the env) userPermissions?: string[]; + datasourceMeta?: DatasourceMeta; } diff --git a/app/client/src/ce/constants/messages.ts b/app/client/src/ce/constants/messages.ts index 6f77ffceee..fb87081bca 100644 --- a/app/client/src/ce/constants/messages.ts +++ b/app/client/src/ce/constants/messages.ts @@ -508,6 +508,7 @@ export const PAGE_SERVER_UNAVAILABLE_ERROR_MESSAGES = ( export const POST = () => "Post"; export const CANCEL = () => "Cancel"; export const REMOVE = () => "Remove"; +export const CREATE = () => "Create"; // Showcase Carousel export const NEXT = () => "NEXT"; diff --git a/app/client/src/ce/pages/Applications/ManageEnvironmentsMenu.tsx b/app/client/src/ce/pages/Applications/ManageEnvironmentsMenu.tsx new file mode 100644 index 0000000000..92aa905e6d --- /dev/null +++ b/app/client/src/ce/pages/Applications/ManageEnvironmentsMenu.tsx @@ -0,0 +1,6 @@ +export const ManageEnvironmentsMenu = ({}: { + workspaceId: string; + workspacePermissions: string[]; +}) => { + return null; +}; diff --git a/app/client/src/ce/pages/Applications/WorkspaceMenu.tsx b/app/client/src/ce/pages/Applications/WorkspaceMenu.tsx index 45962dc772..67af42bbeb 100644 --- a/app/client/src/ce/pages/Applications/WorkspaceMenu.tsx +++ b/app/client/src/ce/pages/Applications/WorkspaceMenu.tsx @@ -18,6 +18,7 @@ import { DropdownOnSelectActions, getOnSelectAction, } from "pages/common/CustomizedDropdown/dropdownHelpers"; +import { ManageEnvironmentsMenu } from "@appsmith/pages/Applications/ManageEnvironmentsMenu"; interface WorkspaceMenuProps { canDeleteWorkspace: boolean; @@ -162,6 +163,10 @@ function WorkspaceMenu({ Members )} + {canInviteToWorkspace && ( ( null, ); + const isManageEnvironmentEnabled = useSelector( + allowManageEnvironmentAccessForUser, + ); const updateApplicationDispatch = ( id: string, data: UpdateApplicationPayload, @@ -605,6 +610,10 @@ export function ApplicationsSection(props: any) { const hasCreateNewApplicationPermission = hasCreateNewAppPermission(workspace.userPermissions) && !isMobile; + const renderManageEnvironmentMenu = + isManageEnvironmentEnabled && + hasManageWorkspaceEnvironmentPermission(workspace.userPermissions); + const onClickAddNewAppButton = (workspaceId: string) => { if ( Object.entries(creatingApplicationMap).length === 0 || @@ -624,7 +633,8 @@ export function ApplicationsSection(props: any) { canInviteToWorkspace || hasManageWorkspacePermissions || hasCreateNewApplicationPermission || - (canDeleteWorkspace && applications.length === 0); + (canDeleteWorkspace && applications.length === 0) || + renderManageEnvironmentMenu; const handleResetMenuState = () => { setWorkspaceToOpenMenu(null); diff --git a/app/client/src/ce/pages/workspace/Members.tsx b/app/client/src/ce/pages/workspace/Members.tsx index 843f53dc4f..dd4ed292b0 100644 --- a/app/client/src/ce/pages/workspace/Members.tsx +++ b/app/client/src/ce/pages/workspace/Members.tsx @@ -52,6 +52,8 @@ export const MembersWrapper = styled.div<{ isMobile?: boolean; }>` &.members-wrapper { + overflow: scroll; + height: 100%; ${(props) => (props.isMobile ? "width: 100%; margin: auto" : null)} table { table-layout: fixed; @@ -124,7 +126,7 @@ export const UserCard = styled(Card)` border: 1px solid var(--ads-v2-color-border); border-radius: var(--ads-v2-border-radius); padding: ${(props) => - `${props.theme.spaces[15]}px ${props.theme.spaces[7] * 4}px;`} + `${props.theme.spaces[15]}px ${props.theme.spaces[7] * 4}px;`}; width: 100%; height: 201px; margin: auto; diff --git a/app/client/src/ce/selectors/environmentSelectors.tsx b/app/client/src/ce/selectors/environmentSelectors.tsx index 17be18aa30..397091a57e 100644 --- a/app/client/src/ce/selectors/environmentSelectors.tsx +++ b/app/client/src/ce/selectors/environmentSelectors.tsx @@ -23,3 +23,5 @@ export const getCurrentEnvironmentDetails = (state: AppState) => ({ name: "", editingId: "unused_env", }); + +export const allowManageEnvironmentAccessForUser = (state: AppState) => false; diff --git a/app/client/src/ce/selectors/workspaceSelectors.tsx b/app/client/src/ce/selectors/workspaceSelectors.tsx index 8f2065eac1..71e6f35f18 100644 --- a/app/client/src/ce/selectors/workspaceSelectors.tsx +++ b/app/client/src/ce/selectors/workspaceSelectors.tsx @@ -25,6 +25,14 @@ export const getCurrentWorkspaceId = (state: AppState) => export const getWorkspaces = (state: AppState) => { return state.ui.applications.userWorkspaces; }; +export const getWorkspaceFromId = (state: AppState, workspaceId: string) => { + const filteredWorkspaces = state.ui.applications.userWorkspaces.filter( + (el) => el.workspace.id === workspaceId, + ); + return !!filteredWorkspaces && filteredWorkspaces.length > 0 + ? filteredWorkspaces[0].workspace + : undefined; +}; export const getCurrentWorkspace = (state: AppState) => { return state.ui.applications.userWorkspaces.map((el) => el.workspace); }; diff --git a/app/client/src/ce/utils/permissionHelpers.tsx b/app/client/src/ce/utils/permissionHelpers.tsx index b052c12fd3..b2a7f479f6 100644 --- a/app/client/src/ce/utils/permissionHelpers.tsx +++ b/app/client/src/ce/utils/permissionHelpers.tsx @@ -101,3 +101,7 @@ export const hasDeleteActionPermission = (_permissions?: string[]) => true; export const hasExecuteActionPermission = (_permissions?: string[]) => true; export const hasAuditLogsReadPermission = (_permissions?: string[]) => true; + +export const hasManageWorkspaceEnvironmentPermission = ( + _permissions?: string[], +) => false; diff --git a/app/client/src/ee/components/WorkspaceSettingsTabs/index.tsx b/app/client/src/ee/components/WorkspaceSettingsTabs/index.tsx new file mode 100644 index 0000000000..7df47f64c4 --- /dev/null +++ b/app/client/src/ee/components/WorkspaceSettingsTabs/index.tsx @@ -0,0 +1 @@ +export * from "ce/components/WorkspaceSettingsTabs"; diff --git a/app/client/src/ee/pages/Applications/ManageEnvironmentsMenu.tsx b/app/client/src/ee/pages/Applications/ManageEnvironmentsMenu.tsx new file mode 100644 index 0000000000..e7f3d901d9 --- /dev/null +++ b/app/client/src/ee/pages/Applications/ManageEnvironmentsMenu.tsx @@ -0,0 +1 @@ +export * from "ce/pages/Applications/ManageEnvironmentsMenu"; diff --git a/app/client/src/pages/Editor/DataSourceEditor/index.tsx b/app/client/src/pages/Editor/DataSourceEditor/index.tsx index 26980a75f8..5a08dc677b 100644 --- a/app/client/src/pages/Editor/DataSourceEditor/index.tsx +++ b/app/client/src/pages/Editor/DataSourceEditor/index.tsx @@ -397,6 +397,8 @@ class DatasourceEditorRouter extends React.Component { const { configProperty, controlType, isRequired } = config; const configDetails = this.state.configDetails; const requiredFields = this.state.requiredFields; + if (!configProperty || !configProperty.includes(this.getEnvironmentId())) + return; configDetails[configProperty] = controlType; if (isRequired) requiredFields[configProperty] = config; @@ -683,6 +685,8 @@ class DatasourceEditorRouter extends React.Component { name, userPermissions, }, + configDetails: {}, + requiredFields: {}, }); this.blockRoutes(); return true; diff --git a/app/client/src/pages/Editor/DatasourceInfo/DatasorceTabs.tsx b/app/client/src/pages/Editor/DatasourceInfo/DatasorceTabs.tsx index 891aa1cc6e..887757e64c 100644 --- a/app/client/src/pages/Editor/DatasourceInfo/DatasorceTabs.tsx +++ b/app/client/src/pages/Editor/DatasourceInfo/DatasorceTabs.tsx @@ -40,7 +40,7 @@ const TabPanelContainer = styled(TabPanel)` const ConfigurationsTabPanelContainer = styled(TabPanel)` margin-top: 0; - overflow: hidden; + overflow: scroll; flex-grow: 1; padding: 0 var(--ads-v2-spaces-7); `; diff --git a/app/client/src/pages/workspace/General.tsx b/app/client/src/pages/workspace/General.tsx index 6741f5d4cd..48f5ac43cf 100644 --- a/app/client/src/pages/workspace/General.tsx +++ b/app/client/src/pages/workspace/General.tsx @@ -22,6 +22,13 @@ import { Classes } from "@blueprintjs/core"; import { getIsFetchingApplications } from "@appsmith/selectors/applicationSelectors"; import { useMediaQuery } from "react-responsive"; +// This wrapper ensures that the scroll behaviour is consistent with the other tabs +const ScrollWrapper = styled.div` + overflow: auto; + height: 100%; + width: 100%; +`; + // trigger tests const GeneralWrapper = styled.div<{ isMobile?: boolean; @@ -170,86 +177,90 @@ export function GeneralSettings() { }); return ( - - - - {isFetchingApplications && } - {!isFetchingApplications && ( - - )} - - + + + + + {isFetchingApplications && } + {!isFetchingApplications && ( + + )} + + - - - - Upload logo - - {isFetchingWorkspace && ( - - )} - {!isFetchingWorkspace && ( - - )} - - + + + + Upload logo + + {isFetchingWorkspace && ( + + )} + {!isFetchingWorkspace && ( + + )} + + - - - {isFetchingApplications && } - {!isFetchingApplications && ( - - )} - - + + + {isFetchingApplications && } + {!isFetchingApplications && ( + + )} + + - - - {isFetchingApplications && } - {!isFetchingApplications && ( - - )} - - - + + + {isFetchingApplications && } + {!isFetchingApplications && ( + + )} + + + + ); } diff --git a/app/client/src/pages/workspace/__tests__/settings.test.tsx b/app/client/src/pages/workspace/__tests__/settings.test.tsx index 30491df119..dafab428ca 100644 --- a/app/client/src/pages/workspace/__tests__/settings.test.tsx +++ b/app/client/src/pages/workspace/__tests__/settings.test.tsx @@ -186,6 +186,7 @@ describe("", () => { it("displays tabs", () => { renderComponent(); const tabList = screen.getAllByRole("tab"); - expect(tabList).toHaveLength(2); + expect(tabList.length).toBeGreaterThanOrEqual(2); + expect(tabList.length).toBeLessThanOrEqual(3); }); }); diff --git a/app/client/src/pages/workspace/settings.tsx b/app/client/src/pages/workspace/settings.tsx index a7c6322270..7cd708a218 100644 --- a/app/client/src/pages/workspace/settings.tsx +++ b/app/client/src/pages/workspace/settings.tsx @@ -1,49 +1,29 @@ -import React, { useCallback, useEffect, useMemo, useState } from "react"; -import { - useRouteMatch, - useLocation, - useParams, - Route, - useHistory, -} from "react-router-dom"; +import React, { useEffect, useState } from "react"; +import { useLocation, useParams } from "react-router-dom"; import { getCurrentWorkspace } from "@appsmith/selectors/workspaceSelectors"; import { useSelector, useDispatch } from "react-redux"; import styled from "styled-components"; - -import { Tabs, Tab, TabsList, TabPanel } from "design-system"; -import MemberSettings from "@appsmith/pages/workspace/Members"; -import { GeneralSettings } from "./General"; -import * as Sentry from "@sentry/react"; import { getAllApplications } from "@appsmith/actions/applicationActions"; import { useMediaQuery } from "react-responsive"; import { BackButton, StickyHeader } from "components/utils/helperComponents"; -import { debounce } from "lodash"; import WorkspaceInviteUsersForm from "pages/workspace/WorkspaceInviteUsersForm"; import { SettingsPageHeader } from "./SettingsPageHeader"; -import { navigateToTab } from "@appsmith/pages/workspace/helpers"; import { isPermitted, PERMISSION_TYPE, } from "@appsmith/utils/permissionHelpers"; import { createMessage, + DOCUMENTATION, INVITE_USERS_PLACEHOLDER, SEARCH_USERS, } from "@appsmith/constants/messages"; -import { APPLICATIONS_URL } from "constants/routes"; import FormDialogComponent from "components/editorComponents/form/FormDialogComponent"; +import { debounce } from "lodash"; +import { WorkspaceSettingsTabs } from "@appsmith/components/WorkspaceSettingsTabs"; import { useFeatureFlag } from "utils/hooks/useFeatureFlag"; import { FEATURE_FLAG } from "@appsmith/entities/FeatureFlag"; -const SentryRoute = Sentry.withSentryRouting(Route); - -interface TabProp { - key: string; - title: string; - count?: number; - panelComponent?: JSX.Element; -} - const SettingsWrapper = styled.div<{ isMobile?: boolean; }>` @@ -77,19 +57,6 @@ const StyledStickyHeader = styled(StickyHeader)<{ isMobile?: boolean }>` width: 954px; `} `; -export const TabsWrapper = styled.div` - padding-top: var(--ads-v2-spaces-4); - - .ads-v2-tabs { - height: 100%; - overflow: hidden; - - .tab-panel { - overflow: auto; - height: calc(100% - 46px); - } - } -`; enum TABS { GENERAL = "general", @@ -101,7 +68,6 @@ export default function Settings() { const currentWorkspace = useSelector(getCurrentWorkspace).filter( (el) => el.id === workspaceId, )[0]; - const { path } = useRouteMatch(); const location = useLocation(); const dispatch = useDispatch(); @@ -109,13 +75,11 @@ export default function Settings() { const [searchValue, setSearchValue] = useState(""); const [pageTitle, setPageTitle] = useState(""); - - const history = useHistory(); + const [tabArrLen, setTabArrLen] = useState(0); const isGACEnabled = useFeatureFlag(FEATURE_FLAG.license_gac_enabled); const currentTab = location.pathname.split("/").pop(); - // const [selectedTab, setSelectedTab] = useState(currentTab); const isMemberofTheWorkspace = isPermitted( currentWorkspace?.userPermissions || [], @@ -125,58 +89,31 @@ export default function Settings() { currentWorkspace?.userPermissions, PERMISSION_TYPE.MANAGE_WORKSPACE, ); - const shouldRedirect = useMemo( - () => - currentWorkspace && - ((!isMemberofTheWorkspace && currentTab === TABS.MEMBERS) || - (!hasManageWorkspacePermissions && currentTab === TABS.GENERAL)), - [ - currentWorkspace, - isMemberofTheWorkspace, - hasManageWorkspacePermissions, - currentTab, - ], - ); + const showMembersTab = + isMemberofTheWorkspace && hasManageWorkspacePermissions; const onButtonClick = () => { setShowModal(true); }; - useEffect(() => { - if (shouldRedirect) { - history.replace(APPLICATIONS_URL); - } - if (currentWorkspace) { - setPageTitle(`${currentWorkspace?.name}`); - } - }, [currentWorkspace, shouldRedirect]); - useEffect(() => { if (!currentWorkspace) { dispatch(getAllApplications()); + } else { + setPageTitle(`${currentWorkspace?.name}`); } }, [dispatch, currentWorkspace]); - const GeneralSettingsComponent = ( - - ); + const pageMenuItems: any[] = [ + { + icon: "book-line", + className: "documentation-page-menu-item", + onSelect: () => {}, + text: createMessage(DOCUMENTATION), + }, + ]; - const MemberSettingsComponent = ( - ( - - ), - [location, searchValue], - )} - location={location} - path={`${path}/members`} - /> - ); + const isMembersPage = tabArrLen > 1 && currentTab === TABS.MEMBERS; const onSearch = debounce((search: string) => { if (search.trim().length > 0) { @@ -186,33 +123,6 @@ export default function Settings() { } }, 300); - const tabArr: TabProp[] = [ - isMemberofTheWorkspace && { - key: "members", - title: "Members", - panelComponent: MemberSettingsComponent, - }, - { - key: "general", - title: "General Settings", - panelComponent: GeneralSettingsComponent, - }, - ].filter(Boolean) as TabProp[]; - - const pageMenuItems: any[] = [ - { - icon: "book-line", - className: "documentation-page-menu-item", - onSelect: () => { - /*console.log("hello onSelect")*/ - }, - text: "Documentation", - }, - ]; - - const isMembersPage = tabArr.length > 1 && currentTab === TABS.MEMBERS; - // const isGeneralPage = tabArr.length === 1 && currentTab === TABS.GENERAL; - const isMobile: boolean = useMediaQuery({ maxWidth: 767 }); return ( <> @@ -230,39 +140,14 @@ export default function Settings() { title={pageTitle} /> - - - navigateToTab(key, location, history) - } - value={currentTab} - > - - {tabArr.map((tab) => { - return ( - -
{tab.title}
-
- ); - })} -
- {tabArr.map((tab) => { - return ( - - {tab.panelComponent} - - ); - })} -
-
+ {currentWorkspace && ( , ) { try { - const currentEnvDetails: { id: string; name: string } = yield select( + const currentEnvDetails: { editingId: string; name: string } = yield select( getCurrentEnvironmentDetails, ); const queryParams = getQueryParams(); const currentEnvironment = - actionPayload.payload?.currEditingEnvId || currentEnvDetails.id; + actionPayload.payload?.currEditingEnvId || currentEnvDetails.editingId; const datasourcePayload = omit(actionPayload.payload, "name"); const datasourceStoragePayload = datasourcePayload.datasourceStorages[currentEnvironment];