diff --git a/client/src/plugins/camunda-plugin/deployment-tool/DeploymentConfigOverlay.js b/client/src/plugins/camunda-plugin/deployment-tool/DeploymentConfigOverlay.js index f274765c9e..c2aef0079f 100644 --- a/client/src/plugins/camunda-plugin/deployment-tool/DeploymentConfigOverlay.js +++ b/client/src/plugins/camunda-plugin/deployment-tool/DeploymentConfigOverlay.js @@ -30,6 +30,7 @@ import { Formik, Field } from 'formik'; +import { GenericApiErrors } from '../shared/RestAPI'; export default class DeploymentConfigOverlay extends React.PureComponent { @@ -153,7 +154,7 @@ export default class DeploymentConfigOverlay extends React.PureComponent { code } = connectionValidation; - if (code === 'UNAUTHORIZED') { + if (code === GenericApiErrors.UNAUTHORIZED) { this.setState({ isAuthNeeded: true }); @@ -213,7 +214,7 @@ export default class DeploymentConfigOverlay extends React.PureComponent { if (!result) { this.onAuthDetection(false); } else if (!result.isExpired) { - this.onAuthDetection(!!result && (result.code === 'UNAUTHORIZED')); + this.onAuthDetection(!!result && (result.code === GenericApiErrors.UNAUTHORIZED)); } }); }; diff --git a/client/src/plugins/camunda-plugin/deployment-tool/DeploymentTool.js b/client/src/plugins/camunda-plugin/deployment-tool/DeploymentTool.js index e2530a5fb8..ed1a5820f2 100644 --- a/client/src/plugins/camunda-plugin/deployment-tool/DeploymentTool.js +++ b/client/src/plugins/camunda-plugin/deployment-tool/DeploymentTool.js @@ -14,12 +14,14 @@ import { omit } from 'min-dash'; import classNames from 'classnames'; -import { default as CamundaAPI, ApiErrors, ConnectionError } from '../shared/CamundaAPI'; +import { default as CamundaAPI, DeploymentError } from '../shared/CamundaAPI'; +import { ConnectionError, GenericApiErrors } from '../shared/RestAPI'; import AUTH_TYPES from '../shared/AuthTypes'; +import CockpitDeploymentLink from '../shared/ui/CockpitDeploymentLink'; + import DeploymentConfigOverlay from './DeploymentConfigOverlay'; import DeploymentConfigValidator from './validation/DeploymentConfigValidator'; -import { DeploymentError } from '../shared/CamundaAPI'; import * as css from './DeploymentTool.less'; @@ -32,6 +34,7 @@ import { Fill } from '../../../app/slot-fill'; import DeployIcon from 'icons/Deploy.svg'; import { ENGINES } from '../../../util/Engines'; +import { determineCockpitUrl } from '../shared/webAppUrls'; const DEPLOYMENT_DETAILS_CONFIG_KEY = 'deployment-tool'; const ENGINE_ENDPOINTS_CONFIG_KEY = 'camundaEngineEndpoints'; @@ -175,7 +178,7 @@ export default class DeploymentTool extends PureComponent { } } - handleDeploymentSuccess(tab, deployment, version, configuration) { + async handleDeploymentSuccess(tab, deployment, version, configuration) { const { displayNotification, triggerAction @@ -189,10 +192,12 @@ export default class DeploymentTool extends PureComponent { url } = endpoint; + const cockpitUrl = await this.getCockpitUrl(url); + displayNotification({ type: 'success', title: `${getDeploymentType(tab)} deployed`, - content: , + content: , duration: 8000 }); @@ -211,6 +216,10 @@ export default class DeploymentTool extends PureComponent { }); } + async getCockpitUrl(engineUrl) { + return await determineCockpitUrl(engineUrl); + } + async saveProcessDefinition(tab, deployment) { if (!deployment || !deployment.deployedProcessDefinition) { @@ -533,9 +542,9 @@ export default class DeploymentTool extends PureComponent { const { code } = result; - return (code !== ApiErrors.NO_INTERNET_CONNECTION && - code !== ApiErrors.CONNECTION_FAILED && - code !== ApiErrors.NOT_FOUND); + return (code !== GenericApiErrors.NO_INTERNET_CONNECTION && + code !== GenericApiErrors.CONNECTION_FAILED && + code !== GenericApiErrors.NOT_FOUND); } closeOverlay(overlayState) { @@ -597,39 +606,6 @@ export default class DeploymentTool extends PureComponent { } -function CockpitLink(props) { - const { - endpointUrl, - deployment - } = props; - - const { - id, - deployedProcessDefinition - } = deployment; - - const baseUrl = getWebAppsBaseUrl(endpointUrl); - - const query = `deploymentsQuery=%5B%7B%22type%22:%22id%22,%22operator%22:%22eq%22,%22value%22:%22${id}%22%7D%5D`; - const cockpitUrl = `${baseUrl}/cockpit/default/#/repository/?${query}`; - - return ( -
- {deployedProcessDefinition ? - ( -
- Process definition ID: - {deployedProcessDefinition.id} -
- ) - : null} - - Open in Camunda Cockpit - -
- ); -} - // helpers ////////// function withoutExtension(name) { @@ -697,13 +673,3 @@ function withSerializedAttachments(deployment) { function basename(filePath) { return filePath.split('\\').pop().split('/').pop(); } - -function getWebAppsBaseUrl(url) { - const [ protocol,, host, restRoot ] = url.split('/'); - - return isTomcat(restRoot) ? `${protocol}//${host}/camunda/app` : `${protocol}//${host}/app`; -} - -function isTomcat(restRoot) { - return restRoot === 'engine-rest'; -} diff --git a/client/src/plugins/camunda-plugin/deployment-tool/DeploymentTool.less b/client/src/plugins/camunda-plugin/deployment-tool/DeploymentTool.less index ff86b27c02..091385d882 100644 --- a/client/src/plugins/camunda-plugin/deployment-tool/DeploymentTool.less +++ b/client/src/plugins/camunda-plugin/deployment-tool/DeploymentTool.less @@ -4,11 +4,4 @@ height: 24px; fill: var(--status-bar-icon-font-color); } -} - -:local(.CockpitLink) { - - & > div { - margin-bottom: 5px; - } } \ No newline at end of file diff --git a/client/src/plugins/camunda-plugin/deployment-tool/__tests__/DeploymentConfigOverlaySpec.js b/client/src/plugins/camunda-plugin/deployment-tool/__tests__/DeploymentConfigOverlaySpec.js index 5c7927d631..4cbc6c20ba 100644 --- a/client/src/plugins/camunda-plugin/deployment-tool/__tests__/DeploymentConfigOverlaySpec.js +++ b/client/src/plugins/camunda-plugin/deployment-tool/__tests__/DeploymentConfigOverlaySpec.js @@ -12,7 +12,7 @@ import React from 'react'; -import { act } from 'react-dom/test-utils'; +import { waitFor } from '@testing-library/react'; import { mount, @@ -24,6 +24,7 @@ import { merge } from 'min-dash'; import AUTH_TYPES from '../../shared/AuthTypes'; import DeploymentConfigOverlay from '../DeploymentConfigOverlay'; import DeploymentConfigValidator from '../validation/DeploymentConfigValidator'; +import { GenericApiErrors } from '../../shared/RestAPI'; let mounted; @@ -75,7 +76,7 @@ describe('', () => { }); - it('should display hint if the username and password are missing when submitting', (done) => { + it('should display hint if the username and password are missing when submitting', async () => { // given const configuration = { @@ -92,7 +93,7 @@ describe('', () => { const validator = new MockValidator({ validateConnection: () => new Promise((resolve, err) => { resolve({ - code: 'UNAUTHORIZED' + code: GenericApiErrors.UNAUTHORIZED }); }) }); @@ -106,7 +107,6 @@ describe('', () => { validator }, mount); - // when setTimeout(() => { // delayed execution because it is async that the deployment @@ -116,15 +116,14 @@ describe('', () => { }); // then - setTimeout(() => { + await waitFor(() => { wrapper.update(); expect(wrapper.find('.invalid-feedback')).to.have.length(2); - done(); - }, 200); + }); }); - it('should display hint if token is missing', (done) => { + it('should display hint if token is missing', async () => { // given const configuration = { @@ -138,43 +137,39 @@ describe('', () => { } }; + const validator = new MockValidator({ + validateConnection: () => Promise.resolve({ + code: GenericApiErrors.UNAUTHORIZED + }) + }); + const { wrapper, instance } = createOverlay({ anchor, - configuration + configuration, + validator }, mount); - // when - act(() => { - instance.setState({ isAuthNeeded: true }); - }); - instance.isOnBeforeSubmit = true; - - wrapper.update(); + setTimeout(() => { - act(() => { + // delayed execution because it is async that the deployment + // tool knows if the authentication is necessary + instance.isOnBeforeSubmit = true; wrapper.find('.btn-primary').simulate('submit'); }); // then - setTimeout(() => { + await waitFor(() => { wrapper.update(); - - try { - expect(wrapper.find('.invalid-feedback')).to.have.length(1); - } catch (err) { - return done(err); - } - - return done(); - }, 100); + expect(wrapper.find('.invalid-feedback')).to.have.length(1); + }); }); - it('should not display hint if the username and password are complete', (done) => { + it('should not display hint if the username and password are complete', async () => { // given const configuration = { @@ -208,15 +203,14 @@ describe('', () => { wrapper.find('.btn-primary').simulate('submit'); // then - setTimeout(() => { + await waitFor(() => { wrapper.update(); expect(wrapper.find('.invalid-feedback')).to.have.length(0); - done(); }); }); - it('should not disable deploy button when connection cannot be established', (done) => { + it('should not disable deploy button when connection cannot be established', async () => { // given const configuration = { @@ -250,15 +244,14 @@ describe('', () => { wrapper.find('.btn-primary').simulate('submit'); // then - setTimeout(() => { + await waitFor(() => { wrapper.setProps({}); expect(wrapper.find('.btn-primary').props()).to.have.property('disabled', false); - done(); }); }); - it('should hide username password fields if auth is not needed', (done) => { + it('should hide username password fields if auth is not needed', async () => { // given const configuration = { @@ -287,16 +280,15 @@ describe('', () => { }, mount); // then - setTimeout(() => { + await waitFor(() => { wrapper.update(); expect(wrapper.find('[id="endpoint.username"]')).to.have.length(0); expect(wrapper.find('[id="endpoint.password"]')).to.have.length(0); - done(); }); }); - it('should hide token field if auth is not needed', (done) => { + it('should hide token field if auth is not needed', async () => { // given const configuration = { @@ -325,16 +317,15 @@ describe('', () => { }, mount); // then - setTimeout(() => { + await waitFor(() => { wrapper.update(); expect(wrapper.find('[id="endpoint.token"]')).to.have.length(0); - done(); }); }); }); - it('should not disable deploy button when form is invalid', (done) => { + it('should not disable deploy button when form is invalid', async () => { // given const configuration = { @@ -359,10 +350,9 @@ describe('', () => { wrapper.find('.btn-primary').simulate('click'); // then - setTimeout(() => { + await waitFor(() => { wrapper.update(); expect(wrapper.find('.btn-primary').props()).to.have.property('disabled', false); - done(); }); }); @@ -400,7 +390,6 @@ describe('', () => { // then expect(saveCredentials).to.have.been.called; - }); diff --git a/client/src/plugins/camunda-plugin/deployment-tool/__tests__/DeploymentToolSpec.js b/client/src/plugins/camunda-plugin/deployment-tool/__tests__/DeploymentToolSpec.js index 43bb41ef1b..b0e78d22cc 100644 --- a/client/src/plugins/camunda-plugin/deployment-tool/__tests__/DeploymentToolSpec.js +++ b/client/src/plugins/camunda-plugin/deployment-tool/__tests__/DeploymentToolSpec.js @@ -21,8 +21,8 @@ import { Config } from './../../../../app/__tests__/mocks'; import DeploymentTool from '../DeploymentTool'; import AUTH_TYPES from '../../shared/AuthTypes'; -import { DeploymentError, - ConnectionError } from '../../shared/CamundaAPI'; +import { DeploymentError } from '../../shared/CamundaAPI'; +import { ConnectionError } from '../../shared/RestAPI'; import { Slot, SlotFillRoot } from '../../../../app/slot-fill'; const CONFIG_KEY = 'deployment-tool'; @@ -1299,13 +1299,16 @@ describe('', () => { const configuration = createConfiguration(); + const getCockpitUrlSpy = sinon.stub().returns('http://localhost:8080/app/cockpit/default/#/'); + const { instance } = createDeploymentTool({ activeTab, configuration, displayNotification, - deploySpy + deploySpy, + getCockpitUrlSpy, }); // when @@ -1315,7 +1318,7 @@ describe('', () => { // then try { - const cockpitLink = shallow(notification.content).find('a').first(); + const cockpitLink = mount(notification.content).find('a').first(); const { href } = cockpitLink.props(); expect(href).to.eql(expectedCockpitLink); @@ -1334,11 +1337,9 @@ describe('', () => { it('should display Spring-specific Cockpit link', testCockpitLink( - `http://localhost:8080/app/cockpit/default/#/repository/?${query('foo')}` + `http://localhost:8080/app/cockpit/default/#/repository?${query('foo')}` )); }); - - }); @@ -1365,6 +1366,11 @@ describe('', () => { return this.props.deploySpy && this.props.deploySpy(...args); } + // removes WellKnownAPI dependency + async getCockpitUrl(engineUrl) { + return this.props.getCockpitUrlSpy && await this.props.getCockpitUrlSpy(engineUrl); + } + getVersion(...args) { if (this.props.getVersionErrorThrown) { throw this.props.getVersionErrorThrown; diff --git a/client/src/plugins/camunda-plugin/deployment-tool/validation/DeploymentConfigValidator.js b/client/src/plugins/camunda-plugin/deployment-tool/validation/DeploymentConfigValidator.js index a4384df7fd..a33e349a0c 100644 --- a/client/src/plugins/camunda-plugin/deployment-tool/validation/DeploymentConfigValidator.js +++ b/client/src/plugins/camunda-plugin/deployment-tool/validation/DeploymentConfigValidator.js @@ -11,6 +11,7 @@ import AUTH_TYPES from '../../shared/AuthTypes'; import { default as CamundaAPI, ApiErrorMessages } from '../../shared/CamundaAPI'; +import { GenericApiErrors } from '../../shared/RestAPI'; import EndpointURLValidator from './EndpointURLValidator'; @@ -62,7 +63,7 @@ export default class DeploymentConfigValidator { }; onExternalError = (authType, details, code, setFieldError) => { - if (code === 'UNAUTHORIZED') { + if (code === GenericApiErrors.UNAUTHORIZED) { if (authType === AUTH_TYPES.BASIC) { this.usernameValidator.onExternalError(details, setFieldError); this.passwordValidator.onExternalError(details, setFieldError); diff --git a/client/src/plugins/camunda-plugin/deployment-tool/validation/EndpointURLValidator.js b/client/src/plugins/camunda-plugin/deployment-tool/validation/EndpointURLValidator.js index 7a7c032860..497ff25597 100644 --- a/client/src/plugins/camunda-plugin/deployment-tool/validation/EndpointURLValidator.js +++ b/client/src/plugins/camunda-plugin/deployment-tool/validation/EndpointURLValidator.js @@ -9,6 +9,7 @@ */ import BaseInputValidator from './BaseInputValidator'; +import { GenericApiErrors } from '../../shared/RestAPI'; export default class EndpointURLValidator extends BaseInputValidator { @@ -78,9 +79,9 @@ export default class EndpointURLValidator extends BaseInputValidator { } onConnectionStatusUpdate(code); - onAuthDetection(code === 'UNAUTHORIZED'); + onAuthDetection(code === GenericApiErrors.UNAUTHORIZED); - if (code !== 'UNAUTHORIZED') { + if (code !== GenericApiErrors.UNAUTHORIZED) { return this.setFieldError(details, setFieldError); } } else { diff --git a/client/src/plugins/camunda-plugin/shared/CamundaAPI.js b/client/src/plugins/camunda-plugin/shared/CamundaAPI.js index 359da3cac2..a66cfd34ef 100644 --- a/client/src/plugins/camunda-plugin/shared/CamundaAPI.js +++ b/client/src/plugins/camunda-plugin/shared/CamundaAPI.js @@ -10,20 +10,12 @@ import AUTH_TYPES from './AuthTypes'; -import debug from 'debug'; +import RestAPI, { ConnectionError, GenericApiErrorMessages, getNetworkErrorCode, getResponseErrorCode } from './RestAPI'; -const FETCH_TIMEOUT = 5000; - -const log = debug('CamundaAPI'); - - -export default class CamundaAPI { +export default class CamundaAPI extends RestAPI { constructor(endpoint) { - - this.baseUrl = normalizeBaseURL(endpoint.url); - - this.authentication = this.getAuthentication(endpoint); + super('CamundaAPI', normalizeBaseURL(endpoint.url), getAuthentication(endpoint)); } async deployDiagram(diagram, deployment) { @@ -130,152 +122,40 @@ export default class CamundaAPI { throw new ConnectionError(response); } +} - getAuthentication(endpoint) { - - const { - authType, - username, - password, - token - } = endpoint; - - switch (authType) { - case AUTH_TYPES.BASIC: - return { - username, - password - }; - case AUTH_TYPES.BEARER: - return { - token - }; - } - } - - getHeaders() { - const headers = { - accept: 'application/json' - }; - - if (this.authentication) { - headers.authorization = this.getAuthHeader(this.authentication); - } - - return headers; - } +function getAuthentication(endpoint) { - getAuthHeader(endpoint) { + const { + authType, + username, + password, + token + } = endpoint; - const { - token, + switch (authType) { + case AUTH_TYPES.BASIC: + return { username, password - } = endpoint; - - if (token) { - return `Bearer ${token}`; - } - - if (username && password) { - const credentials = window.btoa(`${username}:${password}`); - - return `Basic ${credentials}`; - } - } - - async fetch(path, options = {}) { - const url = `${this.baseUrl}${path}`; - const headers = { - ...options.headers, - ...this.getHeaders() }; - - try { - const signal = options.signal || this.setupTimeoutSignal(); - - return await fetch(url, { - ...options, - headers, - signal - }); - } catch (error) { - log('failed to fetch', error); - - return { - url, - json: () => { - return {}; - } - }; - } - } - - setupTimeoutSignal(timeout = FETCH_TIMEOUT) { - const controller = new AbortController(); - - setTimeout(() => controller.abort(), timeout); - - return controller.signal; - } - - async parse(response) { - try { - const json = await response.json(); - - return json; - } catch (error) { - return {}; - } + case AUTH_TYPES.BEARER: + return { + token + }; } } -const NO_INTERNET_CONNECTION = 'NO_INTERNET_CONNECTION'; -const CONNECTION_FAILED = 'CONNECTION_FAILED'; const DIAGRAM_PARSE_ERROR = 'DIAGRAM_PARSE_ERROR'; -const UNAUTHORIZED = 'UNAUTHORIZED'; -const FORBIDDEN = 'FORBIDDEN'; -const NOT_FOUND = 'NOT_FOUND'; -const INTERNAL_SERVER_ERROR = 'INTERNAL_SERVER_ERROR'; -const UNAVAILABLE_ERROR = 'UNAVAILABLE_ERROR'; export const ApiErrors = { - NO_INTERNET_CONNECTION, - CONNECTION_FAILED, DIAGRAM_PARSE_ERROR, - UNAUTHORIZED, - FORBIDDEN, - NOT_FOUND, - INTERNAL_SERVER_ERROR, - UNAVAILABLE_ERROR }; export const ApiErrorMessages = { - [ NO_INTERNET_CONNECTION ]: 'Could not establish a network connection.', - [ CONNECTION_FAILED ]: 'Should point to a running Camunda REST API.', [ DIAGRAM_PARSE_ERROR ]: 'Server could not parse the diagram. Please check log for errors.', - [ UNAUTHORIZED ]: 'Credentials do not match with the server.', - [ FORBIDDEN ]: 'This user is not permitted to deploy. Please use different credentials or get this user enabled to deploy.', - [ NOT_FOUND ]: 'Should point to a running Camunda REST API.', - [ INTERNAL_SERVER_ERROR ]: 'Camunda is reporting an error. Please check the server status.', - [ UNAVAILABLE_ERROR ]: 'Camunda is reporting an error. Please check the server status.' }; -export class ConnectionError extends Error { - - constructor(response) { - super('Connection failed'); - - this.code = ( - getResponseErrorCode(response) || - getNetworkErrorCode(response) - ); - - this.details = ApiErrorMessages[this.code]; - } -} - - export class DeploymentError extends Error { constructor(response, body) { @@ -287,7 +167,7 @@ export class DeploymentError extends Error { getNetworkErrorCode(response) ); - this.details = ApiErrorMessages[this.code]; + this.details = ApiErrorMessages[this.code] || GenericApiErrorMessages[this.code]; this.problems = body && body.message; } @@ -304,7 +184,7 @@ export class StartInstanceError extends Error { getNetworkErrorCode(response) ); - this.details = ApiErrorMessages[this.code]; + this.details = ApiErrorMessages[this.code] || GenericApiErrorMessages[this.code]; this.problems = body && body.message; } @@ -313,29 +193,6 @@ export class StartInstanceError extends Error { // helpers /////////////// -function getNetworkErrorCode(response) { - if (isLocalhost(response.url) || isOnline()) { - return CONNECTION_FAILED; - } - - return NO_INTERNET_CONNECTION; -} - -function getResponseErrorCode(response) { - switch (response.status) { - case 401: - return UNAUTHORIZED; - case 403: - return FORBIDDEN; - case 404: - return NOT_FOUND; - case 500: - return INTERNAL_SERVER_ERROR; - case 503: - return UNAVAILABLE_ERROR; - } -} - function getCamundaErrorCode(response, body) { const PARSE_ERROR_PREFIX = 'ENGINE-09005 Could not parse BPMN process.'; @@ -345,14 +202,6 @@ function getCamundaErrorCode(response, body) { } } -function isLocalhost(url) { - return /^https?:\/\/(127\.0\.0\.1|localhost)/.test(url); -} - -function isOnline() { - return window.navigator.onLine; -} - function normalizeBaseURL(url) { return url.replace(/\/deployment\/create\/?/, ''); } diff --git a/client/src/plugins/camunda-plugin/shared/RestAPI.js b/client/src/plugins/camunda-plugin/shared/RestAPI.js new file mode 100644 index 0000000000..2330952bd8 --- /dev/null +++ b/client/src/plugins/camunda-plugin/shared/RestAPI.js @@ -0,0 +1,170 @@ +/** + * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH + * under one or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information regarding copyright + * ownership. + * + * Camunda licenses this file to you under the MIT; you may not use this file + * except in compliance with the MIT License. + */ + +import debug from 'debug'; + +const DEFAULT_FETCH_TIMEOUT = 5000; + +export default class RestAPI { + + constructor(apiLoggerName, baseUrl, authentication) { + this.log = debug(apiLoggerName); + this.baseUrl = baseUrl; + this.authentication = authentication; + } + + async fetch(path, options = {}) { + const url = `${this.baseUrl}${path}`; + const headers = { + ...options.headers, + ...this.getHeaders() + }; + + try { + const signal = options.signal || this.setupTimeoutSignal(); + + return await fetch(url, { + ...options, + headers, + signal + }); + } catch (error) { + this.log('failed to fetch', error); + + return { + url, + json: () => { + return {}; + } + }; + } + } + + getHeaders() { + const headers = { + accept: 'application/json' + }; + + if (this.authentication) { + headers.authorization = this.getAuthHeader(this.authentication); + } + + return headers; + } + + getAuthHeader(endpoint) { + const { + token, + username, + password + } = endpoint; + + if (token) { + return `Bearer ${token}`; + } + + if (username && password) { + const credentials = window.btoa(`${username}:${password}`); + + return `Basic ${credentials}`; + } + } + + setupTimeoutSignal(timeout = DEFAULT_FETCH_TIMEOUT) { + const controller = new AbortController(); + + setTimeout(() => controller.abort(), timeout); + + return controller.signal; + } + + async parse(response) { + try { + const json = await response.json(); + + return json; + } catch (error) { + return {}; + } + } +} + +const NO_INTERNET_CONNECTION = 'NO_INTERNET_CONNECTION'; +const CONNECTION_FAILED = 'CONNECTION_FAILED'; +const UNAUTHORIZED = 'UNAUTHORIZED'; +const FORBIDDEN = 'FORBIDDEN'; +const NOT_FOUND = 'NOT_FOUND'; +const INTERNAL_SERVER_ERROR = 'INTERNAL_SERVER_ERROR'; +const UNAVAILABLE_ERROR = 'UNAVAILABLE_ERROR'; + +export const GenericApiErrors = { + NO_INTERNET_CONNECTION, + CONNECTION_FAILED, + UNAUTHORIZED, + FORBIDDEN, + NOT_FOUND, + INTERNAL_SERVER_ERROR, + UNAVAILABLE_ERROR +}; + +export const GenericApiErrorMessages = { + [ NO_INTERNET_CONNECTION ]: 'Could not establish a network connection.', + [ CONNECTION_FAILED ]: 'Should point to a running Camunda REST API.', + [ UNAUTHORIZED ]: 'Credentials do not match with the server.', + [ FORBIDDEN ]: 'This user is not permitted to deploy. Please use different credentials or get this user enabled to deploy.', + [ NOT_FOUND ]: 'Should point to a running Camunda REST API.', + [ INTERNAL_SERVER_ERROR ]: 'Camunda is reporting an error. Please check the server status.', + [ UNAVAILABLE_ERROR ]: 'Camunda is reporting an error. Please check the server status.' +}; + +export class ConnectionError extends Error { + + constructor(response) { + super('Connection failed'); + + this.code = ( + getResponseErrorCode(response) || + getNetworkErrorCode(response) + ); + + this.details = GenericApiErrorMessages[this.code]; + } +} + +export function getNetworkErrorCode(response) { + if (isLocalhost(response.url) || isOnline()) { + return CONNECTION_FAILED; + } + + return NO_INTERNET_CONNECTION; +} + +export function getResponseErrorCode(response) { + switch (response.status) { + case 401: + return UNAUTHORIZED; + case 403: + return FORBIDDEN; + case 404: + return NOT_FOUND; + case 500: + return INTERNAL_SERVER_ERROR; + case 503: + return UNAVAILABLE_ERROR; + } +} + +function isLocalhost(url) { + return /^https?:\/\/(127\.0\.0\.1|localhost)/.test(url); +} + +function isOnline() { + return window.navigator.onLine; +} diff --git a/client/src/plugins/camunda-plugin/shared/WellKnownAPI.js b/client/src/plugins/camunda-plugin/shared/WellKnownAPI.js new file mode 100644 index 0000000000..e41f4f5ed6 --- /dev/null +++ b/client/src/plugins/camunda-plugin/shared/WellKnownAPI.js @@ -0,0 +1,83 @@ +/** + * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH + * under one or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information regarding copyright + * ownership. + * + * Camunda licenses this file to you under the MIT; you may not use this file + * except in compliance with the MIT License. + */ + +import RestAPI, { ConnectionError } from './RestAPI'; + +const WELL_KNOWN_PATH_WEBAPPS = '/.well-known/camunda-automation-platform/webapps'; + +export function forEngineRestUrl(engineRestUrl) { + const engineUrl = new URL(engineRestUrl); + return new WellKnownAPI(`${engineUrl.protocol}//${engineUrl.host}`); +} + +export default class WellKnownAPI extends RestAPI { + + constructor(url) { + super('WellKnownAPI', url); + } + + async getWellKnownWebAppUrls() { + const response = await this.fetch(WELL_KNOWN_PATH_WEBAPPS); + + if (response.ok) { + const { + adminUrl, + cockpitUrl, + tasklistUrl, + } = await response.json(); + + return { + admin: this.normalizeWebAppUrl(adminUrl, 'admin'), + cockpit: this.normalizeWebAppUrl(cockpitUrl, 'cockpit'), + tasklist: this.normalizeWebAppUrl(tasklistUrl, 'tasklist'), + }; + } + + throw new ConnectionError(response); + } + + async getAdminUrl() { + const { + admin + } = await this.getWellKnownWebAppUrls(); + + return admin; + } + + async getCockpitUrl() { + const { + cockpit + } = await this.getWellKnownWebAppUrls(); + + return cockpit; + } + + async getTasklistUrl() { + const { + tasklist + } = await this.getWellKnownWebAppUrls(); + + return tasklist; + } + + normalizeWebAppUrl(webAppUrl, appName) { + if (!webAppUrl) { + return webAppUrl; + } + + return webAppUrl + + // ensure trailing slash + .replace(/\/?$/, '/') + + // in case we got the root path, we assume its the default process engine + .replace(new RegExp(`${appName}/$`), `${appName}/default/#/`); + } +} diff --git a/client/src/plugins/camunda-plugin/shared/__tests__/WellKnownAPISpec.js b/client/src/plugins/camunda-plugin/shared/__tests__/WellKnownAPISpec.js new file mode 100644 index 0000000000..90b97a3151 --- /dev/null +++ b/client/src/plugins/camunda-plugin/shared/__tests__/WellKnownAPISpec.js @@ -0,0 +1,403 @@ +/** + * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH + * under one or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information regarding copyright + * ownership. + * + * Camunda licenses this file to you under the MIT; you may not use this file + * except in compliance with the MIT License. + */ + +/* global sinon */ + +import { ConnectionError } from '../RestAPI'; +import WellKnownAPI from '../WellKnownAPI'; + + +describe('', () => { + + /** + * @type {sinon.SinonStub} + */ + let fetchSpy; + + beforeEach(() => { + fetchSpy = sinon.stub(window, 'fetch'); + }); + + afterEach(() => { + fetchSpy.restore(); + }); + + describe('#getWellKnownWebAppUrls', () => { + + it('should retrieve well known urls', async () => { + + // given + const api = createWellKnownAPI(); + + fetchSpy.resolves(new Response({ + json: () => ({ + 'adminUrl': 'http://localhost:18080/camunda/apps/admin/default/#/', + 'tasklistUrl': 'http://localhost:18080/camunda/apps/tasklist/default/#/', + 'cockpitUrl': 'http://localhost/camunda/apps/cockpit/default/#/' + }), + })); + + // when + const wellKnownUrls = await api.getWellKnownWebAppUrls(); + + // then + expectFetched(fetchSpy); + expect(wellKnownUrls).to.have.property('admin', 'http://localhost:18080/camunda/apps/admin/default/#/'); + expect(wellKnownUrls).to.have.property('tasklist', 'http://localhost:18080/camunda/apps/tasklist/default/#/'); + expect(wellKnownUrls).to.have.property('cockpit', 'http://localhost/camunda/apps/cockpit/default/#/'); + }); + + + it('should throw when fetch fails', async () => { + + // given + const api = createWellKnownAPI(); + + fetchSpy.rejects(new ConnectionError('Failed to fetch')); + + // when + let error; + + try { + await api.getWellKnownWebAppUrls(); + } catch (e) { + error = e; + } finally { + expectFetched(fetchSpy); + expect(error).to.exist; + } + }); + + + it('should throw when response is not ok', async () => { + + // given + const api = createWellKnownAPI(); + + fetchSpy.resolves(new Response({ ok: false })); + + // when + let error; + + try { + await api.getWellKnownWebAppUrls(); + } catch (e) { + error = e; + } finally { + + // then + expectFetched(fetchSpy); + expect(error).to.exist; + } + }); + + + it('should handle failed response with non-JSON body', async () => { + + // given + const api = createWellKnownAPI(); + + fetchSpy.resolves(new Response({ + ok: false, + status: 401, + json: () => JSON.parse('401 Unauthorized') + })); + + // when + let error; + + try { + await api.getWellKnownWebAppUrls(); + } catch (e) { + error = e; + } finally { + + // then + expectFetched(fetchSpy); + expect(error).to.exist; + } + }); + + + describe('timeout handling', () => { + + let clock; + + before(() => { + clock = sinon.useFakeTimers(); + }); + + after(() => clock.restore()); + + + it('should abort request on timeout', async () => { + + // given + const api = createWellKnownAPI(); + + fetchSpy.callsFake((_, { signal }) => { + return new Promise(resolve => { + for (let i = 0; i < 10; i++) { + if (signal && signal.aborted) { + throw new Error('timeout'); + } + + clock.tick(2000); + } + + resolve(new Response({ + json: () => ({ + 'adminUrl': 'http://localhost:18080/camunda/apps/admin/default/#/', + 'tasklistUrl': 'http://localhost:18080/camunda/apps/tasklist/default/#/', + 'cockpitUrl': 'http://localhost/camunda/apps/cockpit/default/#/' + }), + })); + }); + }); + + // when + let error; + + try { + await api.getWellKnownWebAppUrls(); + } catch (e) { + error = e; + } finally { + + // then + expectFetched(fetchSpy); + expect(error).to.exist; + } + }); + + }); + + }); + + describe('#getCockpitUrl', () => { + + it('should retrieve well known url', async () => { + + // given + const api = createWellKnownAPI(); + + fetchSpy.resolves(new Response({ + json: () => ({ + 'cockpitUrl': 'http://localhost/camunda/apps/cockpit/default/#/' + }), + })); + + // when + const resultUrl = await api.getCockpitUrl(); + + // then + expectFetched(fetchSpy); + expect(resultUrl).to.equal('http://localhost/camunda/apps/cockpit/default/#/'); + }); + + it('should add trailing slash to well known url', async () => { + + // given + const api = createWellKnownAPI(); + + fetchSpy.resolves(new Response({ + json: () => ({ + 'cockpitUrl': 'http://localhost/camunda/apps/cockpit/default/#/' + }), + })); + + // when + const resultUrl = await api.getCockpitUrl(); + + // then + expectFetched(fetchSpy); + expect(resultUrl).to.equal('http://localhost/camunda/apps/cockpit/default/#/'); + }); + + it('should add default engine to to well known url', async () => { + + // given + const api = createWellKnownAPI(); + + fetchSpy.resolves(new Response({ + json: () => ({ + 'cockpitUrl': 'http://localhost/camunda/apps/cockpit/default/#/' + }), + })); + + // when + const resultUrl = await api.getCockpitUrl(); + + // then + expectFetched(fetchSpy); + expect(resultUrl).to.equal('http://localhost/camunda/apps/cockpit/default/#/'); + }); + + + it('should throw when fetch fails', async () => { + + // given + const api = createWellKnownAPI(); + + fetchSpy.rejects(new ConnectionError('Failed to fetch')); + + // when + let error; + + try { + await api.getCockpitUrl(); + } catch (e) { + error = e; + } finally { + expectFetched(fetchSpy); + expect(error).to.exist; + } + }); + + + it('should throw when response is not ok', async () => { + + // given + const api = createWellKnownAPI(); + + fetchSpy.resolves(new Response({ ok: false })); + + // when + let error; + + try { + await api.getCockpitUrl(); + } catch (e) { + error = e; + } finally { + + // then + expectFetched(fetchSpy); + expect(error).to.exist; + } + }); + + + it('should handle failed response with non-JSON body', async () => { + + // given + const api = createWellKnownAPI(); + + fetchSpy.resolves(new Response({ + ok: false, + status: 401, + json: () => JSON.parse('401 Unauthorized') + })); + + // when + let error; + + try { + await api.getCockpitUrl(); + } catch (e) { + error = e; + } finally { + + // then + expectFetched(fetchSpy); + expect(error).to.exist; + } + }); + + + describe('timeout handling', () => { + + let clock; + + before(() => { + clock = sinon.useFakeTimers(); + }); + + after(() => clock.restore()); + + + it('should abort request on timeout', async () => { + + // given + const api = createWellKnownAPI(); + + fetchSpy.callsFake((_, { signal }) => { + return new Promise(resolve => { + for (let i = 0; i < 10; i++) { + if (signal && signal.aborted) { + throw new Error('timeout'); + } + + clock.tick(2000); + } + + resolve(new Response({ + json: () => ({ + 'cockpitUrl': 'http://localhost/camunda/apps/cockpit/default/#/' + }), + })); + }); + }); + + // when + let error; + + try { + await api.getCockpitUrl(); + } catch (e) { + error = e; + } finally { + + // then + expectFetched(fetchSpy); + expect(error).to.exist; + } + }); + + }); + }); +}); + + +// helpers ////////// +function Response({ + ok = true, + status = 200, + json = async () => { + return {}; + } +} = {}) { + this.ok = ok; + this.status = status; + this.json = json; +} + + +function createWellKnownAPI() { + return new WellKnownAPI('http://localhost:18080'); +} + +function expectFetched(fetchSpy, expectedOptions = {}) { + + const { + url, + ...options + } = expectedOptions; + + expect(fetchSpy).to.have.been.calledOnce; + + const [ argUrl, argOptions ] = fetchSpy.args[0]; + + expect(fetchSpy).to.have.been.calledWith(url || argUrl, { + ...argOptions, + ...options + }); + +} diff --git a/client/src/plugins/camunda-plugin/shared/__tests__/webAppUrlsSpec.js b/client/src/plugins/camunda-plugin/shared/__tests__/webAppUrlsSpec.js new file mode 100644 index 0000000000..8b5de222e5 --- /dev/null +++ b/client/src/plugins/camunda-plugin/shared/__tests__/webAppUrlsSpec.js @@ -0,0 +1,117 @@ +/** + * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH + * under one or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information regarding copyright + * ownership. + * + * Camunda licenses this file to you under the MIT; you may not use this file + * except in compliance with the MIT License. + */ + +/* global sinon */ + +import { ConnectionError } from '../RestAPI'; +import { determineCockpitUrl } from '../webAppUrls'; +import WellKnownAPI from '../WellKnownAPI'; + +let getCockpitUrlStub; + +describe('', () => { + + function stubGetCockpitUrl() { + getCockpitUrlStub = sinon.stub(WellKnownAPI.prototype, 'getCockpitUrl'); + return getCockpitUrlStub; + } + + afterEach(() => { + getCockpitUrlStub.restore(); + }); + + + describe('reachable well known api', () => { + it('should return specific Cockpit link', async () => { + + // given + stubGetCockpitUrl().returns('http://localhost:18080/camunda/app/cockpit/default/#/'); + + // when + const cockpitUrl = await determineCockpitUrl('http://localhost:18080/camunda/rest'); + + // then + expect(cockpitUrl).to.be.equal('http://localhost:18080/camunda/app/cockpit/default/#/'); + }); + + + it('should return default for missing Cockpit link', async () => { + + // given + stubGetCockpitUrl().returns(undefined); + + // when + const cockpitUrl = await determineCockpitUrl('http://localhost:8080/camunda/rest'); + + // then + expect(cockpitUrl).to.be.equal('http://localhost:8080/app/cockpit/default/#/'); + }); + + + it('should return default for empty Cockpit link', async () => { + + // given + stubGetCockpitUrl().returns(''); + + // when + const cockpitUrl = await determineCockpitUrl('http://localhost:8080/camunda/rest'); + + // then + expect(cockpitUrl).to.be.equal('http://localhost:8080/app/cockpit/default/#/'); + }); + }); + + + describe('unreachable well known api', () => { + + beforeEach(() => { + stubGetCockpitUrl().throws(new ConnectionError({ ok: false, status: 404 })); + }); + + + it('should return Spring-specific Cockpit link', async () => { + + // given + const engineRestUrl = 'http://localhost:8080/rest'; + + // when + const cockpitUrl = await determineCockpitUrl(engineRestUrl); + + // then + expect(cockpitUrl).to.be.equal('http://localhost:8080/app/cockpit/default/#/'); + }); + + + it('should return Tomcat-specific Cockpit link', async () => { + + // given + const engineRestUrl = 'http://localhost:8080/engine-rest'; + + // when + const cockpitUrl = await determineCockpitUrl(engineRestUrl); + + // then + expect(cockpitUrl).to.be.equal('http://localhost:8080/camunda/app/cockpit/default/#/'); + }); + + + it('should return Spring-specific Cockpit link for custom rest url', async () => { + + // given + const engineRestUrl = 'http://customized-camunda.bpmn.io/custom-rest'; + + // when + const cockpitUrl = await determineCockpitUrl(engineRestUrl); + + // then + expect(cockpitUrl).to.be.equal('http://customized-camunda.bpmn.io/app/cockpit/default/#/'); + }); + }); +}); diff --git a/client/src/plugins/camunda-plugin/shared/ui/CockpitDeploymentLink.js b/client/src/plugins/camunda-plugin/shared/ui/CockpitDeploymentLink.js new file mode 100644 index 0000000000..78cfea1fcc --- /dev/null +++ b/client/src/plugins/camunda-plugin/shared/ui/CockpitDeploymentLink.js @@ -0,0 +1,43 @@ +/** + * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH + * under one or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information regarding copyright + * ownership. + * + * Camunda licenses this file to you under the MIT; you may not use this file + * except in compliance with the MIT License. + */ + +import React from 'react'; + +import CockpitLink from './CockpitLink'; + +export default function CockpitDeploymentLink(props) { + const { + cockpitUrl, + deployment + } = props; + + const { + id, + deployedProcessDefinition + } = deployment; + + const cockpitPath = 'repository'; + const cockpitQuery = `?deploymentsQuery=%5B%7B%22type%22:%22id%22,%22operator%22:%22eq%22,%22value%22:%22${id}%22%7D%5D`; + + return ( + + { + deployedProcessDefinition + ? ( +
+ Process definition ID: + {deployedProcessDefinition.id} +
+ ) + : null + } +
+ ); +} diff --git a/client/src/plugins/camunda-plugin/shared/ui/CockpitLink.js b/client/src/plugins/camunda-plugin/shared/ui/CockpitLink.js new file mode 100644 index 0000000000..02dcb13ff8 --- /dev/null +++ b/client/src/plugins/camunda-plugin/shared/ui/CockpitLink.js @@ -0,0 +1,43 @@ +/** + * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH + * under one or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information regarding copyright + * ownership. + * + * Camunda licenses this file to you under the MIT; you may not use this file + * except in compliance with the MIT License. + */ + +import React, { useMemo } from 'react'; + +import * as css from './CockpitLink.less'; + +function combineUrlSegments(url, path, query) { + if (!url) { + return null; + } + + if (query) { + return `${url}${path}${query}`; + } else { + return `${url}${path}`; + } +} + +export default function CockpitLink(props) { + const { + cockpitUrl, + path = '', + query = '', + children + } = props; + + const link = useMemo(() => combineUrlSegments(cockpitUrl, path, query), [ cockpitUrl, path, query ]); + + return ( +
+ { children } + { link ? Open in Camunda Cockpit : null } +
+ ); +} diff --git a/client/src/plugins/camunda-plugin/shared/ui/CockpitLink.less b/client/src/plugins/camunda-plugin/shared/ui/CockpitLink.less new file mode 100644 index 0000000000..a3aa79fcc4 --- /dev/null +++ b/client/src/plugins/camunda-plugin/shared/ui/CockpitLink.less @@ -0,0 +1,15 @@ +:local(.CockpitLink) { + + & > div { + margin-bottom: 5px; + } + + code { + user-select: text; + padding-right: 8px; + margin-left: 2px; + background-color: var(--color-grey-225-10-90); + word-break: break-all; + border-radius: 3px; + } +} \ No newline at end of file diff --git a/client/src/plugins/camunda-plugin/shared/ui/CockpitProcessInstanceLink.js b/client/src/plugins/camunda-plugin/shared/ui/CockpitProcessInstanceLink.js new file mode 100644 index 0000000000..2b4fd360f2 --- /dev/null +++ b/client/src/plugins/camunda-plugin/shared/ui/CockpitProcessInstanceLink.js @@ -0,0 +1,35 @@ +/** + * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH + * under one or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information regarding copyright + * ownership. + * + * Camunda licenses this file to you under the MIT; you may not use this file + * except in compliance with the MIT License. + */ + +import React from 'react'; + +import CockpitLink from './CockpitLink'; + +export default function CockpitProcessInstanceLink(props) { + const { + cockpitUrl, + processInstance + } = props; + + const { + id + } = processInstance; + + const cockpitPath = `process-instance/${id}`; + + return ( + +
+ Process instance ID: + {id} +
+
+ ); +} diff --git a/client/src/plugins/camunda-plugin/shared/webAppUrls.js b/client/src/plugins/camunda-plugin/shared/webAppUrls.js new file mode 100644 index 0000000000..e4db9bd85c --- /dev/null +++ b/client/src/plugins/camunda-plugin/shared/webAppUrls.js @@ -0,0 +1,52 @@ +/** + * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH + * under one or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information regarding copyright + * ownership. + * + * Camunda licenses this file to you under the MIT; you may not use this file + * except in compliance with the MIT License. + */ + +import debug from 'debug'; + +import { forEngineRestUrl } from './WellKnownAPI'; + +const log = debug('WebAppUrls'); + +export async function determineCockpitUrl(engineUrl) { + try { + const cockpitUrl = await forEngineRestUrl(engineUrl).getCockpitUrl(); + + if (cockpitUrl) { + log(`Using cockpit url from well known endpoint: ${engineUrl}`); + return cockpitUrl; + } + + const fallbackUrl = getDefaultCockpitUrl(engineUrl); + log(`The well known endpoint did not provide a cockpit url, falling back to ${fallbackUrl}.`); + return fallbackUrl; + + } catch (e) { + const fallbackUrl = getDefaultCockpitUrl(engineUrl); + log(`An error occurred retrieving the cockpit url from well known endpoint, falling back to ${fallbackUrl}. Cause: ${e}`); + return fallbackUrl; + } +} + +// helpers ////////// + +function getDefaultCockpitUrl(engineUrl) { + return getDefaultWebAppsBaseUrl(engineUrl) + 'cockpit/default/#/'; +} + +function getDefaultWebAppsBaseUrl(engineUrl) { + const [ protocol,, host, restRoot ] = engineUrl.split('/'); + + return isTomcat(restRoot) ? `${protocol}//${host}/camunda/app/` : `${protocol}//${host}/app/`; +} + +function isTomcat(restRoot) { + return restRoot === 'engine-rest'; +} + diff --git a/client/src/plugins/camunda-plugin/start-instance-tool/StartInstanceTool.js b/client/src/plugins/camunda-plugin/start-instance-tool/StartInstanceTool.js index 84f2f25b8c..bb68886fbe 100644 --- a/client/src/plugins/camunda-plugin/start-instance-tool/StartInstanceTool.js +++ b/client/src/plugins/camunda-plugin/start-instance-tool/StartInstanceTool.js @@ -12,11 +12,12 @@ import React, { PureComponent } from 'react'; import PlayIcon from 'icons/Play.svg'; -import CamundaAPI, { ConnectionError } from '../shared/CamundaAPI'; +import CamundaAPI, { DeploymentError, StartInstanceError } from '../shared/CamundaAPI'; +import { ConnectionError } from '../shared/RestAPI'; import StartInstanceConfigOverlay from './StartInstanceConfigOverlay'; -import { DeploymentError, StartInstanceError } from '../shared/CamundaAPI'; +import CockpitProcessInstanceLink from '../shared/ui/CockpitProcessInstanceLink'; import * as css from './StartInstanceTool.less'; @@ -28,6 +29,7 @@ import isExecutable from './util/isExecutable'; import { ENGINES } from '../../../util/Engines'; import classNames from 'classnames'; +import { determineCockpitUrl } from '../shared/webAppUrls'; const START_DETAILS_CONFIG_KEY = 'start-instance-tool'; @@ -400,7 +402,7 @@ export default class StartInstanceTool extends PureComponent { return api.startInstance(processDefinition, configuration); } - handleStartSuccess(processInstance, endpoint) { + async handleStartSuccess(processInstance, endpoint) { const { displayNotification } = this.props; @@ -409,10 +411,12 @@ export default class StartInstanceTool extends PureComponent { url } = endpoint; + const cockpitUrl = await this.getCockpitUrl(url); + displayNotification({ type: 'success', title: 'Process instance started', - content: , + content: , duration: 8000 }); } @@ -524,6 +528,10 @@ export default class StartInstanceTool extends PureComponent { } } + async getCockpitUrl(engineUrl) { + return await determineCockpitUrl(engineUrl); + } + render() { const { activeTab, @@ -562,47 +570,8 @@ export default class StartInstanceTool extends PureComponent { } - -function CockpitLink(props) { - const { - endpointUrl, - processInstance - } = props; - - const { - id - } = processInstance; - - const baseUrl = getWebAppsBaseUrl(endpointUrl); - - const cockpitUrl = `${baseUrl}/cockpit/default/#/process-instance/${id}`; - - return ( -
-
- Process instance ID: - {id} -
- - Open in Camunda Cockpit - -
- ); -} - - // helpers ////////// function isBpmnTab(tab) { return tab && tab.type === 'bpmn'; } - -function getWebAppsBaseUrl(url) { - const [ protocol,, host, restRoot ] = url.split('/'); - - return isTomcat(restRoot) ? `${protocol}//${host}/camunda/app` : `${protocol}//${host}/app`; -} - -function isTomcat(restRoot) { - return restRoot === 'engine-rest'; -} diff --git a/client/src/plugins/camunda-plugin/start-instance-tool/StartInstanceTool.less b/client/src/plugins/camunda-plugin/start-instance-tool/StartInstanceTool.less index 5b6661aa6a..031d009f4a 100644 --- a/client/src/plugins/camunda-plugin/start-instance-tool/StartInstanceTool.less +++ b/client/src/plugins/camunda-plugin/start-instance-tool/StartInstanceTool.less @@ -3,20 +3,4 @@ width: 24px; height: 24px; } -} - -:local(.CockpitLink) { - - & > div { - margin-bottom: 5px; - } - - code { - user-select: text; - padding-right: 8px; - margin-left: 2px; - background-color: var(--color-grey-225-10-90); - word-break: break-all; - border-radius: 3px; - } } \ No newline at end of file diff --git a/client/src/plugins/camunda-plugin/start-instance-tool/__tests__/StartInstanceToolSpec.js b/client/src/plugins/camunda-plugin/start-instance-tool/__tests__/StartInstanceToolSpec.js index f8bd3f4710..8b32d0e52c 100644 --- a/client/src/plugins/camunda-plugin/start-instance-tool/__tests__/StartInstanceToolSpec.js +++ b/client/src/plugins/camunda-plugin/start-instance-tool/__tests__/StartInstanceToolSpec.js @@ -25,10 +25,11 @@ import StartInstanceTool from '../StartInstanceTool'; import { DeploymentError, - ConnectionError, StartInstanceError } from '../../shared/CamundaAPI'; +import { ConnectionError } from '../../shared/RestAPI'; + import { Slot, SlotFillRoot @@ -1145,13 +1146,17 @@ describe('', () => { } }; + const [ protocol,, host ] = deploymentUrl.split('/'); + const getCockpitUrlSpy = sinon.stub().returns(`${protocol}//${host}/app/cockpit/default/#/`); + const { instance } = createStartInstanceTool({ activeTab, deployService, displayNotification, - startSpy + startSpy, + getCockpitUrlSpy, }); // when @@ -1161,7 +1166,7 @@ describe('', () => { // then try { - const cockpitLink = shallow(notification.content).find('a').first(); + const cockpitLink = mount(notification.content).find('a').first(); const { href } = cockpitLink.props(); expect(href).to.eql(expectedCockpitLink); @@ -1175,22 +1180,10 @@ describe('', () => { } - it('should display Spring-specific Cockpit link', testCockpitLink( + it('should display Cockpit link', testCockpitLink( 'http://localhost:8080/rest', 'http://localhost:8080/app/cockpit/default/#/process-instance/foo' )); - - - it('should display Tomcat-specific Cockpit link', testCockpitLink( - 'http://localhost:8080/engine-rest', - 'http://localhost:8080/camunda/app/cockpit/default/#/process-instance/foo' - )); - - - it('should display Spring-specific Cockpit link for custom rest url', testCockpitLink( - 'http://customized-camunda.bpmn.io/custom-rest', - 'http://customized-camunda.bpmn.io/app/cockpit/default/#/process-instance/foo' - )); }); }); @@ -1219,6 +1212,11 @@ class TestStartInstanceTool extends StartInstanceTool { return this.props.startSpy && this.props.startSpy(...args); } + // removes WellKnownAPI dependency + async getCockpitUrl(engineUrl) { + return this.props.getCockpitUrlSpy && await this.props.getCockpitUrlSpy(engineUrl); + } + async deploy(...args) { if (this.props.deployErrorThrown) { throw this.props.deployErrorThrown; diff --git a/package-lock.json b/package-lock.json index 328922e485..4552104926 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ ], "devDependencies": { "@electron/notarize": "^2.0.0", + "@testing-library/react": "^16.0.0", "archiver": "^7.0.0", "chai": "^4.5.0", "cpx2": "^7.0.0", @@ -694,6 +695,72 @@ "node": ">=0.12" } }, + "client/node_modules/react": { + "version": "16.14.0", + "resolved": "https://registry.npmjs.org/react/-/react-16.14.0.tgz", + "integrity": "sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1", + "prop-types": "^15.6.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "client/node_modules/react-dom": { + "version": "16.14.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.14.0.tgz", + "integrity": "sha512-1gCeQXDLoIqMgqD3IO2Ah9bnf0w9kzhwN5q4FGnHZ67hBm9yePzB5JJAIQCc8x3pFnNlwFq4RidZggNAAkzWWw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1", + "prop-types": "^15.6.2", + "scheduler": "^0.19.1" + }, + "peerDependencies": { + "react": "^16.14.0" + } + }, + "client/node_modules/react-dom/node_modules/scheduler": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.19.1.tgz", + "integrity": "sha512-n/zwRWRYSUj0/3g/otKDRPMh6qv2SYMWNq85IEa8iZyAv8od9zDYpGSnpBEjNgcMNq6Scbu5KfIPxNF72R/2EA==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1" + } + }, + "client/node_modules/react-test-renderer": { + "version": "16.14.0", + "resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-16.14.0.tgz", + "integrity": "sha512-L8yPjqPE5CZO6rKsKXRO/rVPiaCOy0tQQJbC+UjPNlobl5mad59lvPjwFsQHTvL03caVDIVr9x9/OSgDe6I5Eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "object-assign": "^4.1.1", + "prop-types": "^15.6.2", + "react-is": "^16.8.6", + "scheduler": "^0.19.1" + }, + "peerDependencies": { + "react": "^16.14.0" + } + }, + "client/node_modules/react-test-renderer/node_modules/scheduler": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.19.1.tgz", + "integrity": "sha512-n/zwRWRYSUj0/3g/otKDRPMh6qv2SYMWNq85IEa8iZyAv8od9zDYpGSnpBEjNgcMNq6Scbu5KfIPxNF72R/2EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1" + } + }, "client/node_modules/reflect.ownkeys": { "version": "0.2.0", "dev": true, @@ -8626,6 +8693,175 @@ "node": ">=10" } }, + "node_modules/@testing-library/dom": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz", + "integrity": "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "chalk": "^4.1.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/dom/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@testing-library/dom/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@testing-library/dom/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@testing-library/dom/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@testing-library/dom/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@testing-library/dom/node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@testing-library/dom/node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@testing-library/dom/node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@testing-library/dom/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@testing-library/react": { + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.0.0.tgz", + "integrity": "sha512-guuxUKRWQ+FgNX0h0NS0FIq3Q3uLtWVpBzcLOggmfMoUpgBnzBzvLLd4fbm6yS8ydJd94cIfY4yP9qUQjM2KwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0", + "@types/react-dom": "^18.0.0", + "react": "^18.0.0", + "react-dom": "^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@tootallnate/once": { "version": "2.0.0", "dev": true, @@ -8694,6 +8930,14 @@ "@types/node": "*" } }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/@types/body-parser": { "version": "1.19.5", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", @@ -9929,6 +10173,17 @@ "dev": true, "license": "Python-2.0" }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "dequal": "^2.0.3" + } + }, "node_modules/array-buffer-byte-length": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz", @@ -14807,6 +15062,14 @@ "node": ">=6.0.0" } }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/dom-iterator": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/dom-iterator/-/dom-iterator-1.0.0.tgz", @@ -21329,6 +21592,17 @@ "node": ">=12" } }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "lz-string": "bin/bin.js" + } + }, "node_modules/magic-string": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.27.0.tgz", @@ -26961,30 +27235,30 @@ } }, "node_modules/react": { - "version": "16.14.0", - "resolved": "https://registry.npmjs.org/react/-/react-16.14.0.tgz", - "integrity": "sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "peer": true, "dependencies": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1", - "prop-types": "^15.6.2" + "loose-envify": "^1.1.0" }, "engines": { "node": ">=0.10.0" } }, "node_modules/react-dom": { - "version": "16.14.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.14.0.tgz", - "integrity": "sha512-1gCeQXDLoIqMgqD3IO2Ah9bnf0w9kzhwN5q4FGnHZ67hBm9yePzB5JJAIQCc8x3pFnNlwFq4RidZggNAAkzWWw==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", - "object-assign": "^4.1.1", - "prop-types": "^15.6.2", - "scheduler": "^0.19.1" + "scheduler": "^0.23.2" }, "peerDependencies": { - "react": "^16.14.0" + "react": "^18.3.1" } }, "node_modules/react-fast-compare": { @@ -27049,21 +27323,6 @@ "node": ">=4.0.0" } }, - "node_modules/react-test-renderer": { - "version": "16.14.0", - "resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-16.14.0.tgz", - "integrity": "sha512-L8yPjqPE5CZO6rKsKXRO/rVPiaCOy0tQQJbC+UjPNlobl5mad59lvPjwFsQHTvL03caVDIVr9x9/OSgDe6I5Eg==", - "dev": true, - "dependencies": { - "object-assign": "^4.1.1", - "prop-types": "^15.6.2", - "react-is": "^16.8.6", - "scheduler": "^0.19.1" - }, - "peerDependencies": { - "react": "^16.14.0" - } - }, "node_modules/read": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/read/-/read-2.1.0.tgz", @@ -29714,12 +29973,13 @@ "license": "MIT" }, "node_modules/scheduler": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.19.1.tgz", - "integrity": "sha512-n/zwRWRYSUj0/3g/otKDRPMh6qv2SYMWNq85IEa8iZyAv8od9zDYpGSnpBEjNgcMNq6Scbu5KfIPxNF72R/2EA==", + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "peer": true, "dependencies": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1" + "loose-envify": "^1.1.0" } }, "node_modules/schema-utils": { @@ -39479,6 +39739,117 @@ "defer-to-connect": "^2.0.0" } }, + "@testing-library/dom": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz", + "integrity": "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==", + "dev": true, + "peer": true, + "requires": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "chalk": "^4.1.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "pretty-format": "^27.0.2" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "peer": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "peer": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "peer": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "peer": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "peer": true + }, + "pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "peer": true, + "requires": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "dependencies": { + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "peer": true + } + } + }, + "react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "peer": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "peer": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "@testing-library/react": { + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.0.0.tgz", + "integrity": "sha512-guuxUKRWQ+FgNX0h0NS0FIq3Q3uLtWVpBzcLOggmfMoUpgBnzBzvLLd4fbm6yS8ydJd94cIfY4yP9qUQjM2KwQ==", + "dev": true, + "requires": { + "@babel/runtime": "^7.12.5" + } + }, "@tootallnate/once": { "version": "2.0.0", "dev": true @@ -39533,6 +39904,13 @@ "@types/node": "*" } }, + "@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "peer": true + }, "@types/body-parser": { "version": "1.19.5", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", @@ -40553,6 +40931,16 @@ "version": "2.0.1", "dev": true }, + "aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "peer": true, + "requires": { + "dequal": "^2.0.3" + } + }, "array-buffer-byte-length": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz", @@ -42447,6 +42835,62 @@ "ret": "~0.1.10" } }, + "react": { + "version": "16.14.0", + "resolved": "https://registry.npmjs.org/react/-/react-16.14.0.tgz", + "integrity": "sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g==", + "requires": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1", + "prop-types": "^15.6.2" + } + }, + "react-dom": { + "version": "16.14.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.14.0.tgz", + "integrity": "sha512-1gCeQXDLoIqMgqD3IO2Ah9bnf0w9kzhwN5q4FGnHZ67hBm9yePzB5JJAIQCc8x3pFnNlwFq4RidZggNAAkzWWw==", + "requires": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1", + "prop-types": "^15.6.2", + "scheduler": "^0.19.1" + }, + "dependencies": { + "scheduler": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.19.1.tgz", + "integrity": "sha512-n/zwRWRYSUj0/3g/otKDRPMh6qv2SYMWNq85IEa8iZyAv8od9zDYpGSnpBEjNgcMNq6Scbu5KfIPxNF72R/2EA==", + "requires": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1" + } + } + } + }, + "react-test-renderer": { + "version": "16.14.0", + "resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-16.14.0.tgz", + "integrity": "sha512-L8yPjqPE5CZO6rKsKXRO/rVPiaCOy0tQQJbC+UjPNlobl5mad59lvPjwFsQHTvL03caVDIVr9x9/OSgDe6I5Eg==", + "dev": true, + "requires": { + "object-assign": "^4.1.1", + "prop-types": "^15.6.2", + "react-is": "^16.8.6", + "scheduler": "^0.19.1" + }, + "dependencies": { + "scheduler": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.19.1.tgz", + "integrity": "sha512-n/zwRWRYSUj0/3g/otKDRPMh6qv2SYMWNq85IEa8iZyAv8od9zDYpGSnpBEjNgcMNq6Scbu5KfIPxNF72R/2EA==", + "dev": true, + "requires": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1" + } + } + } + }, "reflect.ownkeys": { "version": "0.2.0", "dev": true @@ -44596,6 +45040,13 @@ "esutils": "^2.0.2" } }, + "dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "peer": true + }, "dom-iterator": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/dom-iterator/-/dom-iterator-1.0.0.tgz", @@ -49262,6 +49713,13 @@ "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.5.0.tgz", "integrity": "sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ==" }, + "lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "peer": true + }, "magic-string": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.27.0.tgz", @@ -53205,24 +53663,22 @@ } }, "react": { - "version": "16.14.0", - "resolved": "https://registry.npmjs.org/react/-/react-16.14.0.tgz", - "integrity": "sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "peer": true, "requires": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1", - "prop-types": "^15.6.2" + "loose-envify": "^1.1.0" } }, "react-dom": { - "version": "16.14.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.14.0.tgz", - "integrity": "sha512-1gCeQXDLoIqMgqD3IO2Ah9bnf0w9kzhwN5q4FGnHZ67hBm9yePzB5JJAIQCc8x3pFnNlwFq4RidZggNAAkzWWw==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "peer": true, "requires": { "loose-envify": "^1.1.0", - "object-assign": "^4.1.1", - "prop-types": "^15.6.2", - "scheduler": "^0.19.1" + "scheduler": "^0.23.2" } }, "react-fast-compare": { @@ -53272,18 +53728,6 @@ } } }, - "react-test-renderer": { - "version": "16.14.0", - "resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-16.14.0.tgz", - "integrity": "sha512-L8yPjqPE5CZO6rKsKXRO/rVPiaCOy0tQQJbC+UjPNlobl5mad59lvPjwFsQHTvL03caVDIVr9x9/OSgDe6I5Eg==", - "dev": true, - "requires": { - "object-assign": "^4.1.1", - "prop-types": "^15.6.2", - "react-is": "^16.8.6", - "scheduler": "^0.19.1" - } - }, "read": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/read/-/read-2.1.0.tgz", @@ -55300,12 +55744,12 @@ "version": "8.1.2" }, "scheduler": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.19.1.tgz", - "integrity": "sha512-n/zwRWRYSUj0/3g/otKDRPMh6qv2SYMWNq85IEa8iZyAv8od9zDYpGSnpBEjNgcMNq6Scbu5KfIPxNF72R/2EA==", + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "peer": true, "requires": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1" + "loose-envify": "^1.1.0" } }, "schema-utils": { diff --git a/package.json b/package.json index d8f3079056..0ede3893dd 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ }, "devDependencies": { "@electron/notarize": "^2.0.0", + "@testing-library/react": "^16.0.0", "archiver": "^7.0.0", "chai": "^4.5.0", "cpx2": "^7.0.0",