diff --git a/packages/compass-aggregations/src/modules/create-view/index.js b/packages/compass-aggregations/src/modules/create-view/index.js index 934db767eed..a1b9d9423f1 100644 --- a/packages/compass-aggregations/src/modules/create-view/index.js +++ b/packages/compass-aggregations/src/modules/create-view/index.js @@ -171,7 +171,7 @@ export const createView = () => { isReadonly: true, sourceName: viewSource, editViewName: null, - isSourceReadonly: state.isReadonly, + sourceReadonly: state.isReadonly, sourceViewOn: state.sourceName, sourcePipeline: viewPipeline } diff --git a/packages/compass-aggregations/src/modules/update-view.js b/packages/compass-aggregations/src/modules/update-view.js index fb03d12dabd..3e633308c7f 100644 --- a/packages/compass-aggregations/src/modules/update-view.js +++ b/packages/compass-aggregations/src/modules/update-view.js @@ -87,7 +87,7 @@ export const updateView = () => { isReadonly: true, sourceName: state.namespace, editViewName: null, - isSourceReadonly: state.isReadonly, + sourceReadonly: state.isReadonly, sourceViewOn: state.sourceName, sourcePipeline: viewPipeline }; diff --git a/packages/compass-collection/electron/renderer/index.js b/packages/compass-collection/electron/renderer/index.js index 9a70b190fa0..df78e210419 100644 --- a/packages/compass-collection/electron/renderer/index.js +++ b/packages/compass-collection/electron/renderer/index.js @@ -102,7 +102,7 @@ dataService.connect((error, ds) => { isReadonly: false, sourceName: null, editViewName: null, - isSourceReadonly: false, + sourceReadonly: false, sourceViewOn: null } ); @@ -113,7 +113,7 @@ dataService.connect((error, ds) => { isReadonly: true, sourceName: 'citibike.trips', editViewName: null, - isSourceReadonly: false, + sourceReadonly: false, sourceViewOn: null, sourcePipeline: [{ '$match': { name: 'testing' }}] } @@ -125,7 +125,7 @@ dataService.connect((error, ds) => { isReadonly: true, sourceName: 'citibike.tripsOfShortDuration', editViewName: null, - isSourceReadonly: true, + sourceReadonly: true, sourceViewOn: 'citibike.trips', sourcePipeline: [{ '$match': { gender: 0 }}] } diff --git a/packages/compass-collection/src/components/collection-header/collection-header.jsx b/packages/compass-collection/src/components/collection-header/collection-header.jsx index 7b19ed94e52..aeec2b875fe 100644 --- a/packages/compass-collection/src/components/collection-header/collection-header.jsx +++ b/packages/compass-collection/src/components/collection-header/collection-header.jsx @@ -13,6 +13,7 @@ class CollectionHeader extends Component { globalAppRegistry: PropTypes.func.isRequired, namespace: PropTypes.string.isRequired, isReadonly: PropTypes.bool.isRequired, + isTimeSeries: PropTypes.bool.isRequired, statsPlugin: PropTypes.func.isRequired, selectOrCreateTab: PropTypes.func.isRequired, statsStore: PropTypes.object.isRequired, @@ -24,27 +25,29 @@ class CollectionHeader extends Component { }; modifySource = () => { - this.props.selectOrCreateTab( - this.props.sourceName, - this.props.sourceReadonly, - this.props.sourceViewOn, - this.props.namespace, - false, - null, - this.props.pipeline - ); + this.props.selectOrCreateTab({ + namespace: this.props.sourceName, + isReadonly: this.props.sourceReadonly, + isTimeSeries: this.props.isTimeSeries, + sourceName: this.props.sourceViewOn, + editViewName: this.props.namespace, + sourceReadonly: false, + sourceViewOn: null, + sourcePipeline: this.props.pipeline + }); } returnToView = () => { - this.props.selectOrCreateTab( - this.props.editViewName, - true, - this.props.namespace, - null, - this.props.isReadonly, - this.props.sourceName, - this.props.pipeline - ); + this.props.selectOrCreateTab({ + namespace: this.props.editViewName, + isReadonly: true, + isTimeSeries: this.props.isTimeSeries, + sourceName: this.props.namespace, + editViewName: null, + sourceReadonly: this.props.isReadonly, + sourceViewOn: this.props.sourceName, + sourcePipeline: this.props.pipeline + }); } handleDBClick = (db) => { diff --git a/packages/compass-collection/src/components/collection/collection.jsx b/packages/compass-collection/src/components/collection/collection.jsx index 0e4a4fa7f1d..84481800958 100644 --- a/packages/compass-collection/src/components/collection/collection.jsx +++ b/packages/compass-collection/src/components/collection/collection.jsx @@ -11,6 +11,7 @@ class Collection extends Component { static propTypes = { namespace: PropTypes.string.isRequired, + isTimeSeries: PropTypes.bool, isReadonly: PropTypes.bool.isRequired, tabs: PropTypes.array.isRequired, views: PropTypes.array.isRequired, @@ -69,6 +70,7 @@ class Collection extends Component { globalAppRegistry={this.props.globalAppRegistry} namespace={this.props.namespace} isReadonly={this.props.isReadonly} + isTimeSeries={this.props.isTimeSeries} statsPlugin={this.props.statsPlugin} statsStore={this.props.statsStore} editViewName={this.props.editViewName} diff --git a/packages/compass-collection/src/components/collection/collection.spec.js b/packages/compass-collection/src/components/collection/collection.spec.js index edd485b941a..c9b4e82a86e 100644 --- a/packages/compass-collection/src/components/collection/collection.spec.js +++ b/packages/compass-collection/src/components/collection/collection.spec.js @@ -19,6 +19,7 @@ describe('Collection [Component]', () => { component = mount( { - this.props.createNewTab( - this.props.activeNamespace, - this.props.activeIsReadonly, - this.props.activeSourceName - ); - } - /** * Render the Create Tab component. * @@ -31,7 +17,10 @@ class CreateTab extends PureComponent { */ render() { return ( -
+
this.props.createNewTab()} + > +
); diff --git a/packages/compass-collection/src/components/create-tab/create-tab.spec.js b/packages/compass-collection/src/components/create-tab/create-tab.spec.js index 2e29e5eecd1..d49c40852db 100644 --- a/packages/compass-collection/src/components/create-tab/create-tab.spec.js +++ b/packages/compass-collection/src/components/create-tab/create-tab.spec.js @@ -13,10 +13,8 @@ describe('CreateTab [Component]', () => { component = mount( + createNewTab={createNewTabSpy} + /> ); }); @@ -32,7 +30,7 @@ describe('CreateTab [Component]', () => { context('when clicking the create button', () => { it('calls the action', () => { component.find(`.${styles['create-tab']}`).simulate('click'); - expect(createNewTabSpy.calledWith('db.coll')).to.equal(true); + expect(createNewTabSpy.called).to.equal(true); }); }); }); diff --git a/packages/compass-collection/src/components/workspace/workspace.jsx b/packages/compass-collection/src/components/workspace/workspace.jsx index 58b2ba9dbc8..58546d9ba29 100644 --- a/packages/compass-collection/src/components/workspace/workspace.jsx +++ b/packages/compass-collection/src/components/workspace/workspace.jsx @@ -41,6 +41,12 @@ const KEY_CLOSE_BRKT = 221; */ const KEY_OPEN_BRKT = 219; +const DEFAULT_NEW_TAB = { + namespace: '', + isReadonly: false, + sourceName: '' +}; + /** * The collection workspace contains tabs of multiple collections. */ @@ -88,6 +94,23 @@ class Workspace extends PureComponent { this.props.moveTab(oldIndex, newIndex); } + onCreateNewTab = () => { + const activeTab = this.activeTab(); + const newTabProps = activeTab + ? { + namespace: activeTab.namespace, + isReadonly: activeTab.isReadonly, + isTimeSeries: activeTab.isTimeSeries, + sourceName: activeTab.sourceName, + editViewName: activeTab.editViewName, + sourceReadonly: activeTab.sourceReadonly, + sourceViewOn: activeTab.sourceViewOn, + sourcePipeline: activeTab.pipeline + } + : DEFAULT_NEW_TAB; + this.props.createNewTab(newTabProps); + } + /** * Handle key press. This listens for CTRL/CMD+T and CTRL/CMD+W to control * natural opening and closing of collection tabs. CTRL/CMD+SHIFT+] and @@ -95,7 +118,7 @@ class Workspace extends PureComponent { * * @param {Event} evt - The event. */ - handleKeypress(evt) { + handleKeypress = (evt) => { if (evt.ctrlKey || evt.metaKey) { if (evt.shiftKey) { if (evt.keyCode === KEY_CLOSE_BRKT) { @@ -109,7 +132,7 @@ class Workspace extends PureComponent { evt.preventDefault(); } } else if (evt.keyCode === KEY_T) { - this.props.createNewTab(this.activeNamespace()); + this.onCreateNewTab(); } } } @@ -118,26 +141,6 @@ class Workspace extends PureComponent { return this.props.tabs.find(tab => tab.isActive); } - /** - * Get the active namespace in the list. - * - * @returns {String} The active namespace in the list. - */ - activeNamespace() { - const activeTab = this.activeTab(); - return activeTab ? activeTab.namespace : ''; - } - - activeIsReadonly() { - const activeTab = this.activeTab(); - return activeTab ? activeTab.isReadonly : false; - } - - activeSourceName() { - const activeTab = this.activeTab(); - return activeTab ? activeTab.sourceName : undefined; - } - renderTab = (tab, i) => { return ( {this.renderTabs()} + createNewTab={this.onCreateNewTab} + />
diff --git a/packages/compass-collection/src/modules/context.js b/packages/compass-collection/src/modules/context.js index e08d74b92a0..5f33cad90a0 100644 --- a/packages/compass-collection/src/modules/context.js +++ b/packages/compass-collection/src/modules/context.js @@ -20,21 +20,22 @@ const setupActions = (role, localAppRegistry) => { /** * Setup a scoped store to the collection. * - * @param {Object} role - The role. - * @param {Object} globalAppRegistry - The global app registry. - * @param {Object} localAppRegistry - The scoped app registry to the collection. - * @param {Object} dataService - The data service. - * @param {String} namespace - The namespace. - * @param {String} serverVersion - The server version. - * @param {Boolean} isReadonly - If the collection is a readonly view. - * @param {Object} actions - The actions for the store. - * @param {Boolean} allowWrites - If writes are allowed. - * @param {String} sourceName - The source namespace for the view. - * @param {String} editViewName - The name of the view we are editing. + * @param {Object} options - The plugin store options. + * @property {Object} options.role - The role. + * @property {Object} options.globalAppRegistry - The global app registry. + * @property {Object} options.localAppRegistry - The scoped app registry to the collection. + * @property {Object} options.dataService - The data service. + * @property {String} options.namespace - The namespace. + * @property {String} options.serverVersion - The server version. + * @property {Boolean} options.isReadonly - If the collection is a readonly view. + * @property {Object} options.actions - The actions for the store. + * @property {Boolean} options.allowWrites - If writes are allowed. + * @property {String} options.sourceName - The source namespace for the view. + * @property {String} options.editViewName - The name of the view we are editing. * * @returns {Object} The configured store. */ -const setupStore = ( +const setupStore = ({ role, globalAppRegistry, localAppRegistry, @@ -42,11 +43,13 @@ const setupStore = ( namespace, serverVersion, isReadonly, + isTimeSeries, actions, allowWrites, sourceName, editViewName, - sourcePipeline) => { + sourcePipeline +}) => { const store = role.configureStore({ localAppRegistry: localAppRegistry, globalAppRegistry: globalAppRegistry, @@ -57,6 +60,7 @@ const setupStore = ( namespace: namespace, serverVersion: serverVersion, isReadonly: isReadonly, + isTimeSeries, actions: actions, allowWrites: allowWrites, sourceName: sourceName, @@ -71,19 +75,21 @@ const setupStore = ( /** * Setup a scoped plugin to the tab. * - * @param {Object} role - The role. - * @param {Object} globalAppRegistry - The global app registry. - * @param {Object} localAppRegistry - The scoped app registry to the collection. - * @param {Object} dataService - The data service. - * @param {String} namespace - The namespace. - * @param {String} serverVersion - The server version. - * @param {Boolean} isReadonly - If the collection is a readonly view. - * @param {Boolean} allowWrites - If writes are allowed. - * @param {String} key - The plugin key. + * @param options - The plugin options. + * @property {Object} options.role - The role. + * @property {Object} options.globalAppRegistry - The global app registry. + * @property {Object} options.localAppRegistry - The scoped app registry to the collection. + * @property {Object} options.dataService - The data service. + * @property {String} options.namespace - The namespace. + * @property {String} options.serverVersion - The server version. + * @property {Boolean} options.isReadonly - If the collection is a readonly view. + * @property {Boolean} options.isTimeSeries - If the collection is a time-series collection. + * @property {Boolean} options.allowWrites - If writes are allowed. + * @property {String} options.key - The plugin key. * * @returns {Component} The plugin. */ -const setupPlugin = ( +const setupPlugin = ({ role, globalAppRegistry, localAppRegistry, @@ -91,10 +97,12 @@ const setupPlugin = ( namespace, serverVersion, isReadonly, + isTimeSeries, allowWrites, - key) => { + key +}) => { const actions = role.configureActions(); - const store = setupStore( + const store = setupStore({ role, globalAppRegistry, localAppRegistry, @@ -102,9 +110,10 @@ const setupPlugin = ( namespace, serverVersion, isReadonly, + isTimeSeries, actions, allowWrites - ); + }); const plugin = role.component; return { component: plugin, @@ -117,28 +126,32 @@ const setupPlugin = ( /** * Setup every scoped modal role. * - * @param {Object} globalAppRegistry - The global app registry. - * @param {Object} localAppRegistry - The scoped app registry to the collection. - * @param {Object} dataService - The data service. - * @param {String} namespace - The namespace. - * @param {String} serverVersion - The server version. - * @param {Boolean} isReadonly - If the collection is a readonly view. - * @param {Boolean} allowWrites - If we allow writes. + * @param options - The scope modal plugin options. + * @property {Object} options.globalAppRegistry - The global app registry. + * @property {Object} options.localAppRegistry - The scoped app registry to the collection. + * @property {Object} options.dataService - The data service. + * @property {String} options.namespace - The namespace. + * @property {String} options.serverVersion - The server version. + * @property {Boolean} options.isReadonly - If the collection is a readonly view. + * @property {Boolean} options.isTimeSeries - If the collection is a time-series. + * @property {Boolean} options.allowWrites - If we allow writes. * * @returns {Array} The components. */ -const setupScopedModals = ( +const setupScopedModals = ({ globalAppRegistry, localAppRegistry, dataService, namespace, serverVersion, isReadonly, - allowWrites) => { + isTimeSeries, + allowWrites +}) => { const roles = globalAppRegistry.getRole('Collection.ScopedModal'); if (roles) { return roles.map((role, i) => { - return setupPlugin( + return setupPlugin({ role, globalAppRegistry, localAppRegistry, @@ -146,9 +159,10 @@ const setupScopedModals = ( namespace, serverVersion, isReadonly, + isTimeSeries, allowWrites, - i - ); + key: i + }); }); } return []; @@ -157,16 +171,27 @@ const setupScopedModals = ( /** * Create the context in which a tab is created. * - * @param {Object} state - The store state. - * @param {String} namespace - The namespace. - * @param {Boolean} isReadonly - Is the namespace readonly. - * @param {Boolean} isDataLake - If we are hitting the data lake. - * @param {String} sourceName - The name of the view source. - * @param {String} editViewName - The name of the view we are editing. + * @param {Object} options - The options for creating the context. + * @property {Object} options.state - The store state. + * @property {String} options.namespace - The namespace. + * @property {Boolean} options.isReadonly - Is the namespace readonly. + * @property {Boolean} options.isDataLake - If we are hitting the data lake. + * @property {String} options.sourceName - The name of the view source. + * @property {String} options.editViewName - The name of the view we are editing. + * @property {String} options.sourcePipeline * * @returns {Object} The tab context. */ -const createContext = (state, namespace, isReadonly, isDataLake, sourceName, editViewName, sourcePipeline) => { +const createContext = ({ + state, + namespace, + isReadonly, + isTimeSeries, + isDataLake, + sourceName, + editViewName, + sourcePipeline +}) => { const serverVersion = state.serverVersion; const localAppRegistry = new AppRegistry(); const globalAppRegistry = state.appRegistry; @@ -192,37 +217,39 @@ const createContext = (state, namespace, isReadonly, isDataLake, sourceName, edi const queryBarRole = globalAppRegistry.getRole('Query.QueryBar')[0]; localAppRegistry.registerRole('Query.QueryBar', queryBarRole); const queryBarActions = setupActions(queryBarRole, localAppRegistry); - setupStore( - queryBarRole, + setupStore({ + role: queryBarRole, globalAppRegistry, localAppRegistry, - state.dataService, + dataService: state.dataService, namespace, serverVersion, isReadonly, - queryBarActions, - !isDataLake - ); + isTimeSeries, + actions: queryBarActions, + allowWrites: !isDataLake + }); // Setup each of the tabs inside the collection tab. They will all get // passed the same information and can determine whether they want to // use it or not. filteredRoles.forEach((role, i) => { const actions = setupActions(role, localAppRegistry); - const store = setupStore( + const store = setupStore({ role, globalAppRegistry, localAppRegistry, - state.dataService, + dataService: state.dataService, namespace, serverVersion, isReadonly, + isTimeSeries, actions, - !isDataLake, + allowWrite: !isDataLake, sourceName, editViewName, sourcePipeline - ); + }); // Add the tab. tabs.push(role.name); @@ -239,28 +266,30 @@ const createContext = (state, namespace, isReadonly, isDataLake, sourceName, edi // Setup the stats in the collection HUD const statsRole = globalAppRegistry.getRole('Collection.HUD')[0]; const statsPlugin = statsRole.component; - const statsStore = setupStore( - statsRole, + const statsStore = setupStore({ + role: statsRole, globalAppRegistry, localAppRegistry, - state.dataService, + dataService: state.dataService, namespace, serverVersion, isReadonly, - {}, - !isDataLake - ); + isTimeSeries, + actions: {}, + allowWrites: !isDataLake + }); // Setup the scoped modals - const scopedModals = setupScopedModals( + const scopedModals = setupScopedModals({ globalAppRegistry, localAppRegistry, - state.dataService, + dataService: state.dataService, namespace, serverVersion, isReadonly, - !isDataLake - ); + isTimeSeries, + allowWrites: !isDataLake + }); const configureFieldStore = globalAppRegistry.getStore('Field.Store'); configureFieldStore({ diff --git a/packages/compass-collection/src/modules/tabs.js b/packages/compass-collection/src/modules/tabs.js index 71b8767c8d4..1ed553eb176 100644 --- a/packages/compass-collection/src/modules/tabs.js +++ b/packages/compass-collection/src/modules/tabs.js @@ -66,6 +66,19 @@ const doClearTabs = () => { return INITIAL_STATE; }; +/** + * Tab options. + * @typedef {Object} CollectionTabOptions + * @property {String} namespace - The namespace to select. + * @property {Boolean} isReadonly - If the ns is readonly. + * @property {Boolean} isTimeSeries - If the ns is a time-series collection. + * @property {String} sourceName - The ns of the resonly view source. + * @property {String} editViewName - The name of the view we are editing. + * @property {String} sourceReadonly + * @property {String} sourceViewOn + * @property {String} sourcePipeline + */ + /** * Handles namespace selected actions. * @@ -85,6 +98,7 @@ const doSelectNamespace = (state, action) => { activeSubTab: subTabIndex, activeSubTabName: action.context.tabs[subTabIndex], isReadonly: action.isReadonly, + isTimeSeries: action.isTimeSeries, tabs: action.context.tabs, views: action.context.views, subtab: action.context.subtab, @@ -126,6 +140,7 @@ const doCreateTab = (state, action) => { activeSubTab: subTabIndex, activeSubTabName: action.context.tabs[subTabIndex], isReadonly: action.isReadonly, + isTimeSeries: action.isTimeSeries, tabs: action.context.tabs, views: action.context.views, subtab: action.context.subtab, @@ -308,20 +323,34 @@ export default function reducer(state = INITIAL_STATE, action) { /** * Action creator for create tab. * - * @param {String} id - The tab id. - * @param {String} namespace - The namespace. - * @param {Boolean} isReadonly - Is the collection readonly? - * @param {String} sourceName - The source namespace. - * @param {String} editViewName - The name of the view we are editing. - * @param {Object} context - The tab context. + * @parma {Object} options + * @property {String} options.id - The tab id. + * @property {String} options.namespace - The namespace. + * @property {Boolean} options.isReadonly - Is the collection readonly? + * @property {String} options.sourceName - The source namespace. + * @property {String} options.editViewName - The name of the view we are editing. + * @property {Object} options.context - The tab context. + * @property {Boolean} options.sourceReadonly + * @property {String} options.sourceViewOn * * @returns {Object} The create tab action. */ -export const createTab = (id, namespace, isReadonly, sourceName, editViewName, context, sourceReadonly, sourceViewOn) => ({ +export const createTab = ({ + id, + namespace, + isReadonly, + isTimeSeries, + sourceName, + editViewName, + context, + sourceReadonly, + sourceViewOn +}) => ({ type: CREATE_TAB, id: id, namespace: namespace, isReadonly: isReadonly || false, + isTimeSeries: isTimeSeries || false, sourceName: sourceName, editViewName: editViewName, context: context, @@ -335,17 +364,31 @@ export const createTab = (id, namespace, isReadonly, sourceName, editViewName, c * @param {String} id - The tab id. * @param {String} namespace - The namespace. * @param {Boolean} isReadonly - Is the collection readonly? + * @param {Boolean} isTimeSeries - Is the collection time-series? * @param {String} sourceName - The source namespace. * @param {String} editViewName - The name of the view we are editing. * @param {Object} context - The tab context. + * @param {Object} sourceReadonly + * @param {Object} sourceViewOn * * @returns {Object} The namespace selected action. */ -export const selectNamespace = (id, namespace, isReadonly, sourceName, editViewName, context, sourceReadonly, sourceViewOn) => ({ +export const selectNamespace = ({ + id, + namespace, + isReadonly, + isTimeSeries, + sourceName, + editViewName, + context, + sourceReadonly, + sourceViewOn +}) => ({ type: SELECT_NAMESPACE, id: id, namespace: namespace, - isReadonly: ((isReadonly === undefined) ? false : isReadonly), + isReadonly: isReadonly || false, + isTimeSeries, sourceName: sourceName, editViewName: editViewName, context: context, @@ -446,16 +489,31 @@ export const changeActiveSubTab = (activeSubTab, id) => ({ * Checks if we need to select a namespace or actually create a new * tab, then dispatches the correct events. * - * @param {String} namespace - The namespace to select. - * @param {Boolean} isReadonly - If the ns is readonly. - * @param {String} sourceName - The ns of the resonly view source. - * @param {String} editViewName - The name of the view we are editing. - */ -export const selectOrCreateTab = (namespace, isReadonly, sourceName, editViewName, sourceReadonly, sourceViewOn, sourcePipeline) => { + * @param {CollectionTabOptions} options +*/ +export const selectOrCreateTab = ({ + namespace, + isReadonly, + isTimeSeries, + sourceName, + editViewName, + sourceReadonly, + sourceViewOn, + sourcePipeline +}) => { return (dispatch, getState) => { const state = getState(); if (state.tabs.length === 0) { - dispatch(createNewTab(namespace, isReadonly, sourceName, editViewName, sourceReadonly, sourceViewOn, sourcePipeline)); + dispatch(createNewTab({ + namespace, + isReadonly, + isTimeSeries, + sourceName, + editViewName, + sourceReadonly, + sourceViewOn, + sourcePipeline + })); } else { // If the namespace is equal to the active tab's namespace, then // there is no need to do anything. @@ -463,7 +521,16 @@ export const selectOrCreateTab = (namespace, isReadonly, sourceName, editViewNam const activeNamespace = state.tabs[activeIndex].namespace; if (namespace !== activeNamespace) { dispatch( - replaceTabContent(namespace, isReadonly, sourceName, editViewName, sourceReadonly, sourceViewOn, sourcePipeline) + replaceTabContent({ + namespace, + isReadonly, + isTimeSeries, + sourceName, + editViewName, + sourceReadonly, + sourceViewOn, + sourcePipeline + }) ); } } @@ -474,34 +541,42 @@ export const selectOrCreateTab = (namespace, isReadonly, sourceName, editViewNam * Handles all the setup of tab creation by creating the stores for each * of the roles in the global app registry. * - * @param {String} namespace - The namespace. - * @param {Boolean} isReadonly - If the namespace is readonly. - * @param {String} sourceName - The view source namespace. - * @param {String} editViewName - The name of the view we are editing. - */ -export const createNewTab = (namespace, isReadonly, sourceName, editViewName, sourceReadonly, sourceViewOn, sourcePipeline) => { + * @param {CollectionTabOptions} options + */ +export const createNewTab = ({ + namespace, + isReadonly, + isTimeSeries, + sourceName, + editViewName, + sourceReadonly, + sourceViewOn, + sourcePipeline +}) => { return (dispatch, getState) => { const state = getState(); - const context = createContext( + const context = createContext({ state, namespace, isReadonly, - state.isDataLake, + isDataLake: state.isDataLake, + isTimeSeries, sourceName, editViewName, sourcePipeline - ); + }); dispatch( - createTab( - new ObjectId().toHexString(), + createTab({ + id: new ObjectId().toHexString(), namespace, isReadonly, + isTimeSeries, sourceName, editViewName, context, - !!sourceReadonly, + sourceReadonly: !!sourceReadonly, sourceViewOn - ) + }) ); }; }; @@ -510,34 +585,43 @@ export const createNewTab = (namespace, isReadonly, sourceName, editViewName, so * Handles all the setup of replacing tab content by creating the stores for each * of the roles in the global app registry. * - * @param {String} namespace - The namespace. - * @param {Boolean} isReadonly - If the namespace is readonly. - * @param {String} sourceName - The view source namespace. - * @param {String} editViewName - The name of the view we are editing. - */ -export const replaceTabContent = (namespace, isReadonly, sourceName, editViewName, sourceReadonly, sourceViewOn, sourcePipeline) => { - return (dispatch, getState) => { + * @param {CollectionTabOptions} options + */ +export const replaceTabContent = ({ + namespace, + isReadonly, + isTimeSeries, + sourceName, + editViewName, + sourceReadonly, + sourceViewOn, + sourcePipeline +}) => { + return (dispatch, + getState) => { const state = getState(); - const context = createContext( + const context = createContext({ state, namespace, isReadonly, - state.isDataLake, + isDataLake: state.isDataLake, + isTimeSeries, sourceName, editViewName, sourcePipeline - ); + }); dispatch( - selectNamespace( - new ObjectId().toHexString(), + selectNamespace({ + id: new ObjectId().toHexString(), namespace, isReadonly, + isTimeSeries, sourceName, editViewName, context, - !!sourceReadonly, + sourceReadonly: !!sourceReadonly, sourceViewOn - ) + }) ); }; }; diff --git a/packages/compass-collection/src/modules/tabs.spec.js b/packages/compass-collection/src/modules/tabs.spec.js index d17806bef89..f0df89e6e3c 100644 --- a/packages/compass-collection/src/modules/tabs.spec.js +++ b/packages/compass-collection/src/modules/tabs.spec.js @@ -18,11 +18,20 @@ import reducer, { describe('tabs module', () => { describe('#selectNamespace', () => { it('returns the SELECT_NAMESPACE action', () => { - expect(selectNamespace('t', 'db.coll', true, 'db.test', 'db.view', {})).to.deep.equal({ + expect(selectNamespace({ + id: 't', + namespace: 'db.coll', + isReadonly: true, + isTimeSeries: false, + sourceName: 'db.test', + editViewName: 'db.view', + context: {} + })).to.deep.equal({ type: SELECT_NAMESPACE, id: 't', namespace: 'db.coll', isReadonly: true, + isTimeSeries: false, sourceName: 'db.test', editViewName: 'db.view', sourceReadonly: undefined, @@ -35,12 +44,20 @@ describe('tabs module', () => { describe('#createTab', () => { it('returns the CREATE_TAB action', () => { expect( - createTab('id', 'db.coll', true, 'db.test', 'db.view', {}) + createTab({ + id: 'id', + namespace: 'db.coll', + isReadonly: true, + sourceName: 'db.test', + editViewName: 'db.view', + context: {} + }) ).to.deep.equal({ id: 'id', type: CREATE_TAB, namespace: 'db.coll', isReadonly: true, + isTimeSeries: false, sourceName: 'db.test', editViewName: 'db.view', sourceReadonly: undefined, diff --git a/packages/compass-collection/src/stores/store.js b/packages/compass-collection/src/stores/store.js index b09b667715f..bbc5ec495d4 100644 --- a/packages/compass-collection/src/stores/store.js +++ b/packages/compass-collection/src/stores/store.js @@ -26,22 +26,23 @@ store.onActivated = (appRegistry) => { /** * When a collection namespace is selected in the sidebar. * - * @param {Object} metatada - The metadata. + * @param {Object} metadata - The metadata. */ appRegistry.on('open-namespace-in-new-tab', (metadata) => { if (metadata.namespace) { const namespace = toNS(metadata.namespace); if (namespace.collection !== '') { store.dispatch( - createNewTab( - metadata.namespace, - metadata.isReadonly, - metadata.sourceName, - metadata.editViewName, - metadata.isSourceReadonly, - metadata.sourceViewOn, - metadata.sourcePipeline - ) + createNewTab({ + namespace: metadata.namespace, + isReadonly: metadata.isReadonly, + sourceName: metadata.sourceName, + editViewName: metadata.editViewName, + sourceReadonly: metadata.sourceReadonly, + isTimeSeries: !!metadata.isTimeSeries, + sourceViewOn: metadata.sourceViewOn, + sourcePipeline: metadata.sourcePipeline + }) ); } } @@ -57,15 +58,16 @@ store.onActivated = (appRegistry) => { const namespace = toNS(metadata.namespace); if (namespace.collection !== '') { store.dispatch( - selectOrCreateTab( - metadata.namespace, - metadata.isReadonly, - metadata.sourceName, - metadata.editViewName, - metadata.isSourceReadonly, - metadata.sourceViewOn, - metadata.sourcePipeline - ) + selectOrCreateTab({ + namespace: metadata.namespace, + isReadonly: metadata.isReadonly, + isTimeSeries: metadata.isTimeSeries, + sourceName: metadata.sourceName, + editViewName: metadata.editViewName, + sourceReadonly: metadata.sourceReadonly, + sourceViewOn: metadata.sourceViewOn, + sourcePipeline: metadata.sourcePipeline + }) ); } } diff --git a/packages/compass-collection/test/stores/store.spec.js b/packages/compass-collection/test/stores/store.spec.js index 95e85661654..28777a46f24 100644 --- a/packages/compass-collection/test/stores/store.spec.js +++ b/packages/compass-collection/test/stores/store.spec.js @@ -144,6 +144,22 @@ describe('Collection Store', () => { expect(store.getState().tabs).to.have.length(0); }); }); + + context.skip('when the colletion is a time-series collection', () => { + beforeEach(() => { + appRegistry.emit( + 'open-namespace-in-new-tab', + { + namespace: 'db.coll', + isTimeSeries: true + } + ); + }); + + it('creates a tab in the store that has isTimeSeries set', () => { + expect(store.getState().tabs[0].isTimeSeries).to.equal(true); + }); + }); }); }); }); diff --git a/packages/compass-crud/electron/renderer/index.js b/packages/compass-crud/electron/renderer/index.js index 98b46567510..6fbc5f4bc93 100644 --- a/packages/compass-crud/electron/renderer/index.js +++ b/packages/compass-crud/electron/renderer/index.js @@ -64,7 +64,8 @@ const store = configureStore({ localAppRegistry: localAppRegistry, globalAppRegistry: appRegistry, namespace: `${DB}.${COLL}`, - isReadonly: false + isReadonly: false, + isTimeSeries: false }); // Create a HMR enabled render function diff --git a/packages/compass-crud/src/components/document-actions.jsx b/packages/compass-crud/src/components/document-actions.jsx index a2ed23e0777..cd50fe9e6fe 100644 --- a/packages/compass-crud/src/components/document-actions.jsx +++ b/packages/compass-crud/src/components/document-actions.jsx @@ -7,47 +7,25 @@ import UpdatableIconButton from './updatable-icon-button'; * Component for actions on the document. */ class DocumentActions extends React.Component { - /** - * Instantiate the actions. - * - * @param {Object} props - The properties. - */ - constructor(props) { - super(props); - this.state = { allExpanded: props.allExpanded }; - } - - /** - * Set the state when new props are received. - * - * @param {Object} nextProps - The new props. - */ - componentWillReceiveProps(nextProps) { - if (nextProps.allExpanded !== this.state.allExpanded) { - this.setState({ allExpanded: nextProps.allExpanded }); - } - } - /** * Render the expand all button. * * @returns {React.Component} The expand all button. */ renderExpandAll() { - if (this.props.expandAll) { - const title = this.state.allExpanded ? 'Collapse All' : 'Expand All'; - const iconClass = this.state.allExpanded ? 'fa-angle-down' : 'fa-angle-right'; - return ( -
- -
- ); - } + const title = this.props.allExpanded ? 'Collapse All' : 'Expand All'; + const iconClass = this.props.allExpanded ? 'fa-angle-down' : 'fa-angle-right'; + return ( +
+ +
+ ); } /** @@ -58,32 +36,36 @@ class DocumentActions extends React.Component { render() { return (
- {this.renderExpandAll()} + {this.props.expandAll && this.renderExpandAll()}
- - } + {this.props.copy && - } + {this.props.clone && - } + {this.props.remove && + clickHandler={this.props.remove} + />}
); @@ -93,10 +75,10 @@ class DocumentActions extends React.Component { DocumentActions.displayName = 'DocumentActions'; DocumentActions.propTypes = { - edit: PropTypes.func.isRequired, - copy: PropTypes.func.isRequired, - remove: PropTypes.func.isRequired, - clone: PropTypes.func.isRequired, + copy: PropTypes.func, + edit: PropTypes.func, + remove: PropTypes.func, + clone: PropTypes.func, allExpanded: PropTypes.bool, expandAll: PropTypes.func }; diff --git a/packages/compass-crud/src/components/document-list-view.jsx b/packages/compass-crud/src/components/document-list-view.jsx index 24e1a5d8c80..ccad7a05af1 100644 --- a/packages/compass-crud/src/components/document-list-view.jsx +++ b/packages/compass-crud/src/components/document-list-view.jsx @@ -37,13 +37,15 @@ class DocumentListView extends React.Component { tz={this.props.tz} key={i} editable={this.props.isEditable} + isTimeSeries={this.props.isTimeSeries} version={this.props.version} copyToClipboard={this.props.copyToClipboard} removeDocument={this.props.removeDocument} replaceDocument={this.props.replaceDocument} updateDocument={this.props.updateDocument} openImportFileDialog={this.props.openImportFileDialog} - openInsertDocumentDialog={this.props.openInsertDocumentDialog} /> + openInsertDocumentDialog={this.props.openInsertDocumentDialog} + /> ); }); @@ -66,6 +68,7 @@ class DocumentListView extends React.Component { DocumentListView.propTypes = { docs: PropTypes.array.isRequired, isEditable: PropTypes.bool.isRequired, + isTimeSeries: PropTypes.bool, copyToClipboard: PropTypes.func, removeDocument: PropTypes.func, replaceDocument: PropTypes.func, diff --git a/packages/compass-crud/src/components/document-list.jsx b/packages/compass-crud/src/components/document-list.jsx index 1141e8511b7..2a108f7837f 100644 --- a/packages/compass-crud/src/components/document-list.jsx +++ b/packages/compass-crud/src/components/document-list.jsx @@ -231,6 +231,7 @@ DocumentList.propTypes = { insertMany: PropTypes.func, isEditable: PropTypes.bool.isRequired, isExportable: PropTypes.bool.isRequired, + isTimeSeries: PropTypes.bool, store: PropTypes.object.isRequired, openInsertDocumentDialog: PropTypes.func, openImportFileDialog: PropTypes.func, diff --git a/packages/compass-crud/src/components/document.jsx b/packages/compass-crud/src/components/document.jsx index ddc23cfbb7d..be28d3eba78 100644 --- a/packages/compass-crud/src/components/document.jsx +++ b/packages/compass-crud/src/components/document.jsx @@ -13,14 +13,27 @@ class Document extends React.Component { * @returns {React.Component} The component. */ render() { + if (this.props.editable && this.props.isTimeSeries) { + return ( + + ); + } if (this.props.editable) { return (); } return ( + expandAll={this.props.expandAll} + /> ); } } @@ -30,8 +43,10 @@ Document.displayName = 'Document'; Document.propTypes = { doc: PropTypes.object.isRequired, tz: PropTypes.string, + copyToClipboard: PropTypes.func, editable: PropTypes.bool, expandAll: PropTypes.bool, + isTimeSeries: PropTypes.bool, removeDocument: PropTypes.func, replaceDocument: PropTypes.func, updateDocument: PropTypes.func, diff --git a/packages/compass-crud/src/components/readonly-document.jsx b/packages/compass-crud/src/components/readonly-document.jsx index 9fc5e2e470f..68beccca654 100644 --- a/packages/compass-crud/src/components/readonly-document.jsx +++ b/packages/compass-crud/src/components/readonly-document.jsx @@ -1,5 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; + +import DocumentActions from './document-actions'; import Element from './element'; import ExpansionBar from './expansion-bar'; @@ -55,6 +57,17 @@ class ReadonlyDocument extends React.Component { this.setState({ renderSize: newLimit }); } + handleClone = () => { + this.props.openInsertDocumentDialog(this.props.doc.generateObject(), true); + } + + /** + * Handle copying JSON to clipboard of the document. + */ + handleCopy = () => { + this.props.copyToClipboard(this.props.doc); + } + /** * Get the elements for the document. * @@ -97,6 +110,15 @@ class ReadonlyDocument extends React.Component { ); } + renderActions() { + return ( + + ); + } + /** * Render a single document list item. * @@ -110,6 +132,7 @@ class ReadonlyDocument extends React.Component { {this.renderElements()} {this.renderExpansion()} + {this.renderActions()}
); @@ -119,8 +142,10 @@ class ReadonlyDocument extends React.Component { ReadonlyDocument.displayName = 'ReadonlyDocument'; ReadonlyDocument.propTypes = { + copyToClipboard: PropTypes.func, doc: PropTypes.object.isRequired, expandAll: PropTypes.bool, + openInsertDocumentDialog: PropTypes.func, tz: PropTypes.string }; diff --git a/packages/compass-crud/src/stores/crud-store.js b/packages/compass-crud/src/stores/crud-store.js index bf8f32fe36c..34102360313 100644 --- a/packages/compass-crud/src/stores/crud-store.js +++ b/packages/compass-crud/src/stores/crud-store.js @@ -104,6 +104,17 @@ export const setIsReadonly = (store, isReadonly) => { store.onReadonlyChanged(isReadonly); }; + +/** + * Set the isTimeSeries flag in the store. + * + * @param {Store} store - The store. + * @param {Boolean} isTimeSeries - If the collection is a time-series collection. + */ +export const setIsTimeSeries = (store, isTimeSeries) => { + store.onTimeSeriesChanged(isTimeSeries); +}; + /** * Set the namespace in the store. * @@ -172,6 +183,7 @@ const configureStore = (options = {}) => { query: this.getInitialQueryState(), isDataLake: false, isReadonly: false, + isTimeSeries: false, status: 'fetching', outdated: false, shardKeys: null @@ -252,6 +264,15 @@ const configureStore = (options = {}) => { this.setState({ isReadonly: isReadonly }); }, + /** + * Set if the collection is a time-series collection. + * + * @param {Boolean} isTimeSeries - If the collection is time-series. + */ + onTimeSeriesChanged(isTimeSeries) { + this.setState({ isTimeSeries: isTimeSeries }); + }, + /** * Plugin lifecycle method that is called when the namespace changes in * Compass. Trigger with new namespace and cleared path/types. @@ -1023,6 +1044,10 @@ const configureStore = (options = {}) => { setNamespace(store, options.namespace); } + if (options.isTimeSeries) { + setIsTimeSeries(store, options.isTimeSeries); + } + if (options.dataProvider) { setDataProvider( store, diff --git a/packages/compass-explain-plan/src/components/explain-json/explain-json.jsx b/packages/compass-explain-plan/src/components/explain-json/explain-json.jsx index 187d3365ad0..a79a4bdc733 100644 --- a/packages/compass-explain-plan/src/components/explain-json/explain-json.jsx +++ b/packages/compass-explain-plan/src/components/explain-json/explain-json.jsx @@ -2,6 +2,7 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import HadronDocument from 'hadron-document'; import { Document } from '@mongodb-js/compass-crud'; +import { clipboard } from 'electron'; import styles from './explain-json.less'; @@ -15,6 +16,10 @@ class ExplainJSON extends Component { rawExplainObject: PropTypes.object.isRequired } + copyToClipboard = () => { + clipboard.writeText(JSON.stringify(this.props.rawExplainObject.originalData)); + } + /** * Renders ExplainJSON component. * @@ -28,7 +33,11 @@ class ExplainJSON extends Component {
    - +
diff --git a/packages/compass-explain-plan/src/components/explain-json/explain-json.less b/packages/compass-explain-plan/src/components/explain-json/explain-json.less index a11e4ec8347..8048668d53e 100644 --- a/packages/compass-explain-plan/src/components/explain-json/explain-json.less +++ b/packages/compass-explain-plan/src/components/explain-json/explain-json.less @@ -7,7 +7,8 @@ padding: 0; .document-list { - padding-left: 15px; + padding: 0; + position: relative; } // Hacks for json view on explain tab diff --git a/packages/compass-sidebar/src/components/sidebar-collection/sidebar-collection.jsx b/packages/compass-sidebar/src/components/sidebar-collection/sidebar-collection.jsx index 3a0b6487ea1..01107e65393 100644 --- a/packages/compass-sidebar/src/components/sidebar-collection/sidebar-collection.jsx +++ b/packages/compass-sidebar/src/components/sidebar-collection/sidebar-collection.jsx @@ -5,12 +5,11 @@ import { DropdownButton, MenuItem } from 'react-bootstrap'; import toNS from 'mongodb-ns'; import Icon from '@leafygreen-ui/icon'; -import { collectionMetadata, getSource } from '../../modules/collection'; +import { collectionMetadata, getSource, TIME_SERIES_COLLECTION_TYPE } from '../../modules/collection'; import styles from './sidebar-collection.less'; const DEFAULT_COLLECTION_TYPE = 'collection'; -const TIME_SERIES_COLLECTION_TYPE = 'timeseries'; class SidebarCollection extends PureComponent { static displayName = 'SidebarCollection'; @@ -29,7 +28,8 @@ class SidebarCollection extends PureComponent { pipeline: PropTypes.any, // undefined or array if view collections: PropTypes.array.isRequired, type: PropTypes.string, - isDataLake: PropTypes.bool.isRequired + isDataLake: PropTypes.bool.isRequired, + isTimeSeries: PropTypes.bool }; /** diff --git a/packages/compass-sidebar/src/components/sidebar-database/sidebar-database.jsx b/packages/compass-sidebar/src/components/sidebar-database/sidebar-database.jsx index ab3f97993f2..edd7d40cabf 100644 --- a/packages/compass-sidebar/src/components/sidebar-database/sidebar-database.jsx +++ b/packages/compass-sidebar/src/components/sidebar-database/sidebar-database.jsx @@ -6,6 +6,7 @@ import styles from './sidebar-database.less'; import { TOOLTIP_IDS } from '../../constants/sidebar-constants'; import SidebarCollection from '../sidebar-collection'; +import { TIME_SERIES_COLLECTION_TYPE } from '../../modules/collection'; class SidebarDatabase extends PureComponent { static displayName = 'SidebarDatabase'; @@ -40,6 +41,7 @@ class SidebarDatabase extends PureComponent { isWritable: this.props.isWritable, description: this.props.description, isDataLake: this.props.isDataLake, + isTimeSeries: c.type === TIME_SERIES_COLLECTION_TYPE, collections: this.props.collections }; return ( diff --git a/packages/compass-sidebar/src/modules/collection.js b/packages/compass-sidebar/src/modules/collection.js index 6e3ef760629..25de42a8be4 100644 --- a/packages/compass-sidebar/src/modules/collection.js +++ b/packages/compass-sidebar/src/modules/collection.js @@ -45,6 +45,8 @@ export const getSourceViewOn = (database, source) => { return null; }; +export const TIME_SERIES_COLLECTION_TYPE = 'timeseries'; + /** * Get the collection metadata to pass to the collection plugin. * @@ -60,8 +62,9 @@ export const collectionMetadata = (collection, collections, database, editViewNa return { namespace: collection._id, isReadonly: collection.readonly, + isTimeSeries: collection.type === TIME_SERIES_COLLECTION_TYPE, sourceName: getSourceName(collection.readonly, database, collection.view_on), - isSourceReadonly: source ? source.readonly : false, + sourceReadonly: source ? source.readonly : false, sourceViewOn: getSourceViewOn(database, source), sourcePipeline: collection.pipeline, editViewName: editViewName diff --git a/packages/compass-sidebar/src/modules/collection.spec.js b/packages/compass-sidebar/src/modules/collection.spec.js index cf7f377d956..ae7b776736c 100644 --- a/packages/compass-sidebar/src/modules/collection.spec.js +++ b/packages/compass-sidebar/src/modules/collection.spec.js @@ -24,7 +24,13 @@ const VIEW_ON_VIEW = { pipeline: [] }; -const COLLECTIONS = [ COLL, VIEW, VIEW_ON_VIEW ]; +const TIME_SERIES = { + _id: 'db.testTimeSeries', + type: 'timeSeries', + readonly: false +}; + +const COLLECTIONS = [ COLL, VIEW, VIEW_ON_VIEW, TIME_SERIES ]; describe('collection module', () => { describe('#getSource', () => { @@ -80,8 +86,9 @@ describe('collection module', () => { expect(collectionMetadata(VIEW_ON_VIEW, COLLECTIONS, 'db', 'db.testView')).to.deep.equal({ namespace: 'db.testViewOnView', isReadonly: true, + isTimeSeries: false, sourceName: 'db.testView', - isSourceReadonly: true, + sourceReadonly: true, sourceViewOn: 'db.test', sourcePipeline: [], editViewName: 'db.testView' diff --git a/packages/databases-collections/src/modules/show-collection.js b/packages/databases-collections/src/modules/show-collection.js index 09cbf4b88e1..ce3a1f7f526 100644 --- a/packages/databases-collections/src/modules/show-collection.js +++ b/packages/databases-collections/src/modules/show-collection.js @@ -1,5 +1,7 @@ import find from 'lodash.find'; import toNS from 'mongodb-ns'; + +import { TIME_SERIES_COLLECTION_TYPE } from '../../../compass-sidebar/src/modules/collection'; import { appRegistryEmit } from './app-registry'; /** @@ -39,9 +41,10 @@ export const showCollection = (name) => { { namespace: collection._id, isReadonly: collection.readonly, + isTimeSeries: collection.type === TIME_SERIES_COLLECTION_TYPE, sourceName: collection.view_on ? `${state.databaseName}.${collection.view_on}` : null, editViewName: null, - isSourceReadonly: source ? source.readonly : false, + sourceReadonly: source ? source.readonly : false, sourceViewOn: source ? `${state.databaseName}.${source.view_on}` : null, sourcePipeline: collection.pipeline || null }