From dabbb95ecc013caf6e8a5b603bd787b2bef19b34 Mon Sep 17 00:00:00 2001 From: Christian Benincasa Date: Fri, 22 Mar 2024 09:28:11 -0400 Subject: [PATCH] WIP --- server/src/api/plexServersApi.ts | 6 +- web/src/external/api.ts | 24 + web/src/pages/settings/PlexSettingsPage.tsx | 505 +++++++++++--------- 3 files changed, 300 insertions(+), 235 deletions(-) diff --git a/server/src/api/plexServersApi.ts b/server/src/api/plexServersApi.ts index e3c48695c..32dc4af89 100644 --- a/server/src/api/plexServersApi.ts +++ b/server/src/api/plexServersApi.ts @@ -7,13 +7,13 @@ import { import { PlexServerSettingsSchema } from '@tunarr/types/schemas'; import { isError, isNil, isObject } from 'lodash-es'; import z from 'zod'; +import { PlexServerSettings } from '../dao/entities/PlexServerSettings.js'; import createLogger from '../logger.js'; import { Plex, PlexApiFactory } from '../plex.js'; import { scheduledJobsById } from '../services/scheduler.js'; import { UpdateXmlTvTask } from '../tasks/updateXmlTvTask.js'; import { RouterPluginAsyncCallback } from '../types/serverType.js'; import { firstDefined, wait } from '../util.js'; -import { PlexServerSettings } from '../dao/entities/PlexServerSettings.js'; const logger = createLogger(import.meta); @@ -51,7 +51,7 @@ export const plexServersRouter: RouterPluginAsyncCallback = async ( response: { 200: z.object({ // TODO Change this, this is very stupid - status: z.union([z.literal(1), z.literal(-1)]), + status: z.boolean(), }), 404: z.void(), 500: z.void(), @@ -80,7 +80,7 @@ export const plexServersRouter: RouterPluginAsyncCallback = async ( ]); return res.send({ - status: s, + status: s === 1, }); } catch (err) { logger.error(err); diff --git a/web/src/external/api.ts b/web/src/external/api.ts index 2636467e3..ad4a70fd3 100644 --- a/web/src/external/api.ts +++ b/web/src/external/api.ts @@ -1,4 +1,5 @@ import { + BaseErrorSchema, BatchLookupExternalProgrammingSchema, CreateCustomShowRequestSchema, CreateFillerListRequestSchema, @@ -196,6 +197,29 @@ export const api = makeApi([ .build(), response: z.any(), }, + { + method: 'get', + path: '/api/plex-servers/:id/status', + alias: 'getPlexServerStatus', + parameters: parametersBuilder() + .addPaths({ + id: z.string(), + }) + .build(), + response: z.object({ + healthy: z.boolean(), + }), + errors: makeErrors([ + { + status: 404, + schema: BaseErrorSchema, + }, + { + status: 500, + schema: BaseErrorSchema, + }, + ]), + }, { method: 'get', path: '/api/jobs', diff --git a/web/src/pages/settings/PlexSettingsPage.tsx b/web/src/pages/settings/PlexSettingsPage.tsx index 643b01c52..8526940c6 100644 --- a/web/src/pages/settings/PlexSettingsPage.tsx +++ b/web/src/pages/settings/PlexSettingsPage.tsx @@ -49,13 +49,13 @@ import { Tooltip, Typography, } from '@mui/material'; -import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { PlexServerSettings, PlexStreamSettings, defaultPlexStreamSettings, } from '@tunarr/types'; -import { fill, isNil, isNull, isUndefined } from 'lodash-es'; +import { fill, isNil, map } from 'lodash-es'; import React, { Fragment, useCallback, useEffect, useState } from 'react'; import { Controller, SubmitHandler, useForm } from 'react-hook-form'; import AddPlexServer from '../../components/settings/AddPlexServer.tsx'; @@ -99,23 +99,73 @@ const supportedAudioBoost = [ { value: 180, string: '4 Seconds' }, ]; -export default function PlexSettingsPage() { - const { - data: servers, - isPending: serversPending, - error: serversError, - } = usePlexServerSettings(); +type PlexServerDeleteDialogProps = { + open: boolean; + onClose: () => void; + serverId: string; +}; + +function PlexServerDeleteDialog({ + open, + onClose, + serverId, +}: PlexServerDeleteDialogProps) { + const queryClient = useQueryClient(); + const removePlexServerMutation = useMutation({ + mutationFn: (id: string) => { + return apiClient.deletePlexServer(null, { params: { id } }); + }, + onSuccess: () => { + return queryClient.invalidateQueries({ + queryKey: ['settings', 'plex-servers'], + }); + }, + }); - const { - data: streamSettings, - isPending: streamSettingsPending, - error: streamsError, - } = usePlexStreamSettings(); + const titleId = `delete-plex-server-${serverId}-title`; + const descId = `delete-plex-server-${serverId}-description`; - const queryClient = useQueryClient(); + return ( + + Delete Plex Server? + + + Deleting a Plex server will remove all programming from your channels + associated with this plex server. Missing programming will be replaced + with Flex time. This action cannot be undone. + + + + + + + + ); +} + +type PlexServerRowProps = { + server: PlexServerSettings; + // isEditing: boolean, +}; - const [currentEditRow, setCurrentEditRow] = useState(null); +function PlexServerRow({ server }: PlexServerRowProps) { + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); + const [isEditing, setIsEditing] = useState(false); const [showAccessToken, setShowAccessToken] = useState(false); + const UIRouteSuccess = true; + const { data: backendStatus } = useQuery({ + queryKey: ['plex-servers', server.id, 'status'], + queryFn: () => apiClient.getPlexServerStatus({ params: { id: server.id } }), + }); + const backendRouteSuccess = true; + const queryClient = useQueryClient(); const { reset, @@ -134,15 +184,213 @@ export default function PlexSettingsPage() { }, }); + const updatePlexServerMutation = useMutation({ + mutationFn: (updatedServer: PlexServerSettings) => { + return apiClient.updatePlexServer(updatedServer, { + params: { id: updatedServer.id }, + }); + }, + onSuccess: () => { + setIsEditing(false); + return queryClient.invalidateQueries({ + queryKey: ['settings', 'plex-servers'], + }); + }, + }); + + const savePlexServer: SubmitHandler> = + useCallback( + (data) => { + if (isEditing) { + updatePlexServerMutation.mutate({ id: server.id, ...data }); + } + }, + [isEditing, server, updatePlexServerMutation], + ); + useEffect(() => { - if ( - !isNull(currentEditRow) && - !isUndefined(servers) && - servers.length > currentEditRow - ) { - reset(servers[currentEditRow]); - } - }, [currentEditRow, servers, reset]); + reset(server); + }, []); + + return ( + + + {server.name} + + {!isEditing ? ( + + {server.uri} + + ) : ( + ( + + )} + /> + )} + + + {UIRouteSuccess ? ( + + ) : ( + + )} + + + {backendRouteSuccess ? ( + + ) : ( + + )} + + + {isEditing ? ( + <> + + + + setIsEditing(false)}> + + + + ) : ( + <> + setIsEditing(true)}> + + + setDeleteDialogOpen(true)} + > + + + + )} + + + + + + + ( + + + Access Token + + + setShowAccessToken(toggle)} + edge="end" + > + {showAccessToken ? ( + + ) : ( + + )} + + + } + label="Access Token" + {...field} + /> + {errors.accessToken && ( + + {errors.accessToken.message} + + )} + + )} + /> + + ( + + )} + /> + } + label="Auto-Update Guide" + /> + + + ( + + )} + /> + } + label="Auto-Update Channels" + /> + + + + + + setDeleteDialogOpen(false)} + serverId={server.id} + /> + + ); +} + +export default function PlexSettingsPage() { + const { + data: servers, + isPending: serversPending, + error: serversError, + } = usePlexServerSettings(); + + const { + data: streamSettings, + isPending: streamSettingsPending, + error: streamsError, + } = usePlexStreamSettings(); + + const queryClient = useQueryClient(); const [deletePlexConfirmation, setDeletePlexConfirmation] = useState< string | undefined @@ -306,35 +554,6 @@ export default function PlexSettingsPage() { setShowSubtitles(!showSubtitles); }; - const updatePlexServerMutation = useMutation({ - mutationFn: (updatedServer: PlexServerSettings) => { - return apiClient.updatePlexServer(updatedServer, { - params: { id: updatedServer.id }, - }); - }, - onSuccess: () => { - setCurrentEditRow(null); - return queryClient.invalidateQueries({ - queryKey: ['settings', 'plex-servers'], - }); - }, - }); - - const savePlexServer: SubmitHandler> = - useCallback( - (data) => { - if ( - !isNull(currentEditRow) && - !isUndefined(servers) && - servers.length > currentEditRow - ) { - const originalServer = servers[currentEditRow]; - updatePlexServerMutation.mutate({ id: originalServer.id, ...data }); - } - }, - [currentEditRow, servers, updatePlexServerMutation], - ); - const renderConfirmationDialog = () => { return ( { - setCurrentEditRow(index); - setShowAccessToken(false); - }, - [setCurrentEditRow, setShowAccessToken], - ); - - const UIRouteSuccess = true; // TODO - const backendRouteSuccess = true; // TODO - // This is messy, lets consider getting rid of combine, it probably isnt useful here if (serversError || streamsError) { return

XML: {(serversError ?? streamsError)!.message}

; @@ -474,175 +682,8 @@ export default function PlexSettingsPage() { }; const getTableRows = () => { - return servers!.map((server, index) => { - const isEditing = index === currentEditRow; - return ( - - - {server.name} - - {!isEditing ? ( - - {server.uri} - - ) : ( - ( - - )} - /> - )} - - - {UIRouteSuccess ? ( - - ) : ( - - )} - - - {backendRouteSuccess ? ( - - ) : ( - - )} - - - {isEditing ? ( - <> - - - - setCurrentEditRow(null)} - > - - - - ) : ( - <> - handleEditServer(index)} - > - - - setDeletePlexConfirmation(server.id)} - > - - - - )} - - - - - - - ( - - - Access Token - - - setShowAccessToken(toggle)} - edge="end" - > - {showAccessToken ? ( - - ) : ( - - )} - - - } - label="Access Token" - {...field} - /> - {errors.accessToken && ( - - {errors.accessToken.message} - - )} - - )} - /> - - ( - - )} - /> - } - label="Auto-Update Guide" - /> - - - ( - - )} - /> - } - label="Auto-Update Channels" - /> - - - - - - - ); + return map(servers, (server) => { + return ; }); };