diff --git a/client/package-lock.json b/client/package-lock.json
index 745284c5fd..47df1f947d 100644
--- a/client/package-lock.json
+++ b/client/package-lock.json
@@ -1612,7 +1612,9 @@
"asap": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
- "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY="
+ "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=",
+ "dev": true,
+ "optional": true
},
"asn1": {
"version": "0.2.4",
@@ -3280,15 +3282,6 @@
"sha.js": "^2.4.8"
}
},
- "create-react-context": {
- "version": "0.2.3",
- "resolved": "https://registry.npmjs.org/create-react-context/-/create-react-context-0.2.3.tgz",
- "integrity": "sha512-CQBmD0+QGgTaxDL3OX1IDXYqjkp2It4RIbcb99jS6AEg27Ga+a9G3JtK6SIu0HBwPLZlmwt9F7UwWA4Bn92Rag==",
- "requires": {
- "fbjs": "^0.8.0",
- "gud": "^1.0.0"
- }
- },
"cross-env": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/cross-env/-/cross-env-5.2.0.tgz",
@@ -4948,27 +4941,6 @@
"websocket-driver": ">=0.5.1"
}
},
- "fbjs": {
- "version": "0.8.17",
- "resolved": "https://registry.npmjs.org/fbjs/-/fbjs-0.8.17.tgz",
- "integrity": "sha1-xNWY6taUkRJlPWWIsBpc3Nn5D90=",
- "requires": {
- "core-js": "^1.0.0",
- "isomorphic-fetch": "^2.1.1",
- "loose-envify": "^1.0.0",
- "object-assign": "^4.1.0",
- "promise": "^7.1.1",
- "setimmediate": "^1.0.5",
- "ua-parser-js": "^0.7.18"
- },
- "dependencies": {
- "core-js": {
- "version": "1.2.7",
- "resolved": "https://registry.npmjs.org/core-js/-/core-js-1.2.7.tgz",
- "integrity": "sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY="
- }
- }
- },
"fd-slicer": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.0.1.tgz",
@@ -5170,19 +5142,34 @@
}
},
"formik": {
- "version": "1.5.3",
- "resolved": "https://registry.npmjs.org/formik/-/formik-1.5.3.tgz",
- "integrity": "sha512-SbNbAPbCD/aR35nJkTdu+JdTHw3sILYCC/ArJJoeHWlkDT0sY82ACFRx2VYH15odQ7zW3CCYN1adoZ+tb6QsOA==",
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/formik/-/formik-2.0.4.tgz",
+ "integrity": "sha512-Y0mR8PGRtq/U072U4tkX1wnS/geDYs0n7uPlvmKtMTIJS4g8xCpaccAerQFWxEfClMK/JGpmEyG93zItAdASJA==",
"requires": {
- "create-react-context": "^0.2.2",
"deepmerge": "^2.1.1",
- "hoist-non-react-statics": "^2.5.5",
- "lodash": "^4.17.11",
- "lodash-es": "^4.17.11",
- "prop-types": "^15.6.1",
+ "hoist-non-react-statics": "^3.3.0",
+ "lodash": "^4.17.14",
+ "lodash-es": "^4.17.14",
"react-fast-compare": "^2.0.1",
+ "scheduler": "^0.17.0",
"tiny-warning": "^1.0.2",
- "tslib": "^1.9.3"
+ "tslib": "^1.10.0"
+ },
+ "dependencies": {
+ "scheduler": {
+ "version": "0.17.0",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.17.0.tgz",
+ "integrity": "sha512-7rro8Io3tnCPuY4la/NuI5F2yfESpnfZyT6TtkXnSWVkcu0BCDJ+8gk5ozUaFaxpIyNuWAPXrH0yFcSi28fnDA==",
+ "requires": {
+ "loose-envify": "^1.1.0",
+ "object-assign": "^4.1.1"
+ }
+ },
+ "tslib": {
+ "version": "1.10.0",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz",
+ "integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ=="
+ }
}
},
"forwarded": {
@@ -5978,11 +5965,6 @@
"integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==",
"dev": true
},
- "gud": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/gud/-/gud-1.0.0.tgz",
- "integrity": "sha512-zGEOVKFM5sVPPrYs7J5/hYEw2Pof8KCyOwyhG8sAF26mCAeUFAcYPu1mwB7hhpIP29zOIBaDqwuHdLp0jvZXjw=="
- },
"hammerjs": {
"version": "2.0.8",
"resolved": "https://registry.npmjs.org/hammerjs/-/hammerjs-2.0.8.tgz",
@@ -6159,9 +6141,12 @@
}
},
"hoist-non-react-statics": {
- "version": "2.5.5",
- "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz",
- "integrity": "sha512-rqcy4pJo55FTTLWt+bU8ukscqHeE/e9KWvsOW2b/a3afxQZhwkQdT1rPPCJ0rYXdj4vNcasY8zHTH+jF/qStxw=="
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz",
+ "integrity": "sha512-wbg3bpgA/ZqWrZuMOeJi8+SKMhr7X9TesL/rXMjTzh0p0JUBo3II8DHboYbuIXWRlttrUFxwcu/5kygrCw8fJw==",
+ "requires": {
+ "react-is": "^16.7.0"
+ }
},
"homedir-polyfill": {
"version": "1.0.3",
@@ -6877,15 +6862,6 @@
"integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=",
"dev": true
},
- "isomorphic-fetch": {
- "version": "2.2.1",
- "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz",
- "integrity": "sha1-YRrhrPFPXoH3KVB0coGf6XM1WKk=",
- "requires": {
- "node-fetch": "^1.0.1",
- "whatwg-fetch": ">=0.10.0"
- }
- },
"isstream": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
@@ -7375,9 +7351,9 @@
"integrity": "sha512-mmKYbW3GLuJeX+iGP+Y7Gp1AiGHGbXHCOh/jZmrawMmsE7MS4znI3RL2FsjbqOyMayHInjOeykW7PEajUk1/xw=="
},
"lodash-es": {
- "version": "4.17.11",
- "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.11.tgz",
- "integrity": "sha512-DHb1ub+rMjjrxqlB3H56/6MXtm1lSksDp2rA2cNWjG8mlDUYFhUj3Di2Zn5IwSU87xLv8tNIQ7sSwE/YOX/D/Q=="
+ "version": "4.17.15",
+ "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.15.tgz",
+ "integrity": "sha512-rlrc3yU3+JNOpZ9zj5pQtxnx2THmvRykwL4Xlxoa8I9lHBlVbbyPhgyPMioxVZ4NqyxaVVtaJnzsyOidQIhyyQ=="
},
"lodash.clonedeep": {
"version": "4.5.0",
@@ -9138,6 +9114,8 @@
"version": "7.3.1",
"resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz",
"integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==",
+ "dev": true,
+ "optional": true,
"requires": {
"asap": "~2.0.3"
}
@@ -9421,8 +9399,7 @@
"react-is": {
"version": "16.8.6",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.8.6.tgz",
- "integrity": "sha512-aUk3bHfZ2bRSVFFbbeVS4i+lNPZr3/WM5jT2J5omUVV1zzcs1nAaf3l51ctA5FFvCRbhrH0bdAsRRQddFJZPtA==",
- "dev": true
+ "integrity": "sha512-aUk3bHfZ2bRSVFFbbeVS4i+lNPZr3/WM5jT2J5omUVV1zzcs1nAaf3l51ctA5FFvCRbhrH0bdAsRRQddFJZPtA=="
},
"react-svg-core": {
"version": "3.0.3",
@@ -10292,7 +10269,8 @@
"setimmediate": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
- "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU="
+ "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=",
+ "dev": true
},
"setprototypeof": {
"version": "1.1.1",
@@ -11411,9 +11389,9 @@
"integrity": "sha512-6XhXIOnwlM6dpuMogF6/C1u3EDUbRbjovbdVbnIGgDxG8HOW79B51MHwBQHQnVL4Pkad4phzQd5WDeBefuWDPg=="
},
"tiny-warning": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.2.tgz",
- "integrity": "sha512-rru86D9CpQRLvsFG5XFdy0KdLAvjdQDyZCsRcuu60WtzFylDM3eAWSxEVz5kzL2Gp544XiUvPbVKtOA/txLi9Q=="
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz",
+ "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA=="
},
"tmp": {
"version": "0.0.33",
@@ -11579,7 +11557,8 @@
"tslib": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz",
- "integrity": "sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ=="
+ "integrity": "sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==",
+ "dev": true
},
"tty-browserify": {
"version": "0.0.0",
@@ -11652,11 +11631,6 @@
"integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=",
"dev": true
},
- "ua-parser-js": {
- "version": "0.7.19",
- "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.19.tgz",
- "integrity": "sha512-T3PVJ6uz8i0HzPxOF9SWzWAlfN/DavlpQqepn22xgve/5QecC+XMCAtmUNnY7C9StehaV6exjUCI801lOI7QlQ=="
- },
"uglify-js": {
"version": "3.5.8",
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.5.8.tgz",
@@ -12327,11 +12301,6 @@
"integrity": "sha512-nqHUnMXmBzT0w570r2JpJxfiSD1IzoI+HGVdd3aZ0yNi3ngvQ4jv1dtHt5VGxfI2yj5yqImPhOK4vmIh2xMbGg==",
"dev": true
},
- "whatwg-fetch": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.0.0.tgz",
- "integrity": "sha512-9GSJUgz1D4MfyKU7KRqwOjXCXTqWdFNvEr7eUBYchQiVc744mqK/MzXPNR2WsPkmkOa4ywfg8C2n8h+13Bey1Q=="
- },
"which": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
diff --git a/client/package.json b/client/package.json
index 082adacc70..f7a2de18c9 100644
--- a/client/package.json
+++ b/client/package.json
@@ -30,7 +30,7 @@
"dmn-js-properties-panel": "^0.3.1",
"drag-tabs": "^2.2.0",
"events": "^3.0.0",
- "formik": "^1.5.3",
+ "formik": "^2.0.4",
"ids": "^0.2.2",
"min-dash": "^3.4.0",
"p-defer": "^3.0.0",
diff --git a/client/src/plugins/deployment-tool/CamundaAPI.js b/client/src/plugins/deployment-tool/CamundaAPI.js
index 3d838057cf..c2980e9468 100644
--- a/client/src/plugins/deployment-tool/CamundaAPI.js
+++ b/client/src/plugins/deployment-tool/CamundaAPI.js
@@ -8,29 +8,33 @@
* except in compliance with the MIT License.
*/
-import {
- ConnectionError,
- DeploymentError
-} from './errors';
+import AuthTypes from './AuthTypes';
+
+import debug from 'debug';
const FETCH_TIMEOUT = 5000;
+const log = debug('CamundaAPI');
+
export default class CamundaAPI {
- constructor(baseUrl) {
- this.baseUrl = baseUrl;
+
+ constructor(endpoint) {
+
+ this.baseUrl = normalizeBaseURL(endpoint.url);
+
+ this.authentication = this.getAuthentication(endpoint);
}
- async deployDiagram(diagram, details) {
+ async deployDiagram(diagram, deployment) {
const {
- auth,
- deploymentName,
+ name,
tenantId
- } = details;
+ } = deployment;
const form = new FormData();
- form.append('deployment-name', deploymentName);
+ form.append('deployment-name', name);
form.append('deployment-source', 'Camunda Modeler');
form.append('deploy-changed-only', 'true');
@@ -44,12 +48,9 @@ export default class CamundaAPI {
form.append(diagramName, blob, diagramName);
- const headers = this.getHeaders(auth);
-
- const response = await this.safelyFetch(`${this.baseUrl}/deployment/create`, {
+ const response = await this.fetch('/deployment/create', {
method: 'POST',
- body: form,
- headers
+ body: form
});
if (response.ok) {
@@ -66,17 +67,14 @@ export default class CamundaAPI {
};
}
- const body = await this.safelyParse(response);
+ const body = await this.parse(response);
throw new DeploymentError(response, body);
}
- async checkConnection(details = {}) {
- const { auth } = details;
-
- const headers = this.getHeaders(auth);
+ async checkConnection() {
- const response = await this.safelyFetch(`${this.baseUrl}/deployment?maxResults=0`, { headers });
+ const response = await this.fetch('/deployment?maxResults=0');
if (response.ok) {
return;
@@ -85,21 +83,50 @@ export default class CamundaAPI {
throw new ConnectionError(response);
}
- getHeaders(auth) {
+ getAuthentication(endpoint) {
+
+ const {
+ authType,
+ username,
+ password,
+ token
+ } = endpoint;
+
+ switch (authType) {
+ case AuthTypes.basic:
+ return {
+ username,
+ password
+ };
+ case AuthTypes.bearer:
+ return {
+ token
+ };
+ }
+ }
+
+ getHeaders() {
const headers = {
accept: 'application/json'
};
- if (auth) {
- headers.authorization = this.getAuthHeader(auth);
+ if (this.authentication) {
+ headers.authorization = this.getAuthHeader(this.authentication);
}
return headers;
}
- getAuthHeader({ bearer, username, password }) {
- if (bearer) {
- return `Bearer ${bearer}`;
+ getAuthHeader(endpoint) {
+
+ const {
+ token,
+ username,
+ password
+ } = endpoint;
+
+ if (token) {
+ return `Bearer ${token}`;
}
if (username && password) {
@@ -111,35 +138,39 @@ export default class CamundaAPI {
throw new Error('Unknown auth options.');
}
- async safelyFetch(url, options = {}) {
- let response;
+ async fetch(path, options = {}) {
+ const url = `${this.baseUrl}${path}`;
+ const headers = this.getHeaders();
try {
- options.signal = options.signal || this.setupTimeoutSignal();
- response = await fetch(url, options);
+ const signal = options.signal || this.setupTimeoutSignal();
+
+ return await fetch(url, {
+ ...options,
+ headers,
+ signal
+ });
} catch (error) {
- response = {
+ log('failed to fetch', error);
+
+ return {
url,
json: () => {
return {};
}
};
}
-
- return response;
}
setupTimeoutSignal(timeout = FETCH_TIMEOUT) {
const controller = new AbortController();
- const { signal } = controller;
-
setTimeout(() => controller.abort(), timeout);
- return signal;
+ return controller.signal;
}
- async safelyParse(response) {
+ async parse(response) {
try {
const json = await response.json();
@@ -149,3 +180,113 @@ export default class CamundaAPI {
}
}
}
+
+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. Most likely your machine is not online right now',
+ [ CONNECTION_FAILED ]: 'Could not connect to the server. Did you run the engine?',
+ [ DIAGRAM_PARSE_ERROR ]: 'Server could not parse the diagram. Please check log for errors.',
+ [ UNAUTHORIZED ]: 'Authentication failed. Please check your credentials.',
+ [ FORBIDDEN ]: 'This user is not permitted to deploy. Please use different credentials or get this user enabled to deploy.',
+ [ NOT_FOUND ]: 'Could not find the Camunda endpoint. Please check the URL and make sure Camunda is running.',
+ [ 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) {
+ super('Deployment failed');
+
+ this.code = (
+ getCamundaErrorCode(response, body) ||
+ getResponseErrorCode(response) ||
+ getNetworkErrorCode(response)
+ );
+
+ this.details = ApiErrorMessages[this.code];
+
+ this.problems = body && body.message;
+ }
+}
+
+
+// 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.';
+
+ if (body && body.message && body.message.startsWith(PARSE_ERROR_PREFIX)) {
+ return DIAGRAM_PARSE_ERROR;
+ }
+}
+
+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/deployment-tool/DeploymentConfigValidator.js b/client/src/plugins/deployment-tool/DeploymentConfigValidator.js
new file mode 100644
index 0000000000..3db1038759
--- /dev/null
+++ b/client/src/plugins/deployment-tool/DeploymentConfigValidator.js
@@ -0,0 +1,325 @@
+/**
+ * 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 pDefer from 'p-defer';
+
+import AuthTypes from './AuthTypes';
+
+import CamundaAPI from './CamundaAPI';
+
+
+export default class DeploymentConfigValidator {
+
+ validateEndpointURL = (value) => {
+ return (
+ this.validateNonEmpty(value, 'Endpoint URL must not be empty.') ||
+ this.validatePattern(value, /^https?:\/\//, 'Endpoint URL must start with "http://" or "https://".') ||
+ null
+ );
+ }
+
+ validatePattern = (value, pattern, message) => {
+ const matches = pattern.test(value);
+
+ return matches ? null : message;
+ }
+
+ validateNonEmpty = (value, message = 'Must provide a value.') => {
+ return value ? null : message;
+ }
+
+ validateDeploymentName = (value) => {
+ return this.validateNonEmpty(value, 'Deployment name must not be empty.');
+ }
+
+ validateToken = (value) => {
+ return this.validateNonEmpty(value, 'Token must not be empty.');
+ }
+
+ validatePassword = (value) => {
+ return this.validateNonEmpty(value, 'Password must not be empty.');
+ }
+
+ validateUsername = (value) => {
+ return this.validateNonEmpty(value, 'Username must not be empty.');
+ }
+
+ validateDeployment(deployment = {}) {
+ return this.validate(deployment, {
+ name: this.validateDeploymentName
+ });
+ }
+
+ validateEndpoint(endpoint = {}) {
+
+ return this.validate(endpoint, {
+ url: this.validateEndpointURL,
+ token: endpoint.authType === AuthTypes.bearer && this.validateToken,
+ password: endpoint.authType === AuthTypes.basic && this.validatePassword,
+ username: endpoint.authType === AuthTypes.basic && this.validateUsername
+ });
+ }
+
+ validate(values, validators) {
+
+ const errors = {};
+
+ for (const [ attr, validator ] of Object.entries(validators)) {
+
+ if (!validator) {
+ continue;
+ }
+
+ const error = validator(values[attr]);
+
+ if (error) {
+ errors[attr] = error;
+ }
+ }
+
+ return errors;
+ }
+
+ validateConnection = async endpoint => {
+
+ const api = new CamundaAPI(endpoint);
+
+ try {
+ await api.checkConnection();
+ } catch (error) {
+ return error;
+ }
+
+ return null;
+ }
+
+ validateBasic(configuration) {
+
+ const {
+ deployment,
+ endpoint
+ } = configuration;
+
+ const deploymentErrors = this.validateDeployment(deployment);
+ const endpointErrors = this.validateEndpoint(endpoint);
+
+ return filterErrors({
+ deployment: deploymentErrors,
+ endpoint: endpointErrors
+ });
+ }
+
+ isConfigurationValid(configuration) {
+
+ const errors = this.validateBasic(configuration);
+
+ return !hasKeys(errors);
+ }
+
+ createConnectionChecker() {
+ return new ConnectionChecker(this);
+ }
+
+}
+
+
+class ConnectionChecker {
+
+ constructor(validator) {
+ this.validator = validator;
+ }
+
+ subscribe(hooks) {
+ this.hooks = hooks;
+ }
+
+ unsubscribe() {
+
+ if (this.checkTimer) {
+ clearTimeout(this.checkTimer);
+
+ this.checkTimer = null;
+ }
+
+ this.endpoint = { __non_existing_endpoint: true };
+
+ this.lastCheck = null;
+
+ this.hooks = null;
+ }
+
+ check(endpoint) {
+ this.setEndpoint(endpoint);
+
+ const {
+ lastCheck
+ } = this;
+
+ // return cached result if endpoint did not change
+ // we'll periodically re-check in background anyway
+ if (lastCheck && shallowEquals(endpoint, lastCheck.endpoint)) {
+ return Promise.resolve(lastCheck.result);
+ }
+
+ const deferred = this.scheduleCheck();
+
+ return deferred.promise;
+ }
+
+ setEndpoint(endpoint) {
+ this.endpoint = endpoint;
+ }
+
+ checkCompleted(endpoint, result) {
+
+ const {
+ endpoint: currentEndpoint,
+ deferred,
+ hooks
+ } = this;
+
+ if (!shallowEquals(endpoint, currentEndpoint)) {
+ return;
+ }
+
+ const {
+ connectionError,
+ endpointErrors
+ } = result;
+
+ this.lastCheck = {
+ endpoint,
+ unauthorized: connectionError && connectionError.code === 'UNAUTHORIZED',
+ result
+ };
+
+ this.deferred = null;
+
+ deferred.resolve(result);
+
+ hooks && hooks.onComplete && hooks.onComplete(result);
+
+ if (!hasKeys(endpointErrors)) {
+ this.scheduleCheck();
+ }
+ }
+
+ checkStart() {
+
+ const {
+ hooks
+ } = this;
+
+ hooks.onStart && hooks.onStart();
+ }
+
+ scheduleCheck() {
+
+ const {
+ endpoint,
+ lastCheck,
+ checkTimer,
+ validator
+ } = this;
+
+ const deferred = this.deferred = this.deferred || pDefer();
+
+ // stop scheduled check
+ if (checkTimer) {
+ clearTimeout(checkTimer);
+ }
+
+ const endpointErrors = validator.validateEndpoint(endpoint);
+
+ if (hasKeys(endpointErrors)) {
+ this.checkCompleted(endpoint, {
+ endpointErrors
+ });
+ } else {
+
+ const delay = this.getCheckDelay(endpoint, lastCheck);
+
+ this.checkTimer = setTimeout(() => {
+ this.triggerCheck();
+ }, delay);
+ }
+
+ return deferred;
+ }
+
+ triggerCheck() {
+ const {
+ endpoint,
+ validator
+ } = this;
+
+ this.checkStart();
+
+ validator.validateConnection(endpoint).then(connectionError => {
+
+ this.checkCompleted(endpoint, {
+ connectionError
+ });
+
+ }).catch(error => {
+ console.error('connection check failed', error);
+ });
+ }
+
+ getCheckDelay(endpoint, lastCheck) {
+
+ if (!lastCheck) {
+ return 1000;
+ }
+
+ const {
+ endpoint: lastEndpoint,
+ unauthorized
+ } = lastCheck;
+
+ const endpointChanged = !shallowEquals(endpoint, lastEndpoint);
+
+ if (endpointChanged) {
+ return 1000;
+ }
+
+ // back-off if last check was unauthorized.
+ // we do not want the user to be blocked by the engine
+ return unauthorized ? 15000 : 5000;
+ }
+
+}
+
+// helpers /////////////////
+
+function hasKeys(obj) {
+ return obj && Object.keys(obj).length > 0;
+}
+
+function filterErrors(errors) {
+
+ return Object.entries(errors).reduce((filtered, [ key, value ]) => {
+
+ if (value && hasKeys(value)) {
+ filtered[key] = value;
+ }
+
+ return filtered;
+ }, {});
+}
+
+
+function hash(el) {
+ return JSON.stringify(el);
+}
+
+function shallowEquals(a, b) {
+ return hash(a) === hash(b);
+}
diff --git a/client/src/plugins/deployment-tool/DeploymentDetailsModal.js b/client/src/plugins/deployment-tool/DeploymentDetailsModal.js
index c3f9e9e0a5..2c8e8f6e84 100644
--- a/client/src/plugins/deployment-tool/DeploymentDetailsModal.js
+++ b/client/src/plugins/deployment-tool/DeploymentDetailsModal.js
@@ -10,208 +10,191 @@
import React from 'react';
-import { debounce } from 'min-dash';
-
import { Modal } from '../../app/primitives';
+import {
+ omit
+} from 'min-dash';
+
import css from './DeploymentDetailsModal.less';
import AuthTypes from './AuthTypes';
import {
- AuthBasic,
- AuthBearer,
- FormControl
+ CheckBox,
+ Select,
+ TextInput
} from './components';
import {
- Field,
- Form,
- Formik
+ Formik,
+ Field
} from 'formik';
-import { ConnectionErrorMessages } from './errors';
+export default class DeploymentDetailsModal extends React.PureComponent {
-const initialFormValues = {
- endpointUrl: 'http://localhost:8080/engine-rest',
- tenantId: '',
- deploymentName: 'diagram',
- authType: AuthTypes.none,
- username: '',
- password: '',
- bearer: ''
-};
+ constructor(props) {
+ super(props);
-export default class DeploymentDetailsModal extends React.PureComponent {
+ const {
+ validator,
+ configuration
+ } = props;
+
+ this.state = {
+ connectionState: {},
+ deploymentDetailsShown: configuration.deployment.tenantId
+ };
- state = {
- detailsOpen: false,
- checkingConnection: null,
- connectionError: null,
- connectionHint: null,
- lastPassword: null,
- lastUsername: null,
- lastAuthType: null
+ this.connectionChecker = validator.createConnectionChecker();
}
- componentDidMount() {
- this.mounted = true;
+ handleConnectionCheckStart = () => {
+ this.setConnectionState({
+ isValidating: true,
+ isValidated: false
+ });
+ }
- // check connection with pre-validated initial form values
- const initialValues = this.getInitialValues();
- const errors = this.props.validate(initialValues);
+ handleConnectionChecked = (result) => {
- this.checkConnectionIfNeeded(initialValues, errors, true);
+ const {
+ endpointErrors,
+ connectionError
+ } = result;
+
+ this.setConnectionState({
+ isValidating: false,
+ isValidated: true,
+ isValid: !hasKeys(endpointErrors) && !connectionError,
+ endpointErrors,
+ connectionError
+ });
}
- componentWillUnmount() {
- this.mounted = false;
+ componentDidMount() {
+ this.connectionChecker.subscribe({
+ onStart: this.handleConnectionCheckStart,
+ onComplete: this.handleConnectionChecked
+ });
}
- getInitialValues() {
- return { ...initialFormValues, ...this.props.details };
+ componentWillUnmount() {
+ this.connectionChecker.unsubscribe();
}
- checkConnection = async values => {
- if (!this.mounted || this.state.checkingConnection) {
- return;
- }
-
+ setConnectionState(connectionState) {
this.setState({
- checkingConnection: true,
- connectionHint: null,
- lastUsername: values.username,
- lastPassword: values.password,
- lastAuthType: values.authType
+ connectionState: {
+ ...this.state.connectionState,
+ ...connectionState
+ }
});
-
- const connectionError = await this.props.checkConnection(values);
-
- this.mounted && this.setState({ connectionError, checkingConnection: false });
}
- lazilyCheckConnection = debounce(this.checkConnection, 1000);
+ validate = (values, form) => {
- moreLazilyCheckConnection = debounce(this.checkConnection, 2000);
+ this.connectionChecker.check(values.endpoint).then(() => {
+ return null;
+ });
+ };
- validate = values => {
- const errors = this.props.validate(values);
+ onClose = (action = 'cancel', data) => this.props.onClose(action, data);
- this.checkConnectionIfNeeded(values, errors);
+ onCancel = () => this.onClose('cancel');
- return errors;
+ onSubmit = (values) => {
+ this.onClose('deploy', values);
}
- checkConnectionIfNeeded(values, errors, immediately = false) {
-
- const {
- authType,
- username,
- password
- } = values;
-
- const missingConfigHint = this.getEndpointConfigHint(values, errors);
+ fieldError = (meta) => {
+ return (this.state.connectionState.isValidated || meta.touched) && meta.error;
+ }
- // skip connection check in case of invalid input
- if (missingConfigHint) {
- this.setState({
- connectionHint: missingConfigHint
- });
+ setAuthType = (form) => {
- return;
- }
+ return event => {
- if (immediately) {
- return this.checkConnection(values);
- }
+ const authType = event.target.value;
- // check connection if authentication type was changed or not using HTTP Basic
- if (authType !== AuthTypes.basic || authType !== this.state.lastAuthType) {
- return this.lazilyCheckConnection(values);
- }
+ const {
+ values,
+ setValues
+ } = form;
- const usernameOrPasswordChanged = !(
- username === this.state.lastUsername && password === this.state.lastPassword
- );
+ let {
+ endpoint
+ } = values;
- const previouslyUnauthorized = this.state.connectionError === ConnectionErrorMessages.unauthorized;
+ if (authType !== AuthTypes.basic) {
+ endpoint = omit(endpoint, [ 'username', 'password' ]);
+ }
- // skip connection check if already failed for provided credentials
- if (!usernameOrPasswordChanged && previouslyUnauthorized) {
- return;
- }
+ if (authType !== AuthTypes.bearer) {
+ endpoint = omit(endpoint, [ 'token' ]);
+ }
- // apply longer delay if unauthorized during last check
- if (previouslyUnauthorized) {
- return this.moreLazilyCheckConnection(values);
- }
+ setValues({
+ ...values,
+ endpoint: {
+ ...endpoint,
+ authType
+ }
+ });
+ };
- return this.lazilyCheckConnection(values);
}
- getEndpointConfigHint(values, errors) {
- const areCredentialsMissing = this.getCredentialsConfigFields(values.authType)
- .some(field => errors[field]);
-
- if (errors.endpointUrl && areCredentialsMissing) {
- return 'Please finish the endpoint configuration to test the server connection.';
- } else if (errors.endpointUrl) {
- return 'Please provide a valid REST endpoint to test the server connection.';
- } else if (areCredentialsMissing) {
- return 'Please add the credentials to test the server connection.';
- }
- }
+ toggleDetails = (form) => {
- getCredentialsConfigFields(authType) {
- switch (authType) {
- case AuthTypes.none:
- return [];
- case AuthTypes.bearer:
- return [ 'bearer' ];
- case AuthTypes.basic:
- return [ 'username', 'password' ];
- }
- }
+ return (event) => {
- onClose = () => this.props.onClose();
+ const {
+ deployment
+ } = form.values;
- onSubmit = (values, { setSubmitting }) => {
- if (this.state.connectionError) {
- return setSubmitting(false);
- }
+ if (deployment.tenantId) {
+ return;
+ }
- this.props.onClose(values);
+ this.setState({
+ deploymentDetailsShown: !this.state.deploymentDetailsShown
+ });
+ };
}
- toggleDetails = () => this.setState(state => ({ ...state, detailsOpen: !state.detailsOpen }));
-
render() {
+
const {
- onFocusChange
- } = this.props;
+ fieldError,
+ onSubmit,
+ validate,
+ onClose
+ } = this;
- const initialValues = this.getInitialValues();
+ const {
+ configuration: values,
+ validator
+ } = this.props;
const {
- checkingConnection,
- connectionError,
- connectionHint,
- detailsOpen
+ connectionState,
+ deploymentDetailsShown
} = this.state;
- const onClose = this.onClose;
-
return (
- {({ isSubmitting, values }) => (
-
-
Deployment Details
- { (detailsOpen || values['tenantId']) ? '-' : '+' }
+ {
+ (deploymentDetailsShown)
+ ? '-'
+ : '+'
+ }
+
- { (detailsOpen || values['tenantId']) && }
-
+
+ Endpoint Configuration
+
- Endpoint Configuration
-
-
+
+
-
- Authentication
-
-
-
-
- None
- HTTP Basic
- Bearer token
-
-
-
- { values.authType === AuthTypes.basic && (
-
) }
-
- { values.authType === AuthTypes.bearer && (
-
) }
-
+
+ None
+ HTTP Basic
+ Bearer token
+
+
+ { form.values.endpoint.authType === AuthTypes.basic && (
+
+
+
+
+
+ )}
+
+ { form.values.endpoint.authType === AuthTypes.bearer && (
+
+ )}
+
+ { form.values.endpoint.authType !== AuthTypes.none && (
+
+ )}
+
+ className="btn btn-primary"
+ disabled={ form.isSubmitting }
+ >
Deploy
+ className="btn"
+ onClick={ () => onClose('cancel') }
+ >
Cancel
+
-
-
+
)}
-
-
);
}
}
-function ConnectionCheckResult({ checkingConnection, error, hint }) {
- if (error) {
+function ConnectionFeedback(props) {
+
+ const {
+ connectionState
+ } = props;
+
+ const {
+ isValid,
+ isValidating,
+ isValidated,
+ endpointErrors,
+ connectionError
+ } = connectionState;
+
+ if (!isValidating && !isValidated) {
+ return null;
+ }
+
+ if (isValidating) {
return (
-
- { error }
+
+ Validating connection.
);
}
- if (hint) {
+ if (isValid) {
+ return (
+
+ Connected successfully.
+
+ );
+ }
+
+ if (hasKeys(endpointErrors)) {
+
+ const message =
+ endpointErrors.url
+ ? 'Please provide a valid REST endpoint to test the server connection.'
+ : (endpointErrors.token || endpointErrors.username || endpointErrors.password)
+ ? 'Please add the credentials to test the server connection'
+ : 'Please correct validation errors';
+
return (
- { hint }
+ { message }
);
}
- if (checkingConnection === false) {
+ if (connectionError) {
return (
-
- Connected successfully.
+
+ { connectionError.details }
);
}
- return (
-
- Testing the connection.
-
- );
+ throw new Error('unexpected connection state');
+}
+
+function hasKeys(obj) {
+ return obj && Object.keys(obj).length > 0;
}
diff --git a/client/src/plugins/deployment-tool/DeploymentDetailsModal.less b/client/src/plugins/deployment-tool/DeploymentDetailsModal.less
index 986c680e88..55f229c01a 100644
--- a/client/src/plugins/deployment-tool/DeploymentDetailsModal.less
+++ b/client/src/plugins/deployment-tool/DeploymentDetailsModal.less
@@ -3,6 +3,10 @@
font-weight: normal;
}
+ .icon {
+ vertical-align: text-top;
+ }
+
.intro {
margin-bottom: 30px;
}
@@ -47,13 +51,13 @@
width: 10px;
}
- label {
- text-align: right;
+ label:only-child {
width: 100%;
display: inline-block;
+ text-align: right;
}
- input,
+ input:not([type="checkbox"]),
select {
width: 100%;
padding: 6px;
diff --git a/client/src/plugins/deployment-tool/DeploymentTool.js b/client/src/plugins/deployment-tool/DeploymentTool.js
index 25a15273e2..e876d69f2f 100644
--- a/client/src/plugins/deployment-tool/DeploymentTool.js
+++ b/client/src/plugins/deployment-tool/DeploymentTool.js
@@ -10,13 +10,15 @@
import React, { PureComponent } from 'react';
-import { omit } from 'min-dash';
-
import AuthTypes from './AuthTypes';
import CamundaAPI from './CamundaAPI';
+
import DeploymentDetailsModal from './DeploymentDetailsModal';
-import getEditMenu from './getEditMenu';
-import validators from './validators';
+import DeploymentConfigValidator from './DeploymentConfigValidator';
+
+import {
+ generateId
+} from '../../util';
import { Fill } from '../../app/slot-fill';
@@ -25,12 +27,14 @@ import {
Icon
} from '../../app/primitives';
-const VALIDATED_FIELDS = [
- 'deploymentName',
- 'endpointUrl'
-];
+const DEPLOYMENT_DETAILS_CONFIG_KEY = 'deployment-tool';
+const ENGINE_ENDPOINTS_CONFIG_KEY = 'camundaEngineEndpoints';
-const CONFIG_KEY = 'deployment-config';
+const DEFAULT_ENDPOINT = {
+ url: 'http://localhost:8080/engine-rest',
+ authType: AuthTypes.none,
+ rememberCredentials: false
+};
export default class DeploymentTool extends PureComponent {
@@ -40,6 +44,8 @@ export default class DeploymentTool extends PureComponent {
activeTab: null
}
+ validator = new DeploymentConfigValidator();
+
componentDidMount() {
this.props.subscribe('app.activeTabChanged', ({ activeTab }) => {
this.setState({ activeTab });
@@ -54,33 +60,19 @@ export default class DeploymentTool extends PureComponent {
return triggerAction('save-tab', { tab });
}
- deploy = () => {
+ deploy = (options = {}) => {
const {
activeTab
} = this.state;
- this.deployTab(activeTab);
+ return this.deployTab(activeTab, options);
}
- async saveDetails(tab, details) {
- const {
- config
- } = this.props;
-
- const savedDetails = this.getDetailsToSave(details);
-
- return config.setForFile(tab.file, CONFIG_KEY, savedDetails);
- }
+ async deployTab(tab, options={}) {
- async getSavedDetails(tab) {
const {
- config
- } = this.props;
-
- return config.getForFile(tab.file, CONFIG_KEY);
- }
-
- async deployTab(tab) {
+ configure
+ } = options;
// (1) Open save file dialog if dirty
tab = await this.saveTab(tab);
@@ -90,199 +82,258 @@ export default class DeploymentTool extends PureComponent {
return;
}
- // (2) Get deployment details
- // (2.1) Try to get existing deployment details
- let details = await this.getSavedDetails(tab);
+ // (2) Get deployment configuration
+ // (2.1) Try to get existing deployment configuration
+ let configuration = await this.getSavedConfiguration(tab);
- // (2.2) Check if details are complete
- const canDeploy = this.canDeployWithDetails(details);
+ // (2.2) Check if configuration are complete
+ const showConfiguration = configure || !this.canDeployWithConfiguration(configuration);
- if (!canDeploy) {
+ if (showConfiguration) {
- // (2.3) Open modal to enter deployment details
- details = await this.getDetailsFromUserInput(tab, details);
+ // (2.3) Open modal to enter deployment configuration
+ const {
+ action,
+ configuration: userConfiguration
+ } = await this.getConfigurationFromUserInput(tab, configuration);
// (2.3.1) Handle user cancelation
- if (!details) {
+ if (action === 'cancel') {
return;
}
- await this.saveDetails(tab, details);
+ configuration = await this.saveConfiguration(tab, userConfiguration);
+
+ if (action === 'save') {
+ return;
+ }
}
// (3) Trigger deployment
// (3.1) Show deployment result (success or error)
+
+ try {
+ const deployment = await this.deployWithConfiguration(tab, configuration);
+
+ await this.handleDeploymentSuccess(tab, deployment);
+ } catch (error) {
+ await this.handleDeploymentError(tab, error);
+ }
+ }
+
+ handleDeploymentSuccess(tab, deployment) {
+ const {
+ displayNotification
+ } = this.props;
+
+ displayNotification({
+ type: 'success',
+ title: 'Deployment succeeded',
+ duration: 4000
+ });
+ }
+
+ handleDeploymentError(tab, error) {
const {
log,
displayNotification
} = this.props;
- try {
- await this.deployWithDetails(tab, details);
+ displayNotification({
+ type: 'error',
+ title: 'Deployment failed',
+ content: 'See the log for further details.',
+ duration: 10000
+ });
- displayNotification({
- type: 'success',
- title: 'Deployment succeeded',
- duration: 4000
- });
- } catch (error) {
- displayNotification({
- type: 'error',
- title: 'Deployment failed',
- content: 'See the log for further details.',
- duration: 10000
- });
- log({ category: 'deploy-error', message: error.problems || error.message });
+ log({
+ category: 'deploy-error',
+ message: error.problems || error.details || error.message
+ });
+ }
+
+ async saveConfiguration(tab, configuration) {
+
+ const {
+ endpoint,
+ deployment
+ } = configuration;
+
+ await this.saveEndpoint(endpoint);
+
+ const tabConfiguration = {
+ deployment,
+ endpointId: endpoint.id
+ };
+
+ await this.setTabConfiguration(tab, tabConfiguration);
+
+ return configuration;
+ }
+
+ async saveEndpoint(endpoint) {
+
+ const {
+ id,
+ url,
+ authType,
+ rememberCredentials,
+ username,
+ password,
+ token
+ } = endpoint;
+
+ const authConfiguration =
+ authType === AuthTypes.none
+ ? {}
+ : authType === AuthTypes.basic
+ ? {
+ username,
+ password: rememberCredentials ? password : ''
+ }
+ : {
+ token: rememberCredentials ? token : ''
+ };
+
+ const endpointConfiguration = {
+ id,
+ url,
+ authType,
+ rememberCredentials,
+ ...authConfiguration
+ };
+
+ const existingEndpoints = await this.getEndpoints();
+
+ const updatedEndpoints = addOrUpdateById(existingEndpoints, endpointConfiguration);
+
+ await this.setEndpoints(updatedEndpoints);
+
+ return endpointConfiguration;
+ }
+
+ async getSavedConfiguration(tab) {
+
+ const tabConfig = await this.getTabConfiguration(tab);
+
+ if (!tabConfig) {
+ return undefined;
}
+
+ const {
+ deployment,
+ endpointId
+ } = tabConfig;
+
+ const endpoints = await this.getEndpoints();
+
+ return {
+ deployment,
+ endpoint: endpoints.find(endpoint => endpoint.id === endpointId)
+ };
}
- deployWithDetails(tab, details) {
- const api = new CamundaAPI(details.endpointUrl);
+ deployWithConfiguration(tab, configuration) {
+
+ const {
+ endpoint,
+ deployment
+ } = configuration;
+
+ const api = new CamundaAPI(endpoint);
- return api.deployDiagram(tab.file, details);
+ return api.deployDiagram(tab.file, deployment);
}
- canDeployWithDetails(details) {
+ canDeployWithConfiguration(configuration) {
- // TODO(barmac): implement for instant deployment
+ // TODO(nikku): we'll re-enable this, once we make re-deploy
+ // the primary button action: https://github.com/camunda/camunda-modeler/issues/1440
return false;
+
+ // return this.validator.isConfigurationValid(configuration);
}
- getDetailsFromUserInput(tab, details) {
- const initialDetails = this.getInitialDetails(tab, details);
+ async getConfigurationFromUserInput(tab, providedConfiguration) {
+ const configuration = await this.getDefaultConfiguration(tab, providedConfiguration);
return new Promise(resolve => {
- const handleClose = result => {
+ const handleClose = (action, configuration) => {
this.setState({
modalState: null
});
- this.updateMenu();
-
- // contract: if details provided, user closed with O.K.
+ // contract: if configuration provided, user closed with O.K.
// otherwise they canceled it
- if (result) {
- return resolve(this.getDetailsFromForm(result));
- }
-
- resolve();
+ return resolve({ action, configuration });
};
this.setState({
modalState: {
tab,
- details: initialDetails,
+ configuration,
handleClose
}
});
});
}
- getDetailsToSave(rawDetails) {
- return omit(rawDetails, 'auth');
+ getEndpoints() {
+ return this.props.config.get(ENGINE_ENDPOINTS_CONFIG_KEY, []);
}
- validateDetails = values => {
- const validatedFields = this.getValidatedFields(values);
-
- const errors = validatedFields.reduce((currentErrors, field) => {
- const error = validators[field] && validators[field](values[field]);
-
- return error ? { ...currentErrors, [field]: error } : currentErrors;
- }, {});
-
- return errors;
+ setEndpoints(endpoints) {
+ return this.props.config.set(ENGINE_ENDPOINTS_CONFIG_KEY, endpoints);
}
- checkConnection = async values => {
- const baseUrl = this.getBaseUrl(values.endpointUrl);
- const auth = this.getAuth(values);
-
- const api = new CamundaAPI(baseUrl);
-
- let connectionError = null;
-
- try {
- await api.checkConnection({ auth });
- } catch (error) {
- connectionError = error.message;
- }
-
- return connectionError;
+ getTabConfiguration(tab) {
+ return this.props.config.getForFile(tab.file, DEPLOYMENT_DETAILS_CONFIG_KEY);
}
- getInitialDetails(tab, providedDetails) {
- const details = { ...providedDetails };
-
- if (!details.deploymentName) {
- details.deploymentName = withoutExtension(tab.name);
- }
-
- return details;
+ setTabConfiguration(tab, configuration) {
+ return this.props.config.setForFile(tab.file, DEPLOYMENT_DETAILS_CONFIG_KEY, configuration);
}
- getValidatedFields(values) {
- switch (values.authType) {
- case AuthTypes.none:
- return VALIDATED_FIELDS;
- case AuthTypes.bearer:
- return VALIDATED_FIELDS.concat('bearer');
- case AuthTypes.basic:
- return VALIDATED_FIELDS.concat('username', 'password');
- }
- }
+ /**
+ * Get endpoint to be used by the current tab.
+ *
+ * @return {EndpointConfig}
+ */
+ async getDefaultEndpoint(tab, providedEndpoint) {
- getDetailsFromForm(values) {
- const endpointUrl = this.getBaseUrl(values.endpointUrl);
+ let endpoint = {};
- const payload = {
- endpointUrl,
- deploymentName: values.deploymentName,
- tenantId: values.tenantId,
- authType: values.authType
- };
+ if (providedEndpoint) {
+ endpoint = providedEndpoint;
+ } else {
- const auth = this.getAuth(values);
+ const existingEndpoints = await this.getEndpoints();
- if (auth) {
- payload.auth = auth;
+ if (existingEndpoints.length) {
+ endpoint = existingEndpoints[0];
+ }
}
- return payload;
- }
-
- /**
- * Extract base url in case `/deployment/create` was added at the end.
- * @param {string} url
- */
- getBaseUrl(url) {
- return url.replace(/\/deployment\/create\/?/, '');
- }
-
- getAuth({ authType, username, password, bearer }) {
- switch (authType) {
- case AuthTypes.basic:
- return {
- username,
- password
- };
- case AuthTypes.bearer: {
- return {
- bearer
- };
- }
- }
+ return {
+ ...DEFAULT_ENDPOINT,
+ ...endpoint,
+ id: endpoint.id || generateId()
+ };
}
- handleFocusChange = event => {
- const editMenu = getEditMenu(isFocusedOnInput(event));
+ async getDefaultConfiguration(tab, providedConfiguration = {}) {
+ const endpoint = await this.getDefaultEndpoint(tab, providedConfiguration.endpoint);
- this.updateMenu({ editMenu });
- }
+ const deployment = providedConfiguration.deployment || {};
- updateMenu(menu) {
- this.props.triggerAction('update-menu', menu);
+ return {
+ endpoint,
+ deployment: {
+ name: withoutExtension(tab.name),
+ ...deployment
+ }
+ };
}
render() {
@@ -294,7 +345,7 @@ export default class DeploymentTool extends PureComponent {
@@ -302,12 +353,10 @@ export default class DeploymentTool extends PureComponent {
{ modalState &&
}
;
}
@@ -317,10 +366,25 @@ export default class DeploymentTool extends PureComponent {
// helpers //////////
-function isFocusedOnInput(event) {
- return event.type === 'focus' && ['INPUT', 'TEXTAREA'].includes(event.target.tagName);
-}
function withoutExtension(name) {
return name.replace(/\.[^.]+$/, '');
}
+
+function addOrUpdateById(collection, element) {
+
+ const index = collection.findIndex(el => el.id === element.id);
+
+ if (index !== -1) {
+ return [
+ ...collection.slice(0, index),
+ element,
+ ...collection.slice(index + 1)
+ ];
+ }
+
+ return [
+ ...collection,
+ element
+ ];
+}
diff --git a/client/src/plugins/deployment-tool/__tests__/CamundaAPISpec.js b/client/src/plugins/deployment-tool/__tests__/CamundaAPISpec.js
index c107ec5e11..ddda8278bc 100644
--- a/client/src/plugins/deployment-tool/__tests__/CamundaAPISpec.js
+++ b/client/src/plugins/deployment-tool/__tests__/CamundaAPISpec.js
@@ -30,7 +30,7 @@ describe('', () => {
beforeEach(() => {
fetchStub = sinon.stub(window, 'fetch');
- api = new CamundaAPI(baseUrl);
+ api = new CamundaAPI({ url: baseUrl });
});
afterEach(() => {
diff --git a/client/src/plugins/deployment-tool/__tests__/DeploymentConfigValidatorSpec.js b/client/src/plugins/deployment-tool/__tests__/DeploymentConfigValidatorSpec.js
new file mode 100644
index 0000000000..6ca04fb939
--- /dev/null
+++ b/client/src/plugins/deployment-tool/__tests__/DeploymentConfigValidatorSpec.js
@@ -0,0 +1,108 @@
+/**
+ * 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 DeploymentConfigValidator from '../DeploymentConfigValidator';
+import AuthTypes from '../AuthTypes';
+
+const EMPTY_ENDPOINT_ERROR = 'Endpoint URL must not be empty.';
+const EMPTY_DEPLOYMENT_NAME_ERROR = 'Deployment name must not be empty.';
+const EMPTY_USERNAME_ERROR = 'Username must not be empty.';
+const EMPTY_PASSWORD_ERROR = 'Password must not be empty.';
+const EMPTY_TOKEN_ERROR = 'Token must not be empty.';
+const INVALID_URL_ERROR = 'Endpoint URL must start with "http://" or "https://".';
+
+
+describe('', () => {
+
+ /**
+ * @type {DeploymentConfigValidator}
+ */
+ let validator;
+
+ beforeEach(() => {
+ validator = new DeploymentConfigValidator();
+ });
+
+
+ it('should validate deployment name', () => {
+
+ // given
+ const validate = name => validator.validateDeployment({
+ name
+ });
+
+ // then
+ expect(validate().name).to.eql(EMPTY_DEPLOYMENT_NAME_ERROR);
+ expect(validate('').name).to.eql(EMPTY_DEPLOYMENT_NAME_ERROR);
+ expect(validate('deployment name').name).to.not.exist;
+ });
+
+
+ it('should validate endpoint url', () => {
+
+ // given
+ const validate = url => validator.validateEndpoint({
+ authType: AuthTypes.none,
+ url
+ });
+
+ // then
+ expect(validate().url).to.eql(EMPTY_ENDPOINT_ERROR);
+ expect(validate('').url).to.eql(EMPTY_ENDPOINT_ERROR);
+ expect(validate('url').url).to.eql(INVALID_URL_ERROR);
+ expect(validate('http://localhost:8080').url).to.not.exist;
+ expect(validate('https://localhost:8080').url).to.not.exist;
+ });
+
+
+ it('should validate username', () => {
+
+ // given
+ const validate = username => validator.validateEndpoint({
+ authType: AuthTypes.basic,
+ username
+ });
+
+ // then
+ expect(validate().username).to.eql(EMPTY_USERNAME_ERROR);
+ expect(validate('').username).to.eql(EMPTY_USERNAME_ERROR);
+ expect(validate('username').username).to.not.exist;
+ });
+
+
+ it('should validate password', () => {
+
+ // given
+ const validate = password => validator.validateEndpoint({
+ authType: AuthTypes.basic,
+ password
+ });
+
+ // then
+ expect(validate().password).to.eql(EMPTY_PASSWORD_ERROR);
+ expect(validate('').password).to.eql(EMPTY_PASSWORD_ERROR);
+ expect(validate('password').password).to.not.exist;
+ });
+
+
+ it('should validate token', () => {
+
+ // given
+ const validate = token => validator.validateEndpoint({
+ authType: AuthTypes.bearer,
+ token
+ });
+
+ // then
+ expect(validate().token).to.eql(EMPTY_TOKEN_ERROR);
+ expect(validate('').token).to.eql(EMPTY_TOKEN_ERROR);
+ expect(validate('token').token).to.not.exist;
+ });
+});
diff --git a/client/src/plugins/deployment-tool/__tests__/DeploymentDetailsModalSpec.js b/client/src/plugins/deployment-tool/__tests__/DeploymentDetailsModalSpec.js
index 1d77328235..6f39f53c33 100644
--- a/client/src/plugins/deployment-tool/__tests__/DeploymentDetailsModalSpec.js
+++ b/client/src/plugins/deployment-tool/__tests__/DeploymentDetailsModalSpec.js
@@ -12,6 +12,8 @@
import React from 'react';
+import pDefer from 'p-defer';
+
import {
mount,
shallow
@@ -19,7 +21,10 @@ import {
import AuthTypes from '../AuthTypes';
import DeploymentDetailsModal from '../DeploymentDetailsModal';
+import DeploymentConfigValidator from '../DeploymentConfigValidator';
+
+let mounted;
describe('', () => {
@@ -30,8 +35,6 @@ describe('', () => {
describe('connection check', () => {
- let mounted = null;
-
afterEach(() => {
if (mounted && mounted.exists()) {
mounted.unmount();
@@ -43,89 +46,96 @@ describe('', () => {
it('should run connection check on mount with provided defaults', () => {
// given
- const checkConnectionStub = sinon.stub().resolves();
-
- const initialFormValues = {
- endpointUrl: 'http://localhost:8088/engine-rest',
- tenantId: '',
- deploymentName: 'diagram',
- authType: AuthTypes.basic,
- username: 'demo',
- password: 'demo',
- bearer: ''
+ const configuration = {
+ deployment: {
+ name: 'diagram',
+ tenantId: ''
+ },
+ endpoint: {
+ url: 'http://localhost:8088/engine-rest',
+ authType: AuthTypes.basic,
+ username: 'demo',
+ password: 'demo'
+ }
};
+ const connectionChecker = new MockConnectionChecker();
+
// when
createModal({
- checkConnection: checkConnectionStub,
- details: initialFormValues
- });
+ connectionChecker,
+ configuration
+ }, mount);
// then
- expect(checkConnectionStub).to.have.been.calledOnce;
- expect(checkConnectionStub.args[0][0]).to.eql(initialFormValues);
+ expect(connectionChecker.check).to.have.been.calledOnce;
+ expect(connectionChecker.check).to.have.been.calledWith(configuration.endpoint);
});
- it('should display hint if the username and password are missing', () => {
+ it('should display hint if the username and password are missing', async () => {
// given
- const checkConnectionStub = sinon.stub().resolves();
-
- const initialFormValues = {
- endpointUrl: 'http://localhost:8088/engine-rest',
- tenantId: '',
- deploymentName: 'diagram',
- authType: AuthTypes.basic
+ const configuration = {
+ deployment: {
+ tenantId: '',
+ name: 'diagram'
+ },
+ endpoint: {
+ url: 'http://localhost:8088/engine-rest',
+ authType: AuthTypes.basic
+ }
};
- // when
- const { wrapper } = createModal({
- checkConnection: checkConnectionStub,
- details: initialFormValues,
- validate: () => ({ username: 'username is missing', password: 'password is missing' })
+ const connectionChecker = new MockConnectionChecker();
+
+ const {
+ wrapper
+ } = createModal({
+ connectionChecker,
+ configuration
}, mount);
- mounted = wrapper;
+ // when
+ await connectionChecker.triggerComplete({});
- // then
- const connectionCheckResult = wrapper.find('ConnectionCheckResult').first();
- const hint = connectionCheckResult.prop('hint');
+ wrapper.update();
- expect(checkConnectionStub).to.not.have.been.called;
- expect(hint).to.exist;
- expect(connectionCheckResult.contains(hint), 'Does not display the hint').to.be.true;
+ // then
+ expect(wrapper.find('.hint.error')).to.have.length(2);
});
- it('should display hint if token is missing', () => {
+ it('should display hint if token is missing', async () => {
// given
- const checkConnectionStub = sinon.stub().resolves();
-
- const initialFormValues = {
- endpointUrl: 'http://localhost:8088/engine-rest',
- tenantId: '',
- deploymentName: 'diagram',
- authType: AuthTypes.bearer
+ const configuration = {
+ deployment: {
+ tenantId: '',
+ name: 'diagram'
+ },
+ endpoint: {
+ url: 'http://localhost:8088/engine-rest',
+ authType: AuthTypes.bearer
+ }
};
- // when
- const { wrapper } = createModal({
- checkConnection: checkConnectionStub,
- details: initialFormValues,
- validate: () => ({ bearer: 'token is missing' })
+ const connectionChecker = new MockConnectionChecker();
+
+ const {
+ wrapper
+ } = createModal({
+ connectionChecker,
+ configuration
}, mount);
- mounted = wrapper;
+ // when
+ await connectionChecker.triggerComplete({});
+
+ wrapper.update();
// then
- const connectionCheckResult = wrapper.find('ConnectionCheckResult').first();
- const hint = connectionCheckResult.prop('hint');
-
- expect(checkConnectionStub).to.not.have.been.called;
- expect(hint).to.exist;
- expect(connectionCheckResult.contains(hint), 'Does not display the hint').to.be.true;
+ expect(wrapper.find('.hint.error')).to.have.length(1);
});
});
@@ -135,14 +145,29 @@ describe('', () => {
// helpers //////////
-function createModal(props, renderFn = shallow) {
- props = {
- checkConnection: noop,
- validate: () => ({}),
- ...props
- };
- const wrapper = renderFn( );
+function createModal(props={}, renderFn = shallow) {
+
+ const {
+ configuration,
+ onClose,
+ connectionChecker,
+ ...apiOverrides
+ } = props;
+
+ const validator = new MockValidator(
+ connectionChecker || new MockConnectionChecker(), apiOverrides
+ );
+
+ const wrapper = renderFn(
+
+ );
+
+ mounted = wrapper;
return {
wrapper,
@@ -151,3 +176,78 @@ function createModal(props, renderFn = shallow) {
}
function noop() {}
+
+function getDefaultConfiguration() {
+ return {
+ deployment: {
+ name: 'diagram',
+ tenantId: ''
+ },
+ endpoint: {
+ url: 'http://localhost:8080/engine-rest',
+ authType: AuthTypes.none
+ }
+ };
+}
+
+class MockConnectionChecker {
+
+ constructor() {
+ sinon.spy(this, 'check');
+ }
+
+ subscribe(hooks) {
+ this.hooks = hooks;
+ }
+
+ unsubscribe() {
+ this.hooks = null;
+ }
+
+ check(endpoint) {
+
+ this.deferred = pDefer();
+
+ return this.deferred.promise.then(result => {
+
+ this.hooks && this.hooks.onComplete(result);
+
+ return result;
+ });
+ }
+
+ triggerStart() {
+ this.hooks && this.hooks.onStart();
+
+ return new Promise(resolve => {
+ setTimeout(resolve, 5);
+ });
+ }
+
+ triggerComplete(result) {
+
+ this.deferred && this.deferred.resolve(result);
+
+ return new Promise(resolve => {
+ setTimeout(resolve, 5);
+ });
+ }
+
+}
+
+class MockValidator extends DeploymentConfigValidator {
+
+ constructor(connectionChecker, apiStubs) {
+ super();
+
+ Object.assign(this, {
+ connectionChecker,
+ ...apiStubs
+ });
+ }
+
+ createConnectionChecker() {
+ return this.connectionChecker;
+ }
+
+}
diff --git a/client/src/plugins/deployment-tool/__tests__/DeploymentToolSpec.js b/client/src/plugins/deployment-tool/__tests__/DeploymentToolSpec.js
index 524439ea3c..803976df15 100644
--- a/client/src/plugins/deployment-tool/__tests__/DeploymentToolSpec.js
+++ b/client/src/plugins/deployment-tool/__tests__/DeploymentToolSpec.js
@@ -12,15 +12,19 @@
import React from 'react';
+import { shallow } from 'enzyme';
import {
- mount,
- shallow
-} from 'enzyme';
-import { omit } from 'min-dash';
+ omit
+} from 'min-dash';
+
+import { Config } from './../../../app/__tests__/mocks';
-import AuthTypes from '../AuthTypes';
-import DeploymentDetailsModal from '../DeploymentDetailsModal';
import DeploymentTool from '../DeploymentTool';
+import AuthTypes from '../AuthTypes';
+
+
+const CONFIG_KEY = 'deployment-tool';
+const ENGINE_ENDPOINTS_CONFIG_KEY = 'camundaEngineEndpoints';
describe('', () => {
@@ -30,118 +34,307 @@ describe('', () => {
});
- describe('#deploy', () => {
+ describe('deploy', () => {
- let fetchStub,
- mounted;
+ it('should derive deployment name from filename', async () => {
- beforeEach(() => {
- fetchStub = sinon.stub(window, 'fetch').resolves({ ok: true, json: () => ({}) });
- });
+ // given
+ const deploySpy = sinon.spy();
+ const activeTab = createTab({ name: 'foo.bpmn' });
+ const {
+ instance
+ } = createDeploymentTool({ activeTab, deploySpy });
- afterEach(() => {
- fetchStub.restore();
+ // when
+ await instance.deploy();
- if (mounted && mounted.exists()) {
- mounted.unmount();
- }
+ // then
+ expect(deploySpy).to.have.been.calledOnce;
+ expect(deploySpy.args[0][1].deployment).to.have.property('name', 'foo');
});
- it('should derive deployment name from filename', async () => {
+ it('should use saved config for deployed file', async () => {
// given
+ const savedEndpoint = {
+ id: 'endpointId',
+ authType: AuthTypes.basic,
+ username: 'demo',
+ password: 'demo',
+ url: 'http://localhost:8088/engine-rest',
+ rememberCredentials: true
+ };
+
+ const savedConfiguration = {
+ deployment: {
+ name: 'diagram',
+ tenantId: '',
+ },
+ endpointId: savedEndpoint.id
+ };
+
+ const config = {
+ get: (key, defaultValue) => {
+ if (key === ENGINE_ENDPOINTS_CONFIG_KEY) {
+ return [
+ { id: 'OTHER_ENDPOINT' },
+ savedEndpoint
+ ];
+ }
+
+ return defaultValue;
+ },
+ getForFile: sinon.stub().returns(savedConfiguration)
+ };
+
+ const deploySpy = sinon.spy();
+
const activeTab = createTab({ name: 'foo.bpmn' });
const {
- instance,
- wrapper
- } = createDeploymentTool({ activeTab }, mount);
-
- mounted = wrapper;
+ instance
+ } = createDeploymentTool({ activeTab, config, deploySpy });
// when
- instance.deploy();
+ await instance.deployTab(activeTab);
- await nextTick();
+ // then
+ expect(deploySpy).to.have.been.calledOnce;
+ expect(deploySpy.args[0]).to.eql([
+ activeTab,
+ {
+ deployment: savedConfiguration.deployment,
+ endpoint: savedEndpoint
+ }
+ ]);
+ });
- wrapper.update();
- // then
- const modal = wrapper.find(DeploymentDetailsModal).first();
+ it('should read and save config for deployed file', async () => {
- const onClose = modal.prop('onClose');
+ // given
+ const config = {
+ getForFile: sinon.spy(),
+ setForFile: sinon.spy()
+ };
- const deploymentName = modal.find('input[name="deploymentName"]').first().getDOMNode().value;
+ const configuration = createConfiguration();
- expect(deploymentName).to.eql('foo');
+ const activeTab = createTab({ name: 'foo.bpmn' });
+
+ const {
+ instance
+ } = createDeploymentTool({ activeTab, config, ...configuration });
+
+ // when
+ await instance.deploy();
+
+ // then
+ expect(config.getForFile).to.have.been.calledOnce;
+ expect(config.getForFile.args[0]).to.eql([
+ activeTab.file,
+ CONFIG_KEY
+ ]);
- onClose();
+ expect(config.setForFile).to.have.been.calledOnce;
+ expect(config.setForFile.args[0]).to.eql([
+ activeTab.file,
+ CONFIG_KEY,
+ { ...omit(configuration, [ 'endpoint' ]), endpointId: configuration.endpoint.id }
+ ]);
});
- it('should read and save config for deployed file', async () => {
+ it('should save credentials', async () => {
// given
- const config = sinon.stub({
- getForFile() {
- return {};
- },
- setForFile() {}
- });
-
- const details = {
- endpointUrl: 'http://localhost:8088/engine-rest',
- tenantId: '',
- deploymentName: 'diagram',
- authType: AuthTypes.basic,
- username: 'demo',
- password: 'demo'
+ const config = {
+ set: sinon.spy()
};
+ const configuration = createConfiguration();
+
const activeTab = createTab({ name: 'foo.bpmn' });
const {
- instance,
- wrapper
- } = createDeploymentTool({ activeTab, config }, mount);
+ instance
+ } = createDeploymentTool({ activeTab, config, ...configuration });
+
+ // when
+ await instance.deploy();
+
+ // then
+ expect(config.set).to.have.been.calledOnce;
+ expect(config.set.args[0]).to.eql([
+ ENGINE_ENDPOINTS_CONFIG_KEY,
+ [ configuration.endpoint ]
+ ]);
+ });
+
+
+ it('should not save credentials if `rememberCredentials` was set to false', async () => {
- mounted = wrapper;
+ // given
+ const config = {
+ set: sinon.spy()
+ };
+
+ const configuration = createConfiguration(null, {
+ rememberCredentials: false
+ });
+
+ const activeTab = createTab({ name: 'foo.bpmn' });
+
+ const {
+ instance
+ } = createDeploymentTool({ activeTab, config, ...configuration });
// when
- instance.deploy();
+ await instance.deploy();
+
+ // then
+ expect(config.set).to.have.been.calledOnce;
+ expect(config.set.args[0]).to.eql([
+ ENGINE_ENDPOINTS_CONFIG_KEY,
+ [ { ...configuration.endpoint, password: '' } ]
+ ]);
+ });
+ });
+
- await nextTick();
+ describe('save', () => {
- wrapper.update();
+ it('should save configuration when user decided to only save it', async () => {
+
+ // given
+ const configuration = createConfiguration();
+ const activeTab = createTab({ name: 'foo.bpmn' });
- const { handleClose } = wrapper.state('modalState');
+ const config = {
+ set: sinon.spy(),
+ setForFile: sinon.spy()
+ };
- handleClose(details);
+ const {
+ instance
+ } = createDeploymentTool({ config, userAction: 'save', ...configuration });
- await nextTick();
+ // when
+ await instance.deploy();
// then
- expect(config.getForFile).to.have.been.calledOnce;
- expect(config.getForFile.getCall(0).args).to.eql([
- activeTab.file,
- 'deployment-config'
+ expect(config.set).to.have.been.calledOnce;
+ expect(config.set.args[0]).to.eql([
+ ENGINE_ENDPOINTS_CONFIG_KEY,
+ [ configuration.endpoint ]
]);
expect(config.setForFile).to.have.been.calledOnce;
- expect(config.setForFile.getCall(0).args).to.eql([
+ expect(config.setForFile.args[0]).to.eql([
activeTab.file,
- 'deployment-config',
- omit(details, [ 'username', 'password' ])
+ CONFIG_KEY,
+ { ...omit(configuration, [ 'endpoint' ]), endpointId: configuration.endpoint.id }
]);
});
+
+ it('should not deploy when user decided to only save configuration', async () => {
+
+ // given
+ const deploySpy = sinon.spy();
+ const {
+ instance
+ } = createDeploymentTool({ userAction: 'save' });
+
+ // when
+ await instance.deploy();
+
+ // then
+ expect(deploySpy).to.have.not.been.called;
+ });
+ });
+
+
+ describe('cancel', () => {
+
+ it('should not save config if user cancelled the deployment', async () => {
+
+ // given
+ const config = {
+ set: sinon.spy(),
+ setForFile: sinon.spy()
+ };
+
+ const activeTab = createTab({ name: 'foo.bpmn' });
+ const {
+ instance
+ } = createDeploymentTool({ activeTab, config, userAction: 'cancel' });
+
+ // when
+ await instance.deploy();
+
+ // then
+ expect(config.setForFile).to.not.have.been.called;
+ expect(config.set).to.not.have.been.called;
+ });
});
});
-// helpers //////////
+// helper ////
+class TestDeploymentTool extends DeploymentTool {
+
+ /**
+ * @param {object} props
+ * @param {'cancel'|'save'|'deploy'} [props.userAction='deploy'] user action in configuration modal
+ * @param {object} [props.endpoint] overrides for endpoint configuration
+ * @param {object} [props.deployment] overrides for deployment configuration
+ */
+ constructor(props) {
+ super(props);
+ }
+
+ // removes CamundaAPI dependency
+ deployWithConfiguration(...args) {
+ this.props.deploySpy && this.props.deploySpy(...args);
+ }
+
+ checkConnection = (...args) => {
+ this.props.checkConnectionSpy && this.props.checkConnectionSpy(...args);
+ }
+
+ // closes automatically when modal is opened
+ componentDidUpdate(...args) {
+ super.componentDidUpdate && super.componentDidUpdate(...args);
+
+ const { modalState } = this.state;
+ const {
+ userAction,
+ endpoint,
+ deployment
+ } = this.props;
+
+ if (modalState) {
+ const action = userAction || 'deploy';
+
+ const configuration = action !== 'cancel' && {
+ endpoint: {
+ ...modalState.configuration.endpoint,
+ ...endpoint
+ },
+ deployment: {
+ ...modalState.configuration.deployment,
+ ...deployment
+ }
+ };
+
+ modalState.handleClose(action, configuration);
+ }
+ }
+}
+
function createDeploymentTool({
activeTab = createTab(),
...props
@@ -157,20 +350,18 @@ function createDeploymentTool({
}
};
- const config = {
- getForFile() {
- return {};
- },
- setForFile() {}
- };
+ const config = new Config({
+ get: (_, defaultValue) => defaultValue,
+ ...props.config
+ });
- const wrapper = render( );
return {
@@ -194,8 +385,23 @@ function createTab(overrides = {}) {
};
}
-function nextTick() {
- return new Promise(resolve => process.nextTick(() => resolve()));
+function createConfiguration(deployment, endpoint) {
+ return {
+ deployment: {
+ name: 'diagram',
+ tenantId: '',
+ ...deployment
+ },
+ endpoint: {
+ id: 'endpointId',
+ url: 'http://localhost:8088/engine-rest',
+ authType: AuthTypes.basic,
+ username: 'demo',
+ password: 'demo',
+ rememberCredentials: true,
+ ...endpoint
+ }
+ };
}
function noop() {}
diff --git a/client/src/plugins/deployment-tool/__tests__/validatorsSpec.js b/client/src/plugins/deployment-tool/__tests__/validatorsSpec.js
deleted file mode 100644
index 3061919e83..0000000000
--- a/client/src/plugins/deployment-tool/__tests__/validatorsSpec.js
+++ /dev/null
@@ -1,76 +0,0 @@
-/**
- * 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 validators from '../validators';
-
-
-describe('', () => {
-
- it('should validate endpoint url', () => {
-
- // given
- const validate = validators.endpointUrl;
-
- // then
- expect(validate()).to.exist;
- expect(validate('')).to.exist;
- expect(validate('ftp://url')).to.exist;
- expect(validate('http://localhost:8080')).to.not.exist;
- expect(validate('https://localhost:8080')).to.not.exist;
- });
-
-
- it('should validate deployment name', () => {
-
- // given
- const validate = validators.deploymentName;
-
- // then
- expect(validate()).to.exist;
- expect(validate('')).to.exist;
- expect(validate('deployment name')).to.not.exist;
- });
-
-
- it('should validate username', () => {
-
- // given
- const validate = validators.username;
-
- // then
- expect(validate()).to.exist;
- expect(validate('')).to.exist;
- expect(validate('username')).to.not.exist;
- });
-
-
- it('should validate password', () => {
-
- // given
- const validate = validators.password;
-
- // then
- expect(validate()).to.exist;
- expect(validate('')).to.exist;
- expect(validate('password')).to.not.exist;
- });
-
-
- it('should validate bearer token', () => {
-
- // given
- const validate = validators.bearer;
-
- // then
- expect(validate()).to.exist;
- expect(validate('')).to.exist;
- expect(validate('token')).to.not.exist;
- });
-});
diff --git a/client/src/plugins/deployment-tool/components/AuthBasic.js b/client/src/plugins/deployment-tool/components/AuthBasic.js
deleted file mode 100644
index 0df968e417..0000000000
--- a/client/src/plugins/deployment-tool/components/AuthBasic.js
+++ /dev/null
@@ -1,40 +0,0 @@
-/**
- * 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 { Field } from 'formik';
-
-import FormControl from './FormControl';
-
-
-export default function AuthBasic({ onFocusChange, ...props }) {
- return (
-
-
-
-
-
- );
-}
\ No newline at end of file
diff --git a/client/src/plugins/deployment-tool/components/CheckBox.js b/client/src/plugins/deployment-tool/components/CheckBox.js
new file mode 100644
index 0000000000..885f23456c
--- /dev/null
+++ b/client/src/plugins/deployment-tool/components/CheckBox.js
@@ -0,0 +1,64 @@
+/**
+ * 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 classNames from 'classnames';
+
+import FormFeedback from './FormFeedback';
+
+import {
+ fieldError as defaultFieldError
+} from './Util';
+
+export default function CheckBox(props) {
+
+ const {
+ hint,
+ label,
+ field,
+ form,
+ fieldError,
+ ...restProps
+ } = props;
+
+ const {
+ name: fieldName
+ } = field;
+
+ const meta = form.getFieldMeta(fieldName);
+
+ const error = (fieldError || defaultFieldError)(meta);
+
+ return (
+
+
+
+
+
+ { label }
+
+
+
+
+
+ );
+}
diff --git a/client/src/plugins/deployment-tool/components/FormControl.js b/client/src/plugins/deployment-tool/components/FormControl.js
deleted file mode 100644
index 4e110afa21..0000000000
--- a/client/src/plugins/deployment-tool/components/FormControl.js
+++ /dev/null
@@ -1,66 +0,0 @@
-/**
- * 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 classnames from 'classnames';
-
-
-export default function FormControl({
- field,
- hint,
- label,
- onFocusChange,
- validated,
- form: { touched, errors, isSubmitting },
- ...props
-}) {
- const { name } = field;
-
- const invalid = errors[name] && touched[name];
-
- return (
-
-
- { label }
-
-
-
-
-
- { invalid ? (
-
{errors[name]}
- ) : null}
-
- { hint ? (
-
{ hint }
- ) : null }
-
-
- );
-}
-
-
-
-// helpers //////
-function compose(...handlers) {
- return function(...args) {
- handlers.forEach(handler => handler(...args));
- };
-}
diff --git a/client/src/plugins/deployment-tool/components/AuthBearer.js b/client/src/plugins/deployment-tool/components/FormFeedback.js
similarity index 58%
rename from client/src/plugins/deployment-tool/components/AuthBearer.js
rename to client/src/plugins/deployment-tool/components/FormFeedback.js
index 7fad395956..e555c34b18 100644
--- a/client/src/plugins/deployment-tool/components/AuthBearer.js
+++ b/client/src/plugins/deployment-tool/components/FormFeedback.js
@@ -9,20 +9,20 @@
*/
import React from 'react';
-import { Field } from 'formik';
-import FormControl from './FormControl';
+export default function FormFeedback(props) {
+
+ const {
+ error,
+ hint
+ } = props;
-export default function AuthBearer({ onFocusChange, ...props }) {
return (
-
+
+ { error && { error }
}
+
+ { hint && { hint }
}
+
);
}
diff --git a/client/src/plugins/deployment-tool/components/Select.js b/client/src/plugins/deployment-tool/components/Select.js
new file mode 100644
index 0000000000..08096ebaac
--- /dev/null
+++ b/client/src/plugins/deployment-tool/components/Select.js
@@ -0,0 +1,68 @@
+/**
+ * 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 classNames from 'classnames';
+
+import FormFeedback from './FormFeedback';
+
+import {
+ fieldError as defaultFieldError
+} from './Util';
+
+
+export default function Select(props) {
+
+ const {
+ hint,
+ label,
+ field,
+ form,
+ fieldError,
+ children,
+ ...restProps
+ } = props;
+
+ const {
+ name: fieldName
+ } = field;
+
+ const meta = form.getFieldMeta(fieldName);
+
+ const error = (fieldError || defaultFieldError)(meta);
+
+ return (
+
+
+ { label }
+
+
+
+
+ { children }
+
+
+
+
+
+ );
+}
diff --git a/client/src/plugins/deployment-tool/components/TextInput.js b/client/src/plugins/deployment-tool/components/TextInput.js
new file mode 100644
index 0000000000..e1f12a7bfe
--- /dev/null
+++ b/client/src/plugins/deployment-tool/components/TextInput.js
@@ -0,0 +1,72 @@
+/**
+ * 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 classNames from 'classnames';
+
+import FormFeedback from './FormFeedback';
+
+import {
+ fieldError as defaultFieldError
+} from './Util';
+
+
+export default function TextInput(props) {
+
+ const {
+ hint,
+ label,
+ field,
+ form,
+ fieldError,
+ children,
+ ...restProps
+ } = props;
+
+ const {
+ name: fieldName,
+ value: fieldValue
+ } = field;
+
+ const meta = form.getFieldMeta(fieldName);
+
+ const error = (fieldError || defaultFieldError)(meta);
+
+ const invalid = error;
+ const valid = !error && meta.touched;
+
+ return (
+
+
+ { label }
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/client/src/plugins/deployment-tool/errors/index.js b/client/src/plugins/deployment-tool/components/Util.js
similarity index 64%
rename from client/src/plugins/deployment-tool/errors/index.js
rename to client/src/plugins/deployment-tool/components/Util.js
index d1ba7d0a0f..9fcb1b1d51 100644
--- a/client/src/plugins/deployment-tool/errors/index.js
+++ b/client/src/plugins/deployment-tool/components/Util.js
@@ -8,11 +8,6 @@
* except in compliance with the MIT License.
*/
-import ConnectionError, { ConnectionErrorMessages } from './ConnectionError';
-import DeploymentError from './DeploymentError';
-
-export {
- ConnectionError,
- ConnectionErrorMessages,
- DeploymentError
-};
+export function fieldError(meta) {
+ return meta.touched && meta.error;
+}
diff --git a/client/src/plugins/deployment-tool/components/index.js b/client/src/plugins/deployment-tool/components/index.js
index 4b840d4b47..33c3db3b62 100644
--- a/client/src/plugins/deployment-tool/components/index.js
+++ b/client/src/plugins/deployment-tool/components/index.js
@@ -8,6 +8,6 @@
* except in compliance with the MIT License.
*/
-export { default as FormControl } from './FormControl';
-export { default as AuthBasic } from './AuthBasic';
-export { default as AuthBearer } from './AuthBearer';
+export { default as TextInput } from './TextInput';
+export { default as CheckBox } from './CheckBox';
+export { default as Select } from './Select';
diff --git a/client/src/plugins/deployment-tool/errors/ConnectionError.js b/client/src/plugins/deployment-tool/errors/ConnectionError.js
deleted file mode 100644
index 623e581d41..0000000000
--- a/client/src/plugins/deployment-tool/errors/ConnectionError.js
+++ /dev/null
@@ -1,54 +0,0 @@
-/**
- * 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.
- */
-
-export const ConnectionErrorMessages = {
- noInternetConnection: 'Could not establish a network connection. Most likely your machine is not online right now.',
- unableToConnect: 'Could not connect to the server. Did you run the engine?',
- unauthorized: 'Authentication failed. Please check your credentials.',
- forbidden: 'This user is not permitted to deploy. Please use different credentials or get this user enabled to deploy.',
- notFound: 'Could not find the Camunda endpoint. Please check the URL and make sure Camunda is running.',
- internalServerError: 'Camunda is reporting an error. Please check the server status.',
- unreachable: 'Camunda is reporting an error. Please check the server status.'
-};
-
-
-export default class ConnectionError extends Error {
- constructor(response) {
- super();
-
- this.message = (
- this.getStatusCodeErrorMessage(response) ||
- this.getNetworkErrorMessage(response)
- );
- }
-
- getStatusCodeErrorMessage(response) {
- switch (response.status) {
- case 401:
- return ConnectionErrorMessages.unauthorized;
- case 403:
- return ConnectionErrorMessages.forbidden;
- case 404:
- return ConnectionErrorMessages.notFound;
- case 500:
- return ConnectionErrorMessages.internalServerError;
- case 503:
- return ConnectionErrorMessages.unavailable;
- }
- }
-
- getNetworkErrorMessage(response) {
- if (!/^https?:\/\/localhost/.test(response.url) && !window.navigator.onLine) {
- return ConnectionErrorMessages.noInternetConnection;
- }
-
- return ConnectionErrorMessages.unableToConnect;
- }
-}
diff --git a/client/src/plugins/deployment-tool/errors/DeploymentError.js b/client/src/plugins/deployment-tool/errors/DeploymentError.js
deleted file mode 100644
index a4d22ed9c1..0000000000
--- a/client/src/plugins/deployment-tool/errors/DeploymentError.js
+++ /dev/null
@@ -1,70 +0,0 @@
-/**
- * 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.
- */
-
-const DeploymentErrorMessages = {
- noInternetConnection: 'Could not establish a network connection. Most likely your machine is not online right now.',
- unableToConnect: 'Could not connect to the server. Did you run the engine?',
- bpmnParsingError: 'Server could not parse the diagram. Please check log for errors.',
- unauthorized: 'The deployment was unauthorized. Please use valid credentials.',
- forbidden: 'The deployment was not permitted for your credentials. Please check your credentials.',
- notFound: 'Could not connect to Camunda. Please check the endpoint URL.',
- internalServerError: 'Camunda reported an unknown error. Please check the server status.',
- serverUnavailable: 'Camunda is currently unavailable. Please try again later.'
-};
-
-const PARSE_ERROR = 'ENGINE-09005 Could not parse BPMN process. Errors:';
-
-
-export default class DeploymentError extends Error {
- constructor(response, body) {
- super();
-
- this.message = (
- this.getCamundaBpmErrorMessage(body) ||
- this.getStatusCodeErrorMessage(response) ||
- this.getNetworkErrorMessage(response)
- );
-
- this.problems = this.getProblems(body);
- }
-
- getCamundaBpmErrorMessage(body) {
- if (body && body.message && body.message.startsWith(PARSE_ERROR)) {
- return DeploymentErrorMessages.bpmnParsingError;
- }
- }
-
- getStatusCodeErrorMessage(response) {
- switch (response.status) {
- case 401:
- return DeploymentErrorMessages.unauthorized;
- case 403:
- return DeploymentErrorMessages.forbidden;
- case 404:
- return DeploymentErrorMessages.notFound;
- case 500:
- return DeploymentErrorMessages.internalServerError;
- case 503:
- return DeploymentErrorMessages.serverUnavailable;
- }
- }
-
- getNetworkErrorMessage(response) {
- if (!/^https?:\/\/localhost/.test(response.url) && !window.navigator.onLine) {
- return DeploymentErrorMessages.noInternetConnection;
- }
-
- return DeploymentErrorMessages.unableToConnect;
- }
-
- getProblems(body) {
- return body.message;
- }
-}
diff --git a/client/src/plugins/deployment-tool/getEditMenu.js b/client/src/plugins/deployment-tool/getEditMenu.js
deleted file mode 100644
index 2a20173be1..0000000000
--- a/client/src/plugins/deployment-tool/getEditMenu.js
+++ /dev/null
@@ -1,42 +0,0 @@
-/**
- * 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.
- */
-
-export default function getEditMenu(enabled) {
- return [
- [
- {
- role: 'undo',
- enabled
- },
- {
- role: 'redo',
- enabled
- },
- ],
- [
- {
- role: 'copy',
- enabled
- },
- {
- role: 'cut',
- enabled
- },
- {
- role: 'paste',
- enabled
- },
- {
- role: 'selectAll',
- enabled
- }
- ]
- ];
-}
diff --git a/client/src/plugins/deployment-tool/validators.js b/client/src/plugins/deployment-tool/validators.js
deleted file mode 100644
index 92cd87abd3..0000000000
--- a/client/src/plugins/deployment-tool/validators.js
+++ /dev/null
@@ -1,51 +0,0 @@
-/**
- * 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.
- */
-
-export default {
- bearer: validateBearer,
- deploymentName: validateDeploymentName,
- endpointUrl: validateEndpointUrl,
- password: validatePassword,
- username: validateUsername
-};
-
-function validateBearer(bearer) {
- if (!bearer) {
- return 'Token must not be empty.';
- }
-}
-
-function validateDeploymentName(name) {
- if (!name) {
- return 'Deployment name must not be empty.';
- }
-}
-
-function validateEndpointUrl(url) {
- if (!url) {
- return 'Endpoint URL must not be empty.';
- }
-
- if (!/^https?:\/\/.+/.test(url)) {
- return 'Endpoint URL must start with "http://" or "https://".';
- }
-}
-
-function validatePassword(password) {
- if (!password) {
- return 'Password must not be empty.';
- }
-}
-
-function validateUsername(username) {
- if (!username) {
- return 'Username must not be empty.';
- }
-}
\ No newline at end of file