Merge branch 'release' into fix/10319-select-api-response

This commit is contained in:
Arsalan 2022-04-04 18:06:17 +05:30
commit 01539dd52d
26 changed files with 1706 additions and 154 deletions

View File

@ -530,13 +530,13 @@ jobs:
path: app/rts/node_modules/
- name: Build docker image
if: success() && github.ref == 'refs/heads/release' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch')
if: steps.run_result.outputs.run_result != 'success'
working-directory: "."
run: |
docker build -t fatcontainer .
- name: Load docker image
if: success() && github.ref == 'refs/heads/release' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch')
if: steps.run_result.outputs.run_result != 'success'
env:
APPSMITH_LICENSE_KEY: ${{ secrets.APPSMITH_LICENSE_KEY }}
working-directory: "."

File diff suppressed because it is too large Load Diff

View File

@ -94,19 +94,28 @@ describe("Slug URLs", () => {
);
});
cy.get(".t--upgrade").click({ force: true });
cy.get(".t--upgrade-confirm").click({ force: true });
cy.wait("@getPagesForCreateApp").then((intercept) => {
const { application, pages } = intercept.response.body.data;
const defaultPage = pages.find((p) => p.isDefault);
cy.Createpage("NewPage");
cy.get("@currentPageId").then((currentPageId) => {
cy.location().should((loc) => {
expect(loc.pathname).includes(
`/${application.slug}/${defaultPage.slug}-${defaultPage.id}`,
`/applications/${application.id}/pages/${currentPageId}`,
);
});
cy.get(".t--upgrade").click({ force: true });
cy.get(".t--upgrade-confirm").click({ force: true });
cy.wait("@getPagesForCreateApp").then((intercept) => {
const { application, pages } = intercept.response.body.data;
const currentPage = pages.find((p) => p.id === currentPageId);
cy.location().should((loc) => {
expect(loc.pathname).includes(
`/${application.slug}/${currentPage.slug}-${currentPage.id}`,
);
});
});
});
});
});

View File

@ -0,0 +1,126 @@
import gitSyncLocators from "../../../../locators/gitSyncLocators";
const homePage = require("../../../../locators/HomePage");
const reconnectDatasourceModal = require("../../../../locators/ReconnectLocators");
let repoName;
let appName;
describe("Git import flow", function() {
before(() => {
cy.NavigateToHome();
cy.createOrg();
cy.wait("@createOrg").then((interception) => {
const newOrganizationName = interception.response.body.data.name;
cy.CreateAppForOrg(newOrganizationName, newOrganizationName);
});
});
it("Import an app from JSON with Postgres, MySQL, Mongo db", () => {
cy.get(homePage.homeIcon).click();
cy.get(homePage.optionsIcon)
.first()
.click();
cy.get(homePage.orgImportAppOption).click({ force: true });
cy.get(homePage.orgImportAppModal).should("be.visible");
cy.xpath(homePage.uploadLogo).attachFile("gitImport.json");
cy.wait("@importNewApplication").then((interception) => {
cy.wait(100);
// should check reconnect modal opening
// const { isPartialImport } = interception.response.body.data;
// if (isPartialImport) {
// should reconnect button
cy.get(reconnectDatasourceModal.Modal).should("be.visible");
cy.ReconnectDatasource("TEDPostgres");
cy.wait(1000);
cy.fillPostgresDatasourceForm();
cy.testSaveDatasource();
cy.wait(2000);
// commenting until bug12535 is closed
/* cy.ReconnectDatasource("TEDMySQL");
cy.wait(2000);
cy.fillMySQLDatasourceForm();
cy.testSaveDatasource();
cy.wait(2000);
cy.ReconnectDatasource("TEDMongo");
cy.wait(2000);
cy.fillMongoDatasourceForm();
cy.testSaveDatasource();
cy.wait(2000);
// } else {
cy.get(homePage.toastMessage).should(
"contain",
"Application imported successfully",
); */
cy.get(reconnectDatasourceModal.SkipToAppBtn).click({ force: true });
cy.wait(2000);
cy.get(".tbody")
.first()
.should("contain.text", "Test user 7");
cy.generateUUID().then((uid) => {
repoName = uid;
cy.createTestGithubRepo(repoName);
cy.connectToGitRepo(repoName);
});
});
});
it("Import an app from Git and reconnect Postgres, MySQL and Mongo db ", () => {
cy.NavigateToHome();
cy.createOrg();
cy.wait("@createOrg").then((interception) => {
const newOrganizationName = interception.response.body.data.name;
cy.CreateAppForOrg(newOrganizationName, "gitImport");
});
cy.get(homePage.homeIcon).click();
cy.get(homePage.optionsIcon)
.first()
.click();
cy.get(homePage.orgImportAppOption).click({ force: true });
cy.get(".t--import-json-card")
.next()
.click();
cy.importAppFromGit(repoName);
// cy.wait("@importNewApplication").then((interception) => {
cy.wait(100);
// should check reconnect modal opening
// const { isPartialImport } = interception.response.body.data;
// if (isPartialImport) {
// should reconnect button
cy.get(reconnectDatasourceModal.Modal).should("be.visible");
cy.ReconnectDatasource("TEDPostgres");
cy.wait(1000);
cy.fillPostgresDatasourceForm();
cy.testSaveDatasource();
cy.wait(1000);
/* cy.ReconnectDatasource("TEDMySQL");
cy.wait(1000);
cy.fillMySQLDatasourceForm();
cy.testSaveDatasource();
cy.wait(1000);
cy.ReconnectDatasource("TEDMongo");
cy.wait(1000);
cy.fillMongoDatasourceForm();
cy.testSaveDatasource();
cy.wait(2000);
} else {
cy.get(homePage.toastMessage).should(
"contain",
"Application imported successfully",
);
} */
cy.get(reconnectDatasourceModal.SkipToAppBtn).click({ force: true });
});
it("Verfiy imported app should have all the data binding visible", () => {
// verify postgres data binded to table
cy.get(".tbody")
.first()
.should("contain.text", "Test user 7");
// verify MySQL data binded to table
// cy.get(".tbody").last().should("contain.text", "New Config")
// verify api response binded to input widget
cy.xpath("//input[@value='this is a test']");
// verify js object binded to input widget
cy.xpath("//input[@value='Success']");
});
after(() => {
cy.deleteTestGithubRepo(repoName);
});
});

View File

