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 2018/04/05 07:09:42 UTC

[8/8] ignite git commit: IGNITE-7894 Web Console: Refactored panel-collapsible to component.

IGNITE-7894 Web Console: Refactored panel-collapsible to component.


Project: http://git-wip-us.apache.org/repos/asf/ignite/repo
Commit: http://git-wip-us.apache.org/repos/asf/ignite/commit/d24dab81
Tree: http://git-wip-us.apache.org/repos/asf/ignite/tree/d24dab81
Diff: http://git-wip-us.apache.org/repos/asf/ignite/diff/d24dab81

Branch: refs/heads/master
Commit: d24dab813936adbe2d33a09c86ddf1f393f0c226
Parents: ca75df1
Author: Ilya Borisov <kl...@gmail.com>
Authored: Thu Apr 5 14:09:17 2018 +0700
Committer: Alexey Kuznetsov <ak...@apache.org>
Committed: Thu Apr 5 14:09:17 2018 +0700

----------------------------------------------------------------------
 .../e2e/testcafe/components/PanelCollapsible.js |  28 ++
 .../e2e/testcafe/components/confirmation.js     |  16 +-
 .../components/pageAdvancedConfiguration.js     |  14 +-
 .../fixtures/user-profile/credentials.js        |  34 +-
 .../testcafe/fixtures/user-profile/profile.js   |  60 +--
 .../e2e/testcafe/page-models/pageProfile.js     |  40 ++
 modules/web-console/frontend/.eslintrc          |   1 +
 modules/web-console/frontend/app/app.js         |   2 +
 .../components/cache-edit-form/controller.js    |   3 +-
 .../components/cache-edit-form/template.tpl.pug |   2 -
 .../components/cluster-edit-form/controller.js  |   2 -
 .../cluster-edit-form/template.tpl.pug          |  81 ++-
 .../components/igfs-edit-form/controller.js     |   3 -
 .../components/igfs-edit-form/template.tpl.pug  |  17 +-
 .../components/model-edit-form/controller.js    |   3 -
 .../components/model-edit-form/template.tpl.pug |   9 +-
 .../page-configure-advanced/style.scss          |  11 +-
 .../page-configure/components/pcValidation.js   |   5 +-
 .../app/components/page-profile/controller.js   |  25 +-
 .../app/components/page-profile/style.scss      |   6 +-
 .../app/components/page-profile/template.pug    |  32 +-
 .../components/panel-collapsible/component.js   |  39 ++
 .../components/panel-collapsible/controller.js  |  52 ++
 .../app/components/panel-collapsible/index.js   |  25 +
 .../components/panel-collapsible/index.spec.js  | 140 ++++++
 .../app/components/panel-collapsible/style.scss |  79 +++
 .../components/panel-collapsible/template.pug   |  24 +
 .../panel-collapsible/transcludeDirective.js    |  51 ++
 .../states/configuration/caches/affinity.pug    | 113 ++---
 .../states/configuration/caches/concurrency.pug |  85 ++--
 .../states/configuration/caches/general.pug     | 176 ++++---
 .../states/configuration/caches/memory.pug      | 263 +++++-----
 .../configuration/caches/near-cache-client.pug  |  55 +-
 .../configuration/caches/near-cache-server.pug  |  57 +--
 .../states/configuration/caches/node-filter.pug |  58 +--
 .../states/configuration/caches/query.pug       | 169 +++----
 .../states/configuration/caches/rebalance.pug   |  89 ++--
 .../states/configuration/caches/statistics.pug  |  30 +-
 .../states/configuration/caches/store.pug       | 499 +++++++++----------
 .../states/configuration/clusters/atomic.pug    | 103 ++--
 .../configuration/clusters/attributes.pug       |  40 +-
 .../states/configuration/clusters/binary.pug    | 117 +++--
 .../configuration/clusters/cache-key-cfg.pug    |  85 ++--
 .../configuration/clusters/checkpoint.pug       | 109 ++--
 .../configuration/clusters/client-connector.pug | 105 ++--
 .../states/configuration/clusters/collision.pug |  75 ++-
 .../configuration/clusters/communication.pug    | 227 +++++----
 .../states/configuration/clusters/connector.pug | 157 +++---
 .../configuration/clusters/data-storage.pug     | 323 ++++++------
 .../configuration/clusters/deployment.pug       | 291 ++++++-----
 .../states/configuration/clusters/discovery.pug | 151 +++---
 .../states/configuration/clusters/events.pug    |  81 ++-
 .../states/configuration/clusters/failover.pug  | 123 +++--
 .../states/configuration/clusters/general.pug   | 111 ++---
 .../states/configuration/clusters/hadoop.pug    | 114 ++---
 .../states/configuration/clusters/igfs.pug      |  29 +-
 .../configuration/clusters/load-balancing.pug   | 177 ++++---
 .../states/configuration/clusters/logger.pug    |  80 ++-
 .../configuration/clusters/marshaller.pug       | 101 ++--
 .../states/configuration/clusters/memory.pug    | 331 ++++++------
 .../states/configuration/clusters/metrics.pug   |  54 +-
 .../states/configuration/clusters/misc.pug      |  78 ++-
 .../states/configuration/clusters/odbc.pug      |  95 ++--
 .../configuration/clusters/persistence.pug      | 119 ++---
 .../states/configuration/clusters/service.pug   | 129 +++--
 .../configuration/clusters/sql-connector.pug    |  69 +--
 .../states/configuration/clusters/ssl.pug       | 128 +++--
 .../states/configuration/clusters/swap.pug      |  99 ++--
 .../states/configuration/clusters/thread.pug    | 240 +++++----
 .../states/configuration/clusters/time.pug      |  48 +-
 .../configuration/clusters/transactions.pug     |  91 ++--
 .../states/configuration/domains/general.pug    |  71 ++-
 .../states/configuration/domains/query.pug      | 431 ++++++++--------
 .../states/configuration/domains/store.pug      |  89 ++--
 .../modules/states/configuration/igfs/dual.pug  |  41 +-
 .../states/configuration/igfs/fragmentizer.pug  |  33 +-
 .../states/configuration/igfs/general.pug       | 105 ++--
 .../modules/states/configuration/igfs/ipc.pug   |  69 ++-
 .../modules/states/configuration/igfs/misc.pug  | 179 ++++---
 .../states/configuration/igfs/secondary.pug     |  69 ++-
 modules/web-console/frontend/package-lock.json  |  73 +++
 modules/web-console/frontend/package.json       |  10 +-
 .../frontend/test/karma.conf.babel.js           |   8 +
 .../frontend/webpack/webpack.test.js            |   5 +-
 84 files changed, 4016 insertions(+), 3645 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/ignite/blob/d24dab81/modules/web-console/e2e/testcafe/components/PanelCollapsible.js
----------------------------------------------------------------------
diff --git a/modules/web-console/e2e/testcafe/components/PanelCollapsible.js b/modules/web-console/e2e/testcafe/components/PanelCollapsible.js
new file mode 100644
index 0000000..d58d48b
--- /dev/null
+++ b/modules/web-console/e2e/testcafe/components/PanelCollapsible.js
@@ -0,0 +1,28 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {Selector} from 'testcafe';
+
+export class PanelCollapsible {
+    constructor(title) {
+        this._selector = Selector('.panel-collapsible__title').withText(title).parent('panel-collapsible');
+        this.heading = this._selector.find('.panel-collapsible__heading');
+        this.body = this._selector.find('.panel-collapsible__content').addCustomDOMProperties({
+            isOpened: (el) => !el.classList.contains('ng-hide')
+        });
+    }
+}

http://git-wip-us.apache.org/repos/asf/ignite/blob/d24dab81/modules/web-console/e2e/testcafe/components/confirmation.js
----------------------------------------------------------------------
diff --git a/modules/web-console/e2e/testcafe/components/confirmation.js b/modules/web-console/e2e/testcafe/components/confirmation.js
index 7b39b23..b4fa2b5 100644
--- a/modules/web-console/e2e/testcafe/components/confirmation.js
+++ b/modules/web-console/e2e/testcafe/components/confirmation.js
@@ -17,15 +17,23 @@
 
 import {Selector, t} from 'testcafe';
 
+const body = Selector('.modal-body');
+const confirmButton = Selector('#confirm-btn-ok');
+const cancelButton = Selector('#confirm-btn-cancel');
+const closeButton = Selector('.modal .close');
+
 export const confirmation = {
-    body: Selector('.modal-body'),
+    body,
+    confirmButton,
+    cancelButton,
+    closeButton,
     async confirm() {
-        await t.click('#confirm-btn-ok');
+        await t.click(confirmButton);
     },
     async cancel() {
-        await t.click('#confirm-btn-cancel');
+        await t.click(cancelButton);
     },
     async close() {
-        await t.click('.modal .close');
+        await t.click(closeButton);
     }
 };

