Dashboard: added the validation of a factory name's uniqueness (#6758)

* CHE-5462: add unique-factory-name validation directive.

Signed-off-by: Oleksii Kurinnyi <okurinny@redhat.com>

* CHE-5462: use validation directive for factory name's uniqueness.

Signed-off-by: Oleksii Kurinnyi <okurinny@redhat.com>
6.19.x
Oleksii Kurinnyi 2017-10-18 09:52:06 +03:00 committed by GitHub
parent 45f4b35233
commit a22f44294b
11 changed files with 465 additions and 38 deletions

View File

@ -22,7 +22,7 @@ export class CreateFactoryCtrl {
private $log: ng.ILogService;
private cheAPI: CheAPI;
private cheNotification: CheNotification;
private lodash: _.LoDashStatic;
private lodash: any;
private $filter: ng.IFilterService;
private $document: ng.IDocumentService;
private isLoading: boolean;
@ -42,7 +42,7 @@ export class CreateFactoryCtrl {
* Default constructor that is using resource injection
* @ngInject for Dependency injection
*/
constructor($location: ng.ILocationService, cheAPI: CheAPI, $log: ng.ILogService, cheNotification: CheNotification, $scope: ng.IScope, $filter: ng.IFilterService, lodash: _.LoDashStatic, $document: ng.IDocumentService) {
constructor($location: ng.ILocationService, cheAPI: CheAPI, $log: ng.ILogService, cheNotification: CheNotification, $scope: ng.IScope, $filter: ng.IFilterService, lodash: any, $document: ng.IDocumentService) {
this.$location = $location;
this.cheAPI = cheAPI;
this.$log = $log;

View File

@ -27,10 +27,12 @@
ng-trim
ng-minlength="3"
ng-maxlength="20"
ng-pattern="/^[ A-Za-z0-9_\-\.]+$/">
ng-pattern="/^[ A-Za-z0-9_\-\.]+$/"
unique-factory-name="">
<div ng-message="pattern">Factory name may contain digits, latin letters, spaces, _ , . , - and should start only with digits, latin letters or underscores</div>
<div ng-message="minlength">The name has to be more than 3 characters long.</div>
<div ng-message="maxlength">The name has to be less than 20 characters long.</div>
<div ng-message="uniqueFactoryName">This factory name is already used.</div>
</che-input-box>
</ng-form>
<!--Factory source-->

View File

@ -24,7 +24,7 @@ export class FactoryInformationController {
private $location: ng.ILocationService;
private $log: ng.ILogService;
private $timeout: ng.ITimeoutService;
private lodash: _.LoDashStatic;
private lodash: any;
private $filter: ng.IFilterService;
private timeoutPromise: ng.IPromise<any>;
@ -40,13 +40,14 @@ export class FactoryInformationController {
private workspaceName: string;
private stackId: string;
private workspaceConfig: any;
private origName: string;
/**
* Default constructor that is using resource injection
* @ngInject for Dependency injection
*/
constructor($scope: ng.IScope, cheAPI: CheAPI, cheNotification: CheNotification, $location: ng.ILocationService, $log: ng.ILogService,
$timeout: ng.ITimeoutService, lodash: _.LoDashStatic, $filter: ng.IFilterService, $q: ng.IQService, confirmDialogService: any) {
$timeout: ng.ITimeoutService, lodash: any, $filter: ng.IFilterService, $q: ng.IQService, confirmDialogService: any) {
this.cheAPI = cheAPI;
this.cheNotification = cheNotification;
this.$location = $location;
@ -93,6 +94,7 @@ export class FactoryInformationController {
this.environmentName = this.factory.workspace.defaultEnv;
this.copyOriginFactory = angular.copy(this.factory);
this.origName = this.factory.name;
if (this.copyOriginFactory.links) {
delete this.copyOriginFactory.links;
}
@ -100,7 +102,7 @@ export class FactoryInformationController {
let factoryContent = this.$filter('json')(this.copyOriginFactory);
if (factoryContent !== this.factoryContent) {
if (!this.factoryContent) {
this.editorLoadedPromise.then((instance) => {
this.editorLoadedPromise.then((instance: any) => {
this.$timeout(() => {
instance.refresh();
}, 500);
@ -139,12 +141,29 @@ export class FactoryInformationController {
}
/**
* Update factory data.
* Update factory name.
*
* @param {string} name new factory name.
* @raram {ng.IFormController} form
*/
updateFactory(): void {
updateName(name: string, form: ng.IFormController): void {
if (form.$invalid) {
return;
}
this.copyOriginFactory.name = name;
this.updateFactory(form);
}
/**
* Update factory data.
* @param {ng.IFormController} form
*/
updateFactory(form: ng.IFormController): void {
this.factoryContent = this.$filter('json')(this.copyOriginFactory);
if (this.factoryInformationForm.$invalid || !this.isFactoryChanged()) {
if (form.$invalid || !this.isFactoryChanged()) {
return;
}

View File

@ -16,16 +16,16 @@
<div layout="column" class="factory-information-input">
<ng-form name="factoryInformationForm">
<che-input che-form="factoryInformationForm"
ng-init="factoryInformationController.factoryInformationForm = factoryInformationForm"
che-name="name"
che-place-holder="Name of the factory"
aria-label="Name of the factory"
ng-model="factoryInformationController.copyOriginFactory.name"
ng-change="factoryInformationController.updateFactory()"
ng-change="factoryInformationController.updateName($value, factoryInformationForm)"
ng-trim
ng-minlength="3"
ng-maxlength="20"
ng-pattern="/^[ A-Za-z0-9_\-\.]+$/">
ng-pattern="/^[ A-Za-z0-9_\-\.]+$/"
unique-factory-name="factoryInformationController.origName">
<div ng-message="required">A name is required.</div>
<div ng-message="pattern">Factory name may contain digits, latin letters, spaces, _ , . , - and should start
only
@ -33,6 +33,7 @@
</div>
<div ng-message="minlength">The name has to be more than 3 characters long.</div>
<div ng-message="maxlength">The name has to be less than 20 characters long.</div>
<div ng-message="uniqueFactoryName">This factory name is already used.</div>
</che-input>
</ng-form>
</div>
@ -94,7 +95,7 @@
che-place-holder="Name of the workspace"
aria-label="Name of the workspace"
ng-model="factoryInformationController.copyOriginFactory.workspace.name"
ng-change="factoryInformationController.updateFactory()"
ng-change="factoryInformationController.updateFactory(factoryInformationForm)"
required
ng-minlength="3"
ng-maxlength="20"
@ -123,7 +124,7 @@
<span ng-if="factoryInformationController.getObjectKeys(environmentValue.machines).length > 1">MACHINE: {{machineKey}}</span>
<che-workspace-ram-allocation-slider
ng-model="machineValue.attributes.memoryLimitBytes"
che-on-change="factoryInformationController.updateFactory()"></che-workspace-ram-allocation-slider>
che-on-change="factoryInformationController.updateFactory(factoryInformationForm)"></che-workspace-ram-allocation-slider>
</div>
</div>
</div>
@ -136,7 +137,7 @@
<che-label-container che-label-name="Configure Commands"
che-label-description="Commands are processes that are invoked by users from a dropdown in the IDE.">
<cdvy-factory-command cdvy-factory-object="factoryInformationController.copyOriginFactory"
cdvy-on-change="factoryInformationController.updateFactory()"></cdvy-factory-command>
cdvy-on-change="factoryInformationController.updateFactory(factoryInformationForm)"></cdvy-factory-command>
</che-label-container>
<!-- Configure actions -->
@ -151,7 +152,7 @@
<cdvy-factory-action-box cdvy-lifecycle="onProjectsLoaded"
cdvy-callback-controller="factoryInformationController"
cdvy-factory-object="factoryInformationController.copyOriginFactory"
cdvy-on-change="factoryInformationController.updateFactory()"></cdvy-factory-action-box>
cdvy-on-change="factoryInformationController.updateFactory(factoryInformationForm)"></cdvy-factory-action-box>
</che-label-container>
<!-- Configuration -->

View File

@ -532,6 +532,17 @@ export class CheFactory {
return this.factoriesById.get(factoryId);
}
/**
* Get the factory by factoryName and userId
* @param factoryName {string} the factory name
* @param userId {string} the user ID
* @returns factory {che.IFactory}
*/
getFactoryByName(factoryName: string, userId: string): che.IFactory {
const key = `${userId}:${factoryName}`;
return this.factoriesByName.get(key);
}
/**
* Set the factory
* @param factory {che.IFactory}

View File

@ -469,6 +469,9 @@ export class CheHttpBackend {
this.httpBackend.when('DELETE', '/api/factory/' + factory.id).respond(() => {
return [200, {success: true, errors: []}];
});
if (this.defaultUser) {
this.httpBackend.when('GET', `/api/factory/find?creator.userId=${this.defaultUser.id}&name=${factory.name}`).respond([factory]);
}
allFactories.push(factory);
}

View File

@ -459,6 +459,7 @@ declare namespace che {
ide?: any;
button?: any;
policies?: any;
links: string[];
}
export interface IRegistry {

View File

@ -0,0 +1,95 @@
/*
* Copyright (c) 2015-2017 Red Hat, Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
*/
'use strict';
import {CheAPI} from '../api/che-api.factory';
interface IFactoryNameValidatorAsyncModelValidators extends ng.IAsyncModelValidators {
uniqueFactoryName: (modelValue: any, viewValue?: any) => ng.IPromise<any>;
}
interface IFactoryNameValidatorAttributes extends ng.IAttributes {
uniqueFactoryName: string;
}
/**
* Defines a directive for checking if the factory name is not already taken
* @author Oleksii Kurinnyi
*/
export class UniqueFactoryNameValidator implements ng.IDirective {
$q: ng.IQService;
cheAPI: CheAPI;
restrict: string = 'A';
require: string = 'ngModel';
user: che.IUser;
/**
* Default constructor that is using resource
* @ngInject for Dependency injection
*/
constructor (cheAPI: CheAPI, $q: ng.IQService) {
this.cheAPI = cheAPI;
this.$q = $q;
this.user = this.cheAPI.getUser().getUser();
}
/**
* Check that the name of workspace is unique
*/
link($scope: ng.IScope, element: ng.IAugmentedJQuery, attributes: IFactoryNameValidatorAttributes, ngModel: ng.INgModelController) {
const asyncValidators = ngModel.$asyncValidators as IFactoryNameValidatorAsyncModelValidators;
// validate only input element
if ('input' === element[0].localName) {
asyncValidators.uniqueFactoryName = (modelValue: any, viewValue: any) => {
// create promise
const deferred = this.$q.defer();
if (!this.user) {
deferred.reject(false);
return deferred.promise;
}
// parent scope ?
let scopingTest = $scope.$parent;
if (!scopingTest) {
scopingTest = $scope;
}
const currentFactoryName = scopingTest.$eval(attributes.uniqueFactoryName);
if (!modelValue || modelValue === currentFactoryName) {
deferred.resolve(true);
} else if (this.cheAPI.getFactory().getFactoryByName(modelValue, this.user.id)) {
deferred.reject(false);
} else {
this.cheAPI.getFactory().fetchFactoryByName(modelValue, this.user.id).finally(() => {
if (this.cheAPI.getFactory().getFactoryByName(modelValue, this.user.id)) {
deferred.reject(false);
} else {
deferred.resolve(true);
}
});
}
// return promise
return deferred.promise;
};
}
}
}

View File

@ -0,0 +1,299 @@
/*
* Copyright (c) 2015-2017 Red Hat, Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
*/
'use strict';
import {CheFactory} from '../api/che-factory.factory';
import {CheAPIBuilder} from '../api/builder/che-api-builder.factory';
import {CheHttpBackend} from '../api/test/che-http-backend';
interface ITestRootScope extends ng.IRootScopeService {
factoryName: string;
myForm: ng.IFormController;
}
interface ITestFormController extends ng.IFormController {
name: ng.INgModelController;
}
/**
* Test the factory name uniqueness
* @author Oleksii Kurinnyi
*/
describe('unique-factory-name-validator >', function() {
let $rootScope: ITestRootScope,
$compile: ng.ICompileService,
myForm: ITestFormController;
/**
* Factory API
*/
let cheFactory: CheFactory;
/**
* API builder.
*/
let cheAPIBuilder: CheAPIBuilder;
/**
* Che backend
*/
let cheHttpBackend: CheHttpBackend;
/**
* Backend for handling http operations
*/
let $httpBackend: ng.IHttpBackendService;
beforeEach(angular.mock.module('userDashboard'));
beforeEach(inject((_$compile_: ng.ICompileService,
_$rootScope_: ITestRootScope,
_cheFactory_: CheFactory,
_cheAPIBuilder_: CheAPIBuilder,
_cheHttpBackend_: CheHttpBackend) => {
$rootScope = _$rootScope_;
$compile = _$compile_;
cheFactory = _cheFactory_;
cheAPIBuilder = _cheAPIBuilder_;
cheHttpBackend = _cheHttpBackend_;
$httpBackend = _cheHttpBackend_.getHttpBackend();
}));
afterEach(function() {
$httpBackend.verifyNoOutstandingExpectation();
$httpBackend.verifyNoOutstandingRequest();
});
describe('validate factory name >', function() {
const factoryName1 = 'factoryName1',
loadedFactoryName = factoryName1,
factoryName2 = 'factoryName2',
uniqueName = 'uniqueName',
userId = 'userId';
beforeEach(() => {
// setup default user
const user = cheAPIBuilder.getUserBuilder().withId(userId).build();
cheHttpBackend.setDefaultUser(user);
cheHttpBackend.usersBackendSetup();
// setup factories
const factoryId1 = 'factoryId1',
factory1 = cheAPIBuilder.getFactoryBuilder().withId(factoryId1).withName(factoryName1).build();
cheHttpBackend.addUserFactory(factory1);
const factoryId2 = 'factoryId2',
factory2 = cheAPIBuilder.getFactoryBuilder().withId(factoryId2).withName(factoryName2).build();
cheHttpBackend.addUserFactory(factory2);
cheHttpBackend.factoriesBackendSetup();
cheHttpBackend.setup();
// setup backend
$httpBackend.whenGET( `/api/factory/find?creator.userId=${userId}&name=${uniqueName}`).respond(404, {error: 'not found'});
// fetch first factory
cheFactory.fetchFactoryByName(factoryName1, userId);
// flush HTTP backend
$httpBackend.flush();
});
describe('initially factory has an unique name >', () => {
beforeEach(() => {
$rootScope.factoryName = uniqueName;
const element = angular.element(`
<form name="myForm">
<input ng-model="factoryName" name="name" unique-factory-name="uniqueFactoryName" />
</form>
`);
$compile(element)($rootScope);
myForm = $rootScope.myForm as ITestFormController;
});
it('the form should be valid >', () => {
// flush HTTP backend
$httpBackend.flush();
// check form (expect valid)
expect(myForm.name.$invalid).toBe(false);
expect(myForm.name.$valid).toBe(true);
});
it('the HTTP request should be made >', () => {
$httpBackend.expectGET(`/api/factory/find?creator.userId=${userId}&name=${uniqueName}`);
// flush HTTP backend
$httpBackend.flush();
});
describe('change name to non-unique, factory is already downloaded >', () => {
beforeEach(() => {
myForm.name.$setViewValue(loadedFactoryName);
});
it ('the form should be invalid >', () => {
// check form (expect invalid)
expect(myForm.name.$invalid).toBe(true);
expect(myForm.name.$valid).toBe(false);
});
it('the HTTP request should not be made >', () => {
spyOn(cheFactory, 'fetchFactoryByName');
expect(cheFactory.fetchFactoryByName).not.toHaveBeenCalled();
});
});
describe('change name to non-unique, factory is not downloaded yet >', () => {
it ('the form should be invalid >', () => {
myForm.name.$setViewValue(factoryName2);
// flush HTTP backend
$httpBackend.flush();
// check form (expect invalid)
expect(myForm.name.$invalid).toBe(true);
expect(myForm.name.$valid).toBe(false);
});
it('the HTTP request should be made >', () => {
$httpBackend.expectGET(`/api/factory/find?creator.userId=${userId}&name=${factoryName2}`);
myForm.name.$setViewValue(factoryName2);
// flush HTTP backend
$httpBackend.flush();
});
});
});
describe('initially factory has a non-unique name, factory is already downloaded >', () => {
beforeEach(() => {
$rootScope.factoryName = loadedFactoryName;
const element = angular.element(`
<form name="myForm">
<input ng-model="factoryName" name="name" unique-factory-name="uniqueFactoryName" />
</form>
`);
$compile(element)($rootScope);
myForm = $rootScope.myForm as ITestFormController;
$rootScope.$digest();
});
it('the form should be invalid >', () => {
// check form (expect invalid)
expect(myForm.name.$invalid).toBe(true);
expect(myForm.name.$valid).toBe(false);
});
it('the HTTP request should not be made >', () => {
spyOn(cheFactory, 'fetchFactoryByName');
expect(cheFactory.fetchFactoryByName).not.toHaveBeenCalled();
});
describe('change name to unique> ', () => {
it ('the form should be valid >', () => {
myForm.name.$setViewValue(uniqueName);
// flush HTTP backend
$httpBackend.flush();
// check form (expect valid)
expect(myForm.name.$invalid).toBe(false);
expect(myForm.name.$valid).toBe(true);
});
it('the HTTP request should be made >', () => {
$httpBackend.expectGET(`/api/factory/find?creator.userId=${userId}&name=${uniqueName}`);
myForm.name.$setViewValue(uniqueName);
// flush HTTP backend
$httpBackend.flush();
});
});
});
describe('initially factory has a non-unique name, factory is not downloaded yet >', () => {
beforeEach(() => {
$rootScope.factoryName = factoryName2;
const element = angular.element(`
<form name="myForm">
<input ng-model="factoryName" name="name" unique-factory-name="uniqueFactoryName" />
</form>
`);
$compile(element)($rootScope);
myForm = $rootScope.myForm as ITestFormController;
});
it('the form should be invalid >', () => {
// flush HTTP backend
$httpBackend.flush();
// check form (expect invalid)
expect(myForm.name.$invalid).toBe(true);
expect(myForm.name.$valid).toBe(false);
});
it('the HTTP request should be made >', () => {
$httpBackend.expectGET(`/api/factory/find?creator.userId=${userId}&name=${factoryName2}`);
// flush HTTP backend
$httpBackend.flush();
});
describe('change name to unique> ', () => {
it ('the form should be valid >', () => {
myForm.name.$setViewValue(uniqueName);
// flush HTTP backend
$httpBackend.flush();
// check form (expect valid)
expect(myForm.name.$invalid).toBe(false);
expect(myForm.name.$valid).toBe(true);
});
it('the HTTP request should be made >', () => {
$httpBackend.expectGET(`/api/factory/find?creator.userId=${userId}&name=${uniqueName}`);
myForm.name.$setViewValue(uniqueName);
// flush HTTP backend
$httpBackend.flush();
});
});
});
});
});

View File

@ -18,18 +18,20 @@ import {UniqueStackNameValidator} from './unique-stack-name-validator.directive'
import {CityNameValidator} from './city-name-validator.directive';
import {CustomAsyncValidator} from './custom-async-validator.directive';
import {UniqueTeamNameValidator} from './unique-team-name-validator.directive';
import {UniqueFactoryNameValidator} from './unique-factory-name-validator.directive';
export class ValidatorConfig {
constructor(register: che.IRegisterService) {
register.directive('gitUrl', GitUrlValidator)
.directive('cityNameValidator', CityNameValidator)
.directive('uniqueProjectName', UniqueProjectNameValidator)
.directive('uniqueWorkspaceName', UniqueWorkspaceNameValidator)
.directive('customValidator', CustomValidator)
.directive('customAsyncValidator', CustomAsyncValidator)
.directive('uniqueStackName', UniqueStackNameValidator)
.directive('uniqueTeamName', UniqueTeamNameValidator);
register.directive('gitUrl', GitUrlValidator);
register.directive('cityNameValidator', CityNameValidator);
register.directive('uniqueProjectName', UniqueProjectNameValidator);
register.directive('uniqueWorkspaceName', UniqueWorkspaceNameValidator);
register.directive('customValidator', CustomValidator);
register.directive('customAsyncValidator', CustomAsyncValidator);
register.directive('uniqueStackName', UniqueStackNameValidator);
register.directive('uniqueTeamName', UniqueTeamNameValidator);
register.directive('uniqueFactoryName', UniqueFactoryNameValidator);
}
}

View File

@ -22,6 +22,7 @@ interface ICheInputAttrs extends ng.IAttributes {
cheReadonly: string;
cheDisabled: string;
cheWidth: string;
ngChange: string;
}
/**
@ -53,13 +54,12 @@ export class CheInput implements ng.IDirective {
placeHolder: '@chePlaceHolder',
pattern: '@chePattern',
myForm: '=cheForm',
isChanged: '&ngChange',
isChanged: '&?ngChange',
readonly: '=cheReadonly',
disabled: '=cheDisabled'
};
}
/**
* Template for the current toolbar
* @param element {ng.IAugmentedJQuery}
@ -85,6 +85,9 @@ export class CheInput implements ng.IDirective {
if (attrs.cheDisabled) {
template = template + ' ng-disabled="disabled"';
}
if (attrs.ngChange) {
template = template + ' ng-change="isChanged({$value: valueModel})" ';
}
template = template + ' ng-trim="false" ng-model="valueModel" >'
+ '<md-icon class="fa fa-pencil che-input-icon che-input-icon-xs"></md-icon>'
+ '<!-- display error messages for the form -->'
@ -107,6 +110,9 @@ export class CheInput implements ng.IDirective {
if (attrs.cheDisabled) {
template = template + ' ng-disabled="disabled"';
}
if (attrs.ngChange) {
template = template + ' ng-change="isChanged({$value: valueModel})" ';
}
template = template + '><md-icon class="fa fa-pencil che-input-icon"></md-icon>';
if (attrs.cheWidth === 'auto') {
template = template + '<div class="che-input-desktop-hidden-text">{{valueModel ? valueModel : placeHolder}}</div>';
@ -191,17 +197,5 @@ export class CheInput implements ng.IDirective {
element.removeClass(desktopPristineClass);
}
});
const ngChange = 'ngChange';
if (!attrs.$attr[ngChange]) {
return;
}
// for ngChange attribute only
$scope.$watch(() => {
return $scope.valueModel;
}, () => {
$scope.isChanged();
});
}
}