Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ test('all participant modes are properly listed and in the correct order', () =>
sid={'4b038eda-ddca-5533-9a49-3a34f133b5f4'}
clusterId={'test-cluster'}
participantModes={['moderator', 'peer', 'observer']}
showCTA={false}
/>
);

Expand All @@ -50,3 +51,28 @@ test('all participant modes are properly listed and in the correct order', () =>
expect(menuItems[1].innerHTML).toBe('moderator');
expect(menuItems[2].innerHTML).toBe('peer');
});

test('all possible participant modes are properly listed in the CTA without join links', () => {
render(
<SessionJoinBtn
sid={'4b038eda-ddca-5533-9a49-3a34f133b5f4'}
clusterId={'test-cluster'}
participantModes={['moderator', 'peer', 'observer']}
showCTA={true}
/>
);

const joinBtn = screen.queryByText(/Join/i);
expect(joinBtn).toBeInTheDocument();

fireEvent.click(joinBtn);

// Make sure that no link to join session is available when showCTA = true.
const menuItems = screen.queryByRole<HTMLAnchorElement>('link');
expect(menuItems).not.toBeInTheDocument();

const cta = screen.queryByText(
'Join Active Sessions with Teleport Enterprise'
);
expect(cta).toBeInTheDocument();
});
153 changes: 134 additions & 19 deletions web/packages/teleport/src/Sessions/SessionList/SessionJoinBtn.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,21 +15,25 @@ limitations under the License.
*/

import React, { useState } from 'react';
import styled from 'styled-components';

import { ButtonBorder, Text, Box, Menu, MenuItem } from 'design';
import { ButtonBorder, Text, Box, Menu, MenuItem, Flex } from 'design';
import { CarrotDown } from 'design/Icon';

import cfg from 'teleport/config';
import { ParticipantMode } from 'teleport/services/session';
import { ButtonLockedFeature } from 'teleport/components/ButtonLockedFeature';

