diff --git a/.circleci/config.yml b/.circleci/config.yml index a8cd87582..381da9c87 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -12,9 +12,11 @@ jobs: DEPLOY_PACKAGES: 1 DEB: xenial bionic RPM: el7 el8 + ST2_VERSION: "3.5dev" ST2_HOST: localhost - ST2_USERNAME: admin - ST2_PASSWORD: 123 + ST2_PROTOCOL: http + ST2_USERNAME: st2admin + ST2_PASSWORD: Ch@ngeMe ST2_TEST_ENVIRONMENT: https://github.com/StackStorm/st2-docker steps: - checkout @@ -68,48 +70,42 @@ jobs: name: Update Docker Compose command: | set -x - sudo sh -c "curl -L https://github.com/docker/compose/releases/download/1.14.0/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose" + sudo sh -c "curl -L https://github.com/docker/compose/releases/download/1.28.6/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose" sudo chmod +x /usr/local/bin/docker-compose - run: name: Clone test containers command: | - # Use DEPRECATED/all-in-one for now, we'll have to circle back around - # and fix this to use the master branch - echo "Cloning ${ST2_DOCKER_BRANCH:-DEPRECATED/all-in-one} branch of st2-docker" - git clone --branch ${ST2_DOCKER_BRANCH:-DEPRECATED/all-in-one} --depth 1 ${ST2_TEST_ENVIRONMENT} ~/st2-docker + echo "Cloning ${ST2_DOCKER_BRANCH:-master} branch of st2-docker" + git clone --branch ${ST2_DOCKER_BRANCH:-master} --depth 1 ${ST2_TEST_ENVIRONMENT} ~/st2-docker - run: - name: Update env variables for test containers + name: Configufe docker compose config command: | - make -C ~/st2-docker env - echo -e "ST2_USER=${ST2_USERNAME}\nST2_PASSWORD=${ST2_PASSWORD}" > ~/st2-docker/conf/stackstorm.env - cat ~/st2-docker/conf/stackstorm.env + # Configure allow origin in the user config + echo "[api]" > ~/st2-docker/files/st2.user.conf + echo "allow_origin = *" >> ~/st2-docker/files/st2.user.conf - run: name: Start test containers command: | docker-compose -f ~/st2-docker/docker-compose.yml up -d - sleep 60 - docker-compose -f ~/st2-docker/docker-compose.yml exec stackstorm crudini --set /etc/st2/st2.conf api allow_origin "*" - docker-compose -f ~/st2-docker/docker-compose.yml exec stackstorm st2ctl restart + sleep 100 - run: name: Check test containers command: | - docker-compose -f ~/st2-docker/docker-compose.yml exec stackstorm st2 run core.noop + docker-compose -f ~/st2-docker/docker-compose.yml exec st2client st2 run core.noop - run: name: Run functional tests command: npm run test-functional - run: name: Reset test containers command: | - docker-compose -f ~/st2-docker/docker-compose.yml down + docker-compose -f ~/st2-docker/docker-compose.yml down --rmi docker-compose -f ~/st2-docker/docker-compose.yml up -d - sleep 60 - docker-compose -f ~/st2-docker/docker-compose.yml exec stackstorm crudini --set /etc/st2/st2.conf api allow_origin "*" - docker-compose -f ~/st2-docker/docker-compose.yml exec stackstorm st2ctl restart + sleep 100 - run: name: Recheck test containers command: | - docker-compose -f ~/st2-docker/docker-compose.yml exec stackstorm st2 run core.noop - docker-compose -f ~/st2-docker/docker-compose.yml exec stackstorm st2 execution list + docker-compose -f ~/st2-docker/docker-compose.yml exec st2client st2 run core.noop + docker-compose -f ~/st2-docker/docker-compose.yml exec st2client st2 execution list - run: name: Run tests on production version command: npm run test-production diff --git a/.eslintignore b/.eslintignore index 79c529ab3..98ccae491 100644 --- a/.eslintignore +++ b/.eslintignore @@ -3,3 +3,4 @@ modules/*/node_modules tasks/*/node_modules node_modules js +package.meta.js diff --git a/README.md b/README.md index 6e7b3f189..a7fa51389 100644 --- a/README.md +++ b/README.md @@ -123,9 +123,9 @@ We're using [zombie](https://github.com/assaf/zombie) as our headless browser fo First of all, you need to make sure you have a running copy of st2 to run tests against. We're using [official docker images](https://github.com/stackstorm/st2-docker) for our automated tests, but the [AIO](https://docs.stackstorm.com/install/index.html) deployment will work just as good (though will take more time to deploy). -To let test runner know the details of your st2 installation, you need to set ST2_HOST, ST2_USERNAME and ST2_PASSWORD env variables, then call `gulp test`. +To let test runner know the details of your st2 installation, you need to set ST2_PROTOCOL, ST2_HOST, ST2_USERNAME and ST2_PASSWORD env variables, then call `gulp test`. - $ ST2_HOST=localhost ST2_USERNAME=admin ST2_PASSWORD=123 gulp test + $ ST2_PROTOCOL=http ST2_HOST=localhost ST2_USERNAME=admin ST2_PASSWORD=123 gulp test Copyright, License, and Contributors Agreement ---------------------------------------------- diff --git a/apps/st2-actions/actions-details.component.js b/apps/st2-actions/actions-details.component.js index b0d189d89..96d2111d0 100644 --- a/apps/st2-actions/actions-details.component.js +++ b/apps/st2-actions/actions-details.component.js @@ -213,9 +213,11 @@ export default class ActionsDetails extends React.Component { }, }); } -setWindowName(e){ - window.name="parent" -} + + setWindowName(e) { + window.name = 'parent'; + } + handleRun(e, ...args) { e.preventDefault(); @@ -259,7 +261,7 @@ setWindowName(e){ target="_blank" to={`/action/${action.ref}`} className="st2-forms__button st2-details__toolbar-button" - onClick ={e => this.setWindowName(e)} + onClick={e => this.setWindowName(e)} > Edit diff --git a/apps/st2-history/history-details.component.js b/apps/st2-history/history-details.component.js index 56730447a..3e2283a4a 100644 --- a/apps/st2-history/history-details.component.js +++ b/apps/st2-history/history-details.component.js @@ -47,6 +47,7 @@ import HistoryPopup from './history-popup.component'; const { execution } = state; return { execution }; }) + export default class HistoryDetails extends React.Component { static propTypes = { handleNavigate: PropTypes.func.isRequired, @@ -55,7 +56,7 @@ export default class HistoryDetails extends React.Component { id: PropTypes.string, section: PropTypes.string, - execution: PropTypes.object, + execution: PropTypes.object, // eslint-disable-line react/no-unused-prop-types displayUTC: PropTypes.bool.isRequired, handleToggleUTC: PropTypes.func, } @@ -81,9 +82,15 @@ export default class HistoryDetails extends React.Component { } fetchExecution(id) { + // We utilize ?max_result_size query parameter filter so we don't retrieve + // large results which we don't render due to that being very slow and + // freezing the browser window + const maxResultSizeForRender = ActionReporter.utils.getMaxExecutionResultSizeForRender(); + const path = `/executions/${id}?max_result_size=${maxResultSizeForRender}`; + store.dispatch({ type: 'FETCH_EXECUTION', - promise: api.request({ path: `/executions/${id}` }), + promise: api.request({ path: path }), }) .catch((err) => { notification.error(`Unable to retrieve execution "${id}".`, { err }); @@ -183,7 +190,7 @@ export default class HistoryDetails extends React.Component { - + { execution.rule ? ( diff --git a/apps/st2-history/history-panel.component.js b/apps/st2-history/history-panel.component.js index 85438df56..c03542dad 100644 --- a/apps/st2-history/history-panel.component.js +++ b/apps/st2-history/history-panel.component.js @@ -94,7 +94,7 @@ export default class HistoryPanel extends React.Component { }).isRequired, filter: PropTypes.string, - filters: PropTypes.object, + filters: PropTypes.array, childExecutions: PropTypes.object, groups: PropTypes.array, collapsed: PropTypes.bool, diff --git a/apps/st2-workflows/workflows.component.js b/apps/st2-workflows/workflows.component.js index 697a728d9..b682e22d4 100644 --- a/apps/st2-workflows/workflows.component.js +++ b/apps/st2-workflows/workflows.component.js @@ -73,7 +73,7 @@ export default class Workflows extends Component { pack: PropTypes.string, meta: PropTypes.object, metaSource: PropTypes.string, - setMeta: PropTypes.func, + setMeta: PropTypes.func, // eslint-disable-line react/no-unused-prop-types input: PropTypes.array, workflowSource: PropTypes.string, dirty: PropTypes.bool, @@ -243,7 +243,7 @@ export default class Workflows extends Component { } save() { - const { pack, meta, actions, workflowSource, metaSource, setMeta } = this.props; + const { pack, meta, actions, workflowSource, metaSource } = this.props; const existingAction = actions.find(e => e.name === meta.name && e.pack === pack); if (!meta.name) { diff --git a/config.js b/config.js index e958e4829..233bcb2c8 100644 --- a/config.js +++ b/config.js @@ -18,6 +18,15 @@ angular.module('main') .constant('st2Config', { + // In case you want to override default value for the result sizes we still render in the + // history details widget. Keep in mind that anything above 200-500 KB will take a long time to + // render and likely freeze the browser window for deeply nested JSON object results. + // Value is in bytes. + // max_execution_result_size_for_render: 200 * 1024, + // + // Set to true to display StackStorm and st2web version in the header + //show_version_in_header: false; + // hosts: [ // { // name: 'Dev Env', diff --git a/index.html b/index.html index d7df4ff73..bc0bfcc61 100644 --- a/index.html +++ b/index.html @@ -19,12 +19,18 @@ throw new Error('The st2web angular-config-polyfill only supports the "main" module.'); } - if (constant !== 'st2Config') { - throw new Error('The st2web angular-config-polyfill only supports the "st2Config" constant.'); + if (constant !== 'st2Config' && constant !== 'st2PackageMeta') { + throw new Error('The st2web angular-config-polyfill only supports the "st2Config" and "st2PackageMeta" constant.'); } window.st2constants = window.st2constants || {}; - window.st2constants.st2Config = value; + + if (constant === 'st2Config') { + window.st2constants.st2Config = value; + } + else if (constant === 'st2PackageMeta') { + window.st2constants.st2PackageMeta = value; + } }, run: (fn) => { if (module !== 'main') { @@ -33,6 +39,7 @@ window.st2constants = window.st2constants || {}; window.st2constants.st2Config = window.st2constants.st2Config || {}; + window.st2constants.st2PackageMeta = window.st2constants.st2PackageMeta || {}; fn(window.st2constants.st2Config); }, @@ -44,6 +51,7 @@
+ diff --git a/modules/st2-action-reporter/action-reporter.component.js b/modules/st2-action-reporter/action-reporter.component.js index 17fa58870..e74432cd1 100644 --- a/modules/st2-action-reporter/action-reporter.component.js +++ b/modules/st2-action-reporter/action-reporter.component.js @@ -20,21 +20,114 @@ import reporters from './reporters'; import style from './style.css'; +// If action execution result is larger than this value (in bytes) we won't try to render it in +// the code highlighter widget, but display a link to the raw result output instead. +// This way we avoid large results freezing and blocking the browser window. +// Keep in mind that rendering time also depends on the result type (aka deeply +// nested JSON object vs a more flat one). +// Based on testing, any larger and more nested JSON object over 100 KB will +// take a while to render and consume a lot of memory (and in case of even +// larger objects, freeze / block the whole browser window). +// Technically we could still display and render results up to 300 KB, but the +// whole code widget and browser window gets lagy and slow. +// Testing was also performed on relatively high end PC so on older ones, even +// lower limit may be more appropriate. +// Can be overriden in the config, but values over 50-100 KB (depending on the client +// resources and how nested the result objects are) are not recommended. +const DEFAULT_MAX_RESULT_SIZE = 100 * 1024; // 100 KB + + +/** + * Return base URL to the API service based on the config value. + */ +function getBaseAPIUrl(api) { + if (!api.server) { + console.log('config.js is not correctly configured - it\'s missing API server URL entry'); + return null; + } + + if (!api.server.api) { + console.log('config.js is not correctly configured - it\'s missing API server URL entry'); + return null; + } + + const url = api.server.api; + let baseUrl; + + if (!url.startsWith('http://') && !(url.startsWith('https://'))) { + baseUrl = `${window.location.protocol}${url}`; + } + else { + baseUrl = `${url}`; + } + + return baseUrl; +} + +/** + * Return value for the ?max_result_size query parameter aka the maximum number for the result size + * (in bytes) we will still try to render and display. + * + * We specify a default value which can be overriden inside the config. + */ +function getMaxExecutionResultSizeForRender() { + let maxResultSizeForRender; + + try { + maxResultSizeForRender = window.st2constants.st2Config.max_execution_result_size_for_render || DEFAULT_MAX_RESULT_SIZE; + } + catch (e) { + maxResultSizeForRender = DEFAULT_MAX_RESULT_SIZE; + } + + return maxResultSizeForRender; +} + export default class ActionReporter extends React.Component { static propTypes = { className: PropTypes.string, runner: PropTypes.string.isRequired, execution: PropTypes.object.isRequired, + api: PropTypes.object.isRequired, + } + + static utils = { + getMaxExecutionResultSizeForRender: getMaxExecutionResultSizeForRender, + getBaseAPIUrl: getBaseAPIUrl, } render() { - const { className, runner, execution, ...props } = this.props; + const { className, runner, execution, api, ...props } = this.props; const reporter = reporters[runner] || reporters.debug; if (!execution) { return null; } + // For backward compatibility with older executions which may not have result_size attribute + // we fall back to execution.result (if available - would only be available when using newer + // st2web with older version of other StackStorm components). + const resultSize = execution.result_size || JSON.stringify(execution.result || {}).length; + const resultSizeMB = ((resultSize / 1024 / 1024)).toFixed(2); + const maxResultSizeForRender = getMaxExecutionResultSizeForRender(); + + if (resultSize && resultSize > maxResultSizeForRender) { + // TODO: Add methods to the client to retrieve full correct URL? + const baseApiUrl = getBaseAPIUrl(api); + const viewRawResultUrl = `${baseApiUrl}/v1/executions/${execution.id}/result?pretty_format=1`; + const downloadRawResultUrl = `${baseApiUrl}/v1/executions/${execution.id}/result?download=1&pretty_format=1`; + const downloadCompressedRawResultUrl = `${baseApiUrl}/v1/executions/${execution.id}/result?download=1&pretty_format=1&compress=1`; + + return ( +
+
Output
+

