diff --git a/README.md b/README.md index f2f6e46..267a5ae 100644 --- a/README.md +++ b/README.md @@ -34,22 +34,25 @@ A [Postman](https://www.postman.com/)-like API client for [protobuf](https://dev 4. **As of version 0.3.0, you can also import / export collections as JSON** - When importing a collection, all the proto definitions / path to .proto files are also imported. Hence, it's recommended to fix the paths to keep the proto definitions up-to-date. +- Note that it's not compatible with Postman collections. -5. **That's it for the current version. Enjoy and leave a star if you like it !** +5. **As of version 0.4.0, you can also reorder requests / set different expected messages for success(2XX) and failures(others)** + +6. **That's it for the current version. Enjoy and leave a star if you like it !** ## Installation ### Mac -[Protoman-0.3.4.dmg](https://github.com/spluxx/Protoman/releases/download/v0.3.4/Protoman-0.3.4.dmg) +[Protoman-0.4.0.dmg](https://github.com/spluxx/Protoman/releases/download/v0.4.0/Protoman-0.4.0.dmg) ### Windows -[Protoman Setup 0.3.4.exe](https://github.com/spluxx/Protoman/releases/download/v0.3.4/Protoman.Setup.0.3.4.exe) - Unlike mac, I don't currently own a license to sign the app. So it might give you some security warnings! +[Protoman Setup 0.4.0.exe](https://github.com/spluxx/Protoman/releases/download/v0.4.0/Protoman.Setup.0.4.0.exe) - Unlike mac, I don't currently own a license to sign the app. So it might give you some security warnings! ### Linux -[Protoman-0.3.4.AppImage](https://github.com/spluxx/Protoman/releases/download/v0.3.4/Protoman-0.3.4.AppImage) +[Protoman-0.4.0.AppImage](https://github.com/spluxx/Protoman/releases/download/v0.4.0/Protoman-0.4.0.AppImage) As a fallback, you can clone the repo and run npm install && npm run build to build, and npm run start to launch the app. Or, you can actually find configurations on [electron builder](https://www.electron.build/) to get the right distribution version yourself! diff --git a/package-lock.json b/package-lock.json index d73254e..86b6cf7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "Protoman", - "version": "0.3.4", + "version": "0.4.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -1285,6 +1285,15 @@ "csstype": "^2.2.0" } }, + "@types/react-beautiful-dnd": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@types/react-beautiful-dnd/-/react-beautiful-dnd-13.0.0.tgz", + "integrity": "sha512-by80tJ8aTTDXT256Gl+RfLRtFjYbUWOnZuEigJgNsJrSEGxvFe5eY6k3g4VIvf0M/6+xoLgfYWoWonlOo6Wqdg==", + "dev": true, + "requires": { + "@types/react": "*" + } + }, "@types/react-dom": { "version": "16.9.5", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-16.9.5.tgz", @@ -3518,6 +3527,14 @@ "component-classes": "^1.2.5" } }, + "css-box-model": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/css-box-model/-/css-box-model-1.2.1.tgz", + "integrity": "sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==", + "requires": { + "tiny-invariant": "^1.0.6" + } + }, "css-color-keywords": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", @@ -9152,6 +9169,11 @@ "p-is-promise": "^2.0.0" } }, + "memoize-one": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.1.1.tgz", + "integrity": "sha512-HKeeBpWvqiVJD57ZUAsJNm71eHTykffzcLZVYWiVfQeI1rJtuEaS7hQiEpWfVVk18donPwJEcFKIkCmPJNOhHA==" + }, "memory-fs": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz", @@ -10450,6 +10472,11 @@ "performance-now": "^2.1.0" } }, + "raf-schd": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.2.tgz", + "integrity": "sha512-VhlMZmGy6A6hrkJWHLNTGl5gtgMUm+xfGza6wbwnE914yeQ5Ybm18vgM734RZhMgfw4tacUrWseGZlpUrrakEQ==" + }, "randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -10882,6 +10909,20 @@ "prop-types": "^15.6.2" } }, + "react-beautiful-dnd": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/react-beautiful-dnd/-/react-beautiful-dnd-13.0.0.tgz", + "integrity": "sha512-87It8sN0ineoC3nBW0SbQuTFXM6bUqM62uJGY4BtTf0yzPl8/3+bHMWkgIe0Z6m8e+gJgjWxefGRVfpE3VcdEg==", + "requires": { + "@babel/runtime": "^7.8.4", + "css-box-model": "^1.2.0", + "memoize-one": "^5.1.1", + "raf-schd": "^4.0.2", + "react-redux": "^7.1.1", + "redux": "^4.0.4", + "use-memo-one": "^1.1.1" + } + }, "react-dom": { "version": "16.12.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.12.0.tgz", @@ -12432,6 +12473,11 @@ "setimmediate": "^1.0.4" } }, + "tiny-invariant": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.1.0.tgz", + "integrity": "sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw==" + }, "tinycolor2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.4.1.tgz", @@ -13053,6 +13099,11 @@ "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", "dev": true }, + "use-memo-one": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/use-memo-one/-/use-memo-one-1.1.1.tgz", + "integrity": "sha512-oFfsyun+bP7RX8X2AskHNTxu+R3QdE/RC5IefMbqptmACAA/gfol1KDD5KRzPsGMa62sWxGZw+Ui43u6x4ddoQ==" + }, "utf8-byte-length": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.4.tgz", diff --git a/package.json b/package.json index 56cb2e7..0f3b99a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "Protoman", - "version": "0.3.4", + "version": "0.4.0", "description": "Basic Postman clone with protobuf functionalities", "author": "Inchan Hwang, Louis Lee", "license": "MIT", @@ -67,6 +67,7 @@ "node-fetch": "^2.6.0", "protobufjs": "^6.8.8", "react": "^16.12.0", + "react-beautiful-dnd": "^13.0.0", "react-dom": "^16.12.0", "react-redux": "^7.1.3", "react-tooltip": "^4.1.0", @@ -80,6 +81,7 @@ "@types/jest": "^25.1.4", "@types/node-fetch": "^2.5.5", "@types/react": "^16.9.19", + "@types/react-beautiful-dnd": "^13.0.0", "@types/react-dom": "^16.9.5", "@types/react-redux": "^7.1.7", "@types/react-tooltip": "^3.11.0", diff --git a/src/core/http_client/client.ts b/src/core/http_client/client.ts index b245842..a154c66 100644 --- a/src/core/http_client/client.ts +++ b/src/core/http_client/client.ts @@ -32,7 +32,7 @@ async function translateResponse( const responseHeaders = unconvertHeaders(response.headers); const saidContentType = responseHeaders.find(([name]) => name === 'content-type')?.[1]; - const { expectedProtobufMsg } = request; + const { expectedProtobufMsg, expectedProtobufMsgOnError } = request; let responseBodyType: ResponseBodyType = 'unknown'; let responseBodyValue: ResponseBodyValue = undefined; @@ -50,7 +50,12 @@ async function translateResponse( responseBodyType = 'html'; responseBodyValue = toStr(buf); } else if (expectedProtobufMsg) { - const res = await deserializeProtobuf(buf, expectedProtobufMsg, protoCtx); + let msgToUse = expectedProtobufMsg; + if (!response.ok && expectedProtobufMsgOnError) { + msgToUse = expectedProtobufMsgOnError; + } + + const res = await deserializeProtobuf(buf, msgToUse, protoCtx); switch (res.tag) { case 'invalid': if (res.value) { diff --git a/src/core/http_client/request.ts b/src/core/http_client/request.ts index 41a02ba..c05ee11 100644 --- a/src/core/http_client/request.ts +++ b/src/core/http_client/request.ts @@ -8,4 +8,5 @@ export interface RequestDescriptor { readonly headers: ReadonlyArray<[string, string]>; readonly body: Uint8Array | undefined; readonly expectedProtobufMsg: string | undefined; + readonly expectedProtobufMsgOnError: string | undefined; } diff --git a/src/main/notification.ts b/src/main/notification.ts index ef02580..de325ec 100644 --- a/src/main/notification.ts +++ b/src/main/notification.ts @@ -6,6 +6,8 @@ const ICON_URL = 'https://raw.githubusercontent.com/spluxx/Protoman/master/asset // couldn't get https://github.com/pd4d10/electron-update-notification to work... :/ export async function checkUpdateAndNotify(window: BrowserWindow): Promise { + if (process.env.NODE_ENV === 'development') return; + try { const res = await fetch(RELEASE_URL); const json = await res.json(); diff --git a/src/renderer/components/Collection/CollectionActions.ts b/src/renderer/components/Collection/CollectionActions.ts index e3f9f25..9477c2a 100644 --- a/src/renderer/components/Collection/CollectionActions.ts +++ b/src/renderer/components/Collection/CollectionActions.ts @@ -64,6 +64,15 @@ type CloseFM = { const CLOSE_FM = 'CLOSE_FM'; +type ReorderFlow = { + type: 'REORDER_FLOW'; + collectionName: string; + src: number; + dst: number; +}; + +const REORDER_FLOW = 'REORDER_FLOW'; + export const CollectionActionTypes = [ CREATE_COLLECTION, CHANGE_COLLECTION_NAME, @@ -74,7 +83,9 @@ export const CollectionActionTypes = [ DELETE_FLOW, OPEN_FM, CLOSE_FM, + REORDER_FLOW, ]; + export type CollectionAction = | CreateCollection | ChangeCollectionName @@ -84,7 +95,8 @@ export type CollectionAction = | SelectFlow | DeleteFlow | OpenFM - | CloseFM; + | CloseFM + | ReorderFlow; export function createCollection(collectionName: string): CreateCollection { return { @@ -151,3 +163,12 @@ export function closeFM(): CloseFM { type: CLOSE_FM, }; } + +export function reorderFlow(collectionName: string, src: number, dst: number): ReorderFlow { + return { + type: REORDER_FLOW, + collectionName, + src, + dst, + }; +} diff --git a/src/renderer/components/Collection/CollectionReducer.ts b/src/renderer/components/Collection/CollectionReducer.ts index 46d0f68..a8fa1d7 100644 --- a/src/renderer/components/Collection/CollectionReducer.ts +++ b/src/renderer/components/Collection/CollectionReducer.ts @@ -80,6 +80,14 @@ export default function CollectionReducer(s: AppState, action: AnyAction): AppSt return produce(s, draft => { draft.fmOpenCollection = undefined; }); + case 'REORDER_FLOW': + return produce(s, draft => { + const flows = getByKey(draft.collections, a.collectionName)?.flows; + if (!flows) return draft; + + const [rm] = flows.splice(a.src, 1); + flows.splice(a.dst, 0, rm); + }); default: return s; } diff --git a/src/renderer/components/Collection/CollectionSider.tsx b/src/renderer/components/Collection/CollectionSider.tsx index 46341b5..90734cd 100644 --- a/src/renderer/components/Collection/CollectionSider.tsx +++ b/src/renderer/components/Collection/CollectionSider.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { Layout, Collapse, Modal, Button, Row } from 'antd'; +import { Layout, Collapse, Modal, Button } from 'antd'; import styled from 'styled-components'; import CollectionCell from './CollectionCell'; import { useSelector, useDispatch } from 'react-redux'; @@ -94,7 +94,7 @@ const CollectionSider: React.FunctionComponent<{}> = ({}) => { {collections.map(([name]) => { const header = ; return ( - + ); diff --git a/src/renderer/components/Collection/FlowList.tsx b/src/renderer/components/Collection/FlowList.tsx index b92c015..e77c05a 100644 --- a/src/renderer/components/Collection/FlowList.tsx +++ b/src/renderer/components/Collection/FlowList.tsx @@ -5,16 +5,17 @@ import styled from 'styled-components'; import { prevent, getByKey } from '../../utils/utils'; import { useSelector, useDispatch } from 'react-redux'; import { AppState } from '../../models/AppState'; -import { selectFlow, deleteFlow } from './CollectionActions'; +import { selectFlow, deleteFlow, reorderFlow } from './CollectionActions'; +import { DragDropContext, DropResult, Draggable, Droppable } from 'react-beautiful-dnd'; const ClickableItem = styled(List.Item)` display: flex; justify-content: space-between; - padding: 8px; &:hover { cursor: pointer; background-color: #f7fcff; } + padding: 0; `; type Props = { @@ -42,29 +43,51 @@ const FlowList: React.FunctionComponent = ({ collectionName }) => { } } + function handleDragEnd(result: DropResult): void { + console.log(result); + if (!result.destination || result.source.droppableId != result.destination.droppableId) return; + + const src = result.source.index; + const dst = result.destination.index; + + dispatch(reorderFlow(collectionName, src, dst)); + } + return ( - ( - - )} - /> + + + {(provided): React.ReactElement => ( +
+ name} + renderItem={(flowName, idx): React.ReactNode => ( + + )} + /> + {provided.placeholder} +
+ )} +
+
); }; type CellProps = { flowName: string; emphasize: boolean; + idx: number; handleSelection: (name: string) => void; handleDelete: (name: string) => void; }; -const FlowCell: React.FC = ({ flowName, emphasize, handleSelection, handleDelete }) => { +const FlowCell: React.FC = ({ flowName, emphasize, handleSelection, handleDelete, idx }) => { const [menuVisible, setMenuVisible] = React.useState(false); function showMenu(): void { setMenuVisible(true); @@ -97,12 +120,34 @@ const FlowCell: React.FC = ({ flowName, emphasize, handleSelection, h onVisibleChange={setMenuVisible} > handleSelection(flowName)} onContextMenu={prevent(showMenu)}> - - {flowName} - + + {(provided): React.ReactElement => { + const style: React.CSSProperties = { + width: '100%', + height: '100%', + padding: 8, + boxSizing: 'border-box', + }; + + const { style: draggableStyle, ...draggableRest } = provided.draggableProps; + + return ( +
+ + {flowName} + +
+ ); + }} +
); diff --git a/src/renderer/components/Flow/request/ExpectedBodyInput/ExpectedBodyInput.tsx b/src/renderer/components/Flow/request/ExpectedBodyInput/ExpectedBodyInput.tsx index db72e71..03cf50f 100644 --- a/src/renderer/components/Flow/request/ExpectedBodyInput/ExpectedBodyInput.tsx +++ b/src/renderer/components/Flow/request/ExpectedBodyInput/ExpectedBodyInput.tsx @@ -1,24 +1,35 @@ import React from 'react'; import { Select } from 'antd'; import { useDispatch } from 'react-redux'; -import { selectResponseMessageName } from './ExpectedBodyInputActions'; +import { selectResponseMessageName, selectResponseMessageOnErrorName } from './ExpectedBodyInputActions'; import { MESSAGE_NAME_WIDTH } from '../BodyInput/BodyInput'; type Props = { messageNames: ReadonlyArray; expectedProtobufMsg: string | undefined; + expectedProtobufMsgOnError: string | undefined; }; -const ExpectedBodyInput: React.FunctionComponent = ({ messageNames, expectedProtobufMsg }) => { +const LABEL_STYLE = { display: 'inline-block', width: 100 }; + +const ExpectedBodyInput: React.FunctionComponent = ({ + messageNames, + expectedProtobufMsg, + expectedProtobufMsgOnError, +}) => { const dispatch = useDispatch(); function onSelectResponseMsg(msgName: string): void { dispatch(selectResponseMessageName(msgName)); } + function onSelectResponseMsgOnError(msgName: string): void { + dispatch(selectResponseMessageOnErrorName(msgName)); + } + return (
- Expected protobuf message: + On [200, 300): + +
+ + On [300, ∞): +
); }; diff --git a/src/renderer/components/Flow/request/ExpectedBodyInput/ExpectedBodyInputActions.ts b/src/renderer/components/Flow/request/ExpectedBodyInput/ExpectedBodyInputActions.ts index 9a67ea4..abf145e 100644 --- a/src/renderer/components/Flow/request/ExpectedBodyInput/ExpectedBodyInputActions.ts +++ b/src/renderer/components/Flow/request/ExpectedBodyInput/ExpectedBodyInputActions.ts @@ -3,10 +3,16 @@ type SelectResponseMessageName = { name: string; }; +type SelectResponseMessageOnErrorName = { + type: 'SELECT_RESPONSE_MESSAGE_ON_ERROR_NAME'; + name: string; +}; + const SELECT_RESPONSE_MESSAGE_NAME = 'SELECT_RESPONSE_MESSAGE_NAME'; +const SELECT_RESPONSE_MESSAGE_ON_ERROR_NAME = 'SELECT_RESPONSE_MESSAGE_ON_ERROR_NAME'; -export const ExpectedBodyInputActionTypes = [SELECT_RESPONSE_MESSAGE_NAME]; -export type ExpectedBodyInputActions = SelectResponseMessageName; +export const ExpectedBodyInputActionTypes = [SELECT_RESPONSE_MESSAGE_NAME, SELECT_RESPONSE_MESSAGE_ON_ERROR_NAME]; +export type ExpectedBodyInputActions = SelectResponseMessageName | SelectResponseMessageOnErrorName; export function selectResponseMessageName(name: string): SelectResponseMessageName { return { @@ -14,3 +20,10 @@ export function selectResponseMessageName(name: string): SelectResponseMessageNa name, }; } + +export function selectResponseMessageOnErrorName(name: string): SelectResponseMessageOnErrorName { + return { + type: SELECT_RESPONSE_MESSAGE_ON_ERROR_NAME, + name, + }; +} diff --git a/src/renderer/components/Flow/request/ExpectedBodyInput/ExpectedBodyInputReducer.ts b/src/renderer/components/Flow/request/ExpectedBodyInput/ExpectedBodyInputReducer.ts index 4be311b..55ca248 100644 --- a/src/renderer/components/Flow/request/ExpectedBodyInput/ExpectedBodyInputReducer.ts +++ b/src/renderer/components/Flow/request/ExpectedBodyInput/ExpectedBodyInputReducer.ts @@ -16,6 +16,17 @@ export default function ExpectedBodyInputReducer(s: AppState, action: AnyAction) const flow = getByKey(collection.flows, draft.currentFlow); if (!flow) return s; flow.requestBuilder.expectedProtobufMsg = a.name; + if (!flow.requestBuilder.expectedProtobufMsgOnError) { + flow.requestBuilder.expectedProtobufMsgOnError = a.name; + } + }); + case 'SELECT_RESPONSE_MESSAGE_ON_ERROR_NAME': + return produce(s, draft => { + const collection = getByKey(draft.collections, draft.currentCollection); + if (!collection) return s; + const flow = getByKey(collection.flows, draft.currentFlow); + if (!flow) return s; + flow.requestBuilder.expectedProtobufMsgOnError = a.name; }); default: return s; diff --git a/src/renderer/components/Flow/request/RequestBuilderView/RequestBuilderView.tsx b/src/renderer/components/Flow/request/RequestBuilderView/RequestBuilderView.tsx index 90cb458..9c90b6d 100644 --- a/src/renderer/components/Flow/request/RequestBuilderView/RequestBuilderView.tsx +++ b/src/renderer/components/Flow/request/RequestBuilderView/RequestBuilderView.tsx @@ -37,7 +37,7 @@ type Props = { }; const RequestBuilderView: React.FunctionComponent = ({ requestBuilder, protoCtx, messageNames, onSend }) => { - const { method, url, headers, bodyType, bodies, expectedProtobufMsg } = requestBuilder; + const { method, url, headers, bodyType, bodies, expectedProtobufMsg, expectedProtobufMsgOnError } = requestBuilder; return ( @@ -53,7 +53,11 @@ const RequestBuilderView: React.FunctionComponent = ({ requestBuilder, pr - + diff --git a/src/renderer/models/request_builder.ts b/src/renderer/models/request_builder.ts index 4ec997c..c5ca9ce 100644 --- a/src/renderer/models/request_builder.ts +++ b/src/renderer/models/request_builder.ts @@ -14,7 +14,8 @@ export interface RequestBuilder { readonly headers: ReadonlyArray<[string, string]>; readonly bodyType: BodyType; readonly bodies: RequestBody; - readonly expectedProtobufMsg: string | undefined; + readonly expectedProtobufMsg?: string; + readonly expectedProtobufMsgOnError?: string; } export interface RequestBody { @@ -27,7 +28,7 @@ export async function toRequestDescriptor( env: Env, ctx: ProtoCtx, ): Promise { - const { url, method, headers, bodyType, bodies, expectedProtobufMsg } = builder; + const { url, method, headers, bodyType, bodies, expectedProtobufMsg, expectedProtobufMsgOnError } = builder; const varMap = toVarMap(env); let body; @@ -44,5 +45,6 @@ export async function toRequestDescriptor( headers: headers.map(([k, v]) => [k, applyEnvs(v, varMap)]), body, expectedProtobufMsg, + expectedProtobufMsgOnError, }; }