maintenance

This commit is contained in:
2025-09-04 20:25:15 -04:00
parent 1469c7f52f
commit e8f2df9e69
214 changed files with 8507 additions and 1836 deletions

View File

@@ -1,11 +1,19 @@
import {JSX} from 'react';
import React from 'react';
import styled from 'styled-components';
import Router from './routers';
import Theme from './theme';
const App = (): JSX.Element => {
const AppContainer = styled.div`
min-height: 100vh;
background: ${Theme.backgrounds.defaultBackground};
background-attachment: fixed;
`;
const App = (): React.JSX.Element => {
return (
<>
<AppContainer>
<Router />
</>
</AppContainer>
);
};

View File

@@ -1,9 +1,9 @@
import {JSX} from 'react';
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: JSX.Element}): JSX.Element {
export function ProtectedRoute({component}: {component: React.JSX.Element}): React.JSX.Element {
const isAuthenticated = useAppSelector(selectIsAuthenticated);
const location = useLocation();

View File

@@ -1,7 +1,7 @@
/**
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
*/
import {useState} from 'react';
import React, {useState} from 'react';
import {faCopy, faCheck} from '@fortawesome/free-solid-svg-icons';
import IconButton from 'components/buttons/icon-button';
@@ -9,7 +9,7 @@ import {CopyButtonContainer} from './styles';
const iconChangeTimeout = 2000;
export const CopyIconButton = (props: {text: string; quoted?: boolean; displayText?: boolean; className?: string}): JSX.Element => {
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 => {
@@ -22,11 +22,7 @@ export const CopyIconButton = (props: {text: string; quoted?: boolean; displayTe
return (
<CopyButtonContainer className={className}>
{displayText && (quoted ? `"${text}"` : text)}
<IconButton
onClick={copyToClipboard}
tooltipText="Copy"
icon={copied ? faCheck : faCopy}
/>
<IconButton onClick={copyToClipboard} tooltipText="Copy" icon={copied ? faCheck : faCopy} />
</CopyButtonContainer>
);
};
};

View File

@@ -6,4 +6,4 @@ import * as styled from 'styled-components';
export const CopyButtonContainer = styled.default.div`
display: flex;
align-items: center;
`;
`;

View File

@@ -1,6 +1,7 @@
/**
* 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';
@@ -12,7 +13,7 @@ interface IExportFileButton {
fileName?: string;
}
export const ExportFileButton = ({label = 'Export File', file, fileName = 'file'}: IExportFileButton): JSX.Element => {
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');
@@ -24,13 +25,8 @@ export const ExportFileButton = ({label = 'Export File', file, fileName = 'file'
};
return (
<Button
onClick={handleExport}
className="testId-exportFile"
backgroundColor={colors.red}
borderColor={colors.red}
>
<Button onClick={handleExport} className="testId-exportFile" backgroundColor={colors.red} borderColor={colors.red}>
{label}
</Button>
);
};
};

View File

@@ -16,17 +16,12 @@ interface IIconButton {
className?: string;
}
const IconButton = ({
onClick,
tooltipText,
icon,
className
}: IIconButton) => (
const IconButton = ({onClick, tooltipText, icon, className}: IIconButton) => (
<Tooltip position={Position.Bottom} message={tooltipText}>
<IconButtonContainer className={`icon-button ${className}`} role="link" tabIndex={-11} onKeyDown={null} onClick={onClick}>
<IconButtonContainer className={`icon-button ${className}`} role="link" tabIndex={-11} onKeyDown={undefined} onClick={onClick}>
<FontAwesomeIcon icon={icon} />
</IconButtonContainer>
</Tooltip>
);
export default IconButton;
export default IconButton;

View File

@@ -2,4 +2,4 @@
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
*/
export {default} from './icon-button';
export {default} from './icon-button';

View File

@@ -27,4 +27,4 @@ export const IconButtonContainer = styled.default.div`
&:hover {
background-color: rgba(255, 255, 255, 0.1);
}
`;
`;

View File

@@ -1,6 +1,7 @@
/**
* 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';
@@ -9,8 +10,8 @@ interface IAddButton {
className: string;
}
export const AddButton = ({onClick, className}: IAddButton): JSX.Element => (
export const AddButton = ({onClick, className}: IAddButton): React.JSX.Element => (
<IconButton onClick={onClick} className={className}>
<img src={addIcon} alt={'Add'} />
</IconButton>
);
);

View File

@@ -2,4 +2,4 @@
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
*/
export * from './refresh-button';
export * from './add-button';
export * from './add-button';

View File

@@ -1,6 +1,7 @@
/**
* 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';
@@ -10,8 +11,8 @@ interface IRefreshButton {
disabled?: boolean;
}
export const RefreshButton = ({onClick, disabled = false}: IRefreshButton): JSX.Element => (
export const RefreshButton = ({onClick, disabled = false}: IRefreshButton): React.JSX.Element => (
<IconButton onClick={onClick} disabled={disabled}>
<img src={refreshIcon} alt={'Refresh'} />
</IconButton>
);
);

View File

@@ -4,14 +4,14 @@
import * as styled from 'styled-components';
import {theme} from 'components/shared/theme';
const {fontSizeL, colors} = 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'};
opacity: ${({disabled}) => (disabled ? 0.3 : 1)};
cursor: ${({disabled}) => (disabled ? 'not-allowed' : 'pointer')};
display: flex;
`;
`;

View File

@@ -2,7 +2,7 @@
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
*/
import * as styled from 'styled-components';
import Theme from 'theme';
import Theme from 'theme';
export const Button = styled.default.button<{
backgroundColor?: string;
@@ -54,4 +54,4 @@ export const CustomButton = styled.default(Button)`
margin: 0.75rem;
padding: ${Theme.spacing.small} ${Theme.spacing.xlarge};
overflow: visible;
`;
`;

View File

@@ -1,15 +1,10 @@
/**
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
*/
import {Fragment} from 'react';
import React, {Fragment} from 'react';
import {Tooltip, Position} from 'components/tooltip';
import {Label} from 'components/label';
import {
RadioGroup,
RadioWrapper,
RadioButtonContainer,
VisibleCheckBox
} from './style';
import {RadioGroup, RadioWrapper, RadioButtonContainer, VisibleCheckBox} from './style';
interface IRadioItems {
label: string;
@@ -17,13 +12,13 @@ interface IRadioItems {
tooltipMessage?: string;
tooltipPosition?: Position;
className?: string;
children?: JSX.Element;
children?: React.JSX.Element;
disabled?: boolean;
}
interface IRadioButtonGroup {
items: IRadioItems[];
handleOnChange: (value) => void;
handleOnChange: (value: string) => void;
currentValue: string;
}
@@ -32,30 +27,22 @@ const RadioButton = (props: {currentValue: string; value: string}) => {
return (
<RadioButtonContainer>
<input type="radio" readOnly={true} value={value} checked={currentValue === value}/>
<VisibleCheckBox checked={currentValue === value}><div/></VisibleCheckBox>
<input type="radio" readOnly={true} value={value} checked={currentValue === value} />
<VisibleCheckBox checked={currentValue === value}>
<div />
</VisibleCheckBox>
</RadioButtonContainer>
);
};
const RadioButtonGroup = (props: IRadioButtonGroup): JSX.Element => {
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}
{items.map(({label, value, disabled, tooltipPosition, tooltipMessage, children, className}: IRadioItems, index: number) => (
<RadioWrapper
tabIndex={-1}
onKeyPress={() => null}
disabled={disabled}
className="button-container"
@@ -65,18 +52,12 @@ const RadioButtonGroup = (props: IRadioButtonGroup): JSX.Element => {
<RadioButton value={value} currentValue={currentValue} />
<Fragment>
{tooltipMessage ? (
<Tooltip position={tooltipPosition} message={tooltipMessage}>
<Label
className={className}
text={label}
/>
<Tooltip position={tooltipPosition || Position.Top} message={tooltipMessage}>
<Label className={className} text={label} />
</Tooltip>
) :
<Label
className={className}
text={label}
/>
}
) : (
<Label className={className} text={label} />
)}
{children}
</Fragment>
</RadioWrapper>
@@ -85,4 +66,4 @@ const RadioButtonGroup = (props: IRadioButtonGroup): JSX.Element => {
);
};
export default RadioButtonGroup;
export default RadioButtonGroup;

View File

@@ -2,13 +2,10 @@
* 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 {theme, Theme} from 'components/shared/theme';
const {
spacing,
primaryFontSize,
colors
} = theme;
const {spacing, typography: {primaryFontSize}, colors} = theme;
const paddings = Theme.paddings;
export const RadioGroup = styled.default.div`
display: flex;
@@ -18,10 +15,12 @@ export const RadioWrapper = styled.default.div<{disabled?: boolean}>`
padding: ${paddings.small};
display: flex;
${({disabled}) => disabled && styled.css`
pointer-events: none;
opacity: .5;
`}
${({disabled}) =>
disabled &&
styled.css`
pointer-events: none;
opacity: 0.5;
`}
`;
export const RadioButtonContainer = styled.default.div`
@@ -43,15 +42,17 @@ export const VisibleCheckBox = styled.default.div<{checked?: boolean}>`
height: ${primaryFontSize};
border-radius: 50%;
${({checked}) => checked && styled.css`
border: none;
background-color: ${colors.red};
${({checked}) =>
checked &&
styled.css`
border: none;
background-color: ${colors.red};
div {
background-color: ${colors.black};
width: 5px;
height: 5px;
border-radius: 50%;
}
`}
`;
div {
background-color: ${colors.black};
width: 5px;
height: 5px;
border-radius: 50%;
}
`}
`;

View File

@@ -1,15 +1,11 @@
/**
* 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';
import {faBackward, faFastBackward, faFastForward, faForward} from '@fortawesome/free-solid-svg-icons';
const {colors, spacing} = theme;
const ScrollButton = styled.default.button`
@@ -30,8 +26,8 @@ const TwinButtons = styled.default.div`
flex-direction: column;
`;
export const ScrollButtons = ({current}: {current: HTMLDivElement}): JSX.Element => {
const getScrollStep = current => {
export const ScrollButtons = ({current}: {current: HTMLDivElement}): React.JSX.Element => {
const getScrollStep = (current: HTMLDivElement) => {
const tableViewHeight = current.offsetHeight;
return tableViewHeight / 2;
@@ -77,4 +73,4 @@ export const ScrollButtons = ({current}: {current: HTMLDivElement}): JSX.Element
</ScrollButton>
</TwinButtons>
);
};
};

View File

@@ -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;

View File

@@ -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;

View File

@@ -0,0 +1,4 @@
/**
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
*/
export {default} from './delete-channel-modal';

View File

@@ -0,0 +1,107 @@
/**
* 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;

View File

@@ -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;

View File

@@ -0,0 +1,4 @@
/**
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
*/
export {default} from './fork-channel-modal';

View 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} />}
</>
);
};

View File

@@ -0,0 +1,4 @@
/**
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
*/
export {default} from './kill-channel-modal';

View File

@@ -0,0 +1,129 @@
/**
* 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;

View File

@@ -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;

View File

@@ -0,0 +1,62 @@
/**
* 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;

View 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';

View File

@@ -0,0 +1,44 @@
/**
* 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>
);
};

View 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 {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;

View File

@@ -0,0 +1,60 @@
/**
* 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;
`;

View File

@@ -0,0 +1,60 @@
/**
* 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;

View File

@@ -0,0 +1,51 @@
/**
* 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>
);
};

View File

@@ -0,0 +1,212 @@
/**
* 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>
);
};

View File

@@ -0,0 +1,59 @@
/**
* 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'};
}
`}
`;

View 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
});
};

View 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};
`;

View File

@@ -0,0 +1,83 @@
/**
* 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;

View File

@@ -0,0 +1,88 @@
/**
* 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;

View File

@@ -1,58 +1,48 @@
/**
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
*/
import {ChangeEvent, forwardRef, ForwardedRef, InputHTMLAttributes} from 'react';
import React, {ChangeEvent, forwardRef, ForwardedRef, InputHTMLAttributes} from 'react';
import * as styled from 'styled-components';
import Theme from 'theme';
import {Label} from '../label';
import {Label} from './label';
export interface IInput extends InputHTMLAttributes<HTMLInputElement> {
onChange?: (event: ChangeEvent<HTMLInputElement>) => void;
error?: boolean;
icon?: JSX.Element;
icon?: React.JSX.Element;
imagePath?: string;
imageAltText?: string;
label?: string;
labelColor?: string;
labelIcon?: JSX.Element;
labelIcon?: React.JSX.Element;
labelClassName?: string;
helperText?: string;
helperTextClassName?: string;
width?: number | string;
}
const {
colors,
typography,
formFieldWidth,
formFieldMaxWidth,
primaryBorderColor,
primaryBorderRadius,
primaryInputHeight,
inputIconWidth,
spacing
} = Theme;
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: 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};
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};
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};
color: ${({error}) => (error ? colors.lightRed : colors.gray600)};
font-weight: 400;
font-size: ${typography.fontSizeS};
margin-top: ${spacing.xxSmall};
@@ -79,56 +69,49 @@ const InputWrapper = styled.default.div`
export const InputComponentWrapper = styled.default.div<IInput>`
position: relative;
width: ${({width}) => width && isNaN(+width) ? width : ((width || formFieldWidth) + 'px')};
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>): JSX.Element => {
const InputIcon = icon || (imagePath && <img src={imagePath} alt={imageAltText} />);
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>
);
});
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 default InputComponent;
export const Input = InputComponent;