http://git-wip-us.apache.org/repos/asf/ignite/blob/d24dab81/modules/web-console/e2e/testcafe/components/pageAdvancedConfiguration.js
----------------------------------------------------------------------
diff --git a/modules/web-console/e2e/testcafe/components/pageAdvancedConfiguration.js b/modules/web-console/e2e/testcafe/components/pageAdvancedConfiguration.js
index eabd337..627a345 100644
--- a/modules/web-console/e2e/testcafe/components/pageAdvancedConfiguration.js
+++ b/modules/web-console/e2e/testcafe/components/pageAdvancedConfiguration.js
@@ -15,7 +15,7 @@
  * limitations under the License.
  */
 
-import {Selector, t} from 'testcafe'
+import {Selector, t} from 'testcafe';
 
 export const pageAdvancedConfiguration = {
     saveButton: Selector('.pc-form-actions-panel .btn-ignite').withText('Save'),
@@ -24,16 +24,6 @@ export const pageAdvancedConfiguration = {
     cachesNavButton: Selector('.pca-menu-link[ui-sref="base.configuration.edit.advanced.caches"]'),
     igfsNavButton: Selector('.pca-menu-link[ui-sref="base.configuration.edit.advanced.igfs"]'),
     async save() {
-        await t.click(this.saveButton)
+        await t.click(this.saveButton);
     }
 };
-
-export class Panel {
-    constructor(title) {
-        this._selector = Selector('.pca-panel-heading-title').withText(title).parent('.pca-panel');
-        this.heading = this._selector.find('.pca-panel-heading')
-        this.body = this._selector.find('.pca-panel-collapse').addCustomDOMProperties({
-            isOpened: el => el.classList.contains('in')
-        })
-    }
-}

http://git-wip-us.apache.org/repos/asf/ignite/blob/d24dab81/modules/web-console/e2e/testcafe/fixtures/user-profile/credentials.js
----------------------------------------------------------------------
diff --git a/modules/web-console/e2e/testcafe/fixtures/user-profile/credentials.js b/modules/web-console/e2e/testcafe/fixtures/user-profile/credentials.js
index 33d2232..42c76cf 100644
--- a/modules/web-console/e2e/testcafe/fixtures/user-profile/credentials.js
+++ b/modules/web-console/e2e/testcafe/fixtures/user-profile/credentials.js
@@ -15,9 +15,11 @@
  * limitations under the License.
  */
 
-import { Selector } from 'testcafe';
 import { dropTestDB, insertTestUser, resolveUrl } from '../../envtools';
 import { createRegularUser } from '../../roles';
+import {pageProfile} from '../../page-models/pageProfile';
+import {confirmation} from '../../components/confirmation';
+import {successNotification} from '../../components/notifications';
 
 const regularUser = createRegularUser();
 
@@ -35,30 +37,24 @@ fixture('Checking user credentials change')
     });
 
 test('Testing secure token change', async(t) => {
-    await t.click(Selector('header').withAttribute('ng-click', '$ctrl.toggleToken()'));
+    await t.click(pageProfile.securityToken.panel.heading);
 
-    const currentToken = await Selector('#current-security-token').innerText;
+    const currentToken = await pageProfile.securityToken.value.innerText;
 
     await t
-        .click(Selector('i').withAttribute('ng-click', '$ctrl.generateToken()'))
-        .expect(Selector('p').withText('Are you sure you want to change security token?').exists)
-        .ok()
-        .click('#confirm-btn-ok');
-
-    await t
-        .expect(await Selector('#current-security-token').innerText)
-        .notEql(currentToken);
+        .click(pageProfile.securityToken.generateTokenButton)
+        .expect(confirmation.body.innerText).contains('Are you sure you want to change security token?')
+        .click(confirmation.confirmButton)
+        .expect(pageProfile.securityToken.value.innerText).notEql(currentToken);
 });
 
 test('Testing password change', async(t) => {
-    await t.click(Selector('header').withAttribute('ng-click', '$ctrl.togglePassword()'));
-
-    await t
-        .typeText('#passwordInput', 'newPass')
-        .typeText('#passwordConfirmInput', 'newPass')
-        .click(Selector('button').withText('Save Changes'));
+    const pass = 'newPass';
 
     await t
-        .expect(Selector('span').withText('Profile saved.').exists)
-        .ok();
+        .click(pageProfile.password.panel.heading)
+        .typeText(pageProfile.password.newPassword.control, pass)
+        .typeText(pageProfile.password.confirmPassword.control, pass)
+        .click(pageProfile.saveChangesButton)
+        .expect(successNotification.withText('Profile saved.').exists).ok();
 });

http://git-wip-us.apache.org/repos/asf/ignite/blob/d24dab81/modules/web-console/e2e/testcafe/fixtures/user-profile/profile.js
----------------------------------------------------------------------
diff --git a/modules/web-console/e2e/testcafe/fixtures/user-profile/profile.js b/modules/web-console/e2e/testcafe/fixtures/user-profile/profile.js
index 398b0ad..1e9f0c3 100644
--- a/modules/web-console/e2e/testcafe/fixtures/user-profile/profile.js
+++ b/modules/web-console/e2e/testcafe/fixtures/user-profile/profile.js
@@ -18,6 +18,7 @@
 import { Selector } from 'testcafe';
 import { dropTestDB, insertTestUser, resolveUrl } from '../../envtools';
 import { createRegularUser } from '../../roles';
+import {pageProfile} from '../../page-models/pageProfile';
 
 const regularUser = createRegularUser();
 
@@ -35,50 +36,25 @@ fixture('Checking user profile')
     });
 
 test('Testing user data change', async(t) => {
-    const newUserData = {
-        firstName: {
-            selector: '#firstNameInput',
-            value: 'Richard'
-        },
-        lastName: {
-            selector: '#lastNameInput',
-            value: 'Roe'
-        },
-        email: {
-            selector: '#emailInput',
-            value: 'r.roe@mail.com'
-        },
-        company: {
-            selector: '#companyInput',
-            value: 'New Company'
-        },
-        country: {
-            selector: '#countryInput',
-            value: 'Israel'
-        }
-    };
-
-    ['firstName', 'lastName', 'email', 'company'].forEach(async(item) => {
-        await t
-            .click(newUserData[item].selector)
-            .pressKey('ctrl+a delete')
-            .typeText(newUserData[item].selector, newUserData[item].value);
-    });
+    const firstName = 'Richard';
+    const lastName = 'Roe';
+    const email = 'r.roe@mail.com';
+    const company = 'New Company';
+    const country = 'Israel';
 
     await t
-        .click(newUserData.country.selector)
-        .click(Selector('span').withText(newUserData.country.value))
-        .click(Selector('button').withText('Save Changes'));
-
-    await t.navigateTo(resolveUrl('/settings/profile'));
-
-    ['firstName', 'lastName', 'email', 'company'].forEach(async(item) => {
-        await t
-            .expect(await Selector(newUserData[item].selector).getAttribute('value'))
-            .eql(newUserData[item].value);
-    });
+        .typeText(pageProfile.firstName.control, firstName, {replace: true})
+        .typeText(pageProfile.lastName.control, lastName, {replace: true})
+        .typeText(pageProfile.email.control, email, {replace: true})
+        .typeText(pageProfile.company.control, company, {replace: true});
+    await pageProfile.country.selectOption(country);
+    await t.click(pageProfile.saveChangesButton);
 
     await t
-        .expect(Selector(newUserData.country.selector).innerText)
-        .eql(newUserData.country.value);
+        .navigateTo(resolveUrl('/settings/profile'))
+        .expect(pageProfile.firstName.control.value).eql(firstName)
+        .expect(pageProfile.lastName.control.value).eql(lastName)
+        .expect(pageProfile.email.control.value).eql(email)
+        .expect(pageProfile.company.control.value).eql(company)
+        .expect(pageProfile.country.control.innerText).eql(country);
 });

