You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@couchdb.apache.org by wi...@apache.org on 2019/09/25 12:58:13 UTC
[couchdb-fauxton] 01/02: Import search
This is an automated email from the ASF dual-hosted git repository.
willholley pushed a commit to branch search_addon
in repository https://gitbox.apache.org/repos/asf/couchdb-fauxton.git
commit b2069d491372d73df4667bbb97fe2788165e2608
Author: Will Holley <wi...@gmail.com>
AuthorDate: Tue Sep 24 15:35:21 2019 +0100
Import search
---
.gitignore | 1 +
app/addons/search/__tests__/components.test.js | 160 ++++++++
app/addons/search/__tests__/search.actions.test.js | 52 +++
.../search/__tests__/search.reducers.test.js | 142 +++++++
app/addons/search/actions.js | 428 +++++++++++++++++++++
app/addons/search/actiontypes.js | 36 ++
app/addons/search/api.js | 32 ++
app/addons/search/assets/less/search.less | 159 ++++++++
app/addons/search/base.js | 125 ++++++
app/addons/search/components/Analyzer.js | 142 +++++++
app/addons/search/components/AnalyzerDropdown.js | 77 ++++
app/addons/search/components/AnalyzerMultiple.js | 94 +++++
app/addons/search/components/AnalyzerRow.js | 84 ++++
app/addons/search/components/SearchForm.js | 126 ++++++
.../search/components/SearchFormContainer.js | 36 ++
app/addons/search/components/SearchIndexEditor.js | 164 ++++++++
.../components/SearchIndexEditorContainer.js | 82 ++++
app/addons/search/constants.js | 18 +
app/addons/search/layout.js | 101 +++++
app/addons/search/reducers.js | 314 +++++++++++++++
app/addons/search/resources.js | 72 ++++
app/addons/search/routes.js | 272 +++++++++++++
.../search/tests/nightwatch/cloneSearchIndex.js | 68 ++++
.../search/tests/nightwatch/createNewSearch.js | 130 +++++++
.../search/tests/nightwatch/deleteSearchIndex.js | 107 ++++++
.../search/tests/nightwatch/searchPageApiBar.js | 55 +++
app/addons/search/tests/nightwatch/sharedSearch.js | 39 ++
27 files changed, 3116 insertions(+)
diff --git a/.gitignore b/.gitignore
index 7865768..e8b3d39 100644
--- a/.gitignore
+++ b/.gitignore
@@ -20,6 +20,7 @@ app/addons/*
!app/addons/styletests
!app/addons/cors
!app/addons/setup
+!app/addons/search
settings.json*
i18n.json
!settings.json.default
diff --git a/app/addons/search/__tests__/components.test.js b/app/addons/search/__tests__/components.test.js
new file mode 100644
index 0000000..6bb6f39
--- /dev/null
+++ b/app/addons/search/__tests__/components.test.js
@@ -0,0 +1,160 @@
+/*
+* Licensed Materials - Property of IBM
+*
+* "Restricted Materials of IBM"
+*
+* (C) Copyright IBM Corp. 2018 All Rights Reserved
+*
+* US Government Users Restricted Rights - Use, duplication or disclosure
+* restricted by GSA ADP Schedule Contract with IBM Corp.
+*/
+
+import {mount} from 'enzyme';
+import React from 'react';
+import sinon from 'sinon';
+import FauxtonAPI from '../../../core/api';
+import AnalyzerDropdown from '../components/AnalyzerDropdown';
+import SearchForm from '../components/SearchForm';
+import SearchIndexEditor from '../components/SearchIndexEditor';
+import '../base';
+
+describe('SearchIndexEditor', () => {
+ const defaultProps = {
+ isLoading: false,
+ isCreatingIndex: false,
+ database: { id: 'my_db' },
+ lastSavedDesignDocName: 'last_ddoc',
+ lastSavedSearchIndexName: 'last_idx',
+ searchIndexFunction: '',
+ saveDoc: {},
+ designDocs: [],
+ searchIndexName: '',
+ ddocPartitioned: false,
+ newDesignDocPartitioned: false,
+ analyzerType: '',
+ singleAnalyzer: '',
+ defaultAnalyzer: '',
+ defaultMultipleAnalyzer: '',
+ analyzerFields: [],
+ setAnalyzerType: () => {},
+ setDefaultMultipleAnalyzer: () => {},
+ setSingleAnalyzer: () => {},
+ addAnalyzerRow: () => {},
+ setSearchIndexName: () => {},
+ saveSearchIndex: () => {},
+ selectDesignDoc: () => {},
+ updateNewDesignDocName: () => {}
+ };
+
+ it('generates the correct cancel link when db, ddoc and views have special chars', () => {
+ const editorEl = mount(<SearchIndexEditor
+ {...defaultProps}
+ database={{ id: 'db%$1' }}
+ lastSavedDesignDocName={'_design/doc/1$2'}
+ lastSavedSearchIndexName={'search?abc/123'}
+ />);
+ const expectedUrl = `/${encodeURIComponent('db%$1')}/_design/${encodeURIComponent('doc/1$2')}/_search/${encodeURIComponent('search?abc/123')}`;
+ expect(editorEl.find('a.index-cancel-link').prop('href')).toMatch(expectedUrl);
+ });
+
+ it('does not save when missing the index name', () => {
+ const spy = sinon.stub();
+ const editorEl = mount(<SearchIndexEditor
+ {...defaultProps}
+ database={{ id: 'test_db' }}
+ designDocs={[{id: '_design/d1'}, {id: '_design/d2'}]}
+ ddocName='_design/d1'
+ searchIndexName={''}
+ saveSearchIndex={spy}
+ saveDoc={{id: '_design/d'}}
+ />);
+
+ editorEl.find('button#save-index').simulate('click', {preventDefault: () => {}});
+ sinon.assert.notCalled(spy);
+ });
+});
+
+describe('AnalyzerDropdown', () => {
+
+ it('check default values and settings', () => {
+ const el = mount(<AnalyzerDropdown />);
+
+ // confirm default label
+ expect(el.find('label').length).toBe(2);
+ expect(el.find('label').first().text()).toBe('Type');
+
+ // confirm default value
+ expect(el.find('select').hasClass('standard')).toBeTruthy();
+ });
+
+ it('omits label element if empty label passed', () => {
+ const el = mount(<AnalyzerDropdown label="" />);
+
+ // (1, because there are normally 2 labels, see prev test)
+ expect(el.find('label').length).toBe(1);
+ });
+
+ it('custom ID works', () => {
+ const customID = 'myCustomID';
+ const el = mount(<AnalyzerDropdown id={customID} />);
+ expect(el.find('select').prop('id')).toBe(customID);
+ });
+
+ it('sets default value', () => {
+ const defaultSelected = 'russian';
+ const el = mount(
+ <AnalyzerDropdown defaultSelected={defaultSelected} />
+ );
+
+ expect(el.find('select').hasClass(defaultSelected)).toBeTruthy();
+ });
+
+ it('custom classes get applied', () => {
+ const el = mount(<AnalyzerDropdown classes="nuthatch vulture" />);
+ expect(el.find('.nuthatch').exists()).toBeTruthy();
+ expect(el.find('.vulture').exists()).toBeTruthy();
+ });
+
+ it('custom change handler gets called', () => {
+ const spy = sinon.spy();
+ const el = mount(<AnalyzerDropdown onChange={spy} />);
+ const newVal = 'whitespace';
+ el.find('select').simulate('change', { target: { value: newVal }});
+ expect(spy.calledOnce).toBeTruthy();
+ });
+
+});
+
+describe('SearchForm', () => {
+ const defaultProps = {
+ searchResults: [{id: 'elephant'}],
+ searchPerformed: true,
+ hasActiveQuery: false,
+ searchQuery: 'a_search',
+ database: { id: 'foo' },
+ querySearch: () => {},
+ setSearchQuery: () => {}
+ };
+
+ beforeEach(() => {
+ sinon.stub(FauxtonAPI, 'urls').returns('/fake/url');
+ });
+
+ afterEach(() => {
+ FauxtonAPI.urls.restore();
+ });
+
+ it('renders docs from the search results', () => {
+ const el = mount(<SearchForm
+ {...defaultProps}
+ />);
+ expect(el.find('pre').first().text('elephant')).toBeTruthy();
+ });
+
+ it('renders with links', () => {
+ const el = mount(<SearchForm
+ {...defaultProps}
+ />);
+ expect(el.find('a')).toBeTruthy();
+ });
+});
diff --git a/app/addons/search/__tests__/search.actions.test.js b/app/addons/search/__tests__/search.actions.test.js
new file mode 100644
index 0000000..99edd28
--- /dev/null
+++ b/app/addons/search/__tests__/search.actions.test.js
@@ -0,0 +1,52 @@
+/*
+* Licensed Materials - Property of IBM
+*
+* "Restricted Materials of IBM"
+*
+* (C) Copyright IBM Corp. 2018 All Rights Reserved
+*
+* US Government Users Restricted Rights - Use, duplication or disclosure
+* restricted by GSA ADP Schedule Contract with IBM Corp.
+*/
+
+import sinon from 'sinon';
+import utils from '../../../../test/mocha/testUtils';
+import FauxtonAPI from '../../../core/api';
+import Actions from '../actions';
+import * as API from '../api';
+import '../base';
+import '../../documents/base';
+
+const {restore} = utils;
+FauxtonAPI.router = new FauxtonAPI.Router([]);
+
+describe('search actions', () => {
+
+ afterEach(() => {
+ restore(FauxtonAPI.navigate);
+ restore(FauxtonAPI.addNotification);
+ restore(API.fetchSearchResults);
+ });
+
+ it("should show a notification and redirect if database doesn't exist", () => {
+ const navigateSpy = sinon.spy(FauxtonAPI, 'navigate');
+ const notificationSpy = sinon.spy(FauxtonAPI, 'addNotification');
+ sinon.stub(API, 'fetchSearchResults').rejects(new Error('db not found'));
+ FauxtonAPI.reduxDispatch = () => {};
+
+ const params = {
+ databaseName: 'safe-id-db',
+ designDoc: 'design-doc',
+ indexName: 'idx1',
+ query: 'a_query'
+ };
+ return Actions.dispatchInitSearchIndex(params)
+ .then(() => {
+ expect(notificationSpy.calledOnce).toBeTruthy();
+ expect(/db not found/.test(notificationSpy.args[0][0].msg)).toBeTruthy();
+ expect(navigateSpy.calledOnce).toBeTruthy();
+ expect(navigateSpy.args[0][0]).toBe('database/safe-id-db/_all_docs');
+ });
+ });
+
+});
diff --git a/app/addons/search/__tests__/search.reducers.test.js b/app/addons/search/__tests__/search.reducers.test.js
new file mode 100644
index 0000000..f08d269
--- /dev/null
+++ b/app/addons/search/__tests__/search.reducers.test.js
@@ -0,0 +1,142 @@
+/*
+* Licensed Materials - Property of IBM
+*
+* "Restricted Materials of IBM"
+*
+* (C) Copyright IBM Corp. 2018 All Rights Reserved
+*
+* US Government Users Restricted Rights - Use, duplication or disclosure
+* restricted by GSA ADP Schedule Contract with IBM Corp.
+*/
+
+import reducer from '../reducers';
+import ActionTypes from '../actiontypes';
+
+describe('Search Reducer', () => {
+
+ it('adds an analyzer row', () => {
+ const action = {
+ type: ActionTypes.SEARCH_INDEX_ADD_ANALYZER_ROW,
+ options: {
+ analyzer: 'sample',
+ fieldName: 'f1'
+ }
+ };
+ let newState = reducer(undefined, { type: 'DO_NOTHING' });
+ expect(newState.analyzerFields).toHaveLength(0);
+ newState = reducer(newState, action);
+ expect(newState.analyzerFields).toHaveLength(1);
+ expect(newState.analyzerFields[0].analyzer).toBe('sample');
+ expect(newState.analyzerFields[0].valid).toBe(true);
+ });
+
+ it('updates field name of an existing row', () => {
+ const action = {
+ type: ActionTypes.SEARCH_INDEX_ADD_ANALYZER_ROW,
+ options: {
+ analyzer: 'sample',
+ fieldName: 'f1'
+ }
+ };
+ let newState = reducer(undefined, action);
+ action.options.fieldName = 'f2';
+ newState = reducer(newState, action);
+ action.options.fieldName = 'f3';
+ newState = reducer(newState, action);
+
+ const removeAction = {
+ type: ActionTypes.SEARCH_INDEX_SET_ANALYZER_ROW_FIELD_NAME,
+ options: { rowIndex: 1, fieldName: 'f100' }
+ };
+ newState = reducer(newState, removeAction);
+ expect(newState.analyzerFields[0].fieldName).toBe('f1');
+ expect(newState.analyzerFields[1].fieldName).toBe('f100');
+ expect(newState.analyzerFields[2].fieldName).toBe('f3');
+ });
+
+ it('updates analyzer of an existing row', () => {
+ const action = {
+ type: ActionTypes.SEARCH_INDEX_ADD_ANALYZER_ROW,
+ options: {
+ analyzer: 'sample',
+ fieldName: 'f1'
+ }
+ };
+ let newState = reducer(undefined, action);
+ action.options.fieldName = 'f2';
+ newState = reducer(newState, action);
+ action.options.fieldName = 'f3';
+ newState = reducer(newState, action);
+
+ const removeAction = {
+ type: ActionTypes.SEARCH_INDEX_SET_ANALYZER_ROW,
+ options: { rowIndex: 1, analyzer: 'keyword' }
+ };
+ newState = reducer(newState, removeAction);
+ expect(newState.analyzerFields[0].analyzer).toBe('sample');
+ expect(newState.analyzerFields[1].analyzer).toBe('keyword');
+ expect(newState.analyzerFields[2].analyzer).toBe('sample');
+ });
+
+ it('removes an analyzer row', () => {
+ const action = {
+ type: ActionTypes.SEARCH_INDEX_ADD_ANALYZER_ROW,
+ options: {
+ analyzer: 'sample',
+ fieldName: 'f1'
+ }
+ };
+ let newState = reducer(undefined, action);
+ action.options.fieldName = 'f2';
+ newState = reducer(newState, action);
+ action.options.fieldName = 'f3';
+ newState = reducer(newState, action);
+
+ const removeAction = {
+ type: ActionTypes.SEARCH_INDEX_REMOVE_ANALYZER_ROW,
+ options: { rowIndex: 1 }
+ };
+ newState = reducer(newState, removeAction);
+ expect(newState.analyzerFields).toHaveLength(2);
+ expect(newState.analyzerFields[0].fieldName).toBe('f1');
+ expect(newState.analyzerFields[1].fieldName).toBe('f3');
+ });
+
+ it('updates search results and resets the hasActiveQuery flag', () => {
+ let newState = reducer(undefined, {
+ type: ActionTypes.SEARCH_INDEX_PREVIEW_REQUEST_MADE
+ });
+ expect(newState.hasActiveQuery).toBe(true);
+
+ const action = {
+ type: ActionTypes.SEARCH_INDEX_PREVIEW_MODEL_UPDATED,
+ options: {
+ searchResults: ['result1', 'result2']
+ }
+ };
+ newState = reducer(newState, action);
+ expect(newState.searchResults).toHaveLength(2);
+ expect(newState.hasActiveQuery).toBe(false);
+ });
+
+ it('resets the search results when the search term is empty', () => {
+ const action = {
+ type: ActionTypes.SEARCH_INDEX_PREVIEW_MODEL_UPDATED,
+ options: {
+ searchResults: ['result1', 'result2']
+ }
+ };
+ let newState = reducer(undefined, action);
+ expect(newState.searchResults).toHaveLength(2);
+
+ const initAction = {
+ type: ActionTypes.SEARCH_INDEX_INIT,
+ options: {
+ searchQuery: ''
+ }
+ };
+ newState = reducer(newState, initAction);
+ expect(newState.searchResults).toBeUndefined();
+ });
+
+});
diff --git a/app/addons/search/actions.js b/app/addons/search/actions.js
new file mode 100644
index 0000000..82ac0f1
--- /dev/null
+++ b/app/addons/search/actions.js
@@ -0,0 +1,428 @@
+/*
+* Licensed Materials - Property of IBM
+*
+* "Restricted Materials of IBM"
+*
+* (C) Copyright IBM Corp. 2018 All Rights Reserved
+*
+* US Government Users Restricted Rights - Use, duplication or disclosure
+* restricted by GSA ADP Schedule Contract with IBM Corp.
+*/
+
+import FauxtonAPI from '../../core/api';
+import ActionTypes from './actiontypes';
+import CloudantSearch from './resources';
+import Documents from '../documents/base';
+import SidebarActions from '../documents/sidebar/actions';
+import IndexEditorActions from '../documents/index-editor/actions';
+import * as API from './api';
+
+const dispatchInitNewSearchIndex = (params) => {
+ // ensure we start with a clear slate
+ FauxtonAPI.reduxDispatch({ type: ActionTypes.SEARCH_INDEX_CLEAR });
+ FauxtonAPI.reduxDispatch({
+ type: ActionTypes.SEARCH_INDEX_SET_LOADING,
+ options: {
+ loading: true
+ }
+ });
+
+ params.designDocs.fetch().then(() => {
+ FauxtonAPI.reduxDispatch({
+ type: ActionTypes.SEARCH_INDEX_DESIGN_DOCS_LOADED,
+ options: {
+ designDocs: params.designDocs,
+ defaultDDoc: params.defaultDDoc,
+ database: params.database
+ }
+ });
+ });
+};
+
+const dispatchInitSearchIndex = (params) => {
+ FauxtonAPI.reduxDispatch({
+ type: ActionTypes.SEARCH_INDEX_SET_LOADING,
+ options: {
+ loading: true
+ }
+ });
+
+ FauxtonAPI.reduxDispatch({
+ type: ActionTypes.SEARCH_INDEX_INIT,
+ options: {
+ databaseName: params.databaseName,
+ partitionKey: params.partitionKey,
+ ddocName: params.designDoc,
+ indexName: params.indexName,
+ searchQuery: params.query ? params.query : ''
+ }
+ });
+
+ if (params.query) {
+ return executSearchQuery(params.databaseName, params.partitionKey, params.designDoc,
+ params.indexName, params.query, FauxtonAPI.reduxDispatch);
+ }
+};
+
+const dispatchEditSearchIndex = (params) => {
+ var ddocInfo = new Documents.DdocInfo({ _id: params.ddocID }, { database: params.database });
+
+ FauxtonAPI.reduxDispatch({
+ type: ActionTypes.SEARCH_INDEX_SET_LOADING,
+ options: {
+ loading: true
+ }
+ });
+
+ FauxtonAPI.Promise.all([params.designDocs.fetch(), ddocInfo.fetch()]).then(([ddocs]) => {
+ const ddoc = ddocs.rows.find(ddoc => ddoc._id === ddocInfo.id).doc;
+ if (!ddoc.indexes || !ddoc.indexes[params.indexName]) {
+ throw Error(`Index "${params.indexName}" not found`);
+ }
+ FauxtonAPI.reduxDispatch({
+ type: ActionTypes.SEARCH_INDEX_INIT_EDIT_SEARCH_INDEX,
+ options: {
+ indexName: params.indexName,
+ database: params.database,
+ ddocInfo: ddocInfo,
+ designDocs: params.designDocs
+ }
+ });
+ }).catch(err => {
+ const details = err.message ? err.message : '';
+ FauxtonAPI.addNotification({
+ msg: `There was a problem editing the search index "${params.indexName}". ` + details,
+ type: 'error',
+ clear: true
+ });
+ });
+};
+
+const selectTab = (tab) => (dispatch) => {
+ dispatch({
+ type: ActionTypes.SEARCH_INDEX_SELECT_TAB,
+ options: {
+ tab: tab
+ }
+ });
+};
+
+const setSearchIndexName = (str) => (dispatch) => {
+ dispatch({
+ type: ActionTypes.SEARCH_INDEX_SET_NAME,
+ options: {
+ value: str
+ }
+ });
+};
+
+const setAnalyzerType = (type) => (dispatch) => {
+ dispatch({
+ type: ActionTypes.SEARCH_INDEX_SET_ANALYZER_TYPE,
+ options: {
+ value: type
+ }
+ });
+};
+
+const addAnalyzerRow = (analyzer) => (dispatch) => {
+ dispatch({
+ type: ActionTypes.SEARCH_INDEX_ADD_ANALYZER_ROW,
+ options: {
+ analyzer: analyzer
+ }
+ });
+};
+
+const removeAnalyzerRow = (rowIndex) => (dispatch) => {
+ dispatch({
+ type: ActionTypes.SEARCH_INDEX_REMOVE_ANALYZER_ROW,
+ options: {
+ rowIndex: rowIndex
+ }
+ });
+};
+
+const setAnalyzerRowFieldName = (params) => (dispatch) => {
+ dispatch({
+ type: ActionTypes.SEARCH_INDEX_SET_ANALYZER_ROW_FIELD_NAME,
+ options: {
+ rowIndex: params.rowIndex,
+ fieldName: params.fieldName
+ }
+ });
+};
+
+const setAnalyzer = (params) => (dispatch) => {
+ dispatch({
+ type: ActionTypes.SEARCH_INDEX_SET_ANALYZER_ROW,
+ options: {
+ rowIndex: params.rowIndex,
+ analyzer: params.analyzer
+ }
+ });
+};
+
+const setDefaultMultipleAnalyzer = (analyzer) => (dispatch) => {
+ dispatch({
+ type: ActionTypes.SEARCH_INDEX_SET_DEFAULT_MULTIPLE_ANALYZER,
+ options: {
+ analyzer: analyzer
+ }
+ });
+};
+
+const setSingleAnalyzer = (analyzer) => (dispatch) => {
+ dispatch({
+ type: ActionTypes.SEARCH_INDEX_SET_SINGLE_ANALYZER,
+ options: {
+ analyzer: analyzer
+ }
+ });
+};
+
+const saveSearchIndex = (doc, info, navigateToUrl) => {
+ doc.setIndex(info.indexName, info.indexFunction, info.analyzerInfo);
+
+ if (info.lastSavedDesignDocName === doc.id && info.lastSavedSearchIndexName !== info.indexName) {
+ var indexes = doc.get('indexes') || {};
+ delete indexes[info.lastSavedSearchIndexName];
+ doc.set({ indexes: indexes });
+ }
+
+ doc.save().then(() => {
+ info.designDocs.add(doc, { merge: true });
+
+ FauxtonAPI.addNotification({
+ msg: 'The search index has been saved.',
+ type: 'success',
+ clear: true
+ });
+
+ // if the user just saved the view to a different design doc, remove the view from the old design doc and
+ // maybe even delete if it's empty
+ if (!info.isCreatingIndex && info.lastSavedDesignDocName !== doc.id) {
+ const oldDesignDoc = IndexEditorActions.helpers.findDesignDoc(info.designDocs, info.lastSavedDesignDocName);
+ IndexEditorActions.safeDeleteIndex(oldDesignDoc, info.designDocs, 'indexes', info.lastSavedSearchIndexName, {
+ onSuccess: () => {
+ SidebarActions.dispatchUpdateDesignDocs(info.designDocs);
+ }
+ });
+ }
+
+ SidebarActions.dispatchUpdateDesignDocs(info.designDocs);
+ FauxtonAPI.navigate(navigateToUrl, { trigger: true });
+ }, (xhr) => {
+ const responseText = JSON.parse(xhr.responseText).reason;
+ FauxtonAPI.addNotification({
+ msg: 'Save failed: ' + responseText,
+ type: 'error',
+ clear: true
+ });
+ });
+};
+
+
+const deleteSearchIndex = (options) => {
+ const onSuccess = () => {
+
+ // if the user was on the index that was just deleted, redirect them back to all docs
+ if (options.isOnIndex) {
+ const url = FauxtonAPI.urls('allDocs', 'app', options.database.safeID());
+ FauxtonAPI.navigate(url);
+ }
+ SidebarActions.dispatchUpdateDesignDocs(options.designDocs);
+
+ FauxtonAPI.addNotification({
+ msg: 'The <code>' + _.escape(options.indexName) + '</code> search index has been deleted.',
+ type: 'info',
+ escape: false,
+ clear: true
+ });
+ SidebarActions.dispatchHideDeleteIndexModal();
+ };
+
+ IndexEditorActions.safeDeleteIndex(options.designDoc, options.designDocs, 'indexes', options.indexName, { onSuccess });
+};
+
+const cloneSearchIndex = (params) => {
+ const targetDesignDoc = getDesignDoc(params.designDocs, params.targetDesignDocName, params.newDesignDocName, params.database);
+ let indexes = targetDesignDoc.get('indexes');
+ if (indexes && _.has(indexes, params.newIndexName)) {
+ FauxtonAPI.addNotification({
+ msg: 'That index name is already used in this design doc. Please enter a new name.',
+ type: 'error',
+ clear: true
+ });
+ return;
+ }
+ if (!indexes) {
+ indexes = {};
+ }
+ const sourceDesignDoc = IndexEditorActions.helpers.findDesignDoc(params.designDocs, '_design/' + params.sourceDesignDocName);
+ const sourceDesignDocJSON = sourceDesignDoc.toJSON();
+
+ // this sets whatever content is in the source index into the target design doc under the new index name
+ indexes[params.newIndexName] = sourceDesignDocJSON.indexes[params.sourceIndexName];
+ targetDesignDoc.set({ indexes: indexes });
+
+ targetDesignDoc.save().then(function () {
+ params.onComplete();
+ FauxtonAPI.addNotification({
+ msg: 'The search index has been cloned.',
+ type: 'success',
+ clear: true
+ });
+
+ SidebarActions.dispatchUpdateDesignDocs(params.designDocs);
+ },
+ function (xhr) {
+ params.onComplete();
+ var responseText = JSON.parse(xhr.responseText).reason;
+ FauxtonAPI.addNotification({
+ msg: 'Clone failed: ' + responseText,
+ type: 'error',
+ clear: true
+ });
+ });
+};
+
+const gotoEditSearchIndexPage = (databaseName, partitionKey, designDocName, indexName) => {
+ const encodedPartKey = partitionKey ? encodeURIComponent(partitionKey) : '';
+ FauxtonAPI.navigate('#' + FauxtonAPI.urls('search', 'edit', encodeURIComponent(databaseName),
+ encodedPartKey, encodeURIComponent(designDocName), encodeURIComponent(indexName)));
+};
+
+const selectDesignDoc = (designDoc) => (dispatch) => {
+ dispatch({
+ type: ActionTypes.SEARCH_INDEX_SELECT_DESIGN_DOC,
+ options: {
+ value: designDoc
+ }
+ });
+};
+
+// const querySearch = (searchQuery, partitionKey) => {
+const querySearch = (databaseName, partitionKey, ddocName, indexName, searchQuery) => {
+ const baseUrl = partitionKey ?
+ FauxtonAPI.urls('partitioned_search', 'app', encodeURIComponent(databaseName), encodeURIComponent(partitionKey), encodeURIComponent(ddocName), encodeURIComponent(indexName)) :
+ FauxtonAPI.urls('search', 'app', encodeURIComponent(databaseName), encodeURIComponent(ddocName), encodeURIComponent(indexName));
+ FauxtonAPI.navigate(`${baseUrl}${encodeURIComponent(indexName)}?${encodeURIComponent(searchQuery)}`, {trigger: true});
+};
+
+const executSearchQuery = (database, partitionKey, ddoc, index, searchQuery, dispatch) => {
+ dispatch({ type: ActionTypes.SEARCH_INDEX_PREVIEW_REQUEST_MADE });
+
+ return API.fetchSearchResults(database, partitionKey, ddoc, index, searchQuery)
+ .then(rows => {
+ dispatch({
+ type: ActionTypes.SEARCH_INDEX_PREVIEW_MODEL_UPDATED,
+ options: {
+ searchResults: rows
+ }
+ });
+ }).catch(err => {
+ dispatch({ type: ActionTypes.SEARCH_INDEX_PREVIEW_REQUEST_ERROR });
+
+ if (err && err.message.includes('`partition` not supported')) {
+ dispatch(partitionParamNotSupported());
+ } else if (err && err.message.includes('`partition` parameter is mandatory')) {
+ dispatch(partitionParamIsMandatory());
+ } else {
+ FauxtonAPI.addNotification({
+ msg: 'Search failed: ' + err.message,
+ type: 'error',
+ clear: true
+ });
+ }
+
+ if (err.message.includes('not found')) {
+ FauxtonAPI.navigate(FauxtonAPI.urls('allDocsSanitized', 'app', database), {trigger: true});
+ }
+ });
+};
+
+const setSearchQuery = (query) => (dispatch) => {
+ dispatch({
+ type: ActionTypes.SEARCH_INDEX_SET_SEARCH_QUERY,
+ options: {
+ query: query
+ }
+ });
+};
+
+const updateNewDesignDocName = (designDocName) => (dispatch) => {
+ dispatch({
+ type: ActionTypes.SEARCH_INDEX_NEW_DESIGN_DOC_NAME_UPDATED,
+ options: {
+ value: designDocName
+ }
+ });
+};
+
+const updateNewDesignDocPartitioned = (isPartitioned) => (dispatch) => {
+ dispatch({
+ type: ActionTypes.SEARCH_INDEX_NEW_DESIGN_DOC_PARTITONED_UPDATED,
+ options: {
+ value: isPartitioned
+ }
+ });
+};
+
+const partitionParamNotSupported = () => {
+ return {
+ type: ActionTypes.SEARCH_INDEX_PARTITION_PARAM_NOT_SUPPORTED
+ };
+};
+
+const partitionParamIsMandatory = () => {
+ return {
+ type: ActionTypes.SEARCH_INDEX_PARTITION_PARAM_MANDATORY
+ };
+};
+
+
+// helpers
+
+function getDesignDoc (designDocs, targetDesignDocName, newDesignDocName, database) {
+ if (targetDesignDocName === 'new-doc') {
+ var doc = {
+ "_id": "_design/" + newDesignDocName,
+ "indexes": {},
+ "language": "javascript"
+ };
+ return new CloudantSearch.Doc(doc, { database: database });
+ }
+
+ var foundDoc = designDocs.find(function (ddoc) {
+ return ddoc.id === targetDesignDocName;
+ });
+ return (!foundDoc) ? null : foundDoc.dDocModel();
+}
+
+
+export default {
+ dispatchInitNewSearchIndex,
+ dispatchInitSearchIndex,
+ dispatchEditSearchIndex,
+ selectTab,
+ setSearchIndexName,
+ setAnalyzerType,
+ addAnalyzerRow,
+ removeAnalyzerRow,
+ setAnalyzerRowFieldName,
+ setAnalyzer,
+ setDefaultMultipleAnalyzer,
+ selectDesignDoc,
+ saveSearchIndex,
+ cloneSearchIndex,
+ deleteSearchIndex,
+ gotoEditSearchIndexPage,
+ querySearch,
+ setSearchQuery,
+ setSingleAnalyzer,
+ updateNewDesignDocName,
+ updateNewDesignDocPartitioned,
+ partitionParamNotSupported,
+ partitionParamIsMandatory
+};
diff --git a/app/addons/search/actiontypes.js b/app/addons/search/actiontypes.js
new file mode 100644
index 0000000..c5bbb93
--- /dev/null
+++ b/app/addons/search/actiontypes.js
@@ -0,0 +1,36 @@
+/*
+* Licensed Materials - Property of IBM
+*
+* "Restricted Materials of IBM"
+*
+* (C) Copyright IBM Corp. 2018 All Rights Reserved
+*
+* US Government Users Restricted Rights - Use, duplication or disclosure
+* restricted by GSA ADP Schedule Contract with IBM Corp.
+*/
+export default {
+ SEARCH_INDEX_DESIGN_DOCS_LOADED: 'SEARCH_INDEX_DESIGN_DOCS_LOADED',
+ SEARCH_INDEX_INIT: 'SEARCH_INDEX_INIT',
+ SEARCH_INDEX_SELECT_TAB: 'SEARCH_INDEX_SELECT_TAB',
+ SEARCH_INDEX_SET_NAME: 'SEARCH_INDEX_SET_NAME',
+ SEARCH_INDEX_SET_ANALYZER_TYPE: 'SEARCH_INDEX_SET_ANALYZER_TYPE',
+ SEARCH_INDEX_ADD_ANALYZER_ROW: 'SEARCH_INDEX_ADD_ANALYZER_ROW',
+ SEARCH_INDEX_REMOVE_ANALYZER_ROW: 'SEARCH_INDEX_REMOVE_ANALYZER_ROW',
+ SEARCH_INDEX_SET_ANALYZER_ROW_FIELD_NAME: 'SEARCH_INDEX_SET_ANALYZER_ROW_FIELD_NAME',
+ SEARCH_INDEX_SET_ANALYZER_ROW: 'SEARCH_INDEX_SET_ANALYZER_ROW',
+ SEARCH_INDEX_SET_DEFAULT_MULTIPLE_ANALYZER: 'SEARCH_INDEX_SET_DEFAULT_MULTIPLE_ANALYZER',
+ SEARCH_INDEX_INIT_EDIT_SEARCH_INDEX: 'SEARCH_INDEX_INIT_EDIT_SEARCH_INDEX',
+ SEARCH_INDEX_PREVIEW_MODEL_UPDATED: 'SEARCH_INDEX_PREVIEW_MODEL_UPDATED',
+ SEARCH_INDEX_PREVIEW_REQUEST_MADE: 'SEARCH_INDEX_PREVIEW_REQUEST_MADE',
+ SEARCH_INDEX_PREVIEW_REQUEST_ERROR: 'PREVIEW_MODEL_REQUEST_ERROR',
+ SEARCH_INDEX_SELECT_DESIGN_DOC: 'SEARCH_INDEX_SELECT_DESIGN_DOC',
+ SEARCH_INDEX_SET_SINGLE_ANALYZER: 'SEARCH_INDEX_SET_SINGLE_ANALYZER',
+ SEARCH_INDEX_SET_SEARCH_QUERY: 'SEARCH_INDEX_SET_SEARCH_QUERY',
+ SEARCH_INDEX_SET_LOADING: 'SEARCH_INDEX_SET_LOADING',
+ SEARCH_INDEX_CLEAR: 'SEARCH_INDEX_CLEAR',
+ SEARCH_INDEX_UPDATE_INDEX_FUNCTION: 'SEARCH_INDEX_UPDATE_INDEX_FUNCTION',
+ SEARCH_INDEX_NEW_DESIGN_DOC_NAME_UPDATED: 'SEARCH_INDEX_NEW_DESIGN_DOC_NAME_UPDATED',
+ SEARCH_INDEX_NEW_DESIGN_DOC_PARTITONED_UPDATED: 'SEARCH_INDEX_NEW_DESIGN_DOC_PARTITONED_UPDATED',
+ SEARCH_INDEX_PARTITION_PARAM_NOT_SUPPORTED: 'SEARCH_INDEX_PARTITION_PARAM_NOT_SUPPORTED',
+ SEARCH_INDEX_PARTITION_PARAM_MANDATORY: 'SEARCH_INDEX_PARTITION_PARAM_MANDATORY'
+};
diff --git a/app/addons/search/api.js b/app/addons/search/api.js
new file mode 100644
index 0000000..cbfdbcd
--- /dev/null
+++ b/app/addons/search/api.js
@@ -0,0 +1,32 @@
+/*
+* Licensed Materials - Property of IBM
+*
+* "Restricted Materials of IBM"
+*
+* (C) Copyright IBM Corp. 2018 All Rights Reserved
+*
+* US Government Users Restricted Rights - Use, duplication or disclosure
+* restricted by GSA ADP Schedule Contract with IBM Corp.
+*/
+
+import FauxtonAPI from '../../core/api';
+import { get } from '../../core/ajax';
+
+function searchUrl(database, partitionKey, ddoc, index, searchQuery) {
+ //https://[username].cloudant.com/animaldb/_design/views101/_search/animals?q=kookaburra
+ const encodedPartKey = partitionKey ? encodeURIComponent(partitionKey) : '';
+ return FauxtonAPI.urls('search', 'server', encodeURIComponent(database), encodedPartKey,
+ encodeURIComponent(ddoc), encodeURIComponent(index),
+ '?limit=10&q=' + encodeURIComponent(searchQuery));
+}
+
+export const fetchSearchResults = (database, partitionKey, ddoc, index, searchQuery) => {
+ const url = searchUrl(database, partitionKey, ddoc, index, searchQuery);
+ return get(url).then((res) => {
+ if (res.error) {
+ throw new Error(res.reason);
+ }
+
+ return res.rows;
+ });
+};
diff --git a/app/addons/search/assets/less/search.less b/app/addons/search/assets/less/search.less
new file mode 100644
index 0000000..68c7589
--- /dev/null
+++ b/app/addons/search/assets/less/search.less
@@ -0,0 +1,159 @@
+/*
+* Licensed Materials - Property of IBM
+*
+* "Restricted Materials of IBM"
+*
+* (C) Copyright IBM Corp. 2018 All Rights Reserved
+*
+* US Government Users Restricted Rights - Use, duplication or disclosure
+* restricted by GSA ADP Schedule Contract with IBM Corp.
+*/
+@import "../../../style/assets/less/variables.less";
+@import "../../../style/assets/less/mixins.less";
+
+.search-index-page-loading {
+ margin-top: 20px;
+}
+
+.edit-search-index-page-loading {
+ margin-top: 25px;
+}
+
+.index-tabs.dashboard-upper-menu {
+ position: inherit;
+ &.index-tabs {
+ border-bottom: 0;
+ .fonticon:before {
+ margin-right: 6px;
+ }
+ }
+}
+
+#queryText{
+ max-width: 300px;
+}
+
+.ace-editor-section .ace_editor {
+ font-size: 13px;
+ width: 100%;
+ line-height: 22px;
+}
+
+.search-query-save {
+ max-width: 100%;
+ .design-doc-group #new-ddoc-section {
+ margin-top: 24px;
+ label {
+ padding-top: 12px;
+ }
+ }
+}
+
+#analyzer-fields {
+ margin-top: 10px;
+
+ li {
+ margin-bottom: 5px;
+
+ &:first-child {
+ button {
+ margin-top: 27px;
+ }
+ }
+ div:first-child {
+ margin-left: 0;
+ }
+ }
+
+ .span4 {
+ margin-right: 3px;
+ width: 30%;
+
+ .styled-select, select {
+ width: 100%;
+ }
+ }
+}
+
+.delete-analyzer {
+ margin-top: 3px;
+ margin-left: 15px;
+}
+
+.search-index-content>div {
+ margin: 20px 10px 0 20px;
+ .icon-question-sign {
+ margin-left: 4px;
+ &:hover {
+ color: @hoverHighlight;
+ }
+ }
+}
+
+.search-index-function {
+ label {
+ margin-right: 0;
+ }
+ .ace_editor {
+ line-height: 22px;
+ }
+}
+
+#search-query-submit {
+ padding: 12px;
+}
+
+#search-index-preview-form {
+ margin: 5px 0 20px;
+ .input-append {
+ vertical-align: bottom;
+ }
+ .help-link {
+ display: inline-block;
+ margin-left: 12px;
+ padding-bottom: 23px;
+ }
+}
+
+#search-index-help {
+ h4 {
+ margin-top: 18px;
+ }
+
+ ul {
+ margin: 0 0 10px;
+ padding: 0;
+ list-style-type: none;
+ }
+ .search-index-examples {
+ li {
+ line-height: 28px;
+ span {
+ .search-index-code-section;
+ }
+ }
+ }
+}
+
+.search-index-tab-content {
+ a:hover {
+ text-decoration: none;
+ }
+}
+
+.search-index-code-section {
+ font-size: 12px;
+ font-family: Monaco, Menlo, Consolas, "Courier New", monospace;
+ background-color: #ddd;
+ padding: 5px;
+ border-radius: 3px;
+}
+
+#search-index-query-button {
+ height: 46px;
+ .border-right-radius(@radius);
+}
+
+body #dashboard-content #search-index-form {
+ position: relative;
+}
diff --git a/app/addons/search/base.js b/app/addons/search/base.js
new file mode 100644
index 0000000..483b8b9
--- /dev/null
+++ b/app/addons/search/base.js
@@ -0,0 +1,125 @@
+/*
+* Licensed Materials - Property of IBM
+*
+* "Restricted Materials of IBM"
+*
+* (C) Copyright IBM Corp. 2018 All Rights Reserved
+*
+* US Government Users Restricted Rights - Use, duplication or disclosure
+* restricted by GSA ADP Schedule Contract with IBM Corp.
+*/
+import app from '../../app';
+import Helpers from '../../helpers';
+import FauxtonAPI from '../../core/api';
+import Actions from './actions';
+import SearchRoutes from './routes';
+import CloudantDocuments from '../cloudantdocuments/routes';
+import reducers from './reducers';
+import './assets/less/search.less';
+
+SearchRoutes.initialize = function () {
+ FauxtonAPI.registerExtension('sidebar:list', {
+ selector: 'indexes',
+ name: 'Search Indexes',
+ urlNamespace: 'search',
+ indexLabel: 'search index', // used for labels
+ onDelete: Actions.deleteSearchIndex,
+ onClone: Actions.cloneSearchIndex,
+ onEdit: Actions.gotoEditSearchIndexPage
+ });
+ FauxtonAPI.registerExtension('sidebar:links', {
+ title: "New Search Index",
+ url: "new_search",
+ icon: 'fonticon-plus-circled',
+ showForPartitionedDDocs: true
+ });
+ FauxtonAPI.registerExtension('sidebar:newLinks', {
+ url: 'new_search',
+ name: 'Search Index'
+ });
+
+ // this tells Fauxton of the new Search Index type. It's used to determine when a design doc is really empty
+ FauxtonAPI.registerExtension('IndexTypes:propNames', 'indexes');
+};
+
+function partitionUrlComponent(partitionKey) {
+ return partitionKey ? '/_partition/' + partitionKey : '';
+}
+
+var proxyUrl = CloudantDocuments.proxyUrl;
+_.extend(CloudantDocuments.sharedUrlPaths, {
+ search: {
+ server: function (db, partitionKey, designDoc, searchName, query) {
+ query = CloudantDocuments.addRemoteAccount(query);
+ return proxyUrl + db + '/_design/' + designDoc + '/_search/' + searchName + query;
+ },
+
+ //NOTE: partitionKey is included here for compatibility with base registered url functions
+ //but is ignored because it's not supported for shared databases
+ app: function (id, designDoc) {
+ return CloudantDocuments.sharedUrlPaths.userHash() + id + '/_design/' + app.utils.safeURLName(designDoc) + '/_search/';
+ },
+
+ edit: function (database, partitionKey, designDoc, indexName) {
+ return CloudantDocuments.sharedUrlPaths.userHash() + database + '/_design/' + designDoc + '/_search/' + indexName + '/edit';
+ },
+
+ apiurl: function (db, partitionKey, designDoc, searchName) {
+ return CloudantDocuments.sharedUrlPaths.host() + encodeURIComponent(db) + '/_design/' +
+ encodeURIComponent(designDoc) + '/_search/' + encodeURIComponent(searchName);
+ },
+
+ fragment: function (id, partitionKey, designDoc, search) {
+ return CloudantDocuments.sharedUrlPaths.userHash() + id + '/_design/' + designDoc + '/_search/' + search;
+ },
+
+ showIndex: function (id, partitionKey, designDoc, search) {
+ return CloudantDocuments.sharedUrlPaths.host() + id + '/' + designDoc +
+ '/_search/' + search;
+ },
+ },
+ partitioned_search: {
+ app: function (id, partitionKey, designDoc) {
+ return CloudantDocuments.sharedUrlPaths.userHash() + id + '/_design/' + app.utils.safeURLName(designDoc) + '/_search/';
+ }
+ }
+});
+
+FauxtonAPI.registerUrls('partitioned_search', {
+ app: function (id, partitionKey, designDoc) {
+ return 'database/' + id + partitionUrlComponent(partitionKey) + '/_design/' + designDoc + '/_search/';
+ }
+});
+
+FauxtonAPI.registerUrls('search', {
+ server: function (id, partitionKey, designDoc, searchName, query) {
+ return Helpers.getServerUrl('/' + id + partitionUrlComponent(partitionKey) + '/_design/' + designDoc + '/_search/' + searchName + query);
+ },
+
+ app: function (id, designDoc) {
+ return 'database/' + id + '/_design/' + designDoc + '/_search/';
+ },
+
+ edit: function (database, partitionKey, designDoc, indexName) {
+ return 'database/' + database + partitionUrlComponent(partitionKey) + '/_design/' + designDoc + '/_search/' + indexName + '/edit';
+ },
+
+ fragment: function (id, partitionKey, designDoc, search) {
+ return 'database/' + id + partitionUrlComponent(partitionKey) + '/_design/' + designDoc + '/_search/' + search;
+ },
+
+ showIndex: function (id, partitionKey, designDoc, search) {
+ return 'database/' + id + partitionUrlComponent(partitionKey) + '/' + designDoc + '/_search/' + search;
+ },
+
+ apiurl: function (id, partitionKey, designDoc, searchName, query) {
+ return window.location.origin + '/' + encodeURIComponent(id) + '/_design/' + encodeURIComponent(designDoc) +
+ '/_search/' + encodeURIComponent(searchName) + '?q=' + encodeURIComponent(query);
+ }
+});
+
+FauxtonAPI.addReducers({
+ search: reducers
+});
+
+export default SearchRoutes;
diff --git a/app/addons/search/components/Analyzer.js b/app/addons/search/components/Analyzer.js
new file mode 100644
index 0000000..78c8a3c
--- /dev/null
+++ b/app/addons/search/components/Analyzer.js
@@ -0,0 +1,142 @@
+/*
+* Licensed Materials - Property of IBM
+*
+* "Restricted Materials of IBM"
+*
+* (C) Copyright IBM Corp. 2018 All Rights Reserved
+*
+* US Government Users Restricted Rights - Use, duplication or disclosure
+* restricted by GSA ADP Schedule Contract with IBM Corp.
+*/
+
+import PropTypes from 'prop-types';
+import React from 'react';
+import Constants from '../constants';
+import AnalyzerMultiple from './AnalyzerMultiple';
+import AnalyzerDropdown from './AnalyzerDropdown';
+
+// handles the entire Analyzer section: Simple and Multiple analyzers
+export default class Analyzer extends React.Component {
+ static propTypes = {
+ analyzerType: PropTypes.string.isRequired,
+ analyzerFields: PropTypes.array.isRequired,
+ defaultMultipleAnalyzer: PropTypes.string.isRequired,
+ singleAnalyzer: PropTypes.string.isRequired,
+ setAnalyzerType: PropTypes.func.isRequired,
+ setSingleAnalyzer: PropTypes.func.isRequired,
+ setDefaultMultipleAnalyzer: PropTypes.func.isRequired,
+ addAnalyzerRow: PropTypes.func.isRequired
+ };
+
+ constructor(props) {
+ super(props);
+ this.selectAnalyzerType = this.selectAnalyzerType.bind(this);
+ this.selectSingleAnalyzer = this.selectSingleAnalyzer.bind(this);
+ this.selectDefaultMultipleAnalyzer = this.selectDefaultMultipleAnalyzer.bind(this);
+ }
+
+ selectAnalyzerType = (e) => {
+ this.props.setAnalyzerType(e.target.value);
+ };
+
+ validate = () => {
+ if (this.props.analyzerType === Constants.ANALYZER_SINGLE) {
+ return true;
+ }
+ return this.analyzerMultiple.validate();
+ };
+
+ getAnalyzerFieldsAsObject = () => {
+ const obj = {};
+ this.props.analyzerFields.forEach(row => {
+ const fieldName = row.fieldName.replace(/["']/g, '');
+ obj[fieldName] = row.analyzer;
+ });
+ return obj;
+ };
+
+ getInfo = () => {
+ let analyzerInfo;
+ if (this.props.analyzerType === Constants.ANALYZER_SINGLE) {
+ analyzerInfo = this.props.singleAnalyzer;
+ } else {
+ analyzerInfo = {
+ name: 'perfield',
+ default: this.props.defaultMultipleAnalyzer,
+ fields: this.getAnalyzerFieldsAsObject()
+ };
+ }
+ return analyzerInfo;
+ };
+
+ selectSingleAnalyzer = (e) => {
+ this.props.setSingleAnalyzer(e.target.value);
+ };
+
+ selectDefaultMultipleAnalyzer = (e) => {
+ this.props.setDefaultMultipleAnalyzer(e.target.value);
+ };
+
+ getAnalyzerType = () => {
+ if (this.props.analyzerType === Constants.ANALYZER_SINGLE) {
+ return (
+ <AnalyzerDropdown
+ label="Type"
+ defaultSelected={this.props.singleAnalyzer}
+ onChange={this.selectSingleAnalyzer}
+ />
+ );
+ }
+ return (
+ <AnalyzerMultiple
+ ref={node => this.analyzerMultiple = node}
+ defaultAnalyzer={this.props.defaultMultipleAnalyzer}
+ selectDefaultMultipleAnalyzer={this.selectDefaultMultipleAnalyzer}
+ fields={this.props.analyzerFields}
+ addAnalyzerRow={this.props.addAnalyzerRow}
+ removeAnalyzerRow={this.props.removeAnalyzerRow}
+ setAnalyzerRowFieldName={this.props.setAnalyzerRowFieldName}
+ setAnalyzer={this.props.setAnalyzer}
+ />
+ );
+
+ };
+
+ render() {
+ let multipleClasses = 'btn';
+ if (this.props.analyzerType === Constants.ANALYZER_MULTIPLE) {
+ multipleClasses += ' active';
+ }
+ let singleClasses = 'btn';
+ if (this.props.analyzerType === Constants.ANALYZER_SINGLE) {
+ singleClasses += ' active';
+ }
+
+ return (
+ <div className="well">
+ <div className="control-group">
+ <label htmlFor="search-analyzer">Analyzer</label>
+ <div className="btn-group toggle-btns" id="analyzer">
+ <label style={{width: '82px'}} htmlFor="single-analyzer" className={singleClasses}>Single</label>
+ <input
+ type="radio"
+ id="single-analyzer"
+ name="search-analyzer"
+ value="single"
+ checked={this.props.analyzerType === Constants.ANALYZER_SINGLE}
+ onChange={this.selectAnalyzerType} />
+ <input
+ type="radio"
+ id="multiple-analyzer"
+ name="search-analyzer"
+ value="multiple"
+ checked={this.props.analyzerType === Constants.ANALYZER_MULTIPLE}
+ onChange={this.selectAnalyzerType} />
+ <label style={{width: '82px'}} htmlFor="multiple-analyzer" className={multipleClasses}>Multiple</label>
+ </div>
+ </div>
+ {this.getAnalyzerType()}
+ </div>
+ );
+ }
+}
diff --git a/app/addons/search/components/AnalyzerDropdown.js b/app/addons/search/components/AnalyzerDropdown.js
new file mode 100644
index 0000000..88ac8e5
--- /dev/null
+++ b/app/addons/search/components/AnalyzerDropdown.js
@@ -0,0 +1,77 @@
+/*
+* Licensed Materials - Property of IBM
+*
+* "Restricted Materials of IBM"
+*
+* (C) Copyright IBM Corp. 2018 All Rights Reserved
+*
+* US Government Users Restricted Rights - Use, duplication or disclosure
+* restricted by GSA ADP Schedule Contract with IBM Corp.
+*/
+
+import PropTypes from 'prop-types';
+import React from 'react';
+import GeneralComponents from '../../components/react-components';
+
+const StyledSelect = GeneralComponents.StyledSelect;
+
+export default class AnalyzerDropdown extends React.Component {
+ static defaultProps = {
+ defaultSelected: 'standard',
+ label: 'Type',
+ id: 'analyzer-default',
+ classes: '',
+ onChange: function () { }
+ };
+ static propTypes = {
+ defaultSelected: PropTypes.string.isRequired,
+ label: PropTypes.string.isRequired,
+ id: PropTypes.string.isRequired,
+ classes: PropTypes.string.isRequired,
+ onChange: PropTypes.func.isRequired
+ };
+
+ getAnalyzers = () => {
+ const analyzers = [
+ 'Standard', 'Keyword', 'Simple', 'Whitespace', 'Classic', 'Email'
+ ];
+ return analyzers.map((i) => {
+ return (<option value={i.toLowerCase()} key={i}>{i}</option>);
+ });
+ };
+
+ getLanguages = () => {
+ const languages = [
+ 'Arabic', 'Armenian', 'Basque', 'Bulgarian', 'Brazilian', 'Catalan', 'Cjk', 'Chinese', 'Czech',
+ 'Danish', 'Dutch', 'English', 'Finnish', 'French', 'Galician', 'German', 'Greek', 'Hindi', 'Hungarian',
+ 'Indonesian', 'Irish', 'Italian', 'Japanese', 'Latvian', 'Norwegian', 'Persian', 'Polish', 'Portuguese',
+ 'Romanian', 'Russian', 'Spanish', 'Swedish', 'Thai', 'Turkish'
+ ];
+ return languages.map((lang) => {
+ return (<option value={lang.toLowerCase()} key={lang}>{lang}</option>);
+ });
+ };
+
+ getLabel = () => {
+ return this.props.label === '' ? null : <label htmlFor={this.props.id}>{this.props.label}</label>;
+ };
+
+ render() {
+ const languages =
+ <optgroup label="Language-specific" key="languages">
+ {this.getLanguages()}
+ </optgroup>;
+
+ return (
+ <div className={this.props.classes}>
+ {this.getLabel()}
+ <StyledSelect
+ selectChange={this.props.onChange}
+ selectValue={this.props.defaultSelected}
+ selectId={this.props.id}
+ selectContent={[this.getAnalyzers(), languages]}
+ />
+ </div>
+ );
+ }
+}
diff --git a/app/addons/search/components/AnalyzerMultiple.js b/app/addons/search/components/AnalyzerMultiple.js
new file mode 100644
index 0000000..6aa4785
--- /dev/null
+++ b/app/addons/search/components/AnalyzerMultiple.js
@@ -0,0 +1,94 @@
+/*
+* Licensed Materials - Property of IBM
+*
+* "Restricted Materials of IBM"
+*
+* (C) Copyright IBM Corp. 2018 All Rights Reserved
+*
+* US Government Users Restricted Rights - Use, duplication or disclosure
+* restricted by GSA ADP Schedule Contract with IBM Corp.
+*/
+
+import FauxtonAPI from '../../../core/api';
+import PropTypes from 'prop-types';
+import React from 'react';
+import AnalyzerRow from './AnalyzerRow';
+import AnalyzerDropdown from './AnalyzerDropdown';
+
+export default class AnalyzerMultiple extends React.Component {
+ static propTypes = {
+ addAnalyzerRow: PropTypes.func.isRequired,
+ defaultAnalyzer: PropTypes.string.isRequired,
+ selectDefaultMultipleAnalyzer: PropTypes.func.isRequired,
+ fields: PropTypes.array.isRequired
+ };
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ showErrors: false
+ };
+ }
+
+ addRow = (e) => {
+ e.preventDefault();
+ this.props.addAnalyzerRow(this.props.defaultAnalyzer);
+ };
+
+ getRows = () => {
+ return this.props.fields.map((row, i) => {
+ return (
+ <AnalyzerRow
+ row={row}
+ key={row.key}
+ rowIndex={i}
+ showErrors={this.state.showErrors}
+ setAnalyzer={this.props.setAnalyzer}
+ setAnalyzerRowFieldName={this.props.setAnalyzerRowFieldName}
+ removeAnalyzerRow={this.props.removeAnalyzerRow}
+ />
+ );
+ });
+ };
+
+ validate = () => {
+ this.setState({ showErrors: true });
+
+ let hasDuplicate = false;
+ const fieldNames = [];
+ const allValid = this.props.fields.every((row) => {
+ if (fieldNames.includes(row.fieldName)) {
+ hasDuplicate = true;
+ }
+ fieldNames.push(row.fieldName);
+ return row.valid;
+ });
+
+ if (!allValid || hasDuplicate) {
+ FauxtonAPI.addNotification({
+ msg: 'Fieldnames cannot be empty and must be unique.',
+ type: 'error',
+ clear: true
+ });
+ }
+ return allValid;
+ };
+
+ render() {
+ return (
+ <div>
+ <AnalyzerDropdown
+ label="Default"
+ id="defaultAnalyzer"
+ defaultSelected={this.props.defaultAnalyzer}
+ onChange={this.props.selectDefaultMultipleAnalyzer}
+ isValidating={this.validate} />
+ <ul id="analyzer-fields" className="unstyled">{this.getRows()}</ul>
+ <button className="addfield btn btn-small btn-primary" onClick={this.addRow}>
+ <i className="cloudant-circle-plus"></i>
+ Add Field
+ </button>
+ </div>
+ );
+ }
+}
diff --git a/app/addons/search/components/AnalyzerRow.js b/app/addons/search/components/AnalyzerRow.js
new file mode 100644
index 0000000..c8ca8d1
--- /dev/null
+++ b/app/addons/search/components/AnalyzerRow.js
@@ -0,0 +1,84 @@
+/*
+* Licensed Materials - Property of IBM
+*
+* "Restricted Materials of IBM"
+*
+* (C) Copyright IBM Corp. 2018 All Rights Reserved
+*
+* US Government Users Restricted Rights - Use, duplication or disclosure
+* restricted by GSA ADP Schedule Contract with IBM Corp.
+*/
+
+import PropTypes from 'prop-types';
+import React from 'react';
+import AnalyzerDropdown from './AnalyzerDropdown';
+
+export default class AnalyzerRow extends React.Component {
+ static propTypes = {
+ rowIndex: PropTypes.number.isRequired,
+ row: PropTypes.object.isRequired,
+ showErrors: PropTypes.bool.isRequired,
+ removeAnalyzerRow: PropTypes.func.isRequired,
+ setAnalyzerRowFieldName: PropTypes.func.isRequired,
+ setAnalyzer: PropTypes.func.isRequired
+ };
+
+ constructor(props) {
+ super(props);
+ }
+
+ deleteRow = (e) => {
+ e.preventDefault();
+ this.props.removeAnalyzerRow(this.props.rowIndex);
+ };
+
+ getFieldNameHeading = (analyzerId) => {
+ return (this.props.rowIndex === 0) ? <label htmlFor={analyzerId}>Fieldname</label> : false;
+ };
+
+ changeFieldName = (e) => {
+ this.props.setAnalyzerRowFieldName({
+ rowIndex: this.props.rowIndex,
+ fieldName: e.target.value
+ });
+ };
+
+ selectAnalyzer = (e) => {
+ this.props.setAnalyzer({
+ rowIndex: this.props.rowIndex,
+ analyzer: e.target.value
+ });
+ };
+
+ render() {
+ const analyzerId = "analyzer-row-" + this.props.rowIndex;
+ const analyzerHeading = (this.props.rowIndex === 0) ? 'Analyzer' : '';
+
+ let fieldNameClasses = 'span12';
+ if (this.props.showErrors && !this.props.row.valid) {
+ fieldNameClasses += ' unhappy';
+ }
+
+ return (
+ <li>
+ <div className="row-fluid">
+ <div className="span4">
+ {this.getFieldNameHeading(analyzerId)}
+ <input type="text" value={this.props.row.fieldName} className={fieldNameClasses} onChange={this.changeFieldName} />
+ </div>
+
+ <AnalyzerDropdown
+ id={analyzerId}
+ label={analyzerHeading}
+ defaultSelected={this.props.row.analyzer}
+ classes="span4"
+ onChange={this.selectAnalyzer} />
+
+ <div className="span4">
+ <button className="btn btn-danger delete-analyzer" onClick={this.deleteRow}>delete</button>
+ </div>
+ </div>
+ </li>
+ );
+ }
+}
diff --git a/app/addons/search/components/SearchForm.js b/app/addons/search/components/SearchForm.js
new file mode 100644
index 0000000..9a85be1
--- /dev/null
+++ b/app/addons/search/components/SearchForm.js
@@ -0,0 +1,126 @@
+/*
+* Licensed Materials - Property of IBM
+*
+* "Restricted Materials of IBM"
+*
+* (C) Copyright IBM Corp. 2018 All Rights Reserved
+*
+* US Government Users Restricted Rights - Use, duplication or disclosure
+* restricted by GSA ADP Schedule Contract with IBM Corp.
+*/
+
+import PropTypes from 'prop-types';
+import React from 'react';
+import FauxtonAPI from '../../../core/api';
+import GeneralComponents from '../../components/react-components';
+
+export default class SearchForm extends React.Component {
+ static propTypes = {
+ hasActiveQuery: PropTypes.bool.isRequired,
+ searchQuery: PropTypes.string.isRequired,
+ searchPerformed: PropTypes.bool.isRequired,
+ querySearch: PropTypes.func.isRequired,
+ setSearchQuery: PropTypes.func.isRequired,
+ searchResults: PropTypes.array
+ };
+
+ componentDidMount() {
+ this.searchInput.focus();
+ }
+
+ querySearch = (e) => {
+ e.preventDefault();
+ if (this.props.searchQuery.trim() === '') {
+ FauxtonAPI.addNotification({
+ msg: 'Please enter a search term.',
+ type: 'error',
+ clear: true
+ });
+ this.searchInput.focus();
+ return;
+ }
+ const {databaseName, partitionKey, ddocName, indexName, searchQuery} = this.props;
+ this.props.querySearch(databaseName, partitionKey, ddocName, indexName, searchQuery);
+ };
+
+ getRows = () => {
+ const database = encodeURIComponent(this.props.databaseName);
+ return this.props.searchResults.map((item) => {
+ const doc = {
+ header: item.id,
+ content: JSON.stringify(item, null, ' '),
+ url: FauxtonAPI.urls('document', 'app', database, encodeURIComponent(item.id))
+ };
+ return <GeneralComponents.Document
+ key={item.id}
+ keylabel={'id:'}
+ doc={doc}
+ header={item.id}
+ isDeletable={false}
+ docContent={doc.content}
+ onClick={this.onClick}
+ docChecked={() => { }}
+ docIdentifier={item.id} />;
+ });
+ };
+
+ onClick = (id, doc) => {
+ if (doc.url) {
+ FauxtonAPI.navigate(doc.url);
+ }
+ };
+
+ onType = (e) => {
+ this.props.setSearchQuery(e.target.value);
+ };
+
+ getResults = () => {
+ if (this.props.hasActiveQuery) {
+ return (<GeneralComponents.LoadLines />);
+ }
+
+ if (this.props.noResultsWarning) {
+ return (<div data-select="search-result-set">{this.props.noResultsWarning}</div>);
+ }
+
+ if (!this.props.searchResults) {
+ return false;
+ }
+
+ if (this.props.searchResults.length === 0) {
+ return (<div data-select="search-result-set">No results found.</div>);
+ }
+
+ return (
+ <div id="doc-list" data-select="search-result-set">
+ {this.getRows()}
+ </div>
+ );
+ };
+
+ render() {
+ const buttonLabel = this.props.hasActiveQuery ? 'Querying...' : 'Query';
+ return (
+ <div>
+ <form id="search-index-preview-form">
+ <span className="input-append">
+ <input
+ className="span4"
+ ref={el => this.searchInput = el}
+ type="text"
+ placeholder="Enter your search query"
+ onChange={this.onType}
+ value={this.props.searchQuery} />
+ <button className="btn btn-primary" id="search-index-query-button" type="submit" disabled={this.props.hasActiveQuery}
+ onClick={this.querySearch}>{buttonLabel}</button>
+ </span>
+ <a className="help-link" data-bypass="true" href={FauxtonAPI.constants.DOC_URLS.SEARCH_INDEX_QUERIES} target="_blank" rel="noopener noreferrer">
+ <i className="icon-question-sign" />
+ </a>
+ </form>
+
+ {this.getResults()}
+ </div>
+ );
+ }
+}
diff --git a/app/addons/search/components/SearchFormContainer.js b/app/addons/search/components/SearchFormContainer.js
new file mode 100644
index 0000000..d5bbba8
--- /dev/null
+++ b/app/addons/search/components/SearchFormContainer.js
@@ -0,0 +1,36 @@
+import { connect } from 'react-redux';
+import SearchForm from './SearchForm';
+import Actions from '../actions';
+
+const mapStateToProps = ({ search }) => {
+ return {
+ databaseName: search.databaseName,
+ partitionKey: search.partitionKey,
+ ddocName: search.ddocName,
+ indexName: search.indexName,
+ hasActiveQuery: search.hasActiveQuery,
+ searchQuery: search.searchQuery,
+ searchPerformed: search.searchPerformed,
+ searchResults: search.searchResults,
+ noResultsWarning: search.noResultsWarning
+ };
+};
+
+const mapDispatchToProps = (dispatch) => {
+ return {
+ querySearch: (databaseName, partitionKey, ddocName, indexName, searchQuery) => {
+ Actions.querySearch(databaseName, partitionKey, ddocName, indexName, searchQuery);
+ },
+
+ setSearchQuery: (query) => {
+ dispatch(Actions.setSearchQuery(query));
+ }
+ };
+};
+
+const SearchFormContainer = connect(
+ mapStateToProps,
+ mapDispatchToProps
+)(SearchForm);
+
+export default SearchFormContainer;
diff --git a/app/addons/search/components/SearchIndexEditor.js b/app/addons/search/components/SearchIndexEditor.js
new file mode 100644
index 0000000..3135ad3
--- /dev/null
+++ b/app/addons/search/components/SearchIndexEditor.js
@@ -0,0 +1,164 @@
+/*
+* Licensed Materials - Property of IBM
+*
+* "Restricted Materials of IBM"
+*
+* (C) Copyright IBM Corp. 2018 All Rights Reserved
+*
+* US Government Users Restricted Rights - Use, duplication or disclosure
+* restricted by GSA ADP Schedule Contract with IBM Corp.
+*/
+
+import FauxtonAPI from '../../../core/api';
+import app from '../../../app';
+import PropTypes from 'prop-types';
+import React from 'react';
+import GeneralComponents from '../../components/react-components';
+import IndexEditorComponents from '../../documents/index-editor/components';
+import Analyzer from './Analyzer';
+
+const DesignDocSelector = IndexEditorComponents.DesignDocSelector;
+
+export default class SearchIndexEditor extends React.Component {
+ static defaultProps = {
+ isCreatingIndex: true,
+ blur: function () { },
+ isLoading: true
+ };
+
+ static propTypes = {
+ isLoading: PropTypes.bool,
+ isCreatingIndex: PropTypes.bool,
+ database: PropTypes.object.isRequired,
+ saveDoc: PropTypes.object.isRequired,
+ newDesignDocName: PropTypes.string,
+ blur: PropTypes.func,
+ setSearchIndexName: PropTypes.func.isRequired,
+ searchIndexFunction: PropTypes.string.isRequired,
+ saveSearchIndex: PropTypes.func.isRequired,
+ selectDesignDoc: PropTypes.func.isRequired,
+ updateNewDesignDocName: PropTypes.func.isRequired
+ };
+
+ updateSearchIndexName = (e) => {
+ this.props.setSearchIndexName(e.target.value);
+ };
+
+ saveIndex = (e) => {
+ e.preventDefault();
+
+ // pass off validation work to the individual form sections
+ if (!this.designDocSelector.validate() || !this.analyzer.validate()) {
+ return;
+ }
+
+ if (!this.props.searchIndexName.trim()) {
+ FauxtonAPI.addNotification({
+ msg: 'Please enter the index name.',
+ type: 'error',
+ clear: true
+ });
+ return;
+ }
+
+ const dDocNameClean = this.props.saveDoc.id.replace(/_design\//, '');
+ const encodedPartKey = this.props.partitionKey ? encodeURIComponent(this.props.partitionKey) : '';
+ const url = FauxtonAPI.urls('search', 'fragment', encodeURIComponent(this.props.database.id), encodedPartKey,
+ encodeURIComponent(dDocNameClean), encodeURIComponent(this.props.searchIndexName));
+
+ this.props.saveSearchIndex(this.props.saveDoc, {
+ isCreatingIndex: this.props.isCreatingIndex,
+ indexName: this.props.searchIndexName,
+ designDocs: this.props.designDocs,
+ database: this.props.database,
+ indexFunction: this.getIndexFunction(),
+ analyzerInfo: this.analyzer.getInfo(),
+ lastSavedSearchIndexName: this.props.lastSavedSearchIndexName,
+ lastSavedDesignDocName: this.props.lastSavedDesignDocName
+ }, url);
+ };
+
+ getIndexFunction = () => {
+ return this.searchIndexEditor.getValue();
+ };
+
+ getDesignDocList = () => {
+ return this.props.designDocs.map(function (doc) {
+ return doc.id;
+ });
+ };
+
+ getCancelLink() {
+ const encodedDatabase = encodeURIComponent(this.props.database.id);
+ const encodedPartitionKey = this.props.partitionKey ? encodeURIComponent(this.props.partitionKey) : '';
+ if (!this.props.lastSavedDesignDocName || this.props.isCreatingIndex) {
+ return '#' + FauxtonAPI.urls('allDocs', 'app', encodedDatabase, encodedPartitionKey);
+ }
+
+ const cleanDDocName = this.props.lastSavedDesignDocName.replace(/^_design\//, '');
+ const encodedDDoc = '_design/' + encodeURIComponent(cleanDDocName);
+ const encodedIndex = encodeURIComponent(this.props.lastSavedSearchIndexName);
+ return '#' + FauxtonAPI.urls('search', 'showIndex', encodedDatabase,
+ encodedPartitionKey, encodedDDoc, encodedIndex);
+ }
+
+ render() {
+ if (this.props.isLoading) {
+ return (
+ <div className="search-index-page-loading">
+ <GeneralComponents.LoadLines />
+ </div>
+ );
+ }
+ // If failed to load
+ if (!this.props.database) {
+ return null;
+ }
+
+ const pageHeader = this.props.isCreatingIndex ? 'New Search Index' : 'Edit Search Index';
+ const btnLabel = this.props.isCreatingIndex ? 'Create Document and Build Index' : 'Save Document and Build Index';
+ return (
+ <form className="form-horizontal search-query-save" id="search-index">
+ <h3 className="simple-header">{pageHeader}</h3>
+
+ <DesignDocSelector
+ ref={node => this.designDocSelector = node}
+ designDocLabel="Save to design document"
+ designDocList={this.getDesignDocList()}
+ isDbPartitioned={this.props.isDbPartitioned}
+ newDesignDocName={this.props.newDesignDocName}
+ newDesignDocPartitioned={this.props.newDesignDocPartitioned}
+ selectedDesignDocName={this.props.ddocName}
+ selectedDesignDocPartitioned={this.props.ddocPartitioned}
+ onSelectDesignDoc={this.props.selectDesignDoc}
+ onChangeNewDesignDocName={this.props.updateNewDesignDocName}
+ onChangeNewDesignDocPartitioned={this.props.updateNewDesignDocPartitioned}
+ docLink={app.helpers.getDocUrl('DOC_URL_DESIGN_DOCS')} />
+
+ <div className="control-group">
+ <label htmlFor="search-name">Index name</label>
+ <input type="text" id="search-name" value={this.props.searchIndexName} onChange={this.updateSearchIndexName} />
+ </div>
+
+ <GeneralComponents.CodeEditorPanel
+ id={'search-function'}
+ className="ace-editor-section"
+ ref={node => this.searchIndexEditor = node}
+ title={"Search index function"}
+ allowZenMode={false}
+ docLink={app.helpers.getDocUrl('SEARCH_INDEXES')}
+ defaultCode={this.props.searchIndexFunction}
+ blur={this.props.blur} />
+
+ <Analyzer ref={node => this.analyzer = node} {...this.props}/>
+
+ <div className="control-group">
+ <button id="save-index" className="btn btn-primary save" onClick={this.saveIndex}>
+ <i className="icon fonticon-ok-circled" />{btnLabel}
+ </button>
+ <a href={this.getCancelLink()} className="index-cancel-link">Cancel</a>
+ </div>
+ </form>
+ );
+ }
+}
diff --git a/app/addons/search/components/SearchIndexEditorContainer.js b/app/addons/search/components/SearchIndexEditorContainer.js
new file mode 100644
index 0000000..ebbde2d
--- /dev/null
+++ b/app/addons/search/components/SearchIndexEditorContainer.js
@@ -0,0 +1,82 @@
+import { connect } from 'react-redux';
+import SearchIndexEditor from './SearchIndexEditor';
+import Actions from '../actions';
+import { getSaveDesignDoc, getSelectedDesignDocPartitioned } from '../reducers';
+
+const mapStateToProps = ({ search, databases }, ownProps) => {
+ const isSelectedDDocPartitioned = getSelectedDesignDocPartitioned(search, databases.isDbPartitioned);
+ return {
+ isCreatingIndex: ownProps.isCreatingIndex,
+ isLoading: search.loading,
+ database: search.database,
+ designDocs: search.designDocs,
+ searchIndexFunction: search.searchIndexFunction,
+ ddocName: search.ddocName,
+ ddocPartitioned: isSelectedDDocPartitioned,
+ lastSavedDesignDocName: search.lastSavedDesignDocName,
+ lastSavedSearchIndexName: search.lastSavedSearchIndexName,
+ searchIndexName: search.searchIndexName,
+ analyzerType: search.analyzerType,
+ analyzerFields: search.analyzerFields,
+ analyzerFieldsObj: search.analyzerFieldsObj,
+ defaultMultipleAnalyzer: search.defaultMultipleAnalyzer,
+ singleAnalyzer: search.singleAnalyzer,
+ saveDoc: getSaveDesignDoc(search, databases.isDbPartitioned),
+ newDesignDocName: search.newDesignDocName,
+ newDesignDocPartitioned: search.newDesignDocPartitioned,
+ isDbPartitioned: databases.isDbPartitioned,
+ partitionKey: ownProps.partitionKey
+ };
+};
+
+const mapDispatchToProps = (dispatch) => {
+ return {
+ setSearchIndexName: (name) => {
+ dispatch(Actions.setSearchIndexName(name));
+ },
+
+ saveSearchIndex: (doc, info, navigateToUrl) => {
+ Actions.saveSearchIndex(doc, info, navigateToUrl);
+ },
+
+ selectDesignDoc: (designDoc) => {
+ dispatch(Actions.selectDesignDoc(designDoc));
+ },
+
+ updateNewDesignDocName: (designDocName) => {
+ dispatch(Actions.updateNewDesignDocName(designDocName));
+ },
+ updateNewDesignDocPartitioned: (isPartitioned) => {
+ dispatch(Actions.updateNewDesignDocPartitioned(isPartitioned));
+ },
+ setAnalyzerType: (type) => {
+ dispatch(Actions.setAnalyzerType(type));
+ },
+ setSingleAnalyzer: (analyzer) => {
+ dispatch(Actions.setSingleAnalyzer(analyzer));
+ },
+ setDefaultMultipleAnalyzer: (analyzer) => {
+ dispatch(Actions.setDefaultMultipleAnalyzer(analyzer));
+ },
+ addAnalyzerRow: (analyzer) => {
+ dispatch(Actions.addAnalyzerRow(analyzer));
+ },
+ removeAnalyzerRow: (rowIndex) => {
+ dispatch(Actions.removeAnalyzerRow(rowIndex));
+ },
+ setAnalyzerRowFieldName: (params) => {
+ dispatch(Actions.setAnalyzerRowFieldName(params));
+ },
+ setAnalyzer: (params) => {
+ dispatch(Actions.setAnalyzer(params));
+ }
+
+ };
+};
+
+const SearchIndexEditorContainer = connect(
+ mapStateToProps,
+ mapDispatchToProps
+)(SearchIndexEditor);
+
+export default SearchIndexEditorContainer;
diff --git a/app/addons/search/constants.js b/app/addons/search/constants.js
new file mode 100644
index 0000000..408a988
--- /dev/null
+++ b/app/addons/search/constants.js
@@ -0,0 +1,18 @@
+/*
+* Licensed Materials - Property of IBM
+*
+* "Restricted Materials of IBM"
+*
+* (C) Copyright IBM Corp. 2018 All Rights Reserved
+*
+* US Government Users Restricted Rights - Use, duplication or disclosure
+* restricted by GSA ADP Schedule Contract with IBM Corp.
+*/
+export default {
+ ANALYZER_SINGLE: 'single',
+ ANALYZER_MULTIPLE: 'multiple',
+ DEFAULT_SEARCH_INDEX_NAME: 'newSearch',
+ DEFAULT_ANALYZER_TYPE: 'single',
+ DEFAULT_ANALYZER: 'standard',
+ DEFAULT_SEARCH_INDEX_FUNCTION: 'function (doc) {\n index("name", doc.name);\n}'
+};
diff --git a/app/addons/search/layout.js b/app/addons/search/layout.js
new file mode 100644
index 0000000..fb41a92
--- /dev/null
+++ b/app/addons/search/layout.js
@@ -0,0 +1,101 @@
+/*
+* Licensed Materials - Property of IBM
+*
+* "Restricted Materials of IBM"
+*
+* (C) Copyright IBM Corp. 2018 All Rights Reserved
+*
+* US Government Users Restricted Rights - Use, duplication or disclosure
+* restricted by GSA ADP Schedule Contract with IBM Corp.
+*/
+import PropTypes from 'prop-types';
+import React from 'react';
+import {TabsSidebarHeader} from '../documents/layouts';
+import SidebarControllerContainer from '../documents/sidebar/SidebarControllerContainer';
+import SearchFormContainer from './components/SearchFormContainer';
+import SearchIndexEditorContainer from './components/SearchIndexEditorContainer';
+
+
+const getContent = (section, database, indexName,
+ ddocName, designDocs, ddoc, partitionKey) => {
+ if (section === 'create') {
+ return <SearchIndexEditorContainer
+ designDocs={designDocs}
+ ddoc={ddoc}
+ database={database}
+ isCreatingIndex={true}
+ partitionKey={partitionKey}
+ />;
+
+ } else if (section === 'edit') {
+ return <SearchIndexEditorContainer
+ database={database}
+ indexName={indexName}
+ ddocName={ddocName}
+ isCreatingIndex={false}
+ partitionKey={partitionKey}
+ />;
+ }
+
+ return <SearchFormContainer />;
+};
+
+export const SearchLayout = ({
+ section,
+ database,
+ indexName,
+ ddocName,
+ docURL, endpoint,
+ dropDownLinks,
+ designDocs,
+ ddoc,
+ selectedNavItem,
+ partitionKey,
+ onPartitionKeySelected,
+ onGlobalModeSelected,
+ globalMode
+}) => {
+ return (
+ <div id="dashboard" className="with-sidebar">
+ <TabsSidebarHeader
+ hideQueryOptions={true}
+ docURL={docURL}
+ endpoint={endpoint}
+ dbName={database.id}
+ dropDownLinks={dropDownLinks}
+ database={database}
+ showPartitionKeySelector={section === 'search'}
+ partitionKey={partitionKey}
+ onPartitionKeySelected={onPartitionKeySelected}
+ onGlobalModeSelected={onGlobalModeSelected}
+ globalMode={globalMode}
+ />
+ <div className="with-sidebar tabs-with-sidebar content-area">
+ <aside id="sidebar-content" className="scrollable">
+ <SidebarControllerContainer selectedNavItem={selectedNavItem} selectedPartitionKey={partitionKey}/>
+ </aside>
+ <section id="dashboard-content" className="flex-layout flex-col">
+ <div className="flex-body" id='dashboard-lower-content'>
+ <div className="search-index-content">
+ <div className="tab-content search-index-tab-content">
+ {getContent(section, database, indexName, ddocName, designDocs, ddoc, partitionKey)}
+ </div>
+ </div>
+ </div>
+ </section>
+ </div>
+ </div>
+ );
+};
+
+SearchLayout.propTypes = {
+ section: PropTypes.string.isRequired,
+ docURL: PropTypes.string,
+ endpoint: PropTypes.string,
+ ddocName: PropTypes.string,
+ indexName: PropTypes.string,
+ dropDownLinks: PropTypes.array.isRequired,
+ database: PropTypes.object.isRequired,
+};
+
+export default SearchLayout;
diff --git a/app/addons/search/reducers.js b/app/addons/search/reducers.js
new file mode 100644
index 0000000..12f27b5
--- /dev/null
+++ b/app/addons/search/reducers.js
@@ -0,0 +1,314 @@
+/*
+* Licensed Materials - Property of IBM
+*
+* "Restricted Materials of IBM"
+*
+* (C) Copyright IBM Corp. 2018 All Rights Reserved
+*
+* US Government Users Restricted Rights - Use, duplication or disclosure
+* restricted by GSA ADP Schedule Contract with IBM Corp.
+*/
+
+import StyleHelpers from '../style/helpers';
+import ActionTypes from './actiontypes';
+import Constants from './constants';
+import SearchResources from './resources';
+import DocumentsHelpers from '../documents/helpers';
+
+const initialState = {
+ designDocs: new Backbone.Collection(),
+ database: {},
+ loading: true,
+ ddocName: 'new-doc',
+ partitionKey: '',
+ indexName: '',
+ databaseName: '',
+ ...softReset()
+};
+
+// called on first load (e.g. editing a search index) and every time the create search index page loads
+function softReset() {
+ return {
+ noResultsWarning: '',
+ hasActiveQuery: false,
+ searchQuery: '',
+ searchResults: undefined,
+ searchPerformed: false,
+ newDesignDocName: '',
+ newDesignDocPartitioned: true,
+ lastSavedSearchIndexName: '',
+ searchIndexFunction: Constants.DEFAULT_SEARCH_INDEX_FUNCTION,
+ searchIndexName: Constants.DEFAULT_SEARCH_INDEX_NAME,
+ analyzerType: Constants.DEFAULT_ANALYZER_TYPE,
+ analyzerFields: [],
+ singleAnalyzer: Constants.DEFAULT_ANALYZER,
+ defaultMultipleAnalyzer: Constants.DEFAULT_ANALYZER
+ };
+}
+
+function addAnalyzerRow (state, { analyzer, fieldName }) {
+ const newAnalyzerFields = state.analyzerFields.slice();
+ newAnalyzerFields.push({
+ key: StyleHelpers.getUniqueKey(),
+ fieldName: (fieldName) ? fieldName : '',
+ analyzer: analyzer,
+ valid: (fieldName && fieldName.trim().length > 0)
+ });
+ return newAnalyzerFields;
+}
+
+function removeAnalyzerRowByIndex (state, rowIndex) {
+ const newAnalyzerFields = state.analyzerFields.slice();
+ rowIndex = parseInt(rowIndex, 10);
+ newAnalyzerFields.splice(rowIndex, 1);
+ return newAnalyzerFields;
+}
+
+function setAnalyzerRowFieldName (state, { fieldName, rowIndex }) {
+ const newAnalyzerFields = state.analyzerFields.slice();
+ const idx = parseInt(rowIndex, 10);
+ newAnalyzerFields[idx].fieldName = fieldName;
+ newAnalyzerFields[idx].valid = fieldName !== '';
+ return newAnalyzerFields;
+}
+
+function setAnalyzerRow (state, { analyzer, rowIndex }) {
+ const newAnalyzerFields = state.analyzerFields.slice();
+ const idx = parseInt(rowIndex, 10);
+ newAnalyzerFields[idx].analyzer = analyzer;
+ return newAnalyzerFields;
+}
+
+function initEditSearch (state, { database, designDocs, ddocInfo, indexName }) {
+ const ddoc = designDocs.find(ddoc => {
+ return ddoc.id === ddocInfo.id;
+ }).dDocModel();
+
+ // the selected analyzer returned in the ddoc can be applied to both the single analyzer and the default multiple
+ // analyzer. We store them separately in the store so those values don't change when toggling from Single to Multiple
+ const analyzer = ddoc.getAnalyzer(indexName);
+ let newSingleAnalyzer;
+ if (_.isString(analyzer)) {
+ newSingleAnalyzer = analyzer;
+ } else {
+ if (_.has(analyzer, 'default')) {
+ newSingleAnalyzer = analyzer.default;
+ } else {
+ newSingleAnalyzer = Constants.DEFAULT_ANALYZER_TYPE;
+ }
+ }
+ const newAnalyzerFields = [];
+ if (analyzer && analyzer.fields) {
+ Object.keys(analyzer.fields).forEach(fieldName => {
+ newAnalyzerFields.push({
+ key: StyleHelpers.getUniqueKey(),
+ fieldName: (fieldName) ? fieldName : '',
+ analyzer: analyzer.fields[fieldName],
+ valid: !_.isUndefined(fieldName) && !_.isEmpty(fieldName)
+ });
+ });
+ }
+
+ return {
+ loading: false,
+ searchPerformed: false,
+ database: database,
+ designDocs: designDocs,
+ searchIndexName: indexName,
+ ddocName: ddocInfo.id,
+ lastSavedSearchIndexName: indexName,
+ lastSavedDesignDocName: ddocInfo.id,
+ searchIndexFunction: ddoc.getIndex(indexName),
+ analyzerType: ddoc.analyzerType(indexName),
+ // this either returns a simple string (single) or a complex object (multiple)
+ singleAnalyzer: newSingleAnalyzer,
+ defaultMultipleAnalyzer: newSingleAnalyzer,
+ analyzerFields: newAnalyzerFields
+ };
+}
+
+export function getSaveDesignDoc (state, isDbPartitioned) {
+ if (state.ddocName === 'new-doc') {
+ const doc = {
+ _id: '_design/' + state.newDesignDocName,
+ views: {},
+ language: 'javascript'
+ };
+ const dDoc = new SearchResources.Doc(doc, { database: state.database });
+ if (isDbPartitioned) {
+ dDoc.setDDocPartitionedOption(state.newDesignDocPartitioned);
+ }
+ return dDoc;
+ }
+
+ const foundDoc = state.designDocs.find((ddoc) => {
+ return ddoc.id === state.ddocName;
+ });
+ return (!foundDoc) ? null : foundDoc.dDocModel();
+}
+
+export function getSelectedDesignDocPartitioned(state, isDbPartitioned) {
+ const designDoc = state.designDocs.find(ddoc => {
+ return state.ddocName === ddoc.id;
+ });
+ if (designDoc) {
+ return DocumentsHelpers.isDDocPartitioned(designDoc.get('doc'), isDbPartitioned);
+ }
+ return false;
+}
+
+export default function search(state = initialState, action) {
+ const options = action.options;
+ switch (action.type) {
+
+ case ActionTypes.SEARCH_INDEX_SET_LOADING:
+ return {
+ ...state,
+ loading: options.loading
+ };
+
+ case ActionTypes.SEARCH_INDEX_DESIGN_DOCS_LOADED:
+ const newState = {
+ ...state,
+ loading: false,
+ designDocs: options.designDocs,
+ database: options.database,
+ ...softReset()
+ };
+ if (options.defaultDDoc) {
+ newState.ddocName = '_design/' + options.defaultDDoc;
+ }
+ return newState;
+
+ case ActionTypes.SEARCH_INDEX_SET_NAME:
+ return {
+ ...state,
+ searchIndexName: options.value
+ };
+
+ case ActionTypes.SEARCH_INDEX_SET_ANALYZER_TYPE:
+ return {
+ ...state,
+ analyzerType: options.value
+ };
+
+ case ActionTypes.SEARCH_INDEX_SET_SINGLE_ANALYZER:
+ return {
+ ...state,
+ singleAnalyzer: options.analyzer
+ };
+
+ case ActionTypes.SEARCH_INDEX_ADD_ANALYZER_ROW:
+ return {
+ ...state,
+ analyzerFields: addAnalyzerRow(state, options)
+ };
+
+ case ActionTypes.SEARCH_INDEX_PREVIEW_REQUEST_MADE:
+ return {
+ ...state,
+ hasActiveQuery: true
+ };
+
+ case ActionTypes.SEARCH_INDEX_PREVIEW_REQUEST_ERROR:
+ return {
+ ...state,
+ hasActiveQuery: false,
+ searchResults: []
+ };
+
+ case ActionTypes.SEARCH_INDEX_SET_SEARCH_QUERY:
+ return {
+ ...state,
+ searchQuery: options.query
+ };
+
+ case ActionTypes.SEARCH_INDEX_PREVIEW_MODEL_UPDATED:
+ return {
+ ...state,
+ searchResults: options.searchResults,
+ hasActiveQuery: false,
+ searchPerformed: true
+ };
+
+ case ActionTypes.SEARCH_INDEX_REMOVE_ANALYZER_ROW:
+ return {
+ ...state,
+ analyzerFields: removeAnalyzerRowByIndex(state, options.rowIndex)
+ };
+
+ case ActionTypes.SEARCH_INDEX_SET_ANALYZER_ROW_FIELD_NAME:
+ return {
+ ...state,
+ analyzerFields: setAnalyzerRowFieldName(state, options)
+ };
+
+ case ActionTypes.SEARCH_INDEX_SET_ANALYZER_ROW:
+ return {
+ ...state,
+ analyzerFields: setAnalyzerRow(state, options)
+ };
+
+ case ActionTypes.SEARCH_INDEX_SET_DEFAULT_MULTIPLE_ANALYZER:
+ return {
+ ...state,
+ defaultMultipleAnalyzer: options.analyzer
+ };
+
+ case ActionTypes.SEARCH_INDEX_INIT_EDIT_SEARCH_INDEX:
+ return {
+ ...state,
+ ...initEditSearch(state, options)
+ };
+
+ case ActionTypes.SEARCH_INDEX_SELECT_DESIGN_DOC:
+ return {
+ ...state,
+ ddocName: options.value
+ };
+
+ case ActionTypes.SEARCH_INDEX_CLEAR:
+ return {
+ ...initialState,
+ designDocs: new Backbone.Collection()
+ };
+
+ case ActionTypes.SEARCH_INDEX_INIT:
+ return {
+ ...state,
+ loading: false,
+ databaseName: options.databaseName,
+ partitionKey: options.partitionKey,
+ ddocName: options.ddocName,
+ indexName: options.indexName,
+ searchQuery: options.searchQuery,
+ searchResults: options.searchQuery === '' ? undefined : state.searchResults,
+ noResultsWarning: ''
+ };
+
+ case ActionTypes.SEARCH_INDEX_NEW_DESIGN_DOC_NAME_UPDATED:
+ return {
+ ...state,
+ newDesignDocName: options.value
+ };
+
+ case ActionTypes.SEARCH_INDEX_NEW_DESIGN_DOC_PARTITONED_UPDATED:
+ return {
+ ...state,
+ newDesignDocPartitioned: options.value
+ };
+
+ case ActionTypes.SEARCH_INDEX_PARTITION_PARAM_NOT_SUPPORTED:
+ return Object.assign({}, state, {
+ noResultsWarning: 'The selected index does not support partitions. Switch back to global mode.'
+ });
+
+ case ActionTypes.SEARCH_INDEX_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.'
+ });
+
+ default:
+ return state;
+ }
+}
diff --git a/app/addons/search/resources.js b/app/addons/search/resources.js
new file mode 100644
index 0000000..c653fbb
--- /dev/null
+++ b/app/addons/search/resources.js
@@ -0,0 +1,72 @@
+/*
+* Licensed Materials - Property of IBM
+*
+* "Restricted Materials of IBM"
+*
+* (C) Copyright IBM Corp. 2018 All Rights Reserved
+*
+* US Government Users Restricted Rights - Use, duplication or disclosure
+* restricted by GSA ADP Schedule Contract with IBM Corp.
+*/
+
+import Constants from './constants';
+import Documents from '../documents/resources';
+
+var CloudantSearch = {};
+
+CloudantSearch.Doc = Documents.Doc.extend({
+ setIndex: function (name, index, analyzer) {
+ if (!this.isDdoc()) {
+ return false;
+ }
+
+ var indexes = this.get('indexes');
+ if (!indexes) {
+ indexes = {};
+ }
+ if (!indexes[name]) {
+ indexes[name] = {};
+ }
+
+ if (analyzer) {
+ indexes[name].analyzer = analyzer;
+ }
+
+ indexes[name].index = index;
+ return this.set({indexes: indexes});
+ },
+
+ getIndex: function (indexName) {
+ return this.get('indexes')[indexName].index;
+ },
+
+ getAnalyzer: function (indexName) {
+ return this.get('indexes')[indexName].analyzer;
+ },
+
+ analyzerType: function (indexName) {
+ if (typeof this.getAnalyzer(indexName) === 'object') {
+ return Constants.ANALYZER_MULTIPLE;
+ }
+ return Constants.ANALYZER_SINGLE;
+ },
+
+ dDocModel: function () {
+ if (!this.isDdoc()) {
+ return false;
+ }
+
+ var doc = this.get('doc');
+ if (doc) {
+ return new CloudantSearch.Doc(doc, { database: this.database });
+ }
+ return false;
+ }
+});
+
+CloudantSearch.AllDocs = Documents.AllDocs.extend({
+ model: CloudantSearch.Doc
+});
+
+
+export default CloudantSearch;
diff --git a/app/addons/search/routes.js b/app/addons/search/routes.js
new file mode 100644
index 0000000..f4ba4d3
--- /dev/null
+++ b/app/addons/search/routes.js
@@ -0,0 +1,272 @@
+/*
+* Licensed Materials - Property of IBM
+*
+* "Restricted Materials of IBM"
+*
+* (C) Copyright IBM Corp. 2018 All Rights Reserved
+*
+* US Government Users Restricted Rights - Use, duplication or disclosure
+* restricted by GSA ADP Schedule Contract with IBM Corp.
+*/
+import app from '../../app';
+import FauxtonAPI from '../../core/api';
+import Resources from './resources';
+import SearchActions from './actions';
+import DatabasesActions from '../databases/actions';
+import Databases from '../databases/base';
+import BaseRoute from '../documents/shared-routes';
+import {SidebarItemSelection} from '../documents/sidebar/helpers';
+import CloudantDocuments from '../cloudantdocuments/resources';
+import CloudantDatabases from '../cloudantdatabases/resources';
+import Account from '../account/resources';
+import SidebarActions from '../documents/sidebar/actions';
+import Actions from './actions';
+import Layout from './layout';
+import React from 'react';
+
+var SearchRouteObject = BaseRoute.extend({
+ routes: {
+ 'database/:database/_design/:ddoc/_search/:search(?*searchQuery)': {
+ route: 'searchNoPartition',
+ roles: ['fx_loggedIn']
+ },
+ 'database/:database/_partition/:partitionkey/_design/:ddoc/_search/:search(?*searchQuery)': {
+ route: 'search',
+ roles: ['fx_loggedIn']
+ },
+ 'database/:database/_design/:ddoc/_indexes/:search(?*searchQuery)': {
+ route: 'searchNoPartition',
+ roles: ['fx_loggedIn']
+ },
+ 'database/:database/_partition/:partitionkey/_design/:ddoc/_indexes/:search(?*searchQuery)': {
+ route: 'search',
+ roles: ['fx_loggedIn']
+ },
+ 'database/:database/_design/:ddoc/_search/:search/edit': {
+ route: 'editNoPartition',
+ roles: ['fx_loggedIn']
+ },
+ 'database/:database/_partition/:partitionkey/_design/:ddoc/_search/:search/edit': {
+ route: 'edit',
+ roles: ['fx_loggedIn']
+ },
+ 'database/:database/new_search': 'createNoPartition',
+ 'database/:database/_partition/:partitionkey/new_search': 'create',
+ 'database/:database/new_search/:designDoc': 'createNoPartition',
+ 'database/:database/_partition/:partitionkey/new_search/:designDoc': 'create'
+ },
+
+ initialize: function (route, options) {
+ this.allDatabases = new Databases.List();
+ this.databaseName = options[0];
+ this.database = new Databases.Model({ id: this.databaseName });
+ this.data = {
+ database: new Databases.Model({ id: this.databaseName })
+ };
+
+ this.data.designDocs = new Resources.AllDocs(null, {
+ database: this.data.database,
+ params: {
+ startkey: '"_design"',
+ endkey: '"_design1"',
+ include_docs: true
+ }
+ });
+
+ SidebarActions.dispatchNewOptions({
+ database: this.data.database,
+ designDocs: this.data.designDocs
+ });
+ },
+
+ searchNoPartition: function (databaseName, ddocName, indexName, query) {
+ return this.search(databaseName, '', ddocName, indexName, query);
+ },
+
+ search: function (databaseName, partitionKey, ddocName, indexName, query) {
+ this.databaseName = databaseName;
+ this.ddocName = ddocName;
+ this.indexName = indexName;
+
+ const selectedNavItem = new SidebarItemSelection('designDoc', {
+ designDocName: ddocName,
+ designDocSection: 'Search Indexes',
+ indexName: indexName
+ });
+ SidebarActions.dispatchExpandSelectedItem(selectedNavItem);
+
+ const dropDownLinks = this.getCrumbs(this.database);
+ Actions.dispatchInitSearchIndex({
+ databaseName: databaseName,
+ partitionKey: partitionKey,
+ designDoc: ddocName,
+ indexName: indexName,
+ query: query
+ });
+ DatabasesActions.fetchSelectedDatabaseInfo(databaseName);
+
+ const encodedPartKey = partitionKey ? encodeURIComponent(partitionKey) : '';
+ const endpointUrl = FauxtonAPI.urls('search', 'apiurl', this.databaseName, encodedPartKey, this.ddocName,
+ this.indexName, (query ? query : '*:*'));
+
+ const encodedQuery = query ? `?${encodeURIComponent(query)}` : '';
+ const navigateToPartitionedView = (partKey) => {
+ const baseUrl = FauxtonAPI.urls('partitioned_search', 'app', encodeURIComponent(databaseName),
+ encodeURIComponent(partKey), encodeURIComponent(ddocName));
+ FauxtonAPI.navigate('#/' + baseUrl + encodeURIComponent(indexName) + encodedQuery);
+ };
+ const navigateToGlobalView = () => {
+ const baseUrl = FauxtonAPI.urls('search', 'app', encodeURIComponent(databaseName), encodeURIComponent(ddocName));
+ FauxtonAPI.navigate('#/' + baseUrl + encodeURIComponent(indexName) + encodedQuery);
+ };
+ return <Layout
+ section={'search'}
+ dropDownLinks={dropDownLinks}
+ endpoint={endpointUrl}
+ docURL={FauxtonAPI.constants.DOC_URLS.SEARCH_INDEXES}
+ database={this.database}
+ indexName={indexName}
+ selectedNavItem={selectedNavItem}
+ partitionKey={partitionKey}
+ onPartitionKeySelected={navigateToPartitionedView}
+ onGlobalModeSelected={navigateToGlobalView}
+ globalMode={partitionKey === ''}
+ />;
+ },
+
+ editNoPartition: function (database, ddocName, indexName) {
+ return this.edit(database, '', ddocName, indexName);
+ },
+
+ edit: function (database, partitionKey, ddocName, indexName) {
+ const selectedNavItem = new SidebarItemSelection('designDoc', {
+ designDocName: ddocName,
+ designDocSection: 'Search Indexes',
+ indexName: indexName
+ });
+ SidebarActions.dispatchExpandSelectedItem(selectedNavItem);
+
+ SearchActions.dispatchEditSearchIndex({
+ ddocID: '_design/' + ddocName,
+ database: this.database,
+ indexName: indexName,
+ designDocs: this.data.designDocs
+ });
+ DatabasesActions.fetchSelectedDatabaseInfo(database);
+
+ const dropDownLinks = this.getCrumbs(this.database);
+ return <Layout
+ section={'edit'}
+ dropDownLinks={dropDownLinks}
+ database={this.database}
+ indexName={indexName}
+ ddocName={ddocName}
+ selectedNavItem={selectedNavItem}
+ partitionKey={partitionKey}
+ />;
+ },
+
+ createNoPartition: function (database, ddoc) {
+ return this.create(database, '', ddoc);
+ },
+
+ create: function (database, partitionKey, ddoc) {
+ const selectedNavItem = new SidebarItemSelection('');
+
+ SearchActions.dispatchInitNewSearchIndex({
+ database: this.database,
+ designDocs: this.data.designDocs,
+ defaultDDoc: ddoc,
+ });
+ DatabasesActions.fetchSelectedDatabaseInfo(database);
+
+ const dropDownLinks = this.getCrumbs(this.database);
+ return <Layout
+ section={'create'}
+ dropDownLinks={dropDownLinks}
+ database={this.database}
+ designDocs={this.data.designDocs}
+ ddoc={ddoc}
+ selectedNavItem={selectedNavItem}
+ partitionKey={partitionKey}
+ />;
+ },
+
+ getCrumbs: function (database) {
+ return [
+ { type: "back", link: "/_all_dbs"},
+ { name: database.id }
+ ];
+ }
+});
+
+
+var SharedSearchRouteObject = SearchRouteObject.extend({
+ routes: {
+ 'database/shared/:user/:database/_design/:ddoc/_search/:search(?*searchQuery)': {
+ route: 'sharedSearch',
+ roles: ['fx_loggedIn']
+ },
+ 'database/shared/:user/:database/_design/:ddoc/_indexes/:search(?*searchQuery)': {
+ route: 'sharedSearch',
+ roles: ['fx_loggedIn']
+ },
+ 'database/shared/:user/:database/new_search': 'sharedNewSearch',
+ 'database/shared/:user/:database/new_search/:designDoc': 'sharedNewSearch',
+ 'database/shared/:user/:database/_design/:ddoc/_search/:search/edit': {
+ route: 'sharedEdit',
+ roles: ['fx_loggedIn']
+ }
+ },
+
+ initialize: function (route, options) {
+ var docOptions = app.getParams();
+ docOptions.include_docs = true;
+
+ this.username = CloudantDocuments.sharedUsername = options[0];
+ this.databaseName = options[1];
+ this.database = new CloudantDatabases.SharedModel({
+ id: this.databaseName,
+ name: this.username
+ });
+ this.user = new Account.User();
+
+ this.data = {
+ username: this.username,
+ database: this.database
+ };
+
+ this.data.designDocs = new CloudantDocuments.SharedAllDocs(null, {
+ username: this.username,
+ database: this.database,
+ paging: {
+ pageSize: 500
+ },
+ params: {
+ startkey: '"_design"',
+ endkey: '"_design1"',
+ include_docs: true,
+ limit: 500
+ }
+ });
+
+ var initOptions = options.slice(1);
+ SearchRouteObject.prototype.initialize.call(this, route, initOptions);
+ },
+
+ sharedSearch: function (user, database, ddoc, search, query) {
+ return this.searchNoPartition(database, ddoc, search, query);
+ },
+
+ sharedEdit: function (user, database, ddoc, indexName) {
+ return this.editNoPartition(database, ddoc, indexName);
+ },
+
+ sharedNewSearch: function (user, database, ddoc) {
+ return this.createNoPartition(database, ddoc);
+ }
+});
+
+Resources.RouteObjects = [SearchRouteObject, SharedSearchRouteObject];
+
+export default Resources;
diff --git a/app/addons/search/tests/nightwatch/cloneSearchIndex.js b/app/addons/search/tests/nightwatch/cloneSearchIndex.js
new file mode 100644
index 0000000..69d6748
--- /dev/null
+++ b/app/addons/search/tests/nightwatch/cloneSearchIndex.js
@@ -0,0 +1,68 @@
+/*
+* Licensed Materials - Property of IBM
+*
+* "Restricted Materials of IBM"
+*
+* (C) Copyright IBM Corp. 2018 All Rights Reserved
+*
+* US Government Users Restricted Rights - Use, duplication or disclosure
+* restricted by GSA ADP Schedule Contract with IBM Corp.
+*/
+// Licensed 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.
+
+// Licensed 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.
+
+module.exports = {
+
+ 'Clones a search index': function (client) {
+ var waitTime = client.globals.maxWaitTime,
+ newDatabaseName = client.globals.testDatabaseName,
+ baseUrl = client.globals.test_settings.launch_url;
+
+ client
+ .createDatabase(newDatabaseName)
+ .loginToGUI()
+ .url(baseUrl + '/#/database/' + newDatabaseName + '/_all_docs')
+
+ // create a search index
+ .waitForElementPresent('#new-design-docs-button', waitTime, false)
+ .click('#new-design-docs-button a')
+ .click('#new-design-docs-button a[href="#/database/' + newDatabaseName + '/new_search"]')
+ .waitForElementVisible('#new-ddoc', waitTime, false)
+ .setValue('#new-ddoc', 'test1')
+ .clearValue('#search-name')
+ .setValue('#search-name', 'test1-index')
+ .clickWhenVisible('#save-index')
+ .waitForElementVisible('#global-notifications .alert.alert-success', waitTime, false)
+
+ .clickWhenVisible('.index-list li span', waitTime, true)
+ .clickWhenVisible('.popover-content .fonticon-files-o', waitTime, true)
+ .waitForElementVisible('#new-index-name', waitTime, true)
+ .setValue('#new-index-name', 'cloned-search-index')
+ .clickWhenVisible('.clone-index-modal .btn-primary', waitTime, true)
+
+ // now wait for the sidebar to be updated with the new view
+ .waitForElementVisible('#test1_cloned-search-index', waitTime, true)
+ .end();
+ }
+};
diff --git a/app/addons/search/tests/nightwatch/createNewSearch.js b/app/addons/search/tests/nightwatch/createNewSearch.js
new file mode 100644
index 0000000..6265b67
--- /dev/null
+++ b/app/addons/search/tests/nightwatch/createNewSearch.js
@@ -0,0 +1,130 @@
+/*
+* Licensed Materials - Property of IBM
+*
+* "Restricted Materials of IBM"
+*
+* (C) Copyright IBM Corp. 2018 All Rights Reserved
+*
+* US Government Users Restricted Rights - Use, duplication or disclosure
+* restricted by GSA ADP Schedule Contract with IBM Corp.
+*/
+module.exports = {
+
+ 'Creates new Search index for Dash': function (client) {
+ /*jshint multistr: true */
+
+ var newDatabaseName = client.globals.testDatabaseName,
+ baseUrl = client.globals.test_settings.launch_url;
+
+ var searchFunctionString = function (append) {
+ return 'function (doc) {' +
+ 'index("name", doc.name ' + append + ');' +
+ '}';
+ };
+
+ client
+ .loginToGUI()
+ .populateDatabase(newDatabaseName)
+ .url(baseUrl + '/#/database/' + newDatabaseName + '/_all_docs')
+ .waitForElementPresent('#new-design-docs-button', client.globals.maxWaitTime, false)
+ .click('#new-design-docs-button a')
+ .click('#new-design-docs-button a[href="#/database/' + newDatabaseName + '/new_search"]')
+ .waitForElementVisible('.faux-header__doc-header-title', client.globals.maxWaitTime, true)
+
+ .clickWhenVisible('.styled-select select')
+ .keys(['_design/keyview', '\uE006'])
+
+ .clearValue('#search-name')
+ .setValue('#search-name', 'fancy_search')
+ .execute('\
+ var editor = ace.edit("search-function");\
+ editor.getSession().setValue("' + searchFunctionString(0) + '");\
+ ')
+
+ .execute('document.querySelector("#save-index").scrollIntoView();')
+ .clickWhenVisible('#save-index')
+ .waitForElementPresent('#keyview_fancy_search', client.globals.maxWaitTime, false)
+ .end();
+ },
+
+ 'Creating a new index has a clean slate': function (client) {
+ var newDatabaseName = client.globals.testDatabaseName,
+ baseUrl = client.globals.test_settings.launch_url;
+
+ client
+ .loginToGUI()
+ .populateDatabase(newDatabaseName)
+ .url(baseUrl + '/#/database/' + newDatabaseName + '/_all_docs')
+
+ // 1. create a search index in _design/keyview design doc
+ .waitForElementPresent('#new-design-docs-button', client.globals.maxWaitTime, false)
+ .click('#new-design-docs-button a')
+ .click('#new-design-docs-button a[href="#/database/' + newDatabaseName + '/new_search"]')
+ .clickWhenVisible('.styled-select select')
+ .keys(['_design/keyview', '\uE006'])
+
+ .clearValue('#search-name')
+ .setValue('#search-name', 'clean-slate-test')
+ .clickWhenVisible('#save-index')
+ .waitForElementVisible('#global-notifications .alert.alert-success', client.globals.maxWaitTime, false)
+ .url(baseUrl + '/#/database/' + newDatabaseName + '/_all_docs')
+ .waitForElementPresent('.tableview-checkbox-cell', client.globals.maxWaitTime, false)
+ .waitForElementNotPresent('.loading-lines', client.globals.maxWaitTime, false)
+
+ // 2. create a new search index. The "Save to design doc" dropdown should default to New Document
+ .waitForElementPresent('#new-design-docs-button', client.globals.maxWaitTime, false)
+ .click('#new-design-docs-button a')
+ .click('#new-design-docs-button a[href="#/database/' + newDatabaseName + '/new_search"]')
+ .waitForElementVisible('.styled-select select', client.globals.maxWaitTime, false)
+ .assert.value('.styled-select select', 'new-doc')
+ .end();
+ },
+
+ 'Adding two indexes in a row does not add multiple indexes': function (client) {
+ var newDatabaseName = client.globals.testDatabaseName,
+ baseUrl = client.globals.test_settings.launch_url;
+
+ client
+ .loginToGUI()
+ .createDatabase(newDatabaseName)
+ .url(baseUrl + '/#/database/' + newDatabaseName + '/_all_docs')
+
+ // 1. create a search index in _design/test1
+ .waitForElementPresent('#new-design-docs-button', client.globals.maxWaitTime, false)
+ .click('#new-design-docs-button a')
+ .click('#new-design-docs-button a[href="#/database/' + newDatabaseName + '/new_search"]')
+ .waitForElementVisible('#new-ddoc', client.globals.maxWaitTime, false)
+ .setValue('#new-ddoc', 'test1')
+ .clearValue('#search-name')
+ .setValue('#search-name', 'test1-index')
+ .clickWhenVisible('#save-index')
+ .waitForElementVisible('#global-notifications .alert.alert-success', client.globals.maxWaitTime, false)
+ .url(baseUrl + '/#/database/' + newDatabaseName + '/_all_docs')
+ .waitForElementPresent('.tableview-checkbox-cell', client.globals.maxWaitTime, false)
+ .waitForElementNotPresent('.loading-lines', client.globals.maxWaitTime, false)
+
+ // 2. create a second index in _design/test2
+ .waitForElementPresent('#new-design-docs-button', client.globals.maxWaitTime, false)
+ .click('#new-design-docs-button a')
+ .click('#new-design-docs-button a[href="#/database/' + newDatabaseName + '/new_search"]')
+ .waitForElementVisible('#new-ddoc', client.globals.maxWaitTime, false)
+ .setValue('#new-ddoc', 'test2')
+ .clearValue('#search-name')
+ .setValue('#search-name', 'test2-index')
+ .clickWhenVisible('#save-index')
+
+ .waitForElementVisible('#global-notifications .alert.alert-success', client.globals.maxWaitTime, false)
+
+ // 3. confirm that the nav bar shows ONLY one search index each:
+ // _design/test1 has the single _design/test1-index
+ // _design/test2 has the single _design/test2-index
+ .waitForElementPresent('#test1_test1-index', client.globals.maxWaitTime, false)
+ .waitForElementNotPresent('#test1_test2-index', client.globals.maxWaitTime, false)
+ .waitForElementNotPresent('#test2_test1-index', client.globals.maxWaitTime, false)
+ .waitForElementPresent('#test2_test2-index', client.globals.maxWaitTime, false)
+
+ .end();
+ }
+
+
+};
diff --git a/app/addons/search/tests/nightwatch/deleteSearchIndex.js b/app/addons/search/tests/nightwatch/deleteSearchIndex.js
new file mode 100644
index 0000000..f4cdc86
--- /dev/null
+++ b/app/addons/search/tests/nightwatch/deleteSearchIndex.js
@@ -0,0 +1,107 @@
+/*
+* Licensed Materials - Property of IBM
+*
+* "Restricted Materials of IBM"
+*
+* (C) Copyright IBM Corp. 2018 All Rights Reserved
+*
+* US Government Users Restricted Rights - Use, duplication or disclosure
+* restricted by GSA ADP Schedule Contract with IBM Corp.
+*/
+// Licensed 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.
+
+// Licensed 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.
+
+module.exports = {
+
+ 'Deletes a search index': function (client) {
+ var newDatabaseName = client.globals.testDatabaseName,
+ waitTime = client.globals.maxWaitTime,
+ baseUrl = client.globals.test_settings.launch_url;
+
+ client
+ .loginToGUI()
+ .url(baseUrl + '/#/database/' + newDatabaseName + '/_all_docs')
+
+ // create a search index
+ .waitForElementPresent('#new-design-docs-button', waitTime, false)
+ .click('#new-design-docs-button a')
+ .click('#new-design-docs-button a[href="#/database/' + newDatabaseName + '/new_search"]')
+ .waitForElementVisible('#new-ddoc', waitTime, false)
+ .setValue('#new-ddoc', 'test1')
+ .clearValue('#search-name')
+ .setValue('#search-name', 'test1-index')
+ .clickWhenVisible('#save-index')
+ .waitForElementVisible('#global-notifications .alert.alert-success', waitTime, false)
+ .url(baseUrl + '/#/database/' + newDatabaseName + '/_all_docs')
+ .waitForElementPresent('.tableview-checkbox-cell', client.globals.maxWaitTime, false)
+ .waitForElementNotPresent('.loading-lines', waitTime, false)
+
+ // confirm search index appears in sidebar
+ .waitForElementVisible('#test1_test1-index', waitTime, true)
+
+ // now delete it and confirm that the entire design doc gets removed (because it's the last index)
+ .clickWhenVisible('.index-list li span', waitTime, true)
+ .clickWhenVisible('.popover-content .fonticon-trash', waitTime, true)
+ .clickWhenVisible('.confirmation-modal button.btn.btn-primary')
+
+ // now wait for the sidebar to have removed the design doc
+ .waitForElementNotPresent('#testdesigndoc', waitTime, true)
+ .end();
+ },
+
+ 'Deleting view when design doc has search index does not remove design doc': function (client) {
+ var waitTime = client.globals.maxWaitTime,
+ newDatabaseName = client.globals.testDatabaseName,
+ baseUrl = client.globals.test_settings.launch_url;
+
+ client
+ // this creates a view
+ .populateDatabase(newDatabaseName)
+ .loginToGUI()
+
+ // now create a search index
+ .url(baseUrl + '/#/database/' + newDatabaseName + '/new_search/testdesigndoc')
+ .waitForElementVisible('#save-index', waitTime, false)
+ .clearValue('#search-name')
+ .setValue('#search-name', 'search-index1')
+ .clickWhenVisible('#save-index')
+ .waitForElementVisible('#global-notifications .alert.alert-success', waitTime, false)
+ .url(baseUrl + '/#/database/' + newDatabaseName + '/_all_docs')
+ .waitForElementPresent('.tableview-checkbox-cell', client.globals.maxWaitTime, false)
+ .waitForElementNotPresent('.loading-lines', waitTime, false)
+
+ // now delete the search index. Since there's a view in this design doc, the design doc should not be removed
+ .clickWhenVisible('#nav-design-function-testdesigndocindexes .index-list li span', waitTime, true)
+ .clickWhenVisible('.popover-content .fonticon-trash', waitTime, true)
+ .waitForElementVisible('div.confirmation-modal', waitTime, false)
+ .clickWhenVisible('.confirmation-modal button.btn.btn-primary')
+
+ // just assert the search indexes section has been removed, but the design doc still exists
+ .waitForElementNotPresent('#nav-design-function-testdesigndocindexes', waitTime, true)
+ .waitForElementPresent('#testdesigndoc', waitTime, true)
+
+ .end();
+ }
+
+};
diff --git a/app/addons/search/tests/nightwatch/searchPageApiBar.js b/app/addons/search/tests/nightwatch/searchPageApiBar.js
new file mode 100644
index 0000000..ac503c6
--- /dev/null
+++ b/app/addons/search/tests/nightwatch/searchPageApiBar.js
@@ -0,0 +1,55 @@
+/*
+* Licensed Materials - Property of IBM
+*
+* "Restricted Materials of IBM"
+*
+* (C) Copyright IBM Corp. 2018 All Rights Reserved
+*
+* US Government Users Restricted Rights - Use, duplication or disclosure
+* restricted by GSA ADP Schedule Contract with IBM Corp.
+*/
+module.exports = {
+
+ 'Check API Bar is present/hidden on appropriate page and is encoded': function (client) {
+ const newDatabaseName = client.globals.testDatabaseName,
+ baseUrl = client.globals.test_settings.launch_url;
+
+ const searchStr = "class:bird";
+ const searchStrEncoded = encodeURIComponent(searchStr);
+ const fullURL = baseUrl + '/' + newDatabaseName + '/_design/keyview/_search/api-bar-test?q=' + searchStrEncoded;
+
+ client
+ .loginToGUI()
+ .populateDatabase(newDatabaseName)
+ .url(baseUrl + '/#/database/' + newDatabaseName + '/_all_docs')
+
+ // start creating a search index in _design/keyview design doc
+ .waitForElementPresent('#new-design-docs-button', client.globals.maxWaitTime, false)
+ .click('#new-design-docs-button a')
+ .click('#new-design-docs-button a[href="#/database/' + newDatabaseName + '/new_search"]')
+ .clickWhenVisible('.styled-select select')
+
+ // confirm there's no API URL field on the create index page
+ .pause(5000)
+ .assert.elementNotPresent('.faux__jsonlink')
+
+ // now create the rest of the index
+ .keys(['_design/keyview', '\uE006'])
+ .clearValue('#search-name')
+ .setValue('#search-name', 'api-bar-test')
+ .clickWhenVisible('#save-index')
+ .waitForElementVisible('#global-notifications .alert.alert-success', client.globals.maxWaitTime, false)
+
+ // confirm the API URL field now shows up (we're on the edit search index page now)
+ .assert.elementPresent('.faux__jsonlink')
+
+ // now enter a search and confirm it's properly encoded in the api URL bar
+ .setValue('#search-index-preview-form input', searchStr)
+ .clickWhenVisible('#search-index-preview-form button')
+
+ .waitForElementNotVisible('#global-notifications', client.globals.maxWaitTime, false)
+ .waitForElementNotPresent('.loading-lines', client.globals.maxWaitTime, false)
+ .assert.attributeContains('.faux__jsonlink-link', 'href', fullURL)
+ .end();
+ }
+};
diff --git a/app/addons/search/tests/nightwatch/sharedSearch.js b/app/addons/search/tests/nightwatch/sharedSearch.js
new file mode 100644
index 0000000..7cae1e2
--- /dev/null
+++ b/app/addons/search/tests/nightwatch/sharedSearch.js
@@ -0,0 +1,39 @@
+/*
+* Licensed Materials - Property of IBM
+*
+* "Restricted Materials of IBM"
+*
+* (C) Copyright IBM Corp. 2018 All Rights Reserved
+*
+* US Government Users Restricted Rights - Use, duplication or disclosure
+* restricted by GSA ADP Schedule Contract with IBM Corp.
+*/
+module.exports = {
+
+ 'Edits existing search route works': function (client) {
+ /*jshint multistr: true */
+ const baseUrl = client.globals.test_settings.launch_url;
+
+ client
+ .loginToGUI()
+ .url(baseUrl + '/#database/shared/dashboard-test-account/shareddatabasesrule/_design/for-tests/_search/test-can-edit/edit')
+ // wait for the page to fully load
+ .waitForElementNotPresent('.loading-lines', client.globals.maxWaitTime, false)
+ .waitForElementVisible('#save-index', client.globals.maxWaitTime)
+ .end();
+ },
+
+ 'Create new search route works': function (client) {
+ /*jshint multistr: true */
+ const baseUrl = client.globals.test_settings.launch_url;
+
+ client
+ .loginToGUI()
+ .url(baseUrl + '/#database/shared/dashboard-test-account/shareddatabasesrule/new_search')
+ // wait for the page to fully load
+ .waitForElementNotPresent('.loading-lines', client.globals.maxWaitTime, false)
+ .waitForElementVisible('#save-index', client.globals.maxWaitTime)
+ .end();
+ }
+
+};