@spinnaker/kubernetes 2026.2.2 → 2026.3.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.
Files changed (158) hide show
  1. package/dist/index.d.ts +1 -2
  2. package/dist/index.js +1 -1
  3. package/dist/index.js.map +1 -1
  4. package/dist/instance/details/KubernetesInstanceDetails.d.ts +54 -0
  5. package/dist/instance/details/kubernetesInstanceDetails.utils.d.ts +13 -0
  6. package/dist/instance/index.d.ts +1 -1
  7. package/dist/kubernetes.module.d.ts +5 -1
  8. package/dist/loadBalancer/transformer.d.ts +1 -1
  9. package/dist/manifest/AnnotationCustomSections.d.ts +3 -2
  10. package/dist/manifest/ManifestKindSearch.d.ts +1 -1
  11. package/dist/manifest/delete/Delete.d.ts +0 -1
  12. package/dist/manifest/delete/DeleteModal.d.ts +1 -1
  13. package/dist/manifest/delete/delete.types.d.ts +4 -0
  14. package/dist/manifest/index.d.ts +3 -6
  15. package/dist/manifest/manifest.service.d.ts +5 -0
  16. package/dist/manifest/manifestCommandBuilder.service.d.ts +1 -1
  17. package/dist/manifest/rollout/PauseRollout.d.ts +0 -1
  18. package/dist/manifest/rollout/ResumeRollout.d.ts +0 -1
  19. package/dist/manifest/rollout/RollingRestart.d.ts +4 -4
  20. package/dist/manifest/rollout/UndoRollout.d.ts +0 -1
  21. package/dist/manifest/scale/Scale.d.ts +1 -2
  22. package/dist/manifest/scale/ScaleModal.d.ts +3 -3
  23. package/dist/manifest/scale/ScaleSettingsForm.d.ts +7 -1
  24. package/dist/manifest/selector/ManifestSelector.d.ts +1 -1
  25. package/dist/manifest/traffic/ManifestTrafficService.d.ts +2 -2
  26. package/dist/pipelines/stages/deleteManifest/DeleteManifestOptionsForm.d.ts +1 -1
  27. package/dist/pipelines/stages/deployManifest/manifestStatus/DeployStatus.d.ts +1 -0
  28. package/dist/pipelines/stages/deployManifest/manifestStatus/ManifestDetailsLink.d.ts +6 -3
  29. package/dist/pipelines/stages/traffic/disableManifest.stage.d.ts +2 -1
  30. package/dist/pipelines/stages/traffic/enableManifest.stage.d.ts +2 -1
  31. package/dist/rawResource/index.d.ts +3 -1
  32. package/dist/rawResource/rawResource.dataSource.d.ts +1 -1
  33. package/dist/rawResource/rawResource.states.d.ts +4 -1
  34. package/dist/resources/resources.state.d.ts +2 -1
  35. package/dist/securityGroup/details/KubernetesSecurityGroupDetails.d.ts +49 -0
  36. package/dist/securityGroup/index.d.ts +3 -1
  37. package/dist/securityGroup/transformer.d.ts +4 -1
  38. package/dist/serverGroup/serverGroupCommandBuilder.d.ts +1 -1
  39. package/dist/serverGroup/serverGroupTransformer.d.ts +1 -1
  40. package/dist/serverGroupManager/details/ServerGroupManagerDetails.d.ts +7 -3
  41. package/package.json +3 -12
  42. package/src/help/kubernetes.help.ts +2 -0
  43. package/src/index.ts +2 -2
  44. package/src/instance/details/KubernetesInstanceDetails.spec.tsx +451 -0
  45. package/src/instance/details/KubernetesInstanceDetails.tsx +342 -0
  46. package/src/instance/details/kubernetesInstanceDetails.utils.ts +77 -0
  47. package/src/instance/index.ts +1 -1
  48. package/src/kubernetes.module.spec.ts +25 -0
  49. package/src/kubernetes.module.ts +23 -87
  50. package/src/loadBalancer/details/KubernetesLoadBalancerActions.tsx +30 -35
  51. package/src/loadBalancer/details/sections/LoadBalancerAnnotationCustomSection.tsx +2 -3
  52. package/src/loadBalancer/transformer.ts +1 -1
  53. package/src/manifest/AnnotationCustomSections.spec.tsx +178 -0
  54. package/src/manifest/AnnotationCustomSections.tsx +179 -2
  55. package/src/manifest/ManifestKindSearch.tsx +1 -1
  56. package/src/manifest/delete/Delete.tsx +0 -9
  57. package/src/manifest/delete/DeleteModal.tsx +69 -72
  58. package/src/manifest/delete/delete.types.ts +4 -0
  59. package/src/manifest/index.ts +3 -6
  60. package/src/manifest/manifest.service.ts +18 -1
  61. package/src/manifest/manifestCommandBuilder.service.spec.ts +41 -0
  62. package/src/manifest/manifestCommandBuilder.service.ts +45 -59
  63. package/src/manifest/rollout/PauseRollout.tsx +1 -9
  64. package/src/manifest/rollout/ResumeRollout.tsx +2 -10
  65. package/src/manifest/rollout/RollingRestart.tsx +16 -22
  66. package/src/manifest/rollout/UndoRollout.tsx +1 -10
  67. package/src/manifest/rollout/UndoRolloutModal.tsx +5 -1
  68. package/src/manifest/scale/Scale.tsx +12 -11
  69. package/src/manifest/scale/ScaleModal.tsx +43 -31
  70. package/src/manifest/scale/ScaleSettingsForm.tsx +8 -1
  71. package/src/manifest/selector/ManifestSelector.spec.tsx +89 -46
  72. package/src/manifest/selector/ManifestSelector.tsx +4 -5
  73. package/src/manifest/traffic/ManifestTrafficService.ts +2 -2
  74. package/src/manifest/wizard/ManifestWizard.tsx +1 -1
  75. package/src/pipelines/stages/deleteManifest/DeleteManifestOptionsForm.tsx +1 -1
  76. package/src/pipelines/stages/deleteManifest/DeleteManifestStageConfig.tsx +1 -1
  77. package/src/pipelines/stages/deployManifest/DeployManifestStageForm.tsx +16 -0
  78. package/src/pipelines/stages/deployManifest/ManifestCopier.spec.ts +52 -77
  79. package/src/pipelines/stages/deployManifest/ManifestDeploymentOptions.spec.tsx +2 -0
  80. package/src/pipelines/stages/deployManifest/manifestStatus/DeployStatus.tsx +39 -4
  81. package/src/pipelines/stages/deployManifest/manifestStatus/ManifestDetailsLink.spec.tsx +35 -0
  82. package/src/pipelines/stages/deployManifest/manifestStatus/ManifestDetailsLink.tsx +10 -5
  83. package/src/pipelines/stages/traffic/ManifestTrafficStageRegistration.spec.ts +34 -0
  84. package/src/pipelines/stages/traffic/disableManifest.stage.ts +17 -19
  85. package/src/pipelines/stages/traffic/enableManifest.stage.ts +17 -19
  86. package/src/rawResource/index.ts +18 -1
  87. package/src/rawResource/rawResource.dataSource.ts +5 -8
  88. package/src/rawResource/rawResource.states.ts +50 -51
  89. package/src/resources/resources.state.spec.ts +138 -0
  90. package/src/resources/resources.state.ts +30 -35
  91. package/src/securityGroup/details/KubernetesSecurityGroupDetails.spec.tsx +292 -0
  92. package/src/securityGroup/details/KubernetesSecurityGroupDetails.tsx +304 -0
  93. package/src/securityGroup/index.ts +3 -1
  94. package/src/securityGroup/transformer.ts +2 -10
  95. package/src/serverGroup/details/KubernetesServerGroupActions.tsx +31 -61
  96. package/src/serverGroup/details/kubernetesServerGroupDetailsGetter.ts +1 -1
  97. package/src/serverGroup/details/sections/ServerGroupAnnotationCustomSection.tsx +2 -3
  98. package/src/serverGroup/serverGroupCommandBuilder.ts +1 -1
  99. package/src/serverGroup/serverGroupTransformer.ts +1 -1
  100. package/src/serverGroupManager/details/ServerGroupManagerActions.tsx +48 -128
  101. package/src/serverGroupManager/details/ServerGroupManagerDetails.spec.ts +12 -0
  102. package/src/serverGroupManager/details/ServerGroupManagerDetails.tsx +19 -8
  103. package/src/serverGroupManager/details/sections/ServerGroupManagerAnnotationCustomSection.tsx +2 -3
  104. package/src/serverGroupManager/details/useKubernetesServerGroupManagerDetails.spec.tsx +112 -0
  105. package/src/serverGroupManager/details/useKubernetesServerGroupManagerDetails.tsx +50 -29
  106. package/dist/instance/details/details.controller.d.ts +0 -1
  107. package/dist/manifest/annotationCustomSections.component.d.ts +0 -3
  108. package/dist/manifest/delete/delete.controller.d.ts +0 -12
  109. package/dist/manifest/delete/deleteOptionsForm.component.d.ts +0 -1
  110. package/dist/manifest/editor/json/jsonEditor.component.d.ts +0 -1
  111. package/dist/manifest/manifestEvents.component.d.ts +0 -1
  112. package/dist/manifest/manifestImageDetails.component.d.ts +0 -1
  113. package/dist/manifest/manifestLabels.component.d.ts +0 -1
  114. package/dist/manifest/manifestQos.component.d.ts +0 -1
  115. package/dist/manifest/manifestResources.component.d.ts +0 -1
  116. package/dist/manifest/rollout/pause.controller.d.ts +0 -1
  117. package/dist/manifest/rollout/resume.controller.d.ts +0 -1
  118. package/dist/manifest/rollout/undo.controller.d.ts +0 -5
  119. package/dist/manifest/scale/scale.controller.d.ts +0 -8
  120. package/dist/manifest/scale/scaleSettingsForm.component.d.ts +0 -1
  121. package/dist/manifest/selector/selector.component.d.ts +0 -1
  122. package/dist/manifest/status/condition.component.d.ts +0 -1
  123. package/dist/manifest/status/status.component.d.ts +0 -1
  124. package/dist/rawResource/rawResource.module.d.ts +0 -1
  125. package/dist/reactShims/index.d.ts +0 -1
  126. package/dist/reactShims/kubernetes.react.injector.d.ts +0 -11
  127. package/dist/reactShims/kubernetes.react.module.d.ts +0 -1
  128. package/dist/securityGroup/details/details.controller.d.ts +0 -1
  129. package/src/instance/details/details.controller.ts +0 -186
  130. package/src/instance/details/details.html +0 -80
  131. package/src/manifest/annotationCustomSections.component.ts +0 -114
  132. package/src/manifest/delete/delete.controller.ts +0 -80
  133. package/src/manifest/delete/delete.html +0 -42
  134. package/src/manifest/delete/deleteOptionsForm.component.ts +0 -52
  135. package/src/manifest/editor/json/jsonEditor.component.ts +0 -12
  136. package/src/manifest/manifestEvents.component.ts +0 -12
  137. package/src/manifest/manifestImageDetails.component.ts +0 -12
  138. package/src/manifest/manifestLabels.component.ts +0 -12
  139. package/src/manifest/manifestQos.component.ts +0 -12
  140. package/src/manifest/manifestResources.component.ts +0 -12
  141. package/src/manifest/rollout/pause.controller.ts +0 -66
  142. package/src/manifest/rollout/pause.html +0 -28
  143. package/src/manifest/rollout/resume.controller.ts +0 -66
  144. package/src/manifest/rollout/resume.html +0 -28
  145. package/src/manifest/rollout/undo.controller.ts +0 -74
  146. package/src/manifest/rollout/undo.html +0 -44
  147. package/src/manifest/scale/scale.controller.ts +0 -75
  148. package/src/manifest/scale/scale.html +0 -32
  149. package/src/manifest/scale/scaleSettingsForm.component.ts +0 -11
  150. package/src/manifest/selector/selector.component.ts +0 -17
  151. package/src/manifest/status/condition.component.ts +0 -12
  152. package/src/manifest/status/status.component.ts +0 -11
  153. package/src/rawResource/rawResource.module.ts +0 -8
  154. package/src/reactShims/index.ts +0 -1
  155. package/src/reactShims/kubernetes.react.injector.ts +0 -30
  156. package/src/reactShims/kubernetes.react.module.ts +0 -12
  157. package/src/securityGroup/details/details.controller.ts +0 -118
  158. package/src/securityGroup/details/details.html +0 -91
