fix: google sheet ui issues, removed redundant code (#18785)

Co-authored-by: “sneha122” <“sneha@appsmith.com”>
This commit is contained in:
Aman Agarwal 2022-12-20 20:40:18 +05:30 committed by GitHub
parent d452b2452c
commit 4a381607eb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 513 additions and 355 deletions

View File

@ -1,5 +1,6 @@
const homePage = require("../../../locators/HomePage"); const homePage = require("../../../locators/HomePage");
const reconnectDatasourceModal = require("../../../locators/ReconnectLocators"); const reconnectDatasourceModal = require("../../../locators/ReconnectLocators");
const datasource = require("../../../locators/DatasourcesEditor.json");
describe("Reconnect Datasource Modal validation while importing application", function() { describe("Reconnect Datasource Modal validation while importing application", function() {
let workspaceId; let workspaceId;
@ -39,12 +40,14 @@ describe("Reconnect Datasource Modal validation while importing application", fu
cy.get(".t--ds-list").contains("PostgreSQL"); cy.get(".t--ds-list").contains("PostgreSQL");
// check the postgres form config with default value // check the postgres form config with default value
cy.get("[data-cy='section-Connection']").should("be.visible"); cy.get("[data-cy='section-Connection']").should("be.visible");
cy.get("[data-cy='section-Authentication']").should("be.visible"); cy.get(datasource.authenticationSettingsSection).should(
cy.get("[data-cy='section-SSL (optional)']").should("be.visible"); "be.visible",
);
cy.get(datasource.sslSettingsSection).should("be.visible");
cy.get( cy.get(
"[data-cy='datasourceConfiguration.connection.mode']", "[data-cy='datasourceConfiguration.connection.mode']",
).should("contain", "Read / Write"); ).should("contain", "Read / Write");
cy.get("[data-cy='section-SSL (optional)']").click({ force: true }); cy.get(datasource.sslSettingsSection).click({ force: true });
// should expand ssl pan // should expand ssl pan
cy.get( cy.get(
"[data-cy='datasourceConfiguration.connection.ssl.authType']", "[data-cy='datasourceConfiguration.connection.ssl.authType']",

View File

@ -12,10 +12,10 @@
"MongoDB": ".t--plugin-name:contains('MongoDB')", "MongoDB": ".t--plugin-name:contains('MongoDB')",
"RESTAPI": ".t--plugin-name:contains('REST API')", "RESTAPI": ".t--plugin-name:contains('REST API')",
"PostgreSQL": ".t--plugin-name:contains('PostgreSQL')", "PostgreSQL": ".t--plugin-name:contains('PostgreSQL')",
"SMTP":".t--plugin-name:contains('SMTP')", "SMTP": ".t--plugin-name:contains('SMTP')",
"MySQL": ".t--plugin-name:contains('MySQL')", "MySQL": ".t--plugin-name:contains('MySQL')",
"GoogleSheets": ".t--plugin-name:contains('Google Sheets')", "GoogleSheets": ".t--plugin-name:contains('Google Sheets')",
"sectionAuthentication": "[data-cy=section-Authentication]", "sectionAuthentication": "[data-cy=section-Authentication] .t--collapse-section-container",
"PostgresEntity": ".t--entity-name:contains(PostgreSQL)", "PostgresEntity": ".t--entity-name:contains(PostgreSQL)",
"MySQLEntity": ".t--entity-name:contains(Mysql)", "MySQLEntity": ".t--entity-name:contains(Mysql)",
"createQuery": ".t--create-query", "createQuery": ".t--create-query",
@ -24,15 +24,15 @@
"datasourceCardMenu": ".t--datasource-menu-option", "datasourceCardMenu": ".t--datasource-menu-option",
"datasourceCardGeneratePageBtn": ".t--generate-template", "datasourceCardGeneratePageBtn": ".t--generate-template",
"datasourceMenuOptionEdit": "t--datasource-option-edit", "datasourceMenuOptionEdit": "t--datasource-option-edit",
"datasourceMenuOptionDelete":"t--datasource-option-delete", "datasourceMenuOptionDelete": "t--datasource-option-delete",
"editDatasource": ".t--edit-datasource", "editDatasource": ".t--edit-datasource",
"datasourceTitle": ".t--edit-datasource-name .bp3-editable-text-content", "datasourceTitle": ".t--edit-datasource-name .bp3-editable-text-content",
"datasourceTitleLocator": ".t--edit-datasource-name", "datasourceTitleLocator": ".t--edit-datasource-name",
"defaultDatabaseName": "input[name='datasourceConfiguration.connection.defaultDatabaseName']", "defaultDatabaseName": "input[name='datasourceConfiguration.connection.defaultDatabaseName']",
"datasourceConfigurationProperty":"input[name='datasourceConfiguration.properties[0]']", "datasourceConfigurationProperty": "input[name='datasourceConfiguration.properties[0]']",
"googleSheets":".t--plugin-name:contains('Google Sheets')", "googleSheets": ".t--plugin-name:contains('Google Sheets')",
"selConnectionType": "[data-cy='datasourceConfiguration.connection.type']", "selConnectionType": "[data-cy='datasourceConfiguration.connection.type']",
"scope":"[data-cy='authentication.scopeString']", "scope": "[data-cy='authentication.scopeString']",
"Mysql": ".t--plugin-name:contains('Mysql')", "Mysql": ".t--plugin-name:contains('Mysql')",
"ElasticSearch": ".t--plugin-name:contains('Elasticsearch')", "ElasticSearch": ".t--plugin-name:contains('Elasticsearch')",
"DynamoDB": ".t--plugin-name:contains('DynamoDB')", "DynamoDB": ".t--plugin-name:contains('DynamoDB')",
@ -51,7 +51,7 @@
"projectID": "[data-cy='datasourceConfiguration.authentication.username'] input", "projectID": "[data-cy='datasourceConfiguration.authentication.username'] input",
"serviceAccCredential": "[data-cy='datasourceConfiguration.authentication.password'] input", "serviceAccCredential": "[data-cy='datasourceConfiguration.authentication.password'] input",
"grantType": "[data-cy='authentication.grantType']", "grantType": "[data-cy='authentication.grantType']",
"authorizationURL":"[data-cy='authentication.authorizationUrl'] input", "authorizationURL": "[data-cy='authentication.authorizationUrl'] input",
"authorizationCode": ".t--dropdown-option:contains('Authorization Code')", "authorizationCode": ".t--dropdown-option:contains('Authorization Code')",
"clientCredentials": ".t--dropdown-option:contains('Client Credentials')", "clientCredentials": ".t--dropdown-option:contains('Client Credentials')",
"clientAuthentication": "[data-cy='authentication.isAuthorizationHeader']", "clientAuthentication": "[data-cy='authentication.isAuthorizationHeader']",
@ -64,20 +64,22 @@
"basic": "//div[contains(@class,'option') and text()='Basic']", "basic": "//div[contains(@class,'option') and text()='Basic']",
"basicUsername": "input[name='authentication.username']", "basicUsername": "input[name='authentication.username']",
"basicPassword": "input[name='authentication.password']", "basicPassword": "input[name='authentication.password']",
"mockUserDatabase":"div[id='mock-database'] span:contains('Users')", "mockUserDatabase": "div[id='mock-database'] span:contains('Users')",
"mockUserDatasources":".t--datasource-name:contains('Users')", "mockUserDatasources": ".t--datasource-name:contains('Users')",
"mongoUriDropdown": "//p[text()='Use Mongo Connection String URI']/following-sibling::div", "mongoUriDropdown": "//p[text()='Use Mongo Connection String URI']/following-sibling::div",
"mongoUriYes": "//div[text()='Yes']", "mongoUriYes": "//div[text()='Yes']",
"mongoUriInput":"//p[text()='Connection String URI']/following-sibling::div//input", "mongoUriInput": "//p[text()='Connection String URI']/following-sibling::div//input",
"advancedSettings": "[data-cy='section-Advanced Settings']", "advancedSettings": "[data-cy='section-Advanced Settings'] .t--collapse-section-container",
"useSelfSignedCert": ".t--connection\\.ssl\\.authType", "useSelfSignedCert": ".t--connection\\.ssl\\.authType",
"useCertInAuth": "[data-cy='authentication.useSelfSignedCert'] input", "useCertInAuth": "[data-cy='authentication.useSelfSignedCert'] input",
"certificateDetails": "[data-cy='section-Certificate Details']", "certificateDetails": "[data-cy='section-Certificate Details'] .t--collapse-section-container",
"saveBtn": ".t--save-datasource", "saveBtn": ".t--save-datasource",
"gSheetsOperationDropdown": "[data-cy='actionConfiguration.formData.command.data']", "gSheetsOperationDropdown": "[data-cy='actionConfiguration.formData.command.data']",
"gSheetsEntityDropdown": "[data-cy='actionConfiguration.formData.entityType.data']", "gSheetsEntityDropdown": "[data-cy='actionConfiguration.formData.entityType.data']",
"gSheetsInsertOneOption": ".t--dropdown-option:contains('Insert One')", "gSheetsInsertOneOption": ".t--dropdown-option:contains('Insert One')",
"gSheetsSheetRowsOption": ".t--dropdown-option:contains('Sheet Row(s)')", "gSheetsSheetRowsOption": ".t--dropdown-option:contains('Sheet Row(s)')",
"gSheetsCodeMirrorPlaceholder": ".CodeMirror-placeholder" "gSheetsCodeMirrorPlaceholder": ".CodeMirror-placeholder",
"connectionSettingsSection": "[data-cy='section-Connection'] .t--collapse-section-container",
"authenticationSettingsSection": "[data-cy='section-Authentication'] .t--collapse-section-container",
"sslSettingsSection": "[data-cy='section-SSL (optional)'] .t--collapse-section-container"
} }

View File

@ -24,7 +24,8 @@ export class DataSources {
"input[name='datasourceConfiguration.authentication.databaseName']"; "input[name='datasourceConfiguration.authentication.databaseName']";
private _username = private _username =
"input[name='datasourceConfiguration.authentication.username']"; "input[name='datasourceConfiguration.authentication.username']";
private _sectionAuthentication = "[data-cy=section-Authentication]"; private _sectionAuthentication =
"[data-cy=section-Authentication] .t--collapse-section-container";
private _password = private _password =
"input[name = 'datasourceConfiguration.authentication.password']"; "input[name = 'datasourceConfiguration.authentication.password']";
private _testDs = ".t--test-datasource"; private _testDs = ".t--test-datasource";

View File

@ -315,6 +315,9 @@ export const OAUTH_AUTHORIZATION_FAILED =
export const OAUTH_AUTHORIZATION_APPSMITH_ERROR = "Something went wrong."; export const OAUTH_AUTHORIZATION_APPSMITH_ERROR = "Something went wrong.";
export const OAUTH_APPSMITH_TOKEN_NOT_FOUND = "Appsmith token not found"; export const OAUTH_APPSMITH_TOKEN_NOT_FOUND = "Appsmith token not found";
export const GSHEET_AUTHORIZATION_ERROR =
"Data source is not authorized, please authorize to continue.";
export const LOCAL_STORAGE_QUOTA_EXCEEDED_MESSAGE = () => export const LOCAL_STORAGE_QUOTA_EXCEEDED_MESSAGE = () =>
"Error saving a key in localStorage. You have exceeded the allowed storage size limit"; "Error saving a key in localStorage. You have exceeded the allowed storage size limit";
export const LOCAL_STORAGE_NO_SPACE_LEFT_ON_DEVICE_MESSAGE = () => export const LOCAL_STORAGE_NO_SPACE_LEFT_ON_DEVICE_MESSAGE = () =>
@ -1400,6 +1403,11 @@ export const PAGE_SETTINGS_SET_AS_HOMEPAGE_TOOLTIP_NON_HOME_PAGE = () =>
export const PAGE_SETTINGS_ACTION_NAME_CONFLICT_ERROR = (name: string) => export const PAGE_SETTINGS_ACTION_NAME_CONFLICT_ERROR = (name: string) =>
`${name} is already being used.`; `${name} is already being used.`;
export const NEW_QUERY_BUTTON_TEXT = () => "New Query";
export const NEW_API_BUTTON_TEXT = () => "New API";
export const GENERATE_NEW_PAGE_BUTTON_TEXT = () => "GENERATE NEW PAGE";
export const RECONNECT_BUTTON_TEXT = () => "RECONNECT";
// Alert options and labels for showMessage types // Alert options and labels for showMessage types
export const ALERT_STYLE_OPTIONS = [ export const ALERT_STYLE_OPTIONS = [
{ label: "Info", value: "'info'", id: "info" }, { label: "Info", value: "'info'", id: "info" },

View File

@ -22,6 +22,7 @@ const IconContainer = styled.div`
//width: 100%; //width: 100%;
height: 30px; height: 30px;
display: flex; display: flex;
flex-shrink: 0;
align-items: center; align-items: center;
cursor: pointer; cursor: pointer;
padding-left: 16px; padding-left: 16px;

View File

@ -2,6 +2,7 @@ import React, { useCallback, useEffect } from "react";
import { Collapse, Icon } from "@blueprintjs/core"; import { Collapse, Icon } from "@blueprintjs/core";
import styled from "styled-components"; import styled from "styled-components";
import { Icon as AdsIcon, IconName, IconSize } from "design-system"; import { Icon as AdsIcon, IconName, IconSize } from "design-system";
import { Colors } from "constants/Colors";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { AppState } from "@appsmith/reducers"; import { AppState } from "@appsmith/reducers";
import { getDatasourceCollapsibleState } from "selectors/ui"; import { getDatasourceCollapsibleState } from "selectors/ui";
@ -31,8 +32,8 @@ const SectionContainer = styled.div`
`; `;
const TopBorder = styled.div` const TopBorder = styled.div`
height: 2px; height: 1px;
background-color: #d0d7dd; background-color: ${Colors.ALTO};
margin-top: 24px; margin-top: 24px;
margin-bottom: 24px; margin-bottom: 24px;
`; `;
@ -46,12 +47,21 @@ interface ComponentProps {
name: IconName; name: IconName;
color?: string; color?: string;
}; };
showTopBorder?: boolean;
showSection?: boolean;
} }
type Props = ComponentProps; type Props = ComponentProps;
function Collapsible(props: Props) { function Collapsible(props: Props) {
const { children, defaultIsOpen, headerIcon, title } = props; const {
children,
defaultIsOpen,
headerIcon,
showSection = true,
showTopBorder = true,
title,
} = props;
const dispatch = useDispatch(); const dispatch = useDispatch();
const isOpen = useSelector((state: AppState) => const isOpen = useSelector((state: AppState) =>
getDatasourceCollapsibleState(state, title), getDatasourceCollapsibleState(state, title),
@ -69,35 +79,35 @@ function Collapsible(props: Props) {
}, [defaultIsOpen, isOpen]); }, [defaultIsOpen, isOpen]);
return ( return (
<> <section data-cy={`section-${title}`} data-replay-id={`section-${title}`}>
<TopBorder className="t--collapse-top-border" /> {showTopBorder && <TopBorder className="t--collapse-top-border" />}
<SectionContainer {showSection && (
className="t--collapse-section-container" <SectionContainer
data-cy={`section-${title}`} className="t--collapse-section-container"
data-replay-id={`section-${title}`} onClick={() => setIsOpen(!isOpen)}
onClick={() => setIsOpen(!isOpen)} >
> <SectionLabel>
<SectionLabel> {title}
{title} {headerIcon && (
{headerIcon && ( <AdsIcon
<AdsIcon fillColor={headerIcon.color}
fillColor={headerIcon.color} name={headerIcon.name}
name={headerIcon.name} size={IconSize.MEDIUM}
size={IconSize.MEDIUM} />
/> )}
)} </SectionLabel>
</SectionLabel> <Icon
<Icon icon={isOpen ? "chevron-up" : "chevron-down"}
icon={isOpen ? "chevron-up" : "chevron-down"} iconSize={16}
iconSize={16} style={{ color: "#2E3D49" }}
style={{ color: "#2E3D49" }} />
/> </SectionContainer>
</SectionContainer> )}
<Collapse isOpen={isOpen} keepChildrenMounted> <Collapse isOpen={isOpen} keepChildrenMounted>
{children} {children}
</Collapse> </Collapse>
</> </section>
); );
} }

View File

@ -31,14 +31,18 @@ const Header = styled.div`
const Wrapper = styled.div` const Wrapper = styled.div`
display: flex; display: flex;
flex-direction: column; flex-direction: column;
border-top: 1px solid #d0d7dd;
border-bottom: 1px solid #d0d7dd; border-bottom: 1px solid #d0d7dd;
padding-top: 24px; padding-top: 24px;
padding-bottom: 24px; padding-bottom: 24px;
margin-top: 18px;
`; `;
function Connected() { function Connected({
errorComponent,
showDatasourceSavedText = true,
}: {
errorComponent?: JSX.Element | null;
showDatasourceSavedText?: boolean;
}) {
const params = useParams<{ datasourceId: string }>(); const params = useParams<{ datasourceId: string }>();
const datasource = useSelector((state: AppState) => const datasource = useSelector((state: AppState) =>
@ -67,25 +71,26 @@ function Connected() {
return ( return (
<Wrapper> <Wrapper>
<Header> {showDatasourceSavedText && (
<ConnectedText> <Header>
<HeaderIcons.SAVE_SUCCESS <ConnectedText>
color={Colors.GREEN} <HeaderIcons.SAVE_SUCCESS
height={30} color={Colors.GREEN}
width={30} height={30}
width={30}
/>
<div style={{ marginLeft: "12px" }}>Datasource Saved</div>
</ConnectedText>
<NewActionButton
datasource={datasource}
disabled={!canCreateDatasourceActions}
eventFrom="datasource-pane"
plugin={plugin}
/> />
</Header>
<div style={{ marginLeft: "12px" }}>Datasource Saved</div> )}
</ConnectedText> {errorComponent}
<div style={{ marginTop: showDatasourceSavedText ? "30px" : "" }}>
<NewActionButton
datasource={datasource}
disabled={!canCreateDatasourceActions}
eventFrom="datasource-pane"
plugin={plugin}
/>
</Header>
<div style={{ marginTop: "30px" }}>
{!isNil(currentFormConfig) && !isNil(datasource) ? ( {!isNil(currentFormConfig) && !isNil(datasource) ? (
<RenderDatasourceInformation <RenderDatasourceInformation
config={currentFormConfig[0]} config={currentFormConfig[0]}

View File

@ -5,8 +5,7 @@ import _ from "lodash";
import { DATASOURCE_DB_FORM } from "@appsmith/constants/forms"; import { DATASOURCE_DB_FORM } from "@appsmith/constants/forms";
import { Icon } from "@blueprintjs/core"; import { Icon } from "@blueprintjs/core";
import FormTitle from "./FormTitle"; import FormTitle from "./FormTitle";
import { Button, Callout, Category, Variant } from "design-system"; import { Callout, Category, Variant } from "design-system";
import { Colors } from "constants/Colors";
import CollapsibleHelp from "components/designSystems/appsmith/help/CollapsibleHelp"; import CollapsibleHelp from "components/designSystems/appsmith/help/CollapsibleHelp";
import Connected from "./Connected"; import Connected from "./Connected";
import { Datasource } from "entities/Datasource"; import { Datasource } from "entities/Datasource";
@ -18,6 +17,7 @@ import { convertArrayToSentence } from "utils/helpers";
import { PluginType } from "entities/Action"; import { PluginType } from "entities/Action";
import { AppState } from "@appsmith/reducers"; import { AppState } from "@appsmith/reducers";
import { import {
EditDatasourceButton,
FormTitleContainer, FormTitleContainer,
Header, Header,
JSONtoForm, JSONtoForm,
@ -66,16 +66,6 @@ const CollapsibleWrapper = styled.div`
width: max-content; width: max-content;
`; `;
const EditDatasourceButton = styled(Button)`
padding: 10px 20px;
&&&& {
height: 32px;
max-width: 160px;
border: 1px solid ${Colors.HIT_GRAY};
width: auto;
}
`;
class DatasourceDBEditor extends JSONtoForm<Props> { class DatasourceDBEditor extends JSONtoForm<Props> {
componentDidUpdate(prevProps: Props) { componentDidUpdate(prevProps: Props) {
if (prevProps.datasourceId !== this.props.datasourceId) { if (prevProps.datasourceId !== this.props.datasourceId) {

View File

@ -21,11 +21,13 @@ const Value = styled.div`
const ValueWrapper = styled.div` const ValueWrapper = styled.div`
display: inline-block; display: inline-block;
margin-left: 10px; &:not(:first-child) {
margin-left: 10px;
}
`; `;
const FieldWrapper = styled.div` const FieldWrapper = styled.div`
&:not(first-child) { &:not(:first-child) {
margin-top: 9px; margin-top: 9px;
} }
`; `;

View File

@ -7,18 +7,48 @@ import { ControlProps } from "components/formControls/BaseControl";
import { Datasource } from "entities/Datasource"; import { Datasource } from "entities/Datasource";
import { isHidden, isKVArray } from "components/formControls/utils"; import { isHidden, isKVArray } from "components/formControls/utils";
import log from "loglevel"; import log from "loglevel";
import CenteredWrapper from "components/designSystems/appsmith/CenteredWrapper";
import CloseEditor from "components/editorComponents/CloseEditor"; import CloseEditor from "components/editorComponents/CloseEditor";
import { getType, Types } from "utils/TypeHelpers"; import { getType, Types } from "utils/TypeHelpers";
import { Colors } from "constants/Colors";
import { Button } from "design-system"; import { Button } from "design-system";
export const LoadingContainer = styled(CenteredWrapper)` export const PluginImageWrapper = styled.div`
height: 50%; height: 34px;
width: 34px;
padding: 8px;
display: flex;
align-items: center;
justify-content: center;
background: ${Colors.GREY_200};
border-radius: 100%;
img {
height: 100%;
width: auto;
}
`; `;
export const PluginImage = styled.img` export const PluginImage = (props: any) => {
height: 40px; return (
width: auto; <PluginImageWrapper>
<img {...props} />
</PluginImageWrapper>
);
};
export const FormContainer = styled.div`
display: flex;
flex-direction: column;
height: 100%;
`;
export const FormContainerBody = styled.div`
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
flex-grow: 1;
overflow-y: auto;
padding: 20px;
`; `;
export const FormTitleContainer = styled.div` export const FormTitleContainer = styled.div`
@ -32,13 +62,13 @@ export const Header = styled.div`
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
border-bottom: 1px solid ${Colors.ALTO};
padding-bottom: 24px;
//margin-top: 16px; //margin-top: 16px;
`; `;
export const SaveButtonContainer = styled.div` export const ActionWrapper = styled.div`
margin-top: 24px;
display: flex; display: flex;
justify-content: flex-end;
`; `;
export const ActionButton = styled(Button)` export const ActionButton = styled(Button)`
@ -54,19 +84,13 @@ export const ActionButton = styled(Button)`
} }
`; `;
const DBForm = styled.div` export const EditDatasourceButton = styled(Button)`
flex: 1; padding: 10px 20px;
padding: 20px; &&&& {
margin-right: 0px; height: 36px;
overflow: auto; max-width: 160px;
.backBtn { border: 1px solid ${Colors.HIT_GRAY};
padding-bottom: 1px; width: auto;
cursor: pointer;
}
.backBtnText {
font-size: 16px;
font-weight: 500;
cursor: pointer;
} }
`; `;
@ -196,15 +220,14 @@ export class JSONtoForm<
return formData; return formData;
}; };
renderForm = (content: any) => { renderForm = (formContent: any) => {
return ( return (
<div <FormContainer className="t--json-to-form-wrapper">
className="t--json-to-form-wrapper"
style={{ height: "100%", display: "flex", flexDirection: "column" }}
>
<CloseEditor /> <CloseEditor />
<DBForm>{content}</DBForm> <FormContainerBody className="t--json-to-form-body">
</div> {formContent}
</FormContainerBody>
</FormContainer>
); );
}; };
@ -214,6 +237,8 @@ export class JSONtoForm<
<Collapsible <Collapsible
defaultIsOpen={index === 0} defaultIsOpen={index === 0}
key={section.sectionName} key={section.sectionName}
showSection={index !== 0}
showTopBorder={index !== 0}
title={section.sectionName} title={section.sectionName}
> >
{this.renderEachConfig(section)} {this.renderEachConfig(section)}

View File

@ -8,7 +8,12 @@ import {
Toaster, Toaster,
Variant, Variant,
} from "design-system"; } from "design-system";
import { ERROR_ADD_API_INVALID_URL } from "@appsmith/constants/messages"; import {
createMessage,
ERROR_ADD_API_INVALID_URL,
NEW_API_BUTTON_TEXT,
NEW_QUERY_BUTTON_TEXT,
} from "@appsmith/constants/messages";
import { createNewQueryAction } from "actions/apiPaneActions"; import { createNewQueryAction } from "actions/apiPaneActions";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { AppState } from "@appsmith/reducers"; import { AppState } from "@appsmith/reducers";
@ -16,6 +21,7 @@ import { getCurrentPageId } from "selectors/editorSelectors";
import { Datasource } from "entities/Datasource"; import { Datasource } from "entities/Datasource";
import { Plugin } from "api/PluginApi"; import { Plugin } from "api/PluginApi";
import { EventLocation } from "utils/AnalyticsUtil"; import { EventLocation } from "utils/AnalyticsUtil";
import { noop } from "utils/AppsmithUtils";
const ActionButton = styled(Button)` const ActionButton = styled(Button)`
padding: 10px 10px; padding: 10px 10px;
@ -43,9 +49,10 @@ type NewActionButtonProps = {
isLoading?: boolean; isLoading?: boolean;
eventFrom?: string; // this is to track from where the new action is being generated eventFrom?: string; // this is to track from where the new action is being generated
plugin?: Plugin; plugin?: Plugin;
style?: any;
}; };
function NewActionButton(props: NewActionButtonProps) { function NewActionButton(props: NewActionButtonProps) {
const { datasource, disabled, plugin } = props; const { datasource, disabled, plugin, style = {} } = props;
const pluginType = plugin?.type; const pluginType = plugin?.type;
const [isSelected, setIsSelected] = useState(false); const [isSelected, setIsSelected] = useState(false);
@ -88,13 +95,18 @@ function NewActionButton(props: NewActionButtonProps) {
return ( return (
<ActionButton <ActionButton
className="t--create-query" className="t--create-query"
disabled={disabled} disabled={!!disabled}
icon="plus" icon="plus"
iconPosition={IconPositions.left} iconPosition={IconPositions.left}
isLoading={isSelected || props.isLoading} isLoading={isSelected || props.isLoading}
onClick={createQueryAction} onClick={disabled ? noop : createQueryAction}
style={style}
tag="button" tag="button"
text={pluginType === PluginType.DB ? "New Query" : "New API"} text={
pluginType === PluginType.DB || pluginType === PluginType.SAAS
? createMessage(NEW_QUERY_BUTTON_TEXT)
: createMessage(NEW_API_BUTTON_TEXT)
}
/> />
); );
} }

View File

@ -13,7 +13,6 @@ import {
import AnalyticsUtil from "utils/AnalyticsUtil"; import AnalyticsUtil from "utils/AnalyticsUtil";
import FormControl from "pages/Editor/FormControl"; import FormControl from "pages/Editor/FormControl";
import { StyledInfo } from "components/formControls/InputTextControl"; import { StyledInfo } from "components/formControls/InputTextControl";
import CenteredWrapper from "components/designSystems/appsmith/CenteredWrapper";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { AppState } from "@appsmith/reducers"; import { AppState } from "@appsmith/reducers";
import { ApiActionConfig, PluginType } from "entities/Action"; import { ApiActionConfig, PluginType } from "entities/Action";
@ -40,12 +39,7 @@ import {
GrantType, GrantType,
SSLType, SSLType,
} from "entities/Datasource/RestAPIForm"; } from "entities/Datasource/RestAPIForm";
import { import { createMessage, INVALID_URL } from "@appsmith/constants/messages";
createMessage,
CONTEXT_DELETE,
CONFIRM_CONTEXT_DELETE,
INVALID_URL,
} from "@appsmith/constants/messages";
import Collapsible from "./Collapsible"; import Collapsible from "./Collapsible";
import _ from "lodash"; import _ from "lodash";
import FormLabel from "components/editorComponents/FormLabel"; import FormLabel from "components/editorComponents/FormLabel";
@ -54,11 +48,18 @@ import { Callout } from "design-system";
import CloseEditor from "components/editorComponents/CloseEditor"; import CloseEditor from "components/editorComponents/CloseEditor";
import { updateReplayEntity } from "actions/pageActions"; import { updateReplayEntity } from "actions/pageActions";
import { ENTITY_TYPE } from "entities/AppsmithConsole"; import { ENTITY_TYPE } from "entities/AppsmithConsole";
import { TEMP_DATASOURCE_ID } from "constants/Datasource";
import { import {
hasDeleteDatasourcePermission, FormContainer,
hasManageDatasourcePermission, FormContainerBody,
} from "@appsmith/utils/permissionHelpers"; FormTitleContainer,
Header,
PluginImage,
} from "./JSONtoForm";
import DatasourceAuth, {
DatasourceButtonType,
} from "pages/common/datasourceAuth";
import { TEMP_DATASOURCE_ID } from "constants/Datasource";
import { hasManageDatasourcePermission } from "@appsmith/utils/permissionHelpers";
interface DatasourceRestApiEditorProps { interface DatasourceRestApiEditorProps {
initializeReplayEntity: (id: string, data: any) => void; initializeReplayEntity: (id: string, data: any) => void;
@ -99,68 +100,10 @@ interface DatasourceRestApiEditorProps {
type Props = DatasourceRestApiEditorProps & type Props = DatasourceRestApiEditorProps &
InjectedFormProps<ApiDatasourceForm, DatasourceRestApiEditorProps>; InjectedFormProps<ApiDatasourceForm, DatasourceRestApiEditorProps>;
const RestApiForm = styled.div`
flex: 1;
padding: 20px;
margin-left: 10px;
margin-right: 0px;
overflow: auto;
.backBtn {
padding-bottom: 1px;
cursor: pointer;
}
.backBtnText {
font-size: 16px;
font-weight: 500;
cursor: pointer;
}
`;
const FormInputContainer = styled.div` const FormInputContainer = styled.div`
margin-top: 16px; margin-top: 16px;
`; `;
export const LoadingContainer = styled(CenteredWrapper)`
height: 50%;
`;
const PluginImage = styled.img`
height: 40px;
width: auto;
`;
export const FormTitleContainer = styled.div`
flex-direction: row;
display: flex;
align-items: center;
`;
export const Header = styled.div`
flex-direction: row;
display: flex;
align-items: center;
justify-content: space-between;
`;
const SaveButtonContainer = styled.div`
margin-top: 24px;
display: flex;
justify-content: flex-end;
`;
const ActionButton = styled(Button)`
&&& {
width: auto;
min-width: 74px;
margin-right: 9px;
min-height: 32px;
& > span {
max-width: 100%;
}
}
`;
const StyledButton = styled(Button)` const StyledButton = styled(Button)`
&&&& { &&&& {
width: 87px; width: 87px;
@ -289,26 +232,23 @@ class DatasourceRestAPIEditor extends React.Component<
} }
}; };
disableSave = (): boolean => { validate = (): boolean => {
const { datasource, datasourceId, formData } = this.props; const { datasource, datasourceId, formData } = this.props;
const createMode = datasourceId === TEMP_DATASOURCE_ID; const createMode = datasourceId === TEMP_DATASOURCE_ID;
const canManageDatasource = hasManageDatasourcePermission( const canManageDatasource = hasManageDatasourcePermission(
datasource?.userPermissions || [], datasource?.userPermissions || [],
); );
if (!formData) return true; if (!formData) return true;
return ( return !formData.url || (!createMode && !canManageDatasource);
!formData.url ||
!this.props.isFormDirty ||
(!createMode && !canManageDatasource)
);
}; };
getSanitizedFormData = () =>
formValuesToDatasource(this.props.datasource, this.props.formData);
save = (onSuccess?: ReduxAction<unknown>) => { save = (onSuccess?: ReduxAction<unknown>) => {
this.props.toggleSaveActionFlag(true); this.props.toggleSaveActionFlag(true);
const normalizedValues = formValuesToDatasource( const normalizedValues = this.getSanitizedFormData();
this.props.datasource,
this.props.formData,
);
AnalyticsUtil.logEvent("SAVE_DATA_SOURCE_CLICK", { AnalyticsUtil.logEvent("SAVE_DATA_SOURCE_CLICK", {
pageId: this.props.pageId, pageId: this.props.pageId,
appId: this.props.applicationId, appId: this.props.applicationId,
@ -387,17 +327,14 @@ class DatasourceRestAPIEditor extends React.Component<
return { isValid: true, message: "" }; return { isValid: true, message: "" };
}; };
handleDeleteDatasource = (datasourceId: string) => {
this.props.deleteDatasource(datasourceId);
this.props.datasourceDeleteTrigger();
};
render = () => { render = () => {
const { datasource, formData, hiddenHeader, pageId } = this.props;
return ( return (
<> <FormContainer>
{/* this is true during import flow */} {/* this is true during import flow */}
{!this.props.hiddenHeader && <CloseEditor />} {!hiddenHeader && <CloseEditor />}
<RestApiForm> <FormContainerBody>
<form <form
onSubmit={(e) => { onSubmit={(e) => {
e.preventDefault(); e.preventDefault();
@ -405,10 +342,23 @@ class DatasourceRestAPIEditor extends React.Component<
> >
{this.renderHeader()} {this.renderHeader()}
{this.renderEditor()} {this.renderEditor()}
{this.renderSave()} <DatasourceAuth
datasource={datasource}
datasourceButtonConfiguration={[
DatasourceButtonType.DELETE,
DatasourceButtonType.SAVE,
]}
datasourceDeleteTrigger={this.props.datasourceDeleteTrigger}
formData={formData}
getSanitizedFormData={this.getSanitizedFormData}
isFormDirty={this.props.isFormDirty}
isInvalid={this.validate()}
pageId={pageId}
shouldRender
/>
</form> </form>
</RestApiForm> </FormContainerBody>
</> </FormContainer>
); );
}; };
@ -437,51 +387,6 @@ class DatasourceRestAPIEditor extends React.Component<
) : null; ) : null;
}; };
renderSave = () => {
const { datasourceId, hiddenHeader, isDeleting, isSaving } = this.props;
const createMode = datasourceId === TEMP_DATASOURCE_ID;
const canDeleteDatasource = hasDeleteDatasourcePermission(
this.props.datasource?.userPermissions || [],
);
return (
<SaveButtonContainer>
{!hiddenHeader && (
<ActionButton
category={Category.primary}
className="t--delete-datasource"
disabled={createMode || !canDeleteDatasource}
isLoading={isDeleting}
onClick={() => {
this.state.confirmDelete
? this.handleDeleteDatasource(datasourceId)
: this.setState({ confirmDelete: true });
}}
size="medium"
tag="button"
text={
this.state.confirmDelete
? createMessage(CONFIRM_CONTEXT_DELETE)
: createMessage(CONTEXT_DELETE)
}
variant={Variant.danger}
/>
)}
<StyledButton
category={Category.primary}
className="t--save-datasource"
disabled={this.disableSave()}
isLoading={isSaving}
onClick={() => this.save()}
size="medium"
tag="button"
text="Save"
variant={Variant.success}
/>
</SaveButtonContainer>
);
};
renderEditor = () => { renderEditor = () => {
const { const {
datasource, datasource,
@ -506,6 +411,46 @@ class DatasourceRestAPIEditor extends React.Component<
messages.map((msg, i) => ( messages.map((msg, i) => (
<Callout fill key={i} text={msg} variant={Variant.warning} /> <Callout fill key={i} text={msg} variant={Variant.warning} />
))} ))}
{this.renderGeneralSettings()}
{this.renderAuthFields()}
{this.renderOauth2AdvancedSettings()}
{this.renderSelfSignedCertificateFields()}
{formData.authType &&
formData.authType === AuthType.OAuth2 &&
_.get(authentication, "grantType") ===
GrantType.AuthorizationCode && (
<FormInputContainer>
<AuthorizeButton
category={Category.primary}
className="t--save-and-authorize-datasource"
disabled={this.validate() || !this.props.isFormDirty}
isLoading={isSaving}
onClick={() =>
this.save(
redirectAuthorizationCode(
pageId,
datasourceId,
PluginType.API,
),
)
}
tag="button"
text={
isAuthorized ? "Save and Re-Authorize" : "Save and Authorize"
}
variant={Variant.success}
/>
</FormInputContainer>
)}
</>
);
};
renderGeneralSettings = () => {
const { formData } = this.props;
return (
<section data-cy="section-General" data-replay-id="section-General">
<FormInputContainer data-replay-id={btoa("url")}> <FormInputContainer data-replay-id={btoa("url")}>
{this.renderInputTextControlViaFormControl( {this.renderInputTextControlViaFormControl(
"url", "url",
@ -598,39 +543,7 @@ class DatasourceRestAPIEditor extends React.Component<
"", "",
)} )}
</FormInputContainer> </FormInputContainer>
{this.renderAuthFields()} </section>
<Collapsible title="Advanced Settings">
{this.renderOauth2AdvancedSettings()}
</Collapsible>
{this.renderSelfSignedCertificateFields()}
{formData.authType &&
formData.authType === AuthType.OAuth2 &&
_.get(authentication, "grantType") ===
GrantType.AuthorizationCode && (
<FormInputContainer>
<AuthorizeButton
category={Category.primary}
className="t--save-and-authorize-datasource"
disabled={this.disableSave()}
isLoading={isSaving}
onClick={() =>
this.save(
redirectAuthorizationCode(
pageId,
datasourceId,
PluginType.API,
),
)
}
tag="button"
text={
isAuthorized ? "Save and Re-Authorize" : "Save and Authorize"
}
variant={Variant.success}
/>
</FormInputContainer>
)}
</>
); );
}; };
@ -933,7 +846,7 @@ class DatasourceRestAPIEditor extends React.Component<
_.get(connection, "ssl.authType") === SSLType.SELF_SIGNED_CERTIFICATE; _.get(connection, "ssl.authType") === SSLType.SELF_SIGNED_CERTIFICATE;
return ( return (
<> <Collapsible title="Advanced Settings">
{isAuthenticationTypeOAuth2 && isGrantTypeAuthorizationCode && ( {isAuthenticationTypeOAuth2 && isGrantTypeAuthorizationCode && (
<FormInputContainer <FormInputContainer
data-replay-id={btoa("authentication.sendScopeWithRefreshToken")} data-replay-id={btoa("authentication.sendScopeWithRefreshToken")}
@ -1012,7 +925,7 @@ class DatasourceRestAPIEditor extends React.Component<
)} )}
</FormInputContainer> </FormInputContainer>
)} )}
</> </Collapsible>
); );
}; };

View File

@ -38,7 +38,10 @@ import {
CONFIRM_CONTEXT_DELETE, CONFIRM_CONTEXT_DELETE,
createMessage, createMessage,
CONFIRM_CONTEXT_DELETING, CONFIRM_CONTEXT_DELETING,
GENERATE_NEW_PAGE_BUTTON_TEXT,
RECONNECT_BUTTON_TEXT,
} from "@appsmith/constants/messages"; } from "@appsmith/constants/messages";
import { isDatasourceAuthorizedForQueryCreation } from "utils/editorContextUtils";
import { import {
getCurrentPageId, getCurrentPageId,
getPagePermissions, getPagePermissions,
@ -319,26 +322,34 @@ function DatasourceCard(props: DatasourceCardProps) {
</Queries> </Queries>
</div> </div>
<ButtonsWrapper className="action-wrapper"> <ButtonsWrapper className="action-wrapper">
{(!datasource.isConfigured || supportTemplateGeneration) && ( {(!datasource.isConfigured || supportTemplateGeneration) &&
<GenerateTemplateOrReconnect isDatasourceAuthorizedForQueryCreation(datasource, plugin) && (
category={Category.secondary} <GenerateTemplateOrReconnect
className={ category={Category.secondary}
datasource.isConfigured className={
? "t--generate-template" datasource.isConfigured
: "t--reconnect-btn" ? "t--generate-template"
} : "t--reconnect-btn"
onClick={ }
datasource.isConfigured ? routeToGeneratePage : editDatasource onClick={
} datasource.isConfigured
text={ ? routeToGeneratePage
datasource.isConfigured ? "GENERATE NEW PAGE" : "RECONNECT" : editDatasource
} }
/> text={
)} datasource.isConfigured
? createMessage(GENERATE_NEW_PAGE_BUTTON_TEXT)
: createMessage(RECONNECT_BUTTON_TEXT)
}
/>
)}
{datasource.isConfigured && ( {datasource.isConfigured && (
<NewActionButton <NewActionButton
datasource={datasource} datasource={datasource}
disabled={!canCreateDatasourceActions} disabled={
!canCreateDatasourceActions ||
!isDatasourceAuthorizedForQueryCreation(datasource, plugin)
}
eventFrom="active-datasources" eventFrom="active-datasources"
plugin={plugin} plugin={plugin}
/> />

View File

@ -1,9 +1,8 @@
import React from "react"; import React from "react";
import styled from "styled-components";
import _, { merge } from "lodash"; import _, { merge } from "lodash";
import { DATASOURCE_SAAS_FORM } from "@appsmith/constants/forms"; import { DATASOURCE_SAAS_FORM } from "@appsmith/constants/forms";
import FormTitle from "pages/Editor/DataSourceEditor/FormTitle"; import FormTitle from "pages/Editor/DataSourceEditor/FormTitle";
import { Button as AdsButton, Category } from "design-system"; import { Category } from "design-system";
import { Datasource } from "entities/Datasource"; import { Datasource } from "entities/Datasource";
import { import {
getFormValues, getFormValues,
@ -18,9 +17,12 @@ import {
getDatasource, getDatasource,
getPluginImages, getPluginImages,
getDatasourceFormButtonConfig, getDatasourceFormButtonConfig,
getPlugin,
} from "selectors/entitiesSelector"; } from "selectors/entitiesSelector";
import { ActionDataState } from "reducers/entityReducers/actionsReducer"; import { ActionDataState } from "reducers/entityReducers/actionsReducer";
import { import {
ActionWrapper,
EditDatasourceButton,
FormTitleContainer, FormTitleContainer,
Header, Header,
JSONtoForm, JSONtoForm,
@ -29,14 +31,24 @@ import {
} from "../DataSourceEditor/JSONtoForm"; } from "../DataSourceEditor/JSONtoForm";
import { getConfigInitialValues } from "components/formControls/utils"; import { getConfigInitialValues } from "components/formControls/utils";
import Connected from "../DataSourceEditor/Connected"; import Connected from "../DataSourceEditor/Connected";
import { Colors } from "constants/Colors";
import { getCurrentApplicationId } from "selectors/editorSelectors"; import {
import DatasourceAuth from "../../common/datasourceAuth"; getCurrentApplicationId,
getPagePermissions,
} from "selectors/editorSelectors";
import DatasourceAuth from "pages/common/datasourceAuth";
import EntityNotFoundPane from "../EntityNotFoundPane"; import EntityNotFoundPane from "../EntityNotFoundPane";
import { saasEditorDatasourceIdURL } from "RouteBuilder"; import { saasEditorDatasourceIdURL } from "RouteBuilder";
import NewActionButton from "../DataSourceEditor/NewActionButton";
import { Plugin } from "api/PluginApi";
import { isDatasourceAuthorizedForQueryCreation } from "utils/editorContextUtils";
import { PluginPackageName } from "entities/Action";
import AuthMessage from "pages/common/datasourceAuth/AuthMessage";
import { isDatasourceInViewMode } from "selectors/ui"; import { isDatasourceInViewMode } from "selectors/ui";
import { hasManageDatasourcePermission } from "@appsmith/utils/permissionHelpers"; import {
hasCreateDatasourceActionPermission,
hasManageDatasourcePermission,
} from "@appsmith/utils/permissionHelpers";
import { TEMP_DATASOURCE_ID } from "constants/Datasource"; import { TEMP_DATASOURCE_ID } from "constants/Datasource";
import { import {
createTempDatasourceFromForm, createTempDatasourceFromForm,
@ -47,15 +59,18 @@ import {
toggleSaveActionFromPopupFlag, toggleSaveActionFromPopupFlag,
} from "actions/datasourceActions"; } from "actions/datasourceActions";
import SaveOrDiscardDatasourceModal from "../DataSourceEditor/SaveOrDiscardDatasourceModal"; import SaveOrDiscardDatasourceModal from "../DataSourceEditor/SaveOrDiscardDatasourceModal";
import { GSHEET_AUTHORIZATION_ERROR } from "ce/constants/messages";
interface StateProps extends JSONtoFormProps { interface StateProps extends JSONtoFormProps {
applicationId: string; applicationId: string;
canManageDatasource?: boolean; canManageDatasource?: boolean;
canCreateDatasourceActions?: boolean;
isSaving: boolean; isSaving: boolean;
isDeleting: boolean; isDeleting: boolean;
loadingFormConfigs: boolean; loadingFormConfigs: boolean;
isNewDatasource: boolean; isNewDatasource: boolean;
pluginImage: string; pluginImage: string;
plugin?: Plugin;
pluginId: string; pluginId: string;
actions: ActionDataState; actions: ActionDataState;
datasource?: Datasource; datasource?: Datasource;
@ -89,16 +104,6 @@ type DatasourceSaaSEditorProps = StateProps &
type Props = DatasourceSaaSEditorProps & type Props = DatasourceSaaSEditorProps &
InjectedFormProps<Datasource, DatasourceSaaSEditorProps>; InjectedFormProps<Datasource, DatasourceSaaSEditorProps>;
const EditDatasourceButton = styled(AdsButton)`
padding: 10px 20px;
&&&& {
height: 32px;
max-width: 160px;
border: 1px solid ${Colors.HIT_GRAY};
width: auto;
}
`;
/* /*
**** State Variables Description **** **** State Variables Description ****
showDialog: flag used to show/hide the datasource discard popup showDialog: flag used to show/hide the datasource discard popup
@ -244,6 +249,7 @@ class DatasourceSaaSEditor extends JSONtoForm<Props, State> {
renderDataSourceConfigForm = (sections: any) => { renderDataSourceConfigForm = (sections: any) => {
const { const {
canCreateDatasourceActions,
canManageDatasource, canManageDatasource,
datasource, datasource,
datasourceButtonConfiguration, datasourceButtonConfiguration,
@ -251,12 +257,23 @@ class DatasourceSaaSEditor extends JSONtoForm<Props, State> {
formData, formData,
hiddenHeader, hiddenHeader,
pageId, pageId,
plugin,
pluginPackageName, pluginPackageName,
} = this.props; } = this.props;
const params: string = location.search; const params: string = location.search;
const viewMode = const viewMode =
!hiddenHeader && new URLSearchParams(params).get("viewMode"); !hiddenHeader && new URLSearchParams(params).get("viewMode");
/*
TODO: This flag will be removed once the multiple environment is merged to avoid design inconsistency between different datasources.
Search for: GoogleSheetPluginFlag to check for all the google sheet conditional logic throughout the code.
*/
const isGoogleSheetPlugin =
plugin?.packageName === PluginPackageName.GOOGLE_SHEETS;
const isPluginAuthorized =
plugin && isDatasourceAuthorizedForQueryCreation(formData, plugin);
const createFlow = datasourceId === TEMP_DATASOURCE_ID; const createFlow = datasourceId === TEMP_DATASOURCE_ID;
return ( return (
@ -277,36 +294,75 @@ class DatasourceSaaSEditor extends JSONtoForm<Props, State> {
</FormTitleContainer> </FormTitleContainer>
{viewMode && ( {viewMode && (
<EditDatasourceButton <ActionWrapper>
category={Category.secondary} <EditDatasourceButton
className="t--edit-datasource" category={Category.secondary}
onClick={() => { className="t--edit-datasource"
this.props.setDatasourceViewMode(false); onClick={() => {
this.props.history.replace( this.props.setDatasourceViewMode(false);
saasEditorDatasourceIdURL({ this.props.history.replace(
pageId: pageId || "", saasEditorDatasourceIdURL({
pluginPackageName, pageId: pageId || "",
datasourceId, pluginPackageName,
params: { datasourceId,
viewMode: false, params: {
}, viewMode: false,
}), },
); }),
}} );
text="EDIT" }}
/> text="EDIT"
/>
{isGoogleSheetPlugin && (
<NewActionButton
datasource={datasource}
disabled={
!canCreateDatasourceActions || !isPluginAuthorized
}
eventFrom="datasource-pane"
plugin={plugin}
style={{
marginLeft: "16px",
}}
/>
)}
</ActionWrapper>
)} )}
</Header> </Header>
)} )}
{(!viewMode || datasourceId === TEMP_DATASOURCE_ID) && ( {(!viewMode || datasourceId === TEMP_DATASOURCE_ID) && (
<> <>
{datasource && isGoogleSheetPlugin && !isPluginAuthorized ? (
<AuthMessage
datasource={datasource}
description={GSHEET_AUTHORIZATION_ERROR}
pageId={pageId}
style={{
paddingTop: "24px",
}}
/>
) : null}
{!_.isNil(sections) {!_.isNil(sections)
? _.map(sections, this.renderMainSection) ? _.map(sections, this.renderMainSection)
: null} : null}
{""} {""}
</> </>
)} )}
{viewMode && <Connected />} {viewMode && (
<Connected
errorComponent={
datasource && isGoogleSheetPlugin && !isPluginAuthorized ? (
<AuthMessage
actionType="authorize"
datasource={datasource}
description={GSHEET_AUTHORIZATION_ERROR}
pageId={pageId}
/>
) : null
}
showDatasourceSavedText={!isGoogleSheetPlugin}
/>
)}
{/* Render datasource form call-to-actions */} {/* Render datasource form call-to-actions */}
{datasource && ( {datasource && (
<DatasourceAuth <DatasourceAuth
@ -317,6 +373,7 @@ class DatasourceSaaSEditor extends JSONtoForm<Props, State> {
getSanitizedFormData={_.memoize(this.getSanitizedData)} getSanitizedFormData={_.memoize(this.getSanitizedData)}
isInvalid={this.validate()} isInvalid={this.validate()}
pageId={pageId} pageId={pageId}
shouldDisplayAuthMessage={!isGoogleSheetPlugin}
shouldRender={!viewMode} shouldRender={!viewMode}
triggerSave={this.props.isDatasourceBeingSavedFromPopup} triggerSave={this.props.isDatasourceBeingSavedFromPopup}
/> />
@ -344,6 +401,7 @@ const mapStateToProps = (state: AppState, props: any) => {
const { formConfigs } = plugins; const { formConfigs } = plugins;
const formData = getFormValues(DATASOURCE_SAAS_FORM)(state) as Datasource; const formData = getFormValues(DATASOURCE_SAAS_FORM)(state) as Datasource;
const pluginId = _.get(datasource, "pluginId", ""); const pluginId = _.get(datasource, "pluginId", "");
const plugin = getPlugin(state, pluginId);
const formConfig = formConfigs[pluginId]; const formConfig = formConfigs[pluginId];
const initialValues = {}; const initialValues = {};
if (formConfig) { if (formConfig) {
@ -360,12 +418,18 @@ const mapStateToProps = (state: AppState, props: any) => {
? true ? true
: isDirty(DATASOURCE_SAAS_FORM)(state); : isDirty(DATASOURCE_SAAS_FORM)(state);
const datsourcePermissions = datasource?.userPermissions || []; const datasourcePermissions = datasource?.userPermissions || [];
const canManageDatasource = hasManageDatasourcePermission( const canManageDatasource = hasManageDatasourcePermission(
datsourcePermissions, datasourcePermissions,
); );
const pagePermissions = getPagePermissions(state);
const canCreateDatasourceActions = hasCreateDatasourceActionPermission([
...datasourcePermissions,
...pagePermissions,
]);
return { return {
datasource, datasource,
datasourceButtonConfiguration, datasourceButtonConfiguration,
@ -377,6 +441,7 @@ const mapStateToProps = (state: AppState, props: any) => {
viewMode: viewMode ?? !props.fromImporting, viewMode: viewMode ?? !props.fromImporting,
isNewDatasource: datasourcePane.newDatasource === TEMP_DATASOURCE_ID, isNewDatasource: datasourcePane.newDatasource === TEMP_DATASOURCE_ID,
pageId: props.pageId || props.match?.params?.pageId, pageId: props.pageId || props.match?.params?.pageId,
plugin: plugin,
pluginImage: getPluginImages(state)[pluginId], pluginImage: getPluginImages(state)[pluginId],
pluginPackageName: pluginPackageName:
props.pluginPackageName || props.match?.params?.pluginPackageName, props.pluginPackageName || props.match?.params?.pluginPackageName,
@ -391,6 +456,7 @@ const mapStateToProps = (state: AppState, props: any) => {
isDatasourceBeingSavedFromPopup: isDatasourceBeingSavedFromPopup:
state.entities.datasources.isDatasourceBeingSavedFromPopup, state.entities.datasources.isDatasourceBeingSavedFromPopup,
isFormDirty, isFormDirty,
canCreateDatasourceActions,
}; };
}; };

View File

@ -106,6 +106,13 @@ const ContentWrapper = styled.div`
.t--json-to-form-wrapper { .t--json-to-form-wrapper {
width: 100%; width: 100%;
.t--json-to-form-body {
padding: 0 20px;
.t--collapse-section-container {
margin-top: 20px;
}
}
.t--close-editor { .t--close-editor {
display: none; display: none;
} }
@ -226,7 +233,6 @@ const TooltipWrapper = styled.div`
`; `;
const DBFormWrapper = styled.div` const DBFormWrapper = styled.div`
padding: 10px;
width: calc(100% - 206px); width: calc(100% - 206px);
overflow: auto; overflow: auto;

View File

@ -0,0 +1,50 @@
import { AppState } from "@appsmith/reducers";
import { redirectAuthorizationCode } from "actions/datasourceActions";
import { CalloutV2 } from "design-system";
import { Datasource } from "entities/Datasource";
import React from "react";
import { useDispatch, useSelector } from "react-redux";
import { getPluginTypeFromDatasourceId } from "selectors/entitiesSelector";
import styled from "styled-components";
const StyledAuthMessage = styled.div`
max-width: 560px;
margin-bottom: 16px;
& > div {
margin: 0;
}
`;
type AuthMessageProps = {
// We can handle for other action types as well eg. save, delete etc.
actionType?: "authorize";
datasource: Datasource;
description: string;
pageId?: string;
style?: any;
};
export default function AuthMessage(props: AuthMessageProps) {
const { actionType, datasource, description, pageId, style = {} } = props;
const dispatch = useDispatch();
const pluginType = useSelector((state: AppState) =>
getPluginTypeFromDatasourceId(state, datasource.id),
);
const handleOauthAuthorization: any = () => {
if (!pluginType || !pageId) return;
dispatch(redirectAuthorizationCode(pageId, datasource.id, pluginType));
};
const extraInfo: Partial<React.ComponentProps<typeof CalloutV2>> = {};
if (actionType === "authorize") {
extraInfo.actionLabel = "Authorize Datasource";
extraInfo.onClick = handleOauthAuthorization;
}
return (
<StyledAuthMessage style={style}>
<CalloutV2 desc={description} type="Warning" {...extraInfo} />
</StyledAuthMessage>
);
}

View File

@ -1,9 +1,6 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
import styled from "styled-components"; import styled from "styled-components";
import { import { ActionButton } from "pages/Editor/DataSourceEditor/JSONtoForm";
ActionButton,
SaveButtonContainer,
} from "pages/Editor/DataSourceEditor/JSONtoForm";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { import {
getEntities, getEntities,
@ -30,6 +27,7 @@ import {
AuthenticationStatus, AuthenticationStatus,
} from "entities/Datasource"; } from "entities/Datasource";
import { import {
CONFIRM_CONTEXT_DELETING,
OAUTH_AUTHORIZATION_APPSMITH_ERROR, OAUTH_AUTHORIZATION_APPSMITH_ERROR,
OAUTH_AUTHORIZATION_FAILED, OAUTH_AUTHORIZATION_FAILED,
} from "@appsmith/constants/messages"; } from "@appsmith/constants/messages";
@ -40,6 +38,7 @@ import {
createMessage, createMessage,
} from "@appsmith/constants/messages"; } from "@appsmith/constants/messages";
import { debounce } from "lodash"; import { debounce } from "lodash";
import { ApiDatasourceForm } from "entities/Datasource/RestAPIForm";
import { TEMP_DATASOURCE_ID } from "constants/Datasource"; import { TEMP_DATASOURCE_ID } from "constants/Datasource";
import { import {
@ -49,12 +48,13 @@ import {
interface Props { interface Props {
datasource: Datasource; datasource: Datasource;
formData: Datasource; formData: Datasource | ApiDatasourceForm;
getSanitizedFormData: () => Datasource; getSanitizedFormData: () => Datasource;
isInvalid: boolean; isInvalid: boolean;
pageId?: string; pageId?: string;
shouldRender: boolean; shouldRender?: boolean;
datasourceButtonConfiguration: string[] | undefined; datasourceButtonConfiguration: string[] | undefined;
shouldDisplayAuthMessage?: boolean;
triggerSave?: boolean; triggerSave?: boolean;
isFormDirty?: boolean; isFormDirty?: boolean;
datasourceDeleteTrigger: () => void; datasourceDeleteTrigger: () => void;
@ -91,6 +91,12 @@ const StyledButton = styled(ActionButton)<{ fluidWidth?: boolean }>`
} }
`; `;
const SaveButtonContainer = styled.div`
margin-top: 24px;
display: flex;
justify-content: flex-end;
`;
const StyledAuthMessage = styled.div` const StyledAuthMessage = styled.div`
color: ${(props) => props.theme.colors.error}; color: ${(props) => props.theme.colors.error};
margin-top: 15px; margin-top: 15px;
@ -109,12 +115,14 @@ function DatasourceAuth({
isInvalid, isInvalid,
pageId: pageIdProp, pageId: pageIdProp,
shouldRender, shouldRender,
shouldDisplayAuthMessage = true,
triggerSave, triggerSave,
isFormDirty, isFormDirty,
}: Props) { }: Props) {
const authType = const authType =
formData && formData && "authType" in formData
formData?.datasourceConfiguration?.authentication?.authenticationType; ? formData?.authType
: formData?.datasourceConfiguration?.authentication?.authenticationType;
const { id: datasourceId, isDeleting } = datasource; const { id: datasourceId, isDeleting } = datasource;
const applicationId = useSelector(getCurrentApplicationId); const applicationId = useSelector(getCurrentApplicationId);
@ -276,12 +284,16 @@ function DatasourceAuth({
isLoading={isDeleting} isLoading={isDeleting}
key={buttonType} key={buttonType}
onClick={() => { onClick={() => {
confirmDelete ? handleDatasourceDelete() : setConfirmDelete(true); if (!isDeleting) {
confirmDelete ? handleDatasourceDelete() : setConfirmDelete(true);
}
}} }}
size="medium" size="medium"
tag="button" tag="button"
text={ text={
confirmDelete && !isDeleting isDeleting
? createMessage(CONFIRM_CONTEXT_DELETING)
: confirmDelete
? createMessage(CONFIRM_CONTEXT_DELETE) ? createMessage(CONFIRM_CONTEXT_DELETE)
: createMessage(CONTEXT_DELETE) : createMessage(CONTEXT_DELETE)
} }
@ -339,9 +351,11 @@ function DatasourceAuth({
return ( return (
<> <>
{authType === AuthType.OAUTH2 && !isAuthorized && ( {authType === AuthType.OAUTH2 &&
<StyledAuthMessage>Datasource not authorized</StyledAuthMessage> !isAuthorized &&
)} shouldDisplayAuthMessage && (
<StyledAuthMessage>Datasource not authorized</StyledAuthMessage>
)}
{shouldRender && ( {shouldRender && (
<SaveButtonContainer> <SaveButtonContainer>
{datasourceButtonConfiguration?.map((btnConfig) => {datasourceButtonConfiguration?.map((btnConfig) =>
@ -349,7 +363,6 @@ function DatasourceAuth({
)} )}
</SaveButtonContainer> </SaveButtonContainer>
)} )}
{""}
</> </>
); );
} }

View File

@ -1,3 +1,10 @@
import { Plugin } from "api/PluginApi";
import { PluginPackageName } from "entities/Action";
import {
AuthenticationStatus,
AuthType,
Datasource,
} from "entities/Datasource";
export function isCurrentFocusOnInput() { export function isCurrentFocusOnInput() {
return ( return (
["input", "textarea"].indexOf( ["input", "textarea"].indexOf(
@ -57,3 +64,36 @@ export function getPropertyControlFocusElement(
} }
} }
} }
/**
* Returns true if :
* - authentication type is not oauth2 or is not a Google Sheet Plugin
* - authentication type is oauth2 and authorized status success and is a Google Sheet Plugin
* @param datasource Datasource
* @param plugin Plugin
* @returns boolean
*/
export function isDatasourceAuthorizedForQueryCreation(
datasource: Datasource,
plugin: Plugin,
): boolean {
if (!datasource) return false;
const authType =
datasource &&
datasource?.datasourceConfiguration?.authentication?.authenticationType;
/*
TODO: This flag will be removed once the multiple environment is merged to avoid design inconsistency between different datasources.
Search for: GoogleSheetPluginFlag to check for all the google sheet conditional logic throughout the code.
*/
const isGoogleSheetPlugin =
plugin.packageName === PluginPackageName.GOOGLE_SHEETS;
if (isGoogleSheetPlugin && authType === AuthType.OAUTH2) {
const isAuthorized =
datasource?.datasourceConfiguration?.authentication
?.authenticationStatus === AuthenticationStatus.SUCCESS;
return isAuthorized;
}
return true;
}