diff --git a/README.md b/README.md index 517ca8d0f..f3d17fbef 100644 --- a/README.md +++ b/README.md @@ -84,6 +84,11 @@ docker build . --tag=nexus-web - `GTAG`: The Google Analytics Identifier. GA won't be present unless an ID is specified. - `SENTRY_DSN`: The sentry URL Nexus Web needs to report errors to. Default is undefined. +The following concern Plugins. [See how to manage plugin deployments](./docs/plugins.md) + +- `PLUGINS_MANIFEST_PATH`: Remote end point where plugins and manifest can be found. for example, `https://bbp-nexus.epfl.ch/plugins` +- `PLUGINS_CONFIG_PATH`: A full file path where a plugins configuration can be found. + ## Deployment You can find out how to deploy a build [in the wiki](https://github.com/BlueBrain/nexus-web/wiki/Deploying-Your-Nexus-Web-Instance) diff --git a/docs/plugins.md b/docs/plugins.md new file mode 100644 index 000000000..db6567df2 --- /dev/null +++ b/docs/plugins.md @@ -0,0 +1,48 @@ +# Plugins and Deployment + +## Plugin Manifest + +The plugin manifest should be available at the same remote endpoint as the plugins. This is so Nexus can find the plugins and apply them dynamically. + +The plugin manifest is a json object with keys that correspond to the plugin name with a value that corresponds to a descriptive payload of where to find the manifest, as well as some information about it's development. It's similar to a package.json file. + +```{ + "circuit": { + "modulePath": "circuit.f7755e13c8b410efdf02.js", + "name": "Circuit", + "description": "", + "version": "", + "tags": [], + "author": "", + "license": "" + } +} +``` + +## Plugin Config + +The plugin config should be available as an adjacent file next to your running Nexus Web instance. It's a json file that describes which resource should be represented by each plugin. + +### Matching all resources + +The following will show `nexus-plugin-test` for *every* resource inside Nexus Web. + +``` +{ + "nexus-plugin-test" : {} +} +``` + +### Matching a resource with a specific type and shape +The following will show `nexus-plugin-test` for any resource of type `File` but only if they have a `distribution.encodingFormat` property that's `application/swc` + +``` +{ + "nexus-plugin-test" : { + "@type": "File", + "distribution:" { + "encodingFormat": "application/swc" + } + } +} +``` diff --git a/package.json b/package.json index 7601416d9..94fb83e04 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "@bbp/react-nexus": "1.3.1", "@sentry/browser": "5.9.1", "@types/cytoscape": "^3.8.5", + "@types/lodash": "^4.14.149", "antd": "^3.19.5", "codemirror": "^5.44.0", "connected-react-router": "^6.3.2", @@ -33,6 +34,7 @@ "express-prom-bundle": "^5.0.2", "history": "^4.7.2", "jwt-decode": "^2.2.0", + "lodash": "^4.17.15", "moment": "^2.24.0", "morgan": "^1.9.1", "object-hash": "^1.3.1", diff --git a/src/server/config/config.ts b/src/server/config/config.ts new file mode 100644 index 000000000..98dcf381d --- /dev/null +++ b/src/server/config/config.ts @@ -0,0 +1 @@ +export const pluginsMap = require('../../../plugin.config.json'); diff --git a/src/server/index.tsx b/src/server/index.tsx index 15087eb41..c745f799d 100644 --- a/src/server/index.tsx +++ b/src/server/index.tsx @@ -1,5 +1,5 @@ import { resolve, join } from 'path'; -import { readdirSync } from 'fs'; +import { readFileSync } from 'fs'; import * as express from 'express'; import * as cookieParser from 'cookie-parser'; import * as morgan from 'morgan'; @@ -17,7 +17,22 @@ const app: express.Express = express(); const rawBase: string = process.env.BASE_PATH || ''; // to develop plugins locally, change PLUGINS_PATH to '/public/plugins' -const pluginsPath = process.env.PLUGINS_PATH || '/plugins'; +const pluginsManifestPath = + process.env.PLUGINS_MANIFEST_PATH || '/public/plugins'; + +const pluginsConfigPath = + process.env.PLUGINS_CONFIG_PATH || + join(__dirname, '/public/plugins/plugins.config.json'); + +const getpluginsConfig = () => { + let pluginsConfig; + try { + pluginsConfig = JSON.parse(readFileSync(pluginsConfigPath).toString()); + } catch (e) { + console.error(e); + } + return pluginsConfig || {}; +}; // remove trailing slash const base: string = rawBase.replace(/\/$/, ''); @@ -52,7 +67,9 @@ app.get('*', async (req: express.Request, res: express.Response) => { const preloadedState: RootState = { auth: {}, config: { - pluginsPath, + pluginsManifestPath, + + pluginsMap: getpluginsConfig(), apiEndpoint: process.env.API_ENDPOINT || '/', basePath: base, clientId: process.env.CLIENT_ID || 'nexus-web', diff --git a/src/shared/App.less b/src/shared/App.less index 6237d31c2..1009fd10e 100644 --- a/src/shared/App.less +++ b/src/shared/App.less @@ -25,16 +25,21 @@ .highShadow(); padding: 1em; width: 100%; - max-width: 50%; background-color: @background-color-subtle; } - .graph-wrapper { - margin-left: 1em; - width: 100%; - } } } +.graph-wrapper-container { + width: 100%; +} + +.graph-wrapper { + margin-left: 1em; + width: 100%; + height: 1000px; +} + .resource-details { .ant-alert-warning { margin: 1em 0; diff --git a/src/shared/App.tsx b/src/shared/App.tsx index a989104c1..d66cd9612 100644 --- a/src/shared/App.tsx +++ b/src/shared/App.tsx @@ -1,14 +1,13 @@ +import { Modal } from 'antd'; import * as React from 'react'; import { Route, Switch, useLocation, useHistory } from 'react-router-dom'; +import { Location } from 'history'; import routes from '../shared/routes'; import NotFound from './views/404'; import MainLayout from './layouts/MainLayout'; +import ResourceViewContainer from './containers/ResourceViewContainer'; import './App.less'; -import { Modal } from 'antd'; -import ResourceViewContainer from './containers/ResourceViewContainer'; -import StudioResourceView from './views/StudioResourceView'; -import { Location } from 'history'; const App: React.FC = () => { const location = useLocation(); @@ -45,9 +44,8 @@ const App: React.FC = () => { render={routeProps => ( history.push(background.pathname, {})} - onOk={() => history.push(location.pathname, {})} - okText="Graph View" className="modal-view" width="inherit" > @@ -55,21 +53,6 @@ const App: React.FC = () => { )} />, - ( - history.push(background.pathname, {})} - footer={null} - className="modal-view -unconstrained" - width="inherit" - > - - - )} - />, ]} ); diff --git a/src/shared/components/DashboardEditor/DashboardConfigEditor.tsx b/src/shared/components/DashboardEditor/DashboardConfigEditor.tsx index 130558a92..b80fed561 100644 --- a/src/shared/components/DashboardEditor/DashboardConfigEditor.tsx +++ b/src/shared/components/DashboardEditor/DashboardConfigEditor.tsx @@ -35,23 +35,9 @@ const DashboardConfigEditorComponent: React.FunctionComponent { const { description, label, dataQuery, plugins = [] } = dashboard || {}; const { getFieldDecorator, getFieldsValue, validateFields } = form; - const [selectedPlugins, setSelectedPlugins] = React.useState( - plugins - ); - const { Panel } = Collapse; - - const formatPluginSource = () => { - if (availablePlugins && availablePlugins.length) { - return availablePlugins.map(plugin => ( - - )); - } - return []; - }; const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); @@ -67,16 +53,11 @@ const DashboardConfigEditorComponent: React.FunctionComponent { - setSelectedPlugins(nextTargetKeys); - }; - return (
)} - - {getFieldDecorator('plugins', { - rules: [ - { - required: false, - }, - ], - })( - - - Plugins{' '} - - - {' '} - Experimental{' | '} - - Read Docs - - - } - key="1" - > - - - - )} -