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/02 12:24:30 UTC

[07/24] ignite git commit: IGNITE-5466 Web Console: Configuration reworked to cluster centric model: 1. Reworked data model. 2. Implemented migrations. 3. Reworked UI for all screens. 4. Reworked validation. 5. Many refactorings to improve code base

http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/frontend/app/modules/states/configuration/domains/query.pug
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/modules/states/configuration/domains/query.pug b/modules/web-console/frontend/app/modules/states/configuration/domains/query.pug
index b4b5abe..9dc513a 100644
--- a/modules/web-console/frontend/app/modules/states/configuration/domains/query.pug
+++ b/modules/web-console/frontend/app/modules/states/configuration/domains/query.pug
@@ -22,169 +22,237 @@ include /app/helpers/jade/mixins
 -var queryFields = `${model}.fields`
 -var queryAliases = `${model}.aliases`
 -var queryIndexes = `${model}.indexes`
--var queryFieldsForm = 'queryFields'
--var queryAliasesForm = 'queryAliases'
--var queryIndexesForm = 'queryIndexes'
-
-//- LEGACY mixin for LEGACY index fields table.
-mixin table-index-item-edit(prefix, index, sortAvailable, idAddition)
-    -var fieldName = `${prefix}FieldName`
-    -var direction = `${prefix}Direction`
-
-    -var fieldNameModel = `indexesTbl.${fieldName}`
-    -var directionModel = `indexesTbl.${direction}`
-
-    -var btnVisible = `tableIndexItemSaveVisible(indexesTbl, ${index})`
-    -var btnSave = `tableIndexItemSave(indexesTbl, itemIndex, ${index})`
-    -var btnVisibleAndSave = `${btnVisible} && ${btnSave}`
-
-    div(ng-if=sortAvailable)
-        .col-xs-8.col-sm-8.col-md-8
-            label.fieldSep /
-            .input-tip
-                button.select-toggle.form-control(id=`{{::'${fieldName}' + ${idAddition}}}` ignite-on-enter-focus-move=`{{::'${direction}S' + ${idAddition}}}` ng-model=fieldNameModel placeholder=`{{fields('${prefix}', ${fieldNameModel}).length > 0 ? 'Choose field' : 'No fields configured'}}` bs-select bs-options=`item.value as item.label for item in fields('${prefix}', ${fieldNameModel})` ng-disabled=`fields('${prefix}', ${fieldNameModel}).length === 0` ignite-on-escape='tableReset(false)' tabindex='0')
-        .col-xs-4.col-sm-4.col-md-4
-            +btn-save(btnVisible, btnSave)
-            .input-tip
-                button.select-toggle.form-control(id=`{{::'${direction}' + ${idAddition}}}` ng-model=directionModel bs-select bs-options='item.value as item.label for item in {{sortDirections}}' ignite-on-enter=btnVisibleAndSave ignite-on-escape='tableReset(false)' tabindex='0')
-    .col-xs-12(ng-if=`!(${sortAvailable})`)
-        +btn-save(btnVisible, btnSave)
-        .input-tip
-            button.select-toggle.form-control(id=`{{::'${fieldName}' + ${idAddition}}}` ng-model=fieldNameModel placeholder=`{{fields('${prefix}', ${fieldNameModel}).length > 0 ? 'Choose index field' : 'No fields configured'}}` bs-select bs-options=`item.value as item.label for item in fields('${prefix}', ${fieldNameModel})` ng-disabled=`fields('${prefix}', ${fieldNameModel}).length === 0` ignite-on-escape='tableReset(false)' tabindex='0')
-
-.panel.panel-default(ng-form=form novalidate)
-    .panel-heading(bs-collapse-toggle)
+
+.pca-panel.pca-panel-default(ng-form=form novalidate)
+    .pca-panel-heading(bs-collapse-toggle)
         ignite-form-panel-chevron
-        label(id='query-title') Domain model for SQL query
-        ignite-form-field-tooltip.tipLabel
-            | Domain model properties for fields queries#[br]
-            | #[a(href='https://apacheignite.readme.io/docs/cache-queries' target='_blank') More info]
-        ignite-form-revert
-    .panel-collapse(role='tabpanel' bs-collapse-target id='query')
-        .panel-body
-            .col-sm-6
-                .content-not-available(ng-if=`${model}.queryMetadata === 'Annotations'`)
+        .pca-panel-heading-title(id='query-title') Domain model for SQL query
+        .pca-panel-heading-description
+            | Domain model properties for fields queries. 
+            a.link-success(href='https://apacheignite.readme.io/docs/cache-queries' target='_blank') More info
+    .pca-panel-collapse(role='tabpanel' bs-collapse-target id='query')
+        .pca-panel-body.pca-form-row
+            .pca-form-column-6.pc-form-grid-row
+                .content-not-available(
+                    ng-if=`${model}.queryMetadata === 'Annotations'`
+                    style='margin-top: 10px'
+                )
                     label Not available for annotated types
-                div(ng-if=`${model}.queryMetadata === 'Configuration'`)
-                    .settings-row
-                        +text('Table name:', `${model}.tableName`, '"tableName"', 'false', 'Enter table name', 'Table name for this query entity')
-                    div(ng-if='$ctrl.available("2.0.0")')
-                        .settings-row
-                            +text('Key field name:', `${model}.keyFieldName`, '"keyFieldName"', 'false', 'Enter key field name',
-                                'Key name.<br/>' +
-                                'Can be used in field list to denote the key as a whole')
-                        .settings-row
-                            +text('Value field name:', `${model}.valueFieldName`, '"valueFieldName"', 'false', 'Enter value field name',
-                                'Value name.<br/>' +
-                                'Can be used in field list to denote the entire value')
-                    .settings-row
-                        +ignite-form-group(ng-model=queryFields ng-form=queryFieldsForm)
-                            ignite-form-field-label(id='queryFields')
-                                | Fields
-                            ignite-form-group-tooltip
-                                | Collection of name-to-type mappings to be queried, in addition to indexed fields
-                            ignite-form-group-add(ng-click='tableNewItem(queryFieldsTbl)')
-                                | Add field to query
-                            .group-content-empty(ng-if=`!((${queryFields} && ${queryFields}.length > 0) || tableNewItemActive(queryFieldsTbl))`)
-                                | Not defined
-                            .group-content(ng-show=`(${queryFields} && ${queryFields}.length > 0) || tableNewItemActive(queryFieldsTbl)`)
-                                table.links-edit(id='fields' st-table=queryFields)
-                                    tbody
-                                        tr(ng-repeat=`item in ${queryFields} track by $index`)
-                                            td.col-sm-12(ng-hide='tableEditing(queryFieldsTbl, $index)')
-                                                a.labelFormField(ng-click='tableStartEdit(backupItem, queryFieldsTbl, $index)') {{item.name}}  / {{item.className}}
-                                                +btn-remove('tableRemove(backupItem, queryFieldsTbl, $index)', '"Remove path"')
-                                            td.col-sm-12(ng-show='tableEditing(queryFieldsTbl, $index)')
-                                                +table-pair-edit('queryFieldsTbl', 'cur', 'Field name', 'Field full class name', true, '{{::queryFieldsTbl.focusId + $index}}', '$index', '/')
-                                    tfoot(ng-show='tableNewItemActive(queryFieldsTbl)')
-                                        tr
-                                            td.col-sm-12
-                                                +table-pair-edit('queryFieldsTbl', 'new', 'Field name', 'Field full class name', true, '{{::queryFieldsTbl.focusId + $index}}', '-1', '/')
-                    .settings-row
-                        +ignite-form-field-dropdown('Key fields:', queryKeyFields, '"queryKeyFields"', false, false, true,
-                            'Select key fields', 'Configure available fields', 'fields(\'cur\', ' + queryKeyFields + ')',
-                            'Query fields that belongs to the key.<br/>\
-                             Used to build / modify keys and values during SQL DML operations when no key - value classes are present on cluster nodes.'
+
+                .pc-form-grid-col-60(ng-if-start=`${model}.queryMetadata === 'Configuration'`)
+                    +text('Table name:', `${model}.tableName`, '"tableName"', 'false', 'Enter table name')
+
+                .pc-form-grid-col-30(ng-if-start='$ctrl.available("2.0.0")')
+                    +text('Key field name:', `${model}.keyFieldName`, '"keyFieldName"', 'false', 'Enter key field name',
+                        'Key name.<br/>' +
+                        'Can be used in field list to denote the key as a whole')
+                .pc-form-grid-col-30(ng-if-end)
+                    +text('Value field name:', `${model}.valueFieldName`, '"valueFieldName"', 'false', 'Enter value field name',
+                        'Value name.<br/>' +
+                        'Can be used in field list to denote the entire value')
+
+                .pc-form-grid-col-60
+                    mixin domains-query-fields
+                        .ignite-form-field
+                            +ignite-form-field__label('Fields:', '"fields"')
+                                +tooltip(`Collection of name-to-type mappings to be queried, in addition to indexed fields`)
+                            .ignite-form-field__control
+                                -let items = queryFields
+                                list-editable(
+                                    ng-model=items
+                                    name='queryFields'
+                                    ng-change=`$ctrl.onQueryFieldsChange(${model})`
+                                )
+                                    list-editable-item-view
+                                        | {{ $item.name}} / {{ $item.className}}
+
+                                    list-editable-item-edit
+                                        - form = '$parent.form'
+                                        .pc-form-grid-row
+                                            .pc-form-grid-col-30(divider='/')
+                                                +ignite-form-field-text('Field name:', '$item.name', '"name"', false, true, 'Enter field name')(
+                                                    data-ignite-unique=items
+                                                    data-ignite-unique-property='name'
+                                                    ignite-auto-focus
+                                                )
+                                                    +unique-feedback('"name"', 'Property with such name already exists!')
+                                            .pc-form-grid-col-30
+                                                +java-class-typeahead('Field full class name:', `$item.className`, '"className"', '$ctrl.queryFieldTypes', true, true, 'Enter field full class name')(
+                                                    ng-model-options='{allowInvalid: true}'
+                                                    extra-valid-java-identifiers='$ctrl.queryFieldTypes'
+                                                )
+
+                                    list-editable-no-items
+                                        list-editable-add-item-button(
+                                            add-item=`$editLast((${items} = ${items} || []).push({}))`
+                                            label-single='field to query'
+                                            label-multiple='fields'
+                                        )
+
+                    +domains-query-fields
+
+                .pc-form-grid-col-60
+                    +sane-ignite-form-field-dropdown({
+                        label: 'Key fields:',
+                        model: queryKeyFields,
+                        name: '"queryKeyFields"',
+                        multiple: true,
+                        placeholder: 'Select key fields',
+                        placeholderEmpty: 'Configure available fields',
+                        options: `$ctrl.fields('cur', ${queryKeyFields})`,
+                        tip: 'Query fields that belongs to the key.<br/>\
+                         Used to build / modify keys and values during SQL DML operations when no key - value classes are present on cluster nodes.'
+                    })
+                .pc-form-grid-col-60
+                    mixin domains-query-aliases
+                        .ignite-form-field
+                            +ignite-form-field__label('Aliases:', '"aliases"')
+                                +tooltip(`Mapping from full property name in dot notation to an alias that will be used as SQL column name<br />
+                                    For example: "parent.name" as "parentName"`)
+                            .ignite-form-field__control
+                                -let items = queryAliases
+
+                                list-editable(ng-model=items name='queryAliases')
+                                    list-editable-item-view
+                                        | {{ $item.field }} &rarr; {{ $item.alias }}
+
+                                    list-editable-item-edit
+                                        - form = '$parent.form'
+                                        .pc-form-grid-row
+                                            .pc-form-grid-col-30(divider='/')
+                                                +ignite-form-field-text('Field name', '$item.field', '"field"', false, true, 'Enter field name')(
+                                                    data-ignite-unique=items
+                                                    data-ignite-unique-property='field'
+                                                    ignite-auto-focus
+                                                )
+                                                    +unique-feedback('"field"', 'Such field already exists!')
+                                            .pc-form-grid-col-30
+                                                +ignite-form-field-text('Field alias', '$item.alias', '"alias"', false, true, 'Enter field alias')
+
+                                    list-editable-no-items
+                                        list-editable-add-item-button(
+                                            add-item=`$editLast((${items} = ${items} || []).push({}))`
+                                            label-single='alias to query'
+                                            label-multiple='aliases'
+                                        )
+
+                    +domains-query-aliases
+
+                .pc-form-grid-col-60(ng-if-end)
+                    .ignite-form-field
+                        +ignite-form-field__label('Indexes:', '"indexes"')
+                        .ignite-form-field__control
+                            list-editable(
+                                ng-model=queryIndexes
+                                ng-model-options='{allowInvalid: true}'
+                                name='queryIndexes'
+                                ui-validate=`{
+                                    complete: '$ctrl.Models.queryIndexes.complete($value)',
+                                    fieldsExist: '$ctrl.Models.queryIndexes.fieldsExist($value, ${queryFields})',
+                                    indexFieldsHaveUniqueNames: '$ctrl.Models.queryIndexes.indexFieldsHaveUniqueNames($value)'
+                                }`
+                                ui-validate-watch=`"[${queryIndexes}, ${queryFields}]"`
+                                ui-validate-watch-object-equality='true'
+                            )
+                                list-editable-item-view(item-name='queryIndex')
+                                    div {{ queryIndex.name }} [{{ queryIndex.indexType }}]
+                                    div(ng-repeat='field in queryIndex.fields track by field._id')
+                                        span {{ field.name }}
+                                        span(ng-if='queryIndex.indexType == "SORTED"')
+                                            |  / {{ field.direction ? 'ASC' : 'DESC'}}
+
+                                list-editable-item-edit(item-name='queryIndex')
+                                    .pc-form-grid-row
+                                        .pc-form-grid-col-30(divider='/')
+                                            +sane-ignite-form-field-text({
+                                                label: 'Index name:',
+                                                model: 'queryIndex.name',
+                                                name: '"name"',
+                                                required: true,
+                                                placeholder: 'Enter index name'
+                                            })(
+                                                ignite-unique=queryIndexes
+                                                ignite-unique-property='name'
+                                                ignite-form-field-input-autofocus='true'
+                                            )
+                                                +unique-feedback(_, 'Such index already exists!')
+                                        .pc-form-grid-col-30
+                                            +sane-ignite-form-field-dropdown({
+                                                label: 'Index type:',
+                                                model: `queryIndex.indexType`,
+                                                name: '"indexType"',
+                                                required: true,
+                                                placeholder: 'Select index type',
+                                                options: '::$ctrl.Models.indexType.values'
+                                            })
+                                        .pc-form-grid-col-60
+                                            .ignite-form-field
+                                                +ignite-form-field__label('Index fields:', '"indexFields"', true)
+                                                .ignite-form-field__control
+                                                    list-editable(
+                                                        ng-model='queryIndex.fields'
+                                                        ng-model-options='{allowInvalid: true}'
+                                                        name='indexFields'
+                                                        ng-required='true'
+                                                    )
+                                                        list-editable-item-view(item-name='indexField')
+                                                            | {{ indexField.name }} 
+                                                            span(ng-if='queryIndex.indexType === "SORTED"')
+                                                                |  / {{ indexField.direction ? "ASC" : "DESC" }}
+
+                                                        list-editable-item-edit(item-name='indexField')
+                                                            .pc-form-grid-row
+                                                                .pc-form-grid-col-60
+                                                                    +sane-ignite-form-field-dropdown({
+                                                                        label: 'Index field:',
+                                                                        model: 'indexField.name',
+                                                                        name: '"indexName"',
+                                                                        placeholder: `{{ ${queryFields}.length > 0 ? 'Choose index field' : 'No fields configured' }}`,
+                                                                        options: queryFields
+                                                                    })(
+                                                                        bs-options=`queryField.name as queryField.name for queryField in ${queryFields}`
+                                                                        ng-disabled=`${queryFields}.length === 0`
+                                                                        ng-model-options='{allowInvalid: true}'
+                                                                        ignite-unique='queryIndex.fields'
+                                                                        ignite-unique-property='name'
+                                                                        ignite-auto-focus
+                                                                    )
+                                                                        +unique-feedback(_, 'Such field already exists!')
+                                                                .pc-form-grid-col-60(
+                                                                    ng-if='queryIndex.indexType === "SORTED"'
+                                                                )
+                                                                    +sane-ignite-form-field-dropdown({
+                                                                        label: 'Sort direction:',
+                                                                        model: 'indexField.direction',
+                                                                        name: '"indexDirection"',
+                                                                        required: true,
+                                                                        options: '::$ctrl.Models.indexSortDirection.values'
+                                                                    })
+                                                        list-editable-no-items
+                                                            list-editable-add-item-button(
+                                                                add-item=`$edit($ctrl.Models.addIndexField(queryIndex.fields))`
+                                                                label-single='field to index'
+                                                                label-multiple='fields in index'
+                                                            )
+                                                .ignite-form-field__errors(
+                                                    ng-messages=`$form.indexFields.$error`
+                                                    ng-show=`$form.indexFields.$invalid`
+                                                )
+                                                    +form-field-feedback(_, 'required', 'Index fields should be configured')
+
+                                list-editable-no-items
+                                    list-editable-add-item-button(
+                                        add-item=`$edit($ctrl.Models.addIndex(${model}))`
+                                        label-single='index'
+                                        label-multiple='fields'
+                                    )
+                        .ignite-form-field__errors(
+                            ng-messages=`query.queryIndexes.$error`
+                            ng-show=`query.queryIndexes.$invalid`
                         )
-                    .settings-row
-                        +ignite-form-group(ng-model=queryAliases ng-form=queryAliasesForm)
-                            ignite-form-field-label
-                                | Aliases
-                            ignite-form-group-tooltip
-                                | Mapping from full property name in dot notation to an alias that will be used as SQL column name
-                                | For example: "parent.name" as "parentName"
-                            ignite-form-group-add(ng-click='tableNewItem(aliasesTbl)')
-                                | Add alias to query
-                            .group-content-empty(ng-if=`!((${queryAliases} && ${queryAliases}.length > 0) || tableNewItemActive(aliasesTbl))`)
-                                | Not defined
-                            .group-content(ng-show=`(${queryAliases} && ${queryAliases}.length > 0) || tableNewItemActive(aliasesTbl)`)
-                                table.links-edit(id='aliases' st-table=queryAliases)
-                                    tbody
-                                        tr(ng-repeat=`item in ${queryAliases} track by $index`)
-                                            td.col-sm-12(ng-hide='tableEditing(aliasesTbl, $index)')
-                                                a.labelFormField(ng-click='tableStartEdit(backupItem, aliasesTbl, $index)') {{item.field}} &rarr; {{item.alias}}
-                                                +btn-remove('tableRemove(backupItem, aliasesTbl, $index)', '"Remove alias"')
-                                            td.col-sm-12(ng-show='tableEditing(aliasesTbl, $index)')
-                                                +table-pair-edit('aliasesTbl', 'cur', 'Field name', 'Field Alias', false, '{{::aliasesTbl.focusId + $index}}', '$index', '&rarr;')
-                                    tfoot(ng-show='tableNewItemActive(aliasesTbl)')
-                                        tr
-                                            td.col-sm-12
-                                                +table-pair-edit('aliasesTbl', 'new', 'Field name', 'Field Alias', false, '{{::aliasesTbl.focusId + $index}}', '-1', '&rarr;')
-                    .settings-row(ng-init='indexesTbl={type: "table-indexes", model: "indexes", focusId: "IndexName", ui: "table-indexes"}')
-                        +ignite-form-group(ng-model=queryIndexes ng-form=queryIndexesForm)
-                            ignite-form-field-label
-                                | Indexes
-                            ignite-form-group-tooltip
-                                | Collection of indexes
-                            ignite-form-group-add(ng-click='tableNewItem(indexesTbl)')
-                                | Add new index
-                            .group-content-empty(id='indexes-add' ng-show=`!((${queryIndexes} && ${queryIndexes}.length > 0) || tableNewItemActive(indexesTbl))`)
-                                | Not defined
-                            .group-content(ng-show=`(${queryIndexes} && ${queryIndexes}.length > 0) || tableNewItemActive(indexesTbl)`)
-                                -var btnVisibleAndSave = 'tableIndexSaveVisible(indexesTbl, $index) && tableIndexSave(indexesTbl, $index)'
-
-                                table.links-edit(st-table=queryIndexes ng-init='newDirection = false')
-                                    tbody
-                                        tr(ng-repeat=`item in ${queryIndexes} track by $index`)
-                                            td
-                                                .col-sm-12(ng-hide='tableEditing(indexesTbl, $index)')
-                                                    a.labelFormField(id='indexes{{$index}}' ng-click='tableStartEdit(backupItem, indexesTbl, $index)') {{$index + 1}}) {{item.name}} [{{item.indexType}}]
-                                                    +btn-remove('tableRemove(backupItem, indexesTbl, $index)', '"Remove index"')
-                                                    +btn-add('tableIndexNewItem(indexesTbl, $index)', '"Add new field to index"')
-                                                div(ng-show='tableEditing(indexesTbl, $index)')
-                                                    .col-sm-7
-                                                        label.fieldSep /
-                                                        .input-tip
-                                                            input.form-control(id='curIndexName{{$index}}' type='text' ignite-on-enter-focus-move='curIndexType{{$index}}' ng-model='indexesTbl.curIndexName' placeholder='Index name' ignite-on-enter=btnVisibleAndSave ignite-on-escape='tableReset(false)')
-                                                    .col-sm-5
-                                                        +btn-save('tableIndexSaveVisible(indexesTbl, $index)', 'tableIndexSave(indexesTbl, $index)')
-                                                        .input-tip
-                                                            button.select-toggle.form-control(id='curIndexType{{$index}}' bs-select ng-model='indexesTbl.curIndexType' data-placeholder='Select index type' bs-options='item.value as item.label for item in indexType' tabindex='0' ignite-on-enter=btnVisibleAndSave ignite-on-escape='tableReset(false)')
-                                                .margin-left-dflt
-                                                    table.links-edit-sub(st-table='item.fields' ng-init='itemIndex = $index')
-                                                        tbody
-                                                            tr(ng-repeat='itemItem in item.fields track by $index')
-                                                                td
-                                                                    div(ng-hide='tableIndexItemEditing(indexesTbl, itemIndex, $index)')
-                                                                        a.labelFormField(ng-if='item.indexType == "SORTED"' ng-click='tableIndexItemStartEdit(indexesTbl, itemIndex, $index)') {{$index + 1}}) {{itemItem.name}} / {{itemItem.direction ? "ASC" : "DESC"}}
-                                                                        a.labelFormField(ng-if='item.indexType != "SORTED"' ng-click='tableIndexItemStartEdit(indexesTbl, itemIndex, $index)') {{$index + 1}}) {{itemItem.name}}
-                                                                        +btn-remove('tableRemoveIndexItem(item, $index)', '"Remove field from index"')
-                                                                    div(ng-show='tableIndexItemEditing(indexesTbl, itemIndex, $index)')
-                                                                        +table-index-item-edit('cur', '$index', 'item.indexType == "SORTED"', 'itemIndex + "-" + $index')
-                                                        tfoot(ng-show='tableIndexNewItemActive(indexesTbl, itemIndex)')
-                                                            tr(style='padding-left: 18px')
-                                                                td
-                                                                    +table-index-item-edit('new', '-1', 'item.indexType == "SORTED"', 'itemIndex')
-                                    tfoot(ng-show='tableNewItemActive(indexesTbl)')
-                                        tr
-                                            td
-                                                .col-sm-7
-                                                    .fieldSep /
-                                                    .input-tip
-                                                        input#newIndexName.form-control(type='text' ignite-on-enter-focus-move='newIndexType' ng-model='indexesTbl.newIndexName' placeholder='Index name' ignite-on-enter='tableIndexSaveVisible(indexesTbl, -1) && tableIndexSave(indexesTbl, -1)' ignite-on-escape='tableReset(false)')
-                                                .col-sm-5
-                                                    +btn-save('tableIndexSaveVisible(indexesTbl, -1)', 'tableIndexSave(indexesTbl, -1)')
-                                                    .input-tip
-                                                        button#newIndexType.select-toggle.form-control(bs-select ng-model='indexesTbl.newIndexType' data-placeholder='Select index type' bs-options='item.value as item.label for item in indexType' tabindex='0' ignite-on-enter=btnVisibleAndSave ignite-on-escape='tableReset(false)')
-            .col-sm-6
+                            +form-field-feedback(_, 'complete', 'Some indexes are incomplete')
+                            +form-field-feedback(_, 'fieldsExist', 'Some indexes use unknown fields')
+                            +form-field-feedback(_, 'indexFieldsHaveUniqueNames', 'Each query index field name should be unique')
+
+            .pca-form-column-6
                 +preview-xml-java(model, 'domainModelQuery')