@ -1908,6 +1908,7 @@ Cypress.Commands.add("Createpage", (pageName) => {
cy.get(pages.editName).click({ force: true });
cy.get(pages.editInput).type(pageName + "{enter}");
pageidcopy = pageName;
cy.wrap(pageId).as("currentPageId");
}
cy.get(generatePage.buildFromScratchActionCard).click();
cy.get("#loading").should("not.exist");
@ -2341,6 +2342,7 @@ Cypress.Commands.add(
cy.get(datasourceEditor["databaseName"])
.clear()
.type(datasourceFormData["mongo-databaseName"]);
cy.get(datasourceEditor.sectionAuthentication).click();
// cy.get(datasourceEditor["username"]).type(
// datasourceFormData["mongo-username"],
// );
@ -2374,7 +2376,6 @@ Cypress.Commands.add(
cy.get(datasourceEditor.databaseName)
.clear()
.type(databaseName);
cy.get(datasourceEditor.sectionAuthentication).click();
cy.get(datasourceEditor.username).type(
datasourceFormData["postgres-username"],
@ -2382,6 +2383,7 @@ Cypress.Commands.add(
cy.get(datasourceEditor.password).type(
datasourceFormData["postgres-password"],
);
cy.get(datasourceEditor.sectionAuthentication).click();
},
);
@ -2408,6 +2410,7 @@ Cypress.Commands.add(
cy.get(datasourceEditor.password).type(
datasourceFormData["mysql-password"],
);
cy.get(datasourceEditor.sectionAuthentication).click();
},
);
@ -2531,6 +2534,7 @@ Cypress.Commands.add(
: datasourceFormData["smtp-host"];
cy.get(datasourceEditor.host).type(hostAddress);
cy.get(datasourceEditor.port).type(datasourceFormData["smtp-port"]);
cy.get(datasourceEditor.sectionAuthentication).click();
cy.get(datasourceEditor.username).type(datasourceFormData["smtp-username"]);
cy.get(datasourceEditor.password).type(datasourceFormData["smtp-password"]);
@ -3067,7 +3071,7 @@ Cypress.Commands.add("startServerAndRoutes", () => {
cy.route("POST", "api/v1/git/connect/*").as("connectGitRepo");
cy.route("POST", "api/v1/git/commit/*").as("commit");
cy.route("POST", "/api/v1/git/import/*").as("importFromGit");
cy.route("PUT", "api/v1/collections/actions/refactor").as("renameJsAction");
cy.route("POST", "/api/v1/collections/actions").as("createNewJSCollection");
@ -3811,6 +3815,78 @@ Cypress.Commands.add(
},
);
Cypress.Commands.add(
"importAppFromGit",
(repo, shouldCommit = true, assertConnectFailure) => {
const testEmail = "test@test.com";
const testUsername = "testusername";
const owner = Cypress.env("TEST_GITHUB_USER_NAME");
let generatedKey;
cy.intercept(
{
url: "api/v1/git/connect/*",
hostname: window.location.host,
},
(req) => {
req.headers["origin"] = "Cypress";
},
);
cy.intercept("GET", "api/v1/git/import/keys").as(`generateKey-${repo}`);
cy.get(gitSyncLocators.gitRepoInput).type(
`git@github.com:${owner}/${repo}.git`,
);
cy.get(gitSyncLocators.generateDeployKeyBtn).click();
cy.wait(`@generateKey-${repo}`).then((result) => {
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: `${GITHUB_API_BASE}/repos/${Cypress.env(
"TEST_GITHUB_USER_NAME",
)}/${repo}/keys`,
headers: {
Authorization: `token ${Cypress.env("GITHUB_PERSONAL_ACCESS_TOKEN")}`,
},
body: {
title: "key0",
key: generatedKey,
},
});
cy.get(gitSyncLocators.useGlobalGitConfig).click();
cy.get(gitSyncLocators.gitConfigNameInput).type(
`{selectall}${testUsername}`,
);
cy.get(gitSyncLocators.gitConfigEmailInput).type(
`{selectall}${testEmail}`,
);
// click on the connect button and verify
cy.get(gitSyncLocators.connectSubmitBtn).click();
if (!assertConnectFailure) {
// check for connect success
cy.wait("@importFromGit").should(
"have.nested.property",
"response.body.responseMeta.status",
201,
);
} else {
cy.wait("@importFromGit").then((interception) => {
const status = interception.response.body.responseMeta.status;
expect(status).to.be.gte(400);
});
}
});
},
);
Cypress.Commands.add("ReconnectDatasource", (datasource) => {
cy.xpath(`//span[text()='${datasource}']`).click();
});
Cypress.Commands.add("clearPropertyValue", (value) => {
cy.get(".CodeMirror textarea")
.eq(value)

View File

@ -80,6 +80,8 @@ export function updateURLFactory(params: Optional<BaseURLBuilderParams>) {
BASE_URL_BUILDER_PARAMS = { ...BASE_URL_BUILDER_PARAMS, ...params };
}
export const getRouteBuilderParams = () => BASE_URL_BUILDER_PARAMS;
/**
* Do not export this method directly. Please write wrappers for your URLs.
* Uses applicationVersion attribute to determine whether to use slug URLs or legacy URLs.

View File

@ -0,0 +1,5 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.8 20.2V3.8H12H15.1686L17.4343 6.06569L19.2 7.83137V12V20.2H4.8Z" fill="#ffffff" stroke="#575757" stroke-width="1.6"/>
<path d="M7.25391 16.0625C7.25391 17.3672 8.10938 18.1562 9.47656 18.1562C10.8438 18.1562 11.6641 17.4297 11.6641 16.1055V12.3633H10.0078V16.0938C10.0078 16.5352 9.8125 16.7695 9.44922 16.7695C9.07422 16.7695 8.83984 16.5039 8.83984 16.0625H7.25391Z" fill="#575757"/>
<path d="M12.7227 16.3555C12.7305 17.4727 13.6484 18.1562 15.1445 18.1562C16.6836 18.1562 17.6055 17.4297 17.6055 16.2109C17.6055 15.2969 17.0977 14.7773 16 14.5859L15.2305 14.4531C14.6914 14.3594 14.4609 14.2031 14.4609 13.9336C14.4609 13.6211 14.7422 13.4258 15.1719 13.4258C15.6133 13.4258 15.9609 13.6836 15.9727 14.0234H17.4883C17.4766 12.9414 16.5273 12.207 15.1367 12.207C13.7266 12.207 12.8164 12.9492 12.8164 14.0977C12.8164 15.0078 13.3789 15.6016 14.4141 15.7812L15.1602 15.9141C15.7656 16.0234 15.9766 16.1484 15.9766 16.4102C15.9766 16.7266 15.668 16.9336 15.2031 16.9336C14.6719 16.9336 14.3125 16.707 14.293 16.3555H12.7227Z" fill="#575757"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -1,6 +1,9 @@
import {
CANNOT_MERGE_DUE_TO_UNCOMMITTED_CHANGES,
CANNOT_PULL_WITH_LOCAL_UNCOMMITTED_CHANGES,
CHANGES_ONLY_MIGRATION,
CHANGES_ONLY_USER,
CHANGES_USER_AND_MIGRATION,
COMMIT_AND_PUSH,
COMMIT_CHANGES,
COMMIT_TO,
@ -256,10 +259,25 @@ describe("git-sync messages", () => {
key: "ERROR_GIT_INVALID_REMOTE",
value: "Remote repo doesn't exist or is unreachable.",
},
{
key: "CHANGES_ONLY_USER",
value: "Changes since last commit",
},
{
key: "CHANGES_ONLY_MIGRATION",
value: "Appsmith update changes since last commit",
},
{
key: "CHANGES_USER_AND_MIGRATION",
value: "Appsmith update and user changes since last commit",
},
];
const functions = [
CANNOT_MERGE_DUE_TO_UNCOMMITTED_CHANGES,
CANNOT_PULL_WITH_LOCAL_UNCOMMITTED_CHANGES,
CHANGES_ONLY_MIGRATION,
CHANGES_ONLY_USER,
CHANGES_USER_AND_MIGRATION,
COMMITTING_AND_PUSHING_CHANGES,
COMMIT_AND_PUSH,
COMMIT_CHANGES,

View File

@ -588,7 +588,6 @@ export const GIT_DISCONNECT_POPUP_MAIN_HEADING = () => `Are you sure ?`;
export const GIT_CONNECTION = () => "Git Connection";
export const GIT_IMPORT = () => "Git Import";
export const DEPLOY = () => "Deploy";
export const MERGE = () => "Merge";
export const GIT_SETTINGS = () => "Git Settings";
export const CONNECT_TO_GIT = () => "Connect to git repository";
@ -618,7 +617,6 @@ export const CHECK_DP = () => "CHECK";
export const DEPLOY_TO_CLOUD = () => "Deploy to cloud";
export const DEPLOY_WITHOUT_GIT = () =>
"Deploy your application without version control";
export const DEPLOY_YOUR_APPLICATION = () => "Deploy your application";
export const COMMIT_CHANGES = () => "Commit changes";
export const COMMIT_TO = () => "Commit to";
export const COMMIT_AND_PUSH = () => "Commit & push";
@ -735,6 +733,16 @@ export const CONNECTING_TO_REPO_DISABLED = () =>
export const DURING_ONBOARDING_TOUR = () => "during the onboarding tour";
export const MERGED_SUCCESSFULLY = () => "Merged successfully";
// GIT DEPLOY begin
export const DEPLOY = () => "Deploy";
export const DEPLOY_YOUR_APPLICATION = () => "Deploy your application";
export const CHANGES_ONLY_USER = () => "Changes since last commit";
export const CHANGES_ONLY_MIGRATION = () =>
"Appsmith update changes since last commit";
export const CHANGES_USER_AND_MIGRATION = () =>
"Appsmith update and user changes since last commit";
// GIT DEPLOY end
// GIT ERRORS begin
export const ERROR_GIT_AUTH_FAIL = () =>
"Please make sure that regenerated SSH key is added and has write access to the repo.";
@ -1149,4 +1157,12 @@ export const CLEAN_URL_UPDATE = {
name: () => "Update URLs",
shortDesc: () =>
"All URLs in your applications will update to a new readable format that includes the application and page names.",
description: [
() =>
"All URLs in your applications will be updated to match our new style. This will make your apps easier to find, and URLs easier to remember.",
(url: string) =>
`The current apps URL will be:<br /><code style="line-break: anywhere; padding: 2px 4px; line-height: 22px">${url}</code>`,
],
disclaimer: () =>
"Existing references to <strong>appsmith.URL.fullpath</strong> and <strong>appsmith.URL.pathname</strong> properties will behave differently.",
};

View File

@ -70,9 +70,10 @@ import { ReactComponent as WorkspaceIcon } from "assets/icons/ads/organizationIc
import { ReactComponent as SettingIcon } from "assets/icons/control/settings.svg";
import { ReactComponent as DropdownIcon } from "assets/icons/ads/dropdown.svg";
import { ReactComponent as ChatIcon } from "assets/icons/ads/app-icons/chat.svg";
import { ReactComponent as JsIcon } from "assets/icons/ads/js.svg";
import styled from "styled-components";
import { CommonComponentProps, Classes } from "./common";
import { Classes, CommonComponentProps } from "./common";
import { noop } from "lodash";
import { theme } from "constants/DefaultTheme";
import Spinner from "./Spinner";
@ -90,6 +91,7 @@ import CheckLineIcon from "remixicon-react/CheckLineIcon";
import CloseLineIcon from "remixicon-react/CloseLineIcon";
import CloseCircleIcon from "remixicon-react/CloseCircleFillIcon";
import CommentContextMenu from "remixicon-react/More2FillIcon";
import More2FillIcon from "remixicon-react/More2FillIcon";
import CompassesLine from "remixicon-react/CompassesLineIcon";
import ContextMenuIcon from "remixicon-react/MoreFillIcon";
import CreateNewIcon from "remixicon-react/AddLineIcon";
@ -102,8 +104,10 @@ import Download from "remixicon-react/DownloadCloud2LineIcon";
import DuplicateIcon from "remixicon-react/FileCopyLineIcon";
import EditIcon from "remixicon-react/PencilFillIcon";
import EditLineIcon from "remixicon-react/EditLineIcon";
import EditUnderlineIcon from "remixicon-react/EditLineIcon";
import Emoji from "remixicon-react/EmotionLineIcon";
import ExpandMore from "remixicon-react/ArrowDownSLineIcon";
import DownArrowIcon from "remixicon-react/ArrowDownSLineIcon";
import ExpandLess from "remixicon-react/ArrowUpSLineIcon";
import EyeOn from "remixicon-react/EyeLineIcon";
import EyeOff from "remixicon-react/EyeOffLineIcon";
@ -122,7 +126,6 @@ import KeyIcon from "remixicon-react/Key2LineIcon";
import LeftArrowIcon2 from "remixicon-react/ArrowLeftSLineIcon";
import Link2 from "remixicon-react/LinkIcon";
import LeftArrowIcon from "remixicon-react/ArrowLeftLineIcon";
import More2FillIcon from "remixicon-react/More2FillIcon";
import NewsPaperLine from "remixicon-react/NewspaperLineIcon";
import OvalCheck from "remixicon-react/CheckboxCircleLineIcon";
import OvalCheckFill from "remixicon-react/CheckboxCircleFillIcon";
@ -137,10 +140,8 @@ import Trash from "remixicon-react/DeleteBinLineIcon";
import UpArrow from "remixicon-react/ArrowUpSFillIcon";
import WarningIcon from "remixicon-react/ErrorWarningFillIcon";
import WarningLineIcon from "remixicon-react/ErrorWarningLineIcon";
import EditUnderlineIcon from "remixicon-react/EditLineIcon";
import LogoutIcon from "remixicon-react/LogoutBoxRLineIcon";
import ShareLineIcon from "remixicon-react/ShareLineIcon";
import DownArrowIcon from "remixicon-react/ArrowDownSLineIcon";
import LoaderLineIcon from "remixicon-react/LoaderLineIcon";
import WidgetIcon from "remixicon-react/FunctionLineIcon";
import RefreshLineIcon from "remixicon-react/RefreshLineIcon";
@ -344,6 +345,7 @@ const ICON_LOOKUP = {
hamburger: <HamburgerIcon />,
help: <HelpIcon />,
info: <InfoIcon />,
js: <JsIcon />,
key: <KeyIcon />,
lightning: <LightningIcon />,
link: <LinkIcon />,

View File

@ -110,6 +110,7 @@ export const Colors = {
WARNING_SOLID: "#FEB811",
WARNING_SOLID_HOVER: "#EFA903",
WARNING_ORANGE: "#FFF8E2",
WARNING_OUTLINE_HOVER: "#FFFAE9",
WARNING_GHOST_HOVER: "#FBEED0",

View File

@ -683,8 +683,8 @@ export const ReduxActionTypes = {
SET_TEMPLATE_NOTIFICATION_SEEN: "SET_TEMPLATE_NOTIFICATION_SEEN",
GET_TEMPLATE_NOTIFICATION_SEEN: "GET_TEMPLATE_NOTIFICATION_SEEN",
GET_SIMILAR_TEMPLATES_INIT: "GET_SIMILAR_TEMPLATES_INIT",
GET_SIMILAR_TEMPLATES_SUCCESS: "GET_SIMILAR_TEMPLATES_SUCCESS",
/* This action constants is for identifying the status of the updates of the entities */
GET_SIMILAR_TEMPLATES_SUCCESS:
"GET_SIMILAR_TEMPLATES_SUCCESS" /* This action constants is for identifying the status of the updates of the entities */,
ENTITY_UPDATE_STARTED: "ENTITY_UPDATE_STARTED",
ENTITY_UPDATE_SUCCESS: "ENTITY_UPDATE_SUCCESS",
FETCH_PLUGIN_AND_JS_ACTIONS_SUCCESS: "FETCH_PLUGIN_AND_JS_ACTIONS_SUCCESS",
@ -925,6 +925,7 @@ export interface PromisePayload {
reject: any;
resolve: any;
}
export interface ReduxActionWithPromise<T> extends ReduxAction<T> {
payload: T & PromisePayload;
}
@ -989,6 +990,8 @@ export interface ApplicationPayload {
modifiedAt?: string;
pages: ApplicationPagePayload[];
applicationVersion: ApplicationVersion;
isAutoUpdate?: boolean;
isManualUpdate?: boolean;
}
export type OrganizationDetails = {

View File

@ -7,6 +7,7 @@ import {
Category,
Icon,
IconSize,
IconWrapper,
Size,
Text,
TextType,
@ -18,23 +19,15 @@ import React, { useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import {
getCurrentApplicationId,
getCurrentPageId,
selectApplicationVersion,
selectURLSlugs,
} from "selectors/editorSelectors";
import styled from "styled-components";
import { useLocalStorage } from "utils/hooks/localstorage";
import { createMessage, CLEAN_URL_UPDATE } from "@appsmith/constants/messages";
const updates = [
{
name: createMessage(CLEAN_URL_UPDATE.name),
shortDesc: createMessage(CLEAN_URL_UPDATE.shortDesc),
description: [
"All URLs in your applications will update to a new readable format that includes the application and page names.",
'Existing references to <code style="background:#ebebeb;padding:2px 5px;border-radius:2px">appsmith.URL.fullpath</code> and <code style="background:#ebebeb;padding:2px 5px;border-radius:2px">appsmith.URL.pathname</code> properties will behave differently.',
],
version: ApplicationVersion.SLUG_URL,
},
];
import { useLocation } from "react-router";
import DisclaimerIcon from "remixicon-react/ErrorWarningLineIcon";
function RedDot() {
return (
@ -51,6 +44,9 @@ const StyledList = styled.ul`
li {
font-size: 14px;
font-weight: 400;
line-height: 19px;
letter-spacing: -0.24px;
margin: 4px 0;
a {
color: rgb(248, 106, 43);
}
@ -70,16 +66,44 @@ const StyledIconContainer = styled.div`
border-radius: 50%;
`;
const DisclaimerContainer = styled.div`
padding: 8px 16px;
display: flex;
justify-content: space-between;
gap: 1rem;
align-items: center;
background: ${Colors.WARNING_ORANGE};
color: ${Colors.BROWN};
margin: 24px 0 0;
`;
const BodyContainer = styled.div`
.close-modal > svg {
height: 28px;
width: 28px;
}
`;
function UpdatesModal({
applicationVersion,
closeModal,
latestVersion,
showModal,
updates,
}: {
showModal: boolean;
closeModal: () => void;
latestVersion: ApplicationVersion;
applicationVersion: ApplicationVersion;
updates: {
name: string;
shortDesc: string;
description: string[];
version: ApplicationVersion;
disclaimer: {
desc: string;
};
}[];
}) {
const dispatch = useDispatch();
const applicationId = useSelector(getCurrentApplicationId);
@ -97,7 +121,7 @@ function UpdatesModal({
scrollContents
width={600}
>
<div className="p-6">
<BodyContainer className="p-6">
<div className="flex justify-between items-center">
<div className="flex items-center justify-start">
<StyledIconContainer>
@ -110,6 +134,7 @@ function UpdatesModal({
<Text type={TextType.H1}>Product Updates</Text>
</div>
<Icon
className="close-modal"
fillColor={Colors.SCORPION}
name="close-modal"
onClick={closeModal}
@ -126,6 +151,14 @@ function UpdatesModal({
<li dangerouslySetInnerHTML={{ __html: desc }} key={idx} />
))}
</StyledList>
<DisclaimerContainer>
<IconWrapper size={IconSize.XXXL}>
<DisclaimerIcon color={Colors.WARNING_SOLID} />
</IconWrapper>
<span
dangerouslySetInnerHTML={{ __html: update.disclaimer.desc }}
/>
</DisclaimerContainer>
</div>
))}
<div className="flex justify-end gap-2 items-center">
@ -157,7 +190,7 @@ function UpdatesModal({
text="Update"
/>
</div>
</div>
</BodyContainer>
</ModalComponent>
);
}
@ -168,6 +201,36 @@ function ManualUpgrades() {
"",
);
const applicationVersion = useSelector(selectApplicationVersion);
const applicationId = useSelector(getCurrentApplicationId);
const pageId = useSelector(getCurrentPageId);
const { applicationSlug, pageSlug } = useSelector(selectURLSlugs);
const location = useLocation();
const updates = React.useMemo(
() => [
{
name: createMessage(CLEAN_URL_UPDATE.name),
shortDesc: createMessage(CLEAN_URL_UPDATE.shortDesc),
description: CLEAN_URL_UPDATE.description.map((formatter) =>
createMessage(
formatter.bind(
null,
window.location.href.replace(
`/applications/${applicationId}/pages/${pageId}`,
`/${applicationSlug}/${pageSlug}-${pageId}`,
),
),
),
),
disclaimer: {
severity: "MODERATE",
desc: createMessage(CLEAN_URL_UPDATE.disclaimer),
},
version: ApplicationVersion.SLUG_URL,
},
],
[location, applicationSlug, pageSlug, pageId, applicationId],
);
const latestVersion = React.useMemo(
() => updates.reduce((max, u) => (max > u.version ? max : u.version), 0),
[],
@ -225,6 +288,7 @@ function ManualUpgrades() {
}}
latestVersion={latestVersion}
showModal={showModal}
updates={updates}
/>
</div>
);

View File

@ -0,0 +1,17 @@
import { render, screen } from "test/testUtils";
import BranchButton from "./BranchButton";
import React from "react";
describe("BranchButton", () => {
it("renders properly", async () => {
render(<BranchButton />);
const buttonContainer = await screen.queryByTestId(
"t--branch-button-container",
);
expect(buttonContainer).not.toBeNull();
const currentBranch = await screen.queryByTestId(
"t--branch-button-currentBranch",
);
expect(currentBranch?.innerHTML).toContain("*");
});
});

View File

@ -20,19 +20,24 @@ import AnalyticsUtil from "utils/AnalyticsUtil";
const ButtonContainer = styled.div`
display: flex;
align-items: center;
& .label {
color: ${(props) => props.theme.colors.editorBottomBar.branchBtnText};
${(props) => getTypographyByKey(props, "p1")};
line-height: 18px;
}
& .icon {
height: 24px;
}
margin: 0 ${(props) => props.theme.spaces[4]}px;
cursor: pointer;
&:hover svg path {
fill: ${Colors.CHARCOAL};
}
& .label {
width: 100px;
overflow: hidden;
@ -57,6 +62,7 @@ function BranchButton() {
return (
<Popover2
content={<BranchList setIsPopupOpen={setIsOpen} />}
data-testid={"t--git-branch-button-popover"}
hasBackdrop
isOpen={isOpen}
minimal
@ -78,11 +84,18 @@ function BranchButton() {
hoverOpenDelay={1}
position={Position.TOP_LEFT}
>
<ButtonContainer className="t--branch-button">
<ButtonContainer
className="t--branch-button"
data-testid={"t--branch-button-container"}
>
<div className="icon">
<Icon name="git-branch" size={IconSize.XXXXL} />
</div>
<div className="label" ref={labelTarget}>
<div
className="label"
data-testid={"t--branch-button-currentBranch"}
ref={labelTarget}
>
{currentBranch}
{!status?.isClean && "*"}
</div>

View File

@ -72,18 +72,19 @@ const QuickActionButtonContainer = styled.div<{ disabled?: boolean }>`
.count {
position: absolute;
width: 20px;
height: 20px;
height: ${(props) => props.theme.spaces[7]}px;
display: flex;
justify-content: center;
align-items: center;
color: ${Colors.WHITE};
background-color: ${Colors.BLACK};
top: -8px;
left: 18px;
border-radius: 50%;
top: ${(props) => -1 * props.theme.spaces[3]}px;
left: ${(props) => props.theme.spaces[8]}px;
border-radius: ${(props) => props.theme.spaces[3]}px;
${(props) => getTypographyByKey(props, "p3")};
z-index: 1;
padding: ${(props) => props.theme.spaces[1]}px
${(props) => props.theme.spaces[2]}px;
}
`;
@ -91,15 +92,6 @@ const capitalizeFirstLetter = (string = " ") => {
return string.charAt(0).toUpperCase() + string.toLowerCase().slice(1);
};
// const SpinnerContainer = styled.div`
// margin-left: ${(props) => props.theme.spaces[2]}px;
// display: flex;
// align-items: center;
// justify-content: center;
// width: 29px;
// height: 26px;
// `;
function QuickActionButton({
className = "",
count = 0,

View File

@ -1,11 +1,14 @@
import React, { useEffect, useRef, useState } from "react";
import { Title } from "../components/StyledComponents";
import { Space, Title } from "../components/StyledComponents";
import {
DEPLOY_YOUR_APPLICATION,
COMMIT_TO,
createMessage,
CHANGES_ONLY_MIGRATION,
CHANGES_ONLY_USER,
CHANGES_USER_AND_MIGRATION,
COMMIT_AND_PUSH,
COMMIT_TO,
COMMITTING_AND_PUSHING_CHANGES,
createMessage,
DEPLOY_YOUR_APPLICATION,
FETCH_GIT_STATUS,
GIT_NO_UPDATED_TOOLTIP,
GIT_UPSTREAM_CHANGES,
@ -18,18 +21,17 @@ import Button, { Size } from "components/ads/Button";
import { LabelContainer } from "components/ads/Checkbox";
import {
getConflictFoundDocUrlDeploy,
getGitCommitAndPushError,
getGitStatus,
getIsFetchingGitStatus,
getIsCommitSuccessful,
getIsCommittingInProgress,
getIsFetchingGitStatus,
getIsPullingProgress,
getPullFailed,
getGitCommitAndPushError,
getUpstreamErrorDocUrl,
getConflictFoundDocUrlDeploy,
} from "selectors/gitSyncSelectors";
import { useDispatch, useSelector } from "react-redux";
import { Space } from "../components/StyledComponents";
import { Colors } from "constants/Colors";
import { getTypographyByKey, Theme } from "constants/DefaultTheme";
@ -40,7 +42,6 @@ import {
fetchGitStatusInit,
gitPullInit,
} from "actions/gitSyncActions";
import { getIsCommitSuccessful } from "selectors/gitSyncSelectors";
import StatusLoader from "../components/StatusLoader";
import { clearCommitSuccessfulState } from "../../../../actions/gitSyncActions";
import Statusbar, {
@ -56,11 +57,15 @@ import Icon, { IconSize } from "components/ads/Icon";
import { isMac } from "utils/helpers";
import AnalyticsUtil from "utils/AnalyticsUtil";
import { getApplicationLastDeployedAt } from "selectors/editorSelectors";
import {
getApplicationLastDeployedAt,
getCurrentApplication,
} from "selectors/editorSelectors";
import GIT_ERROR_CODES from "constants/GitErrorCodes";
import useAutoGrow from "utils/hooks/useAutoGrow";
const Section = styled.div`
margin-top: ${(props) => props.theme.spaces[11]}px;
margin-bottom: ${(props) => props.theme.spaces[11]}px;
`;
@ -132,9 +137,21 @@ function Deploy() {
const currentBranch = gitMetaData?.branchName;
const dispatch = useDispatch();
const currentApplication = useSelector(getCurrentApplication);
const isAutoUpdate = currentApplication?.isAutoUpdate || false;
const isManualUpdate = currentApplication?.isManualUpdate || true;
const changeReason = isAutoUpdate
? isManualUpdate
? CHANGES_USER_AND_MIGRATION
: CHANGES_ONLY_MIGRATION
: CHANGES_ONLY_USER;
const changeReasonText = createMessage(changeReason);
const handleCommit = (doPush: boolean) => {
AnalyticsUtil.logEvent("GS_COMMIT_AND_PUSH_BUTTON_CLICK", {
source: "GIT_DEPLOY_MODAL",
isAutoUpdate,
isManualUpdate,
});
if (currentBranch) {
dispatch(
@ -197,9 +214,15 @@ function Deploy() {
const autogrowHeight = useAutoGrow(commitMessageDisplay, 37);
return (
<Container>
<Container data-testid={"t--deploy-tab-container"}>
<Title>{createMessage(DEPLOY_YOUR_APPLICATION)}</Title>
<Section>
<Text
data-testid={"t--git-deploy-change-reason-text"}
type={TextType.P1}
>
{changeReasonText}
</Text>
<GitChanged />
<Row>
<SectionTitle>

View File

@ -3,97 +3,132 @@ import styled from "constants/DefaultTheme";
import { Classes } from "components/ads/common";
import Text, { TextType } from "components/ads/Text";
import { Colors } from "constants/Colors";
import Icon, { IconName, IconSize } from "components/ads/Icon";
import Icon, { IconSize } from "components/ads/Icon";
import { useSelector } from "react-redux";
import {
getGitStatus,
getIsFetchingGitStatus,
} from "selectors/gitSyncSelectors";
import { GitStatusData } from "../../../../reducers/uiReducers/gitSyncReducer";
const Skeleton = styled.div`
width: 135px;
const DummyChange = styled.div`
width: 50%;
height: ${(props) => props.theme.spaces[9]}px;
background: linear-gradient(
90deg,
${Colors.GREY_2} 0%,
rgba(240, 240, 240, 0) 100%
);
margin-right: ${(props) => props.theme.spaces[8] + 5}px;
margin-top: ${(props) => props.theme.spaces[7]}px;
margin-bottom: ${(props) => props.theme.spaces[7]}px;
`;
const Wrapper = styled.div`
width: 178px;
height: ${(props) => props.theme.spaces[9]}px;
margin-bottom: ${(props) => props.theme.spaces[7]}px;
display: flex;
.${Classes.ICON} {
margin-right: ${(props) => props.theme.spaces[3]}px;
}
.${Classes.TEXT} {
padding-top: ${(props) => props.theme.spaces[1] - 2}px;
}
`;
const GitChangedRow = styled.div`
display: flex;
align-items: center;
const Statuses = styled.div`
margin-top: ${(props) => props.theme.spaces[7]}px;
margin-bottom: ${(props) => props.theme.spaces[11]}px;
`;
export enum Kind {
widget = "widget",
query = "query",
commit = "commit",
// pullRequest = "pullRequest",
WIDGET = "WIDGET",
QUERY = "QUERY",
COMMIT = "COMMIT",
JS_OBJECT = "JS_OBJECT",
}
type GitSyncProps = {
type: Kind;
type StatusProps = {
iconName: string;
message: string;
hasValue: boolean;
};
function GitStatus(props: GitSyncProps) {
const { type } = props;
const status: any = useSelector(getGitStatus);
const loading = useSelector(getIsFetchingGitStatus);
// const loading = true;
let message = "",
iconName: IconName;
switch (type) {
case Kind.widget:
message = `${status?.modifiedPages || 0} page${
(status?.modifiedPages || 0) === 1 ? "" : "s"
} updated`;
iconName = "widget";
break;
case Kind.query:
message = `${status?.modifiedQueries || 0} ${
(status?.modifiedQueries || 0) === 1 ? "query" : "queries"
} modified`;
iconName = "query";
break;
case Kind.commit:
message = `${status?.aheadCount || 0} commit${
(status?.aheadCount || 0) === 1 ? "" : "s"
} to push`;
iconName = "git-commit";
break;
}
return loading ? (
<Skeleton />
) : (
type StatusMap = {
[key in Kind]: (status: GitStatusData) => StatusProps;
};
const STATUS_MAP: StatusMap = {
[Kind.WIDGET]: (status: GitStatusData) => ({
message: `${status?.modifiedPages || 0} ${
(status?.modifiedPages || 0) <= 1 ? "page" : "pages"
} updated`,
iconName: "widget",
hasValue: (status?.modifiedPages || 0) > 0,
}),
[Kind.QUERY]: (status: GitStatusData) => ({
message: `${status?.modifiedQueries || 0} ${
(status?.modifiedQueries || 0) <= 1 ? "query" : "queries"
} modified`,
iconName: "query",
hasValue: (status?.modifiedQueries || 0) > 0,
}),
[Kind.COMMIT]: (status: GitStatusData) => ({
message: commitMessage(status),
iconName: "git-commit",
hasValue: (status?.aheadCount || 0) > 0 || (status?.behindCount || 0) > 0,
}),
[Kind.JS_OBJECT]: (status: GitStatusData) => ({
message: `${status?.modifiedJSObjects || 0} JS ${
(status?.modifiedJSObjects || 0) <= 1 ? "Object" : "Objects"
} modified`,
iconName: "js",
hasValue: (status?.modifiedJSObjects || 0) > 0,
}),
};
function commitMessage(status: GitStatusData) {
const aheadCount = status?.aheadCount || 0;
const behindCount = status?.behindCount || 0;
const aheadMessage =
aheadCount > 0
? (aheadCount || 0) === 1
? `${aheadCount || 0} commit ahead`
: `${aheadCount || 0} commits ahead`
: null;
const behindMessage =
behindCount > 0
? (behindCount || 0) === 1
? `${behindCount || 0} commit behind`
: `${behindCount || 0} commits behind `
: null;
return [aheadMessage, behindMessage].filter((i) => i !== null).join(" and ");
}
function Status(props: Partial<StatusProps>) {
const { iconName, message } = props;
return (
<Wrapper>
<Icon fillColor={Colors.GREY_10} name={iconName} size={IconSize.XXL} />
<Icon name={iconName} size={IconSize.XXL} />
<Text type={TextType.P3}>{message}</Text>
</Wrapper>
);
}
export default function GitChanged() {
const gitStatus: any = useSelector(getGitStatus);
return (
<GitChangedRow>
<GitStatus type={Kind.widget} />
<GitStatus type={Kind.query} />
{gitStatus?.aheadCount > 0 && <GitStatus type={Kind.commit} />}
</GitChangedRow>
const status: GitStatusData = useSelector(getGitStatus) as GitStatusData;
const loading = useSelector(getIsFetchingGitStatus);
const statuses = [Kind.WIDGET, Kind.QUERY, Kind.COMMIT, Kind.JS_OBJECT]
.map((type: Kind) => STATUS_MAP[type](status))
.map((s) =>
s.hasValue ? <Status {...s} key={`change-status-${s.iconName}`} /> : null,
)
.filter((s) => !!s);
return loading ? (
<DummyChange data-testid={"t--git-change-loading-dummy"} />
) : (
<Statuses data-testid={"t--git-change-statuses"}>{statuses}</Statuses>
);
}

View File

@ -450,6 +450,7 @@ export type GitStatusData = {
modifiedPages: number;
modifiedQueries: number;
remoteBranch: string;
modifiedJSObjects: number;
};
type GitErrorPayloadType = {

View File

@ -87,7 +87,6 @@ import { failFastApiCalls } from "./InitSagas";
import { Datasource } from "entities/Datasource";
import { GUIDED_TOUR_STEPS } from "pages/Editor/GuidedTour/constants";
import { PLACEHOLDER_APP_SLUG, PLACEHOLDER_PAGE_SLUG } from "constants/routes";
import { updateSlugNamesInURL } from "utils/helpers";
import { builderURL, generateTemplateURL, viewerURL } from "RouteBuilder";
import { getDefaultPageId as selectDefaultPageId } from "./selectors";
import PageApi from "api/PageApi";
@ -357,9 +356,6 @@ export function* updateApplicationSaga(
type: ReduxActionTypes.CURRENT_APPLICATION_NAME_UPDATE,
payload: response.data,
});
updateSlugNamesInURL({
applicationSlug: response.data.slug,
});
}
}
} catch (error) {

View File

@ -52,11 +52,7 @@ import {
takeLeading,
} from "redux-saga/effects";
import history from "utils/history";
import {
captureInvalidDynamicBindingPath,
isNameValid,
updateSlugNamesInURL,
} from "utils/helpers";
import { captureInvalidDynamicBindingPath, isNameValid } from "utils/helpers";
import { extractCurrentDSL } from "utils/WidgetPropsUtils";
import { checkIfMigrationIsNeeded } from "utils/DSLMigrations";
import {
@ -319,7 +315,7 @@ export function* fetchPublishedPageSaga(
// Update the canvas
yield put(initCanvasLayout(canvasWidgetsPayload));
// set current page
yield put(updateCurrentPage(pageId));
yield put(updateCurrentPage(pageId, response.data.slug));
// dispatch fetch page success
yield put(
fetchPublishedPageSuccess(
@ -599,9 +595,6 @@ export function* updatePageSaga(action: ReduxAction<UpdatePageRequest>) {
payload: response.data,
});
}
updateSlugNamesInURL({
pageSlug: response.data.slug,
});
} catch (error) {
yield put({
type: ReduxActionErrorTypes.UPDATE_PAGE_ERROR,

View File

@ -10,7 +10,8 @@ import { rootSaga } from "sagas";
import { composeWithDevTools } from "redux-devtools-extension/logOnlyInProduction";
import * as Sentry from "@sentry/react";
import { ReduxAction, ReduxActionTypes } from "constants/ReduxActionConstants";
import { updateURLFactory } from "RouteBuilder";
import { getRouteBuilderParams, updateURLFactory } from "RouteBuilder";
import { updateSlugNamesInURL } from "utils/helpers";
const sagaMiddleware = createSagaMiddleware();
const sentryReduxEnhancer = Sentry.createReduxEnhancer({
@ -42,11 +43,21 @@ const routeParamsMiddleware: Middleware = () => (next: any) => (
case ReduxActionTypes.CURRENT_APPLICATION_NAME_UPDATE: {
const { slug } = action.payload;
updateURLFactory({ applicationSlug: slug });
updateSlugNamesInURL({
applicationSlug: slug,
});
break;
}
case ReduxActionTypes.SWITCH_CURRENT_PAGE_ID:
case ReduxActionTypes.UPDATE_PAGE_SUCCESS: {
const { id, slug } = action.payload;
const id = action.payload.id;
const slug = action.payload.slug;
const { pageId } = getRouteBuilderParams();
// Update page slug in URL only if the current page is renamed
if (pageId === id)
updateSlugNamesInURL({
pageSlug: slug,
});
updateURLFactory({ pageId: id, pageSlug: slug });
break;
}

View File

@ -710,6 +710,8 @@ export const getUpdatedRoute = (
export const updateSlugNamesInURL = (params: Record<string, string>) => {
const { pathname, search } = window.location;
// Do not update old URLs
if (isURLDeprecated(pathname)) return;
const newURL = getUpdatedRoute(pathname, params);
history.replace(newURL + search);
};

View File

@ -591,9 +591,9 @@ public class ImportExportApplicationServiceCEImpl implements ImportExportApplica
/**
* This function will take the application reference object to hydrate the application in mongoDB
*
* @param organizationId organization to which application is going to be stored
* @param applicationJson application resource which contains necessary information to save the application
* @param applicationId application which needs to be saved with the updated resources
* @param organizationId organization to which application is going to be stored
* @param applicationJson application resource which contains necessary information to import the application
* @param applicationId application which needs to be saved with the updated resources
* @return Updated application
*/
public Mono<Application> importApplicationInOrganization(String organizationId,
@ -689,6 +689,7 @@ public class ImportExportApplicationServiceCEImpl implements ImportExportApplica
// Check for duplicate datasources to avoid duplicates in target organization
.flatMap(datasource -> {
final String importedDatasourceName = datasource.getName();
// Check if the datasource has gitSyncId and if it's already in DB
if (datasource.getGitSyncId() != null
&& savedDatasourcesGitIdToDatasourceMap.containsKey(datasource.getGitSyncId())) {
@ -702,7 +703,11 @@ public class ImportExportApplicationServiceCEImpl implements ImportExportApplica
datasource.setPluginId(null);
AppsmithBeanUtils.copyNestedNonNullProperties(datasource, existingDatasource);
existingDatasource.setStructure(null);
return datasourceService.update(existingDatasource.getId(), existingDatasource);
return datasourceService.update(existingDatasource.getId(), existingDatasource)
.map(datasource1 -> {
datasourceMap.put(importedDatasourceName, datasource1.getId());
return datasource1;
});
}
// This is explicitly copied over from the map we created before
@ -719,11 +724,11 @@ public class ImportExportApplicationServiceCEImpl implements ImportExportApplica
updateAuthenticationDTO(datasource, decryptedFields);
}
return createUniqueDatasourceIfNotPresent(existingDatasourceFlux, datasource, organizationId, applicationId);
})
.map(datasource -> {
datasourceMap.put(datasource.getName(), datasource.getId());
return datasource;
return createUniqueDatasourceIfNotPresent(existingDatasourceFlux, datasource, organizationId)
.map(datasource1 -> {
datasourceMap.put(importedDatasourceName, datasource1.getId());
return datasource1;
});
})
.collectList();
})
@ -1726,9 +1731,7 @@ public class ImportExportApplicationServiceCEImpl implements ImportExportApplica
*/
private Mono<Datasource> createUniqueDatasourceIfNotPresent(Flux<Datasource> existingDatasourceFlux,
Datasource datasource,
String organizationId,
String applicationId) {
String organizationId) {
/*
1. If same datasource is present return
2. If unable to find the datasource create a new datasource with unique name and return
@ -1743,16 +1746,8 @@ public class ImportExportApplicationServiceCEImpl implements ImportExportApplica
}
return existingDatasourceFlux
.map(ds -> {
final DatasourceConfiguration dsAuthConfig = ds.getDatasourceConfiguration();
if (dsAuthConfig != null && dsAuthConfig.getAuthentication() != null) {
dsAuthConfig.getAuthentication().setAuthenticationResponse(null);
dsAuthConfig.getAuthentication().setAuthenticationType(null);
}
return ds;
})
// For git import exclude datasource configuration
.filter(ds -> applicationId != null ? ds.getName().equals(datasource.getName()) : ds.softEquals(datasource))
.filter(ds -> ds.getName().equals(datasource.getName()) && datasource.getPluginId().equals(ds.getPluginId()))
.next() // Get the first matching datasource, we don't need more than one here.
.switchIfEmpty(Mono.defer(() -> {
if (datasourceConfig != null && datasourceConfig.getAuthentication() != null) {

View File

@ -1702,7 +1702,7 @@ public class GitServiceTest {
StepVerifier
.create(applicationMono)
.expectErrorMatches(throwable -> throwable instanceof AppsmithException
&& throwable.getMessage().equals(AppsmithError.GIT_ACTION_FAILED.getMessage("checkout", "origin/branchInLocal already exists in remote")))
&& throwable.getMessage().equals(AppsmithError.GIT_ACTION_FAILED.getMessage("checkout", "origin/branchInLocal already exists in local - branchInLocal")))
.verify();
}
@ -2486,7 +2486,7 @@ public class GitServiceTest {
@Test
@WithUserDetails(value = "api_user")
public void importApplicationFromGit_validRequestWithDuplicateDatasourceOfSameTypeCancelledMidway_Success() throws GitAPIException, IOException {
public void importApplicationFromGit_validRequestWithDuplicateDatasourceOfSameTypeCancelledMidway_Success() {
Organization organization = new Organization();
organization.setName("gitImportOrgCancelledMidway");
final String testOrgId = organizationService.create(organization)
@ -2497,6 +2497,7 @@ public class GitServiceTest {
GitAuth gitAuth = gitService.generateSSHKey().block();
ApplicationJson applicationJson = createAppJson(filePath).block();
applicationJson.getExportedApplication().setName(null);
applicationJson.getDatasourceList().get(0).setName("db-auth-testGitImportRepo");
String pluginId = pluginRepository.findByPackageName("mongo-plugin").block().getId();
@ -2521,7 +2522,7 @@ public class GitServiceTest {
// Wait for git clone to complete
Mono<Application> gitConnectedAppFromDbMono = Mono.just(testOrgId)
.flatMap(application -> {
.flatMap(ignore -> {
try {
// Before fetching the git connected application, sleep for 5 seconds to ensure that the clone
// completes

View File

@ -2182,5 +2182,111 @@ public class ImportExportApplicationServiceTests {
})
.verifyComplete();
}
@Test
@WithUserDetails(value = "api_user")
public void importApplication_datasourceWithSameNameAndDifferentPlugin_importedWithValidActionsAndSuffixedDatasource() {
ApplicationJson applicationJson = createAppJson("test_assets/ImportExportServiceTest/valid-application.json").block();
Organization testOrganization = new Organization();
testOrganization.setName("Duplicate datasource with different plugin org");
testOrganization = organizationService.create(testOrganization).block();
Datasource testDatasource = new Datasource();
// Chose any plugin except for mongo, as json static file has mongo plugin for datasource
Plugin postgreSQLPlugin = pluginRepository.findByName("PostgreSQL").block();
testDatasource.setPluginId(postgreSQLPlugin.getId());
testDatasource.setOrganizationId(testOrganization.getId());
final String datasourceName = applicationJson.getDatasourceList().get(0).getName();
testDatasource.setName(datasourceName);
datasourceService.create(testDatasource).block();
final Mono<Application> resultMono = importExportApplicationService.importApplicationInOrganization(testOrganization.getId(), applicationJson);
StepVerifier
.create(resultMono
.flatMap(application -> Mono.zip(
Mono.just(application),
datasourceService.findAllByOrganizationId(application.getOrganizationId(), MANAGE_DATASOURCES).collectList(),
newActionService.findAllByApplicationIdAndViewMode(application.getId(), false, READ_ACTIONS, null).collectList()
)))
.assertNext(tuple -> {
final Application application = tuple.getT1();
final List<Datasource> datasourceList = tuple.getT2();
final List<NewAction> actionList = tuple.getT3();
assertThat(application.getName()).isEqualTo("valid_application");
List<String> datasourceNameList = new ArrayList<>();
assertThat(datasourceList).isNotEmpty();
datasourceList.forEach(datasource -> {
assertThat(datasource.getOrganizationId()).isEqualTo(application.getOrganizationId());
datasourceNameList.add(datasource.getName());
});
// Check if both suffixed and newly imported datasource are present
assertThat(datasourceNameList).contains(datasourceName, datasourceName + " #1");
assertThat(actionList).isNotEmpty();
actionList.forEach(newAction -> {
ActionDTO actionDTO = newAction.getUnpublishedAction();
assertThat(actionDTO.getDatasource()).isNotNull();
});
})
.verifyComplete();
}
@Test
@WithUserDetails(value = "api_user")
public void importApplication_datasourceWithSameNameAndPlugin_importedWithValidActionsWithoutSuffixedDatasource() {
ApplicationJson applicationJson = createAppJson("test_assets/ImportExportServiceTest/valid-application.json").block();
Organization testOrganization = new Organization();
testOrganization.setName("Duplicate datasource with same plugin org");
testOrganization = organizationService.create(testOrganization).block();
Datasource testDatasource = new Datasource();
// Chose plugin same as mongo, as json static file has mongo plugin for datasource
Plugin postgreSQLPlugin = pluginRepository.findByName("MongoDB").block();
testDatasource.setPluginId(postgreSQLPlugin.getId());
testDatasource.setOrganizationId(testOrganization.getId());
final String datasourceName = applicationJson.getDatasourceList().get(0).getName();
testDatasource.setName(datasourceName);
datasourceService.create(testDatasource).block();
final Mono<Application> resultMono = importExportApplicationService.importApplicationInOrganization(testOrganization.getId(), applicationJson);
StepVerifier
.create(resultMono
.flatMap(application -> Mono.zip(
Mono.just(application),
datasourceService.findAllByOrganizationId(application.getOrganizationId(), MANAGE_DATASOURCES).collectList(),
newActionService.findAllByApplicationIdAndViewMode(application.getId(), false, READ_ACTIONS, null).collectList()
)))
.assertNext(tuple -> {
final Application application = tuple.getT1();
final List<Datasource> datasourceList = tuple.getT2();
final List<NewAction> actionList = tuple.getT3();
assertThat(application.getName()).isEqualTo("valid_application");
List<String> datasourceNameList = new ArrayList<>();
assertThat(datasourceList).isNotEmpty();
datasourceList.forEach(datasource -> {
assertThat(datasource.getOrganizationId()).isEqualTo(application.getOrganizationId());
datasourceNameList.add(datasource.getName());
});
// Check that there are no datasources are created with suffix names as datasource's are of same plugin
assertThat(datasourceNameList).contains(datasourceName);
assertThat(actionList).isNotEmpty();
actionList.forEach(newAction -> {
ActionDTO actionDTO = newAction.getUnpublishedAction();
assertThat(actionDTO.getDatasource()).isNotNull();
});
})
.verifyComplete();
}
}