diff --git a/.env.docker b/.env.docker new file mode 100644 index 0000000..eca9a73 --- /dev/null +++ b/.env.docker @@ -0,0 +1,5 @@ +REACT_APP_HOST=localhost +REACT_APP_PORT=5173 +REACT_APP_FRIGATE_PROXY=http://localhost:4000 +REACT_APP_OPENID_SERVER=https://your.server.com:443/realms/your-realm +REACT_APP_CLIENT_ID=frontend-client \ No newline at end of file diff --git a/.gitignore b/.gitignore index 8be2e7f..b6f2354 100644 --- a/.gitignore +++ b/.gitignore @@ -13,13 +13,9 @@ /build # misc -.env -.env.development +.env* +!.env.docker .DS_Store -.env.local -.env.development.local -.env.test.local -.env.production.local .idea/* .vscode diff --git a/Dockerfile b/Dockerfile index 47a2a01..20a257e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,19 +1,26 @@ # syntax=docker/dockerfile:1 # Build commands: -# - rm dist -r -Force ; yarn build +# - rm build -r -Force ; yarn build # - $VERSION=0.1 -# - docker build --pull --rm -t oncharterliz/frigate-proxy:latest -t oncharterliz/frigate-proxy:$VERSION "." -# - docker image push --all-tags oncharterliz/frigate-proxy +# - docker build --pull --rm -t oncharterliz/multi-frigate:latest -t oncharterliz/multi-frigate:$VERSION "." +# - docker image push --all-tags oncharterliz/multi-frigate -FROM node:18-alpine AS frigate-proxy -ENV NODE_ENV=production +FROM nginx:alpine AS multi-frigate WORKDIR /app +COPY ./build/ /usr/share/nginx/html/ +# Nginx config +RUN rm -rf /etc/nginx/conf.d/* +COPY ./nginx/default.conf /etc/nginx/conf.d/ -COPY package.json yarn.lock ./ +# Default port exposure +EXPOSE 80 -RUN yarn install --production +# Copy enviornment script and vars +COPY env.sh .env.docker ./ +# Add bash +RUN apk add --no-cache bash +# Make our shell script executable +RUN chmod +x env.sh -COPY ./dist ./dist - -CMD yarn prod -EXPOSE 4000 \ No newline at end of file +# Start Nginx server +CMD ["/bin/bash", "-c", "/app/env.sh && nginx -g \"daemon off;\""] \ No newline at end of file diff --git a/README.md b/README.md index 0c66c5c..059ae75 100644 --- a/README.md +++ b/README.md @@ -1,30 +1,22 @@ # Instruction - - - download - - go to download directory - - run `yarn` to install packages +Frontend for [Proxy Frigate](https://github.com/NlightN22/frigate-proxy) - create file: `docker-compose.yml` ```yml -version: '3.0' +version: '3.1' services: front: - image: nginx:alpine - volumes: - - ./build/:/usr/share/nginx/html/ - - ./nginx/:/etc/nginx/conf.d/ + image: oncharterliz/multi-frigate:latest + environment: + REACT_APP_HOST: localhost + REACT_APP_PORT: 5173 + REACT_APP_FRIGATE_PROXY: http://localhost:4000 + REACT_APP_OPENID_SERVER: https://server:port/realms/your-realm + REACT_APP_CLIENT_ID: frontend-client ports: - - 8080:80 # set your port here -``` -- create file: `.env.production.local` -```bash -REACT_APP_HOST=localhost -REACT_APP_PORT=4000 -REACT_APP_OPENID_SERVER=https://server:port/realms/your-realm -REACT_APP_CLIENT_ID=your-client + - 5173:80 # set your port here ``` - run: ```bash -yarn build docker compose up -d ``` diff --git a/env.ps1 b/env.ps1 new file mode 100644 index 0000000..8e66e0b --- /dev/null +++ b/env.ps1 @@ -0,0 +1,31 @@ +$envFileName = ".env.production.local" +$envOutputFile = "./public/env-config.js" + +# Recreate config file +Remove-Item -Path $envOutputFile -Force +New-Item -Path $envOutputFile -ItemType File + +# Add assignment +Add-Content -Path $envOutputFile -Value "window.env = {" + +# Read each line in env file +foreach ($line in Get-Content -Path $envFileName) { + if ($line -match '=') { + $parts = $line -split '=', 2 + $varname = $parts[0] + $varvalue = $parts[1] + + # Read value of current variable if exists as Environment variable + $value = [System.Environment]::GetEnvironmentVariable($varname) + # Otherwise, use value from env file + if (-not $value) { + $value = $varvalue + } + + # Append configuration property to JS file + $lineToAdd = " {0}: `"{1}`"," -f $varname, $value + Add-Content -Path $envOutputFile -Value $lineToAdd + } +} + +Add-Content -Path $envOutputFile -Value "}" \ No newline at end of file diff --git a/env.sh b/env.sh new file mode 100644 index 0000000..44c59d8 --- /dev/null +++ b/env.sh @@ -0,0 +1,32 @@ +#!/bin/bash + +EnvFileName=".env.docker" +EnvOutputFile="/usr/share/nginx/html/env-config.js" + +# Recreate config file +rm -rf $EnvOutputFile +touch $EnvOutputFile + +# Add assignment +echo "window.env = {" >> $EnvOutputFile + +# Read each line in $EnvFileName file +# Each line represents key=value pairs +while read -r line || [[ -n "$line" ]]; +do + # Split env variables by character `=` + if printf '%s\n' "$line" | grep -q -e '='; then + varname=$(printf '%s\n' "$line" | sed -e 's/=.*//') + varvalue=$(printf '%s\n' "$line" | sed -e 's/^[^=]*=//') + fi + + # Read value of current variable if exists as Environment variable + value=$(printf '%s\n' "${!varname}") + # Otherwise use value from $EnvFileName file + [[ -z $value ]] && value=${varvalue} + + # Append configuration property to JS file + echo " $varname: \"$value\"," >> $EnvOutputFile +done < $EnvFileName + +echo "}" >> $EnvOutputFile \ No newline at end of file diff --git a/example/example.docker-compose.yml b/example/example.docker-compose.yml new file mode 100644 index 0000000..dfcfea1 --- /dev/null +++ b/example/example.docker-compose.yml @@ -0,0 +1,13 @@ +version: '3.0' + +services: + front: + image: oncharterliz/multi-frigate:latest + environment: + REACT_APP_HOST: localhost + REACT_APP_PORT: 5173 + REACT_APP_FRIGATE_PROXY: http://localhost:4000 + REACT_APP_OPENID_SERVER: https://server:port/realms/your-realm + REACT_APP_CLIENT_ID: frontend-client + ports: + - 5173:80 # set your port here \ No newline at end of file diff --git a/public/env-config.js b/public/env-config.js new file mode 100644 index 0000000..08a9736 --- /dev/null +++ b/public/env-config.js @@ -0,0 +1,7 @@ +window.env = { + REACT_APP_HOST: "localhost", + REACT_APP_PORT: "5173", + REACT_APP_FRIGATE_PROXY: "http://localhost:4000", + REACT_APP_OPENID_SERVER: "https://oauth.komponent-m.ru:8443/realms/frigate-realm", + REACT_APP_CLIENT_ID: "frontend-client", +} diff --git a/public/index.html b/public/index.html index 983daf2..866e034 100644 --- a/public/index.html +++ b/public/index.html @@ -25,6 +25,7 @@ Learn how to configure a non-root public URL by running `npm run build`. --> Multi Frigate + diff --git a/src/AppBody.tsx b/src/AppBody.tsx index 3894d4a..3da0dc4 100644 --- a/src/AppBody.tsx +++ b/src/AppBody.tsx @@ -6,6 +6,7 @@ import AppRouter from './router/AppRouter'; import { Context } from '.'; import SideBar from './shared/components/SideBar'; import { observer } from 'mobx-react-lite'; +import { isProduction } from './shared/env.const'; const AppBody = () => { @@ -23,7 +24,7 @@ const AppBody = () => { const theme = useMantineTheme(); - console.log("render Main") + if (!isProduction) console.log("render Main") return ( { const executed = useRef(false) @@ -45,7 +46,7 @@ const AccessSettings = () => { setRoleId(value) } - console.log('AccessSettings rendered') + if (!isProduction) console.log('AccessSettings rendered') return ( {strings.pleaseSelectRole} diff --git a/src/pages/HostConfigPage.tsx b/src/pages/HostConfigPage.tsx index 013be60..7eb6ef0 100644 --- a/src/pages/HostConfigPage.tsx +++ b/src/pages/HostConfigPage.tsx @@ -12,6 +12,7 @@ import RetryErrorPage from './RetryErrorPage'; import { useAdminRole } from '../hooks/useAdminRole'; import Forbidden from './403'; import { observer } from 'mobx-react-lite'; +import { isProduction } from '../shared/env.const'; const HostConfigPage = () => { @@ -84,7 +85,7 @@ const HostConfigPage = () => { if (!editorRef.current) { return; } - console.log('save config', save_option) + if (!isProduction) console.log('save config', save_option) }, [editorRef]) if (configPending || adminLoading) return diff --git a/src/pages/RecordingsPage.tsx b/src/pages/RecordingsPage.tsx index bb42dff..166ed30 100644 --- a/src/pages/RecordingsPage.tsx +++ b/src/pages/RecordingsPage.tsx @@ -10,6 +10,7 @@ import SelectedHostList from '../widgets/SelectedHostList'; import { dateToQueryString, parseQueryDateToDate } from '../shared/utils/dateUtil'; import SelectedDayList from '../widgets/SelectedDayList'; import CenterLoader from '../shared/components/loaders/CenterLoader'; +import { isProduction } from '../shared/env.const'; export const recordingsPageQuery = { @@ -95,7 +96,7 @@ const RecordingsPage = () => { navigate({ pathname: location.pathname, search: queryParams.toString() }); }, [recStore.selectedRange, location.pathname, navigate, queryParams]) - console.log('RecordingsPage rendered') + if (!isProduction) console.log('RecordingsPage rendered') if (!firstRender) return diff --git a/src/pages/SettingsPage.tsx b/src/pages/SettingsPage.tsx index ceeab63..6792db4 100644 --- a/src/pages/SettingsPage.tsx +++ b/src/pages/SettingsPage.tsx @@ -17,6 +17,7 @@ import { Context } from '..'; import { useAdminRole } from '../hooks/useAdminRole'; import Forbidden from './403'; import { observer } from 'mobx-react-lite'; +import { isProduction } from '../shared/env.const'; const SettingsPage = () => { const executed = useRef(false) @@ -59,17 +60,17 @@ const SettingsPage = () => { }) const handleDiscard = () => { - console.log('Discard changes') + if (!isProduction) console.log('Discard changes') refetch() setConfigs(data ? mapEncryptedToView(data) : []) } useEffect(() => { - console.log('data changed') + if (!isProduction) console.log('data changed') setConfigs(mapEncryptedToView(data)) }, [data]) useEffect(() => { - console.log('configs changed') + if (!isProduction) console.log('configs changed') }, [configs]) const handleSubmit = (event: React.FormEvent) => { @@ -95,7 +96,7 @@ const SettingsPage = () => { value: value, } }); - console.log('configsToUpdate', configsToUpdate) + if (!isProduction) console.log('configsToUpdate', configsToUpdate) mutation.mutate(configsToUpdate); } diff --git a/src/shared/components/CamerasTransferList.tsx b/src/shared/components/CamerasTransferList.tsx index cdca270..53d243c 100644 --- a/src/shared/components/CamerasTransferList.tsx +++ b/src/shared/components/CamerasTransferList.tsx @@ -6,6 +6,7 @@ import RetryError from './RetryError'; import { TransferList, Text, TransferListData, TransferListProps, TransferListItem, Button, Flex } from '@mantine/core'; import { OneSelectItem } from './filters.aps/OneSelectFilter'; import { strings } from '../strings/strings'; +import { isProduction } from '../env.const'; interface CamerasTransferListProps { roleId: string @@ -60,7 +61,7 @@ const CamerasTransferList = ({ refetch() } - console.log('CamerasTransferListProps rendered') + if (!isProduction) console.log('CamerasTransferListProps rendered') return ( <> diff --git a/src/shared/components/accordion/CameraAccordion.tsx b/src/shared/components/accordion/CameraAccordion.tsx index fac845f..662f53d 100644 --- a/src/shared/components/accordion/CameraAccordion.tsx +++ b/src/shared/components/accordion/CameraAccordion.tsx @@ -9,6 +9,7 @@ import { getResolvedTimeZone, parseQueryDateToDate } from '../../utils/dateUtil' import RetryError from '../RetryError'; import { strings } from '../../strings/strings'; import { RecordSummary } from '../../../types/record'; +import { isProduction } from '../../env.const'; const CameraAccordion = () => { const { recordingsStore: recStore } = useContext(Context) @@ -60,7 +61,7 @@ const CameraAccordion = () => { return [] } - console.log('CameraAccordion rendered') + if (!isProduction) console.log('CameraAccordion rendered') return ( diff --git a/src/shared/components/accordion/DayAccordion.tsx b/src/shared/components/accordion/DayAccordion.tsx index 82a3e1f..b14aa02 100644 --- a/src/shared/components/accordion/DayAccordion.tsx +++ b/src/shared/components/accordion/DayAccordion.tsx @@ -15,6 +15,7 @@ import { IconExternalLink, IconShare } from '@tabler/icons-react'; import { routesPath } from '../../../router/routes.path'; import AccordionShareButton from '../buttons/AccordionShareButton'; import VideoDownloader from '../../../widgets/VideoDownloader'; +import { isProduction } from '../../env.const'; interface RecordingAccordionProps { recordSummary?: RecordSummary @@ -57,7 +58,7 @@ const DayAccordion = ({ if (playedValue) { const url = createRecordURL(playedValue) if (url) { - console.log('GET URL: ', url) + if (!isProduction) console.log('GET URL: ', url) setPlayerUrl(url) } } else { @@ -85,7 +86,7 @@ const DayAccordion = ({ setVideoPlayerState(undefined) } - console.log('DayAccordion rendered') + if (!isProduction) console.log('DayAccordion rendered') const hourLabel = (hour: string, eventsQty: number) => ( diff --git a/src/shared/components/accordion/EventsAccordion.tsx b/src/shared/components/accordion/EventsAccordion.tsx index f41f3fb..dc0a23d 100644 --- a/src/shared/components/accordion/EventsAccordion.tsx +++ b/src/shared/components/accordion/EventsAccordion.tsx @@ -16,6 +16,7 @@ import AccordionControlButton from '../buttons/AccordionControlButton'; import AccordionShareButton from '../buttons/AccordionShareButton'; import { useNavigate } from 'react-router-dom'; import EventPanel from './EventPanel'; +import { isProduction } from '../../env.const'; /** * @param day frigate format, e.g day: 2024-02-23 @@ -92,10 +93,9 @@ const EventsAccordion = ({ useEffect(() => { if (playedValue) { - // console.log('openVideoPlayer', playedValue) if (playedValue && host) { const url = createEventUrl(playedValue) - console.log('GET EVENT URL: ', url) + if (!isProduction) console.log('GET EVENT URL: ', url) setPlayerUrl(url) } } else { @@ -108,8 +108,6 @@ const EventsAccordion = ({ if (!data || data.length < 1) return
Not have events at that period
const handleOpenPlayer = (openedValue: string) => { - // console.log(`openVideoPlayer day:${day} hour:${hour}, opened value: ${openedValue}`) - // console.log(`opened value: ${openedValue}, eventId: ${playedValue}`) if (openedValue !== playedValue) { setOpenedItem(openedValue) setPlayedValue(openedValue) diff --git a/src/shared/components/buttons/AccordionShareButton.tsx b/src/shared/components/buttons/AccordionShareButton.tsx index 4a8359b..c7ececc 100644 --- a/src/shared/components/buttons/AccordionShareButton.tsx +++ b/src/shared/components/buttons/AccordionShareButton.tsx @@ -3,6 +3,7 @@ import { IconShare } from '@tabler/icons-react'; import React from 'react'; import AccordionControlButton from './AccordionControlButton'; import { routesPath } from '../../../router/routes.path'; +import { isProduction } from '../../env.const'; interface AccordionShareButtonProps { recordUrl?: string @@ -19,13 +20,13 @@ const AccordionShareButton = ({ if (canShare && url) { try { await navigator.share({ url }); - console.log('Content shared successfully'); + if (!isProduction) console.log('Content shared successfully'); } catch (err) { console.error('Error sharing content: ', err); } } else { clipboard.copy(url) - console.log('URL copied to clipboard') + if (!isProduction) console.log('URL copied to clipboard') } } diff --git a/src/shared/components/filters.aps/CameraSelectFilter.tsx b/src/shared/components/filters.aps/CameraSelectFilter.tsx index df76018..24afd76 100644 --- a/src/shared/components/filters.aps/CameraSelectFilter.tsx +++ b/src/shared/components/filters.aps/CameraSelectFilter.tsx @@ -8,6 +8,7 @@ import { Center, Loader, Text } from '@mantine/core'; import OneSelectFilter, { OneSelectItem } from './OneSelectFilter'; import { strings } from '../../strings/strings'; import RetryError from '../RetryError'; +import { isProduction } from '../../env.const'; interface CameraSelectFilterProps { selectedHostId: string, @@ -26,7 +27,7 @@ const CameraSelectFilter = ({ useEffect(() => { if (!data) return if (recStore.cameraIdParam) { - console.log('change camera by param') + if (!isProduction) console.log('change camera by param') recStore.filteredCamera = data.find( camera => camera.id === recStore.cameraIdParam) recStore.cameraIdParam = undefined } @@ -47,8 +48,7 @@ const CameraSelectFilter = ({ recStore.filteredCamera = camera } - console.log('CameraSelectFilter rendered') - // console.log('recStore.selectedCameraId', recStore.selectedCameraId) + if (!isProduction) console.log('CameraSelectFilter rendered') return ( { - console.log('handlePick',value) recStore.selectedRange = value } - console.log('DateRangeSelectFilter rendered') + if (!isProduction) console.log('DateRangeSelectFilter rendered') return ( { return wsUrl; - }, [camera]); + }, [wsUrl]); const play = () => { const currentVideo = videoRef.current; @@ -70,7 +70,7 @@ function MSEPlayer({ return CODECS.filter((codec) => isSupported(`video/mp4; codecs="${codec}"`) ).join(); - }, []); + }, [CODECS]); const onConnect = useCallback(() => { if (!videoRef.current?.isConnected || !wsURL || wsRef.current) return false; @@ -252,7 +252,7 @@ function MSEPlayer({ return () => { onDisconnect(); }; - }, [playbackEnabled, onDisconnect, onConnect]); + }, [playbackEnabled, onDisconnect, onConnect, visibilityCheck]); return (