http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/frontend/app/modules/states/configuration/domains/store.pug
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/modules/states/configuration/domains/store.pug b/modules/web-console/frontend/app/modules/states/configuration/domains/store.pug
index 7afb8e5..eb0f9b7 100644
--- a/modules/web-console/frontend/app/modules/states/configuration/domains/store.pug
+++ b/modules/web-console/frontend/app/modules/states/configuration/domains/store.pug
@@ -20,108 +20,107 @@ include /app/helpers/jade/mixins
 -var model = 'backupItem'
 -var keyFields = `${model}.keyFields`
 -var valueFields = `${model}.valueFields`
--var keyFieldsForm = 'storeKeyFields'
--var valueFieldsForm = 'storeValueFields'
 
-//- LEGACY mixin for LEGACY db fields tables.
-mixin table-db-field-edit(tbl, prefix, focusId, index)
-    -var databaseName = `${prefix}DatabaseFieldName`
-    -var databaseType = `${prefix}DatabaseFieldType`
-    -var javaName = `${prefix}JavaFieldName`
-    -var javaType = `${prefix}JavaFieldType`
+mixin list-db-field-edit({ items, itemName, itemsName })
+    list-editable(
+        ng-model=items
+        ng-model-options='{allowInvalid: true}'
+        ui-validate=`{
+            dbFieldUnique: '$ctrl.Models.storeKeyDBFieldsUnique($value)'
+        }`
+        ui-validate-watch=`"${items}"`
+        ui-validate-watch-object-equality='true'
+    )&attributes(attributes)
+        list-editable-item-view
+            | {{ $item.databaseFieldName }} / {{ $item.databaseFieldType }} / {{ $item.javaFieldName }} / {{ $item.javaFieldType }}
 
