chore: Add code-split for OPL with UI module instances (#40749)

## Description
EE Counterpart PR: https://github.com/appsmithorg/appsmith-ee/pull/7559


Fixes #`Issue Number`  
_or_  
Fixes `Issue URL`
> [!WARNING]  
> _If no issue exists, please create an issue first, and check with the
maintainers if the issue is valid._

## Automation

/ok-to-test tags="@tag.All"

### 🔍 Cypress test results
<!-- This is an auto-generated comment: Cypress test results  -->
> [!TIP]
> 🟢 🟢 🟢 All cypress tests have passed! 🎉 🎉 🎉
> Workflow run:
<https://github.com/appsmithorg/appsmith/actions/runs/15253633122>
> Commit: dd70a1c673da72ef7e5bdea172b90d7ec338f41a
> <a
href="https://internal.appsmith.com/app/cypress-dashboard/rundetails-65890b3c81d7400d08fa9ee5?branch=master&workflowId=15253633122&attempt=2"
target="_blank">Cypress dashboard</a>.
> Tags: `@tag.All`
> Spec:
> <hr>Mon, 26 May 2025 13:55:15 UTC
<!-- end of auto-generated comment: Cypress test results  -->


## Communication
Should the DevRel and Marketing teams inform users about this change?
- [ ] Yes
- [x] No


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **Refactor**
- Enhanced layout update processing by modularizing synthetic widget
handling, enabling future UI-specific widget injection and removal. No
direct impact on user-facing features.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
subratadeypappu 2025-05-26 23:53:13 +06:00 committed by GitHub
parent 678df9c4aa
commit 56d296afc8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -24,6 +24,7 @@ import com.appsmith.server.services.SessionUserService;
import com.appsmith.server.solutions.PagePermission; import com.appsmith.server.solutions.PagePermission;
import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.Sets;
import io.micrometer.observation.ObservationRegistry; import io.micrometer.observation.ObservationRegistry;
import io.micrometer.tracing.Span; import io.micrometer.tracing.Span;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
@ -43,6 +44,7 @@ import java.util.HashSet;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReference;
import java.util.regex.Pattern; import java.util.regex.Pattern;
@ -109,6 +111,12 @@ public class UpdateLayoutServiceCEImpl implements UpdateLayoutServiceCE {
}); });
} }
// Extension point for UI module handling
protected Mono<Map.Entry<JSONObject, Optional<Set<String>>>> processDslAndCreateSyntheticWidgets(
String creatorId, CreatorContextType creatorType, JSONObject dsl, ArrayList<Object> mainChildren) {
return Mono.just(Map.entry(dsl, Optional.empty()));
}
protected Mono<LayoutDTO> updateLayoutDsl( protected Mono<LayoutDTO> updateLayoutDsl(
String creatorId, String creatorId,
String layoutId, String layoutId,
@ -121,122 +129,162 @@ public class UpdateLayoutServiceCEImpl implements UpdateLayoutServiceCE {
return Mono.just(generateResponseDTO(layout)); return Mono.just(generateResponseDTO(layout));
} }
Set<String> widgetNames = new HashSet<>(); // Get the main DSL's children array. We *don't* create an empty list
Map<String, Set<String>> widgetDynamicBindingsMap = new HashMap<>(); // yet; if no UImodule instances are injected we want to leave the DSL
Set<String> escapedWidgetNames = new HashSet<>(); // unchanged so tests that expect a verbatim payload pass.
ArrayList<Object> mainChildren = (ArrayList<Object>) dsl.get(FieldName.CHILDREN);
Span extractAllWidgetNamesAndDynamicBindingsFromDSLSpan = // Process DSL with module instances if any
observationHelper.createSpan(EXTRACT_ALL_WIDGET_NAMES_AND_DYNAMIC_BINDINGS_FROM_DSL); return processDslAndCreateSyntheticWidgets(creatorId, creatorType, dsl, mainChildren)
.flatMap(entry -> {
JSONObject processedDsl = entry.getKey();
Optional<Set<String>> syntheticWidgetNamesOpt = entry.getValue();
observationHelper.startSpan(extractAllWidgetNamesAndDynamicBindingsFromDSLSpan); Set<String> widgetNames = new HashSet<>();
Map<String, Set<String>> widgetDynamicBindingsMap = new HashMap<>();
Set<String> escapedWidgetNames = new HashSet<>();
try { Span extractAllWidgetNamesAndDynamicBindingsFromDSLSpan =
dsl = extractAllWidgetNamesAndDynamicBindingsFromDSL( observationHelper.createSpan(EXTRACT_ALL_WIDGET_NAMES_AND_DYNAMIC_BINDINGS_FROM_DSL);
dsl, widgetNames, widgetDynamicBindingsMap, creatorId, layoutId, escapedWidgetNames, creatorType);
} catch (Throwable t) { observationHelper.startSpan(extractAllWidgetNamesAndDynamicBindingsFromDSLSpan);
return sendUpdateLayoutAnalyticsEvent(creatorId, layoutId, dsl, false, t, creatorType)
.then(Mono.error(t));
}
observationHelper.endSpan(extractAllWidgetNamesAndDynamicBindingsFromDSLSpan); try {
processedDsl = extractAllWidgetNamesAndDynamicBindingsFromDSL(
layout.setWidgetNames(widgetNames); processedDsl,
widgetNames,
if (!escapedWidgetNames.isEmpty()) { widgetDynamicBindingsMap,
layout.setMongoEscapedWidgetNames(escapedWidgetNames); creatorId,
} layoutId,
escapedWidgetNames,
Set<String> executableNames = new HashSet<>(); creatorType);
Set<ExecutableDependencyEdge> edges = new HashSet<>(); } catch (Throwable t) {
Set<String> executablesUsedInDSL = new HashSet<>(); return sendUpdateLayoutAnalyticsEvent(creatorId, layoutId, processedDsl, false, t, creatorType)
List<Executable> flatmapOnLoadExecutables = new ArrayList<>(); .then(Mono.error(t));
List<LayoutExecutableUpdateDTO> executableUpdatesRef = new ArrayList<>();
List<String> messagesRef = new ArrayList<>();
AtomicReference<Boolean> validOnLoadExecutables = new AtomicReference<>(Boolean.TRUE);
// setting the layoutOnLoadActionActionErrors to empty to remove the existing
// errors before new DAG calculation.
layout.setLayoutOnLoadActionErrors(new ArrayList<>());
Mono<List<Set<DslExecutableDTO>>> allOnLoadExecutablesMono = onLoadExecutablesUtil
.findAllOnLoadExecutables(
creatorId,
evaluatedVersion,
widgetNames,
edges,
widgetDynamicBindingsMap,
flatmapOnLoadExecutables,
executablesUsedInDSL,
creatorType)
.name(FIND_ALL_ON_LOAD_EXECUTABLES)
.tap(Micrometer.observation(observationRegistry))
.onErrorResume(AppsmithException.class, error -> {
log.info(error.getMessage());
validOnLoadExecutables.set(FALSE);
layout.setLayoutOnLoadActionErrors(List.of(new ErrorDTO(
error.getAppErrorCode(),
error.getErrorType(),
layoutOnLoadActionErrorToastMessage,
error.getMessage(),
error.getTitle())));
return Mono.just(new ArrayList<>());
});
// First update the actions and set execute on load to true
JSONObject finalDsl = dsl;
Mono<LayoutDTO> layoutDTOMono = allOnLoadExecutablesMono
.flatMap(allOnLoadExecutables -> {
// If there has been an error (e.g. cyclical dependency), then don't update any
// actions.
// This is so that unnecessary updates don't happen to actions while the page is
// in invalid state.
if (!validOnLoadExecutables.get()) {
return Mono.just(allOnLoadExecutables);
} }
// Update these executables to be executed on load, unless the user has touched
// the runBehaviour observationHelper.endSpan(extractAllWidgetNamesAndDynamicBindingsFromDSLSpan);
// setting for this
return onLoadExecutablesUtil // -----------------------------------------------------------------------------
.updateExecutablesRunBehaviour( // If we have synthetic widgets, filter them out and scrub the DSL
flatmapOnLoadExecutables, creatorId, executableUpdatesRef, messagesRef, creatorType) // -----------------------------------------------------------------------------
.name(UPDATE_EXECUTABLES_RUN_BEHAVIOUR) if (syntheticWidgetNamesOpt.isPresent()) {
Set<String> syntheticWidgetNames = syntheticWidgetNamesOpt.get();
Set<String> widgetNamesToPersist = Sets.difference(widgetNames, syntheticWidgetNames);
processedDsl = stripSyntheticWidgets(processedDsl, syntheticWidgetNamesOpt);
removeSpecialCharactersFromKeys(processedDsl, escapedWidgetNames);
layout.setWidgetNames(widgetNamesToPersist);
} else {
layout.setWidgetNames(widgetNames);
}
layout.setDsl(processedDsl);
// We always attach escaped names when we have them
if (!escapedWidgetNames.isEmpty()) {
layout.setMongoEscapedWidgetNames(escapedWidgetNames);
}
Set<String> executableNames = new HashSet<>();
Set<ExecutableDependencyEdge> edges = new HashSet<>();
Set<String> executablesUsedInDSL = new HashSet<>();
List<Executable> flatmapOnLoadExecutables = new ArrayList<>();
List<LayoutExecutableUpdateDTO> executableUpdatesRef = new ArrayList<>();
List<String> messagesRef = new ArrayList<>();
AtomicReference<Boolean> validOnLoadExecutables = new AtomicReference<>(Boolean.TRUE);
// setting the layoutOnLoadActionActionErrors to empty to remove the existing
// errors before new DAG calculation.
layout.setLayoutOnLoadActionErrors(new ArrayList<>());
Mono<List<Set<DslExecutableDTO>>> allOnLoadExecutablesMono = onLoadExecutablesUtil
.findAllOnLoadExecutables(
creatorId,
evaluatedVersion,
widgetNames,
edges,
widgetDynamicBindingsMap,
flatmapOnLoadExecutables,
executablesUsedInDSL,
creatorType)
.name(FIND_ALL_ON_LOAD_EXECUTABLES)
.tap(Micrometer.observation(observationRegistry)) .tap(Micrometer.observation(observationRegistry))
.thenReturn(allOnLoadExecutables); .onErrorResume(AppsmithException.class, error -> {
}) log.info(error.getMessage());
// Now update the page layout with the page load executables and the graph. validOnLoadExecutables.set(FALSE);
.flatMap(onLoadExecutables -> { layout.setLayoutOnLoadActionErrors(List.of(new ErrorDTO(
layout.setLayoutOnLoadActions(onLoadExecutables); error.getAppErrorCode(),
layout.setAllOnPageLoadActionNames(executableNames); error.getErrorType(),
layout.setActionsUsedInDynamicBindings(executablesUsedInDSL); layoutOnLoadActionErrorToastMessage,
// The below field is to ensure that we record if the page load actions error.getMessage(),
// computation was error.getTitle())));
// valid when last stored in the database. return Mono.just(new ArrayList<>());
layout.setValidOnPageLoadActions(validOnLoadExecutables.get()); });
return onLoadExecutablesUtil // First update the actions and set execute on load to true
.findAndUpdateLayout(creatorId, creatorType, layoutId, layout) JSONObject finalDsl = processedDsl;
.tag("no_of_widgets", String.valueOf(widgetNames.size()))
.tag("no_of_executables", String.valueOf(executableNames.size()))
.name(FIND_AND_UPDATE_LAYOUT)
.tap(Micrometer.observation(observationRegistry));
})
.map(savedLayout -> {
savedLayout.setDsl(this.unescapeMongoSpecialCharacters(savedLayout));
return savedLayout;
})
.flatMap(savedLayout -> {
LayoutDTO layoutDTO = generateResponseDTO(savedLayout);
layoutDTO.setActionUpdates(executableUpdatesRef);
layoutDTO.setMessages(messagesRef);
return sendUpdateLayoutAnalyticsEvent(creatorId, layoutId, finalDsl, true, null, creatorType) return allOnLoadExecutablesMono
.thenReturn(layoutDTO); .flatMap(allOnLoadExecutables -> {
// If there has been an error (e.g. cyclical dependency), then don't update any
// actions.
// This is so that unnecessary updates don't happen to actions while the page is
// in invalid state.
if (!validOnLoadExecutables.get()) {
return Mono.just(allOnLoadExecutables);
}
// Update these executables to be executed on load, unless the user has touched
// the runBehaviour
// setting for this
return onLoadExecutablesUtil
.updateExecutablesRunBehaviour(
flatmapOnLoadExecutables,
creatorId,
executableUpdatesRef,
messagesRef,
creatorType)
.name(UPDATE_EXECUTABLES_RUN_BEHAVIOUR)
.tap(Micrometer.observation(observationRegistry))
.thenReturn(allOnLoadExecutables);
})
// Now update the page layout with the page load executables and the graph.
.flatMap(onLoadExecutables -> {
layout.setLayoutOnLoadActions(onLoadExecutables);
layout.setAllOnPageLoadActionNames(executableNames);
layout.setActionsUsedInDynamicBindings(executablesUsedInDSL);
// The below field is to ensure that we record if the page load actions
// computation was
// valid when last stored in the database.
return onLoadExecutablesUtil
.findAndUpdateLayout(creatorId, creatorType, layoutId, layout)
.tag("no_of_widgets", String.valueOf(widgetNames.size()))
.tag("no_of_executables", String.valueOf(executableNames.size()))
.name(FIND_AND_UPDATE_LAYOUT)
.tap(Micrometer.observation(observationRegistry));
})
.map(savedLayout -> {
savedLayout.setDsl(this.unescapeMongoSpecialCharacters(savedLayout));
return savedLayout;
})
.flatMap(savedLayout -> {
LayoutDTO layoutDTO = generateResponseDTO(savedLayout);
layoutDTO.setActionUpdates(executableUpdatesRef);
layoutDTO.setMessages(messagesRef);
return sendUpdateLayoutAnalyticsEvent(
creatorId, layoutId, finalDsl, true, null, creatorType)
.thenReturn(layoutDTO);
});
}); });
}
return layoutDTOMono; protected JSONObject stripSyntheticWidgets(JSONObject dsl, Optional<Set<String>> syntheticNamesOpt) {
return dsl;
} }
// TODO: Add contextType and change all its usage to conform to that so that we can get rid of the overloaded // TODO: Add contextType and change all its usage to conform to that so that we can get rid of the overloaded