diff --git a/react-ui/docker/webserver/Dockerfile b/react-ui/docker/webserver/Dockerfile index 71127c09b528b49714281e3dfc703b18d1902103..9c4f3f6765bed330e062267d329db9f2f5e04ce9 100644 --- a/react-ui/docker/webserver/Dockerfile +++ b/react-ui/docker/webserver/Dockerfile @@ -3,7 +3,11 @@ FROM node:alpine3.20 as builder COPY ./api/openapiv2/gosdn_northbound.swagger.json /app/api/openapiv2/gosdn_northbound.swagger.json COPY ./react-ui /app/react-ui -RUN cd /app/react-ui && yarn && yarn build +RUN cd /app/react-ui && \ + rm -rf node_modules && \ + rm yarn.lock && \ + yarn install --production && \ + yarn build # webserver diff --git a/react-ui/docker/webserver/nginx.conf b/react-ui/docker/webserver/nginx.conf index eb4fc2be925602e892b2bdb0a134d5016cc2c736..4ddf7f20dbef09490329249791126f6aa610dc72 100644 --- a/react-ui/docker/webserver/nginx.conf +++ b/react-ui/docker/webserver/nginx.conf @@ -1,51 +1,82 @@ - -#user nobody; worker_processes 1; -#error_log logs/error.log; -#error_log logs/error.log notice; -#error_log logs/error.log info; - -#pid logs/nginx.pid; - - events { worker_connections 1024; + multi_accept on; + use epoll; } http { include mime.types; default_type application/octet-stream; - #log_format main '$remote_addr - $remote_user [$time_local] "$request" ' - # '$status $body_bytes_sent "$http_referer" ' - # '"$http_user_agent" "$http_x_forwarded_for"'; - - #access_log logs/access.log main; - - sendfile on; - #tcp_nopush on; - - #keepalive_timeout 0; - keepalive_timeout 65; + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + access_log /var/log/nginx/access.log main buffer=16k; + + sendfile on; + tcp_nopush on; + tcp_nodelay on; + keepalive_timeout 65; + types_hash_max_size 2048; + server_tokens off; + + # Buffer size settings + client_body_buffer_size 10K; + client_header_buffer_size 1k; + client_max_body_size 8m; + large_client_header_buffers 2 1k; + + # File descriptor cache + open_file_cache max=2000 inactive=20s; + open_file_cache_valid 60s; + open_file_cache_min_uses 5; + open_file_cache_errors off; + + # Compression settings + gzip on; + gzip_comp_level 6; + gzip_min_length 256; + gzip_proxied any; + gzip_vary on; + gzip_types + application/javascript + application/json + application/x-javascript + application/xml + text/css + text/javascript + text/plain + text/xml + text/html + application/x-font-ttf + font/opentype + application/vnd.ms-fontobject + image/svg+xml; resolver 127.0.0.11 ipv6=off; - #gzip on; - server { listen 80; server_name localhost; - #charset koi8-r; - - #access_log logs/host.access.log main; - location ^~ /api/ { proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; + + # Proxy timeouts + proxy_connect_timeout 60s; + proxy_send_timeout 60s; + proxy_read_timeout 60s; + + # Proxy buffering + proxy_buffering on; + proxy_buffer_size 4k; + proxy_buffers 8 16k; # CORS headers add_header 'Access-Control-Allow-Origin' '*' always; @@ -74,59 +105,23 @@ http { try_files $uri $uri/ /index.html; } - location ~* \.(js|css|jpg|png|svg|woff|woff2|ttf|otf|eot|ico)$ { + + # Static asset handling with improved caching + location ~* \.(js|css|jpg|jpeg|png|gif|ico|svg|woff|woff2|ttf|otf|eot)$ { root /usr/share/nginx/html; - add_header 'Access-Control-Allow-Origin' '*' always; expires 30d; - add_header Cache-Control "public"; + add_header Cache-Control "public, no-transform"; + add_header 'Access-Control-Allow-Origin' '*' always; + + # Enable compression for these files + gzip_static on; # Serve pre-compressed files if available + + # Disable access logs for static files + access_log off; } - - # #error_page 404 /404.html; - - # # redirect server error pages to the static page /50x.html - # # - # error_page 500 502 503 504 /50x.html; - # location = /50x.html { - # root html; - # } - - # proxy the PHP scripts to Apache listening on 127.0.0.1:80 - # - #location ~ \.php$ { - # proxy_pass http://127.0.0.1; - #} - - # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000 - # - #location ~ \.php$ { - # root html; - # fastcgi_pass 127.0.0.1:9000; - # fastcgi_index index.php; - # fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name; - # include fastcgi_params; - #} - - # deny access to .htaccess files, if Apache's document root - # concurs with nginx's one - # - #location ~ /\.ht { - # deny all; - #} } - # another virtual host using mix of IP-, name-, and port-based configuration - # - #server { - # listen 8000; - # listen somename:8080; - # server_name somename alias another.alias; - - # location / { - # root html; - # index index.html index.htm; - # } - #} # HTTPS server @@ -149,5 +144,4 @@ http { # index index.html index.htm; # } #} - } \ No newline at end of file diff --git a/react-ui/package.json b/react-ui/package.json index 4c9271009563219b7aed130bf1d46e0695f02697..5767007a12e35553bf091fd00d8ec09411d7643a 100755 --- a/react-ui/package.json +++ b/react-ui/package.json @@ -13,6 +13,7 @@ "@fortawesome/free-solid-svg-icons": "^6.6.0", "@fortawesome/react-fontawesome": "^0.2.2", "@reduxjs/toolkit": "^2.2.4", + "@vitejs/plugin-react": "^4.2.1", "bootstrap": "^5.3.3", "dompurify": "^3.2.3", "i18next": "^24.0.5", @@ -24,11 +25,40 @@ "react-i18next": "^15.0.0", "react-redux": "^9.1.2", "react-router-dom": "^6.23.1", - "react-scripts": "5.0.1", "react-toastify": "^10.0.5", "redux": "^5.0.1", "redux-observable": "^3.0.0-rc.2", "redux-persist": "^6.0.0", + "sass": "1.82.0", + "sass-embedded": "^1.80.6", + "@fullhuman/postcss-purgecss": "^7.0.2", + "vite": "^6.0.3" + }, + "devDependencies": { + "@babel/runtime": "^7.21.5", + "@rtk-query/codegen-openapi": "^2.0.0", + "@testing-library/jest-dom": "^6.4.8", + "@testing-library/react": "^16.0.0", + "@testing-library/user-event": "^14.5.2", + "@types/react": "^18.2.66", + "@types/react-dom": "^18.2.22", + "@typescript-eslint/eslint-plugin": "^8.0.1", + "@typescript-eslint/parser": "^8.0.1", + "eslint": "^9.9.0", + "eslint-config-airbnb-typescript": "^18.0.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-import": "^2.27.5", + "eslint-plugin-jsx-a11y": "^6.7.1", + "eslint-plugin-prettier": "^5.2.1", + "eslint-plugin-react": "^7.32.2", + "eslint-plugin-react-hooks": "^5.1.0-rc.0", + "eslint-plugin-react-refresh": "^0.4.9", + "globals": "^15.9.0", + "prettier": "^3.3.3", + "react-scripts": "5.0.1", + "typescript": "^5.5.3", + "typescript-eslint": "^8.0.1", + "vite-bundle-visualizer": "^1.2.1", "web-vitals": "^4.2.2" }, "scripts": { @@ -38,7 +68,8 @@ "build": "yarn build::api && yarn build::frontend", "lint": "eslint src", "lint::fix": "eslint src --fix", - "dev": "./scripts/dev.sh" + "dev": "./scripts/dev.sh", + "postbuild": "purgecss --css dist/assets/*.css --content dist/index.html dist/assets/*.js --output dist/purged" }, "eslintConfig": { "extends": [ @@ -56,34 +87,5 @@ "last 1 firefox version", "last 1 safari version" ] - }, - "devDependencies": { - "@babel/runtime": "^7.21.5", - "@rtk-query/codegen-openapi": "^2.0.0", - "@testing-library/jest-dom": "^6.4.8", - "@testing-library/react": "^16.0.0", - "@testing-library/user-event": "^14.5.2", - "@types/react": "^18.2.66", - "@types/react-dom": "^18.2.22", - "@typescript-eslint/eslint-plugin": "^8.0.1", - "@typescript-eslint/parser": "^8.0.1", - "@vitejs/plugin-react": "^4.2.1", - "eslint": "^9.9.0", - "eslint-config-airbnb-typescript": "^18.0.0", - "eslint-config-prettier": "^9.1.0", - "eslint-plugin-import": "^2.27.5", - "eslint-plugin-jsx-a11y": "^6.7.1", - "eslint-plugin-prettier": "^5.2.1", - "eslint-plugin-react": "^7.32.2", - "eslint-plugin-react-hooks": "^5.1.0-rc.0", - "eslint-plugin-react-refresh": "^0.4.9", - "globals": "^15.9.0", - "prettier": "^3.3.3", - "sass": "1.82.0", - "sass-embedded": "^1.80.6", - "typescript": "^5.5.3", - "typescript-eslint": "^8.0.1", - "vite": "^6.0.3", - "vite-bundle-visualizer": "^1.2.1" } } \ No newline at end of file diff --git a/react-ui/public/fonts/inter-webfont.woff b/react-ui/public/fonts/inter-webfont.woff new file mode 100644 index 0000000000000000000000000000000000000000..7eca6fe75d3ba56e14981e8f8e1c50d948bb8b56 Binary files /dev/null and b/react-ui/public/fonts/inter-webfont.woff differ diff --git a/react-ui/public/fonts/inter-webfont.woff2 b/react-ui/public/fonts/inter-webfont.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..25ef870d53d5616ee3e92bbe3e85b5ed164fe4d2 Binary files /dev/null and b/react-ui/public/fonts/inter-webfont.woff2 differ diff --git a/react-ui/src/components/devices/view/device.view.tsx b/react-ui/src/components/devices/view/device.view.tsx index 2602463bba8e51a8be7aff14b50008d0c9ad32ce..a2c8458a7a43af274f6417c411751fad3d686433 100755 --- a/react-ui/src/components/devices/view/device.view.tsx +++ b/react-ui/src/components/devices/view/device.view.tsx @@ -6,7 +6,7 @@ import './device.scss'; import { DeviceViewTable } from './device.view.table'; import { DeviceViewTabs, DeviceViewTabValues } from './device.view.tabs'; -function DeviceView() { +const DeviceView = () => { const { t } = useTranslation('common'); const searchRef = useRef<HTMLInputElement>(null); const { activeTab, setActiveTab, handleActiveTabLink } = useDeviceViewModel(); diff --git a/react-ui/src/components/login/layouts/login.layout.tsx b/react-ui/src/components/login/layouts/login.layout.tsx index 556dc6de436397b283bd426041b1d08749cf505a..58e8fc99133d8674b57c7d39f06e40da2c9f03e4 100755 --- a/react-ui/src/components/login/layouts/login.layout.tsx +++ b/react-ui/src/components/login/layouts/login.layout.tsx @@ -20,4 +20,6 @@ export const LoginLayout = () => { return ( <LoginView>{outlet}</LoginView> ) -} \ No newline at end of file +} + +export default LoginLayout \ No newline at end of file diff --git a/react-ui/src/routes.tsx b/react-ui/src/routes.tsx index 368df55a472bdbc20bc2dd34bb9fb1f33b43047f..532d03fbf0d02924b8fb8e58b9aefed1ae2ab964 100755 --- a/react-ui/src/routes.tsx +++ b/react-ui/src/routes.tsx @@ -1,21 +1,43 @@ import { BasicLayout } from "@layout/basic.layout"; import { ProtectedLayout } from "@layout/protected.layout/protected.layout"; +import { lazy, Suspense } from 'react'; import { createBrowserRouter, createRoutesFromElements, Navigate, Route } from "react-router-dom"; -import DeviceView from "./components/devices/view/device.view"; -import { LoginLayout } from "./components/login/layouts/login.layout"; export const DEVICE_URL = '/device/'; export const LOGIN_URL = '/login'; +// Lazy load components +const DeviceView = lazy(() => import('./components/devices/view/device.view')); +const LoginLayout = lazy(() => import('./components/login/layouts/login.layout')); + +// Loading fallback component +const LoadingFallback = () => <div>Loading...</div>; export const router = createBrowserRouter( createRoutesFromElements( <Route element={<BasicLayout />}> - <Route path={LOGIN_URL} element={<LoginLayout />} /> + <Route + path={LOGIN_URL} + element={ + <Suspense fallback={<LoadingFallback />}> + <LoginLayout /> + </Suspense> + } + /> <Route element={<ProtectedLayout />}> - <Route path={DEVICE_URL} element={<DeviceView />} /> - <Route path="/" element={<Navigate to={DEVICE_URL} replace={true} />} /> + <Route + path={DEVICE_URL} + element={ + <Suspense fallback={<LoadingFallback />}> + <DeviceView /> + </Suspense> + } + /> + <Route + path="/" + element={<Navigate to={DEVICE_URL} replace={true} />} + /> </Route> </Route> ) -) \ No newline at end of file +); \ No newline at end of file diff --git a/react-ui/src/shared/icons/icons.ts b/react-ui/src/shared/icons/icons.ts index 00021aa116faae88600fd06ab461aabd41aee9bb..9c8791cc90cdd443a18fa06648ed2802b9a28b03 100755 --- a/react-ui/src/shared/icons/icons.ts +++ b/react-ui/src/shared/icons/icons.ts @@ -1,4 +1,4 @@ import { library } from '@fortawesome/fontawesome-svg-core' -import { faSpinner, fas } from '@fortawesome/free-solid-svg-icons' +import { faSpinner } from '@fortawesome/free-solid-svg-icons' -library.add(fas, faSpinner) \ No newline at end of file +library.add(faSpinner) \ No newline at end of file diff --git a/react-ui/src/shared/style/fonts.scss b/react-ui/src/shared/style/fonts.scss index c47d1a52fb36da1863fc511e7e1e4deb045694cf..3af44e1559b21507bd509acb4c954a204250f3ba 100755 --- a/react-ui/src/shared/style/fonts.scss +++ b/react-ui/src/shared/style/fonts.scss @@ -1,9 +1,12 @@ @font-face { font-family: Inter; - src: url("/fonts/Inter.ttf"); + src: + url("/fonts/inter-webfont.woff2") format("woff2"), + url("/fonts/inter-webfont.woff") format("woff"); + font-weight: normal; + font-style: normal; } - * { font-family: Inter; -} \ No newline at end of file +} diff --git a/react-ui/vite.config.mjs b/react-ui/vite.config.mjs index fc4ce4a01c9588f72d40aee0e7aeb16d002e068e..83378d6ee939ae6dee49a7b7037621f24f4a1ea6 100755 --- a/react-ui/vite.config.mjs +++ b/react-ui/vite.config.mjs @@ -1,13 +1,35 @@ -import react from '@vitejs/plugin-react' -import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react'; +import { defineConfig } from 'vite'; + export default defineConfig({ plugins: [react()], build: { - sourcemap: true, + sourcemap: false, + + rollupOptions: { + output: { + manualChunks: { + 'required': [ + 'bootstrap', 'react-bootstrap', + 'react', 'react-dom', 'react-router-dom', + 'redux', '@reduxjs/toolkit', 'react-redux', 'redux-observable', 'redux-persist', + 'i18next', 'react-i18next', + '@fortawesome/fontawesome-svg-core', + '@fortawesome/free-regular-svg-icons', + '@fortawesome/free-solid-svg-icons', + '@fortawesome/react-fontawesome' + ], + 'lazy': [ + 'react-toastify' + ] + } + } + } }, // develop server server: { + sourcemap: true, host: true, port: 3000, proxy: { diff --git a/react-ui/yarn.lock b/react-ui/yarn.lock index d054db6d6ed77067217fd0ca327f855376706356..62e8d8fa11691d208d00262c2a3faae13d4170ec 100755 --- a/react-ui/yarn.lock +++ b/react-ui/yarn.lock @@ -1486,6 +1486,13 @@ dependencies: prop-types "^15.8.1" +"@fullhuman/postcss-purgecss@^7.0.2": + version "7.0.2" + resolved "https://registry.yarnpkg.com/@fullhuman/postcss-purgecss/-/postcss-purgecss-7.0.2.tgz#ccacdbc312248c76c42cfac359f4ca5121001e67" + integrity sha512-U4zAXNaVztbDxO9EdcLp51F3UxxYsb/7DN89rFxFJhfk2Wua2pvw2Kf3HdspbPhW/wpHjSjsxWYoIlbTgRSjbQ== + dependencies: + purgecss "^7.0.2" + "@humanfs/core@^0.19.1": version "0.19.1" resolved "https://registry.yarnpkg.com/@humanfs/core/-/core-0.19.1.tgz#17c55ca7d426733fe3c561906b8173c336b40a77" @@ -3906,6 +3913,11 @@ combined-stream@^1.0.8: dependencies: delayed-stream "~1.0.0" +commander@^12.1.0: + version "12.1.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-12.1.0.tgz#01423b36f501259fdaac4d0e4d60c96c991585d3" + integrity sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA== + commander@^2.20.0: version "2.20.3" resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" @@ -5690,6 +5702,18 @@ glob@^10.3.10: package-json-from-dist "^1.0.0" path-scurry "^1.11.1" +glob@^11.0.0: + version "11.0.0" + resolved "https://registry.yarnpkg.com/glob/-/glob-11.0.0.tgz#6031df0d7b65eaa1ccb9b29b5ced16cea658e77e" + integrity sha512-9UiX/Bl6J2yaBbxKoEBRm4Cipxgok8kQYcOPEhScPwebu2I0HoQOuYdIO6S3hLuWoZgpDpwQZMzTFxgpkyT76g== + dependencies: + foreground-child "^3.1.0" + jackspeak "^4.0.1" + minimatch "^10.0.0" + minipass "^7.1.2" + package-json-from-dist "^1.0.0" + path-scurry "^2.0.0" + glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6: version "7.2.3" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" @@ -6469,6 +6493,13 @@ jackspeak@^3.1.2: optionalDependencies: "@pkgjs/parseargs" "^0.11.0" +jackspeak@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-4.0.2.tgz#11f9468a3730c6ff6f56823a820d7e3be9bef015" + integrity sha512-bZsjR/iRjl1Nk1UkjGpAzLNfQtzuijhn2g+pbZb98HQ1Gk8vM9hfbxeMBP+M2/UUdwj0RqGG3mlvk2MsAqwvEw== + dependencies: + "@isaacs/cliui" "^8.0.2" + jake@^10.8.5: version "10.9.2" resolved "https://registry.yarnpkg.com/jake/-/jake-10.9.2.tgz#6ae487e6a69afec3a5e167628996b59f35ae2b7f" @@ -7277,6 +7308,11 @@ lru-cache@^10.2.0: resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.4.3.tgz#410fc8a17b70e598013df257c2446b7f3383f119" integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ== +lru-cache@^11.0.0: + version "11.0.2" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-11.0.2.tgz#fbd8e7cf8211f5e7e5d91905c415a3f55755ca39" + integrity sha512-123qHRfJBmo2jXDbo/a5YOQrJoHF/GNQTLzQ5+IdK5pWpceK17yRc6ozlWd25FxvGKQbIUs91fDFkXmDHTKcyA== + lru-cache@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" @@ -7407,6 +7443,13 @@ minimalistic-assert@^1.0.0: resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== +minimatch@^10.0.0: + version "10.0.1" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-10.0.1.tgz#ce0521856b453c86e25f2c4c0d03e6ff7ddc440b" + integrity sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ== + dependencies: + brace-expansion "^2.0.1" + minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" @@ -7923,6 +7966,14 @@ path-scurry@^1.11.1: lru-cache "^10.2.0" minipass "^5.0.0 || ^6.0.2 || ^7.0.0" +path-scurry@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-2.0.0.tgz#9f052289f23ad8bf9397a2a0425e7b8615c58580" + integrity sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg== + dependencies: + lru-cache "^11.0.0" + minipass "^7.1.2" + path-to-regexp@0.1.12: version "0.1.12" resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.12.tgz#d5e1a12e478a976d432ef3c58d534b9923164bb7" @@ -8653,6 +8704,16 @@ punycode@^2.1.0, punycode@^2.1.1, punycode@^2.3.1: resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== +purgecss@^7.0.2: + version "7.0.2" + resolved "https://registry.yarnpkg.com/purgecss/-/purgecss-7.0.2.tgz#b7dccc3ead65a4301eed98e014793719a511c633" + integrity sha512-4Ku8KoxNhOWi9X1XJ73XY5fv+I+hhTRedKpGs/2gaBKU8ijUiIKF/uyyIyh7Wo713bELSICF5/NswjcuOqYouQ== + dependencies: + commander "^12.1.0" + glob "^11.0.0" + postcss "^8.4.47" + postcss-selector-parser "^6.1.2" + q@^1.1.2: version "1.5.1" resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7"