feat: git connect v2 (#26725)
## Description UX improvements for Git Connect Flow https://zpl.io/W4AQoek #### PR fixes following issue(s) Fixes #25588 #### Media > A video or a GIF is preferred. when using Loom, don’t embed because it looks like it’s a GIF. instead, just link to the video > > #### Type of change - New feature (non-breaking change which adds functionality) - This change requires a documentation update > > > ## Testing > #### How Has This Been Tested? > Please describe the tests that you ran to verify your changes. Also list any relevant details for your test configuration. > Delete anything that is not relevant - [ ] Manual - [ ] JUnit - [ ] Jest - [ ] Cypress > > #### Test Plan > Add Testsmith test cases links that relate to this PR > > #### Issues raised during DP testing https://github.com/appsmithorg/appsmith/pull/26725#issuecomment-1709723205 https://github.com/appsmithorg/appsmith/pull/26725#issuecomment-1711136694 > > > ## Checklist: #### Dev activity - [ ] My code follows the style guidelines of this project - [ ] I have performed a self-review of my own code - [ ] I have commented my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [ ] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [ ] New and existing unit tests pass locally with my changes - [ ] PR is being merged under a feature flag #### QA activity: - [ ] [Speedbreak features](https://github.com/appsmithorg/TestSmith/wiki/Guidelines-for-test-plans#speedbreakers-) have been covered - [ ] Test plan covers all impacted features and [areas of interest](https://github.com/appsmithorg/TestSmith/wiki/Guidelines-for-test-plans#areas-of-interest-) - [ ] Test plan has been peer reviewed by project stakeholders and other QA members - [ ] Manually tested functionality on DP - [ ] We had an implementation alignment call with stakeholders post QA Round 2 - [ ] Cypress test cases have been added and approved by SDET/manual QA - [ ] Added `Test Plan Approved` label after Cypress tests were reviewed - [ ] Added `Test Plan Approved` label after JUnit tests were reviewed
This commit is contained in:
parent
66d5027126
commit
68a439345d
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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<ConnectToGitPayload> {
|
||||
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) => ({
|
||||
|
|
|
|||
|
|
@ -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 = () =>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ export enum GitSyncModalTab {
|
|||
GIT_CONNECTION = "GIT_CONNECTION",
|
||||
DEPLOY = "DEPLOY",
|
||||
MERGE = "MERGE",
|
||||
SETTINGS = "SETTINGS",
|
||||
}
|
||||
|
||||
export type GitConfig = {
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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 (
|
||||
<>
|
||||
<Modal
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
handleClose();
|
||||
}
|
||||
}}
|
||||
open={isModalOpen}
|
||||
>
|
||||
<StyledModalContent data-testid="t--git-sync-modal">
|
||||
<ModalHeader>
|
||||
{modalTitle[activeTabKey] || gitMetadata?.repoName}
|
||||
</ModalHeader>
|
||||
<EnvInfoHeader />
|
||||
{isGitConnected && <ReconnectSSHError />}
|
||||
{possibleMenuOptions.includes(activeTabKey) && (
|
||||
<Menu
|
||||
activeTabKey={activeTabKey}
|
||||
onSelect={(tabKey: string) =>
|
||||
setActiveTabKey(tabKey as GitSyncModalTab)
|
||||
}
|
||||
options={menuOptions}
|
||||
/>
|
||||
)}
|
||||
{activeTabKey === GitSyncModalTab.GIT_CONNECTION &&
|
||||
(!isGitConnected ? (
|
||||
<GitConnectionV2 isImport={isImport} />
|
||||
) : (
|
||||
<ConnectionSuccess />
|
||||
))}
|
||||
{activeTabKey === GitSyncModalTab.DEPLOY && <Deploy />}
|
||||
{activeTabKey === GitSyncModalTab.MERGE && <Merge />}
|
||||
{activeTabKey === GitSyncModalTab.SETTINGS && <GitSettings />}
|
||||
</StyledModalContent>
|
||||
</Modal>
|
||||
<GitErrorPopup />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default GitSyncModalV2;
|
||||
23
app/client/src/pages/Editor/gitSync/GitSyncModal/index.tsx
Normal file
23
app/client/src/pages/Editor/gitSync/GitSyncModal/index.tsx
Normal file
|
|
@ -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 ? (
|
||||
<GitSyncModalV2 {...props} />
|
||||
) : (
|
||||
<GitSyncModalV1 {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
export default GitSyncModal;
|
||||
|
|
@ -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", {
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<>
|
||||
<ModalBody>
|
||||
<Container>
|
||||
<TitleContainer>
|
||||
<StyledIcon color="#059669" name="oval-check" size="lg" />
|
||||
<TitleText kind="heading-s" renderAs="h3">
|
||||
{createMessage(GIT_CONNECT_SUCCESS_TITLE)}
|
||||
</TitleText>
|
||||
</TitleContainer>
|
||||
<Text renderAs="p">{createMessage(GIT_CONNECT_SUCCESS_MESSAGE)}</Text>
|
||||
</Container>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button
|
||||
data-testid="t--start-using-git-button"
|
||||
onClick={handleClose}
|
||||
size="md"
|
||||
>
|
||||
{createMessage(START_USING_GIT)}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default ConnectionSuccess;
|
||||
|
|
@ -285,6 +285,7 @@ function Deploy() {
|
|||
<Container
|
||||
data-testid={"t--deploy-tab-container"}
|
||||
ref={scrollWrapperRef}
|
||||
style={{ minHeight: 360 }}
|
||||
>
|
||||
<Section>
|
||||
{hasChangesToCommit && (
|
||||
|
|
@ -428,7 +429,7 @@ function Deploy() {
|
|||
)}
|
||||
</Container>
|
||||
</ModalBody>
|
||||
<ModalFooter key="footer">
|
||||
<ModalFooter key="footer" style={{ minHeight: 52 }}>
|
||||
{showPullButton && (
|
||||
<Button
|
||||
className="t--pull-button"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,349 @@
|
|||
import React, { useEffect, useState } from "react";
|
||||
import {
|
||||
DemoImage,
|
||||
ErrorCallout,
|
||||
FieldContainer,
|
||||
WellContainer,
|
||||
WellText,
|
||||
WellTitle,
|
||||
WellTitleContainer,
|
||||
} from "./styles";
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleHeader,
|
||||
Icon,
|
||||
Option,
|
||||
Select,
|
||||
Text,
|
||||
toast,
|
||||
} from "design-system";
|
||||
import styled from "styled-components";
|
||||
import { CopyButton } from "../../components/CopyButton";
|
||||
import AnalyticsUtil from "utils/AnalyticsUtil";
|
||||
import {
|
||||
ADD_DEPLOY_KEY_STEP_TITLE,
|
||||
CONSENT_ADDED_DEPLOY_KEY,
|
||||
COPY_SSH_KEY,
|
||||
ERROR_SSH_KEY_MISCONF_MESSAGE,
|
||||
ERROR_SSH_KEY_MISCONF_TITLE,
|
||||
HOW_TO_ADD_DEPLOY_KEY,
|
||||
READ_DOCS,
|
||||
createMessage,
|
||||
} from "@appsmith/constants/messages";
|
||||
import { useSSHKeyPair } from "../../hooks";
|
||||
import type { GitProvider } from "./ChooseGitProvider";
|
||||
import { GIT_DEMO_GIF } from "./constants";
|
||||
import noop from "lodash/noop";
|
||||
import { useSelector } from "react-redux";
|
||||
import { getIsGitSyncModalOpen } from "selectors/gitSyncSelectors";
|
||||
|
||||
export const DeployedKeyContainer = styled.div`
|
||||
height: 36px;
|
||||
border: 1px solid var(--ads-v2-color-border);
|
||||
padding: 8px;
|
||||
box-sizing: border-box;
|
||||
border-radius: var(--ads-v2-border-radius);
|
||||
background-color: #fff;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
`;
|
||||
|
||||
export const FlexRow = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
width: 100%;
|
||||
gap: 3px;
|
||||
`;
|
||||
|
||||
export const KeyType = styled.span`
|
||||
font-size: 10px;
|
||||
text-transform: uppercase;
|
||||
color: var(--ads-v2-color-fg);
|
||||
font-weight: 700;
|
||||
`;
|
||||
|
||||
export const KeyText = styled.span`
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
flex: 1;
|
||||
font-size: 10px;
|
||||
text-transform: uppercase;
|
||||
color: var(--ads-v2-color-fg);
|
||||
direction: rtl;
|
||||
margin-right: 8px;
|
||||
`;
|
||||
|
||||
const StyledSelect = styled(Select)`
|
||||
margin-bottom: 4px;
|
||||
background-color: white;
|
||||
width: initial;
|
||||
|
||||
.rc-select-selector {
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 100px !important;
|
||||
}
|
||||
`;
|
||||
|
||||
const CheckboxTextContainer = styled.div`
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
`;
|
||||
|
||||
const DummyKey = styled.div`
|
||||
height: 36px;
|
||||
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
var(--ads-color-black-200) 0%,
|
||||
rgba(240, 240, 240, 0) 100%
|
||||
);
|
||||
`;
|
||||
|
||||
const getRepositorySettingsUrl = (
|
||||
gitProvider?: GitProvider,
|
||||
remoteUrl?: string,
|
||||
) => {
|
||||
if (!gitProvider) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const ownerRepo = remoteUrl?.split(":")?.[1]?.split(".git")?.[0];
|
||||
|
||||
if (!ownerRepo) {
|
||||
return "";
|
||||
}
|
||||
|
||||
switch (gitProvider) {
|
||||
case "github":
|
||||
return `https://github.com/${ownerRepo}/settings/keys`;
|
||||
case "gitlab":
|
||||
return `https://gitlab.com/${ownerRepo}/-/settings/repository`;
|
||||
case "bitbucket":
|
||||
return `https://bitbucket.org/${ownerRepo}/admin/access-keys/`;
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
};
|
||||
|
||||
interface AddDeployKeyState {
|
||||
gitProvider?: GitProvider;
|
||||
isAddedDeployKey: boolean;
|
||||
remoteUrl: string;
|
||||
}
|
||||
interface AddDeployKeyProps {
|
||||
onChange: (args: Partial<AddDeployKeyState>) => void;
|
||||
value: Partial<AddDeployKeyState>;
|
||||
isImport?: boolean;
|
||||
errorData?: any;
|
||||
connectLoading?: boolean;
|
||||
}
|
||||
|
||||
function AddDeployKey({
|
||||
onChange = noop,
|
||||
value = {},
|
||||
isImport = false,
|
||||
errorData,
|
||||
connectLoading = false,
|
||||
}: AddDeployKeyProps) {
|
||||
const isModalOpen = useSelector(getIsGitSyncModalOpen);
|
||||
const [fetched, setFetched] = useState(false);
|
||||
const [sshKeyType, setSshKeyType] = useState<string>();
|
||||
const {
|
||||
deployKeyDocUrl,
|
||||
fetchingSSHKeyPair,
|
||||
fetchSSHKeyPair,
|
||||
generateSSHKey,
|
||||
generatingSSHKey,
|
||||
SSHKeyPair,
|
||||
} = useSSHKeyPair();
|
||||
|
||||
useEffect(() => {
|
||||
if (isModalOpen && !isImport) {
|
||||
if (!fetched) {
|
||||
fetchSSHKeyPair({
|
||||
onSuccessCallback: () => {
|
||||
setFetched(true);
|
||||
},
|
||||
onErrorCallback: () => {
|
||||
setFetched(true);
|
||||
},
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (!fetched) {
|
||||
setFetched(true);
|
||||
}
|
||||
}
|
||||
}, [isImport, isModalOpen, fetched]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isModalOpen && fetched && !fetchingSSHKeyPair) {
|
||||
if (SSHKeyPair && SSHKeyPair.includes("rsa")) {
|
||||
setSshKeyType("RSA");
|
||||
} else if (
|
||||
!SSHKeyPair &&
|
||||
value?.remoteUrl &&
|
||||
value.remoteUrl.toString().toLocaleLowerCase().includes("azure")
|
||||
) {
|
||||
setSshKeyType("RSA");
|
||||
} else {
|
||||
setSshKeyType("ECDSA");
|
||||
}
|
||||
}
|
||||
}, [isModalOpen, fetched, fetchingSSHKeyPair, SSHKeyPair]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
isModalOpen &&
|
||||
((sshKeyType && !SSHKeyPair) ||
|
||||
(sshKeyType && !SSHKeyPair?.includes(sshKeyType.toLowerCase())))
|
||||
) {
|
||||
generateSSHKey(sshKeyType, {
|
||||
onSuccessCallback: () => {
|
||||
toast.show("SSH Key generated successfully", { kind: "success" });
|
||||
},
|
||||
});
|
||||
}
|
||||
}, [sshKeyType, SSHKeyPair, isModalOpen]);
|
||||
|
||||
const repositorySettingsUrl = getRepositorySettingsUrl(
|
||||
value?.gitProvider,
|
||||
value?.remoteUrl,
|
||||
);
|
||||
|
||||
const loading = fetchingSSHKeyPair || generatingSSHKey;
|
||||
|
||||
return (
|
||||
<>
|
||||
{errorData &&
|
||||
errorData?.responseMeta?.error?.code !== "AE-GIT-4033" &&
|
||||
errorData?.responseMeta?.error?.code !== "AE-GIT-4032" && (
|
||||
<ErrorCallout kind="error">
|
||||
<Text kind="heading-xs" renderAs="h3">
|
||||
{errorData?.responseMeta?.error?.errorType}
|
||||
</Text>
|
||||
<Text renderAs="p">{errorData?.responseMeta?.error?.message}</Text>
|
||||
</ErrorCallout>
|
||||
)}
|
||||
|
||||
{/* hardcoding message because server doesn't support feature flag. Will change this later */}
|
||||
{errorData && errorData?.responseMeta?.error?.code === "AE-GIT-4032" && (
|
||||
<ErrorCallout kind="error">
|
||||
<Text kind="heading-xs" renderAs="h3">
|
||||
{createMessage(ERROR_SSH_KEY_MISCONF_TITLE)}
|
||||
</Text>
|
||||
<Text renderAs="p">
|
||||
{createMessage(ERROR_SSH_KEY_MISCONF_MESSAGE)}
|
||||
</Text>
|
||||
</ErrorCallout>
|
||||
)}
|
||||
|
||||
<WellContainer>
|
||||
<WellTitleContainer>
|
||||
<WellTitle kind="heading-s" renderAs="h3">
|
||||
{createMessage(ADD_DEPLOY_KEY_STEP_TITLE)}
|
||||
</WellTitle>
|
||||
<Button
|
||||
href={
|
||||
deployKeyDocUrl ||
|
||||
"https://docs.appsmith.com/advanced-concepts/version-control-with-git/connecting-to-git-repository"
|
||||
}
|
||||
kind="tertiary"
|
||||
renderAs="a"
|
||||
size="sm"
|
||||
startIcon="book-line"
|
||||
target="_blank"
|
||||
>
|
||||
{" "}
|
||||
{createMessage(READ_DOCS)}
|
||||
</Button>
|
||||
</WellTitleContainer>
|
||||
|
||||
<WellText renderAs="p">
|
||||
Copy below SSH key and paste it in your{" "}
|
||||
{!!repositorySettingsUrl && value.gitProvider !== "others" ? (
|
||||
<a
|
||||
href={repositorySettingsUrl}
|
||||
rel="noreferrer"
|
||||
style={{ color: "var(--ads-color-brand)" }}
|
||||
target="_blank"
|
||||
>
|
||||
repository settings.
|
||||
</a>
|
||||
) : (
|
||||
"repository settings."
|
||||
)}{" "}
|
||||
Now, give write access to it.
|
||||
</WellText>
|
||||
<FieldContainer>
|
||||
<StyledSelect
|
||||
onChange={(v) => setSshKeyType(v)}
|
||||
size="sm"
|
||||
value={sshKeyType}
|
||||
>
|
||||
<Option value="ECDSA">ECDSA 256</Option>
|
||||
<Option value="RSA">RSA 4096</Option>
|
||||
</StyledSelect>
|
||||
{!loading ? (
|
||||
<DeployedKeyContainer>
|
||||
<Icon
|
||||
color="var(--ads-v2-color-fg)"
|
||||
name="key-2-line"
|
||||
size="md"
|
||||
style={{ marginRight: 4 }}
|
||||
/>
|
||||
<KeyType>{sshKeyType}</KeyType>
|
||||
<KeyText>{SSHKeyPair}</KeyText>
|
||||
{!connectLoading && (
|
||||
<CopyButton
|
||||
onCopy={() => {
|
||||
AnalyticsUtil.logEvent("GS_COPY_SSH_KEY_BUTTON_CLICK");
|
||||
}}
|
||||
tooltipMessage={createMessage(COPY_SSH_KEY)}
|
||||
value={SSHKeyPair}
|
||||
/>
|
||||
)}
|
||||
</DeployedKeyContainer>
|
||||
) : (
|
||||
<DummyKey />
|
||||
)}
|
||||
</FieldContainer>
|
||||
{value?.gitProvider !== "others" && (
|
||||
<Collapsible isOpen>
|
||||
<CollapsibleHeader arrowPosition="end">
|
||||
<Icon name="play-circle-line" size="md" />
|
||||
<Text>{createMessage(HOW_TO_ADD_DEPLOY_KEY)}</Text>
|
||||
</CollapsibleHeader>
|
||||
<CollapsibleContent>
|
||||
<DemoImage
|
||||
alt={`Add deploy key in ${value?.gitProvider}`}
|
||||
src={GIT_DEMO_GIF.add_deploykey[value?.gitProvider || "github"]}
|
||||
/>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
)}
|
||||
</WellContainer>
|
||||
<Checkbox
|
||||
data-testid="t--added-deploy-key-checkbox"
|
||||
isSelected={value?.isAddedDeployKey}
|
||||
onChange={(v) => onChange({ isAddedDeployKey: v })}
|
||||
>
|
||||
<CheckboxTextContainer>
|
||||
<Text renderAs="p">{createMessage(CONSENT_ADDED_DEPLOY_KEY)}</Text>
|
||||
<Text color="var(--ads-v2-color-red-600)" renderAs="p">
|
||||
*
|
||||
</Text>
|
||||
</CheckboxTextContainer>
|
||||
</Checkbox>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default AddDeployKey;
|
||||
|
|
@ -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<ChooseGitProviderState>) => void;
|
||||
value: Partial<ChooseGitProviderState>;
|
||||
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 (
|
||||
<>
|
||||
<WellContainer>
|
||||
<WellTitleContainer>
|
||||
<WellTitle kind="heading-s" renderAs="h3">
|
||||
{createMessage(CHOOSE_A_GIT_PROVIDER_STEP)}
|
||||
</WellTitle>
|
||||
</WellTitleContainer>
|
||||
<WellInnerContainer>
|
||||
<FieldContainer>
|
||||
<FieldQuestion renderAs="p">
|
||||
i. {createMessage(CHOOSE_GIT_PROVIDER_QUESTION)}{" "}
|
||||
<Text color="var(--ads-v2-color-red-600)">*</Text>
|
||||
</FieldQuestion>
|
||||
<FieldControl>
|
||||
<RadioGroup
|
||||
onChange={(v) => {
|
||||
if (
|
||||
v === "github" ||
|
||||
v === "gitlab" ||
|
||||
v === "bitbucket" ||
|
||||
v === "others"
|
||||
) {
|
||||
onChange({ gitProvider: v });
|
||||
}
|
||||
}}
|
||||
orientation="horizontal"
|
||||
value={value?.gitProvider}
|
||||
>
|
||||
<Radio
|
||||
data-testid="t--git-provider-radio-github"
|
||||
value="github"
|
||||
>
|
||||
Github
|
||||
</Radio>
|
||||
<Radio
|
||||
data-testid="t--git-provider-radio-gitlab"
|
||||
value="gitlab"
|
||||
>
|
||||
Gitlab
|
||||
</Radio>
|
||||
<Radio
|
||||
data-testid="t--git-provider-radio-bitbucket"
|
||||
value="bitbucket"
|
||||
>
|
||||
Bitbucket
|
||||
</Radio>
|
||||
<Radio
|
||||
data-testid="t--git-provider-radio-others"
|
||||
value="others"
|
||||
>
|
||||
Others
|
||||
</Radio>
|
||||
</RadioGroup>
|
||||
</FieldControl>
|
||||
</FieldContainer>
|
||||
{!isImport && (
|
||||
<FieldContainer>
|
||||
<FieldQuestion
|
||||
renderAs="p"
|
||||
style={{ opacity: !value?.gitProvider ? 0.5 : 1 }}
|
||||
>
|
||||
ii. {createMessage(IS_EMPTY_REPO_QUESTION)}{" "}
|
||||
<Text color="var(--ads-v2-color-red-600)">*</Text>
|
||||
</FieldQuestion>
|
||||
<FieldControl>
|
||||
<RadioGroup
|
||||
isDisabled={!value?.gitProvider}
|
||||
onChange={(v) => onChange({ gitEmptyRepoExists: v })}
|
||||
orientation="horizontal"
|
||||
value={value?.gitEmptyRepoExists}
|
||||
>
|
||||
<Radio data-testid="t--existing-empty-repo-yes" value="yes">
|
||||
Yes
|
||||
</Radio>
|
||||
<Radio data-testid="t--existing-empty-repo-no" value="no">
|
||||
No
|
||||
</Radio>
|
||||
</RadioGroup>
|
||||
</FieldControl>
|
||||
</FieldContainer>
|
||||
)}
|
||||
{!isImport &&
|
||||
value?.gitProvider !== "others" &&
|
||||
value?.gitEmptyRepoExists === "no" && (
|
||||
<Collapsible isOpen>
|
||||
<CollapsibleHeader arrowPosition="end">
|
||||
<Icon name="play-circle-line" size="md" />
|
||||
<Text>{createMessage(HOW_TO_CREATE_EMPTY_REPO)}</Text>
|
||||
</CollapsibleHeader>
|
||||
<CollapsibleContent>
|
||||
<DemoImage
|
||||
alt={`Create an empty repo in ${value?.gitProvider}}`}
|
||||
src={
|
||||
GIT_DEMO_GIF.create_repo[value?.gitProvider || "github"]
|
||||
}
|
||||
/>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
)}
|
||||
{!isImport &&
|
||||
value?.gitProvider === "others" &&
|
||||
value?.gitEmptyRepoExists === "no" && (
|
||||
<Callout kind="warning">
|
||||
{createMessage(NEED_EMPTY_REPO_MESSAGE)}
|
||||
</Callout>
|
||||
)}
|
||||
</WellInnerContainer>
|
||||
</WellContainer>
|
||||
{!isImport && value?.gitEmptyRepoExists === "no" ? (
|
||||
<Callout
|
||||
kind="info"
|
||||
links={
|
||||
hasCreateNewApplicationPermission
|
||||
? [{ children: "Import via git", onClick: handleImport }]
|
||||
: []
|
||||
}
|
||||
>
|
||||
{createMessage(IMPORT_APP_IF_NOT_EMPTY)}
|
||||
</Callout>
|
||||
) : null}
|
||||
{isImport && (
|
||||
<Checkbox
|
||||
isSelected={value?.gitExistingRepoExists}
|
||||
onChange={(v) => onChange({ gitExistingRepoExists: v })}
|
||||
>
|
||||
<CheckboxTextContainer>
|
||||
<Text renderAs="p">{createMessage(I_HAVE_EXISTING_REPO)}</Text>
|
||||
<Text color="var(--ads-v2-color-red-600)" renderAs="p">
|
||||
*
|
||||
</Text>
|
||||
</CheckboxTextContainer>
|
||||
</Checkbox>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default ChooseGitProvider;
|
||||
|
|
@ -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<GenerateSSHState>) => void;
|
||||
value: Partial<GenerateSSHState>;
|
||||
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" && (
|
||||
<ErrorCallout kind="error">
|
||||
<Text kind="heading-xs" renderAs="h3">
|
||||
{createMessage(ERROR_REPO_NOT_EMPTY_TITLE)}
|
||||
</Text>
|
||||
<Text renderAs="p">
|
||||
{createMessage(ERROR_REPO_NOT_EMPTY_MESSAGE)}
|
||||
</Text>
|
||||
</ErrorCallout>
|
||||
)}
|
||||
<WellContainer>
|
||||
<WellTitleContainer>
|
||||
<WellTitle kind="heading-s" renderAs="h3">
|
||||
{createMessage(GENERATE_SSH_KEY_STEP)}
|
||||
</WellTitle>
|
||||
<Button
|
||||
href="https://docs.appsmith.com/advanced-concepts/version-control-with-git/connecting-to-git-repository"
|
||||
kind="tertiary"
|
||||
renderAs="a"
|
||||
size="sm"
|
||||
startIcon="book-line"
|
||||
target="_blank"
|
||||
>
|
||||
{" "}
|
||||
{createMessage(READ_DOCS)}
|
||||
</Button>
|
||||
</WellTitleContainer>
|
||||
<WellText renderAs="p">{createMessage(COPY_SSH_URL_MESSAGE)}</WellText>
|
||||
<FieldContainer>
|
||||
<Input
|
||||
data-testid="git-connect-remote-url-input"
|
||||
errorMessage={isInvalid ? createMessage(PASTE_SSH_URL_INFO) : ""}
|
||||
isRequired
|
||||
label={createMessage(REMOTE_URL_INPUT_LABEL)}
|
||||
onChange={handleChange}
|
||||
placeholder="git@example.com:user/repository.git"
|
||||
size="md"
|
||||
value={value?.remoteUrl}
|
||||
/>
|
||||
</FieldContainer>
|
||||
{value?.gitProvider !== "others" && (
|
||||
<Collapsible isOpen>
|
||||
<CollapsibleHeader arrowPosition="end">
|
||||
<Icon name="play-circle-line" size="md" />
|
||||
<Text>{createMessage(HOW_TO_COPY_REMOTE_URL)}</Text>
|
||||
</CollapsibleHeader>
|
||||
<CollapsibleContent>
|
||||
<DemoImage
|
||||
alt={`Copy and paste remote url from ${value?.gitProvider}`}
|
||||
src={
|
||||
GIT_DEMO_GIF.copy_remoteurl[value?.gitProvider || "github"]
|
||||
}
|
||||
/>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
)}
|
||||
</WellContainer>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default GenerateSSH;
|
||||
|
|
@ -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<StepNumberProps>`
|
||||
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 (
|
||||
<Container>
|
||||
{steps.map((step, index) => {
|
||||
return (
|
||||
<Fragment key={step.key}>
|
||||
{index > 0 && <StepLine />}
|
||||
<StepButton
|
||||
isDisabled={index > activeIndex}
|
||||
kind="tertiary"
|
||||
onClick={() => {
|
||||
if (index < activeIndex) {
|
||||
onActiveKeyChange(step.key);
|
||||
}
|
||||
}}
|
||||
role="button"
|
||||
size="md"
|
||||
style={{ opacity: index > activeIndex ? 0.6 : 1 }}
|
||||
>
|
||||
<StepNumber active={step.key === activeKey}>
|
||||
{index + 1}
|
||||
</StepNumber>
|
||||
<StepText>{step.text}</StepText>
|
||||
</StepButton>
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
export default Steps;
|
||||
|
|
@ -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`),
|
||||
},
|
||||
};
|
||||
|
|
@ -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)<StyledModalFooterProps>`
|
||||
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<any>();
|
||||
const isImportingViaGit = useSelector(getIsImportingApplicationViaGit);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const [formData, setFormData] = useState<FormDataState>({
|
||||
gitProvider: undefined,
|
||||
gitEmptyRepoExists: undefined,
|
||||
gitExistingRepoExists: false,
|
||||
remoteUrl: undefined,
|
||||
isAddedDeployKey: false,
|
||||
sshKeyType: "ECDSA",
|
||||
});
|
||||
|
||||
const handleChange = (partialFormData: Partial<FormDataState>) => {
|
||||
setFormData((s) => ({ ...s, ...partialFormData }));
|
||||
};
|
||||
|
||||
const [activeStep, setActiveStep] = useState<string>(
|
||||
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 (
|
||||
<>
|
||||
<ModalBody>
|
||||
{possibleSteps.includes(activeStep) && (
|
||||
<Steps
|
||||
activeKey={activeStep}
|
||||
onActiveKeyChange={setActiveStep}
|
||||
steps={steps}
|
||||
/>
|
||||
)}
|
||||
<StepContent>
|
||||
{activeStep === GIT_CONNECT_STEPS.CHOOSE_PROVIDER && (
|
||||
<ChooseGitProvider {...stepProps} />
|
||||
)}
|
||||
{activeStep === GIT_CONNECT_STEPS.GENERATE_SSH_KEY && (
|
||||
<GenerateSSH {...stepProps} />
|
||||
)}
|
||||
{activeStep === GIT_CONNECT_STEPS.ADD_DEPLOY_KEY && (
|
||||
<AddDeployKey {...stepProps} connectLoading={loading} />
|
||||
)}
|
||||
</StepContent>
|
||||
</ModalBody>
|
||||
<StyledModalFooter loading={loading}>
|
||||
{loading && (
|
||||
<StatusbarWrapper className="t--connect-statusbar">
|
||||
<GitSyncStatusbar
|
||||
completed={!loading}
|
||||
message={createMessage(
|
||||
isImport ? GIT_IMPORT_WAITING : GIT_CONNECT_WAITING,
|
||||
)}
|
||||
period={4}
|
||||
/>
|
||||
</StatusbarWrapper>
|
||||
)}
|
||||
{!loading && (
|
||||
<Button
|
||||
data-testid="t--git-connect-next-button"
|
||||
endIcon={
|
||||
currentIndex < steps.length - 1 ? "arrow-right-s-line" : undefined
|
||||
}
|
||||
isDisabled={isDisabled[activeStep]}
|
||||
onClick={handleNextStep}
|
||||
size="md"
|
||||
>
|
||||
{nextStepText[activeStep]}
|
||||
</Button>
|
||||
)}
|
||||
{possibleSteps.includes(activeStep) && currentIndex > 0 && !loading && (
|
||||
<Button
|
||||
isDisabled={loading}
|
||||
kind="secondary"
|
||||
onClick={handlePreviousStep}
|
||||
size="md"
|
||||
startIcon="arrow-left-s-line"
|
||||
>
|
||||
{createMessage(PREVIOUS_STEP)}
|
||||
</Button>
|
||||
)}
|
||||
</StyledModalFooter>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default GitConnectionV2;
|
||||
|
|
@ -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;
|
||||
`;
|
||||
|
|
@ -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 (
|
||||
<Container>
|
||||
<HeadContainer>
|
||||
<SectionTitle kind="heading-s">
|
||||
{createMessage(DANGER_ZONE)}
|
||||
</SectionTitle>
|
||||
</HeadContainer>
|
||||
<BodyContainer>
|
||||
<BodyInnerContainer>
|
||||
<Text kind="heading-xs" renderAs="p">
|
||||
{createMessage(DISCONNECT_GIT)}
|
||||
</Text>
|
||||
<Text renderAs="p">{createMessage(DISCONNECT_GIT_MESSAGE)}</Text>
|
||||
</BodyInnerContainer>
|
||||
<Button kind="error" onClick={handleDisconnect} size="md">
|
||||
{createMessage(DISCONNECT_GIT)}
|
||||
</Button>
|
||||
</BodyContainer>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
export default GitDisconnect;
|
||||
|
|
@ -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 (
|
||||
<div style={{ width: "100%" }}>
|
||||
<DummyLabel />
|
||||
<DummyInput />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
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<AuthorInfo>();
|
||||
|
||||
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<AuthorInfo> = (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 (
|
||||
<Container>
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<HeadContainer>
|
||||
<SectionTitle kind="heading-s">
|
||||
{createMessage(GIT_USER_SETTINGS_TITLE)}
|
||||
</SectionTitle>
|
||||
<div>
|
||||
<Controller
|
||||
control={control}
|
||||
name="useGlobalProfile"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Switch
|
||||
data-testid="t--git-user-settings-switch"
|
||||
isDisabled={loading}
|
||||
{...omit(field, ["value"])}
|
||||
isSelected={field.value}
|
||||
>
|
||||
{createMessage(USE_DEFAULT_CONFIGURATION)}
|
||||
</Switch>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</HeadContainer>
|
||||
|
||||
<Space size={5} />
|
||||
<InputContainer>
|
||||
{!loading ? (
|
||||
<Input
|
||||
data-testid="t--git-user-settings-author-name-input"
|
||||
errorMessage={errors?.authorName?.message}
|
||||
isReadOnly={useGlobalProfile}
|
||||
isValid={!errors?.authorName}
|
||||
label={createMessage(AUTHOR_NAME)}
|
||||
size="md"
|
||||
type="text"
|
||||
{...register("authorName", {
|
||||
required: createMessage(AUTHOR_NAME_CANNOT_BE_EMPTY),
|
||||
})}
|
||||
// onChange is overwritten with setValue
|
||||
onChange={(v) => setValue("authorName", v)}
|
||||
/>
|
||||
) : (
|
||||
<DummyField />
|
||||
)}
|
||||
</InputContainer>
|
||||
<InputContainer>
|
||||
{!loading ? (
|
||||
<Input
|
||||
data-testid="t--git-user-settings-author-email-input"
|
||||
errorMessage={errors?.authorEmail?.message}
|
||||
isReadOnly={useGlobalProfile}
|
||||
isValid={!errors?.authorEmail}
|
||||
label={createMessage(AUTHOR_EMAIL)}
|
||||
size="md"
|
||||
// type="email"
|
||||
{...register("authorEmail", {
|
||||
required: createMessage(AUTHOR_EMAIL_CANNOT_BE_EMPTY),
|
||||
pattern: {
|
||||
value: /^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/,
|
||||
message: createMessage(FORM_VALIDATION_INVALID_EMAIL),
|
||||
},
|
||||
})}
|
||||
// onChange is overwritten with setValue
|
||||
onChange={(v) => setValue("authorEmail", v)}
|
||||
/>
|
||||
) : (
|
||||
<DummyField />
|
||||
)}
|
||||
</InputContainer>
|
||||
<div>
|
||||
<Button isDisabled={!isSubmitAllowed} size="md" type="submit">
|
||||
{createMessage(UPDATE_CONFIG)}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default GitUserSettings;
|
||||
|
|
@ -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 (
|
||||
<ModalBody>
|
||||
<Container>
|
||||
<GitUserSettings />
|
||||
<GitDisconnect />
|
||||
</Container>
|
||||
</ModalBody>
|
||||
);
|
||||
}
|
||||
|
||||
export default GitSettings;
|
||||
|
|
@ -220,7 +220,9 @@ export default function Merge() {
|
|||
return (
|
||||
<>
|
||||
<ModalBody>
|
||||
<Container style={{ overflow: "unset", paddingBottom: "4px" }}>
|
||||
<Container
|
||||
style={{ minHeight: 360, overflow: "unset", paddingBottom: "4px" }}
|
||||
>
|
||||
<Text color={"var(--ads-v2-color-fg-emphasis)"} kind="heading-s">
|
||||
{createMessage(SELECT_BRANCH_TO_MERGE)}
|
||||
</Text>
|
||||
|
|
@ -288,7 +290,7 @@ export default function Merge() {
|
|||
) : null}
|
||||
</Container>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<ModalFooter style={{ minHeight: 52 }}>
|
||||
{!showMergeSuccessIndicator && showMergeButton ? (
|
||||
<Button
|
||||
className="t--git-merge-button"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,74 @@
|
|||
import React, { useRef, useState } from "react";
|
||||
import { Button, Icon, Tooltip } from "design-system";
|
||||
import styled from "styled-components";
|
||||
import copy from "copy-to-clipboard";
|
||||
import noop from "lodash/noop";
|
||||
|
||||
export const TooltipWrapper = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
export const IconContainer = styled.div``;
|
||||
|
||||
interface CopyButtonProps {
|
||||
value?: string;
|
||||
delay?: number;
|
||||
onCopy?: () => void;
|
||||
tooltipMessage?: string;
|
||||
isDisabled?: boolean;
|
||||
}
|
||||
|
||||
export function CopyButton({
|
||||
value,
|
||||
delay = 2000,
|
||||
onCopy = noop,
|
||||
tooltipMessage,
|
||||
isDisabled = false,
|
||||
}: CopyButtonProps) {
|
||||
const timerRef = useRef<number>();
|
||||
const [showCopied, setShowCopied] = useState(false);
|
||||
|
||||
const stopShowingCopiedAfterDelay = () => {
|
||||
timerRef.current = setTimeout(() => {
|
||||
setShowCopied(false);
|
||||
}, delay);
|
||||
};
|
||||
const copyToClipboard = () => {
|
||||
if (value) {
|
||||
copy(value);
|
||||
setShowCopied(true);
|
||||
stopShowingCopiedAfterDelay();
|
||||
}
|
||||
onCopy();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{showCopied ? (
|
||||
<IconContainer>
|
||||
<Icon
|
||||
color="var(--ads-v2-color-fg-success)"
|
||||
name="check-line"
|
||||
size="lg"
|
||||
/>
|
||||
</IconContainer>
|
||||
) : (
|
||||
<TooltipWrapper>
|
||||
<Tooltip content={tooltipMessage}>
|
||||
<Button
|
||||
className="t--copy-ssh-key"
|
||||
isDisabled={isDisabled}
|
||||
isIconButton
|
||||
kind="tertiary"
|
||||
onClick={copyToClipboard}
|
||||
size="sm"
|
||||
startIcon="duplicate"
|
||||
/>
|
||||
</Tooltip>
|
||||
</TooltipWrapper>
|
||||
)}{" "}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
import { fetchGitRemoteStatusInit } from "actions/gitSyncActions";
|
||||
import { Callout, Text, toast } from "design-system";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useDispatch } from "react-redux";
|
||||
import styled from "styled-components";
|
||||
import { useSSHKeyPair } from "../hooks";
|
||||
import copy from "copy-to-clipboard";
|
||||
import {
|
||||
COPIED_SSH_KEY,
|
||||
COPY_SSH_KEY,
|
||||
ERROR_SSH_RECONNECT_MESSAGE,
|
||||
ERROR_SSH_RECONNECT_OPTION1,
|
||||
ERROR_SSH_RECONNECT_OPTION2,
|
||||
NO_COPIED_SSH_KEY,
|
||||
createMessage,
|
||||
} from "@appsmith/constants/messages";
|
||||
|
||||
const NumberedList = styled.ol`
|
||||
list-style-type: decimal;
|
||||
margin-left: 20px;
|
||||
`;
|
||||
|
||||
const StyledCallout = styled(Callout)`
|
||||
margin-bottom: 24px;
|
||||
`;
|
||||
|
||||
function ReconnectSSHError() {
|
||||
const [errorData, setErrorData] = useState<{ error: Error; response: any }>();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const { fetchingSSHKeyPair, fetchSSHKeyPair, SSHKeyPair } = useSSHKeyPair();
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(
|
||||
fetchGitRemoteStatusInit({
|
||||
onErrorCallback: (error, response) => {
|
||||
setErrorData({ error, response });
|
||||
},
|
||||
}),
|
||||
);
|
||||
fetchSSHKeyPair();
|
||||
}, []);
|
||||
|
||||
const handleClickOnCopy = () => {
|
||||
if (SSHKeyPair) {
|
||||
copy(SSHKeyPair);
|
||||
toast.show(createMessage(COPIED_SSH_KEY), { kind: "success" });
|
||||
} else {
|
||||
toast.show(createMessage(NO_COPIED_SSH_KEY), { kind: "error" });
|
||||
}
|
||||
};
|
||||
|
||||
if (!errorData) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (
|
||||
errorData &&
|
||||
errorData.response.responseMeta.error.code === "AE-GIT-4044"
|
||||
) {
|
||||
return (
|
||||
<StyledCallout
|
||||
kind="error"
|
||||
links={[
|
||||
{
|
||||
children: createMessage(COPY_SSH_KEY),
|
||||
onClick: handleClickOnCopy,
|
||||
startIcon: "copy-control",
|
||||
isDisabled: fetchingSSHKeyPair,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Text renderAs="p">{createMessage(ERROR_SSH_RECONNECT_MESSAGE)}</Text>
|
||||
<NumberedList>
|
||||
<li>{createMessage(ERROR_SSH_RECONNECT_OPTION1)}</li>
|
||||
<li>{createMessage(ERROR_SSH_RECONNECT_OPTION2)}</li>
|
||||
</NumberedList>
|
||||
</StyledCallout>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledCallout kind="error">
|
||||
{errorData.error?.message || "There was an unexpected error"}
|
||||
</StyledCallout>
|
||||
);
|
||||
}
|
||||
|
||||
export default ReconnectSSHError;
|
||||
|
|
@ -1,32 +1,3 @@
|
|||
import {
|
||||
createMessage,
|
||||
GIT_CONNECTION,
|
||||
DEPLOY,
|
||||
MERGE,
|
||||
CONNECT_TO_GIT,
|
||||
DEPLOY_YOUR_APPLICATION,
|
||||
MERGE_CHANGES,
|
||||
} from "@appsmith/constants/messages";
|
||||
import { GitSyncModalTab } from "entities/GitSync";
|
||||
|
||||
export const MENU_ITEMS_MAP = {
|
||||
[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),
|
||||
},
|
||||
};
|
||||
|
||||
export enum AUTH_TYPE {
|
||||
SSH = "SSH",
|
||||
HTTPS = "HTTPS",
|
||||
|
|
|
|||
|
|
@ -2,38 +2,43 @@ import { useDispatch } from "react-redux";
|
|||
import { useCallback, useState } from "react";
|
||||
import type { ConnectToGitPayload } from "api/GitSyncAPI";
|
||||
import { connectToGitInit } from "actions/gitSyncActions";
|
||||
import noop from "lodash/noop";
|
||||
|
||||
export const useGitConnect = () => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const [errResponse, setErrResponse] = useState();
|
||||
const [isConnectingToGit, setIsConnectingToGit] = useState(false);
|
||||
|
||||
const onGitConnectSuccess = useCallback(() => {
|
||||
setIsConnectingToGit(false);
|
||||
}, [setIsConnectingToGit]);
|
||||
|
||||
const onGitConnectFailure = useCallback(() => {
|
||||
setIsConnectingToGit(false);
|
||||
}, [setIsConnectingToGit]);
|
||||
|
||||
const connectToGit = useCallback(
|
||||
(payload: ConnectToGitPayload) => {
|
||||
(
|
||||
payload: ConnectToGitPayload,
|
||||
{ onErrorCallback = noop, onSuccessCallback = noop } = {},
|
||||
) => {
|
||||
setIsConnectingToGit(true);
|
||||
setErrResponse(undefined);
|
||||
// Here after the ssh key pair generation, we fetch the application data again and on success of it
|
||||
dispatch(
|
||||
connectToGitInit({
|
||||
payload,
|
||||
onSuccessCallback: onGitConnectSuccess,
|
||||
onErrorCallback: onGitConnectFailure,
|
||||
onSuccessCallback: (data) => {
|
||||
onSuccessCallback(data);
|
||||
setIsConnectingToGit(false);
|
||||
},
|
||||
onErrorCallback: (err, response) => {
|
||||
onErrorCallback(err, response);
|
||||
setErrResponse(response);
|
||||
setIsConnectingToGit(false);
|
||||
},
|
||||
}),
|
||||
);
|
||||
},
|
||||
[onGitConnectSuccess, onGitConnectFailure, setIsConnectingToGit],
|
||||
[setIsConnectingToGit],
|
||||
);
|
||||
|
||||
return {
|
||||
isConnectingToGit,
|
||||
connectToGit,
|
||||
onGitConnectFailure,
|
||||
connectErrorResponse: errResponse,
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -3,8 +3,9 @@ import {
|
|||
getSSHKeyDeployDocUrl,
|
||||
getSshKeyPair,
|
||||
} from "selectors/gitSyncSelectors";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useCallback, useState } from "react";
|
||||
import { generateSSHKeyPair, getSSHKeyPair } from "actions/gitSyncActions";
|
||||
import noop from "lodash/noop";
|
||||
|
||||
export const useSSHKeyPair = () => {
|
||||
// As SSHKeyPair fetching and generation is only done only for GitConnection part,
|
||||
|
|
@ -21,44 +22,50 @@ export const useSSHKeyPair = () => {
|
|||
|
||||
const [failedGeneratingSSHKey, setFailedGeneratingSSHKey] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// on change of sshKeyPair if it is defined, then stop the loading state.
|
||||
if (SSHKeyPair) {
|
||||
if (generatingSSHKey) setIsGeneratingSSHKey(false);
|
||||
if (fetchingSSHKeyPair) setIsFetchingSSHKeyPair(false);
|
||||
}
|
||||
}, [SSHKeyPair]);
|
||||
|
||||
const fetchSSHKeyPair = useCallback(() => {
|
||||
setIsFetchingSSHKeyPair(true);
|
||||
dispatch(
|
||||
getSSHKeyPair({
|
||||
onErrorCallback: () => {
|
||||
setIsFetchingSSHKeyPair(false);
|
||||
},
|
||||
}),
|
||||
);
|
||||
}, [setIsFetchingSSHKeyPair]);
|
||||
|
||||
const onGenerateSSHKeyFailure = useCallback(() => {
|
||||
setIsGeneratingSSHKey(false);
|
||||
setFailedGeneratingSSHKey(true);
|
||||
}, [setIsGeneratingSSHKey]);
|
||||
const fetchSSHKeyPair = useCallback(
|
||||
({ onSuccessCallback = noop, onErrorCallback = noop } = {}) => {
|
||||
setIsFetchingSSHKeyPair(true);
|
||||
dispatch(
|
||||
getSSHKeyPair({
|
||||
onErrorCallback: (e) => {
|
||||
onErrorCallback(e);
|
||||
setIsFetchingSSHKeyPair(false);
|
||||
},
|
||||
onSuccessCallback: (data) => {
|
||||
onSuccessCallback(data);
|
||||
setIsFetchingSSHKeyPair(false);
|
||||
},
|
||||
}),
|
||||
);
|
||||
},
|
||||
[setIsFetchingSSHKeyPair],
|
||||
);
|
||||
|
||||
const generateSSHKey = useCallback(
|
||||
(keyType = "ECDSA") => {
|
||||
(
|
||||
keyType = "ECDSA",
|
||||
{ onSuccessCallback = noop, onErrorCallback = noop } = {},
|
||||
) => {
|
||||
// if (currentApplication?.id) {
|
||||
setIsGeneratingSSHKey(true);
|
||||
setFailedGeneratingSSHKey(false);
|
||||
|
||||
dispatch(
|
||||
generateSSHKeyPair({
|
||||
onErrorCallback: onGenerateSSHKeyFailure,
|
||||
onErrorCallback: (e) => {
|
||||
onErrorCallback(e);
|
||||
setIsGeneratingSSHKey(false);
|
||||
setFailedGeneratingSSHKey(true);
|
||||
},
|
||||
onSuccessCallback: (data) => {
|
||||
onSuccessCallback(data);
|
||||
setIsGeneratingSSHKey(false);
|
||||
},
|
||||
payload: { keyType },
|
||||
}),
|
||||
);
|
||||
},
|
||||
[onGenerateSSHKeyFailure, setIsGeneratingSSHKey],
|
||||
[setIsGeneratingSSHKey],
|
||||
);
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ const initialState: GitSyncReducerState = {
|
|||
localGitConfig: { authorEmail: "", authorName: "" },
|
||||
|
||||
isFetchingLocalGitConfig: false,
|
||||
isFetchingGitConfig: false,
|
||||
isFetchingGlobalGitConfig: false,
|
||||
|
||||
isMerging: false,
|
||||
tempRemoteUrl: "",
|
||||
|
|
@ -124,7 +124,7 @@ const gitSyncReducer = createReducer(initialState, {
|
|||
state: GitSyncReducerState,
|
||||
) => ({
|
||||
...state,
|
||||
isFetchingGitConfig: true,
|
||||
isFetchingGlobalGitConfig: true,
|
||||
connectError: null,
|
||||
commitAndPushError: null,
|
||||
pullError: null,
|
||||
|
|
@ -135,7 +135,7 @@ const gitSyncReducer = createReducer(initialState, {
|
|||
state: GitSyncReducerState,
|
||||
) => ({
|
||||
...state,
|
||||
isFetchingGitConfig: true,
|
||||
isFetchingGlobalGitConfig: true,
|
||||
connectError: null,
|
||||
commitAndPushError: null,
|
||||
pullError: null,
|
||||
|
|
@ -148,7 +148,7 @@ const gitSyncReducer = createReducer(initialState, {
|
|||
) => ({
|
||||
...state,
|
||||
globalGitConfig: action.payload,
|
||||
isFetchingGitConfig: false,
|
||||
isFetchingGlobalGitConfig: false,
|
||||
}),
|
||||
[ReduxActionTypes.UPDATE_GLOBAL_GIT_CONFIG_SUCCESS]: (
|
||||
state: GitSyncReducerState,
|
||||
|
|
@ -156,19 +156,19 @@ const gitSyncReducer = createReducer(initialState, {
|
|||
) => ({
|
||||
...state,
|
||||
globalGitConfig: action.payload,
|
||||
isFetchingGitConfig: false,
|
||||
isFetchingGlobalGitConfig: false,
|
||||
}),
|
||||
[ReduxActionErrorTypes.UPDATE_GLOBAL_GIT_CONFIG_ERROR]: (
|
||||
state: GitSyncReducerState,
|
||||
) => ({
|
||||
...state,
|
||||
isFetchingGitConfig: false,
|
||||
isFetchingGlobalGitConfig: false,
|
||||
}),
|
||||
[ReduxActionErrorTypes.FETCH_GLOBAL_GIT_CONFIG_ERROR]: (
|
||||
state: GitSyncReducerState,
|
||||
) => ({
|
||||
...state,
|
||||
isFetchingGitConfig: false,
|
||||
isFetchingGlobalGitConfig: false,
|
||||
}),
|
||||
[ReduxActionTypes.FETCH_BRANCHES_INIT]: (state: GitSyncReducerState) => ({
|
||||
...state,
|
||||
|
|
@ -644,7 +644,7 @@ export type GitSyncReducerState = GitBranchDeleteState & {
|
|||
isCommitSuccessful: boolean;
|
||||
|
||||
fetchingBranches: boolean;
|
||||
isFetchingGitConfig: boolean;
|
||||
isFetchingGlobalGitConfig: boolean;
|
||||
isFetchingLocalGitConfig: boolean;
|
||||
|
||||
isFetchingGitStatus: boolean;
|
||||
|
|
|
|||
|
|
@ -234,7 +234,7 @@ function* connectToGitSaga(action: ConnectToGitReduxAction) {
|
|||
}
|
||||
} catch (error) {
|
||||
if (action.onErrorCallback) {
|
||||
action.onErrorCallback(error as string);
|
||||
action.onErrorCallback(error as Error, response);
|
||||
}
|
||||
|
||||
const isRepoLimitReachedError: boolean = yield call(
|
||||
|
|
@ -571,7 +571,12 @@ function* fetchGitStatusSaga(action: ReduxAction<GitStatusParams>) {
|
|||
}
|
||||
}
|
||||
|
||||
function* fetchGitRemoteStatusSaga() {
|
||||
interface FetchRemoteStatusSagaAction extends ReduxAction<undefined> {
|
||||
onSuccessCallback?: (data: any) => void;
|
||||
onErrorCallback?: (error: Error, response?: any) => void;
|
||||
}
|
||||
|
||||
function* fetchGitRemoteStatusSaga(action: FetchRemoteStatusSagaAction) {
|
||||
let response: ApiResponse | undefined;
|
||||
try {
|
||||
const applicationId: string = yield select(getCurrentApplicationId);
|
||||
|
|
@ -591,8 +596,11 @@ function* fetchGitRemoteStatusSaga() {
|
|||
// @ts-expect-error: response is of type unknown
|
||||
yield put(fetchGitRemoteStatusSuccess(response?.data));
|
||||
}
|
||||
if (typeof action?.onSuccessCallback === "function") {
|
||||
action.onSuccessCallback(response?.data);
|
||||
}
|
||||
} catch (error) {
|
||||
const payload = { error, show: true };
|
||||
const payload = { error, show: !action?.onErrorCallback };
|
||||
if ((error as Error)?.message?.includes("Auth fail")) {
|
||||
payload.error = new Error(createMessage(ERROR_GIT_AUTH_FAIL));
|
||||
} else if ((error as Error)?.message?.includes("Invalid remote: origin")) {
|
||||
|
|
@ -604,6 +612,10 @@ function* fetchGitRemoteStatusSaga() {
|
|||
payload,
|
||||
});
|
||||
|
||||
if (typeof action?.onErrorCallback === "function") {
|
||||
action.onErrorCallback(error as Error, response);
|
||||
}
|
||||
|
||||
// non api error
|
||||
if (!response || response?.responseMeta?.success) {
|
||||
throw error;
|
||||
|
|
@ -861,7 +873,7 @@ function* importAppFromGitSaga(action: ConnectToGitReduxAction) {
|
|||
}
|
||||
} catch (error) {
|
||||
if (action.onErrorCallback) {
|
||||
action.onErrorCallback(error as string);
|
||||
action.onErrorCallback(error as Error, response);
|
||||
}
|
||||
|
||||
const isRepoLimitReachedError: boolean = yield call(
|
||||
|
|
|
|||
|
|
@ -56,16 +56,11 @@ export const getIsGlobalConfigDefined = createSelector(
|
|||
);
|
||||
|
||||
export const getIsFetchingGlobalGitConfig = (state: AppState) =>
|
||||
state.ui.gitSync.isFetchingGitConfig;
|
||||
state.ui.gitSync.isFetchingGlobalGitConfig;
|
||||
|
||||
export const getIsFetchingLocalGitConfig = (state: AppState) =>
|
||||
state.ui.gitSync.isFetchingLocalGitConfig;
|
||||
|
||||
export const getIsGitStatusLiteEnabled = createSelector(
|
||||
selectFeatureFlags,
|
||||
(flags) => !!flags?.release_git_status_lite_enabled,
|
||||
);
|
||||
|
||||
export const getGitStatus = (state: AppState) => state.ui.gitSync.gitStatus;
|
||||
|
||||
export const getGitRemoteStatus = (state: AppState) =>
|
||||
|
|
@ -208,3 +203,14 @@ export const getBranchSwitchingDetails = (state: AppState) => ({
|
|||
isSwitchingBranch: state.ui.gitSync.isSwitchingBranch,
|
||||
switchingToBranch: state.ui.gitSync.switchingToBranch,
|
||||
});
|
||||
|
||||
// feature flag selectors
|
||||
export const getIsGitStatusLiteEnabled = createSelector(
|
||||
selectFeatureFlags,
|
||||
(flags) => !!flags?.release_git_status_lite_enabled,
|
||||
);
|
||||
|
||||
export const getIsGitConnectV2Enabled = createSelector(
|
||||
selectFeatureFlags,
|
||||
(flags) => !!flags?.release_git_connect_v2_enabled,
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user