@spinnaker/cloudrun 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 (97) hide show
  1. package/dist/cloudrun.module.d.ts +1 -1
  2. package/dist/common/ComponentUrlDetails.d.ts +7 -0
  3. package/dist/common/LoadBalancerMessage.d.ts +4 -0
  4. package/dist/index.d.ts +1 -1
  5. package/dist/index.js +1 -1
  6. package/dist/index.js.map +1 -1
  7. package/dist/instance/details/CloudrunInstanceDetails.d.ts +23 -0
  8. package/dist/loadBalancer/configure/wizard/CloudrunLoadBalancerModal.d.ts +32 -0
  9. package/dist/loadBalancer/details/CloudrunLoadBalancerActions.d.ts +3 -0
  10. package/dist/loadBalancer/details/sections.d.ts +7 -0
  11. package/dist/loadBalancer/details/useCloudrunLoadBalancerDetails.d.ts +4 -0
  12. package/dist/loadBalancer/index.d.ts +4 -2
  13. package/dist/loadBalancer/loadBalancerTransformer.d.ts +2 -6
  14. package/dist/manifest/manifest.service.d.ts +5 -0
  15. package/dist/manifest/manifestCommandBuilder.service.d.ts +1 -1
  16. package/dist/pipeline/stages/deployManifest/manifestStatus/DeployStatus.d.ts +1 -0
  17. package/dist/pipeline/stages/deployManifest/manifestStatus/ManifestDetailsLink.d.ts +6 -3
  18. package/dist/pipeline/stages/editLoadBalancer/CloudrunEditLoadBalancerExecutionDetails.d.ts +7 -0
  19. package/dist/pipeline/stages/editLoadBalancer/CloudrunEditLoadBalancerStageConfig.d.ts +30 -0
  20. package/dist/pipeline/stages/editLoadBalancer/cloudrunEditLoadBalancerStage.d.ts +2 -1
  21. package/dist/serverGroup/configure/serverGroupCommandBuilder.service.d.ts +4 -6
  22. package/dist/serverGroup/configure/wizard/BasicSettings.d.ts +5 -2
  23. package/dist/serverGroup/configure/wizard/serverGroupWizard.d.ts +18 -6
  24. package/dist/serverGroup/details/CloudrunServerGroupActions.d.ts +6 -0
  25. package/dist/serverGroup/details/cloudrunServerGroupDetailsGetter.d.ts +3 -0
  26. package/dist/serverGroup/details/sections.d.ts +5 -0
  27. package/dist/serverGroup/index.d.ts +3 -1
  28. package/dist/serverGroup/serverGroupTransformer.service.d.ts +1 -5
  29. package/package.json +4 -11
  30. package/src/cloudrun.module.spec.ts +22 -0
  31. package/src/cloudrun.module.ts +54 -59
  32. package/src/common/ComponentUrlDetails.tsx +25 -0
  33. package/src/common/LoadBalancerMessage.tsx +17 -0
  34. package/src/index.ts +2 -1
  35. package/src/instance/details/CloudrunInstanceDetails.tsx +164 -0
  36. package/src/loadBalancer/configure/wizard/CloudrunLoadBalancerModal.spec.tsx +85 -0
  37. package/src/loadBalancer/configure/wizard/CloudrunLoadBalancerModal.tsx +395 -0
  38. package/src/loadBalancer/details/CloudrunLoadBalancerActions.tsx +66 -0
  39. package/src/loadBalancer/details/sections.spec.ts +19 -0
  40. package/src/loadBalancer/details/sections.tsx +91 -0
  41. package/src/loadBalancer/details/useCloudrunLoadBalancerDetails.ts +56 -0
  42. package/src/loadBalancer/index.ts +4 -2
  43. package/src/loadBalancer/loadBalancerTransformer.ts +3 -13
  44. package/src/manifest/manifest.service.ts +18 -1
  45. package/src/manifest/manifestCommandBuilder.service.ts +46 -53
  46. package/src/pipeline/stages/deployManifest/manifestStatus/DeployStatus.tsx +33 -4
  47. package/src/pipeline/stages/deployManifest/manifestStatus/ManifestDetailsLink.spec.tsx +35 -0
  48. package/src/pipeline/stages/deployManifest/manifestStatus/ManifestDetailsLink.tsx +10 -5
  49. package/src/pipeline/stages/editLoadBalancer/CloudrunEditLoadBalancerExecutionDetails.tsx +70 -0
  50. package/src/pipeline/stages/editLoadBalancer/CloudrunEditLoadBalancerStageConfig.tsx +245 -0
  51. package/src/pipeline/stages/editLoadBalancer/cloudrunEditLoadBalancerStage.spec.ts +205 -0
  52. package/src/pipeline/stages/editLoadBalancer/cloudrunEditLoadBalancerStage.ts +14 -71
  53. package/src/serverGroup/configure/serverGroupCommandBuilder.service.ts +5 -15
  54. package/src/serverGroup/configure/wizard/BasicSettings.tsx +10 -8
  55. package/src/serverGroup/configure/wizard/serverGroupWizard.tsx +59 -15
  56. package/src/serverGroup/details/CloudrunServerGroupActions.tsx +80 -0
  57. package/src/serverGroup/details/cloudrunServerGroupDetailsGetter.spec.ts +46 -0
  58. package/src/serverGroup/details/cloudrunServerGroupDetailsGetter.ts +53 -0
  59. package/src/serverGroup/details/sections.tsx +60 -0
  60. package/src/serverGroup/index.ts +3 -1
  61. package/src/serverGroup/routerConsumers.spec.tsx +152 -0
  62. package/src/serverGroup/serverGroupTransformer.service.ts +2 -12
  63. package/dist/common/componentUrlDetails.component.d.ts +0 -1
  64. package/dist/common/conditionalDescriptionListItem.component.d.ts +0 -1
  65. package/dist/common/loadBalancerMessage.component.d.ts +0 -1
  66. package/dist/instance/details/details.controller.d.ts +0 -1
  67. package/dist/loadBalancer/configure/wizard/allocationConfigurationRow.component.d.ts +0 -1
  68. package/dist/loadBalancer/configure/wizard/basicSettings.component.d.ts +0 -1
  69. package/dist/loadBalancer/configure/wizard/stageAllocationConfigurationRow.component.d.ts +0 -1
  70. package/dist/loadBalancer/configure/wizard/wizard.controller.d.ts +0 -2
  71. package/dist/loadBalancer/details/details.controller.d.ts +0 -1
  72. package/dist/pipeline/pipeline.module.d.ts +0 -1
  73. package/dist/pipeline/stages/editLoadBalancer/loadBalancerChoice.modal.controller.d.ts +0 -1
  74. package/dist/serverGroup/details/details.controller.d.ts +0 -1
  75. package/src/common/componentUrlDetails.component.ts +0 -22
  76. package/src/common/conditionalDescriptionListItem.component.ts +0 -37
  77. package/src/common/loadBalancerMessage.component.html +0 -11
  78. package/src/common/loadBalancerMessage.component.ts +0 -13
  79. package/src/instance/details/details.controller.ts +0 -84
  80. package/src/instance/details/details.html +0 -68
  81. package/src/loadBalancer/configure/wizard/allocationConfigurationRow.component.ts +0 -70
  82. package/src/loadBalancer/configure/wizard/basicSettings.component.html +0 -44
  83. package/src/loadBalancer/configure/wizard/basicSettings.component.ts +0 -86
  84. package/src/loadBalancer/configure/wizard/stageAllocationConfigurationRow.component.html +0 -45
  85. package/src/loadBalancer/configure/wizard/stageAllocationConfigurationRow.component.ts +0 -84
  86. package/src/loadBalancer/configure/wizard/wizard.controller.ts +0 -157
  87. package/src/loadBalancer/configure/wizard/wizard.html +0 -39
  88. package/src/loadBalancer/configure/wizard/wizard.less +0 -9
  89. package/src/loadBalancer/details/details.controller.ts +0 -140
  90. package/src/loadBalancer/details/details.html +0 -89
  91. package/src/pipeline/pipeline.module.ts +0 -6
  92. package/src/pipeline/stages/editLoadBalancer/editLoadBalancerExecutionDetails.html +0 -33
  93. package/src/pipeline/stages/editLoadBalancer/editLoadBalancerStage.html +0 -51
  94. package/src/pipeline/stages/editLoadBalancer/loadBalancerChoice.modal.controller.ts +0 -65
  95. package/src/pipeline/stages/editLoadBalancer/loadBalancerChoice.modal.html +0 -53
  96. package/src/serverGroup/details/details.controller.ts +0 -134
  97. package/src/serverGroup/details/details.html +0 -94
