@spinnaker/cloudrun 2026.2.3 → 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.
- package/dist/cloudrun.module.d.ts +1 -1
- package/dist/common/ComponentUrlDetails.d.ts +7 -0
- package/dist/common/LoadBalancerMessage.d.ts +4 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/instance/details/CloudrunInstanceDetails.d.ts +23 -0
- package/dist/loadBalancer/configure/wizard/CloudrunLoadBalancerModal.d.ts +32 -0
- package/dist/loadBalancer/details/CloudrunLoadBalancerActions.d.ts +3 -0
- package/dist/loadBalancer/details/sections.d.ts +7 -0
- package/dist/loadBalancer/details/useCloudrunLoadBalancerDetails.d.ts +4 -0
- package/dist/loadBalancer/index.d.ts +4 -2
- package/dist/loadBalancer/loadBalancerTransformer.d.ts +2 -6
- package/dist/manifest/manifest.service.d.ts +5 -0
- package/dist/manifest/manifestCommandBuilder.service.d.ts +1 -1
- package/dist/pipeline/stages/deployManifest/manifestStatus/DeployStatus.d.ts +1 -0
- package/dist/pipeline/stages/deployManifest/manifestStatus/ManifestDetailsLink.d.ts +6 -3
- package/dist/pipeline/stages/editLoadBalancer/CloudrunEditLoadBalancerExecutionDetails.d.ts +7 -0
- package/dist/pipeline/stages/editLoadBalancer/CloudrunEditLoadBalancerStageConfig.d.ts +30 -0
- package/dist/pipeline/stages/editLoadBalancer/cloudrunEditLoadBalancerStage.d.ts +2 -1
- package/dist/serverGroup/configure/serverGroupCommandBuilder.service.d.ts +4 -6
- package/dist/serverGroup/configure/wizard/BasicSettings.d.ts +5 -2
- package/dist/serverGroup/configure/wizard/serverGroupWizard.d.ts +18 -6
- package/dist/serverGroup/details/CloudrunServerGroupActions.d.ts +6 -0
- package/dist/serverGroup/details/cloudrunServerGroupDetailsGetter.d.ts +3 -0
- package/dist/serverGroup/details/sections.d.ts +5 -0
- package/dist/serverGroup/index.d.ts +3 -1
- package/dist/serverGroup/serverGroupTransformer.service.d.ts +1 -5
- package/package.json +4 -11
- package/src/cloudrun.module.spec.ts +22 -0
- package/src/cloudrun.module.ts +54 -59
- package/src/common/ComponentUrlDetails.tsx +25 -0
- package/src/common/LoadBalancerMessage.tsx +17 -0
- package/src/index.ts +2 -1
- package/src/instance/details/CloudrunInstanceDetails.tsx +164 -0
- package/src/loadBalancer/configure/wizard/CloudrunLoadBalancerModal.spec.tsx +85 -0
- package/src/loadBalancer/configure/wizard/CloudrunLoadBalancerModal.tsx +395 -0
- package/src/loadBalancer/details/CloudrunLoadBalancerActions.tsx +66 -0
- package/src/loadBalancer/details/sections.spec.ts +19 -0
- package/src/loadBalancer/details/sections.tsx +91 -0
- package/src/loadBalancer/details/useCloudrunLoadBalancerDetails.ts +56 -0
- package/src/loadBalancer/index.ts +4 -2
- package/src/loadBalancer/loadBalancerTransformer.ts +3 -13
- package/src/manifest/manifest.service.ts +18 -1
- package/src/manifest/manifestCommandBuilder.service.ts +46 -53
- package/src/pipeline/stages/deployManifest/manifestStatus/DeployStatus.tsx +33 -4
- package/src/pipeline/stages/deployManifest/manifestStatus/ManifestDetailsLink.spec.tsx +35 -0
- package/src/pipeline/stages/deployManifest/manifestStatus/ManifestDetailsLink.tsx +10 -5
- package/src/pipeline/stages/editLoadBalancer/CloudrunEditLoadBalancerExecutionDetails.tsx +70 -0
- package/src/pipeline/stages/editLoadBalancer/CloudrunEditLoadBalancerStageConfig.tsx +245 -0
- package/src/pipeline/stages/editLoadBalancer/cloudrunEditLoadBalancerStage.spec.ts +205 -0
- package/src/pipeline/stages/editLoadBalancer/cloudrunEditLoadBalancerStage.ts +14 -71
- package/src/serverGroup/configure/serverGroupCommandBuilder.service.ts +5 -15
- package/src/serverGroup/configure/wizard/BasicSettings.tsx +10 -8
- package/src/serverGroup/configure/wizard/serverGroupWizard.tsx +59 -15
- package/src/serverGroup/details/CloudrunServerGroupActions.tsx +80 -0
- package/src/serverGroup/details/cloudrunServerGroupDetailsGetter.spec.ts +46 -0
- package/src/serverGroup/details/cloudrunServerGroupDetailsGetter.ts +53 -0
- package/src/serverGroup/details/sections.tsx +60 -0
- package/src/serverGroup/index.ts +3 -1
- package/src/serverGroup/routerConsumers.spec.tsx +152 -0
- package/src/serverGroup/serverGroupTransformer.service.ts +2 -12
- package/dist/common/componentUrlDetails.component.d.ts +0 -1
- package/dist/common/conditionalDescriptionListItem.component.d.ts +0 -1
- package/dist/common/loadBalancerMessage.component.d.ts +0 -1
- package/dist/instance/details/details.controller.d.ts +0 -1
- package/dist/loadBalancer/configure/wizard/allocationConfigurationRow.component.d.ts +0 -1
- package/dist/loadBalancer/configure/wizard/basicSettings.component.d.ts +0 -1
- package/dist/loadBalancer/configure/wizard/stageAllocationConfigurationRow.component.d.ts +0 -1
- package/dist/loadBalancer/configure/wizard/wizard.controller.d.ts +0 -2
- package/dist/loadBalancer/details/details.controller.d.ts +0 -1
- package/dist/pipeline/pipeline.module.d.ts +0 -1
- package/dist/pipeline/stages/editLoadBalancer/loadBalancerChoice.modal.controller.d.ts +0 -1
- package/dist/serverGroup/details/details.controller.d.ts +0 -1
- package/src/common/componentUrlDetails.component.ts +0 -22
- package/src/common/conditionalDescriptionListItem.component.ts +0 -37
- package/src/common/loadBalancerMessage.component.html +0 -11
- package/src/common/loadBalancerMessage.component.ts +0 -13
- package/src/instance/details/details.controller.ts +0 -84
- package/src/instance/details/details.html +0 -68
- package/src/loadBalancer/configure/wizard/allocationConfigurationRow.component.ts +0 -70
- package/src/loadBalancer/configure/wizard/basicSettings.component.html +0 -44
- package/src/loadBalancer/configure/wizard/basicSettings.component.ts +0 -86
- package/src/loadBalancer/configure/wizard/stageAllocationConfigurationRow.component.html +0 -45
- package/src/loadBalancer/configure/wizard/stageAllocationConfigurationRow.component.ts +0 -84
- package/src/loadBalancer/configure/wizard/wizard.controller.ts +0 -157
- package/src/loadBalancer/configure/wizard/wizard.html +0 -39
- package/src/loadBalancer/configure/wizard/wizard.less +0 -9
- package/src/loadBalancer/details/details.controller.ts +0 -140
- package/src/loadBalancer/details/details.html +0 -89
- package/src/pipeline/pipeline.module.ts +0 -6
- package/src/pipeline/stages/editLoadBalancer/editLoadBalancerExecutionDetails.html +0 -33
- package/src/pipeline/stages/editLoadBalancer/editLoadBalancerStage.html +0 -51
- package/src/pipeline/stages/editLoadBalancer/loadBalancerChoice.modal.controller.ts +0 -65
- package/src/pipeline/stages/editLoadBalancer/loadBalancerChoice.modal.html +0 -53
- package/src/serverGroup/details/details.controller.ts +0 -134
- 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>×</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
|
+
}
|