ignite-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From akuznet...@apache.org
Subject [03/21] ignite git commit: IGNITE-843 Implemented Web Console.
Date Tue, 17 May 2016 16:22:45 GMT
http://git-wip-us.apache.org/repos/asf/ignite/blob/eb5ac0ae/modules/web-console/src/main/js/serve/mail.js
----------------------------------------------------------------------
diff --git a/modules/web-console/src/main/js/serve/mail.js b/modules/web-console/src/main/js/serve/mail.js
new file mode 100644
index 0000000..2c67276
--- /dev/null
+++ b/modules/web-console/src/main/js/serve/mail.js
@@ -0,0 +1,75 @@
+/*
+ * 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.
+ */
+
+'use strict';
+
+// Fire me up!
+
+/**
+ * Module for send email.
+ */
+module.exports = {
+    implements: 'mail',
+    inject: ['require(nodemailer)', 'settings']
+};
+
+module.exports.factory = function(nodemailer, settings) {
+    return {
+        /**
+         * Send mail to user.
+         *
+         * @param {Account} user
+         * @param {String} subject
+         * @param {String} html
+         * @param {String} sendErr
+         * @throws {Error}
+         * @return {Promise}
+         */
+        send: (user, subject, html, sendErr) => {
+            const transporter = {
+                service: settings.smtp.service,
+                auth: {
+                    user: settings.smtp.email,
+                    pass: settings.smtp.password
+                }
+            };
+
+            if (transporter.service === '' || transporter.auth.user === '' || transporter.auth.pass === '')
+                throw new Error('Failed to send email. SMTP server is not configured. Please ask webmaster to setup SMTP server!');
+
+            const mailer = nodemailer.createTransport(transporter);
+
+            const sign = settings.smtp.sign ? `<br><br>--------------<br>${settings.smtp.sign}<br>` : '';
+
+            const mail = {
+                from: settings.smtp.address(settings.smtp.username, settings.smtp.email),
+                to: settings.smtp.address(`${user.firstName} ${user.lastName}`, user.email),
+                subject,
+                html: html + sign
+            };
+
+            return new Promise((resolve, reject) => {
+                mailer.sendMail(mail, (err) => {
+                    if (err)
+                        return reject(sendErr ? new Error(sendErr) : err);
+
+                    resolve(user);
+                });
+            });
+        }
+    };
+};