View File

@@ -0,0 +1,77 @@
/**
* 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;

View File

@@ -0,0 +1 @@
export * from './label';

View File

@@ -1,23 +1,19 @@
/**
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
*/
import {JSX} from 'react';
import React from 'react';
import {Label as StyledLabel} from './style';
interface ILabel {
text: string;
htmlFor?: string;
color?: string;
icon?: JSX.Element;
icon?: React.JSX.Element;
className?: string;
}
export const Label = ({text, htmlFor, color, icon, className}: ILabel): JSX.Element => (
<StyledLabel
className={className}
htmlFor={htmlFor}
color={color}
>
export const Label = ({text, htmlFor, color, icon, className}: ILabel): React.JSX.Element => (
<StyledLabel className={className} htmlFor={htmlFor} color={color}>
{text} {icon}
</StyledLabel>
);
);

View File

@@ -8,8 +8,8 @@ const {spacing, colors, typography} = Theme;
export const Label = styled.default.label<{color?: string}>`
font-size: ${typography.fontSizeS};
color: ${({color}) => (color || colors.gray900)};
color: ${({color}) => color || colors.gray900};
font-weight: bold;
margin: ${spacing.xxSmall} 0;
display: block;
`;
`;

View 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;

View 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';

View 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);
`;

View 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};