+ Action output is too large to be displayed here ({`${resultSizeMB}`} MB).

You can view raw execution output by clicking here or you can download the output by clicking here (uncompressed) or here (compressed). +

+
+ ); + } + return (
{ reporter(execution) } diff --git a/modules/st2-action-reporter/tests/test-action-reporter.js b/modules/st2-action-reporter/tests/test-action-reporter.js index 858896c10..34f2ab37c 100644 --- a/modules/st2-action-reporter/tests/test-action-reporter.js +++ b/modules/st2-action-reporter/tests/test-action-reporter.js @@ -25,9 +25,10 @@ describe(`${ActionReporter.name} Component`, () => { it('proxies className', () => { const instance = ReactTester.create( ); @@ -37,13 +38,29 @@ describe(`${ActionReporter.name} Component`, () => { it('proxies extra props', () => { const instance = ReactTester.create( ); expect(instance.node.props.foo).to.equal('bar'); }); + + it('returns correct message on large result', () => { + const instance = ReactTester.create( + + ); + + const pElem = instance.toJSON().children[1].children.join(''); + expect(pElem).to.contain('Action output is too large to be displayed here'); + expect(pElem).to.contain('You can view raw'); + }); }); }); diff --git a/modules/st2-api/api.js b/modules/st2-api/api.js index b186c89cf..35017f8ec 100644 --- a/modules/st2-api/api.js +++ b/modules/st2-api/api.js @@ -175,28 +175,29 @@ export class API { const headers = { 'content-type': 'application/json', - - }; if (this.token && this.token.token) { headers['x-auth-token'] = this.token.token; } - + const config = { method, url: this.route(opts), params: query, headers, - transformResponse: [function transformResponse(data, headers) { - if (typeof data === 'string' && headers["content-type"] === "application/json") { - try { - data = JSON.parse(data); - } catch (e) { /* Ignore */ } - } - return data; - } - ], + transformResponse: [ function transformResponse(data, headers) { + if (typeof data === 'string' && headers['content-type'] === 'application/json') { + try { + data = JSON.parse(data); + } + catch (e) { + /* Ignore */ + } + } + + return data; + } ], data, withCredentials: true, paramsSerializer: params => { diff --git a/modules/st2-menu/menu.component.js b/modules/st2-menu/menu.component.js index 36a2ac607..68bedb2b1 100644 --- a/modules/st2-menu/menu.component.js +++ b/modules/st2-menu/menu.component.js @@ -73,7 +73,7 @@ export default class Menu extends React.Component { render() { const { className, location, routes: allRoutes, style, ...props } = this.props; - + const routes = _(allRoutes) .filter((e) => !!e.icon) .sortBy((e) => e.position) @@ -82,10 +82,13 @@ export default class Menu extends React.Component { const user = api.token && api.token.user; const server = api.server; + const showVersion = window.st2constants.st2Config.show_version_in_header || false; + const hasPackageMeta = (window.st2constants.st2PackageMeta !== undefined); + const st2webCommitsUrl = (showVersion && hasPackageMeta) ? `https://github.com/StackStorm/st2web/commit/${window.st2constants.st2PackageMeta.git_sha}` : ''; return (
- + { (showVersion && hasPackageMeta) ? st2: v{window.st2constants.st2PackageMeta.version}, st2web: {window.st2constants.st2PackageMeta.git_sha} : '' }
diff --git a/modules/st2-pack-icon/pack-icon.component.js b/modules/st2-pack-icon/pack-icon.component.js index baac4d99a..61b70ce1d 100644 --- a/modules/st2-pack-icon/pack-icon.component.js +++ b/modules/st2-pack-icon/pack-icon.component.js @@ -82,9 +82,11 @@ export default class PackIcon extends React.Component { ); } + /* Unreachable code, commented out :shrug: return ( ); + */ // ^^ WAT? } diff --git a/modules/st2-time/tests/test-time.js b/modules/st2-time/tests/test-time.js index 46a6b6309..c5df04909 100644 --- a/modules/st2-time/tests/test-time.js +++ b/modules/st2-time/tests/test-time.js @@ -19,6 +19,12 @@ import { ReactTester } from '@stackstorm/module-test-utils'; import Time from '..'; +function isDST(d) { + const jan = new Date(d.getFullYear(), 0, 1).getTimezoneOffset(); + const jul = new Date(d.getFullYear(), 6, 1).getTimezoneOffset(); + return Math.max(jan, jul) !== d.getTimezoneOffset(); +} + describe(`${Time.name} Component`, () => { describe('common functionality', () => { it('proxies className', () => { @@ -52,8 +58,18 @@ describe(`${Time.name} Component`, () => { ); // note: this will only work in places with whole hour offsets - const hour = (24 - new Date().getTimezoneOffset() / 60); + // note: it seems like this react time component doesn't correctly take DST + // into account so this is a temporary hack to get tests to pass on systems + // without UTC timezone :/ + const now = new Date(); + const isNowDst = isDST(now); + let hour = (24 - new Date().getTimezoneOffset() / 60); + if (hour >= 24) { + if (isNowDst) { + hour = hour - 1; + } + expect(instance.text).to.equal( `Thu, 01 Jan 1970 ${(hour - 24).toFixed(0).padStart(2, '0')}:00:00` ); @@ -83,8 +99,18 @@ describe(`${Time.name} Component`, () => { ); // note: this will only work in places with whole hour offsets - const hour = (24 - new Date().getTimezoneOffset() / 60); + // note: it seems like this react time component doesn't correctly take DST + // into account so this is a temporary hack to get tests to pass on systems + // without UTC timezone :/ + const now = new Date(); + const isNowDst = isDST(now); + let hour = (24 - new Date().getTimezoneOffset() / 60); + if (hour >= 24) { + if (isNowDst) { + hour = hour - 1; + } + expect(instance.text).to.equal( `January 1 1970 ${(hour - 24).toFixed(0).padStart(2, '0')}:00 AM` ); diff --git a/modules/st2flow-canvas/index.js b/modules/st2flow-canvas/index.js index 19e459cbc..040c2a836 100644 --- a/modules/st2flow-canvas/index.js +++ b/modules/st2flow-canvas/index.js @@ -22,7 +22,6 @@ import { TransitionInterface, } from '@stackstorm/st2flow-model/interfaces'; import { NotificationInterface } from '@stackstorm/st2flow-notifications'; -import { Node } from 'react'; import React, { Component } from 'react'; import { connect } from 'react-redux'; @@ -48,7 +47,6 @@ import PoissonRectangleSampler from './poisson-rect'; import { origin } from './const'; import style from './style.css'; -import store from '../../apps/st2-workflows/store'; type DOMMatrix = { m11: number, m22: number @@ -228,6 +226,9 @@ export default class Canvas extends Component { nextTask: PropTypes.string, isCollapsed: PropTypes.object, toggleCollapse: PropTypes.func, + dirtyflag: PropTypes.bool, + fetchActionscalled: PropTypes.func, + saveData: PropTypes.func, } state = { diff --git a/modules/st2flow-details/orquesta-properties.js b/modules/st2flow-details/orquesta-properties.js index 8913df4da..b0d6bbf80 100644 --- a/modules/st2flow-details/orquesta-properties.js +++ b/modules/st2flow-details/orquesta-properties.js @@ -46,7 +46,6 @@ type TransitionProps = { }) ) export default class OrquestaTransition extends Component { - static propTypes = { task: PropTypes.object.isRequired, issueModelCommand: PropTypes.func, @@ -64,7 +63,10 @@ export default class OrquestaTransition extends Component { } getValue(value) { - if(!isNaN(value) && value!== '') value = parseInt(value,10); + if (!isNaN(value) && value !== '') { + value = parseInt(value,10); + } + return value; } @@ -119,8 +121,8 @@ export default class OrquestaTransition extends Component { this.handleTaskProperty('delay', value ? '10' : false)}> { task.delay != null && ( -
-