Skip to content

Commit

Permalink
feat(rdfInstanceViewer): Add literals
Browse files Browse the repository at this point in the history
Switched out libraries from rdf-parse to n3, this allows
better parsing and writing of rdf

Added logic to add and remove literals.

Store all quads in an N3 store

Fixed logic to work with new store and fixed tests

The entire graph is generated via the RDF
  • Loading branch information
davec504 committed May 9, 2024
1 parent f2cbf3d commit 71893f8
Show file tree
Hide file tree
Showing 25 changed files with 976 additions and 510 deletions.
2 changes: 1 addition & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Telicent Ontology</title>
<title>Telicent Instance</title>
</head>
<body>
<div id="root"></div>
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,15 @@
"@telicent-oss/ds": "^0.0.3",
"@telicent-oss/ontologyservice": "^0.0.6",
"@telicent-oss/rdfservice": "^0.0.6",
"@types/n3": "^1.16.4",
"classnames": "^2.5.1",
"dagre": "^0.8.5",
"fontawesome": "^5.6.3",
"inversify": "^6.0.2",
"mobx": "^6.12.1",
"mobx-react": "^9.1.0",
"monaco-editor": "^0.47.0",
"n3": "^1.17.3",
"rdf-parse": "^2.3.3",
"react": "^18.2.0",
"react-dom": "^18.2.0",
Expand Down
2 changes: 1 addition & 1 deletion public/favicon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 0 additions & 1 deletion public/vite.svg

This file was deleted.

54 changes: 32 additions & 22 deletions src/Components/Diagram/Diagram.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,27 @@ import { RdfPanelProps } from '../../types'
import 'reactflow/dist/style.css'
import ClassInstanceNode from '../../lib/CustomNode/ClassInstanceNode'
import { observer } from 'mobx-react'
import CustomEdge from '../../lib/CustomEdge/CustomEdge'
import getLayoutNodes from './Layout'
import ObjectProperty from '../../lib/ObjectProperty/ObjectProperty'
import { NodeDialog } from './NodeDialog'
import { EdgeDialog } from './EdgeDialog'
import { LiteralDialog } from './LiteralDialog'
import DataTypeProperty from '../../lib/DataTypeProperty/DataTypeProperty'

const nodeTypes = {
classInstanceNode: ClassInstanceNode,
dataTypeProperty: DataTypeProperty
}

const edgeTypes = {
relationshipEdge: CustomEdge
relationshipEdge: ObjectProperty
}

