mirror of
https://github.com/Kong/insomnia.git
synced 2026-04-21 14:47:46 -04:00
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:
@@ -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)
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
2
packages/insomnia-components/components/button/index.js
Normal file
2
packages/insomnia-components/components/button/index.js
Normal file
@@ -0,0 +1,2 @@
|
||||
export { Button } from './button';
|
||||
export { AsyncButton } from './async-button';
|
||||
@@ -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',
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user