summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/components/APIConfig.module.css2
-rw-r--r--src/components/APIConfig.tsx24
-rw-r--r--src/components/APIDiscovery.module.css2
-rw-r--r--src/components/BackendList.module.css99
-rw-r--r--src/components/BackendList.tsx147
-rw-r--r--src/components/Config.js8
-rw-r--r--src/components/ErrorBoundary.js40
-rw-r--r--src/components/Root.js58
-rw-r--r--src/components/TrafficChart.js3
-rw-r--r--src/store/app.js108
-rw-r--r--src/store/configs.js33
-rw-r--r--src/store/index.js4
12 files changed, 412 insertions, 116 deletions
diff --git a/src/components/APIConfig.module.css b/src/components/APIConfig.module.css
index 1092aed..afc2d53 100644
--- a/src/components/APIConfig.module.css
+++ b/src/components/APIConfig.module.css
@@ -20,7 +20,7 @@
}
.body {
- padding: 30px 0 0;
+ padding: 15px 0 0;
}
.hostnamePort {
diff --git a/src/components/APIConfig.tsx b/src/components/APIConfig.tsx
index 88bec8b..3befa5c 100644
--- a/src/components/APIConfig.tsx
+++ b/src/components/APIConfig.tsx
@@ -1,8 +1,9 @@
import * as React from 'react';
import { fetchConfigs } from 'src/api/configs';
+import { BackendList } from 'src/components/BackendList';
import { ClashAPIConfig } from 'src/types';
-import { getClashAPIConfig, updateClashAPIConfig } from '../store/app';
+import { addClashAPIConfig, getClashAPIConfig } from '../store/app';
import s0 from './APIConfig.module.css';
import Button from './Button';
import Field from './Field';
@@ -16,9 +17,9 @@ const mapState = (s) => ({
apiConfig: getClashAPIConfig(s),
});
-function APIConfig({ apiConfig, dispatch }) {
- const [baseURL, setBaseURL] = useState(apiConfig.baseURL);
- const [secret, setSecret] = useState(apiConfig.secret);
+function APIConfig({ dispatch }) {
+ const [baseURL, setBaseURL] = useState('');
+ const [secret, setSecret] = useState('');
const [errMsg, setErrMsg] = useState('');
const userTouchedFlagRef = useRef(false);
@@ -47,14 +48,21 @@ function APIConfig({ apiConfig, dispatch }) {
if (ret[0] !== Ok) {
setErrMsg(ret[1]);
} else {
- dispatch(updateClashAPIConfig({ baseURL, secret }));
+ dispatch(addClashAPIConfig({ baseURL, secret }));
}
});
}, [baseURL, secret, dispatch]);
const handleContentOnKeyDown = useCallback(
- (e) => {
+ (e: React.KeyboardEvent<HTMLInputElement>) => {
+ if (
+ e.target instanceof Element &&
+ (!e.target.tagName || e.target.tagName.toUpperCase() !== 'INPUT')
+ ) {
+ return;
+ }
if (e.key !== 'Enter') return;
+
onConfirm();
},
[onConfirm]
@@ -90,8 +98,10 @@ function APIConfig({ apiConfig, dispatch }) {
</div>
<div className={s0.error}>{errMsg ? errMsg : null}</div>
<div className={s0.footer}>
- <Button label="Confirm" onClick={onConfirm} />
+ <Button label="Add" onClick={onConfirm} />
</div>
+ <div style={{ height: 20 }} />
+ <BackendList />
</div>
);
}
diff --git a/src/components/APIDiscovery.module.css b/src/components/APIDiscovery.module.css
index f2aaf71..6c1295a 100644
--- a/src/components/APIDiscovery.module.css
+++ b/src/components/APIDiscovery.module.css
@@ -12,11 +12,11 @@
display: flex;
justify-content: center;
+ overflow-y: auto;
}
.container {
position: relative;
- top: 10%;
margin-left: 20px;
margin-right: 20px;
}
diff --git a/src/components/BackendList.module.css b/src/components/BackendList.module.css
new file mode 100644
index 0000000..1de1972
--- /dev/null
+++ b/src/components/BackendList.module.css
@@ -0,0 +1,99 @@
+.ul {
+ position: relative;
+ margin: 0;
+ padding: 0;
+ list-style: none;
+ line-height: 1.8;
+
+ --width-max-content: 230px;
+}
+
+.li {
+ position: relative;
+ margin: 5px 0;
+ padding: 10px 0;
+ border-radius: 10px;
+ display: grid;
+ place-content: center;
+ grid-template-columns: 40px 1fr 40px;
+ grid-template-rows: 30px;
+ grid-template-areas: 'close url .';
+ column-gap: 10px;
+}
+
+.li:hover {
+ background-color: var(--bg-near-transparent);
+}
+
+.close {
+ opacity: 0;
+ grid-area: close;
+ place-self: center;
+}
+
+.li:hover .close,
+.li:hover .eye {
+ opacity: 1;
+}
+.close:focus,
+.eye:focus {
+ opacity: 1;
+}
+
+.hasSecret {
+ grid-template-rows: repeat(2, 30px);
+ grid-template-areas:
+ 'close url .'
+ 'close secret eye';
+}
+
+.url {
+ grid-area: url;
+}
+.secret {
+ grid-area: secret;
+}
+.eye {
+ grid-area: eye;
+ opacity: 0;
+ place-self: center;
+ cursor: pointer;
+}
+
+.url,
+.secret {
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.btn {
+ outline: none;
+ appearance: none;
+ border: 1px solid transparent;
+ background-color: transparent;
+ color: inherit;
+ display: flex;
+ align-items: center;
+ padding: 5px;
+ border-radius: 100px;
+}
+.btn:focus {
+ border-color: var(--color-focus-blue);
+}
+.btn:hover:enabled {
+ background-color: var(--color-focus-blue);
+}
+.btn:active:enabled {
+ transform: scale(0.97);
+}
+.btn:disabled {
+ color: var(--color-text-secondary);
+}
+
+.url {
+ cursor: pointer;
+}
+.url:hover {
+ color: var(--color-text-highlight);
+}
diff --git a/src/components/BackendList.tsx b/src/components/BackendList.tsx
new file mode 100644
index 0000000..a0c993f
--- /dev/null
+++ b/src/components/BackendList.tsx
@@ -0,0 +1,147 @@
+import cx from 'clsx';
+import * as React from 'react';
+import { Eye, EyeOff, X as Close } from 'react-feather';
+import { useToggle } from 'src/hooks/basic';
+import {
+ getClashAPIConfigs,
+ getSelectedClashAPIConfigIndex,
+} from 'src/store/app';
+import { ClashAPIConfig } from 'src/types';
+
+import s from './BackendList.module.css';
+import { connect, useStoreActions } from './StateProvider';
+
+type Config = ClashAPIConfig & { addedAt: number };
+
+const mapState = (s) => ({
+ apiConfigs: getClashAPIConfigs(s),
+ selectedClashAPIConfigIndex: getSelectedClashAPIConfigIndex(s),
+});
+
+export const BackendList = connect(mapState)(BackendListImpl);
+
+function BackendListImpl({
+ apiConfigs,
+ selectedClashAPIConfigIndex,
+}: {
+ apiConfigs: Config[];
+ selectedClashAPIConfigIndex: number;
+}) {
+ const {
+ app: { removeClashAPIConfig, selectClashAPIConfig },
+ } = useStoreActions();
+
+ const onRemove = React.useCallback(
+ (conf: ClashAPIConfig) => {
+ removeClashAPIConfig(conf);
+ },
+ [removeClashAPIConfig]
+ );
+ const onSelect = React.useCallback(
+ (conf: ClashAPIConfig) => {
+ selectClashAPIConfig(conf);
+ },
+ [selectClashAPIConfig]
+ );
+
+ return (
+ <>
+ <ul className={s.ul}>
+ {apiConfigs.map((item, idx) => {
+ return (
+ <li
+ className={cx(s.li, {
+ [s.hasSecret]: item.secret,
+ [s.isSelected]: idx === selectedClashAPIConfigIndex,
+ })}
+ key={item.baseURL + item.secret}
+ >
+ <Item
+ disableRemove={idx === selectedClashAPIConfigIndex}
+ baseURL={item.baseURL}
+ secret={item.secret}
+ onRemove={onRemove}
+ onSelect={onSelect}
+ />
+ </li>
+ );
+ })}
+ </ul>
+ </>
+ );
+}
+
+function Item({
+ baseURL,
+ secret,
+ disableRemove,
+ onRemove,
+ onSelect,
+}: {
+ baseURL: string;
+ secret: string;
+ disableRemove: boolean;
+ onRemove: (x: ClashAPIConfig) => void;
+ onSelect: (x: ClashAPIConfig) => void;
+}) {
+ const [show, toggle] = useToggle();
+ const Icon = show ? EyeOff : Eye;
+
+ const handleTap = React.useCallback((e: React.KeyboardEvent) => {
+ e.stopPropagation();
+ }, []);
+
+ return (
+ <>
+ <Button
+ disabled={disableRemove}
+ onClick={() => onRemove({ baseURL, secret })}
+ className={s.close}
+ >
+ <Close size={20} />
+ </Button>
+ <span
+ className={s.url}
+ tabIndex={0}
+ role="button"
+ onClick={() => onSelect({ baseURL, secret })}
+ onKeyUp={handleTap}
+ >
+ {baseURL}
+ </span>
+ <span />
+ {secret ? (
+ <>
+ <span className={s.secret}>{show ? secret : '***'}</span>
+
+ <Button onClick={toggle} className={s.eye}>
+ <Icon size={20} />
+ </Button>
+ </>
+ ) : null}
+ </>
+ );
+}
+
+function Button({
+ children,
+ onClick,
+ className,
+ disabled,
+}: {
+ children: React.ReactNode;
+
+ onClick?: (e: React.MouseEvent<HTMLButtonElement>) => unknown;
+ className: string;
+ disabled?: boolean;
+}) {
+ return (
+ <button
+ disabled={disabled}
+ className={cx(className, s.btn)}
+ onClick={onClick}
+ >
+ {children}
+ </button>
+ );
+}
diff --git a/src/components/Config.js b/src/components/Config.js
index d5850d1..f14f7d9 100644
--- a/src/components/Config.js
+++ b/src/components/Config.js
@@ -2,12 +2,12 @@ import PropTypes from 'prop-types';
import React from 'react';
import {
- clearStorage,
getClashAPIConfig,
getLatencyTestUrl,
getSelectedChartStyleIndex,
} from '../store/app';
import { fetchConfigs, getConfigs, updateConfigs } from '../store/configs';
+import { openModal } from '../store/modals';
import Button from './Button';
import s0 from './Config.module.css';
import ContentHeader from './ContentHeader';
@@ -104,6 +104,10 @@ function ConfigImpl({
refConfigs.current = configs;
}, [configs]);
+ const openAPIConfigModal = useCallback(() => {
+ dispatch(openModal('apiConfig'));
+ }, [dispatch]);
+
const setConfigState = useCallback(
(name, val) => {
setConfigStateInternal({
@@ -256,7 +260,7 @@ function ConfigImpl({
</div>
<div>
<div className={s0.label}>Action</div>
- <Button label="Log out" onClick={clearStorage} />
+ <Button label="Switch backend" onClick={openAPIConfigModal} />
</div>
</div>
</div>
diff --git a/src/components/ErrorBoundary.js b/src/components/ErrorBoundary.js
index cc3898e..ff49e1e 100644
--- a/src/components/ErrorBoundary.js
+++ b/src/components/ErrorBoundary.js
@@ -1,13 +1,10 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
+// import { getSentry } from '../misc/sentry';
import { deriveMessageFromError } from '../misc/errors';
-import { getSentry } from '../misc/sentry';
import ErrorBoundaryFallback from './ErrorBoundaryFallback';
-// XXX this is no Hook equivalents for componentDidCatch
-// we have to use class for now
-
class ErrorBoundary extends Component {
static propTypes = {
children: PropTypes.node,
@@ -15,44 +12,13 @@ class ErrorBoundary extends Component {
state = { error: null };
- loadSentry = async () => {
- if (this.sentry) return this.sentry;
- const x = await getSentry();
- this.sentry = x;
- return this.sentry;
- };
-
- // static getDerivedStateFromError(error) {
- // return { error };
- // }
-
- componentDidMount() {
- // this.loadSentry();
+ static getDerivedStateFromError(error) {
+ return { error };
}
- componentDidCatch(error, _info) {
- this.setState({ error });
- // eslint-disable-next-line no-console
- // console.log(error, errorInfo);
- // this.setState({ error });
- // this.loadSentry().then(Sentry => {
- // Sentry.withScope(scope => {
- // Object.keys(errorInfo).forEach(key => {
- // scope.setExtra(key, errorInfo[key]);
- // });
- // Sentry.captureException(error);
- // });
- // });
- }
-
- showReportDialog = () => {
- this.loadSentry().then((Sentry) => Sentry.showReportDialog());
- };
-
render() {
if (this.state.error) {
const { message, detail } = deriveMessageFromError(this.state.error);
- //render fallback UI
return <ErrorBoundaryFallback message={message} detail={detail} />;
} else {
return this.props.children;
diff --git a/src/components/Root.js b/src/components/Root.js
index 6c13163..8c06100 100644
--- a/src/components/Root.js
+++ b/src/components/Root.js
@@ -1,11 +1,12 @@
import './Root.css';
import React, { lazy, Suspense } from 'react';
-import { HashRouter as Router, Route, Routes } from 'react-router-dom';
+import { HashRouter as Router, useRoutes } from 'react-router-dom';
import { RecoilRoot } from 'recoil';
import { About } from 'src/components/about/About';
import { actions, initialState } from '../store';
+import APIConfig from './APIConfig';
import APIDiscovery from './APIDiscovery';
import ErrorBoundary from './ErrorBoundary';
import Home from './Home';
@@ -52,33 +53,50 @@ const Rules = lazy(() =>
);
const routes = [
- ['home', '/', <Home />],
- ['connections', '/connections', <Connections />],
- ['configs', '/configs', <Config />],
- ['logs', '/logs', <Logs />],
- ['proxies', '/proxies', <Proxies />],
- ['rules', '/rules', <Rules />],
- ['about', '/about', <About />],
- __DEV__ ? ['style', '/style', <StyleGuide />] : false,
+ { path: '/', element: <Home /> },
+ { path: '/connections', element: <Connections /> },
+ { path: '/configs', element: <Config /> },
+ { path: '/logs', element: <Logs /> },
+ { path: '/proxies', element: <Proxies /> },
+ { path: '/rules', element: <Rules /> },
+ { path: '/about', element: <About /> },
+ __DEV__ ? { path: '/style', element: <StyleGuide /> } : false,
].filter(Boolean);
+function RouteInnerApp() {
+ return useRoutes(routes);
+}
+
+function SideBarApp() {
+ return (
+ <>
+ <APIDiscovery />
+ <SideBar />
+ <div className={s0.content}>
+ <Suspense fallback={<Loading2 />}>
+ <RouteInnerApp />
+ </Suspense>
+ </div>
+ </>
+ );
+}
+
+function App() {
+ return useRoutes([
+ { path: '/backend', element: <APIConfig /> },
+ { path: '*', element: <SideBarApp /> },
+ ]);
+}
+
const Root = () => (
<ErrorBoundary>
<RecoilRoot>
<StateProvider initialState={initialState} actions={actions}>
<Router>
<div className={s0.app}>
- <APIDiscovery />
- <SideBar />
- <div className={s0.content}>
- <Suspense fallback={<Loading2 />}>
- <Routes>
- {routes.map(([key, path, element]) => (
- <Route key={key} path={path} element={element} />
- ))}
- </Routes>
- </Suspense>
- </div>
+ <Suspense fallback={<Loading2 />}>
+ <App />
+ </Suspense>
</div>
</Router>
</StateProvider>
diff --git a/src/components/TrafficChart.js b/src/components/TrafficChart.js
index 18bda77..bcfd4dc 100644
--- a/src/components/TrafficChart.js
+++ b/src/components/TrafficChart.js
@@ -25,8 +25,7 @@ export default connect(mapState)(TrafficChart);
function TrafficChart({ apiConfig, selectedChartStyleIndex }) {
const Chart = chartJSResource.read();
- const { hostname, port, secret } = apiConfig;
- const traffic = fetchData({ hostname, port, secret });
+ const traffic = fetchData(apiConfig);
const data = useMemo(
() => ({
labels: traffic.labels,
diff --git a/src/store/app.js b/src/store/app.js
index dc0e269..84ef96b 100644
--- a/src/store/app.js
+++ b/src/store/app.js
@@ -1,4 +1,4 @@
-import { clearState, loadState, saveState } from '../misc/storage';
+import { loadState, saveState } from '../misc/storage';
import { debounce, trimTrailingSlash } from '../misc/utils';
import { fetchConfigs } from './configs';
import { closeModal } from './modals';
@@ -7,6 +7,9 @@ export const getClashAPIConfig = (s) => {
const idx = s.app.selectedClashAPIConfigIndex;
return s.app.clashAPIConfigs[idx];
};
+export const getSelectedClashAPIConfigIndex = (s) =>
+ s.app.selectedClashAPIConfigIndex;
+export const getClashAPIConfigs = (s) => s.app.clashAPIConfigs;
export const getTheme = (s) => s.app.theme;
export const getSelectedChartStyleIndex = (s) => s.app.selectedChartStyleIndex;
export const getLatencyTestUrl = (s) => s.app.latencyTestUrl;
@@ -17,6 +20,66 @@ export const getAutoCloseOldConns = (s) => s.app.autoCloseOldConns;
const saveStateDebounced = debounce(saveState, 600);
+function findClashAPIConfigIndex(getState, { baseURL, secret }) {
+ const arr = getClashAPIConfigs(getState());
+ for (let i = 0; i < arr.length; i++) {
+ const x = arr[i];
+ if (x.baseURL === baseURL && x.secret === secret) return i;
+ }
+}
+
+export function addClashAPIConfig({ baseURL, secret }) {
+ return async (dispatch, getState) => {
+ const idx = findClashAPIConfigIndex(getState, { baseURL, secret });
+ // already exists
+ if (idx) return;
+
+ const clashAPIConfig = { baseURL, secret, addedAt: Date.now() };
+ dispatch('addClashAPIConfig', (s) => {
+ s.app.clashAPIConfigs.push(clashAPIConfig);
+ });
+ // side effect
+ saveState(getState().app);
+ };
+}
+
+export function removeClashAPIConfig({ baseURL, secret }) {
+ return async (dispatch, getState) => {
+ const idx = findClashAPIConfigIndex(getState, { baseURL, secret });
+ dispatch('removeClashAPIConfig', (s) => {
+ s.app.clashAPIConfigs = [
+ ...s.app.clashAPIConfigs.slice(0, idx),
+ ...s.app.clashAPIConfigs.slice(idx + 1),
+ ];
+ });
+ // side effect
+ saveState(getState().app);
+ };
+}
+
+export function selectClashAPIConfig({ baseURL, secret }) {
+ return async (dispatch, getState) => {
+ const idx = findClashAPIConfigIndex(getState, { baseURL, secret });
+ const curr = getSelectedClashAPIConfigIndex(getState());
+ if (curr !== idx) {
+ dispatch('selectClashAPIConfig', (s) => {
+ s.app.selectedClashAPIConfigIndex = idx;
+ });
+ }
+ // side effect
+ saveState(getState().app);
+
+ // manual clean up is too complex
+ // we just reload the app
+ try {
+ window.location.reload();
+ } catch (err) {
+ // ignore
+ }
+ };
+}
+
+// unused
export function updateClashAPIConfig({ baseURL, secret }) {
return async (dispatch, getState) => {
const clashAPIConfig = { baseURL, secret };
@@ -55,15 +118,6 @@ export function switchTheme() {
};
}
-export function clearStorage() {
- clearState();
- try {
- window.location.reload();
- } catch (err) {
- // ignore
- }
-}
-
export function selectChartStyleIndex(selectedChartStyleIndex) {
return (dispatch, getState) => {
dispatch('appSelectChartStyleIndex', (s) => {
@@ -97,6 +151,7 @@ export function updateCollapsibleIsOpen(prefix, name, v) {
const defaultClashAPIConfig = {
baseURL: 'http://127.0.0.1:7892',
secret: '',
+ addedAt: 0,
};
// type Theme = 'light' | 'dark';
const defaultState = {
@@ -133,25 +188,24 @@ export function initialState() {
const query = parseConfigQueryString();
const conf = s.clashAPIConfigs[s.selectedClashAPIConfigIndex];
- const url = new URL(conf.baseURL);
- if (query.hostname) {
- url.hostname = query.hostname;
- }
- if (query.port) {
- url.port = query.port;
- }
- // url.href is a stringifier and it appends a trailing slash
- // that is not we want
- conf.baseURL = trimTrailingSlash(url.href);
-
- if (query.secret) {
- conf.secret = query.secret;
+ if (conf) {
+ const url = new URL(conf.baseURL);
+ if (query.hostname) {
+ url.hostname = query.hostname;
+ }
+ if (query.port) {
+ url.port = query.port;
+ }
+ // url.href is a stringifier and it appends a trailing slash
+ // that is not we want
+ conf.baseURL = trimTrailingSlash(url.href);
+ if (query.secret) {
+ conf.secret = query.secret;
+ }
}
- if (query.theme) {
- if (query.theme === 'dark' || query.theme === 'light') {
- s.theme = query.theme;
- }
+ if (query.theme === 'dark' || query.theme === 'light') {
+ s.theme = query.theme;
}
// set initial theme
setTheme(s.theme);
diff --git a/src/store/configs.js b/src/store/configs.js
index f114cef..bcd4ac8 100644
--- a/src/store/configs.js
+++ b/src/store/configs.js
@@ -2,8 +2,8 @@ import * as configsAPI from '../api/configs';
import * as trafficAPI from '../api/traffic';
import { openModal } from './modals';
-export const getConfigs = s => s.configs.configs;
-export const getLogLevel = s => s.configs.configs['log-level'];
+export const getConfigs = (s) => s.configs.configs;
+export const getLogLevel = (s) => s.configs.configs['log-level'];
export function fetchConfigs(apiConfig) {
return async (dispatch, getState) => {
@@ -11,25 +11,20 @@ export function fetchConfigs(apiConfig) {
try {
res = await configsAPI.fetchConfigs(apiConfig);
} catch (err) {
- // eslint-disable-next-line no-console
- console.log('Error fetch configs', err);
+ // TypeError and AbortError
dispatch(openModal('apiConfig'));
return;
}
if (!res.ok) {
- if (res.status === 404 || res.status === 401) {
- dispatch(openModal('apiConfig'));
- } else {
- // eslint-disable-next-line no-console
- console.log('Error fetch configs', res.statusText);
- }
+ console.log('Error fetch configs', res.statusText);
+ dispatch(openModal('apiConfig'));
return;
}
const payload = await res.json();
- dispatch('store/configs#fetchConfigs', s => {
+ dispatch('store/configs#fetchConfigs', (s) => {
s.configs.configs = payload;
});
@@ -47,25 +42,25 @@ export function fetchConfigs(apiConfig) {
}
function markHaveFetchedConfig() {
- return dispatch => {
- dispatch('store/configs#markHaveFetchedConfig', s => {
+ return (dispatch) => {
+ dispatch('store/configs#markHaveFetchedConfig', (s) => {
s.configs.haveFetchedConfig = true;
});
};
}
export function updateConfigs(apiConfig, partialConfg) {
- return async dispatch => {
+ return async (dispatch) => {
configsAPI
.updateConfigs(apiConfig, partialConfg)
.then(
- res => {
+ (res) => {
if (res.ok === false) {
// eslint-disable-next-line no-console
console.log('Error update configs', res.statusText);
}
},
- err => {
+ (err) => {
// eslint-disable-next-line no-console
console.log('Error update configs', err);
throw err;
@@ -75,7 +70,7 @@ export function updateConfigs(apiConfig, partialConfg) {
dispatch(fetchConfigs(apiConfig));
});
- dispatch('storeConfigsOptimisticUpdateConfigs', s => {
+ dispatch('storeConfigsOptimisticUpdateConfigs', (s) => {
s.configs.configs = { ...s.configs.configs, ...partialConfg };
});
};
@@ -88,7 +83,7 @@ export const initialState = {
'redir-port': 0,
'allow-lan': false,
mode: 'Rule',
- 'log-level': 'info'
+ 'log-level': 'info',
},
- haveFetchedConfig: false
+ haveFetchedConfig: false,
};
diff --git a/src/store/index.js b/src/store/index.js
index bd4e7a9..4fc8e4c 100644
--- a/src/store/index.js
+++ b/src/store/index.js
@@ -1,6 +1,8 @@
import {
initialState as app,
+ removeClashAPIConfig,
selectChartStyleIndex,
+ selectClashAPIConfig,
updateAppConfig,
updateCollapsibleIsOpen,
} from './app';
@@ -24,6 +26,8 @@ export const actions = {
app: {
updateCollapsibleIsOpen,
updateAppConfig,
+ removeClashAPIConfig,
+ selectClashAPIConfig,
},
proxies: proxiesActions,
};