View File

@@ -1,4 +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 './typography';
export * from './table';
export * from './typography';

View File

@@ -0,0 +1,4 @@
/**
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
*/
export * from './indicators';

View File

@@ -0,0 +1,11 @@
/**
* 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>;

View 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;
`;

View File

@@ -0,0 +1,41 @@
/**
* 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;

View File

@@ -0,0 +1 @@
export * from '../forms/label';

View 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';
interface IColumn {
align?: string;
size?: number;
padding?: string;
}
const {footerHeight} = Theme;
export const AppContainer = styled.default.div`
margin: 3.5rem 0 ${footerHeight};
height: calc(100vh - 3.5rem - ${footerHeight});
overflow: auto;
display: flex;
overflow-x: hidden;
`;
export const Body = styled.default.div`
width: 100%;
margin: 4rem 0;
padding: 0 ${Theme.spacing.xlarge};
background: ${Theme.colors.gray900};
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
min-height: calc(100vh - 2rem);
overflow: auto;
max-height: calc(100vh - 2rem);
max-width: calc(100vw - 2rem);
`;
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;
`;

View File

@@ -2,7 +2,7 @@
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
*/
import * as styled from 'styled-components';
import Theme from 'theme';
import Theme from 'theme';
interface LoadingWheelProps {
size?: 'small' | 'medium' | 'large';
@@ -15,11 +15,11 @@ const LoadingWheelContainer = styled.default.div<{
color: string;
}>`
display: inline-block;
width: ${({ size }) => size}px;
height: ${({ size }) => size}px;
border: 3px solid ${({ color }) => color}20;
width: ${({size}) => size}px;
height: ${({size}) => size}px;
border: 3px solid ${({color}) => color}20;
border-radius: 50%;
border-top-color: ${({ color }) => color};
border-top-color: ${({color}) => color};
animation: spin 1s ease-in-out infinite;
@keyframes spin {
@@ -29,26 +29,14 @@ const LoadingWheelContainer = styled.default.div<{
}
`;
export const LoadingWheel: React.FC<LoadingWheelProps> = ({
size = 'medium',
color = Theme.colors.white,
className
}) => {
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"
/>
);
return <LoadingWheelContainer size={sizeMap[size]} color={color} className={className} role="status" aria-label="Loading" />;
};
export default LoadingWheel;

