@spinnaker/core 0.15.0 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +44 -0
- package/dist/domain/IPipeline.d.ts +1 -0
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/application/applications.state.provider.ts +1 -1
- package/src/application/modal/createApplication.modal.controller.js +7 -6
- package/src/application/modal/createApplication.modal.controller.spec.js +1 -0
- package/src/application/search/Applications.tsx +48 -0
- package/src/domain/IPipeline.ts +1 -0
- package/src/help/help.contents.ts +2 -0
- package/src/help/helpContents.registry.spec.ts +8 -0
- package/src/insight/InsightMenu.tsx +3 -0
- package/src/pipeline/config/stages/bakeManifest/helm/BakeHelmConfigForm.spec.tsx +46 -1
- package/src/pipeline/config/stages/bakeManifest/helm/BakeHelmConfigForm.tsx +36 -6
- package/src/pipeline/config/triggers/ExecutionOptionsPageContent.spec.tsx +90 -0
- package/src/pipeline/config/triggers/ExecutionOptionsPageContent.tsx +27 -2
- package/src/pipeline/details/StageFailureMessage.tsx +25 -10
- package/src/search/infrastructure/infrastructure.controller.js +3 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spinnaker/core",
|
|
3
3
|
"license": "Apache-2.0",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.17.0",
|
|
5
5
|
"module": "dist/index.js",
|
|
6
6
|
"typings": "dist/index.d.ts",
|
|
7
7
|
"scripts": {
|
|
@@ -89,7 +89,7 @@
|
|
|
89
89
|
"@graphql-codegen/typescript-operations": "^1.18.3",
|
|
90
90
|
"@graphql-codegen/typescript-react-apollo": "^2.3.0",
|
|
91
91
|
"@spinnaker/eslint-plugin": "^3.0.1",
|
|
92
|
-
"@spinnaker/scripts": "^0.2.
|
|
92
|
+
"@spinnaker/scripts": "^0.2.4",
|
|
93
93
|
"@types/angular": "1.6.26",
|
|
94
94
|
"@types/angular-mocks": "1.5.10",
|
|
95
95
|
"@types/angular-ui-bootstrap": "0.13.41",
|
|
@@ -120,5 +120,5 @@
|
|
|
120
120
|
"shx": "0.3.3",
|
|
121
121
|
"typescript": "4.3.5"
|
|
122
122
|
},
|
|
123
|
-
"gitHead": "
|
|
123
|
+
"gitHead": "fd69b27a052ccb2a170efdbe0b971a8636c96447"
|
|
124
124
|
}
|
|
@@ -13,7 +13,7 @@ module(APPLICATIONS_STATE_PROVIDER, [STATE_CONFIG_PROVIDER, APPLICATION_STATE_PR
|
|
|
13
13
|
(stateConfigProvider: StateConfigProvider, applicationStateProvider: ApplicationStateProvider) => {
|
|
14
14
|
const applicationsState: INestedState = {
|
|
15
15
|
name: 'applications',
|
|
16
|
-
url: '/applications',
|
|
16
|
+
url: '/applications?create',
|
|
17
17
|
views: {
|
|
18
18
|
'main@': {
|
|
19
19
|
component: Applications,
|
|
@@ -30,7 +30,8 @@ module(CORE_APPLICATION_MODAL_CREATEAPPLICATION_MODAL_CONTROLLER, [
|
|
|
30
30
|
'$state',
|
|
31
31
|
'$uibModalInstance',
|
|
32
32
|
'$timeout',
|
|
33
|
-
|
|
33
|
+
'name',
|
|
34
|
+
function ($scope, $q, $log, $state, $uibModalInstance, $timeout, name) {
|
|
34
35
|
const applicationLoader = ApplicationReader.listApplications();
|
|
35
36
|
applicationLoader.then((applications) => (this.data.appNameList = _.map(applications, 'name')));
|
|
36
37
|
|
|
@@ -59,6 +60,10 @@ module(CORE_APPLICATION_MODAL_CREATEAPPLICATION_MODAL_CONTROLLER, [
|
|
|
59
60
|
instancePort: SETTINGS.defaultInstancePort || null,
|
|
60
61
|
};
|
|
61
62
|
|
|
63
|
+
if (name) {
|
|
64
|
+
this.application.name = name;
|
|
65
|
+
}
|
|
66
|
+
|
|
62
67
|
const submitting = () => {
|
|
63
68
|
this.state.errorMessages = [];
|
|
64
69
|
this.state.submitting = true;
|
|
@@ -71,11 +76,7 @@ module(CORE_APPLICATION_MODAL_CREATEAPPLICATION_MODAL_CONTROLLER, [
|
|
|
71
76
|
let navigateTimeout = null;
|
|
72
77
|
|
|
73
78
|
const routeToApplication = () => {
|
|
74
|
-
|
|
75
|
-
$state.go('home.applications.application', {
|
|
76
|
-
application: this.application.name,
|
|
77
|
-
});
|
|
78
|
-
}, 1000);
|
|
79
|
+
$uibModalInstance.close(this.application);
|
|
79
80
|
};
|
|
80
81
|
|
|
81
82
|
$scope.$on('$destroy', () => $timeout.cancel(navigateTimeout));
|
|
@@ -8,9 +8,11 @@ import { distinctUntilChanged, map, takeUntil } from 'rxjs/operators';
|
|
|
8
8
|
import { ApplicationTable } from './ApplicationsTable';
|
|
9
9
|
import { PaginationControls } from './PaginationControls';
|
|
10
10
|
import type { IAccount } from '../../account';
|
|
11
|
+
import type { Application } from '../../application';
|
|
11
12
|
import type { ICache } from '../../cache';
|
|
12
13
|
import { ViewStateCache } from '../../cache';
|
|
13
14
|
import { InsightMenu } from '../../insight/InsightMenu';
|
|
15
|
+
import { ModalInjector, ReactInjector } from '../../reactShims';
|
|
14
16
|
import type { IApplicationSummary } from '../service/ApplicationReader';
|
|
15
17
|
import { ApplicationReader } from '../service/ApplicationReader';
|
|
16
18
|
import { Spinner } from '../../widgets';
|
|
@@ -22,6 +24,10 @@ interface IViewState {
|
|
|
22
24
|
sort: string;
|
|
23
25
|
}
|
|
24
26
|
|
|
27
|
+
interface IApplicationsStateParams {
|
|
28
|
+
create?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
25
31
|
export interface IApplicationPagination {
|
|
26
32
|
currentPage: number;
|
|
27
33
|
itemsPerPage: number;
|
|
@@ -113,6 +119,48 @@ export class Applications extends React.Component<{}, IApplicationsState> {
|
|
|
113
119
|
throw error;
|
|
114
120
|
},
|
|
115
121
|
);
|
|
122
|
+
|
|
123
|
+
const { $stateParams, $state, $rootScope, overrideRegistry } = ReactInjector;
|
|
124
|
+
const { create } = $stateParams as IApplicationsStateParams;
|
|
125
|
+
applicationSummaries$.subscribe((applications: IApplicationSummary[]) => {
|
|
126
|
+
if (create) {
|
|
127
|
+
const found = applications.find((app) => app.name === create);
|
|
128
|
+
if (found) {
|
|
129
|
+
if (found.email) {
|
|
130
|
+
// Application already exists - redirect to app
|
|
131
|
+
$state.go('home.applications.application', { application: create, create: null });
|
|
132
|
+
} else {
|
|
133
|
+
// Inferred application - redirect to config
|
|
134
|
+
$state.go('home.applications.application.config', { application: create, create: null });
|
|
135
|
+
}
|
|
136
|
+
} else {
|
|
137
|
+
// Nonexistant application - open create modal
|
|
138
|
+
ModalInjector.modalService
|
|
139
|
+
.open({
|
|
140
|
+
scope: $rootScope.$new(),
|
|
141
|
+
templateUrl: overrideRegistry.getTemplate(
|
|
142
|
+
'createApplicationModal',
|
|
143
|
+
require('../modal/newapplication.html'),
|
|
144
|
+
),
|
|
145
|
+
resolve: {
|
|
146
|
+
name: () => create,
|
|
147
|
+
},
|
|
148
|
+
controller: overrideRegistry.getController('CreateApplicationModalCtrl'),
|
|
149
|
+
controllerAs: 'newAppModal',
|
|
150
|
+
})
|
|
151
|
+
.result.then(
|
|
152
|
+
(app: Application) => {
|
|
153
|
+
$state.go('home.applications.application', { application: app.name, create: null });
|
|
154
|
+
},
|
|
155
|
+
() => {
|
|
156
|
+
// Clear out the query parameter if the dialog is dismissed
|
|
157
|
+
$state.go('home.applications', { create: null });
|
|
158
|
+
},
|
|
159
|
+
)
|
|
160
|
+
.catch(() => {});
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
});
|
|
116
164
|
}
|
|
117
165
|
|
|
118
166
|
private toggleSort(column: string): void {
|
package/src/domain/IPipeline.ts
CHANGED
|
@@ -364,6 +364,8 @@ const helpContents: { [key: string]: string } = {
|
|
|
364
364
|
'pipeline.config.dependsOn': 'Declares which stages must be run <em>before</em> this stage begins.',
|
|
365
365
|
'pipeline.config.parallel.cancel.queue':
|
|
366
366
|
'<p>If concurrent pipeline execution is disabled, then the pipelines that are in the waiting queue will get canceled when the next execution starts. <br><br>Check this box if you want to keep them in the queue.</p>',
|
|
367
|
+
'pipeline.config.parallel.max.concurrent':
|
|
368
|
+
'<p>If concurrent pipeline execution is enabled, this variable sets the maximum number of concurrent pipelines executing. <br><br>If set to 0, then max is unlimited.</p>',
|
|
367
369
|
'pipeline.config.timeout': `
|
|
368
370
|
<p>Allows you to force the stage to fail if its running time exceeds a specific length.</p>
|
|
369
371
|
<p><b>Note:</b> By default, Spinnaker will use sensible timeouts that depend on the stage type and the operations the stage needs to perform at runtime. These defaults can vary based on chosen configuration and other external factors.
|
|
@@ -21,4 +21,12 @@ describe('Help contents registry', () => {
|
|
|
21
21
|
expect(HelpContentsRegistry.getHelpField('a')).toBe('b');
|
|
22
22
|
});
|
|
23
23
|
});
|
|
24
|
+
|
|
25
|
+
describe('max concurrent definition', () => {
|
|
26
|
+
it('provides the expected definition for max concurrent', () => {
|
|
27
|
+
const definition =
|
|
28
|
+
'<p>If concurrent pipeline execution is enabled, this variable sets the maximum number of concurrent pipelines executing. <br><br>If set to 0, then max is unlimited.</p>';
|
|
29
|
+
expect(HelpContentsRegistry.getHelpField('pipeline.config.parallel.max.concurrent')).toEqual(definition);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
24
32
|
});
|
|
@@ -56,6 +56,9 @@ export class InsightMenu extends React.Component<IInsightMenuProps, IInsightMenu
|
|
|
56
56
|
'createApplicationModal',
|
|
57
57
|
require('../application/modal/newapplication.html'),
|
|
58
58
|
),
|
|
59
|
+
resolve: {
|
|
60
|
+
name: () => '',
|
|
61
|
+
},
|
|
59
62
|
controller: this.overrideRegistry.getController('CreateApplicationModalCtrl'),
|
|
60
63
|
controllerAs: 'newAppModal',
|
|
61
64
|
})
|
|
@@ -6,7 +6,8 @@ import { StageConfigField } from '../../../..';
|
|
|
6
6
|
import { BakeHelmConfigForm } from './BakeHelmConfigForm';
|
|
7
7
|
import { AccountService } from '../../../../../account';
|
|
8
8
|
import { ApplicationModelBuilder } from '../../../../../application';
|
|
9
|
-
import
|
|
9
|
+
import { ExpectedArtifactService } from '../../../../../artifact';
|
|
10
|
+
import type { IExpectedArtifact, IStage } from '../../../../../domain';
|
|
10
11
|
import { SpinFormik } from '../../../../../presentation';
|
|
11
12
|
import { REACT_MODULE } from '../../../../../reactShims';
|
|
12
13
|
|
|
@@ -84,4 +85,48 @@ describe('<BakeHelmConfigForm />', () => {
|
|
|
84
85
|
|
|
85
86
|
expect(component.find(StageConfigField).findWhere((x) => x.text() === helmChartFilePathFieldName).length).toBe(0);
|
|
86
87
|
});
|
|
88
|
+
|
|
89
|
+
it('render the helm chart file path if the id of the git artifact is given but the account value does not exist', async () => {
|
|
90
|
+
const expectedArtifactDisplayName = 'test-artifact';
|
|
91
|
+
const expectedArtifactId = 'test-artifact-id';
|
|
92
|
+
const expectedGitArtifact: IExpectedArtifact = {
|
|
93
|
+
defaultArtifact: {
|
|
94
|
+
customKind: true,
|
|
95
|
+
id: 'defaultArtifact-id',
|
|
96
|
+
},
|
|
97
|
+
displayName: expectedArtifactDisplayName,
|
|
98
|
+
id: expectedArtifactId,
|
|
99
|
+
matchArtifact: {
|
|
100
|
+
artifactAccount: 'gitrepo',
|
|
101
|
+
id: expectedArtifactId,
|
|
102
|
+
reference: 'git repo',
|
|
103
|
+
type: 'git/repo',
|
|
104
|
+
version: 'master',
|
|
105
|
+
},
|
|
106
|
+
useDefaultArtifact: false,
|
|
107
|
+
usePriorArtifact: false,
|
|
108
|
+
};
|
|
109
|
+
const stage = ({
|
|
110
|
+
inputArtifacts: [{ id: expectedArtifactId }],
|
|
111
|
+
} as unknown) as IStage;
|
|
112
|
+
|
|
113
|
+
spyOn(ExpectedArtifactService, 'getExpectedArtifactsAvailableToStage').and.returnValue([expectedGitArtifact]);
|
|
114
|
+
|
|
115
|
+
const props = getProps();
|
|
116
|
+
|
|
117
|
+
const component = mount(
|
|
118
|
+
<SpinFormik
|
|
119
|
+
initialValues={stage}
|
|
120
|
+
onSubmit={() => null}
|
|
121
|
+
validate={() => null}
|
|
122
|
+
render={(formik) => <BakeHelmConfigForm {...props} formik={formik} />}
|
|
123
|
+
/>,
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
await new Promise((resolve) => setTimeout(resolve)); // wait one js tick for promise to resolve
|
|
127
|
+
component.setProps({}); // force a re-render
|
|
128
|
+
|
|
129
|
+
expect(component.find('.Select-value-label > span').text().includes(expectedArtifactDisplayName)).toBe(true);
|
|
130
|
+
expect(component.find(StageConfigField).findWhere((x) => x.text() === helmChartFilePathFieldName).length).toBe(1);
|
|
131
|
+
});
|
|
87
132
|
});
|
|
@@ -2,7 +2,12 @@ import React from 'react';
|
|
|
2
2
|
|
|
3
3
|
import type { IFormikStageConfigInjectedProps } from '../../FormikStageConfig';
|
|
4
4
|
import { AccountService } from '../../../../../account';
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
ArtifactTypePatterns,
|
|
7
|
+
excludeAllTypesExcept,
|
|
8
|
+
ExpectedArtifactService,
|
|
9
|
+
StageArtifactSelectorDelegate,
|
|
10
|
+
} from '../../../../../artifact';
|
|
6
11
|
import { StageConfigField } from '../../common/stageConfigField/StageConfigField';
|
|
7
12
|
import type { IArtifact, IExpectedArtifact } from '../../../../../domain';
|
|
8
13
|
import { MapEditor } from '../../../../../forms';
|
|
@@ -41,6 +46,25 @@ export class BakeHelmConfigForm extends React.Component<IFormikStageConfigInject
|
|
|
41
46
|
},
|
|
42
47
|
]);
|
|
43
48
|
}
|
|
49
|
+
|
|
50
|
+
// If the Expected Artifact id is provided but the account is not, then attempt to find the artifact from
|
|
51
|
+
// upstream stages and set the account value.
|
|
52
|
+
// This is needed because helm chart file path field will need to be rendered if the artifact has a git repo account type
|
|
53
|
+
const expectedArtifact = this.getInputArtifact(stage, 0);
|
|
54
|
+
if (expectedArtifact.id && !expectedArtifact.account) {
|
|
55
|
+
const availableArtifacts = ExpectedArtifactService.getExpectedArtifactsAvailableToStage(
|
|
56
|
+
stage,
|
|
57
|
+
this.props.pipeline,
|
|
58
|
+
);
|
|
59
|
+
const expectedMatchedArtifact = availableArtifacts.find((a) => a.id === expectedArtifact.id);
|
|
60
|
+
if (expectedMatchedArtifact && expectedMatchedArtifact.matchArtifact) {
|
|
61
|
+
this.props.formik.setFieldValue(
|
|
62
|
+
`inputArtifacts[0].account`,
|
|
63
|
+
expectedMatchedArtifact.matchArtifact.artifactAccount,
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
44
68
|
AccountService.getArtifactAccounts().then((artifactAccounts) => {
|
|
45
69
|
this.setState({
|
|
46
70
|
gitRepoArtifactAccountNames: artifactAccounts
|
|
@@ -56,9 +80,16 @@ export class BakeHelmConfigForm extends React.Component<IFormikStageConfigInject
|
|
|
56
80
|
this.props.formik.setFieldValue(`inputArtifacts[${index}].account`, artifact.artifactAccount);
|
|
57
81
|
};
|
|
58
82
|
|
|
59
|
-
private onTemplateArtifactSelected = (
|
|
60
|
-
this.props.formik.setFieldValue(`inputArtifacts[${index}].id`, id);
|
|
83
|
+
private onTemplateArtifactSelected = (artifact: IExpectedArtifact, index: number) => {
|
|
84
|
+
this.props.formik.setFieldValue(`inputArtifacts[${index}].id`, artifact.id);
|
|
61
85
|
this.props.formik.setFieldValue(`inputArtifacts[${index}].artifact`, null);
|
|
86
|
+
// Set the account to matchArtifact.artifactAccount if it exists.
|
|
87
|
+
// This account value will be used to determine if the Helm Chart File Path should be displayed.
|
|
88
|
+
if (artifact.matchArtifact) {
|
|
89
|
+
this.props.formik.setFieldValue(`inputArtifacts[${index}].account`, artifact.matchArtifact.artifactAccount);
|
|
90
|
+
} else {
|
|
91
|
+
this.props.formik.setFieldValue(`inputArtifacts[${index}].account`, null);
|
|
92
|
+
}
|
|
62
93
|
};
|
|
63
94
|
|
|
64
95
|
private addInputArtifact = () => {
|
|
@@ -119,7 +150,6 @@ export class BakeHelmConfigForm extends React.Component<IFormikStageConfigInject
|
|
|
119
150
|
|
|
120
151
|
public render() {
|
|
121
152
|
const stage = this.props.formik.values;
|
|
122
|
-
|
|
123
153
|
return (
|
|
124
154
|
<>
|
|
125
155
|
<h4>Helm Options</h4>
|
|
@@ -150,7 +180,7 @@ export class BakeHelmConfigForm extends React.Component<IFormikStageConfigInject
|
|
|
150
180
|
onArtifactEdited={(artifact) => {
|
|
151
181
|
this.onTemplateArtifactEdited(artifact, 0);
|
|
152
182
|
}}
|
|
153
|
-
onExpectedArtifactSelected={(artifact: IExpectedArtifact) => this.onTemplateArtifactSelected(artifact
|
|
183
|
+
onExpectedArtifactSelected={(artifact: IExpectedArtifact) => this.onTemplateArtifactSelected(artifact, 0)}
|
|
154
184
|
pipeline={this.props.pipeline}
|
|
155
185
|
stage={stage}
|
|
156
186
|
/>
|
|
@@ -180,7 +210,7 @@ export class BakeHelmConfigForm extends React.Component<IFormikStageConfigInject
|
|
|
180
210
|
this.onTemplateArtifactEdited(artifact, index + 1);
|
|
181
211
|
}}
|
|
182
212
|
onExpectedArtifactSelected={(artifact: IExpectedArtifact) =>
|
|
183
|
-
this.onTemplateArtifactSelected(artifact
|
|
213
|
+
this.onTemplateArtifactSelected(artifact, index + 1)
|
|
184
214
|
}
|
|
185
215
|
pipeline={this.props.pipeline}
|
|
186
216
|
stage={stage}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { mount } from 'enzyme';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
|
|
4
|
+
import { ExecutionOptionsPageContent } from './ExecutionOptionsPageContent';
|
|
5
|
+
import type { IPipeline } from '../../../domain';
|
|
6
|
+
|
|
7
|
+
describe('Execution Options Page Content', () => {
|
|
8
|
+
describe('Max Concurrent Options', () => {
|
|
9
|
+
let pipeline: IPipeline;
|
|
10
|
+
const setPipeline = (overrides: any = {}) => {
|
|
11
|
+
pipeline = {
|
|
12
|
+
application: 'test',
|
|
13
|
+
id: 'test1',
|
|
14
|
+
keepWaitingPipelines: false,
|
|
15
|
+
limitConcurrent: false,
|
|
16
|
+
maxConcurrentExecutions: 0,
|
|
17
|
+
name: 'test p 1', // @ts-ignore
|
|
18
|
+
parameterConfig: [], // @ts-ignore
|
|
19
|
+
stages: [], // @ts-ignore
|
|
20
|
+
triggers: [],
|
|
21
|
+
...overrides,
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
const update = (changes: any = {}) => {
|
|
25
|
+
pipeline = {
|
|
26
|
+
...pipeline,
|
|
27
|
+
...changes,
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
describe('enabling max concurrent', () => {
|
|
31
|
+
it('sets keepWaitingPipelines to true if limitConcurrent and keepWaitingPipelines are both not truthy', () => {
|
|
32
|
+
setPipeline();
|
|
33
|
+
const wrapper = mount(<ExecutionOptionsPageContent pipeline={pipeline} updatePipelineConfig={update} />);
|
|
34
|
+
expect(pipeline.keepWaitingPipelines).toBeFalsy();
|
|
35
|
+
const checkbox = wrapper.find('input').at(0);
|
|
36
|
+
checkbox.simulate('change', { target: { checked: true } });
|
|
37
|
+
expect(pipeline.keepWaitingPipelines).toBeTruthy();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('does not alter pipeline if limitConcurrent is true', () => {
|
|
41
|
+
setPipeline({ limitConcurrent: true });
|
|
42
|
+
const wrapper = mount(<ExecutionOptionsPageContent pipeline={pipeline} updatePipelineConfig={update} />);
|
|
43
|
+
expect(pipeline.keepWaitingPipelines).toBeFalsy();
|
|
44
|
+
const checkbox = wrapper.find('input').at(0);
|
|
45
|
+
checkbox.simulate('change', { target: { checked: true } });
|
|
46
|
+
expect(pipeline.keepWaitingPipelines).toBeFalsy();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('does not alter pipeline if keepWaitingPipelines is true', () => {
|
|
50
|
+
setPipeline({ keepWaitingPipelines: true });
|
|
51
|
+
const wrapper = mount(<ExecutionOptionsPageContent pipeline={pipeline} updatePipelineConfig={update} />);
|
|
52
|
+
expect(pipeline.keepWaitingPipelines).toBeTruthy();
|
|
53
|
+
const checkbox = wrapper.find('input').at(0);
|
|
54
|
+
checkbox.simulate('change', { target: { checked: true } });
|
|
55
|
+
expect(pipeline.keepWaitingPipelines).toBeTruthy();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('defaults the max concurrent value to 0', () => {
|
|
59
|
+
setPipeline();
|
|
60
|
+
const wrapper = mount(<ExecutionOptionsPageContent pipeline={pipeline} updatePipelineConfig={update} />);
|
|
61
|
+
const checkbox = wrapper.find('input').at(0);
|
|
62
|
+
checkbox.simulate('change', { target: { checked: true } });
|
|
63
|
+
const concurrentInput = wrapper.find('input[type="number"]').at(0);
|
|
64
|
+
expect(concurrentInput.prop('value')).toEqual(0);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('updates the max concurrent config value when the input is changed', () => {
|
|
69
|
+
setPipeline();
|
|
70
|
+
const value = 22;
|
|
71
|
+
const wrapper = mount(<ExecutionOptionsPageContent pipeline={pipeline} updatePipelineConfig={update} />);
|
|
72
|
+
const checkbox = wrapper.find('input').at(0);
|
|
73
|
+
checkbox.simulate('change', { target: { checked: true } });
|
|
74
|
+
const concurrentInput = wrapper.find('input[type="number"]').at(0);
|
|
75
|
+
concurrentInput.simulate('change', { target: { value } });
|
|
76
|
+
expect(pipeline.maxConcurrentExecutions).toEqual(value);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('sets the max concurrent value to a whole number if a float is entered', () => {
|
|
80
|
+
setPipeline();
|
|
81
|
+
const value = 3.3;
|
|
82
|
+
const wrapper = mount(<ExecutionOptionsPageContent pipeline={pipeline} updatePipelineConfig={update} />);
|
|
83
|
+
const checkbox = wrapper.find('input').at(0);
|
|
84
|
+
checkbox.simulate('change', { target: { checked: true } });
|
|
85
|
+
const concurrentInput = wrapper.find('input[type="number"]').at(0);
|
|
86
|
+
concurrentInput.simulate('change', { target: { value } });
|
|
87
|
+
expect(pipeline.maxConcurrentExecutions).toEqual(3);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
});
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import React from 'react';
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
2
|
|
|
3
3
|
import type { IPipeline } from '../../../domain';
|
|
4
4
|
import { HelpField } from '../../../help';
|
|
5
|
-
import { CheckboxInput, FormField } from '../../../presentation';
|
|
5
|
+
import { CheckboxInput, FormField, NumberInput } from '../../../presentation';
|
|
6
6
|
|
|
7
7
|
export interface IExecutionOptionsPageContentProps {
|
|
8
8
|
pipeline: IPipeline;
|
|
@@ -11,6 +11,13 @@ export interface IExecutionOptionsPageContentProps {
|
|
|
11
11
|
|
|
12
12
|
export function ExecutionOptionsPageContent(props: IExecutionOptionsPageContentProps) {
|
|
13
13
|
const { pipeline, updatePipelineConfig } = props;
|
|
14
|
+
const [currentMaxConcurrent, setCurrentMaxConcurrent] = useState(pipeline.maxConcurrentExecutions || 0);
|
|
15
|
+
const handleMaxConcurrentChange = (changeEvent: any) => {
|
|
16
|
+
const value = Number.parseInt(changeEvent.target.value);
|
|
17
|
+
setCurrentMaxConcurrent(value);
|
|
18
|
+
updatePipelineConfig({ maxConcurrentExecutions: value });
|
|
19
|
+
};
|
|
20
|
+
|
|
14
21
|
return (
|
|
15
22
|
<div className="row">
|
|
16
23
|
<div className="col-md-11 col-md-offset-1">
|
|
@@ -23,6 +30,10 @@ export function ExecutionOptionsPageContent(props: IExecutionOptionsPageContentP
|
|
|
23
30
|
)}
|
|
24
31
|
onChange={() => {
|
|
25
32
|
updatePipelineConfig({ limitConcurrent: !pipeline.limitConcurrent });
|
|
33
|
+
if (!pipeline.limitConcurrent && !pipeline.keepWaitingPipelines) {
|
|
34
|
+
pipeline.keepWaitingPipelines = true;
|
|
35
|
+
updatePipelineConfig({ keepWaitingPipelines: true });
|
|
36
|
+
}
|
|
26
37
|
}}
|
|
27
38
|
value={pipeline.limitConcurrent}
|
|
28
39
|
/>
|
|
@@ -45,6 +56,20 @@ export function ExecutionOptionsPageContent(props: IExecutionOptionsPageContentP
|
|
|
45
56
|
value={pipeline.keepWaitingPipelines}
|
|
46
57
|
/>
|
|
47
58
|
)}
|
|
59
|
+
{!pipeline.limitConcurrent && (
|
|
60
|
+
<div>
|
|
61
|
+
<label className="col-md-3 sm-label-right">
|
|
62
|
+
Maximum concurrent pipeline executions <HelpField id={'pipeline.config.parallel.max.concurrent'} />
|
|
63
|
+
</label>
|
|
64
|
+
<div className="col-md-8">
|
|
65
|
+
<FormField
|
|
66
|
+
input={(inputProps) => <NumberInput {...inputProps} min={0} max={65534} />}
|
|
67
|
+
onChange={handleMaxConcurrentChange}
|
|
68
|
+
value={currentMaxConcurrent}
|
|
69
|
+
/>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
)}
|
|
48
73
|
</div>
|
|
49
74
|
</div>
|
|
50
75
|
);
|
|
@@ -71,18 +71,33 @@ export class StageFailureMessage extends React.Component<IStageFailureMessagePro
|
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
public render() {
|
|
74
|
-
const { message, messages } = this.props;
|
|
74
|
+
const { message, messages, stage } = this.props;
|
|
75
75
|
const { isFailed, failedTask, failedExecutionId, failedStageName, failedStageId } = this.state;
|
|
76
|
-
|
|
76
|
+
|
|
77
|
+
let stageMessages = message || !messages.length ? [message] : messages;
|
|
78
|
+
if (stageMessages.length > 0) {
|
|
77
79
|
const exceptionTitle = isFailed ? (messages.length ? 'Exceptions' : 'Exception') : 'Warning';
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
);
|
|
80
|
+
|
|
81
|
+
// expression evaluation warnings can get really long and hide actual failure messages, source
|
|
82
|
+
// filter out expression evaluation failure messages if either:
|
|
83
|
+
// - there was a stage failure (and failed expressions don't fail the stage)
|
|
84
|
+
// - expression evaluation was explicitly disabled for the stage(as Orca still processes expressions and populates
|
|
85
|
+
// warnings when evaluation is disabled disabled)
|
|
86
|
+
const shouldFilterExpressionFailures =
|
|
87
|
+
(isFailed && !stage.context?.failOnFailedExpressions) || stage.context?.skipExpressionEvaluation;
|
|
88
|
+
|
|
89
|
+
if (shouldFilterExpressionFailures) {
|
|
90
|
+
stageMessages = stageMessages.filter((m) => !m.startsWith('Failed to evaluate'));
|
|
91
|
+
|
|
92
|
+
if (stageMessages.length === 0) {
|
|
93
|
+
// no messages to be displayed after filtering
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const displayMessages = stageMessages.map((m, i) => (
|
|
99
|
+
<Markdown key={i} message={m || StageFailureMessages.NO_REASON_PROVIDED} className="break-word" />
|
|
100
|
+
));
|
|
86
101
|
|
|
87
102
|
if (displayMessages) {
|
|
88
103
|
return (
|
|
@@ -136,6 +136,9 @@ angular
|
|
|
136
136
|
'createApplicationModal',
|
|
137
137
|
require('../../application/modal/newapplication.html'),
|
|
138
138
|
),
|
|
139
|
+
resolve: {
|
|
140
|
+
name: () => '',
|
|
141
|
+
},
|
|
139
142
|
controller: overrideRegistry.getController('CreateApplicationModalCtrl'),
|
|
140
143
|
controllerAs: 'newAppModal',
|
|
141
144
|
})
|