From b5b2d538daf672f9d2a396ec1289c055b5fe8fb1 Mon Sep 17 00:00:00 2001 From: pomelo-nwu Date: Wed, 18 Dec 2024 11:36:40 +0800 Subject: [PATCH 1/9] feat: update code --- .../src/services/cypher/querySearch.tsx | 18 ++++++++++-------- .../src/graph/custom-node/draw.tsx | 10 +++++----- packages/studio-website/src/layouts/index.tsx | 2 +- 3 files changed, 16 insertions(+), 14 deletions(-) diff --git a/packages/studio-explore/src/services/cypher/querySearch.tsx b/packages/studio-explore/src/services/cypher/querySearch.tsx index 65fc875a..b49fff7b 100644 --- a/packages/studio-explore/src/services/cypher/querySearch.tsx +++ b/packages/studio-explore/src/services/cypher/querySearch.tsx @@ -13,16 +13,16 @@ export const querySearch = async params => { } if (label && property) { if (value) { - return queryStatement(`match (n:${label}) where n.${property} CONTAINS "${value}" return n`); + return queryStatement(`match (n:${label}) where n.${property} CONTAINS "${value}" return n limit 100`); } else { - return queryStatement(`match (n:${label}) where n.${property} IS NOT NULL return n`); + return queryStatement(`match (n:${label}) where n.${property} IS NOT NULL return n limit 100`); } } if (!label && property) { if (value) { - return queryStatement(`match (n) where n.${property} CONTAINS "${value}" return n`); + return queryStatement(`match (n) where n.${property} CONTAINS "${value}" return n limit 100`); } else { - return queryStatement(`match (n) where n.${property} IS NOT NULL return n`); + return queryStatement(`match (n) where n.${property} IS NOT NULL return n limit 100`); } } } @@ -32,16 +32,18 @@ export const querySearch = async params => { } if (label && property) { if (value) { - return queryStatement(`match (a)-[r:${label}]->(b) where r.${property} CONTAINS "${value}" return a,r,b`); + return queryStatement( + `match (a)-[r:${label}]->(b) where r.${property} CONTAINS "${value}" return a,r,b limit 100`, + ); } else { - return queryStatement(`match (a)-[r:${label}]->(b) where r.${property} IS NOT NULL return a,r,b`); + return queryStatement(`match (a)-[r:${label}]->(b) where r.${property} IS NOT NULL return a,r,b limit 100`); } } if (!label && property) { if (value) { - return queryStatement(`match (a)-[r]->(b) where r.${property} CONTAINS "${value}" return a,r,b`); + return queryStatement(`match (a)-[r]->(b) where r.${property} CONTAINS "${value}" return a,r,b limit 100`); } else { - return queryStatement(`match (a)-[r]->(b) where r.${property} IS NOT NULL return a,r,b`); + return queryStatement(`match (a)-[r]->(b) where r.${property} IS NOT NULL return a,r,b limit 100`); } } } diff --git a/packages/studio-graph/src/graph/custom-node/draw.tsx b/packages/studio-graph/src/graph/custom-node/draw.tsx index 31825735..0ff9d664 100644 --- a/packages/studio-graph/src/graph/custom-node/draw.tsx +++ b/packages/studio-graph/src/graph/custom-node/draw.tsx @@ -27,9 +27,9 @@ export function drawText(ctx: CanvasRenderingContext2D, options: DrawTextOptions const totalHeight = lines.length * lineHeight; const startY = y - totalHeight / 2; - if (totalHeight < maxWidth) { - lines.forEach((line, index) => { - ctx.fillText(line, x, startY + index * lineHeight); - }); - } + // if (totalHeight < maxWidth) { + lines.forEach((line, index) => { + ctx.fillText(line, x, startY + index * lineHeight); + }); + // } } diff --git a/packages/studio-website/src/layouts/index.tsx b/packages/studio-website/src/layouts/index.tsx index 5e8b5601..662a2915 100644 --- a/packages/studio-website/src/layouts/index.tsx +++ b/packages/studio-website/src/layouts/index.tsx @@ -58,7 +58,7 @@ export default function StudioLayout() { } return { graphs: res, - graphId: (matchGraph && matchGraph.id) || 'DRAFT_GRAPH', + graphId: (matchGraph && matchGraph.id) || '', }; }); }; From 40ad0136554f63fc5c15fa75ae716294633721eb Mon Sep 17 00:00:00 2001 From: pomelo-nwu Date: Wed, 18 Dec 2024 15:45:59 +0800 Subject: [PATCH 2/9] feat: update server --- packages/studio-driver/package.json | 3 +- packages/studio-driver/src/index.ts | 39 +- .../studio-driver/src/kuzu-wasm-driver.ts | 563 ++++++++++++++++++ packages/studio-driver/src/queryGraph.tsx | 49 ++ packages/studio-driver/src/utils.tsx | 105 ++++ packages/studio-explore/src/app.tsx | 4 +- .../Connection/import-into-kuzu/index.tsx | 8 +- .../import-into-kuzu}/kuzu-wasm.tsx | 37 +- .../src/services/cypher/index.tsx | 6 +- .../services/cypher/queryCommonNeighbor.tsx | 9 + .../src/services/cypher/queryNeighbor.tsx | 26 +- .../src/services/cypher/queryStatement.tsx | 62 +- .../ContextMenu/CommonNeighbor/index.tsx | 29 +- .../ContextMenu/NeighborQuery/index.tsx | 62 +- .../src/components/ContextMenu/index.tsx | 1 + .../studio-graph/src/components/index.tsx | 10 + packages/studio-graph/src/index.tsx | 10 - .../query-graph/cypher-services/index.tsx | 17 + .../cypher-services/queryNeighbor.tsx | 67 +++ .../src/components/query-graph/cypher.tsx | 4 - .../query-graph/gremlin-services/index.tsx | 17 + .../gremlin-services/queryNeighbor.tsx | 66 ++ .../src/components/query-graph/gremlin.tsx | 4 - .../src/components/query-graph/index.tsx | 18 +- .../components/query-graph/queryStatement.tsx | 45 ++ pnpm-lock.yaml | 145 ++--- 26 files changed, 1153 insertions(+), 253 deletions(-) create mode 100644 packages/studio-driver/src/kuzu-wasm-driver.ts create mode 100644 packages/studio-driver/src/queryGraph.tsx create mode 100644 packages/studio-driver/src/utils.tsx rename packages/studio-explore/src/{services => components/Connection/import-into-kuzu}/kuzu-wasm.tsx (67%) create mode 100644 packages/studio-explore/src/services/cypher/queryCommonNeighbor.tsx create mode 100644 packages/studio-query/src/components/query-graph/cypher-services/index.tsx create mode 100644 packages/studio-query/src/components/query-graph/cypher-services/queryNeighbor.tsx delete mode 100644 packages/studio-query/src/components/query-graph/cypher.tsx create mode 100644 packages/studio-query/src/components/query-graph/gremlin-services/index.tsx create mode 100644 packages/studio-query/src/components/query-graph/gremlin-services/queryNeighbor.tsx delete mode 100644 packages/studio-query/src/components/query-graph/gremlin.tsx create mode 100644 packages/studio-query/src/components/query-graph/queryStatement.tsx diff --git a/packages/studio-driver/package.json b/packages/studio-driver/package.json index b4ee0bcd..bdc9583e 100644 --- a/packages/studio-driver/package.json +++ b/packages/studio-driver/package.json @@ -22,7 +22,8 @@ "license": "ISC", "dependencies": { "@graphscope/_test_gremlin_": "^0.1.2", - "neo4j-driver": "^5.12.0" + "neo4j-driver": "^5.12.0", + "@kuzu/kuzu-wasm": "0.7.0" }, "publishConfig": { "access": "public", diff --git a/packages/studio-driver/src/index.ts b/packages/studio-driver/src/index.ts index a0f9b9f9..ace751f4 100644 --- a/packages/studio-driver/src/index.ts +++ b/packages/studio-driver/src/index.ts @@ -1,39 +1,6 @@ import CypherDriver from './cypher-driver'; import GremlinDriver from './gremlin-driver'; +import { queryGraph, cancelGraph, getDriver } from './queryGraph'; +import { KuzuDriver } from './kuzu-wasm-driver'; -export { CypherDriver, GremlinDriver }; - -const DriverMap = new Map(); - -export interface QueryParams { - script: string; - language: 'gremlin' | 'cypher'; - endpoint: string; - username?: string; - password?: string; -} - -export const queryGraph = async (params: QueryParams) => { - const { language, endpoint, script, username, password } = params; - const id = `${language}_${endpoint}`; - - if (!DriverMap.has(id)) { - if (language === 'cypher') { - DriverMap.set(id, new CypherDriver(endpoint, username, password)); - } - if (language === 'gremlin') { - DriverMap.set(id, new GremlinDriver(endpoint, username, password)); - } - } - const driver = DriverMap.get(id); - return driver.query(script); -}; - -export const cancelGraph = async (params: QueryParams) => { - const { language, endpoint } = params; - const id = `${language}_${endpoint}`; - if (!DriverMap.has(id)) { - const driver = DriverMap.get(id); - driver.close(); - } -}; +export { CypherDriver, GremlinDriver, KuzuDriver, queryGraph, cancelGraph, getDriver }; diff --git a/packages/studio-driver/src/kuzu-wasm-driver.ts b/packages/studio-driver/src/kuzu-wasm-driver.ts new file mode 100644 index 00000000..dc703f62 --- /dev/null +++ b/packages/studio-driver/src/kuzu-wasm-driver.ts @@ -0,0 +1,563 @@ +import Kuzu from '@kuzu/kuzu-wasm'; +import { uniqueElementsBy } from './utils'; + +interface SchemaItem { + source?: string; + target?: string; + label: string; + properties: { + name: string; + type: string; + }; +} +interface IKuzuResult { + Database: (params: any) => Promise; + Connection: (db: any) => Promise; + FS: Promise; + execute: (queryScript: string) => Promise; +} + +interface IKuzuDatabase { + close: () => Promise; +} + +interface IKuzuConnection { + execute: (queryScript: string) => Promise; + close: () => Promise; +} + +class FileData { + name: string; + content: string; + + constructor(name, content) { + this.name = name; // 文件名 + this.content = content; // 文件内容 + } +} + +class GraphData { + files: FileData[]; + schema: { + nodes: any[]; + edges: any[]; + }; + + constructor(files, schema) { + this.files = files; + this.schema = schema; + } +} + +export class KuzuDriver { + kuzu: typeof Kuzu; + db: IKuzuDatabase | null; + conn: IKuzuConnection | null; + FS: any; + dbname: string; + schema: { nodes: any[]; edges: any[] }; + curDataset: string; + kuzuEngine: IKuzuResult | null; + indexdb: any; + mountedPath: Set; + + constructor() { + this.kuzu = Kuzu; + this.db = null; + this.conn = null; + this.FS = null; + this.schema = { nodes: [], edges: [] }; + this.dbname = ''; + this.curDataset = ''; + this.kuzuEngine = null; + this.mountedPath = new Set(); + } + + async initialize(): Promise { + console.log('initialize again'); + this.kuzuEngine = await this.kuzu(); + + //@ts-ignore + + // this.db = await result.Database(this.dbname, 0, 10, false, false, 4194304 * 16 * 4); + // this.conn = await result.Connection(this.db); + this.FS = this.kuzuEngine.FS; + this.FS?.mkdir('data'); + + // this.FS?.mkdir('export'); + } + + async installUDF(): Promise { + var res = await this.conn?.execute('CREATE MACRO elementid(x) AS CAST(ID(x),"STRING")'); + console.log(res.toString()); + } + + async use(datasetId: string) { + if (this.curDataset !== '') { + await this.closeDataset(); + } + + await this.setDatasetId(datasetId); + try { + await this.FS?.mkdir(`${this.curDataset}`); + } catch (err) { + console.log(`Directory ${this.curDataset} exists`); + } + + const targetPath = this.pathJoin('/', this.curDataset); + if (!this.mountedPath.has(targetPath)) { + console.log(`MOUNT ${this.curDataset} ...`); + try { + await this.FS?.mount(this.FS.filesystems.IDBFS, { autoPersist: true }, targetPath); + this.mountedPath.add(targetPath); + } catch (err) { + console.error(`Error mounting:`, err); + } + } + + if (await this.existDataset(datasetId)) { + console.log(`Loading from indexdb ${datasetId}`); + await this.loadFromIndexDB(); + } + + await this.openDataset(); + await this.installUDF(); + + return this.curDataset; + } + + async setDatasetId(datasetId: string) { + this.curDataset = datasetId; + } + + async existDataset(datasetId: string) { + const datasetPath = this.pathJoin('/', datasetId); + if (indexedDB.databases) { + try { + const databases = await indexedDB.databases(); + return databases.some(db => db.name === datasetPath); + } catch (error) { + console.error('Error accessing databases:', error); + return false; + } + } else { + console.warn('indexedDB.databases() is not supported in this browser.'); + return false; + } + } + + // async existDataset(datasetId: string) { + // const key = this.pathJoin('/', datasetId); + // console.log("exist?"); + // console.log(key); + // console.log(await localforage.getItem(key)); + // return await localforage.getItem(key) + // .then(function(value) { + // return value !== null; + // }) + // .catch(function(err) { + // console.error('An error occurred:', err); + // return false; + // }); + // } + + async datasetLoaded(datasetId: string) { + try { + const { mode, timestamp } = await this.FS.lookupPath('/' + datasetId).node; + if (this.FS.isDir(mode)) { + return true; + } + } catch (err) { + console.error(`Error retrieving "${datasetId}":`, err); + return false; // 在发生错误时返回 false + } + + return false; + } + + typeConvert(input_type: string) { + const input_type_upper = input_type.toUpperCase(); + switch (input_type_upper) { + case 'DT_STRING': + return 'STRING'; + case 'DT_DOUBLE': + return 'DOUBLE'; + case 'DT_SIGNED_INT32': + return 'INT32'; + case 'DT_SIGNED_INT64': + return 'INT64'; + default: + return 'STRING'; + } + } + + async createSchema(schema: { nodes: any[]; edges: any[] }): Promise { + const { nodes, edges } = schema; + + this.schema = schema; + + const node_scripts = nodes.map(node => { + const { label, properties } = node; + + let property_scripts = properties.map(property => { + const { name, type, primaryKey } = property; + return `${name} ${this.typeConvert(type)}`; + }); + const primary_keys = properties + .map(property => { + const { name, type, primaryKey } = property; + if (primaryKey) { + return `PRIMARY KEY (${name})`; + } + return null; + }) + .filter(key => key !== null); + const final_property_scripts = property_scripts.concat(primary_keys); + return `CREATE NODE TABLE ${label} (${final_property_scripts.join(', ')});`; + }); + const edge_scripts = edges.map(edge => { + const { label, source, target, properties } = edge; + + const property_scripts = properties.map(property => { + const { name, type } = property; + return `${name} ${this.typeConvert(type)}`; + }); + if (property_scripts.length === 0) { + return `CREATE REL TABLE ${label} (FROM ${source} TO ${target});`; + } + return `CREATE REL TABLE ${label} (FROM ${source} TO ${target} , ${property_scripts.join(', ')});`; + }); + + /** execute */ + let result: any[] = []; + + for (let i = 0; i < node_scripts.length; i++) { + console.log('create nodes script: ', node_scripts[i]); + const res = await this.conn?.execute(node_scripts[i]); + result.push(res.toString()); + } + for (let i = 0; i < edge_scripts.length; i++) { + console.log('create edges script: ', edge_scripts[i]); + const res = await this.conn?.execute(edge_scripts[i]); + result.push(res.toString()); + } + + console.log('Schema created: ', result); + } + async uploadCsvFile(file: File, meta): Promise { + return new Promise((resolve, reject) => { + if (file) { + const reader = new FileReader(); + reader.onload = async e => { + try { + //@ts-ignore + var fileData = new Uint8Array(e.target.result); + var fileName = file.name; + var filePath = 'data/' + fileName; + var label_name = file.name.split('.')[0]; + await this.FS.writeFile(filePath, fileData); + var res; + + const { delimiter } = meta; + res = await this.conn?.execute( + `COPY ${label_name} FROM "${filePath}" (HEADER=true, DELIM='${delimiter}');`, + ); + resolve(res); // Resolve the Promise + } catch (error) { + reject(error); // Reject the Promise on error + } + }; + + reader.onerror = error => { + reject(error); // Handle error case + }; + + reader.readAsArrayBuffer(file); + } else { + reject(new Error('No file provided')); + } + }); + } + async loadGraph(data_files: File[]): Promise { + const logs: any = { + nodes: [], + edges: [], + }; + + for (const node of this.schema.nodes) { + const file = data_files.find(item => { + //@ts-ignore + return item.name === node.label + '.csv'; + }); + if (file) { + console.log('node: ', file.name); + await this.uploadCsvFile(file, node.meta).then(res => { + logs.nodes.push({ + name: file.name, + message: res.toString(), + }); + }); + } + } + for (const edge of this.schema.edges) { + const file = data_files.find(item => { + //@ts-ignore + return item.name === edge.label + '.csv'; + }); + if (file) { + await this.uploadCsvFile(file, edge.meta).then(res => { + logs.edges.push({ + name: file.name, + message: res.toString(), + }); + }); + } + } + return logs; + } + + pathJoin(...segments) { + return segments + .filter(segment => segment) + .join('/') + .replace(/\/+/g, '/'); + } + + async rmdirs(dir: string) { + let entries = await this.FS.readdir(dir); + console.log(entries); + + let results = await Promise.all( + entries.map(async entry => { + if (entry !== '.' && entry !== '..') { + let fullPath = this.pathJoin(dir, entry); + console.log(`remove ${fullPath}`); + + const { mode, timestamp } = await this.FS.lookupPath(fullPath).node; + if (this.FS.isFile(mode)) { + await this.FS.unlink(fullPath); + } else if (this.FS.isDir(mode)) { + await this.rmdirs(fullPath); + } + } + }), + ); + + try { + await this.FS.rmdir(dir); + } catch { + console.log(`Error when remove ${dir}`); + } + } + + async query(queryScript: string): Promise { + console.time('Query cost'); + const queryResult = await this.conn?.execute(queryScript); + console.timeEnd('Query cost'); + if (!queryResult) { + return { + nodes: [], + edges: [], + raw: [], + }; + } + try { + const data = JSON.parse(queryResult.table.toString()); + + const nodes: any[] = []; + const edges: any[] = []; + + data.forEach(record => { + Object.values(record).forEach(item => { + //@ts-ignore + const { _ID, _SRC, _DST, _LABEL, ...others } = item; + if (_ID) { + const { offset, table } = _ID; + const id = `"${table}:${offset}"`; + const isEdge = _SRC && _DST; + if (isEdge) { + const source = `"${_SRC.table}:${_SRC.offset}"`; + const target = `"${_DST.table}:${_DST.offset}"`; + edges.push({ + id, + label: _LABEL, + source, + target, + properties: { + ...others, + }, + }); + } else { + nodes.push({ + id, + label: _LABEL, + properties: { + ...others, + }, + }); + } + } + }); + }); + const _nodes = uniqueElementsBy(nodes, (a, b) => { + return a.id === b.id; + }); + const _edges = uniqueElementsBy(edges, (a, b) => { + return a.id === b.id; + }); + console.log({ nodes: _nodes, edges: _edges, raw: data }); + return { nodes: _nodes, edges: _edges, raw: data }; + } catch (error) { + console.error(error); + return { + nodes: [], + edges: [], + raw: JSON.parse(queryResult.table.toString()), + }; + } + } + + async querySchema(): Promise { + const tables_raw = await this.conn?.execute(`call show_tables() return *`); + const tables = JSON.parse(tables_raw.table.toString()); + const nodes: { + label: string; + properties: { + name: string; + type: string; + }[]; + }[] = []; + const edges: { + source: string; + target: string; + label: string; + properties: { + name: string; + type: string; + }[]; + }[] = []; + for (let i = 0; i < tables.length; i++) { + const { name, type } = tables[i]; + const properties_raw = await this.conn?.execute(`call TABLE_INFO('${name}') return *`); + const properties = JSON.parse(properties_raw.table.toString()); + if (type === 'NODE') { + nodes.push({ + label: name, + properties: properties.map(c => ({ + name: c.name, + type: c.type, + })), + }); + } + if (type === 'REL') { + const relationship_raw = await this.conn?.execute(`call SHOW_CONNECTION('${name}') return *`); + const relationship = JSON.parse(relationship_raw.table.toString())[0]; + + edges.push({ + source: relationship['source table name'], + target: relationship['destination table name'], + label: name, + properties: properties.map(c => ({ + name: c.name, + type: c.type, + })), + }); + } + } + + return { + nodes, + edges, + }; + } + + async getCount() { + if (this.curDataset === '') { + return [0, 0]; + } + + var vertex_res = await this.conn?.execute('MATCH (n) RETURN COUNT(n) AS vertex_count;'); + var edge_res = await this.conn?.execute('MATCH ()-[r]->() RETURN COUNT(r) AS edge_count;'); + + const vertex_count = parseInt(vertex_res.toString().split('\n')[1], 10); + const edge_count = parseInt(edge_res.toString().split('\n')[1], 10); + const countArray = [vertex_count, edge_count]; + + console.log(countArray); + return countArray; + } + + async loadFromIndexDB(): Promise { + const syncfsPromise = () => { + return new Promise((resolve, reject) => { + this.FS.syncfs(true, function (err) { + if (err) { + console.error('Error loading from indexdb: ', err); + reject(err); + } else { + console.log('Load from indexdb successfully'); + resolve(); + } + }); + }); + }; + + await syncfsPromise(); + console.log('finish load from indexdb'); + } + + async openDataset(): Promise { + console.log(this.FS.readdir('/')); + console.log(`Open dataset ${this.curDataset}`); + + //@ts-ignore + this.db = await this.kuzuEngine.Database(this.curDataset, 0, 10, false, false, 4194304 * 16 * 4); + //@ts-ignore + this.conn = await this.kuzuEngine.Connection(this.db); + + const res = await this.conn?.execute('MATCH (n) RETURN count(n);'); + console.log(res.toString()); + } + + async closeDataset(): Promise { + console.log(`Close dataset ${this.curDataset}`); + + if (this.conn) { + await this.conn.close(); + console.log('Connection closed.'); + } + if (this.db) { + await this.db.close(); + console.log('Database closed.'); + } + } + + async writeBack(): Promise<{ success: boolean; message: string }> { + console.log('start to write back'); + return new Promise(resolve => { + this.FS.syncfs(false, function (err) { + if (err) { + console.error('Error saving to indexdb: ', err); + resolve({ + success: false, + message: err, + }); + } else { + console.log('Save to indexdb successfully'); + resolve({ + success: true, + message: 'Save to indexdb successfully', + }); + } + }); + }); + } + + async close(): Promise { + //await this.writeBack(); + await this.closeDataset(); + } + async cancel(): Promise { + await this.closeDataset(); + } +} diff --git a/packages/studio-driver/src/queryGraph.tsx b/packages/studio-driver/src/queryGraph.tsx new file mode 100644 index 00000000..956e9c59 --- /dev/null +++ b/packages/studio-driver/src/queryGraph.tsx @@ -0,0 +1,49 @@ +import CypherDriver from './cypher-driver'; +import GremlinDriver from './gremlin-driver'; +import { KuzuDriver } from './kuzu-wasm-driver'; + +const DriverMap = new Map(); + +export interface QueryParams { + script: string; + language: 'gremlin' | 'cypher'; + endpoint: string; + username?: string; + password?: string; +} + +export const getDriver = async (params: Pick) => { + const { language, endpoint, username, password } = params; + const id = `${language}_${endpoint}`; + if (!DriverMap.has(id)) { + if (language === 'cypher') { + const [engineId, datasetId] = endpoint.split('://'); + if (engineId === 'kuzu_wasm') { + const driver = new KuzuDriver(); + await driver.initialize(); + const exist = await driver.existDataset(datasetId); + if (exist) { + await driver.use(datasetId); + } + DriverMap.set(id, driver); + } else { + DriverMap.set(id, new CypherDriver(endpoint, username, password)); + } + } + if (language === 'gremlin') { + DriverMap.set(id, new GremlinDriver(endpoint, username, password)); + } + } + return DriverMap.get(id) as CypherDriver | GremlinDriver | KuzuDriver; +}; + +export const queryGraph = async (params: QueryParams) => { + const { script } = params; + const driver = await getDriver(params); + return driver.query(script); +}; + +export const cancelGraph = async (params: QueryParams) => { + const driver = await getDriver(params); + return driver.close(); +}; diff --git a/packages/studio-driver/src/utils.tsx b/packages/studio-driver/src/utils.tsx new file mode 100644 index 00000000..c3f1c784 --- /dev/null +++ b/packages/studio-driver/src/utils.tsx @@ -0,0 +1,105 @@ +export function safeParse(input) { + try { + // 尝试直接解析 + return JSON.parse(input); + } catch (error) { + // 如果直接解析失败,检查是否可以通过添加双引号来修复 + if (typeof input === 'string' && !input.startsWith('"') && !input.endsWith('"')) { + try { + // 尝试添加双引号并重新解析 + return JSON.parse(`"${input}"`); + } catch (innerError) { + // 如果仍然无法解析,则返回原始输入或者抛出错误 + console.error('Failed to parse the input:', innerError); + return input; + } + } else { + // 如果输入已经是有效的 JSON 字符串或其他类型的数据,直接返回 + return input; + } + } +} + +export const storage = { + get(key: string): T | undefined { + try { + const values = localStorage.getItem(key); + if (values) { + return safeParse(values); + } + } catch (error) { + console.error('Error while retrieving data from localStorage:', error); + } + }, + set: (key, value) => { + try { + localStorage.setItem(key, JSON.stringify(value, null, 2)); + } catch (error) { + console.error('Error while storing data in localStorage:', error); + } + }, +}; + +/** 数据去重 */ +export const uniqueElementsBy = (arr: any[], fn: (arg0: any, arg1: any) => any) => + arr.reduce((acc, v) => { + if (!acc.some((x: any) => fn(v, x))) acc.push(v); + return acc; + }, []); + +export function transNeo4jSchema(raw): { nodes: []; edges: [] } { + try { + const nodes: any[] = []; + const edges: any[] = []; + const [obj] = raw.records[0]['_fields']; + Object.keys(obj).forEach(label => { + const item = obj[label]; + const { type, relationships } = item; + if (type === 'node') { + nodes.push({ + id: label, + label: label, + properties: Object.keys(item.properties).map(property => { + return { + name: property, + type: item.properties[property].type, + }; + }), + }); + Object.keys(relationships).forEach(edgeLabel => { + const edge = relationships[edgeLabel]; + const { direction } = edge; + const source = direction === 'out' ? label : relationships[edgeLabel].labels[0]; + const target = direction === 'out' ? relationships[edgeLabel].labels[0] : label; + edges.push({ + id: edgeLabel, + label: edgeLabel, + source, + target, + type: 'edge', + properties: Object.keys(relationships[edgeLabel].properties).map(property => { + return { + name: property, + type: relationships[edgeLabel].properties[property].type, + }; + }), + }); + }); + } + }); + + const _edges = uniqueElementsBy(edges, (a, b) => { + return a.id + a.source + a.target === b.id + b.source + b.target; + }); + return { + nodes: nodes as [], + edges: _edges, + }; + } catch (error) { + console.log('error', error); + return { + nodes: [], + edges: [], + }; + } +} diff --git a/packages/studio-explore/src/app.tsx b/packages/studio-explore/src/app.tsx index bbbe469c..a57bc3e2 100644 --- a/packages/studio-explore/src/app.tsx +++ b/packages/studio-explore/src/app.tsx @@ -127,14 +127,14 @@ const Explore: React.FunctionComponent = props => { {/* - + */} - + {/* */} diff --git a/packages/studio-explore/src/components/Connection/import-into-kuzu/index.tsx b/packages/studio-explore/src/components/Connection/import-into-kuzu/index.tsx index dbfada0d..9a88d280 100644 --- a/packages/studio-explore/src/components/Connection/import-into-kuzu/index.tsx +++ b/packages/studio-explore/src/components/Connection/import-into-kuzu/index.tsx @@ -4,7 +4,7 @@ import { Utils, ImportFiles } from '@graphscope/studio-components'; import { parseSchemaByFiles } from '@graphscope/studio-importor'; import { Button, notification } from 'antd'; import { FormattedMessage } from 'react-intl'; -import { useKuzuGraph, setFiles, getDriver } from '../../../services/kuzu-wasm'; +import { useKuzuGraph, setFiles } from './kuzu-wasm'; import { transform } from './transform'; interface IImportIntoKuzuProps { @@ -33,6 +33,8 @@ const ImportIntoKuzu: React.FunctionComponent = props => { const schemaData = transform(result); const datasetId = 'graph-' + Utils.uuid(); + Utils.storage.set('query_endpoint', `kuzu_wasm://${datasetId}`); + Utils.storage.set('query_language', `cypher`); setFiles(datasetId, { schema: schemaData, @@ -53,8 +55,10 @@ const ImportIntoKuzu: React.FunctionComponent = props => { }); if (success) { handleClose(); - Utils.storage.set('query_endpoint', `kuzu_wasm://${datasetId}`); + // Utils.storage.set('query_endpoint', `kuzu_wasm://${datasetId}`); window.location.reload(); + } else { + Utils.storage.set('query_endpoint', ''); } }; diff --git a/packages/studio-explore/src/services/kuzu-wasm.tsx b/packages/studio-explore/src/components/Connection/import-into-kuzu/kuzu-wasm.tsx similarity index 67% rename from packages/studio-explore/src/services/kuzu-wasm.tsx rename to packages/studio-explore/src/components/Connection/import-into-kuzu/kuzu-wasm.tsx index 3b582053..5569aba1 100644 --- a/packages/studio-explore/src/services/kuzu-wasm.tsx +++ b/packages/studio-explore/src/components/Connection/import-into-kuzu/kuzu-wasm.tsx @@ -1,30 +1,19 @@ import { Utils } from '@graphscope/studio-components'; -import { KuzuDriver } from '@graphscope/graphy-website'; +import { getDriver as getKuzuDriver, KuzuDriver } from '@graphscope/studio-driver'; +const { storage } = Utils; -declare global { - interface Window { - KUZU_DRIVER: KuzuDriver; - } -} -export const getDriver = async () => { - if (!window.KUZU_DRIVER) { - const driver = new KuzuDriver(); - await driver.initialize(); - const query_endpoint = Utils.storage.get('query_endpoint'); - if (query_endpoint) { - const [engineId, datasetId] = query_endpoint.split('://'); - const exist = await driver.existDataset(datasetId); - if (exist) { - await driver.use(datasetId); - } - } - window.KUZU_DRIVER = driver; - } - return window.KUZU_DRIVER; +const getDriver = async () => { + const language = storage.get<'cypher' | 'gremlin'>('query_language') || 'cypher'; + const endpoint = storage.get('query_endpoint') || ''; + const username = storage.get('query_username'); + const password = storage.get('query_password'); + return getKuzuDriver({ + language, + endpoint, + username, + password, + }) as Promise; }; -//@ts-ignore -window.getDriver = getDriver; -let used = false; const __TEMP = {}; export const setFiles = (dataset_id: string, params) => { __TEMP[dataset_id] = params; diff --git a/packages/studio-explore/src/services/cypher/index.tsx b/packages/studio-explore/src/services/cypher/index.tsx index d8d19c4d..abd8511d 100644 --- a/packages/studio-explore/src/services/cypher/index.tsx +++ b/packages/studio-explore/src/services/cypher/index.tsx @@ -3,6 +3,7 @@ import type { INeighborQueryData, INeighborQueryItems, IQueryStatement, + IQueryCommonNeighbor, } from '@graphscope/studio-graph'; import { queryNeighborData, queryNeighborItems } from './queryNeighbor'; import type { IQueryGraphData, IQueryGraphSchema } from '../../components/FetchGraph'; @@ -22,7 +23,8 @@ export type ExploreQueryTypes = | IQueryNeighborStatics | INeighborQueryData | INeighborQueryItems - | IQueryStatement; + | IQueryStatement + | IQueryCommonNeighbor; import { queryGraphSchema } from './queryGraphSchema'; import { queryStatement } from './queryStatement'; @@ -32,6 +34,7 @@ import { queryPropertyStatics } from './queryPropertyStatics'; import { queryNeighborStatics } from './queryNeighborStatics'; import { querySearch } from './querySearch'; import { queryStatistics } from './queryStatistics'; +import { queryCommonNeighbor } from './queryCommonNeighbor'; const services: IServiceQueries = { queryNeighborData, @@ -44,5 +47,6 @@ const services: IServiceQueries = { queryNeighborStatics, querySearch, queryStatistics, + queryCommonNeighbor, }; export default services; diff --git a/packages/studio-explore/src/services/cypher/queryCommonNeighbor.tsx b/packages/studio-explore/src/services/cypher/queryCommonNeighbor.tsx new file mode 100644 index 00000000..945afcaf --- /dev/null +++ b/packages/studio-explore/src/services/cypher/queryCommonNeighbor.tsx @@ -0,0 +1,9 @@ +import type { IQueryCommonNeighbor } from '@graphscope/studio-graph'; +import { queryStatement } from './queryStatement'; +export const queryCommonNeighbor: IQueryCommonNeighbor['query'] = async selectIds => { + const data = { + nodes: [], + edges: [], + }; + return data; +}; diff --git a/packages/studio-explore/src/services/cypher/queryNeighbor.tsx b/packages/studio-explore/src/services/cypher/queryNeighbor.tsx index 62e667b6..5c160ca7 100644 --- a/packages/studio-explore/src/services/cypher/queryNeighbor.tsx +++ b/packages/studio-explore/src/services/cypher/queryNeighbor.tsx @@ -13,7 +13,14 @@ export const queryNeighborData: INeighborQueryData['query'] = async params => { export const queryNeighborItems: INeighborQueryItems['query'] = async params => { const { schema } = params; - const itemMap = {}; + const itemMap = { + all: [ + { + key: `(a)-[b]-(c)`, + label: `One-Hop Neighbors`, + }, + ], + }; schema.nodes.forEach(node => { itemMap[node.label] = getOptionsBySchema(schema, node.label); }); @@ -50,14 +57,11 @@ function getOptionsBySchema(schema, nodeLabel) { } }); - const extraItems = - options.length > 1 - ? [ - { - key: `(a)-[b]-(c)`, - label: `All Neighbors`, - }, - ] - : []; - return [...extraItems, ...options]; + return [ + { + key: `(a)-[b]-(c)`, + label: `One-Hop Neighbors`, + }, + ...options, + ]; } diff --git a/packages/studio-explore/src/services/cypher/queryStatement.tsx b/packages/studio-explore/src/services/cypher/queryStatement.tsx index b96fd077..aa448d8c 100644 --- a/packages/studio-explore/src/services/cypher/queryStatement.tsx +++ b/packages/studio-explore/src/services/cypher/queryStatement.tsx @@ -1,8 +1,6 @@ import { queryGraph } from '@graphscope/studio-driver'; import { Utils } from '@graphscope/studio-components'; import type { IQueryStatement } from '@graphscope/studio-graph'; -import { getQueryEngineType, transNeo4jSchema } from '../utils'; -import { getDriver } from '../kuzu-wasm'; const { storage } = Utils; export const queryStatement: IQueryStatement['query'] = async (script: string) => { const query_language = storage.get<'cypher' | 'gremlin'>('query_language') || 'cypher'; @@ -11,41 +9,33 @@ export const queryStatement: IQueryStatement['query'] = async (script: string) = const query_username = storage.get('query_username'); const query_password = storage.get('query_password'); try { - const engine = getQueryEngineType(); - if (engine === 'graphscope' || engine === 'neo4j') { - const _params = { - script, - language: query_language, - endpoint: query_endpoint, - username: query_username, - password: query_password, - }; - - if (query_initiation === 'Server') { - return await fetch('/graph/query', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(_params), - }) - .then(res => res.json()) - .then(res => { - if (res.success) { - return res.data; - } - return { - nodes: [], - edges: [], - }; - }); - } - return queryGraph(_params); - } - if (engine === 'kuzu_wasm') { - const driver = await getDriver(); - return await driver.queryData(script); + const _params = { + script, + language: query_language, + endpoint: query_endpoint, + username: query_username, + password: query_password, + }; + if (query_initiation === 'Server') { + return await fetch('/graph/query', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(_params), + }) + .then(res => res.json()) + .then(res => { + if (res.success) { + return res.data; + } + return { + nodes: [], + edges: [], + }; + }); } + return queryGraph(_params); } catch (error) { return { nodes: [], diff --git a/packages/studio-graph/src/components/ContextMenu/CommonNeighbor/index.tsx b/packages/studio-graph/src/components/ContextMenu/CommonNeighbor/index.tsx index 4b5fba8b..766f8142 100644 --- a/packages/studio-graph/src/components/ContextMenu/CommonNeighbor/index.tsx +++ b/packages/studio-graph/src/components/ContextMenu/CommonNeighbor/index.tsx @@ -1,12 +1,13 @@ import * as React from 'react'; import { Typography, Button, Menu } from 'antd'; -import { useContext, getDataMap } from '../../../'; +import { useContext, getDataMap, GraphData } from '../../../'; import { BranchesOutlined } from '@ant-design/icons'; import { handleExpand, applyStatus } from '../NeighborQuery/utils'; -interface INeighborQueryProps { - onQuery: (params: any) => Promise; +export interface IQueryCommonNeighbor { + id: 'queryCommonNeighbor'; + query: (selectIds: string[]) => Promise; } const getScript = (selectedIds, dataMap) => { @@ -50,36 +51,28 @@ const getScript = (selectedIds, dataMap) => { RETURN ${returnScript},neighbor `; }; -const CommonNeighbor: React.FunctionComponent = props => { - const { onQuery } = props; +const CommonNeighbor = () => { const { store, updateStore } = useContext(); - const { nodeStatus, schema, dataMap, emitter, graph } = store; + const { selectNodes, emitter, graph, getService } = store; const handleClick = async () => { updateStore(draft => { draft.isLoading = true; }); + emitter?.emit('canvas:click'); - const selectedIds = Object.keys(nodeStatus).filter((key, index) => { - return nodeStatus[key].selected; - }); - const script = getScript(selectedIds, dataMap); + const selectedIds = selectNodes.map(item => item.id); - const res = await onQuery({ - script, - language: 'cypher', - }); + const res = await getService('queryCommonNeighbor')(selectedIds); if (res.nodes.length > 0) { - const { nodeStatus, edgeStatus } = applyStatus(res, item => { + const { nodeStatus, edgeStatus } = applyStatus(res as any, item => { return { selected: true }; }); - + const newData = handleExpand(graph, res, selectedIds); updateStore(draft => { - const newData = handleExpand(graph, res, selectedIds); draft.data = newData; - draft.dataMap = getDataMap(newData); draft.isLoading = false; draft.nodeStatus = nodeStatus; draft.edgeStatus = edgeStatus; diff --git a/packages/studio-graph/src/components/ContextMenu/NeighborQuery/index.tsx b/packages/studio-graph/src/components/ContextMenu/NeighborQuery/index.tsx index 819ef37f..b6e46f61 100644 --- a/packages/studio-graph/src/components/ContextMenu/NeighborQuery/index.tsx +++ b/packages/studio-graph/src/components/ContextMenu/NeighborQuery/index.tsx @@ -1,7 +1,9 @@ import React, { useEffect, useRef, useState } from 'react'; import { Menu } from 'antd'; -import { useContext, getDataMap, type GraphSchema } from '../../../'; +import { useContext, type GraphSchema } from '../../../'; import { handleExpand, applyStatus } from './utils'; +import { ShareAltOutlined } from '@ant-design/icons'; +import { useDynamicStyle } from '@graphscope/studio-components'; interface INeighborQueryProps {} @@ -16,14 +18,23 @@ export interface INeighborQueryData { } export interface INeighborQueryItems { id: 'queryNeighborItems'; - query: (params: { schema: GraphSchema }) => Promise>; + query: (params: { schema: GraphSchema }) => Promise>; } -const getScript = (ids, dataMap) => {}; - const NeighborNeighbor: React.FunctionComponent = props => { const { store, updateStore } = useContext(); - const { nodeStatus, schema, dataMap, emitter, graph, data, getService } = store; + const { schema, selectNodes, emitter, graph, getService } = store; + useDynamicStyle( + ` + .studio-graph-neighbor-query .ant-menu-vertical .ant-menu-submenu-title{ + padding-inline:12px !important; + } + .studio-graph-neighbor-query .ant-menu .ant-menu-submenu-title .anticon span { + margin-inline-start:8px !important; + } + `, + 'studio-neighbor-query', + ); const MenuRef = useRef(null); const [state, setState] = useState({ @@ -46,11 +57,7 @@ const NeighborNeighbor: React.FunctionComponent = props => getMenuItems(); }, [schema]); - const selectIds = Object.keys(nodeStatus).filter(key => { - return nodeStatus[key].selected; - }); - const selectNode = data.nodes.find(item => item.id === selectIds[0]); // dataMap[selectId] || {}; - if (!selectNode) { + if (selectNodes.length === 0) { return null; } @@ -59,7 +66,7 @@ const NeighborNeighbor: React.FunctionComponent = props => updateStore(draft => { draft.isLoading = true; }); - + const selectIds = selectNodes.map(item => item.id); const res = await getService('queryNeighborData')({ selectIds, key, @@ -72,7 +79,6 @@ const NeighborNeighbor: React.FunctionComponent = props => const newData = handleExpand(graph, res, selectIds); updateStore(draft => { draft.data = newData; - draft.dataMap = getDataMap(newData); draft.nodeStatus = nodeStatus; draft.edgeStatus = edgeStatus; draft.isLoading = false; @@ -85,8 +91,29 @@ const NeighborNeighbor: React.FunctionComponent = props => } }; + const defaultChildren = itemMap['all']; + const items = + selectNodes.length === 1 + ? [ + { + icon: , + key: 'NeighborQuery', + label: 'NeighborQuery', + //@ts-ignore + children: itemMap[selectNodes[0].label] || defaultChildren, + }, + ] + : [ + { + icon: , + key: 'NeighborQuery', + label: 'NeighborQuery', + children: defaultChildren, + }, + ]; + console.log(items, itemMap); return ( -
+
{ if (MenuRef.current) { @@ -97,14 +124,7 @@ const NeighborNeighbor: React.FunctionComponent = props => onClick={onClick} style={{ margin: '0px', padding: '0px', width: '103%' }} mode="vertical" - items={[ - { - key: 'NeighborQuery', - label: 'NeighborQuery', - //@ts-ignore - children: itemMap[selectNode.label], - }, - ]} + items={items} />
); diff --git a/packages/studio-graph/src/components/ContextMenu/index.tsx b/packages/studio-graph/src/components/ContextMenu/index.tsx index 36415b41..02869bd4 100644 --- a/packages/studio-graph/src/components/ContextMenu/index.tsx +++ b/packages/studio-graph/src/components/ContextMenu/index.tsx @@ -38,6 +38,7 @@ const ContextMenu: React.FunctionComponent = props => { selected: true, }, }; + draft.selectNodes = [node]; }); }; const handleClear = () => { diff --git a/packages/studio-graph/src/components/index.tsx b/packages/studio-graph/src/components/index.tsx index df0c7329..f3837d39 100644 --- a/packages/studio-graph/src/components/index.tsx +++ b/packages/studio-graph/src/components/index.tsx @@ -13,7 +13,9 @@ export { default as SliderFilter } from './SliderFilter'; export { default as RunCluster } from './RunCluster'; export { default as LayoutSetting } from './LayoutSetting'; export { default as NeighborQuery } from './ContextMenu/NeighborQuery'; +export type { INeighborQueryData, INeighborQueryItems } from './ContextMenu/NeighborQuery'; export { default as CommonNeighbor } from './ContextMenu/CommonNeighbor'; +export type { IQueryCommonNeighbor } from './ContextMenu/CommonNeighbor'; export { default as DeleteNode } from './ContextMenu/DeleteNode'; export { default as Brush } from './Brush'; export { default as Loading } from './Loading'; @@ -29,3 +31,11 @@ export { default as Placeholder } from './Placeholder'; export { default as LayoutSwitch } from './LayoutSwitch'; export { default as ZoomStatus } from './ZoomStatus'; export { default as HoverMenu } from './HoverMenu'; + +export type IServiceQueries Promise }> = { + [K in T['id']]?: T extends { id: K } ? T['query'] : never; +}; +export interface IQueryStatement { + id: 'queryStatement'; + query: (script: string) => Promise; +} diff --git a/packages/studio-graph/src/index.tsx b/packages/studio-graph/src/index.tsx index ac5251a6..40659d97 100644 --- a/packages/studio-graph/src/index.tsx +++ b/packages/studio-graph/src/index.tsx @@ -6,14 +6,4 @@ export * from './graph/types'; export { useContext, GraphProvider } from './graph/useContext'; export { getDataMap, getStyleConfig } from './graph/utils'; export { registerIcons } from './graph/custom-icons'; - -export type { INeighborQueryData, INeighborQueryItems } from './components/ContextMenu/NeighborQuery'; export { useApis } from './graph/hooks/useApis'; - -export type IServiceQueries Promise }> = { - [K in T['id']]?: T extends { id: K } ? T['query'] : never; -}; -export interface IQueryStatement { - id: 'queryStatement'; - query: (script: string) => Promise; -} diff --git a/packages/studio-query/src/components/query-graph/cypher-services/index.tsx b/packages/studio-query/src/components/query-graph/cypher-services/index.tsx new file mode 100644 index 00000000..61ea653a --- /dev/null +++ b/packages/studio-query/src/components/query-graph/cypher-services/index.tsx @@ -0,0 +1,17 @@ +import type { + IServiceQueries, + INeighborQueryData, + INeighborQueryItems, + IQueryStatement, +} from '@graphscope/studio-graph'; +import { queryNeighborData, queryNeighborItems } from './queryNeighbor'; + +import { queryStatement } from '../queryStatement'; + +const services: IServiceQueries = { + queryNeighborData: queryNeighborData, + queryNeighborItems: queryNeighborItems, + queryStatement: queryStatement, +}; + +export default services; diff --git a/packages/studio-query/src/components/query-graph/cypher-services/queryNeighbor.tsx b/packages/studio-query/src/components/query-graph/cypher-services/queryNeighbor.tsx new file mode 100644 index 00000000..91a648ed --- /dev/null +++ b/packages/studio-query/src/components/query-graph/cypher-services/queryNeighbor.tsx @@ -0,0 +1,67 @@ +import type { INeighborQueryData, INeighborQueryItems } from '@graphscope/studio-graph'; +import { queryStatement } from '../queryStatement'; +export const queryNeighborData: INeighborQueryData['query'] = async params => { + const { key, selectIds } = params; + const script = ` + MATCH ${key} + WHERE elementId(a) IN [${selectIds}] + RETURN a,b,c + `; + const data = await queryStatement(script); + return data; +}; + +export const queryNeighborItems: INeighborQueryItems['query'] = async params => { + const { schema } = params; + const itemMap = { + all: [ + { + key: `(a)-[b]-(c)`, + label: `One-Hop Neighbors`, + }, + ], + }; + schema.nodes.forEach(node => { + itemMap[node.label] = getOptionsBySchema(schema, node.label); + }); + return itemMap; +}; + +function getOptionsBySchema(schema, nodeLabel) { + const options: { key: string; label: string }[] = []; + schema.edges.forEach(item => { + let direction = 'out'; + const { source, target, label } = item; + const include = nodeLabel === source || nodeLabel === target; + if (include && source === target) { + direction = 'both'; + options.push({ + key: `(a:${source})-[b:${label}]-(c:${target})`, + label: `[${label}]-(${target})`, + }); + return; + } + if (source === nodeLabel) { + direction = 'out'; + options.push({ + key: `(a:${source})-[b:${label}]->(c:${target})`, + label: `[${label}]->(${target})`, + }); + } + if (target === nodeLabel) { + direction = 'in'; + options.push({ + key: `(a:${source})<-[b:${label}]-(c:${target})`, + label: `[${label}]<-(${target})`, + }); + } + }); + + return [ + { + key: `(a)-[b]-(c)`, + label: `One-Hop Neighbors`, + }, + ...options, + ]; +} diff --git a/packages/studio-query/src/components/query-graph/cypher.tsx b/packages/studio-query/src/components/query-graph/cypher.tsx deleted file mode 100644 index aec5a52c..00000000 --- a/packages/studio-query/src/components/query-graph/cypher.tsx +++ /dev/null @@ -1,4 +0,0 @@ -export default { - id: 'CypherServices', - services: [], -}; diff --git a/packages/studio-query/src/components/query-graph/gremlin-services/index.tsx b/packages/studio-query/src/components/query-graph/gremlin-services/index.tsx new file mode 100644 index 00000000..36545ede --- /dev/null +++ b/packages/studio-query/src/components/query-graph/gremlin-services/index.tsx @@ -0,0 +1,17 @@ +import type { + IServiceQueries, + INeighborQueryData, + INeighborQueryItems, + IQueryStatement, +} from '@graphscope/studio-graph'; +import { queryNeighborData, queryNeighborItems } from './queryNeighbor'; + +import { queryStatement } from '../queryStatement'; + +const services: IServiceQueries = { + queryStatement: queryStatement, + queryNeighborData: queryNeighborData, + queryNeighborItems: queryNeighborItems, +}; + +export default services; diff --git a/packages/studio-query/src/components/query-graph/gremlin-services/queryNeighbor.tsx b/packages/studio-query/src/components/query-graph/gremlin-services/queryNeighbor.tsx new file mode 100644 index 00000000..5552f9aa --- /dev/null +++ b/packages/studio-query/src/components/query-graph/gremlin-services/queryNeighbor.tsx @@ -0,0 +1,66 @@ +import type { INeighborQueryData, INeighborQueryItems } from '@graphscope/studio-graph'; +import { queryStatement } from '../queryStatement'; +export const queryNeighborData: INeighborQueryData['query'] = async params => { + const { key, selectIds } = params; + const script = ` + MATCH ${key} + WHERE elementId(a) IN [${selectIds}] + RETURN a,b,c + `; + // const data = await queryStatement(script); + return { + nodes: [], + edges: [], + }; +}; + +export const queryNeighborItems: INeighborQueryItems['query'] = async params => { + const { schema } = params; + const itemMap = {}; + schema.nodes.forEach(node => { + itemMap[node.label] = getOptionsBySchema(schema, node.label); + }); + return itemMap; +}; + +function getOptionsBySchema(schema, nodeLabel) { + const options: { key: string; label: string }[] = []; + schema.edges.forEach(item => { + let direction = 'out'; + const { source, target, label } = item; + const include = nodeLabel === source || nodeLabel === target; + if (include && source === target) { + direction = 'both'; + options.push({ + key: `(a:${source})-[b:${label}]-(c:${target})`, + label: `[${label}]-(${target})`, + }); + return; + } + if (source === nodeLabel) { + direction = 'out'; + options.push({ + key: `(a:${source})-[b:${label}]->(c:${target})`, + label: `[${label}]->(${target})`, + }); + } + if (target === nodeLabel) { + direction = 'in'; + options.push({ + key: `(a:${source})<-[b:${label}]-(c:${target})`, + label: `[${label}]<-(${target})`, + }); + } + }); + + const extraItems = + options.length > 1 + ? [ + { + key: `(a)-[b]-(c)`, + label: `All Neighbors`, + }, + ] + : []; + return [...extraItems, ...options]; +} diff --git a/packages/studio-query/src/components/query-graph/gremlin.tsx b/packages/studio-query/src/components/query-graph/gremlin.tsx deleted file mode 100644 index bed637bd..00000000 --- a/packages/studio-query/src/components/query-graph/gremlin.tsx +++ /dev/null @@ -1,4 +0,0 @@ -export default { - id: 'GremlinServices', - services: [], -}; diff --git a/packages/studio-query/src/components/query-graph/index.tsx b/packages/studio-query/src/components/query-graph/index.tsx index 294ad4a7..ebf69dea 100644 --- a/packages/studio-query/src/components/query-graph/index.tsx +++ b/packages/studio-query/src/components/query-graph/index.tsx @@ -1,7 +1,7 @@ import React, { useRef } from 'react'; import { Button, Tooltip, theme } from 'antd'; import { FormattedMessage } from 'react-intl'; -import { Section, useSection, FullScreen } from '@graphscope/studio-components'; +import { Section, useSection, FullScreen, Utils } from '@graphscope/studio-components'; import { Toolbar, SwitchEngine, @@ -30,8 +30,8 @@ import { import { Divider } from 'antd'; import { BgColorsOutlined } from '@ant-design/icons'; -import CypherServices from './cypher'; -import GremlinServices from './gremlin'; +import CypherServices from './cypher-services'; +import GremlinServices from './gremlin-services'; interface QueryGraphProps { // instance id @@ -51,16 +51,11 @@ const ToogleButton = () => { }; registerIcons(); + const QueryGraph: React.FunctionComponent = props => { const { data, schema, graphId, id } = props; const containerRef = useRef(null); - const language = 'cypher'; - let services: any = []; - if (language === 'cypher') { - services = CypherServices['services']; - } else { - services = GremlinServices['services']; - } + const services = Utils.storage.get('query_language') === 'gremlin' ? GremlinServices : CypherServices; const { token } = theme.useToken(); return ( @@ -94,8 +89,7 @@ const QueryGraph: React.FunctionComponent = props => { - {/* - */} + diff --git a/packages/studio-query/src/components/query-graph/queryStatement.tsx b/packages/studio-query/src/components/query-graph/queryStatement.tsx new file mode 100644 index 00000000..aa448d8c --- /dev/null +++ b/packages/studio-query/src/components/query-graph/queryStatement.tsx @@ -0,0 +1,45 @@ +import { queryGraph } from '@graphscope/studio-driver'; +import { Utils } from '@graphscope/studio-components'; +import type { IQueryStatement } from '@graphscope/studio-graph'; +const { storage } = Utils; +export const queryStatement: IQueryStatement['query'] = async (script: string) => { + const query_language = storage.get<'cypher' | 'gremlin'>('query_language') || 'cypher'; + const query_endpoint = storage.get('query_endpoint') || ''; + const query_initiation = storage.get<'Server' | 'Browser'>('query_initiation'); + const query_username = storage.get('query_username'); + const query_password = storage.get('query_password'); + try { + const _params = { + script, + language: query_language, + endpoint: query_endpoint, + username: query_username, + password: query_password, + }; + if (query_initiation === 'Server') { + return await fetch('/graph/query', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(_params), + }) + .then(res => res.json()) + .then(res => { + if (res.success) { + return res.data; + } + return { + nodes: [], + edges: [], + }; + }); + } + return queryGraph(_params); + } catch (error) { + return { + nodes: [], + edges: [], + }; + } +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e37323ab..27779e42 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -253,7 +253,7 @@ importers: devDependencies: '@vitejs/plugin-react': specifier: ^4.2.1 - version: 4.3.3(vite@5.4.11(@types/node@22.10.0)(less@4.2.0)(lightningcss@1.22.1)(sass@1.81.0)(terser@5.36.0)) + version: 4.3.3(vite@5.4.11(@types/node@22.10.2)(less@4.2.0)(lightningcss@1.22.1)(sass@1.81.0)(terser@5.36.0)) jszip: specifier: ^3.10.1 version: 3.10.1 @@ -262,13 +262,13 @@ importers: version: 2.8.1 vite: specifier: ^5.4.9 - version: 5.4.11(@types/node@22.10.0)(less@4.2.0)(lightningcss@1.22.1)(sass@1.81.0)(terser@5.36.0) + version: 5.4.11(@types/node@22.10.2)(less@4.2.0)(lightningcss@1.22.1)(sass@1.81.0)(terser@5.36.0) vite-plugin-top-level-await: specifier: ^1.4.4 - version: 1.4.4(@swc/helpers@0.5.15)(rollup@4.27.3)(vite@5.4.11(@types/node@22.10.0)(less@4.2.0)(lightningcss@1.22.1)(sass@1.81.0)(terser@5.36.0)) + version: 1.4.4(@swc/helpers@0.5.15)(rollup@4.27.3)(vite@5.4.11(@types/node@22.10.2)(less@4.2.0)(lightningcss@1.22.1)(sass@1.81.0)(terser@5.36.0)) vite-plugin-wasm: specifier: ^3.3.0 - version: 3.3.0(vite@5.4.11(@types/node@22.10.0)(less@4.2.0)(lightningcss@1.22.1)(sass@1.81.0)(terser@5.36.0)) + version: 3.3.0(vite@5.4.11(@types/node@22.10.2)(less@4.2.0)(lightningcss@1.22.1)(sass@1.81.0)(terser@5.36.0)) packages/studio-components: dependencies: @@ -350,25 +350,28 @@ importers: version: 9.0.8 '@vitejs/plugin-react': specifier: ^4.3.1 - version: 4.3.3(vite@5.4.11(@types/node@22.10.0)(less@4.2.0)(lightningcss@1.22.1)(sass@1.81.0)(terser@5.36.0)) + version: 4.3.3(vite@5.4.11(@types/node@22.10.2)(less@4.2.0)(lightningcss@1.22.1)(sass@1.81.0)(terser@5.36.0)) father: specifier: ^4.4.5 - version: 4.5.1(@babel/core@7.26.0)(@types/node@22.10.0)(styled-components@5.3.11(@babel/core@7.26.0)(react-dom@18.2.0(react@18.2.0))(react-is@18.3.1)(react@18.2.0))(type-fest@1.4.0)(webpack@5.96.1(webpack-cli@5.1.4(webpack@5.96.1))) + version: 4.5.1(@babel/core@7.26.0)(@types/node@22.10.2)(styled-components@5.3.11(@babel/core@7.26.0)(react-dom@18.2.0(react@18.2.0))(react-is@18.3.1)(react@18.2.0))(type-fest@1.4.0)(webpack@5.96.1(webpack-cli@5.1.4(webpack@5.96.1))) typescript: specifier: ^5.5.4 version: 5.6.3 vite: specifier: ^5.4.3 - version: 5.4.11(@types/node@22.10.0)(less@4.2.0)(lightningcss@1.22.1)(sass@1.81.0)(terser@5.36.0) + version: 5.4.11(@types/node@22.10.2)(less@4.2.0)(lightningcss@1.22.1)(sass@1.81.0)(terser@5.36.0) vitest: specifier: ^2.0.5 - version: 2.1.5(@types/node@22.10.0)(less@4.2.0)(lightningcss@1.22.1)(sass@1.81.0)(terser@5.36.0) + version: 2.1.5(@types/node@22.10.2)(less@4.2.0)(lightningcss@1.22.1)(sass@1.81.0)(terser@5.36.0) packages/studio-driver: dependencies: '@graphscope/_test_gremlin_': specifier: ^0.1.2 version: 0.1.2 + '@kuzu/kuzu-wasm': + specifier: 0.7.0 + version: 0.7.0 neo4j-driver: specifier: ^5.12.0 version: 5.26.0 @@ -417,16 +420,16 @@ importers: devDependencies: '@vitejs/plugin-react': specifier: ^4.2.1 - version: 4.3.3(vite@5.4.11(@types/node@22.10.0)(less@4.2.0)(lightningcss@1.22.1)(sass@1.81.0)(terser@5.36.0)) + version: 4.3.3(vite@5.4.11(@types/node@22.10.2)(less@4.2.0)(lightningcss@1.22.1)(sass@1.81.0)(terser@5.36.0)) vite: specifier: ^5.4.9 - version: 5.4.11(@types/node@22.10.0)(less@4.2.0)(lightningcss@1.22.1)(sass@1.81.0)(terser@5.36.0) + version: 5.4.11(@types/node@22.10.2)(less@4.2.0)(lightningcss@1.22.1)(sass@1.81.0)(terser@5.36.0) vite-plugin-top-level-await: specifier: ^1.4.4 - version: 1.4.4(@swc/helpers@0.5.15)(rollup@4.27.3)(vite@5.4.11(@types/node@22.10.0)(less@4.2.0)(lightningcss@1.22.1)(sass@1.81.0)(terser@5.36.0)) + version: 1.4.4(@swc/helpers@0.5.15)(rollup@4.27.3)(vite@5.4.11(@types/node@22.10.2)(less@4.2.0)(lightningcss@1.22.1)(sass@1.81.0)(terser@5.36.0)) vite-plugin-wasm: specifier: ^3.3.0 - version: 3.3.0(vite@5.4.11(@types/node@22.10.0)(less@4.2.0)(lightningcss@1.22.1)(sass@1.81.0)(terser@5.36.0)) + version: 3.3.0(vite@5.4.11(@types/node@22.10.2)(less@4.2.0)(lightningcss@1.22.1)(sass@1.81.0)(terser@5.36.0)) packages/studio-graph: dependencies: @@ -502,10 +505,10 @@ importers: version: 3.1.7 '@vitejs/plugin-react': specifier: ^4.3.4 - version: 4.3.4(vite@6.0.3(@types/node@22.10.0)(less@4.2.0)(lightningcss@1.22.1)(sass@1.81.0)(terser@5.36.0)(yaml@2.5.1)) + version: 4.3.4(vite@6.0.3(@types/node@22.10.2)(less@4.2.0)(lightningcss@1.22.1)(sass@1.81.0)(terser@5.36.0)(yaml@2.5.1)) vite: specifier: ^6.0.3 - version: 6.0.3(@types/node@22.10.0)(less@4.2.0)(lightningcss@1.22.1)(sass@1.81.0)(terser@5.36.0)(yaml@2.5.1) + version: 6.0.3(@types/node@22.10.2)(less@4.2.0)(lightningcss@1.22.1)(sass@1.81.0)(terser@5.36.0)(yaml@2.5.1) packages/studio-graph-editor: dependencies: @@ -560,10 +563,10 @@ importers: version: 4.17.13 '@vitejs/plugin-react': specifier: ^4.2.1 - version: 4.3.3(vite@5.4.11(@types/node@22.10.0)(less@4.2.0)(lightningcss@1.22.1)(sass@1.81.0)(terser@5.36.0)) + version: 4.3.3(vite@5.4.11(@types/node@22.10.2)(less@4.2.0)(lightningcss@1.22.1)(sass@1.81.0)(terser@5.36.0)) vite: specifier: ^5.0.12 - version: 5.4.11(@types/node@22.10.0)(less@4.2.0)(lightningcss@1.22.1)(sass@1.81.0)(terser@5.36.0) + version: 5.4.11(@types/node@22.10.2)(less@4.2.0)(lightningcss@1.22.1)(sass@1.81.0)(terser@5.36.0) packages/studio-importor: dependencies: @@ -673,7 +676,7 @@ importers: devDependencies: father: specifier: ^4.4.1 - version: 4.5.1(@babel/core@7.26.0)(@types/node@22.10.0)(styled-components@5.3.11(@babel/core@7.26.0)(react-dom@18.2.0(react@18.2.0))(react-is@18.3.1)(react@18.2.0))(type-fest@1.4.0)(webpack@5.96.1(webpack-cli@5.1.4(webpack@5.96.1))) + version: 4.5.1(@babel/core@7.26.0)(@types/node@22.10.2)(styled-components@5.3.11(@babel/core@7.26.0)(react-dom@18.2.0(react@18.2.0))(react-is@18.3.1)(react@18.2.0))(type-fest@1.4.0)(webpack@5.96.1(webpack-cli@5.1.4(webpack@5.96.1))) packages/studio-server: dependencies: @@ -841,13 +844,13 @@ importers: version: 18.2.0 '@vitejs/plugin-react': specifier: ^4.3.3 - version: 4.3.3(vite@5.4.11(@types/node@22.10.0)(less@4.2.0)(lightningcss@1.22.1)(sass@1.81.0)(terser@5.36.0)) + version: 4.3.3(vite@5.4.11(@types/node@22.10.2)(less@4.2.0)(lightningcss@1.22.1)(sass@1.81.0)(terser@5.36.0)) father: specifier: ^4.4.1 - version: 4.5.1(@babel/core@7.26.0)(@types/node@22.10.0)(styled-components@5.3.11(@babel/core@7.26.0)(react-dom@18.2.0(react@18.2.0))(react-is@18.3.1)(react@18.2.0))(type-fest@1.4.0)(webpack@5.96.1(webpack-cli@5.1.4(webpack@5.96.1))) + version: 4.5.1(@babel/core@7.26.0)(@types/node@22.10.2)(styled-components@5.3.11(@babel/core@7.26.0)(react-dom@18.2.0(react@18.2.0))(react-is@18.3.1)(react@18.2.0))(type-fest@1.4.0)(webpack@5.96.1(webpack-cli@5.1.4(webpack@5.96.1))) vite: specifier: ^5.4.10 - version: 5.4.11(@types/node@22.10.0)(less@4.2.0)(lightningcss@1.22.1)(sass@1.81.0)(terser@5.36.0) + version: 5.4.11(@types/node@22.10.2)(less@4.2.0)(lightningcss@1.22.1)(sass@1.81.0)(terser@5.36.0) packages: @@ -3714,8 +3717,8 @@ packages: '@types/node@20.17.6': resolution: {integrity: sha512-VEI7OdvK2wP7XHnsuXbAJnEpEkF6NjSN45QJlL4VGqZSXsnicpesdTWsg9RISeSdYd3yeRj/y3k5KGjUXYnFwQ==} - '@types/node@22.10.0': - resolution: {integrity: sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==} + '@types/node@22.10.2': + resolution: {integrity: sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ==} '@types/node@22.8.7': resolution: {integrity: sha512-LidcG+2UeYIWcMuMUpBKOnryBWG/rnmOHQR5apjn8myTQcx3rinFRn7DcIFhMnS0PPFSC6OafdIKEad0lj6U0Q==} @@ -10054,11 +10057,11 @@ packages: robust-predicates@3.0.2: resolution: {integrity: sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==} - rollup-plugin-node-externals@7.1.3: - resolution: {integrity: sha512-RM+7tJAejAoRsCf93TptTSdqUhRA8S78DleihMiu54Kac+uLkd9VIegLPhGnaW3ehZTXh56+R301mFH6j2A7vw==} + rollup-plugin-node-externals@8.0.0: + resolution: {integrity: sha512-2HIOpWsWn5DqBoYl6iCAmB4kd5GoGbF68PR4xKR1YBPvywiqjtYvDEjHFodyqRL51iAMDITP074Zxs0OKs6F+g==} engines: {node: '>= 21 || ^20.6.0 || ^18.19.0'} peerDependencies: - rollup: ^3.0.0 || ^4.0.0 + rollup: ^4.0.0 rollup-plugin-visualizer@5.9.0: resolution: {integrity: sha512-bbDOv47+Bw4C/cgs0czZqfm8L82xOZssk4ayZjG40y9zbXclNk7YikrZTDao6p7+HDiGxrN0b65SgZiVm9k1Cg==} @@ -13675,7 +13678,7 @@ snapshots: dependencies: '@types/istanbul-lib-coverage': 2.0.6 '@types/istanbul-reports': 3.0.4 - '@types/node': 22.10.0 + '@types/node': 22.10.2 '@types/yargs': 16.0.9 chalk: 4.1.2 @@ -13684,7 +13687,7 @@ snapshots: '@jest/schemas': 29.6.3 '@types/istanbul-lib-coverage': 2.0.6 '@types/istanbul-reports': 3.0.4 - '@types/node': 22.10.0 + '@types/node': 22.10.2 '@types/yargs': 17.0.33 chalk: 4.1.2 @@ -13789,11 +13792,11 @@ snapshots: '@types/react': 18.2.0 react: 18.2.0 - '@microsoft/api-extractor-model@7.28.4(@types/node@22.10.0)': + '@microsoft/api-extractor-model@7.28.4(@types/node@22.10.2)': dependencies: '@microsoft/tsdoc': 0.14.2 '@microsoft/tsdoc-config': 0.16.2 - '@rushstack/node-core-library': 3.63.0(@types/node@22.10.0) + '@rushstack/node-core-library': 3.63.0(@types/node@22.10.2) transitivePeerDependencies: - '@types/node' @@ -13805,12 +13808,12 @@ snapshots: transitivePeerDependencies: - '@types/node' - '@microsoft/api-extractor@7.39.1(@types/node@22.10.0)': + '@microsoft/api-extractor@7.39.1(@types/node@22.10.2)': dependencies: - '@microsoft/api-extractor-model': 7.28.4(@types/node@22.10.0) + '@microsoft/api-extractor-model': 7.28.4(@types/node@22.10.2) '@microsoft/tsdoc': 0.14.2 '@microsoft/tsdoc-config': 0.16.2 - '@rushstack/node-core-library': 3.63.0(@types/node@22.10.0) + '@rushstack/node-core-library': 3.63.0(@types/node@22.10.2) '@rushstack/rig-package': 0.5.1 '@rushstack/ts-command-line': 4.17.1 colors: 1.2.5 @@ -14389,7 +14392,7 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.27.3': optional: true - '@rushstack/node-core-library@3.63.0(@types/node@22.10.0)': + '@rushstack/node-core-library@3.63.0(@types/node@22.10.2)': dependencies: colors: 1.2.5 fs-extra: 7.0.1 @@ -14399,7 +14402,7 @@ snapshots: semver: 7.5.4 z-schema: 5.0.5 optionalDependencies: - '@types/node': 22.10.0 + '@types/node': 22.10.2 '@rushstack/node-core-library@3.63.0(@types/node@22.9.1)': dependencies: @@ -14785,13 +14788,13 @@ snapshots: '@types/fs-extra@11.0.1': dependencies: '@types/jsonfile': 6.1.4 - '@types/node': 22.10.0 + '@types/node': 22.10.2 '@types/geojson@7946.0.14': {} '@types/graceful-fs@4.1.9': dependencies: - '@types/node': 22.10.0 + '@types/node': 22.10.2 '@types/hapi__joi@17.1.9': {} @@ -14831,13 +14834,13 @@ snapshots: '@types/jsonfile@6.1.4': dependencies: - '@types/node': 22.10.0 + '@types/node': 22.10.2 '@types/katex@0.16.7': {} '@types/keyv@3.1.4': dependencies: - '@types/node': 22.10.0 + '@types/node': 22.10.2 '@types/lodash@4.17.13': {} @@ -14863,7 +14866,7 @@ snapshots: dependencies: undici-types: 6.19.8 - '@types/node@22.10.0': + '@types/node@22.10.2': dependencies: undici-types: 6.20.0 @@ -14908,11 +14911,11 @@ snapshots: '@types/responselike@1.0.3': dependencies: - '@types/node': 22.10.0 + '@types/node': 22.10.2 '@types/sax@1.2.7': dependencies: - '@types/node': 22.10.0 + '@types/node': 22.10.2 '@types/scheduler@0.23.0': {} @@ -15477,14 +15480,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@vitejs/plugin-react@4.3.3(vite@5.4.11(@types/node@22.10.0)(less@4.2.0)(lightningcss@1.22.1)(sass@1.81.0)(terser@5.36.0))': + '@vitejs/plugin-react@4.3.3(vite@5.4.11(@types/node@22.10.2)(less@4.2.0)(lightningcss@1.22.1)(sass@1.81.0)(terser@5.36.0))': dependencies: '@babel/core': 7.26.0 '@babel/plugin-transform-react-jsx-self': 7.25.9(@babel/core@7.26.0) '@babel/plugin-transform-react-jsx-source': 7.25.9(@babel/core@7.26.0) '@types/babel__core': 7.20.5 react-refresh: 0.14.2 - vite: 5.4.11(@types/node@22.10.0)(less@4.2.0)(lightningcss@1.22.1)(sass@1.81.0)(terser@5.36.0) + vite: 5.4.11(@types/node@22.10.2)(less@4.2.0)(lightningcss@1.22.1)(sass@1.81.0)(terser@5.36.0) transitivePeerDependencies: - supports-color @@ -15499,14 +15502,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@vitejs/plugin-react@4.3.4(vite@6.0.3(@types/node@22.10.0)(less@4.2.0)(lightningcss@1.22.1)(sass@1.81.0)(terser@5.36.0)(yaml@2.5.1))': + '@vitejs/plugin-react@4.3.4(vite@6.0.3(@types/node@22.10.2)(less@4.2.0)(lightningcss@1.22.1)(sass@1.81.0)(terser@5.36.0)(yaml@2.5.1))': dependencies: '@babel/core': 7.26.0 '@babel/plugin-transform-react-jsx-self': 7.25.9(@babel/core@7.26.0) '@babel/plugin-transform-react-jsx-source': 7.25.9(@babel/core@7.26.0) '@types/babel__core': 7.20.5 react-refresh: 0.14.2 - vite: 6.0.3(@types/node@22.10.0)(less@4.2.0)(lightningcss@1.22.1)(sass@1.81.0)(terser@5.36.0)(yaml@2.5.1) + vite: 6.0.3(@types/node@22.10.2)(less@4.2.0)(lightningcss@1.22.1)(sass@1.81.0)(terser@5.36.0)(yaml@2.5.1) transitivePeerDependencies: - supports-color @@ -15517,13 +15520,13 @@ snapshots: chai: 5.1.2 tinyrainbow: 1.2.0 - '@vitest/mocker@2.1.5(vite@5.4.11(@types/node@22.10.0)(less@4.2.0)(lightningcss@1.22.1)(sass@1.81.0)(terser@5.36.0))': + '@vitest/mocker@2.1.5(vite@5.4.11(@types/node@22.10.2)(less@4.2.0)(lightningcss@1.22.1)(sass@1.81.0)(terser@5.36.0))': dependencies: '@vitest/spy': 2.1.5 estree-walker: 3.0.3 magic-string: 0.30.13 optionalDependencies: - vite: 5.4.11(@types/node@22.10.0)(less@4.2.0)(lightningcss@1.22.1)(sass@1.81.0)(terser@5.36.0) + vite: 5.4.11(@types/node@22.10.2)(less@4.2.0)(lightningcss@1.22.1)(sass@1.81.0)(terser@5.36.0) '@vitest/pretty-format@2.1.5': dependencies: @@ -18326,9 +18329,9 @@ snapshots: dependencies: reusify: 1.0.4 - father@4.5.1(@babel/core@7.26.0)(@types/node@22.10.0)(styled-components@5.3.11(@babel/core@7.26.0)(react-dom@18.2.0(react@18.2.0))(react-is@18.3.1)(react@18.2.0))(type-fest@1.4.0)(webpack@5.96.1(webpack-cli@5.1.4(webpack@5.96.1))): + father@4.5.1(@babel/core@7.26.0)(@types/node@22.10.2)(styled-components@5.3.11(@babel/core@7.26.0)(react-dom@18.2.0(react@18.2.0))(react-is@18.3.1)(react@18.2.0))(type-fest@1.4.0)(webpack@5.96.1(webpack-cli@5.1.4(webpack@5.96.1))): dependencies: - '@microsoft/api-extractor': 7.39.1(@types/node@22.10.0) + '@microsoft/api-extractor': 7.39.1(@types/node@22.10.2) '@umijs/babel-preset-umi': 4.3.34 '@umijs/bundler-utils': 4.3.34 '@umijs/bundler-webpack': 4.3.34(type-fest@1.4.0)(typescript@5.3.3)(webpack@5.96.1(webpack-cli@5.1.4(webpack@5.96.1))) @@ -19841,7 +19844,7 @@ snapshots: dependencies: '@jest/types': 29.6.3 '@types/graceful-fs': 4.1.9 - '@types/node': 22.10.0 + '@types/node': 22.10.2 anymatch: 3.1.3 fb-watchman: 2.0.2 graceful-fs: 4.2.11 @@ -19858,7 +19861,7 @@ snapshots: jest-util@29.7.0: dependencies: '@jest/types': 29.6.3 - '@types/node': 22.10.0 + '@types/node': 22.10.2 chalk: 4.1.2 ci-info: 3.9.0 graceful-fs: 4.2.11 @@ -19866,20 +19869,20 @@ snapshots: jest-worker@27.5.1: dependencies: - '@types/node': 22.10.0 + '@types/node': 22.10.2 merge-stream: 2.0.0 supports-color: 8.1.1 jest-worker@29.4.3: dependencies: - '@types/node': 22.10.0 + '@types/node': 22.10.2 jest-util: 29.7.0 merge-stream: 2.0.0 supports-color: 8.1.1 jest-worker@29.7.0: dependencies: - '@types/node': 22.10.0 + '@types/node': 22.10.2 jest-util: 29.7.0 merge-stream: 2.0.0 supports-color: 8.1.1 @@ -23155,7 +23158,7 @@ snapshots: robust-predicates@3.0.2: {} - rollup-plugin-node-externals@7.1.3(rollup@4.27.3): + rollup-plugin-node-externals@8.0.0(rollup@4.27.3): dependencies: rollup: 4.27.3 @@ -24705,13 +24708,13 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.2 - vite-node@2.1.5(@types/node@22.10.0)(less@4.2.0)(lightningcss@1.22.1)(sass@1.81.0)(terser@5.36.0): + vite-node@2.1.5(@types/node@22.10.2)(less@4.2.0)(lightningcss@1.22.1)(sass@1.81.0)(terser@5.36.0): dependencies: cac: 6.7.14 debug: 4.3.7(supports-color@5.5.0) es-module-lexer: 1.5.4 pathe: 1.1.2 - vite: 5.4.11(@types/node@22.10.0)(less@4.2.0)(lightningcss@1.22.1)(sass@1.81.0)(terser@5.36.0) + vite: 5.4.11(@types/node@22.10.2)(less@4.2.0)(lightningcss@1.22.1)(sass@1.81.0)(terser@5.36.0) transitivePeerDependencies: - '@types/node' - less @@ -24725,7 +24728,7 @@ snapshots: vite-plugin-node-externals@0.0.1(rollup@4.27.3)(vite@5.4.11(@types/node@22.9.1)(less@4.2.0)(lightningcss@1.22.1)(sass@1.81.0)(terser@5.36.0)): dependencies: - rollup-plugin-node-externals: 7.1.3(rollup@4.27.3) + rollup-plugin-node-externals: 8.0.0(rollup@4.27.3) vite: 5.4.11(@types/node@22.9.1)(less@4.2.0)(lightningcss@1.22.1)(sass@1.81.0)(terser@5.36.0) transitivePeerDependencies: - rollup @@ -24736,12 +24739,12 @@ snapshots: rollup: 4.27.3 vite: 5.4.11(@types/node@22.9.1)(less@4.2.0)(lightningcss@1.22.1)(sass@1.81.0)(terser@5.36.0) - vite-plugin-top-level-await@1.4.4(@swc/helpers@0.5.15)(rollup@4.27.3)(vite@5.4.11(@types/node@22.10.0)(less@4.2.0)(lightningcss@1.22.1)(sass@1.81.0)(terser@5.36.0)): + vite-plugin-top-level-await@1.4.4(@swc/helpers@0.5.15)(rollup@4.27.3)(vite@5.4.11(@types/node@22.10.2)(less@4.2.0)(lightningcss@1.22.1)(sass@1.81.0)(terser@5.36.0)): dependencies: '@rollup/plugin-virtual': 3.0.2(rollup@4.27.3) '@swc/core': 1.9.2(@swc/helpers@0.5.15) uuid: 10.0.0 - vite: 5.4.11(@types/node@22.10.0)(less@4.2.0)(lightningcss@1.22.1)(sass@1.81.0)(terser@5.36.0) + vite: 5.4.11(@types/node@22.10.2)(less@4.2.0)(lightningcss@1.22.1)(sass@1.81.0)(terser@5.36.0) transitivePeerDependencies: - '@swc/helpers' - rollup @@ -24756,9 +24759,9 @@ snapshots: - '@swc/helpers' - rollup - vite-plugin-wasm@3.3.0(vite@5.4.11(@types/node@22.10.0)(less@4.2.0)(lightningcss@1.22.1)(sass@1.81.0)(terser@5.36.0)): + vite-plugin-wasm@3.3.0(vite@5.4.11(@types/node@22.10.2)(less@4.2.0)(lightningcss@1.22.1)(sass@1.81.0)(terser@5.36.0)): dependencies: - vite: 5.4.11(@types/node@22.10.0)(less@4.2.0)(lightningcss@1.22.1)(sass@1.81.0)(terser@5.36.0) + vite: 5.4.11(@types/node@22.10.2)(less@4.2.0)(lightningcss@1.22.1)(sass@1.81.0)(terser@5.36.0) vite-plugin-wasm@3.3.0(vite@5.4.11(@types/node@22.9.1)(less@4.2.0)(lightningcss@1.22.1)(sass@1.81.0)(terser@5.36.0)): dependencies: @@ -24777,13 +24780,13 @@ snapshots: sass: 1.81.0 terser: 5.36.0 - vite@5.4.11(@types/node@22.10.0)(less@4.2.0)(lightningcss@1.22.1)(sass@1.81.0)(terser@5.36.0): + vite@5.4.11(@types/node@22.10.2)(less@4.2.0)(lightningcss@1.22.1)(sass@1.81.0)(terser@5.36.0): dependencies: esbuild: 0.21.5 postcss: 8.4.49 rollup: 4.27.3 optionalDependencies: - '@types/node': 22.10.0 + '@types/node': 22.10.2 fsevents: 2.3.3 less: 4.2.0 lightningcss: 1.22.1 @@ -24803,13 +24806,13 @@ snapshots: sass: 1.81.0 terser: 5.36.0 - vite@6.0.3(@types/node@22.10.0)(less@4.2.0)(lightningcss@1.22.1)(sass@1.81.0)(terser@5.36.0)(yaml@2.5.1): + vite@6.0.3(@types/node@22.10.2)(less@4.2.0)(lightningcss@1.22.1)(sass@1.81.0)(terser@5.36.0)(yaml@2.5.1): dependencies: esbuild: 0.24.0 postcss: 8.4.49 rollup: 4.27.3 optionalDependencies: - '@types/node': 22.10.0 + '@types/node': 22.10.2 fsevents: 2.3.3 less: 4.2.0 lightningcss: 1.22.1 @@ -24817,10 +24820,10 @@ snapshots: terser: 5.36.0 yaml: 2.5.1 - vitest@2.1.5(@types/node@22.10.0)(less@4.2.0)(lightningcss@1.22.1)(sass@1.81.0)(terser@5.36.0): + vitest@2.1.5(@types/node@22.10.2)(less@4.2.0)(lightningcss@1.22.1)(sass@1.81.0)(terser@5.36.0): dependencies: '@vitest/expect': 2.1.5 - '@vitest/mocker': 2.1.5(vite@5.4.11(@types/node@22.10.0)(less@4.2.0)(lightningcss@1.22.1)(sass@1.81.0)(terser@5.36.0)) + '@vitest/mocker': 2.1.5(vite@5.4.11(@types/node@22.10.2)(less@4.2.0)(lightningcss@1.22.1)(sass@1.81.0)(terser@5.36.0)) '@vitest/pretty-format': 2.1.5 '@vitest/runner': 2.1.5 '@vitest/snapshot': 2.1.5 @@ -24836,11 +24839,11 @@ snapshots: tinyexec: 0.3.1 tinypool: 1.0.2 tinyrainbow: 1.2.0 - vite: 5.4.11(@types/node@22.10.0)(less@4.2.0)(lightningcss@1.22.1)(sass@1.81.0)(terser@5.36.0) - vite-node: 2.1.5(@types/node@22.10.0)(less@4.2.0)(lightningcss@1.22.1)(sass@1.81.0)(terser@5.36.0) + vite: 5.4.11(@types/node@22.10.2)(less@4.2.0)(lightningcss@1.22.1)(sass@1.81.0)(terser@5.36.0) + vite-node: 2.1.5(@types/node@22.10.2)(less@4.2.0)(lightningcss@1.22.1)(sass@1.81.0)(terser@5.36.0) why-is-node-running: 2.3.0 optionalDependencies: - '@types/node': 22.10.0 + '@types/node': 22.10.2 transitivePeerDependencies: - less - lightningcss From bf3714896e7ffcaefc006d40318b8dc2e848b002 Mon Sep 17 00:00:00 2001 From: pomelo-nwu Date: Wed, 18 Dec 2024 16:44:36 +0800 Subject: [PATCH 3/9] feat: support gremlin neighbor query --- .../gremlin-services/queryNeighbor.tsx | 72 ++++--------------- 1 file changed, 13 insertions(+), 59 deletions(-) diff --git a/packages/studio-query/src/components/query-graph/gremlin-services/queryNeighbor.tsx b/packages/studio-query/src/components/query-graph/gremlin-services/queryNeighbor.tsx index 5552f9aa..18274352 100644 --- a/packages/studio-query/src/components/query-graph/gremlin-services/queryNeighbor.tsx +++ b/packages/studio-query/src/components/query-graph/gremlin-services/queryNeighbor.tsx @@ -1,66 +1,20 @@ import type { INeighborQueryData, INeighborQueryItems } from '@graphscope/studio-graph'; import { queryStatement } from '../queryStatement'; export const queryNeighborData: INeighborQueryData['query'] = async params => { - const { key, selectIds } = params; - const script = ` - MATCH ${key} - WHERE elementId(a) IN [${selectIds}] - RETURN a,b,c - `; - // const data = await queryStatement(script); - return { - nodes: [], - edges: [], - }; + const { selectIds } = params; + const ids = selectIds.join(','); + const script = `g.V().hasId(${ids}).both().dedup()`; + const data = await queryStatement(script); + return data; }; export const queryNeighborItems: INeighborQueryItems['query'] = async params => { - const { schema } = params; - const itemMap = {}; - schema.nodes.forEach(node => { - itemMap[node.label] = getOptionsBySchema(schema, node.label); - }); - return itemMap; + return { + all: [ + { + key: `(a)-[b]-(c)`, + label: `One-Hop Neighbors`, + }, + ], + }; }; - -function getOptionsBySchema(schema, nodeLabel) { - const options: { key: string; label: string }[] = []; - schema.edges.forEach(item => { - let direction = 'out'; - const { source, target, label } = item; - const include = nodeLabel === source || nodeLabel === target; - if (include && source === target) { - direction = 'both'; - options.push({ - key: `(a:${source})-[b:${label}]-(c:${target})`, - label: `[${label}]-(${target})`, - }); - return; - } - if (source === nodeLabel) { - direction = 'out'; - options.push({ - key: `(a:${source})-[b:${label}]->(c:${target})`, - label: `[${label}]->(${target})`, - }); - } - if (target === nodeLabel) { - direction = 'in'; - options.push({ - key: `(a:${source})<-[b:${label}]-(c:${target})`, - label: `[${label}]<-(${target})`, - }); - } - }); - - const extraItems = - options.length > 1 - ? [ - { - key: `(a)-[b]-(c)`, - label: `All Neighbors`, - }, - ] - : []; - return [...extraItems, ...options]; -} From 6bb8e392b9fbf3c98c17f776a07f3a968fe45238 Mon Sep 17 00:00:00 2001 From: pomelo-nwu Date: Wed, 18 Dec 2024 17:48:18 +0800 Subject: [PATCH 4/9] feat: update code --- packages/studio-components/src/Icons/Graph2D.tsx | 8 +++++--- packages/studio-components/src/Icons/Graph3D.tsx | 7 ++++--- packages/studio-components/src/Icons/Lasso.tsx | 5 +++-- packages/studio-components/src/Icons/Lock.tsx | 7 ++++--- packages/studio-components/src/Icons/Punctuation.tsx | 5 +++-- packages/studio-components/src/Icons/Sidebar.tsx | 2 +- packages/studio-components/src/Icons/Trash.tsx | 7 +++++-- packages/studio-components/src/Icons/Unlock.tsx | 7 ++++--- packages/studio-components/src/Icons/primary-key.tsx | 5 ++++- packages/studio-explore/package.json | 1 - packages/studio-graph/src/components/Brush/index.tsx | 7 +++---- .../studio-graph/src/components/ContextMenu/index.tsx | 2 +- packages/studio-website/src/layouts/index.tsx | 4 ++-- 13 files changed, 39 insertions(+), 28 deletions(-) diff --git a/packages/studio-components/src/Icons/Graph2D.tsx b/packages/studio-components/src/Icons/Graph2D.tsx index 2c5bf482..c144a7f9 100644 --- a/packages/studio-components/src/Icons/Graph2D.tsx +++ b/packages/studio-components/src/Icons/Graph2D.tsx @@ -1,11 +1,13 @@ import * as React from 'react'; - +import { theme } from 'antd'; interface IGraph3DProps { style?: React.CSSProperties; } const Graph2D: React.FunctionComponent = props => { - const { style } = props; - const { color = '#000', fontSize = '18px' } = style as { color: string; fontSize: string }; + const { style = {} } = props; + const { token } = theme.useToken(); + const { fontSize = 16, color = token.colorText } = style; + return (
= props => { - const { style } = props; - const { color, fontSize = '18px' } = style as { color: string; fontSize: string }; + const { style = {} } = props; + const { token } = theme.useToken(); + const { fontSize = 16, color = token.colorText } = style; return (
diff --git a/packages/studio-components/src/Icons/Lasso.tsx b/packages/studio-components/src/Icons/Lasso.tsx index 8c0a5c10..d92fa6be 100644 --- a/packages/studio-components/src/Icons/Lasso.tsx +++ b/packages/studio-components/src/Icons/Lasso.tsx @@ -1,12 +1,13 @@ import * as React from 'react'; - +import { theme } from 'antd'; interface IClusterProps { style?: React.CSSProperties; } const Lasso: React.FunctionComponent = props => { const { style = {} } = props; - const { fontSize = 18, color = '#000000' } = style; + const { token } = theme.useToken(); + const { fontSize = 16, color = token.colorText } = style; return ( = props => { - const { style } = props; - const { color = '#000', fontSize = '14px' } = style || {}; + const { style = {} } = props; + const { token } = theme.useToken(); + const { fontSize = 16, color = token.colorText } = style; return ( = ({ style = {} }) => { - const { color = '#B668B0', fontSize = '16px' } = style; + const { token } = theme.useToken(); + const { fontSize = 16, color = token.colorText } = style; return ( { - const { color, fontSize = '15px' } = style || {}; +import { theme } from 'antd'; +export default ({ style = {} }: { style?: React.CSSProperties }) => { + const { token } = theme.useToken(); + const { fontSize = 16, color = token.colorText } = style; + return ( = props => { - const { style } = props; - const { color = '#000', fontSize = '14px' } = style || {}; + const { style = {} } = props; + const { token } = theme.useToken(); + const { fontSize = 16, color = token.colorText } = style; return ( { +import { theme } from 'antd'; +export default ({ style = {} }: { style: React.CSSProperties }) => { + const { token } = theme.useToken(); + const { fontSize = 16, color = token.colorText } = style; return ( void; @@ -17,8 +17,7 @@ const Brush: React.FunctionComponent = props => { const { onSelect, title = 'Select nodes by box selection', placement = 'left' } = props; const { store, updateStore, id } = useContext(); const { graph } = store; - const { isLight } = useStudioProvier(); - const color = !isLight ? '#ddd' : '#000'; + const brushRef = useRef(null); const [isBrushActive, setIsBrushActive] = useState(false); const handleSelect = selectedNodes => { @@ -106,7 +105,7 @@ const Brush: React.FunctionComponent = props => { return ( <> }> - } type="text" onClick={handleClick} /> + } type="text" onClick={handleClick} /> {ReactDOM.createPortal(, trigetDOM)} diff --git a/packages/studio-graph/src/components/ContextMenu/index.tsx b/packages/studio-graph/src/components/ContextMenu/index.tsx index 02869bd4..71ff1e18 100644 --- a/packages/studio-graph/src/components/ContextMenu/index.tsx +++ b/packages/studio-graph/src/components/ContextMenu/index.tsx @@ -38,7 +38,7 @@ const ContextMenu: React.FunctionComponent = props => { selected: true, }, }; - draft.selectNodes = [node]; + // draft.selectNodes = [node]; }); }; const handleClear = () => { diff --git a/packages/studio-website/src/layouts/index.tsx b/packages/studio-website/src/layouts/index.tsx index 662a2915..5759ca28 100644 --- a/packages/studio-website/src/layouts/index.tsx +++ b/packages/studio-website/src/layouts/index.tsx @@ -52,12 +52,12 @@ export default function StudioLayout() { const graph_id = Utils.getSearchParams('graph_id'); if (graph_id === 'DRAFT_GRAPH') { return { - graphs: res, + graphs: res || [], graphId: 'DRAFT_GRAPH', }; } return { - graphs: res, + graphs: res || [], graphId: (matchGraph && matchGraph.id) || '', }; }); From 4e85c4a3b1a04b8d6880abeb75e59a8587c74683 Mon Sep 17 00:00:00 2001 From: pomelo-nwu Date: Wed, 18 Dec 2024 17:58:04 +0800 Subject: [PATCH 5/9] feat: update version --- pnpm-lock.yaml | 3 --- 1 file changed, 3 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 27779e42..54bd9d2a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -381,9 +381,6 @@ importers: '@antv/g2': specifier: ^5.2.8 version: 5.2.8 - '@graphscope/graphy-website': - specifier: workspace:* - version: link:../../examples/graphy '@graphscope/studio-components': specifier: workspace:* version: link:../studio-components From 85b30a6de549b2b2db2a84478686dca2a401c118 Mon Sep 17 00:00:00 2001 From: pomelo-nwu Date: Wed, 18 Dec 2024 18:40:59 +0800 Subject: [PATCH 6/9] feat: remove icon disabled style --- .../studio-graph-editor/src/button-controller/clear-canvas.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/studio-graph-editor/src/button-controller/clear-canvas.tsx b/packages/studio-graph-editor/src/button-controller/clear-canvas.tsx index f37509e2..4c4a7bf2 100644 --- a/packages/studio-graph-editor/src/button-controller/clear-canvas.tsx +++ b/packages/studio-graph-editor/src/button-controller/clear-canvas.tsx @@ -32,7 +32,7 @@ const ClearCanvas: React.FunctionComponent = props => { onClick={handleClear} style={style} type="text" - icon={} + icon={} > ); From 0fa671c36c5b33068f258cfc162d5ecc364110a9 Mon Sep 17 00:00:00 2001 From: pomelo-nwu Date: Wed, 18 Dec 2024 18:43:52 +0800 Subject: [PATCH 7/9] feat: update code --- examples/graphy/src/pages/explore/paper-reading/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/graphy/src/pages/explore/paper-reading/index.tsx b/examples/graphy/src/pages/explore/paper-reading/index.tsx index a8361588..f56f8736 100644 --- a/examples/graphy/src/pages/explore/paper-reading/index.tsx +++ b/examples/graphy/src/pages/explore/paper-reading/index.tsx @@ -118,7 +118,7 @@ const PaperReading: React.FunctionComponent = props => { - + From 3315be037cd5b77e3fb09f62e5e238335d84ebbd Mon Sep 17 00:00:00 2001 From: pomelo-nwu Date: Wed, 18 Dec 2024 18:48:40 +0800 Subject: [PATCH 8/9] feat: add typing --- packages/studio-driver/src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/studio-driver/src/index.ts b/packages/studio-driver/src/index.ts index ace751f4..86d17e46 100644 --- a/packages/studio-driver/src/index.ts +++ b/packages/studio-driver/src/index.ts @@ -4,3 +4,4 @@ import { queryGraph, cancelGraph, getDriver } from './queryGraph'; import { KuzuDriver } from './kuzu-wasm-driver'; export { CypherDriver, GremlinDriver, KuzuDriver, queryGraph, cancelGraph, getDriver }; +export type { QueryParams } from './queryGraph'; From a3f563c04043bd7b3dee7f6bc45d0f10827c27cf Mon Sep 17 00:00:00 2001 From: pomelo-nwu Date: Wed, 18 Dec 2024 18:50:51 +0800 Subject: [PATCH 9/9] feat: remove dump code --- .../src/pages/query/services.tsx | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/packages/studio-website/src/pages/query/services.tsx b/packages/studio-website/src/pages/query/services.tsx index 14622d95..d8d0fb9e 100644 --- a/packages/studio-website/src/pages/query/services.tsx +++ b/packages/studio-website/src/pages/query/services.tsx @@ -1,5 +1,5 @@ import { CypherSchemaData } from '@graphscope/studio-query'; -import { GremlinDriver, CypherDriver, queryGraph, cancelGraph } from '@graphscope/studio-driver'; +import { queryGraph, cancelGraph } from '@graphscope/studio-driver'; import type { QueryParams } from '@graphscope/studio-driver'; import type { IStudioQueryProps, IStatement } from '@graphscope/studio-query'; import localforage from 'localforage'; @@ -23,23 +23,6 @@ export async function deleteHistoryStatements(ids: string[]) { return; } -export const queryEndpoint = async (): Promise<{ - cypher_endpoint: string; - gremlin_endpoint: string; -}> => { - return fetch('/graph/endpoint') - .then(res => { - return res.json(); - }) - .then(res => res.data) - .catch(() => { - return { - cypher_endpoint: null, - gremlin_endpoint: null, - }; - }); -}; - export const queryInfo = async (id: string) => { const result = await ServiceApiFactory(undefined, window.COORDINATOR_URL) .getServiceStatusById(id)