export const SessionJoinBtn = ({
sid,
clusterId,
participantModes,
showCTA,
}: {
sid: string;
clusterId: string;
participantModes: ParticipantMode[];
showCTA: boolean;
}) => {
// Sorts the list of participantModes so that they are consistently shown in the order of "observer" -> "moderator" -> "peer"
const modes = {
Expand All @@ -41,6 +45,10 @@ export const SessionJoinBtn = ({
(a, b) => modes[a] - modes[b]
);

if (showCTA) {
return <LockedFeatureJoinMenu modes={sortedParticipantModes} />;
}

return (
<JoinMenu>
{sortedParticipantModes.map(participantMode => (
Expand All @@ -61,7 +69,7 @@ export const SessionJoinBtn = ({
function JoinMenu({ children }: { children: React.ReactNode }) {
const [anchorEl, setAnchorEl] = useState<HTMLElement>(null);

const handleClickListItem = event => {
const handleClickListItem = (event: React.MouseEvent<HTMLElement>) => {
setAnchorEl(event.currentTarget);
};

Expand All @@ -75,24 +83,131 @@ function JoinMenu({ children }: { children: React.ReactNode }) {
Join
<CarrotDown ml={1} fontSize={2} color="text.secondary" />
</ButtonBorder>
<Menu
anchorOrigin={{
vertical: 'bottom',
horizontal: 'center',
}}
transformOrigin={{
vertical: 'top',
horizontal: 'center',
}}
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={handleClose}
>
<Text px="2" fontSize="11px" color="grey.400" bg="subtle">
Join as...
</Text>
<InternalJoinMenu anchorEl={anchorEl} handleClose={handleClose}>
{children}
</Menu>
</InternalJoinMenu>
</Box>
);
}

type LockedFeatureJoinMenu = {
modes: ParticipantMode[];
};
function LockedFeatureJoinMenu({ modes }: LockedFeatureJoinMenu) {
const [anchorEl, setAnchorEl] = useState<HTMLElement>(null);

const handleClickListItem = (event: React.MouseEvent<HTMLElement>) => {
setAnchorEl(event.currentTarget);
};

const handleClose = () => {
setAnchorEl(null);
};

return (
<Box textAlign="center" width="80px">
<ButtonBorder size="small" onClick={handleClickListItem}>
Join
<CarrotDown ml={1} fontSize={2} color="text.secondary" />
</ButtonBorder>
<LockedFeatureInternalJoinMenu
anchorEl={anchorEl}
handleClose={handleClose}
modes={modes}
/>
</Box>
);
}

type InternalJoinMenuProps = {
anchorEl: HTMLElement;
handleClose: () => void;
children: React.ReactNode;
};
function InternalJoinMenu({
anchorEl,
handleClose,
children,
}: InternalJoinMenuProps) {
return (
<Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleClose}>
<Text px="2" fontSize="11px" color="grey.400" bg="subtle">
Join as...
</Text>
{children}
</Menu>
);
}

type LockedFeatureInternalJoinMenuProps = {
anchorEl: HTMLElement;
handleClose: () => void;
modes: ParticipantMode[];
};
function LockedFeatureInternalJoinMenu({
anchorEl,
handleClose,
modes,
}: LockedFeatureInternalJoinMenuProps) {
return (
<Menu
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={handleClose}
menuListCss={() => {
return { backgroundColor: '#222c59' };
}}
>
<div></div>
<LockedJoinMenuContainer>
<ButtonLockedFeature>
Join Active Sessions with Teleport Enterprise
</ButtonLockedFeature>
<Box ml="3">
{modes.includes('observer') ? (
<LockedJoinItem
name={'As an Observer'}
info={'Watch: cannot control any part of the session'}
/>
) : null}

{modes.includes('moderator') ? (
<LockedJoinItem
name={'As a Moderator'}
info={'Review: can view output & terminate the session'}
/>
) : null}

{modes.includes('peer') ? (
<LockedJoinItem
name={'As a Peer'}
info={'Collaborate: can view output and send input'}
/>
) : null}
</Box>
</LockedJoinMenuContainer>
</Menu>
);
}

const LockedJoinMenuContainer = styled(Flex)`
display: flex;
flex-direction: column;
align-items: flex-start;
padding: 16px 12px;
gap: 12px;
background-color: #222c59;
`;

type LockedJoinItemProps = {
name: string;
info: string;
};
function LockedJoinItem({ name, info }: LockedJoinItemProps) {
return (
<Box mb="3">
<Text fontSize="16px">{name}</Text>
<Text fontSize="14px">{info}</Text>
</Box>
);
}
11 changes: 8 additions & 3 deletions web/packages/teleport/src/Sessions/SessionList/SessionList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import { Participant, Session, SessionKind } from 'teleport/services/session';
import { SessionJoinBtn } from './SessionJoinBtn';

export default function SessionList(props: Props) {
const { sessions, pageSize = 100 } = props;
const { sessions, pageSize = 100, showActiveSessionsCTA } = props;

return (
<StyledTable
Expand Down Expand Up @@ -59,7 +59,8 @@ export default function SessionList(props: Props) {
},
{
altKey: 'join-btn',
render: renderJoinCell,
render: session =>
renderJoinCell({ ...session, showActiveSessionsCTA }),
},
]}
emptyText="No Active Sessions Found"
Expand Down Expand Up @@ -102,12 +103,14 @@ const renderIconCell = (kind: SessionKind) => {
);
};

type renderJoinCellProps = Session & { showActiveSessionsCTA: boolean };
const renderJoinCell = ({
sid,
clusterId,
kind,
participantModes,
}: Session) => {
showActiveSessionsCTA,
}: renderJoinCellProps) => {
const { joinable } = kinds[kind];
if (!joinable || participantModes.length === 0) {
return <Cell align="right" height="26px" />;
Expand All @@ -119,6 +122,7 @@ const renderJoinCell = ({
sid={sid}
clusterId={clusterId}
participantModes={participantModes}
showCTA={showActiveSessionsCTA}
/>
</Cell>
);
Expand All @@ -132,6 +136,7 @@ function renderUsersCell({ parties }: Session) {
type Props = {
sessions: Session[];
pageSize?: number;
showActiveSessionsCTA: boolean;
};

function participantMatcher(
Expand Down
7 changes: 6 additions & 1 deletion web/packages/teleport/src/Sessions/Sessions.story.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,14 @@ import React from 'react';

import { render } from 'design/utils/testing';

import { Loaded } from './Sessions.story';
import { Loaded, LoadedWithCTA } from './Sessions.story';

test('loaded', () => {
const { container } = render(<Loaded />);
expect(container.firstChild).toMatchSnapshot();
});

test('loaded with CTA', () => {
const { container } = render(<LoadedWithCTA />);
expect(container.firstChild).toMatchSnapshot();
});
16 changes: 16 additions & 0 deletions web/packages/teleport/src/Sessions/Sessions.story.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,22 @@ export function Loaded() {
isFailed: false,
message: '',
},
showActiveSessionsCTA: false,
};

return <Sessions {...props} />;
}

export function LoadedWithCTA() {
const props = {
sessions,
attempt: {
isSuccess: true,
isProcessing: false,
isFailed: false,
message: '',
},
showActiveSessionsCTA: true,
};

return <Sessions {...props} />;
Expand Down
20 changes: 17 additions & 3 deletions web/packages/teleport/src/Sessions/Sessions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ import useTeleport from 'teleport/useTeleport';

import useStickerClusterId from 'teleport/useStickyClusterId';

import { ButtonLockedFeature } from 'teleport/components/ButtonLockedFeature';

import SessionList from './SessionList';
import useSessions from './useSessions';

Expand All @@ -38,19 +40,31 @@ export default function Container() {
}

export function Sessions(props: ReturnType<typeof useSessions>) {
const { attempt, sessions } = props;
const { attempt, sessions, showActiveSessionsCTA } = props;
return (
<FeatureBox>
<FeatureHeader alignItems="center">
<FeatureHeader alignItems="center" justifyContent="space-between">
<FeatureHeaderTitle>Active Sessions</FeatureHeaderTitle>
{showActiveSessionsCTA && (
<Box width="340px">
<ButtonLockedFeature height="36px">
Join Active Sessions With Teleport Enterprise
</ButtonLockedFeature>
</Box>
)}
</FeatureHeader>
{attempt.isFailed && <Danger>{attempt.message} </Danger>}
{attempt.isProcessing && (
<Box textAlign="center" m={10}>
<Indicator />
</Box>
)}
{attempt.isSuccess && <SessionList sessions={sessions} />}
{attempt.isSuccess && (
<SessionList
sessions={sessions}
showActiveSessionsCTA={showActiveSessionsCTA}
/>
)}
</FeatureBox>
);
}
Loading