## Description Removing the bulk delete applications feature as it not generally used by many users and this was creating some UI conflicts rather than giving us some valuable output. Please follow this thread for more context : https://theappsmith.slack.com/archives/C02Q4B2AGM8/p1706249168968689 #### PR fixes following issue(s) Fixes #30660 > if no issue exists, please create an issue and ask the maintainers about this first > > #### Media > A video or a GIF is preferred. when using Loom, don’t embed because it looks like it’s a GIF. instead, just link to the video > > #### Type of change > Please delete options that are not relevant. - Bug fix (non-breaking change which fixes an issue) - New feature (non-breaking change which adds functionality) - Breaking change (fix or feature that would cause existing functionality to not work as expected) - Chore (housekeeping or task changes that don't impact user perception) - This change requires a documentation update > > > ## Testing > #### How Has This Been Tested? > Please describe the tests that you ran to verify your changes. Also list any relevant details for your test configuration. > Delete anything that is not relevant - [ ] Manual - [ ] JUnit - [ ] Jest - [ ] Cypress > > #### Test Plan > Add Testsmith test cases links that relate to this PR > > #### Issues raised during DP testing > Link issues raised during DP testing for better visiblity and tracking (copy link from comments dropped on this PR) > > > ## Checklist: #### Dev activity - [ ] My code follows the style guidelines of this project - [ ] I have performed a self-review of my own code - [ ] I have commented my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [ ] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [ ] New and existing unit tests pass locally with my changes - [ ] PR is being merged under a feature flag #### QA activity: - [ ] [Speedbreak features](https://github.com/appsmithorg/TestSmith/wiki/Guidelines-for-test-plans#speedbreakers-) have been covered - [ ] Test plan covers all impacted features and [areas of interest](https://github.com/appsmithorg/TestSmith/wiki/Guidelines-for-test-plans#areas-of-interest-) - [ ] Test plan has been peer reviewed by project stakeholders and other QA members - [ ] Manually tested functionality on DP - [ ] We had an implementation alignment call with stakeholders post QA Round 2 - [ ] Cypress test cases have been added and approved by SDET/manual QA - [ ] Added `Test Plan Approved` label after Cypress tests were reviewed - [ ] Added `Test Plan Approved` label after JUnit tests were reviewed <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **Refactor** - Streamlined the application deletion process to handle multiple deletions more efficiently. - **Bug Fixes** - Removed redundant code and unused features related to the multiple selection and deletion of applications. - **Chores** - Cleaned up various files by removing unused imports, constants, and state variables. - **Documentation** - Adjusted code comments and documentation to reflect removal of multiple application deletion features and related UI elements. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1085 lines
34 KiB
TypeScript
1085 lines
34 KiB
TypeScript
import React, {
|
|
Component,
|
|
useCallback,
|
|
useContext,
|
|
useEffect,
|
|
useState,
|
|
} from "react";
|
|
import styled, { ThemeContext } from "styled-components";
|
|
import { connect, useDispatch, useSelector } from "react-redux";
|
|
import MediaQuery from "react-responsive";
|
|
import { useHistory, useLocation, useRouteMatch } from "react-router-dom";
|
|
import type { AppState } from "@appsmith/reducers";
|
|
import { Classes as BlueprintClasses } from "@blueprintjs/core";
|
|
import {
|
|
thinScrollbar,
|
|
truncateTextUsingEllipsis,
|
|
} from "constants/DefaultTheme";
|
|
import {
|
|
getApplicationList,
|
|
getApplicationSearchKeyword,
|
|
getCreateApplicationError,
|
|
getCurrentApplicationIdForCreateNewApp,
|
|
getIsCreatingApplication,
|
|
getIsDeletingApplication,
|
|
} from "@appsmith/selectors/applicationSelectors";
|
|
import type { ApplicationPayload } from "@appsmith/constants/ReduxActionConstants";
|
|
import { ReduxActionTypes } from "@appsmith/constants/ReduxActionConstants";
|
|
import PageWrapper from "pages/common/PageWrapper";
|
|
import WorkspaceInviteUsersForm from "pages/workspace/WorkspaceInviteUsersForm";
|
|
import type { User } from "constants/userConstants";
|
|
import { getCurrentUser } from "selectors/usersSelectors";
|
|
import {
|
|
AppIconCollection,
|
|
Classes,
|
|
EditableText,
|
|
MenuItem as ListItem,
|
|
Text,
|
|
TextType,
|
|
} from "design-system-old";
|
|
import {
|
|
Button,
|
|
Icon,
|
|
Text as NewText,
|
|
Option,
|
|
Select,
|
|
Tooltip,
|
|
} from "design-system";
|
|
import { updateApplication } from "@appsmith/actions/applicationActions";
|
|
import { Position } from "@blueprintjs/core/lib/esm/common/position";
|
|
import type { UpdateApplicationPayload } from "@appsmith/api/ApplicationApi";
|
|
import PerformanceTracker, {
|
|
PerformanceTransactionName,
|
|
} from "utils/PerformanceTracker";
|
|
import { loadingUserWorkspaces } from "pages/Applications/ApplicationLoaders";
|
|
import type { creatingApplicationMap } from "@appsmith/reducers/uiReducers/applicationsReducer";
|
|
import {
|
|
deleteWorkspace,
|
|
fetchAllWorkspaces,
|
|
fetchEntitiesOfWorkspace,
|
|
resetCurrentWorkspace,
|
|
saveWorkspace,
|
|
} from "@appsmith/actions/workspaceActions";
|
|
import { leaveWorkspace } from "actions/userActions";
|
|
import CenteredWrapper from "components/designSystems/appsmith/CenteredWrapper";
|
|
import NoSearchImage from "assets/images/NoSearchResult.svg";
|
|
import { getNextEntityName, getRandomPaletteColor } from "utils/AppsmithUtils";
|
|
import { createWorkspaceSubmitHandler } from "@appsmith/pages/workspace/helpers";
|
|
import ImportApplicationModal from "pages/Applications/ImportApplicationModal";
|
|
import {
|
|
CREATE_A_NEW_WORKSPACE,
|
|
createMessage,
|
|
INVITE_USERS_PLACEHOLDER,
|
|
NO_APPS_FOUND,
|
|
NO_WORKSPACE_DESCRIPTION,
|
|
NO_WORKSPACE_HEADING,
|
|
WORKSPACES_HEADING,
|
|
} from "@appsmith/constants/messages";
|
|
|
|
import { setHeaderMeta } from "actions/themeActions";
|
|
import SharedUserList from "pages/common/SharedUserList";
|
|
import { useIsMobileDevice } from "utils/hooks/useDeviceDetect";
|
|
import { Indices } from "constants/Layers";
|
|
import GitSyncModal from "pages/Editor/gitSync/GitSyncModal";
|
|
import ReconnectDatasourceModal from "pages/Editor/gitSync/ReconnectDatasourceModal";
|
|
import { MOBILE_MAX_WIDTH } from "constants/AppConstants";
|
|
import urlBuilder from "@appsmith/entities/URLRedirect/URLAssembly";
|
|
import RepoLimitExceededErrorModal from "pages/Editor/gitSync/RepoLimitExceededErrorModal";
|
|
import { resetEditorRequest } from "actions/initActions";
|
|
import {
|
|
hasCreateNewAppPermission,
|
|
hasDeleteWorkspacePermission,
|
|
hasManageWorkspaceEnvironmentPermission,
|
|
isPermitted,
|
|
PERMISSION_TYPE,
|
|
} from "@appsmith/utils/permissionHelpers";
|
|
import { getTenantPermissions } from "@appsmith/selectors/tenantSelectors";
|
|
import { getAppsmithConfigs } from "@appsmith/configs";
|
|
import FormDialogComponent from "components/editorComponents/form/FormDialogComponent";
|
|
import WorkspaceMenu from "@appsmith/pages/Applications/WorkspaceMenu";
|
|
import ApplicationCardList from "@appsmith/pages/Applications/ApplicationCardList";
|
|
import { usePackage } from "@appsmith/pages/Applications/helpers";
|
|
import PackageCardList from "@appsmith/pages/Applications/PackageCardList";
|
|
import WorkspaceAction from "@appsmith/pages/Applications/WorkspaceAction";
|
|
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 WorkflowCardList from "@appsmith/pages/Applications/WorkflowCardList";
|
|
import { allowManageEnvironmentAccessForUser } from "@appsmith/selectors/environmentSelectors";
|
|
import CreateNewAppsOption from "@appsmith/pages/Applications/CreateNewAppsOption";
|
|
import { resetCurrentApplicationIdForCreateNewApp } from "actions/onboardingActions";
|
|
import {
|
|
getFetchedWorkspaces,
|
|
getIsDeletingWorkspace,
|
|
getIsFetchingWorkspaces,
|
|
getIsSavingWorkspaceInfo,
|
|
} from "@appsmith/selectors/workspaceSelectors";
|
|
import type { Workspace } from "@appsmith/constants/workspaceConstants";
|
|
import { getPackagesList } from "@appsmith/selectors/packageSelectors";
|
|
import {
|
|
getApplicationsOfWorkspace,
|
|
getIsFetchingApplications,
|
|
getCurrentWorkspaceId,
|
|
} from "@appsmith/selectors/selectedWorkspaceSelectors";
|
|
import { shouldShowLicenseBanner } from "@appsmith/selectors/tenantSelectors";
|
|
import { getWorkflowsList } from "@appsmith/selectors/workflowSelectors";
|
|
|
|
export const { cloudHosting } = getAppsmithConfigs();
|
|
|
|
export const CONTAINER_WRAPPER_PADDING = "var(--ads-v2-spaces-7)";
|
|
|
|
export const WorkspaceDropDown = styled.div<{ isMobile?: boolean }>`
|
|
display: flex;
|
|
padding: ${(props) => (props.isMobile ? `10px 16px` : `24px 0`)};
|
|
font-size: ${(props) => props.theme.fontSizes[1]}px;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
${({ isMobile }) =>
|
|
isMobile &&
|
|
`
|
|
background-color: #fff;
|
|
z-index: ${Indices.Layer8};
|
|
`}
|
|
`;
|
|
|
|
export const WorkspaceSection = styled.div<{ isMobile?: boolean }>`
|
|
padding: ${({ isMobile }) =>
|
|
isMobile ? 0 : `0 ${CONTAINER_WRAPPER_PADDING}`};
|
|
margin-bottom: ${({ isMobile }) => (isMobile ? `8` : `0`)}px;
|
|
`;
|
|
|
|
export const LeftPaneWrapper = styled.div<{ isBannerVisible?: boolean }>`
|
|
overflow: auto;
|
|
width: ${(props) => props.theme.homePage.sidebar}px;
|
|
height: ${(props) =>
|
|
props.isBannerVisible
|
|
? `calc(100% - ${props.theme.homePage.header * 2}px)`
|
|
: "100%"};
|
|
display: flex;
|
|
padding-top: 16px;
|
|
flex-direction: column;
|
|
position: fixed;
|
|
top: calc(
|
|
${(props) => props.theme.homePage.header}px +
|
|
${(props) => (props.isBannerVisible ? 40 : 0)}px
|
|
);
|
|
border-right: 1px solid var(--ads-v2-color-border);
|
|
padding: 0px 4px;
|
|
margin: 0px 8px;
|
|
`;
|
|
|
|
export const ApplicationContainer = styled.div<{ isMobile?: boolean }>`
|
|
${({ isMobile }) =>
|
|
isMobile &&
|
|
`
|
|
margin-left: 0;
|
|
width: 100%;
|
|
padding: 0;
|
|
`}
|
|
`;
|
|
|
|
export const ItemWrapper = styled.div`
|
|
padding: 16px;
|
|
`;
|
|
export const StyledIcon = styled(Icon)`
|
|
margin-right: 11px;
|
|
`;
|
|
export const WorkspaceShareUsers = styled.div`
|
|
display: flex;
|
|
align-items: center;
|
|
|
|
& .t--options-icon {
|
|
margin-left: 8px;
|
|
|
|
svg {
|
|
path {
|
|
fill: #090707;
|
|
}
|
|
}
|
|
}
|
|
|
|
& .t--new-button {
|
|
margin-left: 8px;
|
|
}
|
|
`;
|
|
|
|
export const NoAppsFound = styled.div`
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 100%;
|
|
|
|
& > span {
|
|
margin-bottom: 24px;
|
|
}
|
|
`;
|
|
|
|
export function Item(props: {
|
|
label: string;
|
|
textType: TextType;
|
|
icon?: string;
|
|
isFetchingApplications?: boolean;
|
|
}) {
|
|
return (
|
|
<ItemWrapper>
|
|
{props.icon && <StyledIcon name={props.icon} />}
|
|
<Text
|
|
className={
|
|
!!props.isFetchingApplications ? BlueprintClasses.SKELETON : ""
|
|
}
|
|
color={"var(--ads-v2-color-fg-emphasis)"}
|
|
type={props.textType}
|
|
>
|
|
{" "}
|
|
{props.label}
|
|
</Text>
|
|
</ItemWrapper>
|
|
);
|
|
}
|
|
|
|
const LeftPaneDataSection = styled.div<{ isBannerVisible?: boolean }>`
|
|
position: relative;
|
|
height: calc(100vh - ${(props) => 48 + (props.isBannerVisible ? 48 : 0)}px);
|
|
display: flex;
|
|
flex-direction: column;
|
|
button {
|
|
height: 34px !important;
|
|
}
|
|
`;
|
|
|
|
export function LeftPaneSection(props: {
|
|
heading: string;
|
|
children?: any;
|
|
isFetchingWorkspaces: boolean;
|
|
isBannerVisible?: boolean;
|
|
}) {
|
|
const dispatch = useDispatch();
|
|
const isFeatureEnabled = useFeatureFlag(FEATURE_FLAG.license_gac_enabled);
|
|
const tenantPermissions = useSelector(getTenantPermissions);
|
|
const fetchedWorkspaces = useSelector(getFetchedWorkspaces);
|
|
|
|
const canCreateWorkspace = getHasCreateWorkspacePermission(
|
|
isFeatureEnabled,
|
|
tenantPermissions,
|
|
);
|
|
|
|
const createNewWorkspace = async () => {
|
|
await submitCreateWorkspaceForm(
|
|
{
|
|
name: getNextEntityName(
|
|
"Untitled workspace ",
|
|
fetchedWorkspaces.map((el: any) => el.name),
|
|
),
|
|
},
|
|
dispatch,
|
|
);
|
|
dispatch(fetchAllWorkspaces());
|
|
};
|
|
|
|
return (
|
|
<LeftPaneDataSection isBannerVisible={props.isBannerVisible}>
|
|
<div className="flex items-center justify-between py-3">
|
|
<NewText kind="heading-xs">{props.heading}</NewText>
|
|
{canCreateWorkspace && (
|
|
<Tooltip
|
|
content={createMessage(CREATE_A_NEW_WORKSPACE)}
|
|
placement="right"
|
|
>
|
|
<Button
|
|
data-testid="t--workspace-new-workspace-auto-create"
|
|
isDisabled={props.isFetchingWorkspaces}
|
|
kind="tertiary"
|
|
onClick={createNewWorkspace}
|
|
startIcon="add-line"
|
|
/>
|
|
</Tooltip>
|
|
)}
|
|
</div>
|
|
{props.children}
|
|
</LeftPaneDataSection>
|
|
);
|
|
}
|
|
|
|
export const StyledAnchor = styled.a`
|
|
position: relative;
|
|
top: -24px;
|
|
`;
|
|
|
|
export const WorkpsacesNavigator = styled.div`
|
|
overflow: auto;
|
|
margin-bottom: 4px;
|
|
${thinScrollbar};
|
|
.selected-workspace {
|
|
border-radius: 4px !important;
|
|
&:hover {
|
|
background-color: var(--ads-v2-color-bg-muted);
|
|
}
|
|
}
|
|
`;
|
|
|
|
export const textIconStyles = (props: { color: string; hover: string }) => {
|
|
return `
|
|
&&&&&& {
|
|
.${Classes.TEXT},.${Classes.ICON} svg path {
|
|
color: ${props.color};
|
|
stroke: ${props.color};
|
|
fill: ${props.color};
|
|
}
|
|
|
|
|
|
&:hover {
|
|
.${Classes.TEXT},.${Classes.ICON} svg path {
|
|
color: ${props.hover};
|
|
stroke: ${props.hover};
|
|
fill: ${props.hover};
|
|
}
|
|
}
|
|
}
|
|
`;
|
|
};
|
|
|
|
export function WorkspaceMenuItem({
|
|
isFetchingWorkspaces,
|
|
selected,
|
|
workspace,
|
|
}: any) {
|
|
const history = useHistory();
|
|
const location = useLocation();
|
|
|
|
const handleWorkspaceClick = () => {
|
|
const workspaceId = workspace?.id;
|
|
if (workspaceId) {
|
|
const newUrl = `${location.pathname}?workspaceId=${workspaceId}`;
|
|
history.push(newUrl);
|
|
}
|
|
};
|
|
|
|
if (!workspace.id) return null;
|
|
return (
|
|
<ListItem
|
|
className={selected ? "selected-workspace" : ""}
|
|
containerClassName={isFetchingWorkspaces ? BlueprintClasses.SKELETON : ""}
|
|
ellipsize={
|
|
isFetchingWorkspaces ? 100 : 22
|
|
} /* this is to avoid showing tooltip for loaders */
|
|
icon="group-2-line"
|
|
key={workspace?.id}
|
|
onSelect={handleWorkspaceClick}
|
|
selected={selected}
|
|
text={workspace?.name}
|
|
tooltipPos={Position.BOTTOM_LEFT}
|
|
/>
|
|
);
|
|
}
|
|
|
|
export const submitCreateWorkspaceForm = async (data: any, dispatch: any) => {
|
|
const result = await createWorkspaceSubmitHandler(data, dispatch);
|
|
return result;
|
|
};
|
|
|
|
export interface LeftPaneProps {
|
|
isBannerVisible?: boolean;
|
|
isFetchingWorkspaces: boolean;
|
|
workspaces: any;
|
|
activeWorkspaceId: string | undefined;
|
|
}
|
|
|
|
export function LeftPane(props: LeftPaneProps) {
|
|
const {
|
|
activeWorkspaceId,
|
|
isBannerVisible = false,
|
|
isFetchingWorkspaces,
|
|
workspaces,
|
|
} = props;
|
|
const isMobile = useIsMobileDevice();
|
|
|
|
if (isMobile) return null;
|
|
return (
|
|
<LeftPaneWrapper isBannerVisible={isBannerVisible}>
|
|
<LeftPaneSection
|
|
heading={createMessage(WORKSPACES_HEADING)}
|
|
isBannerVisible={isBannerVisible}
|
|
isFetchingWorkspaces={isFetchingWorkspaces}
|
|
>
|
|
<WorkpsacesNavigator data-testid="t--left-panel">
|
|
{workspaces &&
|
|
workspaces.map(
|
|
(workspace: any) =>
|
|
workspace && (
|
|
<WorkspaceMenuItem
|
|
isFetchingWorkspaces={isFetchingWorkspaces}
|
|
key={workspace?.id}
|
|
selected={workspace?.id === activeWorkspaceId}
|
|
workspace={workspace}
|
|
/>
|
|
),
|
|
)}
|
|
</WorkpsacesNavigator>
|
|
</LeftPaneSection>
|
|
</LeftPaneWrapper>
|
|
);
|
|
}
|
|
|
|
export const CreateNewLabel = styled(Text)`
|
|
margin-top: 18px;
|
|
`;
|
|
|
|
export const WorkspaceNameElement = styled.div<{ isMobile?: boolean }>`
|
|
max-width: ${({ isMobile }) => (isMobile ? 220 : 500)}px;
|
|
${truncateTextUsingEllipsis};
|
|
`;
|
|
|
|
export const WorkspaceNameHolder = styled(Text)`
|
|
display: flex;
|
|
align-items: center;
|
|
`;
|
|
|
|
export const WorkspaceNameWrapper = styled.div<{ disabled?: boolean }>`
|
|
${(props) => {
|
|
const color = props.disabled
|
|
? props.theme.colors.applications.workspaceColor
|
|
: props.theme.colors.applications.hover.workspaceColor[9];
|
|
return `${textIconStyles({
|
|
color: color,
|
|
hover: color,
|
|
})}`;
|
|
}}
|
|
.${Classes.ICON} {
|
|
display: ${(props) => (!props.disabled ? "inline" : "none")};
|
|
margin-left: 8px;
|
|
color: ${(props) => props.theme.colors.applications.iconColor};
|
|
}
|
|
`;
|
|
export const WorkspaceRename = styled(EditableText)`
|
|
padding: 0 2px;
|
|
`;
|
|
|
|
export const NoSearchResultImg = styled.img`
|
|
margin: 1em;
|
|
`;
|
|
|
|
export const ApplicationsWrapper = styled.div<{
|
|
isMobile: boolean;
|
|
isBannerVisible: boolean;
|
|
}>`
|
|
height: calc(100vh - ${(props) => props.theme.homePage.search.height - 40}px);
|
|
overflow: auto;
|
|
margin-left: ${(props) => props.theme.homePage.leftPane.width}px;
|
|
width: calc(100% - ${(props) => props.theme.homePage.leftPane.width}px);
|
|
scroll-behavior: smooth;
|
|
${({ isBannerVisible }) => (isBannerVisible ? "margin-top: 48px;" : "")}
|
|
${({ isMobile }) =>
|
|
isMobile
|
|
? `padding: ${CONTAINER_WRAPPER_PADDING} 0;`
|
|
: `padding: 0 0 ${CONTAINER_WRAPPER_PADDING};`}
|
|
|
|
${({ isMobile }) =>
|
|
isMobile &&
|
|
`
|
|
margin-left: 0;
|
|
width: 100%;
|
|
padding: 0;
|
|
`}
|
|
`;
|
|
|
|
export const WorkspaceSelectorWrapper = styled.div`
|
|
padding: 24px 10px 0;
|
|
`;
|
|
|
|
export function ApplicationsSection(props: any) {
|
|
const { activeWorkspaceId, applications, packages, workflows, workspaces } =
|
|
props;
|
|
const enableImportExport = true;
|
|
const dispatch = useDispatch();
|
|
const theme = useContext(ThemeContext);
|
|
const isSavingWorkspaceInfo = useSelector(getIsSavingWorkspaceInfo);
|
|
const isFetchingWorkspaces = useSelector(getIsFetchingWorkspaces);
|
|
const isFetchingApplications = useSelector(getIsFetchingApplications);
|
|
const isDeletingWorkspace = useSelector(getIsDeletingWorkspace);
|
|
const { isFetchingPackages } = usePackage();
|
|
const creatingApplicationMap = useSelector(getIsCreatingApplication);
|
|
const currentUser = useSelector(getCurrentUser);
|
|
const isMobile = useIsMobileDevice();
|
|
const deleteApplication = (applicationId: string) => {
|
|
if (applicationId && applicationId.length > 0) {
|
|
dispatch({
|
|
type: ReduxActionTypes.DELETE_APPLICATION_INIT,
|
|
payload: {
|
|
applicationId,
|
|
},
|
|
});
|
|
}
|
|
};
|
|
const [warnLeavingWorkspace, setWarnLeavingWorkspace] = useState(false);
|
|
const [warnDeleteWorkspace, setWarnDeleteWorkspace] = useState(false);
|
|
const [workspaceToOpenMenu, setWorkspaceToOpenMenu] = useState<string | null>(
|
|
null,
|
|
);
|
|
const isManageEnvironmentEnabled = useSelector(
|
|
allowManageEnvironmentAccessForUser,
|
|
);
|
|
const updateApplicationDispatch = (
|
|
id: string,
|
|
data: UpdateApplicationPayload,
|
|
) => {
|
|
dispatch(updateApplication(id, data));
|
|
};
|
|
const isLoadingResources =
|
|
isFetchingWorkspaces || isFetchingApplications || isFetchingPackages;
|
|
const isGACEnabled = useFeatureFlag(FEATURE_FLAG.license_gac_enabled);
|
|
|
|
useEffect(() => {
|
|
// Clears URL params cache
|
|
urlBuilder.resetURLParams();
|
|
}, []);
|
|
|
|
const [
|
|
selectedWorkspaceIdForImportApplication,
|
|
setSelectedWorkspaceIdForImportApplication,
|
|
] = useState<string | undefined>();
|
|
|
|
const leaveWS = (workspaceId: string) => {
|
|
setWarnLeavingWorkspace(false);
|
|
setWorkspaceToOpenMenu(null);
|
|
dispatch(leaveWorkspace(workspaceId));
|
|
};
|
|
|
|
const handleDeleteWorkspace = useCallback(
|
|
(workspaceId: string) => {
|
|
setWarnDeleteWorkspace(false);
|
|
setWorkspaceToOpenMenu(null);
|
|
dispatch(deleteWorkspace(workspaceId));
|
|
},
|
|
[dispatch],
|
|
);
|
|
|
|
const workspaceNameChange = (newName: string, workspaceId: string) => {
|
|
dispatch(
|
|
saveWorkspace({
|
|
id: workspaceId as string,
|
|
name: newName,
|
|
}),
|
|
);
|
|
};
|
|
|
|
function WorkspaceMenuTarget(props: {
|
|
workspaceName: string;
|
|
disabled?: boolean;
|
|
workspaceSlug: string;
|
|
}) {
|
|
const { disabled, workspaceName, workspaceSlug } = props;
|
|
|
|
return (
|
|
<WorkspaceNameWrapper
|
|
className="t--workspace-name-text"
|
|
disabled={disabled}
|
|
>
|
|
<StyledAnchor id={workspaceSlug} />
|
|
<WorkspaceNameHolder
|
|
className={isLoadingResources ? BlueprintClasses.SKELETON : ""}
|
|
type={TextType.H4}
|
|
>
|
|
<WorkspaceNameElement
|
|
className={isLoadingResources ? BlueprintClasses.SKELETON : ""}
|
|
isMobile={isMobile}
|
|
>
|
|
<NewText className="!font-semibold" kind="heading-l">
|
|
{workspaceName}
|
|
</NewText>
|
|
</WorkspaceNameElement>
|
|
</WorkspaceNameHolder>
|
|
</WorkspaceNameWrapper>
|
|
);
|
|
}
|
|
|
|
const createNewApplication = (
|
|
applicationName: string,
|
|
workspaceId: string,
|
|
) => {
|
|
const color = getRandomPaletteColor(theme.colors.appCardColors);
|
|
const icon =
|
|
AppIconCollection[Math.floor(Math.random() * AppIconCollection.length)];
|
|
|
|
return dispatch({
|
|
type: ReduxActionTypes.CREATE_APPLICATION_INIT,
|
|
payload: {
|
|
applicationName,
|
|
workspaceId,
|
|
icon,
|
|
color,
|
|
},
|
|
});
|
|
};
|
|
|
|
function NoWorkspaceFound() {
|
|
return (
|
|
<div className="flex flex-col items-center justify-center mt-[180px]">
|
|
<img
|
|
className="mb-6"
|
|
src="https://assets.appsmith.com/no-workspace-found.svg"
|
|
/>
|
|
<NewText className="!mb-3 !font-semibold" kind="heading-s">
|
|
{createMessage(NO_WORKSPACE_HEADING)}
|
|
</NewText>
|
|
<NewText className="w-[328px]" kind="heading-xs">
|
|
{createMessage(NO_WORKSPACE_DESCRIPTION)}
|
|
</NewText>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const activeWorkspace = workspaces.find(
|
|
(workspace: Workspace) => workspace.id === activeWorkspaceId,
|
|
);
|
|
if (!activeWorkspace && !isFetchingWorkspaces) return <NoWorkspaceFound />;
|
|
|
|
if (!activeWorkspace) return null;
|
|
|
|
let workspacesListComponent;
|
|
if (
|
|
!isLoadingResources &&
|
|
props.searchKeyword &&
|
|
props.searchKeyword.trim().length > 0 &&
|
|
workspaces?.length === 0
|
|
) {
|
|
workspacesListComponent = (
|
|
<CenteredWrapper
|
|
style={{
|
|
flexDirection: "column",
|
|
position: "static",
|
|
}}
|
|
>
|
|
<CreateNewLabel type={TextType.H4}>
|
|
{createMessage(NO_APPS_FOUND)}
|
|
</CreateNewLabel>
|
|
<NoSearchResultImg alt="No result found" src={NoSearchImage} />
|
|
</CenteredWrapper>
|
|
);
|
|
} else {
|
|
const hasManageWorkspacePermissions = isPermitted(
|
|
activeWorkspace.userPermissions,
|
|
PERMISSION_TYPE.MANAGE_WORKSPACE,
|
|
);
|
|
const canInviteToWorkspace = isPermitted(
|
|
activeWorkspace.userPermissions,
|
|
PERMISSION_TYPE.INVITE_USER_TO_WORKSPACE,
|
|
);
|
|
const canDeleteWorkspace = hasDeleteWorkspacePermission(
|
|
activeWorkspace?.userPermissions || [],
|
|
);
|
|
const hasCreateNewApplicationPermission =
|
|
hasCreateNewAppPermission(activeWorkspace.userPermissions) && !isMobile;
|
|
const renderManageEnvironmentMenu =
|
|
isManageEnvironmentEnabled &&
|
|
hasManageWorkspaceEnvironmentPermission(activeWorkspace.userPermissions);
|
|
const onClickAddNewAppButton = (workspaceId: string) => {
|
|
if (
|
|
Object.entries(creatingApplicationMap).length === 0 ||
|
|
(creatingApplicationMap && !creatingApplicationMap[workspaceId])
|
|
) {
|
|
createNewApplication(
|
|
getNextEntityName(
|
|
"Untitled application ",
|
|
applications.map((el: any) => el.name),
|
|
),
|
|
workspaceId,
|
|
);
|
|
}
|
|
};
|
|
|
|
const showWorkspaceMenuOptions =
|
|
canInviteToWorkspace ||
|
|
hasManageWorkspacePermissions ||
|
|
(canDeleteWorkspace && applications.length === 0) ||
|
|
renderManageEnvironmentMenu;
|
|
|
|
const handleResetMenuState = () => {
|
|
setWorkspaceToOpenMenu(null);
|
|
setWarnLeavingWorkspace(false);
|
|
setWarnDeleteWorkspace(false);
|
|
};
|
|
|
|
const handleWorkspaceMenuClose = (open: boolean) => {
|
|
if (!open && !warnLeavingWorkspace && !warnDeleteWorkspace) {
|
|
handleResetMenuState();
|
|
}
|
|
};
|
|
|
|
workspacesListComponent = (
|
|
<React.Fragment key={activeWorkspace.id}>
|
|
<WorkspaceSection className="t--workspace-section" isMobile={isMobile}>
|
|
<WorkspaceDropDown isMobile={isMobile}>
|
|
{(currentUser || isLoadingResources) &&
|
|
WorkspaceMenuTarget({
|
|
workspaceName: activeWorkspace.name,
|
|
workspaceSlug: activeWorkspace.id,
|
|
})}
|
|
{selectedWorkspaceIdForImportApplication && (
|
|
<ImportApplicationModal
|
|
isModalOpen={
|
|
selectedWorkspaceIdForImportApplication === activeWorkspace.id
|
|
}
|
|
onClose={() => setSelectedWorkspaceIdForImportApplication("")}
|
|
workspaceId={selectedWorkspaceIdForImportApplication}
|
|
/>
|
|
)}
|
|
{!isLoadingResources && (
|
|
<WorkspaceShareUsers>
|
|
<SharedUserList />
|
|
{canInviteToWorkspace && !isMobile && (
|
|
<FormDialogComponent
|
|
Form={WorkspaceInviteUsersForm}
|
|
placeholder={createMessage(
|
|
INVITE_USERS_PLACEHOLDER,
|
|
!isGACEnabled,
|
|
)}
|
|
workspace={activeWorkspace}
|
|
/>
|
|
)}
|
|
<WorkspaceAction
|
|
enableImportExport={enableImportExport}
|
|
isMobile={isMobile}
|
|
onCreateNewApplication={onClickAddNewAppButton}
|
|
setSelectedWorkspaceIdForImportApplication={
|
|
setSelectedWorkspaceIdForImportApplication
|
|
}
|
|
workspace={activeWorkspace}
|
|
workspaceId={activeWorkspaceId}
|
|
/>
|
|
{(currentUser || isLoadingResources) &&
|
|
!isMobile &&
|
|
showWorkspaceMenuOptions && (
|
|
<WorkspaceMenu
|
|
canDeleteWorkspace={
|
|
applications.length === 0 &&
|
|
packages.length === 0 &&
|
|
workflows.length === 0 &&
|
|
canDeleteWorkspace
|
|
}
|
|
canInviteToWorkspace={canInviteToWorkspace}
|
|
handleDeleteWorkspace={handleDeleteWorkspace}
|
|
handleResetMenuState={handleResetMenuState}
|
|
handleWorkspaceMenuClose={handleWorkspaceMenuClose}
|
|
hasCreateNewApplicationPermission={
|
|
hasCreateNewApplicationPermission
|
|
}
|
|
hasManageWorkspacePermissions={
|
|
hasManageWorkspacePermissions
|
|
}
|
|
isFetchingResources={isLoadingResources}
|
|
isSavingWorkspaceInfo={isSavingWorkspaceInfo}
|
|
leaveWS={leaveWS}
|
|
setWarnDeleteWorkspace={setWarnDeleteWorkspace}
|
|
setWarnLeavingWorkspace={setWarnLeavingWorkspace}
|
|
setWorkspaceToOpenMenu={setWorkspaceToOpenMenu}
|
|
warnDeleteWorkspace={warnDeleteWorkspace}
|
|
warnLeavingWorkspace={warnLeavingWorkspace}
|
|
workspace={activeWorkspace}
|
|
workspaceNameChange={workspaceNameChange}
|
|
workspaceToOpenMenu={workspaceToOpenMenu}
|
|
/>
|
|
)}
|
|
</WorkspaceShareUsers>
|
|
)}
|
|
</WorkspaceDropDown>
|
|
{isLoadingResources || isDeletingWorkspace ? (
|
|
<ResourceListLoader isMobile={isMobile} resources={applications} />
|
|
) : (
|
|
<>
|
|
<ApplicationCardList
|
|
applications={applications}
|
|
canInviteToWorkspace={canInviteToWorkspace}
|
|
deleteApplication={deleteApplication}
|
|
enableImportExport={enableImportExport}
|
|
hasCreateNewApplicationPermission={
|
|
hasCreateNewApplicationPermission
|
|
}
|
|
hasManageWorkspacePermissions={hasManageWorkspacePermissions}
|
|
isMobile={isMobile}
|
|
onClickAddNewButton={onClickAddNewAppButton}
|
|
updateApplicationDispatch={updateApplicationDispatch}
|
|
workspaceId={activeWorkspace.id}
|
|
/>
|
|
<PackageCardList
|
|
isMobile={isMobile}
|
|
packages={packages}
|
|
workspace={activeWorkspace}
|
|
workspaceId={activeWorkspace.id}
|
|
/>
|
|
<WorkflowCardList
|
|
isMobile={isMobile}
|
|
workflows={workflows}
|
|
workspace={activeWorkspace}
|
|
workspaceId={activeWorkspace.id}
|
|
/>
|
|
</>
|
|
)}
|
|
</WorkspaceSection>
|
|
</React.Fragment>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<ApplicationContainer
|
|
className="t--applications-container"
|
|
isMobile={isMobile}
|
|
>
|
|
{workspacesListComponent}
|
|
<GitSyncModal isImport />
|
|
<ReconnectDatasourceModal />
|
|
</ApplicationContainer>
|
|
);
|
|
}
|
|
|
|
export const ApplictionsMainPage = (props: any) => {
|
|
const { searchKeyword } = props;
|
|
const location = useLocation();
|
|
const urlParams = new URLSearchParams(location.search);
|
|
const workspaceIdFromQueryParams = urlParams.get("workspaceId");
|
|
const dispatch = useDispatch();
|
|
const history = useHistory();
|
|
const isFetchingWorkspaces = useSelector(getIsFetchingWorkspaces);
|
|
const fetchedWorkspaces = useSelector(getFetchedWorkspaces);
|
|
const fetchedApplications = useSelector(getApplicationsOfWorkspace);
|
|
const fetchedPackages = useSelector(getPackagesList);
|
|
const fetchedWorkflows = useSelector(getWorkflowsList);
|
|
const fetchedWorkspaceId = useSelector(getCurrentWorkspaceId);
|
|
const showBanner = useSelector(shouldShowLicenseBanner);
|
|
const isHomePage = useRouteMatch("/applications")?.isExact;
|
|
const isLicensePage = useRouteMatch("/license")?.isExact;
|
|
const isBannerVisible = showBanner && (isHomePage || isLicensePage);
|
|
|
|
let workspaces: any;
|
|
if (!isFetchingWorkspaces) {
|
|
workspaces = fetchedWorkspaces;
|
|
} else {
|
|
workspaces = loadingUserWorkspaces as any;
|
|
}
|
|
|
|
const [activeWorkspaceId, setActiveWorkspaceId] = useState<
|
|
string | undefined
|
|
>(
|
|
workspaceIdFromQueryParams ? workspaceIdFromQueryParams : workspaces[0]?.id,
|
|
);
|
|
|
|
useEffect(() => {
|
|
setActiveWorkspaceId(
|
|
workspaceIdFromQueryParams
|
|
? workspaceIdFromQueryParams
|
|
: workspaces[0]?.id,
|
|
);
|
|
if (
|
|
activeWorkspaceId &&
|
|
fetchedWorkspaceId &&
|
|
fetchedWorkspaceId !== activeWorkspaceId
|
|
) {
|
|
const activeWorkspace: Workspace = workspaces.find(
|
|
(workspace: Workspace) => workspace.id === activeWorkspaceId,
|
|
);
|
|
if (activeWorkspace) {
|
|
dispatch({
|
|
type: ReduxActionTypes.SET_CURRENT_WORKSPACE,
|
|
payload: { ...activeWorkspace },
|
|
});
|
|
dispatch(
|
|
fetchEntitiesOfWorkspace({
|
|
workspaceId: activeWorkspaceId,
|
|
}),
|
|
);
|
|
}
|
|
}
|
|
}, [workspaceIdFromQueryParams, fetchedWorkspaces, activeWorkspaceId]);
|
|
|
|
const packagesOfWorkspace = activeWorkspaceId
|
|
? fetchedPackages.filter((pkg) => pkg.workspaceId === activeWorkspaceId)
|
|
: [];
|
|
|
|
const workflowsOfWorkspace = activeWorkspaceId
|
|
? fetchedWorkflows.filter(
|
|
(workflow) => workflow.workspaceId === activeWorkspaceId,
|
|
)
|
|
: [];
|
|
|
|
return (
|
|
<PageWrapper displayName="Applications">
|
|
<LeftPane
|
|
activeWorkspaceId={activeWorkspaceId}
|
|
isBannerVisible={isBannerVisible}
|
|
isFetchingWorkspaces={isFetchingWorkspaces}
|
|
workspaces={workspaces}
|
|
/>
|
|
<MediaQuery maxWidth={MOBILE_MAX_WIDTH}>
|
|
{(matches: boolean) => (
|
|
<ApplicationsWrapper
|
|
isBannerVisible={!!isBannerVisible}
|
|
isMobile={matches}
|
|
>
|
|
{!isFetchingWorkspaces && matches && (
|
|
<WorkspaceSelectorWrapper>
|
|
<Select
|
|
onSelect={(val) =>
|
|
history.push(`/applications?workspaceId=${val}`)
|
|
}
|
|
value={activeWorkspaceId}
|
|
>
|
|
{workspaces.map((workspace: Workspace) => (
|
|
<Option key={workspace.id} value={workspace.id}>
|
|
{workspace.name}
|
|
</Option>
|
|
))}
|
|
</Select>
|
|
</WorkspaceSelectorWrapper>
|
|
)}
|
|
<ApplicationsSection
|
|
activeWorkspaceId={activeWorkspaceId}
|
|
applications={fetchedApplications}
|
|
packages={packagesOfWorkspace}
|
|
searchKeyword={searchKeyword}
|
|
workflows={workflowsOfWorkspace}
|
|
workspaces={workspaces}
|
|
/>
|
|
<RepoLimitExceededErrorModal />
|
|
</ApplicationsWrapper>
|
|
)}
|
|
</MediaQuery>
|
|
</PageWrapper>
|
|
);
|
|
};
|
|
|
|
export interface ApplicationProps {
|
|
applicationList: ApplicationPayload[];
|
|
searchApplications: (keyword: string) => void;
|
|
isCreatingApplication: creatingApplicationMap;
|
|
isFetchingApplications: boolean;
|
|
createApplicationError?: string;
|
|
deleteApplication: (id: string) => void;
|
|
deletingApplication: boolean;
|
|
getAllWorkspaces: (params: {
|
|
fetchEntities: boolean;
|
|
workspaceId: string | null;
|
|
}) => void;
|
|
workspaces: any;
|
|
currentUser?: User;
|
|
searchKeyword: string | undefined;
|
|
setHeaderMetaData: (
|
|
hideHeaderShadow: boolean,
|
|
showHeaderSeparator: boolean,
|
|
) => void;
|
|
resetEditor: () => void;
|
|
queryModuleFeatureFlagEnabled: boolean;
|
|
resetCurrentWorkspace: () => void;
|
|
currentApplicationIdForCreateNewApp?: string;
|
|
resetCurrentApplicationIdForCreateNewApp: () => void;
|
|
currentWorkspaceId: string;
|
|
}
|
|
|
|
export interface ApplicationState {
|
|
selectedWorkspaceId: string;
|
|
showOnboardingForm: boolean;
|
|
}
|
|
|
|
export class Applications<
|
|
Props extends ApplicationProps,
|
|
State extends ApplicationState,
|
|
> extends Component<Props, State> {
|
|
constructor(props: Props) {
|
|
super(props);
|
|
|
|
this.state = {
|
|
selectedWorkspaceId: "",
|
|
showOnboardingForm: false,
|
|
} as State;
|
|
}
|
|
|
|
componentDidMount() {
|
|
PerformanceTracker.stopTracking(PerformanceTransactionName.LOGIN_CLICK);
|
|
PerformanceTracker.stopTracking(PerformanceTransactionName.SIGN_UP);
|
|
const urlParams = new URLSearchParams(window.location.search);
|
|
const workspaceIdFromQueryParams = urlParams.get("workspaceId");
|
|
this.props.getAllWorkspaces({
|
|
workspaceId: workspaceIdFromQueryParams,
|
|
fetchEntities: true,
|
|
});
|
|
this.props.setHeaderMetaData(true, true);
|
|
|
|
// Whenever we go back to home page from application page,
|
|
// we should reset current workspace, as this workspace is not in context anymore
|
|
this.props.resetCurrentWorkspace();
|
|
}
|
|
|
|
componentWillUnmount() {
|
|
this.props.setHeaderMetaData(false, false);
|
|
this.props.searchApplications("");
|
|
}
|
|
|
|
public render() {
|
|
return this.props.currentApplicationIdForCreateNewApp ? (
|
|
// Workspace id condition is added to ensure that we have workspace id present before we show 3 options
|
|
// as workspace id is required to fetch plugins
|
|
!!this.props.currentWorkspaceId ? (
|
|
<CreateNewAppsOption
|
|
currentApplicationIdForCreateNewApp={
|
|
this.props.currentApplicationIdForCreateNewApp
|
|
}
|
|
onClickBack={this.props.resetCurrentApplicationIdForCreateNewApp}
|
|
/>
|
|
) : null
|
|
) : (
|
|
<ApplictionsMainPage
|
|
searchApplications={this.props.searchApplications}
|
|
searchKeyword={this.props.searchKeyword}
|
|
/>
|
|
);
|
|
}
|
|
}
|
|
|
|
export const mapStateToProps = (state: AppState) => ({
|
|
applicationList: getApplicationList(state),
|
|
isFetchingApplications: getIsFetchingApplications(state),
|
|
isCreatingApplication: getIsCreatingApplication(state),
|
|
createApplicationError: getCreateApplicationError(state),
|
|
deletingApplication: getIsDeletingApplication(state),
|
|
workspaces: getFetchedWorkspaces(state),
|
|
currentUser: getCurrentUser(state),
|
|
searchKeyword: getApplicationSearchKeyword(state),
|
|
currentApplicationIdForCreateNewApp:
|
|
getCurrentApplicationIdForCreateNewApp(state),
|
|
currentWorkspaceId: getCurrentWorkspaceId(state),
|
|
});
|
|
|
|
export const mapDispatchToProps = (dispatch: any) => ({
|
|
getAllWorkspaces: ({
|
|
fetchEntities,
|
|
workspaceId,
|
|
}: {
|
|
fetchEntities: boolean;
|
|
workspaceId: string | null;
|
|
}) => {
|
|
dispatch(fetchAllWorkspaces({ workspaceId, fetchEntities }));
|
|
},
|
|
resetEditor: () => {
|
|
dispatch(resetEditorRequest());
|
|
},
|
|
searchApplications: (keyword: string) => {
|
|
dispatch({
|
|
type: ReduxActionTypes.SEARCH_APPLICATIONS,
|
|
payload: {
|
|
keyword,
|
|
},
|
|
});
|
|
},
|
|
setHeaderMetaData: (
|
|
hideHeaderShadow: boolean,
|
|
showHeaderSeparator: boolean,
|
|
) => {
|
|
dispatch(setHeaderMeta(hideHeaderShadow, showHeaderSeparator));
|
|
},
|
|
resetCurrentWorkspace: () => dispatch(resetCurrentWorkspace()),
|
|
resetCurrentApplicationIdForCreateNewApp: () =>
|
|
dispatch(resetCurrentApplicationIdForCreateNewApp()),
|
|
});
|
|
|
|
export default connect(mapStateToProps, mapDispatchToProps)(Applications);
|