From 9a5aa4c05c04105665a6484097f108c19a11a431 Mon Sep 17 00:00:00 2001 From: Charlie Brown Date: Tue, 24 Oct 2023 06:40:38 -0500 Subject: [PATCH 1/3] Implement tabs for payload and response --- packages/webui/src/components/JsonDisplay.tsx | 8 +- packages/webui/src/components/Section.tsx | 8 +- packages/webui/src/components/ui/Tabs.tsx | 39 +++ .../src/components/ui/TraceDetail.test.tsx | 61 +---- .../webui/src/components/ui/TraceDetail.tsx | 222 +++++++++--------- .../webui/src/components/ui/TraceList.tsx | 2 +- .../webui/src/context/ApplicationContext.tsx | 5 + packages/webui/src/hooks/useApplication.ts | 2 + .../webui/src/testing/mockUseApplication.ts | 2 + 9 files changed, 184 insertions(+), 165 deletions(-) create mode 100644 packages/webui/src/components/ui/Tabs.tsx diff --git a/packages/webui/src/components/JsonDisplay.tsx b/packages/webui/src/components/JsonDisplay.tsx index 044b1d1..569685c 100644 --- a/packages/webui/src/components/JsonDisplay.tsx +++ b/packages/webui/src/components/JsonDisplay.tsx @@ -12,10 +12,10 @@ type JsonDisplayProps = Omit, 'children'> & { children: object | string; }; -const bg = colors.slate['100']; +const bg = colors.gray['200']; const fg = colors.black; -const lines = colors.slate['200']; -const meta = colors.slate['400']; +const lines = colors.gray['200']; +const meta = colors.gray['400']; const accent = colors.orange['300']; const customTheme = { @@ -43,7 +43,7 @@ export default function JsonDisplay({ className, children }: JsonDisplayProps) { return ( }> -
+
& { export default function Section({ title, collapsible = true, className, children, ...props }: SectionProps) { const [expanded, setExpanded] = useState(true); - const Icon = expanded ? HiMinus : HiPlus; + const Icon = expanded ? HiOutlineChevronUp : HiOutlineChevronDown; return ( <> {title && (
+
    {children}
+
+ ); +} + +export function TabListItem({ id, title }: { id: string; title: string }) { + const { selectedTab, setSelectedTab } = useApplication(); + + const className = tw( + 'inline-block px-4 py-3 uppercase font-semibold cursor-pointer', + 'border border-b-0', + selectedTab === id ? 'border-green-400 bg-green-100' : 'border-primary bg-primary', + ); + + return ( +
  • + { + setSelectedTab(id); + }} + > + {title} + +
  • + ); +} + +export function TabContent({ id, children }: { id: string; children: React.ReactNode }) { + const { selectedTab } = useApplication(); + return
    {children}
    ; +} diff --git a/packages/webui/src/components/ui/TraceDetail.test.tsx b/packages/webui/src/components/ui/TraceDetail.test.tsx index d7f1079..022e079 100644 --- a/packages/webui/src/components/ui/TraceDetail.test.tsx +++ b/packages/webui/src/components/ui/TraceDetail.test.tsx @@ -40,6 +40,9 @@ jest.mock('@/components', () => ({ XmlDisplay: function ({ children }: any) { return <>Mock XmlDisplay component: {children}; }, + IconButton: function ({ children, Icon, ...props }: any) { + return
    Mock IconButton component: {children}
    ; + }, })); jest.mock( @@ -266,8 +269,7 @@ describe('TraceDetail', () => { const { getByTestId } = render(); - const summary = getByTestId('summary'); - const status = within(summary).getByTestId('status'); + const status = getByTestId('response-status'); const statusCodeCircle = status.firstChild; expect(statusCodeCircle).toHaveClass(`bg-${color}`); @@ -374,8 +376,8 @@ describe('TraceDetail', () => { const { getByTestId } = render(); - const requestDetails = getByTestId('request-details'); - const body = within(requestDetails).getByTestId('body'); + const requestDetails = getByTestId('trace-detail'); + const body = within(requestDetails).getByTestId('request-body'); expect(body).toBeVisible(); expect(body).toHaveTextContent(content); @@ -387,9 +389,8 @@ describe('TraceDetail', () => { const { getByTestId } = render(); - const requestDetails = getByTestId('request-details'); + const requestDetails = getByTestId('trace-detail'); const body = within(requestDetails).queryByTestId('body'); - expect(body).not.toBeInTheDocument(); }); @@ -642,46 +643,6 @@ describe('TraceDetail', () => { expect(responseBody).toBeVisible(); }); - it('should display response content type', () => { - getSelectedTraceFn.mockReturnValue({ - ...mockTrace, - http: { - ...mockTrace.http, - responseHeaders: { - 'content-type': 'text/text', - }, - }, - }); - - const { getByTestId } = render(); - - const responseBody = getByTestId('response-body'); - const contentType = within(responseBody).getByTestId('content-type'); - - expect(contentType).toBeVisible(); - expect(contentType).toHaveTextContent(`text/text`); - }); - - it('should display response content length', () => { - getSelectedTraceFn.mockReturnValue({ - ...mockTrace, - http: { - ...mockTrace.http, - responseHeaders: { - 'content-length': '1234', - }, - }, - }); - - const { getByTestId } = render(); - - const responseBody = getByTestId('response-body'); - const contentLength = within(responseBody).getByTestId('content-length'); - - expect(contentLength).toBeVisible(); - expect(contentLength).toHaveTextContent(`1234`); - }); - it.each([ { contentType: 'application/json', @@ -719,8 +680,8 @@ describe('TraceDetail', () => { const { getByTestId } = render(); - const responseBody = getByTestId('response-body'); - const body = within(responseBody).getByTestId('body'); + const responseBody = getByTestId('trace-detail'); + const body = within(responseBody).queryByTestId('response-body'); expect(body).toBeVisible(); expect(body).toHaveTextContent(content); @@ -732,8 +693,8 @@ describe('TraceDetail', () => { const { getByTestId } = render(); - const responseBody = getByTestId('response-body'); - const body = within(responseBody).queryByTestId('body'); + const responseBody = getByTestId('trace-detail'); + const body = within(responseBody).queryByTestId('response-body'); expect(body).not.toBeInTheDocument(); }); diff --git a/packages/webui/src/components/ui/TraceDetail.tsx b/packages/webui/src/components/ui/TraceDetail.tsx index d8733fc..71e0f54 100644 --- a/packages/webui/src/components/ui/TraceDetail.tsx +++ b/packages/webui/src/components/ui/TraceDetail.tsx @@ -1,7 +1,8 @@ import { HttpRequestState } from '@envyjs/core'; import { useCallback, useEffect, useRef } from 'react'; +import { HiX } from 'react-icons/hi'; -import { Code, DateTime, Field, Fields, JsonDisplay, Loading, Section, XmlDisplay } from '@/components'; +import { Code, DateTime, Field, Fields, IconButton, JsonDisplay, Loading, Section, XmlDisplay } from '@/components'; import useApplication from '@/hooks/useApplication'; import { RequestDetailsComponent, @@ -16,6 +17,7 @@ import CopyAsCurlButton from './CopyAsCurlButton'; import QueryParams from './QueryParams'; import RequestHeaders from './RequestHeaders'; import ResponseHeaders from './ResponseHeaders'; +import { TabContent, TabList, TabListItem } from './Tabs'; import TimingsDiagram from './TimingsDiagram'; type CodeDisplayProps = { @@ -31,7 +33,7 @@ function CodeDisplay({ contentType, children, ...props }: CodeDisplayProps) { const isXml = contentType?.includes('application/xml'); return ( - +
    {isJson ? ( {children} ) : isXml ? ( @@ -39,7 +41,7 @@ function CodeDisplay({ contentType, children, ...props }: CodeDisplayProps) { ) : ( {children} )} - +
    ); } @@ -90,123 +92,131 @@ export default function TraceDetail() { } return ( -
    -
    -
    - -
    - -
    -
    -
    - -
    -
    -
    - - - {method} - - {responseComplete && statusCode && ( - - - {`${statusCode > -1 ? statusCode : ''} ${statusMessage}`} - - )} - - - {url} - +
    +
    +
    +
    +
    +
    -
    -
    +
    +
    + + + {method} + + + + clearSelectedTrace()} + className="py-2.5" + data-test-id="close-trace" + /> + + + + {url} + +
    +
    Sent from {serviceName}
    -
    + + {responseComplete && statusCode && ( +
    + + {`${statusCode > -1 ? statusCode : ''} ${statusMessage}`} +
    + )} + + + + {requestBody && } + {responseBody && } +
    -
    - - - - - - {host} - - - {path} - - - {requestBody} - - - - - -
    - -
    - {responseComplete && duration ? ( - <> - - - +
    + +
    + + + - - {statusCode && statusCode > -1 ? statusCode : ''} {statusMessage} + + {host} - - - {numberFormat(duration)}ms + + {path} - {trace.http?.timingsBlockedByCors && ( - - - Disabled by CORS policy - - - )} - {trace.http?.timings && ( - - - - )} + + + - - - ) : ( - - - - - )} -
    - - {responseComplete && ( -
    - - - {getHeader(responseHeaders, 'content-type')} - - - {getHeader(responseHeaders, 'content-length')} - - + +
    + +
    + {responseComplete && duration ? ( + <> + + + + + + {statusCode && statusCode > -1 ? statusCode : ''} {statusMessage} + + + + {numberFormat(duration)}ms + + {trace.http?.timingsBlockedByCors && ( + + + Disabled by CORS policy + + + )} + {trace.http?.timings && ( + + + + )} + + + + ) : ( + + + + + )} +
    +
    + + + + {requestBody} + + + + + {responseComplete && ( + {responseBody} - -
    - )} + )} + +
    ); } diff --git a/packages/webui/src/components/ui/TraceList.tsx b/packages/webui/src/components/ui/TraceList.tsx index 10dc6e7..151a2fc 100644 --- a/packages/webui/src/components/ui/TraceList.tsx +++ b/packages/webui/src/components/ui/TraceList.tsx @@ -50,7 +50,7 @@ function getRequestDuration(trace: Trace) { const columns: [string, (x: Trace) => string | number | React.ReactNode, string, (x: Trace) => string][] = [ ['Method', getMethodAndStatus, 'w-[40px] md:w-[125px] overflow-hidden text-center', pillStyle], ['Request', getRequestURI, '', () => 'whitespace-nowrap overflow-hidden overflow-ellipsis'], - ['Duration', getRequestDuration, 'w-[100px] text-right', () => 'text-sm'], + ['Duration', getRequestDuration, 'w-[75px] text-right', () => 'text-sm'], ]; type TraceListProps = React.HTMLAttributes & { diff --git a/packages/webui/src/context/ApplicationContext.tsx b/packages/webui/src/context/ApplicationContext.tsx index c40ca99..45d04c2 100644 --- a/packages/webui/src/context/ApplicationContext.tsx +++ b/packages/webui/src/context/ApplicationContext.tsx @@ -21,6 +21,9 @@ export default function ApplicationContextProvider({ children }: React.HTMLAttri searchTerm: '', }); + const initialTab = window.location.hash?.replace('#', '') || 'default'; + const [selectedTab, setSelectedTab] = useState(initialTab); + const changeHandler = (newTraceId?: string) => { if (newTraceId) setNewestTraceId(newTraceId); forceUpdate(curr => !curr); @@ -123,6 +126,8 @@ export default function ApplicationContextProvider({ children }: React.HTMLAttri setSelectedTraceId(undefined); collectorRef.current?.clearTraces(); }, + selectedTab, + setSelectedTab, }; return {children}; diff --git a/packages/webui/src/hooks/useApplication.ts b/packages/webui/src/hooks/useApplication.ts index b41d10f..26e76ee 100644 --- a/packages/webui/src/hooks/useApplication.ts +++ b/packages/webui/src/hooks/useApplication.ts @@ -25,6 +25,8 @@ export type ApplicationContextData = { filters: Filters; setFilters: Dispatch>; clearTraces: () => void; + selectedTab: string; + setSelectedTab: Dispatch>; }; export const ApplicationContext = createContext({} as ApplicationContextData); diff --git a/packages/webui/src/testing/mockUseApplication.ts b/packages/webui/src/testing/mockUseApplication.ts index 1e42728..c13ce99 100644 --- a/packages/webui/src/testing/mockUseApplication.ts +++ b/packages/webui/src/testing/mockUseApplication.ts @@ -19,6 +19,8 @@ const defaults: ApplicationContextData = { }, setFilters: () => void 0, clearTraces: () => void 0, + selectedTab: 'default', + setSelectedTab: () => void 0, }; beforeEach(() => { From 895eec591ef3c46d2f61f897196a6e08e17e8d7b Mon Sep 17 00:00:00 2001 From: Charlie Brown Date: Tue, 24 Oct 2023 06:41:00 -0500 Subject: [PATCH 2/3] Changeset --- .changeset/large-needles-allow.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/large-needles-allow.md diff --git a/.changeset/large-needles-allow.md b/.changeset/large-needles-allow.md new file mode 100644 index 0000000..defde92 --- /dev/null +++ b/.changeset/large-needles-allow.md @@ -0,0 +1,5 @@ +--- +'@envyjs/webui': patch +--- + +Implement tabs for payload and response From 1056e41a4a45cbb9da4315c570cc68c1588b6243 Mon Sep 17 00:00:00 2001 From: Charlie Brown Date: Tue, 24 Oct 2023 06:48:36 -0500 Subject: [PATCH 3/3] Fix duration size --- packages/webui/src/components/ui/TraceList.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/webui/src/components/ui/TraceList.tsx b/packages/webui/src/components/ui/TraceList.tsx index 151a2fc..c7a66cf 100644 --- a/packages/webui/src/components/ui/TraceList.tsx +++ b/packages/webui/src/components/ui/TraceList.tsx @@ -50,7 +50,7 @@ function getRequestDuration(trace: Trace) { const columns: [string, (x: Trace) => string | number | React.ReactNode, string, (x: Trace) => string][] = [ ['Method', getMethodAndStatus, 'w-[40px] md:w-[125px] overflow-hidden text-center', pillStyle], ['Request', getRequestURI, '', () => 'whitespace-nowrap overflow-hidden overflow-ellipsis'], - ['Duration', getRequestDuration, 'w-[75px] text-right', () => 'text-sm'], + ['Duration', getRequestDuration, 'text-right', () => 'text-sm'], ]; type TraceListProps = React.HTMLAttributes & {