@spinnaker/core 0.25.0 → 0.27.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.
@@ -24,6 +24,7 @@ export declare class Executions extends React.Component<IExecutionsProps, IExecu
24
24
  private filterCountOptions;
25
25
  constructor(props: IExecutionsProps);
26
26
  private setReloadingForFilters;
27
+ private loadDefaultFilters;
27
28
  private clearFilters;
28
29
  private forceUpdateExecutionGroups;
29
30
  private updateExecutionGroups;
package/package.json CHANGED
@@ -1,9 +1,12 @@
1
1
  {
2
2
  "name": "@spinnaker/core",
3
3
  "license": "Apache-2.0",
4
- "version": "0.25.0",
4
+ "version": "0.27.0",
5
5
  "module": "dist/index.js",
6
6
  "typings": "dist/index.d.ts",
7
+ "publishConfig": {
8
+ "access": "public"
9
+ },
7
10
  "scripts": {
8
11
  "clean": "shx rm -rf dist",
9
12
  "prepublishOnly": "npm run build",
@@ -17,7 +20,7 @@
17
20
  "@apollo/client": "^3.6.9",
18
21
  "@fortawesome/fontawesome-free": "5.5.0",
19
22
  "@spinnaker/mocks": "1.0.7",
20
- "@spinnaker/presentation": "^0.3.0",
23
+ "@spinnaker/presentation": "^0.3.1",
21
24
  "@spinnaker/styleguide": "2.0.0",
22
25
  "@uirouter/angularjs": "1.0.26",
23
26
  "@uirouter/core": "6.0.8",
@@ -88,8 +91,8 @@
88
91
  "@graphql-codegen/typescript": "^1.22.4",
89
92
  "@graphql-codegen/typescript-operations": "^1.18.3",
90
93
  "@graphql-codegen/typescript-react-apollo": "^2.3.0",
91
- "@spinnaker/eslint-plugin": "^3.0.1",
92
- "@spinnaker/scripts": "^0.3.0",
94
+ "@spinnaker/eslint-plugin": "^3.0.2",
95
+ "@spinnaker/scripts": "^0.3.1",
93
96
  "@types/angular": "1.6.26",
94
97
  "@types/angular-mocks": "1.5.10",
95
98
  "@types/angular-ui-bootstrap": "0.13.41",
@@ -120,5 +123,5 @@
120
123
  "shx": "0.3.3",
121
124
  "typescript": "4.3.5"
122
125
  },
123
- "gitHead": "8de1e3c576a24ce9a62290f86bc52772df4842d2"
126
+ "gitHead": "217b75396c5ec25d9b2019a849593b0b8d52e916"
124
127
  }
@@ -8,6 +8,7 @@ import { CORE_APPLICATION_CONFIG_APPLICATIONSNAPSHOTSECTION_COMPONENT } from './
8
8
  import { CHAOS_MONKEY_CONFIG_COMPONENT } from '../../chaosMonkey/chaosMonkeyConfig.component';
9
9
  import { SETTINGS } from '../../config/settings';
10
10
  import { APPLICATION_DATA_SOURCE_EDITOR } from './dataSources/applicationDataSourceEditor.component';
11
+ import { DEFAULT_TAG_FILTER_CONFIG } from './defaultTagFilter/defaultTagFilterConfig.component';
11
12
  import { DELETE_APPLICATION_SECTION } from './deleteApplicationSection.module';
12
13
  import { CORE_APPLICATION_CONFIG_LINKS_APPLICATIONLINKS_COMPONENT } from './links/applicationLinks.component';
13
14
  import { ApplicationWriter } from '../service/ApplicationWriter';
