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 '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, selectSyncItems, 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, getDataDirectory } 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 { hotKeyRefs } from '../../common/hotkeys'; import { executeHotKey } from '../../common/hotkeys-listener'; 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'; import ExportRequestsModal from '../components/modals/export-requests-modal'; import FileSystemDriver from '../../sync/store/drivers/file-system-driver'; import VCS from '../../sync/vcs'; import SyncMergeModal from '../components/modals/sync-merge-modal'; @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, vcs: null, forceRefreshCounter: 0, forceRefreshHeaderCounter: 0, 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 = [ [ hotKeyRefs.PREFERENCES_SHOW_GENERAL, () => { showModal(SettingsModal); }, ], [ hotKeyRefs.PREFERENCES_SHOW_KEYBOARD_SHORTCUTS, () => { showModal(SettingsModal, TAB_INDEX_SHORTCUTS); }, ], [ hotKeyRefs.SHOW_RECENT_REQUESTS, () => { showModal(RequestSwitcherModal, { disableInput: true, maxRequests: 10, maxWorkspaces: 0, selectOnKeyup: true, title: 'Recent Requests', hideNeverActiveRequests: true, // Add an open delay so the dialog won't show for quick presses openDelay: 150, }); }, ], [ hotKeyRefs.WORKSPACE_SHOW_SETTINGS, () => { const { activeWorkspace } = this.props; showModal(WorkspaceSettingsModal, activeWorkspace); }, ], [ hotKeyRefs.REQUEST_SHOW_SETTINGS, () => { if (this.props.activeRequest) { showModal(RequestSettingsModal, { request: this.props.activeRequest, }); } }, ], [ hotKeyRefs.REQUEST_QUICK_SWITCH, () => { showModal(RequestSwitcherModal); }, ], [hotKeyRefs.REQUEST_SEND, this._handleSendShortcut], [ hotKeyRefs.ENVIRONMENT_SHOW_EDITOR, () => { const { activeWorkspace } = this.props; showModal(WorkspaceEnvironmentsEditModal, activeWorkspace); }, ], [ hotKeyRefs.SHOW_COOKIES_EDITOR, () => { const { activeWorkspace } = this.props; showModal(CookiesModal, activeWorkspace); }, ], [ hotKeyRefs.REQUEST_QUICK_CREATE, async () => { const { activeRequest, activeWorkspace } = this.props; const parentId = activeRequest ? activeRequest.parentId : activeWorkspace._id; const request = await models.request.create({ parentId, name: 'New Request' }); await this._handleSetActiveRequest(request._id); }, ], [ hotKeyRefs.REQUEST_SHOW_CREATE, () => { const { activeRequest, activeWorkspace } = this.props; const parentId = activeRequest ? activeRequest.parentId : activeWorkspace._id; this._requestCreate(parentId); }, ], [ hotKeyRefs.REQUEST_SHOW_DELETE, () => { 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); }, }); }, ], [ hotKeyRefs.REQUEST_SHOW_CREATE_FOLDER, () => { const { activeRequest, activeWorkspace } = this.props; const parentId = activeRequest ? activeRequest.parentId : activeWorkspace._id; this._requestGroupCreate(parentId); }, ], [ hotKeyRefs.REQUEST_SHOW_GENERATE_CODE_EDITOR, async () => { showModal(GenerateCodeModal, this.props.activeRequest); }, ], [ hotKeyRefs.REQUEST_SHOW_DUPLICATE, async () => { await this._requestDuplicate(this.props.activeRequest); }, ], [ hotKeyRefs.REQUEST_TOGGLE_PIN, async () => { if (!this.props.activeRequest) { return; } const metas = Object.values(this.props.entities.requestMetas).find( m => m.parentId === this.props.activeRequest._id, ); await this._handleSetRequestPinned(this.props.activeRequest, !(metas && metas.pinned)); }, ], [hotKeyRefs.PLUGIN_RELOAD, this._handleReloadPlugins], [ hotKeyRefs.ENVIRONMENT_UNCOVER_VARIABLES, async () => { await this._updateIsVariableUncovered(); }, ], [ hotKeyRefs.SIDEBAR_TOGGLE, () => { this._handleToggleSidebar(); }, ], ]; } 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); }, }); } static async _requestGroupDuplicate(requestGroup) { showPrompt({ title: 'Duplicate Folder', defaultValue: requestGroup.name, submitName: 'Create', label: 'New Name', selectText: true, onComplete: async name => { await models.requestGroup.duplicate(requestGroup, { name }); }, }); } static async _requestGroupMove(requestGroup) { showModal(MoveRequestGroupModal, { requestGroup }); } _requestDuplicate(request) { if (!request) { return; } showPrompt({ title: 'Duplicate Request', defaultValue: request.name, submitName: 'Create', label: 'New Name', selectText: true, onComplete: async name => { const newRequest = await models.request.duplicate(request, { name }); await this._handleSetActiveRequest(newRequest._id); }, }); } _workspaceDuplicate(callback) { const workspace = this.props.activeWorkspace; showPrompt({ title: 'Duplicate Workspace', defaultValue: workspace.name, submitName: 'Create', selectText: true, label: 'New Name', 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]) { // NOTE: We're caching promises here to avoid race conditions this._getRenderContextPromiseCache[contextCacheKey] = this._fetchRenderContext(); } // 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() { App._handleGenerateCode(this.props.activeRequest); } static _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); } static 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); } } static 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() { 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 }); await App._updateRequestMetaByParentId(activeRequestId, { lastActive: Date.now() }); } async _handleSetActiveEnvironment(activeEnvironmentId) { await this._updateActiveWorkspaceMeta({ activeEnvironmentId }); // Give it time to update and re-render setTimeout(() => { this._wrapper && this._wrapper._forceRequestPaneRefresh(); }, 300); } _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) { App._updateRequestGroupMetaByParentId(requestGroupId, { collapsed }); } async _handleSetRequestPinned(request, pinned) { App._updateRequestMetaByParentId(request._id, { pinned }); } _handleSetResponsePreviewMode(requestId, previewMode) { App._updateRequestMetaByParentId(requestId, { previewMode }); } _handleUpdateDownloadPath(requestId, downloadPath) { App._updateRequestMetaByParentId(requestId, { downloadPath }); } async _handleSetResponseFilter(requestId, responseFilter) { await App._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 App._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.setState({ forceRefreshHeaderCounter: this.state.forceRefreshHeaderCounter + 1 }); }, 500); 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 { settings, handleStartLoading, handleStopLoading } = this.props; 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 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 && 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, settings.maxHistoryResponses); }); readStream.on('error', async err => { console.warn('Failed to download request after sending', responsePatch.bodyPath, err); await models.response.create(responsePatch, settings.maxHistoryResponses); }); } else { // Save the bad responses so failures are shown still await models.response.create(responsePatch, settings.maxHistoryResponses); } } 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);
}
_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.state.isMigratingChildren) {
return;
}
// Prevent rendering of everything
this.setState({ isMigratingChildren: true }, async () => {
const flushId = await db.bufferChanges();
await models.environment.getOrCreateForWorkspace(activeWorkspace);
await models.cookieJar.getOrCreateForParentId(activeWorkspace._id);
await db.flushChanges(flushId);
this.setState({ isMigratingChildren: false });
});
}
// eslint-disable-next-line camelcase
UNSAFE_componentWillReceiveProps(nextProps) {
this._ensureWorkspaceChildren(nextProps);
// Update VCS if needed
const { activeWorkspace } = this.props;
if (nextProps.activeWorkspace._id !== activeWorkspace._id) {
this._updateVCS(nextProps.activeWorkspace);
}
}
// eslint-disable-next-line camelcase
UNSAFE_componentWillMount() {
this._ensureWorkspaceChildren(this.props);
}
render() {
if (this.state.isMigratingChildren) {
console.log('[app] Waiting for migration to complete');
return null;
}
const { activeWorkspace } = this.props;
const {
paneWidth,
paneHeight,
sidebarWidth,
isVariableUncovered,
vcs,
forceRefreshCounter,
forceRefreshHeaderCounter,
} = this.state;
const uniquenessKey = `${forceRefreshCounter}::${activeWorkspace._id}`;
return (