mirror of
https://github.com/Kong/insomnia
synced 2024-11-07 14:19:58 +00:00
feat: mock resources (#6760)
* project ui changes * project ui changes * create mock server model * model mock server similarly to design doc * use spec modelling and nav * layout pass * rename requestbin to mock-route * sidebar ui pass * load mock server * add url bar * can navigate to headers * Refactor mock server and mock route creation and retrieval * route crud * sidebar layout * add delete guuard * patch route * Add mock response tab to request pane * wire up mock servers in requests * Update mock server and route selection in RequestPane * make it work without internet * can create bin * pass body and headers to bin * can fetch logs but cant see em * split out response pane for hmr * basic table * extract mock url bar * add header tab * made a dumb cache * url bar pass * send request and create response * wire up timeline * wire up preview * timeline useeffect * move to action * fix types * empty states * rebase updating aria * use har type * can edit bins * cookie support * wire up status * status text * magic status text * ui * always use put rather than create bin * add url to mock route * scroll bar * add content types * validation * fix flake * improve logs * fix outlet warning * fix send to mock endpoint * switch table to grid * handle errors * rotate log * create mockbin on open if needed * add full url ux * reverse log order * binId from store * remove http method * rename prefix * use server Id for bin id * fix copy * show log har * fix url bar * fix button padding * tailwind * method select * remove default status text * full tailwind * fix breadcrumb * default to json * move copy to end, remove save * error msg * only patch when needed * fix ws colors * fix command palette * add isMock helper * revert local storage mechanism * fix redirect * fix ignore upsert * extract to constant * ui test * hide actions from mock-server * fix code editor onBlur * lift update to route * refactor to return only errors * add url to mock server model * select mock ui pass * can modify url in settings * use server url from db if selected * hide url option * fix lint error * extract to file * remove binResponse * can sync * move things around * rename name path sync * fix type check * capture kvp onBlur * fix error message * basic mock test * wire up mock patcher and navigate * rename component * remove url prop of route * fix lint * fix test * temporary skip e2e test * fix constant url * fix migration * remove console logs * rename function * only create a single hidden request --------- Co-authored-by: gatzjames <jamesgatzos@gmail.com>
This commit is contained in:
parent
653d497763
commit
557e5c0c6e
@ -1,7 +1,7 @@
|
||||
import { expect } from '@playwright/test';
|
||||
|
||||
import { loadFixture } from '../../playwright/paths';
|
||||
import { test } from '../../playwright/test';;
|
||||
import { test } from '../../playwright/test';
|
||||
|
||||
test('can send requests', async ({ app, page }) => {
|
||||
test.slow(process.platform === 'darwin' || process.platform === 'win32', 'Slow app start on these platforms');
|
||||
|
@ -105,18 +105,15 @@ test.describe('Dashboard', async () => {
|
||||
await expect(page.locator('.app')).toContainText('test123');
|
||||
|
||||
// Duplicate document
|
||||
await page.click('text=Documenttest123just now >> button');
|
||||
await page.getByLabel('test123').getByRole('button').click();
|
||||
await page.getByRole('menuitem', { name: 'Duplicate' }).click();
|
||||
await page.locator('input[name="name"]').fill('test123-duplicate');
|
||||
await page.click('[role="dialog"] button:has-text("Duplicate")');
|
||||
|
||||
await page.getByTestId('project').click();
|
||||
|
||||
const workspaceCards = page.getByLabel('Workspaces').getByRole('gridcell');
|
||||
await expect(workspaceCards).toHaveCount(2);
|
||||
|
||||
// Delete document
|
||||
await page.click('text=Documenttest123just now >> button');
|
||||
await page.getByLabel('test123-duplicate').getByRole('button').click();
|
||||
await page.getByRole('menuitem', { name: 'Delete' }).click();
|
||||
await page.getByRole('button', { name: 'Delete' }).click();
|
||||
// @TODO: Re-enable - Requires mocking VCS operations
|
||||
@ -143,17 +140,15 @@ test.describe('Dashboard', async () => {
|
||||
await expect(page.locator('.app')).toContainText('test123');
|
||||
|
||||
// Duplicate collection
|
||||
await page.click('text=Collectiontest123just now >> button');
|
||||
await page.getByLabel('test123').getByRole('button').click();
|
||||
await page.getByRole('menuitem', { name: 'Duplicate' }).click();
|
||||
await page.locator('input[name="name"]').fill('test123-duplicate');
|
||||
await page.click('[role="dialog"] button:has-text("Duplicate")');
|
||||
|
||||
await page.getByTestId('project').click();
|
||||
const workspaceCards = page.getByLabel('Workspaces').getByRole('gridcell');
|
||||
await expect(workspaceCards).toHaveCount(2);
|
||||
|
||||
// Delete collection
|
||||
await page.click('text=Collectiontest123just now >> button');
|
||||
await page.getByLabel('test123-duplicate').getByRole('button').click();
|
||||
await page.getByRole('menuitem', { name: 'Delete' }).click();
|
||||
await page.getByRole('button', { name: 'Delete' }).click();
|
||||
// @TODO: Re-enable - Requires mocking VCS operations
|
||||
|
25
packages/insomnia-smoke-test/tests/smoke/mock.test.ts
Normal file
25
packages/insomnia-smoke-test/tests/smoke/mock.test.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { test } from '../../playwright/test';
|
||||
|
||||
// TODO: unskip this test when cloud mock is online
|
||||
test.skip('can make a mock route', async ({ page }) => {
|
||||
test.slow(process.platform === 'darwin' || process.platform === 'win32', 'Slow app start on these platforms');
|
||||
await page.getByLabel('New Mock Server').click();
|
||||
await page.getByRole('button', { name: 'Create', exact: true }).click();
|
||||
await page.getByRole('button', { name: 'New Mock Route' }).click();
|
||||
await page.getByText('GET/').click();
|
||||
await page.getByTestId('CodeEditor').getByRole('textbox').fill('123');
|
||||
await page.getByRole('tab', { name: 'Response Headers' }).click();
|
||||
await page.locator('.CodeMirror').first().click();
|
||||
await page.getByRole('textbox').nth(1).fill('my-header');
|
||||
await page.getByRole('textbox').nth(2).fill('my-value');
|
||||
await page.getByRole('tab', { name: 'Response Status' }).click();
|
||||
await page.getByPlaceholder('200').click();
|
||||
await page.getByPlaceholder('200').fill('201');
|
||||
|
||||
await page.getByRole('button', { name: 'Test' }).click();
|
||||
await page.getByRole('tab', { name: 'Preview' }).click();
|
||||
await page.getByLabel('Preview').getByText('123').click();
|
||||
await page.getByRole('tab', { name: 'Timeline' }).click();
|
||||
await page.getByText('HTTP/1.1 201 Created').click();
|
||||
await page.getByText('my-header:').click();
|
||||
});
|
@ -136,6 +136,7 @@ export enum UpdateURL {
|
||||
|
||||
// API
|
||||
export const getApiBaseURL = () => env.INSOMNIA_API_URL || 'https://api.insomnia.rest';
|
||||
export const getMockServiceURL = () => env.INSOMNIA_MOCK_API_URL || 'https://mock.insomnia.rest';
|
||||
export const getAIServiceURL = () => env.INSOMNIA_AI_URL || 'https://ai.insomnia.rest';
|
||||
|
||||
export const getUpdatesBaseURL = () => env.INSOMNIA_UPDATES_URL || 'https://updates.insomnia.rest';
|
||||
@ -267,7 +268,7 @@ export const CONTENT_TYPE_FORM_DATA = 'multipart/form-data';
|
||||
export const CONTENT_TYPE_FILE = 'application/octet-stream';
|
||||
export const CONTENT_TYPE_GRAPHQL = 'application/graphql';
|
||||
export const CONTENT_TYPE_OTHER = '';
|
||||
const contentTypesMap: Record<string, string[]> = {
|
||||
export const contentTypesMap: Record<string, string[]> = {
|
||||
[CONTENT_TYPE_EDN]: ['EDN', 'EDN'],
|
||||
[CONTENT_TYPE_FILE]: ['File', 'Binary File'],
|
||||
[CONTENT_TYPE_FORM_DATA]: ['Multipart', 'Multipart Form'],
|
||||
@ -573,6 +574,8 @@ export const EXPORT_TYPE_REQUEST = 'request';
|
||||
export const EXPORT_TYPE_GRPC_REQUEST = 'grpc_request';
|
||||
export const EXPORT_TYPE_WEBSOCKET_REQUEST = 'websocket_request';
|
||||
export const EXPORT_TYPE_WEBSOCKET_PAYLOAD = 'websocket_payload';
|
||||
export const EXPORT_TYPE_MOCK_SERVER = 'mock';
|
||||
export const EXPORT_TYPE_MOCK_ROUTE = 'mock_route';
|
||||
export const EXPORT_TYPE_REQUEST_GROUP = 'request_group';
|
||||
export const EXPORT_TYPE_UNIT_TEST_SUITE = 'unit_test_suite';
|
||||
export const EXPORT_TYPE_UNIT_TEST = 'unit_test';
|
||||
|
@ -1,5 +1,9 @@
|
||||
import { isDesign, Workspace } from '../models/workspace';
|
||||
import { isDesign, isMockServer, Workspace } from '../models/workspace';
|
||||
import { strings } from './strings';
|
||||
|
||||
export const getWorkspaceLabel = (workspace: Workspace) =>
|
||||
isDesign(workspace) ? strings.document : strings.collection;
|
||||
isDesign(workspace)
|
||||
? strings.document
|
||||
: isMockServer(workspace)
|
||||
? strings.mock
|
||||
: strings.collection;
|
||||
|
@ -414,9 +414,8 @@ function getRequestCookies(renderedRequest: RenderedRequest) {
|
||||
return harCookies;
|
||||
}
|
||||
|
||||
function getResponseCookies(response: Response) {
|
||||
const headers = response.headers.filter(Boolean) as HarCookie[];
|
||||
const responseCookies = getSetCookieHeaders(headers)
|
||||
export function getResponseCookiesFromHeaders(headers: HarCookie[]) {
|
||||
return getSetCookieHeaders(headers)
|
||||
.reduce((accumulator, harCookie) => {
|
||||
let cookie: null | undefined | ToughCookie = null;
|
||||
|
||||
@ -433,7 +432,11 @@ function getResponseCookies(response: Response) {
|
||||
mapCookie(cookie),
|
||||
];
|
||||
}, [] as HarCookie[]);
|
||||
return responseCookies;
|
||||
}
|
||||
|
||||
function getResponseCookies(response: Response) {
|
||||
const headers = response.headers.filter(Boolean);
|
||||
return getResponseCookiesFromHeaders(headers);
|
||||
}
|
||||
|
||||
function mapCookie(cookie: ToughCookie) {
|
||||
|
@ -596,5 +596,7 @@ export async function getRenderContextAncestors(base?: Request | GrpcRequest | W
|
||||
models.requestGroup.type,
|
||||
models.workspace.type,
|
||||
models.project.type,
|
||||
models.mockRoute.type,
|
||||
models.mockServer.type,
|
||||
]);
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ export interface StringInfo {
|
||||
|
||||
type StringId =
|
||||
| 'collection'
|
||||
| 'mock'
|
||||
| 'document'
|
||||
| 'project'
|
||||
| 'workspace'
|
||||
@ -18,6 +19,10 @@ export const strings: Record<StringId, StringInfo> = {
|
||||
singular: 'Collection',
|
||||
plural: 'Collections',
|
||||
},
|
||||
mock: {
|
||||
singular: 'Mock',
|
||||
plural: 'Mocks',
|
||||
},
|
||||
document: {
|
||||
singular: 'Document',
|
||||
plural: 'Documents',
|
||||
|
@ -3,6 +3,8 @@ import {
|
||||
EXPORT_TYPE_COOKIE_JAR,
|
||||
EXPORT_TYPE_ENVIRONMENT,
|
||||
EXPORT_TYPE_GRPC_REQUEST,
|
||||
EXPORT_TYPE_MOCK_ROUTE,
|
||||
EXPORT_TYPE_MOCK_SERVER,
|
||||
EXPORT_TYPE_PROTO_DIRECTORY,
|
||||
EXPORT_TYPE_PROTO_FILE,
|
||||
EXPORT_TYPE_REQUEST,
|
||||
@ -22,6 +24,8 @@ import * as _environment from './environment';
|
||||
import * as _gitRepository from './git-repository';
|
||||
import * as _grpcRequest from './grpc-request';
|
||||
import * as _grpcRequestMeta from './grpc-request-meta';
|
||||
import * as _mockRoute from './mock-route';
|
||||
import * as _mockServer from './mock-server';
|
||||
import * as _oAuth2Token from './o-auth-2-token';
|
||||
import * as _pluginData from './plugin-data';
|
||||
import * as _project from './project';
|
||||
@ -64,6 +68,8 @@ export const caCertificate = _caCertificate;
|
||||
export const cookieJar = _cookieJar;
|
||||
export const environment = _environment;
|
||||
export const gitRepository = _gitRepository;
|
||||
export const mockServer = _mockServer;
|
||||
export const mockRoute = _mockRoute;
|
||||
export const oAuth2Token = _oAuth2Token;
|
||||
export const pluginData = _pluginData;
|
||||
export const request = _request;
|
||||
@ -109,6 +115,8 @@ export function all() {
|
||||
requestVersion,
|
||||
requestMeta,
|
||||
response,
|
||||
mockServer,
|
||||
mockRoute,
|
||||
oAuth2Token,
|
||||
caCertificate,
|
||||
clientCertificate,
|
||||
@ -211,6 +219,8 @@ export const MODELS_BY_EXPORT_TYPE: Record<string, any> = {
|
||||
[EXPORT_TYPE_REQUEST]: request,
|
||||
[EXPORT_TYPE_WEBSOCKET_PAYLOAD]: webSocketPayload,
|
||||
[EXPORT_TYPE_WEBSOCKET_REQUEST]: webSocketRequest,
|
||||
[EXPORT_TYPE_MOCK_SERVER]: mockServer,
|
||||
[EXPORT_TYPE_MOCK_ROUTE]: mockRoute,
|
||||
[EXPORT_TYPE_GRPC_REQUEST]: grpcRequest,
|
||||
[EXPORT_TYPE_REQUEST_GROUP]: requestGroup,
|
||||
[EXPORT_TYPE_UNIT_TEST_SUITE]: unitTestSuite,
|
||||
|
82
packages/insomnia/src/models/mock-route.ts
Normal file
82
packages/insomnia/src/models/mock-route.ts
Normal file
@ -0,0 +1,82 @@
|
||||
import { database as db } from '../common/database';
|
||||
import type { BaseModel } from './index';
|
||||
import { RequestHeader } from './request';
|
||||
|
||||
export const name = 'Mock Route';
|
||||
|
||||
export const type = 'MockRoute';
|
||||
|
||||
export const prefix = 'mock_route';
|
||||
|
||||
export const canDuplicate = true;
|
||||
|
||||
export const canSync = true;
|
||||
|
||||
interface BaseMockRoute {
|
||||
body: string;
|
||||
headers: RequestHeader[];
|
||||
parentId: string;
|
||||
statusCode: number;
|
||||
statusText: string;
|
||||
name: string;
|
||||
mimeType: string;// response body type
|
||||
method: string; // used only for sending the testing request
|
||||
}
|
||||
|
||||
export type MockRoute = BaseModel & BaseMockRoute;
|
||||
|
||||
export function init(): BaseMockRoute {
|
||||
return {
|
||||
body: '',
|
||||
headers: [],
|
||||
parentId: '',
|
||||
statusCode: 200,
|
||||
statusText: '',
|
||||
name: '/',
|
||||
mimeType: 'application/json',
|
||||
method: 'GET',
|
||||
};
|
||||
}
|
||||
|
||||
export const isMockRoute = (model: Pick<BaseModel, 'type'>): model is MockRoute => (
|
||||
model.type === type
|
||||
);
|
||||
|
||||
export function migrate(doc: MockRoute) {
|
||||
return doc;
|
||||
}
|
||||
|
||||
export function create(patch: Partial<MockRoute> = {}) {
|
||||
if (!patch.parentId) {
|
||||
throw new Error('New MockRoute missing `parentId`: ' + JSON.stringify(patch));
|
||||
}
|
||||
|
||||
return db.docCreate<MockRoute>(type, patch);
|
||||
}
|
||||
|
||||
export function update(
|
||||
mockRoute: MockRoute,
|
||||
patch: Partial<MockRoute> = {},
|
||||
) {
|
||||
return db.docUpdate<MockRoute>(mockRoute, patch);
|
||||
}
|
||||
|
||||
export function getById(id: string) {
|
||||
return db.get<MockRoute>(type, id);
|
||||
}
|
||||
|
||||
export function findByParentId(parentId: string) {
|
||||
return db.find<MockRoute>(type, { parentId });
|
||||
}
|
||||
|
||||
export function removeWhere(parentId: string) {
|
||||
return db.removeWhere(type, { parentId });
|
||||
}
|
||||
|
||||
export function remove(mockRoute: MockRoute) {
|
||||
return db.remove(mockRoute);
|
||||
}
|
||||
|
||||
export function all() {
|
||||
return db.all<MockRoute>(type);
|
||||
}
|
91
packages/insomnia/src/models/mock-server.ts
Normal file
91
packages/insomnia/src/models/mock-server.ts
Normal file
@ -0,0 +1,91 @@
|
||||
import { database as db } from '../common/database';
|
||||
import { type BaseModel, workspace } from './index';
|
||||
|
||||
export const name = 'Mock Server';
|
||||
|
||||
export const type = 'MockServer';
|
||||
|
||||
export const prefix = 'mock';
|
||||
|
||||
export const canDuplicate = true;
|
||||
|
||||
export const canSync = true;
|
||||
|
||||
interface BaseMockServer {
|
||||
parentId: string;
|
||||
name: string;
|
||||
url: string;
|
||||
useInsomniaCloud: boolean;
|
||||
}
|
||||
|
||||
export type MockServer = BaseModel & BaseMockServer;
|
||||
|
||||
export function init(): BaseMockServer {
|
||||
return {
|
||||
parentId: '',
|
||||
name: 'New Mock',
|
||||
url: 'http://localhost:8080',
|
||||
useInsomniaCloud: true,
|
||||
};
|
||||
}
|
||||
|
||||
export const isMockServer = (model: Pick<BaseModel, 'type'>): model is MockServer => (
|
||||
model.type === type
|
||||
);
|
||||
|
||||
export function migrate(doc: MockServer) {
|
||||
return doc;
|
||||
}
|
||||
|
||||
export function create(patch: Partial<MockServer> = {}) {
|
||||
if (!patch.parentId) {
|
||||
throw new Error('New MockServer missing `parentId`: ' + JSON.stringify(patch));
|
||||
}
|
||||
|
||||
return db.docCreate<MockServer>(type, patch);
|
||||
}
|
||||
export async function getOrCreateForParentId(
|
||||
workspaceId: string,
|
||||
patch: Partial<MockServer> = {},
|
||||
) {
|
||||
const mockServer = await db.getWhere<MockServer>(type, {
|
||||
parentId: workspaceId,
|
||||
});
|
||||
|
||||
if (!mockServer) {
|
||||
return db.docCreate<MockServer>(type, { ...patch, parentId: workspaceId });
|
||||
}
|
||||
|
||||
return mockServer;
|
||||
}
|
||||
export function update(
|
||||
mockServer: MockServer,
|
||||
patch: Partial<MockServer> = {},
|
||||
) {
|
||||
return db.docUpdate<MockServer>(mockServer, patch);
|
||||
}
|
||||
|
||||
export function getById(id: string) {
|
||||
return db.get<MockServer>(type, id);
|
||||
}
|
||||
|
||||
export function getByParentId(parentId: string) {
|
||||
return db.getWhere<MockServer>(type, { parentId });
|
||||
}
|
||||
|
||||
export async function findByProjectId(projectId: string) {
|
||||
const workspaces = await workspace.findByParentId(projectId);
|
||||
return db.find<MockServer>(type, { parentId: { $in: workspaces.map(ws => ws._id) } });
|
||||
}
|
||||
|
||||
export function removeWhere(parentId: string) {
|
||||
return db.removeWhere(type, { parentId });
|
||||
}
|
||||
|
||||
export function remove(mockServer: MockServer) {
|
||||
return db.remove(mockServer);
|
||||
}
|
||||
|
||||
export function all() {
|
||||
return db.all<MockServer>(type);
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
import type { Merge } from 'type-fest';
|
||||
|
||||
import { ACTIVITY_DEBUG, ACTIVITY_SPEC } from '../common/constants';
|
||||
import { database as db } from '../common/database';
|
||||
import { strings } from '../common/strings';
|
||||
import type { BaseModel } from './index';
|
||||
@ -16,7 +17,7 @@ export interface BaseWorkspace {
|
||||
name: string;
|
||||
description: string;
|
||||
certificates?: any; // deprecated
|
||||
scope: 'design' | 'collection';
|
||||
scope: 'design' | 'collection' | 'mock-server';
|
||||
}
|
||||
|
||||
export type WorkspaceScope = BaseWorkspace['scope'];
|
||||
@ -24,6 +25,7 @@ export type WorkspaceScope = BaseWorkspace['scope'];
|
||||
export const WorkspaceScopeKeys = {
|
||||
design: 'design',
|
||||
collection: 'collection',
|
||||
mockServer: 'mock-server',
|
||||
} as const;
|
||||
|
||||
export type Workspace = BaseModel & BaseWorkspace;
|
||||
@ -40,6 +42,10 @@ export const isCollection = (workspace: Pick<Workspace, 'scope'>) => (
|
||||
workspace.scope === WorkspaceScopeKeys.collection
|
||||
);
|
||||
|
||||
export const isMockServer = (workspace: Pick<Workspace, 'scope'>) => (
|
||||
workspace.scope === WorkspaceScopeKeys.mockServer
|
||||
);
|
||||
|
||||
export const init = (): BaseWorkspace => ({
|
||||
name: `New ${strings.collection.singular}`,
|
||||
description: '',
|
||||
@ -135,9 +141,12 @@ type MigrationWorkspace = Merge<Workspace, { scope: OldScopeTypes | Workspace['s
|
||||
* Ensure workspace scope is set to a valid entry
|
||||
*/
|
||||
function _migrateScope(workspace: MigrationWorkspace) {
|
||||
if (workspace.scope === WorkspaceScopeKeys.design || workspace.scope === WorkspaceScopeKeys.collection) {
|
||||
if (workspace.scope === WorkspaceScopeKeys.design
|
||||
|| workspace.scope === WorkspaceScopeKeys.collection
|
||||
|| workspace.scope === WorkspaceScopeKeys.mockServer) {
|
||||
return workspace as Workspace;
|
||||
}
|
||||
// designer and spec => design, unset => collection
|
||||
if (workspace.scope === 'designer' || workspace.scope === 'spec') {
|
||||
workspace.scope = WorkspaceScopeKeys.design;
|
||||
} else {
|
||||
@ -157,3 +166,16 @@ export const SCRATCHPAD_WORKSPACE_ID = 'wrk_scratchpad';
|
||||
export function isScratchpad(workspace: Workspace) {
|
||||
return workspace._id === SCRATCHPAD_WORKSPACE_ID;
|
||||
}
|
||||
|
||||
export const scopeToActivity = (scope: WorkspaceScope) => {
|
||||
switch (scope) {
|
||||
case WorkspaceScopeKeys.collection:
|
||||
return ACTIVITY_DEBUG;
|
||||
case WorkspaceScopeKeys.design:
|
||||
return ACTIVITY_SPEC;
|
||||
case WorkspaceScopeKeys.mockServer:
|
||||
return 'mock-server';
|
||||
default:
|
||||
return ACTIVITY_DEBUG;
|
||||
}
|
||||
};
|
||||
|
@ -43,9 +43,14 @@ export const fetchRequestData = async (requestId: string) => {
|
||||
models.request.type,
|
||||
models.requestGroup.type,
|
||||
models.workspace.type,
|
||||
models.mockRoute.type,
|
||||
models.mockServer.type,
|
||||
]);
|
||||
const workspaceDoc = ancestors.find(isWorkspace);
|
||||
const workspaceId = workspaceDoc ? workspaceDoc._id : 'n/a';
|
||||
invariant(workspaceDoc?._id, 'failed to find workspace');
|
||||
|
||||
const workspaceId = workspaceDoc._id;
|
||||
|
||||
const workspace = await models.workspace.getById(workspaceId);
|
||||
invariant(workspace, 'failed to find workspace');
|
||||
const workspaceMeta = await models.workspaceMeta.getOrCreateByParentId(workspace._id);
|
||||
|
@ -53,6 +53,8 @@ describe('NeDBClient', () => {
|
||||
models.apiSpec.type,
|
||||
models.environment.type,
|
||||
models.grpcRequest.type,
|
||||
models.mockRoute.type,
|
||||
models.mockServer.type,
|
||||
models.protoDirectory.type,
|
||||
models.protoFile.type,
|
||||
models.request.type,
|
||||
|
@ -147,6 +147,8 @@ export class NeDBClient {
|
||||
models.protoDirectory.type,
|
||||
models.webSocketRequest.type,
|
||||
models.webSocketPayload.type,
|
||||
models.mockRoute.type,
|
||||
models.mockServer.type,
|
||||
];
|
||||
} else if (type !== null && id === null) {
|
||||
const workspace = await db.get(models.workspace.type, this._workspaceId);
|
||||
|
@ -90,7 +90,7 @@ export interface CodeEditorProps {
|
||||
noMatchBrackets?: boolean;
|
||||
noStyleActiveLine?: boolean;
|
||||
// used only for saving env editor state
|
||||
onBlur?: () => void;
|
||||
onBlur?: (e: FocusEvent) => void;
|
||||
onChange?: (value: string) => void;
|
||||
onClickLink?: CodeMirrorLinkClickCallback;
|
||||
pinToBottom?: boolean;
|
||||
@ -397,10 +397,7 @@ export const CodeEditor = forwardRef<CodeEditorHandle, CodeEditorProps>(({
|
||||
};
|
||||
}
|
||||
};
|
||||
codeMirror.current.on('blur', () => {
|
||||
persistState();
|
||||
onBlur?.();
|
||||
});
|
||||
|
||||
codeMirror.current.on('scroll', persistState);
|
||||
codeMirror.current.on('fold', persistState);
|
||||
codeMirror.current.on('unfold', persistState);
|
||||
@ -439,7 +436,6 @@ export const CodeEditor = forwardRef<CodeEditorHandle, CodeEditorProps>(({
|
||||
}
|
||||
});
|
||||
useUnmount(() => {
|
||||
onBlur?.();
|
||||
codeMirror.current?.toTextArea();
|
||||
codeMirror.current?.closeHintDropdown();
|
||||
codeMirror.current = null;
|
||||
@ -471,9 +467,9 @@ export const CodeEditor = forwardRef<CodeEditorHandle, CodeEditorProps>(({
|
||||
}, [lintOptions, noLint, onChange]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleOnBlur = () => onBlur?.();
|
||||
const handleOnBlur = (_: CodeMirror.Editor, e: FocusEvent) => onBlur?.(e);
|
||||
codeMirror.current?.on('blur', handleOnBlur);
|
||||
return () => codeMirror.current?.on('blur', handleOnBlur);
|
||||
return () => codeMirror.current?.off('blur', handleOnBlur);
|
||||
}, [onBlur]);
|
||||
|
||||
const tryToSetOption = (key: keyof EditorConfiguration, value: any) => {
|
||||
|
@ -24,6 +24,7 @@ export interface OneLineEditorProps {
|
||||
readOnly?: boolean;
|
||||
type?: string;
|
||||
onPaste?: (text: string) => void;
|
||||
onBlur?: (e: FocusEvent) => void;
|
||||
}
|
||||
|
||||
export interface OneLineEditorHandle {
|
||||
@ -40,6 +41,7 @@ export const OneLineEditor = forwardRef<OneLineEditorHandle, OneLineEditorProps>
|
||||
readOnly,
|
||||
type,
|
||||
onPaste,
|
||||
onBlur,
|
||||
}, ref) => {
|
||||
const textAreaRef = useRef<HTMLTextAreaElement>(null);
|
||||
const codeMirror = useRef<CodeMirror.EditorFromTextArea | null>(null);
|
||||
@ -123,6 +125,12 @@ export const OneLineEditor = forwardRef<OneLineEditorHandle, OneLineEditorProps>
|
||||
}
|
||||
});
|
||||
|
||||
codeMirror.current.on('blur', (_, e) => {
|
||||
if (onBlur) {
|
||||
onBlur(e);
|
||||
}
|
||||
});
|
||||
|
||||
codeMirror.current.on('keydown', (doc: CodeMirror.Editor, event: KeyboardEvent) => {
|
||||
// Use default tab behaviour if we're told
|
||||
if (event.code === 'Tab') {
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { IconName } from '@fortawesome/fontawesome-svg-core';
|
||||
import React from 'react';
|
||||
import { useState } from 'react';
|
||||
import { Collection, ComboBox, Dialog, Header, Input, Label, ListBox, ListBoxItem, Modal, ModalOverlay, Section, Text } from 'react-aria-components';
|
||||
@ -7,6 +8,7 @@ import { isGrpcRequest } from '../../models/grpc-request';
|
||||
import { isRequest } from '../../models/request';
|
||||
import { isRequestGroup } from '../../models/request-group';
|
||||
import { isWebSocketRequest } from '../../models/websocket-request';
|
||||
import { scopeToActivity, WorkspaceScope } from '../../models/workspace';
|
||||
import { WorkspaceLoaderData } from '../routes/workspace';
|
||||
import { Icon } from './icon';
|
||||
import { useDocBodyKeyboardShortcuts } from './keydown-binder';
|
||||
@ -32,6 +34,12 @@ export const CommandPalette = () => {
|
||||
},
|
||||
});
|
||||
|
||||
const scopeToIconMap: Record<string, IconName> = {
|
||||
design: 'file',
|
||||
collection: 'bars',
|
||||
'mock-server': 'server',
|
||||
};
|
||||
|
||||
return (
|
||||
<ModalOverlay isOpen={isOpen} onOpenChange={setIsOpen} isDismissable className="w-full h-[--visual-viewport-height] fixed z-10 top-0 left-0 flex pt-20 justify-center bg-black/30">
|
||||
<Modal className="max-w-2xl h-max w-full rounded-md flex flex-col overflow-hidden border border-solid border-[--hl-sm] max-h-[80vh] bg-[--color-bg] text-[--color-font]">
|
||||
@ -54,8 +62,11 @@ export const CommandPalette = () => {
|
||||
if (!itemId) {
|
||||
return;
|
||||
}
|
||||
if (itemId.toString().startsWith('wrk_')) {
|
||||
navigate(`/organization/${organizationId}/project/${projectId}/workspace/${itemId}/debug`);
|
||||
const isWorkspace = itemId.toString().startsWith('wrk_');
|
||||
if (isWorkspace) {
|
||||
const [id, scope] = itemId.toString().split('|');
|
||||
const activity = scopeToActivity(scope as WorkspaceScope);
|
||||
navigate(`/organization/${organizationId}/project/${projectId}/workspace/${id}/${activity}`);
|
||||
} else {
|
||||
navigate(`/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/debug/request/${itemId}`);
|
||||
}
|
||||
@ -115,11 +126,11 @@ export const CommandPalette = () => {
|
||||
id: 'collections-and-documents',
|
||||
name: 'Collections and documents',
|
||||
children: workspaces.map(workspace => ({
|
||||
id: workspace._id,
|
||||
icon: <Icon icon={workspace.scope === 'collection' ? 'bars' : 'file'} className="text-[--color-font] w-10 flex-shrink-0 flex items-center justify-center" />,
|
||||
id: workspace._id + '|' + workspace.scope,
|
||||
icon: <Icon icon={scopeToIconMap[workspace.scope]} className="text-[--color-font] w-10 flex-shrink-0 flex items-center justify-center" />,
|
||||
name: workspace.name,
|
||||
description: '',
|
||||
textValue: `${workspace.scope === 'collection' ? 'Collection' : 'Document'} ${workspace.name}`,
|
||||
textValue: `${workspace.scope} ${workspace.name}`,
|
||||
})),
|
||||
},
|
||||
]}
|
||||
|
@ -9,6 +9,7 @@ import { RENDER_PURPOSE_NO_RENDER } from '../../../common/render';
|
||||
import type { ApiSpec } from '../../../models/api-spec';
|
||||
import { CaCertificate } from '../../../models/ca-certificate';
|
||||
import { ClientCertificate } from '../../../models/client-certificate';
|
||||
import { MockServer } from '../../../models/mock-server';
|
||||
import { isRemoteProject, Project } from '../../../models/project';
|
||||
import type { Workspace } from '../../../models/workspace';
|
||||
import { WorkspaceScopeKeys } from '../../../models/workspace';
|
||||
@ -30,6 +31,7 @@ interface Props {
|
||||
workspace: Workspace;
|
||||
workspaceMeta: WorkspaceMeta;
|
||||
apiSpec: ApiSpec | null;
|
||||
mockServer: MockServer | null;
|
||||
project: Project;
|
||||
projects: Project[];
|
||||
clientCertificates: ClientCertificate[];
|
||||
@ -85,7 +87,7 @@ const useDocumentActionPlugins = ({ workspace, apiSpec, project }: Props) => {
|
||||
};
|
||||
|
||||
export const WorkspaceCardDropdown: FC<Props> = props => {
|
||||
const { workspace, project, projects } = props;
|
||||
const { workspace, mockServer, project, projects } = props;
|
||||
const fetcher = useFetcher();
|
||||
const [isDuplicateModalOpen, setIsDuplicateModalOpen] = useState(false);
|
||||
const [isImportModalOpen, setIsImportModalOpen] = useState(false);
|
||||
@ -209,6 +211,7 @@ export const WorkspaceCardDropdown: FC<Props> = props => {
|
||||
{isSettingsModalOpen && (
|
||||
<WorkspaceSettingsModal
|
||||
workspace={workspace}
|
||||
mockServer={mockServer}
|
||||
onClose={() => setIsSettingsModalOpen(false)}
|
||||
/>
|
||||
)}
|
||||
|
@ -41,6 +41,7 @@ export const WorkspaceDropdown: FC = () => {
|
||||
activeWorkspace,
|
||||
activeProject,
|
||||
activeApiSpec,
|
||||
activeMockServer,
|
||||
projects,
|
||||
} = useRouteLoaderData(':workspaceId') as WorkspaceLoaderData;
|
||||
const activeWorkspaceName = activeWorkspace.name;
|
||||
@ -254,6 +255,7 @@ export const WorkspaceDropdown: FC = () => {
|
||||
{isSettingsModalOpen && (
|
||||
<WorkspaceSettingsModal
|
||||
workspace={activeWorkspace}
|
||||
mockServer={activeMockServer}
|
||||
onClose={() => setIsSettingsModalOpen(false)}
|
||||
/>
|
||||
)}
|
||||
|
@ -0,0 +1,124 @@
|
||||
import fs from 'fs/promises';
|
||||
import React, { useState } from 'react';
|
||||
import { Button } from 'react-aria-components';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { useRouteLoaderData } from 'react-router-dom';
|
||||
|
||||
import { useMockRoutePatcher } from '../../routes/mock-route';
|
||||
import { RequestLoaderData } from '../../routes/request';
|
||||
import { HelpTooltip } from '../help-tooltip';
|
||||
import { Icon } from '../icon';
|
||||
|
||||
export const MockResponseExtractor = () => {
|
||||
const { mockServerAndRoutes, activeResponse } = useRouteLoaderData('request/:requestId') as RequestLoaderData;
|
||||
const patchMockRoute = useMockRoutePatcher();
|
||||
const navigate = useNavigate();
|
||||
const {
|
||||
organizationId,
|
||||
projectId,
|
||||
} = useParams();
|
||||
const [selectedMockServer, setSelectedMockServer] = useState('');
|
||||
const [selectedMockRoute, setSelectedMockRoute] = useState('');
|
||||
return (
|
||||
<div className="px-32 h-full flex flex-col justify-center">
|
||||
<div className="flex place-content-center text-9xl pb-2">
|
||||
<Icon icon="cube" />
|
||||
</div>
|
||||
<div className="flex place-content-center pb-2">
|
||||
Export this response to a mock route.
|
||||
</div>
|
||||
<form
|
||||
onSubmit={async e => {
|
||||
e.preventDefault();
|
||||
if (!selectedMockServer || !selectedMockRoute) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (activeResponse) {
|
||||
// TODO: move this out of the renderer, and upsert mock
|
||||
const body = await fs.readFile(activeResponse.bodyPath);
|
||||
|
||||
patchMockRoute(selectedMockRoute, {
|
||||
body: body.toString(),
|
||||
mimeType: activeResponse.contentType,
|
||||
statusCode: activeResponse.statusCode,
|
||||
headers: activeResponse.headers,
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="form-row">
|
||||
<div className="form-control form-control--outlined">
|
||||
<label>
|
||||
Choose Mock Server
|
||||
<HelpTooltip position="top" className="space-left">
|
||||
Select from created mock servers to add the route to
|
||||
</HelpTooltip>
|
||||
<select
|
||||
value={selectedMockServer}
|
||||
onChange={event => {
|
||||
const selected = event.currentTarget.value;
|
||||
setSelectedMockServer(selected);
|
||||
}}
|
||||
>
|
||||
<option value="">-- Select... --</option>
|
||||
{mockServerAndRoutes
|
||||
.map(w => (
|
||||
<option key={w._id} value={w._id}>
|
||||
{w.name}
|
||||
</option>
|
||||
))
|
||||
}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<div className="form-control form-control--outlined">
|
||||
<label>
|
||||
Choose Mock Route
|
||||
<HelpTooltip position="top" className="space-left">
|
||||
Select from created mock routes to send this request to
|
||||
</HelpTooltip>
|
||||
<select
|
||||
value={selectedMockRoute}
|
||||
onChange={event => {
|
||||
const selected = event.currentTarget.value;
|
||||
setSelectedMockRoute(selected);
|
||||
}}
|
||||
>
|
||||
<option value="">-- Select... --</option>
|
||||
{mockServerAndRoutes.find(s => s._id === selectedMockServer)?.routes
|
||||
.map(w => (
|
||||
<option key={w._id} value={w._id}>
|
||||
{w.name}
|
||||
</option>
|
||||
))
|
||||
}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
isDisabled={!selectedMockServer || !selectedMockRoute}
|
||||
onPress={() => {
|
||||
const mockWorkspaceId = mockServerAndRoutes.find(s => s._id === selectedMockServer)?.parentId;
|
||||
navigate(`/organization/${organizationId}/project/${projectId}/workspace/${mockWorkspaceId}/mock-server/mock-route/${selectedMockRoute}`);
|
||||
}}
|
||||
className="mr-2 hover:no-underline bg-[--color-surprise] hover:bg-opacity-90 border border-solid border-[--hl-md] py-2 px-3 text-[--color-font-surprise] transition-colors rounded-sm"
|
||||
>
|
||||
Go to mock
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
isDisabled={!selectedMockServer || !selectedMockRoute}
|
||||
className="hover:no-underline bg-[--color-surprise] hover:bg-opacity-90 border border-solid border-[--hl-md] py-2 px-3 text-[--color-font-surprise] transition-colors rounded-sm"
|
||||
>
|
||||
Export
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,95 @@
|
||||
import React, { FC, useCallback } from 'react';
|
||||
import { useParams, useRouteLoaderData } from 'react-router-dom';
|
||||
|
||||
import { getCommonHeaderNames, getCommonHeaderValues } from '../../../common/common-headers';
|
||||
import type { RequestHeader } from '../../../models/request';
|
||||
import { MockRouteLoaderData, useMockRoutePatcher } from '../../routes/mock-route';
|
||||
import { CodeEditor } from '../codemirror/code-editor';
|
||||
import { KeyValueEditor } from '../key-value-editor/key-value-editor';
|
||||
|
||||
interface Props {
|
||||
bulk: boolean;
|
||||
isDisabled?: boolean;
|
||||
onBlur?: (e: FocusEvent) => void;
|
||||
}
|
||||
|
||||
export const MockResponseHeadersEditor: FC<Props> = ({
|
||||
bulk,
|
||||
isDisabled,
|
||||
onBlur,
|
||||
}) => {
|
||||
const { mockRoute } = useRouteLoaderData(':mockRouteId') as MockRouteLoaderData;
|
||||
const patchMockRoute = useMockRoutePatcher();
|
||||
|
||||
const { mockRouteId } = useParams() as { mockRouteId: string };
|
||||
|
||||
const handleBulkUpdate = useCallback((headersString: string) => {
|
||||
const headers: {
|
||||
name: string;
|
||||
value: string;
|
||||
}[] = [];
|
||||
|
||||
const rows = headersString.split(/\n+/);
|
||||
for (const row of rows) {
|
||||
const [rawName, rawValue] = row.split(/:(.*)$/);
|
||||
const name = (rawName || '').trim();
|
||||
const value = (rawValue || '').trim();
|
||||
|
||||
if (!name && !value) {
|
||||
continue;
|
||||
}
|
||||
|
||||
headers.push({
|
||||
name,
|
||||
value,
|
||||
});
|
||||
}
|
||||
patchMockRoute(mockRouteId, { headers });
|
||||
}, [patchMockRoute, mockRouteId]);
|
||||
|
||||
let headersString = '';
|
||||
for (const header of mockRoute.headers) {
|
||||
// Make sure it's not disabled
|
||||
if (header.disabled) {
|
||||
continue;
|
||||
}
|
||||
// Make sure it's not blank
|
||||
if (!header.name && !header.value) {
|
||||
continue;
|
||||
}
|
||||
|
||||
headersString += `${header.name}: ${header.value}\n`;
|
||||
}
|
||||
|
||||
const onChangeHeaders = useCallback((headers: RequestHeader[]) => {
|
||||
patchMockRoute(mockRouteId, { headers });
|
||||
}, [patchMockRoute, mockRouteId]);
|
||||
|
||||
if (bulk) {
|
||||
return (
|
||||
<div className="tall">
|
||||
<CodeEditor
|
||||
id="request-headers-editor"
|
||||
onChange={handleBulkUpdate}
|
||||
defaultValue={headersString}
|
||||
enableNunjucks
|
||||
onBlur={onBlur}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<KeyValueEditor
|
||||
namePlaceholder="header"
|
||||
valuePlaceholder="value"
|
||||
descriptionPlaceholder="description"
|
||||
pairs={mockRoute.headers}
|
||||
handleGetAutocompleteNameConstants={getCommonHeaderNames}
|
||||
handleGetAutocompleteValueConstants={getCommonHeaderValues}
|
||||
onChange={onChangeHeaders}
|
||||
isDisabled={isDisabled}
|
||||
onBlur={onBlur}
|
||||
/>
|
||||
);
|
||||
};
|
@ -41,6 +41,7 @@ interface Props {
|
||||
}[]) => void;
|
||||
pairs: Pair[];
|
||||
valuePlaceholder?: string;
|
||||
onBlur?: (e: FocusEvent) => void;
|
||||
}
|
||||
|
||||
export const KeyValueEditor: FC<Props> = ({
|
||||
@ -56,6 +57,7 @@ export const KeyValueEditor: FC<Props> = ({
|
||||
onChange,
|
||||
pairs,
|
||||
valuePlaceholder,
|
||||
onBlur,
|
||||
}) => {
|
||||
// We should make the pair.id property required and pass them in from the parent
|
||||
// smelly
|
||||
@ -109,6 +111,7 @@ export const KeyValueEditor: FC<Props> = ({
|
||||
descriptionPlaceholder={descriptionPlaceholder}
|
||||
hideButtons
|
||||
readOnly
|
||||
onBlur={onBlur}
|
||||
onClick={() => onChange([...pairs, {
|
||||
// smelly
|
||||
id: generateId('pair'),
|
||||
@ -150,6 +153,7 @@ export const KeyValueEditor: FC<Props> = ({
|
||||
namePlaceholder={namePlaceholder}
|
||||
valuePlaceholder={valuePlaceholder}
|
||||
descriptionPlaceholder={descriptionPlaceholder}
|
||||
onBlur={onBlur}
|
||||
onChange={pair => onChange(pairsWithIds.map(p => (p.id === pair.id ? pair : p)))}
|
||||
onDelete={pair => onChange(pairsWithIds.filter(p => p.id !== pair.id))}
|
||||
handleGetAutocompleteNameConstants={handleGetAutocompleteNameConstants}
|
||||
|
@ -41,6 +41,7 @@ interface Props {
|
||||
onClick?: () => void;
|
||||
onKeydown?: (e: React.KeyboardEvent) => void;
|
||||
showDescription: boolean;
|
||||
onBlur?: (e: FocusEvent) => void;
|
||||
}
|
||||
|
||||
export const Row: FC<Props> = ({
|
||||
@ -60,6 +61,7 @@ export const Row: FC<Props> = ({
|
||||
onKeydown,
|
||||
valuePlaceholder,
|
||||
showDescription,
|
||||
onBlur,
|
||||
}) => {
|
||||
const { enabled } = useNunjucksEnabled();
|
||||
|
||||
@ -92,6 +94,7 @@ export const Row: FC<Props> = ({
|
||||
getAutocompleteConstants={() => handleGetAutocompleteNameConstants?.(pair) || []}
|
||||
readOnly={readOnly}
|
||||
onChange={name => onChange({ ...pair, name })}
|
||||
onBlur={onBlur}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
@ -126,6 +129,7 @@ export const Row: FC<Props> = ({
|
||||
) : (
|
||||
<OneLineEditor
|
||||
id={'key-value-editor__value' + pair.id}
|
||||
onBlur={onBlur}
|
||||
type="text"
|
||||
readOnly={readOnly}
|
||||
placeholder={valuePlaceholder || 'Value'}
|
||||
|
152
packages/insomnia/src/ui/components/mocks/mock-response-pane.tsx
Normal file
152
packages/insomnia/src/ui/components/mocks/mock-response-pane.tsx
Normal file
@ -0,0 +1,152 @@
|
||||
import { AxiosResponse } from 'axios';
|
||||
import React, { Fragment, useEffect, useState } from 'react';
|
||||
import { useRouteLoaderData } from 'react-router-dom';
|
||||
|
||||
import { getMockServiceURL, PREVIEW_MODE_SOURCE } from '../../../common/constants';
|
||||
import { HarRequest } from '../../../common/har';
|
||||
import { ResponseTimelineEntry } from '../../../main/network/libcurl-promise';
|
||||
import * as models from '../../../models';
|
||||
import { MockRouteLoaderData } from '../../routes/mock-route';
|
||||
import { useRootLoaderData } from '../../routes/root';
|
||||
import { TabItem, Tabs } from '../base/tabs';
|
||||
import { CodeEditor } from '../codemirror/code-editor';
|
||||
import { getTimeFromNow } from '../time-from-now';
|
||||
import { ResponseHeadersViewer } from '../viewers/response-headers-viewer';
|
||||
import { ResponseTimelineViewer } from '../viewers/response-timeline-viewer';
|
||||
import { ResponseViewer } from '../viewers/response-viewer';
|
||||
|
||||
interface MockbinLogOutput {
|
||||
log: {
|
||||
version: string;
|
||||
creator: {
|
||||
name: string;
|
||||
version: string;
|
||||
};
|
||||
entries: [
|
||||
{
|
||||
startedDateTime: string;
|
||||
clientIPAddress: string;
|
||||
request: HarRequest;
|
||||
}
|
||||
];
|
||||
};
|
||||
}
|
||||
|
||||
export const MockResponsePane = () => {
|
||||
const { mockServer, mockRoute, activeResponse } = useRouteLoaderData(':mockRouteId') as MockRouteLoaderData;
|
||||
const { settings } = useRootLoaderData();
|
||||
const [logs, setLogs] = useState<MockbinLogOutput | null>(null);
|
||||
const [timeline, setTimeline] = useState<ResponseTimelineEntry[]>([]);
|
||||
const [logEntryId, setLogEntryId] = useState<number | null>(null);
|
||||
|
||||
// refetches logs whenever the path changes, or a response is recieved
|
||||
useEffect(() => {
|
||||
const mockbinUrl = mockServer.useInsomniaCloud ? getMockServiceURL() : mockServer.url;
|
||||
|
||||
const fn = async () => {
|
||||
const compoundId = mockRoute.parentId + mockRoute.name;
|
||||
try {
|
||||
const res: AxiosResponse<MockbinLogOutput> = await window.main.axiosRequest({
|
||||
url: mockbinUrl + `/bin/log/${compoundId}`,
|
||||
method: 'get',
|
||||
});
|
||||
if (res?.data?.log) {
|
||||
setLogs(res.data);
|
||||
return;
|
||||
}
|
||||
console.log('Error: fetching logs from remote', { mockbinUrl, res });
|
||||
} catch (e) {
|
||||
// network erros will be managed by the upsert trigger, so we can ignore them here
|
||||
console.log({ mockbinUrl, e });
|
||||
}
|
||||
};
|
||||
fn();
|
||||
}, [activeResponse?._id, mockRoute.name, mockRoute.parentId, mockServer.url, mockServer.useInsomniaCloud]);
|
||||
|
||||
useEffect(() => {
|
||||
const fn = async () => {
|
||||
if (activeResponse) {
|
||||
const timeline = await models.response.getTimeline(activeResponse, true);
|
||||
setTimeline(timeline);
|
||||
}
|
||||
};
|
||||
fn();
|
||||
}, [activeResponse]);
|
||||
|
||||
return (
|
||||
<Tabs aria-label="Mock response">
|
||||
<TabItem key="history" title="History">
|
||||
<div className="h-full w-full grid grid-rows-[repeat(auto-fit,minmax(0,1fr))]">
|
||||
<div className="w-full flex-1 overflow-hidden box-border overflow-y-scroll">
|
||||
<div className="grid grid-cols-[repeat(5,auto)] divide-solid divide-y divide-[--hl-sm]">
|
||||
<div className="uppercase p-2 bg-[--hl-sm] text-left text-xs font-semibold focus:outline-none">Method</div>
|
||||
<div className="uppercase p-2 bg-[--hl-sm] text-left text-xs font-semibold focus:outline-none">Size</div>
|
||||
<div className="uppercase p-2 bg-[--hl-sm] text-left text-xs font-semibold focus:outline-none">Date</div>
|
||||
<div className="uppercase p-2 bg-[--hl-sm] text-left text-xs font-semibold focus:outline-none">IP</div>
|
||||
<div className="uppercase p-2 bg-[--hl-sm] text-left text-xs font-semibold focus:outline-none">Path</div>
|
||||
{logs?.log.entries?.map((row, index) => (
|
||||
<Fragment key={row.startedDateTime}>
|
||||
<div onClick={() => setLogEntryId(index)} className={`${index % 2 === 0 ? '' : 'bg-[--hl-xs]'} cursor-pointer whitespace-nowrap text-sm truncate font-medium group-last-of-type:border-none focus:outline-none`}>
|
||||
<div className='p-2'>{row.request.method}</div>
|
||||
</div>
|
||||
<div onClick={() => setLogEntryId(index)} className={`${index % 2 === 0 ? '' : 'bg-[--hl-xs]'} cursor-pointer whitespace-nowrap text-sm truncate font-medium group-last-of-type:border-none focus:outline-none`}>
|
||||
<div className='p-2'>{row.request.bodySize + row.request.headersSize}</div></div>
|
||||
<div onClick={() => setLogEntryId(index)} className={`${index % 2 === 0 ? '' : 'bg-[--hl-xs]'} cursor-pointer whitespace-nowrap text-sm truncate font-medium group-last-of-type:border-none focus:outline-none`}>
|
||||
<div className='p-2 truncate'>{getTimeFromNow(row.startedDateTime, false)}</div>
|
||||
</div>
|
||||
<div onClick={() => setLogEntryId(index)} className={`${index % 2 === 0 ? '' : 'bg-[--hl-xs]'} cursor-pointer whitespace-nowrap text-sm truncate font-medium group-last-of-type:border-none focus:outline-none`}>
|
||||
<div className='p-2 truncate'>{row.clientIPAddress}</div>
|
||||
</div>
|
||||
<div onClick={() => setLogEntryId(index)} className={`${index % 2 === 0 ? '' : 'bg-[--hl-xs]'} cursor-pointer whitespace-nowrap truncate text-sm font-medium group-last-of-type:border-none focus:outline-none`}>
|
||||
<div className='p-2 truncate'>{row.request.url}</div>
|
||||
</div>
|
||||
</Fragment>
|
||||
)).reverse()}
|
||||
</div>
|
||||
</div>
|
||||
{logEntryId !== null && logs?.log.entries?.[logEntryId] && (
|
||||
<div className='flex-1 h-full border-solid border border-[--hl-md]'>
|
||||
<CodeEditor
|
||||
id="log-body-preview"
|
||||
key={logEntryId + logs?.log.entries?.[logEntryId].startedDateTime}
|
||||
hideLineNumbers
|
||||
mode={'text/json'}
|
||||
defaultValue={JSON.stringify(logs?.log.entries?.[logEntryId], null, '\t')}
|
||||
readOnly
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</TabItem>
|
||||
<TabItem key="preview" title="Preview">
|
||||
{activeResponse && <ResponseViewer
|
||||
key={activeResponse._id}
|
||||
bytes={Math.max(activeResponse.bytesContent, activeResponse.bytesRead)}
|
||||
contentType={activeResponse.contentType || ''}
|
||||
disableHtmlPreviewJs={settings.disableHtmlPreviewJs}
|
||||
disablePreviewLinks={settings.disableResponsePreviewLinks}
|
||||
download={() => { }}
|
||||
editorFontSize={settings.editorFontSize}
|
||||
error={activeResponse.error}
|
||||
filter={''}
|
||||
filterHistory={[]}
|
||||
getBody={() => models.response.getBodyBuffer(activeResponse)}
|
||||
previewMode={PREVIEW_MODE_SOURCE}
|
||||
responseId={activeResponse._id}
|
||||
updateFilter={activeResponse.error ? undefined : () => { }}
|
||||
url={activeResponse.url}
|
||||
/>}
|
||||
</TabItem>
|
||||
<TabItem key="headers" title="Headers">
|
||||
<ResponseHeadersViewer headers={activeResponse?.headers || []} />
|
||||
</TabItem>
|
||||
<TabItem key="timeline" title="Timeline">
|
||||
<ResponseTimelineViewer
|
||||
key={activeResponse?._id}
|
||||
timeline={timeline}
|
||||
pinToBottom={true}
|
||||
/>
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
);
|
||||
};
|
72
packages/insomnia/src/ui/components/mocks/mock-url-bar.tsx
Normal file
72
packages/insomnia/src/ui/components/mocks/mock-url-bar.tsx
Normal file
@ -0,0 +1,72 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Button } from 'react-aria-components';
|
||||
import { useRouteLoaderData } from 'react-router-dom';
|
||||
|
||||
import { getMockServiceURL, HTTP_METHODS } from '../../../common/constants';
|
||||
import { MockRouteLoaderData, useMockRoutePatcher } from '../../routes/mock-route';
|
||||
import { Dropdown, DropdownButton, DropdownItem, ItemContent } from '../base/dropdown';
|
||||
import { Icon } from '../icon';
|
||||
import { showModal } from '../modals';
|
||||
import { AlertModal } from '../modals/alert-modal';
|
||||
|
||||
export const MockUrlBar = ({ onPathUpdate, onSend }: { onPathUpdate: (path: string) => void; onSend: (path: string) => void }) => {
|
||||
const { mockServer, mockRoute } = useRouteLoaderData(':mockRouteId') as MockRouteLoaderData;
|
||||
const patchMockRoute = useMockRoutePatcher();
|
||||
const [pathInput, setPathInput] = useState<string>(mockRoute.name);
|
||||
const mockbinUrl = mockServer.useInsomniaCloud ? getMockServiceURL() : mockServer.url;
|
||||
return (<div className='w-full flex justify-between urlbar'>
|
||||
<Dropdown
|
||||
className="method-dropdown"
|
||||
triggerButton={
|
||||
<DropdownButton className="pad-right pad-left vertically-center hover:bg-[--color-surprise] focus:bg-[--color-surprise]">
|
||||
<span className={`http-method-${mockRoute.method}`}>{mockRoute.method}</span>{' '}
|
||||
<i className="fa fa-caret-down space-left" />
|
||||
</DropdownButton>
|
||||
}
|
||||
>{HTTP_METHODS.map(method => (
|
||||
<DropdownItem key={method}>
|
||||
<ItemContent
|
||||
className={`http-method-${method}`}
|
||||
label={method}
|
||||
onClick={() => patchMockRoute(mockRoute._id, { method })}
|
||||
/>
|
||||
</DropdownItem>
|
||||
))}
|
||||
</Dropdown>
|
||||
<div className='flex p-1'>
|
||||
<Button
|
||||
className="bg-[--hl-sm] px-3 rounded-sm"
|
||||
onPress={() => {
|
||||
const compoundId = mockRoute.parentId + pathInput;
|
||||
showModal(AlertModal, {
|
||||
title: 'Full URL',
|
||||
message: mockbinUrl + '/bin/' + compoundId,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Icon icon="eye" /> Show URL
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className='flex flex-1 p-1 items-center'>
|
||||
<input className='flex-1' onBlur={() => onPathUpdate(pathInput)} value={pathInput} onChange={e => setPathInput(e.currentTarget.value)} />
|
||||
</div>
|
||||
<div className='flex p-1'>
|
||||
<Button
|
||||
className="bg-[--hl-sm] px-3 rounded-sm"
|
||||
onPress={() => {
|
||||
const compoundId = mockRoute.parentId + pathInput;
|
||||
window.clipboard.writeText(mockbinUrl + '/bin/' + compoundId);
|
||||
}}
|
||||
>
|
||||
<Icon icon="copy" />
|
||||
</Button>
|
||||
<Button
|
||||
className="px-5 ml-1 text-[--color-font-surprise] bg-[--color-surprise] hover:bg-opacity-90 rounded-sm"
|
||||
onPress={() => onSend(pathInput)}
|
||||
>
|
||||
Test
|
||||
</Button>
|
||||
</div>
|
||||
</div>);
|
||||
};
|
@ -43,7 +43,7 @@ export const AlertModal = forwardRef<AlertModalHandle, ModalProps>((_, ref) => {
|
||||
|
||||
const { message, title, addCancel, okLabel } = state;
|
||||
return (
|
||||
<Modal ref={modalRef} skinny>
|
||||
<Modal ref={modalRef}>
|
||||
<ModalHeader>{title || 'Uh Oh!'}</ModalHeader>
|
||||
<ModalBody className="wide pad">{message}</ModalBody>
|
||||
<ModalFooter>
|
||||
|
@ -1,13 +1,15 @@
|
||||
import React from 'react';
|
||||
import { Button, Dialog, Heading, Input, Label, Modal, ModalOverlay } from 'react-aria-components';
|
||||
import { Button, Dialog, Heading, Input, Label, Modal, ModalOverlay, Radio, RadioGroup, TextField } from 'react-aria-components';
|
||||
import { useFetcher } from 'react-router-dom';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import { database as db } from '../../../common/database';
|
||||
import { getWorkspaceLabel } from '../../../common/get-workspace-label';
|
||||
import * as models from '../../../models/index';
|
||||
import { MockServer } from '../../../models/mock-server';
|
||||
import { isRequest } from '../../../models/request';
|
||||
import { isScratchpad, Workspace } from '../../../models/workspace';
|
||||
import { Link } from '../base/link';
|
||||
import { PromptButton } from '../base/prompt-button';
|
||||
import { Icon } from '../icon';
|
||||
import { MarkdownEditor } from '../markdown-editor';
|
||||
@ -15,9 +17,10 @@ import { MarkdownEditor } from '../markdown-editor';
|
||||
interface Props {
|
||||
onClose: () => void;
|
||||
workspace: Workspace;
|
||||
mockServer: MockServer | null;
|
||||
}
|
||||
|
||||
export const WorkspaceSettingsModal = ({ workspace, onClose }: Props) => {
|
||||
export const WorkspaceSettingsModal = ({ workspace, mockServer, onClose }: Props) => {
|
||||
const hasDescription = !!workspace.description;
|
||||
const isScratchpadWorkspace = isScratchpad(workspace);
|
||||
|
||||
@ -25,6 +28,7 @@ export const WorkspaceSettingsModal = ({ workspace, onClose }: Props) => {
|
||||
|
||||
const { organizationId, projectId } = useParams<{ organizationId: string; projectId: string }>();
|
||||
const workspaceFetcher = useFetcher();
|
||||
const mockServerFetcher = useFetcher();
|
||||
const workspacePatcher = (workspaceId: string, patch: Partial<Workspace>) => {
|
||||
workspaceFetcher.submit({ ...patch, workspaceId }, {
|
||||
action: `/organization/${organizationId}/project/${projectId}/workspace/update`,
|
||||
@ -32,6 +36,13 @@ export const WorkspaceSettingsModal = ({ workspace, onClose }: Props) => {
|
||||
encType: 'application/json',
|
||||
});
|
||||
};
|
||||
const mockServerPatcher = (mockServerId: string, patch: Partial<MockServer>) => {
|
||||
mockServerFetcher.submit({ ...patch, mockServerId }, {
|
||||
action: `/organization/${organizationId}/project/${projectId}/workspace/${workspace._id}/mock-server/update`,
|
||||
method: 'post',
|
||||
encType: 'application/json',
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<ModalOverlay
|
||||
@ -63,45 +74,97 @@ export const WorkspaceSettingsModal = ({ workspace, onClose }: Props) => {
|
||||
</Button>
|
||||
</div>
|
||||
<div className='rounded flex-1 w-full overflow-hidden basis-96 flex flex-col gap-2 select-none overflow-y-auto'>
|
||||
<Label className='flex flex-col gap-1 px-1' aria-label='Host'>
|
||||
<span>Name</span>
|
||||
<Input
|
||||
name='name'
|
||||
type='text'
|
||||
required
|
||||
readOnly={isScratchpadWorkspace}
|
||||
defaultValue={activeWorkspaceName}
|
||||
placeholder='Awesome API'
|
||||
className='p-2 w-full rounded-sm border border-solid border-[--hl-sm] bg-[--color-bg] text-[--color-font] focus:outline-none focus:ring-1 focus:ring-[--hl-md] transition-colors'
|
||||
onChange={event => workspacePatcher(workspace._id, { name: event.target.value })}
|
||||
/>
|
||||
</Label>
|
||||
<Label className='flex flex-col px-1 gap-1' aria-label='Description'>
|
||||
<span>Description</span>
|
||||
<MarkdownEditor
|
||||
defaultPreviewMode={hasDescription}
|
||||
placeholder="Write a description"
|
||||
defaultValue={workspace.description}
|
||||
onChange={(description: string) => {
|
||||
workspacePatcher(workspace._id, { description });
|
||||
}}
|
||||
/>
|
||||
<Label className='text-sm text-[--hl]'>
|
||||
Name
|
||||
</Label>
|
||||
<Input
|
||||
name='name'
|
||||
type='text'
|
||||
required
|
||||
readOnly={isScratchpadWorkspace}
|
||||
defaultValue={activeWorkspaceName}
|
||||
placeholder='Awesome API'
|
||||
className='p-2 w-full rounded-sm border border-solid border-[--hl-sm] bg-[--color-bg] text-[--color-font] focus:outline-none focus:ring-1 focus:ring-[--hl-md] transition-colors'
|
||||
onChange={event => workspacePatcher(workspace._id, { name: event.target.value })}
|
||||
/>
|
||||
{workspace.scope !== 'mock-server' && (
|
||||
<>
|
||||
<Label className='text-sm text-[--hl]' aria-label='Description'>
|
||||
Description
|
||||
</Label>
|
||||
<MarkdownEditor
|
||||
defaultPreviewMode={hasDescription}
|
||||
placeholder="Write a description"
|
||||
defaultValue={workspace.description}
|
||||
onChange={(description: string) => {
|
||||
workspacePatcher(workspace._id, { description });
|
||||
}}
|
||||
/>
|
||||
<Heading>Actions</Heading>
|
||||
<PromptButton
|
||||
onClick={async () => {
|
||||
const docs = await db.withDescendants(workspace, models.request.type);
|
||||
const requests = docs.filter(isRequest);
|
||||
for (const req of requests) {
|
||||
await models.response.removeForRequest(req._id);
|
||||
}
|
||||
close();
|
||||
}}
|
||||
className="width-auto btn btn--clicky inline-block space-left"
|
||||
>
|
||||
<i className="fa fa-trash-o" /> Clear All Responses
|
||||
</PromptButton>
|
||||
</>)}
|
||||
{Boolean(workspace.scope === 'mock-server' && mockServer) && (
|
||||
<>
|
||||
<RadioGroup onChange={value => mockServer && mockServerPatcher(mockServer._id, { useInsomniaCloud: value === 'remote' })} name="type" defaultValue={mockServer?.useInsomniaCloud ? 'remote' : 'local'} className="flex flex-col gap-2">
|
||||
<Label className="text-sm text-[--hl]">
|
||||
Mock type
|
||||
</Label>
|
||||
<div className="flex gap-2">
|
||||
<Radio
|
||||
value="remote"
|
||||
className="data-[selected]:border-[--color-surprise] flex-1 data-[selected]:ring-2 data-[selected]:ring-[--color-surprise] hover:bg-[--hl-xs] focus:bg-[--hl-sm] border border-solid border-[--hl-md] rounded p-4 focus:outline-none transition-colors"
|
||||
>
|
||||
<Icon icon="globe" />
|
||||
<Heading className="text-lg font-bold">Cloud Mock</Heading>
|
||||
<p className='pt-2'>
|
||||
The mock server runs on Insomnia cloud. Ideal for collaboration.
|
||||
</p>
|
||||
</Radio>
|
||||
<Radio
|
||||
value="local"
|
||||
className="data-[selected]:border-[--color-surprise] flex-1 data-[disabled]:opacity-25 data-[selected]:ring-2 data-[selected]:ring-[--color-surprise] hover:bg-[--hl-xs] focus:bg-[--hl-sm] border border-solid border-[--hl-md] rounded p-4 focus:outline-none transition-colors"
|
||||
>
|
||||
<Icon icon="laptop" />
|
||||
<Heading className="text-lg font-bold">Local Mock</Heading>
|
||||
<p className="pt-2">
|
||||
The mock server runs locally.
|
||||
</p>
|
||||
</Radio>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
{!mockServer?.useInsomniaCloud && <TextField
|
||||
autoFocus
|
||||
name="name"
|
||||
defaultValue={mockServer?.url || ''}
|
||||
className={`group relative flex-1 flex flex-col gap-2 ${mockServer?.useInsomniaCloud ? 'disabled' : ''}`}
|
||||
>
|
||||
<Label className='text-sm text-[--hl]'>
|
||||
Local mock server URL
|
||||
</Label>
|
||||
<Input
|
||||
placeholder="http://localhost:8080"
|
||||
onChange={e => mockServer && mockServerPatcher(mockServer._id, { url: e.target.value })}
|
||||
className="py-1 placeholder:italic w-full pl-2 pr-7 rounded-sm border border-solid border-[--hl-sm] bg-[--color-bg] text-[--color-font] focus:outline-none focus:ring-1 focus:ring-[--hl-md] transition-colors"
|
||||
/>
|
||||
<Label className='text-sm text-[--hl]'>
|
||||
You can run a mock server locally. <Link href="https://github.com/Kong/mockbin" className='underline'>Learn more</Link>
|
||||
</Label>
|
||||
</TextField>}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<Heading>Actions</Heading>
|
||||
<PromptButton
|
||||
onClick={async () => {
|
||||
const docs = await db.withDescendants(workspace, models.request.type);
|
||||
const requests = docs.filter(isRequest);
|
||||
for (const req of requests) {
|
||||
await models.response.removeForRequest(req._id);
|
||||
}
|
||||
close();
|
||||
}}
|
||||
className="width-auto btn btn--clicky inline-block space-left"
|
||||
>
|
||||
<i className="fa fa-trash-o" /> Clear All Responses
|
||||
</PromptButton>
|
||||
<div className='flex items-center gap-2 justify-end'>
|
||||
<Button
|
||||
onPress={close}
|
||||
|
@ -73,11 +73,12 @@ const AlmostSquareButton = styled(Button)({
|
||||
interface Props {
|
||||
createRequestCollection: () => void;
|
||||
createDesignDocument: () => void;
|
||||
createMockServer: () => void;
|
||||
importFrom: () => void;
|
||||
cloneFromGit: () => void;
|
||||
}
|
||||
|
||||
export const EmptyStatePane: FC<Props> = ({ createRequestCollection, createDesignDocument, importFrom, cloneFromGit }) => {
|
||||
export const EmptyStatePane: FC<Props> = ({ createRequestCollection, createDesignDocument, createMockServer, importFrom, cloneFromGit }) => {
|
||||
const { organizationId } = useParams<{ organizationId: string }>();
|
||||
const { organizations } = useOrganizationLoaderData();
|
||||
const currentOrg = organizations.find(organization => (organization.id === organizationId));
|
||||
@ -144,6 +145,16 @@ export const EmptyStatePane: FC<Props> = ({ createRequestCollection, createDesig
|
||||
}}
|
||||
/> New Document
|
||||
</SquareButton>
|
||||
<SquareButton
|
||||
onClick={createMockServer}
|
||||
>
|
||||
<i
|
||||
className='fa fa-server'
|
||||
style={{
|
||||
fontSize: 'var(--font-size-xl)',
|
||||
}}
|
||||
/> New Mock Server
|
||||
</SquareButton>
|
||||
</div>
|
||||
<Divider
|
||||
style={{
|
||||
|
@ -76,7 +76,6 @@ export const RequestPane: FC<Props> = ({
|
||||
useState(false);
|
||||
const patchRequest = useRequestPatcher();
|
||||
|
||||
useState(false);
|
||||
const handleImportQueryFromUrl = () => {
|
||||
let query;
|
||||
|
||||
@ -122,6 +121,7 @@ export const RequestPane: FC<Props> = ({
|
||||
const contentType =
|
||||
getContentTypeFromHeaders(activeRequest.headers) ||
|
||||
activeRequest.body.mimeType;
|
||||
|
||||
return (
|
||||
<Pane type="request">
|
||||
<PaneHeader>
|
||||
|
@ -14,6 +14,7 @@ import { useRootLoaderData } from '../../routes/root';
|
||||
import { PanelContainer, TabItem, Tabs } from '../base/tabs';
|
||||
import { PreviewModeDropdown } from '../dropdowns/preview-mode-dropdown';
|
||||
import { ResponseHistoryDropdown } from '../dropdowns/response-history-dropdown';
|
||||
import { MockResponseExtractor } from '../editors/mock-response-extractor';
|
||||
import { ErrorBoundary } from '../error-boundary';
|
||||
import { showError } from '../modals';
|
||||
import { ResponseTimer } from '../response-timer';
|
||||
@ -223,6 +224,9 @@ export const ResponsePane: FC<Props> = ({
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
</TabItem>
|
||||
<TabItem key="mock-response" title="Mock Response">
|
||||
<MockResponseExtractor />
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
<ErrorBoundary errorClassName="font-error pad text-center">
|
||||
{runningRequests[activeRequest._id] && <ResponseTimer
|
||||
|
@ -16,7 +16,7 @@ const toTitleCase = (value: string) => (
|
||||
.join(' ')
|
||||
);
|
||||
|
||||
function getTimeFromNow(timestamp: string | number | Date, titleCase: boolean): string {
|
||||
export function getTimeFromNow(timestamp: string | number | Date, titleCase: boolean): string {
|
||||
const date = new Date(timestamp);
|
||||
let text = formatDistanceToNowStrict(date, { addSuffix: true });
|
||||
const now = new Date();
|
||||
|
@ -44,13 +44,6 @@ const EventViewWrapper = styled.div({
|
||||
height: '100%',
|
||||
});
|
||||
|
||||
const PaneBodyContent = styled.div({
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
display: 'grid',
|
||||
gridTemplateRows: 'repeat(auto-fit, minmax(0, 1fr))',
|
||||
});
|
||||
|
||||
const EventSearchFormControl = styled.div({
|
||||
outline: 'none',
|
||||
width: '100%',
|
||||
@ -180,7 +173,7 @@ const RealtimeActiveResponsePane: FC<{ response: WebSocketResponse | Response }>
|
||||
</PaneHeader>
|
||||
<Tabs aria-label="Curl response pane tabs">
|
||||
<TabItem key="events" title="Events">
|
||||
<PaneBodyContent>
|
||||
<div className='h-full w-full grid grid-rows-[repeat(auto-fit,minmax(0,1fr))]'>
|
||||
{response.error ? <ResponseErrorViewer url={response.url} error={response.error} />
|
||||
: <>
|
||||
<EventLogTableWrapper>
|
||||
@ -246,7 +239,7 @@ const RealtimeActiveResponsePane: FC<{ response: WebSocketResponse | Response }>
|
||||
</EventViewWrapper>
|
||||
)}
|
||||
</>}
|
||||
</PaneBodyContent>
|
||||
</div>
|
||||
</TabItem>
|
||||
<TabItem
|
||||
key="headers"
|
||||
|
@ -42,6 +42,7 @@ const Workspace = lazy(() => import('./routes/workspace'));
|
||||
const UnitTest = lazy(() => import('./routes/unit-test'));
|
||||
const Debug = lazy(() => import('./routes/debug'));
|
||||
const Design = lazy(() => import('./routes/design'));
|
||||
const MockServer = lazy(() => import('./routes/mock-server'));
|
||||
|
||||
initializeSentry();
|
||||
initializeLogging();
|
||||
@ -357,6 +358,13 @@ const router = createMemoryRouter(
|
||||
await import('./routes/request')
|
||||
).createRequestAction(...args),
|
||||
},
|
||||
{
|
||||
path: 'request/new-mock-send',
|
||||
action: async (...args) =>
|
||||
(
|
||||
await import('./routes/request')
|
||||
).createAndSendToMockbinAction(...args),
|
||||
},
|
||||
{
|
||||
path: 'request/delete',
|
||||
action: async (...args) =>
|
||||
@ -425,6 +433,64 @@ const router = createMemoryRouter(
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'mock-server',
|
||||
id: 'mock-server',
|
||||
loader: async (...args) =>
|
||||
(await import('./routes/mock-server')).loader(
|
||||
...args,
|
||||
),
|
||||
element: (
|
||||
<Suspense fallback={<AppLoadingIndicator />}>
|
||||
<MockServer />
|
||||
</Suspense>
|
||||
),
|
||||
children: [
|
||||
{
|
||||
path: 'update',
|
||||
action: async (...args) =>
|
||||
(
|
||||
await import('./routes/actions')
|
||||
).updateMockServerAction(...args),
|
||||
},
|
||||
{
|
||||
path: 'mock-route',
|
||||
id: 'mock-route',
|
||||
children: [
|
||||
{
|
||||
path: ':mockRouteId',
|
||||
id: ':mockRouteId',
|
||||
loader: async (...args) =>
|
||||
(
|
||||
await import('./routes/mock-route')
|
||||
).loader(...args),
|
||||
element: <Outlet />,
|
||||
},
|
||||
{
|
||||
path: 'new',
|
||||
action: async (...args) =>
|
||||
(
|
||||
await import('./routes/actions')
|
||||
).createMockRouteAction(...args),
|
||||
},
|
||||
{
|
||||
path: ':mockRouteId/update',
|
||||
action: async (...args) =>
|
||||
(
|
||||
await import('./routes/actions')
|
||||
).updateMockRouteAction(...args),
|
||||
},
|
||||
{
|
||||
path: ':mockRouteId/delete',
|
||||
action: async (...args) =>
|
||||
(
|
||||
await import('./routes/actions')
|
||||
).deleteMockRouteAction(...args),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'cacert',
|
||||
children: [
|
||||
|
@ -5,7 +5,7 @@ import { ActionFunction, redirect } from 'react-router-dom';
|
||||
|
||||
import * as session from '../../account/session';
|
||||
import { parseApiSpec, resolveComponentSchemaRefs } from '../../common/api-specs';
|
||||
import { ACTIVITY_DEBUG, ACTIVITY_SPEC, getAIServiceURL } from '../../common/constants';
|
||||
import { ACTIVITY_DEBUG, getAIServiceURL } from '../../common/constants';
|
||||
import { database } from '../../common/database';
|
||||
import { database as db } from '../../common/database';
|
||||
import { importResourcesToWorkspace, scanResources } from '../../common/import';
|
||||
@ -16,7 +16,7 @@ import { isRemoteProject } from '../../models/project';
|
||||
import { isRequest, Request } from '../../models/request';
|
||||
import { isRequestGroup, isRequestGroupId } from '../../models/request-group';
|
||||
import { UnitTest } from '../../models/unit-test';
|
||||
import { isCollection, Workspace } from '../../models/workspace';
|
||||
import { isCollection, scopeToActivity, Workspace } from '../../models/workspace';
|
||||
import { WorkspaceMeta } from '../../models/workspace-meta';
|
||||
import { getSendRequestCallback } from '../../network/unit-test-feature';
|
||||
import { initializeLocalBackendProjectAndMarkForSync } from '../../sync/vcs/initialize-backend-project';
|
||||
@ -279,7 +279,7 @@ export const createNewWorkspaceAction: ActionFunction = async ({
|
||||
invariant(typeof name === 'string', 'Name is required');
|
||||
|
||||
const scope = formData.get('scope');
|
||||
invariant(scope === 'design' || scope === 'collection', 'Scope is required');
|
||||
invariant(scope === 'design' || scope === 'collection' || scope === 'mock-server', 'Scope is required');
|
||||
|
||||
const flushId = await database.bufferChanges();
|
||||
|
||||
@ -289,6 +289,12 @@ export const createNewWorkspaceAction: ActionFunction = async ({
|
||||
parentId: projectId,
|
||||
});
|
||||
|
||||
if (scope === 'mock-server') {
|
||||
// create a mock server under the workspace with the same name
|
||||
await models.mockServer.getOrCreateForParentId(workspace._id, { name });
|
||||
return redirect(`/organization/${organizationId}/project/${projectId}/workspace/${workspace._id}/${scopeToActivity(workspace.scope)}`);
|
||||
}
|
||||
|
||||
if (scope === 'design') {
|
||||
await models.apiSpec.getOrCreateForParentId(workspace._id);
|
||||
}
|
||||
@ -313,10 +319,7 @@ export const createNewWorkspaceAction: ActionFunction = async ({
|
||||
: SegmentEvent.documentCreate,
|
||||
});
|
||||
|
||||
return redirect(
|
||||
`/organization/${organizationId}/project/${projectId}/workspace/${workspace._id}/${workspace.scope === 'collection' ? ACTIVITY_DEBUG : ACTIVITY_SPEC
|
||||
}`
|
||||
);
|
||||
return redirect(`/organization/${organizationId}/project/${projectId}/workspace/${workspace._id}/${scopeToActivity(workspace.scope)}`);
|
||||
};
|
||||
|
||||
export const deleteWorkspaceAction: ActionFunction = async ({
|
||||
@ -409,11 +412,8 @@ export const duplicateWorkspaceAction: ActionFunction = async ({ request, params
|
||||
} catch (e) {
|
||||
console.warn('Failed to initialize local backend project', e);
|
||||
}
|
||||
|
||||
return redirect(
|
||||
`/organization/${organizationId}/project/${projectId}/workspace/${newWorkspace._id}/${newWorkspace.scope === 'collection' ? ACTIVITY_DEBUG : ACTIVITY_SPEC
|
||||
}`
|
||||
);
|
||||
const activity = scopeToActivity(newWorkspace.scope);
|
||||
return redirect(`/organization/${organizationId}/project/${projectId}/workspace/${newWorkspace._id}/${activity}`);
|
||||
};
|
||||
|
||||
export const updateWorkspaceAction: ActionFunction = async ({ request }) => {
|
||||
@ -431,6 +431,14 @@ export const updateWorkspaceAction: ActionFunction = async ({ request }) => {
|
||||
fileName: patch.name || workspace.name,
|
||||
});
|
||||
}
|
||||
if (workspace.scope === 'mock-server') {
|
||||
const mockServer = await models.mockServer.getByParentId(workspaceId);
|
||||
invariant(mockServer, 'No MockServer found for this workspace');
|
||||
|
||||
await models.mockServer.update(mockServer, {
|
||||
name: patch.name || workspace.name,
|
||||
});
|
||||
}
|
||||
|
||||
await models.workspace.update(workspace, patch);
|
||||
|
||||
@ -1190,3 +1198,46 @@ export const reorderCollectionAction: ActionFunction = async ({ request, params
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const createMockRouteAction: ActionFunction = async ({ request }) => {
|
||||
const patch = await request.json();
|
||||
invariant(typeof patch.name === 'string', 'Name is required');
|
||||
invariant(typeof patch.parentId === 'string', 'parentId is required');
|
||||
const mockRoute = await models.mockRoute.create(patch);
|
||||
// create a single hidden request under the mock route for testing the mock endpoint
|
||||
await models.request.create({ parentId: mockRoute._id, isPrivate: true });
|
||||
return null;
|
||||
};
|
||||
export const updateMockRouteAction: ActionFunction = async ({ request, params }) => {
|
||||
const { mockRouteId } = params;
|
||||
invariant(typeof mockRouteId === 'string', 'Mock route id is required');
|
||||
const patch = await request.json();
|
||||
|
||||
const mockRoute = await models.mockRoute.getById(mockRouteId);
|
||||
invariant(mockRoute, 'Mock route is required');
|
||||
|
||||
await models.mockRoute.update(mockRoute, patch);
|
||||
return null;
|
||||
};
|
||||
export const deleteMockRouteAction: ActionFunction = async ({ request, params }) => {
|
||||
const { organizationId, projectId, workspaceId, mockRouteId } = params;
|
||||
invariant(typeof mockRouteId === 'string', 'Mock route id is required');
|
||||
const mockRoute = await models.mockRoute.getById(mockRouteId);
|
||||
invariant(mockRoute, 'mockRoute not found');
|
||||
const { isSelected } = await request.json();
|
||||
|
||||
await models.mockRoute.remove(mockRoute);
|
||||
if (isSelected) {
|
||||
return redirect(`/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/mock-server`);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
export const updateMockServerAction: ActionFunction = async ({ request, params }) => {
|
||||
const { workspaceId } = params;
|
||||
invariant(typeof workspaceId === 'string', 'Workspace ID is required');
|
||||
const patch = await request.json();
|
||||
const mockServer = await models.mockServer.getByParentId(workspaceId);
|
||||
invariant(mockServer, 'Mock server not found');
|
||||
await models.mockServer.update(mockServer, patch);
|
||||
return null;
|
||||
};
|
||||
|
271
packages/insomnia/src/ui/routes/mock-route.tsx
Normal file
271
packages/insomnia/src/ui/routes/mock-route.tsx
Normal file
@ -0,0 +1,271 @@
|
||||
import { AxiosResponse } from 'axios';
|
||||
import React from 'react';
|
||||
import { LoaderFunction, useFetcher, useParams, useRouteLoaderData } from 'react-router-dom';
|
||||
|
||||
import { CONTENT_TYPE_JSON, CONTENT_TYPE_PLAINTEXT, CONTENT_TYPE_XML, CONTENT_TYPE_YAML, contentTypesMap, getMockServiceURL, RESPONSE_CODE_REASONS } from '../../common/constants';
|
||||
import { database as db } from '../../common/database';
|
||||
import { getResponseCookiesFromHeaders, HarResponse } from '../../common/har';
|
||||
import * as models from '../../models';
|
||||
import { MockRoute } from '../../models/mock-route';
|
||||
import { MockServer } from '../../models/mock-server';
|
||||
import { Request, RequestHeader } from '../../models/request';
|
||||
import { Response } from '../../models/response';
|
||||
import { invariant } from '../../utils/invariant';
|
||||
import { Dropdown, DropdownButton, DropdownItem, ItemContent } from '../components/base/dropdown';
|
||||
import { PanelContainer, TabItem, Tabs } from '../components/base/tabs';
|
||||
import { CodeEditor } from '../components/codemirror/code-editor';
|
||||
import { MockResponseHeadersEditor } from '../components/editors/mock-response-headers-editor';
|
||||
import { MockResponsePane } from '../components/mocks/mock-response-pane';
|
||||
import { MockUrlBar } from '../components/mocks/mock-url-bar';
|
||||
import { showAlert } from '../components/modals';
|
||||
import { EmptyStatePane } from '../components/panes/empty-state-pane';
|
||||
import { Pane, PaneBody, PaneHeader } from '../components/panes/pane';
|
||||
import { SvgIcon } from '../components/svg-icon';
|
||||
|
||||
export interface MockRouteLoaderData {
|
||||
mockServer: MockServer;
|
||||
mockRoute: MockRoute;
|
||||
activeResponse?: Response;
|
||||
}
|
||||
|
||||
export const loader: LoaderFunction = async ({ params }): Promise<MockRouteLoaderData> => {
|
||||
const { organizationId, projectId, workspaceId, mockRouteId } = params;
|
||||
invariant(organizationId, 'Organization ID is required');
|
||||
invariant(projectId, 'Project ID is required');
|
||||
invariant(workspaceId, 'Workspace ID is required');
|
||||
invariant(mockRouteId, 'Mock route ID is required');
|
||||
const mockServer = await models.mockServer.getByParentId(workspaceId);
|
||||
invariant(mockServer, 'Mock server is required');
|
||||
const mockRoute = await models.mockRoute.getById(mockRouteId);
|
||||
invariant(mockRoute, 'Mock route is required');
|
||||
// get current response via request children of
|
||||
// TODO: use the same request for try mock rather than creating lots of child requests
|
||||
const reqIds = (await models.request.findByParentId(mockRouteId)).map(r => r._id);
|
||||
|
||||
const responses = await db.findMostRecentlyModified<Response>(models.response.type, { parentId: { $in: reqIds } });
|
||||
return {
|
||||
mockServer,
|
||||
mockRoute,
|
||||
activeResponse: responses?.[0],
|
||||
};
|
||||
};
|
||||
|
||||
const mockContentTypes = [
|
||||
CONTENT_TYPE_PLAINTEXT,
|
||||
CONTENT_TYPE_JSON,
|
||||
CONTENT_TYPE_XML,
|
||||
CONTENT_TYPE_YAML,
|
||||
];
|
||||
// mockbin expect a HAR response structure
|
||||
export const mockRouteToHar = ({ statusCode, statusText, mimeType, headersArray, body }: { statusCode: number; statusText: string; mimeType: string; headersArray: RequestHeader[]; body: string }): HarResponse => {
|
||||
const validHeaders = headersArray.filter(({ name }) => !!name);
|
||||
return {
|
||||
status: +statusCode,
|
||||
statusText: statusText || RESPONSE_CODE_REASONS[+statusCode] || '',
|
||||
httpVersion: 'HTTP/1.1',
|
||||
headers: validHeaders,
|
||||
cookies: getResponseCookiesFromHeaders(validHeaders),
|
||||
content: {
|
||||
size: Buffer.byteLength(body),
|
||||
mimeType,
|
||||
text: body,
|
||||
compression: 0,
|
||||
},
|
||||
headersSize: -1,
|
||||
bodySize: -1,
|
||||
redirectURL: '',
|
||||
};
|
||||
};
|
||||
export const useMockRoutePatcher = () => {
|
||||
const { organizationId, projectId, workspaceId } = useParams<{ organizationId: string; projectId: string; workspaceId: string }>();
|
||||
const fetcher = useFetcher();
|
||||
return (id: string, patch: Partial<MockRoute>) => {
|
||||
fetcher.submit(JSON.stringify(patch), {
|
||||
action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/mock-server/mock-route/${id}/update`,
|
||||
method: 'post',
|
||||
encType: 'application/json',
|
||||
});
|
||||
};
|
||||
};
|
||||
interface MockbinResult {
|
||||
data: string; // returns only the mock server id not the path
|
||||
}
|
||||
interface MockbinError {
|
||||
data: { errors: string };
|
||||
}
|
||||
export const MockRouteRoute = () => {
|
||||
const { mockServer, mockRoute } = useRouteLoaderData(':mockRouteId') as MockRouteLoaderData;
|
||||
const patchMockRoute = useMockRoutePatcher();
|
||||
const mockbinUrl = mockServer.useInsomniaCloud ? getMockServiceURL() : mockServer.url;
|
||||
|
||||
const requestFetcher = useFetcher();
|
||||
const { organizationId, projectId, workspaceId } = useParams() as { organizationId: string; projectId: string; workspaceId: string };
|
||||
|
||||
const upsertBinOnRemoteFromResponse = async (compoundId: string | null): Promise<string> => {
|
||||
try {
|
||||
|
||||
const res: AxiosResponse<MockbinResult | MockbinError> = await window.main.axiosRequest({
|
||||
url: mockbinUrl + `/bin/upsert/${compoundId}`,
|
||||
method: 'put',
|
||||
data: mockRouteToHar({
|
||||
statusCode: mockRoute.statusCode,
|
||||
statusText: mockRoute.statusText,
|
||||
headersArray: mockRoute.headers,
|
||||
mimeType: mockRoute.mimeType,
|
||||
body: mockRoute.body,
|
||||
}),
|
||||
});
|
||||
if (typeof res?.data === 'object' && 'errors' in res?.data && typeof res?.data?.errors === 'string') {
|
||||
console.error('error response', res?.data?.errors);
|
||||
return res?.data?.errors;
|
||||
}
|
||||
if (typeof res?.data === 'string') {
|
||||
return '';
|
||||
}
|
||||
console.log('Error: invalid response from remote', { res, mockbinUrl });
|
||||
return 'Invalid response from ' + mockbinUrl;
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
return 'Unhandled error: ' + e.message;
|
||||
}
|
||||
};
|
||||
|
||||
const createandSendPrivateRequest = (patch: Partial<Request>) =>
|
||||
requestFetcher.submit(JSON.stringify(patch),
|
||||
{
|
||||
encType: 'application/json',
|
||||
action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/debug/request/new-mock-send`,
|
||||
method: 'post',
|
||||
});
|
||||
|
||||
const upsertMockbinHar = async (pathInput?: string) => {
|
||||
const compoundId = mockRoute.parentId + pathInput;
|
||||
const error = await upsertBinOnRemoteFromResponse(compoundId);
|
||||
if (error) {
|
||||
showAlert({
|
||||
title: 'Network error',
|
||||
message: (
|
||||
<div>
|
||||
<p>The request failed due to a network error: {mockbinUrl}</p>
|
||||
<pre className="pad-top-sm force-wrap selectable">
|
||||
<code className="wide">{error}</code>
|
||||
</pre>
|
||||
</div>
|
||||
),
|
||||
});
|
||||
return;
|
||||
}
|
||||
patchMockRoute(mockRoute._id, {
|
||||
name: pathInput,
|
||||
});
|
||||
};
|
||||
const onSend = async (pathInput: string) => {
|
||||
await upsertMockbinHar(pathInput);
|
||||
const compoundId = mockRoute.parentId + pathInput;
|
||||
createandSendPrivateRequest({
|
||||
url: mockbinUrl + '/bin/' + compoundId,
|
||||
method: mockRoute.method,
|
||||
parentId: mockRoute._id,
|
||||
});
|
||||
};
|
||||
const onBlurTriggerUpsert = () => upsertMockbinHar(mockRoute.name);
|
||||
return (
|
||||
<Pane type="request">
|
||||
<PaneHeader>
|
||||
<MockUrlBar key={mockRoute._id + mockRoute.name} onSend={onSend} onPathUpdate={upsertMockbinHar} />
|
||||
</PaneHeader>
|
||||
<PaneBody>
|
||||
<Tabs aria-label="Mock response config">
|
||||
<TabItem
|
||||
key="content-type"
|
||||
title={<Dropdown
|
||||
aria-label='Change Body Type'
|
||||
triggerButton={
|
||||
<DropdownButton>
|
||||
{mockRoute.mimeType ? 'Response ' + contentTypesMap[mockRoute.mimeType]?.[0] : 'Response Body'}
|
||||
<i className="fa fa-caret-down space-left" />
|
||||
</DropdownButton>
|
||||
}
|
||||
>
|
||||
{mockContentTypes.map(contentType => (
|
||||
<DropdownItem key={contentType}>
|
||||
<ItemContent
|
||||
label={contentTypesMap[contentType]?.[1]}
|
||||
onClick={() => patchMockRoute(mockRoute._id, { mimeType: contentType })}
|
||||
/>
|
||||
</DropdownItem>
|
||||
))}
|
||||
</Dropdown>
|
||||
}
|
||||
>
|
||||
{mockRoute.mimeType ?
|
||||
(<CodeEditor
|
||||
id="mock-response-body-editor"
|
||||
key={mockRoute._id}
|
||||
showPrettifyButton
|
||||
defaultValue={mockRoute.body}
|
||||
enableNunjucks
|
||||
onChange={body => patchMockRoute(mockRoute._id, { body })}
|
||||
onBlur={onBlurTriggerUpsert}
|
||||
mode={mockRoute.mimeType}
|
||||
placeholder="..."
|
||||
/>) :
|
||||
(<EmptyStatePane
|
||||
icon={<SvgIcon icon="bug" />}
|
||||
documentationLinks={[]}
|
||||
secondaryAction="Set up the mock body and headers you would like to return"
|
||||
title="Choose a mock body to return as a response"
|
||||
/>)}
|
||||
</TabItem>
|
||||
<TabItem key="headers" title="Response Headers">
|
||||
<MockResponseHeadersEditor
|
||||
onBlur={onBlurTriggerUpsert}
|
||||
bulk={false}
|
||||
/>
|
||||
</TabItem>
|
||||
<TabItem key="status" title="Response Status">
|
||||
<PanelContainer className="pad">
|
||||
<div className="form-row">
|
||||
<div className='form-control form-control--outlined'>
|
||||
<label htmlFor="mock-response-status-code-editor">
|
||||
<small>Status Code</small>
|
||||
<input
|
||||
id="mock-response-status-code-editor"
|
||||
type="number"
|
||||
defaultValue={mockRoute.statusCode}
|
||||
onChange={e => patchMockRoute(mockRoute._id, { statusCode: parseInt(e.currentTarget.value, 10) })}
|
||||
onBlur={onBlurTriggerUpsert}
|
||||
placeholder="200"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<div className='form-control form-control--outlined'>
|
||||
<label htmlFor="mock-response-status-text-editor">
|
||||
<small>Status Text</small>
|
||||
<input
|
||||
id="mock-response-status-text-editor"
|
||||
type="string"
|
||||
defaultValue={mockRoute.statusText}
|
||||
onChange={e => patchMockRoute(mockRoute._id, { statusText: e.currentTarget.value })}
|
||||
onBlur={onBlurTriggerUpsert}
|
||||
|
||||
placeholder={RESPONSE_CODE_REASONS[mockRoute.statusCode || 200] || 'Unknown'}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</PanelContainer>
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
</PaneBody>
|
||||
</Pane>
|
||||
);
|
||||
};
|
||||
|
||||
export const MockRouteResponse = () => {
|
||||
return (
|
||||
<MockResponsePane />
|
||||
);
|
||||
};
|
294
packages/insomnia/src/ui/routes/mock-server.tsx
Normal file
294
packages/insomnia/src/ui/routes/mock-server.tsx
Normal file
@ -0,0 +1,294 @@
|
||||
import type { IconName } from '@fortawesome/fontawesome-svg-core';
|
||||
import React, { Suspense } from 'react';
|
||||
import { Breadcrumb, Breadcrumbs, Button, GridList, GridListItem, Menu, MenuTrigger, Popover } from 'react-aria-components';
|
||||
import { LoaderFunction, NavLink, Route, Routes, useFetcher, useLoaderData, useNavigate, useParams } from 'react-router-dom';
|
||||
|
||||
import * as models from '../../models';
|
||||
import { MockRoute } from '../../models/mock-route';
|
||||
import { invariant } from '../../utils/invariant';
|
||||
import { WorkspaceDropdown } from '../components/dropdowns/workspace-dropdown';
|
||||
import { WorkspaceSyncDropdown } from '../components/dropdowns/workspace-sync-dropdown';
|
||||
import { EditableInput } from '../components/editable-input';
|
||||
import { Icon } from '../components/icon';
|
||||
import { showModal, showPrompt } from '../components/modals';
|
||||
import { AskModal } from '../components/modals/ask-modal';
|
||||
import { EmptyStatePane } from '../components/panes/empty-state-pane';
|
||||
import { SidebarLayout } from '../components/sidebar-layout';
|
||||
import { SvgIcon } from '../components/svg-icon';
|
||||
import { formatMethodName } from '../components/tags/method-tag';
|
||||
import { MockRouteResponse, MockRouteRoute, useMockRoutePatcher } from './mock-route';
|
||||
interface LoaderData {
|
||||
mockServerId: string;
|
||||
mockRoutes: MockRoute[];
|
||||
}
|
||||
export const loader: LoaderFunction = async ({ params }): Promise<LoaderData> => {
|
||||
const { organizationId, projectId, workspaceId } = params;
|
||||
invariant(organizationId, 'Organization ID is required');
|
||||
invariant(projectId, 'Project ID is required');
|
||||
invariant(workspaceId, 'Workspace ID is required');
|
||||
|
||||
const activeWorkspace = await models.workspace.getById(workspaceId);
|
||||
invariant(activeWorkspace, 'Workspace not found');
|
||||
const activeMockServer = await models.mockServer.getByParentId(workspaceId);
|
||||
invariant(activeMockServer, 'Mock Server not found');
|
||||
const mockRoutes = await models.mockRoute.findByParentId(activeMockServer._id);
|
||||
|
||||
return {
|
||||
mockServerId: activeMockServer._id,
|
||||
mockRoutes,
|
||||
};
|
||||
};
|
||||
|
||||
const MockServerRoute = () => {
|
||||
const { organizationId, projectId, workspaceId, mockRouteId } = useParams() as {
|
||||
organizationId: string;
|
||||
projectId: string;
|
||||
workspaceId: string;
|
||||
mockRouteId: string;
|
||||
};
|
||||
const { mockServerId, mockRoutes } = useLoaderData() as LoaderData;
|
||||
const fetcher = useFetcher();
|
||||
const navigate = useNavigate();
|
||||
const patchMockRoute = useMockRoutePatcher();
|
||||
const mockRouteActionList: {
|
||||
id: string;
|
||||
name: string;
|
||||
icon: IconName;
|
||||
action: (id: string, name: string) => void;
|
||||
}[] = [
|
||||
{
|
||||
id: 'rename',
|
||||
name: 'Rename',
|
||||
icon: 'edit',
|
||||
action: id => {
|
||||
showPrompt({
|
||||
title: 'Rename mock route',
|
||||
defaultValue: mockRoutes.find(s => s._id === id)?.name,
|
||||
submitName: 'Rename',
|
||||
onComplete: name => {
|
||||
name && patchMockRoute(id, { name });
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'delete-route',
|
||||
name: 'Delete mock route',
|
||||
icon: 'trash',
|
||||
action: (id, name) => {
|
||||
showModal(AskModal, {
|
||||
title: 'Delete route',
|
||||
message: `Do you really want to delete "${name}"?`,
|
||||
yesText: 'Delete',
|
||||
noText: 'Cancel',
|
||||
onDone: async (isYes: boolean) => {
|
||||
if (isYes) {
|
||||
fetcher.submit(
|
||||
{
|
||||
isSelected: mockRouteId === id,
|
||||
},
|
||||
{
|
||||
encType: 'application/json',
|
||||
action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/mock-server/mock-route/${id}/delete`,
|
||||
method: 'POST',
|
||||
}
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
];
|
||||
|
||||
return <SidebarLayout
|
||||
className="new-sidebar"
|
||||
renderPageSidebar={
|
||||
<div className="flex flex-1 flex-col overflow-hidden divide-solid divide-y divide-[--hl-md]">
|
||||
<div className="flex flex-col items-start gap-2 justify-between p-[--padding-sm]">
|
||||
<Breadcrumbs className='flex list-none items-center m-0 p-0 gap-2 font-bold w-full'>
|
||||
<Breadcrumb className="flex select-none items-center gap-2 text-[--color-font] h-full outline-none data-[focused]:outline-none">
|
||||
<NavLink data-testid="project" className="px-1 py-1 aspect-square h-7 flex flex-shrink-0 outline-none data-[focused]:outline-none items-center justify-center gap-2 aria-pressed:bg-[--hl-sm] rounded-sm text-[--color-font] hover:bg-[--hl-xs] focus:ring-inset ring-1 ring-transparent focus:ring-[--hl-md] transition-all text-sm" to={`/organization/${organizationId}/project/${projectId}`}>
|
||||
<Icon className='text-xs' icon="chevron-left" />
|
||||
</NavLink>
|
||||
<span aria-hidden role="separator" className='text-[--hl-lg] h-4 outline outline-1' />
|
||||
</Breadcrumb>
|
||||
<Breadcrumb className="flex truncate select-none items-center gap-2 text-[--color-font] h-full outline-none data-[focused]:outline-none">
|
||||
<WorkspaceDropdown />
|
||||
</Breadcrumb>
|
||||
</Breadcrumbs>
|
||||
</div>
|
||||
<div className="p-[--padding-sm]">
|
||||
<Button
|
||||
className="px-4 py-1 flex items-center justify-center gap-2 aria-pressed:bg-[--hl-sm] rounded-sm text-[--color-font] hover:bg-[--hl-xs] focus:ring-inset ring-1 ring-transparent focus:ring-[--hl-md] transition-all text-sm"
|
||||
onPress={() => {
|
||||
fetcher.submit(
|
||||
{
|
||||
name: '/',
|
||||
parentId: mockServerId,
|
||||
},
|
||||
{
|
||||
encType: 'application/json',
|
||||
method: 'post',
|
||||
action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/mock-server/mock-route/new`,
|
||||
}
|
||||
);
|
||||
}}
|
||||
>
|
||||
<Icon icon="plus" />
|
||||
New Mock Route
|
||||
</Button>
|
||||
</div>
|
||||
<GridList
|
||||
aria-label="Mock Routes"
|
||||
items={mockRoutes.map(route => ({
|
||||
id: route._id,
|
||||
key: route._id,
|
||||
...route,
|
||||
}))}
|
||||
className="overflow-y-auto flex-1 data-[empty]:py-0 py-[--padding-sm]"
|
||||
disallowEmptySelection
|
||||
selectedKeys={[mockRouteId]}
|
||||
selectionMode="single"
|
||||
onSelectionChange={keys => {
|
||||
if (keys !== 'all') {
|
||||
const value = keys.values().next().value;
|
||||
navigate({
|
||||
pathname: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/mock-server/mock-route/${value}`,
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
{item => {
|
||||
return (
|
||||
<GridListItem
|
||||
key={item._id}
|
||||
id={item._id}
|
||||
textValue={item.name}
|
||||
className="group outline-none select-none w-full"
|
||||
>
|
||||
<div className="flex select-none outline-none group-aria-selected:text-[--color-font] relative group-hover:bg-[--hl-xs] group-focus:bg-[--hl-sm] transition-colors gap-2 px-4 items-center h-[--line-height-xs] w-full overflow-hidden text-[--hl]">
|
||||
<span className="group-aria-selected:bg-[--color-surprise] transition-colors top-0 left-0 absolute h-full w-[2px] bg-transparent" />
|
||||
<span
|
||||
className={
|
||||
`w-10 flex-shrink-0 flex text-[0.65rem] rounded-sm border border-solid border-[--hl-sm] items-center justify-center
|
||||
${{
|
||||
'GET': 'text-[--color-font-surprise] bg-[rgba(var(--color-surprise-rgb),0.5)]',
|
||||
'POST': 'text-[--color-font-success] bg-[rgba(var(--color-success-rgb),0.5)]',
|
||||
'HEAD': 'text-[--color-font-info] bg-[rgba(var(--color-info-rgb),0.5)]',
|
||||
'OPTIONS': 'text-[--color-font-info] bg-[rgba(var(--color-info-rgb),0.5)]',
|
||||
'DELETE': 'text-[--color-font-danger] bg-[rgba(var(--color-danger-rgb),0.5)]',
|
||||
'PUT': 'text-[--color-font-warning] bg-[rgba(var(--color-warning-rgb),0.5)]',
|
||||
'PATCH': 'text-[--color-font-notice] bg-[rgba(var(--color-notice-rgb),0.5)]',
|
||||
}[item.method] || 'text-[--color-font] bg-[--hl-md]'}`
|
||||
}
|
||||
>
|
||||
{formatMethodName(item.method)}
|
||||
</span>
|
||||
<EditableInput
|
||||
value={item.name}
|
||||
name="name"
|
||||
ariaLabel="Mock route name"
|
||||
onSingleClick={() => {
|
||||
navigate({
|
||||
pathname: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/mock-server/mock-route/${item._id}`,
|
||||
});
|
||||
}}
|
||||
onSubmit={name => {
|
||||
name && fetcher.submit(
|
||||
{ name },
|
||||
{
|
||||
encType: 'application/json',
|
||||
action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/mock-server/mock-route/${item._id}/update`,
|
||||
method: 'POST',
|
||||
}
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<span className="flex-1" />
|
||||
<MenuTrigger>
|
||||
<Button
|
||||
aria-label="Project Actions"
|
||||
className="opacity-0 items-center hover:opacity-100 focus:opacity-100 data-[pressed]:opacity-100 flex group-focus:opacity-100 group-hover:opacity-100 justify-center h-6 aspect-square data-[pressed]:bg-[--hl-sm] rounded-sm text-[--color-font] hover:bg-[--hl-xs] focus:ring-inset ring-1 ring-transparent focus:ring-[--hl-md] transition-all text-sm"
|
||||
>
|
||||
<Icon icon="caret-down" />
|
||||
</Button>
|
||||
<Popover className="min-w-max">
|
||||
<Menu
|
||||
aria-label="Project Actions Menu"
|
||||
selectionMode="single"
|
||||
onAction={key => {
|
||||
mockRouteActionList
|
||||
.find(({ id }) => key === id)
|
||||
?.action(item._id, item.name);
|
||||
}}
|
||||
items={mockRouteActionList}
|
||||
className="border select-none text-sm min-w-max border-solid border-[--hl-sm] shadow-lg bg-[--color-bg] py-2 rounded-md overflow-y-auto max-h-[85vh] focus:outline-none"
|
||||
>
|
||||
{item => (
|
||||
<Breadcrumb
|
||||
key={item.id}
|
||||
id={item.id}
|
||||
className="flex gap-2 px-[--padding-md] aria-selected:font-bold items-center text-[--color-font] h-[--line-height-xs] w-full text-md whitespace-nowrap bg-transparent hover:bg-[--hl-sm] disabled:cursor-not-allowed focus:bg-[--hl-xs] focus:outline-none transition-colors"
|
||||
aria-label={item.name}
|
||||
>
|
||||
<Icon icon={item.icon} />
|
||||
<span>{item.name}</span>
|
||||
</Breadcrumb>
|
||||
)}
|
||||
</Menu>
|
||||
</Popover>
|
||||
</MenuTrigger>
|
||||
</div>
|
||||
</GridListItem>
|
||||
);
|
||||
}}
|
||||
</GridList>
|
||||
|
||||
<WorkspaceSyncDropdown />
|
||||
</div>}
|
||||
|
||||
renderPaneOne={<Routes>
|
||||
<Route
|
||||
path={'mock-route/:mockRouteId/*'}
|
||||
element={
|
||||
<Suspense>
|
||||
<MockRouteRoute />
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="*"
|
||||
element={
|
||||
<EmptyStatePane
|
||||
icon={<SvgIcon icon="bug" />}
|
||||
documentationLinks={[]}
|
||||
title="Select or create a route to configured response here"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Routes>}
|
||||
renderPaneTwo={<Routes>
|
||||
<Route
|
||||
path={'mock-route/:mockRouteId/*'}
|
||||
element={
|
||||
<Suspense>
|
||||
<MockRouteResponse />
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="*"
|
||||
element={
|
||||
<EmptyStatePane
|
||||
icon={<SvgIcon icon="bug" />}
|
||||
documentationLinks={[]}
|
||||
title="Select or create a route to see activity here"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Routes>}
|
||||
/>;
|
||||
};
|
||||
|
||||
export default MockServerRoute;
|
@ -755,16 +755,16 @@ const OrganizationRoute = () => {
|
||||
</Tooltip>
|
||||
</TooltipTrigger>
|
||||
<span className='w-[1px] h-full bg-[--hl-sm]' />
|
||||
<Link>
|
||||
<a
|
||||
className="flex focus:outline-none focus:underline gap-1 items-center text-xs text-[--color-font] px-[--padding-md]"
|
||||
href="https://konghq.com/"
|
||||
>
|
||||
Made with
|
||||
<Link>
|
||||
<a
|
||||
className="flex focus:outline-none focus:underline gap-1 items-center text-xs text-[--color-font] px-[--padding-md]"
|
||||
href="https://konghq.com/"
|
||||
>
|
||||
Made with
|
||||
<Icon className="text-[--color-surprise-font]" icon="heart" /> by
|
||||
Kong
|
||||
</a>
|
||||
</Link>
|
||||
Kong
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -54,6 +54,7 @@ import { ApiSpec } from '../../models/api-spec';
|
||||
import { CaCertificate } from '../../models/ca-certificate';
|
||||
import { ClientCertificate } from '../../models/client-certificate';
|
||||
import { sortProjects } from '../../models/helpers/project';
|
||||
import { MockServer } from '../../models/mock-server';
|
||||
import { isOwnerOfOrganization, isPersonalOrganization, isScratchpadOrganizationId } from '../../models/organization';
|
||||
import { Organization } from '../../models/organization';
|
||||
import {
|
||||
@ -61,7 +62,7 @@ import {
|
||||
Project,
|
||||
SCRATCHPAD_PROJECT_ID,
|
||||
} from '../../models/project';
|
||||
import { isDesign, Workspace } from '../../models/workspace';
|
||||
import { isDesign, scopeToActivity, Workspace, WorkspaceScope } from '../../models/workspace';
|
||||
import { WorkspaceMeta } from '../../models/workspace-meta';
|
||||
import { showModal } from '../../ui/components/modals';
|
||||
import { AskModal } from '../../ui/components/modals/ask-modal';
|
||||
@ -121,6 +122,7 @@ export interface WorkspaceWithMetadata {
|
||||
specFormat: 'openapi' | 'swagger' | null;
|
||||
name: string;
|
||||
apiSpec: ApiSpec | null;
|
||||
mockServer: MockServer | null;
|
||||
specFormatVersion: string | null;
|
||||
workspace: Workspace;
|
||||
workspaceMeta: WorkspaceMeta;
|
||||
@ -263,6 +265,7 @@ export interface ProjectLoaderData {
|
||||
allFilesCount: number;
|
||||
documentsCount: number;
|
||||
collectionsCount: number;
|
||||
mockServersCount: number;
|
||||
projectsCount: number;
|
||||
activeProject: Project;
|
||||
projects: Project[];
|
||||
@ -303,7 +306,7 @@ export const loader: LoaderFunction = async ({
|
||||
workspace: Workspace
|
||||
): Promise<WorkspaceWithMetadata> => {
|
||||
const apiSpec = await models.apiSpec.getByParentId(workspace._id);
|
||||
|
||||
const mockServer = await models.mockServer.getByParentId(workspace._id);
|
||||
let spec: ParsedApiSpec['contents'] = null;
|
||||
let specFormat: ParsedApiSpec['format'] = null;
|
||||
let specFormatVersion: ParsedApiSpec['formatVersion'] = null;
|
||||
@ -368,6 +371,7 @@ export const loader: LoaderFunction = async ({
|
||||
specFormat,
|
||||
name: workspace.name,
|
||||
apiSpec,
|
||||
mockServer,
|
||||
specFormatVersion,
|
||||
workspaceMeta,
|
||||
clientCertificates,
|
||||
@ -453,6 +457,9 @@ export const loader: LoaderFunction = async ({
|
||||
collectionsCount: workspacesWithMetaData.filter(
|
||||
w => w.workspace.scope === 'collection'
|
||||
).length,
|
||||
mockServersCount: workspacesWithMetaData.filter(
|
||||
w => w.workspace.scope === 'mock-server'
|
||||
).length,
|
||||
};
|
||||
};
|
||||
|
||||
@ -463,6 +470,7 @@ const ProjectRoute: FC = () => {
|
||||
projects,
|
||||
allFilesCount,
|
||||
collectionsCount,
|
||||
mockServersCount,
|
||||
documentsCount,
|
||||
projectsCount,
|
||||
learningFeature,
|
||||
@ -570,6 +578,28 @@ const ProjectRoute: FC = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const createNewMockServer = () => {
|
||||
showPrompt({
|
||||
title: 'Create New Mock Server',
|
||||
submitName: 'Create',
|
||||
placeholder: 'My Mock Server',
|
||||
defaultValue: 'My Mock Server',
|
||||
selectText: true,
|
||||
onComplete: async (name: string) => {
|
||||
fetcher.submit(
|
||||
{
|
||||
name,
|
||||
scope: 'mock-server',
|
||||
},
|
||||
{
|
||||
action: `/organization/${organizationId}/project/${activeProject._id}/workspace/new`,
|
||||
method: 'post',
|
||||
}
|
||||
);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const createNewProjectFetcher = useFetcher();
|
||||
|
||||
useEffect(() => {
|
||||
@ -654,6 +684,12 @@ const ProjectRoute: FC = () => {
|
||||
action: createNewDocument,
|
||||
},
|
||||
{
|
||||
id: 'new-mock-server',
|
||||
name: 'Mock Server',
|
||||
icon: 'server',
|
||||
action: createNewMockServer,
|
||||
},
|
||||
{
|
||||
id: 'import',
|
||||
name: 'Import',
|
||||
icon: 'file-import',
|
||||
@ -704,11 +740,36 @@ const ProjectRoute: FC = () => {
|
||||
run: createNewCollection,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'mock-server',
|
||||
label: `Mock (${mockServersCount})`,
|
||||
icon: 'server',
|
||||
action: {
|
||||
icon: 'plus',
|
||||
label: 'New Mock Server',
|
||||
run: createNewMockServer,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const organization = organizations.find(o => o.id === organizationId);
|
||||
const isUserOwner = organization && accountId && isOwnerOfOrganization({ organization, accountId });
|
||||
const isPersonalOrg = organization && isPersonalOrganization(organization);
|
||||
const scopeToIconMap: Record<string, IconName> = {
|
||||
design: 'file',
|
||||
collection: 'bars',
|
||||
'mock-server': 'server',
|
||||
};
|
||||
const scopeToBgColorMap: Record<string, string> = {
|
||||
design: 'bg-[--color-info]',
|
||||
collection: 'bg-[--color-surprise]',
|
||||
'mock-server': 'bg-[--color-warning]',
|
||||
};
|
||||
const scopeToTextColorMap: Record<string, string> = {
|
||||
design: 'text-[--color-info-font]',
|
||||
collection: 'text-[--color-surprise-font]',
|
||||
'mock-server': 'text-[--color-warning-font]',
|
||||
};
|
||||
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
@ -1153,8 +1214,11 @@ const ProjectRoute: FC = () => {
|
||||
aria-label="Workspaces"
|
||||
items={workspacesWithPresence}
|
||||
onAction={key => {
|
||||
// hack to workaround gridlist not have access to workspace scope
|
||||
const [id, scope] = key.toString().split('|');
|
||||
const activity = scopeToActivity(scope as WorkspaceScope);
|
||||
navigate(
|
||||
`/organization/${organizationId}/project/${projectId}/workspace/${key}/debug`
|
||||
`/organization/${organizationId}/project/${projectId}/workspace/${id}/${activity}`
|
||||
);
|
||||
}}
|
||||
className="data-[empty]:flex data-[empty]:justify-center grid [grid-template-columns:repeat(auto-fit,200px)] [grid-template-rows:repeat(auto-fit,200px)] gap-4 p-[--padding-md]"
|
||||
@ -1169,40 +1233,34 @@ const ProjectRoute: FC = () => {
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<EmptyStatePane
|
||||
createRequestCollection={createNewCollection}
|
||||
createDesignDocument={createNewDocument}
|
||||
importFrom={() => setImportModalType('file')}
|
||||
cloneFromGit={importFromGit}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
return (
|
||||
<EmptyStatePane
|
||||
createRequestCollection={createNewCollection}
|
||||
createDesignDocument={createNewDocument}
|
||||
createMockServer={createNewMockServer}
|
||||
importFrom={() => setImportModalType('file')}
|
||||
cloneFromGit={importFromGit}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
>
|
||||
{item => {
|
||||
return (
|
||||
<GridListItem
|
||||
key={item._id}
|
||||
id={item._id}
|
||||
textValue={item.name}
|
||||
className="flex-1 overflow-hidden flex-col outline-none p-[--padding-md] flex select-none w-full rounded-sm hover:shadow-md aspect-square ring-1 ring-[--hl-md] hover:ring-[--hl-sm] focus:ring-[--hl-lg] hover:bg-[--hl-xs] focus:bg-[--hl-sm] transition-all"
|
||||
>
|
||||
<div className="flex gap-2 h-[20px]">
|
||||
<div className="flex pr-2 h-full flex-shrink-0 items-center rounded-sm gap-2 bg-[--hl-xs] text-[--color-font] text-sm">
|
||||
{isDesign(item.workspace) ? (
|
||||
<div className="px-2 flex justify-center items-center h-[20px] w-[20px] rounded-s-sm bg-[--color-info] text-[--color-font-info]">
|
||||
<Icon icon="file" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="px-2 flex justify-center items-center h-[20px] w-[20px] rounded-s-sm bg-[--color-surprise] text-[--color-font-surprise]">
|
||||
<Icon icon="bars" />
|
||||
</div>
|
||||
)}
|
||||
<span>
|
||||
{isDesign(item.workspace)
|
||||
? 'Document'
|
||||
: 'Collection'}
|
||||
</span>
|
||||
{item => {
|
||||
return (
|
||||
<GridListItem
|
||||
key={item._id}
|
||||
// hack to workaround gridlist not have access to workspace scope
|
||||
id={item._id + '|' + item.workspace.scope}
|
||||
textValue={item.name}
|
||||
className="flex-1 overflow-hidden flex-col outline-none p-[--padding-md] flex select-none w-full rounded-sm hover:shadow-md aspect-square ring-1 ring-[--hl-md] hover:ring-[--hl-sm] focus:ring-[--hl-lg] hover:bg-[--hl-xs] focus:bg-[--hl-sm] transition-all"
|
||||
>
|
||||
<div className="flex gap-2 h-[20px]">
|
||||
<div className="flex pr-2 h-full flex-shrink-0 items-center rounded-sm gap-2 bg-[--hl-xs] text-[--color-font] text-sm">
|
||||
<div className={`${scopeToBgColorMap[item.workspace.scope]} ${scopeToTextColorMap[item.workspace.scope]} px-2 flex justify-center items-center h-[20px] w-[20px] rounded-s-sm`}>
|
||||
<Icon icon={scopeToIconMap[item.workspace.scope]} />
|
||||
</div>
|
||||
<span>
|
||||
{item.workspace.scope}
|
||||
</span>
|
||||
</div>
|
||||
<span className="flex-1" />
|
||||
{item.presence.length > 0 && (
|
||||
|
@ -56,6 +56,13 @@ async function getSyncItems({
|
||||
const testSuites = await models.unitTestSuite.findByParentId(workspaceId);
|
||||
const tests = await database.find(models.unitTest.type, { parentId: { $in: testSuites.map(t => t._id) } });
|
||||
|
||||
const mockServer = await models.mockServer.getByParentId(workspaceId);
|
||||
if (mockServer) {
|
||||
syncItemsList.push(mockServer);
|
||||
const mockRoutes = await database.find(models.mockRoute.type, { parentId: mockServer._id });
|
||||
mockRoutes.map(m => syncItemsList.push(m));
|
||||
}
|
||||
|
||||
const baseEnvironment = await models.environment.getByParentId(workspaceId);
|
||||
invariant(baseEnvironment, 'Base environment not found');
|
||||
|
||||
|
@ -17,6 +17,8 @@ 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 { MockRoute } from '../../models/mock-route';
|
||||
import { MockServer } from '../../models/mock-server';
|
||||
import { getPathParametersFromUrl, isEventStreamRequest, isRequest, Request, RequestAuthentication, RequestBody, RequestHeader, RequestParameter } from '../../models/request';
|
||||
import { isRequestMeta, RequestMeta } from '../../models/request-meta';
|
||||
import { RequestVersion } from '../../models/request-version';
|
||||
@ -49,12 +51,14 @@ export interface RequestLoaderData {
|
||||
activeResponse: Response | null;
|
||||
responses: Response[];
|
||||
requestVersions: RequestVersion[];
|
||||
mockServerAndRoutes: (MockServer & { routes: MockRoute[] })[];
|
||||
}
|
||||
|
||||
export const loader: LoaderFunction = async ({ params }): Promise<RequestLoaderData | WebSocketRequestLoaderData | GrpcRequestLoaderData> => {
|
||||
const { organizationId, projectId, requestId, workspaceId } = params;
|
||||
invariant(requestId, 'Request ID is required');
|
||||
invariant(workspaceId, 'Workspace ID is required');
|
||||
invariant(projectId, 'Project ID is required');
|
||||
const activeRequest = await requestOperations.getById(requestId);
|
||||
if (!activeRequest) {
|
||||
throw redirect(`/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/debug`);
|
||||
@ -85,12 +89,21 @@ export const loader: LoaderFunction = async ({ params }): Promise<RequestLoaderD
|
||||
.filter((r: Response | WebSocketResponse) => r.environmentId === activeWorkspaceMeta.activeEnvironmentId);
|
||||
const responses = (filterResponsesByEnv ? filteredResponses : allResponses)
|
||||
.sort((a: BaseModel, b: BaseModel) => (a.created > b.created ? -1 : 1));
|
||||
|
||||
// Q(gatzjames): load mock servers here or somewhere else?
|
||||
const mockServers = await models.mockServer.findByProjectId(projectId);
|
||||
const mockRoutes = await database.find<MockRoute>(models.mockRoute.type, { parentId: { $in: mockServers.map(s => s._id) } });
|
||||
const mockServerAndRoutes = mockServers.map(mockServer => ({
|
||||
...mockServer,
|
||||
routes: mockRoutes.filter(route => route.parentId === mockServer._id),
|
||||
}));
|
||||
return {
|
||||
activeRequest,
|
||||
activeRequestMeta,
|
||||
activeResponse,
|
||||
responses,
|
||||
requestVersions: await models.requestVersion.findByParentId(requestId),
|
||||
mockServerAndRoutes,
|
||||
} as RequestLoaderData | WebSocketRequestLoaderData;
|
||||
};
|
||||
|
||||
@ -415,6 +428,37 @@ export const sendAction: ActionFunction = async ({ request, params }) => {
|
||||
return redirect(`${url.pathname}?${url.searchParams}`);
|
||||
}
|
||||
};
|
||||
export const createAndSendToMockbinAction: ActionFunction = async ({ request }) => {
|
||||
const patch = await request.json() as Partial<Request>;
|
||||
invariant(typeof patch.url === 'string', 'URL is required');
|
||||
invariant(typeof patch.method === 'string', 'method is required');
|
||||
invariant(typeof patch.parentId === 'string', 'mock route ID is required');
|
||||
const mockRoute = await models.mockRoute.getById(patch.parentId);
|
||||
invariant(mockRoute, 'mock route not found');
|
||||
const childRequests = await models.request.findByParentId(mockRoute._id);
|
||||
const testRequest = childRequests[0];
|
||||
invariant(testRequest, 'mock route is missing a testing request');
|
||||
const req = await models.request.update(testRequest, patch);
|
||||
|
||||
const {
|
||||
environment,
|
||||
settings,
|
||||
clientCertificates,
|
||||
caCert,
|
||||
activeEnvironmentId } = await fetchRequestData(req._id);
|
||||
|
||||
const renderResult = await tryToInterpolateRequest(req, environment._id, RENDER_PURPOSE_SEND);
|
||||
const renderedRequest = await tryToTransformRequestWithPlugins(renderResult);
|
||||
const res = await sendCurlAndWriteTimeline(
|
||||
renderedRequest,
|
||||
clientCertificates,
|
||||
caCert,
|
||||
settings,
|
||||
);
|
||||
const response = await responseTransform(res, activeEnvironmentId, renderedRequest, renderResult.context);
|
||||
await models.response.create(response);
|
||||
return null;
|
||||
};
|
||||
export const deleteAllResponsesAction: ActionFunction = async ({ params }) => {
|
||||
const { workspaceId, requestId } = params;
|
||||
invariant(typeof requestId === 'string', 'Request ID is required');
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { IconName } from '@fortawesome/fontawesome-svg-core';
|
||||
import type { IconName } from '@fortawesome/fontawesome-svg-core';
|
||||
import React, { FC, Fragment, Suspense, useState } from 'react';
|
||||
import {
|
||||
Breadcrumb,
|
||||
|
@ -16,6 +16,7 @@ import { GitRepository } from '../../models/git-repository';
|
||||
import { GrpcRequest } from '../../models/grpc-request';
|
||||
import { GrpcRequestMeta } from '../../models/grpc-request-meta';
|
||||
import { sortProjects } from '../../models/helpers/project';
|
||||
import { MockServer } from '../../models/mock-server';
|
||||
import { Project } from '../../models/project';
|
||||
import { Request } from '../../models/request';
|
||||
import { isRequestGroup, RequestGroup } from '../../models/request-group';
|
||||
@ -43,6 +44,7 @@ export interface WorkspaceLoaderData {
|
||||
baseEnvironment: Environment;
|
||||
subEnvironments: Environment[];
|
||||
activeApiSpec: ApiSpec | null;
|
||||
activeMockServer: MockServer | null;
|
||||
clientCertificates: ClientCertificate[];
|
||||
caCertificate: CaCertificate | null;
|
||||
projects: Project[];
|
||||
@ -106,6 +108,7 @@ export const workspaceLoader: LoaderFunction = async ({
|
||||
invariant(activeCookieJar, 'Cookie jar not found');
|
||||
|
||||
const activeApiSpec = await models.apiSpec.getByParentId(workspaceId);
|
||||
const activeMockServer = await models.mockServer.getByParentId(workspaceId);
|
||||
const clientCertificates = await models.clientCertificate.findByParentId(
|
||||
workspaceId,
|
||||
);
|
||||
@ -266,6 +269,7 @@ export const workspaceLoader: LoaderFunction = async ({
|
||||
subEnvironments,
|
||||
baseEnvironment,
|
||||
activeApiSpec,
|
||||
activeMockServer,
|
||||
clientCertificates,
|
||||
caCertificate: await models.caCertificate.findByParentId(workspaceId),
|
||||
projects,
|
||||
|
Loading…
Reference in New Issue
Block a user