http://git-wip-us.apache.org/repos/asf/ignite/blob/eb5ac0ae/modules/web-console/src/main/js/serve/mongo.js
----------------------------------------------------------------------
diff --git a/modules/web-console/src/main/js/serve/mongo.js b/modules/web-console/src/main/js/serve/mongo.js
new file mode 100644
index 0000000..81b4188
--- /dev/null
+++ b/modules/web-console/src/main/js/serve/mongo.js
@@ -0,0 +1,620 @@
+/*
+ * 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.
+ */
+
+'use strict';
+
+// Fire me up!
+
+/**
+ * Module mongo schema.
+ */
+module.exports = {
+    implements: 'mongo',
+    inject: ['require(mongoose-deep-populate)', 'require(passport-local-mongoose)', 'settings', 'ignite_modules/mongo:*']
+};
+
+module.exports.factory = function(deepPopulatePlugin, passportMongo, settings, pluginMongo) {
+    const mongoose = require('mongoose');
+
+    // Use native promises
+    mongoose.Promise = global.Promise;
+
+    const deepPopulate = deepPopulatePlugin(mongoose);
+
+    // Connect to mongoDB database.
+    mongoose.connect(settings.mongoUrl, {server: {poolSize: 4}});
+
+    const Schema = mongoose.Schema;
+    const ObjectId = mongoose.Schema.Types.ObjectId;
+    const result = { connection: mongoose.connection };
+
+    result.ObjectId = ObjectId;
+
+    // Define Account schema.
+    const AccountSchema = new Schema({
+        firstName: String,
+        lastName: String,
+        email: String,
+        company: String,
+        country: String,
+        lastLogin: Date,
+        admin: Boolean,
+        token: String,
+        resetPasswordToken: String
+    });
+
+    // Install passport plugin.
+    AccountSchema.plugin(passportMongo, {
+        usernameField: 'email', limitAttempts: true, lastLoginField: 'lastLogin',
+        usernameLowerCase: true
+    });
+
+    // Configure transformation to JSON.
+    AccountSchema.set('toJSON', {
+        transform: (doc, ret) => {
+            return {
+                _id: ret._id,
+                email: ret.email,
+                firstName: ret.firstName,
+                lastName: ret.lastName,
+                company: ret.company,
+                country: ret.country,
+                admin: ret.admin,
+                token: ret.token,
+                lastLogin: ret.lastLogin
+            };
+        }
+    });
+
+    // Define Account model.
+    result.Account = mongoose.model('Account', AccountSchema);
+
+    // Define Space model.
+    result.Space = mongoose.model('Space', new Schema({
+        name: String,
+        owner: {type: ObjectId, ref: 'Account'},
+        demo: {type: Boolean, default: false},
+        usedBy: [{
+            permission: {type: String, enum: ['VIEW', 'FULL']},
+            account: {type: ObjectId, ref: 'Account'}
+        }]
+    }));
+
+    // Define Domain model schema.
+    const DomainModelSchema = new Schema({
+        space: {type: ObjectId, ref: 'Space', index: true},
+        caches: [{type: ObjectId, ref: 'Cache'}],
+        queryMetadata: {type: String, enum: ['Annotations', 'Configuration']},
+        kind: {type: String, enum: ['query', 'store', 'both']},
+        databaseSchema: String,
+        databaseTable: String,
+        keyType: String,
+        valueType: String,
+        keyFields: [{
+            databaseFieldName: String,
+            databaseFieldType: String,
+            javaFieldName: String,
+            javaFieldType: String
+        }],
+        valueFields: [{
+            databaseFieldName: String,
+            databaseFieldType: String,
+            javaFieldName: String,
+            javaFieldType: String
+        }],
+        fields: [{name: String, className: String}],
+        aliases: [{field: String, alias: String}],
+        indexes: [{
+            name: String,
+            indexType: {type: String, enum: ['SORTED', 'FULLTEXT', 'GEOSPATIAL']},
+            fields: [{name: String, direction: Boolean}]
+        }],
+        demo: Boolean
+    });
+
+    // Define model of Domain models.
+    result.DomainModel = mongoose.model('DomainModel', DomainModelSchema);
+
+    // Define Cache schema.
+    const CacheSchema = new Schema({
+        space: {type: ObjectId, ref: 'Space', index: true},
+        name: String,
+        clusters: [{type: ObjectId, ref: 'Cluster'}],
+        domains: [{type: ObjectId, ref: 'DomainModel'}],
+        cacheMode: {type: String, enum: ['PARTITIONED', 'REPLICATED', 'LOCAL']},
+        atomicityMode: {type: String, enum: ['ATOMIC', 'TRANSACTIONAL']},
+
+        backups: Number,
+        memoryMode: {type: String, enum: ['ONHEAP_TIERED', 'OFFHEAP_TIERED', 'OFFHEAP_VALUES']},
+        offHeapMaxMemory: Number,
+        startSize: Number,
+        swapEnabled: Boolean,
+
+        evictionPolicy: {
+            kind: {type: String, enum: ['LRU', 'FIFO', 'SORTED']},
+            LRU: {
+                batchSize: Number,
+                maxMemorySize: Number,
+                maxSize: Number
+            },
+            FIFO: {
+                batchSize: Number,
+                maxMemorySize: Number,
+                maxSize: Number
+            },
+            SORTED: {
+                batchSize: Number,
+                maxMemorySize: Number,
+                maxSize: Number
+            }
+        },
+
+        rebalanceMode: {type: String, enum: ['SYNC', 'ASYNC', 'NONE']},
+        rebalanceBatchSize: Number,
+        rebalanceBatchesPrefetchCount: Number,
+        rebalanceOrder: Number,
+        rebalanceDelay: Number,
+        rebalanceTimeout: Number,
+        rebalanceThrottle: Number,
+
+        cacheStoreFactory: {
+            kind: {
+                type: String,
+                enum: ['CacheJdbcPojoStoreFactory', 'CacheJdbcBlobStoreFactory', 'CacheHibernateBlobStoreFactory']
+            },
+            CacheJdbcPojoStoreFactory: {
+                dataSourceBean: String,
+                dialect: {
+                    type: String,
+                    enum: ['Generic', 'Oracle', 'DB2', 'SQLServer', 'MySQL', 'PostgreSQL', 'H2']
+                }
+            },
+            CacheJdbcBlobStoreFactory: {
+                connectVia: {type: String, enum: ['URL', 'DataSource']},
+                connectionUrl: String,
+                user: String,
+                dataSourceBean: String,
+                dialect: {
+                    type: String,
+                    enum: ['Generic', 'Oracle', 'DB2', 'SQLServer', 'MySQL', 'PostgreSQL', 'H2']
+                },
+                initSchema: Boolean,
+                createTableQuery: String,
+                loadQuery: String,
+                insertQuery: String,
+                updateQuery: String,
+                deleteQuery: String
+            },
+            CacheHibernateBlobStoreFactory: {
+                hibernateProperties: [String]
+            }
+        },
+        storeKeepBinary: Boolean,
+        loadPreviousValue: Boolean,
+        readThrough: Boolean,
+        writeThrough: Boolean,
+
+        writeBehindEnabled: Boolean,
+        writeBehindBatchSize: Number,
+        writeBehindFlushSize: Number,
+        writeBehindFlushFrequency: Number,
+        writeBehindFlushThreadCount: Number,
+
+        invalidate: Boolean,
+        defaultLockTimeout: Number,
+        atomicWriteOrderMode: {type: String, enum: ['CLOCK', 'PRIMARY']},
+        writeSynchronizationMode: {type: String, enum: ['FULL_SYNC', 'FULL_ASYNC', 'PRIMARY_SYNC']},
+
+        sqlEscapeAll: Boolean,
+        sqlSchema: String,
+        sqlOnheapRowCacheSize: Number,
+        longQueryWarningTimeout: Number,
+        sqlFunctionClasses: [String],
+        snapshotableIndex: Boolean,
+        statisticsEnabled: Boolean,
+        managementEnabled: Boolean,
+        readFromBackup: Boolean,
+        copyOnRead: Boolean,
+        maxConcurrentAsyncOperations: Number,
+        nearCacheEnabled: Boolean,
+        nearConfiguration: {
+            nearStartSize: Number,
+            nearEvictionPolicy: {
+                kind: {type: String, enum: ['LRU', 'FIFO', 'SORTED']},
+                LRU: {
+                    batchSize: Number,
+                    maxMemorySize: Number,
+                    maxSize: Number
+                },
+                FIFO: {
+                    batchSize: Number,
+                    maxMemorySize: Number,
+                    maxSize: Number
+                },
+                SORTED: {
+                    batchSize: Number,
+                    maxMemorySize: Number,
+                    maxSize: Number
+                }
+            }
+        },
+        demo: Boolean
+    });
+
+    // Install deep populate plugin.
+    CacheSchema.plugin(deepPopulate, {
+        whitelist: ['domains']
+    });
+
+    // Define Cache model.
+    result.Cache = mongoose.model('Cache', CacheSchema);
+
+    const IgfsSchema = new Schema({
+        space: {type: ObjectId, ref: 'Space', index: true},
+        name: String,
+        clusters: [{type: ObjectId, ref: 'Cluster'}],
+        affinnityGroupSize: Number,
+        blockSize: Number,
+        streamBufferSize: Number,
+        dataCacheName: String,
+        metaCacheName: String,
+        defaultMode: {type: String, enum: ['PRIMARY', 'PROXY', 'DUAL_SYNC', 'DUAL_ASYNC']},
+        dualModeMaxPendingPutsSize: Number,
+        dualModePutExecutorService: String,
+        dualModePutExecutorServiceShutdown: Boolean,
+        fragmentizerConcurrentFiles: Number,
+        fragmentizerEnabled: Boolean,
+        fragmentizerThrottlingBlockLength: Number,
+        fragmentizerThrottlingDelay: Number,
+        ipcEndpointConfiguration: {
+            type: {type: String, enum: ['SHMEM', 'TCP']},
+            host: String,
+            port: Number,
+            memorySize: Number,
+            tokenDirectoryPath: String
+        },
+        ipcEndpointEnabled: Boolean,
+        maxSpaceSize: Number,
+        maximumTaskRangeLength: Number,
+        managementPort: Number,
+        pathModes: [{path: String, mode: {type: String, enum: ['PRIMARY', 'PROXY', 'DUAL_SYNC', 'DUAL_ASYNC']}}],
+        perNodeBatchSize: Number,
+        perNodeParallelBatchCount: Number,
+        prefetchBlocks: Number,
+        sequentialReadsBeforePrefetch: Number,
+        trashPurgeTimeout: Number,
+        secondaryFileSystemEnabled: Boolean,
+        secondaryFileSystem: {
+            uri: String,
+            cfgPath: String,
+            userName: String
+        },
+        colocateMetadata: Boolean,
+        relaxedConsistency: Boolean
+    });
+
+    // Define IGFS model.
+    result.Igfs = mongoose.model('Igfs', IgfsSchema);
+
+    // Define Cluster schema.
+    const ClusterSchema = new Schema({
+        space: {type: ObjectId, ref: 'Space', index: true},
+        name: String,
+        localHost: String,
+        discovery: {
+            localAddress: String,
+            localPort: Number,
+            localPortRange: Number,
+            addressResolver: String,
+            socketTimeout: Number,
+            ackTimeout: Number,
+            maxAckTimeout: Number,
+            networkTimeout: Number,
+            joinTimeout: Number,
+            threadPriority: Number,
+            heartbeatFrequency: Number,
+            maxMissedHeartbeats: Number,
+            maxMissedClientHeartbeats: Number,
+            topHistorySize: Number,
+            listener: String,
+            dataExchange: String,
+            metricsProvider: String,
+            reconnectCount: Number,
+            statisticsPrintFrequency: Number,
+            ipFinderCleanFrequency: Number,
+            authenticator: String,
+            forceServerMode: Boolean,
+            clientReconnectDisabled: Boolean,
+            kind: {type: String, enum: ['Vm', 'Multicast', 'S3', 'Cloud', 'GoogleStorage', 'Jdbc', 'SharedFs', 'ZooKeeper']},
+            Vm: {
+                addresses: [String]
+            },
+            Multicast: {
+                multicastGroup: String,
+                multicastPort: Number,
+                responseWaitTime: Number,
+                addressRequestAttempts: Number,
+                localAddress: String,
+                addresses: [String]
+            },
+            S3: {
+                bucketName: String
+            },
+            Cloud: {
+                credential: String,
+                credentialPath: String,
+                identity: String,
+                provider: String,
+                regions: [String],
+                zones: [String]
+            },
+            GoogleStorage: {
+                projectName: String,
+                bucketName: String,
+                serviceAccountP12FilePath: String,
+                serviceAccountId: String,
+                addrReqAttempts: String
+            },
+            Jdbc: {
+                initSchema: Boolean
+            },
+            SharedFs: {
+                path: String
+            },
+            ZooKeeper: {
+                curator: String,
+                zkConnectionString: String,
+                retryPolicy: {
+                    kind: {type: String, enum: ['ExponentialBackoff', 'BoundedExponentialBackoff', 'UntilElapsed',
+                        'NTimes', 'OneTime', 'Forever', 'Custom']},
+                    ExponentialBackoff: {
+                        baseSleepTimeMs: Number,
+                        maxRetries: Number,
+                        maxSleepMs: Number
+                    },
+                    BoundedExponentialBackoff: {
+                        baseSleepTimeMs: Number,
+                        maxSleepTimeMs: Number,
+                        maxRetries: Number
+                    },
+                    UntilElapsed: {
+                        maxElapsedTimeMs: Number,
+                        sleepMsBetweenRetries: Number
+                    },
+                    NTimes: {
+                        n: Number,
+                        sleepMsBetweenRetries: Number
+                    },
+                    OneTime: {
+                        sleepMsBetweenRetry: Number
+                    },
+                    Forever: {
+                        retryIntervalMs: Number
+                    },
+                    Custom: {
+                        className: String
+                    }
+                },
+                basePath: String,
+                serviceName: String,
+                allowDuplicateRegistrations: Boolean
+            }
+        },
+        atomicConfiguration: {
+            backups: Number,
+            cacheMode: {type: String, enum: ['LOCAL', 'REPLICATED', 'PARTITIONED']},
+            atomicSequenceReserveSize: Number
+        },
+        binaryConfiguration: {
+            idMapper: String,
+            nameMapper: String,
+            serializer: String,
+            typeConfigurations: [{
+                typeName: String,
+                idMapper: String,
+                nameMapper: String,
+                serializer: String,
+                enum: Boolean
+            }],
+            compactFooter: Boolean
+        },
+        caches: [{type: ObjectId, ref: 'Cache'}],
+        clockSyncSamples: Number,
+        clockSyncFrequency: Number,
+        deploymentMode: {type: String, enum: ['PRIVATE', 'ISOLATED', 'SHARED', 'CONTINUOUS']},
+        discoveryStartupDelay: Number,
+        igfsThreadPoolSize: Number,
+        igfss: [{type: ObjectId, ref: 'Igfs'}],
+        includeEventTypes: [String],
+        managementThreadPoolSize: Number,
+        marshaller: {
+            kind: {type: String, enum: ['OptimizedMarshaller', 'JdkMarshaller']},
+            OptimizedMarshaller: {
+                poolSize: Number,
+                requireSerializable: Boolean
+            }
+        },
+        marshalLocalJobs: Boolean,
+        marshallerCacheKeepAliveTime: Number,
+        marshallerCacheThreadPoolSize: Number,
+        metricsExpireTime: Number,
+        metricsHistorySize: Number,
+        metricsLogFrequency: Number,
+        metricsUpdateFrequency: Number,
+        networkTimeout: Number,
+        networkSendRetryDelay: Number,
+        networkSendRetryCount: Number,
+        communication: {
+            listener: String,
+            localAddress: String,
+            localPort: Number,
+            localPortRange: Number,
+            sharedMemoryPort: Number,
+            directBuffer: Boolean,
+            directSendBuffer: Boolean,
+            idleConnectionTimeout: Number,
+            connectTimeout: Number,
+            maxConnectTimeout: Number,
+            reconnectCount: Number,
+            socketSendBuffer: Number,
+            socketReceiveBuffer: Number,
+            messageQueueLimit: Number,
+            slowClientQueueLimit: Number,
+            tcpNoDelay: Boolean,
+            ackSendThreshold: Number,
+            unacknowledgedMessagesBufferSize: Number,
+            socketWriteTimeout: Number,
+            selectorsCount: Number,
+            addressResolver: String
+        },
+        connector: {
+            enabled: Boolean,
+            jettyPath: String,
+            host: String,
+            port: Number,
+            portRange: Number,
+            idleTimeout: Number,
+            idleQueryCursorTimeout: Number,
+            idleQueryCursorCheckFrequency: Number,
+            receiveBufferSize: Number,
+            sendBufferSize: Number,
+            sendQueueLimit: Number,
+            directBuffer: Boolean,
+            noDelay: Boolean,
+            selectorCount: Number,
+            threadPoolSize: Number,
+            messageInterceptor: String,
+            secretKey: String,
+            sslEnabled: Boolean,
+            sslClientAuth: Boolean,
+            sslFactory: String
+        },
+        peerClassLoadingEnabled: Boolean,
+        peerClassLoadingLocalClassPathExclude: [String],
+        peerClassLoadingMissedResourcesCacheSize: Number,
+        peerClassLoadingThreadPoolSize: Number,
+        publicThreadPoolSize: Number,
+        swapSpaceSpi: {
+            kind: {type: String, enum: ['FileSwapSpaceSpi']},
+            FileSwapSpaceSpi: {
+                baseDirectory: String,
+                readStripesNumber: Number,
+                maximumSparsity: Number,
+                maxWriteQueueSize: Number,
+                writeBufferSize: Number
+            }
+        },
+        systemThreadPoolSize: Number,
+        timeServerPortBase: Number,
+        timeServerPortRange: Number,
+        transactionConfiguration: {
+            defaultTxConcurrency: {type: String, enum: ['OPTIMISTIC', 'PESSIMISTIC']},
+            defaultTxIsolation: {type: String, enum: ['READ_COMMITTED', 'REPEATABLE_READ', 'SERIALIZABLE']},
+            defaultTxTimeout: Number,
+            pessimisticTxLogLinger: Number,
+            pessimisticTxLogSize: Number,
+            txSerializableEnabled: Boolean,
+            txManagerFactory: String
+        },
+        sslEnabled: Boolean,
+        sslContextFactory: {
+            keyAlgorithm: String,
+            keyStoreFilePath: String,
+            keyStoreType: String,
+            protocol: String,
+            trustStoreFilePath: String,
+            trustStoreType: String,
+            trustManagers: [String]
+        },
+        rebalanceThreadPoolSize: Number
+    });
+
+    // Install deep populate plugin.
+    ClusterSchema.plugin(deepPopulate, {
+        whitelist: [
+            'caches',
+            'caches.domains',
+            'igfss'
+        ]
+    });
+
+    // Define Cluster model.
+    result.Cluster = mongoose.model('Cluster', ClusterSchema);
+
+    result.ClusterDefaultPopulate = '';
+
+    // Define Notebook schema.
+    const NotebookSchema = new Schema({
+        space: {type: ObjectId, ref: 'Space', index: true},
+        name: String,
+        expandedParagraphs: [Number],
+        paragraphs: [{
+            name: String,
+            query: String,
+            editor: Boolean,
+            result: {type: String, enum: ['none', 'table', 'bar', 'pie', 'line', 'area']},
+            pageSize: Number,
+            timeLineSpan: String,
+            hideSystemColumns: Boolean,
+            cacheName: String,
+            chartsOptions: {barChart: {stacked: Boolean}, areaChart: {style: String}},
+            rate: {
+                value: Number,
+                unit: Number
+            }
+        }]
+    });
+
+    // Define Notebook model.
+    result.Notebook = mongoose.model('Notebook', NotebookSchema);
+
+    result.handleError = function(res, err) {
+        // TODO IGNITE-843 Send error to admin
+        res.status(err.code || 500).send(err.message);
+    };
+
+    /**
+     * Query for user spaces.
+     *
+     * @param userId User ID.
+     * @param {Boolean} demo Is need use demo space.
+     * @returns {Promise}
+     */
+    result.spaces = function(userId, demo) {
+        return result.Space.find({owner: userId, demo: !!demo}).lean().exec();
+    };
+
+    /**
+     * Extract IDs from user spaces.
+     *
+     * @param userId User ID.
+     * @param {Boolean} demo Is need use demo space.
+     * @returns {Promise}
+     */
+    result.spaceIds = function(userId, demo) {
+        return result.spaces(userId, demo)
+            .then((spaces) => spaces.map((space) => space._id));
+    };
+
+    // Registering the routes of all plugin modules
+    for (const name in pluginMongo) {
+        if (pluginMongo.hasOwnProperty(name))
+            pluginMongo[name].register(mongoose, deepPopulate, result);
+    }
+
+    return result;
+};

