diff --git a/.vscode/settings.json b/.vscode/settings.json index 30bf8c2..8b91359 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -7,5 +7,8 @@ "out": true // set this to false to include "out" folder in search results }, // Turn off tsc task auto detection since we have the necessary tasks as npm scripts - "typescript.tsc.autoDetect": "off" + "typescript.tsc.autoDetect": "off", + "cSpell.words": [ + "insertable" + ] } \ No newline at end of file diff --git a/commands/src/commands.ts b/commands/src/commands.ts index 4af084d..21498b7 100644 --- a/commands/src/commands.ts +++ b/commands/src/commands.ts @@ -7,20 +7,23 @@ export enum Commands { INFORM = "inform", // Table Panel Commands - CREATE_TABLE = 'createTable', - ROW_CLICKED = 'rowClicked', - HIDE_PROPERTIES = 'hideProperties', - ASK_FOR_VIEWPOINTS = 'askForViewpoints', - ASK_FOR_COMMANDS = 'askForCommands', - GENERATE_TABLE_DATA = 'generateTableData', - UPDATE_CM_STATE = 'updateCmState', - REFRESH_TABLE_DATA = 'refreshTableData', - GET_ELEMENT_RELATIONS = 'getElementRelations', + CREATE_TABLE = "createTable", + ROW_CLICKED = "rowClicked", + HIDE_PROPERTIES = "hideProperties", + ASK_FOR_VIEWPOINTS = "askForViewpoints", + ASK_FOR_COMMANDS = "askForCommands", + GENERATE_TABLE_DATA = "generateTableData", + UPDATE_CM_STATE = "updateCmState", + REFRESH_TABLE_DATA = "refreshTableData", + GET_ELEMENT_RELATIONS = "getElementRelations", // This differs to the GET_ELEMENT_RELATIONS because it grabs the predicate/verb and object instead of the subject of the selected element - GET_ELEMENT_RELATIONS_TOTAL = 'getElementRelationsTotal', - EXECUTE_CREATE_ELEMENTS = 'executeCreateElements', - EXECUTE_DELETE_ELEMENTS = 'executeDeleteElements', - CREATE_FCR = 'createFCR', + GET_ELEMENT_RELATIONS_TOTAL = "getElementRelationsTotal", + // This differs from GET_ELEMENT_RELANTIONS_TOTAL because it gets all relations in OML Model not just a selected element + GET_ALL_ELEMENT_RELATIONS = "getAllElementRelations", + GET_ALL_INSTANCE_CATEGORIES = "getAllInstanceCategories", + EXECUTE_CREATE_ELEMENTS = "executeCreateElements", + EXECUTE_DELETE_ELEMENTS = "executeDeleteElements", + CREATE_FCR = "createFCR", // Property Panel Commands ASK_FOR_PROPERTIES = "askForProperties", @@ -39,27 +42,29 @@ export enum Commands { PING_TRIPLESTORE_TASK = "pingTriplestoreTask", // Extension To Table Panel Commands - UPDATE_LOCAL_VALUE = 'updateLocalValue', - SEND_VIEWPOINTS = 'sendViewpoints', - SEND_COMMANDS = 'sendCommands', - OPEN_WIZARD = 'openWizard', - CREATE_FILTERED_DIAGRAM = 'createFilteredDiagram', - LOADED_PROPERTY_SHEET = 'loadedPropertySheet', - LOADED_TABLE_DATA = 'loadedTableData', - LOADED_ELEMENT_RELATIONS = 'loadedElementRelations', + UPDATE_LOCAL_VALUE = "updateLocalValue", + SEND_VIEWPOINTS = "sendViewpoints", + SEND_COMMANDS = "sendCommands", + OPEN_WIZARD = "openWizard", + CREATE_FILTERED_DIAGRAM = "createFilteredDiagram", + LOADED_PROPERTY_SHEET = "loadedPropertySheet", + LOADED_TABLE_DATA = "loadedTableData", + LOADED_ELEMENT_RELATIONS = "loadedElementRelations", // This differs to the LOADED_ELEMENT_RELATIONS because it loads the predicate/verb and object instead of the subject of the selected element - LOADED_ELEMENT_RELATIONS_TOTAL = 'loadedElementRelationsTotal', - DELETED_ELEMENTS = 'deletedElements', - CREATED_ELEMENT = 'createdElement', - CLONED_ELEMENTS = 'clonedElements', - SHOW_PROPERTIES = 'showProperties', + LOADED_ELEMENT_RELATIONS_TOTAL = "loadedElementRelationsTotal", + // This differs from LOADED_ELEMENT_RELATIONS_TOTAL because it loads all relations in OML Model not just a selected element + LOADED_ALL_ELEMENT_RELATIONS = "loadedAllElementRelations", + LOADED_ALL_INSTANCE_CATEGORIES = "loadedAllInstanceCategories", + DELETED_ELEMENTS = "deletedElements", + CREATED_ELEMENT = "createdElement", + CLONED_ELEMENTS = "clonedElements", + SHOW_PROPERTIES = "showProperties", // Context Menu to Triplestore. All crud commands - CREATE_QUERY = 'createQuery', - READ_QUERY = 'readQuery', - UPDATE_QUERY = 'updateQuery', - DELETE_QUERY = 'deleteQuery', - + CREATE_QUERY = "createQuery", + READ_QUERY = "readQuery", + UPDATE_QUERY = "updateQuery", + DELETE_QUERY = "deleteQuery", } export type CommandStructures = { @@ -94,6 +99,12 @@ export type CommandStructures = { payload: { webviewPath: string; iriArray: string[]; labelArray?: string[] }; wizardId?: string; }; + [Commands.GET_ALL_ELEMENT_RELATIONS]: { + payload: { webviewPath: string }; + }; + [Commands.GET_ALL_INSTANCE_CATEGORIES]: { + payload: { webviewPath: string }; + }; [Commands.EXECUTE_DELETE_ELEMENTS]: { payload: { webviewPath: string; IRIsToDelete: ITableData[] }; wizardId?: string; @@ -140,10 +151,10 @@ export type CommandStructures = { }; [Commands.UPDATE_LOCAL_VALUE]: {}; [Commands.SEND_VIEWPOINTS]: { - payload: { [filename: string]: Record | any[] } + payload: { [filename: string]: Record | any[] }; }; [Commands.SEND_COMMANDS]: { - payload: { [filename: string]: Record | any[] } + payload: { [filename: string]: Record | any[] }; }; [Commands.OPEN_WIZARD]: { payload: { @@ -180,6 +191,18 @@ export type CommandStructures = { relations?: Record[]; }; }; + [Commands.LOADED_ALL_ELEMENT_RELATIONS]: { + errorMessage?: string; + payload: { + relations?: string[]; + }; + }; + [Commands.LOADED_ALL_INSTANCE_CATEGORIES]: { + errorMessage?: string; + payload: { + instances?: string[]; + }; + }; [Commands.DELETED_ELEMENTS]: { errorMessage?: string; wizardId: string; @@ -205,21 +228,21 @@ export type CommandStructures = { }; [Commands.CREATE_QUERY]: { query: string; - selectedElements?: string[] + selectedElements?: string[]; }; [Commands.READ_QUERY]: { query: string; - selectedElements?: string[] + selectedElements?: string[]; }; [Commands.UPDATE_QUERY]: { query: string; - selectedElements?: string[] - before_parameters?: Object - after_parameters?: Object + selectedElements?: string[]; + before_parameters?: Object; + after_parameters?: Object; }; [Commands.DELETE_QUERY]: { query: string; - selectedElements?: string[] + selectedElements?: string[]; }; }; diff --git a/commands/src/tablePanelMessageHandler.ts b/commands/src/tablePanelMessageHandler.ts index 3cfffb9..c687b1d 100644 --- a/commands/src/tablePanelMessageHandler.ts +++ b/commands/src/tablePanelMessageHandler.ts @@ -6,6 +6,8 @@ import { SparqlClient } from "../../controller/src/sparql/SparqlClient"; import { getElementRelations } from "../../controller/src/sparql/data-manager/getElementRelations"; import { executeDeleteElements } from "../../controller/src/sparql/data-manager/executeDeleteElements"; import { getElementRelationsTotal } from "../../controller/src/sparql/data-manager/getElementRelationsTotal"; +import { getAllElementRelations } from "../../controller/src/sparql/data-manager/getAllElementRelations"; +import { getAllInstanceCategories } from "../../controller/src/sparql/data-manager/getAllInstanceCategories"; /** * Handles commands that are sent to a Editor (Table, Tree, or Diagram) @@ -101,7 +103,7 @@ export function handleTablePanelMessage( specificMessage.payload.labelArray ); break; - + case Commands.GET_ELEMENT_RELATIONS_TOTAL: specificMessage = message as CommandStructures[Commands.GET_ELEMENT_RELATIONS_TOTAL]; @@ -121,14 +123,30 @@ export function handleTablePanelMessage( const { webviewPath: delWebviewPath, IRIsToDelete = [] } = specificMessage.payload; - // Refer to the CommandStructures[Commands.EXECUTE_DELETE_ELEMENTS] to see how the parameters are structured - executeDeleteElements( - specificMessage.payload.webviewPath, - specificMessage.wizardId, - specificMessage.payload.IRIsToDelete, - ) + // Refer to the CommandStructures[Commands.EXECUTE_DELETE_ELEMENTS] to see how the parameters are structured + executeDeleteElements( + specificMessage.payload.webviewPath, + specificMessage.wizardId, + specificMessage.payload.IRIsToDelete + ); break; + case Commands.GET_ALL_ELEMENT_RELATIONS: + specificMessage = + message as CommandStructures[Commands.GET_ALL_ELEMENT_RELATIONS]; + const { webviewPath: relationWebviewPath } = specificMessage.payload; + + // Refer to the CommandStructures[Commands.GET_ALL_ELEMENT_RELATIONS] to see how the parameters are structured + getAllElementRelations(specificMessage.payload.webviewPath); + + case Commands.GET_ALL_INSTANCE_CATEGORIES: + specificMessage = + message as CommandStructures[Commands.GET_ALL_INSTANCE_CATEGORIES]; + const { webviewPath: instanceWebviewPath } = specificMessage.payload; + + // Refer to the CommandStructures[Commands.GET_ALL_ELEMENT_RELATIONS] to see how the parameters are structured + getAllInstanceCategories(specificMessage.payload.webviewPath); + case Commands.CREATE_FCR: specificMessage = message as CommandStructures[Commands.CREATE_FCR]; const fcrPayload = specificMessage.payload; diff --git a/controller/src/extension.ts b/controller/src/extension.ts index c648131..8940f36 100644 --- a/controller/src/extension.ts +++ b/controller/src/extension.ts @@ -514,5 +514,3 @@ export function activate(context: vscode.ExtensionContext) { function cloneSelectedRows(context: Record) { throw new Error("Function not implemented."); } - - diff --git a/controller/src/sparql/data-manager/getAllElementRelations.ts b/controller/src/sparql/data-manager/getAllElementRelations.ts new file mode 100644 index 0000000..5baf823 --- /dev/null +++ b/controller/src/sparql/data-manager/getAllElementRelations.ts @@ -0,0 +1,59 @@ +import * as vscode from "vscode"; +import { TablePanel } from "../../panels/TablePanel"; +import { Commands } from "../../../../commands/src/commands"; +import { SparqlClient } from "../SparqlClient"; +import { getAllRelations } from "../queries/getAllRelations"; + +/** + * This SPARQL query gets all distinct relations from a given OML model and sends them through a controller command. + * + * @remarks + * For more information on OML relations please refer to the official documentation found {@link http://www.opencaesar.io/oml/#Relations | here} + * + * For more information on the postMessage controller command please refer to the official documentation found {@link https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage | here} + * + * @returns + * + */ +export const getAllElementRelations = async ( + webviewPath: string +): Promise => { + try { + const relations_query = getAllRelations(); + const relations_data = await SparqlClient(relations_query, "query"); + + // Get all relation values + const relations: string[] = relations_data.map( + (relation: Record) => { + // We only want values because the record will look like verb: relation_value + // We're only grabbing the verb from the key value pair + // If the relation.verb is not undefined then return it else return a blank string + return relation.verb.split("/").pop() ?? ""; + } + ); + + // Send data to current webview + TablePanel.currentPanels.get(webviewPath)?.sendMessage({ + command: Commands.LOADED_ALL_ELEMENT_RELATIONS, + payload: { + relations: relations, + }, + }); + } catch (error) { + if (error instanceof Error) { + vscode.window.showErrorMessage(`Error: ${error.message}`); + TablePanel.currentPanels.get(webviewPath)?.sendMessage({ + command: Commands.LOADED_ALL_ELEMENT_RELATIONS, + payload: {}, + errorMessage: `Error: ${error.message}`, + }); + } else { + vscode.window.showErrorMessage(`An unknown error occurred: ${error}`); + TablePanel.currentPanels.get(webviewPath)?.sendMessage({ + command: Commands.LOADED_ALL_ELEMENT_RELATIONS, + payload: {}, + errorMessage: `An unknown error occurred: ${error}`, + }); + } + } +}; diff --git a/controller/src/sparql/data-manager/getAllInstanceCategories.ts b/controller/src/sparql/data-manager/getAllInstanceCategories.ts new file mode 100644 index 0000000..05cd972 --- /dev/null +++ b/controller/src/sparql/data-manager/getAllInstanceCategories.ts @@ -0,0 +1,57 @@ +import * as vscode from "vscode"; +import { TablePanel } from "../../panels/TablePanel"; +import { Commands } from "../../../../commands/src/commands"; +import { SparqlClient } from "../SparqlClient"; +import { getAllInstances } from "../queries/getAllInstances"; + +/** + * This SPARQL query gets all distinct instances from a given OML model and sends them through a controller command. + * + * @remarks + * For more information on OML instances please refer to the official documentation found {@link http://www.opencaesar.io/oml/#Instances-LR | here} + * + * For more information on the postMessage controller command please refer to the official documentation found {@link https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage | here} + * + * @returns + * + */ +export const getAllInstanceCategories = async ( + webviewPath: string +): Promise => { + try { + const instance_query = getAllInstances(); + const instance_data = await SparqlClient(instance_query, "query"); + // Get all instance values + const instances: string[] = instance_data.map( + (instance: Record) => { + // We only want values because the record will look like verb: instance_value + // We're only grabbing the verb from the key value pair + // If the instance.verb is not undefined then return it else return a blank string + return instance.subject.split("/").pop() ?? ""; + } + ); + // Send data to current webview + TablePanel.currentPanels.get(webviewPath)?.sendMessage({ + command: Commands.LOADED_ALL_INSTANCE_CATEGORIES, + payload: { + instances: instances, + }, + }); + } catch (error) { + if (error instanceof Error) { + vscode.window.showErrorMessage(`Error: ${error.message}`); + TablePanel.currentPanels.get(webviewPath)?.sendMessage({ + command: Commands.LOADED_ALL_INSTANCE_CATEGORIES, + payload: {}, + errorMessage: `Error: ${error.message}`, + }); + } else { + vscode.window.showErrorMessage(`An unknown error occurred: ${error}`); + TablePanel.currentPanels.get(webviewPath)?.sendMessage({ + command: Commands.LOADED_ALL_INSTANCE_CATEGORIES, + payload: {}, + errorMessage: `An unknown error occurred: ${error}`, + }); + } + } +}; diff --git a/controller/src/sparql/queries/getAllInstances.ts b/controller/src/sparql/queries/getAllInstances.ts new file mode 100644 index 0000000..80e8b6c --- /dev/null +++ b/controller/src/sparql/queries/getAllInstances.ts @@ -0,0 +1,21 @@ +/** + * This SPARQL query gets all distinct instances from a given OML model + * + * @remarks + * For more information on OML instances please refer to the official documentation found {@link http://www.opencaesar.io/oml/#Instances-LR | here} + * + * For more information on SPARQL query `regex` and `str` please refer to the official documentation found {@link https://www.w3.org/TR/sparql11-query/ | here} + * + * @returns SPARQL select query string + * + */ + +export function getAllInstances(): string { + return `SELECT DISTINCT ?subject + WHERE { + ?subject ?verb ?object . + FILTER regex(str(?subject), "vocabulary", "i") + FILTER regex(str(?object), "Concept", "i") + } + ORDER BY ?subject`; +} diff --git a/controller/src/sparql/queries/getAllRelations.ts b/controller/src/sparql/queries/getAllRelations.ts new file mode 100644 index 0000000..90dc235 --- /dev/null +++ b/controller/src/sparql/queries/getAllRelations.ts @@ -0,0 +1,21 @@ +/** + * This SPARQL query gets all relations from a given OML model + * + * @remarks + * For more information on OML relations please refer to the official documentation found {@link http://www.opencaesar.io/oml/#Relations | here} + * + * For more information on SPARQL query `regex` and `str` please refer to the official documentation found {@link https://www.w3.org/TR/sparql11-query/ | here} + * + * @returns SPARQL select query string + * + */ + +export function getAllRelations(): string { + return `SELECT DISTINCT ?verb + WHERE { + ?subject ?verb ?object . + FILTER regex(str(?subject), "description", "i") + FILTER regex(str(?verb), "vocabulary", "i") + } + ORDER BY ?verb`; +} diff --git a/controller/src/sparql/queries/getIriRelations.ts b/controller/src/sparql/queries/getIriRelations.ts index b8ab015..4233096 100644 --- a/controller/src/sparql/queries/getIriRelations.ts +++ b/controller/src/sparql/queries/getIriRelations.ts @@ -1,13 +1,13 @@ /** - * This SPARQL query gets all relations for a given IRI which is a owl:NamedIndividual + * This SPARQL query gets all relations for a given IRI which is a owl:NamedIndividual * * @remarks * For more information on OML relations please refer to the official documentation found {@link http://www.opencaesar.io/oml/#Relations | here} - * + * * For more information on SPARQL query `regex` and `str` please refer to the official documentation found {@link https://www.w3.org/TR/sparql11-query/ | here} * * @param IRI of the element. To learn more about IRIs go to this {@link https://www.oxfordsemantic.tech/faqs/what-is-an-iri-what-does-iri-mean | doc} - * + * * @returns SPARQL select query string * */ diff --git a/package-lock.json b/package-lock.json index e35d79f..1c108b4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "SEE LICENSE IN LICENSE.rmd", "dependencies": { "@actions/core": "^1.10.1", + "@atlaskit/pragmatic-drag-and-drop": "^1.1.9", "@comunica/query-sparql": "^2.10.0", "@emotion/react": "^11.1.5", "@emotion/styled": "^11.1.5", @@ -63,13 +64,15 @@ "stream": "^0.0.2", "tailwindcss": "^3.3.2", "terser": "^5.17.7", + "tiny-invariant": "^1.3.3", "tree-model-improved": "^2.0.1", "ts-jest": "^29.1.1", "typescript": "^4.9.5", "use-resize-observer": "^9.1.0", "uuid": "^9.0.0", "uuidv4": "^6.2.13", - "vscode-gradle": "^3.10.1" + "vscode-gradle": "^3.10.1", + "yarn": "^1.22.22" }, "engines": { "vscode": "^1.78.2" @@ -124,6 +127,16 @@ "node": ">=6.0.0" } }, + "node_modules/@atlaskit/pragmatic-drag-and-drop": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@atlaskit/pragmatic-drag-and-drop/-/pragmatic-drag-and-drop-1.1.9.tgz", + "integrity": "sha512-HZkT0mStizzneobq7vINEnEsm7DTu70gaEu0HHSNKv6lkX4tI0h1HoAyDUZqH3xFc5nuhvjdFxygZRzMhs5w+g==", + "dependencies": { + "@babel/runtime": "^7.0.0", + "bind-event-listener": "^3.0.0", + "raf-schd": "^4.0.3" + } + }, "node_modules/@azure/abort-controller": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-1.1.0.tgz", @@ -6093,9 +6106,9 @@ } }, "node_modules/@grpc/grpc-js": { - "version": "1.8.16", - "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.8.16.tgz", - "integrity": "sha512-Nvlq4V7XQmdRVDGgecR8ZPPCeY+uH1LhzbC+QxklwAahpQlq8YLsiOQgfkub9FiakRiohaDy361xqlTLkq9EHw==", + "version": "1.8.22", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.8.22.tgz", + "integrity": "sha512-oAjDdN7fzbUi+4hZjKG96MR6KTEubAeMpQEb+77qy+3r0Ua5xTFuie6JOLr4ZZgl5g+W5/uRTS2M1V8mVAFPuA==", "dependencies": { "@grpc/proto-loader": "^0.7.0", "@types/node": ">=12.12.47" @@ -10115,6 +10128,11 @@ "node": ">=8" } }, + "node_modules/bind-event-listener": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bind-event-listener/-/bind-event-listener-3.0.0.tgz", + "integrity": "sha512-PJvH288AWQhKs2v9zyfYdPzlPqf5bXbGMmhmUIY9x4dAUGIWgomO771oBQNwJnMQSnUIXhKu6sgzpBRXTlvb8Q==" + }, "node_modules/bl": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", @@ -10179,11 +10197,11 @@ } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" @@ -11703,9 +11721,9 @@ } }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dependencies": { "to-regex-range": "^5.0.1" }, @@ -17173,6 +17191,11 @@ } ] }, + "node_modules/raf-schd": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.3.tgz", + "integrity": "sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==" + }, "node_modules/rc": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", @@ -19007,6 +19030,11 @@ "xtend": "~4.0.1" } }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==" + }, "node_modules/tmp": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", @@ -19903,6 +19931,19 @@ "node": ">=12" } }, + "node_modules/yarn": { + "version": "1.22.22", + "resolved": "https://registry.npmjs.org/yarn/-/yarn-1.22.22.tgz", + "integrity": "sha512-prL3kGtyG7o9Z9Sv8IPfBNrWTDmXB4Qbes8A9rEzt6wkJV8mUvoirjU0Mp3GGAU06Y0XQyA3/2/RQFVuK7MTfg==", + "hasInstallScript": true, + "bin": { + "yarn": "bin/yarn.js", + "yarnpkg": "bin/yarn.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/yauzl": { "version": "2.10.0", "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", diff --git a/package.json b/package.json index b68037e..caf0c57 100644 --- a/package.json +++ b/package.json @@ -264,6 +264,7 @@ }, "dependencies": { "@actions/core": "^1.10.1", + "@atlaskit/pragmatic-drag-and-drop": "^1.1.9", "@comunica/query-sparql": "^2.10.0", "@emotion/react": "^11.1.5", "@emotion/styled": "^11.1.5", @@ -317,13 +318,15 @@ "stream": "^0.0.2", "tailwindcss": "^3.3.2", "terser": "^5.17.7", + "tiny-invariant": "^1.3.3", "tree-model-improved": "^2.0.1", "ts-jest": "^29.1.1", "typescript": "^4.9.5", "use-resize-observer": "^9.1.0", "uuid": "^9.0.0", "uuidv4": "^6.2.13", - "vscode-gradle": "^3.10.1" + "vscode-gradle": "^3.10.1", + "yarn": "^1.22.22" }, "overrides": { "react-scripts": { diff --git a/view/src/components/Diagram/Diagram.tsx b/view/src/components/Diagram/Diagram.tsx index 40ec116..f61f0e4 100644 --- a/view/src/components/Diagram/Diagram.tsx +++ b/view/src/components/Diagram/Diagram.tsx @@ -25,6 +25,14 @@ import ReactFlow, { getConnectedEdges, } from "reactflow"; +import { + InsertPanel, + InsertPane, + InstanceInsertItem, + RelationInsertItem, + DefaultRelationIcon, +} from "./InsertPanel"; + // Icons import { IconDownload } from "@nasa-jpl/react-stellar"; import LockIcon from "./Icons/Lock"; @@ -53,21 +61,25 @@ const nodeTypes = { function Diagram({ initData, + instances, + relations, webviewPath, hasFilter, - clearFilter = () => {}, + clearFilter = () => { }, modelCommands, layout, // TODO: Use onNodeSelected while node is highlighted/selected - onNodeSelected = () => {}, - onNodeClicked = () => {}, - onNodeDoubleClicked = () => {}, + onNodeSelected = () => { }, + onNodeClicked = () => { }, + onNodeDoubleClicked = () => { }, }: { initData: { nodes: ITableData[]; edges: Edge[]; legendItems: LegendItem[]; }; + instances: string[]; + relations: string[]; webviewPath: string; hasFilter: boolean; clearFilter: Function; @@ -91,6 +103,7 @@ function Diagram({ const { fitView } = useReactFlow(); const { isInteractive, setInteractivity, toggleInteractivity } = useCanvasInteractivity(); + const [showDownloadMenu, setShowDownloadMenu] = useState(true); // Using negative logic here // FIXME: useOnSelectionChange occurs after a selection occurs and will continously running when clicking a node or edge // useOnSelectionChange({ @@ -473,6 +486,21 @@ function Diagram({ return dropDownOptions; }; + /** + * This function toggles the download menu. + * + * @remarks + * + * @param + * + */ + const toggleDownloadMenu = () => { + setShowDownloadMenu(!showDownloadMenu); + }; + + // This constant sets the control button dropdown indicator arrow size. + const arrowIconSize = 16; + return (
{initData.legendItems.length > 0 && ( - + )} - + {/*
fitView()}> Fit View @@ -536,7 +564,7 @@ function Diagram({ ) : ( - /* TODO: Implement Add Filter functionality */ + //TODO: Implement Add Filter functionality {}}> Add Filter @@ -545,7 +573,8 @@ function Diagram({ {selectedAutoLayout(autoLayout)} {selectedAlgorithmLayout(algorithmLayout)}
-
+
*/} + {/* Implemented custom interactive button to avoid disabling selection in diagram */} : } -
- - PNG -
-
- -
- - SVG + +
+ {/* TODO: Style so it looks like icon.png */} + {/*
+ */} + + +
+ + {!showDownloadMenu && ( +
+ +
+ + PNG + +
+
+ +
+ + SVG + +
+
+
+ )} + + + + {instances.map((instance: string) => { + var split = instance.split("#"); + return ( + + ); + })} + + +
+ {relations.map((relation: string) => { + var split = relation.split("#"); + return ( + + ); + })} +
+
+
+
{/* Check if rightClick and if layout.contextMenu exists */} - {rightClick && layout.contextMenu &&( + {rightClick && layout.contextMenu && ( +); + +// TODO: Wrap text so that it fits within node. Resize dynamically? +/* + * React component `InstanceInsertItem` is a visual representation of an insertable instance that a user can drag from the instance pane of the instance panel and drop into the diagram view. + * + * @param {string} label - String label for the instance + * @param {string} style - TailwindCSS style to specify style attributes for the InstanceNode + */ +export const InstanceInsertItem: React.FC = ({ + instanceLabel, + categoryLabel, + style, +}) => { + // TODO: Use model's node color property instead of hard-coded color + // TODO: handle dropping object after letting go of the mouse + const ref = useRef(null); // ref for dragging + const defaultNodeStyle = "bg-[#ff0000]"; // red background by default + + // Enable dragging of element + useEffect(() => { + const el = ref.current; + invariant(el); + return draggable({ element: el }); + }, []); + + return ( +
+
+
+ +

{categoryLabel}

+

{instanceLabel}

+
+
+
+
+ ); +}; + +// TODO: Dynamically resize pane/panel on relation name size +export const RelationInsertItem: React.FC = ({ + categoryLabel, + relationLabel, + icon, +}) => { + // TODO: Find a better way to handle icons + const ref = useRef(null); + + useEffect(() => { + const el = ref.current; + invariant(el); + return draggable({ element: el }); + }, []); + + return ( +
+
+
+
+
{icon}
+
+
+ {categoryLabel} +
+
+ {relationLabel} +
+
+
+
+
+
+
+
+ ); +}; + +export const InsertPane: React.FC = ({ label, children }) => { + // Refer to http://www.opencaesar.io/oml-tutorials/#tutorial1-create-oml-vocabulary + return ( +
+
+ {label} +
+
+ {children} +
+
+ ); +}; + +export const InsertPanel: React.FC = ({ children }) => { + return ( +
+ {children} +
+ ); +}; + +export default InsertPanel; diff --git a/view/src/components/Diagram/diagramUtils.ts b/view/src/components/Diagram/diagramUtils.ts index ae4d4d9..5445fa7 100644 --- a/view/src/components/Diagram/diagramUtils.ts +++ b/view/src/components/Diagram/diagramUtils.ts @@ -774,3 +774,20 @@ export const getLayoutedElements = ( }) .catch(console.error); }; + +export const combineByCategory = (entities: string[]) => { + let combined: Record = {} // stores the combined entities by category + entities.forEach(entity => { + var split = entity.split("#"); + var value = split.pop() ?? ""; + var category = split.pop() ?? ""; + + if (!combined[category]) { // key is not already in combined + combined[category] = [] // initialize key + } + + combined[category].push(value) // append value to the key's list of entities + }) + + return combined +} \ No newline at end of file diff --git a/view/src/pages/DiagramView.tsx b/view/src/pages/DiagramView.tsx index 3ebd8a8..397511c 100644 --- a/view/src/pages/DiagramView.tsx +++ b/view/src/pages/DiagramView.tsx @@ -27,6 +27,8 @@ const DiagramView: React.FC = () => { }>({ iris: [], filterObject: null }); const [errorMessage, setErrorMessage] = useState(""); const [isLoading, setIsLoading] = useState(true); + const [instances, setInstances] = useState([""]); + const [relations, setRelations] = useState([""]); useEffect(() => { // Only start fetching data when context is loaded @@ -71,6 +73,20 @@ const DiagramView: React.FC = () => { setDiagramLayout(layout); setWebviewPath(webviewPath); + postMessage({ + command: Commands.GET_ALL_ELEMENT_RELATIONS, + payload: { + webviewPath: webviewPath, + }, + }); + + postMessage({ + command: Commands.GET_ALL_INSTANCE_CATEGORIES, + payload: { + webviewPath: webviewPath, + }, + }); + postMessage({ command: Commands.GENERATE_TABLE_DATA, payload: { @@ -178,6 +194,16 @@ const DiagramView: React.FC = () => { command: Commands.REFRESH_TABLE_DATA, }); break; + + case Commands.LOADED_ALL_ELEMENT_RELATIONS: + specificMessage = + message as CommandStructures[Commands.LOADED_ALL_ELEMENT_RELATIONS]; + setRelations(message.payload.relations); + + case Commands.LOADED_ALL_INSTANCE_CATEGORIES: + specificMessage = + message as CommandStructures[Commands.LOADED_ALL_INSTANCE_CATEGORIES]; + setInstances(message.payload.instances); } }; window.addEventListener("message", handler); @@ -224,17 +250,17 @@ const DiagramView: React.FC = () => { @remarks This method uses the {@link https://react.dev/reference/react/useCallback | useCallback} React hook @param node - The node and its data that is clicked */ - const handleDoubleClickNode = useCallback((node: ITableData) => { - // If there is a iri in the node's data then execute the command to open the modal. - if (node.data.iri) { - openWizard("RelationElementsWizard", { iriArray: [node.data.iri] }); - // UI indication to users - postMessage({ - command: Commands.INFORM, - text: "Opening Relations Wizard...", - }); - }; - }, []); + const handleDoubleClickNode = useCallback((node: ITableData) => { + // If there is a iri in the node's data then execute the command to open the modal. + if (node.data.iri) { + openWizard("RelationElementsWizard", { iriArray: [node.data.iri] }); + // UI indication to users + postMessage({ + command: Commands.INFORM, + text: "Opening Relations Wizard...", + }); + } + }, []); const refreshData = () => { setIsLoading(true); @@ -266,6 +292,8 @@ const DiagramView: React.FC = () => { 0} clearFilter={() => setFilter({ iris: [], filterObject: null })} diff --git a/view/tests/diagramUtils/combineByCategoty.test.js b/view/tests/diagramUtils/combineByCategoty.test.js new file mode 100644 index 0000000..4e533bf --- /dev/null +++ b/view/tests/diagramUtils/combineByCategoty.test.js @@ -0,0 +1,10 @@ +import { combineByCategory } from "./diagramUtils"; + +test('combines an array of entries "#"into a Record of {"": [...]}', () => { + const input = ["1#one", "1#uno", "2#deux", "2#zwei"] + const result = combineByCategory(input) // using default delimiter '#' + expect(result["1"]).toContain("one") + expect(result["1"]).toContain("uno") + expect(result["2"]).toContain("zwei") + expect(result["2"]).toContain("deux") +}); \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index bc418fc..6feef99 100644 --- a/yarn.lock +++ b/yarn.lock @@ -31,6 +31,15 @@ "@jridgewell/gen-mapping" "^0.3.0" "@jridgewell/trace-mapping" "^0.3.9" +"@atlaskit/pragmatic-drag-and-drop@^1.1.9": + "integrity" "sha512-HZkT0mStizzneobq7vINEnEsm7DTu70gaEu0HHSNKv6lkX4tI0h1HoAyDUZqH3xFc5nuhvjdFxygZRzMhs5w+g==" + "resolved" "https://registry.npmjs.org/@atlaskit/pragmatic-drag-and-drop/-/pragmatic-drag-and-drop-1.1.9.tgz" + "version" "1.1.9" + dependencies: + "@babel/runtime" "^7.0.0" + "bind-event-listener" "^3.0.0" + "raf-schd" "^4.0.3" + "@azure/abort-controller@^1.0.0": "integrity" "sha512-TrRLIoSQVzfAJX9H1JeFjzAoDGcoK1IYX1UImfceTZpsyYfWr09Ss1aHW1y5TrrR3iq6RZLBwJ3E24uwPhwahw==" "resolved" "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-1.1.0.tgz" @@ -3338,9 +3347,9 @@ "@floating-ui/dom" "^1.2.7" "@grpc/grpc-js@^1.2.10": - "integrity" "sha512-Nvlq4V7XQmdRVDGgecR8ZPPCeY+uH1LhzbC+QxklwAahpQlq8YLsiOQgfkub9FiakRiohaDy361xqlTLkq9EHw==" - "resolved" "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.8.16.tgz" - "version" "1.8.16" + "integrity" "sha512-oAjDdN7fzbUi+4hZjKG96MR6KTEubAeMpQEb+77qy+3r0Ua5xTFuie6JOLr4ZZgl5g+W5/uRTS2M1V8mVAFPuA==" + "resolved" "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.8.22.tgz" + "version" "1.8.22" dependencies: "@grpc/proto-loader" "^0.7.0" "@types/node" ">=12.12.47" @@ -5303,6 +5312,11 @@ "resolved" "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz" "version" "2.2.0" +"bind-event-listener@^3.0.0": + "integrity" "sha512-PJvH288AWQhKs2v9zyfYdPzlPqf5bXbGMmhmUIY9x4dAUGIWgomO771oBQNwJnMQSnUIXhKu6sgzpBRXTlvb8Q==" + "resolved" "https://registry.npmjs.org/bind-event-listener/-/bind-event-listener-3.0.0.tgz" + "version" "3.0.0" + "bl@^4.0.3": "integrity" "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==" "resolved" "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz" @@ -5326,11 +5340,11 @@ "concat-map" "0.0.1" "braces@^3.0.2", "braces@~3.0.2": - "integrity" "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==" - "resolved" "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz" - "version" "3.0.2" + "integrity" "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==" + "resolved" "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz" + "version" "3.0.3" dependencies: - "fill-range" "^7.0.1" + "fill-range" "^7.1.1" "browserslist@^4.21.5", "browserslist@^4.21.9", "browserslist@>= 4.21.0": "integrity" "sha512-M0MFoZzbUrRU4KNfCrDLnvyE7gub+peetoTid3TBIqtunaDJyXlwhakT+/VkvSXcfIzFfK/nkCs4nmyTmxdNSg==" @@ -6310,10 +6324,10 @@ "sparqlxml-parse" "^2.1.1" "stream-to-string" "^1.1.0" -"fill-range@^7.0.1": - "integrity" "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==" - "resolved" "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz" - "version" "7.0.1" +"fill-range@^7.1.1": + "integrity" "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==" + "resolved" "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz" + "version" "7.1.1" dependencies: "to-regex-range" "^5.0.1" @@ -8350,6 +8364,11 @@ "resolved" "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz" "version" "1.2.3" +"raf-schd@^4.0.3": + "integrity" "sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==" + "resolved" "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.3.tgz" + "version" "4.0.3" + "rc-motion@^2.0.1": "integrity" "sha512-2xUvo8yGHdOHeQbdI8BtBsCIrWKchEmFEIskf0nmHtJsou+meLd/JE+vnvSX2JxcBrJtXY2LuBpxAOxrbY/wMQ==" "resolved" "https://registry.npmjs.org/rc-motion/-/rc-motion-2.7.3.tgz" @@ -9553,6 +9572,11 @@ "readable-stream" "~2.3.6" "xtend" "~4.0.1" +"tiny-invariant@^1.3.3": + "integrity" "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==" + "resolved" "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz" + "version" "1.3.3" + "tmp@^0.2.1": "integrity" "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==" "resolved" "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz" @@ -10047,6 +10071,11 @@ "y18n" "^5.0.5" "yargs-parser" "^21.1.1" +"yarn@^1.22.22": + "integrity" "sha512-prL3kGtyG7o9Z9Sv8IPfBNrWTDmXB4Qbes8A9rEzt6wkJV8mUvoirjU0Mp3GGAU06Y0XQyA3/2/RQFVuK7MTfg==" + "resolved" "https://registry.npmjs.org/yarn/-/yarn-1.22.22.tgz" + "version" "1.22.22" + "yauzl@^2.3.1": "integrity" "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==" "resolved" "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz"