diff --git a/build_scripts/webpack-rules.js b/build_scripts/webpack-rules.js index bc22da1f4cd..e2587a2c188 100644 --- a/build_scripts/webpack-rules.js +++ b/build_scripts/webpack-rules.js @@ -118,10 +118,5 @@ module.exports = [ { test: /\.html?$/, use: ['html-loader'] - }, - { - test: /\.(graphql|gql)$/, - exclude: /node_modules/, - loader: 'graphql-tag/loader' } ] diff --git a/e2e_tests/integration/desktop-env-url.spec.js b/e2e_tests/integration/desktop-env-url.spec.js new file mode 100644 index 00000000000..2b4d164e8eb --- /dev/null +++ b/e2e_tests/integration/desktop-env-url.spec.js @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2002-2019 "Neo4j," + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +/* global Cypress, cy, test, expect, before */ + +import { getDesktopContext } from '../support/utils' +let appContextListener + +// This file only esists to be able to test the auto connect using +// the host field. +// We can't load two times in the same file + +describe('Neo4j Desktop environment using url field', () => { + before(() => { + cy.visit(Cypress.config('url'), { + onBeforeLoad: win => { + win.neo4jDesktopApi = { + getContext: () => + Promise.resolve(getDesktopContext(Cypress.config, 'url')), + onContextUpdate: fn => (appContextListener = fn.bind(fn)) + } + } + }) + }) + it('can auto connect using url field', () => { + const frames = cy.get('[data-testid="frameCommand"]', { timeout: 10000 }) + frames.should('have.length', 2) + + // Auto connected = :play start + frames.first().should('contain', ':play start') + cy.wait(1000) + }) + it('switches connection when that event is triggered using url field', () => { + cy.executeCommand(':clear') + cy.wait(1000).then(() => { + appContextListener( + { type: 'GRAPH_ACTIVE', id: 'test' }, + getDesktopContext(Cypress.config, 'url') + ) + }) + + const frames = cy.get('[data-testid="frameCommand"]', { timeout: 10000 }) + frames.should('have.length', 1) + + frames.first().should('contain', ':server switch success') + + cy.get('[data-testid="frame"]', { timeout: 10000 }) + .first() + .should('contain', 'Connection updated') + }) +}) diff --git a/e2e_tests/integration/desktop-env.spec.js b/e2e_tests/integration/desktop-env.spec.js new file mode 100644 index 00000000000..bec363230e4 --- /dev/null +++ b/e2e_tests/integration/desktop-env.spec.js @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2002-2019 "Neo4j," + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +/* global Cypress, cy, test, expect, before */ +import { getDesktopContext } from '../support/utils' + +let appContextListener + +describe('Neo4j Desktop environment', () => { + before(() => { + cy.visit(Cypress.config('url'), { + onBeforeLoad: win => { + win.neo4jDesktopApi = { + getContext: () => + Promise.resolve(getDesktopContext(Cypress.config, 'host')), + onContextUpdate: fn => (appContextListener = fn.bind(fn)) + } + } + }) + }) + it('can auto connect using host + post fields', () => { + const frames = cy.get('[data-testid="frameCommand"]', { timeout: 10000 }) + frames.should('have.length', 2) + + // Auto connected = :play start + frames.first().should('contain', ':play start') + cy.wait(1000) + }) + it('switches connection when that event is triggered using host + port fields', () => { + cy.executeCommand(':clear') + cy.wait(1000).then(() => { + appContextListener( + { type: 'GRAPH_ACTIVE', id: 'test' }, + getDesktopContext(Cypress.config, 'host') + ) + }) + + const frames = cy.get('[data-testid="frameCommand"]', { timeout: 10000 }) + frames.should('have.length', 1) + + frames.first().should('contain', ':server switch success') + + cy.get('[data-testid="frame"]', { timeout: 10000 }) + .first() + .should('contain', 'Connection updated') + }) +}) diff --git a/jest.config.js b/jest.config.js index 9c0120a6f0b..f0ae80d7dd6 100644 --- a/jest.config.js +++ b/jest.config.js @@ -17,7 +17,6 @@ module.exports = { '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga|html)$': '/test_utils/__mocks__/fileMock.js', '\\.(css|less)$': '/test_utils/__mocks__/styleMock.js', - '\\.(graphql)$': '/test_utils/__mocks__/graphqlMock.js', '^browser-styles(.*)$': '/src/browser/styles$1', '^browser-components(.*)$': '/src/browser/components$1', '^browser-hooks(.*)$': '/src/browser/hooks$1', diff --git a/package.json b/package.json index 2c778cc6e0d..78b4858ac80 100644 --- a/package.json +++ b/package.json @@ -123,18 +123,9 @@ "xml2js": "^0.4.19" }, "dependencies": { - "@apollo/react-hooks": "^3.0.1", "@neo4j/browser-lambda-parser": "^1.0.3", "@relate-by-ui/css": "^1.0.4", "@relate-by-ui/saved-scripts": "^1.0.4", - "apollo-boost": "^0.4.4", - "apollo-cache-inmemory": "^1.6.3", - "apollo-link": "^1.2.12", - "apollo-link-context": "^1.0.18", - "apollo-link-error": "^1.1.11", - "apollo-link-http": "^1.5.15", - "apollo-link-ws": "^1.0.18", - "apollo-utilities": "^1.3.2", "ascii-data-table": "^2.1.1", "classnames": "^2.2.5", "codemirror": "^5.29.0", @@ -147,8 +138,6 @@ "dompurify": "^1.0.11", "file-saver": "^1.3.8", "firebase": "^5.8.3", - "graphql": "^14.4.2", - "graphql-tag": "^2.10.1", "isomorphic-fetch": "^2.2.1", "jsonic": "^0.3.0", "jszip": "^3.2.2", @@ -175,7 +164,6 @@ "styled-components": "^4.0.0", "stylis": "^3.4.10", "suber": "^5.0.1", - "subscriptions-transport-ws": "^0.9.16", "swipe-js-iso": "^2.0.4", "url-parse": "^1.1.9", "uuid": "^3.0.1" diff --git a/src/browser/AppInit.jsx b/src/browser/AppInit.jsx index eb4c00579ef..8ff02e749b8 100644 --- a/src/browser/AppInit.jsx +++ b/src/browser/AppInit.jsx @@ -27,7 +27,6 @@ import { createReduxMiddleware as createSuberReduxMiddleware } from 'suber' import { BusProvider } from 'react-suber' - import App from './modules/App/App' import reducers from 'shared/rootReducer' import epics from 'shared/rootEpic' @@ -37,7 +36,6 @@ import { APP_START } from 'shared/modules/app/appDuck' import { GlobalStyle } from './styles/global-styles.js' import { detectRuntimeEnv } from 'services/utils.js' import { NEO4J_CLOUD_DOMAINS } from 'shared/modules/settings/settingsDuck.js' -import RelateApiProvider from 'browser-components/relate-api/relate-api-provider' // Configure localstorage sync applyKeys( @@ -85,11 +83,8 @@ bus.applyMiddleware((_, origin) => (channel, message, source) => { // Introduce environment to be able to fork functionality const env = detectRuntimeEnv(window, NEO4J_CLOUD_DOMAINS) -// URL we're on -const url = window.location.href - // Signal app upstart (for epics) -store.dispatch({ type: APP_START, url, env }) +store.dispatch({ type: APP_START, url: window.location.href, env }) const AppInit = () => { return ( @@ -97,9 +92,11 @@ const AppInit = () => { - - - + diff --git a/src/browser/components/DesktopIntegration/__snapshots__/index.test.js.snap b/src/browser/components/DesktopIntegration/__snapshots__/index.test.js.snap new file mode 100644 index 00000000000..57c681448cb --- /dev/null +++ b/src/browser/components/DesktopIntegration/__snapshots__/index.test.js.snap @@ -0,0 +1,11 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` calls onMount with data on mounting 1`] = `
`; + +exports[` calls onMount with data on mounting 2`] = `
`; + +exports[` calls onXxx with data on event XXX 1`] = `
`; + +exports[` does not render anything if no integration point 1`] = `
`; + +exports[` does not render anything if there is an integration point 1`] = `
`; diff --git a/src/browser/components/DesktopIntegration/helpers.js b/src/browser/components/DesktopIntegration/helpers.js new file mode 100644 index 00000000000..ec2913ce4ce --- /dev/null +++ b/src/browser/components/DesktopIntegration/helpers.js @@ -0,0 +1,131 @@ +/* + * Copyright (c) 2002-2019 "Neo4j," + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +import { NATIVE, KERBEROS } from 'services/bolt/boltHelpers' + +export const getActiveGraph = (context = {}) => { + if (!context) return null + const { projects } = context + if (!Array.isArray(projects)) return null + const activeProject = projects.find(project => { + if (!project) return false + if (!(project.graphs && Array.isArray(project.graphs))) return false + return project.graphs.find(({ status }) => status === 'ACTIVE') + }) + if (!activeProject) return null + return activeProject.graphs.find(({ status }) => status === 'ACTIVE') +} + +export const getCredentials = (type, connection = null) => { + if (!connection) return null + const { configuration = null } = connection + if (!configuration) { + return null + } + if (!configuration.protocols) { + return null + } + if (typeof configuration.protocols[type] === 'undefined') { + return null + } + return configuration.protocols[type] +} + +// XXX_YYY -> onXxxYyy +export const eventToHandler = type => { + if (typeof type !== 'string') return null + return ( + 'on' + + splitOnUnderscore(type) + .filter(notEmpty) + .map(toLower) + .map(upperFirst) + .join('') + ) +} +const notEmpty = str => str.length > 0 +const splitOnUnderscore = str => str.split('_') +const toLower = str => str.toLowerCase() +const upperFirst = str => str[0].toUpperCase() + str.substring(1) + +export const didChangeActiveGraph = (newContext, oldContext) => { + const oldActive = getActiveGraph(oldContext) + const newActive = getActiveGraph(newContext) + if (!oldActive && !newActive) return false // If no active before and after = nu change + return !(oldActive && newActive && newActive.id === oldActive.id) +} + +export const getActiveCredentials = (type, context) => { + const activeGraph = getActiveGraph(context) + if (!activeGraph || typeof activeGraph.connection === 'undefined') return null + const creds = getCredentials(type, activeGraph.connection) + return creds || null +} + +const isKerberosEnabled = context => { + const activeGraph = getActiveGraph(context) + if (!activeGraph || typeof activeGraph.connection === 'undefined') { + return false + } + if (!activeGraph.connection) return null + const { configuration = null } = activeGraph.connection + if (!configuration) { + return false + } + if ( + !configuration.authenticationMethods || + !configuration.authenticationMethods.kerberos + ) { + return false + } + if (!configuration.authenticationMethods.kerberos.enabled) { + return false + } + return configuration.authenticationMethods.kerberos +} + +export const buildConnectionCredentialsObject = async ( + context, + existingData = {}, + getKerberosTicket = () => {} +) => { + const creds = getActiveCredentials('bolt', context) + if (!creds) return // No connection. Ignore and let browser show connection lost msgs. + const httpsCreds = getActiveCredentials('https', context) + const httpCreds = getActiveCredentials('http', context) + const kerberos = isKerberosEnabled(context) + if (kerberos !== false) { + creds.password = await getKerberosTicket(kerberos.servicePrincipal) + } + const restApi = + httpsCreds && httpsCreds.enabled + ? `https://${httpsCreds.host}:${httpsCreds.port}` + : `http://${httpCreds.host}:${httpCreds.port}` + const connectionCreds = { + // Use current connections creds until we get new from API + ...existingData, + ...creds, + encrypted: creds.tlsLevel === 'REQUIRED', + host: creds.url || `bolt://${creds.host}:${creds.port}`, + restApi, + authenticationMethod: kerberos ? KERBEROS : NATIVE + } + return connectionCreds +} diff --git a/src/browser/components/DesktopIntegration/helpers.test.js b/src/browser/components/DesktopIntegration/helpers.test.js new file mode 100644 index 00000000000..e8a1c566b44 --- /dev/null +++ b/src/browser/components/DesktopIntegration/helpers.test.js @@ -0,0 +1,345 @@ +/* + * Copyright (c) 2002-2019 "Neo4j," + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +/* global test, expect, jest */ +import { + getCredentials, + getActiveGraph, + eventToHandler, + didChangeActiveGraph, + getActiveCredentials, + buildConnectionCredentialsObject +} from './helpers' +import { KERBEROS, NATIVE } from 'services/bolt/boltHelpers' + +test('getActiveGraph handles non objects and non-active projects', () => { + // Given + const graphs = [ + null, + 'string', + undefined, + [1], + { project: null }, + { projects: { x: 1 } }, + { projects: [{ x: 1 }] }, + { projects: [{ graphs: [{ status: 'NOPE' }] }] } + ] + + // When && Then + graphs.forEach(graph => { + expect(getActiveGraph(graph)).toEqual(null) + }) +}) +test('getActiveGraph handles expected objects', () => { + // Given + const graph = { + status: 'ACTIVE' + } + const graph2 = { + status: 'INACTIVE' + } + const apiResponse = { + projects: [ + { + graphs: [graph, graph2] + } + ] + } + + // When + const activeGraph = getActiveGraph(apiResponse) + + // Then + expect(activeGraph).toEqual(graph) +}) + +test('getCredentials handles non objects', () => { + // Given + const configs = [null, 'string', undefined, [1]] + + // When && Then + configs.forEach(config => { + expect(getCredentials('xxx', config)).toBe(null) + }) +}) + +test('getCredentials finds credentials on expected format', () => { + // Given + + const config = { + bolt: { + username: 'molly', + password: 'stella' + }, + http: { + username: 'oskar', + password: 'picachu' + } + } + const connection = { + configuration: { protocols: config } + } + + // When + const boltRes = getCredentials('bolt', connection) + const httpRes = getCredentials('http', connection) + const notFoundRes = getCredentials('https', connection) + + // Then + expect(boltRes).toEqual(config.bolt) + expect(httpRes).toEqual(config.http) + expect(notFoundRes).toBe(null) +}) + +test('XXX_YYY -> onXxxYyy', () => { + // Given + const tests = [ + { type: undefined, expect: null }, + { type: true, expect: null }, + { type: 'XXX', expect: 'onXxx' }, + { type: '_XXX', expect: 'onXxx' }, + { type: 'XXX_YYY', expect: 'onXxxYyy' }, + { type: 'XXX_YYY_ZZZ', expect: 'onXxxYyyZzz' }, + { type: 'xxx', expect: 'onXxx' }, + { type: 'xxx_yyy', expect: 'onXxxYyy' }, + { type: 'XXX_123', expect: 'onXxx123' }, + { type: '0', expect: 'on0' }, + { type: '1', expect: 'on1' }, + { type: 1, expect: null } + ] + + // When && Then + tests.forEach(test => { + expect(eventToHandler(test.type)).toEqual(test.expect) + }) +}) + +test('didChangeActiveGraph detects if the active graph changed', () => { + // Given + const createApiResponse = graphs => ({ + projects: [{ graphs }] + }) + const id1Active = createApiResponse([ + { id: 1, status: 'ACTIVE' }, + { id: 2, status: 'INACTIVE' } + ]) + const id2Active = createApiResponse([ + { id: 1, status: 'INACTIVE' }, + { id: 2, status: 'ACTIVE' } + ]) + const noActive = createApiResponse([ + { id: 1, status: 'INACTIVE' }, + { id: 2, status: 'INACTIVE' } + ]) + + // When + const noChange = didChangeActiveGraph(id1Active, id1Active) + const didChange = didChangeActiveGraph(id2Active, id1Active) + const didChange2 = didChangeActiveGraph(noActive, id1Active) + const noChange2 = didChangeActiveGraph(noActive, noActive) + + // Then + expect(noChange).toBe(false) + expect(didChange).toBe(true) + expect(didChange2).toBe(true) + expect(noChange2).toBe(false) +}) + +test('getActiveCredentials finds the active connection from a context object and returns the creds', () => { + // Given + const bolt1 = { + username: 'one', + password: 'one1' + } + const bolt2 = { + username: 'two', + password: 'two2' + } + const createApiResponse = graphs => ({ + projects: [{ graphs }] + }) + const id1Active = createApiResponse([ + { + id: 1, + status: 'ACTIVE', + connection: { + configuration: { + protocols: { bolt: bolt1 } + } + } + }, + { + id: 2, + status: 'INACTIVE', + connection: { + configuration: { + protocols: { bolt: bolt2 } + } + } + } + ]) + const id2Active = createApiResponse([ + { + id: 1, + status: 'INACTIVE', + connection: { + configuration: { + protocols: { bolt: bolt1 } + } + } + }, + { + id: 2, + status: 'ACTIVE', + connection: { + configuration: { + protocols: { bolt: bolt2 } + } + } + } + ]) + const noActive = createApiResponse([ + { + id: 1, + status: 'INACTIVE', + connection: { + configuration: { + protocols: { bolt: bolt1 } + } + } + }, + { + id: 2, + status: 'INACTIVE', + connection: { + configuration: { + protocols: { bolt: bolt2 } + } + } + } + ]) + + // When + const firstActive = getActiveCredentials('bolt', id1Active) + const secondActive = getActiveCredentials('bolt', id2Active) + const zeroActive = getActiveCredentials('bolt', noActive) + + // Then + expect(firstActive).toEqual(bolt1) + expect(secondActive).toEqual(bolt2) + expect(zeroActive).toEqual(null) +}) + +test('getActiveCredentials should extract https and http creds', () => { + const activeConnectionData = createApiResponse(activeGraph()) + + expect(getActiveCredentials('bolt', activeConnectionData)).toEqual(bolt()) + expect(getActiveCredentials('http', activeConnectionData)).toEqual(http) + expect(getActiveCredentials('https', activeConnectionData)).toEqual(https) + expect(getActiveCredentials('foobar', activeConnectionData)).toBeFalsy() +}) + +describe('buildConnectionCredentialsObject', () => { + test('it creates an expected object from context, and adds kerberos ticket as password', async () => { + // Given + const kerberosTicket = 'kerberos-ticket-test' + const activeConnectionData = createApiResponse( + activeGraph({ enc: 'REQUIRED', kerberos: true }) + ) + const getKerberosTicket = jest.fn(() => kerberosTicket) + const connectionData = await buildConnectionCredentialsObject( + activeConnectionData, + {}, + getKerberosTicket + ) + expect(connectionData).toEqual({ + username: 'one', + password: kerberosTicket, + url: 'bolt:port', + tlsLevel: 'REQUIRED', + encrypted: true, + host: 'bolt:port', + restApi: 'http://foo:bar', + authenticationMethod: KERBEROS + }) + expect(getKerberosTicket).toHaveBeenCalledTimes(1) + expect(getKerberosTicket).toHaveBeenCalledWith('https') + }) + test('it creates an expected object from context, without kerberos', async () => { + // Given + const kerberosTicket = 'kerberos-ticket-test' + const getKerberosTicket = jest.fn(() => kerberosTicket) + const activeConnectionData = createApiResponse( + activeGraph({ enc: 'REQUIRED', kerberos: false }) + ) + const connectionData = await buildConnectionCredentialsObject( + activeConnectionData, + {}, + getKerberosTicket + ) + expect(connectionData).toEqual({ + username: 'one', + password: 'one1', + url: 'bolt:port', + tlsLevel: 'REQUIRED', + encrypted: true, + host: 'bolt:port', + restApi: 'http://foo:bar', + authenticationMethod: NATIVE + }) + expect(getKerberosTicket).toHaveBeenCalledTimes(0) + }) +}) + +const bolt = (enc = 'OPTIONAL') => ({ + username: 'one', + password: 'one1', + url: 'bolt:port', + tlsLevel: enc +}) +const http = { + host: 'foo', + port: 'bar' +} + +const https = { + host: 'abc', + port: 'xyz' +} +const createApiResponse = graphs => ({ + projects: [{ graphs }] +}) + +const activeGraph = (props = { enc: 'OPTIONAL', kerberos: false }) => [ + { + id: 1, + status: 'ACTIVE', + connection: { + configuration: { + protocols: { bolt: bolt(props.enc), http, https }, + authenticationMethods: { + kerberos: { + enabled: props.kerberos, + servicePrincipal: 'https' + } + } + } + } + } +] diff --git a/src/browser/components/DesktopIntegration/index.jsx b/src/browser/components/DesktopIntegration/index.jsx new file mode 100644 index 00000000000..d12f1d9ecb3 --- /dev/null +++ b/src/browser/components/DesktopIntegration/index.jsx @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2002-2019 "Neo4j," + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +import { Component } from 'react' +import { getActiveGraph, getCredentials, eventToHandler } from './helpers' + +export default class DesktopIntegration extends Component { + setupListener () { + const { integrationPoint, onArgumentsChange = null } = this.props + if (integrationPoint && integrationPoint.onContextUpdate) { + const getKerberosTicket = + integrationPoint.getKerberosTicket || function () {} + integrationPoint.onContextUpdate((event, newContext, oldContext) => { + const handlerPropName = eventToHandler(event.type) + if (!handlerPropName) return + if (typeof this.props[handlerPropName] === 'undefined') return + this.props[handlerPropName]( + event, + newContext, + oldContext, + getKerberosTicket + ) + }) + } + if ( + integrationPoint && + integrationPoint.onArgumentsChange && + onArgumentsChange + ) { + integrationPoint.onArgumentsChange(onArgumentsChange) + } + } + loadInitialContext () { + const { integrationPoint, onMount = null } = this.props + if (integrationPoint && integrationPoint.getContext) { + const getKerberosTicket = + integrationPoint.getKerberosTicket || function () {} + integrationPoint + .getContext() + .then(context => { + const activeGraph = getActiveGraph(context) || {} + if (onMount) { + const connectionCredentials = getCredentials( + 'bolt', + activeGraph.connection || null + ) + onMount( + activeGraph, + connectionCredentials, + context, + getKerberosTicket + ) + } + }) + .catch(e => {}) // Catch but don't bother + } + } + componentDidMount () { + this.loadInitialContext() + this.setupListener() + } + render () { + return null + } +} diff --git a/src/browser/components/DesktopIntegration/index.test.js b/src/browser/components/DesktopIntegration/index.test.js new file mode 100644 index 00000000000..6e657b5ea3a --- /dev/null +++ b/src/browser/components/DesktopIntegration/index.test.js @@ -0,0 +1,148 @@ +/* + * Copyright (c) 2002-2019 "Neo4j," + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +/* global test, expect, jest */ + +import React from 'react' +import { render, wait } from '@testing-library/react' +import DesktopIntegration from './index' + +describe('', () => { + test('does not render anything if no integration point', () => { + // Given + const integrationPoint = null + + // When + const { container } = render( + + ) + + // Then + expect(container).toMatchSnapshot() + }) + test('does not render anything if there is an integration point', () => { + // Given + const integrationPoint = { x: true } + + // When + const { container } = render( + + ) + + // Then + expect(container).toMatchSnapshot() + }) + test('calls onMount with data on mounting', async () => { + // Given + const mFn = jest.fn() + const context = { + projects: [ + { + graphs: [ + { + status: 'ACTIVE', + configuration: { + protocols: { + bolt: { + username: 'neo4j' + } + } + } + } + ] + } + ] + } + const integrationPoint = { getContext: () => Promise.resolve(context) } + + // When + const { container, rerender } = render( + + ) + await wait(() => expect(mFn).toHaveBeenCalledTimes(1)) + // Then + expect(container).toMatchSnapshot() + + // When + rerender() + + // Then + expect(mFn).toHaveBeenCalledTimes(1) + expect(container).toMatchSnapshot() + }) + test('calls onXxx with data on event XXX', () => { + // Given + let componentOnContextUpdate + const fn = jest.fn() + const oldContext = { projects: [] } + const newContext = { projects: [{ project: {} }] } + const event = { type: 'XXX' } + const nonListenEvent = { type: 'YYY' } + const integrationPoint = { + onContextUpdate: fn => (componentOnContextUpdate = fn), + getKerberosTicket: jest.fn() + } + + // When + const { container } = render( + + ) + + // Then + expect(fn).toHaveBeenCalledTimes(0) + + // When + componentOnContextUpdate(event, newContext, oldContext) + + // Then + expect(fn).toHaveBeenCalledTimes(1) + expect(fn).toHaveBeenLastCalledWith( + event, + newContext, + oldContext, + integrationPoint.getKerberosTicket + ) + + // When + componentOnContextUpdate(nonListenEvent, newContext, oldContext) // We don't listen for this + + // Then + expect(fn).toHaveBeenCalledTimes(1) + expect(fn).toHaveBeenLastCalledWith( + event, + newContext, + oldContext, + integrationPoint.getKerberosTicket + ) + + // When + componentOnContextUpdate(event, newContext, oldContext) // Another one we're listening on + + // Then + expect(fn).toHaveBeenCalledTimes(2) + expect(fn).toHaveBeenLastCalledWith( + event, + newContext, + oldContext, + integrationPoint.getKerberosTicket + ) + expect(container).toMatchSnapshot() + }) +}) diff --git a/src/browser/components/relate-api/on-workspace-change.graphql b/src/browser/components/relate-api/on-workspace-change.graphql deleted file mode 100644 index b64fb2b6ee8..00000000000 --- a/src/browser/components/relate-api/on-workspace-change.graphql +++ /dev/null @@ -1,63 +0,0 @@ -subscription { - onWorkspaceChange { - me { - activationKeys { - featureName - expirationDate - } - name - } - host { - prefersColorScheme - publicInternetAccess - settings { - allowSendReports - allowSendStats - allowStoreCredentials - } - } - projects { - id - name - graphs { - id - name - status - connection { - principals { - path - protocols { - bolt { - enabled - host - password - port - tlsLevel - url - username - } - http { - enabled - host - port - url - } - https { - enabled - host - port - url - } - } - authenticationMethods { - servicePrincipal - kerberos { - enabled - } - } - } - } - } - } - } -} diff --git a/src/browser/components/relate-api/relate-api-provider.jsx b/src/browser/components/relate-api/relate-api-provider.jsx deleted file mode 100644 index a5c0eed08a5..00000000000 --- a/src/browser/components/relate-api/relate-api-provider.jsx +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright (c) 2002-2019 "Neo4j," - * Neo4j Sweden AB [http://neo4j.com] - * - * This file is part of Neo4j. - * - * Neo4j is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program 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 General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -import React from 'react' -import { ApolloProvider } from '@apollo/react-hooks' -import { createClient } from 'browser-components/relate-api/relate-api.utils' - -export default function RelateApiProvider ({ urlString, children }) { - // Load relate api graphql client - const url = new URL(urlString) - const apiEndpoint = url.searchParams.get('neo4jDesktopApiUrl') - const apiClientId = url.searchParams.get('neo4jDesktopGraphAppClientId') - - // If not in relate-api env, render children - if (!apiEndpoint) { - return children - } - const relateApiClient = createClient(apiEndpoint, apiClientId) - - return {children} -} diff --git a/src/browser/components/relate-api/relate-api.hooks.jsx b/src/browser/components/relate-api/relate-api.hooks.jsx deleted file mode 100644 index a2e80e871ee..00000000000 --- a/src/browser/components/relate-api/relate-api.hooks.jsx +++ /dev/null @@ -1,93 +0,0 @@ -import { useState, useEffect } from 'react' -import { useApolloClient } from '@apollo/react-hooks' -import workspaceQuery from './workspace.graphql' -import workspaceSubscription from './on-workspace-change.graphql' -import { getActiveGraphData, getPrefersColorScheme } from './relate-api.utils' -import { deepEquals } from 'services/utils' - -export function useWorkspaceData () { - let client - // We might not be in a GraphQL env - try { - client = useApolloClient() - } catch (e) {} - const [workspaceData, setState] = useState(null) - useEffect(() => { - async function fetchData () { - if (!client) { - return - } - const { data } = await client.query({ query: workspaceQuery }) - setState(data) - } - fetchData() - }, []) // initial load only - - return workspaceData -} - -export function useWorkspaceDataOnChange () { - let client - // We might not be in a GraphQL env - try { - client = useApolloClient() - } catch (e) {} - const [workspaceData, setState] = useState(null) - useEffect(() => { - if (!client) { - return - } - const observer = client.subscribe({ query: workspaceSubscription }) - observer.subscribe({ - next: ({ data }) => { - setState(data) - }, - error (err) { - console.error('err', err) - } - }) - return () => observer.unsubscribe() - }, []) // initial load only - - return workspaceData -} - -export function useActiveGraphMonitor (onWorkspaceChangeData) { - const [activeGraph, setActiveGraph] = useState(undefined) - useEffect( - () => { - // Wait until initial data comes back - if (activeGraph === undefined && !onWorkspaceChangeData) { - return - } - const latestActiveGraph = getActiveGraphData({ - workspace: onWorkspaceChangeData.onWorkspaceChange - }) - if (!deepEquals(activeGraph, latestActiveGraph)) { - setActiveGraph(latestActiveGraph) - } - }, - [onWorkspaceChangeData] - ) - return activeGraph -} - -export function usePrefersColorSchemeMonitor (onWorkspaceChangeData) { - const [prefersColorScheme, setPrefersColorScheme] = useState(undefined) - useEffect( - () => { - // Wait until initial data comes back - if (prefersColorScheme === undefined && !onWorkspaceChangeData) { - return - } - const latestPrefersColorScheme = getPrefersColorScheme( - onWorkspaceChangeData - ) - if (prefersColorScheme !== latestPrefersColorScheme) { - setPrefersColorScheme(latestPrefersColorScheme) - } - }, - [onWorkspaceChangeData] - ) - return prefersColorScheme -} diff --git a/src/browser/components/relate-api/relate-api.jsx b/src/browser/components/relate-api/relate-api.jsx deleted file mode 100644 index a8317278d3b..00000000000 --- a/src/browser/components/relate-api/relate-api.jsx +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Copyright (c) 2002-2019 "Neo4j," - * Neo4j Sweden AB [http://neo4j.com] - * - * This file is part of Neo4j. - * - * Neo4j is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program 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 General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -import { useEffect } from 'react' -import { withBus } from 'react-suber' -import { - useWorkspaceData, - useWorkspaceDataOnChange, - useActiveGraphMonitor, - usePrefersColorSchemeMonitor -} from './relate-api.hooks' -import { - getActiveGraphData, - getPrefersColorScheme, - detectDesktopThemeChanges, - switchConnection, - setInitialConnectionData -} from './relate-api.utils' -import { SILENT_DISCONNECT } from 'shared/modules/connections/connectionsDuck' - -function RelateApi ({ setEnvironmentTheme, defaultConnectionData, bus = {} }) { - // Until supported in relate-api - const getKerberosTicket = - (window.neo4jDesktopApi || {}).getKerberosTicket || function () {} - - // Initial data from Relate-API - const workspaceData = useWorkspaceData() - useEffect( - () => { - if (!workspaceData) { - return - } - const activeGraph = getActiveGraphData(workspaceData) - setInitialConnectionData( - activeGraph, - getKerberosTicket, - defaultConnectionData, - bus - ) - detectDesktopThemeChanges( - setEnvironmentTheme, - getPrefersColorScheme(workspaceData) - ) - }, - [workspaceData] - ) - - // Subscriptions from Relate-API - const onWorkspaceChangeData = useWorkspaceDataOnChange() - - const activeGraphMonitorData = useActiveGraphMonitor(onWorkspaceChangeData) - useEffect( - () => { - if (activeGraphMonitorData === undefined) { - // Not loaded yet - return - } - if (activeGraphMonitorData === null) { - bus.send(SILENT_DISCONNECT, {}) - return - } - switchConnection( - activeGraphMonitorData, - defaultConnectionData, - getKerberosTicket, - bus - ) - }, - [activeGraphMonitorData] - ) - - const prefersColorSchemeMonitorData = usePrefersColorSchemeMonitor( - onWorkspaceChangeData - ) - useEffect( - () => { - detectDesktopThemeChanges( - setEnvironmentTheme, - prefersColorSchemeMonitorData - ) - }, - [prefersColorSchemeMonitorData] - ) - - return null -} - -export default withBus(RelateApi) diff --git a/src/browser/components/relate-api/relate-api.utils.js b/src/browser/components/relate-api/relate-api.utils.js deleted file mode 100644 index 0eeb61846fc..00000000000 --- a/src/browser/components/relate-api/relate-api.utils.js +++ /dev/null @@ -1,236 +0,0 @@ -/* - * Copyright (c) 2002-2019 "Neo4j," - * Neo4j Sweden AB [http://neo4j.com] - * - * This file is part of Neo4j. - * - * Neo4j is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program 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 General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - */ -import { split } from 'apollo-link' -import { createHttpLink } from 'apollo-link-http' -import { WebSocketLink } from 'apollo-link-ws' -import { setContext } from 'apollo-link-context' -import ApolloClient from 'apollo-client' -import { InMemoryCache } from 'apollo-cache-inmemory' -import { getMainDefinition } from 'apollo-utilities' -import { onError } from 'apollo-link-error' -import { NATIVE, KERBEROS } from 'services/bolt/boltHelpers' -import { - SWITCH_CONNECTION, - SWITCH_CONNECTION_FAILED -} from 'shared/modules/connections/connectionsDuck' -import { INJECTED_DISCOVERY } from 'shared/modules/discovery/discoveryDuck' - -export const createClient = (apiEndpoint, apiClientId = null) => { - const apiEndpointUrl = new URL(apiEndpoint) - const apiEndpointNoScheme = `${apiEndpointUrl.host}${ - apiEndpointUrl.pathname ? apiEndpointUrl.pathname : '' - }` - - const httpLink = createHttpLink({ - uri: apiEndpoint - }) - - const wsLink = new WebSocketLink({ - uri: `ws://${apiEndpointNoScheme}`, - options: { - reconnect: true, - connectionParams: { - ClientId: apiClientId - } - } - }) - - const authLink = setContext((_, { headers }) => { - return { - headers: { - ...headers, - ClientId: apiClientId - } - } - }) - - const link = split( - // split based on operation type - ({ query }) => { - const { kind, operation } = getMainDefinition(query) - return kind === 'OperationDefinition' && operation === 'subscription' - }, - wsLink, - authLink.concat(httpLink) - ) - - const errorLink = onError(({ graphQLErrors, networkError }) => { - if (graphQLErrors) { - graphQLErrors.forEach(({ message, locations, path }) => - console.log( - `Relate API GraphQL error: Message: ${message}, Location: ${locations}, Path: ${path}` - ) - ) - } - - if (networkError) { - console.log(`Relate API Network error: ${networkError}`) - } - }) - - const client = new ApolloClient({ - link: errorLink.concat(link), - cache: new InMemoryCache() - }) - - return client -} - -export function getPrefersColorScheme (workspaceData) { - return ( - ((workspaceData.workspace || workspaceData.onWorkspaceChange).host || {}) - .prefersColorScheme || null - ) -} - -const graphStatus = { - ACTIVE: 'ACTIVE' -} -export function getActiveGraphData (workspaceData) { - return ( - (workspaceData && - workspaceData.workspace && - workspaceData.workspace.projects) || - [] - ).reduce((activeGraph, project) => { - if (!project.graphs) { - return activeGraph - } - const active = project.graphs.filter( - graph => graph.status === graphStatus.ACTIVE - ) - if (!active || !active.length) { - return activeGraph - } - return active[0] - }, null) -} - -export const getCredentialsForGraph = (protocol, graph) => { - if (!graph || !graph.connection) { - return null - } - const { principals = null } = graph.connection - if (!principals) { - return null - } - if (!principals.protocols) { - return null - } - if (typeof principals.protocols[protocol] === 'undefined') { - return null - } - return principals.protocols[protocol] -} - -export async function createConnectionCredentialsObject ( - activeGraph, - existingData, - getKerberosTicket = () => {} -) { - const creds = getCredentialsForGraph('bolt', activeGraph) - if (!creds) return // No connection. Ignore and let browser show connection lost msgs. - const httpsCreds = getCredentialsForGraph('https', activeGraph) - const httpCreds = getCredentialsForGraph('http', activeGraph) - const kerberos = isKerberosEnabled(activeGraph) - if (kerberos !== false) { - creds.password = await getKerberosTicket(kerberos.servicePrincipal) - } - const restApi = - httpsCreds && httpsCreds.enabled - ? `https://${httpsCreds.host}:${httpsCreds.port}` - : `http://${httpCreds.host}:${httpCreds.port}` - const connectionCreds = { - // Use current connections creds until we get new from API - ...existingData, - ...creds, - encrypted: creds.tlsLevel === 'REQUIRED', - host: creds.url || `bolt://${creds.host}:${creds.port}`, - restApi, - authenticationMethod: kerberos ? KERBEROS : NATIVE - } - return connectionCreds -} - -function isKerberosEnabled (activeGraph) { - if (!activeGraph || typeof activeGraph.connection === 'undefined') { - return false - } - if (!activeGraph.connection) return null - const { principals = null } = activeGraph.connection - if (!principals) { - return false - } - if ( - !principals.authenticationMethods || - !principals.authenticationMethods.kerberos - ) { - return false - } - if (!principals.authenticationMethods.kerberos.enabled) { - return false - } - return principals.authenticationMethods.kerberos -} - -export function detectDesktopThemeChanges ( - setEnvironmentTheme, - prefersColorScheme -) { - if (prefersColorScheme) { - setEnvironmentTheme(prefersColorScheme) - } else { - setEnvironmentTheme(null) - } -} - -export async function switchConnection ( - activeGraph, - defaultConnectionData, - getKerberosTicket, - bus -) { - const connectionCreds = await createConnectionCredentialsObject( - activeGraph, - defaultConnectionData, - getKerberosTicket - ) - bus.send(SWITCH_CONNECTION, connectionCreds) -} -export async function setInitialConnectionData ( - activeGraph, - defaultConnectionData, - getKerberosTicket, - bus -) { - const connectionsCredentials = await createConnectionCredentialsObject( - activeGraph, - defaultConnectionData, - getKerberosTicket - ) - - // No connection. Probably no graph active. - if (!connectionsCredentials) { - bus.send(SWITCH_CONNECTION_FAILED) - return - } - bus.send(INJECTED_DISCOVERY, connectionsCredentials) -} diff --git a/src/browser/components/relate-api/workspace.graphql b/src/browser/components/relate-api/workspace.graphql deleted file mode 100644 index 5f6a99013eb..00000000000 --- a/src/browser/components/relate-api/workspace.graphql +++ /dev/null @@ -1,76 +0,0 @@ -query { - workspace { - me { - activationKeys { - featureName - expirationDate - } - name - } - host { - prefersColorScheme - publicInternetAccess - settings { - allowSendReports - allowSendStats - allowStoreCredentials - } - } - projects { - id - name - graphs { - id - name - status - connection { - type - databaseType - databaseStatus - info { - edition - version - } - principals { - path - protocols { - bolt { - enabled - host - password - port - tlsLevel - url - username - } - http { - enabled - host - port - url - } - https { - enabled - host - port - url - } - } - authenticationMethods { - servicePrincipal - kerberos { - enabled - } - } - } - } - } - apps { - id - name - publisher - version - } - } - } -} diff --git a/src/browser/modules/App/App.jsx b/src/browser/modules/App/App.jsx index deab7918c91..ce210801c81 100644 --- a/src/browser/modules/App/App.jsx +++ b/src/browser/modules/App/App.jsx @@ -39,10 +39,16 @@ import { getLastConnectionUpdate, getActiveConnectionData, isConnected, - getConnectionData + getConnectionData, + SILENT_DISCONNECT, + SWITCH_CONNECTION, + SWITCH_CONNECTION_FAILED } from 'shared/modules/connections/connectionsDuck' import { toggle } from 'shared/modules/sidebar/sidebarDuck' -import { CONNECTION_ID } from 'shared/modules/discovery/discoveryDuck' +import { + CONNECTION_ID, + INJECTED_DISCOVERY +} from 'shared/modules/discovery/discoveryDuck' import { StyledWrapper, StyledApp, @@ -57,21 +63,24 @@ import asTitleString from '../DocTitle/titleStringBuilder' import Intercom from '../Intercom' import Render from 'browser-components/Render' import BrowserSyncInit from '../Sync/BrowserSyncInit' +import DesktopIntegration from 'browser-components/DesktopIntegration' +import { + getActiveGraph, + buildConnectionCredentialsObject +} from 'browser-components/DesktopIntegration/helpers' import { getMetadata, getUserAuthStatus } from 'shared/modules/sync/syncDuck' import ErrorBoundary from 'browser-components/ErrorBoundary' import { getExperimentalFeatures } from 'shared/modules/experimentalFeatures/experimentalFeaturesDuck' import FeatureToggleProvider from '../FeatureToggle/FeatureToggleProvider' -import { inWebEnv } from 'shared/modules/app/appDuck' +import { inWebEnv, URL_ARGUMENTS_CHANGE } from 'shared/modules/app/appDuck' import useDerivedTheme from 'browser-hooks/useDerivedTheme' import FileDrop from 'browser-components/FileDrop/FileDrop' -import RelateApi from 'browser-components/relate-api/relate-api' export function App (props) { const [derivedTheme, setEnvironmentTheme] = useDerivedTheme( props.theme, LIGHT_THEME ) - const themeData = themes[derivedTheme] || themes[LIGHT_THEME] useEffect(() => { document.addEventListener('keyup', focusEditorOnSlash) @@ -83,6 +92,15 @@ export function App (props) { } }, []) + const detectDesktopThemeChanges = (_, newContext) => { + if (newContext.global.prefersColorScheme) { + setEnvironmentTheme(newContext.global.prefersColorScheme) + } else { + setEnvironmentTheme(null) + } + } + const themeData = themes[derivedTheme] || themes[LIGHT_THEME] + const focusEditorOnSlash = e => { if (['INPUT', 'TEXTAREA'].indexOf(e.target.tagName) > -1) return if (e.key !== '/') return @@ -92,7 +110,6 @@ export function App (props) { if (e.keyCode !== 27) return props.bus && props.bus.send(EXPAND) } - const { drawer, cmdchar, @@ -109,8 +126,7 @@ export function App (props) { browserSyncAuthStatus, experimentalFeatures, store, - codeFontLigatures, - defaultConnectionData + codeFontLigatures } = props const wrapperClassNames = [] @@ -120,16 +136,33 @@ export function App (props) { return ( - + { + props.setInitialConnectionData( + activeGraph, + connectionsCredentials, + context, + getKerberosTicket + ) + detectDesktopThemeChanges(null, context) + }} + onGraphActive={props.switchConnection} + onGraphInactive={props.closeConnectionMaybe} + onColorSchemeUpdated={detectDesktopThemeChanges} + /> @@ -198,9 +231,61 @@ const mapDispatchToProps = dispatch => { } } +const mergeProps = (stateProps, dispatchProps, ownProps) => { + const switchConnection = async ( + event, + newContext, + oldContext, + getKerberosTicket + ) => { + const connectionCreds = await buildConnectionCredentialsObject( + newContext, + stateProps.defaultConnectionData, + getKerberosTicket + ) + ownProps.bus.send(SWITCH_CONNECTION, connectionCreds) + } + const setInitialConnectionData = async ( + graph, + credentials, + context, + getKerberosTicket + ) => { + const connectionCreds = await buildConnectionCredentialsObject( + context, + stateProps.defaultConnectionData, + getKerberosTicket + ) + // No connection. Probably no graph active. + if (!connectionCreds) { + ownProps.bus.send(SWITCH_CONNECTION_FAILED) + return + } + ownProps.bus.send(INJECTED_DISCOVERY, connectionCreds) + } + const closeConnectionMaybe = (event, newContext, oldContext) => { + const activeGraph = getActiveGraph(newContext) + if (activeGraph) return // We still got an active graph, do nothing + ownProps.bus.send(SILENT_DISCONNECT, {}) + } + const onArgumentsChange = argsString => { + ownProps.bus.send(URL_ARGUMENTS_CHANGE, { url: `?${argsString}` }) + } + return { + ...stateProps, + ...ownProps, + ...dispatchProps, + switchConnection, + setInitialConnectionData, + closeConnectionMaybe, + onArgumentsChange + } +} + export default withBus( connect( mapStateToProps, - mapDispatchToProps + mapDispatchToProps, + mergeProps )(App) ) diff --git a/src/browser/modules/App/App.test.js b/src/browser/modules/App/App.test.js index 3c56b3e2a30..62b347847f8 100644 --- a/src/browser/modules/App/App.test.js +++ b/src/browser/modules/App/App.test.js @@ -23,6 +23,8 @@ import React from 'react' import { render } from '@testing-library/react' import configureMockStore from 'redux-mock-store' import { App } from './App' +import { buildConnectionCredentialsObject } from 'browser-components/DesktopIntegration/helpers' +import { flushPromises } from 'services/utils' const mockStore = configureMockStore() const store = mockStore({}) @@ -34,21 +36,89 @@ jest.mock('./styled', () => { const orig = require.requireActual('./styled') return { ...orig, - StyledApp: () =>
Loaded
+ StyledApp: () => null } }) describe('App', () => { test('App loads', async () => { // Given + const getKerberosTicket = jest.fn(() => Promise.resolve('xxx')) + const desktopIntegrationPoint = getIntegrationPoint(true, getKerberosTicket) + let connectionCreds = null const props = { - store + store, + desktopIntegrationPoint, + setInitialConnectionData: async ( + graph, + credentials, + context, + getKerberosTicket + ) => { + connectionCreds = await buildConnectionCredentialsObject( + context, + {}, + getKerberosTicket + ) + } } // When - const { getByText } = render() + render() // Then - expect(getByText('Loaded')) + await flushPromises() + expect(connectionCreds).toMatchObject({ + authenticationMethod: 'KERBEROS', + password: 'xxx' + }) + expect(getKerberosTicket).toHaveBeenCalledTimes(1) }) }) + +const getIntegrationPoint = (kerberosEnabled, getKerberosTicket) => { + const context = Promise.resolve(getDesktopContext(kerberosEnabled)) + return { + getKerberosTicket: getKerberosTicket, + getContext: () => context + } +} + +const getDesktopContext = (kerberosEnabled = false) => ({ + projects: [ + { + graphs: [ + { + status: 'ACTIVE', + connection: { + type: 'REMOTE', + configuration: { + authenticationMethods: { + kerberos: { + enabled: kerberosEnabled, + servicePrincipal: 'KERBEROS' + } + }, + protocols: { + bolt: { + enabled: true, + username: 'neo4j', + password: 'password', + tlsLevel: 'REQUIRED', + url: `bolt://localhost:7687` + }, + http: { + enabled: true, + username: 'neo4j', + password: 'password', + host: 'localhost', + port: '7474' + } + } + } + } + } + ] + } + ] +}) diff --git a/test_utils/__mocks__/graphqlMock.js b/test_utils/__mocks__/graphqlMock.js deleted file mode 100644 index 2dcc429cb72..00000000000 --- a/test_utils/__mocks__/graphqlMock.js +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright (c) 2002-2019 "Neo4j," - * Neo4j Sweden AB [http://neo4j.com] - * - * This file is part of Neo4j. - * - * Neo4j is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program 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 General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -module.exports = {} diff --git a/yarn.lock b/yarn.lock index 0f8d9919048..9c805adc740 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,24 +2,6 @@ # yarn lockfile v1 -"@apollo/react-common@^3.1.1": - version "3.1.1" - resolved "https://neo.jfrog.io/neo/api/npm/npm/@apollo/react-common/-/react-common-3.1.1.tgz#540619d8276d750ce4f381c9cf8f51a6f657f75d" - integrity sha1-VAYZ2CdtdQzk84HJz49RpvZX910= - dependencies: - ts-invariant "^0.4.4" - tslib "^1.10.0" - -"@apollo/react-hooks@^3.0.1": - version "3.1.1" - resolved "https://neo.jfrog.io/neo/api/npm/npm/@apollo/react-hooks/-/react-hooks-3.1.1.tgz#8e5a0f281a9778aaf17496ad38a178d23375077c" - integrity sha1-jloPKBqXeKrxdJatOKF40jN1B3w= - dependencies: - "@apollo/react-common" "^3.1.1" - "@wry/equality" "^0.1.9" - ts-invariant "^0.4.4" - tslib "^1.10.0" - "@babel/code-frame@^7.0.0", "@babel/code-frame@^7.5.5": version "7.5.5" resolved "https://neo.jfrog.io/neo/api/npm/npm/@babel/code-frame/-/code-frame-7.5.5.tgz#bc0782f6d69f7b7d49531219699b988f669a8f9d" @@ -1420,7 +1402,7 @@ resolved "https://neo.jfrog.io/neo/api/npm/npm/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d" integrity sha1-PcoOPzOyAPx9ETnAzZbBJoyt/Z0= -"@types/node@*", "@types/node@>=6": +"@types/node@*": version "12.7.5" resolved "https://neo.jfrog.io/neo/api/npm/npm/@types/node/-/node-12.7.5.tgz#e19436e7f8e9b4601005d73673b6dc4784ffcc2f" integrity sha1-4ZQ25/jptGAQBdc2c7bcR4T/zC8= @@ -1489,11 +1471,6 @@ dependencies: "@types/yargs-parser" "*" -"@types/zen-observable@^0.8.0": - version "0.8.0" - resolved "https://neo.jfrog.io/neo/api/npm/npm/@types/zen-observable/-/zen-observable-0.8.0.tgz#8b63ab7f1aa5321248aad5ac890a485656dcea4d" - integrity sha1-i2OrfxqlMhJIqtWsiQpIVlbc6k0= - "@webassemblyjs/ast@1.8.5": version "1.8.5" resolved "https://neo.jfrog.io/neo/api/npm/npm/@webassemblyjs/ast/-/ast-1.8.5.tgz#51b1c5fe6576a34953bf4b253df9f0d490d9e359" @@ -1640,21 +1617,6 @@ "@webassemblyjs/wast-parser" "1.8.5" "@xtuc/long" "4.2.2" -"@wry/context@^0.4.0": - version "0.4.4" - resolved "https://neo.jfrog.io/neo/api/npm/npm/@wry/context/-/context-0.4.4.tgz#e50f5fa1d6cfaabf2977d1fda5ae91717f8815f8" - integrity sha1-5Q9fodbPqr8pd9H9pa6RcX+IFfg= - dependencies: - "@types/node" ">=6" - tslib "^1.9.3" - -"@wry/equality@^0.1.2", "@wry/equality@^0.1.9": - version "0.1.9" - resolved "https://neo.jfrog.io/neo/api/npm/npm/@wry/equality/-/equality-0.1.9.tgz#b13e18b7a8053c6858aa6c85b54911fb31e3a909" - integrity sha1-sT4Yt6gFPGhYqmyFtUkR+zHjqQk= - dependencies: - tslib "^1.9.3" - "@xtuc/ieee754@^1.2.0": version "1.2.0" resolved "https://neo.jfrog.io/neo/api/npm/npm/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790" @@ -1813,117 +1775,6 @@ anymatch@^2.0.0: micromatch "^3.1.4" normalize-path "^2.1.1" -apollo-boost@^0.4.4: - version "0.4.4" - resolved "https://neo.jfrog.io/neo/api/npm/npm/apollo-boost/-/apollo-boost-0.4.4.tgz#7c278dac6cb6fa3f2f710c56baddc6e3ae730651" - integrity sha1-fCeNrGy2+j8vcQxWut3G465zBlE= - dependencies: - apollo-cache "^1.3.2" - apollo-cache-inmemory "^1.6.3" - apollo-client "^2.6.4" - apollo-link "^1.0.6" - apollo-link-error "^1.0.3" - apollo-link-http "^1.3.1" - graphql-tag "^2.4.2" - ts-invariant "^0.4.0" - tslib "^1.9.3" - -apollo-cache-inmemory@^1.6.3: - version "1.6.3" - resolved "https://neo.jfrog.io/neo/api/npm/npm/apollo-cache-inmemory/-/apollo-cache-inmemory-1.6.3.tgz#826861d20baca4abc45f7ca7a874105905b8525d" - integrity sha1-gmhh0guspKvEX3ynqHQQWQW4Ul0= - dependencies: - apollo-cache "^1.3.2" - apollo-utilities "^1.3.2" - optimism "^0.10.0" - ts-invariant "^0.4.0" - tslib "^1.9.3" - -apollo-cache@1.3.2, apollo-cache@^1.3.2: - version "1.3.2" - resolved "https://neo.jfrog.io/neo/api/npm/npm/apollo-cache/-/apollo-cache-1.3.2.tgz#df4dce56240d6c95c613510d7e409f7214e6d26a" - integrity sha1-303OViQNbJXGE1ENfkCfchTm0mo= - dependencies: - apollo-utilities "^1.3.2" - tslib "^1.9.3" - -apollo-client@^2.6.4: - version "2.6.4" - resolved "https://neo.jfrog.io/neo/api/npm/npm/apollo-client/-/apollo-client-2.6.4.tgz#872c32927263a0d34655c5ef8a8949fbb20b6140" - integrity sha1-hywyknJjoNNGVcXviolJ+7ILYUA= - dependencies: - "@types/zen-observable" "^0.8.0" - apollo-cache "1.3.2" - apollo-link "^1.0.0" - apollo-utilities "1.3.2" - symbol-observable "^1.0.2" - ts-invariant "^0.4.0" - tslib "^1.9.3" - zen-observable "^0.8.0" - -apollo-link-context@^1.0.18: - version "1.0.19" - resolved "https://neo.jfrog.io/neo/api/npm/npm/apollo-link-context/-/apollo-link-context-1.0.19.tgz#3c9ba5bf75ed5428567ce057b8837ef874a58987" - integrity sha1-PJulv3XtVChWfOBXuIN++HSliYc= - dependencies: - apollo-link "^1.2.13" - tslib "^1.9.3" - -apollo-link-error@^1.0.3, apollo-link-error@^1.1.11: - version "1.1.12" - resolved "https://neo.jfrog.io/neo/api/npm/npm/apollo-link-error/-/apollo-link-error-1.1.12.tgz#e24487bb3c30af0654047611cda87038afbacbf9" - integrity sha1-4kSHuzwwrwZUBHYRzahwOK+6y/k= - dependencies: - apollo-link "^1.2.13" - apollo-link-http-common "^0.2.15" - tslib "^1.9.3" - -apollo-link-http-common@^0.2.15: - version "0.2.15" - resolved "https://neo.jfrog.io/neo/api/npm/npm/apollo-link-http-common/-/apollo-link-http-common-0.2.15.tgz#304e67705122bf69a9abaded4351b10bc5efd6d9" - integrity sha1-ME5ncFEiv2mpq63tQ1GxC8Xv1tk= - dependencies: - apollo-link "^1.2.13" - ts-invariant "^0.4.0" - tslib "^1.9.3" - -apollo-link-http@^1.3.1, apollo-link-http@^1.5.15: - version "1.5.16" - resolved "https://neo.jfrog.io/neo/api/npm/npm/apollo-link-http/-/apollo-link-http-1.5.16.tgz#44fe760bcc2803b8a7f57fc9269173afb00f3814" - integrity sha1-RP52C8woA7in9X/JJpFzr7APOBQ= - dependencies: - apollo-link "^1.2.13" - apollo-link-http-common "^0.2.15" - tslib "^1.9.3" - -apollo-link-ws@^1.0.18: - version "1.0.19" - resolved "https://neo.jfrog.io/neo/api/npm/npm/apollo-link-ws/-/apollo-link-ws-1.0.19.tgz#dfa871d4df883a8777c9556c872fc892e103daa5" - integrity sha1-36hx1N+IOod3yVVshy/IkuED2qU= - dependencies: - apollo-link "^1.2.13" - tslib "^1.9.3" - -apollo-link@^1.0.0, apollo-link@^1.0.6, apollo-link@^1.2.12, apollo-link@^1.2.13: - version "1.2.13" - resolved "https://neo.jfrog.io/neo/api/npm/npm/apollo-link/-/apollo-link-1.2.13.tgz#dff00fbf19dfcd90fddbc14b6a3f9a771acac6c4" - integrity sha1-3/APvxnfzZD928FLaj+adxrKxsQ= - dependencies: - apollo-utilities "^1.3.0" - ts-invariant "^0.4.0" - tslib "^1.9.3" - zen-observable-ts "^0.8.20" - -apollo-utilities@1.3.2, apollo-utilities@^1.3.0, apollo-utilities@^1.3.2: - version "1.3.2" - resolved "https://neo.jfrog.io/neo/api/npm/npm/apollo-utilities/-/apollo-utilities-1.3.2.tgz#8cbdcf8b012f664cd6cb5767f6130f5aed9115c9" - integrity sha1-jL3PiwEvZkzWy1dn9hMPWu2RFck= - dependencies: - "@wry/equality" "^0.1.2" - fast-json-stable-stringify "^2.0.0" - ts-invariant "^0.4.0" - tslib "^1.9.3" - app-root-path@^2.0.0: version "2.2.1" resolved "https://neo.jfrog.io/neo/api/npm/npm/app-root-path/-/app-root-path-2.2.1.tgz#d0df4a682ee408273583d43f6f79e9892624bc9a" @@ -2409,11 +2260,6 @@ babylon@^6.17.0, babylon@^6.18.0: resolved "https://neo.jfrog.io/neo/api/npm/npm/babylon/-/babylon-6.18.0.tgz#af2f3b88fa6f5c1e4c634d1a0f8eac4f55b395e3" integrity sha1-ry87iPpvXB5MY00aD46sT1WzleM= -backo2@^1.0.2: - version "1.0.2" - resolved "https://neo.jfrog.io/neo/api/npm/npm/backo2/-/backo2-1.0.2.tgz#31ab1ac8b129363463e35b3ebb69f4dfcfba7947" - integrity sha1-MasayLEpNjRj41s+u2n038+6eUc= - balanced-match@0.1.0: version "0.1.0" resolved "https://neo.jfrog.io/neo/api/npm/npm/balanced-match/-/balanced-match-0.1.0.tgz#b504bd05869b39259dd0c5efc35d843176dccc4a" @@ -4401,7 +4247,7 @@ etag@~1.8.1: resolved "https://neo.jfrog.io/neo/api/npm/npm/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= -eventemitter3@^3.0.0, eventemitter3@^3.1.0: +eventemitter3@^3.0.0: version "3.1.2" resolved "https://neo.jfrog.io/neo/api/npm/npm/eventemitter3/-/eventemitter3-3.1.2.tgz#2d3d48f9c346698fce83a85d7d664e98535df6e7" integrity sha1-LT1I+cNGaY/Og6hdfWZOmFNd9uc= @@ -5276,18 +5122,6 @@ graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6 resolved "https://neo.jfrog.io/neo/api/npm/npm/graceful-fs/-/graceful-fs-4.2.2.tgz#6f0952605d0140c1cfdb138ed005775b92d67b02" integrity sha1-bwlSYF0BQMHP2xOO0AV3W5LWewI= -graphql-tag@^2.10.1, graphql-tag@^2.4.2: - version "2.10.1" - resolved "https://neo.jfrog.io/neo/api/npm/npm/graphql-tag/-/graphql-tag-2.10.1.tgz#10aa41f1cd8fae5373eaf11f1f67260a3cad5e02" - integrity sha1-EKpB8c2PrlNz6vEfH2cmCjytXgI= - -graphql@^14.4.2: - version "14.5.6" - resolved "https://neo.jfrog.io/neo/api/npm/npm/graphql/-/graphql-14.5.6.tgz#3fa12173b50e6ccdef953c31c82f37c50ef58bec" - integrity sha1-P6Ehc7UObM3vlTwxyC83xQ71i+w= - dependencies: - iterall "^1.2.2" - growly@^1.3.0: version "1.3.0" resolved "https://neo.jfrog.io/neo/api/npm/npm/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081" @@ -6264,11 +6098,6 @@ istanbul-reports@^2.2.6: dependencies: handlebars "^4.1.2" -iterall@^1.2.1, iterall@^1.2.2: - version "1.2.2" - resolved "https://neo.jfrog.io/neo/api/npm/npm/iterall/-/iterall-1.2.2.tgz#92d70deb8028e0c39ff3164fdbf4d8b088130cd7" - integrity sha1-ktcN64Ao4MOf8xZP2/TYsIgTDNc= - jest-canvas-mock@^1.1.0: version "1.1.0" resolved "https://neo.jfrog.io/neo/api/npm/npm/jest-canvas-mock/-/jest-canvas-mock-1.1.0.tgz#f6ed0998b6be3ae2b7e095d7173441ae62cc831e" @@ -8106,13 +7935,6 @@ opn@^5.5.0: dependencies: is-wsl "^1.1.0" -optimism@^0.10.0: - version "0.10.3" - resolved "https://neo.jfrog.io/neo/api/npm/npm/optimism/-/optimism-0.10.3.tgz#163268fdc741dea2fb50f300bedda80356445fd7" - integrity sha1-FjJo/cdB3qL7UPMAvt2oA1ZEX9c= - dependencies: - "@wry/context" "^0.4.0" - optimist@^0.6.1: version "0.6.1" resolved "https://neo.jfrog.io/neo/api/npm/npm/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686" @@ -10769,17 +10591,6 @@ suber@^5.0.1: resolved "https://neo.jfrog.io/neo/api/npm/npm/suber/-/suber-5.0.1.tgz#78fc93bfc7444d11a23e160c5f062dc7967d6a58" integrity sha1-ePyTv8dETRGiPhYMXwYtx5Z9alg= -subscriptions-transport-ws@^0.9.16: - version "0.9.16" - resolved "https://neo.jfrog.io/neo/api/npm/npm/subscriptions-transport-ws/-/subscriptions-transport-ws-0.9.16.tgz#90a422f0771d9c32069294c08608af2d47f596ec" - integrity sha1-kKQi8HcdnDIGkpTAhgivLUf1luw= - dependencies: - backo2 "^1.0.2" - eventemitter3 "^3.1.0" - iterall "^1.2.1" - symbol-observable "^1.0.4" - ws "^5.2.0" - sugarss@^2.0.0: version "2.0.0" resolved "https://neo.jfrog.io/neo/api/npm/npm/sugarss/-/sugarss-2.0.0.tgz#ddd76e0124b297d40bf3cca31c8b22ecb43bc61d" @@ -10835,7 +10646,7 @@ symbol-observable@1.0.1: resolved "https://neo.jfrog.io/neo/api/npm/npm/symbol-observable/-/symbol-observable-1.0.1.tgz#8340fc4702c3122df5d22288f88283f513d3fdd4" integrity sha1-g0D8RwLDEi310iKI+IKD9RPT/dQ= -symbol-observable@^1.0.2, symbol-observable@^1.0.3, symbol-observable@^1.0.4, symbol-observable@^1.2.0: +symbol-observable@^1.0.3, symbol-observable@^1.2.0: version "1.2.0" resolved "https://neo.jfrog.io/neo/api/npm/npm/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804" integrity sha1-wiaIrtTqs83C3+rLtWFmBWCgCAQ= @@ -11072,19 +10883,12 @@ tryer@^1.0.1: resolved "https://neo.jfrog.io/neo/api/npm/npm/tryer/-/tryer-1.0.1.tgz#f2c85406800b9b0f74c9f7465b81eaad241252f8" integrity sha1-8shUBoALmw90yfdGW4HqrSQSUvg= -ts-invariant@^0.4.0, ts-invariant@^0.4.4: - version "0.4.4" - resolved "https://neo.jfrog.io/neo/api/npm/npm/ts-invariant/-/ts-invariant-0.4.4.tgz#97a523518688f93aafad01b0e80eb803eb2abd86" - integrity sha1-l6UjUYaI+TqvrQGw6A64A+sqvYY= - dependencies: - tslib "^1.9.3" - tslib@1.9.3: version "1.9.3" resolved "https://neo.jfrog.io/neo/api/npm/npm/tslib/-/tslib-1.9.3.tgz#d7e4dd79245d85428c4d7e4822a79917954ca286" integrity sha1-1+TdeSRdhUKMTX5IIqeZF5VMooY= -tslib@^1.10.0, tslib@^1.9.0, tslib@^1.9.3: +tslib@^1.9.0: version "1.10.0" resolved "https://neo.jfrog.io/neo/api/npm/npm/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a" integrity sha1-w8GflZc/sKYpc/sJ2Q2WHuQ+XIo= @@ -11916,16 +11720,3 @@ yauzl@2.4.1: integrity sha1-lSj0QtqxsihOWLQ3m7GU4i4MQAU= dependencies: fd-slicer "~1.0.1" - -zen-observable-ts@^0.8.20: - version "0.8.20" - resolved "https://neo.jfrog.io/neo/api/npm/npm/zen-observable-ts/-/zen-observable-ts-0.8.20.tgz#44091e335d3fcbc97f6497e63e7f57d5b516b163" - integrity sha1-RAkeM10/y8l/ZJfmPn9X1bUWsWM= - dependencies: - tslib "^1.9.3" - zen-observable "^0.8.0" - -zen-observable@^0.8.0: - version "0.8.14" - resolved "https://neo.jfrog.io/neo/api/npm/npm/zen-observable/-/zen-observable-0.8.14.tgz#d33058359d335bc0db1f0af66158b32872af3bf7" - integrity sha1-0zBYNZ0zW8DbHwr2YVizKHKvO/c=