Skip to content

Commit 4f53b14

Browse files
committed
First pass of create service bindings from the overview
1 parent 722fec3 commit 4f53b14

File tree

17 files changed

+575
-24
lines changed

17 files changed

+575
-24
lines changed

app/index.html

+1
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,7 @@ <h1>JavaScript Required</h1>
337337
<script src="scripts/directives/buildPipeline.js"></script>
338338
<script src="scripts/directives/buildStatus.js"></script>
339339
<script src="scripts/directives/routeServiceBarChart.js"></script>
340+
<script src="scripts/directives/bindService.js"></script>
340341

341342
<!-- Old Overview, TODO: remove -->
342343
<script src="scripts/directives/serviceGroupNotifications.js"></script>

app/scripts/app.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@ angular
3131
'angularMoment',
3232
'ab-base64',
3333
'openshiftCommonServices',
34-
'openshiftCommonUI'
34+
'openshiftCommonUI',
35+
'webCatalog'
3536
])
3637
.config(function ($routeProvider) {
3738
var overviewRoute;

app/scripts/constants.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,8 @@ angular.extend(window.OPENSHIFT_CONSTANTS, {
8989
],
9090

9191
ENABLE_TECH_PREVIEW_FEATURE: {
92-
service_catalog_landing_page: false
92+
service_catalog_landing_page: false,
93+
pod_presets: false
9394
},
9495

9596
SAMPLE_PIPELINE_TEMPLATE: {

app/scripts/directives/bindService.js

+160
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
'use strict';
2+
3+
angular.module('openshiftConsole').component('bindService', {
4+
controller: [
5+
'$filter',
6+
'DataService',
7+
'DNS1123_SUBDOMAIN_VALIDATION',
8+
BindService
9+
],
10+
controllerAs: 'ctrl',
11+
bindings: {
12+
target: '<',
13+
serviceInstances: '<',
14+
serviceClasses: '<',
15+
onClose: '<'
16+
},
17+
templateUrl: 'views/directives/bind-service.html'
18+
});
19+
20+
function BindService($filter,
21+
DataService,
22+
DNS1123_SUBDOMAIN_VALIDATION) {
23+
var ctrl = this;
24+
25+
ctrl.steps = [{
26+
id: 'services',
27+
label: 'Services',
28+
view: 'views/directives/bind-service/select-service.html'
29+
}, {
30+
label: 'Results',
31+
id: 'results',
32+
view: 'views/directives/bind-service/results.html'
33+
}];
34+
35+
ctrl.$onInit = function() {
36+
// TODO: handle not having any service instances, or handle path where coming into binding from svc instance
37+
ctrl.gotoStep(ctrl.steps[0]);
38+
};
39+
40+
var statusCondition = $filter('statusCondition');
41+
ctrl.$onChanges = function(changes) {
42+
if (changes.serviceInstances && !ctrl.serviceToBind) {
43+
var newestReady;
44+
var newestNotReady;
45+
_.each(ctrl.serviceInstances, function(instance) {
46+
var ready = _.get(statusCondition(instance, 'Ready'), 'status') === 'True';
47+
if (ready && (!newestReady || instance.metadata.creationTimestamp > newestReady.metadata.creationTimestamp)) {
48+
newestReady = instance;
49+
}
50+
if (!ready && (!newestNotReady || instance.metadata.creationTimestamp > newestNotReady.metadata.creationTimestamp)) {
51+
newestNotReady = instance;
52+
}
53+
});
54+
ctrl.serviceToBind = _.get(newestReady, 'metadata.name') || _.get(newestNotReady, 'metadata.name');
55+
}
56+
57+
// wait till both service instances and service classes are available so that the sort is stable and items dont jump around
58+
if ((changes.serviceInstances || changes.serviceClasses) && ctrl.serviceClasses && ctrl.serviceInstances) {
59+
var instances = _.toArray(ctrl.serviceInstances);
60+
instances.sort(function(left, right) {
61+
var leftName = _.get(ctrl.serviceClasses, [left.spec.serviceClassName, 'osbMetadata', 'displayName']) || left.spec.serviceClassName;
62+
var rightName = _.get(ctrl.serviceClasses, [left.spec.serviceClassName, 'osbMetadata', 'displayName']) || right.spec.serviceClassName;
63+
64+
// Fall back to sorting by `metadata.name` if the display names are the
65+
// same so that the sort is stable.
66+
if (leftName === rightName) {
67+
leftName = _.get(left, 'metadata.name', '');
68+
rightName = _.get(right, 'metadata.name', '');
69+
}
70+
71+
return leftName.localeCompare(rightName);
72+
});
73+
ctrl.orderedServiceInstances = instances;
74+
}
75+
};
76+
77+
var gotoStepID = function(id) {
78+
var step = _.find(ctrl.steps, { id: id });
79+
ctrl.gotoStep(step);
80+
};
81+
82+
ctrl.gotoStep = function(step) {
83+
_.each(ctrl.steps, function(st) {
84+
st.selected = false;
85+
});
86+
if (ctrl.currentStep) {
87+
ctrl.currentStep.visited = true;
88+
}
89+
ctrl.currentStep = step;
90+
ctrl.currentStep.selected = true;
91+
};
92+
93+
ctrl.stepClick = function(step) {
94+
// Prevent returning to previous steps if the order is complete.
95+
if (ctrl.wizardComplete) {
96+
return;
97+
}
98+
99+
if (!step.visited) {
100+
return;
101+
}
102+
103+
ctrl.gotoStep(step);
104+
};
105+
106+
var generateName = $filter('generateName');
107+
var makeBinding = function() {
108+
var serviceInstanceName = _.get(ctrl.serviceInstances[ctrl.serviceToBind], 'metadata.name');
109+
// TODO - would be better if generateName could take in an optional maxlength
110+
var truncatedSvcInstanceName = _.trunc(serviceInstanceName, DNS1123_SUBDOMAIN_VALIDATION.maxlength - 6);
111+
ctrl.generatedSecretName = generateName(truncatedSvcInstanceName + '-');
112+
var binding = {
113+
kind: 'Binding',
114+
apiVersion: 'servicecatalog.k8s.io/v1alpha1',
115+
metadata: {
116+
generateName: serviceInstanceName + '-'
117+
},
118+
spec: {
119+
instanceRef: {
120+
name: serviceInstanceName
121+
},
122+
secretName: ctrl.generatedSecretName
123+
}
124+
};
125+
126+
return binding;
127+
};
128+
129+
ctrl.bindService = function() {
130+
var context = {
131+
namespace: _.get(ctrl.serviceInstances[ctrl.serviceToBind], 'metadata.namespace')
132+
};
133+
DataService.create({
134+
group: 'servicecatalog.k8s.io',
135+
resource: 'bindings'
136+
}, null, makeBinding(), context).then(function(binding) {
137+
ctrl.binding = binding;
138+
139+
DataService.watchObject({
140+
group: 'servicecatalog.k8s.io',
141+
resource: 'bindings'
142+
}, _.get(ctrl.binding, 'metadata.name'), context, function(binding) {
143+
ctrl.binding = binding;
144+
});
145+
146+
ctrl.wizardComplete = true;
147+
ctrl.error = null;
148+
gotoStepID('results');
149+
}, function(e) {
150+
ctrl.error = e;
151+
});
152+
};
153+
154+
ctrl.closeWizard = function() {
155+
if (_.isFunction(ctrl.onClose)) {
156+
ctrl.onClose();
157+
}
158+
};
159+
160+
}

app/scripts/directives/overview/listRow.js

+9
Original file line numberDiff line numberDiff line change
@@ -319,4 +319,13 @@ function OverviewListRow($filter,
319319
var imageStreamNamespace = _.get(imageChangeTrigger, 'imageChangeParams.from.namespace', deploymentConfigNamespace);
320320
return Navigate.resourceURL(imageStreamName, 'ImageStream', imageStreamNamespace);
321321
};
322+
323+
row.closeOverlayPanel = function() {
324+
_.set(row, 'overlay.panelVisible', false);
325+
};
326+
row.showOverlayPanel = function(panelName, state) {
327+
_.set(row, 'overlay.panelVisible', true);
328+
_.set(row, 'overlay.panelName', panelName);
329+
_.set(row, 'overlay.state', state);
330+
};
322331
}

app/scripts/filters/resources.js

+9
Original file line numberDiff line numberDiff line change
@@ -1106,6 +1106,15 @@ angular.module('openshiftConsole')
11061106
return lastFinishTime;
11071107
};
11081108
})
1109+
.filter('statusCondition', function() {
1110+
return function(apiObject, type) {
1111+
if (!apiObject) {
1112+
return null;
1113+
}
1114+
1115+
return _.find(_.get(apiObject, 'status.conditions'), {type: type});
1116+
};
1117+
})
11091118
.filter('routeIngressCondition', function() {
11101119
return function(ingress, type) {
11111120
if (!ingress) {

app/scripts/filters/util.js

+5
Original file line numberDiff line numberDiff line change
@@ -506,4 +506,9 @@ angular.module('openshiftConsole')
506506
return function(text, target, alreadyEscaped) {
507507
return HTMLService.linkify(text, target, alreadyEscaped);
508508
};
509+
})
510+
.filter('enableTechPreviewFeature', function(Constants) {
511+
return function(feature) {
512+
return _.get(Constants, ['ENABLE_TECH_PREVIEW_FEATURE', feature], false);
513+
};
509514
});

app/styles/_patternfly-additions.less

+5
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,8 @@
99
font-size: @donut-font-size-big * .75; // ~ 22.5px;
1010
font-weight: 300;
1111
}
12+
13+
// TODO: fixed in latest patternfly
14+
.wizard-pf-steps-indicator .wizard-pf-step-number {
15+
left: ~"calc(50% - 13px)";
16+
}
+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<div class="wizard-pf-body">
2+
<div class="wizard-pf-steps">
3+
<ul class="wizard-pf-steps-indicator">
4+
<li class="wizard-pf-step" ng-class="{
5+
active: step.selected,
6+
visited: step.visited && !step.selected
7+
}" ng-repeat="step in ctrl.steps" data-tabgroup="{{$index}}">
8+
<a ng-click="ctrl.stepClick(step)"><span class="wizard-pf-step-number">{{$index + 1}}</span><span class="wizard-pf-step-title">{{step.label}}</span></a>
9+
</li>
10+
</ul>
11+
</div>
12+
<div class="container-fluid wizard-pf-main mar-left-none">
13+
<div ng-include="ctrl.currentStep.view" class="wizard-pf-contents"></div>
14+
</div>
15+
<div class="config-bottom modal-footer wizard-pf-footer">
16+
<button
17+
type="button"
18+
class="btn btn-default wizard-pf-cancel wizard-pf-dismiss"
19+
ng-class="{'btn-cancel': ctrl.steps.length > 2}"
20+
ng-disabled="ctrl.currentStep.id === 'results'"
21+
ng-click="ctrl.closeWizard()">
22+
Cancel
23+
</button>
24+
<button
25+
type="button"
26+
class="btn btn-primary wizard-pf-next"
27+
ng-if="ctrl.currentStep.id !== 'results'"
28+
ng-click="ctrl.bindService()">
29+
Bind
30+
</button>
31+
<button
32+
type="button"
33+
class="btn btn-primary wizard-pf-close wizard-pf-dismiss"
34+
ng-if="ctrl.currentStep.id === 'results'"
35+
ng-click="ctrl.closeWizard()">
36+
Close
37+
</button>
38+
</div>
39+
</div>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<div>
2+
<div ng-if="!ctrl.error">
3+
<div ng-if="(ctrl.binding | statusCondition : 'Ready').status !== 'True'">
4+
<h3 class="mar-top-none center">
5+
<span class="fa fa-spinner fa-pulse fa-3x fa-fw" aria-hidden="true"></span>
6+
<span class="sr-only">Pending</span>
7+
<div class="mar-top-lg">The binding was created but is not ready yet.</div>
8+
</h3>
9+
</div>
10+
<div ng-if="(ctrl.binding | statusCondition : 'Ready').status === 'True'">
11+
<h3 class="mar-top-none">
12+
<strong>{{ctrl.serviceToBind}}</strong> has been bound to <strong>{{ctrl.target.metadata.name}}</strong> successfully
13+
</h3>
14+
<div class="sub-title">
15+
<h3><span class="pficon pficon-ok mar-right-sm"></span>Configuration Created</h3>
16+
The binding operation created the secret
17+
<a ng-if="'secrets' | canI : 'list'"
18+
ng-href="{{ctrl.generatedSecretName | navigateResourceURL : 'Secret' : ctrl.target.metadata.namespace}}">{{ctrl.generatedSecretName}}</a>
19+
<span ng-if="!('secrets' | canI : 'list')">{{ctrl.generatedSecretName}}</span>
20+
that you may need to reference in your application.
21+
<span ng-if="'pod_presets' | enableTechPreviewFeature">Its data will be available to your application as environment variables.</span>
22+
</div>
23+
<div class="alert alert-info mar-top-xxl mar-bottom-xl">
24+
<span class="pficon pficon-info" aria-hidden="true"></span>
25+
<span class="sr-only">Info</span>
26+
The binding secret will only be available to new pods. You will need to redploy your application.
27+
</div>
28+
</div>
29+
</div>
30+
<div ng-if="ctrl.error">
31+
<div class="title">Binding Failed <span class="fa fa-times text-danger"></span></div>
32+
<div class="sub-title">
33+
<span ng-if="ctrl.error.data.message">
34+
{{ctrl.error.data.message | upperFirst}}
35+
</span>
36+
<span ng-if="!ctrl.error.data.message">
37+
An error occurred creating the binding.
38+
</span>
39+
</div>
40+
</div>
41+
</div>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<!-- TODO: handle path where we come in with no target -->
2+
<h3 class="mar-top-none">Select a service to bind to <strong>{{ctrl.target.metadata.name}}</strong></h3>
3+
Binding to a provisioned service will create a secret containing the information necessary
4+
for your application to use the service.
5+
<form name="serviceSelection" class="mar-bottom-lg">
6+
<fieldset ng-disabled="ctrl.isDisabled">
7+
<div class="radio" ng-repeat="serviceInstance in ctrl.orderedServiceInstances">
8+
<label>
9+
<input type="radio" ng-model="ctrl.serviceToBind" value="{{serviceInstance.metadata.name}}">
10+
{{ctrl.serviceClasses[serviceInstance.spec.serviceClassName].osbMetadata.displayName || serviceInstance.spec.serviceClassName}}
11+
<span ng-if="(serviceInstance | statusCondition : 'Ready').status !== 'True'" class="mar-left-sm">
12+
<span class="pficon pficon-info" data-content="This service is not yet ready. If you bind to it, then the binding will be pending until the service is ready." data-toggle="popover" data-trigger="hover"></span>
13+
</span>
14+
<div class="help-block mar-top-none">
15+
{{serviceInstance.metadata.name}}
16+
</div>
17+
</label>
18+
</div>
19+
</fieldset>
20+
</form>

