diff --git a/app/client/src/actions/datasourceActions.ts b/app/client/src/actions/datasourceActions.ts index d19c224691..82bf5e388b 100644 --- a/app/client/src/actions/datasourceActions.ts +++ b/app/client/src/actions/datasourceActions.ts @@ -194,7 +194,7 @@ export const storeAsDatasource = () => { export const getOAuthAccessToken = (datasourceId: string) => { return { - type: ReduxActionTypes.SAAS_GET_OAUTH_ACCESS_TOKEN, + type: ReduxActionTypes.GET_OAUTH_ACCESS_TOKEN, payload: { datasourceId }, }; }; diff --git a/app/client/src/api/CloudServicesApi.ts b/app/client/src/api/CloudServicesApi.ts index 003f04092b..1489f08c69 100644 --- a/app/client/src/api/CloudServicesApi.ts +++ b/app/client/src/api/CloudServicesApi.ts @@ -2,5 +2,5 @@ import { getAppsmithConfigs } from "@appsmith/configs"; const { cloudServicesBaseUrl: BASE_URL } = getAppsmithConfigs(); -export const authorizeSaasWithAppsmithToken = (appsmithToken: string) => +export const authorizeDatasourceWithAppsmithToken = (appsmithToken: string) => `${BASE_URL}/api/v1/integrations/oauth/authorize?appsmithToken=${appsmithToken}`; diff --git a/app/client/src/api/OAuthApi.ts b/app/client/src/api/OAuthApi.ts new file mode 100644 index 0000000000..a0e827486b --- /dev/null +++ b/app/client/src/api/OAuthApi.ts @@ -0,0 +1,28 @@ +import Api from "./Api"; +import { AxiosPromise } from "axios"; +import { GenericApiResponse } from "api/ApiResponses"; +import { Datasource } from "entities/Datasource"; + +class OAuthApi extends Api { + static url = "v1/saas"; + + // Api endpoint to get "Appsmith token" from server + static getAppsmithToken( + datasourceId: string, + pageId: string, + ): AxiosPromise> { + return Api.post(`${OAuthApi.url}/${datasourceId}/pages/${pageId}/oauth`); + } + + // Api endpoint to get access token for datasource authorization + static getAccessToken( + datasourceId: string, + token: string, + ): AxiosPromise> { + return Api.post( + `${OAuthApi.url}/${datasourceId}/token?appsmithToken=${token}`, + ); + } +} + +export default OAuthApi; diff --git a/app/client/src/constants/ReduxActionConstants.tsx b/app/client/src/constants/ReduxActionConstants.tsx index 71ca48dc50..a4eb389170 100644 --- a/app/client/src/constants/ReduxActionConstants.tsx +++ b/app/client/src/constants/ReduxActionConstants.tsx @@ -555,6 +555,7 @@ export const ReduxActionTypes = { RESET_APPLICATION_WIDGET_STATE_REQUEST: "RESET_APPLICATION_WIDGET_STATE_REQUEST", SAAS_GET_OAUTH_ACCESS_TOKEN: "SAAS_GET_OAUTH_ACCESS_TOKEN", + GET_OAUTH_ACCESS_TOKEN: "GET_OAUTH_ACCESS_TOKEN", UPDATE_RECENT_ENTITY: "UPDATE_RECENT_ENTITY", RESTORE_RECENT_ENTITIES_REQUEST: "RESTORE_RECENT_ENTITIES_REQUEST", RESTORE_RECENT_ENTITIES_SUCCESS: "RESTORE_RECENT_ENTITIES_SUCCESS", diff --git a/app/client/src/constants/messages.ts b/app/client/src/constants/messages.ts index c86dc74cb8..73e0428537 100644 --- a/app/client/src/constants/messages.ts +++ b/app/client/src/constants/messages.ts @@ -287,12 +287,12 @@ export const REST_API_AUTHORIZATION_FAILED = () => export const REST_API_AUTHORIZATION_APPSMITH_ERROR = () => "Something went wrong."; -export const SAAS_AUTHORIZATION_SUCCESSFUL = "Authorization was successful!"; -export const SAAS_AUTHORIZATION_FAILED = +export const OAUTH_AUTHORIZATION_SUCCESSFUL = "Authorization was successful!"; +export const OAUTH_AUTHORIZATION_FAILED = "Authorization failed. Please check your details or try again."; // Todo: improve this for appsmith_error error message -export const SAAS_AUTHORIZATION_APPSMITH_ERROR = "Something went wrong."; -export const SAAS_APPSMITH_TOKEN_NOT_FOUND = "Appsmith token not found"; +export const OAUTH_AUTHORIZATION_APPSMITH_ERROR = "Something went wrong."; +export const OAUTH_APPSMITH_TOKEN_NOT_FOUND = "Appsmith token not found"; export const LOCAL_STORAGE_QUOTA_EXCEEDED_MESSAGE = () => "Error saving a key in localStorage. You have exceeded the allowed storage size limit"; diff --git a/app/client/src/entities/Datasource/index.ts b/app/client/src/entities/Datasource/index.ts index 08e891f8a6..b8d01a884a 100644 --- a/app/client/src/entities/Datasource/index.ts +++ b/app/client/src/entities/Datasource/index.ts @@ -1,6 +1,17 @@ import { APIResponseError } from "api/ApiResponses"; import { ActionConfig, Property } from "entities/Action"; import _ from "lodash"; + +export enum AuthType { + OAUTH2 = "oAuth2", + DBAUTH = "dbAuth", +} + +export enum AuthenticationStatus { + NONE = "NONE", + IN_PROGRESS = "IN_PROGRESS", + SUCCESS = "SUCCESS", +} export interface DatasourceAuthentication { authType?: string; username?: string; @@ -11,6 +22,7 @@ export interface DatasourceAuthentication { addTo?: string; bearerToken?: string; authenticationStatus?: string; + authenticationType?: string; } export interface DatasourceColumns { diff --git a/app/client/src/pages/Editor/DataSourceEditor/DBForm.tsx b/app/client/src/pages/Editor/DataSourceEditor/DBForm.tsx index c906a673c6..bca21acd8d 100644 --- a/app/client/src/pages/Editor/DataSourceEditor/DBForm.tsx +++ b/app/client/src/pages/Editor/DataSourceEditor/DBForm.tsx @@ -9,8 +9,6 @@ import Button, { Category } from "components/ads/Button"; import { Colors } from "constants/Colors"; import CollapsibleHelp from "components/designSystems/appsmith/help/CollapsibleHelp"; import Connected from "./Connected"; - -import EditButton from "components/editorComponents/Button"; import { Datasource } from "entities/Datasource"; import { reduxForm, InjectedFormProps } from "redux-form"; import { APPSMITH_IP_ADDRESSES } from "constants/DatasourceEditorConstants"; @@ -24,47 +22,33 @@ import Callout from "components/ads/Callout"; import { Variant } from "components/ads/common"; import { AppState } from "reducers"; import { - ActionButton, FormTitleContainer, Header, JSONtoForm, JSONtoFormProps, PluginImage, - SaveButtonContainer, } from "./JSONtoForm"; -import { ButtonVariantTypes } from "components/constants"; +import DatasourceAuth from "../../common/datasourceAuth"; const { cloudHosting } = getAppsmithConfigs(); interface DatasourceDBEditorProps extends JSONtoFormProps { - onSave: (formValues: Datasource) => void; - onTest: (formValus: Datasource) => void; - handleDelete: (id: string) => void; setDatasourceEditorMode: (id: string, viewMode: boolean) => void; openOmnibarReadMore: (text: string) => void; - isSaving: boolean; - isDeleting: boolean; datasourceId: string; applicationId: string; pageId: string; - isTesting: boolean; isNewDatasource: boolean; pluginImage: string; viewMode: boolean; pluginType: string; messages?: Array; + datasource: Datasource; } type Props = DatasourceDBEditorProps & InjectedFormProps; -const StyledButton = styled(EditButton)` - &&&& { - width: 87px; - height: 32px; - } -`; - const StyledOpenDocsIcon = styled(Icon)` svg { width: 12px; @@ -93,15 +77,9 @@ class DatasourceDBEditor extends JSONtoForm { this.props.setDatasourceEditorMode(this.props.datasourceId, true); } } - - save = () => { - const normalizedValues = this.normalizeValues(); - const trimmedValues = this.getTrimmedData(normalizedValues); - AnalyticsUtil.logEvent("SAVE_DATA_SOURCE_CLICK", { - pageId: this.props.pageId, - appId: this.props.applicationId, - }); - this.props.onSave(trimmedValues); + // returns normalized and trimmed datasource form data + getSanitizedData = () => { + return this.getTrimmedData(this.normalizeValues()); }; openOmnibarReadMore = () => { @@ -110,34 +88,16 @@ class DatasourceDBEditor extends JSONtoForm { AnalyticsUtil.logEvent("OPEN_OMNIBAR", { source: "READ_MORE_DATASOURCE" }); }; - test = () => { - const normalizedValues = this.normalizeValues(); - const trimmedValues = this.getTrimmedData(normalizedValues); - AnalyticsUtil.logEvent("TEST_DATA_SOURCE_CLICK", { - pageId: this.props.pageId, - appId: this.props.applicationId, - }); - - this.props.onTest(trimmedValues); - }; - render() { const { formConfig } = this.props; + const content = this.renderDataSourceConfigForm(formConfig); return this.renderForm(content); } renderDataSourceConfigForm = (sections: any) => { - const { - datasourceId, - handleDelete, - isDeleting, - isSaving, - isTesting, - messages, - pluginType, - } = this.props; - const { viewMode } = this.props; + const { datasource, formData, messages, pluginType, viewMode } = this.props; + return (
{ @@ -193,41 +153,21 @@ class DatasourceDBEditor extends JSONtoForm { {!_.isNil(sections) ? _.map(sections, this.renderMainSection) : undefined} - - handleDelete(datasourceId)} - text="Delete" - /> - - - - + {""} ) : ( )} + {/* Render datasource form call-to-actions */} + {datasource && ( + + )} ); }; @@ -242,6 +182,7 @@ const mapStateToProps = (state: AppState, props: any) => { return { messages: hintMessages, + datasource, }; }; diff --git a/app/client/src/pages/Editor/SaaSEditor/DatasourceForm.tsx b/app/client/src/pages/Editor/SaaSEditor/DatasourceForm.tsx index d0312effd8..49612e00d0 100644 --- a/app/client/src/pages/Editor/SaaSEditor/DatasourceForm.tsx +++ b/app/client/src/pages/Editor/SaaSEditor/DatasourceForm.tsx @@ -4,7 +4,6 @@ import _, { merge } from "lodash"; import { DATASOURCE_SAAS_FORM } from "constants/forms"; import { SAAS_EDITOR_DATASOURCE_ID_URL } from "./constants"; import FormTitle from "pages/Editor/DataSourceEditor/FormTitle"; -import Button from "components/editorComponents/Button"; import AdsButton, { Category } from "components/ads/Button"; import { Datasource } from "entities/Datasource"; import { getFormValues, InjectedFormProps, reduxForm } from "redux-form"; @@ -12,39 +11,21 @@ import { RouteComponentProps } from "react-router"; import { connect } from "react-redux"; import { AppState } from "reducers"; import { getDatasource, getPluginImages } from "selectors/entitiesSelector"; -import { ReduxAction } from "constants/ReduxActionConstants"; -import { - deleteDatasource, - getOAuthAccessToken, - redirectAuthorizationCode, - updateDatasource, -} from "actions/datasourceActions"; -import { createActionRequest } from "actions/pluginActionActions"; import { ActionDataState } from "reducers/entityReducers/actionsReducer"; import { - ActionButton, FormTitleContainer, Header, JSONtoForm, JSONtoFormProps, PluginImage, - SaveButtonContainer, } from "../DataSourceEditor/JSONtoForm"; import { getConfigInitialValues } from "components/formControls/utils"; -import { - SAAS_AUTHORIZATION_APPSMITH_ERROR, - SAAS_AUTHORIZATION_FAILED, -} from "constants/messages"; -import { Variant } from "components/ads/common"; -import { Toaster } from "components/ads/Toast"; -import { Action, PluginType } from "entities/Action"; -import AnalyticsUtil from "utils/AnalyticsUtil"; import Connected from "../DataSourceEditor/Connected"; import { Colors } from "constants/Colors"; -import { redirectToNewIntegrations } from "../../../actions/apiPaneActions"; -import { ButtonVariantTypes } from "components/constants"; import { getCurrentApplicationId } from "selectors/editorSelectors"; +import DatasourceAuth from "../../common/datasourceAuth"; +import EntityNotFoundPane from "../EntityNotFoundPane"; interface StateProps extends JSONtoFormProps { applicationId: string; @@ -58,16 +39,7 @@ interface StateProps extends JSONtoFormProps { datasource?: Datasource; } -interface DispatchFunctions { - updateDatasource: (formData: any, onSuccess?: ReduxAction) => void; - deleteDatasource: (id: string, onSuccess?: ReduxAction) => void; - getOAuthAccessToken: (id: string) => void; - createAction: (data: Partial) => void; - redirectToNewIntegrations: (applicationId: string, pageId: string) => void; -} - type DatasourceSaaSEditorProps = StateProps & - DispatchFunctions & RouteComponentProps<{ datasourceId: string; pageId: string; @@ -77,19 +49,6 @@ type DatasourceSaaSEditorProps = StateProps & type Props = DatasourceSaaSEditorProps & InjectedFormProps; -enum AuthenticationStatus { - NONE = "NONE", - IN_PROGRESS = "IN_PROGRESS", - SUCCESS = "SUCCESS", -} - -const StyledButton = styled(Button)` - &&&& { - width: 180px; - height: 32px; - } -`; - const EditDatasourceButton = styled(AdsButton)` padding: 10px 20px; &&&& { @@ -100,60 +59,25 @@ const EditDatasourceButton = styled(AdsButton)` } `; -const StyledAuthMessage = styled.div` - color: ${(props) => props.theme.colors.error}; - margin-top: 15px; - &:after { - content: " *"; - color: inherit; - } -`; - class DatasourceSaaSEditor extends JSONtoForm { - componentDidMount() { - super.componentDidMount(); - const search = new URLSearchParams(this.props.location.search); - const status = search.get("response_status"); - - if (status) { - const display_message = search.get("display_message"); - // Set default error message - let message = SAAS_AUTHORIZATION_FAILED; - const variant = Variant.danger; - if (status !== "success") { - if (status === "appsmith_error") { - message = SAAS_AUTHORIZATION_APPSMITH_ERROR; - } - Toaster.show({ text: display_message || message, variant }); - } else { - this.props.getOAuthAccessToken(this.props.match.params.datasourceId); - } - AnalyticsUtil.logEvent("GSHEET_AUTH_COMPLETE", { - applicationId: _.get(this.props, "applicationId"), - datasourceId: _.get(this.props, "match.params.datasourceId"), - pageId: _.get(this.props, "match.params.pageId"), - }); - } - } - - save = (onSuccess?: ReduxAction) => { - const normalizedValues = this.normalizeValues(); - this.props.updateDatasource(normalizedValues, onSuccess); - }; - render() { - const { formConfig } = this.props; + const { formConfig, pluginId } = this.props; + if (!pluginId) { + return ; + } const content = this.renderDataSourceConfigForm(formConfig); return this.renderForm(content); } + getSanitizedData = () => { + return this.normalizeValues(); + }; + renderDataSourceConfigForm = (sections: any) => { const { applicationId, datasource, - deleteDatasource, - isDeleting, - isSaving, + formData, match: { params: { datasourceId, pageId, pluginPackageName }, }, @@ -161,10 +85,6 @@ class DatasourceSaaSEditor extends JSONtoForm { const params: string = location.search; const viewMode = new URLSearchParams(params).get("viewMode"); - const isAuthorized = - datasource?.datasourceConfiguration.authentication - ?.authenticationStatus === AuthenticationStatus.SUCCESS; - return (
{ @@ -203,60 +123,20 @@ class DatasourceSaaSEditor extends JSONtoForm { {!_.isNil(sections) ? _.map(sections, this.renderMainSection) : null} - {!isAuthorized && ( - Datasource not authorized - )} - - - deleteDatasource( - datasourceId, - this.props.redirectToNewIntegrations( - applicationId, - pageId, - ) as any, - ) - } - text="Delete" - /> - - { - AnalyticsUtil.logEvent("GSHEET_AUTH_INIT", { - applicationId, - datasourceId, - pageId, - }); - this.save( - redirectAuthorizationCode( - pageId, - datasourceId, - PluginType.SAAS, - ), - ); - }} - size="small" - text={isAuthorized ? "Re-authorize" : "Authorize"} - /> - + {""} ) : ( - <> - - {!isAuthorized && ( - Datasource not authorized - )} - + + )} + {/* Render datasource form call-to-actions */} + {datasource && ( + )} ); @@ -293,27 +173,7 @@ const mapStateToProps = (state: AppState, props: any) => { }; }; -const mapDispatchToProps = (dispatch: any): DispatchFunctions => { - return { - deleteDatasource: (id: string, onSuccess?: ReduxAction) => - dispatch(deleteDatasource({ id }, onSuccess)), - updateDatasource: (formData: any, onSuccess?: ReduxAction) => - dispatch(updateDatasource(formData, onSuccess)), - getOAuthAccessToken: (datasourceId: string) => - dispatch(getOAuthAccessToken(datasourceId)), - createAction: (data: Partial) => { - dispatch(createActionRequest(data)); - }, - redirectToNewIntegrations: (applicationId: string, pageId: string) => { - dispatch(redirectToNewIntegrations(applicationId, pageId)); - }, - }; -}; - -export default connect( - mapStateToProps, - mapDispatchToProps, -)( +export default connect(mapStateToProps)( reduxForm({ form: DATASOURCE_SAAS_FORM, enableReinitialize: true, diff --git a/app/client/src/pages/common/datasourceAuth/DefaultAuth.tsx b/app/client/src/pages/common/datasourceAuth/DefaultAuth.tsx new file mode 100644 index 0000000000..108960a356 --- /dev/null +++ b/app/client/src/pages/common/datasourceAuth/DefaultAuth.tsx @@ -0,0 +1,132 @@ +import { ButtonVariantTypes } from "components/constants"; +import { Datasource } from "entities/Datasource"; +import { + ActionButton, + SaveButtonContainer, +} from "pages/Editor/DataSourceEditor/JSONtoForm"; +import React from "react"; +import styled from "styled-components"; +import EditButton from "components/editorComponents/Button"; +import { useDispatch, useSelector } from "react-redux"; +import { getEntities } from "selectors/entitiesSelector"; +import { + testDatasource, + deleteDatasource, + updateDatasource, +} from "actions/datasourceActions"; +import AnalyticsUtil from "utils/AnalyticsUtil"; +import { redirectToNewIntegrations } from "actions/apiPaneActions"; +import { getQueryParams } from "utils/AppsmithUtils"; +import { getCurrentApplicationId } from "selectors/editorSelectors"; +import { useParams } from "react-router"; +import { ExplorerURLParams } from "pages/Editor/Explorer/helpers"; +import { getIsGeneratePageInitiator } from "utils/GenerateCrudUtil"; + +interface Props { + datasource: Datasource; + getSanitizedFormData: () => Datasource; + isInvalid: boolean; + shouldRender: boolean; +} +const StyledButton = styled(EditButton)` + &&&& { + width: 87px; + height: 32px; + } +`; + +export default function DefaultAuth({ + datasource, + getSanitizedFormData, + isInvalid, + shouldRender, +}: Props): JSX.Element { + const { id: datasourceId } = datasource; + + const applicationId = useSelector(getCurrentApplicationId); + + const dispatch = useDispatch(); + + const { + datasources: { isDeleting, isTesting, loading: isSaving }, + } = useSelector(getEntities); + const { pageId } = useParams(); + + // Handles datasource deletion + const handleDatasourceDelete = () => { + dispatch(deleteDatasource({ id: datasourceId })); + }; + + // Handles datasource testing + const handleDatasourceTest = () => { + AnalyticsUtil.logEvent("TEST_DATA_SOURCE_CLICK", { + pageId: pageId, + appId: applicationId, + }); + dispatch(testDatasource(getSanitizedFormData())); + }; + + // Handles datasource saving + const handleDatasourceSave = () => { + const isGeneratePageInitiator = getIsGeneratePageInitiator(); + AnalyticsUtil.logEvent("SAVE_DATA_SOURCE_CLICK", { + pageId: pageId, + appId: applicationId, + }); + // After saving datasource, only redirect to the 'new integrations' page + // if datasource is not used to generate a page + dispatch( + updateDatasource( + getSanitizedFormData(), + !isGeneratePageInitiator + ? dispatch( + redirectToNewIntegrations( + applicationId, + pageId, + getQueryParams(), + ), + ) + : undefined, + ), + ); + }; + + return ( + <> + {shouldRender && ( + + + + + + + )} + {""} + + ); +} diff --git a/app/client/src/pages/common/datasourceAuth/OAuth.tsx b/app/client/src/pages/common/datasourceAuth/OAuth.tsx new file mode 100644 index 0000000000..23389c41ae --- /dev/null +++ b/app/client/src/pages/common/datasourceAuth/OAuth.tsx @@ -0,0 +1,161 @@ +import { ButtonVariantTypes } from "components/constants"; +import { AuthenticationStatus, Datasource } from "entities/Datasource"; +import { + ActionButton, + SaveButtonContainer, +} from "pages/Editor/DataSourceEditor/JSONtoForm"; +import React, { useEffect } from "react"; +import styled from "styled-components"; +import EditButton from "components/editorComponents/Button"; +import { useDispatch, useSelector } from "react-redux"; +import { + getEntities, + getPluginTypeFromDatasourceId, +} from "selectors/entitiesSelector"; +import { + deleteDatasource, + getOAuthAccessToken, + redirectAuthorizationCode, + updateDatasource, +} from "actions/datasourceActions"; +import { AppState } from "reducers"; +import { + OAUTH_AUTHORIZATION_APPSMITH_ERROR, + OAUTH_AUTHORIZATION_FAILED, +} from "constants/messages"; +import { Variant } from "components/ads/common"; +import { Toaster } from "components/ads/Toast"; +import AnalyticsUtil from "utils/AnalyticsUtil"; +import { getCurrentApplicationId } from "selectors/editorSelectors"; +import { useLocation, useParams } from "react-router"; +import { ExplorerURLParams } from "pages/Editor/Explorer/helpers"; + +interface Props { + datasource: Datasource; + getSanitizedFormData: () => Datasource; + isInvalid: boolean; + shouldRender: boolean; +} + +enum AuthorizationStatus { + SUCCESS = "success", + APPSMITH_ERROR = "appsmith_error", +} + +const StyledButton = styled(EditButton)` + &&&& { + height: 32px; + } +`; + +const StyledAuthMessage = styled.div` + color: ${(props) => props.theme.colors.error}; + margin-top: 15px; + &:after { + content: " *"; + color: inherit; + } +`; + +function OAuth({ + datasource, + getSanitizedFormData, + isInvalid, + shouldRender, +}: Props): JSX.Element { + const { id: datasourceId } = datasource; + const { + datasources: { isDeleting, loading: isSaving }, + } = useSelector(getEntities); + const isAuthorized = + datasource.datasourceConfiguration.authentication?.authenticationStatus === + AuthenticationStatus.SUCCESS; + + const pluginType = useSelector((state: AppState) => + getPluginTypeFromDatasourceId(state, datasourceId), + ); + + const applicationId = useSelector(getCurrentApplicationId); + + const dispatch = useDispatch(); + const location = useLocation(); + const { pageId } = useParams(); + + // Handles datasource saving + const handleDatasourceSave = () => { + dispatch( + updateDatasource( + getSanitizedFormData(), + pluginType + ? redirectAuthorizationCode(pageId, datasourceId, pluginType) + : undefined, + ), + ); + }; + + // Handles datasource deletion + const handleDatasourceDelete = () => { + dispatch(deleteDatasource({ id: datasourceId })); + }; + + useEffect(() => { + // When the authorization server redirects a user to the datasource form page, the url contains the "response_status" query parameter . + // Get the access token if response_status is successful else show a toast error + + const search = new URLSearchParams(location.search); + const status = search.get("response_status"); + if (status) { + const display_message = search.get("display_message"); + const variant = Variant.danger; + + if (status !== AuthorizationStatus.SUCCESS) { + const message = + status === AuthorizationStatus.APPSMITH_ERROR + ? OAUTH_AUTHORIZATION_APPSMITH_ERROR + : OAUTH_AUTHORIZATION_FAILED; + Toaster.show({ text: display_message || message, variant }); + } else { + dispatch(getOAuthAccessToken(datasourceId)); + } + AnalyticsUtil.logEvent("DATASOURCE_AUTH_COMPLETE", { + applicationId, + datasourceId, + pageId, + }); + } + }, []); + + return ( + <> + {!isAuthorized && ( + Datasource not authorized + )} + {shouldRender ? ( + + + + + + ) : null}{" "} + + ); +} + +export default OAuth; diff --git a/app/client/src/pages/common/datasourceAuth/index.tsx b/app/client/src/pages/common/datasourceAuth/index.tsx new file mode 100644 index 0000000000..bb560e335b --- /dev/null +++ b/app/client/src/pages/common/datasourceAuth/index.tsx @@ -0,0 +1,60 @@ +import { AuthType, Datasource } from "entities/Datasource"; +import React from "react"; +import OAuth from "./OAuth"; +import DefaultAuth from "./DefaultAuth"; + +interface Props { + datasource: Datasource; + formData: Datasource; + getSanitizedFormData: () => Datasource; + isInvalid: boolean; + shouldRender: boolean; +} + +function DatasourceAuth({ + datasource, + formData, + getSanitizedFormData, + isInvalid, + shouldRender, +}: Props) { + const authType = + formData && + formData.datasourceConfiguration?.authentication?.authenticationType; + + // Render call-to-actions depending on the datasource authentication type + + switch (authType) { + case AuthType.OAUTH2: + return ( + + ); + + case AuthType.DBAUTH: + return ( + + ); + + default: + return ( + + ); + } +} + +export default DatasourceAuth; diff --git a/app/client/src/sagas/DatasourcesSagas.ts b/app/client/src/sagas/DatasourcesSagas.ts index ffc3d0cb62..8f63f96616 100644 --- a/app/client/src/sagas/DatasourcesSagas.ts +++ b/app/client/src/sagas/DatasourcesSagas.ts @@ -26,6 +26,7 @@ import { getDatasourceDraft, getPluginForm, getGenerateCRUDEnabledPluginMap, + getPluginPackageFromDatasourceId, } from "selectors/entitiesSelector"; import { changeDatasource, @@ -62,24 +63,26 @@ import { Variant } from "components/ads/common"; import { Toaster } from "components/ads/Toast"; import { getConfigInitialValues } from "components/formControls/utils"; import { setActionProperty } from "actions/pluginActionActions"; -import SaasApi from "api/SaasApi"; -import { authorizeSaasWithAppsmithToken } from "api/CloudServicesApi"; +import { authorizeDatasourceWithAppsmithToken } from "api/CloudServicesApi"; import { createMessage, DATASOURCE_CREATE, DATASOURCE_DELETE, DATASOURCE_UPDATE, DATASOURCE_VALID, - SAAS_APPSMITH_TOKEN_NOT_FOUND, - SAAS_AUTHORIZATION_APPSMITH_ERROR, - SAAS_AUTHORIZATION_FAILED, - SAAS_AUTHORIZATION_SUCCESSFUL, + OAUTH_APPSMITH_TOKEN_NOT_FOUND, + OAUTH_AUTHORIZATION_APPSMITH_ERROR, + OAUTH_AUTHORIZATION_FAILED, + OAUTH_AUTHORIZATION_SUCCESSFUL, } from "constants/messages"; import AppsmithConsole from "utils/AppsmithConsole"; import { ENTITY_TYPE } from "entities/AppsmithConsole"; import localStorage from "utils/localStorage"; import log from "loglevel"; -import { APPSMITH_TOKEN_STORAGE_KEY } from "pages/Editor/SaaSEditor/constants"; +import { + APPSMITH_TOKEN_STORAGE_KEY, + SAAS_EDITOR_DATASOURCE_ID_URL, +} from "pages/Editor/SaaSEditor/constants"; import { checkAndGetPluginFormConfigsSaga } from "sagas/PluginSagas"; import { PluginType } from "entities/Action"; import LOG_TYPE from "entities/AppsmithConsole/logtype"; @@ -90,6 +93,8 @@ import { GenerateCRUDEnabledPluginMap } from "../api/PluginApi"; import { getIsGeneratePageInitiator } from "../utils/GenerateCrudUtil"; import { trimQueryString } from "utils/helpers"; import { updateReplayEntity } from "actions/pageActions"; +import OAuthApi from "api/OAuthApi"; +import { AppState } from "reducers"; function* fetchDatasourcesSaga() { try { @@ -218,11 +223,26 @@ export function* deleteDatasourceSaga( if (isValidResponse) { const pageId = yield select(getCurrentPageId); - + const pluginPackageName = yield select((state: AppState) => + getPluginPackageFromDatasourceId(state, id), + ); const datasourcePathWithoutQuery = trimQueryString( DATA_SOURCES_EDITOR_ID_URL(applicationId, pageId, id), ); - if (window.location.pathname === datasourcePathWithoutQuery) { + + const saasPathWithoutQuery = trimQueryString( + SAAS_EDITOR_DATASOURCE_ID_URL( + applicationId, + pageId, + pluginPackageName, + id, + ), + ); + + if ( + window.location.pathname === datasourcePathWithoutQuery || + window.location.pathname === saasPathWithoutQuery + ) { history.push( INTEGRATION_EDITOR_URL( applicationId, @@ -369,23 +389,26 @@ function* redirectAuthorizationCodeSaga( if (pluginType === PluginType.API) { window.location.href = `/api/v1/datasources/${datasourceId}/pages/${pageId}/code`; - } else if (pluginType === PluginType.SAAS) { + } else { try { // Get an "appsmith token" from the server - const response: ApiResponse = yield SaasApi.getAppsmithToken( + const response: ApiResponse = yield OAuthApi.getAppsmithToken( datasourceId, pageId, ); + if (validateResponse(response)) { const appsmithToken = response.data; // Save the token for later use once we come back from the auth flow localStorage.setItem(APPSMITH_TOKEN_STORAGE_KEY, appsmithToken); // Redirect to the cloud services to authorise - window.location.assign(authorizeSaasWithAppsmithToken(appsmithToken)); + window.location.assign( + authorizeDatasourceWithAppsmithToken(appsmithToken), + ); } } catch (e) { Toaster.show({ - text: SAAS_AUTHORIZATION_FAILED, + text: OAUTH_AUTHORIZATION_FAILED, variant: Variant.danger, }); log.error(e); @@ -401,16 +424,16 @@ function* getOAuthAccessTokenSaga( const appsmithToken = localStorage.getItem(APPSMITH_TOKEN_STORAGE_KEY); if (!appsmithToken) { // Error out because auth token should been here - log.error(SAAS_APPSMITH_TOKEN_NOT_FOUND); + log.error(OAUTH_APPSMITH_TOKEN_NOT_FOUND); Toaster.show({ - text: SAAS_AUTHORIZATION_APPSMITH_ERROR, + text: OAUTH_AUTHORIZATION_APPSMITH_ERROR, variant: Variant.danger, }); return; } try { // Get access token for datasource - const response = yield SaasApi.getAccessToken(datasourceId, appsmithToken); + const response = yield OAuthApi.getAccessToken(datasourceId, appsmithToken); if (validateResponse(response)) { // Update the datasource object yield put({ @@ -418,7 +441,7 @@ function* getOAuthAccessTokenSaga( payload: response.data, }); Toaster.show({ - text: SAAS_AUTHORIZATION_SUCCESSFUL, + text: OAUTH_AUTHORIZATION_SUCCESSFUL, variant: Variant.success, }); // Remove the token because it is supposed to be short lived @@ -426,7 +449,7 @@ function* getOAuthAccessTokenSaga( } } catch (e) { Toaster.show({ - text: SAAS_AUTHORIZATION_FAILED, + text: OAUTH_AUTHORIZATION_FAILED, variant: Variant.danger, }); log.error(e); @@ -780,6 +803,7 @@ function* updateDatasourceSuccessSaga(action: UpdateDatasourceSuccessAction) { const isGeneratePageInitiator = getIsGeneratePageInitiator( queryParams.isGeneratePageMode, ); + if ( isGeneratePageInitiator && updatedDatasource.pluginId && @@ -990,10 +1014,7 @@ export function* watchDatasourcesSagas() { ReduxActionTypes.REDIRECT_AUTHORIZATION_CODE, redirectAuthorizationCodeSaga, ), - takeEvery( - ReduxActionTypes.SAAS_GET_OAUTH_ACCESS_TOKEN, - getOAuthAccessTokenSaga, - ), + takeEvery(ReduxActionTypes.GET_OAUTH_ACCESS_TOKEN, getOAuthAccessTokenSaga), takeEvery( ReduxActionTypes.FETCH_DATASOURCE_STRUCTURE_INIT, fetchDatasourceStructureSaga, diff --git a/app/client/src/selectors/entitiesSelector.ts b/app/client/src/selectors/entitiesSelector.ts index ee6ae4677c..a35b1561cd 100644 --- a/app/client/src/selectors/entitiesSelector.ts +++ b/app/client/src/selectors/entitiesSelector.ts @@ -114,6 +114,21 @@ export const getPluginNameFromId = ( return plugin.name; }; +export const getPluginTypeFromDatasourceId = ( + state: AppState, + datasourceId: string, +): PluginType | undefined => { + const datasource = state.entities.datasources.list.find( + (datasource) => datasource.id === datasourceId, + ); + const plugin = state.entities.plugins.list.find( + (plugin) => plugin.id === datasource?.pluginId, + ); + + if (!plugin) return undefined; + return plugin.type; +}; + export const getPluginForm = (state: AppState, pluginId: string): any[] => { return state.entities.plugins.formConfigs[pluginId]; }; diff --git a/app/client/src/utils/AnalyticsUtil.tsx b/app/client/src/utils/AnalyticsUtil.tsx index f737237e76..9cd393bcbc 100644 --- a/app/client/src/utils/AnalyticsUtil.tsx +++ b/app/client/src/utils/AnalyticsUtil.tsx @@ -199,7 +199,17 @@ export type EventName = | "GS_CONNECT_BUTTON_ON_GIT_SYNC_MODAL_CLICK" | "GS_IMPORT_VIA_GIT_CLICK" | "GS_CONTACT_SALES_CLICK" - | "REFLOW_BETA_FLAG"; + | "REFLOW_BETA_FLAG" + | "CONNECT_GIT_CLICK" + | "REPO_URL_EDIT" + | "GENERATE_KEY_BUTTON_CLICK" + | "COPY_SSH_KEY_BUTTON_CLICK" + | "LEARN_MORE_LINK_FOR_REMOTEURL_CLICK" + | "LEARN_MORE_LINK_FOR_SSH_CLICK" + | "DEFAULT_CONFIGURATION_EDIT_BUTTON_CLICK" + | "DEFAULT_CONFIGURATION_CHECKBOX_TOGGLED" + | "CONNECT_BUTTON_ON_GIT_SYNC_MODAL_CLICK" + | "DATASOURCE_AUTH_COMPLETE"; function getApplicationId(location: Location) { const pathSplit = location.pathname.split("/"); diff --git a/app/server/appsmith-plugins/mongoPlugin/src/main/resources/form.json b/app/server/appsmith-plugins/mongoPlugin/src/main/resources/form.json index 1878ba55df..f19a86a605 100644 --- a/app/server/appsmith-plugins/mongoPlugin/src/main/resources/form.json +++ b/app/server/appsmith-plugins/mongoPlugin/src/main/resources/form.json @@ -179,7 +179,7 @@ "controlType": "INPUT_TEXT", "placeholderText": "Password", "encrypted": true - } + } ] } ]