-
+
+
))
- return
{lines}
+ return
{lines}
}
diff --git a/frontend/console/src/features/modules/schema/UnderlyingType.tsx b/frontend/console/src/features/modules/schema/UnderlyingType.tsx
index c187ad76d2..ee912ffc51 100644
--- a/frontend/console/src/features/modules/schema/UnderlyingType.tsx
+++ b/frontend/console/src/features/modules/schema/UnderlyingType.tsx
@@ -1,11 +1,11 @@
import { DeclLink } from '../decls/DeclLink'
-export const UnderlyingType = ({ token }: { token: string }) => {
+export const UnderlyingType = ({ token, containerRect }: { token: string; containerRect?: DOMRect }) => {
if (token.match(/^\[.+\]$/)) {
// Handles lists: [elementType]
return (
- []
+ []
)
}
@@ -15,7 +15,7 @@ export const UnderlyingType = ({ token }: { token: string }) => {
return (
{'{'}
- :
+ :
)
}
@@ -24,7 +24,7 @@ export const UnderlyingType = ({ token }: { token: string }) => {
// Handles last token of map: {KeyType: ValueType}
return (
-
+
{'}'}
)
@@ -34,7 +34,7 @@ export const UnderlyingType = ({ token }: { token: string }) => {
// Handles optional: elementType?
return (
- ?
+ ?
)
}
@@ -43,7 +43,7 @@ export const UnderlyingType = ({ token }: { token: string }) => {
// Handles closing parens in param list of verb signature: verb echo(inputType) outputType
return (
- )
+ )
)
}
@@ -57,7 +57,12 @@ export const UnderlyingType = ({ token }: { token: string }) => {
const declName = maybeSplitRef[1].split('<')[0]
const primaryTypeEl = (
- ]/)[0]} textColors='font-bold text-green-700 dark:text-green-400' />
+ ]/)[0]}
+ textColors='font-bold text-green-700 dark:text-green-400'
+ containerRect={containerRect}
+ />
{[',', '>'].includes(declName.slice(-1)) ? declName.slice(-1) : ''}
)
@@ -69,7 +74,10 @@ export const UnderlyingType = ({ token }: { token: string }) => {
{primaryTypeEl}
{'<'}
-
+
)
}
diff --git a/frontend/console/src/features/modules/schema/schema.utils.ts b/frontend/console/src/features/modules/schema/schema.utils.ts
index 11a140af5c..0e7b6591d6 100644
--- a/frontend/console/src/features/modules/schema/schema.utils.ts
+++ b/frontend/console/src/features/modules/schema/schema.utils.ts
@@ -1,30 +1,126 @@
+import type { UseQueryResult } from '@tanstack/react-query'
+import type { GetModulesResponse } from '../../../protos/xyz/block/ftl/v1/console/console_pb'
+
export const commentPrefix = ' //'
export const staticKeywords = ['module', 'export']
export const declTypes = ['config', 'data', 'database', 'enum', 'fsm', 'topic', 'typealias', 'secret', 'subscription', 'verb']
+// Keep these in sync with backend/schema/module.go#L86-L95
+const skipNewLineDeclTypes = ['config', 'secret', 'database', 'topic', 'subscription']
+const skipGapAfterTypes: { [key: string]: string[] } = {
+ secret: ['config'],
+ subscription: ['topic'],
+}
+
export const specialChars = ['{', '}', '=']
-export function isFirstLineOfBlock(ll: string[], i: number): boolean {
+export function shouldAddLeadingSpace(lines: string[], i: number): boolean {
+ if (!isFirstLineOfBlock(lines, i)) {
+ return false
+ }
+
+ for (const j in skipNewLineDeclTypes) {
+ if (declTypeAndPriorLineMatch(lines, i, skipNewLineDeclTypes[j], skipNewLineDeclTypes[j])) {
+ return false
+ }
+ }
+
+ for (const declType in skipGapAfterTypes) {
+ for (const j in skipGapAfterTypes[declType]) {
+ if (declTypeAndPriorLineMatch(lines, i, declType, skipGapAfterTypes[declType][j])) {
+ return false
+ }
+ }
+ }
+
+ return true
+}
+
+function declTypeAndPriorLineMatch(lines: string[], i: number, declType: string, priorDeclType: string): boolean {
+ if (i === 0 || lines.length === 1) {
+ return false
+ }
+ return regexForDeclType(declType).exec(lines[i]) !== null && regexForDeclType(priorDeclType).exec(lines[i - 1]) !== null
+}
+
+function regexForDeclType(declType: string) {
+ return new RegExp(`^ (export )?${declType} \\w+`)
+}
+
+function isFirstLineOfBlock(lines: string[], i: number): boolean {
if (i === 0) {
// Never add space for the first block
return false
}
- if (ll[i].startsWith(' ')) {
+ if (lines[i].startsWith(' ')) {
// Never add space for nested lines
return false
}
- if (ll[i - 1].startsWith(commentPrefix)) {
+ if (lines[i - 1].startsWith(commentPrefix)) {
// Prior line is a comment
return false
}
- if (ll[i].startsWith(commentPrefix)) {
+ if (lines[i].startsWith(commentPrefix)) {
return true
}
- const tokens = ll[i].trim().split(' ')
+ const tokens = lines[i].trim().split(' ')
if (!tokens || tokens.length === 0) {
return false
}
return staticKeywords.includes(tokens[0]) || declTypes.includes(tokens[0])
}
+
+export interface DeclSchema {
+ schema: string
+ declType: string
+}
+
+export function declFromModules(moduleName: string, declName: string, modules: UseQueryResult
) {
+ if (!modules.isSuccess || modules.data.modules.length === 0) {
+ return
+ }
+ const module = modules.data.modules.find((module) => module.name === moduleName)
+ if (!module?.schema) {
+ return
+ }
+ return declFromModuleSchemaString(declName, module.schema)
+}
+
+export function declFromModuleSchemaString(declName: string, schema: string) {
+ const lines = schema.split('\n')
+ const foundIdx = lines.findIndex((line) => {
+ const regex = new RegExp(`^ (export )?\\w+ ${declName}`)
+ return line.match(regex)
+ })
+
+ if (foundIdx === -1) {
+ return
+ }
+
+ const line = lines[foundIdx]
+ let out = line
+ let subLineIdx = foundIdx + 1
+ while (subLineIdx < lines.length && lines[subLineIdx].startsWith(' ')) {
+ out += `\n${lines[subLineIdx]}`
+ subLineIdx++
+ }
+ // Check for closing parens
+ if (subLineIdx < lines.length && line.endsWith('{') && lines[subLineIdx] === ' }') {
+ out += '\n }'
+ }
+
+ // Scan backwards for comments
+ subLineIdx = foundIdx - 1
+ while (subLineIdx >= 0 && lines[subLineIdx].startsWith(commentPrefix)) {
+ out = `${lines[subLineIdx]}\n${out}}`
+ subLineIdx--
+ }
+
+ const regexExecd = new RegExp(` (\\w+) ${declName}`).exec(line)
+ return {
+ schema: out,
+ declType: regexExecd ? regexExecd[1] : '',
+ }
+}