From dfd7fdee97fc2afc3c08a6924f232c02a91f6874 Mon Sep 17 00:00:00 2001 From: Aman Agarwal Date: Thu, 9 Jan 2025 16:18:44 +0530 Subject: [PATCH] feat: datasource homepage ui redesign and search functionality for the datasources (#38360) --- .../e2e/Sanity/Datasources/Styles_spec.js | 12 +- .../cypress/locators/DatasourcesEditor.json | 2 +- .../cypress/support/Pages/DataSources.ts | 4 +- .../cypress/support/dataSourceCommands.js | 14 +- app/client/src/ce/constants/messages.ts | 23 +- .../Applications/CreateNewAppsOption.tsx | 30 +- .../IntegrationEditor/AIDataSources.tsx | 184 --------- .../Editor/IntegrationEditor/AIPlugins.tsx | 110 ++++++ .../IntegrationEditor/APIOrSaasPlugins.tsx | 340 +++++++++++++++++ .../AddDatasourceSecurely.tsx | 62 ++-- .../CreateNewDatasourceHeader.tsx | 71 ++++ .../CreateNewDatasourceTab.tsx | 302 +++------------ ...rceHome.tsx => DBOrMostPopularPlugins.tsx} | 318 ++++++++-------- .../IntegrationEditor/DatasourceItem.tsx | 64 ++++ .../EmptySearchedPlugins.tsx | 61 +++ .../IntegrationStyledComponents.tsx | 73 ++++ .../IntegrationEditor/MockDataSources.tsx | 173 ++++----- .../pages/Editor/IntegrationEditor/NewApi.tsx | 350 ------------------ .../Editor/IntegrationEditor/NewQuery.tsx | 65 ---- .../PremiumDatasources/Constants.ts} | 6 +- .../PremiumDatasources/ContactForm.tsx | 4 +- .../PremiumDatasources/Helpers.ts} | 2 +- .../PremiumDatasources/index.tsx | 40 +- 23 files changed, 1109 insertions(+), 1201 deletions(-) delete mode 100644 app/client/src/pages/Editor/IntegrationEditor/AIDataSources.tsx create mode 100644 app/client/src/pages/Editor/IntegrationEditor/AIPlugins.tsx create mode 100644 app/client/src/pages/Editor/IntegrationEditor/APIOrSaasPlugins.tsx create mode 100644 app/client/src/pages/Editor/IntegrationEditor/CreateNewDatasourceHeader.tsx rename app/client/src/pages/Editor/IntegrationEditor/{DatasourceHome.tsx => DBOrMostPopularPlugins.tsx} (50%) create mode 100644 app/client/src/pages/Editor/IntegrationEditor/DatasourceItem.tsx create mode 100644 app/client/src/pages/Editor/IntegrationEditor/EmptySearchedPlugins.tsx create mode 100644 app/client/src/pages/Editor/IntegrationEditor/IntegrationStyledComponents.tsx delete mode 100644 app/client/src/pages/Editor/IntegrationEditor/NewApi.tsx delete mode 100644 app/client/src/pages/Editor/IntegrationEditor/NewQuery.tsx rename app/client/src/{constants/PremiumDatasourcesConstants.ts => pages/Editor/IntegrationEditor/PremiumDatasources/Constants.ts} (77%) rename app/client/src/{utils/PremiumDatasourcesHelpers.ts => pages/Editor/IntegrationEditor/PremiumDatasources/Helpers.ts} (97%) diff --git a/app/client/cypress/e2e/Sanity/Datasources/Styles_spec.js b/app/client/cypress/e2e/Sanity/Datasources/Styles_spec.js index c98c1b3dc1f3..d3c3021843ac 100644 --- a/app/client/cypress/e2e/Sanity/Datasources/Styles_spec.js +++ b/app/client/cypress/e2e/Sanity/Datasources/Styles_spec.js @@ -36,8 +36,6 @@ describe( ); //mock datasource image cy.datasourceImageStyle("[data-testid=mock-datasource-image]"); - //header text - cy.datasourceContentWrapperStyle(".t--datasource-name"); //Name wrapper cy.get("[data-testid=mock-datasource-name-wrapper]") .should("have.css", "display", "flex") @@ -61,13 +59,9 @@ describe( "[data-testid=database-datasource-content-wrapper]", ); //Icon wrapper - cy.datasourceIconWrapperStyle( - "[data-testid=database-datasource-content-wrapper] .dataSourceImage", - ); + cy.datasourceIconWrapperStyle("[data-testid=database-datasource-image]"); //Name - cy.datasourceNameStyle( - "[data-testid=database-datasource-content-wrapper] .textBtn", - ); + cy.datasourceNameStyle(".t--plugin-name"); }); it("3. New API datasource card design", () => { @@ -87,7 +81,7 @@ describe( //Icon wrapper cy.datasourceIconWrapperStyle(".content-icon"); //Name - cy.datasourceNameStyle(".t--createBlankApiCard .textBtn"); + cy.datasourceNameStyle(".t--createBlankApiCard .t--plugin-name"); }); after(() => { diff --git a/app/client/cypress/locators/DatasourcesEditor.json b/app/client/cypress/locators/DatasourcesEditor.json index c0d8125eaa21..7fb1c7f37183 100644 --- a/app/client/cypress/locators/DatasourcesEditor.json +++ b/app/client/cypress/locators/DatasourcesEditor.json @@ -62,7 +62,7 @@ "basicUsername": "input[name='authentication.username']", "basicPassword": "input[name='authentication.password']", "mockUserDatabase": "div[id='mock-database'] span:contains('Users')", - "mockUserDatasources": ".t--datasource-name:contains('Users')", + "mockUserDatasources": ".t--plugin-name:contains('Users')", "mongoUriDropdown": "//p[text()='Use mongo connection string URI']/following-sibling::div", "mongoUriYes": "//div[text()='Yes']", "mongoUriInput": "//p[text()='Connection string URI']/following-sibling::div//input", diff --git a/app/client/cypress/support/Pages/DataSources.ts b/app/client/cypress/support/Pages/DataSources.ts index ff559f6e4784..234337cc9a61 100644 --- a/app/client/cypress/support/Pages/DataSources.ts +++ b/app/client/cypress/support/Pages/DataSources.ts @@ -170,7 +170,7 @@ export class DataSources { _usePreparedStatement = "input[name='actionConfiguration.pluginSpecifiedTemplates[0].value'][type='checkbox'], input[name='actionConfiguration.formData.preparedStatement.data'][type='checkbox']"; _mockDB = (dbName: string) => - "//span[text()='" + + "//p[text()='" + dbName + "']/ancestor::div[contains(@class, 't--mock-datasource')][1]"; private _createBlankGraphQL = ".t--createBlankApiGraphqlCard"; @@ -203,7 +203,7 @@ export class DataSources { _queryTimeout = "//input[@name='actionConfiguration.timeoutInMillisecond']"; _getStructureReq = "/api/v1/datasources/*/structure?ignoreCache=true"; _editDatasourceFromActiveTab = (dsName: string) => - ".t--datasource-name:contains('" + dsName + "')"; + ".t--plugin-name:contains('" + dsName + "')"; _mandatoryMark = "//span[text()='*']"; _deleteDSHostPort = ".t--delete-field"; _dsTabSchema = "[data-testid='t--tab-DATASOURCE_TAB']"; diff --git a/app/client/cypress/support/dataSourceCommands.js b/app/client/cypress/support/dataSourceCommands.js index 5b583d130ea2..9fa97b12ab37 100644 --- a/app/client/cypress/support/dataSourceCommands.js +++ b/app/client/cypress/support/dataSourceCommands.js @@ -307,9 +307,8 @@ Cypress.Commands.add("datasourceCardContainerStyle", (tag) => { Cypress.Commands.add("datasourceCardStyle", (tag) => { cy.get(tag) .should("have.css", "display", "flex") - .and("have.css", "justify-content", "space-between") .and("have.css", "align-items", "center") - .and("have.css", "height", "64px") + .and("have.css", "gap", "12px") .realHover() .should("have.css", "background-color", backgroundColorGray1) .and("have.css", "cursor", "pointer"); @@ -324,9 +323,8 @@ Cypress.Commands.add("datasourceImageStyle", (tag) => { Cypress.Commands.add("datasourceContentWrapperStyle", (tag) => { cy.get(tag) .should("have.css", "display", "flex") - .and("have.css", "align-items", "center") - .and("have.css", "gap", "13px") - .and("have.css", "padding-left", "13.5px"); + .and("have.css", "align-items", "flex-start") + .and("have.css", "gap", "normal"); }); Cypress.Commands.add("datasourceIconWrapperStyle", (tag) => { @@ -343,8 +341,7 @@ Cypress.Commands.add("datasourceNameStyle", (tag) => { .should("have.css", "color", backgroundColorBlack) .and("have.css", "font-size", "16px") .and("have.css", "font-weight", "400") - .and("have.css", "line-height", "24px") - .and("have.css", "letter-spacing", "-0.24px"); + .and("have.css", "line-height", "20px"); }); Cypress.Commands.add("mockDatasourceDescriptionStyle", (tag) => { @@ -352,6 +349,5 @@ Cypress.Commands.add("mockDatasourceDescriptionStyle", (tag) => { .should("have.css", "color", backgroundColorGray8) .and("have.css", "font-size", "13px") .and("have.css", "font-weight", "400") - .and("have.css", "line-height", "17px") - .and("have.css", "letter-spacing", "-0.24px"); + .and("have.css", "line-height", "17px"); }); diff --git a/app/client/src/ce/constants/messages.ts b/app/client/src/ce/constants/messages.ts index 28c616798ee6..a7ee8c2d92b2 100644 --- a/app/client/src/ce/constants/messages.ts +++ b/app/client/src/ce/constants/messages.ts @@ -393,8 +393,23 @@ export const CREATE_NEW_DATASOURCE_DATABASE_HEADER = () => "Databases"; export const CREATE_NEW_DATASOURCE_MOST_POPULAR_HEADER = () => "Most popular"; export const CREATE_NEW_DATASOURCE_REST_API = () => "REST API"; export const SAMPLE_DATASOURCES = () => "Sample datasources"; +export const SAMPLE_DATASOURCE_SUBHEADING = () => + "Use sample datasources if you don’t have a datasource for testing"; export const EDIT_DS_CONFIG = () => "Edit datasource configuration"; export const NOT_FOUND = () => "Not found"; +export const CREATE_NEW_DATASOURCE_AUTHENTICATED_REST_API = () => + "Authenticated API"; +export const CREATE_NEW_DATASOURCE_GRAPHQL_API = () => "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"; +export const CONNECT_A_DATASOURCE_HEADING = () => "Connect a datasource"; +export const CONNECT_A_DATASOURCE_SUBHEADING = () => + "Select a sample datasource or connect your own"; +export const SEARCH_FOR_DATASOURCES = () => "Search for datasources"; +export const EMPTY_SEARCH_DATASOURCES_TITLE = () => "No results found"; +export const EMPTY_SEARCH_DATASOURCES_DESCRIPTION = () => + "Please try again with a different search"; export const ERROR_EVAL_ERROR_GENERIC = () => `Unexpected error occurred while evaluating the application`; @@ -2323,9 +2338,6 @@ export const START_FROM_SCRATCH_SUBTITLE = () => export const START_WITH_DATA_TITLE = () => "Start with data"; export const START_WITH_DATA_SUBTITLE = () => "Get started with connecting your data, and easily craft a functional application."; -export const START_WITH_DATA_CONNECT_HEADING = () => "Connect your datasource"; -export const START_WITH_DATA_CONNECT_SUBHEADING = () => - "Select an option to establish a connection. Your data's security is our priority."; export const START_WITH_TEMPLATE_CONNECT_HEADING = () => "Select a template"; export const START_WITH_TEMPLATE_CONNECT_SUBHEADING = () => "Choose an option below to embark on your app-building adventure!"; @@ -2381,8 +2393,6 @@ export const PARTIAL_IMPORT_EXPORT = { }, }; -export const DATASOURCE_SECURELY_TITLE = () => "Secure & fast connection"; - export const CUSTOM_WIDGET_FEATURE = { addEvent: { addCTA: () => "Add", @@ -2612,3 +2622,6 @@ export const PREMIUM_DATASOURCES = { "The Appsmith Team is actively working on it. We’ll let you know when this integration is live. ", NOTIFY_ME: () => "Notify me", }; + +export const DATASOURCE_SECURE_TEXT = () => + `When connecting datasources, your passwords are AES-256 encrypted and we never store any of your data.`; diff --git a/app/client/src/ce/pages/Applications/CreateNewAppsOption.tsx b/app/client/src/ce/pages/Applications/CreateNewAppsOption.tsx index 6afab5aa4fbd..ca377143edef 100644 --- a/app/client/src/ce/pages/Applications/CreateNewAppsOption.tsx +++ b/app/client/src/ce/pages/Applications/CreateNewAppsOption.tsx @@ -1,8 +1,6 @@ import { GO_BACK, SKIP_START_WITH_USE_CASE_TEMPLATES, - START_WITH_DATA_CONNECT_HEADING, - START_WITH_DATA_CONNECT_SUBHEADING, createMessage, } from "ee/constants/messages"; import urlBuilder from "ee/entities/URLRedirect/URLAssembly"; @@ -12,7 +10,7 @@ import { resetCurrentPluginIdForCreateNewApp, } from "actions/onboardingActions"; import { fetchPlugins } from "actions/pluginActions"; -import { Flex, Link, Text } from "@appsmith/ads"; +import { Flex, Link } from "@appsmith/ads"; import CreateNewDatasourceTab from "pages/Editor/IntegrationEditor/CreateNewDatasourceTab"; import { getApplicationsOfWorkspace } from "ee/selectors/selectedWorkspaceSelectors"; import { default as React, useEffect } from "react"; @@ -36,7 +34,6 @@ import { isAirgapped } from "ee/utils/airgapHelpers"; const SectionWrapper = styled.div<{ isBannerVisible: boolean }>` display: flex; flex-direction: column; - padding: 0 var(--ads-v2-spaces-7) var(--ads-v2-spaces-7); ${(props) => ` margin-top: ${ props.theme.homePage.header + (props.isBannerVisible ? 40 : 0) @@ -56,8 +53,9 @@ const BackWrapper = styled.div<{ hidden?: boolean; isBannerVisible: boolean }>` top: ${props.theme.homePage.header + (props.isBannerVisible ? 40 : 0)}px; `} background: inherit; - padding: var(--ads-v2-spaces-3); + padding: var(--ads-v2-spaces-4) var(--ads-v2-spaces-8); z-index: 1; + border-bottom: 1px solid var(--ads-v2-color-gray-300); margin-left: -4px; ${(props) => `${props.hidden && "visibility: hidden; opacity: 0;"}`} `; @@ -66,22 +64,10 @@ const LinkWrapper = styled(Link)<{ hidden?: boolean }>` ${(props) => `${props.hidden && "visibility: hidden; opacity: 0;"}`} `; -const WithDataWrapper = styled.div` +const WithDataWrapper = styled(Flex)` background: var(--ads-v2-color-bg); - padding: var(--ads-v2-spaces-13); - border: 1px solid var(--ads-v2-color-gray-300); - border-radius: 5px; `; -const Header = ({ subtitle, title }: { subtitle: string; title: string }) => { - return ( - - {title} - {subtitle} - - ); -}; - const CreateNewAppsOption = ({ currentApplicationIdForCreateNewApp, }: { @@ -227,12 +213,8 @@ const CreateNewAppsOption = ({ )} - -
- + + {createNewAppPluginId && !!selectedDatasource ? ( selectedPlugin?.type === PluginType.SAAS ? ( void; - // TODO: Fix this the next time the file is edited - // eslint-disable-next-line @typescript-eslint/no-explicit-any - createTempDatasourceFromForm: (data: any) => void; - showSaasAPIs: boolean; // If this is true, only SaaS APIs will be shown -} - -function AIDataSources(props: Props) { - const { plugins } = props; - - const handleOnClick = (plugin: Plugin) => { - AnalyticsUtil.logEvent("CREATE_DATA_SOURCE_CLICK", { - pluginName: plugin.name, - pluginPackageName: plugin.packageName, - }); - - props.createTempDatasourceFromForm({ - pluginId: plugin.id, - type: plugin.type, - }); - }; - - // AI Plugins - const aiPlugins = plugins - .sort((a, b) => { - // Sort the AI plugins alphabetically - return a.name.localeCompare(b.name); - }) - .filter((p) => p.type === PluginType.AI); - - return ( - - - {aiPlugins.map((plugin) => ( - { - handleOnClick(plugin); - }} - > - - {plugin.name} -

{plugin.name}

-
-
- ))} -
-
- ); -} - -const mapStateToProps = (state: AppState) => ({ - plugins: state.entities.plugins.list, -}); - -const mapDispatchToProps = { - createTempDatasourceFromForm, -}; - -export default connect(mapStateToProps, mapDispatchToProps)(AIDataSources); diff --git a/app/client/src/pages/Editor/IntegrationEditor/AIPlugins.tsx b/app/client/src/pages/Editor/IntegrationEditor/AIPlugins.tsx new file mode 100644 index 000000000000..4df348e1a824 --- /dev/null +++ b/app/client/src/pages/Editor/IntegrationEditor/AIPlugins.tsx @@ -0,0 +1,110 @@ +import React from "react"; +import { connect } from "react-redux"; +import { createTempDatasourceFromForm } from "actions/datasourceActions"; +import type { AppState } from "ee/reducers"; +import type { Plugin } from "api/PluginApi"; +import AnalyticsUtil from "ee/utils/AnalyticsUtil"; +import { PluginType } from "entities/Action"; +import { getAssetUrl, isAirgapped } from "ee/utils/airgapHelpers"; +import { + DatasourceContainer, + DatasourceSection, + DatasourceSectionHeading, + StyledDivider, +} from "./IntegrationStyledComponents"; +import DatasourceItem from "./DatasourceItem"; +import { + CREATE_NEW_AI_SECTION_HEADER, + createMessage, +} from "ee/constants/messages"; +import { pluginSearchSelector } from "./CreateNewDatasourceHeader"; +import { getPlugins } from "ee/selectors/entitiesSelector"; + +interface CreateAIPluginsProps { + pageId: string; + isCreating?: boolean; + showUnsupportedPluginDialog: (callback: () => void) => void; + + plugins: Plugin[]; + createTempDatasourceFromForm: typeof createTempDatasourceFromForm; +} + +function AIDataSources(props: CreateAIPluginsProps) { + const { plugins } = props; + + const handleOnClick = (plugin: Plugin) => { + AnalyticsUtil.logEvent("CREATE_DATA_SOURCE_CLICK", { + pluginName: plugin.name, + pluginPackageName: plugin.packageName, + }); + + props.createTempDatasourceFromForm({ + pluginId: plugin.id, + type: plugin.type, + }); + }; + + return ( + + {plugins.map((plugin) => ( + { + handleOnClick(plugin); + }} + icon={getAssetUrl(plugin.iconLocation)} + key={plugin.id} + name={plugin.name} + /> + ))} + + ); +} + +function CreateAIPlugins(props: CreateAIPluginsProps) { + const isAirgappedInstance = isAirgapped(); + + if (isAirgappedInstance || props.plugins.length === 0) return null; + + return ( + <> + + + + {createMessage(CREATE_NEW_AI_SECTION_HEADER)} + + + + + ); +} + +const mapStateToProps = (state: AppState) => { + const searchedPlugin = ( + pluginSearchSelector(state, "search") || "" + ).toLocaleLowerCase(); + + let plugins = getPlugins(state); + + // AI Plugins + plugins = plugins + .sort((a, b) => { + // Sort the AI plugins alphabetically + return a.name.localeCompare(b.name); + }) + .filter( + (plugin) => + plugin.type === PluginType.AI && + plugin.name.toLocaleLowerCase().includes(searchedPlugin), + ); + + return { + plugins, + }; +}; + +const mapDispatchToProps = { + createTempDatasourceFromForm, +}; + +export default connect(mapStateToProps, mapDispatchToProps)(CreateAIPlugins); diff --git a/app/client/src/pages/Editor/IntegrationEditor/APIOrSaasPlugins.tsx b/app/client/src/pages/Editor/IntegrationEditor/APIOrSaasPlugins.tsx new file mode 100644 index 000000000000..4866e929cbe4 --- /dev/null +++ b/app/client/src/pages/Editor/IntegrationEditor/APIOrSaasPlugins.tsx @@ -0,0 +1,340 @@ +import React, { useCallback, useEffect, useRef } from "react"; +import { connect, useSelector } from "react-redux"; +import { + createDatasourceFromForm, + createTempDatasourceFromForm, +} from "actions/datasourceActions"; +import type { AppState } from "ee/reducers"; +import type { GenerateCRUDEnabledPluginMap, Plugin } from "api/PluginApi"; +import AnalyticsUtil from "ee/utils/AnalyticsUtil"; +import { PluginPackageName, PluginType } from "entities/Action"; +import { getQueryParams } from "utils/URLUtils"; +import { + getGenerateCRUDEnabledPluginMap, + getPlugins, +} from "ee/selectors/entitiesSelector"; +import { getIsGeneratePageInitiator } from "utils/GenerateCrudUtil"; +import { getAssetUrl, isAirgapped } from "ee/utils/airgapHelpers"; +import { Spinner } from "@appsmith/ads"; +import { useEditorType } from "ee/hooks"; +import { useParentEntityInfo } from "ee/hooks/datasourceEditorHooks"; +import { createNewApiActionBasedOnEditorType } from "ee/actions/helpers"; +import type { ActionParentEntityTypeInterface } from "ee/entities/Engine/actionHelpers"; +import { + DatasourceContainer, + DatasourceSection, + DatasourceSectionHeading, + StyledDivider, +} from "./IntegrationStyledComponents"; +import { ASSETS_CDN_URL } from "constants/ThirdPartyConstants"; +import DatasourceItem from "./DatasourceItem"; +import { + CREATE_NEW_API_SECTION_HEADER, + CREATE_NEW_DATASOURCE_AUTHENTICATED_REST_API, + CREATE_NEW_DATASOURCE_GRAPHQL_API, + CREATE_NEW_DATASOURCE_REST_API, + CREATE_NEW_SAAS_SECTION_HEADER, + createMessage, +} from "ee/constants/messages"; +import scrollIntoView from "scroll-into-view-if-needed"; +import PremiumDatasources from "./PremiumDatasources"; +import { pluginSearchSelector } from "./CreateNewDatasourceHeader"; +import { + PREMIUM_INTEGRATIONS, + type PremiumIntegration, +} from "./PremiumDatasources/Constants"; + +interface CreateAPIOrSaasPluginsProps { + location: { + search: string; + }; + isCreating?: boolean; + showUnsupportedPluginDialog: (callback: () => void) => void; + isOnboardingScreen?: boolean; + active?: boolean; + pageId: string; + showSaasAPIs?: boolean; // If this is true, only SaaS APIs will be shown + plugins: Plugin[]; + createDatasourceFromForm: typeof createDatasourceFromForm; + createTempDatasourceFromForm: typeof createTempDatasourceFromForm; + createNewApiActionBasedOnEditorType: ( + editorType: string, + editorId: string, + parentEntityId: string, + parentEntityType: ActionParentEntityTypeInterface, + apiType: string, + ) => void; + isPremiumDatasourcesViewEnabled?: boolean; + premiumPlugins: PremiumIntegration[]; + authApiPlugin?: Plugin; + restAPIVisible?: boolean; + graphQLAPIVisible?: boolean; +} + +export const API_ACTION = { + IMPORT_CURL: "IMPORT_CURL", + CREATE_NEW_API: "CREATE_NEW_API", + CREATE_NEW_GRAPHQL_API: "CREATE_NEW_GRAPHQL_API", + CREATE_DATASOURCE_FORM: "CREATE_DATASOURCE_FORM", + AUTH_API: "AUTH_API", +}; + +function APIOrSaasPlugins(props: CreateAPIOrSaasPluginsProps) { + const { authApiPlugin, isCreating, isOnboardingScreen, pageId, plugins } = + props; + const editorType = useEditorType(location.pathname); + const { editorId, parentEntityId, parentEntityType } = + useParentEntityInfo(editorType); + const generateCRUDSupportedPlugin: GenerateCRUDEnabledPluginMap = useSelector( + getGenerateCRUDEnabledPluginMap, + ); + + const handleCreateAuthApiDatasource = useCallback(() => { + if (authApiPlugin) { + AnalyticsUtil.logEvent("CREATE_DATA_SOURCE_AUTH_API_CLICK", { + pluginId: authApiPlugin.id, + }); + AnalyticsUtil.logEvent("CREATE_DATA_SOURCE_CLICK", { + pluginName: authApiPlugin.name, + pluginPackageName: authApiPlugin.packageName, + }); + props.createTempDatasourceFromForm({ + pluginId: authApiPlugin.id, + type: authApiPlugin.type, + }); + } + }, [authApiPlugin, props.createTempDatasourceFromForm]); + + const handleCreateNew = (source: string) => { + AnalyticsUtil.logEvent("CREATE_DATA_SOURCE_CLICK", { + source, + }); + props.createNewApiActionBasedOnEditorType( + editorType, + editorId, + // Set parentEntityId as (parentEntityId or if it is onboarding screen then set it as pageId) else empty string + parentEntityId || (isOnboardingScreen && pageId) || "", + parentEntityType, + source === API_ACTION.CREATE_NEW_GRAPHQL_API + ? PluginPackageName.GRAPHQL + : PluginPackageName.REST_API, + ); + }; + + // On click of any API card, handleOnClick action should be called to check if user came from generate-page flow. + // if yes then show UnsupportedDialog for the API which are not supported to generate CRUD page. + const handleOnClick = ( + actionType: string, + params?: { + skipValidPluginCheck?: boolean; + pluginId?: string; + type?: PluginType; + }, + ) => { + const queryParams = getQueryParams(); + const isGeneratePageInitiator = getIsGeneratePageInitiator( + queryParams.isGeneratePageMode, + ); + + if ( + isGeneratePageInitiator && + !params?.skipValidPluginCheck && + (!params?.pluginId || !generateCRUDSupportedPlugin[params.pluginId]) + ) { + // show modal informing user that this will break the generate flow. + props.showUnsupportedPluginDialog(() => + handleOnClick(actionType, { skipValidPluginCheck: true, ...params }), + ); + + return; + } + + switch (actionType) { + case API_ACTION.CREATE_NEW_API: + case API_ACTION.CREATE_NEW_GRAPHQL_API: + handleCreateNew(actionType); + break; + case API_ACTION.CREATE_DATASOURCE_FORM: { + if (params) { + props.createTempDatasourceFromForm({ + pluginId: params.pluginId!, + type: params.type!, + }); + } + + break; + } + case API_ACTION.AUTH_API: { + handleCreateAuthApiDatasource(); + break; + } + default: + } + }; + + // Api plugins with Graphql + + return ( + + {props.restAPIVisible && ( + handleOnClick(API_ACTION.CREATE_NEW_API)} + icon={getAssetUrl(`${ASSETS_CDN_URL}/plus.png`)} + name={createMessage(CREATE_NEW_DATASOURCE_REST_API)} + rightSibling={isCreating && } + /> + )} + {props.graphQLAPIVisible && ( + handleOnClick(API_ACTION.CREATE_NEW_GRAPHQL_API)} + icon={getAssetUrl(`${ASSETS_CDN_URL}/GraphQL.png`)} + name={createMessage(CREATE_NEW_DATASOURCE_GRAPHQL_API)} + /> + )} + {authApiPlugin && ( + handleOnClick(API_ACTION.AUTH_API)} + icon={getAssetUrl(authApiPlugin.iconLocation)} + name={createMessage(CREATE_NEW_DATASOURCE_AUTHENTICATED_REST_API)} + /> + )} + {plugins.map((p) => ( + { + AnalyticsUtil.logEvent("CREATE_DATA_SOURCE_CLICK", { + pluginName: p.name, + pluginPackageName: p.packageName, + }); + handleOnClick(API_ACTION.CREATE_DATASOURCE_FORM, { + pluginId: p.id, + }); + }} + icon={getAssetUrl(p.iconLocation)} + key={p.id} + name={p.name} + /> + ))} + + + ); +} + +function CreateAPIOrSaasPlugins(props: CreateAPIOrSaasPluginsProps) { + const newAPIRef = useRef(null); + const isMounted = useRef(false); + const isAirgappedInstance = isAirgapped(); + + useEffect(() => { + if (props.active && newAPIRef.current) { + isMounted.current && + scrollIntoView(newAPIRef.current, { + behavior: "smooth", + scrollMode: "always", + block: "start", + boundary: document.getElementById("new-integrations-wrapper"), + }); + } else { + isMounted.current = true; + } + }, [props.active]); + + if (isAirgappedInstance && props.showSaasAPIs) return null; + + if ( + props.premiumPlugins.length === 0 && + props.plugins.length === 0 && + !props.restAPIVisible && + !props.graphQLAPIVisible + ) + return null; + + return ( + <> + + + + {props.showSaasAPIs + ? createMessage(CREATE_NEW_SAAS_SECTION_HEADER) + : createMessage(CREATE_NEW_API_SECTION_HEADER)} + + + + + ); +} + +const mapStateToProps = ( + state: AppState, + props: { showSaasAPIs?: boolean; isPremiumDatasourcesViewEnabled: boolean }, +) => { + const searchedPlugin = ( + pluginSearchSelector(state, "search") || "" + ).toLocaleLowerCase(); + + const allPlugins = getPlugins(state); + + let plugins = allPlugins.filter((p) => + !props.showSaasAPIs + ? p.packageName === PluginPackageName.GRAPHQL + : p.type === PluginType.SAAS || + p.type === PluginType.REMOTE || + p.type === PluginType.EXTERNAL_SAAS, + ); + + plugins = plugins.filter((p) => + p.name.toLocaleLowerCase().includes(searchedPlugin), + ); + + let authApiPlugin = !props.showSaasAPIs + ? allPlugins.find((p) => p.name === "REST API") + : undefined; + + authApiPlugin = createMessage(CREATE_NEW_DATASOURCE_AUTHENTICATED_REST_API) + .toLocaleLowerCase() + .includes(searchedPlugin) + ? authApiPlugin + : undefined; + + const premiumPlugins = + props.showSaasAPIs && props.isPremiumDatasourcesViewEnabled + ? PREMIUM_INTEGRATIONS.filter((p) => + p.name.toLocaleLowerCase().includes(searchedPlugin), + ) + : []; + + const restAPIVisible = + !props.showSaasAPIs && + createMessage(CREATE_NEW_DATASOURCE_REST_API) + .toLocaleLowerCase() + .includes(searchedPlugin); + const graphQLAPIVisible = + !props.showSaasAPIs && + createMessage(CREATE_NEW_DATASOURCE_GRAPHQL_API) + .toLocaleLowerCase() + .includes(searchedPlugin); + + return { + plugins, + premiumPlugins, + authApiPlugin, + restAPIVisible, + graphQLAPIVisible, + }; +}; + +const mapDispatchToProps = { + createDatasourceFromForm, + createTempDatasourceFromForm, + createNewApiActionBasedOnEditorType, +}; + +export default connect( + mapStateToProps, + mapDispatchToProps, +)(CreateAPIOrSaasPlugins); diff --git a/app/client/src/pages/Editor/IntegrationEditor/AddDatasourceSecurely.tsx b/app/client/src/pages/Editor/IntegrationEditor/AddDatasourceSecurely.tsx index 243e22077a2e..cba41258c8f9 100644 --- a/app/client/src/pages/Editor/IntegrationEditor/AddDatasourceSecurely.tsx +++ b/app/client/src/pages/Editor/IntegrationEditor/AddDatasourceSecurely.tsx @@ -1,37 +1,53 @@ import React from "react"; import styled from "styled-components"; -import { Flex, Text } from "@appsmith/ads"; +import { Button, Flex, Text } from "@appsmith/ads"; import { getAssetUrl } from "ee/utils/airgapHelpers"; import { ASSETS_CDN_URL } from "constants/ThirdPartyConstants"; -import { - createMessage, - DATASOURCE_SECURELY_TITLE, -} from "ee/constants/messages"; +import { CalloutCloseClassName } from "@appsmith/ads/src/Callout/Callout.constants"; +import { createMessage, DATASOURCE_SECURE_TEXT } from "ee/constants/messages"; -const Wrapper = styled(Flex)` - background: var(--ads-v2-color-blue-100); - border-radius: var(--ads-v2-border-radius); - padding: var(--ads-v2-spaces-7); +const StyledCalloutWrapper = styled(Flex)<{ isClosed: boolean }>` + ${(props) => (props.isClosed ? "display: none;" : "")} + background-color: var(--ads-v2-colors-response-info-surface-default-bg); + padding: var(--ads-spaces-3); + gap: var(--ads-spaces-3); + flex-grow: 1; align-items: center; + .ads-v2-text { + flex-grow: 1; + } +`; + +const SecureImg = styled.img` + height: 28px; + padding: var(--ads-v2-spaces-2); `; function AddDatasourceSecurely() { + const [isClosed, setClosed] = React.useState(false); + return ( - - {createMessage(DATASOURCE_SECURELY_TITLE)} + + + {createMessage(DATASOURCE_SECURE_TEXT)} + +