@@ -25,6 +26,7 @@ module(CORE_APPLICATION_CONFIG_APPLICATIONCONFIG_CONTROLLER, [
25
26
  CHAOS_MONKEY_CONFIG_COMPONENT,
26
27
  TRAFFIC_GUARD_CONFIG_COMPONENT,
27
28
  CORE_APPLICATION_CONFIG_LINKS_APPLICATIONLINKS_COMPONENT,
29
+ DEFAULT_TAG_FILTER_CONFIG,
28
30
  ]).controller('ApplicationConfigController', [
29
31
  '$state',
30
32
  'app',
@@ -63,6 +65,30 @@ module(CORE_APPLICATION_CONFIG_APPLICATIONCONFIG_CONTROLLER, [
63
65
  });
64
66
  };
65
67
 
68
+ this.defaultTagFilterProps = {
69
+ isSaving: false,
70
+ saveError: false,
71
+ };
72
+ this.updateDefaultTagFilterConfigs = (tagConfigs /* IDefaultTagFilterConfig[] */) => {
73
+ const applicationAttributes = cloneDeep(this.application.attributes);
74
+ applicationAttributes.defaultFilteredTags = tagConfigs;
75
+ $scope.$applyAsync(() => {
76
+ this.defaultTagFilterProps.isSaving = true;
77
+ this.defaultTagFilterProps.saveError = false;
78
+ });
79
+ ApplicationWriter.updateApplication(applicationAttributes)
80
+ .then(() => {
81
+ $scope.$applyAsync(() => {
82
+ this.defaultTagFilterProps.isSaving = false;
83
+ this.application.attributes = applicationAttributes;
84
+ });
85
+ })
86
+ .catch(() => {
87
+ this.defaultTagFilterProps.isSaving = false;
88
+ this.defaultTagFilterProps.saveError = true;
89
+ });
90
+ };
91
+
66
92
  this.notifications = [];
67
93
  this.updateNotifications = (notifications) => {
68
94
  $scope.$applyAsync(() => {
@@ -54,6 +54,15 @@
54
54
  >
55
55
  </custom-banner-config>
56
56
  </page-section>
57
+ <page-section key="default-filters" label="Default Filters">
58
+ <default-tag-filter-config
59
+ default-tag-filter-configs="config.application.attributes.defaultFilteredTags"
60
+ is-saving="config.defaultTagFilterProps.isSaving"
61
+ save-error="config.defaultTagFilterProps.saveError"
62
+ update-default-tag-filter-configs="config.updateDefaultTagFilterConfigs"
63
+ >
64
+ </default-tag-filter-config>
65
+ </page-section>
57
66
  <page-section key="delete" label="Delete Application">
58
67
  <delete-application-section application="config.application"></delete-application-section>
59
68
  </page-section>
@@ -0,0 +1,75 @@
1
+ import { shallow } from 'enzyme';
2
+ import React from 'react';
3
+
4
+ import type { IDefaultTagFilterConfig } from './DefaultTagFilterConfig';
5
+ import { DefaultTagFilterConfig } from './DefaultTagFilterConfig';
6
+ import { noop } from '../../../utils';
7
+
8
+ describe('<DefaultTagFilterConfig />', () => {
9
+ let tagConfigs: IDefaultTagFilterConfig[];
10
+ let wrapper: any;
11
+
12
+ beforeEach(() => {
13
+ tagConfigs = getTestDefaultFilterTagConfigs();
14
+ wrapper = shallow(
15
+ <DefaultTagFilterConfig
16
+ defaultTagFilterConfigs={tagConfigs}
17
+ isSaving={false}
18
+ saveError={false}
19
+ updateDefaultTagFilterConfigs={noop}
20
+ />,
21
+ );
22
+ });
23
+
24
+ describe('view', () => {
25
+ it('renders a row for each banner config', () => {
26
+ expect(wrapper.find('.default-filter-config-row').length).toEqual(tagConfigs.length);
27
+ });
28
+ it('renders an "add" button', () => {
29
+ expect(wrapper.find('.add-new').length).toEqual(1);
30
+ });
31
+ });
32
+
33
+ describe('functionality', () => {
34
+ it('update default tag filter config', () => {
35
+ expect(wrapper.state('defaultTagFilterConfigsEditing')).toEqual(tagConfigs);
36
+ wrapper
37
+ .find('textarea')
38
+ .at(1)
39
+ .simulate('change', { target: { value: 'hello' } });
40
+ const updatedConfigs = [
41
+ {
42
+ ...tagConfigs[0],
43
+ tagValue: 'hello',
44
+ },
45
+ {
46
+ ...tagConfigs[1],
47
+ },
48
+ ];
49
+ expect(wrapper.state('defaultTagFilterConfigsEditing')).toEqual(updatedConfigs);
50
+ });
51
+ it('add default filter tag config', () => {
52
+ expect(wrapper.state('defaultTagFilterConfigsEditing').length).toEqual(2);
53
+ wrapper.find('.add-new').simulate('click');
54
+ expect(wrapper.state('defaultTagFilterConfigsEditing').length).toEqual(3);
55
+ });
56
+ it('remove default filter tag config', () => {
57
+ expect(wrapper.state('defaultTagFilterConfigsEditing').length).toEqual(2);
58
+ wrapper.find('.default-filter-config-remove').at(1).simulate('click');
59
+ expect(wrapper.state('defaultTagFilterConfigsEditing').length).toEqual(1);
60
+ });
61
+ });
62
+ });
63
+
64
+ export function getTestDefaultFilterTagConfigs(): IDefaultTagFilterConfig[] {
65
+ return [
66
+ {
67
+ tagName: 'Pipeline Type',
68
+ tagValue: 'Deployment Pipelines',
69
+ },
70
+ {
71
+ tagName: 'Pipeline Type',
72
+ tagValue: 'Repair Pipelines',
73
+ },
74
+ ];
75
+ }
@@ -0,0 +1,161 @@
1
+ import { isEqual } from 'lodash';
2
+ import React from 'react';
3
+
4
+ import { ConfigSectionFooter } from '../footer/ConfigSectionFooter';
5
+ import { noop } from '../../../utils';
6
+
7
+ import './defaultTagFilterConfig.less';
8
+
9
+ export interface IDefaultTagFilterConfig {
10
+ tagName: string;
11
+ tagValue: string;
12
+ }
13
+
14
+ export interface IDefaultTagFilterProps {
15
+ defaultTagFilterConfigs: IDefaultTagFilterConfig[];
16
+ isSaving: boolean;
17
+ saveError: boolean;
18
+ updateDefaultTagFilterConfigs: (defaultTagFilterConfigs: IDefaultTagFilterConfig[]) => void;
19
+ }
20
+
21
+ export interface IDefaultTagFilterState {
22
+ defaultTagFilterConfigsEditing: IDefaultTagFilterConfig[];
23
+ }
24
+
25
+ export class DefaultTagFilterConfig extends React.Component<IDefaultTagFilterProps, IDefaultTagFilterState> {
26
+ public static defaultProps: Partial<IDefaultTagFilterProps> = {
27
+ defaultTagFilterConfigs: [],
28
+ isSaving: false,
29
+ saveError: false,
30
+ updateDefaultTagFilterConfigs: noop,
31
+ };
32
+
33
+ constructor(props: IDefaultTagFilterProps) {
34
+ super(props);
35
+ this.state = {
36
+ defaultTagFilterConfigsEditing: props.defaultTagFilterConfigs,
37
+ };
38
+ }
39
+
40
+ private onTagNameChange = (idx: number, text: string) => {
41
+ this.setState({
42
+ defaultTagFilterConfigsEditing: this.state.defaultTagFilterConfigsEditing.map((config, i) => {
43
+ if (i === idx) {
44
+ return {
45
+ ...config,
46
+ tagName: text,
47
+ };
48
+ }
49
+ return config;
50
+ }),
51
+ });
52
+ };
53
+
54
+ private onTagValueChange = (idx: number, text: string) => {
55
+ this.setState({
56
+ defaultTagFilterConfigsEditing: this.state.defaultTagFilterConfigsEditing.map((config, i) => {
57
+ if (i === idx) {
58
+ return {
59
+ ...config,
60
+ tagValue: text,
61
+ };
62
+ }
63
+ return config;
64
+ }),
65
+ });
66
+ };
67
+
68
+ private addFilterTag = (): void => {
69
+ this.setState({
70
+ defaultTagFilterConfigsEditing: this.state.defaultTagFilterConfigsEditing.concat([
71
+ {
72
+ tagName: 'Name of the tag (E.g. Pipeline Type)',
73
+ tagValue: 'Value of the tag (E.g. Default Pipelines)',
74
+ } as IDefaultTagFilterConfig,
75
+ ]),
76
+ });
77
+ };
78
+
79
+ private removeFilterTag = (idx: number): void => {
80
+ this.setState({
81
+ defaultTagFilterConfigsEditing: this.state.defaultTagFilterConfigsEditing.filter((_config, i) => i !== idx),
82
+ });
83
+ };
84
+
85
+ private isDirty = (): boolean => {
86
+ return !isEqual(this.props.defaultTagFilterConfigs, this.state.defaultTagFilterConfigsEditing);
87
+ };
88
+
89
+ private onRevertClicked = (): void => {
90
+ this.setState({
91
+ defaultTagFilterConfigsEditing: this.props.defaultTagFilterConfigs,
92
+ });
93
+ };
94
+
95
+ private onSaveClicked = (): void => {
96
+ this.props.updateDefaultTagFilterConfigs(this.state.defaultTagFilterConfigsEditing);
97
+ };
98
+
99
+ public render() {
100
+ return (
101
+ <div className="default-filter-config-container">
102
+ <div className="default-filter-config-description">
103
+ Default Tag filters allow you to specify which tags are immediately filtered to when the pipeline execution
104
+ page is loaded in.
105
+ </div>
106
+ <div className="col-md-10 col-md-offset-1">
107
+ <table className="table table-condensed">
108
+ <thead>
109
+ <tr>
110
+ <th>Tag Name</th>
111
+ <th>Tag Value</th>
112
+ </tr>
113
+ </thead>
114
+ <tbody>
115
+ {this.state.defaultTagFilterConfigsEditing.map((defaultTagFilter, idx) => (
116
+ <tr key={idx} className="default-filter-config-row">
117
+ <td>
118
+ <textarea
119
+ className="form-control input-sm"
120
+ onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) =>
121
+ this.onTagNameChange(idx, e.target.value)
122
+ }
123
+ value={defaultTagFilter.tagName}
124
+ />
125
+ </td>
126
+ <td>
127
+ <textarea
128
+ className="form-control input-sm"
129
+ onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) =>
130
+ this.onTagValueChange(idx, e.target.value)
131
+ }
132
+ value={defaultTagFilter.tagValue}
133
+ />
134
+ </td>
135
+ <td>
136
+ <button className="link default-filter-config-remove" onClick={() => this.removeFilterTag(idx)}>
137
+ <span className="glyphicon glyphicon-trash" />
138
+ </button>
139
+ </td>
140
+ </tr>
141
+ ))}
142
+ </tbody>
143
+ </table>
144
+ </div>
145
+ <div className="col-md-10 col-md-offset-1">
146
+ <button className="btn btn-block add-new" onClick={this.addFilterTag}>
147
+ <span className="glyphicon glyphicon-plus-sign" /> Add Default Filter
148
+ </button>
149
+ </div>
150
+ <ConfigSectionFooter
151
+ isDirty={this.isDirty()}
152
+ isValid={true}
153
+ isSaving={this.props.isSaving}
154
+ saveError={false}
155
+ onRevertClicked={this.onRevertClicked}
156
+ onSaveClicked={this.onSaveClicked}
157
+ />
158
+ </div>
159
+ );
160
+ }
161
+ }
@@ -0,0 +1,15 @@
1
+ import { module } from 'angular';
2
+ import { react2angular } from 'react2angular';
3
+ import { DefaultTagFilterConfig } from './DefaultTagFilterConfig';
4
+ import { withErrorBoundary } from '../../../presentation/SpinErrorBoundary';
5
+
6
+ export const DEFAULT_TAG_FILTER_CONFIG = 'spinnaker.micros.application.defaultTagFilterConfig.component';
7
+ module(DEFAULT_TAG_FILTER_CONFIG, []).component(
8
+ 'defaultTagFilterConfig',
9
+ react2angular(withErrorBoundary(DefaultTagFilterConfig, 'defaultTagFilterConfig'), [
10
+ 'defaultTagFilterConfigs',
11
+ 'isSaving',
12
+ 'saveError',
13
+ 'updateDefaultTagFilterConfigs',
14
+ ]),
15
+ );
@@ -0,0 +1,8 @@
1
+ .default-filter-config-container {
2
+ display: flex;
3
+ flex-direction: column;
4
+ }
5
+
6
+ .default-filter-config-description {
7
+ margin-bottom: 10px;
8
+ }
@@ -10,6 +10,7 @@ import {
10
10
  StageArtifactSelectorDelegate,
11
11
  } from '../../../../../artifact';
