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 (