http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/frontend/app/components/page-configure/defaultNames.js ---------------------------------------------------------------------- diff --git a/modules/web-console/frontend/app/components/page-configure/defaultNames.js b/modules/web-console/frontend/app/components/page-configure/defaultNames.js new file mode 100644 index 0000000..abc3afc --- /dev/null +++ b/modules/web-console/frontend/app/components/page-configure/defaultNames.js @@ -0,0 +1,23 @@ +/* + * 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 const defaultNames = { + cluster: 'Cluster', + cache: 'Cache', + igfs: 'IGFS', + importedCluster: 'ImportedCluster' +};
http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/frontend/app/components/page-configure/index.d.ts ---------------------------------------------------------------------- diff --git a/modules/web-console/frontend/app/components/page-configure/index.d.ts b/modules/web-console/frontend/app/components/page-configure/index.d.ts new file mode 100644 index 0000000..96773fa --- /dev/null +++ b/modules/web-console/frontend/app/components/page-configure/index.d.ts @@ -0,0 +1,151 @@ +/* + * 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' +/// <reference path="./types/uirouter.d.ts" /> + +declare namespace ig { + type menu<T> = Array<{value: T, label: string}> + + namespace config { + namespace formFieldSize { + interface ISizeTypeOption { + label: string, + value: number + } + type ISizeType = Array<ISizeTypeOption> + interface ISizeTypes { + [name: string]: ISizeType + } + } + namespace cluster { + export type DiscoveryKinds = 'Vm' + | 'Multicast' + | 'S3' + | 'Cloud' + | 'GoogleStorage' + | 'Jdbc' + | 'SharedFs' + | 'ZooKeeper' + | 'Kubernetes' + + export type LoadBalancingKinds = 'RoundRobin' + | 'Adaptive' + | 'WeightedRandom' + | 'Custom' + + export type FailoverSPIs = 'JobStealing' | 'Never' | 'Always' | 'Custom' + + export interface ShortCluster { + _id: string, + name: string, + discovery: DiscoveryKinds, + caches: number, + models: number, + igfs: number + } + } + namespace cache { + type CacheModes = 'PARTITIONED' | 'REPLICATED' | 'LOCAL' + type AtomicityModes = 'ATOMIC' | 'TRANSACTIONAL' + export interface ShortCache { + _id: string, + cacheMode: CacheModes, + atomicityMode: AtomicityModes, + backups: number + } + } + namespace model { + type QueryMetadataTypes = 'Annotations' | 'Configuration' + type DomainModelKinds = 'query' | 'store' | 'both' + export interface KeyField { + databaseFieldName: string, + databaseFieldType: string, + javaFieldName: string, + javaFieldType: string + } + export interface ValueField { + databaseFieldName: string, + databaseFieldType: string, + javaFieldName: string, + javaFieldType: string + } + interface Field { + name: string, + className: string + } + interface Alias { + field: string, + alias: string + } + type IndexTypes = 'SORTED' | 'FULLTEXT' | 'GEOSPATIAL' + export interface IndexField { + _id: string, + name?: string, + direction?: boolean + } + export interface Index { + _id: string, + name: string, + indexType: IndexTypes, + fields: Array<IndexField> + } + + export interface DomainModel { + _id: string, + space?: string, + clusters?: Array<string>, + caches?: Array<string>, + queryMetadata?: QueryMetadataTypes, + kind?: DomainModelKinds, + tableName?: string, + keyFieldName?: string, + valueFieldName?: string, + databaseSchema?: string, + databaseTable?: string, + keyType?: string, + valueType?: string, + keyFields?: Array<KeyField>, + valueFields?: Array<ValueField>, + queryKeyFields?: Array<string>, + fields?: Array<Field>, + aliases?: Array<Alias>, + indexes?: Array<Index>, + generatePojo?: boolean + } + + export interface ShortDomainModel { + _id: string, + keyType: string, + valueType: string, + hasIndex: boolean + } + } + namespace igfs { + type DefaultModes = 'PRIMARY' | 'PROXY' | 'DUAL_SYNC' | 'DUAL_ASYNC' + export interface ShortIGFS { + _id: string, + name: string, + defaultMode: DefaultModes, + affinnityGroupSize: number + } + } + } +} + +export as namespace ig +export = ig \ 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/index.js ---------------------------------------------------------------------- diff --git a/modules/web-console/frontend/app/components/page-configure/index.js b/modules/web-console/frontend/app/components/page-configure/index.js index 8c4e3c2..5874df5 100644 --- a/modules/web-console/frontend/app/components/page-configure/index.js +++ b/modules/web-console/frontend/app/components/page-configure/index.js @@ -16,17 +16,147 @@ */ import angular from 'angular'; + +import 'angular1-async-filter'; +import {UIRouterRx} from '@uirouter/rx'; +import {Visualizer} from '@uirouter/visualizer'; +import uiValidate from 'angular-ui-validate'; + import component from './component'; import ConfigureState from './services/ConfigureState'; import PageConfigure from './services/PageConfigure'; import ConfigurationDownload from './services/ConfigurationDownload'; +import ConfigChangesGuard from './services/ConfigChangesGuard'; +import ConfigSelectionManager from './services/ConfigSelectionManager'; +import selectors from './store/selectors'; +import effects from './store/effects'; + +import projectStructurePreview from './components/modal-preview-project'; +import itemsTable from './components/pc-items-table'; +import pcUiGridFilters from './components/pc-ui-grid-filters'; +import pcFormFieldSize from './components/pc-form-field-size'; +import isInCollection from './components/pcIsInCollection'; +import pcValidation from './components/pcValidation'; +import fakeUiCanExit from './components/fakeUICanExit'; +import formUICanExitGuard from './components/formUICanExitGuard'; +import modalImportModels from './components/modal-import-models'; +import buttonImportModels from './components/button-import-models'; +import buttonDownloadProject from './components/button-download-project'; +import buttonPreviewProject from './components/button-preview-project'; + +import {errorState} from './transitionHooks/errorState'; + +import 'rxjs/add/operator/withLatestFrom'; +import 'rxjs/add/operator/skip'; + +import {Observable} from 'rxjs/Observable'; +Observable.prototype.debug = function(l) { + return this.do((v) => console.log(l, v), (e) => console.error(l, e), () => console.log(l, 'completed')); +}; + +import { + editReducer2, + reducer, + editReducer, + loadingReducer, + itemsEditReducerFactory, + mapStoreReducerFactory, + mapCacheReducerFactory, + basicCachesActionTypes, + clustersActionTypes, + shortClustersActionTypes, + cachesActionTypes, + shortCachesActionTypes, + modelsActionTypes, + shortModelsActionTypes, + igfssActionTypes, + shortIGFSsActionTypes, + refsReducer +} from './reducer'; +import {reducer as reduxDevtoolsReducer, devTools} from './reduxDevtoolsIntegration'; export default angular - .module('ignite-console.page-configure', []) + .module('ignite-console.page-configure', [ + 'asyncFilter', + uiValidate, + pcFormFieldSize.name, + pcUiGridFilters.name, + projectStructurePreview.name, + itemsTable.name, + pcValidation.name, + modalImportModels.name, + buttonImportModels.name, + buttonDownloadProject.name, + buttonPreviewProject.name + ]) .config(['DefaultStateProvider', (DefaultState) => { - DefaultState.setRedirectTo(() => 'base.configuration.tabs'); + DefaultState.setRedirectTo(() => 'base.configuration.overview'); + }]) + .run(['ConfigEffects', 'ConfigureState', '$uiRouter', (ConfigEffects, ConfigureState, $uiRouter) => { + $uiRouter.plugin(UIRouterRx); + // $uiRouter.plugin(Visualizer); + if (devTools) { + devTools.subscribe((e) => { + if (e.type === 'DISPATCH' && e.state) ConfigureState.actions$.next(e); + }); + + ConfigureState.actions$ + .filter((e) => e.type !== 'DISPATCH') + .withLatestFrom(ConfigureState.state$.skip(1)) + .subscribe(([action, state]) => devTools.send(action, state)); + + ConfigureState.addReducer(reduxDevtoolsReducer); + } + ConfigureState.addReducer(refsReducer({ + models: {at: 'domains', store: 'caches'}, + caches: {at: 'caches', store: 'models'} + })); + ConfigureState.addReducer((state, action) => Object.assign({}, state, { + clusterConfiguration: editReducer(state.clusterConfiguration, action), + configurationLoading: loadingReducer(state.configurationLoading, action), + basicCaches: itemsEditReducerFactory(basicCachesActionTypes)(state.basicCaches, action), + clusters: mapStoreReducerFactory(clustersActionTypes)(state.clusters, action), + shortClusters: mapCacheReducerFactory(shortClustersActionTypes)(state.shortClusters, action), + caches: mapStoreReducerFactory(cachesActionTypes)(state.caches, action), + shortCaches: mapCacheReducerFactory(shortCachesActionTypes)(state.shortCaches, action), + models: mapStoreReducerFactory(modelsActionTypes)(state.models, action), + shortModels: mapCacheReducerFactory(shortModelsActionTypes)(state.shortModels, action), + igfss: mapStoreReducerFactory(igfssActionTypes)(state.igfss, action), + shortIgfss: mapCacheReducerFactory(shortIGFSsActionTypes)(state.shortIgfss, action), + edit: editReducer2(state.edit, action) + })); + ConfigureState.addReducer((state, action) => { + switch (action.type) { + case 'APPLY_ACTIONS_UNDO': + return action.state; + default: + return state; + } + }); + const la = ConfigureState.actions$.scan((acc, action) => [...acc, action], []); + + ConfigureState.actions$ + .filter((a) => a.type === 'UNDO_ACTIONS') + .withLatestFrom(la, ({actions}, actionsWindow, initialState) => { + return { + type: 'APPLY_ACTIONS_UNDO', + state: actionsWindow.filter((a) => !actions.includes(a)).reduce(ConfigureState._combinedReducer, {}) + }; + }) + .debug('UNDOED') + .do((a) => ConfigureState.dispatchAction(a)) + .subscribe(); + ConfigEffects.connect(); }]) .component('pageConfigure', component) + .directive(isInCollection.name, isInCollection) + .directive(fakeUiCanExit.name, fakeUiCanExit) + .directive(formUICanExitGuard.name, formUICanExitGuard) + .factory('configSelectionManager', ConfigSelectionManager) + .service('ConfigSelectors', selectors) + .service('ConfigEffects', effects) + .service('ConfigChangesGuard', ConfigChangesGuard) .service('PageConfigure', PageConfigure) .service('ConfigureState', ConfigureState) - .service('ConfigurationDownload', ConfigurationDownload); + .service('ConfigurationDownload', ConfigurationDownload) + .run(errorState); http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/frontend/app/components/page-configure/reducer.js ---------------------------------------------------------------------- diff --git a/modules/web-console/frontend/app/components/page-configure/reducer.js b/modules/web-console/frontend/app/components/page-configure/reducer.js index 306649d..f959b0a 100644 --- a/modules/web-console/frontend/app/components/page-configure/reducer.js +++ b/modules/web-console/frontend/app/components/page-configure/reducer.js @@ -15,9 +15,12 @@ * limitations under the License. */ +import difference from 'lodash/difference'; + export const LOAD_LIST = Symbol('LOAD_LIST'); export const ADD_CLUSTER = Symbol('ADD_CLUSTER'); -export const REMOVE_CLUSTER = Symbol('REMOVE_CLUSTER'); +export const ADD_CLUSTERS = Symbol('ADD_CLUSTERS'); +export const REMOVE_CLUSTERS = Symbol('REMOVE_CLUSTERS'); export const UPDATE_CLUSTER = Symbol('UPDATE_CLUSTER'); export const UPSERT_CLUSTERS = Symbol('UPSERT_CLUSTERS'); export const ADD_CACHE = Symbol('ADD_CACHE'); @@ -25,9 +28,13 @@ export const UPDATE_CACHE = Symbol('UPDATE_CACHE'); export const UPSERT_CACHES = Symbol('UPSERT_CACHES'); export const REMOVE_CACHE = Symbol('REMOVE_CACHE'); +import { + REMOVE_CLUSTER_ITEMS_CONFIRMED +} from './store/actionTypes'; + const defaults = {clusters: new Map(), caches: new Map(), spaces: new Map()}; -const mapByID = (array) => { - return new Map(array.map((item) => [item._id, item])); +const mapByID = (items) => { + return Array.isArray(items) ? new Map(items.map((item) => [item._id, item])) : new Map(items); }; export const reducer = (state = defaults, action) => { @@ -35,8 +42,10 @@ export const reducer = (state = defaults, action) => { case LOAD_LIST: { return { clusters: mapByID(action.list.clusters), + domains: mapByID(action.list.domains), caches: mapByID(action.list.caches), - spaces: mapByID(action.list.spaces) + spaces: mapByID(action.list.spaces), + plugins: mapByID(action.list.plugins) }; } case ADD_CLUSTER: { @@ -44,12 +53,25 @@ export const reducer = (state = defaults, action) => { clusters: new Map([...state.clusters.entries(), [action.cluster._id, action.cluster]]) }); } - case REMOVE_CLUSTER: - return state; + case ADD_CLUSTERS: { + return Object.assign({}, state, { + clusters: new Map([...state.clusters.entries(), ...action.clusters.map((c) => [c._id, c])]) + }); + } + case REMOVE_CLUSTERS: { + return Object.assign({}, state, { + clusters: new Map([...state.clusters.entries()].filter(([id, value]) => !action.clusterIDs.includes(id))) + }); + } case UPDATE_CLUSTER: { - const id = action.cluster._id; + const id = action._id || action.cluster._id; return Object.assign({}, state, { - clusters: new Map(state.clusters).set(id, Object.assign({}, state.clusters.get(id), action.cluster)) + // clusters: new Map(state.clusters).set(id, Object.assign({}, state.clusters.get(id), action.cluster)) + clusters: new Map(Array.from(state.clusters.entries()).map(([_id, cluster]) => { + return _id === id + ? [action.cluster._id || _id, Object.assign({}, cluster, action.cluster)] + : [_id, cluster]; + })) }); } case UPSERT_CLUSTERS: { @@ -81,3 +103,318 @@ export const reducer = (state = defaults, action) => { return state; } }; + + +export const RECEIVE_CLUSTER_EDIT = Symbol('RECEIVE_CLUSTER_EDIT'); +export const RECEIVE_CACHE_EDIT = Symbol('RECEIVE_CACHE_EDIT'); +export const RECEIVE_IGFSS_EDIT = Symbol('RECEIVE_IGFSS_EDIT'); +export const RECEIVE_IGFS_EDIT = Symbol('RECEIVE_IGFS_EDIT'); +export const RECEIVE_MODELS_EDIT = Symbol('RECEIVE_MODELS_EDIT'); +export const RECEIVE_MODEL_EDIT = Symbol('RECEIVE_MODEL_EDIT'); + +export const editReducer = (state = {originalCluster: null}, action) => { + switch (action.type) { + case RECEIVE_CLUSTER_EDIT: + return { + ...state, + originalCluster: action.cluster + }; + case RECEIVE_CACHE_EDIT: { + return { + ...state, + originalCache: action.cache + }; + } + case RECEIVE_IGFSS_EDIT: + return { + ...state, + originalIGFSs: action.igfss + }; + case RECEIVE_IGFS_EDIT: { + return { + ...state, + originalIGFS: action.igfs + }; + } + case RECEIVE_MODELS_EDIT: + return { + ...state, + originalModels: action.models + }; + case RECEIVE_MODEL_EDIT: { + return { + ...state, + originalModel: action.model + }; + } + default: + return state; + } +}; + +export const SHOW_CONFIG_LOADING = Symbol('SHOW_CONFIG_LOADING'); +export const HIDE_CONFIG_LOADING = Symbol('HIDE_CONFIG_LOADING'); +const loadingDefaults = {isLoading: false, loadingText: 'Loading...'}; + +export const loadingReducer = (state = loadingDefaults, action) => { + switch (action.type) { + case SHOW_CONFIG_LOADING: + return {...state, isLoading: true, loadingText: action.loadingText}; + case HIDE_CONFIG_LOADING: + return {...state, isLoading: false}; + default: + return state; + } +}; + +export const setStoreReducerFactory = (actionTypes) => (state = new Set(), action = {}) => { + switch (action.type) { + case actionTypes.SET: + return new Set(action.items.map((i) => i._id)); + case actionTypes.RESET: + return new Set(); + case actionTypes.UPSERT: + return action.items.reduce((acc, item) => {acc.add(item._id); return acc;}, new Set(state)); + case actionTypes.REMOVE: + return action.items.reduce((acc, item) => {acc.delete(item); return acc;}, new Set(state)); + default: + return state; + } +}; + +export const mapStoreReducerFactory = (actionTypes) => (state = new Map(), action = {}) => { + switch (action.type) { + case actionTypes.SET: + return new Map(action.items.map((i) => [i._id, i])); + case actionTypes.RESET: + return new Map(); + case actionTypes.UPSERT: + if (!action.items.length) return state; + return action.items.reduce((acc, item) => {acc.set(item._id, item); return acc;}, new Map(state)); + case actionTypes.REMOVE: + if (!action.ids.length) return state; + return action.ids.reduce((acc, id) => {acc.delete(id); return acc;}, new Map(state)); + default: + return state; + } +}; + +export const mapCacheReducerFactory = (actionTypes) => { + const mapStoreReducer = mapStoreReducerFactory(actionTypes); + return (state = {value: mapStoreReducer(), pristine: true}, action) => { + switch (action.type) { + case actionTypes.SET: + case actionTypes.REMOVE: + case actionTypes.UPSERT: + return { + value: mapStoreReducer(state.value, action), + pristine: false + }; + case actionTypes.RESET: + return { + value: mapStoreReducer(state.value, action), + pristine: true + }; + default: + return state; + } + }; +}; + +export const basicCachesActionTypes = { + SET: 'SET_BASIC_CACHES', + RESET: 'RESET_BASIC_CACHES', + LOAD: 'LOAD_BASIC_CACHES', + UPSERT: 'UPSERT_BASIC_CACHES', + REMOVE: 'REMOVE_BASIC_CACHES' +}; + +export const mapStoreActionTypesFactory = (NAME) => ({ + SET: `SET_${NAME}`, + RESET: `RESET_${NAME}`, + UPSERT: `UPSERT_${NAME}`, + REMOVE: `REMOVE_${NAME}` +}); + +export const clustersActionTypes = mapStoreActionTypesFactory('CLUSTERS'); +export const shortClustersActionTypes = mapStoreActionTypesFactory('SHORT_CLUSTERS'); +export const cachesActionTypes = mapStoreActionTypesFactory('CACHES'); +export const shortCachesActionTypes = mapStoreActionTypesFactory('SHORT_CACHES'); +export const modelsActionTypes = mapStoreActionTypesFactory('MODELS'); +export const shortModelsActionTypes = mapStoreActionTypesFactory('SHORT_MODELS'); +export const igfssActionTypes = mapStoreActionTypesFactory('IGFSS'); +export const shortIGFSsActionTypes = mapStoreActionTypesFactory('SHORT_IGFSS'); + +export const itemsEditReducerFactory = (actionTypes) => { + const setStoreReducer = setStoreReducerFactory(actionTypes); + const mapStoreReducer = mapStoreReducerFactory(actionTypes); + return (state = {ids: setStoreReducer(), changedItems: mapStoreReducer()}, action) => { + switch (action.type) { + case actionTypes.SET: + return action.state; + case actionTypes.LOAD: + return { + ...state, + ids: setStoreReducer(state.ids, {...action, type: actionTypes.UPSERT}) + }; + case actionTypes.RESET: + case actionTypes.UPSERT: + return { + ids: setStoreReducer(state.ids, action), + changedItems: mapStoreReducer(state.changedItems, action) + }; + case actionTypes.REMOVE: + return { + ids: setStoreReducer(state.ids, {type: action.type, items: action.ids}), + changedItems: mapStoreReducer(state.changedItems, action) + }; + default: + return state; + } + }; +}; + +export const editReducer2 = (state = editReducer2.getDefaults(), action) => { + switch (action.type) { + case 'SET_EDIT': + return action.state; + case 'EDIT_CLUSTER': { + return { + ...state, + changes: { + ...['caches', 'models', 'igfss'].reduce((a, t) => ({ + ...a, + [t]: { + ids: action.cluster ? action.cluster[t] || [] : [], + changedItems: [] + } + }), state.changes), + cluster: action.cluster + } + }; + } + case 'RESET_EDIT_CHANGES': { + return { + ...state, + changes: { + ...['caches', 'models', 'igfss'].reduce((a, t) => ({ + ...a, + [t]: { + ids: state.changes.cluster ? state.changes.cluster[t] || [] : [], + changedItems: [] + } + }), state.changes), + cluster: {...state.changes.cluster} + } + }; + } + case 'UPSERT_CLUSTER': { + return { + ...state, + changes: { + ...state.changes, + cluster: action.cluster + } + }; + } + case 'UPSERT_CLUSTER_ITEM': { + const {itemType, item} = action; + return { + ...state, + changes: { + ...state.changes, + [itemType]: { + ids: state.changes[itemType].ids.filter((_id) => _id !== item._id).concat(item._id), + changedItems: state.changes[itemType].changedItems.filter(({_id}) => _id !== item._id).concat(item) + } + } + }; + } + case REMOVE_CLUSTER_ITEMS_CONFIRMED: { + const {itemType, itemIDs} = action; + return { + ...state, + changes: { + ...state.changes, + [itemType]: { + ids: state.changes[itemType].ids.filter((_id) => !itemIDs.includes(_id)), + changedItems: state.changes[itemType].changedItems.filter(({_id}) => !itemIDs.includes(_id)) + } + } + }; + } + default: return state; + } +}; +editReducer2.getDefaults = () => ({ + changes: ['caches', 'models', 'igfss'].reduce((a, t) => ({...a, [t]: {ids: [], changedItems: []}}), {cluster: null}) +}); + +export const refsReducer = (refs) => (state, action) => { + switch (action.type) { + case 'ADVANCED_SAVE_COMPLETE_CONFIGURATION': { + const newCluster = action.changedItems.cluster; + const oldCluster = state.clusters.get(newCluster._id) || {}; + const val = Object.keys(refs).reduce((state, ref) => { + if (!state || !state[refs[ref].store].size) return state; + + const addedSources = new Set(difference(newCluster[ref], oldCluster[ref] || [])); + const removedSources = new Set(difference(oldCluster[ref] || [], newCluster[ref])); + const changedSources = new Map(action.changedItems[ref].map((m) => [m._id, m])); + + const targets = new Map(); + const maybeTarget = (id) => { + if (!targets.has(id)) targets.set(id, {[refs[ref].at]: {add: new Set(), remove: new Set()}}); + return targets.get(id); + }; + + [...state[refs[ref].store].values()].forEach((target) => { + target[refs[ref].at] + .filter((sourceID) => removedSources.has(sourceID)) + .forEach((sourceID) => maybeTarget(target._id)[refs[ref].at].remove.add(sourceID)); + }); + [...addedSources.values()].forEach((sourceID) => { + (changedSources.get(sourceID)[refs[ref].store] || []).forEach((targetID) => { + maybeTarget(targetID)[refs[ref].at].add.add(sourceID); + }); + }); + action.changedItems[ref].filter((s) => !addedSources.has(s._id)).forEach((source) => { + const newSource = source; + const oldSource = state[ref].get(source._id); + const addedTargets = difference(newSource[refs[ref].store], oldSource[refs[ref].store]); + const removedCaches = difference(oldSource[refs[ref].store], newSource[refs[ref].store]); + addedTargets.forEach((targetID) => { + maybeTarget(targetID)[refs[ref].at].add.add(source._id); + }); + removedCaches.forEach((targetID) => { + maybeTarget(targetID)[refs[ref].at].remove.add(source._id); + }); + }); + const result = [...targets.entries()] + .filter(([targetID]) => state[refs[ref].store].has(targetID)) + .map(([targetID, changes]) => { + const target = state[refs[ref].store].get(targetID); + return [ + targetID, + { + ...target, + [refs[ref].at]: target[refs[ref].at] + .filter((sourceID) => !changes[refs[ref].at].remove.has(sourceID)) + .concat([...changes[refs[ref].at].add.values()]) + } + ]; + }); + + return result.length + ? { + ...state, + [refs[ref].store]: new Map([...state[refs[ref].store].entries()].concat(result)) + } + : state; + }, state); + return val; + } + default: + return state; + } +}; http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/frontend/app/components/page-configure/reducer.spec.js ---------------------------------------------------------------------- diff --git a/modules/web-console/frontend/app/components/page-configure/reducer.spec.js b/modules/web-console/frontend/app/components/page-configure/reducer.spec.js index 96fc76c..fb47973 100644 --- a/modules/web-console/frontend/app/components/page-configure/reducer.spec.js +++ b/modules/web-console/frontend/app/components/page-configure/reducer.spec.js @@ -19,7 +19,6 @@ import {suite, test} from 'mocha'; import {assert} from 'chai'; import { - LOAD_LIST, ADD_CLUSTER, REMOVE_CLUSTER, UPDATE_CLUSTER, @@ -31,7 +30,7 @@ import { reducer } from './reducer'; -suite('page-configure component reducer', () => { +suite.skip('page-configure component reducer', () => { test('Default state', () => { assert.deepEqual( reducer(void 0, {}), @@ -42,24 +41,6 @@ suite('page-configure component reducer', () => { } ); }); - test('LOAD_LIST action', () => { - assert.deepEqual( - reducer(void 0, { - type: LOAD_LIST, - list: { - clusters: [{_id: 1}, {_id: 2}, {_id: 3}], - caches: [{_id: 1}, {_id: 2}], - spaces: [{_id: 1}] - } - }), - { - clusters: new Map([[1, {_id: 1}], [2, {_id: 2}], [3, {_id: 3}]]), - caches: new Map([[1, {_id: 1}], [2, {_id: 2}]]), - spaces: new Map([[1, {_id: 1}]]) - }, - 'loads caches, clusters and spaces from list into maps with _id as keys' - ); - }); test('ADD_CLUSTER action', () => { assert.deepEqual( reducer( http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/frontend/app/components/page-configure/reduxDevtoolsIntegration.js ---------------------------------------------------------------------- diff --git a/modules/web-console/frontend/app/components/page-configure/reduxDevtoolsIntegration.js b/modules/web-console/frontend/app/components/page-configure/reduxDevtoolsIntegration.js new file mode 100644 index 0000000..43fa323 --- /dev/null +++ b/modules/web-console/frontend/app/components/page-configure/reduxDevtoolsIntegration.js @@ -0,0 +1,75 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export let devTools; + +const replacer = (key, value) => { + if (value instanceof Map) { + return { + data: [...value.entries()], + __serializedType__: 'Map' + }; + } + if (value instanceof Set) { + return { + data: [...value.values()], + __serializedType__: 'Set' + }; + } + if (value instanceof Symbol) { + return { + data: String(value), + __serializedType__: 'Symbol' + }; + } + return value; +}; + +const reviver = (key, value) => { + if (typeof value === 'object' && value !== null && '__serializedType__' in value) { + const data = value.data; + switch (value.__serializedType__) { + case 'Map': + return new Map(value.data); + case 'Set': + return new Set(value.data); + default: + return data; + } + } + return value; +}; + +if (window.__REDUX_DEVTOOLS_EXTENSION__) { + devTools = window.__REDUX_DEVTOOLS_EXTENSION__.connect({ + name: 'Ignite configuration', + serialize: { + replacer, + reviver + } + }); +} + +export const reducer = (state, action) => { + switch (action.type) { + case 'DISPATCH': + case 'JUMP_TO_STATE': + return JSON.parse(action.state, reviver); + default: + return state; + } +}; http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/frontend/app/components/page-configure/services/ConfigChangesGuard.js ---------------------------------------------------------------------- diff --git a/modules/web-console/frontend/app/components/page-configure/services/ConfigChangesGuard.js b/modules/web-console/frontend/app/components/page-configure/services/ConfigChangesGuard.js new file mode 100644 index 0000000..758cc11 --- /dev/null +++ b/modules/web-console/frontend/app/components/page-configure/services/ConfigChangesGuard.js @@ -0,0 +1,66 @@ +/* + * 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 {of} from 'rxjs/observable/of'; +import {Confirm} from 'app/services/Confirm.service'; +import {diff} from 'jsondiffpatch'; +import {html} from 'jsondiffpatch/public/build/jsondiffpatch-formatters.js'; +import 'jsondiffpatch/public/formatters-styles/html.css'; + +export default class ConfigChangesGuard { + static $inject = [Confirm.name, '$sce']; + + /** + * @param {Confirm} Confirm + * @param {ng.ISCEService} $sce + */ + constructor(Confirm, $sce) { + this.Confirm = Confirm; + this.$sce = $sce; + } + + _hasChanges(a, b) { + return diff(a, b); + } + + _confirm(changes) { + return this.Confirm.confirm(this.$sce.trustAsHtml(` + <p> + You have unsaved changes. + Are you sure you want to discard them? + </p> + <details> + <summary>Click here to see changes</summary> + <div style='max-height: 400px; overflow: auto;'>${html.format(changes)}</div> + </details> + `)); + } + + /** + * Compares values and asks user if he wants to continue. + * @template T + * @param {T} a - Left comparison value + * @param {T} b - Right comparison value + */ + guard(a, b) { + if (!a && !b) return Promise.resolve(true); + return of(this._hasChanges(a, b)) + .switchMap((changes) => changes ? this._confirm(changes).then(() => true) : of(true)) + .catch(() => of(false)) + .toPromise(); + } +} http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/frontend/app/components/page-configure/services/ConfigSelectionManager.js ---------------------------------------------------------------------- diff --git a/modules/web-console/frontend/app/components/page-configure/services/ConfigSelectionManager.js b/modules/web-console/frontend/app/components/page-configure/services/ConfigSelectionManager.js new file mode 100644 index 0000000..243302a --- /dev/null +++ b/modules/web-console/frontend/app/components/page-configure/services/ConfigSelectionManager.js @@ -0,0 +1,93 @@ +/* + * 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 {merge} from 'rxjs/observable/merge'; +import {RejectType} from '@uirouter/angularjs'; +import 'rxjs/add/operator/share'; +import 'rxjs/add/operator/mapTo'; +import 'rxjs/add/operator/startWith'; +import isEqual from 'lodash/isEqual'; + +/** + * @param {uirouter.TransitionService} $transitions + */ +export default function configSelectionManager($transitions) { + /** + * Determines what items should be marked as selected and if something is being edited at the moment. + */ + return ({itemID$, selectedItemRows$, visibleRows$, loadedItems$}) => { + // Aborted transitions happen when form has unsaved changes, user attempts to leave + // but decides to stay after screen asks for leave confirmation. + const abortedTransitions$ = Observable.create((observer) => { + return $transitions.onError({}, (t) => observer.next(t)); + }) + .filter((t) => t.error().type === RejectType.ABORTED); + + const firstItemID$ = visibleRows$.withLatestFrom(itemID$, loadedItems$) + .filter(([rows, id, items]) => !id && rows && rows.length === items.length) + .pluck('0', '0', 'entity', '_id'); + + const selectedItemRowsIDs$ = selectedItemRows$.map((rows) => rows.map((r) => r._id)).share(); + const singleSelectionEdit$ = selectedItemRows$.filter((r) => r && r.length === 1).pluck('0', '_id'); + const selectedMultipleOrNone$ = selectedItemRows$.filter((r) => r.length > 1 || r.length === 0); + const loadedItemIDs$ = loadedItems$.map((rows) => new Set(rows.map((r) => r._id))).share(); + const currentItemWasRemoved$ = loadedItemIDs$ + .withLatestFrom( + itemID$.filter((v) => v && v !== 'new'), + /** + * Without startWith currentItemWasRemoved$ won't emit in the following scenario: + * 1. User opens items page (no item id in location). + * 2. Selection manager commands to edit first item. + * 3. User removes said item. + */ + selectedItemRowsIDs$.startWith([]) + ) + .filter(([existingIDs, itemID, selectedIDs]) => !existingIDs.has(itemID)) + .map(([existingIDs, itemID, selectedIDs]) => selectedIDs.filter((id) => id !== itemID)) + .share(); + + // Edit first loaded item or when there's only one item selected + const editGoes$ = merge(firstItemID$, singleSelectionEdit$) + // Don't go to non-existing items. + // Happens when user naviagtes to older history and some items were already removed. + .withLatestFrom(loadedItemIDs$).filter(([id, loaded]) => id && loaded.has(id)).pluck('0'); + // Stop edit when multiple or none items are selected or when current item was removed + const editLeaves$ = merge( + selectedMultipleOrNone$.mapTo({}), + currentItemWasRemoved$.mapTo({location: 'replace', custom: {justIDUpdate: true}}) + ).share(); + + const selectedItemIDs$ = merge( + // Select nothing when creating an item or select current item + itemID$.filter((id) => id).map((id) => id === 'new' ? [] : [id]), + // Restore previous item selection when transition gets aborted + abortedTransitions$.withLatestFrom(itemID$, (_, id) => [id]), + // Select all incoming selected rows + selectedItemRowsIDs$ + ) + // If nothing's selected and there are zero rows, ui-grid will behave as if all rows are selected + .startWith([]) + // Some scenarios cause same item to be selected multiple times in a row, + // so it makes sense to filter out duplicate entries + .distinctUntilChanged(isEqual) + .share(); + + return {selectedItemIDs$, editGoes$, editLeaves$}; + }; +} +configSelectionManager.$inject = ['$transitions']; http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/frontend/app/components/page-configure/services/ConfigurationDownload.js ---------------------------------------------------------------------- diff --git a/modules/web-console/frontend/app/components/page-configure/services/ConfigurationDownload.js b/modules/web-console/frontend/app/components/page-configure/services/ConfigurationDownload.js index 051bae6..3750e63 100644 --- a/modules/web-console/frontend/app/components/page-configure/services/ConfigurationDownload.js +++ b/modules/web-console/frontend/app/components/page-configure/services/ConfigurationDownload.js @@ -25,19 +25,24 @@ export default class ConfigurationDownload { 'IgniteSummaryZipper', 'IgniteVersion', '$q', - '$rootScope' + '$rootScope', + 'PageConfigure' ]; - constructor(messages, activitiesData, configuration, summaryZipper, Version, $q, $rootScope) { - Object.assign(this, {messages, activitiesData, configuration, summaryZipper, Version, $q, $rootScope}); + constructor(messages, activitiesData, configuration, summaryZipper, Version, $q, $rootScope, PageConfigure) { + Object.assign(this, {messages, activitiesData, configuration, summaryZipper, Version, $q, $rootScope, PageConfigure}); this.saver = saver; } + /** + * @param {{_id: string, name: string}} cluster + * @returns {Promise} + */ downloadClusterConfiguration(cluster) { this.activitiesData.post({action: '/configuration/download'}); - return this.configuration.read() + return this.PageConfigure.getClusterConfiguration({clusterID: cluster._id, isDemo: !!this.$rootScope.IgniteDemoMode}) .then((data) => this.configuration.populate(data)) .then(({clusters}) => { return clusters.find(({_id}) => _id === cluster._id) @@ -51,16 +56,16 @@ export default class ConfigurationDownload { targetVer: this.Version.currentSbj.getValue() }); }) - .then((data) => { - const fileName = `${this.escapeFileName(cluster.name)}-project.zip`; - - this.saver.saveAs(data, fileName); - }) + .then((data) => this.saver.saveAs(data, this.nameFile(cluster))) .catch((e) => ( this.messages.showError(`Failed to generate project files. ${e.message}`) )); } + nameFile(cluster) { + return `${this.escapeFileName(cluster.name)}-project.zip`; + } + escapeFileName(name) { return name.replace(/[\\\/*\"\[\],\.:;|=<>?]/g, '-').replace(/ /g, '_'); } http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/frontend/app/components/page-configure/services/ConfigurationDownload.spec.js ---------------------------------------------------------------------- diff --git a/modules/web-console/frontend/app/components/page-configure/services/ConfigurationDownload.spec.js b/modules/web-console/frontend/app/components/page-configure/services/ConfigurationDownload.spec.js index 0a817d6..581993d 100644 --- a/modules/web-console/frontend/app/components/page-configure/services/ConfigurationDownload.spec.js +++ b/modules/web-console/frontend/app/components/page-configure/services/ConfigurationDownload.spec.js @@ -53,7 +53,7 @@ const saverMock = () => ({ saveAs: spy() }); -suite('page-configure, ConfigurationDownload service', () => { +suite.skip('page-configure, ConfigurationDownload service', () => { test('fails and shows error message when cluster not found', () => { const service = new Provider(...mocks().values()); http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/frontend/app/components/page-configure/services/ConfigureState.js ---------------------------------------------------------------------- diff --git a/modules/web-console/frontend/app/components/page-configure/services/ConfigureState.js b/modules/web-console/frontend/app/components/page-configure/services/ConfigureState.js index dfdea80..ea7f527 100644 --- a/modules/web-console/frontend/app/components/page-configure/services/ConfigureState.js +++ b/modules/web-console/frontend/app/components/page-configure/services/ConfigureState.js @@ -16,77 +16,39 @@ */ import {Subject} from 'rxjs/Subject'; +import {BehaviorSubject} from 'rxjs/BehaviorSubject'; +import 'rxjs/add/operator/do'; import 'rxjs/add/operator/scan'; -import 'rxjs/add/operator/share'; -import 'rxjs/add/operator/publishReplay'; -import {reducer as listReducer} from '../reducer'; -import {reducer as configureBasicReducer} from '../../page-configure-basic/reducer'; -let devTools; -const actions$ = new Subject(); - -const replacer = (key, value) => { - if (value instanceof Map) { - return { - data: [...value], - __serializedType__: 'Map' - }; - } - if (value instanceof Symbol) { - return { - data: String(value), - __serializedType__: 'Symbol' +export default class ConfigureState { + constructor() { + /** @type {Subject<{type: string}>} */ + this.actions$ = new Subject(); + this.state$ = new BehaviorSubject({}); + this._combinedReducer = (state, action) => state; + + const reducer = (state = {}, action) => { + try { + return this._combinedReducer(state, action); + } catch (e) { + console.error(e); + return state; + } }; + this.actions$.scan(reducer, {}).do((v) => this.state$.next(v)).subscribe(); } - return value; -}; -const reviver = (key, value) => { - if (typeof value === 'object' && value !== null && '__serializedType__' in value) { - const data = value.data; - switch (value.__serializedType__) { - case 'Map': - return new Map(value.data); - default: - return data; - } + addReducer(combineFn) { + const old = this._combinedReducer; + this._combinedReducer = (state, action) => combineFn(old(state, action), action); + return this; } - return value; -}; - -if (window.__REDUX_DEVTOOLS_EXTENSION__) { - devTools = window.__REDUX_DEVTOOLS_EXTENSION__.connect({ - name: 'Ignite configuration', - serialize: { - replacer, - reviver - } - }); - devTools.subscribe((e) => { - if (e.type === 'DISPATCH' && e.state) actions$.next(e); - }); -} - -const reducer = (state = {}, action) => { - if (action.type === 'DISPATCH') return JSON.parse(action.state, reviver); - const value = { - list: listReducer(state.list, action), - configureBasic: configureBasicReducer(state.configureBasic, action, state) - }; - - devTools && devTools.send(action, value); - - return value; -}; - -const state$ = actions$.scan(reducer, void 0).publishReplay(1).refCount(); - -state$.subscribe(); - -export default class ConfigureState { - state$ = state$; dispatchAction(action) { - actions$.next(action); + if (typeof action === 'function') + return action((a) => this.actions$.next(a), () => this.state$.getValue()); + + this.actions$.next(action); + return action; } } http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/frontend/app/components/page-configure/services/PageConfigure.js ---------------------------------------------------------------------- diff --git a/modules/web-console/frontend/app/components/page-configure/services/PageConfigure.js b/modules/web-console/frontend/app/components/page-configure/services/PageConfigure.js index 34a292a..10200be 100644 --- a/modules/web-console/frontend/app/components/page-configure/services/PageConfigure.js +++ b/modules/web-console/frontend/app/components/page-configure/services/PageConfigure.js @@ -15,52 +15,54 @@ * limitations under the License. */ -import { - ADD_CLUSTER, - UPDATE_CLUSTER, - UPSERT_CLUSTERS, - LOAD_LIST, - UPSERT_CACHES -} from '../reducer'; - -export default class PageConfigure { - static $inject = ['IgniteConfigurationResource', '$state', '$q', 'ConfigureState']; - - constructor(configuration, $state, $q, ConfigureState) { - Object.assign(this, {configuration, $state, $q, ConfigureState}); - } - - onStateEnterRedirect(toState) { - if (toState.name !== 'base.configuration.tabs') - return this.$q.resolve(); +import {Observable} from 'rxjs/Observable'; +import 'rxjs/add/operator/filter'; +import 'rxjs/add/operator/map'; +import 'rxjs/add/operator/take'; +import 'rxjs/add/operator/switchMap'; +import 'rxjs/add/operator/merge'; +import 'rxjs/add/operator/catch'; +import 'rxjs/add/operator/withLatestFrom'; +import 'rxjs/add/observable/empty'; +import 'rxjs/add/observable/of'; +import 'rxjs/add/observable/from'; +import 'rxjs/add/observable/forkJoin'; +import 'rxjs/add/observable/timer'; +import cloneDeep from 'lodash/cloneDeep'; - return this.configuration.read() - .then((data) => { - this.loadList(data); - - return this.$q.resolve(data.clusters.length - ? 'base.configuration.tabs.advanced' - : 'base.configuration.tabs.basic'); - }); - } - - loadList(list) { - this.ConfigureState.dispatchAction({type: LOAD_LIST, list}); - } - - addCluster(cluster) { - this.ConfigureState.dispatchAction({type: ADD_CLUSTER, cluster}); - } +import { + ofType +} from '../store/effects'; - updateCluster(cluster) { - this.ConfigureState.dispatchAction({type: UPDATE_CLUSTER, cluster}); - } +import {default as ConfigureState} from 'app/components/page-configure/services/ConfigureState'; +import {default as ConfigSelectors} from 'app/components/page-configure/store/selectors'; - upsertCaches(caches) { - this.ConfigureState.dispatchAction({type: UPSERT_CACHES, caches}); +export default class PageConfigure { + static $inject = [ConfigureState.name, ConfigSelectors.name]; + /** + * @param {ConfigureState} ConfigureState + * @param {ConfigSelectors} ConfigSelectors + */ + constructor(ConfigureState, ConfigSelectors) { + this.ConfigureState = ConfigureState; + this.ConfigSelectors = ConfigSelectors; } - upsertClusters(clusters) { - this.ConfigureState.dispatchAction({type: UPSERT_CLUSTERS, clusters}); + getClusterConfiguration({clusterID, isDemo}) { + return Observable.merge( + Observable + .timer(1) + .take(1) + .do(() => this.ConfigureState.dispatchAction({type: 'LOAD_COMPLETE_CONFIGURATION', clusterID, isDemo})) + .ignoreElements(), + this.ConfigureState.actions$.let(ofType('LOAD_COMPLETE_CONFIGURATION_ERR')).take(1).map((e) => {throw e;}), + this.ConfigureState.state$ + .let(this.ConfigSelectors.selectCompleteClusterConfiguration({clusterID, isDemo})) + .filter((c) => c.__isComplete) + .take(1) + .map((data) => ({...data, clusters: [cloneDeep(data.cluster)]})) + ) + .take(1) + .toPromise(); } } http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/frontend/app/components/page-configure/services/PageConfigure.spec.js ---------------------------------------------------------------------- diff --git a/modules/web-console/frontend/app/components/page-configure/services/PageConfigure.spec.js b/modules/web-console/frontend/app/components/page-configure/services/PageConfigure.spec.js new file mode 100644 index 0000000..bc72cd3 --- /dev/null +++ b/modules/web-console/frontend/app/components/page-configure/services/PageConfigure.spec.js @@ -0,0 +1,244 @@ +/* + * 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 {suite, test} from 'mocha'; +import {assert} from 'chai'; +import {spy} from 'sinon'; +import {TestScheduler} from 'rxjs/testing/TestScheduler'; +import {Observable} from 'rxjs/Observable'; + +import 'rxjs/add/observable/of'; +import 'rxjs/add/observable/throw'; + +const mocks = () => new Map([ + ['IgniteConfigurationResource', {}], + ['$state', {}], + ['ConfigureState', {}], + ['Clusters', {}] +]); + +import {REMOVE_CLUSTERS_LOCAL_REMOTE, CLONE_CLUSTERS} from './PageConfigure'; +import PageConfigure from './PageConfigure'; +import {REMOVE_CLUSTERS, LOAD_LIST, ADD_CLUSTERS, UPDATE_CLUSTER} from '../reducer'; + +suite.skip('PageConfigure service', () => { + suite('cloneCluster$ effect', () => { + test('successfull clusters clone', () => { + const testScheduler = new TestScheduler((...args) => assert.deepEqual(...args)); + const values = { + s: { + list: { + clusters: new Map([ + [1, {_id: 1, name: 'Cluster 1'}], + [2, {_id: 2, name: 'Cluster 1 (clone)'}] + ]) + } + }, + a: { + type: CLONE_CLUSTERS, + clusters: [ + {_id: 1, name: 'Cluster 1'}, + {_id: 2, name: 'Cluster 1 (clone)'} + ] + }, + b: { + type: ADD_CLUSTERS, + clusters: [ + {_id: -1, name: 'Cluster 1 (clone) (1)'}, + {_id: -2, name: 'Cluster 1 (clone) (clone)'} + ] + }, + c: { + type: UPDATE_CLUSTER, + _id: -1, + cluster: {_id: 99} + }, + d: { + type: UPDATE_CLUSTER, + _id: -2, + cluster: {_id: 99} + } + }; + const actions = '-a----'; + const state = 's-----'; + const output = '-(bcd)'; + + const deps = mocks() + .set('Clusters', { + saveCluster$: (c) => Observable.of({data: 99}) + }) + .set('ConfigureState', { + actions$: testScheduler.createHotObservable(actions, values), + state$: testScheduler.createHotObservable(state, values), + dispatchAction: spy() + }); + + const s = new PageConfigure(...deps.values()); + + testScheduler.expectObservable(s.cloneClusters$).toBe(output, values); + testScheduler.flush(); + assert.equal(s.ConfigureState.dispatchAction.callCount, 3); + }); + test('some clusters clone failure', () => { + const testScheduler = new TestScheduler((...args) => assert.deepEqual(...args)); + const values = { + s: { + list: { + clusters: new Map([ + [1, {_id: 1, name: 'Cluster 1'}], + [2, {_id: 2, name: 'Cluster 1 (clone)'}] + ]) + } + }, + a: { + type: CLONE_CLUSTERS, + clusters: [ + {_id: 1, name: 'Cluster 1'}, + {_id: 2, name: 'Cluster 1 (clone)'} + ] + }, + b: { + type: ADD_CLUSTERS, + clusters: [ + {_id: -1, name: 'Cluster 1 (clone) (1)'}, + {_id: -2, name: 'Cluster 1 (clone) (clone)'} + ] + }, + c: { + type: UPDATE_CLUSTER, + _id: -1, + cluster: {_id: 99} + }, + d: { + type: REMOVE_CLUSTERS, + clusterIDs: [-2] + } + }; + const actions = '-a----'; + const state = 's-----'; + const output = '-(bcd)'; + + const deps = mocks() + .set('Clusters', { + saveCluster$: (c) => c.name === values.b.clusters[0].name + ? Observable.of({data: 99}) + : Observable.throw() + }) + .set('ConfigureState', { + actions$: testScheduler.createHotObservable(actions, values), + state$: testScheduler.createHotObservable(state, values), + dispatchAction: spy() + }); + + const s = new PageConfigure(...deps.values()); + + testScheduler.expectObservable(s.cloneClusters$).toBe(output, values); + testScheduler.flush(); + assert.equal(s.ConfigureState.dispatchAction.callCount, 3); + }); + }); + suite('removeCluster$ effect', () => { + test('successfull clusters removal', () => { + const testScheduler = new TestScheduler((...args) => assert.deepEqual(...args)); + + const values = { + a: { + type: REMOVE_CLUSTERS_LOCAL_REMOTE, + clusters: [1, 2, 3, 4, 5].map((i) => ({_id: i})) + }, + b: { + type: REMOVE_CLUSTERS, + clusterIDs: [1, 2, 3, 4, 5] + }, + c: { + type: LOAD_LIST, + list: [] + }, + d: { + type: REMOVE_CLUSTERS, + clusterIDs: [1, 2, 3, 4, 5] + }, + s: { + list: [] + } + }; + + const actions = '-a'; + const state = 's-'; + const output = '-d'; + + const deps = mocks() + .set('ConfigureState', { + actions$: testScheduler.createHotObservable(actions, values), + state$: testScheduler.createHotObservable(state, values), + dispatchAction: spy() + }) + .set('Clusters', { + removeCluster$: (v) => Observable.of(v) + }); + const s = new PageConfigure(...deps.values()); + + testScheduler.expectObservable(s.removeClusters$).toBe(output, values); + testScheduler.flush(); + assert.equal(s.ConfigureState.dispatchAction.callCount, 1); + }); + test('some clusters removal failure', () => { + const testScheduler = new TestScheduler((...args) => assert.deepEqual(...args)); + + const values = { + a: { + type: REMOVE_CLUSTERS_LOCAL_REMOTE, + clusters: [1, 2, 3, 4, 5].map((i) => ({_id: i})) + }, + b: { + type: REMOVE_CLUSTERS, + clusterIDs: [1, 2, 3, 4, 5] + }, + c: { + type: LOAD_LIST, + list: [] + }, + d: { + type: REMOVE_CLUSTERS, + clusterIDs: [1, 3, 5] + }, + s: { + list: [] + } + }; + + const actions = '-a----'; + const state = 's-----'; + const output = '-(bcd)'; + + const deps = mocks() + .set('ConfigureState', { + actions$: testScheduler.createHotObservable(actions, values), + state$: testScheduler.createHotObservable(state, values), + dispatchAction: spy() + }) + .set('Clusters', { + removeCluster$: (v) => v._id % 2 ? Observable.of(v) : Observable.throw() + }); + const s = new PageConfigure(...deps.values()); + + testScheduler.expectObservable(s.removeClusters$).toBe(output, values); + testScheduler.flush(); + assert.equal(s.ConfigureState.dispatchAction.callCount, 3); + }); + }); +}); http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/frontend/app/components/page-configure/store/actionCreators.js ---------------------------------------------------------------------- diff --git a/modules/web-console/frontend/app/components/page-configure/store/actionCreators.js b/modules/web-console/frontend/app/components/page-configure/store/actionCreators.js new file mode 100644 index 0000000..c911426 --- /dev/null +++ b/modules/web-console/frontend/app/components/page-configure/store/actionCreators.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 { + REMOVE_CLUSTER_ITEMS, + REMOVE_CLUSTER_ITEMS_CONFIRMED, + ADVANCED_SAVE_COMPLETE_CONFIGURATION, + 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, + BASIC_SAVE_ERR +} from './actionTypes'; + +/** + * @typedef {object} IRemoveClusterItemsAction + * @prop {'REMOVE_CLUSTER_ITEMS'} type + * @prop {('caches'|'igfss'|'models')} itemType + * @prop {string} clusterID + * @prop {Array<string>} itemIDs + * @prop {boolean} save + * @prop {boolean} confirm + */ + +/** + * @param {string} clusterID + * @param {('caches'|'igfss'|'models')} itemType + * @param {Array<string>} itemIDs + * @param {boolean} [save=false] + * @param {boolean} [confirm=true] + * @returns {IRemoveClusterItemsAction} + */ +export const removeClusterItems = (clusterID, itemType, itemIDs, save = false, confirm = true) => ({ + type: REMOVE_CLUSTER_ITEMS, + itemType, + clusterID, + itemIDs, + save, + confirm +}); + +/** + * @typedef {object} IRemoveClusterItemsConfirmed + * @prop {string} clusterID + * @prop {'REMOVE_CLUSTER_ITEMS_CONFIRMED'} type + * @prop {('caches'|'igfss'|'models')} itemType + * @prop {Array<string>} itemIDs + */ + +/** + * @param {string} clusterID + * @param {(('caches'|'igfss'|'models'))} itemType + * @param {Array<string>} itemIDs + * @returns {IRemoveClusterItemsConfirmed} + */ +export const removeClusterItemsConfirmed = (clusterID, itemType, itemIDs) => ({ + type: REMOVE_CLUSTER_ITEMS_CONFIRMED, + itemType, + clusterID, + itemIDs +}); + +const applyChangedIDs = (edit) => ({ + cluster: { + ...edit.changes.cluster, + caches: edit.changes.caches.ids, + igfss: edit.changes.igfss.ids, + models: edit.changes.models.ids + }, + caches: edit.changes.caches.changedItems, + igfss: edit.changes.igfss.changedItems, + models: edit.changes.models.changedItems +}); + +const upsertCluster = (cluster) => ({ + type: 'UPSERT_CLUSTER', + cluster +}); + +export const changeItem = (type, item) => ({ + type: 'UPSERT_CLUSTER_ITEM', + itemType: type, + item +}); + +/** + * @typedef {object} IAdvancedSaveCompleteConfigurationAction + * @prop {'ADVANCED_SAVE_COMPLETE_CONFIGURATION'} type + * @prop {object} changedItems + * @prop {Array<object>} [prevActions] + */ + +/** + * @returns {IAdvancedSaveCompleteConfigurationAction} + */ +// TODO: add support for prev actions +export const advancedSaveCompleteConfiguration = (edit) => { + return { + type: ADVANCED_SAVE_COMPLETE_CONFIGURATION, + changedItems: applyChangedIDs(edit) + }; +}; + +/** + * @typedef {object} IConfirmClustersRemovalAction + * @prop {'CONFIRM_CLUSTERS_REMOVAL'} type + * @prop {Array<string>} clusterIDs + */ + +/** + * @param {Array<string>} clusterIDs + * @returns {IConfirmClustersRemovalAction} + */ +export const confirmClustersRemoval = (clusterIDs) => ({ + type: CONFIRM_CLUSTERS_REMOVAL, + clusterIDs +}); + +/** + * @typedef {object} IConfirmClustersRemovalActionOK + * @prop {'CONFIRM_CLUSTERS_REMOVAL_OK'} type + */ + +/** + * @returns {IConfirmClustersRemovalActionOK} + */ +export const confirmClustersRemovalOK = () => ({ + type: CONFIRM_CLUSTERS_REMOVAL_OK +}); + +export const completeConfiguration = (configuration) => ({ + type: COMPLETE_CONFIGURATION, + configuration +}); + +export const advancedSaveCluster = (cluster) => ({type: ADVANCED_SAVE_CLUSTER, cluster}); +export const advancedSaveCache = (cache) => ({type: ADVANCED_SAVE_CACHE, cache}); +export const advancedSaveIGFS = (igfs) => ({type: ADVANCED_SAVE_IGFS, igfs}); +export const advancedSaveModel = (model) => ({type: ADVANCED_SAVE_MODEL, model}); + +export const basicSave = (cluster) => ({type: BASIC_SAVE, cluster}); +export const basicSaveAndDownload = (cluster) => ({type: BASIC_SAVE_AND_DOWNLOAD, cluster}); +export const basicSaveOK = (changedItems) => ({type: BASIC_SAVE_OK, changedItems}); +export const basicSaveErr = (changedItems, res) => ({ + type: BASIC_SAVE_ERR, + changedItems, + error: { + message: `Failed to save cluster "${changedItems.cluster.name}": ${res.data}.` + } +}); http://git-wip-us.apache.org/repos/asf/ignite/blob/7ee1683e/modules/web-console/frontend/app/components/page-configure/store/actionTypes.js ---------------------------------------------------------------------- diff --git a/modules/web-console/frontend/app/components/page-configure/store/actionTypes.js b/modules/web-console/frontend/app/components/page-configure/store/actionTypes.js new file mode 100644 index 0000000..aa8a4f3 --- /dev/null +++ b/modules/web-console/frontend/app/components/page-configure/store/actionTypes.js @@ -0,0 +1,31 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export const CONFIRM_CLUSTERS_REMOVAL = 'CONFIRM_CLUSTERS_REMOVAL'; +export const CONFIRM_CLUSTERS_REMOVAL_OK = 'CONFIRM_CLUSTERS_REMOVAL_OK'; +export const REMOVE_CLUSTER_ITEMS = 'REMOVE_CLUSTER_ITEMS'; +export const REMOVE_CLUSTER_ITEMS_CONFIRMED = 'REMOVE_CLUSTER_ITEMS_CONFIRMED'; +export const ADVANCED_SAVE_COMPLETE_CONFIGURATION = 'ADVANCED_SAVE_COMPLETE_CONFIGURATION'; +export const COMPLETE_CONFIGURATION = 'COMPLETE_CONFIGURATION'; +export const ADVANCED_SAVE_CLUSTER = 'ADVANCED_SAVE_CLUSTER'; +export const ADVANCED_SAVE_CACHE = 'ADVANCED_SAVE_CACHE'; +export const ADVANCED_SAVE_IGFS = 'ADVANCED_SAVE_IGFS'; +export const ADVANCED_SAVE_MODEL = 'ADVANCED_SAVE_MODEL'; +export const BASIC_SAVE = 'BASIC_SAVE'; +export const BASIC_SAVE_AND_DOWNLOAD = 'BASIC_SAVE_AND_DOWNLOAD'; +export const BASIC_SAVE_OK = 'BASIC_SAVE_OK'; +export const BASIC_SAVE_ERR = 'BASIC_SAVE_ERR';
