Skip to content

Commit

Permalink
Secrets view (#1165)
Browse files Browse the repository at this point in the history
* fix: revert

* feat: add basic view

* feat: add secrets tab

* Revert "fix: revert"

This reverts commit 2a29f11.

* chore: refactor

* feat: use form state

* feat: review comments
  • Loading branch information
sans-harness authored Feb 27, 2025
1 parent fe0c55d commit 1c0ba97
Show file tree
Hide file tree
Showing 9 changed files with 294 additions and 0 deletions.
10 changes: 10 additions & 0 deletions apps/design-system/src/pages/view-preview/view-preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import RepoSummaryViewWrapper from '@subjects/views/repo-summary/repo-summary'
import { RepoTagsList } from '@subjects/views/repo-tags/repo-tags-list'
import { RepoWebhooksCreate } from '@subjects/views/repo-webhooks-create/repo-webhooks-list'
import { RepoWebhooksList } from '@subjects/views/repo-webhooks-list/repo-webhooks-list'
import { SecretsPage } from '@subjects/views/secrets/secrets'
import { SignInView } from '@subjects/views/signin'
import { SignUpView } from '@subjects/views/signup'
import { SpaceSettingsMembers } from '@subjects/views/space-settings-members/space-settings-members'
Expand Down Expand Up @@ -470,6 +471,15 @@ export const viewPreviews: Record<string, ViewPreviewGroup> = {
)
}
}
},
secrets: {
label: 'Secrets',
items: {
'secrets-page': {
label: 'Secrets Page',
element: <SecretsPage />
}
}
}
}

Expand Down
51 changes: 51 additions & 0 deletions apps/design-system/src/subjects/views/secrets/secrets.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { useState } from 'react'

import { noop, useTranslationStore } from '@utils/viewUtils'

import { Button, Drawer, Spacer } from '@harnessio/ui/components'
import { CreateSecretPage, NewSecretFormFields, SecretsHeader, SecretType } from '@harnessio/ui/views'

export const SecretsPage = () => {
const [selectedType, setSelectedType] = useState<SecretType>(SecretType.New)

const onSubmit = (data: NewSecretFormFields) => {
console.log(data)
}

const renderContent = () => {
switch (selectedType) {
case SecretType.New:
return (
<CreateSecretPage
onFormSubmit={onSubmit}
onFormCancel={noop}
useTranslationStore={useTranslationStore}
isLoading={false}
apiError={null}
/>
)
case SecretType.Existing:
return <div>Existing Secret Content</div>
default:
return null
}
}

return (
<Drawer.Root direction="right">
<Drawer.Trigger>
<Button>Add Secret</Button>
</Drawer.Trigger>
<Drawer.Content>
<Drawer.Header>
<Drawer.Title className="text-3xl">Secrets</Drawer.Title>
</Drawer.Header>
<Spacer size={5} />

<SecretsHeader onChange={setSelectedType} />
<Spacer size={5} />
{renderContent()}
</Drawer.Content>
</Drawer.Root>
)
}
3 changes: 3 additions & 0 deletions packages/ui/locales/en/views.json
Original file line number Diff line number Diff line change
Expand Up @@ -476,5 +476,8 @@
"create": "Create webhook",
"edit": "Edit webhook",
"delete": "Delete webhook"
},
"secrets": {
"secretsTitle": "Secrets"
}
}
3 changes: 3 additions & 0 deletions packages/ui/locales/es/views.json
Original file line number Diff line number Diff line change
Expand Up @@ -466,6 +466,9 @@
"edit": "Edit webhook",
"delete": "Delete webhook"
},
"secrets": {
"secretsTitle": "Secrets"
},
"profileSettingsTableHeader": {
"token": "Token",
"status": "Status"
Expand Down
3 changes: 3 additions & 0 deletions packages/ui/locales/fr/views.json
Original file line number Diff line number Diff line change
Expand Up @@ -470,5 +470,8 @@
"create": "Create webhook",
"edit": "Edit webhook",
"delete": "Delete webhook"
},
"secrets": {
"secretsTitle": "Secrets"
}
}
3 changes: 3 additions & 0 deletions packages/ui/src/views/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,6 @@ export * from './labels'

// execution
export * from './execution'

// secrets
export * from './secrets'
2 changes: 2 additions & 0 deletions packages/ui/src/views/secrets/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './new-secret/new-secret-form'
export * from './secrets-header'
148 changes: 148 additions & 0 deletions packages/ui/src/views/secrets/new-secret/new-secret-form.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import { useForm, type SubmitHandler } from 'react-hook-form'

import {
Accordion,
Alert,
Button,
ButtonGroup,
ControlGroup,
Fieldset,
FormWrapper,
Input,
Spacer,
Textarea
} from '@/components'
import { SandboxLayout, TranslationStore } from '@/views'
import { zodResolver } from '@hookform/resolvers/zod'
import { z } from 'zod'

const newSecretformSchema = z.object({
name: z.string().min(1, { message: 'Please provide a name' }),
value: z.string().min(1, { message: 'Please provide a value' }),
description: z.string().optional(),
tags: z.string().optional()
})

