From 2875315b6ef52c1a0c7cb4d02e83cd72daf2fcd9 Mon Sep 17 00:00:00 2001 From: Anchel135 Date: Mon, 27 Jan 2025 16:27:06 +0200 Subject: [PATCH 1/8] commit --- app/components/EditorComponent.tsx | 459 ++++++++++++----------------- 1 file changed, 188 insertions(+), 271 deletions(-) diff --git a/app/components/EditorComponent.tsx b/app/components/EditorComponent.tsx index da19d34e..59b483ec 100644 --- a/app/components/EditorComponent.tsx +++ b/app/components/EditorComponent.tsx @@ -7,21 +7,13 @@ import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog"; import { Editor, Monaco } from "@monaco-editor/react" import { useEffect, useRef, useState } from "react" import * as monaco from "monaco-editor"; -import { prepareArg, securedFetch } from "@/lib/utils"; import { Maximize2 } from "lucide-react"; -import { Session } from "next-auth"; +import { prepareArg, securedFetch } from "@/lib/utils"; import { useToast } from "@/components/ui/use-toast"; +import { Session } from "next-auth"; import { Graph } from "../api/graph/model"; import Button from "./ui/Button"; -type Suggestions = { - keywords: monaco.languages.CompletionItem[], - labels: monaco.languages.CompletionItem[], - relationshipTypes: monaco.languages.CompletionItem[], - propertyKeys: monaco.languages.CompletionItem[], - functions: monaco.languages.CompletionItem[] -} - interface Props { currentQuery: string historyQueries: string[] @@ -29,7 +21,6 @@ interface Props { maximize: boolean runQuery: (query: string) => void graph: Graph - isCollapsed: boolean data: Session | null } @@ -184,52 +175,59 @@ const FUNCTIONS = [ "vec.cosineDistance", ] +const SUGGESTIONS: monaco.languages.CompletionItem[] = KEYWORDS.map(key => ({ + insertText: key, + label: key, + kind: monaco.languages.CompletionItemKind.Keyword, + range: new monaco.Range(1, 1, 1, 1), + detail: "(keyword)" +})) + const MAX_HEIGHT = 20 const LINE_HEIGHT = 38 -const getEmptySuggestions = (): Suggestions => ({ - keywords: KEYWORDS.map(key => ({ - insertText: key, - label: key, - kind: monaco.languages.CompletionItemKind.Keyword, - range: new monaco.Range(1, 1, 1, 1), - detail: "(keyword)" - })), - labels: [], - relationshipTypes: [], - propertyKeys: [], - functions: [] -}) - const PLACEHOLDER = "Type your query here to start" -export default function EditorComponent({ currentQuery, historyQueries, setCurrentQuery, maximize, runQuery, graph, isCollapsed, data }: Props) { +export default function EditorComponent({ currentQuery, historyQueries, setCurrentQuery, maximize, runQuery, graph, data }: Props) { const [query, setQuery] = useState(currentQuery) const placeholderRef = useRef(null) const [monacoInstance, setMonacoInstance] = useState() - const [prevGraphName, setPrevGraphName] = useState("") const [sugProvider, setSugProvider] = useState() - const [suggestions, setSuggestions] = useState(getEmptySuggestions()) - const [lineNumber, setLineNumber] = useState(0) + const [lineNumber, setLineNumber] = useState(1) + const [blur, setBlur] = useState(false) + const { toast } = useToast() const submitQuery = useRef(null) const editorRef = useRef(null) - const [blur, setBlur] = useState(false) + const containerRef = useRef(null) const historyRef = useRef({ historyQueries, currentQuery, historyCounter: historyQueries.length }) - const { toast } = useToast() useEffect(() => { historyRef.current.historyQueries = historyQueries }, [historyQueries, currentQuery]) useEffect(() => { - if (!editorRef.current) return - editorRef.current.layout(); - }, [isCollapsed]) + if (!containerRef.current) return + + const handleResize = () => { + editorRef.current?.layout() + } + + window.addEventListener("resize", handleResize) + + const observer = new ResizeObserver(handleResize) + + observer.observe(containerRef.current) + + return () => { + window.removeEventListener("resize", handleResize) + observer.disconnect() + } + }, [containerRef.current]) useEffect(() => { setQuery(currentQuery) @@ -258,159 +256,76 @@ export default function EditorComponent({ currentQuery, historyQueries, setCurre }); } - const addSuggestions = (MonacoI: Monaco, sug?: monaco.languages.CompletionItem[], procedures?: monaco.languages.CompletionItem[]) => { - - sugProvider?.dispose() - const provider = MonacoI.languages.registerCompletionItemProvider('custom-language', { - triggerCharacters: ["."], - provideCompletionItems: (model, position) => { - const word = model.getWordUntilPosition(position) - const range = new monaco.Range(position.lineNumber, word.startColumn, position.lineNumber, word.endColumn) - - return { - suggestions: [ - ...(sug || []).map(s => ({ ...s, range })), - ...suggestions.keywords.map(s => ({ ...s, range })), - ...(procedures || []).map(s => ({ ...s, range, })) - ] + const fetchSuggestions = async (q: string, detail: string): Promise => { + const result = await securedFetch(`api/graph/${graph.Id}/?query=${prepareArg(q)}&role=${data?.user.role}`, { + method: 'GET', + }, toast) + + if (!result) return [] + + const json = await result.json() + + if (json.result.data.length === 0) return [] + + return json.result.data.map(({ sug }: { sug: string }) => ({ + insertTextRules: detail === '(function)' ? monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet : undefined, + insertText: detail === '(function)' ? `${sug}(\${0})` : sug, + label: detail === '(function)' ? `${sug}()` : sug, + kind: (() => { + switch (detail) { + case '(function)': + return monaco.languages.CompletionItemKind.Function; + case '(property key)': + return monaco.languages.CompletionItemKind.Property; + default: + return monaco.languages.CompletionItemKind.Variable; } - }, - }) - - setSugProvider(provider) - } - - const addLabelsSuggestions = async (sug: monaco.languages.CompletionItem[]) => { - const labelsQuery = `CALL db.labels()` - - await securedFetch(`api/graph/${prepareArg(graph.Id)}/?query=${prepareArg(labelsQuery)}&role=${data?.user.role}`, { - method: "GET" - }, toast).then((res) => res.json()).then((json) => { - json.result.data.forEach(({ label }: { label: string }) => { - sug.push({ - label, - kind: monaco.languages.CompletionItemKind.TypeParameter, - insertText: label, - range: new monaco.Range(1, 1, 1, 1), - detail: "(label)" - }) - }) - }) - } - - const addRelationshipTypesSuggestions = async (sug: monaco.languages.CompletionItem[]) => { - const relationshipTypeQuery = `CALL db.relationshipTypes()` - - await securedFetch(`api/graph/${prepareArg(graph.Id)}/?query=${prepareArg(relationshipTypeQuery)}&role=${data?.user.role}`, { - method: "GET" - }, toast).then((res) => res.json()).then((json) => { - json.result.data.forEach(({ relationshipType }: { relationshipType: string }) => { - sug.push({ - label: relationshipType, - kind: monaco.languages.CompletionItemKind.TypeParameter, - insertText: relationshipType, - range: new monaco.Range(1, 1, 1, 1), - detail: "(relationship type)" - }) - }) - }) - } - - const addPropertyKeysSuggestions = async (sug: monaco.languages.CompletionItem[]) => { - const propertyKeysQuery = `CALL db.propertyKeys()` - - await securedFetch(`api/graph/${prepareArg(graph.Id)}/?query=${prepareArg(propertyKeysQuery)}&role=${data?.user.role}`, { - method: "GET" - }, toast).then((res) => res.json()).then((json) => { - json.result.data.forEach(({ propertyKey }: { propertyKey: string }) => { - sug.push({ - label: propertyKey, - kind: monaco.languages.CompletionItemKind.Property, - insertText: propertyKey, - range: new monaco.Range(1, 1, 1, 1), - detail: "(property)" - }) - }) - }) + })(), + range: new monaco.Range(1, 1, 1, 1), + detail + })) } - const addFunctionsSuggestions = async (functions: monaco.languages.CompletionItem[]) => { - const proceduresQuery = `CALL dbms.procedures() YIELD name` - await securedFetch(`api/graph/${prepareArg(graph.Id)}/?query=${prepareArg(proceduresQuery)}&role=${data?.user.role}`, { - method: "GET" - }, toast).then((res) => res.json()).then((json) => { - [...json.result.data.map(({ name }: { name: string }) => name), ...FUNCTIONS].forEach((name: string) => { - functions.push({ - label: name, - kind: monaco.languages.CompletionItemKind.Function, - insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet, - insertText: `${name}($0)`, - range: new monaco.Range(1, 1, 1, 1), - detail: "(function)" - }) - }) - }) + const getSuggestions = async (): Promise => { + const suggestions = await Promise.all([ + ['CALL dbms.procedures() YIELD name as sug', '(function)'], + ['CALL db.propertyKeys() YIELD propertyKey as sug', '(property key)'], + ['CALL db.labels() YIELD label as sug', '(label)'], + ['CALL db.relationshipTypes() YIELD relationshipType as sug', '(relationship type)'] + ].map(([q, detail]) => fetchSuggestions(q, detail))) + + return [...suggestions.flatMap(arr => arr), ...FUNCTIONS.map(f => ({ + insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet, + insertText: `${f}(\${0})`, + label: `${f}()`, + kind: monaco.languages.CompletionItemKind.Function, + range: new monaco.Range(1, 1, 1, 1), + detail: "(function)" + }))] } - const getSuggestions = async (monacoI?: Monaco) => { + const addSuggestions = async (monacoI: Monaco) => { + console.log("addSuggestions"); + const suggestions = SUGGESTIONS - if (!graph.Id || (!monacoInstance && !monacoI)) return - const m = monacoI || monacoInstance - const sug: Suggestions = getEmptySuggestions() - - sugProvider?.dispose() - - await Promise.all([ - addLabelsSuggestions(sug.labels), - addRelationshipTypesSuggestions(sug.relationshipTypes), - addPropertyKeysSuggestions(sug.propertyKeys), - addFunctionsSuggestions(sug.functions) - ]) + if (graph.Id) { + console.log("getSuggestions"); + suggestions.push(...(await getSuggestions())) + } - const namespaces = new Map() + const functions = suggestions.filter(({ detail }) => detail === "(function)") - sug.functions.forEach(({ label }) => { - const names = (label as string).split(".") - names.forEach((name, i) => { - if (i === names.length - 1) return - namespaces.set(name, name) - }) - }) + const namespaces = new Set(functions.filter(({ label }) => (label as string).includes(".")).map(({ label }) => { + const [namespace] = (label as string).split(".") + return namespace + })) - m!.languages.setMonarchTokensProvider('custom-language', { - tokenizer: { - root: [ - [new RegExp(`\\b(${Array.from(namespaces.keys()).join('|')})\\b`), "keyword"], - [new RegExp(`\\b(${KEYWORDS.join('|')})\\b`), "keyword"], - [ - new RegExp(`\\b(${sug.functions.map(({ label }) => { - const labels = (label as string).split(".") - return labels[labels.length - 1] - }).join('|')})\\b`), - "function" - ], - [/"([^"\\]|\\.)*"/, 'string'], - [/'([^'\\]|\\.)*'/, 'string'], - [/\d+/, 'number'], - [/:(\w+)/, 'type'], - [/\{/, { token: 'delimiter.curly', next: '@bracketCounting' }], - [/\[/, { token: 'delimiter.square', next: '@bracketCounting' }], - [/\(/, { token: 'delimiter.parenthesis', next: '@bracketCounting' }], - ], - bracketCounting: [ - [/\{/, 'delimiter.curly', '@bracketCounting'], - [/\}/, 'delimiter.curly', '@pop'], - [/\[/, 'delimiter.square', '@bracketCounting'], - [/\]/, 'delimiter.square', '@pop'], - [/\(/, 'delimiter.parenthesis', '@bracketCounting'], - [/\)/, 'delimiter.parenthesis', '@pop'], - { include: 'root' } - ], - }, - ignoreCase: true, - }) + if (sugProvider) { + sugProvider.dispose() + console.log("dispose"); + } - m!.languages.setLanguageConfiguration('custom-language', { + monacoI.languages.setLanguageConfiguration('custom-language', { brackets: [ ['{', '}'], ['[', ']'], @@ -432,110 +347,109 @@ export default function EditorComponent({ currentQuery, historyQueries, setCurre ] }); - m!.editor.setTheme('custom-theme'); - - addSuggestions(m!, [...sug.labels, ...sug.propertyKeys, ...sug.relationshipTypes], sug.functions) - - setSuggestions(sug) - } - - useEffect(() => { - if (!graph || !monacoInstance || graph.Id !== prevGraphName) return setPrevGraphName(graph.Id) - - const run = async () => { - const sug: Suggestions = getEmptySuggestions() - if (graph.Metadata.length > 0) { - await Promise.all(graph.Metadata.map(async (meta: string) => { - if (meta.includes("Labels")) await addLabelsSuggestions(sug.labels) - if (meta.includes("RelationshipTypes")) await addRelationshipTypesSuggestions(sug.relationshipTypes) - if (meta.includes("PropertyKeys")) await addPropertyKeysSuggestions(sug.propertyKeys) - })) - } - Object.entries(sug).forEach(([key, value]) => { - if (value.length === 0) { - sug[key as keyof Suggestions] = suggestions[key as keyof Suggestions] - } + if (graph.Id) { + monacoI.languages.setMonarchTokensProvider('custom-language', { + tokenizer: { + root: [ + [new RegExp(`\\b(${Array.from(namespaces.keys()).join('|')})\\b`), "keyword"], + [new RegExp(`\\b(${KEYWORDS.join('|')})\\b`), "keyword"], + [ + new RegExp(`\\b(${functions.map(({ label }) => { + if ((label as string).includes(".")) { + const labels = (label as string).split(".") + return labels[labels.length - 1] + } + return label + }).join('|')})\\b`), + "function" + ], + [/"([^"\\]|\\.)*"/, 'string'], + [/'([^'\\]|\\.)*'/, 'string'], + [/\d+/, 'number'], + [/:(\w+)/, 'type'], + [/\{/, { token: 'delimiter.curly', next: '@bracketCounting' }], + [/\[/, { token: 'delimiter.square', next: '@bracketCounting' }], + [/\(/, { token: 'delimiter.parenthesis', next: '@bracketCounting' }], + ], + bracketCounting: [ + [/\{/, 'delimiter.curly', '@bracketCounting'], + [/\}/, 'delimiter.curly', '@pop'], + [/\[/, 'delimiter.square', '@bracketCounting'], + [/\]/, 'delimiter.square', '@pop'], + [/\(/, 'delimiter.parenthesis', '@bracketCounting'], + [/\)/, 'delimiter.parenthesis', '@pop'], + { include: 'root' } + ], + }, + ignoreCase: true, + }) + } else { + monacoI.languages.setMonarchTokensProvider('custom-language', { + tokenizer: { + root: [ + [new RegExp(`\\b(${KEYWORDS.join('|')})\\b`), "keyword"], + [/"([^"\\]|\\.)*"/, 'string'], + [/'([^'\\]|\\.)*'/, 'string'], + [/\d+/, 'number'], + [/:(\w+)/, 'type'], + [/\{/, { token: 'delimiter.curly', next: '@bracketCounting' }], + [/\[/, { token: 'delimiter.square', next: '@bracketCounting' }], + [/\(/, { token: 'delimiter.parenthesis', next: '@bracketCounting' }], + ], + bracketCounting: [ + [/\{/, 'delimiter.curly', '@bracketCounting'], + [/\}/, 'delimiter.curly', '@pop'], + [/\[/, 'delimiter.square', '@bracketCounting'], + [/\]/, 'delimiter.square', '@pop'], + [/\(/, 'delimiter.parenthesis', '@bracketCounting'], + [/\)/, 'delimiter.parenthesis', '@pop'], + { include: 'root' } + ], + }, + ignoreCase: true, }) - - addSuggestions(monacoInstance, [...sug.labels, ...sug.propertyKeys, ...sug.relationshipTypes], sug.functions) - - setSuggestions(sug) } - run() - }, [graph]) + return monacoI.languages.registerCompletionItemProvider("custom-language", { + provideCompletionItems: (model, position) => { + const word = model.getWordUntilPosition(position) + const range = new monaco.Range(position.lineNumber, word.startColumn, position.lineNumber, word.endColumn) + console.log(suggestions); + return { + suggestions: suggestions.map(s => ({ ...s, range })) + } + }, + }) + } useEffect(() => { - const interval = setInterval(() => { - getSuggestions() + const timeout = setTimeout(async () => { + if (!monacoInstance) return + const provider = await addSuggestions(monacoInstance) + setSugProvider(provider) }, 5000) return () => { - clearInterval(interval) - sugProvider?.dispose() + clearTimeout(timeout) + if (sugProvider) { + sugProvider.dispose() + console.log("cleanup dispose"); + } + console.log("cleanup"); } }, [graph.Id]) - const handleEditorWillMount = (monacoI: Monaco) => { - monacoI.languages.setMonarchTokensProvider('custom-language', { - tokenizer: { - root: [ - [new RegExp(`\\b(${KEYWORDS.join('|')})\\b`), "keyword"], - [/"([^"\\]|\\.)*"/, 'string'], - [/'([^'\\]|\\.)*'/, 'string'], - [/\d+/, 'number'], - [/:(\w+)/, 'type'], - [/\{/, { token: 'delimiter.curly', next: '@bracketCounting' }], - [/\[/, { token: 'delimiter.square', next: '@bracketCounting' }], - [/\(/, { token: 'delimiter.parenthesis', next: '@bracketCounting' }], - ], - bracketCounting: [ - [/\{/, 'delimiter.curly', '@bracketCounting'], - [/\}/, 'delimiter.curly', '@pop'], - [/\[/, 'delimiter.square', '@bracketCounting'], - [/\]/, 'delimiter.square', '@pop'], - [/\(/, 'delimiter.parenthesis', '@bracketCounting'], - [/\)/, 'delimiter.parenthesis', '@pop'], - { include: 'root' } - ], - }, - ignoreCase: true, - }); + const handleEditorWillMount = async (monacoI: Monaco) => { - monacoI.languages.register({ id: 'custom-language' }); - - monacoI.languages.setLanguageConfiguration('custom-language', { - brackets: [ - ['{', '}'], - ['[', ']'], - ['(', ')'] - ], - autoClosingPairs: [ - { open: '{', close: '}' }, - { open: '[', close: ']' }, - { open: '(', close: ')' }, - { open: '"', close: '"', notIn: ['string'] }, - { open: "'", close: "'", notIn: ['string', 'comment'] } - ], - surroundingPairs: [ - { open: '{', close: '}' }, - { open: '[', close: ']' }, - { open: '(', close: ')' }, - { open: '"', close: '"' }, - { open: "'", close: "'" } - ] - }); + monacoI.languages.register({ id: "custom-language" }) setTheme(monacoI) - monacoI.editor.setTheme('custom-theme'); + const provider = await addSuggestions(monacoI) - if (graph.Id) { - getSuggestions(monacoI) - } else { - addSuggestions(monacoI) - } - }; + setSugProvider(provider) + setMonacoInstance(monacoI) + } const handleEditorDidMount = (e: monaco.editor.IStandaloneCodeEditor, monacoI: Monaco) => { const updatePlaceholderVisibility = () => { @@ -619,10 +533,6 @@ export default function EditorComponent({ currentQuery, historyQueries, setCurre }); } - useEffect(() => { - setLineNumber(query.split("\n").length) - }, [query]) - return (
{ @@ -637,7 +547,7 @@ export default function EditorComponent({ currentQuery, historyQueries, setCurre runQuery(query) }} > -
+
document.body.clientHeight / 100 * MAX_HEIGHT ? document.body.clientHeight / 100 * MAX_HEIGHT : lineNumber * LINE_HEIGHT} @@ -647,7 +557,14 @@ export default function EditorComponent({ currentQuery, historyQueries, setCurre lineNumbers: lineNumber > 1 ? "on" : "off", }} value={(blur ? query.replace(/\s+/g, ' ').trim() : query)} - onChange={(val) => historyRef.current.historyCounter ? setQuery(val || "") : setCurrentQuery(val || "")} + onChange={(val) => { + if (historyRef.current.historyCounter) { + setQuery(val || ""); + } else { + setCurrentQuery(val || ""); + } + setLineNumber(val?.split("\n").length || 1); + }} theme="custom-theme" beforeMount={handleEditorWillMount} onMount={handleEditorDidMount} From d4fc6be6e635faa6554639d2a2fc5e9045b57ef2 Mon Sep 17 00:00:00 2001 From: Anchel135 Date: Mon, 27 Jan 2025 16:28:37 +0200 Subject: [PATCH 2/8] commit --- app/components/EditorComponent.tsx | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/app/components/EditorComponent.tsx b/app/components/EditorComponent.tsx index 59b483ec..2900c792 100644 --- a/app/components/EditorComponent.tsx +++ b/app/components/EditorComponent.tsx @@ -450,8 +450,10 @@ export default function EditorComponent({ currentQuery, historyQueries, setCurre setSugProvider(provider) setMonacoInstance(monacoI) } - + const handleEditorDidMount = (e: monaco.editor.IStandaloneCodeEditor, monacoI: Monaco) => { + editorRef.current = e + const updatePlaceholderVisibility = () => { const hasContent = !!e.getValue(); if (placeholderRef.current) { @@ -463,25 +465,18 @@ export default function EditorComponent({ currentQuery, historyQueries, setCurre if (placeholderRef.current) { placeholderRef.current.style.display = 'none'; } + setBlur(false) }); e.onDidBlurEditorText(() => { updatePlaceholderVisibility(); + setBlur(true) }); updatePlaceholderVisibility(); setMonacoInstance(monacoI) - editorRef.current = e - - e.onDidBlurEditorText(() => { - setBlur(true) - }) - - e.onDidFocusEditorText(() => { - setBlur(false) - }) const isFirstLine = e.createContextKey('isFirstLine', true); From 9ebc94e2fd1fc63a4e382d6dbe139a99f1e38e76 Mon Sep 17 00:00:00 2001 From: Anchel135 Date: Thu, 30 Jan 2025 12:13:36 +0200 Subject: [PATCH 3/8] fix suggestion duplication --- app/components/EditorComponent.tsx | 282 ++++++++++++++--------------- app/graph/GraphView.tsx | 1 - 2 files changed, 139 insertions(+), 144 deletions(-) diff --git a/app/components/EditorComponent.tsx b/app/components/EditorComponent.tsx index 2900c792..3785fd1d 100644 --- a/app/components/EditorComponent.tsx +++ b/app/components/EditorComponent.tsx @@ -3,7 +3,7 @@ "use client"; -import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog"; +import { Dialog, DialogContent, DialogDescription, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; import { Editor, Monaco } from "@monaco-editor/react" import { useEffect, useRef, useState } from "react" import * as monaco from "monaco-editor"; @@ -11,6 +11,7 @@ import { Maximize2 } from "lucide-react"; import { prepareArg, securedFetch } from "@/lib/utils"; import { useToast } from "@/components/ui/use-toast"; import { Session } from "next-auth"; +import { VisuallyHidden } from "@radix-ui/react-visually-hidden"; import { Graph } from "../api/graph/model"; import Button from "./ui/Button"; @@ -175,13 +176,23 @@ const FUNCTIONS = [ "vec.cosineDistance", ] -const SUGGESTIONS: monaco.languages.CompletionItem[] = KEYWORDS.map(key => ({ - insertText: key, - label: key, - kind: monaco.languages.CompletionItemKind.Keyword, - range: new monaco.Range(1, 1, 1, 1), - detail: "(keyword)" -})) +const SUGGESTIONS: monaco.languages.CompletionItem[] = [ + ...KEYWORDS.map(key => ({ + insertText: key, + label: key, + kind: monaco.languages.CompletionItemKind.Keyword, + range: new monaco.Range(1, 1, 1, 1), + detail: "(keyword)" + })), + ...FUNCTIONS.map(f => ({ + insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet, + insertText: `${f}(\${0})`, + label: `${f}()`, + kind: monaco.languages.CompletionItemKind.Function, + range: new monaco.Range(1, 1, 1, 1), + detail: "(function)" + })) +] const MAX_HEIGHT = 20 const LINE_HEIGHT = 38 @@ -192,10 +203,10 @@ export default function EditorComponent({ currentQuery, historyQueries, setCurre const [query, setQuery] = useState(currentQuery) const placeholderRef = useRef(null) - const [monacoInstance, setMonacoInstance] = useState() - const [sugProvider, setSugProvider] = useState() const [lineNumber, setLineNumber] = useState(1) + const graphIdRef = useRef(graph.Id) const [blur, setBlur] = useState(false) + const [sugDisposed, setSugDisposed] = useState() const { toast } = useToast() const submitQuery = useRef(null) const editorRef = useRef(null) @@ -206,6 +217,14 @@ export default function EditorComponent({ currentQuery, historyQueries, setCurre historyCounter: historyQueries.length }) + useEffect(() => { + graphIdRef.current = graph.Id + }, [graph.Id]) + + useEffect(() => () => { + sugDisposed?.dispose() + }, [sugDisposed]) + useEffect(() => { historyRef.current.historyQueries = historyQueries }, [historyQueries, currentQuery]) @@ -254,10 +273,12 @@ export default function EditorComponent({ currentQuery, historyQueries, setCurre 'editorSuggestWidget.hoverBackground': '#28283F', }, }); + + monacoI.editor.setTheme('custom-theme') } const fetchSuggestions = async (q: string, detail: string): Promise => { - const result = await securedFetch(`api/graph/${graph.Id}/?query=${prepareArg(q)}&role=${data?.user.role}`, { + const result = await securedFetch(`api/graph/${graphIdRef.current}/?query=${prepareArg(q)}&role=${data?.user.role}`, { method: 'GET', }, toast) @@ -286,44 +307,109 @@ export default function EditorComponent({ currentQuery, historyQueries, setCurre })) } - const getSuggestions = async (): Promise => { - const suggestions = await Promise.all([ - ['CALL dbms.procedures() YIELD name as sug', '(function)'], - ['CALL db.propertyKeys() YIELD propertyKey as sug', '(property key)'], - ['CALL db.labels() YIELD label as sug', '(label)'], - ['CALL db.relationshipTypes() YIELD relationshipType as sug', '(relationship type)'] - ].map(([q, detail]) => fetchSuggestions(q, detail))) - - return [...suggestions.flatMap(arr => arr), ...FUNCTIONS.map(f => ({ - insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet, - insertText: `${f}(\${0})`, - label: `${f}()`, - kind: monaco.languages.CompletionItemKind.Function, - range: new monaco.Range(1, 1, 1, 1), - detail: "(function)" - }))] - } + const getSuggestions = async () => (await Promise.all([ + fetchSuggestions('CALL dbms.procedures() YIELD name as sug', '(function)'), + fetchSuggestions('CALL db.propertyKeys() YIELD propertyKey as sug', '(property key)'), + fetchSuggestions('CALL db.labels() YIELD label as sug', '(label)'), + fetchSuggestions('CALL db.relationshipTypes() YIELD relationshipType as sug', '(relationship type)') + ])).flat() const addSuggestions = async (monacoI: Monaco) => { - console.log("addSuggestions"); - const suggestions = SUGGESTIONS + const sug = [ + ...SUGGESTIONS, + ...(graphIdRef.current ? await getSuggestions() : []) + ]; + + const functions = sug.filter(({ detail }) => detail === "(function)") + + const namespaces = new Set( + functions + .filter(({ label }) => (label as string).includes(".")) + .map(({ label }) => { + const newNamespaces = (label as string).split(".") + newNamespaces.pop() + return newNamespaces + }).flat() + ) + + monacoI.languages.setMonarchTokensProvider('custom-language', { + tokenizer: { + root: graphIdRef.current ? [ + [new RegExp(`\\b(${Array.from(namespaces.keys()).join('|')})\\b`), "keyword"], + [new RegExp(`\\b(${KEYWORDS.join('|')})\\b`), "keyword"], + [ + new RegExp(`\\b(${functions.map(({ label }) => { + if ((label as string).includes(".")) { + const labels = (label as string).split(".") + return labels[labels.length - 1] + } + return label + }).join('|')})\\b`), + "function" + ], + [/"([^"\\]|\\.)*"/, 'string'], + [/'([^'\\]|\\.)*'/, 'string'], + [/\d+/, 'number'], + [/:(\w+)/, 'type'], + [/\{/, { token: 'delimiter.curly', next: '@bracketCounting' }], + [/\[/, { token: 'delimiter.square', next: '@bracketCounting' }], + [/\(/, { token: 'delimiter.parenthesis', next: '@bracketCounting' }], + ] : [ + [new RegExp(`\\b(${KEYWORDS.join('|')})\\b`), "keyword"], + [/"([^"\\]|\\.)*"/, 'string'], + [/'([^'\\]|\\.)*'/, 'string'], + [/\d+/, 'number'], + [/:(\w+)/, 'type'], + [/\{/, { token: 'delimiter.curly', next: '@bracketCounting' }], + [/\[/, { token: 'delimiter.square', next: '@bracketCounting' }], + [/\(/, { token: 'delimiter.parenthesis', next: '@bracketCounting' }], + ], + bracketCounting: [ + [/\{/, 'delimiter.curly', '@bracketCounting'], + [/\}/, 'delimiter.curly', '@pop'], + [/\[/, 'delimiter.square', '@bracketCounting'], + [/\]/, 'delimiter.square', '@pop'], + [/\(/, 'delimiter.parenthesis', '@bracketCounting'], + [/\)/, 'delimiter.parenthesis', '@pop'], + { include: 'root' } + ], + }, + ignoreCase: true, + }) - if (graph.Id) { - console.log("getSuggestions"); - suggestions.push(...(await getSuggestions())) - } + return sug + } - const functions = suggestions.filter(({ detail }) => detail === "(function)") + const handleEditorWillMount = async (monacoI: Monaco) => { - const namespaces = new Set(functions.filter(({ label }) => (label as string).includes(".")).map(({ label }) => { - const [namespace] = (label as string).split(".") - return namespace - })) + monacoI.languages.register({ id: "custom-language" }) - if (sugProvider) { - sugProvider.dispose() - console.log("dispose"); - } + monacoI.languages.setMonarchTokensProvider('custom-language', { + tokenizer: { + root: [ + [new RegExp(`\\b(${KEYWORDS.join('|')})\\b`), "keyword"], + [/"([^"\\]|\\.)*"/, 'string'], + [/'([^'\\]|\\.)*'/, 'string'], + [/\d+/, 'number'], + [/:(\w+)/, 'type'], + [/\{/, { token: 'delimiter.curly', next: '@bracketCounting' }], + [/\[/, { token: 'delimiter.square', next: '@bracketCounting' }], + [/\(/, { token: 'delimiter.parenthesis', next: '@bracketCounting' }], + ], + bracketCounting: [ + [/\{/, 'delimiter.curly', '@bracketCounting'], + [/\}/, 'delimiter.curly', '@pop'], + [/\[/, 'delimiter.square', '@bracketCounting'], + [/\]/, 'delimiter.square', '@pop'], + [/\(/, 'delimiter.parenthesis', '@bracketCounting'], + [/\)/, 'delimiter.parenthesis', '@pop'], + { include: 'root' } + ], + }, + ignoreCase: true, + }) + + setTheme(monacoI) monacoI.languages.setLanguageConfiguration('custom-language', { brackets: [ @@ -347,111 +433,20 @@ export default function EditorComponent({ currentQuery, historyQueries, setCurre ] }); - if (graph.Id) { - monacoI.languages.setMonarchTokensProvider('custom-language', { - tokenizer: { - root: [ - [new RegExp(`\\b(${Array.from(namespaces.keys()).join('|')})\\b`), "keyword"], - [new RegExp(`\\b(${KEYWORDS.join('|')})\\b`), "keyword"], - [ - new RegExp(`\\b(${functions.map(({ label }) => { - if ((label as string).includes(".")) { - const labels = (label as string).split(".") - return labels[labels.length - 1] - } - return label - }).join('|')})\\b`), - "function" - ], - [/"([^"\\]|\\.)*"/, 'string'], - [/'([^'\\]|\\.)*'/, 'string'], - [/\d+/, 'number'], - [/:(\w+)/, 'type'], - [/\{/, { token: 'delimiter.curly', next: '@bracketCounting' }], - [/\[/, { token: 'delimiter.square', next: '@bracketCounting' }], - [/\(/, { token: 'delimiter.parenthesis', next: '@bracketCounting' }], - ], - bracketCounting: [ - [/\{/, 'delimiter.curly', '@bracketCounting'], - [/\}/, 'delimiter.curly', '@pop'], - [/\[/, 'delimiter.square', '@bracketCounting'], - [/\]/, 'delimiter.square', '@pop'], - [/\(/, 'delimiter.parenthesis', '@bracketCounting'], - [/\)/, 'delimiter.parenthesis', '@pop'], - { include: 'root' } - ], - }, - ignoreCase: true, - }) - } else { - monacoI.languages.setMonarchTokensProvider('custom-language', { - tokenizer: { - root: [ - [new RegExp(`\\b(${KEYWORDS.join('|')})\\b`), "keyword"], - [/"([^"\\]|\\.)*"/, 'string'], - [/'([^'\\]|\\.)*'/, 'string'], - [/\d+/, 'number'], - [/:(\w+)/, 'type'], - [/\{/, { token: 'delimiter.curly', next: '@bracketCounting' }], - [/\[/, { token: 'delimiter.square', next: '@bracketCounting' }], - [/\(/, { token: 'delimiter.parenthesis', next: '@bracketCounting' }], - ], - bracketCounting: [ - [/\{/, 'delimiter.curly', '@bracketCounting'], - [/\}/, 'delimiter.curly', '@pop'], - [/\[/, 'delimiter.square', '@bracketCounting'], - [/\]/, 'delimiter.square', '@pop'], - [/\(/, 'delimiter.parenthesis', '@bracketCounting'], - [/\)/, 'delimiter.parenthesis', '@pop'], - { include: 'root' } - ], - }, - ignoreCase: true, - }) - } - - return monacoI.languages.registerCompletionItemProvider("custom-language", { - provideCompletionItems: (model, position) => { + const provider = monacoI.languages.registerCompletionItemProvider("custom-language", { + provideCompletionItems: async (model, position) => { const word = model.getWordUntilPosition(position) const range = new monaco.Range(position.lineNumber, word.startColumn, position.lineNumber, word.endColumn) - console.log(suggestions); return { - suggestions: suggestions.map(s => ({ ...s, range })) + suggestions: (await addSuggestions(monacoI)).map(s => ({ ...s, range })) } }, }) - } - - useEffect(() => { - const timeout = setTimeout(async () => { - if (!monacoInstance) return - const provider = await addSuggestions(monacoInstance) - setSugProvider(provider) - }, 5000) - - return () => { - clearTimeout(timeout) - if (sugProvider) { - sugProvider.dispose() - console.log("cleanup dispose"); - } - console.log("cleanup"); - } - }, [graph.Id]) - - const handleEditorWillMount = async (monacoI: Monaco) => { - monacoI.languages.register({ id: "custom-language" }) - - setTheme(monacoI) - - const provider = await addSuggestions(monacoI) - - setSugProvider(provider) - setMonacoInstance(monacoI) + setSugDisposed(provider) } - - const handleEditorDidMount = (e: monaco.editor.IStandaloneCodeEditor, monacoI: Monaco) => { + + const handleEditorDidMount = (e: monaco.editor.IStandaloneCodeEditor) => { editorRef.current = e const updatePlaceholderVisibility = () => { @@ -475,9 +470,6 @@ export default function EditorComponent({ currentQuery, historyQueries, setCurre updatePlaceholderVisibility(); - setMonacoInstance(monacoI) - - const isFirstLine = e.createContextKey('isFirstLine', true); // Update the context key value based on the cursor position @@ -586,6 +578,10 @@ export default function EditorComponent({ currentQuery, historyQueries, setCurre /> + + + + Date: Sun, 2 Feb 2025 11:41:03 +0200 Subject: [PATCH 4/8] commit --- app/components/CreateGraph.tsx | 7 ++++--- app/schema/page.tsx | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/app/components/CreateGraph.tsx b/app/components/CreateGraph.tsx index 8cb14894..bcd2e807 100644 --- a/app/components/CreateGraph.tsx +++ b/app/components/CreateGraph.tsx @@ -36,7 +36,8 @@ export default function CreateGraph({ const handleCreateGraph = async (e: React.FormEvent) => { e.preventDefault() - if (!graphName) { + const name = graphName.trim() + if (!name) { toast({ title: "Error", description: "Graph name cannot be empty", @@ -45,13 +46,13 @@ export default function CreateGraph({ return } const q = 'RETURN 1' - const result = await securedFetch(`api/graph/${prepareArg(graphName)}/?query=${prepareArg(q)}`, { + const result = await securedFetch(`api/graph/${prepareArg(name)}/?query=${prepareArg(q)}`, { method: "GET", }, toast) if (!result.ok) return - onSetGraphName(graphName) + onSetGraphName(name) setGraphName("") setOpen(false) } diff --git a/app/schema/page.tsx b/app/schema/page.tsx index 0b57ea4f..fd3a4400 100644 --- a/app/schema/page.tsx +++ b/app/schema/page.tsx @@ -56,7 +56,7 @@ export default function Page() { return (
-
+
Date: Sun, 2 Feb 2025 12:04:04 +0200 Subject: [PATCH 5/8] Add validation for MAX_INFO_QUERIES configuration limit --- app/settings/Configurations.tsx | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/app/settings/Configurations.tsx b/app/settings/Configurations.tsx index 689937c2..a3404ed4 100644 --- a/app/settings/Configurations.tsx +++ b/app/settings/Configurations.tsx @@ -138,6 +138,17 @@ export default function Configurations() { return false; } + if (name === "MAX_INFO_QUERIES") { + if (Number(value) > 1000) { + toast({ + title: "Error", + description: "Value must be less than 1000", + variant: "destructive" + }); + return false; + } + } + const result = await securedFetch( `api/graph/?config=${prepareArg(name)}&value=${prepareArg(value)}`, { method: 'POST' }, @@ -145,7 +156,7 @@ export default function Configurations() { ); if (!result.ok) return false; - + const configToUpdate = configs.find(row => row.cells[0].value === name); const oldValue = configToUpdate?.cells[2].value; From e758f7a3c2a392aec167da48946c124fd35b50fe Mon Sep 17 00:00:00 2001 From: Anchel135 Date: Mon, 3 Feb 2025 13:39:29 +0200 Subject: [PATCH 6/8] commit --- app/components/EditorComponent.tsx | 36 +++++++++++++++++------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/app/components/EditorComponent.tsx b/app/components/EditorComponent.tsx index 3785fd1d..d12292ac 100644 --- a/app/components/EditorComponent.tsx +++ b/app/components/EditorComponent.tsx @@ -18,6 +18,7 @@ import Button from "./ui/Button"; interface Props { currentQuery: string historyQueries: string[] + setHistoryQueries: (queries: string[]) => void setCurrentQuery: (query: string) => void maximize: boolean runQuery: (query: string) => void @@ -199,7 +200,7 @@ const LINE_HEIGHT = 38 const PLACEHOLDER = "Type your query here to start" -export default function EditorComponent({ currentQuery, historyQueries, setCurrentQuery, maximize, runQuery, graph, data }: Props) { +export default function EditorComponent({ currentQuery, historyQueries, setHistoryQueries, setCurrentQuery, maximize, runQuery, graph, data }: Props) { const [query, setQuery] = useState(currentQuery) const placeholderRef = useRef(null) @@ -214,7 +215,7 @@ export default function EditorComponent({ currentQuery, historyQueries, setCurre const historyRef = useRef({ historyQueries, currentQuery, - historyCounter: historyQueries.length + historyCounter: 0 }) useEffect(() => { @@ -498,9 +499,9 @@ export default function EditorComponent({ currentQuery, historyQueries, setCurre contextMenuOrder: 1.5, run: async () => { if (historyRef.current.historyQueries.length === 0) return - const counter = historyRef.current.historyCounter ? historyRef.current.historyCounter - 1 : historyRef.current.historyQueries.length; - historyRef.current.historyCounter = counter; - setQuery(counter ? historyRef.current.historyQueries[counter - 1] : historyRef.current.currentQuery); + const counter = (historyRef.current.historyCounter + 1) % (historyRef.current.historyQueries.length + 1) + historyRef.current.historyCounter = counter + setQuery(counter ? historyRef.current.historyQueries[counter - 1] : historyRef.current.currentQuery) }, precondition: 'isFirstLine && !suggestWidgetVisible', }); @@ -512,14 +513,24 @@ export default function EditorComponent({ currentQuery, historyQueries, setCurre contextMenuOrder: 1.5, run: async () => { if (historyRef.current.historyQueries.length === 0) return - const counter = (historyRef.current.historyCounter + 1) % (historyRef.current.historyQueries.length + 1) - historyRef.current.historyCounter = counter - setQuery(counter ? historyRef.current.historyQueries[counter - 1] : historyRef.current.currentQuery) + const counter = historyRef.current.historyCounter ? historyRef.current.historyCounter - 1 : historyRef.current.historyQueries.length; + historyRef.current.historyCounter = counter; + setQuery(counter ? historyRef.current.historyQueries[counter - 1] : historyRef.current.currentQuery); }, precondition: 'isFirstLine && !suggestWidgetVisible', }); } + const handleSetHistoryQuery = (val: string) => { + setQuery(val) + historyRef.current.historyQueries[historyRef.current.historyCounter - 1] = val + setHistoryQueries(historyRef.current.historyQueries) + } + + useEffect(() => { + setLineNumber(query.split("\n").length) + }, [query]) + return (
{ @@ -544,14 +555,7 @@ export default function EditorComponent({ currentQuery, historyQueries, setCurre lineNumbers: lineNumber > 1 ? "on" : "off", }} value={(blur ? query.replace(/\s+/g, ' ').trim() : query)} - onChange={(val) => { - if (historyRef.current.historyCounter) { - setQuery(val || ""); - } else { - setCurrentQuery(val || ""); - } - setLineNumber(val?.split("\n").length || 1); - }} + onChange={(val) => historyRef.current.historyCounter ? handleSetHistoryQuery(val || "") : setCurrentQuery(val || "")} theme="custom-theme" beforeMount={handleEditorWillMount} onMount={handleEditorDidMount} From c0b1dc1bad63346a95ea3019a5c7a3835844a69f Mon Sep 17 00:00:00 2001 From: Anchel135 Date: Mon, 3 Feb 2025 13:34:39 +0200 Subject: [PATCH 7/8] commit --- app/graph/GraphView.tsx | 4 +++- app/graph/page.tsx | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/app/graph/GraphView.tsx b/app/graph/GraphView.tsx index bef19f26..09b36f16 100644 --- a/app/graph/GraphView.tsx +++ b/app/graph/GraphView.tsx @@ -23,13 +23,14 @@ import TableView from "./TableView"; const ForceGraph = dynamic(() => import("../components/ForceGraph"), { ssr: false }); const EditorComponent = dynamic(() => import("../components/EditorComponent"), {ssr: false}) -function GraphView({ graph, selectedElement, setSelectedElement, runQuery, historyQuery, historyQueries, fetchCount, session }: { +function GraphView({ graph, selectedElement, setSelectedElement, runQuery, historyQuery, historyQueries, setHistoryQueries, fetchCount, session }: { graph: Graph selectedElement: Node | Link | undefined setSelectedElement: Dispatch> runQuery: (query: string) => Promise historyQuery: string historyQueries: string[] + setHistoryQueries: (queries: string[]) => void fetchCount: () => void session: Session | null }) { @@ -222,6 +223,7 @@ function GraphView({ graph, selectedElement, setSelectedElement, runQuery, histo maximize={maximize} currentQuery={query} historyQueries={historyQueries} + setHistoryQueries={setHistoryQueries} runQuery={handleRunQuery} setCurrentQuery={setQuery} data={session} diff --git a/app/graph/page.tsx b/app/graph/page.tsx index 7e89fdb7..ad47ac94 100644 --- a/app/graph/page.tsx +++ b/app/graph/page.tsx @@ -129,6 +129,7 @@ export default function Page() { runQuery={runQuery} historyQuery={historyQuery} historyQueries={queries.map(({ text }) => text)} + setHistoryQueries={(queriesArr) => setQueries(queries.map((query, i) => ({ text: queriesArr[i], metadata: query.metadata } as Query)))} fetchCount={fetchCount} session={session} /> From 941c9f989bc8a1660e6d2a60d54be9a2cd5271b7 Mon Sep 17 00:00:00 2001 From: Anchel135 Date: Tue, 4 Feb 2025 13:58:18 +0200 Subject: [PATCH 8/8] place empty string when graph name empty --- app/components/ui/combobox.tsx | 6 +++--- app/graph/Selector.tsx | 5 +++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/app/components/ui/combobox.tsx b/app/components/ui/combobox.tsx index 8ccf2e20..c03f4663 100644 --- a/app/components/ui/combobox.tsx +++ b/app/components/ui/combobox.tsx @@ -109,10 +109,10 @@ export default function Combobox({ isSelectGraph = false, disabled = false, inTa { options.map((option) => ( - {option} + {!option ? '""' : option} )) } diff --git a/app/graph/Selector.tsx b/app/graph/Selector.tsx index 96ad75f3..cafbcb21 100644 --- a/app/graph/Selector.tsx +++ b/app/graph/Selector.tsx @@ -74,9 +74,10 @@ export default function Selector({ onChange, graphName, setGraphName, queries, r } const handleOnChange = async (name: string) => { + const formattedName = name === '""' ? "" : name if (runQuery) { const q = 'MATCH (n)-[e]-(m) return n,e,m' - const result = await securedFetch(`api/graph/${prepareArg(name)}_schema/?query=${prepareArg(q)}&create=false&role=${session?.user.role}`, { + const result = await securedFetch(`api/graph/${prepareArg(formattedName)}_schema/?query=${prepareArg(q)}&create=false&role=${session?.user.role}`, { method: "GET" }, toast) @@ -87,7 +88,7 @@ export default function Selector({ onChange, graphName, setGraphName, queries, r setSchema(Graph.create(name, json.result)) } } - onChange(name) + onChange(formattedName) setSelectedValue(name) }