diff --git a/docs/management-create-application.md b/docs/management-create-application.md new file mode 100644 index 0000000000..0940a6e585 --- /dev/null +++ b/docs/management-create-application.md @@ -0,0 +1,8 @@ +# Create application + +An application is required to subscribe to API's plan. It is the intermediate link between a user (you) and an API +managed by someone else. + + +An application is a simple entity composed of small property which are mainly defined to describe the application +, what is does, ... diff --git a/package.json b/package.json index aa77938d74..3fafc12430 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ "bootstrap": "^3.3.7", "codemirror": "^5.25.2", "diff": "^3.2.0", - "dragular": "^4.3.0", + "dragular": "^4.4.6", "highcharts": "^5.0.7", "jquery": "^3.1.1", "js-yaml": "^3.8.1", @@ -46,7 +46,7 @@ "ng-file-upload": "^12.2.13", "ng-infinite-scroll": "^1.3.0", "ng-showdown": "^1.1.0", - "ngclipboard": "^1.1.1", + "ngclipboard": "^1.1.2", "raml-1-parser": "^1.1.19 ", "read-more": "0.0.0", "satellizer": "^0.15.5", diff --git a/src/components/widget/widget-data-table.component.ts b/src/components/widget/widget-data-table.component.ts index 87a21926f4..8b677e4660 100644 --- a/src/components/widget/widget-data-table.component.ts +++ b/src/components/widget/widget-data-table.component.ts @@ -70,7 +70,7 @@ const WidgetDataTableComponent: ng.IComponentOptions = { if (this.widget.chart.link === 'api') { this.$state.go('management.apis.detail.analytics', {apiId: key, from: this.widget.chart.request.from, to: this.widget.chart.request.to}); } else if (this.widget.chart.link === 'application') { - this.$state.go('management.applications.portal.analytics', {applicationId: key, from: this.widget.chart.request.from, to: this.widget.chart.request.to}); + this.$state.go('management.applications.application.analytics', {applicationId: key, from: this.widget.chart.request.from, to: this.widget.chart.request.to}); } } } diff --git a/src/index.scss b/src/index.scss index 2a48977989..bb359881eb 100644 --- a/src/index.scss +++ b/src/index.scss @@ -41,6 +41,8 @@ $backgroundColor: rgb(45, 50, 62); @import 'components/contextual/_contextual-doc.component.scss'; @import 'management/tasks/_tasks.scss'; +$backgroundColor: rgb(45, 50, 62); + html { font-family: 'Roboto Slab', serif; } diff --git a/src/management/api/api-plan.html b/src/management/api/api-plan.html index be2756ae21..4d85755655 100644 --- a/src/management/api/api-plan.html +++ b/src/management/api/api-plan.html @@ -31,7 +31,7 @@
{{characteristic}}
- {{$ctrl.plan.alreadySubscribed ? 'Subscribed' : ($ctrl.plan.validation === 'auto'?'Subscribe': 'Request for subscription')}} diff --git a/src/management/api/apis.route.ts b/src/management/api/apis.route.ts index ed21c1786c..013ab1125e 100644 --- a/src/management/api/apis.route.ts +++ b/src/management/api/apis.route.ts @@ -13,15 +13,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import ViewService from '../../services/view.service'; -import ApisController from './apis.controller'; -import TenantService from '../../services/tenant.service'; -import ResourceService from '../../services/resource.service'; -import TagService from '../../services/tag.service'; -import ApiService from '../../services/api.service'; -import MetadataService from '../../services/metadata.service'; -import GroupService from '../../services/group.service'; -import * as _ from 'lodash'; +import ViewService from "../../services/view.service"; +import ApisController from "./apis.controller"; +import TenantService from "../../services/tenant.service"; +import ResourceService from "../../services/resource.service"; +import TagService from "../../services/tag.service"; +import ApiService from "../../services/api.service"; +import MetadataService from "../../services/metadata.service"; +import GroupService from "../../services/group.service"; +import * as _ from "lodash"; import AuditService from "../../services/audit.service"; export default apisRouterConfig; @@ -35,7 +35,7 @@ function apisRouterConfig($stateProvider: ng.ui.IStateProvider) { }) .state('management.apis.detail', { abstract: true, - url: '/:apiId/settings', + url: '/:apiId', template: require('./apiAdmin.html'), controller: 'ApiAdminController', controllerAs: 'apiCtrl', @@ -227,14 +227,25 @@ function apisRouterConfig($stateProvider: ng.ui.IStateProvider) { } }) .state('management.apis.detail.plans', { - url: '/plans?state', - template: require('./plans/apiPlans.html'), - controller: 'ApiPlansController', - controllerAs: 'apiPlansCtrl', + abstract: true, + url: '/plans', + template: '
', resolve: { - resolvedPlans: function ($stateParams, ApiService) { - return ApiService.getApiPlans($stateParams.apiId); + api: ($stateParams: ng.ui.IStateParamsService, ApiService: ApiService) => + ApiService.get($stateParams.apiId).then(response => response.data) + /* + groups: (GroupService: GroupService) => { + GroupService.list().then(response => response.data); } + */ + } + }) + .state('management.apis.detail.plans.list', { + url: '?state', + component: 'listPlans', + resolve: { + plans: ($stateParams: ng.ui.IStateParamsService, ApiService: ApiService) => + ApiService.getApiPlans($stateParams.apiId).then(response => response.data) }, data: { menu: { @@ -251,19 +262,43 @@ function apisRouterConfig($stateProvider: ng.ui.IStateProvider) { params: { state: { type: 'string', - dynamic: true, + dynamic: true } } }) + .state('management.apis.detail.plans.new', { + url: '/new', + component: 'editPlan' + }) + .state('management.apis.detail.plans.plan', { + url: '/:planId/edit', + component: 'editPlan', + resolve: { + plan: ($stateParams: ng.ui.IStateParamsService, ApiService: ApiService) => + ApiService.getApiPlan($stateParams.apiId, $stateParams.planId).then(response => response.data) + } + }) .state('management.apis.detail.subscriptions', { + abstract: true, url: '/subscriptions', - template: require('./subscriptions/subscriptions.html'), - controller: 'SubscriptionsController', - controllerAs: 'subscriptionsCtrl', + template: '
', resolve: { - resolvedSubscriptions: function ($stateParams, ApiService) { - return ApiService.getSubscriptions($stateParams.apiId); - } + api: ($stateParams: ng.ui.IStateParamsService, ApiService: ApiService) => + ApiService.get($stateParams.apiId).then(response => response.data) + } + }) + .state('management.apis.detail.subscriptions.list', { + url: '', + component: 'apiSubscriptions', + resolve: { + subscriptions: ($stateParams: ng.ui.IStateParamsService, ApiService: ApiService) => + ApiService.getSubscriptions($stateParams.apiId).then(response => response.data), + + subscribers: ($stateParams: ng.ui.IStateParamsService, ApiService: ApiService) => + ApiService.getSubscribers($stateParams.apiId).then(response => response.data), + + plans: ($stateParams: ng.ui.IStateParamsService, ApiService: ApiService) => + ApiService.getApiPlans($stateParams.apiId).then(response => response.data) }, data: { menu: { @@ -278,6 +313,22 @@ function apisRouterConfig($stateProvider: ng.ui.IStateProvider) { } } }) + .state('management.apis.detail.subscriptions.subscription', { + url: '/:subscriptionId', + component: 'apiSubscription', + resolve: { + subscription: ($stateParams: ng.ui.IStateParamsService, ApiService: ApiService) => + ApiService.getSubscription($stateParams.apiId, $stateParams.subscriptionId).then(response => response.data) + }, + data: { + perms: { + only: ['api-subscription-r'] + }, + docs: { + page: 'management-api-subscriptions' + } + } + }) .state('management.apis.detail.resources', { url: '/resources', template: require('./resources/resources.html'), diff --git a/src/management/api/plans/closePlan.dialog.html b/src/management/api/plans/closePlan.dialog.html index 3bd9276297..91f7fabcb1 100644 --- a/src/management/api/plans/closePlan.dialog.html +++ b/src/management/api/plans/closePlan.dialog.html @@ -17,15 +17,15 @@ --> -
-

No subscription is associated to this plan. You can delete it safely.

-

There are {{subscriptions}} active subscriptions associated to this plan.

+
+

No subscription is associated to this plan. You can delete it safely.

+

There are {{subscriptions}} active subscription(s) associated to this plan.

- By closing this plan, all relative subscriptions will also be closed and associated api-keys will be no longer - available. + By closing this plan, all relative subscriptions will also be closed + and associated api-keys will be no longer available.

-
+

Are you sure to close the plan {{plan.name}}?

diff --git a/src/management/api/plans/closePlanDialog.controller.ts b/src/management/api/plans/closePlanDialog.controller.ts index 8763428ec5..34845470da 100644 --- a/src/management/api/plans/closePlanDialog.controller.ts +++ b/src/management/api/plans/closePlanDialog.controller.ts @@ -25,7 +25,7 @@ function DialogClosePlanController($scope, $rootScope, $mdDialog, ApiService, No }; $scope.close = function () { - if ($scope.plan.security === 'api_key' && $scope.subscriptions === 0) { + if ($scope.plan.security !== 'key_less' && $scope.subscriptions === 0) { ApiService.deletePlan($scope.apiId, $scope.plan.id).then(function() { NotificationService.show('Plan ' + plan.name + ' has been deleted'); }).catch(function (error) { diff --git a/src/management/api/plans/list-plans.component.ts b/src/management/api/plans/list-plans.component.ts new file mode 100644 index 0000000000..94085d0e52 --- /dev/null +++ b/src/management/api/plans/list-plans.component.ts @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2015 The Gravitee team (http://gravitee.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +const ApiListPlansComponent: ng.IComponentOptions = { + bindings: { + plans: '<' + }, + controller: 'ApiListPlansController', + template: require('./list-plans.html') +}; + +export default ApiListPlansComponent; diff --git a/src/management/api/plans/list-plans.controller.ts b/src/management/api/plans/list-plans.controller.ts new file mode 100644 index 0000000000..8a820233ad --- /dev/null +++ b/src/management/api/plans/list-plans.controller.ts @@ -0,0 +1,317 @@ +/* + * Copyright (C) 2015 The Gravitee team (http://gravitee.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import _ = require('lodash'); +import angular = require('angular'); +import UserService from '../../../services/user.service'; +import ApiService from "../../../services/api.service"; +import NotificationService from "../../../services/notification.service"; + +class ApiListPlansController { + private api: any; + private plans: any; + private dndEnabled: boolean; + private statusFilters: string[]; + private selectedStatus: string[]; + private countByStatus: any; + private filteredPlans: any; + + constructor( + private $scope: ng.IScope, + private $rootScope: ng.IRootScopeService, + private $state: ng.ui.IStateService, + private $stateParams: ng.ui.IStateParamsService, + private $mdDialog: angular.material.IDialogService, + private dragularService, + private NotificationService: NotificationService, + private UserService: UserService, + private ApiService: ApiService + ) { + 'ngInject'; + this.dndEnabled = UserService.isUserHasPermissions(['api-plan-u']); + this.statusFilters = ['staging', 'published', 'closed']; + this.selectedStatus = ['published']; + + this.countByStatus = {}; + +/* + var that = this; + $scope.configure = function (plan) { + that.resetPlan(); + if (!$mdSidenav('plan-edit').isOpen()) { + $scope.plan = plan; + if (plan['excluded_groups']) { + $scope.plan.authorizedGroups = _.difference(_.map(that.groups, "id"), plan["excluded_groups"]); + } else { + $scope.plan.authorizedGroups = _.map(that.groups, "id"); + } + if ($scope.plan.paths['/']) { + _.forEach($scope.plan.paths['/'], function (path) { + if (path['rate-limit']) { + $scope.rateLimit = path['rate-limit'].rate; + } + if (path['quota']) { + $scope.quota = path['quota'].quota; + } + if (path['resource-filtering']) { + $scope.resourceFiltering.whitelist = path['resource-filtering'].whitelist; + } + }); + } + $mdSidenav('live-preview').toggle(); + $mdSidenav('plan-edit').toggle(); + } + }; + + $scope.rateLimitTimeUnits = ['SECONDS', 'MINUTES']; + $scope.quotaTimeUnits = ['HOURS', 'DAYS', "WEEKS", "MONTHS"]; + + $scope.methods = ['GET', 'POST', 'PUT', 'DELETE', 'HEAD', 'PATCH', 'OPTIONS', 'TRACE', 'CONNECT']; + + this.resetResourceFiltering(); + + $scope.$watch('livePreviewIsOpen', function (livePreviewIsOpen) { + if (livePreviewIsOpen === false && $mdSidenav('plan-edit').isOpen()) { + $mdSidenav('plan-edit').toggle(); + } + }); + + $scope.$on('planChangeSuccess', function(event, args) { + that.changeFilter(args.state); + }); + */ + } + + $onInit() { + /* + if (this.api.visibility === "private") { + if (this.api.groups) { + const apiGroupIds = this.api.groups; + this.groups = _.filter(this.groups, (group) => { + return apiGroupIds.indexOf(group["id"]) > -1; + }); + } else { + this.groups = []; + } + } + */ + + let that = this; + + let d = document.querySelector('.plans'); + this.dragularService([d], { + moves: function () { + return that.dndEnabled; + }, + scope: this.$scope, + containersModel: this.plans, + nameSpace: 'plan' + }); + + this.$scope.$on('dragulardrop', function(e, el, target, source, dragularList, index, targetModel, dropIndex) { + let movedPlan = that.filteredPlans[index]; + movedPlan.order = dropIndex+1; + + that.ApiService.savePlan(that.$stateParams.apiId, movedPlan).then(function () { + // sync list from server because orders has been changed + that.list(); + that.NotificationService.show('Plans have been reordered successfully'); + }); + }); + + if (this.$stateParams.state) { + if (_.includes(this.statusFilters, this.$stateParams.state)) { + this.changeFilter(this.$stateParams.state); + } else { + this.applyFilters(); + } + } else { + this.applyFilters(); + } + } + + list() { + this.ApiService.getApiPlans(this.$stateParams.apiId).then(response => { + let that = this; + this.$scope.$applyAsync(function(){ + that.plans.length = 0; + Array.prototype.push.apply(that.plans, response.data); + + that.applyFilters(); + }); + }); + } + + changeFilter(statusFilter) { + this.selectedStatus = statusFilter; + this.dndEnabled = (statusFilter === 'published'); + + if (_.includes(this.selectedStatus, statusFilter)) { + _.pull(this.selectedStatus, statusFilter); + } else { + this.selectedStatus.push(statusFilter); + } + this.$state.transitionTo( + this.$state.current, + _.merge(this.$state.params, { + state: statusFilter + })); + this.applyFilters(); + } + + applyFilters() { + this.countPlansByStatus(); + var that = this; + this.filteredPlans = _.sortBy( + _.filter(this.plans, function (plan: any) { + return _.includes(that.selectedStatus, plan.status); + }), "order"); + } + + addPlan() { + this.$state.go('management.apis.detail.plans.new'); + } + + editPlan(planId: string) { + this.$state.go('management.apis.detail.plans.plan', {planId: planId}); + } + + /* + save() { + var that = this; + + this.$scope.plan.paths = { + '/': [] + }; + // set resource filtering whitelist + _.remove(this.$scope.resourceFiltering.whitelist, function (whitelistItem: any) { + return !whitelistItem.pattern; + }); + if (this.$scope.resourceFiltering.whitelist.length) { + that.$scope.plan.paths['/'].push({ + 'methods': that.$scope.methods, + 'resource-filtering': { + 'whitelist': this.$scope.resourceFiltering.whitelist + } + }); + } + // set rate limit policy + if (this.$scope.rateLimit && this.$scope.rateLimit.limit) { + this.$scope.plan.paths['/'].push({ + 'methods': this.$scope.methods, + 'rate-limit': { + 'rate': this.$scope.rateLimit + } + }); + } + // set quota policy + if (this.$scope.quota && this.$scope.quota.limit) { + this.$scope.plan.paths['/'].push({ + 'methods': this.$scope.methods, + 'quota': { + 'quota': this.$scope.quota, + 'addHeaders': true + } + }); + } + // convert authorized groups to excludedGroups + that.$scope.plan.excludedGroups = []; + if (that.groups) { + that.$scope.plan.excludedGroups = _.difference(_.map(that.groups, "id"), that.$scope.plan.authorizedGroups); + } + + that.ApiService.savePlan(that.$stateParams.apiId, that.$scope.plan).then(function (response) { + var createMode = !that.planAlreadyCreated(response.data.id); + that.$scope.$parent.apiCtrl.checkAPISynchronization({id: that.$stateParams.apiId}); + that.NotificationService.show('The plan ' + that.$scope.plan.name + ' has been saved with success'); + that.ApiService.getApiPlans(that.$stateParams.apiId).then(function (response) { + that.plans = response.data; + that.$mdSidenav('plan-edit').toggle(); + that.$mdSidenav('live-preview').toggle(); + if (createMode) { + that.changeFilter('staging'); + } else { + that.applyFilters(); + } + }); + }); + } + + planAlreadyCreated(planId) { + return _.some(this.plans, function (plan: any) { + return plan.id === planId; + }); + } + */ + + close(plan, ev) { + this.ApiService.getPlanSubscriptions(this.$stateParams.apiId, plan.id).then( (response) => { + this.$mdDialog.show({ + controller: 'DialogClosePlanController', + template: require('./closePlan.dialog.html'), + parent: angular.element(document.body), + targetEvent: ev, + clickOutsideToClose: true, + locals: { + apiId: this.$stateParams.apiId, + plan: plan, + subscriptions: response.data.page.size + } + }).then((plan) => { + if (plan) { + this.$scope.$parent.apiCtrl.checkAPISynchronization({id: this.$stateParams.apiId}); + this.selectedStatus = ['closed']; + this.list(); + } + }, function() { + // You cancelled the dialog + }); + }); + } + + publish(plan, ev) { + this.$mdDialog.show({ + controller: 'DialogPublishPlanController', + template: require('./publishPlan.dialog.html'), + parent: angular.element(document.body), + targetEvent: ev, + clickOutsideToClose: true, + locals: { + plan: plan + } + }).then( (plan) => { + if (plan) { + this.ApiService.publishPlan(this.$stateParams.apiId, plan.id).then( () => { + this.NotificationService.show('Plan ' + plan.name + ' has been published'); + this.$rootScope.$broadcast("planChangeSuccess", { state: "published"}); + this.$scope.$parent.apiCtrl.checkAPISynchronization({id: this.$stateParams.apiId}); + this.selectedStatus = ['published']; + this.list(); + }).catch( (error) => { + this.NotificationService.showError(error); + }); + } + }, function() { + // You cancelled the dialog + }); + } + + countPlansByStatus() { + this.countByStatus = _.countBy(this.plans, 'status'); + } + +} + +export default ApiListPlansController; diff --git a/src/management/api/plans/list-plans.html b/src/management/api/plans/list-plans.html new file mode 100644 index 0000000000..dbdab28bda --- /dev/null +++ b/src/management/api/plans/list-plans.html @@ -0,0 +1,111 @@ + +
+
+ Plans + +
+ +
+
+ +
+
+
+ + +
+
+

+ {{plan.name}} +

+ Authentication: {{plan.security}} +
+ + + + + Configure plan + + + + + + + Publish plan + + + + + + + Close plan + + + +
+
+
+ + +

{{plan.description}}

+ +
+
{{characteristic}}
+ +
+
+ + +
+ + {{plan.alreadySubscribed ? 'Subscribed' : (plan.validation === 'auto'?'Subscribe': 'Request for subscription')}} + + + No subscription required + +
+
+
+
+
+
+ Plans can be re-ordered by dragging & dropping them. +
+
+
+ + + +
+ + + +
diff --git a/src/management/api/plans/plan/edit-plan.component.ts b/src/management/api/plans/plan/edit-plan.component.ts new file mode 100644 index 0000000000..097bf82291 --- /dev/null +++ b/src/management/api/plans/plan/edit-plan.component.ts @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2015 The Gravitee team (http://gravitee.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +const ApiEditPlanComponent: ng.IComponentOptions = { + bindings: { + plan: '<' + }, + controller: 'ApiEditPlanController', + template: require('./edit-plan.html') +}; + +export default ApiEditPlanComponent; diff --git a/src/management/api/plans/plan/edit-plan.controller.ts b/src/management/api/plans/plan/edit-plan.controller.ts new file mode 100644 index 0000000000..1e21348a10 --- /dev/null +++ b/src/management/api/plans/plan/edit-plan.controller.ts @@ -0,0 +1,120 @@ +/* + * Copyright (C) 2015 The Gravitee team (http://gravitee.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import _ = require('lodash'); +import angular = require('angular'); +import ApiService from "../../../../services/api.service"; +import NotificationService from "../../../../services/notification.service"; + +class ApiEditPlanController { + + plan: any; + + vm: { + selectedStep: number; + stepProgress: number; + maxStep: number; + showBusyText: boolean; + stepData: { + step: number; + label?: string; + completed: boolean; + optional: boolean; + data: any + }[] + }; + + constructor( + private $state: ng.ui.IStateService, + private $stateParams: ng.ui.IStateParamsService, + private $timeout: ng.ITimeoutService, + private ApiService: ApiService, + private NotificationService: NotificationService + ) { + 'ngInject'; + + this.vm = { + selectedStep: 0, + stepProgress: 1, + maxStep: 4, + showBusyText: false, + stepData: [ + {step: 1, completed: false, optional: false, data: {}}, + {step: 2, completed: false, optional: false, data: {}}, + {step: 3, completed: false, optional: true, data: {}}, + {step: 4, completed: false, optional: true, data: {}} + ] + }; + } + + $onInit() { + if (!this.plan) { + this.plan = {characteristics: []}; + } + } + + moveToNextStep(step: any) { + this.submitCurrentStep(step); + } + + moveToPreviousStep() { + if (this.vm.selectedStep > 0) { + this.vm.selectedStep = this.vm.selectedStep - 1; + } + } + + selectStep(step) { + this.vm.selectedStep = step; + } + + submitCurrentStep(stepData) { + this.vm.showBusyText = true; + if (!stepData.completed) { + if (this.vm.selectedStep !== 4) { + this.vm.showBusyText = false; + //move to next step when success + stepData.completed = true; + this.enableNextStep(); + } + } else { + this.vm.showBusyText = false; + this.enableNextStep(); + } + } + + enableNextStep() { + //do not exceed into max step + if (this.vm.selectedStep >= this.vm.maxStep) { + return; + } + //do not increment vm.stepProgress when submitting from previously completed step + if (this.vm.selectedStep === this.vm.stepProgress - 1) { + this.vm.stepProgress = this.vm.stepProgress + 1; + } + + this.$timeout(() => this.vm.selectedStep = this.vm.selectedStep + 1); + } + + saveOrUpdate() { + this.ApiService.savePlan(this.$stateParams.apiId, this.plan).then( () => { + this.NotificationService.show(this.plan.name + ' has been saved successfully'); + this.$state.go( + 'management.apis.detail.plans.list', + this.plan.id === undefined ? {'state': 'staging'} : {}); + }); + } +} + +export default ApiEditPlanController; diff --git a/src/management/api/plans/plan/edit-plan.html b/src/management/api/plans/plan/edit-plan.html new file mode 100644 index 0000000000..b3eb0d6d5e --- /dev/null +++ b/src/management/api/plans/plan/edit-plan.html @@ -0,0 +1,25 @@ +
+
+ Plan +
+ +
+
+ + + + + + + +
+ +
+

Preview

+ + +
+
+
diff --git a/src/management/application/dialog/applicationDialog.controller.ts b/src/management/api/plans/plan/plan-wizard-general.component.ts similarity index 52% rename from src/management/application/dialog/applicationDialog.controller.ts rename to src/management/api/plans/plan/plan-wizard-general.component.ts index d33b952713..6fcd32c083 100644 --- a/src/management/application/dialog/applicationDialog.controller.ts +++ b/src/management/api/plans/plan/plan-wizard-general.component.ts @@ -13,22 +13,19 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -function DialogApplicationController($scope, $mdDialog, ApplicationService, NotificationService) { - 'ngInject'; +const ApiPlanWizardGeneralComponent: ng.IComponentOptions = { + require: { + parent: '^editPlan' + }, + template: require("./plan-wizard-general.html"), + controller: function() { + 'ngInject'; - $scope.hide = function () { - $mdDialog.cancel(); - }; + this.gotoNextStep = function() { + this.parent.vm.stepData[0].data = this.plan; + this.parent.moveToNextStep(this.parent.vm.stepData[0]); + }; + } +}; - $scope.create = function (application) { - ApplicationService.create(application).then(function (result) { - NotificationService.show('Application created'); - $mdDialog.hide(result); - }).catch(function (error) { - NotificationService.show('Error while creating the application'); - $scope.error = error.data.message; - }); - }; -} - -export default DialogApplicationController; +export default ApiPlanWizardGeneralComponent; diff --git a/src/management/api/plans/plan/plan-wizard-general.html b/src/management/api/plans/plan/plan-wizard-general.html new file mode 100644 index 0000000000..d7538307e6 --- /dev/null +++ b/src/management/api/plans/plan/plan-wizard-general.html @@ -0,0 +1,98 @@ + + + + +
+
+ General + +
+ + + +
Plan name
+
+
Name is required.
+
The name has to be more than 4 characters long.
+
The name has to be less than 50 characters long.
+
+
+
+ +
+ + + +
+ Provide a description of your plan, what it does, ... +
+
+
Description is required.
+
+
+
+ +
+ + + + +
+ +
+ +
+ +
+ Authorizations + +
+ + +   Auto validate subscription + + +
+ +
+ + + + + {{group.name}} + + + +
+
+
+ +
+ + +
+ NEXT +
+
+
+
+
diff --git a/src/management/api/plans/plan/plan-wizard-policies.component.ts b/src/management/api/plans/plan/plan-wizard-policies.component.ts new file mode 100644 index 0000000000..1d19175352 --- /dev/null +++ b/src/management/api/plans/plan/plan-wizard-policies.component.ts @@ -0,0 +1,117 @@ +/* + * Copyright (C) 2015 The Gravitee team (http://gravitee.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import _ = require('lodash'); +import PolicyService from "../../../../services/policy.service"; +import ApiEditPlanController from "./edit-plan.controller"; + +class Policy { + id: string; + title: string; + description: string; + enable?: boolean = false; + schema?: string; + model?: object; + form?: ng.IFormController; +} + +const ApiPlanWizardPoliciesComponent: ng.IComponentOptions = { + bindings: { + plan: '<' + }, + require: { + parent: '^editPlan' + }, + template: require("./plan-wizard-policies.html"), + controller: class { + private policies: Policy[]; + private methods: string[] = ['GET', 'POST', 'PUT', 'DELETE', 'HEAD', 'PATCH', 'OPTIONS', 'TRACE', 'CONNECT']; + private plan: any; + private parent: ApiEditPlanController; + + constructor( + private PolicyService: PolicyService) { + 'ngInject'; + + this.policies = [ + { + id: 'rate-limit', title: 'Rate-Limiting', + description: 'Rate limit how many HTTP requests an application can make in a given period of seconds or minutes' + }, { + id: 'quota', title: 'Quota', + description: 'Rate limit how many HTTP requests an application can make in a given period of hours, days or months' + }, { + id: 'resource-filtering', title: 'Path Authorization', + description: 'Restrict paths according to whitelist and / or blacklist rules' + } + ]; + } + + $onInit() { + if (! this.plan.paths) { + this.plan.paths = {'/': []}; + } + + _.each(this.policies, policy => { + this.PolicyService.getSchema(policy.id).then(schema => { + policy.schema = schema.data; + + let pathPolicy = _.find(this.plan.paths['/'], function(pathPolicy) { + return pathPolicy[policy.id] != undefined; + }); + + if (pathPolicy && pathPolicy[policy.id]) { + policy.enable = true; + policy.model = pathPolicy[policy.id]; + } else { + policy.model = {}; + } + }); + }); + } + + validate() { + return ! _.find(this.policies, policy => { + return policy.enable && policy.form && policy.form.$invalid; + }); + } + + gotoNextStep() { + let root = this.plan.paths['/']; + root.length = 0; + + _(this.policies) + .filter(policy => policy.enable) + .map(policy => { + let p = { + methods: this.methods, + enable: true + }; + p[policy.id] = policy.model; + + return p; + }) + .each(policy => root.push(policy)); + + this.parent.saveOrUpdate(); + }; + } +}; + + + + + +export default ApiPlanWizardPoliciesComponent; diff --git a/src/management/api/plans/plan/plan-wizard-policies.html b/src/management/api/plans/plan/plan-wizard-policies.html new file mode 100644 index 0000000000..8a95d3144d --- /dev/null +++ b/src/management/api/plans/plan/plan-wizard-policies.html @@ -0,0 +1,59 @@ + + + + + +
+ + + +
{{policy.title}}
+
{{policy.description}}
+
+ +
+ +
+
+
+
+
+ +
+ + +
+ PREVIOUS +
+
+ SAVE +
+
+
+
+
diff --git a/src/management/api/plans/plan/plan-wizard-security.component.ts b/src/management/api/plans/plan/plan-wizard-security.component.ts new file mode 100644 index 0000000000..2a8b15f364 --- /dev/null +++ b/src/management/api/plans/plan/plan-wizard-security.component.ts @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2015 The Gravitee team (http://gravitee.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import _ = require('lodash'); +import PolicyService from "../../../../services/policy.service"; +import ApiEditPlanController from "./edit-plan.controller"; + +const ApiPlanWizardSecurityComponent: ng.IComponentOptions = { + require: { + parent: '^editPlan' + }, + template: require("./plan-wizard-security.html"), + controller: class { + private securityTypes: any[]; + private securityDefinition: any; + private securitySchema: any; + private parent: ApiEditPlanController; + private plan: any; + + constructor(private PolicyService: PolicyService) { + 'ngInject'; + + this.securityTypes = [ + { + 'id': 'oauth2', + 'name': 'OAuth2', + 'policy': 'oauth2' + }, { + 'id': 'jwt', + 'name': 'JWT', + 'policy': 'jwt' + }, { + 'id': 'api_key', + 'name': 'API Key', + 'policy': 'api-key' + }, { + 'id': 'key_less', + 'name': 'Keyless (public)' + }]; + } + + $onInit() { + if (this.parent.plan.security) { + this.onSecurityTypeChange(); + } + } + + onSecurityTypeChange() { + let securityType: any = _.find(this.securityTypes, {'id': this.parent.plan.security}); + if (securityType && securityType.policy) { + this.PolicyService.getSchema(securityType.policy).then(schema => { + this.securitySchema = schema.data; + if (this.parent.plan.securityDefinition) { + this.parent.plan.securityDefinition = JSON.parse(this.parent.plan.securityDefinition); + } else { + this.parent.plan.securityDefinition = {}; + } + }); + } else { + this.securitySchema = undefined; + this.parent.plan.securityDefinition = {}; + } + }; + + gotoNextStep() { + this.parent.plan.securityDefinition = JSON.stringify(this.parent.plan.securityDefinition); + this.parent.vm.stepData[1].data = this.parent.plan; + this.parent.moveToNextStep(this.parent.vm.stepData[1]); + }; + } +}; + +export default ApiPlanWizardSecurityComponent; diff --git a/src/management/api/plans/plan/plan-wizard-security.html b/src/management/api/plans/plan/plan-wizard-security.html new file mode 100644 index 0000000000..05875edd13 --- /dev/null +++ b/src/management/api/plans/plan/plan-wizard-security.html @@ -0,0 +1,78 @@ + + + + +
+ Authentication +
+
+ + + + {{type.name}} + +
Select the type of authentication
+
+
+
+
+ +
+ +
+ Configuration +
+
+
+ +
+ + + +
+ PREVIOUS +
+
+ NEXT +
+
+
+
+
diff --git a/src/management/api/plans/apiPlans.controller.ts b/src/management/api/plans/plans.controller.ts similarity index 92% rename from src/management/api/plans/apiPlans.controller.ts rename to src/management/api/plans/plans.controller.ts index c51e16c42c..4dcc3579c4 100644 --- a/src/management/api/plans/apiPlans.controller.ts +++ b/src/management/api/plans/plans.controller.ts @@ -18,6 +18,7 @@ import angular = require('angular'); import UserService from '../../../services/user.service'; class ApiPlansController { + private api: any; private plans: any; private groups: any; private dndEnabled: boolean; @@ -26,34 +27,20 @@ class ApiPlansController { private securityTypes: {id: string; name: string}[]; private countByStatus: any; private filteredPlans: any; + constructor( - private resolvedPlans, - private resolvedGroups, - private resolvedApi, private $mdSidenav, private $mdDialog, private $scope, private ApiService, - private $state, - private $stateParams, + private $state: ng.ui.IStateService, + private $stateParams: ng.ui.IStateParamsService, private NotificationService, private dragularService, private UserService: UserService ) { 'ngInject'; - this.plans = resolvedPlans.data; - if (resolvedApi.data.visibility === "private") { - if (resolvedApi.data.groups) { - const apiGroupIds = resolvedApi.data.groups; - this.groups = _.filter(resolvedGroups, (group) => { - return apiGroupIds.indexOf(group["id"]) > -1; - }); - } else { - this.groups = []; - } - } else { - this.groups = resolvedGroups; - } + /* this.dndEnabled = UserService.isUserHasPermissions(['api-plan-u']); this.statusFilters = ['staging', 'published', 'closed']; this.selectedStatus = ['published']; @@ -70,15 +57,7 @@ class ApiPlansController { this.countByStatus = {}; this.resetPlan(); - if ($stateParams.state) { - if (_.includes(this.statusFilters, $stateParams.state)) { - this.changeFilter($stateParams.state); - } else { - this.applyFilters(); - } - } else { - this.applyFilters(); - } + var that = this; $scope.configure = function (plan) { @@ -124,9 +103,21 @@ class ApiPlansController { $scope.$on('planChangeSuccess', function(event, args) { that.changeFilter(args.state); }); + */ } +/* + $onInit() { + if (this.api.visibility === "private") { + if (this.api.groups) { + const apiGroupIds = this.api.groups; + this.groups = _.filter(this.groups, (group) => { + return apiGroupIds.indexOf(group["id"]) > -1; + }); + } else { + this.groups = []; + } + } - init() { let that = this; let d = document.querySelector('.plans'); @@ -149,7 +140,18 @@ class ApiPlansController { that.NotificationService.show('Plans have been reordered successfully'); }); }); + + if (this.$stateParams.state) { + if (_.includes(this.statusFilters, this.$stateParams.state)) { + this.changeFilter(this.$stateParams.state); + } else { + this.applyFilters(); + } + } else { + this.applyFilters(); + } } + */ list() { this.ApiService.getApiPlans(this.$stateParams.apiId).then(response => { @@ -216,10 +218,11 @@ class ApiPlansController { } addPlan() { - this.resetPlan(); - this.$scope.plan.authorizedGroups = _.map(this.groups, "id"); - this.$mdSidenav('plan-edit').toggle(); - this.$mdSidenav('live-preview').toggle(); + this.$state.go('management.apis.detail.plans.new'); + } + + editPlan(planId: string) { + this.$state.go('management.apis.detail.plans.plan', {planId: planId}); } save() { diff --git a/src/management/api/plans/publishPlanDialog.controller.ts b/src/management/api/plans/publishPlanDialog.controller.ts index 9dac2ef441..5eb470a7e5 100644 --- a/src/management/api/plans/publishPlanDialog.controller.ts +++ b/src/management/api/plans/publishPlanDialog.controller.ts @@ -13,10 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -function DialogPublishPlanController($scope, $rootScope, $mdDialog, ApiService, NotificationService, apiId, plan) { +function DialogPublishPlanController($scope, $mdDialog, plan) { 'ngInject'; - $scope.apiId = apiId; $scope.plan = plan; $scope.hide = function () { @@ -24,13 +23,6 @@ function DialogPublishPlanController($scope, $rootScope, $mdDialog, ApiService, }; $scope.publish = function () { - ApiService.publishPlan($scope.apiId, $scope.plan.id).then(function() { - NotificationService.show('Plan ' + plan.name + ' has been published'); - $rootScope.$broadcast("planChangeSuccess", { state: "published"}); - }).catch(function (error) { - $scope.error = error; - }); - $mdDialog.hide($scope.plan); }; } diff --git a/src/management/api/subscriptions/plan-subscriptions.component.ts b/src/management/api/subscriptions/plan-subscriptions.component.ts new file mode 100644 index 0000000000..f2d822cccd --- /dev/null +++ b/src/management/api/subscriptions/plan-subscriptions.component.ts @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2015 The Gravitee team (http://gravitee.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import _ = require('lodash'); + +const ApiPlanSubscriptionsComponent: ng.IComponentOptions = { + bindings: { + api: '<', + subscriptions: '<' + }, + template: require('./plan-subscriptions.html'), + controller: class { + private statusFilters: string[]; + private selectedStatus: string[]; + + private subscriptions: any[]; + private filteredSubscriptions: any[]; + + constructor( + ) { + 'ngInject'; + + this.statusFilters = ['pending', 'accepted', 'closed', 'rejected']; + this.selectedStatus = ['accepted']; + } + + $onInit() { + this.applyFilters(); + } + + changeFilter(statusFilter: string) { + if (_.includes(this.selectedStatus, statusFilter)) { + _.pull(this.selectedStatus, statusFilter); + } else { + this.selectedStatus.push(statusFilter); + } + this.applyFilters(); + } + + applyFilters() { + this.filteredSubscriptions = + _(this.subscriptions) + .filter((subscription) => _.includes(this.selectedStatus, subscription.status)) + .value(); + } + + } +}; + +export default ApiPlanSubscriptionsComponent; diff --git a/src/management/api/subscriptions/plan-subscriptions.html b/src/management/api/subscriptions/plan-subscriptions.html new file mode 100644 index 0000000000..16577c4e00 --- /dev/null +++ b/src/management/api/subscriptions/plan-subscriptions.html @@ -0,0 +1,108 @@ + +
+ Subscriptions + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + +
ApplicationDateProcessed atStart atEnd atStatus + +
+ {{subscription.application}} + {{subscription.created_at | date:'yyyy-MM-dd HH:mm:ss'}}{{subscription.processed_at | date:'yyyy-MM-dd HH:mm:ss'}}{{subscription.starting_at || '-' | date:'yyyy-MM-dd HH:mm:ss'}}{{subscription.ending_at || '-' | date:'yyyy-MM-dd HH:mm:ss'}} + {{subscription.reason}} + {{subscription.status}} + +
+ + Accept subscription + + + + Refuse subscription + + +
+
+ + + Close subscription + + +
+
+
+
+ + + + + diff --git a/src/management/api/subscriptions/subscription.component.ts b/src/management/api/subscriptions/subscription.component.ts new file mode 100644 index 0000000000..fc64caae89 --- /dev/null +++ b/src/management/api/subscriptions/subscription.component.ts @@ -0,0 +1,177 @@ +/* + * Copyright (C) 2015 The Gravitee team (http://gravitee.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import _ = require('lodash'); +import ApiService from "../../../services/api.service"; +import NotificationService from "../../../services/notification.service"; + +const ApiSubscriptionComponent: ng.IComponentOptions = { + bindings: { + api: '<', + subscription: '<' + }, + template: require('./subscription.html'), + controller: class { + + private subscription:any; + private keys:any[]; + private api: any; + + constructor( + private $rootScope: ng.IRootScopeService, + private $mdDialog: angular.material.IDialogService, + private NotificationService: NotificationService, + private ApiService: ApiService + ) { + 'ngInject'; + } + + $onInit() { + this.listApiKeys(); + } + + listApiKeys() { + if (this.subscription.plan.security === 'api_key') { + // Retrieve api_keys for current current subscription + this.ApiService.listApiKeys(this.api.id, this.subscription.id).then((response) => { + this.keys = response.data; + }); + } + } + + close() { + let msg = 'The application will not be able to consume API anymore.'; + if (this.subscription.plan.security === 'api_key') { + msg += 'All Api-keys associated to this subscription will be closed and could no be used.' + } + + this.$mdDialog.show({ + controller: 'DialogConfirmController', + controllerAs: 'ctrl', + template: require('../../../components/dialog/confirmWarning.dialog.html'), + clickOutsideToClose: true, + locals: { + title: 'Are you sure you want to close this subscription?', + msg: msg, + confirmButton: 'Close' + } + }).then( (response) => { + if (response) { + this.ApiService.closeSubscription(this.api.id, this.subscription.id).then((response) => { + this.NotificationService.show('The subscription has been closed'); + this.subscription = response.data; + this.listApiKeys(); + }); + } + }); + } + + reject() { + this.$mdDialog.show({ + controller: 'DialogSubscriptionRejectController', + controllerAs: 'dialogSubscriptionRejectController', + template: require('./subscription.reject.dialog.html'), + clickOutsideToClose: true + }).then( (reason) => { + this.process({accepted: false, reason: reason}); + }); + } + + accept() { + this.$mdDialog.show({ + controller: 'DialogSubscriptionAcceptController', + controllerAs: 'dialogSubscriptionAcceptController', + template: require('./subscription.accept.dialog.html'), + clickOutsideToClose: true + }).then( (subscription) => { + subscription.accepted = true; + this.process(subscription); + this.listApiKeys(); + }); + } + + process(processSubscription) { + this.ApiService.processSubscription(this.api.id, this.subscription.id, processSubscription) + .then( (response) => { + this.NotificationService.show('The subscription has been ' + (processSubscription.accepted ? 'accepted' : 'rejected')); + this.subscription = response.data; + this.$rootScope.$broadcast("graviteeUserTaskRefresh"); + }); + } + + renewApiKey() { + this.$mdDialog.show({ + controller: 'DialogConfirmController', + controllerAs: 'ctrl', + template: require('../../../components/dialog/confirmWarning.dialog.html'), + clickOutsideToClose: true, + locals: { + title: 'Are you sure you want to renew your API Key?', + msg: 'Your previous API Key will be no longer valid in 2 hours!', + confirmButton: 'Renew' + } + }).then( (response) => { + if (response) { + this.ApiService.renewApiKey(this.api.id, this.subscription.id).then(() => { + this.NotificationService.show('A new API Key has been generated'); + this.listApiKeys(); + }); + } + }); + } + + revokeApiKey(apiKey) { + this.$mdDialog.show({ + controller: 'DialogConfirmController', + controllerAs: 'ctrl', + template: require('../../../components/dialog/confirmWarning.dialog.html'), + clickOutsideToClose: true, + locals: { + title: 'Are you sure you want to revoke API Key \'' + apiKey + '\' ?', + confirmButton: 'Revoke' + } + }).then( (response) => { + if (response) { + this.ApiService.revokeApiKey(this.api.id, this.subscription.id, apiKey).then(() => { + this.NotificationService.show('API Key ' + apiKey + ' has been revoked!'); + this.listApiKeys(); + }); + } + }); + } + + expireApiKey(apiKey) { + this.$mdDialog.show({ + controller: 'DialogApiKeyExpirationController', + controllerAs: 'dialogApiKeyExpirationController', + template: require('./apikey.expiration.dialog.html'), + clickOutsideToClose: true + }).then(expirationDate => { + apiKey.expire_at = expirationDate; + + this.ApiService.updateApiKey(this.api.id, apiKey).then(() => { + this.NotificationService.show('An expiration date has been defined for API Key.'); + }); + }); + } + + onCopyApiKeySuccess(e) { + this.NotificationService.show('API Key has been copied to clipboard'); + e.clearSelection(); + } + } +}; + +export default ApiSubscriptionComponent; diff --git a/src/management/api/subscriptions/subscription.create.dialog.controller.ts b/src/management/api/subscriptions/subscription.create.dialog.controller.ts index 33519aecea..a2bd7c1f29 100644 --- a/src/management/api/subscriptions/subscription.create.dialog.controller.ts +++ b/src/management/api/subscriptions/subscription.create.dialog.controller.ts @@ -14,9 +14,14 @@ * limitations under the License. */ import * as _ from 'lodash'; +import ApiService from "../../../services/api.service"; +import ApplicationService from "../../../services/applications.service"; -function DialogSubscriptionCreateController($scope, $mdDialog, plans, ApplicationService) { +function DialogSubscriptionCreateController( + $mdDialog: angular.material.IDialogService, plans, api, + ApplicationService: ApplicationService, ApiService: ApiService) { 'ngInject'; + this.api = api; this.plans = plans; this.selectedApp = null; this.selectedPlan = null; @@ -44,14 +49,11 @@ function DialogSubscriptionCreateController($scope, $mdDialog, plans, Applicatio this.plansWithSubscriptions = []; this.selectedPlan = null; if (this.selectedApp) { - _.map(this.plans, (plan: {id: number}) => { - ApplicationService.listSubscriptions(this.selectedApp.id, plan.id).then((response) => { - var subs = _.filter(response.data, (sub: {status: string}) => { - return sub.status === "pending" || sub.status === "accepted"; - }); - if (subs.length > 0) { - this.plansWithSubscriptions.push(plan.id); - } + ApiService.getSubscriptions( + this.api.id, + '?application=' + this.selectedApp.id + '&status=pending,accepted').then((response) => { + this.plansWithSubscriptions = _.map(response.data.data, function(subscription) { + return subscription.plan; }); }); } @@ -62,7 +64,6 @@ function DialogSubscriptionCreateController($scope, $mdDialog, plans, Applicatio return response.data; }); }; - } export default DialogSubscriptionCreateController; diff --git a/src/management/api/subscriptions/subscription.html b/src/management/api/subscriptions/subscription.html new file mode 100644 index 0000000000..f931055f40 --- /dev/null +++ b/src/management/api/subscriptions/subscription.html @@ -0,0 +1,142 @@ + +
+ Subscriptions + +
+ + Subscription + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ID{{$ctrl.subscription.id | date:'MMM d, y h:mm:ss.sss a'}}
Date{{$ctrl.subscription.created_at | date:'MMM d, y h:mm:ss.sss a'}}
Plan{{$ctrl.subscription.plan.name}} ({{$ctrl.subscription.plan.security}})
Application{{$ctrl.subscription.application.name}} ({{$ctrl.subscription.application.owner.username}})
Status{{$ctrl.subscription.status}}
Rejection reason{{$ctrl.subscription.reason}}
Processed at{{$ctrl.subscription.processed_at || '-' | date:'MMM d, y h:mm:ss.sss a'}}
Starting at{{$ctrl.subscription.starting_at || '-' | date:'MMM d, y h:mm:ss.sss a'}}
Ending at{{$ctrl.subscription.ending_at || '-' | date:'MMM d, y h:mm:ss.sss a'}}
+
+ +
+ + + Accept + + + + Reject + +
+ +
+ + + Close + +
+ +
+
+ Api Keys + + + + + + + + + + + + + + + + + + + +
KeyCreated atRevoked / Expire at
+ + {{key.key}} + + Copy to clipboard + + + {{key.created_at | date:'yyyy-MM-dd HH:mm:ss'}}{{key.revoked_at || key.expire_at | date:'yyyy-MM-dd HH:mm:ss'}} + + Revoke + + + + Set expiration date + + +
+
+ +
+ + + Renew API Key + +
+
+
+
+
diff --git a/src/management/api/subscriptions/subscriptions.component.ts b/src/management/api/subscriptions/subscriptions.component.ts new file mode 100644 index 0000000000..a7d27f3dc7 --- /dev/null +++ b/src/management/api/subscriptions/subscriptions.component.ts @@ -0,0 +1,140 @@ +/* + * Copyright (C) 2015 The Gravitee team (http://gravitee.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import _ = require('lodash'); +import ApiService from "../../../services/api.service"; +import NotificationService from "../../../services/notification.service"; +import { PagedResult } from "../../../entities/pagedResult"; + +export class SubscriptionQuery { + status?: string[] = ['ACCEPTED']; + applications?: string[]; + plans?: string[]; + page?: number = 1; + size?: number = 20; +} + +const ApiSubscriptionsComponent: ng.IComponentOptions = { + bindings: { + api: '<', + plans: '<', + subscriptions: '<', + subscribers: '<' + }, + template: require('./subscriptions.html'), + controller: class { + + private subscriptions: PagedResult; + private api: any; + + private query: SubscriptionQuery = new SubscriptionQuery(); + + private status = { + 'ACCEPTED': 'Accepted', + 'CLOSED': 'Closed', + 'PENDING': 'Pending', + 'REJECTED': 'Rejected' + }; + + private subscriptionsFiltersForm: any; + + constructor( + private ApiService: ApiService, + private NotificationService: NotificationService, + private $mdDialog: angular.material.IDialogService, + private $state: ng.ui.IStateService + ) { + 'ngInject'; + + this.onPaginate = this.onPaginate.bind(this); + } + + onPaginate(page) { + this.query.page = page; + this.doSearch(); + } + + clearFilters() { + this.subscriptionsFiltersForm.$setPristine(); + this.query = new SubscriptionQuery(); + this.doSearch(); + } + + search() { + this.query.page = 1; + this.query.size = 20; + this.doSearch(); + } + + buildQuery() { + let query = '?page=' + this.query.page + '&size=' + this.query.size + '&'; + let parameters = {}; + + if (this.query.status !== undefined) { + parameters['status'] = this.query.status.join(','); + } + + if (this.query.applications !== undefined) { + parameters['application'] = this.query.applications.join(','); + } + + if (this.query.plans !== undefined) { + parameters['plan'] = this.query.plans.join(','); + } + + _.mapKeys(parameters, function( value, key ) { + query += key + '=' + value + '&'; + }); + + return query; + } + + doSearch() { + let query = this.buildQuery(); + + this.ApiService.getSubscriptions(this.api.id, query).then((response) => { + this.subscriptions = response.data as PagedResult; + }); + } + + showAddSubscriptionModal() { + this.ApiService.getPublishedApiPlans(this.api.id).then( (response) => { + // Allow only subscribable plan + let plans = _.filter(response.data, (plan: any) => { return plan.security !== 'key_less'; }); + + this.$mdDialog.show({ + controller: 'DialogSubscriptionCreateController', + controllerAs: 'dialogSubscriptionCreateController', + template: require('./subscription.create.dialog.html'), + clickOutsideToClose: true, + locals: { + api: this.api, + plans: plans + } + }).then( (data) => { + if (data && data.applicationId && data.planId) { + this.ApiService.subscribe(data.applicationId, data.applicationId, data.planId).then( (response) => { + let subscription = response.data; + this.NotificationService.show('A new subscription has been created.'); + this.$state.go('management.apis.detail.subscriptions.subscription', {subscriptionId: subscription.id}, {reload: true}); + }); + } + }); + }); + } + } +}; + +export default ApiSubscriptionsComponent; diff --git a/src/management/api/subscriptions/subscriptions.controller.ts b/src/management/api/subscriptions/subscriptions.controller.ts deleted file mode 100644 index 98fb1a11aa..0000000000 --- a/src/management/api/subscriptions/subscriptions.controller.ts +++ /dev/null @@ -1,246 +0,0 @@ -/* - * Copyright (C) 2015 The Gravitee team (http://gravitee.io) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import * as _ from 'lodash'; - -class SubscriptionsController { - private api: any; - private subscriptions: any; - private statusFilters: string[]; - private selectedStatus: string[]; - private subscriptionsByApplication: any; - - constructor( - private $mdDialog: angular.material.IDialogService, - private $scope, - private $rootScope, - private ApiService, - private NotificationService, - private resolvedApi, - private resolvedSubscriptions, - private ApplicationService - ) { - 'ngInject'; - $scope.data = []; - - this.api = resolvedApi.data; - this.subscriptions = resolvedSubscriptions.data; - $scope.showRevokedKeys = false; - - this.statusFilters = ['accepted', 'pending', 'rejected', 'closed']; - this.selectedStatus = ['accepted', 'pending']; - this.applyFilters(); - } - - changeFilter(statusFilter, notPull) { - if (_.includes(this.selectedStatus, statusFilter)) { - if (!notPull) { - _.pull(this.selectedStatus, statusFilter); - } - } else { - this.selectedStatus.push(statusFilter); - } - this.applyFilters(); - } - - applyFilters() { - var that = this; - this.subscriptionsByApplication = - _(this.subscriptions) - .filter((subscription) => _.includes(that.selectedStatus, subscription.status)) - .groupBy('application.id') - .orderBy((subscriptions) => subscriptions[0].application.name) - .value(); - } - - hasKeysDefined() { - return this.subscriptions !== null && Object.keys(this.subscriptions).length > 0; - } - - revoke(subscription, apiKey) { - var that = this; - this.$mdDialog.show({ - controller: 'DialogConfirmController', - controllerAs: 'ctrl', - template: require('../../../components/dialog/confirmWarning.dialog.html'), - clickOutsideToClose: true, - locals: { - title: 'Are you sure you want to revoke API Key \'' + apiKey + '\' ?', - confirmButton: 'Revoke' - } - }).then(function (response) { - if (response) { - that.ApiService.revokeApiKey(that.api.id, subscription.id, apiKey).then(() => { - that.NotificationService.show('API Key ' + apiKey + ' has been revoked !'); - that.refresh(); - }); - } - }); - } - - onClipboardSuccess(e) { - this.NotificationService.show('API Key has been copied to clipboard'); - e.clearSelection(); - } - - showExpirationModal(apiKey) { - this.$mdDialog.show({ - controller: 'DialogApiKeyExpirationController', - controllerAs: 'dialogApiKeyExpirationController', - template: require('./apikey.expiration.dialog.html'), - clickOutsideToClose: true - }).then(expirationDate =>{ - apiKey.expire_at = expirationDate; - - this.ApiService.updateApiKey(this.api.id, apiKey).then(() => { - this.NotificationService.show('An expiration date has been settled for API Key'); - }); - }); - } - - refresh() { - var that = this; - this.ApiService.getSubscriptions(this.api.id).then(function (response) { - that.subscriptions = response.data; - that.applyFilters(); - }); - } - - process(subscription, accepted) { - var that = this; - if (accepted) { - this.$mdDialog.show({ - controller: 'DialogSubscriptionAcceptController', - controllerAs: 'dialogSubscriptionAcceptController', - template: require('./subscription.accept.dialog.html'), - clickOutsideToClose: true - }).then(function (processSubscription) { - processSubscription.accepted = accepted; - that.doProcessSubscription(subscription, processSubscription); - }); - } else { - this.$mdDialog.show({ - controller: 'DialogSubscriptionRejectController', - controllerAs: 'dialogSubscriptionRejectController', - template: require('./subscription.reject.dialog.html'), - clickOutsideToClose: true - }).then(function (reason) { - that.doProcessSubscription(subscription, {accepted: accepted, reason: reason}); - }); - } - } - - doProcessSubscription(subscription, processSubscription) { - var that = this; - this.ApiService.processPlanSubscription(this.api.id, subscription.plan.id, subscription.id, processSubscription).then(function () { - that.refresh(); - that.NotificationService.show('The subscription has been ' + (processSubscription.accepted ? 'accepted' : 'rejected') + ' with success'); - that.$rootScope.$broadcast("graviteeUserTaskRefresh"); - }); - } - - toggleSubscription(scope, subscription) { - scope.toggle(); - if (!subscription.apiKeys) { - this.listApiKeys(subscription); - } - } - - listApiKeys(subscription) { - this.ApiService.listApiKeys(this.api.id, subscription.id).then(response => { - subscription.apiKeys = response.data; - }); - } - - generateAPIKey(apiId, subscription) { - var _this = this; - this.$mdDialog.show({ - controller: 'DialogConfirmController', - controllerAs: 'ctrl', - template: require('../../../components/dialog/confirmWarning.dialog.html'), - clickOutsideToClose: true, - locals: { - title: 'Are you sure you want to renew your API Key?', - msg: 'Your previous API Key will be no longer valid in 1 hour!', - confirmButton: 'Renew' - } - }).then(function (response) { - if (response) { - _this.ApiService.renewApiKey(apiId, subscription.id).then(() => { - _this.NotificationService.show('A new API Key has been generated'); - _this.listApiKeys(subscription); - }); - } - }); - } - - showAddSubscriptionModal() { - var _this = this; - this.ApiService.getPublishedApiPlans(this.api.id).then( (response) => { - // Allow only subscribable plan - var plans = _.filter(response.data, (plan: any) => { return plan.security !== 'key_less'; }); - - _this.$mdDialog.show({ - controller: 'DialogSubscriptionCreateController', - controllerAs: 'dialogSubscriptionCreateController', - template: require('./subscription.create.dialog.html'), - clickOutsideToClose: true, - locals: { - plans: plans, - } - }).then( (data) => { - if(data && data.applicationId && data.planId) { - _this.ApplicationService.subscribe(data.applicationId, data.planId).then( (response) => { - var newSub = response.data; - _this.NotificationService.show('A new subscription has been created.'); - _this.subscriptions.push(newSub); - if (newSub.status === "pending") { - _this.doProcessSubscription(newSub, {accepted: true}); - } else { - _this.refresh(); - } - }); - } - }); - }); - } - - closeSubscription(apiId, subscription) { - let _this = this; - this.$mdDialog.show({ - controller: 'DialogConfirmController', - controllerAs: 'ctrl', - template: require('../../../components/dialog/confirmWarning.dialog.html'), - clickOutsideToClose: true, - locals: { - title: 'Are you sure you want to close this subscription?', - msg: 'Your API Keys will be revoked!', - confirmButton: 'Close' - } - }).then(function (response) { - if (response) { - _this.ApiService.closeSubscription(apiId, subscription.id).then(() => { - _this.NotificationService.show('The subscription has been closed successfully'); - _this.ApiService.getSubscriptions(apiId).then((response) => { - _this.subscriptions = response.data; - _this.changeFilter('closed', true); - }); - }); - } - }); - } -} - -export default SubscriptionsController; diff --git a/src/management/api/subscriptions/subscriptions.html b/src/management/api/subscriptions/subscriptions.html index 2acab4734c..c23bf9028d 100644 --- a/src/management/api/subscriptions/subscriptions.html +++ b/src/management/api/subscriptions/subscriptions.html @@ -15,50 +15,74 @@ limitations under the License. --> -
+
Subscriptions - - - + +
+ + + Filters + + + +
+
+ + + + {{ plan.name }} + + + + + + {{ subscriber.name }} () + + + + + + {{ value }} + + +
+ + Clear + + + Search + +
+
+
+
+
+
-
-
-

{{subscriptions[0].application.name}} - ( {{subscriptions[0].application.type}} ) - by {{subscriptions[0].application.owner.username}} -

- - - - - - - - - - - - - - - +
PlanSubscription DateProcess DateStart DateEnd DateStatus - - Show revoked - -
- - - - {{subscription.plan.name}} +
+ + + + + + + + + + + + + + + + + + @@ -67,67 +91,28 @@

{{subscriptions[0].application.name}} {{subscription.reason}} {{subscription.status}} -

- - - - - - - -
PlanApplicationCreated atProcessed atStart atEnd atStatus
+ {{$ctrl.subscriptions.metadata[subscription.plan].name}} {{$ctrl.subscriptions.metadata[subscription.application].name}} {{subscription.created_at | date:'yyyy-MM-dd HH:mm:ss'}} {{subscription.processed_at | date:'yyyy-MM-dd HH:mm:ss'}} {{subscription.starting_at || '-' | date:'yyyy-MM-dd HH:mm:ss'}} -
- - Accept subscription - - - - Refuse subscription - - -
-
- - Renew your API Key - - - - Close subscription - - -
-
- - {{apiKey.key}} - - Copy to clipboard - - - {{apiKey.revoked_at || apiKey.expire_at | date:'yyyy-MM-dd HH:mm:ss'}} - - Revoke this API Key - - - - Set expiration for this API Key - - -
- - +
+ + + +
- - + diff --git a/src/management/application/applications.controller.ts b/src/management/application/applications.controller.ts index 4d0fa30980..b39e450473 100644 --- a/src/management/application/applications.controller.ts +++ b/src/management/application/applications.controller.ts @@ -13,9 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import * as angular from 'angular'; -import UserService from '../../services/user.service'; import * as _ from "lodash"; +import UserService from "../../services/user.service"; class ApplicationsController { private applications: any; @@ -24,9 +23,6 @@ class ApplicationsController { private subMessage: string; constructor( - private $mdDialog: ng.material.IDialogService, - private $state: ng.ui.IStateService, - private $rootScope, private UserService: UserService, private $filter ) { @@ -45,31 +41,6 @@ class ApplicationsController { }); } - createInitApplication() { - if (!this.$rootScope.graviteeUser) { - this.$rootScope.$broadcast('authenticationRequired'); - } else { - this.showAddApplicationModal(null); - } - } - - showAddApplicationModal(ev) { - let that = this; - this.$mdDialog.show({ - controller: 'DialogApplicationController', - template: require('./dialog/application.dialog.html'), - parent: angular.element(document.body), - targetEvent: ev, - clickOutsideToClose: true - }).then(function (application) { - if (application) { - that.$state.go('management.applications.portal.general', {applicationId: application.data.id}, {reload: true}); - } - }, function() { - // You cancelled the dialog - }); - } - loadMore = function (order, searchApplications, showNext) { const doNotLoad = showNext && (this.applications && this.applications.length) === (this.applicationsToDisplay && this.applicationsToDisplay.length); if (!doNotLoad && this.applications && this.applications.length) { diff --git a/src/management/application/applications.html b/src/management/application/applications.html index 0e32985569..da3d8ac8e1 100644 --- a/src/management/application/applications.html +++ b/src/management/application/applications.html @@ -37,7 +37,7 @@ - {{application.name}} @@ -58,7 +58,8 @@ create-mode="true">
- +
diff --git a/src/management/application/applications.route.ts b/src/management/application/applications.route.ts index 2b2003bd6d..c212f26840 100644 --- a/src/management/application/applications.route.ts +++ b/src/management/application/applications.route.ts @@ -44,9 +44,25 @@ function applicationsConfig($stateProvider: ng.ui.IStateProvider) { }, resolve: { applications: (ApplicationService: ApplicationService) => ApplicationService.list().then(response => response.data) + } + }) + .state('management.applications.new', { + url: '/new', + component: 'createApplication', + data: { + perms: { + only: ['portal-application-c'] + }, + devMode: true, + docs: { + page: 'management-create-application' + } }, + resolve: { + applications: (ApplicationService: ApplicationService) => ApplicationService.list().then(response => response.data) + } }) - .state('management.applications.portal', { + .state('management.applications.application', { abstract: true, url: '/:applicationId', component: 'application', @@ -68,7 +84,7 @@ function applicationsConfig($stateProvider: ng.ui.IStateProvider) { } } }) - .state('management.applications.portal.general', { + .state('management.applications.application.general', { url: '/', component: 'applicationGeneral', data: { @@ -96,8 +112,13 @@ function applicationsConfig($stateProvider: ng.ui.IStateProvider) { } } }) - .state('management.applications.portal.subscriptions', { + .state('management.applications.application.subscriptions', { + abstract: true, url: '/subscriptions', + template: '
' + }) + .state('management.applications.application.subscriptions.list', { + url: '', component: 'applicationSubscriptions', resolve: { subscriptions: ($stateParams: ng.ui.IStateParamsService, ApplicationService: ApplicationService) => @@ -117,12 +138,28 @@ function applicationsConfig($stateProvider: ng.ui.IStateProvider) { } } }) - .state('management.applications.portal.members', { + .state('management.applications.application.subscriptions.subscription', { + url: '/:subscriptionId', + component: 'applicationSubscription', + resolve: { + subscription: ($stateParams: ng.ui.IStateParamsService, ApplicationService: ApplicationService) => + ApplicationService.getSubscription($stateParams.applicationId, $stateParams.subscriptionId).then(response => response.data) + }, + data: { + perms: { + only: ['application-subscription-r'] + }, + docs: { + page: 'management-application-subscriptions' + } + } + }) + .state('management.applications.application.members', { url: '/members', component: 'applicationMembers', resolve: { members: ($stateParams: ng.ui.IStateParamsService, ApplicationService: ApplicationService) => - ApplicationService.getMembers($stateParams['applicationId']).then(response => response.data), + ApplicationService.getMembers($stateParams.applicationId).then(response => response.data), resolvedGroups: (GroupService: GroupService) => { return GroupService.list().then(response => { return response.data; @@ -143,7 +180,7 @@ function applicationsConfig($stateProvider: ng.ui.IStateProvider) { } } }) - .state('management.applications.portal.analytics', { + .state('management.applications.application.analytics', { url: '/analytics?from&to&q', component: 'applicationAnalytics', data: { @@ -174,7 +211,7 @@ function applicationsConfig($stateProvider: ng.ui.IStateProvider) { } } }) - .state('management.applications.portal.logs', { + .state('management.applications.application.logs', { url: '/logs?from&to&q', component: 'applicationLogs', data: { @@ -205,12 +242,12 @@ function applicationsConfig($stateProvider: ng.ui.IStateProvider) { } } }) - .state('management.applications.portal.log', { + .state('management.applications.application.log', { url: '/logs/:logId', component: 'applicationLog', resolve: { log: ($stateParams: ng.ui.IStateParamsService, ApplicationService: ApplicationService) => - ApplicationService.getLog($stateParams['applicationId'], $stateParams['logId']).then(response => response.data) + ApplicationService.getLog($stateParams.applicationId, $stateParams.logId).then(response => response.data) }, data: { devMode: true, diff --git a/src/management/application/create-application.component.ts b/src/management/application/create-application.component.ts new file mode 100644 index 0000000000..f6f96cb6ca --- /dev/null +++ b/src/management/application/create-application.component.ts @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2015 The Gravitee team (http://gravitee.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import ApplicationService from "../../services/applications.service"; +import NotificationService from "../../services/notification.service"; + +interface IApplicationScope extends ng.IScope { + formApplication: any; +} + +const CreateApplicationComponent: ng.IComponentOptions = { + template: require('./create-application.html'), + controller: class { + + private application: any; + + constructor( + private ApplicationService: ApplicationService, + private NotificationService: NotificationService, + private $scope: IApplicationScope, + private $state: ng.ui.IStateService + ) { + 'ngInject'; + + this.application = {}; + } + + create() { + this.ApplicationService.create(this.application).then((response) => { + this.NotificationService.show('Application ' + this.application.name + ' has been created'); + this.$state.go('management.applications.application.general', {applicationId: response.data.id}, {reload: true}); + }); + } + + reset() { + this.application = {}; + this.$scope.formApplication.$setPristine(); + } + } +}; + +export default CreateApplicationComponent; diff --git a/src/management/application/create-application.html b/src/management/application/create-application.html new file mode 100644 index 0000000000..124a72df38 --- /dev/null +++ b/src/management/application/create-application.html @@ -0,0 +1,82 @@ + +
+ +
+ Create an application +
+
+ +
+
+ + + +
Application name
+
+
Application name is required.
+
The name has to be more than 4 characters long.
+
The name has to be less than 50 characters long.
+
+
+ + + + +
+ Provide a description of your application, what it does, ... +
+
+
Application description is required.
+
+
+ +
+ + + +
+ The Client_id of the application. This field is required to subscribe to certain type of API + Plan (OAuth2, JWT). +
+
+
The name has to be more than 4 characters long.
+
The name has to be less than 50 characters long.
+
+
+ + + +
+ Type of the application (mobile, web, ...). +
+
+
+ +
+ + Create + + + Reset + +
+
+
+
diff --git a/src/management/application/details/analytics/application-analytics.controller.ts b/src/management/application/details/analytics/application-analytics.controller.ts index c2163d51c6..3d7eb87493 100644 --- a/src/management/application/details/analytics/application-analytics.controller.ts +++ b/src/management/application/details/analytics/application-analytics.controller.ts @@ -130,9 +130,9 @@ class ApplicationAnalyticsController { } viewLogs() { - // Update the query parameter + // update the query parameter this.$state.transitionTo( - 'management.applications.portal.logs', + 'management.applications.application.logs', this.$state.params); } } diff --git a/src/management/application/details/general/application-general.controller.ts b/src/management/application/details/general/application-general.controller.ts index d3c447ea34..b55cc904ee 100644 --- a/src/management/application/details/general/application-general.controller.ts +++ b/src/management/application/details/general/application-general.controller.ts @@ -17,7 +17,6 @@ import * as _ from 'lodash'; import ApplicationService from '../../../../services/applications.service'; import NotificationService from '../../../../services/notification.service'; -import GroupService from '../../../../services/group.service'; import SidenavService from '../../../../components/sidenav/sidenav.service'; interface IApplicationScope extends ng.IScope { @@ -27,7 +26,6 @@ interface IApplicationScope extends ng.IScope { class ApplicationGeneralController { private application: any; - private groups: any[]; private initialApplication: any; constructor( @@ -49,19 +47,21 @@ class ApplicationGeneralController { } update() { - this.ApplicationService.update(this.application).then(() => { - this.initialApplication = _.cloneDeep(this.application); - this.$scope.formApplication.$setPristine(); - this.NotificationService.show('Application ' + this.application.name + ' has been updated'); - this.SidenavService.setCurrentResource(this.application.name); - }); + this.ApplicationService.update(this.application) + .then(() => { + this.initialApplication = _.cloneDeep(this.application); + this.$scope.formApplication.$setPristine(); + this.NotificationService.show(this.application.name + ' has been updated'); + this.SidenavService.setCurrentResource(this.application.name); + }); } delete() { - this.ApplicationService.delete(this.application.id).then(() => { - this.NotificationService.show('Application ' + this.application.name + ' has been deleted'); - this.$state.go('management.applications.list', {}, {reload: true}); - }); + this.ApplicationService.delete(this.application.id) + .then(() => { + this.NotificationService.show(this.application.name + ' has been deleted'); + this.$state.go('management.applications.list', {}, {reload: true}); + }); } reset() { diff --git a/src/management/application/details/general/application-general.html b/src/management/application/details/general/application-general.html index ac621cab78..d8de408474 100644 --- a/src/management/application/details/general/application-general.html +++ b/src/management/application/details/general/application-general.html @@ -20,35 +20,68 @@
- - -
- - - - - - - - {{group.name}} - - + +
Application name
+
+
Application name is required.
+
The name has to be more than 4 characters long.
+
The name has to be less than 50 characters long.
+ + - + +
+ Provide a description of your application, what it does, ... +
+
+
Application description is required.
+
+
+ + + +
+ The Client_id of the application. This field is required to subscribe to certain type of API Plan (OAuth2, JWT). +
+
+
The name has to be more than 4 characters long.
+
The name has to be less than 50 characters long.
+
+
+ + + +
+ Type of the application (mobile, web, ...). +
+
+
+ +
+ + + + {{group.name}} + + +
+
Save - + Reset - + Delete
diff --git a/src/management/application/details/logs/application-logs.controller.ts b/src/management/application/details/logs/application-logs.controller.ts index e96a947687..a5bc8125d9 100644 --- a/src/management/application/details/logs/application-logs.controller.ts +++ b/src/management/application/details/logs/application-logs.controller.ts @@ -39,14 +39,9 @@ class ApplicationLogsController { } $onInit() { -// let updated = false; -// if (this.$state.params['from'] && this.$state.params['to']) { this.query.from = this.$state.params['from']; this.query.to = this.$state.params['to']; this.query.query = this.$state.params['q']; -// } - -// this.refresh(); }; timeframeChange(timeframe) { diff --git a/src/management/application/details/logs/application-logs.html b/src/management/application/details/logs/application-logs.html index 2d1d8c2a82..327358266c 100644 --- a/src/management/application/details/logs/application-logs.html +++ b/src/management/application/details/logs/application-logs.html @@ -51,7 +51,7 @@ {{$ctrl.logs.metadata[log.plan].name}} {{log.timestamp | date:'MMM d, y h:mm:ss.sss a'}} - + diff --git a/src/management/application/details/members/application-members.controller.ts b/src/management/application/details/members/application-members.controller.ts index a304d6e6ea..f46f1faed5 100644 --- a/src/management/application/details/members/application-members.controller.ts +++ b/src/management/application/details/members/application-members.controller.ts @@ -131,7 +131,7 @@ class ApplicationMembersController { } }).then(function (application) { if (application) { - that.$state.go('management.applications.portal.members', {applicationId: that.application.id}, {reload: true}); + that.$state.go('management.applications.application.members', {applicationId: that.application.id}, {reload: true}); } }, function() { // You cancelled the dialog diff --git a/src/management/application/details/subscriptions/application-subscription.component.ts b/src/management/application/details/subscriptions/application-subscription.component.ts new file mode 100644 index 0000000000..56fb27a93b --- /dev/null +++ b/src/management/application/details/subscriptions/application-subscription.component.ts @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2015 The Gravitee team (http://gravitee.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import _ = require('lodash'); + +import ApplicationService from "../../../../services/applications.service"; +import NotificationService from "../../../../services/notification.service"; + +const ApplicationSubscriptionComponent: ng.IComponentOptions = { + bindings: { + application: '<', + subscription: '<' + }, + template: require('./application-subscription.html'), + controller: class { + + private subscription:any; + private keys:any[]; + private application: any; + + constructor( + private $mdDialog: angular.material.IDialogService, + private NotificationService: NotificationService, + private ApplicationService: ApplicationService + ) { + 'ngInject'; + } + + $onInit() { + this.listApiKeys(); + } + + listApiKeys() { + if (this.subscription.plan.security === 'api_key') { + // Retrieve api_keys for current current subscription + this.ApplicationService.listApiKeys(this.application.id, this.subscription.id).then((response) => { + this.keys = response.data; + }); + } + } + + renewApiKey() { + this.$mdDialog.show({ + controller: 'DialogConfirmController', + controllerAs: 'ctrl', + template: require('../../../../components/dialog/confirmWarning.dialog.html'), + clickOutsideToClose: true, + locals: { + title: 'Are you sure you want to renew your API Key?', + msg: 'Your previous API Key will be no longer valid in 2 hours!', + confirmButton: 'Renew' + } + }).then( (response) => { + if (response) { + this.ApplicationService.renewApiKey(this.application.id, this.subscription.id).then(() => { + this.NotificationService.show('A new API Key has been generated'); + this.listApiKeys(); + }); + } + }); + } + + revokeApiKey(apiKey) { + this.$mdDialog.show({ + controller: 'DialogConfirmController', + controllerAs: 'ctrl', + template: require('../../../../components/dialog/confirmWarning.dialog.html'), + clickOutsideToClose: true, + locals: { + title: 'Are you sure you want to revoke API Key \'' + apiKey + '\' ?', + confirmButton: 'Revoke' + } + }).then( (response) => { + if (response) { + this.ApplicationService.revokeApiKey(this.application.id, this.subscription.id, apiKey).then(() => { + this.NotificationService.show('API Key ' + apiKey + ' has been revoked!'); + this.listApiKeys(); + }); + } + }); + } + + onCopyApiKeySuccess(e) { + this.NotificationService.show('API Key has been copied to clipboard'); + e.clearSelection(); + } + } +}; + +export default ApplicationSubscriptionComponent; diff --git a/src/management/application/details/subscriptions/application-subscription.html b/src/management/application/details/subscriptions/application-subscription.html new file mode 100644 index 0000000000..546b04a30b --- /dev/null +++ b/src/management/application/details/subscriptions/application-subscription.html @@ -0,0 +1,117 @@ + +
+ Subscriptions + +
+ + Subscription + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ID{{$ctrl.subscription.id | date:'MMM d, y h:mm:ss.sss a'}}
Date{{$ctrl.subscription.created_at | date:'MMM d, y h:mm:ss.sss a'}}
API{{$ctrl.subscription.api.name}} ({{$ctrl.subscription.api.owner.username}})
Plan{{$ctrl.subscription.plan.name}} ({{$ctrl.subscription.plan.security}})
Status{{$ctrl.subscription.status}}
Rejection reason{{$ctrl.subscription.reason}}
Processed at{{$ctrl.subscription.processed_at || '-' | date:'MMM d, y h:mm:ss.sss a'}}
Starting at{{$ctrl.subscription.starting_at || '-' | date:'MMM d, y h:mm:ss.sss a'}}
Ending at{{$ctrl.subscription.ending_at || '-' | date:'MMM d, y h:mm:ss.sss a'}}
+
+ +
+
+ Api Keys + + + + + + + + + + + + + + + + + + + +
KeyCreated atRevoked / Expire at
+ + {{key.key}} + + Copy to clipboard + + + {{key.created_at | date:'yyyy-MM-dd HH:mm:ss'}}{{key.revoked_at || key.expire_at | date:'yyyy-MM-dd HH:mm:ss'}} + + Revoke + + +
+
+ +
+ + + Renew API Key + +
+
+
+
+
diff --git a/src/management/application/details/subscriptions/application-subscriptions.controller.ts b/src/management/application/details/subscriptions/application-subscriptions.controller.ts index c73564c394..417ce20e8d 100644 --- a/src/management/application/details/subscriptions/application-subscriptions.controller.ts +++ b/src/management/application/details/subscriptions/application-subscriptions.controller.ts @@ -19,16 +19,30 @@ import * as angular from 'angular'; import ApplicationService from '../../../../services/applications.service'; import NotificationService from '../../../../services/notification.service'; import ApiService from '../../../../services/api.service'; +import { PagedResult } from "../../../../entities/pagedResult"; + +export class SubscriptionQuery { + status?: string[] = ['ACCEPTED']; + apis?: string[]; + page?: number = 1; + size?: number = 20; +} class ApplicationSubscriptionsController { + + private subscriptions: PagedResult; private application: any; - private subscriptions: any; - private statusFilters: string[]; - private selectedStatus: string[]; -// private apiNameById: any; - private subscriptionsByApi: any; - private showRevokedKeys: boolean; - private data: any[]; + + private query: SubscriptionQuery = new SubscriptionQuery(); + + private status = { + 'ACCEPTED': 'Accepted', + 'CLOSED': 'Closed', + 'PENDING': 'Pending', + 'REJECTED': 'Rejected' + }; + + private subscriptionsFiltersForm: any; constructor( private ApplicationService: ApplicationService, @@ -38,31 +52,50 @@ class ApplicationSubscriptionsController { ) { 'ngInject'; - this.showRevokedKeys = false; - this.data = []; - this.statusFilters = ['accepted', 'pending', 'rejected', 'closed']; - this.selectedStatus = ['accepted', 'pending']; -// this.apiNameById = {}; + this.onPaginate = this.onPaginate.bind(this); + } + + onPaginate(page) { + this.query.page = page; + this.doSearch(); } - $onInit() { - this.applyFilters(); + clearFilters() { + this.subscriptionsFiltersForm.$setPristine(); + this.query = new SubscriptionQuery(); + this.doSearch(); } - changeFilter(statusFilter) { - if (_.includes(this.selectedStatus, statusFilter)) { - _.pull(this.selectedStatus, statusFilter); - } else { - this.selectedStatus.push(statusFilter); + search() { + this.query.page = 1; + this.query.size = 20; + this.doSearch(); + } + + buildQuery() { + let query = '?page=' + this.query.page + '&size=' + this.query.size + '&'; + let parameters = {}; + + if (this.query.status !== undefined) { + parameters['status'] = this.query.status.join(','); + } + + if (this.query.apis !== undefined) { + parameters['api'] = this.query.apis.join(','); } - this.applyFilters(); + + _.mapKeys(parameters, function( value, key ) { + query += key + '=' + value + '&'; + }); + + return query; } - applyFilters() { - let that = this; - this.subscriptionsByApi = _.groupBy(_.filter(that.subscriptions, (subscription: any) => _.includes(that.selectedStatus, subscription.status)), function (sub) { -// that.apiNameById[sub.plan.apis[0].id] = sub.plan.apis[0].name; - return sub.plan.apis[0].id; + doSearch() { + let query = this.buildQuery(); + + this.ApplicationService.listSubscriptions(this.application.id, query).then((response) => { + this.subscriptions = response.data as PagedResult; }); } diff --git a/src/management/application/details/subscriptions/application-subscriptions.html b/src/management/application/details/subscriptions/application-subscriptions.html index 38a2b27614..21f73b4e83 100644 --- a/src/management/application/details/subscriptions/application-subscriptions.html +++ b/src/management/application/details/subscriptions/application-subscriptions.html @@ -15,99 +15,94 @@ limitations under the License. --> -
+
Subscriptions - - - - -
-
-

- {{subscriptions[0].plan.apis[0].name}} - ( {{subscriptions[0].plan.apis[0].version}} ) -

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
PlanSubscription DateProcess DateStart DateEnd DateStatus - - Show revoked - -
- - - - {{subscription.plan.name}} - {{subscription.created_at | date:'yyyy-MM-dd HH:mm:ss'}}{{subscription.processed_at | date:'yyyy-MM-dd HH:mm:ss'}}{{subscription.starting_at || '-' | date:'yyyy-MM-dd HH:mm:ss'}}{{subscription.ending_at || '-' | date:'yyyy-MM-dd HH:mm:ss'}} - {{subscription.reason}} - {{subscription.status}} - -
- Renew your API Key - + +
+ + + Filters + + + +
+
+ + + + {{ subscriber.name }} () + + + + + + {{ value }} + + +
+ + Clear + + + Search +
-
- - {{apiKey.key}} - - Copy to clipboard - - - {{apiKey.revoked_at || apiKey.expire_at | date:'yyyy-MM-dd HH:mm:ss'}} - - Revoke this API Key - - - - Set expiration for this API Key - - -
+
+ +
-
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
APIPlanCreated atProcessed atStart atEnd atStatus
+ {{$ctrl.subscriptions.metadata[subscription.api].name}} + {{$ctrl.subscriptions.metadata[subscription.plan].name}}{{subscription.created_at | date:'yyyy-MM-dd HH:mm:ss'}}{{subscription.processed_at | date:'yyyy-MM-dd HH:mm:ss'}}{{subscription.starting_at || '-' | date:'yyyy-MM-dd HH:mm:ss'}}{{subscription.ending_at || '-' | date:'yyyy-MM-dd HH:mm:ss'}} + {{subscription.reason}} + {{subscription.status}} +
+
+ + +
- diff --git a/src/management/application/dialog/application.dialog.html b/src/management/application/dialog/application.dialog.html deleted file mode 100644 index e37aed14df..0000000000 --- a/src/management/application/dialog/application.dialog.html +++ /dev/null @@ -1,51 +0,0 @@ - - -
- - -

New application

-
- - - - - - - - -
-
- - - - -
- -
-
- - - Cancel - - - Create - - -
-
diff --git a/src/management/management.module.ts b/src/management/management.module.ts index f61eaee081..5d69dd308c 100644 --- a/src/management/management.module.ts +++ b/src/management/management.module.ts @@ -79,11 +79,11 @@ require('../libraries/angular-schema-form/codemirror-decorator'); require('../libraries/angular-ui-codemirror/ui-codemirror'); require('../libraries/angular-swagger-ui/swagger-ui.min'); --require('../libraries/angular-swagger-ui/modules/swagger-markdown.min.js'); --require('../libraries/angular-swagger-ui/modules/swagger-auth.min.js'); --require('../libraries/angular-swagger-ui/modules/swagger-yaml-parser.min.js'); --require('../libraries/angular-swagger-ui/modules/swagger-xml-formatter.min.js'); --require('../libraries/angular-swagger-ui/modules/swagger1-to-swagger2-converter.min.js'); +require('../libraries/angular-swagger-ui/modules/swagger-markdown.min.js'); +require('../libraries/angular-swagger-ui/modules/swagger-auth.min.js'); +require('../libraries/angular-swagger-ui/modules/swagger-yaml-parser.min.js'); +require('../libraries/angular-swagger-ui/modules/swagger-xml-formatter.min.js'); +require('../libraries/angular-swagger-ui/modules/swagger1-to-swagger2-converter.min.js'); require('ngclipboard'); require('angular-ui-validate'); @@ -96,6 +96,7 @@ require('angular-ui-tree'); require('angular-jwt'); require('ng-showdown'); require('showdown-prettify'); + require('angular-gridster'); require('angular-scroll'); require('diff/dist/diff.min.js'); @@ -133,7 +134,6 @@ import ApiResourcesController from '../management/api/resources/resources.contro import NewApiController from '../management/api/creation/newApi.controller'; import DialogApiPermissionsHelpController from '../management/api/members/api-permissions-dialog.controller'; import ApiPropertiesController from '../management/api/properties/properties.controller'; -import SubscriptionsController from '../management/api/subscriptions/subscriptions.controller'; import ApiEventsController from '../management/api/events/apiEvents.controller'; import ApiHistoryController from '../management/api/history/apiHistory.controller'; import DialogAddPropertyController from '../management/api/properties/add-property.dialog.controller'; @@ -169,13 +169,27 @@ import ApiCreationStep2Component from '../management/api/creation/steps/api-crea import ApiCreationStep3Component from '../management/api/creation/steps/api-creation-step3.component'; import ApiCreationStep4Component from '../management/api/creation/steps/api-creation-step4.component'; import ApiCreationStep5Component from '../management/api/creation/steps/api-creation-step5.component'; -import ApiPlanComponent from '../management/api/api-plan.component'; +// API Plan +import ApiPlanComponent from '../management/api/api-plan.component'; +import ApiEditPlanController from '../management/api/plans/plan/edit-plan.controller'; +import ApiEditPlanComponent from '../management/api/plans/plan/edit-plan.component'; +import ApiListPlansComponent from '../management/api/plans/list-plans.component'; +import ApiListPlansController from '../management/api/plans/list-plans.controller'; +import ApiEditPlanWizardGeneralComponent from '../management/api/plans/plan/plan-wizard-general.component'; +import ApiEditPlanWizardSecurityComponent from '../management/api/plans/plan/plan-wizard-security.component'; +import ApiEditPlanWizardPoliciesComponent from '../management/api/plans/plan/plan-wizard-policies.component'; + +// API Subscription +import ApiSubscriptionsComponent from '../management/api/subscriptions/subscriptions.component'; +import ApiPlanSubscriptionsComponent from '../management/api/subscriptions/plan-subscriptions.component'; +import ApiSubscriptionComponent from '../management/api/subscriptions/subscription.component'; // Applications import ApplicationService from '../services/applications.service'; import ApplicationsComponent from './application/applications.component'; import ApplicationsController from './application/applications.controller'; +import CreateApplicationsComponent from './application/create-application.component'; import ApplicationComponent from './application/details/application.component'; import ApplicationHeaderComponent from './application/details/header/application-header.component'; import ApplicationGeneralController from './application/details/general/application-general.controller'; @@ -184,12 +198,12 @@ import ApplicationMembersController from './application/details/members/applicat import ApplicationMembersComponent from './application/details/members/application-members.component'; import ApplicationSubscriptionsController from './application/details/subscriptions/application-subscriptions.controller'; import ApplicationSubscriptionsComponent from './application/details/subscriptions/application-subscriptions.component'; +import ApplicationSubscriptionComponent from './application/details/subscriptions/application-subscription.component'; import ApplicationAnalyticsController from './application/details/analytics/application-analytics.controller'; import ApplicationAnalyticsComponent from './application/details/analytics/application-analytics.component'; import ApplicationLogsController from './application/details/logs/application-logs.controller'; import ApplicationLogsComponent from './application/details/logs/application-logs.component'; import ApplicationLogComponent from './application/details/logs/application-log.component'; -import DialogApplicationController from './application/dialog/applicationDialog.controller'; import DialogAddMemberController from './application/details/members/addMemberDialog.controller'; import DialogApplicationPermissionsHelpController from './application/details/members/application-permissions-dialog.controller'; import DialogTransferApplicationController from './application/details/members/transferApplicationDialog.controller'; @@ -242,7 +256,6 @@ import DialogAddGroupMemberController from '../management/configuration/groups/d import RegistrationController from '../user/registration/registration.controller'; import ConfirmController from '../user/registration/confirm/confirm.controller'; import SubscriptionService from '../services/subscription.service'; -import ApiPlansController from '../management/api/plans/apiPlans.controller'; import DialogSubscriptionRejectController from '../management/api/subscriptions/subscription.reject.dialog.controller'; import DialogSubscriptionAcceptController from '../management/api/subscriptions/subscription.accept.dialog.controller'; import DialogSubscriptionCreateController from '../management/api/subscriptions/subscription.create.dialog.controller'; @@ -401,7 +414,6 @@ angular.module('gravitee-management', [uiRouter, permission, uiPermission, 'ngMa .controller('DialogAssertionInformationController', DialogAssertionInformationController) .controller('DialogApiPermissionsHelpController', DialogApiPermissionsHelpController) .controller('ApiPropertiesController', ApiPropertiesController) - .controller('SubscriptionsController', SubscriptionsController) .controller('ApiEventsController', ApiEventsController) .controller('ApiHistoryController', ApiHistoryController) .controller('ApiResourcesController', ApiResourcesController) @@ -430,7 +442,6 @@ angular.module('gravitee-management', [uiRouter, permission, uiPermission, 'ngMa .controller('DialogAddGroupMemberController', DialogAddGroupMemberController) .controller('RegistrationController', RegistrationController) .controller('ConfirmController', ConfirmController) - .controller('ApiPlansController', ApiPlansController) .controller('DialogSubscriptionRejectController', DialogSubscriptionRejectController) .controller('DialogSubscriptionAcceptController', DialogSubscriptionAcceptController) .controller('DialogSubscriptionCreateController', DialogSubscriptionCreateController) @@ -519,7 +530,6 @@ angular.module('gravitee-management', [uiRouter, permission, uiPermission, 'ngMa .component('apiCreationStep3', ApiCreationStep3Component) .component('apiCreationStep4', ApiCreationStep4Component) .component('apiCreationStep5', ApiCreationStep5Component) - .component('apiPlan', ApiPlanComponent) .component('apiMetadata', ApiMetadataComponent) .component('gvDashboard', DashboardComponent) .component('gvDashboardFilter', DashboardFilterComponent) @@ -527,16 +537,32 @@ angular.module('gravitee-management', [uiRouter, permission, uiPermission, 'ngMa .component('gvDashboardTimeframe', DashboardTimeframeComponent) .controller('DashboardTimeframeController', DashboardTimeframeController) + // Plan + .component('apiPlan', ApiPlanComponent) + .component('editPlan', ApiEditPlanComponent) + .controller('ApiEditPlanController', ApiEditPlanController) + .component('listPlans', ApiListPlansComponent) + .controller('ApiListPlansController', ApiListPlansController) + .component('planWizardGeneral', ApiEditPlanWizardGeneralComponent) + .component('planWizardSecurity', ApiEditPlanWizardSecurityComponent) + .component('planWizardPolicies', ApiEditPlanWizardPoliciesComponent) + + // API subscriptions + .component('apiSubscriptions', ApiSubscriptionsComponent) + .component('planSubscriptions', ApiPlanSubscriptionsComponent) + .component('apiSubscription', ApiSubscriptionComponent) + .component('applications', ApplicationsComponent) .component('application', ApplicationComponent) + .component('createApplication', CreateApplicationsComponent) .component('applicationHeader', ApplicationHeaderComponent) .component('applicationGeneral', ApplicationGeneralComponent) .component('applicationSubscriptions', ApplicationSubscriptionsComponent) + .component('applicationSubscription', ApplicationSubscriptionComponent) .component('applicationMembers', ApplicationMembersComponent) .component('applicationAnalytics', ApplicationAnalyticsComponent) .component('applicationLogs', ApplicationLogsComponent) .component('applicationLog', ApplicationLogComponent) - .controller('DialogApplicationController', DialogApplicationController) .controller('DialogAddMemberController', DialogAddMemberController) .controller('DialogApplicationPermissionsHelpController', DialogApplicationPermissionsHelpController) .controller('ApplicationsController', ApplicationsController) @@ -546,6 +572,7 @@ angular.module('gravitee-management', [uiRouter, permission, uiPermission, 'ngMa .controller('ApplicationAnalyticsController', ApplicationAnalyticsController) .controller('ApplicationLogsController', ApplicationLogsController) .controller('DialogTransferApplicationController', DialogTransferApplicationController) + .component('user', UserComponent) .component('tasks', TasksComponent) diff --git a/src/management/tasks/tasks.component.ts b/src/management/tasks/tasks.component.ts index 9556bdd604..d41353032a 100644 --- a/src/management/tasks/tasks.component.ts +++ b/src/management/tasks/tasks.component.ts @@ -46,10 +46,13 @@ const TasksComponent: ng.IComponentOptions = { }; vm.go = function(task) { - const apiId = vm.tasks.metadata[task.data.plan].api; - $state.go("management.apis.detail.subscriptions", {apiId: apiId}); + $state.go("management.apis.detail.subscriptions.subscription", + { + apiId: task.data.api, + subscriptionId: task.data.id + }); } } }; -export default TasksComponent; \ No newline at end of file +export default TasksComponent; diff --git a/src/portal/api/_api.scss b/src/portal/api/_api.scss index 09038d88f7..e5e5ddc2d9 100644 --- a/src/portal/api/_api.scss +++ b/src/portal/api/_api.scss @@ -195,3 +195,185 @@ a.ui.label:focus, a.ui.label:hover { text-decoration: none; cursor: pointer; } + +/** + * API navigation + */ +.api-nav { + background: #2c3943; +} + +.api-nav ul li:first-child { + margin-left: 0; +} + +.api-nav ul li { + font-size: .75em; + float: left; + margin-left: 20px; + box-sizing: border-box; + + @media (min-width: 768px) { + font-size: 1em; + margin-left: 60px; + } +} + +.api-nav ul li a { + border-bottom: 4px solid transparent; + color: #fafafa; + display: inline-block; + font-size: 1.2em; + font-weight: 600; + line-height: 3; + box-sizing: border-box; +} + +.api-nav ul li.active a { + border-color: #63d0e6; + box-sizing: border-box; +} + +.api-nav ul li.subscribe { + float: right; +} + +.api-nav .container { + padding-top: 0; + padding-bottom: 0; +} + +.api-nav ul li.subscribe button.oui-button.thin-button { + margin: 10px 0 8px; + line-height: 28px; + padding: 2px 2ex; + background-color: transparent; + color: #fafafa; + font-size: 1.2em; + font-weight: 600; + border-radius: 4px; + border: 2px solid #63d0e6; + + @media (min-width: 768px) { + margin: 6px 0; + line-height: 24px; + padding: 1px 2ex; + } +} + +/** + * subscriptions + */ + +/*! + * SmartWizard v4.x + * jQuery Wizard Plugin + * http://www.techlaboratory.net/smartwizard + * + * Created by Dipu Raj + * http://dipuraj.me + * + * Licensed under the terms of MIT License + * https://github.com/techlab/SmartWizard/blob/master/LICENSE + */ + +/* SmartWizard Theme: Circles */ +.sw-theme-circles{ + +} +.sw-theme-circles .sw-container { + min-height: 300px; +} +.sw-theme-circles .step-content { + padding: 10px 0; + background-color: #FFF; + text-align: left; +} +.sw-theme-circles .sw-toolbar{ + background: #fff; + padding-left: 10px; + padding-right: 10px; + margin-bottom: 0 !important; +} +.sw-theme-circles .sw-toolbar-top{ + +} +.sw-theme-circles .sw-toolbar-bottom{ + border-top-color: #ddd !important; + border-bottom-color: #ddd !important; +} +.sw-theme-circles > ul.step-anchor{ + position: relative; + background: #fff; + display: block; + border: none; + list-style: none; + top: 50%; + transform: translate(0, -50%); +} + +.sw-theme-circles > ul.step-anchor > li{ + border: none; +} +.sw-theme-circles > ul.step-anchor > li > a{ + border: 2px solid #f5f5f5; + background: #f5f5f5; + width: 75px; + height: 75px; + text-align: center; + padding: 14px 0; + font-size: 28px; + border-radius: 50%; + box-shadow: inset 0px 0px 0px 3px #fff !important; + text-decoration: none; + outline-style:none; + z-index: 99; + color: #bbb; +} +.sw-theme-circles > ul.step-anchor > li > a:hover { + color: #bbb; + background: #f5f5f5; + border-width: 2px; +} +.sw-theme-circles > ul.step-anchor > li > a > small{ + position: absolute; + bottom: -40px; + display: block; + color: #ccc; +} +.sw-theme-circles > ul.step-anchor > li.clickable > a:hover { + color: #4285F4 !important; +} +.sw-theme-circles > ul.step-anchor > li.active > a { + border-color: #5bc0de; + color: #fff; + background: #5bc0de; +} +.sw-theme-circles > ul.step-anchor > li.active > a > small{ + color: #5bc0de; +} +.sw-theme-circles > ul.step-anchor > li.done > a { + border-color: #5cb85c; + color: #fff; + background: #5cb85c; +} +.sw-theme-circles > ul.step-anchor > li.done > a > small{ + color: #5cb85c; +} +.sw-theme-circles > ul.step-anchor > li.danger > a { + border-color: #d9534f; + color: white; + background: #d9534f; +} +.sw-theme-circles > ul.step-anchor > li.danger > a > small{ + color: #d9534f; +} +.sw-theme-circles > ul.step-anchor > li.disabled > a, .sw-theme-circles > ul.step-anchor > li.disabled > a:hover { + color: #eee !important; +} + +.subscription-step { + background-color: #fff; + border: 2px solid #9a9a9a; + border-radius: 4px; +} diff --git a/src/portal/api/header/api-header.component.ts b/src/portal/api/header/api-header.component.ts index 07ded196d0..bdd440b9e1 100644 --- a/src/portal/api/header/api-header.component.ts +++ b/src/portal/api/header/api-header.component.ts @@ -21,7 +21,7 @@ const ApiHeaderComponent: ng.IComponentOptions = { apiRatingSummary: '<' }, template: require('./api-header.html'), - controller: function(Constants, ApiService: ApiService, $stateParams, $rootScope) { + controller: function(Constants, ApiService: ApiService, $state, $stateParams, $rootScope) { 'ngInject'; this.ratingEnabled = ApiService.isRatingEnabled(); @@ -29,6 +29,10 @@ const ApiHeaderComponent: ng.IComponentOptions = { return Constants.portal.entrypoint + this.api.context_path; }; + this.completeHeader = function () { + return $state.is('portal.api.detail'); + }; + $rootScope.$on('onRatingSave', () => { ApiService.getApiRatingSummaryByApi($stateParams.apiId).then((response) => { this.apiRatingSummary = response.data; diff --git a/src/portal/api/header/api-header.html b/src/portal/api/header/api-header.html index c71960fcdd..3b275767b9 100644 --- a/src/portal/api/header/api-header.html +++ b/src/portal/api/header/api-header.html @@ -35,7 +35,7 @@
-
+
{{::$ctrl.api.description}}
@@ -47,13 +47,23 @@ {{'api.publishedAt' | translate}}: {{::$ctrl.api.deployed_at | humanDateFilter}}
-
- {{'api.documentation.title' | translate}} - {{'api.plan.subscribe' | translate}} -
-
+ +
+
+ +
+
diff --git a/src/portal/api/subscribe/api-subscribe.component.ts b/src/portal/api/subscribe/api-subscribe.component.ts index 9a0e7a9aa3..740629ea01 100644 --- a/src/portal/api/subscribe/api-subscribe.component.ts +++ b/src/portal/api/subscribe/api-subscribe.component.ts @@ -13,17 +13,147 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import ApiSubscribeController from './api-subscribe'; +import ApplicationService from "../../../services/applications.service"; +import ApiService from "../../../services/api.service"; +import NotificationService from "../../../services/notification.service"; +import * as _ from "lodash"; const ApiSubscribeComponent: ng.IComponentOptions = { bindings: { api: '<', - plan: '<', + plans: '<', applications: '<', subscriptions: '<' }, template: require('./api-subscribe.html'), - controller: ApiSubscribeController + controller: class { + + private api: any; + private plans: any; + + private selectedPlan: any; + private selectedApp: any; + + private planInformation: any; + private apiKey: any; + private subscription: any; + + constructor( + private $stateParams: ng.ui.IStateParamsService, + private NotificationService: NotificationService, + private ApplicationService: ApplicationService, + private ApiService: ApiService, + private Constants, + private $translate) { + 'ngInject'; + } + + $onInit() { + this.selectedPlan = _.find(this.plans, (plan: any) => { + return plan.id === this.$stateParams.planId; + }); + + /* + if (this.selectedPlan.paths['/'] && this.selectedPlan.paths['/'].length) { + _.forEach(this.selectedPlan.paths['/'], (path) => { + if (path.quota) { + let quota = path.quota.quota; + this.$translate('common.' + quota.periodTimeUnit).then( (quotaPeriodTimeUnitTranslated) => { + this.$translate('api.subscription.planInformation', + { + quotaLimit: quota.limit, + quotaPeriodTime: quota.periodTime, + quotaPeriodTimeUnit: quotaPeriodTimeUnitTranslated + }) + .then(function (translatedMessage) { + this.planInformation = translatedMessage; + }); + }); + } + }); + } + */ + } + + checkSubscriptions() { + if (this.selectedApp) { + this.ApplicationService.listSubscriptions(this.selectedApp.id, '?status=accepted,pending&plan=' + this.selectedPlan.id) + .then((subscriptions: any) => { + this.subscription = subscriptions.data.data[0]; + if (this.subscription !== undefined) { + this.fetchApiKey(this.subscription.id); + } + }); + } + } + + fetchApiKey(subscriptionId) { + this.ApplicationService.listApiKeys(this.selectedApp.id, subscriptionId).then( (apiKeys) => { + let apiKey = _.find(apiKeys.data, function (apiKey: any) { + return !apiKey.revoked; + }); + if (apiKey) { + this.apiKey = apiKey.key; + } + }); + } + + subscribe(application: any) { + this.ApplicationService.subscribe(application.id, this.selectedPlan.id).then( (subscription) => { + this.NotificationService.show('api.subscription.step3.successful', false, {planName: this.selectedPlan.name}); + + this.ApiService.getPlanSubscriptions(this.api.id, this.selectedPlan.id).then( () => { + this.subscription = subscription.data; + this.fetchApiKey(subscription.data.id); + }); + }); + } + + isPlanSubscribable() { + return this.selectedPlan && 'key_less' !== this.selectedPlan.security; + } + + canApplicationSubscribe() { + return this.isPlanSubscribable() + && this.selectedApp + && ! this.subscription + && ( + (('oauth2' === this.selectedPlan.security || 'jwt' === this.selectedPlan.security) && this.selectedApp.clientId) + || (this.selectedPlan.security === 'api_key') + ); + } + + onApiKeyClipboardSuccess(e) { + this.NotificationService.show('api.subscription.step3.apikey.clipboard'); + e.clearSelection(); + } + + onApplicationSearchChange() { + delete this.apiKey; + delete this.selectedApp; + delete this.subscription; + this.checkSubscriptions(); + } + + onApplicationSelect() { + this.checkSubscriptions(); + } + + onPlanSelect() { + delete this.selectedApp; + delete this.subscription; + } + + getApiKeyCurlSample() { + return 'curl -X GET "' + this.Constants.portal.entrypoint + this.api.context_path + + '" -H "' + this.Constants.portal.apikeyHeader + ': ' + (this.apiKey ? this.apiKey : 'given_api_key') + '"'; + } + + getOAuth2CurlSample() { + return 'curl -X GET "' + this.Constants.portal.entrypoint + this.api.context_path + + '" -H "Authorization: Bearer xxxx-xxxx-xxxx-xxxx"'; + } + } }; export default ApiSubscribeComponent; diff --git a/src/portal/api/subscribe/api-subscribe.html b/src/portal/api/subscribe/api-subscribe.html index a6174e6b6d..277e150385 100644 --- a/src/portal/api/subscribe/api-subscribe.html +++ b/src/portal/api/subscribe/api-subscribe.html @@ -15,96 +15,206 @@ limitations under the License. --> -
-
- - - - {{::$ctrl.plan.name}} - {{::$ctrl.plan.description}} - - - -
-
{{::characteristic}}
- +
+ +
+
+
+ +
+ +
+

+
+ + + + +
+

{{plan.name}} {{plan.security}}

+

{{plan.description}}

+
+
+
+ + + +
+ +
- - -
-

-
-
+
+ +
+
+
- - +
+

+
+ +
+ + + +
+

{{application.name}} {{application.security}}

+

{{application.owner.username}}

+
+
+
+ +
+ + + +
+ +
+
+
+ +
+
+ +
+ +
+

+
+
+
+ +
+
+ +
+ +
+

+
+
{{'api.subscription.step3.apikey.key' | translate}}: {{$ctrl.apiKey}}
+ + {{'common.copyToClipboard' | translate}} + + +
+ +
+
-
-
-

{{ application.name }} ({{ application.type }})

-
{{ application.owner.username }}
-
+
+

{{$ctrl.getApiKeyCurlSample()}}

+
+ + {{'common.copyToClipboard' | translate}} + +
- - - - - - -
-
+
+
+ -
-
-
-
+
+
+ +
-
- -
-

-
-
-
+
+

+
+
+
-
- -
-

-
-
{{'api.subscription.sample' | translate}} {{$ctrl.apiKey}}
- - {{'common.copyToClipboard' | translate}} - - -
-
{{$ctrl.getSampleCall()}}
-
-
+
+
+
+
+

{{$ctrl.getOAuth2CurlSample()}}

+
+ + {{'common.copyToClipboard' | translate}} + + +
+
+
+ + +
+
+ +
-
- -
-

-
-
+
+

+
+
+
-
{{$ctrl.getSampleCall()}}
-
+
+
+
+
+

{{$ctrl.getOAuth2CurlSample()}}

+
+ + {{'common.copyToClipboard' | translate}} + + +
+
+
+
diff --git a/src/portal/api/subscribe/api-subscribe.ts b/src/portal/api/subscribe/api-subscribe.ts deleted file mode 100644 index b6801964cc..0000000000 --- a/src/portal/api/subscribe/api-subscribe.ts +++ /dev/null @@ -1,119 +0,0 @@ -/* - * Copyright (C) 2015 The Gravitee team (http://gravitee.io) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import * as _ from 'lodash'; -import ApiService from '../../../services/api.service'; -import ApplicationService from '../../../services/applications.service'; - -function ApiSubscribeController($state, - NotificationService, - ApplicationService: ApplicationService, - ApiService: ApiService, - Constants, - $translate) { - 'ngInject'; - const vm = this; - - this.$onInit = function () { - if (vm.plan.paths['/'] && vm.plan.paths['/'].length) { - _.forEach(vm.plan.paths['/'], function (path) { - if (path.quota) { - let quota = path.quota.quota; - $translate('common.' + quota.periodTimeUnit).then(function (quotaPeriodTimeUnitTranslated) { - $translate('api.subscription.planInformation', - { - quotaLimit: quota.limit, - quotaPeriodTime: quota.periodTime, - quotaPeriodTimeUnit: quotaPeriodTimeUnitTranslated - }) - .then(function (translatedMessage) { - vm.planInformation = translatedMessage; - }); - }); - } - }); - } - }; - - function getSubscription(application) { - if (!application) { - return; - } - const subscriptionsByApplication = _.groupBy(vm.subscriptions, 'application'); - return _.find(subscriptionsByApplication[application.id], function (subscriptionByApplication: any) { - return _.includes(['accepted', 'pending'], subscriptionByApplication.status); - }); - } - - vm.isNotSelectable = function (application) { - if (!application) { - return true; - } - return getSubscription(application); - }; - - function fetchApiKey(application) { - vm.subscription = getSubscription(application); - if (application && vm.subscription) { - ApplicationService.listApiKeys(application.id, vm.subscription.id).then(function (apiKeys) { - let apiKey = _.find(apiKeys.data, function (apiKey: any) { - return !apiKey.revoked; - }); - if (apiKey) { - vm.apiKey = apiKey.key; - } - }); - } - } - - vm.select = function (application) { - ApplicationService.subscribe(application.id, vm.plan.id).then(function () { - NotificationService.show('api.subscription.successful', false, {planName: vm.plan.name}); - - ApiService.getPlanSubscriptions(vm.api.id, vm.plan.id).then(function (subscriptions) { - vm.subscriptions = subscriptions.data; - fetchApiKey(application); - }); - }); - }; - - vm.goToApplications = function () { - $state.go('applications.list', {}, {reload: true}); - }; - - vm.subscribable = function () { - return 'key_less' !== vm.plan.security; - }; - - vm.onClipboardSuccess = function (e) { - NotificationService.show('api.subscription.apiKeyCopySuccess'); - e.clearSelection(); - }; - - vm.onApplicationSearchChange = function () { - delete vm.apiKey; - }; - - vm.onApplicationSelect = function () { - fetchApiKey(vm.selectedApp); - }; - - vm.getSampleCall = function () { - return 'curl -X GET "' + Constants.portal.entrypoint + vm.api.context_path + - '" -H "' + Constants.portal.apikeyHeader + ': ' + (vm.apiKey ? vm.apiKey : 'given_api_key') + '"'; - } -} - -export default ApiSubscribeController; diff --git a/src/portal/home/home.html b/src/portal/home/home.html index f7908ffee4..542faa04db 100644 --- a/src/portal/home/home.html +++ b/src/portal/home/home.html @@ -54,7 +54,7 @@

- diff --git a/src/portal/i18n/en.json b/src/portal/i18n/en.json index dfa107be2a..90a9644e90 100644 --- a/src/portal/i18n/en.json +++ b/src/portal/i18n/en.json @@ -56,17 +56,44 @@ "owner": "Owner", "publishedAt": "Published", "subscription": { - "title": "Select an application to subscribe", - "subtitle": "You are subscribing to plan {{planName}} {{planInformation}}", - "placeholder": "Select an application...", - "action": "Subscribe for the application {{applicationName}}", - "keyless": "This plan is keyless. You can already use it without subscription.", - "success": "Let's start to use the API \"{{apiName}}\" with the application \"{{applicationName}}\"!", - "sample": "You can access the API with the following api-key", - "validationInProgress": "Waiting for validation for the application \"{{applicationName}}\"!", - "sampleNotValidated": "Once validated, you will be able to access your API with the given api-key as below:", - "successful": "Application has successfully subscribed to plan {{planName}}", - "apiKeyCopySuccess": "API Key has been copied to clipboard", + "step1": { + "title": "Select an API Plan", + "subtitle": "The API plan is corresponding to the service contract between you and the API.", + "keyless": "This plan is key-less and does not require subscription." + }, + "step2": { + "title": "Select an application", + "subtitle": "Application is the link between you, the consumer, and the subscription to an API plan.", + "application": { + "placeholder": "Select an application..." + }, + "clientIdRequired": "A clientId is required to subscribe to this plan. Please check your application's configuration.", + "action": "Subscribe" + }, + "step3": { + "apikey": { + "key": "You can access the API using the following API Key:", + "curl": "Request sample using curl:", + "clipboard": "API Key has been copied to the clipboard." + }, + "oauth2": { + "bearer": "You have to get an access_token from the authorization server before being able to consume this API.", + "curl": "Request sample using curl:" + }, + "jwt": { + "bearer": "You have to get an access_token from the authorization server before being able to consume this API.", + "curl": "Request sample using curl:" + }, + "pending": { + "title": "Subscription is waiting for validation", + "subtitle": "Your subscription is still in pending state, you will receive a notification once processed." + }, + "accepted": { + "title": "Let's start to use \"{{apiName}}\" with \"{{applicationName}}\"!" + }, + "successful": "Subscription request has been submitted." + }, + "planInformation": "with a quota of {{quotaLimit}} requests per {{quotaPeriodTime}} {{quotaPeriodTimeUnit}}" }, "plan": { diff --git a/src/portal/i18n/fr.json b/src/portal/i18n/fr.json index ca28ddbdab..e536a44a15 100644 --- a/src/portal/i18n/fr.json +++ b/src/portal/i18n/fr.json @@ -56,17 +56,44 @@ "owner": "Propriétaire", "publishedAt": "Publié", "subscription": { - "title": "Sélectionner une application pour souscrire", - "subtitle": "Vous êtes sur le point de souscrire au plan {{planName}} {{planInformation}}", - "placeholder": "Sélectionner une application...", - "action": "Souscrire pour l'application {{applicationName}}", - "keyless": "Ce plan est sans clé. Vous pouvez déjà l'utiliser sans souscription.", - "success": "Commençons à utiliser l'API \"{{apiName}}\" avec l'application \"{{applicationName}}\"!", - "sample": "Vous pouvez accéder à l'API avec la clé d'API suivante", - "validationInProgress": "En attente de validation pour l'application \"{{applicationName}}\"!", - "sampleNotValidated": "Une fois validé, vous pourrez accéder à votre API avec la clé d'API donnée ci-dessous :", - "successful": "L'application a souscrit au plan {{planName}} avec succés", - "apiKeyCopySuccess": "La clé d'API a bien été copiée dans le presse-papier", + "step1": { + "title": "Sélectionner un plan", + "subtitle": "Le plan d'API correspond au contrat de service entre l'API et vous.", + "keyless": "Ce plan est sans clé et ne nécessite pas de souscription." + }, + "step2": { + "title": "Sélectionner une application", + "subtitle": "L'application est le lien entre vous, en tant que consommateur, et la souscription à un plan d'API.", + "application": { + "placeholder": "Sélectionner une application..." + }, + "clientIdRequired": "Un clientId est nécessaire pour pouvoir souscrire à ce plan. Veuillez configurer votre application.", + "action": "Souscrire" + }, + "step3": { + "apikey": { + "key": "Vous pouvez accéder à l'API avec la clé d'API suivante:", + "curl": "Exemple d'appel avec curl:", + "clipboard": "La clé d'API a bien été copiée dans le presse-papier" + }, + "oauth2": { + "bearer": "Vous devez récupèrer un access_token auprès du serveur d'autorisation avant de pouvoir consommer cette API.", + "curl": "Exemple d'appel avec curl:" + }, + "jwt": { + "bearer": "Vous devez récupèrer un access_token auprès du serveur d'autorisation avant de pouvoir consommer cette API.", + "curl": "Exemple d'appel avec curl:" + }, + "pending": { + "title": "Souscription en attente de validation", + "subtitle": "Votre souscription est en attente de validation, vous serez notifié une fois celle-ci traitée." + }, + "accepted": { + "title": "Commençons à utiliser \"{{apiName}}\" avec \"{{applicationName}}\" !" + }, + "successful": "La demande de souscription a été soumise." + }, + "planInformation": "avec un quota de {{quotaLimit}} requêtes par {{quotaPeriodTime}} {{quotaPeriodTimeUnit}}" }, "plan": { diff --git a/src/portal/index.html b/src/portal/index.html index 9b684ea7b8..150b9dad06 100644 --- a/src/portal/index.html +++ b/src/portal/index.html @@ -16,7 +16,7 @@ --> -
+