You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ignite.apache.org by ak...@apache.org on 2016/09/09 03:27:00 UTC

[32/52] ignite git commit: Web Console beta-3.

http://git-wip-us.apache.org/repos/asf/ignite/blob/6af6560a/modules/web-console/frontend/app/modules/states/configuration/igfs/misc.jade
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/modules/states/configuration/igfs/misc.jade b/modules/web-console/frontend/app/modules/states/configuration/igfs/misc.jade
new file mode 100644
index 0000000..cb4687a
--- /dev/null
+++ b/modules/web-console/frontend/app/modules/states/configuration/igfs/misc.jade
@@ -0,0 +1,108 @@
+//-
+    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.
+
+include ../../../../../app/helpers/jade/mixins.jade
+
+-var form = 'misc'
+-var model = 'backupItem'
+-var pathModesForm = 'miscPathModes'
+-var pathModes = model + '.pathModes'
+
+//- LEGACY mixin for LEGACY IGFS path modes table.
+mixin table-igfs-path-mode-edit(prefix, focusId, index)
+    -var keyModel = 'tblPathModes.' + prefix + 'Key'
+    -var valModel = 'tblPathModes.' + prefix + 'Value'
+
+    -var keyFocusId = prefix + 'Key' + focusId
+    -var valFocusId = prefix + 'Value' + focusId
+
+    .col-xs-8.col-sm-8.col-md-8
+        .fieldSep /
+        .input-tip
+            input.form-control(id=keyFocusId ignite-on-enter-focus-move=valFocusId type='text' ng-model=keyModel placeholder='Path' ignite-on-escape='tableReset()')
+    .col-xs-4.col-sm-4.col-md-4
+        -var arg = keyModel + ', ' + valModel
+        -var btnVisible = 'tablePairSaveVisible(tblPathModes, ' + index + ')'
+        -var btnSave = 'tablePairSave(tablePairValid, backupItem, tblPathModes, ' + index + ')'
+        -var btnVisibleAndSave = btnVisible + ' && ' + btnSave
+        +btn-save(btnVisible, btnSave)
+        .input-tip
+            button.select-toggle.form-control(id=valFocusId bs-select ng-model=valModel data-placeholder='Mode' bs-options='item.value as item.label for item in igfsModes' tabindex='0' ignite-on-enter=btnVisibleAndSave ignite-on-escape='tableReset()')
+
+.panel.panel-default(ng-form=form novalidate)
+    .panel-heading(bs-collapse-toggle='' ng-click='ui.loadPanel("#{form}")')
+        ignite-form-panel-chevron
+        label Miscellaneous
+        ignite-form-field-tooltip.tipLabel
+            | Various miscellaneous IGFS settings
+        ignite-form-revert
+    .panel-collapse(role='tabpanel' bs-collapse-target id=form)
+        .panel-body(ng-if='ui.isPanelLoaded("#{form}")')
+            .col-sm-6
+                .settings-row
+                    +number('Block size:', model + '.blockSize', '"blockSize"', 'true', '65536', '0', 'File data block size in bytes')
+                .settings-row
+                    +number('Stream buffer size:', model + '.streamBufferSize', '"streamBufferSize"', 'true', '65536', '0', 'Read/write buffer size for IGFS stream operations in bytes')
+                .settings-row
+                    +number('Maximum space size:', model + '.maxSpaceSize', '"maxSpaceSize"', 'true', '0', '0', 'Maximum space available for data cache to store file system entries')
+                .settings-row
+                    +number('Maximum task range length:', model + '.maximumTaskRangeLength', '"maximumTaskRangeLength"', 'true', '0', '0', 'Maximum default range size of a file being split during IGFS task execution')
+                .settings-row
+                    +number-min-max('Management port:', model + '.managementPort', '"managementPort"', 'true', '11400', '0', '65535', 'Port number for management endpoint')
+                .settings-row
+                    +number('Per node batch size:', model + '.perNodeBatchSize', '"perNodeBatchSize"', 'true', '100', '0', 'Number of file blocks collected on local node before sending batch to remote node')
+                .settings-row
+                    +number('Per node parallel batch count:', model + '.perNodeParallelBatchCount', '"perNodeParallelBatchCount"', 'true', '8', '0', 'Number of file block batches that can be concurrently sent to remote node')
+                .settings-row
+                    +number('Prefetch blocks:', model + '.prefetchBlocks', '"prefetchBlocks"', 'true', '0', '0', 'Number of pre-fetched blocks if specific file chunk is requested')
+                .settings-row
+                    +number('Sequential reads before prefetch:', model + '.sequentialReadsBeforePrefetch', '"sequentialReadsBeforePrefetch"', 'true', '0', '0', 'Amount of sequential block reads before prefetch is triggered')
+                .settings-row
+                    +number('Trash purge timeout:', model + '.trashPurgeTimeout', '"trashPurgeTimeout"', 'true', '1000', '0', 'Maximum timeout awaiting for trash purging in case data cache oversize is detected')
+                .settings-row
+                    +checkbox('Colocate metadata', model + '.colocateMetadata', '"colocateMetadata"', 'Whether to co-locate metadata on a single node')
+                .settings-row
+                    +checkbox('Relaxed consistency', model + '.relaxedConsistency', '"relaxedConsistency"',
+                        'If value of this flag is <b>true</b>, IGFS will skip expensive consistency checks<br/>\
+                        It is recommended to set this flag to <b>false</b> if your application has conflicting\
+                        operations, or you do not know how exactly users will use your system')
+                .settings-row
+                    +ignite-form-group(ng-model=pathModes ng-form=pathModesForm)
+                        ignite-form-field-label
+                            | Path modes
+                        ignite-form-group-tooltip
+                            | Map of path prefixes to IGFS modes used for them
+                        ignite-form-group-add(ng-click='tableNewItem(tblPathModes)')
+                            | Add path mode
+
+                        .group-content-empty(ng-if='!((#{pathModes} && #{pathModes}.length > 0) || tableNewItemActive(tblPathModes))') Not defined
+
+                        .group-content(ng-show='(#{pathModes} && #{pathModes}.length > 0) || tableNewItemActive(tblPathModes)')
+                            table.links-edit(id='pathModes' st-table=pathModes)
+                                tbody
+                                    tr(ng-repeat='item in #{pathModes}')
+                                        td.col-sm-12(ng-show='!tableEditing(tblPathModes, $index)')
+                                            a.labelFormField(ng-click='tableStartEdit(backupItem, tblPathModes, $index)') {{item.path + " [" + item.mode + "]"}}
+                                            +btn-remove('tableRemove(backupItem, tblPathModes, $index)', '"Remove path"')
+                                        td.col-sm-12(ng-show='tableEditing(tblPathModes, $index)')
+                                            +table-igfs-path-mode-edit('cur', '{{::tblPathModes.focusId + $index}}', '$index')
+                                tfoot(ng-show='tableNewItemActive(tblPathModes)')
+                                    tr
+                                        td.col-sm-12
+                                            +table-igfs-path-mode-edit('new', 'PathMode', '-1')
+
+            .col-sm-6
+                +preview-xml-java(model, 'igfsMisc')

