Antonio-Maranhao closed pull request #1153: [partitioned dbs] Show results for partitioned views and All Docs filtered by partition key URL: https://github.com/apache/couchdb-fauxton/pull/1153
This is a PR merged from a forked repository. As GitHub hides the original diff on merge, it is displayed below for the sake of provenance: As this is a foreign pull request (from a fork), the diff is supplied below (as it won't show otherwise due to GitHub magic): diff --git a/app/addons/documents/__tests__/fetch-actions.test.js b/app/addons/documents/__tests__/fetch-actions.test.js index ec7af92e5..7940487ec 100644 --- a/app/addons/documents/__tests__/fetch-actions.test.js +++ b/app/addons/documents/__tests__/fetch-actions.test.js @@ -181,10 +181,6 @@ describe('Docs Fetch API', () => { }); describe('queryAllDocs', () => { - const params = { - limit: 21, - skip: 0 - }; const docs = { "total_rows": 2, "offset": 0, @@ -207,12 +203,52 @@ describe('Docs Fetch API', () => { }; it('queries _all_docs with default params', () => { + const params = { + limit: 21, + skip: 0 + }; + const fetchUrl = '/testdb/_all_docs'; + const query = app.utils.queryString(params); + const url = `${fetchUrl}?${query}`; + fetchMock.getOnce(url, docs); + + return queryAllDocs(fetchUrl, '', params).then((res) => { + expect(res).toEqual({ + docType: Constants.INDEX_RESULTS_DOC_TYPE.VIEW, + docs: [ + { + id: "foo", + key: "foo", + value: { + rev: "1-1390740c4877979dbe8998382876556c" + } + }, + { + id: "foo2", + key: "foo2", + value: { + rev: "2-1390740c4877979dbe8998382876556c" + } + }] + }); + }); + }); + + it('queries _all_docs with a partition key', () => { + const partitionKey = 'key1'; + const params = { + limit: 21, + skip: 0, + inclusive_end: false, + start_key: `"${partitionKey}:"`, + end_key: `"${partitionKey}:\ufff0"` + }; const fetchUrl = '/testdb/_all_docs'; const query = app.utils.queryString(params); const url = `${fetchUrl}?${query}`; fetchMock.getOnce(url, docs); - return queryAllDocs(fetchUrl, params).then((res) => { + return queryAllDocs(fetchUrl, partitionKey, params).then((res) => { expect(res).toEqual({ docType: Constants.INDEX_RESULTS_DOC_TYPE.VIEW, docs: [ diff --git a/app/addons/documents/__tests__/results-toolbar.test.js b/app/addons/documents/__tests__/results-toolbar.test.js index 8c6b4d83c..45e2a1352 100644 --- a/app/addons/documents/__tests__/results-toolbar.test.js +++ b/app/addons/documents/__tests__/results-toolbar.test.js @@ -28,6 +28,9 @@ describe('Results Toolbar', () => { isLoading: false, queryOptionsParams: {}, databaseName: 'mydb', + fetchUrl: '/db1/_all_docs', + docType: Constants.INDEX_RESULTS_DOC_TYPE.VIEW, + hasResults: true, resultsStyle: { textOverflow: Constants.INDEX_RESULTS_STYLE.TEXT_OVERFLOW_TRUNCATED, fontSize: Constants.INDEX_RESULTS_STYLE.FONT_SIZE_MEDIUM @@ -106,7 +109,7 @@ describe('Results Toolbar', () => { sinon.assert.calledWith(mockUpdateStyle, { fontSize: Constants.INDEX_RESULTS_STYLE.FONT_SIZE_LARGE}); }); - it.only('does not show Display Density option in JSON layout', () => { + it('does not show Display Density option in JSON layout', () => { const toolbarJson = mount(<ResultsToolBar {...defaultProps} hasResults={true} @@ -133,4 +136,34 @@ describe('Results Toolbar', () => { expect(toolbarTable.find('li.header-label').at(0).text()).toBe('Display density'); expect(toolbarTable.find('li.header-label').at(1).text()).toBe('Font size'); }); + + it('shows Table, Metadata and JSON modes when querying a global view', () => { + const wrapper = mount(<ResultsToolBar + {...defaultProps} + hasResults={true} + isListDeletable={false} + partitionKey={''} + fetchUrl='/my_db/_design/ddoc1/_view/view1'/>); + expect(wrapper.find('button')).toHaveLength(4); + }); + + it('hides Table and JSON modes when querying a partitioned view', () => { + const wrapper = mount(<ResultsToolBar + {...defaultProps} + hasResults={true} + isListDeletable={false} + partitionKey={'partKey1'} + fetchUrl='/my_db/_partition/my_partition/_design/ddoc1/_view/view1'/>); + expect(wrapper.find('button')).toHaveLength(2); + }); + + it('shows Table, Metadata and JSON modes when showing All Documents filtered by partition', () => { + const wrapper = mount(<ResultsToolBar + {...defaultProps} + hasResults={true} + isListDeletable={false} + partitionKey={'partKey1'} + fetchUrl='/my_db/_all_docs'/>); + expect(wrapper.find('button')).toHaveLength(4); + }); }); diff --git a/app/addons/documents/assets/less/index-results.less b/app/addons/documents/assets/less/index-results.less index 8d05cfc2b..6ea7ddcde 100644 --- a/app/addons/documents/assets/less/index-results.less +++ b/app/addons/documents/assets/less/index-results.less @@ -97,6 +97,12 @@ a.document-result-screen__toolbar-create-btn:visited { padding-top: 8px; margin: 0 auto; } + .no-results-screen-warning { + text-align: center; + i { + padding-right: 0.5rem; + } + } } .watermark-logo { background: transparent url('../../../../../assets/img/couch-watermark.png') no-repeat 50% 50%; diff --git a/app/addons/documents/base.js b/app/addons/documents/base.js index 21515ebd8..8ccf98995 100644 --- a/app/addons/documents/base.js +++ b/app/addons/documents/base.js @@ -119,16 +119,16 @@ FauxtonAPI.registerUrls('designDocs', { }); FauxtonAPI.registerUrls('view', { - server: function (database, designDoc, viewName) { - return Helpers.getServerUrl('/' + database + '/_design/' + designDoc + '/_view/' + viewName); + server: function (database, partitionKey, designDoc, viewName) { + return Helpers.getServerUrl('/' + database + partitionUrlComponent(partitionKey) + '/_design/' + designDoc + '/_view/' + viewName); }, app: function (database, designDoc) { return 'database/' + database + '/_design/' + designDoc + '/_view/'; }, - apiurl: function (id, designDoc, viewName) { - return Helpers.getApiUrl('/' + id + '/_design/' + designDoc + '/_view/' + viewName); + apiurl: function (id, partitionKey, designDoc, viewName) { + return Helpers.getApiUrl('/' + id + partitionUrlComponent(partitionKey) + '/_design/' + designDoc + '/_view/' + viewName); }, edit: function (database, partitionKey, designDoc, indexName) { diff --git a/app/addons/documents/components/results-toolbar.js b/app/addons/documents/components/results-toolbar.js index 8c765c231..a610d2ffd 100644 --- a/app/addons/documents/components/results-toolbar.js +++ b/app/addons/documents/components/results-toolbar.js @@ -95,6 +95,7 @@ ResultsToolBar.propTypes = { hasResults: PropTypes.bool.isRequired, isListDeletable: PropTypes.bool, partitionKey: PropTypes.string, + docType: PropTypes.string, resultsStyle: PropTypes.object.isRequired, updateResultsStyle: PropTypes.func.isRequired }; diff --git a/app/addons/documents/header/header.js b/app/addons/documents/header/header.js index ba7c3b4b5..52093e522 100644 --- a/app/addons/documents/header/header.js +++ b/app/addons/documents/header/header.js @@ -23,7 +23,9 @@ export default class BulkDocumentHeaderController extends React.Component { const { selectedLayout, docType, - queryOptionsParams + queryOptionsParams, + partitionKey, + fetchUrl } = this.props; let metadata, json, table; @@ -38,10 +40,11 @@ export default class BulkDocumentHeaderController extends React.Component { return null; } - // reduce doesn't allow for include_docs=true, so we'll prevent JSON and table - // views since they force include_docs=true when reduce is checked in the - // query options panel. - if (!queryOptionsParams.reduce) { + // Reduce doesn't allow for include_docs=true, so we'll prevent JSON and table + // views since they force include_docs=true when reduce is checked in the query options panel. + // Partitioned queries don't supprt include_docs=true either. + const isAllDocsQuery = fetchUrl && fetchUrl.includes('/_all_docs'); + if (isAllDocsQuery || (!queryOptionsParams.reduce && !partitionKey)) { table = <Button className={selectedLayout === Constants.LAYOUT_ORIENTATION.TABLE ? 'active' : ''} onClick={this.toggleLayout.bind(this, Constants.LAYOUT_ORIENTATION.TABLE)} diff --git a/app/addons/documents/index-results/actions/base.js b/app/addons/documents/index-results/actions/base.js index cd9cc881a..c39f1f4d2 100644 --- a/app/addons/documents/index-results/actions/base.js +++ b/app/addons/documents/index-results/actions/base.js @@ -13,6 +13,18 @@ import ActionTypes from '../actiontypes'; import { getDocId, getDocRev, isJSONDocBulkDeletable } from "../helpers/shared-helpers"; +export const partitionParamNotSupported = () => { + return { + type: ActionTypes.INDEX_RESULTS_REDUX_PARTITION_PARAM_NOT_SUPPORTED + }; +}; + +export const partitionParamIsMandatory = () => { + return { + type: ActionTypes.INDEX_RESULTS_REDUX_PARTITION_PARAM_MANDATORY + }; +}; + export const nowLoading = () => { return { type: ActionTypes.INDEX_RESULTS_REDUX_IS_LOADING diff --git a/app/addons/documents/index-results/actions/fetch.js b/app/addons/documents/index-results/actions/fetch.js index fc8df4eb6..6fdb32c07 100644 --- a/app/addons/documents/index-results/actions/fetch.js +++ b/app/addons/documents/index-results/actions/fetch.js @@ -16,7 +16,7 @@ import Constants from '../../constants'; import { errorReason } from '../helpers/shared-helpers'; import * as IndexResultsAPI from '../api'; import { nowLoading, newResultsAvailable, newSelectedDocs, - changeLayout, resetState } from './base'; + changeLayout, resetState, partitionParamNotSupported, partitionParamIsMandatory } from './base'; const maxDocLimit = 10000; @@ -91,23 +91,29 @@ export const fetchDocs = (queryDocs, fetchParams, queryOptionsParams) => { dispatch(nowLoading()); // now fetch the results - return queryDocs(params).then(({ docs, docType, executionStats, warning }) => { + return queryDocs(params).then(({ docs, docType, executionStats, warning, layout }) => { const { finalDocList, canShowNext } = removeOverflowDocsAndCalculateHasNext(docs, totalDocsRemaining, params.limit); - if (docType === Constants.INDEX_RESULTS_DOC_TYPE.MANGO_INDEX) { - dispatch(changeLayout(Constants.LAYOUT_ORIENTATION.JSON)); + if (layout) { + dispatch(changeLayout(layout)); } // dispatch that we're all done dispatch(newResultsAvailable(finalDocList, params, canShowNext, docType, executionStats, warning)); }).catch((error) => { - FauxtonAPI.addNotification({ - msg: 'Error running query. ' + errorReason(error), - type: 'error', - clear: true - }); + if (error && error.message.includes('partition query is not supported')) { + dispatch(partitionParamNotSupported()); + } else if (error && error.message.includes('`partition` parameter is mandatory')) { + dispatch(partitionParamIsMandatory()); + } else { + FauxtonAPI.addNotification({ + msg: 'Error running query. ' + errorReason(error), + type: 'error', + clear: true + }); + } dispatch(resetState()); }); }; diff --git a/app/addons/documents/index-results/actiontypes.js b/app/addons/documents/index-results/actiontypes.js index d69c15d44..fbd9aa2f0 100644 --- a/app/addons/documents/index-results/actiontypes.js +++ b/app/addons/documents/index-results/actiontypes.js @@ -30,5 +30,7 @@ export default { INDEX_RESULTS_REDUX_CHANGE_TABLE_HEADER_ATTRIBUTE: 'INDEX_RESULTS_REDUX_CHANGE_TABLE_HEADER_ATTRIBUTE', INDEX_RESULTS_REDUX_RESET_STATE: 'INDEX_RESULTS_REDUX_RESET_STATE', INDEX_RESULTS_REDUX_NEW_QUERY_OPTIONS: 'INDEX_RESULTS_REDUX_NEW_QUERY_OPTIONS', + INDEX_RESULTS_REDUX_PARTITION_PARAM_NOT_SUPPORTED: 'INDEX_RESULTS_REDUX_PARTITION_PARAM_NOT_SUPPORTED', + INDEX_RESULTS_REDUX_PARTITION_PARAM_MANDATORY: 'INDEX_RESULTS_REDUX_PARTITION_PARAM_MANDATORY', INDEX_RESULTS_SET_STYLE: 'INDEX_RESULTS_SET_STYLE' }; diff --git a/app/addons/documents/index-results/api.js b/app/addons/documents/index-results/api.js index 9f836fedc..9278879b8 100644 --- a/app/addons/documents/index-results/api.js +++ b/app/addons/documents/index-results/api.js @@ -16,9 +16,13 @@ import app from '../../../app'; import Constants from '../constants'; import FauxtonAPI from '../../../core/api'; -export const queryAllDocs = (fetchUrl, params) => { +export const queryAllDocs = (fetchUrl, partitionKey, params) => { // Exclude params 'group', 'reduce' and 'group_level' if present since they not allowed for '_all_docs' Object.assign(params, {reduce: undefined, group: undefined, group_level: undefined}); + if (partitionKey) { + // partition filter overrides any 'between keys' values set + Object.assign(params, {inclusive_end: false, start_key: `"${partitionKey}:"`, end_key: `"${partitionKey}:\ufff0"`}); + } const query = app.utils.queryString(params); const url = `${fetchUrl}${fetchUrl.includes('?') ? '&' : '?'}${query}`; return get(url).then(json => { @@ -43,6 +47,13 @@ export const queryMapReduceView = (fetchUrl, params) => { params.group = undefined; params.group_level = undefined; } + // removes params not supported by partitioned views + const isPartitioned = fetchUrl.includes('/_partition/'); + if (isPartitioned) { + params.include_docs = undefined; + params.stable = undefined; + params.conflicts = undefined; + } const query = app.utils.queryString(params); const url = `${fetchUrl}${fetchUrl.includes('?') ? '&' : '?'}${query}`; return get(url).then(json => { @@ -51,7 +62,8 @@ export const queryMapReduceView = (fetchUrl, params) => { } return { docs: json.rows, - docType: Constants.INDEX_RESULTS_DOC_TYPE.VIEW + docType: Constants.INDEX_RESULTS_DOC_TYPE.VIEW, + layout: isPartitioned ? Constants.LAYOUT_ORIENTATION.METADATA : undefined }; }); }; diff --git a/app/addons/documents/index-results/components/results/IndexResults.js b/app/addons/documents/index-results/components/results/IndexResults.js index 484d3799b..01982fa45 100644 --- a/app/addons/documents/index-results/components/results/IndexResults.js +++ b/app/addons/documents/index-results/components/results/IndexResults.js @@ -41,11 +41,12 @@ export default class IndexResults extends React.Component { queryOptionsParams, ddocsOnly, fetchUrl, - resetState + resetState, + partitionKey } = nextProps; // Indicates the selected sidebar item has changed, so it needs to fetch the new list of docs - if (this.props.ddocsOnly !== ddocsOnly || this.props.fetchUrl !== fetchUrl) { + if (this.props.ddocsOnly !== ddocsOnly || this.props.fetchUrl !== fetchUrl || this.props.partitionKey !== partitionKey) { resetState(); // Need to reset skip and reduce here because 'resetState()' // won't change props until the next update cycle diff --git a/app/addons/documents/index-results/components/results/NoResultsScreen.js b/app/addons/documents/index-results/components/results/NoResultsScreen.js index 0d4e8623d..c01fb01ae 100644 --- a/app/addons/documents/index-results/components/results/NoResultsScreen.js +++ b/app/addons/documents/index-results/components/results/NoResultsScreen.js @@ -14,11 +14,18 @@ import PropTypes from 'prop-types'; import React from 'react'; -export default function NoResultsScreen ({ text }) { +export default function NoResultsScreen ({ text, isWarning }) { + const warningMsg = ( + <div className='no-results-screen-warning'> + <i className='fonticon-attention-circled'></i> + {text} + </div> + ); return ( <div className="no-results-screen"> + {isWarning ? warningMsg : null} <div className="watermark-logo"></div> - <h3>{text}</h3> + {!isWarning ? <h3>{text}</h3> : null} </div> ); } diff --git a/app/addons/documents/index-results/components/results/ResultsScreen.js b/app/addons/documents/index-results/components/results/ResultsScreen.js index 86a7bb4aa..016f77cab 100644 --- a/app/addons/documents/index-results/components/results/ResultsScreen.js +++ b/app/addons/documents/index-results/components/results/ResultsScreen.js @@ -116,6 +116,8 @@ export default class ResultsScreen extends React.Component { if (this.props.isLoading) { mainView = <div className="loading-lines-wrapper"><LoadLines /></div>; + } else if (this.props.noResultsWarning) { + mainView = <NoResultsScreen text={this.props.noResultsWarning} isWarning={true}/>; } else if (!this.props.hasResults) { mainView = <NoResultsScreen text={this.props.textEmptyIndex}/>; } else if (this.props.selectedLayout === Constants.LAYOUT_ORIENTATION.JSON) { diff --git a/app/addons/documents/index-results/containers/IndexResultsContainer.js b/app/addons/documents/index-results/containers/IndexResultsContainer.js index 7914265ef..f0eca768e 100644 --- a/app/addons/documents/index-results/containers/IndexResultsContainer.js +++ b/app/addons/documents/index-results/containers/IndexResultsContainer.js @@ -44,6 +44,7 @@ import { const mapStateToProps = ({indexResults}, ownProps) => { return { + noResultsWarning: indexResults.noResultsWarning, docs: getDocs(indexResults), selectedDocs: getSelectedDocs(indexResults), isLoading: getIsLoading(indexResults), diff --git a/app/addons/documents/index-results/reducers.js b/app/addons/documents/index-results/reducers.js index 5d040c3b9..817ae6e15 100644 --- a/app/addons/documents/index-results/reducers.js +++ b/app/addons/documents/index-results/reducers.js @@ -18,6 +18,7 @@ import {getTableViewData} from './helpers/table-view'; import {getDefaultPerPage, getDocId, isJSONDocBulkDeletable} from './helpers/shared-helpers'; const initialState = { + noResultsWarning: '', docs: [], // raw documents returned from couch selectedDocs: [], // documents selected for manipulation isLoading: false, @@ -94,6 +95,7 @@ export default function resultsState(state = initialState, action) { case ActionTypes.INDEX_RESULTS_REDUX_RESET_STATE: return { ...initialState, + noResultsWarning: state.noResultsWarning, selectedLayout: state.selectedLayout, selectedDocs: [], fetchParams: { @@ -115,6 +117,16 @@ export default function resultsState(state = initialState, action) { isLoading: true }; + case ActionTypes.INDEX_RESULTS_REDUX_PARTITION_PARAM_NOT_SUPPORTED: + return Object.assign({}, state, { + noResultsWarning: 'The selected index does not support partitions. Switch back to global mode.' + }); + + case ActionTypes.INDEX_RESULTS_REDUX_PARTITION_PARAM_MANDATORY: + return Object.assign({}, state, { + noResultsWarning: 'The selected index requires a partition key. Use the selector at the top to enter a partition key.' + }); + case ActionTypes.INDEX_RESULTS_REDUX_NEW_SELECTED_DOCS: return { ...state, @@ -133,6 +145,7 @@ export default function resultsState(state = initialState, action) { ...state, docs: action.docs, isLoading: false, + noResultsWarning: '', isEditable: true, //TODO: determine logic for this fetchParams: Object.assign({}, state.fetchParams, action.params), pagination: Object.assign({}, state.pagination, { diff --git a/app/addons/documents/layouts.js b/app/addons/documents/layouts.js index 7f91c8e12..0d5932955 100644 --- a/app/addons/documents/layouts.js +++ b/app/addons/documents/layouts.js @@ -81,6 +81,7 @@ export const TabsSidebarHeader = ({ fetchUrl={fetchUrl} ddocsOnly={ddocsOnly} queryDocs={queryDocs} + partitionKey={partitionKey} selectedNavItem={selectedNavItem} /> </div> <ApiBarContainer docURL={docURL} endpoint={endpoint} endpointAddQueryOptions={endpointAddQueryOptions} /> @@ -175,7 +176,8 @@ export const DocsTabsSidebarLayout = ({ onGlobalModeSelected, globalMode }) => { - let queryDocs = (params) => { return queryAllDocs(fetchUrl, params); }; + const partitionFilter = selectedNavItem.navItem === 'all-docs' && partitionKey ? partitionKey : ''; + let queryDocs = (params) => { return queryAllDocs(fetchUrl, partitionFilter, params); }; if (Helpers.isViewSelected(selectedNavItem)) { queryDocs = (params) => { return queryMapReduceView(fetchUrl, params); }; } diff --git a/app/addons/documents/mango/mango.api.js b/app/addons/documents/mango/mango.api.js index 067117225..21e5ecd20 100644 --- a/app/addons/documents/mango/mango.api.js +++ b/app/addons/documents/mango/mango.api.js @@ -49,7 +49,8 @@ export const fetchIndexes = (databaseName, params) => { } return { docs: json.indexes, - docType: Constants.INDEX_RESULTS_DOC_TYPE.MANGO_INDEX + docType: Constants.INDEX_RESULTS_DOC_TYPE.MANGO_INDEX, + layout: Constants.LAYOUT_ORIENTATION.JSON }; }); }; diff --git a/app/addons/documents/routes-documents.js b/app/addons/documents/routes-documents.js index fc26dd8e7..b86cb6b60 100644 --- a/app/addons/documents/routes-documents.js +++ b/app/addons/documents/routes-documents.js @@ -100,8 +100,8 @@ var DocumentsRouteObject = BaseRoute.extend({ * They are not the same when paginating */ allDocs: function (databaseName, partitionKey, options) { - const params = this.createParams(options), - docParams = params.docParams; + const params = this.createParams(options); + const docParams = params.docParams; const url = FauxtonAPI.urls('allDocsSanitized', 'server', databaseName); diff --git a/app/addons/documents/routes-index-editor.js b/app/addons/documents/routes-index-editor.js index bfe9c42ba..121afb512 100644 --- a/app/addons/documents/routes-index-editor.js +++ b/app/addons/documents/routes-index-editor.js @@ -90,9 +90,10 @@ const IndexEditorAndResults = BaseRoute.extend({ }); SidebarActions.dispatchExpandSelectedItem(selectedNavItem); - const url = FauxtonAPI.urls('view', 'server', encodeURIComponent(databaseName), + const encodedPartKey = partitionKey ? encodeURIComponent(partitionKey) : ''; + const url = FauxtonAPI.urls('view', 'server', encodeURIComponent(databaseName), encodedPartKey, encodeURIComponent(ddoc), encodeURIComponent(viewName)); - const endpoint = FauxtonAPI.urls('view', 'apiurl', encodeURIComponent(databaseName), + const endpoint = FauxtonAPI.urls('view', 'apiurl', encodeURIComponent(databaseName), encodedPartKey, encodeURIComponent(ddoc), encodeURIComponent(viewName)); const docURL = FauxtonAPI.constants.DOC_URLS.GENERAL; const navigateToPartitionedView = (partKey) => { ---------------------------------------------------------------- This is an automated message from the Apache Git Service. To respond to the message, please log on GitHub and use the URL above to go to the specific comment. For queries about this service, please contact Infrastructure at: us...@infra.apache.org With regards, Apache Git Services