-    -var databaseNameModel = `${tbl}.${databaseName}`
-    -var databaseTypeModel = `${tbl}.${databaseType}`
-    -var javaNameModel = `${tbl}.${javaName}`
-    -var javaTypeModel = `${tbl}.${javaType}`
+        list-editable-item-edit
+            .pc-form-grid-row
+                .pc-form-grid-col-30(divider='/')
+                    +sane-ignite-form-field-text({
+                        label: 'DB name:',
+                        model: '$item.databaseFieldName',
+                        name: '"databaseFieldName"',
+                        required: true,
+                        placeholder: 'Enter DB name'
+                    })(
+                        ng-model-options='{allowInvalid: true}'
+                        ignite-auto-focus
+                        ignite-unique=items
+                        ignite-unique-property='databaseFieldName'
+                    )
+                        +unique-feedback(_, 'DB name should be unique')
+                .pc-form-grid-col-30
+                    +dropdown-required('DB type:', '$item.databaseFieldType', '"databaseFieldType"', true, true, 'Choose DB type', 'supportedJdbcTypes')
+                .pc-form-grid-col-30(divider='/')
+                    +sane-ignite-form-field-text({
+                        label: 'Java name:',
+                        model: '$item.javaFieldName',
+                        name: '"javaFieldName"',
+                        required: true,
+                        placeholder: 'Enter Java name'
+                    })(
+                        ng-model-options='{allowInvalid: true}'
+                        ignite-unique=items
+                        ignite-unique-property='javaFieldName'
+                    )
+                        +unique-feedback(_, 'Java name should be unique')
+                .pc-form-grid-col-30
+                    +dropdown-required('Java type:', '$item.javaFieldType', '"javaFieldType"', true, true, 'Choose Java type', 'supportedJavaTypes')
 
-    -var databaseNameId = `${databaseName}${focusId}`
-    -var databaseTypeId = `${databaseType}${focusId}`
-    -var javaNameId = `${javaName}${focusId}`
-    -var javaTypeId = `${javaType}${focusId}`
+        list-editable-no-items
+            list-editable-add-item-button(
+                add-item=`$editLast((${items} = ${items} || []).push({}))`
+                label-single=itemName
+                label-multiple=itemsName
+            )
 
-    .col-xs-3.col-sm-3.col-md-3
-        .fieldSep /
-        .input-tip
-            input.form-control(id=databaseNameId ignite-on-enter-focus-move=databaseTypeId type='text' ng-model=databaseNameModel placeholder='DB name' ignite-on-enter=`${javaNameModel} = ${javaNameModel} ? ${javaNameModel} : ${databaseNameModel}` ignite-on-escape='tableReset(false)')
-    .col-xs-3.col-sm-3.col-md-3
-        .fieldSep /
-        .input-tip
-            button.select-toggle.form-control(id=databaseTypeId ignite-on-enter-focus-move=javaNameId ng-model=databaseTypeModel data-placeholder='DB type' ng-class=`{placeholder: !${databaseTypeModel}}` bs-select bs-options='item.value as item.label for item in {{supportedJdbcTypes}}' ignite-on-escape='tableReset(false)' tabindex='0')
-    .col-xs-3.col-sm-3.col-md-3
-        .fieldSep /
-        .input-tip
-            input.form-control(id=javaNameId ignite-on-enter-focus-move=javaTypeId type='text' ng-model=javaNameModel placeholder='Java name' ignite-on-escape='tableReset(false)')
-    .col-xs-3.col-sm-3.col-md-3
-        -var btnVisible = `tableDbFieldSaveVisible(${tbl}, ${index})`
-        -var btnSave = `tableDbFieldSave(${tbl}, ${index})`
-        -var btnVisibleAndSave = `${btnVisible} && ${btnSave}`
-
-        +btn-save(btnVisible, btnSave)
-        .input-tip
-            button.select-toggle.form-control(id=javaTypeId ng-model=javaTypeModel data-placeholder='Java type' ng-class=`{placeholder: !${javaTypeModel}}` bs-select bs-options='item.value as item.label for item in {{supportedJavaTypes}}' ignite-on-enter=btnVisibleAndSave ignite-on-escape='tableReset(false)' tabindex='0')
-
-.panel.panel-default(ng-form=form novalidate)
-    .panel-heading(bs-collapse-toggle='' ng-click=`ui.loadPanel('${form}')`)
+.pca-panel.pca-panel-default(ng-form=form novalidate)
+    .pca-panel-heading(bs-collapse-toggle='' ng-click=`ui.loadPanel('${form}')`)
         ignite-form-panel-chevron