http://git-wip-us.apache.org/repos/asf/ignite/blob/6af6560a/modules/web-console/frontend/app/modules/states/configuration/igfs/secondary.jade
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/modules/states/configuration/igfs/secondary.jade b/modules/web-console/frontend/app/modules/states/configuration/igfs/secondary.jade
new file mode 100644
index 0000000..0649527
--- /dev/null
+++ b/modules/web-console/frontend/app/modules/states/configuration/igfs/secondary.jade
@@ -0,0 +1,44 @@
+//-
+    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.
+
+include ../../../../../app/helpers/jade/mixins.jade
+
+-var form = 'secondaryFileSystem'
+-var model = 'backupItem'
+
+.panel.panel-default(ng-form=form novalidate)
+    .panel-heading(bs-collapse-toggle='' ng-click='ui.loadPanel("#{form}")')
+        ignite-form-panel-chevron
+        label(id="secondaryFileSystem-title") Secondary file system
+        ignite-form-field-tooltip.tipLabel
+            | Secondary file system is provided for pass-through, write-through, and read-through purposes
+        ignite-form-revert
+    .panel-collapse(role='tabpanel' bs-collapse-target id=form)
+        .panel-body(ng-if='ui.isPanelLoaded("#{form}")')
+            .col-sm-6
+                -var enabled = model + '.secondaryFileSystemEnabled'
+                -var secondaryFileSystem = model + '.secondaryFileSystem'
+
+                .settings-row
+                    +checkbox('Enabled', enabled, '"secondaryFileSystemEnabled"', 'Secondary file system enabled flag')
+                .settings-row
+                    +text-enabled('URI:', secondaryFileSystem + '.uri', '"hadoopURI"', enabled, 'false', 'hdfs://[namenodehost]:[port]/[path]', 'URI of file system')
+                .settings-row
+                    +text-enabled('Config path:', secondaryFileSystem + '.cfgPath', '"cfgPath"', enabled, 'false', 'Path to additional config', 'Additional path to Hadoop configuration')
+                .settings-row
+                    +text-enabled('User name:', secondaryFileSystem + '.userName', '"userName"', enabled, 'false', 'Input user name', 'User name')
+            .col-sm-6
+                +preview-xml-java(model, 'igfsSecondFS')

http://git-wip-us.apache.org/repos/asf/ignite/blob/6af6560a/modules/web-console/frontend/app/modules/states/configuration/preview-panel.directive.js
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/modules/states/configuration/preview-panel.directive.js b/modules/web-console/frontend/app/modules/states/configuration/preview-panel.directive.js
new file mode 100644
index 0000000..be7bf1e
--- /dev/null
+++ b/modules/web-console/frontend/app/modules/states/configuration/preview-panel.directive.js
@@ -0,0 +1,239 @@
+/*
+ * 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 ace from 'brace';
+
+export default ['previewPanel', ['$interval', '$timeout', ($interval, $timeout) => {
+    let animation = {editor: null, stage: 0, start: 0, stop: 0};
+    let prevContent = [];
+
+    const Range = ace.acequire('ace/range').Range;
+
+    const _clearSelection = (editor) => {
+        _.forEach(editor.session.getMarkers(false), (marker) => {
+            editor.session.removeMarker(marker.id);
+        });
+    };
+
+    /**
+     * Switch to next stage of animation.
+     */
+    const _animate = () => {
+        animation.stage += animation.step;
+
+        const stage = animation.stage;
+        const editor = animation.editor;
+
+        _clearSelection(editor);
+
+        animation.selections.forEach((selection) => {
+            editor.session.addMarker(new Range(selection.start, 0, selection.stop, 0),
+                'preview-highlight-' + stage, 'line', false);
+        });
+
+        if (stage === animation.finalStage) {
+            editor.animatePromise = null;
+
+            if (animation.clearOnFinal)
+                _clearSelection(editor);
+        }
+    };
+
+    /**
+     * Selection with animation.
+     *
+     * @param editor Editor to show selection animation.
+     * @param selections Array of selection intervals.
+     * @param step Step of animation (1 or -1).
+     * @param stage Start stage of animation.
+     * @param finalStage Final stage of animation.
+     * @param clearOnFinal Boolean flat to clear selection on animation finish.
+     */
+    const _fade = (editor, selections, step, stage, finalStage, clearOnFinal) => {
+        const promise = editor.animatePromise;
+
+        if (promise) {
+            $interval.cancel(promise);
+
+            _clearSelection(editor);
+        }
+
+        animation = {editor, selections, step, stage, finalStage, clearOnFinal};
+
+        editor.animatePromise = $interval(_animate, 100, 10, false);
+    };
+
+    /**
+     * Show selections with animation.
+     *
+     * @param editor Editor to show selection.
+     * @param selections Array of selection intervals.
+     */
+    const _fadeIn = (editor, selections) => {
+        _fade(editor, selections, 1, 0, 10, false);
+    };
+
+    /**
+     * Hide selections with animation.
+     *
+     * @param editor Editor to show selection.
+     * @param selections Array of selection intervals.
+     */
+    const _fadeOut = (editor, selections) => {
+        _fade(editor, selections, -1, 10, 0, true);
+    };
+
+    const onChange = ([content, editor]) => {
+        const {clearPromise} = editor;
+        const {lines} = content;
+
+        if (content.action === 'remove')
+            prevContent = lines;
+        else if (prevContent.length > 0 && lines.length > 0 && editor.attractAttention) {
+            if (clearPromise) {
+                $timeout.cancel(clearPromise);
+
+                _clearSelection(editor);
+            }
+
+            const selections = [];
+
+            let newIx = 0;
+            let prevIx = 0;
+
+            let prevLen = prevContent.length - (prevContent[prevContent.length - 1] === '' ? 1 : 0);
+            let newLen = lines.length - (lines[lines.length - 1] === '' ? 1 : 0);
+
+            const removed = newLen < prevLen;
+
+            let skipEnd = 0;
+
+            let selected = false;
+            let scrollTo = -1;
+
+            while (lines[newLen - 1] === prevContent[prevLen - 1] && newLen > 0 && prevLen > 0) {
+                prevLen -= 1;
+                newLen -= 1;
+
+                skipEnd += 1;
+            }
+
+            while (newIx < newLen || prevIx < prevLen) {
+                let start = -1;
+                let stop = -1;
+
+                // Find an index of a first line with different text.
+                for (; (newIx < newLen || prevIx < prevLen) && start < 0; newIx++, prevIx++) {
+                    if (newIx >= newLen || prevIx >= prevLen || lines[newIx] !== prevContent[prevIx]) {
+                        start = newIx;
+
+                        break;
+                    }
+                }
+
+                if (start >= 0) {
+                    // Find an index of a last line with different text by checking last string of old and new content in reverse order.
+                    for (let i = start; i < newLen && stop < 0; i++) {
+                        for (let j = prevIx; j < prevLen && stop < 0; j++) {
+                            if (lines[i] === prevContent[j] && lines[i] !== '') {
+                                stop = i;
+
+                                newIx = i;
+                                prevIx = j;
+
+                                break;
+                            }
+                        }
+                    }
+
+                    if (stop < 0) {
+                        stop = newLen;
+
+                        newIx = newLen;
+                        prevIx = prevLen;
+                    }
+
+                    if (start === stop) {
+                        if (removed)
+                            start = Math.max(0, start - 1);
+
+                        stop = Math.min(newLen + skipEnd, stop + 1);
+                    }
+
+                    if (start <= stop) {
+                        selections.push({start, stop});
+
+                        if (!selected)
+                            scrollTo = start;
+
+                        selected = true;
+                    }
+                }
+            }
+
+            // Run clear selection one time.
+            if (selected) {
+                _fadeIn(editor, selections);
+
+                editor.clearPromise = $timeout(() => {
+                    _fadeOut(editor, selections);
+
+                    editor.clearPromise = null;
+                }, 2000);
+
+                editor.scrollToRow(scrollTo);
+            }
+
+            prevContent = [];
+        }
+        else
+            editor.attractAttention = true;
+    };
+
+
+    const link = (scope, $element, $attrs, [igniteUiAceTabs1, igniteUiAceTabs2]) => {
+        const igniteUiAceTabs = igniteUiAceTabs1 || igniteUiAceTabs2;
+
+        if (!igniteUiAceTabs)
+            return;
+
+        igniteUiAceTabs.onLoad = (editor) => {
+            editor.setReadOnly(true);
+            editor.setOption('highlightActiveLine', false);
+            editor.setAutoScrollEditorIntoView(true);
+            editor.$blockScrolling = Infinity;
+            editor.attractAttention = false;
+
+            const renderer = editor.renderer;
+
+            renderer.setHighlightGutterLine(false);
+            renderer.setShowPrintMargin(false);
+            renderer.setOption('fontSize', '10px');
+            renderer.setOption('maxLines', '50');
+
+            editor.setTheme('ace/theme/chrome');
+        };
+
+        igniteUiAceTabs.onChange = onChange;
+    };
+
+    return {
+        restrict: 'C',
+        link,
+        require: ['?igniteUiAceTabs', '?^igniteUiAceTabs']
+    };
+}]];

