diff --git a/.gitignore b/.gitignore index c6d612fc4..03c7f66fd 100644 --- a/.gitignore +++ b/.gitignore @@ -30,6 +30,7 @@ bld/ [Oo]bj/ [Ll]og/ [Ll]ogs/ +!StreamMaster.WebUI/lib/smAPI/Logs/ # Visual Studio 2015/2017 cache/options directory .vs/ diff --git a/StreamMaster.WebUI/lib/smAPI/Logs/GetLogContentsFetch.ts b/StreamMaster.WebUI/lib/smAPI/Logs/GetLogContentsFetch.ts new file mode 100644 index 000000000..1da998982 --- /dev/null +++ b/StreamMaster.WebUI/lib/smAPI/Logs/GetLogContentsFetch.ts @@ -0,0 +1,31 @@ +import { GetLogContents } from '@lib/smAPI/Logs/LogsCommands'; +import { GetLogContentsRequest } from '../smapiTypes'; +import { isSkipToken } from '@lib/common/isSkipToken'; +import { Logger } from '@lib/common/logger'; +import { createAsyncThunk } from '@reduxjs/toolkit'; + + +export const fetchGetLogContents = createAsyncThunk('cache/getGetLogContents', async (param: GetLogContentsRequest, thunkAPI) => { + try { + if (isSkipToken(param)) + { + Logger.error('Skipping GetEPGFilePreviewById'); + return undefined; + } + Logger.debug('Fetching GetLogContents'); + const fetchDebug = localStorage.getItem('fetchDebug'); + const start = performance.now(); + const response = await GetLogContents(param); + if (fetchDebug) { + const duration = performance.now() - start; + Logger.debug(`Fetch GetLogContents completed in ${duration.toFixed(2)}ms`); + } + + return {param: param, value: response }; + } catch (error) { + console.error('Failed to fetch', error); + return thunkAPI.rejectWithValue({ error: error || 'Unknown error', value: undefined }); + } +}); + + diff --git a/StreamMaster.WebUI/lib/smAPI/Logs/GetLogContentsSlice.ts b/StreamMaster.WebUI/lib/smAPI/Logs/GetLogContentsSlice.ts new file mode 100644 index 000000000..53161d69c --- /dev/null +++ b/StreamMaster.WebUI/lib/smAPI/Logs/GetLogContentsSlice.ts @@ -0,0 +1,112 @@ +import { PayloadAction, createSlice } from '@reduxjs/toolkit'; +import { Logger } from '@lib/common/logger'; +import {FieldData, } from '@lib/smAPI/smapiTypes'; +import { fetchGetLogContents } from '@lib/smAPI/Logs/GetLogContentsFetch'; + + +interface QueryState { + data: Record; + error: Record; + isError: Record; + isForced: boolean; + isLoading: Record; +} + +const initialState: QueryState = { + data: {}, + error: {}, + isError: {}, + isForced: false, + isLoading: {} +}; + +const getLogContentsSlice = createSlice({ + initialState, + name: 'GetLogContents', + reducers: { + clear: (state) => { + state = initialState; + Logger.debug('GetLogContents clear'); + }, + + clearByTag: (state, action: PayloadAction<{ tag: string }>) => { + const tag = action.payload.tag; + for (const key in state.data) { + if (key.includes(tag)) { + state.data[key] = undefined; + } + } + Logger.debug('GetLogContents clearByTag'); + }, + + setField: (state, action: PayloadAction<{ fieldData: FieldData }>) => { + const { fieldData } = action.payload; + + if (fieldData.Entity !== undefined && state.data[fieldData.Id]) { + state.data[fieldData.Id] = fieldData.Value; + return; + } + Logger.debug('GetLogContents setField'); + }, + setIsForced: (state, action: PayloadAction<{ force: boolean }>) => { + const { force } = action.payload; + state.isForced = force; + + const updatedData = { ...state.data }; + for (const key in updatedData) { + if (updatedData[key]) { + updatedData[key] = undefined; + } + } + state.data = updatedData; + Logger.debug('GetLogContents setIsForced ', force); + }, + setIsLoading: (state, action: PayloadAction<{ param: string; isLoading: boolean }>) => { + const { param, isLoading } = action.payload; + if (param !== undefined) { + const paramString = JSON.stringify(param); + state.isLoading[paramString] = isLoading; + } else { + for (const key in state.data) { + state.isLoading[key] = action.payload.isLoading; + } + } + Logger.debug('GetLogContents setIsLoading ', action.payload.isLoading); + } + }, + + extraReducers: (builder) => { + builder + .addCase(fetchGetLogContents.pending, (state, action) => { + const paramString = JSON.stringify(action.meta.arg); + state.isLoading[paramString] = true; + state.isError[paramString] = false; + state.isForced = false; + state.error[paramString] = undefined; + }) + .addCase(fetchGetLogContents.fulfilled, (state, action) => { + if (action.payload) { + const { param, value } = action.payload; + const paramString = JSON.stringify(param); + state.data[paramString] = value; + setIsLoading({ isLoading: false, param: paramString }); + state.isLoading[paramString] = false; + state.isError[paramString] = false; + state.error[paramString] = undefined; + state.isForced = false; + } + }) + .addCase(fetchGetLogContents.rejected, (state, action) => { + const paramString = JSON.stringify(action.meta.arg); + state.error[paramString] = action.error.message || 'Failed to fetch'; + state.isError[paramString] = true; + setIsLoading({ isLoading: false, param: paramString }); + state.isLoading[paramString] = false; + state.isForced = false; + }); + + } +}); + +export const { clear, clearByTag, setIsLoading, setIsForced, setField } = getLogContentsSlice.actions; +export default getLogContentsSlice.reducer; diff --git a/StreamMaster.WebUI/lib/smAPI/Logs/GetLogNamesFetch.ts b/StreamMaster.WebUI/lib/smAPI/Logs/GetLogNamesFetch.ts new file mode 100644 index 000000000..73c280063 --- /dev/null +++ b/StreamMaster.WebUI/lib/smAPI/Logs/GetLogNamesFetch.ts @@ -0,0 +1,24 @@ +import { GetLogNames } from '@lib/smAPI/Logs/LogsCommands'; +import { Logger } from '@lib/common/logger'; +import { createAsyncThunk } from '@reduxjs/toolkit'; + + +export const fetchGetLogNames = createAsyncThunk('cache/getGetLogNames', async (_: void, thunkAPI) => { + try { + Logger.debug('Fetching GetLogNames'); + const fetchDebug = localStorage.getItem('fetchDebug'); + const start = performance.now(); + const response = await GetLogNames(); + if (fetchDebug) { + const duration = performance.now() - start; + Logger.debug(`Fetch GetLogNames completed in ${duration.toFixed(2)}ms`); + } + + return {param: _, value: response }; + } catch (error) { + console.error('Failed to fetch', error); + return thunkAPI.rejectWithValue({ error: error || 'Unknown error', value: undefined }); + } +}); + + diff --git a/StreamMaster.WebUI/lib/smAPI/Logs/GetLogNamesSlice.ts b/StreamMaster.WebUI/lib/smAPI/Logs/GetLogNamesSlice.ts new file mode 100644 index 000000000..308d25969 --- /dev/null +++ b/StreamMaster.WebUI/lib/smAPI/Logs/GetLogNamesSlice.ts @@ -0,0 +1,85 @@ +import { PayloadAction, createSlice } from '@reduxjs/toolkit'; +import { Logger } from '@lib/common/logger'; +import {FieldData, } from '@lib/smAPI/smapiTypes'; +import { fetchGetLogNames } from '@lib/smAPI/Logs/GetLogNamesFetch'; +import { updateFieldInData } from '@lib/redux/updateFieldInData'; + + +interface QueryState { + data: string[] | undefined; + error: string | undefined; + isError: boolean; + isForced: boolean; + isLoading: boolean; +} + +const initialState: QueryState = { + data: undefined, + error: undefined, + isError: false, + isForced: false, + isLoading: false +}; + +const getLogNamesSlice = createSlice({ + initialState, + name: 'GetLogNames', + reducers: { + clear: (state) => { + state = initialState; + Logger.debug('GetLogNames clear'); + }, + + clearByTag: (state, action: PayloadAction<{ tag: string }>) => { + state.data = undefined; + Logger.debug('GetLogNames clearByTag'); + }, + + setField: (state, action: PayloadAction<{ fieldData: FieldData }>) => { + const { fieldData } = action.payload; + state.data = updateFieldInData(state.data, fieldData); + Logger.debug('GetLogNames setField'); + }, + setIsForced: (state, action: PayloadAction<{ force: boolean }>) => { + const { force } = action.payload; + state.isForced = force; + Logger.debug('GetLogNames setIsForced ', force); + }, + setIsLoading: (state, action: PayloadAction<{isLoading: boolean }>) => { + state.isLoading = action.payload.isLoading; + Logger.debug('GetLogNames setIsLoading ', action.payload.isLoading); + } +}, + + extraReducers: (builder) => { + builder + .addCase(fetchGetLogNames.pending, (state, action) => { + state.isLoading = true; + state.isError = false; + state.error = undefined; + state.isForced = false; + }) + .addCase(fetchGetLogNames.fulfilled, (state, action) => { + if (action.payload) { + const { value } = action.payload; + state.data = value ?? undefined; + setIsLoading({ isLoading: false }); + state.isLoading = false; + state.isError = false; + state.error = undefined; + state.isForced = false; + } + }) + .addCase(fetchGetLogNames.rejected, (state, action) => { + state.error = action.error.message || 'Failed to fetch'; + state.isError = true; + setIsLoading({ isLoading: false }); + state.isLoading = false; + state.isForced = false; + }); + + } +}); + +export const { clear, clearByTag, setIsLoading, setIsForced, setField } = getLogNamesSlice.actions; +export default getLogNamesSlice.reducer; diff --git a/StreamMaster.WebUI/lib/smAPI/Logs/LogsCommands.ts b/StreamMaster.WebUI/lib/smAPI/Logs/LogsCommands.ts new file mode 100644 index 000000000..74b431d78 --- /dev/null +++ b/StreamMaster.WebUI/lib/smAPI/Logs/LogsCommands.ts @@ -0,0 +1,17 @@ +import { isSkipToken } from '@lib/common/isSkipToken'; +import SignalRService from '@lib/signalr/SignalRService'; +import { GetLogContentsRequest } from '@lib/smAPI/smapiTypes'; + +export const GetLogContents = async (request: GetLogContentsRequest): Promise => { + if ( request === undefined ) { + return undefined; + } + const signalRService = SignalRService.getInstance(); + return await signalRService.invokeHubCommand('GetLogContents', request); +}; + +export const GetLogNames = async (): Promise => { + const signalRService = SignalRService.getInstance(); + return await signalRService.invokeHubCommand('GetLogNames'); +}; + diff --git a/StreamMaster.WebUI/lib/smAPI/Logs/useGetLogContents.tsx b/StreamMaster.WebUI/lib/smAPI/Logs/useGetLogContents.tsx new file mode 100644 index 000000000..359a9f84f --- /dev/null +++ b/StreamMaster.WebUI/lib/smAPI/Logs/useGetLogContents.tsx @@ -0,0 +1,115 @@ +import { QueryHookResult } from '@lib/apiDefs'; +import store, { RootState } from '@lib/redux/store'; +import { useAppDispatch, useAppSelector } from '@lib/redux/hooks'; +import { clear, clearByTag, setField, setIsForced, setIsLoading } from './GetLogContentsSlice'; +import { useCallback,useEffect } from 'react'; +import { useSMContext } from '@lib/context/SMProvider'; +import { SkipToken } from '@reduxjs/toolkit/query'; +import { getParameters } from '@lib/common/getParameters'; +import { fetchGetLogContents } from './GetLogContentsFetch'; +import {FieldData, GetLogContentsRequest } from '@lib/smAPI/smapiTypes'; + +interface ExtendedQueryHookResult extends QueryHookResult {} +interface Result extends ExtendedQueryHookResult { + Clear: () => void; + ClearByTag: (tag: string) => void; + SetField: (fieldData: FieldData) => void; + SetIsForced: (force: boolean) => void; + SetIsLoading: (isLoading: boolean, query: string) => void; +} +const useGetLogContents = (params?: GetLogContentsRequest | undefined | SkipToken): Result => { + const { isSystemReady } = useSMContext(); + const dispatch = useAppDispatch(); + const param = getParameters(params); + const isForced = useAppSelector((state) => state.GetLogContents.isForced ?? false); + + const SetIsForced = useCallback( + (forceRefresh: boolean): void => { + dispatch(setIsForced({ force: forceRefresh })); + }, + [dispatch] + ); + const ClearByTag = useCallback( + (tag: string): void => { + dispatch(clearByTag({tag: tag })); + }, + [dispatch] + ); + + + + const SetIsLoading = useCallback( + (isLoading: boolean, param: string): void => { + if (param === undefined) return; + dispatch(setIsLoading({ isLoading: isLoading, param: param })); + }, + [dispatch] + ); + +const selectData = (state: RootState) => { + if (param === undefined) return undefined; + return state.GetLogContents.data[param] || undefined; + }; +const data = useAppSelector(selectData); + +const selectError = (state: RootState) => { + if (param === undefined) return undefined; + return state.GetLogContents.error[param] || undefined; + }; +const error = useAppSelector(selectError); + +const selectIsError = (state: RootState) => { + if (param === undefined) return false; + return state.GetLogContents.isError[param] || false; + }; +const isError = useAppSelector(selectIsError); + +const selectIsLoading = (state: RootState) => { + if (param === undefined) return false; + return state.GetLogContents.isLoading[param] || false; + }; +const isLoading = useAppSelector(selectIsLoading); + + +useEffect(() => { + if (param === undefined) return; + const state = store.getState().GetLogContents; + if (data === undefined && state.isLoading[param] !== true && state.isForced !== true) { + SetIsForced(true); + } +}, [data, param, SetIsForced]); + +useEffect(() => { + if (!isSystemReady) return; + if (param === undefined) return; + const state = store.getState().GetLogContents; + if (params === undefined || param === undefined || param === '{}' ) return; + if (state.isLoading[param]) return; + if (data !== undefined && !isForced) return; + + SetIsLoading(true, param); + dispatch(fetchGetLogContents(params as GetLogContentsRequest)); +}, [SetIsLoading, data, dispatch, isForced, isSystemReady, param, params]); + +const SetField = (fieldData: FieldData): void => { + dispatch(setField({ fieldData: fieldData })); +}; + +const Clear = (): void => { + dispatch(clear()); +}; + +return { + Clear, + ClearByTag, + data, + error, + isError, + isLoading, + SetField, + SetIsForced, + SetIsLoading +}; +}; + +export default useGetLogContents; diff --git a/StreamMaster.WebUI/lib/smAPI/Logs/useGetLogNames.tsx b/StreamMaster.WebUI/lib/smAPI/Logs/useGetLogNames.tsx new file mode 100644 index 000000000..edbf27eca --- /dev/null +++ b/StreamMaster.WebUI/lib/smAPI/Logs/useGetLogNames.tsx @@ -0,0 +1,103 @@ +import { QueryHookResult } from '@lib/apiDefs'; +import store, { RootState } from '@lib/redux/store'; +import { useAppDispatch, useAppSelector } from '@lib/redux/hooks'; +import { clear, clearByTag, setField, setIsForced, setIsLoading } from './GetLogNamesSlice'; +import { useCallback,useEffect } from 'react'; +import { useSMContext } from '@lib/context/SMProvider'; +import { fetchGetLogNames } from './GetLogNamesFetch'; +import {FieldData, } from '@lib/smAPI/smapiTypes'; + +interface ExtendedQueryHookResult extends QueryHookResult {} +interface Result extends ExtendedQueryHookResult { + Clear: () => void; + ClearByTag: (tag: string) => void; + SetField: (fieldData: FieldData) => void; + SetIsForced: (force: boolean) => void; + SetIsLoading: (isLoading: boolean, query: string) => void; +} +const useGetLogNames = (): Result => { + const { isSystemReady } = useSMContext(); + const dispatch = useAppDispatch(); + const isForced = useAppSelector((state) => state.GetLogNames.isForced ?? false); + + const SetIsForced = useCallback( + (forceRefresh: boolean): void => { + dispatch(setIsForced({ force: forceRefresh })); + }, + [dispatch] + ); + const ClearByTag = useCallback( + (tag: string): void => { + dispatch(clearByTag({tag: tag })); + }, + [dispatch] + ); + + + +const SetIsLoading = useCallback( + (isLoading: boolean): void => { + dispatch(setIsLoading({ isLoading: isLoading })); + }, + [dispatch] +); +const selectData = (state: RootState) => { + return state.GetLogNames.data; + }; +const data = useAppSelector(selectData); + +const selectError = (state: RootState) => { + return state.GetLogNames.error; + }; +const error = useAppSelector(selectError); + +const selectIsError = (state: RootState) => { + return state.GetLogNames.isError; + }; +const isError = useAppSelector(selectIsError); + +const selectIsLoading = (state: RootState) => { + return state.GetLogNames.isLoading; + }; +const isLoading = useAppSelector(selectIsLoading); + + + useEffect(() => { + const state = store.getState().GetLogNames; + if (data === undefined && state.isLoading !== true && state.isForced !== true) { + SetIsForced(true); + } + }, [SetIsForced, data]); + +useEffect(() => { + if (!isSystemReady) return; + const state = store.getState().GetLogNames; + if (state.isLoading) return; + if (data !== undefined && !isForced) return; + + SetIsLoading(true); + dispatch(fetchGetLogNames()); +}, [SetIsLoading, data, dispatch, isForced, isSystemReady]); + +const SetField = (fieldData: FieldData): void => { + dispatch(setField({ fieldData: fieldData })); +}; + +const Clear = (): void => { + dispatch(clear()); +}; + +return { + Clear, + ClearByTag, + data, + error, + isError, + isLoading, + SetField, + SetIsForced, + SetIsLoading +}; +}; + +export default useGetLogNames;