Skip to content

Commit

Permalink
feat: reusable checkbox component (#3784)
Browse files Browse the repository at this point in the history
- Fixes #3404
- use it for the new json mode flag in config/secrets

![Screenshot 2024-12-16 at 1 59
09 PM](https://github.com/user-attachments/assets/94adf6f4-feeb-4f36-87ab-48592a871a54)
![Screenshot 2024-12-16 at 2 02
35 PM](https://github.com/user-attachments/assets/27143bc2-6315-48ff-95d4-9daed3c2ece2)
  • Loading branch information
wesbillman authored Dec 16, 2024
1 parent 7534973 commit 768fa31
Show file tree
Hide file tree
Showing 7 changed files with 125 additions and 75 deletions.
31 changes: 31 additions & 0 deletions frontend/console/src/components/Checkbox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import type { InputHTMLAttributes, ReactNode } from 'react'

interface CheckboxProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'type'> {
label?: ReactNode
}

export const Checkbox = ({ label, className = '', ...props }: CheckboxProps) => {
return (
<label className='inline-flex items-center w-full'>
<div className='group grid size-4 grid-cols-1 flex-shrink-0'>
<input
defaultChecked
type='checkbox'
aria-describedby='comments-description'
className='col-start-1 row-start-1 appearance-none rounded border border-gray-300 bg-white dark:border-gray-600 dark:bg-gray-700 checked:border-indigo-600 checked:bg-indigo-600 dark:checked:border-indigo-600 dark:checked:bg-indigo-600 hover:bg-white focus:bg-white focus-visible:bg-white active:bg-white dark:hover:bg-gray-700 dark:focus:bg-gray-700 dark:focus-visible:bg-gray-700 dark:active:bg-gray-700 checked:hover:bg-indigo-500 checked:focus:bg-indigo-600 dark:checked:hover:bg-indigo-500 dark:checked:focus:bg-indigo-600 disabled:border-gray-300 disabled:bg-gray-100 disabled:checked:bg-gray-100 dark:disabled:border-gray-700 dark:disabled:bg-gray-900 dark:disabled:checked:bg-gray-700 forced-colors:appearance-auto focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 focus:ring-0 focus:ring-offset-0'
{...props}
/>
<svg
aria-hidden='true'
fill='none'
viewBox='0 0 14 14'
className='pointer-events-none col-start-1 row-start-1 size-3.5 self-center justify-self-center stroke-white group-has-[:disabled]:stroke-gray-950/25'
>
<path d='M3 8L6 11L11 3.5' strokeWidth={2} strokeLinecap='round' strokeLinejoin='round' className='opacity-0 group-has-[:checked]:opacity-100' />
<path d='M3 7H11' strokeWidth={2} strokeLinecap='round' strokeLinejoin='round' className='opacity-0 group-has-[:indeterminate]:opacity-100' />
</svg>
</div>
{label && <div className='ml-2 text-sm dark:text-gray-300 flex-grow'>{label}</div>}
</label>
)
}
2 changes: 1 addition & 1 deletion frontend/console/src/components/Multiselect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ export const Multiselect = ({
<ListboxOptions
anchor='bottom'
transition
className='w-[var(--button-width)] min-w-48 ml-2 pt-1 rounded-md border dark:border-white/5 bg-white dark:bg-gray-800 transition duration-100 ease-in truncate drop-shadow-lg z-20'
className='w-[var(--button-width)] min-w-48 mt-1 pt-1 rounded-md border dark:border-white/5 bg-white dark:bg-gray-800 transition duration-100 ease-in truncate drop-shadow-lg z-20'
>
{allOpts
.filter((o) => !o.group)
Expand Down
25 changes: 10 additions & 15 deletions frontend/console/src/features/modules/ModulesTree.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -178,22 +178,17 @@ export const ModulesTree = ({ modules }: { modules: ModuleTreeItem[] }) => {
<div className='flex grow flex-col h-full gap-y-5 overflow-y-auto bg-gray-100 dark:bg-gray-900'>
<nav>
<div className='sticky top-0 border-b border-gray-300 bg-gray-100 dark:border-gray-800 dark:bg-gray-900 z-10 flex items-center'>
<span className='w-full'>
<div className='flex-1 min-w-0'>
<Multiselect allOpts={declTypeMultiselectOpts} selectedOpts={selectedDeclTypes} onChange={msOnChange} />
</span>
<Button
id='hide-exported'
variant='secondary'
size='sm'
onClick={() => setHideUnexportedState(!hideUnexported)}
title='Show/hide unexported'
className='mr-1'
>
{hideUnexported ? <ViewOffSlashIcon className='size-5 ' /> : <ViewIcon className='size-5' />}
</Button>
<Button variant='secondary' size='sm' onClick={collapseAll} title='Collapse all modules' className='mr-1'>
<ArrowShrink02Icon className='size-5' />
</Button>
</div>
<div className='flex-none flex gap-1'>
<Button id='hide-exported' variant='secondary' size='sm' onClick={() => setHideUnexportedState(!hideUnexported)} title='Show/hide unexported'>
{hideUnexported ? <ViewOffSlashIcon className='size-5' /> : <ViewIcon className='size-5' />}
</Button>
<Button variant='secondary' size='sm' onClick={collapseAll} title='Collapse all modules'>
<ArrowShrink02Icon className='size-5' />
</Button>
</div>
</div>
<ul>
{modules.map((m) => (
Expand Down
15 changes: 3 additions & 12 deletions frontend/console/src/features/modules/decls/KeyValuePairForm.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { CheckmarkSquare01Icon, Delete03Icon, SquareIcon } from 'hugeicons-react'
import { Delete03Icon } from 'hugeicons-react'
import { useEffect, useRef } from 'react'
import { Checkbox } from '../../../components/Checkbox'

interface KeyValuePair {
id: string
Expand Down Expand Up @@ -58,17 +59,7 @@ export const KeyValuePairForm = ({ keyValuePairs, onChange }: KeyValuePairFormPr
<div className='space-y-2'>
{keyValuePairs.map((pair, index) => (
<div key={pair.id} className='flex items-center gap-2'>
<button
type='button'
onClick={() => updatePair(pair.id, { enabled: !pair.enabled })}
className='text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300 flex-shrink-0'
>
{pair.enabled ? (
<CheckmarkSquare01Icon className='h-5 w-5 text-indigo-500 dark:text-indigo-400' />
) : (
<SquareIcon className='h-5 w-5 dark:text-gray-600' />
)}
</button>
<Checkbox checked={pair.enabled} onChange={(e) => updatePair(pair.id, { enabled: e.target.checked })} />
<input
ref={(el) => {
inputRefs.current[pair.id] = el
Expand Down
39 changes: 31 additions & 8 deletions frontend/console/src/features/modules/decls/config/ConfigPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useContext, useEffect, useState } from 'react'
import { Button } from '../../../../components/Button'
import { Checkbox } from '../../../../components/Checkbox'
import { CodeEditor } from '../../../../components/CodeEditor'
import { ResizablePanels } from '../../../../components/ResizablePanels'
import { useClient } from '../../../../hooks/use-client'
Expand All @@ -15,6 +16,7 @@ export const ConfigPanel = ({ value, schema, moduleName, declName }: { value: Co
const client = useClient(ConsoleService)
const [configValue, setConfigValue] = useState('')
const [isLoading, setIsLoading] = useState(false)
const [isJsonMode, setIsJsonMode] = useState(false)
const notification = useContext(NotificationsContext)

useEffect(() => {
Expand All @@ -31,11 +33,29 @@ export const ConfigPanel = ({ value, schema, moduleName, declName }: { value: Co

const handleSetConfig = () => {
setIsLoading(true)
let valueToSend = configValue

if (isJsonMode) {
try {
JSON.parse(configValue)
} catch (e) {
notification?.showNotification({
title: 'Invalid JSON',
message: 'Please enter valid JSON',
type: NotificationType.Error,
})
setIsLoading(false)
return
}
} else {
valueToSend = configValue
}

client
.setConfig({
module: moduleName,
name: declName,
value: new TextEncoder().encode(configValue),
value: new TextEncoder().encode(valueToSend),
})
.then(() => {
setIsLoading(false)
Expand Down Expand Up @@ -71,13 +91,16 @@ export const ConfigPanel = ({ value, schema, moduleName, declName }: { value: Co
<div className=''>
<PanelHeader title='Config' declRef={`${moduleName}.${declName}`} exported={false} comments={decl.comments} />
<CodeEditor value={configValue} onTextChanged={setConfigValue} />
<div className='mt-2 space-x-2 flex flex-nowrap justify-end'>
<Button onClick={handleSetConfig} disabled={isLoading}>
Save
</Button>
<Button onClick={handleGetConfig} disabled={isLoading}>
Refresh
</Button>
<div className='mt-2 flex items-center justify-between'>
<Checkbox checked={isJsonMode} onChange={(e) => setIsJsonMode(e.target.checked)} label='JSON mode' />
<div className='space-x-2 flex flex-nowrap'>
<Button onClick={handleSetConfig} disabled={isLoading}>
Save
</Button>
<Button onClick={handleGetConfig} disabled={isLoading}>
Refresh
</Button>
</div>
</div>
</div>
</div>
Expand Down
37 changes: 29 additions & 8 deletions frontend/console/src/features/modules/decls/secret/SecretPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useContext, useEffect, useState } from 'react'
import { Button } from '../../../../components/Button'
import { Checkbox } from '../../../../components/Checkbox'
import { CodeEditor } from '../../../../components/CodeEditor'
import { ResizablePanels } from '../../../../components/ResizablePanels'
import { useClient } from '../../../../hooks/use-client'
Expand All @@ -15,6 +16,7 @@ export const SecretPanel = ({ value, schema, moduleName, declName }: { value: Se
const client = useClient(ConsoleService)
const [secretValue, setSecretValue] = useState('')
const [isLoading, setIsLoading] = useState(false)
const [isJsonMode, setIsJsonMode] = useState(false)
const notification = useContext(NotificationsContext)

useEffect(() => {
Expand All @@ -41,11 +43,27 @@ export const SecretPanel = ({ value, schema, moduleName, declName }: { value: Se

const handleSetSecret = () => {
setIsLoading(true)
const valueToSend = secretValue

if (isJsonMode) {
try {
JSON.parse(secretValue)
} catch (e) {
notification?.showNotification({
title: 'Invalid JSON',
message: 'Please enter valid JSON',
type: NotificationType.Error,
})
setIsLoading(false)
return
}
}

client
.setSecret({
module: moduleName,
name: declName,
value: new TextEncoder().encode(secretValue),
value: new TextEncoder().encode(valueToSend),
})
.then(() => {
setIsLoading(false)
Expand Down Expand Up @@ -82,13 +100,16 @@ export const SecretPanel = ({ value, schema, moduleName, declName }: { value: Se
<div className=''>
<PanelHeader title='Secret' declRef={`${moduleName}.${declName}`} exported={false} comments={decl.comments} />
<CodeEditor value={secretValue} onTextChanged={setSecretValue} />
<div className='mt-2 space-x-2 flex flex-nowrap justify-end'>
<Button onClick={handleSetSecret} disabled={isLoading}>
Save
</Button>
<Button onClick={handleGetSecret} disabled={isLoading}>
Refresh
</Button>
<div className='mt-2 flex items-center justify-between'>
<Checkbox checked={isJsonMode} onChange={(e) => setIsJsonMode(e.target.checked)} label='JSON mode' />
<div className='space-x-2 flex flex-nowrap'>
<Button onClick={handleSetSecret} disabled={isLoading}>
Save
</Button>
<Button onClick={handleGetSecret} disabled={isLoading}>
Refresh
</Button>
</div>
</div>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type React from 'react'
import { useEffect, useState } from 'react'
import { useModules } from '../../../api/modules/use-modules'
import { eventTypesFilter, logLevelFilter, modulesFilter } from '../../../api/timeline'
import { Checkbox } from '../../../components/Checkbox'
import { EventType, LogLevel } from '../../../protos/xyz/block/ftl/timeline/v1/event_pb'
import type { GetTimelineRequest_Filter } from '../../../protos/xyz/block/ftl/timeline/v1/timeline_pb'
import { textColor } from '../../../utils'
Expand Down Expand Up @@ -109,26 +110,23 @@ export const TimelineFilterPanel = ({
<div className='w-full'>
<div className='mx-auto w-full max-w-md pt-2 pl-2 pb-2'>
<FilterPanelSection title='Event types'>
{Object.keys(EVENT_TYPES).map((key) => (
<div key={key} className='relative flex items-start'>
<div className='flex h-6 items-center'>
<input
<div className='space-y-1'>
{Object.keys(EVENT_TYPES).map((key) => (
<div key={key} className='relative flex items-start'>
<Checkbox
id={`event-type-${key}`}
name={`event-type-${key}`}
type='checkbox'
checked={selectedEventTypes.includes(key)}
onChange={(e) => handleTypeChanged(key, e.target.checked)}
className='h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-600 cursor-pointer'
label={
<div className='flex items-center justify-between w-full'>
<span className={textColor}>{EVENT_TYPES[key].label}</span>
{EVENT_TYPES[key].icon}
</div>
}
/>
</div>
<div className='ml-2 text-sm leading-6 w-full'>
<label htmlFor={`event-type-${key}`} className={`flex justify-between items-center ${textColor} cursor-pointer`}>
{EVENT_TYPES[key].label}
<span>{EVENT_TYPES[key].icon}</span>
</label>
</div>
</div>
))}
))}
</div>
</FilterPanelSection>

<FilterPanelSection title='Log level'>
Expand Down Expand Up @@ -166,22 +164,13 @@ export const TimelineFilterPanel = ({
</button>
</div>
{modules.data.modules.map((module) => (
<div key={module.deploymentKey} className='relative flex items-start'>
<div className='flex h-6 items-center'>
<input
id={`module-${module.deploymentKey}`}
name={`module-${module.deploymentKey}`}
type='checkbox'
checked={selectedModules.includes(module.deploymentKey)}
onChange={(e) => handleModuleChanged(module.deploymentKey, e.target.checked)}
className='h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-600 cursor-pointer'
/>
</div>
<div className='ml-2 text-sm leading-6 w-full'>
<label htmlFor={`module-${module.deploymentKey}`} className={`${textColor} flex cursor-pointer`}>
{module.name}
</label>
</div>
<div key={module.deploymentKey}>
<Checkbox
id={`module-${module.deploymentKey}`}
checked={selectedModules.includes(module.deploymentKey)}
onChange={(e) => handleModuleChanged(module.deploymentKey, e.target.checked)}
label={<span className={textColor}>{module.name}</span>}
/>
</div>
))}
</FilterPanelSection>
Expand Down

0 comments on commit 768fa31

Please sign in to comment.