http://git-wip-us.apache.org/repos/asf/ignite/blob/6af6560a/modules/web-console/frontend/app/modules/states/configuration/summary/summary-tabs.directive.js
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/modules/states/configuration/summary/summary-tabs.directive.js b/modules/web-console/frontend/app/modules/states/configuration/summary/summary-tabs.directive.js
new file mode 100644
index 0000000..f8094af
--- /dev/null
+++ b/modules/web-console/frontend/app/modules/states/configuration/summary/summary-tabs.directive.js
@@ -0,0 +1,50 @@
+/*
+ * 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.
+ */
+
+export default ['summaryTabs', [() => {
+    const link = (scope, $element, $attrs, [igniteUiAceTabs1, igniteUiAceTabs2]) => {
+        const igniteUiAceTabs = igniteUiAceTabs1 || igniteUiAceTabs2;
+
+        if (!igniteUiAceTabs)
+            return;
+
+        igniteUiAceTabs.onLoad = (editor) => {
+            editor.setReadOnly(true);
+            editor.setOption('highlightActiveLine', false);
+            editor.setAutoScrollEditorIntoView(true);
+            editor.$blockScrolling = Infinity;
+
+            const renderer = editor.renderer;
+
+            renderer.setHighlightGutterLine(false);
+            renderer.setShowPrintMargin(false);
+            renderer.setOption('fontFamily', 'monospace');
+            renderer.setOption('fontSize', '12px');
+            renderer.setOption('minLines', '25');
+            renderer.setOption('maxLines', '25');
+
+            editor.setTheme('ace/theme/chrome');
+        };
+    };
+
+    return {
+        priority: 1000,
+        restrict: 'C',
+        link,
+        require: ['?igniteUiAceTabs', '?^igniteUiAceTabs']
+    };
+}]];

