diff --git a/packages/insomnia-app/app/network/grpc/proto-loader.js b/packages/insomnia-app/app/network/grpc/proto-loader.js index 245610560b..e8bfdb359b 100644 --- a/packages/insomnia-app/app/network/grpc/proto-loader.js +++ b/packages/insomnia-app/app/network/grpc/proto-loader.js @@ -27,7 +27,11 @@ export const loadMethods = async ( return []; } - const tempProtoFile = await writeProtoFile(protoFile.protoText); + return await loadMethodsFromText(protoFile.protoText); +}; + +export const loadMethodsFromText = async (text: string): Promise> => { + const tempProtoFile = await writeProtoFile(text); const definition = await protoLoader.load(tempProtoFile, GRPC_LOADER_OPTIONS); return Object.values(definition) diff --git a/packages/insomnia-app/app/ui/components/modals/proto-files-modal.js b/packages/insomnia-app/app/ui/components/modals/proto-files-modal.js index 03d5b38537..b515014059 100644 --- a/packages/insomnia-app/app/ui/components/modals/proto-files-modal.js +++ b/packages/insomnia-app/app/ui/components/modals/proto-files-modal.js @@ -13,10 +13,11 @@ import { showAlert, showError } from './index'; import fs from 'fs'; import path from 'path'; import selectFileOrFolder from '../../../common/select-file-or-folder'; -import { Button } from 'insomnia-components'; +import { AsyncButton } from 'insomnia-components'; import type { GrpcDispatch } from '../../context/grpc'; import { grpcActions, sendGrpcIpcMultiple } from '../../context/grpc'; import { GrpcRequestEventEnum } from '../../../common/grpc-events'; +import * as protoLoader from '../../../network/grpc/proto-loader'; type Props = {| grpcDispatch: GrpcDispatch, @@ -37,6 +38,8 @@ const INITIAL_STATE: State = { selectedProtoFileId: '', }; +const spinner = ; + @autobind class ProtoFilesModal extends React.PureComponent { modal: Modal | null; @@ -98,11 +101,11 @@ class ProtoFilesModal extends React.PureComponent { }); } - _handleAdd() { + _handleAdd(): Promise { return this._handleUpload(); } - async _handleUpload(protoFile?: ProtoFile) { + async _handleUpload(protoFile?: ProtoFile): Promise { const { workspace, grpcDispatch } = this.props; try { @@ -121,6 +124,19 @@ class ProtoFilesModal extends React.PureComponent { const protoText = fs.readFileSync(filePath, 'utf-8'); const name = path.basename(filePath); + // Try parse proto file to make sure the file is valid + try { + await protoLoader.loadMethodsFromText(protoText); + } catch (e) { + showError({ + title: 'Invalid Proto File', + message: `The file ${filePath} and could not be parsed`, + error: e, + }); + + return; + } + // Create or update a protoFile if (protoFile) { await models.protoFile.update(protoFile, { name, protoText }); @@ -150,7 +166,9 @@ class ProtoFilesModal extends React.PureComponent {
Files - + + Add Proto File +
; + const SelectableListItem: React.PureComponent<{ isSelected?: boolean }> = styled(ListGroupItem)` &:hover { background-color: var(--hl-sm) !important; @@ -71,13 +73,14 @@ const ProtoFileListItem = ({
- + -); - -export const text = () => ( - -); - -export const contained = () => ( - -); - -export const disabled = () => ( - -); - -export const withIcon = () => ( - -); - -export const reference = () => ( - - {['default', 'small'].map(s => ( - -

- size={s} -

- - - - - - - - - - - - - - - - - - - - - - - - - - - -
- ))} -
-); diff --git a/packages/insomnia-components/components/button/async-button.js b/packages/insomnia-components/components/button/async-button.js new file mode 100644 index 0000000000..49945940cd --- /dev/null +++ b/packages/insomnia-components/components/button/async-button.js @@ -0,0 +1,49 @@ +// @flow +import * as React from 'react'; +import { Button } from './button'; +import type { ButtonProps } from './button'; + +// Taken from https://github.com/then/is-promise +function isPromise(obj) { + return ( + !!obj && + (typeof obj === 'object' || typeof obj === 'function') && + typeof obj.then === 'function' + ); +} + +type AsyncButtonProps = ButtonProps & { + onClick?: (e: SyntheticEvent) => Promise, + loadingNode?: React.Node, +}; + +export const AsyncButton = ({ + onClick, + disabled, + loadingNode, + children, + ...props +}: AsyncButtonProps) => { + const [loading, setLoading] = React.useState(false); + + const asyncHandler = React.useCallback( + async e => { + const result = onClick(e); + if (isPromise(result)) { + try { + setLoading(true); + await result; + } finally { + setLoading(false); + } + } + }, + [onClick], + ); + + return ( + + ); +}; diff --git a/packages/insomnia-components/components/button/async-button.stories.js b/packages/insomnia-components/components/button/async-button.stories.js new file mode 100644 index 0000000000..c303ec2979 --- /dev/null +++ b/packages/insomnia-components/components/button/async-button.stories.js @@ -0,0 +1,31 @@ +// @flow +import * as React from 'react'; +import { AsyncButton } from './async-button'; +import { select, withKnobs } from '@storybook/addon-knobs'; +import { ButtonSizeEnum, ButtonThemeEnum, ButtonVariantEnum } from './button'; + +export default { + title: 'Buttons | Async Button', + decorators: [withKnobs], +}; + +export const _default = () => ( + new Promise(resolve => setTimeout(resolve, 3000))} + variant={select('Variant', ButtonVariantEnum)} + size={select('Size', ButtonSizeEnum)} + bg={select('Background', ButtonThemeEnum)}> + Do stuff for 3 seconds + +); + +export const customLoader = () => ( + new Promise(resolve => setTimeout(resolve, 3000))} + variant={select('Variant', ButtonVariantEnum)} + size={select('Size', ButtonSizeEnum)} + bg={select('Background', ButtonThemeEnum)} + loadingNode={'Doing stuff...'}> + Do stuff for 3 seconds + +); diff --git a/packages/insomnia-components/components/button.js b/packages/insomnia-components/components/button/button.js similarity index 77% rename from packages/insomnia-components/components/button.js rename to packages/insomnia-components/components/button/button.js index 0c5776c3b0..4653726973 100644 --- a/packages/insomnia-components/components/button.js +++ b/packages/insomnia-components/components/button/button.js @@ -2,14 +2,34 @@ import * as React from 'react'; import styled from 'styled-components'; -type Props = { - onClick?: (e: SyntheticEvent) => any, - bg?: 'default' | 'success' | 'notice' | 'warning' | 'danger' | 'surprise' | 'info', - variant?: 'outlined' | 'contained' | 'text', - size?: 'default' | 'small', +export const ButtonSizeEnum = { + Default: 'default', + Small: 'small', }; -const StyledButton: React.ComponentType = styled.button` +export const ButtonVariantEnum = { + Outlined: 'outlined', + Contained: 'contained', + Text: 'text', +}; + +export const ButtonThemeEnum = { + Default: 'default', + Surprise: 'surprise', + Info: 'info', + Success: 'success', + Notice: 'notice', + Warning: 'warning', + Danger: 'danger', +}; + +export type ButtonProps = React.ElementProps<'button'> & { + bg?: $Values, + variant?: $Values, + size?: $Values, +}; + +const StyledButton: React.ComponentType = styled.button` color: ${({ bg }) => (bg ? `var(--color-${bg})` : 'var(--color-font)')}; text-align: center; font-size: var(--font-size-sm); @@ -100,7 +120,7 @@ const StyledButton: React.ComponentType = styled.button` } `; -const Button = ({ variant, bg, size, ...props }: Props) => ( +export const Button = ({ variant, bg, size, ...props }: ButtonProps) => ( ( size={size || 'default'} /> ); - -export default Button; diff --git a/packages/insomnia-components/components/button/button.stories.js b/packages/insomnia-components/components/button/button.stories.js new file mode 100644 index 0000000000..73e57d1391 --- /dev/null +++ b/packages/insomnia-components/components/button/button.stories.js @@ -0,0 +1,91 @@ +// @flow +import * as React from 'react'; +import { select, withKnobs } from '@storybook/addon-knobs'; +import { Button } from './index'; +import styled from 'styled-components'; +import SvgIcon, { IconEnum } from '../svg-icon'; +import { ButtonSizeEnum, ButtonThemeEnum, ButtonVariantEnum } from './button'; + +export default { + title: 'Buttons | Button', + decorators: [withKnobs], +}; + +const Wrapper: React.ComponentType = styled.div` + display: flex; + + & > * { + margin-right: 0.5rem; + margin-top: 0.8rem; + } +`; +Wrapper.displayName = '...'; + +const Padded: React.ComponentType = styled.div` + margin: 2rem auto; +`; +Padded.displayName = '...'; + +export const outlined = () => ( + +); + +export const text = () => ( + +); + +export const contained = () => ( + +); + +export const disabled = () => ( + +); + +export const withIcon = () => ( + +); + +export const reference = () => ( + + {Object.values(ButtonSizeEnum).map(s => ( + +

+ size={(s: any)} +

+ {Object.values(ButtonVariantEnum).map(v => ( + + {Object.values(ButtonThemeEnum).map(b => ( + + ))} + + ))} +
+ ))} +
+); diff --git a/packages/insomnia-components/components/button/index.js b/packages/insomnia-components/components/button/index.js new file mode 100644 index 0000000000..2f2cc7dfd9 --- /dev/null +++ b/packages/insomnia-components/components/button/index.js @@ -0,0 +1,2 @@ +export { Button } from './button'; +export { AsyncButton } from './async-button'; diff --git a/packages/insomnia-components/components/dropdown/dropdown.stories.js b/packages/insomnia-components/components/dropdown/dropdown.stories.js index f3cf806fb4..e0bfde4975 100644 --- a/packages/insomnia-components/components/dropdown/dropdown.stories.js +++ b/packages/insomnia-components/components/dropdown/dropdown.stories.js @@ -4,7 +4,7 @@ import Dropdown from './dropdown'; import DropdownItem from './dropdown-item'; import DropdownDivider from './dropdown-divider'; import SvgIcon from '../svg-icon'; -import Button from '../button'; +import { Button } from '../button'; export default { title: 'Navigation | Dropdown', diff --git a/packages/insomnia-components/components/list-group/unit-test-item.js b/packages/insomnia-components/components/list-group/unit-test-item.js index 9ea46f130e..aa38a3274d 100644 --- a/packages/insomnia-components/components/list-group/unit-test-item.js +++ b/packages/insomnia-components/components/list-group/unit-test-item.js @@ -4,7 +4,7 @@ import styled from 'styled-components'; import { useToggle } from 'react-use'; import { motion } from 'framer-motion'; import SvgIcon from '../svg-icon'; -import Button from '../button'; +import { Button } from '../button'; import ListGroupItem from './list-group-item'; import UnitTestRequestSelector from './unit-test-request-selector'; diff --git a/packages/insomnia-components/components/notice-table.js b/packages/insomnia-components/components/notice-table.js index 0177b7b023..93ed8dba0d 100644 --- a/packages/insomnia-components/components/notice-table.js +++ b/packages/insomnia-components/components/notice-table.js @@ -1,7 +1,7 @@ // @flow import * as React from 'react'; import { Table, TableBody, TableData, TableHead, TableHeader, TableRow } from './table'; -import Button from './button'; +import { Button } from './button'; import styled from 'styled-components'; import SvgIcon, { IconEnum } from './svg-icon'; diff --git a/packages/insomnia-components/index.js b/packages/insomnia-components/index.js index d6dc5a8687..65fef93cf4 100644 --- a/packages/insomnia-components/index.js +++ b/packages/insomnia-components/index.js @@ -1,5 +1,4 @@ import _Breadcrumb from './components/breadcrumb'; -import _Button from './components/button'; import _Card from './components/card'; import _CardContainer from './components/card-container'; import _Dropdown from './components/dropdown/dropdown'; @@ -19,8 +18,9 @@ import _Switch from './components/switch'; import _ToggleSwitch from './components/toggle-switch'; import * as table from './components/table'; +export { Button, AsyncButton } from './components/button'; + export const Breadcrumb = _Breadcrumb; -export const Button = _Button; export const Card = _Card; export const CardContainer = _CardContainer; export const Dropdown = _Dropdown;