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 {trackEvent} from '../../common/analytics'; 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'; @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 }; 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_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); }] ]; } 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; } _isDragging () { return ( this.state.draggingPaneHorizontal || this.state.draggingPaneVertical || this.state.draggingSidebar ); } _requestGroupCreate (parentId) { showPrompt({ title: 'New Folder', defaultValue: 'My Folder', submitName: 'Create', label: 'Name', selectText: true, onComplete: name => { models.requestGroup.create({parentId, name}); } }); } _requestCreate (parentId) { showModal(RequestCreateModal, { parentId, onComplete: request => { this._handleSetActiveRequest(request._id); } }); } async _requestGroupDuplicate (requestGroup) { models.requestGroup.duplicate(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} = this.props; const environmentId = activeEnvironment ? activeEnvironment._id : null; return render.getRenderContext(activeRequest, environmentId, null); } 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); } } _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._forceRequestPaneRefresh, 300); return newRequest; } 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) { trackEvent('Request', 'Send and Download'); 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}`; const filename = path.join(dir, name); const to = fs.createWriteStream(filename); const readStream = models.response.getBodyStream(responsePatch); if (!readStream) { return; } readStream.pipe(to); readStream.on('end', async () => { trackEvent('Response', 'Download After Save Success'); 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); trackEvent('Response', 'Download After Save Failed'); 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 (