http://git-wip-us.apache.org/repos/asf/ignite/blob/6af6560a/modules/web-console/frontend/app/modules/states/configuration/summary/summary.controller.js
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/modules/states/configuration/summary/summary.controller.js b/modules/web-console/frontend/app/modules/states/configuration/summary/summary.controller.js
new file mode 100644
index 0000000..f0cb842
--- /dev/null
+++ b/modules/web-console/frontend/app/modules/states/configuration/summary/summary.controller.js
@@ -0,0 +1,365 @@
+/*
+ * 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 _ from 'lodash';
+import JSZip from 'jszip';
+import saver from 'file-saver';
+
+export default [
+    '$rootScope', '$scope', '$http', 'IgniteLegacyUtils', 'IgniteMessages', 'IgniteLoading', '$filter', 'igniteConfigurationResource', 'JavaTypes', 'IgniteVersion', 'GeneratorDocker', 'GeneratorPom', 'IgniteFormUtils',
+    function($root, $scope, $http, LegacyUtils, Messages, Loading, $filter, Resource, JavaTypes, IgniteVersion, docker, pom, FormUtils) {
+        const ctrl = this;
+
+        $scope.ui = { ready: false };
+
+        Loading.start('summaryPage');
+
+        Resource.read()
+            .then(Resource.populate)
+            .then(({clusters}) => {
+                $scope.clusters = clusters;
+                $scope.clustersMap = {};
+                $scope.clustersView = _.map(clusters, (item) => {
+                    const {_id, name} = item;
+
+                    $scope.clustersMap[_id] = item;
+
+                    return {_id, name};
+                });
+
+                Loading.finish('summaryPage');
+
+                $scope.ui.ready = true;
+
+                if (!_.isEmpty(clusters)) {
+                    const idx = sessionStorage.summarySelectedId || 0;
+
+                    $scope.selectItem(clusters[idx]);
+                }
+            })
+            .catch(Messages.showError);
+
+        $scope.contentVisible = (rows, row) => {
+            return !row || !row._id || _.findIndex(rows, (item) => item._id === row._id) >= 0;
+        };
+
+        $scope.widthIsSufficient = FormUtils.widthIsSufficient;
+        $scope.dialects = {};
+
+        $scope.projectStructureOptions = {
+            nodeChildren: 'children',
+            dirSelectable: false,
+            injectClasses: {
+                iExpanded: 'fa fa-folder-open-o',
+                iCollapsed: 'fa fa-folder-o'
+            },
+            equality: (node1, node2) => {
+                return node1 === node2;
+            }
+        };
+
+        const javaConfigFolder = {
+            type: 'folder',
+            name: 'config',
+            children: [
+                { type: 'file', name: 'ClientConfigurationFactory.java' },
+                { type: 'file', name: 'ServerConfigurationFactory.java' }
+            ]
+        };
+
+        const javaStartupFolder = {
+            type: 'folder',
+            name: 'startup',
+            children: [
+                { type: 'file', name: 'ClientNodeCodeStartup.java' },
+                { type: 'file', name: 'ClientNodeSpringStartup.java' },
+                { type: 'file', name: 'ServerNodeCodeStartup.java' },
+                { type: 'file', name: 'ServerNodeSpringStartup.java' }
+            ]
+        };
+
+        const demoFolder = {
+            type: 'folder',
+            name: 'demo',
+            children: [
+                { type: 'file', name: 'DemoStartup.java' }
+            ]
+        };
+
+        const resourcesFolder = {
+            type: 'folder',
+            name: 'resources',
+            children: [
+                { type: 'file', name: 'secret.properties' }
+            ]
+        };
+
+        const javaFolder = {
+            type: 'folder',
+            name: 'java',
+            children: [
+                {
+                    type: 'folder',
+                    name: 'config',
+                    children: [
+                        javaConfigFolder,
+                        javaStartupFolder
+                    ]
+                }
+            ]
+        };
+
+        const clnCfg = { type: 'file', name: 'client.xml' };
+
+        const srvCfg = { type: 'file', name: 'server.xml' };
+
+        const mainFolder = {
+            type: 'folder',
+            name: 'main',
+            children: [javaFolder]
+        };
+
+        const projectStructureRoot = {
+            type: 'folder',
+            name: 'project.zip',
+            children: [
+                {
+                    type: 'folder',
+                    name: 'config',
+                    children: [clnCfg, srvCfg]
+                },
+                {
+                    type: 'folder',
+                    name: 'jdbc-drivers',
+                    children: [
+                        { type: 'file', name: 'README.txt' }
+                    ]
+                },
+                {
+                    type: 'folder',
+                    name: 'src',
+                    children: [mainFolder]
+                },
+                { type: 'file', name: '.dockerignore' },
+                { type: 'file', name: 'Dockerfile' },
+                { type: 'file', name: 'pom.xml' },
+                { type: 'file', name: 'README.txt' }
+            ]
+        };
+
+        $scope.projectStructure = [projectStructureRoot];
+
+        $scope.projectStructureExpanded = [projectStructureRoot];
+
+        $scope.tabsServer = { activeTab: 0 };
+        $scope.tabsClient = { activeTab: 0 };
+
+        /**
+         *
+         * @param {Object} node - Tree node.
+         * @param {string[]} path - Path to find.
+         * @returns {Object} Tree node.
+         */
+        function getOrCreateFolder(node, path) {
+            if (_.isEmpty(path))
+                return node;
+
+            const leaf = path.shift();
+
+            let children = null;
+
+            if (!_.isEmpty(node.children)) {
+                children = _.find(node.children, {type: 'folder', name: leaf});
+
+                if (children)
+                    return getOrCreateFolder(children, path);
+            }
+
+            children = {type: 'folder', name: leaf, children: []};
+
+            node.children.push(children);
+
+            node.children = _.orderBy(node.children, ['type', 'name'], ['desc', 'asc']);
+
+            return getOrCreateFolder(children, path);
+        }
+
+        function addClass(fullClsName) {
+            const path = fullClsName.split('.');
+            const leaf = {type: 'file', name: path.pop() + '.java'};
+            const folder = getOrCreateFolder(javaFolder, path);
+
+            if (!_.find(folder.children, leaf))
+                folder.children.push(leaf);
+        }
+
+        $scope.selectItem = (cluster) => {
+            delete ctrl.cluster;
+
+            if (!cluster)
+                return;
+
+            cluster = $scope.clustersMap[cluster._id];
+
+            ctrl.cluster = cluster;
+
+            $scope.cluster = cluster;
+            $scope.selectedItem = cluster;
+            $scope.dialects = {};
+
+            sessionStorage.summarySelectedId = $scope.clusters.indexOf(cluster);
+
+            mainFolder.children = [javaFolder];
+            javaFolder.children = [javaConfigFolder, javaStartupFolder];
+
+            if ($generatorCommon.secretPropertiesNeeded(cluster))
+                mainFolder.children.push(resourcesFolder);
+
+            if ($generatorJava.isDemoConfigured(cluster, $root.IgniteDemoMode))
+                javaFolder.children.push(demoFolder);
+
+            if (cluster.discovery.kind === 'Jdbc' && cluster.discovery.Jdbc.dialect)
+                $scope.dialects[cluster.discovery.Jdbc.dialect] = true;
+
+            _.forEach(cluster.caches, (cache) => {
+                if (cache.cacheStoreFactory) {
+                    const store = cache.cacheStoreFactory[cache.cacheStoreFactory.kind];
+
+                    if (store && store.dialect)
+                        $scope.dialects[store.dialect] = true;
+                }
+
+                _.forEach(cache.domains, (domain) => {
+                    if (!_.isEmpty(domain.keyFields)) {
+                        if (JavaTypes.nonBuiltInClass(domain.keyType))
+                            addClass(domain.keyType);
+
+                        addClass(domain.valueType);
+                    }
+                });
+            });
+
+            projectStructureRoot.name = cluster.name + '-project.zip';
+            clnCfg.name = cluster.name + '-client.xml';
+            srvCfg.name = cluster.name + '-server.xml';
+        };
+
+        $scope.$watch('cluster', (cluster) => {
+            if (!cluster)
+                return;
+
+            if (!$filter('hasPojo')(cluster) && $scope.tabsClient.activeTab === 3)
+                $scope.tabsClient.activeTab = 0;
+        });
+
+        $scope.$watch('cluster._id', () => {
+            $scope.tabsClient.init = [];
+            $scope.tabsServer.init = [];
+        });
+
+        // TODO IGNITE-2114: implemented as independent logic for download.
+        $scope.downloadConfiguration = function() {
+            const cluster = $scope.cluster;
+            const clientNearCfg = cluster.clientNearCfg;
+
+            const zip = new JSZip();
+
+            if (!ctrl.data)
+                ctrl.data = {};
+
+            if (!ctrl.data.docker)
+                ctrl.data.docker = docker.generate(cluster, 'latest');
+
+            zip.file('Dockerfile', ctrl.data.docker);
+            zip.file('.dockerignore', docker.ignoreFile());
+
+            const builder = $generatorProperties.generateProperties(cluster);
+
+            if (builder)
+                zip.file('src/main/resources/secret.properties', builder.asString());
+
+            const srcPath = 'src/main/java/';
+
+            const serverXml = 'config/' + cluster.name + '-server.xml';
+            const clientXml = 'config/' + cluster.name + '-client.xml';
+
+            zip.file(serverXml, $generatorXml.cluster(cluster));
+            zip.file(clientXml, $generatorXml.cluster(cluster, clientNearCfg));
+
+            zip.file(srcPath + 'config/ServerConfigurationFactory.java', $generatorJava.cluster(cluster, 'config', 'ServerConfigurationFactory', null));
+            zip.file(srcPath + 'config/ClientConfigurationFactory.java', $generatorJava.cluster(cluster, 'config', 'ClientConfigurationFactory', clientNearCfg));
+
+            if ($generatorJava.isDemoConfigured(cluster, $root.IgniteDemoMode)) {
+                zip.file(srcPath + 'demo/DemoStartup.java', $generatorJava.nodeStartup(cluster, 'demo', 'DemoStartup',
+                    'ServerConfigurationFactory.createConfiguration()', 'config.ServerConfigurationFactory'));
+            }
+
+            zip.file(srcPath + 'startup/ServerNodeSpringStartup.java', $generatorJava.nodeStartup(cluster, 'startup', 'ServerNodeSpringStartup', '"' + serverXml + '"'));
+            zip.file(srcPath + 'startup/ClientNodeSpringStartup.java', $generatorJava.nodeStartup(cluster, 'startup', 'ClientNodeSpringStartup', '"' + clientXml + '"'));
+
+            zip.file(srcPath + 'startup/ServerNodeCodeStartup.java', $generatorJava.nodeStartup(cluster, 'startup', 'ServerNodeCodeStartup',
+                'ServerConfigurationFactory.createConfiguration()', 'config.ServerConfigurationFactory'));
+            zip.file(srcPath + 'startup/ClientNodeCodeStartup.java', $generatorJava.nodeStartup(cluster, 'startup', 'ClientNodeCodeStartup',
+                'ClientConfigurationFactory.createConfiguration()', 'config.ClientConfigurationFactory', clientNearCfg));
+
+            zip.file('pom.xml', pom.generate(cluster, IgniteVersion.version).asString());
+
+            zip.file('README.txt', $generatorReadme.readme().asString());
+            zip.file('jdbc-drivers/README.txt', $generatorReadme.readmeJdbc().asString());
+
+            if (!ctrl.data.pojos)
+                ctrl.data.pojos = $generatorJava.pojos(cluster.caches);
+
+            for (const pojo of ctrl.data.pojos) {
+                if (pojo.keyClass && JavaTypes.nonBuiltInClass(pojo.keyType))
+                    zip.file(srcPath + pojo.keyType.replace(/\./g, '/') + '.java', pojo.keyClass);
+
+                zip.file(srcPath + pojo.valueType.replace(/\./g, '/') + '.java', pojo.valueClass);
+            }
+
+            $generatorOptional.optionalContent(zip, cluster);
+
+            zip.generateAsync({type: 'blob', compression: 'DEFLATE', mimeType: 'application/octet-stream'})
+                .then((blob) => saver.saveAs(blob, cluster.name + '-project.zip'));
+        };
+
+        /**
+         * @returns {boolean} 'true' if at least one proprietary JDBC driver is configured for cache store.
+         */
+        $scope.downloadJdbcDriversVisible = function() {
+            const dialects = $scope.dialects;
+
+            return !!(dialects.Oracle || dialects.DB2 || dialects.SQLServer);
+        };
+
+        /**
+         * Open download proprietary JDBC driver pages.
+         */
+        $scope.downloadJdbcDrivers = function() {
+            const dialects = $scope.dialects;
+
+            if (dialects.Oracle)
+                window.open('http://www.oracle.com/technetwork/apps-tech/jdbc-112010-090769.html');
+
+            if (dialects.DB2)
+                window.open('http://www-01.ibm.com/support/docview.wss?uid=swg21363866');
+
+            if (dialects.SQLServer)
+                window.open('https://www.microsoft.com/en-us/download/details.aspx?id=11774');
+        };
+    }
+];