http://git-wip-us.apache.org/repos/asf/ignite/blob/eb5ac0ae/modules/web-console/src/main/js/serve/routes/admin.js
----------------------------------------------------------------------
diff --git a/modules/web-console/src/main/js/serve/routes/admin.js b/modules/web-console/src/main/js/serve/routes/admin.js
new file mode 100644
index 0000000..3c2e728
--- /dev/null
+++ b/modules/web-console/src/main/js/serve/routes/admin.js
@@ -0,0 +1,126 @@
+/*
+ * 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.
+ */
+
+'use strict';
+
+// Fire me up!
+
+module.exports = {
+    implements: 'admin-routes',
+    inject: ['require(lodash)', 'require(express)', 'settings', 'mail', 'mongo']
+};
+
+module.exports.factory = function(_, express, settings, mail, mongo) {
+    return new Promise((factoryResolve) => {
+        const router = new express.Router();
+
+        /**
+         * Get list of user accounts.
+         */
+        router.post('/list', (req, res) => {
+
+            Promise.all([
+                mongo.Space.aggregate([
+                    {$match: {demo: false}},
+                    {$lookup: {from: 'clusters', localField: '_id', foreignField: 'space', as: 'clusters'}},
+                    {$lookup: {from: 'caches', localField: '_id', foreignField: 'space', as: 'caches'}},
+                    {$lookup: {from: 'domainmodels', localField: '_id', foreignField: 'space', as: 'domainmodels'}},
+                    {$lookup: {from: 'igfs', localField: '_id', foreignField: 'space', as: 'igfs'}},
+                    {$project: {
+                        owner: 1,
+                        clusters: {$size: '$clusters'},
+                        models: {$size: '$domainmodels'},
+                        caches: {$size: '$caches'},
+                        igfs: {$size: '$igfs'}
+                    }}
+                ]).exec(),
+                mongo.Account.find({}).sort('firstName lastName').lean().exec()
+            ])
+            .then((values) => {
+                const counters = _.keyBy(values[0], 'owner');
+                const accounts = values[1];
+
+                return accounts.map((account) => {
+                    account.counters = _.omit(counters[account._id], '_id', 'owner');
+
+                    return account;
+                });
+            })
+            .then((users) => res.json(users))
+            .catch((err) => mongo.handleError(res, err));
+        });
+
+        // Remove user.
+        router.post('/remove', (req, res) => {
+            const userId = req.body.userId;
+
+            mongo.Account.findByIdAndRemove(userId).exec()
+                .then((user) => {
+                    res.sendStatus(200);
+
+                    return mongo.spaceIds(userId)
+                        .then((spaceIds) => Promise.all([
+                            mongo.Cluster.remove({space: {$in: spaceIds}}).exec(),
+                            mongo.Cache.remove({space: {$in: spaceIds}}).exec(),
+                            mongo.DomainModel.remove({space: {$in: spaceIds}}).exec(),
+                            mongo.Igfs.remove({space: {$in: spaceIds}}).exec(),
+                            mongo.Notebook.remove({space: {$in: spaceIds}}).exec(),
+                            mongo.Space.remove({owner: userId}).exec()
+                        ]))
+                        .then(() => user)
+                        .catch((err) => console.error(`Failed to cleanup spaces [user=${user.username}, err=${err}`));
+                })
+                .then((user) =>
+                    mail.send(user, 'Your account was deleted',
+                        `Hello ${user.firstName} ${user.lastName}!<br><br>` +
+                        `You are receiving this email because your account for <a href="http://${req.headers.host}">${settings.smtp.username}</a> was removed.`,
+                        'Account was removed, but failed to send email notification to user!')
+                )
+                .catch((err) => mongo.handleError(res, err));
+        });
+
+        // Save user.
+        router.post('/save', (req, res) => {
+            const params = req.body;
+
+            mongo.Account.findByIdAndUpdate(params.userId, {admin: params.adminFlag}).exec()
+                .then(() => res.sendStatus(200))
+                .catch((err) => mongo.handleError(res, err));
+        });
+
+        // Become user.
+        router.get('/become', (req, res) => {
+            mongo.Account.findById(req.query.viewedUserId).exec()
+                .then((viewedUser) => {
+                    req.session.viewedUser = viewedUser;
+
+                    res.sendStatus(200);
+                })
+                .catch(() => res.sendStatus(404));
+        });
+
+        // Revert to your identity.
+        router.get('/revert/identity', (req, res) => {
+            req.session.viewedUser = null;
+
+            return res.sendStatus(200);
+        });
+
+        factoryResolve(router);
+    });
+};
+

