Skip to content

Commit 4450d4d

Browse files
willholleyAntonio-Maranhao
authored andcommitted
Add Search support (#1227)
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.
1 parent 9dada0e commit 4450d4d

39 files changed

+3170
-12
lines changed

.env

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Default environment variables for the Docker Compose files in /docker
2+
# This file needs to be placed where the docker-compose command is run from.
3+
4+
# Used to provide a CouchDB 2 / 3 build matrix in .travis.yml
5+
COUCHDB_IMAGE=ibmcom/couchdb3:preview-1569600329

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ app/addons/*
2020
!app/addons/styletests
2121
!app/addons/cors
2222
!app/addons/setup
23+
!app/addons/search
2324
settings.json*
2425
i18n.json
2526
!settings.json.default

.travis.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,14 @@ services:
77
git:
88
depth: 1
99

10+
env:
11+
- COUCHDB_IMAGE=apache/couchdb:2.3.1 NIGHTWATCH_SKIPTAGS="search,partitioned"
12+
- COUCHDB_IMAGE=couchdb:dev NIGHTWATCH_SKIPTAGS="search,nonpartitioned"
13+
- COUCHDB_IMAGE=ibmcom/couchdb3:preview-1569600329 NIGHTWATCH_SKIPTAGS=nonpartitioned
14+
1015
before_install:
1116
- npm install -g npm@latest
17+
- ./bin/build-couchdb-dev.sh
1218
install:
1319
- npm ci
1420
before_script:

app/addons/databases/tests/nightwatch/createsDatabase.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ var newDatabaseName = 'fauxton-selenium-tests-db-create';
1616
var invalidDatabaseName = 'fauxton-selenium-tests-#####';
1717
var helpers = require('../../../../../test/nightwatch_tests/helpers/helpers.js');
1818
module.exports = {
19+
'@tags': ['nonpartitioned'],
1920

2021
before: function (client, done) {
2122
const nano = helpers.getNanoInstance(client.globals.test_settings.db_url);
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
// Licensed under the Apache License, Version 2.0 (the "License"); you may not
2+
// use this file except in compliance with the License. You may obtain a copy of
3+
// the License at
4+
//
5+
// http://www.apache.org/licenses/LICENSE-2.0
6+
//
7+
// Unless required by applicable law or agreed to in writing, software
8+
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9+
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10+
// License for the specific language governing permissions and limitations under
11+
// the License.
12+
13+
14+
15+
var newDatabaseName = 'fauxton-selenium-tests-db-create';
16+
var invalidDatabaseName = 'fauxton-selenium-tests-#####';
17+
var helpers = require('../../../../../test/nightwatch_tests/helpers/helpers.js');
18+
module.exports = {
19+
'@tags': ['partitioned'],
20+
21+
before: function (client, done) {
22+
const nano = helpers.getNanoInstance(client.globals.test_settings.db_url);
23+
nano.db.destroy(newDatabaseName).then(() => {
24+
done();
25+
}).catch(() => {
26+
done();
27+
});
28+
},
29+
30+
after: function (client, done) {
31+
const nano = helpers.getNanoInstance(client.globals.test_settings.db_url);
32+
nano.db.destroy(newDatabaseName).then(() => {
33+
done();
34+
}).catch(() => {
35+
console.warn(`Could not delete ${newDatabaseName} db`);
36+
done();
37+
});
38+
},
39+
40+
'Creates a Database' : function (client) {
41+
var waitTime = client.globals.maxWaitTime,
42+
baseUrl = client.globals.test_settings.launch_url;
43+
44+
client
45+
.loginToGUI()
46+
.checkForDatabaseDeleted(newDatabaseName, waitTime)
47+
.url(baseUrl)
48+
49+
// ensure the page has fully loaded
50+
.waitForElementPresent('.databases.table', waitTime, false)
51+
.clickWhenVisible('.add-new-database-btn')
52+
.waitForElementVisible('#js-new-database-name', waitTime, false)
53+
.setValue('#js-new-database-name', [newDatabaseName])
54+
.clickWhenVisible('#non-partitioned-db', waitTime, false)
55+
.clickWhenVisible('#js-create-database', waitTime, false)
56+
.waitForElementNotPresent('.new-database-tray', waitTime, false)
57+
.checkForDatabaseCreated(newDatabaseName, waitTime)
58+
.url(baseUrl + '/_all_dbs')
59+
.waitForElementVisible('html', waitTime, false)
60+
.getText('html', function (result) {
61+
var data = result.value,
62+
createdDatabaseIsPresent = data.indexOf(newDatabaseName);
63+
64+
this.verify.ok(createdDatabaseIsPresent > 0,
65+
'Checking if new database shows up in _all_dbs.');
66+
})
67+
.end();
68+
},
69+
70+
'Creates a Database with invalid name' : function (client) {
71+
var waitTime = client.globals.maxWaitTime,
72+
baseUrl = client.globals.test_settings.launch_url;
73+
74+
client
75+
.loginToGUI()
76+
.checkForDatabaseDeleted(invalidDatabaseName, waitTime)
77+
.url(baseUrl)
78+
79+
// ensure the page has fully loaded
80+
.waitForElementPresent('.databases.table', waitTime, false)
81+
.clickWhenVisible('.add-new-database-btn')
82+
.waitForElementVisible('#js-new-database-name', waitTime, false)
83+
.setValue('#js-new-database-name', [invalidDatabaseName])
84+
.clickWhenVisible('#non-partitioned-db', waitTime, false)
85+
.clickWhenVisible('#js-create-database', waitTime, false)
86+
.waitForElementVisible('.global-notification.alert.alert-error', waitTime, false)
87+
.url(baseUrl + '/_all_dbs')
88+
.waitForElementVisible('html', waitTime, false)
89+
.getText('html', function (result) {
90+
var data = result.value,
91+
createdDatabaseIsPresent = data.indexOf(invalidDatabaseName);
92+
93+
this.verify.ok(createdDatabaseIsPresent === -1,
94+
'Checking if new database shows up in _all_dbs.');
95+
})
96+
.end();
97+
}
98+
};

app/addons/documents/assets/less/query-options.less

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@
108108
.btn.active {
109109
background: #fff;
110110
color: @linkColorHover;
111-
box-shadow: 2px 2px 0px rgba(0, 0, 0, 0.25) inset, 2px 2px 2px rgba(0, 0, 0, 0.15);
111+
box-shadow: none;
112112
}
113113
label:first-child {
114114
.border-radius(5px 0 0 5px);
@@ -174,7 +174,7 @@
174174
.hide {
175175
display: none;
176176
}
177-
177+
178178
.additionalParams {
179179
margin-bottom: 2px;
180180
}
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
// Licensed under the Apache License, Version 2.0 (the "License"); you may not
2+
// use this file except in compliance with the License. You may obtain a copy of
3+
// the License at
4+
//
5+
// http://www.apache.org/licenses/LICENSE-2.0
6+
//
7+
// Unless required by applicable law or agreed to in writing, software
8+
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9+
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10+
// License for the specific language governing permissions and limitations under
11+
// the License.
12+
import {mount} from 'enzyme';
13+
import React from 'react';
14+
import sinon from 'sinon';
15+
import FauxtonAPI from '../../../core/api';
16+
import AnalyzerDropdown from '../components/AnalyzerDropdown';
17+
import SearchForm from '../components/SearchForm';
18+
import SearchIndexEditor from '../components/SearchIndexEditor';
19+
import '../base';
20+
21+
describe('SearchIndexEditor', () => {
22+
const defaultProps = {
23+
isLoading: false,
24+
isCreatingIndex: false,
25+
database: { id: 'my_db' },
26+
lastSavedDesignDocName: 'last_ddoc',
27+
lastSavedSearchIndexName: 'last_idx',
28+
searchIndexFunction: '',
29+
saveDoc: {},
30+
designDocs: [],
31+
searchIndexName: '',
32+
ddocPartitioned: false,
33+
newDesignDocPartitioned: false,
34+
analyzerType: '',
35+
singleAnalyzer: '',
36+
defaultAnalyzer: '',
37+
defaultMultipleAnalyzer: '',
38+
analyzerFields: [],
39+
setAnalyzerType: () => {},
40+
setDefaultMultipleAnalyzer: () => {},
41+
setSingleAnalyzer: () => {},
42+
addAnalyzerRow: () => {},
43+
setSearchIndexName: () => {},
44+
saveSearchIndex: () => {},
45+
selectDesignDoc: () => {},
46+
updateNewDesignDocName: () => {}
47+
};
48+
49+
it('generates the correct cancel link when db, ddoc and views have special chars', () => {
50+
const editorEl = mount(<SearchIndexEditor
51+
{...defaultProps}
52+
database={{ id: 'db%$1' }}
53+
lastSavedDesignDocName={'_design/doc/1$2'}
54+
lastSavedSearchIndexName={'search?abc/123'}
55+
/>);
56+
const expectedUrl = `/${encodeURIComponent('db%$1')}/_design/${encodeURIComponent('doc/1$2')}/_search/${encodeURIComponent('search?abc/123')}`;
57+
expect(editorEl.find('a.index-cancel-link').prop('href')).toMatch(expectedUrl);
58+
});
59+
60+
it('does not save when missing the index name', () => {
61+
const spy = sinon.stub();
62+
const editorEl = mount(<SearchIndexEditor
63+
{...defaultProps}
64+
database={{ id: 'test_db' }}
65+
designDocs={[{id: '_design/d1'}, {id: '_design/d2'}]}
66+
ddocName='_design/d1'
67+
searchIndexName={''}
68+
saveSearchIndex={spy}
69+
saveDoc={{id: '_design/d'}}
70+
/>);
71+
72+
editorEl.find('button#save-index').simulate('click', {preventDefault: () => {}});
73+
sinon.assert.notCalled(spy);
74+
});
75+
});
76+
77+
describe('AnalyzerDropdown', () => {
78+
79+
it('check default values and settings', () => {
80+
const el = mount(<AnalyzerDropdown />);
81+
82+
// confirm default label
83+
expect(el.find('label').length).toBe(2);
84+
expect(el.find('label').first().text()).toBe('Type');
85+
86+
// confirm default value
87+
expect(el.find('select').hasClass('standard')).toBeTruthy();
88+
});
89+
90+
it('omits label element if empty label passed', () => {
91+
const el = mount(<AnalyzerDropdown label="" />);
92+
93+
// (1, because there are normally 2 labels, see prev test)
94+
expect(el.find('label').length).toBe(1);
95+
});
96+
97+
it('custom ID works', () => {
98+
const customID = 'myCustomID';
99+
const el = mount(<AnalyzerDropdown id={customID} />);
100+
expect(el.find('select').prop('id')).toBe(customID);
101+
});
102+
103+
it('sets default value', () => {
104+
const defaultSelected = 'russian';
105+
const el = mount(
106+
<AnalyzerDropdown defaultSelected={defaultSelected} />
107+
);
108+
109+
expect(el.find('select').hasClass(defaultSelected)).toBeTruthy();
110+
});
111+
112+
it('custom classes get applied', () => {
113+
const el = mount(<AnalyzerDropdown classes="nuthatch vulture" />);
114+
expect(el.find('.nuthatch').exists()).toBeTruthy();
115+
expect(el.find('.vulture').exists()).toBeTruthy();
116+
});
117+
118+
it('custom change handler gets called', () => {
119+
const spy = sinon.spy();
120+
const el = mount(<AnalyzerDropdown onChange={spy} />);
121+
const newVal = 'whitespace';
122+
el.find('select').simulate('change', { target: { value: newVal }});
123+
expect(spy.calledOnce).toBeTruthy();
124+
});
125+
126+
});
127+
128+
describe('SearchForm', () => {
129+
const defaultProps = {
130+
searchResults: [{id: 'elephant'}],
131+
searchPerformed: true,
132+
hasActiveQuery: false,
133+
searchQuery: 'a_search',
134+
database: { id: 'foo' },
135+
querySearch: () => {},
136+
setSearchQuery: () => {}
137+
};
138+
139+
beforeEach(() => {
140+
sinon.stub(FauxtonAPI, 'urls').returns('/fake/url');
141+
});
142+
143+
afterEach(() => {
144+
FauxtonAPI.urls.restore();
145+
});
146+
147+
it('renders docs from the search results', () => {
148+
const el = mount(<SearchForm
149+
{...defaultProps}
150+
/>);
151+
expect(el.find('pre').first().text('elephant')).toBeTruthy();
152+
});
153+
154+
it('renders with links', () => {
155+
const el = mount(<SearchForm
156+
{...defaultProps}
157+
/>);
158+
expect(el.find('a')).toBeTruthy();
159+
});
160+
});
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
// Licensed under the Apache License, Version 2.0 (the "License"); you may not
2+
// use this file except in compliance with the License. You may obtain a copy of
3+
// the License at
4+
//
5+
// http://www.apache.org/licenses/LICENSE-2.0
6+
//
7+
// Unless required by applicable law or agreed to in writing, software
8+
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9+
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10+
// License for the specific language governing permissions and limitations under
11+
// the License.
12+
13+
import sinon from 'sinon';
14+
import utils from '../../../../test/mocha/testUtils';
15+
import FauxtonAPI from '../../../core/api';
16+
import Actions from '../actions';
17+
import * as API from '../api';
18+
import '../base';
19+
import '../../documents/base';
20+
21+
const {restore} = utils;
22+
FauxtonAPI.router = new FauxtonAPI.Router([]);
23+
24+
describe('search actions', () => {
25+
26+
afterEach(() => {
27+
restore(FauxtonAPI.navigate);
28+
restore(FauxtonAPI.addNotification);
29+
restore(API.fetchSearchResults);
30+
});
31+
32+
it("should show a notification and redirect if database doesn't exist", () => {
33+
const navigateSpy = sinon.spy(FauxtonAPI, 'navigate');
34+
const notificationSpy = sinon.spy(FauxtonAPI, 'addNotification');
35+
sinon.stub(API, 'fetchSearchResults').rejects(new Error('db not found'));
36+
FauxtonAPI.reduxDispatch = () => {};
37+
38+
const params = {
39+
databaseName: 'safe-id-db',
40+
designDoc: 'design-doc',
41+
indexName: 'idx1',
42+
query: 'a_query'
43+
};
44+
return Actions.dispatchInitSearchIndex(params)
45+
.then(() => {
46+
expect(notificationSpy.calledOnce).toBeTruthy();
47+
expect(/db not found/.test(notificationSpy.args[0][0].msg)).toBeTruthy();
48+
expect(navigateSpy.calledOnce).toBeTruthy();
49+
expect(navigateSpy.args[0][0]).toBe('database/safe-id-db/_all_docs');
50+
});
51+
});
52+
53+
});

0 commit comments

Comments
 (0)