http://git-wip-us.apache.org/repos/asf/ignite/blob/d24dab81/modules/web-console/e2e/testcafe/page-models/pageProfile.js
----------------------------------------------------------------------
diff --git a/modules/web-console/e2e/testcafe/page-models/pageProfile.js b/modules/web-console/e2e/testcafe/page-models/pageProfile.js
new file mode 100644
index 0000000..a2eb1db
--- /dev/null
+++ b/modules/web-console/e2e/testcafe/page-models/pageProfile.js
@@ -0,0 +1,40 @@
+/*
+ * 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 {CustomFormField} from '../components/FormField';
+import {PanelCollapsible} from '../components/PanelCollapsible';
+import {Selector} from 'testcafe';
+
+export const pageProfile = {
+    firstName: new CustomFormField({id: 'firstNameInput'}),
+    lastName: new CustomFormField({id: 'lastNameInput'}),
+    email: new CustomFormField({id: 'emailInput'}),
+    phone: new CustomFormField({id: 'phoneInput'}),
+    country: new CustomFormField({id: 'countryInput'}),
+    company: new CustomFormField({id: 'companyInput'}),
+    securityToken: {
+        panel: new PanelCollapsible('security token'),
+        generateTokenButton: Selector('i').withAttribute('ng-click', '$ctrl.generateToken()'),
+        value: Selector('#current-security-token')
+    },
+    password: {
+        panel: new PanelCollapsible('password'),
+        newPassword: new CustomFormField({id: 'passwordInput'}),
+        confirmPassword: new CustomFormField({id: 'passwordConfirmInput'})
+    },
+    saveChangesButton: Selector('.btn-ignite.btn-ignite--success').withText('Save Changes')
+};

http://git-wip-us.apache.org/repos/asf/ignite/blob/d24dab81/modules/web-console/frontend/.eslintrc
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/.eslintrc b/modules/web-console/frontend/.eslintrc
index 75de1ea..805b339 100644
--- a/modules/web-console/frontend/.eslintrc
+++ b/modules/web-console/frontend/.eslintrc
@@ -6,6 +6,7 @@ plugins:
 env:
     es6: true
     browser: true
+    mocha: true
 parserOptions:
     sourceType: module
     ecmaFeatures:

http://git-wip-us.apache.org/repos/asf/ignite/blob/d24dab81/modules/web-console/frontend/app/app.js
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/app.js b/modules/web-console/frontend/app/app.js
index 871b06f..757be22 100644
--- a/modules/web-console/frontend/app/app.js
+++ b/modules/web-console/frontend/app/app.js
@@ -134,6 +134,7 @@ import uiGridHovering from './components/ui-grid-hovering';
 import uiGridFilters from './components/ui-grid-filters';
 import listEditable from './components/list-editable';
 import breadcrumbs from './components/breadcrumbs';
+import panelCollapsible from './components/panel-collapsible';
 import clusterSelector from './components/cluster-selector';
 import connectedClusters from './components/connected-clusters';
 import pageSignIn from './components/page-signin';
@@ -226,6 +227,7 @@ angular.module('ignite-console', [
     AngularStrapTooltip.name,
     AngularStrapSelect.name,
     listEditable.name,
+    panelCollapsible.name,
     clusterSelector.name,
     servicesModule.name,
     connectedClusters.name,

http://git-wip-us.apache.org/repos/asf/ignite/blob/d24dab81/modules/web-console/frontend/app/components/page-configure-advanced/components/cache-edit-form/controller.js
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/components/page-configure-advanced/components/cache-edit-form/controller.js b/modules/web-console/frontend/app/components/page-configure-advanced/components/cache-edit-form/controller.js
index 14439c1..f1bba1f 100644
--- a/modules/web-console/frontend/app/components/page-configure-advanced/components/cache-edit-form/controller.js
+++ b/modules/web-console/frontend/app/components/page-configure-advanced/components/cache-edit-form/controller.js
@@ -64,9 +64,8 @@ export default class CacheEditFormController {
             .do(filterModel)
             .subscribe();
 
+        // TODO: Do we really need this?
         this.$scope.ui = this.IgniteFormUtils.formUI();
-        this.$scope.ui.activePanels = [0];
-        this.$scope.ui.topPanels = [0, 1, 2, 3];
     }
     $onDestroy() {
         this.subscription.unsubscribe();

http://git-wip-us.apache.org/repos/asf/ignite/blob/d24dab81/modules/web-console/frontend/app/components/page-configure-advanced/components/cache-edit-form/template.tpl.pug
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/components/page-configure-advanced/components/cache-edit-form/template.tpl.pug b/modules/web-console/frontend/app/components/page-configure-advanced/components/cache-edit-form/template.tpl.pug
index 20cde2e..70cb445 100644
--- a/modules/web-console/frontend/app/components/page-configure-advanced/components/cache-edit-form/template.tpl.pug
+++ b/modules/web-console/frontend/app/components/page-configure-advanced/components/cache-edit-form/template.tpl.pug
@@ -18,8 +18,6 @@ form(
     name='ui.inputForm'
     id='cache'
     novalidate
-    bs-collapse=''
-    data-allow-multiple='true'
     ng-submit='$ctrl.save($ctrl.clonedCache)'
 )
     include /app/modules/states/configuration/caches/general

http://git-wip-us.apache.org/repos/asf/ignite/blob/d24dab81/modules/web-console/frontend/app/components/page-configure-advanced/components/cluster-edit-form/controller.js
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/components/page-configure-advanced/components/cluster-edit-form/controller.js b/modules/web-console/frontend/app/components/page-configure-advanced/components/cluster-edit-form/controller.js
index dc76bd5..35b43e0 100644
--- a/modules/web-console/frontend/app/components/page-configure-advanced/components/cluster-edit-form/controller.js
+++ b/modules/web-console/frontend/app/components/page-configure-advanced/components/cluster-edit-form/controller.js
@@ -86,8 +86,6 @@ export default class ClusterEditFormController {
 
         this.$scope.ui = this.IgniteFormUtils.formUI();
         this.$scope.ui.loadedPanels = ['checkpoint', 'serviceConfiguration', 'odbcConfiguration'];
-        this.$scope.ui.activePanels = [0];
-        this.$scope.ui.topPanels = [0];
     }
     $onChanges(changes) {
         if (

http://git-wip-us.apache.org/repos/asf/ignite/blob/d24dab81/modules/web-console/frontend/app/components/page-configure-advanced/components/cluster-edit-form/template.tpl.pug
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/components/page-configure-advanced/components/cluster-edit-form/template.tpl.pug b/modules/web-console/frontend/app/components/page-configure-advanced/components/cluster-edit-form/template.tpl.pug
index 5bd52ac..4dd0e17 100644
--- a/modules/web-console/frontend/app/components/page-configure-advanced/components/cluster-edit-form/template.tpl.pug
+++ b/modules/web-console/frontend/app/components/page-configure-advanced/components/cluster-edit-form/template.tpl.pug
@@ -16,60 +16,59 @@
 
 include /app/helpers/jade/mixins
 
-div(bs-collapse='' data-allow-multiple='true' ng-model='ui.activePanels')
-    form(id='cluster' name='ui.inputForm' novalidate ng-submit='$ctrl.save()')
-        .panel-group
-            include /app/modules/states/configuration/clusters/general
+form(id='cluster' name='ui.inputForm' novalidate ng-submit='$ctrl.save()')
+    .panel-group
+        include /app/modules/states/configuration/clusters/general
 
-            include /app/modules/states/configuration/clusters/atomic
-            include /app/modules/states/configuration/clusters/binary
-            include /app/modules/states/configuration/clusters/cache-key-cfg
-            include /app/modules/states/configuration/clusters/checkpoint
+        include /app/modules/states/configuration/clusters/atomic
+        include /app/modules/states/configuration/clusters/binary
+        include /app/modules/states/configuration/clusters/cache-key-cfg
+        include /app/modules/states/configuration/clusters/checkpoint
 
-            //- Since ignite 2.3
-            include /app/modules/states/configuration/clusters/client-connector
+        //- Since ignite 2.3
+        include /app/modules/states/configuration/clusters/client-connector
 
-            include /app/modules/states/configuration/clusters/collision
-            include /app/modules/states/configuration/clusters/communication
-            include /app/modules/states/configuration/clusters/connector
-            include /app/modules/states/configuration/clusters/deployment
+        include /app/modules/states/configuration/clusters/collision
+        include /app/modules/states/configuration/clusters/communication
+        include /app/modules/states/configuration/clusters/connector
+        include /app/modules/states/configuration/clusters/deployment
 
-            //- Since ignite 2.3
-            include /app/modules/states/configuration/clusters/data-storage
+        //- Since ignite 2.3
+        include /app/modules/states/configuration/clusters/data-storage
 
-            include /app/modules/states/configuration/clusters/discovery
-            include /app/modules/states/configuration/clusters/events
-            include /app/modules/states/configuration/clusters/failover
-            include /app/modules/states/configuration/clusters/hadoop
-            include /app/modules/states/configuration/clusters/load-balancing
-            include /app/modules/states/configuration/clusters/logger
-            include /app/modules/states/configuration/clusters/marshaller
+        include /app/modules/states/configuration/clusters/discovery
+        include /app/modules/states/configuration/clusters/events
+        include /app/modules/states/configuration/clusters/failover
+        include /app/modules/states/configuration/clusters/hadoop
+        include /app/modules/states/configuration/clusters/load-balancing
+        include /app/modules/states/configuration/clusters/logger
+        include /app/modules/states/configuration/clusters/marshaller
 
-            //- Since ignite 2.0, deprecated in ignite 2.3
-            include /app/modules/states/configuration/clusters/memory
+        //- Since ignite 2.0, deprecated in ignite 2.3
+        include /app/modules/states/configuration/clusters/memory
 
-            include /app/modules/states/configuration/clusters/misc
-            include /app/modules/states/configuration/clusters/metrics
+        include /app/modules/states/configuration/clusters/misc
+        include /app/modules/states/configuration/clusters/metrics
 
-            //- Deprecated in ignite 2.1
-            include /app/modules/states/configuration/clusters/odbc
+        //- Deprecated in ignite 2.1
+        include /app/modules/states/configuration/clusters/odbc
 
-            //- Since ignite 2.1, deprecated in ignite 2.3
-            include /app/modules/states/configuration/clusters/persistence
+        //- Since ignite 2.1, deprecated in ignite 2.3
+        include /app/modules/states/configuration/clusters/persistence
 
-            //- Deprecated in ignite 2.3
-            include /app/modules/states/configuration/clusters/sql-connector
+        //- Deprecated in ignite 2.3
+        include /app/modules/states/configuration/clusters/sql-connector
 
-            include /app/modules/states/configuration/clusters/service
-            include /app/modules/states/configuration/clusters/ssl
+        include /app/modules/states/configuration/clusters/service
+        include /app/modules/states/configuration/clusters/ssl
 
-            //- Removed in ignite 2.0
-            include /app/modules/states/configuration/clusters/swap
+        //- Removed in ignite 2.0
+        include /app/modules/states/configuration/clusters/swap
 
-            include /app/modules/states/configuration/clusters/thread
-            include /app/modules/states/configuration/clusters/time
-            include /app/modules/states/configuration/clusters/transactions
-            include /app/modules/states/configuration/clusters/attributes
+        include /app/modules/states/configuration/clusters/thread
+        include /app/modules/states/configuration/clusters/time
+        include /app/modules/states/configuration/clusters/transactions
+        include /app/modules/states/configuration/clusters/attributes
 
 .pc-form-actions-panel(n_g-show='$ctrl.$scope.selectedItem')
     button-preview-project(cluster='$ctrl.cluster' ng-hide='$ctrl.isNew')

http://git-wip-us.apache.org/repos/asf/ignite/blob/d24dab81/modules/web-console/frontend/app/components/page-configure-advanced/components/igfs-edit-form/controller.js
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/components/page-configure-advanced/components/igfs-edit-form/controller.js b/modules/web-console/frontend/app/components/page-configure-advanced/components/igfs-edit-form/controller.js
index aa150d3..6812002 100644
--- a/modules/web-console/frontend/app/components/page-configure-advanced/components/igfs-edit-form/controller.js
+++ b/modules/web-console/frontend/app/components/page-configure-advanced/components/igfs-edit-form/controller.js
@@ -27,9 +27,6 @@ export default class IgfsEditFormController {
         this.available = this.IgniteVersion.available.bind(this.IgniteVersion);
 
         this.$scope.ui = this.IgniteFormUtils.formUI();
-        this.$scope.ui.activePanels = [0];
-        this.$scope.ui.topPanels = [0];
-        this.$scope.ui.expanded = true;
         this.$scope.ui.loadedPanels = ['general', 'secondaryFileSystem', 'misc'];
     }
 

http://git-wip-us.apache.org/repos/asf/ignite/blob/d24dab81/modules/web-console/frontend/app/components/page-configure-advanced/components/igfs-edit-form/template.tpl.pug
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/components/page-configure-advanced/components/igfs-edit-form/template.tpl.pug b/modules/web-console/frontend/app/components/page-configure-advanced/components/igfs-edit-form/template.tpl.pug
index f14a59c..fe8b218 100644
--- a/modules/web-console/frontend/app/components/page-configure-advanced/components/igfs-edit-form/template.tpl.pug
+++ b/modules/web-console/frontend/app/components/page-configure-advanced/components/igfs-edit-form/template.tpl.pug
@@ -14,17 +14,16 @@
     See the License for the specific language governing permissions and
     limitations under the License.
 
-div(bs-collapse='' data-allow-multiple='true' ng-model='ui.activePanels')
-    form(id='igfs' name='ui.inputForm' novalidate ng-submit='$ctrl.save()')
-        include /app/modules/states/configuration/igfs/general
+form(id='igfs' name='ui.inputForm' novalidate ng-submit='$ctrl.save()')
+    include /app/modules/states/configuration/igfs/general
 
-        include /app/modules/states/configuration/igfs/secondary
-        include /app/modules/states/configuration/igfs/ipc
-        include /app/modules/states/configuration/igfs/fragmentizer
+    include /app/modules/states/configuration/igfs/secondary
+    include /app/modules/states/configuration/igfs/ipc
+    include /app/modules/states/configuration/igfs/fragmentizer
 
-        //- Removed in ignite 2.0
-        include /app/modules/states/configuration/igfs/dual
-        include /app/modules/states/configuration/igfs/misc
+    //- Removed in ignite 2.0
+    include /app/modules/states/configuration/igfs/dual
+    include /app/modules/states/configuration/igfs/misc
 
 .pc-form-actions-panel
     .pc-form-actions-panel__right-after

http://git-wip-us.apache.org/repos/asf/ignite/blob/d24dab81/modules/web-console/frontend/app/components/page-configure-advanced/components/model-edit-form/controller.js
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/components/page-configure-advanced/components/model-edit-form/controller.js b/modules/web-console/frontend/app/components/page-configure-advanced/components/model-edit-form/controller.js
index 1b16a02..20384d5 100644
--- a/modules/web-console/frontend/app/components/page-configure-advanced/components/model-edit-form/controller.js
+++ b/modules/web-console/frontend/app/components/page-configure-advanced/components/model-edit-form/controller.js
@@ -50,9 +50,6 @@ export default class ModelEditFormController {
 
         this.queryFieldTypes = this.LegacyUtils.javaBuiltInClasses.concat('byte[]');
         this.$scope.ui = this.IgniteFormUtils.formUI();
-        this.$scope.ui.activePanels = [0, 1];
-        this.$scope.ui.topPanels = [0, 1, 2];
-        this.$scope.ui.expanded = true;
 
         this.$scope.javaBuiltInClasses = this.LegacyUtils.javaBuiltInClasses;
         this.$scope.supportedJdbcTypes = this.LegacyUtils.mkOptions(this.LegacyUtils.SUPPORTED_JDBC_TYPES);

http://git-wip-us.apache.org/repos/asf/ignite/blob/d24dab81/modules/web-console/frontend/app/components/page-configure-advanced/components/model-edit-form/template.tpl.pug
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/components/page-configure-advanced/components/model-edit-form/template.tpl.pug b/modules/web-console/frontend/app/components/page-configure-advanced/components/model-edit-form/template.tpl.pug
index 685213c..8ebd11c 100644
--- a/modules/web-console/frontend/app/components/page-configure-advanced/components/model-edit-form/template.tpl.pug
+++ b/modules/web-console/frontend/app/components/page-configure-advanced/components/model-edit-form/template.tpl.pug
@@ -14,11 +14,10 @@
     See the License for the specific language governing permissions and
     limitations under the License.
 
-div(bs-collapse='' data-allow-multiple='true' ng-model='ui.activePanels')
-    form(id='model' name='ui.inputForm' novalidate ng-submit='$ctrl.save()')
-        include /app/modules/states/configuration/domains/general
-        include /app/modules/states/configuration/domains/query
-        include /app/modules/states/configuration/domains/store
+form(id='model' name='ui.inputForm' novalidate ng-submit='$ctrl.save()')
+    include /app/modules/states/configuration/domains/general
+    include /app/modules/states/configuration/domains/query
+    include /app/modules/states/configuration/domains/store
 
 .pc-form-actions-panel
     .pc-form-actions-panel__right-after

http://git-wip-us.apache.org/repos/asf/ignite/blob/d24dab81/modules/web-console/frontend/app/components/page-configure-advanced/style.scss
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/components/page-configure-advanced/style.scss b/modules/web-console/frontend/app/components/page-configure-advanced/style.scss
index 7af830c..2bc5e84 100644
--- a/modules/web-console/frontend/app/components/page-configure-advanced/style.scss
+++ b/modules/web-console/frontend/app/components/page-configure-advanced/style.scss
@@ -71,15 +71,12 @@ page-configure-advanced {
         }
     }
 
-    .pca-content {
-        border-left: 1px solid $gray-lighter;
-        padding: 30px;
-        flex: 1;
+    .pca-panel, panel-collapsible {
+        margin-bottom: 20px;
     }
 
-
-    .pca-panel {
-        margin-bottom: 20px;
+    panel-content.pca-form-row {
+        display: flex !important;
     }
 
     .pca-form-row {

http://git-wip-us.apache.org/repos/asf/ignite/blob/d24dab81/modules/web-console/frontend/app/components/page-configure/components/pcValidation.js
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/components/page-configure/components/pcValidation.js b/modules/web-console/frontend/app/components/page-configure/components/pcValidation.js
index 45ca6f2..5ddd4ef 100644
--- a/modules/web-console/frontend/app/components/page-configure/components/pcValidation.js
+++ b/modules/web-console/frontend/app/components/page-configure/components/pcValidation.js
@@ -148,12 +148,13 @@ export default angular.module('ignite-console.page-configure.validation', [])
     })
     .directive('ngModel', ['$timeout', function($timeout) {
         return {
-            require: ['ngModel', '?^^bsCollapseTarget', '?^^igniteFormField'],
-            link(scope, el, attr, [ngModel, bsCollapseTarget, igniteFormField]) {
+            require: ['ngModel', '?^^bsCollapseTarget', '?^^igniteFormField', '?^^panelCollapsible'],
+            link(scope, el, attr, [ngModel, bsCollapseTarget, igniteFormField, panelCollapsible]) {
                 const off = scope.$on('$showValidationError', (e, target) => {
                     if (target !== ngModel) return;
                     ngModel.$setTouched();
                     bsCollapseTarget && bsCollapseTarget.open();
+                    panelCollapsible && panelCollapsible.open();
                     $timeout(() => {
                         if (el[0].scrollIntoViewIfNeeded)
                             el[0].scrollIntoViewIfNeeded();

http://git-wip-us.apache.org/repos/asf/ignite/blob/d24dab81/modules/web-console/frontend/app/components/page-profile/controller.js
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/components/page-profile/controller.js b/modules/web-console/frontend/app/components/page-profile/controller.js
index 3fc7318..05fe118 100644
--- a/modules/web-console/frontend/app/components/page-profile/controller.js
+++ b/modules/web-console/frontend/app/components/page-profile/controller.js
@@ -33,11 +33,8 @@ export default class PageProfileController {
         this.ui.countries = this.Countries.getAll();
     }
 
-    toggleToken() {
-        this.ui.expandedToken = !this.ui.expandedToken;
-
-        if (!this.ui.expandedToken)
-            this.ui.user.token = this.$root.user.token;
+    onSecurityTokenPanelClose() {
+        this.ui.user.token = this.$root.user.token;
     }
 
     generateToken() {
@@ -45,26 +42,16 @@ export default class PageProfileController {
             .then(() => this.ui.user.token = this.LegacyUtils.randomString(20));
     }
 
-    togglePassword() {
-        this.ui.expandedPassword = !this.ui.expandedPassword;
-
-        if (this.ui.expandedPassword)
-            this.Focus.move('profile_password');
-        else {
-            delete this.ui.user.password;
-            delete this.ui.user.confirm;
-        }
+    onPasswordPanelClose() {
+        delete this.ui.user.password;
+        delete this.ui.user.confirm;
     }
 
     saveUser() {
         return this.$http.post('/api/v1/profile/save', this.ui.user)
             .then(this.User.load)
             .then(() => {
-                if (this.ui.expandedPassword)
-                    this.togglePassword();
-
-                if (this.ui.expandedToken)
-                    this.toggleToken();
+                this.ui.expandedPassword = this.ui.expandedToken = false;
 
                 this.Messages.showInfo('Profile saved.');
 

http://git-wip-us.apache.org/repos/asf/ignite/blob/d24dab81/modules/web-console/frontend/app/components/page-profile/style.scss
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/components/page-profile/style.scss b/modules/web-console/frontend/app/components/page-profile/style.scss
index f4eae1a..8c9387f 100644
--- a/modules/web-console/frontend/app/components/page-profile/style.scss
+++ b/modules/web-console/frontend/app/components/page-profile/style.scss
@@ -16,7 +16,7 @@
  */
 
 page-profile {
-    .panel--ignite {
+    panel-collapsible {
         width: 100%;
     }
 
@@ -28,8 +28,4 @@ page-profile {
     .btn-ignite + .btn-ignite {
         margin-left: 10px;
     }
-
-    [ignite-icon='expand'], [ignite-icon='collapse'] {
-        color: #757575;
-    }
 }

http://git-wip-us.apache.org/repos/asf/ignite/blob/d24dab81/modules/web-console/frontend/app/components/page-profile/template.pug
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/components/page-profile/template.pug b/modules/web-console/frontend/app/components/page-profile/template.pug
index 48aa151..eafde6b 100644
--- a/modules/web-console/frontend/app/components/page-profile/template.pug
+++ b/modules/web-console/frontend/app/components/page-profile/template.pug
@@ -90,14 +90,14 @@ div
 
         .row#security-token-section
             .col-50
-                .panel--ignite.panel--collapse(ng-class='{ in: !$ctrl.ui.expandedToken }')
-                    header(ng-click='$ctrl.toggleToken()')
-                        svg(ignite-icon='expand')
-                        svg(ignite-icon='collapse')
-
-                        | {{$ctrl.ui.expandedToken ? 'Cancel security token changing...' : 'Show security token...'}}
-                    hr
-                    section(ng-if='$ctrl.ui.expandedToken')
+                panel-collapsible(
+                    opened='$ctrl.ui.expandedToken'
+                    on-open='$ctrl.ui.expandedToken = true'
+                    on-close='$ctrl.onSecurityTokenPanelClose()'
+                )
+                    panel-title
+                        | {{ $panel.opened ? 'Cancel security token changing...' : 'Show security token...' }}
+                    panel-content
                         .row
                             .col-25
                                 label.required Security token:
@@ -110,14 +110,14 @@ div
 
         .row
             .col-50
-                .panel--ignite.panel--collapse(ng-class='{ in: !$ctrl.ui.expandedPassword }')
-                    header(ng-click='$ctrl.togglePassword()')
-                        svg(ignite-icon='expand')
-                        svg(ignite-icon='collapse')
-
-                        | {{ $ctrl.ui.expandedPassword ? 'Cancel password changing...' : 'Change password...' }}
-                    hr
-                    section(ng-if='$ctrl.ui.expandedPassword')
+                panel-collapsible(
+                    opened='$ctrl.ui.expandedPassword'
+                    on-open='$ctrl.ui.expandedToken = false'
+                    on-close='$ctrl.onPasswordPanelClose()'
+                )
+                    panel-title
+                        | {{ $panel.opened ? 'Cancel password changing...' : 'Change password...' }}
+                    panel-content(ng-if='$panel.opened')
                         .row
                             .col-100
                                 +form-field__password({

http://git-wip-us.apache.org/repos/asf/ignite/blob/d24dab81/modules/web-console/frontend/app/components/panel-collapsible/component.js
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/components/panel-collapsible/component.js b/modules/web-console/frontend/app/components/panel-collapsible/component.js
new file mode 100644
index 0000000..f0cdc65
--- /dev/null
+++ b/modules/web-console/frontend/app/components/panel-collapsible/component.js
@@ -0,0 +1,39 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import template from './template.pug';
+import controller from './controller';
+import './style.scss';
+
+export default {
+    template,
+    controller,
+    bindings: {
+        opened: '<?',
+        onOpen: '&?',
+        onClose: '&?',
+        title: '@?',
+        description: '@?',
+        disabled: '@?'
+    },
+    transclude: {
+        title: '?panelTitle',
+        description: '?panelDescription',
+        actions: '?panelActions',
+        content: 'panelContent'
+    }
+};

http://git-wip-us.apache.org/repos/asf/ignite/blob/d24dab81/modules/web-console/frontend/app/components/panel-collapsible/controller.js
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/components/panel-collapsible/controller.js b/modules/web-console/frontend/app/components/panel-collapsible/controller.js
new file mode 100644
index 0000000..0b8b4da
--- /dev/null
+++ b/modules/web-console/frontend/app/components/panel-collapsible/controller.js
@@ -0,0 +1,52 @@
+/*
+ * 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 PanelCollapsible {
+    /** @type {Boolean} */
+    opened;
+    /** @type {ng.ICompiledExpression} */
+    onOpen;
+    /** @type {ng.ICompiledExpression} */
+    onClose;
+    /** @type {String} */
+    disabled;
+
+    static $inject = ['$transclude'];
+
+    /**
+     * @param {ng.ITranscludeFunction} $transclude
+     */
+    constructor($transclude) {
+        this.$transclude = $transclude;
+    }
+    toggle() {
+        if (this.opened)
+            this.close();
+        else
+            this.open();
+    }
+    open() {
+        if (this.disabled) return;
+        this.opened = true;
+        if (this.onOpen && this.opened) this.onOpen({});
+    }
+    close() {
+        if (this.disabled) return;
+        this.opened = false;
+        if (this.onClose && !this.opened) this.onClose({});
+    }
+}

http://git-wip-us.apache.org/repos/asf/ignite/blob/d24dab81/modules/web-console/frontend/app/components/panel-collapsible/index.js
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/components/panel-collapsible/index.js b/modules/web-console/frontend/app/components/panel-collapsible/index.js
new file mode 100644
index 0000000..6f64b3b
--- /dev/null
+++ b/modules/web-console/frontend/app/components/panel-collapsible/index.js
@@ -0,0 +1,25 @@
+/*
+ * 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 component from './component';
+import transclude from './transcludeDirective';
+
+export default angular
+    .module('ignite-console.panel-collapsible', [])
+    .directive('panelCollapsibleTransclude', transclude)
+    .component('panelCollapsible', component);

http://git-wip-us.apache.org/repos/asf/ignite/blob/d24dab81/modules/web-console/frontend/app/components/panel-collapsible/index.spec.js
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/components/panel-collapsible/index.spec.js b/modules/web-console/frontend/app/components/panel-collapsible/index.spec.js
new file mode 100644
index 0000000..7972246
--- /dev/null
+++ b/modules/web-console/frontend/app/components/panel-collapsible/index.spec.js
@@ -0,0 +1,140 @@
+/*
+ * 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 'mocha';
+import {assert} from 'chai';
+import angular from 'angular';
+import 'angular-mocks';
+import {spy} from 'sinon';
+import componentModule from './index.js';
+
+suite('panel-collapsible', () => {
+    const ICON_COLLAPSE = 'collapse';
+    const ICON_EXPAND = 'expand';
+    /** @type {ng.IScope} */
+    let $scope;
+    /** @type {ng.ICompileService} */
+    let $compile;
+    angular.module('test', [componentModule.name]);
+
+    const isClosed = (el) => el[0].querySelector('.panel-collapsible__content').classList.contains('ng-hide');
+    const click = (el) => el[0].querySelector('.panel-collapsible__status-icon').click();
+    const getIcon = (el) => (
+        el[0]
+            .querySelector('.panel-collapsible__status-icon [ignite-icon]:not(.ng-hide)')
+            .getAttribute('ignite-icon')
+    );
+
+    setup(() => {
+        angular.module('test', [componentModule.name]);
+        angular.mock.module('test');
+        angular.mock.inject((_$rootScope_, _$compile_) => {
+            $compile = _$compile_;
+            $scope = _$rootScope_.$new();
+        });
+    });
+
+    test('Required slot', () => {
+        const el = angular.element(`<panel-collapsible></panel-collapsible>`);
+        assert.throws(
+            () => $compile(el)($scope),
+            /Required transclusion slot `content` was not filled/,
+            'Throws when panel-content slot was not filled'
+        );
+    });
+
+    test('Open/close', () => {
+        $scope.opened = false;
+        const onOpen = $scope.onOpen = spy();
+        const onClose = $scope.onClose = spy();
+        const el = angular.element(`
+            <panel-collapsible
+                opened="opened"
+                on-open="onOpen()"
+                on-close="onClose()"
+            >
+                <panel-content>Content</panel-content>
+            </panel-collapsible>
+        `);
+
+        $compile(el)($scope);
+        $scope.$digest();
+        assert.equal(getIcon(el), ICON_EXPAND, `Shows ${ICON_EXPAND} icon when closed`);
+        assert.ok(isClosed(el), 'Hides content by default');
+        click(el);
+        $scope.$digest();
+        assert.equal(getIcon(el), ICON_COLLAPSE, `Shows ${ICON_COLLAPSE} icon when opened`);
+        assert.notOk(isClosed(el), 'Shows content when clicked');
+        click(el);
+        $scope.$digest();
+        assert.equal(onOpen.callCount, 1, 'Evaluates onOpen expression');
+        assert.equal(onClose.callCount, 1, 'Evaluates onClose expression');
+        $scope.opened = true;
+        $scope.$digest();
+        assert.notOk(isClosed(el), 'Uses opened binding to control visibility');
+    });
+
+    test('Slot transclusion', () => {
+        const el = angular.element(`
+            <panel-collapsible opened='::true'>
+                <panel-title>Title {{$panel.opened}}</panel-title>
+                <panel-description>Description {{$panel.opened}}</panel-description>
+                <panel-actions>
+                    <button
+                        class='my-button'
+                        ng-click='$panel.close()'
+                    >Button {{$panel.opened}}</button>
+                </panel-actions>
+                <panel-content>Content {{$panel.opened}}</panel-content>
+            </panel-collapsible>
+        `);
+        $compile(el)($scope);
+        $scope.$digest();
+        assert.equal(
+            el[0].querySelector('panel-title').textContent,
+            'Title true',
+            'Transcludes title slot and exposes $panel controller'
+        );
+        assert.equal(
+            el[0].querySelector('panel-description').textContent,
+            'Description true',
+            'Transcludes Description slot and exposes $panel controller'
+        );
+        assert.equal(
+            el[0].querySelector('panel-content').textContent,
+            'Content true',
+            'Transcludes content slot and exposes $panel controller'
+        );
+        el[0].querySelector('.my-button').click();
+        $scope.$digest();
+        assert.ok(isClosed(el), 'Can close by calling a method on exposed controller');
+    });
+
+    test('Disabled state', () => {
+        const el = angular.element(`
+            <panel-collapsible disabled='disabled'>
+                <panel-content>Content</panel-content>
+            </panel-collapsible>
+        `);
+        $compile(el)($scope);
+        $scope.$digest();
+        click(el);
+        $scope.$digest();
+        assert.ok(isClosed(el), `Can't be opened when disabled`);
+        // TODO: test disabled styles
+    });
+});

http://git-wip-us.apache.org/repos/asf/ignite/blob/d24dab81/modules/web-console/frontend/app/components/panel-collapsible/style.scss
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/components/panel-collapsible/style.scss b/modules/web-console/frontend/app/components/panel-collapsible/style.scss
new file mode 100644
index 0000000..73eba25
--- /dev/null
+++ b/modules/web-console/frontend/app/components/panel-collapsible/style.scss
@@ -0,0 +1,79 @@
+/*
+ * 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.
+ */
+
+panel-collapsible {
+    display: flex;
+    flex-direction: column;
+    font-family: Roboto;
+    border-radius: 0 0 4px 4px;
+    background-color: #ffffff;
+    box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2);
+
+    &[disabled] {
+        opacity: 0.5;
+    }
+
+    .#{&}__status-icon {
+        margin-right: 10px;
+        color: #757575;
+    }
+
+    .#{&}__heading {
+        padding: 30px 20px 30px;
+        color: #393939;
+        display: flex;
+        flex-direction: row;
+        align-items: baseline;
+        user-select: none;
+        cursor: pointer;
+        line-height: 1.42857;
+    }
+
+    .#{&}__title {
+        font-size: 16px;
+        margin-right: 8px;
+        white-space: nowrap;
+        overflow: hidden;
+        text-overflow: ellipsis;
+    }
+
+    .#{&}__description {
+        font-size: 12px;
+        color: #757575;
+        flex: 1 1;
+    }
+
+    .#{&}__actions {
+        margin-left: auto;
+
+        & > panel-actions {
+            display: flex;
+            flex-direction: row;
+
+            & > * {
+                flex: 0 0 auto;
+                margin-left: 10px;
+            }
+        }
+    }
+
+    .#{&}__content {
+        border-top: 1px solid #dddddd;
+        padding: 15px 20px;
+        contain: content;
+    }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/ignite/blob/d24dab81/modules/web-console/frontend/app/components/panel-collapsible/template.pug
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/components/panel-collapsible/template.pug b/modules/web-console/frontend/app/components/panel-collapsible/template.pug
new file mode 100644
index 0000000..9120f58
--- /dev/null
+++ b/modules/web-console/frontend/app/components/panel-collapsible/template.pug
@@ -0,0 +1,24 @@
+//-
+    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.
+
+.panel-collapsible__heading(ng-click='$ctrl.toggle()')
+    .panel-collapsible__status-icon
+        svg(ng-show='$ctrl.opened' ignite-icon='collapse')
+        svg(ng-hide='$ctrl.opened' ignite-icon='expand')
+    .panel-collapsible__title(panel-collapsible-transclude='title') {{::$ctrl.title}}
+    .panel-collapsible__description(panel-collapsible-transclude='description') {{::$ctrl.description}}
+    .panel-collapsible__actions(panel-collapsible-transclude='actions' ng-click='$event.stopPropagation()')
+.panel-collapsible__content(panel-collapsible-transclude='content' ng-show='$ctrl.opened')
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/ignite/blob/d24dab81/modules/web-console/frontend/app/components/panel-collapsible/transcludeDirective.js
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/components/panel-collapsible/transcludeDirective.js b/modules/web-console/frontend/app/components/panel-collapsible/transcludeDirective.js
new file mode 100644
index 0000000..86509f5
--- /dev/null
+++ b/modules/web-console/frontend/app/components/panel-collapsible/transcludeDirective.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.
+ */
+
+// eslint-disable-next-line
+import {default as Panel} from './controller';
+
+export default function panelCollapsibleTransclude() {
+    return {
+        restrict: 'A',
+        require: {
+            panel: '^panelCollapsible'
+        },
+        scope: true,
+        controller: class {
+            /** @type {Panel} */
+            panel;
+            /** @type {string} */
+            slot;
+            static $inject = ['$element'];
+            /**
+             * @param {JQLite} $element
+             */
+            constructor($element) {
+                this.$element = $element;
+            }
+            $postLink() {
+                this.panel.$transclude((clone, scope) => {
+                    scope.$panel = this.panel;
+                    this.$element.append(clone);
+                }, null, this.slot);
+            }
+        },
+        bindToController: {
+            slot: '@panelCollapsibleTransclude'
+        }
+    };
+}

