mirror of
https://github.com/Kong/insomnia.git
synced 2026-04-21 14:47:46 -04:00
fix(Response Tabs): Tabs with a menu inside are not accessible - Response Panes (#7477)
* use thing scrollbars in the app * fix menu/toast overflow content shift * update content and auth dropdowns * Update auth wrapper styles * update body editors * Request/request-group panes * update request script editor * update e2e tests * remove log
This commit is contained in:
@@ -73,7 +73,7 @@ test.describe('Environment Editor', async () => {
|
||||
await page.getByLabel('Request Collection').getByTestId('New Request').press('Enter');
|
||||
|
||||
// Add number variable to request body
|
||||
await page.getByRole('tab', { name: 'Plain' }).click();
|
||||
await page.getByRole('tab', { name: 'Body' }).click();
|
||||
await page.locator('pre').filter({ hasText: '_.exampleObject.anotherNumber' }).press('Enter');
|
||||
|
||||
await page.getByTestId('CodeEditor').getByRole('textbox').press('Enter');
|
||||
|
||||
@@ -21,9 +21,9 @@ test('can render schema and send GraphQL requests', async ({ app, page }) => {
|
||||
|
||||
// Open the graphql request
|
||||
await page.getByLabel('Request Collection').getByTestId('GraphQL request').press('Enter');
|
||||
await page.getByRole('tab', { name: 'GraphQL' }).click();
|
||||
await page.getByRole('tab', { name: 'Body' }).click();
|
||||
// Assert the schema is fetched after switching to GraphQL request
|
||||
await expect(page.locator('.graphql-editor__meta')).toContainText('schema fetched just now');
|
||||
await expect(page.getByText('Schema fetched just now')).toBeVisible();
|
||||
|
||||
// Assert schema documentation stuff
|
||||
await page.getByRole('button', { name: 'schema' }).click();
|
||||
@@ -63,9 +63,9 @@ test('can render schema and send GraphQL requests with object variables', async
|
||||
|
||||
// Open the graphql request
|
||||
await page.getByLabel('Request Collection').getByTestId('GraphQL request with variables').press('Enter');
|
||||
await page.getByRole('tab', { name: 'GraphQL' }).click();
|
||||
await page.getByRole('tab', { name: 'Body' }).click();
|
||||
// Assert the schema is fetched after switching to GraphQL request
|
||||
await expect(page.locator('.graphql-editor__meta')).toContainText('schema fetched just now');
|
||||
await expect(page.getByText('Schema fetched just now')).toBeVisible();
|
||||
|
||||
// Assert schema documentation stuff
|
||||
await page.getByRole('button', { name: 'schema' }).click();
|
||||
@@ -105,9 +105,9 @@ test('can render numeric environment', async ({ app, page }) => {
|
||||
|
||||
// Open the graphql request
|
||||
await page.getByLabel('Request Collection').getByTestId('GraphQL request with number').press('Enter');
|
||||
await page.getByRole('tab', { name: 'GraphQL' }).click();
|
||||
await page.getByRole('tab', { name: 'Body' }).click();
|
||||
// Assert the schema is fetched after switching to GraphQL request
|
||||
await expect(page.locator('.graphql-editor__meta')).toContainText('schema fetched just now');
|
||||
await expect(page.getByText('Schema fetched just now')).toBeVisible();
|
||||
|
||||
// Assert schema documentation stuff
|
||||
await page.getByRole('button', { name: 'schema' }).click();
|
||||
@@ -144,7 +144,7 @@ test('can send GraphQL requests after editing and prettifying query', async ({ a
|
||||
await page.getByLabel('Request Collection').getByTestId('GraphQL request').press('Enter');
|
||||
|
||||
// Edit and prettify query
|
||||
await page.getByRole('tab', { name: 'GraphQL' }).click();
|
||||
await page.getByRole('tab', { name: 'Body' }).click();
|
||||
await page.locator('pre[role="presentation"]:has-text("bearer")').click();
|
||||
await page.locator('.app').press('Enter');
|
||||
await page.locator('text=Prettify GraphQL').click();
|
||||
|
||||
@@ -47,7 +47,8 @@ test('can make oauth2 requests', async ({ app, page }) => {
|
||||
await expect(responseBody).toContainText('"sub": "admin"');
|
||||
|
||||
// Navigate to the OAuth2 Tab and refresh the token from there
|
||||
await page.getByRole('tab', { name: 'OAuth 2' }).click();
|
||||
await page.getByRole('tab', { name: 'Auth' }).click();
|
||||
await expect(page.getByRole('button', { name: 'OAuth 2.0' })).toBeVisible();
|
||||
|
||||
const tokenInput = page.locator('[for="Access-Token"] > input');
|
||||
const prevToken = await tokenInput.inputValue();
|
||||
|
||||
@@ -217,13 +217,13 @@ test.describe('pre-request features tests', async () => {
|
||||
// set request body
|
||||
await page.getByRole('tab', { name: 'Body' }).click();
|
||||
await page.getByRole('button', { name: 'Body' }).click();
|
||||
await page.getByRole('menuitem', { name: 'JSON' }).click();
|
||||
await page.getByRole('option', { name: 'JSON' }).click();
|
||||
|
||||
const bodyEditor = page.getByTestId('CodeEditor').getByRole('textbox');
|
||||
await bodyEditor.fill('{ "rawBody": {{ _.rawBody }}, "urlencodedBody": {{ _.urlencodedBody }}, "gqlBody": {{ _.gqlBody }}, "fileBody": {{ _.fileBody }}, "formdataBody": {{ _.formdataBody }} }');
|
||||
|
||||
// enter script
|
||||
await page.getByTestId('pre-request-script-tab').click();
|
||||
await page.getByRole('tab', { name: 'Scripts' }).click();
|
||||
const preRequestScriptEditor = page.getByTestId('CodeEditor').getByRole('textbox');
|
||||
await preRequestScriptEditor.fill(`
|
||||
const rawReq = {
|
||||
@@ -500,10 +500,10 @@ test.describe('unhappy paths', async () => {
|
||||
// set request body
|
||||
await page.getByRole('tab', { name: 'Body' }).click();
|
||||
await page.getByRole('button', { name: 'Body' }).click();
|
||||
await page.getByRole('menuitem', { name: 'JSON' }).click();
|
||||
await page.getByRole('option', { name: 'JSON' }).click();
|
||||
|
||||
// enter script
|
||||
await page.getByTestId('pre-request-script-tab').click();
|
||||
await page.getByRole('tab', { name: 'Scripts' }).click();
|
||||
const preRequestScriptEditor = page.getByTestId('CodeEditor').getByRole('textbox');
|
||||
await preRequestScriptEditor.fill(tc.preReqScript);
|
||||
|
||||
|
||||
@@ -8,11 +8,11 @@ test('Request tabs', async ({ page }) => {
|
||||
await page.getByRole('menuitemradio', { name: 'HTTP Request' }).press('Enter');
|
||||
await page.getByRole('tab', { name: 'Body' }).click();
|
||||
await page.getByRole('button', { name: 'Body' }).click();
|
||||
await page.getByRole('menuitem', { name: 'JSON' }).click();
|
||||
await page.getByRole('option', { name: 'JSON' }).click();
|
||||
await page.getByRole('tab', { name: 'Auth' }).click();
|
||||
await page.getByRole('button', { name: 'Auth' }).click();
|
||||
await page.getByRole('menuitem', { name: 'OAuth 1.0' }).click();
|
||||
await page.getByRole('tab', { name: 'Parameters' }).click();
|
||||
await page.getByLabel('OAuth 1.0', { exact: true }).click();
|
||||
await page.getByRole('tab', { name: 'Params' }).click();
|
||||
await page.getByRole('tab', { name: 'Headers' }).click();
|
||||
await page.getByRole('tab', { name: 'Docs' }).click();
|
||||
await page.locator('text=Add Description').click();
|
||||
@@ -26,11 +26,11 @@ test('WS tabs', async ({ page }) => {
|
||||
|
||||
await page.getByLabel('Create in collection').click();
|
||||
await page.getByRole('menuitemradio', { name: 'WebSocket Request' }).click();
|
||||
await page.getByRole('tab', { name: 'JSON' }).click();
|
||||
await page.getByLabel('Websocket request pane tabs').getByRole('button', { name: 'JSON' }).click();
|
||||
await page.getByRole('menuitem', { name: 'JSON' }).click();
|
||||
await page.getByRole('tab', { name: 'Body' }).click();
|
||||
await page.getByRole('button', { name: 'JSON' }).click();
|
||||
await page.getByRole('option', { name: 'JSON' }).click();
|
||||
await page.getByRole('tab', { name: 'Auth' }).click();
|
||||
await page.getByRole('tab', { name: 'Parameters' }).click();
|
||||
await page.getByRole('tab', { name: 'Params' }).click();
|
||||
await page.getByRole('tab', { name: 'Headers' }).click();
|
||||
await page.getByRole('tab', { name: 'Docs' }).click();
|
||||
await page.getByRole('button', { name: 'Add Description' }).click();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en-US" class="w-full h-full">
|
||||
<html lang="en-US" class="w-full h-full overflow-hidden">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { IconName } from '@fortawesome/fontawesome-svg-core';
|
||||
import React, { FC, useCallback } from 'react';
|
||||
import { Button, Collection, Header, ListBox, ListBoxItem, Popover, Section, Select, SelectValue } from 'react-aria-components';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import {
|
||||
getAuthTypeName,
|
||||
HAWK_ALGORITHM_SHA256,
|
||||
} from '../../../common/constants';
|
||||
import type { AuthTypeAPIKey, AuthTypeAwsIam, AuthTypeBasic, AuthTypeNTLM, AuthTypes, RequestAuthentication } from '../../../models/request';
|
||||
@@ -10,7 +11,7 @@ import { getAuthObjectOrNull } from '../../../network/authentication';
|
||||
import { SIGNATURE_METHOD_HMAC_SHA1 } from '../../../network/o-auth-1/constants';
|
||||
import { GRANT_TYPE_AUTHORIZATION_CODE } from '../../../network/o-auth-2/constants';
|
||||
import { useRequestGroupPatcher, useRequestPatcher } from '../../hooks/use-request';
|
||||
import { Dropdown, DropdownButton, DropdownItem, DropdownSection, ItemContent } from '../base/dropdown';
|
||||
import { Icon } from '../icon';
|
||||
|
||||
function castOneAuthTypeToAnother(type: AuthTypes, oldAuth: RequestAuthentication | {}): RequestAuthentication {
|
||||
switch (type) {
|
||||
@@ -133,6 +134,7 @@ interface Props {
|
||||
authTypes?: AuthTypes[];
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export const AuthDropdown: FC<Props> = ({ authentication, authTypes = defaultTypes, disabled = false }) => {
|
||||
const { requestId, requestGroupId } = useParams() as { organizationId: string; projectId: string; workspaceId: string; requestId?: string; requestGroupId?: string };
|
||||
const patchRequest = useRequestPatcher();
|
||||
@@ -147,57 +149,141 @@ export const AuthDropdown: FC<Props> = ({ authentication, authTypes = defaultTyp
|
||||
requestGroupId && patchRequestGroup(requestGroupId, { authentication: newAuthentication });
|
||||
}, [authentication, patchRequest, patchRequestGroup, requestGroupId, requestId]);
|
||||
|
||||
const isSelected = useCallback((type: AuthTypes) => {
|
||||
return type === getAuthObjectOrNull(authentication)?.type;
|
||||
}, [authentication]);
|
||||
const selectedAuthType = getAuthObjectOrNull(authentication)?.type || 'none';
|
||||
|
||||
const authTypesItems: {
|
||||
id: AuthTypes;
|
||||
name: string;
|
||||
}[] = [
|
||||
{
|
||||
id: 'apikey',
|
||||
name: 'API Key',
|
||||
},
|
||||
{
|
||||
id: 'basic',
|
||||
name: 'Basic',
|
||||
},
|
||||
{
|
||||
id: 'digest',
|
||||
name: 'Digest',
|
||||
},
|
||||
{
|
||||
id: 'ntlm',
|
||||
name: 'NTLM',
|
||||
},
|
||||
{
|
||||
id: 'oauth1',
|
||||
name: 'OAuth 1.0',
|
||||
},
|
||||
{
|
||||
id: 'oauth2',
|
||||
name: 'OAuth 2.0',
|
||||
},
|
||||
{
|
||||
id: 'iam',
|
||||
name: 'AWS IAM',
|
||||
},
|
||||
{
|
||||
id: 'bearer',
|
||||
name: 'Bearer Token',
|
||||
},
|
||||
{
|
||||
id: 'hawk',
|
||||
name: 'Hawk',
|
||||
},
|
||||
{
|
||||
id: 'asap',
|
||||
name: 'Atlassian ASAP',
|
||||
},
|
||||
{
|
||||
id: 'netrc',
|
||||
name: 'Netrc',
|
||||
},
|
||||
];
|
||||
|
||||
const authTypeSections: {
|
||||
id: string;
|
||||
icon: IconName;
|
||||
name: string;
|
||||
items: {
|
||||
id: AuthTypes;
|
||||
name: string;
|
||||
}[];
|
||||
}[] = [
|
||||
{
|
||||
id: 'Auth Types',
|
||||
name: 'Auth Types',
|
||||
icon: 'lock',
|
||||
items: authTypesItems.filter(item => authTypes.includes(item.id)),
|
||||
},
|
||||
{
|
||||
id: 'Other',
|
||||
name: 'Other',
|
||||
icon: 'ellipsis-h',
|
||||
items: [
|
||||
{
|
||||
id: 'none',
|
||||
name: 'None',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
aria-label='Authentication Dropdown'
|
||||
<Select
|
||||
isDisabled={disabled}
|
||||
triggerButton={
|
||||
<DropdownButton className="tall !text-[--hl]">
|
||||
{getAuthTypeName(getAuthObjectOrNull(authentication)?.type)}
|
||||
<i className="fa fa-caret-down space-left" />
|
||||
</DropdownButton>
|
||||
}
|
||||
aria-label="Change Authentication type"
|
||||
name="auth-type"
|
||||
onSelectionChange={authType => {
|
||||
onClick(authType as AuthTypes);
|
||||
}}
|
||||
selectedKey={selectedAuthType}
|
||||
>
|
||||
<DropdownSection
|
||||
aria-label='Auth types section'
|
||||
title="Auth Types"
|
||||
>
|
||||
{authTypes.map(authType =>
|
||||
<DropdownItem
|
||||
key={authType}
|
||||
aria-label={getAuthTypeName(authType, true)}
|
||||
>
|
||||
<ItemContent
|
||||
icon={isSelected(authType) ? 'check' : 'empty'}
|
||||
label={getAuthTypeName(authType, true)}
|
||||
onClick={() => onClick(authType)}
|
||||
/>
|
||||
</DropdownItem>
|
||||
)}
|
||||
</DropdownSection>
|
||||
<DropdownSection
|
||||
aria-label="Other types section"
|
||||
title="Other"
|
||||
>
|
||||
<DropdownItem aria-label='None' key="none">
|
||||
<ItemContent
|
||||
icon={isSelected('none') ? 'check' : 'empty'}
|
||||
label={'No Authentication'}
|
||||
onClick={() => onClick('none')}
|
||||
/>
|
||||
</DropdownItem>
|
||||
<DropdownItem aria-label='Inherit from parent' key="inherit">
|
||||
<ItemContent
|
||||
icon={getAuthObjectOrNull(authentication) === null ? 'check' : 'empty'}
|
||||
label={'Inherit from parent'}
|
||||
onClick={() => onClick()}
|
||||
/>
|
||||
</DropdownItem>
|
||||
</DropdownSection>
|
||||
</Dropdown>
|
||||
<Button className="px-4 min-w-[17ch] py-1 font-bold flex flex-1 items-center justify-between 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">
|
||||
<SelectValue className="flex truncate items-center justify-center gap-2">
|
||||
{({ selectedText }) => (
|
||||
<div className='flex items-center gap-2 text-[--hl]'>
|
||||
{selectedText || 'Auth Type'}
|
||||
</div>
|
||||
)}
|
||||
</SelectValue>
|
||||
<Icon icon="caret-down" />
|
||||
</Button>
|
||||
<Popover className="min-w-max">
|
||||
<ListBox
|
||||
items={authTypeSections}
|
||||
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 => (
|
||||
<Section>
|
||||
<Header className='pl-2 py-1 flex items-center gap-2 text-[--hl] text-xs uppercase'>
|
||||
<Icon icon={item.icon} /> <span>{item.name}</span>
|
||||
</Header>
|
||||
<Collection items={item.items}>
|
||||
{item => (
|
||||
<ListBoxItem
|
||||
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}
|
||||
textValue={item.name}
|
||||
>
|
||||
{({ isSelected }) => (
|
||||
<>
|
||||
<span>{item.name}</span>
|
||||
{isSelected && (
|
||||
<Icon
|
||||
icon="check"
|
||||
className="text-[--color-success] justify-self-end"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</ListBoxItem>
|
||||
)}
|
||||
</Collection>
|
||||
</Section>
|
||||
)}
|
||||
</ListBox>
|
||||
</Popover>
|
||||
</Select>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { IconName } from '@fortawesome/fontawesome-svg-core';
|
||||
import React, { FC } from 'react';
|
||||
import { Button, Collection, Header, ListBox, ListBoxItem, Popover, Section, Select, SelectValue } from 'react-aria-components';
|
||||
import { useParams, useRouteLoaderData } from 'react-router-dom';
|
||||
|
||||
import {
|
||||
@@ -20,12 +22,87 @@ import { deconstructQueryStringToParams } from '../../../utils/url/querystring';
|
||||
import { SegmentEvent } from '../../analytics';
|
||||
import { useRequestPatcher } from '../../hooks/use-request';
|
||||
import { RequestLoaderData } from '../../routes/request';
|
||||
import { Dropdown, DropdownButton, DropdownItem, DropdownSection, ItemContent } from '../base/dropdown';
|
||||
import { AlertModal } from '../modals/alert-modal';
|
||||
import { showModal } from '../modals/index';
|
||||
import { Icon } from '../icon';
|
||||
import { showAlert } from '../modals/index';
|
||||
|
||||
const EMPTY_MIME_TYPE = null;
|
||||
|
||||
const contentTypeSections: {
|
||||
id: string;
|
||||
icon: IconName;
|
||||
name: string;
|
||||
items: {
|
||||
id: string;
|
||||
name: string;
|
||||
}[];
|
||||
}[] = [
|
||||
{
|
||||
id: 'structured',
|
||||
name: 'Structured',
|
||||
icon: 'bars',
|
||||
items: [
|
||||
{
|
||||
id: CONTENT_TYPE_FORM_DATA,
|
||||
name: 'Form Data',
|
||||
},
|
||||
{
|
||||
id: CONTENT_TYPE_FORM_URLENCODED,
|
||||
name: 'Form URL Encoded',
|
||||
},
|
||||
{
|
||||
id: CONTENT_TYPE_GRAPHQL,
|
||||
name: 'GraphQL',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'text',
|
||||
icon: 'code',
|
||||
name: 'Text',
|
||||
items: [
|
||||
{
|
||||
id: CONTENT_TYPE_JSON,
|
||||
name: 'JSON',
|
||||
},
|
||||
{
|
||||
id: CONTENT_TYPE_XML,
|
||||
name: 'XML',
|
||||
},
|
||||
{
|
||||
id: CONTENT_TYPE_YAML,
|
||||
name: 'YAML',
|
||||
},
|
||||
{
|
||||
id: CONTENT_TYPE_EDN,
|
||||
name: 'EDN',
|
||||
},
|
||||
{
|
||||
id: CONTENT_TYPE_PLAINTEXT,
|
||||
name: 'Plain Text',
|
||||
},
|
||||
{
|
||||
id: CONTENT_TYPE_OTHER,
|
||||
name: 'Other',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'other',
|
||||
icon: 'ellipsis-h',
|
||||
name: 'Other',
|
||||
items: [
|
||||
{
|
||||
id: CONTENT_TYPE_FILE,
|
||||
name: 'File',
|
||||
},
|
||||
{
|
||||
id: 'no-body',
|
||||
name: 'No Body',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export const ContentTypeDropdown: FC = () => {
|
||||
const { activeRequest } = useRouteLoaderData('request/:requestId') as RequestLoaderData;
|
||||
const patchRequest = useRequestPatcher();
|
||||
@@ -54,14 +131,19 @@ export const ContentTypeDropdown: FC = () => {
|
||||
const willPreserveForm = isFormUrlEncoded && willBeMultipart;
|
||||
|
||||
if (!isEmpty && !willPreserveText && !willPreserveForm) {
|
||||
await showModal(AlertModal, {
|
||||
showAlert({
|
||||
title: 'Switch Body Type?',
|
||||
message: 'Current body will be lost. Are you sure you want to continue?',
|
||||
addCancel: true,
|
||||
onConfirm: async () => {
|
||||
patchRequest(requestId, { body: { mimeType } });
|
||||
window.main.trackSegmentEvent({ event: SegmentEvent.requestBodyTypeSelect, properties: { type: mimeType } });
|
||||
},
|
||||
});
|
||||
} else {
|
||||
patchRequest(requestId, { body: { mimeType } });
|
||||
window.main.trackSegmentEvent({ event: SegmentEvent.requestBodyTypeSelect, properties: { type: mimeType } });
|
||||
}
|
||||
patchRequest(requestId, { body: { mimeType } });
|
||||
window.main.trackSegmentEvent({ event: SegmentEvent.requestBodyTypeSelect, properties: { type: mimeType } });
|
||||
};
|
||||
|
||||
const { body } = activeRequest;
|
||||
@@ -69,137 +151,71 @@ export const ContentTypeDropdown: FC = () => {
|
||||
const hasParams = body && 'params' in body && body.params;
|
||||
const numBodyParams = hasParams ? body.params?.filter(({ disabled }) => !disabled).length : 0;
|
||||
|
||||
const getIcon = (mimeType: string | null) => {
|
||||
const contentType = activeRequest?.body && 'mimeType' in activeRequest.body ? activeRequest.body.mimeType : null;
|
||||
const contentTypeFallback = typeof contentType === 'string' ? contentType : EMPTY_MIME_TYPE;
|
||||
|
||||
return mimeType === contentTypeFallback ? 'check' : 'empty';
|
||||
};
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
aria-label='Change Body Type'
|
||||
triggerButton={
|
||||
<DropdownButton>
|
||||
<div className='flex items-center gap-2 !text-[--hl]'>
|
||||
{hasMimeType ? getContentTypeName(body.mimeType) : 'Body'}
|
||||
{numBodyParams ?
|
||||
<span className="p-2 aspect-square flex items-center color-inherit justify-between border-solid border border-[--hl-md] overflow-hidden rounded-lg text-xs shadow-small">{numBodyParams}</span>
|
||||
: null}
|
||||
<i className="fa fa-caret-down space-left" />
|
||||
</div>
|
||||
</DropdownButton>
|
||||
}
|
||||
<Select
|
||||
aria-label="Change Body Type"
|
||||
name="body-type"
|
||||
onSelectionChange={mimeType => {
|
||||
if (mimeType === 'no-body') {
|
||||
handleChangeMimeType(EMPTY_MIME_TYPE);
|
||||
} else {
|
||||
handleChangeMimeType(mimeType.toString());
|
||||
}
|
||||
}}
|
||||
selectedKey={body.mimeType ?? 'no-body'}
|
||||
>
|
||||
<DropdownSection
|
||||
aria-label='Structured Type Section'
|
||||
title={
|
||||
<span>
|
||||
<i className="fa fa-bars" /> Structured
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<DropdownItem aria-label={getContentTypeName(CONTENT_TYPE_FORM_DATA, true)}>
|
||||
<ItemContent
|
||||
icon={getIcon(CONTENT_TYPE_FORM_DATA)}
|
||||
label={getContentTypeName(CONTENT_TYPE_FORM_DATA, true)}
|
||||
onClick={() => handleChangeMimeType(CONTENT_TYPE_FORM_DATA)}
|
||||
/>
|
||||
</DropdownItem>
|
||||
<DropdownItem aria-label={getContentTypeName(CONTENT_TYPE_FORM_URLENCODED, true)}>
|
||||
<ItemContent
|
||||
icon={getIcon(CONTENT_TYPE_FORM_URLENCODED)}
|
||||
label={getContentTypeName(CONTENT_TYPE_FORM_URLENCODED, true)}
|
||||
onClick={() => handleChangeMimeType(CONTENT_TYPE_FORM_URLENCODED)}
|
||||
/>
|
||||
</DropdownItem>
|
||||
<DropdownItem aria-label={getContentTypeName(CONTENT_TYPE_GRAPHQL, true)}>
|
||||
<ItemContent
|
||||
icon={getIcon(CONTENT_TYPE_GRAPHQL)}
|
||||
label={getContentTypeName(CONTENT_TYPE_GRAPHQL, true)}
|
||||
onClick={() => handleChangeMimeType(CONTENT_TYPE_GRAPHQL)}
|
||||
/>
|
||||
</DropdownItem>
|
||||
</DropdownSection>
|
||||
|
||||
<DropdownSection
|
||||
aria-label='Text Type Section'
|
||||
title={
|
||||
<span>
|
||||
<i className="fa fa-code" /> Text
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<DropdownItem aria-label={getContentTypeName(CONTENT_TYPE_JSON, true)}>
|
||||
<ItemContent
|
||||
icon={getIcon(CONTENT_TYPE_JSON)}
|
||||
label={getContentTypeName(CONTENT_TYPE_JSON, true)}
|
||||
onClick={() => handleChangeMimeType(CONTENT_TYPE_JSON)}
|
||||
/>
|
||||
</DropdownItem>
|
||||
<DropdownItem aria-label={getContentTypeName(CONTENT_TYPE_XML, true)}>
|
||||
<ItemContent
|
||||
icon={getIcon(CONTENT_TYPE_XML)}
|
||||
label={getContentTypeName(CONTENT_TYPE_XML, true)}
|
||||
onClick={() => handleChangeMimeType(CONTENT_TYPE_XML)}
|
||||
/>
|
||||
</DropdownItem>
|
||||
<DropdownItem aria-label={getContentTypeName(CONTENT_TYPE_YAML, true)}>
|
||||
<ItemContent
|
||||
icon={getIcon(CONTENT_TYPE_YAML)}
|
||||
label={getContentTypeName(CONTENT_TYPE_YAML, true)}
|
||||
onClick={() => handleChangeMimeType(CONTENT_TYPE_YAML)}
|
||||
/>
|
||||
</DropdownItem>
|
||||
<DropdownItem aria-label={getContentTypeName(CONTENT_TYPE_EDN, true)}>
|
||||
<ItemContent
|
||||
icon={getIcon(CONTENT_TYPE_EDN)}
|
||||
label={getContentTypeName(CONTENT_TYPE_EDN, true)}
|
||||
onClick={() => handleChangeMimeType(CONTENT_TYPE_EDN)}
|
||||
/>
|
||||
</DropdownItem>
|
||||
<DropdownItem aria-label={getContentTypeName(CONTENT_TYPE_PLAINTEXT, true)}>
|
||||
<ItemContent
|
||||
icon={getIcon(CONTENT_TYPE_PLAINTEXT)}
|
||||
label={getContentTypeName(CONTENT_TYPE_PLAINTEXT, true)}
|
||||
onClick={() => handleChangeMimeType(CONTENT_TYPE_PLAINTEXT)}
|
||||
/>
|
||||
</DropdownItem>
|
||||
<DropdownItem aria-label={getContentTypeName(CONTENT_TYPE_OTHER, true)}>
|
||||
<ItemContent
|
||||
icon={getIcon(CONTENT_TYPE_OTHER)}
|
||||
label={getContentTypeName(CONTENT_TYPE_OTHER, true)}
|
||||
onClick={() => handleChangeMimeType(CONTENT_TYPE_OTHER)}
|
||||
/>
|
||||
</DropdownItem>
|
||||
</DropdownSection>
|
||||
|
||||
<DropdownSection
|
||||
aria-label='Other Type Section'
|
||||
title={
|
||||
<span>
|
||||
<i className="fa fa-ellipsis-h" /> Other
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<DropdownItem aria-label={getContentTypeName(CONTENT_TYPE_FILE, true)}>
|
||||
<ItemContent
|
||||
icon={getIcon(CONTENT_TYPE_FILE)}
|
||||
label={getContentTypeName(CONTENT_TYPE_FILE, true)}
|
||||
onClick={() => handleChangeMimeType(CONTENT_TYPE_FILE)}
|
||||
/>
|
||||
</DropdownItem>
|
||||
<DropdownItem aria-label="No Body">
|
||||
<ItemContent
|
||||
icon={getIcon(EMPTY_MIME_TYPE)}
|
||||
label="No Body"
|
||||
onClick={() => handleChangeMimeType(EMPTY_MIME_TYPE)}
|
||||
/>
|
||||
</DropdownItem>
|
||||
</DropdownSection>
|
||||
</Dropdown>
|
||||
<Button className="px-4 min-w-[12ch] py-1 font-bold flex flex-1 items-center justify-between 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">
|
||||
<SelectValue className="flex truncate items-center justify-center gap-2">
|
||||
<div className='flex items-center gap-2 text-[--hl]'>
|
||||
{hasMimeType ? getContentTypeName(body.mimeType) : 'No Body'}
|
||||
{numBodyParams ?
|
||||
<span className='p-1 min-w-6 h-6 flex items-center justify-center text-xs rounded-lg border border-solid border-[--hl]'>
|
||||
{numBodyParams}
|
||||
</span>
|
||||
: null}
|
||||
</div>
|
||||
</SelectValue>
|
||||
<Icon icon="caret-down" />
|
||||
</Button>
|
||||
<Popover className="min-w-max">
|
||||
<ListBox
|
||||
items={contentTypeSections}
|
||||
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 => (
|
||||
<Section>
|
||||
<Header className='pl-2 py-1 flex items-center gap-2 text-[--hl] text-xs uppercase'>
|
||||
<Icon icon={item.icon} /> <span>{item.name}</span>
|
||||
</Header>
|
||||
<Collection items={item.items}>
|
||||
{item => (
|
||||
<ListBoxItem
|
||||
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}
|
||||
textValue={item.name}
|
||||
>
|
||||
{({ isSelected }) => (
|
||||
<>
|
||||
<span>{item.name}</span>
|
||||
{isSelected && (
|
||||
<Icon
|
||||
icon="check"
|
||||
className="text-[--color-success] justify-self-end"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</ListBoxItem>
|
||||
)}
|
||||
</Collection>
|
||||
</Section>
|
||||
)}
|
||||
</ListBox>
|
||||
</Popover>
|
||||
</Select>
|
||||
);
|
||||
};
|
||||
|
||||
export function newBodyGraphQL(rawBody: string): RequestBody {
|
||||
try {
|
||||
// Only strip the newlines if rawBody is a parsable JSON
|
||||
|
||||
@@ -1,38 +1,76 @@
|
||||
import React, { FC } from 'react';
|
||||
import { Button, ListBox, ListBoxItem, Popover, Select, SelectValue } from 'react-aria-components';
|
||||
|
||||
import { CONTENT_TYPE_JSON, CONTENT_TYPE_PLAINTEXT } from '../../../common/constants';
|
||||
import { Dropdown, DropdownButton, DropdownItem, ItemContent } from '../base/dropdown';
|
||||
import { Icon } from '../icon';
|
||||
|
||||
interface Props {
|
||||
previewMode: string;
|
||||
onClick: (previewMode: string) => void;
|
||||
onSelect: (previewMode: string) => void;
|
||||
}
|
||||
export const WebSocketPreviewMode: FC<Props> = ({ previewMode, onClick }) => {
|
||||
|
||||
const contentTypes: {
|
||||
id: string;
|
||||
name: string;
|
||||
}[] = [
|
||||
{
|
||||
id: CONTENT_TYPE_JSON,
|
||||
name: 'JSON',
|
||||
},
|
||||
{
|
||||
id: CONTENT_TYPE_PLAINTEXT,
|
||||
name: 'Raw',
|
||||
},
|
||||
];
|
||||
|
||||
export const WebSocketPreviewMode: FC<Props> = ({ previewMode, onSelect }) => {
|
||||
return (
|
||||
<Dropdown
|
||||
aria-label="Websocket Preview Mode Dropdown"
|
||||
triggerButton={
|
||||
<DropdownButton className="tall !text-[--hl]">
|
||||
{{
|
||||
[CONTENT_TYPE_JSON]: 'JSON',
|
||||
[CONTENT_TYPE_PLAINTEXT]: 'Raw',
|
||||
}[previewMode]}
|
||||
<i className="fa fa-caret-down space-left" />
|
||||
</DropdownButton>
|
||||
}
|
||||
<Select
|
||||
aria-label="Change Body Type"
|
||||
name="body-type"
|
||||
onSelectionChange={contentType => {
|
||||
onSelect(contentType.toString());
|
||||
}}
|
||||
selectedKey={previewMode}
|
||||
>
|
||||
<DropdownItem aria-label='JSON'>
|
||||
<ItemContent
|
||||
label="JSON"
|
||||
onClick={() => onClick(CONTENT_TYPE_JSON)}
|
||||
/>
|
||||
</DropdownItem>
|
||||
<DropdownItem aria-label='Raw'>
|
||||
<ItemContent
|
||||
label="Raw"
|
||||
onClick={() => onClick(CONTENT_TYPE_PLAINTEXT)}
|
||||
/>
|
||||
</DropdownItem>
|
||||
</Dropdown>
|
||||
<Button className="px-4 min-w-[12ch] py-1 font-bold flex flex-1 items-center justify-between 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">
|
||||
<SelectValue<{ id: string; name: string }>
|
||||
className="flex truncate items-center justify-center gap-2"
|
||||
>
|
||||
{({ selectedText }) => (
|
||||
<div className='flex items-center gap-2 text-[--hl]'>
|
||||
{selectedText}
|
||||
</div>
|
||||
)}
|
||||
</SelectValue>
|
||||
<Icon icon="caret-down" />
|
||||
</Button>
|
||||
<Popover className="min-w-max">
|
||||
<ListBox
|
||||
items={contentTypes}
|
||||
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 => (
|
||||
<ListBoxItem
|
||||
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}
|
||||
textValue={item.name}
|
||||
>
|
||||
{({ isSelected }) => (
|
||||
<>
|
||||
<span>{item.name}</span>
|
||||
{isSelected && (
|
||||
<Icon
|
||||
icon="check"
|
||||
className="text-[--color-success] justify-self-end"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</ListBoxItem>
|
||||
)}
|
||||
</ListBox>
|
||||
</Popover>
|
||||
</Select>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { FC, ReactNode } from 'react';
|
||||
import { Toolbar } from 'react-aria-components';
|
||||
|
||||
import {
|
||||
AUTH_API_KEY,
|
||||
@@ -13,8 +14,9 @@ import {
|
||||
AUTH_OAUTH_1,
|
||||
AUTH_OAUTH_2,
|
||||
} from '../../../../common/constants';
|
||||
import { RequestAuthentication } from '../../../../models/request';
|
||||
import { AuthTypes, RequestAuthentication } from '../../../../models/request';
|
||||
import { getAuthObjectOrNull } from '../../../../network/authentication';
|
||||
import { AuthDropdown } from '../../dropdowns/auth-dropdown';
|
||||
import { ApiKeyAuth } from './api-key-auth';
|
||||
import { AsapAuth } from './asap-auth';
|
||||
import { AWSAuth } from './aws-auth';
|
||||
@@ -27,7 +29,7 @@ import { NTLMAuth } from './ntlm-auth';
|
||||
import { OAuth1Auth } from './o-auth-1-auth';
|
||||
import { OAuth2Auth } from './o-auth-2-auth';
|
||||
|
||||
export const AuthWrapper: FC<{ authentication?: RequestAuthentication | {}; disabled?: boolean }> = ({ authentication, disabled = false }) => {
|
||||
export const AuthWrapper: FC<{ authentication?: RequestAuthentication | {}; disabled?: boolean; authTypes?: AuthTypes[] }> = ({ authentication, disabled = false, authTypes }) => {
|
||||
const type = getAuthObjectOrNull(authentication)?.type || '';
|
||||
let authBody: ReactNode = null;
|
||||
|
||||
@@ -55,8 +57,8 @@ export const AuthWrapper: FC<{ authentication?: RequestAuthentication | {}; disa
|
||||
authBody = <AsapAuth />;
|
||||
} else {
|
||||
authBody = (
|
||||
<div className="vertically-center text-center">
|
||||
<p className="pad super-faint text-sm text-center">
|
||||
<div className="flex w-full h-full select-none items-center justify-center">
|
||||
<p className="text-sm text-center p-4 text-[--hl]">
|
||||
<i
|
||||
className="fa fa-unlock-alt"
|
||||
style={{
|
||||
@@ -72,5 +74,12 @@ export const AuthWrapper: FC<{ authentication?: RequestAuthentication | {}; disa
|
||||
);
|
||||
}
|
||||
|
||||
return <div>{authBody}</div>;
|
||||
return <>
|
||||
<Toolbar className="w-full flex-shrink-0 h-[--line-height-sm] border-b border-solid border-[--hl-md] flex items-center px-2">
|
||||
<AuthDropdown authentication={authentication} authTypes={authTypes} />
|
||||
</Toolbar>
|
||||
<div className='flex-1 overflow-y-auto '>
|
||||
{authBody}
|
||||
</div>
|
||||
</>;
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import clone from 'clone';
|
||||
import { lookup } from 'mime-types';
|
||||
import React, { FC, useCallback } from 'react';
|
||||
import { Toolbar } from 'react-aria-components';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import {
|
||||
@@ -19,6 +20,7 @@ import {
|
||||
} from '../../../../models/request';
|
||||
import { NunjucksEnabledProvider } from '../../../context/nunjucks/nunjucks-enabled-context';
|
||||
import { useRequestPatcher } from '../../../hooks/use-request';
|
||||
import { ContentTypeDropdown } from '../../dropdowns/content-type-dropdown';
|
||||
import { AskModal } from '../../modals/ask-modal';
|
||||
import { showModal } from '../../modals/index';
|
||||
import { EmptyStatePane } from '../../panes/empty-state-pane';
|
||||
@@ -108,7 +110,7 @@ export const BodyEditor: FC<Props> = ({
|
||||
const mimeType = request.body.mimeType;
|
||||
const isBodyEmpty = typeof mimeType !== 'string' && !request.body.text;
|
||||
|
||||
const _render = () => {
|
||||
function renderBodyEditor() {
|
||||
if (mimeType === CONTENT_TYPE_FORM_URLENCODED) {
|
||||
return <UrlEncodedEditor key={uniqueKey} onChange={handleFormUrlEncodedChange} parameters={request.body.params || []} />;
|
||||
} else if (mimeType === CONTENT_TYPE_FORM_DATA) {
|
||||
@@ -121,14 +123,31 @@ export const BodyEditor: FC<Props> = ({
|
||||
const contentType = getContentTypeFromHeaders(request.headers) || mimeType;
|
||||
return <RawEditor uniquenessKey={uniqueKey} contentType={contentType || 'text/plain'} content={request.body.text || ''} onChange={handleRawChange} />;
|
||||
} else if (isEventStreamRequest(request)) {
|
||||
return <EmptyStatePane
|
||||
icon={<i className="fa fa-paper-plane" />}
|
||||
documentationLinks={[]}
|
||||
title="Enter a URL and connect to start receiving event stream data"
|
||||
/>;
|
||||
return (
|
||||
<EmptyStatePane
|
||||
icon={<i className="fa fa-paper-plane" />}
|
||||
documentationLinks={[]}
|
||||
title="Enter a URL and connect to start receiving event stream data"
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<EmptyStatePane
|
||||
icon={<SvgIcon icon="bug" />}
|
||||
documentationLinks={[documentationLinks.introductionToInsomnia]}
|
||||
secondaryAction="Select a body type from above to send data in the body of a request"
|
||||
title="Enter a URL and send to get a response"
|
||||
/>
|
||||
);
|
||||
}
|
||||
return <EmptyStatePane icon={<SvgIcon icon="bug" />} documentationLinks={[documentationLinks.introductionToInsomnia]} secondaryAction="Select a body type from above to send data in the body of a request" title="Enter a URL and send to get a response" />;
|
||||
};
|
||||
}
|
||||
|
||||
return <NunjucksEnabledProvider disable={noRender}>{_render()}</NunjucksEnabledProvider>;
|
||||
return (
|
||||
<NunjucksEnabledProvider disable={noRender}>
|
||||
<Toolbar className="w-full flex-shrink-0 h-[--line-height-sm] border-b border-solid border-[--hl-md] flex items-center px-2">
|
||||
<ContentTypeDropdown />
|
||||
</Toolbar>
|
||||
{renderBodyEditor()}
|
||||
</NunjucksEnabledProvider>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -8,8 +8,9 @@ import { DefinitionNode, DocumentNode, GraphQLNonNull, GraphQLSchema, Kind, NonN
|
||||
import { buildClientSchema, getIntrospectionQuery } from 'graphql/utilities';
|
||||
import { Maybe } from 'graphql-language-service';
|
||||
import React, { FC, useEffect, useRef, useState } from 'react';
|
||||
import { Button, Toolbar } from 'react-aria-components';
|
||||
import { Button, Group, Heading, Toolbar, Tooltip, TooltipTrigger } from 'react-aria-components';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels';
|
||||
import { useLocalStorage } from 'react-use';
|
||||
|
||||
import { CONTENT_TYPE_JSON } from '../../../../common/constants';
|
||||
@@ -28,6 +29,7 @@ import { CodeEditor, CodeEditorHandle } from '../../codemirror/code-editor';
|
||||
import { GraphQLExplorer } from '../../graph-ql-explorer/graph-ql-explorer';
|
||||
import { ActiveReference } from '../../graph-ql-explorer/graph-ql-types';
|
||||
import { HelpTooltip } from '../../help-tooltip';
|
||||
import { Icon } from '../../icon';
|
||||
import { useDocBodyKeyboardShortcuts } from '../../keydown-binder';
|
||||
import { TimeFromNow } from '../../time-from-now';
|
||||
|
||||
@@ -174,7 +176,6 @@ interface Props {
|
||||
interface State {
|
||||
body: GraphQLBody;
|
||||
operations: string[];
|
||||
hideSchemaFetchErrors: boolean;
|
||||
variablesSyntaxError: string;
|
||||
explorerVisible: boolean;
|
||||
activeReference: null | ActiveReference;
|
||||
@@ -214,7 +215,6 @@ export const GraphQLEditor: FC<Props> = ({
|
||||
operationName,
|
||||
},
|
||||
operations,
|
||||
hideSchemaFetchErrors: false,
|
||||
variablesSyntaxError: '',
|
||||
activeReference: null,
|
||||
explorerVisible: false,
|
||||
@@ -343,16 +343,16 @@ export const GraphQLEditor: FC<Props> = ({
|
||||
return '';
|
||||
}
|
||||
if (schemaIsFetching) {
|
||||
return 'fetching schema...';
|
||||
return 'Fetching schema...';
|
||||
}
|
||||
if (schemaLastFetchTime > 0) {
|
||||
return (
|
||||
<span>
|
||||
schema fetched <TimeFromNow timestamp={schemaLastFetchTime} />
|
||||
Schema fetched <TimeFromNow timestamp={schemaLastFetchTime} />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return <span>schema not yet fetched</span>;
|
||||
return <span>Schema not fetched yet</span>;
|
||||
};
|
||||
|
||||
const loadAndSetLocalSchema = async () => {
|
||||
@@ -393,7 +393,6 @@ export const GraphQLEditor: FC<Props> = ({
|
||||
};
|
||||
|
||||
const {
|
||||
hideSchemaFetchErrors,
|
||||
variablesSyntaxError,
|
||||
activeReference,
|
||||
explorerVisible,
|
||||
@@ -468,13 +467,13 @@ export const GraphQLEditor: FC<Props> = ({
|
||||
}
|
||||
const canShowSchema = schema && !schemaIsFetching && !schemaFetchError && schemaLastFetchTime > 0;
|
||||
return (
|
||||
<div className="graphql-editor">
|
||||
<Toolbar className="content-box sticky top-0 z-10 bg-[var(--color-bg)] flex flex-row border-b border-[var(--hl-md)] h-[var(--line-height-sm)] text-[var(--font-size-sm)]">
|
||||
<>
|
||||
<Toolbar aria-label='GraphQL toolbar' className="w-full flex-shrink-0 h-[--line-height-sm] border-b border-solid border-[--hl-md] flex items-center px-2">
|
||||
<Dropdown
|
||||
aria-label='Operations Dropdown'
|
||||
isDisabled={!state.operations.length}
|
||||
triggerButton={
|
||||
<DropdownButton className="btn btn--compact">
|
||||
<DropdownButton className="btn btn--compact text-[var(--hl)] p-[var(--padding-xs)] h-full">
|
||||
{state.body.operationName || 'Operations'}
|
||||
</DropdownButton>
|
||||
}
|
||||
@@ -495,7 +494,7 @@ export const GraphQLEditor: FC<Props> = ({
|
||||
aria-label='Schema Dropdown'
|
||||
triggerButton={
|
||||
<DropdownButton
|
||||
className="btn btn--compact"
|
||||
className="btn btn--compact text-[var(--hl)] p-[var(--padding-xs)] h-full"
|
||||
disableHoverBehavior={false}
|
||||
removeBorderRadius
|
||||
>
|
||||
@@ -523,9 +522,6 @@ export const GraphQLEditor: FC<Props> = ({
|
||||
icon={`refresh ${schemaIsFetching ? 'fa-spin' : ''}`}
|
||||
label="Refresh Schema"
|
||||
onClick={async () => {
|
||||
// First, "forget" preference to hide errors so they always show
|
||||
// again after a refresh
|
||||
setState(state => ({ ...state, hideSchemaFetchErrors: false }));
|
||||
setSchemaIsFetching(true);
|
||||
const newState = await fetchGraphQLSchemaForRequest({
|
||||
requestId: request._id,
|
||||
@@ -573,7 +569,6 @@ export const GraphQLEditor: FC<Props> = ({
|
||||
</>
|
||||
}
|
||||
onClick={() => {
|
||||
setState(state => ({ ...state, hideSchemaFetchErrors: false }));
|
||||
loadAndSetLocalSchema();
|
||||
}}
|
||||
/>
|
||||
@@ -581,80 +576,85 @@ export const GraphQLEditor: FC<Props> = ({
|
||||
</DropdownSection>
|
||||
</Dropdown>
|
||||
</Toolbar>
|
||||
|
||||
<div className="graphql-editor__query">
|
||||
<CodeEditor
|
||||
id="graphql-editor"
|
||||
ref={editorRef}
|
||||
dynamicHeight
|
||||
showPrettifyButton
|
||||
uniquenessKey={uniquenessKey ? uniquenessKey + '::query' : undefined}
|
||||
defaultValue={requestBody.query || ''}
|
||||
className={className}
|
||||
onChange={changeQuery}
|
||||
mode="graphql"
|
||||
placeholder=""
|
||||
hintOptions={graphqlOptions?.hintOptions}
|
||||
infoOptions={graphqlOptions?.infoOptions}
|
||||
jumpOptions={graphqlOptions?.jumpOptions}
|
||||
lintOptions={graphqlOptions?.lintOptions}
|
||||
/>
|
||||
</div>
|
||||
<div className="graphql-editor__schema-error">
|
||||
{!hideSchemaFetchErrors && schemaFetchError && (
|
||||
<div className="notice error margin no-margin-top margin-bottom-sm">
|
||||
<div className="pull-right">
|
||||
<button
|
||||
className="icon"
|
||||
onClick={() => setState(state => ({ ...state, hideSchemaFetchErrors: true }))}
|
||||
>
|
||||
<i className="fa fa-times" />
|
||||
</button>
|
||||
</div>
|
||||
{schemaFetchError.message}
|
||||
<br />
|
||||
<PanelGroup direction={'vertical'}>
|
||||
<Panel id="GraphQL Editor" minSize={20} defaultSize={60}>
|
||||
<CodeEditor
|
||||
id="graphql-editor"
|
||||
ref={editorRef}
|
||||
dynamicHeight
|
||||
showPrettifyButton
|
||||
uniquenessKey={uniquenessKey ? uniquenessKey + '::query' : undefined}
|
||||
defaultValue={requestBody.query || ''}
|
||||
className={className}
|
||||
onChange={changeQuery}
|
||||
mode="graphql"
|
||||
placeholder=""
|
||||
hintOptions={graphqlOptions?.hintOptions}
|
||||
infoOptions={graphqlOptions?.infoOptions}
|
||||
jumpOptions={graphqlOptions?.jumpOptions}
|
||||
lintOptions={graphqlOptions?.lintOptions}
|
||||
/>
|
||||
</Panel>
|
||||
<PanelResizeHandle className={'w-full h-[1px] bg-[--hl-md]'} />
|
||||
<Panel id="GraphQL Variables editor" className='flex flex-col' minSize={20}>
|
||||
<Heading className="w-full px-2 text-[--hl] select-none flex-shrink-0 h-[--line-height-sm] border-b border-solid border-[--hl-md] flex items-center">
|
||||
Query Variables
|
||||
<HelpTooltip className="space-left">
|
||||
Variables to use in GraphQL query <br />
|
||||
(JSON format)
|
||||
</HelpTooltip>
|
||||
{variablesSyntaxError && (
|
||||
<span className="text-danger italic pull-right">{variablesSyntaxError}</span>
|
||||
)}
|
||||
</Heading>
|
||||
<div className='flex-1 overflow-hidden'>
|
||||
<CodeEditor
|
||||
id="graphql-editor-variables"
|
||||
dynamicHeight
|
||||
enableNunjucks
|
||||
uniquenessKey={uniquenessKey ? uniquenessKey + '::variables' : undefined}
|
||||
showPrettifyButton={false}
|
||||
defaultValue={jsonPrettify(requestBody.variables)}
|
||||
className={className}
|
||||
getAutocompleteConstants={() => Object.keys(variableTypes)}
|
||||
lintOptions={{
|
||||
variableToType: variableTypes,
|
||||
}}
|
||||
noLint={!variableTypes}
|
||||
onChange={changeVariables}
|
||||
mode="graphql-variables"
|
||||
placeholder=""
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="graphql-editor__meta">
|
||||
{renderSchemaFetchMessage()}
|
||||
</div>
|
||||
<h2 className="no-margin pad-left-sm pad-top-sm pad-bottom-sm">
|
||||
Query Variables
|
||||
<HelpTooltip className="space-left">
|
||||
Variables to use in GraphQL query <br />
|
||||
(JSON format)
|
||||
</HelpTooltip>
|
||||
{variablesSyntaxError && (
|
||||
<span className="text-danger italic pull-right">{variablesSyntaxError}</span>
|
||||
)}
|
||||
</h2>
|
||||
<div className="graphql-editor__variables">
|
||||
<CodeEditor
|
||||
id="graphql-editor-variables"
|
||||
dynamicHeight
|
||||
enableNunjucks
|
||||
uniquenessKey={uniquenessKey ? uniquenessKey + '::variables' : undefined}
|
||||
showPrettifyButton={false}
|
||||
defaultValue={jsonPrettify(requestBody.variables)}
|
||||
className={className}
|
||||
getAutocompleteConstants={() => Object.keys(variableTypes)}
|
||||
lintOptions={{
|
||||
variableToType: variableTypes,
|
||||
}}
|
||||
noLint={!variableTypes}
|
||||
onChange={changeVariables}
|
||||
mode="graphql-variables"
|
||||
placeholder=""
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-row items-center border-solid border-t border-[--hl-md] h-[--line-height-sm] text-[--font-size-sm]">
|
||||
<Button className="px-4 py-1 h-full flex items-center justify-center gap-2 aria-pressed:bg-[--hl-sm] text-[--color-font] text-xs hover:bg-[--hl-xs] focus:ring-inset ring-1 ring-transparent focus:ring-[--hl-md] transition-all" onPress={beautifyRequestBody}>
|
||||
</Panel>
|
||||
</PanelGroup>
|
||||
<Toolbar className="w-full overflow-y-auto select-none flex-shrink-0 h-[--line-height-sm] border-t border-solid border-[--hl-md] flex items-center">
|
||||
<Button className="px-4 py-1 h-full flex items-center justify-center gap-2 aria-pressed:bg-[--hl-sm] text-[--color-font] text-sm hover:bg-[--hl-xs] focus:ring-inset ring-1 ring-transparent focus:ring-[--hl-md] transition-all" onPress={beautifyRequestBody}>
|
||||
Prettify GraphQL
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<span className='flex-1' />
|
||||
{!schemaFetchError && <div className="flex flex-shrink-0 items-center gap-2 text-sm px-2">
|
||||
<Icon icon="info-circle" />
|
||||
{renderSchemaFetchMessage()}
|
||||
</div>}
|
||||
{schemaFetchError && (
|
||||
<Group className="flex items-center h-full">
|
||||
<TooltipTrigger>
|
||||
<Button className="px-4 py-1 h-full flex items-center justify-center gap-2 aria-pressed:bg-[--hl-sm] text-[--color-font] text-sm hover:bg-[--hl-xs] focus:ring-inset ring-1 ring-transparent focus:ring-[--hl-md] transition-all">
|
||||
<Icon icon="exclamation-triangle" className='text-[--color-warning]' />
|
||||
<span>Error fetching Schema</span>
|
||||
</Button>
|
||||
<Tooltip
|
||||
offset={8}
|
||||
className="border select-none text-sm max-w-xs border-solid border-[--hl-sm] shadow-lg bg-[--color-bg] text-[--color-font] px-4 py-2 rounded-md overflow-y-auto max-h-[85vh] focus:outline-none"
|
||||
>
|
||||
{schemaFetchError.message}
|
||||
</Tooltip>
|
||||
</TooltipTrigger>
|
||||
</Group>
|
||||
)}
|
||||
</Toolbar>
|
||||
{graphQLExplorerPortal}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { Snippet } from 'codemirror';
|
||||
import { CookieObject, Environment, InsomniaObject, Request as ScriptRequest, RequestInfo, Url, Variables } from 'insomnia-sdk';
|
||||
import React, { FC, useRef } from 'react';
|
||||
import { Button, Collection, Header, Menu, MenuItem, MenuTrigger, Popover, Section, Toolbar } from 'react-aria-components';
|
||||
|
||||
import { Settings } from '../../../models/settings';
|
||||
import { translateHandlersInScript } from '../../../utils/importers/importers/postman';
|
||||
import { Dropdown, DropdownButton, DropdownItem, DropdownSection, ItemContent } from '../base/dropdown';
|
||||
import { CodeEditor, CodeEditorHandle } from '../codemirror/code-editor';
|
||||
import { Icon } from '../icon';
|
||||
|
||||
interface Props {
|
||||
onChange: (value: string) => void;
|
||||
@@ -139,6 +140,207 @@ function getRequestScriptSnippets(insomniaObject: InsomniaObject, path: string):
|
||||
return snippets;
|
||||
}
|
||||
|
||||
interface SnippetMenuItem {
|
||||
id: string;
|
||||
name: string;
|
||||
items: ({
|
||||
id: string;
|
||||
name: string;
|
||||
snippet: string;
|
||||
} | {
|
||||
id: string;
|
||||
name: string;
|
||||
items: {
|
||||
id: string;
|
||||
name: string;
|
||||
snippet: string;
|
||||
}[];
|
||||
})[];
|
||||
}
|
||||
|
||||
const variableSnippetsMenu: SnippetMenuItem = {
|
||||
'id': 'variable-snippets',
|
||||
'name': 'Variable Snippets',
|
||||
items: [
|
||||
{
|
||||
'id': 'get-values',
|
||||
'name': 'Get values',
|
||||
items: [
|
||||
{
|
||||
'id': 'get-env-var',
|
||||
'name': 'Get an environment variable',
|
||||
'snippet': getEnvVar,
|
||||
},
|
||||
// {
|
||||
// "id": "get-glb-var",
|
||||
// "name": "Get a global variable",
|
||||
// "snippet": getGlbVar,
|
||||
// },
|
||||
{
|
||||
'id': 'get-var',
|
||||
'name': 'Get a variable',
|
||||
'snippet': getVar,
|
||||
},
|
||||
{
|
||||
'id': 'get-collection-var',
|
||||
'name': 'Get a collection variable',
|
||||
'snippet': getCollectionVar,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'set-values',
|
||||
name: 'Set values',
|
||||
items: [
|
||||
{
|
||||
'id': 'set-env-var',
|
||||
'name': 'Set an environment variable',
|
||||
'snippet': setEnvVar,
|
||||
},
|
||||
// {
|
||||
// "id": "set-glb-var",
|
||||
// "name": "Set a global variable",
|
||||
// "snippet": setGlbVar,
|
||||
// },
|
||||
{
|
||||
'id': 'set-var',
|
||||
'name': 'Set a variable',
|
||||
'snippet': setVar,
|
||||
},
|
||||
{
|
||||
'id': 'set-collection-var',
|
||||
'name': 'Set a collection variable',
|
||||
'snippet': setCollectionVar,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'clear-values',
|
||||
name: 'Clear values',
|
||||
items: [
|
||||
{
|
||||
'id': 'unset-env-var',
|
||||
'name': 'Clear an environment variable',
|
||||
'snippet': unsetEnvVar,
|
||||
},
|
||||
// {
|
||||
// "id": "unset-glb-var",
|
||||
// "name": "Clear a global variable",
|
||||
// "snippet": unsetGlbVar,
|
||||
// },
|
||||
{
|
||||
'id': 'unset-collection-var',
|
||||
'name': 'Clear a collection variable',
|
||||
'snippet': unsetCollectionVar,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const requestManipulationMenu: SnippetMenuItem = {
|
||||
id: 'request-manipulation',
|
||||
name: 'Request Manipulation',
|
||||
items: [
|
||||
{
|
||||
'id': 'add-query-param',
|
||||
'name': 'Add query param',
|
||||
'snippet': addQueryParams,
|
||||
},
|
||||
{
|
||||
'id': 'set-method',
|
||||
'name': 'Set method',
|
||||
'snippet': setMethod,
|
||||
},
|
||||
{
|
||||
'id': 'add-header',
|
||||
'name': 'Add a header',
|
||||
'snippet': addHeader,
|
||||
},
|
||||
{
|
||||
'id': 'remove-header',
|
||||
'name': 'Remove header',
|
||||
'snippet': removeHeader,
|
||||
},
|
||||
{
|
||||
'id': 'update-body-raw',
|
||||
'name': 'Update body as raw',
|
||||
'snippet': updateRequestBody,
|
||||
},
|
||||
{
|
||||
'id': 'update-auth-method',
|
||||
'name': 'Update auth method',
|
||||
'snippet': updateRequestAuth,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const responseHandlingMenu: SnippetMenuItem = {
|
||||
id: 'response-handling',
|
||||
name: 'Response Handling',
|
||||
items: [
|
||||
{
|
||||
'id': 'get-status-code',
|
||||
'name': 'Get status code',
|
||||
'snippet': getStatusCode,
|
||||
},
|
||||
{
|
||||
'id': 'get-status-message',
|
||||
'name': 'Get status message',
|
||||
'snippet': getStatusMsg,
|
||||
},
|
||||
{
|
||||
'id': 'get-response-time',
|
||||
'name': 'Get response time',
|
||||
'snippet': getRespTime,
|
||||
},
|
||||
{
|
||||
'id': 'get-body-json',
|
||||
'name': 'Get body as JSON',
|
||||
'snippet': getJsonBody,
|
||||
},
|
||||
{
|
||||
'id': 'get-body-text',
|
||||
'name': 'Get body as text',
|
||||
'snippet': getTextBody,
|
||||
},
|
||||
{
|
||||
'id': 'find-header',
|
||||
'name': 'Find a header by name',
|
||||
'snippet': findHeader,
|
||||
},
|
||||
{
|
||||
'id': 'get-cookies',
|
||||
'name': 'Get cookies',
|
||||
'snippet': getCookies,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const miscMenu: SnippetMenuItem = {
|
||||
id: 'misc',
|
||||
name: 'Misc',
|
||||
items: [
|
||||
{
|
||||
'id': 'send-request',
|
||||
'name': 'Send a request',
|
||||
'snippet': sendReq,
|
||||
},
|
||||
{
|
||||
'id': 'print-log',
|
||||
'name': 'Print log',
|
||||
'snippet': logValue,
|
||||
},
|
||||
{
|
||||
'id': 'require-module',
|
||||
'name': 'Require a module',
|
||||
'snippet': requireAModule,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const snippetsMenus: SnippetMenuItem[] = [variableSnippetsMenu, requestManipulationMenu, responseHandlingMenu, miscMenu];
|
||||
|
||||
export const RequestScriptEditor: FC<Props> = ({
|
||||
className,
|
||||
defaultValue,
|
||||
@@ -194,6 +396,7 @@ export const RequestScriptEditor: FC<Props> = ({
|
||||
cookies: [],
|
||||
}),
|
||||
requestInfo: new RequestInfo({
|
||||
// @TODO - Look into this event name when we introduce iteration data
|
||||
eventName: 'prerequest',
|
||||
iteration: 1,
|
||||
iterationCount: 1,
|
||||
@@ -208,289 +411,63 @@ export const RequestScriptEditor: FC<Props> = ({
|
||||
);
|
||||
|
||||
return (
|
||||
<div className='h-full flex flex-col'>
|
||||
<div className="flex-1">
|
||||
<CodeEditor
|
||||
id={`script-editor-${uniquenessKey}`}
|
||||
key={uniquenessKey}
|
||||
disableContextMenu={true}
|
||||
showPrettifyButton={true}
|
||||
uniquenessKey={uniquenessKey}
|
||||
defaultValue={defaultValue}
|
||||
className={className}
|
||||
onChange={onChange}
|
||||
mode='text/javascript'
|
||||
placeholder="..."
|
||||
lintOptions={lintOptions}
|
||||
ref={editorRef}
|
||||
getAutocompleteSnippets={() => requestScriptSnippets}
|
||||
onPaste={translateHandlersInScript}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-row border-solid border-t border-[var(--hl-md)] h-[var(--line-height-sm)] text-[var(--font-size-sm)] box-border overflow-x-auto">
|
||||
<Dropdown
|
||||
aria-label='Variable Snippets'
|
||||
placement='top left'
|
||||
triggerButton={
|
||||
<DropdownButton>
|
||||
<ItemContent
|
||||
icon="code"
|
||||
label='Variable Snippets'
|
||||
/>
|
||||
</DropdownButton>
|
||||
}
|
||||
>
|
||||
<DropdownSection
|
||||
aria-label="Get values"
|
||||
title="Get values"
|
||||
>
|
||||
<div className='h-full flex flex-col divide-y divide-solid divide-[--hl-md]'>
|
||||
<CodeEditor
|
||||
id={`script-editor-${uniquenessKey}`}
|
||||
key={uniquenessKey}
|
||||
disableContextMenu={true}
|
||||
showPrettifyButton={true}
|
||||
uniquenessKey={uniquenessKey}
|
||||
defaultValue={defaultValue}
|
||||
className={className}
|
||||
onChange={onChange}
|
||||
mode='text/javascript'
|
||||
placeholder="..."
|
||||
lintOptions={lintOptions}
|
||||
ref={editorRef}
|
||||
getAutocompleteSnippets={() => requestScriptSnippets}
|
||||
onPaste={translateHandlersInScript}
|
||||
/>
|
||||
<Toolbar className="flex items-center h-[--line-height-sm] flex-shrink-0 flex-row text-[var(--font-size-sm)] box-border overflow-x-auto">
|
||||
{snippetsMenus.map(menu => (
|
||||
<MenuTrigger key={menu.id}>
|
||||
<Button className="flex gap-2 px-2 items-center justify-center h-full aria-pressed:bg-[--hl-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="code" />
|
||||
{menu.name}
|
||||
</Button>
|
||||
<Popover className="min-w-max">
|
||||
<Menu
|
||||
aria-label="Create a new request"
|
||||
selectionMode="single"
|
||||
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"
|
||||
items={menu.items}
|
||||
>
|
||||
{item => {
|
||||
if ('items' in item) {
|
||||
return (
|
||||
<Section>
|
||||
<Header className='pl-2 py-1 text-[--hl] text-xs uppercase'>
|
||||
{item.name}
|
||||
</Header>
|
||||
<Collection items={item.items}>
|
||||
{item => (
|
||||
<MenuItem onAction={() => addSnippet(item.snippet)} 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" key={item.name}>{item.name}</MenuItem>
|
||||
)}
|
||||
</Collection>
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
|
||||
<DropdownItem textValue='Get an environment variable' arial-label={'Get an environment variable'}>
|
||||
<ItemContent
|
||||
icon="sliders"
|
||||
label='Get an environment variable'
|
||||
onClick={() => addSnippet(getEnvVar)}
|
||||
/>
|
||||
</DropdownItem>
|
||||
{/* <DropdownItem textValue='Get a global variable' arial-label={'Get a global variable'}>
|
||||
<ItemContent
|
||||
icon="sliders"
|
||||
label='Get a global variable'
|
||||
onClick={() => addSnippet(getGlbVar)}
|
||||
/>
|
||||
</DropdownItem> */}
|
||||
<DropdownItem textValue='Get a variable' arial-label={'Get a variable'}>
|
||||
<ItemContent
|
||||
icon="sliders"
|
||||
label='Get a variable'
|
||||
onClick={() => addSnippet(getVar)}
|
||||
/>
|
||||
</DropdownItem>
|
||||
<DropdownItem textValue='Get a collection variable' arial-label={'Get a collection variable'}>
|
||||
<ItemContent
|
||||
icon="sliders"
|
||||
label='Get a collection variable'
|
||||
onClick={() => addSnippet(getCollectionVar)}
|
||||
/>
|
||||
</DropdownItem>
|
||||
</DropdownSection>
|
||||
return (
|
||||
<MenuItem onAction={() => addSnippet(item.snippet)} 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" key={item.name}>{item.name}</MenuItem>
|
||||
);
|
||||
}}
|
||||
</Menu>
|
||||
</Popover>
|
||||
</MenuTrigger>
|
||||
))}
|
||||
|
||||
<DropdownSection
|
||||
aria-label="Set values"
|
||||
title="Set values"
|
||||
>
|
||||
<DropdownItem textValue='Set an environment variable' arial-label={'Set an environment variable'}>
|
||||
<ItemContent
|
||||
icon="circle-plus"
|
||||
label='Set an environment variable'
|
||||
onClick={() => addSnippet(setEnvVar)}
|
||||
/>
|
||||
</DropdownItem>
|
||||
{/* <DropdownItem textValue='Set a global variable' arial-label={'Set a global variable'}>
|
||||
<ItemContent
|
||||
icon="circle-plus"
|
||||
label='Set a global variable'
|
||||
onClick={() => addSnippet(setGlbVar)}
|
||||
/>
|
||||
</DropdownItem> */}
|
||||
<DropdownItem textValue='Set a variable' arial-label={'Set a variable'}>
|
||||
<ItemContent
|
||||
icon="circle-plus"
|
||||
label='Set a variable'
|
||||
onClick={() => addSnippet(setVar)}
|
||||
/>
|
||||
</DropdownItem>
|
||||
<DropdownItem textValue='Set a collection variable' arial-label={'Set a collection variable'}>
|
||||
<ItemContent
|
||||
icon="circle-plus"
|
||||
label='Set a collection variable'
|
||||
onClick={() => addSnippet(setCollectionVar)}
|
||||
/>
|
||||
</DropdownItem>
|
||||
</DropdownSection>
|
||||
|
||||
<DropdownSection
|
||||
aria-label="Clear values"
|
||||
title="Clear values"
|
||||
>
|
||||
<DropdownItem textValue='Clear an environment variable' arial-label={'Clear an environment variable'}>
|
||||
<ItemContent
|
||||
icon="circle-minus"
|
||||
label='Clear an environment variable'
|
||||
onClick={() => addSnippet(unsetEnvVar)}
|
||||
/>
|
||||
</DropdownItem>
|
||||
{/* <DropdownItem textValue='Clear a global variable' arial-label={'Clear a global variable'}>
|
||||
<ItemContent
|
||||
icon="circle-minus"
|
||||
label='Clear a global variable'
|
||||
onClick={() => addSnippet(unsetGlbVar)}
|
||||
/>
|
||||
</DropdownItem> */}
|
||||
<DropdownItem textValue='Clear a collection variable' arial-label={'Clear a collection variable'}>
|
||||
<ItemContent
|
||||
icon="circle-minus"
|
||||
label='Clear a collection variable'
|
||||
onClick={() => addSnippet(unsetCollectionVar)}
|
||||
/>
|
||||
</DropdownItem>
|
||||
</DropdownSection>
|
||||
</Dropdown>
|
||||
|
||||
<Dropdown
|
||||
aria-label='Request Manipulation'
|
||||
placement='top left'
|
||||
triggerButton={
|
||||
<DropdownButton>
|
||||
<ItemContent
|
||||
icon="code"
|
||||
label='Request Manipulation'
|
||||
/>
|
||||
</DropdownButton>
|
||||
}
|
||||
>
|
||||
<DropdownItem textValue='Add query param' arial-label={'Add query param'}>
|
||||
<ItemContent
|
||||
icon="circle-plus"
|
||||
label='Add a query param'
|
||||
onClick={() => addSnippet(addQueryParams)}
|
||||
/>
|
||||
</DropdownItem>
|
||||
<DropdownItem textValue='Set method' arial-label={'Set method'}>
|
||||
<ItemContent
|
||||
icon="circle-info"
|
||||
label='Set method'
|
||||
onClick={() => addSnippet(setMethod)}
|
||||
/>
|
||||
</DropdownItem>
|
||||
<DropdownItem textValue='Add a header' arial-label={'Add a header'}>
|
||||
<ItemContent
|
||||
icon="circle-plus"
|
||||
label='Add a header'
|
||||
onClick={() => addSnippet(addHeader)}
|
||||
/>
|
||||
</DropdownItem>
|
||||
<DropdownItem textValue='Remove header' arial-label={'Remove header'}>
|
||||
<ItemContent
|
||||
icon="circle-minus"
|
||||
label='Remove a header'
|
||||
onClick={() => addSnippet(removeHeader)}
|
||||
/>
|
||||
</DropdownItem>
|
||||
<DropdownItem textValue='Update body as raw' arial-label={'Update body as raw'}>
|
||||
<ItemContent
|
||||
icon="circle-info"
|
||||
label='Update body as raw'
|
||||
onClick={() => addSnippet(updateRequestBody)}
|
||||
/>
|
||||
</DropdownItem>
|
||||
<DropdownItem textValue='Update auth method' arial-label={'Update auth method'}>
|
||||
<ItemContent
|
||||
icon="circle-user"
|
||||
label='Update auth method'
|
||||
onClick={() => addSnippet(updateRequestAuth)}
|
||||
/>
|
||||
</DropdownItem>
|
||||
</Dropdown>
|
||||
|
||||
<Dropdown
|
||||
aria-label='Response Handling'
|
||||
placement='top left'
|
||||
triggerButton={
|
||||
<DropdownButton>
|
||||
<ItemContent
|
||||
icon="code"
|
||||
label='Response Handling'
|
||||
/>
|
||||
</DropdownButton>
|
||||
}
|
||||
>
|
||||
<DropdownItem textValue='Get status code' arial-label={'Get status code'}>
|
||||
<ItemContent
|
||||
icon="circle-info"
|
||||
label='Get status code'
|
||||
onClick={() => addSnippet(getStatusCode)}
|
||||
/>
|
||||
</DropdownItem>
|
||||
<DropdownItem textValue='Get status message' arial-label={'Get status message'}>
|
||||
<ItemContent
|
||||
icon="circle-info"
|
||||
label='Get status message'
|
||||
onClick={() => addSnippet(getStatusMsg)}
|
||||
/>
|
||||
</DropdownItem>
|
||||
<DropdownItem textValue='Get response time' arial-label={'Get response time'}>
|
||||
<ItemContent
|
||||
icon="circle-info"
|
||||
label='Get response time'
|
||||
onClick={() => addSnippet(getRespTime)}
|
||||
/>
|
||||
</DropdownItem>
|
||||
<DropdownItem textValue='Get body as JSON' arial-label={'Get body as JSON'}>
|
||||
<ItemContent
|
||||
icon="circle-info"
|
||||
label='Get body as JSON'
|
||||
onClick={() => addSnippet(getJsonBody)}
|
||||
/>
|
||||
</DropdownItem>
|
||||
<DropdownItem textValue='Get body as text' arial-label={'Get body as text'}>
|
||||
<ItemContent
|
||||
icon="circle-info"
|
||||
label='Get body as text'
|
||||
onClick={() => addSnippet(getTextBody)}
|
||||
/>
|
||||
</DropdownItem>
|
||||
<DropdownItem textValue='Find a header by name' arial-label={'Find a header by name'}>
|
||||
<ItemContent
|
||||
icon="circle-info"
|
||||
label='Find a header by name'
|
||||
onClick={() => addSnippet(findHeader)}
|
||||
/>
|
||||
</DropdownItem>
|
||||
<DropdownItem textValue='Get cookies' arial-label={'Get cookies'}>
|
||||
<ItemContent
|
||||
icon="circle-info"
|
||||
label='Get cookies'
|
||||
onClick={() => addSnippet(getCookies)}
|
||||
/>
|
||||
</DropdownItem>
|
||||
</Dropdown>
|
||||
|
||||
<Dropdown
|
||||
aria-label='Misc'
|
||||
placement='top left'
|
||||
triggerButton={
|
||||
<DropdownButton>
|
||||
<ItemContent
|
||||
icon="code"
|
||||
label='Misc'
|
||||
/>
|
||||
</DropdownButton>
|
||||
}
|
||||
>
|
||||
<DropdownItem textValue='Send a request' arial-label={'Send a request'}>
|
||||
<ItemContent
|
||||
icon="circle-play"
|
||||
label='Send a request'
|
||||
onClick={() => addSnippet(sendReq)}
|
||||
/>
|
||||
</DropdownItem>
|
||||
<DropdownItem textValue='Print log' arial-label={'Print log'}>
|
||||
<ItemContent
|
||||
icon="print"
|
||||
label='Print log'
|
||||
onClick={() => addSnippet(logValue)}
|
||||
/>
|
||||
</DropdownItem>
|
||||
<DropdownItem textValue='Require a module' arial-label={'Require a module'}>
|
||||
<ItemContent
|
||||
icon="circle-plus"
|
||||
label='Require a module'
|
||||
onClick={() => addSnippet(requireAModule)}
|
||||
/>
|
||||
</DropdownItem>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</Toolbar>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import React, { FC, useState } from 'react';
|
||||
import { Tab, TabList, TabPanel, Tabs } from 'react-aria-components';
|
||||
import { useRouteLoaderData } from 'react-router-dom';
|
||||
|
||||
import { Settings } from '../../../models/settings';
|
||||
import { useActiveRequestSyncVCSVersion, useGitVCSVersion } from '../../hooks/use-vcs-version';
|
||||
import { RequestGroupLoaderData } from '../../routes/request-group';
|
||||
import { WorkspaceLoaderData } from '../../routes/workspace';
|
||||
import { PanelContainer, TabItem, Tabs } from '../base/tabs';
|
||||
import { AuthDropdown } from '../dropdowns/auth-dropdown';
|
||||
import { AuthWrapper } from '../editors/auth/auth-wrapper';
|
||||
import { RequestHeadersEditor } from '../editors/request-headers-editor';
|
||||
import { ErrorBoundary } from '../error-boundary';
|
||||
@@ -25,106 +24,105 @@ export const RequestGroupPane: FC<{ settings: Settings }> = ({ }) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tabs aria-label="Request group pane tabs">
|
||||
<TabItem key="auth" title={<AuthDropdown authentication={activeRequestGroup.authentication} />}>
|
||||
<Tabs aria-label='Request group tabs' className="flex-1 w-full h-full flex flex-col">
|
||||
<TabList className='w-full flex-shrink-0 overflow-x-auto border-solid scro border-b border-b-[--hl-md] bg-[--color-bg] flex items-center h-[--line-height-sm]' aria-label='Request pane tabs'>
|
||||
<Tab
|
||||
className='flex-shrink-0 h-full flex items-center justify-between cursor-pointer gap-2 outline-none select-none px-3 py-1 text-[--hl] aria-selected:text-[--color-font] hover:bg-[--hl-sm] hover:text-[--color-font] aria-selected:bg-[--hl-xs] aria-selected:focus:bg-[--hl-sm] aria-selected:hover:bg-[--hl-sm] focus:bg-[--hl-sm] transition-colors duration-300'
|
||||
id='auth'
|
||||
>
|
||||
Auth
|
||||
</Tab>
|
||||
<Tab
|
||||
className='flex-shrink-0 h-full flex items-center justify-between cursor-pointer gap-2 outline-none select-none px-3 py-1 text-[--hl] aria-selected:text-[--color-font] hover:bg-[--hl-sm] hover:text-[--color-font] aria-selected:bg-[--hl-xs] aria-selected:focus:bg-[--hl-sm] aria-selected:hover:bg-[--hl-sm] focus:bg-[--hl-sm] transition-colors duration-300'
|
||||
id='headers'
|
||||
>
|
||||
<span>Headers</span>
|
||||
{headersCount > 0 && (
|
||||
<span className='p-1 min-w-6 h-6 flex items-center justify-center text-xs rounded-lg border border-solid border-[--hl]'>
|
||||
{headersCount}
|
||||
</span>
|
||||
)}
|
||||
</Tab>
|
||||
<Tab
|
||||
className='flex-shrink-0 h-full flex items-center justify-between cursor-pointer gap-2 outline-none select-none px-3 py-1 text-[--hl] aria-selected:text-[--color-font] hover:bg-[--hl-sm] hover:text-[--color-font] aria-selected:bg-[--hl-xs] aria-selected:focus:bg-[--hl-sm] aria-selected:hover:bg-[--hl-sm] focus:bg-[--hl-sm] transition-colors duration-300'
|
||||
id='docs'
|
||||
>
|
||||
Docs
|
||||
</Tab>
|
||||
</TabList>
|
||||
<TabPanel className='w-full flex-1 flex flex-col overflow-hidden' id='auth'>
|
||||
<ErrorBoundary
|
||||
key={uniqueKey}
|
||||
errorClassName="font-error pad text-center"
|
||||
>
|
||||
<div />
|
||||
<AuthWrapper authentication={activeRequestGroup.authentication} />
|
||||
</ErrorBoundary>
|
||||
</TabItem>
|
||||
<TabItem
|
||||
key="headers"
|
||||
title={
|
||||
<div className='flex items-center gap-2'>
|
||||
Headers{' '}
|
||||
{headersCount > 0 && (
|
||||
<span className="p-2 aspect-square flex items-center color-inherit justify-between border-solid border border-[--hl-md] overflow-hidden rounded-lg text-xs shadow-small">{headersCount}</span>
|
||||
)}
|
||||
</TabPanel>
|
||||
<TabPanel className='w-full flex-1 overflow-y-auto ' id='headers'>
|
||||
<ErrorBoundary
|
||||
key={uniqueKey}
|
||||
errorClassName="font-error pad text-center"
|
||||
>
|
||||
<RequestHeadersEditor
|
||||
bulk={false}
|
||||
headers={folderHeaders}
|
||||
requestType="RequestGroup"
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
</TabPanel>
|
||||
<TabPanel className='w-full flex-1 overflow-y-auto ' id='docs'>
|
||||
{activeRequestGroup.description ? (
|
||||
<div>
|
||||
<div className="pull-right pad bg-default">
|
||||
<button
|
||||
className="btn btn--clicky"
|
||||
onClick={() => setIsRequestGroupSettingsModalOpen(true)}
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<div className="pad">
|
||||
<ErrorBoundary errorClassName="font-error pad text-center">
|
||||
<MarkdownPreview
|
||||
heading={activeRequestGroup.name}
|
||||
markdown={activeRequestGroup.description}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="flex flex-col relative h-full overflow-hidden">
|
||||
<ErrorBoundary
|
||||
key={uniqueKey}
|
||||
errorClassName="font-error pad text-center"
|
||||
>
|
||||
<div className='overflow-y-auto flex-1 flex-shrink-0'>
|
||||
<RequestHeadersEditor
|
||||
bulk={false}
|
||||
headers={folderHeaders}
|
||||
requestType="RequestGroup"
|
||||
/>
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
</TabItem>
|
||||
<TabItem
|
||||
key="docs"
|
||||
title={
|
||||
<>
|
||||
Docs
|
||||
{activeRequestGroup.description && (
|
||||
<span className="ml-2 p-2 border-solid border border-[--hl-md] rounded-lg">
|
||||
<span className="flex w-2 h-2 bg-green-500 rounded-full" />
|
||||
) : (
|
||||
<div className="overflow-hidden editor vertically-center text-center">
|
||||
<p className="pad text-sm text-center">
|
||||
<span className="super-faint">
|
||||
<i
|
||||
className="fa fa-file-text-o"
|
||||
style={{
|
||||
fontSize: '8rem',
|
||||
opacity: 0.3,
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
>
|
||||
<PanelContainer className="tall">
|
||||
{activeRequestGroup.description ? (
|
||||
<div>
|
||||
<div className="pull-right pad bg-default">
|
||||
<button
|
||||
className="btn btn--clicky"
|
||||
onClick={() => setIsRequestGroupSettingsModalOpen(true)}
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<div className="pad">
|
||||
<ErrorBoundary errorClassName="font-error pad text-center">
|
||||
<MarkdownPreview
|
||||
heading={activeRequestGroup.name}
|
||||
markdown={activeRequestGroup.description}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="overflow-hidden editor vertically-center text-center">
|
||||
<p className="pad text-sm text-center">
|
||||
<span className="super-faint">
|
||||
<i
|
||||
className="fa fa-file-text-o"
|
||||
style={{
|
||||
fontSize: '8rem',
|
||||
opacity: 0.3,
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
<br />
|
||||
<br />
|
||||
<button
|
||||
className="btn btn--clicky faint"
|
||||
onClick={() => setIsRequestGroupSettingsModalOpen(true)}
|
||||
>
|
||||
Add Description
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</PanelContainer>
|
||||
</TabItem>
|
||||
<br />
|
||||
<br />
|
||||
<button
|
||||
className="btn btn--clicky faint"
|
||||
onClick={() => setIsRequestGroupSettingsModalOpen(true)}
|
||||
>
|
||||
Add Description
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
{isRequestGroupSettingsModalOpen && (
|
||||
<RequestGroupSettingsModal
|
||||
requestGroup={activeRequestGroup}
|
||||
onHide={() => setIsRequestGroupSettingsModalOpen(false)}
|
||||
/>
|
||||
)}</>
|
||||
{
|
||||
isRequestGroupSettingsModalOpen && (
|
||||
<RequestGroupSettingsModal
|
||||
requestGroup={activeRequestGroup}
|
||||
onHide={() => setIsRequestGroupSettingsModalOpen(false)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { FC, Fragment, useState } from 'react';
|
||||
import { Button, Heading, ToggleButton } from 'react-aria-components';
|
||||
import { Button, Heading, Tab, TabList, TabPanel, Tabs, ToggleButton } from 'react-aria-components';
|
||||
import { useParams, useRouteLoaderData } from 'react-router-dom';
|
||||
import { useLocalStorage } from 'react-use';
|
||||
|
||||
@@ -13,10 +13,7 @@ import { useRequestPatcher, useSettingsPatcher } from '../../hooks/use-request';
|
||||
import { useActiveRequestSyncVCSVersion, useGitVCSVersion } from '../../hooks/use-vcs-version';
|
||||
import { RequestLoaderData } from '../../routes/request';
|
||||
import { WorkspaceLoaderData } from '../../routes/workspace';
|
||||
import { PanelContainer, TabItem, Tabs } from '../base/tabs';
|
||||
import { OneLineEditor } from '../codemirror/one-line-editor';
|
||||
import { AuthDropdown } from '../dropdowns/auth-dropdown';
|
||||
import { ContentTypeDropdown } from '../dropdowns/content-type-dropdown';
|
||||
import { AuthWrapper } from '../editors/auth/auth-wrapper';
|
||||
import { BodyEditor } from '../editors/body/body-editor';
|
||||
import { RequestHeadersEditor } from '../editors/request-headers-editor';
|
||||
@@ -112,39 +109,86 @@ export const RequestPane: FC<Props> = ({
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
</PaneHeader>
|
||||
<Tabs aria-label="Request pane tabs">
|
||||
<TabItem
|
||||
key="query"
|
||||
title={
|
||||
<div className='flex items-center gap-2'>
|
||||
Parameters
|
||||
{parametersCount > 0 && (
|
||||
<span className="p-2 aspect-square flex items-center color-inherit justify-between border-solid border border-[--hl-md] overflow-hidden rounded-lg text-xs shadow-small">{parametersCount}</span>
|
||||
)}
|
||||
<Tabs aria-label='Request pane tabs' className="flex-1 w-full h-full flex flex-col">
|
||||
<TabList className='w-full flex-shrink-0 overflow-x-auto border-solid scro border-b border-b-[--hl-md] bg-[--color-bg] flex items-center h-[--line-height-sm]' aria-label='Request pane tabs'>
|
||||
<Tab
|
||||
className='flex-shrink-0 h-full flex items-center justify-between cursor-pointer gap-2 outline-none select-none px-3 py-1 text-[--hl] aria-selected:text-[--color-font] hover:bg-[--hl-sm] hover:text-[--color-font] aria-selected:bg-[--hl-xs] aria-selected:focus:bg-[--hl-sm] aria-selected:hover:bg-[--hl-sm] focus:bg-[--hl-sm] transition-colors duration-300'
|
||||
id='params'
|
||||
>
|
||||
<span>Params</span>
|
||||
{parametersCount > 0 && (
|
||||
<span className='p-1 min-w-6 h-6 flex items-center justify-center text-xs rounded-lg border border-solid border-[--hl]'>
|
||||
{parametersCount}
|
||||
</span>
|
||||
)}
|
||||
</Tab>
|
||||
<Tab
|
||||
className='flex-shrink-0 h-full flex items-center justify-between cursor-pointer gap-2 outline-none select-none px-3 py-1 text-[--hl] aria-selected:text-[--color-font] hover:bg-[--hl-sm] hover:text-[--color-font] aria-selected:bg-[--hl-xs] aria-selected:focus:bg-[--hl-sm] aria-selected:hover:bg-[--hl-sm] focus:bg-[--hl-sm] transition-colors duration-300'
|
||||
id='content-type'
|
||||
>
|
||||
<span>Body</span>
|
||||
</Tab>
|
||||
<Tab
|
||||
className='flex-shrink-0 h-full flex items-center justify-between cursor-pointer gap-2 outline-none select-none px-3 py-1 text-[--hl] aria-selected:text-[--color-font] hover:bg-[--hl-sm] hover:text-[--color-font] aria-selected:bg-[--hl-xs] aria-selected:focus:bg-[--hl-sm] aria-selected:hover:bg-[--hl-sm] focus:bg-[--hl-sm] transition-colors duration-300'
|
||||
id='auth'
|
||||
>
|
||||
<span>Auth</span>
|
||||
</Tab>
|
||||
<Tab
|
||||
className='flex-shrink-0 h-full flex items-center justify-between cursor-pointer gap-2 outline-none select-none px-3 py-1 text-[--hl] aria-selected:text-[--color-font] hover:bg-[--hl-sm] hover:text-[--color-font] aria-selected:bg-[--hl-xs] aria-selected:focus:bg-[--hl-sm] aria-selected:hover:bg-[--hl-sm] focus:bg-[--hl-sm] transition-colors duration-300'
|
||||
id='headers'
|
||||
>
|
||||
<span>Headers</span>
|
||||
{headersCount > 0 && (
|
||||
<span className='p-1 min-w-6 h-6 flex items-center justify-center text-xs rounded-lg border border-solid border-[--hl]'>
|
||||
{headersCount}
|
||||
</span>
|
||||
)}
|
||||
</Tab>
|
||||
<Tab
|
||||
className='flex-shrink-0 h-full flex items-center justify-between cursor-pointer gap-2 outline-none select-none px-3 py-1 text-[--hl] aria-selected:text-[--color-font] hover:bg-[--hl-sm] hover:text-[--color-font] aria-selected:bg-[--hl-xs] aria-selected:focus:bg-[--hl-sm] aria-selected:hover:bg-[--hl-sm] focus:bg-[--hl-sm] transition-colors duration-300'
|
||||
id='scripts'
|
||||
>
|
||||
<span>Scripts</span>
|
||||
{Boolean(activeRequest.preRequestScript || activeRequest.afterResponseScript) && (
|
||||
<span className='p-1 min-w-6 h-6 flex items-center justify-center text-xs rounded-lg border border-solid border-[--hl]'>
|
||||
<span className='w-2 h-2 bg-green-500 rounded-full' />
|
||||
</span>
|
||||
)}
|
||||
</Tab>
|
||||
<Tab
|
||||
className='flex-shrink-0 h-full flex items-center justify-between cursor-pointer gap-2 outline-none select-none px-3 py-1 text-[--hl] aria-selected:text-[--color-font] hover:bg-[--hl-sm] hover:text-[--color-font] aria-selected:bg-[--hl-xs] aria-selected:focus:bg-[--hl-sm] aria-selected:hover:bg-[--hl-sm] focus:bg-[--hl-sm] transition-colors duration-300'
|
||||
id='docs'
|
||||
>
|
||||
<span>Docs</span>
|
||||
{activeRequest.description && (
|
||||
<span className='p-1 min-w-6 h-6 flex items-center justify-center text-xs rounded-lg border border-solid border-[--hl]'>
|
||||
<span className='w-2 h-2 bg-green-500 rounded-full' />
|
||||
</span>
|
||||
)}
|
||||
</Tab>
|
||||
</TabList>
|
||||
<TabPanel className='w-full flex-1 flex flex-col h-full overflow-y-auto' id='params'>
|
||||
<div className="p-4 flex-shrink-0">
|
||||
<div className="text-xs max-h-32 flex flex-col overflow-y-auto min-h-[2em] bg-[--hl-xs] px-2 py-1 border border-solid border-[--hl-sm]">
|
||||
<label className="label--small no-pad-top">Url Preview</label>
|
||||
<ErrorBoundary
|
||||
key={uniqueKey}
|
||||
errorClassName="tall wide vertically-align font-error pad text-center"
|
||||
>
|
||||
<RenderedQueryString request={activeRequest} />
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className='h-full flex flex-col'>
|
||||
<div className="p-4">
|
||||
<div className="text-xs max-h-32 flex flex-col overflow-y-auto min-h-[2em] bg-[--hl-xs] px-2 py-1 border border-solid border-[--hl-sm]">
|
||||
<label className="label--small no-pad-top">Url Preview</label>
|
||||
<ErrorBoundary
|
||||
key={uniqueKey}
|
||||
errorClassName="tall wide vertically-align font-error pad text-center"
|
||||
>
|
||||
<RenderedQueryString request={activeRequest} />
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid flex-1 [grid-template-rows:minmax(auto,min-content)] [grid-template-columns:100%] overflow-hidden">
|
||||
<div className="min-h-[2rem] max-h-full flex flex-col overflow-y-auto [&_.key-value-editor]:p-0 flex-1">
|
||||
<div className='flex items-center w-full p-4 h-4 justify-between'>
|
||||
</div>
|
||||
<div className="flex-shrink-0 grid flex-1 [grid-template-rows:minmax(auto,min-content)] [grid-template-columns:100%] overflow-hidden">
|
||||
<div className="min-h-[2rem] max-h-full flex flex-col overflow-y-auto [&_.key-value-editor]:p-0 flex-1">
|
||||
<div className='flex items-center w-full p-4 h-4 justify-between'>
|
||||
<Heading className='text-xs font-bold uppercase text-[--hl]'>Query parameters</Heading>
|
||||
<div className='flex items-center gap-2'>
|
||||
<Button
|
||||
isDisabled={!urlHasQueryParameters}
|
||||
onPress={handleImportQueryFromUrl}
|
||||
className="w-[14ch] flex flex-shrink-0 gap-2 items-center justify-start px-2 py-1 h-full aria-pressed:bg-[--hl-sm] aria-selected: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"
|
||||
className="w-[14ch] flex flex-shrink-0 gap-2 items-center justify-start px-2 py-1 h-full asma-pressed:bg-[--hl-sm] aria-selected:bg-[--hl-xs] aria-selected:focus:bg-[--hl-sm] aria-selected:hover:bg-[--hl-sm] focus:bg-[--hl-sm] rounded-sm text-[--color-font] hover:bg-[--hl-xs] focus:ring-inset ring-1 ring-transparent focus:ring-[--hl-md] transition-colors text-sm"
|
||||
>
|
||||
Import from URL
|
||||
</Button>
|
||||
@@ -155,7 +199,7 @@ export const RequestPane: FC<Props> = ({
|
||||
});
|
||||
}}
|
||||
isSelected={settings.useBulkParametersEditor}
|
||||
className="w-[14ch] flex flex-shrink-0 gap-2 items-center justify-start px-2 py-1 h-full rounded-sm text-[--color-font] hover:bg-[--hl-xs] focus:ring-inset ring-1 ring-transparent focus:ring-[--hl-md] transition-all text-sm"
|
||||
className="w-[14ch] flex flex-shrink-0 gap-2 items-center justify-start px-2 py-1 h-full rounded-sm text-[--color-font] hover:bg-[--hl-xs] focus:ring-inset ring-1 ring-transparent focus:ring-[--hl-md] transition-colors text-sm"
|
||||
>
|
||||
{({ isSelected }) => (
|
||||
<Fragment>
|
||||
@@ -178,170 +222,151 @@ export const RequestPane: FC<Props> = ({
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
<div className='flex-1 flex flex-col gap-4 p-4 overflow-y-auto'>
|
||||
<div className='flex-1 flex flex-col gap-4 p-4 overflow-y-auto'>
|
||||
<Heading className='text-xs font-bold uppercase text-[--hl]'>Path parameters</Heading>
|
||||
{pathParameters.length > 0 && (
|
||||
<div className="pr-[72.73px] w-full">
|
||||
<div className='grid gap-x-[20.8px] grid-cols-2 flex-shrink-0 w-full rounded-sm overflow-hidden'>
|
||||
{pathParameters.map(pathParameter => (
|
||||
<Fragment key={pathParameter.name}>
|
||||
<span className='p-2 select-none border-b border-solid border-[--hl-md] truncate flex items-center justify-end rounded-sm'>
|
||||
{pathParameter.name}
|
||||
</span>
|
||||
<div className='px-2 flex items-center h-full border-b border-solid border-[--hl-md]'>
|
||||
<OneLineEditor
|
||||
key={activeRequest._id}
|
||||
id={'key-value-editor__name' + pathParameter.name}
|
||||
placeholder="Parameter value"
|
||||
defaultValue={pathParameter.value || ''}
|
||||
onChange={name => {
|
||||
onPathParameterChange(pathParameters.map(p => p.name === pathParameter.name ? { ...p, value: name } : p));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
{pathParameters.length > 0 && (
|
||||
<div className="pr-[72.73px] w-full">
|
||||
<div className='grid gap-x-[20.8px] grid-cols-2 flex-shrink-0 w-full rounded-sm overflow-hidden'>
|
||||
{pathParameters.map(pathParameter => (
|
||||
<Fragment key={pathParameter.name}>
|
||||
<span className='p-2 select-none border-b border-solid border-[--hl-md] truncate flex items-center justify-end rounded-sm'>
|
||||
{pathParameter.name}
|
||||
</span>
|
||||
<div className='px-2 flex items-center h-full border-b border-solid border-[--hl-md]'>
|
||||
<OneLineEditor
|
||||
key={activeRequest._id}
|
||||
id={'key-value-editor__name' + pathParameter.name}
|
||||
placeholder="Parameter value"
|
||||
defaultValue={pathParameter.value || ''}
|
||||
onChange={name => {
|
||||
onPathParameterChange(pathParameters.map(p => p.name === pathParameter.name ? { ...p, value: name } : p));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{pathParameters.length === 0 && !dismissPathParameterTip && (
|
||||
<div className='text-sm text-[--hl] rounded-sm border border-solid border-[--hl-md] p-2 flex items-center gap-2'>
|
||||
<Icon icon='info-circle' />
|
||||
<span>Path parameters are url path segments that start with a colon ':' e.g. ':id' </span>
|
||||
<Button
|
||||
className="flex flex-shrink-0 items-center justify-center aspect-square h-6 aria-pressed:bg-[--hl-sm] rounded-sm text-[--color-font] hover:bg-[--hl-xs] ml-auto"
|
||||
onPress={() => setDismissPathParameterTip('true')}
|
||||
>
|
||||
<Icon icon='close' />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{pathParameters.length === 0 && !dismissPathParameterTip && (
|
||||
<div className='text-sm text-[--hl] rounded-sm border border-solid border-[--hl-md] p-2 flex items-center gap-2'>
|
||||
<Icon icon='info-circle' />
|
||||
<span>Path parameters are url path segments that start with a colon ':' e.g. ':id' </span>
|
||||
<Button
|
||||
className="flex flex-shrink-0 items-center justify-center aspect-square h-6 aria-pressed:bg-[--hl-sm] rounded-sm text-[--color-font] hover:bg-[--hl-xs] ml-auto"
|
||||
onPress={() => setDismissPathParameterTip('true')}
|
||||
>
|
||||
<Icon icon='close' />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</TabItem>
|
||||
<TabItem key="content-type" title={<ContentTypeDropdown />}>
|
||||
</TabPanel>
|
||||
<TabPanel className='w-full flex-1 flex flex-col' id='content-type'>
|
||||
<BodyEditor
|
||||
key={uniqueKey}
|
||||
request={activeRequest}
|
||||
environmentId={environmentId}
|
||||
/>
|
||||
</TabItem>
|
||||
<TabItem key="auth" title={<AuthDropdown authentication={activeRequest.authentication} />}>
|
||||
</TabPanel>
|
||||
<TabPanel className='w-full flex-1 flex flex-col overflow-hidden' id='auth'>
|
||||
<ErrorBoundary
|
||||
key={uniqueKey}
|
||||
errorClassName="font-error pad text-center"
|
||||
>
|
||||
<AuthWrapper authentication={activeRequest.authentication} />
|
||||
</ErrorBoundary>
|
||||
</TabItem>
|
||||
<TabItem
|
||||
key="headers"
|
||||
title={
|
||||
<div className='flex items-center gap-2'>
|
||||
Headers{' '}
|
||||
{headersCount > 0 && (
|
||||
<span className="p-2 aspect-square flex items-center color-inherit justify-between border-solid border border-[--hl-md] overflow-hidden rounded-lg text-xs shadow-small">{headersCount}</span>
|
||||
)}
|
||||
</TabPanel>
|
||||
<TabPanel className='w-full flex-1 flex flex-col relative overflow-hidden' id='headers'>
|
||||
<ErrorBoundary
|
||||
key={uniqueKey}
|
||||
errorClassName="font-error pad text-center"
|
||||
>
|
||||
<div className='overflow-y-auto flex-1 flex-shrink-0'>
|
||||
<RequestHeadersEditor
|
||||
bulk={settings.useBulkHeaderEditor}
|
||||
headers={activeRequest.headers}
|
||||
requestType="Request"
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="flex flex-col relative h-full overflow-hidden">
|
||||
<ErrorBoundary
|
||||
key={uniqueKey}
|
||||
errorClassName="font-error pad text-center"
|
||||
>
|
||||
<div className='overflow-y-auto flex-1 flex-shrink-0'>
|
||||
<RequestHeadersEditor
|
||||
bulk={settings.useBulkHeaderEditor}
|
||||
headers={activeRequest.headers}
|
||||
requestType="Request"
|
||||
/>
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
</ErrorBoundary>
|
||||
|
||||
<div className="flex flex-row border-solid border-t border-[var(--hl-md)] h-[var(--line-height-sm)] text-[var(--font-size-sm)] box-border">
|
||||
<Button
|
||||
className="px-4 py-1 h-full flex items-center justify-center gap-2 aria-pressed:bg-[--hl-sm] text-[--color-font] text-xs hover:bg-[--hl-xs] focus:ring-inset ring-1 ring-transparent focus:ring-[--hl-md] transition-all"
|
||||
onPress={() =>
|
||||
patchSettings({
|
||||
useBulkHeaderEditor: !settings.useBulkHeaderEditor,
|
||||
})
|
||||
}
|
||||
>
|
||||
{settings.useBulkHeaderEditor ? 'Regular Edit' : 'Bulk Edit'}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex flex-row border-solid border-t border-[var(--hl-md)] h-[var(--line-height-sm)] text-[var(--font-size-sm)] box-border">
|
||||
<Button
|
||||
className="px-4 py-1 h-full flex items-center justify-center gap-2 aria-pressed:bg-[--hl-sm] text-[--color-font] text-xs hover:bg-[--hl-xs] focus:ring-inset ring-1 ring-transparent focus:ring-[--hl-md] transition-colors"
|
||||
onPress={() =>
|
||||
patchSettings({
|
||||
useBulkHeaderEditor: !settings.useBulkHeaderEditor,
|
||||
})
|
||||
}
|
||||
>
|
||||
{settings.useBulkHeaderEditor ? 'Regular Edit' : 'Bulk Edit'}
|
||||
</Button>
|
||||
</div>
|
||||
</TabItem>
|
||||
<TabItem
|
||||
key="pre-request-script"
|
||||
data-testid="pre-request-script-tab"
|
||||
title={
|
||||
<div className='flex items-center gap-2'>
|
||||
Pre-request Script{' '}
|
||||
{activeRequest.preRequestScript && (
|
||||
<span className="ml-2 p-2 border-solid border border-[--hl-md] rounded-lg">
|
||||
<span className="flex w-2 h-2 bg-green-500 rounded-full" />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
aria-label={'experimental'}
|
||||
>
|
||||
<ErrorBoundary
|
||||
key={uniqueKey}
|
||||
errorClassName="tall wide vertically-align font-error pad text-center"
|
||||
>
|
||||
<RequestScriptEditor
|
||||
uniquenessKey={uniqueKey}
|
||||
defaultValue={activeRequest.preRequestScript || ''}
|
||||
onChange={preRequestScript => patchRequest(requestId, { preRequestScript })}
|
||||
settings={settings}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
</TabItem>
|
||||
<TabItem
|
||||
key="after-response-script"
|
||||
data-testid="after-response-script-tab"
|
||||
title={
|
||||
<div className='flex items-center gap-2'>
|
||||
After-response Script{' '}
|
||||
{activeRequest.afterResponseScript && (
|
||||
<span className="ml-2 p-2 border-solid border border-[--hl-md] rounded-lg">
|
||||
<span className="flex w-2 h-2 bg-green-500 rounded-full" />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
aria-label={'experimental'}
|
||||
>
|
||||
<ErrorBoundary
|
||||
key={uniqueKey}
|
||||
errorClassName="tall wide vertically-align font-error pad text-center"
|
||||
>
|
||||
<RequestScriptEditor
|
||||
uniquenessKey={uniqueKey}
|
||||
defaultValue={activeRequest.afterResponseScript || ''}
|
||||
onChange={afterResponseScript => patchRequest(requestId, { afterResponseScript })}
|
||||
settings={settings}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
</TabItem>
|
||||
<TabItem
|
||||
key="docs"
|
||||
title={
|
||||
<>
|
||||
Docs
|
||||
{activeRequest.description && (
|
||||
<span className="ml-2 p-2 border-solid border border-[--hl-md] rounded-lg">
|
||||
<span className="flex w-2 h-2 bg-green-500 rounded-full" />
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
>
|
||||
<PanelContainer className="tall">
|
||||
</TabPanel>
|
||||
<TabPanel className='w-full flex-1' id='scripts'>
|
||||
<Tabs className="w-full h-full flex flex-col overflow-hidden">
|
||||
<TabList className="w-full flex-shrink-0 overflow-x-auto border-solid border-b border-b-[--hl-md] px-2 bg-[--color-bg] flex items-center gap-2 h-[--line-height-sm]" aria-label="Request scripts tabs">
|
||||
<Tab
|
||||
className="rounded-md flex-shrink-0 h-[--line-height-xxs] text-sm flex items-center justify-between cursor-pointer w-[10.5rem] outline-none select-none px-2 py-1 hover:bg-[rgba(var(--color-surprise-rgb),50%)] text-[--hl] aria-selected:text-[--color-font-surprise] hover:text-[--color-font-surprise] aria-selected:bg-[rgba(var(--color-surprise-rgb),40%)] transition-colors duration-300"
|
||||
id="pre-request"
|
||||
>
|
||||
<div className='flex flex-1 items-center gap-2'>
|
||||
<Icon icon="arrow-right-to-bracket" />
|
||||
<span>Pre-request</span>
|
||||
</div>
|
||||
{Boolean(activeRequest.preRequestScript) && (
|
||||
<span className="p-2 rounded-lg">
|
||||
<span className="flex w-2 h-2 bg-green-500 rounded-full" />
|
||||
</span>
|
||||
)}
|
||||
</Tab>
|
||||
<Tab
|
||||
className="rounded-md flex-shrink-0 h-[--line-height-xxs] text-sm flex items-center justify-between cursor-pointer w-[10.5rem] outline-none select-none px-2 py-1 hover:bg-[rgba(var(--color-surprise-rgb),50%)] text-[--hl] aria-selected:text-[--color-font-surprise] hover:text-[--color-font-surprise] aria-selected:bg-[rgba(var(--color-surprise-rgb),40%)] transition-colors duration-300"
|
||||
id="after-response"
|
||||
>
|
||||
<div className='flex flex-1 items-center gap-2'>
|
||||
<Icon icon="arrow-right-from-bracket" />
|
||||
<span>After-response</span>
|
||||
</div>
|
||||
{Boolean(activeRequest.afterResponseScript) && (
|
||||
<span className="p-2 rounded-lg">
|
||||
<span className="flex w-2 h-2 bg-green-500 rounded-full" />
|
||||
</span>
|
||||
)}
|
||||
</Tab>
|
||||
</TabList>
|
||||
<TabPanel className="w-full flex-1" id='pre-request'>
|
||||
<ErrorBoundary
|
||||
key={uniqueKey}
|
||||
errorClassName="tall wide vertically-align font-error pad text-center"
|
||||
>
|
||||
<RequestScriptEditor
|
||||
uniquenessKey={uniqueKey}
|
||||
defaultValue={activeRequest.preRequestScript || ''}
|
||||
onChange={preRequestScript => patchRequest(requestId, { preRequestScript })}
|
||||
settings={settings}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
</TabPanel>
|
||||
<TabPanel className="w-full flex-1" id="after-response">
|
||||
<ErrorBoundary
|
||||
key={uniqueKey}
|
||||
errorClassName="tall wide vertically-align font-error pad text-center"
|
||||
>
|
||||
<RequestScriptEditor
|
||||
uniquenessKey={uniqueKey}
|
||||
defaultValue={activeRequest.afterResponseScript || ''}
|
||||
onChange={afterResponseScript => patchRequest(requestId, { afterResponseScript })}
|
||||
settings={settings}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
</TabPanel>
|
||||
<TabPanel className='w-full flex-1' id='docs'>
|
||||
<div className="tall">
|
||||
{activeRequest.description ? (
|
||||
<div>
|
||||
<div className="pull-right pad bg-default">
|
||||
@@ -384,8 +409,8 @@ export const RequestPane: FC<Props> = ({
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</PanelContainer>
|
||||
</TabItem>
|
||||
</div>
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
{isRequestSettingsModalOpen && (
|
||||
<RequestSettingsModal
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { FC, Fragment, useEffect, useRef, useState } from 'react';
|
||||
import { Button, Heading } from 'react-aria-components';
|
||||
import { Button, Heading, Tab, TabList, TabPanel, Tabs, ToggleButton, Toolbar } from 'react-aria-components';
|
||||
import { useParams, useRouteLoaderData } from 'react-router-dom';
|
||||
import { useLocalStorage } from 'react-use';
|
||||
import styled from 'styled-components';
|
||||
@@ -10,16 +10,14 @@ import { Environment } from '../../../models/environment';
|
||||
import { AuthTypes, getCombinedPathParametersFromUrl, RequestPathParameter } from '../../../models/request';
|
||||
import { WebSocketRequest } from '../../../models/websocket-request';
|
||||
import { tryToInterpolateRequestOrShowRenderErrorModal } from '../../../utils/try-interpolate';
|
||||
import { buildQueryStringFromParams, joinUrlAndQueryString } from '../../../utils/url/querystring';
|
||||
import { buildQueryStringFromParams, deconstructQueryStringToParams, extractQueryStringFromUrl, joinUrlAndQueryString } from '../../../utils/url/querystring';
|
||||
import { useReadyState } from '../../hooks/use-ready-state';
|
||||
import { useRequestPatcher } from '../../hooks/use-request';
|
||||
import { useRequestPatcher, useSettingsPatcher } from '../../hooks/use-request';
|
||||
import { useActiveRequestSyncVCSVersion, useGitVCSVersion } from '../../hooks/use-vcs-version';
|
||||
import { WebSocketRequestLoaderData } from '../../routes/request';
|
||||
import { useRootLoaderData } from '../../routes/root';
|
||||
import { TabItem, Tabs } from '../base/tabs';
|
||||
import { CodeEditor, CodeEditorHandle } from '../codemirror/code-editor';
|
||||
import { OneLineEditor } from '../codemirror/one-line-editor';
|
||||
import { AuthDropdown } from '../dropdowns/auth-dropdown';
|
||||
import { WebSocketPreviewMode } from '../dropdowns/websocket-preview-mode';
|
||||
import { AuthWrapper } from '../editors/auth/auth-wrapper';
|
||||
import { RequestHeadersEditor } from '../editors/request-headers-editor';
|
||||
@@ -42,6 +40,7 @@ const SendMessageForm = styled.form({
|
||||
position: 'relative',
|
||||
boxSizing: 'border-box',
|
||||
});
|
||||
|
||||
const SendButton = styled.button<{ isConnected: boolean }>(({ isConnected }) => ({
|
||||
padding: '0 var(--padding-md)',
|
||||
marginLeft: 'var(--padding-xs)',
|
||||
@@ -55,15 +54,6 @@ const SendButton = styled.button<{ isConnected: boolean }>(({ isConnected }) =>
|
||||
},
|
||||
}));
|
||||
|
||||
const PaneSendButton = styled.div({
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'flex-end',
|
||||
boxSizing: 'border-box',
|
||||
height: 'var(--line-height-sm)',
|
||||
borderBottom: '1px solid var(--hl-lg)',
|
||||
padding: 3,
|
||||
});
|
||||
const PaneHeader = styled(OriginalPaneHeader)({
|
||||
'&&': { alignItems: 'stretch' },
|
||||
});
|
||||
@@ -209,10 +199,7 @@ export const WebSocketRequestPane: FC<Props> = ({ environment }) => {
|
||||
|
||||
const { workspaceId, requestId } = useParams() as { organizationId: string; projectId: string; workspaceId: string; requestId: string };
|
||||
const readyState = useReadyState({ requestId: activeRequest._id, protocol: 'webSocket' });
|
||||
const {
|
||||
settings,
|
||||
} = useRootLoaderData();
|
||||
const { useBulkParametersEditor } = settings;
|
||||
const { settings } = useRootLoaderData();
|
||||
|
||||
const disabled = readyState;
|
||||
|
||||
@@ -248,7 +235,7 @@ export const WebSocketRequestPane: FC<Props> = ({ environment }) => {
|
||||
|
||||
const parametersCount = pathParameters.length + activeRequest.parameters.filter(p => !p.disabled).length;
|
||||
const headersCount = activeRequest.headers.filter(h => !h.disabled).length;
|
||||
|
||||
const patchSettings = useSettingsPatcher();
|
||||
const upsertPayloadWithMode = async (mode: string) => {
|
||||
// @TODO: multiple payloads
|
||||
const payload = await models.webSocketPayload.getByParentId(requestId);
|
||||
@@ -264,9 +251,33 @@ export const WebSocketRequestPane: FC<Props> = ({ environment }) => {
|
||||
};
|
||||
const [isRequestSettingsModalOpen, setIsRequestSettingsModalOpen] = useState(false);
|
||||
|
||||
const handleImportQueryFromUrl = () => {
|
||||
let query;
|
||||
|
||||
try {
|
||||
query = extractQueryStringFromUrl(activeRequest.url);
|
||||
} catch (error) {
|
||||
console.warn('Failed to parse url to import querystring');
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove the search string (?foo=bar&...) from the Url
|
||||
const url = activeRequest.url.replace(`?${query}`, '');
|
||||
const parameters = [
|
||||
...activeRequest.parameters,
|
||||
...deconstructQueryStringToParams(query),
|
||||
];
|
||||
|
||||
// Only update if url changed
|
||||
if (url !== activeRequest.url) {
|
||||
patchRequest(requestId, { url, parameters });
|
||||
}
|
||||
};
|
||||
|
||||
const gitVersion = useGitVCSVersion();
|
||||
const activeRequestSyncVersion = useActiveRequestSyncVCSVersion();
|
||||
const patchRequest = useRequestPatcher();
|
||||
const urlHasQueryParameters = activeRequest.url.indexOf('?') >= 0;
|
||||
// Reset the response pane state when we switch requests, the environment gets modified, or the (Git|Sync)VCS version changes
|
||||
const uniqueKey = `${environment?.modified}::${requestId}::${gitVersion}::${activeRequestSyncVersion}::${activeRequestMeta.activeResponseId}`;
|
||||
|
||||
@@ -282,136 +293,176 @@ export const WebSocketRequestPane: FC<Props> = ({ environment }) => {
|
||||
onChange={url => patchRequest(requestId, { url })}
|
||||
/>
|
||||
</PaneHeader>
|
||||
<Tabs aria-label="Websocket request pane tabs">
|
||||
<TabItem
|
||||
key="query"
|
||||
title={
|
||||
<div className='flex items-center gap-2'>
|
||||
Parameters
|
||||
{parametersCount > 0 && (
|
||||
<span className="p-2 aspect-square flex items-center color-inherit justify-between border-solid border border-[--hl-md] overflow-hidden rounded-lg text-xs shadow-small">{parametersCount}</span>
|
||||
<Tabs aria-label='Websocket request pane tabs' className="flex-1 w-full h-full flex flex-col">
|
||||
<TabList className='w-full flex-shrink-0 overflow-x-auto border-solid scro border-b border-b-[--hl-md] bg-[--color-bg] flex items-center h-[--line-height-sm]' aria-label='Request pane tabs'>
|
||||
<Tab
|
||||
className='flex-shrink-0 h-full flex items-center justify-between cursor-pointer gap-2 outline-none select-none px-3 py-1 text-[--hl] aria-selected:text-[--color-font] hover:bg-[--hl-sm] hover:text-[--color-font] aria-selected:bg-[--hl-xs] aria-selected:focus:bg-[--hl-sm] aria-selected:hover:bg-[--hl-sm] focus:bg-[--hl-sm] transition-colors duration-300'
|
||||
id='params'
|
||||
>
|
||||
<span>Params</span>
|
||||
{parametersCount > 0 && (
|
||||
<span className='p-1 min-w-6 h-6 flex items-center justify-center text-xs rounded-lg border border-solid border-[--hl]'>
|
||||
{parametersCount}
|
||||
</span>
|
||||
)}
|
||||
</Tab>
|
||||
<Tab
|
||||
className='flex-shrink-0 h-full flex items-center justify-between cursor-pointer gap-2 outline-none select-none px-3 py-1 text-[--hl] aria-selected:text-[--color-font] hover:bg-[--hl-sm] hover:text-[--color-font] aria-selected:bg-[--hl-xs] aria-selected:focus:bg-[--hl-sm] aria-selected:hover:bg-[--hl-sm] focus:bg-[--hl-sm] transition-colors duration-300'
|
||||
id='content-type'
|
||||
>
|
||||
<span>Body</span>
|
||||
</Tab>
|
||||
<Tab
|
||||
className='flex-shrink-0 h-full flex items-center justify-between cursor-pointer gap-2 outline-none select-none px-3 py-1 text-[--hl] aria-selected:text-[--color-font] hover:bg-[--hl-sm] hover:text-[--color-font] aria-selected:bg-[--hl-xs] aria-selected:focus:bg-[--hl-sm] aria-selected:hover:bg-[--hl-sm] focus:bg-[--hl-sm] transition-colors duration-300'
|
||||
id='auth'
|
||||
>
|
||||
<span>Auth</span>
|
||||
</Tab>
|
||||
<Tab
|
||||
className='flex-shrink-0 h-full flex items-center justify-between cursor-pointer gap-2 outline-none select-none px-3 py-1 text-[--hl] aria-selected:text-[--color-font] hover:bg-[--hl-sm] hover:text-[--color-font] aria-selected:bg-[--hl-xs] aria-selected:focus:bg-[--hl-sm] aria-selected:hover:bg-[--hl-sm] focus:bg-[--hl-sm] transition-colors duration-300'
|
||||
id='headers'
|
||||
>
|
||||
<span>Headers</span>
|
||||
{headersCount > 0 && (
|
||||
<span className='p-1 min-w-6 h-6 flex items-center justify-center text-xs rounded-lg border border-solid border-[--hl]'>
|
||||
{headersCount}
|
||||
</span>
|
||||
)}
|
||||
</Tab>
|
||||
<Tab
|
||||
className='flex-shrink-0 h-full flex items-center justify-between cursor-pointer gap-2 outline-none select-none px-3 py-1 text-[--hl] aria-selected:text-[--color-font] hover:bg-[--hl-sm] hover:text-[--color-font] aria-selected:bg-[--hl-xs] aria-selected:focus:bg-[--hl-sm] aria-selected:hover:bg-[--hl-sm] focus:bg-[--hl-sm] transition-colors duration-300'
|
||||
id='docs'
|
||||
>
|
||||
Docs
|
||||
</Tab>
|
||||
</TabList>
|
||||
<TabPanel className='w-full flex-1 flex flex-col h-full overflow-y-auto' id='params'>
|
||||
{disabled && <PaneReadOnlyBanner />}
|
||||
<div className="p-4 flex-shrink-0">
|
||||
<div className="text-xs max-h-32 flex flex-col overflow-y-auto min-h-[2em] bg-[--hl-xs] px-2 py-1 border border-solid border-[--hl-sm]">
|
||||
<label className="label--small no-pad-top">Url Preview</label>
|
||||
<ErrorBoundary
|
||||
key={uniqueKey}
|
||||
errorClassName="tall wide vertically-align font-error pad text-center"
|
||||
>
|
||||
<RenderedQueryString request={activeRequest} />
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-shrink-0 grid flex-1 [grid-template-rows:minmax(auto,min-content)] [grid-template-columns:100%] overflow-hidden">
|
||||
<div className="min-h-[2rem] max-h-full flex flex-col overflow-y-auto [&_.key-value-editor]:p-0 flex-1">
|
||||
<div className='flex items-center w-full p-4 h-4 justify-between'>
|
||||
<Heading className='text-xs font-bold uppercase text-[--hl]'>Query parameters</Heading>
|
||||
<div className='flex items-center gap-2'>
|
||||
<Button
|
||||
isDisabled={disabled || !urlHasQueryParameters}
|
||||
onPress={handleImportQueryFromUrl}
|
||||
className="w-[14ch] flex flex-shrink-0 gap-2 items-center justify-start px-2 py-1 h-full asma-pressed:bg-[--hl-sm] aria-selected:bg-[--hl-xs] aria-selected:focus:bg-[--hl-sm] aria-selected:hover:bg-[--hl-sm] focus:bg-[--hl-sm] rounded-sm text-[--color-font] hover:bg-[--hl-xs] focus:ring-inset ring-1 ring-transparent focus:ring-[--hl-md] transition-colors text-sm"
|
||||
>
|
||||
Import from URL
|
||||
</Button>
|
||||
<ToggleButton
|
||||
isDisabled={disabled}
|
||||
onChange={isSelected => {
|
||||
patchSettings({
|
||||
useBulkParametersEditor: isSelected,
|
||||
});
|
||||
}}
|
||||
isSelected={settings.useBulkParametersEditor}
|
||||
className="w-[14ch] flex flex-shrink-0 gap-2 items-center justify-start px-2 py-1 h-full rounded-sm text-[--color-font] hover:bg-[--hl-xs] focus:ring-inset ring-1 ring-transparent focus:ring-[--hl-md] transition-colors text-sm"
|
||||
>
|
||||
{({ isSelected }) => (
|
||||
<Fragment>
|
||||
<Icon icon={isSelected ? 'toggle-on' : 'toggle-off'} className={`${isSelected ? 'text-[--color-success]' : ''}`} />
|
||||
<span>{
|
||||
isSelected ? 'Regular Edit' : 'Bulk Edit'
|
||||
}</span>
|
||||
</Fragment>
|
||||
)}
|
||||
</ToggleButton>
|
||||
</div>
|
||||
</div>
|
||||
<ErrorBoundary
|
||||
key={uniqueKey}
|
||||
errorClassName="tall wide vertically-align font-error pad text-center"
|
||||
>
|
||||
<RequestParametersEditor
|
||||
bulk={settings.useBulkParametersEditor}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
<div className='flex-1 flex flex-col gap-4 p-4 overflow-y-auto'>
|
||||
<Heading className='text-xs font-bold uppercase text-[--hl]'>Path parameters</Heading>
|
||||
{pathParameters.length > 0 && (
|
||||
<div className="pr-[72.73px] w-full">
|
||||
<div className='grid gap-x-[20.8px] grid-cols-2 flex-shrink-0 w-full rounded-sm overflow-hidden'>
|
||||
{pathParameters.map(pathParameter => (
|
||||
<Fragment key={pathParameter.name}>
|
||||
<span className='p-2 select-none border-b border-solid border-[--hl-md] truncate flex items-center justify-end rounded-sm'>
|
||||
{pathParameter.name}
|
||||
</span>
|
||||
<div className='px-2 flex items-center h-full border-b border-solid border-[--hl-md]'>
|
||||
<OneLineEditor
|
||||
readOnly={disabled}
|
||||
key={activeRequest._id}
|
||||
id={'key-value-editor__name' + pathParameter.name}
|
||||
placeholder="Parameter value"
|
||||
defaultValue={pathParameter.value || ''}
|
||||
onChange={name => {
|
||||
onPathParameterChange(pathParameters.map(p => p.name === pathParameter.name ? { ...p, value: name } : p));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{pathParameters.length === 0 && !dismissPathParameterTip && (
|
||||
<div className='text-sm text-[--hl] rounded-sm border border-solid border-[--hl-md] p-2 flex items-center gap-2'>
|
||||
<Icon icon='info-circle' />
|
||||
<span>Path parameters are url path segments that start with a colon ':' e.g. ':id' </span>
|
||||
<Button
|
||||
className="flex flex-shrink-0 items-center justify-center aspect-square h-6 aria-pressed:bg-[--hl-sm] rounded-sm text-[--color-font] hover:bg-[--hl-xs] ml-auto"
|
||||
onPress={() => setDismissPathParameterTip('true')}
|
||||
>
|
||||
<Icon icon='close' />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="grid h-full auto-rows-auto [grid-template-columns:100%] divide-y divide-solid divide-[--hl-md]">
|
||||
{disabled && <PaneReadOnlyBanner />}
|
||||
<div className='h-full flex flex-col'>
|
||||
<div className="p-4">
|
||||
<div className="text-xs max-h-32 flex flex-col overflow-y-auto min-h-[2em] bg-[--hl-xs] px-2 py-1 border border-solid border-[--hl-sm]">
|
||||
<label className="label--small no-pad-top">Url Preview</label>
|
||||
<ErrorBoundary
|
||||
key={uniqueKey}
|
||||
errorClassName="tall wide vertically-align font-error pad text-center"
|
||||
>
|
||||
<RenderedQueryString request={activeRequest} />
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid flex-1 [grid-template-rows:minmax(auto,min-content)] [grid-template-columns:100%] overflow-hidden">
|
||||
<div className="min-h-[2rem] max-h-full flex flex-col overflow-y-auto [&_.key-value-editor]:p-0 flex-1">
|
||||
<div className='flex items-center w-full p-4 h-4 justify-between'>
|
||||
<Heading className='text-xs font-bold uppercase text-[--hl]'>Query parameters</Heading>
|
||||
</div>
|
||||
<ErrorBoundary
|
||||
key={uniqueKey}
|
||||
errorClassName="tall wide vertically-align font-error pad text-center"
|
||||
>
|
||||
<RequestParametersEditor
|
||||
bulk={useBulkParametersEditor}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
<div className='flex-1 flex flex-col gap-4 p-4 overflow-y-auto'>
|
||||
<Heading className='text-xs font-bold uppercase text-[--hl]'>Path parameters</Heading>
|
||||
{pathParameters.length > 0 && (
|
||||
<div className="pr-[72.73px] w-full">
|
||||
<div className='grid gap-x-[20.8px] grid-cols-2 flex-shrink-0 w-full rounded-sm overflow-hidden'>
|
||||
{pathParameters.map(pathParameter => (
|
||||
<Fragment key={pathParameter.name}>
|
||||
<span className='p-2 select-none border-b border-solid border-[--hl-md] truncate flex items-center justify-end rounded-sm'>
|
||||
{pathParameter.name}
|
||||
</span>
|
||||
<div className='px-2 flex items-center h-full border-b border-solid border-[--hl-md]'>
|
||||
<OneLineEditor
|
||||
readOnly={disabled}
|
||||
id={'key-value-editor__name' + pathParameter.name}
|
||||
key={activeRequest._id}
|
||||
placeholder={'Parameter value'}
|
||||
defaultValue={pathParameter.value || ''}
|
||||
onChange={name => {
|
||||
onPathParameterChange(pathParameters.map(p => p.name === pathParameter.name ? { ...p, value: name } : p));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{pathParameters.length === 0 && !dismissPathParameterTip && (
|
||||
<div className='text-sm text-[--hl] rounded-sm border border-solid border-[--hl-md] p-2 flex items-center gap-2'>
|
||||
<Icon icon='info-circle' />
|
||||
<span>Path parameters are url path segments that start with a colon ':' e.g. ':id' </span>
|
||||
<Button
|
||||
className="flex flex-shrink-0 items-center justify-center aspect-square h-6 aria-pressed:bg-[--hl-sm] rounded-sm text-[--color-font] hover:bg-[--hl-xs] ml-auto"
|
||||
onPress={() => setDismissPathParameterTip('true')}
|
||||
>
|
||||
<Icon icon='close' />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TabItem>
|
||||
<TabItem key="websocket-preview-mode" title={<WebSocketPreviewMode previewMode={previewMode} onClick={changeMode} />}>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
height: '100%',
|
||||
}}
|
||||
>
|
||||
<PaneSendButton>
|
||||
<SendButton
|
||||
type="submit"
|
||||
form="websocketMessageForm"
|
||||
isConnected={readyState}
|
||||
>
|
||||
Send
|
||||
</SendButton>
|
||||
</PaneSendButton>
|
||||
<WebSocketRequestForm
|
||||
key={uniqueKey}
|
||||
request={activeRequest}
|
||||
previewMode={previewMode}
|
||||
environmentId={environment?._id || ''}
|
||||
workspaceId={workspaceId}
|
||||
/>
|
||||
</div>
|
||||
</TabItem>
|
||||
<TabItem key="auth" title={<AuthDropdown authentication={activeRequest.authentication} authTypes={supportedAuthTypes} disabled={disabled} />}>
|
||||
</TabPanel>
|
||||
<TabPanel className='w-full flex-1 flex flex-col' id='content-type'>
|
||||
<Toolbar className="w-full flex-shrink-0 px-2 border-b border-solid border-[--hl-md] py-2 h-[--line-height-sm] flex items-center gap-2 justify-between">
|
||||
<WebSocketPreviewMode previewMode={previewMode} onSelect={changeMode} />
|
||||
<SendButton
|
||||
type="submit"
|
||||
form="websocketMessageForm"
|
||||
isConnected={readyState}
|
||||
>
|
||||
Send
|
||||
</SendButton>
|
||||
</Toolbar>
|
||||
<WebSocketRequestForm
|
||||
key={uniqueKey}
|
||||
request={activeRequest}
|
||||
previewMode={previewMode}
|
||||
environmentId={environment?._id || ''}
|
||||
workspaceId={workspaceId}
|
||||
/>
|
||||
</TabPanel>
|
||||
<TabPanel className='w-full flex-1 flex flex-col overflow-hidden' id='auth'>
|
||||
{disabled && <PaneReadOnlyBanner />}
|
||||
<AuthWrapper
|
||||
key={uniqueKey}
|
||||
authentication={activeRequest.authentication}
|
||||
disabled={disabled}
|
||||
authTypes={supportedAuthTypes}
|
||||
/>
|
||||
</TabItem>
|
||||
<TabItem
|
||||
key="headers"
|
||||
title={
|
||||
<div className='flex items-center gap-2'>
|
||||
Headers{' '}
|
||||
{headersCount > 0 && (
|
||||
<span className="p-2 aspect-square flex items-center color-inherit justify-between border-solid border border-[--hl-md] overflow-hidden rounded-lg text-xs shadow-small">{headersCount}</span>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
</TabPanel>
|
||||
<TabPanel className='w-full flex-1 overflow-y-auto ' id='headers'>
|
||||
{disabled && <PaneReadOnlyBanner />}
|
||||
<RequestHeadersEditor
|
||||
key={uniqueKey}
|
||||
@@ -420,20 +471,8 @@ export const WebSocketRequestPane: FC<Props> = ({ environment }) => {
|
||||
isDisabled={readyState}
|
||||
requestType="WebSocketRequest"
|
||||
/>
|
||||
</TabItem>
|
||||
<TabItem
|
||||
key="docs"
|
||||
title={
|
||||
<>
|
||||
Docs
|
||||
{activeRequest.description && (
|
||||
<span className="bubble space-left">
|
||||
<i className="fa fa--skinny fa-check txt-xxs" />
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
>
|
||||
</TabPanel>
|
||||
<TabPanel className='w-full flex-1 overflow-y-auto ' id='docs'>
|
||||
{activeRequest.description ? (
|
||||
<div>
|
||||
<div className="pull-right pad bg-default">
|
||||
@@ -470,7 +509,7 @@ export const WebSocketRequestPane: FC<Props> = ({ environment }) => {
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</TabItem>
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
{isRequestSettingsModalOpen && (
|
||||
<RequestSettingsModal
|
||||
|
||||
@@ -12,6 +12,10 @@
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
* {
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
html {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user