blob: 445386ae7c0903119dccf9297d0ed21765d07229 [file] [log] [blame]
Matteo Scandolofb46ae62017-08-08 09:10:50 -07001
2/*
3 * Copyright 2017-present Open Networking Foundation
4
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8
9 * http://www.apache.org/licenses/LICENSE-2.0
10
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18
Matteo Scandolod58d5042016-12-16 16:59:21 -080019import * as _ from 'lodash';
20import * as pluralize from 'pluralize';
Matteo Scandolocb466ed2017-01-04 17:16:24 -080021import {IXosTableColumn, IXosTableCfg} from '../../table/table';
Matteo Scandolo1aee1982017-02-17 08:33:23 -080022import {IXosModeldef} from '../../../datasources/rest/modeldefs.rest';
Matteo Scandoloe7e052d2017-07-31 19:54:31 -070023import {IXosFormCfg, IXosFormInput, IXosFormInputValidator, IXosFormInputOptions} from '../../form/form';
Matteo Scandolo47860fe2017-02-02 12:05:55 -080024import {IXosModelStoreService} from '../../../datasources/stores/model.store';
Matteo Scandolo1aee1982017-02-17 08:33:23 -080025import {IXosState} from '../runtime-states';
26
27export interface IXosModelDefsFieldValidators {
28 name: string;
29 bool_value?: boolean;
30 int_value?: number;
31}
Matteo Scandolod58d5042016-12-16 16:59:21 -080032
33export interface IXosModelDefsField {
34 name: string;
35 type: string;
Matteo Scandolo1aee1982017-02-17 08:33:23 -080036 validators?: IXosModelDefsFieldValidators[];
Matteo Scandolo04964232017-01-07 12:53:46 -080037 hint?: string;
38 relation?: {
39 model: string;
40 type: string;
41 };
Matteo Scandoloe7e052d2017-07-31 19:54:31 -070042 options?: IXosFormInputOptions[];
43 default?: any | null;
Matteo Scandolod58d5042016-12-16 16:59:21 -080044}
45
46export interface IXosConfigHelpersService {
Matteo Scandoloee655a12016-12-19 15:38:43 -080047 excluded_fields: string[];
Matteo Scandoloc8a58c82017-08-17 17:14:38 -070048 form_excluded_fields: string[];
Matteo Scandolo1aee1982017-02-17 08:33:23 -080049 modelFieldsToColumnsCfg(model: IXosModeldef): IXosTableColumn[];
50 modelToTableCfg(model: IXosModeldef, modelName: string): IXosTableCfg;
Matteo Scandolo80c3a652017-01-06 10:48:31 -080051 modelFieldToInputCfg(fields: IXosModelDefsField[]): IXosFormInput[];
Matteo Scandolo1aee1982017-02-17 08:33:23 -080052 modelToFormCfg(model: IXosModeldef): IXosFormCfg;
Matteo Scandolod58d5042016-12-16 16:59:21 -080053 pluralize(string: string, quantity?: number, count?: boolean): string;
54 toLabel(string: string, pluralize?: boolean): string;
Matteo Scandoloe0d71ea2016-12-19 11:56:12 -080055 toLabels(string: string[], pluralize?: boolean): string[];
Matteo Scandoloa242c872017-01-12 15:13:00 -080056 stateFromCoreModel(name: string): string;
57 stateWithParams(name: string, model: any): string;
Matteo Scandolo8248bca2017-08-09 13:46:04 -070058 relatedStateWithParams(name: string, id: string): string;
Matteo Scandolo86bc26a2017-01-18 11:06:47 -080059 stateWithParamsForJs(name: string, model: any): any;
Matteo Scandolod58d5042016-12-16 16:59:21 -080060}
61
Matteo Scandolo8b2370c2017-02-02 17:19:07 -080062export class ConfigHelpers implements IXosConfigHelpersService {
Matteo Scandolo1aee1982017-02-17 08:33:23 -080063 static $inject = [
64 '$state',
65 'toastr',
Matteo Scandolo1aee1982017-02-17 08:33:23 -080066 'XosModelStore'];
Matteo Scandolod58d5042016-12-16 16:59:21 -080067
Matteo Scandolo1aee1982017-02-17 08:33:23 -080068 public excluded_fields = [
Matteo Scandoloee655a12016-12-19 15:38:43 -080069 'created',
70 'updated',
71 'enacted',
72 'policed',
73 'backend_register',
74 'deleted',
75 'write_protect',
76 'lazy_blocked',
77 'no_sync',
78 'no_policy',
79 'omf_friendly',
80 'enabled',
Matteo Scandolod62ea792016-12-22 14:02:28 -080081 'validators',
Matteo Scandolo80c3a652017-01-06 10:48:31 -080082 'password',
83 'backend_need_delete',
Scott Bakere5dcb9a2017-09-07 10:18:06 -070084 'backend_need_delete_policy',
Matteo Scandolod53ac1d2017-08-01 15:06:09 -070085 'backend_need_reap',
Scott Bakere5dcb9a2017-09-07 10:18:06 -070086 'leaf_model_name',
87 'link_deleted_count',
Matteo Scandolo8cd21b02017-09-20 10:13:13 +090088 'policy_code',
89 'backend_code',
Matteo Scandoloee655a12016-12-19 15:38:43 -080090 ];
91
Matteo Scandolod53ac1d2017-08-01 15:06:09 -070092 public form_excluded_fields = this.excluded_fields.concat([
93 'id',
94 'policy_status',
95 'backend_status',
96 ]);
97
Matteo Scandolocb466ed2017-01-04 17:16:24 -080098 constructor(
Matteo Scandoloa242c872017-01-12 15:13:00 -080099 private $state: ng.ui.IStateService,
Matteo Scandolo0a8b02e2017-01-06 14:43:36 -0800100 private toastr: ng.toastr.IToastrService,
Matteo Scandolo1aee1982017-02-17 08:33:23 -0800101 private XosModelStore: IXosModelStoreService
Matteo Scandolocb466ed2017-01-04 17:16:24 -0800102 ) {
Matteo Scandolo08464e52017-01-17 13:35:27 -0800103 pluralize.addIrregularRule('xos', 'xoses');
Matteo Scandolod58d5042016-12-16 16:59:21 -0800104 pluralize.addPluralRule(/slice$/i, 'slices');
Matteo Scandolo80c3a652017-01-06 10:48:31 -0800105 pluralize.addSingularRule(/slice$/i, 'slice');
Matteo Scandolo1aee1982017-02-17 08:33:23 -0800106 pluralize.addPluralRule(/library$/i, 'librarys');
Matteo Scandolo5d962a32017-08-01 18:16:14 -0700107 pluralize.addPluralRule(/imagedeployments/i, 'imagedeploymentss');
108 pluralize.addPluralRule(/controllerimages/i, 'controllerimagess');
109 pluralize.addPluralRule(/servicedependency/i, 'servicedependencys');
110 pluralize.addPluralRule(/servicemonitoringagentinfo/i, 'servicemonitoringagentinfoes');
Matteo Scandolod58d5042016-12-16 16:59:21 -0800111 }
112
Matteo Scandolo1c5905f2017-01-04 17:41:15 -0800113 public pluralize(string: string, quantity?: number, count?: boolean): string {
Matteo Scandolod58d5042016-12-16 16:59:21 -0800114 return pluralize(string, quantity, count);
115 }
116
Matteo Scandolo1c5905f2017-01-04 17:41:15 -0800117 public toLabels(strings: string[], pluralize?: boolean): string[] {
Matteo Scandoloe0d71ea2016-12-19 11:56:12 -0800118 if (angular.isArray(strings)) {
119 return _.map(strings, s => {
Matteo Scandolod58d5042016-12-16 16:59:21 -0800120 return this.toLabel(s, pluralize);
121 });
122 }
Matteo Scandoloe0d71ea2016-12-19 11:56:12 -0800123 }
124
Matteo Scandolo1c5905f2017-01-04 17:41:15 -0800125 public toLabel(string: string, pluralize?: boolean): string {
Matteo Scandolod58d5042016-12-16 16:59:21 -0800126
127 if (pluralize) {
128 string = this.pluralize(string);
129 }
130
131 string = this.fromCamelCase(string);
132 string = this.fromSnakeCase(string);
133 string = this.fromKebabCase(string);
134
135 return this.capitalizeFirst(string);
136 }
137
Matteo Scandolo1aee1982017-02-17 08:33:23 -0800138 public modelToTableCfg(model: IXosModeldef, baseUrl: string): IXosTableCfg {
Matteo Scandolocb466ed2017-01-04 17:16:24 -0800139 const cfg = {
Matteo Scandolo1aee1982017-02-17 08:33:23 -0800140 columns: this.modelFieldsToColumnsCfg(model),
Matteo Scandolocb466ed2017-01-04 17:16:24 -0800141 filter: 'fulltext',
142 order: {field: 'id', reverse: false},
Matteo Scandolo8b2370c2017-02-02 17:19:07 -0800143 pagination: {
144 pageSize: 10
145 },
Matteo Scandolocb466ed2017-01-04 17:16:24 -0800146 actions: [
147 {
Matteo Scandolocc4bce82017-08-07 13:11:47 -0700148 label: 'details',
149 icon: 'search',
150 cb: (item) => {
151 this.$state.go(this.$state.current.name, {id: item.id});
152 }
153 },
154 {
Matteo Scandolocb466ed2017-01-04 17:16:24 -0800155 label: 'delete',
156 icon: 'remove',
157 color: 'red',
158 cb: (item) => {
159 let obj = angular.copy(item);
Max Chubab0a582017-09-06 08:32:34 -0700160 const objName = (angular.isUndefined(obj.name)) ? 'instance' : obj.name;
Matteo Scandolocb466ed2017-01-04 17:16:24 -0800161
162 item.$delete()
163 .then((res) => {
164 if (res.status === 404) {
165 // TODO understand why it does not go directly in catch
166 throw new Error();
167 }
Max Chubab0a582017-09-06 08:32:34 -0700168 this.toastr.info(`${model.name} ${objName} successfully deleted`);
Matteo Scandolocb466ed2017-01-04 17:16:24 -0800169 })
170 .catch(() => {
Max Chubab0a582017-09-06 08:32:34 -0700171 this.toastr.error(`Error while deleting ${objName}`);
Matteo Scandolocb466ed2017-01-04 17:16:24 -0800172 });
173 }
174 }
175 ]
176 };
177 return cfg;
178 }
179
Matteo Scandolo1aee1982017-02-17 08:33:23 -0800180 public modelFieldsToColumnsCfg(model: IXosModeldef): IXosTableColumn[] {
181 const fields: IXosModelDefsField[] = model.fields;
182 const modelName: string = model.name;
Matteo Scandolo231de262017-01-04 16:33:14 -0800183 const columns = _.map(fields, (f) => {
Matteo Scandolo1aee1982017-02-17 08:33:23 -0800184 if (!angular.isDefined(f) || this.excluded_fields.indexOf(f.name) > -1) {
Matteo Scandolod58d5042016-12-16 16:59:21 -0800185 return;
186 }
187 const col: IXosTableColumn = {
188 label: this.toLabel(f.name),
189 prop: f.name
190 };
191
Matteo Scandoloa8a6fbb2016-12-21 16:59:08 -0800192 if (f.name === 'id' || f.name === 'name') {
Matteo Scandoloa242c872017-01-12 15:13:00 -0800193 col.link = item => this.stateWithParams(modelName, item);
Matteo Scandoloee655a12016-12-19 15:38:43 -0800194 }
195
Matteo Scandolo04964232017-01-07 12:53:46 -0800196 // if the field identify a relation, create a link
Matteo Scandolo18975142017-08-01 14:48:04 -0700197 if (f.relation && f.relation.type === 'manytoone') {
Matteo Scandolo04964232017-01-07 12:53:46 -0800198 col.type = 'custom';
199 col.formatter = item => {
200 this.populateRelated(item, item[f.name], f);
201 return item[f.name];
202 };
Matteo Scandolo8248bca2017-08-09 13:46:04 -0700203 col.link = item => this.relatedStateWithParams(f.relation.model, item[col.prop]);
Matteo Scandolo04964232017-01-07 12:53:46 -0800204 }
205
Matteo Scandolod53ac1d2017-08-01 15:06:09 -0700206 if (f.name === 'backend_status' || f.name === 'policy_status') {
Matteo Scandolo8cd21b02017-09-20 10:13:13 +0900207
208 const statusCol = f.name === 'backend_status' ? 'backend_code' : 'policy_code';
209
Matteo Scandolod58d5042016-12-16 16:59:21 -0800210 col.type = 'icon';
Matteo Scandolo8b2370c2017-02-02 17:19:07 -0800211 col.hover = (item) => {
212 return item[f.name];
213 };
Matteo Scandolod58d5042016-12-16 16:59:21 -0800214 col.formatter = (item) => {
Matteo Scandolo8cd21b02017-09-20 10:13:13 +0900215 if (parseInt(item[statusCol], 10) === 1) {
Matteo Scandolod58d5042016-12-16 16:59:21 -0800216 return 'check';
217 }
Matteo Scandolo8cd21b02017-09-20 10:13:13 +0900218 if (parseInt(item[statusCol], 10) === 2) {
Matteo Scandolod58d5042016-12-16 16:59:21 -0800219 return 'exclamation-circle';
220 }
Matteo Scandolo8cd21b02017-09-20 10:13:13 +0900221 if (parseInt(item[statusCol], 10) === 0) {
Matteo Scandolod58d5042016-12-16 16:59:21 -0800222 return 'clock-o';
223 }
224 };
225 }
Matteo Scandoloa8a6fbb2016-12-21 16:59:08 -0800226
Matteo Scandolod58d5042016-12-16 16:59:21 -0800227 return col;
228 })
229 .filter(v => angular.isDefined(v));
230
Matteo Scandolo231de262017-01-04 16:33:14 -0800231 return columns;
Matteo Scandolod58d5042016-12-16 16:59:21 -0800232 };
233
Matteo Scandoloa242c872017-01-12 15:13:00 -0800234 public stateFromCoreModel(name: string): string {
235 const state: ng.ui.IState = _.find(this.$state.get(), (s: IXosState) => {
236 if (s.data) {
237 return s.data.model === name;
238 }
239 return false;
240 });
Matteo Scandolo1aee1982017-02-17 08:33:23 -0800241 return state ? state.name : null;
Matteo Scandoloa242c872017-01-12 15:13:00 -0800242 }
243
244 public stateWithParams(name: string, model: any): string {
245 const state = this.stateFromCoreModel(name);
246 return `${state}({id: ${model['id']}})`;
247 }
248
Matteo Scandolo8248bca2017-08-09 13:46:04 -0700249 public relatedStateWithParams(name: string, id: string): string {
250 const state = this.stateFromCoreModel(name);
251 return `${state}({id: ${id}})`;
252 }
253
Matteo Scandolo86bc26a2017-01-18 11:06:47 -0800254 public stateWithParamsForJs(name: string, model: any): any {
Matteo Scandolo86bc26a2017-01-18 11:06:47 -0800255 const state = this.stateFromCoreModel(name);
256 return {name: state, params: {id: model.id}};
257 }
258
Matteo Scandolo80c3a652017-01-06 10:48:31 -0800259 public modelFieldToInputCfg(fields: IXosModelDefsField[]): IXosFormInput[] {
260
Matteo Scandolo6f45e262017-01-09 14:47:26 -0800261 return _.map(fields, (f: IXosModelDefsField) => {
Matteo Scandolo1aee1982017-02-17 08:33:23 -0800262 const input: IXosFormInput = {
Matteo Scandolo80c3a652017-01-06 10:48:31 -0800263 name: f.name,
264 label: this.toLabel(f.name),
265 type: f.type,
Matteo Scandolo1aee1982017-02-17 08:33:23 -0800266 validators: this.formatValidators(f.validators),
Matteo Scandoloe7e052d2017-07-31 19:54:31 -0700267 hint: f.hint,
Matteo Scandolof1e68cd2017-09-05 17:30:34 -0700268 default: this.formatDefaultValues(f.default)
Matteo Scandolo80c3a652017-01-06 10:48:31 -0800269 };
Matteo Scandoloe7e052d2017-07-31 19:54:31 -0700270
271 // NOTE populate drop-downs based on relation
Matteo Scandolo1aee1982017-02-17 08:33:23 -0800272 if (f.relation) {
273 input.type = 'select';
274 this.populateSelectField(f, input);
Matteo Scandoloe7e052d2017-07-31 19:54:31 -0700275 }
276 // NOTE if static options are defined in modeldefs
277 // the f.options field is already populated,
278 // we just need to move it to the input
279 else if (f.options && f.options.length > 0) {
280 input.options = f.options;
Matteo Scandolo1aee1982017-02-17 08:33:23 -0800281 }
282 return input;
Matteo Scandolo80c3a652017-01-06 10:48:31 -0800283 })
Matteo Scandolod53ac1d2017-08-01 15:06:09 -0700284 .filter(f => this.form_excluded_fields.indexOf(f.name) === -1);
Matteo Scandolo80c3a652017-01-06 10:48:31 -0800285 }
286
Matteo Scandolo1aee1982017-02-17 08:33:23 -0800287 public modelToFormCfg(model: IXosModeldef): IXosFormCfg {
Matteo Scandolof1e68cd2017-09-05 17:30:34 -0700288
Matteo Scandolo1aee1982017-02-17 08:33:23 -0800289 const formCfg: IXosFormCfg = {
Matteo Scandolo80c3a652017-01-06 10:48:31 -0800290 formName: `${model.name}Form`,
Matteo Scandolod53ac1d2017-08-01 15:06:09 -0700291 exclude: this.form_excluded_fields,
Matteo Scandolo80c3a652017-01-06 10:48:31 -0800292 actions: [{
293 label: 'Save',
294 class: 'success',
295 icon: 'ok',
Matteo Scandolo6f45e262017-01-09 14:47:26 -0800296 cb: null
Matteo Scandolo80c3a652017-01-06 10:48:31 -0800297 }],
298 inputs: this.modelFieldToInputCfg(model.fields)
299 };
Matteo Scandolo6f45e262017-01-09 14:47:26 -0800300
301 formCfg.actions[0].cb = (item, form: angular.IFormController) => {
302
303 if (!form.$valid) {
304 formCfg.feedback = {
305 show: true,
306 message: 'Form is invalid',
307 type: 'danger',
308 closeBtn: true
309 };
Matteo Scandoloac8c8c22017-01-09 15:04:32 -0800310
Matteo Scandolo6f45e262017-01-09 14:47:26 -0800311 return;
312 }
313
Max Chubab0a582017-09-06 08:32:34 -0700314 const itemCopy = angular.copy(item);
Matteo Scandolo6f45e262017-01-09 14:47:26 -0800315
316 // TODO remove ManyToMany relations and save them separately (how??)
317 delete item.networks;
318
Matteo Scandolo1aee1982017-02-17 08:33:23 -0800319 // remove field added by xosTable
320 _.forEach(Object.keys(item), prop => {
321 if (prop.indexOf('-formatted') > -1) {
322 delete item[prop];
323 }
324 });
Matteo Scandolo6f45e262017-01-09 14:47:26 -0800325
Max Chubab0a582017-09-06 08:32:34 -0700326 const itemName = (angular.isUndefined(itemCopy.name)) ? model.name : itemCopy.name;
327
Matteo Scandolo6f45e262017-01-09 14:47:26 -0800328 item.$save()
329 .then((res) => {
Matteo Scandoloac8c8c22017-01-09 15:04:32 -0800330 formCfg.feedback = {
331 show: true,
Max Chubab0a582017-09-06 08:32:34 -0700332 message: `${itemName} successfully saved`,
Matteo Scandoloac8c8c22017-01-09 15:04:32 -0800333 type: 'success',
334 closeBtn: true
335 };
Max Chubab0a582017-09-06 08:32:34 -0700336 this.toastr.success(`${itemName} successfully saved`);
Matteo Scandolo6f45e262017-01-09 14:47:26 -0800337 })
338 .catch(err => {
Matteo Scandolo42c66922017-05-01 17:24:59 -0700339 formCfg.feedback = {
340 show: true,
Max Chubab0a582017-09-06 08:32:34 -0700341 message: `Error while saving ${itemName}: ${err.error}. ${err.specific_error || ''}`,
Matteo Scandolo42c66922017-05-01 17:24:59 -0700342 type: 'danger',
343 closeBtn: true
344 };
Max Chubab0a582017-09-06 08:32:34 -0700345 this.toastr.error(err.specific_error || '', `Error while saving ${itemName}: ${err.error}`);
Matteo Scandolo6f45e262017-01-09 14:47:26 -0800346 });
347 };
348
349 return formCfg;
Matteo Scandolo80c3a652017-01-06 10:48:31 -0800350 }
351
Matteo Scandolof1e68cd2017-09-05 17:30:34 -0700352 private formatDefaultValues(val: any): any {
353
354 if (angular.isString(val)) {
355 const unquoted = val.split('"').join('').toLowerCase();
356 if (unquoted === 'true') {
357 return true;
358 }
359 else if (unquoted === 'false') {
360 return false;
361 }
362 }
363
364 return val || undefined;
365 }
366
Matteo Scandolo1aee1982017-02-17 08:33:23 -0800367 private formatValidators(validators: IXosModelDefsFieldValidators[]): IXosFormInputValidator {
368 // convert validators as expressed from modelDefs,
369 // to the object required by xosForm
370 return _.reduce(validators, (formValidators: IXosFormInputValidator, v: IXosModelDefsFieldValidators) => {
371 formValidators[v.name] = v.bool_value ? v.bool_value : v.int_value;
372 return formValidators;
373 }, {});
374 }
375
Matteo Scandolod58d5042016-12-16 16:59:21 -0800376 private fromCamelCase(string: string): string {
377 return string.split(/(?=[A-Z])/).map(w => w.toLowerCase()).join(' ');
378 }
379
380 private fromSnakeCase(string: string): string {
381 return string.split('_').join(' ').trim();
382 }
383
384 private fromKebabCase(string: string): string {
385 return string.split('-').join(' ').trim();
386 }
387
388 private capitalizeFirst(string: string): string {
389 return string.slice(0, 1).toUpperCase() + string.slice(1);
390 }
Matteo Scandolo04964232017-01-07 12:53:46 -0800391
392 private populateRelated(item: any, fk: string, field: IXosModelDefsField): any {
393 // if the relation is not defined return
394 if (!fk || angular.isUndefined(fk) || fk === null) {
395 return;
396 }
Matteo Scandolo1aee1982017-02-17 08:33:23 -0800397 this.XosModelStore.query(field.relation.model)
Matteo Scandolo04964232017-01-07 12:53:46 -0800398 .subscribe(res => {
399 if (angular.isDefined(res) && angular.isDefined(fk)) {
400 let ri = _.find(res, {id: fk});
401 if (angular.isDefined(ri)) {
Matteo Scandolo1aee1982017-02-17 08:33:23 -0800402 if (angular.isDefined(ri.name)) {
403 item[`${field.name}-formatted`] = ri.name;
404 }
405 else if (angular.isDefined(ri.humanReadableName)) {
406 item[`${field.name}-formatted`] = ri.humanReadableName;
407 }
408 else {
409 item[`${field.name}-formatted`] = ri.id;
410 }
Matteo Scandolo04964232017-01-07 12:53:46 -0800411 }
412 }
413 });
414 }
Matteo Scandolo07e2f622017-01-09 10:54:13 -0800415
416 // augment a select field with related model informations
417 private populateSelectField(field: IXosModelDefsField, input: IXosFormInput): void {
Matteo Scandolo1aee1982017-02-17 08:33:23 -0800418 this.XosModelStore.query(field.relation.model)
Matteo Scandolo07e2f622017-01-09 10:54:13 -0800419 .subscribe(res => {
420 input.options = _.map(res, item => {
Matteo Scandolo1aee1982017-02-17 08:33:23 -0800421 let opt = {id: item.id, label: item.humanReadableName ? item.humanReadableName : item.name};
422 if (!angular.isDefined(item.humanReadableName) && !angular.isDefined(item.name)) {
423 opt.label = item.id;
424 }
425 return opt;
Matteo Scandolo07e2f622017-01-09 10:54:13 -0800426 });
427 });
428 }
Matteo Scandolod58d5042016-12-16 16:59:21 -0800429}