http://git-wip-us.apache.org/repos/asf/ignite/blob/d24dab81/modules/web-console/frontend/app/modules/states/configuration/caches/affinity.pug
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/modules/states/configuration/caches/affinity.pug b/modules/web-console/frontend/app/modules/states/configuration/caches/affinity.pug
index ca781da..ce2cad5 100644
--- a/modules/web-console/frontend/app/modules/states/configuration/caches/affinity.pug
+++ b/modules/web-console/frontend/app/modules/states/configuration/caches/affinity.pug
@@ -27,63 +27,60 @@ include /app/helpers/jade/mixins
 -var rendPartitionsRequired = rendezvousAff + ' && ' + affModel + '.Rendezvous.affinityBackupFilter'
 -var fairPartitionsRequired = fairAff + ' && ' + affModel + '.Fair.affinityBackupFilter'
 
-.pca-panel(ng-form=form novalidate)
-    .pca-panel-heading(bs-collapse-toggle='' ng-click=`ui.loadPanel('${form}')`)
-        ignite-form-panel-chevron
-        .pca-panel-heading-title Affinity Collocation
-        .pca-panel-heading-description
-            | Collocate data with data to improve performance and scalability of your application. 
-            a.link-success(href="https://apacheignite.readme.io/docs/affinity-collocation" target="_blank") More info
-    .pca-panel-collapse(role='tabpanel' bs-collapse-target id=`${form}`)
-        .pca-panel-body.pca-form-row(ng-if=`ui.isPanelLoaded('${form}')`)
-            .pca-form-column-6.pc-form-grid-row
-                .pc-form-grid-col-60(ng-if='$ctrl.available(["1.0.0", "2.0.0"])')
-                    +dropdown('Function:', `${affModel}.kind`, '"AffinityKind"', 'true', 'Default', 'affinityFunction',
-                        'Key topology resolver to provide mapping from keys to nodes<br/>\
-                        <ul>\
-                            <li>Rendezvous - Based on Highest Random Weight algorithm</li>\
-                            <li>Fair - Tries to ensure that all nodes get equal number of partitions with minimum amount of reassignments between existing nodes</li>\
-                            <li>Custom - Custom implementation of key affinity fynction</li>\
-                            <li>Default - By default rendezvous affinity function  with 1024 partitions is used</li>\
-                        </ul>')
-                .pc-form-grid-col-60(ng-if='$ctrl.available("2.0.0")')
-                    +dropdown('Function:', `${affModel}.kind`, '"AffinityKind"', 'true', 'Default', 'affinityFunction',
-                        'Key topology resolver to provide mapping from keys to nodes<br/>\
-                        <ul>\
-                            <li>Rendezvous - Based on Highest Random Weight algorithm</li>\
-                            <li>Custom - Custom implementation of key affinity fynction</li>\
-                            <li>Default - By default rendezvous affinity function  with 1024 partitions is used</li>\
-                        </ul>')
-                .pc-form-group
-                    .pc-form-grid-row(ng-if=rendezvousAff)
-                        .pc-form-grid-col-60
-                            +number-required('Partitions', `${affModel}.Rendezvous.partitions`, '"RendPartitions"', 'true', rendPartitionsRequired, '1024', '1', 'Number of partitions')
-                        .pc-form-grid-col-60
-                            +java-class('Backup filter', `${affModel}.Rendezvous.affinityBackupFilter`, '"RendAffinityBackupFilter"', 'true', 'false',
-                                'Backups will be selected from all nodes that pass this filter')
-                        .pc-form-grid-col-60
-                            +checkbox('Exclude neighbors', `${affModel}.Rendezvous.excludeNeighbors`, '"RendExcludeNeighbors"',
-                                'Exclude same - host - neighbors from being backups of each other and specified number of backups')
-                    .pc-form-grid-row(ng-if=fairAff)
-                        .pc-form-grid-col-60
-                            +number-required('Partitions', `${affModel}.Fair.partitions`, '"FairPartitions"', 'true', fairPartitionsRequired, '256', '1', 'Number of partitions')
-                        .pc-form-grid-col-60
-                            +java-class('Backup filter', `${affModel}.Fair.affinityBackupFilter`, '"FairAffinityBackupFilter"', 'true', 'false',
-                                'Backups will be selected from all nodes that pass this filter')
-                        .pc-form-grid-col-60
-                            +checkbox('Exclude neighbors', `${affModel}.Fair.excludeNeighbors`, '"FairExcludeNeighbors"',
-                                'Exclude same - host - neighbors from being backups of each other and specified number of backups')
-                    .pc-form-grid-row(ng-if=customAff)
-                        .pc-form-grid-col-60
-                            +java-class('Class name:', `${affModel}.Custom.className`, '"AffCustomClassName"', 'true', customAff,
-                                'Custom key affinity function implementation class name')
-                .pc-form-grid-col-60
-                    +java-class('Mapper:', model + '.affinityMapper', '"AffMapCustomClassName"', 'true', 'false',
-                        'Provide custom affinity key for any given key')
+panel-collapsible(ng-form=form on-open=`ui.loadPanel('${form}')`)
+    panel-title Affinity Collocation
+    panel-description
+        | Collocate data with data to improve performance and scalability of your application. 
+        a.link-success(href="https://apacheignite.readme.io/docs/affinity-collocation" target="_blank") More info
+    panel-content.pca-form-row(ng-if=`ui.isPanelLoaded('${form}')`)
+        .pca-form-column-6.pc-form-grid-row
+            .pc-form-grid-col-60(ng-if='$ctrl.available(["1.0.0", "2.0.0"])')
+                +dropdown('Function:', `${affModel}.kind`, '"AffinityKind"', 'true', 'Default', 'affinityFunction',
+                    'Key topology resolver to provide mapping from keys to nodes<br/>\
+                    <ul>\
+                        <li>Rendezvous - Based on Highest Random Weight algorithm</li>\
+                        <li>Fair - Tries to ensure that all nodes get equal number of partitions with minimum amount of reassignments between existing nodes</li>\
+                        <li>Custom - Custom implementation of key affinity fynction</li>\
+                        <li>Default - By default rendezvous affinity function  with 1024 partitions is used</li>\
+                    </ul>')
+            .pc-form-grid-col-60(ng-if='$ctrl.available("2.0.0")')
+                +dropdown('Function:', `${affModel}.kind`, '"AffinityKind"', 'true', 'Default', 'affinityFunction',
+                    'Key topology resolver to provide mapping from keys to nodes<br/>\
+                    <ul>\
+                        <li>Rendezvous - Based on Highest Random Weight algorithm</li>\
+                        <li>Custom - Custom implementation of key affinity fynction</li>\
+                        <li>Default - By default rendezvous affinity function  with 1024 partitions is used</li>\
+                    </ul>')
+            .pc-form-group
+                .pc-form-grid-row(ng-if=rendezvousAff)
+                    .pc-form-grid-col-60
+                        +number-required('Partitions', `${affModel}.Rendezvous.partitions`, '"RendPartitions"', 'true', rendPartitionsRequired, '1024', '1', 'Number of partitions')
+                    .pc-form-grid-col-60
+                        +java-class('Backup filter', `${affModel}.Rendezvous.affinityBackupFilter`, '"RendAffinityBackupFilter"', 'true', 'false',
+                            'Backups will be selected from all nodes that pass this filter')
+                    .pc-form-grid-col-60
+                        +checkbox('Exclude neighbors', `${affModel}.Rendezvous.excludeNeighbors`, '"RendExcludeNeighbors"',
+                            'Exclude same - host - neighbors from being backups of each other and specified number of backups')
+                .pc-form-grid-row(ng-if=fairAff)
+                    .pc-form-grid-col-60
+                        +number-required('Partitions', `${affModel}.Fair.partitions`, '"FairPartitions"', 'true', fairPartitionsRequired, '256', '1', 'Number of partitions')
+                    .pc-form-grid-col-60
+                        +java-class('Backup filter', `${affModel}.Fair.affinityBackupFilter`, '"FairAffinityBackupFilter"', 'true', 'false',
+                            'Backups will be selected from all nodes that pass this filter')
+                    .pc-form-grid-col-60
+                        +checkbox('Exclude neighbors', `${affModel}.Fair.excludeNeighbors`, '"FairExcludeNeighbors"',
+                            'Exclude same - host - neighbors from being backups of each other and specified number of backups')
+                .pc-form-grid-row(ng-if=customAff)
+                    .pc-form-grid-col-60
+                        +java-class('Class name:', `${affModel}.Custom.className`, '"AffCustomClassName"', 'true', customAff,
+                            'Custom key affinity function implementation class name')
+            .pc-form-grid-col-60
+                +java-class('Mapper:', model + '.affinityMapper', '"AffMapCustomClassName"', 'true', 'false',
+                    'Provide custom affinity key for any given key')
 
