Refactor/wrapper-tsx-fc (#5242)

* wrapper-fc squashed

* undrill setFilter

* extract hooks

* eliminate wrapper.tsx
This commit is contained in:
Jack Kavanagh authored and GitHub committed 2022-10-04 21:33:18 +00:00
1 parent edad158d65
commit 898d24c4ba
19 files changed
+719 -867

No files matched your search

@@ -5,7 +5,7 @@ import styled from 'styled-components';
import type { GlobalActivity } from '../../../common/constants';
import { ACTIVITY_DEBUG, ACTIVITY_SPEC, ACTIVITY_UNIT_TEST } from '../../../common/constants';
import { isDesign, Workspace } from '../../../models/workspace';
import { HandleActivityChange } from '../wrapper';
import { HandleActivityChange } from '../workspace-page-header';
const StyledNav = styled.nav({
display: 'flex',
@@ -2,9 +2,10 @@ import { EnvironmentHighlightColorStyle } from 'insomnia-common';
import React, { FC, useCallback, useRef } from 'react';
import { useSelector } from 'react-redux';
import * as models from '../../../models';
import type { Environment } from '../../../models/environment';
import type { Workspace } from '../../../models/workspace';
import { selectEnvironments, selectHotKeyRegistry } from '../../redux/selectors';
import { selectActiveWorkspaceMeta, selectEnvironments, selectHotKeyRegistry } from '../../redux/selectors';
import { type DropdownHandle, Dropdown } from '../base/dropdown/dropdown';
import { DropdownButton } from '../base/dropdown/dropdown-button';
import { DropdownDivider } from '../base/dropdown/dropdown-divider';
@@ -18,18 +19,17 @@ import { Tooltip } from '../tooltip';
interface Props {
activeEnvironment?: Environment | null;
environmentHighlightColorStyle: EnvironmentHighlightColorStyle;
handleSetActiveEnvironment: (id: string | null) => void;
workspace: Workspace;
}
export const EnvironmentsDropdown: FC<Props> = ({
activeEnvironment,
environmentHighlightColorStyle,
handleSetActiveEnvironment,
workspace,
}) => {
const environments = useSelector(selectEnvironments);
const hotKeyRegistry = useSelector(selectHotKeyRegistry);
const activeWorkspaceMeta = useSelector(selectActiveWorkspaceMeta);
const dropdownRef = useRef<DropdownHandle>(null);
const handleShowEnvironmentModal = useCallback(() => {
showModal(WorkspaceEnvironmentsEditModal, workspace);
@@ -48,7 +48,7 @@ export const EnvironmentsDropdown: FC<Props> = ({
const subEnvironments = environments
.filter(environment => environment.parentId === (baseEnvironment && baseEnvironment._id))
.sort((e1, e2) => e1.metaSortKey - e2.metaSortKey);
const description = (!activeEnvironment || activeEnvironment === baseEnvironment) ? 'No Environment' : activeEnvironment.name;
const description = (!activeEnvironment || activeEnvironment === baseEnvironment) ? 'No Environment' : activeEnvironment.name;
return (
<Dropdown ref={dropdownRef}>
@@ -82,8 +82,11 @@ export const EnvironmentsDropdown: FC<Props> = ({
{subEnvironments.map(environment => (
<DropdownItem
key={environment._id}
value={environment._id}
onClick={handleSetActiveEnvironment}
onClick={() => {
if (activeWorkspaceMeta) {
models.workspaceMeta.update(activeWorkspaceMeta, { activeEnvironmentId: environment._id });
}
}}
>
<i
className="fa fa-random"
@@ -95,7 +98,13 @@ export const EnvironmentsDropdown: FC<Props> = ({
</DropdownItem>
))}
<DropdownItem value={null} onClick={handleSetActiveEnvironment}>
<DropdownItem
onClick={() => {
if (activeWorkspaceMeta) {
models.workspaceMeta.update(activeWorkspaceMeta, { activeEnvironmentId: null });
}
}}
>
<i className="fa fa-empty" /> No Environment
</DropdownItem>
@@ -29,7 +29,6 @@ import { GitLogModal } from '../modals/git-log-modal';
import { GitStagingModal } from '../modals/git-staging-modal';
type Props = ReturnType<typeof mapDispatchToProps> & {
handleGitBranchChanged: (branch: string) => void;
workspace: Workspace;
vcs: GitVCS;
gitRepository: GitRepository | null;
@@ -64,7 +63,7 @@ class GitSyncDropdown extends PureComponent<Props, State> {
}
async _refreshState(otherState?: Record<string, any>) {
const { vcs, workspace, handleGitBranchChanged } = this.props;
const { vcs, workspace } = this.props;
const workspaceMeta = await models.workspaceMeta.getOrCreateByParentId(workspace._id);
// Clear cached items and return if no state
@@ -101,7 +100,6 @@ class GitSyncDropdown extends PureComponent<Props, State> {
cachedGitLastAuthor,
cachedGitLastCommitTime,
});
handleGitBranchChanged(branch);
}
async _handleOpen() {
@@ -20,7 +20,6 @@ import { PromptButton } from '../base/prompt-button';
type Props = ReturnType<typeof mapDispatchToProps> & {
vcs: GitVCS;
gitRepository: GitRepository;
handleGitBranchChanged: (branch: string) => void;
};
interface State {
@@ -75,7 +74,7 @@ export class GitBranchesModalClass extends PureComponent<Props, State> {
}
async _refreshState(newState?: Record<string, any>) {
const { vcs, handleGitBranchChanged } = this.props;
const { vcs } = this.props;
const branch = await vcs.getBranch();
const branches = await vcs.listBranches();
const remoteBranches = await vcs.listRemoteBranches();
@@ -85,7 +84,6 @@ export class GitBranchesModalClass extends PureComponent<Props, State> {
remoteBranches,
...newState,
});
handleGitBranchChanged(branch);
}
_handleClearError() {
@@ -52,7 +52,6 @@ const TabPanelBody = styled.div({
interface Props {
environmentId: string;
handleImport: Function;
request?: Request | null;
settings: Settings;
workspace: Workspace;
@@ -60,7 +59,6 @@ interface Props {
export const RequestPane: FC<Props> = ({
environmentId,
handleImport,
request,
settings,
workspace,
@@ -172,7 +170,6 @@ export const RequestPane: FC<Props> = ({
uniquenessKey={uniqueKey}
onUrlChange={updateRequestUrl}
handleAutocompleteUrls={autocompleteUrls}
handleImport={handleImport}
nunjucksPowerUserMode={settings.nunjucksPowerUserMode}
request={request}
/>
@@ -13,6 +13,7 @@ import * as models from '../../../models';
import type { Request } from '../../../models/request';
import type { Response } from '../../../models/response';
import { cancelRequestById } from '../../../network/network';
import { updateRequestMetaByParentId } from '../../hooks/create-request';
import { selectActiveResponse, selectLoadStartTime, selectResponseFilter, selectResponseFilterHistory, selectResponsePreviewMode, selectSettings } from '../../redux/selectors';
import { Button } from '../base/button';
import { PreviewModeDropdown } from '../dropdowns/preview-mode-dropdown';
@@ -32,11 +33,9 @@ import { Pane, paneBodyClasses, PaneHeader } from './pane';
import { PlaceholderResponsePane } from './placeholder-response-pane';
interface Props {
handleSetFilter: (filter: string) => void;
request?: Request | null;
}
export const ResponsePane: FC<Props> = ({
handleSetFilter,
request,
}) => {
const response = useSelector(selectActiveResponse) as Response | null;
@@ -45,7 +44,24 @@ export const ResponsePane: FC<Props> = ({
const settings = useSelector(selectSettings);
const loadStartTime = useSelector(selectLoadStartTime);
const previewMode = useSelector(selectResponsePreviewMode);
const handleSetFilter = async (responseFilter: string) => {
if (!response) {
return;
}
const requestId = response.parentId;
await updateRequestMetaByParentId(requestId, { responseFilter });
const meta = await models.requestMeta.getByParentId(requestId);
if (!meta) {
return;
}
const responseFilterHistory = meta.responseFilterHistory.slice(0, 10);
// Already in history or empty?
if (!responseFilter || responseFilterHistory.includes(responseFilter)) {
return;
}
responseFilterHistory.unshift(responseFilter);
updateRequestMetaByParentId(requestId, { responseFilterHistory });
};
const handleGetResponseBody = useCallback(() => {
if (!response) {
return null;
@@ -58,7 +74,6 @@ export const ResponsePane: FC<Props> = ({
clipboard.writeText(bodyBuffer.toString('utf8'));
}
}, [handleGetResponseBody]);
const handleDownloadResponseBody = useCallback(async (prettify: boolean) => {
if (!response || !request) {
console.warn('Nothing to download');
@@ -1,5 +1,6 @@
import type { SaveDialogOptions } from 'electron';
import fs from 'fs';
import * as importers from 'insomnia-importers';
import { extension as mimeExtension } from 'mime-types';
import path from 'path';
import React, { forwardRef, useCallback, useImperativeHandle, useRef, useState } from 'react';
@@ -7,15 +8,16 @@ import { useDispatch, useSelector } from 'react-redux';
import { useInterval } from 'react-use';
import { SegmentEvent, trackSegmentEvent } from '../../common/analytics';
import { database } from '../../common/database';
import { getContentDispositionHeader } from '../../common/misc';
import * as models from '../../models';
import { update } from '../../models/helpers/request-operations';
import type { Request } from '../../models/request';
import { isRequest, Request } from '../../models/request';
import * as network from '../../network/network';
import { updateRequestMetaByParentId } from '../hooks/create-request';
import { useTimeoutWhen } from '../hooks/useTimeoutWhen';
import { loadRequestStart, loadRequestStop } from '../redux/modules/global';
import { selectActiveEnvironment, selectHotKeyRegistry, selectResponseDownloadPath, selectSettings } from '../redux/selectors';
import { selectActiveEnvironment, selectActiveRequest, selectHotKeyRegistry, selectResponseDownloadPath, selectSettings } from '../redux/selectors';
import { type DropdownHandle, Dropdown } from './base/dropdown/dropdown';
import { DropdownButton } from './base/dropdown/dropdown-button';
import { DropdownDivider } from './base/dropdown/dropdown-divider';
@@ -31,7 +33,6 @@ import { RequestRenderErrorModal } from './modals/request-render-error-modal';
interface Props {
handleAutocompleteUrls: () => Promise<string[]>;
handleImport: Function;
nunjucksPowerUserMode: boolean;
onUrlChange: (r: Request, url: string) => Promise<Request>;
request: Request;
@@ -44,7 +45,6 @@ export interface RequestUrlBarHandle {
export const RequestUrlBar = forwardRef<RequestUrlBarHandle, Props>(({
handleAutocompleteUrls,
handleImport,
onUrlChange,
request,
uniquenessKey,
@@ -52,6 +52,7 @@ export const RequestUrlBar = forwardRef<RequestUrlBarHandle, Props>(({
const downloadPath = useSelector(selectResponseDownloadPath);
const hotKeyRegistry = useSelector(selectHotKeyRegistry);
const activeEnvironment = useSelector(selectActiveEnvironment);
const activeRequest = useSelector(selectActiveRequest);
const settings = useSelector(selectSettings);
const dispatch = useDispatch();
const methodDropdownRef = useRef<DropdownHandle>(null);
@@ -302,6 +303,36 @@ export const RequestUrlBar = forwardRef<RequestUrlBarHandle, Props>(({
});
const [lastPastedText, setLastPastedText] = useState<string>();
const handleImport = useCallback(async (text: string) => {
// Allow user to paste any import file into the url. If it results in
// only one item, it will overwrite the current request.
try {
const { data } = await importers.convert(text);
const { resources } = data;
const r = resources[0];
if (r && r._type === 'request' && activeRequest && isRequest(activeRequest)) {
// Only pull fields that we want to update
return database.update({
...activeRequest,
modified: Date.now(),
url: r.url,
method: r.method,
headers: r.headers,
body: r.body,
authentication: r.authentication,
parameters: r.parameters,
},
// Pass true to indicate that this is an import
true
);
}
} catch (error) {
// Import failed, that's alright
console.error(error);
}
return null;
}, [activeRequest]);
const handleUrlChange = useCallback(async (url: string) => {
const pastedText = lastPastedText;
// If no pasted text in the queue, just fire the regular change handler
@@ -1,37 +1,113 @@
import React, { FunctionComponent, ReactNode, useCallback } from 'react';
import { useSelector } from 'react-redux';
import React, { FC, useCallback } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { ACTIVITY_HOME } from '../../common/constants';
import { selectActiveActivity, selectActiveApiSpec, selectActiveProjectName, selectActiveWorkspace } from '../redux/selectors';
import { ACTIVITY_DEBUG, ACTIVITY_HOME, ACTIVITY_SPEC, ACTIVITY_UNIT_TEST, GlobalActivity } from '../../common/constants';
import { importRaw } from '../../common/import';
import { initializeSpectral, isLintError } from '../../common/spectral';
import * as models from '../../models';
import { isRemoteProject } from '../../models/project';
import { isCollection, isDesign } from '../../models/workspace';
import { useGitVCS } from '../hooks/use-git-vcs';
import { useVCS } from '../hooks/use-vcs';
import { setActiveActivity } from '../redux/modules/global';
import { selectActiveActivity, selectActiveApiSpec, selectActiveGitRepository, selectActiveProject, selectActiveProjectName, selectActiveWorkspace, selectIsLoggedIn } from '../redux/selectors';
import { ActivityToggle } from './activity-toggle/activity-toggle';
import { AppHeader } from './app-header';
import { DropdownButton } from './base/dropdown/dropdown-button';
import GitSyncDropdown from './dropdowns/git-sync-dropdown';
import { SyncDropdown } from './dropdowns/sync-dropdown';
import { WorkspaceDropdown } from './dropdowns/workspace-dropdown';
import { HandleActivityChange } from './wrapper';
import { showModal } from './modals';
import { AlertModal } from './modals/alert-modal';
interface Props {
handleActivityChange: HandleActivityChange;
gridRight: ReactNode;
}
export type HandleActivityChange = (options: {
workspaceId?: string;
nextActivity: GlobalActivity;
}) => Promise<void>;
export const WorkspacePageHeader: FunctionComponent<Props> = ({
gridRight,
handleActivityChange,
}) => {
// TODO(jackkav): find the right place to initializeSpectral
const spectral = initializeSpectral();
export const WorkspacePageHeader: FC = () => {
const activeApiSpec = useSelector(selectActiveApiSpec);
const activeWorkspace = useSelector(selectActiveWorkspace);
const activity = useSelector(selectActiveActivity);
const activeActivity = useSelector(selectActiveActivity);
const dispatch = useDispatch();
const handleActivityChange = useCallback(async ({ workspaceId, nextActivity }) => {
// Remember last activity on workspace for later, but only if it isn't HOME
if (workspaceId && nextActivity !== ACTIVITY_HOME) {
await models.workspaceMeta.updateByParentId(workspaceId, {
activeActivity: nextActivity,
});
}
const editingASpec = activeActivity === ACTIVITY_SPEC;
if (!editingASpec) {
dispatch(setActiveActivity(nextActivity));
return;
}
if (!activeApiSpec || !workspaceId) {
return;
}
const goingToDebugOrTest = nextActivity === ACTIVITY_DEBUG || nextActivity === ACTIVITY_UNIT_TEST;
// If editing a spec and not going to debug or test, don't regenerate anything
if (editingASpec && !goingToDebugOrTest) {
dispatch(setActiveActivity(nextActivity));
return;
}
// Handle switching away from the spec design activity. For this, we want to generate
// requests that can be accessed from debug or test.
// If there are errors in the spec, show the user a warning first
const results = (await spectral.run(activeApiSpec.contents)).filter(isLintError);
if (activeApiSpec.contents && results && results.length) {
showModal(AlertModal, {
title: 'Error Generating Configuration',
message: 'Some requests may not be available due to errors found in the specification. We recommend fixing errors before proceeding.',
okLabel: 'Proceed',
addCancel: true,
onConfirm: () => {
dispatch(setActiveActivity(nextActivity));
},
});
return;
}
// TODO(gatzjames): make import on navigation optional
// Delaying generation so design to debug mode is smooth
dispatch(setActiveActivity(nextActivity));
setTimeout(() => {
importRaw(activeApiSpec.contents, {
getWorkspaceId: () => Promise.resolve(workspaceId),
enableDiffBasedPatching: true,
enableDiffDeep: true,
bypassDiffProps: {
url: true,
},
});
}, 1000);
}, [activeActivity, activeApiSpec, dispatch]);
const homeCallback = useCallback(() => {
handleActivityChange({ workspaceId: activeWorkspace?._id, nextActivity: ACTIVITY_HOME });
}, [activeWorkspace, handleActivityChange]);
const activeProjectName = useSelector(selectActiveProjectName);
const activeProject = useSelector(selectActiveProject);
const isLoggedIn = useSelector(selectIsLoggedIn);
const activeGitRepository = useSelector(selectActiveGitRepository);
if (!activeWorkspace || !activeApiSpec || !activity) {
const vcs = useVCS({
workspaceId: activeWorkspace?._id,
});
const gitVCS = useGitVCS({
workspaceId: activeWorkspace?._id,
projectId: activeProject?._id,
gitRepository: activeGitRepository,
});
const isTeamSync = isLoggedIn && activeWorkspace && isCollection(activeWorkspace) && isRemoteProject(activeProject) && vcs;
const isGitSync = activeWorkspace && gitVCS && isDesign(activeWorkspace);
if (!activeWorkspace || !activeApiSpec || !activeActivity) {
return null;
}
const crumbs = [
{ id: 'project', node: activeProjectName, onClick: homeCallback },
{ id: 'project', node: activeProjectName, onClick: () => handleActivityChange({ workspaceId: activeWorkspace?._id, nextActivity: ACTIVITY_HOME }) },
{ id: 'workspace', node: <WorkspaceDropdown key="workspace-dd" /> },
];
@@ -41,11 +117,25 @@ export const WorkspacePageHeader: FunctionComponent<Props> = ({
gridCenter={
<ActivityToggle
workspace={activeWorkspace}
activity={activity}
activity={activeActivity}
handleActivityChange={handleActivityChange}
/>
}
gridRight={gridRight}
gridRight={isTeamSync ? <SyncDropdown
workspace={activeWorkspace}
project={activeProject}
vcs={vcs}
/> : isGitSync ? <GitSyncDropdown
className="margin-left"
workspace={activeWorkspace}
gitRepository={activeGitRepository}
vcs={gitVCS}
renderDropdownButton={children => (
<DropdownButton className="btn--clicky-small btn-sync">
{children}
</DropdownButton>
)}
/> : null}
/>
);
};
@@ -1,4 +1,4 @@
import React, { FC, Fragment, ReactNode, useEffect } from 'react';
import React, { FC, Fragment, useEffect } from 'react';
import { useSelector } from 'react-redux';
import { SegmentEvent, trackSegmentEvent } from '../../common/analytics';
@@ -6,25 +6,19 @@ import * as models from '../../models';
import { isGrpcRequest } from '../../models/grpc-request';
import { getByParentId as getGrpcRequestMetaByParentId } from '../../models/grpc-request-meta';
import * as requestOperations from '../../models/helpers/request-operations';
import { isRemoteProject } from '../../models/project';
import { getByParentId as getRequestMetaByParentId } from '../../models/request-meta';
import { isWebSocketRequest } from '../../models/websocket-request';
import { isCollection, isDesign } from '../../models/workspace';
import { VCS } from '../../sync/vcs/vcs';
import { updateRequestMetaByParentId } from '../hooks/create-request';
import { createRequestGroup } from '../hooks/create-request-group';
import {
selectActiveEnvironment,
selectActiveProject,
selectActiveRequest,
selectActiveWorkspace,
selectActiveWorkspaceMeta,
selectIsLoggedIn,
selectSettings,
} from '../redux/selectors';
import { selectSidebarFilter } from '../redux/sidebar-selectors';
import { EnvironmentsDropdown } from './dropdowns/environments-dropdown';
import { SyncDropdown } from './dropdowns/sync-dropdown';
import { ErrorBoundary } from './error-boundary';
import { useDocBodyKeyboardShortcuts } from './keydown-binder';
import { showModal } from './modals';
@@ -46,25 +40,8 @@ import { SidebarFilter } from './sidebar/sidebar-filter';
import { WebSocketRequestPane } from './websockets/websocket-request-pane';
import { WebSocketResponsePane } from './websockets/websocket-response-pane';
import { WorkspacePageHeader } from './workspace-page-header';
import type { HandleActivityChange } from './wrapper';
interface Props {
gitSyncDropdown: ReactNode;
handleActivityChange: HandleActivityChange;
handleSetActiveEnvironment: (id: string | null) => void;
handleImport: Function;
vcs: VCS | null;
}
export const WrapperDebug: FC<Props> = ({
gitSyncDropdown,
handleActivityChange,
handleSetActiveEnvironment,
handleImport,
vcs,
}) => {
const activeProject = useSelector(selectActiveProject);
const isLoggedIn = useSelector(selectIsLoggedIn);
export const WrapperDebug: FC = () => {
const activeEnvironment = useSelector(selectActiveEnvironment);
const activeRequest = useSelector(selectActiveRequest);
const activeWorkspace = useSelector(selectActiveWorkspace);
@@ -72,7 +49,6 @@ export const WrapperDebug: FC<Props> = ({
const sidebarFilter = useSelector(selectSidebarFilter);
const activeWorkspaceMeta = useSelector(selectActiveWorkspaceMeta);
const isTeamSync = isLoggedIn && activeWorkspace && isCollection(activeWorkspace) && isRemoteProject(activeProject) && vcs;
useDocBodyKeyboardShortcuts({
request_togglePin:
async () => {
@@ -177,53 +153,16 @@ export const WrapperDebug: FC<Props> = ({
};
}, [activeEnvironment?._id]);
async function handleSetResponseFilter(responseFilter: string) {
if (!activeRequest) {
return;
}
const requestId = activeRequest._id;
await updateRequestMetaByParentId(requestId, { responseFilter });
const meta = await models.requestMeta.getByParentId(requestId);
if (!meta) {
return;
}
const responseFilterHistory = meta.responseFilterHistory.slice(0, 10);
// Already in history?
if (responseFilterHistory.includes(responseFilter)) {
return;
}
// Blank?
if (!responseFilter) {
return;
}
responseFilterHistory.unshift(responseFilter);
await updateRequestMetaByParentId(requestId, {
responseFilterHistory,
});
}
return (
<PageLayout
renderPageHeader={activeWorkspace ?
<WorkspacePageHeader
handleActivityChange={handleActivityChange}
gridRight={isTeamSync ? <SyncDropdown
workspace={activeWorkspace}
project={activeProject}
vcs={vcs}
/> : isDesign(activeWorkspace) ? gitSyncDropdown : null}
/>
<WorkspacePageHeader />
: null}
renderPageSidebar={activeWorkspace ? <Fragment>
<div className="sidebar__menu">
<EnvironmentsDropdown
activeEnvironment={activeEnvironment}
environmentHighlightColorStyle={settings.environmentHighlightColorStyle}
handleSetActiveEnvironment={handleSetActiveEnvironment}
workspace={activeWorkspace}
/>
<button className="btn btn--super-compact" onClick={showCookiesModal}>
@@ -263,7 +202,6 @@ export const WrapperDebug: FC<Props> = ({
) : (
<RequestPane
environmentId={activeEnvironment ? activeEnvironment._id : ''}
handleImport={handleImport}
request={activeRequest}
settings={settings}
workspace={activeWorkspace}
@@ -278,19 +216,12 @@ export const WrapperDebug: FC<Props> = ({
<ErrorBoundary showAlert>
{activeRequest && (
isGrpcRequest(activeRequest) ? (
<GrpcResponsePane
activeRequest={activeRequest}
/>
<GrpcResponsePane activeRequest={activeRequest} />
) : (
isWebSocketRequest(activeRequest) ? (
<WebSocketResponsePane
requestId={activeRequest._id}
/>
<WebSocketResponsePane requestId={activeRequest._id} />
) : (
<ResponsePane
handleSetFilter={handleSetResponseFilter}
request={activeRequest}
/>
<ResponsePane request={activeRequest} />
)
)
)}
@@ -1,6 +1,6 @@
import { IRuleResult } from '@stoplight/spectral-core';
import { Notice, NoticeTable } from 'insomnia-components';
import React, { createRef, FC, ReactNode, RefObject, useCallback, useEffect, useMemo, useState } from 'react';
import React, { createRef, FC, RefObject, useCallback, useEffect, useMemo, useState } from 'react';
import { useSelector } from 'react-redux';
import styled from 'styled-components';
import SwaggerUI from 'swagger-ui-react';
@@ -19,7 +19,6 @@ import { ErrorBoundary } from './error-boundary';
import { PageLayout } from './page-layout';
import { SpecEditorSidebar } from './spec-editor/spec-editor-sidebar';
import { WorkspacePageHeader } from './workspace-page-header';
import type { HandleActivityChange } from './wrapper';
const EmptySpaceHelper = styled.div({
...superFaint,
@@ -30,6 +29,7 @@ const EmptySpaceHelper = styled.div({
textAlign: 'center',
});
// TODO(jackkav): find the right place to do this
const spectral = initializeSpectral();
interface LintMessage extends Notice {
@@ -48,7 +48,7 @@ const RenderEditor: FC<{ editor: RefObject<UnconnectedCodeEditor> }> = ({ editor
return;
}
await database.update({ ...activeApiSpec, modified: Date.now(), contents: value }, true);
await database.update({ ...activeApiSpec, modified: Date.now(), contents: value }, true);
}, [activeApiSpec]);
const uniquenessKey = `::${activeApiSpec?._id}::${gitVersion}::${syncVersion}`;
@@ -68,7 +68,7 @@ const RenderEditor: FC<{ editor: RefObject<UnconnectedCodeEditor> }> = ({ editor
useEffect(() => {
let isMounted = true;
const update = async () => {
// Lint only if spec has content
// Lint only if spec has content
if (contents && contents.length !== 0) {
const results: LintMessage[] = (await spectral.run(contents))
.filter(isLintError)
@@ -233,24 +233,13 @@ const RenderPageSidebar: FC<{ editor: RefObject<UnconnectedCodeEditor> }> = ({ e
);
};
interface Props {
gitSyncDropdown: ReactNode;
handleActivityChange: HandleActivityChange;
}
export const WrapperDesign: FC<Props> = ({
gitSyncDropdown,
handleActivityChange,
}) => {
export const WrapperDesign: FC = () => {
const editor = createRef<UnconnectedCodeEditor>();
return (
<PageLayout
renderPageHeader={(
<WorkspacePageHeader
handleActivityChange={handleActivityChange}
gridRight={gitSyncDropdown}
/>
<WorkspacePageHeader />
)}
renderPaneOne={<RenderEditor editor={editor} />}
renderPaneTwo={<RenderPreview />}
@@ -10,7 +10,7 @@ import {
} from 'insomnia-components';
import { generate, runTests, Test } from 'insomnia-testing';
import { isEmpty } from 'ramda';
import React, { FC, ReactNode, useCallback, useState } from 'react';
import React, { FC, useCallback, useState } from 'react';
import { useSelector } from 'react-redux';
import styled from 'styled-components';
@@ -34,7 +34,6 @@ import { EmptyStatePane } from './panes/empty-state-pane';
import type { Child } from './sidebar/sidebar-children';
import { UnitTestEditable } from './unit-test-editable';
import { WorkspacePageHeader } from './workspace-page-header';
import type { HandleActivityChange } from './wrapper';
const HeaderButton = styled(Button)({
'&&': {
@@ -42,15 +41,7 @@ const HeaderButton = styled(Button)({
},
});
interface Props {
gitSyncDropdown: ReactNode;
handleActivityChange: HandleActivityChange;
}
const WrapperUnitTest: FC<Props> = ({
gitSyncDropdown,
handleActivityChange,
}) => {
const WrapperUnitTest: FC = () => {
const [testsRunning, setTestsRunning] = useState<UnitTest[] | null>(null);
const [resultsError, setResultsError] = useState<string | null>(null);
const lintOptions = {
@@ -421,10 +412,7 @@ const WrapperUnitTest: FC<Props> = ({
<TestRunStatus testsRunning={testsRunning} resultsError={resultsError} />
}
renderPageHeader={
<WorkspacePageHeader
handleActivityChange={handleActivityChange}
gridRight={gitSyncDropdown}
/>
<WorkspacePageHeader />
}
/>
);
@@ -432,9 +420,9 @@ const WrapperUnitTest: FC<Props> = ({
export default WrapperUnitTest;
interface TestRunStatusProps{
testsRunning:UnitTest[] | null;
resultsError:string | null;
interface TestRunStatusProps {
testsRunning: UnitTest[] | null;
resultsError: string | null;
}
const TestRunStatus: FC<TestRunStatusProps> = ({ testsRunning, resultsError }) => {
const activeUnitTestResult = useSelector(selectActiveUnitTestResult);
@@ -1,465 +0,0 @@
import { autoBindMethodsForReact } from 'class-autobind-decorator';
import * as importers from 'insomnia-importers';
import React, { FC, Fragment, lazy, PureComponent, Suspense } from 'react';
import { connect, useSelector } from 'react-redux';
import { Route, Routes, useNavigate } from 'react-router-dom';
import { AnyAction, bindActionCreators, Dispatch } from 'redux';
import type { GlobalActivity } from '../../common/constants';
import {
ACTIVITY_DEBUG,
ACTIVITY_HOME,
ACTIVITY_SPEC,
ACTIVITY_UNIT_TEST,
AUTOBIND_CFG,
} from '../../common/constants';
import { database } from '../../common/database';
import { importRaw } from '../../common/import';
import { initializeSpectral, isLintError } from '../../common/spectral';
import * as models from '../../models/index';
import {
isRequest,
} from '../../models/request';
import { GitVCS } from '../../sync/git/git-vcs';
import { VCS } from '../../sync/vcs/vcs';
import { CookieModifyModal } from '../components/modals/cookie-modify-modal';
import { GrpcDispatchModalWrapper } from '../context/grpc';
import { RootState } from '../redux/modules';
import { setActiveActivity } from '../redux/modules/global';
import { selectActiveActivity, selectActiveApiSpec, selectActiveCookieJar, selectActiveEnvironment, selectActiveGitRepository, selectActiveRequest, selectActiveResponse, selectActiveWorkspace, selectActiveWorkspaceMeta, selectSettings } from '../redux/selectors';
import { DropdownButton } from './base/dropdown/dropdown-button';
import GitSyncDropdown from './dropdowns/git-sync-dropdown';
import { ErrorBoundary } from './error-boundary';
import { AddKeyCombinationModal } from './modals/add-key-combination-modal';
import { AlertModal } from './modals/alert-modal';
import { AnalyticsModal } from './modals/analytics-modal';
import { AskModal } from './modals/ask-modal';
import { CodePromptModal } from './modals/code-prompt-modal';
import { CookiesModalFC } from './modals/cookies-modal';
import { EnvironmentEditModal } from './modals/environment-edit-modal';
import { ErrorModal } from './modals/error-modal';
import { ExportRequestsModal } from './modals/export-requests-modal';
import { FilterHelpModal } from './modals/filter-help-modal';
import { GenerateCodeModal } from './modals/generate-code-modal';
import { GenerateConfigModal } from './modals/generate-config-modal';
import { GitBranchesModal } from './modals/git-branches-modal';
import { GitLogModal } from './modals/git-log-modal';
import { GitRepositorySettingsModal } from './modals/git-repository-settings-modal';
import { GitStagingModal } from './modals/git-staging-modal';
import { registerModal, showModal } from './modals/index';
import { LoginModal } from './modals/login-modal';
import { NunjucksModal } from './modals/nunjucks-modal';
import ProjectSettingsModal from './modals/project-settings-modal';
import { PromptModal } from './modals/prompt-modal';
import ProtoFilesModal from './modals/proto-files-modal';
import { RequestGroupSettingsModal } from './modals/request-group-settings-modal';
import { RequestRenderErrorModal } from './modals/request-render-error-modal';
import { RequestSettingsModal } from './modals/request-settings-modal';
import { RequestSwitcherModal } from './modals/request-switcher-modal';
import { ResponseDebugModal } from './modals/response-debug-modal';
import { SelectModal } from './modals/select-modal';
import { SettingsModal } from './modals/settings-modal';
import { SyncBranchesModal } from './modals/sync-branches-modal';
import { SyncDeleteModal } from './modals/sync-delete-modal';
import { SyncHistoryModal } from './modals/sync-history-modal';
import { SyncMergeModal } from './modals/sync-merge-modal';
import { SyncStagingModal } from './modals/sync-staging-modal';
import { WorkspaceDuplicateModal } from './modals/workspace-duplicate-modal';
import { WorkspaceEnvironmentsEditModal } from './modals/workspace-environments-edit-modal';
import { WorkspaceSettingsModal } from './modals/workspace-settings-modal';
import { WrapperModal } from './modals/wrapper-modal';
const lazyWithPreload = <T extends FC<any>>(
importFn: () => Promise<{ default: T }>
) => {
const LazyComponent = lazy(importFn);
const preload = () => importFn();
return [LazyComponent, preload] as const;
};
const [WrapperHome, preloadWrapperHome] = lazyWithPreload(
() => import('./wrapper-home')
);
const [WrapperDebug, preloadWrapperDebug] = lazyWithPreload(
() => import('./wrapper-debug')
);
const [WrapperDesign, preloadWrapperDesign] = lazyWithPreload(
() => import('./wrapper-design')
);
const [WrapperUnitTest, preloadWrapperUnitTest] = lazyWithPreload(
() => import('./wrapper-unit-test')
);
preloadWrapperHome();
preloadWrapperDebug();
preloadWrapperDesign();
preloadWrapperUnitTest();
const LoadingIndicator = () => (<div
id="app-loading-indicator"
style={{
position: 'fixed',
top: '0',
left: '0',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
height: '100%',
width: '100%',
}}
>
<img src="./ui/images/insomnia-logo.svg" alt="Insomnia" />
</div>);
const ActivityRouter = () => {
const selectedActivity = useSelector(selectActiveActivity);
const activeWorkspace = useSelector(selectActiveWorkspace);
// If there is no active workspace, we want to navigate to home no matter what the previous activity was
const activity = activeWorkspace ? selectedActivity : ACTIVITY_HOME;
const navigate = useNavigate();
React.useEffect(() => {
if (activity) {
navigate(activity);
}
}, [activity, navigate]);
return null;
};
const spectral = initializeSpectral();
export type Props = ReturnType<typeof mapStateToProps> & ReturnType<typeof mapDispatchToProps> & {
vcs: VCS | null;
gitVCS: GitVCS | null;
};
export type HandleActivityChange = (options: {
workspaceId?: string;
nextActivity: GlobalActivity;
}) => Promise<void>;
interface State {
activeGitBranch: string;
}
@autoBindMethodsForReact(AUTOBIND_CFG)
export class WrapperClass extends PureComponent<Props, State> {
state: State = {
activeGitBranch: 'no-vcs',
};
async _handleImport(text: string) {
const { activeRequest } = this.props;
// Allow user to paste any import file into the url. If it results in
// only one item, it will overwrite the current request.
try {
const { data } = await importers.convert(text);
const { resources } = data;
const r = resources[0];
if (r && r._type === 'request' && activeRequest && isRequest(activeRequest)) {
// Only pull fields that we want to update
return database.update({
...activeRequest,
modified: Date.now(),
url: r.url,
method: r.method,
headers: r.headers,
body: r.body,
authentication: r.authentication,
parameters: r.parameters,
},
// Pass true to indicate that this is an import
true
);
}
} catch (error) {
// Import failed, that's alright
}
return null;
}
async _handleWorkspaceActivityChange({ workspaceId, nextActivity }: Parameters<HandleActivityChange>[0]): ReturnType<HandleActivityChange> {
const { activeActivity, activeApiSpec, handleSetActiveActivity } = this.props;
// Remember last activity on workspace for later, but only if it isn't HOME
if (workspaceId && nextActivity !== ACTIVITY_HOME) {
await models.workspaceMeta.updateByParentId(workspaceId, {
activeActivity: nextActivity,
});
}
const editingASpec = activeActivity === ACTIVITY_SPEC;
if (!editingASpec) {
handleSetActiveActivity(nextActivity);
return;
}
if (!activeApiSpec || !workspaceId) {
return;
}
const goingToDebugOrTest = nextActivity === ACTIVITY_DEBUG || nextActivity === ACTIVITY_UNIT_TEST;
// If editing a spec and not going to debug or test, don't regenerate anything
if (editingASpec && !goingToDebugOrTest) {
handleSetActiveActivity(nextActivity);
return;
}
// Handle switching away from the spec design activity. For this, we want to generate
// requests that can be accessed from debug or test.
// If there are errors in the spec, show the user a warning first
const results = (await spectral.run(activeApiSpec.contents)).filter(isLintError);
if (activeApiSpec.contents && results && results.length) {
showModal(AlertModal, {
title: 'Error Generating Configuration',
message: 'Some requests may not be available due to errors found in the specification. We recommend fixing errors before proceeding.',
okLabel: 'Proceed',
addCancel: true,
onConfirm: () => {
handleSetActiveActivity(nextActivity);
},
});
return;
}
// Delaying generation so design to debug mode is smooth
handleSetActiveActivity(nextActivity);
setTimeout(() => {
importRaw(activeApiSpec.contents, {
getWorkspaceId: () => Promise.resolve(workspaceId),
enableDiffBasedPatching: true,
enableDiffDeep: true,
bypassDiffProps: {
url: true,
},
});
}, 1000);
}
_handleGitBranchChanged(branch: string) {
this.setState({
activeGitBranch: branch || 'no-vcs',
});
}
async _handleSetActiveEnvironment(activeEnvironmentId: string | null) {
if (this.props.activeWorkspaceMeta) {
await models.workspaceMeta.update(this.props.activeWorkspaceMeta, { activeEnvironmentId });
}
}
render() {
const {
activeCookieJar,
activeEnvironment,
activeGitRepository,
activeWorkspace,
activeApiSpec,
gitVCS,
vcs,
} = this.props;
// Setup git sync dropdown for use in Design/Debug pages
let gitSyncDropdown: JSX.Element | null = null;
if (activeWorkspace && gitVCS) {
gitSyncDropdown = (
<GitSyncDropdown
className="margin-left"
workspace={activeWorkspace}
gitRepository={activeGitRepository}
vcs={gitVCS}
handleGitBranchChanged={this._handleGitBranchChanged}
renderDropdownButton={children => (
<DropdownButton className="btn--clicky-small btn-sync">
{children}
</DropdownButton>
)}
/>
);
}
return (
<Fragment>
<div key="modals" className="modals">
<ErrorBoundary showAlert>
<AnalyticsModal />
<AlertModal ref={registerModal} />
<ErrorModal ref={registerModal} />
<PromptModal ref={registerModal} />
<WrapperModal ref={registerModal} />
<LoginModal ref={registerModal} />
<AskModal ref={registerModal} />
<SelectModal ref={registerModal} />
<FilterHelpModal ref={registerModal} />
<RequestRenderErrorModal ref={registerModal} />
<GenerateConfigModal ref={registerModal} />
<ProjectSettingsModal ref={registerModal} />
<WorkspaceDuplicateModal ref={registerModal} vcs={vcs || undefined} />
<CodePromptModal ref={registerModal} />
<RequestSettingsModal ref={instance => registerModal(instance, 'RequestSettingsModal')} />
<RequestGroupSettingsModal ref={registerModal} />
{activeWorkspace ? <>
{/* TODO: Figure out why cookieJar is sometimes null */}
{activeCookieJar ? <>
<CookiesModalFC
ref={registerModal}
/>
<CookieModifyModal ref={registerModal} />
</> : null}
<NunjucksModal
ref={registerModal}
workspace={activeWorkspace}
/>
{activeApiSpec ? <WorkspaceSettingsModal
ref={registerModal}
workspace={activeWorkspace}
apiSpec={activeApiSpec}
/> : null}
</> : null}
<GenerateCodeModal
ref={registerModal}
environmentId={activeEnvironment ? activeEnvironment._id : 'n/a'}
/>
<SettingsModal ref={instance => registerModal(instance, 'SettingsModal')} />
<ResponseDebugModal ref={instance => registerModal(instance, 'ResponseDebugModal')} />
<RequestSwitcherModal ref={instance => registerModal(instance, 'RequestSwitcherModal')} />
<EnvironmentEditModal
ref={registerModal}
onChange={models.requestGroup.update}
/>
<GitRepositorySettingsModal ref={registerModal} />
{activeWorkspace && gitVCS ? (
<Fragment>
<GitStagingModal ref={registerModal} workspace={activeWorkspace} vcs={gitVCS} gitRepository={activeGitRepository} />
<GitLogModal ref={registerModal} vcs={gitVCS} />
{activeGitRepository !== null && (
<GitBranchesModal
ref={registerModal}
vcs={gitVCS}
gitRepository={activeGitRepository}
handleGitBranchChanged={this._handleGitBranchChanged}
/>
)}
</Fragment>
) : null}
{activeWorkspace && vcs ? (
<Fragment>
<SyncStagingModal ref={registerModal} vcs={vcs} />
<SyncMergeModal ref={registerModal} vcs={vcs} />
<SyncBranchesModal ref={registerModal} vcs={vcs} />
<SyncDeleteModal ref={registerModal} vcs={vcs} />
<SyncHistoryModal ref={registerModal} vcs={vcs} />
</Fragment>
) : null}
<WorkspaceEnvironmentsEditModal
ref={registerModal}
handleSetActiveEnvironment={this._handleSetActiveEnvironment}
activeEnvironmentId={activeEnvironment ? activeEnvironment._id : null}
/>
<AddKeyCombinationModal ref={instance => registerModal(instance, 'AddKeyCombinationModal')} />
<ExportRequestsModal ref={registerModal} />
<GrpcDispatchModalWrapper>
{dispatch => (
<ProtoFilesModal
ref={registerModal}
grpcDispatch={dispatch}
/>
)}
</GrpcDispatchModalWrapper>
</ErrorBoundary>
</div>
<Routes>
<Route
path="*"
element={
<Suspense fallback={<LoadingIndicator />}>
<WrapperHome
vcs={vcs}
/>
</Suspense>
}
/>
<Route
path={ACTIVITY_UNIT_TEST}
element={
<Suspense fallback={<LoadingIndicator />}>
<WrapperUnitTest
gitSyncDropdown={gitSyncDropdown}
handleActivityChange={this._handleWorkspaceActivityChange}
/>
</Suspense>
}
/>
<Route
path={ACTIVITY_SPEC}
element={
<Suspense fallback={<LoadingIndicator />}>
<WrapperDesign
gitSyncDropdown={gitSyncDropdown}
handleActivityChange={this._handleWorkspaceActivityChange}
/>
</Suspense>
}
/>
<Route
path={ACTIVITY_DEBUG}
element={
<Suspense fallback={<LoadingIndicator />}>
<WrapperDebug
gitSyncDropdown={gitSyncDropdown}
handleActivityChange={this._handleWorkspaceActivityChange}
handleSetActiveEnvironment={this._handleSetActiveEnvironment}
handleImport={this._handleImport}
vcs={vcs}
/>
</Suspense>
}
/>
</Routes>
<ActivityRouter />
</Fragment>
);
}
}
const mapStateToProps = (state: RootState) => ({
activeActivity: selectActiveActivity(state),
activeRequest: selectActiveRequest(state),
activeCookieJar: selectActiveCookieJar(state),
activeEnvironment: selectActiveEnvironment(state),
activeGitRepository: selectActiveGitRepository(state),
activeWorkspace: selectActiveWorkspace(state),
activeWorkspaceMeta: selectActiveWorkspaceMeta(state),
activeApiSpec: selectActiveApiSpec(state),
activeResponse: selectActiveResponse(state),
settings: selectSettings(state),
});
const mapDispatchToProps = (dispatch: Dispatch<AnyAction>) => {
const bound = bindActionCreators({ setActiveActivity }, dispatch);
return {
handleSetActiveActivity: bound.setActiveActivity,
};
};
export const Wrapper = connect(mapStateToProps, mapDispatchToProps, null, { forwardRef: true })(WrapperClass);
@@ -1,12 +1,24 @@
import { FC } from 'react';
import { ipcRenderer } from 'electron/renderer';
import { FC, useEffect } from 'react';
import { useAppCommands } from '../hooks/use-app-commands';
import { useDragAndDropImportFile } from '../hooks/use-drag-and-drop-import-file';
import { useGlobalKeyboardShortcuts } from '../hooks/use-global-keyboard-shortcuts';
import { useSettingsSideEffects } from '../hooks/use-settings-side-effects';
import { useSyncMigration } from '../hooks/use-sync-migration';
import { useThemeChange } from '../hooks/use-theme-change';
export const AppHooks: FC = () => {
useSyncMigration();
useSettingsSideEffects();
useGlobalKeyboardShortcuts();
useDragAndDropImportFile();
useThemeChange();
useAppCommands();
// Give it a bit before letting the backend know it's ready
useEffect(() => {
setTimeout(() => ipcRenderer.send('window-ready'), 500);
}, []);
return null;
};
+259 -245
View File
@@ -1,40 +1,66 @@
import { ipcRenderer } from 'electron';
import path from 'path';
import React, { useEffect, useRef, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { parse as urlParse } from 'url';
import React, { FC, Fragment, lazy, Suspense, useEffect, useState } from 'react';
import { useSelector } from 'react-redux';
import { Route, Routes, useNavigate } from 'react-router-dom';
import {
ACTIVITY_DEBUG,
ACTIVITY_HOME,
ACTIVITY_SPEC,
ACTIVITY_UNIT_TEST,
} from '../../common/constants';
import { database as db } from '../../common/database';
import { getDataDirectory } from '../../common/electron-helpers';
import {
generateId,
} from '../../common/misc';
import * as models from '../../models';
import { GitRepository } from '../../models/git-repository';
import * as themes from '../../plugins/misc';
import { fsClient } from '../../sync/git/fs-client';
import { GIT_CLONE_DIR, GIT_INSOMNIA_DIR, GIT_INTERNAL_DIR, GitVCS } from '../../sync/git/git-vcs';
import { NeDBClient } from '../../sync/git/ne-db-client';
import { routableFSClient } from '../../sync/git/routable-fs-client';
import FileSystemDriver from '../../sync/store/drivers/file-system-driver';
import { type MergeConflict } from '../../sync/types';
import { VCS } from '../../sync/vcs/vcs';
import { ErrorBoundary } from '../components/error-boundary';
import { AddKeyCombinationModal } from '../components/modals/add-key-combination-modal';
import { AlertModal } from '../components/modals/alert-modal';
import { showModal } from '../components/modals/index';
import { AnalyticsModal } from '../components/modals/analytics-modal';
import { AskModal } from '../components/modals/ask-modal';
import { CodePromptModal } from '../components/modals/code-prompt-modal';
import { CookieModifyModal } from '../components/modals/cookie-modify-modal';
import { CookiesModalFC } from '../components/modals/cookies-modal';
import { EnvironmentEditModal } from '../components/modals/environment-edit-modal';
import { ErrorModal } from '../components/modals/error-modal';
import { ExportRequestsModal } from '../components/modals/export-requests-modal';
import { FilterHelpModal } from '../components/modals/filter-help-modal';
import { GenerateCodeModal } from '../components/modals/generate-code-modal';
import { GenerateConfigModal } from '../components/modals/generate-config-modal';
import { GitBranchesModal } from '../components/modals/git-branches-modal';
import { GitLogModal } from '../components/modals/git-log-modal';
import { GitRepositorySettingsModal } from '../components/modals/git-repository-settings-modal';
import { GitStagingModal } from '../components/modals/git-staging-modal';
import { registerModal } from '../components/modals/index';
import { LoginModal } from '../components/modals/login-modal';
import { NunjucksModal } from '../components/modals/nunjucks-modal';
import ProjectSettingsModal from '../components/modals/project-settings-modal';
import { PromptModal } from '../components/modals/prompt-modal';
import ProtoFilesModal from '../components/modals/proto-files-modal';
import { RequestGroupSettingsModal } from '../components/modals/request-group-settings-modal';
import { RequestRenderErrorModal } from '../components/modals/request-render-error-modal';
import { RequestSettingsModal } from '../components/modals/request-settings-modal';
import { RequestSwitcherModal } from '../components/modals/request-switcher-modal';
import { ResponseDebugModal } from '../components/modals/response-debug-modal';
import { SelectModal } from '../components/modals/select-modal';
import { SettingsModal } from '../components/modals/settings-modal';
import { SyncBranchesModal } from '../components/modals/sync-branches-modal';
import { SyncDeleteModal } from '../components/modals/sync-delete-modal';
import { SyncHistoryModal } from '../components/modals/sync-history-modal';
import { SyncMergeModal } from '../components/modals/sync-merge-modal';
import { SyncStagingModal } from '../components/modals/sync-staging-modal';
import { WorkspaceDuplicateModal } from '../components/modals/workspace-duplicate-modal';
import { WorkspaceEnvironmentsEditModal } from '../components/modals/workspace-environments-edit-modal';
import { WorkspaceSettingsModal } from '../components/modals/workspace-settings-modal';
import { WrapperModal } from '../components/modals/wrapper-modal';
import { Toast } from '../components/toast';
import { type WrapperClass, Wrapper } from '../components/wrapper';
import withDragDropContext from '../context/app/drag-drop-context';
import { GrpcProvider } from '../context/grpc';
import { GrpcDispatchModalWrapper, GrpcProvider } from '../context/grpc';
import { NunjucksEnabledProvider } from '../context/nunjucks/nunjucks-enabled-context';
import { useGitVCS } from '../hooks/use-git-vcs';
import { useVCS } from '../hooks/use-vcs';
import {
newCommand,
} from '../redux/modules/global';
import { importUri } from '../redux/modules/import';
import {
selectActiveActivity,
selectActiveApiSpec,
selectActiveCookieJar,
selectActiveEnvironment,
selectActiveGitRepository,
selectActiveProject,
selectActiveWorkspace,
@@ -42,166 +68,88 @@ import {
selectEnvironments,
selectIsFinishedBooting,
selectIsLoggedIn,
selectSettings,
} from '../redux/selectors';
import { AppHooks } from './app-hooks';
const lazyWithPreload = <T extends FC<any>>(
importFn: () => Promise<{ default: T }>
) => {
const LazyComponent = lazy(importFn);
const preload = () => importFn();
return [LazyComponent, preload] as const;
};
const [WrapperHome, preloadWrapperHome] = lazyWithPreload(
() => import('../components/wrapper-home')
);
const [WrapperDebug, preloadWrapperDebug] = lazyWithPreload(
() => import('../components/wrapper-debug')
);
const [WrapperDesign, preloadWrapperDesign] = lazyWithPreload(
() => import('../components/wrapper-design')
);
const [WrapperUnitTest, preloadWrapperUnitTest] = lazyWithPreload(
() => import('../components/wrapper-unit-test')
);
preloadWrapperHome();
preloadWrapperDebug();
preloadWrapperDesign();
preloadWrapperUnitTest();
const LoadingIndicator = () => (<div
id="app-loading-indicator"
style={{
position: 'fixed',
top: '0',
left: '0',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
height: '100%',
width: '100%',
}}
>
<img src="./ui/images/insomnia-logo.svg" alt="Insomnia" />
</div>);
const ActivityRouter = () => {
const selectedActivity = useSelector(selectActiveActivity);
const activeWorkspace = useSelector(selectActiveWorkspace);
// If there is no active workspace, we want to navigate to home no matter what the previous activity was
const activity = activeWorkspace ? selectedActivity : ACTIVITY_HOME;
const navigate = useNavigate();
React.useEffect(() => {
if (activity) {
navigate(activity);
}
}, [activity, navigate]);
return null;
};
interface State {
isMigratingChildren: boolean;
}
function useVCS({
workspaceId,
}: {
workspaceId?: string;
}) {
const vcsInstanceRef = useRef<VCS | null>(null);
const [vcs, setVCS] = useState<VCS | null>(null);
const updateVCSLock = useRef<boolean | string>(false);
// Update VCS when the active workspace changes
useEffect(() => {
const lock = generateId();
updateVCSLock.current = lock;
// Set vcs to null while we update it
setVCS(null);
if (!vcsInstanceRef.current) {
const driver = FileSystemDriver.create(getDataDirectory());
vcsInstanceRef.current = new VCS(driver, async conflicts => {
return new Promise(resolve => {
showModal(SyncMergeModal, {
conflicts,
handleDone: (conflicts: MergeConflict[]) => resolve(conflicts),
});
});
});
}
if (workspaceId) {
vcsInstanceRef.current.switchProject(workspaceId);
} else {
vcsInstanceRef.current.clearBackendProject();
}
// Prevent a potential race-condition when _updateVCS() gets called for different workspaces in rapid succession
if (updateVCSLock.current === lock) {
setVCS(vcsInstanceRef.current);
}
}, [workspaceId]);
return vcs;
}
function useGitVCS({
workspaceId,
projectId,
gitRepository,
}: {
workspaceId?: string;
projectId: string;
gitRepository?: GitRepository | null;
}) {
const gitVCSInstanceRef = useRef<GitVCS | null>(null);
const [gitVCS, setGitVCS] = useState<GitVCS | null>(null);
useEffect(() => {
let isMounted = true;
// Set the instance to null in the state while we update it
if (gitVCSInstanceRef.current) {
setGitVCS(null);
}
if (!gitVCSInstanceRef.current) {
gitVCSInstanceRef.current = new GitVCS();
}
async function update() {
if (workspaceId && gitRepository && gitVCSInstanceRef.current) {
// Create FS client
const baseDir = path.join(
getDataDirectory(),
`version-control/git/${gitRepository._id}`,
);
/** All app data is stored within a namespaced GIT_INSOMNIA_DIR directory at the root of the repository and is read/written from the local NeDB database */
const neDbClient = NeDBClient.createClient(workspaceId, projectId);
/** All git metadata in the GIT_INTERNAL_DIR directory is stored in a git/ directory on the filesystem */
const gitDataClient = fsClient(baseDir);
/** All data outside the directories listed below will be stored in an 'other' directory. This is so we can support files that exist outside the ones the app is specifically in charge of. */
const otherDatClient = fsClient(path.join(baseDir, 'other'));
/** The routable FS client directs isomorphic-git to read/write from the database or from the correct directory on the file system while performing git operations. */
const routableFS = routableFSClient(otherDatClient, {
[GIT_INSOMNIA_DIR]: neDbClient,
[GIT_INTERNAL_DIR]: gitDataClient,
});
// Init VCS
const { credentials, uri } = gitRepository;
if (gitRepository.needsFullClone) {
await models.gitRepository.update(gitRepository, {
needsFullClone: false,
});
await gitVCSInstanceRef.current.initFromClone({
url: uri,
gitCredentials: credentials,
directory: GIT_CLONE_DIR,
fs: routableFS,
gitDirectory: GIT_INTERNAL_DIR,
});
} else {
await gitVCSInstanceRef.current.init({
directory: GIT_CLONE_DIR,
fs: routableFS,
gitDirectory: GIT_INTERNAL_DIR,
});
}
// Configure basic info
const { author, uri: gitUri } = gitRepository;
await gitVCSInstanceRef.current.setAuthor(author.name, author.email);
await gitVCSInstanceRef.current.addRemote(gitUri);
} else {
// Create new one to un-initialize it
gitVCSInstanceRef.current = new GitVCS();
}
isMounted && setGitVCS(gitVCSInstanceRef.current);
}
update();
return () => {
isMounted = false;
};
}, [workspaceId, projectId, gitRepository]);
return gitVCS;
}
const App = () => {
const [state, setState] = useState<State>({
isMigratingChildren: false,
});
const activeProject = useSelector(selectActiveProject);
const activeCookieJar = useSelector(selectActiveCookieJar);
const activeApiSpec = useSelector(selectActiveApiSpec);
const activeWorkspace = useSelector(selectActiveWorkspace);
const activeGitRepository = useSelector(selectActiveGitRepository);
const activeWorkspaceMeta = useSelector(selectActiveWorkspaceMeta);
const environments = useSelector(selectEnvironments);
const isLoggedIn = useSelector(selectIsLoggedIn);
const isFinishedBooting = useSelector(selectIsFinishedBooting);
const settings = useSelector(selectSettings);
const dispatch = useDispatch();
const handleCommand = dispatch(newCommand);
const handleImportUri = dispatch(importUri);
const activeGitRepository = useSelector(selectActiveGitRepository);
const activeEnvironment = useSelector(selectActiveEnvironment);
const activeProject = useSelector(selectActiveProject);
const vcs = useVCS({
workspaceId: activeWorkspace?._id,
});
@@ -212,26 +160,25 @@ const App = () => {
gitRepository: activeGitRepository,
});
const wrapperRef = useRef<WrapperClass | null>(null);
async function handleSetActiveEnvironment(activeEnvironmentId: string | null) {
if (activeWorkspaceMeta) {
await models.workspaceMeta.update(activeWorkspaceMeta, { activeEnvironmentId });
}
}
// Ensure Children: Make sure cookies, env, and meta models are created under this workspace
useEffect(() => {
if (!activeWorkspace) {
return;
}
const baseEnvironments = environments.filter(environment => environment.parentId === activeWorkspace._id);
// Nothing to do
if (baseEnvironments.length && activeCookieJar && activeApiSpec && activeWorkspaceMeta) {
const workspaceHasChildren = baseEnvironments.length && activeCookieJar && activeApiSpec && activeWorkspaceMeta;
if (workspaceHasChildren) {
return;
}
// We already started migrating. Let it finish.
if (state.isMigratingChildren) {
return;
}
// Prevent rendering of everything until we check the workspace has cookies, env, and meta
setState(state => ({ ...state, isMigratingChildren: true }));
async function update() {
@@ -245,72 +192,6 @@ const App = () => {
update();
}, [activeApiSpec, activeCookieJar, activeWorkspace, activeWorkspaceMeta, environments, state.isMigratingChildren]);
// Give it a bit before letting the backend know it's ready
useEffect(() => {
setTimeout(() => ipcRenderer.send('window-ready'), 500);
}, []);
// Handle Application Commands
useEffect(() => {
ipcRenderer.on('run-command', (_, commandUri) => {
const parsed = urlParse(commandUri, true);
const command = `${parsed.hostname}${parsed.pathname}`;
const args = JSON.parse(JSON.stringify(parsed.query));
args.workspaceId = args.workspaceId || activeWorkspace?._id;
handleCommand(command, args);
});
}, [activeWorkspace?._id, handleCommand]);
// Handle System Theme change
useEffect(() => {
const matches = window.matchMedia('(prefers-color-scheme: dark)');
matches.addEventListener('change', () => themes.applyColorScheme(settings));
return () => {
matches.removeEventListener('change', () => themes.applyColorScheme(settings));
};
});
// Global Drag and Drop for importing files
useEffect(() => {
// NOTE: This is required for "drop" event to trigger.
document.addEventListener(
'dragover',
event => {
event.preventDefault();
},
false,
);
document.addEventListener(
'drop',
async event => {
event.preventDefault();
if (!activeWorkspace) {
return;
}
const files = event.dataTransfer?.files || [];
if (files.length === 0) {
console.log('[drag] Ignored drop event because no files present');
return;
}
const file = files[0];
if (!file?.path) {
return;
}
await showModal(AlertModal, {
title: 'Confirm Data Import',
message: (
<span>
Import <code>{file.path}</code>?
</span>
),
addCancel: true,
});
handleImportUri(`file://${file.path}`, { workspaceId: activeWorkspace?._id });
},
false,
);
});
if (state.isMigratingChildren) {
console.log('[app] Waiting for migration to complete');
return null;
@@ -328,11 +209,144 @@ const App = () => {
<AppHooks />
<div className="app" key={uniquenessKey}>
<ErrorBoundary showAlert>
<Wrapper
ref={wrapperRef}
vcs={vcs}
gitVCS={gitVCS}
/>
<div key="modals" className="modals">
<ErrorBoundary showAlert>
<AnalyticsModal />
<AlertModal ref={registerModal} />
<ErrorModal ref={registerModal} />
<PromptModal ref={registerModal} />
<WrapperModal ref={registerModal} />
<LoginModal ref={registerModal} />
<AskModal ref={registerModal} />
<SelectModal ref={registerModal} />
<FilterHelpModal ref={registerModal} />
<RequestRenderErrorModal ref={registerModal} />
<GenerateConfigModal ref={registerModal} />
<ProjectSettingsModal ref={registerModal} />
<WorkspaceDuplicateModal ref={registerModal} vcs={vcs || undefined} />
<CodePromptModal ref={registerModal} />
<RequestSettingsModal ref={instance => registerModal(instance, 'RequestSettingsModal')} />
<RequestGroupSettingsModal ref={registerModal} />
{activeWorkspace ? <>
{/* TODO: Figure out why cookieJar is sometimes null */}
{activeCookieJar ? <>
<CookiesModalFC
ref={registerModal}
/>
<CookieModifyModal ref={registerModal} />
</> : null}
<NunjucksModal
ref={registerModal}
workspace={activeWorkspace}
/>
{activeApiSpec ? <WorkspaceSettingsModal
ref={registerModal}
workspace={activeWorkspace}
apiSpec={activeApiSpec}
/> : null}
</> : null}
<GenerateCodeModal
ref={registerModal}
environmentId={activeEnvironment ? activeEnvironment._id : 'n/a'}
/>
<SettingsModal ref={instance => registerModal(instance, 'SettingsModal')} />
<ResponseDebugModal ref={instance => registerModal(instance, 'ResponseDebugModal')} />
<RequestSwitcherModal ref={instance => registerModal(instance, 'RequestSwitcherModal')} />
<EnvironmentEditModal
ref={registerModal}
onChange={models.requestGroup.update}
/>
<GitRepositorySettingsModal ref={registerModal} />
{activeWorkspace && gitVCS ? (
<Fragment>
<GitStagingModal ref={registerModal} workspace={activeWorkspace} vcs={gitVCS} gitRepository={activeGitRepository} />
<GitLogModal ref={registerModal} vcs={gitVCS} />
{activeGitRepository !== null && (
<GitBranchesModal
ref={registerModal}
vcs={gitVCS}
gitRepository={activeGitRepository}
/>
)}
</Fragment>
) : null}
{activeWorkspace && vcs ? (
<Fragment>
<SyncStagingModal ref={registerModal} vcs={vcs} />
<SyncMergeModal ref={registerModal} vcs={vcs} />
<SyncBranchesModal ref={registerModal} vcs={vcs} />
<SyncDeleteModal ref={registerModal} vcs={vcs} />
<SyncHistoryModal ref={registerModal} vcs={vcs} />
</Fragment>
) : null}
<WorkspaceEnvironmentsEditModal
ref={registerModal}
handleSetActiveEnvironment={handleSetActiveEnvironment}
activeEnvironmentId={activeEnvironment ? activeEnvironment._id : null}
/>
<AddKeyCombinationModal ref={instance => registerModal(instance, 'AddKeyCombinationModal')} />
<ExportRequestsModal ref={registerModal} />
<GrpcDispatchModalWrapper>
{dispatch => (
<ProtoFilesModal
ref={registerModal}
grpcDispatch={dispatch}
/>
)}
</GrpcDispatchModalWrapper>
</ErrorBoundary>
</div>
<Routes>
<Route
path="*"
element={
<Suspense fallback={<LoadingIndicator />}>
<WrapperHome
vcs={vcs}
/>
</Suspense>
}
/>
<Route
path={ACTIVITY_UNIT_TEST}
element={
<Suspense fallback={<LoadingIndicator />}>
<WrapperUnitTest />
</Suspense>
}
/>
<Route
path={ACTIVITY_SPEC}
element={
<Suspense fallback={<LoadingIndicator />}>
<WrapperDesign />
</Suspense>
}
/>
<Route
path={ACTIVITY_DEBUG}
element={
<Suspense fallback={<LoadingIndicator />}>
<WrapperDebug />
</Suspense>
}
/>
</Routes>
<ActivityRouter />
</ErrorBoundary>
<ErrorBoundary showAlert>
@@ -0,0 +1,24 @@
import { ipcRenderer } from 'electron';
import { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { parse } from 'url';
import { newCommand } from '../redux/modules/global';
import { selectActiveWorkspace } from '../redux/selectors';
export const useAppCommands = () => {
const dispatch = useDispatch();
const handleCommand = dispatch(newCommand);
const activeWorkspace = useSelector(selectActiveWorkspace);
// Handle Application Commands
useEffect(() => {
ipcRenderer.on('run-command', (_, commandUri) => {
const parsed = parse(commandUri, true);
const command = `${parsed.hostname}${parsed.pathname}`;
const args = JSON.parse(JSON.stringify(parsed.query));
args.workspaceId = args.workspaceId || activeWorkspace?._id;
handleCommand(command, args);
});
}, [activeWorkspace?._id, handleCommand]);
};
@@ -0,0 +1,54 @@
import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { showModal } from '../components/modals';
import { AlertModal } from '../components/modals/alert-modal';
import { importUri } from '../redux/modules/import';
import { selectActiveWorkspace } from '../redux/selectors';
export const useDragAndDropImportFile = () => {
const activeWorkspace = useSelector(selectActiveWorkspace);
const dispatch = useDispatch();
const handleImportUri = dispatch(importUri);
// Global Drag and Drop for importing files
useEffect(() => {
// NOTE: This is required for "drop" event to trigger.
document.addEventListener(
'dragover',
event => {
event.preventDefault();
},
false,
);
document.addEventListener(
'drop',
async event => {
event.preventDefault();
if (!activeWorkspace) {
return;
}
const files = event.dataTransfer?.files || [];
if (files.length === 0) {
console.log('[drag] Ignored drop event because no files present');
return;
}
const file = files[0];
if (!file?.path) {
return;
}
await showModal(AlertModal, {
title: 'Confirm Data Import',
message: (
<span>
Import <code>{file.path}</code>?
</span>
),
addCancel: true,
});
handleImportUri(`file://${file.path}`, { workspaceId: activeWorkspace?._id });
},
false,
);
});
};
@@ -0,0 +1,97 @@
import path from 'path';
import { useEffect, useRef, useState } from 'react';
import * as models from '../../models';
import { GitRepository } from '../../models/git-repository';
import { fsClient } from '../../sync/git/fs-client';
import { GIT_CLONE_DIR, GIT_INSOMNIA_DIR, GIT_INTERNAL_DIR, GitVCS } from '../../sync/git/git-vcs';
import { NeDBClient } from '../../sync/git/ne-db-client';
import { routableFSClient } from '../../sync/git/routable-fs-client';
export function useGitVCS({
workspaceId,
projectId,
gitRepository,
}: {
workspaceId?: string;
projectId: string;
gitRepository?: GitRepository | null;
}) {
const gitVCSInstanceRef = useRef<GitVCS | null>(null);
const [gitVCS, setGitVCS] = useState<GitVCS | null>(null);
useEffect(() => {
let isMounted = true;
// Set the instance to null in the state while we update it
if (gitVCSInstanceRef.current) {
setGitVCS(null);
}
if (!gitVCSInstanceRef.current) {
gitVCSInstanceRef.current = new GitVCS();
}
async function update() {
if (workspaceId && gitRepository && gitVCSInstanceRef.current) {
// Create FS client
const baseDir = path.join(
process.env['INSOMNIA_DATA_PATH'] || window.app.getPath('userData'),
`version-control/git/${gitRepository._id}`,
);
/** All app data is stored within a namespaced GIT_INSOMNIA_DIR directory at the root of the repository and is read/written from the local NeDB database */
const neDbClient = NeDBClient.createClient(workspaceId, projectId);
/** All git metadata in the GIT_INTERNAL_DIR directory is stored in a git/ directory on the filesystem */
const gitDataClient = fsClient(baseDir);
/** All data outside the directories listed below will be stored in an 'other' directory. This is so we can support files that exist outside the ones the app is specifically in charge of. */
const otherDatClient = fsClient(path.join(baseDir, 'other'));
/** The routable FS client directs isomorphic-git to read/write from the database or from the correct directory on the file system while performing git operations. */
const routableFS = routableFSClient(otherDatClient, {
[GIT_INSOMNIA_DIR]: neDbClient,
[GIT_INTERNAL_DIR]: gitDataClient,
});
// Init VCS
const { credentials, uri } = gitRepository;
if (gitRepository.needsFullClone) {
await models.gitRepository.update(gitRepository, {
needsFullClone: false,
});
await gitVCSInstanceRef.current.initFromClone({
url: uri,
gitCredentials: credentials,
directory: GIT_CLONE_DIR,
fs: routableFS,
gitDirectory: GIT_INTERNAL_DIR,
});
} else {
await gitVCSInstanceRef.current.init({
directory: GIT_CLONE_DIR,
fs: routableFS,
gitDirectory: GIT_INTERNAL_DIR,
});
}
// Configure basic info
const { author, uri: gitUri } = gitRepository;
await gitVCSInstanceRef.current.setAuthor(author.name, author.email);
await gitVCSInstanceRef.current.addRemote(gitUri);
} else {
// Create new one to un-initialize it
gitVCSInstanceRef.current = new GitVCS();
}
isMounted && setGitVCS(gitVCSInstanceRef.current);
}
update();
return () => {
isMounted = false;
};
}, [workspaceId, projectId, gitRepository]);
return gitVCS;
}
@@ -0,0 +1,17 @@
import { useEffect } from 'react';
import { useSelector } from 'react-redux';
import * as themes from '../../plugins/misc';
import { selectSettings } from '../redux/selectors';
export const useThemeChange = () => {
const settings = useSelector(selectSettings);
// Handle System Theme change
useEffect(() => {
const matches = window.matchMedia('(prefers-color-scheme: dark)');
matches.addEventListener('change', () => themes.applyColorScheme(settings));
return () => {
matches.removeEventListener('change', () => themes.applyColorScheme(settings));
};
});
};
+53
View File
@@ -0,0 +1,53 @@
import { useEffect, useRef, useState } from 'react';
import { getDataDirectory } from '../../common/electron-helpers';
import { generateId } from '../../common/misc';
import FileSystemDriver from '../../sync/store/drivers/file-system-driver';
import { type MergeConflict } from '../../sync/types';
import { VCS } from '../../sync/vcs/vcs';
import { showModal } from '../components/modals/index';
import { SyncMergeModal } from '../components/modals/sync-merge-modal';
export function useVCS({
workspaceId,
}: {
workspaceId?: string;
}) {
const vcsInstanceRef = useRef<VCS | null>(null);
const [vcs, setVCS] = useState<VCS | null>(null);
const updateVCSLock = useRef<boolean | string>(false);
// Update VCS when the active workspace changes
useEffect(() => {
const lock = generateId();
updateVCSLock.current = lock;
// Set vcs to null while we update it
setVCS(null);
if (!vcsInstanceRef.current) {
const driver = FileSystemDriver.create(getDataDirectory());
vcsInstanceRef.current = new VCS(driver, async conflicts => {
return new Promise(resolve => {
showModal(SyncMergeModal, {
conflicts,
handleDone: (conflicts: MergeConflict[]) => resolve(conflicts),
});
});
});
}
if (workspaceId) {
vcsInstanceRef.current.switchProject(workspaceId);
} else {
vcsInstanceRef.current.clearBackendProject();
}
// Prevent a potential race-condition when _updateVCS() gets called for different workspaces in rapid succession
if (updateVCSLock.current === lock) {
setVCS(vcsInstanceRef.current);
}
}, [workspaceId]);
return vcs;
}