SPARQL Navigator
Example demonstrating graph data exploration using a generic SPARQL endpoint.
/src/examples/PlaygroundSparql.tsx
import * as React from 'react';
import * as Reactodia from '@reactodia/workspace';
import { ExampleToolbarMenu } from './ExampleCommon';
import { getHashQuery, setHashQueryParam } from './HashQuery';
import {
SparqlConnectionAction, SparqlConnectionSettings, showConnectionDialog,
} from './SparqlConnection';
const Layouts = Reactodia.defineLayoutWorker(() => new Worker(
new URL('@reactodia/workspace/layout.worker', import.meta.url)
));
export function PlaygroundSparql() {
const {defaultLayout} = Reactodia.useWorker(Layouts);
const [connectionSettings, setConnectionSettings] = React.useState(
(): SparqlConnectionSettings | undefined => {
const params = getHashQuery();
const endpointUrl = params?.get('sparql-endpoint');
return endpointUrl ? {endpointUrl} : undefined;
}
);
const applyConnectionSettings = (settings: SparqlConnectionSettings) => {
setHashQueryParam('sparql-endpoint', settings.endpointUrl);
setConnectionSettings(settings);
};
const {onMount} = Reactodia.useLoadedWorkspace(async ({context, signal}) => {
const {model, getCommandBus} = context;
if (connectionSettings) {
const dataProvider = new Reactodia.SparqlDataProvider({
endpointUrl: connectionSettings.endpointUrl,
imagePropertyUris: ['http://xmlns.com/foaf/0.1/img'],
}, Reactodia.OwlStatsSettings);
model.importLayout({
dataProvider: dataProvider,
validateLinks: true,
signal,
});
getCommandBus(Reactodia.UnifiedSearchTopic)
.trigger('focus', {sectionKey: 'elementTypes'});
} else {
showConnectionDialog(connectionSettings, applyConnectionSettings, context);
}
}, [connectionSettings]);
return (
<Reactodia.Workspace ref={onMount}
defaultLayout={defaultLayout}>
<Reactodia.DefaultWorkspace
menu={<ExampleToolbarMenu />}
canvasWidgets={[
<Reactodia.Toolbar key='sparql-settings'
dock='sw'
dockOffsetY={40}>
<SparqlConnectionAction settings={connectionSettings}
applySettings={applyConnectionSettings}
/>
</Reactodia.Toolbar>
]}
languages={[
{code: 'de', label: 'Deutsch'},
{code: 'en', label: 'english'},
{code: 'es', label: 'español'},
{code: 'fr', label: 'français'},
{code: 'ja', label: '日本語'},
{code: 'hi', label: 'हिन्दी'},
{code: 'pt', label: 'português'},
{code: 'ru', label: 'русский'},
{code: 'zh', label: '汉语'},
]}
/>
</Reactodia.Workspace>
);
}
ExampleCommon.tsx
/src/examples/ExampleCommon.tsx
import * as React from 'react';
import * as Reactodia from '@reactodia/workspace';
import { saveAs } from 'file-saver';
export function ExampleToolbarMenu() {
const {model, editor, overlay} = Reactodia.useWorkspace();
return (
<>
<Reactodia.ToolbarActionOpen
fileAccept='.json'
onSelect={async file => {
const preloadedElements = new Map<Reactodia.ElementIri, Reactodia.ElementModel>();
for (const element of model.elements) {
for (const data of Reactodia.iterateEntitiesOf(element)) {
preloadedElements.set(data.id, data);
}
}
const task = overlay.startTask({title: 'Importing a layout from file'});
try {
const json = await file.text();
const diagramLayout = JSON.parse(json);
await model.importLayout({
dataProvider: model.dataProvider,
diagram: diagramLayout,
preloadedElements,
validateLinks: true,
});
} catch (err) {
task.setError(new Error(
'Failed to load specified file with a diagram layout.',
{cause: err}
));
} finally {
task.end();
}
}}>
Open diagram from file
</Reactodia.ToolbarActionOpen>
<Reactodia.ToolbarActionSave mode='layout'
onSelect={() => {
const diagramLayout = model.exportLayout();
const layoutString = JSON.stringify(diagramLayout);
const blob = new Blob([layoutString], {type: 'application/json'});
const timestamp = new Date().toISOString().replaceAll(/[Z\s:-]/g, '');
saveAs(blob, `reactodia-diagram-${timestamp}.json`);
}}>
Save diagram to file
</Reactodia.ToolbarActionSave>
{editor.inAuthoringMode ? (
<Reactodia.ToolbarActionSave mode='authoring'
onSelect={() => {
const state = editor.authoringState;
console.log('Authoring state:', state);
alert('Please check browser console for result');
}}>
Persist changes to data
</Reactodia.ToolbarActionSave>
) : null}
<Reactodia.ToolbarActionClearAll />
<Reactodia.ToolbarActionExport kind='exportRaster' />
<Reactodia.ToolbarActionExport kind='exportSvg' />
<Reactodia.ToolbarActionExport kind='print' />
</>
);
}
HashQuery.ts
/src/examples/HashQuery.ts
export function getHashQuery(): URLSearchParams | undefined {
const hash = window.location.hash;
if (hash.length > 1) {
try {
const hashQuery = new URLSearchParams(hash.substring(1));
return hashQuery;
} catch (e) {
/* ignore */
}
}
return undefined;
}
export function setHashQueryParam(paramName: string, paramValue: string | null): void {
const hashQuery = getHashQuery() ?? new URLSearchParams();
if (paramValue) {
hashQuery.set(paramName, paramValue);
} else {
hashQuery.delete(paramName);
}
window.location.hash = hashQuery.toString();
}
SparqlConnection.ts
/src/examples/SparqlConnection.ts
import * as React from 'react';
import * as Reactodia from '@reactodia/workspace';
export interface SparqlConnectionSettings {
readonly endpointUrl: string;
}
export function SparqlConnectionAction(props: {
settings: SparqlConnectionSettings | undefined;
applySettings: (settings: SparqlConnectionSettings) => void;
}) {
const {settings, applySettings} = props;
if (!settings) {
return null;
}
const context = Reactodia.useWorkspace();
const endpointUrl = URL.canParse(settings.endpointUrl)
? new URL(settings.endpointUrl) : undefined;
return (
<Reactodia.ToolbarAction
onSelect={() => showConnectionDialog(settings, applySettings, context)}>
SPARQL endpoint: <code>{endpointUrl?.host ?? settings.endpointUrl}</code>
</Reactodia.ToolbarAction>
);
}
export function showConnectionDialog(
initialSettings: SparqlConnectionSettings | undefined,
applySettings: (settings: SparqlConnectionSettings) => void,
context: Reactodia.WorkspaceContext
): void {
const { overlay } = context;
overlay.showDialog({
style: {
caption: 'SPARQL connection settings',
defaultSize: {width: 400, height: 250},
resizableBy: 'x',
closable: Boolean(initialSettings),
},
content: (
<SparqlConnectionForm
initialSettings={initialSettings}
onSubmit={settings => {
overlay.hideDialog();
applySettings(settings);
}}
/>
),
});
}
export function SparqlConnectionForm(props: {
initialSettings: SparqlConnectionSettings | undefined;
onSubmit: (settings: SparqlConnectionSettings) => void;
}) {
const {initialSettings, onSubmit} = props;
const [settings, setSettings] = React.useState<SparqlConnectionSettings>(
initialSettings ?? {endpointUrl: ''}
);
const isValidEndpoint = settings.endpointUrl.length === 0 || URL.canParse(settings.endpointUrl);
const canSubmit = settings.endpointUrl.length > 0 && isValidEndpoint;
return (
<div className='reactodia-form'>
<div className='reactodia-form__body'>
<div className='reactodia-form__control-row'>
<label htmlFor='sparqlEndpointUrl'>Endpoint URL</label>
<input id='sparqlEndpointUrl'
type='input'
className='reactodia-form-control'
placeholder='SPARQL endpoint URL'
autoFocus
value={settings.endpointUrl}
onChange={e => {
const endpointUrl = e.currentTarget.value;
setSettings(previous => ({...previous, endpointUrl}));
}}
onKeyDown={e => {
if (e.key === 'Enter' && canSubmit) {
onSubmit(settings);
}
}}
/>
{isValidEndpoint ? null : (
<div className={'reactodia-form__control-error'}>
Invalid URL
</div>
)}
</div>
<div className='reactodia-form__control-row'>
A public SPARQL endpoints will work if only if its configured
to allow cross-origin GET queries (CORS headers).
</div>
</div>
<div className='reactodia-form__controls'>
<button className='reactodia-btn reactodia-btn-primary'
type='button'
disabled={!canSubmit}
onClick={() => onSubmit(settings)}>
Connect
</button>
</div>
</div>
);
}