-        label(id='store-title') Domain model for cache store
-        ignite-form-field-tooltip.tipLabel
-            | Domain model properties for binding database with cache via POJO cache store#[br]
-            | #[a(href="https://apacheignite.readme.io/docs/3rd-party-store" target="_blank") More info]
-        ignite-form-revert
-    .panel-collapse(role='tabpanel' bs-collapse-target id=`${form}`)
-        .panel-body(ng-if=`ui.isPanelLoaded('${form}')`)
-            .col-sm-6
-                .settings-row
+        .pca-panel-heading-title(id='store-title') Domain model for cache store
+        .pca-panel-heading-description
+            | Domain model properties for binding database with cache via POJO cache store. 
+            a.link-success(href="https://apacheignite.readme.io/docs/3rd-party-store" 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-30
                     +text('Database schema:', model + '.databaseSchema', '"databaseSchema"', 'false', 'Input DB schema name', 'Schema name in database')
-                .settings-row
+                .pc-form-grid-col-30
                     +text('Database table:', model + '.databaseTable', '"databaseTable"', 'false', 'Input DB table name', 'Table name in database')
-                .settings-row(ng-init='keysTbl={type: "table-db-fields", model: "keyFields", focusId: "KeyField", ui: "table-db-fields"}')
-                    +ignite-form-group(ng-form=keyFieldsForm ng-model=keyFields)
-                        ignite-form-field-label(id='keyFields')
-                            | Key fields
-                        ignite-form-group-tooltip
-                            | Collection of key fields descriptions for CacheJdbcPojoStore
-                        ignite-form-group-add(ng-click='tableNewItem(keysTbl)')
-                            | Add key field
-                        .group-content-empty(ng-show=`!((${keyFields} && ${keyFields}.length > 0) || tableNewItemActive(keysTbl))`) Not defined
-                        .group-content(ng-show=`(${keyFields} && ${keyFields}.length > 0) || tableNewItemActive(keysTbl)`)
-                            table.links-edit(st-table=keyFields)
-                                tbody
-                                    tr(ng-repeat=`item in ${keyFields} track by $index`)
-                                        td
-                                            div(ng-hide='tableEditing(keysTbl, $index)')
-                                                a.labelFormField(ng-click='tableStartEdit(backupItem, keysTbl, $index)') {{$index + 1}}) {{item.databaseFieldName}} / {{item.databaseFieldType}} / {{item.javaFieldName}} / {{item.javaFieldType}}
-                                                +btn-remove('tableRemove(backupItem, keysTbl, $index)', '"Remove key field"')
-                                            div(ng-if='tableEditing(keysTbl, $index)')
-                                                +table-db-field-edit('keysTbl', 'cur', '{{::keysTbl.focusId + $index}}', '$index')
-                                tfoot(ng-show='tableNewItemActive(keysTbl)')
-                                    tr
-                                        td
-                                            +table-db-field-edit('keysTbl', 'new', 'KeyField', '-1')
-                .settings-row(ng-init='valuesTbl={type: "table-db-fields", model: "valueFields", focusId: "ValueField", ui: "table-db-fields"}')
-                    +ignite-form-group(ng-form=valueFieldsForm ng-model=valueFields)
-                        ignite-form-field-label(id='valueFields')
-                            | Value fields
-                        ignite-form-group-tooltip
-                            | Collection of value fields descriptions for CacheJdbcPojoStore
-                        ignite-form-group-add(ng-click='tableNewItem(valuesTbl)')
-                            | Add value field
-                        .group-content-empty(ng-show=`!((${valueFields} && ${valueFields}.length > 0) || tableNewItemActive(valuesTbl))`) Not defined
-                        .group-content(ng-show=`(${valueFields} && ${valueFields}.length > 0) || tableNewItemActive(valuesTbl)`)
-                            table.links-edit(st-table=valueFields)
-                                tbody
-                                    tr(ng-repeat=`item in ${valueFields} track by $index`)
-                                        td
-                                            div(ng-hide='tableEditing(valuesTbl, $index)')
-                                                a.labelFormField(ng-click='tableStartEdit(backupItem, valuesTbl, $index)') {{$index + 1}}) {{item.databaseFieldName}} / {{item.databaseFieldType}} / {{item.javaFieldName}} / {{item.javaFieldType}}
-                                                +btn-remove('tableRemove(backupItem, valuesTbl, $index)', '"Remove key field"')
-                                            div(ng-if='tableEditing(valuesTbl, $index)')
-                                                +table-db-field-edit('valuesTbl', 'cur', '{{::valuesTbl.focusId + $index}}', '$index')
-                                tfoot(ng-show='tableNewItemActive(valuesTbl)')
-                                    tr
-                                        td
-                                            +table-db-field-edit('valuesTbl', 'new', 'ValueField', '-1')
-            .col-sm-6
+                .pc-form-grid-col-60
+                    .ignite-form-field
+                        +ignite-form-field__label('Key fields:', '"keyFields"')
+                            +tooltip(`Collection of key fields descriptions for CacheJdbcPojoStore`)
+                        .ignite-form-field__control
+                            +list-db-field-edit({
+                                items: keyFields,
+                                itemName: 'key field',
+                                itemsName: 'key fields'
+                            })(name='keyFields')
+                        .ignite-form-field__errors(
+                            ng-messages=`store.keyFields.$error`
+                            ng-show=`store.keyFields.$invalid`
+                        )
+                            +form-field-feedback(_, 'dbFieldUnique', 'Each key field DB name and Java name should be unique')
+
+                .pc-form-grid-col-60
+                    .ignite-form-field
+                        +ignite-form-field__label('Value fields:', '"valueFields"')
+                            +tooltip(`Collection of value fields descriptions for CacheJdbcPojoStore`)
+                        .ignite-form-field__control
+                            +list-db-field-edit({
+                                items: valueFields,
+                                itemName: 'value field',
+                                itemsName: 'value fields'
+                            })(name='valueFields')
+                        .ignite-form-field__errors(
+                            ng-messages=`store.valueFields.$error`
+                            ng-show=`store.valueFields.$invalid`
+                        )
+                            +form-field-feedback(_, 'dbFieldUnique', 'Each value field DB name and Java name should be unique')
+
+            .pca-form-column-6
                 +preview-xml-java(model, 'domainStore')
 

http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/frontend/app/modules/states/configuration/igfs/dual.pug
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/modules/states/configuration/igfs/dual.pug b/modules/web-console/frontend/app/modules/states/configuration/igfs/dual.pug
index 9b49be3..b05caab 100644
--- a/modules/web-console/frontend/app/modules/states/configuration/igfs/dual.pug
+++ b/modules/web-console/frontend/app/modules/states/configuration/igfs/dual.pug
@@ -19,17 +19,16 @@ include /app/helpers/jade/mixins
 -var form = 'dualMode'
 -var model = 'backupItem'
 
-.panel.panel-default(ng-show='$ctrl.available(["1.0.0", "2.0.0"])' ng-form=form novalidate)
-    .panel-heading(bs-collapse-toggle='' ng-click=`ui.loadPanel('${form}')`)
+.pca-panel.pca-panel-default(ng-show='$ctrl.available(["1.0.0", "2.0.0"])' ng-form=form novalidate)
+    .pca-panel-heading(bs-collapse-toggle='' ng-click=`ui.loadPanel('${form}')`)
         ignite-form-panel-chevron
