Browse Source

Merge pull request #2938 from vvlladd28/improvement/minor-fix

[3.0] Optimaze and clear code
pull/3089/head
Igor Kulikov 6 years ago
committed by GitHub
parent
commit
f4c7d6c3e2
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 10
      ui-ngx/src/app/modules/home/components/widget/action/custom-sample-html.raw
  2. 58
      ui-ngx/src/app/modules/home/components/widget/action/custom-sample-js.raw
  3. 224
      ui-ngx/src/app/modules/home/pages/admin/security-settings.component.html
  4. 5
      ui-ngx/src/app/modules/home/pages/admin/security-settings.component.scss
  5. 4
      ui-ngx/src/app/shared/components/dialog/node-script-test-dialog.component.ts
  6. 18
      ui-ngx/src/app/shared/components/time/timewindow-panel.component.html
  7. 24
      ui-ngx/src/app/shared/components/time/timewindow-panel.component.ts

10
ui-ngx/src/app/modules/home/components/widget/action/custom-sample-html.raw

@ -62,14 +62,14 @@
<!-- <mat-form-field fxFlex class="mat-block">-->
<!-- <mat-label>Integer Value</mat-label>-->
<!-- <input type="number" step="1" matInput formControlName="number">-->
<!-- <mat-error *ngIf="editEntityFormGroup.get('attributes').get('number').hasError('pattern')">-->
<!-- <mat-error *ngIf="editEntityFormGroup.get('attributes.number').hasError('pattern')">-->
<!-- Invalid integer value.-->
<!-- </mat-error>-->
<!-- </mat-form-field>-->
<!-- <div class="boolean-value-input" fxLayout="column" fxLayoutAlign="center start" fxFlex>-->
<!-- <label class="checkbox-label">Boolean Value</label>-->
<!-- <mat-checkbox formControlName="booleanValue" style="margin-bottom: 40px;">-->
<!-- {{ (editEntityFormGroup.get('attributes').get('booleanValue').value ? "value.true" : "value.false") | translate }}-->
<!-- {{ (editEntityFormGroup.get('attributes.booleanValue').value ? "value.true" : "value.false") | translate }}-->
<!-- </mat-checkbox>-->
<!-- </div>-->
<!-- </div>-->
@ -184,7 +184,6 @@
<!-- Save-->
<!-- </button>-->
<!-- <button mat-button color="primary"-->
<!-- style="margin-right: 20px;"-->
<!-- type="button"-->
<!-- [disabled]="(isLoading$ | async)"-->
<!-- (click)="cancel()" cdkFocusInitial>-->
@ -270,14 +269,14 @@
<!-- <mat-form-field fxFlex class="mat-block">-->
<!-- <mat-label>Integer Value</mat-label>-->
<!-- <input type="number" step="1" matInput formControlName="number">-->
<!-- <mat-error *ngIf="addEntityFormGroup.get('attributes').get('number').hasError('pattern')">-->
<!-- <mat-error *ngIf="addEntityFormGroup.get('attributes.number').hasError('pattern')">-->
<!-- Invalid integer value.-->
<!-- </mat-error>-->
<!-- </mat-form-field>-->
<!-- <div class="boolean-value-input" fxLayout="column" fxLayoutAlign="center start" fxFlex>-->
<!-- <label class="checkbox-label">Boolean Value</label>-->
<!-- <mat-checkbox formControlName="booleanValue" style="margin-bottom: 40px;">-->
<!-- {{ (addEntityFormGroup.get('attributes').get('booleanValue').value ? "value.true" : "value.false") | translate }}-->
<!-- {{ (addEntityFormGroup.get('attributes.booleanValue').value ? "value.true" : "value.false") | translate }}-->
<!-- </mat-checkbox>-->
<!-- </div>-->
<!-- </div>-->
@ -345,7 +344,6 @@
<!-- Create-->
<!-- </button>-->
<!-- <button mat-button color="primary"-->
<!-- style="margin-right: 20px;"-->
<!-- type="button"-->
<!-- [disabled]="(isLoading$ | async)"-->
<!-- (click)="cancel()" cdkFocusInitial>-->

