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",
|
||||
"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",
|
||||
|
|
|
@ -21,7 +21,7 @@ class ErrorBoundary extends Component {
|
|||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.loadSentry();
|
||||
// this.loadSentry();
|
||||
}
|
||||
|
||||
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 { 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>
|
||||
);
|
||||
}
|
||||
|
|
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 { 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 };
|
||||
}
|
||||
|
|
|
@ -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
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"
|
||||
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"
|
||||
|
|
Loading…
Reference in a new issue