From bb0d48018fb5bc9c61376cfd4d39b904dc114275 Mon Sep 17 00:00:00 2001 From: Katia Aresti Date: Tue, 14 Jul 2026 16:17:21 +0200 Subject: [PATCH] [#752] Adds missing annotations --- src/app/ProtoSchema/CreateProtoSchema.tsx | 14 +- src/app/ProtoSchema/SchemaEditPage.tsx | 18 +- src/app/ProtoSchema/protoLanguage.ts | 272 ++++++++-------------- src/app/Welcome/Welcome.tsx | 4 + src/app/assets/languages/en.json | 17 ++ src/app/hooks/protobufHooks.ts | 13 ++ src/services/protobufService.ts | 20 ++ src/types/InfinispanTypes.ts | 4 + src/types/server.ts | 12 + 9 files changed, 191 insertions(+), 183 deletions(-) diff --git a/src/app/ProtoSchema/CreateProtoSchema.tsx b/src/app/ProtoSchema/CreateProtoSchema.tsx index aacd57c0d..995707e1d 100644 --- a/src/app/ProtoSchema/CreateProtoSchema.tsx +++ b/src/app/ProtoSchema/CreateProtoSchema.tsx @@ -20,7 +20,12 @@ import { AddCircleOIcon } from '@patternfly/react-icons'; import { PopoverHelp } from '@app/Common/PopoverHelp'; import { DARK, ThemeContext } from '@app/providers/ThemeProvider'; import { Language } from '@patternfly/react-code-editor'; -import { PROTO_LANGUAGE_ID, registerProtobufLanguage } from './protoLanguage'; +import { + PROTO_LANGUAGE_ID, + registerProtobufLanguage, + updateAnnotations, +} from './protoLanguage'; +import { useFetchProtobufAnnotations } from '@app/hooks/protobufHooks'; registerProtobufLanguage(); @@ -47,10 +52,17 @@ const CreateProtoSchema = (props: { const { t } = useTranslation(); const { theme } = useContext(ThemeContext); const { addAlert } = useApiAlert(); + const { annotations } = useFetchProtobufAnnotations(); const [error, setError] = useState(undefined); const [schemaName, setSchemaName] = useState(schemaNameInitialState); const [schema, setSchema] = useState(schemaInitialState); + React.useEffect(() => { + if (annotations.length > 0) { + updateAnnotations(annotations); + } + }, [annotations]); + const clearCreateProtoSchema = (createDone: boolean) => { setSchemaName(schemaNameInitialState); setSchema(schemaInitialState); diff --git a/src/app/ProtoSchema/SchemaEditPage.tsx b/src/app/ProtoSchema/SchemaEditPage.tsx index f00b39757..af5899234 100644 --- a/src/app/ProtoSchema/SchemaEditPage.tsx +++ b/src/app/ProtoSchema/SchemaEditPage.tsx @@ -29,12 +29,19 @@ import { CogIcon } from '@patternfly/react-icons'; import { CodeEditor, Language } from '@patternfly/react-code-editor'; import { useNavigate, useParams, Link } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; -import { useFetchProtobufSchemaDetailed } from '@app/hooks/protobufHooks'; +import { + useFetchProtobufAnnotations, + useFetchProtobufSchemaDetailed, +} from '@app/hooks/protobufHooks'; import { useApiAlert } from '@app/utils/useApiAlert'; import { ConsoleServices } from '@services/ConsoleServices'; import { DARK, ThemeContext } from '@app/providers/ThemeProvider'; import { DeleteSchema } from '@app/ProtoSchema/DeleteSchema'; -import { PROTO_LANGUAGE_ID, registerProtobufLanguage } from './protoLanguage'; +import { + PROTO_LANGUAGE_ID, + registerProtobufLanguage, + updateAnnotations, +} from './protoLanguage'; import { PageHeader } from '@patternfly/react-component-groups'; import { DataContainerBreadcrumb } from '@app/Common/DataContainerBreadcrumb'; import './SchemaEditPage.css'; @@ -49,7 +56,14 @@ const SchemaEditPage = () => { const { theme } = useContext(ThemeContext); const { schemaDetail, loading, error, reload } = useFetchProtobufSchemaDetailed(schemaName || ''); + const { annotations } = useFetchProtobufAnnotations(); const [editedContent, setEditedContent] = useState(''); + + useEffect(() => { + if (annotations.length > 0) { + updateAnnotations(annotations); + } + }, [annotations]); const [saving, setSaving] = useState(false); const [isDrawerExpanded, setIsDrawerExpanded] = useState(true); const [isActionsOpen, setIsActionsOpen] = useState(false); diff --git a/src/app/ProtoSchema/protoLanguage.ts b/src/app/ProtoSchema/protoLanguage.ts index 3062ccade..37e3a00f9 100644 --- a/src/app/ProtoSchema/protoLanguage.ts +++ b/src/app/ProtoSchema/protoLanguage.ts @@ -1,4 +1,5 @@ import * as monaco from 'monaco-editor'; +import i18n from 'i18next'; const PROTO_LANGUAGE_ID = 'protobuf'; @@ -26,7 +27,7 @@ const keywords = [ 'optional', 'required', 'repeated', - 'group' + 'group', ]; const typeKeywords = [ @@ -44,127 +45,14 @@ const typeKeywords = [ 'sfixed64', 'bool', 'string', - 'bytes' + 'bytes', ]; -// Infinispan indexing annotations with their parameters -const annotations: Record; doc: string; target: string }> = { - Indexed: { - params: { - index: 'string', - enabled: 'boolean', - keyEntity: 'string', - keyPropertyName: 'string', - keyIncludeDepth: 'int' - }, - doc: 'Mark a message for indexing', - target: 'message' - }, - Basic: { - params: { - name: 'string', - projectable: 'boolean', - sortable: 'boolean', - searchable: 'boolean', - aggregable: 'boolean', - indexNullAs: 'string' - }, - doc: 'Index a field with its native type', - target: 'field' - }, - Text: { - params: { - name: 'string', - analyzer: 'string', - searchAnalyzer: 'string', - norms: 'boolean', - projectable: 'boolean', - searchable: 'boolean' - }, - doc: 'Index a string field as full-text', - target: 'field' - }, - Keyword: { - params: { - name: 'string', - normalizer: 'string', - norms: 'boolean', - projectable: 'boolean', - sortable: 'boolean', - searchable: 'boolean', - aggregable: 'boolean', - indexNullAs: 'string' - }, - doc: 'Index a string field as a keyword (not analyzed)', - target: 'field' - }, - Decimal: { - params: { - name: 'string', - decimalScale: 'int', - projectable: 'boolean', - sortable: 'boolean', - searchable: 'boolean', - aggregable: 'boolean', - indexNullAs: 'string' - }, - doc: 'Index a numeric field as a scaled decimal', - target: 'field' - }, - Embedded: { - params: { - name: 'string', - includeDepth: 'int' - }, - doc: 'Index an embedded message field', - target: 'field' - }, - GeoPoint: { - params: { - fieldName: 'string', - projectable: 'boolean', - sortable: 'boolean' - }, - doc: 'Mark a message as a geo-point (requires @Latitude and @Longitude fields)', - target: 'message' - }, - GeoField: { - params: { - name: 'string', - projectable: 'boolean', - sortable: 'boolean' - }, - doc: 'Index a field containing a geo-point message', - target: 'field' - }, - Latitude: { - params: { - fieldName: 'string' - }, - doc: 'Mark a field as the latitude component of a geo-point', - target: 'field' - }, - Longitude: { - params: { - fieldName: 'string' - }, - doc: 'Mark a field as the longitude component of a geo-point', - target: 'field' - }, - Vector: { - params: { - name: 'string', - projectable: 'boolean', - searchable: 'boolean', - indexNullAs: 'string', - dimension: 'int', - beamWidth: 'int', - maxConnections: 'int' - }, - doc: 'Index a repeated field as a vector for similarity search', - target: 'field' - } -}; +let annotations: ProtoAnnotation[] = []; + +export function updateAnnotations(serverAnnotations: ProtoAnnotation[]) { + annotations = serverAnnotations; +} export function registerProtobufLanguage() { if (registered) return; @@ -176,37 +64,37 @@ export function registerProtobufLanguage() { monaco.languages.setLanguageConfiguration(PROTO_LANGUAGE_ID, { comments: { lineComment: '//', - blockComment: ['/*', '*/'] + blockComment: ['/*', '*/'], }, brackets: [ ['{', '}'], ['[', ']'], - ['(', ')'] + ['(', ')'], ], autoClosingPairs: [ { open: '{', close: '}' }, { open: '[', close: ']' }, { open: '(', close: ')' }, { open: '"', close: '"' }, - { open: "'", close: "'" } + { open: "'", close: "'" }, ], surroundingPairs: [ { open: '{', close: '}' }, { open: '[', close: ']' }, { open: '(', close: ')' }, { open: '"', close: '"' }, - { open: "'", close: "'" } + { open: "'", close: "'" }, ], folding: { markers: { start: /^\s*(message|enum|service|oneof|extend)\b/, - end: /^\s*\}/ - } + end: /^\s*\}/, + }, }, indentationRules: { increaseIndentPattern: /\{\s*$/, - decreaseIndentPattern: /^\s*\}/ - } + decreaseIndentPattern: /^\s*\}/, + }, }); // Syntax highlighting @@ -229,23 +117,23 @@ export function registerProtobufLanguage() { '@keywords': 'keyword', '@typeKeywords': 'type', '@constants': 'constant', - '@default': 'identifier' - } - } - ] + '@default': 'identifier', + }, + }, + ], ], comment: [ [/[^/*]+/, 'comment'], [/\*\//, 'comment', '@pop'], - [/[/*]/, 'comment'] + [/[/*]/, 'comment'], ], docComment: [ [/@[A-Z]\w*/, 'annotation'], [/[^/*@]+/, 'comment.doc'], [/\*\//, 'comment.doc', '@pop'], - [/[/*@]/, 'comment.doc'] - ] - } + [/[/*@]/, 'comment.doc'], + ], + }, }); // Auto-completion @@ -257,7 +145,7 @@ export function registerProtobufLanguage() { startLineNumber: position.lineNumber, endLineNumber: position.lineNumber, startColumn: word.startColumn, - endColumn: word.endColumn + endColumn: word.endColumn, }; // Check if we're inside a comment (look for /** before cursor without closing */) @@ -265,7 +153,7 @@ export function registerProtobufLanguage() { startLineNumber: 1, startColumn: 1, endLineNumber: position.lineNumber, - endColumn: position.column + endColumn: position.column, }); const lastDocOpen = textUntilPosition.lastIndexOf('/**'); const lastDocClose = textUntilPosition.lastIndexOf('*/'); @@ -280,27 +168,38 @@ export function registerProtobufLanguage() { // Suggest annotation names const annotationRange = { ...range, - startColumn: range.startColumn - 1 // include the @ + startColumn: range.startColumn - 1, // include the @ }; - const annotationSuggestions = Object.entries(annotations).map(([name, info]) => { - const paramList = Object.keys(info.params); - const hasParams = paramList.length > 0; + const annotationSuggestions = annotations.map((annot) => { + const attrNames = Object.keys(annot.attributes); + const hasAttrs = attrNames.length > 0; let insertText: string; - if (!hasParams) { - insertText = `@${name}`; + if (!hasAttrs) { + insertText = `@${annot.name}`; } else { - const paramSnippets = paramList.map((p, i) => `\${${i + 1}:${p} = }`).join(', '); - insertText = `@${name}(${paramSnippets})`; + const attrSnippets = attrNames + .map((a, i) => `\${${i + 1}:${a} = }`) + .join(', '); + insertText = `@${annot.name}(${attrSnippets})`; } + const targetLabel = annot.target + .map((t) => t.toLowerCase()) + .join('/'); + const key = `schemas.annotations.${annot.name}`; + const doc = i18n.exists(key) + ? i18n.t(key) + : i18n.t('schemas.annotations.unknown', { name: annot.name }); + return { - label: `@${name}`, + label: `@${annot.name}`, kind: monaco.languages.CompletionItemKind.Interface, insertText, - insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet, - documentation: `${info.doc} (${info.target}-level annotation)\n\nParameters: ${paramList.join(', ')}`, - range: annotationRange + insertTextRules: + monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet, + documentation: `${doc} — ${targetLabel}-level annotation\n\nAttributes: ${attrNames.join(', ')}`, + range: annotationRange, }; }); @@ -317,98 +216,111 @@ export function registerProtobufLanguage() { label: 'message', kind: monaco.languages.CompletionItemKind.Snippet, insertText: 'message ${1:Name} {\n\t$0\n}', - insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet, + insertTextRules: + monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet, documentation: 'Define a new message type', - range + range, }, { label: 'enum', kind: monaco.languages.CompletionItemKind.Snippet, insertText: 'enum ${1:Name} {\n\t${2:UNKNOWN} = 0;\n\t$0\n}', - insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet, + insertTextRules: + monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet, documentation: 'Define a new enum type', - range + range, }, { label: 'service', kind: monaco.languages.CompletionItemKind.Snippet, - insertText: 'service ${1:Name} {\n\trpc ${2:Method} (${3:Request}) returns (${4:Response});\n\t$0\n}', - insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet, + insertText: + 'service ${1:Name} {\n\trpc ${2:Method} (${3:Request}) returns (${4:Response});\n\t$0\n}', + insertTextRules: + monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet, documentation: 'Define a new service', - range + range, }, { label: 'rpc', kind: monaco.languages.CompletionItemKind.Snippet, insertText: 'rpc ${1:Method} (${2:Request}) returns (${3:Response});', - insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet, + insertTextRules: + monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet, documentation: 'Define an RPC method', - range + range, }, { label: 'oneof', kind: monaco.languages.CompletionItemKind.Snippet, insertText: 'oneof ${1:name} {\n\t$0\n}', - insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet, + insertTextRules: + monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet, documentation: 'Define a oneof field group', - range + range, }, { label: 'map', kind: monaco.languages.CompletionItemKind.Snippet, insertText: 'map<${1:string}, ${2:string}> ${3:field_name} = ${4:1};', - insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet, + insertTextRules: + monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet, documentation: 'Define a map field', - range + range, }, { label: 'syntax', kind: monaco.languages.CompletionItemKind.Snippet, insertText: 'syntax = "${1|proto2,proto3|}";', - insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet, + insertTextRules: + monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet, documentation: 'Set the protobuf syntax version', - range + range, }, { label: 'import', kind: monaco.languages.CompletionItemKind.Snippet, insertText: 'import "${1:filename}.proto";', - insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet, + insertTextRules: + monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet, documentation: 'Import another proto file', - range + range, }, { label: 'package', kind: monaco.languages.CompletionItemKind.Snippet, insertText: 'package ${1:name};', - insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet, + insertTextRules: + monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet, documentation: 'Declare the package namespace', - range + range, }, { label: 'repeated', kind: monaco.languages.CompletionItemKind.Snippet, insertText: 'repeated ${1:type} ${2:field_name} = ${3:1};', - insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet, + insertTextRules: + monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet, documentation: 'Define a repeated (list) field', - range + range, }, { label: 'optional', kind: monaco.languages.CompletionItemKind.Snippet, insertText: 'optional ${1:type} ${2:field_name} = ${3:1};', - insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet, + insertTextRules: + monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet, documentation: 'Define an optional field', - range + range, }, { label: 'reserved', kind: monaco.languages.CompletionItemKind.Snippet, insertText: 'reserved ${1:2, 15, 9 to 11};', - insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet, + insertTextRules: + monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet, documentation: 'Reserve field numbers or names', - range - } + range, + }, ]; const snippetLabels = new Set(snippets.map((s) => s.label)); @@ -419,20 +331,20 @@ export function registerProtobufLanguage() { label: kw, kind: monaco.languages.CompletionItemKind.Keyword, insertText: kw, - range + range, })); const typeSuggestions = typeKeywords.map((t) => ({ label: t, kind: monaco.languages.CompletionItemKind.TypeParameter, insertText: t, - range + range, })); return { - suggestions: [...snippets, ...keywordSuggestions, ...typeSuggestions] + suggestions: [...snippets, ...keywordSuggestions, ...typeSuggestions], }; - } + }, }); } diff --git a/src/app/Welcome/Welcome.tsx b/src/app/Welcome/Welcome.tsx index 96652048f..13d5b54e0 100644 --- a/src/app/Welcome/Welcome.tsx +++ b/src/app/Welcome/Welcome.tsx @@ -196,6 +196,7 @@ const Welcome = () => { selectableActions={{ onClickAction: () => console.log(`blog clicked`), selectableActionId: 'blog', + selectableActionAriaLabel: t('welcome-page.blog'), name: 'clickable-card', }} > @@ -221,6 +222,7 @@ const Welcome = () => { selectableActions={{ onClickAction: () => console.log(`connect clicked`), selectableActionId: 'connect', + selectableActionAriaLabel: t('welcome-page.download'), name: 'clickable-card', }} className={'card-heading'} @@ -247,6 +249,7 @@ const Welcome = () => { selectableActions={{ onClickAction: () => console.log(`servers clicked`), selectableActionId: 'servers', + selectableActionAriaLabel: t('welcome-page.learn-more'), name: 'clickable-card', }} className={'card-heading'} @@ -273,6 +276,7 @@ const Welcome = () => { selectableActions={{ onClickAction: () => console.log(`develop clicked`), selectableActionId: 'develop', + selectableActionAriaLabel: t('welcome-page.tutorials'), name: 'clickable-card', }} className={'card-heading'} diff --git a/src/app/assets/languages/en.json b/src/app/assets/languages/en.json index fb5445ca9..2d3d4b537 100644 --- a/src/app/assets/languages/en.json +++ b/src/app/assets/languages/en.json @@ -987,6 +987,23 @@ "dependent-caches-description": "These caches use this schema.", "no-dependent-caches": "No caches use this schema.", "info-panel-toggle": "Toggle info panel" + }, + "annotations": { + "Indexed": "Mark a message for indexing", + "Basic": "Index a field with its native type", + "Text": "Index a string field as full-text", + "Keyword": "Index a string field as a keyword (not analyzed)", + "Decimal": "Index a numeric field as a scaled decimal", + "Embedded": "Index an embedded message field", + "GeoPoint": "Mark a message as a geo-point (requires @Latitude and @Longitude fields)", + "GeoField": "Index a field containing a geo-point message", + "Latitude": "Mark a field as the latitude component of a geo-point", + "Longitude": "Mark a field as the longitude component of a geo-point", + "Vector": "Index a repeated field as a vector for similarity search", + "Field": "Legacy Hibernate Search field annotation", + "Analyzer": "Specify a custom analyzer", + "SortableField": "Legacy annotation to mark a field as sortable", + "unknown": "Documentation missing for annotation {{name}}" } }, "global-stats": { diff --git a/src/app/hooks/protobufHooks.ts b/src/app/hooks/protobufHooks.ts index 1d1143d59..85ec8e9bd 100644 --- a/src/app/hooks/protobufHooks.ts +++ b/src/app/hooks/protobufHooks.ts @@ -3,6 +3,19 @@ import { ConsoleServices } from '@services/ConsoleServices'; import { useApiAlert } from '@app/utils/useApiAlert'; import { useServiceCall } from '@app/hooks/useServiceCall'; +export function useFetchProtobufAnnotations() { + const { + data: annotations, + loading, + error, + } = useServiceCall( + () => ConsoleServices.protobuf().getAnnotations(), + [], + ); + + return { loading, error, annotations }; +} + export function useFetchProtobufTypes() { const { data: protobufTypes, diff --git a/src/services/protobufService.ts b/src/services/protobufService.ts index 923562887..64e568a6f 100644 --- a/src/services/protobufService.ts +++ b/src/services/protobufService.ts @@ -125,4 +125,24 @@ export class ProtobufService { filterInternalProtobufTypes(data), ); } + + /** + * Get registered protobuf indexing annotations + */ + public async getAnnotations(): Promise< + Either + > { + return this.utils.get( + this.endpointBase + '/meta/schemas/_annotations', + (data) => + data.map( + (a) => + { + name: a.name, + target: a.target, + attributes: a.attributes, + }, + ), + ); + } } diff --git a/src/types/InfinispanTypes.ts b/src/types/InfinispanTypes.ts index d8efcc047..87fb6e574 100644 --- a/src/types/InfinispanTypes.ts +++ b/src/types/InfinispanTypes.ts @@ -110,6 +110,8 @@ import type { ProtoError as _ProtoError, ProtoSchema as _ProtoSchema, ProtoSchemaDetail as _ProtoSchemaDetail, + ProtoAnnotation as _ProtoAnnotation, + ProtoAnnotationAttribute as _ProtoAnnotationAttribute, } from './server'; declare global { @@ -201,4 +203,6 @@ declare global { type ProtoError = _ProtoError; type ProtoSchema = _ProtoSchema; type ProtoSchemaDetail = _ProtoSchemaDetail; + type ProtoAnnotation = _ProtoAnnotation; + type ProtoAnnotationAttribute = _ProtoAnnotationAttribute; } diff --git a/src/types/server.ts b/src/types/server.ts index 5531e2ef6..cc6eb7333 100644 --- a/src/types/server.ts +++ b/src/types/server.ts @@ -24,3 +24,15 @@ export interface ProtoSchemaDetail { caches: string[]; error: ProtoError | null; } + +export interface ProtoAnnotationAttribute { + type: string; + defaultValue?: string; + allowedValues: string[]; +} + +export interface ProtoAnnotation { + name: string; + target: string[]; + attributes: Record; +}