Skip to content

Commit cf2c048

Browse files
authored
fix(dash): refactor ux for adding/removing headers and query params (#851)
1 parent be60100 commit cf2c048

File tree

6 files changed

+139
-118
lines changed

6 files changed

+139
-118
lines changed

pkg/dashboard/frontend/cypress/e2e/api-explorer.cy.ts

+4
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,8 @@ describe('APIs spec', () => {
9999
cy.getTestEl('query-0-key').type('firstParam')
100100
cy.getTestEl('query-0-value').type('myValue')
101101

102+
cy.getTestEl('add-row-btn').click()
103+
102104
cy.getTestEl('query-1-key').type('secondParam')
103105
cy.getTestEl('query-1-value').type('mySecondValue')
104106

@@ -149,6 +151,8 @@ describe('APIs spec', () => {
149151
cy.getTestEl('header-2-key').clear().type('X-First-Header')
150152
cy.getTestEl('header-2-value').clear().type('the value')
151153

154+
cy.getTestEl('add-row-btn').click()
155+
152156
cy.getTestEl('header-3-key').clear().type('X-Second-Header')
153157
cy.getTestEl('header-3-value').clear().type('the second value')
154158

pkg/dashboard/frontend/cypress/e2e/websockets.cy.ts

+2
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,8 @@ describe('Websockets Spec', () => {
9494
cy.getTestEl('query-0-key').type('firstParam')
9595
cy.getTestEl('query-0-value').type('myValue')
9696

97+
cy.getTestEl('add-row-btn').click()
98+
9799
cy.getTestEl('query-1-key').type('secondParam')
98100
cy.getTestEl('query-1-value').type('mySecondValue')
99101

pkg/dashboard/frontend/src/components/apis/APIExplorer.tsx

+15-7
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,10 @@ const requestDefault = {
8282
key: 'User-Agent',
8383
value: 'Nitric Client (https://www.nitric.io)',
8484
},
85+
{
86+
key: 'Content-Type',
87+
value: 'application/json',
88+
},
8589
],
8690
}
8791

@@ -318,17 +322,19 @@ const APIExplorer = () => {
318322
const url = `http://${getHost()}/api/call` + path
319323

320324
// Set a default content type if not set
321-
if (!headers.find(({ key }) => key.toLowerCase() === 'content-type')) {
322-
headers.push({
323-
key: 'Content-Type',
324-
value: currentBodyTab.contentType,
325-
})
326-
}
325+
const shouldAddContentType = !headers.find(
326+
({ key }) => key.toLowerCase() === 'content-type',
327+
)
327328

328329
const requestOptions: RequestInit = {
329330
method,
330331
headers: fieldRowArrToHeaders([
331-
...headers,
332+
...(shouldAddContentType
333+
? [
334+
...headers,
335+
{ key: 'Content-Type', value: currentBodyTab.contentType },
336+
]
337+
: headers),
332338
{
333339
key: 'X-Nitric-Local-Call-Address',
334340
value: apiAddress || 'localhost:4001',
@@ -600,6 +606,7 @@ const APIExplorer = () => {
600606
<FieldRows
601607
rows={request.queryParams}
602608
testId="query"
609+
addRowLabel="Add Query Param"
603610
setRows={(rows) => {
604611
setRequest((prev) => ({
605612
...prev,
@@ -615,6 +622,7 @@ const APIExplorer = () => {
615622
<FieldRows
616623
rows={request.headers}
617624
testId="header"
625+
addRowLabel="Add Header"
618626
setRows={(rows) => {
619627
setRequest((prev) => ({
620628
...prev,

pkg/dashboard/frontend/src/components/apis/APIHistory.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ const APIHistory: React.FC<Props> = ({
5757
})
5858

5959
if (!requestHistory.length) {
60-
return <p>There is no history.</p>
60+
return <p className="px-2">There is no history.</p>
6161
}
6262

6363
return (

pkg/dashboard/frontend/src/components/shared/FieldRows.tsx

+116-109
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { ExclamationCircleIcon, XMarkIcon } from '@heroicons/react/20/solid'
22
import { cn } from '@/lib/utils'
3-
import React, { useEffect, useId } from 'react'
3+
import React, { useId } from 'react'
44
import { Input } from '../ui/input'
55
import { Label } from '../ui/label'
6+
import { Button } from '../ui/button'
67

78
export interface FieldRow {
89
key: string
@@ -14,10 +15,10 @@ interface Props {
1415
rows: FieldRow[]
1516
lockKeys?: boolean
1617
readOnly?: boolean
17-
canClearRow?: boolean
1818
valueRequired?: boolean
1919
valueErrors?: Record<number, FieldRow>
2020
setRows: (value: FieldRow[]) => void
21+
addRowLabel?: string
2122
}
2223

2324
const FieldRows: React.FC<Props> = ({
@@ -28,127 +29,133 @@ const FieldRows: React.FC<Props> = ({
2829
setRows,
2930
valueErrors,
3031
valueRequired,
31-
canClearRow = true,
32+
addRowLabel = 'Add Row',
3233
}) => {
3334
const id = useId()
3435

35-
useEffect(() => {
36-
if (
37-
!lockKeys &&
38-
(rows[rows.length - 1].key || rows[rows.length - 1].value)
39-
) {
40-
setRows([
41-
...rows,
42-
{
43-
key: '',
44-
value: '',
45-
},
46-
])
47-
}
48-
}, [rows])
49-
5036
return (
51-
<ul className="divide-y divide-gray-200">
52-
{rows.map((r, i) => {
53-
const keyId = `${id}-${i}-key`
54-
const valueId = `${id}-${i}-value`
55-
const valueHasError = Boolean(valueErrors && valueErrors[i])
37+
<div>
38+
<ul className="divide-y divide-gray-200">
39+
{rows.map((r, i) => {
40+
const keyId = `${id}-${i}-key`
41+
const valueId = `${id}-${i}-value`
42+
const valueHasError = Boolean(valueErrors && valueErrors[i])
5643

57-
return (
58-
<li
59-
key={i}
60-
className="group relative grid grid-cols-2 items-center gap-4 py-4"
61-
>
62-
<div>
63-
<Label htmlFor={keyId} className="sr-only">
64-
Key
65-
</Label>
66-
<div className="mt-2 sm:col-span-2 sm:mt-0">
67-
<Input
68-
type="text"
69-
data-testid={`${testId}-${i}-key`}
70-
readOnly={lockKeys || readOnly}
71-
placeholder="Key"
72-
className="read-only:opacity-100"
73-
onChange={(e) => {
74-
const updatedRow: FieldRow = { ...r, key: e.target.value }
75-
const newArr = [...rows]
44+
return (
45+
<li key={i} className="group flex items-center gap-4 py-4">
46+
<div className="w-full">
47+
<Label htmlFor={keyId} className="sr-only">
48+
Key
49+
</Label>
50+
<div className="mt-2 sm:col-span-2 sm:mt-0">
51+
<Input
52+
type="text"
53+
data-testid={`${testId}-${i}-key`}
54+
readOnly={lockKeys || readOnly}
55+
placeholder="Key"
56+
className="read-only:opacity-100"
57+
onChange={(e) => {
58+
const updatedRow: FieldRow = { ...r, key: e.target.value }
59+
const newArr = [...rows]
7660

77-
newArr[i] = updatedRow
61+
newArr[i] = updatedRow
7862

79-
setRows(newArr)
80-
}}
81-
value={r.key}
82-
name={keyId}
83-
id={keyId}
84-
/>
63+
setRows(newArr)
64+
}}
65+
value={r.key}
66+
name={keyId}
67+
id={keyId}
68+
/>
69+
</div>
8570
</div>
86-
</div>
87-
<div className="pr-8">
88-
<Label htmlFor={valueId} className="sr-only">
89-
{r.value}
90-
</Label>
91-
<div className="relative mt-2 sm:col-span-2 sm:mt-0">
92-
<Input
93-
type="text"
94-
placeholder="Value"
95-
readOnly={readOnly}
96-
data-testid={`${testId}-${i}-value`}
97-
onChange={(e) => {
98-
const updatedRow: FieldRow = {
99-
...r,
100-
value: e.target.value,
101-
}
102-
const newArr = [...rows]
71+
<div className={cn('w-full', lockKeys && 'mr-11')}>
72+
<Label htmlFor={valueId} className="sr-only">
73+
{r.value}
74+
</Label>
75+
<div className="relative mt-2 sm:col-span-2 sm:mt-0">
76+
<Input
77+
type="text"
78+
placeholder="Value"
79+
readOnly={readOnly}
80+
data-testid={`${testId}-${i}-value`}
81+
onChange={(e) => {
82+
const updatedRow: FieldRow = {
83+
...r,
84+
value: e.target.value,
85+
}
86+
const newArr = [...rows]
10387

104-
newArr[i] = updatedRow
88+
newArr[i] = updatedRow
10589

106-
setRows(newArr)
90+
setRows(newArr)
91+
}}
92+
required={valueRequired}
93+
name={valueId}
94+
id={valueId}
95+
value={r.value}
96+
className={cn(
97+
valueHasError &&
98+
'text-red-900 !ring-red-500 placeholder:text-red-300',
99+
)}
100+
/>
101+
{valueHasError && (
102+
<div
103+
data-testid={`${testId}-${i}-value-error-icon`}
104+
className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3"
105+
>
106+
<ExclamationCircleIcon
107+
className="h-5 w-5 text-red-500"
108+
aria-hidden="true"
109+
/>
110+
</div>
111+
)}
112+
</div>
113+
</div>
114+
{!lockKeys && (
115+
<button
116+
type="button"
117+
onClick={() => {
118+
const newArray = [...rows]
119+
newArray.splice(i, 1)
120+
setRows(newArray)
107121
}}
108-
required={valueRequired}
109-
name={valueId}
110-
id={valueId}
111-
value={r.value}
122+
aria-label="Remove row"
112123
className={cn(
113-
valueHasError &&
114-
'text-red-900 !ring-red-500 placeholder:text-red-300',
124+
'rounded-full bg-gray-600 p-1 text-white shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600',
125+
'flex items-center opacity-30 transition-all group-hover:opacity-100',
115126
)}
116-
/>
117-
{valueHasError && (
118-
<div
119-
data-testid={`${testId}-${i}-value-error-icon`}
120-
className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3"
121-
>
122-
<ExclamationCircleIcon
123-
className="h-5 w-5 text-red-500"
124-
aria-hidden="true"
125-
/>
126-
</div>
127-
)}
128-
</div>
129-
</div>
130-
{canClearRow && (
131-
<button
132-
type="button"
133-
onClick={() => {
134-
const newArray = [...rows]
135-
newArray.splice(i, 1)
136-
setRows(newArray)
137-
}}
138-
className={cn(
139-
'absolute right-0 hidden rounded-full bg-gray-600 p-1 text-white shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600',
140-
rows.length > 1 && (r.key || r.value)
141-
? 'group-hover:block'
142-
: '',
143-
)}
144-
>
145-
<XMarkIcon className="h-5 w-5" aria-hidden="true" />
146-
</button>
147-
)}
127+
>
128+
<XMarkIcon className="h-5 w-5" aria-hidden="true" />
129+
</button>
130+
)}
131+
</li>
132+
)
133+
})}
134+
{rows.length === 0 && (
135+
<li className="mt-1 text-sm text-foreground">
136+
No rows to display. Click &apos;{addRowLabel}&apos; to begin adding
137+
data.
148138
</li>
149-
)
150-
})}
151-
</ul>
139+
)}
140+
</ul>
141+
{!lockKeys && (
142+
<Button
143+
onClick={() => {
144+
setRows([
145+
...rows,
146+
{
147+
key: '',
148+
value: '',
149+
},
150+
])
151+
}}
152+
data-testid="add-row-btn"
153+
className="ml-auto mt-4 flex"
154+
>
155+
{addRowLabel}
156+
</Button>
157+
)}
158+
</div>
152159
)
153160
}
154161

pkg/dashboard/frontend/src/components/websockets/WSExplorer.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -559,6 +559,7 @@ const WSExplorer = () => {
559559
<FieldRows
560560
rows={queryParams}
561561
readOnly={connected}
562+
addRowLabel="Add Query Param"
562563
testId="query"
563564
setRows={(rows) => {
564565
setQueryParams(rows)
@@ -587,7 +588,6 @@ const WSExplorer = () => {
587588
</SelectContent>
588589
</Select>
589590
<Button
590-
size={'lg'}
591591
className="ml-auto"
592592
data-testid="send-message-btn"
593593
disabled={!currentPayload || !connected}

0 commit comments

Comments
 (0)