@spinnaker/core 0.27.0 → 0.29.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 +34 -0
- package/dist/config/settings.d.ts +9 -0
- package/dist/domain/IOrchestratedItem.d.ts +1 -0
- package/dist/index.js +36 -36
- package/dist/index.js.map +1 -1
- package/dist/notification/selector/types/cdevents/CDEventsNotificationType.d.ts +5 -0
- package/dist/notification/selector/types/cdevents/cdevents.notification.d.ts +2 -0
- package/dist/notification/selector/types/index.d.ts +1 -0
- package/dist/orchestratedItem/orchestratedItem.transformer.d.ts +5 -4
- package/dist/pipeline/config/stages/overrideTimeout/OverrideTimeout.d.ts +1 -1
- package/dist/presentation/forms/validation/validators.d.ts +2 -0
- package/dist/task/task.read.service.d.ts +1 -1
- package/package.json +3 -3
- package/src/config/settings.ts +7 -0
- package/src/domain/IOrchestratedItem.ts +1 -0
- package/src/manifest/stage/JobStageExecutionLogs.tsx +2 -2
- package/src/notification/modal/NotificationDetails.tsx +4 -1
- package/src/notification/notification.types.ts +2 -0
- package/src/notification/selector/types/cdevents/CDEventsNotificationType.tsx +36 -0
- package/src/notification/selector/types/cdevents/cdevents.notification.ts +8 -0
- package/src/notification/selector/types/index.ts +1 -0
- package/src/orchestratedItem/orchestratedItem.transformer.ts +21 -13
- package/src/pipeline/config/services/PipelineConfigService.ts +3 -0
- package/src/pipeline/config/stages/overrideTimeout/OverrideTimeout.tsx +24 -4
- package/src/pipeline/config/stages/wait/waitStage.ts +1 -0
- package/src/pipeline/config/validation/PipelineConfigValidator.ts +5 -1
- package/src/plugins/sharedLibraries.ts +5 -1
- package/src/presentation/CollapsibleElement.tsx +5 -3
- package/src/presentation/forms/validation/validators.ts +24 -1
- package/src/task/task.dataSource.js +18 -2
- package/src/task/task.read.service.ts +11 -2
- package/dist/presentation/forms/inputs/NumberConcurrencyInput.d.ts +0 -7
- package/src/presentation/forms/inputs/NumberConcurrencyInput.tsx +0 -29
|
@@ -2,11 +2,12 @@ export declare class OrchestratedItemTransformer {
|
|
|
2
2
|
static addRunningTime(item: any): void;
|
|
3
3
|
private static shouldReplace;
|
|
4
4
|
static defineProperties(item: any): void;
|
|
5
|
-
private static
|
|
6
|
-
private static
|
|
5
|
+
private static getFailureMessagesAsString;
|
|
6
|
+
private static getFailureMessages;
|
|
7
|
+
private static getOrchestrationExceptionMessage;
|
|
7
8
|
private static getLockFailureException;
|
|
8
|
-
private static
|
|
9
|
-
private static
|
|
9
|
+
private static getCustomExceptionMessage;
|
|
10
|
+
private static getGeneralExceptionMessages;
|
|
10
11
|
private static calculateRunningTime;
|
|
11
12
|
private static normalizeStatus;
|
|
12
13
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import type { IStage } from '../../../../domain';
|
|
3
3
|
export interface IOverrideTimeoutConfigProps {
|
|
4
4
|
stageConfig: IStageConfig;
|
|
5
|
-
stageTimeoutMs: number;
|
|
5
|
+
stageTimeoutMs: number | any;
|
|
6
6
|
updateStageField: (changes: Partial<IStage>) => void;
|
|
7
7
|
}
|
|
8
8
|
interface IStageConfig {
|
|
@@ -10,6 +10,8 @@ export declare const Validators: {
|
|
|
10
10
|
arrayNotEmpty: (message?: string) => IValidator;
|
|
11
11
|
checkBetween: (fieldName: string, min: number, max: number) => IValidator;
|
|
12
12
|
emailValue: (message?: string) => IValidator;
|
|
13
|
+
cdeventsTypeValue: (message?: string) => IValidator;
|
|
14
|
+
urlValue: (message?: string) => IValidator;
|
|
13
15
|
isNum: (message?: string) => IValidator;
|
|
14
16
|
isRequired: (message?: string) => IValidator;
|
|
15
17
|
isValidJson: (message?: string) => IValidator;
|
|
@@ -2,7 +2,7 @@ import type { Subject } from 'rxjs';
|
|
|
2
2
|
import type { ITask } from '../domain';
|
|
3
3
|
export declare class TaskReader {
|
|
4
4
|
private static activeStatuses;
|
|
5
|
-
static getTasks(applicationName: string, statuses?: string[]): PromiseLike<ITask[]>;
|
|
5
|
+
static getTasks(applicationName: string, statuses?: string[], limitPerPage?: number, page?: number): PromiseLike<ITask[]>;
|
|
6
6
|
static getRunningTasks(applicationName: string): PromiseLike<ITask[]>;
|
|
7
7
|
static getTask(taskId: string): PromiseLike<ITask>;
|
|
8
8
|
static waitUntilTaskMatches(task: ITask, closure: (task: ITask) => boolean, failureClosure?: (task: ITask) => boolean, interval?: number, notifier?: Subject<void>): PromiseLike<ITask>;
|
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.29.0",
|
|
5
5
|
"module": "dist/index.js",
|
|
6
6
|
"typings": "dist/index.d.ts",
|
|
7
7
|
"publishConfig": {
|
|
@@ -92,7 +92,7 @@
|
|
|
92
92
|
"@graphql-codegen/typescript-operations": "^1.18.3",
|
|
93
93
|
"@graphql-codegen/typescript-react-apollo": "^2.3.0",
|
|
94
94
|
"@spinnaker/eslint-plugin": "^3.0.2",
|
|
95
|
-
"@spinnaker/scripts": "^0.
|
|
95
|
+
"@spinnaker/scripts": "^0.4.0",
|
|
96
96
|
"@types/angular": "1.6.26",
|
|
97
97
|
"@types/angular-mocks": "1.5.10",
|
|
98
98
|
"@types/angular-ui-bootstrap": "0.13.41",
|
|
@@ -123,5 +123,5 @@
|
|
|
123
123
|
"shx": "0.3.3",
|
|
124
124
|
"typescript": "4.3.5"
|
|
125
125
|
},
|
|
126
|
-
"gitHead": "
|
|
126
|
+
"gitHead": "082a084908e13888d640e88a5bd6e1571479de14"
|
|
127
127
|
}
|
package/src/config/settings.ts
CHANGED
|
@@ -21,6 +21,7 @@ export interface INotificationSettings {
|
|
|
21
21
|
pubsub: { enabled: boolean };
|
|
22
22
|
slack: { botName: string; enabled: boolean };
|
|
23
23
|
sms: { enabled: boolean };
|
|
24
|
+
cdevents: { enabled: boolean };
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
export interface IFeatures {
|
|
@@ -39,6 +40,7 @@ export interface IFeatures {
|
|
|
39
40
|
mdGitIntegration?: boolean;
|
|
40
41
|
managedServiceAccounts?: boolean;
|
|
41
42
|
managedResources?: boolean;
|
|
43
|
+
multiBlockFailureMessages?: boolean;
|
|
42
44
|
notifications?: boolean;
|
|
43
45
|
pagerDuty?: boolean;
|
|
44
46
|
pipelines?: boolean;
|
|
@@ -140,6 +142,7 @@ export interface ISpinnakerSettings {
|
|
|
140
142
|
};
|
|
141
143
|
stashTriggerInfo?: string;
|
|
142
144
|
pollSchedule: number;
|
|
145
|
+
tasksViewLimitPerPage: number;
|
|
143
146
|
providers?: {
|
|
144
147
|
[key: string]: IProviderSettings; // allows custom providers not typed in here (good for testing too)
|
|
145
148
|
};
|
|
@@ -151,6 +154,10 @@ export interface ISpinnakerSettings {
|
|
|
151
154
|
useClassicFirewallLabels: boolean;
|
|
152
155
|
kubernetesAdHocInfraWritesEnabled: boolean;
|
|
153
156
|
changelogUrl: string;
|
|
157
|
+
cdevents?: {
|
|
158
|
+
validUrlPattern: string;
|
|
159
|
+
validCDEvent: string;
|
|
160
|
+
};
|
|
154
161
|
}
|
|
155
162
|
|
|
156
163
|
export const SETTINGS: ISpinnakerSettings = (window as any).spinnakerSettings || {};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { template } from 'lodash';
|
|
2
2
|
import React from 'react';
|
|
3
3
|
import { from as observableFrom, Subject } from 'rxjs';
|
|
4
4
|
import { takeUntil } from 'rxjs/operators';
|
|
@@ -52,7 +52,7 @@ export class JobStageExecutionLogs extends React.Component<IJobStageExecutionLog
|
|
|
52
52
|
const { manifest } = this.state;
|
|
53
53
|
const { externalLink, podNamesProviders, location, account } = this.props;
|
|
54
54
|
// prefer links to external logging platforms
|
|
55
|
-
if (
|
|
55
|
+
if (externalLink) {
|
|
56
56
|
return (
|
|
57
57
|
<a target="_blank" href={this.renderExternalLink(externalLink, manifest)}>
|
|
58
58
|
Console Output (External)
|
|
@@ -54,7 +54,10 @@ export class NotificationDetails extends React.Component<INotificationDetailsPro
|
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
private renderCustomMessage = (type: string, whenOption: string): React.ReactNode => {
|
|
57
|
-
if (
|
|
57
|
+
if (
|
|
58
|
+
whenOption !== 'manualJudgment' &&
|
|
59
|
+
['email', 'slack', 'googlechat', 'microsoftteams', 'cdevents'].includes(type)
|
|
60
|
+
) {
|
|
58
61
|
return (
|
|
59
62
|
<FormikFormField
|
|
60
63
|
name={`message["${whenOption}"].text`}
|
|
@@ -6,6 +6,7 @@ import type { INotificationTypeConfig } from '../domain';
|
|
|
6
6
|
import { Registry } from '../registry';
|
|
7
7
|
|
|
8
8
|
import { bearyChatNotification } from './selector/types/bearychat/beary.notification';
|
|
9
|
+
import { cdEventsNotification } from './selector/types/cdevents/cdevents.notification';
|
|
9
10
|
import { emailNotification } from './selector/types/email/email.notification';
|
|
10
11
|
import { githubstatusNotification } from './selector/types/githubstatus/githubstatus.notification';
|
|
11
12
|
import { googlechatNotification } from './selector/types/googlechat/googlechat.notification';
|
|
@@ -23,6 +24,7 @@ import { smsNotification } from './selector/types/sms/sms.notification';
|
|
|
23
24
|
pubsubNotification,
|
|
24
25
|
slackNotification,
|
|
25
26
|
smsNotification,
|
|
27
|
+
cdEventsNotification,
|
|
26
28
|
].forEach((config: INotificationTypeConfig) => {
|
|
27
29
|
if (SETTINGS.notifications) {
|
|
28
30
|
const notificationSetting: { enabled: boolean; botName?: string } =
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
import type { INotificationTypeCustomConfig } from '../../../../domain';
|
|
4
|
+
import { FormikFormField, TextInput, Validators } from '../../../../presentation';
|
|
5
|
+
|
|
6
|
+
export class CDEventsNotificationType extends React.Component<INotificationTypeCustomConfig> {
|
|
7
|
+
public render() {
|
|
8
|
+
const { fieldName } = this.props;
|
|
9
|
+
return (
|
|
10
|
+
<>
|
|
11
|
+
<FormikFormField
|
|
12
|
+
label="Events Broker URL"
|
|
13
|
+
name={fieldName ? `${fieldName}.address` : 'address'}
|
|
14
|
+
validate={Validators.skipIfSpel(Validators.urlValue('Please enter a valid URL'))}
|
|
15
|
+
input={(props) => (
|
|
16
|
+
<TextInput
|
|
17
|
+
inputClassName={'form-control input-sm'}
|
|
18
|
+
{...props}
|
|
19
|
+
placeholder="Enter an events message broker URL"
|
|
20
|
+
/>
|
|
21
|
+
)}
|
|
22
|
+
required={true}
|
|
23
|
+
/>
|
|
24
|
+
<FormikFormField
|
|
25
|
+
label="CDEvents Type"
|
|
26
|
+
name={fieldName ? `${fieldName}.cdEventsType` : 'cdEventsType'}
|
|
27
|
+
validate={Validators.skipIfSpel(Validators.cdeventsTypeValue('Please enter a valid CDEvents Type'))}
|
|
28
|
+
input={(props) => (
|
|
29
|
+
<TextInput inputClassName={'form-control input-sm'} {...props} placeholder="Enter a CDEvents type" />
|
|
30
|
+
)}
|
|
31
|
+
required={true}
|
|
32
|
+
/>
|
|
33
|
+
</>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { CDEventsNotificationType } from './CDEventsNotificationType';
|
|
2
|
+
import type { INotificationTypeConfig } from '../../../../domain';
|
|
3
|
+
|
|
4
|
+
export const cdEventsNotification: INotificationTypeConfig = {
|
|
5
|
+
component: CDEventsNotificationType,
|
|
6
|
+
key: 'cdevents',
|
|
7
|
+
label: 'CDEvents',
|
|
8
|
+
};
|
|
@@ -75,8 +75,11 @@ export class OrchestratedItemTransformer {
|
|
|
75
75
|
item.originalStatus = item.status;
|
|
76
76
|
|
|
77
77
|
Object.defineProperties(item, {
|
|
78
|
+
failureMessages: {
|
|
79
|
+
get: (): string[] => this.getFailureMessages(item),
|
|
80
|
+
},
|
|
78
81
|
failureMessage: {
|
|
79
|
-
get: (): string => this.
|
|
82
|
+
get: (): string => this.getFailureMessagesAsString(item),
|
|
80
83
|
},
|
|
81
84
|
isCompleted: {
|
|
82
85
|
get: (): boolean => ['SUCCEEDED', 'SKIPPED'].includes(item.status),
|
|
@@ -131,19 +134,23 @@ export class OrchestratedItemTransformer {
|
|
|
131
134
|
});
|
|
132
135
|
}
|
|
133
136
|
|
|
134
|
-
private static
|
|
135
|
-
const exceptions =
|
|
136
|
-
this.getCustomException(item),
|
|
137
|
-
this.getGeneralException(item),
|
|
138
|
-
this.getOrchestrationException(item),
|
|
139
|
-
].filter((it) => !!it);
|
|
137
|
+
private static getFailureMessagesAsString(item: any): string | null {
|
|
138
|
+
const exceptions = this.getFailureMessages(item);
|
|
140
139
|
if (exceptions.length === 0) {
|
|
141
140
|
return null;
|
|
142
141
|
}
|
|
143
142
|
return exceptions.join('\n\n');
|
|
144
143
|
}
|
|
145
144
|
|
|
146
|
-
private static
|
|
145
|
+
private static getFailureMessages(task: ITask): string[] {
|
|
146
|
+
return [
|
|
147
|
+
this.getCustomExceptionMessage(task),
|
|
148
|
+
...this.getGeneralExceptionMessages(task),
|
|
149
|
+
this.getOrchestrationExceptionMessage(task),
|
|
150
|
+
].filter((it) => !!it);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
private static getOrchestrationExceptionMessage(task: ITask): string {
|
|
147
154
|
const katoTasks: any[] = task.getValueFor('kato.tasks');
|
|
148
155
|
if (katoTasks && katoTasks.length) {
|
|
149
156
|
const failedTask: any = katoTasks.find((t) => t.status && t.status.failed);
|
|
@@ -190,7 +197,7 @@ export class OrchestratedItemTransformer {
|
|
|
190
197
|
return null;
|
|
191
198
|
}
|
|
192
199
|
|
|
193
|
-
private static
|
|
200
|
+
private static getCustomExceptionMessage(task: ITask): string {
|
|
194
201
|
const generalException: any = task.getValueFor('exception');
|
|
195
202
|
if (generalException) {
|
|
196
203
|
if (generalException.exceptionType && generalException.exceptionType === 'LockFailureException') {
|
|
@@ -200,16 +207,17 @@ export class OrchestratedItemTransformer {
|
|
|
200
207
|
return null;
|
|
201
208
|
}
|
|
202
209
|
|
|
203
|
-
private static
|
|
210
|
+
private static getGeneralExceptionMessages(task: ITask): string[] {
|
|
204
211
|
const generalException: any = task.getValueFor('exception');
|
|
205
212
|
if (generalException) {
|
|
206
213
|
const errors = (generalException.details?.errors ?? []).filter((m: any) => !!m);
|
|
207
214
|
if (errors.length) {
|
|
208
|
-
return errors
|
|
215
|
+
return errors;
|
|
209
216
|
}
|
|
210
|
-
|
|
217
|
+
|
|
218
|
+
return generalException.details?.error ? [generalException.details.error] : [];
|
|
211
219
|
}
|
|
212
|
-
return
|
|
220
|
+
return [];
|
|
213
221
|
}
|
|
214
222
|
|
|
215
223
|
private static calculateRunningTime(item: IOrchestratedItem): () => number {
|
|
@@ -142,6 +142,9 @@ export class PipelineConfigService {
|
|
|
142
142
|
private static groupStagesByRequisiteStageRefIds(pipeline: IPipeline) {
|
|
143
143
|
return pipeline.stages.reduce((acc, obj) => {
|
|
144
144
|
const parent = obj['refId'];
|
|
145
|
+
if (obj['requisiteStageRefIds'] === undefined) {
|
|
146
|
+
obj['requisiteStageRefIds'] = [];
|
|
147
|
+
}
|
|
145
148
|
obj.requisiteStageRefIds.forEach((child) => {
|
|
146
149
|
const values = acc.get(child);
|
|
147
150
|
if (values && values.length) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { get } from 'lodash';
|
|
1
|
+
import { get, isNumber } from 'lodash';
|
|
2
2
|
import { Duration } from 'luxon';
|
|
3
3
|
import React from 'react';
|
|
4
4
|
|
|
@@ -10,7 +10,7 @@ const { useEffect, useState } = React;
|
|
|
10
10
|
|
|
11
11
|
export interface IOverrideTimeoutConfigProps {
|
|
12
12
|
stageConfig: IStageConfig;
|
|
13
|
-
stageTimeoutMs: number;
|
|
13
|
+
stageTimeoutMs: number | any;
|
|
14
14
|
updateStageField: (changes: Partial<IStage>) => void;
|
|
15
15
|
}
|
|
16
16
|
|
|
@@ -41,8 +41,13 @@ export const OverrideTimeout = (props: IOverrideTimeoutConfigProps) => {
|
|
|
41
41
|
}, [props.stageTimeoutMs]);
|
|
42
42
|
|
|
43
43
|
const stageChanged = () => {
|
|
44
|
-
if (props.stageTimeoutMs !== undefined) {
|
|
44
|
+
if (props.stageTimeoutMs !== undefined && !isExpression) {
|
|
45
45
|
enableTimeout();
|
|
46
|
+
} else if (props.stageTimeoutMs !== undefined && isExpression) {
|
|
47
|
+
setOverrideTimeout(true);
|
|
48
|
+
props.updateStageField({
|
|
49
|
+
stageTimeoutMs: props.stageTimeoutMs || null,
|
|
50
|
+
});
|
|
46
51
|
} else {
|
|
47
52
|
clearTimeout();
|
|
48
53
|
}
|
|
@@ -74,6 +79,10 @@ export const OverrideTimeout = (props: IOverrideTimeoutConfigProps) => {
|
|
|
74
79
|
};
|
|
75
80
|
|
|
76
81
|
const isConfigurable = !!get(props.stageConfig, 'supportsCustomTimeout');
|
|
82
|
+
const isExpression =
|
|
83
|
+
props.stageTimeoutMs !== undefined && props.stageTimeoutMs !== null && !isNumber(props.stageTimeoutMs)
|
|
84
|
+
? props.stageTimeoutMs.includes('${')
|
|
85
|
+
: false;
|
|
77
86
|
|
|
78
87
|
if (isConfigurable) {
|
|
79
88
|
return (
|
|
@@ -94,7 +103,7 @@ export const OverrideTimeout = (props: IOverrideTimeoutConfigProps) => {
|
|
|
94
103
|
</div>
|
|
95
104
|
</div>
|
|
96
105
|
</div>
|
|
97
|
-
{overrideTimeout && (
|
|
106
|
+
{overrideTimeout && !isExpression && (
|
|
98
107
|
<div>
|
|
99
108
|
<div className="form-group form-inline">
|
|
100
109
|
<div className="col-md-9 col-md-offset-1 checkbox-padding">
|
|
@@ -123,6 +132,17 @@ export const OverrideTimeout = (props: IOverrideTimeoutConfigProps) => {
|
|
|
123
132
|
</div>
|
|
124
133
|
</div>
|
|
125
134
|
)}
|
|
135
|
+
{overrideTimeout && isExpression && (
|
|
136
|
+
<div className="form-group">
|
|
137
|
+
<div className="col-md-9 col-md-offset-1">
|
|
138
|
+
<div className="sm-control-field">
|
|
139
|
+
<span>
|
|
140
|
+
Resolved at runtime from expression: <code>{props.stageTimeoutMs}</code>
|
|
141
|
+
</span>
|
|
142
|
+
</div>
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
)}
|
|
126
146
|
</>
|
|
127
147
|
);
|
|
128
148
|
} else {
|
|
@@ -8,6 +8,7 @@ Registry.pipeline.registerStage({
|
|
|
8
8
|
label: 'Wait',
|
|
9
9
|
description: 'Waits a specified period of time',
|
|
10
10
|
key: 'wait',
|
|
11
|
+
restartable: true,
|
|
11
12
|
component: WaitStageConfig,
|
|
12
13
|
executionDetailsSections: [WaitExecutionDetails, ExecutionDetailsTasks],
|
|
13
14
|
executionLabelComponent: WaitExecutionLabel,
|
|
@@ -54,6 +54,10 @@ export interface ICustomValidator extends IStageOrTriggerValidator, IValidatorCo
|
|
|
54
54
|
[k: string]: any;
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
function isNumberOrSpel(valInput: any) {
|
|
58
|
+
return (isNumber(valInput) && valInput > 0) || (typeof valInput === 'string' && valInput.includes('${'));
|
|
59
|
+
}
|
|
60
|
+
|
|
57
61
|
export class PipelineConfigValidator {
|
|
58
62
|
private static validators: Map<string, IStageOrTriggerValidator> = new Map();
|
|
59
63
|
private static validationStream: Subject<IPipelineValidationResults> = new Subject();
|
|
@@ -151,7 +155,7 @@ export class PipelineConfigValidator {
|
|
|
151
155
|
);
|
|
152
156
|
}
|
|
153
157
|
|
|
154
|
-
if (stage.stageTimeoutMs !== undefined && !(
|
|
158
|
+
if (stage.stageTimeoutMs !== undefined && !isNumberOrSpel(stage.stageTimeoutMs)) {
|
|
155
159
|
stageValidations.set(stage, [
|
|
156
160
|
...(stageValidations.get(stage) || []),
|
|
157
161
|
'Stage is configured to fail after a specific amount of time, but no time is set.',
|
|
@@ -8,7 +8,6 @@ import * as reactDOM from 'react-dom';
|
|
|
8
8
|
import * as rxjs from 'rxjs';
|
|
9
9
|
|
|
10
10
|
import * as spinnakerCore from '../index';
|
|
11
|
-
|
|
12
11
|
export const sharedLibraries = {
|
|
13
12
|
// This is the global (window) variable that the shared libs will be exposed on
|
|
14
13
|
globalVariablePrefix: 'spinnaker.plugins.sharedLibraries',
|
|
@@ -30,7 +29,9 @@ export const sharedLibraries = {
|
|
|
30
29
|
if (destinationObject) {
|
|
31
30
|
// Temporarily expose @spinnaker/core.
|
|
32
31
|
// This should be removed at some point and replaced with a much smaller spinnaker/ui module which doesn't yet exist
|
|
32
|
+
exposeSharedLibrary('ajv', require('ajv'));
|
|
33
33
|
exposeSharedLibrary('@spinnaker/core', spinnakerCore);
|
|
34
|
+
exposeSharedLibrary('@spinnaker/kayenta', require('@spinnaker/kayenta'));
|
|
34
35
|
exposeSharedLibrary('@uirouter/core', uiRouterCore);
|
|
35
36
|
exposeSharedLibrary('@uirouter/react', uiRouterReact);
|
|
36
37
|
exposeSharedLibrary('@uirouter/rx', uiRouterRx);
|
|
@@ -38,6 +39,9 @@ export const sharedLibraries = {
|
|
|
38
39
|
exposeSharedLibrary('prop-types', propTypes);
|
|
39
40
|
exposeSharedLibrary('react', react);
|
|
40
41
|
exposeSharedLibrary('react-dom', reactDOM);
|
|
42
|
+
exposeSharedLibrary('react-redux', require('react-redux'));
|
|
43
|
+
exposeSharedLibrary('redux-actions', require('redux-actions'));
|
|
44
|
+
exposeSharedLibrary('reselect', require('reselect'));
|
|
41
45
|
exposeSharedLibrary('rxjs', rxjs);
|
|
42
46
|
exposeSharedLibrary('rxjs/Observable', { Observable: rxjs.Observable });
|
|
43
47
|
}
|
|
@@ -12,9 +12,11 @@ export const CollapsibleElement: React.FC<{ maxHeight: number }> = ({ children,
|
|
|
12
12
|
setIsOverflowing(contentRef.current.offsetHeight < contentRef.current.scrollHeight);
|
|
13
13
|
}, []);
|
|
14
14
|
|
|
15
|
-
React.
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
React.useLayoutEffect(() => {
|
|
16
|
+
setTimeout(() => {
|
|
17
|
+
checkIsOverflowing();
|
|
18
|
+
});
|
|
19
|
+
}, [checkIsOverflowing]);
|
|
18
20
|
|
|
19
21
|
React.useEffect(() => {
|
|
20
22
|
window.addEventListener('resize', checkIsOverflowing);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { isNumber } from 'lodash';
|
|
2
|
-
import { robotToHuman } from '../../robotToHumanFilter/robotToHuman.filter';
|
|
3
2
|
|
|
3
|
+
import { SETTINGS } from '../../../config/settings';
|
|
4
|
+
import { robotToHuman } from '../../robotToHumanFilter/robotToHuman.filter';
|
|
4
5
|
import type { IValidator } from './validation';
|
|
5
6
|
|
|
6
7
|
const THIS_FIELD = 'This field';
|
|
@@ -9,6 +10,12 @@ const VALID_EMAIL_REGEX = new RegExp(
|
|
|
9
10
|
'^(([^<>()\\[\\]\\\\.,;:\\s@"]+(\\.[^<>()\\[\\]\\\\.,;:\\s@"]+)*)|(".+"))@((\\[[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}])|(([a-zA-Z\\-0-9]+\\.)+[a-zA-Z]{2,}))$',
|
|
10
11
|
);
|
|
11
12
|
|
|
13
|
+
const urlPattern = SETTINGS.cdevents?.validUrlPattern ?? '^https?://.+$';
|
|
14
|
+
const VALID_URL = new RegExp(urlPattern);
|
|
15
|
+
|
|
16
|
+
const cdeventPattern = SETTINGS.cdevents?.validCDEvent ?? '^dev\\.cdevents\\.[^.]+\\.[^.]+$';
|
|
17
|
+
const VALID_CDEVENT_REGEX = new RegExp(cdeventPattern);
|
|
18
|
+
|
|
12
19
|
const emailValue = (message?: string): IValidator => {
|
|
13
20
|
return function emailValue(val: string, label = THIS_FIELD) {
|
|
14
21
|
message = message || `${label} is not a valid email address.`;
|
|
@@ -16,6 +23,20 @@ const emailValue = (message?: string): IValidator => {
|
|
|
16
23
|
};
|
|
17
24
|
};
|
|
18
25
|
|
|
26
|
+
const urlValue = (message?: string): IValidator => {
|
|
27
|
+
return function urlValue(val: string, label = THIS_FIELD) {
|
|
28
|
+
message = message || `${label} is not a valid URL.`;
|
|
29
|
+
return val && !VALID_URL.test(val) && message;
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const cdeventsTypeValue = (message?: string): IValidator => {
|
|
34
|
+
return function cdeventsTypeValue(val: string, label = THIS_FIELD) {
|
|
35
|
+
message = message || `${label} is not a valid CDEvents Type.`;
|
|
36
|
+
return val && !VALID_CDEVENT_REGEX.test(val) && message;
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
|
|
19
40
|
const isRequired = (message?: string): IValidator => {
|
|
20
41
|
return function isRequired(val: any, label = THIS_FIELD) {
|
|
21
42
|
message = message || `${label} is required.`;
|
|
@@ -138,6 +159,8 @@ export const Validators = {
|
|
|
138
159
|
arrayNotEmpty,
|
|
139
160
|
checkBetween,
|
|
140
161
|
emailValue,
|
|
162
|
+
cdeventsTypeValue,
|
|
163
|
+
urlValue,
|
|
141
164
|
isNum,
|
|
142
165
|
isRequired,
|
|
143
166
|
isValidJson,
|
|
@@ -2,6 +2,7 @@ import * as angular from 'angular';
|
|
|
2
2
|
|
|
3
3
|
import { ApplicationDataSourceRegistry } from '../application/service/ApplicationDataSourceRegistry';
|
|
4
4
|
import { CLUSTER_SERVICE } from '../cluster/cluster.service';
|
|
5
|
+
import { SETTINGS } from '../config';
|
|
5
6
|
import { TaskReader } from './task.read.service';
|
|
6
7
|
|
|
7
8
|
export const CORE_TASK_TASK_DATASOURCE = 'spinnaker.core.task.dataSource';
|
|
@@ -14,8 +15,23 @@ angular.module(CORE_TASK_TASK_DATASOURCE, [CLUSTER_SERVICE]).run([
|
|
|
14
15
|
return $q.when(angular.isArray(tasks) ? tasks : []);
|
|
15
16
|
};
|
|
16
17
|
|
|
17
|
-
const
|
|
18
|
-
|
|
18
|
+
const loadPaginatedTasks = async (application, page = 1) => {
|
|
19
|
+
let limitPerPage = SETTINGS.tasksViewLimitPerPage;
|
|
20
|
+
const tasks = await TaskReader.getTasks(application.name, [], limitPerPage, page);
|
|
21
|
+
if (tasks.length === limitPerPage) {
|
|
22
|
+
return tasks.concat(await loadPaginatedTasks(application, page + 1));
|
|
23
|
+
} else {
|
|
24
|
+
return tasks;
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const loadTasks = (application, page = 1) => {
|
|
29
|
+
let limitPerPage = SETTINGS.tasksViewLimitPerPage;
|
|
30
|
+
if (limitPerPage === undefined) {
|
|
31
|
+
return TaskReader.getTasks(application.name);
|
|
32
|
+
} else {
|
|
33
|
+
return loadPaginatedTasks(application, page);
|
|
34
|
+
}
|
|
19
35
|
};
|
|
20
36
|
|
|
21
37
|
const loadRunningTasks = (application) => {
|
|
@@ -8,10 +8,19 @@ import { OrchestratedItemTransformer } from '../orchestratedItem/orchestratedIte
|
|
|
8
8
|
export class TaskReader {
|
|
9
9
|
private static activeStatuses: string[] = ['RUNNING', 'SUSPENDED', 'NOT_STARTED'];
|
|
10
10
|
|
|
11
|
-
public static getTasks(
|
|
11
|
+
public static getTasks(
|
|
12
|
+
applicationName: string,
|
|
13
|
+
statuses: string[] = [],
|
|
14
|
+
limitPerPage: number = null,
|
|
15
|
+
page: number = null,
|
|
16
|
+
): PromiseLike<ITask[]> {
|
|
12
17
|
return REST('/applications')
|
|
13
18
|
.path(applicationName, 'tasks')
|
|
14
|
-
.query({
|
|
19
|
+
.query({
|
|
20
|
+
statuses: statuses.join(','),
|
|
21
|
+
limit: limitPerPage,
|
|
22
|
+
page: page,
|
|
23
|
+
})
|
|
15
24
|
.get()
|
|
16
25
|
.then((tasks: ITask[]) => {
|
|
17
26
|
tasks.forEach((task) => this.setTaskProperties(task));
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import type { IFormInputProps, OmitControlledInputPropsFrom } from './interface';
|
|
3
|
-
interface INumberInputProps extends IFormInputProps, OmitControlledInputPropsFrom<React.InputHTMLAttributes<any>> {
|
|
4
|
-
inputClassName?: string;
|
|
5
|
-
}
|
|
6
|
-
export declare function NumberConcurrencyInput(props: INumberInputProps): JSX.Element;
|
|
7
|
-
export {};
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
|
|
3
|
-
import { useInternalValidator } from './hooks';
|
|
4
|
-
import type { IFormInputProps, OmitControlledInputPropsFrom } from './interface';
|
|
5
|
-
import { orEmptyString, validationClassName } from './utils';
|
|
6
|
-
import type { IValidator } from '../validation';
|
|
7
|
-
import { composeValidators, Validators } from '../validation';
|
|
8
|
-
|
|
9
|
-
interface INumberInputProps extends IFormInputProps, OmitControlledInputPropsFrom<React.InputHTMLAttributes<any>> {
|
|
10
|
-
inputClassName?: string;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
const isNumber = (val: any): val is number => typeof val === 'number';
|
|
14
|
-
|
|
15
|
-
export function NumberConcurrencyInput(props: INumberInputProps) {
|
|
16
|
-
const { value, validation, inputClassName, ...otherProps } = props;
|
|
17
|
-
|
|
18
|
-
const minMaxValidator: IValidator = (val: any, label?: string) => {
|
|
19
|
-
const minValidator = isNumber(props.min) ? Validators.minValue(props.min) : undefined;
|
|
20
|
-
const maxValidator = isNumber(props.max) ? Validators.maxValue(props.max) : undefined;
|
|
21
|
-
const validator = composeValidators([minValidator, maxValidator]);
|
|
22
|
-
return validator ? validator(val, label) : null;
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
useInternalValidator(validation, minMaxValidator);
|
|
26
|
-
|
|
27
|
-
const className = `NumberInput form-control ${orEmptyString(inputClassName)} ${validationClassName(validation)}`;
|
|
28
|
-
return <input className={className} type="number" value={orEmptyString(value)} {...otherProps} />;
|
|
29
|
-
}
|