mirror of
https://github.com/idanoo/autobrr
synced 2025-07-23 16:59:12 +00:00
feat(irc): improve list view (#466)
* feat(irc): add irc status examples * feat(irc): add dropdown menu to list * feat(irc): update heroicons and add expand button * feat(irc): update heroicons and add expand button
This commit is contained in:
parent
f5faf066a9
commit
300418b9f1
34 changed files with 478 additions and 258 deletions
|
@ -19,6 +19,7 @@ type ircService interface {
|
||||||
StoreNetwork(ctx context.Context, network *domain.IrcNetwork) error
|
StoreNetwork(ctx context.Context, network *domain.IrcNetwork) error
|
||||||
UpdateNetwork(ctx context.Context, network *domain.IrcNetwork) error
|
UpdateNetwork(ctx context.Context, network *domain.IrcNetwork) error
|
||||||
StoreChannel(networkID int64, channel *domain.IrcChannel) error
|
StoreChannel(networkID int64, channel *domain.IrcChannel) error
|
||||||
|
RestartNetwork(ctx context.Context, id int64) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type ircHandler struct {
|
type ircHandler struct {
|
||||||
|
@ -38,6 +39,7 @@ func (h ircHandler) Routes(r chi.Router) {
|
||||||
r.Post("/", h.storeNetwork)
|
r.Post("/", h.storeNetwork)
|
||||||
r.Put("/network/{networkID}", h.updateNetwork)
|
r.Put("/network/{networkID}", h.updateNetwork)
|
||||||
r.Post("/network/{networkID}/channel", h.storeChannel)
|
r.Post("/network/{networkID}/channel", h.storeChannel)
|
||||||
|
r.Get("/network/{networkID}/restart", h.restartNetwork)
|
||||||
r.Get("/network/{networkID}", h.getNetworkByID)
|
r.Get("/network/{networkID}", h.getNetworkByID)
|
||||||
r.Delete("/network/{networkID}", h.deleteNetwork)
|
r.Delete("/network/{networkID}", h.deleteNetwork)
|
||||||
}
|
}
|
||||||
|
@ -69,6 +71,21 @@ func (h ircHandler) getNetworkByID(w http.ResponseWriter, r *http.Request) {
|
||||||
h.encoder.StatusResponse(ctx, w, network, http.StatusOK)
|
h.encoder.StatusResponse(ctx, w, network, http.StatusOK)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h ircHandler) restartNetwork(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var (
|
||||||
|
ctx = r.Context()
|
||||||
|
networkID = chi.URLParam(r, "networkID")
|
||||||
|
)
|
||||||
|
|
||||||
|
id, _ := strconv.Atoi(networkID)
|
||||||
|
|
||||||
|
if err := h.service.RestartNetwork(ctx, int64(id)); err != nil {
|
||||||
|
h.encoder.Error(w, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
h.encoder.NoContent(w)
|
||||||
|
}
|
||||||
|
|
||||||
func (h ircHandler) storeNetwork(w http.ResponseWriter, r *http.Request) {
|
func (h ircHandler) storeNetwork(w http.ResponseWriter, r *http.Request) {
|
||||||
var data domain.IrcNetwork
|
var data domain.IrcNetwork
|
||||||
|
|
||||||
|
|
|
@ -20,6 +20,7 @@ type Service interface {
|
||||||
StartHandlers()
|
StartHandlers()
|
||||||
StopHandlers()
|
StopHandlers()
|
||||||
StopNetwork(key handlerKey) error
|
StopNetwork(key handlerKey) error
|
||||||
|
RestartNetwork(ctx context.Context, id int64) error
|
||||||
ListNetworks(ctx context.Context) ([]domain.IrcNetwork, error)
|
ListNetworks(ctx context.Context) ([]domain.IrcNetwork, error)
|
||||||
GetNetworksWithHealth(ctx context.Context) ([]domain.IrcNetworkWithHealth, error)
|
GetNetworksWithHealth(ctx context.Context) ([]domain.IrcNetworkWithHealth, error)
|
||||||
GetNetworkByID(ctx context.Context, id int64) (*domain.IrcNetwork, error)
|
GetNetworkByID(ctx context.Context, id int64) (*domain.IrcNetwork, error)
|
||||||
|
@ -279,6 +280,15 @@ func (s *service) checkIfNetworkRestartNeeded(network *domain.IrcNetwork) error
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *service) RestartNetwork(ctx context.Context, id int64) error {
|
||||||
|
network, err := s.repo.GetNetworkByID(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.restartNetwork(*network)
|
||||||
|
}
|
||||||
|
|
||||||
func (s *service) restartNetwork(network domain.IrcNetwork) error {
|
func (s *service) restartNetwork(network domain.IrcNetwork) error {
|
||||||
// look if we have the network in handlers, if so restart it
|
// look if we have the network in handlers, if so restart it
|
||||||
if existingHandler, found := s.handlers[handlerKey{network.Server, network.NickServ.Account}]; found {
|
if existingHandler, found := s.handlers[handlerKey{network.Server, network.NickServ.Account}]; found {
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fontsource/inter": "^4.5.11",
|
"@fontsource/inter": "^4.5.11",
|
||||||
"@headlessui/react": "^1.6.4",
|
"@headlessui/react": "^1.6.4",
|
||||||
"@heroicons/react": "^1.0.6",
|
"@heroicons/react": "^2.0.11",
|
||||||
"@hookform/error-message": "^2.0.0",
|
"@hookform/error-message": "^2.0.0",
|
||||||
"date-fns": "^2.28.0",
|
"date-fns": "^2.28.0",
|
||||||
"formik": "^2.2.9",
|
"formik": "^2.2.9",
|
||||||
|
|
|
@ -139,7 +139,8 @@ export const APIClient = {
|
||||||
getNetworks: () => appClient.Get<IrcNetworkWithHealth[]>("api/irc"),
|
getNetworks: () => appClient.Get<IrcNetworkWithHealth[]>("api/irc"),
|
||||||
createNetwork: (network: IrcNetworkCreate) => appClient.Post("api/irc", network),
|
createNetwork: (network: IrcNetworkCreate) => appClient.Post("api/irc", network),
|
||||||
updateNetwork: (network: IrcNetwork) => appClient.Put(`api/irc/network/${network.id}`, network),
|
updateNetwork: (network: IrcNetwork) => appClient.Put(`api/irc/network/${network.id}`, network),
|
||||||
deleteNetwork: (id: number) => appClient.Delete(`api/irc/network/${id}`)
|
deleteNetwork: (id: number) => appClient.Delete(`api/irc/network/${id}`),
|
||||||
|
restartNetwork: (id: number) => appClient.Get(`api/irc/network/${id}/restart`)
|
||||||
},
|
},
|
||||||
events: {
|
events: {
|
||||||
logs: () => new EventSource(`${sseBaseUrl()}api/events?stream=logs`, { withCredentials: true })
|
logs: () => new EventSource(`${sseBaseUrl()}api/events?stream=logs`, { withCredentials: true })
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import StackTracey from "stacktracey";
|
import StackTracey from "stacktracey";
|
||||||
import type { FallbackProps } from "react-error-boundary";
|
import type { FallbackProps } from "react-error-boundary";
|
||||||
import { RefreshIcon } from "@heroicons/react/solid";
|
import { ArrowPathIcon } from "@heroicons/react/24/solid";
|
||||||
|
|
||||||
export const ErrorPage = ({ error, resetErrorBoundary }: FallbackProps) => {
|
export const ErrorPage = ({ error, resetErrorBoundary }: FallbackProps) => {
|
||||||
const stack = new StackTracey(error);
|
const stack = new StackTracey(error);
|
||||||
|
@ -73,7 +73,7 @@ export const ErrorPage = ({ error, resetErrorBoundary }: FallbackProps) => {
|
||||||
resetErrorBoundary();
|
resetErrorBoundary();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<RefreshIcon className="-ml-0.5 mr-2 h-5 w-5"/>
|
<ArrowPathIcon className="-ml-0.5 mr-2 h-5 w-5"/>
|
||||||
Reset page state
|
Reset page state
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
|
import { ExclamationTriangleIcon } from "@heroicons/react/24/outline";
|
||||||
import { ExclamationIcon } from "@heroicons/react/solid";
|
|
||||||
|
|
||||||
interface props {
|
interface props {
|
||||||
title?: string;
|
title?: string;
|
||||||
|
@ -11,7 +10,7 @@ export function AlertWarning({ title, text }: props) {
|
||||||
<div className="my-4 rounded-md bg-yellow-50 dark:bg-yellow-100 p-4 border border-yellow-300 dark:border-none">
|
<div className="my-4 rounded-md bg-yellow-50 dark:bg-yellow-100 p-4 border border-yellow-300 dark:border-none">
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<div className="flex-shrink-0">
|
<div className="flex-shrink-0">
|
||||||
<ExclamationIcon
|
<ExclamationTriangleIcon
|
||||||
className="h-5 w-5 text-yellow-400 dark:text-yellow-600"
|
className="h-5 w-5 text-yellow-400 dark:text-yellow-600"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { formatDistanceToNowStrict } from "date-fns";
|
import { formatDistanceToNowStrict } from "date-fns";
|
||||||
import { CheckIcon } from "@heroicons/react/solid";
|
import { CheckIcon } from "@heroicons/react/24/solid";
|
||||||
import { ClockIcon, BanIcon, ExclamationCircleIcon } from "@heroicons/react/outline";
|
import { ClockIcon, ExclamationCircleIcon, NoSymbolIcon } from "@heroicons/react/24/outline";
|
||||||
|
|
||||||
import { classNames, simplifyDate } from "../../utils";
|
import { classNames, simplifyDate } from "../../utils";
|
||||||
import { Tooltip } from "../tooltips/Tooltip";
|
import { Tooltip } from "../tooltips/Tooltip";
|
||||||
|
@ -41,7 +41,7 @@ const StatusCellMap: Record<string, StatusCellMapEntry> = {
|
||||||
},
|
},
|
||||||
"PUSH_REJECTED": {
|
"PUSH_REJECTED": {
|
||||||
colors: "bg-blue-200 dark:bg-blue-100 text-blue-400 dark:text-blue-800 hover:bg-blue-300 dark:hover:bg-blue-400",
|
colors: "bg-blue-200 dark:bg-blue-100 text-blue-400 dark:text-blue-800 hover:bg-blue-300 dark:hover:bg-blue-400",
|
||||||
icon: <BanIcon className="h-5 w-5" aria-hidden="true" />
|
icon: <NoSymbolIcon className="h-5 w-5" aria-hidden="true" />
|
||||||
},
|
},
|
||||||
"PUSH_APPROVED": {
|
"PUSH_APPROVED": {
|
||||||
colors: "bg-green-100 text-green-800 hover:bg-green-300",
|
colors: "bg-green-100 text-green-800 hover:bg-green-300",
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { PlusIcon } from "@heroicons/react/solid";
|
import { PlusIcon } from "@heroicons/react/24/solid";
|
||||||
|
|
||||||
interface EmptySimpleProps {
|
interface EmptySimpleProps {
|
||||||
title: string;
|
title: string;
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { useToggle } from "../../hooks/hooks";
|
import { useToggle } from "../../hooks/hooks";
|
||||||
import { ClipboardCopyIcon, EyeIcon, EyeOffIcon, CheckIcon } from "@heroicons/react/outline";
|
import { CheckIcon, DocumentDuplicateIcon, EyeIcon, EyeSlashIcon } from "@heroicons/react/24/outline";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
interface KeyFieldProps {
|
interface KeyFieldProps {
|
||||||
|
@ -52,7 +52,7 @@ export const KeyField = ({ value }: KeyFieldProps) => {
|
||||||
onClick={toggleVisibility}
|
onClick={toggleVisibility}
|
||||||
title="show"
|
title="show"
|
||||||
>
|
>
|
||||||
{!isVisible ? <EyeIcon className="h-5 w-5 text-gray-400 hover:text-gray-500" aria-hidden="true" /> : <EyeOffIcon className="h-5 w-5 text-gray-400 hover:text-gray-500" aria-hidden="true" />}
|
{!isVisible ? <EyeIcon className="h-5 w-5 text-gray-400 hover:text-gray-500" aria-hidden="true" /> : <EyeSlashIcon className="h-5 w-5 text-gray-400 hover:text-gray-500" aria-hidden="true" />}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
@ -65,7 +65,7 @@ export const KeyField = ({ value }: KeyFieldProps) => {
|
||||||
className="text-blue-500 w-5 h-5"
|
className="text-blue-500 w-5 h-5"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
: <ClipboardCopyIcon
|
: <DocumentDuplicateIcon
|
||||||
className="text-blue-500 w-5 h-5"
|
className="text-blue-500 w-5 h-5"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Field, FieldProps } from "formik";
|
import { Field, FieldProps } from "formik";
|
||||||
import { classNames } from "../../utils";
|
import { classNames } from "../../utils";
|
||||||
import { EyeIcon, EyeOffIcon } from "@heroicons/react/solid";
|
import { EyeIcon, EyeSlashIcon } from "@heroicons/react/24/solid";
|
||||||
import { useToggle } from "../../hooks/hooks";
|
import { useToggle } from "../../hooks/hooks";
|
||||||
|
|
||||||
type COL_WIDTHS = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12;
|
type COL_WIDTHS = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12;
|
||||||
|
@ -24,7 +24,7 @@ export const TextField = ({
|
||||||
columns,
|
columns,
|
||||||
autoComplete,
|
autoComplete,
|
||||||
hidden,
|
hidden,
|
||||||
disabled,
|
disabled
|
||||||
}: TextFieldProps) => (
|
}: TextFieldProps) => (
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
|
@ -52,7 +52,7 @@ export const TextField = ({
|
||||||
className={classNames(
|
className={classNames(
|
||||||
meta.touched && meta.error ? "focus:ring-red-500 focus:border-red-500 border-red-500" : "focus:ring-indigo-500 dark:focus:ring-blue-500 focus:border-indigo-500 dark:focus:border-blue-500 border-gray-300 dark:border-gray-700",
|
meta.touched && meta.error ? "focus:ring-red-500 focus:border-red-500 border-red-500" : "focus:ring-indigo-500 dark:focus:ring-blue-500 focus:border-indigo-500 dark:focus:border-blue-500 border-gray-300 dark:border-gray-700",
|
||||||
disabled ? "bg-gray-100 dark:bg-gray-700 cursor-not-allowed" : "dark:bg-gray-800",
|
disabled ? "bg-gray-100 dark:bg-gray-700 cursor-not-allowed" : "dark:bg-gray-800",
|
||||||
"mt-2 block w-full dark:text-gray-100 rounded-md",
|
"mt-2 block w-full dark:text-gray-100 rounded-md"
|
||||||
)}
|
)}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
|
@ -88,7 +88,7 @@ export const TextArea = ({
|
||||||
rows,
|
rows,
|
||||||
autoComplete,
|
autoComplete,
|
||||||
hidden,
|
hidden,
|
||||||
disabled,
|
disabled
|
||||||
}: TextAreaProps) => (
|
}: TextAreaProps) => (
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
|
@ -103,9 +103,9 @@ export const TextArea = ({
|
||||||
)}
|
)}
|
||||||
<Field name={name}>
|
<Field name={name}>
|
||||||
{({
|
{({
|
||||||
field,
|
field,
|
||||||
meta
|
meta
|
||||||
}: FieldProps) => (
|
}: FieldProps) => (
|
||||||
<div>
|
<div>
|
||||||
<textarea
|
<textarea
|
||||||
{...field}
|
{...field}
|
||||||
|
@ -116,7 +116,7 @@ export const TextArea = ({
|
||||||
className={classNames(
|
className={classNames(
|
||||||
meta.touched && meta.error ? "focus:ring-red-500 focus:border-red-500 border-red-500" : "focus:ring-indigo-500 dark:focus:ring-blue-500 focus:border-indigo-500 dark:focus:border-blue-500 border-gray-300 dark:border-gray-700",
|
meta.touched && meta.error ? "focus:ring-red-500 focus:border-red-500 border-red-500" : "focus:ring-indigo-500 dark:focus:ring-blue-500 focus:border-indigo-500 dark:focus:border-blue-500 border-gray-300 dark:border-gray-700",
|
||||||
disabled ? "bg-gray-100 dark:bg-gray-700 cursor-not-allowed" : "dark:bg-gray-800",
|
disabled ? "bg-gray-100 dark:bg-gray-700 cursor-not-allowed" : "dark:bg-gray-800",
|
||||||
"mt-2 block w-full dark:text-gray-100 rounded-md",
|
"mt-2 block w-full dark:text-gray-100 rounded-md"
|
||||||
)}
|
)}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
@ -181,7 +181,7 @@ export const PasswordField = ({
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="absolute inset-y-0 right-0 px-3 flex items-center" onClick={toggleVisibility}>
|
<div className="absolute inset-y-0 right-0 px-3 flex items-center" onClick={toggleVisibility}>
|
||||||
{!isVisible ? <EyeIcon className="h-5 w-5 text-gray-400 hover:text-gray-500" aria-hidden="true" /> : <EyeOffIcon className="h-5 w-5 text-gray-400 hover:text-gray-500" aria-hidden="true" />}
|
{!isVisible ? <EyeIcon className="h-5 w-5 text-gray-400 hover:text-gray-500" aria-hidden="true" /> : <EyeSlashIcon className="h-5 w-5 text-gray-400 hover:text-gray-500" aria-hidden="true" />}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{help && (
|
{help && (
|
||||||
|
@ -211,7 +211,7 @@ export const NumberField = ({
|
||||||
label,
|
label,
|
||||||
placeholder,
|
placeholder,
|
||||||
step,
|
step,
|
||||||
disabled,
|
disabled
|
||||||
}: NumberFieldProps) => (
|
}: NumberFieldProps) => (
|
||||||
<div className="col-span-12 sm:col-span-6">
|
<div className="col-span-12 sm:col-span-6">
|
||||||
<label htmlFor={name} className="block text-xs font-bold text-gray-700 dark:text-gray-200 uppercase tracking-wide">
|
<label htmlFor={name} className="block text-xs font-bold text-gray-700 dark:text-gray-200 uppercase tracking-wide">
|
||||||
|
@ -233,7 +233,7 @@ export const NumberField = ({
|
||||||
? "focus:ring-red-500 focus:border-red-500 border-red-500"
|
? "focus:ring-red-500 focus:border-red-500 border-red-500"
|
||||||
: "focus:ring-indigo-500 dark:focus:ring-blue-500 focus:border-indigo-500 dark:focus:border-blue-500 border-gray-300",
|
: "focus:ring-indigo-500 dark:focus:ring-blue-500 focus:border-indigo-500 dark:focus:border-blue-500 border-gray-300",
|
||||||
"mt-2 block w-full border border-gray-300 dark:border-gray-700 dark:text-gray-100 rounded-md",
|
"mt-2 block w-full border border-gray-300 dark:border-gray-700 dark:text-gray-100 rounded-md",
|
||||||
disabled ? "bg-gray-100 dark:bg-gray-700 cursor-not-allowed" : "dark:bg-gray-800",
|
disabled ? "bg-gray-100 dark:bg-gray-700 cursor-not-allowed" : "dark:bg-gray-800"
|
||||||
)}
|
)}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { Field } from "formik";
|
|
||||||
import type { FieldProps } from "formik";
|
import type { FieldProps } from "formik";
|
||||||
|
import { Field } from "formik";
|
||||||
import { classNames } from "../../utils";
|
import { classNames } from "../../utils";
|
||||||
import { useToggle } from "../../hooks/hooks";
|
import { useToggle } from "../../hooks/hooks";
|
||||||
import { EyeIcon, EyeOffIcon } from "@heroicons/react/solid";
|
import { EyeIcon, EyeSlashIcon } from "@heroicons/react/24/solid";
|
||||||
import { Switch } from "@headlessui/react";
|
import { Switch } from "@headlessui/react";
|
||||||
import { ErrorField } from "./common";
|
import { ErrorField } from "./common";
|
||||||
|
|
||||||
|
@ -103,7 +103,7 @@ export const PasswordFieldWide = ({
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
/>
|
/>
|
||||||
<div className="absolute inset-y-0 right-0 px-3 flex items-center" onClick={toggleVisibility}>
|
<div className="absolute inset-y-0 right-0 px-3 flex items-center" onClick={toggleVisibility}>
|
||||||
{!isVisible ? <EyeIcon className="h-5 w-5 text-gray-400 hover:text-gray-500" aria-hidden="true" /> : <EyeOffIcon className="h-5 w-5 text-gray-400 hover:text-gray-500" aria-hidden="true" />}
|
{!isVisible ? <EyeIcon className="h-5 w-5 text-gray-400 hover:text-gray-500" aria-hidden="true" /> : <EyeSlashIcon className="h-5 w-5 text-gray-400 hover:text-gray-500" aria-hidden="true" />}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { Fragment } from "react";
|
import { Fragment } from "react";
|
||||||
import { Field, FieldProps } from "formik";
|
import { Field, FieldProps } from "formik";
|
||||||
import { Transition, Listbox } from "@headlessui/react";
|
import { Listbox, Transition } from "@headlessui/react";
|
||||||
import { CheckIcon, SelectorIcon } from "@heroicons/react/solid";
|
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/react/24/solid";
|
||||||
import { MultiSelect as RMSC } from "react-multi-select-component";
|
import { MultiSelect as RMSC } from "react-multi-select-component";
|
||||||
|
|
||||||
import { classNames, COL_WIDTHS } from "../../utils";
|
import { classNames, COL_WIDTHS } from "../../utils";
|
||||||
import { SettingsContext } from "../../utils/Context";
|
import { SettingsContext } from "../../utils/Context";
|
||||||
|
@ -162,7 +162,7 @@ export function DownloadClientSelect({
|
||||||
: "Choose a client"}
|
: "Choose a client"}
|
||||||
</span>
|
</span>
|
||||||
<span className="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
|
<span className="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
|
||||||
<SelectorIcon
|
<ChevronUpDownIcon
|
||||||
className="h-5 w-5 text-gray-400 dark:text-gray-300"
|
className="h-5 w-5 text-gray-400 dark:text-gray-300"
|
||||||
aria-hidden="true" />
|
aria-hidden="true" />
|
||||||
</span>
|
</span>
|
||||||
|
@ -274,7 +274,7 @@ export const Select = ({
|
||||||
}
|
}
|
||||||
</span>
|
</span>
|
||||||
<span className="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
|
<span className="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
|
||||||
<SelectorIcon
|
<ChevronUpDownIcon
|
||||||
className="h-5 w-5 text-gray-400 dark:text-gray-300"
|
className="h-5 w-5 text-gray-400 dark:text-gray-300"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
|
@ -379,7 +379,7 @@ export const SelectWide = ({
|
||||||
}
|
}
|
||||||
</span>
|
</span>
|
||||||
<span className="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
|
<span className="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
|
||||||
<SelectorIcon
|
<ChevronUpDownIcon
|
||||||
className="h-5 w-5 text-gray-400 dark:text-gray-300"
|
className="h-5 w-5 text-gray-400 dark:text-gray-300"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,17 +1,8 @@
|
||||||
import React, {
|
import React, { FC, forwardRef, ReactNode } from "react";
|
||||||
FC,
|
import { DeepMap, FieldError, Path, RegisterOptions, UseFormRegister } from "react-hook-form";
|
||||||
forwardRef,
|
|
||||||
ReactNode
|
|
||||||
} from "react";
|
|
||||||
import {
|
|
||||||
FieldError,
|
|
||||||
UseFormRegister,
|
|
||||||
Path,
|
|
||||||
RegisterOptions, DeepMap
|
|
||||||
} from "react-hook-form";
|
|
||||||
import { classNames, get } from "../../utils";
|
import { classNames, get } from "../../utils";
|
||||||
import { useToggle } from "../../hooks/hooks";
|
import { useToggle } from "../../hooks/hooks";
|
||||||
import { EyeIcon, EyeOffIcon } from "@heroicons/react/solid";
|
import { EyeIcon, EyeSlashIcon } from "@heroicons/react/24/solid";
|
||||||
import { ErrorMessage } from "@hookform/error-message";
|
import { ErrorMessage } from "@hookform/error-message";
|
||||||
|
|
||||||
export type FormErrorMessageProps = {
|
export type FormErrorMessageProps = {
|
||||||
|
@ -183,7 +174,7 @@ export const PasswordInput = <TFormValues extends Record<string, unknown>>({
|
||||||
{...(register && register(name, rules))}
|
{...(register && register(name, rules))}
|
||||||
/>
|
/>
|
||||||
<div className="absolute inset-y-0 right-0 px-3 flex items-center" onClick={toggleVisibility}>
|
<div className="absolute inset-y-0 right-0 px-3 flex items-center" onClick={toggleVisibility}>
|
||||||
{!isVisible ? <EyeIcon className="h-5 w-5 text-gray-400 hover:text-gray-500" aria-hidden="true" /> : <EyeOffIcon className="h-5 w-5 text-gray-400 hover:text-gray-500" aria-hidden="true" />}
|
{!isVisible ? <EyeIcon className="h-5 w-5 text-gray-400 hover:text-gray-500" aria-hidden="true" /> : <EyeSlashIcon className="h-5 w-5 text-gray-400 hover:text-gray-500" aria-hidden="true" />}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ErrorMessage
|
<ErrorMessage
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import React, { Fragment, FC } from "react";
|
import React, { FC, Fragment } from "react";
|
||||||
import { Dialog, Transition } from "@headlessui/react";
|
import { Dialog, Transition } from "@headlessui/react";
|
||||||
import { ExclamationIcon } from "@heroicons/react/solid";
|
import { ExclamationTriangleIcon } from "@heroicons/react/24/solid";
|
||||||
|
|
||||||
interface DeleteModalProps {
|
interface DeleteModalProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
|
@ -49,7 +49,7 @@ export const DeleteModal: FC<DeleteModalProps> = ({ isOpen, buttonRef, toggle, d
|
||||||
<div className="inline-block align-bottom rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full">
|
<div className="inline-block align-bottom rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full">
|
||||||
<div className="bg-white dark:bg-gray-800 px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
|
<div className="bg-white dark:bg-gray-800 px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
|
||||||
<div className="sm:flex sm:items-start">
|
<div className="sm:flex sm:items-start">
|
||||||
<ExclamationIcon className="h-16 w-16 text-red-500 dark:text-red-500" aria-hidden="true" />
|
<ExclamationTriangleIcon className="h-16 w-16 text-red-500 dark:text-red-500" aria-hidden="true" />
|
||||||
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
|
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
|
||||||
<Dialog.Title as="h3" className="text-lg leading-6 font-medium text-gray-900 dark:text-white">
|
<Dialog.Title as="h3" className="text-lg leading-6 font-medium text-gray-900 dark:text-white">
|
||||||
{title}
|
{title}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { FC } from "react";
|
import { FC } from "react";
|
||||||
import { XIcon, CheckCircleIcon, ExclamationIcon, ExclamationCircleIcon } from "@heroicons/react/solid";
|
import { CheckCircleIcon, ExclamationCircleIcon, ExclamationTriangleIcon, XMarkIcon } from "@heroicons/react/24/solid";
|
||||||
import { toast, Toast as Tooast } from "react-hot-toast";
|
import { toast, Toast as Tooast } from "react-hot-toast";
|
||||||
import { classNames } from "../../utils";
|
import { classNames } from "../../utils";
|
||||||
|
|
||||||
|
@ -18,7 +18,7 @@ const Toast: FC<Props> = ({ type, body, t }) => (
|
||||||
<div className="flex-shrink-0">
|
<div className="flex-shrink-0">
|
||||||
{type === "success" && <CheckCircleIcon className="h-6 w-6 text-green-400" aria-hidden="true" />}
|
{type === "success" && <CheckCircleIcon className="h-6 w-6 text-green-400" aria-hidden="true" />}
|
||||||
{type === "error" && <ExclamationCircleIcon className="h-6 w-6 text-red-400" aria-hidden="true" />}
|
{type === "error" && <ExclamationCircleIcon className="h-6 w-6 text-red-400" aria-hidden="true" />}
|
||||||
{type === "warning" && <ExclamationIcon className="h-6 w-6 text-yellow-400" aria-hidden="true" />}
|
{type === "warning" && <ExclamationTriangleIcon className="h-6 w-6 text-yellow-400" aria-hidden="true" />}
|
||||||
</div>
|
</div>
|
||||||
<div className="ml-3 w-0 flex-1 pt-0.5">
|
<div className="ml-3 w-0 flex-1 pt-0.5">
|
||||||
<p className="text-sm font-medium text-gray-900 dark:text-gray-200">
|
<p className="text-sm font-medium text-gray-900 dark:text-gray-200">
|
||||||
|
@ -36,7 +36,7 @@ const Toast: FC<Props> = ({ type, body, t }) => (
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span className="sr-only">Close</span>
|
<span className="sr-only">Close</span>
|
||||||
<XIcon className="h-5 w-5" aria-hidden="true" />
|
<XMarkIcon className="h-5 w-5" aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import React, { Fragment, useRef } from "react";
|
import React, {Fragment, useRef} from "react";
|
||||||
import { XIcon } from "@heroicons/react/solid";
|
import {XMarkIcon} from "@heroicons/react/24/solid";
|
||||||
import { Dialog, Transition } from "@headlessui/react";
|
import {Dialog, Transition} from "@headlessui/react";
|
||||||
import { Form, Formik } from "formik";
|
import {Form, Formik} from "formik";
|
||||||
import DEBUG from "../debug";
|
import DEBUG from "../debug";
|
||||||
import { useToggle } from "../../hooks/hooks";
|
import {useToggle} from "../../hooks/hooks";
|
||||||
import { DeleteModal } from "../modals";
|
import {DeleteModal} from "../modals";
|
||||||
import { classNames } from "../../utils";
|
import {classNames} from "../../utils";
|
||||||
|
|
||||||
interface SlideOverProps<DataType> {
|
interface SlideOverProps<DataType> {
|
||||||
title: string;
|
title: string;
|
||||||
|
@ -101,7 +101,7 @@ function SlideOver<DataType>({
|
||||||
onClick={toggle}
|
onClick={toggle}
|
||||||
>
|
>
|
||||||
<span className="sr-only">Close panel</span>
|
<span className="sr-only">Close panel</span>
|
||||||
<XIcon className="h-6 w-6" aria-hidden="true" />
|
<XMarkIcon className="h-6 w-6" aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import { Fragment } from "react";
|
import { Fragment } from "react";
|
||||||
import { useMutation } from "react-query";
|
import { useMutation } from "react-query";
|
||||||
import { toast } from "react-hot-toast";
|
import { toast } from "react-hot-toast";
|
||||||
import { XIcon } from "@heroicons/react/solid";
|
import { XMarkIcon } from "@heroicons/react/24/solid";
|
||||||
import { Dialog, Transition } from "@headlessui/react";
|
import { Dialog, Transition } from "@headlessui/react";
|
||||||
import { Field, Form, Formik, FormikErrors, FormikValues } from "formik";
|
|
||||||
import type { FieldProps } from "formik";
|
import type { FieldProps } from "formik";
|
||||||
|
import { Field, Form, Formik, FormikErrors, FormikValues } from "formik";
|
||||||
|
|
||||||
import { queryClient } from "../../App";
|
import { queryClient } from "../../App";
|
||||||
import { APIClient } from "../../api/APIClient";
|
import { APIClient } from "../../api/APIClient";
|
||||||
|
@ -87,7 +87,7 @@ function FilterAddForm({ isOpen, toggle }: filterAddFormProps) {
|
||||||
onClick={toggle}
|
onClick={toggle}
|
||||||
>
|
>
|
||||||
<span className="sr-only">Close panel</span>
|
<span className="sr-only">Close panel</span>
|
||||||
<XIcon className="h-6 w-6" aria-hidden="true" />
|
<XMarkIcon className="h-6 w-6" aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { Fragment } from "react";
|
import { Fragment } from "react";
|
||||||
import { useMutation } from "react-query";
|
import { useMutation } from "react-query";
|
||||||
import { toast } from "react-hot-toast";
|
import { toast } from "react-hot-toast";
|
||||||
import { XIcon } from "@heroicons/react/solid";
|
import { XMarkIcon } from "@heroicons/react/24/solid";
|
||||||
import { Dialog, Transition } from "@headlessui/react";
|
import { Dialog, Transition } from "@headlessui/react";
|
||||||
import type { FieldProps } from "formik";
|
import type { FieldProps } from "formik";
|
||||||
import { Field, Form, Formik, FormikErrors, FormikValues } from "formik";
|
import { Field, Form, Formik, FormikErrors, FormikValues } from "formik";
|
||||||
|
@ -83,7 +83,7 @@ function APIKeyAddForm({ isOpen, toggle }: apiKeyAddFormProps) {
|
||||||
onClick={toggle}
|
onClick={toggle}
|
||||||
>
|
>
|
||||||
<span className="sr-only">Close panel</span>
|
<span className="sr-only">Close panel</span>
|
||||||
<XIcon className="h-6 w-6" aria-hidden="true"/>
|
<XMarkIcon className="h-6 w-6" aria-hidden="true"/>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import React, { Fragment, useRef, useState } from "react";
|
import React, { Fragment, useRef, useState } from "react";
|
||||||
import { useMutation } from "react-query";
|
import { useMutation } from "react-query";
|
||||||
import { Dialog, Transition } from "@headlessui/react";
|
import { Dialog, Transition } from "@headlessui/react";
|
||||||
import { XIcon } from "@heroicons/react/solid";
|
import { XMarkIcon } from "@heroicons/react/24/solid";
|
||||||
import { classNames, sleep } from "../../utils";
|
import { classNames, sleep } from "../../utils";
|
||||||
import { Form, Formik, useFormikContext } from "formik";
|
import { Form, Formik, useFormikContext } from "formik";
|
||||||
import DEBUG from "../../components/debug";
|
import DEBUG from "../../components/debug";
|
||||||
|
@ -13,7 +13,13 @@ import { toast } from "react-hot-toast";
|
||||||
import Toast from "../../components/notifications/Toast";
|
import Toast from "../../components/notifications/Toast";
|
||||||
import { useToggle } from "../../hooks/hooks";
|
import { useToggle } from "../../hooks/hooks";
|
||||||
import { DeleteModal } from "../../components/modals";
|
import { DeleteModal } from "../../components/modals";
|
||||||
import { NumberFieldWide, PasswordFieldWide, SwitchGroupWide, TextFieldWide, RadioFieldsetWide } from "../../components/inputs";
|
import {
|
||||||
|
NumberFieldWide,
|
||||||
|
PasswordFieldWide,
|
||||||
|
RadioFieldsetWide,
|
||||||
|
SwitchGroupWide,
|
||||||
|
TextFieldWide
|
||||||
|
} from "../../components/inputs";
|
||||||
import DownloadClient from "../../screens/settings/DownloadClient";
|
import DownloadClient from "../../screens/settings/DownloadClient";
|
||||||
|
|
||||||
interface InitialValuesSettings {
|
interface InitialValuesSettings {
|
||||||
|
@ -511,7 +517,7 @@ export function DownloadClientAddForm({ isOpen, toggle }: formProps) {
|
||||||
onClick={toggle}
|
onClick={toggle}
|
||||||
>
|
>
|
||||||
<span className="sr-only">Close panel</span>
|
<span className="sr-only">Close panel</span>
|
||||||
<XIcon
|
<XMarkIcon
|
||||||
className="h-6 w-6"
|
className="h-6 w-6"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
|
@ -710,7 +716,7 @@ export function DownloadClientUpdateForm({ client, isOpen, toggle }: updateFormP
|
||||||
onClick={toggle}
|
onClick={toggle}
|
||||||
>
|
>
|
||||||
<span className="sr-only">Close panel</span>
|
<span className="sr-only">Close panel</span>
|
||||||
<XIcon
|
<XMarkIcon
|
||||||
className="h-6 w-6"
|
className="h-6 w-6"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -5,7 +5,7 @@ import Select, { components, ControlProps, InputProps, MenuProps, OptionProps }
|
||||||
import type { FieldProps } from "formik";
|
import type { FieldProps } from "formik";
|
||||||
import { Field, Form, Formik, FormikValues } from "formik";
|
import { Field, Form, Formik, FormikValues } from "formik";
|
||||||
|
|
||||||
import { XIcon } from "@heroicons/react/solid";
|
import { XMarkIcon } from "@heroicons/react/24/solid";
|
||||||
import { Dialog, Transition } from "@headlessui/react";
|
import { Dialog, Transition } from "@headlessui/react";
|
||||||
|
|
||||||
import { sleep, slugify } from "../../utils";
|
import { sleep, slugify } from "../../utils";
|
||||||
|
@ -381,7 +381,7 @@ export function IndexerAddForm({ isOpen, toggle }: AddProps) {
|
||||||
onClick={toggle}
|
onClick={toggle}
|
||||||
>
|
>
|
||||||
<span className="sr-only">Close panel</span>
|
<span className="sr-only">Close panel</span>
|
||||||
<XIcon className="h-6 w-6" aria-hidden="true" />
|
<XMarkIcon className="h-6 w-6" aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,20 +1,16 @@
|
||||||
import { useMutation } from "react-query";
|
import {useMutation} from "react-query";
|
||||||
import { toast } from "react-hot-toast";
|
import {toast} from "react-hot-toast";
|
||||||
import { XIcon } from "@heroicons/react/solid";
|
import {XMarkIcon} from "@heroicons/react/24/solid";
|
||||||
import { Field, FieldArray, FormikErrors, FormikValues } from "formik";
|
import type {FieldProps} from "formik";
|
||||||
import type { FieldProps } from "formik";
|
import {Field, FieldArray, FormikErrors, FormikValues} from "formik";
|
||||||
|
|
||||||
import { queryClient } from "../../App";
|
import {queryClient} from "../../App";
|
||||||
import { APIClient } from "../../api/APIClient";
|
import {APIClient} from "../../api/APIClient";
|
||||||
|
|
||||||
import {
|
import {NumberFieldWide, PasswordFieldWide, SwitchGroupWide, TextFieldWide} from "../../components/inputs";
|
||||||
TextFieldWide,
|
import {SlideOver} from "../../components/panels";
|
||||||
PasswordFieldWide,
|
|
||||||
SwitchGroupWide,
|
|
||||||
NumberFieldWide
|
|
||||||
} from "../../components/inputs";
|
|
||||||
import { SlideOver } from "../../components/panels";
|
|
||||||
import Toast from "../../components/notifications/Toast";
|
import Toast from "../../components/notifications/Toast";
|
||||||
|
import {ExclamationTriangleIcon} from "@heroicons/react/24/outline";
|
||||||
|
|
||||||
interface ChannelsFieldArrayProps {
|
interface ChannelsFieldArrayProps {
|
||||||
channels: IrcChannel[];
|
channels: IrcChannel[];
|
||||||
|
@ -62,7 +58,7 @@ const ChannelsFieldArray = ({ channels }: ChannelsFieldArrayProps) => (
|
||||||
onClick={() => remove(index)}
|
onClick={() => remove(index)}
|
||||||
>
|
>
|
||||||
<span className="sr-only">Remove</span>
|
<span className="sr-only">Remove</span>
|
||||||
<XIcon className="h-6 w-6" aria-hidden="true" />
|
<XMarkIcon className="h-6 w-6" aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
|
@ -160,6 +156,8 @@ export function IrcNetworkAddForm({ isOpen, toggle }: AddFormProps) {
|
||||||
>
|
>
|
||||||
{(values) => (
|
{(values) => (
|
||||||
<div className="flex flex-col space-y-4 px-1 py-6 sm:py-0 sm:space-y-0">
|
<div className="flex flex-col space-y-4 px-1 py-6 sm:py-0 sm:space-y-0">
|
||||||
|
<div className="flex justify-center dark:bg-red-300 text-sm font-bold text-center p-4 py-8 dark:text-red-800"><span className="flex"><ExclamationTriangleIcon className="mr-2 h-6 w-6" /> ADD NETWORKS VIA INDEXERS! ONLY USE THIS IS YOU DELETED NETWORKS</span></div>
|
||||||
|
|
||||||
<TextFieldWide
|
<TextFieldWide
|
||||||
name="name"
|
name="name"
|
||||||
label="Name"
|
label="Name"
|
||||||
|
|
|
@ -1,19 +1,19 @@
|
||||||
import { Dialog, Transition } from "@headlessui/react";
|
import {Dialog, Transition} from "@headlessui/react";
|
||||||
import { Fragment } from "react";
|
import {Fragment} from "react";
|
||||||
import type { FieldProps } from "formik";
|
import type {FieldProps} from "formik";
|
||||||
import { Field, Form, Formik, FormikErrors, FormikValues } from "formik";
|
import {Field, Form, Formik, FormikErrors, FormikValues} from "formik";
|
||||||
import { XIcon } from "@heroicons/react/solid";
|
import {XMarkIcon} from "@heroicons/react/24/solid";
|
||||||
import Select, { components, ControlProps, InputProps, MenuProps, OptionProps } from "react-select";
|
import Select, {components, ControlProps, InputProps, MenuProps, OptionProps} from "react-select";
|
||||||
import { PasswordFieldWide, SwitchGroupWide, TextFieldWide } from "../../components/inputs";
|
import {PasswordFieldWide, SwitchGroupWide, TextFieldWide} from "../../components/inputs";
|
||||||
import DEBUG from "../../components/debug";
|
import DEBUG from "../../components/debug";
|
||||||
import { EventOptions, NotificationTypeOptions, SelectOption } from "../../domain/constants";
|
import {EventOptions, NotificationTypeOptions, SelectOption} from "../../domain/constants";
|
||||||
import { useMutation } from "react-query";
|
import {useMutation} from "react-query";
|
||||||
import { APIClient } from "../../api/APIClient";
|
import {APIClient} from "../../api/APIClient";
|
||||||
import { queryClient } from "../../App";
|
import {queryClient} from "../../App";
|
||||||
import { toast } from "react-hot-toast";
|
import {toast} from "react-hot-toast";
|
||||||
import Toast from "../../components/notifications/Toast";
|
import Toast from "../../components/notifications/Toast";
|
||||||
import { SlideOver } from "../../components/panels";
|
import {SlideOver} from "../../components/panels";
|
||||||
import { componentMapType } from "./DownloadClientForms";
|
import {componentMapType} from "./DownloadClientForms";
|
||||||
|
|
||||||
const Input = (props: InputProps) => {
|
const Input = (props: InputProps) => {
|
||||||
return (
|
return (
|
||||||
|
@ -230,7 +230,7 @@ export function NotificationAddForm({ isOpen, toggle }: AddProps) {
|
||||||
onClick={toggle}
|
onClick={toggle}
|
||||||
>
|
>
|
||||||
<span className="sr-only">Close panel</span>
|
<span className="sr-only">Close panel</span>
|
||||||
<XIcon className="h-6 w-6" aria-hidden="true" />
|
<XMarkIcon className="h-6 w-6" aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { Fragment } from "react";
|
import { Fragment } from "react";
|
||||||
import { Link, NavLink, Outlet } from "react-router-dom";
|
import { Link, NavLink, Outlet } from "react-router-dom";
|
||||||
import { Disclosure, Menu, Transition } from "@headlessui/react";
|
import { Disclosure, Menu, Transition } from "@headlessui/react";
|
||||||
import { ExternalLinkIcon } from "@heroicons/react/solid";
|
import { ArrowTopRightOnSquareIcon } from "@heroicons/react/24/solid";
|
||||||
import { ChevronDownIcon, MenuIcon, XIcon } from "@heroicons/react/outline";
|
import { Bars3Icon, ChevronDownIcon, XMarkIcon } from "@heroicons/react/24/outline";
|
||||||
|
|
||||||
import { AuthContext } from "../utils/Context";
|
import { AuthContext } from "../utils/Context";
|
||||||
|
|
||||||
|
@ -76,7 +76,7 @@ export default function Base() {
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
Docs
|
Docs
|
||||||
<ExternalLinkIcon className="inline ml-1 h-5 w-5"
|
<ArrowTopRightOnSquareIcon className="inline ml-1 h-5 w-5"
|
||||||
aria-hidden="true"/>
|
aria-hidden="true"/>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
@ -157,9 +157,9 @@ export default function Base() {
|
||||||
className="bg-gray-200 dark:bg-gray-800 inline-flex items-center justify-center p-2 rounded-md text-gray-600 dark:text-gray-400 hover:text-white hover:bg-gray-700">
|
className="bg-gray-200 dark:bg-gray-800 inline-flex items-center justify-center p-2 rounded-md text-gray-600 dark:text-gray-400 hover:text-white hover:bg-gray-700">
|
||||||
<span className="sr-only">Open main menu</span>
|
<span className="sr-only">Open main menu</span>
|
||||||
{open ? (
|
{open ? (
|
||||||
<XIcon className="block h-6 w-6" aria-hidden="true"/>
|
<XMarkIcon className="block h-6 w-6" aria-hidden="true"/>
|
||||||
) : (
|
) : (
|
||||||
<MenuIcon className="block h-6 w-6" aria-hidden="true"/>
|
<Bars3Icon className="block h-6 w-6" aria-hidden="true"/>
|
||||||
)}
|
)}
|
||||||
</Disclosure.Button>
|
</Disclosure.Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
import { useEffect, useRef, useState } from "react";
|
import {useEffect, useRef, useState} from "react";
|
||||||
import { ExclamationIcon } from "@heroicons/react/solid";
|
import {ExclamationTriangleIcon} from "@heroicons/react/24/solid";
|
||||||
import format from "date-fns/format";
|
import format from "date-fns/format";
|
||||||
import { DebounceInput } from "react-debounce-input";
|
import {DebounceInput} from "react-debounce-input";
|
||||||
|
|
||||||
import { APIClient } from "../api/APIClient";
|
import {APIClient} from "../api/APIClient";
|
||||||
import { Checkbox } from "../components/Checkbox";
|
import {Checkbox} from "../components/Checkbox";
|
||||||
import { classNames } from "../utils";
|
import {classNames} from "../utils";
|
||||||
import { SettingsContext } from "../utils/Context";
|
import {SettingsContext} from "../utils/Context";
|
||||||
|
|
||||||
type LogEvent = {
|
type LogEvent = {
|
||||||
time: string;
|
time: string;
|
||||||
|
@ -79,7 +79,7 @@ export const Logs = () => {
|
||||||
<div className="max-w-screen-xl mx-auto px-4 sm:px-6 lg:px-8">
|
<div className="max-w-screen-xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
<h1 className="text-3xl font-bold text-black dark:text-white">Logs</h1>
|
<h1 className="text-3xl font-bold text-black dark:text-white">Logs</h1>
|
||||||
<div className="flex justify-center">
|
<div className="flex justify-center">
|
||||||
<ExclamationIcon
|
<ExclamationTriangleIcon
|
||||||
className="h-5 w-5 text-yellow-400"
|
className="h-5 w-5 text-yellow-400"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
import { NavLink, Outlet, useLocation } from "react-router-dom";
|
import {NavLink, Outlet, useLocation} from "react-router-dom";
|
||||||
import {
|
import {
|
||||||
BellIcon,
|
BellIcon,
|
||||||
ChatAlt2Icon,
|
ChatBubbleLeftRightIcon,
|
||||||
CogIcon,
|
CogIcon,
|
||||||
CollectionIcon,
|
FolderArrowDownIcon,
|
||||||
DownloadIcon,
|
|
||||||
KeyIcon,
|
KeyIcon,
|
||||||
|
RectangleStackIcon,
|
||||||
RssIcon
|
RssIcon
|
||||||
} from "@heroicons/react/outline";
|
} from "@heroicons/react/24/outline";
|
||||||
|
|
||||||
import { classNames } from "../utils";
|
import {classNames} from "../utils";
|
||||||
|
|
||||||
interface NavTabType {
|
interface NavTabType {
|
||||||
name: string;
|
name: string;
|
||||||
|
@ -20,12 +20,12 @@ interface NavTabType {
|
||||||
const subNavigation: NavTabType[] = [
|
const subNavigation: NavTabType[] = [
|
||||||
{ name: "Application", href: "", icon: CogIcon },
|
{ name: "Application", href: "", icon: CogIcon },
|
||||||
{ name: "Indexers", href: "indexers", icon: KeyIcon },
|
{ name: "Indexers", href: "indexers", icon: KeyIcon },
|
||||||
{ name: "IRC", href: "irc", icon: ChatAlt2Icon },
|
{ name: "IRC", href: "irc", icon: ChatBubbleLeftRightIcon },
|
||||||
{ name: "Feeds", href: "feeds", icon: RssIcon },
|
{ name: "Feeds", href: "feeds", icon: RssIcon },
|
||||||
{ name: "Clients", href: "clients", icon: DownloadIcon },
|
{ name: "Clients", href: "clients", icon: FolderArrowDownIcon },
|
||||||
{ name: "Notifications", href: "notifications", icon: BellIcon },
|
{ name: "Notifications", href: "notifications", icon: BellIcon },
|
||||||
{ name: "API keys", href: "api-keys", icon: KeyIcon },
|
{ name: "API keys", href: "api-keys", icon: KeyIcon },
|
||||||
{ name: "Releases", href: "releases", icon: CollectionIcon }
|
{ name: "Releases", href: "releases", icon: RectangleStackIcon }
|
||||||
// {name: 'Regex Playground', href: 'regex-playground', icon: CogIcon, current: false}
|
// {name: 'Regex Playground', href: 'regex-playground', icon: CogIcon, current: false}
|
||||||
// {name: 'Rules', href: 'rules', icon: ClipboardCheckIcon, current: false},
|
// {name: 'Rules', href: 'rules', icon: ClipboardCheckIcon, current: false},
|
||||||
];
|
];
|
||||||
|
|
|
@ -1,17 +1,17 @@
|
||||||
import { AlertWarning } from "../../components/alerts";
|
import {AlertWarning} from "../../components/alerts";
|
||||||
import { DownloadClientSelect, NumberField, Select, SwitchGroup, TextField } from "../../components/inputs";
|
import {DownloadClientSelect, NumberField, Select, SwitchGroup, TextField} from "../../components/inputs";
|
||||||
import { ActionContentLayoutOptions, ActionTypeNameMap, ActionTypeOptions } from "../../domain/constants";
|
import {ActionContentLayoutOptions, ActionTypeNameMap, ActionTypeOptions} from "../../domain/constants";
|
||||||
import React, { Fragment, useRef } from "react";
|
import React, {Fragment, useRef} from "react";
|
||||||
import { useQuery } from "react-query";
|
import {useQuery} from "react-query";
|
||||||
import { APIClient } from "../../api/APIClient";
|
import {APIClient} from "../../api/APIClient";
|
||||||
import { Field, FieldArray, FieldProps, FormikValues } from "formik";
|
import {Field, FieldArray, FieldProps, FormikValues} from "formik";
|
||||||
import { EmptyListState } from "../../components/emptystates";
|
import {EmptyListState} from "../../components/emptystates";
|
||||||
import { useToggle } from "../../hooks/hooks";
|
import {useToggle} from "../../hooks/hooks";
|
||||||
import { classNames } from "../../utils";
|
import {classNames} from "../../utils";
|
||||||
import { Dialog, Switch as SwitchBasic, Transition } from "@headlessui/react";
|
import {Dialog, Switch as SwitchBasic, Transition} from "@headlessui/react";
|
||||||
import { ChevronRightIcon } from "@heroicons/react/solid";
|
import {ChevronRightIcon} from "@heroicons/react/24/solid";
|
||||||
import { DeleteModal } from "../../components/modals";
|
import {DeleteModal} from "../../components/modals";
|
||||||
import { CollapsableSection } from "./details";
|
import {CollapsableSection} from "./details";
|
||||||
|
|
||||||
interface FilterActionsProps {
|
interface FilterActionsProps {
|
||||||
filter: Filter;
|
filter: Filter;
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import React, { useRef } from "react";
|
import React, {useRef} from "react";
|
||||||
import { useMutation, useQuery } from "react-query";
|
import {useMutation, useQuery} from "react-query";
|
||||||
import { NavLink, Route, Routes, useLocation, useNavigate, useParams } from "react-router-dom";
|
import {NavLink, Route, Routes, useLocation, useNavigate, useParams} from "react-router-dom";
|
||||||
import { toast } from "react-hot-toast";
|
import {toast} from "react-hot-toast";
|
||||||
import { Form, Formik, FormikValues, useFormikContext } from "formik";
|
import {Form, Formik, FormikValues, useFormikContext} from "formik";
|
||||||
import { ChevronDownIcon, ChevronRightIcon } from "@heroicons/react/solid";
|
import {ChevronDownIcon, ChevronRightIcon} from "@heroicons/react/24/solid";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
CODECS_OPTIONS,
|
CODECS_OPTIONS,
|
||||||
|
@ -19,10 +19,10 @@ import {
|
||||||
SOURCES_MUSIC_OPTIONS,
|
SOURCES_MUSIC_OPTIONS,
|
||||||
SOURCES_OPTIONS
|
SOURCES_OPTIONS
|
||||||
} from "../../domain/constants";
|
} from "../../domain/constants";
|
||||||
import { queryClient } from "../../App";
|
import {queryClient} from "../../App";
|
||||||
import { APIClient } from "../../api/APIClient";
|
import {APIClient} from "../../api/APIClient";
|
||||||
import { useToggle } from "../../hooks/hooks";
|
import {useToggle} from "../../hooks/hooks";
|
||||||
import { classNames } from "../../utils";
|
import {classNames} from "../../utils";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
CheckboxField,
|
CheckboxField,
|
||||||
|
@ -35,10 +35,10 @@ import {
|
||||||
} from "../../components/inputs";
|
} from "../../components/inputs";
|
||||||
import DEBUG from "../../components/debug";
|
import DEBUG from "../../components/debug";
|
||||||
import Toast from "../../components/notifications/Toast";
|
import Toast from "../../components/notifications/Toast";
|
||||||
import { DeleteModal } from "../../components/modals";
|
import {DeleteModal} from "../../components/modals";
|
||||||
import { TitleSubtitle } from "../../components/headings";
|
import {TitleSubtitle} from "../../components/headings";
|
||||||
import { TextArea } from "../../components/inputs/input";
|
import {TextArea} from "../../components/inputs/input";
|
||||||
import { FilterActions } from "./action";
|
import {FilterActions} from "./action";
|
||||||
|
|
||||||
interface tabType {
|
interface tabType {
|
||||||
name: string;
|
name: string;
|
||||||
|
|
|
@ -1,26 +1,26 @@
|
||||||
import { Dispatch, FC, Fragment, MouseEventHandler, useReducer, useRef, useState } from "react";
|
import {Dispatch, FC, Fragment, MouseEventHandler, useReducer, useRef, useState} from "react";
|
||||||
import { Link } from "react-router-dom";
|
import {Link} from "react-router-dom";
|
||||||
import { toast } from "react-hot-toast";
|
import {toast} from "react-hot-toast";
|
||||||
import { Listbox, Menu, Switch, Transition } from "@headlessui/react";
|
import {Listbox, Menu, Switch, Transition} from "@headlessui/react";
|
||||||
import { useMutation, useQuery, useQueryClient } from "react-query";
|
import {useMutation, useQuery, useQueryClient} from "react-query";
|
||||||
import {
|
import {
|
||||||
|
ArrowsRightLeftIcon,
|
||||||
CheckIcon,
|
CheckIcon,
|
||||||
ChevronDownIcon,
|
ChevronDownIcon,
|
||||||
DotsHorizontalIcon,
|
DocumentDuplicateIcon,
|
||||||
DuplicateIcon,
|
EllipsisHorizontalIcon,
|
||||||
PencilAltIcon,
|
PencilSquareIcon,
|
||||||
SwitchHorizontalIcon,
|
|
||||||
TrashIcon
|
TrashIcon
|
||||||
} from "@heroicons/react/outline";
|
} from "@heroicons/react/24/outline";
|
||||||
|
|
||||||
import { queryClient } from "../../App";
|
import {queryClient} from "../../App";
|
||||||
import { classNames } from "../../utils";
|
import {classNames} from "../../utils";
|
||||||
import { FilterAddForm } from "../../forms";
|
import {FilterAddForm} from "../../forms";
|
||||||
import { useToggle } from "../../hooks/hooks";
|
import {useToggle} from "../../hooks/hooks";
|
||||||
import { APIClient } from "../../api/APIClient";
|
import {APIClient} from "../../api/APIClient";
|
||||||
import Toast from "../../components/notifications/Toast";
|
import Toast from "../../components/notifications/Toast";
|
||||||
import { EmptyListState } from "../../components/emptystates";
|
import {EmptyListState} from "../../components/emptystates";
|
||||||
import { DeleteModal } from "../../components/modals";
|
import {DeleteModal} from "../../components/modals";
|
||||||
|
|
||||||
type FilterListState = {
|
type FilterListState = {
|
||||||
indexerFilter: string[],
|
indexerFilter: string[],
|
||||||
|
@ -250,7 +250,7 @@ const FilterItemDropdown = ({
|
||||||
text="Are you sure you want to remove this filter? This action cannot be undone."
|
text="Are you sure you want to remove this filter? This action cannot be undone."
|
||||||
/>
|
/>
|
||||||
<Menu.Button className="px-4 py-2">
|
<Menu.Button className="px-4 py-2">
|
||||||
<DotsHorizontalIcon
|
<EllipsisHorizontalIcon
|
||||||
className="w-5 h-5 text-gray-700 hover:text-gray-900 dark:text-gray-100 dark:hover:text-gray-400"
|
className="w-5 h-5 text-gray-700 hover:text-gray-900 dark:text-gray-100 dark:hover:text-gray-400"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
|
@ -277,7 +277,7 @@ const FilterItemDropdown = ({
|
||||||
"font-medium group flex rounded-md items-center w-full px-2 py-2 text-sm"
|
"font-medium group flex rounded-md items-center w-full px-2 py-2 text-sm"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<PencilAltIcon
|
<PencilSquareIcon
|
||||||
className={classNames(
|
className={classNames(
|
||||||
active ? "text-white" : "text-blue-500",
|
active ? "text-white" : "text-blue-500",
|
||||||
"w-5 h-5 mr-2"
|
"w-5 h-5 mr-2"
|
||||||
|
@ -297,7 +297,7 @@ const FilterItemDropdown = ({
|
||||||
)}
|
)}
|
||||||
onClick={() => onToggle(!filter.enabled)}
|
onClick={() => onToggle(!filter.enabled)}
|
||||||
>
|
>
|
||||||
<SwitchHorizontalIcon
|
<ArrowsRightLeftIcon
|
||||||
className={classNames(
|
className={classNames(
|
||||||
active ? "text-white" : "text-blue-500",
|
active ? "text-white" : "text-blue-500",
|
||||||
"w-5 h-5 mr-2"
|
"w-5 h-5 mr-2"
|
||||||
|
@ -317,7 +317,7 @@ const FilterItemDropdown = ({
|
||||||
)}
|
)}
|
||||||
onClick={() => duplicateMutation.mutate(filter.id)}
|
onClick={() => duplicateMutation.mutate(filter.id)}
|
||||||
>
|
>
|
||||||
<DuplicateIcon
|
<DocumentDuplicateIcon
|
||||||
className={classNames(
|
className={classNames(
|
||||||
active ? "text-white" : "text-blue-500",
|
active ? "text-white" : "text-blue-500",
|
||||||
"w-5 h-5 mr-2"
|
"w-5 h-5 mr-2"
|
||||||
|
|
|
@ -1,16 +1,13 @@
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { useQuery } from "react-query";
|
import {useQuery} from "react-query";
|
||||||
import { Listbox, Transition } from "@headlessui/react";
|
import {Listbox, Transition} from "@headlessui/react";
|
||||||
import {
|
import {CheckIcon, ChevronDownIcon} from "@heroicons/react/24/solid";
|
||||||
CheckIcon,
|
|
||||||
ChevronDownIcon
|
|
||||||
} from "@heroicons/react/solid";
|
|
||||||
|
|
||||||
import { APIClient } from "../../api/APIClient";
|
import {APIClient} from "../../api/APIClient";
|
||||||
import { classNames } from "../../utils";
|
import {classNames} from "../../utils";
|
||||||
import { PushStatusOptions } from "../../domain/constants";
|
import {PushStatusOptions} from "../../domain/constants";
|
||||||
import { FilterProps } from "react-table";
|
import {FilterProps} from "react-table";
|
||||||
import { DebounceInput } from "react-debounce-input";
|
import {DebounceInput} from "react-debounce-input";
|
||||||
|
|
||||||
interface ListboxFilterProps {
|
interface ListboxFilterProps {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
|
@ -1,29 +1,20 @@
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { useQuery } from "react-query";
|
import {useQuery} from "react-query";
|
||||||
import {
|
import {Column, useFilters, usePagination, useSortBy, useTable} from "react-table";
|
||||||
useTable,
|
|
||||||
useSortBy,
|
|
||||||
usePagination,
|
|
||||||
useFilters,
|
|
||||||
Column
|
|
||||||
} from "react-table";
|
|
||||||
import {
|
import {
|
||||||
ChevronDoubleLeftIcon,
|
ChevronDoubleLeftIcon,
|
||||||
|
ChevronDoubleRightIcon,
|
||||||
ChevronLeftIcon,
|
ChevronLeftIcon,
|
||||||
ChevronRightIcon,
|
ChevronRightIcon
|
||||||
ChevronDoubleRightIcon
|
} from "@heroicons/react/24/solid";
|
||||||
} from "@heroicons/react/solid";
|
|
||||||
|
|
||||||
import { APIClient } from "../../api/APIClient";
|
import {APIClient} from "../../api/APIClient";
|
||||||
import { EmptyListState } from "../../components/emptystates";
|
import {EmptyListState} from "../../components/emptystates";
|
||||||
|
|
||||||
import * as Icons from "../../components/Icons";
|
import * as Icons from "../../components/Icons";
|
||||||
import * as DataTable from "../../components/data-table";
|
import * as DataTable from "../../components/data-table";
|
||||||
|
|
||||||
import {
|
import {IndexerSelectColumnFilter, PushStatusSelectColumnFilter, SearchColumnFilter} from "./Filters";
|
||||||
IndexerSelectColumnFilter,
|
|
||||||
PushStatusSelectColumnFilter, SearchColumnFilter
|
|
||||||
} from "./Filters";
|
|
||||||
|
|
||||||
type TableState = {
|
type TableState = {
|
||||||
queryPageIndex: number;
|
queryPageIndex: number;
|
||||||
|
|
|
@ -1,16 +1,16 @@
|
||||||
import { queryClient } from "../../App";
|
import {queryClient} from "../../App";
|
||||||
import { useRef } from "react";
|
import {useRef} from "react";
|
||||||
import { useMutation, useQuery } from "react-query";
|
import {useMutation, useQuery} from "react-query";
|
||||||
import { KeyField } from "../../components/fields/text";
|
import {KeyField} from "../../components/fields/text";
|
||||||
import { DeleteModal } from "../../components/modals";
|
import {DeleteModal} from "../../components/modals";
|
||||||
import APIKeyAddForm from "../../forms/settings/APIKeyAddForm";
|
import APIKeyAddForm from "../../forms/settings/APIKeyAddForm";
|
||||||
import Toast from "../../components/notifications/Toast";
|
import Toast from "../../components/notifications/Toast";
|
||||||
import { APIClient } from "../../api/APIClient";
|
import {APIClient} from "../../api/APIClient";
|
||||||
import { useToggle } from "../../hooks/hooks";
|
import {useToggle} from "../../hooks/hooks";
|
||||||
import { toast } from "react-hot-toast";
|
import {toast} from "react-hot-toast";
|
||||||
import { classNames } from "../../utils";
|
import {classNames} from "../../utils";
|
||||||
import { TrashIcon } from "@heroicons/react/outline";
|
import {TrashIcon} from "@heroicons/react/24/outline";
|
||||||
import { EmptySimple } from "../../components/emptystates";
|
import {EmptySimple} from "../../components/emptystates";
|
||||||
|
|
||||||
function APISettings() {
|
function APISettings() {
|
||||||
const [addFormIsOpen, toggleAddForm] = useToggle(false);
|
const [addFormIsOpen, toggleAddForm] = useToggle(false);
|
||||||
|
|
|
@ -1,23 +1,18 @@
|
||||||
import { useToggle } from "../../hooks/hooks";
|
import {useToggle} from "../../hooks/hooks";
|
||||||
import { useMutation, useQuery, useQueryClient } from "react-query";
|
import {useMutation, useQuery, useQueryClient} from "react-query";
|
||||||
import { APIClient } from "../../api/APIClient";
|
import {APIClient} from "../../api/APIClient";
|
||||||
import { Menu, Switch, Transition } from "@headlessui/react";
|
import {Menu, Switch, Transition} from "@headlessui/react";
|
||||||
|
|
||||||
import { classNames } from "../../utils";
|
import {classNames} from "../../utils";
|
||||||
import { Fragment, useRef, useState } from "react";
|
import {Fragment, useRef, useState} from "react";
|
||||||
import { toast } from "react-hot-toast";
|
import {toast} from "react-hot-toast";
|
||||||
import Toast from "../../components/notifications/Toast";
|
import Toast from "../../components/notifications/Toast";
|
||||||
import { queryClient } from "../../App";
|
import {queryClient} from "../../App";
|
||||||
import { DeleteModal } from "../../components/modals";
|
import {DeleteModal} from "../../components/modals";
|
||||||
import {
|
import {ArrowsRightLeftIcon, EllipsisHorizontalIcon, PencilSquareIcon, TrashIcon} from "@heroicons/react/24/outline";
|
||||||
DotsHorizontalIcon,
|
import {FeedUpdateForm} from "../../forms/settings/FeedForms";
|
||||||
PencilAltIcon,
|
import {EmptySimple} from "../../components/emptystates";
|
||||||
SwitchHorizontalIcon,
|
import {ImplementationBadges} from "./Indexer";
|
||||||
TrashIcon
|
|
||||||
} from "@heroicons/react/outline";
|
|
||||||
import { FeedUpdateForm } from "../../forms/settings/FeedForms";
|
|
||||||
import { EmptySimple } from "../../components/emptystates";
|
|
||||||
import { ImplementationBadges } from "./Indexer";
|
|
||||||
|
|
||||||
function FeedSettings() {
|
function FeedSettings() {
|
||||||
const { data } = useQuery(
|
const { data } = useQuery(
|
||||||
|
@ -183,7 +178,7 @@ const FeedItemDropdown = ({
|
||||||
text="Are you sure you want to remove this feed? This action cannot be undone."
|
text="Are you sure you want to remove this feed? This action cannot be undone."
|
||||||
/>
|
/>
|
||||||
<Menu.Button className="px-4 py-2">
|
<Menu.Button className="px-4 py-2">
|
||||||
<DotsHorizontalIcon
|
<EllipsisHorizontalIcon
|
||||||
className="w-5 h-5 text-gray-700 hover:text-gray-900 dark:text-gray-100 dark:hover:text-gray-400"
|
className="w-5 h-5 text-gray-700 hover:text-gray-900 dark:text-gray-100 dark:hover:text-gray-400"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
|
@ -210,7 +205,7 @@ const FeedItemDropdown = ({
|
||||||
)}
|
)}
|
||||||
onClick={() => toggleUpdate()}
|
onClick={() => toggleUpdate()}
|
||||||
>
|
>
|
||||||
<PencilAltIcon
|
<PencilSquareIcon
|
||||||
className={classNames(
|
className={classNames(
|
||||||
active ? "text-white" : "text-blue-500",
|
active ? "text-white" : "text-blue-500",
|
||||||
"w-5 h-5 mr-2"
|
"w-5 h-5 mr-2"
|
||||||
|
@ -230,7 +225,7 @@ const FeedItemDropdown = ({
|
||||||
)}
|
)}
|
||||||
onClick={() => onToggle(!feed.enabled)}
|
onClick={() => onToggle(!feed.enabled)}
|
||||||
>
|
>
|
||||||
<SwitchHorizontalIcon
|
<ArrowsRightLeftIcon
|
||||||
className={classNames(
|
className={classNames(
|
||||||
active ? "text-white" : "text-blue-500",
|
active ? "text-white" : "text-blue-500",
|
||||||
"w-5 h-5 mr-2"
|
"w-5 h-5 mr-2"
|
||||||
|
|
|
@ -1,14 +1,28 @@
|
||||||
import { useQuery } from "react-query";
|
import { useMutation, useQuery, useQueryClient } from "react-query";
|
||||||
|
|
||||||
import { simplifyDate, IsEmptyDate, classNames } from "../../utils";
|
import { classNames, IsEmptyDate, simplifyDate } from "../../utils";
|
||||||
import { IrcNetworkAddForm, IrcNetworkUpdateForm } from "../../forms";
|
import { IrcNetworkAddForm, IrcNetworkUpdateForm } from "../../forms";
|
||||||
import { useToggle } from "../../hooks/hooks";
|
import { useToggle } from "../../hooks/hooks";
|
||||||
import { APIClient } from "../../api/APIClient";
|
import { APIClient } from "../../api/APIClient";
|
||||||
import { EmptySimple } from "../../components/emptystates";
|
import { EmptySimple } from "../../components/emptystates";
|
||||||
import { ExclamationCircleIcon } from "@heroicons/react/outline";
|
import { LockClosedIcon, LockOpenIcon } from "@heroicons/react/24/solid";
|
||||||
import { LockClosedIcon, LockOpenIcon } from "@heroicons/react/solid";
|
import { Menu, Transition } from "@headlessui/react";
|
||||||
|
import { Fragment, useRef } from "react";
|
||||||
|
import { DeleteModal } from "../../components/modals";
|
||||||
|
|
||||||
|
import { toast } from "react-hot-toast";
|
||||||
|
import Toast from "../../components/notifications/Toast";
|
||||||
|
import {
|
||||||
|
ArrowsPointingInIcon,
|
||||||
|
ArrowsPointingOutIcon,
|
||||||
|
EllipsisHorizontalIcon,
|
||||||
|
ExclamationCircleIcon,
|
||||||
|
PencilSquareIcon,
|
||||||
|
TrashIcon
|
||||||
|
} from "@heroicons/react/24/outline";
|
||||||
|
|
||||||
export const IrcSettings = () => {
|
export const IrcSettings = () => {
|
||||||
|
const [expandNetworks, toggleExpand] = useToggle(false);
|
||||||
const [addNetworkIsOpen, toggleAddNetwork] = useToggle(false);
|
const [addNetworkIsOpen, toggleAddNetwork] = useToggle(false);
|
||||||
|
|
||||||
const { data } = useQuery("networks", () => APIClient.irc.getNetworks(), {
|
const { data } = useQuery("networks", () => APIClient.irc.getNetworks(), {
|
||||||
|
@ -42,23 +56,63 @@ export const IrcSettings = () => {
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-between flex-col sm:flex-row mt-10 px-1">
|
||||||
|
<ol className="flex flex-col sm:flex-row sm:gap-2 pb-4 sm:pb-0 sm:divide-x sm:divide-gray-200 sm:dark:divide-gray-700">
|
||||||
|
<li className="flex items-center">
|
||||||
|
<span
|
||||||
|
className="mr-2 flex h-4 w-4 relative"
|
||||||
|
title="Network healthy"
|
||||||
|
>
|
||||||
|
<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-4 w-4 bg-green-500" />
|
||||||
|
</span>
|
||||||
|
<span className="text-sm text-gray-800 dark:text-gray-500">Network healthy</span>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li className="flex items-center sm:pl-2">
|
||||||
|
<span
|
||||||
|
className="mr-2 flex h-4 w-4 rounded-full opacity-75 bg-yellow-400 over:text-yellow-600"
|
||||||
|
title="Network unhealthy"
|
||||||
|
/>
|
||||||
|
<span className="text-sm text-gray-800 dark:text-gray-500">Network unhealthy</span>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li className="flex items-center sm:pl-2">
|
||||||
|
<span
|
||||||
|
className="mr-2 flex h-4 w-4 rounded-full opacity-75 bg-gray-500"
|
||||||
|
title="Network disabled"
|
||||||
|
>
|
||||||
|
</span>
|
||||||
|
<span className="text-sm text-gray-800 dark:text-gray-500">Network disabled</span>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
<div className="flex gap-x-2">
|
||||||
|
<button className="flex items-center text-sm text-gray-800 dark:text-gray-400 p-1 px-2 rounded shadow bg-gray-200 dark:bg-gray-700 hover:bg-gray-300 dark:hover:bg-gray-600" onClick={toggleExpand} title={expandNetworks ? "collapse" : "expand"}>
|
||||||
|
{expandNetworks
|
||||||
|
? <span className="flex items-center">Collapse <ArrowsPointingInIcon className="ml-1 w-4 h-4"/></span>
|
||||||
|
: <span className="flex items-center">Expand <ArrowsPointingOutIcon className="ml-1 w-4 h-4"/></span>
|
||||||
|
}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{data && data.length > 0 ? (
|
{data && data.length > 0 ? (
|
||||||
<section className="mt-6 light:bg-white dark:bg-gray-800 light:shadow sm:rounded-md">
|
<section className="mt-6 light:bg-white dark:bg-gray-800 light:shadow sm:rounded-md">
|
||||||
<ol className="min-w-full">
|
<ol className="min-w-full relative">
|
||||||
<li className="grid grid-cols-12 gap-4 border-b border-gray-200 dark:border-gray-700">
|
<li className="grid grid-cols-12 gap-4 border-b border-gray-200 dark:border-gray-700">
|
||||||
<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">
|
<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">
|
||||||
Network
|
Network
|
||||||
</div>
|
</div>
|
||||||
<div className="col-span-5 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
<div className="hidden sm:flex col-span-5 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
||||||
Server
|
Server
|
||||||
</div>
|
</div>
|
||||||
<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">
|
<div className="hidden sm:flex col-span-3 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
||||||
Nick
|
Nick
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
{data &&
|
{data &&
|
||||||
data.map((network, idx) => (
|
data.map((network, idx) => (
|
||||||
<ListItem key={idx} idx={idx} network={network} />
|
<ListItem key={idx} idx={idx} expanded={expandNetworks} network={network} />
|
||||||
))}
|
))}
|
||||||
</ol>
|
</ol>
|
||||||
</section>
|
</section>
|
||||||
|
@ -78,15 +132,16 @@ export const IrcSettings = () => {
|
||||||
interface ListItemProps {
|
interface ListItemProps {
|
||||||
idx: number;
|
idx: number;
|
||||||
network: IrcNetworkWithHealth;
|
network: IrcNetworkWithHealth;
|
||||||
|
expanded: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ListItem = ({ idx, network }: ListItemProps) => {
|
const ListItem = ({ idx, network, expanded }: ListItemProps) => {
|
||||||
const [updateIsOpen, toggleUpdate] = useToggle(false);
|
const [updateIsOpen, toggleUpdate] = useToggle(false);
|
||||||
const [edit, toggleEdit] = useToggle(false);
|
const [edit, toggleEdit] = useToggle(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li key={idx}>
|
<li key={idx}>
|
||||||
<div className={classNames("grid grid-cols-12 gap-2 lg:gap-4 items-center py-4", 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
|
<IrcNetworkUpdateForm
|
||||||
isOpen={updateIsOpen}
|
isOpen={updateIsOpen}
|
||||||
toggle={toggleUpdate}
|
toggle={toggleUpdate}
|
||||||
|
@ -94,7 +149,7 @@ const ListItem = ({ idx, network }: ListItemProps) => {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className="col-span-3 items-center sm:px-6 text-sm font-medium text-gray-900 dark:text-white cursor-pointer"
|
className="col-span-10 xs:col-span-3 sm:col-span-3 items-center sm:px-6 text-sm font-medium text-gray-900 dark:text-white cursor-pointer"
|
||||||
onClick={toggleEdit}
|
onClick={toggleEdit}
|
||||||
>
|
>
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
|
@ -126,7 +181,7 @@ const ListItem = ({ idx, network }: ListItemProps) => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="col-span-5 sm:px-6 text-sm text-gray-500 dark:text-gray-400 cursor-pointer"
|
className="hidden sm:flex col-span-5 sm:px-6 text-sm text-gray-500 dark:text-gray-400 cursor-pointer"
|
||||||
onClick={toggleEdit}
|
onClick={toggleEdit}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
@ -155,7 +210,7 @@ const ListItem = ({ idx, network }: ListItemProps) => {
|
||||||
</div>
|
</div>
|
||||||
{network.nickserv && network.nickserv.account ? (
|
{network.nickserv && network.nickserv.account ? (
|
||||||
<div
|
<div
|
||||||
className="col-span-3 items-center sm:px-6 text-sm text-gray-500 dark:text-gray-400 cursor-pointer"
|
className="hidden sm:flex col-span-3 items-center sm:px-6 text-sm text-gray-500 dark:text-gray-400 cursor-pointer"
|
||||||
onClick={toggleEdit}
|
onClick={toggleEdit}
|
||||||
>
|
>
|
||||||
<div className="overflow-x-auto flex">
|
<div className="overflow-x-auto flex">
|
||||||
|
@ -166,15 +221,10 @@ const ListItem = ({ idx, network }: ListItemProps) => {
|
||||||
<div className="col-span-3" />
|
<div className="col-span-3" />
|
||||||
)}
|
)}
|
||||||
<div className="col-span-1 text-sm text-gray-500 dark:text-gray-400">
|
<div className="col-span-1 text-sm text-gray-500 dark:text-gray-400">
|
||||||
<span
|
<ListItemDropdown network={network} toggleUpdate={toggleUpdate} />
|
||||||
className="text-indigo-600 dark:text-gray-300 hover:text-indigo-900 cursor-pointer"
|
|
||||||
onClick={toggleUpdate}
|
|
||||||
>
|
|
||||||
Edit
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{edit && (
|
{(edit || expanded) && (
|
||||||
<div className="px-4 py-4 flex border-b border-x-0 dark:border-gray-600 dark:bg-gray-700">
|
<div className="px-4 py-4 flex border-b border-x-0 dark:border-gray-600 dark:bg-gray-700">
|
||||||
<div className="min-w-full">
|
<div className="min-w-full">
|
||||||
{network.channels.length > 0 ? (
|
{network.channels.length > 0 ? (
|
||||||
|
@ -238,3 +288,168 @@ const ListItem = ({ idx, network }: ListItemProps) => {
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
interface ListItemDropdownProps {
|
||||||
|
network: IrcNetwork;
|
||||||
|
toggleUpdate: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ListItemDropdown = ({
|
||||||
|
network,
|
||||||
|
toggleUpdate
|
||||||
|
}: ListItemDropdownProps) => {
|
||||||
|
const cancelModalButtonRef = useRef(null);
|
||||||
|
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const [deleteModalIsOpen, toggleDeleteModal] = useToggle(false);
|
||||||
|
const deleteMutation = useMutation(
|
||||||
|
(id: number) => APIClient.irc.deleteNetwork(id),
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries(["networks"]);
|
||||||
|
queryClient.invalidateQueries(["networks", network.id]);
|
||||||
|
|
||||||
|
toast.custom((t) => <Toast type="success" body={`Network ${network.name} was deleted`} t={t}/>);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const restartMutation = useMutation(
|
||||||
|
(id: number) => APIClient.irc.restartNetwork(id),
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.custom((t) => <Toast type="success"
|
||||||
|
body={`${network.name} was successfully restarted`}
|
||||||
|
t={t}/>);
|
||||||
|
|
||||||
|
queryClient.invalidateQueries(["networks"]);
|
||||||
|
queryClient.invalidateQueries(["networks", network.id]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const restart = (id: number) => {
|
||||||
|
restartMutation.mutate(id);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Menu as="div">
|
||||||
|
<DeleteModal
|
||||||
|
isOpen={deleteModalIsOpen}
|
||||||
|
toggle={toggleDeleteModal}
|
||||||
|
buttonRef={cancelModalButtonRef}
|
||||||
|
deleteAction={() => {
|
||||||
|
deleteMutation.mutate(network.id);
|
||||||
|
toggleDeleteModal();
|
||||||
|
}}
|
||||||
|
title={`Remove network: ${network.name}`}
|
||||||
|
text="Are you sure you want to remove this network? This action cannot be undone."
|
||||||
|
/>
|
||||||
|
<Menu.Button className="px-4 py-2">
|
||||||
|
<EllipsisHorizontalIcon
|
||||||
|
className="w-5 h-5 text-gray-700 hover:text-gray-900 dark:text-gray-100 dark:hover:text-gray-400"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
</Menu.Button>
|
||||||
|
<Transition
|
||||||
|
as={Fragment}
|
||||||
|
enter="transition ease-out duration-100"
|
||||||
|
enterFrom="transform opacity-0 scale-95"
|
||||||
|
enterTo="transform opacity-100 scale-100"
|
||||||
|
leave="transition ease-in duration-75"
|
||||||
|
leaveFrom="transform opacity-100 scale-100"
|
||||||
|
leaveTo="transform opacity-0 scale-95"
|
||||||
|
>
|
||||||
|
<Menu.Items
|
||||||
|
className="absolute right-0 w-32 sm:w-56 mt-2 origin-top-right bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700 rounded-md shadow-lg ring-1 ring-black ring-opacity-10 focus:outline-none"
|
||||||
|
>
|
||||||
|
<div className="px-1 py-1">
|
||||||
|
<Menu.Item>
|
||||||
|
{({ active }) => (
|
||||||
|
<button
|
||||||
|
className={classNames(
|
||||||
|
active ? "bg-blue-600 text-white" : "text-gray-900 dark:text-gray-300",
|
||||||
|
"font-medium group flex rounded-md items-center w-full px-2 py-2 text-sm"
|
||||||
|
)}
|
||||||
|
onClick={() => toggleUpdate()}
|
||||||
|
>
|
||||||
|
<PencilSquareIcon
|
||||||
|
className={classNames(
|
||||||
|
active ? "text-white" : "text-blue-500",
|
||||||
|
"w-5 h-5 mr-2"
|
||||||
|
)}
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
Edit
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</Menu.Item>
|
||||||
|
{/*<Menu.Item>*/}
|
||||||
|
{/* {({ active }) => (*/}
|
||||||
|
{/* <button*/}
|
||||||
|
{/* className={classNames(*/}
|
||||||
|
{/* active ? "bg-blue-600 text-white" : "text-gray-900 dark:text-gray-300",*/}
|
||||||
|
{/* "font-medium group flex rounded-md items-center w-full px-2 py-2 text-sm"*/}
|
||||||
|
{/* )}*/}
|
||||||
|
{/* onClick={() => onToggle(!network.enabled)}*/}
|
||||||
|
{/* >*/}
|
||||||
|
{/* <SwitchHorizontalIcon*/}
|
||||||
|
{/* className={classNames(*/}
|
||||||
|
{/* active ? "text-white" : "text-blue-500",*/}
|
||||||
|
{/* "w-5 h-5 mr-2"*/}
|
||||||
|
{/* )}*/}
|
||||||
|
{/* aria-hidden="true"*/}
|
||||||
|
{/* />*/}
|
||||||
|
{/* {network.enabled ? "Disable" : "Enable"}*/}
|
||||||
|
{/* </button>*/}
|
||||||
|
{/* )}*/}
|
||||||
|
{/*</Menu.Item>*/}
|
||||||
|
<Menu.Item>
|
||||||
|
{({ active }) => (
|
||||||
|
<button
|
||||||
|
className={classNames(
|
||||||
|
active ? "bg-blue-600 text-white" : "text-gray-900 dark:text-gray-300",
|
||||||
|
"font-medium group flex rounded-md items-center w-full px-2 py-2 text-sm"
|
||||||
|
)}
|
||||||
|
onClick={() => restart(network.id)}
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className={classNames(
|
||||||
|
active ? "text-white" : "text-blue-500",
|
||||||
|
"w-5 h-5 mr-2"
|
||||||
|
)}>
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" d="M5.636 5.636a9 9 0 1012.728 0M12 3v9" />
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
Restart
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</Menu.Item>
|
||||||
|
</div>
|
||||||
|
<div className="px-1 py-1">
|
||||||
|
<Menu.Item>
|
||||||
|
{({ active }) => (
|
||||||
|
<button
|
||||||
|
className={classNames(
|
||||||
|
active ? "bg-red-600 text-white" : "text-gray-900 dark:text-gray-300",
|
||||||
|
"font-medium group flex rounded-md items-center w-full px-2 py-2 text-sm"
|
||||||
|
)}
|
||||||
|
onClick={() => toggleDeleteModal()}
|
||||||
|
>
|
||||||
|
<TrashIcon
|
||||||
|
className={classNames(
|
||||||
|
active ? "text-white" : "text-red-500",
|
||||||
|
"w-5 h-5 mr-2"
|
||||||
|
)}
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</Menu.Item>
|
||||||
|
</div>
|
||||||
|
</Menu.Items>
|
||||||
|
</Transition>
|
||||||
|
</Menu>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
|
@ -2012,12 +2012,12 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@heroicons/react@npm:^1.0.6":
|
"@heroicons/react@npm:^2.0.11":
|
||||||
version: 1.0.6
|
version: 2.0.11
|
||||||
resolution: "@heroicons/react@npm:1.0.6"
|
resolution: "@heroicons/react@npm:2.0.11"
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ">= 16"
|
react: ">= 16"
|
||||||
checksum: 372b1eda3ce735ef069777bc96304f70de585ebb71a6d1cedc121bb695f9bca235619112e3ee14e8779e95a03096813cbbe3b755927a54b7580d1ce084fa4096
|
checksum: 9049e12bdb9bb1cc2ebc555877f538055ae01828bda781f0a7f3b2fe17aff8b6337d8562c5984bfe09ee77e95befd4e62854a58c9900c2e2650798b15ab807c3
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
@ -12886,7 +12886,7 @@ __metadata:
|
||||||
dependencies:
|
dependencies:
|
||||||
"@fontsource/inter": ^4.5.11
|
"@fontsource/inter": ^4.5.11
|
||||||
"@headlessui/react": ^1.6.4
|
"@headlessui/react": ^1.6.4
|
||||||
"@heroicons/react": ^1.0.6
|
"@heroicons/react": ^2.0.11
|
||||||
"@hookform/error-message": ^2.0.0
|
"@hookform/error-message": ^2.0.0
|
||||||
"@tailwindcss/forms": ^0.5.2
|
"@tailwindcss/forms": ^0.5.2
|
||||||
"@types/node": ^18.0.0
|
"@types/node": ^18.0.0
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue