diff --git a/package.json b/package.json index 83f8f6e8f..ae6c8dc53 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,10 @@ "@tanstack/react-query": "^4.2.1", "@tanstack/react-query-devtools": "^4.24.4", "@tanstack/react-query-persist-client": "^4.28.0", + "@trpc/client": "^10.29.1", + "@trpc/next": "^10.29.1", + "@trpc/react-query": "^10.29.1", + "@trpc/server": "^10.29.1", "@vitejs/plugin-react": "^4.0.0", "axios": "^1.0.0", "consola": "^3.0.0", @@ -111,6 +115,7 @@ "turbo": "latest", "typescript": "^5.0.4", "video.js": "^8.0.3", + "vite-tsconfig-paths": "^4.2.0", "vitest": "^0.32.0", "vitest-fetch-mock": "^0.2.2" }, diff --git a/src/components/Config/ConfigChanger.tsx b/src/components/Config/ConfigChanger.tsx index 4826fcdd8..19a88b179 100644 --- a/src/components/Config/ConfigChanger.tsx +++ b/src/components/Config/ConfigChanger.tsx @@ -1,12 +1,12 @@ import { Center, Dialog, Loader, Notification, Select, Tooltip } from '@mantine/core'; import { useToggle } from '@mantine/hooks'; -import { useQuery } from '@tanstack/react-query'; +import { notifications } from '@mantine/notifications'; +import { IconCheck } from '@tabler/icons-react'; import { setCookie } from 'cookies-next'; import { useTranslation } from 'next-i18next'; import { useRouter } from 'next/router'; import { useState } from 'react'; -import { notifications } from '@mantine/notifications'; -import { IconCheck } from '@tabler/icons-react'; +import { api } from '~/utils/api'; import { useConfigContext } from '../../config/provider'; export default function ConfigChanger() { @@ -95,10 +95,4 @@ export default function ConfigChanger() { ); } -const useConfigsQuery = () => - useQuery({ - queryKey: ['config/get-all'], - queryFn: fetchConfigs, - }); - -const fetchConfigs = async () => (await (await fetch('/api/configs')).json()) as string[]; +const useConfigsQuery = () => api.config.all.useQuery(); diff --git a/src/components/Config/LoadConfig.tsx b/src/components/Config/LoadConfig.tsx index dfa31f7cd..d9c64c558 100644 --- a/src/components/Config/LoadConfig.tsx +++ b/src/components/Config/LoadConfig.tsx @@ -4,18 +4,20 @@ import { showNotification } from '@mantine/notifications'; import { IconCheck as Check, IconPhoto, IconUpload, IconX, IconX as X } from '@tabler/icons-react'; import { setCookie } from 'cookies-next'; import { useTranslation } from 'next-i18next'; +import { useRouter } from 'next/router'; import { useConfigStore } from '../../config/store'; import { ConfigType } from '../../types/config'; +import { api } from '~/utils/api'; export const LoadConfigComponent = () => { - const { addConfig } = useConfigStore(); const theme = useMantineTheme(); const { t } = useTranslation('settings/general/config-changer'); + const { mutateAsync: loadAsync } = useLoadConfig(); return ( { - const fileName = files[0].name.replaceAll('.json', ''); + const configName = files[0].name.replaceAll('.json', ''); const fileText = await files[0].text(); try { @@ -32,26 +34,7 @@ export const LoadConfigComponent = () => { } const newConfig: ConfigType = JSON.parse(fileText); - - await addConfig(fileName, newConfig, true); - showNotification({ - autoClose: 5000, - radius: 'md', - title: ( - - {t('dropzone.notifications.loadedSuccessfully.title', { - configName: fileName, - })} - - ), - color: 'green', - icon: , - message: undefined, - }); - setCookie('config-name', fileName, { - maxAge: 60 * 60 * 24 * 30, - sameSite: 'strict', - }); + await loadAsync({ name: configName, config: newConfig }); }} accept={['application/json']} > @@ -89,3 +72,34 @@ export const LoadConfigComponent = () => { ); }; + +const useLoadConfig = () => { + const { t } = useTranslation('settings/general/config-changer'); + const { addConfig } = useConfigStore(); + const router = useRouter(); + return api.config.save.useMutation({ + async onSuccess(_data, variables) { + await addConfig(variables.name, variables.config); + + showNotification({ + autoClose: 5000, + radius: 'md', + title: ( + + {t('dropzone.notifications.loadedSuccessfully.title', { + configName: variables.name, + })} + + ), + color: 'green', + icon: , + message: undefined, + }); + setCookie('config-name', variables.name, { + maxAge: 60 * 60 * 24 * 30, + sameSite: 'strict', + }); + router.push(`/${variables.name}`); + }, + }); +}; diff --git a/src/components/Dashboard/Tiles/Apps/AppPing.tsx b/src/components/Dashboard/Tiles/Apps/AppPing.tsx index b2e3bd559..5af4b0938 100644 --- a/src/components/Dashboard/Tiles/Apps/AppPing.tsx +++ b/src/components/Dashboard/Tiles/Apps/AppPing.tsx @@ -1,10 +1,10 @@ import { Indicator, Tooltip } from '@mantine/core'; import Consola from 'consola'; -import { useQuery } from '@tanstack/react-query'; import { motion } from 'framer-motion'; import { useTranslation } from 'next-i18next'; import { useConfigContext } from '../../../../config/provider'; import { AppType } from '../../../../types/app'; +import { api } from '~/utils/api'; interface AppPingProps { app: AppType; @@ -16,18 +16,7 @@ export const AppPing = ({ app }: AppPingProps) => { const active = (config?.settings.customization.layout.enabledPing && app.network.enabledStatusChecker) ?? false; - const { data, isLoading } = useQuery({ - queryKey: ['ping', { id: app.id, name: app.name }], - queryFn: async () => { - const response = await fetch(`/api/modules/ping?url=${encodeURI(app.url)}`); - const isOk = getIsOk(app, response.status); - return { - status: response.status, - state: isOk ? 'online' : 'down', - }; - }, - enabled: active, - }); + const { data, isLoading, error } = usePingQuery(app, active); const isOnline = data?.state === 'online'; @@ -49,7 +38,7 @@ export const AppPing = ({ app }: AppPingProps) => { ? t('states.loading') : isOnline ? t('states.online', { response: data.status }) - : t('states.offline', { response: data?.status }) + : t('states.offline', { response: data?.status ?? error?.data?.httpStatus }) } > { ); }; +const usePingQuery = (app: AppType, isEnabled: boolean) => + api.app.ping.useQuery( + { + url: app.url, + }, + { + enabled: isEnabled, + select: (data) => { + const statusCode = data.status; + const isOk = getIsOk(app, statusCode); + return { + status: statusCode, + state: isOk ? ('online' as const) : ('down' as const), + }; + }, + } + ); + const getIsOk = (app: AppType, status: number) => { -if (app.network.okStatus === undefined || app.network.statusCodes.length >= 1) { -Consola.log('Using new status codes'); -return app.network.statusCodes.includes(status.toString()); -} -Consola.warn('Using deprecated okStatus'); -return app.network.okStatus.includes(status); + if (app.network.okStatus === undefined || app.network.statusCodes.length >= 1) { + Consola.log('Using new status codes'); + return app.network.statusCodes.includes(status.toString()); + } + Consola.warn('Using deprecated okStatus'); + return app.network.okStatus.includes(status); }; diff --git a/src/components/IconSelector/IconSelector.tsx b/src/components/IconSelector/IconSelector.tsx index 55a8ea377..5929fc22c 100644 --- a/src/components/IconSelector/IconSelector.tsx +++ b/src/components/IconSelector/IconSelector.tsx @@ -15,9 +15,9 @@ import { } from '@mantine/core'; import { IconSearch } from '@tabler/icons-react'; import { useTranslation } from 'next-i18next'; -import { useGetDashboardIcons } from '../../hooks/icons/useGetDashboardIcons'; import { humanFileSize } from '../../tools/humanFileSize'; import { DebouncedImage } from './DebouncedImage'; +import { api } from '~/utils/api'; export const IconSelector = forwardRef( ( @@ -175,3 +175,12 @@ interface ItemProps extends SelectItemProps { size: number; copyright: string | undefined; } + +const useGetDashboardIcons = () => + api.icon.all.useQuery(undefined, { + refetchOnMount: false, + // Cache for infinity, refetch every so often. + cacheTime: Infinity, + staleTime: 1000 * 60 * 5, // 5 minutes + refetchOnWindowFocus: false, + }); diff --git a/src/components/Settings/Common/Config/ConfigActions.tsx b/src/components/Settings/Common/Config/ConfigActions.tsx index 2cce6ac20..700ccd70d 100644 --- a/src/components/Settings/Common/Config/ConfigActions.tsx +++ b/src/components/Settings/Common/Config/ConfigActions.tsx @@ -10,23 +10,28 @@ import { import { useDisclosure } from '@mantine/hooks'; import { openConfirmModal } from '@mantine/modals'; import { showNotification } from '@mantine/notifications'; -import { IconAlertTriangle, IconCheck, IconCopy, IconDownload, IconTrash } from '@tabler/icons-react'; +import { + IconAlertTriangle, + IconCheck, + IconCopy, + IconDownload, + IconTrash, + IconX, +} from '@tabler/icons-react'; import fileDownload from 'js-file-download'; import { Trans, useTranslation } from 'next-i18next'; import { useRouter } from 'next/router'; import { useConfigContext } from '../../../../config/provider'; import { useConfigStore } from '../../../../config/store'; -import { useDeleteConfigMutation } from '../../../../tools/config/mutations/useDeleteConfigMutation'; import Tip from '../../../layout/Tip'; import { CreateConfigCopyModal } from './CreateCopyModal'; +import { api } from '~/utils/api'; export default function ConfigActions() { - const router = useRouter(); const { t } = useTranslation(['settings/general/config-changer', 'settings/common', 'common']); const [createCopyModalOpened, createCopyModal] = useDisclosure(false); const { config } = useConfigContext(); - const { removeConfig } = useConfigStore(); - const { mutateAsync } = useDeleteConfigMutation(config?.configProperties.name ?? 'default'); + const { mutateAsync } = useDeleteConfigMutation(); if (!config) return null; @@ -61,28 +66,9 @@ export default function ConfigActions() { }, zIndex: 201, onConfirm: async () => { - const response = await mutateAsync(); - - if (response.error) { - showNotification({ - title: t('buttons.delete.notifications.deleteFailedDefaultConfig.title'), - message: t('buttons.delete.notifications.deleteFailedDefaultConfig.message'), - }); - return; - } - - showNotification({ - title: t('buttons.delete.notifications.deleted.title'), - icon: , - color: 'green', - autoClose: 1500, - radius: 'md', - message: t('buttons.delete.notifications.deleted.message'), + const response = await mutateAsync({ + name: config?.configProperties.name ?? 'default', }); - - removeConfig(config?.configProperties.name ?? 'default'); - - router.push('/'); }, }); }; @@ -124,6 +110,49 @@ export default function ConfigActions() { ); } +const useDeleteConfigMutation = () => { + const { t } = useTranslation(['settings/general/config-changer']); + const router = useRouter(); + const { removeConfig } = useConfigStore(); + + return api.config.delete.useMutation({ + onError(error) { + if (error.data?.code === 'FORBIDDEN') { + showNotification({ + title: t('buttons.delete.notifications.deleteFailedDefaultConfig.title'), + icon: , + color: 'red', + autoClose: 1500, + radius: 'md', + message: t('buttons.delete.notifications.deleteFailedDefaultConfig.message'), + }); + } + showNotification({ + title: t('buttons.delete.notifications.deleteFailed.title'), + icon: , + color: 'red', + autoClose: 1500, + radius: 'md', + message: t('buttons.delete.notifications.deleteFailed.message'), + }); + }, + onSuccess(data, variables) { + showNotification({ + title: t('buttons.delete.notifications.deleted.title'), + icon: , + color: 'green', + autoClose: 1500, + radius: 'md', + message: t('buttons.delete.notifications.deleted.message'), + }); + + removeConfig(variables.name); + + router.push('/'); + }, + }); +}; + const useStyles = createStyles(() => ({ actionIcon: { width: 'auto', diff --git a/src/components/Settings/Common/Config/CreateCopyModal.tsx b/src/components/Settings/Common/Config/CreateCopyModal.tsx index fb924f804..bf1409d1f 100644 --- a/src/components/Settings/Common/Config/CreateCopyModal.tsx +++ b/src/components/Settings/Common/Config/CreateCopyModal.tsx @@ -1,8 +1,11 @@ import { Button, Group, Modal, TextInput, Title } from '@mantine/core'; import { useForm } from '@mantine/form'; import { useTranslation } from 'next-i18next'; +import { IconCheck, IconX } from '@tabler/icons-react'; +import { showNotification } from '@mantine/notifications'; import { useConfigStore } from '../../../../config/store'; -import { useCopyConfigMutation } from '../../../../tools/config/mutations/useCopyConfigMutation'; +import { useConfigContext } from '~/config/provider'; +import { api } from '~/utils/api'; interface CreateConfigCopyModalProps { opened: boolean; @@ -16,6 +19,7 @@ export const CreateConfigCopyModal = ({ initialConfigName, }: CreateConfigCopyModalProps) => { const { configs } = useConfigStore(); + const { config } = useConfigContext(); const { t } = useTranslation(['settings/general/config-changer']); const form = useForm({ @@ -40,7 +44,7 @@ export const CreateConfigCopyModal = ({ validateInputOnBlur: true, }); - const { mutateAsync } = useCopyConfigMutation(form.values.configName); + const { mutateAsync } = useCopyConfigMutation(); const handleClose = () => { form.setFieldValue('configName', initialConfigName); @@ -50,7 +54,17 @@ export const CreateConfigCopyModal = ({ const handleSubmit = async (values: typeof form.values) => { if (!form.isValid) return; - await mutateAsync(); + if (!config) { + throw new Error('config is not defiend'); + } + + const copiedConfig = config; + copiedConfig.configProperties.name = form.values.configName; + + await mutateAsync({ + name: form.values.configName, + config: copiedConfig, + }); closeModal(); }; @@ -76,3 +90,33 @@ export const CreateConfigCopyModal = ({ ); }; + +const useCopyConfigMutation = () => { + const { t } = useTranslation(['settings/general/config-changer']); + const utils = api.useContext(); + + return api.config.save.useMutation({ + onSuccess(_data, variables) { + showNotification({ + title: t('modal.copy.events.configCopied.title'), + icon: , + color: 'green', + autoClose: 1500, + radius: 'md', + message: t('modal.copy.events.configCopied.message', { configName: variables.name }), + }); + // Invalidate a query to fetch new config + utils.config.all.invalidate(); + }, + onError(_error, variables) { + showNotification({ + title: t('modal.events.configNotCopied.title'), + icon: , + color: 'red', + autoClose: 1500, + radius: 'md', + message: t('modal.events.configNotCopied.message', { configName: variables.name }), + }); + }, + }); +}; diff --git a/src/components/layout/header/Actions/ToggleEditMode/ToggleEditMode.tsx b/src/components/layout/header/Actions/ToggleEditMode/ToggleEditMode.tsx index 481e61f42..3cbe17e15 100644 --- a/src/components/layout/header/Actions/ToggleEditMode/ToggleEditMode.tsx +++ b/src/components/layout/header/Actions/ToggleEditMode/ToggleEditMode.tsx @@ -2,13 +2,13 @@ import { ActionIcon, Button, Group, Text, Title, Tooltip } from '@mantine/core'; import { useHotkeys, useWindowEvent } from '@mantine/hooks'; import { hideNotification, showNotification } from '@mantine/notifications'; import { IconEditCircle, IconEditCircleOff } from '@tabler/icons-react'; -import axios from 'axios'; import Consola from 'consola'; import { getCookie } from 'cookies-next'; import { Trans, useTranslation } from 'next-i18next'; import { useConfigContext } from '../../../../../config/provider'; import { useScreenSmallerThan } from '../../../../../hooks/useScreenSmallerThan'; +import { api } from '~/utils/api'; import { useEditModeStore } from '../../../../Dashboard/Views/useEditModeStore'; import { useNamedWrapperColumnCount } from '../../../../Dashboard/Wrappers/gridstack/store'; import { useCardStyles } from '../../../useCardStyles'; @@ -28,6 +28,7 @@ export const ToggleEditModeAction = () => { const smallerThanSm = useScreenSmallerThan('sm'); const { config } = useConfigContext(); const { classes } = useCardStyles(true); + const { mutateAsync: saveConfig } = api.config.save.useMutation(); useHotkeys([['mod+E', toggleEditMode]]); @@ -41,11 +42,12 @@ export const ToggleEditModeAction = () => { return undefined; }); - const toggleButtonClicked = () => { + const toggleButtonClicked = async () => { toggleEditMode(); - if (enabled || config === undefined || config?.schemaVersion === undefined) { + if (config === undefined || config?.schemaVersion === undefined) return; + if (enabled) { const configName = getCookie('config-name')?.toString() ?? 'default'; - axios.put(`/api/configs/${configName}`, { ...config }); + await saveConfig({ name: configName, config }); Consola.log('Saved config to server', configName); hideNotification('toggle-edit-mode'); } else if (!enabled) { diff --git a/src/components/layout/header/Search.tsx b/src/components/layout/header/Search.tsx index 40d6202cb..1327d617e 100644 --- a/src/components/layout/header/Search.tsx +++ b/src/components/layout/header/Search.tsx @@ -13,10 +13,9 @@ import { import { useDebouncedValue, useHotkeys } from '@mantine/hooks'; import { showNotification } from '@mantine/notifications'; import { IconBrandYoutube, IconDownload, IconMovie, IconSearch } from '@tabler/icons-react'; -import { useQuery } from '@tanstack/react-query'; -import axios from 'axios'; import { useTranslation } from 'next-i18next'; import React, { forwardRef, useEffect, useRef, useState } from 'react'; +import { api } from '~/utils/api'; import { useConfigContext } from '../../../config/provider'; import { OverseerrMediaDisplay } from '../../../modules/common'; import { IModule } from '../../../modules/ModuleTypes'; @@ -141,26 +140,12 @@ export function Search() { const openTarget = getOpenTarget(config); const [opened, setOpened] = useState(false); - const { - data: OverseerrResults, - isLoading, - error, - } = useQuery( - ['overseerr', debounced], - async () => { - const res = await axios.get(`/api/modules/overseerr?query=${debounced}`); - return res.data.results ?? []; - }, - { - enabled: - isOverseerrEnabled === true && - selectedSearchEngine.value === 'overseerr' && - debounced.length > 3, - refetchOnWindowFocus: false, - refetchOnMount: false, - refetchInterval: false, - } - ); + const isOverseerrSearchEnabled = + isOverseerrEnabled === true && + selectedSearchEngine.value === 'overseerr' && + debounced.length > 3; + + const { data: overseerrResults } = useOverseerrSearchQuery(debounced, isOverseerrSearchEnabled); const isModuleEnabled = config?.settings.customization.layout.enabledSearchbar; if (!isModuleEnabled) { @@ -173,7 +158,7 @@ export function Search() { 0 && opened && searchQuery.length > 3) ?? + (overseerrResults && overseerrResults.length > 0 && opened && searchQuery.length > 3) ?? false } position="bottom" @@ -224,11 +209,11 @@ export function Search() { - {OverseerrResults && - OverseerrResults.slice(0, 4).map((result: any, index: number) => ( + {overseerrResults && + overseerrResults.slice(0, 4).map((result: any, index: number) => ( - {index < OverseerrResults.length - 1 && index < 3 && ( + {index < overseerrResults.length - 1 && index < 3 && ( )} @@ -312,3 +297,19 @@ const getOpenTarget = (config: ConfigType | undefined): '_blank' | '_self' => { return config.settings.common.searchEngine.properties.openInNewTab ? '_blank' : '_self'; }; + +const useOverseerrSearchQuery = (query: string, isEnabled: boolean) => { + const { name: configName } = useConfigContext(); + return api.overseerr.all.useQuery( + { + query, + configName: configName!, + }, + { + enabled: isEnabled, + refetchOnWindowFocus: false, + refetchOnMount: false, + refetchInterval: false, + } + ); +}; diff --git a/src/config/store.ts b/src/config/store.ts index bca63e0b7..6c0a45279 100644 --- a/src/config/store.ts +++ b/src/config/store.ts @@ -1,5 +1,5 @@ -import axios from 'axios'; import { create } from 'zustand'; +import { trcpProxyClient } from '~/utils/api'; import { ConfigType } from '../types/config'; export const useConfigStore = create((set, get) => ({ @@ -13,7 +13,7 @@ export const useConfigStore = create((set, get) => ({ ], })); }, - addConfig: async (name: string, config: ConfigType, shouldSaveConfigToFileSystem = true) => { + addConfig: async (name: string, config: ConfigType) => { set((old) => ({ ...old, configs: [ @@ -21,11 +21,6 @@ export const useConfigStore = create((set, get) => ({ { value: config, increaseVersion: () => {} }, ], })); - - if (!shouldSaveConfigToFileSystem) { - return; - } - axios.put(`/api/configs/${name}`, { ...config }); }, removeConfig: (name: string) => { set((old) => ({ @@ -66,7 +61,10 @@ export const useConfigStore = create((set, get) => ({ } if (shouldSaveConfigToFileSystem) { - axios.put(`/api/configs/${name}`, { ...updatedConfig }); + trcpProxyClient.config.save.mutate({ + name, + config: updatedConfig, + }); } }, })); @@ -74,11 +72,7 @@ export const useConfigStore = create((set, get) => ({ interface UseConfigStoreType { configs: { increaseVersion: () => void; value: ConfigType }[]; initConfig: (name: string, config: ConfigType, increaseVersion: () => void) => void; - addConfig: ( - name: string, - config: ConfigType, - shouldSaveConfigToFileSystem: boolean - ) => Promise; + addConfig: (name: string, config: ConfigType) => Promise; removeConfig: (name: string) => void; updateConfig: ( name: string, diff --git a/src/hooks/icons/useGetDashboardIcons.tsx b/src/hooks/icons/useGetDashboardIcons.tsx deleted file mode 100644 index bd1cdc6f7..000000000 --- a/src/hooks/icons/useGetDashboardIcons.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { useQuery } from '@tanstack/react-query'; -import { NormalizedIconRepositoryResult } from '../../tools/server/images/abstract-icons-repository'; - -export const useGetDashboardIcons = () => - useQuery({ - queryKey: ['repository-icons'], - queryFn: async () => { - const response = await fetch('/api/icons/'); - const data = await response.json(); - return data as NormalizedIconRepositoryResult[]; - }, - refetchOnMount: false, - // Cache for infinity, refetch every so often. - cacheTime: Infinity, - staleTime: 1000 * 60 * 5, // 5 minutes - refetchOnWindowFocus: false, - }); diff --git a/src/hooks/widgets/dashDot/api.ts b/src/hooks/widgets/dashDot/api.ts index 36c82a55e..c072b3379 100644 --- a/src/hooks/widgets/dashDot/api.ts +++ b/src/hooks/widgets/dashDot/api.ts @@ -1,154 +1,94 @@ -import { useMutation, useQuery } from '@tanstack/react-query'; -import axios from 'axios'; -import { Results } from 'sabnzbd-api'; -import type { - UsenetQueueRequestParams, - UsenetQueueResponse, -} from '../../../pages/api/modules/usenet/queue'; -import type { - UsenetHistoryRequestParams, - UsenetHistoryResponse, -} from '../../../pages/api/modules/usenet/history'; -import { UsenetInfoRequestParams, UsenetInfoResponse } from '../../../pages/api/modules/usenet'; +import { useConfigContext } from '~/config/provider'; +import { RouterInputs, api } from '~/utils/api'; +import { UsenetInfoRequestParams } from '../../../pages/api/modules/usenet'; +import type { UsenetHistoryRequestParams } from '../../../pages/api/modules/usenet/history'; import { UsenetPauseRequestParams } from '../../../pages/api/modules/usenet/pause'; -import { queryClient } from '../../../tools/server/configurations/tanstack/queryClient.tool'; +import type { UsenetQueueRequestParams } from '../../../pages/api/modules/usenet/queue'; import { UsenetResumeRequestParams } from '../../../pages/api/modules/usenet/resume'; const POLLING_INTERVAL = 2000; -export const useGetUsenetInfo = (params: UsenetInfoRequestParams) => - useQuery( - ['usenetInfo', params.appId], - async () => - ( - await axios.get('/api/modules/usenet', { - params, - }) - ).data, +export const useGetUsenetInfo = ({ appId }: UsenetInfoRequestParams) => { + const { name: configName } = useConfigContext(); + + return api.usenet.info.useQuery( + { + appId, + configName: configName!, + }, { refetchInterval: POLLING_INTERVAL, keepPreviousData: true, retry: 2, - enabled: Boolean(params.appId), + enabled: !!appId, } ); +}; -export const useGetUsenetDownloads = (params: UsenetQueueRequestParams) => - useQuery( - ['usenetDownloads', ...Object.values(params)], - async () => - ( - await axios.get('/api/modules/usenet/queue', { - params, - }) - ).data, +export const useGetUsenetDownloads = (params: UsenetQueueRequestParams) => { + const { name: configName } = useConfigContext(); + return api.usenet.queue.useQuery( + { + configName: configName!, + ...params, + }, { refetchInterval: POLLING_INTERVAL, keepPreviousData: true, retry: 2, } ); +}; -export const useGetUsenetHistory = (params: UsenetHistoryRequestParams) => - useQuery( - ['usenetHistory', ...Object.values(params)], - async () => - ( - await axios.get('/api/modules/usenet/history', { - params, - }) - ).data, +export const useGetUsenetHistory = (params: UsenetHistoryRequestParams) => { + const { name: configName } = useConfigContext(); + return api.usenet.history.useQuery( + { + configName: configName!, + ...params, + }, { refetchInterval: POLLING_INTERVAL, keepPreviousData: true, retry: 2, } ); +}; -export const usePauseUsenetQueue = (params: UsenetPauseRequestParams) => - useMutation( - ['usenetPause', ...Object.values(params)], - async () => - ( - await axios.post( - '/api/modules/usenet/pause', - {}, - { - params, - } - ) - ).data, - { - async onMutate() { - await queryClient.cancelQueries(['usenetInfo', params.appId]); - const previousInfo = queryClient.getQueryData([ - 'usenetInfo', - params.appId, - ]); +export const usePauseUsenetQueueMutation = (params: UsenetPauseRequestParams) => { + const { name: configName } = useConfigContext(); + const { mutateAsync } = api.usenet.pause.useMutation(); + const utils = api.useContext(); + return async (variables: Omit) => { + await mutateAsync( + { + configName: configName!, + ...variables, + }, + { + onSettled() { + utils.usenet.info.invalidate({ appId: params.appId }); + }, + } + ); + }; +}; - if (previousInfo) { - queryClient.setQueryData(['usenetInfo', params.appId], { - ...previousInfo, - paused: true, - }); - } - - return { previousInfo }; +export const useResumeUsenetQueueMutation = (params: UsenetResumeRequestParams) => { + const { name: configName } = useConfigContext(); + const { mutateAsync } = api.usenet.resume.useMutation(); + const utils = api.useContext(); + return async (variables: Omit) => { + await mutateAsync( + { + configName: configName!, + ...variables, }, - onError(err, _, context) { - if (context?.previousInfo) { - queryClient.setQueryData( - ['usenetInfo', params.appId], - context.previousInfo - ); - } - }, - onSettled() { - queryClient.invalidateQueries(['usenetInfo', params.appId]); - }, - } - ); - -export const useResumeUsenetQueue = (params: UsenetResumeRequestParams) => - useMutation( - ['usenetResume', ...Object.values(params)], - async () => - ( - await axios.post( - '/api/modules/usenet/resume', - {}, - { - params, - } - ) - ).data, - { - async onMutate() { - await queryClient.cancelQueries(['usenetInfo', params.appId]); - const previousInfo = queryClient.getQueryData([ - 'usenetInfo', - params.appId, - ]); - - if (previousInfo) { - queryClient.setQueryData(['usenetInfo', params.appId], { - ...previousInfo, - paused: false, - }); - } - - return { previousInfo }; - }, - onError(err, _, context) { - if (context?.previousInfo) { - queryClient.setQueryData( - ['usenetInfo', params.appId], - context.previousInfo - ); - } - }, - onSettled() { - queryClient.invalidateQueries(['usenetInfo', params.appId]); - }, - } - ); + { + onSettled() { + utils.usenet.info.invalidate({ appId: params.appId }); + }, + } + ); + }; +}; diff --git a/src/hooks/widgets/download-speed/useGetNetworkSpeed.tsx b/src/hooks/widgets/download-speed/useGetNetworkSpeed.tsx index 2af50a67e..27adce34b 100644 --- a/src/hooks/widgets/download-speed/useGetNetworkSpeed.tsx +++ b/src/hooks/widgets/download-speed/useGetNetworkSpeed.tsx @@ -1,12 +1,14 @@ -import { useQuery } from '@tanstack/react-query'; -import { NormalizedDownloadQueueResponse } from '../../../types/api/downloads/queue/NormalizedDownloadQueueResponse'; +import { useConfigContext } from '~/config/provider'; +import { api } from '~/utils/api'; -export const useGetDownloadClientsQueue = () => - useQuery({ - queryKey: ['network-speed'], - queryFn: async (): Promise => { - const response = await fetch('/api/modules/downloads'); - return response.json(); +export const useGetDownloadClientsQueue = () => { + const { name: configName } = useConfigContext(); + return api.download.get.useQuery( + { + configName: configName!, }, - refetchInterval: 3000, - }); + { + refetchInterval: 3000, + } + ); +}; diff --git a/src/hooks/widgets/media-servers/useGetMediaServers.tsx b/src/hooks/widgets/media-servers/useGetMediaServers.tsx index 7b3d10164..7a0b86029 100644 --- a/src/hooks/widgets/media-servers/useGetMediaServers.tsx +++ b/src/hooks/widgets/media-servers/useGetMediaServers.tsx @@ -1,17 +1,20 @@ -import { useQuery } from '@tanstack/react-query'; -import { MediaServersResponseType } from '../../../types/api/media-server/response'; +import { useConfigContext } from '~/config/provider'; +import { api } from '~/utils/api'; interface GetMediaServersParams { enabled: boolean; } -export const useGetMediaServers = ({ enabled }: GetMediaServersParams) => - useQuery({ - queryKey: ['media-servers'], - queryFn: async (): Promise => { - const response = await fetch('/api/modules/media-server'); - return response.json(); +export const useGetMediaServers = ({ enabled }: GetMediaServersParams) => { + const { name: configName } = useConfigContext(); + + return api.mediaServer.all.useQuery( + { + configName: configName!, }, - enabled, - refetchInterval: 10 * 1000, - }); + { + enabled, + refetchInterval: 10 * 1000, + } + ); +}; diff --git a/src/modules/Docker/ContainerActionBar.tsx b/src/modules/Docker/ContainerActionBar.tsx index 4a8e98177..27dc4c400 100644 --- a/src/modules/Docker/ContainerActionBar.tsx +++ b/src/modules/Docker/ContainerActionBar.tsx @@ -10,56 +10,16 @@ import { IconRotateClockwise, IconTrash, } from '@tabler/icons-react'; -import axios from 'axios'; import Dockerode from 'dockerode'; import { useTranslation } from 'next-i18next'; import { useState } from 'react'; import { v4 as uuidv4 } from 'uuid'; +import { RouterInputs, api } from '~/utils/api'; import { useConfigContext } from '../../config/provider'; import { openContextModalGeneric } from '../../tools/mantineModalManagerExtensions'; import { MatchingImages, ServiceType, tryMatchPort } from '../../tools/types'; import { AppType } from '../../types/app'; -function sendDockerCommand( - action: string, - containerId: string, - containerName: string, - reload: () => void, - t: (key: string) => string, -) { - notifications.show({ - id: containerId, - loading: true, - title: `${t(`actions.${action}.start`)} ${containerName}`, - message: undefined, - autoClose: false, - withCloseButton: false, - }); - axios - .get(`/api/docker/container/${containerId}?action=${action}`) - .then((res) => { - notifications.show({ - id: containerId, - title: containerName, - message: `${t(`actions.${action}.end`)} ${containerName}`, - icon: , - autoClose: 2000, - }); - }) - .catch((err) => { - notifications.update({ - id: containerId, - color: 'red', - title: t('errors.unknownError.title'), - message: err.response.data.reason, - autoClose: 2000, - }); - }) - .finally(() => { - reload(); - }); -} - export interface ContainerActionBarProps { selected: Dockerode.ContainerInfo[]; reload: () => void; @@ -68,8 +28,9 @@ export interface ContainerActionBarProps { export default function ContainerActionBar({ selected, reload }: ContainerActionBarProps) { const { t } = useTranslation('modules/docker'); const [isLoading, setisLoading] = useState(false); - const { name: configName, config } = useConfigContext(); + const { config } = useConfigContext(); const getLowestWrapper = () => config?.wrappers.sort((a, b) => a.position - b.position)[0]; + const sendDockerCommand = useDockerActionMutation(); if (process.env.DISABLE_EDIT_MODE === 'true') { return null; @@ -96,11 +57,7 @@ export default function ContainerActionBar({ selected, reload }: ContainerAction ) : ( - diff --git a/src/widgets/useNet/UsenetHistoryList.tsx b/src/widgets/useNet/UsenetHistoryList.tsx index a012e4689..1c129fb93 100644 --- a/src/widgets/useNet/UsenetHistoryList.tsx +++ b/src/widgets/useNet/UsenetHistoryList.tsx @@ -13,14 +13,13 @@ import { } from '@mantine/core'; import { useElementSize } from '@mantine/hooks'; import { IconAlertCircle } from '@tabler/icons-react'; -import { AxiosError } from 'axios'; import dayjs from 'dayjs'; import duration from 'dayjs/plugin/duration'; import { useTranslation } from 'next-i18next'; import { FunctionComponent, useState } from 'react'; import { useGetUsenetHistory } from '../../hooks/widgets/dashDot/api'; -import { humanFileSize } from '../../tools/humanFileSize'; import { parseDuration } from '../../tools/client/parseDuration'; +import { humanFileSize } from '../../tools/humanFileSize'; dayjs.extend(duration); @@ -65,7 +64,7 @@ export const UsenetHistoryList: FunctionComponent = ({ a > {t('modules/usenet:history.error.message')} - {(error as AxiosError)?.response?.data as string} + {error.message} diff --git a/src/widgets/useNet/UsenetQueueList.tsx b/src/widgets/useNet/UsenetQueueList.tsx index b3ed643dd..e5a613e86 100644 --- a/src/widgets/useNet/UsenetQueueList.tsx +++ b/src/widgets/useNet/UsenetQueueList.tsx @@ -16,7 +16,6 @@ import { } from '@mantine/core'; import { useElementSize } from '@mantine/hooks'; import { IconAlertCircle, IconPlayerPause, IconPlayerPlay } from '@tabler/icons-react'; -import { AxiosError } from 'axios'; import dayjs from 'dayjs'; import duration from 'dayjs/plugin/duration'; import { useTranslation } from 'next-i18next'; @@ -70,7 +69,7 @@ export const UsenetQueueList: FunctionComponent = ({ appId > {t('queue.error.message')} - {(error as AxiosError)?.response?.data as string} + {error.data} diff --git a/tsconfig.json b/tsconfig.json index 922f76b10..1ca49efb1 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -22,7 +22,10 @@ { "name": "next" } - ] + ], + "paths": { + "~/*": ["./src/*"] + }, }, "include": [ "next-env.d.ts", diff --git a/vitest.config.ts b/vitest.config.ts index fdeb8aa25..566d1cfcc 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,10 +1,11 @@ import react from '@vitejs/plugin-react'; +import tsconfigPaths from 'vite-tsconfig-paths'; import { configDefaults, defineConfig } from 'vitest/config'; // https://vitejs.dev/config/ export default defineConfig({ - plugins: [react()], + plugins: [react(), tsconfigPaths()], test: { environment: 'happy-dom', coverage: { @@ -14,9 +15,6 @@ export default defineConfig({ exclude: ['.next/', '.yarn/', 'data/'], }, setupFiles: ['./tests/setupVitest.ts'], - exclude: [ - ...configDefaults.exclude, - '.next', - ], + exclude: [...configDefaults.exclude, '.next'], }, }); diff --git a/yarn.lock b/yarn.lock index 81f3f4a7e..00cbee4d1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2011,6 +2011,52 @@ __metadata: languageName: node linkType: hard +"@trpc/client@npm:^10.29.1": + version: 10.29.1 + resolution: "@trpc/client@npm:10.29.1" + peerDependencies: + "@trpc/server": 10.29.1 + checksum: b617edb56e9ec7fa0665703761c666244e58b8a9da5a2fffcdef880cdde7ec8c92c20d1362ac1c836904518d94db247ec286cc17e01fc4eea41b7cafbf3479fe + languageName: node + linkType: hard + +"@trpc/next@npm:^10.29.1": + version: 10.29.1 + resolution: "@trpc/next@npm:10.29.1" + dependencies: + react-ssr-prepass: ^1.5.0 + peerDependencies: + "@tanstack/react-query": ^4.18.0 + "@trpc/client": 10.29.1 + "@trpc/react-query": 10.29.1 + "@trpc/server": 10.29.1 + next: "*" + react: ">=16.8.0" + react-dom: ">=16.8.0" + checksum: 6afd6d7b1702eda5a26e7e8d2d381729fceb180952a2382314f08dd945b6e3204716c532b0ac6ddef10135816879c61dbe4b27aa88bcd98100890ede8d42fb25 + languageName: node + linkType: hard + +"@trpc/react-query@npm:^10.29.1": + version: 10.29.1 + resolution: "@trpc/react-query@npm:10.29.1" + peerDependencies: + "@tanstack/react-query": ^4.18.0 + "@trpc/client": 10.29.1 + "@trpc/server": 10.29.1 + react: ">=16.8.0" + react-dom: ">=16.8.0" + checksum: 3e52195eb894c0110b7fd99b0ab4fde38ade0b28d76c788d602c97789b76c66c91f678460fd68f3dc67c9b454340c7fe08363dfec69d4127982f9329bb42f503 + languageName: node + linkType: hard + +"@trpc/server@npm:^10.29.1": + version: 10.29.1 + resolution: "@trpc/server@npm:10.29.1" + checksum: d31ac9921764aea774ef14ae3d9d4c7cb245582dde7f75facd355732043eb9d697a8584c19377dce9acdf9b21f473917f77842fda29eb77fd604484e23b42ded + languageName: node + linkType: hard + "@tsconfig/node10@npm:^1.0.7": version: 1.0.9 resolution: "@tsconfig/node10@npm:1.0.9" @@ -5617,6 +5663,13 @@ __metadata: languageName: node linkType: hard +"globrex@npm:^0.1.2": + version: 0.1.2 + resolution: "globrex@npm:0.1.2" + checksum: adca162494a176ce9ecf4dd232f7b802956bb1966b37f60c15e49d2e7d961b66c60826366dc2649093cad5a0d69970cfa8875bd1695b5a1a2f33dcd2aa88da3c + languageName: node + linkType: hard + "gopd@npm:^1.0.1": version: 1.0.1 resolution: "gopd@npm:1.0.1" @@ -5828,6 +5881,10 @@ __metadata: "@tanstack/react-query-persist-client": ^4.28.0 "@testing-library/jest-dom": ^5.16.5 "@testing-library/react": ^14.0.0 + "@trpc/client": ^10.29.1 + "@trpc/next": ^10.29.1 + "@trpc/react-query": ^10.29.1 + "@trpc/server": ^10.29.1 "@types/dockerode": ^3.3.9 "@types/node": 18.16.17 "@types/prismjs": ^1.26.0 @@ -5880,6 +5937,7 @@ __metadata: typescript: ^5.0.4 uuid: ^9.0.0 video.js: ^8.0.3 + vite-tsconfig-paths: ^4.2.0 vitest: ^0.32.0 vitest-fetch-mock: ^0.2.2 xml-js: ^1.6.11 @@ -8414,6 +8472,15 @@ __metadata: languageName: node linkType: hard +"react-ssr-prepass@npm:^1.5.0": + version: 1.5.0 + resolution: "react-ssr-prepass@npm:1.5.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + checksum: efe89b9f8b2053474613f56dfdbeb41d8ee2e572bf819a39377d95d3e4a9acf8a4a16e28d8d8034cb9ac2b316d11dc9e62217743e4322046d08175eb3b4fed3e + languageName: node + linkType: hard + "react-style-singleton@npm:^2.2.1": version: 2.2.1 resolution: "react-style-singleton@npm:2.2.1" @@ -9507,6 +9574,20 @@ __metadata: languageName: node linkType: hard +"tsconfck@npm:^2.1.0": + version: 2.1.1 + resolution: "tsconfck@npm:2.1.1" + peerDependencies: + typescript: ^4.3.5 || ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + bin: + tsconfck: bin/tsconfck.js + checksum: c531525f39763cbbd7e6dbf5e29f12a7ae67eb8712816c14d06a9db6cbdc9dda9ac3cd6db07ef645f8a4cdea906447ab44e2c8679e320871cf9dd598756e8c83 + languageName: node + linkType: hard + "tsconfig-paths@npm:^3.14.1": version: 3.14.2 resolution: "tsconfig-paths@npm:3.14.2" @@ -9981,6 +10062,22 @@ __metadata: languageName: node linkType: hard +"vite-tsconfig-paths@npm:^4.2.0": + version: 4.2.0 + resolution: "vite-tsconfig-paths@npm:4.2.0" + dependencies: + debug: ^4.1.1 + globrex: ^0.1.2 + tsconfck: ^2.1.0 + peerDependencies: + vite: "*" + peerDependenciesMeta: + vite: + optional: true + checksum: 73a8467de72d7ac502328454fd00c19571cd4bad2dd5982643b24718bb95e449a3f4153cfc2d58a358bfc8f37e592fb442fc10884b59ae82138c1329160cd952 + languageName: node + linkType: hard + "vite@npm:^3.0.0 || ^4.0.0": version: 4.3.9 resolution: "vite@npm:4.3.9"