diff --git a/app/client/package.json b/app/client/package.json index 82f9cec123..500d3c2a32 100644 --- a/app/client/package.json +++ b/app/client/package.json @@ -163,7 +163,7 @@ "node-forge": "^1.3.0", "normalizr": "^3.3.0", "object-hash": "^3.0.0", - "path-to-regexp": "^6.2.0", + "path-to-regexp": "^6.3.0", "popper.js": "^1.15.0", "prismjs": "^1.27.0", "proxy-memoize": "^1.2.0", diff --git a/app/client/src/PluginActionEditor/components/PluginActionToolbar.tsx b/app/client/src/PluginActionEditor/components/PluginActionToolbar.tsx index 75ee4dc86b..0d2e4f718d 100644 --- a/app/client/src/PluginActionEditor/components/PluginActionToolbar.tsx +++ b/app/client/src/PluginActionEditor/components/PluginActionToolbar.tsx @@ -1,7 +1,11 @@ -import React from "react"; +import React, { useCallback } from "react"; import { IDEToolbar } from "IDE"; import { Button, Menu, MenuContent, MenuTrigger, Tooltip } from "@appsmith/ads"; import { modText } from "utils/helpers"; +import { usePluginActionContext } from "../PluginActionContext"; +import { useDispatch } from "react-redux"; +import AnalyticsUtil from "ee/utils/AnalyticsUtil"; +import { runAction } from "../../actions/pluginActionActions"; interface PluginActionToolbarProps { runOptions?: React.ReactNode; @@ -10,6 +14,25 @@ interface PluginActionToolbarProps { } const PluginActionToolbar = (props: PluginActionToolbarProps) => { + const { action, datasource, plugin } = usePluginActionContext(); + const dispatch = useDispatch(); + const handleRunClick = useCallback(() => { + AnalyticsUtil.logEvent("RUN_QUERY_CLICK", { + actionName: action.name, + actionId: action.id, + pluginName: plugin.name, + datasourceId: datasource?.id, + isMock: datasource?.isMock, + }); + dispatch(runAction(action.id)); + }, [ + action.id, + action.name, + datasource?.id, + datasource?.isMock, + dispatch, + plugin.name, + ]); return ( {props.children} @@ -20,7 +43,7 @@ const PluginActionToolbar = (props: PluginActionToolbarProps) => { placement="topRight" showArrow={false} > - @@ -30,7 +53,7 @@ const PluginActionToolbar = (props: PluginActionToolbarProps) => { size="sm" startIcon="settings-2-line" /> - + diff --git a/app/client/src/ce/pages/Editor/AppPluginActionEditor/components/AppPluginActionToolbar.tsx b/app/client/src/ce/pages/Editor/AppPluginActionEditor/components/AppPluginActionToolbar.tsx index ab44ba573b..8606cee42f 100644 --- a/app/client/src/ce/pages/Editor/AppPluginActionEditor/components/AppPluginActionToolbar.tsx +++ b/app/client/src/ce/pages/Editor/AppPluginActionEditor/components/AppPluginActionToolbar.tsx @@ -1,9 +1,9 @@ import React from "react"; import { PluginActionToolbar } from "PluginActionEditor"; -import { ConvertToModuleCTA } from "./ConvertToModule"; +import AppPluginActionMenu from "./PluginActionMoreActions"; const AppPluginActionToolbar = () => { - return } />; + return } />; }; export default AppPluginActionToolbar; diff --git a/app/client/src/ce/pages/Editor/AppPluginActionEditor/components/PluginActionMoreActions.tsx b/app/client/src/ce/pages/Editor/AppPluginActionEditor/components/PluginActionMoreActions.tsx new file mode 100644 index 0000000000..d8d37c754d --- /dev/null +++ b/app/client/src/ce/pages/Editor/AppPluginActionEditor/components/PluginActionMoreActions.tsx @@ -0,0 +1,183 @@ +import React, { useCallback, useMemo, useState } from "react"; +import { + getHasDeleteActionPermission, + getHasManageActionPermission, +} from "ee/utils/BusinessFeatures/permissionPageHelpers"; +import { useFeatureFlag } from "utils/hooks/useFeatureFlag"; +import { FEATURE_FLAG } from "ee/entities/FeatureFlag"; +import { usePluginActionContext } from "PluginActionEditor"; +import { + MenuItem, + MenuSub, + MenuSubContent, + MenuSubTrigger, +} from "@appsmith/ads"; +import { + CONFIRM_CONTEXT_DELETE, + CONTEXT_COPY, + CONTEXT_DELETE, + CONTEXT_MOVE, + createMessage, +} from "ee/constants/messages"; +import { useDispatch, useSelector } from "react-redux"; +import { + copyActionRequest, + deleteAction, + moveActionRequest, +} from "actions/pluginActionActions"; +import { getCurrentPageId } from "selectors/editorSelectors"; +import type { Page } from "entities/Page"; +import { getPageList } from "ee/selectors/entitiesSelector"; +import { ConvertToModuleCTA } from "./ConvertToModule"; + +const PageMenuItem = (props: { + page: Page; + onSelect: (id: string) => void; +}) => { + const handleOnSelect = useCallback(() => { + props.onSelect(props.page.pageId); + }, [props]); + return {props.page.pageName}; +}; + +const Copy = () => { + const menuPages = useSelector(getPageList); + const { action } = usePluginActionContext(); + const dispatch = useDispatch(); + + const copyActionToPage = useCallback( + (pageId: string) => + dispatch( + copyActionRequest({ + id: action.id, + destinationPageId: pageId, + name: action.name, + }), + ), + [action.id, action.name, dispatch], + ); + + return ( + + + {createMessage(CONTEXT_COPY)} + + + {menuPages.map((page) => { + return ( + + ); + })} + + + ); +}; + +const Move = () => { + const dispatch = useDispatch(); + const { action } = usePluginActionContext(); + + const currentPageId = useSelector(getCurrentPageId); + const allPages = useSelector(getPageList); + const menuPages = useMemo(() => { + return allPages.filter((page) => page.pageId !== currentPageId); + }, [allPages, currentPageId]); + + const moveActionToPage = useCallback( + (destinationPageId: string) => + dispatch( + moveActionRequest({ + id: action.id, + destinationPageId, + originalPageId: currentPageId, + name: action.name, + }), + ), + [dispatch, action.id, action.name, currentPageId], + ); + + return ( + + + {createMessage(CONTEXT_MOVE)} + + + {menuPages.length > 1 ? ( + menuPages.map((page) => { + return ( + + ); + }) + ) : ( + No pages + )} + + + ); +}; + +const Delete = () => { + const dispatch = useDispatch(); + const { action } = usePluginActionContext(); + + const [confirmDelete, setConfirmDelete] = useState(false); + + const deleteActionFromPage = useCallback(() => { + dispatch(deleteAction({ id: action.id, name: action.name })); + }, [action.id, action.name, dispatch]); + + const handleSelect = useCallback(() => { + confirmDelete ? deleteActionFromPage() : setConfirmDelete(true); + }, [confirmDelete, deleteActionFromPage]); + + const menuLabel = confirmDelete + ? createMessage(CONFIRM_CONTEXT_DELETE) + : createMessage(CONTEXT_DELETE); + + return ( + + {menuLabel} + + ); +}; + +const AppPluginActionMenu = () => { + const { action } = usePluginActionContext(); + + const isFeatureEnabled = useFeatureFlag(FEATURE_FLAG.license_gac_enabled); + const isChangePermitted = getHasManageActionPermission( + isFeatureEnabled, + action.userPermissions, + ); + const isDeletePermitted = getHasDeleteActionPermission( + isFeatureEnabled, + action?.userPermissions, + ); + + return ( + <> + + {isChangePermitted && ( + <> + + + + )} + {isDeletePermitted && } + + ); +}; + +export default AppPluginActionMenu; diff --git a/app/client/yarn.lock b/app/client/yarn.lock index 03470d21a8..cbd1c58cd4 100644 --- a/app/client/yarn.lock +++ b/app/client/yarn.lock @@ -13391,7 +13391,7 @@ __metadata: node-forge: ^1.3.0 normalizr: ^3.3.0 object-hash: ^3.0.0 - path-to-regexp: ^6.2.0 + path-to-regexp: ^6.3.0 pg: ^8.11.3 plop: ^3.1.1 popper.js: ^1.15.0 @@ -27112,10 +27112,10 @@ __metadata: languageName: node linkType: hard -"path-to-regexp@npm:^6.2.0": - version: 6.2.0 - resolution: "path-to-regexp@npm:6.2.0" - checksum: a6aca74d2d6e2e7594d812f653cf85e9cb5054d3a8d80f099722a44ef6ad22639b02078e5ea83d11db16321c3e4359e3f1ab0274fa78dad0754a6e53f630b0fc +"path-to-regexp@npm:^6.3.0": + version: 6.3.0 + resolution: "path-to-regexp@npm:6.3.0" + checksum: eca78602e6434a1b6799d511d375ec044e8d7e28f5a48aa5c28d57d8152fb52f3fc62fb1cfc5dfa2198e1f041c2a82ed14043d75740a2fe60e91b5089a153250 languageName: node linkType: hard diff --git a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/models/DatasourceConfiguration.java b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/models/DatasourceConfiguration.java index 5a81b66b73..637879eb75 100644 --- a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/models/DatasourceConfiguration.java +++ b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/models/DatasourceConfiguration.java @@ -34,8 +34,10 @@ public class DatasourceConfiguration implements AppsmithDomain { @JsonView({Views.Public.class, FromRequest.class}) AuthenticationDTO authentication; + @JsonView({Views.Public.class, FromRequest.class}) SSHConnection sshProxy; + @JsonView({Views.Public.class, FromRequest.class}) Boolean sshProxyEnabled; @JsonView({Views.Public.class, FromRequest.class, Git.class}) diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/migrations/JsonSchemaMigration.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/migrations/JsonSchemaMigration.java index 03d543dd3d..badec22d19 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/migrations/JsonSchemaMigration.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/migrations/JsonSchemaMigration.java @@ -67,28 +67,38 @@ public class JsonSchemaMigration { // TODO: make import flow migration reactive return Mono.just(migrateServerSchema(appJson)) .flatMap(migratedApplicationJson -> { - if (migratedApplicationJson.getServerSchemaVersion() == 9 - && Boolean.TRUE.equals(MigrationHelperMethods.doesRestApiRequireMigration( - migratedApplicationJson))) { - return jsonSchemaMigrationHelper - .addDatasourceConfigurationToDefaultRestApiActions( - baseApplicationId, branchName, migratedApplicationJson) - .map(applicationJsonWithMigration10 -> { - applicationJsonWithMigration10.setServerSchemaVersion(10); - return applicationJsonWithMigration10; - }); + // In Server version 9, there was a bug where the Embedded REST API datasource URL + // was not being persisted correctly. Once the bug was fixed, + // any previously uncommitted changes started appearing as uncommitted modifications + // in the apps. To automatically commit these changes + // (which were now appearing as uncommitted), a migration process was needed. + // This migration fetches the datasource URL from the database + // and serializes it in Git if the URL exists. + // If the URL is missing, it copies the empty datasource configuration + // if the configuration is present in the database. + // Otherwise, it leaves the configuration unchanged. + // Due to an update in the migration logic after version 10 was shipped, + // the entire migration process was moved to version 11. + // This adjustment ensures that the same operation can be + // performed again for the changes introduced in version 10. + if (migratedApplicationJson.getServerSchemaVersion() == 9) { + migratedApplicationJson.setServerSchemaVersion(10); + } + + if (migratedApplicationJson.getServerSchemaVersion() == 10) { + if (Boolean.TRUE.equals(MigrationHelperMethods.doesRestApiRequireMigration( + migratedApplicationJson))) { + return jsonSchemaMigrationHelper + .addDatasourceConfigurationToDefaultRestApiActions( + baseApplicationId, branchName, migratedApplicationJson); + } + + migratedApplicationJson.setServerSchemaVersion(11); } - migratedApplicationJson.setServerSchemaVersion(10); return Mono.just(migratedApplicationJson); }) .map(migratedAppJson -> { - if (applicationJson - .getServerSchemaVersion() - .equals(jsonSchemaVersions.getServerVersion())) { - return applicationJson; - } - applicationJson.setServerSchemaVersion(jsonSchemaVersions.getServerVersion()); return applicationJson; }); @@ -193,16 +203,14 @@ public class JsonSchemaMigration { switch (applicationJson.getServerSchemaVersion()) { case 9: + applicationJson.setServerSchemaVersion(10); + case 10: // this if for cases where we have empty datasource configs MigrationHelperMethods.migrateApplicationJsonToVersionTen(applicationJson, Map.of()); - applicationJson.setServerSchemaVersion(10); + applicationJson.setServerSchemaVersion(11); default: } - if (applicationJson.getServerSchemaVersion().equals(jsonSchemaVersions.getServerVersion())) { - return applicationJson; - } - applicationJson.setServerSchemaVersion(jsonSchemaVersions.getServerVersion()); return applicationJson; } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/migrations/JsonSchemaVersionsFallback.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/migrations/JsonSchemaVersionsFallback.java index 06518c18b4..c85b95bed5 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/migrations/JsonSchemaVersionsFallback.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/migrations/JsonSchemaVersionsFallback.java @@ -4,7 +4,7 @@ import org.springframework.stereotype.Component; @Component public class JsonSchemaVersionsFallback { - private static final Integer serverVersion = 10; + private static final Integer serverVersion = 11; public static final Integer clientVersion = 1; public Integer getServerVersion() { diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/migrations/MigrationHelperMethods.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/migrations/MigrationHelperMethods.java index 87e3d93e6f..277a3ac576 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/migrations/MigrationHelperMethods.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/migrations/MigrationHelperMethods.java @@ -44,6 +44,8 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; +import static com.appsmith.external.constants.PluginConstants.PackageName.GRAPHQL_PLUGIN; +import static com.appsmith.external.constants.PluginConstants.PackageName.REST_API_PLUGIN; import static com.appsmith.server.constants.ResourceModes.EDIT; import static com.appsmith.server.constants.ResourceModes.VIEW; import static org.springframework.data.mongodb.core.query.Criteria.where; @@ -1235,13 +1237,34 @@ public class MigrationHelperMethods { } } - private static boolean conditionForDefaultRestDatasourceMigration(NewAction action) { + public static boolean conditionForDefaultRestDatasource(NewAction action) { + if (action.getUnpublishedAction() == null + || action.getUnpublishedAction().getDatasource() == null) { + return false; + } + Datasource actionDatasource = action.getUnpublishedAction().getDatasource(); + // probable check for the default rest datasource action is. + // it has no datasource id and action's plugin id is either rest-api or graphql plugin. + boolean probableCheckForDefaultRestDatasource = !org.springframework.util.StringUtils.hasText( + actionDatasource.getId()) + && (REST_API_PLUGIN.equals(action.getPluginId()) || GRAPHQL_PLUGIN.equals(action.getPluginId())); + // condition to check if the action is default rest datasource. // it has no datasource id and name is equal to DEFAULT_REST_DATASOURCE - boolean isActionDefaultRestDatasource = !org.springframework.util.StringUtils.hasText(actionDatasource.getId()) - && PluginConstants.DEFAULT_REST_DATASOURCE.equals(actionDatasource.getName()); + boolean certainCheckForDefaultRestDatasource = + !org.springframework.util.StringUtils.hasText(actionDatasource.getId()) + && PluginConstants.DEFAULT_REST_DATASOURCE.equals(actionDatasource.getName()); + + // Two separate types of checks over here, it's either the obvious certain way to identify or + // the likely chance that the datasource is present. + return certainCheckForDefaultRestDatasource || probableCheckForDefaultRestDatasource; + } + + private static boolean conditionForDefaultRestDatasourceMigration(NewAction action) { + boolean isActionDefaultRestDatasource = conditionForDefaultRestDatasource(action); + Datasource actionDatasource = action.getUnpublishedAction().getDatasource(); // condition to check if the action has missing url or has no config at all boolean isDatasourceConfigurationOrUrlMissing = actionDatasource.getDatasourceConfiguration() == null @@ -1322,18 +1345,25 @@ public class MigrationHelperMethods { if (defaultDatasourceActionMap.containsKey(action.getGitSyncId())) { NewAction actionFromMap = defaultDatasourceActionMap.get(action.getGitSyncId()); + // NPE check to avoid migration failures + if (actionFromMap.getUnpublishedAction() == null + || actionFromMap.getUnpublishedAction().getDatasource() == null + || actionFromMap.getUnpublishedAction().getDatasource().getDatasourceConfiguration() == null) { + return; + } + + // set the datasource config in the json action only if the datasource config from db is not null, + // else it'll start to show as uncommited changes. DatasourceConfiguration datasourceConfigurationFromDBAction = actionFromMap.getUnpublishedAction().getDatasource().getDatasourceConfiguration(); - if (datasourceConfigurationFromDBAction != null) { - datasourceConfiguration.setUrl(datasourceConfigurationFromDBAction.getUrl()); - } - } + // At this point it's established that datasource config of db action is not null. + datasourceConfiguration.setUrl(datasourceConfigurationFromDBAction.getUrl()); + actionDatasource.setDatasourceConfiguration(datasourceConfiguration); - if (!org.springframework.util.StringUtils.hasText(datasourceConfiguration.getUrl())) { + } else { datasourceConfiguration.setUrl(""); + actionDatasource.setDatasourceConfiguration(datasourceConfiguration); } - - actionDatasource.setDatasourceConfiguration(datasourceConfiguration); } } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/migrations/utils/JsonSchemaMigrationHelper.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/migrations/utils/JsonSchemaMigrationHelper.java index 934bf79c6e..1f1e27de8c 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/migrations/utils/JsonSchemaMigrationHelper.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/migrations/utils/JsonSchemaMigrationHelper.java @@ -1,6 +1,8 @@ package com.appsmith.server.migrations.utils; import com.appsmith.external.constants.PluginConstants; +import com.appsmith.external.models.Datasource; +import com.appsmith.external.models.PluginType; import com.appsmith.server.applications.base.ApplicationService; import com.appsmith.server.domains.Application; import com.appsmith.server.domains.NewAction; @@ -51,14 +53,27 @@ public class JsonSchemaMigrationHelper { return false; } - boolean reverseFlag = StringUtils.hasText(action.getUnpublishedAction() - .getDatasource() - .getId()) - || !PluginConstants.DEFAULT_REST_DATASOURCE.equals(action.getUnpublishedAction() - .getDatasource() - .getName()); + Datasource actionDatasource = + action.getUnpublishedAction().getDatasource(); - return !reverseFlag; + // lenient probable check for the default rest datasource action is. + // As we don't have any harm in the allowing API actions present in db. + // it has no datasource id and action's plugin type is API + boolean probableCheckForDefaultRestDatasource = + !org.springframework.util.StringUtils.hasText(actionDatasource.getId()) + && PluginType.API.equals(action.getPluginType()); + + // condition to check if the action is default rest datasource. + // it has no datasource id and name is equal to DEFAULT_REST_DATASOURCE + boolean certainCheckForDefaultRestDatasource = + !org.springframework.util.StringUtils.hasText(actionDatasource.getId()) + && PluginConstants.DEFAULT_REST_DATASOURCE.equals( + actionDatasource.getName()); + + // Two separate types of checks over here, it's either the obvious certain way to + // identify or + // the likely chance that the datasource is present. + return certainCheckForDefaultRestDatasource || probableCheckForDefaultRestDatasource; }) .collectMap(NewAction::getGitSyncId); }) diff --git a/app/server/appsmith-server/src/test/java/com/appsmith/server/services/ConsolidatedAPIServiceImplTest.java b/app/server/appsmith-server/src/test/java/com/appsmith/server/services/ce/ConsolidatedAPIServiceImplTest.java similarity index 98% rename from app/server/appsmith-server/src/test/java/com/appsmith/server/services/ConsolidatedAPIServiceImplTest.java rename to app/server/appsmith-server/src/test/java/com/appsmith/server/services/ce/ConsolidatedAPIServiceImplTest.java index cddb33847c..97e651a6b2 100644 --- a/app/server/appsmith-server/src/test/java/com/appsmith/server/services/ConsolidatedAPIServiceImplTest.java +++ b/app/server/appsmith-server/src/test/java/com/appsmith/server/services/ce/ConsolidatedAPIServiceImplTest.java @@ -1,4 +1,4 @@ -package com.appsmith.server.services; +package com.appsmith.server.services.ce; import com.appsmith.external.models.ActionDTO; import com.appsmith.external.models.Datasource; @@ -32,6 +32,14 @@ import com.appsmith.server.newpages.base.NewPageService; import com.appsmith.server.plugins.base.PluginService; import com.appsmith.server.repositories.ApplicationRepository; import com.appsmith.server.repositories.NewPageRepository; +import com.appsmith.server.services.ApplicationPageService; +import com.appsmith.server.services.ConsolidatedAPIService; +import com.appsmith.server.services.MockDataService; +import com.appsmith.server.services.ProductAlertService; +import com.appsmith.server.services.SessionUserService; +import com.appsmith.server.services.TenantService; +import com.appsmith.server.services.UserDataService; +import com.appsmith.server.services.UserService; import com.appsmith.server.themes.base.ThemeService; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired;