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

feat: new console pages for config, data, database, enum, secret, and typealias #2617

Merged
merged 4 commits into from
Sep 5, 2024
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
31 changes: 26 additions & 5 deletions frontend/console/src/features/modules/ModulesTree.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,24 @@ const DeclNode = ({ decl, href, isSelected }: { decl: Decl; href: string; isSele
return []
}
const navigate = useNavigate()
const declRef = useRef<HTMLDivElement>(null)

// Scroll to the selected decl on page load
useEffect(() => {
if (isSelected && declRef.current) {
const { top } = declRef.current.getBoundingClientRect()
const { innerHeight } = window
if (top < 64 || top > innerHeight) {
declRef.current.scrollIntoView()
}
}
}, [isSelected])

const Icon = useMemo(() => icons[decl.value.case || ''] || CodeIcon, [decl.value.case])
return (
<li className='my-1'>
<div
ref={declRef}
id={`decl-${decl.value.value.name}`}
className={classNames(
isSelected ? 'bg-gray-200 dark:bg-gray-700 hover:bg-gray-300 hover:dark:bg-gray-600' : 'hover:bg-gray-200 hover:dark:bg-gray-700',
Expand All @@ -72,17 +86,24 @@ const ModuleSection = ({ module, isExpanded, toggleExpansion }: { module: Module
const { moduleName, declName } = useParams()
const navigate = useNavigate()
const isSelected = useMemo(() => moduleName === module.name, [moduleName, module.name])
const selectedRef = useRef<HTMLButtonElement>(null)
const refProp = isSelected ? { ref: selectedRef } : {}
const moduleRef = useRef<HTMLButtonElement>(null)

// Scroll to the selected module on the first page load
useEffect(() => selectedRef.current?.scrollIntoView(), [])
// Scroll to the selected module on page load
useEffect(() => {
if (isSelected && !declName && moduleRef.current) {
const { top } = moduleRef.current.getBoundingClientRect()
const { innerHeight } = window
if (top < 64 || top > innerHeight) {
moduleRef.current.scrollIntoView()
}
}
}, [moduleName]) // moduleName is the selected module; module.name is the one being rendered

return (
<li key={module.name} id={`module-tree-module-${module.name}`} className='my-2'>
<Disclosure as='div' defaultOpen={isExpanded}>
<DisclosureButton
{...refProp}
ref={moduleRef}
className={classNames(
isSelected ? 'bg-gray-200 dark:bg-gray-700 hover:bg-gray-300 hover:dark:bg-gray-600' : 'hover:bg-gray-200 hover:dark:bg-gray-700',
'group flex w-full modules-center gap-x-2 space-y-1 rounded-md px-2 text-left text-sm font-medium leading-6',
Expand Down
16 changes: 16 additions & 0 deletions frontend/console/src/features/modules/decls/ConfigPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type { Config } from '../../../protos/xyz/block/ftl/v1/schema/schema_pb'
import { PanelHeader } from './PanelHeader'
import { TypeEl } from './TypeEl'

export const ConfigPanel = ({ value, moduleName, declName }: { value: Config; moduleName: string; declName: string }) => {
return (
<div className='py-2 px-4'>
<PanelHeader exported={false} comments={value.comments}>
Config: {moduleName}.{declName}
</PanelHeader>
<div className='text-sm my-4'>
Type: <TypeEl t={value.type} />
</div>
</div>
)
}
25 changes: 11 additions & 14 deletions frontend/console/src/features/modules/decls/DataPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,18 @@
import { Badge } from '../../../components/Badge'
import type { Data } from '../../../protos/xyz/block/ftl/v1/schema/schema_pb'
import { PanelHeader } from './PanelHeader'
import { TypeEl } from './TypeEl'

export const DataPanel = ({ value, moduleName, declName }: { value: Data; moduleName: string; declName: string }) => {
const maybeTypeParams = value.typeParameters.length === 0 ? '' : `<${value.typeParameters.map((p) => p.name).join(', ')}>`
return (
<div className='flex-1 py-2 px-4'>
{value.export ? (
<div>
<Badge name='Exported' />
</div>
) : (
[]
)}
<div className='inline-block mr-3 align-middle'>
<p>
data: {moduleName}.{declName}
</p>
{value.comments.length > 0 ? <p className='text-xs my-1'>{value.comments}</p> : []}
<div className='py-2 px-4'>
<PanelHeader exported={value.export} comments={value.comments}>
data: {moduleName}.{declName}
{maybeTypeParams}
</PanelHeader>
{value.fields.length === 0 || <div className='mt-8 mb-3'>Fields</div>}
<div className='text-xs font-mono inline-grid grid-cols-2 gap-x-4 gap-y-2' style={{ gridTemplateColumns: 'auto auto' }}>
{value.fields.map((f, i) => [<span key={`field-name-${i}`}>{f.name}</span>, <TypeEl key={`field-type-${i}`} t={f.type} />])}
</div>
</div>
)
Expand Down
13 changes: 13 additions & 0 deletions frontend/console/src/features/modules/decls/DatabasePanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type { Database } from '../../../protos/xyz/block/ftl/v1/schema/schema_pb'
import { PanelHeader } from './PanelHeader'

export const DatabasePanel = ({ value, moduleName, declName }: { value: Database; moduleName: string; declName: string }) => {
return (
<div className='py-2 px-4'>
<PanelHeader exported={false} comments={value.comments}>
Database: {moduleName}.{declName}
</PanelHeader>
<div className='text-sm my-4'>Type: {value.type}</div>
</div>
)
}
31 changes: 31 additions & 0 deletions frontend/console/src/features/modules/decls/DeclLink.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { useMemo } from 'react'
import { Link } from 'react-router-dom'
import { useSchema } from '../../../api/schema/use-schema'
import type { PullSchemaResponse } from '../../../protos/xyz/block/ftl/v1/ftl_pb.ts'

export const DeclLink = ({ moduleName, declName }: { moduleName?: string; declName: string }) => {
const schema = useSchema()
const decl = useMemo(() => {
const modules = (schema?.data || []) as PullSchemaResponse[]
const module = modules.find((m: PullSchemaResponse) => m.moduleName === moduleName)
if (!module?.schema) {
return
}
return module.schema.decls.find((d) => d.value.value?.name === declName)
}, [moduleName, declName, schema?.data])

const str = moduleName ? `${moduleName}.${declName}` : declName
Comment on lines +7 to +17
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've been personally finding that it's easier to make "dumb" components and feed the data into them, vs. having to have useSchema type hooks in the smaller sub components. This also makes them much easier to write storybook stories for since they don't have any real dependencies. Whatcha think?

Copy link
Contributor Author

@deniseli deniseli Sep 5, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Normally I follow that pattern too, but in this case, DeclLink is used by TypeEl, which in turn is used by most of the decl-type panels. We'd end up having to thread it through all the way, which would make the props for all of those components more complicated just for only DeclLink to actually use the contents. It gets worse from there, because DeclLink is probably going to end up all over the console once we're through, at which point we're basically passing schema down through nearly every single component in the tree. That would almost completely defeat the purpose of the hook pattern to begin with. :/

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome! Yeah, definitely seems easier to keep this here then. Thanks for the clarification!


if (!decl) {
return str
}

return (
<Link
className='rounded-md cursor-pointer text-indigo-600 dark:text-indigo-400 hover:bg-gray-100 hover:dark:bg-gray-700 p-1 -m-1'
to={`/modules/${moduleName}/${decl.value.case}/${declName}`}
>
{str}
</Link>
)
}
19 changes: 17 additions & 2 deletions frontend/console/src/features/modules/decls/DeclPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,40 @@ import { useParams } from 'react-router-dom'
import { useSchema } from '../../../api/schema/use-schema'
import { VerbPage } from '../../verbs/VerbPage'
import { declFromSchema } from '../module.utils'
import { ConfigPanel } from './ConfigPanel'
import { DataPanel } from './DataPanel'
import { DatabasePanel } from './DatabasePanel'
import { EnumPanel } from './EnumPanel'
import { SecretPanel } from './SecretPanel'
import { TypeAliasPanel } from './TypeAliasPanel'

export const DeclPanel = () => {
const { moduleName, declCase, declName } = useParams()
if (!moduleName || !declName) {
// Should be impossible, but validate anyway for type safety
return []
return
}

const schema = useSchema()
const decl = useMemo(() => declFromSchema(moduleName, declName, schema?.data || []), [schema?.data, moduleName, declCase, declName])
if (!decl) {
return []
return
}

const nameProps = { moduleName, declName }
switch (decl.value.case) {
case 'config':
return <ConfigPanel value={decl.value.value} {...nameProps} />
case 'data':
return <DataPanel value={decl.value.value} {...nameProps} />
case 'database':
return <DatabasePanel value={decl.value.value} {...nameProps} />
case 'enum':
return <EnumPanel value={decl.value.value} {...nameProps} />
case 'secret':
return <SecretPanel value={decl.value.value} {...nameProps} />
case 'typeAlias':
return <TypeAliasPanel value={decl.value.value} {...nameProps} />
case 'verb':
return <VerbPage {...nameProps} />
}
Expand Down
75 changes: 75 additions & 0 deletions frontend/console/src/features/modules/decls/EnumPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import type { Enum, Type, Value } from '../../../protos/xyz/block/ftl/v1/schema/schema_pb'
import { classNames } from '../../../utils'
import { PanelHeader } from './PanelHeader'
import { TypeEl } from './TypeEl'

const VariantComments = ({ comments, fullRow }: { comments?: string[]; fullRow?: boolean }) => {
if (!comments) {
return
}
return comments.map((c, i) => (
<div key={i} className={classNames('text-gray-500 dark:text-gray-400 mb-0.5', fullRow ? 'col-start-1 col-end-3' : '')}>
{c}
</div>
))
}

const VariantValue = ({ name, value }: { name?: string; value?: Value }) => {
const v = value?.value.value?.value
if (v === undefined) {
return
}
const valueText = value?.value.case === 'intValue' ? v.toString() : `"${v}"`
return (
<div className='mb-3'>
{name && `${name} = `}
{valueText}
</div>
)
}

const VariantNameAndType = ({ name, t }: { name: string; t: Type }) => {
return [
<span key='n' className='mb-3'>
{name}
</span>,
<TypeEl key='t' t={t} />,
]
}

const ValueEnumVariants = ({ value }: { value: Enum }) => {
return value.variants.map((v) => [<VariantComments key='c' comments={v.comments} />, <VariantValue key='v' name={v.name} value={v.value} />])
}

const TypeEnumVariants = ({ value }: { value: Enum }) => {
return (
<div className='inline-grid grid-cols-2 gap-x-4' style={{ gridTemplateColumns: 'auto auto' }}>
{value.variants.map((v) => [
<VariantComments key='c' fullRow comments={v.comments} />,
<VariantNameAndType key='n' name={v.name} t={v.value?.value.value?.value as Type} />,
])}
</div>
)
}

function enumType(value: Enum): string {
if (!value.type) {
return 'Type'
}
return value.type.value.case === 'string' ? 'String' : 'Int'
}

export const EnumPanel = ({ value, moduleName, declName }: { value: Enum; moduleName: string; declName: string }) => {
const isValueEnum = value.type !== undefined
return (
<div className='py-2 px-4'>
<PanelHeader exported={value.export} comments={value.comments}>
{enumType(value)} Enum: {moduleName}.{declName}
</PanelHeader>
<div className='mt-8'>
<div className='mb-2'>Variants</div>
<div className='text-xs font-mono'>{isValueEnum ? <ValueEnumVariants value={value} /> : <TypeEnumVariants value={value} />}</div>
</div>
</div>
)
}
16 changes: 16 additions & 0 deletions frontend/console/src/features/modules/decls/PanelHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type { ReactNode } from 'react'
import { Badge } from '../../../components/Badge'

export const PanelHeader = ({ children, exported, comments }: { children?: ReactNode; exported: boolean; comments?: string[] }) => {
return (
<div className='flex-1'>
{exported && (
<div className='mb-2'>
<Badge name='Exported' />
</div>
)}
{children}
{comments && comments.length > 0 && <p className='text-xs my-1'>{comments}</p>}
</div>
)
}
16 changes: 16 additions & 0 deletions frontend/console/src/features/modules/decls/SecretPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type { Secret } from '../../../protos/xyz/block/ftl/v1/schema/schema_pb'
import { PanelHeader } from './PanelHeader'
import { TypeEl } from './TypeEl'

export const SecretPanel = ({ value, moduleName, declName }: { value: Secret; moduleName: string; declName: string }) => {
return (
<div className='py-2 px-4'>
<PanelHeader exported={false} comments={value.comments}>
Secret: {moduleName}.{declName}
</PanelHeader>
<div className='text-sm my-4'>
Type: <TypeEl t={value.type} />
</div>
</div>
)
}
16 changes: 16 additions & 0 deletions frontend/console/src/features/modules/decls/TypeAliasPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type { TypeAlias } from '../../../protos/xyz/block/ftl/v1/schema/schema_pb'
import { PanelHeader } from './PanelHeader'
import { TypeEl } from './TypeEl'

export const TypeAliasPanel = ({ value, moduleName, declName }: { value: TypeAlias; moduleName: string; declName: string }) => {
return (
<div className='py-2 px-4'>
<PanelHeader exported={value.export} comments={value.comments}>
Type Alias: {moduleName}.{declName}
</PanelHeader>
<div className='text-sm my-4'>
Underlying type: <TypeEl t={value.type} />
</div>
</div>
)
}
63 changes: 63 additions & 0 deletions frontend/console/src/features/modules/decls/TypeEl.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import type { Optional, Ref, Array as SchArray, Map as SchMap, Type } from '../../../protos/xyz/block/ftl/v1/schema/schema_pb'
import { DeclLink } from './DeclLink'

// TypeParams ironically is built to work with the `Type` message, not the
// `TypeParameter` message, which just has a simple string param name without
// any higher level type information.
const TypeParams = ({ types }: { types?: (Type | undefined)[] }) => {
const definedTypes = types?.filter((t) => t !== undefined)
if (!definedTypes || definedTypes.length === 0) {
return
}
return (
<span>
<span>{'<'}</span>
{definedTypes.map((t, i) => [<TypeEl key='t' t={t} />, i === definedTypes.length - 1 ? '' : ', '])}
<span>{'>'}</span>
</span>
)
}

export const TypeEl = ({ t }: { t?: Type }) => {
if (!t) {
return ''
}

const v = t.value.value
if (!v) {
return t.value.case
}

switch (t.value.case) {
case 'array':
return (
<span>
array
<TypeParams types={[(v as SchArray).element]} />
</span>
)
case 'map':
return (
<span>
map
<TypeParams types={[(v as SchMap).key, (v as SchMap).value]} />
</span>
)
case 'optional':
return (
<span>
optional
<TypeParams types={[(v as Optional).type]} />
</span>
)
case 'ref':
return (
<span>
<DeclLink moduleName={(v as Ref).module} declName={(v as Ref).name} />
<TypeParams types={(v as Ref).typeParameters} />
</span>
)
default:
return t.value.case || ''
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const ioBlockClassName =
'rounded-md inline-block align-middle w-40 bg-gray-200 dark:bg-gray-900 my-3 mr-3 py-1 px-2 hover:bg-gray-100 hover:cursor-pointer hover:dark:bg-gray-700'
const IOBlock = ({ heading, t }: { heading: string; t?: Type }) => {
if (!t) {
return []
return
}
if (t.value.case === 'ref') {
return <DataRef heading={heading} r={t.value.value} />
Expand Down
Loading