parent
27f4f3b728
commit
a0b536eced
|
|
@ -28,6 +28,7 @@
|
|||
"eslint": "^6.4.0",
|
||||
"flow-bin": "^0.91.0",
|
||||
"fontfaceobserver": "^2.1.0",
|
||||
"history": "^4.10.1",
|
||||
"husky": "^3.0.5",
|
||||
"jsonpath-plus": "^1.0.0",
|
||||
"lint-staged": "^9.2.5",
|
||||
|
|
@ -53,7 +54,6 @@
|
|||
"react-router-dom": "^5.0.1",
|
||||
"react-scripts": "^3.1.1",
|
||||
"react-select": "^3.0.8",
|
||||
"react-tabs": "^3.0.0",
|
||||
"redux": "^4.0.1",
|
||||
"redux-form": "^8.2.6",
|
||||
"redux-saga": "^1.0.0",
|
||||
|
|
|
|||
|
|
@ -3,7 +3,14 @@ import { RestAction } from "../api/ActionAPI";
|
|||
|
||||
export const createAction = (payload: RestAction) => {
|
||||
return {
|
||||
type: ReduxActionTypes.CREATE_ACTION,
|
||||
type: ReduxActionTypes.CREATE_ACTION_INIT,
|
||||
payload,
|
||||
};
|
||||
};
|
||||
|
||||
export const createActionSuccess = (payload: RestAction) => {
|
||||
return {
|
||||
type: ReduxActionTypes.CREATE_ACTION_SUCCESS,
|
||||
payload,
|
||||
};
|
||||
};
|
||||
|
|
@ -14,13 +21,6 @@ export const fetchActions = () => {
|
|||
};
|
||||
};
|
||||
|
||||
export const selectAction = (payload: { id: string }) => {
|
||||
return {
|
||||
type: ReduxActionTypes.SELECT_ACTION,
|
||||
payload,
|
||||
};
|
||||
};
|
||||
|
||||
export const fetchApiConfig = (payload: { id: string }) => {
|
||||
return {
|
||||
type: ReduxActionTypes.FETCH_ACTION,
|
||||
|
|
@ -30,21 +30,35 @@ export const fetchApiConfig = (payload: { id: string }) => {
|
|||
|
||||
export const runAction = (payload: { id: string }) => {
|
||||
return {
|
||||
type: ReduxActionTypes.RUN_ACTION,
|
||||
payload,
|
||||
};
|
||||
};
|
||||
|
||||
export const deleteAction = (payload: { id: string }) => {
|
||||
return {
|
||||
type: ReduxActionTypes.DELETE_ACTION,
|
||||
type: ReduxActionTypes.RUN_ACTION_INIT,
|
||||
payload,
|
||||
};
|
||||
};
|
||||
|
||||
export const updateAction = (payload: { data: RestAction }) => {
|
||||
return {
|
||||
type: ReduxActionTypes.UPDATE_ACTION,
|
||||
type: ReduxActionTypes.UPDATE_ACTION_INIT,
|
||||
payload,
|
||||
};
|
||||
};
|
||||
|
||||
export const updateActionSuccess = (payload: { data: RestAction }) => {
|
||||
return {
|
||||
type: ReduxActionTypes.UPDATE_ACTION_SUCCESS,
|
||||
payload,
|
||||
};
|
||||
};
|
||||
|
||||
export const deleteAction = (payload: { id: string }) => {
|
||||
return {
|
||||
type: ReduxActionTypes.DELETE_ACTION_INIT,
|
||||
payload,
|
||||
};
|
||||
};
|
||||
|
||||
export const deleteActionSuccess = (payload: { id: string }) => {
|
||||
return {
|
||||
type: ReduxActionTypes.DELETE_ACTION_SUCCESS,
|
||||
payload,
|
||||
};
|
||||
};
|
||||
|
|
@ -55,4 +69,7 @@ export default {
|
|||
fetchApiConfig,
|
||||
runAction,
|
||||
deleteAction,
|
||||
deleteActionSuccess,
|
||||
updateAction,
|
||||
updateActionSuccess,
|
||||
};
|
||||
|
|
|
|||
3
app/client/src/assets/icons/form/add-new.svg
Normal file
3
app/client/src/assets/icons/form/add-new.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3.40782 0L0.393555 2.90999V16H8.47325V14.609H1.79346V3.89572H4.42798V1.39105H10.3326V5.5642H11.7325V0H3.40782ZM9.71082 6.78307V10.1043H6.38865V12.6788H9.71082V16H12.2844V12.6788H15.6066V10.1043H12.2844V6.78307H9.71082Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 348 B |
|
|
@ -38,7 +38,6 @@ class CreatableDropdown extends React.Component<DropdownProps> {
|
|||
onCreateOption={onCreateOption}
|
||||
{...input}
|
||||
onChange={value => input.onChange(value)}
|
||||
onBlur={() => input.onBlur(input.value)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,12 +8,13 @@ type DropdownProps = {
|
|||
label: string;
|
||||
}>;
|
||||
input: WrappedFieldInputProps;
|
||||
placeholder: string;
|
||||
};
|
||||
|
||||
const selectStyles = {
|
||||
control: (styles: any) => ({
|
||||
...styles,
|
||||
width: 100,
|
||||
width: 120,
|
||||
}),
|
||||
};
|
||||
|
||||
|
|
@ -21,12 +22,11 @@ export const BaseDropdown = (props: DropdownProps) => {
|
|||
const { input, options } = props;
|
||||
return (
|
||||
<Select
|
||||
defaultValue={options[0]}
|
||||
placeholder={props.placeholder}
|
||||
options={options}
|
||||
styles={selectStyles}
|
||||
{...input}
|
||||
onChange={value => input.onChange(value)}
|
||||
onBlur={() => input.onBlur(input.value)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,18 +1,21 @@
|
|||
import React from "react";
|
||||
import { Tab, Tabs, TabList, TabPanel } from "react-tabs";
|
||||
import "react-tabs/style/react-tabs.scss";
|
||||
import { Tab, Tabs } from "@blueprintjs/core";
|
||||
import styled from "styled-components";
|
||||
|
||||
const TabsWrapper = styled(Tabs)`
|
||||
ul {
|
||||
border-bottom-color: #d0d7dd;
|
||||
li {
|
||||
&.react-tabs__tab--selected {
|
||||
border-color: #d0d7dd;
|
||||
left: -1px;
|
||||
border-radius: 0;
|
||||
border-top: 5px solid ${props => props.theme.colors.primary};
|
||||
}
|
||||
const TabsWrapper = styled.div`
|
||||
padding: 0 5px;
|
||||
.bp3-tab-indicator {
|
||||
background-color: ${props => props.theme.colors.primary};
|
||||
}
|
||||
.bp3-tab {
|
||||
&[aria-selected="true"] {
|
||||
color: ${props => props.theme.colors.primary};
|
||||
}
|
||||
:hover {
|
||||
color: ${props => props.theme.colors.primary};
|
||||
}
|
||||
:focus {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
|
@ -21,21 +24,23 @@ type TabbedViewComponentType = {
|
|||
tabs: Array<{
|
||||
key: string;
|
||||
title: string;
|
||||
panelComponent: () => React.ReactNode;
|
||||
panelComponent: JSX.Element;
|
||||
}>;
|
||||
};
|
||||
|
||||
export const BaseTabbedView = (props: TabbedViewComponentType) => {
|
||||
return (
|
||||
<TabsWrapper>
|
||||
<TabList>
|
||||
<Tabs>
|
||||
{props.tabs.map(tab => (
|
||||
<Tab key={tab.key}>{tab.title}</Tab>
|
||||
<Tab
|
||||
key={tab.key}
|
||||
id={tab.key}
|
||||
title={tab.title}
|
||||
panel={tab.panelComponent}
|
||||
/>
|
||||
))}
|
||||
</TabList>
|
||||
{props.tabs.map(tab => (
|
||||
<TabPanel key={tab.key}>{tab.panelComponent()}</TabPanel>
|
||||
))}
|
||||
</Tabs>
|
||||
</TabsWrapper>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ const InputStyles = css`
|
|||
border: 1px solid ${props => props.theme.colors.inputInactiveBorders};
|
||||
border-radius: 4px;
|
||||
height: 32px;
|
||||
background-color: ${props => props.theme.colors.inputInactiveBG};
|
||||
background-color: ${props => props.theme.colors.textOnDarkBG};
|
||||
&:focus {
|
||||
border-color: ${props => props.theme.colors.secondary};
|
||||
background-color: ${props => props.theme.colors.textOnDarkBG};
|
||||
|
|
|
|||
106
app/client/src/components/editor/ApiResponseView.tsx
Normal file
106
app/client/src/components/editor/ApiResponseView.tsx
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { withRouter, RouteComponentProps } from "react-router";
|
||||
import FormRow from "./FormRow";
|
||||
import { BaseText } from "../canvas/TextViewComponent";
|
||||
import { BaseTabbedView } from "../canvas/TabbedView";
|
||||
import JSONViewer from "./JSONViewer";
|
||||
import styled from "styled-components";
|
||||
import { AppState } from "../../reducers";
|
||||
import { ActionApiResponse } from "../../reducers/entityReducers/actionsReducer";
|
||||
|
||||
const ResponseWrapper = styled.div`
|
||||
flex: 4;
|
||||
`;
|
||||
const ResponseMetaInfo = styled.div`
|
||||
display: flex;
|
||||
${BaseText} {
|
||||
color: #768896;
|
||||
margin: 0 5px;
|
||||
}
|
||||
`;
|
||||
|
||||
interface ReduxStateProps {
|
||||
responses: {
|
||||
[id: string]: ActionApiResponse;
|
||||
};
|
||||
}
|
||||
|
||||
const TableWrapper = styled.div`
|
||||
&&& {
|
||||
table {
|
||||
table-layout: fixed;
|
||||
width: 100%;
|
||||
td {
|
||||
font-size: 12px;
|
||||
width: 50%;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const ResponseHeadersView = (props: {
|
||||
data: { [name: string]: Array<string> };
|
||||
}) => {
|
||||
if (!props.data) return <div />;
|
||||
return (
|
||||
<TableWrapper>
|
||||
<table className="bp3-html-table bp3-html-table-striped bp3-html-table-condensed">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Key</th>
|
||||
<th>Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{Object.keys(props.data).map(k => (
|
||||
<tr key={k}>
|
||||
<td>{k}</td>
|
||||
<td>{props.data[k].join(", ")}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</TableWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
type Props = ReduxStateProps & RouteComponentProps<{ id: string }>;
|
||||
|
||||
const ApiResponseView = (props: Props) => {
|
||||
const response = props.responses[props.match.params.id] || {};
|
||||
return (
|
||||
<ResponseWrapper>
|
||||
<FormRow>
|
||||
<BaseText styleName="secondary">{response.statusCode}</BaseText>
|
||||
<ResponseMetaInfo>
|
||||
<BaseText styleName="secondary">300ms</BaseText>
|
||||
<BaseText styleName="secondary">203 kb</BaseText>
|
||||
</ResponseMetaInfo>
|
||||
</FormRow>
|
||||
<BaseTabbedView
|
||||
tabs={[
|
||||
{
|
||||
key: "body",
|
||||
title: "Response Body",
|
||||
panelComponent: <JSONViewer data={response.body} />,
|
||||
},
|
||||
{
|
||||
key: "headers",
|
||||
title: "Response Headers",
|
||||
panelComponent: <ResponseHeadersView data={response.headers} />,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</ResponseWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
const mapStateToProps = (state: AppState): ReduxStateProps => ({
|
||||
responses: state.entities.actions.responses,
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps)(withRouter(ApiResponseView));
|
||||
|
|
@ -19,11 +19,12 @@ const JSONEditor = (props: any) => {
|
|||
highlightActiveLine={true}
|
||||
width="100%"
|
||||
setOptions={{
|
||||
enableBasicAutocompletion: true,
|
||||
enableBasicAutocompletion: false,
|
||||
enableLiveAutocompletion: false,
|
||||
enableSnippets: false,
|
||||
showLineNumbers: true,
|
||||
tabSize: 2,
|
||||
useWorker: false,
|
||||
}}
|
||||
name={input.name}
|
||||
onBlur={aceOnBlur(input.onBlur)}
|
||||
|
|
|
|||
|
|
@ -5,10 +5,13 @@ import styled from "styled-components";
|
|||
const JSONViewWrapper = styled.div`
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
& > div {
|
||||
font-size: ${props => props.theme.fontSizes[2]}px;
|
||||
}
|
||||
`;
|
||||
|
||||
const JSONViewer = (props: { data: JSON }) => {
|
||||
if (!props.data) return null;
|
||||
if (!props.data) return <div />;
|
||||
return (
|
||||
<JSONViewWrapper>
|
||||
<ReactJson
|
||||
|
|
@ -17,9 +20,6 @@ const JSONViewer = (props: { data: JSON }) => {
|
|||
displayDataTypes={false}
|
||||
indentWidth={2}
|
||||
enableClipboard={false}
|
||||
style={{
|
||||
fontSize: "10px",
|
||||
}}
|
||||
/>
|
||||
</JSONViewWrapper>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import WidgetSidebar from "../../pages/Editor/WidgetSidebar";
|
|||
import ApiSidebar from "../../pages/Editor/ApiSidebar";
|
||||
|
||||
const SidebarWrapper = styled.div`
|
||||
flex: 7;
|
||||
flex: 9;
|
||||
background-color: ${props => props.theme.colors.paneBG};
|
||||
padding: 5px 10px;
|
||||
color: ${props => props.theme.colors.textOnDarkBG};
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ interface DropdownFieldProps {
|
|||
label: string;
|
||||
value: string;
|
||||
}>;
|
||||
placeholder: string;
|
||||
}
|
||||
|
||||
const DropdownField = (props: DropdownFieldProps) => {
|
||||
|
|
@ -17,6 +18,7 @@ const DropdownField = (props: DropdownFieldProps) => {
|
|||
name={props.name}
|
||||
component={BaseDropdown}
|
||||
options={props.options}
|
||||
placeholder={props.placeholder}
|
||||
format={(value: string) => _.find(props.options, { value })}
|
||||
normalize={(option: { value: string }) => option.value}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ const ResourcesField = (
|
|||
component={CreatableDropdown}
|
||||
isLoading={props.resources.loading}
|
||||
options={options}
|
||||
placeholder="Resource"
|
||||
onCreateOption={props.createResource}
|
||||
format={(value: string) => _.find(options, { value })}
|
||||
normalize={(option: { value: string }) => option.value}
|
||||
|
|
|
|||
|
|
@ -7,8 +7,6 @@ import {
|
|||
HTTP_METHOD_OPTIONS,
|
||||
} from "../../constants/ApiEditorConstants";
|
||||
import FormLabel from "../editor/FormLabel";
|
||||
import { BaseText } from "../canvas/TextViewComponent";
|
||||
import { BaseTabbedView } from "../canvas/TabbedView";
|
||||
import styled from "styled-components";
|
||||
import FormContainer from "../editor/FormContainer";
|
||||
import { BaseButton } from "../canvas/Button";
|
||||
|
|
@ -16,7 +14,7 @@ import KeyValueFieldArray from "../fields/KeyValueFieldArray";
|
|||
import JSONEditorField from "../fields/JSONEditorField";
|
||||
import DropdownField from "../fields/DropdownField";
|
||||
import { RestAction } from "../../api/ActionAPI";
|
||||
import JSONViewer from "../../components/editor/JSONViewer";
|
||||
import ApiResponseView from "../editor/ApiResponseView";
|
||||
import { API_EDITOR_FORM_NAME } from "../../constants/forms";
|
||||
import ResourcesField from "../fields/ResourcesField";
|
||||
|
||||
|
|
@ -54,10 +52,11 @@ const ForwardSlash = styled.div`
|
|||
const RequestParamsWrapper = styled.div`
|
||||
flex: 5;
|
||||
border-right: 1px solid #d0d7dd;
|
||||
overflow-y: scroll;
|
||||
`;
|
||||
|
||||
const ResponseWrapper = styled.div`
|
||||
flex: 4;
|
||||
const ActionButtons = styled.div`
|
||||
flex: 1;
|
||||
`;
|
||||
|
||||
const ActionButton = styled(BaseButton)`
|
||||
|
|
@ -65,14 +64,6 @@ const ActionButton = styled(BaseButton)`
|
|||
margin: 0 5px;
|
||||
`;
|
||||
|
||||
const ResponseMetaInfo = styled.div`
|
||||
display: flex;
|
||||
${BaseText} {
|
||||
color: #768896;
|
||||
margin: 0 5px;
|
||||
}
|
||||
`;
|
||||
|
||||
const JSONEditorFieldWrapper = styled.div`
|
||||
flex: 1;
|
||||
border: 1px solid #d0d7dd;
|
||||
|
|
@ -84,33 +75,40 @@ interface APIFormProps {
|
|||
onSaveClick: () => void;
|
||||
onRunClick: () => void;
|
||||
onDeleteClick: () => void;
|
||||
response: any;
|
||||
isEdit: boolean;
|
||||
}
|
||||
|
||||
type Props = APIFormProps & InjectedFormProps<RestAction, APIFormProps>;
|
||||
|
||||
class ApiEditorForm extends React.Component<Props> {
|
||||
render() {
|
||||
const { onSaveClick, onDeleteClick, onRunClick } = this.props;
|
||||
const { onSaveClick, onDeleteClick, onRunClick, isEdit } = this.props;
|
||||
return (
|
||||
<Form>
|
||||
<FormRow>
|
||||
<TextField name="name" placeholderMessage="API Name" />
|
||||
<ActionButton
|
||||
text="Delete"
|
||||
styleName="error"
|
||||
onClick={onDeleteClick}
|
||||
/>
|
||||
<ActionButton text="Run" styleName="secondary" onClick={onRunClick} />
|
||||
<ActionButton
|
||||
text="Save"
|
||||
styleName="primary"
|
||||
filled
|
||||
onClick={onSaveClick}
|
||||
/>
|
||||
<ActionButtons>
|
||||
<ActionButton
|
||||
text="Delete"
|
||||
styleName="error"
|
||||
onClick={onDeleteClick}
|
||||
/>
|
||||
<ActionButton
|
||||
text="Run"
|
||||
styleName="secondary"
|
||||
onClick={onRunClick}
|
||||
/>
|
||||
<ActionButton
|
||||
text={isEdit ? "Update" : "Save"}
|
||||
styleName="primary"
|
||||
filled
|
||||
onClick={onSaveClick}
|
||||
/>
|
||||
</ActionButtons>
|
||||
</FormRow>
|
||||
<FormRow>
|
||||
<DropdownField
|
||||
placeholder="Method"
|
||||
name="actionConfiguration.httpMethod"
|
||||
options={HTTP_METHOD_OPTIONS}
|
||||
/>
|
||||
|
|
@ -138,35 +136,7 @@ class ApiEditorForm extends React.Component<Props> {
|
|||
</JSONEditorFieldWrapper>
|
||||
</FormRow>
|
||||
</RequestParamsWrapper>
|
||||
<ResponseWrapper>
|
||||
<FormRow>
|
||||
<BaseText styleName="secondary">
|
||||
{this.props.response.statusCode}
|
||||
</BaseText>
|
||||
<ResponseMetaInfo>
|
||||
<BaseText styleName="secondary">300ms</BaseText>
|
||||
<BaseText styleName="secondary">203 kb</BaseText>
|
||||
</ResponseMetaInfo>
|
||||
</FormRow>
|
||||
<BaseTabbedView
|
||||
tabs={[
|
||||
{
|
||||
key: "body",
|
||||
title: "Response Body",
|
||||
panelComponent: () => (
|
||||
<JSONViewer data={this.props.response.body} />
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "headers",
|
||||
title: "Response Headers",
|
||||
panelComponent: () => (
|
||||
<JSONViewer data={this.props.response.headers} />
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</ResponseWrapper>
|
||||
<ApiResponseView />
|
||||
</SecondaryWrapper>
|
||||
</Form>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -6,19 +6,26 @@ export const HTTP_METHOD_OPTIONS = HTTP_METHODS.map(method => ({
|
|||
}));
|
||||
|
||||
export const FORM_INITIAL_VALUES = {
|
||||
resourceId: "5d808014795dc6000482bc83",
|
||||
actionConfiguration: {
|
||||
headers: [
|
||||
{
|
||||
key: "",
|
||||
value: "",
|
||||
},
|
||||
{
|
||||
key: "",
|
||||
value: "",
|
||||
},
|
||||
],
|
||||
queryParameters: [
|
||||
{
|
||||
key: "",
|
||||
value: "",
|
||||
},
|
||||
{
|
||||
key: "",
|
||||
value: "",
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -110,7 +110,7 @@ export const theme: Theme = {
|
|||
color: Colors.GEYSER_LIGHT,
|
||||
},
|
||||
],
|
||||
sidebarWidth: "300px",
|
||||
sidebarWidth: "350px",
|
||||
headerHeight: "50px",
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -40,12 +40,17 @@ export const ReduxActionTypes: { [key: string]: string } = {
|
|||
FETCH_PROPERTY_PANE_CONFIGS_SUCCESS: "FETCH_PROPERTY_PANE_CONFIGS_SUCCESS",
|
||||
FETCH_CONFIGS_INIT: "FETCH_CONFIGS_INIT",
|
||||
ADD_WIDGET_REF: "ADD_WIDGET_REF",
|
||||
CREATE_ACTION: "CREATE_ACTION",
|
||||
CREATE_ACTION_INIT: "CREATE_ACTION_INIT",
|
||||
CREATE_ACTION_SUCCESS: "CREATE_ACTION_SUCCESS",
|
||||
FETCH_ACTIONS_INIT: "FETCH_ACTIONS_INIT",
|
||||
FETCH_ACTIONS_SUCCESS: "FETCH_ACTIONS_SUCCESS",
|
||||
FETCH_ACTION: "FETCH_ACTION",
|
||||
RUN_ACTION: "RUN_ACTION",
|
||||
RUN_ACTION_INIT: "RUN_ACTION_INIT",
|
||||
RUN_ACTION_SUCCESS: "RUN_ACTION_SUCCESS",
|
||||
UPDATE_ACTION_INIT: "UPDATE_ACTION_INIT",
|
||||
UPDATE_ACTION_SUCCESS: "UPDATE_ACTION_SUCCESS",
|
||||
DELETE_ACTION_INIT: "DELETE_ACTION_INIT",
|
||||
DELETE_ACTION_SUCCESS: "DELETE_ACTION_SUCCESS",
|
||||
UPDATE_ACTION: "UPDATE_ACTION",
|
||||
DELETE_ACTION: "DELETE_ACTION",
|
||||
FETCH_RESOURCES_INIT: "FETCH_RESOURCES_INIT",
|
||||
|
|
@ -71,6 +76,8 @@ export const ReduxActionErrorTypes: { [key: string]: string } = {
|
|||
PROPERTY_PANE_ERROR: "PROPERTY_PANE_ERROR",
|
||||
FETCH_ACTIONS_ERROR: "FETCH_ACTIONS_ERROR",
|
||||
UPDATE_WIDGET_PROPERTY_ERROR: "UPDATE_WIDGET_PROPERTY_ERROR",
|
||||
UPDATE_ACTION_ERROR: "UPDATE_ACTION_ERROR",
|
||||
DELETE_ACTION_ERROR: "DELETE_ACTION_ERROR",
|
||||
FETCH_RESOURCES_ERROR: "FETCH_RESOURCES_ERROR",
|
||||
CREATE_RESOURCE_ERROR: "CREATE_RESOURCE_ERROR",
|
||||
};
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { Icon } from "@blueprintjs/core";
|
|||
import { IconNames } from "@blueprintjs/icons";
|
||||
import { IconProps, IconWrapper } from "../constants/IconConstants";
|
||||
import { ReactComponent as DeleteIcon } from "../assets/icons/form/trash.svg";
|
||||
import { ReactComponent as AddNewIcon } from "../assets/icons/form/add-new.svg";
|
||||
|
||||
/* eslint-disable react/display-name */
|
||||
|
||||
|
|
@ -14,6 +15,11 @@ export const FormIcons: {
|
|||
<DeleteIcon />
|
||||
</IconWrapper>
|
||||
),
|
||||
ADD_NEW_ICON: (props: IconProps) => (
|
||||
<IconWrapper {...props}>
|
||||
<AddNewIcon />
|
||||
</IconWrapper>
|
||||
),
|
||||
PLUS_ICON: (props: IconProps) => (
|
||||
<IconWrapper {...props}>
|
||||
<Icon icon={IconNames.PLUS} color={props.color} iconSize={props.height} />
|
||||
|
|
|
|||
|
|
@ -7,8 +7,9 @@ import Editor from "./pages/Editor";
|
|||
import PageNotFound from "./pages/common/PageNotFound";
|
||||
import LoginPage from "./pages/common/LoginPage";
|
||||
import * as serviceWorker from "./serviceWorker";
|
||||
import { BrowserRouter, Route, Switch } from "react-router-dom";
|
||||
import { Router, Route, Switch } from "react-router-dom";
|
||||
import { createStore, applyMiddleware } from "redux";
|
||||
import history from "./utils/history";
|
||||
import appReducer from "./reducers";
|
||||
import { ThemeProvider, theme } from "./constants/DefaultTheme";
|
||||
import createSagaMiddleware from "redux-saga";
|
||||
|
|
@ -32,14 +33,14 @@ ReactDOM.render(
|
|||
<DndProvider backend={HTML5Backend}>
|
||||
<Provider store={store}>
|
||||
<ThemeProvider theme={theme}>
|
||||
<BrowserRouter>
|
||||
<Router history={history}>
|
||||
<Switch>
|
||||
<Route exact path={BASE_URL} component={App} />
|
||||
<ProtectedRoute path={BUILDER_URL} component={Editor} />
|
||||
<Route exact path={LOGIN_URL} component={LoginPage} />
|
||||
<Route component={PageNotFound} />
|
||||
</Switch>
|
||||
</BrowserRouter>
|
||||
</Router>
|
||||
</ThemeProvider>
|
||||
</Provider>
|
||||
</DndProvider>,
|
||||
|
|
|
|||
15
app/client/src/normalizers/ApiFormNormalizer.ts
Normal file
15
app/client/src/normalizers/ApiFormNormalizer.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import { RestAction } from "../api/ActionAPI";
|
||||
|
||||
export const normalizeApiFormData = (formData: any): RestAction => {
|
||||
return {
|
||||
...formData,
|
||||
actionConfiguration: {
|
||||
...formData.actionConfiguration,
|
||||
body: formData.actionConfiguration.body
|
||||
? typeof formData.actionConfiguration.body === "string"
|
||||
? JSON.parse(formData.actionConfiguration.body)
|
||||
: formData.actionConfiguration.body
|
||||
: null,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
@ -15,10 +15,11 @@ import { API_EDITOR_URL } from "../../constants/routes";
|
|||
import { API_EDITOR_FORM_NAME } from "../../constants/forms";
|
||||
import { ResourceDataState } from "../../reducers/entityReducers/resourcesReducer";
|
||||
import { fetchResources } from "../../actions/resourcesActions";
|
||||
import { FORM_INITIAL_VALUES } from "../../constants/ApiEditorConstants";
|
||||
import { normalizeApiFormData } from "../../normalizers/ApiFormNormalizer";
|
||||
|
||||
interface ReduxStateProps {
|
||||
actions: RestAction[];
|
||||
response: any;
|
||||
formData: any;
|
||||
resources: ResourceDataState;
|
||||
}
|
||||
|
|
@ -55,6 +56,9 @@ class ApiEditor extends React.Component<Props> {
|
|||
|
||||
componentDidUpdate(prevProps: Readonly<Props>): void {
|
||||
const currentId = this.props.match.params.id;
|
||||
if (!currentId && prevProps.match.params.id) {
|
||||
this.props.initialize(API_EDITOR_FORM_NAME, FORM_INITIAL_VALUES);
|
||||
}
|
||||
if (currentId && currentId !== prevProps.match.params.id) {
|
||||
const data = this.props.actions.filter(
|
||||
action => action.id === currentId,
|
||||
|
|
@ -65,17 +69,7 @@ class ApiEditor extends React.Component<Props> {
|
|||
|
||||
handleSubmit = (values: RestAction) => {
|
||||
const { formData } = this.props;
|
||||
const data: RestAction = {
|
||||
...formData,
|
||||
actionConfiguration: {
|
||||
...formData.actionConfiguration,
|
||||
body: formData.actionConfiguration.body
|
||||
? typeof formData.actionConfiguration.body === "string"
|
||||
? JSON.parse(formData.actionConfiguration.body)
|
||||
: formData.actionConfiguration.body
|
||||
: null,
|
||||
},
|
||||
};
|
||||
const data = normalizeApiFormData(formData);
|
||||
if (data.id) {
|
||||
this.props.updateAction(data);
|
||||
} else {
|
||||
|
|
@ -100,7 +94,7 @@ class ApiEditor extends React.Component<Props> {
|
|||
onSaveClick={this.handleSaveClick}
|
||||
onDeleteClick={this.handleDeleteClick}
|
||||
onRunClick={this.handleRunClick}
|
||||
response={this.props.response}
|
||||
isEdit={!!this.props.match.params.id}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -108,7 +102,6 @@ class ApiEditor extends React.Component<Props> {
|
|||
|
||||
const mapStateToProps = (state: AppState): ReduxStateProps => ({
|
||||
actions: state.entities.actions.data,
|
||||
response: state.entities.actions.response,
|
||||
formData: getFormValues(API_EDITOR_FORM_NAME)(state),
|
||||
resources: state.entities.resources,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -5,15 +5,29 @@ import styled from "styled-components";
|
|||
import { AppState } from "../../reducers";
|
||||
import { fetchActions } from "../../actions/actionActions";
|
||||
import { ActionDataState } from "../../reducers/entityReducers/actionsReducer";
|
||||
import { API_EDITOR_ID_URL } from "../../constants/routes";
|
||||
import { API_EDITOR_ID_URL, API_EDITOR_URL } from "../../constants/routes";
|
||||
import { BaseButton } from "../../components/canvas/Button";
|
||||
import { FormIcons } from "../../icons/FormIcons";
|
||||
|
||||
const ApiSidebarWrapper = styled.div`
|
||||
display: flex;
|
||||
height: 100%;
|
||||
flex-direction: column;
|
||||
`;
|
||||
const ApiItemsWrapper = styled.div`
|
||||
flex: 1;
|
||||
`;
|
||||
|
||||
const ApiItem = styled.div<{ isSelected: boolean }>`
|
||||
height: 32px;
|
||||
width: 100%;
|
||||
padding: 5px 10px;
|
||||
padding: 5px 12px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 2px;
|
||||
background-color: ${props =>
|
||||
props.isSelected ? props.theme.colors.paneCard : props.theme.colors.paneBG}
|
||||
:hover {
|
||||
|
|
@ -23,6 +37,7 @@ const ApiItem = styled.div<{ isSelected: boolean }>`
|
|||
|
||||
const HTTPMethod = styled.span<{ method: string | undefined }>`
|
||||
flex: 1;
|
||||
font-size: 12px;
|
||||
color: ${props => {
|
||||
switch (props.method) {
|
||||
case "GET":
|
||||
|
|
@ -41,6 +56,29 @@ const HTTPMethod = styled.span<{ method: string | undefined }>`
|
|||
|
||||
const ActionName = styled.span`
|
||||
flex: 3;
|
||||
padding: 0 2px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
`;
|
||||
|
||||
const CreateNewButton = styled(BaseButton)`
|
||||
&& {
|
||||
border: none;
|
||||
color: ${props => props.theme.colors.textOnDarkBG};
|
||||
height: 32px;
|
||||
&:hover {
|
||||
color: ${props => props.theme.colors.paneBG};
|
||||
svg {
|
||||
path {
|
||||
fill: ${props => props.theme.colors.paneBG};
|
||||
}
|
||||
}
|
||||
}
|
||||
svg {
|
||||
height: 12px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
interface ReduxStateProps {
|
||||
|
|
@ -58,28 +96,42 @@ type Props = ReduxStateProps &
|
|||
|
||||
class ApiSidebar extends React.Component<Props> {
|
||||
componentDidMount(): void {
|
||||
this.props.fetchActions();
|
||||
if (!this.props.actions.data.length) {
|
||||
this.props.fetchActions();
|
||||
}
|
||||
}
|
||||
|
||||
handleCreateNew = () => {
|
||||
const { history } = this.props;
|
||||
history.push(API_EDITOR_URL);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { actions, history, match } = this.props;
|
||||
const activeActionId = match.params.id;
|
||||
return (
|
||||
<React.Fragment>
|
||||
{actions.loading && "Loading..."}
|
||||
{actions.data.map(action => (
|
||||
<ApiItem
|
||||
key={action.id}
|
||||
onClick={() => history.push(API_EDITOR_ID_URL(action.id))}
|
||||
isSelected={activeActionId === action.id}
|
||||
>
|
||||
<HTTPMethod method={action.actionConfiguration.httpMethod}>
|
||||
{action.actionConfiguration.httpMethod}
|
||||
</HTTPMethod>
|
||||
<ActionName>{action.name}</ActionName>
|
||||
</ApiItem>
|
||||
))}
|
||||
</React.Fragment>
|
||||
<ApiSidebarWrapper>
|
||||
<ApiItemsWrapper>
|
||||
{actions.data.map(action => (
|
||||
<ApiItem
|
||||
key={action.id}
|
||||
onClick={() => history.push(API_EDITOR_ID_URL(action.id))}
|
||||
isSelected={activeActionId === action.id}
|
||||
className={actions.loading ? "bp3-skeleton" : ""}
|
||||
>
|
||||
<HTTPMethod method={action.actionConfiguration.httpMethod}>
|
||||
{action.actionConfiguration.httpMethod}
|
||||
</HTTPMethod>
|
||||
<ActionName>{action.name}</ActionName>
|
||||
</ApiItem>
|
||||
))}
|
||||
</ApiItemsWrapper>
|
||||
<CreateNewButton
|
||||
text="Create new API"
|
||||
icon={FormIcons.ADD_NEW_ICON()}
|
||||
onClick={this.handleCreateNew}
|
||||
/>
|
||||
</ApiSidebarWrapper>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,23 +11,25 @@ import { RestAction } from "../../api/ActionAPI";
|
|||
const initialState: ActionDataState = {
|
||||
list: {},
|
||||
data: [],
|
||||
response: {
|
||||
body: null,
|
||||
headers: null,
|
||||
statusCode: "",
|
||||
},
|
||||
responses: {},
|
||||
loading: false,
|
||||
};
|
||||
|
||||
export interface ActionApiResponse {
|
||||
body: JSON;
|
||||
headers: any;
|
||||
statusCode: string;
|
||||
timeTaken: number;
|
||||
size: number;
|
||||
}
|
||||
|
||||
export interface ActionDataState {
|
||||
list: {
|
||||
[name: string]: PageAction;
|
||||
};
|
||||
data: RestAction[];
|
||||
response: {
|
||||
body: any;
|
||||
headers: any;
|
||||
statusCode: string;
|
||||
responses: {
|
||||
[id: string]: ActionApiResponse;
|
||||
};
|
||||
loading: boolean;
|
||||
}
|
||||
|
|
@ -42,24 +44,51 @@ const actionsReducer = createReducer(initialState, {
|
|||
});
|
||||
return { ...state, list: { ...actionMap } };
|
||||
},
|
||||
[ReduxActionTypes.FETCH_ACTIONS_INIT]: (state: ActionDataState) => {
|
||||
return { ...state, loading: true };
|
||||
},
|
||||
[ReduxActionTypes.FETCH_ACTIONS_INIT]: (state: ActionDataState) => ({
|
||||
...state,
|
||||
loading: true,
|
||||
}),
|
||||
[ReduxActionTypes.FETCH_ACTIONS_SUCCESS]: (
|
||||
state: ActionDataState,
|
||||
action: ReduxAction<RestAction[]>,
|
||||
) => {
|
||||
return { ...state, data: action.payload, loading: false };
|
||||
},
|
||||
[ReduxActionErrorTypes.FETCH_ACTIONS_ERROR]: (state: ActionDataState) => {
|
||||
return { ...state, data: [], loading: false };
|
||||
},
|
||||
) => ({
|
||||
...state,
|
||||
data: action.payload,
|
||||
loading: false,
|
||||
}),
|
||||
[ReduxActionErrorTypes.FETCH_ACTIONS_ERROR]: (state: ActionDataState) => ({
|
||||
...state,
|
||||
data: [],
|
||||
loading: false,
|
||||
}),
|
||||
[ReduxActionTypes.RUN_ACTION_SUCCESS]: (
|
||||
state: ActionDataState,
|
||||
action: ReduxAction<any>,
|
||||
) => {
|
||||
return { ...state, response: action.payload };
|
||||
},
|
||||
action: ReduxAction<{ [id: string]: ActionApiResponse }>,
|
||||
) => ({ ...state, responses: { ...state.responses, ...action.payload } }),
|
||||
[ReduxActionTypes.CREATE_ACTION_SUCCESS]: (
|
||||
state: ActionDataState,
|
||||
action: ReduxAction<RestAction>,
|
||||
) => ({
|
||||
...state,
|
||||
data: state.data.concat([action.payload]),
|
||||
}),
|
||||
[ReduxActionTypes.UPDATE_ACTION_SUCCESS]: (
|
||||
state: ActionDataState,
|
||||
action: ReduxAction<{ data: RestAction }>,
|
||||
) => ({
|
||||
...state,
|
||||
data: state.data.map(d => {
|
||||
if (d.id === action.payload.data.id) return action.payload.data;
|
||||
return d;
|
||||
}),
|
||||
}),
|
||||
[ReduxActionTypes.DELETE_ACTION_SUCCESS]: (
|
||||
state: ActionDataState,
|
||||
action: ReduxAction<{ id: string }>,
|
||||
) => ({
|
||||
...state,
|
||||
data: state.data.filter(d => d.id !== action.payload.id),
|
||||
}),
|
||||
});
|
||||
|
||||
export default actionsReducer;
|
||||
|
|
|
|||
|
|
@ -4,7 +4,14 @@ import {
|
|||
ReduxActionTypes,
|
||||
} from "../constants/ReduxActionConstants";
|
||||
import { Intent } from "@blueprintjs/core";
|
||||
import { all, call, select, put, takeEvery } from "redux-saga/effects";
|
||||
import {
|
||||
all,
|
||||
call,
|
||||
select,
|
||||
put,
|
||||
takeEvery,
|
||||
takeLatest,
|
||||
} from "redux-saga/effects";
|
||||
import { initialize } from "redux-form";
|
||||
import { ActionPayload, PageAction } from "../constants/ActionConstants";
|
||||
import ActionAPI, {
|
||||
|
|
@ -18,8 +25,14 @@ import _ from "lodash";
|
|||
import { mapToPropList } from "../utils/AppsmithUtils";
|
||||
import AppToaster from "../components/editor/ToastComponent";
|
||||
import { GenericApiResponse } from "../api/ApiResponses";
|
||||
import { fetchActions } from "../actions/actionActions";
|
||||
import { API_EDITOR_FORM_NAME } from "../constants/forms";
|
||||
import {
|
||||
createActionSuccess,
|
||||
deleteActionSuccess,
|
||||
updateActionSuccess,
|
||||
} from "../actions/actionActions";
|
||||
import { API_EDITOR_ID_URL, API_EDITOR_URL } from "../constants/routes";
|
||||
import history from "../utils/history";
|
||||
|
||||
const getDataTree = (state: AppState) => {
|
||||
return state.entities;
|
||||
|
|
@ -82,7 +95,8 @@ export function* createActionSaga(actionPayload: ReduxAction<RestAction>) {
|
|||
message: `${actionPayload.payload.name} Action created`,
|
||||
intent: Intent.SUCCESS,
|
||||
});
|
||||
yield put(fetchActions());
|
||||
yield put(createActionSuccess(response.data));
|
||||
history.push(API_EDITOR_ID_URL(response.data.id));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -116,7 +130,7 @@ export function* runActionSaga(actionPayload: ReduxAction<{ id: string }>) {
|
|||
const response: any = yield ActionAPI.executeAction({ actionId: id });
|
||||
yield put({
|
||||
type: ReduxActionTypes.RUN_ACTION_SUCCESS,
|
||||
payload: response,
|
||||
payload: { [id]: response },
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -135,7 +149,7 @@ export function* updateActionSaga(
|
|||
message: `${actionPayload.payload.data.name} Action updated`,
|
||||
intent: Intent.SUCCESS,
|
||||
});
|
||||
yield put(fetchActions());
|
||||
yield put(updateActionSuccess({ data: response.data }));
|
||||
} else {
|
||||
AppToaster.show({
|
||||
message: "Error occurred when updating action",
|
||||
|
|
@ -154,7 +168,8 @@ export function* deleteActionSaga(actionPayload: ReduxAction<{ id: string }>) {
|
|||
message: `${response.data.name} Action deleted`,
|
||||
intent: Intent.SUCCESS,
|
||||
});
|
||||
yield put(fetchActions());
|
||||
yield put(deleteActionSuccess({ id }));
|
||||
history.push(API_EDITOR_URL);
|
||||
} else {
|
||||
AppToaster.show({
|
||||
message: "Error occurred when deleting action",
|
||||
|
|
@ -166,11 +181,11 @@ export function* deleteActionSaga(actionPayload: ReduxAction<{ id: string }>) {
|
|||
export function* watchActionSagas() {
|
||||
yield all([
|
||||
takeEvery(ReduxActionTypes.FETCH_ACTIONS_INIT, fetchActionsSaga),
|
||||
takeEvery(ReduxActionTypes.EXECUTE_ACTION, executeActionSaga),
|
||||
takeEvery(ReduxActionTypes.CREATE_ACTION, createActionSaga),
|
||||
takeLatest(ReduxActionTypes.EXECUTE_ACTION, executeActionSaga),
|
||||
takeLatest(ReduxActionTypes.CREATE_ACTION_INIT, createActionSaga),
|
||||
takeEvery(ReduxActionTypes.FETCH_ACTION, fetchActionSaga),
|
||||
takeEvery(ReduxActionTypes.RUN_ACTION, runActionSaga),
|
||||
takeEvery(ReduxActionTypes.UPDATE_ACTION, updateActionSaga),
|
||||
takeEvery(ReduxActionTypes.DELETE_ACTION, deleteActionSaga),
|
||||
takeLatest(ReduxActionTypes.RUN_ACTION_INIT, runActionSaga),
|
||||
takeLatest(ReduxActionTypes.UPDATE_ACTION_INIT, updateActionSaga),
|
||||
takeLatest(ReduxActionTypes.DELETE_ACTION_INIT, deleteActionSaga),
|
||||
]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import ResourcesApi, {
|
|||
CreateResourceConfig,
|
||||
Resource,
|
||||
} from "../api/ResourcesApi";
|
||||
import { API_EDITOR_FORM_NAME } from "../constants/forms";
|
||||
|
||||
function* fetchResourcesSaga() {
|
||||
const response: GenericApiResponse<
|
||||
|
|
@ -37,7 +38,7 @@ function* createResourceSaga(actionPayload: ReduxAction<CreateResourceConfig>) {
|
|||
type: ReduxActionTypes.CREATE_RESOURCE_SUCCESS,
|
||||
payload: response.data,
|
||||
});
|
||||
yield put(change("ApiEditorForm", "resourceId", response.data.id));
|
||||
yield put(change(API_EDITOR_FORM_NAME, "resourceId", response.data.id));
|
||||
} else {
|
||||
yield put({
|
||||
type: ReduxActionTypes.CREATE_RESOURCE_ERROR,
|
||||
|
|
|
|||
2
app/client/src/utils/history.ts
Normal file
2
app/client/src/utils/history.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
const createHistory = require("history").createBrowserHistory;
|
||||
export default createHistory();
|
||||
|
|
@ -5587,7 +5587,7 @@ hex-color-regex@^1.1.0:
|
|||
resolved "https://registry.yarnpkg.com/hex-color-regex/-/hex-color-regex-1.1.0.tgz#4c06fccb4602fe2602b3c93df82d7e7dbf1a8a8e"
|
||||
integrity sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ==
|
||||
|
||||
history@^4.9.0:
|
||||
history@^4.10.1, history@^4.9.0:
|
||||
version "4.10.1"
|
||||
resolved "https://registry.yarnpkg.com/history/-/history-4.10.1.tgz#33371a65e3a83b267434e2b3f3b1b4c58aad4cf3"
|
||||
integrity sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user