Skip to content

Commit 2f59a09

Browse files
authored
feat: support redirect to single request (Kong#9610)
* support redirect to single request * support workspace select * fix tests * remove colon * add a request loading workaround * fix tests * only navigate to single requests * support streaming imports
1 parent 0bd02b0 commit 2f59a09

File tree

7 files changed

+100
-20
lines changed

7 files changed

+100
-20
lines changed

packages/insomnia-smoke-test/tests/smoke/cookie-editor-interactions.test.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ test.describe('Cookie editor', () => {
1818
await page.click('button:has-text("Cookies")');
1919

2020
// Edit existing cookie
21-
await page.getByRole('button', { name: 'Edit' }).first().click();
21+
await page.getByTestId('cookie-test-iteration-0').getByRole('button', { name: 'Edit' }).click();
2222
await page.click('pre[role="presentation"]:has-text("bar")');
2323
await page.locator('[data-testid="CookieValue"] >> textarea').nth(1).fill('123');
2424
await page.locator('text=Done').nth(1).click();
@@ -27,7 +27,7 @@ test.describe('Cookie editor', () => {
2727
// Create a new cookie
2828
await page.getByRole('button', { name: 'Add Cookie' }).click();
2929

30-
await page.getByRole('button', { name: 'Edit' }).first().click();
30+
await page.getByTestId('cookie-test-iteration-0').getByRole('button', { name: 'Edit' }).click();
3131

3232
// Try to replace text in Raw view
3333
await page.getByRole('tab', { name: 'Raw' }).click();
@@ -66,7 +66,7 @@ test.describe('Cookie editor', () => {
6666
await page.getByRole('button', { name: 'Add Cookie' }).click();
6767

6868
// Edit the new cookie
69-
await page.getByRole('button', { name: 'Edit' }).first().click();
69+
await page.getByTestId('cookie-test-iteration-0').getByRole('button', { name: 'Edit' }).click();
7070
await page.getByText('HostOnly').click();
7171
await expect.soft(page.locator('input[name="hostOnly"]')).toBeChecked();
7272
await page.getByRole('tab', { name: 'Raw' }).click();
@@ -90,7 +90,7 @@ test.describe('Cookie editor', () => {
9090
await page.click('button:has-text("Cookies")');
9191

9292
// Set domain to empty
93-
await page.getByRole('button', { name: 'Edit' }).first().click();
93+
await page.getByTestId('cookie-test-iteration-0').getByRole('button', { name: 'Edit' }).click();
9494
await page.getByRole('tab', { name: 'Raw' }).click();
9595
await page
9696
.locator('text=Raw Cookie String >> input[type="text"]')
@@ -100,7 +100,7 @@ test.describe('Cookie editor', () => {
100100
await expect.soft(page.getByTestId('cookie-test-iteration-0').getByTestId('cookie-domain')).toBeEmpty();
101101

102102
// Set domain to example.com
103-
await page.getByRole('button', { name: 'Edit' }).first().click();
103+
await page.getByTestId('cookie-test-iteration-0').getByRole('button', { name: 'Edit' }).click();
104104
await page.getByRole('tab', { name: 'Raw' }).click();
105105
await page
106106
.locator('text=Raw Cookie String >> input[type="text"]')

packages/insomnia-smoke-test/tests/smoke/import-from-url.test.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ test.describe('Import from URL', () => {
1616
test('Should work as expected in HTTP request', async ({ page }) => {
1717
const requestUrl = 'http://localhost:4010/echo?foo=bar&baz=qux';
1818
const codeMirror = page.getByTestId('OneLineEditor').first().locator('.CodeMirror');
19-
2019
await page.getByText('example http').click();
2120

2221
const importFromUrlButton = page.getByRole('button', { name: 'Import from URL' });

packages/insomnia-smoke-test/tests/smoke/pre-request-script-features.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -459,7 +459,7 @@ test.describe('pre-request features tests', () => {
459459
await expect.soft(responsePane).toContainText('Adding SSL KEY certificate');
460460
});
461461

462-
test('pre: insomnia.test and insomnia.expect can work together', async ({ page }) => {
462+
test('insomnia.test and insomnia.expect can work together', async ({ page }) => {
463463
await page.getByLabel('Request Collection').getByTestId('insomnia.test').press('Enter');
464464

465465
// send

packages/insomnia/src/models/helpers/request-operations.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,22 @@ import type { Request } from '../request';
55
import { isSocketIORequest, isSocketIORequestId, type SocketIORequest } from '../socket-io-request';
66
import { isWebSocketRequest, isWebSocketRequestId, type WebSocketRequest } from '../websocket-request';
77

8+
export function findByParentId(
9+
parentId: string,
10+
): Promise<(Request | GrpcRequest | WebSocketRequest | SocketIORequest | McpRequest)[]> {
11+
return Promise.all([
12+
models.request.findByParentId(parentId),
13+
models.grpcRequest.findByParentId(parentId),
14+
models.webSocketRequest.findByParentId(parentId),
15+
models.socketIORequest.findByParentId(parentId),
16+
]).then(([requests, grpcRequests, webSocketRequests, socketIORequests]) => [
17+
...requests,
18+
...grpcRequests,
19+
...webSocketRequests,
20+
...socketIORequests,
21+
]);
22+
}
23+
824
export function getById(
925
requestId: string,
1026
): Promise<Request | GrpcRequest | WebSocketRequest | SocketIORequest | McpRequest | undefined> {

packages/insomnia/src/models/socket-io-request.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ export const create = (patch: Partial<SocketIORequest> = {}) => {
6565

6666
export const getById = (_id: string) => database.findOne<SocketIORequest>(type, { _id });
6767

68+
export const findByParentId = (parentId: string) => database.find<SocketIORequest>(type, { parentId });
69+
6870
export const migrate = (doc: SocketIORequest) => doc;
6971

7072
export const remove = (obj: SocketIORequest) => database.remove(obj);

packages/insomnia/src/routes/import.resources.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { href } from 'react-router';
22

33
import { importResourcesToProject, importResourcesToWorkspace } from '~/common/import';
44
import * as models from '~/models';
5+
import * as requestOperations from '~/models/helpers/request-operations';
56
import { isRemoteProject } from '~/models/project';
67
import type { Workspace } from '~/models/workspace';
78
import {
@@ -65,9 +66,11 @@ export async function clientAction({ request }: Route.ClientActionArgs) {
6566
workspaceId,
6667
options,
6768
});
68-
// Ignore multiple workspace imports
69+
// When navigating, we are interested in knowing if there was only one workspace and only one request
6970
const singleImportedWorkspace = Array.isArray(result) && result.length === 1 && result[0];
70-
return { done: true, workspace: singleImportedWorkspace };
71+
const requests = singleImportedWorkspace && (await requestOperations.findByParentId(singleImportedWorkspace._id));
72+
const singleImportedRequest = Array.isArray(requests) && requests.length === 1 && requests.at(0);
73+
return { done: true, singleImportedWorkspace, singleImportedRequest };
7174
} catch (error) {
7275
console.error('Failed to import resources:', error);
7376
return {

packages/insomnia/src/ui/components/modals/import-modal/import-modal.tsx

Lines changed: 71 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
import classNames from 'classnames';
2+
import { formatDistanceToNowStrict } from 'date-fns';
23
import React, { type FC, Fragment, type ReactNode, useEffect, useId, useMemo, useRef, useState } from 'react';
34
import { type DirectoryDropItem, type FileDropItem, OverlayContainer, useDrop } from 'react-aria';
45
import { Heading } from 'react-aria-components';
5-
import { useNavigate } from 'react-router';
6+
import { useNavigate, useParams } from 'react-router';
67

8+
import { isNotNullOrUndefined } from '~/common/misc';
79
import { scopeToActivity } from '~/models/workspace';
810
import { useImportResourcesFetcher } from '~/routes/import.resources';
911
import { useScanResourcesFetcher } from '~/routes/import.scan';
12+
import { useProjectListWorkspacesLoaderFetcher } from '~/routes/organization.$organizationId.project.$projectId.list-workspaces';
1013
import { Checkbox } from '~/ui/components/base/checkbox';
1114

1215
import type { ScanResult } from '../../../../common/import';
@@ -217,12 +220,21 @@ export const ImportModal: FC<ImportModalProps> = ({
217220
requests: scanResourcesFetcherData.map(scanResult => scanResult.requests?.length || 0),
218221
},
219222
});
220-
const workspace = importFetcher?.data?.workspace;
221-
workspace
222-
? navigate(
223-
`/organization/${organizationId}/project/${defaultProjectId}/workspace/${workspace._id}/${scopeToActivity(workspace.scope)}`,
224-
)
225-
: navigate(`/organization/${organizationId}/project/${defaultProjectId}`);
223+
const workspace = importFetcher?.data?.singleImportedWorkspace;
224+
const request = importFetcher?.data?.singleImportedRequest;
225+
if (workspace && request) {
226+
navigate(
227+
`/organization/${organizationId}/project/${defaultProjectId}/workspace/${workspace._id}/debug/request/${request._id}`,
228+
);
229+
return modalRef.current?.hide();
230+
}
231+
if (workspace) {
232+
navigate(
233+
`/organization/${organizationId}/project/${defaultProjectId}/workspace/${workspace._id}/${scopeToActivity(workspace.scope)}`,
234+
);
235+
return modalRef.current?.hide();
236+
}
237+
navigate(`/organization/${organizationId}/project/${defaultProjectId}`);
226238
modalRef.current?.hide();
227239
}
228240
}, [defaultProjectId, defaultWorkspaceId, importFetcher?.data, navigate, organizationId, scanResourcesFetcherData]);
@@ -276,13 +288,13 @@ export const ImportModal: FC<ImportModalProps> = ({
276288
loading={importFetcher.state !== 'idle'}
277289
disabled={importErrors.length > 0}
278290
isImportingBaseEnvironmentToWorkspace={!!isImportingBaseEnvironmentToWorkspace}
279-
onImport={(overrideBaseEnvironmentData: boolean) => {
291+
onImport={(overrideBaseEnvironmentData: boolean, selectedWorkspaceId?: string) => {
280292
invariant(Array.isArray(scanResourcesFetcherData));
281293

282294
importFetcher.submit({
283295
organizationId,
284296
projectId: defaultProjectId || '',
285-
workspaceId: shouldImportToWorkspace ? defaultWorkspaceId : undefined,
297+
workspaceId: selectedWorkspaceId || (shouldImportToWorkspace ? defaultWorkspaceId : undefined),
286298
options: {
287299
overrideBaseEnvironmentData,
288300
},
@@ -419,17 +431,65 @@ const ImportResourcesForm = ({
419431
}: {
420432
scanResults: ScanResult[];
421433
errors?: string[];
422-
onImport: (overrideBaseEnvironmentData: boolean) => void;
434+
onImport: (overrideBaseEnvironmentData: boolean, selectedWorkspaceId?: string) => void;
423435
disabled: boolean;
424436
loading: boolean;
425437
isImportingBaseEnvironmentToWorkspace: boolean;
426438
}) => {
439+
const { organizationId, projectId, workspaceId } = useParams() as {
440+
organizationId: string;
441+
projectId: string;
442+
workspaceId: string;
443+
};
427444
const [overrideBaseEnvironmentData, setOverrideBaseEnvironmentData] = useState(true);
445+
const isSingleRequest = scanResults.length === 1 && (scanResults[0].requests?.length || 0) === 1;
446+
const workspacesFetcher = useProjectListWorkspacesLoaderFetcher();
447+
const [selectedWorkspaceId, setSelectedWorkspaceId] = useState(workspaceId || '');
448+
useEffect(() => {
449+
const isIdleAndUninitialized = workspacesFetcher.state === 'idle' && !workspacesFetcher.data;
450+
if (isIdleAndUninitialized) {
451+
workspacesFetcher.load({
452+
organizationId,
453+
projectId,
454+
});
455+
}
456+
}, [organizationId, projectId, workspacesFetcher]);
457+
// List collections for active project, sorted by last modified timestamp descending
458+
// Should we list design or mcp?
459+
const workspacesForActiveProject =
460+
workspacesFetcher?.data?.files
461+
.toSorted((a, b) => b.lastModifiedTimestamp - a.lastModifiedTimestamp)
462+
.map(w => ({ ...w.workspace, lastModifiedTimestamp: w.lastModifiedTimestamp }))
463+
.filter(isNotNullOrUndefined)
464+
.filter(w => w.scope === 'collection' || w.scope === 'design') || [];
465+
const shouldShowWorkspaceSelect = !workspaceId && isSingleRequest && workspacesForActiveProject.length > 0;
428466
return (
429467
<Fragment>
430468
<div className="flex max-h-[50vh] flex-col gap-(--padding-md) overflow-auto">
431469
<div className="overflow-y-auto">
432470
<ScanResultsTable scanResults={scanResults} />
471+
{shouldShowWorkspaceSelect && (
472+
<div className="form-row mt-2">
473+
<div className="form-control form-control--outlined">
474+
<label>
475+
Select Workspace:
476+
<select
477+
aria-label="Select Workspace"
478+
name="workspaceId"
479+
value={selectedWorkspaceId}
480+
onChange={e => setSelectedWorkspaceId(e.target.value)}
481+
>
482+
<option value="">-- New Workspace --</option>
483+
{workspacesForActiveProject.map(w => (
484+
<option key={w._id} value={w._id}>
485+
{w.name} - {formatDistanceToNowStrict(w.lastModifiedTimestamp)}
486+
</option>
487+
))}
488+
</select>
489+
</label>
490+
</div>
491+
</div>
492+
)}
433493
{isImportingBaseEnvironmentToWorkspace && (
434494
<Checkbox
435495
isSelected={overrideBaseEnvironmentData}
@@ -462,7 +522,7 @@ const ImportResourcesForm = ({
462522
variant="contained"
463523
bg="surprise"
464524
disabled={disabled}
465-
onClick={() => onImport(overrideBaseEnvironmentData)}
525+
onClick={() => onImport(overrideBaseEnvironmentData, selectedWorkspaceId)}
466526
className="btn h-10 gap-(--padding-sm)"
467527
>
468528
{loading ? (

0 commit comments

Comments
 (0)