Skip to content

Commit

Permalink
feat: show schema snippets on hover of decl links in the console (#2706)
Browse files Browse the repository at this point in the history
Fixes #2673

This only adds the snippet implementations for data, enum, and
typealias, since those are the types that currently would actually come
up as a link in a decl panel.

This also changes the static decl panel bodies for enum + data to follow
the actual FTL schema format.

<img width="563" alt="Screenshot 2024-09-17 at 3 32 26 PM"
src="https://github.com/user-attachments/assets/e3763141-081f-4d6f-8d28-775f5b2fbc57">
<img width="393" alt="Screenshot 2024-09-17 at 3 33 01 PM"
src="https://github.com/user-attachments/assets/f21029e2-91a0-4b03-b9d9-97bd43c230b3">
  • Loading branch information
deniseli authored Sep 17, 2024
1 parent c19246a commit 8c0526b
Show file tree
Hide file tree
Showing 11 changed files with 179 additions and 81 deletions.
7 changes: 2 additions & 5 deletions frontend/console/src/features/modules/decls/DataPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { Data } from '../../../protos/xyz/block/ftl/v1/schema/schema_pb'
import { DataSnippet } from './DataSnippet'
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(', ')}>`
Expand All @@ -10,10 +10,7 @@ export const DataPanel = ({ value, moduleName, declName }: { value: Data; module
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>
<DataSnippet value={value} />
</div>
)
}
22 changes: 22 additions & 0 deletions frontend/console/src/features/modules/decls/DataSnippet.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import type { Data } from '../../../protos/xyz/block/ftl/v1/schema/schema_pb'
import { TypeEl } from './TypeEl'

export const DataSnippet = ({ value }: { value: Data }) => {
const maybeTypeParams = value.typeParameters.length === 0 ? '' : `<${value.typeParameters.map((p) => p.name).join(', ')}>`
return (
<div className='text-xs font-mono'>
<div>
{value.export ? 'export ' : ''}
data {value.name}
{maybeTypeParams}
{value.fields.length === 0 ? ' {}' : ' {'}
</div>
{value.fields.length === 0 || (
<div className='text-xs font-mono inline-grid grid-cols-2 gap-x-4 gap-y-2 ml-8 my-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>{value.fields.length === 0 ? '' : '}'}</div>
</div>
)
}
33 changes: 27 additions & 6 deletions frontend/console/src/features/modules/decls/DeclLink.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,26 @@
import { useMemo } from 'react'
import { Link } from 'react-router-dom'
import { useMemo, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { useSchema } from '../../../api/schema/use-schema'
import type { PullSchemaResponse } from '../../../protos/xyz/block/ftl/v1/ftl_pb.ts'
import type { Decl } from '../../../protos/xyz/block/ftl/v1/schema/schema_pb'
import { DeclSnippet } from './DeclSnippet'

const SnippetContainer = ({ decl }: { decl: Decl }) => {
return (
<div className='absolute p-4 mt-4 -ml-1 rounded-md dark:bg-gray-700 dark:text-white text-xs'>
<div className='absolute -mt-7 dark:text-gray-700'>
<svg height='20' width='20'>
<title>triangle</title>
<polygon points='11,0 9,0 0,20 20,20' fill='currentColor' />
</svg>
</div>
<DeclSnippet decl={decl} />
</div>
)
}

export const DeclLink = ({ moduleName, declName }: { moduleName?: string; declName: string }) => {
const [isHovering, setIsHovering] = useState(false)
const schema = useSchema()
const decl = useMemo(() => {
const modules = (schema?.data || []) as PullSchemaResponse[]
Expand All @@ -20,12 +37,16 @@ export const DeclLink = ({ moduleName, declName }: { moduleName?: string; declNa
return str
}

const navigate = useNavigate()
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}`}
<span
className='inline-block rounded-md cursor-pointer text-indigo-600 dark:text-indigo-400 hover:bg-gray-100 hover:dark:bg-gray-700 p-1 -m-1'
onClick={() => navigate(`/modules/${moduleName}/${decl.value.case}/${declName}`)}
onMouseEnter={() => setIsHovering(true)}
onMouseLeave={() => setIsHovering(false)}
>
{str}
</Link>
{isHovering && <SnippetContainer decl={decl} />}
</span>
)
}
16 changes: 16 additions & 0 deletions frontend/console/src/features/modules/decls/DeclSnippet.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type { Decl } from '../../../protos/xyz/block/ftl/v1/schema/schema_pb'
import { DataSnippet } from './DataSnippet'
import { EnumSnippet } from './EnumSnippet'
import { TypeAliasSnippet } from './TypeAliasSnippet'

