Compare commits

..

1 Commits

Author SHA1 Message Date
5719743b57 - Updated the table background color to use Theme.colors.gray900 for better contrast
- Removed the dropdown component and its associated styles, as well as the publishing state indicator and related files, to streamline the codebase and eliminate unused features
2025-09-05 03:03:03 -04:00
49 changed files with 467 additions and 326 deletions

View File

@@ -4,10 +4,7 @@
import * as styled from 'styled-components'; import * as styled from 'styled-components';
import {theme} from 'components/shared/theme'; import {theme} from 'components/shared/theme';
const { const {typography: {fontSizeL}, colors} = theme;
typography: {fontSizeL},
colors
} = theme;
export const IconButton = styled.default.button` export const IconButton = styled.default.button`
border: none; border: none;

View File

@@ -4,11 +4,7 @@
import * as styled from 'styled-components'; import * as styled from 'styled-components';
import {theme, Theme} from 'components/shared/theme'; import {theme, Theme} from 'components/shared/theme';
const { const {spacing, typography: {primaryFontSize}, colors} = theme;
spacing,
typography: {primaryFontSize},
colors
} = theme;
const paddings = Theme.paddings; const paddings = Theme.paddings;
export const RadioGroup = styled.default.div` export const RadioGroup = styled.default.div`

View File