@@ -1,5 +1,7 @@
1
1
  import { mount } from 'enzyme';
2
+ import type { ReactWrapper } from 'enzyme';
2
3
  import React from 'react';
4
+ import { act } from 'react-dom/test-utils';
3
5
  import type { Option } from 'react-select';
4
6
  import Select, { Creatable } from 'react-select';
5
7
 
@@ -21,16 +23,40 @@ import LabelEditor from './labelEditor/LabelEditor';
21
23
  import Spy = jasmine.Spy;
22
24
 
23
25
  describe('<ManifestSelector />', () => {
26
+ let accountService: Spy;
27
+ let mountedComponents: Array<ReactWrapper<ManifestSelector>>;
24
28
  let searchService: Spy;
25
29
 
26
30
  beforeEach(() => {
31
+ mountedComponents = [];
27
32
  searchService = spyOn(ManifestKindSearchService, 'search').and.returnValue(Promise.resolve([]));
28
- spyOn(AccountService, 'getAllAccountDetailsForProvider').and.returnValue(Promise.resolve([]));
33
+ accountService = spyOn(AccountService, 'getAllAccountDetailsForProvider').and.returnValue(Promise.resolve([]));
29
34
  });
30
35
 
36
+ afterEach(async () => {
37
+ await act(async () => {
38
+ await Promise.all(searchService.calls.all().map(({ returnValue }) => returnValue));
39
+ mountedComponents.splice(0).forEach((wrapper) => wrapper.unmount());
40
+ });
41
+ });
42
+
43
+ const component = async (selector: any, props: any = {}) => {
44
+ const wrapper = mount<ManifestSelector>(
45
+ (<ManifestSelector onChange={noop} selector={selector} {...props} />) as any,
46
+ );
47
+ mountedComponents.push(wrapper);
48
+
49
+ await act(async () => {
50
+ await accountService.calls.mostRecent().returnValue;
51
+ await Promise.resolve();
52
+ });
53
+ wrapper.update();
54
+ return wrapper;
55
+ };
56
+
31
57
  describe('initialization', () => {
32
- it('renders namespace from input props', () => {
33
- const wrapper = component({
58
+ it('renders namespace from input props', async () => {
59
+ const wrapper = await component({
34
60
  manifestName: 'configMap my-config-map',
35
61
  account: 'my-account',
36
62
  location: 'default',
@@ -40,8 +66,8 @@ describe('<ManifestSelector />', () => {
40
66
  expect((namespace.props().value as Option).value).toEqual('default');
41
67
  });
42
68
 
43
- it('renders kind from input props', () => {
44
- const wrapper = component({
69
+ it('renders kind from input props', async () => {
70
+ const wrapper = await component({
45
71
  manifestName: 'configMap my-config-map',
46
72
  account: 'my-account',
47
73
  location: 'default',
@@ -51,8 +77,8 @@ describe('<ManifestSelector />', () => {
51
77
  expect((kind.props().value as Option).value).toEqual('configMap');
52
78
  });
53
79
 
54
- it('renders name from input props', () => {
55
- const wrapper = component({
80
+ it('renders name from input props', async () => {
81
+ const wrapper = await component({
56
82
  manifestName: 'configMap my-config-map',
57
83
  account: 'my-account',
58
84
  location: 'default',
@@ -62,8 +88,8 @@ describe('<ManifestSelector />', () => {
62
88
  expect((name.props().value as Option).value).toEqual('my-config-map');
63
89
  });
64
90
 
65
- it('renders kinds from input props', () => {
66
- const wrapper = component({
91
+ it('renders kinds from input props', async () => {
92
+ const wrapper = await component({
67
93
  account: 'my-account',
68
94
  kinds: ['configMap', 'deployment'],
69
95
  location: 'default',
@@ -74,7 +100,7 @@ describe('<ManifestSelector />', () => {
74
100
  expect(kinds.props().value).toEqual(['configMap', 'deployment']);
75
101
  });
76
102
 
77
- it('renders labels from input props', () => {
103
+ it('renders labels from input props', async () => {
78
104
  const labelSelectors: IManifestLabelSelector[] = [
79
105
  {
80
106
  key: 'label-key',
@@ -82,7 +108,7 @@ describe('<ManifestSelector />', () => {
82
108
  values: ['label-value'],
83
109
  },
84
110
  ];
85
- const wrapper = component({
111
+ const wrapper = await component({
86
112
  account: 'my-account',
87
113
  kinds: ['configMap', 'deployment'],
88
114
  labelSelectors: {
@@ -102,8 +128,8 @@ describe('<ManifestSelector />', () => {
102
128
  application: { getDataSource: () => ({ data }) },
103
129
  });
104
130
 
105
- it("includes cluster if selected kind matches the cluster's server groups' kind", () => {
106
- const wrapper = component(
131
+ it("includes cluster if selected kind matches the cluster's server groups' kind", async () => {
132
+ const wrapper = await component(
107
133
  {
108
134
  kind: 'replicaSet',
109
135
  account: 'my-account',
@@ -124,8 +150,8 @@ describe('<ManifestSelector />', () => {
124
150
  expect(cluster.props().clusters).toEqual(['replicaSet my-replica-set']);
125
151
  });
126
152
 
127
- it("does not include cluster if selected kind does not match cluster's server groups' kind", () => {
128
- const wrapper = component(
153
+ it("does not include cluster if selected kind does not match cluster's server groups' kind", async () => {
154
+ const wrapper = await component(
129
155
  {
130
156
  kind: 'statefulSet',
131
157
  account: 'my-account',
@@ -146,8 +172,8 @@ describe('<ManifestSelector />', () => {
146
172
  expect(cluster.props().clusters).toEqual([]);
147
173
  });
148
174
 
149
- it('handles case in which a cluster has two different kinds of server groups', () => {
150
- const wrapper = component(
175
+ it('handles case in which a cluster has two different kinds of server groups', async () => {
176
+ const wrapper = await component(
151
177
  {
152
178
  kind: 'statefulSet',
153
179
  account: 'my-account',
@@ -174,8 +200,8 @@ describe('<ManifestSelector />', () => {
174
200
  expect(cluster.props().clusters).toEqual(['my-cluster']);
175
201
  });
176
202
 
177
- it("does not include cluster if the cluster's server groups are managed", () => {
178
- const wrapper = component(
203
+ it("does not include cluster if the cluster's server groups are managed", async () => {
204
+ const wrapper = await component(
179
205
  {
180
206
  kind: 'replicaSet',
181
207
  account: 'my-account',
@@ -197,8 +223,8 @@ describe('<ManifestSelector />', () => {
197
223
  expect(cluster.props().clusters).toEqual([]);
198
224
  });
199
225
 
200
- it('filters clusters by account', () => {
201
- const wrapper = component(
226
+ it('filters clusters by account', async () => {
227
+ const wrapper = await component(
202
228
  {
203
229
  kind: 'replicaSet',
204
230
  account: 'my-other-account',
@@ -219,8 +245,8 @@ describe('<ManifestSelector />', () => {
219
245
  expect(cluster.props().clusters).toEqual([]);
220
246
  });
221
247
 
222
- it('filters clusters by namespace', () => {
223
- const wrapper = component(
248
+ it('filters clusters by namespace', async () => {
249
+ const wrapper = await component(
224
250
  {
225
251
  kind: 'replicaSet',
226
252
  account: 'my-account',
@@ -244,8 +270,8 @@ describe('<ManifestSelector />', () => {
244
270
  });
245
271
 
246
272
  describe('change handlers', () => {
247
- it('calls the search service after updating the `Kind` field', () => {
248
- const wrapper = component({
273
+ it('calls the search service after updating the `Kind` field', async () => {
274
+ const wrapper = await component({
249
275
  manifestName: 'configMap my-config-map',
250
276
  account: 'my-account',
251
277
  location: 'default',
@@ -256,8 +282,8 @@ describe('<ManifestSelector />', () => {
256
282
  expect(searchService).toHaveBeenCalledWith('deployment', 'default', 'my-account');
257
283
  });
258
284
 
259
- it('calls the search service after updating the `Namespace` field', () => {
260
- const wrapper = component({
285
+ it('calls the search service after updating the `Namespace` field', async () => {
286
+ const wrapper = await component({
261
287
  manifestName: 'configMap my-config-map',
262
288
  account: 'my-account',
263
289
  location: 'default',
@@ -268,8 +294,8 @@ describe('<ManifestSelector />', () => {
268
294
  expect(searchService).toHaveBeenCalledWith('configMap', 'kube-system', 'my-account');
269
295
  });
270
296
 
271
- it('calls the search service after updating the `Account` field', () => {
272
- const wrapper = component({
297
+ it('calls the search service after updating the `Account` field', async () => {
298
+ const wrapper = await component({
273
299
  manifestName: 'configMap my-config-map',
274
300
  account: 'my-account',
275
301
  location: 'default',
@@ -286,8 +312,28 @@ describe('<ManifestSelector />', () => {
286
312
  expect(searchService).toHaveBeenCalledWith('configMap', 'default', 'my-other-account');
287
313
  });
288
314
 
289
- it('clears namespace when changing account if account does not have selected namespace', () => {
290
- const wrapper = component({
315
+ it('waits for complete manifest search criteria', async () => {
316
+ const wrapper = await component({ mode: SelectorMode.Static });
317
+ wrapper.setState({
318
+ accounts: [{ name: 'my-account', namespaces: ['default'] }],
319
+ } as IManifestSelectorState);
320
+ searchService.calls.reset();
321
+
322
+ const account = wrapper.find(AccountSelectInput).first();
323
+ account.props().onChange(createFakeReactSyntheticEvent({ value: 'my-account' }));
324
+ expect(searchService).not.toHaveBeenCalled();
325
+
326
+ const namespace = wrapper.find({ label: 'Namespace' }).find(Creatable).first();
327
+ namespace.props().onChange({ value: 'default', label: 'default' });
328
+ expect(searchService).not.toHaveBeenCalled();
329
+
330
+ const kind = wrapper.find({ label: 'Kind' }).find(Creatable).first();
331
+ kind.props().onChange({ value: 'deployment', label: 'deployment' });
332
+ expect(searchService).toHaveBeenCalledOnceWith('deployment', 'default', 'my-account');
333
+ });
334
+
335
+ it('clears namespace when changing account if account does not have selected namespace', async () => {
336
+ const wrapper = await component({
291
337
  manifestName: 'configMap my-config-map',
292
338
  account: 'my-account',
293
339
  location: 'default',
@@ -306,8 +352,8 @@ describe('<ManifestSelector />', () => {
306
352
  });
307
353
 
308
354
  describe('mode change', () => {
309
- it('handles kind during static -> dynamic mode transition', () => {
310
- const wrapper = component(
355
+ it('handles kind during static -> dynamic mode transition', async () => {
356
+ const wrapper = await component(
311
357
  {
312
358
  manifestName: 'configMap my-config-map',
313
359
  account: 'my-account',
@@ -320,8 +366,8 @@ describe('<ManifestSelector />', () => {
320
366
  expect(wrapper.state().selector.kind).toEqual('configMap');
321
367
  });
322
368
 
323
- it('handles kind during static -> label mode transition', () => {
324
- const wrapper = component(
369
+ it('handles kind during static -> label mode transition', async () => {
370
+ const wrapper = await component(
325
371
  {
326
372
  manifestName: 'configMap my-config-map',
327
373
  account: 'my-account',
@@ -336,8 +382,8 @@ describe('<ManifestSelector />', () => {
336
382
  expect(wrapper.state().selector.manifestName).toBeNull();
337
383
  });
338
384
 
339
- it('handles kind during dynamic -> static mode transition', () => {
340
- const wrapper = component(
385
+ it('handles kind during dynamic -> static mode transition', async () => {
386
+ const wrapper = await component(
341
387
  {
342
388
  account: 'my-account',
343
389
  location: 'default',
@@ -352,8 +398,8 @@ describe('<ManifestSelector />', () => {
352
398
  expect(wrapper.state().selector.manifestName).toEqual('configMap');
353
399
  });
354
400
 
355
- it('handles kind during dynamic -> label mode transition', () => {
356
- const wrapper = component(
401
+ it('handles kind during dynamic -> label mode transition', async () => {
402
+ const wrapper = await component(
357
403
  {
358
404
  account: 'my-account',
359
405
  location: 'default',
@@ -369,8 +415,8 @@ describe('<ManifestSelector />', () => {
369
415
  expect(wrapper.state().selector.manifestName).toBeNull();
370
416
  });
371
417
 
372
- it('handles kind during label -> static mode transition', () => {
373
- const wrapper = component(
418
+ it('handles kind during label -> static mode transition', async () => {
419
+ const wrapper = await component(
374
420
  {
375
421
  account: 'my-account',
376
422
  location: 'default',
@@ -388,8 +434,8 @@ describe('<ManifestSelector />', () => {
388
434
  expect(wrapper.state().selector.kinds).toBeNull();
389
435
  });
390
436
 
391
- it('handles kind during label -> dynamic mode transition', () => {
392
- const wrapper = component(
437
+ it('handles kind during label -> dynamic mode transition', async () => {
438
+ const wrapper = await component(
393
439
  {
394
440
  account: 'my-account',
395
441
  location: 'default',
@@ -408,6 +454,3 @@ describe('<ManifestSelector />', () => {
408
454
  });
409
455
  });
410
456
  });
411
-
412
- const component = (selector: any, props: any = {}) =>
413
- mount<ManifestSelector>((<ManifestSelector onChange={noop} selector={selector} {...props} />) as any);
@@ -10,12 +10,12 @@ import {
10
10
  AccountSelectInput,
11
11
  AccountService,
12
12
  AppListExtractor,
13
- NgReact,
14
13
  noop,
15
14
  ScopeClusterSelector,
16
15
  SETTINGS,
17
16
  StageConfigField,
18
17
  StageConstants,
18
+ TargetSelect,
19
19
  } from '@spinnaker/core';
20
20
 
21
21
  import type { IManifestLabelSelector } from './IManifestLabelSelector';
@@ -164,7 +164,7 @@ export class ManifestSelector extends React.Component<IManifestSelectorProps, IM
164
164
 
165
165
  public componentWillUnmount = () => this.destroy$.next();
166
166
 
167
- public loadAccounts = (): PromiseLike<void> => {
167
+ public loadAccounts = (): Promise<void> => {
168
168
  return AccountService.getAllAccountDetailsForProvider('kubernetes').then((accounts) => {
169
169
  const selector = this.state.selector;
170
170
  const kind = parseSpinnakerName(selector.manifestName).kind;
@@ -244,8 +244,8 @@ export class ManifestSelector extends React.Component<IManifestSelectorProps, IM
244
244
 
245
245
  private isExpression = (value: string): boolean => (typeof value === 'string' ? value.includes('${') : false);
246
246
 
247
- private search = (kind: string, namespace: string, account: string): PromiseLike<string[]> => {
248
- if (this.isExpression(account)) {
247
+ private search = (kind: string, namespace: string, account: string): Promise<string[]> => {
248
+ if (!kind || !namespace || !account || this.isExpression(account)) {
249
249
  return Promise.resolve([]);
250
250
  }
251
251
  return ManifestKindSearchService.search(kind, namespace, account).then((results) =>
@@ -310,7 +310,6 @@ export class ManifestSelector extends React.Component<IManifestSelectorProps, IM
310
310
  };
311
311
 
312
312
  public render() {
313
- const { TargetSelect } = NgReact;
314
313
  const selectedMode = this.getSelectedMode();
315
314
  const modes = this.props.modes || [selectedMode];
316
315
  const { selector, accounts, kinds, namespaces, resources, loading } = this.state;
@@ -7,7 +7,7 @@ export class ManifestTrafficService {
7
7
  public static readonly ENABLE_MANIFEST_OPERATION = 'enableManifest';
8
8
  public static readonly DISABLE_MANIFEST_OPERATION = 'disableManifest';
9
9
 
10
- public static enable = (manifest: IManifest, application: Application, reason?: string): PromiseLike<ITask> => {
10
+ public static enable = (manifest: IManifest, application: Application, reason?: string): Promise<ITask> => {
11
11
  return TaskExecutor.executeTask({
12
12
  job: [
13
13
  {
@@ -24,7 +24,7 @@ export class ManifestTrafficService {
24
24
  });
25
25
  };
26
26
 
27
- public static disable = (manifest: IManifest, application: Application, reason?: string): PromiseLike<ITask> => {
27
+ public static disable = (manifest: IManifest, application: Application, reason?: string): Promise<ITask> => {
28
28
  return TaskExecutor.executeTask({
29
29
  job: [
30
30
  {
@@ -47,7 +47,7 @@ export class ManifestWizard extends React.Component<IKubernetesManifestModalProp
47
47
  taskMonitor: new TaskMonitor({
48
48
  application: props.application,
49
49
  title: `${this.props.isNew ? 'Deploying' : 'Updating'} your manifest`,
50
- modalInstance: TaskMonitor.modalInstanceEmulation(() => this.props.dismissModal()),
50
+ onDismiss: () => this.props.dismissModal(),
51
51
  }),
52
52
  };
53
53
  }
@@ -2,7 +2,7 @@ import React from 'react';
2
2
 
3
3
  import { noop, StageConfigField } from '@spinnaker/core';
4
4
 
5
- import type { IDeleteOptions } from '../../../manifest/delete/delete.controller';
5
+ import type { IDeleteOptions } from '../../../manifest/delete/delete.types';
6
6
 
7
7
  import './deleteManifestOptionsForm.less';
8
8
 
@@ -4,7 +4,7 @@ import React from 'react';
4
4
  import type { Application, IStage, IStageConfigProps } from '@spinnaker/core';
5
5
 
6
6
  import DeleteManifestOptionsForm from './DeleteManifestOptionsForm';
7
- import type { IDeleteOptions } from '../../../manifest/delete/delete.controller';
7
+ import type { IDeleteOptions } from '../../../manifest/delete/delete.types';
8
8
  import type { IManifestSelector } from '../../../manifest/selector/IManifestSelector';
9
9
  import { SelectorMode } from '../../../manifest/selector/IManifestSelector';
10
10
  import { ManifestSelector } from '../../../manifest/selector/ManifestSelector';
@@ -12,6 +12,7 @@ import type {
12
12
  import {
13
13
  ArtifactTypePatterns,
14
14
  CheckboxInput,
15
+ NumberInput,
15
16
  RadioButtonInput,
16
17
  SETTINGS,
17
18
  StageArtifactSelectorDelegate,
@@ -254,6 +255,21 @@ export class DeployManifestStageForm extends React.Component<
254
255
  onConfigChange={(config) => this.props.formik.setFieldValue('trafficManagement', config)}
255
256
  selectedAccount={stage.account}
256
257
  />
258
+ <hr />
259
+ <h4>Stability Timeout</h4>
260
+ <StageConfigField label="Timeout (minutes)" helpKey="kubernetes.manifest.stableManifestTimeout">
261
+ <NumberInput
262
+ value={stage.stableManifestTimeoutMinutes ?? ''}
263
+ min={1}
264
+ onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
265
+ const val = e.target.value;
266
+ this.props.formik.setFieldValue('stableManifestTimeoutMinutes', val === '' ? undefined : Number(val));
267
+ }}
268
+ />
269
+ <div className="form-inline">
270
+ <span className="input-label sp-padding-xs-left">Leave blank to use the default (30 minutes).</span>
271
+ </div>
272
+ </StageConfigField>
257
273
  </div>
258
274
  );
259
275
  }
@@ -1,86 +1,13 @@
1
- import type { IQService, IScope } from 'angular';
2
- import { mock } from 'angular';
3
-
4
1
  import type { Application } from '@spinnaker/core';
5
- import { ApplicationModelBuilder, noop } from '@spinnaker/core';
2
+ import { noop } from '@spinnaker/core';
6
3
  import { ManifestCopier } from './ManifestCopier';
7
4
 
8
5
  describe('<ManifestCopier />', () => {
9
6
  let application: Application;
10
7
 
11
- beforeEach(
12
- mock.inject(($q: IQService, $rootScope: IScope) => {
13
- const $scope = $rootScope.$new();
14
- // The application model implicitly depends on a bunch of Angular things, which is why
15
- // we need the Angular mock environment (even though we're testing a React component).
16
- application = ApplicationModelBuilder.createApplicationForTests(
17
- 'app',
18
- {
19
- key: 'serverGroups',
20
- loader: () =>
21
- $q.resolve([
22
- // Replica sets in same cluster, no manager.
23
- {
24
- name: 'replicaSet my-replicaSet-v002',
25
- region: 'default',
26
- category: 'serverGroup',
27
- account: 'my-k8s-account',
28
- cloudProvider: 'kubernetes',
29
- cluster: 'replicaSet my-replicaSet',
30
- },
31
- {
32
- name: 'replicaSet my-replicaSet-v001',
33
- region: 'default',
34
- category: 'serverGroup',
35
- account: 'my-k8s-account',
36
- cloudProvider: 'kubernetes',
37
- cluster: 'replicaSet my-replicaSet',
38
- },
39
- // Replica set managed by deployment.
40
- {
41
- name: 'replicaSet my-managed-replicaSet-v001',
42
- region: 'default',
43
- category: 'serverGroup',
44
- account: 'my-k8s-account',
45
- cloudProvider: 'kubernetes',
46
- cluster: 'deployment my-deployment',
47
- serverGroupManagers: [{ name: 'deployment my-deployment' }],
48
- },
49
- ]),
50
- onLoad: (_app: Application, data: any) => $q.resolve(data),
51
- defaultData: [],
52
- },
53
- {
54
- key: 'serverGroupManagers',
55
- loader: () =>
56
- $q.resolve([
57
- {
58
- name: 'deployment my-deployment',
59
- region: 'default',
60
- account: 'my-k8s-account',
61
- cloudProvider: 'kubernetes',
62
- },
63
- ]),
64
- onLoad: (_app: Application, data: any) => $q.resolve(data),
65
- defaultData: [],
66
- },
67
- {
68
- key: 'securityGroups',
69
- loader: () => $q.resolve([]),
70
- onLoad: (_app: Application, data: any) => $q.resolve(data),
71
- defaultData: [],
72
- },
73
- {
74
- key: 'loadBalancers',
75
- loader: () => $q.resolve([]),
76
- onLoad: (_app: Application, data: any) => $q.resolve(data),
77
- defaultData: [],
78
- },
79
- );
80
- application.refresh();
81
- $scope.$digest();
82
- }),
83
- );
8
+ beforeEach(() => {
9
+ application = buildApplication();
10
+ });
84
11
 
85
12
  describe('dropdown grouping & ordering', () => {
86
13
  it('sorts deployments to the top of the list', () => {
@@ -108,3 +35,51 @@ const buildProps = (application: Application) => ({
108
35
  onManifestSelected: noop,
109
36
  show: true,
110
37
  });
38
+
39
+ const buildApplication = (): Application => {
40
+ const dataSources = {
41
+ serverGroups: [
42
+ // Replica sets in same cluster, no manager.
43
+ {
44
+ name: 'replicaSet my-replicaSet-v002',
45
+ region: 'default',
46
+ category: 'serverGroup',
47
+ account: 'my-k8s-account',
48
+ cloudProvider: 'kubernetes',
49
+ cluster: 'replicaSet my-replicaSet',
50
+ },
51
+ {
52
+ name: 'replicaSet my-replicaSet-v001',
53
+ region: 'default',
54
+ category: 'serverGroup',
55
+ account: 'my-k8s-account',
56
+ cloudProvider: 'kubernetes',
57
+ cluster: 'replicaSet my-replicaSet',
58
+ },
59
+ // Replica set managed by deployment.
60
+ {
61
+ name: 'replicaSet my-managed-replicaSet-v001',
62
+ region: 'default',
63
+ category: 'serverGroup',
64
+ account: 'my-k8s-account',
65
+ cloudProvider: 'kubernetes',
66
+ cluster: 'deployment my-deployment',
67
+ serverGroupManagers: [{ name: 'deployment my-deployment' }],
68
+ },
69
+ ],
70
+ serverGroupManagers: [
71
+ {
72
+ name: 'deployment my-deployment',
73
+ region: 'default',
74
+ account: 'my-k8s-account',
75
+ cloudProvider: 'kubernetes',
76
+ },
77
+ ],
78
+ securityGroups: [],
79
+ loadBalancers: [],
80
+ };
81
+
82
+ return ({
83
+ getDataSource: (key: keyof typeof dataSources) => ({ data: dataSources[key] }),
84
+ } as unknown) as Application;
85
+ };
@@ -5,6 +5,7 @@ import { StageConfigField } from '@spinnaker/core';
5
5
 
6
6
  import type { IManifestDeploymentOptionsProps } from './ManifestDeploymentOptions';
7
7
  import { defaultTrafficManagementConfig, ManifestDeploymentOptions } from './ManifestDeploymentOptions';
8
+ import { ManifestKindSearchService } from '../../../manifest/ManifestKindSearch';
8
9
 
9
10
  describe('<ManifestDeploymentOptions />', () => {
10
11
  const onConfigChangeSpy = jasmine.createSpy('onConfigChangeSpy');
@@ -12,6 +13,7 @@ describe('<ManifestDeploymentOptions />', () => {
12
13
  let props: IManifestDeploymentOptionsProps;
13
14
 
14
15
  beforeEach(() => {
16
+ spyOn(ManifestKindSearchService, 'search').and.returnValue(Promise.resolve([]));
15
17
  props = {
16
18
  accounts: [],
17
19
  config: defaultTrafficManagementConfig,
@@ -2,11 +2,17 @@ import { get } from 'lodash';
2
2
  import React from 'react';
3
3
 
4
4
  import type { IExecutionDetailsSectionProps, IManifest } from '@spinnaker/core';
5
- import { CollapsibleElement, ExecutionDetailsSection, SETTINGS, StageFailureMessage } from '@spinnaker/core';
5
+ import {
6
+ CollapsibleElement,
7
+ ExecutionDetailsSection,
8
+ ManifestYaml,
9
+ SETTINGS,
10
+ StageFailureMessage,
11
+ } from '@spinnaker/core';
6
12
 
7
13
  import { ManifestStatus } from './ManifestStatus';
8
- import type { IStageManifest } from '../../../../manifest/manifest.service';
9
- import { KubernetesManifestService } from '../../../../manifest/manifest.service';
14
+ import type { IStageManifest, IStoredManifestReference } from '../../../../manifest/manifest.service';
15
+ import { isStoredManifestReference, KubernetesManifestService } from '../../../../manifest/manifest.service';
10
16
 
11
17
  import './DeployStatus.less';
12
18
 
@@ -38,7 +44,9 @@ export class DeployStatus extends React.Component<IExecutionDetailsSectionProps,
38
44
  }
39
45
 
40
46
  public componentDidUpdate(_prevProps: IExecutionDetailsSectionProps, prevState: IDeployStatusState) {
41
- const manifests: IStageManifest[] = get(this.props.stage, ['context', 'outputs.manifests'], []).filter((m) => !!m);
47
+ const manifests: IStageManifest[] = get(this.props.stage, ['context', 'outputs.manifests'], [])
48
+ .filter((m) => !!m)
49
+ .filter((m) => !isStoredManifestReference(m));
42
50
  const manifestIds = manifests.map((m) => KubernetesManifestService.manifestIdentifier(m)).sort();
43
51
  if (prevState.manifestIds.join('') !== manifestIds.join('')) {
44
52
  this.unsubscribeAll();
@@ -71,6 +79,16 @@ export class DeployStatus extends React.Component<IExecutionDetailsSectionProps,
71
79
  this.state.subscriptions.forEach(({ unsubscribe }) => unsubscribe());
72
80
  }
73
81
 
82
+ // Manifests the artifact-store entity storage feature replaced with a reference instead
83
+ // of expanding them before deck saw them. We can't determine their kind/name/namespace
84
+ // (and so can't subscribe to live status for them) without fetching the content, so we
85
+ // let the user load them on demand instead, same as a baked manifest's YAML.
86
+ private get storedManifestRefs(): IStoredManifestReference[] {
87
+ return get(this.props.stage, ['context', 'outputs.manifests'], [])
88
+ .filter((m: unknown) => !!m)
89
+ .filter(isStoredManifestReference);
90
+ }
91
+
74
92
  private stageManifestToIManifest(manifest: IStageManifest, account: string): IManifest {
75
93
  return {
76
94
  name: get(manifest, 'metadata.name', ''),
@@ -113,6 +131,23 @@ export class DeployStatus extends React.Component<IExecutionDetailsSectionProps,
113
131
  </div>
114
132
  </div>
115
133
  )}
134
+ {!!this.storedManifestRefs.length && (
135
+ <div className="row">
136
+ <div className="col-md-12">
137
+ <div className="well alert alert-info">
138
+ {this.storedManifestRefs.map((ref, i) => (
139
+ <div key={i}>
140
+ <ManifestYaml
141
+ linkName="Load Manifest Details"
142
+ modalTitle="Manifest"
143
+ manifestUri={ref.reference.replace(/^ref?:\/\//, '')}
144
+ />
145
+ </div>
146
+ ))}
147
+ </div>
148
+ </div>
149
+ </div>
150
+ )}
116
151
  </ExecutionDetailsSection>
117
152
  </div>
118
153
  );