-                //- Since ignite 2.0
-                .pc-form-grid-col-60(ng-if='$ctrl.available("2.0.0")')
-                    +java-class('Topology validator:', model + '.topologyValidator', '"topologyValidator"', 'true', 'false')
+            //- Since ignite 2.0
+            .pc-form-grid-col-60(ng-if='$ctrl.available("2.0.0")')
+                +java-class('Topology validator:', model + '.topologyValidator', '"topologyValidator"', 'true', 'false')
 
-            .pca-form-column-6
-                +preview-xml-java(model, 'cacheAffinity')
+        .pca-form-column-6
+            +preview-xml-java(model, 'cacheAffinity')

http://git-wip-us.apache.org/repos/asf/ignite/blob/d24dab81/modules/web-console/frontend/app/modules/states/configuration/caches/concurrency.pug
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/modules/states/configuration/caches/concurrency.pug b/modules/web-console/frontend/app/modules/states/configuration/caches/concurrency.pug
index 2902f21..d99f894 100644
--- a/modules/web-console/frontend/app/modules/states/configuration/caches/concurrency.pug
+++ b/modules/web-console/frontend/app/modules/states/configuration/caches/concurrency.pug
@@ -19,49 +19,46 @@ include /app/helpers/jade/mixins
 -var form = 'concurrency'
 -var model = '$ctrl.clonedCache'
 
