summaryrefslogtreecommitdiff
path: root/src/components
diff options
context:
space:
mode:
authorHaishan <[email protected]>2019-12-20 17:45:05 +0800
committerHaishan <[email protected]>2019-12-20 17:45:05 +0800
commitd81592ec970d207d4e37beb6c275ad6b77979e39 (patch)
tree33aac796297864d95307f21d6a9aa790e3c33c09 /src/components
parent040c5de04a75415490f9c478d931b7707bfa2486 (diff)
feat: support proxy provider
Diffstat (limited to 'src/components')
-rw-r--r--src/components/Button.js9
-rw-r--r--src/components/Button.module.css16
-rw-r--r--src/components/Proxies.js70
-rw-r--r--src/components/Proxies.module.css4
-rw-r--r--src/components/Proxy.js77
-rw-r--r--src/components/Proxy.module.css14
-rw-r--r--src/components/ProxyGroup.js130
-rw-r--r--src/components/ProxyGroup.module.css25
-rw-r--r--src/components/ProxyLatency.js35
-rw-r--r--src/components/ProxyProvider.js211
-rw-r--r--src/components/ProxyProvider.module.css43
-rw-r--r--src/components/ProxyProviderList.js19
-rw-r--r--src/components/Root.css13
-rw-r--r--src/components/Root.js51
-rw-r--r--src/components/StateProvider.js79
-rw-r--r--src/components/shared/Basic.js12
-rw-r--r--src/components/shared/Basic.module.css14
17 files changed, 664 insertions, 158 deletions
diff --git a/src/components/Button.js b/src/components/Button.js
index f56049e..5b0365b 100644
--- a/src/components/Button.js
+++ b/src/components/Button.js
@@ -1,4 +1,5 @@
import React from 'react';
+import cx from 'classnames';
import s0 from 'c/Button.module.css';
const noop = () => {};
@@ -13,6 +14,14 @@ function Button({ children, label, onClick = noop }, ref) {
);
}
+export function ButtonPlain({ children, label, onClick = noop }) {
+ return (
+ <button className={cx(s0.btn, s0.plain)} onClick={onClick}>
+ {children || label}
+ </button>
+ );
+}
+
function WithIcon({ text, icon, onClick = noop }, ref) {
return (
<button className={s0.btn} ref={ref} onClick={onClick}>
diff --git a/src/components/Button.module.css b/src/components/Button.module.css
index 205bfe9..c232a66 100644
--- a/src/components/Button.module.css
+++ b/src/components/Button.module.css
@@ -24,6 +24,22 @@
font-size: 1em;
padding: 6px 12px;
}
+
+ &.plain {
+ border-radius: 100%;
+ padding: 0;
+ display: flex;
+ border-color: transparent;
+ background: none;
+ &:focus {
+ border-color: var(--color-focus-blue);
+ }
+ &:hover {
+ background: #387cec;
+ border: 1px solid #387cec;
+ color: #fff;
+ }
+ }
}
.withIconWrapper {
diff --git a/src/components/Proxies.js b/src/components/Proxies.js
index fef6a14..0020114 100644
--- a/src/components/Proxies.js
+++ b/src/components/Proxies.js
@@ -1,63 +1,81 @@
import React from 'react';
-import { useActions, useStoreState } from 'm/store';
+import { useStoreState } from 'm/store';
-import ContentHeader from 'c/ContentHeader';
-import ProxyGroup from 'c/ProxyGroup';
-import { ButtonWithIcon } from 'c/Button';
+import { connect } from './StateProvider';
+
+import ContentHeader from './ContentHeader';
+import ProxyGroup from './ProxyGroup';
+import { ButtonWithIcon } from './Button';
import { Zap } from 'react-feather';
-import s0 from 'c/Proxies.module.css';
+import ProxyProviderList from './ProxyProviderList';
+
+import s0 from './Proxies.module.css';
import {
getProxies,
+ getDelay,
getProxyGroupNames,
+ getProxyProviders,
fetchProxies,
requestDelayAll
-} from 'd/proxies';
+} from '../store/proxies';
+
+import { getClashAPIConfig } from '../ducks/app';
-const { useEffect, useMemo } = React;
+const { useEffect, useMemo, useCallback } = React;
const mapStateToProps = s => ({
- proxies: getProxies(s),
- groupNames: getProxyGroupNames(s)
+ apiConfig: getClashAPIConfig(s)
});
-const actions = {
- fetchProxies,
- requestDelayAll
-};
-
-export default function Proxies() {
- const { fetchProxies, requestDelayAll } = useActions(actions);
+function Proxies({ dispatch, groupNames, proxies, delay, proxyProviders }) {
+ const { apiConfig } = useStoreState(mapStateToProps);
useEffect(() => {
- (async () => {
- await fetchProxies();
- // await requestDelayAll();
- })();
- }, [fetchProxies, requestDelayAll]);
- const { groupNames } = useStoreState(mapStateToProps);
+ dispatch(fetchProxies(apiConfig));
+ }, [dispatch, apiConfig]);
+ const requestDelayAllFn = useCallback(
+ () => dispatch(requestDelayAll(apiConfig)),
+ [apiConfig, dispatch]
+ );
const icon = useMemo(() => <Zap width={16} />, []);
return (
<>
<ContentHeader title="Proxies" />
- <div className={s0.body}>
+ <div>
<div className="fabgrp">
<ButtonWithIcon
text="Test Latency"
icon={icon}
- onClick={requestDelayAll}
+ onClick={requestDelayAllFn}
/>
- {/* <Button onClick={requestDelayAll}>Test Latency</Button> */}
</div>
{groupNames.map(groupName => {
return (
<div className={s0.group} key={groupName}>
- <ProxyGroup name={groupName} />
+ <ProxyGroup
+ name={groupName}
+ proxies={proxies}
+ delay={delay}
+ apiConfig={apiConfig}
+ dispatch={dispatch}
+ />
</div>
);
})}
</div>
+ <ProxyProviderList items={proxyProviders} />
+ <div style={{ height: 60 }} />
</>
);
}
+
+const mapState = s => ({
+ groupNames: getProxyGroupNames(s),
+ proxies: getProxies(s),
+ proxyProviders: getProxyProviders(s),
+ delay: getDelay(s)
+});
+
+export default connect(mapState)(Proxies);
diff --git a/src/components/Proxies.module.css b/src/components/Proxies.module.css
index 72b70fb..5520a2e 100644
--- a/src/components/Proxies.module.css
+++ b/src/components/Proxies.module.css
@@ -1,7 +1,3 @@
-.body {
- padding-bottom: 50px;
-}
-
.group {
padding: 10px 15px;
@media (--breakpoint-not-small) {
diff --git a/src/components/Proxy.js b/src/components/Proxy.js
index b7efc84..117ff76 100644
--- a/src/components/Proxy.js
+++ b/src/components/Proxy.js
@@ -1,13 +1,36 @@
import React from 'react';
-import PropTypes from 'prop-types';
import cx from 'classnames';
-import { useStoreState } from 'm/store';
-import ProxyLatency from 'c/ProxyLatency';
+import { connect } from './StateProvider';
+import ProxyLatency from './ProxyLatency';
+
+import { getProxies, getDelay } from '../store/proxies';
import s0 from './Proxy.module.css';
-import { getDelay, getProxies } from 'd/proxies';
+const { useMemo } = React;
+
+const colorMap = {
+ // green
+ good: '#67c23a',
+ // yellow
+ normal: '#d4b75c',
+ // orange
+ bad: '#e67f3c',
+ // bad: '#F56C6C',
+ na: '#909399'
+};
+
+function getLabelColor({ number, error } = {}) {
+ if (number < 200) {
+ return colorMap.good;
+ } else if (number < 400) {
+ return colorMap.normal;
+ } else if (typeof number === 'number') {
+ return colorMap.bad;
+ }
+ return colorMap.na;
+}
/*
const colors = {
@@ -22,18 +45,28 @@ const colors = {
};
*/
-const mapStateToProps = s => {
- return {
- proxies: getProxies(s),
- delay: getDelay(s)
- };
+type ProxyProps = {
+ name: string,
+ now?: boolean,
+
+ // connect injected
+ // TODO refine type
+ proxy: any,
+ latency: any
};
-function Proxy({ now, name }) {
- const { proxies, delay } = useStoreState(mapStateToProps);
- const latency = delay[name];
- const proxy = proxies[name];
+function ProxySmallImpl({ now, name, proxy, latency }: ProxyProps) {
+ const color = useMemo(() => getLabelColor(latency), [latency]);
+ return (
+ <div
+ className={cx(s0.proxySmall, { [s0.now]: now })}
+ style={{ backgroundColor: color }}
+ />
+ );
+}
+function Proxy({ now, name, proxy, latency }: ProxyProps) {
+ const color = useMemo(() => getLabelColor(latency), [latency]);
return (
<div
className={cx(s0.proxy, {
@@ -46,14 +79,22 @@ function Proxy({ now, name }) {
{proxy.type}
</div>
<div className={s0.proxyLatencyWrap}>
- {latency && latency.number ? <ProxyLatency latency={latency} /> : null}
+ {latency && latency.number ? (
+ <ProxyLatency number={latency.number} color={color} />
+ ) : null}
</div>
</div>
);
}
-Proxy.propTypes = {
- now: PropTypes.bool,
- name: PropTypes.string
+
+const mapState = (s, { name }) => {
+ const proxies = getProxies(s);
+ const delay = getDelay(s);
+ return {
+ proxy: proxies[name],
+ latency: delay[name]
+ };
};
-export default Proxy;
+export default connect(mapState)(Proxy);
+export const ProxySmall = connect(mapState)(ProxySmallImpl);
diff --git a/src/components/Proxy.module.css b/src/components/Proxy.module.css
index 6f42ccf..2af1ce8 100644
--- a/src/components/Proxy.module.css
+++ b/src/components/Proxy.module.css
@@ -2,10 +2,15 @@
position: relative;
padding: 5px;
border-radius: 8px;
+ overflow: hidden;
+
+ max-width: 280px;
@media (--breakpoint-not-small) {
+ min-width: 150px;
border-radius: 10px;
padding: 10px;
}
+
background-color: var(--color-bg-proxy-selected);
&.now {
background-color: var(--color-focus-blue);
@@ -40,3 +45,12 @@
display: flex;
align-items: flex-end;
}
+
+.proxySmall {
+ .now {
+ outline: pink solid 1px;
+ }
+ width: 12px;
+ height: 12px;
+ border-radius: 8px;
+}
diff --git a/src/components/ProxyGroup.js b/src/components/ProxyGroup.js
index 337b09b..d824920 100644
--- a/src/components/ProxyGroup.js
+++ b/src/components/ProxyGroup.js
@@ -1,60 +1,108 @@
import React from 'react';
-import PropTypes from 'prop-types';
import cx from 'classnames';
-import { useActions, useStoreState } from 'm/store';
-import Proxy from 'c/Proxy';
+import Proxy, { ProxySmall } from './Proxy';
+import { SectionNameType } from './shared/Basic';
import s0 from './ProxyGroup.module.css';
-import { getProxies, switchProxy } from 'd/proxies';
+import { switchProxy } from '../store/proxies';
-const mapStateToProps = s => ({
- proxies: getProxies(s)
-});
+const { memo, useCallback, useMemo } = React;
-export default function ProxyGroup({ name }) {
- const { proxies } = useStoreState(mapStateToProps);
- const actions = useActions({ switchProxy });
+function ProxyGroup({ name, proxies, apiConfig, dispatch }) {
const group = proxies[name];
- const { all } = group;
+ const { all, type, now } = group;
+
+ const isSelectable = useMemo(() => type === 'Selector', [type]);
+
+ const itemOnTapCallback = useCallback(
+ proxyName => {
+ if (!isSelectable) return;
+
+ dispatch(switchProxy(apiConfig, name, proxyName));
+ // switchProxyFn(name, proxyName);
+ },
+ [apiConfig, dispatch, name, isSelectable]
+ );
return (
<div className={s0.group}>
<div className={s0.header}>
- <h2>
- <span>{name}</span>
- <span>{group.type}</span>
- </h2>
- </div>
- <div className={s0.list}>
- {all.map(proxyName => {
- const isSelectable = group.type === 'Selector';
- const proxyClassName = cx(s0.proxy, {
- [s0.proxySelectable]: isSelectable
- });
- return (
- <div
- className={proxyClassName}
- key={proxyName}
- onClick={() => {
- if (!isSelectable) return;
- actions.switchProxy(name, proxyName);
- }}
- >
- <Proxy
- isSelectable={isSelectable}
- name={proxyName}
- now={proxyName === group.now}
- />
- </div>
- );
- })}
+ <SectionNameType name={name} type={group.type} />
</div>
+ <ProxyList
+ all={all}
+ now={now}
+ isSelectable={isSelectable}
+ itemOnTapCallback={itemOnTapCallback}
+ />
</div>
);
}
-ProxyGroup.propTypes = {
- name: PropTypes.string
+type ProxyListProps = {
+ all: string[],
+ now?: string,
+ isSelectable?: boolean,
+ itemOnTapCallback?: string => void
};
+export function ProxyList({
+ all,
+ now,
+ isSelectable,
+ itemOnTapCallback
+}: ProxyListProps) {
+ return (
+ <div className={s0.list}>
+ {all.map(proxyName => {
+ const proxyClassName = cx(s0.proxy, {
+ [s0.proxySelectable]: isSelectable
+ });
+ return (
+ <div
+ className={proxyClassName}
+ key={proxyName}
+ onClick={() => {
+ if (!isSelectable || !itemOnTapCallback) return;
+ itemOnTapCallback(proxyName);
+ }}
+ >
+ <Proxy name={proxyName} now={proxyName === now} />
+ </div>
+ );
+ })}
+ </div>
+ );
+}
+
+export function ProxyListSummaryView({
+ all,
+ now,
+ isSelectable,
+ itemOnTapCallback
+}: ProxyListProps) {
+ return (
+ <div className={s0.list}>
+ {all.map(proxyName => {
+ const proxyClassName = cx(s0.proxy, {
+ [s0.proxySelectable]: isSelectable
+ });
+ return (
+ <div
+ className={proxyClassName}
+ key={proxyName}
+ onClick={() => {
+ if (!isSelectable || !itemOnTapCallback) return;
+ itemOnTapCallback(proxyName);
+ }}
+ >
+ <ProxySmall name={proxyName} now={proxyName === now} />
+ </div>
+ );
+ })}
+ </div>
+ );
+}
+
+export default memo(ProxyGroup);
diff --git a/src/components/ProxyGroup.module.css b/src/components/ProxyGroup.module.css
index 08c4b42..748aa67 100644
--- a/src/components/ProxyGroup.module.css
+++ b/src/components/ProxyGroup.module.css
@@ -1,32 +1,19 @@
.header {
- > h2 {
- margin-top: 0;
-
- font-size: 1.3em;
- @media (--breakpoint-not-small) {
- font-size: 1.5em;
- }
-
- span:nth-child(2) {
- font-size: 12px;
- color: #777;
- font-weight: normal;
- margin: 0 0.3em;
- }
- }
+ margin-bottom: 12px;
}
.list {
display: flex;
flex-wrap: wrap;
+ margin-top: 8px;
}
.proxy {
- max-width: 280px;
- margin: 2px;
+ margin-right: 5px;
+ margin-bottom: 5px;
@media (--breakpoint-not-small) {
- min-width: 150px;
- margin: 10px;
+ margin-right: 10px;
+ margin-bottom: 10px;
}
transition: transform 0.2s ease-in-out;
diff --git a/src/components/ProxyLatency.js b/src/components/ProxyLatency.js
index 33a94f5..5cde880 100644
--- a/src/components/ProxyLatency.js
+++ b/src/components/ProxyLatency.js
@@ -1,39 +1,16 @@
-import React, { useMemo } from 'react';
-import PropTypes from 'prop-types';
+import React from 'react';
import s0 from './ProxyLatency.module.css';
-const colorMap = {
- good: '#67C23A',
- normal: '#E6A23C',
- bad: '#F56C6C',
- na: '#909399'
+type ProxyLatencyProps = {
+ number: number,
+ color: string
};
-function getLabelColor(number, error) {
- if (error !== '') {
- return colorMap.na;
- } else if (number < 200) {
- return colorMap.good;
- } else if (number < 400) {
- return colorMap.normal;
- }
- return colorMap.bad;
-}
-
-export default function ProxyLatency({ latency }) {
- const { number, error } = latency;
- const color = useMemo(() => getLabelColor(number, error), [number, error]);
+export default function ProxyLatency({ number, color }: ProxyLatencyProps) {
return (
<span className={s0.proxyLatency} style={{ color }}>
- {error !== '' ? <span>{error}</span> : <span>{number} ms</span>}
+ <span>{number} ms</span>
</span>
);
}
-
-ProxyLatency.propTypes = {
- latency: PropTypes.shape({
- number: PropTypes.number,
- error: PropTypes.string
- })
-};
diff --git a/src/components/ProxyProvider.js b/src/components/ProxyProvider.js
new file mode 100644
index 0000000..e18fe17
--- /dev/null
+++ b/src/components/ProxyProvider.js
@@ -0,0 +1,211 @@
+import React from 'react';
+import { ChevronDown, RotateCw } from 'react-feather';
+import { formatDistance } from 'date-fns';
+import ResizeObserver from 'resize-observer-polyfill';
+import { motion } from 'framer-motion';
+import cx from 'classnames';
+
+import { useStoreState } from '../misc/store';
+import { getClashAPIConfig } from '../ducks/app';
+import { connect } from './StateProvider';
+import { SectionNameType } from './shared/Basic';
+import { ProxyList, ProxyListSummaryView } from './ProxyGroup';
+import { ButtonWithIcon, ButtonPlain } from './Button';
+
+import { updateProviderByName } from '../store/proxies';
+
+import s from './ProxyProvider.module.css';
+
+const { memo, useState, useRef, useEffect, useCallback } = React;
+
+type Props = {
+ item: Array<{
+ name: string,
+ proxies: Array<string>,
+ type: 'Proxy' | 'Rule',
+ vehicleType: 'HTTP' | 'File' | 'Compatible',
+ updatedAt?: string
+ }>,
+ proxies: {
+ [string]: any
+ },
+ dispatch: any => void
+};
+
+const mapStateToProps = s => ({
+ apiConfig: getClashAPIConfig(s)
+});
+
+function ProxyProvider({ item, dispatch }: Props) {
+ const { apiConfig } = useStoreState(mapStateToProps);
+ const updateProvider = useCallback(
+ () => dispatch(updateProviderByName(apiConfig, item.name)),
+ [apiConfig, dispatch, item.name]
+ );
+
+ const [isCollapsibleOpen, setCollapsibleOpen] = useState(false);
+ const toggle = useCallback(() => setCollapsibleOpen(x => !x), []);
+ const timeAgo = formatDistance(new Date(item.updatedAt), new Date());
+ return (
+ <div className={s.body}>
+ <div className={s.header} onClick={toggle}>
+ <SectionNameType name={item.name} type={item.vehicleType} />
+ <ButtonPlain>
+ <span className={cx(s.arrow, { [s.isOpen]: isCollapsibleOpen })}>
+ <ChevronDown />
+ </span>
+ </ButtonPlain>
+ </div>
+ <div className={s.updatedAt}>
+ <small>Updated {timeAgo} ago</small>
+ </div>
+ <Collapsible2 isOpen={isCollapsibleOpen}>
+ <ProxyList all={item.proxies} />
+ <div className={s.actionFooter}>
+ <ButtonWithIcon
+ text="Update"
+ icon={<Refresh />}
+ onClick={updateProvider}
+ />
+ </div>
+ </Collapsible2>
+ <Collapsible2 isOpen={!isCollapsibleOpen}>
+ <ProxyListSummaryView all={item.proxies} />
+ </Collapsible2>
+ </div>
+ );
+}
+
+const button = {
+ rest: { scale: 1 },
+ // hover: { scale: 1.1 },
+ pressed: { scale: 0.95 }
+};
+const arrow = {
+ rest: { rotate: 0 },
+ hover: { rotate: 360, transition: { duration: 0.3 } }
+};
+function Refresh() {
+ return (
+ <motion.div
+ className={s.refresh}
+ variants={button}
+ initial="rest"
+ whileHover="hover"
+ whileTap="pressed"
+ >
+ <motion.div className="flexCenter" variants={arrow}>
+ <RotateCw size={16} />
+ </motion.div>
+ </motion.div>
+ );
+}
+
+function usePrevious(value) {
+ const ref = useRef();
+ useEffect(() => void (ref.current = value), [value]);
+ return ref.current;
+}
+
+function useMeasure() {
+ const ref = useRef();
+ const [bounds, set] = useState({ height: 0 });
+ useEffect(() => {
+ const ro = new ResizeObserver(([entry]) => set(entry.contentRect));
+ if (ref.current) ro.observe(ref.current);
+ return () => ro.disconnect();
+ }, []);
+ return [ref, bounds];
+}
+
+// import { useSpring, a } from 'react-spring';
+// const Collapsible = memo(({ children, isOpen }) => {
+// const previous = usePrevious(isOpen);
+// const [refToMeature, { height: viewHeight }] = useMeasure();
+// const { height, opacity, visibility, transform } = useSpring({
+// from: {
+// height: 0,
+// opacity: 0,
+// transform: 'translate3d(20px,0,0)',
+// visibility: 'hidden'
+// },
+// to: {
+// height: isOpen ? viewHeight : 0,
+// opacity: isOpen ? 1 : 0,
+// visibility: isOpen ? 'visible' : 'hidden',
+// transform: `translate3d(${isOpen ? 0 : 20}px,0,0)`
+// }
+// });
+// return (
+// <div>
+// <a.div
+// style={{
+// opacity,
+// willChange: 'transform, opacity, height, visibility',
+// visibility,
+// height: isOpen && previous === isOpen ? 'auto' : height
+// }}>
+// <a.div style={{ transform }} ref={refToMeature} children={children} />
+// </a.div>
+// </div>
+// );
+// });
+
+const variantsCollpapsibleWrap = {
+ initialOpen: {
+ height: 'auto',
+ transition: { duration: 0 }
+ },
+ open: height => ({
+ height,
+ opacity: 1,
+ visibility: 'visible',
+ transition: { duration: 0.3 }
+ }),
+ closed: {
+ height: 0,
+ opacity: 0,
+ visibility: 'hidden',
+ transition: { duration: 0.3 }
+ }
+};
+const variantsCollpapsibleChildContainer = {
+ open: {
+ x: 0
+ },
+ closed: {
+ x: 20
+ }
+};
+
+const Collapsible2 = memo(({ children, isOpen }) => {
+ const previous = usePrevious(isOpen);
+ const [refToMeature, { height }] = useMeasure();
+ return (
+ <div>
+ <motion.div
+ animate={
+ isOpen && previous === isOpen
+ ? 'initialOpen'
+ : isOpen
+ ? 'open'
+ : 'closed'
+ }
+ custom={height}
+ variants={variantsCollpapsibleWrap}
+ >
+ <motion.div
+ variants={variantsCollpapsibleChildContainer}
+ ref={refToMeature}
+ >
+ {children}
+ </motion.div>
+ </motion.div>
+ </div>
+ );
+});
+
+const mapState = s => ({
+ // proxies: getProxies(s)
+});
+export default connect(mapState)(ProxyProvider);
diff --git a/src/components/ProxyProvider.module.css b/src/components/ProxyProvider.module.css
new file mode 100644
index 0000000..6668f67
--- /dev/null
+++ b/src/components/ProxyProvider.module.css
@@ -0,0 +1,43 @@
+.header {
+ display: flex;
+ align-items: center;
+ cursor: pointer;
+
+ .arrow {
+ display: inline-flex;
+ transform: rotate(0deg);
+ transition: transform 0.3s;
+ &.isOpen {
+ transform: rotate(180deg);
+ }
+
+ &:focus {
+ outline: var(--color-focus-blue) solid 1px;
+ }
+ }
+}
+
+.updatedAt {
+ margin-bottom: 12px;
+ small {
+ color: #777;
+ }
+}
+
+.body {
+ padding: 10px 15px;
+ @media (--breakpoint-not-small) {
+ padding: 10px 40px;
+ }
+}
+
+.actionFooter {
+ display: flex;
+}
+
+.refresh {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ cursor: pointer;
+}
diff --git a/src/components/ProxyProviderList.js b/src/components/ProxyProviderList.js
new file mode 100644
index 0000000..2ae0fce
--- /dev/null
+++ b/src/components/ProxyProviderList.js
@@ -0,0 +1,19 @@
+import React from 'react';
+
+import ContentHeader from './ContentHeader';
+import ProxyProvider from './ProxyProvider';
+
+function ProxyProviderList({ items }) {
+ return (
+ <>
+ <ContentHeader title="Proxy Provider" />
+ <div>
+ {items.map(item => (
+ <ProxyProvider key={item.name} item={item} />
+ ))}
+ </div>
+ </>
+ );
+}
+
+export default ProxyProviderList;
diff --git a/src/components/Root.css b/src/components/Root.css
index d611a02..3622d57 100644
--- a/src/components/Root.css
+++ b/src/components/Root.css
@@ -10,6 +10,13 @@
U+FEFF, U+FFFD;
}
+.relative {
+ position: relative;
+}
+/* .absolute { */
+/* position: absolute; */
+/* } */
+
.border-left,
.border-top,
.border-bottom {
@@ -113,6 +120,12 @@ body.light {
--bg-modal: #fbfbfb;
}
+.flexCenter {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
/* TODO remove fabgrp in component css files */
.fabgrp {
position: fixed;
diff --git a/src/components/Root.js b/src/components/Root.js
index 56c0be0..4147fd2 100644
--- a/src/components/Root.js
+++ b/src/components/Root.js
@@ -1,5 +1,6 @@
import React, { Suspense } from 'react';
-import { Provider } from 'm/store';
+import { Provider } from '../misc/store';
+import StateProvider from './StateProvider';
import { HashRouter as Router, Route } from 'react-router-dom';
import { hot } from 'react-hot-loader/root';
import Loading2 from 'c/Loading2';
@@ -36,30 +37,38 @@ const Rules = React.lazy(() =>
window.store = store;
+const initialState = {
+ proxies: {
+ proxies: {},
+ delay: {},
+ groupNames: []
+ }
+};
+
const Root = () => (
<ErrorBoundary>
- <Provider store={store}>
- <Router>
- <div className={s0.app}>
- <APIDiscovery />
- <Route path="/" render={props => <SideBar {...props} />} />
- <div className={s0.content}>
- <Suspense fallback={<Loading2 />}>
- <Route exact path="/" render={() => <Home />} />
- <Route exact path="/connections" component={Connections} />
- <Route exact path="/overview" render={() => <Home />} />
- <Route exact path="/configs" component={Config} />
- <Route exact path="/logs" component={Logs} />
- <Route exact path="/proxies" render={() => <Proxies />} />
- <Route exact path="/rules" render={() => <Rules />} />
- </Suspense>
+ <StateProvider initialState={initialState}>
+ <Provider store={store}>
+ <Router>
+ <div className={s0.app}>
+ <APIDiscovery />
+ <Route path="/" render={props => <SideBar {...props} />} />
+ <div className={s0.content}>
+ <Suspense fallback={<Loading2 />}>
+ <Route exact path="/" render={() => <Home />} />
+ <Route exact path="/connections" component={Connections} />
+ <Route exact path="/overview" render={() => <Home />} />
+ <Route exact path="/configs" component={Config} />
+ <Route exact path="/logs" component={Logs} />
+ <Route exact path="/proxies" render={() => <Proxies />} />
+ <Route exact path="/rules" render={() => <Rules />} />
+ </Suspense>
+ </div>
</div>
- </div>
- </Router>
- </Provider>
+ </Router>
+ </Provider>
+ </StateProvider>
</ErrorBoundary>
);
-// <Route exact path="/__0" render={() => <StyleGuide />} />
-// <Route exact path="/__1" component={Loading} />
export default hot(Root);
diff --git a/src/components/StateProvider.js b/src/components/StateProvider.js
new file mode 100644
index 0000000..adb1b24
--- /dev/null
+++ b/src/components/StateProvider.js
@@ -0,0 +1,79 @@
+import React from 'react';
+import produce, * as immer from 'immer';
+
+const {
+ createContext,
+ memo,
+ useRef,
+ useEffect,
+ useCallback,
+ useContext,
+ useState
+} = React;
+
+const StateContext = createContext(null);
+const DispatchContext = createContext(null);
+
+export { immer };
+
+export function useStoreState() {
+ return useContext(StateContext);
+}
+
+export function useStoreDispatch() {
+ return useContext(DispatchContext);
+}
+
+export default function Provider({ initialState, children }) {
+ const stateRef = useRef(initialState);
+ const [state, setState] = useState(initialState);
+ const getState = useCallback(() => stateRef.current, []);
+ useEffect(() => {
+ if (process.env.NODE_ENV === 'development') {
+ window.getState2 = getState;
+ }
+ }, [getState]);
+ const dispatch = useCallback(
+ (actionId, fn, thunk) => {
+ // if (thunk) return thunk(dispatch, getState);
+ if (typeof actionId === 'function') return actionId(dispatch, getState);
+
+ const stateNext = produce(getState(), fn);
+ if (stateNext !== stateRef.current) {
+ if (process.env.NODE_ENV === 'development') {
+ // eslint-disable-next-line no-console
+ console.log(actionId, stateNext);
+ }
+ stateRef.current = stateNext;
+ setState(stateNext);
+ }
+ },
+ [getState]
+ );
+
+ return (
+ <StateContext.Provider value={state}>
+ <DispatchContext.Provider value={dispatch}>
+ {children}
+ </DispatchContext.Provider>
+ </StateContext.Provider>
+ );
+}
+
+export function connect(mapStateToProps) {
+ return Component => {
+ const MemoComponent = memo(Component);
+ function Connected(props) {
+ const state = useContext(StateContext);
+ const dispatch = useContext(DispatchContext);
+ const mapped = mapStateToProps(state, props);
+ const nextProps = {
+ ...props,
+ ...mapped,
+ dispatch
+ };
+ return <MemoComponent {...nextProps} />;
+ }
+ return Connected;
+ };
+}
diff --git a/src/components/shared/Basic.js b/src/components/shared/Basic.js
new file mode 100644
index 0000000..9d07a39
--- /dev/null
+++ b/src/components/shared/Basic.js
@@ -0,0 +1,12 @@
+import React from 'react';
+
+import s from './Basic.module.css';
+
+export function SectionNameType({ name, type }) {
+ return (
+ <h2 className={s.sectionNameType}>
+ <span>{name}</span>
+ <span>{type}</span>
+ </h2>
+ );
+}
diff --git a/src/components/shared/Basic.module.css b/src/components/shared/Basic.module.css
new file mode 100644
index 0000000..8e3e65c
--- /dev/null
+++ b/src/components/shared/Basic.module.css
@@ -0,0 +1,14 @@
+h2.sectionNameType {
+ margin: 0;
+ font-size: 1.3em;
+ @media (--breakpoint-not-small) {
+ font-size: 1.5em;
+ }
+
+ span:nth-child(2) {
+ font-size: 12px;
+ color: #777;
+ font-weight: normal;
+ margin: 0 0.3em;
+ }
+}