feat: datasource homepage ui redesign and search functionality for the datasources (#38360)

This commit is contained in:
Aman Agarwal 2025-01-09 16:18:44 +05:30 committed by GitHub
parent c302b64be5
commit dfd7fdee97
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 1121 additions and 1213 deletions

View File

@ -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(() => {

View File

@ -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",

View File

@ -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']";

View File

@ -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");
});

View File

@ -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 dont 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. Well 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.`;

View File

@ -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 (
<Flex flexDirection="column" mb="spaces-14" mt="spaces-7">
<Text kind="heading-xl">{title}</Text>
<Text>{subtitle}</Text>
</Flex>
);
};
const CreateNewAppsOption = ({
currentApplicationIdForCreateNewApp,
}: {
@ -227,12 +213,8 @@ const CreateNewAppsOption = ({
</LinkWrapper>
)}
</BackWrapper>
<Flex flexDirection="column" pl="spaces-3" pr="spaces-3">
<Header
subtitle={createMessage(START_WITH_DATA_CONNECT_SUBHEADING)}
title={createMessage(START_WITH_DATA_CONNECT_HEADING)}
/>
<WithDataWrapper>
<Flex flex={"1"} flexDirection="column">
<WithDataWrapper flex={"1"} flexDirection="column">
{createNewAppPluginId && !!selectedDatasource ? (
selectedPlugin?.type === PluginType.SAAS ? (
<DatasourceForm

View File

@ -1,184 +0,0 @@
import React from "react";
import { connect } from "react-redux";
import styled from "styled-components";
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 } from "ee/utils/airgapHelpers";
export const StyledContainer = styled.div`
flex: 1;
margin-top: 8px;
.textBtn {
font-size: 16px;
line-height: 24px;
margin: 0;
justify-content: center;
text-align: center;
letter-spacing: -0.24px;
color: var(--ads-v2-color-fg);
font-weight: 400;
text-decoration: none !important;
flex-wrap: wrap;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
@media (min-width: 2500px) {
.textBtn {
font-size: 18px;
}
}
@media (min-width: 2500px) {
.eachCard {
width: 240px;
height: 200px;
}
.apiImage {
margin-top: 25px;
margin-bottom: 20px;
height: 80px;
}
.curlImage {
width: 100px;
}
.createIcon {
height: 70px;
}
}
`;
export const DatasourceCardsContainer = styled.div`
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 16px;
text-align: center;
min-width: 150px;
border-radius: 4px;
align-items: center;
.create-new-api {
&:hover {
cursor: pointer;
}
}
`;
export const DatasourceCard = styled.div`
display: flex;
align-items: center;
justify-content: space-between;
height: 64px;
border-radius: var(--ads-v2-border-radius);
&:hover {
background-color: var(--ads-v2-color-bg-subtle);
cursor: pointer;
}
.content-icon {
height: 34px;
width: auto;
margin: 0 auto;
max-width: 100%;
}
.cta {
display: none;
margin-right: 32px;
}
&:hover {
.cta {
display: flex;
}
}
`;
export const CardContentWrapper = styled.div`
display: flex;
align-items: center;
gap: 13px;
padding-left: 13.5px;
`;
interface Props {
location: {
search: string;
};
pageId: string;
plugins: Plugin[];
isCreating: boolean;
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
showUnsupportedPluginDialog: (callback: any) => 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 (
<StyledContainer>
<DatasourceCardsContainer data-testid="newai-datasource-card-container">
{aiPlugins.map((plugin) => (
<DatasourceCard
className={`t--createBlankApi-${plugin.packageName}`}
key={plugin.id}
onClick={() => {
handleOnClick(plugin);
}}
>
<CardContentWrapper>
<img
alt={plugin.name}
className={
"content-icon saasImage t--saas-" +
plugin.packageName +
"-image"
}
src={getAssetUrl(plugin.iconLocation)}
/>
<p className="t--plugin-name textBtn">{plugin.name}</p>
</CardContentWrapper>
</DatasourceCard>
))}
</DatasourceCardsContainer>
</StyledContainer>
);
}
const mapStateToProps = (state: AppState) => ({
plugins: state.entities.plugins.list,
});
const mapDispatchToProps = {
createTempDatasourceFromForm,
};
export default connect(mapStateToProps, mapDispatchToProps)(AIDataSources);

View File

@ -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 (
<DatasourceContainer data-testid="newai-datasource-card-container">
{plugins.map((plugin) => (
<DatasourceItem
className={`t--createBlankApi-${plugin.packageName}`}
handleOnClick={() => {
handleOnClick(plugin);
}}
icon={getAssetUrl(plugin.iconLocation)}
key={plugin.id}
name={plugin.name}
/>
))}
</DatasourceContainer>
);
}
function CreateAIPlugins(props: CreateAIPluginsProps) {
const isAirgappedInstance = isAirgapped();
if (isAirgappedInstance || props.plugins.length === 0) return null;
return (
<>
<StyledDivider />
<DatasourceSection id="new-ai-query">
<DatasourceSectionHeading kind="heading-m">
{createMessage(CREATE_NEW_AI_SECTION_HEADER)}
</DatasourceSectionHeading>
<AIDataSources {...props} />
</DatasourceSection>
</>
);
}
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);

View File

@ -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 (
<DatasourceContainer data-testid="newapi-datasource-card-container">
{props.restAPIVisible && (
<DatasourceItem
className="t--createBlankApiCard create-new-api"
dataCardWrapperTestId="newapi-datasource-content-wrapper"
handleOnClick={() => handleOnClick(API_ACTION.CREATE_NEW_API)}
icon={getAssetUrl(`${ASSETS_CDN_URL}/plus.png`)}
name={createMessage(CREATE_NEW_DATASOURCE_REST_API)}
rightSibling={isCreating && <Spinner className="cta" size={"sm"} />}
/>
)}
{props.graphQLAPIVisible && (
<DatasourceItem
className="t--createBlankApiGraphqlCard"
dataCardWrapperTestId="graphqlapi-datasource-content-wrapper"
handleOnClick={() => handleOnClick(API_ACTION.CREATE_NEW_GRAPHQL_API)}
icon={getAssetUrl(`${ASSETS_CDN_URL}/GraphQL.png`)}
name={createMessage(CREATE_NEW_DATASOURCE_GRAPHQL_API)}
/>
)}
{authApiPlugin && (
<DatasourceItem
className="t--createAuthApiDatasource"
dataCardWrapperTestId="authapi-datasource-content-wrapper"
handleOnClick={() => handleOnClick(API_ACTION.AUTH_API)}
icon={getAssetUrl(authApiPlugin.iconLocation)}
name={createMessage(CREATE_NEW_DATASOURCE_AUTHENTICATED_REST_API)}
/>
)}
{plugins.map((p) => (
<DatasourceItem
handleOnClick={() => {
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}
/>
))}
<PremiumDatasources plugins={props.premiumPlugins} />
</DatasourceContainer>
);
}
function CreateAPIOrSaasPlugins(props: CreateAPIOrSaasPluginsProps) {
const newAPIRef = useRef<HTMLDivElement>(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 (
<>
<StyledDivider />
<DatasourceSection id="new-api" ref={newAPIRef}>
<DatasourceSectionHeading kind="heading-m">
{props.showSaasAPIs
? createMessage(CREATE_NEW_SAAS_SECTION_HEADER)
: createMessage(CREATE_NEW_API_SECTION_HEADER)}
</DatasourceSectionHeading>
<APIOrSaasPlugins {...props} />
</DatasourceSection>
</>
);
}
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);

View File

@ -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 (
<Wrapper>
<img
alt={createMessage(DATASOURCE_SECURELY_TITLE)}
src={getAssetUrl(`${ASSETS_CDN_URL}/secure-lock.svg`)}
<StyledCalloutWrapper isClosed={isClosed}>
<SecureImg
alt={"datasource securely"}
src={getAssetUrl(`${ASSETS_CDN_URL}/secure-lock.png`)}
/>
<Flex flexDirection="column" ml="spaces-4">
<Text color="var(--ads-v2-color-gray-700)" kind="heading-m">
{createMessage(DATASOURCE_SECURELY_TITLE)}
</Text>
<Text color="var(--ads-v2-color-gray-600)" kind="body-m">
Connect a datasource to start building workflows. Your passwords are{" "}
<u>AES-256 encrypted</u> and we never store any of your data.
</Text>
</Flex>
</Wrapper>
<Text color="var(--ads-v2-color-gray-600)" kind="body-m">
{createMessage(DATASOURCE_SECURE_TEXT)}
</Text>
<Button
aria-label="Close"
aria-labelledby="Close"
className={CalloutCloseClassName}
isIconButton
kind="tertiary"
onClick={() => {
setClosed(true);
}}
size="sm"
startIcon="close-line"
/>
</StyledCalloutWrapper>
);
}

View File

@ -0,0 +1,71 @@
import { Flex, Text } from "@appsmith/ads";
import type { AppState } from "ee/reducers";
import {
CONNECT_A_DATASOURCE_HEADING,
CONNECT_A_DATASOURCE_SUBHEADING,
createMessage,
SEARCH_FOR_DATASOURCES,
} from "ee/constants/messages";
import React from "react";
import { connect } from "react-redux";
import styled from "styled-components";
import { Field, formValueSelector, reduxForm } from "redux-form";
import ReduxFormTextField from "components/utils/ReduxFormTextField";
const CREATE_NEW_INTEGRATION_SEARCH_FORM = "CREATE_NEW_INTEGRATION_SEARCH_FORM";
export const pluginSearchSelector = formValueSelector(
CREATE_NEW_INTEGRATION_SEARCH_FORM,
);
const HeaderText = styled(Flex)`
flex-grow: 1;
flex-shrink: 0;
`;
const SearchContainer = styled(Flex)`
flex-grow: 1;
max-width: 480px;
input {
height: 28px;
font-size: var(--ads-v2-font-size-3);
}
`;
interface HeaderProps {
search?: string;
}
const CreateNewDatasourceHeader = () => {
return (
<Flex alignItems="flex-end" gap="spaces-5">
<HeaderText flexDirection="column" flexGrow="1">
<Text kind="heading-l">
{createMessage(CONNECT_A_DATASOURCE_HEADING)}
</Text>
<Text>{createMessage(CONNECT_A_DATASOURCE_SUBHEADING)}</Text>
</HeaderText>
<SearchContainer justifyContent="flex-end">
<Field
component={ReduxFormTextField}
name="search"
placeholder={createMessage(SEARCH_FOR_DATASOURCES)}
size="md"
startIcon="search-line"
type="search"
/>
</SearchContainer>
</Flex>
);
};
export default connect((state: AppState) => {
return {
search: pluginSearchSelector(state, "search"),
};
}, null)(
reduxForm<HeaderProps>({
form: CREATE_NEW_INTEGRATION_SEARCH_FORM,
enableReinitialize: true,
})(CreateNewDatasourceHeader),
);

View File

@ -1,10 +1,13 @@
import AddDatasourceSecurely from "./AddDatasourceSecurely";
import React, { useEffect, useRef } from "react";
import React from "react";
import styled from "styled-components";
import { thinScrollbar } from "constants/DefaultTheme";
import type { AppState } from "ee/reducers";
import { getCurrentAppWorkspace } from "ee/selectors/selectedWorkspaceSelectors";
import { selectFeatureFlags } from "ee/selectors/featureFlagsSelectors";
import {
selectFeatureFlagCheck,
selectFeatureFlags,
} from "ee/selectors/featureFlagsSelectors";
import { isGACEnabled } from "ee/utils/planHelpers";
import { getHasCreateDatasourcePermission } from "ee/utils/BusinessFeatures/permissionPageHelpers";
import {
@ -17,235 +20,36 @@ import {
} from "selectors/editorSelectors";
import { connect } from "react-redux";
import type { Datasource, MockDatasource } from "entities/Datasource";
import scrollIntoView from "scroll-into-view-if-needed";
import { Text } from "@appsmith/ads";
import MockDataSources from "./MockDataSources";
import NewApiScreen from "./NewApi";
import NewQueryScreen from "./NewQuery";
import { isAirgapped } from "ee/utils/airgapHelpers";
import APIOrSaasPlugins from "./APIOrSaasPlugins";
import DBPluginsOrMostPopular from "./DBOrMostPopularPlugins";
import AIPlugins from "./AIPlugins";
import { showDebuggerFlag } from "selectors/debuggerSelectors";
import {
createMessage,
CREATE_NEW_DATASOURCE_DATABASE_HEADER,
CREATE_NEW_DATASOURCE_MOST_POPULAR_HEADER,
SAMPLE_DATASOURCES,
} from "ee/constants/messages";
import { Divider } from "@appsmith/ads";
import {
getApplicationByIdFromWorkspaces,
getCurrentApplicationIdForCreateNewApp,
} from "ee/selectors/applicationSelectors";
import { useEditorType } from "ee/hooks";
import { useParentEntityInfo } from "ee/hooks/datasourceEditorHooks";
import AIDataSources from "./AIDataSources";
import Debugger from "../DataSourceEditor/Debugger";
import { isPluginActionCreating } from "PluginActionEditor/store";
import RequestNewIntegration from "./RequestNewIntegration";
import PremiumDatasources from "pages/Editor/IntegrationEditor/PremiumDatasources";
import { StyledDivider } from "./IntegrationStyledComponents";
import CreateNewDatasourceHeader from "./CreateNewDatasourceHeader";
import EmptySearchedPlugins from "./EmptySearchedPlugins";
import { FEATURE_FLAG } from "ee/entities/FeatureFlag";
const NewIntegrationsContainer = styled.div`
const NewIntegrationsContainer = styled.div<{ isOnboardingScreen?: boolean }>`
${thinScrollbar};
overflow: auto;
flex: 1;
${(props) =>
props.isOnboardingScreen
? "padding: var(--ads-v2-spaces-5) var(--ads-spaces-11);"
: "padding: var(--ads-spaces-8);"}
& > div {
margin-bottom: var(--ads-spaces-9);
margin-bottom: var(--ads-spaces-7);
}
`;
const StyledDivider = styled(Divider)`
margin-bottom: var(--ads-spaces-9);
`;
interface MockDataSourcesProps {
mockDatasources: MockDatasource[];
active: boolean;
}
function UseMockDatasources({ active, mockDatasources }: MockDataSourcesProps) {
const useMockRef = useRef<HTMLDivElement>(null);
const isMounted = useRef(false);
useEffect(() => {
if (active && useMockRef.current) {
isMounted.current &&
scrollIntoView(useMockRef.current, {
behavior: "smooth",
scrollMode: "always",
block: "start",
boundary: document.getElementById("new-integrations-wrapper"),
});
} else {
isMounted.current = true;
}
}, [active]);
return (
<div id="mock-database" ref={useMockRef}>
<Text kind="heading-m">{createMessage(SAMPLE_DATASOURCES)}</Text>
<MockDataSources mockDatasources={mockDatasources} />
</div>
);
}
function CreateNewAPI({
active,
isCreating,
isOnboardingScreen,
pageId,
showUnsupportedPluginDialog, // TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
}: any) {
const newAPIRef = useRef<HTMLDivElement>(null);
const isMounted = useRef(false);
useEffect(() => {
if (active && newAPIRef.current) {
isMounted.current &&
scrollIntoView(newAPIRef.current, {
behavior: "smooth",
scrollMode: "always",
block: "start",
boundary: document.getElementById("new-integrations-wrapper"),
});
} else {
isMounted.current = true;
}
}, [active]);
return (
<div id="new-api" ref={newAPIRef}>
<Text kind="heading-m">APIs</Text>
<NewApiScreen
isCreating={isCreating}
isOnboardingScreen={isOnboardingScreen}
location={location}
pageId={pageId}
showSaasAPIs={false}
showUnsupportedPluginDialog={showUnsupportedPluginDialog}
/>
</div>
);
}
function CreateNewDatasource({
active,
isCreating,
isOnboardingScreen,
pageId,
showMostPopularPlugins,
showUnsupportedPluginDialog, // TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
}: any) {
const editorType = useEditorType(location.pathname);
const { editorId, parentEntityId, parentEntityType } =
useParentEntityInfo(editorType);
const newDatasourceRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (active && newDatasourceRef.current) {
scrollIntoView(newDatasourceRef.current, {
behavior: "smooth",
scrollMode: "always",
block: "start",
boundary: document.getElementById("new-integrations-wrapper"),
});
}
}, [active]);
const isAirgappedInstance = isAirgapped();
return (
<div id="new-datasources" ref={newDatasourceRef}>
<Text kind="heading-m">
{showMostPopularPlugins
? createMessage(CREATE_NEW_DATASOURCE_MOST_POPULAR_HEADER)
: createMessage(CREATE_NEW_DATASOURCE_DATABASE_HEADER)}
</Text>
<NewQueryScreen
editorId={editorId}
editorType={editorType}
isAirgappedInstance={isAirgappedInstance}
isCreating={isCreating}
location={location}
parentEntityId={parentEntityId || (isOnboardingScreen && pageId) || ""}
parentEntityType={parentEntityType}
showMostPopularPlugins={showMostPopularPlugins}
showUnsupportedPluginDialog={showUnsupportedPluginDialog}
/>
</div>
);
}
function CreateNewSaasIntegration({
active,
isCreating,
isPremiumDatasourcesViewEnabled,
pageId,
showUnsupportedPluginDialog, // TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
}: any) {
const newSaasAPIRef = useRef<HTMLDivElement>(null);
const isMounted = useRef(false);
const isAirgappedInstance = isAirgapped();
useEffect(() => {
if (active && newSaasAPIRef.current) {
isMounted.current &&
scrollIntoView(newSaasAPIRef.current, {
behavior: "smooth",
scrollMode: "always",
block: "start",
boundary: document.getElementById("new-integrations-wrapper"),
});
} else {
isMounted.current = true;
}
}, [active]);
return !isAirgappedInstance ? (
<>
<StyledDivider />
<div id="new-saas-api" ref={newSaasAPIRef}>
<Text kind="heading-m">SaaS integrations</Text>
<NewApiScreen
isCreating={isCreating}
location={location}
pageId={pageId}
showSaasAPIs
showUnsupportedPluginDialog={showUnsupportedPluginDialog}
>
{isPremiumDatasourcesViewEnabled && <PremiumDatasources />}
</NewApiScreen>
</div>
</>
) : null;
}
function CreateNewAIIntegration({
isCreating,
pageId,
showUnsupportedPluginDialog, // TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
}: any) {
const isAirgappedInstance = isAirgapped();
return !isAirgappedInstance ? (
<>
<StyledDivider />
<div id="new-ai-query">
<Text kind="heading-m">AI integrations</Text>
<AIDataSources
isCreating={isCreating}
location={location}
pageId={pageId}
showSaasAPIs
showUnsupportedPluginDialog={showUnsupportedPluginDialog}
/>
</div>
</>
) : null;
}
interface CreateNewDatasourceScreenProps {
isCreating: boolean;
dataSources: Datasource[];
@ -296,26 +100,24 @@ class CreateNewDatasourceTab extends React.Component<
if (!canCreateDatasource) return null;
const mockDataSection =
this.props.mockDatasources.length > 0 ? (
<UseMockDatasources
active={false}
mockDatasources={this.props.mockDatasources}
/>
) : null;
const mockDataSectionVisible = this.props.mockDatasources.length > 0;
return (
<>
<NewIntegrationsContainer className="p-4" id="new-integrations-wrapper">
<NewIntegrationsContainer
id="new-integrations-wrapper"
isOnboardingScreen={!!isOnboardingScreen}
>
<CreateNewDatasourceHeader />
<StyledDivider />
{dataSources.length === 0 && <AddDatasourceSecurely />}
{dataSources.length === 0 &&
this.props.mockDatasources.length > 0 && (
<>
{mockDataSection}
<StyledDivider />
</>
)}
<CreateNewDatasource
{dataSources.length === 0 && mockDataSectionVisible && (
<MockDataSources
mockDatasources={this.props.mockDatasources}
postDivider
/>
)}
<DBPluginsOrMostPopular
active={false}
isCreating={isCreating}
isOnboardingScreen={!!isOnboardingScreen}
@ -324,42 +126,48 @@ class CreateNewDatasourceTab extends React.Component<
showMostPopularPlugins
showUnsupportedPluginDialog={this.showUnsupportedPluginDialog}
/>
<StyledDivider />
<CreateNewAPI
<APIOrSaasPlugins
active={false}
isCreating={isCreating}
isOnboardingScreen={!!isOnboardingScreen}
location={location}
pageId={pageId}
showUnsupportedPluginDialog={this.showUnsupportedPluginDialog}
/>
<StyledDivider />
<CreateNewDatasource
active={false}
isCreating={isCreating}
location={location}
pageId={pageId}
showUnsupportedPluginDialog={this.showUnsupportedPluginDialog}
/>
<CreateNewSaasIntegration
active={false}
isCreating={isCreating}
isPremiumDatasourcesViewEnabled={isPremiumDatasourcesViewEnabled}
location={location}
pageId={pageId}
showUnsupportedPluginDialog={this.showUnsupportedPluginDialog}
/>
<CreateNewAIIntegration
<DBPluginsOrMostPopular
active={false}
addDivider
isCreating={isCreating}
location={location}
pageId={pageId}
showUnsupportedPluginDialog={this.showUnsupportedPluginDialog}
/>
<APIOrSaasPlugins
active={false}
isCreating={isCreating}
isPremiumDatasourcesViewEnabled={isPremiumDatasourcesViewEnabled}
location={location}
pageId={pageId}
showSaasAPIs
showUnsupportedPluginDialog={this.showUnsupportedPluginDialog}
/>
<AIPlugins
isCreating={isCreating}
pageId={pageId}
showUnsupportedPluginDialog={this.showUnsupportedPluginDialog}
/>
{dataSources.length > 0 && this.props.mockDatasources.length > 0 && (
<>
<StyledDivider />
{mockDataSection}
</>
{dataSources.length > 0 && mockDataSectionVisible && (
<MockDataSources
mockDatasources={this.props.mockDatasources}
preDivider
/>
)}
<EmptySearchedPlugins
isPremiumDatasourcesViewEnabled={
this.props.isPremiumDatasourcesViewEnabled
}
/>
</NewIntegrationsContainer>
{isRequestNewIntegrationEnabled && <RequestNewIntegration />}
{showDebugger && <Debugger />}
@ -390,11 +198,15 @@ const mapStateToProps = (state: AppState) => {
userWorkspacePermissions,
);
const isRequestNewIntegrationEnabled =
!!featureFlags?.ab_request_new_integration_enabled;
const isRequestNewIntegrationEnabled = selectFeatureFlagCheck(
state,
FEATURE_FLAG.ab_request_new_integration_enabled,
);
const isPremiumDatasourcesViewEnabled =
!!featureFlags?.ab_premium_datasources_view_enabled;
const isPremiumDatasourcesViewEnabled = selectFeatureFlagCheck(
state,
FEATURE_FLAG.ab_premium_datasources_view_enabled,
);
return {
dataSources: getDatasources(state),

View File

@ -1,5 +1,4 @@
import React, { type ReactNode } from "react";
import styled from "styled-components";
import React, { useEffect, useRef, type ReactNode } from "react";
import { connect } from "react-redux";
import { initialize } from "redux-form";
import {
@ -21,18 +20,34 @@ import { getQueryParams } from "utils/URLUtils";
import { getGenerateCRUDEnabledPluginMap } from "ee/selectors/entitiesSelector";
import type { GenerateCRUDEnabledPluginMap } from "api/PluginApi";
import { getIsGeneratePageInitiator } from "utils/GenerateCrudUtil";
import { getAssetUrl } from "ee/utils/airgapHelpers";
import { ApiCard, API_ACTION, CardContentWrapper } from "./NewApi";
import { getAssetUrl, isAirgapped } from "ee/utils/airgapHelpers";
import { API_ACTION } from "./APIOrSaasPlugins";
import { PluginPackageName, PluginType } from "entities/Action";
import { Spinner } from "@appsmith/ads";
import PlusLogo from "assets/images/Plus-logo.svg";
import {
createMessage,
CREATE_NEW_DATASOURCE_REST_API,
CREATE_NEW_DATASOURCE_MOST_POPULAR_HEADER,
CREATE_NEW_DATASOURCE_DATABASE_HEADER,
} from "ee/constants/messages";
import { createNewApiActionBasedOnEditorType } from "ee/actions/helpers";
import type { ActionParentEntityTypeInterface } from "ee/entities/Engine/actionHelpers";
import history from "utils/history";
import {
DatasourceContainer,
DatasourceSection,
DatasourceSectionHeading,
StyledDivider,
} from "./IntegrationStyledComponents";
import DatasourceItem from "./DatasourceItem";
import { ASSETS_CDN_URL } from "constants/ThirdPartyConstants";
import { useEditorType } from "ee/hooks";
import { useParentEntityInfo } from "ee/hooks/datasourceEditorHooks";
import scrollIntoView from "scroll-into-view-if-needed";
import { pluginSearchSelector } from "./CreateNewDatasourceHeader";
import type { CreateDatasourceConfig } from "api/DatasourcesApi";
import type { Datasource } from "entities/Datasource";
import type { AnyAction, Dispatch } from "redux";
// This function remove the given key from queryParams and return string
const removeQueryParams = (paramKeysToRemove: Array<string>) => {
@ -54,71 +69,7 @@ const removeQueryParams = (paramKeysToRemove: Array<string>) => {
return "";
};
const DatasourceHomePage = styled.div`
.textBtn {
justify-content: center;
text-align: center;
color: var(--ads-v2-color-fg);
font-weight: 400;
text-decoration: none !important;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-size: 16px;
line-height: 24px;
letter-spacing: -0.24px;
margin: 0;
}
`;
const DatasourceCardsContainer = styled.div`
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 16px;
text-align: center;
min-width: 150px;
border-radius: 4px;
align-items: center;
`;
const DatasourceCard = styled.div`
display: flex;
align-items: center;
justify-content: space-between;
height: 64px;
border-radius: var(--ads-v2-border-radius);
&:hover {
background: var(--ads-v2-color-bg-subtle);
cursor: pointer;
}
.dataSourceImage {
height: 34px;
width: auto;
margin: 0 auto;
max-width: 100%;
}
.cta {
display: none;
margin-right: 32px;
}
&:hover {
.cta {
display: flex;
}
}
`;
const DatasourceContentWrapper = styled.div`
display: flex;
align-items: center;
gap: 13px;
padding-left: 13.5px;
`;
interface DatasourceHomeScreenProps {
interface DBOrMostPopularPluginsProps {
editorType: string;
editorId: string;
parentEntityId: string;
@ -128,23 +79,14 @@ interface DatasourceHomeScreenProps {
};
showMostPopularPlugins?: boolean;
isCreating?: boolean;
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
showUnsupportedPluginDialog: (callback: any) => void;
isAirgappedInstance?: boolean;
showUnsupportedPluginDialog: (callback: () => void) => void;
children?: ReactNode;
}
interface ReduxDispatchProps {
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
initializeForm: (data: Record<string, any>) => void;
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
createDatasource: (data: any) => void;
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
createTempDatasource: (data: any) => void;
initializeForm: (data: Record<string, unknown>) => void;
createDatasource: (data: CreateDatasourceConfig & Datasource) => void;
createTempDatasource: typeof createTempDatasourceFromForm;
createNewApiActionBasedOnEditorType: (
editorType: string,
editorId: string,
@ -162,15 +104,35 @@ interface ReduxStateProps {
generateCRUDSupportedPlugin: GenerateCRUDEnabledPluginMap;
}
type Props = ReduxStateProps & DatasourceHomeScreenProps & ReduxDispatchProps;
interface CreateDBOrMostPopularPluginsProps {
location: {
search: string;
};
showMostPopularPlugins?: boolean;
isCreating?: boolean;
showUnsupportedPluginDialog: (callback: () => void) => void;
children?: ReactNode;
isOnboardingScreen?: boolean;
active?: boolean;
pageId: string;
addDivider?: boolean;
}
class DatasourceHomeScreen extends React.Component<Props> {
type CreateDBOrMostPopularPluginsType = ReduxStateProps &
CreateDBOrMostPopularPluginsProps &
ReduxDispatchProps;
type Props = DBOrMostPopularPluginsProps & CreateDBOrMostPopularPluginsType;
class DBOrMostPopularPlugins extends React.Component<Props> {
goToCreateDatasource = (
pluginId: string,
pluginName: string,
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
params?: any,
params?: {
skipValidPluginCheck?: boolean;
packageName?: string;
type?: PluginType;
},
) => {
const {
currentApplication,
@ -215,6 +177,7 @@ class DatasourceHomeScreen extends React.Component<Props> {
this.props.createTempDatasource({
pluginId,
type: params!.type!,
});
};
@ -244,101 +207,140 @@ class DatasourceHomeScreen extends React.Component<Props> {
} = this.props;
return (
<DatasourceHomePage>
<DatasourceCardsContainer data-testid="database-datasource-card-container">
{plugins.map((plugin, idx) => {
return plugin.type === PluginType.API ? (
!!showMostPopularPlugins ? (
<ApiCard
className="t--createBlankApiCard create-new-api"
key={`${plugin.id}_${idx}`}
onClick={() => this.handleOnClick()}
>
<CardContentWrapper data-testid="newapi-datasource-content-wrapper">
<img
alt="New"
className="curlImage t--plusImage content-icon"
src={PlusLogo}
/>
<p className="textBtn">
{createMessage(CREATE_NEW_DATASOURCE_REST_API)}
</p>
</CardContentWrapper>
{/*@ts-expect-error Fix this the next time the file is edited*/}
{isCreating && <Spinner className="cta" size={25} />}
</ApiCard>
) : null
) : (
<DatasourceCard
data-testid="database-datasource-card"
<DatasourceContainer data-testid="database-datasource-card-container">
{plugins.map((plugin, idx) => {
return plugin.type === PluginType.API ? (
!!showMostPopularPlugins ? (
<DatasourceItem
className="t--createBlankApiCard create-new-api"
dataCardWrapperTestId="newapi-datasource-content-wrapper"
handleOnClick={this.handleOnClick}
icon={getAssetUrl(`${ASSETS_CDN_URL}/plus.png`)}
key={`${plugin.id}_${idx}`}
onClick={() => {
AnalyticsUtil.logEvent("CREATE_DATA_SOURCE_CLICK", {
appName: currentApplication?.name,
pluginName: plugin.name,
pluginPackageName: plugin.packageName,
});
this.goToCreateDatasource(plugin.id, plugin.name, {
packageName: plugin.packageName,
});
}}
>
<DatasourceContentWrapper data-testid="database-datasource-content-wrapper">
<img
alt="Datasource"
className="dataSourceImage"
data-testid="database-datasource-image"
src={getAssetUrl(pluginImages[plugin.id])}
/>
<p className="t--plugin-name textBtn">{plugin.name}</p>
</DatasourceContentWrapper>
</DatasourceCard>
);
})}
</DatasourceCardsContainer>
</DatasourceHomePage>
name={createMessage(CREATE_NEW_DATASOURCE_REST_API)}
/>
) : null
) : (
<DatasourceItem
dataCardImageTestId="database-datasource-image"
dataCardTestId="database-datasource-card"
dataCardWrapperTestId="database-datasource-content-wrapper"
handleOnClick={() => {
AnalyticsUtil.logEvent("CREATE_DATA_SOURCE_CLICK", {
appName: currentApplication?.name,
pluginName: plugin.name,
pluginPackageName: plugin.packageName,
});
this.goToCreateDatasource(plugin.id, plugin.name, {
packageName: plugin.packageName,
type: plugin.type,
});
}}
icon={getAssetUrl(pluginImages[plugin.id])}
key={`${plugin.id}_${idx}`}
name={plugin.name}
rightSibling={
isCreating && <Spinner className="cta" size={"sm"} />
}
/>
);
})}
</DatasourceContainer>
);
}
}
function CreateDBOrMostPopularPlugins(props: CreateDBOrMostPopularPluginsType) {
const editorType = useEditorType(location.pathname);
const { editorId, parentEntityId, parentEntityType } =
useParentEntityInfo(editorType);
const newDatasourceRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (props.active && newDatasourceRef.current) {
scrollIntoView(newDatasourceRef.current, {
behavior: "smooth",
scrollMode: "always",
block: "start",
boundary: document.getElementById("new-integrations-wrapper"),
});
}
}, [props.active]);
if (props.plugins.length === 0) return null;
return (
<>
{props.addDivider && <StyledDivider />}
<DatasourceSection id="new-datasources" ref={newDatasourceRef}>
<DatasourceSectionHeading kind="heading-m">
{props.showMostPopularPlugins
? createMessage(CREATE_NEW_DATASOURCE_MOST_POPULAR_HEADER)
: createMessage(CREATE_NEW_DATASOURCE_DATABASE_HEADER)}
</DatasourceSectionHeading>
<DBOrMostPopularPlugins
{...props}
editorId={editorId}
editorType={editorType}
isCreating={props.isCreating}
location={location}
parentEntityId={
parentEntityId || (props.isOnboardingScreen && props.pageId) || ""
}
parentEntityType={parentEntityType}
/>
</DatasourceSection>
</>
);
}
const mapStateToProps = (
state: AppState,
props: { showMostPopularPlugins?: boolean; isAirgappedInstance?: boolean },
props: {
active?: boolean;
pageId: string;
showMostPopularPlugins?: boolean;
isOnboardingScreen?: boolean;
isCreating?: boolean;
},
) => {
const { datasources } = state.entities;
const mostPopularPlugins = getMostPopularPlugins(state);
const filteredMostPopularPlugins: Plugin[] = !!props?.isAirgappedInstance
const isAirgappedInstance = isAirgapped();
const searchedPlugin = (
pluginSearchSelector(state, "search") || ""
).toLocaleLowerCase();
const filteredMostPopularPlugins: Plugin[] = !!isAirgappedInstance
? mostPopularPlugins.filter(
(plugin: Plugin) =>
plugin?.packageName !== PluginPackageName.GOOGLE_SHEETS,
)
: mostPopularPlugins;
let plugins = !!props?.showMostPopularPlugins
? filteredMostPopularPlugins
: getDBPlugins(state);
plugins = plugins.filter((plugin) =>
plugin.name.toLocaleLowerCase().includes(searchedPlugin),
);
return {
pluginImages: getPluginImages(state),
plugins: !!props?.showMostPopularPlugins
? filteredMostPopularPlugins
: getDBPlugins(state),
plugins,
currentApplication: getCurrentApplication(state),
isSaving: datasources.loading,
generateCRUDSupportedPlugin: getGenerateCRUDEnabledPluginMap(state),
};
};
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const mapDispatchToProps = (dispatch: any) => {
const mapDispatchToProps = (dispatch: Dispatch<AnyAction>) => {
return {
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
initializeForm: (data: Record<string, any>) =>
initializeForm: (data: Record<string, unknown>) =>
dispatch(initialize(DATASOURCE_DB_FORM, data)),
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
createDatasource: (data: any) => dispatch(createDatasourceFromForm(data)),
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
createTempDatasource: (data: any) =>
createDatasource: (data: CreateDatasourceConfig & Datasource) =>
dispatch(createDatasourceFromForm(data)),
createTempDatasource: (data: { pluginId: string; type: PluginType }) =>
dispatch(createTempDatasourceFromForm(data)),
createNewApiActionBasedOnEditorType: (
editorType: string,
@ -362,4 +364,4 @@ const mapDispatchToProps = (dispatch: any) => {
export default connect(
mapStateToProps,
mapDispatchToProps,
)(DatasourceHomeScreen);
)(CreateDBOrMostPopularPlugins);

View File

@ -0,0 +1,64 @@
import React, { type ReactNode } from "react";
import {
DatasourceCard,
DatasourceDescription,
DatasourceImage,
DatasourceName,
DatasourceNameWrapper,
} from "./IntegrationStyledComponents";
interface DatasourceItem {
className?: string;
name: string;
icon: string;
description?: string;
handleOnClick: () => unknown;
rightSibling?: ReactNode;
dataNameTestId?: string;
dataCardTestId?: string;
dataCardWrapperTestId?: string;
dataCardDescriptionTestId?: string;
dataCardImageTestId?: string;
}
export default function DatasourceItem({
className = "",
dataCardDescriptionTestId = "datasource-description",
dataCardImageTestId = "datasource-image",
dataCardTestId = "datasource-card",
dataCardWrapperTestId = "datasource-content-wrapper",
dataNameTestId = "datasource-name",
description,
handleOnClick,
icon,
name,
rightSibling,
}: DatasourceItem) {
return (
<DatasourceCard
className={`t--create-${name} ${className}`}
data-testid={dataCardTestId}
onClick={handleOnClick}
>
<DatasourceImage
alt={name}
className="content-icon"
data-testid={dataCardImageTestId}
src={icon}
/>
<DatasourceNameWrapper data-testid={dataCardWrapperTestId}>
<DatasourceName
className="t--plugin-name"
data-testid={dataNameTestId}
renderAs="p"
>
{name}
</DatasourceName>
<DatasourceDescription data-testid={dataCardDescriptionTestId}>
{description}
</DatasourceDescription>
</DatasourceNameWrapper>
{rightSibling}
</DatasourceCard>
);
}

View File

@ -0,0 +1,61 @@
import React from "react";
import { Flex, Text } from "@appsmith/ads";
import { getAssetUrl } from "ee/utils/airgapHelpers";
import { ASSETS_CDN_URL } from "constants/ThirdPartyConstants";
import {
CREATE_NEW_DATASOURCE_AUTHENTICATED_REST_API,
createMessage,
EMPTY_SEARCH_DATASOURCES_DESCRIPTION,
EMPTY_SEARCH_DATASOURCES_TITLE,
} from "ee/constants/messages";
import { useSelector } from "react-redux";
import { pluginSearchSelector } from "./CreateNewDatasourceHeader";
import { getPlugins } from "ee/selectors/entitiesSelector";
import { PREMIUM_INTEGRATIONS } from "./PremiumDatasources/Constants";
import styled from "styled-components";
const EmptyImage = styled.img`
margin-bottom: var(--ads-v2-spaces-6);
width: 96px;
`;
export default function EmptySearchedPlugins({
isPremiumDatasourcesViewEnabled,
}: {
isPremiumDatasourcesViewEnabled: boolean;
}) {
let searchedPlugin = useSelector((state) =>
pluginSearchSelector(state, "search"),
);
searchedPlugin = (searchedPlugin || "").toLocaleLowerCase();
const plugins = useSelector(getPlugins);
let searchedItems = plugins.some((p) =>
p.name.toLocaleLowerCase().includes(searchedPlugin),
);
searchedItems =
searchedItems ||
createMessage(CREATE_NEW_DATASOURCE_AUTHENTICATED_REST_API)
.toLocaleLowerCase()
.includes(searchedPlugin) ||
(isPremiumDatasourcesViewEnabled &&
PREMIUM_INTEGRATIONS.some((p) =>
p.name.toLocaleLowerCase().includes(searchedPlugin),
));
if (searchedItems) return null;
return (
<Flex alignItems={"center"} flexDirection="column">
<EmptyImage
alt="empty search"
src={getAssetUrl(`${ASSETS_CDN_URL}/empty-search.png`)}
/>
<Text kind="heading-s">
{createMessage(EMPTY_SEARCH_DATASOURCES_TITLE)}
</Text>
<Text>{createMessage(EMPTY_SEARCH_DATASOURCES_DESCRIPTION)}</Text>
</Flex>
);
}

View File

@ -0,0 +1,73 @@
import { Divider, Text } from "@appsmith/ads";
import styled from "styled-components";
export const StyledDivider = styled(Divider)`
margin-bottom: var(--ads-spaces-7);
border-color: var(--ads-v2-color-bg-muted);
`;
export const DatasourceSection = styled.div`
gap: var(--ads-v2-spaces-5);
display: flex;
flex-direction: column;
`;
export const DatasourceSectionHeading = styled(Text)`
font-size: var(--ads-v2-font-size-10);
`;
export const DatasourceContainer = styled.div`
display: grid;
grid-template-columns: repeat(auto-fill, minmax(218.25px, 1fr));
gap: var(--ads-v2-spaces-5);
min-width: 150px;
border-radius: 4px;
align-items: center;
`;
export const DatasourceCard = styled.div`
display: flex;
align-items: center;
gap: var(--ads-v2-spaces-4);
padding: var(--ads-v2-spaces-4);
cursor: pointer;
border-radius: var(--ads-v2-border-radius);
.cta {
display: none;
margin-right: var(--ads-v2-spaces-9);
}
&:hover {
background-color: var(--ads-v2-color-bg-subtle);
.cta {
display: flex;
}
}
`;
export const DatasourceImage = styled.img`
height: 34px;
width: auto;
max-width: 100%;
flex-shrink: 0;
`;
export const DatasourceNameWrapper = styled.div`
display: flex;
flex-direction: column;
align-items: flex-start;
`;
export const DatasourceName = styled(Text)`
font-size: var(--ads-v2-font-size-6);
font-weight: var(--ads-v2-font-weight-normal);
line-height: var(--ads-v2-line-height-4);
color: var(--ads-v2-color-fg);
`;
export const DatasourceDescription = styled.div`
color: var(--ads-v2-color-fg-muted);
font-size: var(--ads-v2-font-size-3);
font-weight: var(--ads-v2-font-weight-normal);
line-height: var(--ads-v2-line-height-2);
`;

View File

@ -1,5 +1,4 @@
import React from "react";
import styled from "styled-components";
import { useDispatch, useSelector } from "react-redux";
import type { MockDatasource } from "entities/Datasource";
import { getPluginImages } from "ee/selectors/entitiesSelector";
@ -10,90 +9,27 @@ import type { AppState } from "ee/reducers";
import AnalyticsUtil from "ee/utils/AnalyticsUtil";
import { getAssetUrl } from "ee/utils/airgapHelpers";
import { DatasourceCreateEntryPoints } from "constants/Datasource";
const MockDataSourceWrapper = styled.div`
overflow: auto;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 16px;
min-width: 150px;
align-items: center;
margin-top: 8px;
`;
const Description = styled.div`
color: var(--ads-v2-color-fg-muted);
font-size: 13px;
font-weight: 400;
line-height: 17px;
letter-spacing: -0.24px;
`;
function MockDataSources(props: { mockDatasources: MockDatasource[] }) {
const workspaceId = useSelector(getCurrentWorkspaceId);
return (
<MockDataSourceWrapper className="t--mock-datasource-list">
{props.mockDatasources.map((datasource: MockDatasource, idx) => {
return (
<MockDatasourceCard
datasource={datasource}
key={`${datasource.name}_${datasource.packageName}_${idx}`}
workspaceId={workspaceId}
/>
);
})}
</MockDataSourceWrapper>
);
}
export default MockDataSources;
const CardWrapper = styled.div`
display: flex;
align-items: center;
justify-content: space-between;
height: 64px;
border-radius: var(--ads-v2-border-radius);
&:hover {
background-color: var(--ads-v2-color-bg-subtle);
cursor: pointer;
}
`;
const DatasourceImage = styled.img`
height: 34px;
width: auto;
margin: 0 auto;
max-width: 100%;
`;
const DatasourceName = styled.span`
font-size: 16px;
font-weight: 400;
line-height: 24px;
letter-spacing: -0.24px;
color: var(--ads-v2-color-fg);
`;
const DatasourceCardHeader = styled.div`
display: flex;
align-items: center;
gap: 13px;
padding-left: 13.5px;
`;
const DatasourceNameWrapper = styled.div`
display: flex;
flex-direction: column;
`;
import {
DatasourceContainer,
DatasourceSection,
DatasourceSectionHeading,
StyledDivider,
} from "./IntegrationStyledComponents";
import DatasourceItem from "./DatasourceItem";
import {
createMessage,
SAMPLE_DATASOURCE_SUBHEADING,
SAMPLE_DATASOURCES,
} from "ee/constants/messages";
import { pluginSearchSelector } from "./CreateNewDatasourceHeader";
import { Flex, Text } from "@appsmith/ads";
interface MockDatasourceCardProps {
datasource: MockDatasource;
workspaceId: string;
}
export function MockDatasourceCard(props: MockDatasourceCardProps) {
function MockDatasourceCard(props: MockDatasourceCardProps) {
const { datasource, workspaceId } = props;
const dispatch = useDispatch();
const pluginImages = useSelector(getPluginImages);
@ -137,22 +73,67 @@ export function MockDatasourceCard(props: MockDatasourceCardProps) {
};
return (
<CardWrapper className="t--mock-datasource" onClick={addMockDataSource}>
<DatasourceCardHeader className="t--datasource-name">
<DatasourceImage
alt="Datasource"
data-testid="mock-datasource-image"
src={getAssetUrl(pluginImages[currentPlugin.id])}
/>
<DatasourceNameWrapper data-testid="mock-datasource-name-wrapper">
<DatasourceName data-testid="mockdatasource-name">
{datasource.name}
</DatasourceName>
<Description data-testid="mockdatasource-description">
{datasource.description}
</Description>
</DatasourceNameWrapper>
</DatasourceCardHeader>
</CardWrapper>
<DatasourceItem
className="t--mock-datasource"
dataCardDescriptionTestId="mockdatasource-description"
dataCardImageTestId="mock-datasource-image"
dataCardWrapperTestId="mock-datasource-name-wrapper"
dataNameTestId="mockdatasource-name"
description={datasource.description}
handleOnClick={addMockDataSource}
icon={getAssetUrl(pluginImages[currentPlugin.id])}
name={datasource.name}
/>
);
}
interface MockDataSourcesProps {
mockDatasources: MockDatasource[];
preDivider?: boolean;
postDivider?: boolean;
}
export default function MockDataSources({
mockDatasources,
postDivider,
preDivider,
}: MockDataSourcesProps) {
const workspaceId = useSelector(getCurrentWorkspaceId);
let searchedPlugin = useSelector((state) =>
pluginSearchSelector(state, "search"),
);
searchedPlugin = (searchedPlugin || "").toLocaleLowerCase();
const filteredDatasources = mockDatasources.filter((m) =>
m.name.toLocaleLowerCase().includes(searchedPlugin),
);
if (filteredDatasources.length === 0) return null;
return (
<>
{preDivider && <StyledDivider />}
<DatasourceSection id="mock-database">
<Flex flexDirection="column">
<DatasourceSectionHeading kind="heading-m">
{createMessage(SAMPLE_DATASOURCES)}
</DatasourceSectionHeading>
<Text>{createMessage(SAMPLE_DATASOURCE_SUBHEADING)}</Text>
</Flex>
<DatasourceContainer className="t--mock-datasource-list">
{filteredDatasources.map((datasource: MockDatasource, idx) => {
return (
<MockDatasourceCard
datasource={datasource}
key={`${datasource.name}_${datasource.packageName}_${idx}`}
workspaceId={workspaceId}
/>
);
})}
</DatasourceContainer>
</DatasourceSection>
{postDivider && <StyledDivider />}
</>
);
}

View File

@ -1,350 +0,0 @@
import React, { useCallback, useEffect, useState, type ReactNode } from "react";
import { connect, useSelector } from "react-redux";
import styled from "styled-components";
import {
createDatasourceFromForm,
createTempDatasourceFromForm,
} from "actions/datasourceActions";
import type { AppState } from "ee/reducers";
import PlusLogo from "assets/images/Plus-logo.svg";
import GraphQLLogo from "assets/images/Graphql-logo.svg";
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 } from "ee/selectors/entitiesSelector";
import { getIsGeneratePageInitiator } from "utils/GenerateCrudUtil";
import { getAssetUrl } 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";
export const StyledContainer = styled.div`
flex: 1;
margin-top: 8px;
.textBtn {
font-size: 16px;
line-height: 24px;
margin: 0;
justify-content: center;
text-align: center;
letter-spacing: -0.24px;
color: var(--ads-v2-color-fg);
font-weight: 400;
text-decoration: none !important;
flex-wrap: wrap;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
@media (min-width: 2500px) {
.textBtn {
font-size: 18px;
}
}
@media (min-width: 2500px) {
.eachCard {
width: 240px;
height: 200px;
}
.apiImage {
margin-top: 25px;
margin-bottom: 20px;
height: 80px;
}
.curlImage {
width: 100px;
}
.createIcon {
height: 70px;
}
}
`;
export const ApiCardsContainer = styled.div`
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 16px;
text-align: center;
min-width: 150px;
border-radius: 4px;
align-items: center;
.create-new-api {
&:hover {
cursor: pointer;
}
}
`;
export const ApiCard = styled.div`
display: flex;
align-items: center;
justify-content: space-between;
height: 64px;
border-radius: var(--ads-v2-border-radius);
&:hover {
background-color: var(--ads-v2-color-bg-subtle);
cursor: pointer;
}
.content-icon {
height: 34px;
width: auto;
margin: 0 auto;
max-width: 100%;
}
.cta {
display: none;
margin-right: 32px;
}
&:hover {
.cta {
display: flex;
}
}
`;
export const CardContentWrapper = styled.div`
display: flex;
align-items: center;
gap: 13px;
padding-left: 13.5px;
`;
interface ApiHomeScreenProps {
location: {
search: string;
};
pageId: string;
plugins: Plugin[];
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
createDatasourceFromForm: (data: any) => void;
isCreating: boolean;
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
showUnsupportedPluginDialog: (callback: any) => 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
createNewApiActionBasedOnEditorType: (
editorType: string,
editorId: string,
parentEntityId: string,
parentEntityType: ActionParentEntityTypeInterface,
apiType: string,
) => void;
isOnboardingScreen?: boolean;
children?: ReactNode;
}
type Props = ApiHomeScreenProps;
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 NewApiScreen(props: Props) {
const { isCreating, isOnboardingScreen, pageId, plugins, showSaasAPIs } =
props;
const editorType = useEditorType(location.pathname);
const { editorId, parentEntityId, parentEntityType } =
useParentEntityInfo(editorType);
const generateCRUDSupportedPlugin: GenerateCRUDEnabledPluginMap = useSelector(
getGenerateCRUDEnabledPluginMap,
);
const [authApiPlugin, setAuthAPiPlugin] = useState<Plugin | undefined>();
useEffect(() => {
const plugin = plugins.find((p) => p.name === "REST API");
setAuthAPiPlugin(plugin);
}, [plugins]);
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,
});
}
}, [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.
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const handleOnClick = (actionType: string, params?: any) => {
const queryParams = getQueryParams();
const isGeneratePageInitiator = getIsGeneratePageInitiator(
queryParams.isGeneratePageMode,
);
if (
isGeneratePageInitiator &&
!params?.skipValidPluginCheck &&
!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: {
props.createTempDatasourceFromForm({
pluginId: params.pluginId,
type: params.type,
});
break;
}
case API_ACTION.AUTH_API: {
handleCreateAuthApiDatasource();
break;
}
default:
}
};
// Api plugins with Graphql
const API_PLUGINS = plugins.filter((p) =>
!showSaasAPIs
? p.packageName === PluginPackageName.GRAPHQL
: p.type === PluginType.SAAS ||
p.type === PluginType.REMOTE ||
p.type === PluginType.EXTERNAL_SAAS,
);
return (
<StyledContainer>
<ApiCardsContainer data-testid="newapi-datasource-card-container">
{!showSaasAPIs && (
<>
<ApiCard
className="t--createBlankApiCard create-new-api"
onClick={() => handleOnClick(API_ACTION.CREATE_NEW_API)}
>
<CardContentWrapper data-testid="newapi-datasource-content-wrapper">
<img
alt="New"
className="curlImage t--plusImage content-icon"
src={PlusLogo}
/>
<p className="textBtn">REST API</p>
</CardContentWrapper>
{/*@ts-expect-error Fix this the next time the file is edited*/}
{isCreating && <Spinner className="cta" size={25} />}
</ApiCard>
<ApiCard
className="t--createBlankApiGraphqlCard"
onClick={() => handleOnClick(API_ACTION.CREATE_NEW_GRAPHQL_API)}
>
<CardContentWrapper>
<img
alt="New"
className="curlImage t--plusImage content-icon"
src={GraphQLLogo}
/>
<p className="textBtn">GraphQL API</p>
</CardContentWrapper>
</ApiCard>
{authApiPlugin && (
<ApiCard
className="t--createAuthApiDatasource"
onClick={() => handleOnClick(API_ACTION.AUTH_API)}
>
<CardContentWrapper>
<img
alt="OAuth2"
className="authApiImage t--authApiImage content-icon"
src={getAssetUrl(authApiPlugin.iconLocation)}
/>
<p className="t--plugin-name textBtn">Authenticated API</p>
</CardContentWrapper>
</ApiCard>
)}
</>
)}
{API_PLUGINS.map((p) => (
<ApiCard
className={`t--createBlankApi-${p.packageName}`}
key={p.id}
onClick={() => {
AnalyticsUtil.logEvent("CREATE_DATA_SOURCE_CLICK", {
pluginName: p.name,
pluginPackageName: p.packageName,
});
handleOnClick(API_ACTION.CREATE_DATASOURCE_FORM, {
pluginId: p.id,
});
}}
>
<CardContentWrapper>
<img
alt={p.name}
className={
"content-icon saasImage t--saas-" + p.packageName + "-image"
}
src={getAssetUrl(p.iconLocation)}
/>
<p className="t--plugin-name textBtn">{p.name}</p>
</CardContentWrapper>
</ApiCard>
))}
{props.children}
</ApiCardsContainer>
</StyledContainer>
);
}
const mapStateToProps = (state: AppState) => ({
plugins: state.entities.plugins.list,
});
const mapDispatchToProps = {
createDatasourceFromForm,
createTempDatasourceFromForm,
createNewApiActionBasedOnEditorType,
};
export default connect(mapStateToProps, mapDispatchToProps)(NewApiScreen);

View File

@ -1,65 +0,0 @@
import React from "react";
import styled from "styled-components";
import DataSourceHome from "./DatasourceHome";
import type { ActionParentEntityTypeInterface } from "ee/entities/Engine/actionHelpers";
const QueryHomePage = styled.div`
display: flex;
flex-direction: column;
margin-top: 8px;
.sectionHeader {
font-weight: ${(props) => props.theme.fontWeights[2]};
font-size: ${(props) => props.theme.fontSizes[4]}px;
margin-top: 10px;
}
`;
interface QueryHomeScreenProps {
editorId: string;
editorType: string;
parentEntityId: string;
parentEntityType: ActionParentEntityTypeInterface;
isCreating: boolean;
location: {
search: string;
};
showMostPopularPlugins?: boolean;
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
showUnsupportedPluginDialog: (callback: any) => void;
isAirgappedInstance?: boolean;
}
class QueryHomeScreen extends React.Component<QueryHomeScreenProps> {
render() {
const {
editorId,
editorType,
isAirgappedInstance,
isCreating,
location,
parentEntityId,
parentEntityType,
showMostPopularPlugins,
showUnsupportedPluginDialog,
} = this.props;
return (
<QueryHomePage>
<DataSourceHome
editorId={editorId}
editorType={editorType}
isAirgappedInstance={isAirgappedInstance}
isCreating={isCreating}
location={location}
parentEntityId={parentEntityId}
parentEntityType={parentEntityType}
showMostPopularPlugins={showMostPopularPlugins}
showUnsupportedPluginDialog={showUnsupportedPluginDialog}
/>
</QueryHomePage>
);
}
}
export default QueryHomeScreen;

View File

@ -1,7 +1,7 @@
import { getAssetUrl } from "ee/utils/airgapHelpers";
import { ASSETS_CDN_URL } from "./ThirdPartyConstants";
import { ASSETS_CDN_URL } from "../../../../constants/ThirdPartyConstants";
interface PremiumIntegration {
export interface PremiumIntegration {
name: string;
icon: string;
}
@ -13,7 +13,7 @@ export const PREMIUM_INTEGRATIONS: PremiumIntegration[] = [
},
{
name: "Salesforce",
icon: getAssetUrl(`${ASSETS_CDN_URL}/salesforce-icon.png`),
icon: getAssetUrl(`${ASSETS_CDN_URL}/salesforce-image.png`),
},
{
name: "Slack",

View File

@ -27,8 +27,8 @@ import {
handleLearnMoreClick,
handleSubmitEvent,
shouldLearnMoreButtonBeVisible,
} from "utils/PremiumDatasourcesHelpers";
import { PREMIUM_INTEGRATION_CONTACT_FORM } from "constants/PremiumDatasourcesConstants";
} from "./Helpers";
import { PREMIUM_INTEGRATION_CONTACT_FORM } from "./Constants";
const FormWrapper = styled.form`
display: flex;

View File

@ -1,4 +1,4 @@
import { SCHEDULE_CALL_URL } from "constants/PremiumDatasourcesConstants";
import { SCHEDULE_CALL_URL } from "./Constants";
import { createMessage, PREMIUM_DATASOURCES } from "ee/constants/messages";
import AnalyticsUtil from "ee/utils/AnalyticsUtil";
import { isRelevantEmail } from "utils/formhelpers";

View File

@ -1,16 +1,13 @@
import React, { useState } from "react";
import { ApiCard, CardContentWrapper } from "../NewApi";
import { getAssetUrl } from "ee/utils/airgapHelpers";
import { Modal, ModalContent, Tag, Text } from "@appsmith/ads";
import { Modal, ModalContent, Tag } from "@appsmith/ads";
import styled from "styled-components";
import ContactForm from "./ContactForm";
import { PREMIUM_INTEGRATIONS } from "constants/PremiumDatasourcesConstants";
import {
getTagText,
handlePremiumDatasourceClick,
} from "utils/PremiumDatasourcesHelpers";
import { getTagText, handlePremiumDatasourceClick } from "./Helpers";
import { isFreePlan } from "ee/selectors/tenantSelectors";
import { useSelector } from "react-redux";
import DatasourceItem from "../DatasourceItem";
import type { PremiumIntegration } from "./Constants";
const ModalContentWrapper = styled(ModalContent)`
max-width: 518px;
@ -35,7 +32,9 @@ const PremiumTag = styled(Tag)<{ isBusinessOrEnterprise: boolean }>`
}
`;
export default function PremiumDatasources() {
export default function PremiumDatasources(props: {
plugins: PremiumIntegration[];
}) {
const [selectedIntegration, setSelectedIntegration] = useState<string>("");
const isFreePlanInstance = useSelector(isFreePlan);
const handleOnClick = (name: string) => {
@ -51,23 +50,16 @@ export default function PremiumDatasources() {
return (
<>
{PREMIUM_INTEGRATIONS.map((integration) => (
<ApiCard
{props.plugins.map((integration) => (
<DatasourceItem
className={`t--create-${integration.name}`}
key={integration.name}
onClick={() => {
handleOnClick={() => {
handleOnClick(integration.name);
}}
>
<CardContentWrapper>
<img
alt={integration.name}
className={"content-icon saasImage"}
src={getAssetUrl(integration.icon)}
/>
<Text className="t--plugin-name textBtn" renderAs="p">
{integration.name}
</Text>
icon={getAssetUrl(integration.icon)}
key={integration.name}
name={integration.name}
rightSibling={
<PremiumTag
isBusinessOrEnterprise={!isFreePlanInstance}
isClosable={false}
@ -75,8 +67,8 @@ export default function PremiumDatasources() {
>
{getTagText(!isFreePlanInstance)}
</PremiumTag>
</CardContentWrapper>
</ApiCard>
}
/>
))}
<Modal onOpenChange={onOpenChange} open={!!selectedIntegration}>
<ModalContentWrapper>