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 (
+
+ );
+}
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 (
-
- );
-}
-
-
// 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",