Fixes for the API Editor #109 #112 #111 #116

This commit is contained in:
Hetu Nandu 2019-10-25 05:35:20 +00:00
parent 27f4f3b728
commit a0b536eced
27 changed files with 410 additions and 178 deletions

View File

@ -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",

View File

@ -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,
};

View 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

View File

@ -38,7 +38,6 @@ class CreatableDropdown extends React.Component<DropdownProps> {
onCreateOption={onCreateOption}
{...input}
onChange={value => input.onChange(value)}
onBlur={() => input.onBlur(input.value)}
/>
);
}

View File

@ -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)}
/>
);
};

View File

@ -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>
);
};

View File

@ -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};

View 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));

View File

@ -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)}

View File

@ -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>
);

View File

@ -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};

View File

@ -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}
/>

View File

@ -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}

View File

@ -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>
);

View File

@ -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: "",
},
],
},
};

View File

@ -110,7 +110,7 @@ export const theme: Theme = {
color: Colors.GEYSER_LIGHT,
},
],
sidebarWidth: "300px",
sidebarWidth: "350px",
headerHeight: "50px",
};

View File

@ -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",
};

View File

@ -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} />

View File

@ -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>,

View 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,
},
};
};

View File

@ -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,
});

View File

@ -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>
);
}
}

View File

@ -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;

View File

@ -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),
]);
}

View File

@ -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,

View File

@ -0,0 +1,2 @@
const createHistory = require("history").createBrowserHistory;
export default createHistory();

View File

@ -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==