Commit d0248d0d authored by Laurent Wouters's avatar Laurent Wouters
Browse files

Export view library in external package

parent 79dc3a956ee4
{
"name": "cubicweb-libview",
"version": "0.1.0",
"description": "Library for view specification and definition for a data browser.",
"scripts": {
"build": "webpack",
"clean": "rm -rf build/*",
"test": "echo \"Error: no test specified\" && exit 1",
"build:production": "NODE_ENV=production webpack"
},
"types": "./build/lib/index.d.ts",
"repository": {
"type": "hg",
"url": "https://bitbucket.org/laurentw/logilab-cubicweb-client"
},
"keywords": [
"DataBrowser",
"Typescript"
],
"author": "LOGILAB <contact@logilab.fr>",
"license": "LGPL-3.0",
"dependencies": {
"@types/rdflib": "file:../rdflib-types",
"@types/react": "^16.4.6",
"rdflib": "^0.17.0",
"react": "^16.2.0"
},
"devDependencies": {
"source-map-loader": "^0.2.3",
"ts-loader": "^4.4.2",
"typescript": "^2.9.2",
"webpack": "^4.0.1",
"webpack-cli": "^2.0.10"
}
}
/*******************************************************************************
* Copyright 2003-2018 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
* contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
*
* This file is part of CubicWeb.
*
* CubicWeb is free software: you can redistribute it and/or modify it under the
* terms of the GNU Lesser General Public License as published by the Free
* Software Foundation, either version 2.1 of the License, or (at your option)
* any later version.
*
* CubicWeb is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
* details.
*
* You should have received a copy of the GNU Lesser General Public License along
* with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
******************************************************************************/
import { RdfStore } from "rdflib";
import { ReactNode } from "react";
/**
* A renderer for an application
*/
export interface ViewRenderer {
/**
* Renders a resource belonging to an RDF store
* @param store The RDF store holding the data to be rendered
* @param root The root resource
* @param target The target resource for this view
*/
render(store: RdfStore, root: string, target: string): ReactNode;
}
/**
* The public interface for the application
*/
export interface Application {
/**
* Sets the content for the application
* @param content The content
* @param root The URI of the root resource
*/
onContent(renderer: ViewRenderer, content: RdfStore, root: string): void;
/**
* Sets the application on error
* @param description The error's description
*/
onError(description: string): void;
}
/*******************************************************************************
* Copyright 2003-2018 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
* contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
*
* This file is part of CubicWeb.
*
* CubicWeb is free software: you can redistribute it and/or modify it under the
* terms of the GNU Lesser General Public License as published by the Free
* Software Foundation, either version 2.1 of the License, or (at your option)
* any later version.
*
* CubicWeb is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
* details.
*
* You should have received a copy of the GNU Lesser General Public License along
* with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
******************************************************************************/
import * as application from "./application";
import * as definition from "./view-def";
import * as implementation from "./view-impl";
export { application, definition, implementation };
/*******************************************************************************
* Copyright 2003-2018 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
* contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
*
* This file is part of CubicWeb.
*
* CubicWeb is free software: you can redistribute it and/or modify it under the
* terms of the GNU Lesser General Public License as published by the Free
* Software Foundation, either version 2.1 of the License, or (at your option)
* any later version.
*
* CubicWeb is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
* details.
*
* You should have received a copy of the GNU Lesser General Public License along
* with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
******************************************************************************/
/**
* The kind of locations for the location of a view definition
*/
export enum ViewLocationKind {
Embedded = "embedded",
Remote = "remote"
}
/**
* The location of a view definition
*/
export interface ViewLocation {
/**
* The kind of location
*/
kind: ViewLocationKind;
}
/**
* The location of a view definition when already embedded
*/
export interface ViewLocationEmbedded extends ViewLocation {}
/**
* the location of a view definition as a remote script
*/
export interface ViewLocationRemote extends ViewLocation {
/**
* The location of the definition as an URI
*/
uri: string;
}
/**
* The metadata of a view
*/
export interface ViewDescriptor {
/**
* The unique identifier for this view
*/
identifier: string;
/**
* The human readable name
*/
name: string;
/**
* A longer description for the view
*/
description: string;
/**
* The name of the entry point in the view definition
*/
entrypoint: string;
/**
* The location of the view definition
*/
location: ViewLocation;
}
/**
* A repository of view descriptors
*/
export interface ViewDescriptors {
[id: string]: ViewDescriptor;
}
/**
* Kinds of sources for view descriptors for a registry
*/
export enum ViewRegistrySourceKind {
Inlined = "inlined",
Remote = "remote"
}
/**
* A source of view descriptors for a registry
*/
export interface ViewRegistrySource {
/**
* The kind of source
*/
kind: ViewRegistrySourceKind;
/**
* The name of the source
*/
name: string;
}
/**
* A source of view descriptors with the descriptors inlined within it
*/
export interface ViewRegistrySourceInline extends ViewRegistrySource {
/**
* The inlined descriptors
*/
descriptors: ViewDescriptor[];
}
/**
* A remote source of view descriptors
*/
export interface ViewRegistrySourceRemote extends ViewRegistrySource {
/**
* The uri to fetch the descriptors at
*/
uri: string;
}
/**
* Fetches the descriptors for a source
* @param source The source to fetch from
*/
function fetchDescriptors(
source: ViewRegistrySource
): Promise<ViewDescriptor[]> {
if (source.kind == ViewRegistrySourceKind.Inlined) {
return new Promise<ViewDescriptor[]>(
(
resolve: (result: ViewDescriptor[] | Promise<ViewDescriptor[]>) => void,
reject: (reason: string) => void
) => {
resolve((source as ViewRegistrySourceInline).descriptors);
}
);
} else if (source.kind == ViewRegistrySourceKind.Remote) {
return new Promise<ViewDescriptor[]>(
(
resolve: (result: ViewDescriptor[] | Promise<ViewDescriptor[]>) => void,
reject: (reason: string) => void
) => {
let xmlHttp = new XMLHttpRequest();
xmlHttp.onreadystatechange = function() {
if (xmlHttp.readyState == 4) {
if (xmlHttp.status < 200 || xmlHttp.status >= 300) {
reject("HTTP error: " + xmlHttp.status);
}
if (xmlHttp.responseText.length == 0) {
reject("Empty result");
}
resolve(JSON.parse(xmlHttp.responseText));
}
};
xmlHttp.open("GET", (source as ViewRegistrySourceRemote).uri, true);
xmlHttp.setRequestHeader("Accept", "application/json");
xmlHttp.send();
}
);
} else {
return new Promise<ViewDescriptor[]>(
(
resolve: (result: ViewDescriptor[] | Promise<ViewDescriptor[]>) => void,
reject: (reason: string) => void
) => {
reject("Invalid source");
}
);
}
}
/**
* Loads all descriptors from the registered sources
* @param sources The sources to load from
*/
function loadDescriptors(
sources: ViewRegistrySource[]
): Promise<ViewDescriptors> {
return new Promise<ViewDescriptors>(
(
resolve: (descriptors: ViewDescriptors) => void,
reject: (reason: any) => void
) => {
var finished = 0;
var target = sources.length;
var descriptors: ViewDescriptors = {};
let onPartFinished: () => void = () => {
finished += 1;
if (finished >= target) resolve(descriptors);
};
sources.map((source: ViewRegistrySource) => {
fetchDescriptors(source)
.then((result: ViewDescriptor[]) => {
result.map((descriptor: ViewDescriptor) => {
descriptors[descriptor.identifier] = descriptor;
onPartFinished();
});
})
.catch((reason: string) => {
console.log(
"Failed to fetch descriptor from source " +
source.name +
": " +
reason
);
onPartFinished();
});
});
}
);
}
/**
* A registry of views
*/
export class ViewRegistry {
constructor() {
this.sources = [];
this.descriptors = {};
}
/**
* The sources for this registry
*/
sources: ViewRegistrySource[];
/**
* The registry of descriptors
*/
descriptors: ViewDescriptors;
}
/**
* Loads the descriptors for this registry
*/
export function loadRegistry(registry: ViewRegistry): Promise<ViewRegistry> {
return new Promise<ViewRegistry>(
(
resolve: (result: ViewRegistry) => void,
reject: (reason: any) => void
) => {
loadDescriptors(registry.sources).then((descriptors: ViewDescriptors) => {
registry.descriptors = descriptors;
resolve(registry);
});
}
);
}
/*******************************************************************************
* Copyright 2003-2018 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
* contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
*
* This file is part of CubicWeb.
*
* CubicWeb is free software: you can redistribute it and/or modify it under the
* terms of the GNU Lesser General Public License as published by the Free
* Software Foundation, either version 2.1 of the License, or (at your option)
* any later version.
*
* CubicWeb is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
* details.
*
* You should have received a copy of the GNU Lesser General Public License along
* with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
******************************************************************************/
import { RdfStore } from "rdflib";
import { ReactNode } from "react";
import { ViewRenderer, Application } from "./application";
import {
ViewDescriptor,
ViewLocationKind,
ViewLocationRemote,
ViewRegistry
} from "./view-def";
/**
* The implementation of a view
*/
export interface ViewImplementation {
/**
* The descriptor for ths implementation
*/
descriptor: ViewDescriptor;
/**
* Gets the view's priority (greater number indicate greater priority)
* @param store The RDF store holding the data to be rendered
* @param target The target resource to be rendered
*/
priorityFor(store: RdfStore, target: string): number;
/**
* Renders a resource belonging to an RDF store
* @param renderer The parent renderer
* @param store The RDF store holding the data to be rendered
* @param root The root resource
* @param target The target resource for this view
*/
render(
renderer: ViewRenderer,
store: RdfStore,
root: string,
target: string
): ReactNode;
}
/**
* A repository of view implementations
*/
export interface ViewImplementations {
[id: string]: ViewImplementation;
}
/**
* The global reposistory of embedded view implementations
*/
export const IMPL_EMBEDDED: ViewImplementations = {};
/**
* Registers an embedded view implementation
* @param implementation A view implementation
*/
export function registerImplementation(implementation: ViewImplementation) {
IMPL_EMBEDDED[implementation.descriptor.identifier] = implementation;
}
/**
* Fetches the implementation corresponding to a descriptor
* @param descriptor The descriptor
*/
function fetchImplementation(
descriptor: ViewDescriptor
): Promise<ViewImplementation> {
if (descriptor.location.kind == ViewLocationKind.Embedded) {
return new Promise((resolve, reject) => {
let result = IMPL_EMBEDDED[descriptor.identifier];
if (result == null || result == undefined) {
reject("No implementation");
}
resolve(result);
});
} else if (descriptor.location.kind == ViewLocationKind.Remote) {
return new Promise((resolve, reject) => {
if (document != null && document != undefined) {
// we are in the context of a document
var scriptTag = document.createElement("script");
scriptTag.setAttribute(
"src",
(descriptor.location as ViewLocationRemote).uri
);
scriptTag.addEventListener(
"load",
(event: Event): any => {
let result = eval(descriptor.entrypoint);
if (result == null) {
reject("No implementation");
}
resolve(result);
},
false
);
document.head.appendChild(scriptTag);
} else {
let xmlHttp = new XMLHttpRequest();
xmlHttp.onreadystatechange = function() {
if (xmlHttp.readyState == 4) {
if (xmlHttp.status < 200 || xmlHttp.status >= 300) {
reject("HTTP error: " + xmlHttp.status);
}
if (xmlHttp.responseText.length == 0) {
reject("Empty result");
}
let evaluation = eval(xmlHttp.responseText);
let result = eval(descriptor.entrypoint);
if (result == null) {
reject("No implementation");
}
resolve(result);
}
};
xmlHttp.open(
"GET",
(descriptor.location as ViewLocationRemote).uri,
true
);
xmlHttp.setRequestHeader("Accept", "application/javascript");
xmlHttp.send();
}
});
}
}
/**
* Loads all implementations for the registered views
* @param registry The registry to load
*/
function loadImplementations(
registry: ViewRegistry
): Promise<ViewImplementations> {
return new Promise<ViewImplementations>(
(
resolve: (impls: ViewImplementations) => void,
reject: (reason: any) => void
) => {
var finished = 0;
var target = Object.keys(registry.descriptors).length;
var implementations: ViewImplementations = {};
let onPartFinished: () => void = () => {
finished += 1;
if (finished >= target) resolve(implementations);
};
Object.keys(registry.descriptors).map((id: string) => {
let descriptor = registry.descriptors[id];
fetchImplementation(descriptor)
.then((implementation: ViewImplementation) => {
implementations[id] = implementation;
onPartFinished();
})
.catch((reason: string) => {
console.log(
"Failed to fetch implementation for view " +
descriptor.name +
": " +
reason
);
onPartFinished();
});
});
}
);
}
/**
* A prioritized view implementation
*/
interface PrioritizedViewImplementation {
/**
* The related implementation
*/
implementation: ViewImplementation;
/**
* The associated priority
*/
priority: number;
}
/**
* Resoles a view for an RDF datastore and a resource in it
* @param implementations The view implementations to use
* @param store The RDF store holding the data to be rendered
* @param target The target RDF resource to render
*/
function resolveViewFor(
implementations: ViewImplementations,
store: RdfStore,
target: string
): ViewImplementation {
let sorted: PrioritizedViewImplementation[] = Object.keys(implementations)
.map((id: string) => {
let impl = implementations[id];
return {
implementation: implementations[id],
priority: impl.priorityFor(store, target)
};
})
.filter((a: PrioritizedViewImplementation) => a.priority >= 0)
.sort(
(x: PrioritizedViewImplementation, y: PrioritizedViewImplementation) =>
x.priority - y.priority
);
if (sorted.length == 0) return null;
return sorted[0].implementation;
}
/**
* A renderer for an application
*/
class ViewRendererImpl implements ViewRenderer {
/**
* Initializes a renderer
* @param application The parent application
* @param registry The registry to use
* @param implementations The view implementations
*/
constructor(
application: Application,
registry: ViewRegistry,
implementations: ViewImplementations
) {
this.application = application;
this.registry = registry;
this.implementations = implementations;
}
/**
* The parent application
*/
application: Application;