import React, { PureComponent } from 'react'; import PropTypes from 'prop-types'; import autobind from 'autobind-decorator'; import fs from 'fs'; import { clipboard, ipcRenderer, remote } from 'electron'; import { parse as urlParse } from 'url'; import HTTPSnippet from 'insomnia-httpsnippet'; import ReactDOM from 'react-dom'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; import Wrapper from '../components/wrapper'; import WorkspaceEnvironmentsEditModal from '../components/modals/workspace-environments-edit-modal'; import Toast from '../components/toast'; import CookiesModal from '../components/modals/cookies-modal'; import RequestSwitcherModal from '../components/modals/request-switcher-modal'; import SettingsModal, { TAB_INDEX_SHORTCUTS } from '../components/modals/settings-modal'; import { COLLAPSE_SIDEBAR_REMS, DEFAULT_PANE_HEIGHT, DEFAULT_PANE_WIDTH, DEFAULT_SIDEBAR_WIDTH, MAX_PANE_HEIGHT, MAX_PANE_WIDTH, MAX_SIDEBAR_REMS, MIN_PANE_HEIGHT, MIN_PANE_WIDTH, MIN_SIDEBAR_REMS, PREVIEW_MODE_SOURCE, } from '../../common/constants'; import * as globalActions from '../redux/modules/global'; import * as db from '../../common/database'; import * as models from '../../models'; import { selectActiveCookieJar, selectActiveOAuth2Token, selectActiveRequest, selectActiveRequestMeta, selectActiveRequestResponses, selectActiveResponse, selectActiveWorkspace, selectActiveWorkspaceClientCertificates, selectActiveWorkspaceMeta, selectEntitiesLists, selectSidebarChildren, selectUnseenWorkspaces, selectWorkspaceRequestsAndRequestGroups, } from '../redux/selectors'; import RequestCreateModal from '../components/modals/request-create-modal'; import GenerateCodeModal from '../components/modals/generate-code-modal'; import WorkspaceSettingsModal from '../components/modals/workspace-settings-modal'; import RequestSettingsModal from '../components/modals/request-settings-modal'; import RequestRenderErrorModal from '../components/modals/request-render-error-modal'; import * as network from '../../network/network'; import { debounce, getContentDispositionHeader } from '../../common/misc'; import * as mime from 'mime-types'; import * as path from 'path'; import * as render from '../../common/render'; import { getKeys } from '../../templating/utils'; import { showAlert, showModal, showPrompt } from '../components/modals/index'; import { exportHarRequest } from '../../common/har'; import * as hotkeys from '../../common/hotkeys'; import KeydownBinder from '../components/keydown-binder'; import ErrorBoundary from '../components/error-boundary'; import * as plugins from '../../plugins'; import * as templating from '../../templating/index'; import AskModal from '../components/modals/ask-modal'; import { updateMimeType } from '../../models/request'; import MoveRequestGroupModal from '../components/modals/move-request-group-modal'; import * as themes from '../../plugins/misc'; @autobind class App extends PureComponent { constructor(props) { super(props); this.state = { showDragOverlay: false, draggingSidebar: false, draggingPaneHorizontal: false, draggingPaneVertical: false, sidebarWidth: props.sidebarWidth || DEFAULT_SIDEBAR_WIDTH, paneWidth: props.paneWidth || DEFAULT_PANE_WIDTH, paneHeight: props.paneHeight || DEFAULT_PANE_HEIGHT, isVariableUncovered: props.isVariableUncovered || false, }; this._isMigratingChildren = false; this._getRenderContextPromiseCache = {}; this._savePaneWidth = debounce(paneWidth => this._updateActiveWorkspaceMeta({ paneWidth })); this._savePaneHeight = debounce(paneHeight => this._updateActiveWorkspaceMeta({ paneHeight })); this._saveSidebarWidth = debounce(sidebarWidth => this._updateActiveWorkspaceMeta({ sidebarWidth }), ); this._globalKeyMap = null; } _setGlobalKeyMap() { this._globalKeyMap = [ [ hotkeys.SHOW_KEYBOARD_SHORTCUTS, () => { showModal(SettingsModal, TAB_INDEX_SHORTCUTS); }, ], [ hotkeys.SHOW_WORKSPACE_SETTINGS, () => { const { activeWorkspace } = this.props; showModal(WorkspaceSettingsModal, activeWorkspace); }, ], [ hotkeys.SHOW_REQUEST_SETTINGS, () => { if (this.props.activeRequest) { showModal(RequestSettingsModal, { request: this.props.activeRequest, }); } }, ], [ hotkeys.SHOW_QUICK_SWITCHER, () => { showModal(RequestSwitcherModal); }, ], [hotkeys.SEND_REQUEST, this._handleSendShortcut], [hotkeys.SEND_REQUEST_F5, this._handleSendShortcut], [ hotkeys.SHOW_ENVIRONMENTS, () => { const { activeWorkspace } = this.props; showModal(WorkspaceEnvironmentsEditModal, activeWorkspace); }, ], [ hotkeys.SHOW_COOKIES, () => { const { activeWorkspace } = this.props; showModal(CookiesModal, activeWorkspace); }, ], [ hotkeys.CREATE_REQUEST, () => { const { activeRequest, activeWorkspace } = this.props; const parentId = activeRequest ? activeRequest.parentId : activeWorkspace._id; this._requestCreate(parentId); }, ], [ hotkeys.DELETE_REQUEST, () => { const { activeRequest } = this.props; if (!activeRequest) { return; } showModal(AskModal, { title: 'Delete Request?', message: `Really delete ${activeRequest.name}?`, onDone: confirmed => { if (!confirmed) { return; } models.request.remove(activeRequest); }, }); }, ], [ hotkeys.CREATE_FOLDER, () => { const { activeRequest, activeWorkspace } = this.props; const parentId = activeRequest ? activeRequest.parentId : activeWorkspace._id; this._requestGroupCreate(parentId); }, ], [ hotkeys.GENERATE_CODE, async () => { showModal(GenerateCodeModal, this.props.activeRequest); }, ], [ hotkeys.DUPLICATE_REQUEST, async () => { await this._requestDuplicate(this.props.activeRequest); }, ], [ hotkeys.UNCOVER_VARIABLES, async () => { await this._updateIsVariableUncovered(); }, ], ]; } async _handleSendShortcut() { const { activeRequest, activeEnvironment } = this.props; await this._handleSendRequestWithEnvironment( activeRequest ? activeRequest._id : 'n/a', activeEnvironment ? activeEnvironment._id : 'n/a', ); } _setRequestPaneRef(n) { this._requestPane = n; } _setResponsePaneRef(n) { this._responsePane = n; } _setSidebarRef(n) { this._sidebar = n; } _requestGroupCreate(parentId) { showPrompt({ title: 'New Folder', defaultValue: 'My Folder', submitName: 'Create', label: 'Name', selectText: true, onComplete: async name => { const requestGroup = await models.requestGroup.create({ parentId, name, }); await models.requestGroupMeta.create({ parentId: requestGroup._id, collapsed: false, }); }, }); } _requestCreate(parentId) { showModal(RequestCreateModal, { parentId, onComplete: request => { this._handleSetActiveRequest(request._id); }, }); } async _requestGroupDuplicate(requestGroup) { models.requestGroup.duplicate(requestGroup); } async _requestGroupMove(requestGroup) { showModal(MoveRequestGroupModal, { requestGroup }); } async _requestDuplicate(request) { if (!request) { return; } const newRequest = await models.request.duplicate(request); await this._handleSetActiveRequest(newRequest._id); } async _workspaceDuplicate(callback) { const workspace = this.props.activeWorkspace; showPrompt({ title: 'Duplicate Workspace', defaultValue: `${workspace.name} (Copy)`, submitName: 'Duplicate', selectText: true, onComplete: async name => { const newWorkspace = await db.duplicate(workspace, { name }); await this.props.handleSetActiveWorkspace(newWorkspace._id); callback(); }, }); } async _fetchRenderContext() { const { activeEnvironment, activeRequest, activeWorkspace } = this.props; const environmentId = activeEnvironment ? activeEnvironment._id : null; const ancestors = await db.withAncestors(activeRequest || activeWorkspace, [ models.request.type, models.requestGroup.type, models.workspace.type, ]); return render.getRenderContext(activeRequest, environmentId, ancestors); } async _handleGetRenderContext() { const context = await this._fetchRenderContext(); const keys = getKeys(context); return { context, keys }; } /** * Heavily optimized render function * * @param text - template to render * @param contextCacheKey - if rendering multiple times in parallel, set this * @returns {Promise} * @private */ async _handleRenderText(text, contextCacheKey = null) { if (!contextCacheKey || !this._getRenderContextPromiseCache[contextCacheKey]) { const context = this._fetchRenderContext(); // NOTE: We're caching promises here to avoid race conditions this._getRenderContextPromiseCache[contextCacheKey] = context; } // Set timeout to delete the key eventually setTimeout(() => delete this._getRenderContextPromiseCache[contextCacheKey], 5000); const context = await this._getRenderContextPromiseCache[contextCacheKey]; return render.render(text, context); } _handleGenerateCodeForActiveRequest() { this._handleGenerateCode(this.props.activeRequest); } _handleGenerateCode(request) { showModal(GenerateCodeModal, request); } async _handleCopyAsCurl(request) { const { activeEnvironment } = this.props; const environmentId = activeEnvironment ? activeEnvironment._id : 'n/a'; const har = await exportHarRequest(request._id, environmentId); const snippet = new HTTPSnippet(har); const cmd = snippet.convert('shell', 'curl'); clipboard.writeText(cmd); } async _updateRequestGroupMetaByParentId(requestGroupId, patch) { const requestGroupMeta = await models.requestGroupMeta.getByParentId(requestGroupId); if (requestGroupMeta) { await models.requestGroupMeta.update(requestGroupMeta, patch); } else { const newPatch = Object.assign({ parentId: requestGroupId }, patch); await models.requestGroupMeta.create(newPatch); } } async _updateActiveWorkspaceMeta(patch) { const workspaceId = this.props.activeWorkspace._id; const workspaceMeta = await models.workspaceMeta.getOrCreateByParentId(workspaceId); if (workspaceMeta) { return models.workspaceMeta.update(workspaceMeta, patch); } else { const newPatch = Object.assign({ parentId: workspaceId }, patch); return models.workspaceMeta.create(newPatch); } } async _updateRequestMetaByParentId(requestId, patch) { const requestMeta = await models.requestMeta.getByParentId(requestId); if (requestMeta) { return models.requestMeta.update(requestMeta, patch); } else { const newPatch = Object.assign({ parentId: requestId }, patch); return models.requestMeta.create(newPatch); } } _updateIsVariableUncovered(paneWidth) { this.setState({ isVariableUncovered: !this.state.isVariableUncovered }); } _handleSetPaneWidth(paneWidth) { this.setState({ paneWidth }); this._savePaneWidth(paneWidth); } _handleSetPaneHeight(paneHeight) { this.setState({ paneHeight }); this._savePaneHeight(paneHeight); } async _handleSetActiveRequest(activeRequestId) { await this._updateActiveWorkspaceMeta({ activeRequestId }); } async _handleSetActiveEnvironment(activeEnvironmentId) { await this._updateActiveWorkspaceMeta({ activeEnvironmentId }); // Give it time to update and re-render setTimeout(() => { this._wrapper._forceRequestPaneRefresh(); }, 100); } _handleSetSidebarWidth(sidebarWidth) { this.setState({ sidebarWidth }); this._saveSidebarWidth(sidebarWidth); } async _handleSetSidebarHidden(sidebarHidden) { await this._updateActiveWorkspaceMeta({ sidebarHidden }); } async _handleSetSidebarFilter(sidebarFilter) { await this._updateActiveWorkspaceMeta({ sidebarFilter }); } _handleSetRequestGroupCollapsed(requestGroupId, collapsed) { this._updateRequestGroupMetaByParentId(requestGroupId, { collapsed }); } _handleSetResponsePreviewMode(requestId, previewMode) { this._updateRequestMetaByParentId(requestId, { previewMode }); } async _handleSetResponseFilter(requestId, responseFilter) { await this._updateRequestMetaByParentId(requestId, { responseFilter }); clearTimeout(this._responseFilterHistorySaveTimeout); this._responseFilterHistorySaveTimeout = setTimeout(async () => { const meta = await models.requestMeta.getByParentId(requestId); const responseFilterHistory = meta.responseFilterHistory.slice(0, 10); // Already in history? if (responseFilterHistory.includes(responseFilter)) { return; } // Blank? if (!responseFilter) { return; } responseFilterHistory.unshift(responseFilter); await this._updateRequestMetaByParentId(requestId, { responseFilterHistory, }); }, 2000); } async _handleUpdateRequestMimeType(mimeType) { if (!this.props.activeRequest) { console.warn('Tried to update request mime-type when no active request'); return null; } const requestMeta = await models.requestMeta.getOrCreateByParentId( this.props.activeRequest._id, ); const savedBody = requestMeta.savedRequestBody; const saveValue = typeof mimeType !== 'string' // Switched to No body ? this.props.activeRequest.body : {}; // Clear saved value in requestMeta await models.requestMeta.update(requestMeta, { savedRequestBody: saveValue, }); const newRequest = await updateMimeType(this.props.activeRequest, mimeType, false, savedBody); // Force it to update, because other editor components (header editor) // needs to change. Need to wait a delay so the next render can finish setTimeout(this._wrapper._forceRequestPaneRefresh, 300); return newRequest; } async _getDownloadLocation() { return new Promise(resolve => { const options = { title: 'Select Download Location', buttonLabel: 'Send and Save', defaultPath: window.localStorage.getItem('insomnia.sendAndDownloadLocation'), }; remote.dialog.showSaveDialog(options, filename => { window.localStorage.setItem('insomnia.sendAndDownloadLocation', filename); resolve(filename); }); }); } async _handleSendAndDownloadRequestWithEnvironment(requestId, environmentId, dir) { const request = await models.request.getById(requestId); if (!request) { return; } // NOTE: Since request is by far the most popular event, we will throttle // it so that we only track it if the request has changed since the last one const key = request._id; if (this._sendRequestTrackingKey !== key) { this._sendRequestTrackingKey = key; } // Start loading this.props.handleStartLoading(requestId); try { const responsePatch = await network.send(requestId, environmentId); const headers = responsePatch.headers || []; const header = getContentDispositionHeader(headers); const nameFromHeader = header ? header.value : null; if (!responsePatch.bodyPath) { return; } if (responsePatch.statusCode >= 200 && responsePatch.statusCode < 300) { const extension = mime.extension(responsePatch.contentType) || 'unknown'; const name = nameFromHeader || `${request.name.replace(/\s/g, '-').toLowerCase()}.${extension}`; let filename; if (dir) { filename = path.join(dir, name); } else { filename = await this._getDownloadLocation(); } const to = fs.createWriteStream(filename); const readStream = models.response.getBodyStream(responsePatch); if (!readStream) { return; } readStream.pipe(to); readStream.on('end', async () => { responsePatch.error = `Saved to ${filename}`; await models.response.create(responsePatch); }); readStream.on('error', async err => { console.warn('Failed to download request after sending', responsePatch.bodyPath, err); await models.response.create(responsePatch); }); } } catch (err) { showAlert({ title: 'Unexpected Request Failure', message: (
The request failed due to an unhandled error:
{err.message}
The request failed due to an unhandled error:
{err.message}
{path}
?
),
addCancel: true,
});
handleImportUriToWorkspace(activeWorkspace._id, uri);
},
false,
);
ipcRenderer.on('toggle-sidebar', this._handleToggleSidebar);
// handle this
this._handleToggleMenuBar(this.props.settings.autoHideMenuBar);
// Give it a bit before letting the backend know it's ready
setTimeout(() => ipcRenderer.send('window-ready'), 500);
}
componentWillUnmount() {
// Remove mouse and key handlers
document.removeEventListener('mouseup', this._handleMouseUp);
document.removeEventListener('mousemove', this._handleMouseMove);
}
async _ensureWorkspaceChildren(props) {
const { activeWorkspace, activeCookieJar, environments } = props;
const baseEnvironments = environments.filter(e => e.parentId === activeWorkspace._id);
// Nothing to do
if (baseEnvironments.length && activeCookieJar) {
return;
}
// We already started migrating. Let it finish.
if (this._isMigratingChildren) {
return;
}
// Prevent rendering of everything
this._isMigratingChildren = true;
await db.bufferChanges();
if (baseEnvironments.length === 0) {
await models.environment.create({ parentId: activeWorkspace._id });
console.log(`[app] Created missing base environment for ${activeWorkspace.name}`);
}
if (!activeCookieJar) {
await models.cookieJar.create({
parentId: this.props.activeWorkspace._id,
});
console.log(`[app] Created missing cookie jar for ${activeWorkspace.name}`);
}
await db.flushChanges();
// Flush "transaction"
this._isMigratingChildren = false;
}
componentWillReceiveProps(nextProps) {
this._ensureWorkspaceChildren(nextProps);
}
componentWillMount() {
this._ensureWorkspaceChildren(this.props);
}
render() {
if (this._isMigratingChildren) {
console.log('[app] Waiting for migration to complete');
return null;
}
return (