Minor UX improvements to HAR export

This commit is contained in:
Gregory Schier 2017-11-01 13:40:24 +01:00
parent ccdd0c0a8b
commit ecfce11d7b
8 changed files with 240 additions and 96 deletions

View File

@ -13,6 +13,7 @@ import type {Response as ResponseModel} from '../models/response';
import {getAuthHeader} from '../network/authentication';
import {getAppVersion} from './constants';
import {getSetCookieHeaders} from './misc';
import {RenderError} from '../templating/index';
export type HarCookie = {
name: string,
@ -264,8 +265,16 @@ export async function exportHarRequest (requestId: string, environmentId: string
}
export async function exportHarWithRequest (request: Request, environmentId: string, addContentLength: boolean = false): Promise<HarRequest | null> {
const renderedRequest = await getRenderedRequest(request, environmentId);
return await exportHarWithRenderedRequest(renderedRequest, addContentLength);
try {
const renderedRequest = await getRenderedRequest(request, environmentId);
return exportHarWithRenderedRequest(renderedRequest, addContentLength);
} catch (err) {
if (err instanceof RenderError) {
throw new Error(`Failed to render "${request.name}:${err.path}"\n ${err.message}`);
} else {
throw new Error(`Failed to export request "${request.name}"\n ${err.message}`);
}
}
}
export async function exportHarWithRenderedRequest (renderedRequest: RenderedRequest, addContentLength: boolean = false): Promise<HarRequest> {
@ -328,8 +337,7 @@ function getReponseCookies (response: ResponseModel): Array<HarCookie> {
}
return mapCookie(cookie);
})
.filter(Boolean);
}).filter(Boolean);
}
function mapCookie (cookie: Cookie): HarCookie {

View File

@ -20,6 +20,10 @@ class AskModal extends PureComponent {
this.modal = m;
}
_setYesButtonRef (n) {
this.yesButton = n;
}
_handleYes () {
this.hide();
this._doneCallback && this._doneCallback(true);
@ -49,6 +53,10 @@ class AskModal extends PureComponent {
this.modal.show();
setTimeout(() => {
this.yesButton && this.yesButton.focus();
}, 100);
return new Promise(resolve => {
this._promiseCallback = resolve;
});
@ -68,7 +76,7 @@ class AskModal extends PureComponent {
<button className="btn" onClick={this._handleNo}>
No
</button>
<button className="btn" onClick={this._handleYes}>
<button ref={this._setYesButtonRef} className="btn" onClick={this._handleYes}>
Yes
</button>
</div>

View File

@ -46,7 +46,7 @@ class ErrorModal extends PureComponent {
const {error, message, title, addCancel} = this.state;
return (
<Modal ref={this._setModalRef} closeOnKeyCodes={[13]}>
<Modal ref={this._setModalRef}>
<ModalHeader>{title || 'Uh Oh!'}</ModalHeader>
<ModalBody className="wide pad">
{message ? (

View File

@ -0,0 +1,101 @@
// @flow
import * as React from 'react';
import autobind from 'autobind-decorator';
import Modal from '../base/modal';
import ModalBody from '../base/modal-body';
import ModalHeader from '../base/modal-header';
import ModalFooter from '../base/modal-footer';
type Props = {};
type State = {
title: string,
options: Array<{name: string, value: string}>,
value: string,
message: string
};
@autobind
class SelectModal extends React.PureComponent<Props, State> {
modal: ?Modal;
doneButton: ?HTMLSelectElement;
_doneCallback: ?Function;
constructor (props: Props) {
super(props);
this.state = {
title: '',
options: [],
message: '',
value: ''
};
}
_setModalRef (m: ?Modal) {
this.modal = m;
}
_setDoneButtonRef (n: ?HTMLSelectElement) {
this.doneButton = n;
}
_handleDone () {
this.hide();
this._doneCallback && this._doneCallback(this.state.value);
}
_handleSelectChange (e: SyntheticEvent<HTMLInputElement>) {
this.setState({value: e.currentTarget.value});
}
hide () {
this.modal && this.modal.hide();
}
show (data: Object = {}) {
const {
title,
message,
options,
value,
onDone
} = data;
this._doneCallback = onDone;
this.setState({title, message, options, value});
this.modal && this.modal.show();
setTimeout(() => {
this.doneButton && this.doneButton.focus();
}, 100);
}
render () {
const {message, title, options, value} = this.state;
return (
<Modal noEscape ref={this._setModalRef}>
<ModalHeader>{title || 'Confirm?'}</ModalHeader>
<ModalBody className="wide pad">
<p>{message}</p>
<div className="form-control form-control--outlined">
<select onChange={this._handleSelectChange} value={value}>
{options.map(({name, value}) => (
<option key={value} value={value}>{name}</option>
))}
</select>
</div>
</ModalBody>
<ModalFooter>
<button ref={this._setDoneButtonRef} className="btn" onClick={this._handleDone}>
Done
</button>
</ModalFooter>
</Modal>
);
}
}
export default SelectModal;

View File

@ -22,6 +22,7 @@ import PaymentNotificationModal from './modals/payment-notification-modal';
import NunjucksModal from './modals/nunjucks-modal';
import PromptModal from './modals/prompt-modal';
import AskModal from './modals/ask-modal';
import SelectModal from './modals/select-modal';
import RequestCreateModal from './modals/request-create-modal';
import RequestPane from './request-pane';
import RequestSwitcherModal from './modals/request-switcher-modal';
@ -403,6 +404,7 @@ class Wrapper extends React.PureComponent<Props, State> {
<ChangelogModal ref={registerModal}/>
<LoginModal ref={registerModal}/>
<AskModal ref={registerModal}/>
<SelectModal ref={registerModal}/>
<RequestCreateModal ref={registerModal}/>
<PaymentNotificationModal ref={registerModal}/>
<FilterHelpModal ref={registerModal}/>

View File

@ -13,6 +13,8 @@ import {showModal} from '../../components/modals';
import PaymentNotificationModal from '../../components/modals/payment-notification-modal';
import LoginModal from '../../components/modals/login-modal';
import * as models from '../../../models';
import SelectModal from '../../components/modals/select-modal';
import {showError} from '../../components/modals/index';
const LOCALSTORAGE_PREFIX = `insomnia::meta`;
@ -180,79 +182,108 @@ export function importUri (workspaceId, uri) {
}
export function exportFile (workspaceId = null) {
return async dispatch => {
return dispatch => {
dispatch(loadStart());
const workspace = await models.workspace.getById(workspaceId);
const VALUE_JSON = 'json';
const VALUE_HAR = 'har';
// Check if we want to export private environments
let environments;
if (workspace) {
const parentEnv = await models.environment.getOrCreateForWorkspace(workspace);
environments = [
parentEnv,
...await models.environment.findByParentId(parentEnv._id)
];
} else {
environments = await models.environment.all();
}
let exportPrivateEnvironments = false;
const privateEnvironments = environments.filter(e => e.isPrivate);
if (privateEnvironments.length) {
const names = privateEnvironments.map(e => e.name).join(', ');
exportPrivateEnvironments = await showModal(AskModal, {
title: 'Export Private Environments?',
message: `Do you want to include private environments (${names}) in your export?`
});
}
const date = moment().format('YYYY-MM-DD');
const name = (workspace ? workspace.name : 'Insomnia All').replace(/ /g, '-');
const lastDir = window.localStorage.getItem('insomnia.lastExportPath');
const dir = lastDir || electron.remote.app.getPath('desktop');
const options = {
title: 'Export Insomnia Data',
buttonLabel: 'Export',
defaultPath: path.join(dir, `${name}_${date}`),
filters: [{
name: 'Insomnia Export', extensions: ['json']
}, {
name: 'HTTP Archive 1.2', extensions: ['har']
}]
};
electron.remote.dialog.showSaveDialog(options, async filename => {
if (!filename) {
trackEvent('Export', 'Cancel');
// It was cancelled, so let's bail out
showModal(SelectModal, {
title: 'Select Export Type',
options: [
{name: 'Insomnia Sharable with other Insomnia users', value: VALUE_JSON},
{name: 'HAR HTTP Archive Format', value: VALUE_HAR}
],
message: 'Which format would you like to export as?',
onCancel: () => {
dispatch(loadStop());
return;
}
},
onDone: async selectedFormat => {
const workspace = await
models.workspace.getById(workspaceId);
let json;
if (path.extname(filename) === '.har') {
json = await importUtils.exportHAR(workspace, exportPrivateEnvironments);
} else {
json = await importUtils.exportJSON(workspace, exportPrivateEnvironments);
}
// Remember last exported path
window.localStorage.setItem(
'insomnia.lastExportPath',
path.dirname(filename)
);
fs.writeFile(filename, json, {}, err => {
if (err) {
console.warn('Export failed', err);
trackEvent('Export', 'Failure');
return;
// Check if we want to export private environments
let environments;
if (workspace) {
const parentEnv = await models.environment.getOrCreateForWorkspace(workspace);
environments = [
parentEnv,
...await models.environment.findByParentId(parentEnv._id)
];
} else {
environments = await models.environment.all();
}
trackEvent('Export', 'Success');
dispatch(loadStop());
});
let exportPrivateEnvironments = false;
const privateEnvironments = environments.filter(e => e.isPrivate);
if (privateEnvironments.length) {
const names = privateEnvironments.map(e => e.name).join(', ');
exportPrivateEnvironments = await showModal(AskModal, {
title: 'Export Private Environments?',
message: `Do you want to include private environments (${names}) in your export?`
});
}
const date = moment().format('YYYY-MM-DD');
const name = (workspace ? workspace.name : 'Insomnia All').replace(/ /g, '-');
const lastDir = window.localStorage.getItem('insomnia.lastExportPath');
const dir = lastDir || electron.remote.app.getPath('desktop');
const options = {
title: 'Export Insomnia Data',
buttonLabel: 'Export',
defaultPath: path.join(dir, `${name}_${date}`),
filters: []
};
if (selectedFormat === VALUE_HAR) {
options.filters = [{name: 'HTTP Archive 1.2', extensions: ['har', 'har.json', 'json']}];
} else {
options.filters = [{name: 'Insomnia Export', extensions: ['json']}];
}
electron.remote.dialog.showSaveDialog(options, async filename => {
if (!filename) {
trackEvent('Export', 'Cancel');
// It was cancelled, so let's bail out
dispatch(loadStop());
return;
}
let json;
try {
if (selectedFormat === VALUE_HAR) {
json = await importUtils.exportHAR(workspace, exportPrivateEnvironments);
} else {
json = await importUtils.exportJSON(workspace, exportPrivateEnvironments);
}
} catch (err) {
showError({
title: 'Export Failed',
error: err,
message: 'Export failed due to an unexpected error'
});
dispatch(loadStop());
return;
}
// Remember last exported path
window.localStorage.setItem(
'insomnia.lastExportPath',
path.dirname(filename)
);
fs.writeFile(filename, json, {}, err => {
if (err) {
console.warn('Export failed', err);
trackEvent('Export', 'Failure');
return;
}
trackEvent('Export', 'Success');
dispatch(loadStop());
});
});
}
});
};
}

38
package-lock.json generated
View File

@ -5,9 +5,9 @@
"requires": true,
"dependencies": {
"@types/node": {
"version": "7.0.43",
"resolved": "https://registry.npmjs.org/@types/node/-/node-7.0.43.tgz",
"integrity": "sha512-7scYwwfHNppXvH/9JzakbVxk0o0QUILVk1Lv64GRaxwPuGpnF1QBiwdvhDpLcymb8BpomQL3KYoWKq3wUdDMhQ==",
"version": "7.0.46",
"resolved": "https://registry.npmjs.org/@types/node/-/node-7.0.46.tgz",
"integrity": "sha512-u+JAi1KtmaUoU/EHJkxoiuvzyo91FCE41Z9TZWWcOUU3P8oUdlDLdrGzCGWySPgbRMD17B0B+1aaJLYI9egQ6A==",
"dev": true
},
"7zip": {
@ -2997,14 +2997,14 @@
"dev": true
},
"electron": {
"version": "1.7.8",
"resolved": "https://registry.npmjs.org/electron/-/electron-1.7.8.tgz",
"integrity": "sha1-J7eRpolRcafVKZG5lELNvRCjU50=",
"version": "1.7.9",
"resolved": "https://registry.npmjs.org/electron/-/electron-1.7.9.tgz",
"integrity": "sha1-rdVOn4+D7QL2UZ7BATX2mLGTNs8=",
"dev": true,
"requires": {
"@types/node": "7.0.43",
"@types/node": "7.0.46",
"electron-download": "3.3.0",
"extract-zip": "1.6.5"
"extract-zip": "1.6.6"
}
},
"electron-builder": {
@ -4057,24 +4057,24 @@
}
},
"extract-zip": {
"version": "1.6.5",
"resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-1.6.5.tgz",
"integrity": "sha1-maBnNbbqIOqbcF13ms/8yHz/BEA=",
"version": "1.6.6",
"resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-1.6.6.tgz",
"integrity": "sha1-EpDt6NINCHK0Kf0/NRyhKOxe+Fw=",
"dev": true,
"requires": {
"concat-stream": "1.6.0",
"debug": "2.2.0",
"debug": "2.6.9",
"mkdirp": "0.5.0",
"yauzl": "2.4.1"
},
"dependencies": {
"debug": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz",
"integrity": "sha1-+HBX6ZWxofauaklgZkE3vFbwOdo=",
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"dev": true,
"requires": {
"ms": "0.7.1"
"ms": "2.0.0"
}
},
"mkdirp": {
@ -4085,12 +4085,6 @@
"requires": {
"minimist": "0.0.8"
}
},
"ms": {
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz",
"integrity": "sha1-nNE8A62/8ltl7/3nzoZO6VIBcJg=",
"dev": true
}
}
},

View File

@ -181,7 +181,7 @@
"concurrently": "^2.0.0",
"cross-env": "^2.0.0",
"css-loader": "^0.26.2",
"electron": "^1.7.8",
"electron": "^1.7.9",
"electron-builder": "^10.17.3",
"electron-rebuild": "^1.6.0",
"eslint": "^3.16.1",