Skip to content

Commit

Permalink
Add Search support (#1227)
Browse files Browse the repository at this point in the history
Adds a search module to Fauxton. The functionality is only enabled
upon detection of "search" in the reported CouchDB features.

When enabled, it adds:
 * New dropdown options to create/update search indexes in the sidebar
 * New panel to run search queries from the sidebar
 * Text index templates to the Mango Index editor

Also added a CouchDB 2 / 3 / dev build matrix to Travis since the official CouchDB image doesn't include the search feature yet.
  • Loading branch information
willholley authored and Antonio-Maranhao committed Oct 7, 2019
1 parent 9dada0e commit 4450d4d
Show file tree
Hide file tree
Showing 39 changed files with 3,170 additions and 12 deletions.
5 changes: 5 additions & 0 deletions .env
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Default environment variables for the Docker Compose files in /docker
# This file needs to be placed where the docker-compose command is run from.

# Used to provide a CouchDB 2 / 3 build matrix in .travis.yml
COUCHDB_IMAGE=ibmcom/couchdb3:preview-1569600329
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,14 @@ services:
git:
depth: 1

env:
- COUCHDB_IMAGE=apache/couchdb:2.3.1 NIGHTWATCH_SKIPTAGS="search,partitioned"
- COUCHDB_IMAGE=couchdb:dev NIGHTWATCH_SKIPTAGS="search,nonpartitioned"
- COUCHDB_IMAGE=ibmcom/couchdb3:preview-1569600329 NIGHTWATCH_SKIPTAGS=nonpartitioned

before_install:
- npm install -g npm@latest
- ./bin/build-couchdb-dev.sh
install:
- npm ci
before_script:
Expand Down
1 change: 1 addition & 0 deletions app/addons/databases/tests/nightwatch/createsDatabase.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ var newDatabaseName = 'fauxton-selenium-tests-db-create';
var invalidDatabaseName = 'fauxton-selenium-tests-#####';
var helpers = require('../../../../../test/nightwatch_tests/helpers/helpers.js');
module.exports = {
'@tags': ['nonpartitioned'],

before: function (client, done) {
const nano = helpers.getNanoInstance(client.globals.test_settings.db_url);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// 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
//
// https://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.



var newDatabaseName = 'fauxton-selenium-tests-db-create';
var invalidDatabaseName = 'fauxton-selenium-tests-#####';
var helpers = require('../../../../../test/nightwatch_tests/helpers/helpers.js');
module.exports = {
'@tags': ['partitioned'],

before: function (client, done) {
const nano = helpers.getNanoInstance(client.globals.test_settings.db_url);
nano.db.destroy(newDatabaseName).then(() => {
done();
}).catch(() => {
done();
});
},

after: function (client, done) {
const nano = helpers.getNanoInstance(client.globals.test_settings.db_url);
nano.db.destroy(newDatabaseName).then(() => {
done();
}).catch(() => {
console.warn(`Could not delete ${newDatabaseName} db`);
done();
});
},

'Creates a Database' : function (client) {
var waitTime = client.globals.maxWaitTime,
baseUrl = client.globals.test_settings.launch_url;

client
.loginToGUI()
.checkForDatabaseDeleted(newDatabaseName, waitTime)
.url(baseUrl)

// ensure the page has fully loaded
.waitForElementPresent('.databases.table', waitTime, false)
.clickWhenVisible('.add-new-database-btn')
.waitForElementVisible('#js-new-database-name', waitTime, false)
.setValue('#js-new-database-name', [newDatabaseName])
.clickWhenVisible('#non-partitioned-db', waitTime, false)
.clickWhenVisible('#js-create-database', waitTime, false)
.waitForElementNotPresent('.new-database-tray', waitTime, false)
.checkForDatabaseCreated(newDatabaseName, waitTime)
.url(baseUrl + '/_all_dbs')
.waitForElementVisible('html', waitTime, false)
.getText('html', function (result) {
var data = result.value,
createdDatabaseIsPresent = data.indexOf(newDatabaseName);

this.verify.ok(createdDatabaseIsPresent > 0,
'Checking if new database shows up in _all_dbs.');
})
.end();
},

'Creates a Database with invalid name' : function (client) {
var waitTime = client.globals.maxWaitTime,
baseUrl = client.globals.test_settings.launch_url;

client
.loginToGUI()
.checkForDatabaseDeleted(invalidDatabaseName, waitTime)
.url(baseUrl)

// ensure the page has fully loaded
.waitForElementPresent('.databases.table', waitTime, false)
.clickWhenVisible('.add-new-database-btn')
.waitForElementVisible('#js-new-database-name', waitTime, false)
.setValue('#js-new-database-name', [invalidDatabaseName])
.clickWhenVisible('#non-partitioned-db', waitTime, false)
.clickWhenVisible('#js-create-database', waitTime, false)
.waitForElementVisible('.global-notification.alert.alert-error', waitTime, false)
.url(baseUrl + '/_all_dbs')
.waitForElementVisible('html', waitTime, false)
.getText('html', function (result) {
var data = result.value,
createdDatabaseIsPresent = data.indexOf(invalidDatabaseName);

this.verify.ok(createdDatabaseIsPresent === -1,
'Checking if new database shows up in _all_dbs.');
})
.end();
}
};
4 changes: 2 additions & 2 deletions app/addons/documents/assets/less/query-options.less
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@
.btn.active {
background: #fff;
color: @linkColorHover;
box-shadow: 2px 2px 0px rgba(0, 0, 0, 0.25) inset, 2px 2px 2px rgba(0, 0, 0, 0.15);
box-shadow: none;
}
label:first-child {
.border-radius(5px 0 0 5px);
Expand Down Expand Up @@ -174,7 +174,7 @@
.hide {
display: none;
}

.additionalParams {
margin-bottom: 2px;
}
Expand Down
160 changes: 160 additions & 0 deletions app/addons/search/__tests__/components.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
// 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
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations under
// the License.
import {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();
});
});
53 changes: 53 additions & 0 deletions app/addons/search/__tests__/search.actions.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// 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
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations under
// the License.

import 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');
});
});

});
Loading

0 comments on commit 4450d4d

Please sign in to comment.