Sfoglia il codice sorgente

feat: Optimizing Provider Support

MystiPanda 1 anno fa
parent
commit
cbc7612451

+ 75 - 28
src/components/proxy/provider-button.tsx

@@ -7,25 +7,30 @@ import {
   List,
   ListItem,
   ListItemText,
+  styled,
+  Box,
+  alpha,
+  Typography,
+  Divider,
 } from "@mui/material";
 import { RefreshRounded } from "@mui/icons-material";
 import { useTranslation } from "react-i18next";
 import { useLockFn } from "ahooks";
-import { getProviders, providerUpdate } from "@/services/api";
+import { getProxyProviders, proxyProviderUpdate } from "@/services/api";
 import { BaseDialog } from "../base";
 
 export const ProviderButton = () => {
   const { t } = useTranslation();
-  const { data } = useSWR("getProviders", getProviders);
+  const { data } = useSWR("getProxyProviders", getProxyProviders);
 
   const [open, setOpen] = useState(false);
 
   const hasProvider = Object.keys(data || {}).length > 0;
 
   const handleUpdate = useLockFn(async (key: string) => {
-    await providerUpdate(key);
+    await proxyProviderUpdate(key);
     await mutate("getProxies");
-    await mutate("getProviders");
+    await mutate("getProxyProviders");
   });
 
   if (!hasProvider) return null;
@@ -43,7 +48,23 @@ export const ProviderButton = () => {
 
       <BaseDialog
         open={open}
-        title={t("Proxy Provider")}
+        title={
+          <Box display="flex" justifyContent="space-between" gap={1}>
+            <Typography variant="h6">{t("Proxy Provider")}</Typography>
+            <Button
+              variant="contained"
+              onClick={async () => {
+                Object.entries(data || {}).forEach(async ([key, item]) => {
+                  await proxyProviderUpdate(key);
+                  await mutate("getProxies");
+                  await mutate("getProxyProviders");
+                });
+              }}
+            >
+              {t("Update All")}
+            </Button>
+          </Box>
+        }
         contentSx={{ width: 400 }}
         disableOk
         cancelBtn={t("Cancel")}
@@ -54,29 +75,43 @@ export const ProviderButton = () => {
           {Object.entries(data || {}).map(([key, item]) => {
             const time = dayjs(item.updatedAt);
             return (
-              <ListItem sx={{ p: 0 }} key={key}>
-                <ListItemText
-                  primary={key}
-                  secondary={
-                    <>
-                      <span style={{ marginRight: "4em" }}>
-                        Type: {item.vehicleType}
-                      </span>
-                      <span title={time.format("YYYY-MM-DD HH:mm:ss")}>
-                        Updated: {time.fromNow()}
-                      </span>
-                    </>
-                  }
-                />
-                <IconButton
-                  size="small"
-                  color="inherit"
-                  title="Update Provider"
-                  onClick={() => handleUpdate(key)}
-                >
-                  <RefreshRounded />
-                </IconButton>
-              </ListItem>
+              <>
+                <ListItem sx={{ p: 0 }} key={key}>
+                  <ListItemText
+                    primary={
+                      <>
+                        <Typography
+                          variant="h6"
+                          component="span"
+                          noWrap
+                          title={key}
+                        >
+                          {key}
+                        </Typography>
+                      </>
+                    }
+                    secondary={
+                      <>
+                        <StyledTypeBox component="span">
+                          {item.vehicleType}
+                        </StyledTypeBox>
+                        <StyledTypeBox component="span">
+                          {t("Update At")} {time.fromNow()}
+                        </StyledTypeBox>
+                      </>
+                    }
+                  />
+                  <IconButton
+                    size="small"
+                    color="inherit"
+                    title="Update Provider"
+                    onClick={() => handleUpdate(key)}
+                  >
+                    <RefreshRounded />
+                  </IconButton>
+                </ListItem>
+                <Divider />
+              </>
             );
           })}
         </List>
@@ -84,3 +119,15 @@ export const ProviderButton = () => {
     </>
   );
 };
+
+const StyledTypeBox = styled(Box)(({ theme }) => ({
+  display: "inline-block",
+  border: "1px solid #ccc",
+  borderColor: alpha(theme.palette.primary.main, 0.5),
+  color: alpha(theme.palette.primary.main, 0.8),
+  borderRadius: 4,
+  fontSize: 10,
+  marginRight: "4px",
+  padding: "0 2px",
+  lineHeight: 1.25,
+}));

+ 150 - 0
src/components/rule/provider-button.tsx

@@ -0,0 +1,150 @@
+import dayjs from "dayjs";
+import useSWR, { mutate } from "swr";
+import { useState } from "react";
+import {
+  Button,
+  IconButton,
+  List,
+  ListItem,
+  ListItemText,
+  Typography,
+  styled,
+  Box,
+  alpha,
+  Divider,
+} from "@mui/material";
+import { RefreshRounded } from "@mui/icons-material";
+import { useTranslation } from "react-i18next";
+import { useLockFn } from "ahooks";
+import { getRuleProviders, ruleProviderUpdate } from "@/services/api";
+import { BaseDialog } from "../base";
+
+export const ProviderButton = () => {
+  const { t } = useTranslation();
+  const { data } = useSWR("getRuleProviders", getRuleProviders);
+
+  const [open, setOpen] = useState(false);
+
+  const hasProvider = Object.keys(data || {}).length > 0;
+
+  const handleUpdate = useLockFn(async (key: string) => {
+    await ruleProviderUpdate(key);
+    await mutate("getRules");
+    await mutate("getRuleProviders");
+  });
+
+  if (!hasProvider) return null;
+
+  return (
+    <>
+      <Button
+        size="small"
+        variant="outlined"
+        sx={{ textTransform: "capitalize" }}
+        onClick={() => setOpen(true)}
+      >
+        {t("Provider")}
+      </Button>
+
+      <BaseDialog
+        open={open}
+        title={
+          <Box display="flex" justifyContent="space-between" gap={1}>
+            <Typography variant="h6">{t("Rule Provider")}</Typography>
+            <Button
+              variant="contained"
+              onClick={async () => {
+                Object.entries(data || {}).forEach(async ([key, item]) => {
+                  await ruleProviderUpdate(key);
+                  await mutate("getRules");
+                  await mutate("getRuleProviders");
+                });
+              }}
+            >
+              {t("Update All")}
+            </Button>
+          </Box>
+        }
+        contentSx={{ width: 400 }}
+        disableOk
+        cancelBtn={t("Cancel")}
+        onClose={() => setOpen(false)}
+        onCancel={() => setOpen(false)}
+      >
+        <List sx={{ py: 0, minHeight: 250 }}>
+          {Object.entries(data || {}).map(([key, item]) => {
+            const time = dayjs(item.updatedAt);
+            return (
+              <>
+                <ListItem sx={{ p: 0 }} key={key}>
+                  <ListItemText
+                    primary={
+                      <>
+                        <Typography
+                          variant="h6"
+                          component="span"
+                          noWrap
+                          title={key}
+                        >
+                          {key}
+                        </Typography>
+                        <TypeBox component="span" sx={{ marginLeft: "8px" }}>
+                          {item.ruleCount}
+                        </TypeBox>
+                      </>
+                    }
+                    secondary={
+                      <>
+                        <StyledTypeBox component="span">
+                          {item.vehicleType}
+                        </StyledTypeBox>
+                        <StyledTypeBox component="span">
+                          {item.behavior}
+                        </StyledTypeBox>
+                        <StyledTypeBox component="span">
+                          {t("Update At")} {time.fromNow()}
+                        </StyledTypeBox>
+                      </>
+                    }
+                  />
+                  <IconButton
+                    size="small"
+                    color="inherit"
+                    title="Update Provider"
+                    onClick={() => handleUpdate(key)}
+                  >
+                    <RefreshRounded />
+                  </IconButton>
+                </ListItem>
+                <Divider />
+              </>
+            );
+          })}
+        </List>
+      </BaseDialog>
+    </>
+  );
+};
+const TypeBox = styled(Box)(({ theme }) => ({
+  display: "inline-block",
+  border: "1px solid #ccc",
+  borderColor: alpha(theme.palette.secondary.main, 0.5),
+  color: alpha(theme.palette.secondary.main, 0.8),
+  borderRadius: 4,
+  fontSize: 10,
+  marginRight: "4px",
+  padding: "0 2px",
+  lineHeight: 1.25,
+}));
+
+const StyledTypeBox = styled(Box)(({ theme }) => ({
+  display: "inline-block",
+  border: "1px solid #ccc",
+  borderColor: alpha(theme.palette.primary.main, 0.5),
+  color: alpha(theme.palette.primary.main, 0.8),
+  borderRadius: 4,
+  fontSize: 10,
+  marginRight: "4px",
+  padding: "0 2px",
+  lineHeight: 1.25,
+}));

+ 2 - 0
src/locales/en.json

@@ -57,6 +57,8 @@
   "Filter conditions": "Filter conditions",
   "Refresh profiles": "Refresh profiles",
   "Rules": "Rules",
+  "Update All": "Update All",
+  "Update At": "Update At",
 
   "Type": "Type",
   "Name": "Name",

+ 3 - 0
src/locales/ru.json

@@ -56,6 +56,9 @@
   "Filter": "Фильтр",
   "Filter conditions": "Условия фильтрации",
   "Refresh profiles": "Обновить профили",
+  "Rules": "Правила",
+  "Update All": "Обновить все",
+  "Update At": "Обновлено в",
 
   "Type": "Тип",
   "Name": "Название",

+ 2 - 0
src/locales/zh.json

@@ -57,6 +57,8 @@
   "Filter conditions": "过滤条件",
   "Refresh profiles": "刷新订阅",
   "Rules": "规则",
+  "Update All": "更新全部",
+  "Update At": "更新于",
 
   "Type": "类型",
   "Name": "名称",

+ 1 - 1
src/pages/_layout.tsx

@@ -54,7 +54,7 @@ const Layout = () => {
       mutate("getProxies");
       mutate("getVersion");
       mutate("getClashConfig");
-      mutate("getProviders");
+      mutate("getProxyProviders");
     });
 
     // update the verge config

+ 12 - 2
src/pages/rules.tsx

@@ -2,10 +2,11 @@ import useSWR from "swr";
 import { useState, useMemo } from "react";
 import { useTranslation } from "react-i18next";
 import { Virtuoso } from "react-virtuoso";
-import { Box, Paper, TextField } from "@mui/material";
+import { Box, TextField } from "@mui/material";
 import { getRules } from "@/services/api";
 import { BaseEmpty, BasePage } from "@/components/base";
 import RuleItem from "@/components/rule/rule-item";
+import { ProviderButton } from "@/components/rule/provider-button";
 
 const RulesPage = () => {
   const { t } = useTranslation();
@@ -18,7 +19,16 @@ const RulesPage = () => {
   }, [data, filterText]);
 
   return (
-    <BasePage full title={t("Rules")} contentStyle={{ height: "100%" }}>
+    <BasePage
+      full
+      title={t("Rules")}
+      contentStyle={{ height: "100%" }}
+      header={
+        <Box display="flex" alignItems="center" gap={1}>
+          <ProviderButton />
+        </Box>
+      }
+    >
       <Box
         sx={{
           pt: 1,

+ 29 - 4
src/services/api.ts

@@ -105,7 +105,7 @@ export const getProxiesInner = async () => {
 export const getProxies = async () => {
   const [proxyRecord, providerRecord] = await Promise.all([
     getProxiesInner(),
-    getProviders(),
+    getProxyProviders(),
   ]);
 
   // provider name map
@@ -166,11 +166,31 @@ export const getProxies = async () => {
 };
 
 // get proxy providers
-export const getProviders = async () => {
+export const getProxyProviders = async () => {
   const instance = await getAxios();
   const response = await instance.get<any, any>("/providers/proxies");
 
-  const providers = (response.providers || {}) as Record<string, IProviderItem>;
+  const providers = (response.providers || {}) as Record<
+    string,
+    IProxyProviderItem
+  >;
+
+  return Object.fromEntries(
+    Object.entries(providers).filter(([key, item]) => {
+      const type = item.vehicleType.toLowerCase();
+      return type === "http" || type === "file";
+    })
+  );
+};
+
+export const getRuleProviders = async () => {
+  const instance = await getAxios();
+  const response = await instance.get<any, any>("/providers/rules");
+
+  const providers = (response.providers || {}) as Record<
+    string,
+    IRuleProviderItem
+  >;
 
   return Object.fromEntries(
     Object.entries(providers).filter(([key, item]) => {
@@ -188,11 +208,16 @@ export const providerHealthCheck = async (name: string) => {
   );
 };
 
-export const providerUpdate = async (name: string) => {
+export const proxyProviderUpdate = async (name: string) => {
   const instance = await getAxios();
   return instance.put(`/providers/proxies/${encodeURIComponent(name)}`);
 };
 
+export const ruleProviderUpdate = async (name: string) => {
+  const instance = await getAxios();
+  return instance.put(`/providers/rules/${encodeURIComponent(name)}`);
+};
+
 export const getConnections = async () => {
   const instance = await getAxios();
   const result = await instance.get("/connections");

+ 11 - 1
src/services/types.d.ts

@@ -61,7 +61,7 @@ type IProxyGroupItem = Omit<IProxyItem, "all"> & {
   all: IProxyItem[];
 };
 
-interface IProviderItem {
+interface IProxyProviderItem {
   name: string;
   type: string;
   proxies: IProxyItem[];
@@ -69,6 +69,16 @@ interface IProviderItem {
   vehicleType: string;
 }
 
+interface IRuleProviderItem {
+  name: string;
+  behavior: string;
+  format: string;
+  ruleCount: number;
+  type: string;
+  updatedAt: string;
+  vehicleType: string;
+}
+
 interface ITrafficItem {
   up: number;
   down: number;