diff --git a/.github/workflows/review.yml b/.github/workflows/review.yml index 98554f80b..10bc0de14 100644 --- a/.github/workflows/review.yml +++ b/.github/workflows/review.yml @@ -153,7 +153,7 @@ jobs: - name: Run e2e tests run: >- echo | timeout --verbose 20m docker exec - -e 'DEBUG=cypress:launcher:browsers' + -e 'DEBUG=cypress:launcher:browsers NODE_TLS_REJECT_UNAUTHORIZED=0' -t cypress cypress run diff --git a/README.md b/README.md index f27af9513..3af5bb0d8 100644 --- a/README.md +++ b/README.md @@ -62,13 +62,31 @@ Run end to end tests: a. To run the tests in headed mode: ```sh -yarn cy:open +yarn e2e:open +``` + +If you encounter issues with `project:setup` or `resources:create` tasks because of SSL or certificate errors when running the tests locally, try the following: + +```sh +NODE_TLS_REJECT_UNAUTHORIZED=0 yarn e2e:open +``` + +If you encounter issues with `project:setup` or `resources:create` tasks because of SSL or certificate errors when running the tests locally, try the following: + +```sh +NODE_TLS_REJECT_UNAUTHORIZED=0 yarn cy:open +``` + +If you encounter issues with `project:setup` or `resources:create` tasks because of SSL or certificate errors when running the tests locally, try the following: + +```sh +NODE_TLS_REJECT_UNAUTHORIZED=0 yarn cy:open ``` b. To run the tests in headless mode: ```sh -yarn cy:run +yarn e2e:run ``` ## Build for production diff --git a/cypress.config.ts b/cypress.config.ts index 27c990f6a..4a866a1b1 100644 --- a/cypress.config.ts +++ b/cypress.config.ts @@ -115,8 +115,8 @@ export default defineConfig({ orgLabel, projectLabel, }); - } catch (e) { - console.log('Error encountered in project:teardown task.', e); + } catch (error) { + console.log('Error encountered in project:teardown task.', error); } return null; @@ -141,19 +141,29 @@ export default defineConfig({ token: authToken, }); - return await createResource({ + const createdResource = await createResource({ nexus, orgLabel, projectLabel, resource: resourcePayload, }); + if (!createResource) { + throw new Error('Test Resource was not created'); + } + return createdResource; } catch (e) { - console.log( - 'Error encountered in analysisResource:create task.', - e + console.error( + 'Error encountered in resource:create task.', + e.message ); + console.error(e.stack); // Log stack trace if available + // If the error is an instance of HTTP error, log response details + if (e.response) { + console.error('HTTP Status:', e.response.status); + console.error('Response body:', e.response.data); + } + throw e; // Rethrow the error to let Cypress handle it } - return null; }, }); diff --git a/cypress/e2e/AnalysisPlugin.cy.ts b/cypress/e2e/AnalysisPlugin.cy.ts index c045a467d..65368d57d 100644 --- a/cypress/e2e/AnalysisPlugin.cy.ts +++ b/cypress/e2e/AnalysisPlugin.cy.ts @@ -14,7 +14,7 @@ describe('Report (formerly Analysis) Plugin', () => { Cypress.env('users').morty.realm, Cypress.env('users').morty.username, Cypress.env('users').morty.password - ).then(session => { + ).then(() => { cy.window().then(win => { const authToken = win.localStorage.getItem('nexus__token'); cy.wrap(authToken).as('nexusToken'); @@ -36,8 +36,14 @@ describe('Report (formerly Analysis) Plugin', () => { orgLabel, projectLabel, resourcePayload, - }).then((resource: Resource) => { - cy.wrap(resource['@id']).as('fullResourceId'); + }).then((resource: Resource | null) => { + if (resource && resource['@id']) { + cy.wrap(resource['@id']).as('fullResourceId'); + } else { + throw new Error( + 'Resource creation failed, received null resource object.' + ); + } }); }); }); @@ -54,14 +60,14 @@ describe('Report (formerly Analysis) Plugin', () => { ); }); - // after(function() { - // cy.task('project:teardown', { - // nexusApiUrl: Cypress.env('NEXUS_API_URL'), - // authToken: this.nexusToken, - // orgLabel: Cypress.env('ORG_LABEL'), - // projectLabel: this.projectLabel, - // }); - // }); + after(function() { + cy.task('project:teardown', { + nexusApiUrl: Cypress.env('NEXUS_API_URL'), + authToken: this.nexusToken, + orgLabel: Cypress.env('ORG_LABEL'), + projectLabel: this.projectLabel, + }); + }); it('user can add a report with name, description and files, categories, types', function() { cy.visit( diff --git a/cypress/e2e/ResourceContainer.cy.ts b/cypress/e2e/ResourceContainer.cy.ts index 6d336f39a..9c24eb7cd 100644 --- a/cypress/e2e/ResourceContainer.cy.ts +++ b/cypress/e2e/ResourceContainer.cy.ts @@ -41,8 +41,6 @@ describe('Resource with id that contains URL encoded characters', () => { orgLabel, projectLabel, resourcePayload, - }).then((resource: Resource) => { - cy.wrap(resource['@id']).as('fullResourceId'); }); } ); @@ -70,7 +68,7 @@ describe('Resource with id that contains URL encoded characters', () => { }); function testResourceDataInJsonViewer() { - cy.findByText('Advanced View').click(); + cy.findByTestId('admin-collapse').click(); cy.contains(`"@id"`); cy.contains(resourceIdWithEncodedCharacters); @@ -132,14 +130,18 @@ describe('Resource with id that contains URL encoded characters', () => { cy.wait('@idResolution').then(interception => { const resolvedResources = interception.response.body._results; - if (resolvedResources.length === 1) { + if (resolvedResources && resolvedResources.length === 1) { testResourceDataInJsonViewer(); } else { // Multiple resources with same id found. - cy.findByText('Open Resource', { - selector: `a[href="${resourcePage}"]`, - }).click(); + // find element by data-testid open-resource and click it. + cy.findByTestId('open-resource').click(); testResourceDataInJsonViewer(); + + // cy.findByText('Open Resource', { + // selector: `a[href="${resourcePage}"]`, + // }).click(); + // testResourceDataInJsonViewer(); } }); }); diff --git a/docs/development.md b/docs/development.md index d598d54e3..bd1df3ae0 100644 --- a/docs/development.md +++ b/docs/development.md @@ -85,116 +85,10 @@ console.log(add(1, 2)); They usually do some more advance operations, like tree-shaking for example, in order to minimise the final bundle size. -## Server-side rendering (SSR) - -In our case, we are doing server side rendering, which means we are rendering the HTML generated by our React app in the server first, and send that document to the client, along side the JS bundle so the browser can take over. - -Our code structure looks like: - -```txt -. -+-- server -| +-- index.ts -+-- client -| +-- index.ts -+-- shared -| +-- index.ts - -``` - -After we transpile and bundle, we end up with: - -```txt -. -+-- server -| +-- index.js -+-- public -| +-- bundle.js -+-- shared -| +-- index.js - -``` - -## ts-node and Hot module replacement (HMR) - -Being able to compile our application into runnable server/client code is cool, but running our tool chain on every change isn't ideal in term of development flow. - -### ts-node - -The [ts-node](https://github.com/TypeStrong/ts-node) CLI is like `node` CLI but is does TypeScript transpiling on the fly. The same way you can run `node index.js` on a JS file, you can run `ts-node index.ts` on a TS file. It is quite handy to be able to run our TS code directly, without compiling it first. This is a development too however an you should **not** use it in production. For babel-based project, you can use [babel-node](https://babeljs.io/docs/en/next/babel-node.html). - -Combined with [nodemon](https://github.com/remy/nodemon), you can also restart your server when a file has been changed. - -You start script would look like that: - -```json -{ - "start": "nodemon server/index.ts --exec ts-node" -} -``` - -### HMR - -On the client, traditionally we would take a similar approach with technologies such as _livereload_ but we don't want to reload the page _every time_ we have a new bundle. The reason for that is, bt reloading the page we loose the current context. - -If you are editing a popup for example, you after to click on the button that triggers the popup after each reload, or if you are editing step 4 of a form, you have to complete step 1/2/3 before reaching your changes. You can partially solve this by having a very strict stateless application (reload /form/step/2) and re-hydrate the state of your app but if a particular part of your app is dependent on fetching data first, you'll make async calls on each reload (and that data might not be stateless, which means side effects can break your logic). - -Instead what we can do is Hot Module Replacement, which will dynamically re-render your app, using the same state but with the new code, and dynamically replace it on the client. You can read more about HMR online, there are tons of articles about it. - -[Webpack](https://webpack.js.org/concepts/hot-module-replacement/) does support HMR. You can set it up directly into your webpack config and using Webpack CLI, or you can use [webpack-hot-middleware](https://github.com/webpack-contrib/webpack-hot-middleware) for express (the web server framework we use). - -Combined with [webpack-dev-middleware](https://github.com/webpack/webpack-dev-middleware), we can trigger our webpack build directly from our express, when we run in development mode. - -```javascript -// our express app -const app = express(); - -// load webpack -const webpack = require('webpack'); - -// load our webpack the config -const webpackConfig = require('../../webpack.config'); - -// we need to overwrite and add a few things -const devConfig = {...webpackConfig, { - mode: 'development', // mode is development (no minify, watch changes, etc...) - entry: {...webpackConfig.entry, { - bundle: [ - ...webpackConfig.entry.bundle, - 'webpack-hot-middleware/client', // add hmr client js code to the bundle - ], - }}, - plugins: [ - ...webpackConfig.plugins, - new webpack.HotModuleReplacementPlugin() // run the HMR plugin - ], -}}; - -// create a new webpack compiler with our dev config -const compiler = webpack(devConfig); - -// add the dev middleware (runs webpack when server starts) -app.use(require('webpack-dev-middleware')(compiler, { - publicPath: '/public', // this needs to match the public path from our config.output.publicPath if set -})); - -// add the HMR middleware -app.use(require('webpack-hot-middleware')(compiler, { - path: '/__webpack_hmr', - timeout: 20000, -})); -``` - -This means that for development, all we need to run is `ts-node server/index.ts`. - ## Lint We use `ts-lint` with Airbnb rules. -## Unit and Integration tests - -We use `jest` with `react-testing-library``. - ## End-to-End Testing End-to-testing is implemented with [Cypress]('https://www.cypress.io'). The `cypress-testing-library`` package is used to support the same dom-testing queries as used in our unit and integration tests. @@ -220,7 +114,7 @@ CYPRESS_AUTH_REALM=https://auth.realm.url/ \ CYPRESS_AUTH_USERNAME=nexus_username \ CYPRESS_AUTH_PASSWORD=nexus_password \ CYPRESS_NEXUS_API_URL=https://nexusapi.url/v1 \ -yarn cy:open --e2e --browser chrome +yarn e2e:open --e2e --browser chrome ``` ### CLI diff --git a/package.json b/package.json index e17dd413c..7b460f2cd 100644 --- a/package.json +++ b/package.json @@ -66,7 +66,7 @@ "deep-object-diff": "^1.1.0", "express": "^4.18.2", "handlebars": "^4.7.7", - "history": "^4.7.2", + "history": "4.5.1", "https-browserify": "^1.0.0", "json2csv": "^5.0.5", "jwt-decode": "^2.2.0", @@ -228,7 +228,8 @@ }, "resolutions": { "d3-interpolate": "2.0.1", - "headers-polyfill": "3.0.10" + "headers-polyfill": "3.0.10", + "history": "4.5.1" }, "overrides": { "d3-interpolate": "2.0.1" diff --git a/public/screenshots/001.png b/public/screenshots/001.png new file mode 100644 index 000000000..e7b1c7182 Binary files /dev/null and b/public/screenshots/001.png differ diff --git a/public/screenshots/002.png b/public/screenshots/002.png new file mode 100644 index 000000000..75788aa39 Binary files /dev/null and b/public/screenshots/002.png differ diff --git a/public/screenshots/003.png b/public/screenshots/003.png new file mode 100644 index 000000000..4dea506bf Binary files /dev/null and b/public/screenshots/003.png differ diff --git a/public/screenshots/004.png b/public/screenshots/004.png new file mode 100644 index 000000000..60915835a Binary files /dev/null and b/public/screenshots/004.png differ diff --git a/public/web-manifest.json b/public/web-manifest.json index 92b4864b1..f4be65f94 100644 --- a/public/web-manifest.json +++ b/public/web-manifest.json @@ -1,14 +1,48 @@ { - "name": "Nexus Fusion", - "short_name": "BBP-NF", + "name": "Blue Brain Nexus Fusion", + "short_name": "Fusion", "description": "The interface of Blue Brain Nexus, the open-source knowledge graph for data-driven science.", "dir": "left", "lang": "en-US", "display": "standalone", - "orientation": "portrait", + "orientation": "landscape", "background_color": "#fff", - "theme_color": "#fff", - "start_url": "", + "theme_color": "#050a56", + "start_url": "/", + "scope": "/", + "categories": [ + "science", + "knowledge graph", + "neuroscience", + "data-driven", + "in silico" + ], + "screenshots": [ + { + "src": "screenshots/001.png", + "form_factor": "wide", + "type": "image/png", + "sizes": "3024x1694" + }, + { + "src": "screenshots/002.png", + "form_factor": "wide", + "type": "image/png", + "sizes": "3024x1694" + }, + { + "src": "screenshots/003.png", + "form_factor": "wide", + "type": "image/png", + "sizes": "3024x1694" + }, + { + "src": "screenshots/004.png", + "form_factor": "wide", + "type": "image/png", + "sizes": "3024x1694" + } + ], "icons": [ { "src": "favicon-64x64.png", diff --git a/server/index.ts b/server/index.ts index dd8006b49..a143c1e85 100644 --- a/server/index.ts +++ b/server/index.ts @@ -190,7 +190,7 @@ app.get(`${base}/web-manifest`, (req, res) => { NODE_ENV === 'development' ? req.protocol : 'https' }://${host}${base}`; - const manifestTempalte = fs.readFileSync( + const manifestTemplate = fs.readFileSync( path.join( __dirname, NODE_ENV === 'development' ? '../public' : '', @@ -199,7 +199,7 @@ app.get(`${base}/web-manifest`, (req, res) => { 'utf-8' ); - const manifest = manifestTempalte.replace('', startUrl); + const manifest = manifestTemplate.replace('', startUrl); res.header('content-type', 'application/json'); return res.status(200).send(manifest); diff --git a/src/shared/components/EditTableForm.tsx b/src/shared/components/EditTableForm.tsx index 893dfecc2..e3126f0cb 100644 --- a/src/shared/components/EditTableForm.tsx +++ b/src/shared/components/EditTableForm.tsx @@ -274,14 +274,18 @@ const EditTableForm: React.FC<{ return Object.assign(result, current); }, {}); - return Object.keys(mergedItem).map(title => ({ - '@type': '', - name: title, - format: '', - enableSearch: false, - enableSort: false, - enableFilter: false, - })); + return Object.keys(mergedItem).map(title => { + const currentItemConfig = findCurrentColumnConfig(title); + return { + '@type': '', + name: title, + format: '', + enableSearch: false, + enableSort: false, + enableFilter: false, + ...currentItemConfig, + }; + }); } const result = await querySparql( @@ -296,14 +300,19 @@ const EditTableForm: React.FC<{ .sort((a, b) => { return a.title > b.title ? 1 : -1; }) - .map(x => ({ - '@type': 'text', - name: x.dataIndex, - format: '', - enableSearch: false, - enableSort: false, - enableFilter: false, - })); + .map(header => { + const currentHeaderConfig = + findCurrentColumnConfig(header.dataIndex) ?? {}; + return { + '@type': 'text', + name: header.dataIndex, + format: '', + enableSearch: false, + enableSort: false, + enableFilter: false, + ...currentHeaderConfig, + }; + }); }) .catch(error => { // Sometimes delta's error message can be in `name` or `reason` field. @@ -323,12 +332,7 @@ const EditTableForm: React.FC<{ { onSuccess: data => { updateTableDataError(null); - if ( - isNil(configuration) || - (configuration as TableColumn[]).length === 0 - ) { - setConfiguration(data); - } + setConfiguration(data); }, onError: (error: Error) => { updateTableDataError(error); @@ -340,6 +344,16 @@ const EditTableForm: React.FC<{ } ); + const findCurrentColumnConfig = (name: string) => { + if (Array.isArray(configuration)) { + return configuration.find(column => column.name === name); + } + if (configuration?.name === name) { + return { ...configuration }; + } + return undefined; + }; + const onChangeName = (event: any) => { setName(event.target.value); setNameError(false); diff --git a/src/shared/containers/AdminPluginContainer.tsx b/src/shared/containers/AdminPluginContainer.tsx index 79e142ed0..3905a91fa 100644 --- a/src/shared/containers/AdminPluginContainer.tsx +++ b/src/shared/containers/AdminPluginContainer.tsx @@ -192,6 +192,7 @@ const AdminPlugin: React.FunctionComponent = ({ return ( { message="Resource resolved successfully" description={