Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(dash): refactor ux for adding/removing headers and query params #851

Merged
merged 3 commits into from
Feb 18, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions pkg/dashboard/frontend/cypress/e2e/api-explorer.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ describe('APIs spec', () => {
cy.getTestEl('query-0-key').type('firstParam')
cy.getTestEl('query-0-value').type('myValue')

cy.getTestEl('add-row-btn').click()

cy.getTestEl('query-1-key').type('secondParam')
cy.getTestEl('query-1-value').type('mySecondValue')

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

cy.getTestEl('add-row-btn').click()

cy.getTestEl('header-3-key').clear().type('X-Second-Header')
cy.getTestEl('header-3-value').clear().type('the second value')

Expand Down
2 changes: 2 additions & 0 deletions pkg/dashboard/frontend/cypress/e2e/websockets.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@ describe('Websockets Spec', () => {
cy.getTestEl('query-0-key').type('firstParam')
cy.getTestEl('query-0-value').type('myValue')

cy.getTestEl('add-row-btn').click()

cy.getTestEl('query-1-key').type('secondParam')
cy.getTestEl('query-1-value').type('mySecondValue')

Expand Down
22 changes: 15 additions & 7 deletions pkg/dashboard/frontend/src/components/apis/APIExplorer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,10 @@ const requestDefault = {
key: 'User-Agent',
value: 'Nitric Client (https://www.nitric.io)',
},
{
key: 'Content-Type',
value: 'application/json',
},
],
}

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

// Set a default content type if not set
if (!headers.find(({ key }) => key.toLowerCase() === 'content-type')) {
headers.push({
key: 'Content-Type',
value: currentBodyTab.contentType,
})
}
const shouldAddContentType = !headers.find(
({ key }) => key.toLowerCase() === 'content-type',
)

const requestOptions: RequestInit = {
method,
headers: fieldRowArrToHeaders([
...headers,
...(shouldAddContentType
? [
...headers,
{ key: 'Content-Type', value: currentBodyTab.contentType },
]
: headers),
{
key: 'X-Nitric-Local-Call-Address',
value: apiAddress || 'localhost:4001',
Expand Down Expand Up @@ -600,6 +606,7 @@ const APIExplorer = () => {
<FieldRows
rows={request.queryParams}
testId="query"
addRowLabel="Add Query Param"
setRows={(rows) => {
setRequest((prev) => ({
...prev,
Expand All @@ -615,6 +622,7 @@ const APIExplorer = () => {
<FieldRows
rows={request.headers}
testId="header"
addRowLabel="Add Header"
setRows={(rows) => {
setRequest((prev) => ({
...prev,
Expand Down
2 changes: 1 addition & 1 deletion pkg/dashboard/frontend/src/components/apis/APIHistory.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ const APIHistory: React.FC<Props> = ({
})

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

return (
Expand Down
225 changes: 116 additions & 109 deletions pkg/dashboard/frontend/src/components/shared/FieldRows.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { ExclamationCircleIcon, XMarkIcon } from '@heroicons/react/20/solid'
import { cn } from '@/lib/utils'
import React, { useEffect, useId } from 'react'
import React, { useId } from 'react'
import { Input } from '../ui/input'
import { Label } from '../ui/label'
import { Button } from '../ui/button'

export interface FieldRow {
key: string
Expand All @@ -14,10 +15,10 @@ interface Props {
rows: FieldRow[]
lockKeys?: boolean
readOnly?: boolean
canClearRow?: boolean
valueRequired?: boolean
valueErrors?: Record<number, FieldRow>
setRows: (value: FieldRow[]) => void
addRowLabel?: string
}

const FieldRows: React.FC<Props> = ({
Expand All @@ -28,127 +29,133 @@ const FieldRows: React.FC<Props> = ({
setRows,
valueErrors,
valueRequired,
canClearRow = true,
addRowLabel = 'Add Row',
}) => {
const id = useId()

useEffect(() => {
if (
!lockKeys &&
(rows[rows.length - 1].key || rows[rows.length - 1].value)
) {
setRows([
...rows,
{
key: '',
value: '',
},
])
}
}, [rows])

return (
<ul className="divide-y divide-gray-200">
{rows.map((r, i) => {
const keyId = `${id}-${i}-key`
const valueId = `${id}-${i}-value`
const valueHasError = Boolean(valueErrors && valueErrors[i])
<div>
<ul className="divide-y divide-gray-200">
{rows.map((r, i) => {
const keyId = `${id}-${i}-key`
const valueId = `${id}-${i}-value`
const valueHasError = Boolean(valueErrors && valueErrors[i])

return (
<li
key={i}
className="group relative grid grid-cols-2 items-center gap-4 py-4"
>
<div>
<Label htmlFor={keyId} className="sr-only">
Key
</Label>
<div className="mt-2 sm:col-span-2 sm:mt-0">
<Input
type="text"
data-testid={`${testId}-${i}-key`}
readOnly={lockKeys || readOnly}
placeholder="Key"
className="read-only:opacity-100"
onChange={(e) => {
const updatedRow: FieldRow = { ...r, key: e.target.value }
const newArr = [...rows]
return (
<li key={i} className="group flex items-center gap-4 py-4">
<div className="w-full">
<Label htmlFor={keyId} className="sr-only">
Key
</Label>
<div className="mt-2 sm:col-span-2 sm:mt-0">
<Input
type="text"
data-testid={`${testId}-${i}-key`}
readOnly={lockKeys || readOnly}
placeholder="Key"
className="read-only:opacity-100"
onChange={(e) => {
const updatedRow: FieldRow = { ...r, key: e.target.value }
const newArr = [...rows]

newArr[i] = updatedRow
newArr[i] = updatedRow

setRows(newArr)
}}
value={r.key}
name={keyId}
id={keyId}
/>
setRows(newArr)
}}
value={r.key}
name={keyId}
id={keyId}
/>
</div>
</div>
</div>
<div className="pr-8">
<Label htmlFor={valueId} className="sr-only">
{r.value}
</Label>
<div className="relative mt-2 sm:col-span-2 sm:mt-0">
<Input
type="text"
placeholder="Value"
readOnly={readOnly}
data-testid={`${testId}-${i}-value`}
onChange={(e) => {
const updatedRow: FieldRow = {
...r,
value: e.target.value,
}
const newArr = [...rows]
<div className={cn('w-full', lockKeys && 'mr-11')}>
<Label htmlFor={valueId} className="sr-only">
{r.value}
</Label>
<div className="relative mt-2 sm:col-span-2 sm:mt-0">
<Input
type="text"
placeholder="Value"
readOnly={readOnly}
data-testid={`${testId}-${i}-value`}
onChange={(e) => {
const updatedRow: FieldRow = {
...r,
value: e.target.value,
}
const newArr = [...rows]

newArr[i] = updatedRow
newArr[i] = updatedRow

setRows(newArr)
setRows(newArr)
}}
required={valueRequired}
name={valueId}
id={valueId}
value={r.value}
className={cn(
valueHasError &&
'text-red-900 !ring-red-500 placeholder:text-red-300',
)}
/>
{valueHasError && (
<div
data-testid={`${testId}-${i}-value-error-icon`}
className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3"
>
<ExclamationCircleIcon
className="h-5 w-5 text-red-500"
aria-hidden="true"
/>
</div>
)}
</div>
</div>
{!lockKeys && (
<button
type="button"
onClick={() => {
const newArray = [...rows]
newArray.splice(i, 1)
setRows(newArray)
}}
required={valueRequired}
name={valueId}
id={valueId}
value={r.value}
aria-label="Remove row"
className={cn(
valueHasError &&
'text-red-900 !ring-red-500 placeholder:text-red-300',
'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',
'flex items-center opacity-30 transition-all group-hover:opacity-100',
)}
/>
{valueHasError && (
<div
data-testid={`${testId}-${i}-value-error-icon`}
className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3"
>
<ExclamationCircleIcon
className="h-5 w-5 text-red-500"
aria-hidden="true"
/>
</div>
)}
</div>
</div>
{canClearRow && (
<button
type="button"
onClick={() => {
const newArray = [...rows]
newArray.splice(i, 1)
setRows(newArray)
}}
className={cn(
'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',
rows.length > 1 && (r.key || r.value)
? 'group-hover:block'
: '',
)}
>
<XMarkIcon className="h-5 w-5" aria-hidden="true" />
</button>
)}
>
<XMarkIcon className="h-5 w-5" aria-hidden="true" />
</button>
)}
</li>
)
})}
{rows.length === 0 && (
<li className="mt-1 text-sm text-foreground">
No rows to display. Click &apos;{addRowLabel}&apos; to begin adding
data.
</li>
)
})}
</ul>
)}
</ul>
{!lockKeys && (
<Button
onClick={() => {
setRows([
...rows,
{
key: '',
value: '',
},
])
}}
data-testid="add-row-btn"
className="ml-auto mt-4 flex"
>
{addRowLabel}
</Button>
)}
</div>
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -559,6 +559,7 @@ const WSExplorer = () => {
<FieldRows
rows={queryParams}
readOnly={connected}
addRowLabel="Add Query Param"
testId="query"
setRows={(rows) => {
setQueryParams(rows)
Expand Down Expand Up @@ -587,7 +588,6 @@ const WSExplorer = () => {
</SelectContent>
</Select>
<Button
size={'lg'}
className="ml-auto"
data-testid="send-message-btn"
disabled={!currentPayload || !connected}
Expand Down