Skip to content

Commit 5aab138

Browse files
committed
feat(frontend): allow editing of multiple incoming auth tokens with EditableTable
1 parent 8dc334b commit 5aab138

File tree

2 files changed

+162
-3
lines changed

2 files changed

+162
-3
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
import type { ReactNode } from 'react'
2+
import { useEffect, useState } from 'react'
3+
import { Input } from './Input'
4+
import { Table } from './Table'
5+
import { FieldError } from './FieldError'
6+
import { Button } from './Button'
7+
8+
type EditableTableProps = {
9+
name: string
10+
label: string
11+
options: EditableTableOption[]
12+
error?: string | string[]
13+
description?: ReactNode
14+
valueFormatter?: (values: string[]) => string
15+
required?: boolean
16+
}
17+
18+
type EditableTableOption = {
19+
label: string
20+
value: string
21+
canDelete?: boolean
22+
canEdit?: boolean
23+
showInput?: boolean
24+
}
25+
26+
export const EditableTable = ({
27+
name,
28+
label,
29+
options,
30+
error,
31+
description = undefined,
32+
valueFormatter = (values) => values.join(','),
33+
required = false
34+
}: EditableTableProps) => {
35+
const [optionsList, setOptionsList] = useState<EditableTableOption[]>(options)
36+
const [value, setValue] = useState<string>('')
37+
38+
const toggleEditInput = (index: number) => {
39+
setOptionsList(
40+
optionsList.map((option, i) => {
41+
if (i === index) {
42+
return {
43+
...option,
44+
showInput: true
45+
}
46+
}
47+
return option
48+
})
49+
)
50+
}
51+
52+
const editOption = (index: number, value: string) => {
53+
if (!value) {
54+
deleteOption(index)
55+
return
56+
}
57+
setOptionsList(
58+
optionsList.map((option, i) => {
59+
if (i === index) {
60+
return {
61+
...option,
62+
showInput: false,
63+
value
64+
}
65+
}
66+
return option
67+
})
68+
)
69+
}
70+
71+
const deleteOption = (index: number) => {
72+
setOptionsList(optionsList.filter((_, i) => i !== index))
73+
}
74+
75+
const addOption = () => {
76+
setOptionsList([
77+
...optionsList,
78+
{ label: '', value: '', canDelete: true, canEdit: true, showInput: true }
79+
])
80+
}
81+
82+
useEffect(() => {
83+
setValue(getValue())
84+
}, [optionsList])
85+
86+
const getValue = () => {
87+
return valueFormatter(optionsList.map((option) => option.value))
88+
}
89+
90+
return (
91+
<>
92+
<Input
93+
type='hidden'
94+
name={name}
95+
value={value}
96+
required={required}
97+
label={label}
98+
/>
99+
<Table>
100+
<Table.Head columns={['Token', 'Action']} />
101+
<Table.Body>
102+
{(optionsList || []).map((option, index) => (
103+
<Table.Row key={index}>
104+
<Table.Cell key={0}>
105+
{option.showInput ? (
106+
<Input
107+
type='text'
108+
onKeyDown={(e) =>
109+
e.key === 'Enter' &&
110+
(e.preventDefault(),
111+
editOption(index, e.currentTarget.value))
112+
}
113+
defaultValue={option.value}
114+
required={required}
115+
/>
116+
) : (
117+
<span>{option.value}</span>
118+
)}
119+
</Table.Cell>
120+
<Table.Cell key={1}>
121+
{option.canEdit && !option.showInput ? (
122+
<Button
123+
aria-label='save http information'
124+
onClick={() => toggleEditInput(index)}
125+
>
126+
Edit
127+
</Button>
128+
) : null}
129+
{option.canDelete ? (
130+
<Button
131+
className='ml-2'
132+
aria-label='delete http information'
133+
onClick={() => deleteOption(index)}
134+
>
135+
Delete
136+
</Button>
137+
) : null}
138+
</Table.Cell>
139+
</Table.Row>
140+
))}
141+
</Table.Body>
142+
</Table>
143+
<div className='flex justify-end mt-2'>
144+
<Button aria-label='add http information' onClick={() => addOption()}>
145+
Add
146+
</Button>
147+
</div>
148+
{description ? (
149+
<div className='font-medium text-sm'>{description}</div>
150+
) : null}
151+
<FieldError error={error} />
152+
</>
153+
)
154+
}

packages/frontend/app/routes/peers.$peerId.tsx

+8-3
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import {
3030
import type { ZodFieldErrors } from '~/shared/types'
3131
import { formatAmount } from '~/shared/utils'
3232
import { checkAuthAndRedirect } from '../lib/kratos_checks.server'
33+
import { EditableTable } from '~/components/ui/EditableTable'
3334

3435
export async function loader({ request, params }: LoaderFunctionArgs) {
3536
const cookies = request.headers.get('cookie')
@@ -204,10 +205,15 @@ export default function ViewPeerPage() {
204205
<fieldset disabled={currentPageAction}>
205206
<div className='w-full p-4 space-y-3'>
206207
<Input type='hidden' name='id' value={peer.id} />
207-
<Input
208+
<EditableTable
208209
name='incomingAuthTokens'
209210
label='Incoming Auth Tokens'
210-
placeholder='Accepts a comma separated list of tokens'
211+
options={(peer.incomingTokens || []).map((token) => ({
212+
label: token,
213+
value: token,
214+
canDelete: true,
215+
canEdit: true
216+
}))}
211217
error={response?.errors.http.fieldErrors.incomingAuthTokens}
212218
description={
213219
<>
@@ -435,7 +441,6 @@ export async function action({ request }: ActionFunctionArgs) {
435441
result.error.flatten().fieldErrors
436442
return json({ ...actionResponse }, { status: 400 })
437443
}
438-
439444
const response = await updatePeer({
440445
id: result.data.id,
441446
http: {

0 commit comments

Comments
 (0)