Skip to content

ODC-7802: Remove custom Drawer and Tabs from web-terminal #15077

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
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
3 changes: 0 additions & 3 deletions frontend/package.json
Original file line number Diff line number Diff line change
@@ -192,7 +192,6 @@
"react-dnd": "^11.1.3",
"react-dnd-html5-backend": "^11.1.3",
"react-dom": "^17.0.1",
"react-draggable": "4.x",
"react-helmet-async": "^2.0.5",
"react-i18next": "^11.12.0",
"react-linkify": "^0.2.2",
@@ -204,7 +203,6 @@
"react-router-hash-link": "^2.0.0",
"react-svg": "^16.2.0",
"react-tagsinput": "3.20.x",
"react-transition-group": "2.3.x",
"react-virtualized": "9.x",
"redux": "4.0.1",
"redux-thunk": "2.4.0",
@@ -248,7 +246,6 @@
"@types/react-jsonschema-form": "^1.3.8",
"@types/react-router": "^5.1.20",
"@types/react-router-dom": "5.3.x",
"@types/react-transition-group": "2.x",
"@types/react-virtualized": "9.x",
"@types/semver": "^6.0.0",
"@types/showdown": "1.9.4",
479 changes: 0 additions & 479 deletions frontend/packages/console-app/src/components/tabs/Tabs.tsx

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

1 change: 0 additions & 1 deletion frontend/packages/console-app/src/components/tabs/index.ts

This file was deleted.

This file was deleted.

This file was deleted.

53 changes: 0 additions & 53 deletions frontend/packages/console-shared/src/components/drawer/Drawer.scss

This file was deleted.

147 changes: 0 additions & 147 deletions frontend/packages/console-shared/src/components/drawer/Drawer.tsx

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

