diff --git a/app/client/src/ce/components/editorComponents/Debugger/ErrorLogs/getLogIconForEntity.tsx b/app/client/src/ce/components/editorComponents/Debugger/ErrorLogs/getLogIconForEntity.tsx index bf0c2283989..053d4892156 100644 --- a/app/client/src/ce/components/editorComponents/Debugger/ErrorLogs/getLogIconForEntity.tsx +++ b/app/client/src/ce/components/editorComponents/Debugger/ErrorLogs/getLogIconForEntity.tsx @@ -10,6 +10,8 @@ import { } from "pages/Editor/Explorer/ExplorerIcons"; import { getAssetUrl } from "ee/utils/airgapHelpers"; import { ENTITY_TYPE } from "ee/entities/DataTree/types"; +import { ENTITY_TYPE as DEBUGGER_ENTITY_TYPE } from "ee/entities/AppsmithConsole/utils"; +import { Icon } from "@appsmith/ads"; type IconProps = LogItemProps & { pluginImages: Record; @@ -47,4 +49,7 @@ export const getIconForEntity: IconEntityMapper = { return icon; }, + [DEBUGGER_ENTITY_TYPE.GIT]: () => { + return ; + }, }; diff --git a/app/client/src/ce/components/editorComponents/Debugger/entityTypeLinkMap.tsx b/app/client/src/ce/components/editorComponents/Debugger/entityTypeLinkMap.tsx index 4c86ca66210..754efc8102c 100644 --- a/app/client/src/ce/components/editorComponents/Debugger/entityTypeLinkMap.tsx +++ b/app/client/src/ce/components/editorComponents/Debugger/entityTypeLinkMap.tsx @@ -3,10 +3,12 @@ import DatasourceLink from "components/editorComponents/Debugger/DataSourceLink" import WidgetLink from "components/editorComponents/Debugger/WidgetLink"; import JSCollectionLink from "components/editorComponents/Debugger/JSCollectionLink"; import { ENTITY_TYPE } from "ee/entities/AppsmithConsole/utils"; +import GitEntityLink from "components/editorComponents/Debugger/GitEntityLink"; export const entityTypeLinkMap = { [ENTITY_TYPE.WIDGET]: WidgetLink, [ENTITY_TYPE.ACTION]: ActionLink, [ENTITY_TYPE.DATASOURCE]: DatasourceLink, [ENTITY_TYPE.JSACTION]: JSCollectionLink, + [ENTITY_TYPE.GIT]: GitEntityLink, }; diff --git a/app/client/src/ce/constants/messages.ts b/app/client/src/ce/constants/messages.ts index d8360fa9649..68afc15bf8d 100644 --- a/app/client/src/ce/constants/messages.ts +++ b/app/client/src/ce/constants/messages.ts @@ -1128,6 +1128,14 @@ export const GIT_AUTHOR = () => "Git author"; export const DISCONNECT_GIT = () => "Disconnect Git"; export const DISCONNECT_GIT_MESSAGE = () => "This is irreversible. If you wish to reconnect, you will have to connect a new empty repository."; +export const GENERATE_DEPLOY_KEY_TITLE = () => "Deploy key"; +export const GENERATE_DEPLOY_KEY_MESSAGE = () => + "Generate a new deploy key. Then, add the new key to your Git repository to restore this app’s connection."; +export const GENERATE_DEPLOY_KEY_BTN = () => "Generate deploy key"; +export const GENERATE_DEPLOY_KEY_MODAL_TITLE = () => "Generate deploy key"; +export const GENERATE_DEPLOY_KEY_MODAL_WAIT_TEXT = () => "Saving deploy key..."; +export const INVALID_DEPLOY_KEY_WARNING = () => + "Your current deploy key may be invalid or outdated. Try generating a new key below and adding it to your Git repository."; export const AUTOCOMMIT = () => "Auto - commit"; export const AUTOCOMMIT_MESSAGE = () => "Enable/disable auto migrations from Appsmith."; diff --git a/app/client/src/ce/entities/AppsmithConsole/utils.ts b/app/client/src/ce/entities/AppsmithConsole/utils.ts index e27e10cfe0b..6065b99b9c0 100644 --- a/app/client/src/ce/entities/AppsmithConsole/utils.ts +++ b/app/client/src/ce/entities/AppsmithConsole/utils.ts @@ -8,6 +8,7 @@ export enum ENTITY_TYPE { DATASOURCE = "DATASOURCE", WIDGET = "WIDGET", JSACTION = "JSACTION", + GIT = "GIT", } export enum PLATFORM_ERROR { diff --git a/app/client/src/components/editorComponents/Debugger/GitEntityLink.tsx b/app/client/src/components/editorComponents/Debugger/GitEntityLink.tsx new file mode 100644 index 00000000000..1b825e38d73 --- /dev/null +++ b/app/client/src/components/editorComponents/Debugger/GitEntityLink.tsx @@ -0,0 +1,22 @@ +import React from "react"; +import { DebuggerEntityLink, type EntityLinkProps } from "./DebuggerEntityLink"; +import { useCallback } from "react"; +import useSettings from "git/hooks/useSettings"; +import { GitSettingsTab } from "git/constants/enums"; + +export default function GitEntityLink(props: EntityLinkProps) { + const { toggleSettingsModal } = useSettings(); + + const onClick = useCallback(() => { + toggleSettingsModal(true, GitSettingsTab.General); + }, [toggleSettingsModal]); + + return ( + + ); +} diff --git a/app/client/src/entities/AppsmithConsole/logtype.ts b/app/client/src/entities/AppsmithConsole/logtype.ts index b81f013242b..17c645707e1 100644 --- a/app/client/src/entities/AppsmithConsole/logtype.ts +++ b/app/client/src/entities/AppsmithConsole/logtype.ts @@ -15,6 +15,7 @@ enum LOG_TYPE { CYCLIC_DEPENDENCY_ERROR, LINT_ERROR, MISSING_MODULE, + INVALID_GIT_DEPLOY_KEY, } export default LOG_TYPE; diff --git a/app/client/src/git/ce/components/GitModals/index.tsx b/app/client/src/git/ce/components/GitModals/index.tsx index 9e75f833386..4d77a2f4c92 100644 --- a/app/client/src/git/ce/components/GitModals/index.tsx +++ b/app/client/src/git/ce/components/GitModals/index.tsx @@ -7,6 +7,7 @@ import OpsModal from "git/components/OpsModal"; import RepoLimitErrorModal from "git/components/RepoLimitErrorModal"; import SettingsModal from "git/components/SettingsModal"; import React from "react"; +import GenerateDeployKeyModal from "git/components/GenerateDeployKeyModal"; function GitModals() { return ( @@ -19,6 +20,7 @@ function GitModals() { + ); } diff --git a/app/client/src/git/components/ConnectModal/ConnectInitialize/ChooseGitProvider.tsx b/app/client/src/git/components/ConnectModal/ConnectInitialize/ChooseGitProvider.tsx index d7d49c7ab3c..9e5dc1ff936 100644 --- a/app/client/src/git/components/ConnectModal/ConnectInitialize/ChooseGitProvider.tsx +++ b/app/client/src/git/components/ConnectModal/ConnectInitialize/ChooseGitProvider.tsx @@ -7,7 +7,7 @@ import { WellContainer, WellTitle, WellTitleContainer, -} from "./common"; +} from "../../common/GitUIComponents"; import { Callout, Checkbox, @@ -20,7 +20,7 @@ import { Text, } from "@appsmith/ads"; import styled from "styled-components"; -import { GIT_DEMO_GIF, GIT_PROVIDERS } from "./constants"; +import { GIT_DEMO_GIF, GIT_PROVIDERS } from "../../common/constants"; import noop from "lodash/noop"; import { CHOOSE_A_GIT_PROVIDER_STEP, @@ -33,7 +33,7 @@ import { createMessage, } from "ee/constants/messages"; import log from "loglevel"; -import type { ConnectFormDataState, GitProvider } from "./types"; +import type { ConnectFormDataState, GitProvider } from "../../common/types"; import { useIsMobileDevice } from "utils/hooks/useDeviceDetect"; const WellInnerContainer = styled.div` diff --git a/app/client/src/git/components/ConnectModal/ConnectInitialize/GenerateSSH.test.tsx b/app/client/src/git/components/ConnectModal/ConnectInitialize/GenerateSSH.test.tsx index 8e1cb7057eb..7363a689044 100644 --- a/app/client/src/git/components/ConnectModal/ConnectInitialize/GenerateSSH.test.tsx +++ b/app/client/src/git/components/ConnectModal/ConnectInitialize/GenerateSSH.test.tsx @@ -4,7 +4,7 @@ import { render, screen, fireEvent, waitFor } from "@testing-library/react"; import { isValidGitRemoteUrl } from "../../utils"; import GenerateSSH from "./GenerateSSH"; import "@testing-library/jest-dom"; -import type { GitProvider } from "./types"; +import type { GitProvider } from "../../common/types"; jest.mock("../../utils", () => ({ isValidGitRemoteUrl: jest.fn(), diff --git a/app/client/src/git/components/ConnectModal/ConnectInitialize/GenerateSSH.tsx b/app/client/src/git/components/ConnectModal/ConnectInitialize/GenerateSSH.tsx index 194dad595e7..fb8bfc26c0e 100644 --- a/app/client/src/git/components/ConnectModal/ConnectInitialize/GenerateSSH.tsx +++ b/app/client/src/git/components/ConnectModal/ConnectInitialize/GenerateSSH.tsx @@ -8,7 +8,7 @@ import { WellText, WellTitle, WellTitleContainer, -} from "./common"; +} from "../../common/GitUIComponents"; import { Button, Collapsible, @@ -29,10 +29,10 @@ import { REMOTE_URL_INPUT_LABEL, createMessage, } from "ee/constants/messages"; -import { GIT_DEMO_GIF } from "./constants"; +import { GIT_DEMO_GIF } from "../../common/constants"; import { isValidGitRemoteUrl } from "../../utils"; import type { GitApiError } from "git/store/types"; -import type { GitProvider } from "./types"; +import type { GitProvider } from "../../common/types"; interface GenerateSSHState { gitProvider?: GitProvider; diff --git a/app/client/src/git/components/ConnectModal/ConnectInitialize/index.tsx b/app/client/src/git/components/ConnectModal/ConnectInitialize/index.tsx index 23d6873e0f2..8e3d99c2aa8 100644 --- a/app/client/src/git/components/ConnectModal/ConnectInitialize/index.tsx +++ b/app/client/src/git/components/ConnectModal/ConnectInitialize/index.tsx @@ -1,19 +1,19 @@ import React, { useCallback, useEffect, useState } from "react"; import styled from "styled-components"; -import AddDeployKey from "./AddDeployKey"; +import AddDeployKey from "git/components/common/AddDeployKey"; import AnalyticsUtil from "ee/utils/AnalyticsUtil"; import ChooseGitProvider from "./ChooseGitProvider"; import GenerateSSH from "./GenerateSSH"; import Steps from "./Steps"; import Statusbar from "../../Statusbar"; import { Button, ModalBody, ModalFooter, ModalHeader } from "@appsmith/ads"; -import { GIT_CONNECT_STEPS } from "./constants"; +import { GIT_CONNECT_STEPS } from "../../common/constants"; import { isValidGitRemoteUrl } from "../../utils"; import type { ConnectRequestParams } from "git/requests/connectRequest.types"; import noop from "lodash/noop"; import type { GitApiError } from "git/store/types"; -import type { ConnectFormDataState } from "./types"; +import type { ConnectFormDataState } from "../../common/types"; import type { GitImportRequestParams } from "git/requests/gitImportRequest.types"; import { GitErrorCodes } from "git/constants/enums"; import { CONNECT_GIT, IMPORT_GIT } from "git/ee/constants/messages"; diff --git a/app/client/src/git/components/DangerZone/DangerZoneView.tsx b/app/client/src/git/components/DangerZone/DangerZoneView.tsx index 038afe54c10..dfd80dd2049 100644 --- a/app/client/src/git/components/DangerZone/DangerZoneView.tsx +++ b/app/client/src/git/components/DangerZone/DangerZoneView.tsx @@ -6,6 +6,9 @@ import { DANGER_ZONE, DISCONNECT_GIT, DISCONNECT_GIT_MESSAGE, + GENERATE_DEPLOY_KEY_BTN, + GENERATE_DEPLOY_KEY_MESSAGE, + GENERATE_DEPLOY_KEY_TITLE, createMessage, } from "ee/constants/messages"; import { Button, Divider, Text } from "@appsmith/ads"; @@ -57,6 +60,7 @@ interface DangerZoneViewProps { isToggleAutocommitLoading: boolean; isAutocommitEnabled: boolean; isFetchMetadataLoading: boolean; + toggleGenerateSSHKeyModal: (open: boolean) => void; openDisconnectModal: () => void; toggleAutocommit: () => void; toggleDisableAutocommitModal: (open: boolean) => void; @@ -75,6 +79,7 @@ function DangerZoneView({ openDisconnectModal = noop, toggleAutocommit = noop, toggleDisableAutocommitModal = noop, + toggleGenerateSSHKeyModal = noop, toggleSettingsModal = noop, }: DangerZoneViewProps) { const handleDisconnect = useCallback(() => { @@ -100,9 +105,13 @@ function DangerZoneView({ toggleSettingsModal, ]); + const handleOpenGenerateDeployKeyModal = useCallback(() => { + toggleSettingsModal(false); + toggleGenerateSSHKeyModal(true); + }, [toggleGenerateSSHKeyModal, toggleSettingsModal]); + const showAutoCommit = isManageAutocommitPermitted; const showDisconnect = isConnectPermitted; - const showDivider = showAutoCommit && showDisconnect; return ( @@ -133,7 +142,26 @@ function DangerZoneView({ )} - {showDivider && } + {showAutoCommit && } + + + + {createMessage(GENERATE_DEPLOY_KEY_TITLE)} + + + {createMessage(GENERATE_DEPLOY_KEY_MESSAGE)} + + + + + {showDisconnect && } {showDisconnect && ( diff --git a/app/client/src/git/components/DangerZone/index.tsx b/app/client/src/git/components/DangerZone/index.tsx index 9f8b41b3f59..9424f347b25 100644 --- a/app/client/src/git/components/DangerZone/index.tsx +++ b/app/client/src/git/components/DangerZone/index.tsx @@ -5,6 +5,7 @@ import React, { useCallback } from "react"; import DangerZoneView from "./DangerZoneView"; import useMetadata from "git/hooks/useMetadata"; import { useGitContext } from "../GitContextProvider"; +import useGenerateDeployKey from "git/hooks/useGenerateDeployKey"; function DangerZone() { const { @@ -20,6 +21,7 @@ function DangerZone() { toggleAutocommit, toggleAutocommitDisableModal, } = useAutocommit(); + const { toggleGenerateSSHKeyModal } = useGenerateDeployKey(); const { toggleSettingsModal } = useSettings(); const { isFetchMetadataLoading } = useMetadata(); @@ -40,6 +42,7 @@ function DangerZone() { openDisconnectModal={handleOpenDisconnectModal} toggleAutocommit={toggleAutocommit} toggleDisableAutocommitModal={toggleAutocommitDisableModal} + toggleGenerateSSHKeyModal={toggleGenerateSSHKeyModal} toggleSettingsModal={toggleSettingsModal} /> ); diff --git a/app/client/src/git/components/GenerateDeployKeyModal/GenerateDeployKeyModalView.tsx b/app/client/src/git/components/GenerateDeployKeyModal/GenerateDeployKeyModalView.tsx new file mode 100644 index 00000000000..afb289bdaef --- /dev/null +++ b/app/client/src/git/components/GenerateDeployKeyModal/GenerateDeployKeyModalView.tsx @@ -0,0 +1,113 @@ +import React from "react"; +import { + Button, + Flex, + Modal, + ModalBody, + ModalFooter, + ModalHeader, +} from "@appsmith/ads"; +import { + GENERATE_DEPLOY_KEY_MODAL_TITLE, + GENERATE_DEPLOY_KEY_MODAL_WAIT_TEXT, + createMessage, +} from "ee/constants/messages"; +import { StyledModalContent } from "git/components/common/GitUIComponents"; +import styled from "styled-components"; +import AddDeployKey from "git/components/common/AddDeployKey"; +import type { ConnectFormDataState } from "git/components/common/types"; +import type { GitApiError } from "git/store/types"; +import Statusbar from "../Statusbar"; + +interface GenerateDeployKeyModalViewProps { + error: GitApiError | null; + formData: ConnectFormDataState; + isModalOpen: boolean; + isSSHKeyLoading: boolean; + onChange: (args: Partial) => void; + onFetchSSHKey: () => void; + onGenerateSSHKey: (keyType: string) => void; + onModalOpenChange: (open: boolean) => void; + sshPublicKey: string | null; + onUpdateGeneratedSSHKey: () => void; + isUpdateGeneratedSSHKeyLoading: boolean; +} + +const OFFSET = 200; +const OUTER_PADDING = 32; +const FOOTER = 56; +const HEADER = 44; + +const StyledModalBody = styled(ModalBody)` + flex: 1; + overflow-y: initial; + display: flex; + flex-direction: column; + max-height: calc( + 100vh - ${OFFSET}px - ${OUTER_PADDING}px - ${FOOTER}px - ${HEADER}px + ); +`; + +function GenerateDeployKeyModalView({ + error, + formData, + isModalOpen, + isSSHKeyLoading, + isUpdateGeneratedSSHKeyLoading, + onChange, + onFetchSSHKey, + onGenerateSSHKey, + onModalOpenChange, + onUpdateGeneratedSSHKey, + sshPublicKey, +}: GenerateDeployKeyModalViewProps) { + const isSubmitLoading = false; // This modal doesn't have submit functionality + + return ( + + + + {createMessage(GENERATE_DEPLOY_KEY_MODAL_TITLE)} + + + + + + + + {isUpdateGeneratedSSHKeyLoading && ( + + )} + + + + + ); +} + +export default GenerateDeployKeyModalView; diff --git a/app/client/src/git/components/GenerateDeployKeyModal/index.tsx b/app/client/src/git/components/GenerateDeployKeyModal/index.tsx new file mode 100644 index 00000000000..a6d75406952 --- /dev/null +++ b/app/client/src/git/components/GenerateDeployKeyModal/index.tsx @@ -0,0 +1,120 @@ +import React, { useCallback, useEffect, useState } from "react"; +import GenerateDeployKeyModalView from "./GenerateDeployKeyModalView"; +import useGenerateDeployKey from "git/hooks/useGenerateDeployKey"; +import type { + ConnectFormDataState, + GitProvider, +} from "git/components/common/types"; +import { noop } from "lodash"; +import useGlobalSSHKey from "git/hooks/useGlobalSSHKey"; +import useMetadata from "git/hooks/useMetadata"; +import type { GitMetadata } from "reducers/uiReducers/gitSyncReducer"; + +const INITIAL_FORM_DATA: ConnectFormDataState = { + isAddedDeployKey: false, +}; + +const getGitProviderFromRemoteUrl = ( + remoteUrl: string | undefined, +): GitProvider | undefined => { + if (!remoteUrl) { + return undefined; + } + + if (remoteUrl.includes("github.com")) { + return "github"; + } + + if (remoteUrl.includes("gitlab.com")) { + return "gitlab"; + } + + if (remoteUrl.includes("bitbucket.org")) { + return "bitbucket"; + } + + return undefined; +}; + +const getInitialFormData = (metadata: GitMetadata): ConnectFormDataState => { + const remoteUrl = metadata?.remoteUrl; + + return { + ...INITIAL_FORM_DATA, + remoteUrl, + gitProvider: getGitProviderFromRemoteUrl(remoteUrl), + }; +}; + +function GenerateDeployKeyModal() { + const { + isGenerateSSHKeyModalOpen, + isUpdateGeneratedSSHKeyLoading, + resetUpdateGeneratedSSHKey, + toggleGenerateSSHKeyModal, + updateGeneratedSSHKey, + updateGeneratedSSHKeyError, + } = useGenerateDeployKey(); + + const { + fetchGlobalSSHKey, + globalSSHKey, + isFetchGlobalSSHKeyLoading, + resetGlobalSSHKey, + } = useGlobalSSHKey(); + + const { metadata } = useMetadata(); + + const [formData, setFormData] = useState(() => + getInitialFormData(metadata), + ); + + const sshPublicKey = globalSSHKey?.publicKey ?? null; + + useEffect( + function resetFormDataWhenModalClosesEffect() { + if (!isGenerateSSHKeyModalOpen) { + setFormData(getInitialFormData(metadata)); + } + }, + [isGenerateSSHKeyModalOpen, metadata], + ); + + const handleChange = useCallback( + (partialFormData: Partial) => { + setFormData((prev) => ({ ...prev, ...partialFormData })); + }, + [setFormData], + ); + + const handleModalOpenChange = useCallback( + (open: boolean) => { + if (!open) { + resetGlobalSSHKey(); + resetUpdateGeneratedSSHKey(); + toggleGenerateSSHKeyModal(false); + } else { + toggleGenerateSSHKeyModal(true); + } + }, + [resetGlobalSSHKey, resetUpdateGeneratedSSHKey, toggleGenerateSSHKeyModal], + ); + + return ( + + ); +} + +export default GenerateDeployKeyModal; diff --git a/app/client/src/git/components/SettingsModal/TabGeneral/InvalidKeyWarning.tsx b/app/client/src/git/components/SettingsModal/TabGeneral/InvalidKeyWarning.tsx new file mode 100644 index 00000000000..f4875541114 --- /dev/null +++ b/app/client/src/git/components/SettingsModal/TabGeneral/InvalidKeyWarning.tsx @@ -0,0 +1,43 @@ +import React, { useCallback } from "react"; +import useStatus from "git/hooks/useStatus"; +import { + createMessage, + GENERATE_DEPLOY_KEY_BTN, + INVALID_DEPLOY_KEY_WARNING, +} from "ee/constants/messages"; +import { Callout } from "@appsmith/ads"; +import useGenerateDeployKey from "git/hooks/useGenerateDeployKey"; +import useSettings from "git/hooks/useSettings"; +import { GitErrorCodes } from "git/constants/enums"; + +function InvalidKeyWarning() { + const { fetchStatusError } = useStatus(); + const { toggleGenerateSSHKeyModal } = useGenerateDeployKey(); + const { toggleSettingsModal } = useSettings(); + + const handleOpenGenerateDeployKeyModal = useCallback(() => { + toggleSettingsModal(false); + toggleGenerateSSHKeyModal(true); + }, [toggleGenerateSSHKeyModal, toggleSettingsModal]); + + if (fetchStatusError?.code !== GitErrorCodes.INVALID_DEPLOY_KEY) { + return null; + } + + return ( + + {createMessage(INVALID_DEPLOY_KEY_WARNING)} + + ); +} + +export default InvalidKeyWarning; diff --git a/app/client/src/git/components/SettingsModal/TabGeneral/index.tsx b/app/client/src/git/components/SettingsModal/TabGeneral/index.tsx index ba95a3d4ec0..0dba0d49aca 100644 --- a/app/client/src/git/components/SettingsModal/TabGeneral/index.tsx +++ b/app/client/src/git/components/SettingsModal/TabGeneral/index.tsx @@ -2,6 +2,7 @@ import React from "react"; import LocalProfile from "../../LocalProfile"; import DangerZone from "../../DangerZone"; import styled from "styled-components"; +import InvalidKeyWarning from "./InvalidKeyWarning"; const Container = styled.div` overflow: auto; @@ -21,6 +22,7 @@ function TabGeneral({ return ( + {showDangerZone && } diff --git a/app/client/src/git/components/ConnectModal/ConnectInitialize/AddDeployKey.tsx b/app/client/src/git/components/common/AddDeployKey.tsx similarity index 99% rename from app/client/src/git/components/ConnectModal/ConnectInitialize/AddDeployKey.tsx rename to app/client/src/git/components/common/AddDeployKey.tsx index 549534c3322..a3b32af630c 100644 --- a/app/client/src/git/components/ConnectModal/ConnectInitialize/AddDeployKey.tsx +++ b/app/client/src/git/components/common/AddDeployKey.tsx @@ -7,7 +7,7 @@ import { WellText, WellTitle, WellTitleContainer, -} from "./common"; +} from "./GitUIComponents"; import { Button, Checkbox, diff --git a/app/client/src/git/components/ConnectModal/ConnectInitialize/CopyButton.tsx b/app/client/src/git/components/common/CopyButton.tsx similarity index 100% rename from app/client/src/git/components/ConnectModal/ConnectInitialize/CopyButton.tsx rename to app/client/src/git/components/common/CopyButton.tsx diff --git a/app/client/src/git/components/ConnectModal/ConnectInitialize/common.tsx b/app/client/src/git/components/common/GitUIComponents.tsx similarity index 80% rename from app/client/src/git/components/ConnectModal/ConnectInitialize/common.tsx rename to app/client/src/git/components/common/GitUIComponents.tsx index cd69abf40ea..af9ae803c92 100644 --- a/app/client/src/git/components/ConnectModal/ConnectInitialize/common.tsx +++ b/app/client/src/git/components/common/GitUIComponents.tsx @@ -1,4 +1,4 @@ -import { Callout, Text } from "@appsmith/ads"; +import { Callout, ModalContent, Text } from "@appsmith/ads"; import styled from "styled-components"; export const WellContainer = styled.div` @@ -50,3 +50,13 @@ export const DemoImage = styled.img` export const ErrorCallout = styled(Callout)` margin-bottom: 16px; `; + +export const StyledModalContent = styled(ModalContent)` + &&& { + width: 640px; + transform: none !important; + top: 100px; + left: calc(50% - 320px); + max-height: calc(100vh - 200px); + } +`; diff --git a/app/client/src/git/components/ConnectModal/ConnectInitialize/constants.ts b/app/client/src/git/components/common/constants.ts similarity index 100% rename from app/client/src/git/components/ConnectModal/ConnectInitialize/constants.ts rename to app/client/src/git/components/common/constants.ts diff --git a/app/client/src/git/components/ConnectModal/ConnectInitialize/types.ts b/app/client/src/git/components/common/types.ts similarity index 100% rename from app/client/src/git/components/ConnectModal/ConnectInitialize/types.ts rename to app/client/src/git/components/common/types.ts diff --git a/app/client/src/git/constants/enums.ts b/app/client/src/git/constants/enums.ts index 1b2fe9084e8..a1662cb81ea 100644 --- a/app/client/src/git/constants/enums.ts +++ b/app/client/src/git/constants/enums.ts @@ -38,4 +38,5 @@ export enum GitErrorCodes { REPO_LIMIT_REACHED = "AE-GIT-4043", PUSH_FAILED_REMOTE_COUNTERPART_IS_AHEAD = "AE-GIT-4048", DUPLICATE_ARTIFACT_OVERRIDE = "AE-GIT-5004", + INVALID_DEPLOY_KEY = "AE-GIT-4032", } diff --git a/app/client/src/git/hooks/useGenerateDeployKey.ts b/app/client/src/git/hooks/useGenerateDeployKey.ts new file mode 100644 index 00000000000..56ae08f1aeb --- /dev/null +++ b/app/client/src/git/hooks/useGenerateDeployKey.ts @@ -0,0 +1,51 @@ +import { useGitContext } from "git/components/GitContextProvider"; +import { selectGenerateSSHKeyModalOpen } from "git/store/selectors/gitGlobalSelectors"; +import { useCallback } from "react"; +import { useDispatch, useSelector } from "react-redux"; +import useArtifactSelector from "./useArtifactSelector"; +import { gitGlobalActions } from "git/store/gitGlobalSlice"; +import { selectUpdateGeneratedSSHKeyState } from "git/store/selectors/gitGlobalSelectors"; + +function useGenerateDeployKey() { + const { artifactDef } = useGitContext(); + + const dispatch = useDispatch(); + + const isGenerateSSHKeyModalOpen = useArtifactSelector( + selectGenerateSSHKeyModalOpen, + ); + const updateGeneratedSSHKeyState = useSelector( + selectUpdateGeneratedSSHKeyState, + ); + + const toggleGenerateSSHKeyModal = useCallback( + (open: boolean) => { + if (artifactDef) { + dispatch(gitGlobalActions.toggleGenerateSSHKeyModal({ open })); + } + }, + [artifactDef, dispatch], + ); + + const updateGeneratedSSHKey = useCallback(() => { + if (artifactDef) { + dispatch(gitGlobalActions.updateGeneratedSSHKeyInit({ artifactDef })); + } + }, [dispatch, artifactDef]); + + const resetUpdateGeneratedSSHKey = useCallback(() => { + dispatch(gitGlobalActions.resetUpdateGeneratedSSHKey()); + }, [dispatch]); + + return { + isGenerateSSHKeyModalOpen: isGenerateSSHKeyModalOpen ?? false, + toggleGenerateSSHKeyModal, + updateGeneratedSSHKey, + isUpdateGeneratedSSHKeyLoading: + updateGeneratedSSHKeyState?.loading ?? false, + updateGeneratedSSHKeyError: updateGeneratedSSHKeyState?.error ?? null, + resetUpdateGeneratedSSHKey, + }; +} + +export default useGenerateDeployKey; diff --git a/app/client/src/git/requests/updateGeneratedSSHKeyRequest.ts b/app/client/src/git/requests/updateGeneratedSSHKeyRequest.ts new file mode 100644 index 00000000000..897e6a68e3f --- /dev/null +++ b/app/client/src/git/requests/updateGeneratedSSHKeyRequest.ts @@ -0,0 +1,20 @@ +import type { AxiosPromise } from "axios"; +import Api from "api/Api"; +import { GIT_BASE_URL } from "./constants"; +import { GitArtifactType } from "git/constants/enums"; +import type { UpdateGeneratedSSHKeyResponse } from "./updateGeneratedSSHKeyRequest.types"; + +const TYPE_MAPPING: Record = { + [GitArtifactType.Application]: "APPLICATION", + [GitArtifactType.Package]: "PACKAGE", + [GitArtifactType.Workflow]: "WORKFLOW", +}; + +export default async function updateGeneratedSSHKeyRequest( + artifactType: GitArtifactType, + baseArtifactId: string, +): AxiosPromise { + return Api.post( + `${GIT_BASE_URL}/artifacts/${baseArtifactId}/ssh-keypair?artifactType=${TYPE_MAPPING[artifactType]}`, + ); +} diff --git a/app/client/src/git/requests/updateGeneratedSSHKeyRequest.types.ts b/app/client/src/git/requests/updateGeneratedSSHKeyRequest.types.ts new file mode 100644 index 00000000000..a16ab378dab --- /dev/null +++ b/app/client/src/git/requests/updateGeneratedSSHKeyRequest.types.ts @@ -0,0 +1,7 @@ +import type { ApiResponse } from "api/types"; +import type { GitArtifact } from "git/types"; + +export type UpdateGeneratedSSHKeyResponseData = GitArtifact; + +export type UpdateGeneratedSSHKeyResponse = + ApiResponse; diff --git a/app/client/src/git/sagas/fetchStatusSaga.ts b/app/client/src/git/sagas/fetchStatusSaga.ts index 90160125fbc..e1ab7120756 100644 --- a/app/client/src/git/sagas/fetchStatusSaga.ts +++ b/app/client/src/git/sagas/fetchStatusSaga.ts @@ -7,6 +7,10 @@ import type { GitArtifactPayloadAction } from "git/store/types"; import { call, put, select } from "redux-saga/effects"; import { validateResponse } from "sagas/ErrorSagas"; import handleApiErrors from "./helpers/handleApiErrors"; +import LOG_TYPE from "entities/AppsmithConsole/logtype"; +import AppsmithConsole from "utils/AppsmithConsole"; +import { ENTITY_TYPE } from "ee/entities/AppsmithConsole/utils"; +import { GitErrorCodes } from "git/constants/enums"; export default function* fetchStatusSaga( action: GitArtifactPayloadAction, @@ -40,6 +44,20 @@ export default function* fetchStatusSaga( } catch (e) { const error = handleApiErrors(e as Error, response); + if (error?.code === GitErrorCodes.INVALID_DEPLOY_KEY) { + AppsmithConsole.error({ + id: `invalid-deploy-key-${artifactDef.baseArtifactId}`, + text: "Git: Invalid Deploy Key", + messages: [{ message: new Error(error?.message || "") }], + logType: LOG_TYPE.INVALID_GIT_DEPLOY_KEY, + source: { + type: ENTITY_TYPE.GIT, + name: "Global SSH Key", + id: artifactDef.baseArtifactId, + }, + }); + } + if (error) { yield put(gitArtifactActions.fetchStatusError({ artifactDef, error })); } diff --git a/app/client/src/git/sagas/index.ts b/app/client/src/git/sagas/index.ts index bbace975683..03037fb03b3 100644 --- a/app/client/src/git/sagas/index.ts +++ b/app/client/src/git/sagas/index.ts @@ -42,6 +42,7 @@ import gitImportSaga from "./gitImportSaga"; import mergeSaga from "./mergeSaga"; import discardSaga from "./discardSaga"; import pretagSaga from "./pretagSaga"; +import { updateGeneratedSSHKeySaga } from "./updateGeneratedSSHKeySaga"; const blockingActionSagas: Record< string, @@ -107,6 +108,7 @@ const nonBlockingActionSagas: Record< [gitArtifactActions.fetchSSHKeyInit.type]: fetchSSHKeySaga, [gitArtifactActions.generateSSHKeyInit.type]: generateSSHKeySaga, [gitGlobalActions.fetchGlobalSSHKeyInit.type]: fetchGlobalSSHKeySaga, + [gitGlobalActions.updateGeneratedSSHKeyInit.type]: updateGeneratedSSHKeySaga, // EE ...nonBlockingActionSagasExtended, diff --git a/app/client/src/git/sagas/initGitSaga.ts b/app/client/src/git/sagas/initGitSaga.ts index bbba466f918..2e5f8620a54 100644 --- a/app/client/src/git/sagas/initGitSaga.ts +++ b/app/client/src/git/sagas/initGitSaga.ts @@ -11,7 +11,11 @@ import { put, take } from "redux-saga/effects"; export default function* initGitForEditorSaga( action: GitArtifactPayloadAction, ) { - const { artifact, artifactDef } = action.payload; + const { + artifact, + artifactDef, + skipCurrentBranchUpdate = false, + } = action.payload; const artifactId = artifact?.id; yield put(gitArtifactActions.mount({ artifactDef })); @@ -28,10 +32,13 @@ export default function* initGitForEditorSaga( } if (!!branchName) { - updateBranchParam(branchName); - yield put( - gitArtifactActions.updateCurrentBranch({ artifactDef, branchName }), - ); + if (!skipCurrentBranchUpdate) { + updateBranchParam(branchName); + + yield put( + gitArtifactActions.updateCurrentBranch({ artifactDef, branchName }), + ); + } yield put(gitArtifactActions.fetchMetadataInit({ artifactDef })); yield take(gitArtifactActions.fetchMetadataSuccess.type); diff --git a/app/client/src/git/sagas/updateGeneratedSSHKeySaga.ts b/app/client/src/git/sagas/updateGeneratedSSHKeySaga.ts new file mode 100644 index 00000000000..fe3843e72af --- /dev/null +++ b/app/client/src/git/sagas/updateGeneratedSSHKeySaga.ts @@ -0,0 +1,47 @@ +import type { PayloadAction } from "@reduxjs/toolkit"; +import type { UpdateGeneratedSSHKeyResponse } from "git/requests/updateGeneratedSSHKeyRequest.types"; +import type { GitArtifactBasePayload } from "git/store/types"; +import { gitGlobalActions } from "git/store/gitGlobalSlice"; +import { call, put } from "redux-saga/effects"; +import handleApiErrors from "./helpers/handleApiErrors"; +import updateGeneratedSSHKeyRequest from "git/requests/updateGeneratedSSHKeyRequest"; +import { validateResponse } from "sagas/ErrorSagas"; +import { gitArtifactActions } from "git/store/gitArtifactSlice"; +import AppsmithConsole from "utils/AppsmithConsole"; + +export function* updateGeneratedSSHKeySaga( + action: PayloadAction, +) { + const { artifactDef } = action.payload; + let response: UpdateGeneratedSSHKeyResponse | undefined; + + try { + response = yield call( + updateGeneratedSSHKeyRequest, + artifactDef.artifactType, + artifactDef.baseArtifactId, + ); + const isValidResponse: boolean = yield validateResponse(response, false); + + if (response && isValidResponse) { + yield put( + gitArtifactActions.initGitForEditor({ + artifactDef, + artifact: response.data, + skipCurrentBranchUpdate: true, + }), + ); + yield put(gitGlobalActions.updateGeneratedSSHKeySuccess()); + AppsmithConsole.deleteErrors([ + { id: `invalid-deploy-key-${artifactDef.baseArtifactId}` }, + ]); + yield put(gitGlobalActions.toggleGenerateSSHKeyModal({ open: false })); + } + } catch (e) { + const error = handleApiErrors(e as Error, response); + + if (error) { + yield put(gitGlobalActions.updateGeneratedSSHKeyError({ error })); + } + } +} diff --git a/app/client/src/git/store/actions/initGitActions.ts b/app/client/src/git/store/actions/initGitActions.ts index afe34136990..9a5e3735f06 100644 --- a/app/client/src/git/store/actions/initGitActions.ts +++ b/app/client/src/git/store/actions/initGitActions.ts @@ -3,6 +3,7 @@ import { createArtifactAction } from "../helpers/createArtifactAction"; export interface InitGitForEditorPayload { artifact: GitArtifact | null; + skipCurrentBranchUpdate?: boolean; } export const initGitForEditorAction = diff --git a/app/client/src/git/store/actions/uiActions.ts b/app/client/src/git/store/actions/uiActions.ts index ebf205366e9..1627a35faad 100644 --- a/app/client/src/git/store/actions/uiActions.ts +++ b/app/client/src/git/store/actions/uiActions.ts @@ -32,6 +32,22 @@ export const toggleConnectSuccessModalAction = return state; }); +// generate SSH key modal +export interface ToggleGenerateSSHKeyModalPayload { + open: boolean; +} + +export const toggleGenerateSSHKeyModalAction = ( + state: GitGlobalReduxState, + action: PayloadAction, +) => { + const { open } = action.payload; + + state.isGenerateSSHKeyModalOpen = open; + + return state; +}; + // import export interface ToggleImportModalPayload { open: boolean; diff --git a/app/client/src/git/store/actions/updateGeneratedSSHKeyActions.ts b/app/client/src/git/store/actions/updateGeneratedSSHKeyActions.ts new file mode 100644 index 00000000000..05c4216b2d4 --- /dev/null +++ b/app/client/src/git/store/actions/updateGeneratedSSHKeyActions.ts @@ -0,0 +1,47 @@ +import type { + GitArtifactBasePayload, + GitAsyncErrorPayload, + GitGlobalReduxState, +} from "../types"; +import type { PayloadAction } from "@reduxjs/toolkit"; + +export const updateGeneratedSSHKeyInitAction = ( + state: GitGlobalReduxState, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + action: PayloadAction, +) => { + state.updateGeneratedSSHKey.loading = true; + state.updateGeneratedSSHKey.error = null; + + return state; +}; + +export const updateGeneratedSSHKeySuccessAction = ( + state: GitGlobalReduxState, +) => { + state.updateGeneratedSSHKey.loading = false; + state.updateGeneratedSSHKey.error = null; + + return state; +}; + +export const updateGeneratedSSHKeyErrorAction = ( + state: GitGlobalReduxState, + action: PayloadAction, +) => { + const { error } = action.payload; + + state.updateGeneratedSSHKey.loading = false; + state.updateGeneratedSSHKey.error = error; + + return state; +}; + +export const resetUpdateGeneratedSSHKeyAction = ( + state: GitGlobalReduxState, +) => { + state.updateGeneratedSSHKey.loading = false; + state.updateGeneratedSSHKey.error = null; + + return state; +}; diff --git a/app/client/src/git/store/gitGlobalSlice.ts b/app/client/src/git/store/gitGlobalSlice.ts index 2dc8d002839..613b5bd7d70 100644 --- a/app/client/src/git/store/gitGlobalSlice.ts +++ b/app/client/src/git/store/gitGlobalSlice.ts @@ -14,6 +14,7 @@ import { resetImportOverrideDetailsAction, setImportOverrideDetailsAction, toggleImportModalAction, + toggleGenerateSSHKeyModalAction, } from "./actions/uiActions"; import { gitImportErrorAction, @@ -28,6 +29,12 @@ import { resetGlobalSSHKeyAction, } from "./actions/fetchGlobalSSHKeyActions"; import { toggleRepoLimitErrorModalAction } from "./actions/repoLimitErrorModalActions"; +import { + updateGeneratedSSHKeyInitAction, + updateGeneratedSSHKeySuccessAction, + updateGeneratedSSHKeyErrorAction, + resetUpdateGeneratedSSHKeyAction, +} from "./actions/updateGeneratedSSHKeyActions"; export const gitGlobalSlice = createSlice({ name: "git/config", @@ -42,6 +49,10 @@ export const gitGlobalSlice = createSlice({ fetchGlobalSSHKeyInit: fetchGlobalSSHKeyInitAction, fetchGlobalSSHKeySuccess: fetchGlobalSSHKeySuccessAction, fetchGlobalSSHKeyError: fetchGlobalSSHKeyErrorAction, + updateGeneratedSSHKeyInit: updateGeneratedSSHKeyInitAction, + updateGeneratedSSHKeySuccess: updateGeneratedSSHKeySuccessAction, + updateGeneratedSSHKeyError: updateGeneratedSSHKeyErrorAction, + resetUpdateGeneratedSSHKey: resetUpdateGeneratedSSHKeyAction, resetGlobalSSHKey: resetGlobalSSHKeyAction, gitImportInit: gitImportInitAction, gitImportSuccess: gitImportSuccessAction, @@ -51,6 +62,7 @@ export const gitGlobalSlice = createSlice({ resetImportOverrideDetails: resetImportOverrideDetailsAction, setImportOverrideDetails: setImportOverrideDetailsAction, toggleRepoLimitErrorModal: toggleRepoLimitErrorModalAction, + toggleGenerateSSHKeyModal: toggleGenerateSSHKeyModalAction, }, }); diff --git a/app/client/src/git/store/helpers/initialState.ts b/app/client/src/git/store/helpers/initialState.ts index 324cace8d9b..16c7fe22fd4 100644 --- a/app/client/src/git/store/helpers/initialState.ts +++ b/app/client/src/git/store/helpers/initialState.ts @@ -163,11 +163,16 @@ export const gitGlobalInitialState: GitGlobalReduxState = { loading: false, error: null, }, + updateGeneratedSSHKey: { + loading: false, + error: null, + }, gitImport: { loading: false, error: null, }, isImportModalOpen: false, + isGenerateSSHKeyModalOpen: false, importOverrideDetails: null, repoLimitErrorModalOpen: false, }; diff --git a/app/client/src/git/store/selectors/gitGlobalSelectors.ts b/app/client/src/git/store/selectors/gitGlobalSelectors.ts index 84d320cd4c2..263c0c11983 100644 --- a/app/client/src/git/store/selectors/gitGlobalSelectors.ts +++ b/app/client/src/git/store/selectors/gitGlobalSelectors.ts @@ -28,3 +28,9 @@ export const selectFetchGlobalSSHKeyState = (state: GitRootState) => export const selectRepoLimitErrorModalOpen = (state: GitRootState) => selectGitGlobal(state).repoLimitErrorModalOpen; + +export const selectUpdateGeneratedSSHKeyState = (state: GitRootState) => + selectGitGlobal(state).updateGeneratedSSHKey; + +export const selectGenerateSSHKeyModalOpen = (state: GitRootState) => + selectGitGlobal(state).isGenerateSSHKeyModalOpen; diff --git a/app/client/src/git/store/types.ts b/app/client/src/git/store/types.ts index 93d26e5b440..554ad53f7d5 100644 --- a/app/client/src/git/store/types.ts +++ b/app/client/src/git/store/types.ts @@ -99,6 +99,8 @@ export interface GitGlobalReduxState { globalSSHKey: GitAsyncState; // ui isImportModalOpen: boolean; + isGenerateSSHKeyModalOpen: boolean; + updateGeneratedSSHKey: GitAsyncStateWithoutValue; importOverrideDetails: { params: GitImportRequestParams; oldArtifactName: string; diff --git a/app/client/src/sagas/DebuggerSagas.ts b/app/client/src/sagas/DebuggerSagas.ts index 41441ba5895..acf74b66a82 100644 --- a/app/client/src/sagas/DebuggerSagas.ts +++ b/app/client/src/sagas/DebuggerSagas.ts @@ -329,6 +329,10 @@ function* debuggerLogSaga(action: ReduxAction) { yield put(addErrorLogs(payload)); yield put(debuggerLog(payload)); break; + case LOG_TYPE.INVALID_GIT_DEPLOY_KEY: + yield put(addErrorLogs(payload)); + yield put(debuggerLog(payload)); + break; default: otherLogs = otherLogs.concat(payload); }