58
ui-ngx/src/app/modules/home/components/widget/action/custom-sample-js.raw

@ -186,28 +186,27 @@
// }
//
// function getEntityInfo() {
// entityService.getEntity(entityId.entityType, entityId.id).subscribe(function (entity) {
// vm.entity = entity;
// widgetContext.rxjs.forkJoin([
// entityRelationService.findInfoByFrom(entityId),
// entityRelationService.findInfoByTo(entityId),
// attributeService.getEntityAttributes(entityId, 'SERVER_SCOPE')
// ]).subscribe(
// function (data) {
// getEntityRelations(data.slice(0,2));
// getEntityAttributes(data[2]);
// vm.editEntityFormGroup.patchValue({
// entityName: vm.entity.name,
// entityType: vm.entityType,
// entityLabel: vm.entity.label,
// type: vm.entity.type,
// attributes: vm.attributes,
// oldRelations: vm.oldRelationsData
// }, {emitEvent: false});
// }
// );
// });
// }
// widgetContext.rxjs.forkJoin([
// entityRelationService.findInfoByFrom(entityId),
// entityRelationService.findInfoByTo(entityId),
// attributeService.getEntityAttributes(entityId, 'SERVER_SCOPE'),
// entityService.getEntity(entityId.entityType, entityId.id)
// ]).subscribe(
// function (data) {
// getEntityRelations(data.slice(0,2));
// getEntityAttributes(data[2]);
// vm.entity = data[3];
// vm.editEntityFormGroup.patchValue({
// entityName: vm.entity.name,
// entityType: vm.entityType,
// entityLabel: vm.entity.label,
// type: vm.entity.type,
// attributes: vm.attributes,
// oldRelations: vm.oldRelationsData
// }, {emitEvent: false});
// }
// );
// }
//
// function saveEntity() {
// const formValues = vm.editEntityFormGroup.value;
@ -218,9 +217,8 @@
// } else if (formValues.entityType == 'DEVICE') {
// return deviceService.saveDevice(vm.entity);
// }
// } else {
// return widgetContext.rxjs.of([]);
// }
// return widgetContext.rxjs.of([]);
// }
//
// function saveAttributes(entityId) {
@ -233,9 +231,8 @@
// }
// if (attributesArray.length > 0) {
// return attributeService.saveEntityAttributes(entityId, "SERVER_SCOPE", attributesArray);
// } else {
// return widgetContext.rxjs.of([]);
// }
// return widgetContext.rxjs.of([]);
// }
//
// function saveRelations(entityId) {
@ -270,9 +267,8 @@
// }
// if (tasks.length > 0) {
// return widgetContext.rxjs.forkJoin(tasks);
// } else {
// return widgetContext.rxjs.of([]);
// }
// return widgetContext.rxjs.of([]);
// }
//}
//
@ -380,9 +376,8 @@
// }
// if (attributesArray.length > 0) {
// return attributeService.saveEntityAttributes(entityId, "SERVER_SCOPE", attributesArray);
// } else {
// return widgetContext.rxjs.of([]);
// }
// return widgetContext.rxjs.of([]);
// }
//
// function saveRelations(entityId) {
@ -404,8 +399,7 @@
// }
// if (tasks.length > 0) {
// return widgetContext.rxjs.forkJoin(tasks);
// } else {
// return widgetContext.rxjs.of([]);
// }
// return widgetContext.rxjs.of([]);
// }
//}

224
ui-ngx/src/app/modules/home/pages/admin/security-settings.component.html