@@ -15,17 +15,7 @@ import {Capabilities} from 'components/create-token-components';
import Theme from 'theme'; import Theme from 'theme';
const forkChannelOption = ['force', 'additive']; const forkChannelOption = ['force', 'additive'];
const ForkChannelForm = ({ const ForkChannelForm = ({setFormData, setIsValid, channelList, alias}: {setFormData: (data: any) => void; setIsValid: (isValid: boolean) => void; channelList: any[]; alias: string}): React.JSX.Element => {
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 [error, setError] = useState<string | null>(null);
const [tags, setTags] = useState<string[]>([]); const [tags, setTags] = useState<string[]>([]);
const [forkOptions, setForkOptions] = useState<string[]>([]); const [forkOptions, setForkOptions] = useState<string[]>([]);

View File

@@ -19,15 +19,7 @@ const keepStreamsTooltipMessage = 'Keeps the removed streams alive';
const destroyRequired = 'destroy-required'; const destroyRequired = 'destroy-required';
const destroyRequiredTooltipMessage = 'Returns an error if destroying of a stream fails'; const destroyRequiredTooltipMessage = 'Returns an error if destroying of a stream fails';
const defaultReason = 'portal:killed'; const defaultReason = 'portal:killed';
const KillChannelForm = ({ const KillChannelForm = ({setFormData, setIsValid, alias}: {setFormData: (data: any) => void; setIsValid: (isValid: boolean) => void; alias: string}): React.JSX.Element => {
setFormData,
setIsValid,
alias
}: {
setFormData: (data: any) => void;
setIsValid: (isValid: boolean) => void;
alias: string;
}): React.JSX.Element => {
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [enteredAlias, setEnteredAlias] = useState(''); const [enteredAlias, setEnteredAlias] = useState('');
const [reason, setReason] = useState(defaultReason); const [reason, setReason] = useState(defaultReason);

View File

@@ -50,11 +50,7 @@ export const Capabilities = ({
allowMultiple={allowMultiple} allowMultiple={allowMultiple}
label={label} label={label}
labelColor={labelColor} labelColor={labelColor}
labelIcon={ labelIcon={capabilitiesSetTitle !== CapabilitiesType.Quality ? <NewTabLink link={documentationLinks.supportedStreamCapabilities} icon={faQuestionCircle} iconColor={iconColor} /> : undefined}
capabilitiesSetTitle !== CapabilitiesType.Quality ? (
<NewTabLink link={documentationLinks.supportedStreamCapabilities} icon={faQuestionCircle} iconColor={iconColor} />
) : undefined
}
data={capabilitiesSet} data={capabilitiesSet}
selectedItems={selectedItems} selectedItems={selectedItems}
setSelectedItems={setSelectedItems} setSelectedItems={setSelectedItems}
@@ -63,4 +59,4 @@ export const Capabilities = ({
); );
}; };
export default Capabilities; export default Capabilities;

View File

@@ -5,4 +5,4 @@ export {default as Capabilities} from './capabilities';
export {default as ValidityTimeComponent} from './validity-time'; export {default as ValidityTimeComponent} from './validity-time';
export * from './validity-time'; export * from './validity-time';
export {default as LabelIconTooltip} from './label-icon-tooltip'; export {default as LabelIconTooltip} from './label-icon-tooltip';
export * from './styles'; export * from './styles';

View File

@@ -14,13 +14,20 @@ interface CapabilitiesProps {
setSelectedItems: (items: IAdvancedSelectItem[]) => void; setSelectedItems: (items: IAdvancedSelectItem[]) => void;
} }
export const Capabilities: React.FC<CapabilitiesProps> = ({label, labelColor, iconColor, capabilitiesSetTitle, selectedItems, setSelectedItems}) => { 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 // Mock capabilities data - in a real app this would come from constants/capabilities
const mockCapabilities: IAdvancedSelectItem[] = [ const mockCapabilities: IAdvancedSelectItem[] = [
{value: 'streaming', text: 'Streaming'}, { value: 'streaming', text: 'Streaming' },
{value: 'recording', text: 'Recording'}, { value: 'recording', text: 'Recording' },
{value: 'analytics', text: 'Analytics'}, { value: 'analytics', text: 'Analytics' },
{value: 'transcoding', text: 'Transcoding'} { value: 'transcoding', text: 'Transcoding' }
]; ];
return ( return (
@@ -34,4 +41,4 @@ export const Capabilities: React.FC<CapabilitiesProps> = ({label, labelColor, ic
/> />
</div> </div>
); );
}; };

View File

@@ -14,9 +14,13 @@ interface ILabelIconTooltip {
} }
export const LabelIconTooltip = ({message, icon, position}: ILabelIconTooltip): React.JSX.Element => ( export const LabelIconTooltip = ({message, icon, position}: ILabelIconTooltip): React.JSX.Element => (
<Tooltip position={position || Position.Right} message={message} width={300}> <Tooltip
position={position || Position.Right}
message={message}
width={300}
>
<FontAwesomeIcon icon={icon || faQuestionCircle} /> <FontAwesomeIcon icon={icon || faQuestionCircle} />
</Tooltip> </Tooltip>
); );
export default LabelIconTooltip; export default LabelIconTooltip;

View File

@@ -7,7 +7,10 @@ import {theme} from 'components/shared/theme';
import {RadioGroup} from 'components/buttons/radio-button/style'; import {RadioGroup} from 'components/buttons/radio-button/style';
// import Input from 'components/forms/Input'; // import Input from 'components/forms/Input';
const {spacing, colors} = theme; const {
spacing,
colors
} = theme;
export const CreateTokenContainer = styled.default.div` export const CreateTokenContainer = styled.default.div`
margin: 0.5rem 0 0; margin: 0.5rem 0 0;
@@ -54,4 +57,4 @@ export const RowsWrapper = styled.default.div`
width: 100%; width: 100%;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
`; `;

View File

@@ -48,9 +48,13 @@ export const ValidityTimeComponent = ({currentValue, onChange}: IValidityTimeCom
<Fragment> <Fragment>
<Label text="Valid For:" color="white" /> <Label text="Valid For:" color="white" />
<RadioButtonContainer className="testId-expirationTime"> <RadioButtonContainer className="testId-expirationTime">
<RadioButtonGroup items={Object.values(validityTimeOptions)} currentValue={currentValue} handleOnChange={onChange} /> <RadioButtonGroup
items={Object.values(validityTimeOptions)}
currentValue={currentValue}
handleOnChange={onChange}
/>
</RadioButtonContainer> </RadioButtonContainer>
</Fragment> </Fragment>
); );
export default ValidityTimeComponent; export default ValidityTimeComponent;

View File

@@ -7,7 +7,13 @@ import {Moment} from 'moment';
import {AppStore} from 'store'; import {AppStore} from 'store';
import {getTimezoneAbbreviation, isoTimeFormat} from 'utility/date'; import {getTimezoneAbbreviation, isoTimeFormat} from 'utility/date';
export const DateComponent = ({date, className}: {date: Moment; className?: string}): JSX.Element => { export const DateComponent = ({
date,
className
}: {
date: Moment;
className?: string;
}): JSX.Element => {
const preferredTimeFormat = useSelector((state: AppStore) => state.preferredTimeFormat.timeFormat); const preferredTimeFormat = useSelector((state: AppStore) => state.preferredTimeFormat.timeFormat);
const isUTC = preferredTimeFormat === 'utc'; const isUTC = preferredTimeFormat === 'utc';
const utcDate = date.isValid() ? date.format(`${isoTimeFormat} UTC`) : ''; const utcDate = date.isValid() ? date.format(`${isoTimeFormat} UTC`) : '';
@@ -33,8 +39,13 @@ export const DateComponent = ({date, className}: {date: Moment; className?: stri
}; };
return ( return (
<p onMouseEnter={onMouseEnter} onMouseOut={onMouseOut} onBlur={onBlur} className={className}> <p
onMouseEnter={onMouseEnter}
onMouseOut={onMouseOut}
onBlur={onBlur}
className={className}
>
{currentDate} {currentDate}
</p> </p>
); );
}; };

View File

@@ -48,12 +48,12 @@ export const Dropdown = (props: IDropdown): React.JSX.Element => {
} }
const isOutOfUpperView = menuItem.offsetTop < dropdownMenu.scrollTop; const isOutOfUpperView = menuItem.offsetTop < dropdownMenu.scrollTop;
const isOutOfLowerView = menuItem.offsetTop + menuItem.clientHeight > dropdownMenu.scrollTop + dropdownMenu.clientHeight; const isOutOfLowerView = (menuItem.offsetTop + menuItem.clientHeight) > (dropdownMenu.scrollTop + dropdownMenu.clientHeight);
if (isOutOfUpperView) { if (isOutOfUpperView) {
dropdownMenu.scrollTop = menuItem.offsetTop; dropdownMenu.scrollTop = menuItem.offsetTop;
} else if (isOutOfLowerView) { } else if (isOutOfLowerView) {
dropdownMenu.scrollTop = menuItem.offsetTop + menuItem.clientHeight - dropdownMenu.clientHeight; dropdownMenu.scrollTop = (menuItem.offsetTop + menuItem.clientHeight) - dropdownMenu.clientHeight;
} }
}; };
@@ -153,8 +153,8 @@ export const Dropdown = (props: IDropdown): React.JSX.Element => {
}; };
const generateMenuOptions = () => { const generateMenuOptions = () => {
return filteredItems.length ? ( return filteredItems.length
filteredItems.slice(0, maxNumOfItemsShown).map((item, index) => { ? filteredItems.slice(0, maxNumOfItemsShown).map((item, index) => {
return ( return (
<DropdownMenuItem <DropdownMenuItem
ref={ref => { ref={ref => {
@@ -162,14 +162,17 @@ export const Dropdown = (props: IDropdown): React.JSX.Element => {
}} }}
selected={selectedIndex === index} selected={selectedIndex === index}
key={`dropdown-menu-item-${index}`} key={`dropdown-menu-item-${index}`}
onClick={() => selectItem(index)}> onClick={() => selectItem(index)}
>
{(item as any)[itemKey]} {(item as any)[itemKey]}
</DropdownMenuItem> </DropdownMenuItem>
); );
}) })
) : ( : (
<DropdownMenuItem disabled={true}>No results found</DropdownMenuItem> <DropdownMenuItem disabled={true}>
); No results found
</DropdownMenuItem>
);
}; };
const handleInput = (event: React.ChangeEvent<HTMLInputElement>) => { const handleInput = (event: React.ChangeEvent<HTMLInputElement>) => {
@@ -185,7 +188,10 @@ export const Dropdown = (props: IDropdown): React.JSX.Element => {
return ( return (
<DropdownContainer> <DropdownContainer>
<Label htmlFor="autocomplete" text={label} /> <Label
htmlFor="autocomplete"
text={label}
/>
<DropdownInput <DropdownInput
onKeyDown={handleOnKeyDown} onKeyDown={handleOnKeyDown}
showMenu={showDropdownMenu} showMenu={showDropdownMenu}
@@ -203,4 +209,4 @@ export const Dropdown = (props: IDropdown): React.JSX.Element => {
)} )}
</DropdownContainer> </DropdownContainer>
); );
}; };

View File

@@ -14,13 +14,11 @@ export const DropdownContainer = styled.default.div`
`; `;
export const DropdownInput = styled.default(Input)<{showMenu?: boolean}>` export const DropdownInput = styled.default(Input)<{showMenu?: boolean}>`
${({showMenu}) => ${({showMenu}) => showMenu && styled.css`
showMenu && border-bottom-left-radius: 0px;
styled.css` border-bottom-right-radius: 0px;
border-bottom-left-radius: 0px; border-bottom-width: 0;
border-bottom-right-radius: 0px; `}
border-bottom-width: 0;
`}
`; `;
export const DropdownMenu = styled.default.div` export const DropdownMenu = styled.default.div`
@@ -58,4 +56,4 @@ export const DropdownMenuItem = styled.default.div<{
cursor: ${disabled ? 'text' : 'pointer'}; cursor: ${disabled ? 'text' : 'pointer'};
} }
`} `}
`; `;

View File

@@ -44,29 +44,31 @@ const StyledCheckbox = styled.div<{checked?: boolean}>`
display: inline-block; display: inline-block;
width: 1rem; width: 1rem;
height: 1rem; height: 1rem;
background: ${({checked}) => (checked ? colors.red : colors.transparent)}; background: ${({checked}) => checked ? colors.red : colors.transparent};
border-radius: 3px; border-radius: 3px;
transition: all 150ms; transition: all 150ms;
outline: none; outline: none;
border: 1px solid ${({checked}) => (checked ? 'none' : colors.gray400)}; border: 1px solid ${({checked}) => checked ? 'none' : colors.gray400};
${Icon} { ${Icon} {
visibility: ${({checked}) => (checked ? 'visible' : 'hidden')}; visibility: ${({checked}) => checked ? 'visible' : 'hidden'}
} }
`; `;
const Checkbox = (props: { const Checkbox = (
value: string; props: {
id?: string; value: string;
checked?: boolean; id?: string;
onChange: (event: ChangeEvent<HTMLInputElement> | MouseEvent<HTMLDivElement>) => void; checked?: boolean;
label?: string; onChange: (event: ChangeEvent<HTMLInputElement> | MouseEvent<HTMLDivElement>) => void;
}): React.JSX.Element => { label?: string;
}
): React.JSX.Element => {
const {checked, onChange, label, id} = props; const {checked, onChange, label, id} = props;
return ( return (
<Container> <Container>
<CheckboxContainer> <CheckboxContainer>
<HiddenCheckbox {...props} /> <HiddenCheckbox {...props}/>
<StyledCheckbox onClick={onChange} checked={checked}> <StyledCheckbox onClick={onChange} checked={checked}>
<Icon viewBox="0 0 24 24"> <Icon viewBox="0 0 24 24">
<polyline points="20 6 9 17 4 12" /> <polyline points="20 6 9 17 4 12" />
@@ -78,4 +80,4 @@ const Checkbox = (props: {
); );
}; };
export default Checkbox; export default Checkbox;

View File

@@ -61,14 +61,28 @@ interface CheckboxProps {
className?: string; className?: string;
} }
const Checkbox: React.FC<CheckboxProps> = ({id, checked, onChange, label, value, disabled = false, className}) => { const Checkbox: React.FC<CheckboxProps> = ({
id,
checked,
onChange,
label,
value,
disabled = false,
className
}) => {
return ( return (
<CheckboxContainer className={className}> <CheckboxContainer className={className}>
<HiddenCheckbox id={id} checked={checked} onChange={onChange} value={value} disabled={disabled} /> <HiddenCheckbox
id={id}
checked={checked}
onChange={onChange}
value={value}
disabled={disabled}
/>
<StyledCheckbox checked={checked} /> <StyledCheckbox checked={checked} />
{label && <Label htmlFor={id}>{label}</Label>} {label && <Label htmlFor={id}>{label}</Label>}
</CheckboxContainer> </CheckboxContainer>
); );
}; };
export default Checkbox; export default Checkbox;

View File

@@ -40,7 +40,11 @@ export const SearchInputWrapper = styled.default.div`
} }
`; `;
export const Search = ({search, defaultValue = '', minLengthForSearch = 2}: ISearchInput): JSX.Element => { export const Search = ({
search,
defaultValue = '',
minLengthForSearch = 2
}: ISearchInput): JSX.Element => {
const [currentSearchTerm, setCurrentSearchTerm] = useState<string>(defaultValue); const [currentSearchTerm, setCurrentSearchTerm] = useState<string>(defaultValue);
const onChange = (event: ChangeEvent<HTMLInputElement>): void => { const onChange = (event: ChangeEvent<HTMLInputElement>): void => {
setCurrentSearchTerm(event.target.value); setCurrentSearchTerm(event.target.value);
@@ -70,4 +74,4 @@ export const Search = ({search, defaultValue = '', minLengthForSearch = 2}: ISea
); );
}; };
export default Search; export default Search;

View File

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

View File

@@ -5,23 +5,7 @@ import {JSX} from 'react';
import {LoadingWheel as Loader} from 'components/loaders'; import {LoadingWheel as Loader} from 'components/loaders';
import {SingleStreamSymbol, OfflineSymbol, MultipleStreamSymbol, Indicator} from './style'; import {SingleStreamSymbol, OfflineSymbol, MultipleStreamSymbol, Indicator} from './style';
export const OfflineIndicator = (): JSX.Element => ( export const OfflineIndicator = (): JSX.Element => <Indicator><OfflineSymbol /></Indicator>;
<Indicator> export const SingleStreamIndicator = (): JSX.Element => <Indicator className="single-stream-indicator"><SingleStreamSymbol className="testId-singleStreamIndicator" /></Indicator>;
<OfflineSymbol /> export const MultiStreamIndicator = (): JSX.Element => <Indicator className="multi-stream-indicator"><MultipleStreamSymbol className="testId-multiStreamIndicator" /></Indicator>;
</Indicator> export const LoadingIndicator = (): JSX.Element => <Indicator><Loader size="medium" /></Indicator>;
);
export const SingleStreamIndicator = (): JSX.Element => (
<Indicator className="single-stream-indicator">
<SingleStreamSymbol className="testId-singleStreamIndicator" />
</Indicator>
);
export const MultiStreamIndicator = (): JSX.Element => (
<Indicator className="multi-stream-indicator">
<MultipleStreamSymbol className="testId-multiStreamIndicator" />
</Indicator>
);
export const LoadingIndicator = (): JSX.Element => (
<Indicator>
<Loader size="medium" />
</Indicator>
);

View File

@@ -40,4 +40,4 @@ export const MultipleStreamSymbol = styled.default(SingleStreamSymbol)`
height: 1rem; height: 1rem;
margin-left: -2px; margin-left: -2px;
box-shadow: 9px 0 0 0 #08BD0B; box-shadow: 9px 0 0 0 #08BD0B;
`; `;

View File

@@ -14,7 +14,11 @@ interface IPublishingStateIndicator {
idKey: string; idKey: string;
} }
const PublishingStateIndicator = ({row, publishingStateKey, idKey}: IPublishingStateIndicator): JSX.Element => { const PublishingStateIndicator = ({
row,
publishingStateKey,
idKey
}: IPublishingStateIndicator): JSX.Element => {
const id = row[idKey]; const id = row[idKey];
const publishingState = useSelector((state: AppStore) => publishingStateKey && state[publishingStateKey as keyof AppStore]?.publishingState); const publishingState = useSelector((state: AppStore) => publishingStateKey && state[publishingStateKey as keyof AppStore]?.publishingState);
const rowPublishingState = publishingState.find((record: Record<string, any>) => record[idKey] === id); const rowPublishingState = publishingState.find((record: Record<string, any>) => record[idKey] === id);
@@ -34,4 +38,4 @@ const PublishingStateIndicator = ({row, publishingStateKey, idKey}: IPublishingS
return <SingleStreamIndicator />; return <SingleStreamIndicator />;
}; };
export default PublishingStateIndicator; export default PublishingStateIndicator;

View File

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

View File

@@ -70,11 +70,7 @@ const Modal = (props: IModal): React.JSX.Element => {
<CloseButton onClick={close} /> <CloseButton onClick={close} />
{children} {children}
<ModalButtonsContaier> <ModalButtonsContaier>
{submitButton.onClick && ( {submitButton.onClick && <ConfirmButton {...submitButton} onClick={submitButton.onClick}>{submitButton.label}</ConfirmButton>}
<ConfirmButton {...submitButton} onClick={submitButton.onClick}>
{submitButton.label}
</ConfirmButton>
)}
{!props.cancelButton || {!props.cancelButton ||
(!cancelButton.disabled && ( (!cancelButton.disabled && (
<ConfirmButton className="testId-cancel" {...cancelButton} onClick={handleCancel}> <ConfirmButton className="testId-cancel" {...cancelButton} onClick={handleCancel}>

View File

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

View File

@@ -2,7 +2,12 @@
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved. * Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
*/ */
import {Fragment} from 'react'; import {Fragment} from 'react';
import {PaginationWrapper, ItemRange, PaginationContainer, PageButton} from './style'; import {
PaginationWrapper,
ItemRange,
PaginationContainer,
PageButton
} from './style';
interface IPagination { interface IPagination {
currentPageNumber: number; currentPageNumber: number;
@@ -22,8 +27,8 @@ export const Pagination = (props: IPagination): JSX.Element => {
setCurrentPage(page); setCurrentPage(page);
}; };
const minimumItemBoundPerPage = (currentPageNumber - 1) * itemsPerPage + 1; const minimumItemBoundPerPage = ((currentPageNumber - 1) * itemsPerPage) + 1;
const maximumItemBoundPerPage = currentPageNumber * itemsPerPage; const maximumItemBoundPerPage = (currentPageNumber) * itemsPerPage;
const generateButtons = (bounds = 1): JSX.Element[] => { const generateButtons = (bounds = 1): JSX.Element[] => {
const buttonToShow = maxNumberOfButtonsToShow > totalNumberOfPages ? totalNumberOfPages : maxNumberOfButtonsToShow; const buttonToShow = maxNumberOfButtonsToShow > totalNumberOfPages ? totalNumberOfPages : maxNumberOfButtonsToShow;
const buttons = []; const buttons = [];
@@ -31,7 +36,7 @@ export const Pagination = (props: IPagination): JSX.Element => {
if (currentPageNumber <= bounds) { if (currentPageNumber <= bounds) {
count = 1; count = 1;
} else if (currentPageNumber + bounds > totalNumberOfPages) { } else if ((currentPageNumber + bounds) > totalNumberOfPages) {
count = currentPageNumber - (buttonToShow - (totalNumberOfPages - currentPageNumber + 1)); count = currentPageNumber - (buttonToShow - (totalNumberOfPages - currentPageNumber + 1));
} else { } else {
count = currentPageNumber - bounds; count = currentPageNumber - bounds;
@@ -39,7 +44,10 @@ export const Pagination = (props: IPagination): JSX.Element => {
for (let y = 0; y < buttonToShow; y++) { for (let y = 0; y < buttonToShow; y++) {
buttons.push( buttons.push(
<PageButton key={`page-button-${y}`} onClick={() => setPage(count + y)} active={currentPageNumber === count + y}> <PageButton
key={`page-button-${y}`}
onClick={() => setPage(count + y)}
active={currentPageNumber === count + y}>
{count + y} {count + y}
</PageButton> </PageButton>
); );
@@ -52,29 +60,28 @@ export const Pagination = (props: IPagination): JSX.Element => {
<PaginationContainer className="pagination-container"> <PaginationContainer className="pagination-container">
{numberOfItems ? ( {numberOfItems ? (
<PaginationWrapper> <PaginationWrapper>
{currentPageNumber >= LowerBoundLimit && ( {(currentPageNumber >= LowerBoundLimit) && (
<Fragment> <Fragment>
{currentPageNumber > 2 && totalNumberOfPages !== 3 && <PageButton onClick={() => setPage(1)}>1</PageButton>} {currentPageNumber > 2 && totalNumberOfPages !== 3 && <PageButton onClick={() => setPage(1)}>1</PageButton>}
{currentPageNumber > LowerBoundLimit && currentPageNumber !== 3 && <p>...</p>} {(currentPageNumber > LowerBoundLimit) && currentPageNumber !== 3 && <p>...</p>}
</Fragment> </Fragment>
)} )}
{generateButtons()} {generateButtons()}
{currentPageNumber <= totalNumberOfPages - higherBoundLimit && ( {(currentPageNumber <= totalNumberOfPages - higherBoundLimit) && (
<Fragment> <Fragment>
{currentPageNumber < totalNumberOfPages - higherBoundLimit && currentPageNumber + 2 !== totalNumberOfPages && <p>...</p>} {(currentPageNumber < totalNumberOfPages - higherBoundLimit) && currentPageNumber + 2 !== totalNumberOfPages && <p>...</p>}
{currentPageNumber + 1 !== totalNumberOfPages && totalNumberOfPages !== 3 && ( {currentPageNumber + 1 !== totalNumberOfPages && totalNumberOfPages !== 3 && <PageButton onClick={() => setPage(totalNumberOfPages)}>
<PageButton onClick={() => setPage(totalNumberOfPages)}>{totalNumberOfPages}</PageButton> {totalNumberOfPages}
)} </PageButton>}
</Fragment> </Fragment>
)} )}
</PaginationWrapper> </PaginationWrapper>
) : null} ) : null}
<ItemRange> <ItemRange>
{numberOfItems > 0 ? minimumItemBoundPerPage : 0} - {maximumItemBoundPerPage > numberOfItems ? numberOfItems : maximumItemBoundPerPage} of{' '} {numberOfItems > 0 ? minimumItemBoundPerPage : 0} - {maximumItemBoundPerPage > numberOfItems ? numberOfItems : maximumItemBoundPerPage} of {numberOfItems} {itemText ? itemText : `channels`}
{numberOfItems} {itemText ? itemText : `channels`}
</ItemRange> </ItemRange>
</PaginationContainer> </PaginationContainer>
); );
}; };
export default Pagination; export default Pagination;

View File

@@ -4,7 +4,12 @@
import * as styled from 'styled-components'; import * as styled from 'styled-components';
import {theme, paddings} from 'components/shared/theme'; import {theme, paddings} from 'components/shared/theme';
const {colors, fontSizeS, spacing, primaryThemeColor} = theme; const {
colors,
fontSizeS,
spacing,
primaryThemeColor
} = theme;
export const PaginationContainer = styled.default.div` export const PaginationContainer = styled.default.div`
display: flex; display: flex;
@@ -27,7 +32,7 @@ export const ItemRange = styled.default.div`
color: ${colors.lightBlue}; color: ${colors.lightBlue};
`; `;
export const PageButton = styled.default.button<{active?: boolean}>` export const PageButton = styled.default.button <{active?: boolean}>`
margin: 0 ${paddings.xsmall} ${paddings.xsmall} 0; margin: 0 ${paddings.xsmall} ${paddings.xsmall} 0;
font-size: ${fontSizeS}; font-size: ${fontSizeS};
border: 1px solid ${primaryThemeColor}; border: 1px solid ${primaryThemeColor};
@@ -35,6 +40,6 @@ export const PageButton = styled.default.button<{active?: boolean}>`
height: 28px; height: 28px;
width: 36px; width: 36px;
cursor: pointer; cursor: pointer;
background-color: ${({active}) => (active ? primaryThemeColor : 'transparent')}; background-color: ${({active}) => active ? primaryThemeColor : 'transparent'};
color: ${({active}) => (active ? colors.white : primaryThemeColor)}; color: ${({active}) => active ? colors.white : primaryThemeColor};
`; `;

View File

@@ -3,4 +3,4 @@
*/ */
export {default as RestrictedTextWithLabel} from './restricted-text-with-label'; export {default as RestrictedTextWithLabel} from './restricted-text-with-label';
export {default as RestrictedText} from './restricted-text'; export {default as RestrictedText} from './restricted-text';
export * from './restricted-text'; export * from './restricted-text';

View File

@@ -14,12 +14,16 @@ interface IRestrictedTextWithLabel {
labelIcon?: JSX.Element; labelIcon?: JSX.Element;
} }
const RestrictedTextWithLabel = ({label, text, labelIcon, isLink = false, linkClassName = ''}: IRestrictedTextWithLabel): JSX.Element => { const RestrictedTextWithLabel = ({
label,
text,
labelIcon,
isLink = false,
linkClassName = ''
}: IRestrictedTextWithLabel): JSX.Element => {
return ( return (
<RestrictedRowWrapper> <RestrictedRowWrapper>
<RestrictedRowLabel> <RestrictedRowLabel>{label} {labelIcon}</RestrictedRowLabel>
{label} {labelIcon}
</RestrictedRowLabel>
<RestrictedRowText> <RestrictedRowText>
<RestrictedText text={text} isLink={isLink} linkClassName={linkClassName} /> <RestrictedText text={text} isLink={isLink} linkClassName={linkClassName} />
</RestrictedRowText> </RestrictedRowText>
@@ -27,4 +31,4 @@ const RestrictedTextWithLabel = ({label, text, labelIcon, isLink = false, linkCl
); );
}; };
export default RestrictedTextWithLabel; export default RestrictedTextWithLabel;

View File

@@ -66,12 +66,21 @@ export const RestrictedText = ({
const handleShowValueChange = () => setShowValue(!showValue); const handleShowValueChange = () => setShowValue(!showValue);
const CurrentTextView = (): JSX.Element => { const CurrentTextView = (): JSX.Element => {
if (isLink && !linkValue) { if (isLink && !linkValue) {
return <NewTabLink link={text} className={linkClassName} text={text} />; return (
<NewTabLink
link={text}
className={linkClassName}
text={text}
/>
);
} }
if (isLink && linkValue) { if (isLink && linkValue) {
return ( return (
<Link to={linkValue} className={linkClassName}> <Link
to={linkValue}
className={linkClassName}
>
{text} {text}
</Link> </Link>
); );
@@ -95,10 +104,12 @@ export const RestrictedText = ({
className={`${isLink ? 'testId-permalink' : ''} testId-viewDetails`} className={`${isLink ? 'testId-permalink' : ''} testId-viewDetails`}
/> />
)} )}
{hasCopyOption && <CopyIconButton text={text} displayText={false} className="testId-copyDetails" />} {hasCopyOption && (
<CopyIconButton text={text} displayText={false} className="testId-copyDetails" />
)}
</div> </div>
</RestrictedDiv> </RestrictedDiv>
); );
}; };
export default RestrictedText; export default RestrictedText;

View File

@@ -5,7 +5,11 @@ import * as styled from 'styled-components';
import Theme from 'theme'; import Theme from 'theme';
const {colors, typography, spacing} = Theme; const {
colors,
typography,
spacing,
} = Theme;
export const RestrictedDiv = styled.default.div` export const RestrictedDiv = styled.default.div`
display: flex; display: flex;
@@ -89,4 +93,4 @@ export const RestrictedRowText = styled.default.div`
text-decoration: none; text-decoration: none;
font-size: ${typography.fontSizeS}; font-size: ${typography.fontSizeS};
} }
`; `;