http://git-wip-us.apache.org/repos/asf/ignite/blob/eb5ac0ae/modules/web-console/src/main/js/serve/routes/agent.js
----------------------------------------------------------------------
diff --git a/modules/web-console/src/main/js/serve/routes/agent.js b/modules/web-console/src/main/js/serve/routes/agent.js
new file mode 100644
index 0000000..8fd8b75
--- /dev/null
+++ b/modules/web-console/src/main/js/serve/routes/agent.js
@@ -0,0 +1,82 @@
+/*
+ * 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.
+ */
+
+'use strict';
+
+// Fire me up!
+
+module.exports = {
+    implements: 'agent-routes',
+    inject: ['require(lodash)', 'require(express)', 'require(fs)', 'require(jszip)', 'settings', 'agent-manager']
+};
+
+/**
+ * @param _
+ * @param express
+ * @param fs
+ * @param JSZip
+ * @param settings
+ * @param {AgentManager} agentMgr
+ * @returns {Promise}
+ */
+module.exports.factory = function(_, express, fs, JSZip, settings, agentMgr) {
+    return new Promise((resolveFactory) => {
+        const router = new express.Router();
+
+        /* Get grid topology. */
+        router.get('/download/zip', (req, res) => {
+            const latest = agentMgr.supportedAgents.latest;
+
+            if (_.isEmpty(latest))
+                return res.status(500).send('Missing agent zip on server. Please ask webmaster to upload agent zip!');
+
+            const agentFld = latest.fileName.substr(0, latest.fileName.length - 4);
+            const agentZip = latest.fileName;
+            const agentPathZip = latest.filePath;
+
+            // Read a zip file.
+            fs.readFile(agentPathZip, (errFs, data) => {
+                if (errFs)
+                    return res.download(agentPathZip, agentZip);
+
+                const zip = new JSZip(data);
+
+                const prop = [];
+
+                const host = req.hostname.match(/:/g) ? req.hostname.slice(0, req.hostname.indexOf(':')) : req.hostname;
+
+                prop.push('token=' + req.user.token);
+                prop.push('server-uri=' + (settings.agent.SSLOptions ? 'https' : 'http') + '://' + host + ':' + settings.agent.port);
+                prop.push('#Uncomment following options if needed:');
+                prop.push('#node-uri=http://localhost:8080');
+                prop.push('#driver-folder=./jdbc-drivers');
+
+                zip.file(agentFld + '/default.properties', prop.join('\n'));
+
+                const buffer = zip.generate({type: 'nodebuffer', platform: 'UNIX'});
+
+                // Set the archive name.
+                res.attachment(agentZip);
+
+                res.send(buffer);
+            });
+        });
+
+        resolveFactory(router);
+    });
+};
+

http://git-wip-us.apache.org/repos/asf/ignite/blob/eb5ac0ae/modules/web-console/src/main/js/serve/routes/caches.js
----------------------------------------------------------------------
diff --git a/modules/web-console/src/main/js/serve/routes/caches.js b/modules/web-console/src/main/js/serve/routes/caches.js
new file mode 100644
index 0000000..61a0cfb
--- /dev/null
+++ b/modules/web-console/src/main/js/serve/routes/caches.js
@@ -0,0 +1,132 @@
+/*
+ * 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.
+ */
+
+'use strict';
+
+// Fire me up!
+
+module.exports = {
+    implements: 'caches-routes',
+    inject: ['require(lodash)', 'require(express)', 'mongo']
+};
+
+module.exports.factory = function(_, express, mongo) {
+    return new Promise((factoryResolve) => {
+        const router = new express.Router();
+
+        /**
+         * Get spaces and caches accessed for user account.
+         *
+         * @param req Request.
+         * @param res Response.
+         */
+        router.post('/list', (req, res) => {
+            const result = {};
+            let spaceIds = [];
+
+            // Get owned space and all accessed space.
+            mongo.spaces(req.currentUserId(), req.header('IgniteDemoMode'))
+                .then((spaces) => {
+                    result.spaces = spaces;
+                    spaceIds = spaces.map((space) => space._id);
+
+                    return mongo.Cluster.find({space: {$in: spaceIds}}).sort('name').lean().exec();
+                })
+                .then((clusters) => {
+                    result.clusters = clusters;
+
+                    return mongo.DomainModel.find({space: {$in: spaceIds}}).sort('name').lean().exec();
+                })
+                .then((domains) => {
+                    result.domains = domains;
+
+                    return mongo.Cache.find({space: {$in: spaceIds}}).sort('name').lean().exec();
+                })
+                .then((caches) => {
+                    result.caches = caches;
+
+                    res.json(result);
+                })
+                .catch((err) => mongo.handleError(res, err));
+        });
+
+        /**
+         * Save cache.
+         */
+        router.post('/save', (req, res) => {
+            const params = req.body;
+            const clusters = params.clusters;
+            const domains = params.domains;
+
+            mongo.Cache.findOne({space: params.space, name: params.name}).exec()
+                .then((existingCache) => {
+                    const cacheId = params._id;
+
+                    if (existingCache && cacheId !== existingCache._id.toString())
+                        return res.status(500).send('Cache with name: "' + existingCache.name + '" already exist.');
+
+                    if (cacheId) {
+                        return mongo.Cache.update({_id: cacheId}, params, {upsert: true}).exec()
+                            .then(() => mongo.Cluster.update({_id: {$in: clusters}}, {$addToSet: {caches: cacheId}}, {multi: true}).exec())
+                            .then(() => mongo.Cluster.update({_id: {$nin: clusters}}, {$pull: {caches: cacheId}}, {multi: true}).exec())
+                            .then(() => mongo.DomainModel.update({_id: {$in: domains}}, {$addToSet: {caches: cacheId}}, {multi: true}).exec())
+                            .then(() => mongo.DomainModel.update({_id: {$nin: domains}}, {$pull: {caches: cacheId}}, {multi: true}).exec())
+                            .then(() => res.send(cacheId));
+                    }
+
+                    return (new mongo.Cache(params)).save()
+                        .then((cache) =>
+                            mongo.Cluster.update({_id: {$in: clusters}}, {$addToSet: {caches: cacheId}}, {multi: true}).exec()
+                                .then(() => mongo.DomainModel.update({_id: {$in: domains}}, {$addToSet: {caches: cacheId}}, {multi: true}).exec())
+                                .then(() => res.send(cache._id))
+                        );
+                })
+                .catch((err) => mongo.handleError(res, err));
+        });
+
+        /**
+         * Remove cache by ._id.
+         */
+        router.post('/remove', (req, res) => {
+            const params = req.body;
+            const cacheId = params._id;
+
+            mongo.Cluster.update({caches: {$in: [cacheId]}}, {$pull: {caches: cacheId}}, {multi: true}).exec()
+                .then(() => mongo.DomainModel.update({caches: {$in: [cacheId]}}, {$pull: {caches: cacheId}}, {multi: true}).exec())
+                .then(() => mongo.Cache.remove(params).exec())
+                .then(() => res.sendStatus(200))
+                .catch((err) => mongo.handleError(res, err));
+        });
+
+        /**
+         * Remove all caches.
+         */
+        router.post('/remove/all', (req, res) => {
+            mongo.spaceIds(req.currentUserId(), req.header('IgniteDemoMode'))
+                .then((spaceIds) =>
+                    mongo.Cluster.update({space: {$in: spaceIds}}, {caches: []}, {multi: true}).exec()
+                        .then(() => mongo.DomainModel.update({space: {$in: spaceIds}}, {caches: []}, {multi: true}).exec())
+                        .then(() => mongo.Cache.remove({space: {$in: spaceIds}}).exec())
+                )
+                .then(() => res.sendStatus(200))
+                .catch((err) => mongo.handleError(res, err));
+        });
+
+        factoryResolve(router);
+    });
+};
+

