initial commit
This commit is contained in:
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
3
.npmrc
Normal file
3
.npmrc
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
save-exact=true
|
||||||
|
package-lock=false
|
||||||
|
@techniker-me:registry=https://registry-node.techniker.me
|
||||||
12
.prettierrc
Normal file
12
.prettierrc
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"arrowParens": "avoid",
|
||||||
|
"bracketSameLine": true,
|
||||||
|
"bracketSpacing": false,
|
||||||
|
"printWidth": 160,
|
||||||
|
"semi": true,
|
||||||
|
"singleAttributePerLine": false,
|
||||||
|
"singleQuote": true,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"trailingComma": "none",
|
||||||
|
"useTabs": false
|
||||||
|
}
|
||||||
69
README.md
Normal file
69
README.md
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
# React + TypeScript + Vite
|
||||||
|
|
||||||
|
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
||||||
|
|
||||||
|
Currently, two official plugins are available:
|
||||||
|
|
||||||
|
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh
|
||||||
|
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
||||||
|
|
||||||
|
## Expanding the ESLint configuration
|
||||||
|
|
||||||
|
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
|
||||||
|
|
||||||
|
```js
|
||||||
|
export default tseslint.config([
|
||||||
|
globalIgnores(['dist']),
|
||||||
|
{
|
||||||
|
files: ['**/*.{ts,tsx}'],
|
||||||
|
extends: [
|
||||||
|
// Other configs...
|
||||||
|
|
||||||
|
// Remove tseslint.configs.recommended and replace with this
|
||||||
|
...tseslint.configs.recommendedTypeChecked,
|
||||||
|
// Alternatively, use this for stricter rules
|
||||||
|
...tseslint.configs.strictTypeChecked,
|
||||||
|
// Optionally, add this for stylistic rules
|
||||||
|
...tseslint.configs.stylisticTypeChecked
|
||||||
|
|
||||||
|
// Other configs...
|
||||||
|
],
|
||||||
|
languageOptions: {
|
||||||
|
parserOptions: {
|
||||||
|
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
||||||
|
tsconfigRootDir: import.meta.dirname
|
||||||
|
}
|
||||||
|
// other options...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// eslint.config.js
|
||||||
|
import reactX from 'eslint-plugin-react-x';
|
||||||
|
import reactDom from 'eslint-plugin-react-dom';
|
||||||
|
|
||||||
|
export default tseslint.config([
|
||||||
|
globalIgnores(['dist']),
|
||||||
|
{
|
||||||
|
files: ['**/*.{ts,tsx}'],
|
||||||
|
extends: [
|
||||||
|
// Other configs...
|
||||||
|
// Enable lint rules for React
|
||||||
|
reactX.configs['recommended-typescript'],
|
||||||
|
// Enable lint rules for React DOM
|
||||||
|
reactDom.configs.recommended
|
||||||
|
],
|
||||||
|
languageOptions: {
|
||||||
|
parserOptions: {
|
||||||
|
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
||||||
|
tsconfigRootDir: import.meta.dirname
|
||||||
|
}
|
||||||
|
// other options...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
```
|
||||||
10
bunfig.toml
Normal file
10
bunfig.toml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
[install]
|
||||||
|
exact = true
|
||||||
|
saveTextLockfile = false
|
||||||
|
frozenLockfile = true
|
||||||
|
|
||||||
|
[install.lockfile]
|
||||||
|
save = false
|
||||||
|
|
||||||
|
[install.scopes]
|
||||||
|
"@techniker-me"="https://registry-node.techniker.me"
|
||||||
28
eslint.config.js
Normal file
28
eslint.config.js
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import js from '@eslint/js';
|
||||||
|
import globals from 'globals';
|
||||||
|
import reactHooks from 'eslint-plugin-react-hooks';
|
||||||
|
import reactRefresh from 'eslint-plugin-react-refresh';
|
||||||
|
import tseslint from 'typescript-eslint';
|
||||||
|
import {globalIgnores} from 'eslint/config';
|
||||||
|
|
||||||
|
export default tseslint.config([
|
||||||
|
globalIgnores(['dist']),
|
||||||
|
{
|
||||||
|
files: ['**/*.{ts,tsx}'],
|
||||||
|
extends: [js.configs.recommended, tseslint.configs.recommended, reactHooks.configs['recommended-latest'], reactRefresh.configs.vite],
|
||||||
|
languageOptions: {
|
||||||
|
ecmaVersion: 2020,
|
||||||
|
globals: globals.browser
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
'@typescript-eslint/no-unused-vars': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
'argsIgnorePattern': '^ignored',
|
||||||
|
'varsIgnorePattern': '^ignored',
|
||||||
|
'caughtErrorsIgnorePattern': '^ignored'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]);
|
||||||
18
index.html
Normal file
18
index.html
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
||||||
|
<meta name="theme-color" content="#000000" />
|
||||||
|
<meta name="description" content="Phenix Customer Portal" />
|
||||||
|
<script>
|
||||||
|
__phenixPageLoadTime = new Date().getTime();
|
||||||
|
</script>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Phenix Customer Portal</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
50
package.json
Normal file
50
package.json
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
{
|
||||||
|
"name": "webcontrolcenter",
|
||||||
|
"private": true,
|
||||||
|
"version": "2025.2.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"generate-version": "node scripts/generate-version.js",
|
||||||
|
"dev": "vite",
|
||||||
|
"format": "prettier --write ./",
|
||||||
|
"prelint": "bun run format",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"lint:fix": "eslint --fix .",
|
||||||
|
"build": "tsc -b && vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@redux-devtools/extension": "3.3.0",
|
||||||
|
"@reduxjs/toolkit": "2.8.2",
|
||||||
|
"@techniker-me/pcast-api": "2025.1.4",
|
||||||
|
"@techniker-me/tools": "2025.0.16",
|
||||||
|
"@types/node": "24.3.0",
|
||||||
|
"phenix-web-closest-endpoint-resolver": "2020.0.3",
|
||||||
|
"phenix-web-detect-browser": "2021.0.1",
|
||||||
|
"phenix-web-proto": "2020.0.3",
|
||||||
|
"react": "19.1.1",
|
||||||
|
"react-dom": "19.1.1",
|
||||||
|
"react-redux": "9.2.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/js": "9.34.0",
|
||||||
|
"@types/react": "19.1.12",
|
||||||
|
"@types/react-dom": "19.1.9",
|
||||||
|
"@vitejs/plugin-react-swc": "4.0.1",
|
||||||
|
"babel-plugin-transform-amd-to-commonjs": "1.6.0",
|
||||||
|
"commander": "14.0.0",
|
||||||
|
"eslint": "9.34.0",
|
||||||
|
"eslint-plugin-react-hooks": "5.2.0",
|
||||||
|
"eslint-plugin-react-refresh": "0.4.20",
|
||||||
|
"globals": "16.3.0",
|
||||||
|
"prettier": "3.6.2",
|
||||||
|
"typescript": "5.9.2",
|
||||||
|
"typescript-eslint": "8.41.0",
|
||||||
|
"vite": "7.1.3",
|
||||||
|
"vite-plugin-babel": "1.3.2",
|
||||||
|
"vite-plugin-commonjs": "0.10.4"
|
||||||
|
},
|
||||||
|
"trustedDependencies": [
|
||||||
|
"@swc/core"
|
||||||
|
]
|
||||||
|
}
|
||||||
21
scripts/generate-version.js
Normal file
21
scripts/generate-version.js
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||||
|
*/
|
||||||
|
import {writeFileSync} from 'fs';
|
||||||
|
import {join} from 'path';
|
||||||
|
import {program} from 'commander';
|
||||||
|
import packageJson from './../package.json' with {type: 'json'};
|
||||||
|
|
||||||
|
program.option('-e --environment <environment>', 'Specific environment (optional)');
|
||||||
|
|
||||||
|
program.parse(process.argv);
|
||||||
|
|
||||||
|
const {environment} = program.opts();
|
||||||
|
const prefix = environment ? `${environment}-` : 'local-';
|
||||||
|
const version = `${prefix}${new Date().toISOString()} (${packageJson.version})`;
|
||||||
|
const fileLocation = join(process.cwd(), 'src', 'config', 'version.json');
|
||||||
|
const controlVersion = {version};
|
||||||
|
|
||||||
|
writeFileSync(fileLocation, JSON.stringify(controlVersion, null, 2));
|
||||||
|
|
||||||
|
console.log(`Generated control center version [${version}]`);
|
||||||
47
src/App.tsx
Normal file
47
src/App.tsx
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import {useAppSelector, useAppDispatch} from 'hooks/store';
|
||||||
|
import {WebSocketStatusViewComponent, LoginForm} from './components';
|
||||||
|
import AuthenticationService from './services/authentication.service';
|
||||||
|
import {useEffect, useState} from 'react';
|
||||||
|
import {authenticationActions, selectError} from 'store/slices/Authentication.slice';
|
||||||
|
import hostService from 'services/url.service';
|
||||||
|
import LoggerFactory from './services/logger/LoggerFactory';
|
||||||
|
|
||||||
|
export default function App() {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const [isInitialized, setIsInitialized] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Initialize logger after all modules are loaded to avoid circular dependencies
|
||||||
|
LoggerFactory.applyLoggerConfig();
|
||||||
|
|
||||||
|
// Set WebSocket URI only once during initialization
|
||||||
|
if (!isInitialized) {
|
||||||
|
AuthenticationService.setWebSocketUri(hostService.getWebSocketUrl());
|
||||||
|
setIsInitialized(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subscribe to WebSocket status changes
|
||||||
|
AuthenticationService.status?.subscribe(status =>
|
||||||
|
dispatch(authenticationActions.setStatus(status))
|
||||||
|
);
|
||||||
|
}, [dispatch, isInitialized]);
|
||||||
|
|
||||||
|
const error = useAppSelector(selectError);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{error && <div>{error}</div>}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: '200px',
|
||||||
|
height: '100px',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
margin: 'auto'
|
||||||
|
}}>
|
||||||
|
<WebSocketStatusViewComponent />
|
||||||
|
<LoginForm />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
16
src/components/FlexContainer/FlexContainerComponent.tsx
Normal file
16
src/components/FlexContainer/FlexContainerComponent.tsx
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import {PropsWithChildren} from 'react';
|
||||||
|
|
||||||
|
export interface IFlexContainerProps {
|
||||||
|
style?: Record<string, string>;
|
||||||
|
children: PropsWithChildren['children'];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FlexContainer({style, children}: IFlexContainerProps) {
|
||||||
|
const flexContainerStyle = {
|
||||||
|
display: 'flex',
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
...style
|
||||||
|
};
|
||||||
|
return <div style={flexContainerStyle}>{children}</div>;
|
||||||
|
}
|
||||||
1
src/components/FlexContainer/index.ts
Normal file
1
src/components/FlexContainer/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './FlexContainerComponent';
|
||||||
34
src/components/InputField/InputField.tsx
Normal file
34
src/components/InputField/InputField.tsx
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import {JSX} from 'react';
|
||||||
|
|
||||||
|
export interface IInputFieldProps {
|
||||||
|
label?: string;
|
||||||
|
type: 'text' | 'password' | 'number';
|
||||||
|
placeholder?: string;
|
||||||
|
style?: Record<string, string>;
|
||||||
|
disabled?: boolean;
|
||||||
|
value: string;
|
||||||
|
onValueChange: (value: string) => void;
|
||||||
|
isRequired?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function InputField({label, type, value, onValueChange, placeholder, style, disabled, isRequired}: IInputFieldProps): JSX.Element {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<label
|
||||||
|
style={{
|
||||||
|
display: 'block',
|
||||||
|
...(style || {})
|
||||||
|
}}>
|
||||||
|
{label}
|
||||||
|
<input
|
||||||
|
type={type}
|
||||||
|
value={value}
|
||||||
|
onChange={e => onValueChange(e.target.value)}
|
||||||
|
placeholder={placeholder}
|
||||||
|
disabled={disabled || false}
|
||||||
|
required={isRequired}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
1
src/components/InputField/index.ts
Normal file
1
src/components/InputField/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './InputField';
|
||||||
61
src/components/LoginForm/LoginForm.tsx
Normal file
61
src/components/LoginForm/LoginForm.tsx
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import {JSX, useState} from 'react';
|
||||||
|
import {useAppSelector, useAppDispatch} from '../../hooks';
|
||||||
|
import {authenticateCredentialsThunk, selectIsAuthenticating, selectStatus} from '../../store/slices/Authentication.slice';
|
||||||
|
import {InputField} from '../InputField';
|
||||||
|
|
||||||
|
export function LoginForm(): JSX.Element {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const [applicationId, setApplicationId] = useState<string>('phenixrts.com-alex.zinn');
|
||||||
|
const [secret, setSecret] = useState<string>('AMAsDzr.dIuGMZ.Zu52Dt~MQvP!DZwYg');
|
||||||
|
|
||||||
|
const isAuthenticating = useAppSelector(selectIsAuthenticating);
|
||||||
|
const connectionStatus = useAppSelector(selectStatus);
|
||||||
|
|
||||||
|
// Only allow authentication when WebSocket is Online
|
||||||
|
const isConnectionReady = connectionStatus === 'Online';
|
||||||
|
const isDisabled = isAuthenticating || !isConnectionReady;
|
||||||
|
|
||||||
|
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (!isConnectionReady) {
|
||||||
|
alert('Please wait for WebSocket connection to be established before attempting to authenticate.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(authenticateCredentialsThunk({applicationId, secret}));
|
||||||
|
setApplicationId('');
|
||||||
|
setSecret('');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<InputField
|
||||||
|
type="text"
|
||||||
|
placeholder="Application Id"
|
||||||
|
value={applicationId}
|
||||||
|
onValueChange={value => setApplicationId(value)}
|
||||||
|
disabled={isDisabled}
|
||||||
|
isRequired={true}
|
||||||
|
/>
|
||||||
|
<InputField
|
||||||
|
type="password"
|
||||||
|
placeholder="secret"
|
||||||
|
value={secret}
|
||||||
|
onValueChange={value => setSecret(value)}
|
||||||
|
disabled={isDisabled}
|
||||||
|
isRequired={true}
|
||||||
|
/>
|
||||||
|
<button type="submit" disabled={isDisabled}>
|
||||||
|
{isConnectionReady ? 'Login' : 'Connecting...'}
|
||||||
|
</button>
|
||||||
|
{!isConnectionReady && (
|
||||||
|
<div style={{color: 'orange', fontSize: '12px', marginTop: '5px'}}>
|
||||||
|
Waiting for WebSocket connection...
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</form>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
1
src/components/LoginForm/index.ts
Normal file
1
src/components/LoginForm/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './LoginForm';
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import {useAppSelector} from 'hooks/store';
|
||||||
|
import {selectStatus} from 'store/slices/Authentication.slice';
|
||||||
|
|
||||||
|
export function WebSocketStatusViewComponent() {
|
||||||
|
const storeStatus = useAppSelector(selectStatus);
|
||||||
|
const displayStatus = storeStatus ?? 'unknown';
|
||||||
|
|
||||||
|
return <>Status: {displayStatus}</>;
|
||||||
|
}
|
||||||
1
src/components/WebSocketStatusView/index.ts
Normal file
1
src/components/WebSocketStatusView/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './WebSocketStatusViewComponent';
|
||||||
4
src/components/index.ts
Normal file
4
src/components/index.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export * from './InputField';
|
||||||
|
export * from './LoginForm';
|
||||||
|
export * from './FlexContainer';
|
||||||
|
export * from './WebSocketStatusView';
|
||||||
6
src/config/index.ts
Normal file
6
src/config/index.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||||
|
*/
|
||||||
|
import controlVersion from 'config/version.json';
|
||||||
|
|
||||||
|
export default {controlCenterVersion: controlVersion['version']};
|
||||||
3
src/config/version.json
Normal file
3
src/config/version.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"version": "local-2025-08-31T19:58:27.686Z (2025.2.0)"
|
||||||
|
}
|
||||||
1
src/hooks/index.ts
Normal file
1
src/hooks/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './store';
|
||||||
10
src/hooks/store.ts
Normal file
10
src/hooks/store.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import {useDispatch, useSelector} from 'react-redux';
|
||||||
|
import {store} from '../store';
|
||||||
|
// Use throughout the app instead of plain `useDispatch` and `useSelector`
|
||||||
|
// Infer the `RootState`, `AppDispatch`, and `AppStore` types from the store itself
|
||||||
|
export type RootState = ReturnType<typeof store.getState>;
|
||||||
|
// Inferred type: {posts: PostsState, comments: C
|
||||||
|
export type AppDispatch = typeof store.dispatch;
|
||||||
|
export type AppStore = typeof store;
|
||||||
|
export const useAppDispatch = useDispatch.withTypes<AppDispatch>();
|
||||||
|
export const useAppSelector = useSelector.withTypes<RootState>();
|
||||||
74
src/index.css
Normal file
74
src/index.css
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
:root {
|
||||||
|
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||||
|
line-height: 1.5;
|
||||||
|
font-weight: 400;
|
||||||
|
|
||||||
|
color-scheme: light dark;
|
||||||
|
color: rgba(255, 255, 255, 0.87);
|
||||||
|
background-color: #242424;
|
||||||
|
|
||||||
|
font-synthesis: none;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #646cff;
|
||||||
|
text-decoration: inherit;
|
||||||
|
}
|
||||||
|
a:hover {
|
||||||
|
color: #535bf2;
|
||||||
|
}
|
||||||
|
|
||||||
|
#root {
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
|
place-items: center;
|
||||||
|
min-width: 320px;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 3.2em;
|
||||||
|
line-height: 1.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
text-align: 'center';
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
padding: 0.6em 1.2em;
|
||||||
|
font-size: 1em;
|
||||||
|
font-weight: 500;
|
||||||
|
font-family: inherit;
|
||||||
|
background-color: #1a1a1a;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: border-color 0.25s;
|
||||||
|
}
|
||||||
|
button:hover {
|
||||||
|
border-color: #646cff;
|
||||||
|
}
|
||||||
|
button:focus,
|
||||||
|
button:focus-visible {
|
||||||
|
outline: 4px auto -webkit-focus-ring-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: light) {
|
||||||
|
:root {
|
||||||
|
color: #213547;
|
||||||
|
background-color: #ffffff;
|
||||||
|
}
|
||||||
|
a:hover {
|
||||||
|
color: #747bff;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
}
|
||||||
|
}
|
||||||
11
src/lang/Strings.ts
Normal file
11
src/lang/Strings.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
export default class Strings {
|
||||||
|
public static randomString(length: number): string {
|
||||||
|
return Math.random()
|
||||||
|
.toString(36)
|
||||||
|
.substring(2, 2 + length);
|
||||||
|
}
|
||||||
|
|
||||||
|
private constructor() {
|
||||||
|
throw new Error('Strings is a static class that may not be instantiated');
|
||||||
|
}
|
||||||
|
}
|
||||||
3
src/lang/assertUnreachable.ts
Normal file
3
src/lang/assertUnreachable.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export default function assertUnreachable(x: never): never {
|
||||||
|
throw new Error(`Error: Reached un-reachable code with [${x}]`);
|
||||||
|
}
|
||||||
11
src/main.tsx
Normal file
11
src/main.tsx
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import {createRoot} from 'react-dom/client';
|
||||||
|
import {Provider} from 'react-redux';
|
||||||
|
import {store} from './store';
|
||||||
|
import App from './App';
|
||||||
|
import './index.css';
|
||||||
|
|
||||||
|
createRoot(document.getElementById('root')!).render(
|
||||||
|
<Provider store={store}>
|
||||||
|
<App />
|
||||||
|
</Provider>
|
||||||
|
);
|
||||||
32
src/routes/index.ts
Normal file
32
src/routes/index.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
export default {
|
||||||
|
fetchChannelsUrl: () => '/pcast/channels',
|
||||||
|
createChannelUrl: () => '/pcast/channel',
|
||||||
|
deleteChannelUrl: () => '/pcast/channel',
|
||||||
|
fetchChannelUrl: (channelId: string): string => `/pcast/channel/${channelId}`,
|
||||||
|
fetchChannelMembersUrl: (channelId: string): string => `/pcast/channel/${channelId}/members`,
|
||||||
|
fetchRoomsUrl: () => '/pcast/rooms',
|
||||||
|
deleteRoomUrl: () => '/pcast/room',
|
||||||
|
createRoomUrl: () => '/pcast/room',
|
||||||
|
fetchRoomUrl: (roomId: string): string => `/pcast/room/${roomId}`,
|
||||||
|
fetchRoomMembersUrl: (roomId: string): string => `/pcast/room/${roomId}/members`,
|
||||||
|
stateOfChannelUrl: (channelId: string): string => `/pcast/channel/${encodeURIComponent(channelId)}/publishers/count`,
|
||||||
|
stateOfRoomUrl: (roomId: string): string => `/pcast/room/${encodeURIComponent(roomId)}/publishers/count`,
|
||||||
|
sendChannelMessageUrl: (channelId: string): string => `/pcast/channel/${encodeURIComponent(channelId)}/message`,
|
||||||
|
getChannelMessagesUrl: (channelId: string): string => `/pcast/channel/${encodeURIComponent(channelId)}/messages`,
|
||||||
|
getRoomMessagesUrl: (roomId: string): string => `/pcast/room/${encodeURIComponent(roomId)}/messages`,
|
||||||
|
playlistInformation: () => '/pcast/stream/playlist',
|
||||||
|
killChannel: (channelId: string): string => `/pcast/channel/${encodeURIComponent(channelId)}/kill`,
|
||||||
|
forkChannel: (destinationChannelId: string, sourceChannelId: string): string =>
|
||||||
|
`/pcast/channel/${encodeURIComponent(destinationChannelId)}/fork/${encodeURIComponent(sourceChannelId)}`,
|
||||||
|
viewReporting: () => '/pcast/reporting/viewing',
|
||||||
|
forkHistoryReporting: () => '/pcast/analytics/fork/history',
|
||||||
|
publishReporting: () => '/pcast/reporting/publishing',
|
||||||
|
concurrentViewers: () => '/pcast/streams/concurrent',
|
||||||
|
ingestReports: () => '/pcast/ingest/buffer',
|
||||||
|
terminateStream: () => '/pcast/stream',
|
||||||
|
edgeAuth: () => '/pcast/edgeAuth',
|
||||||
|
authTokenUrl: () => '/pcast/auth',
|
||||||
|
publishUri: (tenancy: string, publicationType: string): string => `/pcast/${tenancy}/stream/publish/uri/${publicationType}`,
|
||||||
|
networkThroughput: () => '/pcast/network/throughput',
|
||||||
|
fetchPlaylist: () => '/pcast/stream/playlist'
|
||||||
|
};
|
||||||
49
src/services/authentication.service.ts
Normal file
49
src/services/authentication.service.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import PlatformDetectionService from './platform-detection.service';
|
||||||
|
import {Strings, Subject, ReadOnlySubject} from '@techniker-me/tools';
|
||||||
|
import {PhenixWebSocket, PhenixWebSocketStatus, PhenixWebSocketMessage} from 'services/net/PhenixWebSocket';
|
||||||
|
import config from 'config';
|
||||||
|
|
||||||
|
class AuthenticationService {
|
||||||
|
private static readonly _instance: AuthenticationService = new AuthenticationService();
|
||||||
|
private readonly _phenixWebSocket: Subject<PhenixWebSocket | null> = new Subject(null);
|
||||||
|
private readonly _sessionId: string = Strings.random(10);
|
||||||
|
|
||||||
|
public static getInstance(): AuthenticationService {
|
||||||
|
return AuthenticationService._instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
get status(): ReadOnlySubject<PhenixWebSocketStatus> | undefined {
|
||||||
|
return this._phenixWebSocket.value?.status || undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
public setWebSocketUri(webSocketUri: string): void {
|
||||||
|
this._phenixWebSocket.value?.dispose();
|
||||||
|
this._phenixWebSocket.value = new PhenixWebSocket(webSocketUri);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async authenticateCredentials(applicationId: string, secret: string): Promise<{error: unknown; response: unknown}> {
|
||||||
|
const authenicate = {
|
||||||
|
applicationId,
|
||||||
|
secret,
|
||||||
|
clientVersion: config.controlCenterVersion,
|
||||||
|
deviceId: '',
|
||||||
|
platform: PlatformDetectionService.platform,
|
||||||
|
platformVersion: PlatformDetectionService.platformVersion,
|
||||||
|
sessionId: this._sessionId,
|
||||||
|
authenticationToken: secret
|
||||||
|
};
|
||||||
|
|
||||||
|
return this._phenixWebSocket.value?.sendMessage(PhenixWebSocketMessage.Authenticate, authenicate) as Promise<{error: unknown; response: unknown}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async signout(): Promise<{error: unknown; response: unknown}> {
|
||||||
|
const {error, response} = await (this._phenixWebSocket.value?.sendMessage(PhenixWebSocketMessage.Bye, {sessionId: this._sessionId}) as Promise<{
|
||||||
|
error: unknown;
|
||||||
|
response: unknown;
|
||||||
|
}>);
|
||||||
|
|
||||||
|
return {error, response};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AuthenticationService.getInstance();
|
||||||
26
src/services/logger/Appenders.ts
Normal file
26
src/services/logger/Appenders.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||||
|
*/
|
||||||
|
import {IAppender} from './IAppender';
|
||||||
|
|
||||||
|
export default class Appenders {
|
||||||
|
private _appenders: Array<IAppender> = [];
|
||||||
|
|
||||||
|
get value(): Array<IAppender> {
|
||||||
|
return this._appenders;
|
||||||
|
}
|
||||||
|
|
||||||
|
add(appender: IAppender): void {
|
||||||
|
this._appenders.push(appender);
|
||||||
|
}
|
||||||
|
|
||||||
|
remove(appender: IAppender): void {
|
||||||
|
this._appenders = this._appenders.reduce((items, item) => {
|
||||||
|
if (!(item === appender)) {
|
||||||
|
items.push(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
return items;
|
||||||
|
}, [] as Array<IAppender>);
|
||||||
|
}
|
||||||
|
}
|
||||||
29
src/services/logger/ConsoleAppender.ts
Normal file
29
src/services/logger/ConsoleAppender.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||||
|
*/
|
||||||
|
import {IAppender} from './IAppender';
|
||||||
|
import {LoggingLevel} from './Logger';
|
||||||
|
|
||||||
|
export default class ConsoleAppender implements IAppender {
|
||||||
|
private readonly _threshold: LoggingLevel;
|
||||||
|
|
||||||
|
log(logLevel: LoggingLevel, message: string, category: string, date: Date): void {
|
||||||
|
if (logLevel < this._threshold) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fullMessage = `${date.toISOString()} [${category}] [${LoggingLevel[logLevel]}] ${message}`;
|
||||||
|
|
||||||
|
if (logLevel < LoggingLevel.Warn) {
|
||||||
|
console.log(fullMessage);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error(fullMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(threshold: LoggingLevel) {
|
||||||
|
this._threshold = threshold;
|
||||||
|
}
|
||||||
|
}
|
||||||
8
src/services/logger/IAppender.ts
Normal file
8
src/services/logger/IAppender.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||||
|
*/
|
||||||
|
import {LoggingLevel} from './Logger';
|
||||||
|
|
||||||
|
export interface IAppender {
|
||||||
|
log: (logLevel: LoggingLevel, message: string, category: string, date: Date) => void;
|
||||||
|
}
|
||||||
197
src/services/logger/Logger.ts
Normal file
197
src/services/logger/Logger.ts
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||||
|
*/
|
||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
import {IAppender} from './IAppender';
|
||||||
|
import Appenders from './Appenders';
|
||||||
|
import LoggingThreshold from './LoggingThreshold';
|
||||||
|
|
||||||
|
export enum LoggingLevel {
|
||||||
|
All = -1,
|
||||||
|
Trace = 10,
|
||||||
|
Debug = 20,
|
||||||
|
Info = 30,
|
||||||
|
Warn = 40,
|
||||||
|
Error = 50,
|
||||||
|
Fatal = 60,
|
||||||
|
Off = 100
|
||||||
|
}
|
||||||
|
|
||||||
|
export type LoggingLevelType = 'Off' | 'Trace' | 'Debug' | 'Info' | 'Warn' | 'Error' | 'Fatal' | 'All';
|
||||||
|
|
||||||
|
export default class Logger {
|
||||||
|
private readonly _category: string;
|
||||||
|
private readonly _appenders: Appenders;
|
||||||
|
private readonly _threshold: LoggingThreshold;
|
||||||
|
|
||||||
|
get category(): string {
|
||||||
|
return this._category;
|
||||||
|
}
|
||||||
|
|
||||||
|
get appenders(): Appenders {
|
||||||
|
return this._appenders;
|
||||||
|
}
|
||||||
|
|
||||||
|
get threshold(): LoggingThreshold {
|
||||||
|
return this._threshold;
|
||||||
|
}
|
||||||
|
|
||||||
|
trace(...args: any): void {
|
||||||
|
if (!this._threshold.value || this._threshold.value > LoggingLevel.Trace) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.log(LoggingLevel.Trace, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
debug(...args: any): void {
|
||||||
|
if (!this._threshold.value || this._threshold.value > LoggingLevel.Debug) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.log(LoggingLevel.Debug, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
info(...args: any): void {
|
||||||
|
if (!this._threshold.value || this._threshold.value > LoggingLevel.Info) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.log(LoggingLevel.Info, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
warn(...args: any): void {
|
||||||
|
if (!this._threshold.value || this._threshold.value > LoggingLevel.Warn) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.log(LoggingLevel.Warn, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
error(...args: any): void {
|
||||||
|
if (!this._threshold.value || this._threshold.value > LoggingLevel.Error) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.log(LoggingLevel.Error, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
fatal(...args: any): void {
|
||||||
|
if (!this._threshold.value || this._threshold.value > LoggingLevel.Fatal) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.log(LoggingLevel.Fatal, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
private log(level: number, args: any): void {
|
||||||
|
const date = new Date();
|
||||||
|
const message = this.replacePlaceholders(args);
|
||||||
|
|
||||||
|
this._appenders.value.forEach((appender: IAppender) => {
|
||||||
|
appender.log(level, message, this.category, date);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private replacePlaceholders(args: any): string {
|
||||||
|
let replacePlaceholdersString = args[0];
|
||||||
|
let index = 0;
|
||||||
|
|
||||||
|
while (replacePlaceholdersString.indexOf && args.length > 1 && index >= 0) {
|
||||||
|
index = replacePlaceholdersString.indexOf('%', index);
|
||||||
|
|
||||||
|
if (index > 0) {
|
||||||
|
const type = replacePlaceholdersString.substring(index + 1, index + 2);
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case '%':
|
||||||
|
// Escaped '%%' turns into '%'
|
||||||
|
replacePlaceholdersString = replacePlaceholdersString.substring(0, index) + replacePlaceholdersString.substring(index + 1);
|
||||||
|
index++;
|
||||||
|
|
||||||
|
break;
|
||||||
|
case 's':
|
||||||
|
case 'd':
|
||||||
|
// Replace '%d' or '%s' with the argument
|
||||||
|
args[0] = replacePlaceholdersString = this.replaceArgument(this.toString(args[1]), replacePlaceholdersString, index);
|
||||||
|
args.splice(1, 1);
|
||||||
|
|
||||||
|
break;
|
||||||
|
case 'j':
|
||||||
|
// Replace %j' with the argument
|
||||||
|
args[0] = replacePlaceholdersString = this.replaceArgument(this.stringify(args[1]), replacePlaceholdersString, index);
|
||||||
|
|
||||||
|
args.splice(1, 1);
|
||||||
|
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return args.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.length > 1) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
args = args.reduce((accumulator: any, currentValue: any, index: number, array: any[]) => {
|
||||||
|
if (index + 1 === array.length && currentValue instanceof Error) {
|
||||||
|
return accumulator + '\n' + this.toString(currentValue.stack);
|
||||||
|
}
|
||||||
|
|
||||||
|
return accumulator + `[${this.toString(currentValue)}]`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return args.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private stringify(arg: any): string {
|
||||||
|
try {
|
||||||
|
return JSON.stringify(arg instanceof Error ? this.toString(arg) : arg, null, 2);
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
} catch (e) {
|
||||||
|
return '[object invalid JSON.stringify]';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private replaceArgument(argument: any, replacePlaceholdersString: string, index: number): string {
|
||||||
|
return replacePlaceholdersString.substring(0, index) + this.toString(argument) + replacePlaceholdersString.substring(index + 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
private toString(data: any): string {
|
||||||
|
if (typeof data === 'string') {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof data === 'boolean') {
|
||||||
|
return data ? 'true' : 'false';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof data === 'number') {
|
||||||
|
return data.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
let toStringStr = '';
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
if (typeof data === 'function') {
|
||||||
|
toStringStr = data.toString();
|
||||||
|
} else if (data instanceof Object) {
|
||||||
|
try {
|
||||||
|
toStringStr = data.toString();
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
} catch (e) {
|
||||||
|
toStringStr = '[object invalid toString()]';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return toStringStr;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(category: string, appenders: Appenders, threshold: LoggingThreshold) {
|
||||||
|
this._category = category;
|
||||||
|
this._appenders = appenders;
|
||||||
|
this._threshold = threshold;
|
||||||
|
}
|
||||||
|
}
|
||||||
51
src/services/logger/LoggerDefaults.ts
Normal file
51
src/services/logger/LoggerDefaults.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||||
|
*/
|
||||||
|
import {LoggingLevel, LoggingLevelType} from '../logger/Logger';
|
||||||
|
|
||||||
|
declare const __FEATURES__: {
|
||||||
|
sendLogs: LoggingLevelType;
|
||||||
|
logToConsole: LoggingLevelType;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class BuildFeatures {
|
||||||
|
private static _sendLogs: LoggingLevelType;
|
||||||
|
private static _logToConsole: LoggingLevelType;
|
||||||
|
|
||||||
|
static get sendLogs(): LoggingLevelType {
|
||||||
|
return this._sendLogs;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get logToConsole(): LoggingLevelType {
|
||||||
|
return this._logToConsole;
|
||||||
|
}
|
||||||
|
|
||||||
|
static applyFeatures(): void {
|
||||||
|
try {
|
||||||
|
const features = __FEATURES__;
|
||||||
|
|
||||||
|
this._sendLogs = 'sendLogs' in features ? features.sendLogs : 'All';
|
||||||
|
this._logToConsole = 'logToConsole' in features ? features.logToConsole : 'All';
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
} catch (e) {
|
||||||
|
this._sendLogs = 'All';
|
||||||
|
this._logToConsole = 'All';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BuildFeatures.applyFeatures();
|
||||||
|
|
||||||
|
export default class LoggerDefaults {
|
||||||
|
static get defaultLoggingLevel(): LoggingLevel {
|
||||||
|
return LoggingLevel[BuildFeatures.sendLogs];
|
||||||
|
}
|
||||||
|
|
||||||
|
static get defaultConsoleLoggingLevel(): LoggingLevel {
|
||||||
|
return LoggingLevel[BuildFeatures.logToConsole];
|
||||||
|
}
|
||||||
|
|
||||||
|
static get defaultTelemetryLoggingLevel(): LoggingLevel {
|
||||||
|
return LoggingLevel.Info;
|
||||||
|
}
|
||||||
|
}
|
||||||
86
src/services/logger/LoggerFactory.ts
Normal file
86
src/services/logger/LoggerFactory.ts
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||||
|
*/
|
||||||
|
import PlatformDetectionService from 'services/platform-detection.service';
|
||||||
|
import hostService from 'services/url.service';
|
||||||
|
import TelemetryConfiguration from 'services/telemetry/TelemetryConfiguration';
|
||||||
|
import TelemetryAppender from 'services/telemetry/TelemetryApender';
|
||||||
|
// import userStore from 'services/user-store';
|
||||||
|
import {ILogger} from './LoggerInterface';
|
||||||
|
import Logger, {LoggingLevel} from './Logger';
|
||||||
|
import Appenders from './Appenders';
|
||||||
|
import LoggingThreshold from './LoggingThreshold';
|
||||||
|
import ConsoleAppender from './ConsoleAppender';
|
||||||
|
import LoggerDefaults from './LoggerDefaults';
|
||||||
|
|
||||||
|
export default class LoggerFactory {
|
||||||
|
private static _loggers: {[category: string]: ILogger} = {};
|
||||||
|
private static _appenders: Appenders = new Appenders();
|
||||||
|
private static _threshold: LoggingThreshold = new LoggingThreshold();
|
||||||
|
private static _telemetryConfiguration: TelemetryConfiguration = new TelemetryConfiguration();
|
||||||
|
private static _initialized: boolean = false;
|
||||||
|
|
||||||
|
static get telemetryConfiguration(): TelemetryConfiguration {
|
||||||
|
return this._telemetryConfiguration;
|
||||||
|
}
|
||||||
|
|
||||||
|
static applyLoggerConfig(): void {
|
||||||
|
if (this._initialized) return;
|
||||||
|
|
||||||
|
LoggerFactory.applyConsoleLogger(LoggingLevel['All']);
|
||||||
|
LoggerFactory.applyLoggingLevel();
|
||||||
|
LoggerFactory.applyTelemetryLogger();
|
||||||
|
this._initialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static getLogger(category: string): ILogger {
|
||||||
|
if (typeof category !== 'string') {
|
||||||
|
category = 'portal';
|
||||||
|
}
|
||||||
|
|
||||||
|
const logger = LoggerFactory._loggers[category];
|
||||||
|
|
||||||
|
if (logger) {
|
||||||
|
return logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (LoggerFactory._loggers[category] = new Logger(category, this._appenders, this._threshold));
|
||||||
|
}
|
||||||
|
|
||||||
|
static applyLoggingLevel(): void {
|
||||||
|
this._threshold.setThreshold(LoggingLevel['All']);
|
||||||
|
}
|
||||||
|
|
||||||
|
static applyConsoleLogger(level: LoggingLevel): void {
|
||||||
|
this._appenders.add(new ConsoleAppender(level || LoggerDefaults.defaultConsoleLoggingLevel));
|
||||||
|
}
|
||||||
|
|
||||||
|
static async applyTelemetryConfiguration(level: LoggingLevel): Promise<void> {
|
||||||
|
this._telemetryConfiguration.threshold = level || LoggerDefaults.defaultTelemetryLoggingLevel;
|
||||||
|
this._telemetryConfiguration.url = hostService.getTelemetryUrl();
|
||||||
|
this._telemetryConfiguration.environment = hostService.getBaseUrlOrigin();
|
||||||
|
// this._telemetryConfiguration.tenancy = await userStore.get('applicationId');
|
||||||
|
// this._telemetryConfiguration.userId = await userStore.get('applicationId');
|
||||||
|
// this._telemetryConfiguration.sessionId = await userStore.get('sessionId');
|
||||||
|
|
||||||
|
// Lazy access to PlatformDetectionService to avoid circular dependency
|
||||||
|
try {
|
||||||
|
this._telemetryConfiguration.browser = PlatformDetectionService.browser
|
||||||
|
? `${PlatformDetectionService.browser}/${PlatformDetectionService.version}`
|
||||||
|
: 'unknown';
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to get platform detection info for telemetry:', error);
|
||||||
|
this._telemetryConfiguration.browser = 'unknown';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static applyTelemetryLogger(): void {
|
||||||
|
LoggerFactory.applyTelemetryConfiguration(LoggingLevel['Info']);
|
||||||
|
|
||||||
|
this._appenders.add(new TelemetryAppender(this._telemetryConfiguration));
|
||||||
|
}
|
||||||
|
|
||||||
|
private constructor() {
|
||||||
|
throw new Error('LoggerFactory is a static class that may not be instantiated');
|
||||||
|
}
|
||||||
|
}
|
||||||
27
src/services/logger/LoggerInterface.ts
Normal file
27
src/services/logger/LoggerInterface.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||||
|
*/
|
||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
import Appenders from './Appenders';
|
||||||
|
import LoggingThreshold from './LoggingThreshold';
|
||||||
|
|
||||||
|
export interface ILogger {
|
||||||
|
readonly category: string;
|
||||||
|
|
||||||
|
readonly appenders: Appenders;
|
||||||
|
|
||||||
|
readonly threshold: LoggingThreshold;
|
||||||
|
|
||||||
|
trace: (...args: any) => void;
|
||||||
|
|
||||||
|
debug: (...args: any) => void;
|
||||||
|
|
||||||
|
info: (...args: any) => void;
|
||||||
|
|
||||||
|
warn: (...args: any) => void;
|
||||||
|
|
||||||
|
error: (...args: any) => void;
|
||||||
|
|
||||||
|
fatal: (...args: any) => void;
|
||||||
|
}
|
||||||
|
/* eslint-enable */
|
||||||
56
src/services/logger/LoggingLevelMapping.ts
Normal file
56
src/services/logger/LoggingLevelMapping.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||||
|
*/
|
||||||
|
import {LoggingLevel, LoggingLevelType} from './Logger';
|
||||||
|
|
||||||
|
function assertUnreachable(x: never): never {
|
||||||
|
throw new Error(`Unexpected value [${x}]. This should never be reached`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class LoggingLevelMapping {
|
||||||
|
static convertLoggingLevelToLoggingLevelType(loggingLevel: LoggingLevel): LoggingLevelType {
|
||||||
|
switch (loggingLevel) {
|
||||||
|
case LoggingLevel.Off:
|
||||||
|
return 'Off';
|
||||||
|
case LoggingLevel.Trace:
|
||||||
|
return 'Trace';
|
||||||
|
case LoggingLevel.Debug:
|
||||||
|
return 'Debug';
|
||||||
|
case LoggingLevel.Info:
|
||||||
|
return 'Trace';
|
||||||
|
case LoggingLevel.Warn:
|
||||||
|
return 'Warn';
|
||||||
|
case LoggingLevel.Error:
|
||||||
|
return 'Error';
|
||||||
|
case LoggingLevel.Fatal:
|
||||||
|
return 'Fatal';
|
||||||
|
case LoggingLevel.All:
|
||||||
|
return 'All';
|
||||||
|
default:
|
||||||
|
assertUnreachable(loggingLevel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static convertLoggingLevelTypeToLoggingLevel(loggingLevelType: LoggingLevelType): LoggingLevel {
|
||||||
|
switch (loggingLevelType) {
|
||||||
|
case 'Off':
|
||||||
|
return LoggingLevel.Off;
|
||||||
|
case 'Trace':
|
||||||
|
return LoggingLevel.Trace;
|
||||||
|
case 'Debug':
|
||||||
|
return LoggingLevel.Debug;
|
||||||
|
case 'Info':
|
||||||
|
return LoggingLevel.Info;
|
||||||
|
case 'Warn':
|
||||||
|
return LoggingLevel.Warn;
|
||||||
|
case 'Error':
|
||||||
|
return LoggingLevel.Error;
|
||||||
|
case 'Fatal':
|
||||||
|
return LoggingLevel.Fatal;
|
||||||
|
case 'All':
|
||||||
|
return LoggingLevel.All;
|
||||||
|
default:
|
||||||
|
assertUnreachable(loggingLevelType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
17
src/services/logger/LoggingThreshold.ts
Normal file
17
src/services/logger/LoggingThreshold.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||||
|
*/
|
||||||
|
import LoggerDefaults from './LoggerDefaults';
|
||||||
|
import {LoggingLevel} from './Logger';
|
||||||
|
|
||||||
|
export default class LoggingThreshold {
|
||||||
|
private _threshold: LoggingLevel = LoggerDefaults.defaultLoggingLevel;
|
||||||
|
|
||||||
|
get value(): LoggingLevel {
|
||||||
|
return this._threshold;
|
||||||
|
}
|
||||||
|
|
||||||
|
setThreshold(threshold: LoggingLevel): void {
|
||||||
|
this._threshold = threshold;
|
||||||
|
}
|
||||||
|
}
|
||||||
391
src/services/messaging/analytics.proto.json
Normal file
391
src/services/messaging/analytics.proto.json
Normal file
@@ -0,0 +1,391 @@
|
|||||||
|
{
|
||||||
|
"package": "analytics",
|
||||||
|
"messages": [
|
||||||
|
{
|
||||||
|
"name": "Usage",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"rule": "required",
|
||||||
|
"type": "uint64",
|
||||||
|
"name": "streams",
|
||||||
|
"id": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "required",
|
||||||
|
"type": "uint64",
|
||||||
|
"name": "users",
|
||||||
|
"id": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "required",
|
||||||
|
"type": "uint64",
|
||||||
|
"name": "devices",
|
||||||
|
"id": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "required",
|
||||||
|
"type": "uint64",
|
||||||
|
"name": "minutes",
|
||||||
|
"id": 4
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "UsageByType",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"rule": "required",
|
||||||
|
"type": "string",
|
||||||
|
"name": "type",
|
||||||
|
"id": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "required",
|
||||||
|
"type": "string",
|
||||||
|
"name": "subtype",
|
||||||
|
"id": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "required",
|
||||||
|
"type": "Usage",
|
||||||
|
"name": "usage",
|
||||||
|
"id": 3
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "UsageByCountry",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"rule": "required",
|
||||||
|
"type": "string",
|
||||||
|
"name": "continent",
|
||||||
|
"id": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "required",
|
||||||
|
"type": "string",
|
||||||
|
"name": "country",
|
||||||
|
"id": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "repeated",
|
||||||
|
"type": "UsageByType",
|
||||||
|
"name": "usageByType",
|
||||||
|
"id": 3
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "GetGeographicUsage",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"rule": "repeated",
|
||||||
|
"type": "string",
|
||||||
|
"name": "applicationIds",
|
||||||
|
"id": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "required",
|
||||||
|
"type": "uint64",
|
||||||
|
"name": "start",
|
||||||
|
"id": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "required",
|
||||||
|
"type": "uint64",
|
||||||
|
"name": "end",
|
||||||
|
"id": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "optional",
|
||||||
|
"type": "string",
|
||||||
|
"name": "continuationId",
|
||||||
|
"id": 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "optional",
|
||||||
|
"type": "string",
|
||||||
|
"name": "routeKey",
|
||||||
|
"id": 5
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "GetGeographicUsageResponse",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"rule": "required",
|
||||||
|
"type": "string",
|
||||||
|
"name": "status",
|
||||||
|
"id": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "optional",
|
||||||
|
"type": "Usage",
|
||||||
|
"name": "usage",
|
||||||
|
"id": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "repeated",
|
||||||
|
"type": "UsageByType",
|
||||||
|
"name": "usageByType",
|
||||||
|
"id": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "repeated",
|
||||||
|
"type": "UsageByCountry",
|
||||||
|
"name": "usageByCountry",
|
||||||
|
"id": 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "optional",
|
||||||
|
"type": "string",
|
||||||
|
"name": "continuationId",
|
||||||
|
"id": 5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "optional",
|
||||||
|
"type": "string",
|
||||||
|
"name": "routeKey",
|
||||||
|
"id": 6
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "CDF",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"rule": "repeated",
|
||||||
|
"type": "double",
|
||||||
|
"name": "data",
|
||||||
|
"id": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "GetTimeToFirstFrameCDF",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"rule": "repeated",
|
||||||
|
"type": "string",
|
||||||
|
"name": "applicationIds",
|
||||||
|
"id": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "required",
|
||||||
|
"type": "uint64",
|
||||||
|
"name": "start",
|
||||||
|
"id": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "required",
|
||||||
|
"type": "uint64",
|
||||||
|
"name": "end",
|
||||||
|
"id": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "required",
|
||||||
|
"type": "Kind",
|
||||||
|
"name": "kind",
|
||||||
|
"id": 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "optional",
|
||||||
|
"type": "string",
|
||||||
|
"name": "continuationId",
|
||||||
|
"id": 5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "optional",
|
||||||
|
"type": "string",
|
||||||
|
"name": "routeKey",
|
||||||
|
"id": 6
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"enums": [
|
||||||
|
{
|
||||||
|
"name": "Kind",
|
||||||
|
"values": [
|
||||||
|
{
|
||||||
|
"name": "All",
|
||||||
|
"id": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "RealTime",
|
||||||
|
"id": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Live",
|
||||||
|
"id": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Dash",
|
||||||
|
"id": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Hls",
|
||||||
|
"id": 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "PeerAssisted",
|
||||||
|
"id": 5
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "GetTimeToFirstFrameCDFResponse",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"rule": "required",
|
||||||
|
"type": "string",
|
||||||
|
"name": "status",
|
||||||
|
"id": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "optional",
|
||||||
|
"type": "uint64",
|
||||||
|
"name": "count",
|
||||||
|
"id": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "optional",
|
||||||
|
"type": "double",
|
||||||
|
"name": "average",
|
||||||
|
"id": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "optional",
|
||||||
|
"type": "CDF",
|
||||||
|
"name": "cdf",
|
||||||
|
"id": 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "optional",
|
||||||
|
"type": "string",
|
||||||
|
"name": "continuationId",
|
||||||
|
"id": 5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "optional",
|
||||||
|
"type": "string",
|
||||||
|
"name": "routeKey",
|
||||||
|
"id": 6
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "GetActiveUsers",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"rule": "repeated",
|
||||||
|
"type": "string",
|
||||||
|
"name": "applicationIds",
|
||||||
|
"id": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "required",
|
||||||
|
"type": "uint64",
|
||||||
|
"name": "snapshotTime",
|
||||||
|
"id": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "optional",
|
||||||
|
"type": "string",
|
||||||
|
"name": "continuationId",
|
||||||
|
"id": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "optional",
|
||||||
|
"type": "string",
|
||||||
|
"name": "routeKey",
|
||||||
|
"id": 4
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "UsersAndSessionsGrouped",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"rule": "required",
|
||||||
|
"type": "string",
|
||||||
|
"name": "groupName",
|
||||||
|
"id": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "required",
|
||||||
|
"type": "uint64",
|
||||||
|
"name": "users",
|
||||||
|
"id": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "required",
|
||||||
|
"type": "uint64",
|
||||||
|
"name": "sessions",
|
||||||
|
"id": 3
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "GetActiveUsersResponse",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"rule": "required",
|
||||||
|
"type": "string",
|
||||||
|
"name": "status",
|
||||||
|
"id": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "optional",
|
||||||
|
"type": "uint64",
|
||||||
|
"name": "users",
|
||||||
|
"id": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "optional",
|
||||||
|
"type": "uint64",
|
||||||
|
"name": "sessions",
|
||||||
|
"id": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "repeated",
|
||||||
|
"type": "UsersAndSessionsGrouped",
|
||||||
|
"name": "byPlatform",
|
||||||
|
"id": 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "repeated",
|
||||||
|
"type": "UsersAndSessionsGrouped",
|
||||||
|
"name": "byManufacturer",
|
||||||
|
"id": 5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "repeated",
|
||||||
|
"type": "UsersAndSessionsGrouped",
|
||||||
|
"name": "byCity",
|
||||||
|
"id": 6
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "repeated",
|
||||||
|
"type": "UsersAndSessionsGrouped",
|
||||||
|
"name": "byCountry",
|
||||||
|
"id": 7
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "optional",
|
||||||
|
"type": "string",
|
||||||
|
"name": "continuationId",
|
||||||
|
"id": 8
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "optional",
|
||||||
|
"type": "string",
|
||||||
|
"name": "routeKey",
|
||||||
|
"id": 9
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
1777
src/services/messaging/pcast.proto.json
Normal file
1777
src/services/messaging/pcast.proto.json
Normal file
File diff suppressed because it is too large
Load Diff
140
src/services/net/PhenixWebSocket.ts
Normal file
140
src/services/net/PhenixWebSocket.ts
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
import {MQWebSocket} from 'phenix-web-proto';
|
||||||
|
import {ILogger} from 'services/logger/LoggerInterface';
|
||||||
|
import LoggerFactory from 'services/logger/LoggerFactory';
|
||||||
|
import assertUnreachable from 'lang/assertUnreachable';
|
||||||
|
import {Subject, ReadOnlySubject, IDisposable} from '@techniker-me/tools';
|
||||||
|
import pcastProtocol from 'services/messaging/pcast.proto.json';
|
||||||
|
import analyticsProtocol from 'services/messaging/analytics.proto.json';
|
||||||
|
|
||||||
|
export enum PhenixWebSocketStatus {
|
||||||
|
Offline = 0,
|
||||||
|
Connecting = 1,
|
||||||
|
Online = 2,
|
||||||
|
Reconnecting = 3,
|
||||||
|
Error = 4
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PhenixWebSocketStatusType = 'Offline' | 'Connecting' | 'Online' | 'Reconnecting' | 'Error';
|
||||||
|
|
||||||
|
export class PhenixWebSocketStatusMapping {
|
||||||
|
public static convertPhenixWebSocketStatusToPhenixWebSocketStatusType(status: PhenixWebSocketStatus): PhenixWebSocketStatusType {
|
||||||
|
switch (status) {
|
||||||
|
case PhenixWebSocketStatus.Error:
|
||||||
|
return 'Error';
|
||||||
|
case PhenixWebSocketStatus.Offline:
|
||||||
|
return 'Offline';
|
||||||
|
case PhenixWebSocketStatus.Connecting:
|
||||||
|
return 'Connecting';
|
||||||
|
case PhenixWebSocketStatus.Online:
|
||||||
|
return 'Online';
|
||||||
|
case PhenixWebSocketStatus.Reconnecting:
|
||||||
|
return 'Reconnecting';
|
||||||
|
default:
|
||||||
|
assertUnreachable(status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static convertPhenixWebSocketStatusTypeToPhenixWebSocketStatus(statusType: PhenixWebSocketStatusType): PhenixWebSocketStatus {
|
||||||
|
switch (statusType) {
|
||||||
|
case 'Error':
|
||||||
|
return PhenixWebSocketStatus.Error;
|
||||||
|
case 'Offline':
|
||||||
|
return PhenixWebSocketStatus.Offline;
|
||||||
|
case 'Connecting':
|
||||||
|
return PhenixWebSocketStatus.Connecting;
|
||||||
|
case 'Online':
|
||||||
|
return PhenixWebSocketStatus.Online;
|
||||||
|
case 'Reconnecting':
|
||||||
|
return PhenixWebSocketStatus.Reconnecting;
|
||||||
|
default:
|
||||||
|
assertUnreachable(statusType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum PhenixWebSocketMessage {
|
||||||
|
Authenticate = 'pcast.Authenticate',
|
||||||
|
Bye = 'pcast.Bye'
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PhenixWebSocket implements IDisposable {
|
||||||
|
private readonly _logger: ILogger = LoggerFactory.getLogger('PhenixWebSocket');
|
||||||
|
private readonly _status: Subject<PhenixWebSocketStatus> = new Subject(PhenixWebSocketStatus.Offline);
|
||||||
|
private readonly _readOnlyStatus: ReadOnlySubject<PhenixWebSocketStatus> = new ReadOnlySubject(this._status);
|
||||||
|
private readonly _websocketMQ: MQWebSocket;
|
||||||
|
|
||||||
|
constructor(uri: string) {
|
||||||
|
console.log('PhenixWebSocket constructor', uri);
|
||||||
|
this._websocketMQ = new MQWebSocket(uri, this._logger, [pcastProtocol, analyticsProtocol]);
|
||||||
|
this.initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
get status(): ReadOnlySubject<PhenixWebSocketStatus> {
|
||||||
|
return this._readOnlyStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
public isConnected(): boolean {
|
||||||
|
return this._status.value === PhenixWebSocketStatus.Online;
|
||||||
|
}
|
||||||
|
|
||||||
|
public isConnecting(): boolean {
|
||||||
|
return this._status.value === PhenixWebSocketStatus.Connecting;
|
||||||
|
}
|
||||||
|
|
||||||
|
public isReconnecting(): boolean {
|
||||||
|
return this._status.value === PhenixWebSocketStatus.Reconnecting;
|
||||||
|
}
|
||||||
|
|
||||||
|
public sendMessage<T>(message: PhenixWebSocketMessage, data: T): Promise<unknown> {
|
||||||
|
if (!Object.values(PhenixWebSocketMessage).includes(message)) {
|
||||||
|
throw new Error(`Invalid message: ${message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if WebSocket is in a valid state for sending messages
|
||||||
|
const currentStatus = this._status.value;
|
||||||
|
if (currentStatus !== PhenixWebSocketStatus.Online) {
|
||||||
|
return Promise.reject(new Error(`WebSocket is not ready. Current status: ${PhenixWebSocketStatus[currentStatus]}`));
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
return this._websocketMQ.sendRequest(message, data, (error: unknown, response: unknown) => {
|
||||||
|
if (error) {
|
||||||
|
reject(error);
|
||||||
|
} else {
|
||||||
|
resolve({error, response});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public dispose(): void {
|
||||||
|
this._websocketMQ.disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
private initialize(): void {
|
||||||
|
this._websocketMQ.onEvent('connecting', () => {
|
||||||
|
this._logger.info('WebSocket connecting...');
|
||||||
|
this._status.value = PhenixWebSocketStatus.Connecting;
|
||||||
|
});
|
||||||
|
|
||||||
|
this._websocketMQ.onEvent('connected', () => {
|
||||||
|
this._logger.info('WebSocket connected successfully');
|
||||||
|
this._status.value = PhenixWebSocketStatus.Online;
|
||||||
|
});
|
||||||
|
|
||||||
|
this._websocketMQ.onEvent('disconnected', () => {
|
||||||
|
this._logger.info('WebSocket disconnected');
|
||||||
|
this._status.value = PhenixWebSocketStatus.Offline;
|
||||||
|
});
|
||||||
|
|
||||||
|
this._websocketMQ.onEvent('reconnecting', () => {
|
||||||
|
this._logger.info('WebSocket reconnecting...');
|
||||||
|
this._status.value = PhenixWebSocketStatus.Reconnecting;
|
||||||
|
});
|
||||||
|
|
||||||
|
this._websocketMQ.onEvent('error', (error: unknown) => {
|
||||||
|
this._logger.error('WebSocket error occurred:', error);
|
||||||
|
this._status.value = PhenixWebSocketStatus.Error;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
165
src/services/platform-detection.service.ts
Normal file
165
src/services/platform-detection.service.ts
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
export default class PlatformDetectionService {
|
||||||
|
private static _userAgent: string = navigator.userAgent;
|
||||||
|
private static _areClientHintsSupported: boolean = 'userAgentData' in navigator;
|
||||||
|
private static _platform: string = '?';
|
||||||
|
private static _platformVersion: string = '?';
|
||||||
|
private static _browser: string = 'Unknown';
|
||||||
|
private static _version: string | number = '?';
|
||||||
|
private static _isWebview: boolean = false;
|
||||||
|
private static _initialized: boolean = false;
|
||||||
|
|
||||||
|
private constructor() {
|
||||||
|
throw new Error('PlatformDetectionService is a static class that may not be instantiated');
|
||||||
|
}
|
||||||
|
|
||||||
|
static get platform(): string {
|
||||||
|
this.initializeIfNeeded();
|
||||||
|
return this._platform;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get platformVersion(): string {
|
||||||
|
this.initializeIfNeeded();
|
||||||
|
return this._platformVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get userAgent(): string {
|
||||||
|
return this._userAgent;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get browser(): string {
|
||||||
|
this.initializeIfNeeded();
|
||||||
|
return this._browser;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get version(): string | number {
|
||||||
|
this.initializeIfNeeded();
|
||||||
|
return this._version;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get isWebview(): boolean {
|
||||||
|
this.initializeIfNeeded();
|
||||||
|
return this._isWebview;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get areClientHintsSupported(): boolean {
|
||||||
|
return this._areClientHintsSupported;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static initializeIfNeeded(): void {
|
||||||
|
if (this._initialized) return;
|
||||||
|
this.initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static initialize(): void {
|
||||||
|
try {
|
||||||
|
const browserVersionMatch = this._userAgent.match(/(Chrome|Chromium|Firefox|Opera|Safari|Edge|OPR)\/([0-9]+)/);
|
||||||
|
|
||||||
|
if (browserVersionMatch) {
|
||||||
|
const [, browser, version] = browserVersionMatch;
|
||||||
|
PlatformDetectionService._browser = browser === 'OPR' ? 'Opera' : browser;
|
||||||
|
PlatformDetectionService._version = parseInt(version, 10)?.toString() || '?';
|
||||||
|
} else if (this._userAgent.match(/^\(?Mozilla/)) {
|
||||||
|
PlatformDetectionService._browser = 'Mozilla';
|
||||||
|
|
||||||
|
// Check for IE/Edge
|
||||||
|
if (this._userAgent.match(/MSIE/) || this._userAgent.match(/; Trident\/.*rv:[0-9]+/)) {
|
||||||
|
PlatformDetectionService._browser = 'IE';
|
||||||
|
const ieVersionMatch = this._userAgent.match(/rv:([0-9]+)/);
|
||||||
|
|
||||||
|
if (ieVersionMatch) {
|
||||||
|
PlatformDetectionService._version = parseInt(ieVersionMatch[1], 10)?.toString() || '?';
|
||||||
|
}
|
||||||
|
} else if (this._userAgent.match(/Edge\//)) {
|
||||||
|
PlatformDetectionService._browser = 'Edge';
|
||||||
|
const edgeVersionMatch = this._userAgent.match(/Edge\/([0-9]+)/);
|
||||||
|
|
||||||
|
if (edgeVersionMatch) {
|
||||||
|
PlatformDetectionService._version = parseInt(edgeVersionMatch[1], 10)?.toString() || '?';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle Opera masquerading as other browsers
|
||||||
|
if (this._userAgent.match(/OPR\//)) {
|
||||||
|
PlatformDetectionService._browser = 'Opera';
|
||||||
|
const operaVersionMatch = this._userAgent.match(/OPR\/([0-9]+)/);
|
||||||
|
if (operaVersionMatch) {
|
||||||
|
PlatformDetectionService._version = parseInt(operaVersionMatch[1], 10)?.toString() || '?';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Safari and iOS webviews
|
||||||
|
if (this._userAgent.match(/AppleWebKit/i)) {
|
||||||
|
if (this._userAgent.match(/iphone|ipod|ipad/i)) {
|
||||||
|
PlatformDetectionService._browser = 'Safari';
|
||||||
|
PlatformDetectionService._isWebview = true;
|
||||||
|
const iosVersionMatch = this._userAgent.match(/OS\s([0-9]+)/);
|
||||||
|
if (iosVersionMatch) {
|
||||||
|
PlatformDetectionService._version = parseInt(iosVersionMatch[1], 10)?.toString() || '?';
|
||||||
|
}
|
||||||
|
} else if (this._userAgent.match(/Safari\//) && !this._userAgent.match(/Chrome/)) {
|
||||||
|
PlatformDetectionService._browser = 'Safari';
|
||||||
|
const safariVersionMatch = this._userAgent.match(/Version\/([0-9]+)/);
|
||||||
|
if (safariVersionMatch) {
|
||||||
|
PlatformDetectionService._version = parseInt(safariVersionMatch[1], 10)?.toString() || '?';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Android webviews
|
||||||
|
if (this._userAgent.match(/; wv/) || (this._userAgent.match(/Android/) && this._userAgent.match(/Version\/[0-9].[0-9]/))) {
|
||||||
|
PlatformDetectionService._isWebview = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// React Native
|
||||||
|
if (globalThis.navigator.product === 'ReactNative') {
|
||||||
|
PlatformDetectionService._browser = 'ReactNative';
|
||||||
|
PlatformDetectionService._version = navigator.productSub || '?';
|
||||||
|
}
|
||||||
|
|
||||||
|
// platform information
|
||||||
|
if (this._userAgent.match(/Windows/)) {
|
||||||
|
PlatformDetectionService._platform = 'Windows';
|
||||||
|
const windowsVersionMatch = this._userAgent.match(/Windows NT ([0-9.]+)/);
|
||||||
|
|
||||||
|
if (windowsVersionMatch) {
|
||||||
|
PlatformDetectionService._platformVersion = windowsVersionMatch[1];
|
||||||
|
}
|
||||||
|
} else if (this._userAgent.match(/Mac OS X/)) {
|
||||||
|
PlatformDetectionService._platform = 'macOS';
|
||||||
|
const macVersionMatch = this._userAgent.match(/Mac OS X ([0-9._]+)/);
|
||||||
|
|
||||||
|
if (macVersionMatch) {
|
||||||
|
PlatformDetectionService._platformVersion = macVersionMatch[1].replace(/_/g, '.');
|
||||||
|
}
|
||||||
|
} else if (this._userAgent.match(/Linux/)) {
|
||||||
|
PlatformDetectionService._platform = 'Linux';
|
||||||
|
} else if (this._userAgent.match(/Android/)) {
|
||||||
|
PlatformDetectionService._platform = 'Android';
|
||||||
|
const androidVersionMatch = this._userAgent.match(/Android ([0-9.]+)/);
|
||||||
|
|
||||||
|
if (androidVersionMatch) {
|
||||||
|
PlatformDetectionService._platformVersion = androidVersionMatch[1];
|
||||||
|
}
|
||||||
|
} else if (this._userAgent.match(/iPhone|iPad|iPod/)) {
|
||||||
|
PlatformDetectionService._platform = 'iOS';
|
||||||
|
const iosVersionMatch = this._userAgent.match(/OS ([0-9_]+)/);
|
||||||
|
|
||||||
|
if (iosVersionMatch) {
|
||||||
|
PlatformDetectionService._platformVersion = iosVersionMatch[1].replace(/_/g, '.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this._initialized = true;
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to initialize PlatformDetectionService:', error);
|
||||||
|
// fallback values
|
||||||
|
this._browser = 'Unknown';
|
||||||
|
this._version = '?';
|
||||||
|
this._platform = '?';
|
||||||
|
this._platformVersion = '?';
|
||||||
|
this._isWebview = false;
|
||||||
|
this._initialized = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
25
src/services/telemetry/TelemetryApender.ts
Normal file
25
src/services/telemetry/TelemetryApender.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||||
|
*/
|
||||||
|
import TelemetryService from './TelemetryService';
|
||||||
|
import TelemetryConfiguration from './TelemetryConfiguration';
|
||||||
|
import {IAppender} from '../logger/IAppender';
|
||||||
|
import {LoggingLevel} from '../logger/Logger';
|
||||||
|
|
||||||
|
export default class TelemetryAppender implements IAppender {
|
||||||
|
private readonly _telemetryService: TelemetryService;
|
||||||
|
private readonly _threshold: LoggingLevel;
|
||||||
|
|
||||||
|
constructor(telemetryConfiguration: TelemetryConfiguration) {
|
||||||
|
this._threshold = telemetryConfiguration.threshold;
|
||||||
|
this._telemetryService = new TelemetryService(telemetryConfiguration);
|
||||||
|
}
|
||||||
|
|
||||||
|
async log(logLevel: LoggingLevel, message: string, category: string, date: Date): Promise<void> {
|
||||||
|
if (logLevel < this._threshold) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._telemetryService.push(logLevel, message, category, date);
|
||||||
|
}
|
||||||
|
}
|
||||||
75
src/services/telemetry/TelemetryConfiguration.ts
Normal file
75
src/services/telemetry/TelemetryConfiguration.ts
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||||
|
*/
|
||||||
|
import {LoggingLevel} from '../logger/Logger';
|
||||||
|
import LoggerDefaults from '../logger/LoggerDefaults';
|
||||||
|
|
||||||
|
export default class TelemetryConfiguration {
|
||||||
|
private _url = 'https://telemetry.phenixrts.com/telemetry/logs';
|
||||||
|
private _tenancy = '';
|
||||||
|
private _userId = '';
|
||||||
|
private _sessionId = '';
|
||||||
|
private _environment = '';
|
||||||
|
private _threshold = LoggerDefaults.defaultTelemetryLoggingLevel;
|
||||||
|
private _browser = '';
|
||||||
|
|
||||||
|
get url(): string {
|
||||||
|
return this._url;
|
||||||
|
}
|
||||||
|
|
||||||
|
set url(url: string) {
|
||||||
|
const telemetryUrl = new URL(url);
|
||||||
|
|
||||||
|
telemetryUrl.pathname = telemetryUrl.pathname + '/logs';
|
||||||
|
|
||||||
|
this._url = telemetryUrl.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
get environment(): string {
|
||||||
|
return this._environment;
|
||||||
|
}
|
||||||
|
|
||||||
|
set environment(environment: string) {
|
||||||
|
this._environment = environment;
|
||||||
|
}
|
||||||
|
|
||||||
|
get browser(): string {
|
||||||
|
return this._browser;
|
||||||
|
}
|
||||||
|
|
||||||
|
set browser(browser: string) {
|
||||||
|
this._browser = browser;
|
||||||
|
}
|
||||||
|
|
||||||
|
get tenancy(): string {
|
||||||
|
return this._tenancy;
|
||||||
|
}
|
||||||
|
|
||||||
|
set tenancy(tenancy: string) {
|
||||||
|
this._tenancy = tenancy;
|
||||||
|
}
|
||||||
|
|
||||||
|
get userId(): string {
|
||||||
|
return this._userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
set userId(userId: string) {
|
||||||
|
this._userId = userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
get sessionId(): string {
|
||||||
|
return this._sessionId;
|
||||||
|
}
|
||||||
|
|
||||||
|
set sessionId(sessionId: string) {
|
||||||
|
this._sessionId = sessionId;
|
||||||
|
}
|
||||||
|
|
||||||
|
get threshold(): LoggingLevel {
|
||||||
|
return this._threshold;
|
||||||
|
}
|
||||||
|
|
||||||
|
set threshold(threshold: LoggingLevel) {
|
||||||
|
this._threshold = threshold;
|
||||||
|
}
|
||||||
|
}
|
||||||
137
src/services/telemetry/TelemetryService.ts
Normal file
137
src/services/telemetry/TelemetryService.ts
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||||
|
*/
|
||||||
|
import config from 'config';
|
||||||
|
import {LoggingLevel} from '../logger/Logger';
|
||||||
|
import TelemetryConfiguration from './TelemetryConfiguration';
|
||||||
|
|
||||||
|
// Extend Window interface to include custom properties
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
__phenixPageLoadTime?: number;
|
||||||
|
__pageLoadTime?: number;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestSizeLimit = 8192;
|
||||||
|
const pageLoadTime = window.__phenixPageLoadTime || window.__pageLoadTime || Date.now();
|
||||||
|
|
||||||
|
interface ILogItem {
|
||||||
|
timestamp: string;
|
||||||
|
tenancy: string;
|
||||||
|
level: string;
|
||||||
|
category: string;
|
||||||
|
message: string;
|
||||||
|
sessionId: string;
|
||||||
|
userId: string;
|
||||||
|
version: string;
|
||||||
|
environment: string;
|
||||||
|
fullQualifiedName: string;
|
||||||
|
source: string;
|
||||||
|
runtime: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class TelemetryService {
|
||||||
|
private readonly _telemetryConfiguration: TelemetryConfiguration;
|
||||||
|
|
||||||
|
private _logs: Array<ILogItem> = [];
|
||||||
|
private _isSending: boolean = false;
|
||||||
|
private _domain = location.hostname;
|
||||||
|
|
||||||
|
constructor(telemetryConfiguration: TelemetryConfiguration) {
|
||||||
|
this._telemetryConfiguration = telemetryConfiguration;
|
||||||
|
}
|
||||||
|
|
||||||
|
push(logLevel: LoggingLevel, message: string, category: string, timestamp: Date): void {
|
||||||
|
const now = Date.now();
|
||||||
|
const runtime = (now - pageLoadTime) / 1000;
|
||||||
|
const logRecord = {
|
||||||
|
timestamp: timestamp.toISOString(),
|
||||||
|
tenancy: this._telemetryConfiguration.tenancy,
|
||||||
|
userId: this._telemetryConfiguration.userId,
|
||||||
|
level: LoggingLevel[logLevel],
|
||||||
|
runtime,
|
||||||
|
category,
|
||||||
|
message,
|
||||||
|
sessionId: this._telemetryConfiguration.sessionId,
|
||||||
|
version: config.controlCenterVersion,
|
||||||
|
environment: this._telemetryConfiguration.environment,
|
||||||
|
fullQualifiedName: this._domain,
|
||||||
|
source: `Portal (${this._telemetryConfiguration.browser})`
|
||||||
|
} as ILogItem;
|
||||||
|
|
||||||
|
if (logLevel < LoggingLevel.Error) {
|
||||||
|
this._logs.push(logRecord);
|
||||||
|
} else {
|
||||||
|
this._logs.unshift(logRecord);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// @ts-expect-error: Unused variable intentionally
|
||||||
|
const ignored = this.sendLogsIfAble();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async sendLogs(logMessages: Array<ILogItem>): Promise<Response | void> {
|
||||||
|
const formData = new FormData();
|
||||||
|
|
||||||
|
formData.append('jsonBody', JSON.stringify({records: logMessages}));
|
||||||
|
|
||||||
|
return await fetch(this._telemetryConfiguration.url, {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async sendLogsIfAble(): Promise<Response | void> {
|
||||||
|
if (this._logs.length <= 0 || this._isSending) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let numberOfLogsToSend = 0;
|
||||||
|
let sizeOfLogsToSend = 0;
|
||||||
|
|
||||||
|
this._isSending = true;
|
||||||
|
|
||||||
|
const getLogSize = (log: ILogItem): number => Object.values(log).reduce((sum, item) => sum + (item ? `${item}`.length : 0), 0);
|
||||||
|
|
||||||
|
while (this._logs.length > numberOfLogsToSend && getLogSize(this._logs[numberOfLogsToSend]) + sizeOfLogsToSend < requestSizeLimit) {
|
||||||
|
sizeOfLogsToSend += getLogSize(this._logs[numberOfLogsToSend]);
|
||||||
|
numberOfLogsToSend++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!numberOfLogsToSend) {
|
||||||
|
this._logs[numberOfLogsToSend].message = this._logs[numberOfLogsToSend].message.substring(
|
||||||
|
0,
|
||||||
|
getLogSize(this._logs[numberOfLogsToSend]) + (requestSizeLimit - getLogSize(this._logs[numberOfLogsToSend]))
|
||||||
|
);
|
||||||
|
numberOfLogsToSend = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const logMessages = this._logs.slice(0, numberOfLogsToSend);
|
||||||
|
|
||||||
|
this._logs = this._logs.slice(numberOfLogsToSend);
|
||||||
|
|
||||||
|
return this.sendLogs(logMessages)
|
||||||
|
.then(response => {
|
||||||
|
this._isSending = false;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// @ts-expect-error: Unused variable intentionally
|
||||||
|
|
||||||
|
const ignored = this.sendLogsIfAble();
|
||||||
|
|
||||||
|
return response;
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
this._isSending = false;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// @ts-expect-error: Unused variable intentionally
|
||||||
|
|
||||||
|
const ignored = this.sendLogsIfAble();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
76
src/services/url.service.ts
Normal file
76
src/services/url.service.ts
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type UrlType = 'pcast' | 'telemetry' | 'websocket';
|
||||||
|
|
||||||
|
export default class UrlService {
|
||||||
|
private static getBaseUrlOrigin(url: string = window?.location?.href, urlType: UrlType = 'pcast'): string {
|
||||||
|
if (!url) {
|
||||||
|
throw new Error('Invalid URL');
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseURL = new URL(url);
|
||||||
|
const backendUrlParam = baseURL.searchParams.get('backend');
|
||||||
|
|
||||||
|
if (backendUrlParam) {
|
||||||
|
const backendUrl = new URL(backendUrlParam);
|
||||||
|
if (urlType === 'websocket') {
|
||||||
|
backendUrl.protocol = 'wss:';
|
||||||
|
backendUrl.pathname = '/ws';
|
||||||
|
}
|
||||||
|
|
||||||
|
return backendUrl.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
const segments = baseURL.hostname.split('.');
|
||||||
|
const prefix = urlType === 'telemetry' ? 'telemetry' : 'pcast';
|
||||||
|
|
||||||
|
// Normalize WebSocket protocols to HTTP
|
||||||
|
switch (baseURL.protocol) {
|
||||||
|
case 'ws:':
|
||||||
|
baseURL.protocol = 'http:';
|
||||||
|
break;
|
||||||
|
case 'wss:':
|
||||||
|
baseURL.protocol = 'https:';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply routing logic based on URL structure
|
||||||
|
if (segments.length === 2 || (segments.length === 3 && segments[segments.length - 2].length <= 2 && segments[segments.length - 1].length <= 3)) {
|
||||||
|
segments.unshift(prefix);
|
||||||
|
} else if (segments[0].startsWith('stg-') || segments[0].endsWith('-stg') || segments[0].includes('-stg-') || segments[0] === 'stg') {
|
||||||
|
segments[0] = `${prefix}-stg`;
|
||||||
|
} else if (segments[0].startsWith('local') || segments[0].endsWith('-local')) {
|
||||||
|
// Leave URL unchanged for local development
|
||||||
|
} else {
|
||||||
|
segments[0] = prefix;
|
||||||
|
}
|
||||||
|
|
||||||
|
baseURL.hostname = segments.join('.');
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
return baseURL.origin;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static getPcastBaseUrlOrigin(url: string = window?.location?.href): string {
|
||||||
|
return this.getBaseUrlOrigin(url, 'pcast');
|
||||||
|
}
|
||||||
|
|
||||||
|
public static getTelemetryUrl(url: string = window?.location?.href): string {
|
||||||
|
try {
|
||||||
|
const baseUrlOrigin = this.getBaseUrlOrigin(url, 'telemetry');
|
||||||
|
return `${baseUrlOrigin}/telemetry`;
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
} catch (e) {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static getWebSocketUrl(url: string = window?.location?.href): string {
|
||||||
|
return this.getBaseUrlOrigin(url, 'websocket');
|
||||||
|
}
|
||||||
|
}
|
||||||
1
src/store/index.ts
Normal file
1
src/store/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './store';
|
||||||
12
src/store/middlewares/PhenixWebSocket.middleware.ts
Normal file
12
src/store/middlewares/PhenixWebSocket.middleware.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { Middleware } from '@reduxjs/toolkit';
|
||||||
|
|
||||||
|
const phenixWebSocketMiddleware: Middleware = (storeApi) => (next) => (action) => {
|
||||||
|
console.log('phenixWebSocketMiddleware', action);
|
||||||
|
if (typeof action === 'function') {
|
||||||
|
return action(storeApi.getState(), storeApi.dispatch);
|
||||||
|
}
|
||||||
|
|
||||||
|
return next(action);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default phenixWebSocketMiddleware;
|
||||||
8
src/store/reducer.ts
Normal file
8
src/store/reducer.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import {combineReducers} from '@reduxjs/toolkit';
|
||||||
|
import {authenticationReducer} from './slices';
|
||||||
|
|
||||||
|
const rootReducer = combineReducers({
|
||||||
|
authentication: authenticationReducer
|
||||||
|
});
|
||||||
|
|
||||||
|
export default rootReducer;
|
||||||
110
src/store/slices/Authentication.slice.ts
Normal file
110
src/store/slices/Authentication.slice.ts
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
import {createSlice, createAsyncThunk, createSelector} from '@reduxjs/toolkit';
|
||||||
|
import AuthenticationService from 'services/authentication.service';
|
||||||
|
import {PhenixWebSocketStatusMapping, PhenixWebSocketStatusType} from 'services/net/PhenixWebSocket';
|
||||||
|
|
||||||
|
export interface IAuthenticationState {
|
||||||
|
applicationId: string | null;
|
||||||
|
secret: string | null;
|
||||||
|
isAuthenticated: boolean;
|
||||||
|
isAuthenticating: boolean;
|
||||||
|
error: string | null;
|
||||||
|
status: PhenixWebSocketStatusType;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialAuthenticationState: IAuthenticationState = {
|
||||||
|
applicationId: null,
|
||||||
|
secret: null,
|
||||||
|
isAuthenticated: false,
|
||||||
|
isAuthenticating: false,
|
||||||
|
error: null,
|
||||||
|
status: 'Offline'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Memoized selectors
|
||||||
|
export const selectAuthentication = (state: {authentication: IAuthenticationState}) => state.authentication;
|
||||||
|
|
||||||
|
export const selectIsAuthenticating = createSelector([selectAuthentication], authentication => authentication.isAuthenticating);
|
||||||
|
|
||||||
|
export const selectIsAuthenticated = createSelector([selectAuthentication], authentication => authentication.isAuthenticated);
|
||||||
|
|
||||||
|
export const selectError = createSelector([selectAuthentication], authentication => authentication.error);
|
||||||
|
|
||||||
|
export const selectStatus = createSelector([selectAuthentication], authentication => authentication.status);
|
||||||
|
|
||||||
|
export const selectCredentials = createSelector([selectAuthentication], authentication => ({
|
||||||
|
applicationId: authentication.applicationId,
|
||||||
|
secret: authentication.secret
|
||||||
|
}));
|
||||||
|
|
||||||
|
const authenticateCredentialsThunk = createAsyncThunk(
|
||||||
|
'authentication/authenticate',
|
||||||
|
async (credentials: {applicationId: string; secret: string}, {rejectWithValue}) => {
|
||||||
|
return AuthenticationService.authenticateCredentials(credentials.applicationId, credentials.secret).catch(rejectWithValue);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const authenticationSlice = createSlice({
|
||||||
|
name: 'authentication',
|
||||||
|
initialState: {...initialAuthenticationState},
|
||||||
|
reducers: {
|
||||||
|
setStatus: (state, action) => {
|
||||||
|
state.status = PhenixWebSocketStatusMapping.convertPhenixWebSocketStatusToPhenixWebSocketStatusType(action.payload);
|
||||||
|
},
|
||||||
|
signout: state => {
|
||||||
|
state.isAuthenticated = false;
|
||||||
|
state.isAuthenticating = false;
|
||||||
|
state.applicationId = null;
|
||||||
|
state.secret = null;
|
||||||
|
state.error = null;
|
||||||
|
},
|
||||||
|
error: (state, action) => {
|
||||||
|
// Store only the error message string, not the Error object
|
||||||
|
if (typeof action.payload === 'string') {
|
||||||
|
state.error = action.payload;
|
||||||
|
} else if (action.payload instanceof Error) {
|
||||||
|
state.error = action.payload.message;
|
||||||
|
} else if (action.payload && typeof action.payload === 'object' && 'message' in action.payload) {
|
||||||
|
state.error = String((action.payload as {message: unknown}).message);
|
||||||
|
} else {
|
||||||
|
state.error = action.payload ? String(action.payload) : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
extraReducers: builder => {
|
||||||
|
builder
|
||||||
|
.addCase(authenticateCredentialsThunk.pending, state => {
|
||||||
|
state.isAuthenticating = true;
|
||||||
|
state.error = null;
|
||||||
|
})
|
||||||
|
.addCase(authenticateCredentialsThunk.fulfilled, state => {
|
||||||
|
state.isAuthenticating = false;
|
||||||
|
state.error = null;
|
||||||
|
state.isAuthenticated = true;
|
||||||
|
})
|
||||||
|
.addCase(authenticateCredentialsThunk.rejected, (state, action) => {
|
||||||
|
state.isAuthenticating = false;
|
||||||
|
// Extract error message string instead of creating new Error object
|
||||||
|
let errorMessage = 'Authentication failed';
|
||||||
|
|
||||||
|
if (action.payload) {
|
||||||
|
if (action.payload instanceof Error) {
|
||||||
|
errorMessage = action.payload.message;
|
||||||
|
} else if (typeof action.payload === 'object' && action.payload !== null && 'message' in action.payload) {
|
||||||
|
errorMessage = String((action.payload as {message: unknown}).message);
|
||||||
|
} else if (typeof action.payload === 'string') {
|
||||||
|
errorMessage = action.payload;
|
||||||
|
} else {
|
||||||
|
errorMessage = String(action.payload);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
state.error = errorMessage;
|
||||||
|
state.isAuthenticated = false;
|
||||||
|
state.applicationId = null;
|
||||||
|
state.secret = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const {reducer: authenticationReducer, actions: authenticationActions} = authenticationSlice;
|
||||||
|
export {authenticateCredentialsThunk};
|
||||||
1
src/store/slices/index.ts
Normal file
1
src/store/slices/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './Authentication.slice';
|
||||||
7
src/store/store.ts
Normal file
7
src/store/store.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import {configureStore} from '@reduxjs/toolkit';
|
||||||
|
import reducer from './reducer';
|
||||||
|
import phenixWebSocketMiddleware from './middlewares/PhenixWebSocket.middleware';
|
||||||
|
|
||||||
|
export const store = configureStore({reducer, middleware: getDefaultMiddleware => getDefaultMiddleware().concat(phenixWebSocketMiddleware)});
|
||||||
|
|
||||||
|
export type RootState = ReturnType<typeof store.getState>;
|
||||||
22
src/types/phenix-web-proto.d.ts
vendored
Normal file
22
src/types/phenix-web-proto.d.ts
vendored
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
declare module 'phenix-web-proto' {
|
||||||
|
import { ILogger } from 'services/logger/LoggerInterface';
|
||||||
|
|
||||||
|
export class MQWebSocket {
|
||||||
|
constructor(uri: string, logger: ILogger, protocols: any[], apiVersion?: string);
|
||||||
|
onEvent(eventName: string, handler: (event: 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;
|
||||||
|
sendResponse(requestId: string, type: string, message: unknown, callback?: (error: unknown, response: unknown) => void): void;
|
||||||
|
disconnect(): void;
|
||||||
|
disposeOfPendingRequests(): void;
|
||||||
|
getApiVersion(): string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class BatchHttpProto {
|
||||||
|
// Add methods as needed
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MQService {
|
||||||
|
// Add methods as needed
|
||||||
|
}
|
||||||
|
}
|
||||||
1
src/vite-env.d.ts
vendored
Normal file
1
src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
46
tsconfig.app.json
Normal file
46
tsconfig.app.json
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es2022",
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"emitDecoratorMetadata": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"downlevelIteration": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"noEmitHelpers": true,
|
||||||
|
"importHelpers": true,
|
||||||
|
"strictNullChecks": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noImplicitAny": true, // TODO(AZ): Fix types to enable setting this to true
|
||||||
|
"lib": ["dom", "dom.iterable", "es2022"],
|
||||||
|
"baseUrl": "./src",
|
||||||
|
"paths": {
|
||||||
|
// Relative to baseUrl https://www.typescriptlang.org/tsconfig/#paths
|
||||||
|
"assets/*": ["./assets/*"],
|
||||||
|
"components/*": ["./components/*"],
|
||||||
|
"config/*": ["./config/*"],
|
||||||
|
"constant-data/*": ["./constant-data/*"],
|
||||||
|
"hooks/*": ["./hooks/*"],
|
||||||
|
"interfaces/*": ["./interfaces/*"],
|
||||||
|
"routers/*": ["./routers/*"],
|
||||||
|
"store/*": ["./store/*"],
|
||||||
|
"services/*": ["./services/*"],
|
||||||
|
"lang/*": ["./lang/*"],
|
||||||
|
"utility/*": ["./utility/*"],
|
||||||
|
"views/*": ["./views/*"]
|
||||||
|
},
|
||||||
|
"allowJs": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"strict": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"noFallthroughCasesInSwitch": true
|
||||||
|
},
|
||||||
|
"exclude": ["node_modules", "public"],
|
||||||
|
"include": ["src", "test"]
|
||||||
|
}
|
||||||
27
tsconfig.app.json.orig
Normal file
27
tsconfig.app.json.orig
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||||
|
"target": "ES2022",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"erasableSyntaxOnly": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
1
tsconfig.app.tsbuildinfo
Normal file
1
tsconfig.app.tsbuildinfo
Normal file
@@ -0,0 +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"}
|
||||||
4
tsconfig.json
Normal file
4
tsconfig.json
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"files": [],
|
||||||
|
"references": [{"path": "./tsconfig.app.json"}, {"path": "./tsconfig.node.json"}]
|
||||||
|
}
|
||||||
25
tsconfig.node.json
Normal file
25
tsconfig.node.json
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||||
|
"target": "ES2023",
|
||||||
|
"lib": ["ES2023"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"erasableSyntaxOnly": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
||||||
33
vite.config.ts
Normal file
33
vite.config.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import path from 'path';
|
||||||
|
import {defineConfig} from 'vite';
|
||||||
|
import react from '@vitejs/plugin-react-swc';
|
||||||
|
import babel from 'vite-plugin-babel';
|
||||||
|
|
||||||
|
console.log('process.cwd()', process.cwd());
|
||||||
|
// https://vite.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [
|
||||||
|
react(),
|
||||||
|
babel({
|
||||||
|
babelConfig: {
|
||||||
|
plugins: ['transform-amd-to-commonjs']
|
||||||
|
}
|
||||||
|
})
|
||||||
|
],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
assets: path.resolve(process.cwd(), 'src', 'assets'),
|
||||||
|
components: path.resolve(process.cwd(), 'src/components'),
|
||||||
|
config: path.resolve(process.cwd(), 'src', 'config'),
|
||||||
|
'constant-data': path.resolve(process.cwd(), 'src', 'constant-data'),
|
||||||
|
hooks: path.resolve(process.cwd(), 'src', 'hooks'),
|
||||||
|
interfaces: path.resolve(process.cwd(), 'src', 'interfaces'),
|
||||||
|
routers: path.resolve(process.cwd(), 'src', 'routers'),
|
||||||
|
store: path.resolve(process.cwd(), 'src', 'store'),
|
||||||
|
services: path.resolve(process.cwd(), 'src', 'services'),
|
||||||
|
lang: path.resolve(process.cwd(), 'src', 'lang'),
|
||||||
|
utility: path.resolve(process.cwd(), 'src', 'utility'),
|
||||||
|
views: path.resolve(process.cwd(), 'src', 'views')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user