-.pca-panel(ng-form=form novalidate)
-    .pca-panel-heading(bs-collapse-toggle='' ng-click=`ui.loadPanel('${form}')`)
-        ignite-form-panel-chevron
-        .pca-panel-heading-title Concurrency control
-        .pca-panel-heading-description
-            | Cache concurrent asynchronous operations settings.
-    .pca-panel-collapse(role='tabpanel' bs-collapse-target id=`${form}`)
-        .pca-panel-body.pca-form-row(ng-if=`ui.isPanelLoaded('${form}')`)
-            .pca-form-column-6.pc-form-grid-row
-                .pc-form-grid-col-30
-                    +number('Max async operations:', `${model}.maxConcurrentAsyncOperations`, '"maxConcurrentAsyncOperations"', 'true', '500', '0',
-                        'Maximum number of allowed concurrent asynchronous operations<br/>\
-                        If <b>0</b> then number of concurrent asynchronous operations is unlimited')
-                .pc-form-grid-col-30
-                    +number('Default lock timeout:', `${model}.defaultLockTimeout`, '"defaultLockTimeout"', 'true', '0', '0',
-                        'Default lock acquisition timeout in milliseconds<br/>\
-                        If <b>0</b> then lock acquisition will never timeout')
+panel-collapsible(ng-form=form on-open=`ui.loadPanel('${form}')`)
+    panel-title Concurrency control
+    panel-description
+        | Cache concurrent asynchronous operations settings.
+    panel-content.pca-form-row(ng-if=`ui.isPanelLoaded('${form}')`)
+        .pca-form-column-6.pc-form-grid-row
+            .pc-form-grid-col-30
+                +number('Max async operations:', `${model}.maxConcurrentAsyncOperations`, '"maxConcurrentAsyncOperations"', 'true', '500', '0',
+                    'Maximum number of allowed concurrent asynchronous operations<br/>\
+                    If <b>0</b> then number of concurrent asynchronous operations is unlimited')
+            .pc-form-grid-col-30
+                +number('Default lock timeout:', `${model}.defaultLockTimeout`, '"defaultLockTimeout"', 'true', '0', '0',
+                    'Default lock acquisition timeout in milliseconds<br/>\
+                    If <b>0</b> then lock acquisition will never timeout')
 