http://git-wip-us.apache.org/repos/asf/ignite/blob/6af6560a/modules/web-console/frontend/app/modules/states/errors.state.js
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/modules/states/errors.state.js b/modules/web-console/frontend/app/modules/states/errors.state.js
new file mode 100644
index 0000000..2bdb80a
--- /dev/null
+++ b/modules/web-console/frontend/app/modules/states/errors.state.js
@@ -0,0 +1,43 @@
+/*
+ * 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 templateNotFoundPage from '../../../views/404.jade';
+import templateNotAuthorizedPage from '../../../views/403.jade';
+
+angular
+    .module('ignite-console.states.errors', [
+        'ui.router'
+    ])
+    .config(['$stateProvider', 'AclRouteProvider', function($stateProvider) {
+        // set up the states
+        $stateProvider
+            .state('404', {
+                url: '/404',
+                templateUrl: templateNotFoundPage,
+                metaTags: {
+                    title: 'Page not found'
+                }
+            })
+            .state('403', {
+                url: '/403',
+                templateUrl: templateNotAuthorizedPage,
+                metaTags: {
+                    title: 'Not authorized'
+                }
+            });
+    }]);

http://git-wip-us.apache.org/repos/asf/ignite/blob/6af6560a/modules/web-console/frontend/app/modules/states/logout.state.js
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/modules/states/logout.state.js b/modules/web-console/frontend/app/modules/states/logout.state.js
new file mode 100644
index 0000000..42795ea
--- /dev/null
+++ b/modules/web-console/frontend/app/modules/states/logout.state.js
@@ -0,0 +1,35 @@
+/*
+ * 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';
+
+angular
+.module('ignite-console.states.logout', [
+    'ui.router'
+])
+.config(['$stateProvider', 'AclRouteProvider', function($stateProvider, AclRoute) {
+    // set up the states
+    $stateProvider
+    .state('logout', {
+        url: '/logout',
+        onEnter: AclRoute.checkAccess('logout'),
+        controller: ['Auth', (Auth) => Auth.logout()],
+        metaTags: {
+            title: 'Logout'
+        }
+    });
+}]);

http://git-wip-us.apache.org/repos/asf/ignite/blob/6af6560a/modules/web-console/frontend/app/modules/states/password.state.js
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/modules/states/password.state.js b/modules/web-console/frontend/app/modules/states/password.state.js
new file mode 100644
index 0000000..48d01df
--- /dev/null
+++ b/modules/web-console/frontend/app/modules/states/password.state.js
@@ -0,0 +1,46 @@
+/*
+ * 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';
+
+angular
+.module('ignite-console.states.password', [
+    'ui.router'
+])
+.config(['$stateProvider', function($stateProvider) {
+    // set up the states
+    $stateProvider
+    .state('password', {
+        url: '/password',
+        abstract: true,
+        template: '<ui-view></ui-view>'
+    })
+    .state('password.reset', {
+        url: '/reset?{token}',
+        templateUrl: '/reset.html',
+        metaTags: {
+            title: 'Reset password'
+        }
+    })
+    .state('password.send', {
+        url: '/send',
+        templateUrl: '/reset.html',
+        metaTags: {
+            title: 'Password Send'
+        }
+    });
+}]);

http://git-wip-us.apache.org/repos/asf/ignite/blob/6af6560a/modules/web-console/frontend/app/modules/states/profile.state.js
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/modules/states/profile.state.js b/modules/web-console/frontend/app/modules/states/profile.state.js
new file mode 100644
index 0000000..9c31340
--- /dev/null
+++ b/modules/web-console/frontend/app/modules/states/profile.state.js
@@ -0,0 +1,35 @@
+/*
+ * 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';
+
+angular
+.module('ignite-console.states.profile', [
+    'ui.router'
+])
+.config(['$stateProvider', 'AclRouteProvider', function($stateProvider, AclRoute) {
+    // set up the states
+    $stateProvider
+    .state('settings.profile', {
+        url: '/profile',
+        templateUrl: '/settings/profile.html',
+        onEnter: AclRoute.checkAccess('profile'),
+        metaTags: {
+            title: 'User profile'
+        }
+    });
+}]);

http://git-wip-us.apache.org/repos/asf/ignite/blob/6af6560a/modules/web-console/frontend/app/modules/states/signin.state.js
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/modules/states/signin.state.js b/modules/web-console/frontend/app/modules/states/signin.state.js
new file mode 100644
index 0000000..14ebc1b
--- /dev/null
+++ b/modules/web-console/frontend/app/modules/states/signin.state.js
@@ -0,0 +1,43 @@
+/*
+ * 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 templateUrl from 'views/signin.jade';
+
+angular
+.module('ignite-console.states.login', [
+    'ui.router',
+    // services
+    'ignite-console.user'
+])
+.config(['$stateProvider', 'AclRouteProvider', function($stateProvider) {
+    // set up the states
+    $stateProvider
+    .state('signin', {
+        url: '/',
+        templateUrl,
+        resolve: {
+            user: ['$state', 'User', ($state, User) => {
+                return User.read()
+                    .then(() => $state.go('base.configuration.clusters'))
+                    .catch(() => {});
+            }]
+        },
+        metaTags: {
+        }
+    });
+}]);

http://git-wip-us.apache.org/repos/asf/ignite/blob/6af6560a/modules/web-console/frontend/app/modules/user/AclRoute.provider.js
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/modules/user/AclRoute.provider.js b/modules/web-console/frontend/app/modules/user/AclRoute.provider.js
new file mode 100644
index 0000000..40abea5
--- /dev/null
+++ b/modules/web-console/frontend/app/modules/user/AclRoute.provider.js
@@ -0,0 +1,47 @@
+/*
+ * 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.
+ */
+
+export default [() => {
+    class AclRoute {
+        static checkAccess = (permissions, failState) => {
+            failState = failState || '403';
+
+            return ['$state', 'AclService', 'User', ($state, AclService, User) => {
+                User.read()
+                    .then(() => {
+                        if (AclService.can(permissions))
+                            return;
+
+                        return $state.go(failState);
+                    })
+                    .catch(() => {
+                        User.clean();
+
+                        if ($state.current.name !== 'signin')
+                            $state.go('signin');
+                    });
+            }];
+        }
+    }
+
+    return {
+        checkAccess: AclRoute.checkAccess,
+        $get: () => {
+            return AclRoute;
+        }
+    };
+}];