http://git-wip-us.apache.org/repos/asf/ignite/blob/eb5ac0ae/modules/web-console/src/main/js/serve/routes/clusters.js
----------------------------------------------------------------------
diff --git a/modules/web-console/src/main/js/serve/routes/clusters.js b/modules/web-console/src/main/js/serve/routes/clusters.js
new file mode 100644
index 0000000..bfe89d3
--- /dev/null
+++ b/modules/web-console/src/main/js/serve/routes/clusters.js
@@ -0,0 +1,146 @@
+/*
+ * 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.
+ */
+
+'use strict';
+
+// Fire me up!
+
+module.exports = {
+    implements: 'clusters-routes',
+    inject: ['require(lodash)', 'require(express)', 'mongo']
+};
+
+module.exports.factory = function(_, express, mongo) {
+    return new Promise((factoryResolve) => {
+        const router = new express.Router();
+
+        /**
+         * Get spaces and clusters accessed for user account.
+         *
+         * @param req Request.
+         * @param res Response.
+         */
+        router.post('/list', (req, res) => {
+            const result = {};
+            let spaceIds = [];
+            let domains = {};
+
+            mongo.spaces(req.currentUserId(), req.header('IgniteDemoMode'))
+                .then((spaces) => {
+                    result.spaces = spaces;
+                    spaceIds = spaces.map((space) => space._id);
+
+                    return mongo.DomainModel.find({space: {$in: spaceIds}}).lean().exec();
+                })
+                .then((_domains) => {
+                    domains = _domains.reduce((map, obj) => {
+                        map[obj._id] = obj;
+
+                        return map;
+                    }, {});
+
+                    return mongo.Cache.find({space: {$in: spaceIds}}).sort('name').lean().exec();
+                })
+                .then((caches) => {
+                    _.forEach(caches, (cache) => {
+                        cache.domains = _.map(cache.domains, (domainId) => domains[domainId]);
+                    });
+
+                    result.caches = caches;
+
+                    return mongo.Igfs.find({space: {$in: spaceIds}}).sort('name').lean().exec();
+                })
+                .then((igfss) => {
+                    result.igfss = igfss;
+
+                    return mongo.Cluster.find({space: {$in: spaceIds}}).sort('name').deepPopulate(mongo.ClusterDefaultPopulate).lean().exec();
+                })
+                .then((clusters) => {
+                    result.clusters = clusters;
+
+                    res.json(result);
+                })
+                .catch((err) => mongo.handleError(res, err));
+        });
+
+        /**
+         * Save cluster.
+         */
+        router.post('/save', (req, res) => {
+            const params = req.body;
+            const caches = params.caches;
+            const igfss = params.igfss;
+
+            mongo.Cluster.findOne({space: params.space, name: params.name}).exec()
+                .then((existingCluster) => {
+                    const clusterId = params._id;
+
+                    if (existingCluster && clusterId !== existingCluster._id.toString())
+                        throw new Error('Cluster with name: "' + existingCluster.name + '" already exist.');
+
+                    if (clusterId) {
+                        return mongo.Cluster.update({_id: clusterId}, params, {upsert: true}).exec()
+                            .then(() => mongo.Cache.update({_id: {$in: caches}}, {$addToSet: {clusters: clusterId}}, {multi: true}).exec())
+                            .then(() => mongo.Cache.update({_id: {$nin: caches}}, {$pull: {clusters: clusterId}}, {multi: true}).exec())
+                            .then(() => mongo.Igfs.update({_id: {$in: igfss}}, {$addToSet: {clusters: clusterId}}, {multi: true}).exec())
+                            .then(() => mongo.Igfs.update({_id: {$nin: igfss}}, {$pull: {clusters: clusterId}}, {multi: true}).exec())
+                            .then(() => res.send(clusterId));
+                    }
+
+                    return (new mongo.Cluster(params)).save()
+                        .then((cluster) =>
+                            mongo.Cache.update({_id: {$in: caches}}, {$addToSet: {clusters: clusterId}}, {multi: true}).exec()
+                                .then(() => mongo.Cache.update({_id: {$nin: caches}}, {$pull: {clusters: clusterId}}, {multi: true}).exec())
+                                .then(() => mongo.Igfs.update({_id: {$in: igfss}}, {$addToSet: {clusters: clusterId}}, {multi: true}).exec())
+                                .then(() => mongo.Igfs.update({_id: {$nin: igfss}}, {$pull: {clusters: clusterId}}, {multi: true}).exec())
+                                .then(() => res.send(cluster._id))
+                        );
+                })
+                .catch((err) => mongo.handleError(res, err));
+        });
+
+        /**
+         * Remove cluster by ._id.
+         */
+        router.post('/remove', (req, res) => {
+            const params = req.body;
+            const clusterId = params._id;
+
+            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(params).exec())
+                .then(() => res.sendStatus(200))
+                .catch((err) => mongo.handleError(res, err));
+        });
+
+        /**
+         * Remove all clusters.
+         */
+        router.post('/remove/all', (req, res) => {
+            // Get owned space and all accessed space.
+            mongo.spaceIds(req.currentUserId(), req.header('IgniteDemoMode'))
+                .then((spaceIds) => mongo.Cache.update({space: {$in: spaceIds}}, {clusters: []}, {multi: true}).exec()
+                    .then(() => mongo.Igfs.update({space: {$in: spaceIds}}, {clusters: []}, {multi: true}).exec())
+                    .then(() => mongo.Cluster.remove({space: {$in: spaceIds}}).exec())
+                )
+                .then(() => res.sendStatus(200))
+                .catch((err) => mongo.handleError(res, err));
+        });
+
+        factoryResolve(router);
+    });
+};

http://git-wip-us.apache.org/repos/asf/ignite/blob/eb5ac0ae/modules/web-console/src/main/js/serve/routes/demo.js
----------------------------------------------------------------------
diff --git a/modules/web-console/src/main/js/serve/routes/demo.js b/modules/web-console/src/main/js/serve/routes/demo.js
new file mode 100644
index 0000000..dd47eb9
--- /dev/null
+++ b/modules/web-console/src/main/js/serve/routes/demo.js
@@ -0,0 +1,135 @@
+/*
+ * 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.
+ */
+
+'use strict';
+
+// Fire me up!
+
+module.exports = {
+    implements: 'demo-routes',
+    inject: [
+        'require(lodash)',
+        'require(express)',
+        'settings',
+        'mongo',
+        'require(./demo/domains.json)',
+        'require(./demo/caches.json)',
+        'require(./demo/igfss.json)',
+        'require(./demo/clusters.json)'
+    ]
+};
+
+module.exports.factory = (_, express, settings, mongo, domains, caches, igfss, clusters) => {
+    return new Promise((factoryResolve) => {
+        const router = new express.Router();
+
+        /**
+         * Reset demo configuration.
+         */
+        router.post('/reset', (req, res) => {
+            mongo.spaces(req.user._id, true)
+                .then((spaces) => {
+                    if (spaces.length) {
+                        const spaceIds = spaces.map((space) => space._id);
+
+                        return Promise.all([
+                            mongo.Cluster.remove({space: {$in: spaceIds}}).exec(),
+                            mongo.Cache.remove({space: {$in: spaceIds}}).exec(),
+                            mongo.DomainModel.remove({space: {$in: spaceIds}}).exec(),
+                            mongo.Igfs.remove({space: {$in: spaceIds}}).exec()
+                        ]).then(() => spaces[0]);
+                    }
+
+                    return new mongo.Space({name: 'Demo space', owner: req.user._id, demo: true}).save();
+                })
+                .then((space) => {
+                    return Promise.all(_.map(clusters, (cluster) => {
+                        const clusterDoc = new mongo.Cluster(cluster);
+
+                        clusterDoc.space = space._id;
+
+                        return clusterDoc.save();
+                    }));
+                })
+                .then((clusterDocs) => {
+                    return _.map(clusterDocs, (cluster) => {
+                        const addCacheToCluster = (cacheDoc) => cluster.caches.push(cacheDoc._id);
+                        const addIgfsToCluster = (igfsDoc) => cluster.igfss.push(igfsDoc._id);
+
+                        if (cluster.name.endsWith('-caches')) {
+                            const cachePromises = _.map(caches, (cacheData) => {
+                                const cache = new mongo.Cache(cacheData);
+
+                                cache.space = cluster.space;
+                                cache.clusters.push(cluster._id);
+
+                                return cache.save()
+                                    .then((cacheDoc) => {
+                                        const domainData = _.find(domains, (item) =>
+                                            item.databaseTable === cacheDoc.name.slice(0, -5).toUpperCase());
+
+                                        if (domainData) {
+                                            const domain = new mongo.DomainModel(domainData);
+
+                                            domain.space = cacheDoc.space;
+                                            domain.caches.push(cacheDoc._id);
+
+                                            return domain.save()
+                                                .then((domainDoc) => {
+                                                    cacheDoc.domains.push(domainDoc._id);
+
+                                                    return cacheDoc.save();
+                                                });
+                                        }
+
+                                        return cacheDoc;
+                                    });
+                            });
+
+                            return Promise.all(cachePromises)
+                                .then((cacheDocs) => {
+                                    _.forEach(cacheDocs, addCacheToCluster);
+
+                                    return cluster.save();
+                                });
+                        }
+
+                        if (cluster.name.endsWith('-igfs')) {
+                            return Promise.all(_.map(igfss, (igfs) => {
+                                const igfsDoc = new mongo.Igfs(igfs);
+
+                                igfsDoc.space = cluster.space;
+                                igfsDoc.clusters.push(cluster._id);
+
+                                return igfsDoc.save();
+                            }))
+                            .then((igfsDocs) => {
+                                _.forEach(igfsDocs, addIgfsToCluster);
+
+                                return cluster.save();
+                            });
+                        }
+                    });
+                })
+                .then(() => res.sendStatus(200))
+                .catch((err) => res.status(500).send(err.message));
+        });
+
+        factoryResolve(router);
+    });
+};
+

http://git-wip-us.apache.org/repos/asf/ignite/blob/eb5ac0ae/modules/web-console/src/main/js/serve/routes/demo/caches.json
----------------------------------------------------------------------
diff --git a/modules/web-console/src/main/js/serve/routes/demo/caches.json b/modules/web-console/src/main/js/serve/routes/demo/caches.json
new file mode 100644
index 0000000..f7a8690
--- /dev/null
+++ b/modules/web-console/src/main/js/serve/routes/demo/caches.json
@@ -0,0 +1,87 @@
+[
+  {
+    "name": "CarCache",
+    "cacheMode": "PARTITIONED",
+    "atomicityMode": "ATOMIC",
+    "readThrough": true,
+    "writeThrough": true,
+    "sqlFunctionClasses": [],
+    "cacheStoreFactory": {
+      "kind": "CacheJdbcPojoStoreFactory",
+      "CacheJdbcPojoStoreFactory": {
+        "dataSourceBean": "dsH2",
+        "dialect": "H2"
+      }
+    },
+    "domains": [],
+    "clusters": []
+  },
+  {
+    "name": "ParkingCache",
+    "cacheMode": "PARTITIONED",
+    "atomicityMode": "ATOMIC",
+    "readThrough": true,
+    "writeThrough": true,
+    "sqlFunctionClasses": [],
+    "cacheStoreFactory": {
+      "kind": "CacheJdbcPojoStoreFactory",
+      "CacheJdbcPojoStoreFactory": {
+        "dataSourceBean": "dsH2",
+        "dialect": "H2"
+      }
+    },
+    "domains": [],
+    "clusters": []
+  },
+  {
+    "name": "CountryCache",
+    "cacheMode": "PARTITIONED",
+    "atomicityMode": "ATOMIC",
+    "readThrough": true,
+    "writeThrough": true,
+    "sqlFunctionClasses": [],
+    "cacheStoreFactory": {
+      "kind": "CacheJdbcPojoStoreFactory",
+      "CacheJdbcPojoStoreFactory": {
+        "dataSourceBean": "dsH2",
+        "dialect": "H2"
+      }
+    },
+    "domains": [],
+    "clusters": []
+  },
+  {
+    "name": "DepartmentCache",
+    "cacheMode": "PARTITIONED",
+    "atomicityMode": "ATOMIC",
+    "readThrough": true,
+    "writeThrough": true,
+    "sqlFunctionClasses": [],
+    "cacheStoreFactory": {
+      "kind": "CacheJdbcPojoStoreFactory",
+      "CacheJdbcPojoStoreFactory": {
+        "dataSourceBean": "dsH2",
+        "dialect": "H2"
+      }
+    },
+    "domains": [],
+    "clusters": []
+  },
+  {
+    "name": "EmployeeCache",
+    "cacheMode": "PARTITIONED",
+    "atomicityMode": "ATOMIC",
+    "readThrough": true,
+    "writeThrough": true,
+    "sqlFunctionClasses": [],
+    "cacheStoreFactory": {
+      "kind": "CacheJdbcPojoStoreFactory",
+      "CacheJdbcPojoStoreFactory": {
+        "dataSourceBean": "dsH2",
+        "dialect": "H2"
+      }
+    },
+    "domains": [],
+    "clusters": []
+  }
+]