-        label Dual mode
-        ignite-form-field-tooltip.tipLabel
-            | IGFS supports dual-mode that allows it to work as either a standalone file system in Hadoop cluster, or work in tandem with HDFS, providing a primary caching layer for the secondary HDFS#[br]
-            | As a caching layer it provides highly configurable read-through and write-through behaviour
-        ignite-form-revert
-    .panel-collapse(role='tabpanel' bs-collapse-target id=`${form}`)
-        .panel-body(ng-if=`$ctrl.available(["1.0.0", "2.0.0"]) && ui.isPanelLoaded('${form}')`)
-            .col-sm-6
+        .pca-panel-heading-title Dual mode
+        .pca-panel-heading-description
+            | IGFS supports dual-mode that allows it to work as either a standalone file system in Hadoop cluster, or work in tandem with HDFS, providing a primary caching layer for the secondary HDFS.
+            | As a caching layer it provides highly configurable read-through and write-through behaviour.
+    .pca-panel-collapse(role='tabpanel' bs-collapse-target id=`${form}`)
+        .pca-panel-body.pca-form-row(ng-if=`$ctrl.available(["1.0.0", "2.0.0"]) && ui.isPanelLoaded('${form}')`)
+            .pca-form-column-6
                 .settings-row
                     +number('Maximum pending puts size:', `${model}.dualModeMaxPendingPutsSize`, '"dualModeMaxPendingPutsSize"', 'true', '0', 'Number.MIN_SAFE_INTEGER',
                         'Maximum amount of pending data read from the secondary file system and waiting to be written to data cache<br/>\
@@ -38,5 +37,5 @@ include /app/helpers/jade/mixins
                     +java-class('Put executor service:', `${model}.dualModePutExecutorService`, '"dualModePutExecutorService"', 'true', 'false', 'DUAL mode put operation executor service')
                 .settings-row
                     +checkbox('Put executor service shutdown', `${model}.dualModePutExecutorServiceShutdown`, '"dualModePutExecutorServiceShutdown"', 'DUAL mode put operation executor service shutdown flag')
-            .col-sm-6
+            .pca-form-column-6
                 +preview-xml-java(model, 'igfsDualMode')

http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/frontend/app/modules/states/configuration/igfs/fragmentizer.pug
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/modules/states/configuration/igfs/fragmentizer.pug b/modules/web-console/frontend/app/modules/states/configuration/igfs/fragmentizer.pug
index b112697..7d5052e 100644
--- a/modules/web-console/frontend/app/modules/states/configuration/igfs/fragmentizer.pug
+++ b/modules/web-console/frontend/app/modules/states/configuration/igfs/fragmentizer.pug
@@ -19,25 +19,22 @@ include /app/helpers/jade/mixins
 -var form = 'fragmentizer'
 -var model = 'backupItem'
 
-.panel.panel-default(ng-form=form novalidate)
-    .panel-heading(bs-collapse-toggle='' ng-click=`ui.loadPanel('${form}')`)
+.pca-panel.pca-panel-default(ng-form=form novalidate)
+    .pca-panel-heading(bs-collapse-toggle='' ng-click=`ui.loadPanel('${form}')`)
         ignite-form-panel-chevron
-        label Fragmentizer
-        ignite-form-field-tooltip.tipLabel
-            | Fragmentizer settings
-        ignite-form-revert
-    .panel-collapse(role='tabpanel' bs-collapse-target id=`${form}`)
-        .panel-body(ng-if=`ui.isPanelLoaded('${form}')`)
-            .col-sm-6
+        .pca-panel-heading-title Fragmentizer
+    .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
                 -var enabled = `${model}.fragmentizerEnabled`
 
-                .settings-row
+                .pc-form-grid-col-60
                     +checkbox('Enabled', enabled, '"fragmentizerEnabled"', 'Fragmentizer enabled flag')
-                .settings-row
+                .pc-form-grid-col-30
                     +number('Concurrent files:', `${model}.fragmentizerConcurrentFiles`, '"fragmentizerConcurrentFiles"', enabled, '0', '0', 'Number of files to process concurrently by fragmentizer')
-                .settings-row
+                .pc-form-grid-col-30
                     +number('Throttling block length:', `${model}.fragmentizerThrottlingBlockLength`, '"fragmentizerThrottlingBlockLength"', enabled, '16777216', '1', 'Length of file chunk to transmit before throttling is delayed')
-                .settings-row
+                .pc-form-grid-col-60
                     +number('Throttling delay:', `${model}.fragmentizerThrottlingDelay`, '"fragmentizerThrottlingDelay"', enabled, '200', '0', 'Delay in milliseconds for which fragmentizer is paused')
-            .col-sm-6
+            .pca-form-column-6
                 +preview-xml-java(model, 'igfsFragmentizer')

http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/frontend/app/modules/states/configuration/igfs/general.pug
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/modules/states/configuration/igfs/general.pug b/modules/web-console/frontend/app/modules/states/configuration/igfs/general.pug
index e002d36..9f65f41 100644
--- a/modules/web-console/frontend/app/modules/states/configuration/igfs/general.pug
+++ b/modules/web-console/frontend/app/modules/states/configuration/igfs/general.pug
@@ -19,39 +19,57 @@ include /app/helpers/jade/mixins
 -var form = 'general'
 -var model = 'backupItem'
 
-.panel.panel-default(ng-form=form novalidate)
-    .panel-heading(bs-collapse-toggle)
+.pca-panel(ng-form=form novalidate)
+    .pca-panel-heading(bs-collapse-toggle)
         ignite-form-panel-chevron
-        label General
-        ignite-form-field-tooltip.tipLabel
-            | General IGFS configuration#[br]
-            | #[a(href="https://apacheignite-fs.readme.io/docs/in-memory-file-system" target="_blank") More info]
-        ignite-form-revert
-    .panel-collapse(role='tabpanel' bs-collapse-target id='general')
-        .panel-body
-            .col-sm-6
-                .settings-row
-                    +text('Name:', `${model}.name`, '"igfsName"', 'true', 'Input name', 'IGFS name')
-                .settings-row
-                    +clusters(model, 'Associate clusters with the current IGFS')
-                .settings-row
-                    +dropdown('IGFS mode:', `${model}.defaultMode`, '"defaultMode"', 'true', 'DUAL_ASYNC',
-                    '[\
-                        {value: "PRIMARY", label: "PRIMARY"},\
-                        {value: "PROXY", label: "PROXY"},\
-                        {value: "DUAL_SYNC", label: "DUAL_SYNC"},\
-                        {value: "DUAL_ASYNC", label: "DUAL_ASYNC"}\
-                    ]',
-                    'Mode to specify how IGFS interacts with Hadoop file system\
-                    <ul>\
-                        <li>PRIMARY - in this mode IGFS will not delegate to secondary Hadoop file system and will cache all the files in memory only</li>\
-                        <li>PROXY - in this mode IGFS will not cache any files in memory and will only pass them through to secondary file system</li>\
-                        <li>DUAL_SYNC - in this mode IGFS will cache files locally and also <b>synchronously</b> write them through to secondary file system</li>\
-                        <li>DUAL_ASYNC - in this mode IGFS will cache files locally and also <b> asynchronously </b> write them through to secondary file system</li>\
-                    </ul>')
-                .settings-row
-                    +number('Group size:', `${model}.affinnityGroupSize`, '"affinnityGroupSize"', 'true', '512', '1',
-                        'Size of the group in blocks<br/>\
-                        Required for construction of affinity mapper in IGFS data cache')
-            .col-sm-6
+        .pca-panel-heading-title General
+        .pca-panel-heading-description
+            | General IGFS configuration. 
+            a.link-success(href="https://apacheignite-fs.readme.io/docs/in-memory-file-system" target="_blank") More info
+    .pca-panel-collapse(role='tabpanel' bs-collapse-target id='general')
+        .pca-panel-body.pca-form-row
+            .pca-form-column-6.pc-form-grid-row
+                .pc-form-grid-col-60
+                    +sane-ignite-form-field-text({
+                        label: 'Name:',
+                        model: `${model}.name`,
+                        name: '"igfsName"',
+                        placeholder: 'Input name',
+                        required: true
+                    })(
+                        ignite-unique='$ctrl.igfss'
+                        ignite-unique-property='name'
+                        ignite-unique-skip=`["_id", ${model}]`
+                    )
+                        +unique-feedback(`${model}.name`, 'IGFS name should be unique.')
+                .pc-form-grid-col-30
+                    +sane-ignite-form-field-dropdown({
+                        label: 'IGFS mode:',
+                        model: `${model}.defaultMode`,
+                        name: '"defaultMode"',
+                        placeholder: '{{::$ctrl.IGFSs.defaultMode.default}}',
+                        options: '{{::$ctrl.IGFSs.defaultMode.values}}',
+                        tip: `
+                        Mode to specify how IGFS interacts with Hadoop file system
+                        <ul>
+                            <li>PRIMARY - in this mode IGFS will not delegate to secondary Hadoop file system and will cache all the files in memory only</li>
+                            <li>PROXY - in this mode IGFS will not cache any files in memory and will only pass them through to secondary file system</li>
+                            <li>DUAL_SYNC - in this mode IGFS will cache files locally and also <b>synchronously</b> write them through to secondary file system</li>
+                            <li>DUAL_ASYNC - in this mode IGFS will cache files locally and also <b> asynchronously </b> write them through to secondary file system</li>
+                        </ul>
+                        `
+                    })
+                .pc-form-grid-col-30
+                    +sane-ignite-form-field-number({
+                        label: 'Group size:',
+                        model: `${model}.affinnityGroupSize`,
+                        name: '"affinnityGroupSize"',
+                        placeholder: '{{::$ctrl.IGFSs.affinnityGroupSize.default}}',
+                        min: '{{::$ctrl.IGFSs.affinnityGroupSize.min}}',
+                        tip: `
+                            Size of the group in blocks<br/>
+                            Required for construction of affinity mapper in IGFS data cache
+                        `
+                    })
+            .pca-form-column-6
                 +preview-xml-java(model, 'igfsGeneral')

http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/frontend/app/modules/states/configuration/igfs/ipc.pug
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/modules/states/configuration/igfs/ipc.pug b/modules/web-console/frontend/app/modules/states/configuration/igfs/ipc.pug
index 7c8c056..e123b3c 100644
--- a/modules/web-console/frontend/app/modules/states/configuration/igfs/ipc.pug
+++ b/modules/web-console/frontend/app/modules/states/configuration/igfs/ipc.pug
@@ -19,22 +19,20 @@ include /app/helpers/jade/mixins
 -var form = 'ipc'
 -var model = 'backupItem'
 
-.panel.panel-default(ng-form=form novalidate)
-    .panel-heading(bs-collapse-toggle='' ng-click=`ui.loadPanel('${form}')`)
+.pca-panel.pca-panel-default(ng-form=form novalidate)
+    .pca-panel-heading(bs-collapse-toggle='' ng-click=`ui.loadPanel('${form}')`)
         ignite-form-panel-chevron
-        label IPC
-        ignite-form-field-tooltip.tipLabel
-            | IGFS Inter-process communication properties
-        ignite-form-revert
-    .panel-collapse(role='tabpanel' bs-collapse-target id=`${form}`)
-        .panel-body(ng-if=`ui.isPanelLoaded('${form}')`)
-            .col-sm-6
+        .pca-panel-heading-title IPC
+        .pca-panel-heading-description IGFS Inter-process communication properties.
+    .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
                 -var ipcEndpointConfiguration = `${model}.ipcEndpointConfiguration`
                 -var enabled = `${model}.ipcEndpointEnabled`
 
-                .settings-row
+                .pc-form-grid-col-60
                     +checkbox('Enabled', enabled, '"ipcEndpointEnabled"', 'IPC endpoint enabled flag')
-                .settings-row
+                .pc-form-grid-col-60
                     +dropdown('Type:', `${ipcEndpointConfiguration}.type`, '"ipcEndpointConfigurationType"', enabled, 'TCP',
                         '[\
                             {value: "SHMEM", label: "SHMEM"},\
@@ -45,16 +43,16 @@ include /app/helpers/jade/mixins
                             <li>SHMEM - shared memory endpoint</li>\
                             <li>TCP - TCP endpoint</li>\
                         </ul>')
-                .settings-row
+                .pc-form-grid-col-30
                     +text-ip-address('Host:', `${ipcEndpointConfiguration}.host`, '"ipcEndpointConfigurationHost"', enabled, '127.0.0.1', 'Host endpoint is bound to')
-                .settings-row
+                .pc-form-grid-col-30
                     +number-min-max('Port:', `${ipcEndpointConfiguration}.port`, '"ipcEndpointConfigurationPort"', enabled, '10500', '1', '65535', 'Port endpoint is bound to')
-                .settings-row
+                .pc-form-grid-col-30
                     +number('Memory size:', `${ipcEndpointConfiguration}.memorySize`, '"ipcEndpointConfigurationMemorySize"', enabled, '262144', '1', 'Shared memory size in bytes allocated for endpoint communication')
-                .settings-row
-                    +text-enabled('Token directory:', `${ipcEndpointConfiguration}.tokenDirectoryPath`, '"ipcEndpointConfigurationTokenDirectoryPath"', enabled, 'false', 'ipc/shmem', 'Directory where shared memory tokens are stored')
-                .settings-row
+                .pc-form-grid-col-30
                     +number('Thread count:', `${ipcEndpointConfiguration}.threadCount`, '"ipcEndpointConfigurationThreadCount"', enabled, 'availableProcessors', '1',
                         'Number of threads used by this endpoint to process incoming requests')
-            .col-sm-6
+                .pc-form-grid-col-60
+                    +text-enabled('Token directory:', `${ipcEndpointConfiguration}.tokenDirectoryPath`, '"ipcEndpointConfigurationTokenDirectoryPath"', enabled, 'false', 'ipc/shmem', 'Directory where shared memory tokens are stored')
+            .pca-form-column-6
                 +preview-xml-java(model, 'igfsIPC')

http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/frontend/app/modules/states/configuration/igfs/misc.pug
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/modules/states/configuration/igfs/misc.pug b/modules/web-console/frontend/app/modules/states/configuration/igfs/misc.pug
index 72d0649..63e5e46 100644
--- a/modules/web-console/frontend/app/modules/states/configuration/igfs/misc.pug
+++ b/modules/web-console/frontend/app/modules/states/configuration/igfs/misc.pug
@@ -18,106 +18,96 @@ include /app/helpers/jade/mixins
 
 -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(false)')
-    .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(false)')
-
-.panel.panel-default(ng-form=form novalidate)
-    .panel-heading(bs-collapse-toggle='' ng-click=`ui.loadPanel('${form}')`)
+.pca-panel.pca-panel-default(ng-form=form novalidate)
+    .pca-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
+        .pca-panel-heading-title Miscellaneous
+        .pca-panel-heading-description Various miscellaneous IGFS 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-60
                     +number('Block size:', `${model}.blockSize`, '"blockSize"', 'true', '65536', '0', 'File data block size in bytes')
 
                 //- Since ignite 2.0
-                .settings-row(ng-if='$ctrl.available("2.0.0")')
+                .pc-form-grid-col-60(ng-if='$ctrl.available("2.0.0")')
                     +number('Buffer size:', `${model}.streamBufferSize`, '"streamBufferSize"', 'true', '65536', '0', 'Read/write buffer size for IGFS stream operations in bytes')
 
                 //- Removed in ignite 2.0
-                div(ng-if='$ctrl.available(["1.0.0", "2.0.0"])')
-                    .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')
+                .pc-form-grid-col-60(ng-if-start='$ctrl.available(["1.0.0", "2.0.0"])')
+                    +number('Stream buffer size:', `${model}.streamBufferSize`, '"streamBufferSize"', 'true', '65536', '0', 'Read/write buffer size for IGFS stream operations in bytes')
+                .pc-form-grid-col-60(ng-if-end)
+                    +number('Maximum space size:', `${model}.maxSpaceSize`, '"maxSpaceSize"', 'true', '0', '0', 'Maximum space available for data cache to store file system entries')
 
-                .settings-row
+                .pc-form-grid-col-30
                     +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
+                .pc-form-grid-col-30
                     +number-min-max('Management port:', `${model}.managementPort`, '"managementPort"', 'true', '11400', '0', '65535', 'Port number for management endpoint')
-                .settings-row
+                .pc-form-grid-col-30
                     +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
+                .pc-form-grid-col-30
                     +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
+                .pc-form-grid-col-60
                     +number('Prefetch blocks:', `${model}.prefetchBlocks`, '"prefetchBlocks"', 'true', '0', '0', 'Number of pre-fetched blocks if specific file chunk is requested')
-                .settings-row
+                .pc-form-grid-col-60
                     +number('Sequential reads before prefetch:', `${model}.sequentialReadsBeforePrefetch`, '"sequentialReadsBeforePrefetch"', 'true', '0', '0', 'Amount of sequential block reads before prefetch is triggered')
 
                 //- Removed in ignite 2.0
-                .settings-row(ng-if='$ctrl.available(["1.0.0", "2.0.0"])')
+                .pc-form-grid-col-60(ng-if='$ctrl.available(["1.0.0", "2.0.0"])')
                     +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
+                .pc-form-grid-col-60
                     +checkbox('Colocate metadata', `${model}.colocateMetadata`, '"colocateMetadata"', 'Whether to co-locate metadata on a single node')
-                .settings-row
+                .pc-form-grid-col-60
                     +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')
 
                 //- Since ignite 2.0
-                .settings-row(ng-if='$ctrl.available("2.0.0")')
+                .pc-form-grid-col-60(ng-if='$ctrl.available("2.0.0")')
                     +checkbox('Update file length on flush', model + '.updateFileLengthOnFlush', '"updateFileLengthOnFlush"', 'Update file length on flush flag')
 
-                .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} track by $index`)
-                                        td.col-sm-12(ng-hide='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
+                .pc-form-grid-col-60
+                    mixin igfs-misc-path-modes
+                        .ignite-form-field
+                            +ignite-form-field__label('Path modes:', '"pathModes"')
+                                +tooltip(`Map of path prefixes to IGFS modes used for them`)
+                            .ignite-form-field__control
+                                -let items = pathModes
+
+                                list-editable(ng-model=items)
+                                    list-editable-item-view
+                                        | {{ $item.path + " [" + $item.mode + "]"}}
+
+                                    list-editable-item-edit
+                                        - form = '$parent.form'
+
+                                        .pc-form-grid-row
+                                            .pc-form-grid-col-30
+                                                +ignite-form-field-text('Path:', '$item.path', '"path"', false, true, 'Enter path')(ignite-auto-focus)
+                                            .pc-form-grid-col-30
+                                                +sane-ignite-form-field-dropdown({
+                                                    label: 'Mode:',
+                                                    model: `$item.mode`,
+                                                    name: '"mode"',
+                                                    required: true,
+                                                    placeholder: 'Choose igfs mode',
+                                                    options: '{{::$ctrl.IGFSs.defaultMode.values}}'
+                                                })(
+                                                    ng-model-options='{allowInvalid: true}'
+                                                )
+
+                                    list-editable-no-items
+                                        list-editable-add-item-button(
+                                            add-item=`$editLast((${items} = ${items} || []).push({}))`
+                                            label-single='path mode'
+                                            label-multiple='path modes'
+                                        )
+
+                    +igfs-misc-path-modes
+
+            .pca-form-column-6
                 +preview-xml-java(model, 'igfsMisc')

http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/frontend/app/modules/states/configuration/igfs/secondary.pug
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/modules/states/configuration/igfs/secondary.pug b/modules/web-console/frontend/app/modules/states/configuration/igfs/secondary.pug
index 797e877..da34596 100644
--- a/modules/web-console/frontend/app/modules/states/configuration/igfs/secondary.pug
+++ b/modules/web-console/frontend/app/modules/states/configuration/igfs/secondary.pug
@@ -19,27 +19,40 @@ include /app/helpers/jade/mixins
 -var form = 'secondaryFileSystem'
 -var model = 'backupItem'
 
-.panel.panel-default(ng-form=form novalidate)
-    .panel-heading(bs-collapse-toggle='' ng-click=`ui.loadPanel('${form}')`)
+.pca-panel.pca-panel-default(ng-form=form novalidate)
+    .pca-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#[br]
-            | #[a(href="https://apacheignite-fs.readme.io/docs/secondary-file-system" target="_blank") More info]
-        ignite-form-revert
-    .panel-collapse(role='tabpanel' bs-collapse-target id=`${form}`)
-        .panel-body(ng-if=`ui.isPanelLoaded('${form}')`)
-            .col-sm-6
+        .pca-panel-heading-title Secondary file system
+        .pca-panel-heading-description
+            | Secondary file system is provided for pass-through, write-through, and read-through purposes. 
+            a.link-success(href="https://apacheignite-fs.readme.io/docs/secondary-file-system" 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
                 -var enabled = `${model}.secondaryFileSystemEnabled`
                 -var secondaryFileSystem = `${model}.secondaryFileSystem`
 
-                .settings-row
-                    +checkbox('Enabled', enabled, '"secondaryFileSystemEnabled"', 'Secondary file system enabled flag')
-                .settings-row
+                .pc-form-grid-col-60
+                    +sane-form-field-checkbox({
+                        label: 'Enabled',
+                        name: '"secondaryFileSystemEnabled"',
+                        model: enabled
+                    })(
+                        ng-model-options='{allowInvalid: true}'
+                        ui-validate=`{
+                            requiredWhenIGFSProxyMode: '$ctrl.IGFSs.secondaryFileSystemEnabled.requiredWhenIGFSProxyMode(${model})',
+                            requiredWhenPathModeProxyMode: '$ctrl.IGFSs.secondaryFileSystemEnabled.requiredWhenPathModeProxyMode(${model})'
+                        }`
+                        ui-validate-watch-collection=`"[${model}.defaultMode, ${model}.pathModes]"`
+                        ui-validate-watch-object-equality='true'
+                    )
+                        +form-field-feedback(null, 'requiredWhenIGFSProxyMode', 'Secondary file system should be configured for "PROXY" IGFS mode')
+                        +form-field-feedback(null, 'requiredWhenPathModeProxyMode', 'Secondary file system should be configured for "PROXY" path mode')
+                .pc-form-grid-col-60
                     +text-enabled('URI:', `${secondaryFileSystem}.uri`, '"hadoopURI"', enabled, 'false', 'hdfs://[namenodehost]:[port]/[path]', 'URI of file system')
-                .settings-row
+                .pc-form-grid-col-60
                     +text-enabled('Config path:', `${secondaryFileSystem}.cfgPath`, '"cfgPath"', enabled, 'false', 'Path to additional config', 'Additional path to Hadoop configuration')
-                .settings-row
+                .pc-form-grid-col-60
                     +text-enabled('User name:', `${secondaryFileSystem}.userName`, '"userName"', enabled, 'false', 'Input user name', 'User name')
-            .col-sm-6
+            .pca-form-column-6
                 +preview-xml-java(model, 'igfsSecondFS')

http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/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
deleted file mode 100644
index f8094af..0000000
--- a/modules/web-console/frontend/app/modules/states/configuration/summary/summary-tabs.directive.js
+++ /dev/null
@@ -1,50 +0,0 @@
-/*
- * 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/7ee1683e/modules/web-console/frontend/app/modules/states/configuration/summary/summary-zipper.service.js
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/modules/states/configuration/summary/summary-zipper.service.js b/modules/web-console/frontend/app/modules/states/configuration/summary/summary-zipper.service.js
index 47ce9ad..2f6b9e3 100644
--- a/modules/web-console/frontend/app/modules/states/configuration/summary/summary-zipper.service.js
+++ b/modules/web-console/frontend/app/modules/states/configuration/summary/summary-zipper.service.js
@@ -26,10 +26,12 @@ export default ['$q', function($q) {
 
         worker.onmessage = (e) => {
             defer.resolve(e.data);
+            worker.terminate();
         };
 
         worker.onerror = (err) => {
             defer.reject(err);
+            worker.terminate();
         };
 
         return defer.promise;