From 3ff203ec44b9ef24734e36ea24741ffc5173a93f Mon Sep 17 00:00:00 2001 From: Stacey Levine Date: Fri, 17 Jul 2026 16:39:57 -0400 Subject: [PATCH 1/3] feat: block new Appsmith AI datasource creation and show deprecation banner MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Appsmith AI (appsmithai-plugin) is being deprecated in 2.3 to stop Appsmith-paid managed AI spend; users migrate to the BYOK OpenAI / Anthropic / Google AI datasources. Server: - New DEPRECATED_DATASOURCE_PLUGIN error (400, AE-DTS-4031). - DatasourceServiceCEImpl.create() rejects brand-new datasources whose plugin package is in getDeprecatedPluginPackageNames() (protected seam so EE can add appsmith-agent-plugin). Storage-saves for existing datasources (id present) are unaffected. - create()'s old body moved to createWithoutDeprecationCheck(); the fork service uses it so forking apps with existing Appsmith AI datasources keeps working. Import (createWithoutPermissions) is untouched and therefore exempt by construction — deliberate product decision so existing apps keep importing until the T+60 credential shutoff. Client: - Appsmith AI removed from the AI Integrations create-new list; other AI plugins remain. - New AppsmithAIDeprecationCallout (warning, kill date September 30, 2026, migration guidance incl. file-context loss, docs link) shown on the datasource editor (view + edit) and Appsmith AI query editor. Tests: 3 new DatasourceServiceTest cases (create rejected, bypass method succeeds, existing-datasource storage-save allowed); jest tests for the callout and the create-list filter. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01AN5jb73MFwozkWuB4jvs6W --- .../components/UQIEditor/UQIEditorForm.tsx | 11 ++- app/client/src/ce/constants/messages.ts | 5 + .../AppsmithAIDeprecationCallout.test.tsx | 43 +++++++++ .../AppsmithAIDeprecationCallout.tsx | 39 ++++++++ .../src/constants/DocumentationLinks.ts | 4 + .../pages/Editor/DataSourceEditor/index.tsx | 6 ++ .../Editor/IntegrationEditor/AIPlugins.tsx | 9 +- .../__tests__/aiPlugins.test.tsx | 47 +++++++++ .../datasources/base/DatasourceServiceCE.java | 6 ++ .../base/DatasourceServiceCEImpl.java | 30 ++++++ .../fork/DatasourceForkableServiceCEImpl.java | 20 ++-- .../server/exceptions/AppsmithError.java | 9 ++ .../server/exceptions/AppsmithErrorCode.java | 1 + .../services/DatasourceServiceTest.java | 95 +++++++++++++++++++ 14 files changed, 311 insertions(+), 14 deletions(-) create mode 100644 app/client/src/components/editorComponents/AppsmithAIDeprecationCallout.test.tsx create mode 100644 app/client/src/components/editorComponents/AppsmithAIDeprecationCallout.tsx create mode 100644 app/client/src/pages/Editor/IntegrationEditor/__tests__/aiPlugins.test.tsx diff --git a/app/client/src/PluginActionEditor/components/PluginActionForm/components/UQIEditor/UQIEditorForm.tsx b/app/client/src/PluginActionEditor/components/PluginActionForm/components/UQIEditor/UQIEditorForm.tsx index b4a6ea2adc5d..602562fc26b7 100644 --- a/app/client/src/PluginActionEditor/components/PluginActionForm/components/UQIEditor/UQIEditorForm.tsx +++ b/app/client/src/PluginActionEditor/components/PluginActionForm/components/UQIEditor/UQIEditorForm.tsx @@ -4,14 +4,14 @@ import { usePluginActionContext } from "../../../../PluginActionContext"; import { QUERY_EDITOR_FORM_NAME } from "ee/constants/forms"; import { reduxForm } from "redux-form"; import { Flex } from "@appsmith/ads"; +import { PluginPackageName } from "entities/Plugin"; +import AppsmithAIDeprecationCallout from "components/editorComponents/AppsmithAIDeprecationCallout"; import { useGoogleSheetsSetDefaultProperty } from "./hooks/useGoogleSheetsSetDefaultProperty"; import { useFormData } from "./hooks/useFormData"; const UQIEditorForm = () => { - const { - editorConfig, - plugin: { uiComponent }, - } = usePluginActionContext(); + const { editorConfig, plugin } = usePluginActionContext(); + const { uiComponent } = plugin; // Set default values for Google Sheets useGoogleSheetsSetDefaultProperty(); @@ -29,6 +29,9 @@ const UQIEditorForm = () => { flexDirection="column" w="100%" > + {plugin.packageName === PluginPackageName.APPSMITH_AI && ( + + )} "GraphQL API"; export const CREATE_NEW_API_SECTION_HEADER = () => "APIs"; export const CREATE_NEW_SAAS_SECTION_HEADER = () => "SaaS Integrations"; export const CREATE_NEW_AI_SECTION_HEADER = () => "AI Integrations"; +// Appsmith AI deprecation (release 2.3): update this date if the shutdown date moves +export const APPSMITH_AI_KILL_DATE = "September 30, 2026"; +export const APPSMITH_AI_DEPRECATION_MESSAGE = () => + `Appsmith AI is deprecated and will stop working on ${APPSMITH_AI_KILL_DATE}. Migrate to OpenAI, Anthropic, or Google AI with your own API key. Uploaded files (file context) will not be available after this date.`; +export const APPSMITH_AI_DEPRECATION_LEARN_MORE = () => "Learn how to migrate"; export const CONNECT_A_DATASOURCE_HEADING = () => "Connect a datasource"; export const CONNECT_A_DATASOURCE_SUBHEADING = () => "Select a sample datasource or connect your own"; diff --git a/app/client/src/components/editorComponents/AppsmithAIDeprecationCallout.test.tsx b/app/client/src/components/editorComponents/AppsmithAIDeprecationCallout.test.tsx new file mode 100644 index 000000000000..3827d2d61790 --- /dev/null +++ b/app/client/src/components/editorComponents/AppsmithAIDeprecationCallout.test.tsx @@ -0,0 +1,43 @@ +import React from "react"; +import { render, screen } from "@testing-library/react"; +import "@testing-library/jest-dom"; +import { ThemeProvider } from "styled-components"; +import { BrowserRouter as Router } from "react-router-dom"; +import { lightTheme } from "selectors/themeSelectors"; +import AppsmithAIDeprecationCallout from "./AppsmithAIDeprecationCallout"; +import { + APPSMITH_AI_DEPRECATION_LEARN_MORE, + APPSMITH_AI_KILL_DATE, + createMessage, +} from "ee/constants/messages"; + +const renderCallout = () => + render( + + + + + , + ); + +describe("AppsmithAIDeprecationCallout", () => { + it("renders the deprecation message with the kill date", () => { + renderCallout(); + + expect( + screen.getByTestId("t--appsmith-ai-deprecation-callout"), + ).toBeInTheDocument(); + expect(screen.getByText(/Appsmith AI is deprecated/i)).toBeInTheDocument(); + expect( + screen.getByText(new RegExp(APPSMITH_AI_KILL_DATE)), + ).toBeInTheDocument(); + }); + + it("renders the migration docs link", () => { + renderCallout(); + + expect( + screen.getByText(createMessage(APPSMITH_AI_DEPRECATION_LEARN_MORE)), + ).toBeInTheDocument(); + }); +}); diff --git a/app/client/src/components/editorComponents/AppsmithAIDeprecationCallout.tsx b/app/client/src/components/editorComponents/AppsmithAIDeprecationCallout.tsx new file mode 100644 index 000000000000..6d261ce99ead --- /dev/null +++ b/app/client/src/components/editorComponents/AppsmithAIDeprecationCallout.tsx @@ -0,0 +1,39 @@ +import React from "react"; +import styled from "styled-components"; +import { Callout } from "@appsmith/ads"; +import { DocsLink, openDoc } from "constants/DocumentationLinks"; +import { + APPSMITH_AI_DEPRECATION_LEARN_MORE, + APPSMITH_AI_DEPRECATION_MESSAGE, + createMessage, +} from "ee/constants/messages"; + +const CalloutWrapper = styled.div` + width: 100%; + margin-bottom: var(--ads-v2-spaces-4); +`; + +/** + * Deprecation notice for the managed Appsmith AI plugin (release 2.3). + * Shown on existing Appsmith AI datasources and queries; creating new + * Appsmith AI datasources is blocked separately (client filter + server guard). + */ +function AppsmithAIDeprecationCallout() { + return ( + + openDoc(DocsLink.APPSMITH_AI_DEPRECATION), + }, + ]} + > + {createMessage(APPSMITH_AI_DEPRECATION_MESSAGE)} + + + ); +} + +export default AppsmithAIDeprecationCallout; diff --git a/app/client/src/constants/DocumentationLinks.ts b/app/client/src/constants/DocumentationLinks.ts index 1083c4b2a800..717dcb96b6ff 100644 --- a/app/client/src/constants/DocumentationLinks.ts +++ b/app/client/src/constants/DocumentationLinks.ts @@ -7,6 +7,7 @@ export enum DocsLink { QUERY = "QUERY", TROUBLESHOOT_ERROR = "TROUBLESHOOT_ERROR", QUERY_SETTINGS = "QUERY_SETTINGS", + APPSMITH_AI_DEPRECATION = "APPSMITH_AI_DEPRECATION", } const LinkData: Record = { @@ -22,6 +23,9 @@ const LinkData: Record = { "https://docs.appsmith.com/help-and-support/troubleshooting-guide", QUERY_SETTINGS: "https://docs.appsmith.com/connect-data/reference/query-settings", + // TODO: replace with the dedicated Appsmith AI → BYOK migration guide once published + APPSMITH_AI_DEPRECATION: + "https://docs.appsmith.com/connect-data/reference/appsmith-ai", }; export const openDoc = (type: DocsLink, link?: string, subType?: string) => { diff --git a/app/client/src/pages/Editor/DataSourceEditor/index.tsx b/app/client/src/pages/Editor/DataSourceEditor/index.tsx index a34c19573a87..90877659194b 100644 --- a/app/client/src/pages/Editor/DataSourceEditor/index.tsx +++ b/app/client/src/pages/Editor/DataSourceEditor/index.tsx @@ -85,6 +85,7 @@ import { formValuesToDatasource } from "PluginActionEditor/transformers/RestAPID import { DSFormHeader } from "./DSFormHeader"; import type { PluginType } from "entities/Plugin"; import { DatasourceComponentTypes, PluginPackageName } from "entities/Plugin"; +import AppsmithAIDeprecationCallout from "components/editorComponents/AppsmithAIDeprecationCallout"; import DSDataFilter from "ee/components/DSDataFilter"; import { DEFAULT_ENV_ID } from "ee/api/ApiUtils"; import { isStorageEnvironmentCreated } from "ee/utils/Environments"; @@ -1031,6 +1032,11 @@ class DatasourceEditorRouter extends React.Component { showingTabsOnViewMode && "db-form-resizer-content-show-tabs" }`} > + {/* Mounted above DSEditorWrapper: the wrapper is a row flex, so a + child callout would render as a side column instead of a banner */} + {pluginPackageName === PluginPackageName.APPSMITH_AI && ( + + )} { // Sort the AI plugins alphabetically return a.name.localeCompare(b.name); }) - .filter((plugin) => plugin.type === PluginType.AI), + .filter( + (plugin) => + plugin.type === PluginType.AI && + // Appsmith AI is deprecated — creating new datasources for it is blocked + plugin.packageName !== PluginPackageName.APPSMITH_AI, + ), searchedPlugin, ) as Plugin[]; diff --git a/app/client/src/pages/Editor/IntegrationEditor/__tests__/aiPlugins.test.tsx b/app/client/src/pages/Editor/IntegrationEditor/__tests__/aiPlugins.test.tsx new file mode 100644 index 000000000000..17c213db9a6e --- /dev/null +++ b/app/client/src/pages/Editor/IntegrationEditor/__tests__/aiPlugins.test.tsx @@ -0,0 +1,47 @@ +import React from "react"; +import { ReduxActionTypes } from "ee/constants/ReduxActionConstants"; +import store from "store"; +import { render } from "test/testUtils"; +import { PluginPackageName } from "entities/Plugin"; +import AIPlugins from "../AIPlugins"; + +const aiPlugins = [ + { + id: "appsmith-ai-plugin-id", + name: "Appsmith AI", + type: "AI", + packageName: PluginPackageName.APPSMITH_AI, + iconLocation: "", + }, + { + id: "openai-plugin-id", + name: "OpenAI", + type: "AI", + packageName: "openai-plugin", + iconLocation: "", + }, +]; + +describe("AIPlugins create-new list", () => { + it("hides deprecated Appsmith AI but keeps other AI plugins", () => { + store.dispatch({ + type: ReduxActionTypes.FETCH_PLUGINS_SUCCESS, + payload: aiPlugins, + }); + + const { container } = render( + {}} />, + ); + + // BYOK AI plugins must remain creatable + expect( + container.querySelector(".t--createBlankApi-openai-plugin"), + ).not.toBeNull(); + // The deprecated managed Appsmith AI plugin must not be offered for creation + expect( + container.querySelector( + `.t--createBlankApi-${PluginPackageName.APPSMITH_AI}`, + ), + ).toBeNull(); + }); +}); diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/datasources/base/DatasourceServiceCE.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/datasources/base/DatasourceServiceCE.java index 7f88310d3185..1b810c015525 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/datasources/base/DatasourceServiceCE.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/datasources/base/DatasourceServiceCE.java @@ -65,6 +65,12 @@ public interface DatasourceServiceCE { Mono create(Datasource datasource); + /** + * Same as {@link #create(Datasource)} but skips the deprecated-plugin creation guard. Meant for internal flows + * (e.g. forking an application) that must keep working for datasources of deprecated plugins that already exist. + */ + Mono createWithoutDeprecationCheck(Datasource datasource); + Mono createWithoutPermissions(Datasource datasource); Mono createWithoutPermissions( diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/datasources/base/DatasourceServiceCEImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/datasources/base/DatasourceServiceCEImpl.java index 5fcedc47b575..eea8ed1ec01a 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/datasources/base/DatasourceServiceCEImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/datasources/base/DatasourceServiceCEImpl.java @@ -1,6 +1,7 @@ package com.appsmith.server.datasources.base; import com.appsmith.external.constants.AnalyticsEvents; +import com.appsmith.external.constants.PluginConstants; import com.appsmith.external.enums.FeatureFlagEnum; import com.appsmith.external.models.Datasource; import com.appsmith.external.models.DatasourceConfiguration; @@ -139,11 +140,40 @@ public DatasourceServiceCEImpl( @Override public Mono create(Datasource datasource) { + return validatePluginNotDeprecated(datasource) + .then(Mono.defer(() -> createWithoutDeprecationCheck(datasource))); + } + + @Override + public Mono createWithoutDeprecationCheck(Datasource datasource) { return workspacePermission .getDatasourceCreatePermission() .flatMap(permission -> createEx(datasource, permission, false, null)); } + /** + * Plugins for which creating new datasources is blocked because the plugin is deprecated. + * EE overrides this to extend the blocked list (e.g. appsmith-agent-plugin). + */ + protected Set getDeprecatedPluginPackageNames() { + return Set.of(PluginConstants.PackageName.APPSMITH_AI_PLUGIN); + } + + private Mono validatePluginNotDeprecated(Datasource datasource) { + // Only block brand-new datasources. Calls that carry an id are storage-saves for datasources that already + // exist and must keep working so users can keep using them until they migrate. A missing pluginId falls + // through to the INVALID_PARAMETER validation in createEx. + if (hasText(datasource.getId()) || !hasText(datasource.getPluginId())) { + return Mono.empty(); + } + + return pluginService + .findById(datasource.getPluginId()) + .filter(plugin -> getDeprecatedPluginPackageNames().contains(plugin.getPackageName())) + .flatMap(plugin -> Mono.error( + new AppsmithException(AppsmithError.DEPRECATED_DATASOURCE_PLUGIN, plugin.getName()))); + } + // TODO: Check usage @Override public Mono createWithoutPermissions( diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/datasources/fork/DatasourceForkableServiceCEImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/datasources/fork/DatasourceForkableServiceCEImpl.java index e9655431471d..64d163c00e74 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/datasources/fork/DatasourceForkableServiceCEImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/datasources/fork/DatasourceForkableServiceCEImpl.java @@ -157,13 +157,17 @@ private Mono createSuffixedDatasource(Datasource datasource) { private Mono createSuffixedDatasource(Datasource datasource, String name, int suffix) { final String actualName = name + (suffix == 0 ? "" : " (" + suffix + ")"); datasource.setName(actualName); - return datasourceService.create(datasource).onErrorResume(DuplicateKeyException.class, error -> { - if (error.getMessage() != null - && error.getMessage().contains("workspace_datasource_deleted_compound_index")) { - // The duplicate key error is because of the `name` field. - return createSuffixedDatasource(datasource, name, 1 + suffix); - } - throw error; - }); + // Forking must keep working for apps that already contain datasources of deprecated plugins, + // so skip the deprecated-plugin creation guard here. + return datasourceService + .createWithoutDeprecationCheck(datasource) + .onErrorResume(DuplicateKeyException.class, error -> { + if (error.getMessage() != null + && error.getMessage().contains("workspace_datasource_deleted_compound_index")) { + // The duplicate key error is because of the `name` field. + return createSuffixedDatasource(datasource, name, 1 + suffix); + } + throw error; + }); } } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/exceptions/AppsmithError.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/exceptions/AppsmithError.java index 4bf2a1efa211..9dbf4c614c09 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/exceptions/AppsmithError.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/exceptions/AppsmithError.java @@ -516,6 +516,15 @@ public enum AppsmithError { "Datasource cannot be deleted", ErrorType.BAD_REQUEST, null), + DEPRECATED_DATASOURCE_PLUGIN( + 400, + AppsmithErrorCode.DEPRECATED_DATASOURCE_PLUGIN.getCode(), + "Creating new datasources with the {0} plugin is not supported because the plugin is deprecated." + + " Please create an OpenAI, Anthropic, or Google AI datasource with your own API key instead.", + AppsmithErrorAction.DEFAULT, + "Plugin deprecated", + ErrorType.BAD_REQUEST, + null), WORKSPACE_ID_NOT_GIVEN( 400, AppsmithErrorCode.WORKSPACE_ID_NOT_GIVEN.getCode(), diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/exceptions/AppsmithErrorCode.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/exceptions/AppsmithErrorCode.java index 91f8a319badd..cc6fd35acc35 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/exceptions/AppsmithErrorCode.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/exceptions/AppsmithErrorCode.java @@ -82,6 +82,7 @@ public enum AppsmithErrorCode { INVALID_DATASOURCE("AE-DTS-4013", "Invalid datasource"), INVALID_DATASOURCE_CONFIGURATION("AE-DTS-4015", "Invalid datasource configuration"), DATASOURCE_HAS_ACTIONS("AE-DTS-4030", "Datasource has actions"), + DEPRECATED_DATASOURCE_PLUGIN("AE-DTS-4031", "Datasource creation is blocked for a deprecated plugin"), APPLICATION_FORKING_NOT_ALLOWED("AE-FRK-4034", "Application forking not allowed"), INVALID_GIT_CONFIGURATION("AE-GIT-4031", "Invalid git configuration"), INVALID_GIT_SSH_CONFIGURATION("AE-GIT-4032", "Invalid git ssh configuration"), diff --git a/app/server/appsmith-server/src/test/java/com/appsmith/server/services/DatasourceServiceTest.java b/app/server/appsmith-server/src/test/java/com/appsmith/server/services/DatasourceServiceTest.java index 06df6669fe29..6f1c8e02b8a0 100644 --- a/app/server/appsmith-server/src/test/java/com/appsmith/server/services/DatasourceServiceTest.java +++ b/app/server/appsmith-server/src/test/java/com/appsmith/server/services/DatasourceServiceTest.java @@ -1,5 +1,6 @@ package com.appsmith.server.services; +import com.appsmith.external.constants.PluginConstants; import com.appsmith.external.helpers.EncryptionHelper; import com.appsmith.external.models.ActionConfiguration; import com.appsmith.external.models.ActionDTO; @@ -12,6 +13,7 @@ import com.appsmith.external.models.DatasourceTestResult; import com.appsmith.external.models.Endpoint; import com.appsmith.external.models.OAuth2; +import com.appsmith.external.models.PluginType; import com.appsmith.external.models.Policy; import com.appsmith.external.models.SSLDetails; import com.appsmith.external.models.UploadedFile; @@ -34,6 +36,7 @@ import com.appsmith.server.plugins.base.PluginService; import com.appsmith.server.repositories.NewActionRepository; import com.appsmith.server.repositories.PermissionGroupRepository; +import com.appsmith.server.repositories.PluginRepository; import com.appsmith.server.repositories.WorkspaceRepository; import com.appsmith.server.solutions.ApplicationPermission; import com.appsmith.server.solutions.EnvironmentPermission; @@ -109,6 +112,9 @@ public class DatasourceServiceTest { @Autowired PermissionGroupRepository permissionGroupRepository; + @Autowired + PluginRepository pluginRepository; + @MockBean PluginExecutorHelper pluginExecutorHelper; @@ -294,6 +300,95 @@ public void createDatasourceWithId() { .verify(); } + private Plugin getOrCreateAppsmithAiPlugin() { + Plugin aiPlugin = pluginService + .findByPackageName(PluginConstants.PackageName.APPSMITH_AI_PLUGIN) + .block(); + + if (aiPlugin == null) { + aiPlugin = new Plugin(); + aiPlugin.setName(PluginConstants.PluginName.APPSMITH_AI_PLUGIN_NAME); + aiPlugin.setType(PluginType.AI); + aiPlugin.setPackageName(PluginConstants.PackageName.APPSMITH_AI_PLUGIN); + aiPlugin = pluginRepository.save(aiPlugin).block(); + } + + return aiPlugin; + } + + private Datasource buildAppsmithAiDatasource(Plugin aiPlugin, String name) { + Datasource datasource = new Datasource(); + datasource.setName(name); + datasource.setWorkspaceId(workspaceId); + datasource.setPluginId(aiPlugin.getId()); + HashMap storages = new HashMap<>(); + storages.put(defaultEnvironmentId, new DatasourceStorageDTO(null, defaultEnvironmentId, null)); + datasource.setDatasourceStorages(storages); + return datasource; + } + + @Test + @WithUserDetails(value = "api_user") + public void createDatasource_withDeprecatedAppsmithAiPlugin_throwsException() { + Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())) + .thenReturn(Mono.just(new MockPluginExecutor())); + + Plugin aiPlugin = getOrCreateAppsmithAiPlugin(); + Datasource datasource = buildAppsmithAiDatasource(aiPlugin, "Deprecated Appsmith AI DS"); + + StepVerifier.create(datasourceService.create(datasource)) + .expectErrorMatches(throwable -> throwable instanceof AppsmithException + && throwable + .getMessage() + .equals(AppsmithError.DEPRECATED_DATASOURCE_PLUGIN.getMessage(aiPlugin.getName()))) + .verify(); + } + + @Test + @WithUserDetails(value = "api_user") + public void createWithoutDeprecationCheck_withDeprecatedAppsmithAiPlugin_succeeds() { + Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())) + .thenReturn(Mono.just(new MockPluginExecutor())); + + Plugin aiPlugin = getOrCreateAppsmithAiPlugin(); + Datasource datasource = buildAppsmithAiDatasource(aiPlugin, "Existing Appsmith AI DS"); + + // This is the path fork uses; it must keep working for already-existing datasources. + StepVerifier.create(datasourceService.createWithoutDeprecationCheck(datasource)) + .assertNext(createdDatasource -> { + assertThat(createdDatasource.getId()).isNotEmpty(); + assertThat(createdDatasource.getPluginId()).isEqualTo(aiPlugin.getId()); + }) + .verifyComplete(); + } + + @Test + @WithUserDetails(value = "api_user") + public void createDatasource_existingAppsmithAiDatasource_storageSaveAllowed() { + Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())) + .thenReturn(Mono.just(new MockPluginExecutor())); + + Plugin aiPlugin = getOrCreateAppsmithAiPlugin(); + Datasource datasource = buildAppsmithAiDatasource(aiPlugin, "Existing Appsmith AI DS for storage save"); + + Datasource savedDatasource = + datasourceService.createWithoutDeprecationCheck(datasource).block(); + assertThat(savedDatasource).isNotNull(); + assertThat(savedDatasource.getId()).isNotEmpty(); + + // Calls that carry an id are storage-saves for existing datasources and must bypass the deprecation guard. + Datasource existingDatasource = new Datasource(); + existingDatasource.setId(savedDatasource.getId()); + existingDatasource.setWorkspaceId(workspaceId); + existingDatasource.setPluginId(aiPlugin.getId()); + existingDatasource.setDatasourceStorages(new HashMap<>(savedDatasource.getDatasourceStorages())); + + StepVerifier.create(datasourceService.create(existingDatasource)) + .assertNext( + resultDatasource -> assertThat(resultDatasource.getId()).isEqualTo(savedDatasource.getId())) + .verifyComplete(); + } + @Test @WithUserDetails(value = "api_user") public void createDatasourceNotInstalledPlugin() { From 2abbe4478610b4b69ab02e4ae68f6c597f7913a5 Mon Sep 17 00:00:00 2001 From: Stacey Levine Date: Mon, 20 Jul 2026 17:58:07 -0400 Subject: [PATCH 2/3] fix: relabel deprecation callout link to 'Learn more' and test link destination The BYOK migration guide is not yet published, so the callout link still points at the Appsmith AI reference page; 'Learn how to migrate' promised guidance that page doesn't have. Relabel to 'Learn more' until the guide ships (TODO tracks restoring the label), and assert the link opens DocsLink.APPSMITH_AI_DEPRECATION on click. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01WfztenqynzJcU2DZJD6nR5 --- app/client/src/ce/constants/messages.ts | 4 +++- .../AppsmithAIDeprecationCallout.test.tsx | 18 +++++++++++++++++- app/client/src/constants/DocumentationLinks.ts | 4 +++- 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/app/client/src/ce/constants/messages.ts b/app/client/src/ce/constants/messages.ts index 98a045010648..b3cecbcae95e 100644 --- a/app/client/src/ce/constants/messages.ts +++ b/app/client/src/ce/constants/messages.ts @@ -419,7 +419,9 @@ export const CREATE_NEW_AI_SECTION_HEADER = () => "AI Integrations"; export const APPSMITH_AI_KILL_DATE = "September 30, 2026"; export const APPSMITH_AI_DEPRECATION_MESSAGE = () => `Appsmith AI is deprecated and will stop working on ${APPSMITH_AI_KILL_DATE}. Migrate to OpenAI, Anthropic, or Google AI with your own API key. Uploaded files (file context) will not be available after this date.`; -export const APPSMITH_AI_DEPRECATION_LEARN_MORE = () => "Learn how to migrate"; +// TODO: restore "Learn how to migrate" once the BYOK migration guide is +// published and DocsLink.APPSMITH_AI_DEPRECATION points to it +export const APPSMITH_AI_DEPRECATION_LEARN_MORE = () => "Learn more"; export const CONNECT_A_DATASOURCE_HEADING = () => "Connect a datasource"; export const CONNECT_A_DATASOURCE_SUBHEADING = () => "Select a sample datasource or connect your own"; diff --git a/app/client/src/components/editorComponents/AppsmithAIDeprecationCallout.test.tsx b/app/client/src/components/editorComponents/AppsmithAIDeprecationCallout.test.tsx index 3827d2d61790..a7e1988b4dc0 100644 --- a/app/client/src/components/editorComponents/AppsmithAIDeprecationCallout.test.tsx +++ b/app/client/src/components/editorComponents/AppsmithAIDeprecationCallout.test.tsx @@ -1,16 +1,22 @@ import React from "react"; -import { render, screen } from "@testing-library/react"; +import { fireEvent, render, screen } from "@testing-library/react"; import "@testing-library/jest-dom"; import { ThemeProvider } from "styled-components"; import { BrowserRouter as Router } from "react-router-dom"; import { lightTheme } from "selectors/themeSelectors"; import AppsmithAIDeprecationCallout from "./AppsmithAIDeprecationCallout"; +import { DocsLink, openDoc } from "constants/DocumentationLinks"; import { APPSMITH_AI_DEPRECATION_LEARN_MORE, APPSMITH_AI_KILL_DATE, createMessage, } from "ee/constants/messages"; +jest.mock("constants/DocumentationLinks", () => ({ + ...jest.requireActual("constants/DocumentationLinks"), + openDoc: jest.fn(), +})); + const renderCallout = () => render( @@ -40,4 +46,14 @@ describe("AppsmithAIDeprecationCallout", () => { screen.getByText(createMessage(APPSMITH_AI_DEPRECATION_LEARN_MORE)), ).toBeInTheDocument(); }); + + it("opens the deprecation docs when the link is clicked", () => { + renderCallout(); + + fireEvent.click( + screen.getByText(createMessage(APPSMITH_AI_DEPRECATION_LEARN_MORE)), + ); + + expect(openDoc).toHaveBeenCalledWith(DocsLink.APPSMITH_AI_DEPRECATION); + }); }); diff --git a/app/client/src/constants/DocumentationLinks.ts b/app/client/src/constants/DocumentationLinks.ts index 717dcb96b6ff..5649ffe3e650 100644 --- a/app/client/src/constants/DocumentationLinks.ts +++ b/app/client/src/constants/DocumentationLinks.ts @@ -23,7 +23,9 @@ const LinkData: Record = { "https://docs.appsmith.com/help-and-support/troubleshooting-guide", QUERY_SETTINGS: "https://docs.appsmith.com/connect-data/reference/query-settings", - // TODO: replace with the dedicated Appsmith AI → BYOK migration guide once published + // TODO: replace with the dedicated Appsmith AI → BYOK migration guide once + // published, and restore the "Learn how to migrate" label in messages.ts + // (APPSMITH_AI_DEPRECATION_LEARN_MORE) APPSMITH_AI_DEPRECATION: "https://docs.appsmith.com/connect-data/reference/appsmith-ai", }; From e800ae19bddf0e0ca143bd6d7c68d73b27861907 Mon Sep 17 00:00:00 2001 From: Stacey Levine Date: Mon, 20 Jul 2026 18:19:48 -0400 Subject: [PATCH 3/3] fix: restore 'Learn how to migrate' label now that the docs URL hosts the migration guide appsmith-docs#3025 replaces the Appsmith AI reference page with a migration guide at the same URL, so the callout link label and target now match; merge that PR before or with this one. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01WfztenqynzJcU2DZJD6nR5 --- app/client/src/ce/constants/messages.ts | 4 +--- app/client/src/constants/DocumentationLinks.ts | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/app/client/src/ce/constants/messages.ts b/app/client/src/ce/constants/messages.ts index b3cecbcae95e..98a045010648 100644 --- a/app/client/src/ce/constants/messages.ts +++ b/app/client/src/ce/constants/messages.ts @@ -419,9 +419,7 @@ export const CREATE_NEW_AI_SECTION_HEADER = () => "AI Integrations"; export const APPSMITH_AI_KILL_DATE = "September 30, 2026"; export const APPSMITH_AI_DEPRECATION_MESSAGE = () => `Appsmith AI is deprecated and will stop working on ${APPSMITH_AI_KILL_DATE}. Migrate to OpenAI, Anthropic, or Google AI with your own API key. Uploaded files (file context) will not be available after this date.`; -// TODO: restore "Learn how to migrate" once the BYOK migration guide is -// published and DocsLink.APPSMITH_AI_DEPRECATION points to it -export const APPSMITH_AI_DEPRECATION_LEARN_MORE = () => "Learn more"; +export const APPSMITH_AI_DEPRECATION_LEARN_MORE = () => "Learn how to migrate"; export const CONNECT_A_DATASOURCE_HEADING = () => "Connect a datasource"; export const CONNECT_A_DATASOURCE_SUBHEADING = () => "Select a sample datasource or connect your own"; diff --git a/app/client/src/constants/DocumentationLinks.ts b/app/client/src/constants/DocumentationLinks.ts index 5649ffe3e650..66a88e80ee89 100644 --- a/app/client/src/constants/DocumentationLinks.ts +++ b/app/client/src/constants/DocumentationLinks.ts @@ -23,9 +23,7 @@ const LinkData: Record = { "https://docs.appsmith.com/help-and-support/troubleshooting-guide", QUERY_SETTINGS: "https://docs.appsmith.com/connect-data/reference/query-settings", - // TODO: replace with the dedicated Appsmith AI → BYOK migration guide once - // published, and restore the "Learn how to migrate" label in messages.ts - // (APPSMITH_AI_DEPRECATION_LEARN_MORE) + // This URL hosts the Appsmith AI → BYOK migration guide (appsmith-docs#3025) APPSMITH_AI_DEPRECATION: "https://docs.appsmith.com/connect-data/reference/appsmith-ai", };