feat(rules): support search

This commit is contained in:
Haishan 2019-01-05 00:45:33 +08:00
parent 79b9b1db1a
commit 57b353387a
10 changed files with 161 additions and 11 deletions

View file

@ -38,6 +38,7 @@
"classnames": "^2.2.6",
"history": "^4.7.2",
"invariant": "^2.2.4",
"lodash-es": "^4.17.11",
"memoize-one": "^5.0.0",
"modern-normalize": "^0.5.0",
"prop-types": "^15.5.10",

View file

@ -21,7 +21,7 @@ class ErrorBoundary extends Component {
};
componentDidMount() {
this.loadSentry();
// this.loadSentry();
}
componentDidCatch(error, errorInfo) {

View file

@ -0,0 +1,49 @@
import React, { useState, useMemo } from 'react';
import Icon from 'c/Icon';
import search from 's/search.svg';
import { useActions, useStoreState } from 'm/store';
import debounce from 'lodash-es/debounce';
import s0 from './RuleSearch.module.scss';
import { getSearchText, updateSearchText } from 'd/rules';
const mapStateToProps = s => ({
searchText: getSearchText(s)
});
const actions = { updateSearchText };
export default React.memo(function RuleSearch() {
const { updateSearchText } = useActions(actions);
const updateSearchTextDebounced = useMemo(
() => debounce(updateSearchText, 300),
[updateSearchText]
);
const { searchText } = useStoreState(mapStateToProps);
const [text, setText] = useState(searchText);
const onChange = e => {
setText(e.target.value);
updateSearchTextDebounced(e.target.value);
};
return (
<div className={s0.RuleSearch}>
<div className={s0.RuleSearchContainer}>
<div className={s0.inputWrapper}>
<input
type="text"
value={text}
onChange={onChange}
className={s0.input}
/>
</div>
<div className={s0.iconWrapper}>
<Icon id={search.id} />
</div>
</div>
</div>
);
});

View file

@ -0,0 +1,44 @@
.RuleSearch {
// width: 100%;
// padding: 0 40px;
// height: 40px;
padding: 0 40px 5px;
// height: 40px;
}
.RuleSearchContainer {
position: relative;
height: 40px;
}
.inputWrapper {
position: absolute;
top: 50%;
transform: translateY(-50%);
left: 0;
width: 100%;
}
.input {
-webkit-appearance: none;
background-color: var(--color-input-bg);
background-image: none;
border-radius: 20px;
border: 1px solid var(--color-input-border);
box-sizing: border-box;
color: #c1c1c1;
display: inline-block;
font-size: inherit;
height: 40px;
outline: none;
padding: 0 15px 0 35px;
transition: border-color 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);
width: 100%;
}
.iconWrapper {
position: absolute;
top: 50%;
transform: translateY(-50%);
left: 10px;
}

View file

@ -1,34 +1,42 @@
import React, { useEffect } from 'react';
import { useActions, useStoreState } from 'm/store';
import Button from 'c/Button';
import ContentHeader from 'c/ContentHeader';
import Rule from 'c/Rule';
import RuleSearch from 'c/RuleSearch';
import { getRules, fetchRules } from 'd/rules';
import { getRules, fetchRulesOnce } from 'd/rules';
import s0 from './Rules.module.scss';
const mapStateToProps = s => ({
rules: getRules(s)
});
const actions = {
fetchRules
fetchRulesOnce
};
export default function Rules() {
const { fetchRules } = useActions(actions);
const { fetchRulesOnce } = useActions(actions);
useEffect(() => {
fetchRules();
fetchRulesOnce();
}, []);
const { rules } = useStoreState(mapStateToProps);
return (
<div>
<ContentHeader title="Rules" />
<RuleSearch />
<div style={{ paddingBottom: 30 }}>
{rules.map(r => {
return <Rule key={r.id} {...r} />;
})}
</div>
<div className={s0.fabgrp}>
<Button label="Refresh" onClick={() => {}} />
</div>
</div>
);
}

View file

@ -0,0 +1,5 @@
.fabgrp {
position: fixed;
right: 20px;
bottom: 20px;
}

View file

@ -1,11 +1,29 @@
import * as rulesAPI from 'a/rules';
import { getClashAPIConfig } from 'd/app';
import invariant from 'invariant';
// import { createSelector } from 'reselect';
// import { debounce } from 'lodash-es';
import { createSelector } from 'reselect';
export const getRules = s => s.rules.allRules;
export const getAllRules = s => s.rules.allRules;
export const getSearchText = s => s.rules.searchText;
export const getRules = createSelector(
getSearchText,
getAllRules,
(searchText, allRules) => {
if (searchText === '') return allRules;
return allRules.filter(r => r.payload.indexOf(searchText) >= 0);
}
);
const CompletedFetchRules = 'rules/CompletedFetchRules';
const UpdateSearchText = 'rule/UpdateSearchText';
export function updateSearchText(text) {
return {
type: UpdateSearchText,
payload: { searchText: text.toLowerCase() }
};
}
export function fetchRules() {
return async (dispatch, getState) => {
@ -30,15 +48,24 @@ export function fetchRules() {
};
}
export function fetchRulesOnce() {
return async (dispatch, getState) => {
const allRules = getAllRules(getState());
if (allRules.length === 0) return await dispatch(fetchRules());
};
}
// {"type":"FINAL","payload":"","proxy":"Proxy"}
// {"type":"IPCIDR","payload":"172.16.0.0/12","proxy":"DIRECT"}
const initialState = {
// filteredRules: [],
allRules: []
allRules: [],
searchText: ''
};
export default function reducer(state = initialState, { type, payload }) {
switch (type) {
case UpdateSearchText:
case CompletedFetchRules: {
return { ...state, ...payload };
}

View file

@ -1,4 +1,10 @@
import React, { createContext, useState, useEffect, useContext } from 'react';
import React, {
createContext,
useState,
useEffect,
useContext,
useMemo
} from 'react';
import PropTypes from 'prop-types';
import { bindActionCreators } from 'redux';
import shallowEqual from './shallowEqual';
@ -22,12 +28,16 @@ export function useStore() {
return useContext(StoreContext);
}
export function useActions(actions) {
const { dispatch } = useStore();
function bindActions(actions, dispatch) {
const a = typeof actions === 'function' ? actions() : actions;
return bindActionCreators(a, dispatch);
}
export function useActions(actions) {
const { dispatch } = useStore();
return useMemo(() => bindActions(actions, dispatch), [actions]);
}
export function useStoreState(selector) {
const store = useStore();
const initialMappedState = selector(store.getState());

1
src/svg/search.svg Normal file
View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>

After

Width:  |  Height:  |  Size: 278 B

View file

@ -4667,6 +4667,11 @@ locate-path@^3.0.0:
p-locate "^3.0.0"
path-exists "^3.0.0"
lodash-es@^4.17.11:
version "4.17.11"
resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.11.tgz#145ab4a7ac5c5e52a3531fb4f310255a152b4be0"
integrity sha512-DHb1ub+rMjjrxqlB3H56/6MXtm1lSksDp2rA2cNWjG8mlDUYFhUj3Di2Zn5IwSU87xLv8tNIQ7sSwE/YOX/D/Q==
lodash.assign@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/lodash.assign/-/lodash.assign-4.2.0.tgz#0d99f3ccd7a6d261d19bdaeb9245005d285808e7"