View File

@@ -1,4 +1,4 @@
/**
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
*/
export * from './LoadingWheel';
export * from './LoadingWheel';

View File

@@ -0,0 +1,5 @@
/**
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
*/
export {default as Modal} from './modal';

View 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;

View 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';

View 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;
`;

View File

@@ -0,0 +1,87 @@
/**
* 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;

View 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';

View 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;

View 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;
}
`;

View 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;

View 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';

View File

@@ -0,0 +1,87 @@
/**
* 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;

View File

@@ -0,0 +1,45 @@
/**
* 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};
`;

View File

@@ -0,0 +1,5 @@
/**
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
*/
export {default} from './pre-formatted-code';

View 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;

View 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;
}
`;

View 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';

View File

@@ -0,0 +1,34 @@
/**
* 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;

View File

@@ -0,0 +1,115 @@
/**
* 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;

View File

@@ -0,0 +1,96 @@
/**
* 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};
}
`;

View File

@@ -0,0 +1,2 @@
export {default, theme, Theme} from '../../theme';
export * from '../../theme';

View File

@@ -12,9 +12,9 @@ export const truncateWord = (word = '', length = 30): string => {
return word;
};
export const isEqual = (a: any, b: any): boolean => { // eslint-disable-line
if (typeof(a) === typeof(b)) {
if (typeof(a) === 'object') {
export const isEqual = (a: unknown, b: unknown): boolean => {
if (typeof a === typeof b) {
if (typeof a === 'object') {
return JSON.stringify(a) === JSON.stringify(b);
}
@@ -39,8 +39,8 @@ export const mergeSimilarCSVFiles = (formerCSV: string, latterCSV: string): stri
return formerCSV;
};
export const chunk = (array: any[], size: number = 1) => { // eslint-disable-line
const arrayChunks = [];
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);
@@ -66,10 +66,7 @@ export const getRangeByInterval = (start: Date, end: Date, intervalAmount: Durat
export const getBrowserLimits = (): IBrowserLimits => browserLimits[PlatformDetectionService.browserName.toLowerCase()];
export const getMaxByPropertyName = (
array: Record<string, number | string | boolean>[],
propertyName: string
): number => {
export const getMaxByPropertyName = (array: Record<string, number | string | boolean>[], propertyName: string): number => {
let length = array.length;
let max = -Infinity;
@@ -82,4 +79,4 @@ export const getMaxByPropertyName = (
}
return max;
};
};

View 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';

View 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};
}
`;

View File

@@ -0,0 +1,48 @@
/**
* 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;

View 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';

View File

@@ -0,0 +1,205 @@
/**
* 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 (
<>
<TableScreenHeader
title={title}
screenHeader={screenHeader}
renderControl={renderControl}
/>
<Table
columns={columns}
data={currentData}
sortColumn={sortColumn}
sortDirection={sortDirection}
style={style}
sort={sort}
errorMessage={errorMessage}
/>
<Pagination
currentPageNumber={currentPageNumber}
setCurrentPage={setCurrentPageNumber}
numberOfItems={total}
itemsPerPage={rowsCount}
itemText={paginationItemText}
/>
</>
);
};
export default TableWithPagination;

View File

@@ -0,0 +1,8 @@
/**
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
*/
export * from 'interfaces/tableProps';
export {default as Table} from './table';
export * from './table-cells';
export * from './props';
export * from './style';

View File

@@ -0,0 +1,74 @@
/**
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
*/
import React, {CSSProperties} from 'react';
import {DirectionType, ITableSearch, ITableSort} from 'interfaces/tableProps';
import {ITableCellDropdown} from '.';
export enum CellType {
Text = 'text',
Link = 'link',
Date = 'date',
DateTime = 'datetime',
DropDown = 'dropdown',
Component = 'component'
}
export type DataValueType = string | number | null | undefined;
export type DataRowType = Record<string, DataValueType>;
interface ITableCellText {
propName: string;
hasShow?: boolean;
hasCopy?: boolean;
isSortable?: boolean;
direction?: DirectionType;
sortable?: boolean;
mutate?: (value: DataValueType | boolean) => string | number;
}
export interface ITableColumn {
title: string;
type?: CellType;
textCell?: ITableCellText;
renderCell?: (row?: DataRowType) => React.JSX.Element;
dropdownCell?: Omit<ITableCellDropdown, 'row'>;
thStyle?: CSSProperties;
tdStyle?: CSSProperties;
hasBorder?: boolean;
sortAction?: (key: string) => void;
hideColumnAt?: number;
isHidden?: boolean;
width?: number;
path?: string;
}
export type ColumnsType = Record<string, ITableColumn>;
export interface ITableRow {
columns: ColumnsType;
row: DataRowType;
}
export interface ITable {
title?: string;
columns: ColumnsType;
className?: string;
data?: DataRowType[];
tableName?: string;
addRow?: (row?: DataRowType) => void;
searchProps?: string[];
sortColumn?: string;
sortDirection?: DirectionType;
style?: CSSProperties;
tableHeader?: Element;
tableBottom?: Element;
sort?: (data: ITableSort) => void;
errorMessage?: null | string;
isFetching?: boolean;
hasMore?: boolean;
searchValue?: string;
changeSearch?: (val: ITableSearch) => void;
changeSortProps?: (data: ITableSort) => void;
}

View File

@@ -0,0 +1,75 @@
/**
* 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, primaryBackground} = Theme;
export const Table = styled.default.table`
width: 100%;
max-height: calc(100vh - 2rem);
max-width: calc(100vw - 2rem);
border-collapse: collapse;
margin: 0;
table-layout: fixed;
background-color: ${Theme.blackWithOpacity};
`;
export const Thead = styled.default.thead`
color: ${colors.white};
& tr {
vertical-align: middle;
border: none;
& th {
top: -1px;
background-color: ${primaryBackground};
padding: ${Theme.spacing.small} 0;
z-index: 10;
&:last-child {
border: none;
}
}
}
`;
export const TH = styled.default.th<{active?: boolean; border?: boolean; width?: number}>`
color: ${({active}) => (active ? colors.white : colors.gray500)};
font-size: ${typography.fontSizeS};
cursor: pointer;
border: 1px solid transparent;
border-right-color: ${colors.gray700};
& span {
margin-left: .4rem;
}
`;
export const Tbody = styled.default.tbody`
tr:nth-child(odd) {
background: ${colors.gray800};
}
& tr {
border-color: transparent transparent ${colors.black} transparent;
height: 3rem;
& td {
color: ${colors.white};
padding: ${Theme.spacing.small};
word-wrap: break-word;
font-size: ${typography.fontSizeS};
position: relative;
text-align: center;
& > div,
& > p {
text-align: center;
justify-content: center;
}
& a {
color: ${colors.lightBlue};
text-decoration: none;
}
}
}
`;

View File

@@ -0,0 +1,18 @@
/**
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
*/
import React from 'react';
import TableRow from './table-row';
import {Tbody} from './style';
import {ColumnsType} from './props';
interface ITableBody {
columns: ColumnsType;
data: Record<string, string | number | null>[];
}
export const TableBody = ({columns, data}: ITableBody): React.JSX.Element => (
<Tbody>{data.length ? data.map((row, idx) => <TableRow key={`tableRow-${idx}-${Date.now()}`} columns={columns} row={row} />) : null}</Tbody>
);
export default TableBody;

View File

@@ -0,0 +1,4 @@
/**
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
*/
export * from './table-cell-dropdown';

View File

@@ -0,0 +1,29 @@
/**
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
*/
import React from 'react';
import {DataRowType} from '../props';
export interface ITableCellDropdown {
row: DataRowType;
title?: string;
icon?: React.JSX.Element;
Component: (props: any) => React.JSX.Element; //eslint-disable-line
keys: string[];
action?: (key: string) => void;
componentProps?: Record<string, unknown>;
}
export const TableCellDropdown = ({title = '', Component, keys, action, componentProps, row}: ITableCellDropdown): React.JSX.Element => {
const data = {} as Record<string, unknown>;
keys.forEach(value => {
data[value] = row[value];
});
if (data.showDropdown !== undefined) {
return data.showDropdown ? <Component title={title} data={data} action={action} {...componentProps} /> : null;
}
return <Component title={title} data={data} action={action} {...componentProps} />;
};

View File

@@ -0,0 +1,66 @@
/**
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
*/
import React from 'react';
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import {faCaretDown, faCaretUp} from '@fortawesome/free-solid-svg-icons';
import {DirectionType} from 'interfaces/tableProps';
import {TH} from './style';
import {ITableColumn, CellType} from './props';
interface ITableHeaderCell {
column: Partial<ITableColumn>;
sortHeaderColumn: string | null;
sortAction?: (key: string) => void;
}
export const TableHeaderCell = ({
column: {type, title, hasBorder = true, textCell, isHidden, width, thStyle},
sortHeaderColumn,
sortAction
}: ITableHeaderCell): React.JSX.Element | null => {
switch (type) {
case CellType.DropDown:
case CellType.Component: {
return (
<TH
border={hasBorder.toString()}
style={{
width,
maxWidth: width,
minWidth: width,
...thStyle
}}>
{title}
</TH>
);
}
case CellType.Date:
case CellType.DateTime:
case CellType.Link:
case CellType.Text:
default: {
const {propName, isSortable = true, direction} = textCell;
const handleOnClick = () => {
if (sortAction && isSortable) {
sortAction(propName);
}
};
const isActive = isSortable && sortAction && sortHeaderColumn && sortHeaderColumn === propName;
return isHidden ? null : (
<TH active={isActive} border={hasBorder.toString()} key={propName} onClick={handleOnClick} width={width} style={thStyle}>
{title}
{isActive && (
<span>
<FontAwesomeIcon icon={direction === DirectionType.Asc ? faCaretDown : faCaretUp} />
</span>
)}
</TH>
);
}
}
};

View File

@@ -0,0 +1,30 @@
/**
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
*/
import React from 'react';
import {Thead} from './style';
import {TableHeaderCell} from './table-header-cell';
import {ColumnsType} from './props';
export interface ITableHeader {
columns: ColumnsType;
sortHeaderColumn: string | null;
sortAction: (key: string) => void;
}
export const TableHeader = ({columns, sortHeaderColumn, sortAction}: ITableHeader): React.JSX.Element => {
const renderHeader = (): React.JSX.Element[] =>
Object.keys(columns).map(
(key: string, index: number): React.JSX.Element => (
<TableHeaderCell key={`table-header-cell-${index}`} column={columns[key]} sortHeaderColumn={sortHeaderColumn} sortAction={sortAction} />
)
);
return (
<Thead>
<tr>{renderHeader()}</tr>
</Thead>
);
};
export default TableHeader;

View File

@@ -0,0 +1,91 @@
/**
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
*/
import React from 'react';
import moment from 'moment';
import {getDateFromNow} from 'utility/date';
import {DateComponent} from 'components/date-render-component';
import {RestrictedText} from 'components/restricted-text';
import {TableCellDropdown} from './table-cells';
import {CellType, ITableRow} from './props';
export const TableRow = ({columns, row}: ITableRow): React.JSX.Element | null => {
if (!row) {
return null;
}
return (
<tr className="table-row">
{Object.keys(columns).map((key, idx) => {
const {isHidden, renderCell, dropdownCell, textCell, type, path = '', tdStyle, width} = columns[key];
if (isHidden) {
return null;
}
if (renderCell) {
return (
<td key={`tableData${idx}`} style={tdStyle}>
{renderCell(row)}
</td>
);
} else if (dropdownCell) {
return (
<td
key={`tableData${idx}`}
style={{
width,
maxWidth: width,
minWidth: width,
...tdStyle
}}>
<TableCellDropdown row={row} {...dropdownCell} />
</td>
);
} else if (textCell) {
const {propName} = textCell;
const value = row[propName] || '';
if (type === CellType.Date) {
return (
<td key={`tableData${idx}`} style={tdStyle}>
{getDateFromNow(value)}
</td>
);
} else if (type === CellType.DateTime) {
return (
<td key={`tableData${idx}`} style={tdStyle}>
<DateComponent date={moment.utc(value)} />
</td>
);
} else if (type === CellType.Link) {
let link = path || '';
if (row.extraPath) {
link += `${path ? '/' : ''}${row.extraPath}`;
}
return (
<td key={`tableData${idx}`} style={tdStyle}>
<div>
<RestrictedText isLink linkValue={link} linkClassName="testId-tableLink" text={`${value}`} />
</div>
</td>
);
}
return (
<td key={`tableData${idx}`} style={tdStyle}>
<RestrictedText index={idx} text={`${value}`} />
</td>
);
}
return <td key={`tableData${idx}`} />;
})}
</tr>
);
};
export default TableRow;

View File

@@ -0,0 +1,107 @@
/**
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
*/
import React, {useEffect, useLayoutEffect, useState} from 'react';
import {DirectionType} from 'interfaces/tableProps';
import {compare} from 'utility/sort';
import {Error} from 'components/error-renderer/style';
import {Table as TableContainer} from './style';
import TableHeader from './table-header';
import TableBody from './table-body';
import {ITable, DataRowType, ColumnsType} from './props';
const defaultHideColumnWidth = 100;
const TableComponent = ({columns, data, sortColumn, sortDirection, sort, errorMessage}: ITable): React.JSX.Element => {
const getHeadersBasedOnWidth = (columns: ColumnsType) => {
const newHeaders: ColumnsType = {};
Object.keys(columns).forEach(key => {
const hideColumnAt = columns[key]?.hideColumnAt || defaultHideColumnWidth;
const isHidden: boolean = columns[key]?.isHidden || window.innerWidth <= hideColumnAt;
newHeaders[key] = {
...columns[key],
isHidden
};
});
return newHeaders;
};
const [sortHeaderColumn, setSortHeaderColumn] = useState<string | null>(sortColumn || null);
const [headers, setHeaders] = useState<ColumnsType>(getHeadersBasedOnWidth(columns));
const [modifiedData, setModifiedData] = useState<DataRowType[]>(data);
useEffect(() => {
if (sortColumn && sortDirection) {
const newHeaders: ColumnsType = {...headers};
if (headers[sortColumn]?.textCell) {
newHeaders[sortColumn] = {
...newHeaders[sortColumn],
textCell: {
...newHeaders[sortColumn].textCell,
direction: sortDirection,
propName: newHeaders[sortColumn].textCell?.propName || sortColumn
}
};
}
setHeaders(newHeaders);
}
}, [sortColumn, sortDirection]);
useEffect(() => {
setModifiedData(data);
}, [data]);
useEffect(() => {
setHeaders(columns);
}, [columns]);
useLayoutEffect(() => {
const updateComponentBasedOnWidth = () => {
setHeaders(getHeadersBasedOnWidth(columns));
};
window.addEventListener('resize', updateComponentBasedOnWidth);
return () => window.removeEventListener('resize', updateComponentBasedOnWidth);
});
const sortAction = (propName: string) => {
if (headers[propName]) {
const newHeaders = {...headers};
const currentDirection = newHeaders[propName].textCell.direction;
const newDirection = currentDirection === DirectionType.Asc ? DirectionType.Desc : DirectionType.Asc;
newHeaders[propName].textCell.direction = newDirection;
if (sort) {
sort({
sortColumn: propName,
sortDirection: newDirection
});
} else {
const newData = modifiedData.sort((a, b) => compare(a, b, newDirection, propName));
setModifiedData(newData);
}
setHeaders(newHeaders);
setSortHeaderColumn(propName);
}
};
return !errorMessage ? (
<TableContainer className="testId-table">
<TableHeader columns={headers} sortHeaderColumn={sortHeaderColumn} sortAction={sortAction} />
<TableBody columns={headers} data={modifiedData} />
</TableContainer>
) : (
<Error>{errorMessage || 'Error'}</Error>
);
};
export default TableComponent;

View File

@@ -0,0 +1,30 @@
/**
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
*/
import {FC, MouseEvent} from 'react';
import {TagContainer} from './style';
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import {faTimes} from '@fortawesome/free-solid-svg-icons';
export interface ITag {
label: string;
onClose?: (event: MouseEvent<HTMLSpanElement>) => void | null;
handleClick?: () => void;
disabled?: boolean;
className?: string;
}
export const Tag: FC<ITag> = ({className, label, onClose = null, handleClick = () => null, disabled = false}: ITag) => {
const onKeyPress = (): void => null;
return (
<TagContainer onClick={handleClick} disabled={disabled} className={className}>
<p>{label}</p>
{onClose && (
<span role="button" tabIndex={-1} onKeyPress={onKeyPress} onClick={onClose}>
<FontAwesomeIcon icon={faTimes} />
</span>
)}
</TagContainer>
);
};

View 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, paddings} from 'components/shared/theme';
const {colors, spacing, fontSizeXS} = theme;
export const TagContainer = styled.default.div<{size?: string; disabled?: boolean}>`
display: inline-flex;
align-items: center;
border-radius: 1rem;
font-weight: 500;
margin: ${spacing.xxSmall};
color: ${colors.white};
background-color: ${colors.gray700};
height: 24px;
padding: 0 ${paddings.small};
font-size: ${fontSizeXS};
${({disabled}) =>
disabled &&
styled.css`
opacity: 0.3;
pointer-events: none;
`}
p {
text-align: center;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
margin: 0;
}
span{
margin-top: 2px;
margin-left: ${spacing.xSmall};
}
`;

View File

@@ -0,0 +1,43 @@
/**
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
*/
import React, {useState} from 'react';
import {TooltipContainer, TooltipBubble} from './style';
import {Position} from './types';
export {Position};
interface ITooltip {
message: string;
position: Position;
children: React.JSX.Element | React.JSX.Element[];
width?: number;
}
export const Tooltip = (props: ITooltip): React.JSX.Element => {
const {message, position, children, width} = props;
const [displayTooltip, setDisplayTooltip] = useState(false);
const showTooltip = () => {
setDisplayTooltip(true);
};
const hideTooltip = () => {
setDisplayTooltip(false);
};
return (
<TooltipContainer onBlur={hideTooltip} onMouseOut={hideTooltip}>
{displayTooltip && (
<TooltipBubble className={`tooltip-${position}`} width={width}>
<div className="tooltip-message">{message}</div>
</TooltipBubble>
)}
<span onFocus={showTooltip} onMouseOver={showTooltip}>
{children}
</span>
</TooltipContainer>
);
};
Tooltip.Position = Position;

View File

@@ -0,0 +1,89 @@
/**
* 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, spacing, typography} = Theme;
export const TooltipContainer = styled.default.span`
position: relative;
`;
export const TooltipBubble = styled.default.div<{width?: number}>`
min-width: 120px;
max-width: 600px;
width: ${({width}) => width}px;
word-wrap: break-word;
position: absolute;
z-index: 999;
&::after {
content: '';
position: absolute;
}
&.tooltip-top {
bottom: 100%;
left: 50%;
padding-bottom: ${spacing.small};
transform: translateX(-50%);
&::after {
border-left: 9px solid transparent;
border-right: 9px solid transparent;
border-top: 9px solid rgba(0,0,0, .7);
bottom: 0;
left: 50%;
transform: translateX(-50%);
}
}
&.tooltip-bottom {
top: 100%;
left: 50%;
padding-top: ${spacing.small};
transform: translateX(-50%);
&::after {
border-left: 9px solid transparent;
border-right: 9px solid transparent;
border-bottom: 9px solid rgba(0,0,0, .7);
top: 0;
left: 50%;
transform: translateX(-50%);
}
}
&.tooltip-left {
top: 50%;
right: 100%;
padding-right: ${spacing.small};
transform: translateY(-50%);
&::after {
border-left: 9px solid rgba(0,0,0, .7);
border-top: 9px solid transparent;
border-bottom: 9px solid transparent;
top: 50%;
right: 0;
transform: translateY(-50%);
}
}
&.tooltip-right {
top: 50%;
left: 100%;
padding-left: ${spacing.small};
transform: translateY(-50%);
&::after {
border-right: 9px solid rgba(0,0,0, .7);
border-top: 9px solid transparent;
border-bottom: 9px solid transparent;
top: 50%;
left: 0;
transform: translateY(-50%);
}
}
.tooltip-message {
background: rgba(0,0,0, .7);
border-radius: 3px;
color: ${colors.white};
font-size: ${typography.fontSizeS};
line-height: 1.4;
padding: ${spacing.small};
text-align: center;
}
`;

View File

@@ -0,0 +1,10 @@
/**
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
*/
export enum Position {
Top = 'top',
Bottom = 'bottom',
Left = 'left',
Right = 'right'
}

View File

@@ -2,8 +2,7 @@
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
*/
import * as styled from 'styled-components';
import Theme from 'theme';
import Theme from 'theme';
export const H1 = styled.default.h1`
font-family: ${Theme.typography.primaryFont};
@@ -28,4 +27,4 @@ export const Heading = styled.default.h2`
export const WhiteText = styled.default(P)`
color: ${Theme.colors.white};
`;
`;

View File

@@ -0,0 +1,202 @@
/**
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
*/
import React, {useState, useEffect, useRef, KeyboardEvent, ChangeEvent, ReactElement} from 'react';
import {Tag, ITag} from 'components/tags';
import {isEqual} from 'components/shared/utils';
import {Label} from 'components/forms/label';
import {AdvancedSelectContainer, ComponentWrapper, InputContainer, SelectInput, TagOptions} from './style';
export interface IAdvancedSelectItem {
value: string;
type: string[];
dependency?: string[];
set?: string[];
}
interface IKeyboardEventWithTarget extends KeyboardEvent<HTMLInputElement> {
target: HTMLInputElement;
}
const advancedSelectHeight = 250;
interface IAdvancedSelect {
selectedItems: IAdvancedSelectItem[];
setSelectedItems: (items: IAdvancedSelectItem[]) => void;
data: IAdvancedSelectItem[];
label?: string;
labelColor?: string;
labelIcon?: React.JSX.Element;
labelClassName?: string;
labelHtmlFor?: string;
width?: number | string;
className?: string;
allowMultiple?: boolean;
}
export const AdvancedSelect = ({
allowMultiple = true,
selectedItems,
setSelectedItems,
data,
label,
labelColor,
labelIcon,
labelClassName,
labelHtmlFor,
width,
className
}: IAdvancedSelect): React.JSX.Element => {
const [inputValue, setInputValue] = useState<string>('');
const [selectedSets, setSelectedSets] = useState<string[]>([]);
const firstUpdate = useRef(true);
const ref = useRef(null);
const scrollToBottom = (): void => {
ref.current.scrollTop = ref.current.scrollHeight;
};
const getSelectedItemsSets = () => {
const selectedSets = [];
selectedItems.forEach((item: IAdvancedSelectItem) => {
if (item.set) {
item.set.forEach(set => {
selectedSets.push(set);
});
}
});
setSelectedSets(selectedSets);
};
useEffect(() => {
getSelectedItemsSets();
}, []);
useEffect(() => {
if (firstUpdate.current) {
firstUpdate.current = false;
return;
}
const timeout = setTimeout(() => scrollToBottom(), 0);
getSelectedItemsSets();
return () => clearTimeout(timeout);
}, [selectedItems]);
const getTagsContainerHeight = () => {
return advancedSelectHeight - 32 - ref?.current?.clientHeight;
};
const getItemDependencies = (item: IAdvancedSelectItem): IAdvancedSelectItem[] => {
const allDependencies = [];
const getNestedDependencies = (dependencyItem: IAdvancedSelectItem): void => {
const itemAlreadySelected = selectedItems.some(selectedItem => selectedItem.value === dependencyItem.value);
const selectedItemsSet = new Set(selectedItems.map(item => item.value));
const dependenciesSet = new Set(allDependencies.map(item => item.value));
const oneOfTheDependenciesAlreadySelected = item.dependency?.some(dependency => selectedItemsSet.has(dependency) || dependenciesSet.has(dependency));
const itemIsANestedDependency = item.dependency?.includes(dependencyItem.value);
if ((itemIsANestedDependency && oneOfTheDependenciesAlreadySelected) || itemAlreadySelected) {
return;
}
allDependencies.push(dependencyItem);
if (!dependencyItem.dependency) {
return;
}
dependencyItem.dependency.forEach((dependencyName: string) => {
const originalDependencyItem = data.find((element: IAdvancedSelectItem) => isEqual(dependencyName, element.value));
getNestedDependencies(originalDependencyItem);
});
};
getNestedDependencies(item);
return allDependencies;
};
const selectItem = (item: IAdvancedSelectItem): void => {
const itemsToSelect = getItemDependencies(item);
setSelectedItems([...selectedItems, ...itemsToSelect]);
setInputValue('');
};
const filterTags = (datum: IAdvancedSelectItem) => {
return datum.value.toLowerCase().includes(inputValue.toLowerCase());
};
const generateAllItems = (): ReactElement<ITag>[] => {
const itemsToSelectFrom = data.filter((datum: IAdvancedSelectItem) => !selectedItems.includes(datum)).filter(filterTags);
return itemsToSelectFrom.map((datum: IAdvancedSelectItem) => (
<Tag
disabled={(!allowMultiple && !!selectedItems.length) || (datum.set && selectedSets.some(set => datum.set.includes(set)))}
key={datum.value}
label={datum.value}
handleClick={() => selectItem(datum)}
/>
));
};
const deselectItem = (item: IAdvancedSelectItem): void => {
const filteredItems = selectedItems.filter((datum: IAdvancedSelectItem) => {
if (datum.dependency) {
return !isEqual(datum, item) && !datum.dependency.includes(item.value);
}
return !isEqual(datum, item);
});
setSelectedItems(filteredItems);
};
const generateSelectedItems = (): ReactElement<ITag>[] => {
return selectedItems.map((item: IAdvancedSelectItem) => (
<Tag key={item.value} label={item.value} onClose={() => deselectItem(item)} className="testId-selectedItem" />
));
};
const handleInput = (event: ChangeEvent<HTMLInputElement>): void => {
setInputValue(event.target.value);
};
const onKeyDown = (event: IKeyboardEventWithTarget): void => {
if (event.key === 'Enter') {
const {value} = event.target;
selectItem({
value,
type: []
});
}
};
return (
<ComponentWrapper width={width}>
{!!label && <Label text={label} color={labelColor} icon={labelIcon} className={labelClassName} htmlFor={labelHtmlFor} />}
<AdvancedSelectContainer className={className}>
<InputContainer ref={ref}>
{generateSelectedItems()}
<SelectInput
disabled={!allowMultiple && !!selectedItems.length}
className="testId-advancedSelectInput"
onChange={handleInput}
onKeyDown={onKeyDown}
value={inputValue}
/>
</InputContainer>
<TagOptions height={getTagsContainerHeight() || 0}>{generateAllItems()}</TagOptions>
</AdvancedSelectContainer>
</ComponentWrapper>
);
};

Some files were not shown because too many files have changed in this diff Show More