12
12
  import { StageConfigField } from '../../common/stageConfigField/StageConfigField';
13
+ import { SETTINGS } from '../../../../../config';
13
14
  import type { IArtifact, IExpectedArtifact } from '../../../../../domain';
14
15
  import { MapEditor } from '../../../../../forms';
15
16
  import { CheckboxInput, TextInput } from '../../../../../presentation';
@@ -152,9 +153,30 @@ export class BakeHelmConfigForm extends React.Component<IFormikStageConfigInject
152
153
 
153
154
  public render() {
154
155
  const stage = this.props.formik.values;
156
+ const enableApiVersions = SETTINGS.feature.helmApiVersions;
155
157
  return (
156
158
  <>
157
159
  <h4>Helm Options</h4>
160
+ {enableApiVersions && ( // Only render if enableApiVersions is true
161
+ <>
162
+ <StageConfigField fieldColumns={3} label={'ApiVersions'}>
163
+ <TextInput
164
+ onChange={(e: React.ChangeEvent<any>) => {
165
+ this.props.formik.setFieldValue('apiVersions', e.target.value);
166
+ }}
167
+ value={stage.apiVersions}
168
+ />
169
+ </StageConfigField>
170
+ <StageConfigField fieldColumns={3} label={'KubeVersion'}>
171
+ <TextInput
172
+ onChange={(e: React.ChangeEvent<any>) => {
173
+ this.props.formik.setFieldValue('kubeVersion', e.target.value);
174
+ }}
175
+ value={stage.kubeVersion}
176
+ />
177
+ </StageConfigField>
178
+ </>
179
+ )}
158
180
  <StageConfigField fieldColumns={3} label={'Name'}>
159
181
  <TextInput
160
182
  onChange={(e: React.ChangeEvent<any>) => {
@@ -73,33 +73,19 @@ export class StageFailureMessage extends React.Component<IStageFailureMessagePro
73
73
  }
74
74
 
75
75
  public render() {
76
- const { message, messages, stage } = this.props;
76
+ const { message, messages } = this.props;
77
77
  const { isFailed, failedTask, failedExecutionId, failedStageName, failedStageId } = this.state;
78
78
 
79
- let stageMessages = message && !messages.length ? [message] : messages;
80
- if (stageMessages.length > 0) {
79
+ if (isFailed || failedTask || message || messages.length) {
81
80
  const exceptionTitle = isFailed ? (messages.length ? 'Exceptions' : 'Exception') : 'Warning';
82
-
83
- // expression evaluation warnings can get really long and hide actual failure messages, source
84
- // filter out expression evaluation failure messages if either:
85
- // - there was a stage failure (and failed expressions don't fail the stage)
86
- // - expression evaluation was explicitly disabled for the stage(as Orca still processes expressions and populates
87
- // warnings when evaluation is disabled disabled)
88
- const shouldFilterExpressionFailures =
89
- (isFailed && !stage.context?.failOnFailedExpressions) || stage.context?.skipExpressionEvaluation;
90
-
91
- if (shouldFilterExpressionFailures) {
92
- stageMessages = stageMessages.filter((m) => !m.startsWith('Failed to evaluate'));
93
-
94
- if (stageMessages.length === 0) {
95
- // no messages to be displayed after filtering
96
- return null;
97
- }
98
- }
99
-
100
- const displayMessages = stageMessages.map((m, i) => (
101
- <Markdown key={i} message={m || StageFailureMessages.NO_REASON_PROVIDED} className="break-word" />
102
- ));
81
+ const displayMessages =
82
+ message || !messages.length ? (
83
+ <Markdown message={message || StageFailureMessages.NO_REASON_PROVIDED} className="break-word" />
84
+ ) : (
85
+ messages.map((m, i) => (
86
+ <Markdown key={i} message={m || StageFailureMessages.NO_REASON_PROVIDED} className="break-word" />
87
+ ))
88
+ );
103
89
 
104
90
  if (displayMessages) {
105
91
  return (
@@ -4,6 +4,7 @@ import React from 'react';
4
4
  import type { Subscription } from 'rxjs';
5
5
 
6
6
  import type { Application } from '../../application';
7
+ import type { IDefaultTagFilterConfig } from '../../application/config/defaultTagFilter/DefaultTagFilterConfig';
7
8
  import { CreatePipeline } from '../config/CreatePipeline';
8
9
  import { CreatePipelineButton } from '../create/CreatePipelineButton';
9
10
  import type { IExecution, IPipeline, IPipelineCommand } from '../../domain';
@@ -74,6 +75,16 @@ export class Executions extends React.Component<IExecutionsProps, IExecutionsSta
74
75
  }
75
76
  };
76
77
 
78
+ private loadDefaultFilters = (): void => {
79
+ const defaultTags = this.props.app.attributes.defaultFilteredTags;
80
+ if (defaultTags != null) {
81
+ this.props.app.attributes.defaultFilteredTags.forEach((defaultTag: IDefaultTagFilterConfig) => {
82
+ ExecutionState.filterModel.asFilterModel.sortFilter.tags[`${defaultTag.tagName}:${defaultTag.tagValue}`] = true;
83
+ });
84
+ this.updateExecutionGroups(true);
85
+ }
86
+ };
87
+
77
88
  private clearFilters = (): void => {
78
89
  ExecutionFilterService.clearFilters();
79
90
  this.updateExecutionGroups(true);
@@ -235,6 +246,8 @@ export class Executions extends React.Component<IExecutionsProps, IExecutionsSta
235
246
  () => this.dataInitializationFailure(),
236
247
  );
237
248
 
249
+ this.loadDefaultFilters();
250
+
238
251
  $q.all([app.executions.ready(), app.pipelineConfigs.ready()]).then(() => {
239
252
  this.updateExecutionGroups();
240
253
  const nameOrIdToStart = ReactInjector.$stateParams.startManualExecution;
@@ -7,7 +7,7 @@ interface IExecutionCancellationReasonProps {
7
7
  }
8
8
 
9
9
  export function ExecutionCancellationReason({ cancellationReason }: IExecutionCancellationReasonProps) {
10
- const [isExpanded, setIsExpanded] = React.useState(false);
10
+ const [isExpanded, setIsExpanded] = React.useState(true);
11
11
  return (
12
12
  <>
13
13
  <div className="execution-cancellation-reason-button">