http://git-wip-us.apache.org/repos/asf/ignite/blob/6af6560a/modules/web-console/frontend/app/modules/user/Auth.service.js
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/modules/user/Auth.service.js b/modules/web-console/frontend/app/modules/user/Auth.service.js
new file mode 100644
index 0000000..43e2f92
--- /dev/null
+++ b/modules/web-console/frontend/app/modules/user/Auth.service.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.
+ */
+
+export default ['Auth', ['$http', '$rootScope', '$state', '$window', 'IgniteErrorPopover', 'IgniteMessages', 'gettingStarted', 'User', 'IgniteAgentMonitor',
+    ($http, $root, $state, $window, ErrorPopover, Messages, gettingStarted, User, agentMonitor) => {
+        return {
+            forgotPassword(userInfo) {
+                $http.post('/api/v1/password/forgot', userInfo)
+                    .success(() => $state.go('password.send'))
+                    .error((err) => ErrorPopover.show('forgot_email', Messages.errorMessage(null, err)));
+            },
+            auth(action, userInfo) {
+                $http.post('/api/v1/' + action, userInfo)
+                    .catch(({data}) => Promise.reject(data))
+                    .then(() => {
+                        if (action === 'password/forgot')
+                            return;
+
+                        User.read()
+                            .then((user) => {
+                                $root.$broadcast('user', user);
+
+                                $state.go('base.configuration.clusters');
+
+                                agentMonitor.init();
+
+                                $root.gettingStarted.tryShow();
+                            });
+                    })
+                    .catch((err) => ErrorPopover.show(action + '_email', Messages.errorMessage(null, err)));
+            },
+            logout() {
+                $http.post('/api/v1/logout')
+                    .success(() => {
+                        User.clean();
+
+                        $window.open($state.href('signin'), '_self');
+                    })
+                    .error(Messages.showError);
+            }
+        };
+    }]];

http://git-wip-us.apache.org/repos/asf/ignite/blob/6af6560a/modules/web-console/frontend/app/modules/user/User.service.js
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/modules/user/User.service.js b/modules/web-console/frontend/app/modules/user/User.service.js
new file mode 100644
index 0000000..8b9a1e7
--- /dev/null
+++ b/modules/web-console/frontend/app/modules/user/User.service.js
@@ -0,0 +1,51 @@
+/*
+ * 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.
+ */
+
+export default ['User', ['$q', '$injector', '$rootScope', '$state', '$http', function($q, $injector, $root, $state, $http) {
+    let user;
+
+    return {
+        load() {
+            return user = $http.post('/api/v1/user')
+                .then(({data}) => {
+                    $root.user = data;
+
+                    $root.$broadcast('user', $root.user);
+
+                    return $root.user;
+                })
+                .catch(({data}) => {
+                    user = null;
+
+                    return $q.reject(data);
+                });
+        },
+        read() {
+            if (user)
+                return user;
+
+            return this.load();
+        },
+        clean() {
+            delete $root.user;
+
+            delete $root.IgniteDemoMode;
+
+            sessionStorage.removeItem('IgniteDemoMode');
+        }
+    };
+}]];

http://git-wip-us.apache.org/repos/asf/ignite/blob/6af6560a/modules/web-console/frontend/app/modules/user/permissions.js
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/modules/user/permissions.js b/modules/web-console/frontend/app/modules/user/permissions.js
new file mode 100644
index 0000000..e13509c
--- /dev/null
+++ b/modules/web-console/frontend/app/modules/user/permissions.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.
+ */
+
+const guest = ['login'];
+const becomed = ['profile', 'configuration', 'query'];
+const user = becomed.concat(['logout']);
+const admin = user.concat(['admin_page']);
+
+export default {
+    guest,
+    user,
+    admin,
+    becomed
+};