app/views/overview/_list-row-actions.html

+5
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@
2323
<li ng-if="'deploymentconfigs' | canI : 'update'" role="menuitem">
2424
<a ng-href="{{row.apiObject | editResourceURL}}">Edit</a>
2525
</li>
26+
<!-- FIXME: Can't enable canI checks on svc cat resources until we have aggregation
27+
<li ng-if="(row.state.serviceInstances | hashSize) > 0 && {resource: 'bindings', group: 'servicecatalog.k8s.io'} | canI : 'create'" role="menuitem"> -->
28+
<li ng-if="('pod_presets' | enableTechPreviewFeature) && (row.state.serviceInstances | hashSize) > 0" role="menuitem">
29+
<a href="" ng-click="row.showOverlayPanel('bindService', {target: row.apiObject})">Create Binding</a>
30+
</li>
2631
<li ng-if="row.current && ('deploymentconfigs/log' | canI : 'get')" role="menuitem">
2732
<a ng-href="{{row.current | navigateResourceURL}}?tab=logs">View Logs</a>
2833
</li>

app/views/overview/_list-row.html

+8
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,11 @@
1919
<div ng-include src=" 'views/overview/_list-row-expanded.html' "></div>
2020
</div>
2121
</div>
22+
<overlay-panel show-panel="row.overlay.panelVisible" show-close="true" handle-close="row.closeOverlayPanel">
23+
<div ng-if="row.overlay.panelName === 'bindService'">
24+
<bind-service target="row.overlay.state.target"
25+
service-instances="row.state.serviceInstances"
26+
service-classes="row.state.serviceClasses"
27+
on-close="row.closeOverlayPanel"></bind-service>
28+
</div>
29+
</overlay-panel>

0 commit comments

Comments
 (0)