feat: manage env code split (#28215)

Coed split PR for custom environments [EE
PR](https://github.com/appsmithorg/appsmith-ee/pull/2207)
This commit is contained in:
Ayush Pahwa 2023-11-07 13:34:47 +07:00 committed by Ayush Pahwa
parent 80e0458cbc
commit fc56e8fbbd
18 changed files with 342 additions and 226 deletions

View File

@ -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 = (
<SentryRoute
component={GeneralSettings}
location={location}
path={`${path}/general`}
/>
);
const MemberSettingsComponent = (
<SentryRoute
component={useCallback(
(props: any) => (
<MemberSettings {...props} searchValue={searchValue} />
),
[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 (
<TabsWrapper
className="tabs-wrapper"
data-testid="t--user-edit-tabs-wrapper"
>
<Tabs
defaultValue={currentTab}
onValueChange={(key: string) => navigateToTab(key, location, history)}
value={currentTab}
>
<TabsList>
{tabArr.map((tab) => {
return (
<Tab
data-testid={`t--tab-${tab.key}`}
key={tab.key}
value={tab.key}
>
<div className="tab-item">{tab.title}</div>
</Tab>
);
})}
</TabsList>
{tabArr.map((tab) => {
return (
<TabPanel className="tab-panel" key={tab.key} value={tab.key}>
{tab.panelComponent}
</TabPanel>
);
})}
</Tabs>
</TabsWrapper>
);
};

View File

@ -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;
}

View File

@ -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";

View File

@ -0,0 +1,6 @@
export const ManageEnvironmentsMenu = ({}: {
workspaceId: string;
workspacePermissions: string[];
}) => {
return null;
};

View File

@ -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
</MenuItem>
)}
<ManageEnvironmentsMenu
workspaceId={workspace.id}
workspacePermissions={workspace.userPermissions || []}
/>
{canInviteToWorkspace && (
<MenuItem
className="error-menuitem"

View File

@ -87,6 +87,7 @@ import { resetEditorRequest } from "actions/initActions";
import {
hasCreateNewAppPermission,
hasDeleteWorkspacePermission,
hasManageWorkspaceEnvironmentPermission,
isPermitted,
PERMISSION_TYPE,
} from "@appsmith/utils/permissionHelpers";
@ -102,6 +103,7 @@ import ResourceListLoader from "@appsmith/pages/Applications/ResourceListLoader"
import { useFeatureFlag } from "utils/hooks/useFeatureFlag";
import { FEATURE_FLAG } from "@appsmith/entities/FeatureFlag";
import { getHasCreateWorkspacePermission } from "@appsmith/utils/BusinessFeatures/permissionPageHelpers";
import { allowManageEnvironmentAccessForUser } from "@appsmith/selectors/environmentSelectors";
export const { cloudHosting } = getAppsmithConfigs();
@ -468,6 +470,9 @@ export function ApplicationsSection(props: any) {
const [workspaceToOpenMenu, setWorkspaceToOpenMenu] = useState<string | null>(
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);

View File

@ -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;

View File

@ -23,3 +23,5 @@ export const getCurrentEnvironmentDetails = (state: AppState) => ({
name: "",
editingId: "unused_env",
});
export const allowManageEnvironmentAccessForUser = (state: AppState) => false;

View File

@ -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);
};

View File

@ -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;

View File

@ -0,0 +1 @@
export * from "ce/components/WorkspaceSettingsTabs";

View File

@ -0,0 +1 @@
export * from "ce/pages/Applications/ManageEnvironmentsMenu";

View File

@ -397,6 +397,8 @@ class DatasourceEditorRouter extends React.Component<Props, State> {
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<Props, State> {
name,
userPermissions,
},
configDetails: {},
requiredFields: {},
});
this.blockRoutes();
return true;

View File

@ -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);
`;

View File

@ -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 (
<GeneralWrapper isMobile={isMobile} isPortrait={isPortrait}>
<SettingWrapper>
<Row>
{isFetchingApplications && <Loader className={Classes.SKELETON} />}
{!isFetchingApplications && (
<Input
data-testid="t--workspace-name-input"
defaultValue={currentWorkspace && currentWorkspace.name}
isRequired
label="Workspace name"
labelPosition="top"
onChange={onWorkspaceNameChange}
placeholder="Workspace name"
renderAs="input"
size="md"
type="text"
/>
)}
</Row>
</SettingWrapper>
<ScrollWrapper>
<GeneralWrapper isMobile={isMobile} isPortrait={isPortrait}>
<SettingWrapper>
<Row>
{isFetchingApplications && <Loader className={Classes.SKELETON} />}
{!isFetchingApplications && (
<Input
data-testid="t--workspace-name-input"
defaultValue={currentWorkspace && currentWorkspace.name}
isRequired
label="Workspace name"
labelPosition="top"
onChange={onWorkspaceNameChange}
placeholder="Workspace name"
renderAs="input"
size="md"
type="text"
/>
)}
</Row>
</SettingWrapper>
<SettingWrapper>
<Row className="t--workspace-settings-filepicker">
<InputLabelWrapper>
<Text type={TextType.P1}>Upload logo</Text>
</InputLabelWrapper>
{isFetchingWorkspace && (
<FilePickerLoader className={Classes.SKELETON} />
)}
{!isFetchingWorkspace && (
<FilePickerV2
fileType={FileType.IMAGE}
fileUploader={FileUploader}
logoUploadError={logoUploadError.message}
onFileRemoved={DeleteLogo}
url={currentWorkspace && currentWorkspace.logoUrl}
/>
)}
</Row>
</SettingWrapper>
<SettingWrapper>
<Row className="t--workspace-settings-filepicker">
<InputLabelWrapper>
<Text type={TextType.P1}>Upload logo</Text>
</InputLabelWrapper>
{isFetchingWorkspace && (
<FilePickerLoader className={Classes.SKELETON} />
)}
{!isFetchingWorkspace && (
<FilePickerV2
fileType={FileType.IMAGE}
fileUploader={FileUploader}
logoUploadError={logoUploadError.message}
onFileRemoved={DeleteLogo}
url={currentWorkspace && currentWorkspace.logoUrl}
/>
)}
</Row>
</SettingWrapper>
<SettingWrapper>
<Row>
{isFetchingApplications && <Loader className={Classes.SKELETON} />}
{!isFetchingApplications && (
<Input
data-testid="t--workspace-website-input"
defaultValue={
(currentWorkspace && currentWorkspace.website) || ""
}
label="Website"
labelPosition="top"
onChange={onWebsiteChange}
placeholder="Your website"
renderAs="input"
size="md"
type="text"
/>
)}
</Row>
</SettingWrapper>
<SettingWrapper>
<Row>
{isFetchingApplications && <Loader className={Classes.SKELETON} />}
{!isFetchingApplications && (
<Input
data-testid="t--workspace-website-input"
defaultValue={
(currentWorkspace && currentWorkspace.website) || ""
}
label="Website"
labelPosition="top"
onChange={onWebsiteChange}
placeholder="Your website"
renderAs="input"
size="md"
type="text"
/>
)}
</Row>
</SettingWrapper>
<SettingWrapper>
<Row>
{isFetchingApplications && <Loader className={Classes.SKELETON} />}
{!isFetchingApplications && (
<Input
data-testid="t--workspace-email-input"
defaultValue={(currentWorkspace && currentWorkspace.email) || ""}
label="Email"
labelPosition="top"
onChange={onEmailChange}
placeholder="Email"
renderAs="input"
size="md"
type="text"
/>
)}
</Row>
</SettingWrapper>
</GeneralWrapper>
<SettingWrapper>
<Row>
{isFetchingApplications && <Loader className={Classes.SKELETON} />}
{!isFetchingApplications && (
<Input
data-testid="t--workspace-email-input"
defaultValue={
(currentWorkspace && currentWorkspace.email) || ""
}
label="Email"
labelPosition="top"
onChange={onEmailChange}
placeholder="Email"
renderAs="input"
size="md"
type="text"
/>
)}
</Row>
</SettingWrapper>
</GeneralWrapper>
</ScrollWrapper>
);
}

View File

@ -186,6 +186,7 @@ describe("<Settings />", () => {
it("displays tabs", () => {
renderComponent();
const tabList = screen.getAllByRole("tab");
expect(tabList).toHaveLength(2);
expect(tabList.length).toBeGreaterThanOrEqual(2);
expect(tabList.length).toBeLessThanOrEqual(3);
});
});

View File

@ -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<string>("");
const history = useHistory();
const [tabArrLen, setTabArrLen] = useState<number>(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 = (
<SentryRoute
component={GeneralSettings}
location={location}
path={`${path}/general`}
/>
);
const pageMenuItems: any[] = [
{
icon: "book-line",
className: "documentation-page-menu-item",
onSelect: () => {},
text: createMessage(DOCUMENTATION),
},
];
const MemberSettingsComponent = (
<SentryRoute
component={useCallback(
(props: any) => (
<MemberSettings {...props} searchValue={searchValue} />
),
[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}
/>
</StyledStickyHeader>
<TabsWrapper
className="tabs-wrapper"
data-testid="t--user-edit-tabs-wrapper"
>
<Tabs
defaultValue={currentTab}
onValueChange={(key: string) =>
navigateToTab(key, location, history)
}
value={currentTab}
>
<TabsList>
{tabArr.map((tab) => {
return (
<Tab
data-testid={`t--tab-${tab.key}`}
key={tab.key}
value={tab.key}
>
<div className="tab-item">{tab.title}</div>
</Tab>
);
})}
</TabsList>
{tabArr.map((tab) => {
return (
<TabPanel className="tab-panel" key={tab.key} value={tab.key}>
{tab.panelComponent}
</TabPanel>
);
})}
</Tabs>
</TabsWrapper>
<WorkspaceSettingsTabs
currentTab={currentTab}
hasManageWorkspacePermissions={hasManageWorkspacePermissions}
isMemberofTheWorkspace={showMembersTab}
searchValue={searchValue}
setTabArrLen={setTabArrLen}
workspacePermissions={currentWorkspace?.userPermissions}
/>
</SettingsWrapper>
{currentWorkspace && (
<FormDialogComponent

View File

@ -452,12 +452,12 @@ function* updateDatasourceSaga(
>,
) {
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];