http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/frontend/app/components/page-configure/store/effects.js ---------------------------------------------------------------------- diff --git a/modules/web-console/frontend/app/components/page-configure/store/effects.js b/modules/web-console/frontend/app/components/page-configure/store/effects.js new file mode 100644 index 0000000..3ab216a --- /dev/null +++ b/modules/web-console/frontend/app/components/page-configure/store/effects.js @@ -0,0 +1,664 @@ +/* + * 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 {Observable} from 'rxjs/Observable'; +import 'rxjs/add/operator/ignoreElements'; +import 'rxjs/add/operator/let'; +import 'rxjs/add/operator/zip'; +import {merge} from 'rxjs/observable/merge'; +import {empty} from 'rxjs/observable/empty'; +import {of} from 'rxjs/observable/of'; +import {fromPromise} from 'rxjs/observable/fromPromise'; +import {uniqueName} from 'app/utils/uniqueName'; +import uniq from 'lodash/uniq'; + +import { + clustersActionTypes, + cachesActionTypes, + shortClustersActionTypes, + shortCachesActionTypes, + shortModelsActionTypes, + shortIGFSsActionTypes, + modelsActionTypes, + igfssActionTypes +} from './../reducer'; + +import { + REMOVE_CLUSTER_ITEMS, + REMOVE_CLUSTER_ITEMS_CONFIRMED, + CONFIRM_CLUSTERS_REMOVAL, + CONFIRM_CLUSTERS_REMOVAL_OK, + COMPLETE_CONFIGURATION, + ADVANCED_SAVE_CLUSTER, + ADVANCED_SAVE_CACHE, + ADVANCED_SAVE_IGFS, + ADVANCED_SAVE_MODEL, + BASIC_SAVE, + BASIC_SAVE_AND_DOWNLOAD, + BASIC_SAVE_OK +} from './actionTypes'; + +import { + removeClusterItemsConfirmed, + advancedSaveCompleteConfiguration, + confirmClustersRemoval, + confirmClustersRemovalOK, + completeConfiguration, + basicSave, + basicSaveOK, + basicSaveErr +} from './actionCreators'; + +import ConfigureState from 'app/components/page-configure/services/ConfigureState'; +import ConfigurationDownload from 'app/components/page-configure/services/ConfigurationDownload'; +import ConfigSelectors from 'app/components/page-configure/store/selectors'; +import Clusters from 'app/services/Clusters'; +import Caches from 'app/services/Caches'; +import Models from 'app/services/Models'; +import IGFSs from 'app/services/IGFSs'; +import {Confirm} from 'app/services/Confirm.service'; + +export const ofType = (type) => (s) => s.filter((a) => a.type === type); + +export default class ConfigEffects { + static $inject = [ + ConfigureState.name, + Caches.name, + IGFSs.name, + Models.name, + ConfigSelectors.name, + Clusters.name, + '$state', + 'IgniteMessages', + 'IgniteConfirm', + Confirm.name, + ConfigurationDownload.name + ]; + /** + * @param {ConfigureState} ConfigureState + * @param {Caches} Caches + * @param {IGFSs} IGFSs + * @param {Models} Models + * @param {ConfigSelectors} ConfigSelectors + * @param {Clusters} Clusters + * @param {object} $state + * @param {object} IgniteMessages + * @param {object} IgniteConfirm + * @param {Confirm} Confirm + * @param {ConfigurationDownload} ConfigurationDownload + */ + constructor(ConfigureState, Caches, IGFSs, Models, ConfigSelectors, Clusters, $state, IgniteMessages, IgniteConfirm, Confirm, ConfigurationDownload) { + this.ConfigureState = ConfigureState; + this.ConfigSelectors = ConfigSelectors; + this.IGFSs = IGFSs; + this.Models = Models; + this.Caches = Caches; + this.Clusters = Clusters; + this.$state = $state; + this.IgniteMessages = IgniteMessages; + this.IgniteConfirm = IgniteConfirm; + this.Confirm = Confirm; + this.configurationDownload = ConfigurationDownload; + + this.loadConfigurationEffect$ = this.ConfigureState.actions$ + .let(ofType('LOAD_COMPLETE_CONFIGURATION')) + .exhaustMap((action) => { + return fromPromise(this.Clusters.getConfiguration(action.clusterID)) + .switchMap(({data}) => of( + completeConfiguration(data), + {type: 'LOAD_COMPLETE_CONFIGURATION_OK', data} + )) + .catch((error) => of({ + type: 'LOAD_COMPLETE_CONFIGURATION_ERR', + error: { + message: `Failed to load cluster configuration: ${error.data}.` + }, + action + })); + }); + + this.storeConfigurationEffect$ = this.ConfigureState.actions$ + .let(ofType(COMPLETE_CONFIGURATION)) + .exhaustMap(({configuration: {cluster, caches, models, igfss}}) => of(...[ + cluster && {type: clustersActionTypes.UPSERT, items: [cluster]}, + caches && caches.length && {type: cachesActionTypes.UPSERT, items: caches}, + models && models.length && {type: modelsActionTypes.UPSERT, items: models}, + igfss && igfss.length && {type: igfssActionTypes.UPSERT, items: igfss} + ].filter((v) => v))); + + this.saveCompleteConfigurationEffect$ = this.ConfigureState.actions$ + .let(ofType('ADVANCED_SAVE_COMPLETE_CONFIGURATION')) + .switchMap((action) => { + const actions = [ + { + type: modelsActionTypes.UPSERT, + items: action.changedItems.models + }, + { + type: shortModelsActionTypes.UPSERT, + items: action.changedItems.models.map((m) => this.Models.toShortModel(m)) + }, + { + type: igfssActionTypes.UPSERT, + items: action.changedItems.igfss + }, + { + type: shortIGFSsActionTypes.UPSERT, + items: action.changedItems.igfss + }, + { + type: cachesActionTypes.UPSERT, + items: action.changedItems.caches + }, + { + type: shortCachesActionTypes.UPSERT, + items: action.changedItems.caches.map(Caches.toShortCache) + }, + { + type: clustersActionTypes.UPSERT, + items: [action.changedItems.cluster] + }, + { + type: shortClustersActionTypes.UPSERT, + items: [Clusters.toShortCluster(action.changedItems.cluster)] + } + ].filter((a) => a.items.length); + + return of(...actions) + .merge( + fromPromise(Clusters.saveAdvanced(action.changedItems)) + .switchMap((res) => { + return of( + {type: 'EDIT_CLUSTER', cluster: action.changedItems.cluster}, + {type: 'ADVANCED_SAVE_COMPLETE_CONFIGURATION_OK', changedItems: action.changedItems} + ); + }) + .catch((res) => { + return of({ + type: 'ADVANCED_SAVE_COMPLETE_CONFIGURATION_ERR', + changedItems: action.changedItems, + action, + error: { + message: `Failed to save cluster "${action.changedItems.cluster.name}": ${res.data}.` + } + }, { + type: 'UNDO_ACTIONS', + actions + }); + }) + ); + }); + + this.addCacheToEditEffect$ = this.ConfigureState.actions$ + .let(ofType('ADD_CACHE_TO_EDIT')) + .switchMap(() => this.ConfigureState.state$.let(this.ConfigSelectors.selectCacheToEdit('new')).take(1)) + .map((cache) => ({type: 'UPSERT_CLUSTER_ITEM', itemType: 'caches', item: cache})); + + this.errorNotificationsEffect$ = this.ConfigureState.actions$ + .filter((a) => a.error) + .do((action) => this.IgniteMessages.showError(action.error)) + .ignoreElements(); + + this.loadUserClustersEffect$ = this.ConfigureState.actions$ + .let(ofType('LOAD_USER_CLUSTERS')) + .exhaustMap((a) => { + return fromPromise(this.Clusters.getClustersOverview()) + .switchMap(({data}) => of( + {type: shortClustersActionTypes.SET, items: data}, + {type: `${a.type}_OK`} + )) + .catch((error) => of({ + type: `${a.type}_ERR`, + error: { + message: `Failed to load clusters: ${error.data}` + }, + action: a + })); + }); + + this.loadAndEditClusterEffect$ = ConfigureState.actions$ + .let(ofType('LOAD_AND_EDIT_CLUSTER')) + .exhaustMap((a) => { + if (a.clusterID === 'new') { + return of( + {type: 'EDIT_CLUSTER', cluster: this.Clusters.getBlankCluster()}, + {type: 'LOAD_AND_EDIT_CLUSTER_OK'} + ); + } + return this.ConfigureState.state$.let(this.ConfigSelectors.selectCluster(a.clusterID)).take(1) + .switchMap((cluster) => { + if (cluster) { + return of( + {type: 'EDIT_CLUSTER', cluster}, + {type: 'LOAD_AND_EDIT_CLUSTER_OK'} + ); + } + return fromPromise(this.Clusters.getCluster(a.clusterID)) + .switchMap(({data}) => of( + {type: clustersActionTypes.UPSERT, items: [data]}, + {type: 'EDIT_CLUSTER', cluster: data}, + {type: 'LOAD_AND_EDIT_CLUSTER_OK'} + )) + .catch((error) => of({ + type: 'LOAD_AND_EDIT_CLUSTER_ERR', + error: { + message: `Failed to load cluster: ${error.data}.` + } + })); + }); + }); + + this.loadCacheEffect$ = this.ConfigureState.actions$ + .let(ofType('LOAD_CACHE')) + .exhaustMap((a) => { + return this.ConfigureState.state$.let(this.ConfigSelectors.selectCache(a.cacheID)).take(1) + .switchMap((cache) => { + if (cache) return of({type: `${a.type}_OK`, cache}); + return fromPromise(this.Caches.getCache(a.cacheID)) + .switchMap(({data}) => of( + {type: 'CACHE', cache: data}, + {type: `${a.type}_OK`, cache: data} + )); + }) + .catch((error) => of({ + type: `${a.type}_ERR`, + error: { + message: `Failed to load cache: ${error.data}.` + } + })); + }); + + this.storeCacheEffect$ = this.ConfigureState.actions$ + .let(ofType('CACHE')) + .map((a) => ({type: cachesActionTypes.UPSERT, items: [a.cache]})); + + this.loadShortCachesEffect$ = ConfigureState.actions$ + .let(ofType('LOAD_SHORT_CACHES')) + .exhaustMap((a) => { + if (!(a.ids || []).length) return of({type: `${a.type}_OK`}); + return this.ConfigureState.state$.let(this.ConfigSelectors.selectShortCaches()).take(1) + .switchMap((items) => { + if (!items.pristine && a.ids && a.ids.every((_id) => items.value.has(_id))) + return of({type: `${a.type}_OK`}); + + return fromPromise(this.Clusters.getClusterCaches(a.clusterID)) + .switchMap(({data}) => of( + {type: shortCachesActionTypes.UPSERT, items: data}, + {type: `${a.type}_OK`} + )); + }) + .catch((error) => of({ + type: `${a.type}_ERR`, + error: { + message: `Failed to load caches: ${error.data}.` + }, + action: a + })); + }); + + this.loadIgfsEffect$ = this.ConfigureState.actions$ + .let(ofType('LOAD_IGFS')) + .exhaustMap((a) => { + return this.ConfigureState.state$.let(this.ConfigSelectors.selectIGFS(a.igfsID)).take(1) + .switchMap((igfs) => { + if (igfs) return of({type: `${a.type}_OK`, igfs}); + return fromPromise(this.IGFSs.getIGFS(a.igfsID)) + .switchMap(({data}) => of( + {type: 'IGFS', igfs: data}, + {type: `${a.type}_OK`, igfs: data} + )); + }) + .catch((error) => of({ + type: `${a.type}_ERR`, + error: { + message: `Failed to load IGFS: ${error.data}.` + } + })); + }); + + this.storeIgfsEffect$ = this.ConfigureState.actions$ + .let(ofType('IGFS')) + .map((a) => ({type: igfssActionTypes.UPSERT, items: [a.igfs]})); + + this.loadShortIgfssEffect$ = ConfigureState.actions$ + .let(ofType('LOAD_SHORT_IGFSS')) + .exhaustMap((a) => { + if (!(a.ids || []).length) { + return of( + {type: shortIGFSsActionTypes.UPSERT, items: []}, + {type: `${a.type}_OK`} + ); + } + return this.ConfigureState.state$.let(this.ConfigSelectors.selectShortIGFSs()).take(1) + .switchMap((items) => { + if (!items.pristine && a.ids && a.ids.every((_id) => items.value.has(_id))) + return of({type: `${a.type}_OK`}); + + return fromPromise(this.Clusters.getClusterIGFSs(a.clusterID)) + .switchMap(({data}) => of( + {type: shortIGFSsActionTypes.UPSERT, items: data}, + {type: `${a.type}_OK`} + )); + }) + .catch((error) => of({ + type: `${a.type}_ERR`, + error: { + message: `Failed to load IGFSs: ${error.data}.` + }, + action: a + })); + }); + + this.loadModelEffect$ = this.ConfigureState.actions$ + .let(ofType('LOAD_MODEL')) + .exhaustMap((a) => { + return this.ConfigureState.state$.let(this.ConfigSelectors.selectModel(a.modelID)).take(1) + .switchMap((model) => { + if (model) return of({type: `${a.type}_OK`, model}); + return fromPromise(this.Models.getModel(a.modelID)) + .switchMap(({data}) => of( + {type: 'MODEL', model: data}, + {type: `${a.type}_OK`, model: data} + )); + }) + .catch((error) => of({ + type: `${a.type}_ERR`, + error: { + message: `Failed to load domain model: ${error.data}.` + } + })); + }); + + this.storeModelEffect$ = this.ConfigureState.actions$ + .let(ofType('MODEL')) + .map((a) => ({type: modelsActionTypes.UPSERT, items: [a.model]})); + + this.loadShortModelsEffect$ = this.ConfigureState.actions$ + .let(ofType('LOAD_SHORT_MODELS')) + .exhaustMap((a) => { + if (!(a.ids || []).length) { + return of( + {type: shortModelsActionTypes.UPSERT, items: []}, + {type: `${a.type}_OK`} + ); + } + return this.ConfigureState.state$.let(this.ConfigSelectors.selectShortModels()).take(1) + .switchMap((items) => { + if (!items.pristine && a.ids && a.ids.every((_id) => items.value.has(_id))) + return of({type: `${a.type}_OK`}); + + return fromPromise(this.Clusters.getClusterModels(a.clusterID)) + .switchMap(({data}) => of( + {type: shortModelsActionTypes.UPSERT, items: data}, + {type: `${a.type}_OK`} + )); + }) + .catch((error) => of({ + type: `${a.type}_ERR`, + error: { + message: `Failed to load domain models: ${error.data}.` + }, + action: a + })); + }); + + this.basicSaveRedirectEffect$ = this.ConfigureState.actions$ + .let(ofType(BASIC_SAVE_OK)) + .do((a) => this.$state.go('base.configuration.edit.basic', {clusterID: a.changedItems.cluster._id}, {location: 'replace', custom: {justIDUpdate: true}})) + .ignoreElements(); + + this.basicDownloadAfterSaveEffect$ = this.ConfigureState.actions$.let(ofType(BASIC_SAVE_AND_DOWNLOAD)) + .zip(this.ConfigureState.actions$.let(ofType(BASIC_SAVE_OK))) + .pluck('1') + .do((a) => this.configurationDownload.downloadClusterConfiguration(a.changedItems.cluster)) + .ignoreElements(); + + this.advancedSaveRedirectEffect$ = this.ConfigureState.actions$ + .let(ofType('ADVANCED_SAVE_COMPLETE_CONFIGURATION_OK')) + .withLatestFrom(this.ConfigureState.actions$.let(ofType('ADVANCED_SAVE_COMPLETE_CONFIGURATION'))) + .pluck('1', 'changedItems') + .map((req) => { + const firstChangedItem = Object.keys(req).filter((k) => k !== 'cluster') + .map((k) => Array.isArray(req[k]) ? [k, req[k][0]] : [k, req[k]]) + .filter((v) => v[1]) + .pop(); + return firstChangedItem ? [...firstChangedItem, req.cluster] : ['cluster', req.cluster, req.cluster]; + }) + .do(([type, value, cluster]) => { + const go = (state, params = {}) => this.$state.go( + state, {...params, clusterID: cluster._id}, {location: 'replace', custom: {justIDUpdate: true}} + ); + switch (type) { + case 'models': { + const state = 'base.configuration.edit.advanced.models.model'; + this.IgniteMessages.showInfo(`Model "${value.valueType}" saved`); + if ( + this.$state.is(state) && this.$state.params.modelID !== value._id + ) return go(state, {modelID: value._id}); + break; + } + case 'caches': { + const state = 'base.configuration.edit.advanced.caches.cache'; + this.IgniteMessages.showInfo(`Cache "${value.name}" saved`); + if ( + this.$state.is(state) && this.$state.params.cacheID !== value._id + ) return go(state, {cacheID: value._id}); + break; + } + case 'igfss': { + const state = 'base.configuration.edit.advanced.igfs.igfs'; + this.IgniteMessages.showInfo(`IGFS "${value.name}" saved`); + if ( + this.$state.is(state) && this.$state.params.igfsID !== value._id + ) return go(state, {igfsID: value._id}); + break; + } + case 'cluster': { + const state = 'base.configuration.edit.advanced.cluster'; + this.IgniteMessages.showInfo(`Cluster "${value.name}" saved`); + if ( + this.$state.is(state) && this.$state.params.clusterID !== value._id + ) return go(state); + break; + } + default: break; + } + }) + .ignoreElements(); + + this.removeClusterItemsEffect$ = this.ConfigureState.actions$ + .let(ofType(REMOVE_CLUSTER_ITEMS)) + .exhaustMap((a) => { + return a.confirm + // TODO: list items to remove in confirmation + ? fromPromise(this.Confirm.confirm('Are you sure want to remove these items?')) + .mapTo(a) + .catch(() => empty()) + : of(a); + }) + .map((a) => removeClusterItemsConfirmed(a.clusterID, a.itemType, a.itemIDs)); + + this.persistRemovedClusterItemsEffect$ = this.ConfigureState.actions$ + .let(ofType(REMOVE_CLUSTER_ITEMS_CONFIRMED)) + .withLatestFrom(this.ConfigureState.actions$.let(ofType(REMOVE_CLUSTER_ITEMS))) + .filter(([a, b]) => { + return a.itemType === b.itemType + && b.save + && JSON.stringify(a.itemIDs) === JSON.stringify(b.itemIDs); + }) + .pluck('0') + .withLatestFrom(this.ConfigureState.state$.pluck('edit')) + .map(([action, edit]) => advancedSaveCompleteConfiguration(edit)); + + this.confirmClustersRemovalEffect$ = this.ConfigureState.actions$ + .let(ofType(CONFIRM_CLUSTERS_REMOVAL)) + .pluck('clusterIDs') + .switchMap((ids) => this.ConfigureState.state$.let(this.ConfigSelectors.selectClusterNames(ids)).take(1)) + .exhaustMap((names) => { + return fromPromise(this.Confirm.confirm(` + <p>Are you sure want to remove these clusters?</p> + <ul>${names.map((name) => `<li>${name}</li>`).join('')}</ul> + `)) + .map(confirmClustersRemovalOK) + .catch(() => Observable.empty()); + }); + + this.persistRemovedClustersLocallyEffect$ = this.ConfigureState.actions$ + .let(ofType(CONFIRM_CLUSTERS_REMOVAL_OK)) + .withLatestFrom(this.ConfigureState.actions$.let(ofType(CONFIRM_CLUSTERS_REMOVAL))) + .switchMap(([, {clusterIDs}]) => of( + {type: shortClustersActionTypes.REMOVE, ids: clusterIDs}, + {type: clustersActionTypes.REMOVE, ids: clusterIDs} + )); + + this.persistRemovedClustersRemotelyEffect$ = this.ConfigureState.actions$ + .let(ofType(CONFIRM_CLUSTERS_REMOVAL_OK)) + .withLatestFrom( + this.ConfigureState.actions$.let(ofType(CONFIRM_CLUSTERS_REMOVAL)), + this.ConfigureState.actions$.let(ofType(shortClustersActionTypes.REMOVE)), + this.ConfigureState.actions$.let(ofType(clustersActionTypes.REMOVE)) + ) + .switchMap(([, {clusterIDs}, ...backup]) => this.Clusters.removeCluster$(clusterIDs) + .mapTo({ + type: 'REMOVE_CLUSTERS_OK' + }) + .catch((e) => of( + { + type: 'REMOVE_CLUSTERS_ERR', + error: { + message: `Failed to remove clusters: ${e.data}` + } + }, + { + type: 'UNDO_ACTIONS', + actions: backup + } + )) + ); + + this.notifyRemoteClustersRemoveSuccessEffect$ = this.ConfigureState.actions$ + .let(ofType('REMOVE_CLUSTERS_OK')) + .withLatestFrom(this.ConfigureState.actions$.let(ofType(CONFIRM_CLUSTERS_REMOVAL))) + .do(([, {clusterIDs}]) => this.IgniteMessages.showInfo(`Cluster(s) removed: ${clusterIDs.length}`)) + .ignoreElements(); + + const _applyChangedIDs = (edit, {cache, igfs, model, cluster} = {}) => ({ + cluster: { + ...edit.changes.cluster, + ...(cluster ? cluster : {}), + caches: cache ? uniq([...edit.changes.caches.ids, cache._id]) : edit.changes.caches.ids, + igfss: igfs ? uniq([...edit.changes.igfss.ids, igfs._id]) : edit.changes.igfss.ids, + models: model ? uniq([...edit.changes.models.ids, model._id]) : edit.changes.models.ids + }, + caches: cache ? uniq([...edit.changes.caches.changedItems, cache]) : edit.changes.caches.changedItems, + igfss: igfs ? uniq([...edit.changes.igfss.changedItems, igfs]) : edit.changes.igfss.changedItems, + models: model ? uniq([...edit.changes.models.changedItems, model]) : edit.changes.models.changedItems + }); + + this.advancedSaveCacheEffect$ = merge( + this.ConfigureState.actions$.let(ofType(ADVANCED_SAVE_CLUSTER)), + this.ConfigureState.actions$.let(ofType(ADVANCED_SAVE_CACHE)), + this.ConfigureState.actions$.let(ofType(ADVANCED_SAVE_IGFS)), + this.ConfigureState.actions$.let(ofType(ADVANCED_SAVE_MODEL)), + ) + .withLatestFrom(this.ConfigureState.state$.pluck('edit')) + .map(([action, edit]) => ({ + type: 'ADVANCED_SAVE_COMPLETE_CONFIGURATION', + changedItems: _applyChangedIDs(edit, action) + })); + + this.basicSaveEffect$ = this.ConfigureState.actions$ + .let(ofType(BASIC_SAVE)) + .merge(this.ConfigureState.actions$.let(ofType(BASIC_SAVE_AND_DOWNLOAD))) + .withLatestFrom(this.ConfigureState.state$.pluck('edit')) + .switchMap(([action, edit]) => { + const changedItems = _applyChangedIDs(edit, {cluster: action.cluster}); + const actions = [{ + type: cachesActionTypes.UPSERT, + items: changedItems.caches + }, + { + type: shortCachesActionTypes.UPSERT, + items: changedItems.caches + }, + { + type: clustersActionTypes.UPSERT, + items: [changedItems.cluster] + }, + { + type: shortClustersActionTypes.UPSERT, + items: [this.Clusters.toShortCluster(changedItems.cluster)] + } + ].filter((a) => a.items.length); + + return Observable.of(...actions) + .merge( + Observable.fromPromise(this.Clusters.saveBasic(changedItems)) + .switchMap((res) => Observable.of( + {type: 'EDIT_CLUSTER', cluster: changedItems.cluster}, + basicSaveOK(changedItems) + )) + .catch((res) => Observable.of( + basicSaveErr(changedItems, res), + {type: 'UNDO_ACTIONS', actions} + )) + ); + }); + + this.basicSaveOKMessagesEffect$ = this.ConfigureState.actions$ + .let(ofType(BASIC_SAVE_OK)) + .do((action) => this.IgniteMessages.showInfo(`Cluster "${action.changedItems.cluster.name}" saved.`)) + .ignoreElements(); + } + + /** + * @name etp + * @function + * @param {object} action + * @returns {Promise} + */ + /** + * @name etp^2 + * @function + * @param {string} type + * @param {object} [params] + * @returns {Promise} + */ + etp = (...args) => { + const action = typeof args[0] === 'object' ? args[0] : {type: args[0], ...args[1]}; + const ok = `${action.type}_OK`; + const err = `${action.type}_ERR`; + + setTimeout(() => this.ConfigureState.dispatchAction(action)); + return this.ConfigureState.actions$ + .filter((a) => a.type === ok || a.type === err) + .take(1) + .map((a) => { + if (a.type === err) + throw a; + else + return a; + }) + .toPromise(); + }; + + connect() { + return merge( + ...Object.keys(this).filter((k) => k.endsWith('Effect$')).map((k) => this[k]) + ).do((a) => this.ConfigureState.dispatchAction(a)).subscribe(); + } +}
http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/frontend/app/components/page-configure/store/selectors.js ---------------------------------------------------------------------- diff --git a/modules/web-console/frontend/app/components/page-configure/store/selectors.js b/modules/web-console/frontend/app/components/page-configure/store/selectors.js new file mode 100644 index 0000000..0f60214 --- /dev/null +++ b/modules/web-console/frontend/app/components/page-configure/store/selectors.js @@ -0,0 +1,170 @@ +/* + * 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 {uniqueName} from 'app/utils/uniqueName'; +import {of} from 'rxjs/observable/of'; +import {empty} from 'rxjs/observable/empty'; +import {combineLatest} from 'rxjs/observable/combineLatest'; +import 'rxjs/add/operator/mergeMap'; +import 'rxjs/add/observable/combineLatest'; +import {Observable} from 'rxjs/Observable'; +import {defaultNames} from '../defaultNames'; + +import {default as Caches} from 'app/services/Caches'; +import {default as Clusters} from 'app/services/Clusters'; +import {default as IGFSs} from 'app/services/IGFSs'; +import {default as Models} from 'app/services/Models'; + +const isDefined = (s) => s.filter((v) => v); +const selectItems = (path) => (s) => s.filter((s) => s).pluck(path).filter((v) => v); +const selectValues = (s) => s.map((v) => v && [...v.value.values()]); +export const selectMapItem = (mapPath, key) => (s) => s.pluck(mapPath).map((v) => v && v.get(key)); +const selectMapItems = (mapPath, keys) => (s) => s.pluck(mapPath).map((v) => v && keys.map((key) => v.get(key))); +const selectItemToEdit = ({items, itemFactory, defaultName = '', itemID}) => (s) => s.switchMap((item) => { + if (item) return of(Object.assign(itemFactory(), item)); + if (itemID === 'new') return items.take(1).map((items) => Object.assign(itemFactory(), {name: uniqueName(defaultName, items)})); + if (!itemID) return of(null); + return empty(); +}); +const currentShortItems = ({changesKey, shortKey}) => (state$) => { + return Observable.combineLatest( + state$.pluck('edit', 'changes', changesKey).let(isDefined).distinctUntilChanged(), + state$.pluck(shortKey, 'value').let(isDefined).distinctUntilChanged() + ) + .map(([{ids = [], changedItems}, shortItems]) => { + if (!ids.length || !shortItems) return []; + return ids.map((id) => changedItems.find(({_id}) => _id === id) || shortItems.get(id)); + }) + .map((v) => v.filter((v) => v)); +}; + +const selectNames = (itemIDs, nameAt = 'name') => (items) => items + .pluck('value') + .map((items) => itemIDs.map((id) => items.get(id)[nameAt])); + +export default class ConfigSelectors { + static $inject = ['Caches', 'Clusters', 'IGFSs', 'Models']; + /** + * @param {Caches} Caches + * @param {Clusters} Clusters + * @param {IGFSs} IGFSs + * @param {Models} Models + */ + constructor(Caches, Clusters, IGFSs, Models) { + this.Caches = Caches; + this.Clusters = Clusters; + this.IGFSs = IGFSs; + this.Models = Models; + + /** + * @param {string} id + * @returns {(state$: Observable) => Observable<ig.config.model.DomainModel>} + */ + this.selectModel = (id) => selectMapItem('models', id); + /** + * @returns {(state$: Observable) => Observable<{pristine: boolean, value: Map<string, ig.config.model.ShortDomainModel>}>} + */ + this.selectShortModels = () => selectItems('shortModels'); + this.selectShortModelsValue = () => (state$) => state$.let(this.selectShortModels()).let(selectValues); + /** + * @returns {(state$: Observable) => Observable<Array<ig.config.cluster.ShortCluster>>} + */ + this.selectShortClustersValue = () => (state$) => state$.let(this.selectShortClusters()).let(selectValues); + /** + * @returns {(state$: Observable) => Observable<Array<string>>} + */ + this.selectClusterNames = (clusterIDs) => (state$) => state$ + .let(this.selectShortClusters()) + .let(selectNames(clusterIDs)); + } + selectCluster = (id) => selectMapItem('clusters', id); + selectShortClusters = () => selectItems('shortClusters'); + selectCache = (id) => selectMapItem('caches', id); + selectIGFS = (id) => selectMapItem('igfss', id); + selectShortCaches = () => selectItems('shortCaches'); + selectShortCachesValue = () => (state$) => state$.let(this.selectShortCaches()).let(selectValues); + selectShortIGFSs = () => selectItems('shortIgfss'); + selectShortIGFSsValue = () => (state$) => state$.let(this.selectShortIGFSs()).let(selectValues); + selectShortModelsValue = () => (state$) => state$.let(this.selectShortModels()).let(selectValues); + selectCacheToEdit = (cacheID) => (state$) => state$ + .let(this.selectCache(cacheID)) + .distinctUntilChanged() + .let(selectItemToEdit({ + items: state$.let(this.selectCurrentShortCaches), + itemFactory: () => this.Caches.getBlankCache(), + defaultName: defaultNames.cache, + itemID: cacheID + })); + selectIGFSToEdit = (itemID) => (state$) => state$ + .let(this.selectIGFS(itemID)) + .distinctUntilChanged() + .let(selectItemToEdit({ + items: state$.let(this.selectCurrentShortIGFSs), + itemFactory: () => this.IGFSs.getBlankIGFS(), + defaultName: defaultNames.igfs, + itemID + })); + selectModelToEdit = (itemID) => (state$) => state$ + .let(this.selectModel(itemID)) + .distinctUntilChanged() + .let(selectItemToEdit({ + items: state$.let(this.selectCurrentShortModels), + itemFactory: () => this.Models.getBlankModel(), + itemID + })); + selectClusterToEdit = (clusterID, defaultName = defaultNames.cluster) => (state$) => state$ + .let(this.selectCluster(clusterID)) + .distinctUntilChanged() + .let(selectItemToEdit({ + items: state$.let(this.selectShortClustersValue()), + itemFactory: () => this.Clusters.getBlankCluster(), + defaultName, + itemID: clusterID + })); + selectCurrentShortCaches = currentShortItems({changesKey: 'caches', shortKey: 'shortCaches'}); + selectCurrentShortIGFSs = currentShortItems({changesKey: 'igfss', shortKey: 'shortIgfss'}); + selectCurrentShortModels = currentShortItems({changesKey: 'models', shortKey: 'shortModels'}); + selectClusterShortCaches = (clusterID) => (state$) => { + if (clusterID === 'new') return of([]); + return combineLatest( + state$.let(this.selectCluster(clusterID)).pluck('caches'), + state$.let(this.selectShortCaches()).pluck('value'), + (ids, items) => ids.map((id) => items.get(id)) + ); + }; + selectCompleteClusterConfiguration = ({clusterID, isDemo}) => (state$) => { + const hasValues = (array) => !array.some((v) => !v); + return state$.let(this.selectCluster(clusterID)) + .exhaustMap((cluster) => { + if (!cluster) return of({__isComplete: false}); + const withSpace = (array) => array.map((c) => ({...c, space: cluster.space})); + return Observable.forkJoin( + state$.let(selectMapItems('caches', cluster.caches || [])).take(1), + state$.let(selectMapItems('models', cluster.models || [])).take(1), + state$.let(selectMapItems('igfss', cluster.igfss || [])).take(1), + ) + .map(([caches, models, igfss]) => ({ + cluster, + caches, + domains: models, + igfss, + spaces: [{_id: cluster.space, demo: isDemo}], + __isComplete: !!cluster && !(!hasValues(caches) || !hasValues(models) || !hasValues(igfss)) + })); + }); + }; +} http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/frontend/app/components/page-configure/style.scss ---------------------------------------------------------------------- diff --git a/modules/web-console/frontend/app/components/page-configure/style.scss b/modules/web-console/frontend/app/components/page-configure/style.scss index 424e6ba..94a3ebc 100644 --- a/modules/web-console/frontend/app/components/page-configure/style.scss +++ b/modules/web-console/frontend/app/components/page-configure/style.scss @@ -16,8 +16,289 @@ */ page-configure { - h1 version-picker { + font-family: Roboto; + flex: 1 0 auto; + display: flex; + flex-direction: column; + + &>.pc-page-header { + display: flex; + + .pc-page-header-title { + margin-right: auto; + } + } + + .pc-form-actions-panel { + display: flex; + flex-direction: row; + padding: 10px 20px 10px 30px; + box-shadow: 0 0px 4px 0 rgba(0, 0, 0, 0.2), 0px 3px 4px -1px rgba(0, 0, 0, 0.2); + position: sticky; + bottom: 0px; + // margin: 20px -30px -30px; + background: white; + border-radius: 0 0 4px 4px; + z-index: 10; + + &>*+* { + margin-left: 10px; + } + + .link-primary + .link-primary { + margin-left: 40px; + } + + .pc-form-actions-panel__right-after { + width: 0; + margin-left: auto; + } + } + + .pc-hide-tooltips { + .tipField:not(.fa-remove), .icon-help, [ignite-icon='info'] { + display: none; + } + } + + .pc-content-container { + position: relative; + + &, &>ui-view { + flex: 1 0 auto; + display: flex; + flex-direction: column; + } + + .pc-tooltips-toggle { + position: absolute; + top: 0; + right: 0; + } + + &>.tabs { + flex: 0 0 auto; + } + } + + .pc-tooltips-toggle { + display: inline-flex; + height: 40px; + align-items: center; + width: auto; + max-width: none !important; + user-select: none; + + &>*:not(input) { + margin-left: 5px; + flex: 0 0 auto; + } + + input { + pointer-events: none; + } + + &>div { + margin-left: 10px !important; + } + } +} + +.pc-form-group { + $input-height: 36px; + width: 100%; + border: 1px solid rgba(197, 197, 197, 0.5); + border-radius: 4px; + padding-bottom: 10px; + padding-top: $input-height / 2; + margin-top: $input-height / -2; + + &:empty { + display: none; + } + +} + +.pc-form-group__text-title { + transform: translateY(-9px); + --pc-form-group-title-bg-color: white; + + &>span { + padding-left: 10px; + padding-right: 10px; + background: var(--pc-form-group-title-bg-color); + } + + &>.form-field-checkbox .ignite-form-field__control { + & > span { + position: relative; + + &:after { + content: ''; + display: block; + position: absolute; + background-color: var(--pc-form-group-title-bg-color); + z-index: -1; + top: 0; + bottom: 0; + left: -26px; + right: -5px; + } + } + [ignite-icon] { + background-color: var(--pc-form-group-title-bg-color); + } + } + + &+.pc-form-group { + padding-top: 10px; + } +} + +.pc-form-grid-row > .pc-form-group__text-title[class*='pc-form-grid-col-'] { + margin-top: 20px !important; +} + +list-editable .pc-form-group__text-title { + --pc-form-group-title-bg-color: var(--le-row-bg-color); +} + +.pc-form-grid-row { + display: flex; + flex-direction: row; + flex-wrap: wrap; + align-content: flex-start; + + &>[class*='pc-form-grid-col-'] { + margin: 10px 0 0 !important; + max-width: none; + padding: 0 10px; + flex-grow: 0; + flex-shrink: 0; + } + + .group-section { + width: 100%; + } + + &>.pc-form-grid__break { + flex: 1 0 100%; + } +} + +.pc-form-grid-row { + &>.pc-form-grid-col-10 { + flex-basis: calc(100% / 6); + } + + &>.pc-form-grid-col-20 { + flex-basis: calc(100% / 3); + } + + &>.pc-form-grid-col-30 { + flex-basis: calc(100% / 2); + } + + &>.pc-form-grid-col-40 { + flex-basis: calc(100% / 1.5); + } + + &>.pc-form-grid-col-60 { + flex-basis: calc(100% / 1); + } + + @media(max-width: 992px) { + &>.pc-form-grid-col-10 { + flex-basis: calc(25%); + } + &>.pc-form-grid-col-20 { + flex-basis: calc(50%); + } + + &>.pc-form-grid-col-30 { + flex-basis: calc(100%); + } + + &>.pc-form-grid-col-60 { + flex-basis: calc(100%); + } + } + // IE11 does not count padding towards flex width + @media screen and (-ms-high-contrast: active), (-ms-high-contrast: none) { + &>.pc-form-grid-col-10 { + flex-basis: calc(99.9% / 6 - 20px); + } + + &>.pc-form-grid-col-20 { + flex-basis: calc(99.9% / 3 - 20px); + } + + &>.pc-form-grid-col-30 { + flex-basis: calc(100% / 2 - 20px); + } + + &>.pc-form-grid-col-40 { + flex-basis: calc(100% / 1.5 - 20px); + } + + &>.pc-form-grid-col-60 { + flex-basis: calc(100% / 1 - 20px); + } + + @media(max-width: 992px) { + &>.pc-form-grid-col-20 { + flex-basis: calc(50% - 20px); + } + + &>.pc-form-grid-col-30 { + flex-basis: calc(100% - 20px); + } + + &>.pc-form-grid-col-60 { + flex-basis: calc(100% - 20px); + } + } + } +} +.pc-page-header { + font-family: Roboto; + font-size: 24px; + line-height: 36px; + color: #393939; + margin: 40px 0 30px; + padding: 0; + border: none; + word-break: break-all; + + .pc-page-header-sub { + font-size: 14px; + line-height: 20px; + color: #757575; + margin-left: 8px; + } + + version-picker { margin-left: 8px; - vertical-align: bottom; + vertical-align: 2px; + } + + button-import-models { + flex: 0 0 auto; + margin-left: 30px; + position: relative; + // For some reason button is heigher up by 1px than on overview page + top: 1px; } +} + +.pc-form-grid__text-only-item { + padding-top: 16px; + align-self: flex-start; + height: 54px; + display: flex; + justify-content: center; + align-content: center; + align-items: center; + background: white; + z-index: 2; } \ No newline at end of file http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/frontend/app/components/page-configure/template.pug ---------------------------------------------------------------------- diff --git a/modules/web-console/frontend/app/components/page-configure/template.pug b/modules/web-console/frontend/app/components/page-configure/template.pug index 72ca614..c56320b 100644 --- a/modules/web-console/frontend/app/components/page-configure/template.pug +++ b/modules/web-console/frontend/app/components/page-configure/template.pug @@ -14,17 +14,34 @@ See the License for the specific language governing permissions and limitations under the License. -.docs-content - header - h1 - | Configure - version-picker - div - ul.tabs.tabs--blue - li(role='presentation' ui-sref-active='active') - a(ui-sref='base.configuration.tabs.basic') Basic - li(role='presentation' ui-sref-active='active') - a(ui-sref='base.configuration.tabs.advanced') Advanced +h1.pc-page-header + span.pc-page-header-title + | {{ $ctrl.clusterName$|async:this }} + version-picker + button-import-models(cluster-id='$ctrl.clusterID$|async:this') +div.pc-content-container + ul.tabs.tabs--blue + li(role='presentation' ui-sref-active='active') + a(ui-sref='base.configuration.edit.basic') Basic + li(role='presentation' ui-sref-active='{active: "base.configuration.edit.advanced"}') + a(ui-sref='base.configuration.edit.advanced.cluster') Advanced - .panel--ignite - ui-view \ No newline at end of file + label.pc-tooltips-toggle.switcher--ignite + svg.icon-left( + ignite-icon='info' + bs-tooltip='' + data-title='This setting is needed to hide and show tooltips with hints.' + data-placement='left' + ) + span Tooltips + input(type='checkbox' ng-model='$ctrl.tooltipsVisible') + div + + ui-view.theme--ignite( + ignite-loading='configuration' + ignite-loading-text='{{ $ctrl.loadingText }}' + ignite-loading-position='top' + ng-class=`{ + 'pc-hide-tooltips': !$ctrl.tooltipsVisible + }` + ) \ No newline at end of file http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/frontend/app/components/page-configure/transitionHooks/errorState.js ---------------------------------------------------------------------- diff --git a/modules/web-console/frontend/app/components/page-configure/transitionHooks/errorState.js b/modules/web-console/frontend/app/components/page-configure/transitionHooks/errorState.js new file mode 100644 index 0000000..f36a931 --- /dev/null +++ b/modules/web-console/frontend/app/components/page-configure/transitionHooks/errorState.js @@ -0,0 +1,55 @@ +/* + * 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 {RejectType} from '@uirouter/angularjs'; + +const isPromise = (object) => object && typeof object.then === 'function'; +const match = { + to(state) { + return state.data && state.data.errorState; + } +}; +const go = ($transition) => $transition.router.stateService.go( + $transition.to().data.errorState, + $transition.params(), + {location: 'replace'} +); + +/** + * @returns {Array<Promise>} + */ +const getResolvePromises = ($transition) => $transition.getResolveTokens() + .filter((token) => typeof token === 'string') + .map((token) => $transition.injector().getAsync(token)) + .filter(isPromise); + +/** + * Global transition hook that redirects to data.errorState if: + * 1. Transition throws an error. + * 2. Any resolve promise throws an error. onError does not work for this case if resolvePolicy is set to 'NOWAIT'. + */ +export const errorState = ($uiRouter) => { + $uiRouter.transitionService.onError(match, ($transition) => { + if ($transition.error().type !== RejectType.ERROR) return; + go($transition); + }); + $uiRouter.transitionService.onStart(match, ($transition) => { + Promise.all(getResolvePromises($transition)).catch((e) => go($transition)); + }); +}; + +errorState.$inject = ['$uiRouter']; http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/frontend/app/components/page-configure/types/uirouter.d.ts ---------------------------------------------------------------------- diff --git a/modules/web-console/frontend/app/components/page-configure/types/uirouter.d.ts b/modules/web-console/frontend/app/components/page-configure/types/uirouter.d.ts new file mode 100644 index 0000000..90d8434 --- /dev/null +++ b/modules/web-console/frontend/app/components/page-configure/types/uirouter.d.ts @@ -0,0 +1,20 @@ +/* + * 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 * as _uirouter from '@uirouter/angularjs' +export as namespace uirouter +export = _uirouter \ No newline at end of file http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/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 a96914e..f4eae1a 100644 --- a/modules/web-console/frontend/app/components/page-profile/style.scss +++ b/modules/web-console/frontend/app/components/page-profile/style.scss @@ -28,4 +28,8 @@ 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/7ee1683e/modules/web-console/frontend/app/components/page-queries/components/queries-notebook/controller.js ---------------------------------------------------------------------- diff --git a/modules/web-console/frontend/app/components/page-queries/components/queries-notebook/controller.js b/modules/web-console/frontend/app/components/page-queries/components/queries-notebook/controller.js index 55bee85..fcb3609 100644 --- a/modules/web-console/frontend/app/components/page-queries/components/queries-notebook/controller.js +++ b/modules/web-console/frontend/app/components/page-queries/components/queries-notebook/controller.js @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - +import {nonEmpty, nonNil} from 'app/utils/lodashMixins'; import 'rxjs/add/operator/mergeMap'; import 'rxjs/add/operator/merge'; import 'rxjs/add/operator/switchMap'; @@ -147,8 +147,8 @@ class Paragraph { let cause = err; - while (_.nonNil(cause)) { - if (_.nonEmpty(cause.className) && + while (nonNil(cause)) { + if (nonEmpty(cause.className) && _.includes(['SQLException', 'JdbcSQLException', 'QueryCancelledException'], JavaTypes.shortClassName(cause.className))) { this.error.message = cause.message || cause.className; @@ -158,10 +158,10 @@ class Paragraph { cause = cause.cause; } - if (_.isEmpty(this.error.message) && _.nonEmpty(err.className)) { + if (_.isEmpty(this.error.message) && nonEmpty(err.className)) { this.error.message = 'Internal cluster error'; - if (_.nonEmpty(err.className)) + if (nonEmpty(err.className)) this.error.message += ': ' + err.className; } }; @@ -171,7 +171,7 @@ class Paragraph { if (_.isNil(this.queryArgs)) return null; - if (_.nonEmpty(this.error.message)) + if (nonEmpty(this.error.message)) return 'error'; if (_.isEmpty(this.rows)) @@ -197,7 +197,7 @@ class Paragraph { } queryExecuted() { - return _.nonEmpty(this.meta) || _.nonEmpty(this.error.message); + return nonEmpty(this.meta) || nonEmpty(this.error.message); } scanExplain() { @@ -209,11 +209,11 @@ class Paragraph { } chartColumnsConfigured() { - return _.nonEmpty(this.chartKeyCols) && _.nonEmpty(this.chartValCols); + return nonEmpty(this.chartKeyCols) && nonEmpty(this.chartValCols); } chartTimeLineEnabled() { - return _.nonEmpty(this.chartKeyCols) && _.eq(this.chartKeyCols[0], TIME_LINE); + return nonEmpty(this.chartKeyCols) && _.eq(this.chartKeyCols[0], TIME_LINE); } executionInProgress(showLocal = false) { @@ -922,7 +922,7 @@ export class NotebookCtrl { }); // Await for demo caches. - if (!$ctrl.demoStarted && $root.IgniteDemoMode && _.nonEmpty(cacheNames)) { + if (!$ctrl.demoStarted && $root.IgniteDemoMode && nonEmpty(cacheNames)) { $ctrl.demoStarted = true; Loading.finish('sqlLoading'); @@ -935,7 +935,7 @@ export class NotebookCtrl { const _startWatch = () => { const awaitClusters$ = fromPromise( - agentMgr.startClusterWatch('Back to Configuration', 'base.configuration.tabs.advanced.clusters')); + agentMgr.startClusterWatch('Back to Configuration', 'base.configuration.overview')); const finishLoading$ = defer(() => { if (!$root.IgniteDemoMode) @@ -1919,7 +1919,7 @@ export class NotebookCtrl { const tab = ' '; const addToTrace = (item) => { - if (_.nonNil(item)) { + if (nonNil(item)) { const clsName = _.isEmpty(item.className) ? '' : '[' + JavaTypes.shortClassName(item.className) + '] '; scope.content.push((scope.content.length > 0 ? tab : '') + clsName + (item.message || '')); http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/frontend/app/components/page-queries/components/queries-notebooks-list/controller.js ---------------------------------------------------------------------- diff --git a/modules/web-console/frontend/app/components/page-queries/components/queries-notebooks-list/controller.js b/modules/web-console/frontend/app/components/page-queries/components/queries-notebooks-list/controller.js index 8131483..7c06f2a 100644 --- a/modules/web-console/frontend/app/components/page-queries/components/queries-notebooks-list/controller.js +++ b/modules/web-console/frontend/app/components/page-queries/components/queries-notebooks-list/controller.js @@ -126,7 +126,7 @@ export class NotebooksListCtrl { _checkActionsAllow() { // Dissallow clone and rename if more then one item is selectted. - const oneItemIsSelected = this.gridApi.selection.getSelectedRows().length === 1; + const oneItemIsSelected = this.gridApi.selection.legacyGetSelectedRows().length === 1; this.actionOptions[0].available = oneItemIsSelected; this.actionOptions[1].available = oneItemIsSelected; } @@ -154,7 +154,7 @@ export class NotebooksListCtrl { async renameNotebok() { try { - const currentNotebook = this.gridApi.selection.getSelectedRows()[0]; + const currentNotebook = this.gridApi.selection.legacyGetSelectedRows()[0]; const newNotebookName = await this.IgniteInput.input('Rename notebook', 'Notebook name', currentNotebook.name); if (this.getNotebooksNames().find((name) => newNotebookName === name)) @@ -174,7 +174,7 @@ export class NotebooksListCtrl { async cloneNotebook() { try { - const clonedNotebook = Object.assign({}, this.gridApi.selection.getSelectedRows()[0]); + const clonedNotebook = Object.assign({}, this.gridApi.selection.legacyGetSelectedRows()[0]); const newNotebookName = await this.IgniteInput.clone(clonedNotebook.name, this.getNotebooksNames()); this.IgniteLoading.start('notebooksLoading'); @@ -201,7 +201,7 @@ export class NotebooksListCtrl { async deleteNotebooks() { try { this.IgniteLoading.start('notebooksLoading'); - await this.IgniteNotebook.removeBatch(this.gridApi.selection.getSelectedRows()); + await this.IgniteNotebook.removeBatch(this.gridApi.selection.legacyGetSelectedRows()); await this.IgniteLoading.finish('notebooksLoading'); this._loadAllNotebooks(); http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/frontend/app/components/page-queries/components/queries-notebooks-list/template.tpl.pug ---------------------------------------------------------------------- diff --git a/modules/web-console/frontend/app/components/page-queries/components/queries-notebooks-list/template.tpl.pug b/modules/web-console/frontend/app/components/page-queries/components/queries-notebooks-list/template.tpl.pug index 25785a4..75b5e99 100644 --- a/modules/web-console/frontend/app/components/page-queries/components/queries-notebooks-list/template.tpl.pug +++ b/modules/web-console/frontend/app/components/page-queries/components/queries-notebooks-list/template.tpl.pug @@ -34,7 +34,7 @@ page-queries-slot(slot-name="'queriesButtons'" ng-if="!$root.IgniteDemoMode") label: 'Actions', model: '$ctrl.action', name: 'action', - disabled: '$ctrl.gridApi.selection.getSelectedRows().length === 0', + disabled: '$ctrl.gridApi.selection.legacyGetSelectedRows().length === 0', required: false, options: '$ctrl.actionOptions' }) http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/frontend/app/components/version-picker/style.scss ---------------------------------------------------------------------- diff --git a/modules/web-console/frontend/app/components/version-picker/style.scss b/modules/web-console/frontend/app/components/version-picker/style.scss index 98a4e9d..6d962c2 100644 --- a/modules/web-console/frontend/app/components/version-picker/style.scss +++ b/modules/web-console/frontend/app/components/version-picker/style.scss @@ -29,9 +29,8 @@ version-picker { padding-bottom: 1px; } - .icon-help { + [ignite-icon] { margin-left: 5px; - font-size: 16px; } .dropdown-menu a { http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/frontend/app/components/version-picker/template.pug ---------------------------------------------------------------------- diff --git a/modules/web-console/frontend/app/components/version-picker/template.pug b/modules/web-console/frontend/app/components/version-picker/template.pug index 1abe471..5d35b78 100644 --- a/modules/web-console/frontend/app/components/version-picker/template.pug +++ b/modules/web-console/frontend/app/components/version-picker/template.pug @@ -24,10 +24,14 @@ ) | {{$ctrl.currentVersion}} span.icon-right.fa.fa-caret-down - -i.icon-help( + +svg.icon-help( + ignite-icon='info' bs-tooltip='' - data-title='Web Console supports multiple Ignite versions.<br /> \ - Select version you need to configure cluster.' + data-title=` + Web Console supports multiple Ignite versions. + <br> + Select version you need to configure cluster. + ` data-placement='right' -) +) \ No newline at end of file http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/frontend/app/core/activities/Activities.data.d.ts ---------------------------------------------------------------------- diff --git a/modules/web-console/frontend/app/core/activities/Activities.data.d.ts b/modules/web-console/frontend/app/core/activities/Activities.data.d.ts new file mode 100644 index 0000000..88f9dd4 --- /dev/null +++ b/modules/web-console/frontend/app/core/activities/Activities.data.d.ts @@ -0,0 +1,37 @@ +/* + * 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. + */ + +interface IActivityDataResponse { + action: string, + amount: number, + date: string, + group: string, + owner: string, + _id: string +} + +/** + * Activities data service + */ +declare class ActivitiesData { + /** + * Posts activity to backend, sends current state if no options specified + */ + post({group, action}?:{group?: string, action?: string}): ng.IPromise<ng.IHttpResponse<IActivityDataResponse>> +} + +export default ActivitiesData \ No newline at end of file http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/frontend/app/core/activities/Activities.data.js ---------------------------------------------------------------------- diff --git a/modules/web-console/frontend/app/core/activities/Activities.data.js b/modules/web-console/frontend/app/core/activities/Activities.data.js index 8d9447c..35b44e6 100644 --- a/modules/web-console/frontend/app/core/activities/Activities.data.js +++ b/modules/web-console/frontend/app/core/activities/Activities.data.js @@ -18,6 +18,10 @@ export default class ActivitiesData { static $inject = ['$http', '$state']; + /** + * @param {ng.IHttpService} $http + * @param {uirouter.StateService} $state + */ constructor($http, $state) { this.$http = $http; this.$state = $state; @@ -26,8 +30,10 @@ export default class ActivitiesData { post(options = {}) { let { group, action } = options; - action = action || this.$state.$current.url.source; - group = group || action.match(/^\/([^/]+)/)[1]; + // TODO IGNITE-5466: since upgrade to UIRouter 1, "url.source" is undefined. + // Actions like that won't be saved to DB. Think of a better solution later. + action = action || this.$state.$current.url.source || ''; + group = group || (action.match(/^\/([^/]+)/) || [])[1]; return this.$http.post('/api/v1/activities/page', { group, action }); } http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/frontend/app/data/getting-started.json ---------------------------------------------------------------------- diff --git a/modules/web-console/frontend/app/data/getting-started.json b/modules/web-console/frontend/app/data/getting-started.json index fd1869c..caed920 100644 --- a/modules/web-console/frontend/app/data/getting-started.json +++ b/modules/web-console/frontend/app/data/getting-started.json @@ -74,23 +74,6 @@ ] }, { - "title": "Summary", - "message": [ - "<div class='col-xs-7'>", - " <img src='/images/summary.png' width='100%' />", - "</div>", - "<div class='col-xs-5'>", - " <ul>", - " <li>Preview XML configuration</li>", - " <li>Preview code configuration</li>", - " <li>Preview Docker file</li>", - " <li>Preview POM dependencies</li>", - " <li>Download ready-to-use project</li>", - " </ul>", - "</div>" - ] - }, - { "title": "SQL Queries", "message": [ "<div class='col-xs-7'>", http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/frontend/app/data/i18n.js ---------------------------------------------------------------------- diff --git a/modules/web-console/frontend/app/data/i18n.js b/modules/web-console/frontend/app/data/i18n.js index 3385f60..d992153 100644 --- a/modules/web-console/frontend/app/data/i18n.js +++ b/modules/web-console/frontend/app/data/i18n.js @@ -18,11 +18,19 @@ export default { '/agent/start': 'Agent start', '/agent/download': 'Agent download', - '/configuration/clusters': 'Configure clusters', - '/configuration/caches': 'Configure caches', - '/configuration/domains': 'Configure domain model', - '/configuration/igfs': 'Configure IGFS', - '/configuration/summary': 'Configurations summary', + 'base.configuration.overview': 'Cluster configurations', + '/configuration/overview': 'Cluster configurations', + 'base.configuration.edit.basic': 'Basic cluster configuration edit', + '/configuration/new': 'Сluster configuration create', + '/configuration/new/basic': 'Basic cluster configuration create', + '/configuration/new/advanced/cluster': 'Advanced cluster configuration create', + 'base.configuration.edit.advanced.cluster': 'Advanced cluster configuration edit', + 'base.configuration.edit.advanced.caches': 'Advanced cluster caches', + 'base.configuration.edit.advanced.caches.cache': 'Advanced cluster cache edit', + 'base.configuration.edit.advanced.models': 'Advanced cluster models', + 'base.configuration.edit.advanced.models.model': 'Advanced cluster model edit', + 'base.configuration.edit.advanced.igfs': 'Advanced cluster IGFSs', + 'base.configuration.edit.advanced.igfs.igfs': 'Advanced cluster IGFS edit', '/configuration/download': 'Download project', '/demo/resume': 'Demo resume', '/demo/reset': 'Demo reset', http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/frontend/app/directives/on-focus-out.directive.js ---------------------------------------------------------------------- diff --git a/modules/web-console/frontend/app/directives/on-focus-out.directive.js b/modules/web-console/frontend/app/directives/on-focus-out.directive.js index 802691f..fe59477 100644 --- a/modules/web-console/frontend/app/directives/on-focus-out.directive.js +++ b/modules/web-console/frontend/app/directives/on-focus-out.directive.js @@ -15,23 +15,93 @@ * limitations under the License. */ -export default ['$parse', ($parse) => { - return ($scope, $element, $attrs) => { - const parsedExpr = $parse($attrs.igniteOnFocusOut); +/** + * @type {ng.IComponentController} + */ +class OnFocusOutController { + /** @type {OnFocusOutController} */ + parent; + /** @type {Array<OnFocusOutController>} */ + children = []; + /** @type {Array<string>} */ + ignoredClasses = []; + /** @type {function} */ + igniteOnFocusOut; - const handlerCheckFocusOut = (FocusClick) => { - if ($element.find(FocusClick.target).length) - return; + static $inject = ['$element', '$window', '$scope']; + /** + * @param {JQLite} $element + * @param {ng.IWindowService} $window + * @param {ng.IScope} $scope + */ + constructor($element, $window, $scope) { + this.$element = $element; + this.$window = $window; + this.$scope = $scope; - $scope.$evalAsync(() => parsedExpr($scope)); + /** @param {MouseEvent|FocusEvent} e */ + this._eventHandler = (e) => { + this.children.forEach((c) => c._eventHandler(e)); + if (this.shouldPropagate(e) && this.isFocused) { + this.$scope.$applyAsync(() => { + this.igniteOnFocusOut(); + this.isFocused = false; + }); + } }; + /** @param {FocusEvent} e */ + this._onFocus = (e) => { + this.isFocused = true; + }; + } + $onDestroy() { + this.$window.removeEventListener('click', this._eventHandler, true); + this.$window.removeEventListener('focusin', this._eventHandler, true); + this.$element[0].removeEventListener('focus', this._onFocus, true); + if (this.parent) this.parent.children.splice(this.parent.children.indexOf(this), 1); + this.$element = this.$window = this._eventHandler = this._onFocus = null; + } + shouldPropagate(e) { + return !this.targetHasIgnoredClasses(e) && this.targetIsOutOfElement(e); + } + targetIsOutOfElement(e) { + return !this.$element.find(e.target).length; + } + targetHasIgnoredClasses(e) { + return this.ignoredClasses.some((c) => e.target.classList.contains(c)); + } + /** + * @param {ng.IOnChangesObject} changes [description] + */ + $onChanges(changes) { + if ( + 'ignoredClasses' in changes && + changes.ignoredClasses.currentValue !== changes.ignoredClasses.previousValue + ) + this.ignoredClasses = changes.ignoredClasses.currentValue.split(' ').concat('body-overlap'); + } + $onInit() { + if (this.parent) this.parent.children.push(this); + } + $postLink() { + this.$window.addEventListener('click', this._eventHandler, true); + this.$window.addEventListener('focusin', this._eventHandler, true); + this.$element[0].addEventListener('focus', this._onFocus, true); + } +} - window.addEventListener('click', handlerCheckFocusOut, true); - window.addEventListener('focusin', handlerCheckFocusOut, true); - - $scope.$on('$destroy', () => { - window.removeEventListener('click', handlerCheckFocusOut, true); - window.removeEventListener('focusin', handlerCheckFocusOut, true); - }); +/** + * @type {ng.IDirectiveFactory} + */ +export default function() { + return { + controller: OnFocusOutController, + require: { + parent: '^^?igniteOnFocusOut' + }, + bindToController: { + igniteOnFocusOut: '&', + ignoredClasses: '@?igniteOnFocusOutIgnoredClasses' + } }; -}]; +} http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/frontend/app/directives/ui-ace-pojos/ui-ace-pojos.controller.js ---------------------------------------------------------------------- diff --git a/modules/web-console/frontend/app/directives/ui-ace-pojos/ui-ace-pojos.controller.js b/modules/web-console/frontend/app/directives/ui-ace-pojos/ui-ace-pojos.controller.js index 774d73e..3f9580f 100644 --- a/modules/web-console/frontend/app/directives/ui-ace-pojos/ui-ace-pojos.controller.js +++ b/modules/web-console/frontend/app/directives/ui-ace-pojos/ui-ace-pojos.controller.js @@ -15,6 +15,8 @@ * limitations under the License. */ +import {nonNil} from 'app/utils/lodashMixins'; + export default ['$scope', 'JavaTypes', 'JavaTransformer', function($scope, JavaTypes, generator) { const ctrl = this; @@ -47,7 +49,7 @@ export default ['$scope', 'JavaTypes', 'JavaTransformer', function($scope, JavaT const classes = ctrl.classes = []; _.forEach(ctrl.pojos, (pojo) => { - if (_.nonNil(pojo.keyClass)) + if (nonNil(pojo.keyClass)) classes.push(pojo.keyType); classes.push(pojo.valueType); http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/frontend/app/directives/ui-ace.controller.js ---------------------------------------------------------------------- diff --git a/modules/web-console/frontend/app/directives/ui-ace.controller.js b/modules/web-console/frontend/app/directives/ui-ace.controller.js index 96e9c5e..3b35b2a 100644 --- a/modules/web-console/frontend/app/directives/ui-ace.controller.js +++ b/modules/web-console/frontend/app/directives/ui-ace.controller.js @@ -34,23 +34,6 @@ export default class IgniteUiAceGeneratorFactory { this.generate = (cluster) => this.generatorFactory.cluster(cluster, this.Version.currentSbj.getValue(), this.client === 'true'); break; - case 'clusterCaches': - this.generate = (cluster, caches) => { - const clusterCaches = _.reduce(caches, (acc, cache) => { - if (_.includes(cluster.caches, cache.value)) - acc.push(cache.cache); - - return acc; - }, []); - - const cfg = this.generatorFactory.generator.clusterGeneral(cluster, available); - - this.generatorFactory.generator.clusterCaches(cluster, clusterCaches, null, available, false, cfg); - - return this.generatorFactory.toSection(cfg); - }; - - break; case 'cacheStore': case 'cacheQuery': this.generate = (cache, domains) => { @@ -79,27 +62,13 @@ export default class IgniteUiAceGeneratorFactory { break; case 'clusterServiceConfiguration': this.generate = (cluster, caches) => { - const clusterCaches = _.reduce(caches, (acc, cache) => { - if (_.includes(cluster.caches, cache.value)) - acc.push(cache.cache); - - return acc; - }, []); - - return this.generatorFactory.clusterServiceConfiguration(cluster.serviceConfigurations, clusterCaches); + return this.generatorFactory.clusterServiceConfiguration(cluster.serviceConfigurations, caches); }; break; case 'clusterCheckpoint': this.generate = (cluster, caches) => { - const clusterCaches = _.reduce(caches, (acc, cache) => { - if (_.includes(cluster.caches, cache.value)) - acc.push(cache.cache); - - return acc; - }, []); - - return this.generatorFactory.clusterCheckpoint(cluster, available, clusterCaches); + return this.generatorFactory.clusterCheckpoint(cluster, available, caches); }; break; http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/frontend/app/helpers/jade/form.pug ---------------------------------------------------------------------- diff --git a/modules/web-console/frontend/app/helpers/jade/form.pug b/modules/web-console/frontend/app/helpers/jade/form.pug index 6fddbf6..44eaed9 100644 --- a/modules/web-console/frontend/app/helpers/jade/form.pug +++ b/modules/web-console/frontend/app/helpers/jade/form.pug @@ -24,5 +24,3 @@ include ./form/form-field-checkbox include ./form/form-field-number include ./form/form-field-up include ./form/form-field-down - -include ./form/form-group http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/frontend/app/helpers/jade/form/form-field-checkbox.pug ---------------------------------------------------------------------- diff --git a/modules/web-console/frontend/app/helpers/jade/form/form-field-checkbox.pug b/modules/web-console/frontend/app/helpers/jade/form/form-field-checkbox.pug index fcd6f9d..a8236a9 100644 --- a/modules/web-console/frontend/app/helpers/jade/form/form-field-checkbox.pug +++ b/modules/web-console/frontend/app/helpers/jade/form/form-field-checkbox.pug @@ -15,25 +15,30 @@ limitations under the License. mixin form-field-checkbox(label, model, name, disabled, required, tip) - .checkbox(id=`{{ ${name} }}Field`) - label(id=`{{ ${name} }}Label`) - .input-tip + label.form-field-checkbox.ignite-form-field + .ignite-form-field__control + input( + id=`{{ ${name} }}Input` + name=`{{ ${name} }}` + type='checkbox' + + ng-model=model + ng-required=required && `${required}` + ng-disabled=disabled && `${disabled}` + expose-ignite-form-field-control='$input' + )&attributes(attributes ? attributes.attributes ? attributes.attributes : attributes : {}) + span #{label} + +tooltip(tip, tipOpts, 'tipLabel') + .ignite-form-field__errors( + ng-messages=`$input.$error` + ng-show=`($input.$dirty || $input.$touched || $input.$submitted) && $input.$invalid` + ) if block block - else - input( - id=`{{ ${name} }}Input` - name=`{{ ${name} }}` - type='checkbox' - - data-ng-model=model - data-ng-required=required && `${required}` - data-ng-disabled=disabled && `${disabled}` - - data-ng-focus='tableReset()' - - data-ignite-form-panel-field='' - ) - span #{label} + if required + +form-field-feedback(name, 'required', `${errLbl} could not be empty!`) - +tooltip(tip, tipOpts, 'tipLabel') +mixin sane-form-field-checkbox({label, model, name, disabled, required, tip}) + +form-field-checkbox(label, model, name, disabled = false, required = false, tip)&attributes(attributes) + if block + block \ No newline at end of file http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/frontend/app/helpers/jade/form/form-field-datalist.pug ---------------------------------------------------------------------- diff --git a/modules/web-console/frontend/app/helpers/jade/form/form-field-datalist.pug b/modules/web-console/frontend/app/helpers/jade/form/form-field-datalist.pug index 6da1255..888634b 100644 --- a/modules/web-console/frontend/app/helpers/jade/form/form-field-datalist.pug +++ b/modules/web-console/frontend/app/helpers/jade/form/form-field-datalist.pug @@ -23,29 +23,30 @@ mixin form-field-datalist(label, model, name, disabled, required, placeholder, o name=`{{ ${name} }}` placeholder=placeholder - data-ng-model=model + ng-model=model - data-ng-required=required && `${required}` - data-ng-disabled=disabled && `${disabled}` || `!${options}.length` + ng-required=required && `${required}` + ng-disabled=disabled && `${disabled}` || `!${options}.length` bs-typeahead bs-options=`item for item in ${options}` container='body' data-min-length='1' ignite-retain-selection - - data-ignite-form-panel-field='' + expose-ignite-form-field-control='$input' )&attributes(attributes.attributes) .ignite-form-field - +ignite-form-field__label(label, name, required) - .ignite-form-field__control + +ignite-form-field__label(label, name, required, disabled) +tooltip(tip, tipOpts) - - +form-field-feedback(name, 'required', errLbl + ' could not be empty!') - + .ignite-form-field__control + .input-tip + +form-field-input(attributes=attributes) + .ignite-form-field__errors( + ng-messages=`$input.$error` + ng-if=`($input.$dirty || $input.$touched || $input.$submitted) && $input.$invalid` + ) if block block - .input-tip - +form-field-input(attributes=attributes) + +form-field-feedback(name, 'required', `${errLbl} could not be empty!`) http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/frontend/app/helpers/jade/form/form-field-dropdown.pug ---------------------------------------------------------------------- diff --git a/modules/web-console/frontend/app/helpers/jade/form/form-field-dropdown.pug b/modules/web-console/frontend/app/helpers/jade/form/form-field-dropdown.pug index cf7d50a..c6579e3 100644 --- a/modules/web-console/frontend/app/helpers/jade/form/form-field-dropdown.pug +++ b/modules/web-console/frontend/app/helpers/jade/form/form-field-dropdown.pug @@ -16,33 +16,45 @@ mixin ignite-form-field-dropdown(label, model, name, disabled, required, multiple, placeholder, placeholderEmpty, options, tip) mixin form-field-input() + -var errLbl = label.substring(0, label.length - 1) + button.select-toggle.form-control( + type='button' id=`{{ ${name} }}Input` name=`{{ ${name} }}` data-placeholder=placeholderEmpty ? `{{ ${options}.length > 0 ? '${placeholder}' : '${placeholderEmpty}' }}` : placeholder - data-ng-model=model - data-ng-disabled=disabled && `${disabled}` - data-ng-required=required && `${required}` + ng-model=model + ng-disabled=disabled && `${disabled}` + ng-required=required && `${required}` bs-select bs-options=`item.value as item.label for item in ${options}` + expose-ignite-form-field-control='$input' data-multiple=multiple ? '1' : false tabindex='0' - - data-ignite-form-panel-field='' )&attributes(attributes.attributes) - .ignite-form-field - +ignite-form-field__label(label, name, required) - .ignite-form-field__control + .ignite-form-field.ignite-form-field-dropdown + +ignite-form-field__label(label, name, required, disabled) +tooltip(tip, tipOpts) - + .ignite-form-field__control + .input-tip + +form-field-input(attributes=attributes) + .ignite-form-field__errors( + ng-messages=`$input.$error` + ng-show=`($input.$dirty || $input.$touched || $input.$submitted) && $input.$invalid` + ) if block block - .input-tip - +form-field-input(attributes=attributes) + if required + +form-field-feedback(name, 'required', multiple ? 'At least one option should be selected' : 'An option should be selected') + +mixin sane-ignite-form-field-dropdown({label, model, name, disabled = false, required = false, multiple = false, placeholder, placeholderEmpty, options, tip}) + +ignite-form-field-dropdown(label, model, name, disabled, required, multiple, placeholder, placeholderEmpty, options, tip)&attributes(attributes) + if block + block \ No newline at end of file http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/frontend/app/helpers/jade/form/form-field-feedback.pug ---------------------------------------------------------------------- diff --git a/modules/web-console/frontend/app/helpers/jade/form/form-field-feedback.pug b/modules/web-console/frontend/app/helpers/jade/form/form-field-feedback.pug index c70e7a3..2fa0a3c 100644 --- a/modules/web-console/frontend/app/helpers/jade/form/form-field-feedback.pug +++ b/modules/web-console/frontend/app/helpers/jade/form/form-field-feedback.pug @@ -15,18 +15,4 @@ limitations under the License. mixin form-field-feedback(name, error, message) - -var __field = `${form}[${name}]` - -var __error = `${__field}.$error.${error}` - -var __pristine = `${__field}.$pristine` - - i.fa.fa-exclamation-triangle.form-field-feedback( - ng-if=`!${__pristine} && ${__error}` - name=`{{ ${name} }}` - - bs-tooltip - data-title=message - - ignite-error=error - ignite-error-message=message - ignite-restore-input-focus - ) + div(ng-message=error) #{message} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/frontend/app/helpers/jade/form/form-field-label.pug ---------------------------------------------------------------------- diff --git a/modules/web-console/frontend/app/helpers/jade/form/form-field-label.pug b/modules/web-console/frontend/app/helpers/jade/form/form-field-label.pug index d0275c9..2edd115 100644 --- a/modules/web-console/frontend/app/helpers/jade/form/form-field-label.pug +++ b/modules/web-console/frontend/app/helpers/jade/form/form-field-label.pug @@ -14,10 +14,12 @@ See the License for the specific language governing permissions and limitations under the License. -mixin ignite-form-field__label(label, name, required) +mixin ignite-form-field__label(label, name, required, disabled) label.ignite-form-field__label( id=`{{ ${name} }}Label` for=`{{ ${name} }}Input` - class=`{{ ${required} ? 'required' : '' }}` + ng-class=disabled && `{'ignite-form-field__label-disabled': ${disabled}}` ) - span !{label} + span(class=`{{ ${required} ? 'required' : '' }}`) !{label} + if block + block \ No newline at end of file