@@ -0,0 +1,164 @@
1
+ import { UISref } from '@uirouter/react';
2
+ import { flattenDeep } from 'lodash';
3
+ import React from 'react';
4
+
5
+ import type { Application, ILoadBalancer } from '@spinnaker/core';
6
+ import {
7
+ AccountTag,
8
+ CollapsibleSection,
9
+ InstanceReader,
10
+ RecentHistoryService,
11
+ Spinner,
12
+ timestamp,
13
+ } from '@spinnaker/core';
14
+
15
+ import type { ICloudrunInstance } from '../../common/domain';
16
+
17
+ interface ICloudrunInstanceDetailsProps {
18
+ app: Application;
19
+ instance: {
20
+ instanceId: string;
21
+ };
22
+ }
23
+
24
+ interface InstanceManager {
25
+ account: string;
26
+ region: string;
27
+ category: string;
28
+ name: string;
29
+ instances: ICloudrunInstance[];
30
+ }
31
+
32
+ interface ICloudrunInstanceDetailsState {
33
+ loading: boolean;
34
+ instance?: ICloudrunInstance;
35
+ instanceIdNotFound?: string;
36
+ }
37
+
38
+ export class CloudrunInstanceDetails extends React.Component<
39
+ ICloudrunInstanceDetailsProps,
40
+ ICloudrunInstanceDetailsState
41
+ > {
42
+ public state: ICloudrunInstanceDetailsState = { loading: true };
43
+ private isUnmounted = false;
44
+
45
+ public componentDidMount(): void {
46
+ this.props.app
47
+ .ready()
48
+ .then(() => this.retrieveInstance())
49
+ .then((instance) => !this.isUnmounted && this.setState({ instance, loading: false }))
50
+ .catch(
51
+ () =>
52
+ !this.isUnmounted && this.setState({ instanceIdNotFound: this.props.instance.instanceId, loading: false }),
53
+ );
54
+ }
55
+
56
+ public componentWillUnmount(): void {
57
+ this.isUnmounted = true;
58
+ }
59
+
60
+ private retrieveInstance(): Promise<ICloudrunInstance> {
61
+ const { app, instance } = this.props;
62
+ const dataSources: InstanceManager[] = flattenDeep([
63
+ app.getDataSource('serverGroups').data,
64
+ app.getDataSource('loadBalancers').data,
65
+ app.getDataSource('loadBalancers').data.map((loadBalancer: ILoadBalancer) => loadBalancer.serverGroups),
66
+ ]);
67
+ const instanceManager = dataSources.find((dataSource) =>
68
+ dataSource.instances.some((possibleMatch) => possibleMatch.id === instance.instanceId),
69
+ );
70
+
71
+ if (!instanceManager) {
72
+ return Promise.reject();
73
+ }
74
+
75
+ const recentHistoryExtraData: { [key: string]: string } = {
76
+ region: instanceManager.region,
77
+ account: instanceManager.account,
78
+ };
79
+ if (instanceManager.category === 'serverGroup') {
80
+ recentHistoryExtraData.serverGroup = instanceManager.name;
81
+ }
82
+ RecentHistoryService.addExtraDataToLatest('instances', recentHistoryExtraData);
83
+
84
+ return InstanceReader.getInstanceDetails(instanceManager.account, instanceManager.region, instance.instanceId).then(
85
+ (instanceDetails: ICloudrunInstance) => ({
86
+ ...instanceDetails,
87
+ account: instanceManager.account,
88
+ region: instanceManager.region,
89
+ }),
90
+ );
91
+ }
92
+
93
+ public render() {
94
+ const { loading, instance, instanceIdNotFound } = this.state;
95
+ const upToolTip = "A Cloud Run instance is 'Up' if a load balancer is directing traffic to its server group.";
96
+ const outOfServiceToolTip =
97
+ "A Cloud Run instance is 'Out Of Service' if no load balancers are directing traffic to its server group.";
98
+
99
+ return (
100
+ <div className="details-panel">
101
+ <div className="header">
102
+ <div className="header-text horizontal middle">
103
+ <h3 className="horizontal middle space-between flex-1">{instance ? instance.name : instanceIdNotFound}</h3>
104
+ </div>
105
+ {loading && <Spinner size="small" />}
106
+ </div>
107
+ {!loading && instance && (
108
+ <div className="content">
109
+ <CollapsibleSection heading="Instance Information" defaultExpanded={true}>
110
+ <dl className="dl-horizontal dl-narrow">
111
+ <dt>Launched</dt>
112
+ <dd>{instance.launchTime && timestamp(instance.launchTime)}</dd>
113
+ <dt>In</dt>
114
+ <dd>
115
+ <AccountTag account={instance.account} />
116
+ </dd>
117
+ {instance.serverGroup && <dt>Server Group</dt>}
118
+ {instance.serverGroup && (
119
+ <dd>
120
+ <UISref
121
+ to="^.serverGroup"
122
+ params={{
123
+ region: instance.region,
124
+ accountId: instance.account,
125
+ serverGroup: instance.serverGroup,
126
+ provider: 'cloudrun',
127
+ }}
128
+ >
129
+ <a>{instance.serverGroup}</a>
130
+ </UISref>
131
+ </dd>
132
+ )}
133
+ <dt>Region</dt>
134
+ <dd>{instance.region}</dd>
135
+ {instance.vmZoneName && <dt>Zone</dt>}
136
+ {instance.vmZoneName && <dd>{instance.vmZoneName}</dd>}
137
+ </dl>
138
+ </CollapsibleSection>
139
+ <CollapsibleSection heading="Status" defaultExpanded={true}>
140
+ <dl>
141
+ <dt>Load Balancer</dt>
142
+ <dd>
143
+ <span className="pull-left" title={instance.healthState === 'Up' ? upToolTip : outOfServiceToolTip}>
144
+ <span className={`glyphicon glyphicon-${instance.healthState}-triangle`} />{' '}
145
+ {instance.loadBalancers?.[0]}
146
+ </span>
147
+ </dd>
148
+ </dl>
149
+ </CollapsibleSection>
150
+ </div>
151
+ )}
152
+ {!loading && !instance && (
153
+ <div className="content">
154
+ <div className="content-section">
155
+ <div className="content-body text-center">
156
+ <h3>Instance not found.</h3>
157
+ </div>
158
+ </div>
159
+ </div>
160
+ )}
161
+ </div>
162
+ );
163
+ }
164
+ }
@@ -0,0 +1,85 @@
1
+ import { CloudrunLoadBalancerModalComponent as CloudrunLoadBalancerModal } from './CloudrunLoadBalancerModal';
2
+
3
+ describe('CloudrunLoadBalancerModal', () => {
4
+ function buildModal(overrides: any = {}) {
5
+ const props = {
6
+ app: {
7
+ loadBalancers: { refresh: jasmine.createSpy('refresh'), onNextRefresh: jasmine.createSpy('onNextRefresh') },
8
+ },
9
+ closeModal: jasmine.createSpy('closeModal'),
10
+ dismissModal: jasmine.createSpy('dismissModal'),
11
+ isNew: false,
12
+ loadBalancer: { name: 'service', account: 'test', region: 'us-central1' },
13
+ router: {},
14
+ stateParams: {},
15
+ stateService: { go: jasmine.createSpy('go'), includes: () => false },
16
+ ...overrides,
17
+ } as any;
18
+
19
+ return new CloudrunLoadBalancerModal(props);
20
+ }
21
+
22
+ it('dismisses the modal when edit conversion fails', async () => {
23
+ const modal = buildModal();
24
+ (modal as any).transformer = {
25
+ convertLoadBalancerForEditing: () => Promise.reject(new Error('conversion failed')),
26
+ };
27
+
28
+ modal.componentDidMount();
29
+ await Promise.resolve();
30
+ await Promise.resolve();
31
+
32
+ expect(modal.props.dismissModal).toHaveBeenCalled();
33
+ });
34
+
35
+ it('ignores application refresh callbacks after unmount', () => {
36
+ const modal = buildModal();
37
+
38
+ modal.componentWillUnmount();
39
+ (modal as any).onApplicationRefresh();
40
+
41
+ expect(modal.props.dismissModal).not.toHaveBeenCalled();
42
+ });
43
+
44
+ it('owns its refresh subscription across replacement and unmount', () => {
45
+ const firstUnsubscribe = jasmine.createSpy('firstUnsubscribe');
46
+ const secondUnsubscribe = jasmine.createSpy('secondUnsubscribe');
47
+ const callbacks: Array<() => void> = [];
48
+ const onNextRefresh = jasmine.createSpy('onNextRefresh').and.callFake((callback: () => void) => {
49
+ callbacks.push(callback);
50
+ return callbacks.length === 1 ? firstUnsubscribe : secondUnsubscribe;
51
+ });
52
+ const refresh = jasmine.createSpy('refresh');
53
+ const modal = buildModal({ app: { loadBalancers: { onNextRefresh, refresh } } }) as any;
54
+
55
+ modal.onTaskComplete();
56
+
57
+ expect(onNextRefresh.calls.first().invocationOrder).toBeLessThan(refresh.calls.first().invocationOrder);
58
+
59
+ modal.onTaskComplete();
60
+
61
+ expect(firstUnsubscribe).toHaveBeenCalledTimes(1);
62
+
63
+ modal.componentWillUnmount();
64
+ callbacks[1]();
65
+
66
+ expect(secondUnsubscribe).toHaveBeenCalledTimes(1);
67
+ expect(modal.applicationRefreshUnsubscribe).toBeUndefined();
68
+ expect(modal.props.dismissModal).not.toHaveBeenCalled();
69
+ expect(modal.props.stateService.go).not.toHaveBeenCalled();
70
+ });
71
+
72
+ it('opens updated load balancer details through the injected state service', () => {
73
+ const modal = buildModal();
74
+ modal.state.loadBalancer = { credentials: 'test', name: 'service', region: 'us-central1' } as any;
75
+
76
+ (modal as any).onApplicationRefresh();
77
+
78
+ expect(modal.props.stateService.go).toHaveBeenCalledWith('.loadBalancerDetails', {
79
+ accountId: 'test',
80
+ name: 'service',
81
+ provider: 'cloudrun',
82
+ region: 'us-central1',
83
+ });
84
+ });
85
+ });
@@ -0,0 +1,395 @@
1
+ import { cloneDeep, difference, uniq } from 'lodash';
2
+ import React from 'react';
3
+
4
+ import type { ILoadBalancerModalProps, IRouterInjectedProps } from '@spinnaker/core';
5
+ import { HelpField, LoadBalancerWriter, ReactModal, StageConstants, TaskMonitor, withRouter } from '@spinnaker/core';
6
+
7
+ import { LoadBalancerMessage } from '../../../common/LoadBalancerMessage';
8
+ import type { ICloudrunLoadBalancer } from '../../../common/domain';
9
+ import type { ICloudrunAllocationDescription, ICloudrunTrafficSplitDescription } from '../../loadBalancerTransformer';
10
+ import { CloudrunLoadBalancerTransformer, CloudrunLoadBalancerUpsertDescription } from '../../loadBalancerTransformer';
11
+
12
+ export interface ICloudrunLoadBalancerModalState {
13
+ loading: boolean;
14
+ loadBalancer?: CloudrunLoadBalancerUpsertDescription;
15
+ taskMonitor: TaskMonitor;
16
+ }
17
+
18
+ export interface ICloudrunLoadBalancerModalProps extends ILoadBalancerModalProps {
19
+ isNew?: boolean;
20
+ }
21
+
22
+ function allocationTotalIsValid(splitDescription: ICloudrunTrafficSplitDescription): boolean {
23
+ return (
24
+ splitDescription.allocationDescriptions.reduce((sum, description) => sum + Number(description.percent), 0) === 100
25
+ );
26
+ }
27
+
28
+ export class CloudrunLoadBalancerModalComponent extends React.Component<
29
+ ICloudrunLoadBalancerModalProps & IRouterInjectedProps,
30
+ ICloudrunLoadBalancerModalState
31
+ > {
32
+ public static show(props: ICloudrunLoadBalancerModalProps): Promise<CloudrunLoadBalancerUpsertDescription> {
33
+ return ReactModal.show(CloudrunLoadBalancerModal, props, { dialogClassName: 'wizard-modal modal-lg' });
34
+ }
35
+
36
+ private transformer = new CloudrunLoadBalancerTransformer();
37
+ private isUnmounted = false;
38
+ private applicationRefreshUnsubscribe?: () => void;
39
+
40
+ constructor(props: ICloudrunLoadBalancerModalProps & IRouterInjectedProps) {
41
+ super(props);
42
+ this.state = {
43
+ loading: !props.isNew,
44
+ taskMonitor: new TaskMonitor({
45
+ application: props.app,
46
+ title: 'Updating your load balancer',
47
+ onDismiss: () => props.dismissModal(),
48
+ onTaskComplete: this.onTaskComplete,
49
+ }),
50
+ };
51
+ }
52
+
53
+ public componentDidMount(): void {
54
+ const { app, isNew, loadBalancer } = this.props;
55
+ if (isNew) {
56
+ return;
57
+ }
58
+ this.transformer
59
+ .convertLoadBalancerForEditing(loadBalancer as ICloudrunLoadBalancer, app)
60
+ .then((convertedLoadBalancer) => {
61
+ if (this.isUnmounted) {
62
+ return;
63
+ }
64
+ const command = this.transformer.convertLoadBalancerToUpsertDescription(
65
+ convertedLoadBalancer as ICloudrunLoadBalancer,
66
+ );
67
+ if ((loadBalancer as ICloudrunLoadBalancer).split && !command.splitDescription) {
68
+ command.splitDescription = CloudrunLoadBalancerUpsertDescription.convertTrafficSplitToTrafficSplitDescription(
69
+ (loadBalancer as ICloudrunLoadBalancer).split,
70
+ );
71
+ } else {
72
+ command.splitDescription = (loadBalancer as any).splitDescription;
73
+ }
74
+ command.mapAllocationsToPercentages();
75
+ this.setState({ loadBalancer: command, loading: false });
76
+ })
77
+ .catch(() => {
78
+ if (!this.isUnmounted) {
79
+ this.props.dismissModal();
80
+ }
81
+ });
82
+ }
83
+
84
+ public componentWillUnmount(): void {
85
+ this.isUnmounted = true;
86
+ this.clearApplicationRefreshSubscription();
87
+ }
88
+
89
+ private onTaskComplete = (): void => {
90
+ this.clearApplicationRefreshSubscription();
91
+ this.applicationRefreshUnsubscribe = this.props.app.loadBalancers.onNextRefresh(this.onApplicationRefresh);
92
+ this.props.app.loadBalancers.refresh();
93
+ };
94
+
95
+ private onApplicationRefresh = (): void => {
96
+ this.clearApplicationRefreshSubscription();
97
+ if (this.isUnmounted) {
98
+ return;
99
+ }
100
+ const { loadBalancer } = this.state;
101
+ this.props.dismissModal();
102
+ const newStateParams = {
103
+ name: loadBalancer.name,
104
+ accountId: loadBalancer.credentials,
105
+ region: loadBalancer.region,
106
+ provider: 'cloudrun',
107
+ };
108
+ if (!this.props.stateService.includes('**.loadBalancerDetails')) {
109
+ this.props.stateService.go('.loadBalancerDetails', newStateParams);
110
+ } else {
111
+ this.props.stateService.go('^.loadBalancerDetails', newStateParams);
112
+ }
113
+ };
114
+
115
+ private clearApplicationRefreshSubscription = (): void => {
116
+ this.applicationRefreshUnsubscribe?.();
117
+ this.applicationRefreshUnsubscribe = undefined;
118
+ };
119
+
120
+ private submit = (): void => {
121
+ const description = cloneDeep(this.state.loadBalancer);
122
+ description.mapAllocationsToPercentages();
123
+ delete description.serverGroups;
124
+
125
+ if (this.props.forPipelineConfig) {
126
+ this.props.closeModal(description);
127
+ } else {
128
+ this.state.taskMonitor.submit(() => LoadBalancerWriter.upsertLoadBalancer(description, this.props.app, 'Update'));
129
+ }
130
+ };
131
+
132
+ private updateLoadBalancer = (loadBalancer: CloudrunLoadBalancerUpsertDescription): void => {
133
+ this.setState({ loadBalancer });
134
+ };
135
+
136
+ public render() {
137
+ const { dismissModal, forPipelineConfig, isNew, loadBalancer: sourceLoadBalancer } = this.props;
138
+ const { loading, loadBalancer, taskMonitor } = this.state;
139
+ const heading = isNew
140
+ ? 'Create New Load Balancer'
141
+ : `Edit ${[
142
+ sourceLoadBalancer.name,
143
+ sourceLoadBalancer.region,
144
+ sourceLoadBalancer.account || sourceLoadBalancer.credentials,
145
+ ].join(':')}`;
146
+ const submitButtonLabel = forPipelineConfig ? 'Done' : 'Update';
147
+ const canSubmit = loadBalancer?.splitDescription && allocationTotalIsValid(loadBalancer.splitDescription);
148
+
149
+ return (
150
+ <form name="form">
151
+ <div className="modal-header">
152
+ <button type="button" className="close" onClick={dismissModal}>
153
+ <span>&times;</span>
154
+ </button>
155
+ <h4 className="modal-title">{heading}</h4>
156
+ </div>
157
+ <div className="modal-body">
158
+ {loading && !isNew && (
159
+ <div style={{ height: 200 }} className="horizontal center middle">
160
+ Loading...
161
+ </div>
162
+ )}
163
+ {isNew && <LoadBalancerMessage showCreateMessage={true} />}
164
+ {!loading && !isNew && loadBalancer && (
165
+ <CloudrunLoadBalancerBasicSettings
166
+ loadBalancer={loadBalancer}
167
+ forPipelineConfig={forPipelineConfig}
168
+ onChange={this.updateLoadBalancer}
169
+ />
170
+ )}
171
+ </div>
172
+ <div className="modal-footer">
173
+ <button type="button" className="btn btn-default" onClick={dismissModal}>
174
+ Cancel
175
+ </button>
176
+ {!isNew && canSubmit && (
177
+ <button
178
+ type="button"
179
+ className="btn btn-primary"
180
+ disabled={taskMonitor.submitting || loading}
181
+ onClick={this.submit}
182
+ >
183
+ {submitButtonLabel}
184
+ </button>
185
+ )}
186
+ </div>
187
+ </form>
188
+ );
189
+ }
190
+ }
191
+
192
+ export const CloudrunLoadBalancerModal = Object.assign(withRouter(CloudrunLoadBalancerModalComponent), {
193
+ show: CloudrunLoadBalancerModalComponent.show,
194
+ });
195
+
196
+ interface ICloudrunLoadBalancerBasicSettingsProps {
197
+ loadBalancer: CloudrunLoadBalancerUpsertDescription;
198
+ forPipelineConfig: boolean;
199
+ onChange: (loadBalancer: CloudrunLoadBalancerUpsertDescription) => void;
200
+ }
201
+
202
+ class CloudrunLoadBalancerBasicSettings extends React.Component<ICloudrunLoadBalancerBasicSettingsProps> {
203
+ private serverGroupsWithoutAllocation(loadBalancer = this.props.loadBalancer): string[] {
204
+ const serverGroupsWithAllocation = loadBalancer.splitDescription.allocationDescriptions.map(
205
+ (description) => description.revisionName,
206
+ );
207
+ const allServerGroups = loadBalancer.serverGroups.map((serverGroup) => serverGroup.name);
208
+ return difference(allServerGroups, serverGroupsWithAllocation);
209
+ }
210
+
211
+ private changeLoadBalancer(mutator: (loadBalancer: CloudrunLoadBalancerUpsertDescription) => void): void {
212
+ const loadBalancer = cloneDeep(this.props.loadBalancer);
213
+ mutator(loadBalancer);
214
+ this.props.onChange(loadBalancer);
215
+ }
216
+
217
+ private addAllocation = (): void => {
218
+ this.changeLoadBalancer((loadBalancer) => {
219
+ const remainingServerGroups = this.serverGroupsWithoutAllocation(loadBalancer);
220
+ loadBalancer.splitDescription.allocationDescriptions.push({
221
+ revisionName: remainingServerGroups[0] || '',
222
+ percent: 0,
223
+ });
224
+ });
225
+ };
226
+
227
+ private removeAllocation = (index: number): void => {
228
+ this.changeLoadBalancer((loadBalancer) => loadBalancer.splitDescription.allocationDescriptions.splice(index, 1));
229
+ };
230
+
231
+ private updateAllocation = (index: number, changes: Partial<ICloudrunAllocationDescription>): void => {
232
+ this.changeLoadBalancer((loadBalancer) => {
233
+ loadBalancer.splitDescription.allocationDescriptions[index] = {
234
+ ...loadBalancer.splitDescription.allocationDescriptions[index],
235
+ ...changes,
236
+ };
237
+ });
238
+ };
239
+
240
+ public render() {
241
+ const { forPipelineConfig, loadBalancer } = this.props;
242
+ const serverGroupOptions = this.serverGroupsWithoutAllocation();
243
+ const showAddButton = forPipelineConfig || serverGroupOptions.length > 0;
244
+ const allocationIsInvalid = !allocationTotalIsValid(loadBalancer.splitDescription);
245
+
246
+ return (
247
+ <div className="row">
248
+ <div className="form-group">
249
+ <div className="col-md-3 sm-label-right">
250
+ Allocations <HelpField id="cloudrun.loadBalancer.allocations" />
251
+ </div>
252
+ <div className="col-md-9">
253
+ {loadBalancer.splitDescription.allocationDescriptions.map((description, index) => (
254
+ <CloudrunAllocationConfigurationRow
255
+ key={index}
256
+ allocationDescription={description}
257
+ loadBalancer={loadBalancer}
258
+ forPipelineConfig={forPipelineConfig}
259
+ serverGroupOptions={serverGroupOptions}
260
+ onChange={(changes) => this.updateAllocation(index, changes)}
261
+ onRemove={() => this.removeAllocation(index)}
262
+ />
263
+ ))}
264
+ {showAddButton && (
265
+ <button type="button" className="add-new col-md-11" onClick={this.addAllocation}>
266
+ <span className="glyphicon glyphicon-plus-sign" /> Add allocation
267
+ </button>
268
+ )}
269
+ </div>
270
+ </div>
271
+ {allocationIsInvalid && (
272
+ <div className="form-group">
273
+ <div className="col-md-12 text-center">
274
+ <p className="alert alert-warning">Allocations must sum to 100%.</p>
275
+ </div>
276
+ </div>
277
+ )}
278
+ </div>
279
+ );
280
+ }
281
+ }
282
+
283
+ interface ICloudrunAllocationConfigurationRowProps {
284
+ allocationDescription: ICloudrunAllocationDescription;
285
+ loadBalancer: CloudrunLoadBalancerUpsertDescription;
286
+ forPipelineConfig: boolean;
287
+ serverGroupOptions: string[];
288
+ onChange: (changes: Partial<ICloudrunAllocationDescription>) => void;
289
+ onRemove: () => void;
290
+ }
291
+
292
+ function CloudrunAllocationConfigurationRow({
293
+ allocationDescription,
294
+ forPipelineConfig,
295
+ loadBalancer,
296
+ onChange,
297
+ onRemove,
298
+ serverGroupOptions,
299
+ }: ICloudrunAllocationConfigurationRowProps) {
300
+ const options = allocationDescription.revisionName
301
+ ? uniq(serverGroupOptions.concat(allocationDescription.revisionName))
302
+ : serverGroupOptions;
303
+ const target = StageConstants.TARGET_LIST.find(
304
+ (targetOption: any) => targetOption.val === allocationDescription.target,
305
+ );
306
+ const targetLabel = target ? target.label : '';
307
+
308
+ return (
309
+ <>
310
+ <div className="form-group">
311
+ <div className="row">
312
+ <div className="col-md-7">
313
+ {forPipelineConfig ? (
314
+ <input
315
+ value={
316
+ allocationDescription.cluster && allocationDescription.target
317
+ ? `${targetLabel} (${allocationDescription.cluster})`
318
+ : ''
319
+ }
320
+ type="text"
321
+ className="form-control input-sm"
322
+ readOnly={true}
323
+ />
324
+ ) : (
325
+ <select
326
+ value={allocationDescription.revisionName || ''}
327
+ className="form-control input-sm"
328
+ onChange={(event) => onChange({ revisionName: event.target.value })}
329
+ >
330
+ {options.map((serverGroup) => (
331
+ <option key={serverGroup} value={serverGroup}>
332
+ {serverGroup}
333
+ </option>
334
+ ))}
335
+ </select>
336
+ )}
337
+ </div>
338
+ <div className="col-md-3">
339
+ <div className="input-group input-group-sm">
340
+ <input
341
+ type="number"
342
+ value={allocationDescription.percent}
343
+ required={true}
344
+ className="form-control input-sm"
345
+ min={0}
346
+ max={100}
347
+ onChange={(event) => onChange({ percent: Number(event.target.value) })}
348
+ />
349
+ <span className="input-group-addon">%</span>
350
+ </div>
351
+ </div>
352
+ <div className="col-md-2">
353
+ <button type="button" className="btn btn-link sm-label" onClick={onRemove}>
354
+ <span className="glyphicon glyphicon-trash" />
355
+ </button>
356
+ </div>
357
+ </div>
358
+ </div>
359
+ {forPipelineConfig && (
360
+ <div className="form-group">
361
+ <div className="well col-md-11" style={{ paddingTop: 5, paddingBottom: 10 }}>
362
+ <div className="row">
363
+ <div className="form-group">
364
+ <div className="col-md-3 sm-label-right">Cluster</div>
365
+ <div className="col-md-7" style={{ marginBottom: 10 }}>
366
+ <input
367
+ value={allocationDescription.cluster || loadBalancer.name}
368
+ type="text"
369
+ className="form-control input-sm"
370
+ readOnly={true}
371
+ />
372
+ </div>
373
+ <div className="col-md-3 sm-label-right">Target</div>
374
+ <div className="col-md-7">
375
+ <select
376
+ value={allocationDescription.target || ''}
377
+ className="form-control input-sm"
378
+ onChange={(event) => onChange({ target: event.target.value, cluster: loadBalancer.name })}
379
+ >
380
+ <option value="">Select...</option>
381
+ {StageConstants.TARGET_LIST.map((targetOption: any) => (
382
+ <option key={targetOption.val} value={targetOption.val}>
383
+ {targetOption.label}
384
+ </option>
385
+ ))}
386
+ </select>
387
+ </div>
388
+ </div>
389
+ </div>
390
+ </div>
391
+ </div>
392
+ )}
393
+ </>
394
+ );
395
+ }