http://git-wip-us.apache.org/repos/asf/ignite/blob/eb5ac0ae/modules/web-console/src/main/js/serve/routes/demo/clusters.json
----------------------------------------------------------------------
diff --git a/modules/web-console/src/main/js/serve/routes/demo/clusters.json b/modules/web-console/src/main/js/serve/routes/demo/clusters.json
new file mode 100644
index 0000000..014b519
--- /dev/null
+++ b/modules/web-console/src/main/js/serve/routes/demo/clusters.json
@@ -0,0 +1,50 @@
+[
+  {
+    "name": "cluster-igfs",
+    "connector": {
+      "noDelay": true
+    },
+    "communication": {
+      "tcpNoDelay": true
+    },
+    "igfss": [],
+    "caches": [],
+    "binaryConfiguration": {
+      "compactFooter": true,
+      "typeConfigurations": []
+    },
+    "discovery": {
+      "kind": "Multicast",
+      "Multicast": {
+        "addresses": ["127.0.0.1:47500..47510"]
+      },
+      "Vm": {
+        "addresses": ["127.0.0.1:47500..47510"]
+      }
+    }
+  },
+  {
+    "name": "cluster-caches",
+    "connector": {
+      "noDelay": true
+    },
+    "communication": {
+      "tcpNoDelay": true
+    },
+    "igfss": [],
+    "caches": [],
+    "binaryConfiguration": {
+      "compactFooter": true,
+      "typeConfigurations": []
+    },
+    "discovery": {
+      "kind": "Multicast",
+      "Multicast": {
+        "addresses": ["127.0.0.1:47500..47510"]
+      },
+      "Vm": {
+        "addresses": ["127.0.0.1:47500..47510"]
+      }
+    }
+  }
+]

http://git-wip-us.apache.org/repos/asf/ignite/blob/eb5ac0ae/modules/web-console/src/main/js/serve/routes/demo/domains.json
----------------------------------------------------------------------
diff --git a/modules/web-console/src/main/js/serve/routes/demo/domains.json b/modules/web-console/src/main/js/serve/routes/demo/domains.json
new file mode 100644
index 0000000..980d8d1
--- /dev/null
+++ b/modules/web-console/src/main/js/serve/routes/demo/domains.json
@@ -0,0 +1,307 @@
+[
+  {
+    "keyType": "Integer",
+    "valueType": "model.Parking",
+    "queryMetadata": "Configuration",
+    "databaseSchema": "CARS",
+    "databaseTable": "PARKING",
+    "indexes": [],
+    "aliases": [],
+    "fields": [
+      {
+        "name": "name",
+        "className": "String"
+      },
+      {
+        "name": "capacity",
+        "className": "Integer"
+      }
+    ],
+    "valueFields": [
+      {
+        "databaseFieldName": "NAME",
+        "databaseFieldType": "VARCHAR",
+        "javaFieldName": "name",
+        "javaFieldType": "String"
+      },
+      {
+        "databaseFieldName": "CAPACITY",
+        "databaseFieldType": "INTEGER",
+        "javaFieldName": "capacity",
+        "javaFieldType": "int"
+      }
+    ],
+    "keyFields": [
+      {
+        "databaseFieldName": "ID",
+        "databaseFieldType": "INTEGER",
+        "javaFieldName": "id",
+        "javaFieldType": "int"
+      }
+    ],
+    "caches": []
+  },
+  {
+    "keyType": "Integer",
+    "valueType": "model.Department",
+    "queryMetadata": "Configuration",
+    "databaseSchema": "PUBLIC",
+    "databaseTable": "DEPARTMENT",
+    "indexes": [],
+    "aliases": [],
+    "fields": [
+      {
+        "name": "countryId",
+        "className": "Integer"
+      },
+      {
+        "name": "name",
+        "className": "String"
+      }
+    ],
+    "valueFields": [
+      {
+        "databaseFieldName": "COUNTRY_ID",
+        "databaseFieldType": "INTEGER",
+        "javaFieldName": "countryId",
+        "javaFieldType": "int"
+      },
+      {
+        "databaseFieldName": "NAME",
+        "databaseFieldType": "VARCHAR",
+        "javaFieldName": "name",
+        "javaFieldType": "String"
+      }
+    ],
+    "keyFields": [
+      {
+        "databaseFieldName": "ID",
+        "databaseFieldType": "INTEGER",
+        "javaFieldName": "id",
+        "javaFieldType": "int"
+      }
+    ],
+    "caches": []
+  },
+  {
+    "keyType": "Integer",
+    "valueType": "model.Employee",
+    "queryMetadata": "Configuration",
+    "databaseSchema": "PUBLIC",
+    "databaseTable": "EMPLOYEE",
+    "indexes": [
+      {
+        "name": "EMP_NAMES",
+        "indexType": "SORTED",
+        "fields": [
+          {
+            "name": "firstName",
+            "direction": true
+          },
+          {
+            "name": "lastName",
+            "direction": true
+          }
+        ]
+      },
+      {
+        "name": "EMP_SALARY",
+        "indexType": "SORTED",
+        "fields": [
+          {
+            "name": "salary",
+            "direction": true
+          }
+        ]
+      }
+    ],
+    "aliases": [],
+    "fields": [
+      {
+        "name": "departmentId",
+        "className": "Integer"
+      },
+      {
+        "name": "managerId",
+        "className": "Integer"
+      },
+      {
+        "name": "firstName",
+        "className": "String"
+      },
+      {
+        "name": "lastName",
+        "className": "String"
+      },
+      {
+        "name": "email",
+        "className": "String"
+      },
+      {
+        "name": "phoneNumber",
+        "className": "String"
+      },
+      {
+        "name": "hireDate",
+        "className": "Date"
+      },
+      {
+        "name": "job",
+        "className": "String"
+      },
+      {
+        "name": "salary",
+        "className": "Double"
+      }
+    ],
+    "valueFields": [
+      {
+        "databaseFieldName": "DEPARTMENT_ID",
+        "databaseFieldType": "INTEGER",
+        "javaFieldName": "departmentId",
+        "javaFieldType": "int"
+      },
+      {
+        "databaseFieldName": "MANAGER_ID",
+        "databaseFieldType": "INTEGER",
+        "javaFieldName": "managerId",
+        "javaFieldType": "Integer"
+      },
+      {
+        "databaseFieldName": "FIRST_NAME",
+        "databaseFieldType": "VARCHAR",
+        "javaFieldName": "firstName",
+        "javaFieldType": "String"
+      },
+      {
+        "databaseFieldName": "LAST_NAME",
+        "databaseFieldType": "VARCHAR",
+        "javaFieldName": "lastName",
+        "javaFieldType": "String"
+      },
+      {
+        "databaseFieldName": "EMAIL",
+        "databaseFieldType": "VARCHAR",
+        "javaFieldName": "email",
+        "javaFieldType": "String"
+      },
+      {
+        "databaseFieldName": "PHONE_NUMBER",
+        "databaseFieldType": "VARCHAR",
+        "javaFieldName": "phoneNumber",
+        "javaFieldType": "String"
+      },
+      {
+        "databaseFieldName": "HIRE_DATE",
+        "databaseFieldType": "DATE",
+        "javaFieldName": "hireDate",
+        "javaFieldType": "Date"
+      },
+      {
+        "databaseFieldName": "JOB",
+        "databaseFieldType": "VARCHAR",
+        "javaFieldName": "job",
+        "javaFieldType": "String"
+      },
+      {
+        "databaseFieldName": "SALARY",
+        "databaseFieldType": "DOUBLE",
+        "javaFieldName": "salary",
+        "javaFieldType": "Double"
+      }
+    ],
+    "keyFields": [
+      {
+        "databaseFieldName": "ID",
+        "databaseFieldType": "INTEGER",
+        "javaFieldName": "id",
+        "javaFieldType": "int"
+      }
+    ],
+    "caches": []
+  },
+  {
+    "keyType": "Integer",
+    "valueType": "model.Country",
+    "queryMetadata": "Configuration",
+    "databaseSchema": "PUBLIC",
+    "databaseTable": "COUNTRY",
+    "indexes": [],
+    "aliases": [],
+    "fields": [
+      {
+        "name": "name",
+        "className": "String"
+      },
+      {
+        "name": "population",
+        "className": "Integer"
+      }
+    ],
+    "valueFields": [
+      {
+        "databaseFieldName": "NAME",
+        "databaseFieldType": "VARCHAR",
+        "javaFieldName": "name",
+        "javaFieldType": "String"
+      },
+      {
+        "databaseFieldName": "POPULATION",
+        "databaseFieldType": "INTEGER",
+        "javaFieldName": "population",
+        "javaFieldType": "int"
+      }
+    ],
+    "keyFields": [
+      {
+        "databaseFieldName": "ID",
+        "databaseFieldType": "INTEGER",
+        "javaFieldName": "id",
+        "javaFieldType": "int"
+      }
+    ],
+    "caches": []
+  },
+  {
+    "keyType": "Integer",
+    "valueType": "model.Car",
+    "queryMetadata": "Configuration",
+    "databaseSchema": "CARS",
+    "databaseTable": "CAR",
+    "indexes": [],
+    "aliases": [],
+    "fields": [
+      {
+        "name": "parkingId",
+        "className": "Integer"
+      },
+      {
+        "name": "name",
+        "className": "String"
+      }
+    ],
+    "valueFields": [
+      {
+        "databaseFieldName": "PARKING_ID",
+        "databaseFieldType": "INTEGER",
+        "javaFieldName": "parkingId",
+        "javaFieldType": "int"
+      },
+      {
+        "databaseFieldName": "NAME",
+        "databaseFieldType": "VARCHAR",
+        "javaFieldName": "name",
+        "javaFieldType": "String"
+      }
+    ],
+    "keyFields": [
+      {
+        "databaseFieldName": "ID",
+        "databaseFieldType": "INTEGER",
+        "javaFieldName": "id",
+        "javaFieldType": "int"
+      }
+    ],
+    "caches": []
+  }
+]