export type NewSecretFormFields = z.infer<typeof newSecretformSchema> // Automatically generate a type from the schema

interface CreateSecretPageProps {
onFormSubmit: (data: NewSecretFormFields) => void
onFormCancel: () => void
useTranslationStore: () => TranslationStore
isLoading: boolean
apiError: string | null
}

export function CreateSecretPage({
onFormSubmit,
onFormCancel,
useTranslationStore,
isLoading = false,
apiError = null
}: CreateSecretPageProps) {
const { t: _t } = useTranslationStore()

const {
register,
handleSubmit,
reset,
formState: { errors }
} = useForm<NewSecretFormFields>({
resolver: zodResolver(newSecretformSchema),
mode: 'onChange',
defaultValues: {
name: '',
value: '',
description: '',
tags: ''
}
})

const onSubmit: SubmitHandler<NewSecretFormFields> = data => {
onFormSubmit(data)
reset()
}

const handleCancel = () => {
onFormCancel()
}

return (
<SandboxLayout.Content className="px-0 pt-0 h-full">
<Spacer size={5} />
<FormWrapper className="flex h-full flex-col" onSubmit={handleSubmit(onSubmit)}>
{/* NAME */}
<Fieldset>
<Input
id="name"
label="Secret Name"
{...register('name')}
placeholder="Enter secret name"
size="md"
error={errors.name?.message?.toString()}
autoFocus
/>

<Input
id="value"
{...register('value')}
type="password"
label="Secret Value"
placeholder="Add your secret value"
size="md"
error={errors.value?.message?.toString()}
/>
</Fieldset>
<Accordion.Root type="single" collapsible>
<Accordion.Item value="secret-details">
<Accordion.Trigger>Metadata</Accordion.Trigger>
<Accordion.Content>
<Fieldset className="rounded-md border-2 p-4">
{/* DESCRIPTION */}
<Textarea
id="description"
{...register('description')}
placeholder="Enter a description of this secret"
label="Description"
error={errors.description?.message?.toString()}
optional
/>
{/* TAGS */}
<Input
id="tags"
{...register('tags')}
label="Tags"
placeholder="Enter tags"
size="md"
error={errors.tags?.message?.toString()}
optional
/>
</Fieldset>
</Accordion.Content>
</Accordion.Item>
</Accordion.Root>

{apiError && (
<Alert.Container variant="destructive" className="mb-8">
<Alert.Description>{apiError?.toString()}</Alert.Description>
</Alert.Container>
)}

<div className="fixed bottom-0 left-0 right-0 bg-background-2 p-4 shadow-md">
<ControlGroup>
<ButtonGroup className="flex flex-row justify-between">
<Button type="button" variant="outline" onClick={handleCancel}>
Cancel
</Button>
<Button type="submit" disabled={isLoading}>
{!isLoading ? 'Save' : 'Saving...'}
</Button>
</ButtonGroup>
</ControlGroup>
</div>

<div className="pb-16"></div>
</FormWrapper>
</SandboxLayout.Content>
)
}
71 changes: 71 additions & 0 deletions packages/ui/src/views/secrets/secrets-header.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { useForm } from 'react-hook-form'

import { Option, RadioButton, RadioGroup, StackedList } from '@/components'
import { cn } from '@utils/cn'

export enum SecretType {
New = 'new',
Existing = 'existing'
}

interface SecretTypeForm {
type: SecretType
}

export const SecretsHeader = ({ onChange }: { onChange: (type: SecretType) => void }) => {
const { watch, setValue } = useForm<SecretTypeForm>({
defaultValues: {
type: SecretType.New
}
})

const selectedType = watch('type')

const handleTypeChange = (value: SecretType) => {
setValue('type', value)
onChange(value)
}

return (
<RadioGroup value={selectedType} onValueChange={handleTypeChange} id="secret-type">
<div className="flex flex-col gap-2">
<Option
id="new-secret"
control={
<StackedList.Root className="overflow-hidden" borderBackground>
<StackedList.Item
className={cn('cursor-pointer !rounded px-5 py-3', {
'!bg-background-4': selectedType === SecretType.New
})}
isHeader
isLast
actions={<RadioButton value={SecretType.New} />}
onClick={() => handleTypeChange(SecretType.New)}
>
<StackedList.Field title="New Secret" description="Create a new secret." />
</StackedList.Item>
</StackedList.Root>
}
/>
<Option
id="existing-secret"
control={
<StackedList.Root className="overflow-hidden" borderBackground>
<StackedList.Item
className={cn('cursor-pointer !rounded px-5 py-3', {
'!bg-background-4': selectedType === SecretType.Existing
})}
isHeader
isLast
actions={<RadioButton value={SecretType.Existing} />}
onClick={() => handleTypeChange(SecretType.Existing)}
>
<StackedList.Field title="Existing Secret" description="Use an existing secret." />
</StackedList.Item>
</StackedList.Root>
}
/>
</div>
</RadioGroup>
)
}

0 comments on commit 1c0ba97

Please sign in to comment.