improvements
@@ -18,9 +18,9 @@ export default tseslint.config([
|
|||||||
'@typescript-eslint/no-unused-vars': [
|
'@typescript-eslint/no-unused-vars': [
|
||||||
'error',
|
'error',
|
||||||
{
|
{
|
||||||
'argsIgnorePattern': '^ignored',
|
argsIgnorePattern: '^ignored',
|
||||||
'varsIgnorePattern': '^ignored',
|
varsIgnorePattern: '^ignored',
|
||||||
'caughtErrorsIgnorePattern': '^ignored'
|
caughtErrorsIgnorePattern: '^ignored'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
120
index.html
@@ -1,17 +1,123 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<!-- Performance Monitoring -->
|
||||||
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
|
||||||
<meta name="theme-color" content="#000000" />
|
|
||||||
<meta name="description" content="Phenix Customer Portal" />
|
|
||||||
<script>
|
<script>
|
||||||
__phenixPageLoadTime = new Date().getTime();
|
window.__phenixPageLoadTime = new Date().getTime();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Basic Meta Tags -->
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<title>Customer Portal - Phenix</title>
|
||||||
|
<meta name="description" content="Phenix Real Time Solutions Customer Portal" />
|
||||||
|
<meta name="author" content="Phenix Real Time Solutions" />
|
||||||
|
|
||||||
|
<!-- Mobile & Display -->
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<meta name="theme-color" content="#000000" />
|
||||||
|
|
||||||
|
<!-- Favicon -->
|
||||||
|
<link rel="shortcut icon" href="./phenix.ico" />
|
||||||
|
<link rel="icon" type="image/x-icon" href="./phenix.ico" />
|
||||||
|
|
||||||
|
<!-- Base Path Configuration for React Router -->
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// Define main application routes
|
||||||
|
const APP_ROUTES = ['channels', 'login', 'rooms', 'analytics', 'qos', 'dashboard'];
|
||||||
|
|
||||||
|
// Get current URL path and split into segments
|
||||||
|
const currentPath = window.location.pathname;
|
||||||
|
const pathSegments = currentPath.split('/').filter(segment => segment.length > 0);
|
||||||
|
|
||||||
|
// Find the first segment that matches an app route
|
||||||
|
const routeIndex = pathSegments.findIndex(segment => APP_ROUTES.includes(segment));
|
||||||
|
|
||||||
|
// Calculate base path for the application
|
||||||
|
let basePath = '/';
|
||||||
|
|
||||||
|
if (routeIndex >= 0) {
|
||||||
|
// Extract segments up to (but not including) the route
|
||||||
|
// Special handling for login route
|
||||||
|
const includeRouteSegment = pathSegments[routeIndex] === 'login';
|
||||||
|
const baseSegments = pathSegments.slice(0, routeIndex + (includeRouteSegment ? 1 : 0));
|
||||||
|
|
||||||
|
if (baseSegments.length > 0) {
|
||||||
|
basePath = '/' + baseSegments.join('/') + '/';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the base href for the application
|
||||||
|
document.write(`<base href="${basePath}" />`);
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Browser Compatibility Check -->
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// Redirect unsupported browsers
|
||||||
|
const isUnsupportedBrowser = /MSIE|Trident/.test(window.navigator.userAgent);
|
||||||
|
if (isUnsupportedBrowser) {
|
||||||
|
const searchParams = window.location.search || '';
|
||||||
|
window.location.href = '/unsupported-browser.html' + searchParams;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Google Analytics -->
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// Skip analytics for local environments
|
||||||
|
const hostname = window.location.hostname;
|
||||||
|
const isLocal = hostname.startsWith('local') || hostname.endsWith('-local') || hostname.includes('localhost');
|
||||||
|
|
||||||
|
if (isLocal) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine environment and set GA ID
|
||||||
|
const isStaging = hostname.startsWith('stg-') || hostname.endsWith('-stg') || hostname.includes('-stg-') || hostname === 'stg';
|
||||||
|
|
||||||
|
const GA_MEASUREMENT_ID = isStaging ? 'G-TXBY6KV20H' : 'G-0RYP945E1S';
|
||||||
|
|
||||||
|
// Initialize dataLayer
|
||||||
|
window.dataLayer = window.dataLayer || [];
|
||||||
|
|
||||||
|
// Load Google Analytics script
|
||||||
|
const script = document.createElement('script');
|
||||||
|
script.src = `https://www.googletagmanager.com/gtag/js?id=${GA_MEASUREMENT_ID}`;
|
||||||
|
script.async = true;
|
||||||
|
document.head.appendChild(script);
|
||||||
|
|
||||||
|
// Initialize gtag
|
||||||
|
function gtag() {
|
||||||
|
window.dataLayer.push(arguments);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.gtag = gtag;
|
||||||
|
gtag('js', new Date());
|
||||||
|
gtag('config', GA_MEASUREMENT_ID);
|
||||||
|
})();
|
||||||
</script>
|
</script>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>Phenix Customer Portal</title>
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
<!-- Fallback for users with JavaScript disabled -->
|
||||||
|
<!-- <noscript>
|
||||||
|
<div style="text-align: center; padding: 2rem; font-family: Arial, sans-serif;">
|
||||||
|
<h1>JavaScript Required</h1>
|
||||||
|
<p>You need to enable JavaScript to use the Phenix Customer Portal.</p>
|
||||||
|
<p>Please enable JavaScript in your browser settings and refresh this page.</p>
|
||||||
|
</div>
|
||||||
|
</noscript> -->
|
||||||
|
|
||||||
|
<!-- React App Root -->
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
<script type="module" src="/src/main.tsx"></script>
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -38,6 +38,7 @@
|
|||||||
"eslint-plugin-react-refresh": "0.4.20",
|
"eslint-plugin-react-refresh": "0.4.20",
|
||||||
"globals": "16.3.0",
|
"globals": "16.3.0",
|
||||||
"prettier": "3.6.2",
|
"prettier": "3.6.2",
|
||||||
|
"react-router-dom": "7.8.2",
|
||||||
"typescript": "5.9.2",
|
"typescript": "5.9.2",
|
||||||
"typescript-eslint": "8.41.0",
|
"typescript-eslint": "8.41.0",
|
||||||
"vite": "7.1.3",
|
"vite": "7.1.3",
|
||||||
|
|||||||
39
src/App.tsx
@@ -1,47 +1,44 @@
|
|||||||
import {useAppSelector, useAppDispatch} from 'hooks/store';
|
import {useAppSelector, useAppDispatch} from 'hooks/store';
|
||||||
import {WebSocketStatusViewComponent, LoginForm} from './components';
|
|
||||||
import AuthenticationService from './services/authentication.service';
|
import AuthenticationService from './services/authentication.service';
|
||||||
import {useEffect, useState} from 'react';
|
import {useEffect, useState} from 'react';
|
||||||
import {authenticationActions, selectError} from 'store/slices/Authentication.slice';
|
import {authenticationActions, selectError, selectIsAuthenticated, selectCredentials} from 'store/slices/Authentication.slice';
|
||||||
import hostService from 'services/url.service';
|
import hostService from 'services/url.service';
|
||||||
import LoggerFactory from './services/logger/LoggerFactory';
|
import LoggerFactory from './services/logger/LoggerFactory';
|
||||||
|
import Router from 'routers/router';
|
||||||
|
import PCastApiService from './services/pcast-api.service';
|
||||||
|
import { ApplicationCredentials } from '@techniker-me/pcast-api';
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const [isInitialized, setIsInitialized] = useState(false);
|
const [isInitialized, setIsInitialized] = useState(false);
|
||||||
|
const error = useAppSelector(selectError);
|
||||||
|
const credentials = useAppSelector(selectCredentials);
|
||||||
|
const isAuthenticated = useAppSelector(selectIsAuthenticated);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Initialize logger after all modules are loaded to avoid circular dependencies
|
|
||||||
LoggerFactory.applyLoggerConfig();
|
LoggerFactory.applyLoggerConfig();
|
||||||
|
|
||||||
// Set WebSocket URI only once during initialization
|
|
||||||
if (!isInitialized) {
|
if (!isInitialized) {
|
||||||
AuthenticationService.setWebSocketUri(hostService.getWebSocketUrl());
|
AuthenticationService.setWebSocketUri(hostService.getWebSocketUrl());
|
||||||
setIsInitialized(true);
|
setIsInitialized(true);
|
||||||
|
AuthenticationService.status?.subscribe(status => dispatch(authenticationActions.setStatus(status)));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Subscribe to WebSocket status changes
|
|
||||||
AuthenticationService.status?.subscribe(status =>
|
|
||||||
dispatch(authenticationActions.setStatus(status))
|
|
||||||
);
|
|
||||||
}, [dispatch, isInitialized]);
|
}, [dispatch, isInitialized]);
|
||||||
|
|
||||||
const error = useAppSelector(selectError);
|
useEffect(() => {
|
||||||
|
if (credentials.applicationId && credentials.secret && isAuthenticated) {
|
||||||
|
const appCredentials: ApplicationCredentials = {
|
||||||
|
id: credentials.applicationId,
|
||||||
|
secret: credentials.secret
|
||||||
|
};
|
||||||
|
PCastApiService.initialize(hostService.getPcastBaseUrlOrigin(), appCredentials);
|
||||||
|
}
|
||||||
|
}, [credentials, isAuthenticated]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{error && <div>{error}</div>}
|
{error && <div>{error}</div>}
|
||||||
<div
|
<Router />
|
||||||
style={{
|
|
||||||
width: '200px',
|
|
||||||
height: '100px',
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
margin: 'auto'
|
|
||||||
}}>
|
|
||||||
<WebSocketStatusViewComponent />
|
|
||||||
<LoginForm />
|
|
||||||
</div>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
src/assets/images/background-1415x959.png
Normal file
|
After Width: | Height: | Size: 321 KiB |
BIN
src/assets/images/calendar-24x24.png
Normal file
|
After Width: | Height: | Size: 652 B |
1
src/assets/images/caret-down.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" ?><!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'><svg enable-background="new 0 0 29 14" height="6px" id="Layer_1" version="1.1" viewBox="0 0 29 14" width="29px" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><polygon fill="#F9F9F9" points="0.15,0 14.5,14.35 28.85,0 "/></svg>
|
||||||
|
After Width: | Height: | Size: 400 B |
1
src/assets/images/caret-up.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" ?><!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'><svg enable-background="new 0 0 29 14" height="8px" id="Layer_1" version="1.1" viewBox="0 0 29 14" width="29px" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><polygon fill="#f9f9f9" points="0.15,14 14.5,-0.35 28.85,14 "/></svg>
|
||||||
|
After Width: | Height: | Size: 402 B |
BIN
src/assets/images/chart-down-50x33.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
src/assets/images/chart-up-50x33.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
1
src/assets/images/icon/error.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!-- Generator: Gravit.io --><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="isolation:isolate" viewBox="0 0 206.751 186.399" width="206.751pt" height="186.399pt"><defs><clipPath id="_clipPath_Wy0NWkkA4mKqtolphxnGK0Cf0TrWZqlo"><rect width="206.751" height="186.399"/></clipPath></defs><g clip-path="url(#_clipPath_Wy0NWkkA4mKqtolphxnGK0Cf0TrWZqlo)"><g><path d=" M 179.102 178.898 L 103.306 178.559 L 27.51 178.22 C 9.581 178.14 2.36 165.502 11.394 150.015 L 49.585 84.544 L 87.777 19.072 C 96.811 3.586 111.366 3.651 120.261 19.218 L 157.865 85.028 L 195.469 150.839 C 204.364 166.406 197.03 178.979 179.102 178.898 Z " fill="none" vector-effect="non-scaling-stroke" stroke-width="5" stroke="rgb(247,13,26)" stroke-linejoin="miter" stroke-linecap="square" stroke-miterlimit="3"/><line x1="103.306" y1="47.028" x2="103.306" y2="120.028" vector-effect="non-scaling-stroke" stroke-width="5" stroke="rgb(247,13,26)" stroke-linejoin="miter" stroke-linecap="square" stroke-miterlimit="3"/><line x1="103.306" y1="138.028" x2="103.306" y2="143.028" vector-effect="non-scaling-stroke" stroke-width="5" stroke="rgb(247,13,26)" stroke-linejoin="miter" stroke-linecap="square" stroke-miterlimit="3"/></g></g></svg>
|
||||||
|
After Width: | Height: | Size: 1.3 KiB |
10
src/assets/images/icon/hash-plus.svg
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg width="19px" height="18px" viewBox="0 0 19 18" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
|
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||||
|
<g id="01-home-copy-7" transform="translate(-1214.000000, -84.000000)" fill="#FFFFFF" fill-rule="nonzero">
|
||||||
|
<g id="icon/white/add-channel" transform="translate(1211.000000, 80.000000)">
|
||||||
|
<path d="M18,14 C18.5522847,14 19,14.4477153 19,15 L18.999068,17 L21,17 C21.5522847,17 22,17.4477153 22,18 C22,18.5522847 21.5522847,19 21,19 L18.999068,19 L19,21 C19,21.5522847 18.5522847,22 18,22 C17.4477153,22 17,21.5522847 17,21 L16.999068,18.999 L15,19 C14.4477153,19 14,18.5522847 14,18 C14,17.4477153 14.4477153,17 15,17 L16.999068,16.999 L17,15 C17,14.4477153 17.4477153,14 18,14 Z M14,4 C14.4287818,4 14.8254386,4.13754318 15.1482567,4.37091574 C15.4794061,4.13462978 15.8829334,4 16.3080311,4 L18,4 C19.0836085,4 19.9620467,4.87843824 19.9620467,5.96204672 C19.9620467,6.23272939 19.9060382,6.5004841 19.7975435,6.748472 L19.687068,6.999 L19.763932,7 C20.8685015,7 21.763932,7.8954305 21.763932,9 C21.763932,9.31049019 21.6916418,9.61671632 21.5527864,9.89442719 L20.5527864,11.8944272 C20.3949049,12.2101902 20.1601982,12.4717535 19.8778882,12.6610569 C19.3634108,12.2470293 18.7105538,12 18,12 C17.1114527,12 16.3131288,12.3862919 15.7638055,13.0000983 L15.083068,13 L15.916068,11 L18.381966,11 C18.7607381,11 19.1070012,10.7859976 19.2763932,10.4472136 L19.5,10 C19.6706654,9.65866925 19.5323138,9.24361439 19.190983,9.07294902 C19.0950363,9.02497564 18.9892377,9 18.881966,9 L16.750068,9 L17.6153846,6.92307692 C17.7569961,6.58320938 17.5962778,6.19289353 17.2564103,6.05128205 C17.1751635,6.01742923 17.0880173,6 17,6 L16.6666667,6 C16.2629658,6 15.8988593,6.24273768 15.7435897,6.61538462 L14.749068,9 L12.750068,9 L13.6153846,6.92307692 C13.7569961,6.58320938 13.5962778,6.19289353 13.2564103,6.05128205 C13.1751635,6.01742923 13.0880173,6 13,6 L12.6666667,6 C12.2629658,6 11.8988593,6.24273768 11.7435897,6.61538462 L10.749068,9 L8.61803399,9 C8.23926193,9 7.89299881,9.21400238 7.7236068,9.5527864 L7.5,10 C7.32933463,10.3413307 7.46768625,10.7563856 7.80901699,10.927051 C7.90496374,10.9750244 8.01076227,11 8.11803399,11 L9.91606798,11 L9.08206798,13 L6.61803399,13 C6.23926193,13 5.89299881,13.2140024 5.7236068,13.5527864 L5.5,14 C5.32933463,14.3413307 5.46768625,14.7563856 5.80901699,14.927051 C5.90496374,14.9750244 6.01076227,15 6.11803399,15 L8.24906798,15 L7.38461538,17.0769231 C7.24300391,17.4167906 7.40372221,17.8071065 7.74358974,17.9487179 C7.82483652,17.9825708 7.91198265,18 8,18 L8.33333333,18 C8.73703418,18 9.1011407,17.7572623 9.25641026,17.3846154 L10.249068,15 L12.249068,15 L11.3846154,17.0769231 C11.2430039,17.4167906 11.4037222,17.8071065 11.7435897,17.9487179 C11.8248365,17.9825708 11.9119827,18 12,18 L12.0354373,18.0000497 C12.0120835,18.1633354 12,18.3302566 12,18.5 C12,19.037207 12.1210295,19.5461461 12.3373087,20.0010375 L11,20 C10.5712182,20 10.1745614,19.8624568 9.85174329,19.6290843 C9.52059393,19.8653702 9.11706659,20 8.69196885,20 L7,20 C5.91639152,20 5.03795328,19.1215618 5.03795328,18.0379533 C5.03795328,17.7672706 5.09396179,17.4995159 5.2024565,17.251528 L5.31206798,17 L5.23606798,17 C4.13149848,17 3.23606798,16.1045695 3.23606798,15 C3.23606798,14.6895098 3.30835816,14.3832837 3.4472136,14.1055728 L4.4472136,12.1055728 C4.62888297,11.7422341 4.91227191,11.4506579 5.25285994,11.2583596 C5.24175599,11.1747423 5.23606798,11.0880354 5.23606798,11 C5.23606798,10.6895098 5.30835816,10.3832837 5.4472136,10.1055728 L6.4472136,8.10557281 C6.78599762,7.42800475 7.47852386,7 8.23606798,7 L9.68706798,6.999 L10.4757165,5.19836233 C10.7942054,4.47038768 11.5134355,4 12.3080311,4 L14,4 Z M15,15 L14.9990501,15.0355804 C14.6965332,15.0789368 14.406503,15.160979 14.1341327,15.276534 L14.249068,15 L15,15 Z M13.916068,11 L13.082068,13 L11.083068,13 L11.916068,11 L13.916068,11 Z" id="Combined-Shape"></path>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 4.0 KiB |
53
src/assets/images/icon/menu.svg
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
|
||||||
|
<g>
|
||||||
|
<g>
|
||||||
|
<path style="fill:#F9F9F9" d="M501.333,96H10.667C4.779,96,0,100.779,0,106.667s4.779,10.667,10.667,10.667h490.667c5.888,0,10.667-4.779,10.667-10.667
|
||||||
|
S507.221,96,501.333,96z"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<g>
|
||||||
|
<path style="fill:#F9F9F9" d="M501.333,245.333H10.667C4.779,245.333,0,250.112,0,256s4.779,10.667,10.667,10.667h490.667
|
||||||
|
c5.888,0,10.667-4.779,10.667-10.667S507.221,245.333,501.333,245.333z"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<g>
|
||||||
|
<path style="fill:#F9F9F9" d="M501.333,394.667H10.667C4.779,394.667,0,399.445,0,405.333C0,411.221,4.779,416,10.667,416h490.667
|
||||||
|
c5.888,0,10.667-4.779,10.667-10.667C512,399.445,507.221,394.667,501.333,394.667z"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
1
src/assets/images/icon/ok.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg fill="#00bc8c" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50" width="250px" height="250px"><path fill="none" stroke="#00bc8c" stroke-miterlimit="10" stroke-width="2" d="M25,3C12.85,3,3,12.85,3,25s9.85,22,22,22 s22-9.85,22-22S37.15,3,25,3z"/><path fill="none" stroke="#00bc8c" stroke-linecap="round" stroke-miterlimit="10" stroke-width="2" d="M16,24.444L24.143,32L35,16"/></svg>
|
||||||
|
After Width: | Height: | Size: 390 B |
12
src/assets/images/icon/refresh.svg
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<svg width="19px" height="18px" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
viewBox="0 0 341.333 341.333" style="enable-background:new 0 0 341.333 341.333;" xml:space="preserve">
|
||||||
|
<g>
|
||||||
|
<g>
|
||||||
|
<path fill="#ffffff" d="M341.227,149.333V0l-50.133,50.133C260.267,19.2,217.707,0,170.56,0C76.267,0,0.107,76.373,0.107,170.667
|
||||||
|
s76.16,170.667,170.453,170.667c79.467,0,146.027-54.4,164.907-128h-44.373c-17.6,49.707-64.747,85.333-120.533,85.333
|
||||||
|
c-70.72,0-128-57.28-128-128s57.28-128,128-128c35.307,0,66.987,14.72,90.133,37.867l-68.8,68.8H341.227z"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 796 B |
BIN
src/assets/images/logo-no-text.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
src/assets/images/phenix-logo-101x41.png
Executable file
|
After Width: | Height: | Size: 4.7 KiB |
BIN
src/assets/images/phenix-offline-screen-1920x1080.gif
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
BIN
src/assets/images/search-150x150.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
src/assets/images/symbol-lock-24x24.png
Executable file
|
After Width: | Height: | Size: 374 B |
BIN
src/assets/images/symbol-person-24x24.png
Executable file
|
After Width: | Height: | Size: 563 B |
68
src/components/AuthorizedOnly/AuthorizedOnly.tsx
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import {JSX, useEffect, useRef} from 'react';
|
||||||
|
import Routes from '../../routers/static-routes';
|
||||||
|
import {useAppSelector} from '../../hooks/store';
|
||||||
|
import {selectIsAuthenticated, selectIsAuthenticating} from '../../store/slices/Authentication.slice';
|
||||||
|
|
||||||
|
interface AuthorizedOnlyProps {
|
||||||
|
children: JSX.Element;
|
||||||
|
fallback?: JSX.Element;
|
||||||
|
redirectTo?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AuthorizedOnly({children, fallback, redirectTo = Routes.loginUrl()}: AuthorizedOnlyProps): JSX.Element {
|
||||||
|
const isAuthenticated = useAppSelector(selectIsAuthenticated);
|
||||||
|
const isAuthenticating = useAppSelector(selectIsAuthenticating);
|
||||||
|
const hasRedirected = useRef(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// If not authenticating and not authenticated, redirect to login
|
||||||
|
if (!isAuthenticating && !isAuthenticated && !hasRedirected.current) {
|
||||||
|
hasRedirected.current = true;
|
||||||
|
|
||||||
|
// Build the redirect URL with current query parameters
|
||||||
|
const currentSearch = window.location.search;
|
||||||
|
const currentUrl = window.location.href;
|
||||||
|
|
||||||
|
console.log('AuthorizedOnly: Redirecting to login');
|
||||||
|
console.log('Current URL:', currentUrl);
|
||||||
|
console.log('Current search:', currentSearch);
|
||||||
|
console.log('Redirect to:', redirectTo);
|
||||||
|
|
||||||
|
// Ensure we have the full path for redirect
|
||||||
|
const loginPath = redirectTo.startsWith('/') ? redirectTo : `/${redirectTo}`;
|
||||||
|
const redirectUrl = currentSearch ? `${loginPath}${currentSearch}` : loginPath;
|
||||||
|
|
||||||
|
window.location.href = redirectUrl;
|
||||||
|
}
|
||||||
|
}, [isAuthenticated, isAuthenticating, redirectTo]);
|
||||||
|
|
||||||
|
// Reset redirect flag when authentication state changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (isAuthenticated) {
|
||||||
|
hasRedirected.current = false;
|
||||||
|
}
|
||||||
|
}, [isAuthenticated]);
|
||||||
|
|
||||||
|
// Show fallback while checking authentication or redirecting
|
||||||
|
if (!isAuthenticated) {
|
||||||
|
return (
|
||||||
|
fallback || (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
height: '100vh',
|
||||||
|
color: '#666'
|
||||||
|
}}>
|
||||||
|
<h2>Checking Authentication...</h2>
|
||||||
|
<p>Please wait while we verify your credentials.</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// User is authenticated, render the protected content
|
||||||
|
return children;
|
||||||
|
}
|
||||||
1
src/components/AuthorizedOnly/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export {AuthorizedOnly} from './AuthorizedOnly';
|
||||||
@@ -1,20 +1,31 @@
|
|||||||
import {JSX, useState} from 'react';
|
import {JSX, useState, useEffect} from 'react';
|
||||||
import {useAppSelector, useAppDispatch} from '../../hooks';
|
import {useAppSelector, useAppDispatch} from '../../hooks';
|
||||||
import {authenticateCredentialsThunk, selectIsAuthenticating, selectStatus} from '../../store/slices/Authentication.slice';
|
import {useNavigate} from 'react-router-dom';
|
||||||
|
import Routes from '../../routers/static-routes';
|
||||||
|
import {authenticateCredentialsThunk, selectIsAuthenticated, selectIsAuthenticating, selectStatus} from '../../store/slices/Authentication.slice';
|
||||||
import {InputField} from '../InputField';
|
import {InputField} from '../InputField';
|
||||||
|
|
||||||
export function LoginForm(): JSX.Element {
|
export function LoginForm(): JSX.Element | null {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
const navigate = useNavigate();
|
||||||
const [applicationId, setApplicationId] = useState<string>('phenixrts.com-alex.zinn');
|
const [applicationId, setApplicationId] = useState<string>('phenixrts.com-alex.zinn');
|
||||||
const [secret, setSecret] = useState<string>('AMAsDzr.dIuGMZ.Zu52Dt~MQvP!DZwYg');
|
const [secret, setSecret] = useState<string>('AMAsDzr.dIuGMZ.Zu52Dt~MQvP!DZwYg');
|
||||||
|
|
||||||
const isAuthenticating = useAppSelector(selectIsAuthenticating);
|
const isAuthenticating = useAppSelector(selectIsAuthenticating);
|
||||||
const connectionStatus = useAppSelector(selectStatus);
|
const connectionStatus = useAppSelector(selectStatus);
|
||||||
|
const isAuthenticated = useAppSelector(selectIsAuthenticated);
|
||||||
|
|
||||||
// Only allow authentication when WebSocket is Online
|
|
||||||
const isConnectionReady = connectionStatus === 'Online';
|
const isConnectionReady = connectionStatus === 'Online';
|
||||||
const isDisabled = isAuthenticating || !isConnectionReady;
|
const isDisabled = isAuthenticating || !isConnectionReady;
|
||||||
|
|
||||||
|
// Redirect to dashboard if already authenticated
|
||||||
|
useEffect(() => {
|
||||||
|
if (isAuthenticated) {
|
||||||
|
const dashboardUrl = Routes.dashboardUrl() + window.location.search;
|
||||||
|
navigate(dashboardUrl, {replace: true});
|
||||||
|
}
|
||||||
|
}, [isAuthenticated, navigate]);
|
||||||
|
|
||||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
@@ -28,6 +39,11 @@ export function LoginForm(): JSX.Element {
|
|||||||
setSecret('');
|
setSecret('');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Don't render login form if already authenticated
|
||||||
|
if (isAuthenticated) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit}>
|
||||||
@@ -39,22 +55,11 @@ export function LoginForm(): JSX.Element {
|
|||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
isRequired={true}
|
isRequired={true}
|
||||||
/>
|
/>
|
||||||
<InputField
|
<InputField type="password" placeholder="secret" value={secret} onValueChange={value => setSecret(value)} disabled={isDisabled} isRequired={true} />
|
||||||
type="password"
|
|
||||||
placeholder="secret"
|
|
||||||
value={secret}
|
|
||||||
onValueChange={value => setSecret(value)}
|
|
||||||
disabled={isDisabled}
|
|
||||||
isRequired={true}
|
|
||||||
/>
|
|
||||||
<button type="submit" disabled={isDisabled}>
|
<button type="submit" disabled={isDisabled}>
|
||||||
{isConnectionReady ? 'Login' : 'Connecting...'}
|
{isConnectionReady ? 'Login' : 'Connecting...'}
|
||||||
</button>
|
</button>
|
||||||
{!isConnectionReady && (
|
{!isConnectionReady && <div style={{color: 'orange', fontSize: '12px', marginTop: '5px'}}>Waiting for WebSocket connection...</div>}
|
||||||
<div style={{color: 'orange', fontSize: '12px', marginTop: '5px'}}>
|
|
||||||
Waiting for WebSocket connection...
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</form>
|
</form>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
45
src/components/Navigation/Navigation.tsx
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import {JSX} from 'react';
|
||||||
|
import {Link, useLocation} from 'react-router-dom';
|
||||||
|
|
||||||
|
export function Navigation(): JSX.Element {
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
const navItems = [
|
||||||
|
{path: '/dashboard', label: 'Dashboard'},
|
||||||
|
{path: '/channels', label: 'Channels'}
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<nav
|
||||||
|
style={{
|
||||||
|
background: '#f8f9fa',
|
||||||
|
padding: '1rem',
|
||||||
|
borderBottom: '1px solid #dee2e6',
|
||||||
|
marginBottom: '2rem'
|
||||||
|
}}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
maxWidth: '1200px',
|
||||||
|
margin: '0 auto',
|
||||||
|
display: 'flex',
|
||||||
|
gap: '1rem'
|
||||||
|
}}>
|
||||||
|
{navItems.map(item => (
|
||||||
|
<Link
|
||||||
|
key={item.path}
|
||||||
|
to={item.path}
|
||||||
|
style={{
|
||||||
|
padding: '0.5rem 1rem',
|
||||||
|
textDecoration: 'none',
|
||||||
|
color: location.pathname === item.path ? '#007bff' : '#6c757d',
|
||||||
|
background: location.pathname === item.path ? '#e7f3ff' : 'transparent',
|
||||||
|
borderRadius: '4px',
|
||||||
|
border: location.pathname === item.path ? '1px solid #007bff' : '1px solid transparent'
|
||||||
|
}}>
|
||||||
|
{item.label}
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
}
|
||||||
1
src/components/Navigation/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export {Navigation} from './Navigation';
|
||||||
16
src/components/ProtectedRoute/ProtectedRoute.tsx
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import {JSX} from 'react';
|
||||||
|
import {AuthorizedOnly} from '../AuthorizedOnly';
|
||||||
|
|
||||||
|
interface ProtectedRouteProps {
|
||||||
|
children: JSX.Element;
|
||||||
|
fallback?: JSX.Element;
|
||||||
|
redirectTo?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ProtectedRoute({children, fallback, redirectTo = '/login'}: ProtectedRouteProps): JSX.Element {
|
||||||
|
return (
|
||||||
|
<AuthorizedOnly fallback={fallback} redirectTo={redirectTo}>
|
||||||
|
{children}
|
||||||
|
</AuthorizedOnly>
|
||||||
|
);
|
||||||
|
}
|
||||||
1
src/components/ProtectedRoute/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export {ProtectedRoute} from './ProtectedRoute';
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import {JSX, useEffect} from 'react';
|
||||||
|
import {useNavigate, useLocation} from 'react-router-dom';
|
||||||
|
|
||||||
|
interface QueryPreservingRedirectProps {
|
||||||
|
to: string;
|
||||||
|
replace?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function QueryPreservingRedirect({to, replace = true}: QueryPreservingRedirectProps): JSX.Element | null {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Preserve query parameters and hash
|
||||||
|
const currentSearch = location.search;
|
||||||
|
const currentHash = location.hash;
|
||||||
|
|
||||||
|
// Build the redirect URL with preserved query parameters and hash
|
||||||
|
let redirectUrl = to;
|
||||||
|
if (currentSearch) {
|
||||||
|
redirectUrl += currentSearch;
|
||||||
|
}
|
||||||
|
if (currentHash) {
|
||||||
|
redirectUrl += currentHash;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('QueryPreservingRedirect: Root route redirect detected');
|
||||||
|
console.log('Original path:', location.pathname);
|
||||||
|
console.log('Query parameters:', currentSearch || '(none)');
|
||||||
|
console.log('Hash:', currentHash || '(none)');
|
||||||
|
console.log('Redirecting to:', redirectUrl);
|
||||||
|
console.log('Full original URL:', window.location.href);
|
||||||
|
|
||||||
|
// Small delay to ensure console logs are visible
|
||||||
|
setTimeout(() => {
|
||||||
|
navigate(redirectUrl, {replace});
|
||||||
|
}, 100);
|
||||||
|
}, [navigate, location, to, replace]);
|
||||||
|
|
||||||
|
// Return null since this is just a redirect component
|
||||||
|
return null;
|
||||||
|
}
|
||||||
1
src/components/QueryPreservingRedirect/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export {QueryPreservingRedirect} from './QueryPreservingRedirect';
|
||||||
@@ -2,3 +2,7 @@ export * from './InputField';
|
|||||||
export * from './LoginForm';
|
export * from './LoginForm';
|
||||||
export * from './FlexContainer';
|
export * from './FlexContainer';
|
||||||
export * from './WebSocketStatusView';
|
export * from './WebSocketStatusView';
|
||||||
|
export * from './AuthorizedOnly';
|
||||||
|
export * from './ProtectedRoute';
|
||||||
|
export * from './Navigation';
|
||||||
|
export * from './QueryPreservingRedirect';
|
||||||
|
|||||||
40
src/routers/router.tsx
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import {BrowserRouter, Routes, Route} from 'react-router-dom';
|
||||||
|
import {LoginView, DashboardView, ChannelsView, TestView} from 'views';
|
||||||
|
import {ProtectedRoute, QueryPreservingRedirect} from 'components';
|
||||||
|
|
||||||
|
export default function Router() {
|
||||||
|
return (
|
||||||
|
<BrowserRouter>
|
||||||
|
<Routes>
|
||||||
|
{/* Public routes */}
|
||||||
|
<Route path="/login" element={<LoginView />} />
|
||||||
|
|
||||||
|
{/* Test route for debugging query parameters */}
|
||||||
|
<Route path="/test" element={<TestView />} />
|
||||||
|
|
||||||
|
{/* Protected routes - require authentication */}
|
||||||
|
<Route
|
||||||
|
path="/dashboard"
|
||||||
|
element={
|
||||||
|
<ProtectedRoute>
|
||||||
|
<DashboardView />
|
||||||
|
</ProtectedRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Route
|
||||||
|
path="/channels"
|
||||||
|
element={
|
||||||
|
<ProtectedRoute>
|
||||||
|
<ChannelsView />
|
||||||
|
</ProtectedRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Default redirects with query parameter preservation */}
|
||||||
|
<Route path="/" element={<QueryPreservingRedirect to="/dashboard" />} />
|
||||||
|
<Route path="*" element={<QueryPreservingRedirect to="/dashboard" />} />
|
||||||
|
</Routes>
|
||||||
|
</BrowserRouter>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
export default {
|
export default {
|
||||||
|
loginUrl: () => '/login',
|
||||||
|
dashboardUrl: () => '/dashboard',
|
||||||
fetchChannelsUrl: () => '/pcast/channels',
|
fetchChannelsUrl: () => '/pcast/channels',
|
||||||
createChannelUrl: () => '/pcast/channel',
|
createChannelUrl: () => '/pcast/channel',
|
||||||
deleteChannelUrl: () => '/pcast/channel',
|
deleteChannelUrl: () => '/pcast/channel',
|
||||||
@@ -131,7 +131,6 @@ export default class Logger {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (args.length > 1) {
|
if (args.length > 1) {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
args = args.reduce((accumulator: any, currentValue: any, index: number, array: any[]) => {
|
args = args.reduce((accumulator: any, currentValue: any, index: number, array: any[]) => {
|
||||||
if (index + 1 === array.length && currentValue instanceof Error) {
|
if (index + 1 === array.length && currentValue instanceof Error) {
|
||||||
return accumulator + '\n' + this.toString(currentValue.stack);
|
return accumulator + '\n' + this.toString(currentValue.stack);
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ export default class LoggerFactory {
|
|||||||
static async applyTelemetryConfiguration(level: LoggingLevel): Promise<void> {
|
static async applyTelemetryConfiguration(level: LoggingLevel): Promise<void> {
|
||||||
this._telemetryConfiguration.threshold = level || LoggerDefaults.defaultTelemetryLoggingLevel;
|
this._telemetryConfiguration.threshold = level || LoggerDefaults.defaultTelemetryLoggingLevel;
|
||||||
this._telemetryConfiguration.url = hostService.getTelemetryUrl();
|
this._telemetryConfiguration.url = hostService.getTelemetryUrl();
|
||||||
this._telemetryConfiguration.environment = hostService.getBaseUrlOrigin();
|
this._telemetryConfiguration.environment = hostService.getPcastBaseUrlOrigin();
|
||||||
// this._telemetryConfiguration.tenancy = await userStore.get('applicationId');
|
// this._telemetryConfiguration.tenancy = await userStore.get('applicationId');
|
||||||
// this._telemetryConfiguration.userId = await userStore.get('applicationId');
|
// this._telemetryConfiguration.userId = await userStore.get('applicationId');
|
||||||
// this._telemetryConfiguration.sessionId = await userStore.get('sessionId');
|
// this._telemetryConfiguration.sessionId = await userStore.get('sessionId');
|
||||||
|
|||||||
25
src/services/pcast-api.service.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import PCastApi, { ApplicationCredentials, Channel } from "@techniker-me/pcast-api";
|
||||||
|
|
||||||
|
class PCastApiService {
|
||||||
|
private static _instance: PCastApiService = new PCastApiService();
|
||||||
|
private _pcastApi: PCastApi | null = null;
|
||||||
|
|
||||||
|
public static getInstance(): PCastApiService {
|
||||||
|
return PCastApiService._instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async initialize(pcastUri: string, applicationCredentials: ApplicationCredentials): Promise<void> {
|
||||||
|
console.log('initialize', pcastUri, applicationCredentials);
|
||||||
|
this._pcastApi = PCastApi.create(pcastUri, applicationCredentials);
|
||||||
|
}
|
||||||
|
|
||||||
|
public get pcastApi(): PCastApi | null {
|
||||||
|
return this._pcastApi;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async fetchChannelsList(): Promise<Channel[]> {
|
||||||
|
return this._pcastApi?.channels.list() || [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PCastApiService.getInstance();
|
||||||
@@ -66,8 +66,6 @@ export default class TelemetryService {
|
|||||||
this._logs.unshift(logRecord);
|
this._logs.unshift(logRecord);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// @ts-expect-error: Unused variable intentionally
|
// @ts-expect-error: Unused variable intentionally
|
||||||
const ignored = this.sendLogsIfAble();
|
const ignored = this.sendLogsIfAble();
|
||||||
}
|
}
|
||||||
@@ -116,8 +114,6 @@ export default class TelemetryService {
|
|||||||
.then(response => {
|
.then(response => {
|
||||||
this._isSending = false;
|
this._isSending = false;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// @ts-expect-error: Unused variable intentionally
|
// @ts-expect-error: Unused variable intentionally
|
||||||
|
|
||||||
const ignored = this.sendLogsIfAble();
|
const ignored = this.sendLogsIfAble();
|
||||||
@@ -127,8 +123,6 @@ export default class TelemetryService {
|
|||||||
.catch(() => {
|
.catch(() => {
|
||||||
this._isSending = false;
|
this._isSending = false;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// @ts-expect-error: Unused variable intentionally
|
// @ts-expect-error: Unused variable intentionally
|
||||||
|
|
||||||
const ignored = this.sendLogsIfAble();
|
const ignored = this.sendLogsIfAble();
|
||||||
|
|||||||
@@ -5,22 +5,17 @@
|
|||||||
export type UrlType = 'pcast' | 'telemetry' | 'websocket';
|
export type UrlType = 'pcast' | 'telemetry' | 'websocket';
|
||||||
|
|
||||||
export default class UrlService {
|
export default class UrlService {
|
||||||
private static getBaseUrlOrigin(url: string = window?.location?.href, urlType: UrlType = 'pcast'): string {
|
public static getBaseUrlOrigin(url: string = window?.location?.href, urlType: UrlType = 'pcast'): string {
|
||||||
if (!url) {
|
if (!url) {
|
||||||
throw new Error('Invalid URL');
|
throw new Error('Invalid URL');
|
||||||
}
|
}
|
||||||
|
|
||||||
const baseURL = new URL(url);
|
const baseURL = new URL(url);
|
||||||
const backendUrlParam = baseURL.searchParams.get('backend');
|
const searchParams = new URLSearchParams(baseURL.search);
|
||||||
|
const backendUrlParam = searchParams.get('backend');
|
||||||
|
|
||||||
if (backendUrlParam) {
|
if (backendUrlParam) {
|
||||||
const backendUrl = new URL(backendUrlParam);
|
return backendUrlParam;
|
||||||
if (urlType === 'websocket') {
|
|
||||||
backendUrl.protocol = 'wss:';
|
|
||||||
backendUrl.pathname = '/ws';
|
|
||||||
}
|
|
||||||
|
|
||||||
return backendUrl.toString();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const segments = baseURL.hostname.split('.');
|
const segments = baseURL.hostname.split('.');
|
||||||
@@ -51,8 +46,6 @@ export default class UrlService {
|
|||||||
|
|
||||||
baseURL.hostname = segments.join('.');
|
baseURL.hostname = segments.join('.');
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return baseURL.origin;
|
return baseURL.origin;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,6 +64,10 @@ export default class UrlService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static getWebSocketUrl(url: string = window?.location?.href): string {
|
public static getWebSocketUrl(url: string = window?.location?.href): string {
|
||||||
return this.getBaseUrlOrigin(url, 'websocket');
|
const baseUrl = new URL(this.getBaseUrlOrigin(url, 'pcast'));
|
||||||
|
baseUrl.protocol = 'wss:';
|
||||||
|
baseUrl.pathname = '/ws';
|
||||||
|
|
||||||
|
return baseUrl.toString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import {Middleware} from '@reduxjs/toolkit';
|
import {Middleware} from '@reduxjs/toolkit';
|
||||||
|
|
||||||
const phenixWebSocketMiddleware: Middleware = (storeApi) => (next) => (action) => {
|
const phenixWebSocketMiddleware: Middleware = storeApi => next => action => {
|
||||||
console.log('phenixWebSocketMiddleware', action);
|
console.log('phenixWebSocketMiddleware', action);
|
||||||
if (typeof action === 'function') {
|
if (typeof action === 'function') {
|
||||||
return action(storeApi.getState(), storeApi.dispatch);
|
return action(storeApi.getState(), storeApi.dispatch);
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ export interface IAuthenticationState {
|
|||||||
isAuthenticating: boolean;
|
isAuthenticating: boolean;
|
||||||
error: string | null;
|
error: string | null;
|
||||||
status: PhenixWebSocketStatusType;
|
status: PhenixWebSocketStatusType;
|
||||||
|
sessionId: string | null;
|
||||||
|
roles: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialAuthenticationState: IAuthenticationState = {
|
const initialAuthenticationState: IAuthenticationState = {
|
||||||
@@ -17,7 +19,9 @@ const initialAuthenticationState: IAuthenticationState = {
|
|||||||
isAuthenticated: false,
|
isAuthenticated: false,
|
||||||
isAuthenticating: false,
|
isAuthenticating: false,
|
||||||
error: null,
|
error: null,
|
||||||
status: 'Offline'
|
status: 'Offline',
|
||||||
|
sessionId: null,
|
||||||
|
roles: []
|
||||||
};
|
};
|
||||||
|
|
||||||
// Memoized selectors
|
// Memoized selectors
|
||||||
@@ -36,6 +40,11 @@ export const selectCredentials = createSelector([selectAuthentication], authenti
|
|||||||
secret: authentication.secret
|
secret: authentication.secret
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
export const selectSessionInfo = createSelector([selectAuthentication], authentication => ({
|
||||||
|
sessionId: authentication.sessionId,
|
||||||
|
roles: authentication.roles
|
||||||
|
}));
|
||||||
|
|
||||||
const authenticateCredentialsThunk = createAsyncThunk(
|
const authenticateCredentialsThunk = createAsyncThunk(
|
||||||
'authentication/authenticate',
|
'authentication/authenticate',
|
||||||
async (credentials: {applicationId: string; secret: string}, {rejectWithValue}) => {
|
async (credentials: {applicationId: string; secret: string}, {rejectWithValue}) => {
|
||||||
@@ -43,6 +52,10 @@ const authenticateCredentialsThunk = createAsyncThunk(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const signoutThunk = createAsyncThunk('authentication/signout', async (_, {rejectWithValue}) => {
|
||||||
|
return AuthenticationService.signout().catch(rejectWithValue);
|
||||||
|
});
|
||||||
|
|
||||||
const authenticationSlice = createSlice({
|
const authenticationSlice = createSlice({
|
||||||
name: 'authentication',
|
name: 'authentication',
|
||||||
initialState: {...initialAuthenticationState},
|
initialState: {...initialAuthenticationState},
|
||||||
@@ -56,6 +69,8 @@ const authenticationSlice = createSlice({
|
|||||||
state.applicationId = null;
|
state.applicationId = null;
|
||||||
state.secret = null;
|
state.secret = null;
|
||||||
state.error = null;
|
state.error = null;
|
||||||
|
state.sessionId = null;
|
||||||
|
state.roles = [];
|
||||||
},
|
},
|
||||||
error: (state, action) => {
|
error: (state, action) => {
|
||||||
// Store only the error message string, not the Error object
|
// Store only the error message string, not the Error object
|
||||||
@@ -76,11 +91,6 @@ const authenticationSlice = createSlice({
|
|||||||
state.isAuthenticating = true;
|
state.isAuthenticating = true;
|
||||||
state.error = null;
|
state.error = null;
|
||||||
})
|
})
|
||||||
.addCase(authenticateCredentialsThunk.fulfilled, state => {
|
|
||||||
state.isAuthenticating = false;
|
|
||||||
state.error = null;
|
|
||||||
state.isAuthenticated = true;
|
|
||||||
})
|
|
||||||
.addCase(authenticateCredentialsThunk.rejected, (state, action) => {
|
.addCase(authenticateCredentialsThunk.rejected, (state, action) => {
|
||||||
state.isAuthenticating = false;
|
state.isAuthenticating = false;
|
||||||
// Extract error message string instead of creating new Error object
|
// Extract error message string instead of creating new Error object
|
||||||
@@ -102,9 +112,58 @@ const authenticationSlice = createSlice({
|
|||||||
state.isAuthenticated = false;
|
state.isAuthenticated = false;
|
||||||
state.applicationId = null;
|
state.applicationId = null;
|
||||||
state.secret = null;
|
state.secret = null;
|
||||||
|
state.sessionId = null;
|
||||||
|
state.roles = [];
|
||||||
|
})
|
||||||
|
.addCase(authenticateCredentialsThunk.fulfilled, (state, action) => {
|
||||||
|
state.isAuthenticating = false;
|
||||||
|
|
||||||
|
if (action.payload.error) {
|
||||||
|
state.error = action.payload.error as string;
|
||||||
|
state.isAuthenticated = false;
|
||||||
|
} else if (action.payload.response && (action.payload.response as {status: string}).status === 'ok') {
|
||||||
|
state.isAuthenticated = true;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Safely extract sessionId and roles with type checking
|
||||||
|
if (action.payload.response && typeof action.payload.response === 'object') {
|
||||||
|
const response = action.payload.response as any;
|
||||||
|
if (response.sessionId && typeof response.sessionId === 'string') {
|
||||||
|
state.sessionId = response.sessionId;
|
||||||
|
}
|
||||||
|
if (Array.isArray(response.roles)) {
|
||||||
|
state.roles = response.roles;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
state.error = 'Authentication failed';
|
||||||
|
state.isAuthenticated = false;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.addCase(signoutThunk.pending, state => {
|
||||||
|
state.isAuthenticating = true;
|
||||||
|
state.error = null;
|
||||||
|
})
|
||||||
|
.addCase(signoutThunk.rejected, (state, action) => {
|
||||||
|
state.isAuthenticating = false;
|
||||||
|
state.error = action.payload as string;
|
||||||
|
state.isAuthenticated = false;
|
||||||
|
state.applicationId = null;
|
||||||
|
state.secret = null;
|
||||||
|
state.sessionId = null;
|
||||||
|
state.roles = [];
|
||||||
|
})
|
||||||
|
.addCase(signoutThunk.fulfilled, state => {
|
||||||
|
state.isAuthenticated = false;
|
||||||
|
state.isAuthenticating = false;
|
||||||
|
state.applicationId = null;
|
||||||
|
state.secret = null;
|
||||||
|
state.sessionId = null;
|
||||||
|
state.roles = [];
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export const {reducer: authenticationReducer, actions: authenticationActions} = authenticationSlice;
|
export const {reducer: authenticationReducer, actions: authenticationActions} = authenticationSlice;
|
||||||
export {authenticateCredentialsThunk};
|
export {authenticateCredentialsThunk, signoutThunk};
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import {configureStore} from '@reduxjs/toolkit';
|
import {configureStore} from '@reduxjs/toolkit';
|
||||||
import reducer from './reducer';
|
import reducer from './reducer';
|
||||||
import phenixWebSocketMiddleware from './middlewares/PhenixWebSocket.middleware';
|
|
||||||
|
|
||||||
export const store = configureStore({reducer, middleware: getDefaultMiddleware => getDefaultMiddleware().concat(phenixWebSocketMiddleware)});
|
export const store = configureStore({reducer});
|
||||||
|
|
||||||
export type RootState = ReturnType<typeof store.getState>;
|
export type RootState = ReturnType<typeof store.getState>;
|
||||||
|
|||||||
2
src/types/phenix-web-proto.d.ts
vendored
@@ -2,7 +2,7 @@ declare module 'phenix-web-proto' {
|
|||||||
import {ILogger} from 'services/logger/LoggerInterface';
|
import {ILogger} from 'services/logger/LoggerInterface';
|
||||||
|
|
||||||
export class MQWebSocket {
|
export class MQWebSocket {
|
||||||
constructor(uri: string, logger: ILogger, protocols: any[], apiVersion?: string);
|
constructor(uri: string, logger: ILogger, protocols: unknown[], apiVersion?: string);
|
||||||
onEvent(eventName: string, handler: (event: unknown) => void): void;
|
onEvent(eventName: string, handler: (event: unknown) => void): void;
|
||||||
onRequest(requestName: string, handler: (request: unknown) => void): void;
|
onRequest(requestName: string, handler: (request: unknown) => void): void;
|
||||||
sendRequest(type: string, message: unknown, callback?: (error: unknown, response: unknown) => void, settings?: unknown): void;
|
sendRequest(type: string, message: unknown, callback?: (error: unknown, response: unknown) => void, settings?: unknown): void;
|
||||||
|
|||||||
40
src/views/ChannelsView.tsx
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import {JSX} from 'react';
|
||||||
|
import {Navigation} from '../components';
|
||||||
|
|
||||||
|
export function ChannelsView(): JSX.Element {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Navigation />
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
padding: '2rem',
|
||||||
|
maxWidth: '1200px',
|
||||||
|
margin: '0 auto'
|
||||||
|
}}>
|
||||||
|
<h1>Channels</h1>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
background: '#f5f5f5',
|
||||||
|
padding: '1rem',
|
||||||
|
borderRadius: '8px',
|
||||||
|
marginBottom: '2rem'
|
||||||
|
}}>
|
||||||
|
<p>This is a protected view that requires authentication.</p>
|
||||||
|
<p>If you can see this, you are successfully authenticated!</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
background: 'white',
|
||||||
|
padding: '1rem',
|
||||||
|
borderRadius: '8px',
|
||||||
|
border: '1px solid #ddd'
|
||||||
|
}}>
|
||||||
|
<h3>Channel List</h3>
|
||||||
|
<p>No channels available at the moment.</p>
|
||||||
|
<p>This view demonstrates the AuthorizedOnly middleware in action.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
105
src/views/DashboardView.tsx
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
import {JSX} from 'react';
|
||||||
|
import {useAppDispatch, useAppSelector} from '../hooks/store';
|
||||||
|
import {selectCredentials, selectSessionInfo, authenticationActions} from '../store/slices/Authentication.slice';
|
||||||
|
import {useNavigate} from 'react-router-dom';
|
||||||
|
import {Navigation} from '../components';
|
||||||
|
|
||||||
|
export function DashboardView(): JSX.Element {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const credentials = useAppSelector(selectCredentials);
|
||||||
|
const sessionInfo = useAppSelector(selectSessionInfo);
|
||||||
|
|
||||||
|
const handleLogout = () => {
|
||||||
|
dispatch(authenticationActions.signout());
|
||||||
|
navigate('/login', {replace: true});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Navigation />
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
padding: '2rem',
|
||||||
|
maxWidth: '1200px',
|
||||||
|
margin: '0 auto'
|
||||||
|
}}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginBottom: '2rem'
|
||||||
|
}}>
|
||||||
|
<h1>Dashboard</h1>
|
||||||
|
<button
|
||||||
|
onClick={handleLogout}
|
||||||
|
style={{
|
||||||
|
padding: '0.5rem 1rem',
|
||||||
|
background: '#dc3545',
|
||||||
|
color: 'white',
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: '4px',
|
||||||
|
cursor: 'pointer'
|
||||||
|
}}>
|
||||||
|
Logout
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
background: '#222',
|
||||||
|
padding: '1rem',
|
||||||
|
borderRadius: '8px',
|
||||||
|
marginBottom: '2rem'
|
||||||
|
}}>
|
||||||
|
<h3>Welcome!</h3>
|
||||||
|
<p>You are successfully authenticated.</p>
|
||||||
|
<p>
|
||||||
|
<strong>Application ID:</strong> {credentials.applicationId || 'N/A'}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>Session ID:</strong> {sessionInfo.sessionId || 'N/A'}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>Roles:</strong> {sessionInfo.roles.length > 0 ? sessionInfo.roles.join(', ') : 'None'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'grid',
|
||||||
|
gridTemplateColumns: 'repeat(auto-fit, minmax(300px, 1fr))',
|
||||||
|
gap: '1rem'
|
||||||
|
}}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
background: 'white',
|
||||||
|
padding: '1rem',
|
||||||
|
borderRadius: '8px',
|
||||||
|
border: '1px solid #ddd'
|
||||||
|
}}>
|
||||||
|
<h4>Quick Actions</h4>
|
||||||
|
<ul>
|
||||||
|
<li>View Channels</li>
|
||||||
|
<li>Check Analytics</li>
|
||||||
|
<li>Monitor QoS</li>
|
||||||
|
<li>Manage Rooms</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
background: 'white',
|
||||||
|
padding: '1rem',
|
||||||
|
borderRadius: '8px',
|
||||||
|
border: '1px solid #ddd'
|
||||||
|
}}>
|
||||||
|
<h4>Recent Activity</h4>
|
||||||
|
<p>No recent activity to display.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
17
src/views/LoginView.tsx
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import {JSX} from 'react';
|
||||||
|
import {LoginForm} from 'components';
|
||||||
|
|
||||||
|
export function LoginView(): JSX.Element {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
height: '100vh'
|
||||||
|
}}>
|
||||||
|
<LoginForm />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
99
src/views/TestView.tsx
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
import {JSX} from 'react';
|
||||||
|
import {useSearchParams} from 'react-router-dom';
|
||||||
|
|
||||||
|
export function TestView(): JSX.Element {
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
|
|
||||||
|
// Get all query parameters
|
||||||
|
const allParams = Array.from(searchParams.entries());
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
padding: '2rem',
|
||||||
|
maxWidth: '800px',
|
||||||
|
margin: '0 auto'
|
||||||
|
}}>
|
||||||
|
<h1>Query Parameter Test Page</h1>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
background: '#f8f9fa',
|
||||||
|
padding: '1rem',
|
||||||
|
borderRadius: '8px',
|
||||||
|
marginBottom: '2rem',
|
||||||
|
border: '1px solid #dee2e6'
|
||||||
|
}}>
|
||||||
|
<h3>Current URL Information</h3>
|
||||||
|
<p>
|
||||||
|
<strong>Full URL:</strong> {window.location.href}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>Path:</strong> {window.location.pathname}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>Search:</strong> {window.location.search || '(none)'}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>Hash:</strong> {window.location.hash || '(none)'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
background: '#e7f3ff',
|
||||||
|
padding: '1rem',
|
||||||
|
borderRadius: '8px',
|
||||||
|
marginBottom: '2rem',
|
||||||
|
border: '1px solid #007bff'
|
||||||
|
}}>
|
||||||
|
<h3>Query Parameters</h3>
|
||||||
|
{allParams.length > 0 ? (
|
||||||
|
<ul style={{margin: 0, paddingLeft: '1.5rem'}}>
|
||||||
|
{allParams.map(([key, value]) => (
|
||||||
|
<li key={key}>
|
||||||
|
<strong>{key}:</strong> {value}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
) : (
|
||||||
|
<p>No query parameters found</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
background: '#fff3cd',
|
||||||
|
padding: '1rem',
|
||||||
|
borderRadius: '8px',
|
||||||
|
border: '1px solid #ffc107'
|
||||||
|
}}>
|
||||||
|
<h3>Test Instructions</h3>
|
||||||
|
<p>To test query parameter preservation:</p>
|
||||||
|
<ol>
|
||||||
|
<li>
|
||||||
|
Navigate to this page with query parameters: <code>/test?param1=value1¶m2=value2</code>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
If not authenticated, you should be redirected to: <code>/login?param1=value1¶m2=value2</code>
|
||||||
|
</li>
|
||||||
|
<li>Check the console for debug information from AuthorizedOnly</li>
|
||||||
|
<li>After login, you should be able to return to the original URL with parameters</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<h4>Test URLs to try:</h4>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<code>/test?tab=dashboard&filter=active</code>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<code>/test?user=123&view=detailed&sort=date</code>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<code>/test?lang=en&theme=dark&debug=true</code>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
4
src/views/index.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export {LoginView} from './LoginView';
|
||||||
|
export {DashboardView} from './DashboardView';
|
||||||
|
export {ChannelsView} from './ChannelsView';
|
||||||
|
export {TestView} from './TestView';
|
||||||
@@ -1 +1 @@
|
|||||||
{"root":["./src/app.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/components/index.ts","./src/components/flexcontainer/flexcontainercomponent.tsx","./src/components/flexcontainer/index.ts","./src/components/inputfield/inputfield.tsx","./src/components/inputfield/index.ts","./src/components/loginform/loginform.tsx","./src/components/loginform/index.ts","./src/components/websocketstatusview/websocketstatusviewcomponent.tsx","./src/components/websocketstatusview/index.ts","./src/config/index.ts","./src/hooks/index.ts","./src/hooks/store.ts","./src/lang/strings.ts","./src/lang/assertunreachable.ts","./src/routes/index.ts","./src/services/authentication.service.ts","./src/services/host-url.service.ts","./src/services/platform-detection.service.ts","./src/services/logger/appenders.ts","./src/services/logger/consoleappender.ts","./src/services/logger/iappender.ts","./src/services/logger/logger.ts","./src/services/logger/loggerdefaults.ts","./src/services/logger/loggerfactory.ts","./src/services/logger/loggerinterface.ts","./src/services/logger/logginglevelmapping.ts","./src/services/logger/loggingthreshold.ts","./src/services/net/phenixwebsocket.ts","./src/services/telemetry/telemetryapender.ts","./src/services/telemetry/telemetryconfiguration.ts","./src/services/telemetry/telemetryservice.ts","./src/store/index.ts","./src/store/reducer.ts","./src/store/store.ts","./src/store/middlewares/phenixwebsocket.middleware.ts","./src/store/slices/authentication.slice.ts","./src/store/slices/index.ts","./src/types/phenix-web-proto.d.ts"],"version":"5.9.2"}
|
{"root":["./src/app.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/components/index.ts","./src/components/authorizedonly/authorizedonly.tsx","./src/components/authorizedonly/index.ts","./src/components/flexcontainer/flexcontainercomponent.tsx","./src/components/flexcontainer/index.ts","./src/components/inputfield/inputfield.tsx","./src/components/inputfield/index.ts","./src/components/loginform/loginform.tsx","./src/components/loginform/index.ts","./src/components/navigation/navigation.tsx","./src/components/navigation/index.ts","./src/components/protectedroute/protectedroute.tsx","./src/components/protectedroute/index.ts","./src/components/querypreservingredirect/querypreservingredirect.tsx","./src/components/querypreservingredirect/index.ts","./src/components/websocketstatusview/websocketstatusviewcomponent.tsx","./src/components/websocketstatusview/index.ts","./src/config/index.ts","./src/hooks/index.ts","./src/hooks/store.ts","./src/lang/strings.ts","./src/lang/assertunreachable.ts","./src/routers/router.tsx","./src/routers/static-routes.ts","./src/services/authentication.service.ts","./src/services/pcast-api.service.ts","./src/services/platform-detection.service.ts","./src/services/url.service.ts","./src/services/logger/appenders.ts","./src/services/logger/consoleappender.ts","./src/services/logger/iappender.ts","./src/services/logger/logger.ts","./src/services/logger/loggerdefaults.ts","./src/services/logger/loggerfactory.ts","./src/services/logger/loggerinterface.ts","./src/services/logger/logginglevelmapping.ts","./src/services/logger/loggingthreshold.ts","./src/services/net/phenixwebsocket.ts","./src/services/telemetry/telemetryapender.ts","./src/services/telemetry/telemetryconfiguration.ts","./src/services/telemetry/telemetryservice.ts","./src/store/index.ts","./src/store/reducer.ts","./src/store/store.ts","./src/store/middlewares/phenixwebsocket.middleware.ts","./src/store/slices/authentication.slice.ts","./src/store/slices/index.ts","./src/types/phenix-web-proto.d.ts","./src/views/channelsview.tsx","./src/views/dashboardview.tsx","./src/views/loginview.tsx","./src/views/testview.tsx","./src/views/index.ts"],"version":"5.9.2"}
|
||||||