feat: Support OAuth for all plugin types (#9657)
* customize datasource authorization * improve performance * fix save datasource bug * switch auth type from form config * better naming of components * fix minor bug * minor bug fix * minor bug fix * syntax cleanup * Add comments where necessary * Added comments where necessary * fix broken airtable page * code refactor and annotation
This commit is contained in:
parent
2bcd73e41d
commit
72f8b7e2e1
|
|
@ -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 },
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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}`;
|
||||
|
|
|
|||
28
app/client/src/api/OAuthApi.ts
Normal file
28
app/client/src/api/OAuthApi.ts
Normal file
|
|
@ -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<GenericApiResponse<string>> {
|
||||
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<GenericApiResponse<Datasource>> {
|
||||
return Api.post(
|
||||
`${OAuthApi.url}/${datasourceId}/token?appsmithToken=${token}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default OAuthApi;
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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<string>;
|
||||
datasource: Datasource;
|
||||
}
|
||||
|
||||
type Props = DatasourceDBEditorProps &
|
||||
InjectedFormProps<Datasource, DatasourceDBEditorProps>;
|
||||
|
||||
const StyledButton = styled(EditButton)`
|
||||
&&&& {
|
||||
width: 87px;
|
||||
height: 32px;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledOpenDocsIcon = styled(Icon)`
|
||||
svg {
|
||||
width: 12px;
|
||||
|
|
@ -93,15 +77,9 @@ class DatasourceDBEditor extends JSONtoForm<Props> {
|
|||
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<Props> {
|
|||
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 (
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
|
|
@ -193,41 +153,21 @@ class DatasourceDBEditor extends JSONtoForm<Props> {
|
|||
{!_.isNil(sections)
|
||||
? _.map(sections, this.renderMainSection)
|
||||
: undefined}
|
||||
<SaveButtonContainer>
|
||||
<ActionButton
|
||||
buttonStyle="DANGER"
|
||||
buttonVariant={ButtonVariantTypes.PRIMARY}
|
||||
// accent="error"
|
||||
className="t--delete-datasource"
|
||||
loading={isDeleting}
|
||||
onClick={() => handleDelete(datasourceId)}
|
||||
text="Delete"
|
||||
/>
|
||||
|
||||
<ActionButton
|
||||
// accent="secondary"
|
||||
buttonStyle="PRIMARY"
|
||||
buttonVariant={ButtonVariantTypes.SECONDARY}
|
||||
className="t--test-datasource"
|
||||
loading={isTesting}
|
||||
onClick={this.test}
|
||||
text="Test"
|
||||
/>
|
||||
<StyledButton
|
||||
className="t--save-datasource"
|
||||
disabled={this.validate()}
|
||||
filled
|
||||
intent="primary"
|
||||
loading={isSaving}
|
||||
onClick={this.save}
|
||||
size="small"
|
||||
text="Save"
|
||||
/>
|
||||
</SaveButtonContainer>
|
||||
{""}
|
||||
</>
|
||||
) : (
|
||||
<Connected />
|
||||
)}
|
||||
{/* Render datasource form call-to-actions */}
|
||||
{datasource && (
|
||||
<DatasourceAuth
|
||||
datasource={datasource}
|
||||
formData={formData}
|
||||
getSanitizedFormData={_.memoize(this.getSanitizedData)}
|
||||
isInvalid={this.validate()}
|
||||
shouldRender={!viewMode}
|
||||
/>
|
||||
)}
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
|
@ -242,6 +182,7 @@ const mapStateToProps = (state: AppState, props: any) => {
|
|||
|
||||
return {
|
||||
messages: hintMessages,
|
||||
datasource,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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<unknown>) => void;
|
||||
deleteDatasource: (id: string, onSuccess?: ReduxAction<unknown>) => void;
|
||||
getOAuthAccessToken: (id: string) => void;
|
||||
createAction: (data: Partial<Action>) => 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<Datasource, DatasourceSaaSEditorProps>;
|
||||
|
||||
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<Props> {
|
||||
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<unknown>) => {
|
||||
const normalizedValues = this.normalizeValues();
|
||||
this.props.updateDatasource(normalizedValues, onSuccess);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { formConfig } = this.props;
|
||||
const { formConfig, pluginId } = this.props;
|
||||
if (!pluginId) {
|
||||
return <EntityNotFoundPane />;
|
||||
}
|
||||
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<Props> {
|
|||
|
||||
const params: string = location.search;
|
||||
const viewMode = new URLSearchParams(params).get("viewMode");
|
||||
const isAuthorized =
|
||||
datasource?.datasourceConfiguration.authentication
|
||||
?.authenticationStatus === AuthenticationStatus.SUCCESS;
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
|
|
@ -203,60 +123,20 @@ class DatasourceSaaSEditor extends JSONtoForm<Props> {
|
|||
{!_.isNil(sections)
|
||||
? _.map(sections, this.renderMainSection)
|
||||
: null}
|
||||
{!isAuthorized && (
|
||||
<StyledAuthMessage>Datasource not authorized</StyledAuthMessage>
|
||||
)}
|
||||
<SaveButtonContainer>
|
||||
<ActionButton
|
||||
// accent="error"
|
||||
buttonStyle="DANGER"
|
||||
buttonVariant={ButtonVariantTypes.PRIMARY}
|
||||
className="t--delete-datasource"
|
||||
loading={isDeleting}
|
||||
onClick={() =>
|
||||
deleteDatasource(
|
||||
datasourceId,
|
||||
this.props.redirectToNewIntegrations(
|
||||
applicationId,
|
||||
pageId,
|
||||
) as any,
|
||||
)
|
||||
}
|
||||
text="Delete"
|
||||
/>
|
||||
|
||||
<StyledButton
|
||||
className="t--save-datasource"
|
||||
disabled={this.validate()}
|
||||
filled
|
||||
intent="primary"
|
||||
loading={isSaving}
|
||||
onClick={() => {
|
||||
AnalyticsUtil.logEvent("GSHEET_AUTH_INIT", {
|
||||
applicationId,
|
||||
datasourceId,
|
||||
pageId,
|
||||
});
|
||||
this.save(
|
||||
redirectAuthorizationCode(
|
||||
pageId,
|
||||
datasourceId,
|
||||
PluginType.SAAS,
|
||||
),
|
||||
);
|
||||
}}
|
||||
size="small"
|
||||
text={isAuthorized ? "Re-authorize" : "Authorize"}
|
||||
/>
|
||||
</SaveButtonContainer>
|
||||
{""}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Connected />
|
||||
{!isAuthorized && (
|
||||
<StyledAuthMessage>Datasource not authorized</StyledAuthMessage>
|
||||
)}
|
||||
</>
|
||||
<Connected />
|
||||
)}
|
||||
{/* Render datasource form call-to-actions */}
|
||||
{datasource && (
|
||||
<DatasourceAuth
|
||||
datasource={datasource}
|
||||
formData={formData}
|
||||
getSanitizedFormData={_.memoize(this.getSanitizedData)}
|
||||
isInvalid={this.validate()}
|
||||
shouldRender={!viewMode}
|
||||
/>
|
||||
)}
|
||||
</form>
|
||||
);
|
||||
|
|
@ -293,27 +173,7 @@ const mapStateToProps = (state: AppState, props: any) => {
|
|||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch: any): DispatchFunctions => {
|
||||
return {
|
||||
deleteDatasource: (id: string, onSuccess?: ReduxAction<unknown>) =>
|
||||
dispatch(deleteDatasource({ id }, onSuccess)),
|
||||
updateDatasource: (formData: any, onSuccess?: ReduxAction<unknown>) =>
|
||||
dispatch(updateDatasource(formData, onSuccess)),
|
||||
getOAuthAccessToken: (datasourceId: string) =>
|
||||
dispatch(getOAuthAccessToken(datasourceId)),
|
||||
createAction: (data: Partial<Action>) => {
|
||||
dispatch(createActionRequest(data));
|
||||
},
|
||||
redirectToNewIntegrations: (applicationId: string, pageId: string) => {
|
||||
dispatch(redirectToNewIntegrations(applicationId, pageId));
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps,
|
||||
)(
|
||||
export default connect(mapStateToProps)(
|
||||
reduxForm<Datasource, DatasourceSaaSEditorProps>({
|
||||
form: DATASOURCE_SAAS_FORM,
|
||||
enableReinitialize: true,
|
||||
|
|
|
|||
132
app/client/src/pages/common/datasourceAuth/DefaultAuth.tsx
Normal file
132
app/client/src/pages/common/datasourceAuth/DefaultAuth.tsx
Normal file
|
|
@ -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<ExplorerURLParams>();
|
||||
|
||||
// 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 && (
|
||||
<SaveButtonContainer>
|
||||
<ActionButton
|
||||
buttonStyle="DANGER"
|
||||
buttonVariant={ButtonVariantTypes.PRIMARY}
|
||||
// accent="error"
|
||||
className="t--delete-datasource"
|
||||
loading={isDeleting}
|
||||
onClick={handleDatasourceDelete}
|
||||
text="Delete"
|
||||
/>
|
||||
|
||||
<ActionButton
|
||||
// accent="secondary"
|
||||
buttonStyle="PRIMARY"
|
||||
buttonVariant={ButtonVariantTypes.SECONDARY}
|
||||
className="t--test-datasource"
|
||||
loading={isTesting}
|
||||
onClick={handleDatasourceTest}
|
||||
text="Test"
|
||||
/>
|
||||
<StyledButton
|
||||
className="t--save-datasource"
|
||||
disabled={isInvalid}
|
||||
filled
|
||||
intent="primary"
|
||||
loading={isSaving}
|
||||
onClick={handleDatasourceSave}
|
||||
size="small"
|
||||
text="Save"
|
||||
/>
|
||||
</SaveButtonContainer>
|
||||
)}
|
||||
{""}
|
||||
</>
|
||||
);
|
||||
}
|
||||
161
app/client/src/pages/common/datasourceAuth/OAuth.tsx
Normal file
161
app/client/src/pages/common/datasourceAuth/OAuth.tsx
Normal file
|
|
@ -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<ExplorerURLParams>();
|
||||
|
||||
// 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 && (
|
||||
<StyledAuthMessage>Datasource not authorized</StyledAuthMessage>
|
||||
)}
|
||||
{shouldRender ? (
|
||||
<SaveButtonContainer>
|
||||
<ActionButton
|
||||
// accent="error"
|
||||
buttonStyle="DANGER"
|
||||
buttonVariant={ButtonVariantTypes.PRIMARY}
|
||||
className="t--delete-datasource"
|
||||
loading={isDeleting}
|
||||
onClick={handleDatasourceDelete}
|
||||
text="Delete"
|
||||
/>
|
||||
|
||||
<StyledButton
|
||||
className="t--save-datasource"
|
||||
disabled={isInvalid}
|
||||
filled
|
||||
intent="primary"
|
||||
loading={isSaving}
|
||||
onClick={handleDatasourceSave}
|
||||
size="small"
|
||||
text={isAuthorized ? "Save and Re-authorize" : "Save and Authorize"}
|
||||
/>
|
||||
</SaveButtonContainer>
|
||||
) : null}{" "}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default OAuth;
|
||||
60
app/client/src/pages/common/datasourceAuth/index.tsx
Normal file
60
app/client/src/pages/common/datasourceAuth/index.tsx
Normal file
|
|
@ -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 (
|
||||
<OAuth
|
||||
datasource={datasource}
|
||||
getSanitizedFormData={getSanitizedFormData}
|
||||
isInvalid={isInvalid}
|
||||
shouldRender={shouldRender}
|
||||
/>
|
||||
);
|
||||
|
||||
case AuthType.DBAUTH:
|
||||
return (
|
||||
<DefaultAuth
|
||||
datasource={datasource}
|
||||
getSanitizedFormData={getSanitizedFormData}
|
||||
isInvalid={isInvalid}
|
||||
shouldRender={shouldRender}
|
||||
/>
|
||||
);
|
||||
|
||||
default:
|
||||
return (
|
||||
<DefaultAuth
|
||||
datasource={datasource}
|
||||
getSanitizedFormData={getSanitizedFormData}
|
||||
isInvalid={isInvalid}
|
||||
shouldRender={shouldRender}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default DatasourceAuth;
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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("/");
|
||||
|
|
|
|||
|
|
@ -179,7 +179,7 @@
|
|||
"controlType": "INPUT_TEXT",
|
||||
"placeholderText": "Password",
|
||||
"encrypted": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user