Embedded Datasource

- Show path along with url in the api pane for embedded datasource
- Add store as datasource menu option for embedded datasource
- Allow users to type the  path for global datasources
- Show the base url as a tag along with the path for global datasource.
This commit is contained in:
Akash N 2020-06-03 05:40:48 +00:00 committed by Hetu Nandu
parent 2eb6d6cbe7
commit 62f003df6e
20 changed files with 554 additions and 171 deletions

View File

@ -17,6 +17,7 @@
"responsetext2": "qui est esse",
"baseUrl3": "https://reqres.in",
"methods2": "api/users/2",
"invalidPath": "api/users/a",
"responsetext3": "Josh M Krantz",
"postUrl": "https://reqres.in",
"deleteUrl": "",

View File

@ -23,7 +23,7 @@ describe("API Panel Test Functionality", function() {
cy.ClearSearch();
cy.SearchAPIandClick("SecondAPI");
//invalid api end point check
cy.EditSourceDetail(testdata.baseUrl3, testdata.methods2);
cy.EditSourceDetail(testdata.baseUrl3, testdata.invalidPath);
cy.ResponseStatusCheck("404 NOT_FOUND");
cy.DeleteAPI();
});

View File

@ -202,7 +202,8 @@ Cypress.Commands.add("enterDatasourceAndPath", (datasource, path) => {
cy.xpath(apiwidget.autoSuggest)
.first()
.click({ force: true });
cy.get(apiwidget.path)
cy.get(apiwidget.editResourceUrl)
.first()
.click({ force: true })
.type(path, { parseSpecialCharSequences: false });
});
@ -228,16 +229,17 @@ Cypress.Commands.add(
Cypress.Commands.add("EditSourceDetail", (baseUrl, v1method) => {
cy.get(apiwidget.editResourceUrl)
.first()
.clear()
.click({ force: true })
.type(baseUrl);
.clear()
.type(`{backspace}${baseUrl}`);
cy.xpath(apiwidget.autoSuggest)
.first()
.click({ force: true });
cy.get(ApiEditor.ApiRunBtn).scrollIntoView();
cy.get(apiwidget.path)
cy.get(apiwidget.editResourceUrl)
.first()
.focus()
.type("{selectall}{backspace}api/users/2")
.type(v1method)
.should("have.value", v1method);
cy.SaveAPI();
});
@ -905,7 +907,7 @@ Cypress.Commands.add("createApi", (url, parameters) => {
cy.contains(url).click({
force: true,
});
cy.get(".CodeMirror.CodeMirror-empty textarea")
cy.get(apiwidget.editResourceUrl)
.first()
.click({ force: true })
.type(parameters, { force: true });

View File

@ -48,6 +48,16 @@ export const createNewApiAction = (
payload: { pageId },
});
export const setDatasourceFieldText = (
apiId: string,
value: string,
): ReduxAction<{ apiId: string; value: string }> => {
return {
type: ReduxActionTypes.SET_DATASOURCE_FIELD_TEXT,
payload: { apiId, value },
};
};
export const setExtraFormData = (
apiId: string,
extraformData: {},

View File

@ -77,6 +77,12 @@ export const initDatasourcePane = (
};
};
export const storeAsDatasource = () => {
return {
type: ReduxActionTypes.STORE_AS_DATASOURCE_INIT,
};
};
export default {
createDatasource,
fetchDatasources,

View File

@ -0,0 +1,3 @@
<svg width="20" height="16" viewBox="0 0 20 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 16H20V12H0V16ZM2 13H4V15H2V13ZM0 0V4H20V0H0ZM4 3H2V1H4V3ZM0 10H20V6H0V10ZM2 7H4V9H2V7Z" fill="#F4F4F4"/>
</svg>

After

Width:  |  Height:  |  Size: 220 B

View File

@ -1,7 +1,9 @@
import React from "react";
import Creatable from "react-select/creatable";
import Select, { InputActionMeta } from "react-select";
import { WrappedFieldInputProps, WrappedFieldMetaProps } from "redux-form";
import { theme } from "constants/DefaultTheme";
import { SelectComponents } from "react-select/src/components";
type DropdownProps = {
options: Array<{
@ -12,9 +14,12 @@ type DropdownProps = {
isLoading?: boolean;
input: WrappedFieldInputProps;
meta: WrappedFieldMetaProps;
components: SelectComponents<any>;
onCreateOption: (inputValue: string) => void;
formatCreateLabel?: (value: string) => React.ReactNode;
noOptionsMessage?: (obj: { inputValue: string }) => string;
inputValue?: string;
onInputChange: (value: string, actionMeta: InputActionMeta) => void;
};
const selectStyles = {
@ -22,7 +27,7 @@ const selectStyles = {
...provided,
color: "#a3b3bf",
}),
singleValue: (provided: any) => ({
multiValue: (provided: any) => ({
...provided,
backgroundColor: "rgba(104,113,239,0.1)",
border: "1px solid rgba(104, 113, 239, 0.5)",
@ -34,9 +39,15 @@ const selectStyles = {
display: "inline-block",
transform: "none",
}),
multiValueRemove: () => {
return {
display: "none",
};
},
container: (styles: any) => ({
...styles,
flex: 1,
zIndex: "5",
}),
control: (styles: any, state: any) => ({
...styles,
@ -69,23 +80,34 @@ class CreatableDropdown extends React.Component<DropdownProps> {
placeholder,
options,
isLoading,
onCreateOption,
input,
formatCreateLabel,
noOptionsMessage,
components,
inputValue,
onInputChange,
} = this.props;
const optionalProps: Partial<DropdownProps> = {};
if (formatCreateLabel) optionalProps.formatCreateLabel = formatCreateLabel;
if (noOptionsMessage) optionalProps.noOptionsMessage = noOptionsMessage;
if (components) optionalProps.components = components;
if (inputValue) optionalProps.inputValue = inputValue;
if (onInputChange) optionalProps.onInputChange = onInputChange;
return (
<Creatable
<Select
isMulti
placeholder={placeholder}
options={options}
styles={selectStyles}
isLoading={isLoading}
onCreateOption={onCreateOption}
{...input}
onChange={value => input.onChange(value)}
onChange={value => {
const formattedValue = value;
if (formattedValue && formattedValue.length > 1) {
formattedValue.shift();
}
input.onChange(formattedValue);
}}
onBlur={() => input.value}
isClearable
{...optionalProps}

View File

@ -9,7 +9,6 @@ import { AppState } from "reducers";
import { ActionResponse } from "api/ActionAPI";
import { formatBytes } from "utils/helpers";
import { APIEditorRouteParams } from "constants/routes";
import { ApiPaneReduxState } from "reducers/uiReducers/apiPaneReducer";
import LoadingOverlayScreen from "components/editorComponents/LoadingOverlayScreen";
import CodeEditor from "components/editorComponents/CodeEditor";
import { getActionResponses } from "selectors/entitiesSelector";
@ -53,7 +52,7 @@ const TableWrapper = styled.div`
interface ReduxStateProps {
responses: Record<string, ActionResponse | undefined>;
apiPane: ApiPaneReduxState;
isRunning: Record<string, boolean>;
}
const ResponseHeadersView = (props: { data: Record<string, string[]> }) => {
@ -109,14 +108,13 @@ const ApiResponseView = (props: Props) => {
params: { apiId },
},
responses,
apiPane,
} = props;
let response: ActionResponse = EMPTY_RESPONSE;
let isRunning = false;
let hasFailed = false;
if (apiId && apiId in responses) {
response = responses[apiId] || EMPTY_RESPONSE;
isRunning = apiPane.isRunning[apiId];
isRunning = props.isRunning[apiId];
hasFailed = response.statusCode ? response.statusCode[0] !== "2" : false;
}
@ -217,7 +215,7 @@ const ApiResponseView = (props: Props) => {
const mapStateToProps = (state: AppState): ReduxStateProps => {
return {
responses: getActionResponses(state),
apiPane: state.ui.apiPane,
isRunning: state.ui.apiPane.isRunning,
};
};

View File

@ -1,65 +1,266 @@
import React from "react";
import React, { useState, useEffect } from "react";
import CreatableDropdown from "components/designSystems/appsmith/CreatableDropdown";
import { connect } from "react-redux";
import { Field } from "redux-form";
import { Field, formValueSelector, change } from "redux-form";
import { AppState } from "reducers";
import { ReactComponent as StorageIcon } from "assets/icons/menu/storage.svg";
import { DatasourceDataState } from "reducers/entityReducers/datasourceReducer";
import { Plugin } from "api/PluginApi";
import { getDatasourcePlugins } from "selectors/entitiesSelector";
import _ from "lodash";
import { createDatasource } from "actions/datasourceActions";
import { createDatasource, storeAsDatasource } from "actions/datasourceActions";
import AnalyticsUtil from "utils/AnalyticsUtil";
import { Datasource, CreateDatasourceConfig } from "api/DatasourcesApi";
import styled, { createGlobalStyle } from "styled-components";
import { MenuItem, Menu, Popover, Position } from "@blueprintjs/core";
import { IconWrapper } from "constants/IconConstants";
import { theme } from "constants/DefaultTheme";
import { ControlIcons } from "icons/ControlIcons";
import { API_EDITOR_FORM_NAME } from "constants/forms";
import { InputActionMeta } from "react-select";
import { setDatasourceFieldText } from "actions/apiPaneActions";
interface ReduxStateProps {
datasources: DatasourceDataState;
validDatasourcePlugins: Plugin[];
apiId: string;
value: Datasource;
}
interface ReduxActionProps {
createDatasource: (value: string) => void;
storeAsDatasource: () => void;
changeDatasource: (value: Datasource | CreateDatasourceConfig) => void;
changePath: (value: string) => void;
setDatasourceFieldText: (apiId: string, value: string) => void;
}
interface ComponentProps {
name: string;
pluginId: string;
appName: string;
datasourceFieldText: string;
}
const StyledMenuItem = styled(MenuItem)`
&&&&.bp3-menu-item {
align-items: center;
width: 202px;
justify-content: center;
}
`;
const StyledMenu = styled(Menu)`
&&&&.bp3-menu {
padding: 8px;
background: #ffffff;
border: 1px solid #ebeff2;
box-sizing: border-box;
box-shadow: 0px 2px 4px rgba(67, 70, 74, 0.14);
border-radius: 4px;
}
`;
const TooltipStyles = createGlobalStyle`
.helper-tooltip{
.bp3-popover {
margin-right: 10px;
margin-top: 5px;
}
}
`;
const DatasourcesField = (
props: ReduxActionProps & ReduxStateProps & ComponentProps,
) => {
const options = props.datasources.list
.filter(r => r.pluginId === props.pluginId)
.filter(r =>
props.validDatasourcePlugins.some(plugin => plugin.id === r.pluginId),
)
.filter(r => r.datasourceConfiguration)
.filter(r => r.datasourceConfiguration.url)
.map(r => ({
label: r.datasourceConfiguration?.url.endsWith("/")
? r.datasourceConfiguration?.url.slice(0, -1)
: r.datasourceConfiguration.url,
value: r.id,
}));
const [inputValue, setValue] = useState(props.datasourceFieldText);
useEffect(() => {
setValue(props.datasourceFieldText);
}, [props.datasourceFieldText]);
const options = React.useMemo(() => {
return props.datasources.list
.filter(r => r.pluginId === props.pluginId)
.filter(r => {
return props.validDatasourcePlugins.some(
plugin => plugin.id === r.pluginId,
);
})
.filter(r => r.datasourceConfiguration)
.filter(r => r.datasourceConfiguration.url)
.map(r => ({
label: r.datasourceConfiguration?.url,
value: r.id,
}));
}, [props.datasources.list, props.validDatasourcePlugins, props.pluginId]);
const { storeAsDatasource } = props;
let isEmbeddedDatasource = true;
if (props.value && props.value.id) {
isEmbeddedDatasource = false;
} else if (props.value && props.value.datasourceConfiguration) {
isEmbeddedDatasource = true;
}
const DropdownIndicator = (props: any) => {
if (props.hasValue) return null;
const MenuContainer = (
<StyledMenu>
<StyledMenuItem
icon={
<IconWrapper
width={theme.fontSizes[4]}
height={theme.fontSizes[4]}
color={"#535B62"}
>
<StorageIcon />
</IconWrapper>
}
text="Store as datasource"
onClick={storeAsDatasource}
/>
</StyledMenu>
);
return (
<>
<TooltipStyles />
<Popover
content={MenuContainer}
position={Position.BOTTOM_LEFT}
usePortal
portalClassName="helper-tooltip"
>
<div
style={{
padding: "8px 13px 3px 13px",
}}
onMouseDown={e => {
e.stopPropagation();
}}
>
<ControlIcons.MORE_HORIZONTAL_CONTROL
width={theme.fontSizes[4]}
height={theme.fontSizes[4]}
color="#C4C4C4"
/>
</div>
</Popover>
</>
);
};
return (
<Field
name={props.name}
component={CreatableDropdown}
isLoading={props.datasources.loading}
options={options}
components={{
ClearIndicator: () => null,
IndicatorSeparator: () => null,
DropdownIndicator,
}}
placeholder="https://<base-url>.com"
onCreateOption={props.createDatasource}
format={(value: string) => _.find(options, { value }) || ""}
parse={(option: { value: string }) => (option ? option.value : null)}
formatCreateLabel={(value: string) => `Create data source "${value}"`}
noOptionsMessage={() => "No data sources created"}
onInputChange={(value: string, actionMeta: InputActionMeta) => {
const { action } = actionMeta;
if (action === "input-blur") {
props.setDatasourceFieldText(props.apiId, inputValue);
return value;
} else if (action === "set-value") {
setValue("");
return "";
} else if (action === "menu-close") {
return value;
}
setValue(value);
if (isEmbeddedDatasource) {
let datasourcePayload: Datasource | CreateDatasourceConfig;
let pathPayload: string;
try {
const url = new URL(value);
const path = url.pathname === "/" ? "" : url.pathname;
const params = url.search;
const baseUrl = url.origin;
datasourcePayload = {
name: baseUrl,
datasourceConfiguration: {
url: baseUrl,
},
pluginId: props.pluginId,
appName: props.appName,
};
pathPayload = path + params;
} catch (e) {
datasourcePayload = {
name: value,
datasourceConfiguration: {
url: value,
},
pluginId: props.pluginId,
appName: props.appName,
};
pathPayload = "";
}
const updateValues = _.debounce(() => {
props.changeDatasource(datasourcePayload);
props.changePath(pathPayload);
}, 50);
updateValues();
} else {
const updatePath = _.debounce(() => {
props.changePath(value);
}, 50);
updatePath();
}
}}
format={(value: Datasource) => {
if (!value || !value.datasourceConfiguration) return "";
if (!value.id) {
return "";
}
const option = _.find(options, { value: value.id });
return option ? [option] : "";
}}
parse={(option: { value: string }[]) => {
if (!option) return null;
if (option.length) {
const datasources = props.datasources.list;
return datasources.find(
datasource => datasource.id === option[0].value,
);
}
}}
inputValue={inputValue}
noOptionsMessage={() => null}
/>
);
};
const mapStateToProps = (state: AppState): ReduxStateProps => ({
datasources: state.entities.datasources,
validDatasourcePlugins: getDatasourcePlugins(state),
});
const mapStateToProps = (state: AppState): ReduxStateProps => {
const selector = formValueSelector(API_EDITOR_FORM_NAME);
const apiId = selector(state, "id");
const datasource = selector(state, "datasource");
return {
datasources: state.entities.datasources,
validDatasourcePlugins: getDatasourcePlugins(state),
apiId,
value: datasource,
};
};
const mapDispatchToProps = (
dispatch: any,
@ -88,6 +289,16 @@ const mapDispatchToProps = (
}),
);
},
storeAsDatasource: () => dispatch(storeAsDatasource()),
changeDatasource: value => {
dispatch(change(API_EDITOR_FORM_NAME, "datasource", value));
},
changePath: (value: string) => {
dispatch(change(API_EDITOR_FORM_NAME, "actionConfiguration.path", value));
},
setDatasourceFieldText: (apiId, value) => {
dispatch(setDatasourceFieldText(apiId, value));
},
});
export default connect(mapStateToProps, mapDispatchToProps)(DatasourcesField);

View File

@ -77,6 +77,10 @@ export const ReduxActionTypes: { [key: string]: string } = {
FETCH_PUBLISHED_PAGE_SUCCESS: "FETCH_PUBLISHED_PAGE_SUCCESS",
DELETE_DATASOURCE_INIT: "DELETE_DATASOURCE_INIT",
DELETE_DATASOURCE_SUCCESS: "DELETE_DATASOURCE_SUCCESS",
STORE_AS_DATASOURCE_INIT: "STORE_AS_DATASOURCE_INIT",
STORE_AS_DATASOURCE_UPDATE: "STORE_AS_DATASOURCE_UPDATE",
STORE_AS_DATASOURCE_COMPLETE: "STORE_AS_DATASOURCE_COMPLETE",
SET_DATASOURCE_FIELD_TEXT: "SET_DATASOURCE_FIELD_TEXT",
PUBLISH_APPLICATION_INIT: "PUBLISH_APPLICATION_INIT",
PUBLISH_APPLICATION_SUCCESS: "PUBLISH_APPLICATION_SUCCESS",
CREATE_PAGE_INIT: "CREATE_PAGE_INIT",

View File

@ -9,29 +9,26 @@ import {
import {
HTTP_METHOD_OPTIONS,
HTTP_METHODS,
CONTENT_TYPE,
} from "constants/ApiEditorConstants";
import styled from "styled-components";
import PostBodyData from "./PostBodyData";
import FormLabel from "components/editorComponents/FormLabel";
import FormRow from "components/editorComponents/FormRow";
import { BaseButton } from "components/designSystems/blueprint/ButtonComponent";
import { RestAction, PaginationField } from "api/ActionAPI";
import { ReduxActionTypes } from "constants/ReduxActionConstants";
import TextField from "components/editorComponents/form/fields/TextField";
import DynamicTextField from "components/editorComponents/form/fields/DynamicTextField";
import DropdownField from "components/editorComponents/form/fields/DropdownField";
import DatasourcesField from "components/editorComponents/form/fields/DatasourcesField";
import KeyValueFieldArray from "components/editorComponents/form/fields/KeyValueFieldArray";
import ApiResponseView from "components/editorComponents/ApiResponseView";
import { API_EDITOR_FORM_NAME } from "constants/forms";
import LoadingOverlayScreen from "components/editorComponents/LoadingOverlayScreen";
import { FormIcons } from "icons/FormIcons";
import { BaseTabbedView } from "components/designSystems/appsmith/TabbedView";
import Pagination, { PaginationType } from "./Pagination";
import { Icon } from "@blueprintjs/core";
import { HelpMap, HelpBaseURL } from "constants/HelpConstants";
import CollapsibleHelp from "components/designSystems/appsmith/help/CollapsibleHelp";
import { BaseTabbedView } from "components/designSystems/appsmith/TabbedView";
import KeyValueFieldArray from "components/editorComponents/form/fields/KeyValueFieldArray";
import PostBodyData from "./PostBodyData";
import ApiResponseView from "components/editorComponents/ApiResponseView";
const Form = styled.form`
display: flex;
@ -59,23 +56,6 @@ const MainConfiguration = styled.div`
padding-left: 17px;
`;
const SecondaryWrapper = styled.div`
display: flex;
height: 100%;
border-top: 1px solid #d0d7dd;
margin-top: 15px;
`;
const RequestParamsWrapper = styled.div`
flex: 4;
border-right: 1px solid #d0d7dd;
height: 100%;
overflow-y: auto;
padding-top: 6px;
padding-left: 17px;
padding-right: 10px;
`;
const ActionButtons = styled.div`
flex: 1;
`;
@ -88,13 +68,15 @@ const ActionButton = styled(BaseButton)`
}
`;
const HeadersSection = styled.div`
margin-bottom: 32px;
`;
const DatasourceWrapper = styled.div`
width: 100%;
max-width: 320px;
`;
const SecondaryWrapper = styled.div`
display: flex;
height: 100%;
border-top: 1px solid #d0d7dd;
margin-top: 15px;
`;
const TabbedViewContainer = styled.div`
@ -113,6 +95,19 @@ const StyledOpenDocsIcon = styled(Icon)`
height: 18px;
}
`;
const RequestParamsWrapper = styled.div`
flex: 4;
border-right: 1px solid #d0d7dd;
height: 100%;
overflow-y: auto;
padding-top: 6px;
padding-left: 17px;
padding-right: 10px;
`;
const HeadersSection = styled.div`
margin-bottom: 32px;
`;
interface APIFormProps {
pluginId: string;
@ -126,18 +121,15 @@ interface APIFormProps {
isDeleting: boolean;
paginationType: PaginationType;
appName: string;
actionConfiguration?: any;
httpMethodFromForm: string;
actionConfigurationBody: object | string;
actionConfigurationHeaders?: any;
contentType: {
key: string;
value: string;
};
apiId: string;
location: {
pathname: string;
};
dispatch: any;
datasourceFieldText: string;
}
type Props = APIFormProps & InjectedFormProps<RestAction, APIFormProps>;
@ -153,15 +145,13 @@ const ApiEditorForm: React.FC<Props> = (props: Props) => {
isDeleting,
isRunning,
isSaving,
actionConfiguration,
actionConfigurationHeaders,
actionConfigurationBody,
httpMethodFromForm,
location,
dispatch,
apiId,
httpMethodFromForm,
} = props;
const allowPostBody =
httpMethodFromForm && httpMethodFromForm !== HTTP_METHODS[0];
useEffect(() => {
dispatch({
type: ReduxActionTypes.SET_LAST_USED_EDITOR_PAGE,
@ -170,6 +160,8 @@ const ApiEditorForm: React.FC<Props> = (props: Props) => {
},
});
});
const allowPostBody =
httpMethodFromForm && httpMethodFromForm !== HTTP_METHODS[0];
return (
<Form onSubmit={handleSubmit}>
@ -219,20 +211,13 @@ const ApiEditorForm: React.FC<Props> = (props: Props) => {
/>
<DatasourceWrapper className="t--dataSourceField">
<DatasourcesField
name="datasource.id"
key={apiId}
name="datasource"
pluginId={pluginId}
datasourceFieldText={props.datasourceFieldText}
appName={props.appName}
/>
</DatasourceWrapper>
<DynamicTextField
className="t--path"
placeholder="v1/method"
name="actionConfiguration.path"
leftIcon={FormIcons.SLASH_ICON}
normalize={value => value.trim()}
singleLine
setMaxHeight
/>
</FormRow>
</MainConfiguration>
<SecondaryWrapper>
@ -259,9 +244,7 @@ const ApiEditorForm: React.FC<Props> = (props: Props) => {
<KeyValueFieldArray
name="actionConfiguration.headers"
label="Headers"
actionConfig={
actionConfiguration && actionConfigurationHeaders
}
actionConfig={actionConfigurationHeaders}
placeholder="Value"
pushFields
/>
@ -305,24 +288,17 @@ const selector = formValueSelector(API_EDITOR_FORM_NAME);
export default connect(state => {
const httpMethodFromForm = selector(state, "actionConfiguration.httpMethod");
const actionConfiguration = selector(state, "actionConfiguration");
const actionConfigurationBody = selector(state, "actionConfiguration.body");
const actionConfigurationHeaders = selector(
state,
"actionConfiguration.headers",
);
let contentType;
if (actionConfigurationHeaders) {
contentType = actionConfigurationHeaders.find(
(header: any) => header.key.toLowerCase() === CONTENT_TYPE,
);
}
const apiId = selector(state, "id");
return {
apiId,
httpMethodFromForm,
actionConfiguration,
actionConfigurationBody,
contentType,
actionConfigurationHeaders,
};
})(

View File

@ -17,7 +17,6 @@ import {
ActionData,
ActionDataState,
} from "reducers/entityReducers/actionsReducer";
import { ApiPaneReduxState } from "reducers/uiReducers/apiPaneReducer";
import { REST_PLUGIN_PACKAGE_NAME } from "constants/ApiEditorConstants";
import _ from "lodash";
import { getCurrentApplication } from "selectors/applicationSelectors";
@ -28,6 +27,7 @@ import { Plugin } from "api/PluginApi";
import styled from "styled-components";
import FeatureFlag from "utils/featureFlags";
import { FeatureFlagsEnum } from "configs/types";
import { PaginationType } from "./Pagination";
const EmptyStateContainer = styled.div`
display: flex;
@ -39,15 +39,19 @@ const EmptyStateContainer = styled.div`
interface ReduxStateProps {
actions: ActionDataState;
apiPane: ApiPaneReduxState;
formData: RestAction;
isRunning: Record<string, boolean>;
isSaving: Record<string, boolean>;
isDeleting: Record<string, boolean>;
allowSave: boolean;
apiName: string;
currentApplication: UserApplication;
currentPageName: string | undefined;
pages: any;
plugins: Plugin[];
pluginId: any;
apiAction: RestAction | ActionData | RapidApiAction | undefined;
data: RestAction | ActionData | RapidApiAction | undefined;
paginationType: PaginationType;
datasourceFieldText: string;
}
interface ReduxActionProps {
submitForm: (name: string) => void;
@ -67,35 +71,42 @@ type Props = ReduxActionProps &
class ApiEditor extends React.Component<Props> {
handleSubmit = (values: RestAction) => {
const { formData } = this.props;
this.props.updateAction(formData);
this.props.updateAction(values);
};
handleSaveClick = () => {
const pageName = getPageName(this.props.pages, this.props.formData.pageId);
const pageName = getPageName(
this.props.pages,
this.props.match.params.pageId,
);
AnalyticsUtil.logEvent("SAVE_API_CLICK", {
apiName: this.props.formData.name,
apiName: this.props.apiName,
apiID: this.props.match.params.apiId,
pageName: pageName,
});
this.props.submitForm(API_EDITOR_FORM_NAME);
};
handleDeleteClick = () => {
const pageName = getPageName(this.props.pages, this.props.formData.pageId);
const pageName = getPageName(
this.props.pages,
this.props.match.params.pageId,
);
AnalyticsUtil.logEvent("DELETE_API_CLICK", {
apiName: this.props.formData.name,
apiName: this.props.apiName,
apiID: this.props.match.params.apiId,
pageName: pageName,
});
this.props.deleteAction(
this.props.match.params.apiId,
this.props.formData.name,
);
this.props.deleteAction(this.props.match.params.apiId, this.props.apiName);
};
handleRunClick = (paginationField?: PaginationField) => {
const pageName = getPageName(this.props.pages, this.props.formData.pageId);
const pageName = getPageName(
this.props.pages,
this.props.match.params.pageId,
);
AnalyticsUtil.logEvent("RUN_API_CLICK", {
apiName: this.props.formData.name,
apiName: this.props.apiName,
apiID: this.props.match.params.apiId,
pageName: pageName,
});
@ -130,13 +141,16 @@ class ApiEditor extends React.Component<Props> {
render() {
const {
apiPane,
match: {
params: { apiId },
},
plugins,
pluginId,
data,
isSaving,
isRunning,
isDeleting,
allowSave,
paginationType,
} = this.props;
let formUiComponent: string | undefined;
@ -148,8 +162,6 @@ class ApiEditor extends React.Component<Props> {
}
}
const { isSaving, isRunning, isDeleting, drafts } = apiPane;
const paginationType = _.get(data, "actionConfiguration.paginationType");
const apiHomeScreen = (
<ApiHomeScreen
applicationId={this.props.match.params.applicationId}
@ -177,7 +189,7 @@ class ApiEditor extends React.Component<Props> {
{formUiComponent === "ApiEditorForm" && (
<ApiEditorForm
pluginId={pluginId}
allowSave={apiId in drafts}
allowSave={allowSave}
paginationType={paginationType}
isSaving={isSaving[apiId]}
isRunning={isRunning[apiId]}
@ -186,6 +198,7 @@ class ApiEditor extends React.Component<Props> {
onSaveClick={this.handleSaveClick}
onDeleteClick={this.handleDeleteClick}
onRunClick={this.handleRunClick}
datasourceFieldText={this.props.datasourceFieldText}
appName={
this.props.currentApplication
? this.props.currentApplication.name
@ -197,7 +210,7 @@ class ApiEditor extends React.Component<Props> {
{formUiComponent === "RapidApiEditorForm" && (
<RapidApiEditorForm
allowSave={apiId in drafts}
allowSave={allowSave}
paginationType={paginationType}
isSaving={isSaving[apiId]}
isRunning={isRunning[apiId]}
@ -227,25 +240,34 @@ const mapStateToProps = (state: AppState, props: any): ReduxStateProps => {
const formData = getFormValues(API_EDITOR_FORM_NAME)(state) as RestAction;
const apiAction = getActionById(state, props);
const { drafts } = state.ui.apiPane;
const { drafts, isSaving, isDeleting, isRunning } = state.ui.apiPane;
let data: RestAction | ActionData | RapidApiAction | undefined;
let allowSave;
if (apiAction && apiAction.id in drafts) {
data = drafts[apiAction.id];
allowSave = true;
} else {
data = apiAction;
allowSave = false;
}
const datasourceFieldText =
state.ui.apiPane.datasourceFieldText[formData?.id ?? ""] || "";
return {
datasourceFieldText,
actions: state.entities.actions,
apiPane: state.ui.apiPane,
currentApplication: getCurrentApplication(state),
currentPageName: getCurrentPageName(state),
pages: state.entities.pageList.pages,
formData,
data,
apiName: formData?.name || "",
plugins: state.entities.plugins.list,
pluginId: _.get(data, "pluginId"),
paginationType: _.get(data, "actionConfiguration.paginationType"),
apiAction,
isSaving,
isRunning,
isDeleting,
allowSave,
};
};

View File

@ -41,6 +41,7 @@ interface DatasourceDBEditorProps {
isTesting: boolean;
loadingFormConfigs: boolean;
formConfig: [];
isNewDatasource: boolean;
}
interface DatasourceDBEditorState {
@ -327,7 +328,7 @@ class DatasourceDBEditor extends React.Component<
<Field
name="name"
component={FormTitle}
focusOnMount={this.isNewDatasource()}
focusOnMount={this.props.isNewDatasource}
/>
</FormTitleContainer>
{!_.isNil(sections)

View File

@ -26,6 +26,7 @@ interface ReduxStateProps {
formConfig: [];
loadingFormConfigs: boolean;
isDeleting: boolean;
newDatasource: string;
}
type Props = ReduxStateProps &
@ -58,6 +59,7 @@ class DataSourceEditor extends React.Component<Props> {
formConfig,
isDeleting,
deleteDatasource,
newDatasource,
} = this.props;
return (
@ -69,6 +71,7 @@ class DataSourceEditor extends React.Component<Props> {
isSaving={isSaving}
isTesting={isTesting}
isDeleting={isDeleting}
isNewDatasource={newDatasource === datasourceId}
onSubmit={this.handleSubmit}
onSave={this.handleSave}
onTest={this.props.testDatasource}
@ -112,6 +115,7 @@ const mapStateToProps = (state: AppState): ReduxStateProps => {
isTesting: datasources.isTesting,
formConfig: formConfigs[datasourcePane.selectedPlugin] || [],
loadingFormConfigs,
newDatasource: datasourcePane.newDatasource,
};
};

View File

@ -18,6 +18,7 @@ const initialState: ApiPaneReduxState = {
lastUsedEditorPage: "",
lastSelectedPage: "",
extraformData: {},
datasourceFieldText: {},
};
export interface ApiPaneReduxState {
@ -29,6 +30,7 @@ export interface ApiPaneReduxState {
isDeleting: Record<string, boolean>;
currentCategory: string;
lastUsedEditorPage: string;
datasourceFieldText: Record<string, string>;
lastSelectedPage: string;
extraformData: Record<string, any>;
}
@ -201,6 +203,19 @@ const apiPaneReducer = createReducer(initialState, {
},
};
},
[ReduxActionTypes.SET_DATASOURCE_FIELD_TEXT]: (
state: ApiPaneReduxState,
action: ReduxAction<{ apiId: string; value: string }>,
) => {
const { apiId } = action.payload;
return {
...state,
datasourceFieldText: {
...state.datasourceFieldText,
[apiId]: action.payload.value,
},
};
},
});
export default apiPaneReducer;

View File

@ -8,12 +8,21 @@ const initialState: DatasourcePaneReduxState = {
selectedPlugin: "",
datasourceRefs: {},
drafts: {},
actionRouteInfo: {},
newDatasource: "",
};
export interface DatasourcePaneReduxState {
selectedPlugin: string;
datasourceRefs: {};
drafts: Record<string, Datasource>;
actionRouteInfo: Partial<{
apiId: string;
datasourceId: string;
pageId: string;
applicationId: string;
}>;
newDatasource: string;
}
const datasourcePaneReducer = createReducer(initialState, {
@ -64,6 +73,43 @@ const datasourcePaneReducer = createReducer(initialState, {
...state,
drafts: _.omit(state.drafts, action.payload.id),
}),
[ReduxActionTypes.STORE_AS_DATASOURCE_UPDATE]: (
state: DatasourcePaneReduxState,
action: ReduxAction<{
apiId: string;
datasourceId: string;
pageId: string;
applicationId: string;
}>,
) => {
return {
...state,
actionRouteInfo: action.payload,
};
},
[ReduxActionTypes.STORE_AS_DATASOURCE_COMPLETE]: (
state: DatasourcePaneReduxState,
) => ({
...state,
actionRouteInfo: {},
}),
[ReduxActionTypes.CREATE_DATASOURCE_SUCCESS]: (
state: DatasourcePaneReduxState,
action: ReduxAction<{ id: string }>,
) => {
return {
...state,
newDatasource: action.payload.id,
};
},
[ReduxActionTypes.UPDATE_DATASOURCE_SUCCESS]: (
state: DatasourcePaneReduxState,
) => {
return {
...state,
newDatasource: "",
};
},
});
export default datasourcePaneReducer;

View File

@ -34,7 +34,7 @@ import { initialize, autofill, change } from "redux-form";
import { getAction } from "./ActionSagas";
import { AppState } from "reducers";
import { Property, RestAction } from "api/ActionAPI";
import { changeApi } from "actions/apiPaneActions";
import { changeApi, setDatasourceFieldText } from "actions/apiPaneActions";
import {
API_PATH_START_WITH_SLASH_ERROR,
FIELD_REQUIRED_ERROR,
@ -147,6 +147,28 @@ function* syncApiParamsSaga(
`${currentPath}${paramsString}`,
),
);
if (
actionPayload.type === ReduxFormActionTypes.VALUE_CHANGE ||
actionPayload.type === ReduxFormActionTypes.ARRAY_REMOVE
) {
if (values.datasource && values.datasource.id) {
yield put(
setDatasourceFieldText(values.id, `${currentPath}${paramsString}`),
);
} else if (
values.datasource &&
values.datasource.datasourceConfiguration
) {
yield put(
setDatasourceFieldText(
values.id,
values.datasource.datasourceConfiguration.url +
`${currentPath}${paramsString}`,
),
);
}
}
}
}
@ -191,13 +213,6 @@ function* changeApiSaga(actionPayload: ReduxAction<{ id: string }>) {
data = draft;
}
if (data.actionConfiguration.path) {
if (data.actionConfiguration.path.charAt(0) === "/")
data.actionConfiguration.path = data.actionConfiguration.path.substring(
1,
);
}
if (
data.actionConfiguration.httpMethod !== "GET" &&
!data.providerId &&

View File

@ -1,11 +1,9 @@
import { takeLatest, put, all, select } from "redux-saga/effects";
import { initialize } from "redux-form";
import { takeLatest, put, all, select, take } from "redux-saga/effects";
import {
ReduxActionTypes,
ReduxActionErrorTypes,
ReduxAction,
} from "constants/ReduxActionConstants";
import { API_EDITOR_FORM_NAME } from "constants/forms";
import { validateResponse } from "sagas/ErrorSagas";
import CurlImportApi, { CurlImportRequest } from "api/ImportApi";
import { ApiResponse } from "api/ApiResponses";
@ -13,14 +11,10 @@ import AnalyticsUtil from "utils/AnalyticsUtil";
import { AppToaster } from "components/editorComponents/ToastComponent";
import { ToastType } from "react-toastify";
import { CURL_IMPORT_SUCCESS } from "constants/messages";
import { API_EDITOR_ID_URL } from "constants/routes";
import history from "utils/history";
import {
getCurrentApplicationId,
getCurrentPageId,
} from "selectors/editorSelectors";
import { getCurrentApplicationId } from "selectors/editorSelectors";
import { fetchActions } from "actions/actionActions";
import { CURL } from "constants/ApiConstants";
import { changeApi } from "actions/apiPaneActions";
export function* curlImportSaga(action: ReduxAction<CurlImportRequest>) {
const { type, pageId, name } = action.payload;
@ -35,12 +29,16 @@ export function* curlImportSaga(action: ReduxAction<CurlImportRequest>) {
const response: ApiResponse = yield CurlImportApi.curlImport(request);
const isValidResponse = yield validateResponse(response);
const applicationId = yield select(getCurrentApplicationId);
const currentPageId = yield select(getCurrentPageId);
if (isValidResponse) {
AnalyticsUtil.logEvent("IMPORT_API", {
importSource: CURL,
});
yield put(fetchActions(applicationId));
const data = { ...response.data };
yield take(ReduxActionTypes.FETCH_ACTIONS_SUCCESS);
AppToaster.show({
message: CURL_IMPORT_SUCCESS,
type: ToastType.SUCCESS,
@ -49,12 +47,8 @@ export function* curlImportSaga(action: ReduxAction<CurlImportRequest>) {
type: ReduxActionTypes.SUBMIT_CURL_FORM_SUCCESS,
payload: response.data,
});
yield put(fetchActions(applicationId));
const data = { ...response.data };
yield put(initialize(API_EDITOR_FORM_NAME, data));
history.push(
API_EDITOR_ID_URL(applicationId, currentPageId, response.data.id),
);
yield put(changeApi(data.id));
}
} catch (error) {
yield put({

View File

@ -1,4 +1,4 @@
import { all, put, takeEvery, select, call } from "redux-saga/effects";
import { all, put, takeEvery, select, call, take } from "redux-saga/effects";
import { change, initialize, getFormValues } from "redux-form";
import _ from "lodash";
import {
@ -18,7 +18,11 @@ import {
getDatasource,
getDatasourceDraft,
} from "selectors/entitiesSelector";
import { selectPlugin } from "actions/datasourceActions";
import {
selectPlugin,
createDatasource,
changeDatasource,
} from "actions/datasourceActions";
import { fetchPluginForm } from "actions/pluginActions";
import { GenericApiResponse } from "api/ApiResponses";
import DatasourcesApi, {
@ -26,6 +30,7 @@ import DatasourcesApi, {
Datasource,
} from "api/DatasourcesApi";
import PluginApi, { DatasourceForm } from "api/PluginApi";
import {
DATA_SOURCES_EDITOR_ID_URL,
DATA_SOURCES_EDITOR_URL,
@ -37,6 +42,7 @@ import AnalyticsUtil from "utils/AnalyticsUtil";
import { AppToaster } from "components/editorComponents/ToastComponent";
import { ToastType } from "react-toastify";
import { getFormData } from "selectors/formSelectors";
import { changeApi, setDatasourceFieldText } from "actions/apiPaneActions";
function* fetchDatasourcesSaga() {
try {
@ -73,9 +79,7 @@ function* createDatasourceSaga(
type: ReduxActionTypes.CREATE_DATASOURCE_SUCCESS,
payload: response.data,
});
yield put(
change(API_EDITOR_FORM_NAME, "datasource.id", response.data.id),
);
yield put(change(API_EDITOR_FORM_NAME, "datasource", response.data));
}
} catch (error) {
yield put({
@ -344,6 +348,53 @@ function* formValueChangeSaga(
yield all([call(updateDraftsSaga)]);
}
function* storeAsDatasourceSaga() {
const { values } = yield select(getFormData, API_EDITOR_FORM_NAME);
const applicationId = yield select(getCurrentApplicationId);
const pageId = yield select(getCurrentPageId);
const datasource = _.get(values, "datasource");
history.push(DATA_SOURCES_EDITOR_URL(applicationId, pageId));
yield put(createDatasource(datasource));
const createDatasourceSuccessAction = yield take(
ReduxActionTypes.CREATE_DATASOURCE_SUCCESS,
);
const createdDatasource = createDatasourceSuccessAction.payload;
yield put({
type: ReduxActionTypes.STORE_AS_DATASOURCE_UPDATE,
payload: {
pageId,
applicationId,
apiId: values.id,
datasourceId: createdDatasource.id,
},
});
yield put(changeDatasource(createdDatasource));
}
function* updateDatasourceSuccessSaga(action: ReduxAction<Datasource>) {
const state = yield select();
const actionRouteInfo = _.get(state, "ui.datasourcePane.actionRouteInfo");
const updatedDatasource = action.payload;
if (
actionRouteInfo &&
updatedDatasource.id === actionRouteInfo.datasourceId
) {
const { apiId } = actionRouteInfo;
yield put(setDatasourceFieldText(apiId, ""));
yield put(changeApi(apiId));
}
yield put({
type: ReduxActionTypes.STORE_AS_DATASOURCE_COMPLETE,
});
}
export function* watchDatasourcesSagas() {
yield all([
takeEvery(ReduxActionTypes.FETCH_DATASOURCES_INIT, fetchDatasourcesSaga),
@ -356,6 +407,11 @@ export function* watchDatasourcesSagas() {
takeEvery(ReduxActionTypes.TEST_DATASOURCE_INIT, testDatasourceSaga),
takeEvery(ReduxActionTypes.DELETE_DATASOURCE_INIT, deleteDatasourceSaga),
takeEvery(ReduxActionTypes.CHANGE_DATASOURCE, changeDatasourceSaga),
takeEvery(ReduxActionTypes.STORE_AS_DATASOURCE_INIT, storeAsDatasourceSaga),
takeEvery(
ReduxActionTypes.UPDATE_DATASOURCE_SUCCESS,
updateDatasourceSuccessSaga,
),
// Intercepting the redux-form change actionType
takeEvery(ReduxFormActionTypes.VALUE_CHANGE, formValueChangeSaga),
]);

View File

@ -154,11 +154,9 @@ export const getQueryActions = (state: AppState): ActionDataState => {
const getCurrentPageId = (state: AppState) =>
state.entities.pageList.currentPageId;
export const getDatasourcePlugins = (state: AppState) => {
return state.entities.plugins.list.filter(
plugin => plugin?.allowUserDatasources ?? true,
);
};
export const getDatasourcePlugins = createSelector(getPlugins, plugins => {
return plugins.filter(plugin => plugin?.allowUserDatasources ?? true);
});
export const getActionsForCurrentPage = createSelector(
getCurrentPageId,
@ -169,13 +167,12 @@ export const getActionsForCurrentPage = createSelector(
},
);
export const getActionResponses = (
state: AppState,
): Record<string, ActionResponse | undefined> => {
export const getActionResponses = createSelector(getActions, actions => {
const responses: Record<string, ActionResponse | undefined> = {};
state.entities.actions.forEach(a => {
actions.forEach(a => {
responses[a.config.id] = a.data;
});
return responses;
};
});