diff --git a/src/components/Dashboard/Tiles/Apps/AppPing.tsx b/src/components/Dashboard/Tiles/Apps/AppPing.tsx index cbdd8c959..b4118c9b3 100644 --- a/src/components/Dashboard/Tiles/Apps/AppPing.tsx +++ b/src/components/Dashboard/Tiles/Apps/AppPing.tsx @@ -3,7 +3,7 @@ import { IconCheck, IconLoader, IconX } from '@tabler/icons-react'; import Consola from 'consola'; import { TargetAndTransition, Transition, motion } from 'framer-motion'; import { useTranslation } from 'next-i18next'; -import { api } from '~/utils/api'; +import { RouterOutputs, api } from '~/utils/api'; import { useConfigContext } from '../../../../config/provider'; import { AppType } from '../../../../types/app'; @@ -13,57 +13,19 @@ interface AppPingProps { } export const AppPing = ({ app }: AppPingProps) => { - const { t } = useTranslation('modules/ping'); const { config } = useConfigContext(); - const active = - (config?.settings.customization.layout.enabledPing && app.network.enabledStatusChecker) ?? - false; - - const { data, isFetching, isError, error } = api.app.ping.useQuery(app.id, { - retry: false, - enabled: active, - select: (data) => { - const isOk = getIsOk(app, data.status); - if (isOk) - Consola.info(`Ping of app "${app.name}" (${app.url}) returned ${data.status} (Accepted)`); - else - Consola.warn(`Ping of app "${app.name}" (${app.url}) returned ${data.status} (Refused)`); - return { - status: data.status, - state: isOk ? ('online' as const) : ('down' as const), - statusText: data.statusText, - }; - }, - }); - - if (!active) return null; + const { data, isFetching, isError, error, isActive } = usePing(app); + const tooltipLabel = useTooltipLabel({isFetching, isError, data, errorMessage: error?.message}) const isOnline = isError ? false : data?.state === 'online'; + const pulse = usePingPulse({isOnline}); + + if (!isActive) return null; + - const disablePulse = config?.settings.customization.accessibility?.disablePingPulse ?? false; const replaceDotWithIcon = - config?.settings.customization.accessibility?.replacePingDotsWithIcons ?? false; - - const scaleAnimation = isOnline ? [1, 0.7, 1] : 1; - const animate: TargetAndTransition | undefined = disablePulse - ? undefined - : { - scale: scaleAnimation, - }; - const transition: Transition | undefined = disablePulse - ? undefined - : { - repeat: Infinity, - duration: 2.5, - ease: 'easeInOut', - }; - - const label = () => { - if (isFetching) return t('states.loading'); - if (isError) return error?.message; - if (data?.state === 'online') return t('states.online', { response: data?.status ?? 'N/A' }); - return `${data?.statusText}: ${data?.status} (denied)`; - } + config?.settings.customization.accessibility?.replacePingDotsWithIcons ?? false; + return ( { right: replaceDotWithIcon ? 8 : 20, zIndex: 2, }} - animate={animate} - transition={transition} + animate={pulse.animate} + transition={pulse.transition} > - {config?.settings.customization.accessibility?.replacePingDotsWithIcons ? ( + {replaceDotWithIcon ? ( - + ) : ( { ); }; -const AccessibleIndicatorPing = ({ - isLoading, - isOnline, -}: { +type AccessibleIndicatorPingProps = { isOnline: boolean; - isLoading: boolean; -}) => { + isFetching: boolean; +} + +const AccessibleIndicatorPing = ({ + isFetching, + isOnline, +}: AccessibleIndicatorPingProps) => { if (isOnline) { return ; } - if (isLoading) { + if (isFetching) { return ; } return ; }; -export const getIsOk = (app: AppType, status: number) => { +export const isStatusOk = (app: AppType, status: number) => { if (app.network.okStatus === undefined || app.network.statusCodes.length >= 1) { return app.network.statusCodes.includes(status.toString()); } return app.network.okStatus.includes(status); }; + +type TooltipLabelProps = { + isFetching: boolean; + isError: boolean; + data: RouterOutputs['app']['ping'] | undefined; + errorMessage: string | undefined; +} + +const useTooltipLabel = ({isFetching, isError, data, errorMessage}: TooltipLabelProps) => { + const { t } = useTranslation('modules/ping'); + + if (isFetching) return t('states.loading'); + if (isError) return errorMessage; + if (data?.state === 'online') return t('states.online', { response: data?.status ?? 'N/A' }); + return `${data?.statusText}: ${data?.status} (denied)`; +} + +const usePing = (app: AppType) => { + const { config, name } = useConfigContext(); + const isActive = (config?.settings.customization.layout.enabledPing && app.network.enabledStatusChecker) ?? + false; + + const queryResult = api.app.ping.useQuery({ + id: app.id, + configName: name ?? '' + }, { + retry: false, + enabled: isActive, + select: (data) => { + const isOk = isStatusOk(app, data.status); + if (isOk) + Consola.info(`Ping of app "${app.name}" (${app.url}) returned ${data.status} (Accepted)`); + else + Consola.warn(`Ping of app "${app.name}" (${app.url}) returned ${data.status} (Refused)`); + return { + status: data.status, + state: isOk ? ('online' as const) : ('down' as const), + statusText: data.statusText, + }; + }, + }); + + return { + ...queryResult, + isActive + } +} + +type PingPulse = { + animate?: TargetAndTransition; + transition?: Transition; +} + +const usePingPulse = ({isOnline}: {isOnline: boolean}): PingPulse => { + const { config } = useConfigContext(); + const disablePulse = config?.settings.customization.accessibility?.disablePingPulse ?? false; + + if (disablePulse) { + return {}; + } + + return { + animate: { + scale: isOnline ? [1, 0.7, 1] : 1 + }, + transition: { + repeat: Infinity, + duration: 2.5, + ease: 'easeInOut', + } + } +} \ No newline at end of file diff --git a/src/server/api/routers/app.ts b/src/server/api/routers/app.ts index 4e403c0ca..f8fdfc268 100644 --- a/src/server/api/routers/app.ts +++ b/src/server/api/routers/app.ts @@ -1,24 +1,24 @@ import { TRPCError } from '@trpc/server'; import axios, { AxiosError } from 'axios'; import Consola from 'consola'; -import { getCookie } from 'cookies-next'; import https from 'https'; import { z } from 'zod'; -import { getIsOk } from '~/components/Dashboard/Tiles/Apps/AppPing'; +import { isStatusOk } from '~/components/Dashboard/Tiles/Apps/AppPing'; import { getConfig } from '~/tools/config/getConfig'; import { AppType } from '~/types/app'; import { createTRPCRouter, publicProcedure } from '../trpc'; export const appRouter = createTRPCRouter({ - ping: publicProcedure.input(z.string()).query(async ({ input }) => { + ping: publicProcedure.input(z.object({ + id: z.string(), + configName: z.string() + })).query(async ({ input }) => { const agent = new https.Agent({ rejectUnauthorized: false }); - const configName = getCookie('config-name'); - const config = getConfig(configName?.toString() ?? 'default'); - const app = config.apps.find((app) => app.id === input); + const config = getConfig(input.configName); + const app = config.apps.find((app) => app.id === input.id); - const url = app?.url; - if (url === undefined || !app) { + if (!app?.url) { Consola.error(`App ${input} not found`); throw new TRPCError({ code: 'NOT_FOUND', @@ -27,22 +27,23 @@ export const appRouter = createTRPCRouter({ }); } const res = await axios - .get(url, { httpsAgent: agent, timeout: 2000 }) + .get(app.url, { httpsAgent: agent, timeout: 2000 }) .then((response) => ({ status: response.status, statusText: response.statusText, + state: isStatusOk(app as AppType, response.status) ? 'online' : 'offline' })) .catch((error: AxiosError) => { if (error.response) { return { - state: getIsOk(app as AppType, error.response.status) ? 'online' : 'offline', + state: isStatusOk(app as AppType, error.response.status) ? 'online' : 'offline', status: error.response.status, statusText: error.response.statusText, }; } if (error.code === 'ECONNABORTED') { - Consola.error(`Ping timed out for app with id : ${input} (url: ${url})`); + Consola.error(`Ping timed out for app with id : ${input} (url: ${app.url})`); throw new TRPCError({ code: 'TIMEOUT', cause: input,