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