From 21956b527957b12c1d4567b3c95380030f1d21f9 Mon Sep 17 00:00:00 2001 From: Denise Li Date: Thu, 5 Sep 2024 11:29:08 -0400 Subject: [PATCH] feat: new console pages for config, data, database, enum, secret, and typealias (#2617) Part 1 of https://github.com/TBD54566975/ftl/issues/2616 Adds (or significantly adds to) pages for the following decl types: * config * data * database * enum * secret * typealias Not yet added: FSM, topic, subscription Adds components for: * `DeclLink`: takes a ref and links to the page for that decl * `TypeEl`: renders an appropriate string for a given Type, with links if appropriate * `PanelHeader` renders the standard decl panel page header (e.g. comments, export badge) --- .../src/features/modules/ModulesTree.tsx | 31 ++++++-- .../features/modules/decls/ConfigPanel.tsx | 16 ++++ .../src/features/modules/decls/DataPanel.tsx | 25 +++---- .../features/modules/decls/DatabasePanel.tsx | 13 ++++ .../src/features/modules/decls/DeclLink.tsx | 31 ++++++++ .../src/features/modules/decls/DeclPanel.tsx | 19 ++++- .../src/features/modules/decls/EnumPanel.tsx | 75 +++++++++++++++++++ .../features/modules/decls/PanelHeader.tsx | 16 ++++ .../features/modules/decls/SecretPanel.tsx | 16 ++++ .../features/modules/decls/TypeAliasPanel.tsx | 16 ++++ .../src/features/modules/decls/TypeEl.tsx | 63 ++++++++++++++++ .../src/features/modules/decls/VerbPanel.tsx | 2 +- 12 files changed, 301 insertions(+), 22 deletions(-) create mode 100644 frontend/console/src/features/modules/decls/ConfigPanel.tsx create mode 100644 frontend/console/src/features/modules/decls/DatabasePanel.tsx create mode 100644 frontend/console/src/features/modules/decls/DeclLink.tsx create mode 100644 frontend/console/src/features/modules/decls/EnumPanel.tsx create mode 100644 frontend/console/src/features/modules/decls/PanelHeader.tsx create mode 100644 frontend/console/src/features/modules/decls/SecretPanel.tsx create mode 100644 frontend/console/src/features/modules/decls/TypeAliasPanel.tsx create mode 100644 frontend/console/src/features/modules/decls/TypeEl.tsx diff --git a/frontend/console/src/features/modules/ModulesTree.tsx b/frontend/console/src/features/modules/ModulesTree.tsx index 247278581f..3000802b2b 100644 --- a/frontend/console/src/features/modules/ModulesTree.tsx +++ b/frontend/console/src/features/modules/ModulesTree.tsx @@ -46,10 +46,24 @@ const DeclNode = ({ decl, href, isSelected }: { decl: Decl; href: string; isSele return [] } const navigate = useNavigate() + const declRef = useRef(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 (
  • moduleName === module.name, [moduleName, module.name]) - const selectedRef = useRef(null) - const refProp = isSelected ? { ref: selectedRef } : {} + const moduleRef = useRef(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 (
  • { + return ( +
    + + Config: {moduleName}.{declName} + +
    + Type: +
    +
    + ) +} diff --git a/frontend/console/src/features/modules/decls/DataPanel.tsx b/frontend/console/src/features/modules/decls/DataPanel.tsx index 191f5bcb0b..54c8401981 100644 --- a/frontend/console/src/features/modules/decls/DataPanel.tsx +++ b/frontend/console/src/features/modules/decls/DataPanel.tsx @@ -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 ( -
    - {value.export ? ( -
    - -
    - ) : ( - [] - )} -
    -

    - data: {moduleName}.{declName} -

    - {value.comments.length > 0 ?

    {value.comments}

    : []} +
    + + data: {moduleName}.{declName} + {maybeTypeParams} + + {value.fields.length === 0 ||
    Fields
    } +
    + {value.fields.map((f, i) => [{f.name}, ])}
    ) diff --git a/frontend/console/src/features/modules/decls/DatabasePanel.tsx b/frontend/console/src/features/modules/decls/DatabasePanel.tsx new file mode 100644 index 0000000000..2d4b31c805 --- /dev/null +++ b/frontend/console/src/features/modules/decls/DatabasePanel.tsx @@ -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 ( +
    + + Database: {moduleName}.{declName} + +
    Type: {value.type}
    +
    + ) +} diff --git a/frontend/console/src/features/modules/decls/DeclLink.tsx b/frontend/console/src/features/modules/decls/DeclLink.tsx new file mode 100644 index 0000000000..d1559b4260 --- /dev/null +++ b/frontend/console/src/features/modules/decls/DeclLink.tsx @@ -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 + + if (!decl) { + return str + } + + return ( + + {str} + + ) +} diff --git a/frontend/console/src/features/modules/decls/DeclPanel.tsx b/frontend/console/src/features/modules/decls/DeclPanel.tsx index 864efc5eda..ab2e07ef53 100644 --- a/frontend/console/src/features/modules/decls/DeclPanel.tsx +++ b/frontend/console/src/features/modules/decls/DeclPanel.tsx @@ -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 case 'data': return + case 'database': + return + case 'enum': + return + case 'secret': + return + case 'typeAlias': + return case 'verb': return } diff --git a/frontend/console/src/features/modules/decls/EnumPanel.tsx b/frontend/console/src/features/modules/decls/EnumPanel.tsx new file mode 100644 index 0000000000..c7920a1c04 --- /dev/null +++ b/frontend/console/src/features/modules/decls/EnumPanel.tsx @@ -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) => ( +
    + {c} +
    + )) +} + +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 ( +
    + {name && `${name} = `} + {valueText} +
    + ) +} + +const VariantNameAndType = ({ name, t }: { name: string; t: Type }) => { + return [ + + {name} + , + , + ] +} + +const ValueEnumVariants = ({ value }: { value: Enum }) => { + return value.variants.map((v) => [, ]) +} + +const TypeEnumVariants = ({ value }: { value: Enum }) => { + return ( +
    + {value.variants.map((v) => [ + , + , + ])} +
    + ) +} + +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 ( +
    + + {enumType(value)} Enum: {moduleName}.{declName} + +
    +
    Variants
    +
    {isValueEnum ? : }
    +
    +
    + ) +} diff --git a/frontend/console/src/features/modules/decls/PanelHeader.tsx b/frontend/console/src/features/modules/decls/PanelHeader.tsx new file mode 100644 index 0000000000..435f0701c7 --- /dev/null +++ b/frontend/console/src/features/modules/decls/PanelHeader.tsx @@ -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 ( +
    + {exported && ( +
    + +
    + )} + {children} + {comments && comments.length > 0 &&

    {comments}

    } +
    + ) +} diff --git a/frontend/console/src/features/modules/decls/SecretPanel.tsx b/frontend/console/src/features/modules/decls/SecretPanel.tsx new file mode 100644 index 0000000000..61a5a9ee31 --- /dev/null +++ b/frontend/console/src/features/modules/decls/SecretPanel.tsx @@ -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 ( +
    + + Secret: {moduleName}.{declName} + +
    + Type: +
    +
    + ) +} diff --git a/frontend/console/src/features/modules/decls/TypeAliasPanel.tsx b/frontend/console/src/features/modules/decls/TypeAliasPanel.tsx new file mode 100644 index 0000000000..a9404bb0f9 --- /dev/null +++ b/frontend/console/src/features/modules/decls/TypeAliasPanel.tsx @@ -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 ( +
    + + Type Alias: {moduleName}.{declName} + +
    + Underlying type: +
    +
    + ) +} diff --git a/frontend/console/src/features/modules/decls/TypeEl.tsx b/frontend/console/src/features/modules/decls/TypeEl.tsx new file mode 100644 index 0000000000..fb1c9acc8d --- /dev/null +++ b/frontend/console/src/features/modules/decls/TypeEl.tsx @@ -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 ( + + {'<'} + {definedTypes.map((t, i) => [, i === definedTypes.length - 1 ? '' : ', '])} + {'>'} + + ) +} + +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 ( + + array + + + ) + case 'map': + return ( + + map + + + ) + case 'optional': + return ( + + optional + + + ) + case 'ref': + return ( + + + + + ) + default: + return t.value.case || '' + } +} diff --git a/frontend/console/src/features/modules/decls/VerbPanel.tsx b/frontend/console/src/features/modules/decls/VerbPanel.tsx index b01f3f66c4..42f94b86aa 100644 --- a/frontend/console/src/features/modules/decls/VerbPanel.tsx +++ b/frontend/console/src/features/modules/decls/VerbPanel.tsx @@ -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