feat: datasource homepage ui redesign and search functionality for the datasources (#38360)
This commit is contained in:
parent
c302b64be5
commit
dfd7fdee97
|
|
@ -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(() => {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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']";
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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.`;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
110
app/client/src/pages/Editor/IntegrationEditor/AIPlugins.tsx
Normal file
110
app/client/src/pages/Editor/IntegrationEditor/AIPlugins.tsx
Normal 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);
|
||||
|
|
@ -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);
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
);
|
||||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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);
|
||||
`;
|
||||
|
|
@ -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 />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
@ -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;
|
||||
|
|
@ -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",
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user