http://git-wip-us.apache.org/repos/asf/ignite/blob/eb5ac0ae/modules/web-console/src/main/js/serve/routes/demo/igfss.json
----------------------------------------------------------------------
diff --git a/modules/web-console/src/main/js/serve/routes/demo/igfss.json b/modules/web-console/src/main/js/serve/routes/demo/igfss.json
new file mode 100644
index 0000000..cd128a6
--- /dev/null
+++ b/modules/web-console/src/main/js/serve/routes/demo/igfss.json
@@ -0,0 +1,10 @@
+[
+  {
+    "ipcEndpointEnabled": true,
+    "fragmentizerEnabled": true,
+    "name": "igfs",
+    "dataCacheName": "igfs-data",
+    "metaCacheName": "igfs-meta",
+    "clusters": []
+  }
+]

http://git-wip-us.apache.org/repos/asf/ignite/blob/eb5ac0ae/modules/web-console/src/main/js/serve/routes/domains.js
----------------------------------------------------------------------
diff --git a/modules/web-console/src/main/js/serve/routes/domains.js b/modules/web-console/src/main/js/serve/routes/domains.js
new file mode 100644
index 0000000..9dbf418
--- /dev/null
+++ b/modules/web-console/src/main/js/serve/routes/domains.js
@@ -0,0 +1,195 @@
+/*
+ * 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.
+ */
+
+'use strict';
+
+// Fire me up!
+
+module.exports = {
+    implements: 'domains-routes',
+    inject: ['require(lodash)', 'require(express)', 'mongo']
+};
+
+module.exports.factory = (_, express, mongo) => {
+    return new Promise((factoryResolve) => {
+        const router = new express.Router();
+
+        /**
+         * Get spaces and domain models accessed for user account.
+         *
+         * @param req Request.
+         * @param res Response.
+         */
+        router.post('/list', (req, res) => {
+            const result = {};
+            let spacesIds = [];
+
+            mongo.spaces(req.currentUserId(), req.header('IgniteDemoMode'))
+                .then((spaces) => {
+                    result.spaces = spaces;
+                    spacesIds = spaces.map((space) => space._id);
+
+                    return mongo.Cluster.find({space: {$in: spacesIds}}, '_id name').sort('name').lean().exec();
+                })
+                .then((clusters) => {
+                    result.clusters = clusters;
+
+                    return mongo.Cache.find({space: {$in: spacesIds}}).sort('name').lean().exec();
+                })
+                .then((caches) => {
+                    result.caches = caches;
+
+                    return mongo.DomainModel.find({space: {$in: spacesIds}}).sort('valueType').lean().exec();
+                })
+                .then((domains) => {
+                    result.domains = domains;
+
+                    res.json(result);
+                })
+                .catch((err) => mongo.handleError(res, err));
+        });
+
+        function _updateCacheStore(cacheStoreChanges) {
+            const promises = [];
+
+            _.forEach(cacheStoreChanges, (change) =>
+                promises.push(mongo.Cache.update({_id: {$eq: change.cacheId}}, change.change, {}).exec())
+            );
+
+            return Promise.all(promises);
+        }
+
+        const _saveDomainModel = (domain, savedDomains) => {
+            const caches = domain.caches;
+            const cacheStoreChanges = domain.cacheStoreChanges;
+            const domainId = domain._id;
+
+            return mongo.DomainModel.findOne({space: domain.space, valueType: domain.valueType}).exec()
+                .then((_domain) => {
+                    if (_domain && domainId !== _domain._id.toString())
+                        throw new Error('Domain model with value type: "' + _domain.valueType + '" already exist.');
+
+                    if (domainId) {
+                        return mongo.DomainModel.update({_id: domain._id}, domain, {upsert: true}).exec()
+                            .then(() => mongo.Cache.update({_id: {$in: caches}}, {$addToSet: {domains: domainId}}, {multi: true}).exec())
+                            .then(() => mongo.Cache.update({_id: {$nin: caches}}, {$pull: {domains: domainId}}, {multi: true}).exec())
+                            .then(() => {
+                                savedDomains.push(domain);
+
+                                return _updateCacheStore(cacheStoreChanges);
+                            });
+                    }
+
+                    return (new mongo.DomainModel(domain)).save()
+                        .then((savedDomain) => {
+                            savedDomains.push(savedDomain);
+
+                            return mongo.Cache.update({_id: {$in: caches}}, {$addToSet: {domains: savedDomain._id}}, {multi: true}).exec();
+                        })
+                        .then(() => _updateCacheStore(cacheStoreChanges));
+                });
+        };
+
+        const _save = (domains, res) => {
+            if (domains && domains.length > 0) {
+                const savedDomains = [];
+                const generatedCaches = [];
+                const promises = [];
+
+                _.forEach(domains, (domain) => {
+                    if (domain.newCache) {
+                        promises.push(
+                            mongo.Cache.findOne({space: domain.space, name: domain.newCache.name}).exec()
+                                .then((cache) => {
+                                    if (cache)
+                                        return Promise.resolve(cache);
+
+                                    // If cache not found, then create it and associate with domain model.
+                                    const newCache = domain.newCache;
+                                    newCache.space = domain.space;
+
+                                    return (new mongo.Cache(newCache)).save()
+                                        .then((_cache) => {
+                                            generatedCaches.push(_cache);
+
+                                            return mongo.Cluster.update({_id: {$in: _cache.clusters}}, {$addToSet: {caches: _cache._id}}, {multi: true}).exec()
+                                                .then(() => Promise.resolve(_cache));
+                                        });
+                                })
+                                .then((cache) => {
+                                    domain.caches = [cache._id];
+
+                                    return _saveDomainModel(domain, savedDomains);
+                                })
+                                .catch((err) => mongo.handleError(res, err))
+                        );
+                    }
+                    else
+                        promises.push(_saveDomainModel(domain, savedDomains));
+                });
+
+                Promise.all(promises)
+                    .then(() => res.send({savedDomains, generatedCaches}))
+                    .catch((err) => mongo.handleError(res, err));
+            }
+            else
+                res.status(500).send('Nothing to save!');
+        };
+
+        /**
+         * Save domain model.
+         */
+        router.post('/save', (req, res) => {
+            _save([req.body], res);
+        });
+
+        /**
+         * Batch save domain models.
+         */
+        router.post('/save/batch', (req, res) => {
+            _save(req.body, res);
+        });
+
+        /**
+         * Remove domain model by ._id.
+         */
+        router.post('/remove', (req, res) => {
+            const params = req.body;
+            const domainId = params._id;
+
+            mongo.DomainModel.findOne(params).exec()
+                .then((domain) => mongo.Cache.update({_id: {$in: domain.caches}}, {$pull: {domains: domainId}}, {multi: true}).exec())
+                .then(() => mongo.DomainModel.remove(params).exec())
+                .then(() => res.sendStatus(200))
+                .catch((err) => mongo.handleError(res, err));
+        });
+
+        /**
+         * Remove all domain models.
+         */
+        router.post('/remove/all', (req, res) => {
+            mongo.spaceIds(req.currentUserId(), req.header('IgniteDemoMode'))
+                .then((spaceIds) => mongo.Cache.update({space: {$in: spaceIds}}, {domains: []}, {multi: true}).exec()
+                        .then(() => mongo.DomainModel.remove({space: {$in: spaceIds}}).exec()))
+                .then(() => res.sendStatus(200))
+                .catch((err) => mongo.handleError(res, err));
+        });
+
+        factoryResolve(router);
+    });
+};
+

