Skip to content

Commit

Permalink
feat(logs): new design LogsPage
Browse files Browse the repository at this point in the history
  • Loading branch information
keiko233 committed Jun 7, 2024
1 parent 3cc14dd commit b2fcc55
Show file tree
Hide file tree
Showing 14 changed files with 359 additions and 132 deletions.
12 changes: 9 additions & 3 deletions frontend/interface/ipc/useClashWS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,21 @@ export const useClashWS = () => {
return `${getBaseUrl()}/${path}?${getTokenUrl()}`;
};

const connectionsUrl = useMemo(() => {
const url = useMemo(() => {
if (getClashInfo.data) {
return resolveUrl("connections");
return {
connections: resolveUrl("connections"),
logs: resolveUrl("logs"),
};
}
}, [getClashInfo.data]);

const connections = useWebSocket(connectionsUrl ?? "");
const connections = useWebSocket(url?.connections ?? "");

const logs = useWebSocket(url?.logs ?? "");

return {
connections,
logs,
};
};
6 changes: 6 additions & 0 deletions frontend/interface/service/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,3 +165,9 @@ export namespace Connection {
connections?: Item[];
}
}

export interface LogMessage {
type: string;
time?: string;
payload: string;
}
2 changes: 1 addition & 1 deletion frontend/nyanpasu/src/components/log/log-item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ const LogItem = (props: Props) => {
sx={{
ml: 3.5,
mr: 3.5,
pt: index === 0 ? 8 : 0,
// pt: index === 0 ? 8 : 0,
}}
>
<div>
Expand Down
26 changes: 26 additions & 0 deletions frontend/nyanpasu/src/components/logs/clear-log-button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { atomLogData } from "@/store";
import { Close } from "@mui/icons-material";
import { Tooltip } from "@mui/material";
import { FloatingButton } from "@nyanpasu/ui";
import { useSetAtom } from "jotai";
import { useTranslation } from "react-i18next";

export const ClearLogButton = () => {
const { t } = useTranslation();

const setLogData = useSetAtom(atomLogData);

const onClear = () => {
setLogData([]);
};

return (
<Tooltip title={t("Clear")}>
<FloatingButton onClick={onClear}>
<Close className="!size-8 absolute" />
</FloatingButton>
</Tooltip>
);
};

export default ClearLogButton;
38 changes: 38 additions & 0 deletions frontend/nyanpasu/src/components/logs/log-filter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { FilledInputProps, TextField, alpha, useTheme } from "@mui/material";
import { useTranslation } from "react-i18next";

export interface LogFilterProps {
value: string;
onChange: (value: string) => void;
}

export const LogFilter = ({ value, onChange }: LogFilterProps) => {
const { t } = useTranslation();

const { palette } = useTheme();

const inputProps: Partial<FilledInputProps> = {
sx: {
borderRadius: 7,
backgroundColor: alpha(palette.primary.main, 0.1),

fieldset: {
border: "none",
},
},
};

return (
<TextField
hiddenLabel
autoComplete="off"
spellCheck="false"
value={value}
placeholder={t("Filter conditions")}
onChange={(e) => onChange(e.target.value)}
className="!pb-0"
sx={{ input: { py: 1, fontSize: 14 } }}
InputProps={inputProps}
/>
);
};
22 changes: 22 additions & 0 deletions frontend/nyanpasu/src/components/logs/log-item.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
.item {
:global(.shiki) {
margin-bottom: 0;
background-color: transparent !important;

* {
font-family: var(--item-font);
}

span {
white-space: normal;
}
}

&.dark {
:global(.shiki) {
span {
color: var(--shiki-dark) !important;
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
declare const classNames: {
readonly item: "item";
readonly shiki: "shiki";
readonly dark: "dark";
};
export default classNames;
55 changes: 55 additions & 0 deletions frontend/nyanpasu/src/components/logs/log-item.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { formatAnsi } from "@/utils/shiki";
import { useTheme } from "@mui/material";
import { LogMessage } from "@nyanpasu/interface";
import { useAsyncEffect } from "ahooks";
import { useState } from "react";
import styles from "./log-item.module.scss";
import { classNames } from "@/utils";

export const LogItem = ({ value }: { value: LogMessage }) => {
const { palette } = useTheme();

const [payload, setPayload] = useState(value.payload);

const colorMapping: { [key: string]: string } = {
error: palette.error.main,
warning: palette.warning.main,
info: palette.info.main,
};

useAsyncEffect(async () => {
setPayload(await formatAnsi(value.payload));
}, [value.payload]);

return (
<div className="w-full font-mono p-4 pt-2 pb-0">
<div className="flex gap-2">
<span className="font-thin">{value.time}</span>

<span
className="inline-block font-semibold uppercase"
style={{
color: colorMapping[value.type],
}}
>
{value.type}
</span>
</div>

<div className="text-wrap border-slate-200 border-b pb-2">
<p
className={classNames(
styles.item,
palette.mode === "dark" && styles.dark,
"data",
)}
dangerouslySetInnerHTML={{
__html: payload,
}}
/>
</div>
</div>
);
};

export default LogItem;
54 changes: 54 additions & 0 deletions frontend/nyanpasu/src/components/logs/log-level.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { Button, Menu, MenuItem, alpha, useTheme } from "@mui/material";
import { useState } from "react";

export interface LogLevelProps {
value: string;
onChange: (value: string) => void;
}

export const LogLevel = ({ value, onChange }: LogLevelProps) => {
const { palette } = useTheme();

const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);

const handleClick = (value: string) => {
setAnchorEl(null);
onChange(value);
};

const mapping: { [key: string]: string } = {
all: "ALL",
inf: "INFO",
warn: "WARN",
err: "ERROR",
};

return (
<>
<Button
size="small"
sx={{
textTransform: "none",
backgroundColor: alpha(palette.primary.main, 0.1),
}}
onClick={(e) => setAnchorEl(e.currentTarget)}
>
{mapping[value]}
</Button>

<Menu
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={() => setAnchorEl(null)}
>
{Object.entries(mapping).map(([key, value], index) => {
return (
<MenuItem key={index} onClick={() => handleClick(key)}>
{value}
</MenuItem>
);
})}
</Menu>
</>
);
};
40 changes: 40 additions & 0 deletions frontend/nyanpasu/src/components/logs/log-list.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { LogMessage } from "@nyanpasu/interface";
import { useThrottleFn } from "ahooks";
import { useEffect, useRef } from "react";
import { VList, VListHandle } from "virtua";
import LogItem from "./log-item";

export const LogList = ({ data }: { data: LogMessage[] }) => {
const vListRef = useRef<VListHandle>(null);

const shouldStickToBottom = useRef(true);

const { run: scrollToBottom } = useThrottleFn(
() => {
if (shouldStickToBottom.current) {
setTimeout(() => {
vListRef.current?.scrollToIndex(data.length - 1, {
align: "end",
smooth: true,
});
}, 100);
}
},
{ wait: 100 },
);

useEffect(() => {
scrollToBottom();
}, [data]);

return (
<VList
ref={vListRef}
className="flex flex-col gap-2 p-2 overflow-auto select-text min-h-full"
>
{data.map((item, index) => {
return <LogItem key={index} value={item} />;
})}
</VList>
);
};
37 changes: 37 additions & 0 deletions frontend/nyanpasu/src/components/logs/log-provider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { atomLogData } from "@/store";
import { LogMessage, useClashWS } from "@nyanpasu/interface";
import dayjs from "dayjs";
import { useSetAtom } from "jotai";
import { useEffect } from "react";

const MAX_LOG_NUM = 1000;

const time = dayjs().format("MM-DD HH:mm:ss");

export const LogProvider = () => {
const {
logs: { latestMessage },
} = useClashWS();

const setLogData = useSetAtom(atomLogData);

useEffect(() => {
if (!latestMessage?.data) {
return;
}

const data = JSON.parse(latestMessage?.data) as LogMessage;

setLogData((prev) => {
if (prev.length >= MAX_LOG_NUM) {
prev.shift();
}

return [...prev, { ...data, time }];
});
}, [latestMessage?.data]);

return null;
};

export default LogProvider;
23 changes: 23 additions & 0 deletions frontend/nyanpasu/src/components/logs/log-toggle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { atomEnableLog } from "@/store";
import { IconButton } from "@mui/material";
import { useAtom } from "jotai";
import {
PauseCircleOutlineRounded,
PlayCircleOutlineRounded,
} from "@mui/icons-material";

export const LogToggle = () => {
const [enableLog, setEnableLog] = useAtom(atomEnableLog);

return (
<IconButton
size="small"
color="inherit"
onClick={() => setEnableLog((e) => !e)}
>
{enableLog ? <PauseCircleOutlineRounded /> : <PlayCircleOutlineRounded />}
</IconButton>
);
};

export default LogToggle;
3 changes: 3 additions & 0 deletions frontend/nyanpasu/src/pages/_app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import AnimatedLogo from "@/components/layout/animated-logo";
import { FallbackProps } from "react-error-boundary";
import styles from "./_app.module.scss";
import { Experimental_CssVarsProvider as CssVarsProvider } from "@mui/material/styles";
import LogProvider from "@/components/logs/log-provider";

dayjs.extend(relativeTime);

Expand Down Expand Up @@ -148,6 +149,8 @@ export default function App() {
<SWRConfig value={{ errorRetryCount: 3 }}>
<CssVarsProvider theme={theme}>
<ThemeModeProvider />
<LogProvider />

<Paper
square
elevation={0}
Expand Down
Loading

0 comments on commit b2fcc55

Please sign in to comment.