diff --git a/.github/workflows/ci-test-limited.yml b/.github/workflows/ci-test-limited.yml index d58b7cb1c23e..e32ac5f9ccd7 100644 --- a/.github/workflows/ci-test-limited.yml +++ b/.github/workflows/ci-test-limited.yml @@ -41,20 +41,15 @@ jobs: # Service containers to run with this job. Required for running tests services: - # Label used to access the service container redis: - # Docker Hub image for Redis image: redis ports: - # Opens tcp port 6379 on the host and service container - 6379:6379 postgres: - if: github.base_ref == 'pg' || github.ref_name == 'pg' image: postgres:14 ports: - 5432:5432 mongo: - if: github.base_ref == 'release' || github.ref_name == 'release' image: mongo ports: - 27017:27017 diff --git a/app/client/cypress/e2e/Regression/ClientSide/BugTests/DS_Bug25148_Spec.ts b/app/client/cypress/e2e/Regression/ClientSide/BugTests/DS_Bug25148_Spec.ts index a2c5228fa9dc..83fc9d8a7955 100644 --- a/app/client/cypress/e2e/Regression/ClientSide/BugTests/DS_Bug25148_Spec.ts +++ b/app/client/cypress/e2e/Regression/ClientSide/BugTests/DS_Bug25148_Spec.ts @@ -17,10 +17,10 @@ describe( dataSources.FillAuthAPIUrl(); dataSources.SaveDatasource(); apiPage.CreateApi("API" + uid, "GET", true); + agHelper.AssertElementAbsence(apiPage._saveAsDS); apiPage.SelectPaneTab("Authentication"); - agHelper.AssertElementEnabledDisabled(apiPage._saveAsDS, 0, false); // Last one if present on the authentication tab. - agHelper.AssertElementEnabledDisabled(apiPage._saveAsDS, 1, false); + agHelper.AssertElementEnabledDisabled(apiPage._saveAsDS, 0, false); }); }); }, diff --git a/app/client/cypress/e2e/Regression/ClientSide/BugTests/DS_Bug26941_Spec.ts b/app/client/cypress/e2e/Regression/ClientSide/BugTests/DS_Bug26941_Spec.ts index 95a6395c3bd5..007f41758a2a 100644 --- a/app/client/cypress/e2e/Regression/ClientSide/BugTests/DS_Bug26941_Spec.ts +++ b/app/client/cypress/e2e/Regression/ClientSide/BugTests/DS_Bug26941_Spec.ts @@ -12,7 +12,7 @@ describe( it("1. Checking whether the appropriate error is displayed even after the removal of invalid chars in header key.", function () { const randomApi = `${ dataManager.dsValues[dataManager.defaultEnviorment].mockApiUrl - }123`; + }`; apiPage.CreateAndFillApi(randomApi); apiPage.RunAPI(false, 2000, { expectedPath: "response.body.data.body.data.isExecutionSuccess", diff --git a/app/client/cypress/e2e/Regression/ClientSide/OneClickBinding/PropertyControl_spec.ts b/app/client/cypress/e2e/Regression/ClientSide/OneClickBinding/PropertyControl_spec.ts index 003c08a2d5cb..465f5d765297 100644 --- a/app/client/cypress/e2e/Regression/ClientSide/OneClickBinding/PropertyControl_spec.ts +++ b/app/client/cypress/e2e/Regression/ClientSide/OneClickBinding/PropertyControl_spec.ts @@ -97,7 +97,7 @@ describe( propPane.ToggleJSMode("Table data", false); oneClickBinding.ChooseAndAssertForm("Users", "Users", "public.users", { - searchableColumn: "gender", + searchableColumn: "email", }); propPane.MoveToTab("Style"); @@ -105,7 +105,7 @@ describe( propPane.MoveToTab("Content"); oneClickBinding.ChooseAndAssertForm("sample Movies", "movies", "movies", { - searchableColumn: "status", + searchableColumn: "imdb_id", }); dataSources.NavigateToDSCreateNew(); dataSources.CreatePlugIn("Mongo"); diff --git a/app/client/cypress/e2e/Regression/ClientSide/OtherUIFeatures/EntityBottomBar_spec.ts b/app/client/cypress/e2e/Regression/ClientSide/OtherUIFeatures/EntityBottomBar_spec.ts index 806170b30ff9..8472a1fab691 100644 --- a/app/client/cypress/e2e/Regression/ClientSide/OtherUIFeatures/EntityBottomBar_spec.ts +++ b/app/client/cypress/e2e/Regression/ClientSide/OtherUIFeatures/EntityBottomBar_spec.ts @@ -102,7 +102,7 @@ describe( //Create and run query. _.dataSources.EnterQuery( - "SELECT * FROM users ORDER BY id LIMIT 10;", + "SELECT * FROM users ORDER BY username LIMIT 10;", 1000, ); _.dataSources.RunQuery(); diff --git a/app/client/cypress/e2e/Regression/ClientSide/Widgets/Iframe/IframeTest_spec.ts b/app/client/cypress/e2e/Regression/ClientSide/Widgets/Iframe/IframeTest_spec.ts index 9047459daf07..af06ad40ac23 100644 --- a/app/client/cypress/e2e/Regression/ClientSide/Widgets/Iframe/IframeTest_spec.ts +++ b/app/client/cypress/e2e/Regression/ClientSide/Widgets/Iframe/IframeTest_spec.ts @@ -46,7 +46,11 @@ describe( // User interaction - Click getIframeBody(1).find(locators._pageHeaderToggle).click({ force: true }); - getIframeBody(1).find(locators._pageHeaderMenuList).should("be.visible"); + agHelper.WaitForCondition(() => { + return getIframeBody(1) + .find(locators._pageHeaderMenuList) + .then(($el: JQuery) => $el.length > 0); + }); }); it("2. Verify colors, borders and shadows", () => { @@ -81,7 +85,7 @@ describe( propPane.UpdatePropertyFieldValue("URL", " "); agHelper.ValidateToastMessage("url updated"); - agHelper.ClickButton("Submit"); + agHelper.ClickButton("Submit", { force: true }); getIframeBody(0) .find("input") .should("be.visible") @@ -90,6 +94,7 @@ describe( expect(inputValue).to.equal("submitclicked"); }); + EditorNavigation.SelectEntityByName("Iframe1", EntityType.Widget); propPane.UpdatePropertyFieldValue( "srcDoc", ` diff --git a/app/client/cypress/e2e/Sanity/Datasources/GraphQL_spec.ts b/app/client/cypress/e2e/Sanity/Datasources/GraphQL_spec.ts index 33dd9ba475ab..b20e8f054b70 100644 --- a/app/client/cypress/e2e/Sanity/Datasources/GraphQL_spec.ts +++ b/app/client/cypress/e2e/Sanity/Datasources/GraphQL_spec.ts @@ -305,6 +305,7 @@ describe( variable: GRAPHQL_VARIABLES, }); apiPage.RunAPI(); + apiPage.SelectPaneTab("Authentication"); agHelper.GetNClick(locators._saveDatasource); dataSources.AssertDataSourceInfo([ dataManager.dsValues[ diff --git a/app/client/cypress/e2e/Sanity/Datasources/RestApiDatasource_spec.js b/app/client/cypress/e2e/Sanity/Datasources/RestApiDatasource_spec.js index 41224748eaac..d8cc12216982 100644 --- a/app/client/cypress/e2e/Sanity/Datasources/RestApiDatasource_spec.js +++ b/app/client/cypress/e2e/Sanity/Datasources/RestApiDatasource_spec.js @@ -5,7 +5,8 @@ import { apiPage, } from "../../../support/Objects/ObjectsCore"; -describe( +//Skip test case due to : https://github.com/appsmithorg/appsmith/issues/37353 +describe.skip( "Create a rest datasource", { tags: ["@tag.Datasource", "@tag.Sanity", "@tag.Git", "@tag.AccessControl"], diff --git a/app/client/cypress/support/Objects/CommonLocators.ts b/app/client/cypress/support/Objects/CommonLocators.ts index d7a534bc4439..85f7e173b819 100644 --- a/app/client/cypress/support/Objects/CommonLocators.ts +++ b/app/client/cypress/support/Objects/CommonLocators.ts @@ -326,7 +326,7 @@ export class CommonLocators { _treeSelectedContent = ".rc-tree-select-selection-item-content"; _switcherIcon = ".switcher-icon"; _root = "#root"; - _pageHeaderToggle = ".navbar__items>button"; + _pageHeaderToggle = ".navbar__items > button"; _pageHeaderMenuList = ".navbar-sidebar__backdrop"; _enterFullScreen = ".application-demo-new-dashboard-control-enter-fullscreen"; _dashboardContainer = ".application-demo-new-dashboard-container"; diff --git a/app/client/cypress/support/Pages/DataSources.ts b/app/client/cypress/support/Pages/DataSources.ts index 58a3099f2b49..85ad360fb8fa 100644 --- a/app/client/cypress/support/Pages/DataSources.ts +++ b/app/client/cypress/support/Pages/DataSources.ts @@ -206,7 +206,7 @@ export class DataSources { ".t--datasource-name:contains('" + dsName + "')"; _mandatoryMark = "//span[text()='*']"; _deleteDSHostPort = ".t--delete-field"; - _dsTabSchema = "[data-testid='t--tab-SCHEMA_TAB']"; + _dsTabSchema = "[data-testid='t--tab-DATASOURCE_TAB']"; private _pageSelectionMenu = "[data-testid='t--page-selection']"; private _pageSelectMenuItem = ".ads-v2-menu__menu-item"; @@ -1891,7 +1891,9 @@ export class DataSources { cy.intercept("GET", "/api/v1/datasources/*/structure?ignoreCache=*").as( `getDatasourceStructureUpdated_${ds_entity_name}`, ); - cy.get("[data-testid=t--tab-SCHEMA_TAB]").first().click({ force: true }); + cy.get("[data-testid=t--tab-DATASOURCE_TAB]") + .first() + .click({ force: true }); this.RefreshDatasourceSchema(); this.assertHelper .WaitForNetworkCall(`@getDatasourceStructureUpdated_${ds_entity_name}`) diff --git a/app/client/packages/design-system/ads/src/Icon/Icon.provider.tsx b/app/client/packages/design-system/ads/src/Icon/Icon.provider.tsx index 5a5ff05eb1e0..bf7406c6ee75 100644 --- a/app/client/packages/design-system/ads/src/Icon/Icon.provider.tsx +++ b/app/client/packages/design-system/ads/src/Icon/Icon.provider.tsx @@ -1033,6 +1033,10 @@ const DashboardLineIcon = importSvg( async () => import("../__assets__/icons/ads/dashboard-line.svg"), ); +const DatasourceConfigIcon = importSvg( + async () => import("../__assets__/icons/ads/datasource-config.svg"), +); + // v3 icons const JsSquareV3Icon = importSvg( async () => import("../__assets__/icons/ads/js-square-v3-icon.svg"), @@ -1173,6 +1177,7 @@ const ICON_LOOKUP = { "cut-control": CutIcon, "dashboard-line": DashboardLineIcon, "database-2-line": Database2Line, + "datasource-config": DatasourceConfigIcon, "datasource-v3": DatasourceV3Icon, "datasources-2": Datasources2, "decrease-control": DecreaseIcon, diff --git a/app/client/packages/design-system/ads/src/Switch/Switch.styles.tsx b/app/client/packages/design-system/ads/src/Switch/Switch.styles.tsx index 96b9bee34147..2b0358ffeee2 100644 --- a/app/client/packages/design-system/ads/src/Switch/Switch.styles.tsx +++ b/app/client/packages/design-system/ads/src/Switch/Switch.styles.tsx @@ -31,6 +31,7 @@ export const StyledSwitchLabel = styled(Text)<{ justify-content: space-between; min-width: 9rem; cursor: pointer; + word-break: break-all; ${({ isDisabled }) => isDisabled && diff --git a/app/client/packages/design-system/ads/src/__assets__/icons/ads/datasource-config.svg b/app/client/packages/design-system/ads/src/__assets__/icons/ads/datasource-config.svg new file mode 100644 index 000000000000..dbc7387c46f7 --- /dev/null +++ b/app/client/packages/design-system/ads/src/__assets__/icons/ads/datasource-config.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/app/client/src/PluginActionEditor/components/PluginActionForm/components/CommonEditorForm/components/EmbeddedDatasourcePathField.tsx b/app/client/src/PluginActionEditor/components/PluginActionForm/components/CommonEditorForm/components/EmbeddedDatasourcePathField.tsx index 87615cbf7f2f..55511f3b9710 100644 --- a/app/client/src/PluginActionEditor/components/PluginActionForm/components/CommonEditorForm/components/EmbeddedDatasourcePathField.tsx +++ b/app/client/src/PluginActionEditor/components/PluginActionForm/components/CommonEditorForm/components/EmbeddedDatasourcePathField.tsx @@ -593,7 +593,7 @@ class EmbeddedDatasourcePathComponent extends React.Component< )} - {displayValue && ( + {displayValue && shouldSave && ( { +const Datasource = (props: Props) => { const dispatch = useDispatch(); const datasourceStructure = useSelector((state) => @@ -44,7 +45,8 @@ const Schema = (props: Props) => { getPluginIdFromDatasourceId(state, props.datasourceId), ); - const currentPageId = useSelector(getCurrentPageId); + const editorType = useEditorType(location.pathname); + const { parentEntityId } = useParentEntityInfo(editorType); const [selectedTable, setSelectedTable] = useState(); @@ -107,7 +109,7 @@ const Schema = (props: Props) => { }); const url = datasourcesEditorIdURL({ - basePageId: currentPageId, + baseParentEntityId: parentEntityId, datasourceId: props.datasourceId, params: { ...omit(getQueryParams(), "viewMode"), viewMode: false }, generateEditorPath: true, @@ -137,13 +139,12 @@ const Schema = (props: Props) => { } return ( - <> - - - + + { } state={statusState} /> - + ); }; @@ -164,7 +165,7 @@ const Schema = (props: Props) => { return ( - { ); }; -export { Schema }; +export { Datasource }; diff --git a/app/client/src/PluginActionEditor/components/PluginActionResponse/components/DatasourceTab/DatasourceInfo.tsx b/app/client/src/PluginActionEditor/components/PluginActionResponse/components/DatasourceTab/DatasourceInfo.tsx new file mode 100644 index 000000000000..e671e70d4afc --- /dev/null +++ b/app/client/src/PluginActionEditor/components/PluginActionResponse/components/DatasourceTab/DatasourceInfo.tsx @@ -0,0 +1,69 @@ +import React from "react"; +import { Button, Flex, Tooltip } from "@appsmith/ads"; +import DatasourceSelector from "./DatasourceSelector"; +import { createMessage, EDIT_DS_CONFIG } from "ee/constants/messages"; +import { DatasourceEditEntryPoints } from "constants/Datasource"; +import AnalyticsUtil from "ee/utils/AnalyticsUtil"; +import { datasourcesEditorIdURL } from "ee/RouteBuilder"; +import { omit } from "lodash"; +import { getQueryParams } from "utils/URLUtils"; +import history from "utils/history"; +import { useEditorType } from "ee/hooks"; +import { useParentEntityInfo } from "ee/hooks/datasourceEditorHooks"; + +interface Props { + datasourceId: string; + datasourceName: string; + showEditButton: boolean; +} + +const DatasourceInfo = ({ + datasourceId, + datasourceName, + showEditButton, +}: Props) => { + const editorType = useEditorType(location.pathname); + const { parentEntityId } = useParentEntityInfo(editorType); + + // eslint-disable-next-line react-perf/jsx-no-new-function-as-prop + const editDatasource = () => { + const entryPoint = DatasourceEditEntryPoints.QUERY_EDITOR_DATASOURCE_SCHEMA; + + AnalyticsUtil.logEvent("EDIT_DATASOURCE_CLICK", { + datasourceId: datasourceId, + pluginName: "", + entryPoint: entryPoint, + }); + + const url = datasourcesEditorIdURL({ + baseParentEntityId: parentEntityId, + datasourceId: datasourceId, + params: { ...omit(getQueryParams(), "viewMode"), viewMode: false }, + generateEditorPath: true, + }); + + history.push(url); + }; + + return ( + + + {showEditButton && ( + + + + + + + ); +}; + +const REQUEST_NEW_INTEGRATION_FORM_NAME = "REQUEST_NEW_INTEGRATION"; + +const selector = formValueSelector(REQUEST_NEW_INTEGRATION_FORM_NAME); + +interface RequestIntegrationFormValues { + integration?: string; + email?: string; + useCase?: string; +} + +type RequestIntegrationFormProps = RequestIntegrationFormValues & { + formSyncErrors?: FormErrors; + closeModal: () => void; +} & InjectedFormProps< + RequestIntegrationFormValues, + { + formSyncErrors?: FormErrors; + closeModal: () => void; + } + >; + +const validate = (values: RequestIntegrationFormValues) => { + const errors: Partial = {}; + + if (!values.integration) { + errors.integration = createMessage( + REQUEST_NEW_INTEGRATIONS.REQUEST_MODAL_INTEGRATION.ERROR, + ); + } + + if (!values.email || !isEmail(values.email)) { + errors.email = createMessage( + REQUEST_NEW_INTEGRATIONS.REQUEST_MODAL_EMAIL.ERROR, + ); + } + + return errors; +}; + +export default connect((state: AppState) => { + const currentUser = getCurrentUser(state); + + return { + integration: selector(state, "integration"), + email: selector(state, "email"), + useCase: selector(state, "useCase"), + initialValues: { + email: currentUser?.email, + }, + formSyncErrors: getFormSyncErrors(REQUEST_NEW_INTEGRATION_FORM_NAME)(state), + }; +}, null)( + reduxForm< + RequestIntegrationFormValues, + { + formSyncErrors?: FormErrors; + closeModal: () => void; + } + >({ + validate, + form: REQUEST_NEW_INTEGRATION_FORM_NAME, + enableReinitialize: true, + })(RequestIntegrationForm), +); diff --git a/app/client/src/pages/Editor/IntegrationEditor/RequestNewIntegration/index.tsx b/app/client/src/pages/Editor/IntegrationEditor/RequestNewIntegration/index.tsx new file mode 100644 index 000000000000..0154902442b1 --- /dev/null +++ b/app/client/src/pages/Editor/IntegrationEditor/RequestNewIntegration/index.tsx @@ -0,0 +1,59 @@ +import { + Button, + Flex, + Modal, + ModalContent, + ModalHeader, + ModalTrigger, +} from "@appsmith/ads"; +import { createMessage, REQUEST_NEW_INTEGRATIONS } from "ee/constants/messages"; +import React, { useState, type ReactNode } from "react"; +import styled from "styled-components"; +import Form from "./form"; +import AnalyticsUtil from "ee/utils/AnalyticsUtil"; + +const RequestNewIntegrationWrapper = styled(Flex)` + padding: var(--ads-spaces-7); + border-top: 1px solid var(--ads-v2-colors-content-surface-default-border); + position: sticky; + bottom: 0; + background: var(--ads-v2-color-bg); +`; + +const ModalContentWrapper = styled(ModalContent)` + max-width: 518px; +`; + +function RequestModal({ children }: { children: ReactNode }) { + const [open, setOpen] = useState(false); + + return ( + + {children} + + + {createMessage(REQUEST_NEW_INTEGRATIONS.REQUEST_MODAL_HEADING)} + +
setOpen(false)} /> + + + ); +} + +export default function RequestNewIntegration() { + return ( + +

{createMessage(REQUEST_NEW_INTEGRATIONS.UNABLE_TO_FIND)}

+ + + +
+ ); +} diff --git a/app/client/src/pages/Editor/QueryEditor/QueryDebuggerTabs.tsx b/app/client/src/pages/Editor/QueryEditor/QueryDebuggerTabs.tsx index eb5171787a16..5d18de6c5ec1 100644 --- a/app/client/src/pages/Editor/QueryEditor/QueryDebuggerTabs.tsx +++ b/app/client/src/pages/Editor/QueryEditor/QueryDebuggerTabs.tsx @@ -12,7 +12,7 @@ import { } from "ee/constants/messages"; import DebuggerLogs from "components/editorComponents/Debugger/DebuggerLogs"; import ErrorLogs from "components/editorComponents/Debugger/Errors"; -import { Schema } from "PluginActionEditor/components/PluginActionResponse/components/Schema"; +import { Datasource } from "PluginActionEditor/components/PluginActionResponse/components/DatasourceTab"; import type { ActionResponse } from "api/ActionAPI"; import type { SourceEntity } from "entities/AppsmithConsole"; import type { Action } from "entities/Action"; @@ -141,7 +141,7 @@ function QueryDebuggerTabs({ dispatch( setPluginActionEditorDebuggerState({ open: true, - selectedTab: DEBUGGER_TAB_KEYS.SCHEMA_TAB, + selectedTab: DEBUGGER_TAB_KEYS.DATASOURCE_TAB, }), ); } @@ -217,10 +217,10 @@ function QueryDebuggerTabs({ if (showSchema && currentActionConfig && currentActionConfig.datasource) { responseTabs.unshift({ - key: DEBUGGER_TAB_KEYS.SCHEMA_TAB, - title: "Schema", + key: DEBUGGER_TAB_KEYS.DATASOURCE_TAB, + title: "Datasource", panelComponent: ( - executeCommon( ExecuteActionDTO executeActionDTO) { log.debug(Thread.currentThread().getName() + ": executeCommon() called for AppsmithAI plugin."); - // Initializing object for error condition - ActionExecutionResult errorResult = new ActionExecutionResult(); - initUtils.initializeResponseWithError(errorResult); Feature feature = Feature.valueOf(RequestUtils.extractDataFromFormData(actionConfiguration.getFormData(), USECASE)); AiFeatureService aiFeatureService = AiFeatureServiceFactory.getAiFeatureService(feature); Query query = aiFeatureService.createQuery(actionConfiguration, datasourceConfiguration, executeActionDTO); AiServerRequestDTO aiServerRequestDTO = new AiServerRequestDTO(feature, query); - ActionExecutionResult actionExecutionResult = new ActionExecutionResult(); ActionExecutionRequest actionExecutionRequest = RequestCaptureFilter.populateRequestFields( actionConfiguration, RequestUtils.getQueryUri(), insertedParams, objectMapper); + SourceDetails sourceDetails = SourceDetails.createSourceDetails(executeActionDTO); + + return handleExecution(aiFeatureService, aiServerRequestDTO, sourceDetails, actionExecutionRequest); + } + + private Mono handleExecution( + AiFeatureService aiFeatureService, + AiServerRequestDTO aiServerRequestDTO, + SourceDetails sourceDetails, + ActionExecutionRequest actionExecutionRequest) { + Mono actionExecutionResultMono; + actionExecutionResultMono = + handleExecuteForAiServerService(aiServerRequestDTO, sourceDetails, actionExecutionRequest); + return actionExecutionResultMono.onErrorResume(this::handleError); + } + + private Mono handleExecuteForAiServerService( + AiServerRequestDTO aiServerRequestDTO, + SourceDetails sourceDetails, + ActionExecutionRequest actionExecutionRequest) { return aiServerService - .executeQuery(aiServerRequestDTO, SourceDetails.createSourceDetails(executeActionDTO)) + .executeQuery(aiServerRequestDTO, sourceDetails) .map(response -> { + ActionExecutionResult actionExecutionResult = new ActionExecutionResult(); actionExecutionResult.setIsExecutionSuccess(true); actionExecutionResult.setBody(response); actionExecutionResult.setRequest(actionExecutionRequest); return actionExecutionResult; - }) - .onErrorResume(error -> { - errorResult.setIsExecutionSuccess(false); - log.error( - "An error has occurred while trying to run the AI server API query. Error: {}", - error.getMessage()); - if (!(error instanceof AppsmithPluginException)) { - error = new AppsmithPluginException( - AppsmithPluginError.PLUGIN_ERROR, error.getMessage(), error); - } - errorResult.setErrorInfo(error); - return Mono.just(errorResult); }); } + private Mono handleError(Throwable error) { + ActionExecutionResult errorResult = new ActionExecutionResult(); + initUtils.initializeResponseWithError(errorResult); + log.error( + "An error has occurred while trying to run the AI server API query. Error: {}", error.getMessage()); + if (!(error instanceof AppsmithPluginException)) { + error = new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, error.getMessage(), error); + } + errorResult.setErrorInfo(error); + return Mono.just(errorResult); + } + @Override public Set validateDatasource(DatasourceConfiguration datasourceConfiguration, boolean isEmbedded) { log.debug(Thread.currentThread().getName() + ": validateDatasource() called for AppsmithAI plugin."); diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/ce/GitArtifactMetadataCE.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/ce/GitArtifactMetadataCE.java index 1c3b700c56a4..f5b038489cd1 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/ce/GitArtifactMetadataCE.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/ce/GitArtifactMetadataCE.java @@ -2,6 +2,7 @@ import com.appsmith.external.models.AppsmithDomain; import com.appsmith.external.views.Views; +import com.appsmith.server.constants.ce.RefType; import com.appsmith.server.domains.AutoCommitConfig; import com.appsmith.server.domains.GitAuth; import com.appsmith.server.domains.GitProfile; @@ -24,6 +25,16 @@ public class GitArtifactMetadataCE implements AppsmithDomain { @JsonView(Views.Public.class) String branchName; + // TODO: make this public view and remove transient annotation once implmentation completes + @Transient + @JsonView(Views.Internal.class) + String refName; + + // TODO: make this public view and remove transient annotation once implementation completes + @Transient + @JsonView(Views.Internal.class) + RefType refType; + // Git default branch corresponding to the remote git repo to which the application is connected to @JsonView(Views.Public.class) String defaultBranchName; diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/git/central/CentralGitServiceCE.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/git/central/CentralGitServiceCE.java index 27768442c07e..576a7fb99cd3 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/git/central/CentralGitServiceCE.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/git/central/CentralGitServiceCE.java @@ -2,6 +2,7 @@ import com.appsmith.git.dto.CommitDTO; import com.appsmith.server.constants.ArtifactType; +import com.appsmith.server.constants.ce.RefType; import com.appsmith.server.domains.Artifact; import com.appsmith.server.dtos.ArtifactImportDTO; import com.appsmith.server.dtos.GitConnectDTO; @@ -23,4 +24,11 @@ Mono commitArtifact( CommitDTO commitDTO, String branchedArtifactId, ArtifactType artifactType, GitType gitType); Mono detachRemote(String branchedArtifactId, ArtifactType artifactType, GitType gitType); + + Mono fetchRemoteChanges( + String referenceArtifactId, + boolean isFileLock, + ArtifactType artifactType, + GitType gitType, + RefType refType); } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/git/central/CentralGitServiceCECompatibleImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/git/central/CentralGitServiceCECompatibleImpl.java index b996e5849121..f33fa75b1286 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/git/central/CentralGitServiceCECompatibleImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/git/central/CentralGitServiceCECompatibleImpl.java @@ -10,6 +10,7 @@ import com.appsmith.server.helpers.GitPrivateRepoHelper; import com.appsmith.server.imports.internal.ImportService; import com.appsmith.server.plugins.base.PluginService; +import com.appsmith.server.services.SessionUserService; import com.appsmith.server.services.UserDataService; import com.appsmith.server.services.WorkspaceService; import com.appsmith.server.solutions.DatasourcePermission; @@ -26,6 +27,7 @@ public CentralGitServiceCECompatibleImpl( GitProfileUtils gitProfileUtils, GitAnalyticsUtils gitAnalyticsUtils, UserDataService userDataService, + SessionUserService sessionUserService, GitArtifactHelperResolver gitArtifactHelperResolver, GitHandlingServiceResolver gitHandlingServiceResolver, GitPrivateRepoHelper gitPrivateRepoHelper, @@ -41,6 +43,7 @@ public CentralGitServiceCECompatibleImpl( gitProfileUtils, gitAnalyticsUtils, userDataService, + sessionUserService, gitArtifactHelperResolver, gitHandlingServiceResolver, gitPrivateRepoHelper, diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/git/central/CentralGitServiceCEImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/git/central/CentralGitServiceCEImpl.java index 9af8b9ce9ee3..64bf970ce994 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/git/central/CentralGitServiceCEImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/git/central/CentralGitServiceCEImpl.java @@ -18,6 +18,7 @@ import com.appsmith.server.domains.GitAuth; import com.appsmith.server.domains.GitProfile; import com.appsmith.server.domains.Plugin; +import com.appsmith.server.domains.User; import com.appsmith.server.domains.UserData; import com.appsmith.server.domains.Workspace; import com.appsmith.server.dtos.ArtifactExchangeJson; @@ -36,6 +37,7 @@ import com.appsmith.server.imports.internal.ImportService; import com.appsmith.server.plugins.base.PluginService; import com.appsmith.server.services.GitArtifactHelper; +import com.appsmith.server.services.SessionUserService; import com.appsmith.server.services.UserDataService; import com.appsmith.server.services.WorkspaceService; import com.appsmith.server.solutions.DatasourcePermission; @@ -44,6 +46,7 @@ import lombok.extern.slf4j.Slf4j; import org.eclipse.jgit.api.errors.InvalidRemoteException; import org.eclipse.jgit.api.errors.TransportException; +import org.eclipse.jgit.lib.BranchTrackingStatus; import org.springframework.stereotype.Service; import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; @@ -65,6 +68,7 @@ import static com.appsmith.external.git.constants.ce.GitConstantsCE.GIT_PROFILE_ERROR; import static com.appsmith.external.git.constants.ce.GitSpanCE.OPS_COMMIT; import static com.appsmith.external.helpers.AppsmithBeanUtils.copyNestedNonNullProperties; +import static com.appsmith.server.constants.FieldName.BRANCH_NAME; import static com.appsmith.server.constants.FieldName.DEFAULT; import static com.appsmith.server.constants.SerialiseArtifactObjective.VERSION_CONTROL; import static java.lang.Boolean.FALSE; @@ -79,6 +83,7 @@ public class CentralGitServiceCEImpl implements CentralGitServiceCE { private final GitProfileUtils gitProfileUtils; private final GitAnalyticsUtils gitAnalyticsUtils; private final UserDataService userDataService; + private final SessionUserService sessionUserService; protected final GitArtifactHelperResolver gitArtifactHelperResolver; protected final GitHandlingServiceResolver gitHandlingServiceResolver; @@ -940,6 +945,108 @@ private boolean isBaseGitMetadataInvalid(GitArtifactMetadata gitArtifactMetadata .isGitAuthInvalid(gitArtifactMetadata.getGitAuth()); } + public Mono fetchRemoteChanges( + Artifact baseArtifact, Artifact refArtifact, boolean isFileLock, GitType gitType, RefType refType) { + + if (refArtifact == null + || baseArtifact == null + || isBaseGitMetadataInvalid(baseArtifact.getGitArtifactMetadata(), gitType)) { + return Mono.error(new AppsmithException(AppsmithError.GIT_GENERIC_ERROR)); + } + + GitArtifactMetadata baseArtifactGitData = baseArtifact.getGitArtifactMetadata(); + GitArtifactMetadata refArtifactGitData = refArtifact.getGitArtifactMetadata(); + + String baseArtifactId = baseArtifactGitData.getDefaultArtifactId(); + + // TODO add gitType in all error messages. + if (refArtifactGitData == null || !hasText(refArtifactGitData.getRefName())) { + return Mono.error(new AppsmithException(AppsmithError.INVALID_PARAMETER, BRANCH_NAME)); + } + + Mono currUserMono = sessionUserService.getCurrentUser().cache(); // will be used to send analytics event + Mono acquireGitLockMono = + gitRedisUtils.acquireGitLock(baseArtifactId, GitConstants.GitCommandConstants.FETCH_REMOTE, isFileLock); + + ArtifactJsonTransformationDTO jsonTransformationDTO = new ArtifactJsonTransformationDTO(); + jsonTransformationDTO.setWorkspaceId(baseArtifact.getWorkspaceId()); + jsonTransformationDTO.setBaseArtifactId(baseArtifactGitData.getDefaultArtifactId()); + jsonTransformationDTO.setRepoName(baseArtifactGitData.getRepoName()); + jsonTransformationDTO.setArtifactType(baseArtifact.getArtifactType()); + jsonTransformationDTO.setRefName(refArtifactGitData.getRefName()); + jsonTransformationDTO.setRefType(refType); + + GitHandlingService gitHandlingService = gitHandlingServiceResolver.getGitHandlingService(gitType); + + // current user mono has been zipped just to run in parallel. + Mono fetchRemoteMono = acquireGitLockMono + .then(Mono.defer(() -> + gitHandlingService.fetchRemoteChanges(jsonTransformationDTO, baseArtifactGitData.getGitAuth()))) + .flatMap(fetchedRemoteStatusString -> { + return gitRedisUtils.releaseFileLock(baseArtifactId).thenReturn(fetchedRemoteStatusString); + }) + .onErrorResume(throwable -> { + /* + in case of any error, the global exception handler will release the lock + hence we don't need to do that manually + */ + log.error( + "Error to fetch from remote for application: {}, branch: {}, git type {}", + baseArtifactId, + refArtifactGitData.getRefName(), + gitType, + throwable); + return Mono.error( + new AppsmithException(AppsmithError.GIT_ACTION_FAILED, "fetch", throwable.getMessage())); + }) + .elapsed() + .zipWith(currUserMono) + .flatMap(objects -> { + Long elapsedTime = objects.getT1().getT1(); + String fetchRemote = objects.getT1().getT2(); + User currentUser = objects.getT2(); + return gitAnalyticsUtils + .sendUnitExecutionTimeAnalyticsEvent( + AnalyticsEvents.GIT_FETCH.getEventName(), elapsedTime, currentUser, refArtifact) + .thenReturn(fetchRemote); + }) + .name(GitSpan.OPS_FETCH_REMOTE) + .tap(Micrometer.observation(observationRegistry)); + + return Mono.create(sink -> { + fetchRemoteMono.subscribe(sink::success, sink::error, null, sink.currentContext()); + }); + } + + /** + * This method is responsible to compare the current branch with the remote branch. + * Comparing means finding two numbers - how many commits ahead and behind the local branch is. + * It'll do the following things - + * 1. Checkout (if required) to the branch to make sure we are comparing the right branch + * 2. Run a git fetch command to fetch the latest changes from the remote + * + * @param refArtifactId id of the reference + * @param isFileLock whether to add file lock or not + * @param artifactType + * @return Mono of {@link BranchTrackingStatus} + */ + @Override + public Mono fetchRemoteChanges( + String refArtifactId, boolean isFileLock, ArtifactType artifactType, GitType gitType, RefType refType) { + GitArtifactHelper artifactGitHelper = gitArtifactHelperResolver.getArtifactHelper(artifactType); + AclPermission artifactEditPermission = artifactGitHelper.getArtifactEditPermission(); + + Mono> baseAndBranchedArtifactMono = + getBaseAndBranchedArtifacts(refArtifactId, artifactType, artifactEditPermission); + + return baseAndBranchedArtifactMono.flatMap(artifactTuples -> { + Artifact baseArtifact = artifactTuples.getT1(); + Artifact refArtifact = artifactTuples.getT2(); + + return fetchRemoteChanges(baseArtifact, refArtifact, isFileLock, gitType, refType); + }); + } + /** * Returns baseArtifact and branchedArtifact * This operation is quite frequently used, hence providing the right set diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/git/central/CentralGitServiceImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/git/central/CentralGitServiceImpl.java index 7c642d832d0e..0a85050f1ffa 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/git/central/CentralGitServiceImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/git/central/CentralGitServiceImpl.java @@ -10,6 +10,7 @@ import com.appsmith.server.helpers.GitPrivateRepoHelper; import com.appsmith.server.imports.internal.ImportService; import com.appsmith.server.plugins.base.PluginService; +import com.appsmith.server.services.SessionUserService; import com.appsmith.server.services.UserDataService; import com.appsmith.server.services.WorkspaceService; import com.appsmith.server.solutions.DatasourcePermission; @@ -25,6 +26,7 @@ public CentralGitServiceImpl( GitProfileUtils gitProfileUtils, GitAnalyticsUtils gitAnalyticsUtils, UserDataService userDataService, + SessionUserService sessionUserService, GitArtifactHelperResolver gitArtifactHelperResolver, GitHandlingServiceResolver gitHandlingServiceResolver, GitPrivateRepoHelper gitPrivateRepoHelper, @@ -40,6 +42,7 @@ public CentralGitServiceImpl( gitProfileUtils, gitAnalyticsUtils, userDataService, + sessionUserService, gitArtifactHelperResolver, gitHandlingServiceResolver, gitPrivateRepoHelper, diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/git/central/GitHandlingServiceCE.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/git/central/GitHandlingServiceCE.java index 3b79a0f17957..5b9551ffa675 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/git/central/GitHandlingServiceCE.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/git/central/GitHandlingServiceCE.java @@ -56,4 +56,6 @@ Mono prepareChangesToBeCommitted( Mono> commitArtifact( Artifact branchedArtifact, CommitDTO commitDTO, ArtifactJsonTransformationDTO jsonTransformationDTO); + + Mono fetchRemoteChanges(ArtifactJsonTransformationDTO jsonTransformationDTO, GitAuth gitAuth); } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/git/fs/GitFSServiceCEImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/git/fs/GitFSServiceCEImpl.java index a65c46733cf3..ffd8c26e4f50 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/git/fs/GitFSServiceCEImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/git/fs/GitFSServiceCEImpl.java @@ -568,4 +568,31 @@ private Mono pushArtifactErrorRecovery(String pushResult, Artifact artif } return Mono.just(pushResult); } + + /** + * File system implementation of fetching remote changes. equivalent to git fetch + * @param jsonTransformationDTO : DTO to create path and other ref related details + * @param gitAuth : authentication holder + * @return : returns string for remote fetch + */ + @Override + public Mono fetchRemoteChanges(ArtifactJsonTransformationDTO jsonTransformationDTO, GitAuth gitAuth) { + + String workspaceId = jsonTransformationDTO.getWorkspaceId(); + String baseArtifactId = jsonTransformationDTO.getBaseArtifactId(); + String repoName = jsonTransformationDTO.getRepoName(); + String refName = jsonTransformationDTO.getRefName(); + + ArtifactType artifactType = jsonTransformationDTO.getArtifactType(); + GitArtifactHelper gitArtifactHelper = gitArtifactHelperResolver.getArtifactHelper(artifactType); + Path repoSuffix = gitArtifactHelper.getRepoSuffixPath(workspaceId, baseArtifactId, repoName); + + Path repoPath = fsGitHandler.createRepoPath(repoSuffix); + Mono checkoutBranchMono = fsGitHandler.checkoutToBranch(repoSuffix, refName); + + Mono fetchRemoteMono = fsGitHandler.fetchRemote( + repoPath, gitAuth.getPublicKey(), gitAuth.getPrivateKey(), true, refName, false); + + return checkoutBranchMono.then(Mono.defer(() -> fetchRemoteMono)); + } } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/git/utils/GitAnalyticsUtils.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/git/utils/GitAnalyticsUtils.java index add712ac1c77..1cc0f71d61f5 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/git/utils/GitAnalyticsUtils.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/git/utils/GitAnalyticsUtils.java @@ -5,6 +5,7 @@ import com.appsmith.server.domains.ApplicationMode; import com.appsmith.server.domains.Artifact; import com.appsmith.server.domains.GitArtifactMetadata; +import com.appsmith.server.domains.User; import com.appsmith.server.helpers.GitUtils; import com.appsmith.server.services.AnalyticsService; import com.appsmith.server.services.SessionUserService; @@ -122,4 +123,27 @@ public Mono addAnalyticsForGitOperation( .sendEvent(event.getEventName(), user.getUsername(), analyticsProps) .thenReturn(artifact)); } + + public Mono sendUnitExecutionTimeAnalyticsEvent( + String flowName, Long elapsedTime, User currentUser, Artifact artifact) { + GitArtifactMetadata gitArtifactMetadata = artifact.getGitArtifactMetadata(); + + final Map data = Map.of( + FieldName.FLOW_NAME, + flowName, + FieldName.APPLICATION_ID, + gitArtifactMetadata.getDefaultArtifactId(), + "appId", + gitArtifactMetadata.getDefaultArtifactId(), + FieldName.BRANCH_NAME, + gitArtifactMetadata.getBranchName(), + "organizationId", + artifact.getWorkspaceId(), + "repoUrl", + gitArtifactMetadata.getRemoteUrl(), + "executionTime", + elapsedTime); + return analyticsService.sendEvent( + AnalyticsEvents.UNIT_EXECUTION_TIME.getEventName(), currentUser.getUsername(), data); + } } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/migrations/db/ce/Migration064AddPluginDocUrlToRestApiPlugin.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/migrations/db/ce/Migration064AddPluginDocUrlToRestApiPlugin.java new file mode 100644 index 000000000000..9fd1ffdf1cbe --- /dev/null +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/migrations/db/ce/Migration064AddPluginDocUrlToRestApiPlugin.java @@ -0,0 +1,33 @@ +package com.appsmith.server.migrations.db.ce; + +import com.appsmith.server.domains.Plugin; +import io.mongock.api.annotations.ChangeUnit; +import io.mongock.api.annotations.Execution; +import io.mongock.api.annotations.RollbackExecution; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.core.query.Criteria; +import org.springframework.data.mongodb.core.query.Query; +import org.springframework.data.mongodb.core.query.Update; + +@Slf4j +@ChangeUnit(order = "064", id = "add_plugin_doc_url_to_rest_api_plugin") +public class Migration064AddPluginDocUrlToRestApiPlugin { + + private final MongoTemplate mongoTemplate; + + public Migration064AddPluginDocUrlToRestApiPlugin(MongoTemplate mongoTemplate) { + this.mongoTemplate = mongoTemplate; + } + + @RollbackExecution + public void rollbackExecution() {} + + @Execution + public void execute() { + Query query = new Query().addCriteria(Criteria.where("packageName").is("restapi-plugin")); + Update update = + new Update().set("documentationLink", "https://docs.appsmith.com/connect-data/reference/rest-api"); + mongoTemplate.updateMulti(query, update, Plugin.class); + } +} diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/ce/CustomUserRepositoryCE.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/ce/CustomUserRepositoryCE.java index d071b5e04e7e..6ceef7aed4a5 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/ce/CustomUserRepositoryCE.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/ce/CustomUserRepositoryCE.java @@ -3,6 +3,7 @@ import com.appsmith.server.acl.AclPermission; import com.appsmith.server.domains.User; import com.appsmith.server.repositories.AppsmithRepository; +import org.springframework.data.mongodb.core.query.UpdateDefinition; import reactor.core.publisher.Mono; public interface CustomUserRepositoryCE extends AppsmithRepository { @@ -12,4 +13,6 @@ public interface CustomUserRepositoryCE extends AppsmithRepository { Mono findByEmailAndTenantId(String email, String tenantId); Mono isUsersEmpty(); + + Mono updateById(String id, UpdateDefinition updateObj); } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/ce/CustomUserRepositoryCEImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/ce/CustomUserRepositoryCEImpl.java index 8508431a7257..b09ac27cc259 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/ce/CustomUserRepositoryCEImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/ce/CustomUserRepositoryCEImpl.java @@ -3,11 +3,14 @@ import com.appsmith.server.acl.AclPermission; import com.appsmith.server.constants.FieldName; import com.appsmith.server.domains.User; +import com.appsmith.server.exceptions.AppsmithError; +import com.appsmith.server.exceptions.AppsmithException; import com.appsmith.server.helpers.ce.bridge.Bridge; import com.appsmith.server.helpers.ce.bridge.BridgeQuery; import com.appsmith.server.projections.IdOnly; import com.appsmith.server.repositories.BaseAppsmithRepositoryImpl; import lombok.extern.slf4j.Slf4j; +import org.springframework.data.mongodb.core.query.UpdateDefinition; import reactor.core.publisher.Mono; import java.util.HashSet; @@ -50,4 +53,12 @@ protected Set getSystemGeneratedUserEmails() { systemGeneratedEmails.add(FieldName.ANONYMOUS_USER); return systemGeneratedEmails; } + + @Override + public Mono updateById(String id, UpdateDefinition updateObj) { + if (id == null) { + return Mono.error(new AppsmithException(AppsmithError.INVALID_PARAMETER, FieldName.ID)); + } + return queryBuilder().byId(id).updateFirst(updateObj); + } } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/UsagePulseServiceCEImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/UsagePulseServiceCEImpl.java index 1d15be8c9007..55027b06933b 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/UsagePulseServiceCEImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/UsagePulseServiceCEImpl.java @@ -7,6 +7,8 @@ import com.appsmith.server.dtos.UsagePulseDTO; import com.appsmith.server.exceptions.AppsmithError; import com.appsmith.server.exceptions.AppsmithException; +import com.appsmith.server.helpers.ce.bridge.Bridge; +import com.appsmith.server.helpers.ce.bridge.BridgeUpdate; import com.appsmith.server.repositories.UsagePulseRepository; import com.appsmith.server.services.ConfigService; import com.appsmith.server.services.SessionUserService; @@ -83,19 +85,21 @@ public Mono createPulse(UsagePulseDTO usagePulseDTO) { return save(usagePulse); } usagePulse.setIsAnonymousUser(false); - User updateUser = new User(); + BridgeUpdate updateUserObj = Bridge.update(); + String hashedEmail = user.getHashedEmail(); if (StringUtils.isEmpty(hashedEmail)) { hashedEmail = DigestUtils.sha256Hex(user.getEmail()); // Hashed user email is stored to user for future mapping of user and pulses - updateUser.setHashedEmail(hashedEmail); + updateUserObj.set(User.Fields.hashedEmail, hashedEmail); } usagePulse.setUser(hashedEmail); - updateUser.setLastActiveAt(Instant.now()); - // Avoid updating policies - updateUser.setPolicies(null); - return userService.updateWithoutPermission(user.getId(), updateUser).then(save(usagePulse)); + updateUserObj.set(User.Fields.lastActiveAt, Instant.now()); + + return userService + .updateWithoutPermission(user.getId(), updateUserObj) + .then(save(usagePulse)); }); } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/UserServiceCE.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/UserServiceCE.java index 3c489065891f..2bdb85f67803 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/UserServiceCE.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/UserServiceCE.java @@ -8,6 +8,7 @@ import com.appsmith.server.dtos.UserSignupDTO; import com.appsmith.server.dtos.UserUpdateDTO; import com.appsmith.server.services.CrudService; +import org.springframework.data.mongodb.core.query.UpdateDefinition; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -30,6 +31,8 @@ public interface UserServiceCE extends CrudService { Mono userCreate(User user, boolean isAdminUser); + Mono updateWithoutPermission(String id, UpdateDefinition updateObj); + Mono updateCurrentUser(UserUpdateDTO updates, ServerWebExchange exchange); Mono isUsersEmpty(); diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/UserServiceCEImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/UserServiceCEImpl.java index 9cede7c2f620..35d24c352aa4 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/UserServiceCEImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/UserServiceCEImpl.java @@ -43,6 +43,7 @@ import org.apache.hc.core5.http.message.BasicNameValuePair; import org.apache.hc.core5.net.WWWFormCodec; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.mongodb.core.query.UpdateDefinition; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.ReactiveSecurityContextHolder; @@ -568,6 +569,15 @@ public Mono updateWithoutPermission(String id, User update) { return userFromRepository.flatMap(existingUser -> this.update(existingUser, update)); } + @Override + public Mono updateWithoutPermission(String id, UpdateDefinition updateObj) { + Mono userFromRepository = repository + .findById(id) + .switchIfEmpty(Mono.error(new AppsmithException(AppsmithError.NO_RESOURCE_FOUND, FieldName.USER, id))); + + return userFromRepository.flatMap(existingUser -> repository.updateById(id, updateObj)); + } + private Mono update(User existingUser, User userUpdate) { // The password is being updated. Hash it first and then store it