Add toggle for pretty view

This commit is contained in:
Gregory Schier
2023-02-27 09:08:48 -08:00
parent fe18cd1806
commit 9dc8234f4b
8 changed files with 93 additions and 16 deletions

43
package-lock.json generated
View File

@@ -12,6 +12,7 @@
"@codemirror/lang-html": "^6.4.2", "@codemirror/lang-html": "^6.4.2",
"@codemirror/lang-javascript": "^6.1.4", "@codemirror/lang-javascript": "^6.1.4",
"@codemirror/lang-json": "^6.0.1", "@codemirror/lang-json": "^6.0.1",
"@codemirror/lang-xml": "^6.0.2",
"@codemirror/language": "^6.6.0", "@codemirror/language": "^6.6.0",
"@codemirror/search": "^6.2.3", "@codemirror/search": "^6.2.3",
"@lezer/generator": "^1.2.2", "@lezer/generator": "^1.2.2",
@@ -497,6 +498,18 @@
"@lezer/json": "^1.0.0" "@lezer/json": "^1.0.0"
} }
}, },
"node_modules/@codemirror/lang-xml": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/@codemirror/lang-xml/-/lang-xml-6.0.2.tgz",
"integrity": "sha512-JQYZjHL2LAfpiZI2/qZ/qzDuSqmGKMwyApYmEUUCTxLM4MWS7sATUEfIguZQr9Zjx/7gcdnewb039smF6nC2zw==",
"dependencies": {
"@codemirror/autocomplete": "^6.0.0",
"@codemirror/language": "^6.4.0",
"@codemirror/state": "^6.0.0",
"@lezer/common": "^1.0.0",
"@lezer/xml": "^1.0.0"
}
},
"node_modules/@codemirror/language": { "node_modules/@codemirror/language": {
"version": "6.6.0", "version": "6.6.0",
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.6.0.tgz", "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.6.0.tgz",
@@ -1126,6 +1139,15 @@
"@lezer/common": "^1.0.0" "@lezer/common": "^1.0.0"
} }
}, },
"node_modules/@lezer/xml": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@lezer/xml/-/xml-1.0.1.tgz",
"integrity": "sha512-jMDXrV953sDAUEMI25VNrI9dz94Ai96FfeglytFINhhwQ867HKlCE2jt3AwZTCT7M528WxdDWv/Ty8e9wizwmQ==",
"dependencies": {
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.0.0"
}
},
"node_modules/@motionone/animation": { "node_modules/@motionone/animation": {
"version": "10.15.1", "version": "10.15.1",
"resolved": "https://registry.npmjs.org/@motionone/animation/-/animation-10.15.1.tgz", "resolved": "https://registry.npmjs.org/@motionone/animation/-/animation-10.15.1.tgz",
@@ -7055,6 +7077,18 @@
"@lezer/json": "^1.0.0" "@lezer/json": "^1.0.0"
} }
}, },
"@codemirror/lang-xml": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/@codemirror/lang-xml/-/lang-xml-6.0.2.tgz",
"integrity": "sha512-JQYZjHL2LAfpiZI2/qZ/qzDuSqmGKMwyApYmEUUCTxLM4MWS7sATUEfIguZQr9Zjx/7gcdnewb039smF6nC2zw==",
"requires": {
"@codemirror/autocomplete": "^6.0.0",
"@codemirror/language": "^6.4.0",
"@codemirror/state": "^6.0.0",
"@lezer/common": "^1.0.0",
"@lezer/xml": "^1.0.0"
}
},
"@codemirror/language": { "@codemirror/language": {
"version": "6.6.0", "version": "6.6.0",
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.6.0.tgz", "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.6.0.tgz",
@@ -7450,6 +7484,15 @@
"@lezer/common": "^1.0.0" "@lezer/common": "^1.0.0"
} }
}, },
"@lezer/xml": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@lezer/xml/-/xml-1.0.1.tgz",
"integrity": "sha512-jMDXrV953sDAUEMI25VNrI9dz94Ai96FfeglytFINhhwQ867HKlCE2jt3AwZTCT7M528WxdDWv/Ty8e9wizwmQ==",
"requires": {
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.0.0"
}
},
"@motionone/animation": { "@motionone/animation": {
"version": "10.15.1", "version": "10.15.1",
"resolved": "https://registry.npmjs.org/@motionone/animation/-/animation-10.15.1.tgz", "resolved": "https://registry.npmjs.org/@motionone/animation/-/animation-10.15.1.tgz",

View File

@@ -16,6 +16,7 @@
"@codemirror/lang-html": "^6.4.2", "@codemirror/lang-html": "^6.4.2",
"@codemirror/lang-javascript": "^6.1.4", "@codemirror/lang-javascript": "^6.1.4",
"@codemirror/lang-json": "^6.0.1", "@codemirror/lang-json": "^6.0.1",
"@codemirror/lang-xml": "^6.0.2",
"@codemirror/language": "^6.6.0", "@codemirror/language": "^6.6.0",
"@codemirror/search": "^6.2.3", "@codemirror/search": "^6.2.3",
"@lezer/generator": "^1.2.2", "@lezer/generator": "^1.2.2",

View File

@@ -0,0 +1,9 @@
import type { LinkProps } from 'react-router-dom';
import { Link } from 'react-router-dom';
import { Button, ButtonProps } from './Button';
type Props = ButtonProps<typeof Link> & LinkProps;
export function ButtonLink({ ...props }: Props) {
return <Button as={Link} {...props} />;
}

View File

@@ -25,6 +25,7 @@ import {
rectangularSelection, rectangularSelection,
} from '@codemirror/view'; } from '@codemirror/view';
import { html } from '@codemirror/lang-html'; import { html } from '@codemirror/lang-html';
import { xml } from '@codemirror/lang-xml';
import { parseMixed } from '@lezer/common'; import { parseMixed } from '@lezer/common';
import { EditorState } from '@codemirror/state'; import { EditorState } from '@codemirror/state';
import { json } from '@codemirror/lang-json'; import { json } from '@codemirror/lang-json';
@@ -44,15 +45,15 @@ export const myHighlightStyle = HighlightStyle.define([
{ {
tag: [t.documentMeta, t.blockComment, t.lineComment, t.docComment, t.comment], tag: [t.documentMeta, t.blockComment, t.lineComment, t.docComment, t.comment],
color: '#757b93', color: '#757b93',
fontStyle: 'italic',
}, },
{ tag: [t.name], color: '#4699de' }, { tag: [t.name, t.tagName, t.angleBracket, t.docString], color: '#4699de' },
{ tag: [t.variableName], color: '#31c434' }, { tag: [t.variableName], color: '#31c434' },
{ tag: [t.bool], color: '#e864f6' }, { tag: [t.bool], color: '#e864f6' },
{ tag: [t.attributeName], color: '#8f68ff' }, { tag: [t.attributeName], color: '#a773ff' },
{ tag: [t.attributeValue], color: '#ff964b' }, { tag: [t.attributeValue], color: '#ff964b' },
{ tag: [t.string], color: '#e8b045' }, { tag: [t.string], color: '#e8b045' },
{ tag: [t.keyword, t.meta], color: '#45e8a4' }, { tag: [t.keyword, t.meta], color: '#45e8a4' },
{ tag: [t.comment], color: '#cec4cc', fontStyle: 'italic' },
]); ]);
// export const defaultHighlightStyle = HighlightStyle.define([ // export const defaultHighlightStyle = HighlightStyle.define([
@@ -81,6 +82,8 @@ const syntaxExtensions: Record<string, { base: LanguageSupport; ext: any[] }> =
'application/json': { base: json(), ext: [] }, 'application/json': { base: json(), ext: [] },
'application/javascript': { base: javascript(), ext: [] }, 'application/javascript': { base: javascript(), ext: [] },
'text/html': { base: html(), ext: [] }, 'text/html': { base: html(), ext: [] },
'application/xml': { base: xml(), ext: [] },
'text/xml': { base: xml(), ext: [] },
}; };
export function syntaxExtension({ export function syntaxExtension({
@@ -90,7 +93,8 @@ export function syntaxExtension({
contentType: string; contentType: string;
useTemplating?: boolean; useTemplating?: boolean;
}) { }) {
const { base, ext } = syntaxExtensions[contentType] ?? { base: json(), ext: [] }; const justContentType = contentType.split(';')[0] ?? contentType;
const { base, ext } = syntaxExtensions[justContentType] ?? { base: json(), ext: [] };
if (!useTemplating) { if (!useTemplating) {
return [base]; return [base];
} }

View File

@@ -2,10 +2,14 @@ import {
ArchiveIcon, ArchiveIcon,
CameraIcon, CameraIcon,
CheckIcon, CheckIcon,
CodeIcon,
EyeOpenIcon,
GearIcon, GearIcon,
HomeIcon, HomeIcon,
MoonIcon, MoonIcon,
PaperPlaneIcon, PaperPlaneIcon,
PlusCircledIcon,
PlusIcon,
QuestionMarkIcon, QuestionMarkIcon,
SunIcon, SunIcon,
TriangleDownIcon, TriangleDownIcon,
@@ -19,17 +23,23 @@ type IconName =
| 'home' | 'home'
| 'camera' | 'camera'
| 'gear' | 'gear'
| 'eye'
| 'triangle-down' | 'triangle-down'
| 'paper-plane' | 'paper-plane'
| 'update' | 'update'
| 'question' | 'question'
| 'check' | 'check'
| 'plus'
| 'plus-circled'
| 'sun' | 'sun'
| 'code'
| 'moon'; | 'moon';
const icons: Record<IconName, NamedExoticComponent<{ className: string }>> = { const icons: Record<IconName, NamedExoticComponent<{ className: string }>> = {
'paper-plane': PaperPlaneIcon, 'paper-plane': PaperPlaneIcon,
'triangle-down': TriangleDownIcon, 'triangle-down': TriangleDownIcon,
plus: PlusIcon,
'plus-circled': PlusCircledIcon,
archive: ArchiveIcon, archive: ArchiveIcon,
camera: CameraIcon, camera: CameraIcon,
check: CheckIcon, check: CheckIcon,
@@ -39,6 +49,8 @@ const icons: Record<IconName, NamedExoticComponent<{ className: string }>> = {
sun: SunIcon, sun: SunIcon,
moon: MoonIcon, moon: MoonIcon,
question: QuestionMarkIcon, question: QuestionMarkIcon,
eye: EyeOpenIcon,
code: CodeIcon,
}; };
export interface IconProps { export interface IconProps {

View File

@@ -15,6 +15,7 @@ interface Props {
export function ResponsePane({ requestId, error }: Props) { export function ResponsePane({ requestId, error }: Props) {
const [activeResponseId, setActiveResponseId] = useState<string | null>(null); const [activeResponseId, setActiveResponseId] = useState<string | null>(null);
const [viewMode, setViewMode] = useState<'pretty' | 'raw'>('pretty');
const responses = useResponses(requestId); const responses = useResponses(requestId);
const response = activeResponseId const response = activeResponseId
? responses.data.find((r) => r.id === activeResponseId) ? responses.data.find((r) => r.id === activeResponseId)
@@ -73,15 +74,23 @@ export function ResponsePane({ requestId, error }: Props) {
<> <>
<HStack <HStack
items="center" items="center"
className="italic text-gray-500 text-sm w-full pointer-events-none h-10 mb-3 flex-shrink-0" className="italic text-gray-500 text-sm w-full h-10 mb-3 flex-shrink-0"
> >
{response.status} <div>
{response.statusReason && ` ${response.statusReason}`} {response.status}
&nbsp;&bull;&nbsp; {response.statusReason && ` ${response.statusReason}`}
{response.elapsed}ms &nbsp;&bull;&nbsp; &nbsp;&bull;&nbsp;
{Math.round(response.body.length / 1000)} KB {response.elapsed}ms &nbsp;&bull;&nbsp;
{Math.round(response.body.length / 1000)} KB
</div>
<IconButton
icon={viewMode === 'pretty' ? 'eye' : 'code'}
size="sm"
className="ml-auto"
onClick={() => setViewMode((m) => (m === 'pretty' ? 'raw' : 'pretty'))}
/>
</HStack> </HStack>
{contentType.includes('html') ? ( {viewMode === 'pretty' && contentType.includes('html') ? (
<iframe <iframe
title="Response preview" title="Response preview"
srcDoc={contentForIframe} srcDoc={contentForIframe}

View File

@@ -27,7 +27,7 @@ export function Sidebar({ className, activeRequestId, workspaceId, requests, ...
<IconButton size="sm" icon="sun" onClick={toggleTheme} /> <IconButton size="sm" icon="sun" onClick={toggleTheme} />
<IconButton <IconButton
size="sm" size="sm"
icon="camera" icon="plus-circled"
onClick={() => createRequest.mutate({ name: 'Test Request' })} onClick={() => createRequest.mutate({ name: 'Test Request' })}
/> />
</HStack> </HStack>

View File

@@ -1,15 +1,14 @@
import { Link } from 'react-router-dom';
import { useWorkspaces } from '../hooks/useWorkspaces'; import { useWorkspaces } from '../hooks/useWorkspaces';
import { Button } from '../components/Button'; import { ButtonLink } from '../components/ButtonLink';
export function Workspaces() { export function Workspaces() {
const workspaces = useWorkspaces(); const workspaces = useWorkspaces();
return ( return (
<ul className="p-12"> <ul className="p-12">
{workspaces.data?.map((w) => ( {workspaces.data?.map((w) => (
<Button as={Link} key={w.id} to={`/workspaces/${w.id}`}> <ButtonLink key={w.id} to={`/workspaces/${w.id}`}>
{w.name} {w.name}
</Button> </ButtonLink>
))} ))}
</ul> </ul>
); );