feat(rules): support search
This commit is contained in:
parent
79b9b1db1a
commit
57b353387a
10 changed files with 161 additions and 11 deletions
|
@ -38,6 +38,7 @@
|
||||||
"classnames": "^2.2.6",
|
"classnames": "^2.2.6",
|
||||||
"history": "^4.7.2",
|
"history": "^4.7.2",
|
||||||
"invariant": "^2.2.4",
|
"invariant": "^2.2.4",
|
||||||
|
"lodash-es": "^4.17.11",
|
||||||
"memoize-one": "^5.0.0",
|
"memoize-one": "^5.0.0",
|
||||||
"modern-normalize": "^0.5.0",
|
"modern-normalize": "^0.5.0",
|
||||||
"prop-types": "^15.5.10",
|
"prop-types": "^15.5.10",
|
||||||
|
|
|
@ -21,7 +21,7 @@ class ErrorBoundary extends Component {
|
||||||
};
|
};
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.loadSentry();
|
// this.loadSentry();
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidCatch(error, errorInfo) {
|
componentDidCatch(error, errorInfo) {
|
||||||
|
|
49
src/components/RuleSearch.js
Normal file
49
src/components/RuleSearch.js
Normal 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>
|
||||||
|
);
|
||||||
|
});
|
44
src/components/RuleSearch.module.scss
Normal file
44
src/components/RuleSearch.module.scss
Normal 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;
|
||||||
|
}
|
|
@ -1,34 +1,42 @@
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { useActions, useStoreState } from 'm/store';
|
import { useActions, useStoreState } from 'm/store';
|
||||||
|
import Button from 'c/Button';
|
||||||
|
|
||||||
import ContentHeader from 'c/ContentHeader';
|
import ContentHeader from 'c/ContentHeader';
|
||||||
import Rule from 'c/Rule';
|
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 => ({
|
const mapStateToProps = s => ({
|
||||||
rules: getRules(s)
|
rules: getRules(s)
|
||||||
});
|
});
|
||||||
|
|
||||||
const actions = {
|
const actions = {
|
||||||
fetchRules
|
fetchRulesOnce
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function Rules() {
|
export default function Rules() {
|
||||||
const { fetchRules } = useActions(actions);
|
const { fetchRulesOnce } = useActions(actions);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchRules();
|
fetchRulesOnce();
|
||||||
}, []);
|
}, []);
|
||||||
const { rules } = useStoreState(mapStateToProps);
|
const { rules } = useStoreState(mapStateToProps);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<ContentHeader title="Rules" />
|
<ContentHeader title="Rules" />
|
||||||
|
<RuleSearch />
|
||||||
<div style={{ paddingBottom: 30 }}>
|
<div style={{ paddingBottom: 30 }}>
|
||||||
{rules.map(r => {
|
{rules.map(r => {
|
||||||
return <Rule key={r.id} {...r} />;
|
return <Rule key={r.id} {...r} />;
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
<div className={s0.fabgrp}>
|
||||||
|
<Button label="Refresh" onClick={() => {}} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
5
src/components/Rules.module.scss
Normal file
5
src/components/Rules.module.scss
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
.fabgrp {
|
||||||
|
position: fixed;
|
||||||
|
right: 20px;
|
||||||
|
bottom: 20px;
|
||||||
|
}
|
|
@ -1,11 +1,29 @@
|
||||||
import * as rulesAPI from 'a/rules';
|
import * as rulesAPI from 'a/rules';
|
||||||
import { getClashAPIConfig } from 'd/app';
|
import { getClashAPIConfig } from 'd/app';
|
||||||
import invariant from 'invariant';
|
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 CompletedFetchRules = 'rules/CompletedFetchRules';
|
||||||
|
const UpdateSearchText = 'rule/UpdateSearchText';
|
||||||
|
|
||||||
|
export function updateSearchText(text) {
|
||||||
|
return {
|
||||||
|
type: UpdateSearchText,
|
||||||
|
payload: { searchText: text.toLowerCase() }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function fetchRules() {
|
export function fetchRules() {
|
||||||
return async (dispatch, getState) => {
|
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":"FINAL","payload":"","proxy":"Proxy"}
|
||||||
// {"type":"IPCIDR","payload":"172.16.0.0/12","proxy":"DIRECT"}
|
// {"type":"IPCIDR","payload":"172.16.0.0/12","proxy":"DIRECT"}
|
||||||
const initialState = {
|
const initialState = {
|
||||||
// filteredRules: [],
|
// filteredRules: [],
|
||||||
allRules: []
|
allRules: [],
|
||||||
|
searchText: ''
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function reducer(state = initialState, { type, payload }) {
|
export default function reducer(state = initialState, { type, payload }) {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
|
case UpdateSearchText:
|
||||||
case CompletedFetchRules: {
|
case CompletedFetchRules: {
|
||||||
return { ...state, ...payload };
|
return { ...state, ...payload };
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 PropTypes from 'prop-types';
|
||||||
import { bindActionCreators } from 'redux';
|
import { bindActionCreators } from 'redux';
|
||||||
import shallowEqual from './shallowEqual';
|
import shallowEqual from './shallowEqual';
|
||||||
|
@ -22,12 +28,16 @@ export function useStore() {
|
||||||
return useContext(StoreContext);
|
return useContext(StoreContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useActions(actions) {
|
function bindActions(actions, dispatch) {
|
||||||
const { dispatch } = useStore();
|
|
||||||
const a = typeof actions === 'function' ? actions() : actions;
|
const a = typeof actions === 'function' ? actions() : actions;
|
||||||
return bindActionCreators(a, dispatch);
|
return bindActionCreators(a, dispatch);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useActions(actions) {
|
||||||
|
const { dispatch } = useStore();
|
||||||
|
return useMemo(() => bindActions(actions, dispatch), [actions]);
|
||||||
|
}
|
||||||
|
|
||||||
export function useStoreState(selector) {
|
export function useStoreState(selector) {
|
||||||
const store = useStore();
|
const store = useStore();
|
||||||
const initialMappedState = selector(store.getState());
|
const initialMappedState = selector(store.getState());
|
||||||
|
|
1
src/svg/search.svg
Normal file
1
src/svg/search.svg
Normal 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 |
|
@ -4667,6 +4667,11 @@ locate-path@^3.0.0:
|
||||||
p-locate "^3.0.0"
|
p-locate "^3.0.0"
|
||||||
path-exists "^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:
|
lodash.assign@^4.2.0:
|
||||||
version "4.2.0"
|
version "4.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/lodash.assign/-/lodash.assign-4.2.0.tgz#0d99f3ccd7a6d261d19bdaeb9245005d285808e7"
|
resolved "https://registry.yarnpkg.com/lodash.assign/-/lodash.assign-4.2.0.tgz#0d99f3ccd7a6d261d19bdaeb9245005d285808e7"
|
||||||
|
|
Loading…
Reference in a new issue