Initial Commit
22
src/App.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import Router from './routers';
|
||||
import Theme from './theme';
|
||||
|
||||
const AppContainer = styled.div`
|
||||
height: 100vh;
|
||||
background: ${Theme.backgrounds.defaultBackground};
|
||||
background-attachment: fixed;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
`;
|
||||
|
||||
const App = (): React.JSX.Element => {
|
||||
return (
|
||||
<AppContainer>
|
||||
<Router />
|
||||
</AppContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
||||
BIN
src/assets/images/background-1415x959.png
Normal file
|
After Width: | Height: | Size: 321 KiB |
BIN
src/assets/images/calendar-24x24.png
Normal file
|
After Width: | Height: | Size: 652 B |
1
src/assets/images/caret-down.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" ?><!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'><svg enable-background="new 0 0 29 14" height="6px" id="Layer_1" version="1.1" viewBox="0 0 29 14" width="29px" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><polygon fill="#F9F9F9" points="0.15,0 14.5,14.35 28.85,0 "/></svg>
|
||||
|
After Width: | Height: | Size: 400 B |
1
src/assets/images/caret-up.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" ?><!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'><svg enable-background="new 0 0 29 14" height="8px" id="Layer_1" version="1.1" viewBox="0 0 29 14" width="29px" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><polygon fill="#f9f9f9" points="0.15,14 14.5,-0.35 28.85,14 "/></svg>
|
||||
|
After Width: | Height: | Size: 402 B |
BIN
src/assets/images/chart-down-50x33.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
src/assets/images/chart-up-50x33.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
1
src/assets/images/icon/error.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!-- Generator: Gravit.io --><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="isolation:isolate" viewBox="0 0 206.751 186.399" width="206.751pt" height="186.399pt"><defs><clipPath id="_clipPath_Wy0NWkkA4mKqtolphxnGK0Cf0TrWZqlo"><rect width="206.751" height="186.399"/></clipPath></defs><g clip-path="url(#_clipPath_Wy0NWkkA4mKqtolphxnGK0Cf0TrWZqlo)"><g><path d=" M 179.102 178.898 L 103.306 178.559 L 27.51 178.22 C 9.581 178.14 2.36 165.502 11.394 150.015 L 49.585 84.544 L 87.777 19.072 C 96.811 3.586 111.366 3.651 120.261 19.218 L 157.865 85.028 L 195.469 150.839 C 204.364 166.406 197.03 178.979 179.102 178.898 Z " fill="none" vector-effect="non-scaling-stroke" stroke-width="5" stroke="rgb(247,13,26)" stroke-linejoin="miter" stroke-linecap="square" stroke-miterlimit="3"/><line x1="103.306" y1="47.028" x2="103.306" y2="120.028" vector-effect="non-scaling-stroke" stroke-width="5" stroke="rgb(247,13,26)" stroke-linejoin="miter" stroke-linecap="square" stroke-miterlimit="3"/><line x1="103.306" y1="138.028" x2="103.306" y2="143.028" vector-effect="non-scaling-stroke" stroke-width="5" stroke="rgb(247,13,26)" stroke-linejoin="miter" stroke-linecap="square" stroke-miterlimit="3"/></g></g></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
10
src/assets/images/icon/hash-plus.svg
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="19px" height="18px" viewBox="0 0 19 18" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="01-home-copy-7" transform="translate(-1214.000000, -84.000000)" fill="#FFFFFF" fill-rule="nonzero">
|
||||
<g id="icon/white/add-channel" transform="translate(1211.000000, 80.000000)">
|
||||
<path d="M18,14 C18.5522847,14 19,14.4477153 19,15 L18.999068,17 L21,17 C21.5522847,17 22,17.4477153 22,18 C22,18.5522847 21.5522847,19 21,19 L18.999068,19 L19,21 C19,21.5522847 18.5522847,22 18,22 C17.4477153,22 17,21.5522847 17,21 L16.999068,18.999 L15,19 C14.4477153,19 14,18.5522847 14,18 C14,17.4477153 14.4477153,17 15,17 L16.999068,16.999 L17,15 C17,14.4477153 17.4477153,14 18,14 Z M14,4 C14.4287818,4 14.8254386,4.13754318 15.1482567,4.37091574 C15.4794061,4.13462978 15.8829334,4 16.3080311,4 L18,4 C19.0836085,4 19.9620467,4.87843824 19.9620467,5.96204672 C19.9620467,6.23272939 19.9060382,6.5004841 19.7975435,6.748472 L19.687068,6.999 L19.763932,7 C20.8685015,7 21.763932,7.8954305 21.763932,9 C21.763932,9.31049019 21.6916418,9.61671632 21.5527864,9.89442719 L20.5527864,11.8944272 C20.3949049,12.2101902 20.1601982,12.4717535 19.8778882,12.6610569 C19.3634108,12.2470293 18.7105538,12 18,12 C17.1114527,12 16.3131288,12.3862919 15.7638055,13.0000983 L15.083068,13 L15.916068,11 L18.381966,11 C18.7607381,11 19.1070012,10.7859976 19.2763932,10.4472136 L19.5,10 C19.6706654,9.65866925 19.5323138,9.24361439 19.190983,9.07294902 C19.0950363,9.02497564 18.9892377,9 18.881966,9 L16.750068,9 L17.6153846,6.92307692 C17.7569961,6.58320938 17.5962778,6.19289353 17.2564103,6.05128205 C17.1751635,6.01742923 17.0880173,6 17,6 L16.6666667,6 C16.2629658,6 15.8988593,6.24273768 15.7435897,6.61538462 L14.749068,9 L12.750068,9 L13.6153846,6.92307692 C13.7569961,6.58320938 13.5962778,6.19289353 13.2564103,6.05128205 C13.1751635,6.01742923 13.0880173,6 13,6 L12.6666667,6 C12.2629658,6 11.8988593,6.24273768 11.7435897,6.61538462 L10.749068,9 L8.61803399,9 C8.23926193,9 7.89299881,9.21400238 7.7236068,9.5527864 L7.5,10 C7.32933463,10.3413307 7.46768625,10.7563856 7.80901699,10.927051 C7.90496374,10.9750244 8.01076227,11 8.11803399,11 L9.91606798,11 L9.08206798,13 L6.61803399,13 C6.23926193,13 5.89299881,13.2140024 5.7236068,13.5527864 L5.5,14 C5.32933463,14.3413307 5.46768625,14.7563856 5.80901699,14.927051 C5.90496374,14.9750244 6.01076227,15 6.11803399,15 L8.24906798,15 L7.38461538,17.0769231 C7.24300391,17.4167906 7.40372221,17.8071065 7.74358974,17.9487179 C7.82483652,17.9825708 7.91198265,18 8,18 L8.33333333,18 C8.73703418,18 9.1011407,17.7572623 9.25641026,17.3846154 L10.249068,15 L12.249068,15 L11.3846154,17.0769231 C11.2430039,17.4167906 11.4037222,17.8071065 11.7435897,17.9487179 C11.8248365,17.9825708 11.9119827,18 12,18 L12.0354373,18.0000497 C12.0120835,18.1633354 12,18.3302566 12,18.5 C12,19.037207 12.1210295,19.5461461 12.3373087,20.0010375 L11,20 C10.5712182,20 10.1745614,19.8624568 9.85174329,19.6290843 C9.52059393,19.8653702 9.11706659,20 8.69196885,20 L7,20 C5.91639152,20 5.03795328,19.1215618 5.03795328,18.0379533 C5.03795328,17.7672706 5.09396179,17.4995159 5.2024565,17.251528 L5.31206798,17 L5.23606798,17 C4.13149848,17 3.23606798,16.1045695 3.23606798,15 C3.23606798,14.6895098 3.30835816,14.3832837 3.4472136,14.1055728 L4.4472136,12.1055728 C4.62888297,11.7422341 4.91227191,11.4506579 5.25285994,11.2583596 C5.24175599,11.1747423 5.23606798,11.0880354 5.23606798,11 C5.23606798,10.6895098 5.30835816,10.3832837 5.4472136,10.1055728 L6.4472136,8.10557281 C6.78599762,7.42800475 7.47852386,7 8.23606798,7 L9.68706798,6.999 L10.4757165,5.19836233 C10.7942054,4.47038768 11.5134355,4 12.3080311,4 L14,4 Z M15,15 L14.9990501,15.0355804 C14.6965332,15.0789368 14.406503,15.160979 14.1341327,15.276534 L14.249068,15 L15,15 Z M13.916068,11 L13.082068,13 L11.083068,13 L11.916068,11 L13.916068,11 Z" id="Combined-Shape"></path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.0 KiB |
53
src/assets/images/icon/menu.svg
Normal file
@@ -0,0 +1,53 @@
|
||||
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
|
||||
<g>
|
||||
<g>
|
||||
<path style="fill:#F9F9F9" d="M501.333,96H10.667C4.779,96,0,100.779,0,106.667s4.779,10.667,10.667,10.667h490.667c5.888,0,10.667-4.779,10.667-10.667
|
||||
S507.221,96,501.333,96z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path style="fill:#F9F9F9" d="M501.333,245.333H10.667C4.779,245.333,0,250.112,0,256s4.779,10.667,10.667,10.667h490.667
|
||||
c5.888,0,10.667-4.779,10.667-10.667S507.221,245.333,501.333,245.333z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path style="fill:#F9F9F9" d="M501.333,394.667H10.667C4.779,394.667,0,399.445,0,405.333C0,411.221,4.779,416,10.667,416h490.667
|
||||
c5.888,0,10.667-4.779,10.667-10.667C512,399.445,507.221,394.667,501.333,394.667z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
1
src/assets/images/icon/ok.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg fill="#00bc8c" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50" width="250px" height="250px"><path fill="none" stroke="#00bc8c" stroke-miterlimit="10" stroke-width="2" d="M25,3C12.85,3,3,12.85,3,25s9.85,22,22,22 s22-9.85,22-22S37.15,3,25,3z"/><path fill="none" stroke="#00bc8c" stroke-linecap="round" stroke-miterlimit="10" stroke-width="2" d="M16,24.444L24.143,32L35,16"/></svg>
|
||||
|
After Width: | Height: | Size: 390 B |
12
src/assets/images/icon/refresh.svg
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg width="19px" height="18px" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 341.333 341.333" style="enable-background:new 0 0 341.333 341.333;" xml:space="preserve">
|
||||
<g>
|
||||
<g>
|
||||
<path fill="#ffffff" d="M341.227,149.333V0l-50.133,50.133C260.267,19.2,217.707,0,170.56,0C76.267,0,0.107,76.373,0.107,170.667
|
||||
s76.16,170.667,170.453,170.667c79.467,0,146.027-54.4,164.907-128h-44.373c-17.6,49.707-64.747,85.333-120.533,85.333
|
||||
c-70.72,0-128-57.28-128-128s57.28-128,128-128c35.307,0,66.987,14.72,90.133,37.867l-68.8,68.8H341.227z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 796 B |
BIN
src/assets/images/logo-no-text.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
src/assets/images/phenix-logo-101x41.png
Executable file
|
After Width: | Height: | Size: 4.7 KiB |
BIN
src/assets/images/phenix-offline-screen-1920x1080.gif
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
BIN
src/assets/images/search-150x150.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
src/assets/images/symbol-lock-24x24.png
Executable file
|
After Width: | Height: | Size: 374 B |
BIN
src/assets/images/symbol-person-24x24.png
Executable file
|
After Width: | Height: | Size: 563 B |
15
src/components/ProtectedRoute.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import React from 'react';
|
||||
import {Navigate, useLocation} from 'react-router-dom';
|
||||
import {useAppSelector} from 'store';
|
||||
import {selectIsAuthenticated} from 'store/slices/Authentication.slice';
|
||||
|
||||
export function ProtectedRoute({component}: {component: React.JSX.Element}): React.JSX.Element {
|
||||
const isAuthenticated = useAppSelector(selectIsAuthenticated);
|
||||
const location = useLocation();
|
||||
|
||||
if (!isAuthenticated) {
|
||||
return <Navigate to="/login" state={{from: location}} replace />;
|
||||
}
|
||||
|
||||
return component;
|
||||
}
|
||||
28
src/components/buttons/copy-icon-button/index.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
import React, {useState} from 'react';
|
||||
import {faCopy, faCheck} from '@fortawesome/free-solid-svg-icons';
|
||||
import IconButton from 'components/buttons/icon-button';
|
||||
|
||||
import {CopyButtonContainer} from './styles';
|
||||
|
||||
const iconChangeTimeout = 2000;
|
||||
|
||||
export const CopyIconButton = (props: {text: string; quoted?: boolean; displayText?: boolean; className?: string}): React.JSX.Element => {
|
||||
const {text, quoted = false, displayText = true, className} = props;
|
||||
const [copied, setCopied] = useState(false);
|
||||
const copyToClipboard = (): void => {
|
||||
navigator.clipboard.writeText(text);
|
||||
setCopied(true);
|
||||
|
||||
setTimeout(() => setCopied(false), iconChangeTimeout);
|
||||
};
|
||||
|
||||
return (
|
||||
<CopyButtonContainer className={className}>
|
||||
{displayText && (quoted ? `"${text}"` : text)}
|
||||
<IconButton onClick={copyToClipboard} tooltipText="Copy" icon={copied ? faCheck : faCopy} />
|
||||
</CopyButtonContainer>
|
||||
);
|
||||
};
|
||||
9
src/components/buttons/copy-icon-button/styles.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
import * as styled from 'styled-components';
|
||||
|
||||
export const CopyButtonContainer = styled.default.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`;
|
||||
32
src/components/buttons/export-file-button/index.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
import React from 'react';
|
||||
import {theme} from 'components/shared/theme';
|
||||
import {Button} from 'components/buttons';
|
||||
|
||||
const {colors} = theme;
|
||||
|
||||
interface IExportFileButton {
|
||||
label?: string;
|
||||
file: string;
|
||||
fileName?: string;
|
||||
}
|
||||
|
||||
export const ExportFileButton = ({label = 'Export File', file, fileName = 'file'}: IExportFileButton): React.JSX.Element => {
|
||||
const handleExport = () => {
|
||||
const downloadUrl = URL.createObjectURL(new Blob([file]));
|
||||
const linkTag = document.createElement('a');
|
||||
|
||||
linkTag.href = downloadUrl;
|
||||
linkTag.setAttribute('target', '_blank');
|
||||
linkTag.setAttribute('download', fileName);
|
||||
linkTag.click();
|
||||
};
|
||||
|
||||
return (
|
||||
<Button onClick={handleExport} className="testId-exportFile" backgroundColor={colors.red} borderColor={colors.red}>
|
||||
{label}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
27
src/components/buttons/icon-button/icon-button.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
|
||||
import {IconProp} from '@fortawesome/fontawesome-svg-core';
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
|
||||
|
||||
import {Position, Tooltip} from 'components/tooltip';
|
||||
|
||||
import {IconButtonContainer} from './styles';
|
||||
|
||||
interface IIconButton {
|
||||
onClick: () => void;
|
||||
tooltipText: string;
|
||||
icon: IconProp;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const IconButton = ({onClick, tooltipText, icon, className}: IIconButton) => (
|
||||
<Tooltip position={Position.Bottom} message={tooltipText}>
|
||||
<IconButtonContainer className={`icon-button ${className}`} role="link" tabIndex={-11} onKeyDown={undefined} onClick={onClick}>
|
||||
<FontAwesomeIcon icon={icon} />
|
||||
</IconButtonContainer>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
export default IconButton;
|
||||
5
src/components/buttons/icon-button/index.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
|
||||
export {default} from './icon-button';
|
||||
30
src/components/buttons/icon-button/styles.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
import * as styled from 'styled-components';
|
||||
|
||||
export const IconButtonContainer = styled.default.div`
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
|
||||
&.disabled {
|
||||
cursor: not-allowed;
|
||||
pointer-events: none;
|
||||
opacity: 0.5;
|
||||
}
|
||||
&.icon-button {
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
}
|
||||
|
||||
svg {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
`;
|
||||
17
src/components/buttons/icon-buttons/add-button.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
import React from 'react';
|
||||
import {IconButton} from './style';
|
||||
import addIcon from 'assets/images/icon/hash-plus.svg';
|
||||
|
||||
interface IAddButton {
|
||||
onClick: () => void;
|
||||
className: string;
|
||||
}
|
||||
|
||||
export const AddButton = ({onClick, className}: IAddButton): React.JSX.Element => (
|
||||
<IconButton onClick={onClick} className={className}>
|
||||
<img src={addIcon} alt={'Add'} />
|
||||
</IconButton>
|
||||
);
|
||||
5
src/components/buttons/icon-buttons/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
export * from './refresh-button';
|
||||
export * from './add-button';
|
||||
18
src/components/buttons/icon-buttons/refresh-button.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
import React from 'react';
|
||||
import {IconButton} from './style';
|
||||
|
||||
import refreshIcon from 'assets/images/icon/refresh.svg';
|
||||
|
||||
interface IRefreshButton {
|
||||
onClick: () => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export const RefreshButton = ({onClick, disabled = false}: IRefreshButton): React.JSX.Element => (
|
||||
<IconButton onClick={onClick} disabled={disabled}>
|
||||
<img src={refreshIcon} alt={'Refresh'} />
|
||||
</IconButton>
|
||||
);
|
||||
20
src/components/buttons/icon-buttons/style.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
import * as styled from 'styled-components';
|
||||
import {theme} from 'components/shared/theme';
|
||||
|
||||
const {
|
||||
typography: {fontSizeL},
|
||||
colors
|
||||
} = theme;
|
||||
|
||||
export const IconButton = styled.default.button`
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
font-size: ${fontSizeL};
|
||||
color: ${colors.white};
|
||||
opacity: ${({disabled}) => (disabled ? 0.3 : 1)};
|
||||
cursor: ${({disabled}) => (disabled ? 'not-allowed' : 'pointer')};
|
||||
display: flex;
|
||||
`;
|
||||
57
src/components/buttons/index.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
import * as styled from 'styled-components';
|
||||
import Theme from 'theme';
|
||||
|
||||
export const Button = styled.default.button<{
|
||||
backgroundColor?: string;
|
||||
borderColor?: string;
|
||||
textColor?: string;
|
||||
disabled?: boolean;
|
||||
}>`
|
||||
${({backgroundColor, textColor, borderColor}) => styled.css`
|
||||
color: ${textColor || Theme.colors.white};
|
||||
background-color: ${backgroundColor || Theme.colors.white};
|
||||
border-color: ${borderColor || backgroundColor || Theme.colors.lightRed};
|
||||
`}
|
||||
${({disabled}) => styled.css`
|
||||
opacity: ${disabled ? 0.8 : 1};
|
||||
cursor: ${disabled ? 'not-allowed' : 'pointer'};
|
||||
`}
|
||||
min-width: 120px;
|
||||
text-align: center;
|
||||
outline: none;
|
||||
vertical-align: middle;
|
||||
border-radius: ${Theme.primaryBorderRadius};
|
||||
padding: ${Theme.spacing.small} ${Theme.spacing.medium};
|
||||
font-size: ${Theme.typography.primaryFontSize};
|
||||
transition: color .15s ease-in-out, background-color .15s ease-in-out;
|
||||
`;
|
||||
|
||||
export const FilterButton = styled.default(Button)`
|
||||
font-weight: bolder;
|
||||
justify-self: center;
|
||||
margin: 0 ${Theme.spacing.xSmall};
|
||||
padding: ${Theme.spacing.small} ${Theme.spacing.xlarge};
|
||||
`;
|
||||
|
||||
export const ConfirmButton = styled.default(Button)`
|
||||
margin-right: 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
export const CancelButton = styled.default(ConfirmButton)``;
|
||||
|
||||
export const CustomButton = styled.default(Button)`
|
||||
background-color: ${Theme.dangerColor};
|
||||
border-color: ${Theme.dangerColor};
|
||||
color: ${Theme.colors.white};
|
||||
font-weight: bolder;
|
||||
justify-self: center;
|
||||
margin: 0.75rem;
|
||||
padding: ${Theme.spacing.small} ${Theme.spacing.xlarge};
|
||||
overflow: visible;
|
||||
`;
|
||||
69
src/components/buttons/radio-button/index.tsx
Normal file
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
import React, {Fragment} from 'react';
|
||||
import {Tooltip, Position} from 'components/tooltip';
|
||||
import {Label} from 'components/label';
|
||||
import {RadioGroup, RadioWrapper, RadioButtonContainer, VisibleCheckBox} from './style';
|
||||
|
||||
interface IRadioItems {
|
||||
label: string;
|
||||
value: string;
|
||||
tooltipMessage?: string;
|
||||
tooltipPosition?: Position;
|
||||
className?: string;
|
||||
children?: React.JSX.Element;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
interface IRadioButtonGroup {
|
||||
items: IRadioItems[];
|
||||
handleOnChange: (value: string) => void;
|
||||
currentValue: string;
|
||||
}
|
||||
|
||||
const RadioButton = (props: {currentValue: string; value: string}) => {
|
||||
const {currentValue, value} = props;
|
||||
|
||||
return (
|
||||
<RadioButtonContainer>
|
||||
<input type="radio" readOnly={true} value={value} checked={currentValue === value} />
|
||||
<VisibleCheckBox checked={currentValue === value}>
|
||||
<div />
|
||||
</VisibleCheckBox>
|
||||
</RadioButtonContainer>
|
||||
);
|
||||
};
|
||||
|
||||
const RadioButtonGroup = (props: IRadioButtonGroup): React.JSX.Element => {
|
||||
const {items, handleOnChange, currentValue} = props;
|
||||
|
||||
return (
|
||||
<RadioGroup>
|
||||
{items.map(({label, value, disabled, tooltipPosition, tooltipMessage, children, className}: IRadioItems, index: number) => (
|
||||
<RadioWrapper
|
||||
tabIndex={-1}
|
||||
onKeyPress={() => null}
|
||||
disabled={disabled}
|
||||
className="button-container"
|
||||
role="button"
|
||||
key={label + index}
|
||||
onClick={() => handleOnChange(value)}>
|
||||
<RadioButton value={value} currentValue={currentValue} />
|
||||
<Fragment>
|
||||
{tooltipMessage ? (
|
||||
<Tooltip position={tooltipPosition || Position.Top} message={tooltipMessage}>
|
||||
<Label className={className} text={label} />
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Label className={className} text={label} />
|
||||
)}
|
||||
{children}
|
||||
</Fragment>
|
||||
</RadioWrapper>
|
||||
))}
|
||||
</RadioGroup>
|
||||
);
|
||||
};
|
||||
|
||||
export default RadioButtonGroup;
|
||||
62
src/components/buttons/radio-button/style.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
import * as styled from 'styled-components';
|
||||
import {theme, Theme} from 'components/shared/theme';
|
||||
|
||||
const {
|
||||
spacing,
|
||||
typography: {primaryFontSize},
|
||||
colors
|
||||
} = theme;
|
||||
const paddings = Theme.paddings;
|
||||
|
||||
export const RadioGroup = styled.default.div`
|
||||
display: flex;
|
||||
`;
|
||||
|
||||
export const RadioWrapper = styled.default.div<{disabled?: boolean}>`
|
||||
padding: ${paddings.small};
|
||||
display: flex;
|
||||
|
||||
${({disabled}) =>
|
||||
disabled &&
|
||||
styled.css`
|
||||
pointer-events: none;
|
||||
opacity: 0.5;
|
||||
`}
|
||||
`;
|
||||
|
||||
export const RadioButtonContainer = styled.default.div`
|
||||
margin-right: ${spacing.xSmall};
|
||||
align-self: center;
|
||||
|
||||
input {
|
||||
position: absolute;
|
||||
visibility: hidden;
|
||||
}
|
||||
`;
|
||||
|
||||
export const VisibleCheckBox = styled.default.div<{checked?: boolean}>`
|
||||
border: 2px solid ${colors.gray400};
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: ${primaryFontSize};
|
||||
height: ${primaryFontSize};
|
||||
border-radius: 50%;
|
||||
|
||||
${({checked}) =>
|
||||
checked &&
|
||||
styled.css`
|
||||
border: none;
|
||||
background-color: ${colors.red};
|
||||
|
||||
div {
|
||||
background-color: ${colors.black};
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
`}
|
||||
`;
|
||||
76
src/components/buttons/scroll-buttons/index.tsx
Normal file
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
import React from 'react';
|
||||
import * as styled from 'styled-components';
|
||||
import {theme} from 'components/shared/theme';
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
|
||||
import {faBackward, faFastBackward, faFastForward, faForward} from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
const {colors, spacing} = theme;
|
||||
const ScrollButton = styled.default.button`
|
||||
border: none;
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
color: ${colors.white};
|
||||
margin: ${spacing.xxSmall} 0;
|
||||
background-color: ${colors.gray600};
|
||||
cursor: pointer;
|
||||
transform: rotate(90deg)
|
||||
`;
|
||||
const TwinButtons = styled.default.div`
|
||||
display: flex;
|
||||
position: absolute;
|
||||
right: 64px;
|
||||
bottom: 50px;
|
||||
flex-direction: column;
|
||||
`;
|
||||
|
||||
export const ScrollButtons = ({current}: {current: HTMLDivElement}): React.JSX.Element => {
|
||||
const getScrollStep = (current: HTMLDivElement) => {
|
||||
const tableViewHeight = current.offsetHeight;
|
||||
|
||||
return tableViewHeight / 2;
|
||||
};
|
||||
|
||||
const scrollToTop = () => {
|
||||
if (current) {
|
||||
current.scrollTop = 0;
|
||||
}
|
||||
};
|
||||
|
||||
const scrollTop = () => {
|
||||
if (current) {
|
||||
current.scrollTop = current.scrollTop - getScrollStep(current);
|
||||
}
|
||||
};
|
||||
|
||||
const scrollBottom = () => {
|
||||
if (current) {
|
||||
current.scrollTop = current.scrollTop + getScrollStep(current);
|
||||
}
|
||||
};
|
||||
|
||||
const scrollToBottom = () => {
|
||||
if (current) {
|
||||
current.scrollTop = current.scrollHeight;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<TwinButtons>
|
||||
<ScrollButton onClick={scrollToTop}>
|
||||
<FontAwesomeIcon icon={faFastBackward} />
|
||||
</ScrollButton>
|
||||
<ScrollButton onClick={scrollTop}>
|
||||
<FontAwesomeIcon icon={faBackward} />
|
||||
</ScrollButton>
|
||||
<ScrollButton onClick={scrollBottom}>
|
||||
<FontAwesomeIcon icon={faForward} />
|
||||
</ScrollButton>
|
||||
<ScrollButton onClick={scrollToBottom}>
|
||||
<FontAwesomeIcon icon={faFastForward} />
|
||||
</ScrollButton>
|
||||
</TwinButtons>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
import React, {useState, useEffect, ChangeEvent, useCallback} from 'react';
|
||||
import {faQuestionCircle} from '@fortawesome/free-solid-svg-icons';
|
||||
import {documentationLinks} from 'constants/links';
|
||||
|
||||
import {NewTabLink} from 'components/new-tab-link';
|
||||
import Input from 'components/forms/Input';
|
||||
import {CopyIconButton} from 'components/buttons/copy-icon-button';
|
||||
import {DialogForm, Error} from 'components/modal/modal-form-response/style';
|
||||
|
||||
const DeleteChannelForm = ({setIsValid, alias}: {setIsValid: (isValid: boolean) => void; alias: string}): React.JSX.Element => {
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [enteredAlias, setEnteredAlias] = useState('');
|
||||
|
||||
const validate = useCallback((): boolean => {
|
||||
if (!enteredAlias) {
|
||||
setError('Please enter a channel alias');
|
||||
setIsValid(false);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (enteredAlias !== alias) {
|
||||
setError('Entered alias does not match the channels alias');
|
||||
setIsValid(false);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
setError(null);
|
||||
setIsValid(true);
|
||||
|
||||
return true;
|
||||
}, [enteredAlias, alias, setIsValid]);
|
||||
|
||||
useEffect(() => {
|
||||
if (enteredAlias.length) {
|
||||
validate();
|
||||
}
|
||||
}, [enteredAlias, validate]);
|
||||
|
||||
const handleEnteredAlias = (event: ChangeEvent<HTMLInputElement>): void => {
|
||||
if (error) {
|
||||
setError(null);
|
||||
}
|
||||
|
||||
setEnteredAlias(event.target.value);
|
||||
};
|
||||
|
||||
return (
|
||||
<DialogForm>
|
||||
<h3 className="testId-deleteChannelForm">
|
||||
Delete Channel <NewTabLink link={documentationLinks.deleteChannel} icon={faQuestionCircle} iconColor="black" />
|
||||
</h3>
|
||||
<p>To delete channel, please enter the channel alias:</p>
|
||||
<strong>
|
||||
<CopyIconButton text={alias} quoted />
|
||||
</strong>
|
||||
<Input name="alias" error={!!error} onChange={handleEnteredAlias} value={enteredAlias} />
|
||||
{error && <Error className="error-text testId-displayMessage">{error}</Error>}
|
||||
</DialogForm>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeleteChannelForm;
|
||||
@@ -0,0 +1,111 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
import React, {useState} from 'react';
|
||||
import {useDispatch} from 'react-redux';
|
||||
|
||||
import LoggerFactory from 'services/logger/LoggerFactory';
|
||||
import {deleteChannel} from 'services/Channel.service';
|
||||
import {listChannels} from 'store/action/channels';
|
||||
|
||||
import {transformToPortalError} from 'utility/error-handler';
|
||||
import {deleteChannelErrorMessages} from 'constants/error-messages';
|
||||
|
||||
import {MultiStepModal} from 'components/modal/multi-step-modal';
|
||||
|
||||
import DeleteChannelForm from './delete-channel-form';
|
||||
import {FormResponse} from 'components/modal/modal-form-response';
|
||||
|
||||
interface IDeleteChannelModal {
|
||||
isOpen: boolean;
|
||||
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
channelId: string;
|
||||
alias: string;
|
||||
redirect?: () => void;
|
||||
}
|
||||
|
||||
export const DeleteChannelModal = ({isOpen, setIsOpen, channelId, alias, redirect}: IDeleteChannelModal): React.JSX.Element => {
|
||||
const logger = LoggerFactory.getLogger('components/channel-icon-menu/delete-channel/DeleteChannelModal');
|
||||
const dispatch = useDispatch();
|
||||
const [isFormValid, setIsFormValid] = useState(false);
|
||||
const [isFetching, setIsFetching] = useState(false);
|
||||
const [deleteChannelResponse, setDeleteChannelResponse] = useState<{error: string | boolean | Error; status: string | number; data?: any} | null>(null);
|
||||
const handleSubmit = async () => {
|
||||
if (!isFormValid) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsFetching(true);
|
||||
|
||||
logger.info('Deleting a channel [%s][%s]', alias, channelId);
|
||||
|
||||
const response = await deleteChannel({channelId, alias});
|
||||
|
||||
logger.info('Channel [%s][%s] was successfully deleted', alias, channelId);
|
||||
|
||||
setDeleteChannelResponse({status: 'ok', error: false, data: response});
|
||||
setIsFetching(false);
|
||||
} catch (e) {
|
||||
const {status, message, requestPayload, statusCode} = transformToPortalError(e);
|
||||
|
||||
setIsFetching(false);
|
||||
|
||||
const errorMessage = (deleteChannelErrorMessages as Record<string, string>)[status] || message || deleteChannelErrorMessages['default'];
|
||||
setDeleteChannelResponse({
|
||||
status: statusCode || 'error',
|
||||
error: errorMessage
|
||||
});
|
||||
|
||||
logger.error(`${errorMessage} [%s]`, status, requestPayload);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCloseModal = (): void => {
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
const onDeleteSuccess = async (): Promise<void> => {
|
||||
setIsOpen(false);
|
||||
|
||||
logger.info('Updating the list of channels after the [%s][%s] channel was deleted', alias, channelId);
|
||||
|
||||
await dispatch(listChannels() as any);
|
||||
|
||||
logger.info('The list of channels was updated successfully');
|
||||
|
||||
if (redirect) {
|
||||
redirect();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<MultiStepModal
|
||||
isOpen={isOpen}
|
||||
closeModal={deleteChannelResponse?.status === 'ok' ? onDeleteSuccess : handleCloseModal}
|
||||
steps={[
|
||||
{
|
||||
title: 'Delete Channel',
|
||||
component: <DeleteChannelForm alias={alias} setIsValid={setIsFormValid} />,
|
||||
saveButton: {
|
||||
text: 'Delete Channel',
|
||||
className: 'testId-deleteChannel',
|
||||
disabled: !isFormValid || isFetching,
|
||||
onClick: handleSubmit
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'Delete Channel',
|
||||
component: <FormResponse response={deleteChannelResponse || {error: '', status: '', data: null}} />,
|
||||
cancelDisabled: true,
|
||||
saveButton: {
|
||||
className: 'testId-doneButton',
|
||||
onClick: onDeleteSuccess
|
||||
}
|
||||
}
|
||||
]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeleteChannelModal;
|
||||
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
export {default} from './delete-channel-modal';
|
||||
@@ -0,0 +1,117 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
import React, {useState, useEffect, useCallback} from 'react';
|
||||
import {faQuestionCircle} from '@fortawesome/free-solid-svg-icons';
|
||||
import {CapabilitiesType} from 'constants/capabilities';
|
||||
import {documentationLinks} from 'constants/links';
|
||||
import {Label} from 'components/forms/label';
|
||||
import Checkbox from 'components/forms/Checkbox';
|
||||
import {NewTabLink} from 'components/new-tab-link';
|
||||
import {Dropdown} from 'components/drop-down';
|
||||
import {InputWithTags} from 'components/ui/input-with-tags';
|
||||
import {DialogForm, Error, Options} from 'components/modal/modal-form-response/style';
|
||||
import {Capabilities} from 'components/create-token-components';
|
||||
import Theme from 'theme';
|
||||
|
||||
const forkChannelOption = ['force', 'additive'];
|
||||
const ForkChannelForm = ({
|
||||
setFormData,
|
||||
setIsValid,
|
||||
channelList,
|
||||
alias
|
||||
}: {
|
||||
setFormData: (data: any) => void;
|
||||
setIsValid: (isValid: boolean) => void;
|
||||
channelList: any[];
|
||||
alias: string;
|
||||
}): React.JSX.Element => {
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [tags, setTags] = useState<string[]>([]);
|
||||
const [forkOptions, setForkOptions] = useState<string[]>([]);
|
||||
const [destinationChannelId, setDestinationChannelId] = useState('');
|
||||
const [selectedCapabilities, setSelectedCapabilities] = useState<any[]>([]);
|
||||
const [force, additive] = forkChannelOption;
|
||||
const destinationChannelsList = channelList.filter(channel => channel.alias !== alias);
|
||||
|
||||
const validate = useCallback((): boolean => {
|
||||
if (!destinationChannelId) {
|
||||
setIsValid(false);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
setError(null);
|
||||
setIsValid(true);
|
||||
setFormData({
|
||||
streamCapabilities: selectedCapabilities.map(({value}) => value),
|
||||
streamTags: tags,
|
||||
options: forkOptions,
|
||||
destinationChannelId
|
||||
});
|
||||
|
||||
return true;
|
||||
}, [destinationChannelId, selectedCapabilities, tags, forkOptions, setIsValid, setFormData]);
|
||||
|
||||
useEffect(() => {
|
||||
validate();
|
||||
}, [validate]);
|
||||
|
||||
const handleForkOptions = (value: string) => {
|
||||
if (forkOptions.indexOf(value) > -1) {
|
||||
const currentForkOptions = forkOptions.filter(item => item !== value);
|
||||
|
||||
setForkOptions(currentForkOptions);
|
||||
} else {
|
||||
setForkOptions([...forkOptions, value]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSetDestinationChannelId = (channelAlias: string): void => {
|
||||
const channels = channelList.filter(channel => channel.alias === channelAlias);
|
||||
|
||||
if (channels[0]) {
|
||||
const {channelId} = channels[0];
|
||||
|
||||
setDestinationChannelId(channelId);
|
||||
setError(null);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<DialogForm>
|
||||
<h3 className="testId-forkChannelForm">
|
||||
Fork Channel <NewTabLink link={documentationLinks.forkChannel} icon={faQuestionCircle} iconColor="black" />
|
||||
</h3>
|
||||
<p>
|
||||
Fork <strong>"{alias}"</strong> to destination channel:
|
||||
</p>
|
||||
<Dropdown
|
||||
name={'alias'}
|
||||
label="Destination Channel Alias"
|
||||
items={destinationChannelsList}
|
||||
itemKey={'alias'}
|
||||
onSelect={handleSetDestinationChannelId}
|
||||
className="testId-destinationChannelAlias"
|
||||
/>
|
||||
{error && <Error>Please select a Channel from the list</Error>}
|
||||
<Capabilities
|
||||
label="Capabilities:"
|
||||
labelColor={Theme.colors.gray900}
|
||||
iconColor={Theme.colors.gray900}
|
||||
capabilitiesSetTitle={CapabilitiesType.Forking}
|
||||
selectedItems={selectedCapabilities}
|
||||
setSelectedItems={setSelectedCapabilities}
|
||||
/>
|
||||
<Label text="Options" />
|
||||
<Options>
|
||||
<Checkbox value={force} id={force} onChange={() => handleForkOptions(force)} checked={forkOptions.indexOf(force) > -1} label={force} />
|
||||
<Checkbox value={additive} id={additive} onChange={() => handleForkOptions(additive)} checked={forkOptions.indexOf(additive) > -1} label={additive} />
|
||||
</Options>
|
||||
<Label htmlFor="input-tag" text="Stream Tags" />
|
||||
<InputWithTags id="input-tag" onTagListChange={setTags} defaultValue={tags} />
|
||||
</DialogForm>
|
||||
);
|
||||
};
|
||||
|
||||
export default ForkChannelForm;
|
||||
@@ -0,0 +1,113 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
import React, {useState} from 'react';
|
||||
import {useSelector} from 'react-redux';
|
||||
|
||||
import LoggerFactory from 'services/logger/LoggerFactory';
|
||||
import {forkChannel} from 'services/Channel.service';
|
||||
import {AppStore} from 'store';
|
||||
import {channelsSelector} from 'store/action/channels';
|
||||
|
||||
import {transformToPortalError} from 'utility/error-handler';
|
||||
import {forkChannelErrorMessages} from 'constants/error-messages';
|
||||
|
||||
import {MultiStepModal} from 'components/modal/multi-step-modal';
|
||||
|
||||
import ForkChannelForm from './fork-channel-form';
|
||||
import {FormResponse} from 'components/modal/modal-form-response';
|
||||
|
||||
interface IForkChannelModal {
|
||||
isOpen: boolean;
|
||||
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
channelId: string;
|
||||
alias: string;
|
||||
}
|
||||
|
||||
export const ForkChannelModal = ({isOpen, setIsOpen, channelId, alias}: IForkChannelModal): React.JSX.Element => {
|
||||
const logger = LoggerFactory.getLogger('components/channel-icon-menu/fork-channel/ForkChannelModal');
|
||||
const channelsState = useSelector((state: AppStore) => channelsSelector(state));
|
||||
const channelList = channelsState.channels || [];
|
||||
const [formData, setFormData] = useState({
|
||||
streamCapabilities: [] as string[],
|
||||
streamTags: [] as string[],
|
||||
options: [] as string[],
|
||||
destinationChannelId: ''
|
||||
});
|
||||
const [isFormValid, setIsFormValid] = useState(false);
|
||||
const [isFetching, setIsFetching] = useState(false);
|
||||
const [forkResponse, setForkResponse] = useState<{error: string | boolean | Error; status: string | number; data?: any} | null>(null);
|
||||
const handleSubmit = async () => {
|
||||
if (!isFormValid) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsFetching(true);
|
||||
|
||||
const {streamCapabilities, streamTags, options, destinationChannelId} = formData;
|
||||
|
||||
logger.info('Forking a channel [%s] to [%s]', channelId, destinationChannelId);
|
||||
|
||||
const response = await forkChannel({
|
||||
sourceChannelId: channelId,
|
||||
destinationChannelId,
|
||||
streamCapabilities,
|
||||
streamTags,
|
||||
options
|
||||
});
|
||||
|
||||
logger.info('The channel [%s] was successfully forked to [%s]', channelId, destinationChannelId);
|
||||
|
||||
setForkResponse({
|
||||
status: 'ok',
|
||||
error: false,
|
||||
data: response
|
||||
});
|
||||
setIsFetching(false);
|
||||
} catch (e) {
|
||||
const {status, message, requestPayload, statusCode} = transformToPortalError(e);
|
||||
|
||||
setIsFetching(false);
|
||||
|
||||
const errorMessage = (forkChannelErrorMessages as Record<string, string>)[status] || message || forkChannelErrorMessages['default'];
|
||||
setForkResponse({
|
||||
status: statusCode || 'error',
|
||||
error: errorMessage
|
||||
});
|
||||
|
||||
logger.error(`${errorMessage} [%s]`, status, requestPayload);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCloseModal = (): void => {
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<MultiStepModal
|
||||
isOpen={isOpen}
|
||||
closeModal={handleCloseModal}
|
||||
steps={[
|
||||
{
|
||||
title: 'Fork Channel',
|
||||
component: <ForkChannelForm alias={alias} channelList={channelList} setIsValid={setIsFormValid} setFormData={setFormData} />,
|
||||
saveButton: {
|
||||
text: 'Fork Channel',
|
||||
className: 'testId-forkChannel',
|
||||
disabled: !isFormValid || isFetching,
|
||||
onClick: handleSubmit
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'Fork Channel',
|
||||
component: <FormResponse response={forkResponse || {error: '', status: '', data: null}} />,
|
||||
cancelDisabled: true,
|
||||
saveButton: {className: 'testId-doneButton'}
|
||||
}
|
||||
]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default ForkChannelModal;
|
||||
4
src/components/channel-icon-menu/fork-channel/index.tsx
Normal file
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
export {default} from './fork-channel-modal';
|
||||
65
src/components/channel-icon-menu/index.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
import React, {useState} from 'react';
|
||||
import {IconDefinition} from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
import {IconMenu} from 'components/icon-menu';
|
||||
import {IconMenuPosition} from 'components/icon-menu/types';
|
||||
|
||||
import ForkChannelModal from './fork-channel';
|
||||
import DeleteChannelModal from './delete-channel';
|
||||
import KillChannelModal from './kill-channel';
|
||||
|
||||
interface IChannelIconMenu {
|
||||
data: {
|
||||
name: string;
|
||||
channelId: string;
|
||||
applicationId: string;
|
||||
alias: string;
|
||||
};
|
||||
redirect?: () => void;
|
||||
showTail?: boolean;
|
||||
position?: IconMenuPosition;
|
||||
icon?: IconDefinition;
|
||||
margin?: number;
|
||||
}
|
||||
|
||||
export const ChannelIconMenu = (props: IChannelIconMenu): React.JSX.Element => {
|
||||
const {data, icon, redirect, showTail, position, margin} = props;
|
||||
const {alias, channelId} = data;
|
||||
const [isForkModalOpen, setIsForkModalOpen] = useState(false);
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
const [isKillModalOpen, setIsKillModalOpen] = useState(false);
|
||||
const items = [
|
||||
{
|
||||
value: 'delete',
|
||||
title: 'Delete',
|
||||
className: 'testId-deleteMenuItem',
|
||||
onClick: () => setIsDeleteModalOpen(true)
|
||||
},
|
||||
{
|
||||
value: 'kill',
|
||||
title: 'Kill',
|
||||
className: 'testId-killMenuItem',
|
||||
onClick: () => setIsKillModalOpen(true)
|
||||
},
|
||||
{
|
||||
value: 'fork',
|
||||
title: 'Fork',
|
||||
className: 'testId-forkMenuItem',
|
||||
onClick: () => setIsForkModalOpen(true)
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<IconMenu icon={icon} items={items} showTail={showTail} position={position} margin={margin} />
|
||||
{isForkModalOpen && <ForkChannelModal isOpen={isForkModalOpen} setIsOpen={setIsForkModalOpen} channelId={channelId} alias={alias} />}
|
||||
{isDeleteModalOpen && (
|
||||
<DeleteChannelModal isOpen={isDeleteModalOpen} setIsOpen={setIsDeleteModalOpen} channelId={channelId} alias={alias} redirect={redirect} />
|
||||
)}
|
||||
{isKillModalOpen && <KillChannelModal isOpen={isKillModalOpen} setIsOpen={setIsKillModalOpen} channelId={channelId} alias={alias} />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
4
src/components/channel-icon-menu/kill-channel/index.tsx
Normal file
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
export {default} from './kill-channel-modal';
|
||||
@@ -0,0 +1,137 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
import React, {useState, useEffect, ChangeEvent, useCallback} from 'react';
|
||||
import {faQuestionCircle} from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
import {documentationLinks} from 'constants/links';
|
||||
|
||||
import Checkbox from 'components/forms/Checkbox';
|
||||
import {Label} from 'components/label';
|
||||
import {NewTabLink} from 'components/new-tab-link';
|
||||
import {DialogForm, Error, Options} from 'components/modal/modal-form-response/style';
|
||||
import {CopyIconButton} from 'components/buttons/copy-icon-button';
|
||||
import {Tooltip, Position} from 'components/tooltip';
|
||||
import {Input} from 'components/ui';
|
||||
|
||||
const keepStreams = 'keep-streams';
|
||||
const keepStreamsTooltipMessage = 'Keeps the removed streams alive';
|
||||
const destroyRequired = 'destroy-required';
|
||||
const destroyRequiredTooltipMessage = 'Returns an error if destroying of a stream fails';
|
||||
const defaultReason = 'portal:killed';
|
||||
const KillChannelForm = ({
|
||||
setFormData,
|
||||
setIsValid,
|
||||
alias
|
||||
}: {
|
||||
setFormData: (data: any) => void;
|
||||
setIsValid: (isValid: boolean) => void;
|
||||
alias: string;
|
||||
}): React.JSX.Element => {
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [enteredAlias, setEnteredAlias] = useState('');
|
||||
const [reason, setReason] = useState(defaultReason);
|
||||
const [options, setOptions] = useState<string[]>([]);
|
||||
|
||||
const validate = useCallback((): boolean => {
|
||||
if (!enteredAlias) {
|
||||
setError('Please enter a channel alias');
|
||||
setIsValid(false);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (enteredAlias !== alias) {
|
||||
setError('Entered alias does not match the channels alias');
|
||||
setIsValid(false);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
setError(null);
|
||||
setIsValid(true);
|
||||
setFormData({
|
||||
reason,
|
||||
options,
|
||||
enteredAlias
|
||||
});
|
||||
|
||||
return true;
|
||||
}, [enteredAlias, alias, reason, options, setIsValid, setFormData, setError]);
|
||||
|
||||
useEffect(() => {
|
||||
if (enteredAlias.length) {
|
||||
validate();
|
||||
}
|
||||
}, [enteredAlias, validate]);
|
||||
|
||||
const handleAlias = (event: ChangeEvent<HTMLInputElement>): void => {
|
||||
if (error) {
|
||||
setError(null);
|
||||
}
|
||||
|
||||
setEnteredAlias(event.target.value);
|
||||
};
|
||||
|
||||
const handleKillOptions = (value: string) => {
|
||||
if (options.indexOf(value) > -1) {
|
||||
const currentOptions = options.filter(item => item !== value);
|
||||
|
||||
setOptions(currentOptions);
|
||||
} else {
|
||||
setOptions([...options, value]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleReason = (event: ChangeEvent<HTMLInputElement>): void => {
|
||||
if (error) {
|
||||
setError(null);
|
||||
}
|
||||
|
||||
setReason(event.target.value);
|
||||
};
|
||||
|
||||
return (
|
||||
<DialogForm>
|
||||
<h3 className="testId-killChannelForm">
|
||||
Kill Channel <NewTabLink link={documentationLinks.killChannel} icon={faQuestionCircle} iconColor="black" />
|
||||
</h3>
|
||||
<p>Terminates all streams and removes them from the channel.</p>
|
||||
<p>To kill the channel, please enter the channel alias:</p>
|
||||
<strong>
|
||||
<CopyIconButton text={alias} quoted />
|
||||
</strong>
|
||||
<Input error={!!error} onChange={handleAlias} value={enteredAlias} name="alias" />
|
||||
{error && (
|
||||
<Error className="error-text">
|
||||
Please enter the channel alias <i>{alias}</i>
|
||||
</Error>
|
||||
)}
|
||||
<h5>Kill options:</h5>
|
||||
<Options>
|
||||
<Tooltip position={Position.Bottom} message={keepStreamsTooltipMessage}>
|
||||
<Checkbox
|
||||
value={keepStreams}
|
||||
id={keepStreams}
|
||||
onChange={() => handleKillOptions(keepStreams)}
|
||||
checked={options.indexOf(keepStreams) > -1}
|
||||
label={keepStreams}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip position={Position.Bottom} message={destroyRequiredTooltipMessage}>
|
||||
<Checkbox
|
||||
value={destroyRequired}
|
||||
id={destroyRequired}
|
||||
onChange={() => handleKillOptions(destroyRequired)}
|
||||
checked={options.indexOf(destroyRequired) > -1}
|
||||
label={destroyRequired}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Options>
|
||||
<Label htmlFor="reason" text="Reason:" />
|
||||
<Input id="reason" error={!!error} onChange={handleReason} value={reason} />
|
||||
</DialogForm>
|
||||
);
|
||||
};
|
||||
|
||||
export default KillChannelForm;
|
||||
@@ -0,0 +1,106 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
import React, {useState} from 'react';
|
||||
|
||||
import LoggerFactory from 'services/logger/LoggerFactory';
|
||||
import {killChannel} from 'services/Channel.service';
|
||||
|
||||
import {transformToPortalError} from 'utility/error-handler';
|
||||
import {killChannelErrorMessages} from 'constants/error-messages';
|
||||
|
||||
import {MultiStepModal} from 'components/modal/multi-step-modal';
|
||||
|
||||
import KillChannelForm from './kill-channel-form';
|
||||
import {FormResponse} from 'components/modal/modal-form-response';
|
||||
|
||||
interface IKillChannelModal {
|
||||
isOpen: boolean;
|
||||
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
channelId: string;
|
||||
alias: string;
|
||||
}
|
||||
|
||||
export const KillChannelModal = ({isOpen, setIsOpen, channelId, alias}: IKillChannelModal): React.JSX.Element => {
|
||||
const logger = LoggerFactory.getLogger('components/channel-icon-menu/kill-channel/KillChannelModal');
|
||||
const [formData, setFormData] = useState({
|
||||
reason: '',
|
||||
options: [] as string[],
|
||||
enteredAlias: ''
|
||||
});
|
||||
const [isFormValid, setIsFormValid] = useState(false);
|
||||
const [isFetching, setIsFetching] = useState(false);
|
||||
const [killResponse, setKillResponse] = useState<{error: string | boolean | Error; status: string | number; data?: any} | null>(null);
|
||||
const handleSubmit = async () => {
|
||||
if (!isFormValid) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsFetching(true);
|
||||
|
||||
const {reason, options} = formData;
|
||||
|
||||
logger.info('Killing a channel [%s] with reason [%s]', channelId, reason);
|
||||
|
||||
const response = await killChannel({
|
||||
channelId,
|
||||
reason,
|
||||
options,
|
||||
enteredAlias: formData.enteredAlias
|
||||
});
|
||||
|
||||
logger.info('The channel [%s] was successfully killed', channelId);
|
||||
|
||||
setKillResponse({
|
||||
status: 'ok',
|
||||
error: false,
|
||||
data: response
|
||||
});
|
||||
setIsFetching(false);
|
||||
} catch (e) {
|
||||
const {status, message, requestPayload, statusCode} = transformToPortalError(e);
|
||||
|
||||
setIsFetching(false);
|
||||
|
||||
const errorMessage = (killChannelErrorMessages as Record<string, string>)[status] || message || killChannelErrorMessages['default'];
|
||||
setKillResponse({
|
||||
status: statusCode || 'error',
|
||||
error: errorMessage
|
||||
});
|
||||
|
||||
logger.error(`${errorMessage} [%s]`, status, requestPayload);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCloseModal = (): void => {
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<MultiStepModal
|
||||
isOpen={isOpen}
|
||||
closeModal={handleCloseModal}
|
||||
steps={[
|
||||
{
|
||||
title: 'Kill Channel',
|
||||
component: <KillChannelForm alias={alias} setIsValid={setIsFormValid} setFormData={setFormData} />,
|
||||
saveButton: {
|
||||
text: 'Kill Channel',
|
||||
className: 'testId-killChannel',
|
||||
disabled: !isFormValid || isFetching,
|
||||
onClick: handleSubmit
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'Kill Channel',
|
||||
component: <FormResponse response={killResponse || {error: '', status: '', data: null}} />,
|
||||
cancelDisabled: true,
|
||||
saveButton: {className: 'testId-doneButton'}
|
||||
}
|
||||
]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default KillChannelModal;
|
||||
66
src/components/create-token-components/capabilities.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
import {JSX, useEffect, useState} from 'react';
|
||||
|
||||
import {faQuestionCircle} from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
import {documentationLinks} from 'constants/links';
|
||||
import {CapabilitiesType, capabilities} from 'constants/capabilities';
|
||||
|
||||
import {AdvancedSelect, IAdvancedSelectItem} from 'components/ui/advanced-select';
|
||||
import {NewTabLink} from 'components/new-tab-link';
|
||||
import Theme from 'theme';
|
||||
|
||||
interface ICapabilities {
|
||||
selectedItems: IAdvancedSelectItem[];
|
||||
setSelectedItems: (items: IAdvancedSelectItem[]) => void;
|
||||
data?: IAdvancedSelectItem[];
|
||||
label?: string;
|
||||
labelColor?: string;
|
||||
iconColor?: string;
|
||||
capabilitiesSetTitle?: string;
|
||||
className?: string;
|
||||
allowMultiple?: boolean;
|
||||
}
|
||||
|
||||
export const Capabilities = ({
|
||||
label,
|
||||
labelColor = Theme.colors.white,
|
||||
iconColor = Theme.colors.white,
|
||||
data = capabilities,
|
||||
selectedItems,
|
||||
setSelectedItems,
|
||||
capabilitiesSetTitle = '',
|
||||
className,
|
||||
allowMultiple = true
|
||||
}: ICapabilities): JSX.Element => {
|
||||
const [capabilitiesSet, setCapabilitiesSet] = useState(data);
|
||||
|
||||
useEffect(() => {
|
||||
const filteredCapabilities = data.filter(capability => {
|
||||
return capabilitiesSetTitle ? capability.type.includes(capabilitiesSetTitle) : true;
|
||||
});
|
||||
|
||||
setCapabilitiesSet(filteredCapabilities);
|
||||
}, [capabilitiesSetTitle]);
|
||||
|
||||
return (
|
||||
<AdvancedSelect
|
||||
allowMultiple={allowMultiple}
|
||||
label={label}
|
||||
labelColor={labelColor}
|
||||
labelIcon={
|
||||
capabilitiesSetTitle !== CapabilitiesType.Quality ? (
|
||||
<NewTabLink link={documentationLinks.supportedStreamCapabilities} icon={faQuestionCircle} iconColor={iconColor} />
|
||||
) : undefined
|
||||
}
|
||||
data={capabilitiesSet}
|
||||
selectedItems={selectedItems}
|
||||
setSelectedItems={setSelectedItems}
|
||||
className={className}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default Capabilities;
|
||||
8
src/components/create-token-components/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
export {default as Capabilities} from './capabilities';
|
||||
export {default as ValidityTimeComponent} from './validity-time';
|
||||
export * from './validity-time';
|
||||
export {default as LabelIconTooltip} from './label-icon-tooltip';
|
||||
export * from './styles';
|
||||
37
src/components/create-token-components/index.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
import React from 'react';
|
||||
import {AdvancedSelect, IAdvancedSelectItem} from '../ui/advanced-select';
|
||||
import {Label} from '../forms/label';
|
||||
|
||||
interface CapabilitiesProps {
|
||||
label: string;
|
||||
labelColor?: string;
|
||||
iconColor?: string;
|
||||
capabilitiesSetTitle: string;
|
||||
selectedItems: IAdvancedSelectItem[];
|
||||
setSelectedItems: (items: IAdvancedSelectItem[]) => void;
|
||||
}
|
||||
|
||||
export const Capabilities: React.FC<CapabilitiesProps> = ({label, labelColor, iconColor, capabilitiesSetTitle, selectedItems, setSelectedItems}) => {
|
||||
// Mock capabilities data - in a real app this would come from constants/capabilities
|
||||
const mockCapabilities: IAdvancedSelectItem[] = [
|
||||
{value: 'streaming', text: 'Streaming'},
|
||||
{value: 'recording', text: 'Recording'},
|
||||
{value: 'analytics', text: 'Analytics'},
|
||||
{value: 'transcoding', text: 'Transcoding'}
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Label text={label} />
|
||||
<AdvancedSelect
|
||||
items={mockCapabilities}
|
||||
selectedItems={selectedItems}
|
||||
setSelectedItems={setSelectedItems}
|
||||
placeholder={`Select ${capabilitiesSetTitle} capabilities...`}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
import React from 'react';
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
|
||||
import {faQuestionCircle, IconDefinition} from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
import {Tooltip, Position} from 'components/tooltip';
|
||||
|
||||
interface ILabelIconTooltip {
|
||||
message: string;
|
||||
icon?: IconDefinition;
|
||||
position?: Position;
|
||||
}
|
||||
|
||||
export const LabelIconTooltip = ({message, icon, position}: ILabelIconTooltip): React.JSX.Element => (
|
||||
<Tooltip position={position || Position.Right} message={message} width={300}>
|
||||
<FontAwesomeIcon icon={icon || faQuestionCircle} />
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
export default LabelIconTooltip;
|
||||
57
src/components/create-token-components/styles.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
import * as styled from 'styled-components';
|
||||
|
||||
import {theme} from 'components/shared/theme';
|
||||
import {RadioGroup} from 'components/buttons/radio-button/style';
|
||||
// import Input from 'components/forms/Input';
|
||||
|
||||
const {spacing, colors} = theme;
|
||||
|
||||
export const CreateTokenContainer = styled.default.div`
|
||||
margin: 0.5rem 0 0;
|
||||
`;
|
||||
|
||||
export const FormWell = styled.default.div`
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export const RadioButtonContainer = styled.default.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-top: ${spacing.small} 0;
|
||||
label {
|
||||
font-size: 14px;
|
||||
color: ${colors.white};
|
||||
}
|
||||
${RadioGroup}{
|
||||
flex-direction: column;
|
||||
.reason-input {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const InputGroup = styled.default.div`
|
||||
margin: 0 1rem 1rem 0;
|
||||
input {
|
||||
width: 200px;
|
||||
padding-left: ${spacing.small};
|
||||
}
|
||||
> label {
|
||||
display: block;
|
||||
}
|
||||
select {
|
||||
width: 200px;
|
||||
}
|
||||
`;
|
||||
|
||||
export const RowsWrapper = styled.default.div`
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
`;
|
||||
56
src/components/create-token-components/validity-time.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
import React, {Fragment} from 'react';
|
||||
|
||||
import moment from 'moment';
|
||||
|
||||
import {ISelectItemWithClassOptions} from 'interfaces';
|
||||
|
||||
import RadioButtonGroup from 'components/buttons/radio-button';
|
||||
import {Label} from 'components/label';
|
||||
|
||||
import {RadioButtonContainer} from '.';
|
||||
|
||||
export const validityTimeOptions: ISelectItemWithClassOptions<string> = {
|
||||
hour: {
|
||||
label: '1 Hour',
|
||||
value: `${moment.duration(1, 'hour').asSeconds()}`
|
||||
},
|
||||
day: {
|
||||
label: '1 Day',
|
||||
value: `${moment.duration(1, 'day').asSeconds()}`
|
||||
},
|
||||
week: {
|
||||
label: '1 Week',
|
||||
value: `${moment.duration(1, 'week').asSeconds()}`
|
||||
},
|
||||
month: {
|
||||
label: '1 Month',
|
||||
value: `${moment.duration(1, 'month').asSeconds()}`
|
||||
},
|
||||
year: {
|
||||
label: '1 Year',
|
||||
value: `${moment.duration(1, 'year').asSeconds()}`
|
||||
},
|
||||
decade: {
|
||||
label: '10 Years',
|
||||
value: `${moment.duration(10, 'year').asSeconds()}`
|
||||
}
|
||||
};
|
||||
|
||||
interface IValidityTimeComponent {
|
||||
onChange: (value: string) => void;
|
||||
currentValue: string;
|
||||
}
|
||||
|
||||
export const ValidityTimeComponent = ({currentValue, onChange}: IValidityTimeComponent): React.JSX.Element => (
|
||||
<Fragment>
|
||||
<Label text="Valid For:" color="white" />
|
||||
<RadioButtonContainer className="testId-expirationTime">
|
||||
<RadioButtonGroup items={Object.values(validityTimeOptions)} currentValue={currentValue} handleOnChange={onChange} />
|
||||
</RadioButtonContainer>
|
||||
</Fragment>
|
||||
);
|
||||
|
||||
export default ValidityTimeComponent;
|
||||
40
src/components/date-render-component/index.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
import {JSX, useEffect, useState} from 'react';
|
||||
import {useSelector} from 'react-redux';
|
||||
import {Moment} from 'moment';
|
||||
import {AppStore} from 'store';
|
||||
import {getTimezoneAbbreviation, isoTimeFormat} from 'utility/date';
|
||||
|
||||
export const DateComponent = ({date, className}: {date: Moment; className?: string}): JSX.Element => {
|
||||
const preferredTimeFormat = useSelector((state: AppStore) => state.preferredTimeFormat.timeFormat);
|
||||
const isUTC = preferredTimeFormat === 'utc';
|
||||
const utcDate = date.isValid() ? date.format(`${isoTimeFormat} UTC`) : '';
|
||||
const localDate = date.isValid() ? `${date.clone().local().format(isoTimeFormat)} ${getTimezoneAbbreviation(date.toDate())}` : '';
|
||||
const defaultDate = isUTC ? utcDate : localDate;
|
||||
const hoverDate = isUTC ? localDate : utcDate;
|
||||
const [currentDate, setCurrentDate] = useState(defaultDate);
|
||||
|
||||
useEffect(() => {
|
||||
setCurrentDate(defaultDate);
|
||||
}, [date, preferredTimeFormat]);
|
||||
|
||||
const onMouseEnter = () => {
|
||||
setCurrentDate(hoverDate);
|
||||
};
|
||||
|
||||
const onMouseOut = () => {
|
||||
setCurrentDate(defaultDate);
|
||||
};
|
||||
|
||||
const onBlur = () => {
|
||||
setCurrentDate(defaultDate);
|
||||
};
|
||||
|
||||
return (
|
||||
<p onMouseEnter={onMouseEnter} onMouseOut={onMouseOut} onBlur={onBlur} className={className}>
|
||||
{currentDate}
|
||||
</p>
|
||||
);
|
||||
};
|
||||
206
src/components/drop-down/index.tsx
Normal file
@@ -0,0 +1,206 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
import React, {useState, useEffect, useRef} from 'react';
|
||||
|
||||
import {IChannel} from 'interfaces';
|
||||
import {Label} from 'components/label';
|
||||
|
||||
import {DropdownContainer, DropdownInput, DropdownMenu, DropdownMenuItem} from './style';
|
||||
|
||||
const keys = ['ArrowDown', 'ArrowUp', 'Enter', 'Escape'];
|
||||
|
||||
interface IDropdown {
|
||||
itemKey: string;
|
||||
searchTerm?: string;
|
||||
onSelect: (key: string) => void;
|
||||
items: IChannel[];
|
||||
label: string;
|
||||
name: string;
|
||||
className?: string;
|
||||
}
|
||||
const maxNumOfItemsShown = 30;
|
||||
|
||||
export const Dropdown = (props: IDropdown): React.JSX.Element => {
|
||||
const {itemKey, searchTerm, onSelect, items, label, name, className} = props;
|
||||
const [showDropdownMenu, setShowDropdownMenu] = useState(false);
|
||||
const [input, setInput] = useState(searchTerm || '');
|
||||
const [filteredItems, setFilteredItems] = useState(items);
|
||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||
const allItemsRef = useRef<any[]>([]);
|
||||
const parentElementRef = useRef<HTMLDivElement | null>(null);
|
||||
const scrollSelectedItemInView = (index: number) => {
|
||||
if (!parentElementRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dropdownMenu = parentElementRef.current;
|
||||
const menuItems = allItemsRef.current;
|
||||
|
||||
if (!menuItems) {
|
||||
return;
|
||||
}
|
||||
|
||||
const menuItem = menuItems[index];
|
||||
|
||||
if (!menuItem) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isOutOfUpperView = menuItem.offsetTop < dropdownMenu.scrollTop;
|
||||
const isOutOfLowerView = menuItem.offsetTop + menuItem.clientHeight > dropdownMenu.scrollTop + dropdownMenu.clientHeight;
|
||||
|
||||
if (isOutOfUpperView) {
|
||||
dropdownMenu.scrollTop = menuItem.offsetTop;
|
||||
} else if (isOutOfLowerView) {
|
||||
dropdownMenu.scrollTop = menuItem.offsetTop + menuItem.clientHeight - dropdownMenu.clientHeight;
|
||||
}
|
||||
};
|
||||
|
||||
const setSelectInput = (value: string) => {
|
||||
setInput(value);
|
||||
onSelect(value);
|
||||
};
|
||||
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (!parentElementRef.current || parentElementRef.current.contains(event.target as Node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!filteredItems.length) {
|
||||
setSelectInput('');
|
||||
}
|
||||
|
||||
setShowDropdownMenu(false);
|
||||
onSelect(input);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
scrollSelectedItemInView(selectedIndex);
|
||||
}, [selectedIndex]);
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('click', handleClickOutside, true);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('click', handleClickOutside, true);
|
||||
};
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const filteredItems = items.filter(item => {
|
||||
if ((item as any)[itemKey].toLowerCase().indexOf(input.toLowerCase()) > -1) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
if (filteredItems.length > 0) {
|
||||
setSelectedIndex(0);
|
||||
}
|
||||
|
||||
setFilteredItems(filteredItems);
|
||||
}, [input, items, itemKey]);
|
||||
|
||||
const selectItemInFocusBy = (offset: number) => {
|
||||
const lastIndex = filteredItems.length - 1;
|
||||
const nextIndex = selectedIndex + offset;
|
||||
|
||||
if (nextIndex > lastIndex) {
|
||||
setSelectedIndex(0);
|
||||
} else if (nextIndex < 0) {
|
||||
setSelectedIndex(lastIndex);
|
||||
} else {
|
||||
setSelectedIndex(nextIndex);
|
||||
}
|
||||
};
|
||||
|
||||
const handleOnKeyDown = (event: React.KeyboardEvent) => {
|
||||
if (keys.indexOf(event.key) === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [arrDown, arrUp, enter, escape] = keys;
|
||||
const moves = {
|
||||
[arrDown]: 1,
|
||||
[arrUp]: -1
|
||||
};
|
||||
const move = moves[event.key];
|
||||
|
||||
if (move !== undefined) {
|
||||
event.preventDefault();
|
||||
selectItemInFocusBy(move);
|
||||
}
|
||||
|
||||
if (event.key === enter) {
|
||||
if (filteredItems[selectedIndex]) {
|
||||
setSelectInput((filteredItems[selectedIndex] as any)[itemKey]);
|
||||
setShowDropdownMenu(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (event.key === escape) {
|
||||
event.preventDefault();
|
||||
setShowDropdownMenu(false);
|
||||
}
|
||||
};
|
||||
|
||||
const selectItem = (index: number) => {
|
||||
setSelectInput((filteredItems[index] as any)[itemKey]);
|
||||
setSelectedIndex(index);
|
||||
setShowDropdownMenu(false);
|
||||
};
|
||||
|
||||
const generateMenuOptions = () => {
|
||||
return filteredItems.length ? (
|
||||
filteredItems.slice(0, maxNumOfItemsShown).map((item, index) => {
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
ref={ref => {
|
||||
allItemsRef.current[index] = ref;
|
||||
}}
|
||||
selected={selectedIndex === index}
|
||||
key={`dropdown-menu-item-${index}`}
|
||||
onClick={() => selectItem(index)}>
|
||||
{(item as any)[itemKey]}
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<DropdownMenuItem disabled={true}>No results found</DropdownMenuItem>
|
||||
);
|
||||
};
|
||||
|
||||
const handleInput = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const {target} = event;
|
||||
setInput(target.value);
|
||||
|
||||
if (!showDropdownMenu) {
|
||||
setShowDropdownMenu(true);
|
||||
}
|
||||
};
|
||||
|
||||
const toggleDropdownMenu = () => setShowDropdownMenu(!showDropdownMenu);
|
||||
|
||||
return (
|
||||
<DropdownContainer>
|
||||
<Label htmlFor="autocomplete" text={label} />
|
||||
<DropdownInput
|
||||
onKeyDown={handleOnKeyDown}
|
||||
showMenu={showDropdownMenu}
|
||||
autoComplete="off"
|
||||
name={name}
|
||||
onChange={handleInput}
|
||||
value={input}
|
||||
onClick={toggleDropdownMenu}
|
||||
className={className}
|
||||
/>
|
||||
{showDropdownMenu && (
|
||||
<DropdownMenu ref={parentElementRef} className="testId-generatedMenuOptions">
|
||||
{generateMenuOptions()}
|
||||
</DropdownMenu>
|
||||
)}
|
||||
</DropdownContainer>
|
||||
);
|
||||
};
|
||||
61
src/components/drop-down/style.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
import * as styled from 'styled-components';
|
||||
import Input from 'components/forms/Input';
|
||||
import Theme from 'theme';
|
||||
|
||||
const {spacing, colors, typography, primaryBorderRadius} = Theme;
|
||||
|
||||
export const DropdownContainer = styled.default.div`
|
||||
width: 100%;
|
||||
position: relative;
|
||||
margin: ${spacing.xSmall} 0;
|
||||
`;
|
||||
|
||||
export const DropdownInput = styled.default(Input)<{showMenu?: boolean}>`
|
||||
${({showMenu}) =>
|
||||
showMenu &&
|
||||
styled.css`
|
||||
border-bottom-left-radius: 0px;
|
||||
border-bottom-right-radius: 0px;
|
||||
border-bottom-width: 0;
|
||||
`}
|
||||
`;
|
||||
|
||||
export const DropdownMenu = styled.default.div`
|
||||
width: 100%;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
top: calc(100% - 4px);
|
||||
position: absolute;
|
||||
height: auto;
|
||||
max-height: 210px;
|
||||
border: 1px solid ${colors.gray400};
|
||||
border-top-width: 0;
|
||||
z-index: 4;
|
||||
background-color: ${colors.white};
|
||||
border-bottom-left-radius: ${primaryBorderRadius};
|
||||
border-bottom-right-radius: ${primaryBorderRadius};
|
||||
`;
|
||||
|
||||
export const DropdownMenuItem = styled.default.div<{
|
||||
active?: boolean;
|
||||
disabled?: boolean;
|
||||
selected?: boolean;
|
||||
}>`
|
||||
line-height: ${spacing.large};
|
||||
padding: ${spacing.xsmall} ${spacing.medium};
|
||||
font-size: ${typography.fontSizeS};
|
||||
word-wrap: break-word;
|
||||
${({active, selected, disabled}) => styled.css`
|
||||
background-color: ${active || selected ? colors.gray300 : 'transparent'};
|
||||
font-weight: ${selected ? 'bold' : 'normal'};
|
||||
color: ${disabled ? colors.gray500 : colors.gray900}
|
||||
|
||||
:hover{
|
||||
background-color: ${disabled ? colors.white : colors.gray300};
|
||||
cursor: ${disabled ? 'text' : 'pointer'};
|
||||
}
|
||||
`}
|
||||
`;
|
||||
12
src/components/error-renderer/index.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
import {toast} from 'react-toastify';
|
||||
|
||||
export const toastErrorRenderer = (error: string): void => {
|
||||
toast.error(String(error), {
|
||||
autoClose: 10000,
|
||||
draggable: false,
|
||||
hideProgressBar: true
|
||||
});
|
||||
};
|
||||
23
src/components/error-renderer/style.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
import * as styled from 'styled-components';
|
||||
import Theme from 'theme';
|
||||
|
||||
const {headerAllowance, dangerColor, typography} = Theme;
|
||||
|
||||
export const Error = styled.default.div`
|
||||
height: ${window.innerHeight - headerAllowance}px;
|
||||
width: 100%;
|
||||
display: grid;
|
||||
align-content: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
color: ${dangerColor};
|
||||
font-size: ${typography.fontSizeL};
|
||||
`;
|
||||
|
||||
export const ErrorSubMessage = styled.default.div`
|
||||
font-size: ${typography.fontSizeS};
|
||||
color: ${dangerColor};
|
||||
`;
|
||||
81
src/components/forms/Checkbox.tsx
Normal file
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
import React, {ChangeEvent, MouseEvent} from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import {Label} from 'components/label';
|
||||
import {theme} from 'components/shared/theme';
|
||||
|
||||
const {colors, spacing} = theme;
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
label {
|
||||
margin-left: ${spacing.xxSmall};
|
||||
margin-right: ${spacing.xxSmall};
|
||||
}
|
||||
`;
|
||||
const CheckboxContainer = styled.div`
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
outline: none;
|
||||
height: 1rem;
|
||||
`;
|
||||
const Icon = styled.svg`
|
||||
fill: none;
|
||||
stroke: ${colors.black};
|
||||
stroke-width: 2px;
|
||||
`;
|
||||
const HiddenCheckbox = styled.input.attrs({type: 'checkbox'})`
|
||||
border: 0;
|
||||
clip: rect(0 0 0 0);
|
||||
clip-path: inset(50%);
|
||||
height: 1px;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
position: absolute;
|
||||
white-space: nowrap;
|
||||
width: 1px;
|
||||
`;
|
||||
const StyledCheckbox = styled.div<{checked?: boolean}>`
|
||||
display: inline-block;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
background: ${({checked}) => (checked ? colors.red : colors.transparent)};
|
||||
border-radius: 3px;
|
||||
transition: all 150ms;
|
||||
outline: none;
|
||||
border: 1px solid ${({checked}) => (checked ? 'none' : colors.gray400)};
|
||||
|
||||
${Icon} {
|
||||
visibility: ${({checked}) => (checked ? 'visible' : 'hidden')};
|
||||
}
|
||||
`;
|
||||
const Checkbox = (props: {
|
||||
value: string;
|
||||
id?: string;
|
||||
checked?: boolean;
|
||||
onChange: (event: ChangeEvent<HTMLInputElement> | MouseEvent<HTMLDivElement>) => void;
|
||||
label?: string;
|
||||
}): React.JSX.Element => {
|
||||
const {checked, onChange, label, id} = props;
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<CheckboxContainer>
|
||||
<HiddenCheckbox {...props} />
|
||||
<StyledCheckbox onClick={onChange} checked={checked}>
|
||||
<Icon viewBox="0 0 24 24">
|
||||
<polyline points="20 6 9 17 4 12" />
|
||||
</Icon>
|
||||
</StyledCheckbox>
|
||||
</CheckboxContainer>
|
||||
{label && <Label htmlFor={id || label} text={label} />}
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default Checkbox;
|
||||
74
src/components/forms/Checkbox/index.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
import React, {ChangeEvent} from 'react';
|
||||
import styled from 'styled-components';
|
||||
import {theme} from 'components/shared/theme';
|
||||
|
||||
const CheckboxContainer = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: ${theme.spacing.small};
|
||||
`;
|
||||
|
||||
const HiddenCheckbox = styled.input.attrs({type: 'checkbox'})`
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
cursor: pointer;
|
||||
height: 0;
|
||||
width: 0;
|
||||
`;
|
||||
|
||||
const StyledCheckbox = styled.div<{checked: boolean}>`
|
||||
display: inline-block;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background: ${props => (props.checked ? theme.colors.red : theme.colors.white)};
|
||||
border: 2px solid ${theme.colors.gray400};
|
||||
border-radius: 3px;
|
||||
transition: all 150ms;
|
||||
cursor: pointer;
|
||||
margin-right: ${theme.spacing.small};
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: relative;
|
||||
display: ${props => (props.checked ? 'block' : 'none')};
|
||||
left: 3px;
|
||||
top: 0px;
|
||||
width: 4px;
|
||||
height: 8px;
|
||||
border: solid ${theme.colors.white};
|
||||
border-width: 0 2px 2px 0;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
`;
|
||||
|
||||
const Label = styled.label`
|
||||
cursor: pointer;
|
||||
color: ${theme.colors.gray900};
|
||||
font-size: ${theme.typography.primaryFontSize};
|
||||
user-select: none;
|
||||
`;
|
||||
|
||||
interface CheckboxProps {
|
||||
id: string;
|
||||
checked: boolean;
|
||||
onChange: (event: ChangeEvent<HTMLInputElement>) => void;
|
||||
label?: string;
|
||||
value?: string;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const Checkbox: React.FC<CheckboxProps> = ({id, checked, onChange, label, value, disabled = false, className}) => {
|
||||
return (
|
||||
<CheckboxContainer className={className}>
|
||||
<HiddenCheckbox id={id} checked={checked} onChange={onChange} value={value} disabled={disabled} />
|
||||
<StyledCheckbox checked={checked} />
|
||||
{label && <Label htmlFor={id}>{label}</Label>}
|
||||
</CheckboxContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default Checkbox;
|
||||
117
src/components/forms/Input.tsx
Normal file
@@ -0,0 +1,117 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
import React, {ChangeEvent, forwardRef, ForwardedRef, InputHTMLAttributes} from 'react';
|
||||
import * as styled from 'styled-components';
|
||||
import Theme from 'theme';
|
||||
import {Label} from './label';
|
||||
|
||||
export interface IInput extends InputHTMLAttributes<HTMLInputElement> {
|
||||
onChange?: (event: ChangeEvent<HTMLInputElement>) => void;
|
||||
error?: boolean;
|
||||
icon?: React.JSX.Element;
|
||||
imagePath?: string;
|
||||
imageAltText?: string;
|
||||
label?: string;
|
||||
labelColor?: string;
|
||||
labelIcon?: React.JSX.Element;
|
||||
labelClassName?: string;
|
||||
helperText?: string;
|
||||
helperTextClassName?: string;
|
||||
width?: number | string;
|
||||
}
|
||||
|
||||
const {colors, typography, formFieldWidth, formFieldMaxWidth, primaryBorderColor, primaryBorderRadius, primaryInputHeight, inputIconWidth, spacing} = Theme;
|
||||
|
||||
export const InputElement = styled.default.input<IInput>`
|
||||
background-color: ${colors.white};
|
||||
border: 1px solid ${({error}) => (error ? colors.lightRed : primaryBorderColor)};
|
||||
border-radius: ${primaryBorderRadius};
|
||||
display: block;
|
||||
font-size: ${typography.primaryFontSize};
|
||||
height: ${primaryInputHeight};
|
||||
line-height: ${typography.primaryLineHeight};
|
||||
outline: none;
|
||||
padding: ${spacing.small} ${({icon, imagePath}) => (icon || imagePath ? spacing.xlarge : spacing.small)};
|
||||
transition: border-color .15s ease-in-out, box-shadow .15s ease-in-out;
|
||||
background-position: 1rem center;
|
||||
background-repeat: no-repeat;
|
||||
width: inherit;
|
||||
opacity: ${({disabled}) => (disabled ? 0.8 : 1)};
|
||||
cursor: ${({disabled}) => disabled && 'not-allowed'};
|
||||
-webkit-text-fill-color: ${({disabled}) => disabled && colors.gray800};
|
||||
`;
|
||||
const HelperText = styled.default.p<IInput>`
|
||||
color: ${({error}) => (error ? colors.lightRed : colors.gray600)};
|
||||
font-weight: 400;
|
||||
font-size: ${typography.fontSizeS};
|
||||
margin-top: ${spacing.xxSmall};
|
||||
`;
|
||||
const ImageWrapper = styled.default.div`
|
||||
width: ${inputIconWidth}px;
|
||||
height: ${inputIconWidth}px;
|
||||
z-index: 1;
|
||||
top: calc(50% - ${inputIconWidth / 2}px);
|
||||
left: 8px;
|
||||
position: absolute;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
& img, svg {
|
||||
width: ${inputIconWidth}px;
|
||||
height: ${inputIconWidth}px;
|
||||
}
|
||||
`;
|
||||
const InputWrapper = styled.default.div`
|
||||
width: 100%;
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
export const InputComponentWrapper = styled.default.div<IInput>`
|
||||
position: relative;
|
||||
width: ${({width}) => (width && isNaN(+width) ? width : (width || formFieldWidth) + 'px')};
|
||||
max-width: ${formFieldMaxWidth}px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
`;
|
||||
export const InputComponent = forwardRef(
|
||||
(
|
||||
{
|
||||
label,
|
||||
labelColor,
|
||||
labelIcon,
|
||||
labelClassName,
|
||||
icon,
|
||||
imagePath,
|
||||
imageAltText = '',
|
||||
helperText,
|
||||
helperTextClassName,
|
||||
error,
|
||||
width,
|
||||
disabled,
|
||||
name,
|
||||
...props
|
||||
}: IInput,
|
||||
ref: ForwardedRef<HTMLInputElement>
|
||||
): React.JSX.Element => {
|
||||
const InputIcon = icon || (imagePath && <img src={imagePath} alt={imageAltText} />);
|
||||
|
||||
return (
|
||||
<InputComponentWrapper width={width}>
|
||||
{!!label && <Label text={label} color={labelColor} icon={labelIcon} className={labelClassName} />}
|
||||
<InputWrapper>
|
||||
<ImageWrapper>{InputIcon}</ImageWrapper>
|
||||
<InputElement icon={icon} name={name} imagePath={imagePath} disabled={disabled} ref={ref} {...props} />
|
||||
</InputWrapper>
|
||||
{!!helperText && (
|
||||
<HelperText className={helperTextClassName} error={error}>
|
||||
{helperText}
|
||||
</HelperText>
|
||||
)}
|
||||
</InputComponentWrapper>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export default InputComponent;
|
||||
export const Input = InputComponent;
|
||||
73
src/components/forms/SearchInput.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
import * as styled from 'styled-components';
|
||||
import {JSX, useState, useEffect, ChangeEvent} from 'react';
|
||||
import moment from 'moment';
|
||||
import {IInput} from './Input';
|
||||
import Input from './Input';
|
||||
import Theme from 'theme';
|
||||
import searchImage from 'assets/images/search-150x150.png';
|
||||
|
||||
const searchWaitTimeout = moment.duration(1, 'seconds').asMilliseconds();
|
||||
|
||||
interface ISearchInput extends IInput {
|
||||
search: (str: string) => void;
|
||||
defaultValue?: string;
|
||||
minLengthForSearch?: number;
|
||||
}
|
||||
|
||||
const SearchInput = styled.default(Input)`
|
||||
min-width: 100%;
|
||||
padding-left: ${Theme.spacing.xlarge};
|
||||
`;
|
||||
|
||||
export const SearchInputWrapper = styled.default.div`
|
||||
position: relative;
|
||||
display: flex;
|
||||
& div {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
align-self: center;
|
||||
& img {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
left: 0.5rem;
|
||||
position: absolute;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const Search = ({search, defaultValue = '', minLengthForSearch = 2}: ISearchInput): JSX.Element => {
|
||||
const [currentSearchTerm, setCurrentSearchTerm] = useState<string>(defaultValue);
|
||||
const onChange = (event: ChangeEvent<HTMLInputElement>): void => {
|
||||
setCurrentSearchTerm(event.target.value);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
let timeout = null;
|
||||
|
||||
if (currentSearchTerm.length >= minLengthForSearch || currentSearchTerm.length === 0) {
|
||||
timeout = setTimeout(() => search(currentSearchTerm), searchWaitTimeout);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (timeout) {
|
||||
clearInterval(timeout);
|
||||
}
|
||||
};
|
||||
}, [currentSearchTerm, minLengthForSearch, search]);
|
||||
|
||||
return (
|
||||
<SearchInputWrapper>
|
||||
<div className="search-bar">
|
||||
<img src={searchImage} alt={'searchImage'} />
|
||||
<SearchInput value={currentSearchTerm} onChange={onChange} name="testId-search" />
|
||||
</div>
|
||||
</SearchInputWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Search;
|
||||
1
src/components/forms/label/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './label';
|
||||
19
src/components/forms/label/label.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
import React from 'react';
|
||||
import {Label as StyledLabel} from './style';
|
||||
|
||||
interface ILabel {
|
||||
text: string;
|
||||
htmlFor?: string;
|
||||
color?: string;
|
||||
icon?: React.JSX.Element;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const Label = ({text, htmlFor, color, icon, className}: ILabel): React.JSX.Element => (
|
||||
<StyledLabel className={className} htmlFor={htmlFor} color={color}>
|
||||
{text} {icon}
|
||||
</StyledLabel>
|
||||
);
|
||||
15
src/components/forms/label/style.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
import * as styled from 'styled-components';
|
||||
import Theme from 'theme';
|
||||
|
||||
const {spacing, colors, typography} = Theme;
|
||||
|
||||
export const Label = styled.default.label<{color?: string}>`
|
||||
font-size: ${typography.fontSizeS};
|
||||
color: ${({color}) => color || colors.gray900};
|
||||
font-weight: bold;
|
||||
margin: ${spacing.xxSmall} 0;
|
||||
display: block;
|
||||
`;
|
||||
198
src/components/icon-menu/icon-menu.tsx
Normal file
@@ -0,0 +1,198 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
import React, {useEffect, useRef, useState, ReactNode} from 'react';
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
|
||||
import {IconDefinition, IconProp} from '@fortawesome/fontawesome-svg-core';
|
||||
|
||||
import {useComponentVisible, useCurrentWidth} from 'utility/custom-hooks';
|
||||
import {IconMenuWrapper, IconMenuDropDownWrapper, IconMenuContent, IconMenuPointer, DropdownIcon, IconMenuDropDownItem} from './style';
|
||||
import {IconMenuPosition, defaultArrowWidth, defaultArrowEdgeGap} from './types';
|
||||
|
||||
interface IIconMenuItems {
|
||||
title: string;
|
||||
value: string;
|
||||
className?: string;
|
||||
onSelect?: () => void;
|
||||
onClick?: () => void;
|
||||
modal?: {
|
||||
component: ReactNode;
|
||||
action?: () => void;
|
||||
};
|
||||
}
|
||||
|
||||
interface IMenuPointerStyle {
|
||||
left?: number | string;
|
||||
right?: number | string;
|
||||
top?: number | string;
|
||||
bottom?: number | string;
|
||||
width?: number | string;
|
||||
height?: number | string;
|
||||
}
|
||||
|
||||
interface IIconMenu {
|
||||
items: IIconMenuItems[];
|
||||
icon?: IconDefinition | IconProp;
|
||||
iconClassName?: string;
|
||||
iconColor?: string;
|
||||
title?: string;
|
||||
menuVisible?: boolean;
|
||||
showTail?: boolean;
|
||||
position?: IconMenuPosition;
|
||||
hasPointer?: boolean;
|
||||
dropdownMargin?: number;
|
||||
margin?: number | string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const IconMenu = ({
|
||||
items = [],
|
||||
icon,
|
||||
iconClassName,
|
||||
iconColor = 'white',
|
||||
menuVisible = false,
|
||||
title,
|
||||
showTail = true,
|
||||
position = IconMenuPosition.BottomLeft,
|
||||
hasPointer = true,
|
||||
dropdownMargin = 0,
|
||||
margin,
|
||||
disabled
|
||||
}: IIconMenu): React.JSX.Element => {
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
const [dropdownStyle, setDropdownStyle] = useState<any>(null);
|
||||
const [menuPointerStyle, setMenuPointerStyle] = useState<IMenuPointerStyle>({
|
||||
right: -defaultArrowWidth / 2,
|
||||
top: '50%'
|
||||
});
|
||||
const {ref, isComponentVisible, setIsComponentVisible} = useComponentVisible(menuVisible);
|
||||
const width = useCurrentWidth();
|
||||
|
||||
useEffect(() => {
|
||||
if (!isComponentVisible) {
|
||||
setDropdownStyle(null);
|
||||
}
|
||||
}, [isComponentVisible]);
|
||||
|
||||
useEffect(() => {
|
||||
if (ref.current && dropdownRef.current && isComponentVisible && position) {
|
||||
const refRect = (ref.current as HTMLElement).getBoundingClientRect();
|
||||
const dropdownRefRect = (dropdownRef.current as HTMLElement).getBoundingClientRect();
|
||||
const newArrowPosition: IMenuPointerStyle = {
|
||||
width: `${defaultArrowWidth}px`,
|
||||
height: `${defaultArrowWidth}px`
|
||||
};
|
||||
const pointerMargin = Math.sqrt((defaultArrowWidth * defaultArrowWidth) / 2);
|
||||
let top = 0;
|
||||
let left = 0;
|
||||
|
||||
switch (position) {
|
||||
case IconMenuPosition.Right: {
|
||||
newArrowPosition.top = `calc(50% - ${defaultArrowWidth / 2}px)`;
|
||||
newArrowPosition.left = `-${defaultArrowWidth / 2}px`;
|
||||
top = refRect.top + refRect.height / 2 - dropdownRefRect.height / 2;
|
||||
left = refRect.left + refRect.width + pointerMargin + dropdownMargin;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case IconMenuPosition.Top: {
|
||||
newArrowPosition.left = '50%';
|
||||
newArrowPosition.bottom = `-${defaultArrowWidth / 2}px`;
|
||||
top = refRect.top - dropdownRefRect.height - pointerMargin - dropdownMargin;
|
||||
left = refRect.left + refRect.width / 2 - dropdownRefRect.width / 2;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case IconMenuPosition.TopLeft: {
|
||||
newArrowPosition.right = `${defaultArrowEdgeGap}px`;
|
||||
newArrowPosition.bottom = `-${defaultArrowWidth / 2}px`;
|
||||
top = refRect.top - dropdownRefRect.height - pointerMargin - dropdownMargin;
|
||||
left = refRect.left + refRect.width / 2 - dropdownRefRect.width + defaultArrowWidth / 2 + defaultArrowEdgeGap;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case IconMenuPosition.TopRight: {
|
||||
newArrowPosition.left = `${defaultArrowEdgeGap}px`;
|
||||
newArrowPosition.bottom = `-${defaultArrowWidth / 2}px`;
|
||||
top = refRect.top - dropdownRefRect.height - pointerMargin - dropdownMargin;
|
||||
left = refRect.left + refRect.width / 2 - defaultArrowWidth / 2 - defaultArrowEdgeGap;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case IconMenuPosition.Bottom: {
|
||||
newArrowPosition.left = '50%';
|
||||
newArrowPosition.top = `-${defaultArrowWidth / 2}px`;
|
||||
top = refRect.top + refRect.height + pointerMargin + dropdownMargin;
|
||||
left = refRect.left + refRect.width / 2 - dropdownRefRect.width / 2;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case IconMenuPosition.BottomLeft: {
|
||||
newArrowPosition.right = `${defaultArrowEdgeGap}px`;
|
||||
newArrowPosition.top = `-${defaultArrowWidth / 2}px`;
|
||||
top = refRect.top + refRect.height + pointerMargin + dropdownMargin;
|
||||
left = refRect.left + refRect.width / 2 - dropdownRefRect.width + defaultArrowWidth / 2 + defaultArrowEdgeGap;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case IconMenuPosition.BottomRight: {
|
||||
newArrowPosition.left = `${defaultArrowEdgeGap}px`;
|
||||
newArrowPosition.top = `-${defaultArrowWidth / 2}px`;
|
||||
top = refRect.top + refRect.height + pointerMargin + dropdownMargin;
|
||||
left = refRect.left + refRect.width / 2 - defaultArrowWidth / 2 - defaultArrowEdgeGap;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case IconMenuPosition.Left:
|
||||
default: {
|
||||
newArrowPosition.top = `calc(50% - ${defaultArrowWidth / 2}px)`;
|
||||
newArrowPosition.right = `-${defaultArrowWidth / 2}px`;
|
||||
top = refRect.top + refRect.height / 2 - dropdownRefRect.height / 2;
|
||||
left = refRect.left - dropdownRefRect.width - pointerMargin - dropdownMargin;
|
||||
}
|
||||
}
|
||||
|
||||
setDropdownStyle({
|
||||
top,
|
||||
left
|
||||
});
|
||||
setMenuPointerStyle(newArrowPosition);
|
||||
}
|
||||
}, [isComponentVisible, ref, position, dropdownMargin, width]);
|
||||
|
||||
const handleClickIcon = () => {
|
||||
setIsComponentVisible(!isComponentVisible);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<IconMenuWrapper ref={ref} margin={margin}>
|
||||
<DropdownIcon role="button" tabIndex={0} onClick={handleClickIcon} className={`testId-dropdownIcon ${iconClassName} ${disabled && 'disabled'}`}>
|
||||
{title}
|
||||
{icon && <FontAwesomeIcon icon={icon} color={iconColor} size="lg" />}
|
||||
</DropdownIcon>
|
||||
{isComponentVisible && (
|
||||
<IconMenuDropDownWrapper ref={dropdownRef} visible={isComponentVisible && !!dropdownStyle} showTail={showTail} style={dropdownStyle || undefined}>
|
||||
<IconMenuContent className="testId-dropdownItems">
|
||||
{!!hasPointer && <IconMenuPointer style={menuPointerStyle} />}
|
||||
{items.map((item, i) => (
|
||||
<IconMenuDropDownItem key={`${item.value}-${i}`} className={item.className} onMouseUp={item.onClick} data-value={item.value}>
|
||||
{item.title}
|
||||
</IconMenuDropDownItem>
|
||||
))}
|
||||
</IconMenuContent>
|
||||
</IconMenuDropDownWrapper>
|
||||
)}
|
||||
</IconMenuWrapper>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default IconMenu;
|
||||
4
src/components/icon-menu/index.tsx
Normal file
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
export {default as IconMenu} from './icon-menu';
|
||||
79
src/components/icon-menu/style.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
import * as styled from 'styled-components';
|
||||
import Theme from 'theme';
|
||||
import caretUp from 'assets/images/caret-up.svg';
|
||||
|
||||
const {colors, spacing} = Theme;
|
||||
|
||||
export const IconMenuWrapper = styled.default.div<{margin?: number | string}>`
|
||||
margin: ${({margin}) => (margin !== undefined ? margin : '.5rem 0 .5rem 1rem')};
|
||||
div {
|
||||
outline: none;
|
||||
}
|
||||
& img{
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
cursor: pointer;
|
||||
}
|
||||
`;
|
||||
export const IconMenuDropDownWrapper = styled.default.div<{showTail?: boolean; visible?: boolean}>`
|
||||
position: fixed;
|
||||
width: 200px;
|
||||
min-height: 31px;
|
||||
right: 2.5rem;
|
||||
${({showTail}) =>
|
||||
showTail &&
|
||||
styled.css`
|
||||
background-image: url(${caretUp});
|
||||
`};
|
||||
background-position: top 0 right .5rem;
|
||||
background-repeat: no-repeat;
|
||||
z-index: 9999;
|
||||
visibility: hidden;
|
||||
${({visible}) =>
|
||||
visible &&
|
||||
styled.css`
|
||||
visibility: visible;
|
||||
`}
|
||||
`;
|
||||
export const IconMenuContent = styled.default.div`
|
||||
background-color: ${colors.gray100};
|
||||
border: 1px solid ${colors.gray200};
|
||||
width: 200px;
|
||||
overflow: auto;
|
||||
border-radius: ${spacing.xxSmall};
|
||||
padding: ${spacing.small} 0;
|
||||
z-index: 10;
|
||||
`;
|
||||
|
||||
export const IconMenuDropDownItem = styled.default.div`
|
||||
padding: ${spacing.small};
|
||||
text-decoration: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
color: ${colors.black};
|
||||
background-color: ${colors.white};
|
||||
&:hover {
|
||||
background-color: ${colors.gray400};
|
||||
}
|
||||
`;
|
||||
export const DropdownIcon = styled.default.div<{disabled?: boolean}>`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
min-width: 1rem;
|
||||
|
||||
&.disabled {
|
||||
pointer-events: none;
|
||||
opacity: 0.5;
|
||||
}
|
||||
`;
|
||||
export const IconMenuPointer = styled.default.div`
|
||||
position: absolute;
|
||||
background-color: ${colors.gray100};
|
||||
transform: rotate(45deg);
|
||||
`;
|
||||
19
src/components/icon-menu/types.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
|
||||
export enum IconMenuPosition {
|
||||
Left = 'left',
|
||||
Right = 'right',
|
||||
Top = 'top',
|
||||
TopLeft = 'top-left',
|
||||
TopRight = 'top-right',
|
||||
Bottom = 'bottom',
|
||||
BottomLeft = 'bottom-left',
|
||||
BottomRight = 'bottom-right'
|
||||
}
|
||||
|
||||
const defaultArrowWidth = 12;
|
||||
const defaultArrowEdgeGap = 8;
|
||||
|
||||
export {defaultArrowWidth, defaultArrowEdgeGap};
|
||||
16
src/components/index.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
export * from './buttons';
|
||||
export * from './channel-icon-menu';
|
||||
export * from './error-renderer';
|
||||
export * from './layout';
|
||||
export * from './loaders';
|
||||
export * from './modal';
|
||||
export * from './new-tab-link';
|
||||
export * from './pre-formatted-code';
|
||||
export * from './tags';
|
||||
export * from './table-screen-header';
|
||||
export * from './table-with-pagination';
|
||||
export * from './ui';
|
||||
export * from './tooltip';
|
||||
export * from './ProtectedRoute';
|
||||
export * from './table';
|
||||
export * from './typography';
|
||||
4
src/components/indicator-component/indicators/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
export * from './indicators';
|
||||
27
src/components/indicator-component/indicators/indicators.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
import {JSX} from 'react';
|
||||
import {LoadingWheel as Loader} from 'components/loaders';
|
||||
import {SingleStreamSymbol, OfflineSymbol, MultipleStreamSymbol, Indicator} from './style';
|
||||
|
||||
export const OfflineIndicator = (): JSX.Element => (
|
||||
<Indicator>
|
||||
<OfflineSymbol />
|
||||
</Indicator>
|
||||
);
|
||||
export const SingleStreamIndicator = (): JSX.Element => (
|
||||
<Indicator className="single-stream-indicator">
|
||||
<SingleStreamSymbol className="testId-singleStreamIndicator" />
|
||||
</Indicator>
|
||||
);
|
||||
export const MultiStreamIndicator = (): JSX.Element => (
|
||||
<Indicator className="multi-stream-indicator">
|
||||
<MultipleStreamSymbol className="testId-multiStreamIndicator" />
|
||||
</Indicator>
|
||||
);
|
||||
export const LoadingIndicator = (): JSX.Element => (
|
||||
<Indicator>
|
||||
<Loader size="medium" />
|
||||
</Indicator>
|
||||
);
|
||||
43
src/components/indicator-component/indicators/style.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
import * as styled from 'styled-components';
|
||||
|
||||
import {LoadingWheel as Loader} from 'components';
|
||||
|
||||
export const Indicator = styled.default.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
${Loader} {
|
||||
flex: 0;
|
||||
margin: 0;
|
||||
|
||||
& span {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const IndicatorOutline = styled.default.div`
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
`;
|
||||
|
||||
export const OfflineSymbol = styled.default(IndicatorOutline)`
|
||||
border: 3px solid #707070;
|
||||
background-color: transparent;
|
||||
`;
|
||||
|
||||
export const SingleStreamSymbol = styled.default(IndicatorOutline)`
|
||||
background-color: #08BD0B;
|
||||
`;
|
||||
|
||||
export const MultipleStreamSymbol = styled.default(SingleStreamSymbol)`
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
margin-left: -2px;
|
||||
box-shadow: 9px 0 0 0 #08BD0B;
|
||||
`;
|
||||
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
import {JSX} from 'react';
|
||||
import {useSelector} from 'react-redux';
|
||||
|
||||
import {AppStore} from 'store';
|
||||
import {DataRowType} from 'components/table';
|
||||
import {SingleStreamIndicator, OfflineIndicator, MultiStreamIndicator, LoadingIndicator} from './indicators';
|
||||
|
||||
interface IPublishingStateIndicator {
|
||||
row: DataRowType;
|
||||
publishingStateKey: string;
|
||||
idKey: string;
|
||||
}
|
||||
|
||||
const PublishingStateIndicator = ({row, publishingStateKey, idKey}: IPublishingStateIndicator): JSX.Element => {
|
||||
const id = row[idKey];
|
||||
const publishingState = useSelector((state: AppStore) => publishingStateKey && state[publishingStateKey as keyof AppStore]?.publishingState);
|
||||
const rowPublishingState = publishingState.find((record: Record<string, any>) => record[idKey] === id);
|
||||
|
||||
if (!rowPublishingState) {
|
||||
return <LoadingIndicator />;
|
||||
}
|
||||
|
||||
if (!rowPublishingState.isOnline) {
|
||||
return <OfflineIndicator />;
|
||||
}
|
||||
|
||||
if (rowPublishingState.multipleStreams) {
|
||||
return <MultiStreamIndicator />;
|
||||
}
|
||||
|
||||
return <SingleStreamIndicator />;
|
||||
};
|
||||
|
||||
export default PublishingStateIndicator;
|
||||
1
src/components/label/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from '../forms/label';
|
||||
56
src/components/layout/index.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
import * as styled from 'styled-components';
|
||||
import Theme from 'theme';
|
||||
|
||||
interface IColumn {
|
||||
align?: string;
|
||||
size?: number;
|
||||
padding?: string;
|
||||
}
|
||||
|
||||
const {footerHeight} = Theme;
|
||||
|
||||
export const AppContainer = styled.default.div`
|
||||
flex: 1;
|
||||
display: flex;
|
||||
overflow-x: hidden;
|
||||
min-height: 0;
|
||||
`;
|
||||
|
||||
export const Body = styled.default.div`
|
||||
flex: 1;
|
||||
margin: 1rem;
|
||||
padding: 1rem ${Theme.spacing.xlarge};
|
||||
background: ${Theme.colors.gray900};
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
`;
|
||||
|
||||
export const Row = styled.default.div`
|
||||
display: flex;
|
||||
`;
|
||||
|
||||
export const Column = styled.default.div<IColumn>`
|
||||
flex: ${({size}): number => size || 1};
|
||||
text-align: ${({align}): string => align || 'right'};
|
||||
${({padding}) =>
|
||||
padding &&
|
||||
styled.css`
|
||||
padding: ${padding};
|
||||
`}
|
||||
`;
|
||||
|
||||
export const Main = styled.default.div`
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 2rem;
|
||||
`;
|
||||
42
src/components/loaders/LoadingWheel.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
import * as styled from 'styled-components';
|
||||
import Theme from 'theme';
|
||||
|
||||
interface LoadingWheelProps {
|
||||
size?: 'small' | 'medium' | 'large';
|
||||
color?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const LoadingWheelContainer = styled.default.div<{
|
||||
size: number;
|
||||
color: string;
|
||||
}>`
|
||||
display: inline-block;
|
||||
width: ${({size}) => size}px;
|
||||
height: ${({size}) => size}px;
|
||||
border: 3px solid ${({color}) => color}20;
|
||||
border-radius: 50%;
|
||||
border-top-color: ${({color}) => color};
|
||||
animation: spin 1s ease-in-out infinite;
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const LoadingWheel: React.FC<LoadingWheelProps> = ({size = 'medium', color = Theme.colors.white, className}) => {
|
||||
const sizeMap = {
|
||||
small: Theme.loaderSize.small,
|
||||
medium: Theme.loaderSize.medium,
|
||||
large: Theme.loaderSize.large
|
||||
};
|
||||
|
||||
return <LoadingWheelContainer size={sizeMap[size]} color={color} className={className} role="status" aria-label="Loading" />;
|
||||
};
|
||||
|
||||
export default LoadingWheel;
|
||||
4
src/components/loaders/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
export * from './LoadingWheel';
|
||||
5
src/components/modal/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
|
||||
export {default as Modal} from './modal';
|
||||
40
src/components/modal/modal-form-response/form-response.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
import {LoadingWheel as Loader} from 'components';
|
||||
import PreFormattedCode from 'components/pre-formatted-code';
|
||||
import okImage from 'assets/images/icon/ok.svg';
|
||||
import errorImage from 'assets/images/icon/error.svg';
|
||||
import React from 'react';
|
||||
|
||||
import {DialogResponse, StatusCode, FormLoaderContainer} from './style';
|
||||
|
||||
interface IFormResponseComponent {
|
||||
response: {
|
||||
error: string | boolean | Error;
|
||||
status: number | string;
|
||||
data?: any; //eslint-disable-line
|
||||
};
|
||||
}
|
||||
|
||||
const FormResponse = (props: IFormResponseComponent): React.JSX.Element => {
|
||||
const {response} = props;
|
||||
|
||||
return !response ? (
|
||||
<FormLoaderContainer>
|
||||
<Loader color="dark" size="medium" />
|
||||
</FormLoaderContainer>
|
||||
) : (
|
||||
<DialogResponse error={response.error || ''}>
|
||||
<h3 className="testId-responseModal">{response.error ? 'Error' : 'Success'}</h3>
|
||||
<div className="response-icon">
|
||||
<img src={response.error ? errorImage : okImage} alt="status icon" />
|
||||
</div>
|
||||
<StatusCode error={response.error}>Status: {response.status}</StatusCode>
|
||||
{response.data && typeof response.data === 'object' && <PreFormattedCode data={response.data} />}
|
||||
{response.error && typeof response.error === 'string' && <PreFormattedCode data={{error: response.error}} />}
|
||||
</DialogResponse>
|
||||
);
|
||||
};
|
||||
|
||||
export default FormResponse;
|
||||
4
src/components/modal/modal-form-response/index.tsx
Normal file
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
export {default as FormResponse} from './form-response';
|
||||
175
src/components/modal/modal-form-response/style.ts
Normal file
@@ -0,0 +1,175 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
import * as styled from 'styled-components';
|
||||
import Theme from 'theme';
|
||||
import {InputComponentWrapper} from 'components/forms/Input';
|
||||
// import {AdvancedSelectContainer, ComponentWrapper} from 'components/ui/advanced-select/advanced-select';
|
||||
// import InputWithTagsWrapper from 'components/ui/input-with-tags/input-with-tags';
|
||||
// import TagContainer from 'components/ui/input-with-tags/input-with-tags';
|
||||
// import TagWrapper from 'components/ui/input-with-tags/input-with-tags';
|
||||
|
||||
const {spacing, blackWithOpacity, typography, colors, primaryBorderRadius, primaryBorderColor} = Theme;
|
||||
|
||||
export const DialogContent = styled.default.div`
|
||||
display: flex;
|
||||
width: 400px;
|
||||
margin: ${spacing.small};
|
||||
`;
|
||||
|
||||
export const DialogForm = styled.default.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
/* stylelint-disable */
|
||||
& ${InputComponentWrapper} {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
/* TagWrapper {
|
||||
& > div {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
} */
|
||||
/* stylelint-enable */
|
||||
h3 {
|
||||
color: ${colors.gray700};
|
||||
font-size: ${typography.fontSizeXl};
|
||||
line-height: 24px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.36px;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
a {
|
||||
font-size: ${typography.fontSizeS};
|
||||
margin: 0 0 0 0.5rem;
|
||||
}
|
||||
}
|
||||
> p {
|
||||
color: ${blackWithOpacity};
|
||||
font-size: ${typography.primaryFontSize};
|
||||
margin-top: ${spacing.small};
|
||||
line-height: 24px;
|
||||
i {
|
||||
font-weight: bold;
|
||||
padding: 0 ${spacing.xsmall};
|
||||
}
|
||||
a {
|
||||
color: ${colors.lightBlue};
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
strong {
|
||||
p {
|
||||
margin-top: 0;
|
||||
margin-bottom: ${spacing.xSmall};
|
||||
}
|
||||
svg {
|
||||
margin: 0 ${spacing.xxSmall};
|
||||
color: ${colors.lightBlue};
|
||||
}
|
||||
}
|
||||
select {
|
||||
background: ${colors.transparent};
|
||||
border: 1px solid ${primaryBorderColor} !important;
|
||||
}
|
||||
input {
|
||||
padding: ${spacing.small};
|
||||
margin-bottom: ${spacing.xSmall};
|
||||
}
|
||||
.button-group {
|
||||
display: flex;
|
||||
margin-top: ${spacing.small};
|
||||
justify-content: center;
|
||||
}
|
||||
.error-text {
|
||||
font-size: ${typography.fontSizeXS};
|
||||
color: ${colors.lightRed};
|
||||
}
|
||||
.loading {
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
background-color: rgba(0,0,0,0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
`;
|
||||
|
||||
export const Options = styled.default.div`
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
|
||||
&>div {
|
||||
margin: 0 1rem 0 0;
|
||||
}
|
||||
`;
|
||||
|
||||
export const DialogResponse = styled.default(DialogContent)<{error?: string | boolean | Error}>`
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
h3 {
|
||||
color: ${colors.green};
|
||||
font-size: ${typography.fontSizeXl};
|
||||
line-height: 24px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.36px;
|
||||
${({error}) =>
|
||||
error &&
|
||||
styled.css`
|
||||
color: ${colors.lightRed};
|
||||
`}
|
||||
}
|
||||
.response-icon{
|
||||
img {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
margin: ${spacing.small} 0;
|
||||
}
|
||||
}
|
||||
.json-renderer {
|
||||
margin-top: ${spacing.small};
|
||||
width: 100%;
|
||||
height: auto;
|
||||
height: 300px;
|
||||
padding: ${spacing.small};
|
||||
overflow: auto;
|
||||
background-color: ${colors.gray200};
|
||||
font-size: ${typography.fontSizeS};
|
||||
}
|
||||
`;
|
||||
|
||||
export const StatusCode = styled.default.div<{error?: string | boolean | Error}>`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
margin: ${spacing.xxSmall} 0;
|
||||
padding: ${spacing.xsmall} 0;
|
||||
color: ${colors.green};
|
||||
border-radius: ${primaryBorderRadius};
|
||||
border: 1px solid ${colors.green};
|
||||
${({error}) =>
|
||||
error &&
|
||||
styled.css`
|
||||
border-color: ${colors.lightRed};
|
||||
color: ${colors.lightRed};
|
||||
`}
|
||||
`;
|
||||
|
||||
export const Error = styled.default.div`
|
||||
font-size: ${typography.fontSizeXS};
|
||||
color: ${colors.lightRed};
|
||||
`;
|
||||
|
||||
export const FormLoaderContainer = styled.default.div`
|
||||
height: 430px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
`;
|
||||
91
src/components/modal/modal.tsx
Normal file
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
import React from 'react';
|
||||
import {createPortal} from 'react-dom';
|
||||
|
||||
import {ConfirmButton} from 'components/buttons';
|
||||
import {theme} from 'components/shared/theme';
|
||||
|
||||
import {ModalOverlay, ModalContainer, ModalButtonsContaier, CloseButton} from './style';
|
||||
|
||||
interface IModal {
|
||||
children: React.JSX.Element;
|
||||
close: () => void;
|
||||
className?: string;
|
||||
submitButton?: {
|
||||
onClick: () => void;
|
||||
label?: string;
|
||||
className?: string;
|
||||
disabled?: boolean;
|
||||
textColor?: string;
|
||||
backgroundColor?: string;
|
||||
borderColor?: string;
|
||||
};
|
||||
cancelButton?: {
|
||||
onClick?: () => void;
|
||||
label?: string;
|
||||
className?: string;
|
||||
disabled?: boolean;
|
||||
textColor?: string;
|
||||
backgroundColor?: string;
|
||||
borderColor?: string;
|
||||
};
|
||||
}
|
||||
|
||||
const Modal = (props: IModal): React.JSX.Element => {
|
||||
const {children, className, close} = props;
|
||||
const submitButton = {
|
||||
onClick: props.submitButton?.onClick || null,
|
||||
label: 'Submit',
|
||||
disabled: false,
|
||||
textColor: theme.colors.gray700,
|
||||
backgroundColor: theme.colors.white,
|
||||
borderColor: theme.colors.gray700,
|
||||
...props.submitButton
|
||||
};
|
||||
const cancelButton = {
|
||||
label: 'Cancel',
|
||||
disabled: false,
|
||||
textColor: theme.colors.white,
|
||||
backgroundColor: theme.colors.gray700,
|
||||
borderColor: theme.colors.gray700,
|
||||
...props.cancelButton
|
||||
};
|
||||
const handleCancel = () => {
|
||||
if (cancelButton?.onClick) {
|
||||
cancelButton.onClick();
|
||||
} else {
|
||||
close();
|
||||
}
|
||||
};
|
||||
|
||||
const clickModalContainer = (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
return createPortal(
|
||||
<ModalOverlay onClick={close}>
|
||||
<ModalContainer className={className} onClick={clickModalContainer}>
|
||||
<CloseButton onClick={close} />
|
||||
{children}
|
||||
<ModalButtonsContaier>
|
||||
{submitButton.onClick && (
|
||||
<ConfirmButton {...submitButton} onClick={submitButton.onClick}>
|
||||
{submitButton.label}
|
||||
</ConfirmButton>
|
||||
)}
|
||||
{!props.cancelButton ||
|
||||
(!cancelButton.disabled && (
|
||||
<ConfirmButton className="testId-cancel" {...cancelButton} onClick={handleCancel}>
|
||||
{cancelButton.label}
|
||||
</ConfirmButton>
|
||||
))}
|
||||
</ModalButtonsContaier>
|
||||
</ModalContainer>
|
||||
</ModalOverlay>,
|
||||
document.body
|
||||
);
|
||||
};
|
||||
|
||||
export default Modal;
|
||||
5
src/components/modal/multi-step-modal/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
|
||||
export {default as MultiStepModal} from './multi-step-modal';
|
||||
66
src/components/modal/multi-step-modal/multi-step-modal.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
|
||||
import React, {useState} from 'react';
|
||||
|
||||
import {Modal} from 'components/modal';
|
||||
|
||||
interface IMultiStepModal {
|
||||
isOpen: boolean;
|
||||
closeModal: () => void;
|
||||
steps: {
|
||||
title: string;
|
||||
component: React.JSX.Element;
|
||||
saveButton?: {
|
||||
onClick?: () => void;
|
||||
text?: string;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
};
|
||||
cancel?: () => void;
|
||||
cancelDisabled?: boolean;
|
||||
}[];
|
||||
}
|
||||
|
||||
const MultiStepModal = ({isOpen, closeModal, steps}: IMultiStepModal): React.JSX.Element => {
|
||||
const [currentStep, setCurrentStep] = useState(0);
|
||||
const isLastStep = steps.length - 1 === currentStep;
|
||||
const saveButtonText = isLastStep ? 'Done' : 'Next';
|
||||
const handleCloseModal = () => {
|
||||
closeModal();
|
||||
setCurrentStep(0);
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
if (steps[currentStep]?.saveButton?.onClick) {
|
||||
steps[currentStep].saveButton.onClick();
|
||||
}
|
||||
|
||||
if (isLastStep) {
|
||||
handleCloseModal();
|
||||
} else {
|
||||
setCurrentStep(currentStep + 1);
|
||||
}
|
||||
};
|
||||
|
||||
if (isOpen) {
|
||||
return (
|
||||
<Modal
|
||||
close={handleCloseModal}
|
||||
submitButton={{
|
||||
className: steps[currentStep]?.saveButton?.className,
|
||||
disabled: steps[currentStep]?.saveButton?.disabled,
|
||||
onClick: handleSave,
|
||||
label: steps[currentStep]?.saveButton?.text || saveButtonText
|
||||
}}
|
||||
cancelButton={{disabled: steps[currentStep]?.cancelDisabled}}>
|
||||
{steps[currentStep]?.component || null}
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default MultiStepModal;
|
||||
80
src/components/modal/style.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
import * as styled from 'styled-components';
|
||||
|
||||
import {theme, paddings} from 'components/shared/theme';
|
||||
import {ConfirmButton} from 'components/buttons';
|
||||
|
||||
const {colors} = theme;
|
||||
|
||||
export const ModalOverlay = styled.default.div`
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: ${colors.halfTransparentBlack};
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 9999;
|
||||
`;
|
||||
|
||||
export const ModalContainer = styled.default.div`
|
||||
width: 60%;
|
||||
max-width: 800px;
|
||||
border-radius: 4px;
|
||||
padding: ${paddings.xlarge};
|
||||
background-color: ${colors.white};
|
||||
position: relative;
|
||||
max-height: 90%;
|
||||
overflow: auto;
|
||||
|
||||
&.full-width {
|
||||
width: auto;
|
||||
max-width: 100%;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
&.dark {
|
||||
background-color: ${colors.gray1000}
|
||||
}
|
||||
`;
|
||||
|
||||
export const CloseButton = styled.default.span`
|
||||
cursor: pointer;
|
||||
height: 1rem;
|
||||
width: 1rem;
|
||||
color: ${colors.gray700};
|
||||
position: absolute;
|
||||
top: 0.5rem;
|
||||
right: 0.5rem;
|
||||
|
||||
&:before,
|
||||
&:after {
|
||||
position: absolute;
|
||||
left: 0.4rem;
|
||||
content: ' ';
|
||||
height: 1rem;
|
||||
width: 0.2rem;
|
||||
background-color: ${colors.gray700};
|
||||
}
|
||||
&:before {
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
&:after {
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
`;
|
||||
|
||||
export const ModalButtonsContaier = styled.default.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin: 24px 0 0;
|
||||
|
||||
${ConfirmButton} {
|
||||
margin: 0 0.5rem;
|
||||
}
|
||||
`;
|
||||
26
src/components/new-tab-link/index.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
import React from 'react';
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
|
||||
import {IconDefinition} from '@fortawesome/fontawesome-svg-core';
|
||||
|
||||
interface ILink {
|
||||
icon?: IconDefinition;
|
||||
text?: string;
|
||||
iconColor?: string;
|
||||
link: string;
|
||||
}
|
||||
|
||||
export const NewTabLink = (props: ILink & Partial<HTMLLinkElement>): React.JSX.Element => {
|
||||
const {icon, className, text, link, iconColor = 'white'} = props;
|
||||
|
||||
return (
|
||||
<a rel="noopener noreferrer" target="_blank" href={link} className={className}>
|
||||
{icon && <FontAwesomeIcon icon={icon} style={{color: iconColor}} />}
|
||||
{text}
|
||||
</a>
|
||||
);
|
||||
};
|
||||
|
||||
export default NewTabLink;
|
||||
5
src/components/pagination/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
export * from './pagination';
|
||||
export {default as Pagination} from './pagination';
|
||||
80
src/components/pagination/pagination.tsx
Normal file
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
import {Fragment} from 'react';
|
||||
import {PaginationWrapper, ItemRange, PaginationContainer, PageButton} from './style';
|
||||
|
||||
interface IPagination {
|
||||
currentPageNumber: number;
|
||||
numberOfItems: number;
|
||||
itemsPerPage: number;
|
||||
setCurrentPage: (page: number) => void;
|
||||
itemText?: string;
|
||||
}
|
||||
|
||||
export const Pagination = (props: IPagination): JSX.Element => {
|
||||
const maxNumberOfButtonsToShow = 3;
|
||||
const LowerBoundLimit = 2;
|
||||
const higherBoundLimit = 1;
|
||||
const {currentPageNumber, numberOfItems, itemsPerPage, setCurrentPage, itemText} = props;
|
||||
const totalNumberOfPages = Math.ceil(numberOfItems / itemsPerPage);
|
||||
const setPage = (page: number): void => {
|
||||
setCurrentPage(page);
|
||||
};
|
||||
|
||||
const minimumItemBoundPerPage = (currentPageNumber - 1) * itemsPerPage + 1;
|
||||
const maximumItemBoundPerPage = currentPageNumber * itemsPerPage;
|
||||
const generateButtons = (bounds = 1): JSX.Element[] => {
|
||||
const buttonToShow = maxNumberOfButtonsToShow > totalNumberOfPages ? totalNumberOfPages : maxNumberOfButtonsToShow;
|
||||
const buttons = [];
|
||||
let count: number;
|
||||
|
||||
if (currentPageNumber <= bounds) {
|
||||
count = 1;
|
||||
} else if (currentPageNumber + bounds > totalNumberOfPages) {
|
||||
count = currentPageNumber - (buttonToShow - (totalNumberOfPages - currentPageNumber + 1));
|
||||
} else {
|
||||
count = currentPageNumber - bounds;
|
||||
}
|
||||
|
||||
for (let y = 0; y < buttonToShow; y++) {
|
||||
buttons.push(
|
||||
<PageButton key={`page-button-${y}`} onClick={() => setPage(count + y)} active={currentPageNumber === count + y}>
|
||||
{count + y}
|
||||
</PageButton>
|
||||
);
|
||||
}
|
||||
|
||||
return buttons;
|
||||
};
|
||||
|
||||
return (
|
||||
<PaginationContainer className="pagination-container">
|
||||
{numberOfItems ? (
|
||||
<PaginationWrapper>
|
||||
{currentPageNumber >= LowerBoundLimit && (
|
||||
<Fragment>
|
||||
{currentPageNumber > 2 && totalNumberOfPages !== 3 && <PageButton onClick={() => setPage(1)}>1</PageButton>}
|
||||
{currentPageNumber > LowerBoundLimit && currentPageNumber !== 3 && <p>...</p>}
|
||||
</Fragment>
|
||||
)}
|
||||
{generateButtons()}
|
||||
{currentPageNumber <= totalNumberOfPages - higherBoundLimit && (
|
||||
<Fragment>
|
||||
{currentPageNumber < totalNumberOfPages - higherBoundLimit && currentPageNumber + 2 !== totalNumberOfPages && <p>...</p>}
|
||||
{currentPageNumber + 1 !== totalNumberOfPages && totalNumberOfPages !== 3 && (
|
||||
<PageButton onClick={() => setPage(totalNumberOfPages)}>{totalNumberOfPages}</PageButton>
|
||||
)}
|
||||
</Fragment>
|
||||
)}
|
||||
</PaginationWrapper>
|
||||
) : null}
|
||||
<ItemRange>
|
||||
{numberOfItems > 0 ? minimumItemBoundPerPage : 0} - {maximumItemBoundPerPage > numberOfItems ? numberOfItems : maximumItemBoundPerPage} of{' '}
|
||||
{numberOfItems} {itemText ? itemText : `channels`}
|
||||
</ItemRange>
|
||||
</PaginationContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default Pagination;
|
||||
40
src/components/pagination/style.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
import * as styled from 'styled-components';
|
||||
import {theme, paddings} from 'components/shared/theme';
|
||||
|
||||
const {colors, fontSizeS, spacing, primaryThemeColor} = theme;
|
||||
|
||||
export const PaginationContainer = styled.default.div`
|
||||
display: flex;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
margin: ${spacing.small} 0;
|
||||
`;
|
||||
|
||||
export const PaginationWrapper = styled.default.div`
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
`;
|
||||
|
||||
export const ItemRange = styled.default.div`
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-left: auto;
|
||||
font-size: ${fontSizeS};
|
||||
color: ${colors.lightBlue};
|
||||
`;
|
||||
|
||||
export const PageButton = styled.default.button<{active?: boolean}>`
|
||||
margin: 0 ${paddings.xsmall} ${paddings.xsmall} 0;
|
||||
font-size: ${fontSizeS};
|
||||
border: 1px solid ${primaryThemeColor};
|
||||
border-radius: 4px;
|
||||
height: 28px;
|
||||
width: 36px;
|
||||
cursor: pointer;
|
||||
background-color: ${({active}) => (active ? primaryThemeColor : 'transparent')};
|
||||
color: ${({active}) => (active ? colors.white : primaryThemeColor)};
|
||||
`;
|
||||
5
src/components/pre-formatted-code/index.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
|
||||
export {default} from './pre-formatted-code';
|
||||
58
src/components/pre-formatted-code/pre-formatted-code.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
import React, {Fragment, useState} from 'react';
|
||||
import {faCopy, faCheck} from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
import IconButton from 'components/buttons/icon-button';
|
||||
|
||||
import {CodeContainer} from './styles';
|
||||
|
||||
interface IPreFormattedCode {
|
||||
data: Record<string, any> | Array<any>; // eslint-disable-line
|
||||
prefixCharacter?: string;
|
||||
color?: string;
|
||||
showCopyIcon?: boolean;
|
||||
}
|
||||
|
||||
const iconChangeTimeout = 2000;
|
||||
const PreFormattedCode = ({data, prefixCharacter, color, showCopyIcon = true}: IPreFormattedCode): React.JSX.Element => {
|
||||
const [copied, setCopied] = useState(false);
|
||||
const json = JSON.stringify(data, null, 2);
|
||||
const lines = json.split('\n');
|
||||
const text = lines.map(line => line.replace(/^< /, '')).join('\n');
|
||||
const copyJsonToClipboard = (text: string): void => {
|
||||
navigator.clipboard.writeText(text);
|
||||
setCopied(true);
|
||||
|
||||
setTimeout(() => setCopied(false), iconChangeTimeout);
|
||||
};
|
||||
|
||||
const handleCopy = event => {
|
||||
const originalString = event.target.textContent;
|
||||
const modifiedString = originalString.replace(/[<>]/g, '');
|
||||
|
||||
event.clipboardData.setData('text/plain', modifiedString);
|
||||
event.preventDefault();
|
||||
};
|
||||
|
||||
const handleCopyIconClick = () => copyJsonToClipboard(text);
|
||||
|
||||
return (
|
||||
<CodeContainer color={color}>
|
||||
<pre>
|
||||
<code onCopy={handleCopy}>
|
||||
{lines.map((line, index) => (
|
||||
<Fragment key={index}>
|
||||
{prefixCharacter} {line} <br />
|
||||
{index === lines.length - 1 && prefixCharacter}
|
||||
</Fragment>
|
||||
))}
|
||||
</code>
|
||||
</pre>
|
||||
{showCopyIcon && <IconButton onClick={handleCopyIconClick} tooltipText="Copy" icon={copied ? faCheck : faCopy} />}
|
||||
</CodeContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default PreFormattedCode;
|
||||
34
src/components/pre-formatted-code/styles.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
import * as styled from 'styled-components';
|
||||
import Theme from 'theme';
|
||||
|
||||
const {colors} = Theme;
|
||||
|
||||
export const CodeContainer = styled.default.div<{color?: string}>`
|
||||
background-color: ${colors.gray200};
|
||||
padding: 1rem;
|
||||
margin: 1rem 0 0;
|
||||
display: flex;
|
||||
color: ${({color}) => color || colors.black};
|
||||
position: relative;
|
||||
width: 100%;
|
||||
|
||||
&>span {
|
||||
position: absolute;
|
||||
right: 0.5rem;
|
||||
top: 0.5rem;
|
||||
}
|
||||
|
||||
pre {
|
||||
width: 100%;
|
||||
white-space: pre-wrap;
|
||||
max-height: 40vh;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
code {
|
||||
word-wrap: break-word;
|
||||
}
|
||||
`;
|
||||
6
src/components/restricted-text/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
export {default as RestrictedTextWithLabel} from './restricted-text-with-label';
|
||||
export {default as RestrictedText} from './restricted-text';
|
||||
export * from './restricted-text';
|
||||
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
import {JSX} from 'react';
|
||||
import {RestrictedRowLabel, RestrictedRowText, RestrictedRowWrapper} from './style';
|
||||
|
||||
import {RestrictedText} from '.';
|
||||
|
||||
interface IRestrictedTextWithLabel {
|
||||
label: string;
|
||||
text: string;
|
||||
isLink?: boolean;
|
||||
linkClassName?: string;
|
||||
labelIcon?: JSX.Element;
|
||||
}
|
||||
|
||||
const RestrictedTextWithLabel = ({label, text, labelIcon, isLink = false, linkClassName = ''}: IRestrictedTextWithLabel): JSX.Element => {
|
||||
return (
|
||||
<RestrictedRowWrapper>
|
||||
<RestrictedRowLabel>
|
||||
{label} {labelIcon}
|
||||
</RestrictedRowLabel>
|
||||
<RestrictedRowText>
|
||||
<RestrictedText text={text} isLink={isLink} linkClassName={linkClassName} />
|
||||
</RestrictedRowText>
|
||||
</RestrictedRowWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default RestrictedTextWithLabel;
|
||||
104
src/components/restricted-text/restricted-text.tsx
Normal file
@@ -0,0 +1,104 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
import {JSX, useEffect, useRef, useState} from 'react';
|
||||
import {faEye, faEyeSlash} from '@fortawesome/free-regular-svg-icons';
|
||||
|
||||
import {Link} from 'components/ui';
|
||||
import IconButton from 'components/buttons/icon-button';
|
||||
import {CopyIconButton} from 'components/buttons/copy-icon-button';
|
||||
import NewTabLink from 'components/new-tab-link';
|
||||
|
||||
import {RestrictedDiv, MeasuringBlock, TruncatedBlock} from './style';
|
||||
|
||||
export interface IRestrictedText {
|
||||
text: string;
|
||||
hasViewOption?: boolean;
|
||||
hasCopyOption?: boolean;
|
||||
isLink?: boolean;
|
||||
linkValue?: string;
|
||||
linkClassName?: string;
|
||||
index?: number;
|
||||
fullWidth?: boolean;
|
||||
}
|
||||
|
||||
const iconButtonsWidth = 48;
|
||||
|
||||
export const RestrictedText = ({
|
||||
text,
|
||||
hasViewOption = true,
|
||||
hasCopyOption = true,
|
||||
isLink = false,
|
||||
linkValue,
|
||||
linkClassName = '',
|
||||
fullWidth = true
|
||||
}: IRestrictedText): JSX.Element => {
|
||||
const restrictedDivRef = useRef(null);
|
||||
const measuringRef = useRef(null);
|
||||
const [showValue, setShowValue] = useState(false);
|
||||
const [showEyeIcon, setShowEyeIcon] = useState(hasViewOption);
|
||||
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
handleShowEyeIcon();
|
||||
};
|
||||
|
||||
window.addEventListener('resize', handleResize);
|
||||
|
||||
handleShowEyeIcon();
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', handleResize);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleShowEyeIcon = () => {
|
||||
if (!restrictedDivRef.current || !measuringRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const containerWidth = restrictedDivRef.current.clientWidth - iconButtonsWidth;
|
||||
const fullTextWidth = measuringRef.current.getBoundingClientRect()?.width;
|
||||
|
||||
setShowEyeIcon(fullTextWidth > containerWidth);
|
||||
};
|
||||
|
||||
const handleShowValueChange = () => setShowValue(!showValue);
|
||||
const CurrentTextView = (): JSX.Element => {
|
||||
if (isLink && !linkValue) {
|
||||
return <NewTabLink link={text} className={linkClassName} text={text} />;
|
||||
}
|
||||
|
||||
if (isLink && linkValue) {
|
||||
return (
|
||||
<Link to={linkValue} className={linkClassName}>
|
||||
{text}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
return <>{text}</>;
|
||||
};
|
||||
|
||||
return (
|
||||
<RestrictedDiv ref={restrictedDivRef} className={`testId-truncatedDetails ${fullWidth && 'full-width'}`}>
|
||||
<MeasuringBlock ref={measuringRef}>{text}</MeasuringBlock>
|
||||
<TruncatedBlock className={showValue ? '' : 'truncated'}>
|
||||
<CurrentTextView />
|
||||
</TruncatedBlock>
|
||||
<div>
|
||||
{showEyeIcon && (
|
||||
<IconButton
|
||||
onClick={handleShowValueChange}
|
||||
tooltipText={showValue ? 'Hide' : 'Show'}
|
||||
icon={showValue ? faEyeSlash : faEye}
|
||||
className={`${isLink ? 'testId-permalink' : ''} testId-viewDetails`}
|
||||
/>
|
||||
)}
|
||||
{hasCopyOption && <CopyIconButton text={text} displayText={false} className="testId-copyDetails" />}
|
||||
</div>
|
||||
</RestrictedDiv>
|
||||
);
|
||||
};
|
||||
|
||||
export default RestrictedText;
|
||||
92
src/components/restricted-text/style.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
import * as styled from 'styled-components';
|
||||
|
||||
import Theme from 'theme';
|
||||
|
||||
const {colors, typography, spacing} = Theme;
|
||||
|
||||
export const RestrictedDiv = styled.default.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: start;
|
||||
word-wrap: break-word;
|
||||
&.full-width {
|
||||
width: 100%;
|
||||
}
|
||||
& div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
align-content: center;
|
||||
& span {
|
||||
color: ${Theme.primaryThemeColor};
|
||||
display: none;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
}
|
||||
.icon-button {
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
& span {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const TruncatedBlock = styled.default.p`
|
||||
overflow: hidden;
|
||||
padding: ${spacing.small} 0;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
&.truncated {
|
||||
white-space: nowrap;
|
||||
}
|
||||
`;
|
||||
|
||||
export const MeasuringBlock = styled.default.p`
|
||||
position: fixed;
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
export const RestrictedRowWrapper = styled.default.div`
|
||||
display: flex;
|
||||
`;
|
||||
|
||||
export const RestrictedRowLabel = styled.default.div`
|
||||
width: 135px;
|
||||
display: flex;
|
||||
align-self: center;
|
||||
color: ${colors.gray300};
|
||||
font-size: ${typography.fontSizeS};
|
||||
padding: ${spacing.small};
|
||||
|
||||
a {
|
||||
margin: 0 0.25rem;
|
||||
}
|
||||
|
||||
span {
|
||||
margin: 0 2px;
|
||||
}
|
||||
`;
|
||||
|
||||
export const RestrictedRowText = styled.default.div`
|
||||
color: ${colors.gray300};
|
||||
font-size: ${typography.fontSizeS};
|
||||
border-bottom: 1px solid ${colors.gray700};
|
||||
padding: ${spacing.small};
|
||||
width: calc(100% - 135px);
|
||||
|
||||
a {
|
||||
color: ${colors.lightBlue};
|
||||
overflow-wrap: anywhere;
|
||||
text-decoration: none;
|
||||
font-size: ${typography.fontSizeS};
|
||||
}
|
||||
`;
|
||||
2
src/components/shared/theme.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export {default, theme, Theme} from '../../theme';
|
||||
export * from '../../theme';
|
||||
82
src/components/shared/utils.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
import moment, {DurationInputArg1, unitOfTime} from 'moment';
|
||||
import PlatformDetectionService from 'services/platformDetection.service';
|
||||
|
||||
export const truncateWord = (word = '', length = 30): string => {
|
||||
if (word.length >= length) {
|
||||
return `${word.substring(0, length)}...`;
|
||||
}
|
||||
|
||||
return word;
|
||||
};
|
||||
|
||||
export const isEqual = (a: unknown, b: unknown): boolean => {
|
||||
if (typeof a === typeof b) {
|
||||
if (typeof a === 'object') {
|
||||
return JSON.stringify(a) === JSON.stringify(b);
|
||||
}
|
||||
|
||||
return a === b;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
export const splitWord = (string = ' ', seperator = ' '): string => {
|
||||
return string.split(/(?=[A-Z])/).join(seperator);
|
||||
};
|
||||
|
||||
export const mergeSimilarCSVFiles = (formerCSV: string, latterCSV: string): string => {
|
||||
const formerCsvArray = formerCSV.split('\n').filter(line => line.trim() !== '');
|
||||
const latterCsvArray = latterCSV.split('\n').filter(line => line.trim() !== '');
|
||||
|
||||
if (latterCsvArray.length > 1) {
|
||||
return `${formerCsvArray.join('\n')}\n${latterCsvArray.splice(1).join('\n')}`;
|
||||
}
|
||||
|
||||
return formerCSV;
|
||||
};
|
||||
|
||||
export const chunk = <T>(array: T[], size: number = 1): T[][] => {
|
||||
const arrayChunks: T[][] = [];
|
||||
|
||||
for (let i = 0; i < array.length; i += size) {
|
||||
const arrayChunk = array.slice(i, i + size);
|
||||
|
||||
arrayChunks.push(arrayChunk);
|
||||
}
|
||||
|
||||
return arrayChunks;
|
||||
};
|
||||
|
||||
export const getRangeByInterval = (start: Date, end: Date, intervalAmount: DurationInputArg1 = 24, intervalUnits: unitOfTime.DurationConstructor = 'hours') => {
|
||||
const datesArray = [];
|
||||
let currentDate = moment.utc(start);
|
||||
const stopDate = moment.utc(end);
|
||||
|
||||
while (currentDate.isSameOrBefore(stopDate)) {
|
||||
datesArray.push(currentDate.toISOString());
|
||||
currentDate = currentDate.add(intervalAmount, intervalUnits);
|
||||
}
|
||||
|
||||
return datesArray.reverse();
|
||||
};
|
||||
|
||||
export const getBrowserLimits = (): IBrowserLimits => browserLimits[PlatformDetectionService.browserName.toLowerCase()];
|
||||
|
||||
export const getMaxByPropertyName = (array: Record<string, number | string | boolean>[], propertyName: string): number => {
|
||||
let length = array.length;
|
||||
let max = -Infinity;
|
||||
|
||||
while (length--) {
|
||||
const element = parseFloat(array[length][propertyName] as string);
|
||||
|
||||
if (isFinite(element) && element > max) {
|
||||
max = element;
|
||||
}
|
||||
}
|
||||
|
||||
return max;
|
||||
};
|
||||
4
src/components/table-screen-header/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
export {default as TableScreenHeader} from './table-screen-header';
|
||||
37
src/components/table-screen-header/style.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
import * as styled from 'styled-components';
|
||||
import Theme from 'theme';
|
||||
|
||||
const {colors} = Theme;
|
||||
|
||||
export const ScreenHeader = styled.default.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
flex-wrap: wrap;
|
||||
width: 100%;
|
||||
margin: 0 0 .5rem;
|
||||
`;
|
||||
|
||||
export const ScreenHeaderControls = styled.default.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
export const HeaderControlWrapper = styled.default.div`
|
||||
margin-left: 12px;
|
||||
`;
|
||||
|
||||
export const HeaderTitle = styled.default.div`
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
h2 {
|
||||
color: ${colors.white};
|
||||
margin: 0 .5rem 0 0;
|
||||
}
|
||||
p {
|
||||
color: ${colors.gray200};
|
||||
}
|
||||
`;
|
||||
35
src/components/table-screen-header/table-screen-header.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
import {JSX} from 'react';
|
||||
import {ITableWithLoadMoreHeader, ITableWithPaginationHeader, TableHeaderKey} from 'interfaces/tableProps';
|
||||
import {ScreenHeader, ScreenHeaderControls, HeaderControlWrapper, HeaderTitle} from './style';
|
||||
|
||||
type ScreenHeaderProps = ITableWithLoadMoreHeader | ITableWithPaginationHeader;
|
||||
|
||||
export interface ITableScreenHeader {
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
screenHeader: ScreenHeaderProps;
|
||||
renderControl: (screenHeader: ScreenHeaderProps, key: TableHeaderKey) => JSX.Element | null;
|
||||
}
|
||||
|
||||
export const TableScreenHeader = ({title, subtitle = '', screenHeader, renderControl}: ITableScreenHeader): JSX.Element => {
|
||||
return (
|
||||
<ScreenHeader className="table-header">
|
||||
<HeaderTitle>
|
||||
<h2>{title}</h2>
|
||||
{subtitle && <p>{subtitle}</p>}
|
||||
</HeaderTitle>
|
||||
<ScreenHeaderControls>
|
||||
{Object.keys(screenHeader).map(key => {
|
||||
const headerControl = screenHeader[key].render ? screenHeader[key].render(key) : renderControl(screenHeader, key as TableHeaderKey);
|
||||
|
||||
return headerControl ? <HeaderControlWrapper key={key}>{headerControl}</HeaderControlWrapper> : null;
|
||||
})}
|
||||
</ScreenHeaderControls>
|
||||
</ScreenHeader>
|
||||
);
|
||||
};
|
||||
|
||||
export default TableScreenHeader;
|
||||
6
src/components/table-with-pagination/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
export * from 'components/table';
|
||||
export {default as TableWithPagination} from './table-with-pagination';
|
||||
export {default as Pagination} from '../pagination/pagination';
|
||||
192
src/components/table-with-pagination/table-with-pagination.tsx
Normal file
@@ -0,0 +1,192 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
import {DataRowType, Table, TableHeaderKey, ITable, ITableWithPaginationHeader, ITableSort} from 'components/table';
|
||||
import {isEqual} from 'utility/validators';
|
||||
import SearchInput from 'components/forms/SearchInput';
|
||||
import {compare} from 'utility/sort';
|
||||
import {TableScreenHeader} from 'components/table-screen-header/table-screen-header';
|
||||
import {AddButton} from 'components/buttons/icon-buttons';
|
||||
import {Pagination} from 'components/pagination/pagination';
|
||||
import {Select} from 'components/ui/select';
|
||||
|
||||
import {useSort, useSearch} from 'utility/custom-hooks';
|
||||
|
||||
interface ITableWithPagination extends ITable {
|
||||
screenHeader: ITableWithPaginationHeader;
|
||||
paginationItemText?: string;
|
||||
getCurrentDisplayList?: (data: Record<string, string | number | null>[]) => void;
|
||||
}
|
||||
|
||||
const TableWithPagination = ({
|
||||
title = '',
|
||||
screenHeader,
|
||||
columns,
|
||||
data,
|
||||
sortColumn,
|
||||
sortDirection,
|
||||
style,
|
||||
paginationItemText,
|
||||
errorMessage,
|
||||
getCurrentDisplayList,
|
||||
changeSortProps,
|
||||
searchValue: propsSearchValue = '',
|
||||
changeSearch
|
||||
}: ITableWithPagination): JSX.Element => {
|
||||
const [currentPageNumber, setCurrentPageNumber] = useState<number>(1);
|
||||
const [currentData, setCurrentData] = useState<DataRowType[]>([]);
|
||||
const [filteredData, setFilteredData] = useState<DataRowType[]>(data || []);
|
||||
const [total, setTotal] = useState<number>(data ? data.length : 0);
|
||||
const [rowsCount, setRowsCount] = useState(10);
|
||||
const [sortData, setSortData] = useSort({
|
||||
sortColumn,
|
||||
sortDirection
|
||||
});
|
||||
const [searchValue, setSearchValue] = useSearch(propsSearchValue);
|
||||
const amendMarginsInElementsHeight = (selector: string, action: 'add' | 'remove') => {
|
||||
const element = document.querySelector(selector);
|
||||
let elementHeight = element?.clientHeight || 0;
|
||||
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (action === 'add') {
|
||||
elementHeight += parseInt(window?.getComputedStyle(element).getPropertyValue('margin-top'));
|
||||
elementHeight += parseInt(window?.getComputedStyle(element).getPropertyValue('margin-bottom'));
|
||||
} else {
|
||||
elementHeight -= parseInt(window?.getComputedStyle(element).getPropertyValue('margin-top'));
|
||||
elementHeight -= parseInt(window?.getComputedStyle(element).getPropertyValue('margin-bottom'));
|
||||
}
|
||||
|
||||
return elementHeight;
|
||||
};
|
||||
|
||||
const calculateRowsCount = () => {
|
||||
const tableContainerHeight = amendMarginsInElementsHeight('.table-container', 'remove') || 0;
|
||||
const paginationHeight = amendMarginsInElementsHeight('.pagination-container', 'add') || 0;
|
||||
const tableHeaderHeight = amendMarginsInElementsHeight('.table-header', 'add') || 0;
|
||||
const tableRow = document.querySelector('.table-row') || {clientHeight: 50};
|
||||
const rowHeight = tableRow?.clientHeight || 50;
|
||||
const newRowsCount = Math.round((tableContainerHeight - paginationHeight - tableHeaderHeight) / rowHeight);
|
||||
|
||||
setRowsCount(newRowsCount || rowsCount);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
let pxRatioBeforeZoom = window.devicePixelRatio;
|
||||
let windowHeightBeforeResize = window.innerHeight;
|
||||
const trackResize = e => {
|
||||
const pxRatioAfterZoom = e.devicePixelRatio;
|
||||
const windowHeightAfterResize = e.target.innerHeight;
|
||||
|
||||
if (pxRatioAfterZoom !== pxRatioBeforeZoom || windowHeightAfterResize !== windowHeightBeforeResize) {
|
||||
pxRatioBeforeZoom = pxRatioAfterZoom;
|
||||
windowHeightBeforeResize = windowHeightAfterResize;
|
||||
|
||||
calculateRowsCount();
|
||||
}
|
||||
};
|
||||
|
||||
calculateRowsCount();
|
||||
|
||||
window.addEventListener('resize', trackResize);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', trackResize);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
let newData = Array.isArray(data) ? [...data] : [];
|
||||
|
||||
if (searchValue && Array.isArray(data)) {
|
||||
newData = data.filter(prop => {
|
||||
return Object.keys(columns).some(key => {
|
||||
return String(prop[key]).toLowerCase().includes(searchValue.toLowerCase());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (!isEqual(newData, filteredData)) {
|
||||
setFilteredData(newData);
|
||||
}
|
||||
|
||||
if (!isEqual(total, newData.length)) {
|
||||
setTotal(newData.length);
|
||||
}
|
||||
}, [data, searchValue, rowsCount, columns]);
|
||||
|
||||
useEffect(() => {
|
||||
const start = (currentPageNumber - 1) * rowsCount;
|
||||
const sortedData = sortData ? filteredData.sort((a, b) => compare(a, b, sortData.sortDirection, sortData.sortColumn)) : filteredData;
|
||||
const newCurrentData = sortedData.slice(start, start + rowsCount);
|
||||
|
||||
if (!isEqual(newCurrentData, currentData)) {
|
||||
setCurrentData(newCurrentData);
|
||||
|
||||
if (getCurrentDisplayList) {
|
||||
getCurrentDisplayList(newCurrentData);
|
||||
}
|
||||
}
|
||||
}, [filteredData, currentPageNumber, rowsCount, sortData]);
|
||||
|
||||
const search = (val: string) => {
|
||||
if (val !== searchValue) {
|
||||
if (changeSearch) {
|
||||
changeSearch({searchValue: val});
|
||||
} else {
|
||||
setSearchValue(val);
|
||||
}
|
||||
|
||||
setCurrentPageNumber(1);
|
||||
}
|
||||
};
|
||||
|
||||
const sort = (data: ITableSort) => {
|
||||
if (changeSortProps) {
|
||||
changeSortProps(data);
|
||||
} else {
|
||||
setSortData(data);
|
||||
}
|
||||
};
|
||||
|
||||
const renderControl = (screenHeader: ITableWithPaginationHeader, key: TableHeaderKey) => {
|
||||
switch (key) {
|
||||
case TableHeaderKey.Search:
|
||||
return <SearchInput search={search} defaultValue={searchValue} />;
|
||||
case TableHeaderKey.AddRow:
|
||||
return <AddButton onClick={screenHeader[key]?.openAddRowModal} className="testId-hashAddButton" />;
|
||||
case TableHeaderKey.SelectType:
|
||||
return <Select {...screenHeader[key]} />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{display: 'flex', flexDirection: 'column', height: '100%'}}>
|
||||
<div style={{flexShrink: 0}}>
|
||||
<TableScreenHeader title={title} screenHeader={screenHeader} renderControl={renderControl} />
|
||||
</div>
|
||||
<div style={{flex: 1, overflow: 'auto', minHeight: 0, position: 'relative'}}>
|
||||
<div style={{overflow: 'visible'}}>
|
||||
<Table columns={columns} data={currentData} sortColumn={sortColumn} sortDirection={sortDirection} style={style} sort={sort} errorMessage={errorMessage} />
|
||||
</div>
|
||||
</div>
|
||||
<div style={{flexShrink: 0}} className="pagination-container">
|
||||
<Pagination
|
||||
currentPageNumber={currentPageNumber}
|
||||
setCurrentPage={setCurrentPageNumber}
|
||||
numberOfItems={total}
|
||||
itemsPerPage={rowsCount}
|
||||
itemText={paginationItemText}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TableWithPagination;
|
||||