mirror of
https://github.com/clash-verge-rev/clash-verge-rev
synced 2025-05-05 06:53:44 +08:00
feat: supports show connection detail
This commit is contained in:
parent
53a207e859
commit
2ce944034d
104
src/components/connection/connection-detail.tsx
Normal file
104
src/components/connection/connection-detail.tsx
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
import dayjs from "dayjs";
|
||||||
|
import { forwardRef, useImperativeHandle, useState } from "react";
|
||||||
|
import { useLockFn } from "ahooks";
|
||||||
|
import { Box, Button, Snackbar } from "@mui/material";
|
||||||
|
import { deleteConnection } from "@/services/api";
|
||||||
|
import { truncateStr } from "@/utils/truncate-str";
|
||||||
|
import parseTraffic from "@/utils/parse-traffic";
|
||||||
|
|
||||||
|
export interface ConnectionDetailRef {
|
||||||
|
open: (detail: IConnectionsItem) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ConnectionDetail = forwardRef<ConnectionDetailRef>(
|
||||||
|
(props, ref) => {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [detail, setDetail] = useState<IConnectionsItem>(null!);
|
||||||
|
|
||||||
|
useImperativeHandle(ref, () => ({
|
||||||
|
open: (detail: IConnectionsItem) => {
|
||||||
|
if (open) return;
|
||||||
|
setOpen(true);
|
||||||
|
setDetail(detail);
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const onClose = () => setOpen(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Snackbar
|
||||||
|
anchorOrigin={{ vertical: "bottom", horizontal: "right" }}
|
||||||
|
open={open}
|
||||||
|
onClose={onClose}
|
||||||
|
message={
|
||||||
|
detail ? (
|
||||||
|
<InnerConnectionDetail data={detail} onClose={onClose} />
|
||||||
|
) : null
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
interface InnerProps {
|
||||||
|
data: IConnectionsItem;
|
||||||
|
onClose?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const InnerConnectionDetail = ({ data, onClose }: InnerProps) => {
|
||||||
|
const { metadata, rulePayload } = data;
|
||||||
|
const chains = [...data.chains].reverse().join(" / ");
|
||||||
|
const rule = rulePayload ? `${data.rule}(${rulePayload})` : data.rule;
|
||||||
|
const host = metadata.host
|
||||||
|
? `${metadata.host}:${metadata.destinationPort}`
|
||||||
|
: `${metadata.destinationIP}:${metadata.destinationPort}`;
|
||||||
|
|
||||||
|
const information = [
|
||||||
|
{ label: "Host", value: host },
|
||||||
|
{ label: "Download", value: parseTraffic(data.download).join(" ") },
|
||||||
|
{ label: "Upload", value: parseTraffic(data.upload).join(" ") },
|
||||||
|
{
|
||||||
|
label: "DL Speed",
|
||||||
|
value: parseTraffic(data.curDownload ?? -1).join(" ") + "/s",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "UL Speed",
|
||||||
|
value: parseTraffic(data.curUpload ?? -1).join(" ") + "/s",
|
||||||
|
},
|
||||||
|
{ label: "Chains", value: chains },
|
||||||
|
{ label: "Rule", value: rule },
|
||||||
|
{
|
||||||
|
label: "Process",
|
||||||
|
value: truncateStr(metadata.process || metadata.processPath),
|
||||||
|
},
|
||||||
|
{ label: "Time", value: dayjs(data.start).fromNow() },
|
||||||
|
{ label: "Source", value: `${metadata.sourceIP}:${metadata.sourcePort}` },
|
||||||
|
{ label: "Destination IP", value: metadata.destinationIP },
|
||||||
|
{ label: "Type", value: `${metadata.type}(${metadata.network})` },
|
||||||
|
];
|
||||||
|
|
||||||
|
const onDelete = useLockFn(async () => deleteConnection(data.id));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{ userSelect: "text" }}>
|
||||||
|
{information.map((each) => (
|
||||||
|
<div key={each.label}>
|
||||||
|
<b>{each.label}</b>: <span>{each.value}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<Box sx={{ textAlign: "right" }}>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
title="Close Connection"
|
||||||
|
onClick={() => {
|
||||||
|
onDelete();
|
||||||
|
onClose?.();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
@ -24,10 +24,11 @@ const Tag = styled("span")(({ theme }) => ({
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
value: IConnectionsItem;
|
value: IConnectionsItem;
|
||||||
|
onShowDetail?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ConnectionItem = (props: Props) => {
|
export const ConnectionItem = (props: Props) => {
|
||||||
const { value } = props;
|
const { value, onShowDetail } = props;
|
||||||
|
|
||||||
const { id, metadata, chains, start, curUpload, curDownload } = value;
|
const { id, metadata, chains, start, curUpload, curDownload } = value;
|
||||||
|
|
||||||
@ -44,8 +45,9 @@ const ConnectionItem = (props: Props) => {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
sx={{ userSelect: "text" }}
|
sx={{ userSelect: "text", cursor: "pointer" }}
|
||||||
primary={metadata.host || metadata.destinationIP}
|
primary={metadata.host || metadata.destinationIP}
|
||||||
|
onClick={onShowDetail}
|
||||||
secondary={
|
secondary={
|
||||||
<Box sx={{ display: "flex", flexWrap: "wrap" }}>
|
<Box sx={{ display: "flex", flexWrap: "wrap" }}>
|
||||||
<Tag sx={{ textTransform: "uppercase", color: "success" }}>
|
<Tag sx={{ textTransform: "uppercase", color: "success" }}>
|
||||||
@ -71,5 +73,3 @@ const ConnectionItem = (props: Props) => {
|
|||||||
</ListItem>
|
</ListItem>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ConnectionItem;
|
|
||||||
|
@ -1,37 +1,29 @@
|
|||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import { DataGrid, GridColDef } from "@mui/x-data-grid";
|
import { DataGrid, GridColDef } from "@mui/x-data-grid";
|
||||||
import { Snackbar } from "@mui/material";
|
import { truncateStr } from "@/utils/truncate-str";
|
||||||
import parseTraffic from "@/utils/parse-traffic";
|
import parseTraffic from "@/utils/parse-traffic";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
connections: IConnectionsItem[];
|
connections: IConnectionsItem[];
|
||||||
|
onShowDetail: (data: IConnectionsItem) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ConnectionTable = (props: Props) => {
|
export const ConnectionTable = (props: Props) => {
|
||||||
const { connections } = props;
|
const { connections, onShowDetail } = props;
|
||||||
|
|
||||||
const [openedDetail, setOpenedDetail] = useState<IConnectionsItem | null>(
|
const [columnVisible, setColumnVisible] = useState<
|
||||||
null
|
Partial<Record<keyof IConnectionsItem, boolean>>
|
||||||
);
|
>({});
|
||||||
|
|
||||||
const columns: GridColDef[] = [
|
const columns: GridColDef[] = [
|
||||||
{
|
{ field: "host", headerName: "Host", flex: 220, minWidth: 220 },
|
||||||
field: "host",
|
|
||||||
headerName: "Host",
|
|
||||||
flex: 200,
|
|
||||||
minWidth: 200,
|
|
||||||
resizable: false,
|
|
||||||
disableColumnMenu: true,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
field: "download",
|
field: "download",
|
||||||
headerName: "Download",
|
headerName: "Download",
|
||||||
width: 88,
|
width: 88,
|
||||||
align: "right",
|
align: "right",
|
||||||
headerAlign: "right",
|
headerAlign: "right",
|
||||||
disableColumnMenu: true,
|
|
||||||
valueFormatter: (params: any) => parseTraffic(params.value).join(" "),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: "upload",
|
field: "upload",
|
||||||
@ -39,18 +31,13 @@ const ConnectionTable = (props: Props) => {
|
|||||||
width: 88,
|
width: 88,
|
||||||
align: "right",
|
align: "right",
|
||||||
headerAlign: "right",
|
headerAlign: "right",
|
||||||
disableColumnMenu: true,
|
|
||||||
valueFormatter: (params: any) => parseTraffic(params.value).join(" "),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: "dlSpeed",
|
field: "dlSpeed",
|
||||||
headerName: "DL Speed",
|
headerName: "DL Speed",
|
||||||
align: "right",
|
|
||||||
width: 88,
|
width: 88,
|
||||||
|
align: "right",
|
||||||
headerAlign: "right",
|
headerAlign: "right",
|
||||||
disableColumnMenu: true,
|
|
||||||
valueFormatter: (params: any) =>
|
|
||||||
parseTraffic(params.value).join(" ") + "/s",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: "ulSpeed",
|
field: "ulSpeed",
|
||||||
@ -58,55 +45,26 @@ const ConnectionTable = (props: Props) => {
|
|||||||
width: 88,
|
width: 88,
|
||||||
align: "right",
|
align: "right",
|
||||||
headerAlign: "right",
|
headerAlign: "right",
|
||||||
disableColumnMenu: true,
|
|
||||||
valueFormatter: (params: any) =>
|
|
||||||
parseTraffic(params.value).join(" ") + "/s",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: "chains",
|
|
||||||
headerName: "Chains",
|
|
||||||
width: 360,
|
|
||||||
disableColumnMenu: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: "rule",
|
|
||||||
headerName: "Rule",
|
|
||||||
width: 225,
|
|
||||||
disableColumnMenu: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: "process",
|
|
||||||
headerName: "Process",
|
|
||||||
width: 480,
|
|
||||||
disableColumnMenu: true,
|
|
||||||
},
|
},
|
||||||
|
{ field: "chains", headerName: "Chains", flex: 360, minWidth: 360 },
|
||||||
|
{ field: "rule", headerName: "Rule", flex: 300, minWidth: 250 },
|
||||||
|
{ field: "process", headerName: "Process", flex: 480, minWidth: 480 },
|
||||||
{
|
{
|
||||||
field: "time",
|
field: "time",
|
||||||
headerName: "Time",
|
headerName: "Time",
|
||||||
width: 120,
|
flex: 120,
|
||||||
|
minWidth: 100,
|
||||||
align: "right",
|
align: "right",
|
||||||
headerAlign: "right",
|
headerAlign: "right",
|
||||||
disableColumnMenu: true,
|
|
||||||
valueFormatter: (params) => dayjs(params.value).fromNow(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: "source",
|
|
||||||
headerName: "Source",
|
|
||||||
width: 150,
|
|
||||||
disableColumnMenu: true,
|
|
||||||
},
|
},
|
||||||
|
{ field: "source", headerName: "Source", flex: 200, minWidth: 130 },
|
||||||
{
|
{
|
||||||
field: "destinationIP",
|
field: "destinationIP",
|
||||||
headerName: "Destination IP",
|
headerName: "Destination IP",
|
||||||
width: 125,
|
flex: 200,
|
||||||
disableColumnMenu: true,
|
minWidth: 130,
|
||||||
},
|
|
||||||
{
|
|
||||||
field: "type",
|
|
||||||
headerName: "Type",
|
|
||||||
width: 160,
|
|
||||||
disableColumnMenu: true,
|
|
||||||
},
|
},
|
||||||
|
{ field: "type", headerName: "Type", flex: 160, minWidth: 100 },
|
||||||
];
|
];
|
||||||
|
|
||||||
const connRows = useMemo(() => {
|
const connRows = useMemo(() => {
|
||||||
@ -120,18 +78,16 @@ const ConnectionTable = (props: Props) => {
|
|||||||
host: metadata.host
|
host: metadata.host
|
||||||
? `${metadata.host}:${metadata.destinationPort}`
|
? `${metadata.host}:${metadata.destinationPort}`
|
||||||
: `${metadata.destinationIP}:${metadata.destinationPort}`,
|
: `${metadata.destinationIP}:${metadata.destinationPort}`,
|
||||||
download: each.download,
|
download: parseTraffic(each.download).join(" "),
|
||||||
upload: each.upload,
|
upload: parseTraffic(each.upload).join(" "),
|
||||||
dlSpeed: each.curDownload,
|
dlSpeed: parseTraffic(each.curDownload).join(" ") + "/s",
|
||||||
ulSpeed: each.curUpload,
|
ulSpeed: parseTraffic(each.curUpload).join(" ") + "/s",
|
||||||
chains,
|
chains,
|
||||||
rule,
|
rule,
|
||||||
process: truncateStr(
|
process: truncateStr(metadata.process || metadata.processPath)?.repeat(
|
||||||
metadata.process || metadata.processPath || "",
|
10
|
||||||
16,
|
|
||||||
56
|
|
||||||
),
|
),
|
||||||
time: each.start,
|
time: dayjs(each.start).fromNow(),
|
||||||
source: `${metadata.sourceIP}:${metadata.sourcePort}`,
|
source: `${metadata.sourceIP}:${metadata.sourcePort}`,
|
||||||
destinationIP: metadata.destinationIP,
|
destinationIP: metadata.destinationIP,
|
||||||
type: `${metadata.type}(${metadata.network})`,
|
type: `${metadata.type}(${metadata.network})`,
|
||||||
@ -142,101 +98,15 @@ const ConnectionTable = (props: Props) => {
|
|||||||
}, [connections]);
|
}, [connections]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<DataGrid
|
||||||
<DataGrid
|
hideFooter
|
||||||
rows={connRows}
|
rows={connRows}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
onRowClick={(e) => setOpenedDetail(e.row.connectionData)}
|
onRowClick={(e) => onShowDetail(e.row.connectionData)}
|
||||||
density="compact"
|
density="compact"
|
||||||
sx={{ border: "none", "div:focus": { outline: "none !important" } }}
|
sx={{ border: "none", "div:focus": { outline: "none !important" } }}
|
||||||
hideFooter
|
columnVisibilityModel={columnVisible}
|
||||||
/>
|
onColumnVisibilityModelChange={(e) => setColumnVisible(e)}
|
||||||
<Snackbar
|
/>
|
||||||
anchorOrigin={{ vertical: "bottom", horizontal: "right" }}
|
|
||||||
open={Boolean(openedDetail)}
|
|
||||||
onClose={() => setOpenedDetail(null)}
|
|
||||||
message={
|
|
||||||
openedDetail ? <SingleConnectionDetail data={openedDetail} /> : null
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ConnectionTable;
|
|
||||||
|
|
||||||
const truncateStr = (str: string, prefixLen: number, maxLen: number) => {
|
|
||||||
if (str.length <= maxLen) return str;
|
|
||||||
return (
|
|
||||||
str.slice(0, prefixLen) + " ... " + str.slice(-(maxLen - prefixLen - 5))
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const SingleConnectionDetail = ({ data }: { data: IConnectionsItem }) => {
|
|
||||||
const { metadata, rulePayload } = data;
|
|
||||||
const chains = [...data.chains].reverse().join(" / ");
|
|
||||||
const rule = rulePayload ? `${data.rule}(${rulePayload})` : data.rule;
|
|
||||||
const host = metadata.host
|
|
||||||
? `${metadata.host}:${metadata.destinationPort}`
|
|
||||||
: `${metadata.destinationIP}:${metadata.destinationPort}`;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div>
|
|
||||||
{" "}
|
|
||||||
<b>Host</b>: <span>{host}</span>{" "}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
{" "}
|
|
||||||
<b>Download</b>: <span>{parseTraffic(data.download).join(" ")}</span>{" "}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
{" "}
|
|
||||||
<b>Upload</b>: <span>{parseTraffic(data.upload).join(" ")}</span>{" "}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
{" "}
|
|
||||||
<b>DL Speed</b>:{" "}
|
|
||||||
<span>{parseTraffic(data.curDownload ?? -1).join(" ") + "/s"}</span>{" "}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
{" "}
|
|
||||||
<b>UL Speed</b>:{" "}
|
|
||||||
<span>{parseTraffic(data.curUpload ?? -1).join(" ") + "/s"}</span>{" "}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
{" "}
|
|
||||||
<b>Chains</b>: <span>{chains}</span>{" "}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
{" "}
|
|
||||||
<b>Rule</b>: <span>{rule}</span>{" "}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
{" "}
|
|
||||||
<b>Process</b>: <span>{metadata.process}</span>{" "}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
{" "}
|
|
||||||
<b>ProcessPath</b>: <span>{metadata.processPath}</span>{" "}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
{" "}
|
|
||||||
<b>Time</b>: <span>{dayjs(data.start).fromNow()}</span>{" "}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
{" "}
|
|
||||||
<b>Source</b>:{" "}
|
|
||||||
<span>{`${metadata.sourceIP}:${metadata.sourcePort}`}</span>{" "}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
{" "}
|
|
||||||
<b>Destination IP</b>: <span>{metadata.destinationIP}</span>{" "}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
{" "}
|
|
||||||
<b>Type</b>: <span>{`${metadata.type}(${metadata.network})`}</span>{" "}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { useLockFn } from "ahooks";
|
import { useLockFn } from "ahooks";
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
@ -18,8 +18,12 @@ import { atomConnectionSetting } from "@/services/states";
|
|||||||
import { useClashInfo } from "@/hooks/use-clash";
|
import { useClashInfo } from "@/hooks/use-clash";
|
||||||
import { BaseEmpty, BasePage } from "@/components/base";
|
import { BaseEmpty, BasePage } from "@/components/base";
|
||||||
import { useWebsocket } from "@/hooks/use-websocket";
|
import { useWebsocket } from "@/hooks/use-websocket";
|
||||||
import ConnectionItem from "@/components/connection/connection-item";
|
import { ConnectionItem } from "@/components/connection/connection-item";
|
||||||
import ConnectionTable from "@/components/connection/connection-table";
|
import { ConnectionTable } from "@/components/connection/connection-table";
|
||||||
|
import {
|
||||||
|
ConnectionDetail,
|
||||||
|
ConnectionDetailRef,
|
||||||
|
} from "@/components/connection/connection-detail";
|
||||||
|
|
||||||
const initConn = { uploadTotal: 0, downloadTotal: 0, connections: [] };
|
const initConn = { uploadTotal: 0, downloadTotal: 0, connections: [] };
|
||||||
|
|
||||||
@ -106,6 +110,8 @@ const ConnectionsPage = () => {
|
|||||||
|
|
||||||
const onCloseAll = useLockFn(closeAllConnections);
|
const onCloseAll = useLockFn(closeAllConnections);
|
||||||
|
|
||||||
|
const detailRef = useRef<ConnectionDetailRef>(null!);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BasePage
|
<BasePage
|
||||||
title={t("Connections")}
|
title={t("Connections")}
|
||||||
@ -186,14 +192,24 @@ const ConnectionsPage = () => {
|
|||||||
{filterConn.length === 0 ? (
|
{filterConn.length === 0 ? (
|
||||||
<BaseEmpty text="No Connections" />
|
<BaseEmpty text="No Connections" />
|
||||||
) : isTableLayout ? (
|
) : isTableLayout ? (
|
||||||
<ConnectionTable connections={filterConn} />
|
<ConnectionTable
|
||||||
|
connections={filterConn}
|
||||||
|
onShowDetail={(detail) => detailRef.current?.open(detail)}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Virtuoso
|
<Virtuoso
|
||||||
data={filterConn}
|
data={filterConn}
|
||||||
itemContent={(index, item) => <ConnectionItem value={item} />}
|
itemContent={(index, item) => (
|
||||||
|
<ConnectionItem
|
||||||
|
value={item}
|
||||||
|
onShowDetail={() => detailRef.current?.open(item)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
<ConnectionDetail ref={detailRef} />
|
||||||
</Paper>
|
</Paper>
|
||||||
</BasePage>
|
</BasePage>
|
||||||
);
|
);
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
const UNITS = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
|
const UNITS = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
|
||||||
|
|
||||||
const parseTraffic = (num: number) => {
|
const parseTraffic = (num?: number) => {
|
||||||
|
if (typeof num !== "number") return ["NaN", ""];
|
||||||
if (num < 1000) return [`${Math.round(num)}`, "B"];
|
if (num < 1000) return [`${Math.round(num)}`, "B"];
|
||||||
const exp = Math.min(Math.floor(Math.log2(num) / 10), UNITS.length - 1);
|
const exp = Math.min(Math.floor(Math.log2(num) / 10), UNITS.length - 1);
|
||||||
const dat = num / Math.pow(1024, exp);
|
const dat = num / Math.pow(1024, exp);
|
||||||
|
6
src/utils/truncate-str.ts
Normal file
6
src/utils/truncate-str.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export const truncateStr = (str?: string, prefixLen = 16, maxLen = 56) => {
|
||||||
|
if (!str || str.length <= maxLen) return str;
|
||||||
|
return (
|
||||||
|
str.slice(0, prefixLen) + " ... " + str.slice(-(maxLen - prefixLen - 5))
|
||||||
|
);
|
||||||
|
};
|
Loading…
x
Reference in New Issue
Block a user