1 change: 0 additions & 1 deletion frontend/packages/console-shared/src/components/index.ts
Original file line number Diff line number Diff line change
@@ -10,7 +10,6 @@ export * from './status';
export * from './pod';
export * from './popper';
export * from './shortcuts';
export * from './drawer';
export * from './health-checks';
export * from './virtualized-grid';
export * from './alerts';
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
export const webTerminalPO = {
webTerminalIcon: '[data-tour-id="tour-cloud-shell-button"]',
addTerminalIcon: '[data-test="add-terminal-icon"]',
closeTerminalIcon: '[data-test="close-terminal-icon"]',
addTerminalIcon: '[data-test="multi-tab-terminal"] [aria-label="Add new tab"]',
closeTerminalIcon: '[aria-label="Close terminal tab"]',
tabsList: '[data-test="multi-tab-terminal"] ul',
openCommandLine: 'button[data-tour-id="tour-cloud-shell-button"]',
terminalWindow: 'canvas.xterm-cursor-layer',
terminalWindowWithEnabledMouseEvent: 'div.xterm-screen>canvas.xterm-cursor-layer',
terminalOpenInNewTabBtn: "a[href='/terminal']",
terminalCloseWindowBtn: "button[data-test='close-terminal-icon']",
terminalCloseWindowBtn: "button[aria-label='Close terminal'], [aria-label='Close terminal tab']",
terminalInnactivityMessageArea: 'div.co-cloudshell-exec__error-msg',
createProjectMenu: {
createProjectDropdownMenu: '[data-test-id="namespace-bar-dropdown"] [type="button"]',
Original file line number Diff line number Diff line change
@@ -106,7 +106,7 @@ export const catalogPage = {
cy.get(catalogPO.installHelmChart.releaseName).clear().type(releaseName),
selectCardInCatalog: (card: catalogCards | string) => {
cy.get('.skeleton-catalog--grid').should('not.exist');
cy.byLegacyTestID('perspective-switcher-toggle').click();
cy.byLegacyTestID('perspective-switcher-toggle').click({ force: true });
switch (card) {
case catalogCards.mariaDB || 'MariaDB': {
cy.get(catalogPO.cards.mariaDBTemplate).first().click();
Original file line number Diff line number Diff line change
@@ -7,7 +7,7 @@ export const samplesPage = {
},
selectCardInSamples: (card: string) => {
cy.get('.skeleton-catalog--grid').should('not.exist');
cy.byLegacyTestID('perspective-switcher-toggle').click();
cy.byLegacyTestID('perspective-switcher-toggle').click({ force: true });
switch (card) {
case 'Httpd': {
cy.get(samplesPO.cards.httpdTemplate).first().click();
Original file line number Diff line number Diff line change
@@ -6,12 +6,12 @@ export const checkDeveloperPerspective = () => {
cy.byLegacyTestID('perspective-switcher-toggle').then(($switcher) => {
// switcher is present
if ($switcher.attr('aria-hidden') !== 'true') {
cy.byLegacyTestID('perspective-switcher-toggle').click();
cy.byLegacyTestID('perspective-switcher-toggle').click({ force: true });

if ($body.find('[data-test-id="perspective-switcher-menu-option"]').length !== 0) {
cy.log('perspective switcher menu enabled');
cy.byLegacyTestID('perspective-switcher-menu-option').contains('Developer');
cy.byLegacyTestID('perspective-switcher-toggle').click();
cy.byLegacyTestID('perspective-switcher-toggle').click({ force: true });
return;
}
}
6 changes: 3 additions & 3 deletions frontend/packages/integration-tests-cypress/views/nav.ts
Original file line number Diff line number Diff line change
@@ -39,7 +39,7 @@ export const nav = {
.contains(newPerspective);
} else {
cy.byLegacyTestID('perspective-switcher-toggle')
.click()
.click({ force: true })
.byLegacyTestID('perspective-switcher-menu-option')
.contains(newPerspective)
.click({ force: true });
@@ -61,7 +61,7 @@ export const nav = {
} else {
checkDeveloperPerspective();
cy.byLegacyTestID('perspective-switcher-toggle')
.click()
.click({ force: true })
.byLegacyTestID('perspective-switcher-menu-option')
.contains(newPerspective)
.click({ force: true });
@@ -70,7 +70,7 @@ export const nav = {
break;
default:
cy.byLegacyTestID('perspective-switcher-toggle')
.click()
.click({ force: true })
.byLegacyTestID('perspective-switcher-menu-option')
.contains(newPerspective)
.click({ force: true });
Original file line number Diff line number Diff line change
@@ -31,7 +31,7 @@ export const initTerminalPage = {
if ($body.find('[data-test="loading-box-body"]').length === 0) {
cy.log('loading did not go through');
cy.wait(10000);
cy.get(webTerminalPO.terminalCloseWindowBtn).click();
cy.get(webTerminalPO.closeTerminalIcon).click();
cy.reload();
app.waitForDocumentLoad();
perspective.switchTo(switchPerspective.Administrator);
Original file line number Diff line number Diff line change
@@ -46,7 +46,7 @@ Given('user can see terminal icon on masthead', () => {
});

When('user clicks on the Web Terminal icon on the Masthead', () => {
cy.get(webTerminalPO.webTerminalIcon).click();
cy.get(webTerminalPO.webTerminalIcon).click({ force: true });
cy.get('cos-status-box cos-status-box--loading').should('not.exist');
});

@@ -76,7 +76,7 @@ When('user closed web terminal window', () => {

Then('user is able see {int} web terminal tabs', (n: number) => {
cy.get(webTerminalPO.tabsList).then(($el) => {
expect($el.prop('children').length).toEqual(n + 1);
expect($el.prop('children').length).toEqual(n);
});
});

Original file line number Diff line number Diff line change
@@ -20,9 +20,9 @@
"OpenShift command line": "OpenShift command line",
"Command line terminal": "Command line terminal",
"Failed to connect to your OpenShift command line terminal": "Failed to connect to your OpenShift command line terminal",
"Terminal {{number}}": "Terminal {{number}}",
"Close terminal tab": "Close terminal tab",
"Add new tab": "Add new tab",
"Close terminal tab": "Close terminal tab",
"Terminal {{number}}": "Terminal {{number}}",
"Project": "Project",
"This Project will be used to initialize your command line terminal": "This Project will be used to initialize your command line terminal",
"Initialize terminal": "Initialize terminal",
Original file line number Diff line number Diff line change
@@ -6,7 +6,6 @@ import { FLAG_DEVWORKSPACE } from '../../const';
import { toggleCloudShellExpanded } from '../../redux/actions/cloud-shell-actions';
import { isCloudShellExpanded } from '../../redux/reducers/cloud-shell-selectors';
import CloudShellDrawer from './CloudShellDrawer';
import MultiTabTerminal from './MultiTabbedTerminal';

type StateProps = {
open: boolean;
@@ -18,15 +17,15 @@ type DispatchProps = {

type CloudShellProps = WithFlagsProps & StateProps & DispatchProps;

const CloudShell: React.FC<CloudShellProps> = ({ flags, open, onClose }) => {
const CloudShell: React.FC<CloudShellProps> = ({ flags, open, onClose, children }) => {
if (!flags[FLAG_DEVWORKSPACE]) {
return null;
return <>{children}</>;
}
return open ? (
<CloudShellDrawer onClose={onClose}>
<MultiTabTerminal onClose={onClose} />
return (
<CloudShellDrawer onClose={onClose} open={open}>
{children}
</CloudShellDrawer>
) : null;
);
};

const stateToProps = (state: RootState): StateProps => ({
Original file line number Diff line number Diff line change
@@ -1,14 +1,25 @@
@use '../../../../../public/style/vars';

.co-cloud-shell-drawer {
&__heading {
padding-left: var(--pf-t--global--spacer--md);
&__header {
background-color: var(--pf-t--global--background--color--secondary--default);
}
&__body {
display: flex;
flex-direction: column;
overflow: hidden;
height: 100%;
// since we removed the gap and padding from the drawer header, the height
// is exactly the height of the buttons
--co-cloud-shell-header-height: #{vars.$co-button-height};

&-collapsed .pf-v6-c-drawer__panel-main {
overflow-y: hidden;
}
&.pf-v6-c-drawer__panel {
gap: 0;
}
& .pf-v6-c-tabs {
flex-shrink: 0;
}
& .pf-v6-c-drawer__panel-main {
padding: 0;
}
}
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,30 @@
import * as React from 'react';
import { Tooltip, Flex, FlexItem, Button } from '@patternfly/react-core';
import { ExternalLinkAltIcon } from '@patternfly/react-icons/dist/esm/icons/external-link-alt-icon';
import {
Drawer,
DrawerActions,
DrawerCloseButton,
DrawerContent,
DrawerContentBody,
DrawerHead,
DrawerPanelContent,
Flex,
FlexItem,
Tooltip,
} from '@patternfly/react-core';
import { css } from '@patternfly/react-styles';
import { c_drawer_m_inline_m_panel_bottom__splitter_Height as pfSplitterHeight } from '@patternfly/react-tokens/dist/esm/c_drawer_m_inline_m_panel_bottom__splitter_Height';
import { useTranslation } from 'react-i18next';
import CloseButton from '@console/shared/src/components/close-button';
import Drawer from '@console/shared/src/components/drawer/Drawer';
import { ExternalLinkButton } from '@console/shared/src/components/links/ExternalLinkButton';
import { useTelemetry } from '@console/shared/src/hooks/useTelemetry';
import MinimizeRestoreButton from './MinimizeRestoreButton';
import { MinimizeRestoreButton } from '@console/webterminal-plugin/src/components/cloud-shell/MinimizeRestoreButton';
import { MultiTabbedTerminal } from '@console/webterminal-plugin/src/components/cloud-shell/MultiTabbedTerminal';

import './CloudShellDrawer.scss';

type CloudShellDrawerProps = {
open?: boolean;
onClose: () => void;
TerminalBody?: React.FC<{ onClose: () => void }>;
};

const getMastheadHeight = (): number => {
@@ -20,9 +34,17 @@ const getMastheadHeight = (): number => {
return height;
};

const CloudShellDrawer: React.FC<CloudShellDrawerProps> = ({ children, onClose }) => {
const HEADER_HEIGHT = `calc(${pfSplitterHeight.var} + var(--co-cloud-shell-header-height))`;

const CloudShellDrawer: React.FC<CloudShellDrawerProps> = ({
open = true,
onClose,
TerminalBody = MultiTabbedTerminal,
children,
}) => {
const [expanded, setExpanded] = React.useState<boolean>(true);
const { t } = useTranslation();
const [height, setHeight] = React.useState<number>(385);
const { t } = useTranslation('webterminal-plugin');
const fireTelemetryEvent = useTelemetry();

const onMRButtonClick = (expandedState: boolean) => {
@@ -31,51 +53,64 @@ const CloudShellDrawer: React.FC<CloudShellDrawerProps> = ({ children, onClose }
minimized: expandedState,
});
};
const handleChange = (openState: boolean) => {
setExpanded(openState);
};
const header = (
<Flex style={{ flexGrow: 1 }} data-test="cloudshell-drawer-header">
<FlexItem className="co-cloud-shell-drawer__heading">
{t('webterminal-plugin~OpenShift command line terminal')}
</FlexItem>
<FlexItem align={{ default: 'alignRight' }}>
<Tooltip content={t('webterminal-plugin~Open terminal in new tab')}>
<Button
icon={<ExternalLinkAltIcon />}
variant="plain"
component="a"
href="/terminal"
target="_blank"
aria-label={t('webterminal-plugin~Open terminal in new tab')}
/>
</Tooltip>
<MinimizeRestoreButton
minimize={expanded}
minimizeText={t('webterminal-plugin~Minimize terminal')}
restoreText={t('webterminal-plugin~Restore terminal')}
onClick={onMRButtonClick}
/>
<Tooltip content={t('webterminal-plugin~Close terminal')}>
<CloseButton
ariaLabel={t('webterminal-plugin~Close terminal')}
onClick={onClose}
data-test="cloudshell-drawer-close-button"
/>
</Tooltip>
</FlexItem>
</Flex>

const panelContent = (
<DrawerPanelContent
className={css('co-cloud-shell-drawer__body', 'pf-v6-u-p-0', {
'co-cloud-shell-drawer__body-collapsed': !expanded,
})}
isResizable
onResize={(_, w) => {
setExpanded(w > 47); // 47px is an arbitrary computed value of HEADER_HEIGHT.
setHeight(w);
}}
defaultSize={expanded ? `${height}px` : '0px'}
minSize={HEADER_HEIGHT}
maxSize={`calc(100vh - ${getMastheadHeight()}px)`}
>
<DrawerHead className="co-cloud-shell-drawer__header pf-v6-u-p-0">
<Flex grow={{ default: 'grow' }} data-test="cloudshell-drawer-header">
<FlexItem className="pf-v6-u-px-sm">{t('OpenShift command line terminal')}</FlexItem>
<FlexItem align={{ default: 'alignRight' }}>
<DrawerActions className="pf-v6-u-m-0">
<Tooltip content={t('Open terminal in new tab')}>
<ExternalLinkButton
variant="plain"
href="/terminal"
aria-label={t('Open terminal in new tab')}
iconProps={{ title: undefined }} // aria-label is sufficient
/>
</Tooltip>
<MinimizeRestoreButton
minimize={expanded}
minimizeText={t('Minimize terminal')}
restoreText={t('Restore terminal')}
onClick={onMRButtonClick}
// By design, PatternFly's drawers are full-height and non-resizable on < md viewports.
// When the viewport shrinks to below md, the drawer's height is set to 100vh by PF.
// We can't override this. The best we can do is hide the button.
className="pf-v6-u-display-none pf-v6-u-display-block-on-md"
/>
<Tooltip content={t('Close terminal')}>
<DrawerCloseButton
aria-label={t('Close terminal')}
onClose={onClose}
data-test="cloudshell-drawer-close-button"
/>
</Tooltip>
</DrawerActions>
</FlexItem>
</Flex>
</DrawerHead>
<TerminalBody onClose={onClose} />
</DrawerPanelContent>
);

return (
<Drawer
open={expanded}
defaultHeight={385}
header={header}
maxHeight={`calc(100vh - ${getMastheadHeight()}px)`}
onChange={handleChange}
resizable
>
<div className="co-cloud-shell-drawer__body">{children}</div>
<Drawer isInline isExpanded={open} position="bottom" id="co-cloud-shell-drawer">
<DrawerContent panelContent={panelContent}>
<DrawerContentBody>{children}</DrawerContentBody>
</DrawerContent>
</Drawer>
);
};
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as React from 'react';
import { Button } from '@patternfly/react-core';
import { Button, Tooltip } from '@patternfly/react-core';
import { TerminalIcon } from '@patternfly/react-icons/dist/esm/icons/terminal-icon';
import { css } from '@patternfly/react-styles';
import { useTranslation } from 'react-i18next';
@@ -24,7 +24,7 @@ const ClouldShellMastheadButton: React.FC<Props> = ({ onClick, open }) => {
const terminalAvailable = useCloudShellAvailable();
const fireTelemetryEvent = useTelemetry();

const { t } = useTranslation();
const { t } = useTranslation('webterminal-plugin');

if (!terminalAvailable) {
return null;
@@ -36,15 +36,17 @@ const ClouldShellMastheadButton: React.FC<Props> = ({ onClick, open }) => {
};

return (
<Button
icon={<TerminalIcon />}
variant="plain"
aria-label={t('webterminal-plugin~Command line terminal')}
onClick={openCloudshell}
className={css({ 'pf-m-selected': open }, 'co-masthead-button')}
data-tour-id="tour-cloud-shell-button"
data-quickstart-id="qs-masthead-cloudshell"
/>
<Tooltip content={t('OpenShift command line')} position="bottom" enableFlip>
<Button
icon={<TerminalIcon />}
variant="plain"
aria-label={t('Command line terminal')}
onClick={openCloudshell}
className={css({ 'pf-m-selected': open }, 'co-masthead-button')}
data-tour-id="tour-cloud-shell-button"
data-quickstart-id="qs-masthead-cloudshell"
/>
</Tooltip>
);
};

Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
@use '../../../../../public/style/vars';

.co-cloud-shell-tab {
&__body {
display: flex;
@@ -12,9 +14,6 @@
align-items: center;
display: flex;
flex-shrink: 0;
min-height: var(--pf-v6-global--target-size--MinHeight);
&-text {
padding: 0 var(--pf-t--global--spacer--md);
}
min-height: #{vars.$co-button-height};
}
}
Original file line number Diff line number Diff line change
@@ -3,25 +3,23 @@ import { useTranslation } from 'react-i18next';
import { Navigate } from 'react-router-dom-v5-compat';
import { useFlag } from '@console/shared';
import { FLAG_DEVWORKSPACE } from '../../const';
import MultiTabTerminal from './MultiTabbedTerminal';
import { MultiTabbedTerminal } from './MultiTabbedTerminal';

import './CloudShellTab.scss';

const CloudShellTab: React.FC = () => {
const { t } = useTranslation();
const { t } = useTranslation('webterminal-plugin');
const devWorkspaceFlag = useFlag(FLAG_DEVWORKSPACE);

if (devWorkspaceFlag === false) return <Navigate to="/" replace />;

return (
<>
<div className="co-cloud-shell-tab__header">
<div className="co-cloud-shell-tab__header-text">
{t('webterminal-plugin~OpenShift command line terminal')}
</div>
<div className="pf-v6-u-px-sm">{t('OpenShift command line terminal')}</div>
</div>
<div className="co-cloud-shell-tab__body">
<MultiTabTerminal />
<MultiTabbedTerminal />
</div>
</>
);
Original file line number Diff line number Diff line change
@@ -33,17 +33,17 @@ type StateProps = {
user: UserInfo;
};

type Props = {
export type CloudShellTerminalProps = {
onCancel?: () => void;
terminalNumber?: number;
setWorkspaceName?: (name: string, terminalNumber: number) => void;
setWorkspaceNamespace?: (namespace: string, terminalNumber: number) => void;
};

type CloudShellTerminalProps = StateProps & Props;
type CloudShellTerminalInternalProps = StateProps & CloudShellTerminalProps;

const CloudShellTerminal: React.FC<
CloudShellTerminalProps & WithUserSettingsCompatibilityProps<string>
CloudShellTerminalInternalProps & WithUserSettingsCompatibilityProps<string>
> = ({
user,
onCancel,
@@ -261,7 +261,7 @@ const stateToProps = (state: RootState): StateProps => ({
user: getUser(state),
});

export default connect<StateProps, null, Props>(stateToProps)(
export default connect<StateProps, null, CloudShellTerminalProps>(stateToProps)(
withUserSettingsCompatibility<
CloudShellTerminalProps & WithUserSettingsCompatibilityProps<string>,
string
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
import * as React from 'react';
import { Tooltip, Button } from '@patternfly/react-core';
import { Tooltip, Button, ButtonProps } from '@patternfly/react-core';
import { OutlinedWindowMinimizeIcon } from '@patternfly/react-icons/dist/esm/icons/outlined-window-minimize-icon';
import { OutlinedWindowRestoreIcon } from '@patternfly/react-icons/dist/esm/icons/outlined-window-restore-icon';

type MinimizeRestoreButtonProps = {
type MinimizeRestoreButtonProps = Omit<ButtonProps, 'onClick'> & {
minimizeText: string;
restoreText: string;
minimize?: boolean;
onClick: (minimized: boolean) => void;
};

const MinimizeRestoreButton: React.FC<MinimizeRestoreButtonProps> = ({
export const MinimizeRestoreButton: React.FC<MinimizeRestoreButtonProps> = ({
minimizeText,
restoreText,
minimize = true,
onClick,
...props
}) => {
const onMinimize = () => {
onClick(true);
@@ -34,9 +35,8 @@ const MinimizeRestoreButton: React.FC<MinimizeRestoreButtonProps> = ({
onClick={minimize ? onMinimize : onRestore}
aria-label={minimize ? minimizeText : restoreText}
isInline
{...props}
/>
</Tooltip>
);
};

export default MinimizeRestoreButton;
Original file line number Diff line number Diff line change
@@ -1,27 +1,28 @@
import * as React from 'react';
import { Button, Tab, TabTitleText, TabTitleIcon } from '@patternfly/react-core';
import { CloseIcon } from '@patternfly/react-icons/dist/esm/icons/close-icon';
import { PlusIcon } from '@patternfly/react-icons/dist/esm/icons/plus-icon';
import { Tabs, Tab } from '@patternfly/react-core';
import { useTranslation } from 'react-i18next';
import { Tabs } from '@console/app/src/components/tabs';
import { useTelemetry } from '@console/shared/src/hooks/useTelemetry';
import { sendActivityTick } from './cloud-shell-utils';
import CloudShellTerminal from './CloudShellTerminal';
import CloudShellTerminal, { CloudShellTerminalProps } from './CloudShellTerminal';
import { TICK_INTERVAL } from './useActivityTick';
import './MultiTabbedTerminal.scss';

const MAX_TERMINAL_TABS = 8;

interface MultiTabbedTerminalProps {
onClose?: () => void;
TerminalComponent?: React.ComponentType<CloudShellTerminalProps>;
}

export const MultiTabbedTerminal: React.FC<MultiTabbedTerminalProps> = ({ onClose }) => {
export const MultiTabbedTerminal: React.FC<MultiTabbedTerminalProps> = ({
onClose,
TerminalComponent = CloudShellTerminal,
}) => {
const [terminalTabs, setTerminalTabs] = React.useState<number[]>([1]);
const [activeTabKey, setActiveTabKey] = React.useState<number>(1);
const [tickNamespace, setTickNamespace] = React.useState<string>(null);
const [tickWorkspace, setTickWorkspace] = React.useState<string>(null);
const { t } = useTranslation();
const { t } = useTranslation('webterminal-plugin');
const fireTelemetryEvent = useTelemetry();

const tick = React.useCallback(() => {
@@ -57,8 +58,8 @@ export const MultiTabbedTerminal: React.FC<MultiTabbedTerminalProps> = ({ onClos
}
};

const removeCurrentTerminal = (event, tabIndex: number) => {
event.stopPropagation();
const removeCurrentTerminal = (_, eventKey: number) => {
const tabIndex = terminalTabs.indexOf(eventKey);
const tabs = [...terminalTabs];
if (tabs[tabIndex] === activeTabKey) {
setActiveTabKey(tabIndex > 0 ? tabs[tabIndex - 1] : tabs[tabs.length - 1]);
@@ -75,69 +76,44 @@ export const MultiTabbedTerminal: React.FC<MultiTabbedTerminalProps> = ({ onClos
terminal === activeTabKey && name !== tickWorkspace && setTickWorkspace(name);
};

const removeTabFunction = terminalTabs.length > 1 ? removeCurrentTerminal : onClose;

return (
<Tabs activeKey={activeTabKey} isBox data-test="multi-tab-terminal">
{terminalTabs.map((terminalNumber, tabIndex) => (
<Tabs
activeKey={activeTabKey}
isBox
data-test="multi-tab-terminal"
className="co-cloud-shell-drawer__header"
onClose={removeTabFunction}
onAdd={terminalTabs.length < MAX_TERMINAL_TABS ? addNewTerminal : undefined}
addButtonAriaLabel={t('Add new tab')}
>
{terminalTabs.map((terminalNumber) => (
<Tab
className="co-multi-tabbed-terminal__tab"
closeButtonAriaLabel={t('Close terminal tab')}
data-test="multi-tab-terminal-tab"
eventKey={terminalNumber}
key={terminalNumber}
title={
<div>
<TabTitleText onClick={() => setActiveTabKey(terminalNumber)}>
{t('webterminal-plugin~Terminal {{number}}', { number: terminalNumber })}
</TabTitleText>
<TabTitleIcon>
{terminalTabs.length > 1 ? (
<Button
icon={<CloseIcon />}
variant="plain"
style={{ padding: '0' }}
aria-label={t('webterminal-plugin~Close terminal tab')}
data-test="close-terminal-icon"
onClick={(event) => removeCurrentTerminal(event, tabIndex)}
/>
) : (
<Button
icon={<CloseIcon />}
variant="plain"
style={{ padding: '0' }}
aria-label={t('webterminal-plugin~Close terminal')}
data-test="close-terminal-icon"
onClick={onClose}
/>
)}
</TabTitleIcon>
</div>
}
onClick={() => setActiveTabKey(terminalNumber)}
onMouseDown={(event) => {
// middle click to close
if (event.button === 1) {
event.preventDefault();
if (typeof removeTabFunction === 'function') {
removeTabFunction(event, terminalNumber);
}
}
}}
title={t('Terminal {{number}}', { number: terminalNumber })}
>
<CloudShellTerminal
<TerminalComponent
terminalNumber={terminalNumber}
setWorkspaceName={getWorkspaceName}
setWorkspaceNamespace={getWorkspaceNamespace}
/>
</Tab>
))}
{terminalTabs.length < MAX_TERMINAL_TABS && (
<Tab
eventKey="add-tab"
onClick={addNewTerminal}
title={
<TabTitleIcon>
<Button
icon={<PlusIcon />}
variant="plain"
style={{ padding: '0' }}
aria-label={t('webterminal-plugin~Add new tab')}
data-test="add-terminal-icon"
/>
</TabTitleIcon>
}
/>
)}
</Tabs>
);
};

export default MultiTabbedTerminal;
Original file line number Diff line number Diff line change
@@ -1,34 +1,31 @@
import { shallow } from 'enzyme';
import { Drawer } from '@console/shared';
import CloseButton from '@console/shared/src/components/close-button';
import { configure, render } from '@testing-library/react';
import CloudShellDrawer from '../CloudShellDrawer';

jest.mock('@console/shared/src/hooks/useTelemetry', () => ({
useTelemetry: () => {},
}));

const MockMultiTabbedTerminal = () => <p data-test="terminal-content">Terminal content</p>;

describe('CloudShellDrawerComponent', () => {
beforeEach(() => {
configure({ testIdAttribute: 'data-test' });
});
it('should render children as Drawer children when present', () => {
const wrapper = shallow(
<CloudShellDrawer onClose={() => null}>
<p data-test="terminal-content">Terminal content</p>
const wrapper = render(
<CloudShellDrawer onClose={() => null} TerminalBody={MockMultiTabbedTerminal}>
<p>Console webapp</p>
</CloudShellDrawer>,
);
expect(wrapper.find(Drawer).children().find('[data-test="terminal-content"]').text()).toEqual(
'Terminal content',
);
expect(wrapper.getByTestId('terminal-content').innerHTML).toEqual('Terminal content');
});

it('should call onClose when clicked on close button', () => {
const onClose = jest.fn();
const wrapper = shallow(
<CloudShellDrawer onClose={onClose}>
<p>Terminal content</p>
it('should still render children when the Drawer is closed', () => {
const wrapper = render(
<CloudShellDrawer onClose={() => null} open={false} TerminalBody={MockMultiTabbedTerminal}>
<p data-test="body">Console webapp</p>
</CloudShellDrawer>,
);
const closeButton = wrapper.find(Drawer).shallow().find(CloseButton);
expect(closeButton.props().ariaLabel).toEqual('Close terminal');
closeButton.simulate('click');
expect(onClose).toHaveBeenCalled();
expect(wrapper.getByTestId('body').innerHTML).toEqual('Console webapp');
});
});
Original file line number Diff line number Diff line change
@@ -2,7 +2,7 @@ import { shallow } from 'enzyme';
import { Navigate } from 'react-router-dom-v5-compat';
import * as flagsModule from '@console/dynamic-plugin-sdk/src/utils/flags';
import CloudShellTab from '../CloudShellTab';
import MultiTabTerminal from '../MultiTabbedTerminal';
import { MultiTabbedTerminal } from '../MultiTabbedTerminal';

describe('CloudShellTab', () => {
it('should not render redirect component if flag check is pending', () => {
@@ -20,6 +20,6 @@ describe('CloudShellTab', () => {
it('should render CloudShellTerminal when Devworkspaceflag is true and not MultiCluster', () => {
spyOn(flagsModule, 'useFlag').and.returnValue(true);
const cloudShellTabWrapper = shallow(<CloudShellTab />);
expect(cloudShellTabWrapper.find(MultiTabTerminal).exists()).toBe(true);
expect(cloudShellTabWrapper.find(MultiTabbedTerminal).exists()).toBe(true);
});
});
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Button } from '@patternfly/react-core';
import { shallow } from 'enzyme';
import MinimizeRestoreButton from '../MinimizeRestoreButton';
import { MinimizeRestoreButton } from '../MinimizeRestoreButton';

describe('MinimizeRestoreButton', () => {
it('should render a button', () => {
Original file line number Diff line number Diff line change
@@ -1,67 +1,93 @@
import { PlusIcon } from '@patternfly/react-icons/dist/esm/icons/plus-icon';
import { mount } from 'enzyme';
import { configure, render } from '@testing-library/react';
import { Provider } from 'react-redux';
import store from '@console/internal/redux';
import { sendActivityTick } from '../cloud-shell-utils';
import ChoudShellTerminal from '../CloudShellTerminal';
import MultiTabTerminal from '../MultiTabbedTerminal';

Object.defineProperty(window, 'requestAnimationFrame', {
writable: true,
value: (callback) => callback(),
});
import { MultiTabbedTerminal } from '../MultiTabbedTerminal';

jest.mock('../cloud-shell-utils', () => {
return {
sendActivityTick: jest.fn(),
};
});

const originalWindowRequestAnimationFrame = window.requestAnimationFrame;

describe('MultiTabTerminal', () => {
jest.useFakeTimers();
let count = 0;
jest
.spyOn(window, 'requestAnimationFrame')
.mockImplementation((cb) => setTimeout(() => cb(100 * ++count), 100));
(sendActivityTick as jest.Mock).mockImplementation((a, b) => [a, b]);
const multiTabTerminalWrapper = mount(
<Provider store={store}>
<MultiTabTerminal />
</Provider>,
);

const MockCloudShellTerminal = () => <p data-test="terminal-content">Terminal content</p>;

beforeEach(() => {
configure({ testIdAttribute: 'data-test' });
});

beforeAll(() => {
window.requestAnimationFrame = (cb) => setTimeout(cb, 0);
});

afterAll(() => {
window.requestAnimationFrame = originalWindowRequestAnimationFrame;
});

it('should initially load with only one console', () => {
expect(multiTabTerminalWrapper.find(ChoudShellTerminal).length).toBe(1);
const multiTabTerminalWrapper = render(
<Provider store={store}>
<MultiTabbedTerminal TerminalComponent={MockCloudShellTerminal} />
</Provider>,
);

expect(multiTabTerminalWrapper.getAllByTestId('terminal-content').length).toBe(1);
});

it('should add terminals on add terminal icon click', () => {
const addTerminalButton = multiTabTerminalWrapper.find('[data-test="add-terminal-icon"]').at(0);
addTerminalButton.simulate('click');
expect(multiTabTerminalWrapper.find(ChoudShellTerminal).length).toBe(2);
addTerminalButton.simulate('click');
addTerminalButton.simulate('click');
expect(multiTabTerminalWrapper.find(ChoudShellTerminal).length).toBe(4);
const multiTabTerminalWrapper = render(
<Provider store={store}>
<MultiTabbedTerminal TerminalComponent={MockCloudShellTerminal} />
</Provider>,
);

const addTerminalButton = multiTabTerminalWrapper.getByLabelText('Add new tab');
addTerminalButton.click();
expect(multiTabTerminalWrapper.getAllByTestId('terminal-content').length).toBe(2);
addTerminalButton.click();
addTerminalButton.click();
expect(multiTabTerminalWrapper.getAllByTestId('terminal-content').length).toBe(4);
});

it('should not allow more than 8 terminals', () => {
const addTerminalButton = multiTabTerminalWrapper.find('[data-test="add-terminal-icon"]').at(0);
addTerminalButton.simulate('click');
addTerminalButton.simulate('click');
addTerminalButton.simulate('click');
addTerminalButton.simulate('click');
expect(multiTabTerminalWrapper.find(ChoudShellTerminal).length).toBe(8);
expect(addTerminalButton.find(<PlusIcon />).exists()).toBe(false);
expect(multiTabTerminalWrapper.find('[data-test="add-terminal-icon"]').exists()).toBe(false);
const multiTabTerminalWrapper = render(
<Provider store={store}>
<MultiTabbedTerminal TerminalComponent={MockCloudShellTerminal} />
</Provider>,
);

const addTerminalButton = multiTabTerminalWrapper.getByLabelText('Add new tab');
for (let i = 0; i < 8; i++) {
addTerminalButton.click();
}
expect(multiTabTerminalWrapper.getAllByTestId('terminal-content')).toHaveLength(8);
expect(multiTabTerminalWrapper.queryByLabelText('Add new tab')).toBeNull();
});

it('should remove terminals on remove terminal icon click', () => {
multiTabTerminalWrapper.find('[data-test="close-terminal-icon"]').at(0).simulate('click');
expect(multiTabTerminalWrapper.find(ChoudShellTerminal).length).toBe(7);
multiTabTerminalWrapper.find('[data-test="close-terminal-icon"]').at(0).simulate('click');
multiTabTerminalWrapper.find('[data-test="close-terminal-icon"]').at(0).simulate('click');
expect(multiTabTerminalWrapper.find(ChoudShellTerminal).length).toBe(5);
const multiTabTerminalWrapper = render(
<Provider store={store}>
<MultiTabbedTerminal TerminalComponent={MockCloudShellTerminal} />
</Provider>,
);

const addTerminalButton = multiTabTerminalWrapper.getByLabelText('Add new tab');
for (let i = 0; i < 8; i++) {
addTerminalButton.click();
}

multiTabTerminalWrapper.getAllByLabelText('Close terminal tab').at(7).click();
expect(multiTabTerminalWrapper.getAllByTestId('terminal-content').length).toBe(7);
multiTabTerminalWrapper.getAllByLabelText('Close terminal tab').at(6).click();
multiTabTerminalWrapper.getAllByLabelText('Close terminal tab').at(5).click();
expect(multiTabTerminalWrapper.getAllByTestId('terminal-content').length).toBe(5);
});

(window.requestAnimationFrame as any).mockRestore();
jest.clearAllTimers();
});
18 changes: 11 additions & 7 deletions frontend/public/components/_sysevent-stream.scss
Original file line number Diff line number Diff line change
@@ -3,15 +3,19 @@
position: relative;
}

.slide-entering {
left: 100%;
opacity: 0;
.co-sysevent-slide-in {
animation: co-sysevent-enter
var(--pf-t--global--motion--duration--slide-in--default)
var(--pf-t--global--motion--timing-function--accelerate);
}

.slide-entered {
left: 0;
opacity: 1;
transition: all 0.5s;
@keyframes co-sysevent-enter {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}

.co-sysevent-stream__connection-error {
97 changes: 49 additions & 48 deletions frontend/public/components/app.tsx
Original file line number Diff line number Diff line change
@@ -24,7 +24,7 @@ import { fetchSwagger, getCachedResources } from '../module/k8s';
import { receivedResources, startAPIDiscovery } from '../actions/k8s';
import { pluginStore } from '../plugins';
// cloud shell imports must come later than features
import CloudShell from '@console/webterminal-plugin/src/components/cloud-shell/CloudShell';
import CloudShellDrawer from '@console/webterminal-plugin/src/components/cloud-shell/CloudShell';
import CloudShellTab from '@console/webterminal-plugin/src/components/cloud-shell/CloudShellTab';
import DetectPerspective from '@console/app/src/components/detect-perspective/DetectPerspective';
import DetectNamespace from '@console/app/src/components/detect-namespace/DetectNamespace';
@@ -227,54 +227,55 @@ const App = (props) => {
<Helmet titleTemplate={`%s · ${productName}`} defaultTitle={productName} />
<ConsoleNotifier location="BannerTop" />
<QuickStartDrawer>
<Flex
id="app-content"
direction={{ default: 'column' }}
style={{ flex: '1 0 auto', height: '100%' }}
>
<Page
isContentFilled
id="content"
// Need to pass mainTabIndex=null to enable keyboard scrolling as default tabIndex is set to -1 by patternfly
mainTabIndex={null}
masthead={
<Masthead
isNavOpen={isNavOpen}
onNavToggle={onNavToggle}
isMastheadStacked={isMastheadStacked}
/>
}
sidebar={
<Navigation
isNavOpen={isNavOpen}
onNavSelect={onNavSelect}
onPerspectiveSelected={onNavSelect}
/>
}
skipToContent={
<SkipToContent href={`${location.pathname}${location.search}#content-scrollable`}>
{t('public~Skip to content')}
</SkipToContent>
}
notificationDrawer={
<NotificationDrawer
onDrawerChange={onNotificationDrawerToggle}
isDrawerExpanded={isNotificationDrawerExpanded}
drawerRef={drawerRef}
/>
}
onNotificationDrawerExpand={() => focusDrawer()}
isNotificationDrawerExpanded={isNotificationDrawerExpanded}
style={{ flex: '1', height: '0' }}
<CloudShellDrawer>
<Flex
id="app-content"
direction={{ default: 'column' }}
style={{ flex: '1 0 auto', height: '100%' }}
>
<AppContents />
</Page>
<CloudShell />
<GuidedTour />
</Flex>
{consoleCapabilityLightspeedButtonIsEnabled && lightspeedIsAvailableToInstall && (
<Lightspeed />
)}
<Page
isContentFilled
id="content"
// Need to pass mainTabIndex=null to enable keyboard scrolling as default tabIndex is set to -1 by patternfly
mainTabIndex={null}
masthead={
<Masthead
isNavOpen={isNavOpen}
onNavToggle={onNavToggle}
isMastheadStacked={isMastheadStacked}
/>
}
sidebar={
<Navigation
isNavOpen={isNavOpen}
onNavSelect={onNavSelect}
onPerspectiveSelected={onNavSelect}
/>
}
skipToContent={
<SkipToContent href={`${location.pathname}${location.search}#content-scrollable`}>
{t('public~Skip to content')}
</SkipToContent>
}
notificationDrawer={
<NotificationDrawer
onDrawerChange={onNotificationDrawerToggle}
isDrawerExpanded={isNotificationDrawerExpanded}
drawerRef={drawerRef}
/>
}
onNotificationDrawerExpand={() => focusDrawer()}
isNotificationDrawerExpanded={isNotificationDrawerExpanded}
style={{ flex: '1', height: '0' }}
>
<AppContents />
</Page>
<GuidedTour />
</Flex>
{consoleCapabilityLightspeedButtonIsEnabled && lightspeedIsAvailableToInstall && (
<Lightspeed />
)}
</CloudShellDrawer>
<div id="modal-container" role="dialog" aria-modal="true" aria-label={t('public~Modal')} />
</QuickStartDrawer>
<ConsoleNotifier location="BannerBottom" />
28 changes: 11 additions & 17 deletions frontend/public/components/utils/event-stream.tsx
Original file line number Diff line number Diff line change
@@ -7,15 +7,13 @@ import {
CellMeasurer,
CellMeasurerCache,
} from 'react-virtualized';
import { CSSTransition } from 'react-transition-group';
import { css } from '@patternfly/react-styles';

import { EventKind } from '../../module/k8s';
import { WithScrollContainer } from './dom-utils';

// Keep track of seen events so we only animate new ones.
const seen = new Set();
const timeout = { enter: 150 };

const measurementCache = new CellMeasurerCache({
fixedWidth: true,
@@ -50,21 +48,16 @@ class SysEvent extends React.Component<SysEventProps> {
}

return (
<div className={css('co-sysevent--transition', className)} style={style} role="row">
<CSSTransition
mountOnEnter={true}
appear={shouldAnimate}
in
exit={false}
timeout={timeout}
classNames="slide"
>
{(status) => (
<div className={`slide-${status}`}>
<EventComponent event={event} list={list} cache={measurementCache} index={index} />
</div>
)}
</CSSTransition>
<div
className={css(
{ 'co-sysevent-slide-in': shouldAnimate },
'co-sysevent--transition',
className,
)}
style={style}
role="row"
>
<EventComponent event={event} list={list} cache={measurementCache} index={index} />
</div>
);
}
@@ -115,6 +108,7 @@ export const EventStreamList: React.FC<EventStreamListProps> = ({
{({ width }) => (
<div ref={registerChild}>
<VirtualList
className="co-sysevent-slide-in"
autoHeight
data={events}
deferredMeasurementCache={measurementCache}
6 changes: 6 additions & 0 deletions frontend/public/style/_overrides.scss
Original file line number Diff line number Diff line change
@@ -173,6 +173,12 @@ $masthead-logo-max-height: 60px;
position: relative;
}

// Fixes a very strange web-terminal e2e bug where the masthead disappears
#co-cloud-shell-drawer > .pf-v6-c-drawer__main > .pf-v6-c-drawer__content {
position: sticky;
top: 0;
}

.pf-v6-c-form--no-gap {
gap: 0;
}
40 changes: 1 addition & 39 deletions frontend/yarn.lock
Original file line number Diff line number Diff line change
@@ -2721,12 +2721,6 @@
"@types/history" "^4.7.11"
"@types/react" "*"

"@types/react-transition-group@2.x":
version "2.0.8"
resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-2.0.8.tgz#1ea86f6d8288e4bba8d743317ba9cc61cdacc1ad"
dependencies:
"@types/react" "*"

"@types/react-virtualized@9.x":
version "9.18.3"
resolved "https://registry.yarnpkg.com/@types/react-virtualized/-/react-virtualized-9.18.3.tgz#777b3c28bd2b972d0a402f2f9d11bacef550be1c"
@@ -4945,10 +4939,6 @@ chai@^4.1.2:
pathval "^1.1.0"
type-detect "^4.0.5"

chain-function@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/chain-function/-/chain-function-1.0.0.tgz#0d4ab37e7e18ead0bdc47b920764118ce58733dc"

chalk@2.4.x, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.0, chalk@^2.4.1, chalk@^2.4.2:
version "2.4.2"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
@@ -5323,7 +5313,7 @@ clone@^2.1.2:
resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f"
integrity sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=

clsx@^1.0.4, clsx@^1.1.1:
clsx@^1.0.4:
version "1.2.1"
resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12"
integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==
@@ -7079,10 +7069,6 @@ dom-converter@^0.2.0:
dependencies:
utila "~0.4"

dom-helpers@^3.2.0:
version "3.3.1"
resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-3.3.1.tgz#fc1a4e15ffdf60ddde03a480a9c0fece821dd4a6"

dom-helpers@^5.1.3:
version "5.2.0"
resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.2.0.tgz#57fd054c5f8f34c52a3eeffdb7e7e93cd357d95b"
@@ -14311,14 +14297,6 @@ react-dom@^17.0.1:
object-assign "^4.1.1"
scheduler "^0.20.1"

react-draggable@4.x:
version "4.4.6"
resolved "https://registry.yarnpkg.com/react-draggable/-/react-draggable-4.4.6.tgz#63343ee945770881ca1256a5b6fa5c9f5983fe1e"
integrity sha512-LtY5Xw1zTPqHkVmtM3X8MUOxNDOUhv/khTgBgrUvwaS064bwVvxT+q5El0uUFNx5IEPKXuRejr7UqLwBIg5pdw==
dependencies:
clsx "^1.1.1"
prop-types "^15.8.1"

react-dropzone@14.3.5, react-dropzone@^14.3.5:
version "14.3.5"
resolved "https://registry.yarnpkg.com/react-dropzone/-/react-dropzone-14.3.5.tgz#1a8bd312c8a353ec78ef402842ccb3589c225add"
@@ -14520,16 +14498,6 @@ react-test-renderer@^17.0.0:
react-shallow-renderer "^16.13.1"
scheduler "^0.20.1"

react-transition-group@2.3.x:
version "2.3.0"
resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-2.3.0.tgz#8dd1af58f6af284b19fd057f512e74f20438ad31"
dependencies:
chain-function "^1.0.0"
dom-helpers "^3.2.0"
loose-envify "^1.3.1"
prop-types "^15.5.8"
warning "^3.0.0"

react-virtualized@9.x, react-virtualized@^9.22.5:
version "9.22.5"
resolved "https://registry.yarnpkg.com/react-virtualized/-/react-virtualized-9.22.5.tgz#bfb96fed519de378b50d8c0064b92994b3b91620"
@@ -17860,12 +17828,6 @@ walker@~1.0.5:
dependencies:
makeerror "1.0.x"

warning@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/warning/-/warning-3.0.0.tgz#32e5377cb572de4ab04753bdf8821c01ed605b7c"
dependencies:
loose-envify "^1.0.0"

warning@^4.0.3:
version "4.0.3"
resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.3.tgz#16e9e077eb8a86d6af7d64aa1e05fd85b4678ca3"