diff --git a/app/client/cypress/e2e/Regression/ClientSide/BugTests/Bug24486_Spec.ts b/app/client/cypress/e2e/Regression/ClientSide/BugTests/Bug24486_Spec.ts index 5c244fb596..e9addda8c9 100644 --- a/app/client/cypress/e2e/Regression/ClientSide/BugTests/Bug24486_Spec.ts +++ b/app/client/cypress/e2e/Regression/ClientSide/BugTests/Bug24486_Spec.ts @@ -1,10 +1,10 @@ import * as _ from "../../../../support/Objects/ObjectsCore"; -let guid; +let guid: any; let ws1Name; let ws2Name; -let app1Name; -let repoName; +let app1Name: any; +let repoName: any; let branchName; describe("Issue 24486 - Issue with Export Application", () => { @@ -73,7 +73,7 @@ describe("Issue 24486 - Issue with Export Application", () => { _.propPane.ValidatePropertyFieldValue("Label", "Submit"); }); - // after(() => { - // _.gitSync.DeleteTestGithubRepo(repoName); - // }); + after(() => { + _.gitSync.DeleteTestGithubRepo(repoName); + }); }); diff --git a/app/client/cypress/e2e/Regression/ClientSide/Git/GitSync/GitConnectV2_spec.ts b/app/client/cypress/e2e/Regression/ClientSide/Git/GitSync/GitConnectV2_spec.ts new file mode 100644 index 0000000000..40d57e5d20 --- /dev/null +++ b/app/client/cypress/e2e/Regression/ClientSide/Git/GitSync/GitConnectV2_spec.ts @@ -0,0 +1,30 @@ +import { featureFlagIntercept } from "../../../../../support/Objects/FeatureFlags"; +import * as _ from "../../../../../support/Objects/ObjectsCore"; + +let repoName: any; + +describe("Git Connect V2", function () { + before(() => { + _.agHelper.GenerateUUID(); + cy.get("@guid").then((uid) => { + _.homePage.CreateNewWorkspace("GitConnectV2" + uid, true); + _.homePage.CreateAppInWorkspace("GitConnectV2" + uid); + }); + }); + + it("Test Git Connect V2", function () { + featureFlagIntercept({ + release_git_connect_v2_enabled: true, + }); + + _.gitSync.CreateNConnectToGitV2(); + + cy.get("@gitRepoName").then((repName) => { + repoName = repName; + }); + }); + + after(() => { + _.gitSync.DeleteTestGithubRepo(repoName); + }); +}); diff --git a/app/client/cypress/support/Pages/GitSync.ts b/app/client/cypress/support/Pages/GitSync.ts index d78b7b2247..b290e67831 100644 --- a/app/client/cypress/support/Pages/GitSync.ts +++ b/app/client/cypress/support/Pages/GitSync.ts @@ -160,6 +160,85 @@ export class GitSync { } } + private providerRadioOthers = "[data-testid='t--git-provider-radio-others']"; + private existingEmptyRepoYes = "[data-testid='t--existing-empty-repo-yes']"; + private gitConnectNextBtn = "[data-testid='t--git-connect-next-button']"; + private remoteUrlInput = "[data-testid='git-connect-remote-url-input']"; + private addedDeployKeyCheckbox = + "[data-testid='t--added-deploy-key-checkbox']"; + private startUsingGitButton = "[data-testid='t--start-using-git-button']"; + + CreateNConnectToGitV2( + repoName = "Repo", + assertConnect = true, + privateFlag = false, + ) { + this.agHelper.GenerateUUID(); + cy.get("@guid").then((uid) => { + repoName += uid; + this.CreateTestGiteaRepo(repoName, privateFlag); + this.AuthorizeKeyToGiteaV2(repoName, assertConnect); + cy.wrap(repoName).as("gitRepoName"); + }); + } + + public AuthorizeKeyToGiteaV2(repo: string, assertConnect = true) { + let generatedKey; + + cy.intercept("POST", "/api/v1/applications/ssh-keypair/*").as( + `generateKey-${repo}`, + ); + + this.OpenGitSyncModal(); + + this.agHelper.GetNClick(this.providerRadioOthers); + this.agHelper.GetNClick(this.existingEmptyRepoYes); + this.agHelper.GetNClick(this.gitConnectNextBtn); + this.agHelper.AssertAttribute( + this.remoteUrlInput, + "placeholder", + "git@example.com:user/repository.git", + ); + this.agHelper.TypeText( + this.remoteUrlInput, + `${this.dataManager.GITEA_API_URL_TED}/${repo}.git`, + ); + this.agHelper.GetNClick(this.gitConnectNextBtn); + + this.agHelper.GenerateUUID(); + cy.get("@guid").then((uid) => { + cy.wait(`@generateKey-${repo}`).then((result: any) => { + generatedKey = result.response.body.data.publicKey; + generatedKey = generatedKey.slice(0, generatedKey.length - 1); + // fetch the generated key and post to the github repo + cy.request({ + method: "POST", + url: `${this.dataManager.GITEA_API_BASE_TED}:${this.dataManager.GITEA_API_PORT_TED}/api/v1/repos/Cypress/${repo}/keys`, + headers: { + Authorization: `token ${Cypress.env("GITEA_TOKEN")}`, + }, + body: { + title: "key_" + uid, + key: generatedKey, + read_only: false, + }, + }).then((resp: any) => { + cy.log("Deploy Key Id ", resp.body.key_id); + cy.wrap(resp.body.key_id).as("deployKeyId"); + }); + }); + }); + this.agHelper.GetNClick(this.addedDeployKeyCheckbox, 0, true); + this.agHelper.GetNClick(this.gitConnectNextBtn); + + if (assertConnect) { + this.assertHelper.AssertNetworkStatus("@connectGitLocalRepo"); + this.agHelper.GetNClick(this.startUsingGitButton); + this.agHelper.AssertElementExist(this._bottomBarCommit, 0, 30000); + this.CloseGitSyncModal(); + } + } + public ImportAppFromGit( workspaceName: string, repo: string, diff --git a/app/client/src/actions/gitSyncActions.ts b/app/client/src/actions/gitSyncActions.ts index 1581cfb1ed..4cd2aae631 100644 --- a/app/client/src/actions/gitSyncActions.ts +++ b/app/client/src/actions/gitSyncActions.ts @@ -1,4 +1,7 @@ -import type { ReduxActionWithCallbacks } from "@appsmith/constants/ReduxActionConstants"; +import type { + ReduxAction, + ReduxActionWithCallbacks, +} from "@appsmith/constants/ReduxActionConstants"; import { ReduxActionErrorTypes, ReduxActionTypes, @@ -11,6 +14,7 @@ import type { GitRemoteStatusData, } from "reducers/uiReducers/gitSyncReducer"; import type { ResponseMeta } from "api/ApiResponses"; +import { noop } from "lodash"; export type GitStatusParams = { compareRemote?: boolean; @@ -64,14 +68,14 @@ export type ConnectToGitResponse = { type ConnectToGitRequestParams = { payload: ConnectToGitPayload; onSuccessCallback?: (payload: ConnectToGitResponse) => void; - onErrorCallback?: (error: string) => void; + onErrorCallback?: (error: any, response?: any) => void; }; -export type ConnectToGitReduxAction = ReduxActionWithCallbacks< - ConnectToGitPayload, - ConnectToGitResponse, - string ->; +export interface ConnectToGitReduxAction + extends ReduxAction { + onSuccessCallback?: (response: ConnectToGitResponse) => void; + onErrorCallback?: (error: Error, response?: any) => void; +} export const connectToGitInit = ({ onErrorCallback, @@ -177,8 +181,13 @@ export const fetchGitStatusSuccess = (payload: GitStatusData) => ({ payload, }); -export const fetchGitRemoteStatusInit = () => ({ +export const fetchGitRemoteStatusInit = ({ + onSuccessCallback = noop, + onErrorCallback = noop, +} = {}) => ({ type: ReduxActionTypes.FETCH_GIT_REMOTE_STATUS_INIT, + onSuccessCallback, + onErrorCallback, }); export const fetchGitRemoteStatusSuccess = (payload: GitRemoteStatusData) => ({ diff --git a/app/client/src/ce/constants/messages.ts b/app/client/src/ce/constants/messages.ts index 8a10ceeed7..408f1bdccb 100644 --- a/app/client/src/ce/constants/messages.ts +++ b/app/client/src/ce/constants/messages.ts @@ -760,6 +760,9 @@ export const GIT_DISCONNECT_POPUP_SUBTITLE = () => `Git features will no more be shown for this application`; export const GIT_DISCONNECT_POPUP_MAIN_HEADING = () => `Are you sure?`; +export const CONFIGURE_GIT = () => "Configure git"; +export const SETTINGS_GIT = () => "Settings"; + export const GIT_CONNECTION = () => "Git connection"; export const GIT_IMPORT = () => "Git import"; export const MERGE = () => "Merge"; @@ -774,11 +777,14 @@ export const IMPORT_URL_INFO = () => `Paste the remote URL here:`; export const REMOTE_URL_VIA = () => "Remote URL via"; export const USER_PROFILE_SETTINGS_TITLE = () => "User settings"; +export const GIT_USER_SETTINGS_TITLE = () => "Git author"; export const USE_DEFAULT_CONFIGURATION = () => "Use default configuration"; export const AUTHOR_NAME = () => "Author name"; export const AUTHOR_NAME_CANNOT_BE_EMPTY = () => "Author name cannot be empty"; export const AUTHOR_EMAIL = () => "Author email"; +export const AUTHOR_EMAIL_CANNOT_BE_EMPTY = () => + "Author email cannot be empty"; export const NAME_YOUR_NEW_BRANCH = () => "Name your new branch"; export const SWITCH_BRANCHES = () => "Switch branches"; @@ -829,7 +835,6 @@ export const GIT_USER_UPDATED_SUCCESSFULLY = () => export const REMOTE_URL_INPUT_PLACEHOLDER = () => "git@example.com:user/repository.git"; export const GIT_COMMIT_MESSAGE_PLACEHOLDER = () => "Your commit message here"; -export const COPIED_SSH_KEY = () => "Copied SSH key"; export const INVALID_USER_DETAILS_MSG = () => "Please enter valid user details"; export const PASTE_SSH_URL_INFO = () => "Please enter a valid SSH URL of your repository"; @@ -972,6 +977,69 @@ export const ERROR_GIT_AUTH_FAIL = () => export const ERROR_GIT_INVALID_REMOTE = () => "Either the remote repository doesn't exist or is unreachable."; // GIT ERRORS end + +// Git Connect V2 +export const CHOOSE_A_GIT_PROVIDER_STEP = () => "Choose a git provider"; +export const GENERATE_SSH_KEY_STEP = () => "Generate SSH key"; +export const ADD_DEPLOY_KEY_STEP = () => "Add deploy key"; + +export const CHOOSE_GIT_PROVIDER_QUESTION = () => + "To begin with, choose your git service provider"; +export const IS_EMPTY_REPO_QUESTION = () => + "Do you have an existing empty repository to connect to git?"; +export const HOW_TO_CREATE_EMPTY_REPO = () => "How to create a new repository?"; +export const IMPORT_APP_IF_NOT_EMPTY = () => + "If you already have an app connected to git, you can import it to the workspace."; +export const I_HAVE_EXISTING_REPO = () => + "I have an existing appsmith app connected to git"; +export const ERROR_REPO_NOT_EMPTY_TITLE = () => + "The repo you added isn't empty"; +export const ERROR_REPO_NOT_EMPTY_MESSAGE = () => + "Kindly create a new repository and provide its remote SSH URL here. We require an empty repository to continue."; +export const READ_DOCS = () => "Read Docs"; +export const COPY_SSH_URL_MESSAGE = () => + "In your repo, copy the Remote SSH URL & paste it in the input field below."; +export const REMOTE_URL_INPUT_LABEL = () => "Remote SSH URL"; +export const HOW_TO_COPY_REMOTE_URL = () => + "How to copy & paste SSH remote URL"; +export const ERROR_SSH_KEY_MISCONF_TITLE = () => "SSH key misconfiguration"; +export const ERROR_SSH_KEY_MISCONF_MESSAGE = () => + "It seems that your SSH key hasn't been added to your repository. To proceed, please revisit the steps below and configure your SSH key correctly."; +export const ADD_DEPLOY_KEY_STEP_TITLE = () => + "Add deploy key & give write access"; +export const HOW_TO_ADD_DEPLOY_KEY = () => + "How to paste SSH Key in repo and give write access?"; +export const CONSENT_ADDED_DEPLOY_KEY = () => + "I've added deploy key and gave it write access"; +export const PREVIOUS_STEP = () => "Previous step"; +export const GIT_CONNECT_SUCCESS_TITLE = () => + "Successfully connected to your git remote repository"; +export const GIT_CONNECT_SUCCESS_MESSAGE = () => + "Now you can start collaborating with your team members by committing, merging and deploying your app"; +export const START_USING_GIT = () => "Start using git"; + +export const GIT_AUTHOR = () => "Git author"; +export const DISCONNECT_GIT = () => "Disconnect git"; +export const DISCONNECT_GIT_MESSAGE = () => + "Once you delete a repository, there is no going back. Please be certain."; +export const NEED_EMPTY_REPO_MESSAGE = () => + "You need an empty repository to connect to Git on Appsmith, please create one on your Git service provider to continue."; +export const GIT_IMPORT_WAITING = () => + "Please wait while we import the app..."; +export const GIT_CONNECT_WAITING = () => + "Please wait while we connect to git..."; +export const CONNECT_GIT_TEXT = () => "Connect git"; + +export const ERROR_SSH_RECONNECT_MESSAGE = () => + "We couldn't connect to the repo due to a missing deploy key. You can fix this in two ways:"; +export const ERROR_SSH_RECONNECT_OPTION1 = () => + "Copy the SSH key below and add it to your repository."; +export const ERROR_SSH_RECONNECT_OPTION2 = () => + "If you want to connect a new repository, you can disconnect and do that instead."; +export const COPIED_SSH_KEY = () => "Copied SSH key"; +export const NO_COPIED_SSH_KEY = () => "Could not copy SSH key"; +// Git Connect V2 end + export const NAV_DESCRIPTION = () => `Navigate to any page, widget or file across this project.`; export const ACTION_OPERATION_DESCRIPTION = () => diff --git a/app/client/src/ce/entities/FeatureFlag.ts b/app/client/src/ce/entities/FeatureFlag.ts index 6e2eafc15e..6eb7debe96 100644 --- a/app/client/src/ce/entities/FeatureFlag.ts +++ b/app/client/src/ce/entities/FeatureFlag.ts @@ -22,6 +22,7 @@ export const FEATURE_FLAG = { release_git_status_lite_enabled: "release_git_status_lite_enabled", license_sso_saml_enabled: "license_sso_saml_enabled", license_sso_oidc_enabled: "license_sso_oidc_enabled", + release_git_connect_v2_enabled: "release_git_connect_v2_enabled", deprecate_custom_fusioncharts_enabled: "deprecate_custom_fusioncharts_enabled", } as const; @@ -49,6 +50,7 @@ export const DEFAULT_FEATURE_FLAG_VALUE: FeatureFlags = { release_git_status_lite_enabled: false, license_sso_saml_enabled: false, license_sso_oidc_enabled: false, + release_git_connect_v2_enabled: false, deprecate_custom_fusioncharts_enabled: false, }; diff --git a/app/client/src/ce/utils/analyticsUtilTypes.ts b/app/client/src/ce/utils/analyticsUtilTypes.ts index 7da68839c5..45173397bf 100644 --- a/app/client/src/ce/utils/analyticsUtilTypes.ts +++ b/app/client/src/ce/utils/analyticsUtilTypes.ts @@ -177,6 +177,12 @@ export type EventName = | "SIGNPOSTING_MODAL_FIRST_TIME_OPEN" | "SIGNPOSTING_STEP_COMPLETE" | "GS_BRANCH_MORE_MENU_OPEN" + | "GS_CONFIGURE_GIT" + | "GS_IMPORT_VIA_GIT_DURING_GC" + | "GS_EXISTING_EMPTY_REPO" + | "GS_GENERATE_KEY_BUTTON_CLICK" + | "GS_CONNECT_BUTTON_ON_GIT_SYNC_MODAL_CLICK" + | "GS_START_USING_GIT" | "GIT_DISCARD_WARNING" | "GIT_DISCARD_CANCEL" | "GIT_DISCARD" @@ -192,6 +198,7 @@ export type EventName = | "GS_DEPLOY_GIT_CLICK" | "GS_DEPLOY_GIT_MODAL_TRIGGERED" | "GS_MERGE_GIT_MODAL_TRIGGERED" + | "GS_SETTINGS_GIT_MODAL_TRIGGERED" | "GS_REPO_LIMIT_ERROR_MODAL_TRIGGERED" | "GS_GIT_DOCUMENTATION_LINK_CLICK" | "GS_MERGE_CHANGES_BUTTON_CLICK" diff --git a/app/client/src/entities/GitSync.ts b/app/client/src/entities/GitSync.ts index b55c7f3058..65f464028c 100644 --- a/app/client/src/entities/GitSync.ts +++ b/app/client/src/entities/GitSync.ts @@ -2,6 +2,7 @@ export enum GitSyncModalTab { GIT_CONNECTION = "GIT_CONNECTION", DEPLOY = "DEPLOY", MERGE = "MERGE", + SETTINGS = "SETTINGS", } export type GitConfig = { diff --git a/app/client/src/pages/Editor/gitSync/DisconnectGitModal.tsx b/app/client/src/pages/Editor/gitSync/DisconnectGitModal.tsx index 2182832d5d..63d0f135b5 100644 --- a/app/client/src/pages/Editor/gitSync/DisconnectGitModal.tsx +++ b/app/client/src/pages/Editor/gitSync/DisconnectGitModal.tsx @@ -33,8 +33,14 @@ import { } from "@appsmith/constants/messages"; import AnalyticsUtil from "utils/AnalyticsUtil"; import { Space } from "./components/StyledComponents"; +import { GitSyncModalTab } from "entities/GitSync"; +import { useFeatureFlag } from "utils/hooks/useFeatureFlag"; +import { FEATURE_FLAG } from "@appsmith/entities/FeatureFlag"; function DisconnectGitModal() { + const isGitConnectV2Enabled = useFeatureFlag( + FEATURE_FLAG.release_git_connect_v2_enabled, + ); const dispatch = useDispatch(); const isModalOpen = useSelector(getIsDisconnectGitModalOpen); const disconnectingApp = useSelector(getDisconnectingGitApplication); @@ -44,7 +50,14 @@ function DisconnectGitModal() { const handleClickOnBack = useCallback(() => { dispatch(setIsDisconnectGitModalOpen(false)); - dispatch(setIsGitSyncModalOpen({ isOpen: true })); + dispatch( + setIsGitSyncModalOpen({ + isOpen: true, + tab: isGitConnectV2Enabled + ? GitSyncModalTab.SETTINGS + : GitSyncModalTab.GIT_CONNECTION, + }), + ); dispatch(setDisconnectingGitApplication({ id: "", name: "" })); }, [dispatch]); diff --git a/app/client/src/pages/Editor/gitSync/GitSyncModal.tsx b/app/client/src/pages/Editor/gitSync/GitSyncModal/GitSyncModalV1.tsx similarity index 80% rename from app/client/src/pages/Editor/gitSync/GitSyncModal.tsx rename to app/client/src/pages/Editor/gitSync/GitSyncModal/GitSyncModalV1.tsx index fe9c9fe5d4..22d43f06f3 100644 --- a/app/client/src/pages/Editor/gitSync/GitSyncModal.tsx +++ b/app/client/src/pages/Editor/gitSync/GitSyncModal/GitSyncModalV1.tsx @@ -7,46 +7,67 @@ import { import { useDispatch, useSelector } from "react-redux"; import { setIsGitSyncModalOpen } from "actions/gitSyncActions"; import { setWorkspaceIdForImport } from "@appsmith/actions/applicationActions"; -import Menu from "./Menu"; -import { MENU_ITEMS_MAP } from "./constants"; -import Deploy from "./Tabs/Deploy"; -import Merge from "./Tabs/Merge"; -import GitConnection from "./Tabs/GitConnection"; +import Menu from "../Menu"; +import Deploy from "../Tabs/Deploy"; +import Merge from "../Tabs/Merge"; +import GitConnection from "../Tabs/GitConnection"; -import GitErrorPopup from "./components/GitErrorPopup"; +import GitErrorPopup from "../components/GitErrorPopup"; import styled from "styled-components"; -import { GitSyncModalTab } from "entities/GitSync"; +import AnalyticsUtil from "utils/AnalyticsUtil"; +import { Modal, ModalContent, ModalHeader } from "design-system"; +import { EnvInfoHeader } from "@appsmith/components/EnvInfoHeader"; import { createMessage, + GIT_CONNECTION, + DEPLOY, + MERGE, + CONNECT_TO_GIT, + DEPLOY_YOUR_APPLICATION, + MERGE_CHANGES, GIT_IMPORT, IMPORT_FROM_GIT_REPOSITORY, } from "@appsmith/constants/messages"; -import AnalyticsUtil from "utils/AnalyticsUtil"; -import { useGitConnect } from "./hooks"; -import { Modal, ModalContent, ModalHeader } from "design-system"; -import { EnvInfoHeader } from "@appsmith/components/EnvInfoHeader"; +import { GitSyncModalTab } from "entities/GitSync"; const ModalContentContainer = styled(ModalContent)` min-height: 650px; `; -const ComponentsByTab = { +const ComponentsByTab: { [K in GitSyncModalTab]?: any } = { [GitSyncModalTab.GIT_CONNECTION]: GitConnection, [GitSyncModalTab.DEPLOY]: Deploy, [GitSyncModalTab.MERGE]: Merge, }; +const MENU_ITEMS_MAP: { [K in GitSyncModalTab]?: any } = { + [GitSyncModalTab.GIT_CONNECTION]: { + key: GitSyncModalTab.GIT_CONNECTION, + title: createMessage(GIT_CONNECTION), + modalTitle: createMessage(CONNECT_TO_GIT), + }, + [GitSyncModalTab.DEPLOY]: { + key: GitSyncModalTab.DEPLOY, + title: createMessage(DEPLOY), + modalTitle: createMessage(DEPLOY_YOUR_APPLICATION), + }, + [GitSyncModalTab.MERGE]: { + key: GitSyncModalTab.MERGE, + title: createMessage(MERGE), + modalTitle: createMessage(MERGE_CHANGES), + }, +}; + const allMenuOptions = Object.values(MENU_ITEMS_MAP); -function GitSyncModal(props: { isImport?: boolean }) { +function GitSyncModalV1(props: { isImport?: boolean }) { const dispatch = useDispatch(); const isModalOpen = useSelector(getIsGitSyncModalOpen); const isGitConnected = useSelector(getIsGitConnected); + const activeTabKey = useSelector(getActiveGitSyncModalTab); - const { onGitConnectFailure: resetGitConnectStatus } = useGitConnect(); const handleClose = useCallback(() => { - resetGitConnectStatus(); dispatch(setIsGitSyncModalOpen({ isOpen: false })); dispatch(setWorkspaceIdForImport("")); }, [dispatch, setIsGitSyncModalOpen]); @@ -144,4 +165,4 @@ function GitSyncModal(props: { isImport?: boolean }) { ); } -export default GitSyncModal; +export default GitSyncModalV1; diff --git a/app/client/src/pages/Editor/gitSync/GitSyncModal/GitSyncModalV2.tsx b/app/client/src/pages/Editor/gitSync/GitSyncModal/GitSyncModalV2.tsx new file mode 100644 index 0000000000..5d8939a4c3 --- /dev/null +++ b/app/client/src/pages/Editor/gitSync/GitSyncModal/GitSyncModalV2.tsx @@ -0,0 +1,147 @@ +import React, { useCallback } from "react"; +import { + getActiveGitSyncModalTab, + getIsGitConnected, + getIsGitSyncModalOpen, +} from "selectors/gitSyncSelectors"; +import { useDispatch, useSelector } from "react-redux"; +import { setIsGitSyncModalOpen } from "actions/gitSyncActions"; +import { setWorkspaceIdForImport } from "@appsmith/actions/applicationActions"; +import Menu from "../Menu"; +import Deploy from "../Tabs/Deploy"; +import Merge from "../Tabs/Merge"; + +import GitErrorPopup from "../components/GitErrorPopup"; + +import { + CONFIGURE_GIT, + createMessage, + DEPLOY, + MERGE, + SETTINGS_GIT, +} from "@appsmith/constants/messages"; +import AnalyticsUtil from "utils/AnalyticsUtil"; +import { Modal, ModalContent, ModalHeader } from "design-system"; +import { EnvInfoHeader } from "@appsmith/components/EnvInfoHeader"; +import GitConnectionV2 from "../Tabs/GitConnectionV2"; +import GitSettings from "../Tabs/GitSettings"; +import { GitSyncModalTab } from "entities/GitSync"; +import ConnectionSuccess from "../Tabs/ConnectionSuccess"; +import styled from "styled-components"; +import ReconnectSSHError from "../components/ReconnectSSHError"; +import { getCurrentAppGitMetaData } from "@appsmith/selectors/applicationSelectors"; + +const StyledModalContent = styled(ModalContent)` + &&& { + width: 640px; + transform: none !important; + top: 100px; + left: calc(50% - 320px); + max-height: calc(100vh - 200px); + } +`; + +export const modalTitle: Partial<{ [K in GitSyncModalTab]: string }> = { + [GitSyncModalTab.GIT_CONNECTION]: createMessage(CONFIGURE_GIT), +}; + +const menuOptions = [ + { + key: GitSyncModalTab.DEPLOY, + title: createMessage(DEPLOY), + }, + { + key: GitSyncModalTab.MERGE, + title: createMessage(MERGE), + }, + { + key: GitSyncModalTab.SETTINGS, + title: createMessage(SETTINGS_GIT), + }, +]; + +const possibleMenuOptions = menuOptions.map((option) => option.key); + +interface GitSyncModalV2Props { + isImport?: boolean; +} + +function GitSyncModalV2({ isImport = false }: GitSyncModalV2Props) { + const gitMetadata = useSelector(getCurrentAppGitMetaData); + const isModalOpen = useSelector(getIsGitSyncModalOpen); + const isGitConnected = useSelector(getIsGitConnected); + + let activeTabKey = useSelector(getActiveGitSyncModalTab); + if (!isGitConnected && activeTabKey !== GitSyncModalTab.GIT_CONNECTION) { + activeTabKey = GitSyncModalTab.GIT_CONNECTION; + } + + const dispatch = useDispatch(); + + const setActiveTabKey = useCallback( + (tabKey: GitSyncModalTab) => { + if (tabKey === GitSyncModalTab.DEPLOY) { + AnalyticsUtil.logEvent("GS_DEPLOY_GIT_MODAL_TRIGGERED", { + source: `${activeTabKey}_TAB`, + }); + } else if (tabKey === GitSyncModalTab.MERGE) { + AnalyticsUtil.logEvent("GS_MERGE_GIT_MODAL_TRIGGERED", { + source: `${activeTabKey}_TAB`, + }); + } else if (tabKey === GitSyncModalTab.SETTINGS) { + AnalyticsUtil.logEvent("GS_SETTINGS_GIT_MODAL_TRIGGERED", { + source: `${activeTabKey}_TAB`, + }); + } + dispatch(setIsGitSyncModalOpen({ isOpen: isModalOpen, tab: tabKey })); + }, + [dispatch, setIsGitSyncModalOpen, isModalOpen], + ); + + const handleClose = useCallback(() => { + dispatch(setIsGitSyncModalOpen({ isOpen: false })); + dispatch(setWorkspaceIdForImport("")); + }, [dispatch, setIsGitSyncModalOpen]); + + return ( + <> + { + if (!open) { + handleClose(); + } + }} + open={isModalOpen} + > + + + {modalTitle[activeTabKey] || gitMetadata?.repoName} + + + {isGitConnected && } + {possibleMenuOptions.includes(activeTabKey) && ( + + setActiveTabKey(tabKey as GitSyncModalTab) + } + options={menuOptions} + /> + )} + {activeTabKey === GitSyncModalTab.GIT_CONNECTION && + (!isGitConnected ? ( + + ) : ( + + ))} + {activeTabKey === GitSyncModalTab.DEPLOY && } + {activeTabKey === GitSyncModalTab.MERGE && } + {activeTabKey === GitSyncModalTab.SETTINGS && } + + + + + ); +} + +export default GitSyncModalV2; diff --git a/app/client/src/pages/Editor/gitSync/GitSyncModal/index.tsx b/app/client/src/pages/Editor/gitSync/GitSyncModal/index.tsx new file mode 100644 index 0000000000..cdc3c72ec5 --- /dev/null +++ b/app/client/src/pages/Editor/gitSync/GitSyncModal/index.tsx @@ -0,0 +1,23 @@ +import { FEATURE_FLAG } from "@appsmith/entities/FeatureFlag"; +import React from "react"; +import { useFeatureFlag } from "utils/hooks/useFeatureFlag"; +import GitSyncModalV1 from "./GitSyncModalV1"; +import GitSyncModalV2 from "./GitSyncModalV2"; + +interface GitSyncModalProps { + isImport?: boolean; +} + +function GitSyncModal(props: GitSyncModalProps) { + const isGitConnectV2Enabled = useFeatureFlag( + FEATURE_FLAG.release_git_connect_v2_enabled, + ); + + return isGitConnectV2Enabled ? ( + + ) : ( + + ); +} + +export default GitSyncModal; diff --git a/app/client/src/pages/Editor/gitSync/QuickGitActions/index.tsx b/app/client/src/pages/Editor/gitSync/QuickGitActions/index.tsx index be4afc4477..4ea9846825 100644 --- a/app/client/src/pages/Editor/gitSync/QuickGitActions/index.tsx +++ b/app/client/src/pages/Editor/gitSync/QuickGitActions/index.tsx @@ -22,11 +22,7 @@ import { import { Colors } from "constants/Colors"; import { useDispatch, useSelector } from "react-redux"; -import { - gitPullInit, - setIsGitSyncModalOpen, - showConnectGitModal, -} from "actions/gitSyncActions"; +import { gitPullInit, setIsGitSyncModalOpen } from "actions/gitSyncActions"; import { GitSyncModalTab } from "entities/GitSync"; import { getCountOfChangesToCommit, @@ -41,6 +37,8 @@ import { inGuidedTour } from "selectors/onboardingSelectors"; import { getTypographyByKey } from "design-system-old"; import { Button, Icon, Tooltip } from "design-system"; import AnalyticsUtil from "utils/AnalyticsUtil"; +import { useFeatureFlag } from "utils/hooks/useFeatureFlag"; +import { FEATURE_FLAG } from "@appsmith/entities/FeatureFlag"; type QuickActionButtonProps = { className?: string; @@ -139,18 +137,18 @@ const getPullBtnStatus = (gitStatus: any, pullFailed: boolean) => { const getQuickActionButtons = ({ changesToCommit, commit, - connect, gitStatus, isFetchingGitStatus, merge, pull, pullDisabled, pullTooltipMessage, + settings, showPullLoadingState, }: { changesToCommit: number; commit: () => void; - connect: () => void; + settings: () => void; pull: () => void; merge: () => void; gitStatus: any; @@ -186,7 +184,7 @@ const getQuickActionButtons = ({ { className: "t--bottom-git-settings", icon: "settings-2-line", - onClick: connect, + onClick: settings, tooltipText: createMessage(GIT_SETTINGS), }, ]; @@ -253,7 +251,12 @@ function ConnectGitPlaceholder() { source: "BOTTOM_BAR_GIT_CONNECT_BUTTON", }); - dispatch(showConnectGitModal()); + dispatch( + setIsGitSyncModalOpen({ + isOpen: true, + tab: GitSyncModalTab.GIT_CONNECTION, + }), + ); }} size="sm" > @@ -284,6 +287,10 @@ export default function QuickGitActions() { const showPullLoadingState = isPullInProgress || isFetchingGitStatus; const changesToCommit = useSelector(getCountOfChangesToCommit); + const isGitConnectV2Enabled = useFeatureFlag( + FEATURE_FLAG.release_git_connect_v2_enabled, + ); + const quickActionButtons = getQuickActionButtons({ commit: () => { dispatch( @@ -296,11 +303,13 @@ export default function QuickGitActions() { source: "BOTTOM_BAR_GIT_COMMIT_BUTTON", }); }, - connect: () => { + settings: () => { dispatch( setIsGitSyncModalOpen({ isOpen: true, - tab: GitSyncModalTab.GIT_CONNECTION, + tab: isGitConnectV2Enabled + ? GitSyncModalTab.SETTINGS + : GitSyncModalTab.GIT_CONNECTION, }), ); AnalyticsUtil.logEvent("GS_SETTING_CLICK", { diff --git a/app/client/src/pages/Editor/gitSync/Tabs/ConnectionSuccess.tsx b/app/client/src/pages/Editor/gitSync/Tabs/ConnectionSuccess.tsx new file mode 100644 index 0000000000..a231f1e797 --- /dev/null +++ b/app/client/src/pages/Editor/gitSync/Tabs/ConnectionSuccess.tsx @@ -0,0 +1,75 @@ +import { setIsGitSyncModalOpen } from "actions/gitSyncActions"; +import { + GIT_CONNECT_SUCCESS_MESSAGE, + GIT_CONNECT_SUCCESS_TITLE, + START_USING_GIT, + createMessage, +} from "@appsmith/constants/messages"; +import { Button, Icon, ModalBody, ModalFooter, Text } from "design-system"; +import { GitSyncModalTab } from "entities/GitSync"; +import React from "react"; +import { useDispatch, useSelector } from "react-redux"; +import styled from "styled-components"; +import { getCurrentAppGitMetaData } from "@appsmith/selectors/applicationSelectors"; +import AnalyticsUtil from "utils/AnalyticsUtil"; + +const Container = styled.div``; + +const TitleContainer = styled.div` + display: flex; + align-items: center; + margin-bottom: 16px; +`; + +const TitleText = styled(Text)` + flex: 1; + font-weight: 600; +`; + +const StyledIcon = styled(Icon)` + margin-right: 8px; +`; + +function ConnectionSuccess() { + const gitMetadata = useSelector(getCurrentAppGitMetaData); + const dispatch = useDispatch(); + + const handleClose = () => { + dispatch( + setIsGitSyncModalOpen({ + isOpen: true, + tab: GitSyncModalTab.DEPLOY, + }), + ); + AnalyticsUtil.logEvent("GS_START_USING_GIT", { + repoUrl: gitMetadata?.remoteUrl, + }); + }; + + return ( + <> + + + + + + {createMessage(GIT_CONNECT_SUCCESS_TITLE)} + + + {createMessage(GIT_CONNECT_SUCCESS_MESSAGE)} + + + + + + + ); +} + +export default ConnectionSuccess; diff --git a/app/client/src/pages/Editor/gitSync/Tabs/Deploy.tsx b/app/client/src/pages/Editor/gitSync/Tabs/Deploy.tsx index 4985f83b24..f881d78ee5 100644 --- a/app/client/src/pages/Editor/gitSync/Tabs/Deploy.tsx +++ b/app/client/src/pages/Editor/gitSync/Tabs/Deploy.tsx @@ -285,6 +285,7 @@ function Deploy() {
{hasChangesToCommit && ( @@ -428,7 +429,7 @@ function Deploy() { )} - + {showPullButton && ( + + + + Copy below SSH key and paste it in your{" "} + {!!repositorySettingsUrl && value.gitProvider !== "others" ? ( + + repository settings. + + ) : ( + "repository settings." + )}{" "} + Now, give write access to it. + + + setSshKeyType(v)} + size="sm" + value={sshKeyType} + > + + + + {!loading ? ( + + + {sshKeyType} + {SSHKeyPair} + {!connectLoading && ( + { + AnalyticsUtil.logEvent("GS_COPY_SSH_KEY_BUTTON_CLICK"); + }} + tooltipMessage={createMessage(COPY_SSH_KEY)} + value={SSHKeyPair} + /> + )} + + ) : ( + + )} + + {value?.gitProvider !== "others" && ( + + + + {createMessage(HOW_TO_ADD_DEPLOY_KEY)} + + + + + + )} + + onChange({ isAddedDeployKey: v })} + > + + {createMessage(CONSENT_ADDED_DEPLOY_KEY)} + +  * + + + + + ); +} + +export default AddDeployKey; diff --git a/app/client/src/pages/Editor/gitSync/Tabs/GitConnectionV2/ChooseGitProvider.tsx b/app/client/src/pages/Editor/gitSync/Tabs/GitConnectionV2/ChooseGitProvider.tsx new file mode 100644 index 0000000000..fd07341f52 --- /dev/null +++ b/app/client/src/pages/Editor/gitSync/Tabs/GitConnectionV2/ChooseGitProvider.tsx @@ -0,0 +1,235 @@ +import React from "react"; +import { + DemoImage, + FieldContainer, + FieldControl, + FieldQuestion, + WellContainer, + WellTitle, + WellTitleContainer, +} from "./styles"; +import { + Callout, + Checkbox, + Collapsible, + CollapsibleContent, + CollapsibleHeader, + Icon, + Radio, + RadioGroup, + Text, +} from "design-system"; +import styled from "styled-components"; +import { GIT_DEMO_GIF } from "./constants"; +import { useDispatch, useSelector } from "react-redux"; +import { ReduxActionTypes } from "@appsmith/constants/ReduxActionConstants"; +import { setWorkspaceIdForImport } from "@appsmith/actions/applicationActions"; +import { setIsGitSyncModalOpen } from "actions/gitSyncActions"; +import { GitSyncModalTab } from "entities/GitSync"; +import { getCurrentAppWorkspace } from "@appsmith/selectors/workspaceSelectors"; +import history from "utils/history"; +import noop from "lodash/noop"; +import { hasCreateNewAppPermission } from "@appsmith/utils/permissionHelpers"; +import { useIsMobileDevice } from "utils/hooks/useDeviceDetect"; +import { + CHOOSE_A_GIT_PROVIDER_STEP, + CHOOSE_GIT_PROVIDER_QUESTION, + HOW_TO_CREATE_EMPTY_REPO, + IMPORT_APP_IF_NOT_EMPTY, + IS_EMPTY_REPO_QUESTION, + I_HAVE_EXISTING_REPO, + NEED_EMPTY_REPO_MESSAGE, + createMessage, +} from "@appsmith/constants/messages"; +import AnalyticsUtil from "utils/AnalyticsUtil"; + +const WellInnerContainer = styled.div` + padding-left: 16px; +`; + +const CheckboxTextContainer = styled.div` + display: flex; + justify-content: flex-start; +`; + +export type GitProvider = "github" | "gitlab" | "bitbucket" | "others"; + +interface ChooseGitProviderState { + gitProvider?: GitProvider; + gitEmptyRepoExists: string; + gitExistingRepoExists: boolean; +} +interface ChooseGitProviderProps { + onChange: (args: Partial) => void; + value: Partial; + isImport?: boolean; +} + +function ChooseGitProvider({ + onChange = noop, + value = {}, + isImport = false, +}: ChooseGitProviderProps) { + const workspace = useSelector(getCurrentAppWorkspace); + const isMobile = useIsMobileDevice(); + + const dispatch = useDispatch(); + const handleImport = () => { + history.push("/applications"); + + dispatch({ + type: ReduxActionTypes.GIT_INFO_INIT, + }); + dispatch(setWorkspaceIdForImport(workspace.id)); + + dispatch( + setIsGitSyncModalOpen({ + isOpen: true, + tab: GitSyncModalTab.GIT_CONNECTION, + }), + ); + AnalyticsUtil.logEvent("GS_IMPORT_VIA_GIT_DURING_GC"); + }; + + const hasCreateNewApplicationPermission = + hasCreateNewAppPermission(workspace.userPermissions) && !isMobile; + + return ( + <> + + + + {createMessage(CHOOSE_A_GIT_PROVIDER_STEP)} + + + + + + i. {createMessage(CHOOSE_GIT_PROVIDER_QUESTION)}{" "} + * + + + { + if ( + v === "github" || + v === "gitlab" || + v === "bitbucket" || + v === "others" + ) { + onChange({ gitProvider: v }); + } + }} + orientation="horizontal" + value={value?.gitProvider} + > + + Github + + + Gitlab + + + Bitbucket + + + Others + + + + + {!isImport && ( + + + ii. {createMessage(IS_EMPTY_REPO_QUESTION)}{" "} + * + + + onChange({ gitEmptyRepoExists: v })} + orientation="horizontal" + value={value?.gitEmptyRepoExists} + > + + Yes + + + No + + + + + )} + {!isImport && + value?.gitProvider !== "others" && + value?.gitEmptyRepoExists === "no" && ( + + + + {createMessage(HOW_TO_CREATE_EMPTY_REPO)} + + + + + + )} + {!isImport && + value?.gitProvider === "others" && + value?.gitEmptyRepoExists === "no" && ( + + {createMessage(NEED_EMPTY_REPO_MESSAGE)} + + )} + + + {!isImport && value?.gitEmptyRepoExists === "no" ? ( + + {createMessage(IMPORT_APP_IF_NOT_EMPTY)} + + ) : null} + {isImport && ( + onChange({ gitExistingRepoExists: v })} + > + + {createMessage(I_HAVE_EXISTING_REPO)} + +  * + + + + )} + + ); +} + +export default ChooseGitProvider; diff --git a/app/client/src/pages/Editor/gitSync/Tabs/GitConnectionV2/GenerateSSH.tsx b/app/client/src/pages/Editor/gitSync/Tabs/GitConnectionV2/GenerateSSH.tsx new file mode 100644 index 0000000000..ce194d6001 --- /dev/null +++ b/app/client/src/pages/Editor/gitSync/Tabs/GitConnectionV2/GenerateSSH.tsx @@ -0,0 +1,126 @@ +import React, { useState } from "react"; +import { + DemoImage, + ErrorCallout, + FieldContainer, + WellContainer, + WellText, + WellTitle, + WellTitleContainer, +} from "./styles"; +import { + Button, + Collapsible, + CollapsibleContent, + CollapsibleHeader, + Icon, + Input, + Text, +} from "design-system"; +import { isValidGitRemoteUrl } from "../../utils"; +import { + COPY_SSH_URL_MESSAGE, + ERROR_REPO_NOT_EMPTY_MESSAGE, + ERROR_REPO_NOT_EMPTY_TITLE, + GENERATE_SSH_KEY_STEP, + HOW_TO_COPY_REMOTE_URL, + PASTE_SSH_URL_INFO, + READ_DOCS, + REMOTE_URL_INPUT_LABEL, + createMessage, +} from "@appsmith/constants/messages"; +import type { GitProvider } from "./ChooseGitProvider"; +import { GIT_DEMO_GIF } from "./constants"; +import noop from "lodash/noop"; + +interface GenerateSSHState { + gitProvider?: GitProvider; + remoteUrl: string; +} +interface GenerateSSHProps { + onChange: (args: Partial) => void; + value: Partial; + errorData?: any; +} + +function GenerateSSH({ + onChange = noop, + value = {}, + errorData, +}: GenerateSSHProps) { + const [isTouched, setIsTouched] = useState(false); + const isInvalid = + isTouched && + (typeof value?.remoteUrl !== "string" || + !isValidGitRemoteUrl(value?.remoteUrl)); + + const handleChange = (v: string) => { + setIsTouched(true); + onChange({ remoteUrl: v }); + }; + + return ( + <> + {/* hardcoding messages because server doesn't support feature flag. Will change this later */} + {errorData && errorData?.responseMeta?.error?.code === "AE-GIT-4033" && ( + + + {createMessage(ERROR_REPO_NOT_EMPTY_TITLE)} + + + {createMessage(ERROR_REPO_NOT_EMPTY_MESSAGE)} + + + )} + + + + {createMessage(GENERATE_SSH_KEY_STEP)} + + + + {createMessage(COPY_SSH_URL_MESSAGE)} + + + + {value?.gitProvider !== "others" && ( + + + + {createMessage(HOW_TO_COPY_REMOTE_URL)} + + + + + + )} + + + ); +} + +export default GenerateSSH; diff --git a/app/client/src/pages/Editor/gitSync/Tabs/GitConnectionV2/Steps.tsx b/app/client/src/pages/Editor/gitSync/Tabs/GitConnectionV2/Steps.tsx new file mode 100644 index 0000000000..a621ae5188 --- /dev/null +++ b/app/client/src/pages/Editor/gitSync/Tabs/GitConnectionV2/Steps.tsx @@ -0,0 +1,116 @@ +import { Button, Divider, Text } from "design-system"; +import noop from "lodash/noop"; +import React, { Fragment } from "react"; +import styled from "styled-components"; + +const Container = styled.div` + display: flex; + margin-bottom: 16px; + align-items: center; +`; + +const StepButton = styled(Button)` + display: flex; + align-items: center; + + .ads-v2-button__content { + padding: 4px; + } + + .ads-v2-button__content-children { + font-weight: var(--ads-v2-font-weight-bold); + } + + .ads-v2-button__content-children > * { + font-weight: var(--ads-v2-font-weight-bold); + } +`; + +interface StepNumberProps { + active: boolean; +} + +const StepNumber = styled.div` + width: 20px; + height: 20px; + border-radius: 10px; + border-style: solid; + border-width: 1px; + border-color: ${(p) => + p.active + ? "var(--ads-v2-color-border-success)" + : "var(--ads-v2-color-border-emphasis)"}; + background-color: ${(p) => + p.active + ? "var(--ads-v2-color-bg-success)" + : "var(--ads-v2-color-bg-subtle)"}; + color: ${(p) => + p.active + ? "var(--ads-v2-color-border-success)" + : "var(--ads-v2-color-text)"}; + display: flex; + justify-content: center; + align-items: center; + line-height: 1; + margin-right: 4px; + flex-shrink: 0; +`; + +const StepText = styled(Text)` + font-weight: 500; +`; + +const StepLine = styled(Divider)` + width: initial; + margin-left: 8px; + margin-right: 8px; + flex: 1; +`; + +interface StepsProps { + steps: { + key: string; + text: string; + }[]; + activeKey: string; + onActiveKeyChange: (activeKey: string) => void; +} + +function Steps({ + steps = [], + activeKey, + onActiveKeyChange = noop, +}: StepsProps) { + const activeIndex = steps.findIndex((s) => s.key === activeKey); + + return ( + + {steps.map((step, index) => { + return ( + + {index > 0 && } + activeIndex} + kind="tertiary" + onClick={() => { + if (index < activeIndex) { + onActiveKeyChange(step.key); + } + }} + role="button" + size="md" + style={{ opacity: index > activeIndex ? 0.6 : 1 }} + > + + {index + 1} + + {step.text} + + + ); + })} + + ); +} + +export default Steps; diff --git a/app/client/src/pages/Editor/gitSync/Tabs/GitConnectionV2/constants.ts b/app/client/src/pages/Editor/gitSync/Tabs/GitConnectionV2/constants.ts new file mode 100644 index 0000000000..258e9ac169 --- /dev/null +++ b/app/client/src/pages/Editor/gitSync/Tabs/GitConnectionV2/constants.ts @@ -0,0 +1,26 @@ +import { getAssetUrl } from "@appsmith/utils/airgapHelpers"; +import { ASSETS_CDN_URL } from "constants/ThirdPartyConstants"; + +export const GIT_CONNECT_STEPS = { + CHOOSE_PROVIDER: "choose-provider", + GENERATE_SSH_KEY: "generate-ssh-key", + ADD_DEPLOY_KEY: "add-deploy-key", +}; + +export const GIT_DEMO_GIF = { + create_repo: { + github: getAssetUrl(`${ASSETS_CDN_URL}/Github_create_empty_repo.gif`), + gitlab: getAssetUrl(`${ASSETS_CDN_URL}/Gitlab_create_a_repo.gif`), + bitbucket: getAssetUrl(`${ASSETS_CDN_URL}/Bitbucket_create_a_repo.gif`), + }, + copy_remoteurl: { + github: getAssetUrl(`${ASSETS_CDN_URL}/Github_SSHkey.gif`), + gitlab: getAssetUrl(`${ASSETS_CDN_URL}/Gitlab_SSHKey.gif`), + bitbucket: getAssetUrl(`${ASSETS_CDN_URL}/Bitbucket_Copy_SSHKey.gif`), + }, + add_deploykey: { + github: getAssetUrl(`${ASSETS_CDN_URL}/Github_add_deploykey.gif`), + gitlab: getAssetUrl(`${ASSETS_CDN_URL}/Gitlab_add_deploy_key.gif`), + bitbucket: getAssetUrl(`${ASSETS_CDN_URL}/Bitbucket_add_a_deploykey.gif`), + }, +}; diff --git a/app/client/src/pages/Editor/gitSync/Tabs/GitConnectionV2/index.tsx b/app/client/src/pages/Editor/gitSync/Tabs/GitConnectionV2/index.tsx new file mode 100644 index 0000000000..0a09c500b2 --- /dev/null +++ b/app/client/src/pages/Editor/gitSync/Tabs/GitConnectionV2/index.tsx @@ -0,0 +1,280 @@ +import React, { useState } from "react"; +import { Button, ModalBody, ModalFooter } from "design-system"; +import Steps from "./Steps"; +import type { GitProvider } from "./ChooseGitProvider"; +import ChooseGitProvider from "./ChooseGitProvider"; +import GenerateSSH from "./GenerateSSH"; +import AddDeployKey from "./AddDeployKey"; +import styled from "styled-components"; +import { GIT_CONNECT_STEPS } from "./constants"; +import { useGitConnect } from "../../hooks"; +import { isValidGitRemoteUrl } from "../../utils"; +import { importAppFromGit } from "actions/gitSyncActions"; +import { useDispatch, useSelector } from "react-redux"; +import { getIsImportingApplicationViaGit } from "selectors/gitSyncSelectors"; +import { + ADD_DEPLOY_KEY_STEP, + CHOOSE_A_GIT_PROVIDER_STEP, + CONFIGURE_GIT, + CONNECT_GIT_TEXT, + GENERATE_SSH_KEY_STEP, + GIT_CONNECT_WAITING, + GIT_IMPORT_WAITING, + PREVIOUS_STEP, + createMessage, +} from "@appsmith/constants/messages"; +import GitSyncStatusbar from "../../components/Statusbar"; +import AnalyticsUtil from "utils/AnalyticsUtil"; + +interface StyledModalFooterProps { + loading?: boolean; +} + +const StepContent = styled.div` + display: flex; + flex-direction: column; + max-height: 580px; +`; + +const StyledModalFooter = styled(ModalFooter)` + justify-content: space-between; + flex-direction: ${(p) => (!p.loading ? "row-reverse" : "row")}; +`; + +const StatusbarWrapper = styled.div` + margin-top: 16px; + + > div { + display: flex; + height: initial; + align-items: center; + } + + > div > div { + margin-top: 0px; + width: 200px; + margin-right: 12px; + } + + > div > p { + margin-top: 0; + } +`; + +const steps = [ + { + key: GIT_CONNECT_STEPS.CHOOSE_PROVIDER, + text: createMessage(CHOOSE_A_GIT_PROVIDER_STEP), + }, + { + key: GIT_CONNECT_STEPS.GENERATE_SSH_KEY, + text: createMessage(GENERATE_SSH_KEY_STEP), + }, + { + key: GIT_CONNECT_STEPS.ADD_DEPLOY_KEY, + text: createMessage(ADD_DEPLOY_KEY_STEP), + }, +]; + +const possibleSteps = steps.map((s) => s.key); + +const nextStepText = { + [GIT_CONNECT_STEPS.CHOOSE_PROVIDER]: createMessage(CONFIGURE_GIT), + [GIT_CONNECT_STEPS.GENERATE_SSH_KEY]: createMessage(GENERATE_SSH_KEY_STEP), + [GIT_CONNECT_STEPS.ADD_DEPLOY_KEY]: createMessage(CONNECT_GIT_TEXT), +}; + +interface FormDataState { + gitProvider?: GitProvider; + gitEmptyRepoExists?: string; + gitExistingRepoExists?: boolean; + remoteUrl?: string; + isAddedDeployKey?: boolean; + sshKeyType?: "RSA" | "ECDSA"; +} + +interface GitConnectionV2Props { + isImport?: boolean; +} + +function GitConnectionV2({ isImport = false }: GitConnectionV2Props) { + const [errorData, setErrorData] = useState(); + const isImportingViaGit = useSelector(getIsImportingApplicationViaGit); + const dispatch = useDispatch(); + + const [formData, setFormData] = useState({ + gitProvider: undefined, + gitEmptyRepoExists: undefined, + gitExistingRepoExists: false, + remoteUrl: undefined, + isAddedDeployKey: false, + sshKeyType: "ECDSA", + }); + + const handleChange = (partialFormData: Partial) => { + setFormData((s) => ({ ...s, ...partialFormData })); + }; + + const [activeStep, setActiveStep] = useState( + GIT_CONNECT_STEPS.CHOOSE_PROVIDER, + ); + const currentIndex = steps.findIndex((s) => s.key === activeStep); + + const { connectToGit, isConnectingToGit } = useGitConnect(); + + const isDisabled = { + [GIT_CONNECT_STEPS.CHOOSE_PROVIDER]: !isImport + ? !formData.gitProvider || + !formData.gitEmptyRepoExists || + formData.gitEmptyRepoExists === "no" + : !formData.gitProvider || !formData.gitExistingRepoExists, + [GIT_CONNECT_STEPS.GENERATE_SSH_KEY]: + typeof formData?.remoteUrl !== "string" || + !isValidGitRemoteUrl(formData?.remoteUrl), + [GIT_CONNECT_STEPS.ADD_DEPLOY_KEY]: !formData.isAddedDeployKey, + }; + + const handlePreviousStep = () => { + if (currentIndex > 0) { + setActiveStep(steps[currentIndex - 1].key); + } + }; + + const handleNextStep = () => { + if (currentIndex < steps.length) { + switch (activeStep) { + case GIT_CONNECT_STEPS.CHOOSE_PROVIDER: { + setActiveStep(GIT_CONNECT_STEPS.GENERATE_SSH_KEY); + AnalyticsUtil.logEvent("GS_CONFIGURE_GIT"); + break; + } + case GIT_CONNECT_STEPS.GENERATE_SSH_KEY: { + setActiveStep(GIT_CONNECT_STEPS.ADD_DEPLOY_KEY); + AnalyticsUtil.logEvent("GS_GENERATE_KEY_BUTTON_CLICK", { + repoUrl: formData?.remoteUrl, + connectFlow: "v2", + }); + break; + } + case GIT_CONNECT_STEPS.ADD_DEPLOY_KEY: { + const gitProfile = { + authorName: "", + authorEmail: "", + }; + if (formData.remoteUrl) { + if (!isImport) { + connectToGit( + { + remoteUrl: formData.remoteUrl, + gitProfile, + isDefaultProfile: true, + }, + { + onErrorCallback: (err: Error, response?: any) => { + // AE-GIT-4033 is repo not empty error + if (response?.responseMeta?.error?.code === "AE-GIT-4033") { + setActiveStep(GIT_CONNECT_STEPS.GENERATE_SSH_KEY); + } + setErrorData(response); + }, + }, + ); + AnalyticsUtil.logEvent( + "GS_CONNECT_BUTTON_ON_GIT_SYNC_MODAL_CLICK", + { repoUrl: formData?.remoteUrl, connectFlow: "v2" }, + ); + } else { + dispatch( + importAppFromGit({ + payload: { + remoteUrl: formData.remoteUrl, + gitProfile, + isDefaultProfile: true, + }, + onErrorCallback(error, response) { + setErrorData(response); + }, + }), + ); + } + } + break; + } + } + } + }; + + const stepProps = { + onChange: handleChange, + value: formData, + isImport, + errorData, + }; + + const loading = + (!isImport && isConnectingToGit) || (isImport && isImportingViaGit); + + return ( + <> + + {possibleSteps.includes(activeStep) && ( + + )} + + {activeStep === GIT_CONNECT_STEPS.CHOOSE_PROVIDER && ( + + )} + {activeStep === GIT_CONNECT_STEPS.GENERATE_SSH_KEY && ( + + )} + {activeStep === GIT_CONNECT_STEPS.ADD_DEPLOY_KEY && ( + + )} + + + + {loading && ( + + + + )} + {!loading && ( + + )} + {possibleSteps.includes(activeStep) && currentIndex > 0 && !loading && ( + + )} + + + ); +} + +export default GitConnectionV2; diff --git a/app/client/src/pages/Editor/gitSync/Tabs/GitConnectionV2/styles.tsx b/app/client/src/pages/Editor/gitSync/Tabs/GitConnectionV2/styles.tsx new file mode 100644 index 0000000000..bf872e895b --- /dev/null +++ b/app/client/src/pages/Editor/gitSync/Tabs/GitConnectionV2/styles.tsx @@ -0,0 +1,50 @@ +import { Callout, Text } from "design-system"; +import styled from "styled-components"; + +export const WellContainer = styled.div` + padding: 16px; + border-radius: 4px; + background-color: var(--ads-color-background-secondary); + margin-bottom: 16px; + overflow-y: auto; + max-height: calc(100vh - 540px); +`; + +export const WellTitleContainer = styled.div` + display: flex; + align-items: center; + margin-bottom: 16px; + justify-content: space-between; +`; + +export const WellTitle = styled(Text)` + font-weight: 600; +`; + +export const WellText = styled(Text)` + margin-bottom: 16px; +`; + +export const FieldContainer = styled.div` + margin-bottom: 16px; +`; + +export const FieldControl = styled.div` + padding-left: 24px; +`; + +export const FieldQuestion = styled(Text)` + margin-bottom: 16px; +`; + +export const DemoImage = styled.img` + width: 100%; + height: 300px; + object-fit: cover; + object-position: 50% 0; + background-color: var(--ads-color-black-200); +`; + +export const ErrorCallout = styled(Callout)` + margin-bottom: 16px; +`; diff --git a/app/client/src/pages/Editor/gitSync/Tabs/GitSettings/GitDisconnect.tsx b/app/client/src/pages/Editor/gitSync/Tabs/GitSettings/GitDisconnect.tsx new file mode 100644 index 0000000000..2785b82ca0 --- /dev/null +++ b/app/client/src/pages/Editor/gitSync/Tabs/GitSettings/GitDisconnect.tsx @@ -0,0 +1,81 @@ +import { + DANGER_ZONE, + DISCONNECT_GIT, + DISCONNECT_GIT_MESSAGE, + createMessage, +} from "@appsmith/constants/messages"; +import { + setDisconnectingGitApplication, + setIsDisconnectGitModalOpen, + setIsGitSyncModalOpen, +} from "actions/gitSyncActions"; +import { Button, Text } from "design-system"; +import React from "react"; +import { useDispatch, useSelector } from "react-redux"; +import { getCurrentApplication } from "selectors/editorSelectors"; +import styled from "styled-components"; +import AnalyticsUtil from "utils/AnalyticsUtil"; + +const Container = styled.div` + padding-top: 16px; + padding-bottom: 16px; +`; + +const HeadContainer = styled.div` + margin-bottom: 16px; +`; + +const BodyContainer = styled.div` + display: flex; +`; + +const BodyInnerContainer = styled.div` + flex: 1; + margin-right: 32px; +`; + +const SectionTitle = styled(Text)` + font-weight: 600; +`; + +function GitDisconnect() { + const dispatch = useDispatch(); + const currentApp = useSelector(getCurrentApplication); + + const handleDisconnect = () => { + AnalyticsUtil.logEvent("GS_DISCONNECT_GIT_CLICK", { + source: "GIT_CONNECTION_MODAL", + }); + dispatch(setIsGitSyncModalOpen({ isOpen: false })); + dispatch( + setDisconnectingGitApplication({ + id: currentApp?.id || "", + name: currentApp?.name || "", + }), + ); + dispatch(setIsDisconnectGitModalOpen(true)); + }; + + return ( + + + + {createMessage(DANGER_ZONE)} + + + + + + {createMessage(DISCONNECT_GIT)} + + {createMessage(DISCONNECT_GIT_MESSAGE)} + + + + + ); +} + +export default GitDisconnect; diff --git a/app/client/src/pages/Editor/gitSync/Tabs/GitSettings/GitUserSettings.tsx b/app/client/src/pages/Editor/gitSync/Tabs/GitSettings/GitUserSettings.tsx new file mode 100644 index 0000000000..e598ce05f9 --- /dev/null +++ b/app/client/src/pages/Editor/gitSync/Tabs/GitSettings/GitUserSettings.tsx @@ -0,0 +1,296 @@ +import React, { useEffect, useMemo } from "react"; +import { Space } from "../../components/StyledComponents"; +import { + AUTHOR_EMAIL, + AUTHOR_EMAIL_CANNOT_BE_EMPTY, + AUTHOR_NAME, + AUTHOR_NAME_CANNOT_BE_EMPTY, + FORM_VALIDATION_INVALID_EMAIL, + GIT_USER_SETTINGS_TITLE, + UPDATE_CONFIG, + USE_DEFAULT_CONFIGURATION, + createMessage, +} from "@appsmith/constants/messages"; +import styled, { keyframes } from "styled-components"; +import { Button, Input, Switch, Text } from "design-system"; +import { + getGlobalGitConfig, + getLocalGitConfig, + getIsFetchingGlobalGitConfig, + getIsFetchingLocalGitConfig, +} from "selectors/gitSyncSelectors"; +import { useDispatch, useSelector } from "react-redux"; +import type { SubmitHandler } from "react-hook-form"; +import { Controller, useForm } from "react-hook-form"; +import { omit } from "lodash"; +import { + fetchGlobalGitConfigInit, + fetchLocalGitConfigInit, + updateLocalGitConfigInit, +} from "actions/gitSyncActions"; + +const Container = styled.div` + padding-top: 16px; + padding-bottom: 16px; +`; + +const HeadContainer = styled.div` + display: flex; + align-items: center; + justify-content: space-between; +`; + +const InputContainer = styled.div` + margin-bottom: ${(props) => props.theme.spaces[5]}px; +`; + +const SectionTitle = styled(Text)` + font-weight: 600; +`; + +const loadingKeyframe = keyframes` + 100% { + transform: translateX(100%); + } +`; + +const DummyLabel = styled.div` + height: 17px; + width: 100px; + margin-bottom: 8px; + border-radius: 4px; + background-color: var(--ads-color-black-100); + position: relative; + + overflow: hidden; + + &::after { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + transform: translateX(-100%); + background-image: linear-gradient( + 90deg, + rgba(255, 255, 255, 0) 0, + rgba(255, 255, 255, 0.5) 20%, + rgba(255, 255, 255, 0.8) 60%, + rgba(255, 255, 255, 0) + ); + animation: ${loadingKeyframe} 5s infinite; + content: ""; + } +`; + +const DummyInput = styled.div` + height: 36px; + border-radius: 4px; + background-color: linear-gradient( + 90deg, + var(--ads-color-black-200) 0%, + rgba(240, 240, 240, 0) 100% + ); + background-color: var(--ads-color-black-100); + position: relative; + + overflow: hidden; + + &::after { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + transform: translateX(-100%); + background-image: linear-gradient( + 90deg, + rgba(255, 255, 255, 0) 0, + rgba(255, 255, 255, 0.5) 20%, + rgba(255, 255, 255, 0.8) 60%, + rgba(255, 255, 255, 0) + ); + animation: ${loadingKeyframe} 5s infinite; + content: ""; + } +`; + +const DummyField = () => { + return ( +
+ + +
+ ); +}; + +export type AuthorInfo = { + authorName: string; + authorEmail: string; + useGlobalProfile: boolean; +}; + +const GitUserSettings = () => { + const dispatch = useDispatch(); + const isFetchingGlobalGitConfig = useSelector(getIsFetchingGlobalGitConfig); + const isFetchingLocalGitConfig = useSelector(getIsFetchingLocalGitConfig); + const globalConfig = useSelector(getGlobalGitConfig); + const localConfig = useSelector(getLocalGitConfig); + + const { + control, + formState: { errors }, + handleSubmit, + register, + setValue, + watch, + } = useForm(); + + const useGlobalProfile = watch("useGlobalProfile"); + const authorName = watch("authorName"); + const authorEmail = watch("authorEmail"); + + useEffect(() => { + dispatch(fetchGlobalGitConfigInit()); + dispatch(fetchLocalGitConfigInit()); + }, []); + + useEffect(() => { + if (!isFetchingGlobalGitConfig && !isFetchingLocalGitConfig) { + setValue("useGlobalProfile", !!localConfig?.useGlobalProfile); + } + }, [isFetchingGlobalGitConfig, isFetchingLocalGitConfig]); + + useEffect(() => { + if (!isFetchingGlobalGitConfig && !isFetchingLocalGitConfig) { + if (!useGlobalProfile) { + setValue("authorName", localConfig?.authorName); + setValue("authorEmail", localConfig?.authorEmail); + } else { + setValue("authorName", globalConfig?.authorName); + setValue("authorEmail", globalConfig?.authorEmail); + } + } + }, [isFetchingGlobalGitConfig, isFetchingLocalGitConfig, useGlobalProfile]); + + const onSubmit: SubmitHandler = (data) => { + if (data.useGlobalProfile) { + data.authorName = localConfig?.authorName; + data.authorEmail = localConfig?.authorEmail; + } + dispatch(updateLocalGitConfigInit(data)); + }; + + const isSubmitAllowed = useMemo(() => { + if (!isFetchingGlobalGitConfig && !isFetchingLocalGitConfig) { + if (useGlobalProfile) { + return ( + authorName !== globalConfig?.authorName || + authorEmail !== globalConfig?.authorEmail || + useGlobalProfile !== localConfig?.useGlobalProfile + ); + } else { + return ( + authorName !== localConfig?.authorName || + authorEmail !== localConfig?.authorEmail || + useGlobalProfile !== localConfig?.useGlobalProfile + ); + } + } else { + return false; + } + }, [ + isFetchingGlobalGitConfig, + isFetchingLocalGitConfig, + localConfig, + globalConfig, + useGlobalProfile, + authorName, + authorEmail, + ]); + + const loading = isFetchingGlobalGitConfig || isFetchingLocalGitConfig; + + return ( + +
+ + + {createMessage(GIT_USER_SETTINGS_TITLE)} + +
+ { + return ( + + {createMessage(USE_DEFAULT_CONFIGURATION)} + + ); + }} + /> +
+
+ + + + {!loading ? ( + setValue("authorName", v)} + /> + ) : ( + + )} + + + {!loading ? ( + setValue("authorEmail", v)} + /> + ) : ( + + )} + +
+ +
+ +
+ ); +}; + +export default GitUserSettings; diff --git a/app/client/src/pages/Editor/gitSync/Tabs/GitSettings/index.tsx b/app/client/src/pages/Editor/gitSync/Tabs/GitSettings/index.tsx new file mode 100644 index 0000000000..cdb8431a28 --- /dev/null +++ b/app/client/src/pages/Editor/gitSync/Tabs/GitSettings/index.tsx @@ -0,0 +1,23 @@ +import React from "react"; +import GitUserSettings from "./GitUserSettings"; +import GitDisconnect from "./GitDisconnect"; +import styled from "styled-components"; +import { ModalBody } from "design-system"; + +const Container = styled.div` + overflow: auto; + min-height: calc(360px + 52px); +`; + +function GitSettings() { + return ( + + + + + + + ); +} + +export default GitSettings; diff --git a/app/client/src/pages/Editor/gitSync/Tabs/Merge.tsx b/app/client/src/pages/Editor/gitSync/Tabs/Merge.tsx index e442371614..0bca25a0a6 100644 --- a/app/client/src/pages/Editor/gitSync/Tabs/Merge.tsx +++ b/app/client/src/pages/Editor/gitSync/Tabs/Merge.tsx @@ -220,7 +220,9 @@ export default function Merge() { return ( <> - + {createMessage(SELECT_BRANCH_TO_MERGE)} @@ -288,7 +290,7 @@ export default function Merge() { ) : null} - + {!showMergeSuccessIndicator && showMergeButton ? (