mirror of
https://github.com/idanoo/autobrr
synced 2025-07-23 08:49:13 +00:00
feat(irc): view announces per channel (#948)
* feat(irc): add sse to handler * feat(irc): view and send irc messages per network * refactor(irc): use id as handlerkey * refactor(irc): use id as handlerkey * feat(web): add irc context * refactor: create sse stream per network channel * fix(irc): remove non-working wildcard callback handler * feat: use fork of sse * chore(deps): update ergo/irc-go to v0.3.0 * fix: clean irc msg before sse publish * feat: add view channel button * feat: styling improvements * feat: show time
This commit is contained in:
parent
bbfcf303ef
commit
ccabe96bdf
14 changed files with 446 additions and 125 deletions
|
@ -150,7 +150,9 @@ export const APIClient = {
|
|||
createNetwork: (network: IrcNetworkCreate) => appClient.Post("api/irc", network),
|
||||
updateNetwork: (network: IrcNetwork) => appClient.Put(`api/irc/network/${network.id}`, network),
|
||||
deleteNetwork: (id: number) => appClient.Delete(`api/irc/network/${id}`),
|
||||
restartNetwork: (id: number) => appClient.Get(`api/irc/network/${id}/restart`)
|
||||
restartNetwork: (id: number) => appClient.Get(`api/irc/network/${id}/restart`),
|
||||
sendCmd: (cmd: SendIrcCmdRequest) => appClient.Post(`api/irc/network/${cmd.network_id}/cmd`, cmd),
|
||||
events: (network: string) => new EventSource(`${sseBaseUrl()}api/irc/events?stream=${network}`, { withCredentials: true })
|
||||
},
|
||||
logs: {
|
||||
files: () => appClient.Get<LogFileResponse>("api/logs/files"),
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
* SPDX-License-Identifier: GPL-2.0-or-later
|
||||
*/
|
||||
|
||||
import { Fragment, useRef, useState, useMemo } from "react";
|
||||
import { Fragment, useRef, useState, useMemo, useEffect } from "react";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { LockClosedIcon, LockOpenIcon } from "@heroicons/react/24/solid";
|
||||
import { Menu, Switch, Transition } from "@headlessui/react";
|
||||
|
@ -24,6 +24,8 @@ import { APIClient } from "@api/APIClient";
|
|||
import { EmptySimple } from "@components/emptystates";
|
||||
import { DeleteModal } from "@components/modals";
|
||||
import Toast from "@components/notifications/Toast";
|
||||
import { SettingsContext } from "@utils/Context";
|
||||
import { useForm } from "react-hook-form";
|
||||
|
||||
export const ircKeys = {
|
||||
all: ["irc_networks"] as const,
|
||||
|
@ -235,7 +237,7 @@ const ListItem = ({ idx, network, expanded }: ListItemProps) => {
|
|||
|
||||
return (
|
||||
<li key={idx}>
|
||||
<div className={classNames("grid grid-cols-12 gap-2 lg:gap-4 items-center py-2", network.enabled && !network.healthy ? "bg-red-50 dark:bg-red-900 hover:bg-red-100 dark:hover:bg-red-800" : "hover:bg-gray-50 dark:hover:bg-gray-700 ")}>
|
||||
<div className={classNames("grid grid-cols-12 gap-2 lg:gap-4 items-center py-2", network.enabled && !network.healthy ? "bg-red-50 dark:bg-red-900 hover:bg-red-100 dark:hover:bg-red-800" : "hover:bg-gray-50 dark:hover:bg-gray-700")}>
|
||||
<IrcNetworkUpdateForm
|
||||
isOpen={updateIsOpen}
|
||||
toggle={toggleUpdate}
|
||||
|
@ -344,45 +346,12 @@ const ListItem = ({ idx, network, expanded }: ListItemProps) => {
|
|||
<div className="col-span-4 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
||||
Monitoring since
|
||||
</div>
|
||||
<div className="col-span-4 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
||||
<div className="col-span-3 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
||||
Last announce
|
||||
</div>
|
||||
</li>
|
||||
{network.channels.map((c) => (
|
||||
<li key={c.id} className="text-gray-500 dark:text-gray-400">
|
||||
<div className="grid grid-cols-12 gap-4 items-center py-4">
|
||||
<div className="col-span-4 flex items-center md:px-6 ">
|
||||
<span className="relative inline-flex items-center">
|
||||
{network.enabled ? (
|
||||
c.monitoring ? (
|
||||
<span
|
||||
className="mr-3 flex h-3 w-3 relative"
|
||||
title="monitoring"
|
||||
>
|
||||
<span className="animate-ping inline-flex h-full w-full rounded-full bg-green-400 opacity-75" />
|
||||
<span className="inline-flex absolute rounded-full h-3 w-3 bg-green-500" />
|
||||
</span>
|
||||
) : (
|
||||
<span className="mr-3 flex h-3 w-3 rounded-full opacity-75 bg-red-400" />
|
||||
)
|
||||
) : (
|
||||
<span className="mr-3 flex h-3 w-3 rounded-full opacity-75 bg-gray-500" />
|
||||
)}
|
||||
{c.name}
|
||||
</span>
|
||||
</div>
|
||||
<div className="col-span-4 flex items-center md:px-6 ">
|
||||
<span title={simplifyDate(c.monitoring_since)}>
|
||||
{IsEmptyDate(c.monitoring_since)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="col-span-4 flex items-center md:px-6 ">
|
||||
<span title={simplifyDate(c.last_announce)}>
|
||||
{IsEmptyDate(c.last_announce)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<ChannelItem network={network} channel={c} />
|
||||
))}
|
||||
</ol>
|
||||
) : (
|
||||
|
@ -397,6 +366,59 @@ const ListItem = ({ idx, network, expanded }: ListItemProps) => {
|
|||
);
|
||||
};
|
||||
|
||||
interface ChannelItemProps {
|
||||
network: IrcNetwork;
|
||||
channel: IrcChannelWithHealth;
|
||||
}
|
||||
|
||||
const ChannelItem = ({ network, channel }: ChannelItemProps) => {
|
||||
const [viewChannel, toggleView] = useToggle(false);
|
||||
|
||||
return (
|
||||
<li key={channel.id} className={classNames("mb-2 text-gray-500 dark:text-gray-400", viewChannel ? "bg-gray-200 dark:bg-gray-800 rounded-md" : "")}>
|
||||
<div className="grid grid-cols-12 gap-4 items-center py-4 hover:bg-gray-300 dark:hover:bg-gray-800 hover:cursor-pointer rounded-md" onClick={toggleView}>
|
||||
<div className="col-span-4 flex items-center md:px-6 ">
|
||||
<span className="relative inline-flex items-center">
|
||||
{network.enabled ? (
|
||||
channel.monitoring ? (
|
||||
<span
|
||||
className="mr-3 flex h-3 w-3 relative"
|
||||
title="monitoring"
|
||||
>
|
||||
<span
|
||||
className="animate-ping inline-flex h-full w-full rounded-full bg-green-400 opacity-75"/>
|
||||
<span className="inline-flex absolute rounded-full h-3 w-3 bg-green-500"/>
|
||||
</span>
|
||||
) : (
|
||||
<span className="mr-3 flex h-3 w-3 rounded-full opacity-75 bg-red-400"/>
|
||||
)
|
||||
) : (
|
||||
<span className="mr-3 flex h-3 w-3 rounded-full opacity-75 bg-gray-500"/>
|
||||
)}
|
||||
{channel.name}
|
||||
</span>
|
||||
</div>
|
||||
<div className="col-span-4 flex items-center md:px-6 ">
|
||||
<span title={simplifyDate(channel.monitoring_since)}>
|
||||
{IsEmptyDate(channel.monitoring_since)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="col-span-3 flex items-center md:px-6 ">
|
||||
<span title={simplifyDate(channel.last_announce)}>
|
||||
{IsEmptyDate(channel.last_announce)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="col-span-1 flex items-center">
|
||||
<button className="hover:text-gray-500 px-2 py-1 dark:bg-gray-800 rounded dark:border-gray-900">{viewChannel ? "Hide" : "View"}</button>
|
||||
</div>
|
||||
</div>
|
||||
{viewChannel && (
|
||||
<Events network={network} channel={channel.name}/>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
interface ListItemDropdownProps {
|
||||
network: IrcNetwork;
|
||||
toggleUpdate: () => void;
|
||||
|
@ -557,4 +579,106 @@ const ListItemDropdown = ({
|
|||
);
|
||||
};
|
||||
|
||||
type IrcEvent = {
|
||||
channel: string;
|
||||
nick: string;
|
||||
msg: string;
|
||||
time: string;
|
||||
};
|
||||
|
||||
type IrcMsg = {
|
||||
msg: string;
|
||||
};
|
||||
|
||||
interface EventsProps {
|
||||
network: IrcNetwork;
|
||||
channel: string;
|
||||
}
|
||||
|
||||
export const Events = ({ network, channel }: EventsProps) => {
|
||||
const [settings] = SettingsContext.use();
|
||||
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const [logs, setLogs] = useState<IrcEvent[]>([]);
|
||||
|
||||
// const scrollToBottom = () => {
|
||||
// messagesEndRef.current?.scrollIntoView({ behavior: "smooth", block: "end", inline: "end" });
|
||||
// };
|
||||
|
||||
const { handleSubmit, register , resetField } = useForm<IrcMsg>({
|
||||
defaultValues: { msg: "" },
|
||||
mode: "onBlur"
|
||||
});
|
||||
|
||||
const cmdMutation = useMutation({
|
||||
mutationFn: (data: SendIrcCmdRequest) => APIClient.irc.sendCmd(data),
|
||||
onSuccess: (_, variables) => {
|
||||
resetField("msg");
|
||||
},
|
||||
onError: () => {
|
||||
toast.custom((t) => (
|
||||
<Toast type="error" body="Error sending IRC cmd" t={t} />
|
||||
));
|
||||
}
|
||||
});
|
||||
|
||||
const onSubmit = (msg: IrcMsg) => {
|
||||
const payload = { network_id: network.id, nick: network.nick, server: network.server, channel: channel, msg: msg.msg };
|
||||
cmdMutation.mutate(payload);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const key = `${network.id}${channel.replace("#", "")}`;
|
||||
const es = APIClient.irc.events(key);
|
||||
|
||||
es.onmessage = (event) => {
|
||||
const newData = JSON.parse(event.data) as IrcEvent;
|
||||
setLogs((prevState) => [...prevState, newData]);
|
||||
|
||||
// if (settings.scrollOnNewLog)
|
||||
// scrollToBottom();
|
||||
};
|
||||
|
||||
return () => es.close();
|
||||
}, [settings]);
|
||||
|
||||
return (
|
||||
<div className="dark:bg-gray-800 rounded-lg shadow-lg p-1.5">
|
||||
<div className="flex relative">
|
||||
</div>
|
||||
|
||||
<div className="overflow-y-auto px-2 rounded-lg h-[60vh] min-w-full bg-gray-100 dark:bg-gray-900 overflow-auto">
|
||||
{logs.map((entry, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className={classNames(
|
||||
settings.indentLogLines ? "grid justify-start grid-flow-col" : "",
|
||||
settings.hideWrappedText ? "truncate hover:text-ellipsis hover:whitespace-normal" : ""
|
||||
)}
|
||||
>
|
||||
<span className="font-mono text-gray-500 dark:text-gray-500 mr-1"><span className="dark:text-gray-600" title={simplifyDate(entry.time)}>{entry.nick}:</span> {entry.msg}</span>
|
||||
</div>
|
||||
))}
|
||||
<div className="mt-6" ref={messagesEndRef} />
|
||||
</div>
|
||||
|
||||
{/*<div>*/}
|
||||
{/* <form onSubmit={handleSubmit(onSubmit)}>*/}
|
||||
{/* <input*/}
|
||||
{/* id="msg"*/}
|
||||
{/* {...(register && register("msg"))}*/}
|
||||
{/* type="text"*/}
|
||||
{/* minLength={2}*/}
|
||||
{/* className={classNames(*/}
|
||||
{/* "focus:ring-blue-500 dark:focus:ring-blue-500 focus:border-blue-500 dark:focus:border-blue-500 border-gray-300 dark:border-gray-700",*/}
|
||||
{/* "block w-full dark:bg-gray-900 shadow-sm dark:text-gray-100 sm:text-sm rounded-md"*/}
|
||||
{/* )}*/}
|
||||
{/* />*/}
|
||||
{/* </form>*/}
|
||||
{/*</div>*/}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default IrcSettings;
|
||||
|
|
8
web/src/types/Irc.d.ts
vendored
8
web/src/types/Irc.d.ts
vendored
|
@ -72,3 +72,11 @@ interface IrcAuth {
|
|||
account?: string; // optional
|
||||
password?: string; // optional
|
||||
}
|
||||
|
||||
interface SendIrcCmdRequest {
|
||||
network_id: number;
|
||||
server: string;
|
||||
channel: string;
|
||||
nick: string;
|
||||
msg: string;
|
||||
}
|
||||
|
|
|
@ -28,6 +28,7 @@ export const InitializeGlobalContext = () => {
|
|||
FilterListContext.set(JSON.parse(filterList_ctx));
|
||||
}
|
||||
};
|
||||
|
||||
interface AuthInfo {
|
||||
username: string;
|
||||
isLoggedIn: boolean;
|
||||
|
@ -108,4 +109,39 @@ export const FilterListContext = newRidgeState<FilterListState>(
|
|||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
);
|
||||
|
||||
export type IrcNetworkState = {
|
||||
id: number;
|
||||
name: string;
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type IrcBufferType = "NICK" | "CHANNEL" | "SERVER";
|
||||
|
||||
export type IrcBufferState = {
|
||||
id: number;
|
||||
name: string;
|
||||
type: IrcBufferType;
|
||||
messages: string[];
|
||||
};
|
||||
|
||||
export type IrcState = {
|
||||
networks: Map<string, IrcNetworkState>;
|
||||
buffers: Map<string, IrcBufferState>
|
||||
};
|
||||
export const IrcContext = newRidgeState<IrcState>(
|
||||
{
|
||||
networks: new Map(),
|
||||
buffers: new Map()
|
||||
},
|
||||
{
|
||||
onSet: (new_state) => {
|
||||
try {
|
||||
console.log("set irc state", new_state);
|
||||
} catch (e) {
|
||||
console.log("Error:", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue