diff --git a/package.json b/package.json
index c775402..29a25c0 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/src/components/ErrorBoundary.js b/src/components/ErrorBoundary.js
index 94927b7..384a291 100644
--- a/src/components/ErrorBoundary.js
+++ b/src/components/ErrorBoundary.js
@@ -21,7 +21,7 @@ class ErrorBoundary extends Component {
};
componentDidMount() {
- this.loadSentry();
+ // this.loadSentry();
}
componentDidCatch(error, errorInfo) {
diff --git a/src/components/RuleSearch.js b/src/components/RuleSearch.js
new file mode 100644
index 0000000..80d42d7
--- /dev/null
+++ b/src/components/RuleSearch.js
@@ -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 (
+
+ );
+});
diff --git a/src/components/RuleSearch.module.scss b/src/components/RuleSearch.module.scss
new file mode 100644
index 0000000..31e6e36
--- /dev/null
+++ b/src/components/RuleSearch.module.scss
@@ -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;
+}
diff --git a/src/components/Rules.js b/src/components/Rules.js
index 5bca9e4..5292d49 100644
--- a/src/components/Rules.js
+++ b/src/components/Rules.js
@@ -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 (
+
{rules.map(r => {
return ;
})}
+
+
);
}
diff --git a/src/components/Rules.module.scss b/src/components/Rules.module.scss
new file mode 100644
index 0000000..1fb94eb
--- /dev/null
+++ b/src/components/Rules.module.scss
@@ -0,0 +1,5 @@
+.fabgrp {
+ position: fixed;
+ right: 20px;
+ bottom: 20px;
+}
diff --git a/src/ducks/rules.js b/src/ducks/rules.js
index ffd8c64..c38dc6c 100644
--- a/src/ducks/rules.js
+++ b/src/ducks/rules.js
@@ -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 };
}
diff --git a/src/misc/store.js b/src/misc/store.js
index c44396f..d658d71 100644
--- a/src/misc/store.js
+++ b/src/misc/store.js
@@ -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());
diff --git a/src/svg/search.svg b/src/svg/search.svg
new file mode 100644
index 0000000..a9905ed
--- /dev/null
+++ b/src/svg/search.svg
@@ -0,0 +1 @@
+
diff --git a/yarn.lock b/yarn.lock
index a8496f5..fe721d7 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -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"