@spinnaker/core 2025.0.6 → 2025.1.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/dist/api/ApiService.d.ts +2 -2
- package/dist/api/mock/mockHttpUtils.d.ts +2 -2
- package/dist/application/listExtractor/AppListExtractor.d.ts +2 -2
- package/dist/bootstrap/paramChangedHelper.d.ts +1 -1
- package/dist/cloudProvider/providerSelection/ProviderSelectionService.d.ts +1 -1
- package/dist/cluster/filter/ClusterFilterService.d.ts +1 -1
- package/dist/cluster/task.matcher.d.ts +1 -1
- package/dist/domain/IManagedEntity.d.ts +8 -8
- package/dist/domain/IServerGroup.d.ts +1 -0
- package/dist/domain/IStageTypeConfig.d.ts +1 -1
- package/dist/entityTag/notifications/NotificationsPopover.d.ts +1 -1
- package/dist/image/image.reader.d.ts +2 -2
- package/dist/index.js +1726 -1521
- package/dist/index.js.map +1 -1
- package/dist/managed/config/Configuration.d.ts +1 -1
- package/dist/managed/config/GitIntegration.d.ts +1 -1
- package/dist/managed/constraints/registry.d.ts +2 -2
- package/dist/managed/graphql/graphql-sdk.d.ts +137 -137
- package/dist/managed/managed.states.d.ts +1 -1
- package/dist/managed/overview/artifact/ArtifactActionModal.d.ts +1 -1
- package/dist/managed/overview/artifact/VersionOperation.d.ts +3 -3
- package/dist/managed/overview/artifact/utils.d.ts +2 -2
- package/dist/managed/overview/types.d.ts +9 -9
- package/dist/managed/resourceHistory/ManagedResourceHistoryModal.d.ts +1 -1
- package/dist/managed/resources/ResourceDefinitionModal.d.ts +1 -1
- package/dist/managed/resources/resourceRegistry.d.ts +1 -1
- package/dist/managed/versionMetadata/MetadataComponents.d.ts +1 -1
- package/dist/managed/versionsHistory/types.d.ts +6 -6
- package/dist/manifest/ManifestYaml.d.ts +1 -1
- package/dist/modal/wizard/WizardPage.d.ts +1 -1
- package/dist/navigation/urlParser.d.ts +1 -1
- package/dist/pagerDuty/Pager.d.ts +1 -1
- package/dist/pipeline/config/actions/pipelineJson/EditPipelineJsonModal.d.ts +1 -1
- package/dist/pipeline/config/stages/FormikStageConfig.d.ts +2 -2
- package/dist/pipeline/config/stages/bakeManifest/helm/BakeHelmConfigForm.d.ts +1 -0
- package/dist/pipeline/config/stages/common/ExecutionDetailsSection.d.ts +1 -1
- package/dist/pipeline/config/stages/entityTags/TagEditor.d.ts +1 -1
- package/dist/pipeline/config/templates/PipelineTemplateReader.d.ts +1 -1
- package/dist/pipeline/config/triggers/artifacts/helm-image/HelmImageArtifactEditor.d.ts +3 -0
- package/dist/pipeline/config/validation/anyFieldRequired.validator.d.ts +1 -1
- package/dist/pipeline/config/validation/requiredField.validator.d.ts +1 -1
- package/dist/plugins/plugin.registry.d.ts +1 -1
- package/dist/presentation/Placement.d.ts +1 -1
- package/dist/presentation/Popover.d.ts +1 -1
- package/dist/presentation/details/Details.d.ts +7 -0
- package/dist/presentation/forms/fields/FormField.d.ts +1 -1
- package/dist/presentation/forms/fields/FormikExpressionField.d.ts +1 -1
- package/dist/presentation/forms/fields/FormikExpressionRegexField.d.ts +1 -1
- package/dist/presentation/forms/fields/FormikFormField.d.ts +1 -1
- package/dist/presentation/forms/inputs/interface.d.ts +2 -2
- package/dist/presentation/forms/validation/categories.d.ts +3 -3
- package/dist/presentation/forms/validation/validation.d.ts +3 -3
- package/dist/presentation/hooks/useLatestPromise.hook.d.ts +1 -1
- package/dist/presentation/modal/showModal.d.ts +1 -1
- package/dist/presentation/tables/Table.d.ts +1 -1
- package/dist/presentation/tables/TableCell.d.ts +1 -1
- package/dist/presentation/tables/TableRow.d.ts +1 -1
- package/dist/presentation/tables/standardGridTableLayout.d.ts +1 -1
- package/dist/projects/Projects.d.ts +2 -0
- package/dist/projects/index.d.ts +1 -0
- package/dist/projects/projects.module.d.ts +3 -2
- package/dist/reactShims/AngularJSAdapter.d.ts +3 -3
- package/dist/search/infrastructure/SearchResultPods.d.ts +1 -1
- package/dist/search/infrastructure/infrastructureSearch.service.d.ts +1 -1
- package/dist/serverGroup/details/ServerGroupDetailsWrapper.d.ts +1 -1
- package/dist/serverGroupManager/index.d.ts +1 -0
- package/dist/serverGroupManager/serverGroupManager.states.d.ts +1 -1
- package/dist/utils/Logger.d.ts +1 -1
- package/dist/utils/feature/Feature.d.ts +58 -0
- package/dist/utils/feature/FeatureContext.d.ts +24 -0
- package/dist/utils/feature/index.d.ts +3 -0
- package/dist/utils/feature/useFeature.hook.d.ts +15 -0
- package/dist/utils/index.d.ts +2 -0
- package/dist/utils/json/traverseObject.d.ts +1 -1
- package/dist/utils/parseNum.d.ts +1 -0
- package/dist/utils/testUtils/index.d.ts +7 -0
- package/dist/utils/workerPool.d.ts +1 -1
- package/package.json +3 -3
- package/src/artifact/ArtifactIconService.ts +1 -0
- package/src/artifact/ArtifactTypes.ts +1 -0
- package/src/artifact/ExpectedArtifactSelectorViewController.ts +1 -1
- package/src/config/VersionChecker.tsx +1 -1
- package/src/domain/IServerGroup.ts +1 -0
- package/src/help/help.contents.ts +1 -1
- package/src/navigation/UrlBuilder.ts +15 -0
- package/src/notification/NotificationsList.tsx +12 -8
- package/src/pipeline/config/stages/bakeManifest/helm/BakeHelmConfigForm.tsx +8 -2
- package/src/pipeline/config/triggers/artifacts/helm-image/HelmImageArtifactEditor.tsx +169 -0
- package/src/pipeline/config/triggers/artifacts/index.ts +3 -0
- package/src/pipeline/config/validation/PipelineConfigValidator.ts +2 -2
- package/src/pipeline/executions/executionGroup/ExecutionGroups.tsx +37 -2
- package/src/pipeline/filter/executionFilter.service.ts +50 -5
- package/src/presentation/details/Details.tsx +18 -1
- package/src/presentation/forms/FormikForm.tsx +1 -1
- package/src/projects/ProjectHeader.tsx +12 -9
- package/src/projects/Projects.spec.tsx +141 -0
- package/src/projects/Projects.tsx +148 -0
- package/src/projects/index.ts +1 -0
- package/src/projects/{projects.module.js → projects.module.ts} +0 -2
- package/src/projects/projects.states.ts +4 -6
- package/src/serverGroup/details/ServerGroupDetails.tsx +1 -1
- package/src/serverGroupManager/ServerGroupManager.tsx +2 -0
- package/src/serverGroupManager/ServerGroupManagerTag.tsx +1 -1
- package/src/serverGroupManager/index.ts +1 -0
- package/src/serverGroupManager/serverGroupManager.states.ts +3 -3
- package/src/utils/feature/Feature.tsx +98 -0
- package/src/utils/feature/FeatureContext.tsx +49 -0
- package/src/utils/feature/index.ts +3 -0
- package/src/utils/feature/useFeature.hook.tsx +25 -0
- package/src/utils/index.ts +2 -0
- package/src/utils/parseNum.ts +2 -0
- package/src/utils/testUtils/index.tsx +30 -0
- package/dist/projects/projects.controller.d.ts +0 -2
- package/src/projects/projects.controller.js +0 -112
- package/src/projects/projects.controller.spec.js +0 -86
- package/src/projects/projects.html +0 -87
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import type { IFeatures } from '../../config';
|
|
3
|
+
/**
|
|
4
|
+
* Type alias for feature flags, extending from IFeatures interface.
|
|
5
|
+
*/
|
|
6
|
+
export type FeatureFlags = IFeatures;
|
|
7
|
+
/**
|
|
8
|
+
* Creates a React Context for feature flags.
|
|
9
|
+
* Defaults to `SETTINGS.feature` if no provider is found in the component tree.
|
|
10
|
+
*/
|
|
11
|
+
export declare const FeatureFlagsContext: React.Context<IFeatures>;
|
|
12
|
+
/**
|
|
13
|
+
* A context provider component that merges the global feature flags
|
|
14
|
+
* from `SETTINGS.feature` with any feature flags passed in via props.
|
|
15
|
+
*
|
|
16
|
+
* @param {Object} props
|
|
17
|
+
* @param {FeatureFlags} [props.features] - Feature flags to merge with the global flags.
|
|
18
|
+
* @param {React.ReactNode} props.children - Components that will consume the merged flags.
|
|
19
|
+
* @returns {JSX.Element} - The provider that holds merged feature flags in context.
|
|
20
|
+
*/
|
|
21
|
+
export declare function FeaturesProvider({ features, children, }: {
|
|
22
|
+
features?: FeatureFlags;
|
|
23
|
+
children: React.ReactNode;
|
|
24
|
+
}): JSX.Element;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { FeatureFlags } from './FeatureContext';
|
|
2
|
+
/**
|
|
3
|
+
* A hook to access the current set of feature flags from the context.
|
|
4
|
+
* If no provider is found, it will default to `SETTINGS.feature`.
|
|
5
|
+
*
|
|
6
|
+
* @returns {FeatureFlags} - The feature flags currently in the context.
|
|
7
|
+
*/
|
|
8
|
+
export declare function useFeatures(): FeatureFlags;
|
|
9
|
+
/**
|
|
10
|
+
* A hook to determine if a particular feature is enabled.
|
|
11
|
+
*
|
|
12
|
+
* @param {keyof FeatureFlags} feature - The name of the feature flag to check.
|
|
13
|
+
* @returns {boolean} - True if the feature is enabled; otherwise false.
|
|
14
|
+
*/
|
|
15
|
+
export declare function useFeature(feature: keyof FeatureFlags): boolean;
|
package/dist/utils/index.d.ts
CHANGED
|
@@ -12,6 +12,7 @@ export * from './json/traverseObject';
|
|
|
12
12
|
export * from './noop';
|
|
13
13
|
export * from './q';
|
|
14
14
|
export * from './renderIfFeature.component';
|
|
15
|
+
export * from './feature';
|
|
15
16
|
export * from './scrollTo/scrollTo.service';
|
|
16
17
|
export * from './timeFormatters';
|
|
17
18
|
export * from './unicodeBase64';
|
|
@@ -19,3 +20,4 @@ export * from './uuid.service';
|
|
|
19
20
|
export * from './workerPool';
|
|
20
21
|
export * from './json/filterObjectValues';
|
|
21
22
|
export * from './Logger';
|
|
23
|
+
export * from './parseNum';
|
|
@@ -10,5 +10,5 @@
|
|
|
10
10
|
* i.e., properties that are neither a nested object, nor an array.
|
|
11
11
|
*/
|
|
12
12
|
export declare const traverseObject: (object: object, callback: ITraverseCallback, traverseLeafNodesOnly?: boolean) => void;
|
|
13
|
-
|
|
13
|
+
type ITraverseCallback = (path: string, obj: object) => void;
|
|
14
14
|
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const parseNum: (numOrStr: number | string) => number;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Create a minimal UIRouter environment so components can render RouteLinks, etc.
|
|
4
|
+
*/
|
|
5
|
+
export declare const wrapWithRouter: (node: React.ReactElement) => JSX.Element;
|
|
6
|
+
/** Helper to DRY mounting + async flush + update. */
|
|
7
|
+
export declare function mountAndFlush(element: React.ReactElement): Promise<import("enzyme").ReactWrapper<any, Readonly<{}>, React.Component<{}, {}, any>>>;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spinnaker/core",
|
|
3
3
|
"license": "Apache-2.0",
|
|
4
|
-
"version": "2025.0
|
|
4
|
+
"version": "2025.1.0",
|
|
5
5
|
"module": "dist/index.js",
|
|
6
6
|
"typings": "dist/index.d.ts",
|
|
7
7
|
"publishConfig": {
|
|
@@ -122,7 +122,7 @@
|
|
|
122
122
|
"graphql": "^15.5.1",
|
|
123
123
|
"rollup-plugin-copy": "^3.4.0",
|
|
124
124
|
"shx": "0.3.3",
|
|
125
|
-
"typescript": "
|
|
125
|
+
"typescript": "5.0.4"
|
|
126
126
|
},
|
|
127
|
-
"gitHead": "
|
|
127
|
+
"gitHead": "ed6c0f8929f68fcb6295c4d56c4bfbdec8d21ce6"
|
|
128
128
|
}
|
|
@@ -57,3 +57,4 @@ ArtifactIconService.registerType(ArtifactTypePatterns.JENKINS_FILE, jenkinsFileI
|
|
|
57
57
|
ArtifactIconService.registerType(ArtifactTypePatterns.MAVEN_FILE, mavenFileIcon);
|
|
58
58
|
ArtifactIconService.registerType(ArtifactTypePatterns.HTTP_FILE, httpFileIcon);
|
|
59
59
|
ArtifactIconService.registerType(ArtifactTypePatterns.ORACLE_OBJECT, oracleObjectIcon);
|
|
60
|
+
ArtifactIconService.registerType(ArtifactTypePatterns.HELM_IMAGE, helmChartIcon);
|
|
@@ -22,6 +22,7 @@ export const ArtifactTypePatterns: IArtifactTypePatterns = {
|
|
|
22
22
|
HTTP_FILE: /http\/file/,
|
|
23
23
|
FRONT50_PIPELINE_TEMPLATE: /front50\/pipelineTemplate/,
|
|
24
24
|
ORACLE_OBJECT: /oracle\/object/,
|
|
25
|
+
HELM_IMAGE: /helm\/image/,
|
|
25
26
|
};
|
|
26
27
|
|
|
27
28
|
export const excludeAllTypesExcept = (...types: RegExp[]) =>
|
|
@@ -31,7 +31,7 @@ export class ExpectedArtifactSelectorViewController {
|
|
|
31
31
|
const artifact = ExpectedArtifactService.artifactFromExpected(expectedArtifact);
|
|
32
32
|
const allAccounts = this.delegate.getExpectedArtifactAccounts();
|
|
33
33
|
this.accountsForArtifact =
|
|
34
|
-
artifact.type === 'helm/chart'
|
|
34
|
+
artifact.type === 'helm/chart' || artifact.type === 'helm/image'
|
|
35
35
|
? allAccounts.filter((a) => a.types.includes(artifact.type) && a.name === artifact.artifactAccount)
|
|
36
36
|
: allAccounts.filter((a) => a.types.includes(artifact.type));
|
|
37
37
|
const selected = this.delegate.getSelectedAccount();
|
|
@@ -46,7 +46,7 @@ export class VersionChecker {
|
|
|
46
46
|
content: (
|
|
47
47
|
<div>
|
|
48
48
|
A new version of Spinnaker is available{' '}
|
|
49
|
-
<a role="button" className="action" onClick={() => document.location.reload(
|
|
49
|
+
<a role="button" className="action" onClick={() => document.location.reload()}>
|
|
50
50
|
Refresh
|
|
51
51
|
</a>
|
|
52
52
|
</div>
|
|
@@ -303,7 +303,7 @@ const helpContents: { [key: string]: string } = {
|
|
|
303
303
|
'<p>Explicitly evaluate SpEL expressions in overrides just prior to manifest baking. Can be paired with the "Skip SpEL evaluation" option in the Deploy Manifest stage when baking a third-party manifest artifact with expressions not meant for Spinnaker to evaluate as SpEL.</p>',
|
|
304
304
|
'pipeline.config.bake.manifest.templateRenderer': '<p>This is the engine used for rendering your manifest.</p>',
|
|
305
305
|
'pipeline.config.bake.manifest.helm.chartFilePath': `
|
|
306
|
-
<p>This is the relative path to the directory containing the Chart.yaml file within your Git repo.</p>
|
|
306
|
+
<p>This is the relative path to the directory containing the Chart.yaml file within your Git repo (or helm/image artifact).</p>
|
|
307
307
|
<p>e.g.: <b>helm/my-chart</b></p>`,
|
|
308
308
|
'pipeline.config.bake.manifest.helm.rawOverrides':
|
|
309
309
|
'Use <i>--set</i> instead of <i>--set-string</i> when injecting override values. Values injected using <i>--set</i> will be converted to primitive types by Helm.',
|
|
@@ -244,6 +244,21 @@ class ServerGroupsUrlBuilder implements IUrlBuilder {
|
|
|
244
244
|
{ inherit: false },
|
|
245
245
|
);
|
|
246
246
|
|
|
247
|
+
if (
|
|
248
|
+
input.provider === 'ecs' ||
|
|
249
|
+
(input.provider && input.provider.includes('ecs')) ||
|
|
250
|
+
input.provider === 'aws' ||
|
|
251
|
+
(input.provider && input.provider.includes('aws'))
|
|
252
|
+
) {
|
|
253
|
+
const serverGroupParts = input.serverGroup.split('-');
|
|
254
|
+
let clusterName = input.serverGroup;
|
|
255
|
+
if (serverGroupParts.length > 1 && serverGroupParts[serverGroupParts.length - 1].match(/^v\d+$/)) {
|
|
256
|
+
clusterName = serverGroupParts.slice(0, -1).join('-');
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return UrlBuilderUtils.buildUrl(href, { clusters: `${input.account}:${clusterName}` });
|
|
260
|
+
}
|
|
261
|
+
|
|
247
262
|
return UrlBuilderUtils.buildUrl(href, { q: input.serverGroup, acct: input.account, reg: input.region });
|
|
248
263
|
}
|
|
249
264
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import classNames from 'classnames';
|
|
2
|
-
import { capitalize,
|
|
2
|
+
import { capitalize, get, isEmpty } from 'lodash';
|
|
3
3
|
import React from 'react';
|
|
4
4
|
import { from as observableFrom, Subject } from 'rxjs';
|
|
5
5
|
import { takeUntil } from 'rxjs/operators';
|
|
@@ -154,15 +154,19 @@ export class NotificationsList extends React.Component<INotificationsListProps,
|
|
|
154
154
|
observableFrom(AppNotificationsService.getNotificationsForApplication(application.name))
|
|
155
155
|
.pipe(takeUntil(this.destroy$))
|
|
156
156
|
.subscribe((notifications) => {
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
157
|
+
const newNotifications = supportedNotificationTypes.reduce<INotification[]>((acc, type) => {
|
|
158
|
+
const typeNotifications = notifications[type];
|
|
159
|
+
if (typeNotifications && typeof typeNotifications !== 'string') {
|
|
160
|
+
acc.push(...typeNotifications);
|
|
161
|
+
}
|
|
162
|
+
return acc;
|
|
163
|
+
}, []);
|
|
164
|
+
|
|
165
|
+
const applicationNotifications = newNotifications.filter(
|
|
163
166
|
(allow) => allow !== undefined && allow.level === 'application',
|
|
164
167
|
);
|
|
165
|
-
|
|
168
|
+
|
|
169
|
+
updateNotifications(applicationNotifications);
|
|
166
170
|
});
|
|
167
171
|
this.setState({ isNotificationsDirty: false });
|
|
168
172
|
};
|
|
@@ -17,12 +17,13 @@ import { CheckboxInput, TextInput } from '../../../../../presentation';
|
|
|
17
17
|
|
|
18
18
|
export interface IBakeHelmConfigFormState {
|
|
19
19
|
gitRepoArtifactAccountNames: string[];
|
|
20
|
+
helmImageArtifactAccountNames: string[];
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
export class BakeHelmConfigForm extends React.Component<IFormikStageConfigInjectedProps, IBakeHelmConfigFormState> {
|
|
23
24
|
constructor(props: IFormikStageConfigInjectedProps) {
|
|
24
25
|
super(props);
|
|
25
|
-
this.state = { gitRepoArtifactAccountNames: [] };
|
|
26
|
+
this.state = { gitRepoArtifactAccountNames: [], helmImageArtifactAccountNames: [] };
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
private static readonly excludedArtifactTypes = excludeAllTypesExcept(
|
|
@@ -37,6 +38,7 @@ export class BakeHelmConfigForm extends React.Component<IFormikStageConfigInject
|
|
|
37
38
|
ArtifactTypePatterns.HELM_CHART,
|
|
38
39
|
ArtifactTypePatterns.HTTP_FILE,
|
|
39
40
|
ArtifactTypePatterns.ORACLE_OBJECT,
|
|
41
|
+
ArtifactTypePatterns.HELM_IMAGE,
|
|
40
42
|
);
|
|
41
43
|
|
|
42
44
|
public componentDidMount() {
|
|
@@ -73,6 +75,9 @@ export class BakeHelmConfigForm extends React.Component<IFormikStageConfigInject
|
|
|
73
75
|
gitRepoArtifactAccountNames: artifactAccounts
|
|
74
76
|
.filter((account) => account.types.some((type) => ArtifactTypePatterns.GIT_REPO.test(type)))
|
|
75
77
|
.map((account) => account.name),
|
|
78
|
+
helmImageArtifactAccountNames: artifactAccounts
|
|
79
|
+
.filter((account) => account.types.some((type) => ArtifactTypePatterns.HELM_IMAGE.test(type)))
|
|
80
|
+
.map((account) => account.name),
|
|
76
81
|
});
|
|
77
82
|
});
|
|
78
83
|
}
|
|
@@ -208,7 +213,8 @@ export class BakeHelmConfigForm extends React.Component<IFormikStageConfigInject
|
|
|
208
213
|
pipeline={this.props.pipeline}
|
|
209
214
|
stage={stage}
|
|
210
215
|
/>
|
|
211
|
-
{this.state.gitRepoArtifactAccountNames.includes(this.getInputArtifact(stage, 0).account)
|
|
216
|
+
{(this.state.gitRepoArtifactAccountNames.includes(this.getInputArtifact(stage, 0).account) ||
|
|
217
|
+
this.state.helmImageArtifactAccountNames.includes(this.getInputArtifact(stage, 0).account)) && (
|
|
212
218
|
<StageConfigField label="Helm Chart File Path" helpKey="pipeline.config.bake.manifest.helm.chartFilePath">
|
|
213
219
|
<TextInput
|
|
214
220
|
onChange={(e: React.ChangeEvent<any>) => {
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { cloneDeep } from 'lodash';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import type { Option } from 'react-select';
|
|
4
|
+
|
|
5
|
+
import { ArtifactService } from '../ArtifactService';
|
|
6
|
+
import { ArtifactTypePatterns } from '../../../../../artifact/ArtifactTypes';
|
|
7
|
+
import type { IArtifact, IArtifactEditorProps, IArtifactKindConfig } from '../../../../../domain';
|
|
8
|
+
import { TetheredCreatable, TetheredSelect } from '../../../../../presentation';
|
|
9
|
+
import { StageConfigField } from '../../../stages/common';
|
|
10
|
+
import { Spinner } from '../../../../../widgets/spinners/Spinner';
|
|
11
|
+
|
|
12
|
+
const TYPE = 'helm/image';
|
|
13
|
+
|
|
14
|
+
interface IHelmImageArtifactEditorState {
|
|
15
|
+
names: string[];
|
|
16
|
+
versions: Array<Option<string>>;
|
|
17
|
+
versionsLoading: boolean;
|
|
18
|
+
namesLoading: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
class HelmImageEditor extends React.Component<IArtifactEditorProps, IHelmImageArtifactEditorState> {
|
|
22
|
+
public state: IHelmImageArtifactEditorState = {
|
|
23
|
+
names: [],
|
|
24
|
+
versions: [],
|
|
25
|
+
versionsLoading: true,
|
|
26
|
+
namesLoading: true,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// taken from https://github.com/semver/semver/issues/232
|
|
30
|
+
private SEMVER = new RegExp(
|
|
31
|
+
/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(-(0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(\.(0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*)?(\+[0-9a-zA-Z-]+(\.[0-9a-zA-Z-]+)*)?$/,
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
constructor(props: IArtifactEditorProps) {
|
|
35
|
+
super(props);
|
|
36
|
+
const { artifact } = this.props;
|
|
37
|
+
if (artifact.type !== TYPE) {
|
|
38
|
+
const clonedArtifact = cloneDeep(artifact);
|
|
39
|
+
clonedArtifact.type = TYPE;
|
|
40
|
+
props.onChange(clonedArtifact);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
ArtifactService.getArtifactNames(TYPE, this.props.account.name).then(
|
|
44
|
+
(names) => {
|
|
45
|
+
this.setState({
|
|
46
|
+
names,
|
|
47
|
+
namesLoading: false,
|
|
48
|
+
});
|
|
49
|
+
},
|
|
50
|
+
() => {
|
|
51
|
+
this.setState({
|
|
52
|
+
names: [],
|
|
53
|
+
namesLoading: false,
|
|
54
|
+
versionsLoading: false,
|
|
55
|
+
versions: [],
|
|
56
|
+
});
|
|
57
|
+
},
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
public componentDidMount() {
|
|
62
|
+
const { artifact } = this.props;
|
|
63
|
+
if (artifact.name) {
|
|
64
|
+
this.getChartVersionOptions(artifact.name);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
public componentDidUpdate(prevProps: IArtifactEditorProps) {
|
|
69
|
+
if (this.props.account.name !== prevProps.account.name) {
|
|
70
|
+
ArtifactService.getArtifactNames(TYPE, this.props.account.name).then(
|
|
71
|
+
(names) => {
|
|
72
|
+
this.setState({
|
|
73
|
+
names,
|
|
74
|
+
namesLoading: false,
|
|
75
|
+
versions: [],
|
|
76
|
+
});
|
|
77
|
+
},
|
|
78
|
+
() => {
|
|
79
|
+
this.setState({
|
|
80
|
+
names: [],
|
|
81
|
+
namesLoading: false,
|
|
82
|
+
versions: [],
|
|
83
|
+
});
|
|
84
|
+
},
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
public render() {
|
|
90
|
+
const { artifact } = this.props;
|
|
91
|
+
const nameOptions = this.state.names.map((name) => ({ value: name, label: name }));
|
|
92
|
+
|
|
93
|
+
return (
|
|
94
|
+
<>
|
|
95
|
+
<StageConfigField label="Name">
|
|
96
|
+
{!this.state.namesLoading && (
|
|
97
|
+
<TetheredSelect
|
|
98
|
+
options={nameOptions}
|
|
99
|
+
value={artifact.name || ''}
|
|
100
|
+
onChange={(e: Option) => {
|
|
101
|
+
this.onChange(e, 'name');
|
|
102
|
+
this.getChartVersionOptions(e.value.toString());
|
|
103
|
+
}}
|
|
104
|
+
clearable={false}
|
|
105
|
+
/>
|
|
106
|
+
)}
|
|
107
|
+
{this.state.namesLoading && <Spinner />}
|
|
108
|
+
</StageConfigField>
|
|
109
|
+
<StageConfigField label="Version">
|
|
110
|
+
{!this.state.versionsLoading && (
|
|
111
|
+
<TetheredCreatable
|
|
112
|
+
options={this.state.versions}
|
|
113
|
+
value={artifact.version || ''}
|
|
114
|
+
onChange={(e: Option) => {
|
|
115
|
+
this.onChange(e, 'version');
|
|
116
|
+
}}
|
|
117
|
+
clearable={false}
|
|
118
|
+
/>
|
|
119
|
+
)}
|
|
120
|
+
{this.state.versionsLoading && <Spinner />}
|
|
121
|
+
</StageConfigField>
|
|
122
|
+
</>
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
private onChange = (e: Option, field: keyof IArtifact) => {
|
|
127
|
+
const clone = cloneDeep(this.props.artifact);
|
|
128
|
+
(clone[field] as any) = e.value.toString();
|
|
129
|
+
this.props.onChange(clone);
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
private getChartVersionOptions(chartName: string) {
|
|
133
|
+
const { artifact, account } = this.props;
|
|
134
|
+
this.setState({ versionsLoading: true });
|
|
135
|
+
ArtifactService.getArtifactVersions(TYPE, account.name, chartName).then((versions: string[]) => {
|
|
136
|
+
// if the version doesn't match SEMVER we assume that it's a regular expression or SpEL expression
|
|
137
|
+
// and add it to the list of valid versions
|
|
138
|
+
if (artifact.version && !this.SEMVER.test(artifact.version)) {
|
|
139
|
+
versions = versions.concat(artifact.version);
|
|
140
|
+
}
|
|
141
|
+
this.setState({
|
|
142
|
+
versions: versions.map((v) => ({ label: v, value: v })),
|
|
143
|
+
versionsLoading: false,
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export const HelmImageMatch: IArtifactKindConfig = {
|
|
150
|
+
label: 'Helm',
|
|
151
|
+
typePattern: ArtifactTypePatterns.HELM_IMAGE,
|
|
152
|
+
type: TYPE,
|
|
153
|
+
isDefault: false,
|
|
154
|
+
isMatch: true,
|
|
155
|
+
description: 'A helm chart to be deployed',
|
|
156
|
+
key: 'helm',
|
|
157
|
+
editCmp: HelmImageEditor,
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
export const HelmImageDefault: IArtifactKindConfig = {
|
|
161
|
+
label: 'Helm',
|
|
162
|
+
typePattern: ArtifactTypePatterns.HELM_IMAGE,
|
|
163
|
+
type: TYPE,
|
|
164
|
+
isDefault: true,
|
|
165
|
+
isMatch: false,
|
|
166
|
+
description: 'A helm chart to be deployed',
|
|
167
|
+
key: 'default.helm',
|
|
168
|
+
editCmp: HelmImageEditor,
|
|
169
|
+
};
|
|
@@ -7,6 +7,7 @@ import { GcsDefault, GcsMatch } from './gcs/GcsArtifactEditor';
|
|
|
7
7
|
import { GithubDefault, GithubMatch } from './github/GithubArtifactEditor';
|
|
8
8
|
import { GitlabDefault, GitlabMatch } from './gitlab/GitlabArtifactEditor';
|
|
9
9
|
import { GitRepoDefault, GitRepoMatch } from './gitrepo/GitRepoArtifactEditor';
|
|
10
|
+
import { HelmImageDefault, HelmImageMatch } from './helm-image/HelmImageArtifactEditor';
|
|
10
11
|
import { HelmDefault, HelmMatch } from './helm/HelmArtifactEditor';
|
|
11
12
|
import { HttpDefault, HttpMatch } from './http/HttpArtifactEditor';
|
|
12
13
|
import { IvyDefault, IvyMatch } from './ivy/IvyArtifactEditor';
|
|
@@ -35,6 +36,8 @@ export const artifactKindConfigs: IArtifactKindConfig[] = [
|
|
|
35
36
|
GitlabDefault,
|
|
36
37
|
HelmMatch,
|
|
37
38
|
HelmDefault,
|
|
39
|
+
HelmImageMatch,
|
|
40
|
+
HelmImageDefault,
|
|
38
41
|
HttpMatch,
|
|
39
42
|
HttpDefault,
|
|
40
43
|
IvyMatch,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { FormikErrors } from 'formik';
|
|
2
|
-
import { flatten, isNumber, values } from 'lodash';
|
|
2
|
+
import { flatten, isEmpty, isNumber, values } from 'lodash';
|
|
3
3
|
import { $log, $q } from 'ngimport';
|
|
4
4
|
import type { Subscription } from 'rxjs';
|
|
5
5
|
import { Subject } from 'rxjs';
|
|
@@ -195,7 +195,7 @@ export class PipelineConfigValidator {
|
|
|
195
195
|
if (pipeline.strategy && !pipeline.stages.some((stage) => stage.type === 'deploy')) {
|
|
196
196
|
messages.push('To be able to create new server groups, a custom strategy should contain a Deploy stage.');
|
|
197
197
|
}
|
|
198
|
-
if ((pipeline.expectedArtifacts || []).some((a) => !a.matchArtifact || (a.matchArtifact
|
|
198
|
+
if ((pipeline.expectedArtifacts || []).some((a) => !a.matchArtifact || isEmpty(a.matchArtifact))) {
|
|
199
199
|
messages.push('Every expected artifact must specify an artifact to match against.');
|
|
200
200
|
}
|
|
201
201
|
return messages;
|
|
@@ -89,9 +89,44 @@ export class ExecutionGroups extends React.Component<IExecutionGroupsProps, IExe
|
|
|
89
89
|
const { groups = [], container, showingDetails } = this.state;
|
|
90
90
|
const hasGroups = groups.length > 0;
|
|
91
91
|
const className = `row pipelines executions ${showingDetails ? 'showing-details' : ''}`;
|
|
92
|
-
|
|
92
|
+
|
|
93
|
+
// Check if there are any duplicate execution IDs before applying deduplication
|
|
94
|
+
const allExecutionIds = groups.flatMap((group) => group.executions.map((e) => e.id));
|
|
95
|
+
const uniqueIds = new Set(allExecutionIds);
|
|
96
|
+
const hasDuplicates = allExecutionIds.length !== uniqueIds.size;
|
|
97
|
+
|
|
98
|
+
let processedGroups;
|
|
99
|
+
|
|
100
|
+
if (hasDuplicates) {
|
|
101
|
+
// Only apply deduplication if there are actually duplicate executions
|
|
102
|
+
const processedExecutionIds = new Set<string>();
|
|
103
|
+
const deduplicatedGroups = groups.map((group) => ({
|
|
104
|
+
...group,
|
|
105
|
+
executions: [...group.executions],
|
|
106
|
+
runningExecutions: [...group.runningExecutions],
|
|
107
|
+
}));
|
|
108
|
+
|
|
109
|
+
deduplicatedGroups.forEach((group) => {
|
|
110
|
+
group.executions = group.executions.filter((execution) => {
|
|
111
|
+
if (processedExecutionIds.has(execution.id)) return false;
|
|
112
|
+
processedExecutionIds.add(execution.id);
|
|
113
|
+
return true;
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
group.runningExecutions = group.runningExecutions.filter((execution) =>
|
|
117
|
+
group.executions.some((e) => e.id === execution.id),
|
|
118
|
+
);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
processedGroups = deduplicatedGroups.filter((group) => group.executions.length > 0);
|
|
122
|
+
} else {
|
|
123
|
+
// No duplicates, use the original groups
|
|
124
|
+
processedGroups = groups;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const allGroups = (processedGroups || [])
|
|
93
128
|
.filter((g: IExecutionGroup) => g?.config?.migrationStatus === 'Started')
|
|
94
|
-
.concat(
|
|
129
|
+
.concat(processedGroups.filter((g) => g?.config?.migrationStatus !== 'Started'));
|
|
95
130
|
|
|
96
131
|
const executionGroups = allGroups.map((group: IExecutionGroup) => (
|
|
97
132
|
<ExecutionGroup parent={container} key={group.heading} group={group} application={this.props.application} />
|
|
@@ -255,9 +255,27 @@ export class ExecutionFilterService {
|
|
|
255
255
|
private static groupExecutions(filteredExecutions: IExecution[], application: Application): IExecutionGroup[] {
|
|
256
256
|
const groups: IExecutionGroup[] = [];
|
|
257
257
|
let executions: IExecution[] = [];
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
258
|
+
|
|
259
|
+
// Check if there are any duplicate execution IDs before applying deduplication
|
|
260
|
+
const executionIds = filteredExecutions.map((e) => e.id);
|
|
261
|
+
const uniqueIds = new Set(executionIds);
|
|
262
|
+
const hasDuplicates = executionIds.length !== uniqueIds.size;
|
|
263
|
+
|
|
264
|
+
if (hasDuplicates) {
|
|
265
|
+
// Only apply deduplication if there are actually duplicate executions
|
|
266
|
+
const processedExecutionIds = new Set<string>();
|
|
267
|
+
|
|
268
|
+
forOwn(groupBy(filteredExecutions, 'name'), (groupedExecutions) => {
|
|
269
|
+
const uniqueExecutions = groupedExecutions.filter((execution) => !processedExecutionIds.has(execution.id));
|
|
270
|
+
uniqueExecutions.forEach((execution) => processedExecutionIds.add(execution.id));
|
|
271
|
+
executions = executions.concat(uniqueExecutions.sort((a, b) => this.executionSorter(a, b)));
|
|
272
|
+
});
|
|
273
|
+
} else {
|
|
274
|
+
// No duplicates, use the original logic
|
|
275
|
+
forOwn(groupBy(filteredExecutions, 'name'), (groupedExecutions) => {
|
|
276
|
+
executions = executions.concat(groupedExecutions.sort((a, b) => this.executionSorter(a, b)));
|
|
277
|
+
});
|
|
278
|
+
}
|
|
261
279
|
|
|
262
280
|
executions.forEach((execution: IExecution) => {
|
|
263
281
|
const config: IPipeline = application.pipelineConfigs.data.find(
|
|
@@ -273,6 +291,11 @@ export class ExecutionFilterService {
|
|
|
273
291
|
if (sortFilter.groupBy === 'name') {
|
|
274
292
|
const executionGroups = groupBy(executions, 'name');
|
|
275
293
|
forOwn(executionGroups, (groupExecutions, key) => {
|
|
294
|
+
// Skip empty groups
|
|
295
|
+
if (groupExecutions.length === 0) {
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
|
|
276
299
|
const matchId = (pipelineConfig: IPipeline) => pipelineConfig.id === groupExecutions[0].pipelineConfigId;
|
|
277
300
|
const config = application.pipelineConfigs.data.concat(application.strategyConfigs?.data ?? []).find(matchId);
|
|
278
301
|
groupExecutions.sort((a, b) => this.executionSorter(a, b));
|
|
@@ -291,6 +314,11 @@ export class ExecutionFilterService {
|
|
|
291
314
|
if (sortFilter.groupBy === 'timeBoundary') {
|
|
292
315
|
const grouped = this.groupByTimeBoundary(executions);
|
|
293
316
|
forOwn(grouped, (groupExecutions: IExecution[], key) => {
|
|
317
|
+
// Skip empty groups
|
|
318
|
+
if (groupExecutions.length === 0) {
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
|
|
294
322
|
groupExecutions.sort((a, b) => this.executionSorter(a, b));
|
|
295
323
|
groups.push({
|
|
296
324
|
heading: key,
|
|
@@ -333,6 +361,19 @@ export class ExecutionFilterService {
|
|
|
333
361
|
|
|
334
362
|
private static executionsAreDifferent(oldGroup: IExecutionGroup, newGroup: IExecutionGroup): boolean {
|
|
335
363
|
let changeDetected = false;
|
|
364
|
+
|
|
365
|
+
const uniqueOldExecutions: IExecution[] = [];
|
|
366
|
+
const oldExecutionIds = new Set<string>();
|
|
367
|
+
|
|
368
|
+
oldGroup.executions.forEach((execution) => {
|
|
369
|
+
if (!oldExecutionIds.has(execution.id)) {
|
|
370
|
+
uniqueOldExecutions.push(execution);
|
|
371
|
+
oldExecutionIds.add(execution.id);
|
|
372
|
+
}
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
oldGroup.executions = uniqueOldExecutions;
|
|
376
|
+
|
|
336
377
|
oldGroup.executions.forEach((execution) => {
|
|
337
378
|
const newExecution = newGroup.executions.find((g) => g.id === execution.id);
|
|
338
379
|
if (!newExecution) {
|
|
@@ -345,14 +386,18 @@ export class ExecutionFilterService {
|
|
|
345
386
|
}
|
|
346
387
|
}
|
|
347
388
|
});
|
|
389
|
+
|
|
390
|
+
const existingExecutionIds = new Set(oldGroup.executions.map((e) => e.id));
|
|
391
|
+
|
|
348
392
|
newGroup.executions.forEach((execution) => {
|
|
349
|
-
|
|
350
|
-
if (!oldExecution) {
|
|
393
|
+
if (!existingExecutionIds.has(execution.id)) {
|
|
351
394
|
changeDetected = true;
|
|
352
395
|
$log.debug('new execution found, adding', execution.id);
|
|
353
396
|
oldGroup.executions.push(execution);
|
|
397
|
+
existingExecutionIds.add(execution.id);
|
|
354
398
|
}
|
|
355
399
|
});
|
|
400
|
+
|
|
356
401
|
return changeDetected;
|
|
357
402
|
}
|
|
358
403
|
|
|
@@ -10,10 +10,18 @@ interface IDetailsProps {
|
|
|
10
10
|
interface IDetailsHeaderProps {
|
|
11
11
|
icon: React.ReactNode;
|
|
12
12
|
name: string;
|
|
13
|
+
notifications?: React.ReactNode;
|
|
14
|
+
actions?: React.ReactNode;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface IDetailsContentProps {
|
|
18
|
+
loading: boolean;
|
|
19
|
+
children: React.ReactNode;
|
|
13
20
|
}
|
|
14
21
|
|
|
15
22
|
interface IDetailsSFCWithExtras extends React.SFC<IDetailsProps> {
|
|
16
23
|
Header: React.SFC<IDetailsHeaderProps>;
|
|
24
|
+
Content: React.FunctionComponent<IDetailsContentProps>;
|
|
17
25
|
}
|
|
18
26
|
|
|
19
27
|
const CloseButton = (
|
|
@@ -31,8 +39,12 @@ const DetailsHeader: React.SFC<IDetailsHeaderProps> = (props) => (
|
|
|
31
39
|
{CloseButton}
|
|
32
40
|
<div className="header-text horizontal middle">
|
|
33
41
|
{props.icon}
|
|
34
|
-
<h3 className="horizontal middle space-between flex-1">
|
|
42
|
+
<h3 className="horizontal middle space-between flex-1">
|
|
43
|
+
{props.name}
|
|
44
|
+
{props.notifications && props.notifications}
|
|
45
|
+
</h3>
|
|
35
46
|
</div>
|
|
47
|
+
{props.actions && <div className="actions">{props.actions}</div>}
|
|
36
48
|
<div>{props.children}</div>
|
|
37
49
|
</div>
|
|
38
50
|
);
|
|
@@ -49,6 +61,11 @@ const Details: IDetailsSFCWithExtras = (props) => (
|
|
|
49
61
|
<div className="details-panel">{props.loading ? loading : props.children}</div>
|
|
50
62
|
);
|
|
51
63
|
|
|
64
|
+
const DetailsContent = ({ loading, children }: IDetailsContentProps) => (
|
|
65
|
+
<div className="content">{loading ? loading : children}</div>
|
|
66
|
+
);
|
|
67
|
+
|
|
52
68
|
Details.Header = DetailsHeader;
|
|
69
|
+
Details.Content = DetailsContent;
|
|
53
70
|
|
|
54
71
|
export { Details };
|
|
@@ -27,5 +27,5 @@ interface IFormikFormImplProps<T> extends IFormikFormProps<T> {
|
|
|
27
27
|
* }} />
|
|
28
28
|
* ```
|
|
29
29
|
*/
|
|
30
|
-
const FormikFormImpl = <T
|
|
30
|
+
const FormikFormImpl = <T,>({ render, formik }: IFormikFormImplProps<T>) => render(formik);
|
|
31
31
|
export const FormikForm: React.ComponentType<IFormikFormProps<any>> = connect(FormikFormImpl);
|