Commit 05db2def authored by Eugen Rochko's avatar Eugen Rochko

Add search to the instance picker

parent 5c0a0ac0
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
"private": true, "private": true,
"dependencies": { "dependencies": {
"axios": "^0.16.2", "axios": "^0.16.2",
"fuzzysearch": "^1.0.3",
"gh-pages": "^0.12.0", "gh-pages": "^0.12.0",
"react": "^15.5.3", "react": "^15.5.3",
"react-custom-scrollbars": "^4.1.2", "react-custom-scrollbars": "^4.1.2",
......
import React from 'react'; import React from 'react';
import { fetchInstances } from './actions';
import WizardRow from './WizardRow'; import WizardRow from './WizardRow';
import { Scrollbars } from 'react-custom-scrollbars'; import { Scrollbars } from 'react-custom-scrollbars';
export default class Wizard extends React.Component { export default class Wizard extends React.PureComponent {
componentDidMount () { componentDidMount () {
this.props.dispatch(fetchInstances()); this.props.onMount();
}
handleChange = e => {
this.props.onChange(e.target.value);
}
handleClear = e => {
e.preventDefault();
this.props.onClear();
} }
render () { render () {
const { instances } = this.props; const { instances, searchValue } = this.props;
const hasValue = searchValue.length > 0;
return ( return (
<div className='wizard-page' id='getting-started'> <div className='wizard-page' id='getting-started'>
...@@ -31,6 +40,23 @@ export default class Wizard extends React.Component { ...@@ -31,6 +40,23 @@ export default class Wizard extends React.Component {
)} )}
</Scrollbars> </Scrollbars>
</div> </div>
<div className='wizard-controls'>
<div className='search'>
<input
className='search__input'
type='text'
placeholder='Search'
value={searchValue}
onChange={this.handleChange}
/>
<div role='button' tabIndex='0' className='search__icon' onClick={this.handleClear}>
<i className={`ion-android-search ${hasValue ? '' : 'active'}`} />
<i className={`ion-android-cancel ${hasValue ? 'active' : ''}`} />
</div>
</div>
</div>
</div> </div>
); );
} }
......
import Wizard from './Wizard'; import Wizard from './Wizard';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { fetchInstances, changeSearchValue } from './actions';
import fuzzysearch from 'fuzzysearch';
const getInstances = createSelector( const getInstances = createSelector(
[state => state.instances], [
instances => instances.filter(item => !item.dead && item.uptime > 0.70) state => state.searchValue,
state => state.instances,
],
(searchValue, instances) => {
searchValue = searchValue.toLowerCase();
return instances.filter(item => {
const eligible = !item.dead && item.uptime > 0.70;
const isSearching = searchValue.length > 0;
return eligible &&
(!isSearching || fuzzysearch(searchValue, item.searchable));
});
}
); );
const mapStateToProps = state => ({ const mapStateToProps = state => ({
instances: getInstances(state), instances: getInstances(state),
searchValue: state.searchValue,
});
const mapDispatchToProps = dispatch => ({
onMount: () => dispatch(fetchInstances()),
onChange: value => dispatch(changeSearchValue(value)),
onClear: () => dispatch(changeSearchValue('')),
}); });
export default connect(mapStateToProps)(Wizard); export default connect(mapStateToProps, mapDispatchToProps)(Wizard);
import axios from 'axios'; import axios from 'axios';
export const INSTANCES_FETCH_SUCCESS = 'INSTANCES_FETCH_SUCCESS'; export const INSTANCES_FETCH_SUCCESS = 'INSTANCES_FETCH_SUCCESS';
export const SEARCH_VALUE_CHANGE = 'SEARCH_VALUE_CHANGE';
export function fetchInstances() { export function fetchInstances() {
return (dispatch, getState) => { return (dispatch, getState) => {
...@@ -19,3 +20,10 @@ export function fetchInstancesSuccess(data) { ...@@ -19,3 +20,10 @@ export function fetchInstancesSuccess(data) {
data, data,
}; };
}; };
export function changeSearchValue(data) {
return {
type: SEARCH_VALUE_CHANGE,
data,
};
};
import { INSTANCES_FETCH_SUCCESS } from './actions'; import {
INSTANCES_FETCH_SUCCESS,
SEARCH_VALUE_CHANGE,
} from './actions';
const initialState = { const initialState = {
instances: [], instances: [],
searchValue: '',
};
const createSearchable = item => {
let searchable = [];
searchable.push(item.name);
if (item.info) {
searchable.push(item.info.theme || 'general');
}
return { ...item, searchable: searchable.join(' ').toLowerCase() };
}; };
export default function reducer(state = initialState, action) { export default function reducer(state = initialState, action) {
switch(action.type) { switch(action.type) {
case INSTANCES_FETCH_SUCCESS: case INSTANCES_FETCH_SUCCESS:
return { ...state, instances: action.data }; return { ...state, instances: action.data.map(createSearchable) };
case SEARCH_VALUE_CHANGE:
return { ...state, searchValue: action.data };
default: default:
return state; return state;
} }
......
...@@ -489,6 +489,10 @@ $phi: 1.6180339887498948482; ...@@ -489,6 +489,10 @@ $phi: 1.6180339887498948482;
height: 120px; height: 120px;
} }
} }
.as-seen-on .logo-grid {
flex-direction: column;
}
} }
@keyframes floating { @keyframes floating {
......
...@@ -21,10 +21,12 @@ ...@@ -21,10 +21,12 @@
.wizard { .wizard {
margin: 50px auto; margin: 50px auto;
margin-bottom: 15px;
border: 1px solid darken($darkest, 4%); border: 1px solid darken($darkest, 4%);
background: lighten($darkest, 8%); background: lighten($darkest, 8%);
border-radius: 10px; border-radius: 10px;
max-width: 800px; max-width: 800px;
overflow: hidden;
.wizard-header { .wizard-header {
display: flex; display: flex;
...@@ -132,3 +134,94 @@ ...@@ -132,3 +134,94 @@
background: $error; background: $error;
} }
} }
.search {
position: relative;
}
.wizard-controls {
margin-bottom: 50px;
display: flex;
justify-content: flex-end;
}
.search__input {
padding-right: 30px;
outline: 0;
box-sizing: border-box;
border-radius: 4px;
display: block;
width: 100%;
border: none;
padding: 10px;
padding-right: 30px;
font-family: inherit;
background: darken($darkest, 8%);
color: $lighter;
font-size: 14px;
margin: 0;
&::-moz-focus-inner {
border: 0;
}
&::-moz-focus-inner,
&:focus,
&:active {
outline: 0 !important;
}
&:focus {
background: darken($darkest, 4%);
}
@media screen and (max-width: 600px) {
font-size: 16px;
}
}
.search__icon {
.ion-android-search, .ion-android-cancel {
position: absolute;
top: 10px;
right: 10px;
z-index: 2;
display: inline-block;
opacity: 0;
transition: all 100ms linear;
font-size: 18px;
width: 18px;
height: 18px;
line-height: 18px;
color: $lighter;
cursor: default;
pointer-events: none;
&.active {
pointer-events: auto;
opacity: 0.3;
}
}
.ion-android-search {
transform: translateZ(0) rotate(90deg);
&.active {
pointer-events: none;
transform: translateZ(0) rotate(0deg);
}
}
.ion-android-cancel {
transform: translateZ(0) rotate(0deg);
cursor: pointer;
&.active {
transform: translateZ(0) rotate(90deg);
}
&:hover {
color: $lightest;
}
}
}
...@@ -2337,6 +2337,10 @@ function-bind@^1.0.2, function-bind@^1.1.0: ...@@ -2337,6 +2337,10 @@ function-bind@^1.0.2, function-bind@^1.1.0:
version "1.1.0" version "1.1.0"
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.0.tgz#16176714c801798e4e8f2cf7f7529467bb4a5771" resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.0.tgz#16176714c801798e4e8f2cf7f7529467bb4a5771"
fuzzysearch@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/fuzzysearch/-/fuzzysearch-1.0.3.tgz#dffc80f6d6b04223f2226aa79dd194231096d008"
gauge@~2.7.1: gauge@~2.7.1:
version "2.7.3" version "2.7.3"
resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.3.tgz#1c23855f962f17b3ad3d0dc7443f304542edfe09" resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.3.tgz#1c23855f962f17b3ad3d0dc7443f304542edfe09"
...@@ -4423,14 +4427,14 @@ promise@7.1.1, promise@^7.1.1: ...@@ -4423,14 +4427,14 @@ promise@7.1.1, promise@^7.1.1:
dependencies: dependencies:
asap "~2.0.3" asap "~2.0.3"
prop-types@^15.5.10: prop-types@^15.5.10, prop-types@~15.5.0:
version "15.5.10" version "15.5.10"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.5.10.tgz#2797dfc3126182e3a95e3dfbb2e893ddd7456154" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.5.10.tgz#2797dfc3126182e3a95e3dfbb2e893ddd7456154"
dependencies: dependencies:
fbjs "^0.8.9" fbjs "^0.8.9"
loose-envify "^1.3.1" loose-envify "^1.3.1"
prop-types@^15.5.2, prop-types@~15.5.0: prop-types@^15.5.2:
version "15.5.4" version "15.5.4"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.5.4.tgz#2ed3692716a5060f8cc020946d8238e7419d92c0" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.5.4.tgz#2ed3692716a5060f8cc020946d8238e7419d92c0"
dependencies: dependencies:
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment