diff --git a/app/client/cypress/integration/Smoke_TestSuite/Application/ReconnectDatasource_spec.js b/app/client/cypress/integration/Smoke_TestSuite/Application/ReconnectDatasource_spec.js index d822a980de..ddd18c9d4a 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/Application/ReconnectDatasource_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/Application/ReconnectDatasource_spec.js @@ -1,5 +1,6 @@ const homePage = require("../../../locators/HomePage"); const reconnectDatasourceModal = require("../../../locators/ReconnectLocators"); +const datasource = require("../../../locators/DatasourcesEditor.json"); describe("Reconnect Datasource Modal validation while importing application", function() { let workspaceId; @@ -39,12 +40,14 @@ describe("Reconnect Datasource Modal validation while importing application", fu cy.get(".t--ds-list").contains("PostgreSQL"); // check the postgres form config with default value cy.get("[data-cy='section-Connection']").should("be.visible"); - cy.get("[data-cy='section-Authentication']").should("be.visible"); - cy.get("[data-cy='section-SSL (optional)']").should("be.visible"); + cy.get(datasource.authenticationSettingsSection).should( + "be.visible", + ); + cy.get(datasource.sslSettingsSection).should("be.visible"); cy.get( "[data-cy='datasourceConfiguration.connection.mode']", ).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 cy.get( "[data-cy='datasourceConfiguration.connection.ssl.authType']", diff --git a/app/client/cypress/locators/DatasourcesEditor.json b/app/client/cypress/locators/DatasourcesEditor.json index e1e2f8fbb7..cc09af71b8 100644 --- a/app/client/cypress/locators/DatasourcesEditor.json +++ b/app/client/cypress/locators/DatasourcesEditor.json @@ -12,10 +12,10 @@ "MongoDB": ".t--plugin-name:contains('MongoDB')", "RESTAPI": ".t--plugin-name:contains('REST API')", "PostgreSQL": ".t--plugin-name:contains('PostgreSQL')", - "SMTP":".t--plugin-name:contains('SMTP')", + "SMTP": ".t--plugin-name:contains('SMTP')", "MySQL": ".t--plugin-name:contains('MySQL')", "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)", "MySQLEntity": ".t--entity-name:contains(Mysql)", "createQuery": ".t--create-query", @@ -24,15 +24,15 @@ "datasourceCardMenu": ".t--datasource-menu-option", "datasourceCardGeneratePageBtn": ".t--generate-template", "datasourceMenuOptionEdit": "t--datasource-option-edit", - "datasourceMenuOptionDelete":"t--datasource-option-delete", + "datasourceMenuOptionDelete": "t--datasource-option-delete", "editDatasource": ".t--edit-datasource", "datasourceTitle": ".t--edit-datasource-name .bp3-editable-text-content", "datasourceTitleLocator": ".t--edit-datasource-name", "defaultDatabaseName": "input[name='datasourceConfiguration.connection.defaultDatabaseName']", - "datasourceConfigurationProperty":"input[name='datasourceConfiguration.properties[0]']", - "googleSheets":".t--plugin-name:contains('Google Sheets')", + "datasourceConfigurationProperty": "input[name='datasourceConfiguration.properties[0]']", + "googleSheets": ".t--plugin-name:contains('Google Sheets')", "selConnectionType": "[data-cy='datasourceConfiguration.connection.type']", - "scope":"[data-cy='authentication.scopeString']", + "scope": "[data-cy='authentication.scopeString']", "Mysql": ".t--plugin-name:contains('Mysql')", "ElasticSearch": ".t--plugin-name:contains('Elasticsearch')", "DynamoDB": ".t--plugin-name:contains('DynamoDB')", @@ -51,7 +51,7 @@ "projectID": "[data-cy='datasourceConfiguration.authentication.username'] input", "serviceAccCredential": "[data-cy='datasourceConfiguration.authentication.password'] input", "grantType": "[data-cy='authentication.grantType']", - "authorizationURL":"[data-cy='authentication.authorizationUrl'] input", + "authorizationURL": "[data-cy='authentication.authorizationUrl'] input", "authorizationCode": ".t--dropdown-option:contains('Authorization Code')", "clientCredentials": ".t--dropdown-option:contains('Client Credentials')", "clientAuthentication": "[data-cy='authentication.isAuthorizationHeader']", @@ -64,20 +64,22 @@ "basic": "//div[contains(@class,'option') and text()='Basic']", "basicUsername": "input[name='authentication.username']", "basicPassword": "input[name='authentication.password']", - "mockUserDatabase":"div[id='mock-database'] span:contains('Users')", - "mockUserDatasources":".t--datasource-name:contains('Users')", + "mockUserDatabase": "div[id='mock-database'] span:contains('Users')", + "mockUserDatasources": ".t--datasource-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", - "advancedSettings": "[data-cy='section-Advanced Settings']", + "mongoUriInput": "//p[text()='Connection String URI']/following-sibling::div//input", + "advancedSettings": "[data-cy='section-Advanced Settings'] .t--collapse-section-container", "useSelfSignedCert": ".t--connection\\.ssl\\.authType", "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", "gSheetsOperationDropdown": "[data-cy='actionConfiguration.formData.command.data']", "gSheetsEntityDropdown": "[data-cy='actionConfiguration.formData.entityType.data']", "gSheetsInsertOneOption": ".t--dropdown-option:contains('Insert One')", "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" +} \ No newline at end of file diff --git a/app/client/cypress/support/Pages/DataSources.ts b/app/client/cypress/support/Pages/DataSources.ts index 0006b3070b..6730b7630b 100644 --- a/app/client/cypress/support/Pages/DataSources.ts +++ b/app/client/cypress/support/Pages/DataSources.ts @@ -24,7 +24,8 @@ export class DataSources { "input[name='datasourceConfiguration.authentication.databaseName']"; private _username = "input[name='datasourceConfiguration.authentication.username']"; - private _sectionAuthentication = "[data-cy=section-Authentication]"; + private _sectionAuthentication = + "[data-cy=section-Authentication] .t--collapse-section-container"; private _password = "input[name = 'datasourceConfiguration.authentication.password']"; private _testDs = ".t--test-datasource"; diff --git a/app/client/src/ce/constants/messages.ts b/app/client/src/ce/constants/messages.ts index 74dda08685..3bbb1a066d 100644 --- a/app/client/src/ce/constants/messages.ts +++ b/app/client/src/ce/constants/messages.ts @@ -315,6 +315,9 @@ export const OAUTH_AUTHORIZATION_FAILED = export const OAUTH_AUTHORIZATION_APPSMITH_ERROR = "Something went wrong."; 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 = () => "Error saving a key in localStorage. You have exceeded the allowed storage size limit"; 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) => `${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 export const ALERT_STYLE_OPTIONS = [ { label: "Info", value: "'info'", id: "info" }, diff --git a/app/client/src/components/editorComponents/CloseEditor.tsx b/app/client/src/components/editorComponents/CloseEditor.tsx index 6dfb506990..cd551899f8 100644 --- a/app/client/src/components/editorComponents/CloseEditor.tsx +++ b/app/client/src/components/editorComponents/CloseEditor.tsx @@ -22,6 +22,7 @@ const IconContainer = styled.div` //width: 100%; height: 30px; display: flex; + flex-shrink: 0; align-items: center; cursor: pointer; padding-left: 16px; diff --git a/app/client/src/pages/Editor/DataSourceEditor/Collapsible.tsx b/app/client/src/pages/Editor/DataSourceEditor/Collapsible.tsx index 362ed518c9..378de76506 100644 --- a/app/client/src/pages/Editor/DataSourceEditor/Collapsible.tsx +++ b/app/client/src/pages/Editor/DataSourceEditor/Collapsible.tsx @@ -2,6 +2,7 @@ import React, { useCallback, useEffect } from "react"; import { Collapse, Icon } from "@blueprintjs/core"; import styled from "styled-components"; import { Icon as AdsIcon, IconName, IconSize } from "design-system"; +import { Colors } from "constants/Colors"; import { useDispatch, useSelector } from "react-redux"; import { AppState } from "@appsmith/reducers"; import { getDatasourceCollapsibleState } from "selectors/ui"; @@ -31,8 +32,8 @@ const SectionContainer = styled.div` `; const TopBorder = styled.div` - height: 2px; - background-color: #d0d7dd; + height: 1px; + background-color: ${Colors.ALTO}; margin-top: 24px; margin-bottom: 24px; `; @@ -46,12 +47,21 @@ interface ComponentProps { name: IconName; color?: string; }; + showTopBorder?: boolean; + showSection?: boolean; } type Props = ComponentProps; function Collapsible(props: Props) { - const { children, defaultIsOpen, headerIcon, title } = props; + const { + children, + defaultIsOpen, + headerIcon, + showSection = true, + showTopBorder = true, + title, + } = props; const dispatch = useDispatch(); const isOpen = useSelector((state: AppState) => getDatasourceCollapsibleState(state, title), @@ -69,35 +79,35 @@ function Collapsible(props: Props) { }, [defaultIsOpen, isOpen]); return ( - <> - - setIsOpen(!isOpen)} - > - - {title} - {headerIcon && ( - - )} - - - +
+ {showTopBorder && } + {showSection && ( + setIsOpen(!isOpen)} + > + + {title} + {headerIcon && ( + + )} + + + + )} {children} - +
); } diff --git a/app/client/src/pages/Editor/DataSourceEditor/Connected.tsx b/app/client/src/pages/Editor/DataSourceEditor/Connected.tsx index 6e6b406dae..1c30fa25d2 100644 --- a/app/client/src/pages/Editor/DataSourceEditor/Connected.tsx +++ b/app/client/src/pages/Editor/DataSourceEditor/Connected.tsx @@ -31,14 +31,18 @@ const Header = styled.div` const Wrapper = styled.div` display: flex; flex-direction: column; - border-top: 1px solid #d0d7dd; border-bottom: 1px solid #d0d7dd; padding-top: 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 datasource = useSelector((state: AppState) => @@ -67,25 +71,26 @@ function Connected() { return ( -
- - + + +
Datasource Saved
+
+ - -
Datasource Saved
-
- - -
-
+ + )} + {errorComponent} +
{!isNil(currentFormConfig) && !isNil(datasource) ? ( { componentDidUpdate(prevProps: Props) { if (prevProps.datasourceId !== this.props.datasourceId) { diff --git a/app/client/src/pages/Editor/DataSourceEditor/DatasourceSection.tsx b/app/client/src/pages/Editor/DataSourceEditor/DatasourceSection.tsx index 20590217d7..1e22f05f43 100644 --- a/app/client/src/pages/Editor/DataSourceEditor/DatasourceSection.tsx +++ b/app/client/src/pages/Editor/DataSourceEditor/DatasourceSection.tsx @@ -21,11 +21,13 @@ const Value = styled.div` const ValueWrapper = styled.div` display: inline-block; - margin-left: 10px; + &:not(:first-child) { + margin-left: 10px; + } `; const FieldWrapper = styled.div` - &:not(first-child) { + &:not(:first-child) { margin-top: 9px; } `; diff --git a/app/client/src/pages/Editor/DataSourceEditor/JSONtoForm.tsx b/app/client/src/pages/Editor/DataSourceEditor/JSONtoForm.tsx index e3748635dd..2f84b1096f 100644 --- a/app/client/src/pages/Editor/DataSourceEditor/JSONtoForm.tsx +++ b/app/client/src/pages/Editor/DataSourceEditor/JSONtoForm.tsx @@ -7,18 +7,48 @@ import { ControlProps } from "components/formControls/BaseControl"; import { Datasource } from "entities/Datasource"; import { isHidden, isKVArray } from "components/formControls/utils"; import log from "loglevel"; -import CenteredWrapper from "components/designSystems/appsmith/CenteredWrapper"; import CloseEditor from "components/editorComponents/CloseEditor"; import { getType, Types } from "utils/TypeHelpers"; +import { Colors } from "constants/Colors"; import { Button } from "design-system"; -export const LoadingContainer = styled(CenteredWrapper)` - height: 50%; +export const PluginImageWrapper = styled.div` + 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` - height: 40px; - width: auto; +export const PluginImage = (props: any) => { + return ( + + + + ); +}; + +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` @@ -32,13 +62,13 @@ export const Header = styled.div` display: flex; align-items: center; justify-content: space-between; + border-bottom: 1px solid ${Colors.ALTO}; + padding-bottom: 24px; //margin-top: 16px; `; -export const SaveButtonContainer = styled.div` - margin-top: 24px; +export const ActionWrapper = styled.div` display: flex; - justify-content: flex-end; `; export const ActionButton = styled(Button)` @@ -54,19 +84,13 @@ export const ActionButton = styled(Button)` } `; -const DBForm = styled.div` - flex: 1; - padding: 20px; - margin-right: 0px; - overflow: auto; - .backBtn { - padding-bottom: 1px; - cursor: pointer; - } - .backBtnText { - font-size: 16px; - font-weight: 500; - cursor: pointer; +export const EditDatasourceButton = styled(Button)` + padding: 10px 20px; + &&&& { + height: 36px; + max-width: 160px; + border: 1px solid ${Colors.HIT_GRAY}; + width: auto; } `; @@ -196,15 +220,14 @@ export class JSONtoForm< return formData; }; - renderForm = (content: any) => { + renderForm = (formContent: any) => { return ( -
+ - {content} -
+ + {formContent} + + ); }; @@ -214,6 +237,8 @@ export class JSONtoForm< {this.renderEachConfig(section)} diff --git a/app/client/src/pages/Editor/DataSourceEditor/NewActionButton.tsx b/app/client/src/pages/Editor/DataSourceEditor/NewActionButton.tsx index af6b46c69d..3384368cb6 100644 --- a/app/client/src/pages/Editor/DataSourceEditor/NewActionButton.tsx +++ b/app/client/src/pages/Editor/DataSourceEditor/NewActionButton.tsx @@ -8,7 +8,12 @@ import { Toaster, Variant, } 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 { useDispatch, useSelector } from "react-redux"; import { AppState } from "@appsmith/reducers"; @@ -16,6 +21,7 @@ import { getCurrentPageId } from "selectors/editorSelectors"; import { Datasource } from "entities/Datasource"; import { Plugin } from "api/PluginApi"; import { EventLocation } from "utils/AnalyticsUtil"; +import { noop } from "utils/AppsmithUtils"; const ActionButton = styled(Button)` padding: 10px 10px; @@ -43,9 +49,10 @@ type NewActionButtonProps = { isLoading?: boolean; eventFrom?: string; // this is to track from where the new action is being generated plugin?: Plugin; + style?: any; }; function NewActionButton(props: NewActionButtonProps) { - const { datasource, disabled, plugin } = props; + const { datasource, disabled, plugin, style = {} } = props; const pluginType = plugin?.type; const [isSelected, setIsSelected] = useState(false); @@ -88,13 +95,18 @@ function NewActionButton(props: NewActionButtonProps) { return ( ); } diff --git a/app/client/src/pages/Editor/DataSourceEditor/RestAPIDatasourceForm.tsx b/app/client/src/pages/Editor/DataSourceEditor/RestAPIDatasourceForm.tsx index a180ddd1a8..81f63fe245 100644 --- a/app/client/src/pages/Editor/DataSourceEditor/RestAPIDatasourceForm.tsx +++ b/app/client/src/pages/Editor/DataSourceEditor/RestAPIDatasourceForm.tsx @@ -13,7 +13,6 @@ import { import AnalyticsUtil from "utils/AnalyticsUtil"; import FormControl from "pages/Editor/FormControl"; import { StyledInfo } from "components/formControls/InputTextControl"; -import CenteredWrapper from "components/designSystems/appsmith/CenteredWrapper"; import { connect } from "react-redux"; import { AppState } from "@appsmith/reducers"; import { ApiActionConfig, PluginType } from "entities/Action"; @@ -40,12 +39,7 @@ import { GrantType, SSLType, } from "entities/Datasource/RestAPIForm"; -import { - createMessage, - CONTEXT_DELETE, - CONFIRM_CONTEXT_DELETE, - INVALID_URL, -} from "@appsmith/constants/messages"; +import { createMessage, INVALID_URL } from "@appsmith/constants/messages"; import Collapsible from "./Collapsible"; import _ from "lodash"; import FormLabel from "components/editorComponents/FormLabel"; @@ -54,11 +48,18 @@ import { Callout } from "design-system"; import CloseEditor from "components/editorComponents/CloseEditor"; import { updateReplayEntity } from "actions/pageActions"; import { ENTITY_TYPE } from "entities/AppsmithConsole"; -import { TEMP_DATASOURCE_ID } from "constants/Datasource"; import { - hasDeleteDatasourcePermission, - hasManageDatasourcePermission, -} from "@appsmith/utils/permissionHelpers"; + FormContainer, + FormContainerBody, + 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 { initializeReplayEntity: (id: string, data: any) => void; @@ -99,68 +100,10 @@ interface DatasourceRestApiEditorProps { type Props = DatasourceRestApiEditorProps & InjectedFormProps; -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` 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)` &&&& { width: 87px; @@ -289,26 +232,23 @@ class DatasourceRestAPIEditor extends React.Component< } }; - disableSave = (): boolean => { + validate = (): boolean => { const { datasource, datasourceId, formData } = this.props; const createMode = datasourceId === TEMP_DATASOURCE_ID; const canManageDatasource = hasManageDatasourcePermission( datasource?.userPermissions || [], ); if (!formData) return true; - return ( - !formData.url || - !this.props.isFormDirty || - (!createMode && !canManageDatasource) - ); + return !formData.url || (!createMode && !canManageDatasource); }; + getSanitizedFormData = () => + formValuesToDatasource(this.props.datasource, this.props.formData); + save = (onSuccess?: ReduxAction) => { this.props.toggleSaveActionFlag(true); - const normalizedValues = formValuesToDatasource( - this.props.datasource, - this.props.formData, - ); + const normalizedValues = this.getSanitizedFormData(); + AnalyticsUtil.logEvent("SAVE_DATA_SOURCE_CLICK", { pageId: this.props.pageId, appId: this.props.applicationId, @@ -387,17 +327,14 @@ class DatasourceRestAPIEditor extends React.Component< return { isValid: true, message: "" }; }; - handleDeleteDatasource = (datasourceId: string) => { - this.props.deleteDatasource(datasourceId); - this.props.datasourceDeleteTrigger(); - }; - render = () => { + const { datasource, formData, hiddenHeader, pageId } = this.props; + return ( - <> + {/* this is true during import flow */} - {!this.props.hiddenHeader && } - + {!hiddenHeader && } +
{ e.preventDefault(); @@ -405,10 +342,23 @@ class DatasourceRestAPIEditor extends React.Component< > {this.renderHeader()} {this.renderEditor()} - {this.renderSave()} + -
- + +
); }; @@ -437,51 +387,6 @@ class DatasourceRestAPIEditor extends React.Component< ) : null; }; - renderSave = () => { - const { datasourceId, hiddenHeader, isDeleting, isSaving } = this.props; - const createMode = datasourceId === TEMP_DATASOURCE_ID; - const canDeleteDatasource = hasDeleteDatasourcePermission( - this.props.datasource?.userPermissions || [], - ); - - return ( - - {!hiddenHeader && ( - { - 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} - /> - )} - this.save()} - size="medium" - tag="button" - text="Save" - variant={Variant.success} - /> - - ); - }; - renderEditor = () => { const { datasource, @@ -506,6 +411,46 @@ class DatasourceRestAPIEditor extends React.Component< messages.map((msg, i) => ( ))} + {this.renderGeneralSettings()} + {this.renderAuthFields()} + {this.renderOauth2AdvancedSettings()} + {this.renderSelfSignedCertificateFields()} + {formData.authType && + formData.authType === AuthType.OAuth2 && + _.get(authentication, "grantType") === + GrantType.AuthorizationCode && ( + + + this.save( + redirectAuthorizationCode( + pageId, + datasourceId, + PluginType.API, + ), + ) + } + tag="button" + text={ + isAuthorized ? "Save and Re-Authorize" : "Save and Authorize" + } + variant={Variant.success} + /> + + )} + + ); + }; + + renderGeneralSettings = () => { + const { formData } = this.props; + + return ( +
{this.renderInputTextControlViaFormControl( "url", @@ -598,39 +543,7 @@ class DatasourceRestAPIEditor extends React.Component< "", )} - {this.renderAuthFields()} - - {this.renderOauth2AdvancedSettings()} - - {this.renderSelfSignedCertificateFields()} - {formData.authType && - formData.authType === AuthType.OAuth2 && - _.get(authentication, "grantType") === - GrantType.AuthorizationCode && ( - - - this.save( - redirectAuthorizationCode( - pageId, - datasourceId, - PluginType.API, - ), - ) - } - tag="button" - text={ - isAuthorized ? "Save and Re-Authorize" : "Save and Authorize" - } - variant={Variant.success} - /> - - )} - +
); }; @@ -933,7 +846,7 @@ class DatasourceRestAPIEditor extends React.Component< _.get(connection, "ssl.authType") === SSLType.SELF_SIGNED_CERTIFICATE; return ( - <> + {isAuthenticationTypeOAuth2 && isGrantTypeAuthorizationCode && ( )} - + ); }; diff --git a/app/client/src/pages/Editor/IntegrationEditor/DatasourceCard.tsx b/app/client/src/pages/Editor/IntegrationEditor/DatasourceCard.tsx index 3929f0ea15..349cfede21 100644 --- a/app/client/src/pages/Editor/IntegrationEditor/DatasourceCard.tsx +++ b/app/client/src/pages/Editor/IntegrationEditor/DatasourceCard.tsx @@ -38,7 +38,10 @@ import { CONFIRM_CONTEXT_DELETE, createMessage, CONFIRM_CONTEXT_DELETING, + GENERATE_NEW_PAGE_BUTTON_TEXT, + RECONNECT_BUTTON_TEXT, } from "@appsmith/constants/messages"; +import { isDatasourceAuthorizedForQueryCreation } from "utils/editorContextUtils"; import { getCurrentPageId, getPagePermissions, @@ -319,26 +322,34 @@ function DatasourceCard(props: DatasourceCardProps) {
- {(!datasource.isConfigured || supportTemplateGeneration) && ( - - )} + {(!datasource.isConfigured || supportTemplateGeneration) && + isDatasourceAuthorizedForQueryCreation(datasource, plugin) && ( + + )} {datasource.isConfigured && ( diff --git a/app/client/src/pages/Editor/SaaSEditor/DatasourceForm.tsx b/app/client/src/pages/Editor/SaaSEditor/DatasourceForm.tsx index 4024fea459..6c2d619b0e 100644 --- a/app/client/src/pages/Editor/SaaSEditor/DatasourceForm.tsx +++ b/app/client/src/pages/Editor/SaaSEditor/DatasourceForm.tsx @@ -1,9 +1,8 @@ import React from "react"; -import styled from "styled-components"; import _, { merge } from "lodash"; import { DATASOURCE_SAAS_FORM } from "@appsmith/constants/forms"; 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 { getFormValues, @@ -18,9 +17,12 @@ import { getDatasource, getPluginImages, getDatasourceFormButtonConfig, + getPlugin, } from "selectors/entitiesSelector"; import { ActionDataState } from "reducers/entityReducers/actionsReducer"; import { + ActionWrapper, + EditDatasourceButton, FormTitleContainer, Header, JSONtoForm, @@ -29,14 +31,24 @@ import { } from "../DataSourceEditor/JSONtoForm"; import { getConfigInitialValues } from "components/formControls/utils"; import Connected from "../DataSourceEditor/Connected"; -import { Colors } from "constants/Colors"; -import { getCurrentApplicationId } from "selectors/editorSelectors"; -import DatasourceAuth from "../../common/datasourceAuth"; +import { + getCurrentApplicationId, + getPagePermissions, +} from "selectors/editorSelectors"; +import DatasourceAuth from "pages/common/datasourceAuth"; import EntityNotFoundPane from "../EntityNotFoundPane"; 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 { hasManageDatasourcePermission } from "@appsmith/utils/permissionHelpers"; +import { + hasCreateDatasourceActionPermission, + hasManageDatasourcePermission, +} from "@appsmith/utils/permissionHelpers"; import { TEMP_DATASOURCE_ID } from "constants/Datasource"; import { createTempDatasourceFromForm, @@ -47,15 +59,18 @@ import { toggleSaveActionFromPopupFlag, } from "actions/datasourceActions"; import SaveOrDiscardDatasourceModal from "../DataSourceEditor/SaveOrDiscardDatasourceModal"; +import { GSHEET_AUTHORIZATION_ERROR } from "ce/constants/messages"; interface StateProps extends JSONtoFormProps { applicationId: string; canManageDatasource?: boolean; + canCreateDatasourceActions?: boolean; isSaving: boolean; isDeleting: boolean; loadingFormConfigs: boolean; isNewDatasource: boolean; pluginImage: string; + plugin?: Plugin; pluginId: string; actions: ActionDataState; datasource?: Datasource; @@ -89,16 +104,6 @@ type DatasourceSaaSEditorProps = StateProps & type Props = DatasourceSaaSEditorProps & InjectedFormProps; -const EditDatasourceButton = styled(AdsButton)` - padding: 10px 20px; - &&&& { - height: 32px; - max-width: 160px; - border: 1px solid ${Colors.HIT_GRAY}; - width: auto; - } -`; - /* **** State Variables Description **** showDialog: flag used to show/hide the datasource discard popup @@ -244,6 +249,7 @@ class DatasourceSaaSEditor extends JSONtoForm { renderDataSourceConfigForm = (sections: any) => { const { + canCreateDatasourceActions, canManageDatasource, datasource, datasourceButtonConfiguration, @@ -251,12 +257,23 @@ class DatasourceSaaSEditor extends JSONtoForm { formData, hiddenHeader, pageId, + plugin, pluginPackageName, } = this.props; const params: string = location.search; const 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; return ( @@ -277,36 +294,75 @@ class DatasourceSaaSEditor extends JSONtoForm { {viewMode && ( - { - this.props.setDatasourceViewMode(false); - this.props.history.replace( - saasEditorDatasourceIdURL({ - pageId: pageId || "", - pluginPackageName, - datasourceId, - params: { - viewMode: false, - }, - }), - ); - }} - text="EDIT" - /> + + { + this.props.setDatasourceViewMode(false); + this.props.history.replace( + saasEditorDatasourceIdURL({ + pageId: pageId || "", + pluginPackageName, + datasourceId, + params: { + viewMode: false, + }, + }), + ); + }} + text="EDIT" + /> + {isGoogleSheetPlugin && ( + + )} + )} )} {(!viewMode || datasourceId === TEMP_DATASOURCE_ID) && ( <> + {datasource && isGoogleSheetPlugin && !isPluginAuthorized ? ( + + ) : null} {!_.isNil(sections) ? _.map(sections, this.renderMainSection) : null} {""} )} - {viewMode && } + {viewMode && ( + + ) : null + } + showDatasourceSavedText={!isGoogleSheetPlugin} + /> + )} {/* Render datasource form call-to-actions */} {datasource && ( { getSanitizedFormData={_.memoize(this.getSanitizedData)} isInvalid={this.validate()} pageId={pageId} + shouldDisplayAuthMessage={!isGoogleSheetPlugin} shouldRender={!viewMode} triggerSave={this.props.isDatasourceBeingSavedFromPopup} /> @@ -344,6 +401,7 @@ const mapStateToProps = (state: AppState, props: any) => { const { formConfigs } = plugins; const formData = getFormValues(DATASOURCE_SAAS_FORM)(state) as Datasource; const pluginId = _.get(datasource, "pluginId", ""); + const plugin = getPlugin(state, pluginId); const formConfig = formConfigs[pluginId]; const initialValues = {}; if (formConfig) { @@ -360,12 +418,18 @@ const mapStateToProps = (state: AppState, props: any) => { ? true : isDirty(DATASOURCE_SAAS_FORM)(state); - const datsourcePermissions = datasource?.userPermissions || []; + const datasourcePermissions = datasource?.userPermissions || []; const canManageDatasource = hasManageDatasourcePermission( - datsourcePermissions, + datasourcePermissions, ); + const pagePermissions = getPagePermissions(state); + const canCreateDatasourceActions = hasCreateDatasourceActionPermission([ + ...datasourcePermissions, + ...pagePermissions, + ]); + return { datasource, datasourceButtonConfiguration, @@ -377,6 +441,7 @@ const mapStateToProps = (state: AppState, props: any) => { viewMode: viewMode ?? !props.fromImporting, isNewDatasource: datasourcePane.newDatasource === TEMP_DATASOURCE_ID, pageId: props.pageId || props.match?.params?.pageId, + plugin: plugin, pluginImage: getPluginImages(state)[pluginId], pluginPackageName: props.pluginPackageName || props.match?.params?.pluginPackageName, @@ -391,6 +456,7 @@ const mapStateToProps = (state: AppState, props: any) => { isDatasourceBeingSavedFromPopup: state.entities.datasources.isDatasourceBeingSavedFromPopup, isFormDirty, + canCreateDatasourceActions, }; }; diff --git a/app/client/src/pages/Editor/gitSync/ReconnectDatasourceModal.tsx b/app/client/src/pages/Editor/gitSync/ReconnectDatasourceModal.tsx index 53fcf78f36..45cd2caa3c 100644 --- a/app/client/src/pages/Editor/gitSync/ReconnectDatasourceModal.tsx +++ b/app/client/src/pages/Editor/gitSync/ReconnectDatasourceModal.tsx @@ -106,6 +106,13 @@ const ContentWrapper = styled.div` .t--json-to-form-wrapper { width: 100%; + .t--json-to-form-body { + padding: 0 20px; + .t--collapse-section-container { + margin-top: 20px; + } + } + .t--close-editor { display: none; } @@ -226,7 +233,6 @@ const TooltipWrapper = styled.div` `; const DBFormWrapper = styled.div` - padding: 10px; width: calc(100% - 206px); overflow: auto; diff --git a/app/client/src/pages/common/datasourceAuth/AuthMessage.tsx b/app/client/src/pages/common/datasourceAuth/AuthMessage.tsx new file mode 100644 index 0000000000..1a407f95b9 --- /dev/null +++ b/app/client/src/pages/common/datasourceAuth/AuthMessage.tsx @@ -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> = {}; + + if (actionType === "authorize") { + extraInfo.actionLabel = "Authorize Datasource"; + extraInfo.onClick = handleOauthAuthorization; + } + + return ( + + + + ); +} diff --git a/app/client/src/pages/common/datasourceAuth/index.tsx b/app/client/src/pages/common/datasourceAuth/index.tsx index a60d733643..b073a875d4 100644 --- a/app/client/src/pages/common/datasourceAuth/index.tsx +++ b/app/client/src/pages/common/datasourceAuth/index.tsx @@ -1,9 +1,6 @@ import React, { useState, useEffect } from "react"; import styled from "styled-components"; -import { - ActionButton, - SaveButtonContainer, -} from "pages/Editor/DataSourceEditor/JSONtoForm"; +import { ActionButton } from "pages/Editor/DataSourceEditor/JSONtoForm"; import { useDispatch, useSelector } from "react-redux"; import { getEntities, @@ -30,6 +27,7 @@ import { AuthenticationStatus, } from "entities/Datasource"; import { + CONFIRM_CONTEXT_DELETING, OAUTH_AUTHORIZATION_APPSMITH_ERROR, OAUTH_AUTHORIZATION_FAILED, } from "@appsmith/constants/messages"; @@ -40,6 +38,7 @@ import { createMessage, } from "@appsmith/constants/messages"; import { debounce } from "lodash"; +import { ApiDatasourceForm } from "entities/Datasource/RestAPIForm"; import { TEMP_DATASOURCE_ID } from "constants/Datasource"; import { @@ -49,12 +48,13 @@ import { interface Props { datasource: Datasource; - formData: Datasource; + formData: Datasource | ApiDatasourceForm; getSanitizedFormData: () => Datasource; isInvalid: boolean; pageId?: string; - shouldRender: boolean; + shouldRender?: boolean; datasourceButtonConfiguration: string[] | undefined; + shouldDisplayAuthMessage?: boolean; triggerSave?: boolean; isFormDirty?: boolean; 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` color: ${(props) => props.theme.colors.error}; margin-top: 15px; @@ -109,12 +115,14 @@ function DatasourceAuth({ isInvalid, pageId: pageIdProp, shouldRender, + shouldDisplayAuthMessage = true, triggerSave, isFormDirty, }: Props) { const authType = - formData && - formData?.datasourceConfiguration?.authentication?.authenticationType; + formData && "authType" in formData + ? formData?.authType + : formData?.datasourceConfiguration?.authentication?.authenticationType; const { id: datasourceId, isDeleting } = datasource; const applicationId = useSelector(getCurrentApplicationId); @@ -276,12 +284,16 @@ function DatasourceAuth({ isLoading={isDeleting} key={buttonType} onClick={() => { - confirmDelete ? handleDatasourceDelete() : setConfirmDelete(true); + if (!isDeleting) { + confirmDelete ? handleDatasourceDelete() : setConfirmDelete(true); + } }} size="medium" tag="button" text={ - confirmDelete && !isDeleting + isDeleting + ? createMessage(CONFIRM_CONTEXT_DELETING) + : confirmDelete ? createMessage(CONFIRM_CONTEXT_DELETE) : createMessage(CONTEXT_DELETE) } @@ -339,9 +351,11 @@ function DatasourceAuth({ return ( <> - {authType === AuthType.OAUTH2 && !isAuthorized && ( - Datasource not authorized - )} + {authType === AuthType.OAUTH2 && + !isAuthorized && + shouldDisplayAuthMessage && ( + Datasource not authorized + )} {shouldRender && ( {datasourceButtonConfiguration?.map((btnConfig) => @@ -349,7 +363,6 @@ function DatasourceAuth({ )} )} - {""} ); } diff --git a/app/client/src/utils/editorContextUtils.ts b/app/client/src/utils/editorContextUtils.ts index 153c9c7cff..4506c5c57f 100644 --- a/app/client/src/utils/editorContextUtils.ts +++ b/app/client/src/utils/editorContextUtils.ts @@ -1,3 +1,10 @@ +import { Plugin } from "api/PluginApi"; +import { PluginPackageName } from "entities/Action"; +import { + AuthenticationStatus, + AuthType, + Datasource, +} from "entities/Datasource"; export function isCurrentFocusOnInput() { return ( ["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; +}