maintenance
This commit is contained in:
16
src/App.tsx
16
src/App.tsx
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -6,4 +6,4 @@ import * as styled from 'styled-components';
|
||||
export const CopyButtonContainer = styled.default.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`;
|
||||
`;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -27,4 +27,4 @@ export const IconButtonContainer = styled.default.div`
|
||||
&:hover {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
`;
|
||||
`;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
);
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
`;
|
||||
`;
|
||||
|
||||
@@ -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;
|
||||
`;
|
||||
`;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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%;
|
||||
}
|
||||
`}
|
||||
`;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
import React, {useState, useEffect, ChangeEvent, useCallback} from 'react';
|
||||
import {faQuestionCircle} from '@fortawesome/free-solid-svg-icons';
|
||||
import {documentationLinks} from 'constants/links';
|
||||
|
||||
import {NewTabLink} from 'components/new-tab-link';
|
||||
import Input from 'components/forms/Input';
|
||||
import {CopyIconButton} from 'components/buttons/copy-icon-button';
|
||||
import {DialogForm, Error} from 'components/modal/modal-form-response/style';
|
||||
|
||||
const DeleteChannelForm = ({setIsValid, alias}: {setIsValid: (isValid: boolean) => void; alias: string}): React.JSX.Element => {
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [enteredAlias, setEnteredAlias] = useState('');
|
||||
|
||||
const validate = useCallback((): boolean => {
|
||||
if (!enteredAlias) {
|
||||
setError('Please enter a channel alias');
|
||||
setIsValid(false);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (enteredAlias !== alias) {
|
||||
setError('Entered alias does not match the channels alias');
|
||||
setIsValid(false);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
setError(null);
|
||||
setIsValid(true);
|
||||
|
||||
return true;
|
||||
}, [enteredAlias, alias, setIsValid]);
|
||||
|
||||
useEffect(() => {
|
||||
if (enteredAlias.length) {
|
||||
validate();
|
||||
}
|
||||
}, [enteredAlias, validate]);
|
||||
|
||||
const handleEnteredAlias = (event: ChangeEvent<HTMLInputElement>): void => {
|
||||
if (error) {
|
||||
setError(null);
|
||||
}
|
||||
|
||||
setEnteredAlias(event.target.value);
|
||||
};
|
||||
|
||||
return (
|
||||
<DialogForm>
|
||||
<h3 className="testId-deleteChannelForm">
|
||||
Delete Channel <NewTabLink link={documentationLinks.deleteChannel} icon={faQuestionCircle} iconColor="black" />
|
||||
</h3>
|
||||
<p>To delete channel, please enter the channel alias:</p>
|
||||
<strong>
|
||||
<CopyIconButton text={alias} quoted />
|
||||
</strong>
|
||||
<Input name="alias" error={!!error} onChange={handleEnteredAlias} value={enteredAlias} />
|
||||
{error && <Error className="error-text testId-displayMessage">{error}</Error>}
|
||||
</DialogForm>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeleteChannelForm;
|
||||
@@ -0,0 +1,111 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
import React, {useState} from 'react';
|
||||
import {useDispatch} from 'react-redux';
|
||||
|
||||
import LoggerFactory from 'services/logger/LoggerFactory';
|
||||
import {deleteChannel} from 'services/channel.service';
|
||||
import {listChannels} from 'store/action/channels';
|
||||
|
||||
import {transformToPortalError} from 'utility/error-handler';
|
||||
import {deleteChannelErrorMessages} from 'constants/error-messages';
|
||||
|
||||
import {MultiStepModal} from 'components/modal/multi-step-modal';
|
||||
|
||||
import DeleteChannelForm from './delete-channel-form';
|
||||
import {FormResponse} from 'components/modal/modal-form-response';
|
||||
|
||||
interface IDeleteChannelModal {
|
||||
isOpen: boolean;
|
||||
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
channelId: string;
|
||||
alias: string;
|
||||
redirect?: () => void;
|
||||
}
|
||||
|
||||
export const DeleteChannelModal = ({isOpen, setIsOpen, channelId, alias, redirect}: IDeleteChannelModal): React.JSX.Element => {
|
||||
const logger = LoggerFactory.getLogger('components/channel-icon-menu/delete-channel/DeleteChannelModal');
|
||||
const dispatch = useDispatch();
|
||||
const [isFormValid, setIsFormValid] = useState(false);
|
||||
const [isFetching, setIsFetching] = useState(false);
|
||||
const [deleteChannelResponse, setDeleteChannelResponse] = useState<{error: string | boolean | Error; status: string | number; data?: any} | null>(null);
|
||||
const handleSubmit = async () => {
|
||||
if (!isFormValid) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsFetching(true);
|
||||
|
||||
logger.info('Deleting a channel [%s][%s]', alias, channelId);
|
||||
|
||||
const response = await deleteChannel({channelId, alias});
|
||||
|
||||
logger.info('Channel [%s][%s] was successfully deleted', alias, channelId);
|
||||
|
||||
setDeleteChannelResponse({status: 'ok', error: false, data: response});
|
||||
setIsFetching(false);
|
||||
} catch (e) {
|
||||
const {status, message, requestPayload, statusCode} = transformToPortalError(e);
|
||||
|
||||
setIsFetching(false);
|
||||
|
||||
const errorMessage = (deleteChannelErrorMessages as Record<string, string>)[status] || message || deleteChannelErrorMessages['default'];
|
||||
setDeleteChannelResponse({
|
||||
status: statusCode || 'error',
|
||||
error: errorMessage
|
||||
});
|
||||
|
||||
logger.error(`${errorMessage} [%s]`, status, requestPayload);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCloseModal = (): void => {
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
const onDeleteSuccess = async (): Promise<void> => {
|
||||
setIsOpen(false);
|
||||
|
||||
logger.info('Updating the list of channels after the [%s][%s] channel was deleted', alias, channelId);
|
||||
|
||||
await dispatch(listChannels() as any);
|
||||
|
||||
logger.info('The list of channels was updated successfully');
|
||||
|
||||
if (redirect) {
|
||||
redirect();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<MultiStepModal
|
||||
isOpen={isOpen}
|
||||
closeModal={deleteChannelResponse?.status === 'ok' ? onDeleteSuccess : handleCloseModal}
|
||||
steps={[
|
||||
{
|
||||
title: 'Delete Channel',
|
||||
component: <DeleteChannelForm alias={alias} setIsValid={setIsFormValid} />,
|
||||
saveButton: {
|
||||
text: 'Delete Channel',
|
||||
className: 'testId-deleteChannel',
|
||||
disabled: !isFormValid || isFetching,
|
||||
onClick: handleSubmit
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'Delete Channel',
|
||||
component: <FormResponse response={deleteChannelResponse || {error: '', status: '', data: null}} />,
|
||||
cancelDisabled: true,
|
||||
saveButton: {
|
||||
className: 'testId-doneButton',
|
||||
onClick: onDeleteSuccess
|
||||
}
|
||||
}
|
||||
]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeleteChannelModal;
|
||||
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
export {default} from './delete-channel-modal';
|
||||
@@ -0,0 +1,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;
|
||||
@@ -0,0 +1,113 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
import React, {useState} from 'react';
|
||||
import {useSelector} from 'react-redux';
|
||||
|
||||
import LoggerFactory from 'services/logger/LoggerFactory';
|
||||
import {forkChannel} from 'services/channel.service';
|
||||
import {AppStore} from 'store';
|
||||
import {channelsSelector} from 'store/action/channels';
|
||||
|
||||
import {transformToPortalError} from 'utility/error-handler';
|
||||
import {forkChannelErrorMessages} from 'constants/error-messages';
|
||||
|
||||
import {MultiStepModal} from 'components/modal/multi-step-modal';
|
||||
|
||||
import ForkChannelForm from './fork-channel-form';
|
||||
import {FormResponse} from 'components/modal/modal-form-response';
|
||||
|
||||
interface IForkChannelModal {
|
||||
isOpen: boolean;
|
||||
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
channelId: string;
|
||||
alias: string;
|
||||
}
|
||||
|
||||
export const ForkChannelModal = ({isOpen, setIsOpen, channelId, alias}: IForkChannelModal): React.JSX.Element => {
|
||||
const logger = LoggerFactory.getLogger('components/channel-icon-menu/fork-channel/ForkChannelModal');
|
||||
const channelsState = useSelector((state: AppStore) => channelsSelector(state));
|
||||
const channelList = channelsState.channels || [];
|
||||
const [formData, setFormData] = useState({
|
||||
streamCapabilities: [] as string[],
|
||||
streamTags: [] as string[],
|
||||
options: [] as string[],
|
||||
destinationChannelId: ''
|
||||
});
|
||||
const [isFormValid, setIsFormValid] = useState(false);
|
||||
const [isFetching, setIsFetching] = useState(false);
|
||||
const [forkResponse, setForkResponse] = useState<{error: string | boolean | Error; status: string | number; data?: any} | null>(null);
|
||||
const handleSubmit = async () => {
|
||||
if (!isFormValid) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsFetching(true);
|
||||
|
||||
const {streamCapabilities, streamTags, options, destinationChannelId} = formData;
|
||||
|
||||
logger.info('Forking a channel [%s] to [%s]', channelId, destinationChannelId);
|
||||
|
||||
const response = await forkChannel({
|
||||
sourceChannelId: channelId,
|
||||
destinationChannelId,
|
||||
streamCapabilities,
|
||||
streamTags,
|
||||
options
|
||||
});
|
||||
|
||||
logger.info('The channel [%s] was successfully forked to [%s]', channelId, destinationChannelId);
|
||||
|
||||
setForkResponse({
|
||||
status: 'ok',
|
||||
error: false,
|
||||
data: response
|
||||
});
|
||||
setIsFetching(false);
|
||||
} catch (e) {
|
||||
const {status, message, requestPayload, statusCode} = transformToPortalError(e);
|
||||
|
||||
setIsFetching(false);
|
||||
|
||||
const errorMessage = (forkChannelErrorMessages as Record<string, string>)[status] || message || forkChannelErrorMessages['default'];
|
||||
setForkResponse({
|
||||
status: statusCode || 'error',
|
||||
error: errorMessage
|
||||
});
|
||||
|
||||
logger.error(`${errorMessage} [%s]`, status, requestPayload);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCloseModal = (): void => {
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<MultiStepModal
|
||||
isOpen={isOpen}
|
||||
closeModal={handleCloseModal}
|
||||
steps={[
|
||||
{
|
||||
title: 'Fork Channel',
|
||||
component: <ForkChannelForm alias={alias} channelList={channelList} setIsValid={setIsFormValid} setFormData={setFormData} />,
|
||||
saveButton: {
|
||||
text: 'Fork Channel',
|
||||
className: 'testId-forkChannel',
|
||||
disabled: !isFormValid || isFetching,
|
||||
onClick: handleSubmit
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'Fork Channel',
|
||||
component: <FormResponse response={forkResponse || {error: '', status: '', data: null}} />,
|
||||
cancelDisabled: true,
|
||||
saveButton: {className: 'testId-doneButton'}
|
||||
}
|
||||
]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default ForkChannelModal;
|
||||
4
src/components/channel-icon-menu/fork-channel/index.tsx
Normal file
4
src/components/channel-icon-menu/fork-channel/index.tsx
Normal file
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
export {default} from './fork-channel-modal';
|
||||
65
src/components/channel-icon-menu/index.tsx
Normal file
65
src/components/channel-icon-menu/index.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
import React, {useState} from 'react';
|
||||
import {IconDefinition} from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
import {IconMenu} from 'components/icon-menu';
|
||||
import {IconMenuPosition} from 'components/icon-menu/types';
|
||||
|
||||
import ForkChannelModal from './fork-channel';
|
||||
import DeleteChannelModal from './delete-channel';
|
||||
import KillChannelModal from './kill-channel';
|
||||
|
||||
interface IChannelIconMenu {
|
||||
data: {
|
||||
name: string;
|
||||
channelId: string;
|
||||
applicationId: string;
|
||||
alias: string;
|
||||
};
|
||||
redirect?: () => void;
|
||||
showTail?: boolean;
|
||||
position?: IconMenuPosition;
|
||||
icon?: IconDefinition;
|
||||
margin?: number;
|
||||
}
|
||||
|
||||
export const ChannelIconMenu = (props: IChannelIconMenu): React.JSX.Element => {
|
||||
const {data, icon, redirect, showTail, position, margin} = props;
|
||||
const {alias, channelId} = data;
|
||||
const [isForkModalOpen, setIsForkModalOpen] = useState(false);
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
const [isKillModalOpen, setIsKillModalOpen] = useState(false);
|
||||
const items = [
|
||||
{
|
||||
value: 'delete',
|
||||
title: 'Delete',
|
||||
className: 'testId-deleteMenuItem',
|
||||
onClick: () => setIsDeleteModalOpen(true)
|
||||
},
|
||||
{
|
||||
value: 'kill',
|
||||
title: 'Kill',
|
||||
className: 'testId-killMenuItem',
|
||||
onClick: () => setIsKillModalOpen(true)
|
||||
},
|
||||
{
|
||||
value: 'fork',
|
||||
title: 'Fork',
|
||||
className: 'testId-forkMenuItem',
|
||||
onClick: () => setIsForkModalOpen(true)
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<IconMenu icon={icon} items={items} showTail={showTail} position={position} margin={margin} />
|
||||
{isForkModalOpen && <ForkChannelModal isOpen={isForkModalOpen} setIsOpen={setIsForkModalOpen} channelId={channelId} alias={alias} />}
|
||||
{isDeleteModalOpen && (
|
||||
<DeleteChannelModal isOpen={isDeleteModalOpen} setIsOpen={setIsDeleteModalOpen} channelId={channelId} alias={alias} redirect={redirect} />
|
||||
)}
|
||||
{isKillModalOpen && <KillChannelModal isOpen={isKillModalOpen} setIsOpen={setIsKillModalOpen} channelId={channelId} alias={alias} />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
4
src/components/channel-icon-menu/kill-channel/index.tsx
Normal file
4
src/components/channel-icon-menu/kill-channel/index.tsx
Normal file
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
export {default} from './kill-channel-modal';
|
||||
@@ -0,0 +1,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;
|
||||
@@ -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;
|
||||
62
src/components/create-token-components/capabilities.tsx
Normal file
62
src/components/create-token-components/capabilities.tsx
Normal 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;
|
||||
8
src/components/create-token-components/index.ts
Normal file
8
src/components/create-token-components/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
export {default as Capabilities} from './capabilities';
|
||||
export {default as ValidityTimeComponent} from './validity-time';
|
||||
export * from './validity-time';
|
||||
export {default as LabelIconTooltip} from './label-icon-tooltip';
|
||||
export * from './styles';
|
||||
44
src/components/create-token-components/index.tsx
Normal file
44
src/components/create-token-components/index.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
60
src/components/create-token-components/styles.ts
Normal file
60
src/components/create-token-components/styles.ts
Normal 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;
|
||||
`;
|
||||
60
src/components/create-token-components/validity-time.tsx
Normal file
60
src/components/create-token-components/validity-time.tsx
Normal 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;
|
||||
51
src/components/date-render-component/index.tsx
Normal file
51
src/components/date-render-component/index.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
212
src/components/drop-down/index.tsx
Normal file
212
src/components/drop-down/index.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
59
src/components/drop-down/style.ts
Normal file
59
src/components/drop-down/style.ts
Normal 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'};
|
||||
}
|
||||
`}
|
||||
`;
|
||||
12
src/components/error-renderer/index.tsx
Normal file
12
src/components/error-renderer/index.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
import {toast} from 'react-toastify';
|
||||
|
||||
export const toastErrorRenderer = (error: string): void => {
|
||||
toast.error(String(error), {
|
||||
autoClose: 10000,
|
||||
draggable: false,
|
||||
hideProgressBar: true
|
||||
});
|
||||
};
|
||||
23
src/components/error-renderer/style.ts
Normal file
23
src/components/error-renderer/style.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
import * as styled from 'styled-components';
|
||||
import Theme from 'theme';
|
||||
|
||||
const {headerAllowance, dangerColor, typography} = Theme;
|
||||
|
||||
export const Error = styled.default.div`
|
||||
height: ${window.innerHeight - headerAllowance}px;
|
||||
width: 100%;
|
||||
display: grid;
|
||||
align-content: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
color: ${dangerColor};
|
||||
font-size: ${typography.fontSizeL};
|
||||
`;
|
||||
|
||||
export const ErrorSubMessage = styled.default.div`
|
||||
font-size: ${typography.fontSizeS};
|
||||
color: ${dangerColor};
|
||||
`;
|
||||
83
src/components/forms/Checkbox.tsx
Normal file
83
src/components/forms/Checkbox.tsx
Normal 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;
|
||||
88
src/components/forms/Checkbox/index.tsx
Normal file
88
src/components/forms/Checkbox/index.tsx
Normal 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;
|
||||
@@ -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;
|
||||
|
||||
77
src/components/forms/SearchInput.tsx
Normal file
77
src/components/forms/SearchInput.tsx
Normal 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;
|
||||
1
src/components/forms/label/index.ts
Normal file
1
src/components/forms/label/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './label';
|
||||
@@ -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>
|
||||
);
|
||||
);
|
||||
@@ -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;
|
||||
`;
|
||||
`;
|
||||
|
||||
198
src/components/icon-menu/icon-menu.tsx
Normal file
198
src/components/icon-menu/icon-menu.tsx
Normal file
@@ -0,0 +1,198 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
import React, {useEffect, useRef, useState, ReactNode} from 'react';
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
|
||||
import {IconDefinition, IconProp} from '@fortawesome/fontawesome-svg-core';
|
||||
|
||||
import {useComponentVisible, useCurrentWidth} from 'utility/custom-hooks';
|
||||
import {IconMenuWrapper, IconMenuDropDownWrapper, IconMenuContent, IconMenuPointer, DropdownIcon, IconMenuDropDownItem} from './style';
|
||||
import {IconMenuPosition, defaultArrowWidth, defaultArrowEdgeGap} from './types';
|
||||
|
||||
interface IIconMenuItems {
|
||||
title: string;
|
||||
value: string;
|
||||
className?: string;
|
||||
onSelect?: () => void;
|
||||
onClick?: () => void;
|
||||
modal?: {
|
||||
component: ReactNode;
|
||||
action?: () => void;
|
||||
};
|
||||
}
|
||||
|
||||
interface IMenuPointerStyle {
|
||||
left?: number | string;
|
||||
right?: number | string;
|
||||
top?: number | string;
|
||||
bottom?: number | string;
|
||||
width?: number | string;
|
||||
height?: number | string;
|
||||
}
|
||||
|
||||
interface IIconMenu {
|
||||
items: IIconMenuItems[];
|
||||
icon?: IconDefinition | IconProp;
|
||||
iconClassName?: string;
|
||||
iconColor?: string;
|
||||
title?: string;
|
||||
menuVisible?: boolean;
|
||||
showTail?: boolean;
|
||||
position?: IconMenuPosition;
|
||||
hasPointer?: boolean;
|
||||
dropdownMargin?: number;
|
||||
margin?: number | string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const IconMenu = ({
|
||||
items = [],
|
||||
icon,
|
||||
iconClassName,
|
||||
iconColor = 'white',
|
||||
menuVisible = false,
|
||||
title,
|
||||
showTail = true,
|
||||
position = IconMenuPosition.BottomLeft,
|
||||
hasPointer = true,
|
||||
dropdownMargin = 0,
|
||||
margin,
|
||||
disabled
|
||||
}: IIconMenu): React.JSX.Element => {
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
const [dropdownStyle, setDropdownStyle] = useState<any>(null);
|
||||
const [menuPointerStyle, setMenuPointerStyle] = useState<IMenuPointerStyle>({
|
||||
right: -defaultArrowWidth / 2,
|
||||
top: '50%'
|
||||
});
|
||||
const {ref, isComponentVisible, setIsComponentVisible} = useComponentVisible(menuVisible);
|
||||
const width = useCurrentWidth();
|
||||
|
||||
useEffect(() => {
|
||||
if (!isComponentVisible) {
|
||||
setDropdownStyle(null);
|
||||
}
|
||||
}, [isComponentVisible]);
|
||||
|
||||
useEffect(() => {
|
||||
if (ref.current && dropdownRef.current && isComponentVisible && position) {
|
||||
const refRect = (ref.current as HTMLElement).getBoundingClientRect();
|
||||
const dropdownRefRect = (dropdownRef.current as HTMLElement).getBoundingClientRect();
|
||||
const newArrowPosition: IMenuPointerStyle = {
|
||||
width: `${defaultArrowWidth}px`,
|
||||
height: `${defaultArrowWidth}px`
|
||||
};
|
||||
const pointerMargin = Math.sqrt((defaultArrowWidth * defaultArrowWidth) / 2);
|
||||
let top = 0;
|
||||
let left = 0;
|
||||
|
||||
switch (position) {
|
||||
case IconMenuPosition.Right: {
|
||||
newArrowPosition.top = `calc(50% - ${defaultArrowWidth / 2}px)`;
|
||||
newArrowPosition.left = `-${defaultArrowWidth / 2}px`;
|
||||
top = refRect.top + refRect.height / 2 - dropdownRefRect.height / 2;
|
||||
left = refRect.left + refRect.width + pointerMargin + dropdownMargin;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case IconMenuPosition.Top: {
|
||||
newArrowPosition.left = '50%';
|
||||
newArrowPosition.bottom = `-${defaultArrowWidth / 2}px`;
|
||||
top = refRect.top - dropdownRefRect.height - pointerMargin - dropdownMargin;
|
||||
left = refRect.left + refRect.width / 2 - dropdownRefRect.width / 2;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case IconMenuPosition.TopLeft: {
|
||||
newArrowPosition.right = `${defaultArrowEdgeGap}px`;
|
||||
newArrowPosition.bottom = `-${defaultArrowWidth / 2}px`;
|
||||
top = refRect.top - dropdownRefRect.height - pointerMargin - dropdownMargin;
|
||||
left = refRect.left + refRect.width / 2 - dropdownRefRect.width + defaultArrowWidth / 2 + defaultArrowEdgeGap;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case IconMenuPosition.TopRight: {
|
||||
newArrowPosition.left = `${defaultArrowEdgeGap}px`;
|
||||
newArrowPosition.bottom = `-${defaultArrowWidth / 2}px`;
|
||||
top = refRect.top - dropdownRefRect.height - pointerMargin - dropdownMargin;
|
||||
left = refRect.left + refRect.width / 2 - defaultArrowWidth / 2 - defaultArrowEdgeGap;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case IconMenuPosition.Bottom: {
|
||||
newArrowPosition.left = '50%';
|
||||
newArrowPosition.top = `-${defaultArrowWidth / 2}px`;
|
||||
top = refRect.top + refRect.height + pointerMargin + dropdownMargin;
|
||||
left = refRect.left + refRect.width / 2 - dropdownRefRect.width / 2;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case IconMenuPosition.BottomLeft: {
|
||||
newArrowPosition.right = `${defaultArrowEdgeGap}px`;
|
||||
newArrowPosition.top = `-${defaultArrowWidth / 2}px`;
|
||||
top = refRect.top + refRect.height + pointerMargin + dropdownMargin;
|
||||
left = refRect.left + refRect.width / 2 - dropdownRefRect.width + defaultArrowWidth / 2 + defaultArrowEdgeGap;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case IconMenuPosition.BottomRight: {
|
||||
newArrowPosition.left = `${defaultArrowEdgeGap}px`;
|
||||
newArrowPosition.top = `-${defaultArrowWidth / 2}px`;
|
||||
top = refRect.top + refRect.height + pointerMargin + dropdownMargin;
|
||||
left = refRect.left + refRect.width / 2 - defaultArrowWidth / 2 - defaultArrowEdgeGap;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case IconMenuPosition.Left:
|
||||
default: {
|
||||
newArrowPosition.top = `calc(50% - ${defaultArrowWidth / 2}px)`;
|
||||
newArrowPosition.right = `-${defaultArrowWidth / 2}px`;
|
||||
top = refRect.top + refRect.height / 2 - dropdownRefRect.height / 2;
|
||||
left = refRect.left - dropdownRefRect.width - pointerMargin - dropdownMargin;
|
||||
}
|
||||
}
|
||||
|
||||
setDropdownStyle({
|
||||
top,
|
||||
left
|
||||
});
|
||||
setMenuPointerStyle(newArrowPosition);
|
||||
}
|
||||
}, [isComponentVisible, ref, position, dropdownMargin, width]);
|
||||
|
||||
const handleClickIcon = () => {
|
||||
setIsComponentVisible(!isComponentVisible);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<IconMenuWrapper ref={ref} margin={margin}>
|
||||
<DropdownIcon role="button" tabIndex={0} onClick={handleClickIcon} className={`testId-dropdownIcon ${iconClassName} ${disabled && 'disabled'}`}>
|
||||
{title}
|
||||
{icon && <FontAwesomeIcon icon={icon} color={iconColor} size="lg" />}
|
||||
</DropdownIcon>
|
||||
{isComponentVisible && (
|
||||
<IconMenuDropDownWrapper ref={dropdownRef} visible={isComponentVisible && !!dropdownStyle} showTail={showTail} style={dropdownStyle || undefined}>
|
||||
<IconMenuContent className="testId-dropdownItems">
|
||||
{!!hasPointer && <IconMenuPointer style={menuPointerStyle} />}
|
||||
{items.map((item, i) => (
|
||||
<IconMenuDropDownItem key={`${item.value}-${i}`} className={item.className} onMouseUp={item.onClick} data-value={item.value}>
|
||||
{item.title}
|
||||
</IconMenuDropDownItem>
|
||||
))}
|
||||
</IconMenuContent>
|
||||
</IconMenuDropDownWrapper>
|
||||
)}
|
||||
</IconMenuWrapper>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default IconMenu;
|
||||
4
src/components/icon-menu/index.tsx
Normal file
4
src/components/icon-menu/index.tsx
Normal file
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
export {default as IconMenu} from './icon-menu';
|
||||
79
src/components/icon-menu/style.ts
Normal file
79
src/components/icon-menu/style.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
import * as styled from 'styled-components';
|
||||
import Theme from 'theme';
|
||||
import caretUp from 'assets/images/caret-up.svg';
|
||||
|
||||
const {colors, spacing} = Theme;
|
||||
|
||||
export const IconMenuWrapper = styled.default.div<{margin?: number | string}>`
|
||||
margin: ${({margin}) => (margin !== undefined ? margin : '.5rem 0 .5rem 1rem')};
|
||||
div {
|
||||
outline: none;
|
||||
}
|
||||
& img{
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
cursor: pointer;
|
||||
}
|
||||
`;
|
||||
export const IconMenuDropDownWrapper = styled.default.div<{showTail?: boolean; visible?: boolean}>`
|
||||
position: fixed;
|
||||
width: 200px;
|
||||
min-height: 31px;
|
||||
right: 2.5rem;
|
||||
${({showTail}) =>
|
||||
showTail &&
|
||||
styled.css`
|
||||
background-image: url(${caretUp});
|
||||
`};
|
||||
background-position: top 0 right .5rem;
|
||||
background-repeat: no-repeat;
|
||||
z-index: 9999;
|
||||
visibility: hidden;
|
||||
${({visible}) =>
|
||||
visible &&
|
||||
styled.css`
|
||||
visibility: visible;
|
||||
`}
|
||||
`;
|
||||
export const IconMenuContent = styled.default.div`
|
||||
background-color: ${colors.gray100};
|
||||
border: 1px solid ${colors.gray200};
|
||||
width: 200px;
|
||||
overflow: auto;
|
||||
border-radius: ${spacing.xxSmall};
|
||||
padding: ${spacing.small} 0;
|
||||
z-index: 10;
|
||||
`;
|
||||
|
||||
export const IconMenuDropDownItem = styled.default.div`
|
||||
padding: ${spacing.small};
|
||||
text-decoration: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
color: ${colors.black};
|
||||
background-color: ${colors.white};
|
||||
&:hover {
|
||||
background-color: ${colors.gray400};
|
||||
}
|
||||
`;
|
||||
export const DropdownIcon = styled.default.div<{disabled?: boolean}>`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
min-width: 1rem;
|
||||
|
||||
&.disabled {
|
||||
pointer-events: none;
|
||||
opacity: 0.5;
|
||||
}
|
||||
`;
|
||||
export const IconMenuPointer = styled.default.div`
|
||||
position: absolute;
|
||||
background-color: ${colors.gray100};
|
||||
transform: rotate(45deg);
|
||||
`;
|
||||
19
src/components/icon-menu/types.ts
Normal file
19
src/components/icon-menu/types.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
|
||||
export enum IconMenuPosition {
|
||||
Left = 'left',
|
||||
Right = 'right',
|
||||
Top = 'top',
|
||||
TopLeft = 'top-left',
|
||||
TopRight = 'top-right',
|
||||
Bottom = 'bottom',
|
||||
BottomLeft = 'bottom-left',
|
||||
BottomRight = 'bottom-right'
|
||||
}
|
||||
|
||||
const defaultArrowWidth = 12;
|
||||
const defaultArrowEdgeGap = 8;
|
||||
|
||||
export {defaultArrowWidth, defaultArrowEdgeGap};
|
||||
@@ -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';
|
||||
|
||||
4
src/components/indicator-component/indicators/index.ts
Normal file
4
src/components/indicator-component/indicators/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
export * from './indicators';
|
||||
11
src/components/indicator-component/indicators/indicators.tsx
Normal file
11
src/components/indicator-component/indicators/indicators.tsx
Normal 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>;
|
||||
43
src/components/indicator-component/indicators/style.ts
Normal file
43
src/components/indicator-component/indicators/style.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
import * as styled from 'styled-components';
|
||||
|
||||
import {LoadingWheel as Loader} from 'components';
|
||||
|
||||
export const Indicator = styled.default.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
${Loader} {
|
||||
flex: 0;
|
||||
margin: 0;
|
||||
|
||||
& span {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const IndicatorOutline = styled.default.div`
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
`;
|
||||
|
||||
export const OfflineSymbol = styled.default(IndicatorOutline)`
|
||||
border: 3px solid #707070;
|
||||
background-color: transparent;
|
||||
`;
|
||||
|
||||
export const SingleStreamSymbol = styled.default(IndicatorOutline)`
|
||||
background-color: #08BD0B;
|
||||
`;
|
||||
|
||||
export const MultipleStreamSymbol = styled.default(SingleStreamSymbol)`
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
margin-left: -2px;
|
||||
box-shadow: 9px 0 0 0 #08BD0B;
|
||||
`;
|
||||
@@ -0,0 +1,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;
|
||||
1
src/components/label/index.ts
Normal file
1
src/components/label/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from '../forms/label';
|
||||
57
src/components/layout/index.tsx
Normal file
57
src/components/layout/index.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
import * as styled from 'styled-components';
|
||||
import Theme from 'theme';
|
||||
|
||||
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;
|
||||
`;
|
||||
@@ -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;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
export * from './LoadingWheel';
|
||||
export * from './LoadingWheel';
|
||||
|
||||
5
src/components/modal/index.ts
Normal file
5
src/components/modal/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
|
||||
export {default as Modal} from './modal';
|
||||
40
src/components/modal/modal-form-response/form-response.tsx
Normal file
40
src/components/modal/modal-form-response/form-response.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
import {LoadingWheel as Loader} from 'components';
|
||||
import PreFormattedCode from 'components/pre-formatted-code';
|
||||
import okImage from 'assets/images/icon/ok.svg';
|
||||
import errorImage from 'assets/images/icon/error.svg';
|
||||
import React from 'react';
|
||||
|
||||
import {DialogResponse, StatusCode, FormLoaderContainer} from './style';
|
||||
|
||||
interface IFormResponseComponent {
|
||||
response: {
|
||||
error: string | boolean | Error;
|
||||
status: number | string;
|
||||
data?: any; //eslint-disable-line
|
||||
};
|
||||
}
|
||||
|
||||
const FormResponse = (props: IFormResponseComponent): React.JSX.Element => {
|
||||
const {response} = props;
|
||||
|
||||
return !response ? (
|
||||
<FormLoaderContainer>
|
||||
<Loader color="dark" size="medium" />
|
||||
</FormLoaderContainer>
|
||||
) : (
|
||||
<DialogResponse error={response.error || ''}>
|
||||
<h3 className="testId-responseModal">{response.error ? 'Error' : 'Success'}</h3>
|
||||
<div className="response-icon">
|
||||
<img src={response.error ? errorImage : okImage} alt="status icon" />
|
||||
</div>
|
||||
<StatusCode error={response.error}>Status: {response.status}</StatusCode>
|
||||
{response.data && typeof response.data === 'object' && <PreFormattedCode data={response.data} />}
|
||||
{response.error && typeof response.error === 'string' && <PreFormattedCode data={{error: response.error}} />}
|
||||
</DialogResponse>
|
||||
);
|
||||
};
|
||||
|
||||
export default FormResponse;
|
||||
4
src/components/modal/modal-form-response/index.tsx
Normal file
4
src/components/modal/modal-form-response/index.tsx
Normal file
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
export {default as FormResponse} from './form-response';
|
||||
175
src/components/modal/modal-form-response/style.ts
Normal file
175
src/components/modal/modal-form-response/style.ts
Normal file
@@ -0,0 +1,175 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
import * as styled from 'styled-components';
|
||||
import Theme from 'theme';
|
||||
import {InputComponentWrapper} from 'components/forms/Input';
|
||||
// import {AdvancedSelectContainer, ComponentWrapper} from 'components/ui/advanced-select/advanced-select';
|
||||
// import InputWithTagsWrapper from 'components/ui/input-with-tags/input-with-tags';
|
||||
// import TagContainer from 'components/ui/input-with-tags/input-with-tags';
|
||||
// import TagWrapper from 'components/ui/input-with-tags/input-with-tags';
|
||||
|
||||
const {spacing, blackWithOpacity, typography, colors, primaryBorderRadius, primaryBorderColor} = Theme;
|
||||
|
||||
export const DialogContent = styled.default.div`
|
||||
display: flex;
|
||||
width: 400px;
|
||||
margin: ${spacing.small};
|
||||
`;
|
||||
|
||||
export const DialogForm = styled.default.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
/* stylelint-disable */
|
||||
& ${InputComponentWrapper} {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
/* TagWrapper {
|
||||
& > div {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
} */
|
||||
/* stylelint-enable */
|
||||
h3 {
|
||||
color: ${colors.gray700};
|
||||
font-size: ${typography.fontSizeXl};
|
||||
line-height: 24px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.36px;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
a {
|
||||
font-size: ${typography.fontSizeS};
|
||||
margin: 0 0 0 0.5rem;
|
||||
}
|
||||
}
|
||||
> p {
|
||||
color: ${blackWithOpacity};
|
||||
font-size: ${typography.primaryFontSize};
|
||||
margin-top: ${spacing.small};
|
||||
line-height: 24px;
|
||||
i {
|
||||
font-weight: bold;
|
||||
padding: 0 ${spacing.xsmall};
|
||||
}
|
||||
a {
|
||||
color: ${colors.lightBlue};
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
strong {
|
||||
p {
|
||||
margin-top: 0;
|
||||
margin-bottom: ${spacing.xSmall};
|
||||
}
|
||||
svg {
|
||||
margin: 0 ${spacing.xxSmall};
|
||||
color: ${colors.lightBlue};
|
||||
}
|
||||
}
|
||||
select {
|
||||
background: ${colors.transparent};
|
||||
border: 1px solid ${primaryBorderColor} !important;
|
||||
}
|
||||
input {
|
||||
padding: ${spacing.small};
|
||||
margin-bottom: ${spacing.xSmall};
|
||||
}
|
||||
.button-group {
|
||||
display: flex;
|
||||
margin-top: ${spacing.small};
|
||||
justify-content: center;
|
||||
}
|
||||
.error-text {
|
||||
font-size: ${typography.fontSizeXS};
|
||||
color: ${colors.lightRed};
|
||||
}
|
||||
.loading {
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
background-color: rgba(0,0,0,0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
`;
|
||||
|
||||
export const Options = styled.default.div`
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
|
||||
&>div {
|
||||
margin: 0 1rem 0 0;
|
||||
}
|
||||
`;
|
||||
|
||||
export const DialogResponse = styled.default(DialogContent)<{error?: string | boolean | Error}>`
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
h3 {
|
||||
color: ${colors.green};
|
||||
font-size: ${typography.fontSizeXl};
|
||||
line-height: 24px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.36px;
|
||||
${({error}) =>
|
||||
error &&
|
||||
styled.css`
|
||||
color: ${colors.lightRed};
|
||||
`}
|
||||
}
|
||||
.response-icon{
|
||||
img {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
margin: ${spacing.small} 0;
|
||||
}
|
||||
}
|
||||
.json-renderer {
|
||||
margin-top: ${spacing.small};
|
||||
width: 100%;
|
||||
height: auto;
|
||||
height: 300px;
|
||||
padding: ${spacing.small};
|
||||
overflow: auto;
|
||||
background-color: ${colors.gray200};
|
||||
font-size: ${typography.fontSizeS};
|
||||
}
|
||||
`;
|
||||
|
||||
export const StatusCode = styled.default.div<{error?: string | boolean | Error}>`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
margin: ${spacing.xxSmall} 0;
|
||||
padding: ${spacing.xsmall} 0;
|
||||
color: ${colors.green};
|
||||
border-radius: ${primaryBorderRadius};
|
||||
border: 1px solid ${colors.green};
|
||||
${({error}) =>
|
||||
error &&
|
||||
styled.css`
|
||||
border-color: ${colors.lightRed};
|
||||
color: ${colors.lightRed};
|
||||
`}
|
||||
`;
|
||||
|
||||
export const Error = styled.default.div`
|
||||
font-size: ${typography.fontSizeXS};
|
||||
color: ${colors.lightRed};
|
||||
`;
|
||||
|
||||
export const FormLoaderContainer = styled.default.div`
|
||||
height: 430px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
`;
|
||||
87
src/components/modal/modal.tsx
Normal file
87
src/components/modal/modal.tsx
Normal 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;
|
||||
5
src/components/modal/multi-step-modal/index.ts
Normal file
5
src/components/modal/multi-step-modal/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
|
||||
export {default as MultiStepModal} from './multi-step-modal';
|
||||
66
src/components/modal/multi-step-modal/multi-step-modal.tsx
Normal file
66
src/components/modal/multi-step-modal/multi-step-modal.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
|
||||
import React, {useState} from 'react';
|
||||
|
||||
import {Modal} from 'components/modal';
|
||||
|
||||
interface IMultiStepModal {
|
||||
isOpen: boolean;
|
||||
closeModal: () => void;
|
||||
steps: {
|
||||
title: string;
|
||||
component: React.JSX.Element;
|
||||
saveButton?: {
|
||||
onClick?: () => void;
|
||||
text?: string;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
};
|
||||
cancel?: () => void;
|
||||
cancelDisabled?: boolean;
|
||||
}[];
|
||||
}
|
||||
|
||||
const MultiStepModal = ({isOpen, closeModal, steps}: IMultiStepModal): React.JSX.Element => {
|
||||
const [currentStep, setCurrentStep] = useState(0);
|
||||
const isLastStep = steps.length - 1 === currentStep;
|
||||
const saveButtonText = isLastStep ? 'Done' : 'Next';
|
||||
const handleCloseModal = () => {
|
||||
closeModal();
|
||||
setCurrentStep(0);
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
if (steps[currentStep]?.saveButton?.onClick) {
|
||||
steps[currentStep].saveButton.onClick();
|
||||
}
|
||||
|
||||
if (isLastStep) {
|
||||
handleCloseModal();
|
||||
} else {
|
||||
setCurrentStep(currentStep + 1);
|
||||
}
|
||||
};
|
||||
|
||||
if (isOpen) {
|
||||
return (
|
||||
<Modal
|
||||
close={handleCloseModal}
|
||||
submitButton={{
|
||||
className: steps[currentStep]?.saveButton?.className,
|
||||
disabled: steps[currentStep]?.saveButton?.disabled,
|
||||
onClick: handleSave,
|
||||
label: steps[currentStep]?.saveButton?.text || saveButtonText
|
||||
}}
|
||||
cancelButton={{disabled: steps[currentStep]?.cancelDisabled}}>
|
||||
{steps[currentStep]?.component || null}
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default MultiStepModal;
|
||||
80
src/components/modal/style.ts
Normal file
80
src/components/modal/style.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
import * as styled from 'styled-components';
|
||||
|
||||
import {theme, paddings} from 'components/shared/theme';
|
||||
import {ConfirmButton} from 'components/buttons';
|
||||
|
||||
const {colors} = theme;
|
||||
|
||||
export const ModalOverlay = styled.default.div`
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: ${colors.halfTransparentBlack};
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 9999;
|
||||
`;
|
||||
|
||||
export const ModalContainer = styled.default.div`
|
||||
width: 60%;
|
||||
max-width: 800px;
|
||||
border-radius: 4px;
|
||||
padding: ${paddings.xlarge};
|
||||
background-color: ${colors.white};
|
||||
position: relative;
|
||||
max-height: 90%;
|
||||
overflow: auto;
|
||||
|
||||
&.full-width {
|
||||
width: auto;
|
||||
max-width: 100%;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
&.dark {
|
||||
background-color: ${colors.gray1000}
|
||||
}
|
||||
`;
|
||||
|
||||
export const CloseButton = styled.default.span`
|
||||
cursor: pointer;
|
||||
height: 1rem;
|
||||
width: 1rem;
|
||||
color: ${colors.gray700};
|
||||
position: absolute;
|
||||
top: 0.5rem;
|
||||
right: 0.5rem;
|
||||
|
||||
&:before,
|
||||
&:after {
|
||||
position: absolute;
|
||||
left: 0.4rem;
|
||||
content: ' ';
|
||||
height: 1rem;
|
||||
width: 0.2rem;
|
||||
background-color: ${colors.gray700};
|
||||
}
|
||||
&:before {
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
&:after {
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
`;
|
||||
|
||||
export const ModalButtonsContaier = styled.default.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin: 24px 0 0;
|
||||
|
||||
${ConfirmButton} {
|
||||
margin: 0 0.5rem;
|
||||
}
|
||||
`;
|
||||
26
src/components/new-tab-link/index.tsx
Normal file
26
src/components/new-tab-link/index.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
import React from 'react';
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
|
||||
import {IconDefinition} from '@fortawesome/fontawesome-svg-core';
|
||||
|
||||
interface ILink {
|
||||
icon?: IconDefinition;
|
||||
text?: string;
|
||||
iconColor?: string;
|
||||
link: string;
|
||||
}
|
||||
|
||||
export const NewTabLink = (props: ILink & Partial<HTMLLinkElement>): React.JSX.Element => {
|
||||
const {icon, className, text, link, iconColor = 'white'} = props;
|
||||
|
||||
return (
|
||||
<a rel="noopener noreferrer" target="_blank" href={link} className={className}>
|
||||
{icon && <FontAwesomeIcon icon={icon} style={{color: iconColor}} />}
|
||||
{text}
|
||||
</a>
|
||||
);
|
||||
};
|
||||
|
||||
export default NewTabLink;
|
||||
5
src/components/pagination/index.ts
Normal file
5
src/components/pagination/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
export * from './pagination';
|
||||
export {default as Pagination} from './pagination';
|
||||
87
src/components/pagination/pagination.tsx
Normal file
87
src/components/pagination/pagination.tsx
Normal 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;
|
||||
45
src/components/pagination/style.tsx
Normal file
45
src/components/pagination/style.tsx
Normal 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};
|
||||
`;
|
||||
5
src/components/pre-formatted-code/index.tsx
Normal file
5
src/components/pre-formatted-code/index.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
|
||||
export {default} from './pre-formatted-code';
|
||||
58
src/components/pre-formatted-code/pre-formatted-code.tsx
Normal file
58
src/components/pre-formatted-code/pre-formatted-code.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
import React, {Fragment, useState} from 'react';
|
||||
import {faCopy, faCheck} from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
import IconButton from 'components/buttons/icon-button';
|
||||
|
||||
import {CodeContainer} from './styles';
|
||||
|
||||
interface IPreFormattedCode {
|
||||
data: Record<string, any> | Array<any>; // eslint-disable-line
|
||||
prefixCharacter?: string;
|
||||
color?: string;
|
||||
showCopyIcon?: boolean;
|
||||
}
|
||||
|
||||
const iconChangeTimeout = 2000;
|
||||
const PreFormattedCode = ({data, prefixCharacter, color, showCopyIcon = true}: IPreFormattedCode): React.JSX.Element => {
|
||||
const [copied, setCopied] = useState(false);
|
||||
const json = JSON.stringify(data, null, 2);
|
||||
const lines = json.split('\n');
|
||||
const text = lines.map(line => line.replace(/^< /, '')).join('\n');
|
||||
const copyJsonToClipboard = (text: string): void => {
|
||||
navigator.clipboard.writeText(text);
|
||||
setCopied(true);
|
||||
|
||||
setTimeout(() => setCopied(false), iconChangeTimeout);
|
||||
};
|
||||
|
||||
const handleCopy = event => {
|
||||
const originalString = event.target.textContent;
|
||||
const modifiedString = originalString.replace(/[<>]/g, '');
|
||||
|
||||
event.clipboardData.setData('text/plain', modifiedString);
|
||||
event.preventDefault();
|
||||
};
|
||||
|
||||
const handleCopyIconClick = () => copyJsonToClipboard(text);
|
||||
|
||||
return (
|
||||
<CodeContainer color={color}>
|
||||
<pre>
|
||||
<code onCopy={handleCopy}>
|
||||
{lines.map((line, index) => (
|
||||
<Fragment key={index}>
|
||||
{prefixCharacter} {line} <br />
|
||||
{index === lines.length - 1 && prefixCharacter}
|
||||
</Fragment>
|
||||
))}
|
||||
</code>
|
||||
</pre>
|
||||
{showCopyIcon && <IconButton onClick={handleCopyIconClick} tooltipText="Copy" icon={copied ? faCheck : faCopy} />}
|
||||
</CodeContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default PreFormattedCode;
|
||||
34
src/components/pre-formatted-code/styles.ts
Normal file
34
src/components/pre-formatted-code/styles.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
import * as styled from 'styled-components';
|
||||
import Theme from 'theme';
|
||||
|
||||
const {colors} = Theme;
|
||||
|
||||
export const CodeContainer = styled.default.div<{color?: string}>`
|
||||
background-color: ${colors.gray200};
|
||||
padding: 1rem;
|
||||
margin: 1rem 0 0;
|
||||
display: flex;
|
||||
color: ${({color}) => color || colors.black};
|
||||
position: relative;
|
||||
width: 100%;
|
||||
|
||||
&>span {
|
||||
position: absolute;
|
||||
right: 0.5rem;
|
||||
top: 0.5rem;
|
||||
}
|
||||
|
||||
pre {
|
||||
width: 100%;
|
||||
white-space: pre-wrap;
|
||||
max-height: 40vh;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
code {
|
||||
word-wrap: break-word;
|
||||
}
|
||||
`;
|
||||
6
src/components/restricted-text/index.ts
Normal file
6
src/components/restricted-text/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
export {default as RestrictedTextWithLabel} from './restricted-text-with-label';
|
||||
export {default as RestrictedText} from './restricted-text';
|
||||
export * from './restricted-text';
|
||||
@@ -0,0 +1,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;
|
||||
115
src/components/restricted-text/restricted-text.tsx
Normal file
115
src/components/restricted-text/restricted-text.tsx
Normal 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;
|
||||
96
src/components/restricted-text/style.ts
Normal file
96
src/components/restricted-text/style.ts
Normal 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};
|
||||
}
|
||||
`;
|
||||
2
src/components/shared/theme.ts
Normal file
2
src/components/shared/theme.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export {default, theme, Theme} from '../../theme';
|
||||
export * from '../../theme';
|
||||
@@ -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;
|
||||
};
|
||||
};
|
||||
|
||||
4
src/components/table-screen-header/index.ts
Normal file
4
src/components/table-screen-header/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
export {default as TableScreenHeader} from './table-screen-header';
|
||||
37
src/components/table-screen-header/style.tsx
Normal file
37
src/components/table-screen-header/style.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
import * as styled from 'styled-components';
|
||||
import Theme from 'theme';
|
||||
|
||||
const {colors} = Theme;
|
||||
|
||||
export const ScreenHeader = styled.default.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
flex-wrap: wrap;
|
||||
width: 100%;
|
||||
margin: 0 0 .5rem;
|
||||
`;
|
||||
|
||||
export const ScreenHeaderControls = styled.default.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
export const HeaderControlWrapper = styled.default.div`
|
||||
margin-left: 12px;
|
||||
`;
|
||||
|
||||
export const HeaderTitle = styled.default.div`
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
h2 {
|
||||
color: ${colors.white};
|
||||
margin: 0 .5rem 0 0;
|
||||
}
|
||||
p {
|
||||
color: ${colors.gray200};
|
||||
}
|
||||
`;
|
||||
48
src/components/table-screen-header/table-screen-header.tsx
Normal file
48
src/components/table-screen-header/table-screen-header.tsx
Normal 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;
|
||||
6
src/components/table-with-pagination/index.ts
Normal file
6
src/components/table-with-pagination/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
export * from 'components/table';
|
||||
export {default as TableWithPagination} from './table-with-pagination';
|
||||
export {default as Pagination} from '../pagination/pagination';
|
||||
205
src/components/table-with-pagination/table-with-pagination.tsx
Normal file
205
src/components/table-with-pagination/table-with-pagination.tsx
Normal 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;
|
||||
8
src/components/table/index.ts
Normal file
8
src/components/table/index.ts
Normal 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';
|
||||
74
src/components/table/props.ts
Normal file
74
src/components/table/props.ts
Normal 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;
|
||||
}
|
||||
75
src/components/table/style.tsx
Normal file
75
src/components/table/style.tsx
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
18
src/components/table/table-body.tsx
Normal file
18
src/components/table/table-body.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
import React from 'react';
|
||||
import 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;
|
||||
4
src/components/table/table-cells/index.ts
Normal file
4
src/components/table/table-cells/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
export * from './table-cell-dropdown';
|
||||
29
src/components/table/table-cells/table-cell-dropdown.tsx
Normal file
29
src/components/table/table-cells/table-cell-dropdown.tsx
Normal 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} />;
|
||||
};
|
||||
66
src/components/table/table-header-cell.tsx
Normal file
66
src/components/table/table-header-cell.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
30
src/components/table/table-header.tsx
Normal file
30
src/components/table/table-header.tsx
Normal 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;
|
||||
91
src/components/table/table-row.tsx
Normal file
91
src/components/table/table-row.tsx
Normal file
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
import React from 'react';
|
||||
import 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;
|
||||
107
src/components/table/table.tsx
Normal file
107
src/components/table/table.tsx
Normal 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;
|
||||
30
src/components/tags/index.tsx
Normal file
30
src/components/tags/index.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
37
src/components/tags/style.ts
Normal file
37
src/components/tags/style.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
|
||||
*/
|
||||
import * as styled from 'styled-components';
|
||||
import {theme, 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};
|
||||
}
|
||||
`;
|
||||
43
src/components/tooltip/index.tsx
Normal file
43
src/components/tooltip/index.tsx
Normal 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;
|
||||
89
src/components/tooltip/style.ts
Normal file
89
src/components/tooltip/style.ts
Normal 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;
|
||||
}
|
||||
`;
|
||||
10
src/components/tooltip/types.ts
Normal file
10
src/components/tooltip/types.ts
Normal 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'
|
||||
}
|
||||
@@ -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};
|
||||
`;
|
||||
`;
|
||||
|
||||
202
src/components/ui/advanced-select/advanced-select.tsx
Normal file
202
src/components/ui/advanced-select/advanced-select.tsx
Normal 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
Reference in New Issue
Block a user