http://git-wip-us.apache.org/repos/asf/ignite/blob/eb5ac0ae/modules/web-console/src/main/js/serve/routes/igfs.js
----------------------------------------------------------------------
diff --git a/modules/web-console/src/main/js/serve/routes/igfs.js b/modules/web-console/src/main/js/serve/routes/igfs.js
new file mode 100644
index 0000000..6e5e60c
--- /dev/null
+++ b/modules/web-console/src/main/js/serve/routes/igfs.js
@@ -0,0 +1,122 @@
+/*
+ * 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.
+ */
+
+'use strict';
+
+// Fire me up!
+
+module.exports = {
+    implements: 'igfs-routes',
+    inject: ['require(lodash)', 'require(express)', 'mongo']
+};
+
+module.exports.factory = function(_, express, mongo) {
+    return new Promise((factoryResolve) => {
+        const router = new express.Router();
+
+        /**
+         * Get spaces and IGFSs accessed for user account.
+         *
+         * @param req Request.
+         * @param res Response.
+         */
+        router.post('/list', (req, res) => {
+            const result = {};
+            let spaceIds = [];
+
+            // Get owned space and all accessed space.
+            mongo.spaces(req.currentUserId(), req.header('IgniteDemoMode'))
+                .then((spaces) => {
+                    result.spaces = spaces;
+                    spaceIds = spaces.map((space) => space._id);
+
+                    return mongo.Cluster.find({space: {$in: spaceIds}}, '_id name').sort('name').lean().exec();
+                })
+                .then((clusters) => {
+                    result.clusters = clusters;
+
+                    return mongo.Igfs.find({space: {$in: spaceIds}}).sort('name').lean().exec();
+                })
+                .then((igfss) => {
+                    result.igfss = igfss;
+
+                    res.json(result);
+                })
+                .catch((err) => mongo.handleError(res, err));
+        });
+
+        /**
+         * Save IGFS.
+         */
+        router.post('/save', (req, res) => {
+            const params = req.body;
+            const clusters = params.clusters;
+
+            mongo.Igfs.findOne({space: params.space, name: params.name}).exec()
+                .then((_igfs) => {
+                    const igfsId = params._id;
+
+                    if (_igfs && igfsId !== _igfs._id.toString())
+                        return res.status(500).send('IGFS with name: "' + params.name + '" already exist.');
+
+                    if (params._id) {
+                        return mongo.Igfs.update({_id: igfsId}, params, {upsert: true}).exec()
+                            .then(() => mongo.Cluster.update({_id: {$in: clusters}}, {$addToSet: {igfss: igfsId}}, {multi: true}).exec())
+                            .then(() => mongo.Cluster.update({_id: {$nin: clusters}}, {$pull: {igfss: igfsId}}, {multi: true}).exec())
+                            .then(() => res.send(igfsId));
+                    }
+
+                    return (new mongo.Igfs(params)).save()
+                        .then((igfs) =>
+                            mongo.Cluster.update({_id: {$in: clusters}}, {$addToSet: {igfss: igfsId}}, {multi: true}).exec()
+                                .then(() => res.send(igfs._id))
+                        );
+                })
+                .catch((err) => mongo.handleError(res, err));
+        });
+
+        /**
+         * Remove IGFS by ._id.
+         */
+        router.post('/remove', (req, res) => {
+            const params = req.body;
+            const igfsId = params._id;
+
+            mongo.Cluster.update({igfss: {$in: [igfsId]}}, {$pull: {igfss: igfsId}}, {multi: true}).exec()
+                .then(() => mongo.Igfs.remove(params).exec())
+                .then(() => res.sendStatus(200))
+                .catch((err) => mongo.handleError(res, err));
+        });
+
+        /**
+         * Remove all IGFSs.
+         */
+        router.post('/remove/all', (req, res) => {
+            // Get owned space and all accessed space.
+            mongo.spaceIds(req.currentUserId(), req.header('IgniteDemoMode'))
+                .then((spaceIds) =>
+                    mongo.Cluster.update({space: {$in: spaceIds}}, {igfss: []}, {multi: true}).exec()
+                        .then(() => mongo.Igfs.remove({space: {$in: spaceIds}}).exec())
+                )
+                .then(() => res.sendStatus(200))
+                .catch((err) => mongo.handleError(res, err));
+        });
+
+        factoryResolve(router);
+    });
+};
+

http://git-wip-us.apache.org/repos/asf/ignite/blob/eb5ac0ae/modules/web-console/src/main/js/serve/routes/notebooks.js
----------------------------------------------------------------------
diff --git a/modules/web-console/src/main/js/serve/routes/notebooks.js b/modules/web-console/src/main/js/serve/routes/notebooks.js
new file mode 100644
index 0000000..37665bf
--- /dev/null
+++ b/modules/web-console/src/main/js/serve/routes/notebooks.js
@@ -0,0 +1,121 @@
+/*
+ * 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.
+ */
+
+'use strict';
+
+// Fire me up!
+
+module.exports = {
+    implements: 'notebooks-routes',
+    inject: ['require(express)', 'mongo']
+};
+
+module.exports.factory = function(express, mongo) {
+    return new Promise((factoryResolve) => {
+        const router = new express.Router();
+
+        /**
+         * Get notebooks names accessed for user account.
+         *
+         * @param req Request.
+         * @param res Response.
+         */
+        router.post('/list', (req, res) => {
+            mongo.spaces(req.currentUserId())
+                .then((spaces) => mongo.Notebook.find({space: {$in: spaces.map((value) => value._id)}}).select('_id name').sort('name').lean().exec())
+                .then((notebooks) => res.json(notebooks))
+                .catch((err) => mongo.handleError(res, err));
+
+        });
+
+        /**
+         * Get notebook accessed for user account.
+         *
+         * @param req Request.
+         * @param res Response.
+         */
+        router.post('/get', (req, res) => {
+            mongo.spaces(req.currentUserId())
+                .then((spaces) => mongo.Notebook.findOne({space: {$in: spaces.map((value) => value._id)}, _id: req.body.noteId}).lean().exec())
+                .then((notebook) => res.json(notebook))
+                .catch((err) => mongo.handleError(res, err));
+        });
+
+        /**
+         * Save notebook accessed for user account.
+         *
+         * @param req Request.
+         * @param res Response.
+         */
+        router.post('/save', (req, res) => {
+            const note = req.body;
+
+            mongo.Notebook.findOne({space: note.space, name: note.name}).exec()
+                .then((notebook) => {
+                    const noteId = note._id;
+
+                    if (notebook && noteId !== notebook._id.toString())
+                        throw new Error('Notebook with name: "' + notebook.name + '" already exist.');
+
+                    if (noteId) {
+                        return mongo.Notebook.update({_id: noteId}, note, {upsert: true}).exec()
+                            .then(() => res.send(noteId))
+                            .catch((err) => mongo.handleError(res, err));
+                    }
+
+                    return (new mongo.Notebook(req.body)).save();
+                })
+                .then((notebook) => res.send(notebook._id))
+                .catch((err) => mongo.handleError(res, err));
+        });
+
+        /**
+         * Remove notebook by ._id.
+         *
+         * @param req Request.
+         * @param res Response.
+         */
+        router.post('/remove', (req, res) => {
+            mongo.Notebook.remove(req.body).exec()
+                .then(() => res.sendStatus(200))
+                .catch((err) => mongo.handleError(res, err));
+        });
+
+        /**
+         * Create new notebook for user account.
+         *
+         * @param req Request.
+         * @param res Response.
+         */
+        router.post('/new', (req, res) => {
+            mongo.spaceIds(req.currentUserId())
+                .then((spaceIds) =>
+                    mongo.Notebook.findOne({space: spaceIds[0], name: req.body.name})
+                        .then((notebook) => {
+                            if (notebook)
+                                throw new Error('Notebook with name: "' + notebook.name + '" already exist.');
+
+                            return spaceIds;
+                        }))
+                .then((spaceIds) => (new mongo.Notebook({space: spaceIds[0], name: req.body.name})).save())
+                .then((notebook) => res.send(notebook._id))
+                .catch((err) => mongo.handleError(res, err));
+        });
+
+        factoryResolve(router);
+    });
+};

http://git-wip-us.apache.org/repos/asf/ignite/blob/eb5ac0ae/modules/web-console/src/main/js/serve/routes/profile.js
----------------------------------------------------------------------
diff --git a/modules/web-console/src/main/js/serve/routes/profile.js b/modules/web-console/src/main/js/serve/routes/profile.js
new file mode 100644
index 0000000..5e4278f
--- /dev/null
+++ b/modules/web-console/src/main/js/serve/routes/profile.js
@@ -0,0 +1,95 @@
+/*
+ * 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.
+ */
+
+'use strict';
+
+// Fire me up!
+
+module.exports = {
+    implements: 'profile-routes',
+    inject: ['require(lodash)', 'require(express)', 'mongo', 'agent-manager']
+};
+
+/**
+ *
+ * @param _ Lodash module
+ * @param express Express module
+ * @param mongo
+ * @param {AgentManager} agentMgr
+ * @returns {Promise}
+ */
+module.exports.factory = function(_, express, mongo, agentMgr) {
+    return new Promise((resolveFactory) => {
+        const router = new express.Router();
+
+        /**
+         * Save user profile.
+         */
+        router.post('/save', (req, res) => {
+            const params = req.body;
+
+            if (params.password && _.isEmpty(params.password))
+                return res.status(500).send('Wrong value for new password!');
+
+            mongo.Account.findById(params._id).exec()
+                .then((user) => {
+                    if (!params.password)
+                        return Promise.resolve(user);
+
+                    return new Promise((resolve, reject) => {
+                        user.setPassword(params.password, (err, _user) => {
+                            if (err)
+                                return reject(err);
+
+                            delete params.password;
+
+                            resolve(_user);
+                        });
+                    });
+                })
+                .then((user) => {
+                    if (!params.email || user.email === params.email)
+                        return Promise.resolve(user);
+
+                    return new Promise((resolve, reject) => {
+                        mongo.Account.findOne({email: params.email}, (err, _user) => {
+                            // TODO send error to admin
+                            if (err)
+                                reject(new Error('Failed to check email!'));
+
+                            if (_user && _user._id !== user._id)
+                                reject(new Error('User with this email already registered!'));
+
+                            resolve(user);
+                        });
+                    });
+                })
+                .then((user) => {
+                    if (params.token && user.token !== params.token)
+                        agentMgr.close(user._id);
+
+                    _.extend(user, params);
+
+                    return user.save();
+                })
+                .then(() => res.sendStatus(200))
+                .catch((err) => mongo.handleError(res, err));
+        });
+
+        resolveFactory(router);
+    });
+};


Mime
View raw message