export const DeclSnippet = ({ decl }: { decl: Decl }) => {
switch (decl.value.case) {
case 'data':
return <DataSnippet value={decl.value.value} />
case 'enum':
return <EnumSnippet value={decl.value.value} />
case 'typeAlias':
return <TypeAliasSnippet value={decl.value.value} />
}
return <div>under construction: {decl.value.case}</div>
}
68 changes: 4 additions & 64 deletions frontend/console/src/features/modules/decls/EnumPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,75 +1,15 @@
import type { Enum, Type, Value } from '../../../protos/xyz/block/ftl/v1/schema/schema_pb'
import { classNames } from '../../../utils'
import type { Enum } from '../../../protos/xyz/block/ftl/v1/schema/schema_pb'
import { EnumSnippet } from './EnumSnippet'
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'
}
import { enumType } from './enum.utils'

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>
<EnumSnippet value={value} />
</div>
)
}
77 changes: 77 additions & 0 deletions frontend/console/src/features/modules/decls/EnumSnippet.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import type { Enum, Type, Value } from '../../../protos/xyz/block/ftl/v1/schema/schema_pb'
import { classNames } from '../../../utils'
import { TypeEl } from './TypeEl'
import { enumType } from './enum.utils'

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-2'>
{name && `${name} = `}
{valueText}
</div>
)
}

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

const ValueEnumSnippet = ({ value }: { value: Enum }) => {
return (
<div>
<div>
{value.export ? 'export ' : ''}
enum {value.name}: {`${enumType(value)} {`}
</div>
<div className='my-2 ml-8'>
{value.variants.map((v) => [<VariantComments key='c' comments={v.comments} />, <VariantValue key='v' name={v.name} value={v.value} />])}
</div>
<div>{'}'}</div>
</div>
)
}

const TypeEnumSnippet = ({ value }: { value: Enum }) => {
return (
<div>
<div>
{value.export ? 'export ' : ''}
enum {value.name}
{' {'}
</div>
<div className='inline-grid grid-cols-2 gap-x-4 mt-2 ml-8' 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>
<div>{'}'}</div>
</div>
)
}

export const EnumSnippet = ({ value }: { value: Enum }) => {
const isValueEnum = value.type !== undefined
return <div className='text-xs font-mono'>{isValueEnum ? <ValueEnumSnippet value={value} /> : <TypeEnumSnippet value={value} />}</div>
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Badge } from '../../../components/Badge'

export const PanelHeader = ({ children, exported, comments }: { children?: ReactNode; exported: boolean; comments?: string[] }) => {
return (
<div className='flex-1'>
<div className='flex-1 mb-8'>
{exported && (
<div className='mb-2'>
<Badge name='Exported' />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
import type { TypeAlias } from '../../../protos/xyz/block/ftl/v1/schema/schema_pb'
import { PanelHeader } from './PanelHeader'
import { TypeEl } from './TypeEl'
import { TypeAliasSnippet } from './TypeAliasSnippet'

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>
<TypeAliasSnippet value={value} />
</div>
)
}
11 changes: 11 additions & 0 deletions frontend/console/src/features/modules/decls/TypeAliasSnippet.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import type { TypeAlias } from '../../../protos/xyz/block/ftl/v1/schema/schema_pb'
import { TypeEl } from './TypeEl'

export const TypeAliasSnippet = ({ value }: { value: TypeAlias }) => {
return (
<div className='font-mono text-xs'>
{value.export ? 'export ' : ''}
typealias {value.name} <TypeEl t={value.type} />
</div>
)
}
10 changes: 9 additions & 1 deletion frontend/console/src/features/modules/decls/TypeEl.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export const RefLink = ({ r }: { r?: Ref }) => {
)
}

export const TypeEl = ({ t }: { t?: Type }) => {
export const TypeElContents = ({ t }: { t?: Type }) => {
if (!t) {
return ''
}
Expand Down Expand Up @@ -68,3 +68,11 @@ export const TypeEl = ({ t }: { t?: Type }) => {
return t.value.case || ''
}
}

export const TypeEl = ({ t }: { t?: Type }) => {
return (
<span className='font-mono'>
<TypeElContents t={t} />
</span>
)
}
8 changes: 8 additions & 0 deletions frontend/console/src/features/modules/decls/enum.utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type { Enum } from '../../../protos/xyz/block/ftl/v1/schema/schema_pb'

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

0 comments on commit 8c0526b

Please sign in to comment.