-                //- Removed in ignite 2.0
-                .pc-form-grid-col-60(ng-if='$ctrl.available(["1.0.0", "2.0.0"])' ng-hide=`${model}.atomicityMode === 'TRANSACTIONAL'`)
-                    +dropdown('Entry versioning:', `${model}.atomicWriteOrderMode`, '"atomicWriteOrderMode"', 'true', 'Choose versioning',
-                        '[\
-                            {value: "CLOCK", label: "CLOCK"},\
-                            {value: "PRIMARY", label: "PRIMARY"}\
-                        ]',
-                        'Write ordering mode determines which node assigns the write version, sender or the primary node\
-                        <ul>\
-                            <li>CLOCK - in this mode write versions are assigned on a sender node which generally leads to better performance</li>\
-                            <li>PRIMARY - in this mode version is assigned only on primary node. This means that sender will only send write request to primary node, which in turn will assign write version and forward it to backups</li>\
-                        </ul>')
+            //- Removed in ignite 2.0
+            .pc-form-grid-col-60(ng-if='$ctrl.available(["1.0.0", "2.0.0"])' ng-hide=`${model}.atomicityMode === 'TRANSACTIONAL'`)
+                +dropdown('Entry versioning:', `${model}.atomicWriteOrderMode`, '"atomicWriteOrderMode"', 'true', 'Choose versioning',
+                    '[\
+                        {value: "CLOCK", label: "CLOCK"},\
+                        {value: "PRIMARY", label: "PRIMARY"}\
+                    ]',
+                    'Write ordering mode determines which node assigns the write version, sender or the primary node\
+                    <ul>\
+                        <li>CLOCK - in this mode write versions are assigned on a sender node which generally leads to better performance</li>\
+                        <li>PRIMARY - in this mode version is assigned only on primary node. This means that sender will only send write request to primary node, which in turn will assign write version and forward it to backups</li>\
+                    </ul>')
 
