Skip to content

Commit

Permalink
Merge pull request #561 from mesosphere/fix/3018-ranked-search
Browse files Browse the repository at this point in the history
Better ranked search results in app list
  • Loading branch information
aldipower committed Jan 20, 2016
2 parents 60e1483 + 4215d80 commit 5e612c6
Show file tree
Hide file tree
Showing 4 changed files with 150 additions and 3 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
- \#2831 - Convert alert, confirm and prompt dialogs to new design
- \#2909 - Update dialog messages
- \#2655 - Never preselect dangerous actions in modal dialogs
- \#3018 - Better search result ranking and fuzzy matching

### Fixed
- \#2892 - Version in task detail component shouldn't be localised
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"eslint": "^1.10.1",
"eslint-plugin-react": "^3.10.0",
"flux": "^2.0.3",
"fuzzaldrin": "^2.1.0",
"highlight.js": "^9.0.0",
"isomorphic-fetch": "^2.2.0",
"lazy.js": "^0.4.0",
Expand Down Expand Up @@ -60,9 +61,9 @@
"gulp-util": "^3.0.5",
"gulp-zip": "^3.0.2",
"ionicons": "^2.0.1",
"jsdom": "^6.5.1",
"istanbul": "^1.0.0-alpha.2",
"istanbul-coveralls": "^1.0.3",
"jsdom": "^6.5.1",
"json-loader": "^0.5.2",
"mocha": "^2.3.4",
"mocha-teamcity-reporter": "0.0.4",
Expand Down
20 changes: 18 additions & 2 deletions src/js/components/AppListComponent.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import classNames from "classnames";
import lazy from "lazy.js";
import {Link} from "react-router";
import React from "react/addons";
import {score} from "fuzzaldrin";

import AppListViewTypes from "../constants/AppListViewTypes";
import AppStatus from "../constants/AppStatus";
Expand Down Expand Up @@ -186,7 +187,10 @@ var AppListComponent = React.createClass({

if (filterText != null && filterText !== "") {
nodesSequence = nodesSequence
.filter(app => app.id.indexOf(filterText) !== -1);
.filter(app => {
return score(app.id, filterText) > 0.004;
});

} else if (currentGroup !== "/") {
nodesSequence = nodesSequence
.filter(app => app.id.startsWith(currentGroup));
Expand Down Expand Up @@ -305,6 +309,15 @@ var AppListComponent = React.createClass({
return app[sortKey];
}, state.sortDescending);

let filterText = props.filters[FilterTypes.TEXT];
if (filterText != null && sortKey === "id") {
nodesSequence = nodesSequence.sort((a, b) => {
return score(a.id, filterText) > score(b.id, filterText)
? -1
: 1;
});
}

// Grouped node view
} else {
nodesSequence = lazy(this.getGroupedNodes(state.apps, filterCounts))
Expand Down Expand Up @@ -343,7 +356,10 @@ var AppListComponent = React.createClass({
},

getCaret: function (sortKey) {
if (sortKey === this.state.sortKey) {
var filterText = this.props.filters[FilterTypes.TEXT];

if (sortKey === this.state.sortKey &&
(sortKey !== "id" || filterText == null || filterText === "")) {
return (
<span className="caret"></span>
);
Expand Down
129 changes: 129 additions & 0 deletions src/test/apps.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1043,6 +1043,135 @@ describe("App component", function () {

});

describe("App List component", function () {

var longestAppId = "/omega/long-group-name-1/long-group-name-2/" +
"long-group-name-3/long-group-name-4/long-group-name-5/" +
"long-group-name-6/ok-that-should-do";
var apps = [
{id: "/app-alpha", instances: 1, mem: 16, cpus: 1},
{id: "/app-beta", instances: 1, mem: 16, cpus: 1},
{id: "/apps/sleep", instances: 1, mem: 16, cpus: 1},
{id: "/group/apps/sleepz", instances: 1, mem: 16, cpus: 1},
{id: longestAppId, instances: 1, mem: 16, cpus: 1}
];

before(function () {
AppDispatcher.dispatch({
actionType: AppsEvents.REQUEST_APPS,
data: {body: {apps: apps}}
});
});

it("displays the right entries", function () {
this.component = mount(<AppListComponent currentGroup="/" />);

var appNames = this.component
.find(AppListItemComponent)
.map(appNode => appNode.find(".name-cell").text());

expect(appNames).to.deep.equal([
"apps", "group", "omega", "app-alpha", "app-beta"
]);
this.component.instance().componentWillUnmount();
});

// The following cannot use mount() due to the BreadcrumbComponent's Link
// (see Breadcrumb Component tests)
describe("when the user enters a text filter", function () {
it("displays the exact search result match", function () {
var filters = {
filterText: "app-alpha"
};

this.component = shallow(<AppListComponent filters={filters}
currentGroup="/" />);

var appNames = this.component
.find(AppListItemComponent)
.map(app => app.props().model.id);

expect(appNames).to.deep.equal([
"/app-alpha"
]);
this.component.instance().componentWillUnmount();
});

it("handles fuzzy search input", function () {
var filters = {
filterText: "appsleep"
};

this.component = shallow(<AppListComponent filters={filters}
currentGroup="/" />);

var appNames = this.component
.find(AppListItemComponent)
.map(app => app.props().model.id);

expect(appNames).to.deep.equal([
"/apps/sleep",
"/group/apps/sleepz"
]);
this.component.instance().componentWillUnmount();
});

it("shows the right result for deeply nested paths", function () {
var filters = {
filterText: "omega"
};

this.component = shallow(<AppListComponent filters={filters}
currentGroup="/" />);

var appNames = this.component
.find(AppListItemComponent)
.map(app => app.props().model.id);

expect(appNames).to.deep.equal([longestAppId]);
this.component.instance().componentWillUnmount();
});

it("shows the best match first", function () {
var filters = {
filterText: "app"
};

this.component = shallow(<AppListComponent filters={filters}
currentGroup="/" />);

var appNames = this.component
.find(AppListItemComponent)
.map(app => app.props().model.id);

expect(appNames).to.deep.equal([
"/app-beta",
"/app-alpha",
"/apps/sleep",
"/group/apps/sleepz"
]);
this.component.instance().componentWillUnmount();
});

it("returns 0 results when no matches are found", function () {
var filters = {
filterText: "nope"
};

this.component = shallow(<AppListComponent filters={filters}
currentGroup="/" />);

var appNames = this.component
.find(AppListItemComponent)
.map(app => app.props().model.id);

expect(appNames).to.have.length(0);
this.component.instance().componentWillUnmount();
});
});

});

describe("App Health Bar", function () {

var model = {
Expand Down

0 comments on commit 5e612c6

Please sign in to comment.