response routing (#6214)

* pass 1

* refactoring notes

* simplify select children

* remove switcher

* add responses

* filter by env

* patch settings

* patch settings

* remove selector

* history dereduxed

* move folder settings to conditional render

* fix types

* fix types

* fix types

* fix types

* fix lint

* migrate group settings to remix

* fix use effects

* conditional render of export modal

* more sidebar refactoring

* unpack sidebar children in export list

* unpick sidebar children

* clean up

* remove console log

* mostly working connect

* clean up

* conditionally render cookie modals

* cond render env modal

* cond render sync delete

* fix

* fix

* flatten syncitems

* remove helper

* clean

* fix grpc initial re-render

* redirect to last active request

* optimize

* fix lint

* improve typing

* add settings hotkey

* env modal hotkey

* remove active response null setting

* fix shakey test

* move delete response to action

* fix type

* fix windows flake

* cond render sync branches

* fix loading bug

* add spinner

* delete request

* fix stream with temporary hack
This commit is contained in:
Jack Kavanagh
2023-08-04 12:42:00 +02:00
committed by GitHub
parent 70add78801
commit 52e969d803
75 changed files with 1768 additions and 2469 deletions

View File

@@ -1,4 +1,5 @@
import fs from 'fs';
import { stat } from 'node:fs/promises';
import NeDB from 'nedb';
import path from 'path';
@@ -8,7 +9,9 @@ import type { BaseModel } from '../models/types';
const neDbAdapter: DbAdapter = async (dir, filterTypes) => {
// Confirm if db files exist
if (!fs.existsSync(path.join(dir, 'insomnia.Workspace.db'))) {
try {
await stat(path.join(dir, 'insomnia.Workspace.db'));
} catch (err) {
return null;
}

View File

@@ -58,7 +58,7 @@ test('can send GraphQL requests after editing and prettifying query', async ({ a
await page.getByRole('button', { name: 'GraphQL request' }).click();
// Edit and prettify query
await page.locator('pre[role="presentation"]:has-text("hello,")').click();
await page.locator('pre[role="presentation"]:has-text("bearer")').click();
await page.locator('.app').press('Enter');
await page.locator('text=Prettify GraphQL').click();
await page.click('[data-testid="request-pane"] >> text=Send');

View File

@@ -317,7 +317,6 @@ export async function getRenderContext(
const project = ancestors.find(isProject);
const workspace = ancestors.find(isWorkspace);
console.log('getRenderContext', { request, environmentId, ancestors, purpose, extraInfo, project, workspace });
if (!workspace) {
throw new Error('Failed to render. Could not find workspace');
}

View File

@@ -1,10 +0,0 @@
import { StatusCandidate } from '../../sync/types';
import { BaseModel, canSync } from '..';
const toStatusCandidate = (doc: BaseModel): StatusCandidate => ({
key: doc._id,
name: doc.name || '',
document: doc,
});
export const getStatusCandidates = (docs: BaseModel[]) => docs.filter(canSync).map(toStatusCandidate);

View File

@@ -53,6 +53,10 @@ export function getById(id: string) {
return db.get<RequestVersion>(type, id);
}
export function findByParentId(parentId: string) {
return db.find<RequestVersion>(type, { parentId });
}
export async function create(request: Request | WebSocketRequest | GrpcRequest) {
if (!isRequest(request) && !isWebSocketRequest(request)) {
throw new Error(`New ${type} was not given a valid ${request.type} instance`);

View File

@@ -109,6 +109,10 @@ export function getById(id: string) {
return db.get<Response>(type, id);
}
export function findByParentId(parentId: string) {
return db.find<Response>(type, { parentId: parentId });
}
export async function all() {
return db.all<Response>(type);
}

View File

@@ -82,6 +82,10 @@ export function getById(id: string) {
return db.get<WebSocketResponse>(type, id);
}
export function findByParentId(parentId: string) {
return db.find<WebSocketResponse>(type, { parentId: parentId });
}
export async function all() {
return db.all<WebSocketResponse>(type);
}

View File

@@ -1,9 +1,10 @@
import { database } from '../../common/database';
import * as models from '../../models';
import { getStatusCandidates } from '../../models/helpers/get-status-candidates';
import { BaseModel, canSync } from '../../models';
import { Project } from '../../models/project';
import { Workspace } from '../../models/workspace';
import { WorkspaceMeta } from '../../models/workspace-meta';
import { StatusCandidate } from '../types';
import { VCS } from './vcs';
const blankStage = {};
@@ -13,7 +14,11 @@ export const initializeLocalBackendProjectAndMarkForSync = async ({ vcs, workspa
await vcs.switchAndCreateBackendProjectIfNotExist(workspace._id, workspace.name);
// Everything unstaged
const candidates = getStatusCandidates(await database.withDescendants(workspace));
const candidates = (await database.withDescendants(workspace)).filter(canSync).map((doc: BaseModel): StatusCandidate => ({
key: doc._id,
name: doc.name || '',
document: doc,
}));
const status = await vcs.status(candidates, blankStage);
// Stage everything

View File

@@ -1,4 +1,4 @@
import React, { FC, useCallback } from 'react';
import React, { FC, useCallback, useState } from 'react';
import { Cookie as ToughCookie } from 'tough-cookie';
import { v4 as uuidv4 } from 'uuid';
@@ -6,7 +6,6 @@ import { cookieToString } from '../../common/cookies';
import { Cookie } from '../../models/cookie-jar';
import { Dropdown, DropdownButton, DropdownItem, ItemContent } from './base/dropdown';
import { PromptButton } from './base/prompt-button';
import { showModal } from './modals';
import { CookieModifyModal } from './modals/cookie-modify-modal';
import { RenderedText } from './rendered-text';
@@ -27,6 +26,7 @@ const CookieRow: FC<{
index: number;
deleteCookie: (cookie: Cookie) => void;
}> = ({ cookie, index, deleteCookie }) => {
const [isCookieModalOpen, setIsCookieModalOpen] = useState(false);
const c = ToughCookie.fromJSON(cookie);
const cookieString = c ? cookieToString(c) : '';
return <tr className="selectable" key={index}>
@@ -39,7 +39,7 @@ const CookieRow: FC<{
<td onClick={() => { }} className="text-right no-wrap">
<button
className="btn btn--super-compact btn--outlined"
onClick={() => showModal(CookieModifyModal, { cookie })}
onClick={() => setIsCookieModalOpen(true)}
title="Edit cookie properties"
>
Edit
@@ -52,6 +52,12 @@ const CookieRow: FC<{
>
<i className="fa fa-trash-o" />
</PromptButton>
{isCookieModalOpen && (
<CookieModifyModal
cookie={cookie}
onHide={() => setIsCookieModalOpen(false)}
/>
)}
</td>
</tr>;

View File

@@ -6,7 +6,7 @@ import {
getAuthTypeName,
HAWK_ALGORITHM_SHA256,
} from '../../../common/constants';
import { Request, RequestAuthentication } from '../../../models/request';
import { RequestAuthentication } from '../../../models/request';
import { SIGNATURE_METHOD_HMAC_SHA1 } from '../../../network/o-auth-1/constants';
import { GRANT_TYPE_AUTHORIZATION_CODE } from '../../../network/o-auth-2/constants';
import { useRequestPatcher } from '../../hooks/use-request';
@@ -129,7 +129,7 @@ interface Props {
disabled?: boolean;
}
export const AuthDropdown: FC<Props> = ({ authTypes = defaultTypes, disabled = false }) => {
const { activeRequest } = useRouteLoaderData('request/:requestId') as RequestLoaderData<Request, any>;
const { activeRequest } = useRouteLoaderData('request/:requestId') as RequestLoaderData;
const { requestId } = useParams() as { organizationId: string; projectId: string; workspaceId: string; requestId: string };
const patchRequest = useRequestPatcher();
const onClick = useCallback(async (type: AuthType) => {

View File

@@ -29,7 +29,7 @@ import { showModal } from '../modals/index';
const EMPTY_MIME_TYPE = null;
export const ContentTypeDropdown: FC = () => {
const { activeRequest } = useRouteLoaderData('request/:requestId') as RequestLoaderData<Request, any>;
const { activeRequest } = useRouteLoaderData('request/:requestId') as RequestLoaderData;
const patchRequest = useRequestPatcher();
const { requestId } = useParams() as { requestId: string };
const handleChangeMimeType = async (mimeType: string | null) => {

View File

@@ -1,22 +1,21 @@
import React, { FC, useRef, useState } from 'react';
import { useFetcher, useParams, useRouteLoaderData } from 'react-router-dom';
import type { Environment } from '../../../models/environment';
import { type Environment } from '../../../models/environment';
import { RootLoaderData } from '../../routes/root';
import { WorkspaceLoaderData } from '../../routes/workspace';
import { Dropdown, DropdownButton, type DropdownHandle, DropdownItem, DropdownSection, ItemContent } from '../base/dropdown';
import { useDocBodyKeyboardShortcuts } from '../keydown-binder';
import { showModal } from '../modals/index';
import { WorkspaceEnvironmentsEditModal } from '../modals/workspace-environments-edit-modal';
import { Tooltip } from '../tooltip';
interface Props {
activeEnvironment?: Environment | null;
workspaceId: string;
setEnvironmentModalOpen: (isOpen: boolean) => void;
}
export const EnvironmentsDropdown: FC<Props> = () => {
const { organizationId, projectId, workspaceId } = useParams<{ organizationId: string; projectId: string; workspaceId: string}>();
export const EnvironmentsDropdown: FC<Props> = ({ setEnvironmentModalOpen }) => {
const { organizationId, projectId, workspaceId } = useParams<{ organizationId: string; projectId: string; workspaceId: string }>();
const {
baseEnvironment,
activeEnvironment,
@@ -89,10 +88,10 @@ export const EnvironmentsDropdown: FC<Props> = () => {
setActiveEnvironmentFetcher.submit({
environmentId: environment._id,
},
{
method: 'post',
action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/environment/set-active`,
});
{
method: 'post',
action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/environment/set-active`,
});
}}
/>
</DropdownItem>
@@ -107,22 +106,17 @@ export const EnvironmentsDropdown: FC<Props> = () => {
setActiveEnvironmentFetcher.submit({
environmentId: baseEnvironment._id,
},
{
method: 'post',
action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/environment/set-active`,
});
{
method: 'post',
action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/environment/set-active`,
});
}}
/>
</DropdownItem>
<DropdownSection title="General">
<DropdownItem>
<ItemContent
icon="wrench"
label="Manage Environments"
hint={hotKeyRegistry.environment_showEditor}
onClick={() => showModal(WorkspaceEnvironmentsEditModal)}
/>
<ItemContent icon="wrench" label="Manage Environments" hint={hotKeyRegistry.environment_showEditor} onClick={() => setEnvironmentModalOpen(true)} />
</DropdownItem>
</DropdownSection>
</Dropdown>

View File

@@ -167,7 +167,7 @@ export const GitSyncDropdown: FC<Props> = ({ className, gitRepository, isInsomni
gitPushFetcher.state === 'loading' ||
gitPullFetcher.state === 'loading';
const isSynced = Boolean(gitRepository?.uri && !isLoading && gitRepoDataFetcher.data && !('errors' in gitRepoDataFetcher.data));
const isSynced = Boolean(gitRepository?.uri && gitRepoDataFetcher.data && !('errors' in gitRepoDataFetcher.data));
const { branches, branch: currentBranch } =
gitRepoDataFetcher.data && 'branches' in gitRepoDataFetcher.data
@@ -263,6 +263,7 @@ export const GitSyncDropdown: FC<Props> = ({ className, gitRepository, isInsomni
justifyContent: 'flex-start !important',
height: 'var(--line-height-sm)',
}}
disabled={isLoading}
>
<div
style={{
@@ -289,7 +290,7 @@ export const GitSyncDropdown: FC<Props> = ({ className, gitRepository, isInsomni
opacity: status?.localChanges ? 1 : 0.5,
color: status?.localChanges ? 'var(--color-notice)' : 'var(--color-hl)',
}}
><i className="fa fa-cube space-left" /></span>
><i className={`fa fa-${isLoading ? 'refresh fa-spin' : 'cube'} space-left`} /></span>
</Tooltip>
</div>
</div>

View File

@@ -1,16 +1,13 @@
import fs from 'fs';
import React, { FC, useCallback } from 'react';
import { useSelector } from 'react-redux';
import { useRouteLoaderData } from 'react-router-dom';
import { getPreviewModeName, PREVIEW_MODE_SOURCE, PREVIEW_MODES, PreviewMode } from '../../../common/constants';
import { getPreviewModeName, PREVIEW_MODE_SOURCE, PREVIEW_MODES } from '../../../common/constants';
import { exportHarCurrentRequest } from '../../../common/har';
import * as models from '../../../models';
import { isRequest, Request } from '../../../models/request';
import { RequestMeta } from '../../../models/request-meta';
import { isRequest } from '../../../models/request';
import { isResponse } from '../../../models/response';
import { useRequestMetaPatcher } from '../../hooks/use-request';
import { selectActiveResponse } from '../../redux/selectors';
import { RequestLoaderData } from '../../routes/request';
import { Dropdown, DropdownButton, DropdownItem, DropdownSection, ItemContent } from '../base/dropdown';
@@ -23,24 +20,20 @@ export const PreviewModeDropdown: FC<Props> = ({
download,
copyToClipboard,
}) => {
const { activeRequest, activeRequestMeta } = useRouteLoaderData('request/:requestId') as RequestLoaderData<Request, RequestMeta>;
const { activeRequest, activeRequestMeta, activeResponse } = useRouteLoaderData('request/:requestId') as RequestLoaderData;
const previewMode = activeRequestMeta.previewMode || PREVIEW_MODE_SOURCE;
const response = useSelector(selectActiveResponse);
const patchRequestMeta = useRequestMetaPatcher();
const handleClick = async (previewMode: PreviewMode) => {
patchRequestMeta(activeRequest._id, { previewMode });
};
const handleDownloadPrettify = useCallback(() => download(true), [download]);
const handleDownloadNormal = useCallback(() => download(false), [download]);
const exportAsHAR = useCallback(async () => {
if (!response || !activeRequest || !isRequest(activeRequest) || !isResponse(response)) {
if (!activeResponse || !activeRequest || !isRequest(activeRequest) || !isResponse(activeResponse)) {
console.warn('Nothing to download');
return;
}
const data = await exportHarCurrentRequest(activeRequest, response);
const data = await exportHarCurrentRequest(activeRequest, activeResponse);
const har = JSON.stringify(data, null, '\t');
const { filePath } = await window.dialog.showSaveDialog({
@@ -57,15 +50,15 @@ export const PreviewModeDropdown: FC<Props> = ({
console.warn('Failed to export har', err);
});
to.end(har);
}, [activeRequest, response]);
}, [activeRequest, activeResponse]);
const exportDebugFile = useCallback(async () => {
if (!response || !activeRequest || !isResponse(response)) {
if (!activeResponse || !activeRequest || !isResponse(activeResponse)) {
console.warn('Nothing to download');
return;
}
const timeline = models.response.getTimeline(response);
const timeline = models.response.getTimeline(activeResponse);
const headers = timeline
.filter(v => v.name === 'HeaderIn')
.map(v => v.value)
@@ -80,7 +73,7 @@ export const PreviewModeDropdown: FC<Props> = ({
if (canceled) {
return;
}
const readStream = models.response.getBodyStream(response);
const readStream = models.response.getBodyStream(activeResponse);
if (readStream && filePath && typeof readStream !== 'string') {
const to = fs.createWriteStream(filePath);
@@ -90,8 +83,8 @@ export const PreviewModeDropdown: FC<Props> = ({
console.warn('Failed to save full response', err);
});
}
}, [activeRequest, response]);
const shouldPrettifyOption = response.contentType.includes('json');
}, [activeRequest, activeResponse]);
const shouldPrettifyOption = activeResponse?.contentType.includes('json');
return (
<Dropdown
@@ -115,7 +108,7 @@ export const PreviewModeDropdown: FC<Props> = ({
<ItemContent
icon={previewMode === mode ? 'check' : 'empty'}
label={getPreviewModeName(mode, true)}
onClick={() => handleClick(mode)}
onClick={() => patchRequestMeta(activeRequest._id, { previewMode: mode })}
/>
</DropdownItem>
)}

View File

@@ -1,7 +1,6 @@
import { differenceInHours, differenceInMinutes, isThisWeek, isToday } from 'date-fns';
import React, { useCallback, useRef } from 'react';
import { useSelector } from 'react-redux';
import { useRouteLoaderData } from 'react-router-dom';
import { useFetcher, useRouteLoaderData } from 'react-router-dom';
import { useParams } from 'react-router-dom';
import { decompressObject } from '../../../common/misc';
@@ -11,7 +10,7 @@ import { Response } from '../../../models/response';
import { WebSocketRequest } from '../../../models/websocket-request';
import { isWebSocketResponse, WebSocketResponse } from '../../../models/websocket-response';
import { useRequestMetaPatcher } from '../../hooks/use-request';
import { selectActiveRequestResponses, selectRequestVersions } from '../../redux/selectors';
import { RequestLoaderData, WebSocketRequestLoaderData } from '../../routes/request';
import { WorkspaceLoaderData } from '../../routes/workspace';
import { Dropdown, DropdownButton, type DropdownHandle, DropdownItem, DropdownSection, ItemContent } from '../base/dropdown';
import { useDocBodyKeyboardShortcuts } from '../keydown-binder';
@@ -20,25 +19,22 @@ import { StatusTag } from '../tags/status-tag';
import { TimeTag } from '../tags/time-tag';
import { URLTag } from '../tags/url-tag';
import { TimeFromNow } from '../time-from-now';
interface Props<GenericResponse extends Response | WebSocketResponse> {
activeResponse: GenericResponse;
className?: string;
}
export const ResponseHistoryDropdown = <GenericResponse extends Response | WebSocketResponse>({
export const ResponseHistoryDropdown = ({
activeResponse,
className,
}: Props<GenericResponse>) => {
}: { activeResponse: Response | WebSocketResponse }) => {
const { requestId } = useParams() as { requestId: string };
const dropdownRef = useRef<DropdownHandle>(null);
const patchRequestMeta = useRequestMetaPatcher();
const {
activeEnvironment,
} = useRouteLoaderData(':workspaceId') as WorkspaceLoaderData;
const responses = useSelector(selectActiveRequestResponses) as GenericResponse[];
const requestVersions = useSelector(selectRequestVersions);
const {
responses,
requestVersions,
} = useRouteLoaderData('request/:requestId') as RequestLoaderData | WebSocketRequestLoaderData;
const now = new Date();
const categories: Record<string, GenericResponse[]> = {
const categories: Record<string, (Response | WebSocketResponse)[]> = {
minutes: [],
hours: [],
today: [],
@@ -46,6 +42,9 @@ export const ResponseHistoryDropdown = <GenericResponse extends Response | WebSo
other: [],
};
const { organizationId, projectId, workspaceId } = useParams<{ organizationId: string; projectId: string; workspaceId: string }>();
const fetcher = useFetcher();
const handleSetActiveResponse = useCallback(async (requestId: string, activeResponse: Response | WebSocketResponse) => {
if (isWebSocketResponse(activeResponse)) {
window.main.webSocket.close({ requestId });
@@ -61,63 +60,40 @@ export const ResponseHistoryDropdown = <GenericResponse extends Response | WebSo
const handleDeleteResponses = useCallback(async () => {
if (isWebSocketResponse(activeResponse)) {
window.main.webSocket.closeAll();
await models.webSocketResponse.removeForRequest(requestId, activeEnvironment._id);
} else {
await models.response.removeForRequest(requestId, activeEnvironment._id);
}
await patchRequestMeta(requestId, { activeResponseId: null });
}, [activeEnvironment._id, activeResponse, requestId, patchRequestMeta]);
fetcher.submit({}, {
action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/debug/request/${requestId}/response/delete-all`,
method: 'post',
encType: 'application/json',
});
}, [activeResponse, fetcher, organizationId, projectId, requestId, workspaceId]);
const handleDeleteResponse = useCallback(async () => {
let response: Response | WebSocketResponse | null = null;
if (activeResponse) {
if (isWebSocketResponse(activeResponse)) {
window.main.webSocket.close({ requestId });
await models.webSocketResponse.remove(activeResponse);
const environmentId = activeEnvironment?._id || null;
response = await models.webSocketResponse.getLatestForRequest(requestId, environmentId);
} else {
await models.response.remove(activeResponse);
const environmentId = activeEnvironment?._id || null;
response = await models.response.getLatestForRequest(requestId, environmentId);
}
if (response?.requestVersionId) {
// Deleting a response restores latest request body
await models.requestVersion.restore(response.requestVersionId);
}
await patchRequestMeta(requestId, { activeResponseId: response?._id || null });
}
}, [activeEnvironment?._id, activeResponse, requestId, patchRequestMeta]);
fetcher.submit({ responseId: activeResponse._id }, {
action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/debug/request/${requestId}/response/delete`,
method: 'post',
encType: 'application/json',
});
}, [activeResponse, fetcher, requestId, organizationId, projectId, workspaceId]);
responses.forEach(response => {
responses.forEach((response: Response | WebSocketResponse) => {
const responseTime = new Date(response.created);
if (differenceInMinutes(now, responseTime) < 5) {
categories.minutes.push(response);
return;
}
if (differenceInHours(now, responseTime) < 2) {
categories.hours.push(response);
return;
}
if (isToday(responseTime)) {
categories.today.push(response);
return;
}
if (isThisWeek(responseTime)) {
categories.week.push(response);
return;
}
categories.other.push(response);
const match = Object.entries({
'minutes': differenceInMinutes(now, responseTime) < 5,
'hours': differenceInHours(now, responseTime) < 2,
'today': isToday(responseTime),
'week': isThisWeek(responseTime),
'other': true,
}).find(([, value]) => value === true)?.[0] || 'other';
categories[match].push(response);
});
const renderResponseRow = (response: GenericResponse) => {
const renderResponseRow = (response: Response | WebSocketResponse) => {
const activeResponseId = activeResponse ? activeResponse._id : 'n/a';
const active = response._id === activeResponseId;
const requestVersion = requestVersions.find(({ _id }) => _id === response.requestVersionId);
@@ -180,7 +156,7 @@ export const ResponseHistoryDropdown = <GenericResponse extends Response | WebSo
aria-label="Response history dropdown"
key={activeResponse ? activeResponse._id : 'n/a'}
closeOnSelect={false}
className={className}
className="tall pane__header__right"
triggerButton={
<DropdownButton className="btn btn--super-compact tall" title="Response history">
{activeResponse && <TimeFromNow timestamp={activeResponse.created} titleCase />}

View File

@@ -19,7 +19,7 @@ import { BackendProjectWithTeam } from '../../../sync/vcs/normalize-backend-proj
import { pullBackendProject } from '../../../sync/vcs/pull-backend-project';
import { interceptAccessError } from '../../../sync/vcs/util';
import { VCS } from '../../../sync/vcs/vcs';
import { selectRemoteProjects, selectSyncItems } from '../../redux/selectors';
import { selectSyncItems } from '../../redux/selectors';
import { WorkspaceLoaderData } from '../../routes/workspace';
import { Dropdown, DropdownButton, DropdownItem, DropdownSection, ItemContent } from '../base/dropdown';
import { Link } from '../base/link';
@@ -81,11 +81,13 @@ export const SyncDropdown: FC<Props> = ({ vcs, workspace, project }) => {
});
const { organizationId, projectId } = useParams<{ organizationId: string; projectId: string }>();
const navigate = useNavigate();
const remoteProjects = useSelector(selectRemoteProjects);
const syncItems = useSelector(selectSyncItems);
const {
activeWorkspaceMeta,
projects,
} = useRouteLoaderData(':workspaceId') as WorkspaceLoaderData;
const remoteProjects = projects.filter(isRemoteProject);
const refetchRemoteBranch = useCallback(async () => {
if (session.isLoggedIn()) {
try {
@@ -130,7 +132,10 @@ export const SyncDropdown: FC<Props> = ({ vcs, workspace, project }) => {
}, REFRESH_PERIOD);
const [isGitRepoSettingsModalOpen, setIsGitRepoSettingsModalOpen] = useState(false);
const [isSyncDeleteModalOpen, setIsSyncDeleteModalOpen] = useState(false);
// const [isSyncHistoryModalOpen, setIsSyncHistoryModalOpen] = useState(false);
// const [isSyncStagingModalOpen, setIsSyncStagingModalOpen] = useState(false);
const [isSyncBranchesModalOpen, setIsSyncBranchesModalOpen] = useState(false);
useMount(async () => {
setState(state => ({
...state,
@@ -577,7 +582,7 @@ export const SyncDropdown: FC<Props> = ({ vcs, workspace, project }) => {
<ItemContent
icon="code-fork"
label="Branches"
onClick={() => showModal(SyncBranchesModal, { onHide: refreshVCSAndRefetchRemote })}
onClick={() => setIsSyncBranchesModalOpen(true)}
/>
</DropdownItem>
@@ -586,7 +591,7 @@ export const SyncDropdown: FC<Props> = ({ vcs, workspace, project }) => {
icon="remove"
isDisabled={historyCount === 0}
label={<>Delete {strings.collection.singular}</>}
onClick={() => showModal(SyncDeleteModal, { onHide: refreshVCSAndRefetchRemote })}
onClick={() => setIsSyncDeleteModalOpen(true)}
/>
</DropdownItem>
</DropdownSection>
@@ -675,6 +680,24 @@ export const SyncDropdown: FC<Props> = ({ vcs, workspace, project }) => {
onHide={() => setIsGitRepoSettingsModalOpen(false)}
/>
)}
{isSyncDeleteModalOpen && (
<SyncDeleteModal
vcs={vcs}
onHide={() => {
refreshVCSAndRefetchRemote();
setIsSyncDeleteModalOpen(false);
}}
/>
)}
{isSyncBranchesModalOpen && (
<SyncBranchesModal
vcs={vcs}
onHide={() => {
refreshVCSAndRefetchRemote();
setIsSyncBranchesModalOpen(false);
}}
/>
)}
</div>
);
};

View File

@@ -88,6 +88,7 @@ export const WorkspaceCardDropdown: FC<Props> = props => {
const fetcher = useFetcher();
const [isDuplicateModalOpen, setIsDuplicateModalOpen] = useState(false);
const [isImportModalOpen, setIsImportModalOpen] = useState(false);
const [isExportModalOpen, setIsExportModalOpen] = useState(false);
const [isSettingsModalOpen, setIsSettingsModalOpen] = useState(false);
const {
organizationId,
@@ -151,7 +152,7 @@ export const WorkspaceCardDropdown: FC<Props> = props => {
<ItemContent
label="Export"
icon="file-export"
onClick={() => showModal(ExportRequestsModal)}
onClick={() => setIsExportModalOpen(true)}
/>
</DropdownItem>
<DropdownItem aria-label='Settings'>
@@ -214,6 +215,11 @@ export const WorkspaceCardDropdown: FC<Props> = props => {
defaultWorkspaceId={workspace._id}
/>
)}
{isExportModalOpen && (
<ExportRequestsModal
onHide={() => setIsExportModalOpen(false)}
/>
)}
{isSettingsModalOpen && (
<WorkspaceSettingsModal
workspace={workspace}

View File

@@ -17,7 +17,7 @@ import { useAIContext } from '../../context/app/ai-context';
import { WorkspaceLoaderData } from '../../routes/workspace';
import { Dropdown, DropdownButton, type DropdownHandle, DropdownItem, DropdownSection, ItemContent } from '../base/dropdown';
import { InsomniaAI } from '../insomnia-ai-icon';
import { showError, showModal, showPrompt } from '../modals';
import { showError, showPrompt } from '../modals';
import { ExportRequestsModal } from '../modals/export-requests-modal';
import { configGenerators, showGenerateConfigModal } from '../modals/generate-config-modal';
import { ImportModal } from '../modals/import-modal';
@@ -40,6 +40,7 @@ export const WorkspaceDropdown: FC = () => {
const activeWorkspaceName = activeWorkspace.name;
const [isDuplicateModalOpen, setIsDuplicateModalOpen] = useState(false);
const [isImportModalOpen, setIsImportModalOpen] = useState(false);
const [isExportModalOpen, setIsExportModalOpen] = useState(false);
const [isSettingsModalOpen, setIsSettingsModalOpen] = useState(false);
const workspaceName = activeWorkspace.name;
const projectName = activeProject.name ?? getProductName();
@@ -166,13 +167,13 @@ export const WorkspaceDropdown: FC = () => {
/>
</DropdownItem>
<DropdownItem aria-label='Export'>
<ItemContent
icon="file-export"
label="Export"
onClick={() => showModal(ExportRequestsModal)}
/>
</DropdownItem>
<DropdownItem aria-label='Export'>
<ItemContent
icon="file-export"
label="Export"
onClick={() => setIsExportModalOpen(true)}
/>
</DropdownItem>
<DropdownItem aria-label="Settings">
<ItemContent
@@ -272,6 +273,11 @@ export const WorkspaceDropdown: FC = () => {
defaultWorkspaceId={workspaceId}
/>
)}
{isExportModalOpen && (
<ExportRequestsModal
onHide={() => setIsExportModalOpen(false)}
/>
)}
{isSettingsModalOpen && (
<WorkspaceSettingsModal
workspace={activeWorkspace}

View File

@@ -14,7 +14,6 @@ import {
AUTH_OAUTH_1,
AUTH_OAUTH_2,
} from '../../../../common/constants';
import { Request } from '../../../../models/request';
import { RequestLoaderData } from '../../../routes/request';
import { ApiKeyAuth } from './api-key-auth';
import { AsapAuth } from './asap-auth';
@@ -29,7 +28,7 @@ import { OAuth1Auth } from './o-auth-1-auth';
import { OAuth2Auth } from './o-auth-2-auth';
export const AuthWrapper: FC<{ disabled?: boolean }> = ({ disabled = false }) => {
const { activeRequest } = useRouteLoaderData('request/:requestId') as RequestLoaderData<Request, any>;
const { activeRequest } = useRouteLoaderData('request/:requestId') as RequestLoaderData;
const { authentication: { type } } = activeRequest;

View File

@@ -2,8 +2,7 @@ import classnames from 'classnames';
import React, { FC, PropsWithChildren } from 'react';
import { useRouteLoaderData } from 'react-router-dom';
import { Request } from '../../../../../models/request';
import { RequestAccordionKeys, RequestMeta } from '../../../../../models/request-meta';
import { RequestAccordionKeys } from '../../../../../models/request-meta';
import { useRequestMetaPatcher } from '../../../../hooks/use-request';
import { RequestLoaderData } from '../../../../routes/request';
@@ -13,7 +12,7 @@ interface Props {
}
export const AuthAccordion: FC<PropsWithChildren<Props>> = ({ accordionKey, label, children }) => {
const { activeRequest, activeRequestMeta } = useRouteLoaderData('request/:requestId') as RequestLoaderData<Request, RequestMeta>;
const { activeRequest, activeRequestMeta } = useRouteLoaderData('request/:requestId') as RequestLoaderData;
const expanded = Boolean(activeRequestMeta?.expandedAccordionKeys[accordionKey]);
const patchRequestMeta = useRequestMetaPatcher();

View File

@@ -3,7 +3,6 @@ import { useRouteLoaderData } from 'react-router-dom';
import { useToggle } from 'react-use';
import { toKebabCase } from '../../../../../common/misc';
import { Request } from '../../../../../models/request';
import { useRequestPatcher } from '../../../../hooks/use-request';
import { RequestLoaderData } from '../../../../routes/request';
import { RootLoaderData } from '../../../../routes/root';
@@ -23,7 +22,7 @@ export const AuthInputRow: FC<Props> = ({ label, getAutocompleteConstants, prope
settings,
} = useRouteLoaderData('root') as RootLoaderData;
const { showPasswords } = settings;
const { activeRequest: { authentication, _id: requestId } } = useRouteLoaderData('request/:requestId') as RequestLoaderData<Request, any>;
const { activeRequest: { authentication, _id: requestId } } = useRouteLoaderData('request/:requestId') as RequestLoaderData;
const patchRequest = useRequestPatcher();
const [masked, toggleMask] = useToggle(true);
const canBeMasked = !showPasswords && mask;

View File

@@ -2,7 +2,6 @@ import React, { FC, ReactNode, useCallback } from 'react';
import { useRouteLoaderData } from 'react-router-dom';
import { toKebabCase } from '../../../../../common/misc';
import { Request } from '../../../../../models/request';
import { useNunjucks } from '../../../../context/nunjucks/use-nunjucks';
import { useRequestPatcher } from '../../../../hooks/use-request';
import { RequestLoaderData } from '../../../../routes/request';
@@ -30,7 +29,7 @@ interface Props {
}
export const AuthPrivateKeyRow: FC<Props> = ({ label, property, help }) => {
const { activeRequest: { authentication, _id: requestId } } = useRouteLoaderData('request/:requestId') as RequestLoaderData<Request, any>;
const { activeRequest: { authentication, _id: requestId } } = useRouteLoaderData('request/:requestId') as RequestLoaderData;
const patchRequest = useRequestPatcher();
const { handleGetRenderContext, handleRender } = useNunjucks();

View File

@@ -2,7 +2,6 @@ import classnames from 'classnames';
import React, { FC, PropsWithChildren, ReactNode } from 'react';
import { useRouteLoaderData } from 'react-router-dom';
import { Request } from '../../../../../models/request';
import { RequestLoaderData } from '../../../../routes/request';
import { HelpTooltip } from '../../../help-tooltip';
@@ -14,7 +13,7 @@ interface Props {
}
export const AuthRow: FC<PropsWithChildren<Props>> = ({ labelFor, label, help, disabled, children }) => {
const { activeRequest: { authentication } } = useRouteLoaderData('request/:requestId') as RequestLoaderData<Request, any>;
const { activeRequest: { authentication } } = useRouteLoaderData('request/:requestId') as RequestLoaderData;
return (
<tr key={labelFor}>

View File

@@ -2,7 +2,6 @@ import React, { ChangeEvent, FC, ReactNode, useCallback } from 'react';
import { useRouteLoaderData } from 'react-router-dom';
import { toKebabCase } from '../../../../../common/misc';
import { Request } from '../../../../../models/request';
import { useRequestPatcher } from '../../../../hooks/use-request';
import { RequestLoaderData } from '../../../../routes/request';
import { AuthRow } from './auth-row';
@@ -19,7 +18,7 @@ interface Props {
}
export const AuthSelectRow: FC<Props> = ({ label, property, help, options, disabled }) => {
const { activeRequest: { authentication, _id: requestId } } = useRouteLoaderData('request/:requestId') as RequestLoaderData<Request, any>;
const { activeRequest: { authentication, _id: requestId } } = useRouteLoaderData('request/:requestId') as RequestLoaderData;
const patchRequest = useRequestPatcher();
const selectedValue = authentication.hasOwnProperty(property) ? authentication[property] : options[0].value;

View File

@@ -2,7 +2,6 @@ import React, { FC, ReactNode, useCallback } from 'react';
import { useRouteLoaderData } from 'react-router-dom';
import { toKebabCase } from '../../../../../common/misc';
import { Request } from '../../../../../models/request';
import { useRequestPatcher } from '../../../../hooks/use-request';
import { RequestLoaderData } from '../../../../routes/request';
import { AuthRow } from './auth-row';
@@ -28,7 +27,7 @@ export const AuthToggleRow: FC<Props> = ({
offTitle = 'Enable item',
disabled = false,
}) => {
const { activeRequest: { authentication, _id: requestId } } = useRouteLoaderData('request/:requestId') as RequestLoaderData<Request, any>;
const { activeRequest: { authentication, _id: requestId } } = useRouteLoaderData('request/:requestId') as RequestLoaderData;
const patchRequest = useRequestPatcher();
const databaseValue = Boolean(authentication[property]);

View File

@@ -1,7 +1,6 @@
import React, { FC } from 'react';
import { useRouteLoaderData } from 'react-router-dom';
import { Request } from '../../../../models/request';
import {
OAuth1SignatureMethod,
SIGNATURE_METHOD_HMAC_SHA1,
@@ -35,7 +34,7 @@ const signatureMethodOptions: {name: string; value: OAuth1SignatureMethod}[] = [
}];
export const OAuth1Auth: FC = () => {
const { activeRequest: { authentication: { signatureMethod } } } = useRouteLoaderData('request/:requestId') as RequestLoaderData<Request, any>;
const { activeRequest: { authentication: { signatureMethod } } } = useRouteLoaderData('request/:requestId') as RequestLoaderData;
return (
<AuthTableBody>

View File

@@ -244,7 +244,7 @@ const getFieldsForGrantType = (authentication: Request['authentication']) => {
};
export const OAuth2Auth: FC = () => {
const { activeRequest: { authentication } } = useRouteLoaderData('request/:requestId') as RequestLoaderData<Request, any>;
const { activeRequest: { authentication } } = useRouteLoaderData('request/:requestId') as RequestLoaderData;
const { basic, advanced } = getFieldsForGrantType(authentication);
@@ -331,7 +331,7 @@ const renderAccessTokenExpiry = (token?: Pick<OAuth2Token, 'accessToken' | 'expi
};
const OAuth2TokenInput: FC<{ token: OAuth2Token | null; label: string; property: keyof Pick<OAuth2Token, 'accessToken' | 'refreshToken' | 'identityToken'> }> = ({ token, label, property }) => {
const { activeRequest } = useRouteLoaderData('request/:requestId') as RequestLoaderData<Request, any>;
const { activeRequest } = useRouteLoaderData('request/:requestId') as RequestLoaderData;
const onChange = async ({ currentTarget: { value } }: ChangeEvent<HTMLInputElement>) => {
if (token) {
@@ -414,7 +414,7 @@ const OAuth2Error: FC<{ token: OAuth2Token | null }> = ({ token }) => {
};
const OAuth2Tokens: FC = () => {
const { activeRequest: { authentication, _id: requestId } } = useRouteLoaderData('request/:requestId') as RequestLoaderData<Request, any>;
const { activeRequest: { authentication, _id: requestId } } = useRouteLoaderData('request/:requestId') as RequestLoaderData;
const [token, setToken] = useState<OAuth2Token | null>(null);
useEffect(() => {
const fn = async () => {

View File

@@ -2,10 +2,10 @@ import React, { FC, useCallback } from 'react';
import { useParams, useRouteLoaderData } from 'react-router-dom';
import { getCommonHeaderNames, getCommonHeaderValues } from '../../../common/common-headers';
import type { Request, RequestHeader } from '../../../models/request';
import { isWebSocketRequest, WebSocketRequest } from '../../../models/websocket-request';
import type { RequestHeader } from '../../../models/request';
import { isWebSocketRequest } from '../../../models/websocket-request';
import { useRequestPatcher } from '../../hooks/use-request';
import { RequestLoaderData } from '../../routes/request';
import { RequestLoaderData, WebSocketRequestLoaderData } from '../../routes/request';
import { CodeEditor } from '../codemirror/code-editor';
import { KeyValueEditor } from '../key-value-editor/key-value-editor';
@@ -18,7 +18,7 @@ export const RequestHeadersEditor: FC<Props> = ({
bulk,
isDisabled,
}) => {
const { activeRequest } = useRouteLoaderData('request/:requestId') as RequestLoaderData<Request | WebSocketRequest, any>;
const { activeRequest } = useRouteLoaderData('request/:requestId') as RequestLoaderData | WebSocketRequestLoaderData;
const patchRequest = useRequestPatcher();
const { requestId } = useParams() as { requestId: string };

View File

@@ -1,10 +1,9 @@
import React, { FC, useCallback } from 'react';
import { useParams, useRouteLoaderData } from 'react-router-dom';
import { Request, RequestParameter } from '../../../models/request';
import { WebSocketRequest } from '../../../models/websocket-request';
import { RequestParameter } from '../../../models/request';
import { useRequestPatcher } from '../../hooks/use-request';
import { RequestLoaderData } from '../../routes/request';
import { RequestLoaderData, WebSocketRequestLoaderData } from '../../routes/request';
import { CodeEditor } from '../codemirror/code-editor';
import { KeyValueEditor } from '../key-value-editor/key-value-editor';
@@ -18,7 +17,7 @@ export const RequestParametersEditor: FC<Props> = ({
disabled = false,
}) => {
const { requestId } = useParams() as { requestId: string };
const { activeRequest } = useRouteLoaderData('request/:requestId') as RequestLoaderData<Request | WebSocketRequest, any>;
const { activeRequest } = useRouteLoaderData('request/:requestId') as RequestLoaderData | WebSocketRequestLoaderData;
const patchRequest = useRequestPatcher();
const handleBulkUpdate = useCallback((paramsString: string) => {
const parameters: {

View File

@@ -1,6 +1,7 @@
import clone from 'clone';
import { isValid } from 'date-fns';
import React, { forwardRef, useImperativeHandle, useRef, useState } from 'react';
import React, { useEffect, useRef, useState } from 'react';
import { OverlayContainer } from 'react-aria';
import { useRouteLoaderData } from 'react-router-dom';
import { useFetcher, useParams } from 'react-router-dom';
import { Cookie as ToughCookie } from 'tough-cookie';
@@ -17,29 +18,16 @@ import { OneLineEditor } from '../codemirror/one-line-editor';
export interface CookieModifyModalOptions {
cookie: Cookie;
}
export interface CookieModifyModalHandle {
show: (options: CookieModifyModalOptions) => void;
hide: () => void;
}
export const CookieModifyModal = forwardRef<CookieModifyModalHandle, ModalProps>((_, ref) => {
export const CookieModifyModal = ((props: ModalProps & CookieModifyModalOptions) => {
const modalRef = useRef<ModalHandle>(null);
const [cookie, setCookie] = useState<Cookie | null>(null);
const [cookie, setCookie] = useState<Cookie | null>(props.cookie);
const { activeCookieJar } = useRouteLoaderData(':workspaceId') as WorkspaceLoaderData;
const { organizationId, projectId, workspaceId } = useParams<{ organizationId: string; projectId: string; workspaceId: string }>();
const updateCookieJarFetcher = useFetcher<CookieJar>();
useImperativeHandle(ref, () => ({
hide: () => {
modalRef.current?.hide();
},
show: ({ cookie }) => {
if (!activeCookieJar.cookies.find(c => c.id === cookie.id)) {
return;
}
setCookie(cookie);
modalRef.current?.show();
},
}), [activeCookieJar.cookies]);
useEffect(() => {
modalRef.current?.show();
}, []);
const updateCookieJar = async (cookieJarId: string, patch: CookieJar) => {
updateCookieJarFetcher.submit(JSON.stringify({ patch, cookieJarId }), {
encType: 'application/json',
@@ -88,119 +76,120 @@ export const CookieModifyModal = forwardRef<CookieModifyModalHandle, ModalProps>
}
}
return (
<Modal ref={modalRef}>
<ModalHeader>Edit Cookie</ModalHeader>
<ModalBody className="cookie-modify">
{activeCookieJar && cookie && (
<Tabs aria-label="Cookie modify tabs">
<TabItem key="friendly" title="Friendly">
<PanelContainer className="pad">
<div className="form-row">
<div className="form-control form-control--outlined">
<label data-testid="CookieKey">
Key
<OneLineEditor
defaultValue={(cookie && cookie.key || '').toString()}
onChange={value => handleCookieUpdate(Object.assign({}, cookie, { key: value.trim() }))}
/>
</label>
<OverlayContainer>
<Modal ref={modalRef} onHide={props.onHide}>
<ModalHeader>Edit Cookie</ModalHeader>
<ModalBody className="cookie-modify">
{activeCookieJar && cookie && (
<Tabs aria-label="Cookie modify tabs">
<TabItem key="friendly" title="Friendly">
<PanelContainer className="pad">
<div className="form-row">
<div className="form-control form-control--outlined">
<label data-testid="CookieKey">
Key
<OneLineEditor
defaultValue={(cookie && cookie.key || '').toString()}
onChange={value => handleCookieUpdate(Object.assign({}, cookie, { key: value.trim() }))}
/>
</label>
</div>
<div className="form-control form-control--outlined">
<label data-testid="CookieValue">
Value
<OneLineEditor
defaultValue={(cookie && cookie.value || '').toString()}
onChange={value => handleCookieUpdate(Object.assign({}, cookie, { value: value.trim() }))}
/>
</label>
</div>
</div>
<div className="form-row">
<div className="form-control form-control--outlined">
<label data-testid="CookieDomain">
Domain
<OneLineEditor
defaultValue={(cookie && cookie.domain || '').toString()}
onChange={value => handleCookieUpdate(Object.assign({}, cookie, { domain: value.trim() }))}
/>
</label>
</div>
<div className="form-control form-control--outlined">
<label data-testid="CookiePath">
Path
<OneLineEditor
defaultValue={(cookie && cookie.path || '').toString()}
onChange={value => handleCookieUpdate(Object.assign({}, cookie, { path: value.trim() }))}
/>
</label>
</div>
</div>
<div className="form-control form-control--outlined">
<label data-testid="CookieValue">
Value
<OneLineEditor
defaultValue={(cookie && cookie.value || '').toString()}
onChange={value => handleCookieUpdate(Object.assign({}, cookie, { value: value.trim() }))}
/>
<label data-testid="CookieExpires">
Expires
<input type="datetime-local" defaultValue={localDateTime} onChange={event => handleCookieUpdate(Object.assign({}, cookie, { expires: event.target.value }))} />
</label>
</div>
</div>
<div className="form-row">
<div className="form-control form-control--outlined">
<label data-testid="CookieDomain">
Domain
<OneLineEditor
defaultValue={(cookie && cookie.domain || '').toString()}
onChange={value => handleCookieUpdate(Object.assign({}, cookie, { domain: value.trim() }))}
/>
</label>
</div>
<div className="form-control form-control--outlined">
<label data-testid="CookiePath">
Path
<OneLineEditor
defaultValue={(cookie && cookie.path || '').toString()}
onChange={value => handleCookieUpdate(Object.assign({}, cookie, { path: value.trim() }))}
/>
</label>
</div>
</div>
<div className="form-control form-control--outlined">
<label data-testid="CookieExpires">
Expires
<input type="datetime-local" defaultValue={localDateTime} onChange={event => handleCookieUpdate(Object.assign({}, cookie, { expires: event.target.value }))} />
</label>
</div>
</PanelContainer>
<div className="pad no-pad-top cookie-modify__checkboxes row-around txt-lg">
<label>
Secure
<input
className="space-left"
type="checkbox"
name="secure"
defaultChecked={cookie.secure || false}
onChange={event => handleCookieUpdate(Object.assign({}, cookie, { secure: event.target.checked }))}
/>
</label>
<label>
httpOnly
<input
className="space-left"
type="checkbox"
name="httpOnly"
defaultChecked={cookie.httpOnly || false}
onChange={event => handleCookieUpdate(Object.assign({}, cookie, { httpOnly: event.target.checked }))}
/>
</label>
</div>
</TabItem>
<TabItem key="raw" title="Raw">
<PanelContainer className="pad">
<div className="form-control form-control--outlined">
</PanelContainer>
<div className="pad no-pad-top cookie-modify__checkboxes row-around txt-lg">
<label>
Raw Cookie String
Secure
<input
type="text"
onChange={event => {
try {
// NOTE: Perform toJSON so we have a plain JS object instead of Cookie instance
const parsed = ToughCookie.parse(event.target.value)?.toJSON();
if (parsed) {
// Make sure cookie has an id
parsed.id = cookie.id;
handleCookieUpdate(parsed);
}
} catch (err) {
console.warn(`Failed to parse cookie string "${event.target.value}"`, err);
return;
}
}}
defaultValue={rawDefaultValue}
className="space-left"
type="checkbox"
name="secure"
defaultChecked={cookie.secure || false}
onChange={event => handleCookieUpdate(Object.assign({}, cookie, { secure: event.target.checked }))}
/>
</label>
<label>
httpOnly
<input
className="space-left"
type="checkbox"
name="httpOnly"
defaultChecked={cookie.httpOnly || false}
onChange={event => handleCookieUpdate(Object.assign({}, cookie, { httpOnly: event.target.checked }))}
/>
</label>
</div>
</PanelContainer>
</TabItem>
</Tabs>
)}
</ModalBody>
<ModalFooter>
<button className="btn" onClick={() => modalRef.current?.hide()}>
Done
</button>
</ModalFooter>
</Modal>
</TabItem>
<TabItem key="raw" title="Raw">
<PanelContainer className="pad">
<div className="form-control form-control--outlined">
<label>
Raw Cookie String
<input
type="text"
onChange={event => {
try {
// NOTE: Perform toJSON so we have a plain JS object instead of Cookie instance
const parsed = ToughCookie.parse(event.target.value)?.toJSON();
if (parsed) {
// Make sure cookie has an id
parsed.id = cookie.id;
handleCookieUpdate(parsed);
}
} catch (err) {
console.warn(`Failed to parse cookie string "${event.target.value}"`, err);
return;
}
}}
defaultValue={rawDefaultValue}
/>
</label>
</div>
</PanelContainer>
</TabItem>
</Tabs>
)}
</ModalBody>
<ModalFooter>
<button className="btn" onClick={() => modalRef.current?.hide()}>
Done
</button>
</ModalFooter>
</Modal>
</OverlayContainer>
);
});
CookieModifyModal.displayName = 'CookieModifyModal';

View File

@@ -1,4 +1,5 @@
import React, { forwardRef, useImperativeHandle, useRef, useState } from 'react';
import React, { useEffect, useRef, useState } from 'react';
import { OverlayContainer } from 'react-aria';
import { useFetcher, useParams, useRouteLoaderData } from 'react-router-dom';
import { fuzzyMatch } from '../../../common/misc';
@@ -10,12 +11,8 @@ import { ModalBody } from '../base/modal-body';
import { ModalFooter } from '../base/modal-footer';
import { ModalHeader } from '../base/modal-header';
import { CookieList } from '../cookie-list';
import { showModal } from '.';
export interface CookiesModalHandle {
show: () => void;
hide: () => void;
}
export const CookiesModal = forwardRef<CookiesModalHandle, ModalProps>((_, ref) => {
export const CookiesModal = ({ onHide }: ModalProps) => {
const modalRef = useRef<ModalHandle>(null);
const { handleRender } = useNunjucks();
const [filter, setFilter] = useState<string>('');
@@ -23,15 +20,10 @@ export const CookiesModal = forwardRef<CookiesModalHandle, ModalProps>((_, ref)
const { activeCookieJar } = useRouteLoaderData(':workspaceId') as WorkspaceLoaderData;
const { organizationId, projectId, workspaceId } = useParams<{ organizationId: string; projectId: string; workspaceId: string }>();
const updateCookieJarFetcher = useFetcher<CookieJar>();
useEffect(() => {
modalRef.current?.show();
}, []);
useImperativeHandle(ref, () => ({
hide: () => {
modalRef.current?.hide();
},
show: () => {
modalRef.current?.show();
},
}), []);
const updateCookieJar = async (cookieJarId: string, patch: CookieJar) => {
updateCookieJarFetcher.submit(JSON.stringify({ patch, cookieJarId }), {
encType: 'application/json',
@@ -41,80 +33,79 @@ export const CookiesModal = forwardRef<CookiesModalHandle, ModalProps>((_, ref)
};
const filteredCookies = visibleCookieIndexes ? (activeCookieJar?.cookies || []).filter((_, i) => visibleCookieIndexes.includes(i)) : (activeCookieJar?.cookies || []);
return (
<Modal ref={modalRef} wide tall>
<ModalHeader>Manage Cookies</ModalHeader>
<ModalBody noScroll>
{activeCookieJar && (
<div className="cookie-list">
<div className="pad">
<div className="form-control form-control--outlined">
<label>
Filter Cookies
<input
onChange={async event => {
setFilter(event.target.value);
const renderedCookies: Cookie[] = [];
for (const cookie of (activeCookieJar?.cookies || [])) {
try {
renderedCookies.push(await handleRender(cookie));
} catch (err) {
renderedCookies.push(cookie);
<OverlayContainer>
<Modal ref={modalRef} wide tall onHide={onHide}>
<ModalHeader>Manage Cookies</ModalHeader>
<ModalBody noScroll>
{activeCookieJar && (
<div className="cookie-list">
<div className="pad">
<div className="form-control form-control--outlined">
<label>
Filter Cookies
<input
onChange={async event => {
setFilter(event.target.value);
const renderedCookies: Cookie[] = [];
for (const cookie of (activeCookieJar?.cookies || [])) {
try {
renderedCookies.push(await handleRender(cookie));
} catch (err) {
renderedCookies.push(cookie);
}
}
}
if (!filter) {
setVisibleCookieIndexes(null);
}
const visibleCookieIndexes: number[] = [];
renderedCookies.forEach((cookie, i) => {
if (fuzzyMatch(filter, JSON.stringify(cookie), { splitSpace: true })) {
visibleCookieIndexes.push(i);
if (!filter) {
setVisibleCookieIndexes(null);
}
});
setVisibleCookieIndexes(visibleCookieIndexes);
}}
type="text"
placeholder="insomnia.rest"
defaultValue=""
/>
</label>
const visibleCookieIndexes: number[] = [];
renderedCookies.forEach((cookie, i) => {
if (fuzzyMatch(filter, JSON.stringify(cookie), { splitSpace: true })) {
visibleCookieIndexes.push(i);
}
});
setVisibleCookieIndexes(visibleCookieIndexes);
}}
type="text"
placeholder="insomnia.rest"
defaultValue=""
/>
</label>
</div>
</div>
<div className="cookie-list__list border-tops pad">
<CookieList
cookies={filteredCookies}
handleDeleteAll={() => {
const updated = activeCookieJar;
updated.cookies = [];
updateCookieJar(activeCookieJar._id, updated);
}}
handleCookieAdd={cookie => {
const updated = activeCookieJar;
updated.cookies = [cookie, ...activeCookieJar.cookies];
updateCookieJar(activeCookieJar._id, updated);
}}
handleCookieDelete={cookie => {
const updated = activeCookieJar;
updated.cookies = activeCookieJar.cookies.filter(c => c.id !== cookie.id);
updateCookieJar(activeCookieJar._id, updated);
}}
// Set the domain to the filter so that it shows up if we're filtering
newCookieDomainName={filter || 'domain.com'}
/>
</div>
</div>
<div className="cookie-list__list border-tops pad">
<CookieList
cookies={filteredCookies}
handleDeleteAll={() => {
const updated = activeCookieJar;
updated.cookies = [];
updateCookieJar(activeCookieJar._id, updated);
}}
handleCookieAdd={cookie => {
const updated = activeCookieJar;
updated.cookies = [cookie, ...activeCookieJar.cookies];
updateCookieJar(activeCookieJar._id, updated);
}}
handleCookieDelete={cookie => {
const updated = activeCookieJar;
updated.cookies = activeCookieJar.cookies.filter(c => c.id !== cookie.id);
updateCookieJar(activeCookieJar._id, updated);
}}
// Set the domain to the filter so that it shows up if we're filtering
newCookieDomainName={filter || 'domain.com'}
/>
</div>
)}
</ModalBody>
<ModalFooter>
<div className="margin-left faint italic txt-sm">
* cookies are automatically sent with relevant requests
</div>
)}
</ModalBody>
<ModalFooter>
<div className="margin-left faint italic txt-sm">
* cookies are automatically sent with relevant requests
</div>
<button className="btn" onClick={() => modalRef.current?.hide()}>
Done
</button>
</ModalFooter>
</Modal>
<button className="btn" onClick={() => modalRef.current?.hide()}>
Done
</button>
</ModalFooter>
</Modal>
</OverlayContainer>
);
});
CookiesModal.displayName = 'CookiesModal';
export const showCookiesModal = () => showModal(CookiesModal);
};

View File

@@ -1,5 +1,6 @@
import React, { forwardRef, useCallback, useImperativeHandle, useRef, useState } from 'react';
import { useSelector } from 'react-redux';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { OverlayContainer } from 'react-aria';
import { useRouteLoaderData } from 'react-router-dom';
import { exportRequestsToFile } from '../../../common/export';
import * as models from '../../../models';
@@ -7,7 +8,7 @@ import { GrpcRequest, isGrpcRequest } from '../../../models/grpc-request';
import { isRequest, Request } from '../../../models/request';
import { isRequestGroup, RequestGroup } from '../../../models/request-group';
import { isWebSocketRequest, WebSocketRequest } from '../../../models/websocket-request';
import { selectSidebarChildren } from '../../redux/selectors';
import { WorkspaceLoaderData } from '../../routes/workspace';
import { Modal, type ModalHandle, ModalProps } from '../base/modal';
import { ModalBody } from '../base/modal-body';
import { ModalFooter } from '../base/modal-footer';
@@ -29,10 +30,13 @@ export interface ExportRequestsModalHandle {
show: () => void;
hide: () => void;
}
export const ExportRequestsModal = forwardRef<ExportRequestsModalHandle, ModalProps>((props, ref) => {
export const ExportRequestsModal = ({ onHide }: ModalProps) => {
const modalRef = useRef<ModalHandle>(null);
const [state, setState] = useState<State>({ treeRoot: null });
const sidebarChildren = useSelector(selectSidebarChildren);
const {
requestTree,
} = useRouteLoaderData(':workspaceId') as WorkspaceLoaderData;
const createNode = useCallback((item: Record<string, any>): Node => {
const children: Node[] = item.children.map((child: Record<string, any>) => createNode(child));
let totalRequests = children
@@ -50,39 +54,35 @@ export const ExportRequestsModal = forwardRef<ExportRequestsModalHandle, ModalPr
selectedRequests: totalRequests, // Default select all
};
}, []);
useImperativeHandle(ref, () => ({
hide: () => {
modalRef.current?.hide();
},
show: () => {
modalRef.current?.show();
const childObjects = sidebarChildren.all;
const children: Node[] = childObjects.map(child => createNode(child));
const totalRequests = children
.map(child => child.totalRequests)
.reduce((acc, totalRequests) => acc + totalRequests, 0);
// @ts-expect-error -- TSCONVERSION missing property
const rootFolder: RequestGroup = {
...models.requestGroup.init(),
_id: 'all',
type: models.requestGroup.type,
name: 'All requests',
parentId: '',
modified: 0,
created: 0,
};
setState({
treeRoot: {
doc: rootFolder,
collapsed: false,
children: children,
totalRequests: totalRequests,
selectedRequests: totalRequests, // Default select all
},
});
const children: Node[] = requestTree.map(child => createNode(child));
const totalRequests = children
.map(child => child.totalRequests)
.reduce((acc, totalRequests) => acc + totalRequests, 0);
// @ts-expect-error -- TSCONVERSION missing property
const rootFolder: RequestGroup = {
...models.requestGroup.init(),
_id: 'all',
type: models.requestGroup.type,
name: 'All requests',
parentId: '',
modified: 0,
created: 0,
};
const [state, setState] = useState<State>({
treeRoot: {
doc: rootFolder,
collapsed: false,
children: children,
totalRequests: totalRequests,
selectedRequests: totalRequests, // Default select all
},
}), [createNode, sidebarChildren.all]);
});
useEffect(() => {
modalRef.current?.show();
}, []);
const getSelectedRequestIds = (node: Node): string[] => {
const docIsRequest = isRequest(node.doc) || isWebSocketRequest(node.doc) || isGrpcRequest(node.doc);
if (docIsRequest && node.selectedRequests === node.totalRequests) {
@@ -168,32 +168,33 @@ export const ExportRequestsModal = forwardRef<ExportRequestsModalHandle, ModalPr
const { treeRoot } = state;
const isExportDisabled = treeRoot != null ? treeRoot.selectedRequests === 0 : false;
return (
<Modal ref={modalRef} tall {...props}>
<ModalHeader>Select Requests to Export</ModalHeader>
<ModalBody>
<div className="requests-tree">
<Tree
root={treeRoot}
handleSetRequestGroupCollapsed={handleSetRequestGroupCollapsed}
handleSetItemSelected={handleSetItemSelected}
/>
</div>
</ModalBody>
<ModalFooter>
<div>
<button className="btn" onClick={() => modalRef.current?.hide()}>
Cancel
</button>
<button
className="btn"
onClick={handleExport}
disabled={isExportDisabled}
>
Export
</button>
</div>
</ModalFooter>
</Modal>
<OverlayContainer>
<Modal ref={modalRef} tall onHide={onHide}>
<ModalHeader>Select Requests to Export</ModalHeader>
<ModalBody>
<div className="requests-tree">
<Tree
root={treeRoot}
handleSetRequestGroupCollapsed={handleSetRequestGroupCollapsed}
handleSetItemSelected={handleSetItemSelected}
/>
</div>
</ModalBody>
<ModalFooter>
<div>
<button className="btn" onClick={() => modalRef.current?.hide()}>
Cancel
</button>
<button
className="btn"
onClick={handleExport}
disabled={isExportDisabled}
>
Export
</button>
</div>
</ModalFooter>
</Modal>
</OverlayContainer>
);
});
ExportRequestsModal.displayName = 'ExportRequestsModal';
};

View File

@@ -21,7 +21,7 @@ export const ProjectSettingsModal: FC<ProjectSettingsModalProps> = ({ project, o
useEffect(() => {
modalRef.current?.show();
});
}, []);
const isRemote = isRemoteProject(project);

View File

@@ -1,11 +1,11 @@
import React, { forwardRef, useImperativeHandle, useRef, useState } from 'react';
import React, { useEffect, useRef, useState } from 'react';
import { OverlayContainer } from 'react-aria';
import { useSelector } from 'react-redux';
import { useFetcher, useParams } from 'react-router-dom';
import { database as db } from '../../../common/database';
import * as models from '../../../models';
import type { RequestGroup } from '../../../models/request-group';
import type { Workspace } from '../../../models/workspace';
import { invariant } from '../../../utils/invariant';
import { useRequestGroupPatcher } from '../../hooks/use-request';
import { selectWorkspacesForActiveProject } from '../../redux/selectors';
import { Modal, type ModalHandle, ModalProps } from '../base/modal';
import { ModalBody } from '../base/modal-body';
@@ -16,127 +16,81 @@ import { MarkdownEditor } from '../markdown-editor';
export interface RequestGroupSettingsModalOptions {
requestGroup: RequestGroup;
forceEditMode?: boolean;
}
interface State {
requestGroup: RequestGroup | null;
showDescription: boolean;
defaultPreviewMode: boolean;
activeWorkspaceIdToCopyTo: string | null;
workspace?: Workspace;
workspacesForActiveProject: Workspace[];
}
export interface RequestGroupSettingsModalHandle {
show: (options: RequestGroupSettingsModalOptions) => void;
hide: () => void;
}
export const RequestGroupSettingsModal = forwardRef<RequestGroupSettingsModalHandle, ModalProps>((_, ref) => {
export const RequestGroupSettingsModal = ({ requestGroup, onHide }: ModalProps & {
requestGroup: RequestGroup;
}) => {
const modalRef = useRef<ModalHandle>(null);
const editorRef = useRef<CodeEditorHandle>(null);
const workspacesForActiveProject = useSelector(selectWorkspacesForActiveProject);
const { organizationId, projectId, workspaceId } = useParams() as { organizationId: string; projectId: string; workspaceId: string };
const [state, setState] = useState<State>({
requestGroup: null,
showDescription: false,
defaultPreviewMode: false,
activeWorkspaceIdToCopyTo: null,
workspace: undefined,
workspacesForActiveProject: [],
defaultPreviewMode: !!requestGroup.description,
});
const patchRequestGroup = useRequestGroupPatcher();
const requestFetcher = useFetcher();
useImperativeHandle(ref, () => ({
hide: () => {
modalRef.current?.hide();
},
show: async ({ requestGroup, forceEditMode }) => {
const hasDescription = !!requestGroup.description;
// Find this request workspace for filtering out of workspaces list
const ancestors = await db.withAncestors(requestGroup);
const workspace = workspacesForActiveProject
.find(w => w._id === ancestors.find(doc => doc.type === models.workspace.type)?._id);
setState(state => ({
...state,
requestGroup,
workspace,
activeWorkspaceIdToCopyTo: null,
showDescription: forceEditMode || hasDescription,
defaultPreviewMode: hasDescription && !forceEditMode,
}));
modalRef.current?.show();
},
}), [workspacesForActiveProject]);
const duplicateRequestGroup = (r: Partial<RequestGroup>) => {
requestFetcher.submit(JSON.stringify(r),
{
action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/debug/request-group/${requestGroup._id}/duplicate`,
method: 'post',
encType: 'application/json',
});
};
useEffect(() => {
modalRef.current?.show();
}, []);
const handleMoveToWorkspace = async () => {
const { activeWorkspaceIdToCopyTo, requestGroup } = state;
if (!requestGroup || !activeWorkspaceIdToCopyTo) {
return;
}
const workspace = await models.workspace.getById(activeWorkspaceIdToCopyTo);
if (!workspace) {
return;
}
// TODO: if there are gRPC requests in a request group
// we should also copy the protofiles to the destination workspace - INS-267
await models.requestGroup.duplicate(requestGroup, {
metaSortKey: -1e9,
parentId: activeWorkspaceIdToCopyTo,
name: requestGroup.name, // Because duplicating will add (Copy) suffix
});
// TODO clean up this so it doesn't orphan descendants
await models.requestGroup.remove(requestGroup);
invariant(state.activeWorkspaceIdToCopyTo, 'Workspace ID is required');
patchRequestGroup(requestGroup._id, { parentId: state.activeWorkspaceIdToCopyTo });
modalRef.current?.hide();
};
const handleCopyToWorkspace = async () => {
const { activeWorkspaceIdToCopyTo, requestGroup } = state;
if (!requestGroup || !activeWorkspaceIdToCopyTo) {
return;
}
const workspace = await models.workspace.getById(activeWorkspaceIdToCopyTo);
if (!workspace) {
return;
}
const patch = {
invariant(state.activeWorkspaceIdToCopyTo, 'Workspace ID is required');
duplicateRequestGroup({
metaSortKey: -1e9, // Move to top of sort order
name: requestGroup.name, // Because duplicate will add (Copy) suffix if name is not provided in patch
parentId: activeWorkspaceIdToCopyTo,
};
await models.requestGroup.duplicate(requestGroup, patch);
models.stats.incrementCreatedRequests();
parentId: state.activeWorkspaceIdToCopyTo,
});
};
const {
requestGroup,
showDescription,
defaultPreviewMode,
activeWorkspaceIdToCopyTo,
workspace,
} = state;
return (
<Modal ref={modalRef}>
<ModalHeader>
Folder Settings{' '}
<span className="txt-sm selectable faint monospace">
{requestGroup?._id || ''}
</span>
</ModalHeader>
<ModalBody className="pad"><div>
<div className="form-control form-control--outlined">
<label>
Name
<input
type="text"
placeholder={requestGroup?.name || 'My Folder'}
defaultValue={requestGroup?.name}
onChange={async event => {
invariant(requestGroup, 'No request group');
const updatedRequestGroup = await models.requestGroup.update(requestGroup, { name: event.target.value });
setState(state => ({ ...state, requestGroup: updatedRequestGroup }));
}}
/>
</label>
</div>
{showDescription ? (
<OverlayContainer onClick={e => e.stopPropagation()}>
<Modal ref={modalRef} onHide={onHide}>
<ModalHeader>
Folder Settings{' '}
<span className="txt-sm selectable faint monospace">
{requestGroup?._id || ''}
</span>
</ModalHeader>
<ModalBody className="pad"><div>
<div className="form-control form-control--outlined">
<label>
Name
<input
type="text"
placeholder={requestGroup?.name || 'My Folder'}
defaultValue={requestGroup?.name}
onChange={async event => {
invariant(requestGroup, 'No request group');
patchRequestGroup(requestGroup._id, { name: event.target.value });
}}
/>
</label>
</div>
<MarkdownEditor
ref={editorRef}
className="margin-top"
@@ -145,66 +99,54 @@ export const RequestGroupSettingsModal = forwardRef<RequestGroupSettingsModalHan
defaultValue={requestGroup?.description || ''}
onChange={async (description: string) => {
invariant(requestGroup, 'No request group');
const updated = await models.requestGroup.update(requestGroup, { description });
setState(state => ({ ...state, requestGroup: updated, defaultPreviewMode: false }));
patchRequestGroup(requestGroup._id, { description });
}}
/>
) : (
<button
onClick={() => setState(state => ({ ...state, showDescription: true }))}
className="btn btn--outlined btn--super-duper-compact"
>
Add Description
</button>
)}
<hr />
<div className="form-row">
<div className="form-control form-control--outlined">
<label>
Move/Copy to Workspace
<HelpTooltip position="top" className="space-left">
Copy or move the current folder to a new workspace. It will be
placed at the root of the new workspace's folder structure.
</HelpTooltip>
<select
value={activeWorkspaceIdToCopyTo || '__NULL__'}
onChange={event => {
const workspaceId = event.currentTarget.value === '__NULL__' ? null : event.currentTarget.value;
setState(state => ({ ...state, activeWorkspaceIdToCopyTo: workspaceId }));
}}
<hr />
<div className="form-row">
<div className="form-control form-control--outlined">
<label>
Move/Copy to Workspace
<HelpTooltip position="top" className="space-left">
Copy or move the current folder to a new workspace. It will be
placed at the root of the new workspace's folder structure.
</HelpTooltip>
<select
value={activeWorkspaceIdToCopyTo || '__NULL__'}
onChange={event => setState(state => ({ ...state, activeWorkspaceIdToCopyTo: event.currentTarget.value === '__NULL__' ? null : event.currentTarget.value }))}
>
<option value="__NULL__">-- Select Workspace --</option>
{workspacesForActiveProject
.filter(w => workspaceId !== w._id)
.map(w => (
<option key={w._id} value={w._id}>
{w.name}
</option>
))}
</select>
</label>
</div>
<div className="form-control form-control--no-label width-auto">
<button
disabled={!activeWorkspaceIdToCopyTo}
className="btn btn--clicky"
onClick={handleCopyToWorkspace}
>
<option value="__NULL__">-- Select Workspace --</option>
{workspacesForActiveProject
.filter(w => workspace?._id !== w._id)
.map(w => (
<option key={w._id} value={w._id}>
{w.name}
</option>
))}
</select>
</label>
Copy
</button>
</div>
<div className="form-control form-control--no-label width-auto">
<button
disabled={!activeWorkspaceIdToCopyTo}
className="btn btn--clicky"
onClick={handleMoveToWorkspace}
>
Move
</button>
</div>
</div>
<div className="form-control form-control--no-label width-auto">
<button
disabled={!activeWorkspaceIdToCopyTo}
className="btn btn--clicky"
onClick={handleCopyToWorkspace}
>
Copy
</button>
</div>
<div className="form-control form-control--no-label width-auto">
<button
disabled={!activeWorkspaceIdToCopyTo}
className="btn btn--clicky"
onClick={handleMoveToWorkspace}
>
Move
</button>
</div>
</div>
</div></ModalBody>
</Modal>
</div></ModalBody>
</Modal>
</OverlayContainer>
);
});
RequestGroupSettingsModal.displayName = 'RequestGroupSettingsModal';
};

View File

@@ -4,15 +4,12 @@ import React, { forwardRef, useImperativeHandle, useRef, useState } from 'react'
import { docsTemplateTags } from '../../../common/documentation';
import { GrpcRequest } from '../../../models/grpc-request';
import { Request } from '../../../models/request';
import { isRequest } from '../../../models/request';
import { WebSocketRequest } from '../../../models/websocket-request';
import { RenderError } from '../../../templating';
import { Link } from '../base/link';
import { Modal, type ModalHandle, ModalProps } from '../base/modal';
import { ModalBody } from '../base/modal-body';
import { ModalHeader } from '../base/modal-header';
import { RequestSettingsModal } from '../modals/request-settings-modal';
import { showModal } from './index';
export interface RequestRenderErrorModalOptions {
error: RenderError | null;
request: Request | WebSocketRequest | GrpcRequest | null;
@@ -55,17 +52,6 @@ export const RequestRenderErrorModal = forwardRef<RequestRenderErrorModalHandle,
Failed to render <strong>{fullPath}</strong> prior to sending
</p>
<div className="pad-top-sm">
{error.path?.match(/^body/) && isRequest(request) && (
<button
className="btn btn--clicky margin-right-sm"
onClick={() => {
modalRef.current?.hide();
showModal(RequestSettingsModal, { request });
}}
>
Adjust Render Settings
</button>
)}
<Link button href={docsTemplateTags} className="btn btn--clicky">
Templating Documentation <i className="fa fa-external-link" />
</Link>

View File

@@ -1,13 +1,12 @@
import React, { forwardRef, useImperativeHandle, useRef, useState } from 'react';
import React, { useEffect, useRef, useState } from 'react';
import { OverlayContainer } from 'react-aria';
import { useSelector } from 'react-redux';
import { useFetcher, useParams } from 'react-router-dom';
import { database as db } from '../../../common/database';
import * as models from '../../../models';
import { GrpcRequest, isGrpcRequest } from '../../../models/grpc-request';
import { isRequest, Request } from '../../../models/request';
import { isWebSocketRequest, WebSocketRequest } from '../../../models/websocket-request';
import { isWorkspace, Workspace } from '../../../models/workspace';
import { invariant } from '../../../utils/invariant';
import { useRequestPatcher } from '../../hooks/use-request';
import { selectWorkspacesForActiveProject } from '../../redux/selectors';
@@ -20,413 +19,346 @@ import { MarkdownEditor } from '../markdown-editor';
export interface RequestSettingsModalOptions {
request: Request | GrpcRequest | WebSocketRequest;
forceEditMode?: boolean;
}
interface State {
request: Request | GrpcRequest | WebSocketRequest | null;
showDescription: boolean;
defaultPreviewMode: boolean;
activeWorkspaceIdToCopyTo: string | null;
workspace?: Workspace;
}
export interface RequestSettingsModalHandle {
show: (options: RequestSettingsModalOptions) => void;
hide: () => void;
}
export const RequestSettingsModal = forwardRef<RequestSettingsModalHandle, ModalProps>((_, ref) => {
export const RequestSettingsModal = ({ request, onHide }: ModalProps & RequestSettingsModalOptions) => {
const modalRef = useRef<ModalHandle>(null);
const editorRef = useRef<CodeEditorHandle>(null);
const workspacesForActiveProject = useSelector(selectWorkspacesForActiveProject);
const [state, setState] = useState<State>({
request: null,
showDescription: false,
defaultPreviewMode: false,
defaultPreviewMode: !!request?.description,
activeWorkspaceIdToCopyTo: null,
workspace: undefined,
});
useEffect(() => {
modalRef.current?.show();
}, []);
const requestFetcher = useFetcher();
const { organizationId, projectId, workspaceId } = useParams() as { organizationId: string; projectId: string; workspaceId: string };
const patchRequest = useRequestPatcher();
const workspacesForActiveProject = useSelector(selectWorkspacesForActiveProject);
useImperativeHandle(ref, () => ({
hide: () => {
modalRef.current?.hide();
},
show: async ({ request, forceEditMode }) => {
const hasDescription = !!request.description;
// Find this request workspace for filtering out of workspaces list
const ancestors = await db.withAncestors(request);
const workspace = workspacesForActiveProject.find(w => w._id === ancestors.find(isWorkspace)?._id);
setState(state => ({
...state,
request,
workspace: workspace,
activeWorkspaceIdToCopyTo: null,
showDescription: forceEditMode || hasDescription,
defaultPreviewMode: hasDescription && !forceEditMode,
}));
modalRef.current?.show();
},
}), [workspacesForActiveProject]);
const updateRequest = (req: Partial<Request>) => {
invariant(state.request, 'Request is required');
patchRequest(state.request._id, req);
};
const duplicateRequest = (r: Partial<Request>) => {
invariant(state.request, 'Request is required');
requestFetcher.submit(JSON.stringify(r),
{
action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/debug/request/${state.request._id}/duplicate`,
action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/debug/request/${request._id}/duplicate`,
method: 'post',
encType: 'application/json',
});
};
async function handleMoveToWorkspace() {
const { activeWorkspaceIdToCopyTo } = state;
invariant(activeWorkspaceIdToCopyTo, 'Workspace ID is required');
updateRequest({ parentId: activeWorkspaceIdToCopyTo });
invariant(state.activeWorkspaceIdToCopyTo, 'Workspace ID is required');
patchRequest(request._id, { parentId: state.activeWorkspaceIdToCopyTo });
modalRef.current?.hide();
}
async function handleCopyToWorkspace() {
const { activeWorkspaceIdToCopyTo } = state;
invariant(activeWorkspaceIdToCopyTo, 'Workspace ID is required');
duplicateRequest({ parentId: activeWorkspaceIdToCopyTo });
invariant(state.activeWorkspaceIdToCopyTo, 'Workspace ID is required');
duplicateRequest({ parentId: state.activeWorkspaceIdToCopyTo });
}
const { request, showDescription, defaultPreviewMode, activeWorkspaceIdToCopyTo, workspace } = state;
const { defaultPreviewMode, activeWorkspaceIdToCopyTo } = state;
const toggleCheckBox = async (event: any) => {
updateRequest({ [event.currentTarget.name]: event.currentTarget.checked ? true : false });
const updated = { ...state.request, [event.currentTarget.name]: event.currentTarget.checked } as Request;
setState(state => ({ ...state, request: updated }));
patchRequest(request._id, { [event.currentTarget.name]: event.currentTarget.checked ? true : false });
};
const updateDescription = (description: string) => {
updateRequest({ description });
const updated = { ...state.request, description } as Request;
patchRequest(request._id, { description });
setState({
...state,
request: updated,
defaultPreviewMode: false,
});
};
const updateName = (name: string) => {
updateRequest({ name });
const updated = { ...state.request, name } as Request;
setState(state => ({
...state,
request: updated,
}));
};
return (
<Modal ref={modalRef}>
<ModalHeader>
Request Settings{' '}
<span className="txt-sm selectable faint monospace">{request ? request._id : ''}</span>
</ModalHeader>
<ModalBody className="pad">
<div>
<div className="form-control form-control--outlined">
<label>
Name{' '}
<span className="txt-sm faint italic">(also rename by double-clicking in sidebar)</span>
<input
type="text"
placeholder={request?.url || 'My Request'}
defaultValue={request?.name}
onChange={event => updateName(event.target.value)}
/>
</label>
<OverlayContainer>
<Modal ref={modalRef} onHide={onHide}>
<ModalHeader>
Request Settings{' '}
<span className="txt-sm selectable faint monospace">{request ? request._id : ''}</span>
</ModalHeader>
<ModalBody className="pad">
<div>
<div className="form-control form-control--outlined">
<label>
Name{' '}
<span className="txt-sm faint italic">(also rename by double-clicking in sidebar)</span>
<input
type="text"
placeholder={request?.url || 'My Request'}
defaultValue={request?.name}
onChange={event => patchRequest(request._id, { name: event.target.value })}
/>
</label>
</div>
{request && isWebSocketRequest(request) && (
<>
<MarkdownEditor
ref={editorRef}
className="margin-top"
defaultPreviewMode={defaultPreviewMode}
placeholder="Write a description"
defaultValue={request.description}
onChange={updateDescription}
/>
<>
<div className="pad-top pad-bottom">
<div className="form-control form-control--thin">
<label>
Send cookies automatically
<input
type="checkbox"
name="settingSendCookies"
checked={request.settingSendCookies}
onChange={toggleCheckBox}
/>
</label>
</div>
<div className="form-control form-control--thin">
<label>
Store cookies automatically
<input
type="checkbox"
name="settingStoreCookies"
checked={request.settingStoreCookies}
onChange={toggleCheckBox}
/>
</label>
</div>
</div>
<div className="form-control form-control--outlined">
<label>
Follow redirects <span className="txt-sm faint italic">(overrides global setting)</span>
<select
defaultValue={request.settingFollowRedirects}
name="settingFollowRedirects"
onChange={toggleCheckBox}
>
<option value={'global'}>Use global setting</option>
<option value={'off'}>Don't follow redirects</option>
<option value={'on'}>Follow redirects</option>
</select>
</label>
</div>
</>
<hr />
<div className="form-row">
<div className="form-control form-control--outlined">
<label>
Move/Copy to Workspace
<HelpTooltip position="top" className="space-left">
Copy or move the current request to a new workspace. It will be placed at the root of
the new workspace's folder structure.
</HelpTooltip>
<select
value={activeWorkspaceIdToCopyTo || '__NULL__'}
onChange={event => {
const { value } = event.currentTarget;
const workspaceId = value === '__NULL__' ? null : value;
setState(state => ({ ...state, activeWorkspaceIdToCopyTo: workspaceId }));
}}
>
<option value="__NULL__">-- Select Workspace --</option>
{workspacesForActiveProject.map(w => {
if (workspaceId === w._id) {
return null;
}
return (
<option key={w._id} value={w._id}>
{w.name}
</option>
);
})}
</select>
</label>
</div>
<div className="form-control form-control--no-label width-auto">
<button
disabled={!activeWorkspaceIdToCopyTo}
className="btn btn--clicky"
onClick={handleCopyToWorkspace}
>
Copy
</button>
</div>
<div className="form-control form-control--no-label width-auto">
<button
disabled={!activeWorkspaceIdToCopyTo}
className="btn btn--clicky"
onClick={handleMoveToWorkspace}
>
Move
</button>
</div>
</div>
</>)}
{request && isGrpcRequest(request) && (
<p className="faint italic">
Are there any gRPC settings you expect to see? Create a{' '}
<a href={'https://github.com/Kong/insomnia/issues/new/choose'}>feature request</a>!
</p>
)}
{request && isRequest(request) && (
<>
<MarkdownEditor
ref={editorRef}
className="margin-top"
defaultPreviewMode={defaultPreviewMode}
placeholder="Write a description"
defaultValue={request.description}
onChange={updateDescription}
/>
<>
<div className="pad-top pad-bottom">
<div className="form-control form-control--thin">
<label>
Send cookies automatically
<input
type="checkbox"
name="settingSendCookies"
checked={request.settingSendCookies}
onChange={toggleCheckBox}
/>
</label>
</div>
<div className="form-control form-control--thin">
<label>
Store cookies automatically
<input
type="checkbox"
name="settingStoreCookies"
checked={request.settingStoreCookies}
onChange={toggleCheckBox}
/>
</label>
</div>
<div className="form-control form-control--thin">
<label>
Automatically encode special characters in URL
<input
type="checkbox"
name="settingEncodeUrl"
checked={request.settingEncodeUrl}
onChange={toggleCheckBox}
/>
<HelpTooltip position="top" className="space-left">
Automatically encode special characters at send time (does not apply to query
parameters editor)
</HelpTooltip>
</label>
</div>
<div className="form-control form-control--thin">
<label>
Skip rendering of request body
<input
type="checkbox"
name="settingDisableRenderRequestBody"
checked={request.settingDisableRenderRequestBody}
onChange={toggleCheckBox}
/>
<HelpTooltip position="top" className="space-left">
Disable rendering of environment variables and tags for the request body
</HelpTooltip>
</label>
</div>
<div className="form-control form-control--thin">
<label>
Rebuild path dot sequences
<HelpTooltip position="top" className="space-left">
This instructs libcurl to squash sequences of "/../" or "/./" that may exist in the
URL's path part and that is supposed to be removed according to RFC 3986 section
5.2.4
</HelpTooltip>
<input
type="checkbox"
name="settingRebuildPath"
checked={request['settingRebuildPath']}
onChange={toggleCheckBox}
/>
</label>
</div>
</div>
<div className="form-control form-control--outlined">
<label>
Follow redirects <span className="txt-sm faint italic">(overrides global setting)</span>
<select
defaultValue={request.settingFollowRedirects}
name="settingFollowRedirects"
onChange={async event => {
const updated = await models.request.update(request, {
[event.currentTarget.name]: event.currentTarget.value,
});
setState(state => ({ ...state, request: updated }));
}}
>
<option value={'global'}>Use global setting</option>
<option value={'off'}>Don't follow redirects</option>
<option value={'on'}>Follow redirects</option>
</select>
</label>
</div>
</>
<hr />
<div className="form-row">
<div className="form-control form-control--outlined">
<label>
Move/Copy to Workspace
<HelpTooltip position="top" className="space-left">
Copy or move the current request to a new workspace. It will be placed at the root of
the new workspace's folder structure.
</HelpTooltip>
<select
value={activeWorkspaceIdToCopyTo || '__NULL__'}
onChange={event => {
const { value } = event.currentTarget;
const workspaceId = value === '__NULL__' ? null : value;
setState(state => ({ ...state, activeWorkspaceIdToCopyTo: workspaceId }));
}}
>
<option value="__NULL__">-- Select Workspace --</option>
{workspacesForActiveProject.map(w => {
if (workspaceId === w._id) {
return null;
}
return (
<option key={w._id} value={w._id}>
{w.name}
</option>
);
})}
</select>
</label>
</div>
<div className="form-control form-control--no-label width-auto">
<button
disabled={!activeWorkspaceIdToCopyTo}
className="btn btn--clicky"
onClick={handleCopyToWorkspace}
>
Copy
</button>
</div>
<div className="form-control form-control--no-label width-auto">
<button
disabled={!activeWorkspaceIdToCopyTo}
className="btn btn--clicky"
onClick={handleMoveToWorkspace}
>
Move
</button>
</div>
</div>
</>)
}
</div>
{request && isWebSocketRequest(request) && (
<>
<>
{showDescription ? (
<MarkdownEditor
ref={editorRef}
className="margin-top"
defaultPreviewMode={defaultPreviewMode}
placeholder="Write a description"
defaultValue={request.description}
onChange={updateDescription}
/>
) : (
<button
onClick={() => setState(state => ({ ...state, showDescription: true }))}
className="btn btn--outlined btn--super-duper-compact"
>
Add Description
</button>
)}
</>
<>
<div className="pad-top pad-bottom">
<div className="form-control form-control--thin">
<label>
Send cookies automatically
<input
type="checkbox"
name="settingSendCookies"
checked={request.settingSendCookies}
onChange={toggleCheckBox}
/>
</label>
</div>
<div className="form-control form-control--thin">
<label>
Store cookies automatically
<input
type="checkbox"
name="settingStoreCookies"
checked={request.settingStoreCookies}
onChange={toggleCheckBox}
/>
</label>
</div>
</div>
<div className="form-control form-control--outlined">
<label>
Follow redirects <span className="txt-sm faint italic">(overrides global setting)</span>
<select
defaultValue={request.settingFollowRedirects}
name="settingFollowRedirects"
onChange={toggleCheckBox}
>
<option value={'global'}>Use global setting</option>
<option value={'off'}>Don't follow redirects</option>
<option value={'on'}>Follow redirects</option>
</select>
</label>
</div>
</>
<hr />
<div className="form-row">
<div className="form-control form-control--outlined">
<label>
Move/Copy to Workspace
<HelpTooltip position="top" className="space-left">
Copy or move the current request to a new workspace. It will be placed at the root of
the new workspace's folder structure.
</HelpTooltip>
<select
value={activeWorkspaceIdToCopyTo || '__NULL__'}
onChange={event => {
const { value } = event.currentTarget;
const workspaceId = value === '__NULL__' ? null : value;
setState(state => ({ ...state, activeWorkspaceIdToCopyTo: workspaceId }));
}}
>
<option value="__NULL__">-- Select Workspace --</option>
{workspacesForActiveProject.map(w => {
if (workspace && workspace._id === w._id) {
return null;
}
return (
<option key={w._id} value={w._id}>
{w.name}
</option>
);
})}
</select>
</label>
</div>
<div className="form-control form-control--no-label width-auto">
<button
disabled={!activeWorkspaceIdToCopyTo}
className="btn btn--clicky"
onClick={handleCopyToWorkspace}
>
Copy
</button>
</div>
<div className="form-control form-control--no-label width-auto">
<button
disabled={!activeWorkspaceIdToCopyTo}
className="btn btn--clicky"
onClick={handleMoveToWorkspace}
>
Move
</button>
</div>
</div>
</>)}
{request && isGrpcRequest(request) && (
<p className="faint italic">
Are there any gRPC settings you expect to see? Create a{' '}
<a href={'https://github.com/Kong/insomnia/issues/new/choose'}>feature request</a>!
</p>
)}
{request && isRequest(request) && (
<>
<>
{showDescription ? (
<MarkdownEditor
ref={editorRef}
className="margin-top"
defaultPreviewMode={defaultPreviewMode}
placeholder="Write a description"
defaultValue={request.description}
onChange={updateDescription}
/>
) : (
<button
onClick={() => setState(state => ({ ...state, showDescription: true }))}
className="btn btn--outlined btn--super-duper-compact"
>
Add Description
</button>
)}
</>
<>
<div className="pad-top pad-bottom">
<div className="form-control form-control--thin">
<label>
Send cookies automatically
<input
type="checkbox"
name="settingSendCookies"
checked={request.settingSendCookies}
onChange={toggleCheckBox}
/>
</label>
</div>
<div className="form-control form-control--thin">
<label>
Store cookies automatically
<input
type="checkbox"
name="settingStoreCookies"
checked={request.settingStoreCookies}
onChange={toggleCheckBox}
/>
</label>
</div>
<div className="form-control form-control--thin">
<label>
Automatically encode special characters in URL
<input
type="checkbox"
name="settingEncodeUrl"
checked={request.settingEncodeUrl}
onChange={toggleCheckBox}
/>
<HelpTooltip position="top" className="space-left">
Automatically encode special characters at send time (does not apply to query
parameters editor)
</HelpTooltip>
</label>
</div>
<div className="form-control form-control--thin">
<label>
Skip rendering of request body
<input
type="checkbox"
name="settingDisableRenderRequestBody"
checked={request.settingDisableRenderRequestBody}
onChange={toggleCheckBox}
/>
<HelpTooltip position="top" className="space-left">
Disable rendering of environment variables and tags for the request body
</HelpTooltip>
</label>
</div>
<div className="form-control form-control--thin">
<label>
Rebuild path dot sequences
<HelpTooltip position="top" className="space-left">
This instructs libcurl to squash sequences of "/../" or "/./" that may exist in the
URL's path part and that is supposed to be removed according to RFC 3986 section
5.2.4
</HelpTooltip>
<input
type="checkbox"
name="settingRebuildPath"
checked={request['settingRebuildPath']}
onChange={toggleCheckBox}
/>
</label>
</div>
</div>
<div className="form-control form-control--outlined">
<label>
Follow redirects <span className="txt-sm faint italic">(overrides global setting)</span>
<select
defaultValue={request.settingFollowRedirects}
name="settingFollowRedirects"
onChange={async event => {
const updated = await models.request.update(request, {
[event.currentTarget.name]: event.currentTarget.value,
});
setState(state => ({ ...state, request: updated }));
}}
>
<option value={'global'}>Use global setting</option>
<option value={'off'}>Don't follow redirects</option>
<option value={'on'}>Follow redirects</option>
</select>
</label>
</div>
</>
<hr />
<div className="form-row">
<div className="form-control form-control--outlined">
<label>
Move/Copy to Workspace
<HelpTooltip position="top" className="space-left">
Copy or move the current request to a new workspace. It will be placed at the root of
the new workspace's folder structure.
</HelpTooltip>
<select
value={activeWorkspaceIdToCopyTo || '__NULL__'}
onChange={event => {
const { value } = event.currentTarget;
const workspaceId = value === '__NULL__' ? null : value;
setState(state => ({ ...state, activeWorkspaceIdToCopyTo: workspaceId }));
}}
>
<option value="__NULL__">-- Select Workspace --</option>
{workspacesForActiveProject.map(w => {
if (workspace && workspace._id === w._id) {
return null;
}
return (
<option key={w._id} value={w._id}>
{w.name}
</option>
);
})}
</select>
</label>
</div>
<div className="form-control form-control--no-label width-auto">
<button
disabled={!activeWorkspaceIdToCopyTo}
className="btn btn--clicky"
onClick={handleCopyToWorkspace}
>
Copy
</button>
</div>
<div className="form-control form-control--no-label width-auto">
<button
disabled={!activeWorkspaceIdToCopyTo}
className="btn btn--clicky"
onClick={handleMoveToWorkspace}
>
Move
</button>
</div>
</div>
</>)
}
</div>
</ModalBody>
</Modal>
</ModalBody>
</Modal>
</OverlayContainer>
);
});
RequestSettingsModal.displayName = 'RequestSettingsModal';
};

View File

@@ -1,458 +0,0 @@
import classnames from 'classnames';
import React, { forwardRef, Fragment, useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react';
import { useSelector } from 'react-redux';
import { useNavigate, useParams, useRouteLoaderData } from 'react-router-dom';
import { METHOD_GRPC } from '../../../common/constants';
import { fuzzyMatchAll } from '../../../common/misc';
import * as models from '../../../models';
import { GrpcRequest, isGrpcRequest } from '../../../models/grpc-request';
import { isRequest, Request } from '../../../models/request';
import { isRequestGroup, RequestGroup } from '../../../models/request-group';
import { isWebSocketRequest, WebSocketRequest } from '../../../models/websocket-request';
import { Workspace } from '../../../models/workspace';
import { buildQueryStringFromParams, joinUrlAndQueryString } from '../../../utils/url/querystring';
import { useRequestMetaPatcher } from '../../hooks/use-request';
import { selectGrpcRequestMetas, selectRequestMetas, selectWorkspaceRequestsAndRequestGroups, selectWorkspacesForActiveProject } from '../../redux/selectors';
import { RequestLoaderData } from '../../routes/request';
import { WorkspaceLoaderData } from '../../routes/workspace';
import { Highlight } from '../base/highlight';
import { Modal, ModalHandle, ModalProps } from '../base/modal';
import { ModalBody } from '../base/modal-body';
import { ModalHeader } from '../base/modal-header';
import { createKeybindingsHandler, useDocBodyKeyboardShortcuts } from '../keydown-binder';
import { GrpcTag } from '../tags/grpc-tag';
import { MethodTag } from '../tags/method-tag';
import { WebSocketTag } from '../tags/websocket-tag';
import { wrapToIndex } from './utils';
interface State {
searchString: string;
workspacesForActiveProject: Workspace[];
matchedRequests: (Request | WebSocketRequest | GrpcRequest)[];
matchedWorkspaces: Workspace[];
activeIndex: number;
maxRequests: number;
maxWorkspaces: number;
disableInput: boolean;
selectOnKeyup: boolean;
hideNeverActiveRequests: boolean;
isModalVisible: boolean;
title: string | null;
}
interface RequestSwitcherModalOptions {
maxRequests?: number;
maxWorkspaces?: number;
disableInput?: boolean;
selectOnKeyup?: boolean;
hideNeverActiveRequests?: boolean;
title?: string;
openDelay?: number;
}
export interface RequestSwitcherModalHandle {
show: (options?: RequestSwitcherModalOptions) => void;
hide: () => void;
}
export const RequestSwitcherModal = forwardRef<RequestSwitcherModalHandle, ModalProps>((_, ref) => {
const modalRef = useRef<ModalHandle>(null);
const [state, setState] = useState<State>({
searchString: '',
workspacesForActiveProject: [],
matchedRequests: [],
matchedWorkspaces: [],
activeIndex: -1,
maxRequests: 20,
maxWorkspaces: 20,
disableInput: false,
selectOnKeyup: false,
hideNeverActiveRequests: false,
isModalVisible: true,
title: null,
});
const { organizationId, projectId } = useParams<{ organizationId: string; projectId: string }>();
const navigate = useNavigate();
const requestData = useRouteLoaderData('request/:requestId') as RequestLoaderData<Request, any> | undefined;
const { activeRequest } = requestData || {};
const {
activeWorkspace: workspace,
activeWorkspaceMeta,
} = useRouteLoaderData(':workspaceId') as WorkspaceLoaderData;
const workspacesForActiveProject = useSelector(selectWorkspacesForActiveProject);
const requestMetas = useSelector(selectRequestMetas);
const grpcRequestMetas = useSelector(selectGrpcRequestMetas);
const workspaceRequestsAndRequestGroups = useSelector(selectWorkspaceRequestsAndRequestGroups);
const patchRequestMeta = useRequestMetaPatcher();
/** Return array of path segments for given folders */
const pathSegments = useCallback((requestOrRequestGroup: Request | WebSocketRequest | GrpcRequest | RequestGroup): string[] => {
const folders = workspaceRequestsAndRequestGroups.filter(isRequestGroup)
.filter(g => g._id === requestOrRequestGroup.parentId);
const folderName = isRequestGroup(requestOrRequestGroup) ? `${requestOrRequestGroup.name}` : '';
// It's the final parent
if (folders.length === 0) {
return [folderName];
}
// Still has more parents
if (folderName) {
return [...pathSegments(folders[0]), folderName];
}
// It's the child
return pathSegments(folders[0]);
}, [workspaceRequestsAndRequestGroups]);
const getLastActiveRequestMap = useCallback(() => {
// requestIds: lastActive datetime
const lastActiveMap: Record<string, number> = {};
for (const meta of requestMetas) {
lastActiveMap[meta.parentId] = meta.lastActive;
}
for (const meta of grpcRequestMetas) {
lastActiveMap[meta.parentId] = meta.lastActive;
}
return lastActiveMap;
}, [grpcRequestMetas, requestMetas]);
const isMatch = useCallback((request: Request | WebSocketRequest | GrpcRequest, searchStrings: string): number | null => {
if (request._id === searchStrings) {
return Infinity;
}
// name
const searchIndexes = [request.name];
// url
isGrpcRequest(request) ? searchIndexes.push(request.url + request.protoMethodName)
: searchIndexes.push(joinUrlAndQueryString(request.url, buildQueryStringFromParams(request.parameters)));
// http method
const method = isRequest(request) && request.method;
method && searchIndexes.push(method);
isGrpcRequest(request) && searchIndexes.push(METHOD_GRPC);
// path segments
searchIndexes.push(pathSegments(request).join('/'));
const match = fuzzyMatchAll(
searchStrings,
searchIndexes,
{ splitSpace: true },
);
if (!match) {
return null;
}
return match.score;
}, [pathSegments]);
const handleChangeValue = useCallback((searchString: string) => {
const { maxRequests, maxWorkspaces, hideNeverActiveRequests } = state;
const lastActiveMap = getLastActiveRequestMap();
// OPTIMIZATION: This only filters if we have a filter
let matchedRequests = (workspaceRequestsAndRequestGroups
.filter(child => isRequest(child) || isWebSocketRequest(child) || isGrpcRequest(child)) as (Request | WebSocketRequest | GrpcRequest)[])
.sort((a, b) => {
const aLA = lastActiveMap[a._id] || 0;
const bLA = lastActiveMap[b._id] || 0;
// If lastActive same, go by name
if (aLA === bLA) {
return a.name > b.name ? 1 : -1;
}
return bLA - aLA;
});
if (hideNeverActiveRequests) {
matchedRequests = matchedRequests.filter(r => lastActiveMap[r._id]);
}
if (searchString) {
matchedRequests = matchedRequests
.map(r => ({
request: r,
score: isMatch(r, searchString),
}))
.filter(v => v.score !== null)
.sort((a, b) => Number(a.score || -Infinity) - Number(b.score || -Infinity))
.map(v => v.request);
}
const matchedWorkspaces = workspacesForActiveProject
.filter(w => w._id !== workspace._id)
.filter(w => {
const name = w.name.toLowerCase();
const toMatch = searchString.toLowerCase();
return name.indexOf(toMatch) !== -1;
});
// Make sure we select the first item but we don't want to select the currently active
// one because that wouldn't make any sense.
const activeRequestId = activeRequest ? activeRequest._id : 'n/a';
const indexOfFirstNonActiveRequest = matchedRequests.findIndex(r => r._id !== activeRequestId);
setState(state => ({
...state,
searchString,
activeIndex: indexOfFirstNonActiveRequest >= 0 ? indexOfFirstNonActiveRequest : 0,
matchedRequests: matchedRequests.slice(0, maxRequests),
matchedWorkspaces: matchedWorkspaces.slice(0, maxWorkspaces),
}));
}, [state, getLastActiveRequestMap, workspaceRequestsAndRequestGroups, workspacesForActiveProject, activeRequest, isMatch, workspace._id]);
useImperativeHandle(ref, () => ({
hide: () => {
modalRef.current?.hide();
},
show: options => {
if (modalRef.current?.isOpen()) {
return;
}
setState(state => ({
...state,
maxRequests: options?.maxRequests ?? 20,
maxWorkspaces: options?.maxWorkspaces ?? 20,
disableInput: !!options?.disableInput,
hideNeverActiveRequests: !!options?.hideNeverActiveRequests,
selectOnKeyup: !!options?.selectOnKeyup,
title: options?.title || null,
isModalVisible: true,
}));
handleChangeValue('');
modalRef.current?.show();
},
}), [handleChangeValue]);
const activateWorkspaceAndHide = useCallback((workspace: Workspace) => {
if (!workspace) {
return;
}
console.log(`[app] Activating workspace "${workspace.name}"`);
navigate(`/organization/${organizationId}/project/${projectId}/workspace/${workspace._id}`);
modalRef.current?.hide();
}, [navigate, organizationId, projectId]);
const activateRequestAndHide = useCallback((request?: Request | WebSocketRequest | GrpcRequest) => {
if (!request) {
return;
}
models.workspaceMeta.update(activeWorkspaceMeta, { activeRequestId: request._id });
patchRequestMeta(request._id, { lastActive: Date.now() });
modalRef.current?.hide();
}, [activeWorkspaceMeta, patchRequestMeta]);
const activateCurrentIndex = useCallback(async () => {
const { activeIndex, matchedRequests, matchedWorkspaces, searchString } = state;
if (activeIndex < matchedRequests.length) {
// Activate the request if there is one
const request = matchedRequests[activeIndex];
activateRequestAndHide(request);
return;
}
if (activeIndex < matchedRequests.length + matchedWorkspaces.length) {
// Activate the workspace if there is one
const index = activeIndex - matchedRequests.length;
const workspace = matchedWorkspaces[index];
if (workspace) {
activateWorkspaceAndHide(workspace);
}
return;
}
if (searchString) {
// Create request if no match
if (!workspace) {
return;
}
// Create the request if nothing matched
const request = await models.request.create({
parentId: activeRequest ? activeRequest.parentId : workspace._id,
name: state.searchString,
});
activateRequestAndHide(request);
}
}, [activateRequestAndHide, activateWorkspaceAndHide, activeRequest, state, workspace]);
const handleInputKeydown = createKeybindingsHandler({
'ArrowUp': e => {
e.preventDefault();
setState(state => ({
...state,
activeIndex: wrapToIndex(state.activeIndex - 1, state.matchedRequests.length + state.matchedWorkspaces.length),
}));
},
'Shift+Tab': e => {
e.preventDefault();
setState(state => ({
...state,
activeIndex: wrapToIndex(state.activeIndex - 1, state.matchedRequests.length + state.matchedWorkspaces.length),
}));
},
'ArrowDown': e => {
e.preventDefault();
setState(state => ({
...state,
activeIndex: wrapToIndex(state.activeIndex + 1, state.matchedRequests.length + state.matchedWorkspaces.length),
}));
},
'Tab': e => {
e.preventDefault();
setState(state => ({
...state,
activeIndex: wrapToIndex(state.activeIndex + 1, state.matchedRequests.length + state.matchedWorkspaces.length),
}));
},
'Enter': e => {
e.preventDefault();
activateCurrentIndex();
},
});
useDocBodyKeyboardShortcuts({
request_showRecent: () => {
if (state.isModalVisible) {
setState(state => ({
...state,
activeIndex: wrapToIndex(state.activeIndex + 1, state.matchedRequests.length + state.matchedWorkspaces.length),
}));
}
},
request_showRecentPrevious: () => {
if (state.isModalVisible) {
setState(state => ({
...state,
activeIndex: wrapToIndex(state.activeIndex - 1, state.matchedRequests.length + state.matchedWorkspaces.length),
}));
}
},
});
useEffect(() => {
const handleKeyup = async (event: KeyboardEvent) => {
// Handle selection if unpresses all modifier keys. Ideally this would trigger once
// the user unpresses the hotkey that triggered this modal but we currently do not
// have the facilities to do that.
const isMetaKeyDown = event.ctrlKey || event.shiftKey || event.metaKey || event.altKey;
if (state.selectOnKeyup && modalRef.current?.isOpen() && !isMetaKeyDown) {
await activateCurrentIndex();
modalRef.current?.hide();
}
};
document.body.addEventListener('keyup', handleKeyup);
return () => {
document.body.removeEventListener('keyup', handleKeyup);
};
}, [activateCurrentIndex, state.selectOnKeyup]);
const {
searchString,
activeIndex,
matchedRequests,
matchedWorkspaces,
disableInput,
title,
isModalVisible,
} = state;
const requestGroups = workspaceRequestsAndRequestGroups.filter(isRequestGroup);
return (
<Modal
ref={modalRef}
className={isModalVisible ? '' : 'hide'}
>
<ModalHeader hideCloseButton>
{title || (
<Fragment>
<div className="pull-right txt-sm pad-right tall">
<span className="vertically-center">
<div>
<span className="monospace">tab</span> or&nbsp;
<span className="monospace"></span> to navigate&nbsp;&nbsp;&nbsp;&nbsp;
<span className="monospace"></span> &nbsp;to select&nbsp;&nbsp;&nbsp;&nbsp;
<span className="monospace">esc</span> to dismiss
</div>
</span>
</div>
<div>Quick Switch</div>
</Fragment>
)}
</ModalHeader>
<ModalBody className="request-switcher">
{!disableInput && (
<div className="pad" onKeyDown={handleInputKeydown}>
<div className="form-control form-control--outlined no-margin">
<input
type="text"
autoFocus
placeholder="Filter by name or folder"
value={searchString}
onChange={event => handleChangeValue(event.currentTarget.value)}
/>
</div>
</div>
)}
<ul>
{matchedRequests.map((r: Request | WebSocketRequest | GrpcRequest, i) => {
const requestGroup = requestGroups.find(rg => rg._id === r.parentId);
const buttonClasses = classnames(
'btn btn--expandable-small wide text-left pad-bottom',
{
focus: activeIndex === i,
},
);
return (
<li key={r._id}>
<button onClick={() => activateRequestAndHide(r)} className={buttonClasses}>
<div>
{requestGroup ? (
<div className="pull-right faint italic">
<Highlight search={searchString} text={pathSegments(r).join(' / ')} />
&nbsp;&nbsp;
<i className="fa fa-folder-o" />
</div>
) : null}
<Highlight search={searchString} text={r.name} />
</div>
<div className="margin-left-xs faint">
{isRequest(r) ? <MethodTag method={r.method} /> : null}
{isGrpcRequest(r) ? <GrpcTag /> : null}
{isWebSocketRequest(r) ? <WebSocketTag /> : null}
{<Highlight search={searchString} text={isGrpcRequest(r) ? r.url + r.protoMethodName : r.url} />}
</div>
</button>
</li>
);
})}
{matchedRequests.length > 0 && matchedWorkspaces.length > 0 && (
<li className="pad-left pad-right">
<hr />
</li>
)}
{matchedWorkspaces.map((w, i) => {
const buttonClasses = classnames('btn btn--super-compact wide text-left', {
focus: activeIndex - matchedRequests.length === i,
});
return (
<li key={w._id}>
<button onClick={() => activateWorkspaceAndHide(w)} className={buttonClasses}>
<i className="fa fa-random" />
&nbsp;&nbsp;&nbsp; Switch to <strong>{w.name}</strong>
</button>
</li>
);
})}
</ul>
{searchString && matchedRequests.length === 0 && matchedWorkspaces.length === 0 && (
<div className="text-center pad-bottom">
<p>
No matches found for <strong>{searchString}</strong>
</p>
{workspace ? <button
className="btn btn--outlined btn--compact"
disabled={!searchString}
onClick={activateCurrentIndex}
>
Create a request named {searchString}
</button> : null}
</div>
)}
</ModalBody>
</Modal>
);
});
RequestSwitcherModal.displayName = 'RequestSwitcherModal';

View File

@@ -1,5 +1,6 @@
import classnames from 'classnames';
import React, { forwardRef, useCallback, useImperativeHandle, useRef, useState } from 'react';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { OverlayContainer } from 'react-aria';
import { useSelector } from 'react-redux';
import { database as db, Operation } from '../../../common/database';
@@ -30,7 +31,7 @@ export interface SyncBranchesModalHandle {
show: (options: SyncBranchesModalOptions) => void;
hide: () => void;
}
export const SyncBranchesModal = forwardRef<SyncBranchesModalHandle, Props>(({ vcs }, ref) => {
export const SyncBranchesModal = ({ vcs, onHide }: Props) => {
const modalRef = useRef<ModalHandle>(null);
const [state, setState] = useState<State>({
error: '',
@@ -69,17 +70,11 @@ export const SyncBranchesModal = forwardRef<SyncBranchesModalHandle, Props>(({ v
}));
}
}, [vcs]);
useImperativeHandle(ref, () => ({
hide: () => modalRef.current?.hide(),
show: ({ onHide }) => {
setState(state => ({
...state,
onHide,
}));
refreshState();
modalRef.current?.show({ onHide });
},
}), [refreshState]);
refreshState();
useEffect(() => {
modalRef.current?.show();
}, []);
const syncItems = useSelector(selectSyncItems);
async function handleCheckout(branch: string) {
try {
@@ -160,138 +155,140 @@ export const SyncBranchesModal = forwardRef<SyncBranchesModalHandle, Props>(({ v
const { branches, remoteBranches, currentBranch, newBranchName, error } = state;
return (
<Modal ref={modalRef}>
<ModalHeader>Branches</ModalHeader>
<ModalBody className="wide pad">
{error && (
<p className="notice error margin-bottom-sm no-margin-top">
<button className="pull-right icon" onClick={() => setState(state => ({ ...state, error: '' }))}>
<i className="fa fa-times" />
</button>
{error}
</p>
)}
<form onSubmit={handleCreate}>
<div className="form-row">
<div className="form-control form-control--outlined">
<label>
New Branch Name
<input
type="text"
onChange={event => setState(state => ({ ...state, newBranchName: event.target.value }))}
placeholder="testing-branch"
value={newBranchName}
/>
</label>
</div>
<div className="form-control form-control--no-label width-auto">
<button type="submit" className="btn btn--clicky" disabled={!newBranchName}>
Create
<OverlayContainer>
<Modal ref={modalRef} onHide={onHide}>
<ModalHeader>Branches</ModalHeader>
<ModalBody className="wide pad">
{error && (
<p className="notice error margin-bottom-sm no-margin-top">
<button className="pull-right icon" onClick={() => setState(state => ({ ...state, error: '' }))}>
<i className="fa fa-times" />
</button>
{error}
</p>
)}
<form onSubmit={handleCreate}>
<div className="form-row">
<div className="form-control form-control--outlined">
<label>
New Branch Name
<input
type="text"
onChange={event => setState(state => ({ ...state, newBranchName: event.target.value }))}
placeholder="testing-branch"
value={newBranchName}
/>
</label>
</div>
<div className="form-control form-control--no-label width-auto">
<button type="submit" className="btn btn--clicky" disabled={!newBranchName}>
Create
</button>
</div>
</div>
</div>
</form>
</form>
<div className="pad-top">
<table className="table--fancy table--outlined">
<thead>
<tr>
<th className="text-left">Branches</th>
<th className="text-right">&nbsp;</th>
</tr>
</thead>
<tbody>
{branches.map(name => (
<tr key={name} className="table--no-outline-row">
<td>
<span
className={classnames({
bold: name === currentBranch,
})}
>
{name}
</span>
{name === currentBranch ? (
<span className="txt-sm space-left">(current)</span>
) : null}
{name === 'master' && <i className="fa fa-lock space-left faint" />}
</td>
<td className="text-right">
<PromptButton
className="btn btn--micro btn--outlined space-left"
doneMessage="Merged"
disabled={name === currentBranch}
onClick={() => handleMerge(name)}
>
Merge
</PromptButton>
<PromptButton
className="btn btn--micro btn--outlined space-left"
doneMessage="Deleted"
disabled={name === currentBranch || name === 'master'}
onClick={() => handleDelete(name)}
>
Delete
</PromptButton>
<button
className="btn btn--micro btn--outlined space-left"
disabled={name === currentBranch}
onClick={() => handleCheckout(name)}
>
Checkout
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
{remoteBranches.length > 0 && (
<div className="pad-top">
<table className="table--fancy table--outlined">
<thead>
<tr>
<th className="text-left">Remote Branches</th>
<th className="text-left">Branches</th>
<th className="text-right">&nbsp;</th>
</tr>
</thead>
<tbody>
{remoteBranches.map(name => (
{branches.map(name => (
<tr key={name} className="table--no-outline-row">
<td>
{name}
<span
className={classnames({
bold: name === currentBranch,
})}
>
{name}
</span>
{name === currentBranch ? (
<span className="txt-sm space-left">(current)</span>
) : null}
{name === 'master' && <i className="fa fa-lock space-left faint" />}
</td>
<td className="text-right">
{name !== 'master' && (
<PromptButton
className="btn btn--micro btn--outlined space-left"
doneMessage="Deleted"
disabled={name === currentBranch}
onClick={() => handleRemoteDelete(name)}
>
Delete
</PromptButton>
)}
<SyncPullButton
<PromptButton
className="btn btn--micro btn--outlined space-left"
branch={name}
onPull={refreshState}
doneMessage="Merged"
disabled={name === currentBranch}
vcs={vcs}
onClick={() => handleMerge(name)}
>
Fetch
</SyncPullButton>
Merge
</PromptButton>
<PromptButton
className="btn btn--micro btn--outlined space-left"
doneMessage="Deleted"
disabled={name === currentBranch || name === 'master'}
onClick={() => handleDelete(name)}
>
Delete
</PromptButton>
<button
className="btn btn--micro btn--outlined space-left"
disabled={name === currentBranch}
onClick={() => handleCheckout(name)}
>
Checkout
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</ModalBody>
</Modal >
{remoteBranches.length > 0 && (
<div className="pad-top">
<table className="table--fancy table--outlined">
<thead>
<tr>
<th className="text-left">Remote Branches</th>
<th className="text-right">&nbsp;</th>
</tr>
</thead>
<tbody>
{remoteBranches.map(name => (
<tr key={name} className="table--no-outline-row">
<td>
{name}
{name === 'master' && <i className="fa fa-lock space-left faint" />}
</td>
<td className="text-right">
{name !== 'master' && (
<PromptButton
className="btn btn--micro btn--outlined space-left"
doneMessage="Deleted"
disabled={name === currentBranch}
onClick={() => handleRemoteDelete(name)}
>
Delete
</PromptButton>
)}
<SyncPullButton
className="btn btn--micro btn--outlined space-left"
branch={name}
onPull={refreshState}
disabled={name === currentBranch}
vcs={vcs}
>
Fetch
</SyncPullButton>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</ModalBody>
</Modal>
</OverlayContainer>
);
});
};
SyncBranchesModal.displayName = 'SyncBranchesModal';

View File

@@ -1,4 +1,5 @@
import React, { forwardRef, useImperativeHandle, useRef, useState } from 'react';
import React, { useEffect, useRef, useState } from 'react';
import { OverlayContainer } from 'react-aria';
import { useRouteLoaderData } from 'react-router-dom';
import { strings } from '../../../common/strings';
@@ -16,16 +17,9 @@ type Props = ModalProps & {
interface State {
error?: string;
workspaceName: string;
onHide?: () => void;
}
export interface SyncDeleteModalOptions {
onHide?: () => void;
}
export interface SyncDeleteModalHandle {
show: (options: SyncDeleteModalOptions) => void;
hide: () => void;
}
export const SyncDeleteModal = forwardRef<SyncDeleteModalHandle, Props>(({ vcs }, ref) => {
export const SyncDeleteModal = ({ vcs, onHide }: Props) => {
const modalRef = useRef<ModalHandle>(null);
const [state, setState] = useState<State>({
error: '',
@@ -35,17 +29,9 @@ export const SyncDeleteModal = forwardRef<SyncDeleteModalHandle, Props>(({ vcs }
activeWorkspace,
} = useRouteLoaderData(':workspaceId') as WorkspaceLoaderData;
useImperativeHandle(ref, () => ({
hide: () => modalRef.current?.hide(),
show: ({ onHide }) => {
setState({
error: '',
workspaceName: '',
onHide,
});
modalRef.current?.show({ onHide });
},
}), []);
useEffect(() => {
modalRef.current?.show();
}, []);
const onSubmit = async (event: React.SyntheticEvent<HTMLFormElement>) => {
event.preventDefault();
try {
@@ -56,7 +42,7 @@ export const SyncDeleteModal = forwardRef<SyncDeleteModalHandle, Props>(({ vcs }
resourceType: strings.collection.singular.toLowerCase(),
});
modalRef.current?.hide();
state.onHide?.();
onHide?.();
} catch (err) {
setState(state => ({
...state,
@@ -67,29 +53,30 @@ export const SyncDeleteModal = forwardRef<SyncDeleteModalHandle, Props>(({ vcs }
const { error, workspaceName } = state;
return (
<Modal ref={modalRef} skinny>
<ModalHeader>Delete {strings.collection.singular}</ModalHeader>
<ModalBody className="wide pad-left pad-right text-center" noScroll>
{error && <p className="notice error margin-bottom-sm no-margin-top">{error}</p>}
<p className="selectable">
This will permanently delete the {<strong style={{ whiteSpace: 'pre-wrap' }}>{activeWorkspace?.name}</strong>}{' '}
{strings.collection.singular.toLowerCase()} remotely.
</p>
<p className="selectable">Please type {<strong style={{ whiteSpace: 'pre-wrap' }}>{activeWorkspace?.name}</strong>} to confirm.</p>
<form onSubmit={onSubmit}>
<div className="form-control form-control--outlined">
<input
type="text"
onChange={event => setState(state => ({ ...state, workspaceName: event.target.value }))}
value={workspaceName}
/>
<Button bg="danger" disabled={workspaceName !== activeWorkspace?.name}>
Delete {strings.collection.singular}
</Button>
</div>
</form>
</ModalBody>
</Modal>
<OverlayContainer>
<Modal ref={modalRef} skinny onHide={onHide}>
<ModalHeader>Delete {strings.collection.singular}</ModalHeader>
<ModalBody className="wide pad-left pad-right text-center" noScroll>
{error && <p className="notice error margin-bottom-sm no-margin-top">{error}</p>}
<p className="selectable">
This will permanently delete the {<strong style={{ whiteSpace: 'pre-wrap' }}>{activeWorkspace?.name}</strong>}{' '}
{strings.collection.singular.toLowerCase()} remotely.
</p>
<p className="selectable">Please type {<strong style={{ whiteSpace: 'pre-wrap' }}>{activeWorkspace?.name}</strong>} to confirm.</p>
<form onSubmit={onSubmit}>
<div className="form-control form-control--outlined">
<input
type="text"
onChange={event => setState(state => ({ ...state, workspaceName: event.target.value }))}
value={workspaceName}
/>
<Button bg="danger" disabled={workspaceName !== activeWorkspace?.name}>
Delete {strings.collection.singular}
</Button>
</div>
</form>
</ModalBody>
</Modal>
</OverlayContainer>
);
});
SyncDeleteModal.displayName = 'SyncDeleteModal';
};

View File

@@ -1,6 +1,6 @@
import classnames from 'classnames';
import React, { FC, forwardRef, Fragment, useImperativeHandle, useRef } from 'react';
import { ListDropTargetDelegate, ListKeyboardDelegate, mergeProps, useDraggableCollection, useDraggableItem, useDropIndicator, useDroppableCollection, useDroppableItem, useFocusRing, useListBox, useOption } from 'react-aria';
import React, { FC, Fragment, useEffect, useRef } from 'react';
import { ListDropTargetDelegate, ListKeyboardDelegate, mergeProps, OverlayContainer, useDraggableCollection, useDraggableItem, useDropIndicator, useDroppableCollection, useDroppableItem, useFocusRing, useListBox, useOption } from 'react-aria';
import { useFetcher, useParams, useRouteLoaderData } from 'react-router-dom';
import { DraggableCollectionState, DroppableCollectionState, Item, ListState, useDraggableCollectionState, useDroppableCollectionState, useListState } from 'react-stately';
@@ -226,11 +226,7 @@ const ReorderableListBox = props => {
);
};
export interface WorkspaceEnvironmentsEditModalHandle {
show: () => void;
hide: () => void;
}
export const WorkspaceEnvironmentsEditModal = forwardRef<WorkspaceEnvironmentsEditModalHandle, ModalProps>((props, ref) => {
export const WorkspaceEnvironmentsEditModal = (props: ModalProps) => {
const { organizationId, projectId, workspaceId } = useParams<{ organizationId: string; projectId: string; workspaceId: string }>();
const routeData = useRouteLoaderData(
':workspaceId'
@@ -244,15 +240,9 @@ export const WorkspaceEnvironmentsEditModal = forwardRef<WorkspaceEnvironmentsEd
const updateEnvironmentFetcher = useFetcher();
const setActiveEnvironmentFetcher = useFetcher();
const duplicateEnvironmentFetcher = useFetcher();
useImperativeHandle(ref, () => ({
hide: () => {
modalRef.current?.hide();
},
show: async () => {
modalRef.current?.show();
},
}), []);
useEffect(() => {
modalRef.current?.show();
}, []);
if (!routeData) {
return null;
@@ -272,10 +262,10 @@ export const WorkspaceEnvironmentsEditModal = forwardRef<WorkspaceEnvironmentsEd
setActiveEnvironmentFetcher.submit({
environmentId,
},
{
method: 'post',
action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/environment/set-active`,
});
{
method: 'post',
action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/environment/set-active`,
});
}
}
@@ -283,10 +273,10 @@ export const WorkspaceEnvironmentsEditModal = forwardRef<WorkspaceEnvironmentsEd
deleteEnvironmentFetcher.submit({
environmentId,
},
{
method: 'post',
action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/environment/delete`,
});
{
method: 'post',
action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/environment/delete`,
});
}
const updateEnvironment = async (environmentId: string, patch: Partial<Environment>) => {
@@ -294,11 +284,11 @@ export const WorkspaceEnvironmentsEditModal = forwardRef<WorkspaceEnvironmentsEd
patch,
environmentId,
},
{
encType: 'application/json',
method: 'post',
action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/environment/update`,
});
{
encType: 'application/json',
method: 'post',
action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/environment/update`,
});
};
function onReorder(e: any) {
@@ -319,233 +309,234 @@ export const WorkspaceEnvironmentsEditModal = forwardRef<WorkspaceEnvironmentsEd
}
return (
<Modal ref={modalRef} wide tall {...props}>
<ModalHeader>Manage Environments</ModalHeader>
<ModalBody noScroll className="env-modal">
<div className="env-modal__sidebar">
<div
className={classnames('env-modal__sidebar-root-item', {
'env-modal__sidebar-item--active': activeEnvironment._id === baseEnvironment._id,
})}
>
<button
onClick={() => {
if (environmentEditorRef.current?.isValid() && activeEnvironment._id !== baseEnvironment._id) {
setActiveEnvironmentFetcher.submit({
environmentId: baseEnvironment._id,
},
{
method: 'post',
action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/environment/set-active`,
});
}
}}
<OverlayContainer>
<Modal ref={modalRef} wide tall onHide={props.onHide}>
<ModalHeader>Manage Environments</ModalHeader>
<ModalBody noScroll className="env-modal">
<div className="env-modal__sidebar">
<div
className={classnames('env-modal__sidebar-root-item', {
'env-modal__sidebar-item--active': activeEnvironment._id === baseEnvironment._id,
})}
>
{ROOT_ENVIRONMENT_NAME}
<HelpTooltip className="space-left">
The variables in this environment are always available, regardless of which
sub-environment is active. Useful for storing default or fallback values.
</HelpTooltip>
</button>
</div>
<div className="pad env-modal__sidebar-heading">
<h3 className="no-margin">Sub Environments</h3>
<Dropdown
aria-label='Create Environment Dropdown'
triggerButton={
<DropdownButton
data-testid='CreateEnvironmentDropdown'
>
<i className="fa fa-plus-circle" />
<i className="fa fa-caret-down" />
</DropdownButton>
}
>
<DropdownItem aria-label='Environment'>
<ItemContent
icon="eye"
label="Environment"
onClick={async () => {
createEnvironmentFetcher.submit({
isPrivate: false,
<button
onClick={() => {
if (environmentEditorRef.current?.isValid() && activeEnvironment._id !== baseEnvironment._id) {
setActiveEnvironmentFetcher.submit({
environmentId: baseEnvironment._id,
},
{
encType: 'application/json',
method: 'post',
action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/environment/create`,
});
}}
/>
</DropdownItem>
<DropdownItem aria-label='Private Environment'>
<ItemContent
icon="eye-slash"
label="Private Environment"
onClick={async () => {
createEnvironmentFetcher.submit({
isPrivate: true,
},
{
encType: 'application/json',
method: 'post',
action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/environment/create`,
});
}}
/>
</DropdownItem>
</Dropdown>
</div>
<ReorderableListBox
items={subEnvironments}
onSelectionChange={onSelectionChange}
onReorder={onReorder}
selectionMode="multiple"
selectionBehavior="replace"
aria-label="list of subenvironments"
>
{(environment: any) =>
<Item key={environment._id}>
{environment.name}
</Item>
}
</ReorderableListBox>
</div>
<div className="env-modal__main">
<div className="env-modal__main__header">
<h1>
{baseEnvironment._id === activeEnvironment._id ? (
ROOT_ENVIRONMENT_NAME
) : (
<Editable
singleClick
className="wide"
onSubmit={name => {
if (activeEnvironment._id && name) {
updateEnvironment(activeEnvironment._id, { name });
}
}}
value={activeEnvironment.name}
/>
)}
</h1>
{baseEnvironment._id !== activeEnvironment._id ? (
<Fragment>
<input
className="hidden"
type="color"
ref={inputRef}
onChange={event => updateEnvironment(activeEnvironment._id, { color: event.target.value })}
/>
<Dropdown
aria-label='Environment Color Dropdown'
className="space-right"
triggerButton={
<DropdownButton
className="btn btn--clicky"
disableHoverBehavior={false}
>
{activeEnvironment.color && (
<i
className="fa fa-circle space-right"
style={{
color: activeEnvironment.color,
}}
/>
)}
Color <i className="fa fa-caret-down" />
</DropdownButton>
}
>
<DropdownItem aria-label={activeEnvironment.color ? 'Change Color' : 'Assign Color'}>
<ItemContent
icon="circle"
label={activeEnvironment.color ? 'Change Color' : 'Assign Color'}
iconStyle={{
...(activeEnvironment.color ? { color: activeEnvironment.color } : {}),
}}
onClick={() => {
if (!activeEnvironment.color) {
// TODO: fix magic-number. Currently this is the `surprise` background color for the default theme,
// but we should be grabbing the actual value from the user's actual theme instead.
updateEnvironment(activeEnvironment._id, { color: '#7d69cb' });
}
inputRef.current?.click();
}}
/>
</DropdownItem>
<DropdownItem aria-label='Unset Color'>
<ItemContent
isDisabled={!activeEnvironment.color}
icon="minus-circle"
label="Unset Color"
onClick={() => updateEnvironment(activeEnvironment._id, { color: null })}
/>
</DropdownItem>
</Dropdown>
<button
onClick={async () => {
if (activeEnvironment) {
duplicateEnvironmentFetcher.submit({
environmentId: activeEnvironment._id,
}, {
{
method: 'post',
action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/environment/duplicate`,
action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/environment/set-active`,
});
}
}}
className="btn btn--clicky space-right"
>
<i className="fa fa-copy" /> Duplicate
</button>
}
}}
>
{ROOT_ENVIRONMENT_NAME}
<HelpTooltip className="space-left">
The variables in this environment are always available, regardless of which
sub-environment is active. Useful for storing default or fallback values.
</HelpTooltip>
</button>
</div>
<div className="pad env-modal__sidebar-heading">
<h3 className="no-margin">Sub Environments</h3>
<Dropdown
aria-label='Create Environment Dropdown'
triggerButton={
<DropdownButton
data-testid='CreateEnvironmentDropdown'
>
<i className="fa fa-plus-circle" />
<i className="fa fa-caret-down" />
</DropdownButton>
}
>
<DropdownItem aria-label='Environment'>
<ItemContent
icon="eye"
label="Environment"
onClick={async () => {
createEnvironmentFetcher.submit({
isPrivate: false,
},
{
encType: 'application/json',
method: 'post',
action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/environment/create`,
});
}}
/>
</DropdownItem>
<DropdownItem aria-label='Private Environment'>
<ItemContent
icon="eye-slash"
label="Private Environment"
onClick={async () => {
createEnvironmentFetcher.submit({
isPrivate: true,
},
{
encType: 'application/json',
method: 'post',
action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/environment/create`,
});
}}
/>
</DropdownItem>
</Dropdown>
</div>
<ReorderableListBox
items={subEnvironments}
onSelectionChange={onSelectionChange}
onReorder={onReorder}
selectionMode="multiple"
selectionBehavior="replace"
aria-label="list of subenvironments"
>
{(environment: any) =>
<Item key={environment._id}>
{environment.name}
</Item>
}
</ReorderableListBox>
</div>
<div className="env-modal__main">
<div className="env-modal__main__header">
<h1>
{baseEnvironment._id === activeEnvironment._id ? (
ROOT_ENVIRONMENT_NAME
) : (
<Editable
singleClick
className="wide"
onSubmit={name => {
if (activeEnvironment._id && name) {
updateEnvironment(activeEnvironment._id, { name });
}
}}
value={activeEnvironment.name}
/>
)}
</h1>
{activeEnvironment._id !== baseEnvironment._id && <PromptButton
onClick={() => handleDeleteEnvironment(activeEnvironment._id)}
className="btn btn--clicky"
>
<i className="fa fa-trash-o" />
</PromptButton>}
</Fragment>
) : null}
{baseEnvironment._id !== activeEnvironment._id ? (
<Fragment>
<input
className="hidden"
type="color"
ref={inputRef}
onChange={event => updateEnvironment(activeEnvironment._id, { color: event.target.value })}
/>
<Dropdown
aria-label='Environment Color Dropdown'
className="space-right"
triggerButton={
<DropdownButton
className="btn btn--clicky"
disableHoverBehavior={false}
>
{activeEnvironment.color && (
<i
className="fa fa-circle space-right"
style={{
color: activeEnvironment.color,
}}
/>
)}
Color <i className="fa fa-caret-down" />
</DropdownButton>
}
>
<DropdownItem aria-label={activeEnvironment.color ? 'Change Color' : 'Assign Color'}>
<ItemContent
icon="circle"
label={activeEnvironment.color ? 'Change Color' : 'Assign Color'}
iconStyle={{
...(activeEnvironment.color ? { color: activeEnvironment.color } : {}),
}}
onClick={() => {
if (!activeEnvironment.color) {
// TODO: fix magic-number. Currently this is the `surprise` background color for the default theme,
// but we should be grabbing the actual value from the user's actual theme instead.
updateEnvironment(activeEnvironment._id, { color: '#7d69cb' });
}
inputRef.current?.click();
}}
/>
</DropdownItem>
<DropdownItem aria-label='Unset Color'>
<ItemContent
isDisabled={!activeEnvironment.color}
icon="minus-circle"
label="Unset Color"
onClick={() => updateEnvironment(activeEnvironment._id, { color: null })}
/>
</DropdownItem>
</Dropdown>
<button
onClick={async () => {
if (activeEnvironment) {
duplicateEnvironmentFetcher.submit({
environmentId: activeEnvironment._id,
}, {
method: 'post',
action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/environment/duplicate`,
});
}
}}
className="btn btn--clicky space-right"
>
<i className="fa fa-copy" /> Duplicate
</button>
{activeEnvironment._id !== baseEnvironment._id && <PromptButton
onClick={() => handleDeleteEnvironment(activeEnvironment._id)}
className="btn btn--clicky"
>
<i className="fa fa-trash-o" />
</PromptButton>}
</Fragment>
) : null}
</div>
<div className="env-modal__editor">
<EnvironmentEditor
ref={environmentEditorRef}
key={`${activeEnvironment._id}`}
environmentInfo={{
object: activeEnvironment.data,
propertyOrder: activeEnvironment.dataPropertyOrder,
}}
onBlur={() => {
// Only save if it's valid
if (!environmentEditorRef.current || !environmentEditorRef.current?.isValid()) {
return;
}
const data = environmentEditorRef.current?.getValue();
if (activeEnvironment && data) {
updateEnvironment(activeEnvironment._id, {
data: data.object,
dataPropertyOrder: data.propertyOrder,
});
}
}}
/>
</div>
</div>
<div className="env-modal__editor">
<EnvironmentEditor
ref={environmentEditorRef}
key={`${activeEnvironment._id}`}
environmentInfo={{
object: activeEnvironment.data,
propertyOrder: activeEnvironment.dataPropertyOrder,
}}
onBlur={() => {
// Only save if it's valid
if (!environmentEditorRef.current || !environmentEditorRef.current?.isValid()) {
return;
}
const data = environmentEditorRef.current?.getValue();
if (activeEnvironment && data) {
updateEnvironment(activeEnvironment._id, {
data: data.object,
dataPropertyOrder: data.propertyOrder,
});
}
}}
/>
</ModalBody>
<ModalFooter>
<div className="margin-left italic txt-sm">
* Environment data can be used for&nbsp;
<Link href={docsTemplateTags}>Nunjucks Templating</Link> in your requests
</div>
</div>
</ModalBody>
<ModalFooter>
<div className="margin-left italic txt-sm">
* Environment data can be used for&nbsp;
<Link href={docsTemplateTags}>Nunjucks Templating</Link> in your requests
</div>
<button className="btn" onClick={() => modalRef.current?.hide()}>
Close
</button>
</ModalFooter>
</Modal>
<button className="btn" onClick={() => modalRef.current?.hide()}>
Close
</button>
</ModalFooter>
</Modal>
</OverlayContainer>
);
});
WorkspaceEnvironmentsEditModal.displayName = 'WorkspaceEnvironmentsEditModal';
};

View File

@@ -93,7 +93,7 @@ export const WorkspaceSettingsModal = ({ workspace, workspaceMeta, clientCertifi
const activeWorkspaceName = workspace.name;
useEffect(() => {
modalRef.current?.show();
});
}, []);
const { organizationId, projectId } = useParams<{ organizationId: string; projectId: string }>();
const workspaceFetcher = useFetcher();

View File

@@ -9,12 +9,12 @@ import { generateId } from '../../../common/misc';
import { getRenderContext, getRenderedGrpcRequest, getRenderedGrpcRequestMessage, render, RENDER_PURPOSE_SEND } from '../../../common/render';
import { GrpcMethodType } from '../../../main/ipc/grpc';
import * as models from '../../../models';
import type { GrpcRequest, GrpcRequestHeader } from '../../../models/grpc-request';
import type { GrpcRequestHeader } from '../../../models/grpc-request';
import { queryAllWorkspaceUrls } from '../../../models/helpers/query-all-workspace-urls';
import { useRequestPatcher } from '../../hooks/use-request';
import { useActiveRequestSyncVCSVersion, useGitVCSVersion } from '../../hooks/use-vcs-version';
import { GrpcRequestState } from '../../routes/debug';
import { RequestLoaderData } from '../../routes/request';
import { GrpcRequestLoaderData } from '../../routes/request';
import { WorkspaceLoaderData } from '../../routes/workspace';
import { PanelContainer, TabItem, Tabs } from '../base/tabs';
import { GrpcSendButton } from '../buttons/grpc-send-button';
@@ -73,7 +73,7 @@ export const GrpcRequestPane: FunctionComponent<Props> = ({
setGrpcState,
reloadRequests,
}) => {
const { activeRequest } = useRouteLoaderData('request/:requestId') as RequestLoaderData<GrpcRequest, any>;
const { activeRequest } = useRouteLoaderData('request/:requestId') as GrpcRequestLoaderData;
const [isProtoModalOpen, setIsProtoModalOpen] = useState(false);
const { requestMessages, running, methods } = grpcState;

View File

@@ -1,4 +1,4 @@
import React, { FC, useCallback, useEffect, useRef } from 'react';
import React, { FC, useCallback, useEffect, useRef, useState } from 'react';
import { useParams, useRouteLoaderData } from 'react-router-dom';
import styled from 'styled-components';
@@ -6,11 +6,9 @@ import { getContentTypeFromHeaders } from '../../../common/constants';
import { database } from '../../../common/database';
import * as models from '../../../models';
import { queryAllWorkspaceUrls } from '../../../models/helpers/query-all-workspace-urls';
import { Request } from '../../../models/request';
import { RequestMeta } from '../../../models/request-meta';
import type { Settings } from '../../../models/settings';
import { deconstructQueryStringToParams, extractQueryStringFromUrl } from '../../../utils/url/querystring';
import { useRequestPatcher } from '../../hooks/use-request';
import { useRequestPatcher, useSettingsPatcher } from '../../hooks/use-request';
import { useActiveRequestSyncVCSVersion, useGitVCSVersion } from '../../hooks/use-vcs-version';
import { RequestLoaderData } from '../../routes/request';
import { WorkspaceLoaderData } from '../../routes/workspace';
@@ -24,7 +22,6 @@ import { RequestHeadersEditor } from '../editors/request-headers-editor';
import { RequestParametersEditor } from '../editors/request-parameters-editor';
import { ErrorBoundary } from '../error-boundary';
import { MarkdownPreview } from '../markdown-preview';
import { showModal } from '../modals';
import { RequestSettingsModal } from '../modals/request-settings-modal';
import { RenderedQueryString } from '../rendered-query-string';
import { RequestUrlBar, RequestUrlBarHandle } from '../request-url-bar';
@@ -68,25 +65,11 @@ export const RequestPane: FC<Props> = ({
settings,
setLoading,
}) => {
const { activeRequest, activeRequestMeta } = useRouteLoaderData('request/:requestId') as RequestLoaderData<Request, RequestMeta>;
const { activeRequest, activeRequestMeta } = useRouteLoaderData('request/:requestId') as RequestLoaderData;
const { workspaceId, requestId } = useParams() as { organizationId: string; projectId: string; workspaceId: string; requestId: string };
const patchRequest = useRequestPatcher();
const handleEditDescription = useCallback((forceEditMode: boolean) => {
showModal(RequestSettingsModal, { request: activeRequest, forceEditMode });
}, [activeRequest]);
const handleEditDescriptionAdd = useCallback(() => {
handleEditDescription(true);
}, [handleEditDescription]);
const handleUpdateSettingsUseBulkHeaderEditor = useCallback(() => {
models.settings.update(settings, { useBulkHeaderEditor: !settings.useBulkHeaderEditor });
}, [settings]);
const handleUpdateSettingsUseBulkParametersEditor = useCallback(() => {
models.settings.update(settings, { useBulkParametersEditor: !settings.useBulkParametersEditor });
}, [settings]);
const patchSettings = useSettingsPatcher();
const [isRequestSettingsModalOpen, setIsRequestSettingsModalOpen] = useState(false);
const handleImportQueryFromUrl = useCallback(() => {
let query;
@@ -202,7 +185,7 @@ export const RequestPane: FC<Props> = ({
</button>
<button
className="btn btn--compact"
onClick={handleUpdateSettingsUseBulkParametersEditor}
onClick={() => patchSettings({ useBulkParametersEditor: !settings.useBulkParametersEditor })}
>
{settings.useBulkParametersEditor ? 'Regular Edit' : 'Bulk Edit'}
</button>
@@ -222,7 +205,7 @@ export const RequestPane: FC<Props> = ({
<TabPanelFooter>
<button
className="btn btn--compact"
onClick={handleUpdateSettingsUseBulkHeaderEditor}
onClick={() => patchSettings({ useBulkHeaderEditor: !settings.useBulkHeaderEditor })}
>
{settings.useBulkHeaderEditor ? 'Regular Edit' : 'Bulk Edit'}
</button>
@@ -246,8 +229,7 @@ export const RequestPane: FC<Props> = ({
{activeRequest.description ? (
<div>
<div className="pull-right pad bg-default">
{/* @ts-expect-error -- TSCONVERSION the click handler expects a boolean prop... */}
<button className="btn btn--clicky" onClick={handleEditDescription}>
<button className="btn btn--clicky" onClick={() => setIsRequestSettingsModalOpen(true)}>
Edit
</button>
</div>
@@ -274,10 +256,7 @@ export const RequestPane: FC<Props> = ({
</span>
<br />
<br />
<button
className="btn btn--clicky faint"
onClick={handleEditDescriptionAdd}
>
<button className="btn btn--clicky faint" onClick={() => setIsRequestSettingsModalOpen(true)}>
Add Description
</button>
</p>
@@ -286,6 +265,12 @@ export const RequestPane: FC<Props> = ({
</PanelContainer>
</TabItem>
</Tabs>
{isRequestSettingsModalOpen && (
<RequestSettingsModal
request={activeRequest}
onHide={() => setIsRequestSettingsModalOpen(false)}
/>
)}
</Pane>
);
};

View File

@@ -1,19 +1,14 @@
import fs from 'fs';
import { extension as mimeExtension } from 'mime-types';
import React, { FC, useCallback } from 'react';
import { useSelector } from 'react-redux';
import { useRouteLoaderData } from 'react-router-dom';
import { PREVIEW_MODE_SOURCE } from '../../../common/constants';
import { getSetCookieHeaders } from '../../../common/misc';
import * as models from '../../../models';
import type { Request } from '../../../models/request';
import { RequestMeta } from '../../../models/request-meta';
import type { Response } from '../../../models/response';
import { cancelRequestById } from '../../../network/cancellation';
import { jsonPrettify } from '../../../utils/prettify/json';
import { useRequestMetaPatcher } from '../../hooks/use-request';
import { selectActiveResponse } from '../../redux/selectors';
import { RequestLoaderData } from '../../routes/request';
import { RootLoaderData } from '../../routes/root';
import { PanelContainer, TabItem, Tabs } from '../base/tabs';
@@ -39,8 +34,7 @@ interface Props {
export const ResponsePane: FC<Props> = ({
runningRequests,
}) => {
const { activeRequest, activeRequestMeta } = useRouteLoaderData('request/:requestId') as RequestLoaderData<Request, RequestMeta>;
const response = useSelector(selectActiveResponse) as Response | null;
const { activeRequest, activeRequestMeta, activeResponse } = useRouteLoaderData('request/:requestId') as RequestLoaderData;
const filterHistory = activeRequestMeta.responseFilterHistory || [];
const filter = activeRequestMeta.responseFilter || '';
const patchRequestMeta = useRequestMetaPatcher();
@@ -49,10 +43,10 @@ export const ResponsePane: FC<Props> = ({
} = useRouteLoaderData('root') as RootLoaderData;
const previewMode = activeRequestMeta.previewMode || PREVIEW_MODE_SOURCE;
const handleSetFilter = async (responseFilter: string) => {
if (!response) {
if (!activeResponse) {
return;
}
const requestId = response.parentId;
const requestId = activeResponse.parentId;
await patchRequestMeta(requestId, { responseFilter });
const meta = await models.requestMeta.getByParentId(requestId);
if (!meta) {
@@ -67,11 +61,11 @@ export const ResponsePane: FC<Props> = ({
patchRequestMeta(requestId, { responseFilterHistory });
};
const handleGetResponseBody = useCallback(() => {
if (!response) {
if (!activeResponse) {
return null;
}
return models.response.getBodyBuffer(response);
}, [response]);
return models.response.getBodyBuffer(activeResponse);
}, [activeResponse]);
const handleCopyResponseToClipboard = useCallback(async () => {
const bodyBuffer = handleGetResponseBody();
if (bodyBuffer) {
@@ -79,12 +73,12 @@ export const ResponsePane: FC<Props> = ({
}
}, [handleGetResponseBody]);
const handleDownloadResponseBody = useCallback(async (prettify: boolean) => {
if (!response || !activeRequest) {
if (!activeResponse || !activeRequest) {
console.warn('Nothing to download');
return;
}
const { contentType } = response;
const { contentType } = activeResponse;
const extension = mimeExtension(contentType) || 'unknown';
const { canceled, filePath: outputPath } = await window.dialog.showSaveDialog({
title: 'Save Response Body',
@@ -96,7 +90,7 @@ export const ResponsePane: FC<Props> = ({
return;
}
const readStream = models.response.getBodyStream(response);
const readStream = models.response.getBodyStream(activeResponse);
const dataBuffers: any[] = [];
if (readStream && outputPath && typeof readStream !== 'string') {
@@ -123,14 +117,14 @@ export const ResponsePane: FC<Props> = ({
to.end();
});
}
}, [activeRequest, response]);
}, [activeRequest, activeResponse]);
if (!activeRequest) {
return <BlankPane type="response" />;
}
// If there is no previous response, show placeholder for loading indicator
if (!response) {
if (!activeResponse) {
return (
<PlaceholderResponsePane>
{runningRequests[activeRequest._id] && <ResponseTimer
@@ -139,20 +133,19 @@ export const ResponsePane: FC<Props> = ({
</PlaceholderResponsePane>
);
}
const timeline = models.response.getTimeline(response);
const cookieHeaders = getSetCookieHeaders(response.headers);
const timeline = models.response.getTimeline(activeResponse);
const cookieHeaders = getSetCookieHeaders(activeResponse.headers);
return (
<Pane type="response">
{!response ? null : (
{!activeResponse ? null : (
<PaneHeader className="row-spaced">
<div aria-atomic="true" aria-live="polite" className="no-wrap scrollable scrollable--no-bars pad-left">
<StatusTag statusCode={response.statusCode} statusMessage={response.statusMessage} />
<TimeTag milliseconds={response.elapsedTime} />
<SizeTag bytesRead={response.bytesRead} bytesContent={response.bytesContent} />
<StatusTag statusCode={activeResponse.statusCode} statusMessage={activeResponse.statusMessage} />
<TimeTag milliseconds={activeResponse.elapsedTime} />
<SizeTag bytesRead={activeResponse.bytesRead} bytesContent={activeResponse.bytesContent} />
</div>
<ResponseHistoryDropdown
activeResponse={response}
className="tall pane__header__right"
activeResponse={activeResponse}
/>
</PaneHeader>
)}
@@ -167,21 +160,21 @@ export const ResponsePane: FC<Props> = ({
}
>
<ResponseViewer
key={response._id}
bytes={Math.max(response.bytesContent, response.bytesRead)}
contentType={response.contentType || ''}
key={activeResponse._id}
bytes={Math.max(activeResponse.bytesContent, activeResponse.bytesRead)}
contentType={activeResponse.contentType || ''}
disableHtmlPreviewJs={settings.disableHtmlPreviewJs}
disablePreviewLinks={settings.disableResponsePreviewLinks}
download={handleDownloadResponseBody}
editorFontSize={settings.editorFontSize}
error={response.error}
error={activeResponse.error}
filter={filter}
filterHistory={filterHistory}
getBody={handleGetResponseBody}
previewMode={response.error ? PREVIEW_MODE_SOURCE : previewMode}
responseId={response._id}
updateFilter={response.error ? undefined : handleSetFilter}
url={response.url}
previewMode={activeResponse.error ? PREVIEW_MODE_SOURCE : previewMode}
responseId={activeResponse._id}
updateFilter={activeResponse.error ? undefined : handleSetFilter}
url={activeResponse.url}
/>
</TabItem>
<TabItem
@@ -189,15 +182,15 @@ export const ResponsePane: FC<Props> = ({
title={
<>
Headers
{response.headers.length > 0 && (
<span className="bubble">{response.headers.length}</span>
{activeResponse.headers.length > 0 && (
<span className="bubble">{activeResponse.headers.length}</span>
)}
</>
}
>
<PanelContainer className="pad">
<ErrorBoundary key={response._id} errorClassName="font-error pad text-center">
<ResponseHeadersViewer headers={response.headers} />
<ErrorBoundary key={activeResponse._id} errorClassName="font-error pad text-center">
<ResponseHeadersViewer headers={activeResponse.headers} />
</ErrorBoundary>
</PanelContainer>
</TabItem>
@@ -213,19 +206,19 @@ export const ResponsePane: FC<Props> = ({
}
>
<PanelContainer className="pad">
<ErrorBoundary key={response._id} errorClassName="font-error pad text-center">
<ErrorBoundary key={activeResponse._id} errorClassName="font-error pad text-center">
<ResponseCookiesViewer
cookiesSent={response.settingSendCookies}
cookiesStored={response.settingStoreCookies}
cookiesSent={activeResponse.settingSendCookies}
cookiesStored={activeResponse.settingStoreCookies}
headers={cookieHeaders}
/>
</ErrorBoundary>
</PanelContainer>
</TabItem>
<TabItem key="timeline" title="Timeline">
<ErrorBoundary key={response._id} errorClassName="font-error pad text-center">
<ErrorBoundary key={activeResponse._id} errorClassName="font-error pad text-center">
<ResponseTimelineViewer
key={response._id}
key={activeResponse._id}
timeline={timeline}
/>
</ErrorBoundary>

View File

@@ -4,7 +4,7 @@ import fs from 'fs';
import { extension as mimeExtension } from 'mime-types';
import path from 'path';
import React, { forwardRef, useCallback, useImperativeHandle, useRef, useState } from 'react';
import { useParams, useRouteLoaderData } from 'react-router-dom';
import { useFetcher, useParams, useRouteLoaderData } from 'react-router-dom';
import { useInterval } from 'react-use';
import styled from 'styled-components';
@@ -12,8 +12,7 @@ import { database } from '../../common/database';
import { getContentDispositionHeader } from '../../common/misc';
import { getRenderContext, render, RENDER_PURPOSE_SEND } from '../../common/render';
import * as models from '../../models';
import { isEventStreamRequest, isRequest, Request } from '../../models/request';
import { RequestMeta } from '../../models/request-meta';
import { isEventStreamRequest, isRequest } from '../../models/request';
import * as network from '../../network/network';
import { convert } from '../../utils/importers/convert';
import { buildQueryStringFromParams, joinUrlAndQueryString } from '../../utils/url/querystring';
@@ -22,7 +21,7 @@ import { useReadyState } from '../hooks/use-ready-state';
import { useRequestPatcher } from '../hooks/use-request';
import { useRequestMetaPatcher } from '../hooks/use-request';
import { useTimeoutWhen } from '../hooks/useTimeoutWhen';
import { RequestLoaderData } from '../routes/request';
import { ConnectActionParams, RequestLoaderData } from '../routes/request';
import { RootLoaderData } from '../routes/root';
import { WorkspaceLoaderData } from '../routes/workspace';
import { Dropdown, DropdownButton, type DropdownHandle, DropdownItem, DropdownSection, ItemContent } from './base/dropdown';
@@ -69,10 +68,9 @@ export const RequestUrlBar = forwardRef<RequestUrlBarHandle, Props>(({
settings,
} = useRouteLoaderData('root') as RootLoaderData;
const { hotKeyRegistry } = settings;
const { activeRequest, activeRequestMeta } = useRouteLoaderData('request/:requestId') as RequestLoaderData<Request, RequestMeta>;
const { activeRequest, activeRequestMeta } = useRouteLoaderData('request/:requestId') as RequestLoaderData;
const downloadPath = activeRequestMeta.downloadPath;
const patchRequestMeta = useRequestMetaPatcher();
const { requestId } = useParams() as { requestId: string };
const methodDropdownRef = useRef<DropdownHandle>(null);
const dropdownRef = useRef<DropdownHandle>(null);
const inputRef = useRef<OneLineEditorHandle>(null);
@@ -191,12 +189,9 @@ export const RequestUrlBar = forwardRef<RequestUrlBarHandle, Props>(({
),
});
} finally {
// Unset active response because we just made a new one
// TODO: remove this with the redux fallback to first element
await patchRequestMeta(activeRequest._id, { activeResponseId: null });
setLoading(false);
}
}, [activeEnvironment._id, activeRequest, setLoading, settings.maxHistoryResponses, settings.preferredHttpVersion, patchRequestMeta]);
}, [activeEnvironment._id, activeRequest, setLoading, settings.maxHistoryResponses, settings.preferredHttpVersion]);
const handleSend = useCallback(async () => {
if (!activeRequest) {
@@ -241,8 +236,17 @@ export const RequestUrlBar = forwardRef<RequestUrlBarHandle, Props>(({
await patchRequestMeta(activeRequest._id, { activeResponseId: null });
setLoading(false);
}, [activeEnvironment._id, activeRequest, setLoading, settings.maxHistoryResponses, settings.preferredHttpVersion, patchRequestMeta]);
const send = useCallback(() => {
const fetcher = useFetcher();
const { organizationId, projectId, workspaceId, requestId } = useParams() as { organizationId: string; projectId: string; workspaceId: string; requestId: string };
const connect = (connectParams: ConnectActionParams) => {
fetcher.submit(JSON.stringify(connectParams),
{
action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/debug/request/${requestId}/connect`,
method: 'post',
encType: 'application/json',
});
};
const send = () => {
setCurrentTimeout(undefined);
if (downloadPath) {
sendThenSetFilePath(downloadPath);
@@ -262,9 +266,7 @@ export const RequestUrlBar = forwardRef<RequestUrlBarHandle, Props>(({
parameters: activeRequest.parameters.filter(p => !p.disabled),
workspaceCookieJar,
}, renderContext);
window.main.curl.open({
requestId: activeRequest._id,
workspaceId,
connect({
url: joinUrlAndQueryString(rendered.url, buildQueryStringFromParams(rendered.parameters)),
headers: rendered.headers,
authentication: rendered.authentication,
@@ -275,7 +277,7 @@ export const RequestUrlBar = forwardRef<RequestUrlBarHandle, Props>(({
return;
}
handleSend();
}, [activeEnvironment._id, activeWorkspace, downloadPath, handleSend, activeRequest, sendThenSetFilePath]);
};
useInterval(send, currentInterval ? currentInterval : null);
useTimeoutWhen(send, currentTimeout, !!currentTimeout);

View File

@@ -1,9 +1,9 @@
import React, { ChangeEventHandler, FC, ReactNode, useCallback } from 'react';
import React, { FC, ReactNode } from 'react';
import { useRouteLoaderData } from 'react-router-dom';
import styled from 'styled-components';
import { SettingsOfType } from '../../../common/settings';
import * as models from '../../../models/index';
import { useSettingsPatcher } from '../../hooks/use-request';
import { RootLoaderData } from '../../routes/root';
import { HelpTooltip } from '../help-tooltip';
const Descriptions = styled.div({
@@ -36,10 +36,7 @@ export const BooleanSetting: FC<{
if (!settings.hasOwnProperty(setting)) {
throw new Error(`Invalid boolean setting name ${setting}`);
}
const onChange = useCallback<ChangeEventHandler<HTMLInputElement>>(async ({ currentTarget: { checked } }) => {
await models.settings.patch({ [setting]: checked });
}, [setting]);
const patchSettings = useSettingsPatcher();
return (
<>
@@ -50,7 +47,7 @@ export const BooleanSetting: FC<{
<input
checked={Boolean(settings[setting])}
name={setting}
onChange={onChange}
onChange={event => patchSettings({ [setting]: event.currentTarget.checked })}
type="checkbox"
disabled={disabled}
/>

View File

@@ -1,8 +1,8 @@
import React, { ChangeEventHandler, PropsWithChildren, ReactNode, useCallback } from 'react';
import React, { PropsWithChildren, ReactNode } from 'react';
import { useRouteLoaderData } from 'react-router-dom';
import { SettingsOfType } from '../../../common/settings';
import * as models from '../../../models/index';
import { useSettingsPatcher } from '../../hooks/use-request';
import { RootLoaderData } from '../../routes/root';
import { HelpTooltip } from '../help-tooltip';
interface Props<T> {
@@ -25,9 +25,7 @@ export const EnumSetting = <T extends string | number>({
settings,
} = useRouteLoaderData('root') as RootLoaderData;
const onChange = useCallback<ChangeEventHandler<HTMLSelectElement>>(async ({ currentTarget: { value } }) => {
await models.settings.patch({ [setting]: value });
}, [setting]);
const patchSettings = useSettingsPatcher();
return (
<div className="form-control form-control--outlined">
@@ -37,7 +35,8 @@ export const EnumSetting = <T extends string | number>({
<select
value={String(settings[setting]) || '__NULL__'}
name={setting}
onChange={onChange}
onChange={event => patchSettings({ [setting]: event.currentTarget.value })}
>
{values.map(({ name, value }) => (
<option key={value} value={value}>

View File

@@ -8,15 +8,12 @@ import { docsImportExport } from '../../../common/documentation';
import { exportAllToFile } from '../../../common/export';
import { getWorkspaceLabel } from '../../../common/get-workspace-label';
import { strings } from '../../../common/strings';
import { isRequestGroup } from '../../../models/request-group';
import { selectWorkspaceRequestsAndRequestGroups, selectWorkspacesForActiveProject } from '../../redux/selectors';
import { selectWorkspacesForActiveProject } from '../../redux/selectors';
import { WorkspaceLoaderData } from '../../routes/workspace';
import { Dropdown, DropdownButton, DropdownItem, DropdownSection, ItemContent } from '../base/dropdown';
import { Link } from '../base/link';
import { AlertModal } from '../modals/alert-modal';
import { ExportRequestsModal } from '../modals/export-requests-modal';
import { ImportModal } from '../modals/import-modal';
import { showModal } from '../modals/index';
import { Button } from '../themed-button';
interface Props {
hideSettingsModal: () => void;
@@ -34,20 +31,8 @@ export const ImportExport: FC<Props> = ({ hideSettingsModal }) => {
const projectName = workspaceData?.activeProject.name ?? getProductName();
const workspacesForActiveProject = useSelector(selectWorkspacesForActiveProject);
const workspaceRequestsAndRequestGroups = useSelector(selectWorkspaceRequestsAndRequestGroups);
const [isImportModalOpen, setIsImportModalOpen] = useState(false);
const showExportRequestsModal = useCallback(() => {
if (!workspaceRequestsAndRequestGroups.filter(r => !isRequestGroup(r)).length) {
showModal(AlertModal, {
title: 'Cannot export',
message: <>There are no requests to export.</>,
});
return;
}
showModal(ExportRequestsModal);
hideSettingsModal();
}, [hideSettingsModal, workspaceRequestsAndRequestGroups]);
const [isExportModalOpen, setIsExportModalOpen] = useState(false);
const handleExportAllToFile = useCallback(() => {
exportAllToFile(projectName, workspacesForActiveProject);
@@ -81,7 +66,7 @@ export const ImportExport: FC<Props> = ({ hideSettingsModal }) => {
<ItemContent
icon="home"
label={`Export the "${activeWorkspaceName}" ${getWorkspaceLabel(workspaceData.activeWorkspace).singular}`}
onClick={showExportRequestsModal}
onClick={() => setIsExportModalOpen(true)}
/>
</DropdownItem>
<DropdownItem aria-label={`Export files from the "${projectName}" ${strings.project.singular}`}>
@@ -94,7 +79,7 @@ export const ImportExport: FC<Props> = ({ hideSettingsModal }) => {
</DropdownSection>
</Dropdown>) : (<Button onClick={handleExportAllToFile}>{`Export files from the "${projectName}" ${strings.project.singular}`}</Button>)
}
&nbsp;&nbsp;
&nbsp;&nbsp;
<Button
style={{
display: 'flex',
@@ -106,7 +91,7 @@ export const ImportExport: FC<Props> = ({ hideSettingsModal }) => {
<i className="fa fa-file-import" />
{`Import to the "${projectName}" ${strings.project.singular}`}
</Button>
&nbsp;&nbsp;
&nbsp;&nbsp;
<Link href="https://insomnia.rest/create-run-button" className="btn btn--compact" button>
Create Run Button
</Link>
@@ -124,6 +109,11 @@ export const ImportExport: FC<Props> = ({ hideSettingsModal }) => {
defaultWorkspaceId={workspaceId}
/>
)}
{isExportModalOpen && (
<ExportRequestsModal
onHide={() => setIsExportModalOpen(false)}
/>
)}
</Fragment>
);
};

View File

@@ -1,9 +1,9 @@
import React, { ChangeEvent, FC } from 'react';
import React, { FC } from 'react';
import { useRouteLoaderData } from 'react-router-dom';
import { useToggle } from 'react-use';
import { SettingsOfType } from '../../../common/settings';
import * as models from '../../../models';
import { useSettingsPatcher } from '../../hooks/use-request';
import { RootLoaderData } from '../../routes/root';
import { HelpTooltip } from '../help-tooltip';
@@ -29,12 +29,7 @@ export const MaskedSetting: FC<{
if (!settings.hasOwnProperty(setting)) {
throw new Error(`Invalid setting name ${setting}`);
}
const onChange = async (event: ChangeEvent<HTMLInputElement>) => {
await models.settings.patch({
[setting]: event.currentTarget.value,
});
};
const patchSettings = useSettingsPatcher();
return (
<div>
@@ -47,7 +42,7 @@ export const MaskedSetting: FC<{
defaultValue={String(settings[setting])}
disabled={disabled}
name={setting}
onChange={onChange}
onChange={event => patchSettings({ [setting]: event.currentTarget.value })}
placeholder={placeholder}
type={!settings.showPasswords && isHidden ? 'password' : 'text'}
/>

View File

@@ -3,7 +3,7 @@ import { useRouteLoaderData } from 'react-router-dom';
import { snapNumberToLimits } from '../../../common/misc';
import { SettingsOfType } from '../../../common/settings';
import * as models from '../../../models/index';
import { useSettingsPatcher } from '../../hooks/use-request';
import { RootLoaderData } from '../../routes/root';
import { HelpTooltip } from '../help-tooltip';
@@ -31,6 +31,7 @@ export const NumberSetting: FC<Props> = ({
if (!Object.prototype.hasOwnProperty.call(settings, setting)) {
throw new Error(`Invalid setting name ${setting}`);
}
const patchSettings = useSettingsPatcher();
const handleOnChange = useCallback<ChangeEventHandler<HTMLInputElement>>(async ({ currentTarget: { value, min, max } }) => {
const updatedValue = snapNumberToLimits(
@@ -38,8 +39,8 @@ export const NumberSetting: FC<Props> = ({
parseInt(min, 10),
parseInt(max, 10),
);
await models.settings.patch({ [setting]: updatedValue });
}, [setting]);
patchSettings({ [setting]: updatedValue });
}, [patchSettings, setting]);
let defaultValue: string | number = settings[setting];
if (typeof defaultValue !== 'number') {

View File

@@ -7,11 +7,11 @@ import {
PLUGIN_HUB_BASE,
} from '../../../common/constants';
import { docsPlugins } from '../../../common/documentation';
import * as models from '../../../models';
import { createPlugin } from '../../../plugins/create';
import type { Plugin } from '../../../plugins/index';
import { getPlugins } from '../../../plugins/index';
import { reload } from '../../../templating/index';
import { useSettingsPatcher } from '../../hooks/use-request';
import { RootLoaderData } from '../../routes/root';
import { CopyButton } from '../base/copy-button';
import { Link } from '../base/link';
@@ -59,6 +59,7 @@ export const Plugins: FC = () => {
setState(state => ({ ...state, plugins, isRefreshingPlugins: false }));
}
const patchSettings = useSettingsPatcher();
return (
<div>
@@ -91,9 +92,7 @@ export const Plugins: FC = () => {
onChange={async event => {
const newConfig = { ...plugin.config, disabled: !event.target.checked };
setState(state => ({ ...state, isRefreshingPlugins: true }));
await models.settings.update(settings, {
pluginConfig: { ...settings.pluginConfig, [plugin.name]: newConfig },
});
patchSettings({ pluginConfig: { ...settings.pluginConfig, [plugin.name]: newConfig } });
refreshPlugins();
}}
/>

View File

@@ -9,7 +9,7 @@ import {
newDefaultRegistry,
} from '../../../common/hotkeys';
import { HotKeyRegistry, KeyboardShortcut, KeyCombination } from '../../../common/settings';
import * as models from '../../../models/index';
import { useSettingsPatcher } from '../../hooks/use-request';
import { RootLoaderData } from '../../routes/root';
import { Dropdown, DropdownButton, DropdownItem, DropdownSection, ItemContent } from '../base/dropdown';
import { PromptButton } from '../base/prompt-button';
@@ -27,12 +27,13 @@ export const Shortcuts: FC = () => {
settings,
} = useRouteLoaderData('root') as RootLoaderData;
const { hotKeyRegistry } = settings;
const patchSettings = useSettingsPatcher();
return (
<div className="shortcuts">
<div className="row-spaced margin-bottom-xs">
<div>
<PromptButton className="btn btn--clicky" onClick={() => models.settings.update(settings, { hotKeyRegistry: newDefaultRegistry() })}>
<PromptButton className="btn btn--clicky" onClick={() => patchSettings({ hotKeyRegistry: newDefaultRegistry() })}>
Reset all
</PromptButton>
</div>
@@ -84,7 +85,7 @@ export const Shortcuts: FC = () => {
addKeyCombination: (keyboardShortcut: KeyboardShortcut, keyComb: KeyCombination) => {
const keyCombs = getPlatformKeyCombinations(hotKeyRegistry[keyboardShortcut]);
keyCombs.push(keyComb);
models.settings.update(settings, { hotKeyRegistry });
patchSettings({ hotKeyRegistry });
},
}
)}
@@ -116,7 +117,7 @@ export const Shortcuts: FC = () => {
});
if (toBeRemovedIndex >= 0) {
keyCombosForThisPlatform.splice(toBeRemovedIndex, 1);
models.settings.update(settings, { hotKeyRegistry });
patchSettings({ hotKeyRegistry });
}
}}
/>
@@ -134,7 +135,7 @@ export const Shortcuts: FC = () => {
withPrompt
onClick={() => {
hotKeyRegistry[keyboardShortcut] = newDefaultRegistry()[keyboardShortcut];
models.settings.update(settings, { hotKeyRegistry });
patchSettings({ hotKeyRegistry });
}}
/>
</DropdownItem>

View File

@@ -2,7 +2,7 @@ import React, { ChangeEventHandler, FC, InputHTMLAttributes, useCallback } from
import { useRouteLoaderData } from 'react-router-dom';
import { SettingsOfType } from '../../../common/settings';
import * as models from '../../../models/index';
import { useSettingsPatcher } from '../../hooks/use-request';
import { RootLoaderData } from '../../routes/root';
import { HelpTooltip } from '../help-tooltip';
@@ -26,11 +26,12 @@ export const TextSetting: FC<{
if (!Object.prototype.hasOwnProperty.call(settings, setting)) {
throw new Error(`Invalid setting name ${setting}`);
}
const patchSettings = useSettingsPatcher();
const handleOnChange = useCallback<ChangeEventHandler<HTMLInputElement>>(async ({ currentTarget: { value } }) => {
const updatedValue = value === null ? '__NULL__' : value;
await models.settings.patch({ [setting]: updatedValue });
}, [setting]);
patchSettings({ [setting]: updatedValue });
}, [patchSettings, setting]);
let defaultValue = settings[setting];
if (typeof defaultValue !== 'string') {

View File

@@ -1,24 +1,13 @@
import React, { FC, Fragment } from 'react';
import ReactDOM from 'react-dom';
import { useSelector } from 'react-redux';
import { useParams } from 'react-router-dom';
import { useParams, useRouteLoaderData } from 'react-router-dom';
import { GrpcRequest } from '../../../models/grpc-request';
import { Request } from '../../../models/request';
import { isRequestGroup, RequestGroup } from '../../../models/request-group';
import { WebSocketRequest } from '../../../models/websocket-request';
import { selectSidebarChildren } from '../../redux/selectors';
import { isRequestGroup } from '../../../models/request-group';
import { Child, WorkspaceLoaderData } from '../../routes/workspace';
import { SidebarCreateDropdown } from './sidebar-create-dropdown';
import { SidebarRequestGroupRow } from './sidebar-request-group-row';
import { SidebarRequestRow } from './sidebar-request-row';
export interface Child {
doc: Request | GrpcRequest | WebSocketRequest | RequestGroup;
children: Child[];
collapsed: boolean;
hidden: boolean;
pinned: boolean;
}
export interface SidebarChildObjects {
pinned: Child[];
all: Child[];
@@ -34,10 +23,12 @@ interface Props {
export const SidebarChildren: FC<Props> = ({
filter,
}) => {
const sidebarChildren = useSelector(selectSidebarChildren);
const {
requestTree,
} = useRouteLoaderData(':workspaceId') as WorkspaceLoaderData;
const { all, pinned } = sidebarChildren;
const showSeparator = sidebarChildren.pinned.length > 0;
const pinned = requestTree.filter((child: Child) => child.pinned);
const showSeparator = pinned.length > 0;
const contextMenuPortal = ReactDOM.createPortal(
<div className="hide">
<SidebarCreateDropdown />
@@ -62,7 +53,7 @@ export const SidebarChildren: FC<Props> = ({
<RecursiveSidebarRows
filter={filter}
isInPinnedList={false}
rows={all}
rows={requestTree}
/>
</ul>
{contextMenuPortal}

View File

@@ -5,9 +5,9 @@ import { DragSource, DragSourceSpec, DropTarget, DropTargetMonitor, DropTargetSp
import * as models from '../../../models/index';
import { RequestGroup } from '../../../models/request-group';
import { useRequestGroupMetaPatcher } from '../../hooks/use-request';
import { Highlight } from '../base/highlight';
import { RequestGroupActionsDropdown, RequestGroupActionsDropdownHandle } from '../dropdowns/request-group-actions-dropdown';
import { showModal } from '../modals';
import { RequestGroupSettingsModal } from '../modals/request-group-settings-modal';
import { DnDDragProps, DnDDropProps, DnDProps, DragObject, dropHandleCreator, hoverHandleCreator, sourceCollect, targetCollect } from './dnd';
import { SidebarRequestRow } from './sidebar-request-row';
@@ -37,7 +37,7 @@ export const SidebarRequestGroupRowFC = forwardRef<SidebarRequestGroupRowHandle,
const [dragDirection, setDragDirection] = useState(0);
const dropdownRef = useRef<RequestGroupActionsDropdownHandle>(null);
const expandTagRef = useRef<HTMLDivElement>(null);
const [isRequestGroupSettingsModalOpen, setIsRequestGroupSettingsModalOpen] = useState(false);
useImperativeHandle(ref, () => ({
setDragDirection,
getExpandTag:() => expandTagRef.current,
@@ -51,17 +51,13 @@ export const SidebarRequestGroupRowFC = forwardRef<SidebarRequestGroupRowHandle,
'sidebar__row--dragging-above': isDraggingOver && dragDirection > 0,
'sidebar__row--dragging-below': isDraggingOver && dragDirection < 0,
});
const groupMetaPatcher = useRequestGroupMetaPatcher();
// NOTE: We only want the button draggable, not the whole container (ie. no children)
const button = connectDragSource(
connectDropTarget(
<button
onClick={async () => {
const requestGroupMeta = await models.requestGroupMeta.getByParentId(requestGroup._id);
if (requestGroupMeta) {
models.requestGroupMeta.update(requestGroupMeta, { collapsed: !isCollapsed });
return;
}
models.requestGroupMeta.create({ parentId: requestGroup._id, collapsed: false });
groupMetaPatcher(requestGroup._id, { collapsed: !isCollapsed });
}}
onContextMenu={event => {
event.preventDefault();
@@ -99,9 +95,13 @@ export const SidebarRequestGroupRowFC = forwardRef<SidebarRequestGroupRowHandle,
<div className="sidebar__actions">
<RequestGroupActionsDropdown
ref={dropdownRef}
handleShowSettings={() => showModal(RequestGroupSettingsModal, { requestGroup })}
handleShowSettings={() => setIsRequestGroupSettingsModalOpen(true)}
requestGroup={requestGroup}
/>
{isRequestGroupSettingsModalOpen && <RequestGroupSettingsModal
requestGroup={requestGroup}
onHide={() => setIsRequestGroupSettingsModalOpen(false)}
/>}
</div>
</div>
<ul className={classnames('sidebar__list', { 'sidebar__list--collapsed': isCollapsed })}>

View File

@@ -20,7 +20,7 @@ import { Editable } from '../base/editable';
import { Highlight } from '../base/highlight';
import { RequestActionsDropdown } from '../dropdowns/request-actions-dropdown';
import { WebSocketRequestActionsDropdown } from '../dropdowns/websocket-request-actions-dropdown';
import { showModal, showPrompt } from '../modals/index';
import { showPrompt } from '../modals/index';
import { RequestSettingsModal } from '../modals/request-settings-modal';
import { GrpcTag } from '../tags/grpc-tag';
import { MethodTag } from '../tags/method-tag';
@@ -144,9 +144,7 @@ export const _SidebarRequestRow: FC<Props> = forwardRef(({
});
}, [requestGroup?._id, requestFetcher, organizationId, projectId, workspaceId]);
const handleShowRequestSettings = useCallback(() => {
request && showModal(RequestSettingsModal, { request });
}, [request]);
const [isRequestSettingsModalOpen, setIsRequestSettingsModalOpen] = useState(false);
const [methodOverrideValue, setMethodOverrideValue] = useState<string | null>(null);
@@ -270,27 +268,32 @@ export const _SidebarRequestRow: FC<Props> = forwardRef(({
</div>
</button>
<div className="sidebar__actions">
{isWebSocketRequest(request) ? (
<WebSocketRequestActionsDropdown
{isWebSocketRequest(request) ?
(<WebSocketRequestActionsDropdown
ref={requestActionsDropdown}
handleDuplicateRequest={handleDuplicateRequest}
request={request}
isPinned={isPinned}
handleShowSettings={handleShowRequestSettings}
/>
) : (
<RequestActionsDropdown
handleShowSettings={() => setIsRequestSettingsModalOpen(true)}
/>)
:
(<RequestActionsDropdown
ref={requestActionsDropdown}
handleDuplicateRequest={handleDuplicateRequest}
handleShowSettings={handleShowRequestSettings}
handleShowSettings={() => setIsRequestSettingsModalOpen(true)}
request={request}
isPinned={isPinned}
requestGroup={requestGroup}
activeEnvironment={activeEnvironment}
activeProject={activeProject}
/>
)}
/>)}
</div>
{isRequestSettingsModalOpen && (
<RequestSettingsModal
request={request}
onHide={() => setIsRequestSettingsModalOpen(false)}
/>
)}
{isPinned && (
<div className="sidebar__item__icon-pin">
<i className="fa fa-thumb-tack" />

View File

@@ -1,7 +1,7 @@
import React, { FC } from 'react';
import React, { FC, useState } from 'react';
import { Cookie } from 'tough-cookie';
import { showCookiesModal } from '../modals/cookies-modal';
import { CookiesModal } from '../modals/cookies-modal';
interface Props {
cookiesSent?: boolean | null;
@@ -10,6 +10,7 @@ interface Props {
}
export const ResponseCookiesViewer: FC<Props> = props => {
const [isCookieModalOpen, setIsCookieModalOpen] = useState(false);
const renderRow = (h: any, i: number) => {
let cookie: Cookie | undefined | null = null;
@@ -59,9 +60,14 @@ export const ResponseCookiesViewer: FC<Props> = props => {
<tbody>{!headers.length ? renderRow(null, -1) : headers.map(renderRow)}</tbody>
</table>
<p className="pad-top">
<button className="pull-right btn btn--clicky" onClick={showCookiesModal}>
<button className="pull-right btn btn--clicky" onClick={() => setIsCookieModalOpen(true)}>
Manage Cookies
</button>
</p>
{isCookieModalOpen && (
<CookiesModal
onHide={() => setIsCookieModalOpen(false)}
/>
)}
</div>;
};

View File

@@ -1,10 +1,12 @@
import React, { FC, useCallback, useLayoutEffect, useRef } from 'react';
import React, { FC, useLayoutEffect, useRef } from 'react';
import { useFetcher, useParams } from 'react-router-dom';
import styled from 'styled-components';
import { getRenderContext, render, RENDER_PURPOSE_SEND } from '../../../common/render';
import * as models from '../../../models';
import { WebSocketRequest } from '../../../models/websocket-request';
import { buildQueryStringFromParams, joinUrlAndQueryString } from '../../../utils/url/querystring';
import { ConnectActionParams } from '../../routes/request';
import { OneLineEditor, OneLineEditorHandle } from '../codemirror/one-line-editor';
import { createKeybindingsHandler, useDocBodyKeyboardShortcuts } from '../keydown-binder';
import { showAlert, showModal } from '../modals';
@@ -25,7 +27,6 @@ const Button = styled.button<{ warning?: boolean }>(({ warning }) => ({
interface ActionBarProps {
request: WebSocketRequest;
workspaceId: string;
environmentId: string;
defaultValue: string;
readyState: boolean;
@@ -66,13 +67,24 @@ export const ConnectionCircle = styled.span({
borderRadius: '50%',
});
export const WebSocketActionBar: FC<ActionBarProps> = ({ request, workspaceId, environmentId, defaultValue, onChange, readyState }) => {
export const WebSocketActionBar: FC<ActionBarProps> = ({ request, environmentId, defaultValue, onChange, readyState }) => {
const isOpen = readyState;
const oneLineEditorRef = useRef<OneLineEditorHandle>(null);
useLayoutEffect(() => {
oneLineEditorRef.current?.focusEnd();
}, []);
const handleSubmit = useCallback(async () => {
const fetcher = useFetcher();
const { organizationId, projectId, workspaceId, requestId } = useParams() as { organizationId: string; projectId: string; workspaceId: string; requestId: string };
const connect = (connectParams: ConnectActionParams) => {
fetcher.submit(JSON.stringify(connectParams),
{
action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/debug/request/${requestId}/connect`,
method: 'post',
encType: 'application/json',
});
};
const handleSubmit = async () => {
if (isOpen) {
window.main.webSocket.close({ requestId: request._id });
return;
@@ -88,9 +100,7 @@ export const WebSocketActionBar: FC<ActionBarProps> = ({ request, workspaceId, e
parameters: request.parameters.filter(p => !p.disabled),
workspaceCookieJar,
}, renderContext);
window.main.webSocket.open({
requestId: request._id,
workspaceId,
connect({
url: joinUrlAndQueryString(rendered.url, buildQueryStringFromParams(rendered.parameters)),
headers: rendered.headers,
authentication: rendered.authentication,
@@ -116,7 +126,7 @@ export const WebSocketActionBar: FC<ActionBarProps> = ({ request, workspaceId, e
});
}
}
}, [environmentId, isOpen, request, workspaceId]);
};
useDocBodyKeyboardShortcuts({
request_send: () => handleSubmit(),

View File

@@ -7,7 +7,6 @@ import { PREVIEW_MODE_FRIENDLY, PREVIEW_MODE_RAW, PREVIEW_MODE_SOURCE, PreviewMo
import { CurlEvent, CurlMessageEvent } from '../../../main/network/curl';
import { WebSocketEvent, WebSocketMessageEvent } from '../../../main/network/websocket';
import { requestMeta } from '../../../models';
import { RequestMeta } from '../../../models/request-meta';
import { RequestLoaderData } from '../../routes/request';
import { CodeEditor } from '../codemirror/code-editor';
import { showError } from '../modals';
@@ -94,7 +93,7 @@ export const MessageEventView: FC<Props<CurlMessageEvent | WebSocketMessageEvent
} catch {
// Can't parse as JSON.
}
const { activeRequestMeta } = useRouteLoaderData('request/:requestId') as RequestLoaderData<any, RequestMeta>;
const { activeRequestMeta } = useRouteLoaderData('request/:requestId') as RequestLoaderData;
const previewMode = activeRequestMeta.previewMode || PREVIEW_MODE_SOURCE;
return (
<PreviewPane>

View File

@@ -1,6 +1,6 @@
import fs from 'fs';
import React, { FC, useEffect, useRef, useState } from 'react';
import { useSelector } from 'react-redux';
import { useRouteLoaderData } from 'react-router-dom';
import styled from 'styled-components';
import { getSetCookieHeaders } from '../../../common/misc';
@@ -10,7 +10,7 @@ import { WebSocketEvent } from '../../../main/network/websocket';
import { Response } from '../../../models/response';
import { WebSocketResponse } from '../../../models/websocket-response';
import { useRealtimeConnectionEvents } from '../../hooks/use-realtime-connection-events';
import { selectActiveResponse } from '../../redux/selectors';
import { RequestLoaderData, WebSocketRequestLoaderData } from '../../routes/request';
import { PanelContainer, TabItem, Tabs } from '../base/tabs';
import { ResponseHistoryDropdown } from '../dropdowns/response-history-dropdown';
import { ErrorBoundary } from '../error-boundary';
@@ -85,8 +85,9 @@ const PaddedButton = styled('button')({
});
export const RealtimeResponsePane: FC<{ requestId: string }> = () => {
const response = useSelector(selectActiveResponse) as WebSocketResponse | Response | null;
if (!response) {
const { activeResponse } = useRouteLoaderData('request/:requestId') as RequestLoaderData | WebSocketRequestLoaderData;
if (!activeResponse) {
return (
<Pane type="response">
<PaneHeader />
@@ -94,7 +95,7 @@ export const RealtimeResponsePane: FC<{ requestId: string }> = () => {
</Pane>
);
}
return <RealtimeActiveResponsePane response={response} />;
return <RealtimeActiveResponsePane response={activeResponse} />;
};
const RealtimeActiveResponsePane: FC<{ response: WebSocketResponse | Response }> = ({
@@ -175,7 +176,6 @@ const RealtimeActiveResponsePane: FC<{ response: WebSocketResponse | Response }>
</div>
<ResponseHistoryDropdown
activeResponse={response}
className="tall pane__header__right"
/>
</PaneHeader>
<Tabs aria-label="Curl response pane tabs">

View File

@@ -1,4 +1,4 @@
import React, { FC, useCallback, useEffect, useRef, useState } from 'react';
import React, { FC, useEffect, useRef, useState } from 'react';
import { useParams, useRouteLoaderData } from 'react-router-dom';
import styled from 'styled-components';
@@ -6,13 +6,12 @@ import { AuthType, CONTENT_TYPE_JSON } from '../../../common/constants';
import { getRenderContext, render, RENDER_PURPOSE_SEND } from '../../../common/render';
import * as models from '../../../models';
import { Environment } from '../../../models/environment';
import { RequestMeta } from '../../../models/request-meta';
import { WebSocketRequest } from '../../../models/websocket-request';
import { buildQueryStringFromParams, joinUrlAndQueryString } from '../../../utils/url/querystring';
import { useReadyState } from '../../hooks/use-ready-state';
import { useRequestPatcher } from '../../hooks/use-request';
import { useActiveRequestSyncVCSVersion, useGitVCSVersion } from '../../hooks/use-vcs-version';
import { RequestLoaderData } from '../../routes/request';
import { WebSocketRequestLoaderData } from '../../routes/request';
import { RootLoaderData } from '../../routes/root';
import { TabItem, Tabs } from '../base/tabs';
import { CodeEditor, CodeEditorHandle } from '../codemirror/code-editor';
@@ -202,7 +201,7 @@ interface Props {
// currently this is blocked by the way page layout divide the panes with dragging functionality
// TODO: @gatzjames discuss above assertion in light of request and settings drills
export const WebSocketRequestPane: FC<Props> = ({ environment }) => {
const { activeRequest, activeRequestMeta } = useRouteLoaderData('request/:requestId') as RequestLoaderData<WebSocketRequest, RequestMeta>;
const { activeRequest, activeRequestMeta } = useRouteLoaderData('request/:requestId') as WebSocketRequestLoaderData;
const { workspaceId, requestId } = useParams() as { organizationId: string; projectId: string; workspaceId: string; requestId: string };
const readyState = useReadyState({ requestId: activeRequest._id, protocol: 'webSocket' });
@@ -247,14 +246,7 @@ export const WebSocketRequestPane: FC<Props> = ({ environment }) => {
});
}
};
const handleEditDescription = useCallback(() => {
showModal(RequestSettingsModal, { request: activeRequest });
}, [activeRequest]);
const handleEditDescriptionAdd = useCallback(() => {
showModal(RequestSettingsModal, { request: activeRequest, forceEditMode: true });
}, [activeRequest]);
const [isRequestSettingsModalOpen, setIsRequestSettingsModalOpen] = useState(false);
const gitVersion = useGitVCSVersion();
const activeRequestSyncVersion = useActiveRequestSyncVCSVersion();
@@ -268,7 +260,6 @@ export const WebSocketRequestPane: FC<Props> = ({ environment }) => {
<WebSocketActionBar
key={uniqueKey}
request={activeRequest}
workspaceId={workspaceId}
environmentId={environment?._id || ''}
defaultValue={activeRequest.url}
readyState={readyState}
@@ -360,7 +351,7 @@ export const WebSocketRequestPane: FC<Props> = ({ environment }) => {
{activeRequest.description ? (
<div>
<div className="pull-right pad bg-default">
<button className="btn btn--clicky" onClick={handleEditDescription}>
<button className="btn btn--clicky" onClick={() => setIsRequestSettingsModalOpen(true)}>
Edit
</button>
</div>
@@ -387,10 +378,7 @@ export const WebSocketRequestPane: FC<Props> = ({ environment }) => {
</span>
<br />
<br />
<button
className="btn btn--clicky faint"
onClick={handleEditDescriptionAdd}
>
<button className="btn btn--clicky faint" onClick={() => setIsRequestSettingsModalOpen(true)}>
Add Description
</button>
</p>
@@ -398,6 +386,12 @@ export const WebSocketRequestPane: FC<Props> = ({ environment }) => {
)}
</TabItem>
</Tabs>
{isRequestSettingsModalOpen && (
<RequestSettingsModal
request={activeRequest}
onHide={() => setIsRequestSettingsModalOpen(false)}
/>
)}
</Pane>
);
};

View File

@@ -2,7 +2,6 @@ import { useCallback } from 'react';
import { useRouteLoaderData } from 'react-router-dom';
import { getRenderContext, getRenderContextAncestors, HandleGetRenderContext, HandleRender, render } from '../../../common/render';
import { Request } from '../../../models/request';
import { NUNJUCKS_TEMPLATE_GLOBAL_PROPERTY_NAME } from '../../../templating';
import { getKeys } from '../../../templating/utils';
import { RequestLoaderData } from '../../routes/request';
@@ -19,7 +18,7 @@ initializeNunjucksRenderPromiseCache();
* Access to functions useful for Nunjucks rendering
*/
export const useNunjucks = () => {
const requestData = useRouteLoaderData('request/:requestId') as RequestLoaderData<Request, any> | undefined;
const requestData = useRouteLoaderData('request/:requestId') as RequestLoaderData | undefined;
const workspaceData = useRouteLoaderData(':workspaceId') as WorkspaceLoaderData;
const fetchRenderContext = useCallback(async () => {

View File

@@ -2,11 +2,11 @@ import { ChangeEvent, useCallback, useState } from 'react';
import { useRouteLoaderData } from 'react-router-dom';
import { useAsync } from 'react-use';
import * as models from '../../models';
import { ThemeSettings } from '../../models/settings';
import { ColorScheme, getThemes } from '../../plugins';
import { applyColorScheme, PluginTheme } from '../../plugins/misc';
import { RootLoaderData } from '../routes/root';
import { useSettingsPatcher } from './use-request';
export const useThemes = () => {
const {
@@ -38,6 +38,7 @@ export const useThemes = () => {
}
return pluginTheme.name === theme;
}, [autoDetectColorScheme, isActiveDark, isActiveLight, theme]);
const patchSettings = useSettingsPatcher();
// Apply the theme and update settings
const apply = useCallback(async (patch: Partial<ThemeSettings>) => {
@@ -48,8 +49,9 @@ export const useThemes = () => {
lightTheme,
...patch,
});
await models.settings.patch(patch);
}, [autoDetectColorScheme, darkTheme, lightTheme, theme]);
patchSettings(patch);
}, [autoDetectColorScheme, darkTheme, lightTheme, patchSettings, theme]);
const changeAutoDetect = useCallback(({ target: { checked } }: ChangeEvent<HTMLInputElement>) => apply({ autoDetectColorScheme: checked }), [apply]);

View File

@@ -2,7 +2,6 @@ import { useEffect } from 'react';
import { useRouteLoaderData } from 'react-router-dom';
import { getProductName } from '../../common/constants';
import { Request } from '../../models/request';
import { RequestLoaderData } from '../routes/request';
import { WorkspaceLoaderData } from '../routes/workspace';
export const useDocumentTitle = () => {
@@ -12,7 +11,7 @@ export const useDocumentTitle = () => {
activeProject,
} = useRouteLoaderData(':workspaceId') as WorkspaceLoaderData;
const { activeRequest } = useRouteLoaderData('request/:requestId') as RequestLoaderData<Request, any>;
const { activeRequest } = useRouteLoaderData('request/:requestId') as RequestLoaderData;
// Update document title
useEffect(() => {

View File

@@ -7,17 +7,20 @@ import { showModal } from '../components/modals';
import { SettingsModal, TAB_INDEX_SHORTCUTS } from '../components/modals/settings-modal';
import { RootLoaderData } from '../routes/root';
import { WorkspaceLoaderData } from '../routes/workspace';
import { useSettingsPatcher } from './use-request';
export const useGlobalKeyboardShortcuts = () => {
const workspaceData = useRouteLoaderData(':workspaceId') as WorkspaceLoaderData | undefined;
const {
settings,
} = useRouteLoaderData('root') as RootLoaderData;
const { activeWorkspaceMeta } = workspaceData || {};
const patchSettings = useSettingsPatcher();
useDocBodyKeyboardShortcuts({
plugin_reload:
() => plugins.reloadPlugins(),
environment_showVariableSourceAndValue:
() => models.settings.update(settings, { showVariableSourceAndValue: !settings.showVariableSourceAndValue }),
() => patchSettings({ showVariableSourceAndValue: !settings.showVariableSourceAndValue }),
preferences_showGeneral:
() => showModal(SettingsModal),
preferences_showKeyboardShortcuts:

View File

@@ -4,14 +4,17 @@ import { useFetcher, useParams } from 'react-router-dom';
import { GrpcRequest } from '../../models/grpc-request';
import { GrpcRequestMeta } from '../../models/grpc-request-meta';
import { Request } from '../../models/request';
import { RequestGroup } from '../../models/request-group';
import { RequestGroupMeta } from '../../models/request-group-meta';
import { RequestMeta } from '../../models/request-meta';
import { Settings } from '../../models/settings';
import { WebSocketRequest } from '../../models/websocket-request';
export const useRequestPatcher = () => {
const { organizationId, projectId, workspaceId } = useParams<{ organizationId: string; projectId: string; workspaceId: string }>();
const requestMetaFetcher = useFetcher();
const fetcher = useFetcher();
return (requestId: string, patch: Partial<GrpcRequest> | Partial<Request> | Partial<WebSocketRequest>) => {
requestMetaFetcher.submit(JSON.stringify(patch), {
fetcher.submit(JSON.stringify(patch), {
action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/debug/request/${requestId}/update`,
method: 'post',
encType: 'application/json',
@@ -21,9 +24,9 @@ export const useRequestPatcher = () => {
export const useRequestMetaPatcher = () => {
const { organizationId, projectId, workspaceId } = useParams<{ organizationId: string; projectId: string; workspaceId: string }>();
const requestMetaFetcher = useFetcher();
const fetcher = useFetcher();
return (requestId: string, patch: Partial<GrpcRequestMeta> | Partial<RequestMeta>) => {
requestMetaFetcher.submit(patch, {
fetcher.submit(patch, {
action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/debug/request/${requestId}/update-meta`,
method: 'post',
encType: 'application/json',
@@ -31,4 +34,39 @@ export const useRequestMetaPatcher = () => {
};
};
export const useRequestGroupPatcher = () => {
const { organizationId, projectId, workspaceId } = useParams<{ organizationId: string; projectId: string; workspaceId: string }>();
const fetcher = useFetcher();
return (requestGroupId: string, patch: Partial<RequestGroup>) => {
fetcher.submit(JSON.stringify(patch), {
action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/debug/request-group/${requestGroupId}/update`,
method: 'post',
encType: 'application/json',
});
};
};
export const useRequestGroupMetaPatcher = () => {
const { organizationId, projectId, workspaceId } = useParams<{ organizationId: string; projectId: string; workspaceId: string }>();
const fetcher = useFetcher();
return (requestGroupId: string, patch: Partial<RequestGroupMeta>) => {
fetcher.submit(patch, {
action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/debug/request-group/${requestGroupId}/update-meta`,
method: 'post',
encType: 'application/json',
});
};
};
export const useSettingsPatcher = () => {
const fetcher = useFetcher();
return (patch: Partial<Settings>) => {
fetcher.submit(JSON.stringify(patch), {
action: '/settings/update',
method: 'post',
encType: 'application/json',
});
};
};
export type CreateRequestType = 'HTTP' | 'gRPC' | 'GraphQL' | 'WebSocket' | 'Event Stream';

View File

@@ -79,6 +79,10 @@ const router = createMemoryRouter(
},
],
},
{
path: 'settings/update',
action: async (...args) => (await import('./routes/actions')).updateSettingsAction(...args),
},
{
path: 'organization',
children: [
@@ -139,6 +143,7 @@ const router = createMemoryRouter(
children: [
{
path: `${ACTIVITY_DEBUG}`,
loader: async (...args) => (await import('./routes/debug')).loader(...args),
element: (
<Suspense fallback={<AppLoadingIndicator />}>
<Debug />
@@ -150,6 +155,10 @@ const router = createMemoryRouter(
id: 'request/:requestId',
loader: async (...args) => (await import('./routes/request')).loader(...args),
children: [
{
path: 'connect',
action: async (...args) => (await import('./routes/request')).connectAction(...args),
},
{
path: 'duplicate',
action: async (...args) => (await import('./routes/request')).duplicateRequestAction(...args),
@@ -162,6 +171,14 @@ const router = createMemoryRouter(
path: 'update-meta',
action: async (...args) => (await import('./routes/request')).updateRequestMetaAction(...args),
},
{
path: 'response/delete-all',
action: async (...args) => (await import('./routes/request')).deleteAllResponsesAction(...args),
},
{
path: 'response/delete',
action: async (...args) => (await import('./routes/request')).deleteResponseAction(...args),
},
],
},
{
@@ -184,6 +201,10 @@ const router = createMemoryRouter(
path: 'request-group/update',
action: async (...args) => (await import('./routes/request-group')).updateRequestGroupAction(...args),
},
{
path: 'request-group/:requestGroupId/update-meta',
action: async (...args) => (await import('./routes/request-group')).updateRequestGroupMetaAction(...args),
},
],
},
{

View File

@@ -1,18 +1,10 @@
import { createSelector } from 'reselect';
import type { ValueOf } from 'type-fest';
import { fuzzyMatchAll } from '../../common/misc';
import * as models from '../../models';
import { BaseModel } from '../../models';
import { GrpcRequest, isGrpcRequest } from '../../models/grpc-request';
import { getStatusCandidates } from '../../models/helpers/get-status-candidates';
import { sortProjects } from '../../models/helpers/project';
import { DEFAULT_PROJECT_ID, isRemoteProject } from '../../models/project';
import { isRequest, Request } from '../../models/request';
import { isRequestGroup, RequestGroup } from '../../models/request-group';
import { type Response } from '../../models/response';
import { isWebSocketRequest, WebSocketRequest } from '../../models/websocket-request';
import { type WebSocketResponse } from '../../models/websocket-response';
import { BaseModel, canSync } from '../../models';
import { DEFAULT_PROJECT_ID } from '../../models/project';
import { isRequest } from '../../models/request';
import { StatusCandidate } from '../../sync/types';
import { RootState } from './modules';
type EntitiesLists = {
@@ -31,28 +23,14 @@ const selectEntitiesLists = createSelector(
},
);
export const selectRequestMetas = createSelector(
selectEntitiesLists,
entities => entities.requestMetas,
);
export const selectGrpcRequestMetas = createSelector(
selectEntitiesLists,
entities => entities.grpcRequestMetas,
);
export const selectRemoteProjects = createSelector(
selectEntitiesLists,
entities => sortProjects(entities.projects).filter(isRemoteProject),
);
// list workspaces for move/copy switcher, and export
export const selectWorkspacesForActiveProject = createSelector(
selectEntitiesLists,
(state: RootState) => state.global.activeProjectId,
(entities, activeProjectId) => entities.workspaces.filter(workspace => workspace.parentId === (activeProjectId || DEFAULT_PROJECT_ID)),
);
export const selectActiveWorkspace = createSelector(
const selectActiveWorkspace = createSelector(
selectEntitiesLists,
(state: RootState) => state.global.activeWorkspaceId,
(state: RootState) => state.global.activeProjectId,
@@ -62,11 +40,6 @@ export const selectActiveWorkspace = createSelector(
},
);
export const selectRequestVersions = createSelector(
selectEntitiesLists,
entities => entities.requestVersions,
);
const selectEntitiesChildrenMap = createSelector(selectEntitiesLists, entities => {
const parentLookupMap: any = {};
// group entities by parent
@@ -114,235 +87,12 @@ const selectActiveWorkspaceEntities = createSelector(
},
);
export const selectWorkspaceRequestsAndRequestGroups = createSelector(
selectActiveWorkspaceEntities,
entities => {
return entities.filter(
entity => isRequest(entity) || isWebSocketRequest(entity) || isGrpcRequest(entity) || isRequestGroup(entity),
) as (Request | WebSocketRequest | GrpcRequest | RequestGroup)[];
},
);
const selectActiveWorkspaceMeta = createSelector(
selectActiveWorkspace,
selectEntitiesLists,
(activeWorkspace, entities) => {
return entities.workspaceMetas.find(workspaceMeta => workspaceMeta.parentId === activeWorkspace?._id);
},
);
export const selectActiveRequest = createSelector(
(state: RootState) => state.entities,
selectActiveWorkspaceMeta,
(entities, workspaceMeta) => {
const id = workspaceMeta?.activeRequestId || 'n/a';
if (id in entities.requests) {
return entities.requests[id];
}
if (id in entities.grpcRequests) {
return entities.grpcRequests[id];
}
if (id in entities.webSocketRequests) {
return entities.webSocketRequests[id];
}
return null;
},
);
export const selectActiveRequestResponses = createSelector(
selectActiveRequest,
selectEntitiesLists,
selectActiveWorkspace,
(activeRequest, entities, activeWorkspace) => {
const requestId = activeRequest ? activeRequest._id : 'n/a';
const responses: (Response | WebSocketResponse)[] = (activeRequest && isWebSocketRequest(activeRequest)) ? entities.webSocketResponses : entities.responses;
// Filter responses down if the setting is enabled
return responses.filter(response => {
const requestMatches = requestId === response.parentId;
if ((entities.settings[0] || models.settings.init()).filterResponsesByEnv) {
const meta = entities.workspaceMetas.find(workspaceMeta => workspaceMeta.parentId === activeWorkspace?._id);
const activeEnvironment = meta ? entities.environments.find(environment => environment._id === meta.activeEnvironmentId) : null;
const activeEnvironmentId = activeEnvironment ? activeEnvironment._id : null;
const environmentMatches = response.environmentId === activeEnvironmentId;
return requestMatches && environmentMatches;
} else {
return requestMatches;
}
})
.sort((a, b) => (a.created > b.created ? -1 : 1));
},
);
const selectActiveRequestMeta = createSelector(
selectActiveRequest,
selectEntitiesLists,
(activeRequest, entities) => {
const id = activeRequest?._id || 'n/a';
return entities.requestMetas.find(m => m.parentId === id);
},
);
export const selectActiveResponse = createSelector(
selectActiveRequestMeta,
selectActiveRequestResponses,
(activeRequestMeta, responses) => {
const activeResponseId = activeRequestMeta ? activeRequestMeta.activeResponseId : 'n/a';
const activeResponse = responses.find(response => response._id === activeResponseId);
if (activeResponse) {
return activeResponse;
}
return responses[0] || null;
},
);
// sync dropdown, branches, history and staging
export const selectSyncItems = createSelector(
selectActiveWorkspaceEntities,
getStatusCandidates,
);
type SidebarModel = Request | GrpcRequest | RequestGroup;
const shouldShowInSidebar = (model: BaseModel): boolean =>
isRequest(model) || isWebSocketRequest(model) || isGrpcRequest(model) || isRequestGroup(model);
const shouldIgnoreChildrenOf = (model: SidebarModel): boolean =>
isRequest(model) || isWebSocketRequest(model) || isGrpcRequest(model);
const sortByMetaKeyOrId = (a: SidebarModel, b: SidebarModel): number => {
if (a.metaSortKey === b.metaSortKey) {
return a._id > b._id ? -1 : 1; // ascending
} else {
return a.metaSortKey < b.metaSortKey ? -1 : 1; // descending
}
};
interface Child {
doc: SidebarModel;
hidden: boolean;
collapsed: boolean;
pinned: boolean;
children: Child[];
}
export interface SidebarChildren {
all: Child[];
pinned: Child[];
}
const selectSidebarFilter = createSelector(
selectActiveWorkspaceMeta,
activeWorkspaceMeta => activeWorkspaceMeta ? activeWorkspaceMeta.sidebarFilter : '',
);
const selectPinnedRequests = createSelector(selectEntitiesLists, entities => {
const pinned: Record<string, boolean> = {};
const requests = [...entities.requests, ...entities.grpcRequests, ...entities.webSocketRequests];
const requestMetas = [...entities.requestMetas, ...entities.grpcRequestMetas];
// Default all to unpinned
for (const request of requests) {
pinned[request._id] = false;
}
// Update those that have metadata (not all do)
for (const meta of requestMetas) {
pinned[meta.parentId] = meta.pinned;
}
return pinned;
});
const selectCollapsedRequestGroups = createSelector(
selectEntitiesLists,
entities => {
const collapsed: Record<string, boolean> = {};
// Default all to collapsed
for (const requestGroup of entities.requestGroups) {
collapsed[requestGroup._id] = true;
}
// Update those that have metadata (not all do)
for (const meta of entities.requestGroupMetas) {
collapsed[meta.parentId] = meta.collapsed;
}
return collapsed;
});
export const selectSidebarChildren = createSelector(
selectCollapsedRequestGroups,
selectPinnedRequests,
selectActiveWorkspace,
selectEntitiesChildrenMap,
selectSidebarFilter,
(collapsed, pinned, activeWorkspace, childrenMap, sidebarFilter): SidebarChildren => {
if (!activeWorkspace) {
return { all: [], pinned: [] };
}
function next(parentId: string, pinnedChildren: Child[]) {
const children: SidebarModel[] = (childrenMap[parentId] || [])
.filter(shouldShowInSidebar)
.sort(sortByMetaKeyOrId);
if (children.length > 0) {
return children.map(c => {
const child: Child = {
doc: c,
hidden: false,
collapsed: !!collapsed[c._id],
pinned: !!pinned[c._id],
children: [],
};
if (child.pinned) {
pinnedChildren.push(child);
}
// Don't add children of requests
child.children = shouldIgnoreChildrenOf(c) ? [] : next(c._id, pinnedChildren);
return child;
});
} else {
return [];
}
}
function matchChildren(children: Child[], parentNames: string[] = []) {
// Bail early if no filter defined
if (!sidebarFilter) {
return children;
}
for (const child of children) {
// Gather all parents so we can match them too
matchChildren(child.children, [...parentNames, child.doc.name]);
const hasMatchedChildren = child.children.find(c => c.hidden === false);
// Try to match request attributes
const name = child.doc.name;
const method = isGrpcRequest(child.doc) ? 'gRPC' : isRequest(child.doc) ? child.doc.method : '';
const match = fuzzyMatchAll(sidebarFilter, [name, method, ...parentNames], {
splitSpace: true,
});
// Update hidden state depending on whether it matched
const matched = hasMatchedChildren || match;
child.hidden = !matched;
}
return children;
}
const pinnedChildren: Child[] = [];
const childrenTree = next(activeWorkspace._id, pinnedChildren);
const matchedChildren = matchChildren(childrenTree);
return {
pinned: pinnedChildren,
all: matchedChildren,
};
},
(docs: BaseModel[]) => docs.filter(canSync).map((doc: BaseModel): StatusCandidate => ({
key: doc._id,
name: doc.name || '',
document: doc,
})),
);

View File

@@ -937,3 +937,9 @@ export const deleteClientCertificateAction: ActionFunction = async ({ request })
await models.clientCertificate.remove(certificateId);
return null;
};
export const updateSettingsAction: ActionFunction = async ({ request }) => {
const patch = await request.json();
await models.settings.patch(patch);
return null;
};

View File

@@ -1,16 +1,16 @@
import { ServiceError, StatusObject } from '@grpc/grpc-js';
import React, { FC, Fragment, useEffect, useState } from 'react';
import { useFetcher, useParams, useRouteLoaderData } from 'react-router-dom';
import { LoaderFunction, redirect, useFetcher, useParams, useRouteLoaderData } from 'react-router-dom';
import { ChangeBufferEvent, database as db } from '../../common/database';
import { generateId } from '../../common/misc';
import type { GrpcMethodInfo } from '../../main/ipc/grpc';
import * as models from '../../models';
import { GrpcRequest, isGrpcRequest, isGrpcRequestId } from '../../models/grpc-request';
import { isGrpcRequest, isGrpcRequestId } from '../../models/grpc-request';
import { getByParentId as getGrpcRequestMetaByParentId } from '../../models/grpc-request-meta';
import { isEventStreamRequest, isRequest, isRequestId, Request } from '../../models/request';
import { isEventStreamRequest, isRequest, isRequestId } from '../../models/request';
import { getByParentId as getRequestMetaByParentId } from '../../models/request-meta';
import { isWebSocketRequest, isWebSocketRequestId, WebSocketRequest } from '../../models/websocket-request';
import { isWebSocketRequest, isWebSocketRequestId } from '../../models/websocket-request';
import { invariant } from '../../utils/invariant';
import { EnvironmentsDropdown } from '../components/dropdowns/environments-dropdown';
import { WorkspaceSyncDropdown } from '../components/dropdowns/workspace-sync-dropdown';
@@ -18,11 +18,10 @@ import { ErrorBoundary } from '../components/error-boundary';
import { useDocBodyKeyboardShortcuts } from '../components/keydown-binder';
import { showModal, showPrompt } from '../components/modals';
import { AskModal } from '../components/modals/ask-modal';
import { CookiesModal, showCookiesModal } from '../components/modals/cookies-modal';
import { CookiesModal } from '../components/modals/cookies-modal';
import { GenerateCodeModal } from '../components/modals/generate-code-modal';
import { PromptModal } from '../components/modals/prompt-modal';
import { RequestSettingsModal } from '../components/modals/request-settings-modal';
import { RequestSwitcherModal } from '../components/modals/request-switcher-modal';
import { WorkspaceEnvironmentsEditModal } from '../components/modals/workspace-environments-edit-modal';
import { GrpcRequestPane } from '../components/panes/grpc-request-pane';
import { GrpcResponsePane } from '../components/panes/grpc-response-pane';
@@ -35,7 +34,7 @@ import { SidebarLayout } from '../components/sidebar-layout';
import { RealtimeResponsePane } from '../components/websockets/realtime-response-pane';
import { WebSocketRequestPane } from '../components/websockets/websocket-request-pane';
import { useRequestMetaPatcher } from '../hooks/use-request';
import { RequestLoaderData } from './request';
import { GrpcRequestLoaderData, RequestLoaderData, WebSocketRequestLoaderData } from './request';
import { RootLoaderData } from './root';
import { WorkspaceLoaderData } from './workspace';
export interface GrpcMessage {
@@ -62,18 +61,38 @@ const INITIAL_GRPC_REQUEST_STATE = {
error: undefined,
methods: [],
};
export const loader: LoaderFunction = async ({ params }) => {
if (!params.requestId) {
const { projectId, workspaceId, organizationId } = params;
invariant(workspaceId, 'Workspace ID is required');
invariant(projectId, 'Project ID is required');
const activeWorkspace = await models.workspace.getById(workspaceId);
invariant(activeWorkspace, 'Workspace not found');
const activeWorkspaceMeta = await models.workspaceMeta.getOrCreateByParentId(workspaceId);
invariant(activeWorkspaceMeta, 'Workspace meta not found');
const activeRequestId = activeWorkspaceMeta.activeRequestId;
if (activeRequestId) {
return redirect(`/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/debug/request/${activeRequestId}`);
}
}
return null;
};
export const Debug: FC = () => {
const {
activeWorkspace,
activeWorkspaceMeta,
activeEnvironment,
grpcRequests,
} = useRouteLoaderData(':workspaceId') as WorkspaceLoaderData;
const requestData = useRouteLoaderData('request/:requestId') as RequestLoaderData<Request | GrpcRequest | WebSocketRequest, any> | undefined;
const requestData = useRouteLoaderData('request/:requestId') as RequestLoaderData | GrpcRequestLoaderData | WebSocketRequestLoaderData | undefined;
const { activeRequest } = requestData || {};
const requestFetcher = useFetcher();
const { organizationId, projectId, workspaceId, requestId } = useParams() as { organizationId: string; projectId: string; workspaceId: string; requestId: string };
const [grpcStates, setGrpcStates] = useState<GrpcRequestState[]>([]);
const [grpcStates, setGrpcStates] = useState<GrpcRequestState[]>(grpcRequests.map(r => ({ requestId: r._id, ...INITIAL_GRPC_REQUEST_STATE })));
const [isCookieModalOpen, setIsCookieModalOpen] = useState(false);
const [isRequestSettingsModalOpen, setIsRequestSettingsModalOpen] = useState(false);
const [isEnvironmentModalOpen, setEnvironmentModalOpen] = useState(false);
const patchRequestMeta = useRequestMetaPatcher();
useEffect(() => {
db.onChange(async (changes: ChangeBufferEvent[]) => {
@@ -85,15 +104,7 @@ export const Debug: FC = () => {
}
});
}, []);
useEffect(() => {
const fn = async () => {
const workspace = await models.workspace.getById(workspaceId);
const children = await db.withDescendants(workspace);
const grpcRequests = children.filter(d => isGrpcRequest(d));
setGrpcStates(grpcRequests.map(r => ({ requestId: r._id, ...INITIAL_GRPC_REQUEST_STATE })));
};
fn();
}, [workspaceId]);
const {
settings,
} = useRouteLoaderData('root') as RootLoaderData;
@@ -133,6 +144,7 @@ export const Debug: FC = () => {
useEffect(() => window.main.on('grpc.status', (_, id, status) => {
setGrpcStates(state => state.map(s => s.requestId === id ? { ...s, status } : s));
}), []);
useDocBodyKeyboardShortcuts({
request_togglePin:
async () => {
@@ -144,7 +156,7 @@ export const Debug: FC = () => {
request_showSettings:
() => {
if (activeRequest) {
showModal(RequestSettingsModal, { request: activeRequest });
setIsRequestSettingsModalOpen(true);
}
},
request_showDelete:
@@ -211,23 +223,14 @@ export const Debug: FC = () => {
}),
});
},
// TODO: fix these
request_showRecent:
() => 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,
}),
() => { },
request_quickSwitch:
() => showModal(RequestSwitcherModal),
() => { },
environment_showEditor:
() => showModal(WorkspaceEnvironmentsEditModal),
showCookiesEditor:
() => showModal(CookiesModal),
() => setEnvironmentModalOpen(true),
showCookiesEditor: () => setIsCookieModalOpen(true),
request_showGenerateCodeEditor:
() => {
if (activeRequest && isRequest(activeRequest)) {
@@ -250,13 +253,22 @@ export const Debug: FC = () => {
<EnvironmentsDropdown
activeEnvironment={activeEnvironment}
workspaceId={workspaceId}
setEnvironmentModalOpen={setEnvironmentModalOpen}
/>
<button className="btn btn--super-compact" onClick={showCookiesModal}>
<button className="btn btn--super-compact" onClick={() => setIsCookieModalOpen(true)}>
<div className="sidebar__menu__thing">
<span>Cookies</span>
</div>
</button>
</div>
{isEnvironmentModalOpen && (
<WorkspaceEnvironmentsEditModal onHide={() => setEnvironmentModalOpen(false)} />)
}
{isCookieModalOpen && (
<CookiesModal
onHide={() => setIsCookieModalOpen(false)}
/>
)}
<SidebarFilter
key={`${workspaceId}::filter`}
@@ -284,6 +296,12 @@ export const Debug: FC = () => {
setLoading={setLoading}
/>)}
{!requestId && <PlaceholderRequestPane />}
{isRequestSettingsModalOpen && activeRequest && (
<RequestSettingsModal
request={activeRequest}
onHide={() => setIsRequestSettingsModalOpen(false)}
/>
)}
</ErrorBoundary>
: null}
renderPaneTwo={

View File

@@ -7,37 +7,28 @@ import { AddKeyCombinationModal } from '../components/modals/add-key-combination
import { AlertModal } from '../components/modals/alert-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 { CookiesModal } 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 { LoginModal } from '../components/modals/login-modal';
import { NunjucksModal } from '../components/modals/nunjucks-modal';
import { PromptModal } from '../components/modals/prompt-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 { WorkspaceEnvironmentsEditModal } from '../components/modals/workspace-environments-edit-modal';
import { WrapperModal } from '../components/modals/wrapper-modal';
import { useVCS } from '../hooks/use-vcs';
import { WorkspaceLoaderData } from './workspace';
const Modals: FC = () => {
const workspaceData = useRouteLoaderData(':workspaceId') as WorkspaceLoaderData | undefined;
const { activeWorkspace, activeEnvironment, activeCookieJar } = workspaceData || {};
const { activeWorkspace, activeEnvironment } = workspaceData || {};
const vcs = useVCS({
workspaceId: activeWorkspace?._id,
});
@@ -71,38 +62,13 @@ const Modals: FC = () => {
<CodePromptModal
ref={instance => registerModal(instance, 'CodePromptModal')}
/>
<RequestSettingsModal
ref={instance => registerModal(instance, 'RequestSettingsModal')}
/>
<RequestGroupSettingsModal
ref={instance =>
registerModal(instance, 'RequestGroupSettingsModal')
}
/>
{activeWorkspace ? (
<>
{/* TODO: Figure out why cookieJar is sometimes null */}
{activeCookieJar ? (
<>
<CookiesModal
ref={instance => registerModal(instance, 'CookiesModal')}
/>
<CookieModifyModal
ref={instance =>
registerModal(instance, 'CookieModifyModal')
}
/>
</>
) : null}
<NunjucksModal
ref={instance => registerModal(instance, 'NunjucksModal')}
workspace={activeWorkspace}
/>
<RequestSwitcherModal
ref={instance => registerModal(instance, 'RequestSwitcherModal')}
/>
</>
) : null}
@@ -131,14 +97,6 @@ const Modals: FC = () => {
<SyncMergeModal
ref={instance => registerModal(instance, 'SyncMergeModal')}
/>
<SyncBranchesModal
ref={instance => registerModal(instance, 'SyncBranchesModal')}
vcs={vcs}
/>
<SyncDeleteModal
ref={instance => registerModal(instance, 'SyncDeleteModal')}
vcs={vcs}
/>
<SyncHistoryModal
ref={instance => registerModal(instance, 'SyncHistoryModal')}
vcs={vcs}
@@ -146,18 +104,9 @@ const Modals: FC = () => {
</Fragment>
) : null}
<WorkspaceEnvironmentsEditModal
ref={instance =>
registerModal(instance, 'WorkspaceEnvironmentsEditModal')
}
/>
<AddKeyCombinationModal
ref={instance => registerModal(instance, 'AddKeyCombinationModal')}
/>
<ExportRequestsModal
ref={instance => registerModal(instance, 'ExportRequestsModal')}
/>
</ErrorBoundary>
</div>

View File

@@ -2,6 +2,7 @@ import { ActionFunction } from 'react-router-dom';
import * as models from '../../models';
import { RequestGroup } from '../../models/request-group';
import { RequestGroupMeta } from '../../models/request-group-meta';
import { invariant } from '../../utils/invariant';
export const createRequestGroupAction: ActionFunction = async ({ request, params }) => {
@@ -18,9 +19,7 @@ export const updateRequestGroupAction: ActionFunction = async ({ request }) => {
invariant(typeof patch._id === 'string', 'Request Group ID is required');
const reqGroup = await models.requestGroup.getById(patch._id);
invariant(reqGroup, 'Request Group not found');
if (name !== null) {
models.requestGroup.update(reqGroup, patch);
}
models.requestGroup.update(reqGroup, patch);
return null;
};
export const deleteRequestGroupAction: ActionFunction = async ({ request }) => {
@@ -32,3 +31,16 @@ export const deleteRequestGroupAction: ActionFunction = async ({ request }) => {
models.requestGroup.remove(requestGroup);
return null;
};
export const updateRequestGroupMetaAction: ActionFunction = async ({ request, params }) => {
const { requestGroupId } = params;
invariant(typeof requestGroupId === 'string', 'Request Group ID is required');
const patch = await request.json() as Partial<RequestGroupMeta>;
const requestGroupMeta = await models.requestGroupMeta.getByParentId(requestGroupId);
if (requestGroupMeta) {
models.requestGroupMeta.update(requestGroupMeta, patch);
return null;
}
models.requestGroupMeta.create({ parentId: requestGroupId, collapsed: false });
return null;
};

View File

@@ -1,22 +1,46 @@
import { ActionFunction, LoaderFunction, redirect } from 'react-router-dom';
import { CONTENT_TYPE_EVENT_STREAM, CONTENT_TYPE_GRAPHQL, CONTENT_TYPE_JSON, METHOD_GET, METHOD_POST } from '../../common/constants';
import { delay } from '../../common/misc';
import * as models from '../../models';
import { BaseModel } from '../../models';
import { CookieJar } from '../../models/cookie-jar';
import { GrpcRequest, isGrpcRequestId } from '../../models/grpc-request';
import { GrpcRequestMeta } from '../../models/grpc-request-meta';
import * as requestOperations from '../../models/helpers/request-operations';
import { isRequest, Request } from '../../models/request';
import { isEventStreamRequest, isRequest, Request, RequestAuthentication, RequestHeader } from '../../models/request';
import { RequestMeta } from '../../models/request-meta';
import { WebSocketRequest } from '../../models/websocket-request';
import { RequestVersion } from '../../models/request-version';
import { Response } from '../../models/response';
import { isWebSocketRequestId, WebSocketRequest } from '../../models/websocket-request';
import { WebSocketResponse } from '../../models/websocket-response';
import { invariant } from '../../utils/invariant';
import { SegmentEvent } from '../analytics';
import { updateMimeType } from '../components/dropdowns/content-type-dropdown';
export interface RequestLoaderData<A, B> {
activeRequest: A;
activeRequestMeta: B;
export interface WebSocketRequestLoaderData {
activeRequest: WebSocketRequest;
activeRequestMeta: RequestMeta;
activeResponse: WebSocketResponse | null;
responses: WebSocketResponse[];
requestVersions: RequestVersion[];
}
export const loader: LoaderFunction = async ({ params }): Promise<RequestLoaderData<Request | WebSocketRequest | GrpcRequest, RequestMeta | GrpcRequestMeta>> => {
export interface GrpcRequestLoaderData {
activeRequest: GrpcRequest;
activeRequestMeta: GrpcRequestMeta;
activeResponse: null;
responses: [];
requestVersions: RequestVersion[];
}
export interface RequestLoaderData {
activeRequest: Request;
activeRequestMeta: RequestMeta;
activeResponse: Response | null;
responses: Response[];
requestVersions: RequestVersion[];
}
export const loader: LoaderFunction = async ({ params }): Promise<RequestLoaderData | WebSocketRequestLoaderData | GrpcRequestLoaderData> => {
const { requestId, workspaceId } = params;
invariant(requestId, 'Request ID is required');
invariant(workspaceId, 'Workspace ID is required');
@@ -25,18 +49,36 @@ export const loader: LoaderFunction = async ({ params }): Promise<RequestLoaderD
const activeWorkspaceMeta = await models.workspaceMeta.getByParentId(workspaceId);
invariant(activeWorkspaceMeta, 'Active workspace meta not found');
// NOTE: loaders shouldnt mutate data, this should be moved somewhere else
models.workspaceMeta.update(activeWorkspaceMeta, { activeRequestId: requestId });
await models.workspaceMeta.update(activeWorkspaceMeta, { activeRequestId: requestId });
if (isGrpcRequestId(requestId)) {
return {
activeRequest,
activeRequestMeta: await models.grpcRequestMeta.updateOrCreateByParentId(requestId, { lastActive: Date.now() }),
};
} else {
return {
activeRequest,
activeRequestMeta: await models.requestMeta.updateOrCreateByParentId(requestId, { lastActive: Date.now() }),
};
activeResponse: null,
responses: [],
requestVersions: [],
} as GrpcRequestLoaderData;
}
const activeRequestMeta = await models.requestMeta.updateOrCreateByParentId(requestId, { lastActive: Date.now() });
invariant(activeRequestMeta, 'Request meta not found');
const { filterResponsesByEnv } = await models.settings.getOrCreate();
const responseModelName = isWebSocketRequestId(requestId) ? 'webSocketResponse' : 'response';
const activeResponse = activeRequestMeta.activeResponseId
? await models[responseModelName].getById(activeRequestMeta.activeResponseId)
: await models[responseModelName].getLatestForRequest(requestId, activeWorkspaceMeta.activeEnvironmentId);
const allResponses = await models[responseModelName].findByParentId(requestId) as (Response | WebSocketResponse)[];
const filteredResponses = allResponses
.filter((r: Response | WebSocketResponse) => r.environmentId === activeWorkspaceMeta.activeEnvironmentId);
const responses = (filterResponsesByEnv ? filteredResponses : allResponses)
.sort((a: BaseModel, b: BaseModel) => (a.created > b.created ? -1 : 1));
return {
activeRequest,
activeRequestMeta,
activeResponse,
responses,
requestVersions: await models.requestVersion.findByParentId(requestId),
} as RequestLoaderData | WebSocketRequestLoaderData;
};
export const createRequestAction: ActionFunction = async ({ request, params }) => {
@@ -124,15 +166,20 @@ export const updateRequestAction: ActionFunction = async ({ request, params }) =
export const deleteRequestAction: ActionFunction = async ({ request, params }) => {
const { organizationId, projectId, workspaceId } = params;
invariant(typeof workspaceId === 'string', 'Workspace ID is required');
const formData = await request.formData();
const id = formData.get('id') as string;
const req = await requestOperations.getById(id);
invariant(req, 'Request not found');
models.stats.incrementDeletedRequests();
requestOperations.remove(req);
// TODO: redirect only if we are deleting the active request
return redirect(`/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/debug`);
await requestOperations.remove(req);
const workspaceMeta = await models.workspaceMeta.getByParentId(workspaceId);
invariant(workspaceMeta, 'Workspace meta not found');
if (workspaceMeta.activeRequestId === id) {
await models.workspaceMeta.updateByParentId(workspaceId, { activeRequestId: null });
return redirect(`/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/debug`);
}
return null;
};
export const duplicateRequestAction: ActionFunction = async ({ request, params }) => {
@@ -170,6 +217,92 @@ export const updateRequestMetaAction: ActionFunction = async ({ request, params
await models.requestMeta.updateOrCreateByParentId(requestId, patch);
return null;
};
export interface ConnectActionParams {
url: string;
headers: RequestHeader[];
authentication: RequestAuthentication;
cookieJar: CookieJar;
}
export const connectAction: ActionFunction = async ({ request, params }) => {
const { requestId, workspaceId } = params;
invariant(typeof requestId === 'string', 'Request ID is required');
const req = await requestOperations.getById(requestId);
invariant(req, 'Request not found');
invariant(workspaceId, 'Workspace ID is required');
const rendered = await request.json() as ConnectActionParams;
if (isWebSocketRequestId(requestId)) {
window.main.webSocket.open({
requestId,
workspaceId,
url: rendered.url,
headers: rendered.headers,
authentication: rendered.authentication,
cookieJar: rendered.cookieJar,
});
}
if (isEventStreamRequest(req)) {
window.main.curl.open({
requestId,
workspaceId,
url: rendered.url,
headers: rendered.headers,
authentication: rendered.authentication,
cookieJar: rendered.cookieJar,
});
}
// TODO: remove hack, show loading and reload after connection create response and set activeResponseId
await delay(2000);
return null;
};
export const deleteAllResponsesAction: ActionFunction = async ({ params }) => {
const { workspaceId, requestId } = params;
invariant(typeof requestId === 'string', 'Request ID is required');
const req = await requestOperations.getById(requestId);
invariant(req, 'Request not found');
invariant(workspaceId, 'Workspace ID is required');
const workspaceMeta = await models.workspaceMeta.getByParentId(workspaceId);
invariant(workspaceMeta, 'Active workspace meta not found');
if (isWebSocketRequestId(requestId)) {
await models.webSocketResponse.removeForRequest(requestId, workspaceMeta.activeEnvironmentId);
} else {
await models.response.removeForRequest(requestId, workspaceMeta.activeEnvironmentId);
}
return null;
};
export const deleteResponseAction: ActionFunction = async ({ request, params }) => {
const { workspaceId, requestId } = params;
invariant(typeof requestId === 'string', 'Request ID is required');
const req = await requestOperations.getById(requestId);
invariant(req, 'Request not found');
const { responseId } = await request.json();
invariant(typeof responseId === 'string', 'Response ID is required');
invariant(workspaceId, 'Workspace ID is required');
const workspaceMeta = await models.workspaceMeta.getByParentId(workspaceId);
invariant(workspaceMeta, 'Active workspace meta not found');
if (isWebSocketRequestId(requestId)) {
const res = await models.webSocketResponse.getById(responseId);
invariant(res, 'Response not found');
await models.webSocketResponse.remove(res);
const response = await models.webSocketResponse.getLatestForRequest(requestId, workspaceMeta.activeEnvironmentId);
if (response?.requestVersionId) {
await models.requestVersion.restore(response.requestVersionId);
}
await models.requestMeta.updateOrCreateByParentId(requestId, { activeResponseId: response?._id || null });
} else {
const res = await models.response.getById(responseId);
invariant(res, 'Response not found');
await models.response.remove(res);
const response = await models.response.getLatestForRequest(requestId, workspaceMeta.activeEnvironmentId);
if (response?.requestVersionId) {
await models.requestVersion.restore(response.requestVersionId);
}
await models.requestMeta.updateOrCreateByParentId(requestId, { activeResponseId: response?._id || null });
}
return null;
};
// const RequestRoute = () => {
// const { requestId } = useParams() as { requestId: string };

View File

@@ -47,6 +47,7 @@ import { AppHooks } from '../containers/app-hooks';
import { AIProvider } from '../context/app/ai-context';
import withDragDropContext from '../context/app/drag-drop-context';
import { NunjucksEnabledProvider } from '../context/nunjucks/nunjucks-enabled-context';
import { useSettingsPatcher } from '../hooks/use-request';
import Modals from './modals';
import { WorkspaceLoaderData } from './workspace';
@@ -101,6 +102,7 @@ const Root = () => {
const { revalidate } = useRevalidator();
const workspaceData = useRouteLoaderData(':workspaceId') as WorkspaceLoaderData | null;
const [importUri, setImportUri] = useState('');
const patchSettings = useSettingsPatcher();
useEffect(() => {
onLoginLogout(() => {
@@ -199,10 +201,7 @@ const Root = () => {
'0.0.1',
mainJsContent
);
const settings = await models.settings.getOrCreate();
await models.settings.update(settings, {
theme: parsedTheme.name,
});
patchSettings({ theme: parsedTheme.name });
await reloadPlugins();
await setTheme(parsedTheme.name);
showModal(SettingsModal, { tab: TAB_INDEX_THEMES });
@@ -250,7 +249,7 @@ const Root = () => {
}
}
);
}, []);
}, [patchSettings]);
const { organizationId } = useParams() as {
organizationId: string;

View File

@@ -8,9 +8,13 @@ import { ClientCertificate } from '../../models/client-certificate';
import { CookieJar } from '../../models/cookie-jar';
import { Environment } from '../../models/environment';
import { GitRepository } from '../../models/git-repository';
import { GrpcRequest } from '../../models/grpc-request';
import { sortProjects } from '../../models/helpers/project';
import { DEFAULT_ORGANIZATION_ID } from '../../models/organization';
import { isRemoteProject, Project } from '../../models/project';
import { Request } from '../../models/request';
import { RequestGroup } from '../../models/request-group';
import { WebSocketRequest } from '../../models/websocket-request';
import { Workspace } from '../../models/workspace';
import { WorkspaceMeta } from '../../models/workspace-meta';
import { invariant } from '../../utils/invariant';
@@ -27,6 +31,15 @@ export interface WorkspaceLoaderData {
clientCertificates: ClientCertificate[];
caCertificate: CaCertificate | null;
projects: Project[];
requestTree: Child[];
grpcRequests: GrpcRequest[];
}
export interface Child {
doc: Request | GrpcRequest | WebSocketRequest | RequestGroup;
children: Child[];
collapsed: boolean;
hidden: boolean;
pinned: boolean;
}
export const workspaceLoader: LoaderFunction = async ({
@@ -67,6 +80,7 @@ export const workspaceLoader: LoaderFunction = async ({
const activeApiSpec = await models.apiSpec.getByParentId(workspaceId);
const clientCertificates = await models.clientCertificate.findByParentId(workspaceId);
const allProjects = await models.project.all();
const organizationProjects =
@@ -75,6 +89,31 @@ export const workspaceLoader: LoaderFunction = async ({
: [activeProject];
const projects = sortProjects(organizationProjects);
const requestMetas = await models.requestMeta.all();
const grpcRequestMetas = await models.grpcRequestMeta.all();
const metas = [...requestMetas, ...grpcRequestMetas];
const folderMetas = (await models.requestGroupMeta.all());
const grpcRequestList: GrpcRequest[] = [];
const recurse = async ({ parentId }: { parentId: string }): Promise<Child[]> => {
const folders = await models.requestGroup.findByParentId(parentId);
const requests = await models.request.findByParentId(parentId);
const webSocketRequests = await models.webSocketRequest.findByParentId(parentId);
const grpcRequests = await models.grpcRequest.findByParentId(parentId);
// TODO: remove this state hack when the grpc responses go somewhere else
grpcRequests.map(r => grpcRequestList.push(r));
const childrenWithChildren = await Promise.all([...folders, ...requests, ...webSocketRequests, ...grpcRequests].map(async doc => ({
doc,
pinned: metas.find(m => m.parentId === doc._id)?.pinned || false,
collapsed: folderMetas.find(m => m.parentId === doc._id)?.collapsed || false,
hidden: false,
children: await recurse({ parentId: doc._id }),
})));
return childrenWithChildren;
};
const requestTree = await recurse({ parentId: activeWorkspace._id });
const grpcRequests = grpcRequestList;
return {
activeWorkspace,
activeProject,
@@ -88,6 +127,8 @@ export const workspaceLoader: LoaderFunction = async ({
clientCertificates,
caCertificate: await models.caCertificate.findByParentId(workspaceId),
projects,
requestTree,
grpcRequests,
};
};