const DiagramComponent: FC<RdfPanelProps> = observer((props: RdfPanelProps) => {
const [nodes, setNodes, onNodesChange] = useNodesState(props.presenter.viewModel.nodes)
const [nodes, setNodes, onNodesChange] = useNodesState([])
const [edges, setEdges, onEdgesChange] = useEdgesState([])
const [open, setOpen] = useState(false)
const [edgeDialogOpen, setEdgeDialogOpen] = useState(false)
const [literalDialogOpen, setLiteralDialogOpen] = useState(false)

useEffect(() => {
if (!props.presenter) {
Expand All @@ -34,11 +37,9 @@ const DiagramComponent: FC<RdfPanelProps> = observer((props: RdfPanelProps) => {
}, [])

useEffect(() => {
const { nodes, edges } = getLayoutNodes(props.presenter.viewModel.nodes, props.presenter.viewModel.edges)

setNodes(nodes)
setEdges(edges)
}, [props.presenter.viewModel.nodes, props.presenter.viewModel.edges, setEdges, setNodes])
setNodes(props.presenter.diagram.nodes)
setEdges(props.presenter.diagram.edges)
}, [props.presenter.diagram.nodes, props.presenter.diagram.edges, setEdges, setNodes])

const onDragOver = useCallback((event: React.DragEvent<HTMLDivElement>) => {
event.preventDefault()
Expand Down Expand Up @@ -72,26 +73,21 @@ const DiagramComponent: FC<RdfPanelProps> = observer((props: RdfPanelProps) => {
const onCloseDialog = () => {
setOpen(false)
setEdgeDialogOpen(false)
setLiteralDialogOpen(false)
}

// NOTE: deleting a node will automatically trigger
// the edges delete too, this is a reactflow feature
const onNodesDelete = (nodes: Array<Node>) => {
props.presenter.deleteNodeAndAssociatedEdges(nodes[0].id)
props.presenter.deleteNode(nodes[0].id)
}

const onEdgesDelete = (edges: Array<Edge>) => {
console.log({ edges })
if (edges.length === 0) return
const edge = edges[0]
if (!edge.label || !edge.target || !edge.source) {
console.warn("cannot delete edge, missing information")
return
}

props.presenter.deleteEdge(edge.source, edge.target, edge.label as string)
props.presenter.deleteEdges(edges)
}

const onSubmitNode = (prefix: string, name: string): void => {
props.presenter.newNodeName = `${prefix}${name}`
props.presenter.newNodeName = `${prefix}:${name}`
props.presenter.lastSelectedPrefix = prefix
props.presenter.addNode()

Expand All @@ -104,6 +100,18 @@ const DiagramComponent: FC<RdfPanelProps> = observer((props: RdfPanelProps) => {
setEdgeDialogOpen(false)
}

const onSubmitLiteral = (edgeType: string, attributeValue: string): void => {
props.presenter.addLiteral(edgeType, attributeValue)
setLiteralDialogOpen(false)
}


const onNodeContextMenu = (event: React.MouseEvent, node: Node): void => {
props.presenter.selectedNode = node.data.id
event.preventDefault()
setLiteralDialogOpen(true)
}

return (<>
<ReactFlow
fitView
Expand All @@ -113,6 +121,7 @@ const DiagramComponent: FC<RdfPanelProps> = observer((props: RdfPanelProps) => {
onConnect={onConnect}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onNodeContextMenu={onNodeContextMenu}
nodeTypes={nodeTypes}
onDragOver={onDragOver}
onDrop={onDrop}
Expand All @@ -126,8 +135,9 @@ const DiagramComponent: FC<RdfPanelProps> = observer((props: RdfPanelProps) => {
} />
<Background variant={BackgroundVariant.Dots} gap={12} size={1} />
</ReactFlow>
{open && <NodeDialog title="Add node details:" onClose={onCloseDialog} options={Object.keys(props.presenter.viewModel.prefixes)} onSubmit={onSubmitNode} lastSelectedPrefix={props.presenter.viewModel.lastSelectedPrefix} />}
{edgeDialogOpen && <EdgeDialog title="Add edge details:" onClose={onCloseDialog} options={props.presenter.viewModel.relationships} onSubmit={onSubmitEdge} />}
{open && <NodeDialog title="Add Class Instance:" onClose={onCloseDialog} options={Object.keys(props.presenter.viewModel.prefixes)} onSubmit={onSubmitNode} lastSelectedPrefix={props.presenter.viewModel.lastSelectedPrefix.toString()} />}
{edgeDialogOpen && <EdgeDialog title="Add ObjectProperty:" onClose={onCloseDialog} options={props.presenter.viewModel.relationships} onSubmit={onSubmitEdge} />}
{literalDialogOpen && <LiteralDialog title="Add attribute:" onClose={onCloseDialog} options={props.presenter.viewModel.dataTypes} onSubmit={onSubmitLiteral} lastSelected={props.presenter.viewModel.lastSelectedLiteral} />}
</>
)

Expand Down
2 changes: 1 addition & 1 deletion src/Components/Diagram/EdgeDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export const EdgeDialog: FC<EdgeDialogProps> = ({ options, onClose, title, onSub
<DialogBox onClose={onClose} title={title}>
<div className="dark:text-whiteSmoke flex flex-col gap-y-8 rounded">
<div className='flex gap-x-2'>
<TeliAutocomplete options={options} width={150} label="Edge Type:" onChange={onChangePrefix} />
<TeliAutocomplete options={options} width={150} label="Object Property:" onChange={onChangePrefix} />
</div>
<div className='flex justify-end w-full'>
<TeliButton onClick={onHandleSubmit} variant="secondary" disabled={!selectedEdgeType}>Submit</TeliButton>
Expand Down
65 changes: 65 additions & 0 deletions src/Components/Diagram/LiteralDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import React, { FC, useEffect, useState } from 'react'
import { TeliAutocomplete, TeliButton, TeliTextField } from "@telicent-oss/ds"
import { DialogBox } from '../../lib/DialogBox/DialogBox'

interface LiteralDialogProps {
onClose: () => void
onSubmit: (prefix: string, name: string) => void
options: Array<string>
title: string
lastSelected: string
}

export const LiteralDialog: FC<LiteralDialogProps> = ({ options, onClose, title, onSubmit, lastSelected }) => {
const [selectedEdgeType, setSelectedEdgeType] = useState<string>(lastSelected)
const [attributeValue, setAttributeValue] = useState<string>("")

useEffect(() => {
const handleKeyPress = (event: KeyboardEvent) => {
if (event.key === 'Enter') {
onHandleSubmit()
}
}
document.addEventListener('keypress', handleKeyPress)
return () => {
document.removeEventListener('keypress', handleKeyPress)
}
}, [])

const onChangeName: React.ChangeEventHandler<HTMLInputElement> = (event) => {
event.preventDefault()

setAttributeValue(event.target.value)
}

const onChangeEdgeType = (event: React.SyntheticEvent<Element, Event>, value: string | null) => {
event.preventDefault()
if (!value) {
console.warn("Invalid value", value)
return
}
setSelectedEdgeType(value)
}

const onHandleSubmit = () => {
if (!selectedEdgeType || !attributeValue) {
console.warn("Literal must have valid inputs")
return
}
onSubmit(selectedEdgeType, attributeValue)
}

return (
<DialogBox onClose={onClose} title={title}>
<div className="dark:text-whiteSmoke flex flex-col gap-y-8 rounded">
<div className='flex gap-x-2'>
<TeliAutocomplete options={options} width={150} label="DataTypeProperty" onChange={onChangeEdgeType} value={selectedEdgeType} />
<TeliTextField id="attribute-value" label="Value" onChange={onChangeName} value={attributeValue} required />
</div>
<div className='flex justify-end w-full'>
<TeliButton onClick={onHandleSubmit} variant="secondary" disabled={!attributeValue || !selectedEdgeType}>Submit</TeliButton>
</div>
</div>
</DialogBox>
)
}
3 changes: 2 additions & 1 deletion src/Components/Diagram/NodeDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ interface NodeDialogProps {
title: string
lastSelectedPrefix: string
}

export const NodeDialog: FC<NodeDialogProps> = ({ options, onClose, title, onSubmit, lastSelectedPrefix }) => {
const [selectedPrefix, setSelectedPrefix] = useState<string>(lastSelectedPrefix)
const [name, setName] = useState<string>(crypto.randomUUID())
Expand Down Expand Up @@ -54,7 +55,7 @@ export const NodeDialog: FC<NodeDialogProps> = ({ options, onClose, title, onSub
<div className="dark:text-whiteSmoke flex flex-col gap-y-8 rounded">
<div className='flex gap-x-2'>
<TeliAutocomplete options={options} width={150} label="Prefix" onChange={onChangePrefix} value={selectedPrefix} />
<TeliTextField id="node-name" label="Node name" onChange={onChangeName} value={name} required />
<TeliTextField id="node-name" label="URI" onChange={onChangeName} value={name} required />
</div>
<div className='flex justify-end w-full'>
<TeliButton onClick={onHandleSubmit} variant="secondary" disabled={!name || !selectedPrefix}>Submit</TeliButton>
Expand Down
10 changes: 2 additions & 8 deletions src/Components/Terminal/Terminal.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import Editor, { Monaco } from "@monaco-editor/react";
// import type monaco from "monaco-editor";
import rdfParser from "rdf-parse";
import { Readable } from "readable-stream";
import { useRef } from "react";
// import { MarkerSeverity } from "monaco-editor";
import { RdfInstancePresenter } from "../../rdfInstanceViewer/RdfInstancePresenter";
import { withInjection } from "../../Core/Providers/injection";
import { RdfPanelProps } from "../../types";
Expand All @@ -14,14 +12,10 @@ import { themeRules } from "./themeRules";

const TerminalComponent = observer((props: RdfPanelProps) => {
const { handleRdfInput, viewModel } = props.presenter;
// console.log({ viewModel })
const monacoRef = useRef<Monaco | null>(null);
// const monaco = useMonaco()
// const markers: monaco.editor.IMarkerData[] = [];
// console.log({ viewModel })
if (monacoRef?.current) {
// @ts-expect-error Property from does not exist on type Readable
const input = Readable.from([viewModel.rdf]);
const input = Readable.from([props.presenter.rdfInstanceRepository.rdf]);
// console.log({ rdf: viewModel.rdf })
rdfParser
.parse(input, { contentType: "text/turtle" })
Expand Down Expand Up @@ -107,7 +101,7 @@ const TerminalComponent = observer((props: RdfPanelProps) => {
height="93.5vh"
language="turtle"
onChange={handleRdfInput}
value={viewModel.rdf ?? ""}
value={props.presenter.rdfInstanceRepository.rdf ?? ""}
options={{
selectOnLineNumbers: true,
automaticLayout: true,
Expand Down
20 changes: 13 additions & 7 deletions src/Core/FakeHttpGateway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,28 @@ import { z } from "zod";
import { injectable } from "inversify";
import OntologyService from "@telicent-oss/ontologyservice"
import { ResponseSchema } from "../rdfInstanceViewer/Types";
import { ObjectPropertiesResponseStub } from "../TestTools/ObjectPropertyResponseStub";
import { DataTypePropertyResponseStub } from "../TestTools/DataTypePropertyResponseStub";

@injectable()
export class FakeHttpGateway {
private ontologyService = new OntologyService("fake-address", "fake-topic")

get = async <T extends z.infer<typeof ResponseSchema>>(
_: string,
query: string,
validationCallback: (data: unknown) => T
): Promise<T['results']['bindings']> => {
await Promise.resolve();
// console.log(query)
// Create a stubbed response matching the structure expected by T
const stubbedResponse: unknown = {
results: {
bindings: []
}
};
let stubbedResponse: unknown

if (query.includes("object_property")) {
stubbedResponse = ObjectPropertiesResponseStub
}
if (query.includes("data_type_property")) {
stubbedResponse = DataTypePropertyResponseStub
}
// await Promise.resolve();

// Validate the stubbed response with the provided callback
const validatedResponse = validationCallback(stubbedResponse);
Expand Down
35 changes: 35 additions & 0 deletions src/TestTools/DataTypePropertyResponseStub.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
export const DataTypePropertyResponseStub = {
"head": {
"vars": [
"data_type_property"
]
},
"results": {
"bindings": [
{
"data_type_property": {
"type": "uri",
"value": "http://ies.data.gov.uk/ontology/ies4#idEmergencyContactTelNo"
}
},
{
"data_type_property": {
"type": "uri",
"value": "http://ies.data.gov.uk/ontology/ies4#endsIn"
}
},
{
"data_type_property": {
"type": "uri",
"value": "http://ies.data.gov.uk/ontology/ies4#issuerIdentificationNumber"
}
},
{
"data_type_property": {
"type": "uri",
"value": "http://ies.data.gov.uk/ontology/ies4#hmlConfidence"
}
},
]
}
}
Loading

0 comments on commit 71893f8

Please # to comment.