@ -28,117 +28,121 @@
</mat-progress-bar>
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
<mat-card-content style="padding-top: 16px;">
<form [formGroup]="securitySettingsFormGroup" (ngSubmit)="save()">
<form [formGroup]="securitySettingsFormGroup" (ngSubmit)="save()" autocomplete="off">
<fieldset [disabled]="isLoading$ | async">
<mat-expansion-panel [expanded]="true">
<mat-expansion-panel-header>
<mat-panel-title>
<div class="tb-panel-title" translate>admin.general-policy</div>
</mat-panel-title>
</mat-expansion-panel-header>
<mat-form-field class="mat-block">
<mat-label translate>admin.max-failed-login-attempts</mat-label>
<input matInput type="number"
formControlName="maxFailedLoginAttempts"
step="1"
min="0"/>
<mat-error *ngIf="securitySettingsFormGroup.get('maxFailedLoginAttempts').hasError('min')">
{{ 'admin.minimum-max-failed-login-attempts-range' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field class="mat-block">
<mat-label translate>admin.user-lockout-notification-email</mat-label>
<input matInput type="email"
formControlName="userLockoutNotificationEmail"/>
</mat-form-field>
</mat-expansion-panel>
<mat-expansion-panel [expanded]="true">
<mat-expansion-panel-header>
<mat-panel-title>
<div class="tb-panel-title" translate>admin.password-policy</div>
</mat-panel-title>
</mat-expansion-panel-header>
<section formGroupName="passwordPolicy">
<mat-form-field class="mat-block">
<mat-label translate>admin.minimum-password-length</mat-label>
<input matInput type="number"
formControlName="minimumLength"
step="1"
min="5"
max="50"
required/>
<mat-error *ngIf="securitySettingsFormGroup.get('passwordPolicy').get('minimumLength').hasError('required')">
{{ 'admin.minimum-password-length-required' | translate }}
</mat-error>
<mat-error *ngIf="securitySettingsFormGroup.get('passwordPolicy').get('minimumLength').hasError('min')">
{{ 'admin.minimum-password-length-range' | translate }}
</mat-error>
<mat-error *ngIf="securitySettingsFormGroup.get('passwordPolicy').get('minimumLength').hasError('max')">
{{ 'admin.minimum-password-length-range' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field class="mat-block">
<mat-label translate>admin.minimum-uppercase-letters</mat-label>
<input matInput type="number"
formControlName="minimumUppercaseLetters"
step="1"
min="0"/>
<mat-error *ngIf="securitySettingsFormGroup.get('passwordPolicy').get('minimumUppercaseLetters').hasError('min')">
{{ 'admin.minimum-uppercase-letters-range' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field class="mat-block">
<mat-label translate>admin.minimum-lowercase-letters</mat-label>
<input matInput type="number"
formControlName="minimumLowercaseLetters"
step="1"
min="0"/>
<mat-error *ngIf="securitySettingsFormGroup.get('passwordPolicy').get('minimumLowercaseLetters').hasError('min')">
{{ 'admin.minimum-lowercase-letters-range' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field class="mat-block">
<mat-label translate>admin.minimum-digits</mat-label>
<input matInput type="number"
formControlName="minimumDigits"
step="1"
min="0"/>
<mat-error *ngIf="securitySettingsFormGroup.get('passwordPolicy').get('minimumDigits').hasError('min')">
{{ 'admin.minimum-digits-range' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field class="mat-block">
<mat-label translate>admin.minimum-special-characters</mat-label>
<input matInput type="number"
formControlName="minimumSpecialCharacters"
step="1"
min="0"/>
<mat-error *ngIf="securitySettingsFormGroup.get('passwordPolicy').get('minimumSpecialCharacters').hasError('min')">
{{ 'admin.minimum-special-characters-range' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field class="mat-block">
<mat-label translate>admin.password-expiration-period-days</mat-label>
<input matInput type="number"
formControlName="passwordExpirationPeriodDays"
step="1"
min="0"/>
<mat-error *ngIf="securitySettingsFormGroup.get('passwordPolicy').get('passwordExpirationPeriodDays').hasError('min')">
{{ 'admin.password-expiration-period-days-range' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field class="mat-block">
<mat-label translate>admin.password-reuse-frequency-days</mat-label>
<input matInput type="number"
formControlName="passwordReuseFrequencyDays"
step="1"
min="0"/>
<mat-error *ngIf="securitySettingsFormGroup.get('passwordPolicy').get('passwordReuseFrequencyDays').hasError('min')">
{{ 'admin.password-reuse-frequency-days-range' | translate }}
</mat-error>
</mat-form-field>
</section>
</mat-expansion-panel>
<div class="mat-accordion-container">
<mat-accordion multi="true">
<mat-expansion-panel [expanded]="true">
<mat-expansion-panel-header>
<mat-panel-title>
<div class="tb-panel-title" translate>admin.general-policy</div>
</mat-panel-title>
</mat-expansion-panel-header>
<mat-form-field class="mat-block">
<mat-label translate>admin.max-failed-login-attempts</mat-label>
<input matInput type="number"
formControlName="maxFailedLoginAttempts"
step="1"
min="0"/>
<mat-error *ngIf="securitySettingsFormGroup.get('maxFailedLoginAttempts').hasError('min')">
{{ 'admin.minimum-max-failed-login-attempts-range' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field class="mat-block">
<mat-label translate>admin.user-lockout-notification-email</mat-label>
<input matInput type="email"
formControlName="userLockoutNotificationEmail"/>
</mat-form-field>
</mat-expansion-panel>
<mat-expansion-panel [expanded]="true">
<mat-expansion-panel-header>
<mat-panel-title>
<div class="tb-panel-title" translate>admin.password-policy</div>
</mat-panel-title>
</mat-expansion-panel-header>
<section formGroupName="passwordPolicy">
<mat-form-field class="mat-block">
<mat-label translate>admin.minimum-password-length</mat-label>
<input matInput type="number"
formControlName="minimumLength"
step="1"
min="5"
max="50"
required/>
<mat-error *ngIf="securitySettingsFormGroup.get('passwordPolicy.minimumLength').hasError('required')">
{{ 'admin.minimum-password-length-required' | translate }}
</mat-error>
<mat-error *ngIf="securitySettingsFormGroup.get('passwordPolicy.minimumLength').hasError('min')">
{{ 'admin.minimum-password-length-range' | translate }}
</mat-error>
<mat-error *ngIf="securitySettingsFormGroup.get('passwordPolicy.minimumLength').hasError('max')">
{{ 'admin.minimum-password-length-range' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field class="mat-block">
<mat-label translate>admin.minimum-uppercase-letters</mat-label>
<input matInput type="number"
formControlName="minimumUppercaseLetters"
step="1"
min="0"/>
<mat-error *ngIf="securitySettingsFormGroup.get('passwordPolicy.minimumUppercaseLetters').hasError('min')">
{{ 'admin.minimum-uppercase-letters-range' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field class="mat-block">
<mat-label translate>admin.minimum-lowercase-letters</mat-label>
<input matInput type="number"
formControlName="minimumLowercaseLetters"
step="1"
min="0"/>
<mat-error *ngIf="securitySettingsFormGroup.get('passwordPolicy.minimumLowercaseLetters').hasError('min')">
{{ 'admin.minimum-lowercase-letters-range' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field class="mat-block">
<mat-label translate>admin.minimum-digits</mat-label>
<input matInput type="number"
formControlName="minimumDigits"
step="1"
min="0"/>
<mat-error *ngIf="securitySettingsFormGroup.get('passwordPolicy.minimumDigits').hasError('min')">
{{ 'admin.minimum-digits-range' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field class="mat-block">
<mat-label translate>admin.minimum-special-characters</mat-label>
<input matInput type="number"
formControlName="minimumSpecialCharacters"
step="1"
min="0"/>
<mat-error *ngIf="securitySettingsFormGroup.get('passwordPolicy.minimumSpecialCharacters').hasError('min')">
{{ 'admin.minimum-special-characters-range' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field class="mat-block">
<mat-label translate>admin.password-expiration-period-days</mat-label>
<input matInput type="number"
formControlName="passwordExpirationPeriodDays"
step="1"
min="0"/>
<mat-error *ngIf="securitySettingsFormGroup.get('passwordPolicy.passwordExpirationPeriodDays').hasError('min')">
{{ 'admin.password-expiration-period-days-range' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field class="mat-block">
<mat-label translate>admin.password-reuse-frequency-days</mat-label>
<input matInput type="number"
formControlName="passwordReuseFrequencyDays"
step="1"
min="0"/>
<mat-error *ngIf="securitySettingsFormGroup.get('passwordPolicy.passwordReuseFrequencyDays').hasError('min')">
{{ 'admin.password-reuse-frequency-days-range' | translate }}
</mat-error>
</mat-form-field>
</section>
</mat-expansion-panel>
</mat-accordion>
</div>
<div fxLayout="row" fxLayoutAlign="end center" style="width: 100%;" class="layout-wrap">
<button mat-button mat-raised-button color="primary" [disabled]="(isLoading$ | async) || securitySettingsFormGroup.invalid || !securitySettingsFormGroup.dirty"
type="submit">{{'action.save' | translate}}

5
ui-ngx/src/app/modules/home/pages/admin/security-settings.component.scss

@ -14,10 +14,7 @@
* limitations under the License.
*/
:host {
mat-expansion-panel {
.mat-accordion-container {
margin-bottom: 16px;
}
.tb-panel-title {
}
}

4
ui-ngx/src/app/shared/components/dialog/node-script-test-dialog.component.ts

@ -174,8 +174,8 @@ export class NodeScriptTestDialogComponent extends DialogComponent<NodeScriptTes
const inputParams: TestScriptInputParams = {
argNames: this.data.argNames,
scriptType: this.data.scriptType,
msgType: this.nodeScriptTestFormGroup.get('payload').get('msgType').value,
msg: this.nodeScriptTestFormGroup.get('payload').get('msg').value,
msgType: this.nodeScriptTestFormGroup.get('payload.msgType').value,
msg: this.nodeScriptTestFormGroup.get('payload.msg').value,
metadata: this.nodeScriptTestFormGroup.get('metadata').value,
script: this.nodeScriptTestFormGroup.get('script').value
};

18
ui-ngx/src/app/shared/components/time/timewindow-panel.component.html

@ -48,9 +48,9 @@
<tb-timeinterval
formControlName="timewindowMs"
predefinedName="timewindow.last"
[fxShow]="timewindowForm.get('history').get('historyType').value === historyTypes.LAST_INTERVAL"
[fxShow]="timewindowForm.get('history.historyType').value === historyTypes.LAST_INTERVAL"
[required]="timewindow.selectedTab === timewindowTypes.HISTORY &&
timewindowForm.get('history').get('historyType').value === historyTypes.LAST_INTERVAL"
timewindowForm.get('history.historyType').value === historyTypes.LAST_INTERVAL"
style="padding-top: 8px;"></tb-timeinterval>
</section>
</mat-radio-button>
@ -59,9 +59,9 @@
<span translate>timewindow.time-period</span>
<tb-datetime-period
formControlName="fixedTimewindow"
[fxShow]="timewindowForm.get('history').get('historyType').value === historyTypes.FIXED"
[fxShow]="timewindowForm.get('history.historyType').value === historyTypes.FIXED"
[required]="timewindow.selectedTab === timewindowTypes.HISTORY &&
timewindowForm.get('history').get('historyType').value === historyTypes.FIXED"
timewindowForm.get('history.historyType').value === historyTypes.FIXED"
style="padding-top: 8px;"></tb-datetime-period>
</section>
</mat-radio-button>
@ -89,7 +89,7 @@
</mat-form-field>
</section>
</section>
<section fxLayout="row" [fxShow]="timewindowForm.get('aggregation').get('type').value === aggregationTypes.NONE">
<section fxLayout="row" [fxShow]="timewindowForm.get('aggregation.type').value === aggregationTypes.NONE">
<section fxLayout="column" [fxShow]="isEdit">
<label class="tb-small hide-label" translate>timewindow.hide</label>
<mat-checkbox [ngModelOptions]="{standalone: true}" [(ngModel)]="timewindow.hideAggInterval"
@ -101,13 +101,13 @@
<span translate>aggregation.limit</span>
<mat-slider fxFlex formControlName="limit"
thumbLabel
[value]="timewindowForm.get('aggregation').get('limit').value"
[value]="timewindowForm.get('aggregation.limit').value"
min="{{minDatapointsLimit()}}"
max="{{maxDatapointsLimit()}}">
</mat-slider>
<mat-form-field style="max-width: 80px;">
<input matInput formControlName="limit" type="number" step="1"
[value]="timewindowForm.get('aggregation').get('limit').value"
[value]="timewindowForm.get('aggregation.limit').value"
min="{{minDatapointsLimit()}}"
max="{{maxDatapointsLimit()}}"/>
</mat-form-field>
@ -116,7 +116,7 @@
</section>
</div>
<div formGroupName="realtime"
*ngIf="aggregation && timewindowForm.get('aggregation').get('type').value !== aggregationTypes.NONE &&
*ngIf="aggregation && timewindowForm.get('aggregation.type').value !== aggregationTypes.NONE &&
timewindow.selectedTab === timewindowTypes.REALTIME" class="mat-content mat-padding" fxLayout="column">
<tb-timeinterval
formControlName="interval"
@ -128,7 +128,7 @@
</tb-timeinterval>
</div>
<div formGroupName="history"
*ngIf="aggregation && timewindowForm.get('aggregation').get('type').value !== aggregationTypes.NONE &&
*ngIf="aggregation && timewindowForm.get('aggregation.type').value !== aggregationTypes.NONE &&
timewindow.selectedTab === timewindowTypes.HISTORY" class="mat-content mat-padding" fxLayout="column">
<tb-timeinterval
formControlName="interval"

24
ui-ngx/src/app/shared/components/time/timewindow-panel.component.ts

@ -174,11 +174,11 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit {
}
minRealtimeAggInterval() {
return this.timeService.minIntervalLimit(this.timewindowForm.get('realtime').get('timewindowMs').value);
return this.timeService.minIntervalLimit(this.timewindowForm.get('realtime.timewindowMs').value);
}
maxRealtimeAggInterval() {
return this.timeService.maxIntervalLimit(this.timewindowForm.get('realtime').get('timewindowMs').value);
return this.timeService.maxIntervalLimit(this.timewindowForm.get('realtime.timewindowMs').value);
}
minHistoryAggInterval() {
@ -203,31 +203,31 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit {
onHideIntervalChanged() {
if (this.timewindow.hideInterval) {
this.timewindowForm.get('history').get('historyType').disable({emitEvent: false});
this.timewindowForm.get('history').get('timewindowMs').disable({emitEvent: false});
this.timewindowForm.get('history').get('fixedTimewindow').disable({emitEvent: false});
this.timewindowForm.get('history.historyType').disable({emitEvent: false});
this.timewindowForm.get('history.timewindowMs').disable({emitEvent: false});
this.timewindowForm.get('history.fixedTimewindow').disable({emitEvent: false});
} else {
this.timewindowForm.get('history').get('historyType').enable({emitEvent: false});
this.timewindowForm.get('history').get('timewindowMs').enable({emitEvent: false});
this.timewindowForm.get('history').get('fixedTimewindow').enable({emitEvent: false});
this.timewindowForm.get('history.historyType').enable({emitEvent: false});
this.timewindowForm.get('history.timewindowMs').enable({emitEvent: false});
this.timewindowForm.get('history.fixedTimewindow').enable({emitEvent: false});
}
this.timewindowForm.markAsDirty();
}
onHideAggregationChanged() {
if (this.timewindow.hideAggregation) {
this.timewindowForm.get('aggregation').get('type').disable({emitEvent: false});
this.timewindowForm.get('aggregation.type').disable({emitEvent: false});
} else {
this.timewindowForm.get('aggregation').get('type').enable({emitEvent: false});
this.timewindowForm.get('aggregation.type').enable({emitEvent: false});
}
this.timewindowForm.markAsDirty();
}
onHideAggIntervalChanged() {
if (this.timewindow.hideAggInterval) {
this.timewindowForm.get('aggregation').get('limit').disable({emitEvent: false});
this.timewindowForm.get('aggregation.limit').disable({emitEvent: false});
} else {
this.timewindowForm.get('aggregation').get('limit').enable({emitEvent: false});
this.timewindowForm.get('aggregation.limit').enable({emitEvent: false});
}
this.timewindowForm.markAsDirty();
}

Loading…
Cancel
Save