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"