http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/backend/services/clusters.js ---------------------------------------------------------------------- diff --git a/modules/web-console/backend/services/clusters.js b/modules/web-console/backend/services/clusters.js index 49e6f09..0cc2b9f 100644 --- a/modules/web-console/backend/services/clusters.js +++ b/modules/web-console/backend/services/clusters.js @@ -23,16 +23,19 @@ const _ = require('lodash'); module.exports = { implements: 'services/clusters', - inject: ['mongo', 'services/spaces', 'errors'] + inject: ['mongo', 'services/spaces', 'services/caches', 'services/domains', 'services/igfss', 'errors'] }; /** * @param mongo * @param {SpacesService} spacesService + * @param {CachesService} cachesService + * @param {DomainsService} modelsService + * @param {IgfssService} igfssService * @param errors * @returns {ClustersService} */ -module.exports.factory = (mongo, spacesService, errors) => { +module.exports.factory = (mongo, spacesService, cachesService, modelsService, igfssService, errors) => { /** * Convert remove status operation to own presentation. * @@ -71,17 +74,17 @@ module.exports.factory = (mongo, spacesService, errors) => { */ const create = (cluster) => { return mongo.Cluster.create(cluster) - .then((savedCluster) => - mongo.Cache.update({_id: {$in: savedCluster.caches}}, {$addToSet: {clusters: savedCluster._id}}, {multi: true}).exec() - .then(() => mongo.Igfs.update({_id: {$in: savedCluster.igfss}}, {$addToSet: {clusters: savedCluster._id}}, {multi: true}).exec()) - .then(() => savedCluster) - ) .catch((err) => { if (err.code === mongo.errCodes.DUPLICATE_KEY_ERROR) - throw new errors.DuplicateKeyException('Cluster with name: "' + cluster.name + '" already exist.'); + throw new errors.DuplicateKeyException(`Cluster with name: "${cluster.name}" already exist.`); else throw err; - }); + }) + .then((savedCluster) => + mongo.Cache.update({_id: {$in: savedCluster.caches}}, {$addToSet: {clusters: savedCluster._id}}, {multi: true}).exec() + .then(() => mongo.Igfs.update({_id: {$in: savedCluster.igfss}}, {$addToSet: {clusters: savedCluster._id}}, {multi: true}).exec()) + .then(() => savedCluster) + ); }; /** @@ -97,6 +100,110 @@ module.exports.factory = (mongo, spacesService, errors) => { }; class ClustersService { + static shortList(userId, demo) { + return spacesService.spaceIds(userId, demo) + .then((spaceIds) => mongo.Cluster.find({space: {$in: spaceIds}}).select('name discovery.kind caches models igfss').lean().exec()) + .then((clusters) => _.map(clusters, (cluster) => ({ + _id: cluster._id, + name: cluster.name, + discovery: cluster.discovery.kind, + cachesCount: _.size(cluster.caches), + modelsCount: _.size(cluster.models), + igfsCount: _.size(cluster.igfss) + }))); + } + + static get(userId, demo, _id) { + return spacesService.spaceIds(userId, demo) + .then((spaceIds) => mongo.Cluster.findOne({space: {$in: spaceIds}, _id}).lean().exec()); + } + + static normalize(spaceId, cluster, ...models) { + cluster.space = spaceId; + + _.forEach(models, (model) => { + _.forEach(model, (item) => { + item.space = spaceId; + item.clusters = [cluster._id]; + }); + }); + } + + static removedInCluster(oldCluster, newCluster, field) { + return _.difference(_.invokeMap(_.get(oldCluster, field), 'toString'), _.get(newCluster, field)); + } + + static upsertBasic(userId, demo, {cluster, caches}) { + if (_.isNil(cluster._id)) + return Promise.reject(new errors.IllegalArgumentException('Cluster id can not be undefined or null')); + + return spacesService.spaceIds(userId, demo) + .then((spaceIds) => { + this.normalize(_.head(spaceIds), cluster, caches); + + const query = _.pick(cluster, ['space', '_id']); + const basicCluster = _.pick(cluster, [ + 'space', + '_id', + 'name', + 'discovery', + 'caches', + 'memoryConfiguration.memoryPolicies', + 'dataStorageConfiguration.defaultDataRegionConfiguration.maxSize' + ]); + + return mongo.Cluster.findOneAndUpdate(query, {$set: basicCluster}, {projection: 'caches', upsert: true}).lean().exec() + .catch((err) => { + if (err.code === mongo.errCodes.DUPLICATE_KEY_ERROR) + throw new errors.DuplicateKeyException(`Cluster with name: "${cluster.name}" already exist.`); + + throw err; + }) + .then((oldCluster) => { + if (oldCluster) { + const ids = this.removedInCluster(oldCluster, cluster, 'caches'); + + return cachesService.remove(ids); + } + + cluster.caches = _.map(caches, '_id'); + + return mongo.Cluster.update(query, {$set: cluster, new: true}, {upsert: true}).exec(); + }); + }) + .then(() => _.map(caches, cachesService.upsertBasic)) + .then(() => ({rowsAffected: 1})); + } + + static upsert(userId, demo, {cluster, caches, models, igfss}) { + if (_.isNil(cluster._id)) + return Promise.reject(new errors.IllegalArgumentException('Cluster id can not be undefined or null')); + + return spacesService.spaceIds(userId, demo) + .then((spaceIds) => { + this.normalize(_.head(spaceIds), cluster, caches, models, igfss); + + const query = _.pick(cluster, ['space', '_id']); + + return mongo.Cluster.findOneAndUpdate(query, {$set: cluster}, {projection: {models: 1, caches: 1, igfss: 1}, upsert: true}).lean().exec() + .catch((err) => { + if (err.code === mongo.errCodes.DUPLICATE_KEY_ERROR) + throw new errors.DuplicateKeyException(`Cluster with name: "${cluster.name}" already exist.`); + + throw err; + }) + .then((oldCluster) => { + const modelIds = this.removedInCluster(oldCluster, cluster, 'models'); + const cacheIds = this.removedInCluster(oldCluster, cluster, 'caches'); + const igfsIds = this.removedInCluster(oldCluster, cluster, 'igfss'); + + return Promise.all([modelsService.remove(modelIds), cachesService.remove(cacheIds), igfssService.remove(igfsIds)]); + }); + }) + .then(() => Promise.all(_.concat(_.map(models, modelsService.upsert), _.map(caches, cachesService.upsert), _.map(igfss, igfssService.upsert)))) + .then(() => ({rowsAffected: 1})); + } + /** * Create or update cluster. * @@ -121,19 +228,31 @@ module.exports.factory = (mongo, spacesService, errors) => { } /** - * Remove cluster. + * Remove clusters. * - * @param {mongo.ObjectId|String} clusterId - The cluster id for remove. + * @param {Array.<String>|String} ids - The cluster ids for remove. * @returns {Promise.<{rowsAffected}>} - The number of affected rows. */ - static remove(clusterId) { - if (_.isNil(clusterId)) + static remove(ids) { + if (_.isNil(ids)) return Promise.reject(new errors.IllegalArgumentException('Cluster id can not be undefined or null')); - return mongo.Cache.update({clusters: {$in: [clusterId]}}, {$pull: {clusters: clusterId}}, {multi: true}).exec() - .then(() => mongo.Igfs.update({clusters: {$in: [clusterId]}}, {$pull: {clusters: clusterId}}, {multi: true}).exec()) - .then(() => mongo.Cluster.remove({_id: clusterId}).exec()) - .then(convertRemoveStatus); + if (_.isEmpty(ids)) + return Promise.resolve({rowsAffected: 0}); + + ids = _.castArray(ids); + + return Promise.all(_.map(ids, (id) => { + return mongo.Cluster.findByIdAndRemove(id).exec() + .then((cluster) => { + return Promise.all([ + mongo.DomainModel.remove({_id: {$in: cluster.models}}).exec(), + mongo.Cache.remove({_id: {$in: cluster.caches}}).exec(), + mongo.Igfs.remove({_id: {$in: cluster.igfss}}).exec() + ]); + }); + })) + .then(() => ({rowsAffected: ids.length})); } /**
http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/backend/services/configurations.js ---------------------------------------------------------------------- diff --git a/modules/web-console/backend/services/configurations.js b/modules/web-console/backend/services/configurations.js index 36d9932..da431c3 100644 --- a/modules/web-console/backend/services/configurations.js +++ b/modules/web-console/backend/services/configurations.js @@ -52,6 +52,18 @@ module.exports.factory = (mongo, spacesService, clustersService, cachesService, ])) .then(([clusters, domains, caches, igfss]) => ({clusters, domains, caches, igfss, spaces})); } + + static get(userId, demo, _id) { + return clustersService.get(userId, demo, _id) + .then((cluster) => + Promise.all([ + mongo.Cache.find({space: cluster.space, _id: {$in: cluster.caches}}).lean().exec(), + mongo.DomainModel.find({space: cluster.space, _id: {$in: cluster.models}}).lean().exec(), + mongo.Igfs.find({space: cluster.space, _id: {$in: cluster.igfss}}).lean().exec() + ]) + .then(([caches, models, igfss]) => ({cluster, caches, models, igfss})) + ); + } } return ConfigurationsService; http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/backend/services/domains.js ---------------------------------------------------------------------- diff --git a/modules/web-console/backend/services/domains.js b/modules/web-console/backend/services/domains.js index 986991d..ba3a6a5 100644 --- a/modules/web-console/backend/services/domains.js +++ b/modules/web-console/backend/services/domains.js @@ -149,6 +149,87 @@ module.exports.factory = (mongo, spacesService, cachesService, errors) => { }; class DomainsService { + static shortList(userId, demo, clusterId) { + return spacesService.spaceIds(userId, demo) + .then((spaceIds) => { + const sIds = _.map(spaceIds, (spaceId) => mongo.ObjectId(spaceId)); + + return mongo.DomainModel.aggregate([ + {$match: {space: {$in: sIds}, clusters: mongo.ObjectId(clusterId)}}, + {$project: { + keyType: 1, + valueType: 1, + queryMetadata: 1, + hasIndex: { + $or: [ + { + $and: [ + {$eq: ['$queryMetadata', 'Annotations']}, + { + $or: [ + {$eq: ['$generatePojo', false]}, + { + $and: [ + {$eq: ['$databaseSchema', '']}, + {$eq: ['$databaseTable', '']} + ] + } + ] + } + ] + }, + {$gt: [{$size: {$ifNull: ['$keyFields', []]}}, 0]} + ] + } + }} + ]).exec(); + }); + } + + static get(userId, demo, _id) { + return spacesService.spaceIds(userId, demo) + .then((spaceIds) => mongo.DomainModel.findOne({space: {$in: spaceIds}, _id}).lean().exec()); + } + + static upsert(model) { + if (_.isNil(model._id)) + return Promise.reject(new errors.IllegalArgumentException('Model id can not be undefined or null')); + + const query = _.pick(model, ['space', '_id']); + + return mongo.DomainModel.update(query, {$set: model}, {upsert: true}).exec() + .then(() => mongo.Cache.update({_id: {$in: model.caches}}, {$addToSet: {domains: model._id}}, {multi: true}).exec()) + .then(() => mongo.Cache.update({_id: {$nin: model.caches}}, {$pull: {domains: model._id}}, {multi: true}).exec()) + .then(() => _updateCacheStore(model.cacheStoreChanges)) + .catch((err) => { + if (err.code === mongo.errCodes.DUPLICATE_KEY_ERROR) + throw new errors.DuplicateKeyException(`Model with value type: "${model.valueType}" already exist.`); + + throw err; + }); + } + + /** + * Remove model. + * + * @param {mongo.ObjectId|String} ids - The model id for remove. + * @returns {Promise.<{rowsAffected}>} - The number of affected rows. + */ + static remove(ids) { + if (_.isNil(ids)) + return Promise.reject(new errors.IllegalArgumentException('Model id can not be undefined or null')); + + ids = _.castArray(ids); + + if (_.isEmpty(ids)) + return Promise.resolve({rowsAffected: 0}); + + return mongo.Cache.update({domains: {$in: ids}}, {$pull: {domains: ids}}, {multi: true}).exec() + .then(() => mongo.Cluster.update({models: {$in: ids}}, {$pull: {models: ids}}, {multi: true}).exec()) + .then(() => mongo.DomainModel.remove({_id: {$in: ids}}).exec()) + .then(convertRemoveStatus); + } + /** * Batch merging domains. * @@ -169,21 +250,6 @@ module.exports.factory = (mongo, spacesService, cachesService, errors) => { } /** - * Remove domain. - * - * @param {mongo.ObjectId|String} domainId - The domain id for remove. - * @returns {Promise.<{rowsAffected}>} - The number of affected rows. - */ - static remove(domainId) { - if (_.isNil(domainId)) - return Promise.reject(new errors.IllegalArgumentException('Domain id can not be undefined or null')); - - return mongo.Cache.update({domains: {$in: [domainId]}}, {$pull: {domains: domainId}}, {multi: true}).exec() - .then(() => mongo.DomainModel.remove({_id: domainId}).exec()) - .then(convertRemoveStatus); - } - - /** * Remove all domains by user. * @param {mongo.ObjectId|String} userId - The user id that own domain. * @param {Boolean} demo - The flag indicates that need lookup in demo space. http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/backend/services/igfss.js ---------------------------------------------------------------------- diff --git a/modules/web-console/backend/services/igfss.js b/modules/web-console/backend/services/igfss.js index 5296f16..b75d677 100644 --- a/modules/web-console/backend/services/igfss.js +++ b/modules/web-console/backend/services/igfss.js @@ -93,6 +93,31 @@ module.exports.factory = (mongo, spacesService, errors) => { }; class IgfssService { + static shortList(userId, demo, clusterId) { + return spacesService.spaceIds(userId, demo) + .then((spaceIds) => mongo.Igfs.find({space: {$in: spaceIds}, clusters: clusterId }).select('name defaultMode affinnityGroupSize').lean().exec()); + } + + static get(userId, demo, _id) { + return spacesService.spaceIds(userId, demo) + .then((spaceIds) => mongo.Igfs.findOne({space: {$in: spaceIds}, _id}).lean().exec()); + } + + static upsert(igfs) { + if (_.isNil(igfs._id)) + return Promise.reject(new errors.IllegalArgumentException('IGFS id can not be undefined or null')); + + const query = _.pick(igfs, ['space', '_id']); + + return mongo.Igfs.update(query, {$set: igfs}, {upsert: true}).exec() + .catch((err) => { + if (err.code === mongo.errCodes.DUPLICATE_KEY_ERROR) + throw new errors.DuplicateKeyException(`IGFS with name: "${igfs.name}" already exist.`); + + throw err; + }); + } + /** * Create or update IGFS. * @@ -117,22 +142,27 @@ module.exports.factory = (mongo, spacesService, errors) => { } /** - * Remove IGFS. + * Remove IGFSs. * - * @param {mongo.ObjectId|String} igfsId - The IGFS id for remove. + * @param {Array.<String>|String} ids - The IGFS ids for remove. * @returns {Promise.<{rowsAffected}>} - The number of affected rows. */ - static remove(igfsId) { - if (_.isNil(igfsId)) + static remove(ids) { + if (_.isNil(ids)) return Promise.reject(new errors.IllegalArgumentException('IGFS id can not be undefined or null')); - return mongo.Cluster.update({igfss: {$in: [igfsId]}}, {$pull: {igfss: igfsId}}, {multi: true}).exec() - // TODO WC-201 fix clenup on node filter on deletion for cluster serviceConfigurations and caches. + ids = _.castArray(ids); + + if (_.isEmpty(ids)) + return Promise.resolve({rowsAffected: 0}); + + return mongo.Cluster.update({igfss: {$in: ids}}, {$pull: {igfss: {$in: ids}}}, {multi: true}).exec() + // TODO WC-201 fix cleanup on node filter on deletion for cluster serviceConfigurations and caches. // .then(() => mongo.Cluster.update({ 'serviceConfigurations.$.nodeFilter.kind': { $ne: 'IGFS' }, 'serviceConfigurations.nodeFilter.IGFS.igfs': igfsId}, // {$unset: {'serviceConfigurations.$.nodeFilter.IGFS.igfs': ''}}, {multi: true}).exec()) // .then(() => mongo.Cluster.update({ 'serviceConfigurations.nodeFilter.kind': 'IGFS', 'serviceConfigurations.nodeFilter.IGFS.igfs': igfsId}, // {$unset: {'serviceConfigurations.$.nodeFilter': ''}}, {multi: true}).exec()) - .then(() => mongo.Igfs.remove({_id: igfsId}).exec()) + .then(() => mongo.Igfs.remove({_id: {$in: ids}}).exec()) .then(convertRemoveStatus); } http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/backend/services/sessions.js ---------------------------------------------------------------------- diff --git a/modules/web-console/backend/services/sessions.js b/modules/web-console/backend/services/sessions.js index 0518ce2..0ea851b 100644 --- a/modules/web-console/backend/services/sessions.js +++ b/modules/web-console/backend/services/sessions.js @@ -51,7 +51,7 @@ module.exports.factory = (mongo, errors) => { return new Promise((resolve) => { delete session.viewedUser; - resolve(); + resolve(true); }); } } http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/backend/services/spaces.js ---------------------------------------------------------------------- diff --git a/modules/web-console/backend/services/spaces.js b/modules/web-console/backend/services/spaces.js index 85f346e..fe62f77 100644 --- a/modules/web-console/backend/services/spaces.js +++ b/modules/web-console/backend/services/spaces.js @@ -57,7 +57,7 @@ module.exports.factory = (mongo, errors) => { */ static spaceIds(userId, demo) { return this.spaces(userId, demo) - .then((spaces) => spaces.map((space) => space._id)); + .then((spaces) => spaces.map((space) => space._id.toString())); } /** http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/backend/test/unit/CacheService.test.js ---------------------------------------------------------------------- diff --git a/modules/web-console/backend/test/unit/CacheService.test.js b/modules/web-console/backend/test/unit/CacheService.test.js index 304f62c..52936a0 100644 --- a/modules/web-console/backend/test/unit/CacheService.test.js +++ b/modules/web-console/backend/test/unit/CacheService.test.js @@ -21,7 +21,7 @@ const testCaches = require('../data/caches.json'); const testAccounts = require('../data/accounts.json'); const testSpaces = require('../data/spaces.json'); -let cacheService; +let cachesService; let mongo; let errors; let db; @@ -34,7 +34,7 @@ suite('CacheServiceTestsSuite', () => { injector('dbHelper')]) .then(([_cacheService, _mongo, _errors, _db]) => { mongo = _mongo; - cacheService = _cacheService; + cachesService = _cacheService; errors = _errors; db = _db; }); @@ -42,12 +42,24 @@ suite('CacheServiceTestsSuite', () => { setup(() => db.init()); + test('Get cache', (done) => { + const _id = testCaches[0]._id; + + cachesService.get(testCaches[0].space, false, _id) + .then((cache) => { + assert.isNotNull(cache); + assert.equal(cache._id, _id); + }) + .then(done) + .catch(done); + }); + test('Create new cache', (done) => { const dupleCache = Object.assign({}, testCaches[0], {name: 'Other name'}); delete dupleCache._id; - cacheService.merge(dupleCache) + cachesService.merge(dupleCache) .then((cache) => mongo.Cache.findById(cache._id)) .then((cache) => assert.isNotNull(cache)) .then(done) @@ -59,7 +71,7 @@ suite('CacheServiceTestsSuite', () => { const cacheBeforeMerge = Object.assign({}, testCaches[0], {name: newName}); - cacheService.merge(cacheBeforeMerge) + cachesService.merge(cacheBeforeMerge) .then((cache) => mongo.Cache.findById(cache._id)) .then((cacheAfterMerge) => assert.equal(cacheAfterMerge.name, newName)) .then(done) @@ -71,7 +83,7 @@ suite('CacheServiceTestsSuite', () => { delete dupleCache._id; - cacheService.merge(dupleCache) + cachesService.merge(dupleCache) .catch((err) => { assert.instanceOf(err, errors.DuplicateKeyException); @@ -80,7 +92,7 @@ suite('CacheServiceTestsSuite', () => { }); test('Remove existed cache', (done) => { - cacheService.remove(testCaches[0]._id) + cachesService.remove(testCaches[0]._id) .then(({rowsAffected}) => assert.equal(rowsAffected, 1) ) @@ -93,7 +105,7 @@ suite('CacheServiceTestsSuite', () => { }); test('Remove cache without identifier', (done) => { - cacheService.remove() + cachesService.remove() .catch((err) => { assert.instanceOf(err, errors.IllegalArgumentException); @@ -104,7 +116,7 @@ suite('CacheServiceTestsSuite', () => { test('Remove missed cache', (done) => { const validNoExistingId = 'FFFFFFFFFFFFFFFFFFFFFFFF'; - cacheService.remove(validNoExistingId) + cachesService.remove(validNoExistingId) .then(({rowsAffected}) => assert.equal(rowsAffected, 0) ) @@ -113,7 +125,7 @@ suite('CacheServiceTestsSuite', () => { }); test('Get all caches by space', (done) => { - cacheService.listBySpaces(testSpaces[0]._id) + cachesService.listBySpaces(testSpaces[0]._id) .then((caches) => assert.equal(caches.length, 5) ) @@ -122,7 +134,7 @@ suite('CacheServiceTestsSuite', () => { }); test('Remove all caches in space', (done) => { - cacheService.removeAll(testAccounts[0]._id, false) + cachesService.removeAll(testAccounts[0]._id, false) .then(({rowsAffected}) => assert.equal(rowsAffected, 5) ) @@ -130,6 +142,19 @@ suite('CacheServiceTestsSuite', () => { .catch(done); }); + test('List of all caches in cluster', (done) => { + cachesService.shortList(testAccounts[0]._id, false, testCaches[0].clusters[0]) + .then((caches) => { + assert.equal(caches.length, 2); + assert.isNotNull(caches[0]._id); + assert.isNotNull(caches[0].name); + assert.isNotNull(caches[0].cacheMode); + assert.isNotNull(caches[0].atomicityMode); + }) + .then(done) + .catch(done); + }); + test('Update linked entities on update cache', (done) => { // TODO IGNITE-3262 Add test. done(); http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/backend/test/unit/ClusterService.test.js ---------------------------------------------------------------------- diff --git a/modules/web-console/backend/test/unit/ClusterService.test.js b/modules/web-console/backend/test/unit/ClusterService.test.js index ed04c45..66c7cf1 100644 --- a/modules/web-console/backend/test/unit/ClusterService.test.js +++ b/modules/web-console/backend/test/unit/ClusterService.test.js @@ -15,13 +15,17 @@ * limitations under the License. */ +const _ = require('lodash'); const assert = require('chai').assert; const injector = require('../injector'); + const testClusters = require('../data/clusters.json'); +const testCaches = require('../data/caches.json'); const testAccounts = require('../data/accounts.json'); const testSpaces = require('../data/spaces.json'); let clusterService; +let cacheService; let mongo; let errors; let db; @@ -29,12 +33,14 @@ let db; suite('ClusterServiceTestsSuite', () => { suiteSetup(() => { return Promise.all([injector('services/clusters'), + injector('services/caches'), injector('mongo'), injector('errors'), injector('dbHelper')]) - .then(([_clusterService, _mongo, _errors, _db]) => { + .then(([_clusterService, _cacheService, _mongo, _errors, _db]) => { mongo = _mongo; clusterService = _clusterService; + cacheService = _cacheService; errors = _errors; db = _db; }); @@ -42,6 +48,18 @@ suite('ClusterServiceTestsSuite', () => { setup(() => db.init()); + test('Get cluster', (done) => { + const _id = testClusters[0]._id; + + clusterService.get(testClusters[0].space, false, _id) + .then((cluster) => { + assert.isNotNull(cluster); + assert.equal(cluster._id, _id); + }) + .then(done) + .catch(done); + }); + test('Create new cluster', (done) => { const dupleCluster = Object.assign({}, testClusters[0], {name: 'Other name'}); @@ -130,6 +148,219 @@ suite('ClusterServiceTestsSuite', () => { .catch(done); }); + test('List of all clusters in space', (done) => { + clusterService.shortList(testAccounts[0]._id, false) + .then((clusters) => { + assert.equal(clusters.length, 2); + + assert.equal(clusters[0].name, 'cluster-caches'); + assert.isNotNull(clusters[0].discovery); + assert.equal(clusters[0].cachesCount, 5); + assert.equal(clusters[0].modelsCount, 5); + assert.equal(clusters[0].igfsCount, 0); + + assert.equal(clusters[1].name, 'cluster-igfs'); + assert.isNotNull(clusters[1].discovery); + assert.equal(clusters[1].cachesCount, 2); + assert.equal(clusters[1].modelsCount, 5); + assert.equal(clusters[1].igfsCount, 1); + }) + .then(done) + .catch(done); + }); + + test('Create new cluster from basic', (done) => { + const cluster = _.head(testClusters); + const caches = _.filter(testCaches, ({_id}) => _.includes(cluster.caches, _id)); + + db.drop() + .then(() => Promise.all([mongo.Account.create(testAccounts), mongo.Space.create(testSpaces)])) + .then(() => clusterService.upsertBasic(testAccounts[0]._id, false, {cluster, caches})) + .then((output) => { + assert.isNotNull(output); + + assert.equal(output.n, 1); + }) + .then(() => clusterService.get(testAccounts[0]._id, false, cluster._id)) + .then((savedCluster) => { + assert.isNotNull(savedCluster); + + assert.equal(savedCluster._id, cluster._id); + assert.equal(savedCluster.name, cluster.name); + assert.notStrictEqual(savedCluster.caches, cluster.caches); + + assert.notStrictEqual(savedCluster, cluster); + }) + .then(() => cacheService.get(testAccounts[0]._id, false, caches[0]._id)) + .then((cb1) => { + assert.isNotNull(cb1); + }) + .then(() => cacheService.get(testAccounts[0]._id, false, caches[1]._id)) + .then((cb2) => { + assert.isNotNull(cb2); + }) + .then(done) + .catch(done); + }); + + // test('Create new cluster without space', (done) => { + // const cluster = _.cloneDeep(_.head(testClusters)); + // const caches = _.filter(testCaches, ({_id}) => _.includes(cluster.caches, _id)); + // + // delete cluster.space; + // + // db.drop() + // .then(() => Promise.all([mongo.Account.create(testAccounts), mongo.Space.create(testSpaces)])) + // .then(() => clusterService.upsertBasic(testAccounts[0]._id, false, {cluster, caches})) + // .then(() => done()) + // .catch(done); + // }); + + test('Create new cluster with duplicated name', (done) => { + const cluster = _.cloneDeep(_.head(testClusters)); + const caches = _.filter(testCaches, ({_id}) => _.includes(cluster.caches, _id)); + + cluster.name = _.last(testClusters).name; + + clusterService.upsertBasic(testAccounts[0]._id, false, {cluster, caches}) + .then(done) + .catch((err) => { + assert.instanceOf(err, errors.DuplicateKeyException); + + done(); + }); + }); + + test('Update cluster from basic', (done) => { + const cluster = _.cloneDeep(_.head(testClusters)); + + cluster.communication.tcpNoDelay = false; + cluster.igfss = []; + + cluster.memoryConfiguration = { + defaultMemoryPolicySize: 10, + memoryPolicies: [ + { + name: 'default', + maxSize: 100 + } + ] + }; + + cluster.caches = _.dropRight(cluster.caches, 1); + + const caches = _.filter(testCaches, ({_id}) => _.includes(cluster.caches, _id)); + + _.head(caches).cacheMode = 'REPLICATED'; + _.head(caches).readThrough = false; + + clusterService.upsertBasic(testAccounts[0]._id, false, {cluster, caches}) + .then(() => clusterService.get(testAccounts[0]._id, false, cluster._id)) + .then((savedCluster) => { + assert.isNotNull(savedCluster); + + assert.deepEqual(_.invokeMap(savedCluster.caches, 'toString'), cluster.caches); + + _.forEach(savedCluster.memoryConfiguration.memoryPolicies, (plc) => delete plc._id); + + assert.notExists(savedCluster.memoryConfiguration.defaultMemoryPolicySize); + assert.deepEqual(savedCluster.memoryConfiguration.memoryPolicies, cluster.memoryConfiguration.memoryPolicies); + + assert.notDeepEqual(_.invokeMap(savedCluster.igfss, 'toString'), cluster.igfss); + assert.notDeepEqual(savedCluster.communication, cluster.communication); + }) + .then(() => cacheService.get(testAccounts[0]._id, false, _.head(caches)._id)) + .then((cb1) => { + assert.isNotNull(cb1); + assert.equal(cb1.cacheMode, 'REPLICATED'); + assert.isTrue(cb1.readThrough); + }) + .then(() => cacheService.get(testAccounts[0]._id, false, _.head(testClusters).caches[1])) + .then((c2) => { + assert.isNotNull(c2); + assert.equal(c2.cacheMode, 'PARTITIONED'); + assert.isTrue(c2.readThrough); + }) + .then(done) + .catch(done); + }); + + test('Update cluster from basic with cache removing', (done) => { + const cluster = _.cloneDeep(_.head(testClusters)); + + const removedCache = _.head(cluster.caches); + const upsertedCache = _.last(cluster.caches); + + _.pull(cluster.caches, removedCache); + + const caches = _.filter(testCaches, ({_id}) => _.includes(cluster.caches, _id)); + + db.drop() + .then(() => Promise.all([mongo.Account.create(testAccounts), mongo.Space.create(testSpaces)])) + .then(() => clusterService.upsertBasic(testAccounts[0]._id, false, {cluster, caches})) + .then(() => cacheService.get(testAccounts[0]._id, false, removedCache)) + .then((cache) => { + assert.isNull(cache); + }) + .then(() => cacheService.get(testAccounts[0]._id, false, upsertedCache)) + .then((cache) => { + assert.isNotNull(cache); + + done(); + }) + .catch(done); + }); + + test('Update cluster from advanced with cache removing', (done) => { + const cluster = _.cloneDeep(_.head(testClusters)); + + cluster.communication.tcpNoDelay = false; + cluster.igfss = []; + + cluster.memoryConfiguration = { + defaultMemoryPolicySize: 10, + memoryPolicies: [ + { + name: 'default', + maxSize: 100 + } + ] + }; + + const removedCache = _.head(cluster.caches); + const upsertedCache = _.last(cluster.caches); + + _.pull(cluster.caches, removedCache); + + const caches = _.filter(testCaches, ({_id}) => _.includes(cluster.caches, _id)); + + clusterService.upsert(testAccounts[0]._id, false, {cluster, caches}) + .then(() => clusterService.get(testAccounts[0]._id, false, cluster._id)) + .then((savedCluster) => { + assert.isNotNull(savedCluster); + + assert.deepEqual(_.invokeMap(savedCluster.caches, 'toString'), cluster.caches); + + _.forEach(savedCluster.memoryConfiguration.memoryPolicies, (plc) => delete plc._id); + + assert.deepEqual(savedCluster.memoryConfiguration, cluster.memoryConfiguration); + + assert.deepEqual(_.invokeMap(savedCluster.igfss, 'toString'), cluster.igfss); + assert.deepEqual(savedCluster.communication, cluster.communication); + }) + .then(() => cacheService.get(testAccounts[0]._id, false, removedCache)) + .then((cache) => { + assert.isNull(cache); + }) + .then(() => cacheService.get(testAccounts[0]._id, false, upsertedCache)) + .then((cache) => { + assert.isNotNull(cache); + + done(); + }) + .catch(done); + }); + test('Update linked entities on update cluster', (done) => { // TODO IGNITE-3262 Add test. done(); http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/backend/test/unit/DomainService.test.js ---------------------------------------------------------------------- diff --git a/modules/web-console/backend/test/unit/DomainService.test.js b/modules/web-console/backend/test/unit/DomainService.test.js index c7cf149..e4c531d 100644 --- a/modules/web-console/backend/test/unit/DomainService.test.js +++ b/modules/web-console/backend/test/unit/DomainService.test.js @@ -150,6 +150,11 @@ suite('DomainsServiceTestsSuite', () => { .catch(done); }); + test('List of domains in cluster', (done) => { + // TODO IGNITE-5737 Add test. + done(); + }); + test('Update linked entities on update domain', (done) => { // TODO IGNITE-3262 Add test. done(); http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/e2e/testcafe/components/ListEditable.js ---------------------------------------------------------------------- diff --git a/modules/web-console/e2e/testcafe/components/ListEditable.js b/modules/web-console/e2e/testcafe/components/ListEditable.js new file mode 100644 index 0000000..acce0c6 --- /dev/null +++ b/modules/web-console/e2e/testcafe/components/ListEditable.js @@ -0,0 +1,83 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 {Selector, t} from 'testcafe' +import {FormField} from './FormField' + +const addItemButton = Selector(value => { + value = value(); + const innerButton = value.querySelector('.le-row:not(.ng-hide) list-editable-add-item-button [ng-click]'); + + if (innerButton) + return innerButton; + + /** @type {Element} */ + const outerButton = value.nextElementSibling; + + if (outerButton.getAttribute('ng-click') === '$ctrl.addItem()') + return outerButton; +}); + +export class ListEditableItem { + /** + * @param {Selector} selector + * @param {Object.<string, {id: string}>} fieldsMap + */ + constructor(selector, fieldsMap = {}) { + this._selector = selector; + this._fieldsMap = fieldsMap; + /** @type {SelectorAPI} */ + this.editView = this._selector.find('list-editable-item-edit'); + /** @type {SelectorAPI} */ + this.itemView = this._selector.find('list-editable-item-view'); + /** @type {Object.<string, FormField>} Inline form fields */ + this.fields = Object.keys(fieldsMap).reduce((acc, key) => ({...acc, [key]: new FormField(this._fieldsMap[key])}), {}) + } + async startEdit() { + await t.click(this.itemView) + } + async stopEdit() { + await t.click('.wrapper') + } + /** + * @param {number} index + */ + getItemViewColumn(index) { + return this.itemView.child(index) + } +} + +export class ListEditable { + static ADD_ITEM_BUTTON_SELECTOR = '[ng-click="$ctrl.addItem()"]'; + /** @param {SelectorAPI} selector */ + constructor(selector, fieldsMap) { + this._selector = selector; + this._fieldsMap = fieldsMap; + this.addItemButton = Selector(addItemButton(selector)) + } + + async addItem() { + await t.click(this.addItemButton) + } + + /** + * @param {number} index Zero-based index of item in the list + */ + getItem(index) { + return new ListEditableItem(this._selector.find(`.le-body>.le-row[ng-repeat]`).nth(index), this._fieldsMap) + } +} http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/e2e/testcafe/components/Table.js ---------------------------------------------------------------------- diff --git a/modules/web-console/e2e/testcafe/components/Table.js b/modules/web-console/e2e/testcafe/components/Table.js new file mode 100644 index 0000000..e690599 --- /dev/null +++ b/modules/web-console/e2e/testcafe/components/Table.js @@ -0,0 +1,56 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 {Selector, t} from 'testcafe' + +const findCell = Selector((table, rowIndex, columnLabel) => { + table = table(); + + const columnIndex = [].constructor.from( + table.querySelectorAll('.ui-grid-header-cell:not(.ui-grid-header-span)'), + e => e.textContent + ).findIndex(t => t.includes(columnLabel)); + + const row = table.querySelector(`.ui-grid-render-container:not(.left) .ui-grid-viewport .ui-grid-row:nth-of-type(${rowIndex+1})`); + const cell = row.querySelector(`.ui-grid-cell:nth-of-type(${columnIndex})`); + return cell; +}); + +export class Table { + constructor(selector) { + this._selector = selector; + this.title = this._selector.find('.panel-title'); + this.actionsButton = this._selector.find('.btn-ignite').withText('Actions'); + this.allItemsCheckbox = this._selector.find('[role="checkbox button"]') + } + + async performAction(label) { + await t.hover(this.actionsButton).click(Selector('.dropdown-menu a').withText(label)) + } + + /** + * Toggles grid row selection + * @param {number} index Index of row, starting with 1 + */ + async toggleRowSelection(index) { + await t.click(this._selector.find(`.ui-grid-pinned-container .ui-grid-row:nth-of-type(${index}) .ui-grid-selection-row-header-buttons`)) + } + + findCell(rowIndex, columnLabel) { + return Selector(findCell(this._selector, rowIndex, columnLabel)) + } +} http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/e2e/testcafe/components/pageAdvancedConfiguration.js ---------------------------------------------------------------------- diff --git a/modules/web-console/e2e/testcafe/components/pageAdvancedConfiguration.js b/modules/web-console/e2e/testcafe/components/pageAdvancedConfiguration.js new file mode 100644 index 0000000..eabd337 --- /dev/null +++ b/modules/web-console/e2e/testcafe/components/pageAdvancedConfiguration.js @@ -0,0 +1,39 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 {Selector, t} from 'testcafe' + +export const pageAdvancedConfiguration = { + saveButton: Selector('.pc-form-actions-panel .btn-ignite').withText('Save'), + clusterNavButton: Selector('.pca-menu-link[ui-sref="base.configuration.edit.advanced.cluster"]'), + modelsNavButton: Selector('.pca-menu-link[ui-sref="base.configuration.edit.advanced.models"]'), + cachesNavButton: Selector('.pca-menu-link[ui-sref="base.configuration.edit.advanced.caches"]'), + igfsNavButton: Selector('.pca-menu-link[ui-sref="base.configuration.edit.advanced.igfs"]'), + async save() { + await t.click(this.saveButton) + } +}; + +export class Panel { + constructor(title) { + this._selector = Selector('.pca-panel-heading-title').withText(title).parent('.pca-panel'); + this.heading = this._selector.find('.pca-panel-heading') + this.body = this._selector.find('.pca-panel-collapse').addCustomDOMProperties({ + isOpened: el => el.classList.contains('in') + }) + } +} http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/e2e/testcafe/components/pageConfiguration.js ---------------------------------------------------------------------- diff --git a/modules/web-console/e2e/testcafe/components/pageConfiguration.js b/modules/web-console/e2e/testcafe/components/pageConfiguration.js new file mode 100644 index 0000000..c364208 --- /dev/null +++ b/modules/web-console/e2e/testcafe/components/pageConfiguration.js @@ -0,0 +1,21 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 {Selector} from 'testcafe' + +export const basicNavButton = Selector('.tabs.tabs--blue a[ui-sref="base.configuration.edit.basic"]'); +export const advancedNavButton = Selector('.tabs.tabs--blue a[ui-sref="base.configuration.edit.advanced.cluster"]'); http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/e2e/testcafe/fixtures/configuration/basic.js ---------------------------------------------------------------------- diff --git a/modules/web-console/e2e/testcafe/fixtures/configuration/basic.js b/modules/web-console/e2e/testcafe/fixtures/configuration/basic.js new file mode 100644 index 0000000..090fd0a --- /dev/null +++ b/modules/web-console/e2e/testcafe/fixtures/configuration/basic.js @@ -0,0 +1,89 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 {Selector, Role} from 'testcafe'; +import {dropTestDB, insertTestUser, resolveUrl} from '../../envtools'; +import {createRegularUser} from '../../roles'; +import {PageConfigurationBasic} from '../../page-models/PageConfigurationBasic'; +import {successNotification} from '../../components/notifications'; + +const regularUser = createRegularUser(); + +fixture('Basic configuration') + .before(async(t) => { + await dropTestDB(); + await insertTestUser(); + }) + .beforeEach(async(t) => { + await t + .useRole(regularUser) + .navigateTo(resolveUrl('/configuration/new/basic')); + }) + .after(dropTestDB); + +test('Off-heap size visibility for different Ignite versions', async(t) => { + const page = new PageConfigurationBasic(); + const ignite2 = 'Ignite 2.4'; + const ignite1 = 'Ignite 1.x'; + + await page.versionPicker.pickVersion(ignite2); + await t.expect(page.totalOffheapSizeInput.exists).ok('Visible in latest 2.x version'); + await page.versionPicker.pickVersion(ignite1); + await t.expect(page.totalOffheapSizeInput.count).eql(0, 'Invisible in Ignite 1.x'); +}); + +test('Default form action', async(t) => { + const page = new PageConfigurationBasic(); + + await t + .expect(page.mainFormAction.textContent) + .eql(PageConfigurationBasic.SAVE_CHANGES_AND_DOWNLOAD_LABEL); +}); + +test('Basic editing', async(t) => { + const page = new PageConfigurationBasic(); + const clusterName = 'Test basic cluster #1'; + const localMode = 'LOCAL'; + const atomic = 'ATOMIC'; + + await t + .expect(page.buttonPreviewProject.visible).notOk('Preview project button is hidden for new cluster configs') + .expect(page.buttonDownloadProject.visible).notOk('Download project button is hidden for new cluster configs') + .typeText(page.clusterNameInput.control, clusterName, {replace: true}); + await page.cachesList.addItem(); + await page.cachesList.addItem(); + await page.cachesList.addItem(); + + const cache1 = page.cachesList.getItem(1); + await cache1.startEdit(); + await t.typeText(cache1.fields.name.control, 'Foobar'); + await cache1.fields.cacheMode.selectOption(localMode); + await cache1.fields.atomicityMode.selectOption(atomic); + await cache1.stopEdit(); + + await t.expect(cache1.getItemViewColumn(0).textContent).contains(`Cache1Foobar`, 'Can edit cache name'); + await t.expect(cache1.getItemViewColumn(1).textContent).eql(localMode, 'Can edit cache mode'); + await t.expect(cache1.getItemViewColumn(2).textContent).eql(atomic, 'Can edit cache atomicity'); + + // TODO IGNITE-8094: restore to save method call. + await page.saveWithoutDownload(); + await t + .expect(successNotification.visible).ok('Shows success notifications') + .expect(successNotification.textContent).contains(`Cluster "${clusterName}" saved.`, 'Success notification has correct text', {timeout: 500}); + await t.eval(() => window.location.reload()); + await t.expect(page.pageHeader.textContent).contains(`Edit cluster configuration â${clusterName}â`); +}); http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/e2e/testcafe/fixtures/configuration/overview.js ---------------------------------------------------------------------- diff --git a/modules/web-console/e2e/testcafe/fixtures/configuration/overview.js b/modules/web-console/e2e/testcafe/fixtures/configuration/overview.js new file mode 100644 index 0000000..f0495bd --- /dev/null +++ b/modules/web-console/e2e/testcafe/fixtures/configuration/overview.js @@ -0,0 +1,147 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 {Selector} from 'testcafe'; +import {getLocationPathname} from '../../helpers'; +import {dropTestDB, insertTestUser, resolveUrl} from '../../envtools'; +import {createRegularUser} from '../../roles'; +import {PageConfigurationOverview} from '../../page-models/PageConfigurationOverview'; +import {PageConfigurationBasic} from '../../page-models/PageConfigurationBasic'; +import * as pageConfiguration from '../../components/pageConfiguration'; +import {pageAdvancedConfiguration} from '../../components/pageAdvancedConfiguration'; +import {PageConfigurationAdvancedCluster} from '../../page-models/PageConfigurationAdvancedCluster'; +import {confirmation} from '../../components/confirmation'; +import {successNotification} from '../../components/notifications'; +import * as models from '../../page-models/pageConfigurationAdvancedModels'; +import * as igfs from '../../page-models/pageConfigurationAdvancedIGFS'; +import {configureNavButton} from '../../components/topNavigation'; + +const regularUser = createRegularUser(); + +const repeat = (times, fn) => [...Array(times).keys()].reduce((acc, i) => acc.then(() => fn(i)), Promise.resolve()); + +fixture('Configuration overview') + .before(async(t) => { + await dropTestDB(); + await insertTestUser(); + }) + .beforeEach(async(t) => { + await t.useRole(regularUser).navigateTo(resolveUrl(`/configuration/overview`)); + }) + .after(dropTestDB); + +const overviewPage = new PageConfigurationOverview(); +const basicConfigPage = new PageConfigurationBasic(); +const advancedConfigPage = new PageConfigurationAdvancedCluster(); + +test('Create cluster basic/advanced clusters amount redirect', async(t) => { + const clustersAmountThershold = 10; + + await repeat(clustersAmountThershold + 2, async(i) => { + await t.click(overviewPage.createClusterConfigButton); + + if (i <= clustersAmountThershold) { + await t.expect(getLocationPathname()).contains('basic', 'Opens basic'); + await basicConfigPage.saveWithoutDownload(); + } else { + await t.expect(getLocationPathname()).contains('advanced', 'Opens advanced'); + await advancedConfigPage.save(); + } + + await t.click(configureNavButton); + }); + await overviewPage.removeAllItems(); +}); + + +test('Cluster edit basic/advanced redirect based on caches amount', async(t) => { + const clusterName = 'Seven caches cluster'; + const clusterEditLink = overviewPage.clustersTable.findCell(0, 'Name').find('a'); + const cachesAmountThreshold = 5; + + await t.click(overviewPage.createClusterConfigButton); + await repeat(cachesAmountThreshold, () => basicConfigPage.cachesList.addItem()); + await basicConfigPage.saveWithoutDownload(); + await t + .click(configureNavButton) + .click(clusterEditLink) + .expect(getLocationPathname()).contains('basic', `Opens basic with ${cachesAmountThreshold} caches`); + await basicConfigPage.cachesList.addItem(); + await basicConfigPage.saveWithoutDownload(); + await t + .click(configureNavButton) + .click(clusterEditLink) + .expect(getLocationPathname()).contains('advanced', `Opens advanced with ${cachesAmountThreshold + 1} caches`); + await t.click(configureNavButton); + await overviewPage.removeAllItems(); +}); + +test('Cluster removal', async(t) => { + const name = 'FOO bar BAZ'; + + await t + .click(overviewPage.createClusterConfigButton) + .typeText(basicConfigPage.clusterNameInput.control, name, {replace: true}); + await basicConfigPage.saveWithoutDownload(); + await t.click(configureNavButton); + await overviewPage.clustersTable.toggleRowSelection(1); + await overviewPage.clustersTable.performAction('Delete'); + await t.expect(confirmation.body.textContent).contains(name, 'Lists cluster names in remove confirmation'); + await confirmation.confirm(); + await t.expect(successNotification.textContent).contains('Cluster(s) removed: 1', 'Shows cluster removal notification'); +}); + +test('Cluster cell values', async(t) => { + const name = 'Non-empty cluster config'; + const staticDiscovery = 'Static IPs'; + const cachesAmount = 3; + const modelsAmount = 2; + const igfsAmount = 1; + + await t + .click(overviewPage.createClusterConfigButton) + .typeText(basicConfigPage.clusterNameInput.control, name, {replace: true}); + await basicConfigPage.clusterDiscoveryInput.selectOption(staticDiscovery); + await repeat(cachesAmount, () => basicConfigPage.cachesList.addItem()); + await basicConfigPage.saveWithoutDownload(); + await t + .click(pageConfiguration.advancedNavButton) + .click(pageAdvancedConfiguration.modelsNavButton); + await repeat(modelsAmount, async(i) => { + await t + .click(models.createModelButton) + .click(models.general.generatePOJOClasses.control); + await models.general.queryMetadata.selectOption('Annotations'); + await t + .typeText(models.general.keyType.control, `foo${i}`) + .typeText(models.general.valueType.control, `bar${i}`) + .click(pageAdvancedConfiguration.saveButton); + }); + await t.click(pageAdvancedConfiguration.igfsNavButton); + await repeat(igfsAmount, async() => { + await t + .click(igfs.createIGFSButton) + .click(pageAdvancedConfiguration.saveButton); + }); + await t + .click(configureNavButton) + .expect(overviewPage.clustersTable.findCell(0, 'Name').textContent).contains(name) + .expect(overviewPage.clustersTable.findCell(0, 'Discovery').textContent).contains(staticDiscovery) + .expect(overviewPage.clustersTable.findCell(0, 'Caches').textContent).contains(cachesAmount) + .expect(overviewPage.clustersTable.findCell(0, 'Models').textContent).contains(modelsAmount) + .expect(overviewPage.clustersTable.findCell(0, 'IGFS').textContent).contains(igfsAmount); +}); http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/e2e/testcafe/fixtures/menu-smoke.js ---------------------------------------------------------------------- diff --git a/modules/web-console/e2e/testcafe/fixtures/menu-smoke.js b/modules/web-console/e2e/testcafe/fixtures/menu-smoke.js index fea019b..fdd0edb 100644 --- a/modules/web-console/e2e/testcafe/fixtures/menu-smoke.js +++ b/modules/web-console/e2e/testcafe/fixtures/menu-smoke.js @@ -39,7 +39,7 @@ test('Ingite main menu smoke test', async(t) => { await t .click(configureNavButton) .expect(Selector('title').innerText) - .eql('Basic Configuration â Apache Ignite Web Console'); + .eql('Configuration â Apache Ignite Web Console'); await t .click(queriesNavButton) http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/e2e/testcafe/package.json ---------------------------------------------------------------------- diff --git a/modules/web-console/e2e/testcafe/package.json b/modules/web-console/e2e/testcafe/package.json index a6e5b0d..2501102 100644 --- a/modules/web-console/e2e/testcafe/package.json +++ b/modules/web-console/e2e/testcafe/package.json @@ -39,7 +39,7 @@ "objectid": "3.2.1", "path": "0.12.7", "sinon": "2.3.8", - "testcafe": "0.18.5", + "testcafe": "^0.19.0", "testcafe-angular-selectors": "0.3.0", "testcafe-reporter-teamcity": "1.0.9", "type-detect": "4.0.3", http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/e2e/testcafe/page-models/PageConfigurationAdvancedCluster.js ---------------------------------------------------------------------- diff --git a/modules/web-console/e2e/testcafe/page-models/PageConfigurationAdvancedCluster.js b/modules/web-console/e2e/testcafe/page-models/PageConfigurationAdvancedCluster.js new file mode 100644 index 0000000..0f62707 --- /dev/null +++ b/modules/web-console/e2e/testcafe/page-models/PageConfigurationAdvancedCluster.js @@ -0,0 +1,28 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 {Selector, t} from 'testcafe' + +export class PageConfigurationAdvancedCluster { + constructor() { + this._selector = Selector('page-configure-advanced-cluster') + this.saveButton = Selector('.pc-form-actions-panel .btn-ignite').withText('Save') + } + async save() { + await t.click(this.saveButton) + } +} http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/e2e/testcafe/page-models/PageConfigurationBasic.js ---------------------------------------------------------------------- diff --git a/modules/web-console/e2e/testcafe/page-models/PageConfigurationBasic.js b/modules/web-console/e2e/testcafe/page-models/PageConfigurationBasic.js new file mode 100644 index 0000000..38610bc --- /dev/null +++ b/modules/web-console/e2e/testcafe/page-models/PageConfigurationBasic.js @@ -0,0 +1,68 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 {Selector, t} from 'testcafe' +import {FormField} from '../components/FormField' +import {ListEditable} from '../components/ListEditable' + +class VersionPicker { + constructor() { + this._selector = Selector('version-picker') + } + /** + * @param {string} label Version label + */ + pickVersion(label) { + return t + .hover(this._selector) + .click(this._selector.find('[role="menuitem"]').withText(label)) + } +} + +export class PageConfigurationBasic { + static SAVE_CHANGES_AND_DOWNLOAD_LABEL = 'Save changes and download project'; + static SAVE_CHANGES_LABEL = 'Save changes'; + + constructor() { + this._selector = Selector('page-configure-basic'); + this.versionPicker = new VersionPicker; + this.totalOffheapSizeInput = Selector('pc-form-field-size#memory'); + this.mainFormAction = Selector('.pc-form-actions-panel .btn-ignite-group .btn-ignite:nth-of-type(1)'); + this.contextFormActionsButton = Selector('.pc-form-actions-panel .btn-ignite-group .btn-ignite:nth-of-type(2)'); + this.contextSaveButton = Selector('a[role=menuitem]').withText(new RegExp(`^${PageConfigurationBasic.SAVE_CHANGES_LABEL}$`)); + this.contextSaveAndDownloadButton = Selector('a[role=menuitem]').withText(PageConfigurationBasic.SAVE_CHANGES_AND_DOWNLOAD_LABEL); + this.buttonPreviewProject = Selector('button-preview-project'); + this.buttonDownloadProject = Selector('button-download-project'); + this.clusterNameInput = new FormField({id: 'clusterNameInput'}); + this.clusterDiscoveryInput = new FormField({id: 'discoveryInput'}); + this.cachesList = new ListEditable(Selector('.pcb-caches-list'), { + name: {id: 'nameInput'}, + cacheMode: {id: 'cacheModeInput'}, + atomicityMode: {id: 'atomicityModeInput'}, + backups: {id: 'backupsInput'} + }); + this.pageHeader = Selector('.pc-page-header') + } + + async save() { + await t.click(this.mainFormAction) + } + + async saveWithoutDownload() { + return await t.click(this.contextFormActionsButton).click(this.contextSaveButton) + } +} http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/e2e/testcafe/page-models/PageConfigurationOverview.js ---------------------------------------------------------------------- diff --git a/modules/web-console/e2e/testcafe/page-models/PageConfigurationOverview.js b/modules/web-console/e2e/testcafe/page-models/PageConfigurationOverview.js new file mode 100644 index 0000000..34a6486 --- /dev/null +++ b/modules/web-console/e2e/testcafe/page-models/PageConfigurationOverview.js @@ -0,0 +1,36 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 {Selector, t} from 'testcafe' +import {Table} from '../components/Table' +import {confirmation} from '../components/confirmation' +import {successNotification} from '../components/notifications' + +export class PageConfigurationOverview { + constructor() { + this.createClusterConfigButton = Selector('.btn-ignite').withText('Create Cluster Configuration'); + this.importFromDBButton = Selector('.btn-ignite').withText('Import from Database'); + this.clustersTable = new Table(Selector('pc-items-table')); + this.pageHeader = Selector('.pc-page-header') + } + async removeAllItems() { + await t.click(this.clustersTable.allItemsCheckbox); + await this.clustersTable.performAction('Delete'); + await confirmation.confirm(); + await t.expect(successNotification.visible).ok(); + } +} http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/e2e/testcafe/page-models/pageConfigurationAdvancedIGFS.js ---------------------------------------------------------------------- diff --git a/modules/web-console/e2e/testcafe/page-models/pageConfigurationAdvancedIGFS.js b/modules/web-console/e2e/testcafe/page-models/pageConfigurationAdvancedIGFS.js new file mode 100644 index 0000000..f3ac35c --- /dev/null +++ b/modules/web-console/e2e/testcafe/page-models/pageConfigurationAdvancedIGFS.js @@ -0,0 +1,21 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 {Selector} from 'testcafe' +import {isVisible} from '../helpers' + +export const createIGFSButton = Selector('pc-items-table footer-slot .link-success').filter(isVisible); http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/e2e/testcafe/page-models/pageConfigurationAdvancedModels.js ---------------------------------------------------------------------- diff --git a/modules/web-console/e2e/testcafe/page-models/pageConfigurationAdvancedModels.js b/modules/web-console/e2e/testcafe/page-models/pageConfigurationAdvancedModels.js new file mode 100644 index 0000000..196ac3c --- /dev/null +++ b/modules/web-console/e2e/testcafe/page-models/pageConfigurationAdvancedModels.js @@ -0,0 +1,28 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 {Selector} from 'testcafe' +import {FormField} from '../components/FormField' +import {isVisible} from '../helpers' + +export const createModelButton = Selector('pc-items-table footer-slot .link-success').filter(isVisible); +export const general = { + generatePOJOClasses: new FormField({id: 'generatePojoInput'}), + queryMetadata: new FormField({id: 'queryMetadataInput'}), + keyType: new FormField({id: 'keyTypeInput'}), + valueType: new FormField({id: 'valueTypeInput'}) +}; http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/e2e/testcafe/roles.js ---------------------------------------------------------------------- diff --git a/modules/web-console/e2e/testcafe/roles.js b/modules/web-console/e2e/testcafe/roles.js index 99a4d31..5f584b2 100644 --- a/modules/web-console/e2e/testcafe/roles.js +++ b/modules/web-console/e2e/testcafe/roles.js @@ -27,7 +27,6 @@ export const createRegularUser = () => { await t.eval(() => window.localStorage.showGettingStarted = 'false'); const page = new PageSignIn(); - await page.open(); await page.login('a@a', 'a'); }); }; http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/frontend/.babelrc ---------------------------------------------------------------------- diff --git a/modules/web-console/frontend/.babelrc b/modules/web-console/frontend/.babelrc index da16f08..1759c44 100644 --- a/modules/web-console/frontend/.babelrc +++ b/modules/web-console/frontend/.babelrc @@ -1,4 +1,4 @@ { "presets": ["es2015", "stage-1"], - "plugins": ["add-module-exports"] + "plugins": ["add-module-exports", "transform-object-rest-spread"] } http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/frontend/.eslintrc ---------------------------------------------------------------------- diff --git a/modules/web-console/frontend/.eslintrc b/modules/web-console/frontend/.eslintrc index 3c26fa7..75de1ea 100644 --- a/modules/web-console/frontend/.eslintrc +++ b/modules/web-console/frontend/.eslintrc @@ -159,7 +159,7 @@ rules: no-unneeded-ternary: 2 no-unreachable: 2 no-unused-expressions: [2, { allowShortCircuit: true }] - no-unused-vars: [2, {"vars": "all", "args": "after-used"}] + no-unused-vars: [0, {"vars": "all", "args": "after-used"}] no-use-before-define: 2 no-useless-call: 2 no-void: 0 http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/frontend/.gitignore ---------------------------------------------------------------------- diff --git a/modules/web-console/frontend/.gitignore b/modules/web-console/frontend/.gitignore index 4fc11f46..60d2029 100644 --- a/modules/web-console/frontend/.gitignore +++ b/modules/web-console/frontend/.gitignore @@ -1,3 +1,8 @@ +*.idea +*.log *.log.* +.npmrc +build/* +node_modules public/stylesheets/*.css http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/frontend/app/app.config.js ---------------------------------------------------------------------- diff --git a/modules/web-console/frontend/app/app.config.js b/modules/web-console/frontend/app/app.config.js index 6ba1d98..aa604af 100644 --- a/modules/web-console/frontend/app/app.config.js +++ b/modules/web-console/frontend/app/app.config.js @@ -17,12 +17,16 @@ import _ from 'lodash'; import angular from 'angular'; +import negate from 'lodash/negate'; +import isNil from 'lodash/isNil'; +import isEmpty from 'lodash/isEmpty'; +import mixin from 'lodash/mixin'; -const nonNil = _.negate(_.isNil); -const nonEmpty = _.negate(_.isEmpty); +const nonNil = negate(isNil); +const nonEmpty = negate(isEmpty); const id8 = (uuid) => uuid.substring(0, 8).toUpperCase(); -_.mixin({ +mixin({ nonNil, nonEmpty, id8 @@ -36,7 +40,7 @@ const igniteConsoleCfg = angular.module('ignite-console.config', ['ngAnimate', ' // Configure AngularJS animation: do not animate fa-spin. igniteConsoleCfg.config(['$animateProvider', ($animateProvider) => { - $animateProvider.classNameFilter(/^((?!(fa-spin)).)*$/); + $animateProvider.classNameFilter(/^((?!(fa-spin|ng-animate-disabled)).)*$/); }]); // AngularStrap modal popup configuration. @@ -115,3 +119,20 @@ igniteConsoleCfg.config(['$datepickerProvider', ($datepickerProvider) => { igniteConsoleCfg.config(['$translateProvider', ($translateProvider) => { $translateProvider.useSanitizeValueStrategy('sanitize'); }]); + +// Restores pre 4.3.0 ui-grid getSelectedRows method behavior +// ui-grid 4.4+ getSelectedRows additionally skips entries without $$hashKey, +// which breaks most of out code that works with selected rows. +igniteConsoleCfg.directive('uiGridSelection', function() { + function legacyGetSelectedRows() { + return this.rows.filter((row) => row.isSelected).map((row) => row.entity); + } + return { + require: '^uiGrid', + restrict: 'A', + link(scope, el, attr, ctrl) { + ctrl.grid.api.registerMethodsFromObject({selection: {legacyGetSelectedRows}}); + } + }; +}); + http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/frontend/app/app.d.ts ---------------------------------------------------------------------- diff --git a/modules/web-console/frontend/app/app.d.ts b/modules/web-console/frontend/app/app.d.ts new file mode 100644 index 0000000..69cc7ab --- /dev/null +++ b/modules/web-console/frontend/app/app.d.ts @@ -0,0 +1,29 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + */ + +declare module '*.pug' { + const pug: string; + export default pug; +} +declare module '*.scss' { + const scss: any; + export default scss; +} +declare module '*.json' { + const value: any; + export default value; +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/frontend/app/app.js ---------------------------------------------------------------------- diff --git a/modules/web-console/frontend/app/app.js b/modules/web-console/frontend/app/app.js index d01d9aa..871b06f 100644 --- a/modules/web-console/frontend/app/app.js +++ b/modules/web-console/frontend/app/app.js @@ -15,6 +15,7 @@ * limitations under the License. */ +import './vendor'; import '../public/stylesheets/style.scss'; import '../app/primitives'; @@ -41,6 +42,7 @@ import './modules/dialog/dialog.module'; import './modules/ace.module'; import './modules/socket.module'; import './modules/loading/loading.module'; +import servicesModule from './services'; // endignite // Data @@ -67,10 +69,11 @@ import igniteUiAceDocker from './directives/ui-ace-docker/ui-ace-docker.directiv import igniteUiAceTabs from './directives/ui-ace-tabs.directive'; import igniteRetainSelection from './directives/retain-selection.directive'; import btnIgniteLink from './directives/btn-ignite-link'; +import exposeInput from './components/expose-ignite-form-field-control'; // Services. import ChartColors from './services/ChartColors.service'; -import Confirm from './services/Confirm.service.js'; +import {default as IgniteConfirm, Confirm} from './services/Confirm.service.js'; import ConfirmBatch from './services/ConfirmBatch.service.js'; import CopyToClipboard from './services/CopyToClipboard.service'; import Countries from './services/Countries.service'; @@ -85,10 +88,11 @@ import LegacyUtils from './services/LegacyUtils.service'; import Messages from './services/Messages.service'; import ModelNormalizer from './services/ModelNormalizer.service.js'; import UnsavedChangesGuard from './services/UnsavedChangesGuard.service'; -import Clusters from './services/Clusters'; import Caches from './services/Caches'; import {CSV} from './services/CSV'; import {$exceptionHandler} from './services/exceptionHandler.js'; +import IGFSs from './services/IGFSs'; +import Models from './services/Models'; import AngularStrapTooltip from './services/AngularStrapTooltip.decorator'; import AngularStrapSelect from './services/AngularStrapSelect.decorator'; @@ -119,6 +123,7 @@ import pageConfigure from './components/page-configure'; import pageConfigureBasic from './components/page-configure-basic'; import pageConfigureAdvanced from './components/page-configure-advanced'; import pageQueries from './components/page-queries'; +import pageConfigureOverview from './components/page-configure-overview'; import gridColumnSelector from './components/grid-column-selector'; import gridItemSelected from './components/grid-item-selected'; import gridNoData from './components/grid-no-data'; @@ -209,6 +214,7 @@ angular.module('ignite-console', [ pageConfigureBasic.name, pageConfigureAdvanced.name, pageQueries.name, + pageConfigureOverview.name, gridColumnSelector.name, gridItemSelected.name, gridNoData.name, @@ -221,9 +227,11 @@ angular.module('ignite-console', [ AngularStrapSelect.name, listEditable.name, clusterSelector.name, + servicesModule.name, connectedClusters.name, igniteListOfRegisteredUsers.name, pageProfile.name, + exposeInput.name, pageSignIn.name, pageLanding.name, pagePasswordChanged.name, @@ -262,8 +270,9 @@ angular.module('ignite-console', [ .service('JavaTypes', JavaTypes) .service('SqlTypes', SqlTypes) .service(...ChartColors) -.service(...Confirm) -.service(...ConfirmBatch) +.service(...IgniteConfirm) +.service(Confirm.name, Confirm) +.service('IgniteConfirmBatch', ConfirmBatch) .service(...CopyToClipboard) .service(...Countries) .service(...Focus) @@ -275,9 +284,10 @@ angular.module('ignite-console', [ .service(...LegacyUtils) .service(...UnsavedChangesGuard) .service('IgniteActivitiesUserDialog', IgniteActivitiesUserDialog) -.service('Clusters', Clusters) .service('Caches', Caches) .service(CSV.name, CSV) +.service('IGFSs', IGFSs) +.service('Models', Models) // Controllers. .controller(...resetPassword) // Filters. @@ -346,11 +356,7 @@ angular.module('ignite-console', [ $root.revertIdentity = () => { $http.get('/api/v1/admin/revert/identity') .then(() => User.load()) - .then((user) => { - $root.$broadcast('user', user); - - $state.go('base.settings.admin'); - }) + .then(() => $state.go('base.settings.admin')) .then(() => Notebook.load()) .catch(Messages.showError); }; http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/frontend/app/components/bs-select-menu/style.scss ---------------------------------------------------------------------- diff --git a/modules/web-console/frontend/app/components/bs-select-menu/style.scss b/modules/web-console/frontend/app/components/bs-select-menu/style.scss index d82bf19..4c071f6 100644 --- a/modules/web-console/frontend/app/components/bs-select-menu/style.scss +++ b/modules/web-console/frontend/app/components/bs-select-menu/style.scss @@ -84,13 +84,13 @@ z-index: -1; } - &.bssm-multiple { + [class*='bssm-multiple'] { .bssm-active-indicator { display: initial; } } - &:not(.bssm-multiple) { + &:not([class*='bssm-multiple']) { .bssm-active-indicator { display: none; } http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/frontend/app/components/expose-ignite-form-field-control/directives.js ---------------------------------------------------------------------- diff --git a/modules/web-console/frontend/app/components/expose-ignite-form-field-control/directives.js b/modules/web-console/frontend/app/components/expose-ignite-form-field-control/directives.js new file mode 100644 index 0000000..5184032 --- /dev/null +++ b/modules/web-console/frontend/app/components/expose-ignite-form-field-control/directives.js @@ -0,0 +1,53 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + */ + +// eslint-disable-next-line +import {IgniteFormField} from 'app/components/page-configure/components/pcValidation' + +/** + * Exposes input to .ignite-form-field scope + */ +class ExposeIgniteFormFieldControl { + /** @type {IgniteFormField} */ + formField; + /** @type {ng.INgModelController} */ + ngModel; + /** + * Name used to access control from $scope. + * @type {string} + */ + name; + + $onInit() { + if (this.formField && this.ngModel) this.formField.exposeControl(this.ngModel, this.name); + } +} + +export function exposeIgniteFormFieldControl() { + return { + restrict: 'A', + controller: ExposeIgniteFormFieldControl, + bindToController: { + name: '@exposeIgniteFormFieldControl' + }, + require: { + formField: '^^?igniteFormField', + ngModel: '?ngModel' + }, + scope: false + }; +} http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/frontend/app/components/expose-ignite-form-field-control/index.js ---------------------------------------------------------------------- diff --git a/modules/web-console/frontend/app/components/expose-ignite-form-field-control/index.js b/modules/web-console/frontend/app/components/expose-ignite-form-field-control/index.js new file mode 100644 index 0000000..9a22478 --- /dev/null +++ b/modules/web-console/frontend/app/components/expose-ignite-form-field-control/index.js @@ -0,0 +1,23 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 angular from 'angular'; +import {igniteFormField, exposeIgniteFormFieldControl} from './directives'; + +export default angular +.module('expose-ignite-form-field-control', []) +.directive('exposeIgniteFormFieldControl', exposeIgniteFormFieldControl); http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/frontend/app/components/grid-column-selector/template.pug ---------------------------------------------------------------------- diff --git a/modules/web-console/frontend/app/components/grid-column-selector/template.pug b/modules/web-console/frontend/app/components/grid-column-selector/template.pug index 86fd152..afb246e 100644 --- a/modules/web-console/frontend/app/components/grid-column-selector/template.pug +++ b/modules/web-console/frontend/app/components/grid-column-selector/template.pug @@ -24,5 +24,6 @@ button.btn-ignite.btn-ignite--link-dashed-secondary( bs-on-before-show='$ctrl.onShow' data-multiple='true' ng-transclude + ng-show='$ctrl.columnsMenu.length' ) svg(ignite-icon='gear').icon