http://git-wip-us.apache.org/repos/asf/ignite/blob/6af6560a/modules/web-console/frontend/app/modules/user/user.module.js
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/modules/user/user.module.js b/modules/web-console/frontend/app/modules/user/user.module.js
new file mode 100644
index 0000000..11798d0
--- /dev/null
+++ b/modules/web-console/frontend/app/modules/user/user.module.js
@@ -0,0 +1,73 @@
+/*
+ * 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 aclData from './permissions';
+
+import Auth from './Auth.service';
+import User from './User.service';
+import AclRouteProvider from './AclRoute.provider';
+
+angular
+.module('ignite-console.user', [
+    'mm.acl',
+    'ignite-console.config'
+])
+.factory('sessionRecoverer', ['$injector', '$q', ($injector, $q) => {
+    return {
+        responseError: (response) => {
+            // Session has expired
+            if (response.status === 401) {
+                $injector.get('User').clean();
+
+                const $state = $injector.get('$state');
+
+                if ($state.current.name !== 'signin')
+                    $state.go('signin');
+            }
+
+            return $q.reject(response);
+        }
+    };
+}])
+.config(['$httpProvider', ($httpProvider) => {
+    $httpProvider.interceptors.push('sessionRecoverer');
+}])
+.service(...Auth)
+.service(...User)
+.provider('AclRoute', AclRouteProvider)
+.run(['$rootScope', 'AclService', ($root, AclService) => {
+    AclService.setAbilities(aclData);
+    AclService.attachRole('guest');
+
+    $root.$on('user', (event, user) => {
+        if (!user)
+            return;
+
+        AclService.flushRoles();
+
+        let role = 'user';
+
+        if (user.admin)
+            role = 'admin';
+
+        if (user.becomeUsed)
+            role = 'becomed';
+
+        AclService.attachRole(role);
+    });
+}]);

http://git-wip-us.apache.org/repos/asf/ignite/blob/6af6560a/modules/web-console/frontend/app/modules/version/Version.provider.js
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/modules/version/Version.provider.js b/modules/web-console/frontend/app/modules/version/Version.provider.js
new file mode 100644
index 0000000..fe503ab
--- /dev/null
+++ b/modules/web-console/frontend/app/modules/version/Version.provider.js
@@ -0,0 +1,32 @@
+/*
+ * 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';
+
+angular
+    .module('ignite-console.version', [])
+    .provider('IgniteVersion', function() {
+        const version = {
+            version: '1.6.0'
+        };
+
+        this.update = (newVersion) => {
+            version.version = newVersion;
+        };
+
+        this.$get = [() => version];
+    });

http://git-wip-us.apache.org/repos/asf/ignite/blob/6af6560a/modules/web-console/frontend/app/services/ChartColors.service.js
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/services/ChartColors.service.js b/modules/web-console/frontend/app/services/ChartColors.service.js
new file mode 100644
index 0000000..843aa5c
--- /dev/null
+++ b/modules/web-console/frontend/app/services/ChartColors.service.js
@@ -0,0 +1,22 @@
+/*
+ * 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 COLORS from 'app/data/colors.json';
+
+export default ['IgniteChartColors', function() {
+    return COLORS;
+}];

http://git-wip-us.apache.org/repos/asf/ignite/blob/6af6560a/modules/web-console/frontend/app/services/Clone.service.js
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/services/Clone.service.js b/modules/web-console/frontend/app/services/Clone.service.js
new file mode 100644
index 0000000..52a4e4e
--- /dev/null
+++ b/modules/web-console/frontend/app/services/Clone.service.js
@@ -0,0 +1,64 @@
+/*
+ * 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.
+ */
+
+// Service for clone objects.
+export default ['IgniteClone', ['$rootScope', '$q', '$modal', ($root, $q, $modal) => {
+    const scope = $root.$new();
+
+    let _names = [];
+    let deferred;
+    let _validator;
+
+    function _nextAvailableName(name) {
+        let num = 1;
+        let tmpName = name;
+
+        while (_.includes(_names, tmpName)) {
+            tmpName = name + '_' + num.toString();
+
+            num++;
+        }
+
+        return tmpName;
+    }
+
+    const cloneModal = $modal({templateUrl: '/templates/clone.html', scope, placement: 'center', show: false});
+
+    scope.ok = function(newName) {
+        if (!_validator || _validator(newName)) {
+            deferred.resolve(_nextAvailableName(newName));
+
+            cloneModal.hide();
+        }
+    };
+
+    cloneModal.confirm = function(oldName, names, validator) {
+        _names = names;
+
+        scope.newName = _nextAvailableName(oldName);
+
+        _validator = validator;
+
+        deferred = $q.defer();
+
+        cloneModal.$promise.then(cloneModal.show);
+
+        return deferred.promise;
+    };
+
+    return cloneModal;
+}]];

http://git-wip-us.apache.org/repos/asf/ignite/blob/6af6560a/modules/web-console/frontend/app/services/Confirm.service.js
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/services/Confirm.service.js b/modules/web-console/frontend/app/services/Confirm.service.js
new file mode 100644
index 0000000..8208ea2
--- /dev/null
+++ b/modules/web-console/frontend/app/services/Confirm.service.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.
+ */
+
+// Confirm popup service.
+export default ['IgniteConfirm', ['$rootScope', '$q', '$modal', '$animate', ($root, $q, $modal, $animate) => {
+    const scope = $root.$new();
+
+    const modal = $modal({templateUrl: '/templates/confirm.html', scope, placement: 'center', show: false, backdrop: true});
+
+    const _hide = () => {
+        $animate.enabled(modal.$element, false);
+
+        modal.hide();
+    };
+
+    let deferred;
+
+    scope.confirmYes = () => {
+        _hide();
+
+        deferred.resolve(true);
+    };
+
+    scope.confirmNo = () => {
+        _hide();
+
+        deferred.resolve(false);
+    };
+
+    scope.confirmCancel = () => {
+        _hide();
+
+        deferred.reject('cancelled');
+    };
+
+    /**
+     *
+     * @param {String } content
+     * @param {Boolean} [yesNo]
+     * @returns {Promise}
+     */
+    modal.confirm = (content, yesNo) => {
+        scope.content = content || 'Confirm?';
+        scope.yesNo = !!yesNo;
+
+        deferred = $q.defer();
+
+        modal.$promise.then(modal.show);
+
+        return deferred.promise;
+    };
+
+    return modal;
+}]];

http://git-wip-us.apache.org/repos/asf/ignite/blob/6af6560a/modules/web-console/frontend/app/services/ConfirmBatch.service.js
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/services/ConfirmBatch.service.js b/modules/web-console/frontend/app/services/ConfirmBatch.service.js
new file mode 100644
index 0000000..ef66335
--- /dev/null
+++ b/modules/web-console/frontend/app/services/ConfirmBatch.service.js
@@ -0,0 +1,92 @@
+/*
+ * 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.
+ */
+
+// Service for confirm or skip several steps.
+export default ['IgniteConfirmBatch', ['$rootScope', '$q', '$modal', ($root, $q, $modal) => {
+    const scope = $root.$new();
+
+    scope.confirmModal = $modal({
+        templateUrl: '/templates/batch-confirm.html',
+        scope,
+        placement: 'center',
+        show: false,
+        backdrop: 'static',
+        keyboard: false
+    });
+
+    const _done = (cancel) => {
+        scope.confirmModal.hide();
+
+        if (cancel)
+            scope.deferred.reject('cancelled');
+        else
+            scope.deferred.resolve();
+    };
+
+    const _nextElement = (skip) => {
+        scope.items[scope.curIx++].skip = skip;
+
+        if (scope.curIx < scope.items.length)
+            scope.content = scope.contentGenerator(scope.items[scope.curIx]);
+        else
+            _done();
+    };
+
+    scope.cancel = () => {
+        _done(true);
+    };
+
+    scope.skip = (applyToAll) => {
+        if (applyToAll) {
+            for (let i = scope.curIx; i < scope.items.length; i++)
+                scope.items[i].skip = true;
+
+            _done();
+        }
+        else
+            _nextElement(true);
+    };
+
+    scope.overwrite = (applyToAll) => {
+        if (applyToAll)
+            _done();
+        else
+            _nextElement(false);
+    };
+
+    return {
+        /**
+         * Show confirm all dialog.
+         *
+         * @param confirmMessageFn Function to generate a confirm message.
+         * @param itemsToConfirm Array of element to process by confirm.
+         */
+        confirm(confirmMessageFn, itemsToConfirm) {
+            scope.deferred = $q.defer();
+
+            scope.contentGenerator = confirmMessageFn;
+
+            scope.items = itemsToConfirm;
+            scope.curIx = 0;
+            scope.content = (scope.items && scope.items.length > 0) ? scope.contentGenerator(scope.items[0]) : null;
+
+            scope.confirmModal.$promise.then(scope.confirmModal.show);
+
+            return scope.deferred.promise;
+        }
+    };
+}]];

