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: show schema snippets on hover of decl links in the console #2706

Merged
merged 2 commits into from
Sep 17, 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
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'
}
Loading