View File

@@ -1,4 +1,4 @@
/** /**
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved. * Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
*/ */
export {default as TableScreenHeader} from './table-screen-header'; export {default as TableScreenHeader} from './table-screen-header';

View File

@@ -34,4 +34,4 @@ export const HeaderTitle = styled.default.div`
p { p {
color: ${colors.gray200}; color: ${colors.gray200};
} }
`; `;

View File

@@ -2,7 +2,11 @@
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved. * Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
*/ */
import {JSX} from 'react'; import {JSX} from 'react';
import {ITableWithLoadMoreHeader, ITableWithPaginationHeader, TableHeaderKey} from 'interfaces/tableProps'; import {
ITableWithLoadMoreHeader,
ITableWithPaginationHeader,
TableHeaderKey
} from 'interfaces/tableProps';
import {ScreenHeader, ScreenHeaderControls, HeaderControlWrapper, HeaderTitle} from './style'; import {ScreenHeader, ScreenHeaderControls, HeaderControlWrapper, HeaderTitle} from './style';
type ScreenHeaderProps = ITableWithLoadMoreHeader | ITableWithPaginationHeader; type ScreenHeaderProps = ITableWithLoadMoreHeader | ITableWithPaginationHeader;
@@ -14,7 +18,12 @@ export interface ITableScreenHeader {
renderControl: (screenHeader: ScreenHeaderProps, key: TableHeaderKey) => JSX.Element | null; renderControl: (screenHeader: ScreenHeaderProps, key: TableHeaderKey) => JSX.Element | null;
} }
export const TableScreenHeader = ({title, subtitle = '', screenHeader, renderControl}: ITableScreenHeader): JSX.Element => { export const TableScreenHeader = ({
title,
subtitle = '',
screenHeader,
renderControl
}: ITableScreenHeader): JSX.Element => {
return ( return (
<ScreenHeader className="table-header"> <ScreenHeader className="table-header">
<HeaderTitle> <HeaderTitle>
@@ -23,13 +32,17 @@ export const TableScreenHeader = ({title, subtitle = '', screenHeader, renderCon
</HeaderTitle> </HeaderTitle>
<ScreenHeaderControls> <ScreenHeaderControls>
{Object.keys(screenHeader).map(key => { {Object.keys(screenHeader).map(key => {
const headerControl = screenHeader[key].render ? screenHeader[key].render(key) : renderControl(screenHeader, key as TableHeaderKey); const headerControl = screenHeader[key].render
? screenHeader[key].render(key)
: renderControl(screenHeader, key as TableHeaderKey);
return headerControl ? <HeaderControlWrapper key={key}>{headerControl}</HeaderControlWrapper> : null; return headerControl ? (
<HeaderControlWrapper key={key}>{headerControl}</HeaderControlWrapper>
) : null;
})} })}
</ScreenHeaderControls> </ScreenHeaderControls>
</ScreenHeader> </ScreenHeader>
); );
}; };
export default TableScreenHeader; export default TableScreenHeader;

View File

@@ -3,4 +3,4 @@
*/ */
export * from 'components/table'; export * from 'components/table';
export {default as TableWithPagination} from './table-with-pagination'; export {default as TableWithPagination} from './table-with-pagination';
export {default as Pagination} from '../pagination/pagination'; export {default as Pagination} from '../pagination/pagination';

View File

@@ -3,13 +3,20 @@
*/ */
import {useEffect, useState} from 'react'; import {useEffect, useState} from 'react';
import {DataRowType, Table, TableHeaderKey, ITable, ITableWithPaginationHeader, ITableSort} from 'components/table'; import {
DataRowType,
Table,
TableHeaderKey,
ITable,
ITableWithPaginationHeader,
ITableSort
} from 'components/table';
import {isEqual} from 'utility/validators'; import {isEqual} from 'utility/validators';
import SearchInput from 'components/forms/SearchInput'; import SearchInput from 'components/forms/SearchInput';
import {compare} from 'utility/sort'; import {compare} from 'utility/sort';
import {TableScreenHeader} from 'components/table-screen-header/table-screen-header'; import {TableScreenHeader} from 'components/table-screen-header/table-screen-header';
import {AddButton} from 'components/buttons/icon-buttons'; import {AddButton} from 'components/buttons/icon-buttons';
import {Pagination} from 'components/pagination/pagination'; import {Pagination} from 'components/pagination/pagination' ;
import {Select} from 'components/ui/select'; import {Select} from 'components/ui/select';
import {useSort, useSearch} from 'utility/custom-hooks'; import {useSort, useSearch} from 'utility/custom-hooks';
@@ -121,7 +128,9 @@ const TableWithPagination = ({
useEffect(() => { useEffect(() => {
const start = (currentPageNumber - 1) * rowsCount; const start = (currentPageNumber - 1) * rowsCount;
const sortedData = sortData ? filteredData.sort((a, b) => compare(a, b, sortData.sortDirection, sortData.sortColumn)) : filteredData; const sortedData = sortData
? filteredData.sort((a, b) => compare(a, b, sortData.sortDirection, sortData.sortColumn))
: filteredData;
const newCurrentData = sortedData.slice(start, start + rowsCount); const newCurrentData = sortedData.slice(start, start + rowsCount);
if (!isEqual(newCurrentData, currentData)) { if (!isEqual(newCurrentData, currentData)) {
@@ -168,8 +177,20 @@ const TableWithPagination = ({
return ( return (
<> <>
<TableScreenHeader title={title} screenHeader={screenHeader} renderControl={renderControl} /> <TableScreenHeader
<Table columns={columns} data={currentData} sortColumn={sortColumn} sortDirection={sortDirection} style={style} sort={sort} errorMessage={errorMessage} /> title={title}
screenHeader={screenHeader}
renderControl={renderControl}
/>
<Table
columns={columns}
data={currentData}
sortColumn={sortColumn}
sortDirection={sortDirection}
style={style}
sort={sort}
errorMessage={errorMessage}
/>
<Pagination <Pagination
currentPageNumber={currentPageNumber} currentPageNumber={currentPageNumber}
setCurrentPage={setCurrentPageNumber} setCurrentPage={setCurrentPageNumber}
@@ -181,4 +202,4 @@ const TableWithPagination = ({
); );
}; };
export default TableWithPagination; export default TableWithPagination;

View File

@@ -1 +1 @@
export * from '../constants/capabilities'; export * from '../constants/capabilities';

View File

@@ -1 +1 @@
export * from '../constants/error-messages'; export * from '../constants/error-messages';

View File

@@ -1 +1 @@
export * from '../constants/links'; export * from '../constants/links';

View File

@@ -3,8 +3,9 @@ body {
padding: 0; padding: 0;
background: #f8f9fa; background: #f8f9fa;
min-height: 100vh; min-height: 100vh;
font-family: font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} }

View File

@@ -1,8 +1,8 @@
import LoggerFactory from './logger/LoggerFactory'; import LoggerFactory from './logger/LoggerFactory';
import ILogger from './logger/LoggerInterface'; import ILogger from './logger/LoggerInterface';
import PlatformDetectionService from './PlatformDetection.service'; import PlatformDetectionService from './PlatformDetection.service';
import {AuthenticationResponse, PhenixWebSocket} from './net/websockets/PhenixWebSocket'; import { AuthenticationResponse, PhenixWebSocket } from './net/websockets/PhenixWebSocket';
import {PhenixWebSocketMessage} from './net/websockets/PhenixWebSocketMessage'; import { PhenixWebSocketMessage } from './net/websockets/PhenixWebSocketMessage';
import UserStoreService from './user-store'; import UserStoreService from './user-store';
//TEMPORARY //TEMPORARY

View File

@@ -1,7 +1,7 @@
/** /**
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved. * Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
*/ */
import {Channel, Channels} from '@techniker-me/pcast-api'; import { Channel, Channels } from '@techniker-me/pcast-api';
import PCastApiService from './PCastApi.service'; import PCastApiService from './PCastApi.service';
import LoggerFactory from './logger/LoggerFactory'; import LoggerFactory from './logger/LoggerFactory';
@@ -48,10 +48,12 @@ export interface KillChannelParams {
class ChannelService { class ChannelService {
private phenixChannelService: PhenixChannelService | null = null; private phenixChannelService: PhenixChannelService | null = null;
public async initializeWithPCastApi(channels: Channels) { public async initializeWithPCastApi(channels: Channels) {
try { try {
// Wait for PCastApiService to be initialized // Wait for PCastApiService to be initialized
this.phenixChannelService = channels; this.phenixChannelService = channels;
} catch (error) { } catch (error) {
logger.error('Failed to initialize ChannelService', error); logger.error('Failed to initialize ChannelService', error);
} }
@@ -63,11 +65,11 @@ class ChannelService {
async listChannels(): Promise<Channel[]> { async listChannels(): Promise<Channel[]> {
try { try {
logger.debug('Fetching channel list'); logger.debug('Fetching channel list');
if (!this.phenixChannelService) { if (!this.phenixChannelService) {
return PCastApiService.channels.list(); return PCastApiService.channels.list();
} }
return await this.phenixChannelService.list(); return await this.phenixChannelService.list();
} catch (error) { } catch (error) {
logger.error('Failed to list channels', error); logger.error('Failed to list channels', error);
@@ -81,7 +83,7 @@ class ChannelService {
async createChannel(params: CreateChannelParams): Promise<Channel> { async createChannel(params: CreateChannelParams): Promise<Channel> {
try { try {
logger.debug('Creating channel', params); logger.debug('Creating channel', params);
const channelData = { const channelData = {
name: params.name, name: params.name,
alias: params.alias, alias: params.alias,
@@ -93,7 +95,7 @@ class ChannelService {
if (!this.phenixChannelService) { if (!this.phenixChannelService) {
return PCastApiService.channels.create(name, description, options); return PCastApiService.channels.create(name, description, options);
} }
return await this.phenixChannelService.create(channelData); return await this.phenixChannelService.create(channelData);
} catch (error) { } catch (error) {
logger.error('Failed to create channel', error); logger.error('Failed to create channel', error);
@@ -107,19 +109,19 @@ class ChannelService {
async updateChannel(params: UpdateChannelParams): Promise<Channel> { async updateChannel(params: UpdateChannelParams): Promise<Channel> {
try { try {
logger.debug('Updating channel', params); logger.debug('Updating channel', params);
const updateData = { const updateData = {
...(params.name && {name: params.name}), ...params.name && { name: params.name },
...(params.alias && {alias: params.alias}), ...params.alias && { alias: params.alias },
...(params.description && {description: params.description}), ...params.description && { description: params.description },
...(params.tags && {tags: params.tags}), ...params.tags && { tags: params.tags },
...(params.capabilities && {capabilities: params.capabilities}) ...params.capabilities && { capabilities: params.capabilities }
}; };
if (!this.phenixChannelService) { if (!this.phenixChannelService) {
return PCastApiService.channels.update(params.channelId, updateData); return PCastApiService.channels.update(params.channelId, updateData);
} }
return await this.phenixChannelService.update(params.channelId, updateData); return await this.phenixChannelService.update(params.channelId, updateData);
} catch (error) { } catch (error) {
logger.error('Failed to update channel', error); logger.error('Failed to update channel', error);
@@ -133,11 +135,11 @@ class ChannelService {
async deleteChannel(params: DeleteChannelParams): Promise<void> { async deleteChannel(params: DeleteChannelParams): Promise<void> {
try { try {
logger.debug('Deleting channel', params); logger.debug('Deleting channel', params);
if (!this.phenixChannelService) { if (!this.phenixChannelService) {
return PCastApiService.channels.delete(params.channelId); return PCastApiService.channels.delete(params.channelId);
} }
await this.phenixChannelService.delete(params.channelId); await this.phenixChannelService.delete(params.channelId);
} catch (error) { } catch (error) {
logger.error('Failed to delete channel', error); logger.error('Failed to delete channel', error);
@@ -151,7 +153,7 @@ class ChannelService {
async forkChannel(params: ForkChannelParams): Promise<void> { async forkChannel(params: ForkChannelParams): Promise<void> {
try { try {
logger.debug('Forking channel', params); logger.debug('Forking channel', params);
const forkData = { const forkData = {
destinationChannelId: params.destinationChannelId, destinationChannelId: params.destinationChannelId,
streamCapabilities: params.streamCapabilities || [], streamCapabilities: params.streamCapabilities || [],
@@ -163,7 +165,7 @@ class ChannelService {
if (!this.phenixChannelService) { if (!this.phenixChannelService) {
return PCastApiService.channels.fork(params.sourceChannelId, forkData); return PCastApiService.channels.fork(params.sourceChannelId, forkData);
} }
await this.phenixChannelService.fork(params.sourceChannelId, forkData); await this.phenixChannelService.fork(params.sourceChannelId, forkData);
} catch (error) { } catch (error) {
logger.error('Failed to fork channel', error); logger.error('Failed to fork channel', error);
@@ -177,7 +179,7 @@ class ChannelService {
async killChannel(params: KillChannelParams): Promise<void> { async killChannel(params: KillChannelParams): Promise<void> {
try { try {
logger.debug('Killing channel', params); logger.debug('Killing channel', params);
const killData = { const killData = {
reason: params.reason || 'portal:killed', reason: params.reason || 'portal:killed',
options: params.options || [] options: params.options || []
@@ -186,7 +188,7 @@ class ChannelService {
if (!this.phenixChannelService) { if (!this.phenixChannelService) {
return PCastApiService.channels.kill(params.channelId, killData); return PCastApiService.channels.kill(params.channelId, killData);
} }
await this.phenixChannelService.kill(params.channelId, killData); await this.phenixChannelService.kill(params.channelId, killData);
} catch (error) { } catch (error) {
logger.error('Failed to kill channel', error); logger.error('Failed to kill channel', error);
@@ -199,12 +201,12 @@ class ChannelService {
*/ */
async getChannel(channelId: string): Promise<Channel | null> { async getChannel(channelId: string): Promise<Channel | null> {
try { try {
logger.debug('Getting channel', {channelId}); logger.debug('Getting channel', { channelId });
if (!this.phenixChannelService) { if (!this.phenixChannelService) {
return PCastApiService.channels.get(channelId); return PCastApiService.channels.get(channelId);
} }
return await this.phenixChannelService.get(channelId); return await this.phenixChannelService.get(channelId);
} catch (error) { } catch (error) {
logger.error('Failed to get channel', error); logger.error('Failed to get channel', error);
@@ -217,12 +219,12 @@ class ChannelService {
*/ */
async getPublisherCount(channelId: string): Promise<number> { async getPublisherCount(channelId: string): Promise<number> {
try { try {
logger.debug('Getting publisher count', {channelId}); logger.debug('Getting publisher count', { channelId });
if (!this.phenixChannelService) { if (!this.phenixChannelService) {
return PCastApiService.channels.getPublisherCount(channelId); return PCastApiService.channels.getPublisherCount(channelId);
} }
return await this.phenixChannelService.getPublisherCount(channelId); return await this.phenixChannelService.getPublisherCount(channelId);
} catch (error) { } catch (error) {
logger.error('Failed to get publisher count', error); logger.error('Failed to get publisher count', error);
@@ -244,4 +246,4 @@ export const killChannel = channelService.killChannel.bind(channelService);
export const getChannel = channelService.getChannel.bind(channelService); export const getChannel = channelService.getChannel.bind(channelService);
export const getPublisherCount = channelService.getPublisherCount.bind(channelService); export const getPublisherCount = channelService.getPublisherCount.bind(channelService);
export default channelService; export default channelService;

View File

@@ -188,4 +188,4 @@ export default class PlatformDetectionService {
return match ? match[1] : null; return match ? match[1] : null;
} }
} }

View File

@@ -30,7 +30,8 @@ const initialState: ChannelsPublishingState = {
}; };
// Selectors // Selectors
export const channelsPublishingSelector = (state: RootState): ChannelsPublishingState => state.channelsPublishing; export const channelsPublishingSelector = (state: RootState): ChannelsPublishingState =>
state.channelsPublishing;
export const selectChannelsPublishingState = createSelector( export const selectChannelsPublishingState = createSelector(
[channelsPublishingSelector], [channelsPublishingSelector],
@@ -44,7 +45,8 @@ export const selectChannelsPublishingLoading = createSelector(
export const selectChannelPublishingState = createSelector( export const selectChannelPublishingState = createSelector(
[selectChannelsPublishingState, (_: RootState, channelId: string) => channelId], [selectChannelsPublishingState, (_: RootState, channelId: string) => channelId],
(publishingStates: ChannelPublishingState[], channelId: string) => publishingStates.find(state => state.channelId === channelId) (publishingStates: ChannelPublishingState[], channelId: string) =>
publishingStates.find(state => state.channelId === channelId)
); );
// Async thunks // Async thunks
@@ -106,7 +108,7 @@ const channelsPublishingSlice = createSlice({
name: 'channelsPublishing', name: 'channelsPublishing',
initialState, initialState,
reducers: { reducers: {
clearPublishingState: state => { clearPublishingState: (state) => {
state.publishingState = []; state.publishingState = [];
state.error = null; state.error = null;
}, },
@@ -114,8 +116,10 @@ const channelsPublishingSlice = createSlice({
state.error = action.payload; state.error = action.payload;
}, },
updateChannelState: (state, action: PayloadAction<ChannelPublishingState>) => { updateChannelState: (state, action: PayloadAction<ChannelPublishingState>) => {
const index = state.publishingState.findIndex(item => item.channelId === action.payload.channelId); const index = state.publishingState.findIndex(
item => item.channelId === action.payload.channelId
);
if (index >= 0) { if (index >= 0) {
state.publishingState[index] = action.payload; state.publishingState[index] = action.payload;
} else { } else {
@@ -123,9 +127,9 @@ const channelsPublishingSlice = createSlice({
} }
} }
}, },
extraReducers: builder => { extraReducers: (builder) => {
builder builder
.addCase(fetchChannelsPublishingState.pending, state => { .addCase(fetchChannelsPublishingState.pending, (state) => {
state.isLoading = true; state.isLoading = true;
state.error = null; state.error = null;
}) })
@@ -140,8 +144,10 @@ const channelsPublishingSlice = createSlice({
state.error = action.payload as string; state.error = action.payload as string;
}) })
.addCase(updateChannelPublishingState.fulfilled, (state, action) => { .addCase(updateChannelPublishingState.fulfilled, (state, action) => {
const index = state.publishingState.findIndex(item => item.channelId === action.payload.channelId); const index = state.publishingState.findIndex(
item => item.channelId === action.payload.channelId
);
if (index >= 0) { if (index >= 0) {
state.publishingState[index] = action.payload; state.publishingState[index] = action.payload;
} else { } else {
@@ -155,4 +161,4 @@ const channelsPublishingSlice = createSlice({
}); });
export const {clearPublishingState, setPublishingError, updateChannelState} = channelsPublishingSlice.actions; export const {clearPublishingState, setPublishingError, updateChannelState} = channelsPublishingSlice.actions;
export default channelsPublishingSlice.reducer; export default channelsPublishingSlice.reducer;

View File

@@ -9,85 +9,118 @@ import {IChannelsState} from '../slices/Channels.slice';
// Selectors // Selectors
export const channelsSelector = (state: RootState): IChannelsState => state.channels; export const channelsSelector = (state: RootState): IChannelsState => state.channels;
export const selectChannelList = createSelector([channelsSelector], (channels: IChannelsState) => channels.channels); export const selectChannelList = createSelector(
[channelsSelector],
(channels: IChannelsState) => channels.channels
);
export const selectChannelsLoading = createSelector([channelsSelector], (channels: IChannelsState) => channels.isLoading); export const selectChannelsLoading = createSelector(
[channelsSelector],
(channels: IChannelsState) => channels.isLoading
);
export const selectChannelsError = createSelector([channelsSelector], (channels: IChannelsState) => channels.error); export const selectChannelsError = createSelector(
[channelsSelector],
(channels: IChannelsState) => channels.error
);
export const selectSelectedChannel = createSelector([channelsSelector], (channels: IChannelsState) => channels.selectedChannel); export const selectSelectedChannel = createSelector(
[channelsSelector],
(channels: IChannelsState) => channels.selectedChannel
);
// Async thunks for channel operations // Async thunks for channel operations
export const listChannels = createAsyncThunk('channels/listChannels', async (_, {rejectWithValue}) => { export const listChannels = createAsyncThunk(
try { 'channels/listChannels',
const channels = await channelService.listChannels(); async (_, {rejectWithValue}) => {
return channels; try {
} catch (error) { const channels = await channelService.listChannels();
return rejectWithValue(error instanceof Error ? error.message : 'Failed to fetch channels'); return channels;
} catch (error) {
return rejectWithValue(error instanceof Error ? error.message : 'Failed to fetch channels');
}
} }
}); );
export const createChannelThunk = createAsyncThunk('channels/createChannel', async (params: CreateChannelParams, {rejectWithValue, dispatch}) => { export const createChannelThunk = createAsyncThunk(
try { 'channels/createChannel',
const newChannel = await channelService.createChannel(params); async (params: CreateChannelParams, {rejectWithValue, dispatch}) => {
// Refresh the channel list after creation try {
dispatch(listChannels()); const newChannel = await channelService.createChannel(params);
return newChannel; // Refresh the channel list after creation
} catch (error) { dispatch(listChannels());
return rejectWithValue(error instanceof Error ? error.message : 'Failed to create channel'); return newChannel;
} catch (error) {
return rejectWithValue(error instanceof Error ? error.message : 'Failed to create channel');
}
} }
}); );
export const deleteChannelThunk = createAsyncThunk('channels/deleteChannel', async (params: DeleteChannelParams, {rejectWithValue, dispatch}) => { export const deleteChannelThunk = createAsyncThunk(
try { 'channels/deleteChannel',
await channelService.deleteChannel(params); async (params: DeleteChannelParams, {rejectWithValue, dispatch}) => {
// Refresh the channel list after deletion try {
dispatch(listChannels()); await channelService.deleteChannel(params);
return params.channelId; // Refresh the channel list after deletion
} catch (error) { dispatch(listChannels());
return rejectWithValue(error instanceof Error ? error.message : 'Failed to delete channel'); return params.channelId;
} catch (error) {
return rejectWithValue(error instanceof Error ? error.message : 'Failed to delete channel');
}
} }
}); );
export const forkChannelThunk = createAsyncThunk('channels/forkChannel', async (params: ForkChannelParams, {rejectWithValue, dispatch}) => { export const forkChannelThunk = createAsyncThunk(
try { 'channels/forkChannel',
await channelService.forkChannel(params); async (params: ForkChannelParams, {rejectWithValue, dispatch}) => {
// Refresh the channel list after forking try {
dispatch(listChannels()); await channelService.forkChannel(params);
return params; // Refresh the channel list after forking
} catch (error) { dispatch(listChannels());
return rejectWithValue(error instanceof Error ? error.message : 'Failed to fork channel'); return params;
} catch (error) {
return rejectWithValue(error instanceof Error ? error.message : 'Failed to fork channel');
}
} }
}); );
export const killChannelThunk = createAsyncThunk('channels/killChannel', async (params: KillChannelParams, {rejectWithValue, dispatch}) => { export const killChannelThunk = createAsyncThunk(
try { 'channels/killChannel',
await channelService.killChannel(params); async (params: KillChannelParams, {rejectWithValue, dispatch}) => {
// Refresh the channel list after killing try {
dispatch(listChannels()); await channelService.killChannel(params);
return params.channelId; // Refresh the channel list after killing
} catch (error) { dispatch(listChannels());
return rejectWithValue(error instanceof Error ? error.message : 'Failed to kill channel'); return params.channelId;
} catch (error) {
return rejectWithValue(error instanceof Error ? error.message : 'Failed to kill channel');
}
} }
}); );
export const getChannelThunk = createAsyncThunk('channels/getChannel', async (channelId: string, {rejectWithValue}) => { export const getChannelThunk = createAsyncThunk(
try { 'channels/getChannel',
const channel = await channelService.getChannel(channelId); async (channelId: string, {rejectWithValue}) => {
return channel; try {
} catch (error) { const channel = await channelService.getChannel(channelId);
return rejectWithValue(error instanceof Error ? error.message : 'Failed to get channel'); return channel;
} catch (error) {
return rejectWithValue(error instanceof Error ? error.message : 'Failed to get channel');
}
} }
}); );
export const getPublisherCountThunk = createAsyncThunk('channels/getPublisherCount', async (channelId: string, {rejectWithValue}) => { export const getPublisherCountThunk = createAsyncThunk(
try { 'channels/getPublisherCount',
const count = await channelService.getPublisherCount(channelId); async (channelId: string, {rejectWithValue}) => {
return {channelId, count}; try {
} catch (error) { const count = await channelService.getPublisherCount(channelId);
return rejectWithValue(error instanceof Error ? error.message : 'Failed to get publisher count'); return {channelId, count};
} catch (error) {
return rejectWithValue(error instanceof Error ? error.message : 'Failed to get publisher count');
}
} }
}); );
// Export all actions and selectors // Export all actions and selectors
export * from '../slices/Channels.slice'; export * from '../slices/Channels.slice';

View File

@@ -10,7 +10,7 @@ export enum StoreScreensType {
ChannelDetail = 'channelDetail', ChannelDetail = 'channelDetail',
Settings = 'settings', Settings = 'settings',
Login = 'login', Login = 'login',
Channels = 'Channels' Channels = "Channels"
} }
// Screen state interface // Screen state interface
@@ -35,13 +35,25 @@ const initialState: ScreenState = {
// Selectors // Selectors
export const screensSelector = (state: RootState): ScreenState => state.screens; export const screensSelector = (state: RootState): ScreenState => state.screens;
export const selectCurrentScreen = createSelector([screensSelector], (screens: ScreenState) => screens.currentScreen); export const selectCurrentScreen = createSelector(
[screensSelector],
(screens: ScreenState) => screens.currentScreen
);
export const selectScreenProps = createSelector([screensSelector], (screens: ScreenState) => screens.screenProps); export const selectScreenProps = createSelector(
[screensSelector],
(screens: ScreenState) => screens.screenProps
);
export const selectPreviousScreen = createSelector([screensSelector], (screens: ScreenState) => screens.previousScreen); export const selectPreviousScreen = createSelector(
[screensSelector],
(screens: ScreenState) => screens.previousScreen
);
export const selectNavigationHistory = createSelector([screensSelector], (screens: ScreenState) => screens.navigationHistory); export const selectNavigationHistory = createSelector(
[screensSelector],
(screens: ScreenState) => screens.navigationHistory
);
// Slice // Slice
const screensSlice = createSlice({ const screensSlice = createSlice({
@@ -51,7 +63,7 @@ const screensSlice = createSlice({
setCurrentScreen: (state, action: PayloadAction<StoreScreensType>) => { setCurrentScreen: (state, action: PayloadAction<StoreScreensType>) => {
state.previousScreen = state.currentScreen; state.previousScreen = state.currentScreen;
state.currentScreen = action.payload; state.currentScreen = action.payload;
// Add to navigation history (keep last 10) // Add to navigation history (keep last 10)
state.navigationHistory.push(action.payload); state.navigationHistory.push(action.payload);
if (state.navigationHistory.length > 10) { if (state.navigationHistory.length > 10) {
@@ -70,28 +82,28 @@ const screensSlice = createSlice({
navigateToScreen: (state, action: PayloadAction<{screen: StoreScreensType; props?: ScreenProps}>) => { navigateToScreen: (state, action: PayloadAction<{screen: StoreScreensType; props?: ScreenProps}>) => {
state.previousScreen = state.currentScreen; state.previousScreen = state.currentScreen;
state.currentScreen = action.payload.screen; state.currentScreen = action.payload.screen;
if (action.payload.props) { if (action.payload.props) {
state.screenProps = action.payload.props; state.screenProps = action.payload.props;
} }
// Add to navigation history // Add to navigation history
state.navigationHistory.push(action.payload.screen); state.navigationHistory.push(action.payload.screen);
if (state.navigationHistory.length > 10) { if (state.navigationHistory.length > 10) {
state.navigationHistory.shift(); state.navigationHistory.shift();
} }
}, },
navigateBack: state => { navigateBack: (state) => {
if (state.previousScreen) { if (state.previousScreen) {
const temp = state.currentScreen; const temp = state.currentScreen;
state.currentScreen = state.previousScreen; state.currentScreen = state.previousScreen;
state.previousScreen = temp; state.previousScreen = temp;
} }
}, },
clearScreenProps: state => { clearScreenProps: (state) => {
state.screenProps = {}; state.screenProps = {};
}, },
resetNavigation: state => { resetNavigation: (state) => {
state.currentScreen = StoreScreensType.Login; state.currentScreen = StoreScreensType.Login;
state.previousScreen = null; state.previousScreen = null;
state.screenProps = {}; state.screenProps = {};
@@ -100,6 +112,14 @@ const screensSlice = createSlice({
} }
}); });
export const {setCurrentScreen, setScreenProps, updateScreenProps, navigateToScreen, navigateBack, clearScreenProps, resetNavigation} = screensSlice.actions; export const {
setCurrentScreen,
setScreenProps,
updateScreenProps,
navigateToScreen,
navigateBack,
clearScreenProps,
resetNavigation
} = screensSlice.actions;
export default screensSlice.reducer; export default screensSlice.reducer;

View File

@@ -56,13 +56,13 @@ export const authenticateRequestMiddleware: Middleware = store => next => async
try { try {
console.log('[authenticateRequest] Attempting auto-authentication'); console.log('[authenticateRequest] Attempting auto-authentication');
// Use the Redux thunk to properly update the state // Use the Redux thunk to properly update the state
const authResult = await store.dispatch(authenticateCredentialsThunk({applicationId, secret}) as any); const authResult = await store.dispatch(authenticateCredentialsThunk({ applicationId, secret }) as any);
if (authResult.type.endsWith('/rejected') || authResult.payload === 'Authentication failed') { if (authResult.type.endsWith('/rejected') || authResult.payload === 'Authentication failed') {
console.log('[authenticateRequest] Authentication failed'); console.log('[authenticateRequest] Authentication failed');
return next(setUnauthorized()); return next(setUnauthorized());
} }
console.log('[authenticateRequest] Auto-authentication successful, proceeding with action'); console.log('[authenticateRequest] Auto-authentication successful, proceeding with action');
return next(action); return next(action);
} catch (error) { } catch (error) {

View File

@@ -4,4 +4,4 @@
// Re-export from the action module for backwards compatibility // Re-export from the action module for backwards compatibility
export {StoreScreensType} from '../action/screens'; export {StoreScreensType} from '../action/screens';
export type {ScreenState, ScreenProps} from '../action/screens'; export type {ScreenState, ScreenProps} from '../action/screens';

View File

@@ -24,6 +24,7 @@ export const selectChannelList = createSelector([selectChannels], channels => ch
export const fetchChannelList = createAsyncThunk('channels/fetchChannelList', async (_, {rejectWithValue}) => { export const fetchChannelList = createAsyncThunk('channels/fetchChannelList', async (_, {rejectWithValue}) => {
try { try {
return PCastApiService.channels.list(); return PCastApiService.channels.list();
} catch (error) { } catch (error) {
return rejectWithValue(error); return rejectWithValue(error);
} }

View File

@@ -57,7 +57,7 @@ export class Theme {
} }
static get background() { static get background() {
return Theme; return Theme
} }
} }

View File

@@ -18,11 +18,9 @@ import {columns} from './columns-config';
import {CreateChannelModal} from './create-channel'; import {CreateChannelModal} from './create-channel';
const POLLING_INTERVAL = 5000; // 5 seconds const POLLING_INTERVAL = 5000; // 5 seconds
const ChannelListLoading = () => ( const ChannelListLoading = () => <Main>
<Main> <Loader />
<Loader /> </Main>
</Main>
);
export const ChannelList = (): React.JSX.Element => { export const ChannelList = (): React.JSX.Element => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
@@ -39,6 +37,7 @@ export const ChannelList = (): React.JSX.Element => {
// Memoized columns to prevent unnecessary re-renders // Memoized columns to prevent unnecessary re-renders
const channelsColumns = React.useMemo(() => ({...columns}), []); const channelsColumns = React.useMemo(() => ({...columns}), []);
// Load channels on component mount // Load channels on component mount
useEffect(() => { useEffect(() => {
dispatch(listChannels()); dispatch(listChannels());
@@ -65,17 +64,14 @@ export const ChannelList = (): React.JSX.Element => {
}, [dispatch, channels.length, isFetching]); }, [dispatch, channels.length, isFetching]);
// Memoized screen header to prevent unnecessary re-renders // Memoized screen header to prevent unnecessary re-renders
const screenHeader: ITableWithPaginationHeader = React.useMemo( const screenHeader: ITableWithPaginationHeader = React.useMemo(() => ({
() => ({ [TableHeaderKey.Search]: {},
[TableHeaderKey.Search]: {}, [TableHeaderKey.AddRow]: {
[TableHeaderKey.AddRow]: { openAddRowModal: () => {
openAddRowModal: () => { setCreateChannelModalOpened(true);
setCreateChannelModalOpened(true);
}
} }
}), }
[] }), []);
);
// Callback for handling search and sort changes (no-op since TableWithPagination handles internally) // Callback for handling search and sort changes (no-op since TableWithPagination handles internally)
const changeScreenProps = useCallback((_data: Partial<ITableSortSearch>) => { const changeScreenProps = useCallback((_data: Partial<ITableSortSearch>) => {
@@ -94,7 +90,10 @@ export const ChannelList = (): React.JSX.Element => {
<Body className="table-container"> <Body className="table-container">
<Error> <Error>
{(channelListErrorMessages as Record<string, string>)[error] || error} {(channelListErrorMessages as Record<string, string>)[error] || error}
<button onClick={() => dispatch(listChannels())} style={{marginLeft: '10px', padding: '5px 10px'}}> <button
onClick={() => dispatch(listChannels())}
style={{ marginLeft: '10px', padding: '5px 10px' }}
>
Retry Retry
</button> </button>
</Error> </Error>
@@ -105,31 +104,37 @@ export const ChannelList = (): React.JSX.Element => {
return ( return (
<div> <div>
<Body className="table-container"> <Body className="table-container">
{isFetching ? ( {isFetching ? <ChannelListLoading /> : channels.length === 0 ? (
<ChannelListLoading /> <Main>
) : channels.length === 0 ? ( <div style={{ textAlign: 'center', padding: '2rem' }}>
<Main> <h3>No channels found</h3>
<div style={{textAlign: 'center', padding: '2rem'}}> <p>Get started by creating your first channel.</p>
<h3>No channels found</h3> <button
<p>Get started by creating your first channel.</p> onClick={() => setCreateChannelModalOpened(true)}
<button onClick={() => setCreateChannelModalOpened(true)} style={{padding: '10px 20px', marginTop: '1rem'}}> style={{ padding: '10px 20px', marginTop: '1rem' }}
Create Channel >
</button> Create Channel
</div> </button>
</Main> </div>
) : ( </Main>
<TableWithPagination ) : (
title="Channels" <TableWithPagination
screenHeader={screenHeader} title="Channels"
columns={channelsColumns} screenHeader={screenHeader}
data={channels as any[]} columns={channelsColumns}
paginationItemText="channels" data={channels as any[]}
changeSortProps={changeScreenProps} paginationItemText="channels"
changeSearch={changeScreenProps} changeSortProps={changeScreenProps}
/> changeSearch={changeScreenProps}
)} />
)}
</Body> </Body>
{isCreateChannelModalOpened && <CreateChannelModal getChannelList={refreshChannelList} setCreateChannelModalOpened={setCreateChannelModalOpened} />} {isCreateChannelModalOpened && (
<CreateChannelModal
getChannelList={refreshChannelList}
setCreateChannelModalOpened={setCreateChannelModalOpened}
/>
)}
</div> </div>
); );
}; };