http://git-wip-us.apache.org/repos/asf/ignite/blob/6af6560a/modules/web-console/frontend/app/services/CopyToClipboard.service.js
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/services/CopyToClipboard.service.js b/modules/web-console/frontend/app/services/CopyToClipboard.service.js
new file mode 100644
index 0000000..74c4764
--- /dev/null
+++ b/modules/web-console/frontend/app/services/CopyToClipboard.service.js
@@ -0,0 +1,50 @@
+/*
+ * 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.
+ */
+
+// Service to copy some value to OS clipboard.
+export default ['IgniteCopyToClipboard', ['$window', 'IgniteMessages', ($window, Messages) => {
+    const body = angular.element($window.document.body);
+
+    const textArea = angular.element('<textarea/>');
+
+    textArea.css({
+        position: 'fixed',
+        opacity: '0'
+    });
+
+    return {
+        copy(toCopy) {
+            textArea.val(toCopy);
+
+            body.append(textArea);
+
+            textArea[0].select();
+
+            try {
+                if (document.execCommand('copy'))
+                    Messages.showInfo('Value copied to clipboard');
+                else
+                    window.prompt('Copy to clipboard: Ctrl+C, Enter', toCopy);  // eslint-disable-line no-alert
+            }
+            catch (err) {
+                window.prompt('Copy to clipboard: Ctrl+C, Enter', toCopy);  // eslint-disable-line no-alert
+            }
+
+            textArea.remove();
+        }
+    };
+}]];

http://git-wip-us.apache.org/repos/asf/ignite/blob/6af6560a/modules/web-console/frontend/app/services/Countries.service.js
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/services/Countries.service.js b/modules/web-console/frontend/app/services/Countries.service.js
new file mode 100644
index 0000000..5ad3e6a
--- /dev/null
+++ b/modules/web-console/frontend/app/services/Countries.service.js
@@ -0,0 +1,31 @@
+/*
+ * 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 COUNTRIES from 'app/data/countries.json';
+
+export default ['IgniteCountries', function() {
+    const indexByName = _.keyBy(COUNTRIES, 'name');
+    const UNDEFINED_COUNTRY = {name: '', code: ''};
+
+    const getByName = (name) => (indexByName[name] || UNDEFINED_COUNTRY);
+    const getAll = () => (COUNTRIES);
+
+    return {
+        getByName,
+        getAll
+    };
+}];

http://git-wip-us.apache.org/repos/asf/ignite/blob/6af6560a/modules/web-console/frontend/app/services/ErrorPopover.service.js
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/services/ErrorPopover.service.js b/modules/web-console/frontend/app/services/ErrorPopover.service.js
new file mode 100644
index 0000000..85e4fda
--- /dev/null
+++ b/modules/web-console/frontend/app/services/ErrorPopover.service.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.
+ */
+
+export default class ErrorPopover {
+    static $inject = ['$popover', '$anchorScroll', '$location', '$timeout', 'IgniteFormUtils'];
+
+    /**
+     * @param $popover
+     * @param $anchorScroll
+     * @param $location
+     * @param $timeout
+     * @param FormUtils
+     */
+    constructor($popover, $anchorScroll, $location, $timeout, FormUtils) {
+        this.$popover = $popover;
+        this.$anchorScroll = $anchorScroll;
+        this.$location = $location;
+        this.$timeout = $timeout;
+        this.FormUtils = FormUtils;
+
+        this.$anchorScroll.yOffset = 55;
+
+        this._popover = null;
+    }
+
+    /**
+     * Check that element is document area.
+     *
+     * @param el Element to check.
+     * @returns {boolean} True when element in document area.
+     */
+    static _isElementInViewport(el) {
+        const rect = el.getBoundingClientRect();
+
+        return (
+            rect.top >= 0 &&
+            rect.left >= 0 &&
+            rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
+            rect.right <= (window.innerWidth || document.documentElement.clientWidth)
+        );
+    }
+
+    /**
+     * Internal show popover message with detected properties.
+     *
+     * @param id Id element to show popover message.
+     * @param message Message to show.
+     * @param showTime Time before popover will be hidden.
+     */
+    _show(id, message, showTime = 5000) {
+        const body = $('body');
+
+        let el = body.find('#' + id);
+
+        if (!el || el.length === 0)
+            el = body.find('[name="' + id + '"]');
+
+        if (el && el.length > 0) {
+            if (!ErrorPopover._isElementInViewport(el[0])) {
+                this.$location.hash(el[0].id);
+
+                this.$anchorScroll();
+            }
+
+            const newPopover = this.$popover(el, {content: message});
+
+            this._popover = newPopover;
+
+            this.$timeout(() => newPopover.$promise.then(() => {
+                newPopover.show();
+
+                // Workaround to fix popover location when content is longer than content template.
+                // https://github.com/mgcrea/angular-strap/issues/1497
+                this.$timeout(newPopover.$applyPlacement);
+            }), 400);
+            this.$timeout(() => newPopover.hide(), showTime);
+        }
+    }
+
+    /**
+     * Show popover message.
+     *
+     * @param {String} id ID of element to show popover.
+     * @param {String} message Message to show.
+     * @param {Object} [ui] Form UI object. When specified extend section with that name.
+     * @param {String} [panelId] ID of element owner panel. When specified focus element with that ID.
+     * @param {Number} [showTime] Time before popover will be hidden. 5 sec when not specified.
+     * @returns {boolean} False always.
+     */
+    show(id, message, ui, panelId, showTime) {
+        if (this._popover)
+            this._popover.hide();
+
+        if (ui) {
+            this.FormUtils.ensureActivePanel(ui, panelId, id);
+
+            this.$timeout(() => this._show(id, message, showTime), ui.isPanelLoaded(panelId) ? 200 : 500);
+        }
+        else
+            this._show(id, message);
+
+        return false;
+    }
+
+    /**
+     * Hide popover message.
+     */
+    hide() {
+        if (this._popover)
+            this._popover.hide();
+    }
+}

http://git-wip-us.apache.org/repos/asf/ignite/blob/6af6560a/modules/web-console/frontend/app/services/Focus.service.js
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/services/Focus.service.js b/modules/web-console/frontend/app/services/Focus.service.js
new file mode 100644
index 0000000..a07e181
--- /dev/null
+++ b/modules/web-console/frontend/app/services/Focus.service.js
@@ -0,0 +1,33 @@
+/*
+ * 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.
+ */
+
+// Service to transfer focus for specified element.
+export default ['IgniteFocus', ['$timeout', ($timeout) => {
+    return {
+        move(id) {
+            // Timeout makes sure that is invoked after any other event has been triggered.
+            // E.g. click events that need to run before the focus or inputs elements that are
+            // in a disabled state but are enabled when those events are triggered.
+            $timeout(() => {
+                const elem = $('#' + id);
+
+                if (elem.length > 0)
+                    elem[0].focus();
+            }, 100);
+        }
+    };
+}]];