Skip to content

Commit 68df3b5

Browse files
authored
Merge pull request #276 from oasisprotocol/mz/secretsLayout
Secrets table layout
2 parents a0f116a + f9da891 commit 68df3b5

8 files changed

Lines changed: 311 additions & 245 deletions

File tree

src/components/InputFormField/index.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { Eye, EyeOff } from 'lucide-react'
99
type InputFormFieldProps<T extends FieldValues> = {
1010
control: Control<T>
1111
name: Path<T>
12-
label: string
12+
label?: string
1313
placeholder?: string
1414
type?: 'input' | 'password' | 'number' | 'textarea'
1515
disabled?: boolean
@@ -40,7 +40,7 @@ export const InputFormField = <T extends FieldValues>({
4040

4141
return (
4242
<div className="grid gap-2">
43-
<Label htmlFor={name}>{label}</Label>
43+
{label && <Label htmlFor={name}>{label}</Label>}
4444
<Controller
4545
control={control}
4646
name={name}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { type FC } from 'react'
2+
import { Button } from '@oasisprotocol/ui-library/src/components/ui/button'
3+
import { z } from 'zod'
4+
import { zodResolver } from '@hookform/resolvers/zod'
5+
import { useForm } from 'react-hook-form'
6+
import { InputFormField } from '../InputFormField'
7+
import { Plus } from 'lucide-react'
8+
9+
const formSchema = z.object({
10+
name: z.string().min(1, {
11+
message: 'Name is required.',
12+
}),
13+
value: z.string().min(1, {
14+
message: 'Secret is required.',
15+
}),
16+
})
17+
18+
type AddSecretProps = {
19+
handleAddSecret?: (name: string, value: string) => void
20+
disabled?: boolean
21+
}
22+
23+
export const AddSecret: FC<AddSecretProps> = ({ handleAddSecret, disabled }) => {
24+
const form = useForm<z.infer<typeof formSchema>>({
25+
resolver: zodResolver(formSchema),
26+
defaultValues: {
27+
name: '',
28+
value: '',
29+
},
30+
})
31+
32+
function onSubmit(values: z.infer<typeof formSchema>) {
33+
handleAddSecret?.(values.name, values.value)
34+
form.reset()
35+
}
36+
37+
return (
38+
<form onSubmit={form.handleSubmit(onSubmit)} className="flex gap-4 ">
39+
<div className="w-1/2">
40+
<InputFormField control={form.control} name="name" placeholder="Type Name" disabled={disabled} />
41+
</div>
42+
<div className="w-1/2">
43+
<InputFormField
44+
control={form.control}
45+
name="value"
46+
placeholder="Type Value"
47+
type="password"
48+
disabled={disabled}
49+
/>
50+
</div>
51+
<Button variant="secondary" size="icon" type="submit" disabled={disabled}>
52+
<Plus className="h-4 w-4" />
53+
</Button>
54+
</form>
55+
)
56+
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import { type FC, useEffect } from 'react'
2+
import { Button } from '@oasisprotocol/ui-library/src/components/ui/button'
3+
import {
4+
Dialog,
5+
DialogContent,
6+
DialogDescription,
7+
DialogFooter,
8+
DialogHeader,
9+
DialogTitle,
10+
} from '@oasisprotocol/ui-library/src/components/ui/dialog'
11+
import { z } from 'zod'
12+
import { zodResolver } from '@hookform/resolvers/zod'
13+
import { useForm } from 'react-hook-form'
14+
import { InputFormField } from '../InputFormField'
15+
16+
const formSchema = z.object({
17+
name: z.string().min(1, {
18+
message: 'Name is required.',
19+
}),
20+
value: z.string().min(1, {
21+
message: 'Secret is required.',
22+
}),
23+
})
24+
25+
type EditSecretDialogProps = {
26+
open: boolean
27+
onOpenChange: (open: boolean) => void
28+
secret?: string
29+
handleEditSecret?: (name: string, value: string) => void
30+
editEnabled?: boolean
31+
}
32+
33+
export const EditSecretDialog: FC<EditSecretDialogProps> = ({
34+
open,
35+
onOpenChange,
36+
secret,
37+
handleEditSecret,
38+
}) => {
39+
const form = useForm<z.infer<typeof formSchema>>({
40+
resolver: zodResolver(formSchema),
41+
defaultValues: {
42+
name: '',
43+
value: '',
44+
},
45+
})
46+
47+
function onCancel() {
48+
form.reset()
49+
onOpenChange(false)
50+
}
51+
52+
useEffect(() => {
53+
if (open) {
54+
form.reset({
55+
name: secret,
56+
value: '',
57+
})
58+
}
59+
}, [open, secret, form])
60+
61+
function handleDialogOpenChange(newOpen: boolean) {
62+
if (!newOpen) {
63+
onCancel()
64+
}
65+
onOpenChange(newOpen)
66+
}
67+
68+
function onSubmit(values: z.infer<typeof formSchema>) {
69+
handleEditSecret?.(secret!, values.value)
70+
71+
form.reset()
72+
onOpenChange(false)
73+
}
74+
75+
return (
76+
<Dialog open={open} onOpenChange={handleDialogOpenChange}>
77+
<DialogContent className="sm:max-w-[425px]">
78+
<DialogHeader>
79+
<DialogTitle>Edit secret</DialogTitle>
80+
</DialogHeader>
81+
<DialogDescription className="mb-6">Please provide a new secret value.</DialogDescription>
82+
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
83+
<InputFormField control={form.control} name="name" label="Name" disabled />
84+
85+
<InputFormField control={form.control} name="value" label="Value" type="password" />
86+
87+
<DialogFooter>
88+
<div className="flex flex-1 justify-between">
89+
<Button variant="outline" onClick={onCancel} type="reset">
90+
Cancel
91+
</Button>
92+
<Button type="submit">Replace</Button>
93+
</div>
94+
</DialogFooter>
95+
</form>
96+
</DialogContent>
97+
</Dialog>
98+
)
99+
}

src/pages/Dashboard/AppDetails/RemoveSecret.tsx renamed to src/components/SecretsTable/RemoveSecretDialog.tsx

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,24 +8,23 @@ import {
88
DialogFooter,
99
DialogHeader,
1010
DialogTitle,
11-
DialogTrigger,
1211
} from '@oasisprotocol/ui-library/src/components/ui/dialog'
13-
import { Trash2 } from 'lucide-react'
1412

15-
type RemoveSecretProps = {
13+
type RemoveSecretDialogProps = {
1614
secret: string
1715
handleRemoveSecret: (secret: string) => void
18-
editEnabled?: boolean
16+
open: boolean
17+
onOpenChange: (open: boolean) => void
1918
}
2019

21-
export const RemoveSecret: FC<RemoveSecretProps> = ({ secret, handleRemoveSecret, editEnabled }) => {
20+
export const RemoveSecretDialog: FC<RemoveSecretDialogProps> = ({
21+
secret,
22+
handleRemoveSecret,
23+
open,
24+
onOpenChange,
25+
}) => {
2226
return (
23-
<Dialog>
24-
<DialogTrigger asChild>
25-
<Button disabled={!editEnabled} variant="ghost" className="text-destructive hover:text-destructive">
26-
<Trash2 />
27-
</Button>
28-
</DialogTrigger>
27+
<Dialog open={open} onOpenChange={onOpenChange}>
2928
<DialogContent className="sm:max-w-[425px]">
3029
<DialogHeader>
3130
<DialogTitle>Please confirm your action</DialogTitle>
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
import { useState, type FC } from 'react'
2+
import {
3+
Table,
4+
TableHeader,
5+
TableBody,
6+
TableRow,
7+
TableHead,
8+
TableCell,
9+
} from '@oasisprotocol/ui-library/src/components/ui/table'
10+
import { RoflApp, type RoflAppSecrets } from '../../nexus/api'
11+
import { RemoveSecretDialog } from './RemoveSecretDialog'
12+
import { EditSecretDialog } from './EditSecretDialog'
13+
import * as oasis from '@oasisprotocol/client'
14+
import * as oasisRT from '@oasisprotocol/client-rt'
15+
import { Button } from '@oasisprotocol/ui-library/src/components/ui/button'
16+
import {
17+
DropdownMenu,
18+
DropdownMenuTrigger,
19+
DropdownMenuContent,
20+
DropdownMenuItem,
21+
} from '@oasisprotocol/ui-library/src/components/ui/dropdown-menu'
22+
import { MoreVertical } from 'lucide-react'
23+
import { AddSecret } from './AddSecret'
24+
25+
type SecretsTableProps = {
26+
appSek: RoflApp['sek']
27+
secrets: RoflAppSecrets
28+
setViewSecretsState: (state: { isDirty: boolean; secrets: RoflAppSecrets }) => void
29+
editEnabled?: boolean
30+
}
31+
32+
export const SecretsTable: FC<SecretsTableProps> = ({
33+
appSek,
34+
secrets,
35+
setViewSecretsState,
36+
editEnabled,
37+
}) => {
38+
const [openRemoveDialog, setOpenRemoveDialog] = useState(false)
39+
const [selectedSecret, setSelectedSecret] = useState<string>('')
40+
const [secretDialogState, setSecretDialogState] = useState<{
41+
open: boolean
42+
mode: 'add' | 'edit'
43+
secretKey?: string
44+
}>({ open: false, mode: 'add' })
45+
46+
const handleOpenRemoveDialog = (secretKey: string) => {
47+
setSelectedSecret(secretKey)
48+
setOpenRemoveDialog(true)
49+
}
50+
51+
const handleOpenEditDialog = (key: string) => {
52+
setSecretDialogState({ open: true, mode: 'edit', secretKey: key })
53+
}
54+
55+
const hasSecrets = Object.keys(secrets).length > 0
56+
57+
function handleRemoveSecret(secret: string) {
58+
const updatedSecrets = { ...secrets }
59+
delete updatedSecrets[secret]
60+
setViewSecretsState({
61+
isDirty: true,
62+
secrets: updatedSecrets,
63+
})
64+
}
65+
66+
function handleEditSecret(key: string, value: string) {
67+
const secretValue = oasisRT.rofl.encryptSecret(
68+
key,
69+
oasis.misc.fromString(value),
70+
oasis.misc.fromBase64(appSek),
71+
)
72+
const updatedSecrets = { ...secrets, [key]: secretValue }
73+
setViewSecretsState({
74+
isDirty: true,
75+
secrets: updatedSecrets,
76+
})
77+
}
78+
79+
return (
80+
<div className="space-y-4 gap-4 flex flex-col">
81+
{hasSecrets && (
82+
<Table>
83+
<TableHeader>
84+
<TableRow>
85+
<TableHead className="w-[45%]">
86+
<span className="text-muted-foreground">Name</span>
87+
</TableHead>
88+
<TableHead className="w-[45%]">
89+
<span className="text-muted-foreground">Value</span>
90+
</TableHead>
91+
<TableHead className="w-10"></TableHead>
92+
</TableRow>
93+
</TableHeader>
94+
<TableBody>
95+
{Object.keys(secrets).map(key => (
96+
<TableRow key={key}>
97+
<TableCell>
98+
{key}: [{(secrets[key] as string).length} bytes]
99+
</TableCell>
100+
<TableCell>
101+
<span className="text-muted-foreground">Encrypted</span>
102+
</TableCell>
103+
<TableCell className="w-10" align="right">
104+
<DropdownMenu>
105+
<DropdownMenuTrigger asChild>
106+
<Button variant="ghost" size="icon" disabled={!editEnabled}>
107+
<MoreVertical className="h-4 w-4" />
108+
</Button>
109+
</DropdownMenuTrigger>
110+
<DropdownMenuContent align="end">
111+
<DropdownMenuItem onClick={() => handleOpenEditDialog(key)}>Edit</DropdownMenuItem>
112+
<DropdownMenuItem variant="destructive" onClick={() => handleOpenRemoveDialog(key)}>
113+
Remove
114+
</DropdownMenuItem>
115+
</DropdownMenuContent>
116+
</DropdownMenu>
117+
</TableCell>
118+
</TableRow>
119+
))}
120+
</TableBody>
121+
</Table>
122+
)}
123+
124+
<AddSecret disabled={!editEnabled} handleAddSecret={handleEditSecret} />
125+
126+
<EditSecretDialog
127+
open={secretDialogState.open}
128+
secret={secretDialogState.secretKey}
129+
onOpenChange={open => setSecretDialogState(prev => ({ ...prev, open }))}
130+
handleEditSecret={handleEditSecret}
131+
editEnabled={editEnabled}
132+
/>
133+
134+
<RemoveSecretDialog
135+
open={openRemoveDialog}
136+
handleRemoveSecret={handleRemoveSecret}
137+
onOpenChange={setOpenRemoveDialog}
138+
secret={selectedSecret}
139+
/>
140+
</div>
141+
)
142+
}

0 commit comments

Comments
 (0)