-                .pc-form-grid-col-60
-                    +dropdown('Write synchronization mode:', `${model}.writeSynchronizationMode`, '"writeSynchronizationMode"', 'true', 'PRIMARY_SYNC',
-                        '[\
-                            {value: "FULL_SYNC", label: "FULL_SYNC"},\
-                            {value: "FULL_ASYNC", label: "FULL_ASYNC"},\
-                            {value: "PRIMARY_SYNC", label: "PRIMARY_SYNC"}\
-                        ]',
-                        'Write synchronization mode\
-                        <ul>\
-                            <li>FULL_SYNC - Ignite will wait for write or commit replies from all nodes</li>\
-                            <li>FULL_ASYNC - Ignite will not wait for write or commit responses from participating nodes</li>\
-                            <li>PRIMARY_SYNC - Makes sense for PARTITIONED mode. Ignite will wait for write or commit to complete on primary node</li>\
-                        </ul>')
-            .pca-form-column-6
-                +preview-xml-java(model, 'cacheConcurrency')
+            .pc-form-grid-col-60
+                +dropdown('Write synchronization mode:', `${model}.writeSynchronizationMode`, '"writeSynchronizationMode"', 'true', 'PRIMARY_SYNC',
+                    '[\
+                        {value: "FULL_SYNC", label: "FULL_SYNC"},\
+                        {value: "FULL_ASYNC", label: "FULL_ASYNC"},\
+                        {value: "PRIMARY_SYNC", label: "PRIMARY_SYNC"}\
+                    ]',
+                    'Write synchronization mode\
+                    <ul>\
+                        <li>FULL_SYNC - Ignite will wait for write or commit replies from all nodes</li>\
+                        <li>FULL_ASYNC - Ignite will not wait for write or commit responses from participating nodes</li>\
+                        <li>PRIMARY_SYNC - Makes sense for PARTITIONED mode. Ignite will wait for write or commit to complete on primary node</li>\
+                    </ul>')
+        .pca-form-column-6
+            +preview-xml-java(model, 'cacheConcurrency')