Validate protofile during upload (#2864)

* feat(insomnia-components): add async-button

* feat: show error if parsing protofile fails during upload/add

* chore: flow is poop

* feat: use try-finally
This commit is contained in:
Opender Singh
2020-11-25 10:02:28 +13:00
committed by GitHub
parent 4a9235f10f
commit ef2fc3baec
13 changed files with 238 additions and 190 deletions

View File

@@ -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<Array<GrpcMethodDefinition>> => {
const tempProtoFile = await writeProtoFile(text);
const definition = await protoLoader.load(tempProtoFile, GRPC_LOADER_OPTIONS);
return Object.values(definition)

View File

@@ -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 = <i className="fa fa-spin fa-refresh" />;
@autobind
class ProtoFilesModal extends React.PureComponent<Props, State> {
modal: Modal | null;
@@ -98,11 +101,11 @@ class ProtoFilesModal extends React.PureComponent<Props, State> {
});
}
_handleAdd() {
_handleAdd(): Promise<void> {
return this._handleUpload();
}
async _handleUpload(protoFile?: ProtoFile) {
async _handleUpload(protoFile?: ProtoFile): Promise<void> {
const { workspace, grpcDispatch } = this.props;
try {
@@ -121,6 +124,19 @@ class ProtoFilesModal extends React.PureComponent<Props, State> {
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<Props, State> {
<ModalBody className="wide pad">
<div className="row-spaced margin-bottom bold">
Files
<Button onClick={this._handleAdd}>Add Proto File</Button>
<AsyncButton onClick={this._handleAdd} loadingNode={spinner}>
Add Proto File
</AsyncButton>
</div>
<ProtoFileList
protoFiles={protoFiles}

View File

@@ -8,7 +8,7 @@ import type {
SelectProtoFileHandler,
UpdateProtoFileHandler,
} from './proto-file-list';
import { ListGroupItem, Button } from '../../../../../insomnia-components';
import { ListGroupItem, Button, AsyncButton } from '../../../../../insomnia-components';
import Editable from '../base/editable';
type Props = {
@@ -20,6 +20,8 @@ type Props = {
handleUpdate: UpdateProtoFileHandler,
};
const spinner = <i className="fa fa-spin fa-refresh" />;
const SelectableListItem: React.PureComponent<{ isSelected?: boolean }> = styled(ListGroupItem)`
&:hover {
background-color: var(--hl-sm) !important;
@@ -71,13 +73,14 @@ const ProtoFileListItem = ({
<div className="row-spaced">
<Editable className="wide" onSubmit={handleRenameCallback} value={name} preventBlank />
<div className="row">
<Button
<AsyncButton
variant="text"
title="Re-upload Proto File"
onClick={handleUpdateCallback}
loadingNode={spinner}
className="space-right">
<i className="fa fa-upload" />
</Button>
</AsyncButton>
<Button
variant="text"
title="Delete Proto File"

View File

@@ -1,168 +0,0 @@
// @flow
import * as React from 'react';
import { select, withKnobs } from '@storybook/addon-knobs';
import Button from './button';
import styled from 'styled-components';
import SvgIcon, { IconEnum } from './svg-icon';
export default {
title: 'Buttons | Button',
decorators: [withKnobs],
};
const Wrapper: React.ComponentType<any> = styled.div`
display: flex;
& > * {
margin-right: 0.5rem;
margin-top: 0.8rem;
}
`;
Wrapper.displayName = '...';
const Padded: React.ComponentType<any> = styled.div`
margin: 2rem auto;
`;
Padded.displayName = '...';
const sizes = {
Default: 'default',
Small: 'small',
};
const variants = {
Outlined: 'outlined',
Contained: 'contained',
Text: 'text',
};
const themeColors = {
Default: null,
Surprise: 'surprise',
Info: 'info',
Success: 'success',
Notice: 'notice',
Warning: 'warning',
Danger: 'danger',
};
export const outlined = () => (
<Button
variant={select('Variant', variants, 'outlined')}
size={select('Size', sizes, null)}
onClick={() => window.alert('Clicked!')}
bg={select('Background', themeColors, null)}>
Outlined
</Button>
);
export const text = () => (
<Button
variant={select('Variant', variants, 'text')}
onClick={() => window.alert('Clicked!')}
bg={select('Background', themeColors)}>
Text
</Button>
);
export const contained = () => (
<Button
variant={select('Variant', variants, 'contained')}
onClick={() => window.alert('Clicked!')}
bg={select('Background', themeColors)}>
Contained
</Button>
);
export const disabled = () => (
<Button onClick={() => window.alert('Clicked!')} bg={select('Background', themeColors)} disabled>
Can't Touch This
</Button>
);
export const withIcon = () => (
<Button onClick={() => window.alert('Clicked!')} bg={select('Background', themeColors)}>
Expand <SvgIcon icon={IconEnum.chevronDown} />
</Button>
);
export const reference = () => (
<React.Fragment>
{['default', 'small'].map(s => (
<Padded>
<h2>
<code>size={s}</code>
</h2>
<Wrapper>
<Button variant="contained" size={s}>
Default
</Button>
<Button bg="success" variant="contained" size={s}>
Success
</Button>
<Button bg="surprise" variant="contained" size={s}>
Surprise
</Button>
<Button bg="danger" variant="contained" size={s}>
Danger
</Button>
<Button bg="warning" variant="contained" size={s}>
Warning
</Button>
<Button bg="notice" variant="contained" size={s}>
Notice
</Button>
<Button bg="info" variant="contained" size={s}>
Info
</Button>
</Wrapper>
<Wrapper>
<Button variant="outlined" size={s}>
Default
</Button>
<Button bg="success" variant="outlined" size={s}>
Success
</Button>
<Button bg="surprise" variant="outlined" size={s}>
Surprise
</Button>
<Button bg="danger" variant="outlined" size={s}>
Danger
</Button>
<Button bg="warning" variant="outlined" size={s}>
Warning
</Button>
<Button bg="notice" variant="outlined" size={s}>
Notice
</Button>
<Button bg="info" variant="outlined" size={s}>
Info
</Button>
</Wrapper>
<Wrapper>
<Button variant="text" size={s}>
Default
</Button>
<Button bg="success" variant="text" size={s}>
Success
</Button>
<Button bg="surprise" variant="text" size={s}>
Surprise
</Button>
<Button bg="danger" variant="text" size={s}>
Danger
</Button>
<Button bg="warning" variant="text" size={s}>
Warning
</Button>
<Button bg="notice" variant="text" size={s}>
Notice
</Button>
<Button bg="info" variant="text" size={s}>
Info
</Button>
</Wrapper>
</Padded>
))}
</React.Fragment>
);

View File

@@ -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<HTMLButtonElement>) => Promise<any>,
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 (
<Button {...props} onClick={asyncHandler} disabled={loading || disabled}>
{(loading && loadingNode) || children}
</Button>
);
};

View File

@@ -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 = () => (
<AsyncButton
onClick={() => new Promise(resolve => setTimeout(resolve, 3000))}
variant={select('Variant', ButtonVariantEnum)}
size={select('Size', ButtonSizeEnum)}
bg={select('Background', ButtonThemeEnum)}>
Do stuff for 3 seconds
</AsyncButton>
);
export const customLoader = () => (
<AsyncButton
onClick={() => 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
</AsyncButton>
);

View File

@@ -2,14 +2,34 @@
import * as React from 'react';
import styled from 'styled-components';
type Props = {
onClick?: (e: SyntheticEvent<HTMLButtonElement>) => 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<Props> = 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<typeof ButtonThemeEnum>,
variant?: $Values<typeof ButtonVariantEnum>,
size?: $Values<typeof ButtonSizeEnum>,
};
const StyledButton: React.ComponentType<ButtonProps> = 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<Props> = styled.button`
}
`;
const Button = ({ variant, bg, size, ...props }: Props) => (
export const Button = ({ variant, bg, size, ...props }: ButtonProps) => (
<StyledButton
{...props}
variant={variant || 'outlined'}
@@ -108,5 +128,3 @@ const Button = ({ variant, bg, size, ...props }: Props) => (
size={size || 'default'}
/>
);
export default Button;

View File

@@ -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<any> = styled.div`
display: flex;
& > * {
margin-right: 0.5rem;
margin-top: 0.8rem;
}
`;
Wrapper.displayName = '...';
const Padded: React.ComponentType<any> = styled.div`
margin: 2rem auto;
`;
Padded.displayName = '...';
export const outlined = () => (
<Button
variant={select('Variant', ButtonVariantEnum)}
size={select('Size', ButtonSizeEnum)}
onClick={() => window.alert('Clicked!')}
bg={select('Background', ButtonThemeEnum)}>
Outlined
</Button>
);
export const text = () => (
<Button
variant={select('Variant', ButtonVariantEnum, ButtonVariantEnum.Text)}
onClick={() => window.alert('Clicked!')}
bg={select('Background', ButtonThemeEnum)}>
Text
</Button>
);
export const contained = () => (
<Button
variant={select('Variant', ButtonVariantEnum, ButtonVariantEnum.Contained)}
onClick={() => window.alert('Clicked!')}
bg={select('Background', ButtonThemeEnum)}>
Contained
</Button>
);
export const disabled = () => (
<Button
onClick={() => window.alert('Clicked!')}
bg={select('Background', ButtonThemeEnum)}
disabled>
Can't Touch This
</Button>
);
export const withIcon = () => (
<Button onClick={() => window.alert('Clicked!')} bg={select('Background', ButtonThemeEnum)}>
Expand <SvgIcon icon={IconEnum.chevronDown} />
</Button>
);
export const reference = () => (
<React.Fragment>
{Object.values(ButtonSizeEnum).map(s => (
<Padded>
<h2>
<code>size={(s: any)}</code>
</h2>
{Object.values(ButtonVariantEnum).map(v => (
<Wrapper>
{Object.values(ButtonThemeEnum).map(b => (
<Button bg={b} variant={v} size={s}>
{b || 'Default'}
</Button>
))}
</Wrapper>
))}
</Padded>
))}
</React.Fragment>
);

View File

@@ -0,0 +1,2 @@
export { Button } from './button';
export { AsyncButton } from './async-button';

View File

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

View File

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

View File

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

View File

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