Skip to content
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

Feat(cli-terminal): Create cli-terminal #4762

Merged
merged 1 commit into from
Apr 7, 2020
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,21 +1,38 @@
import * as React from 'react';
import { Button } from '@patternfly/react-core';
import CloudShellBody from './CloudShellBody';
import { Dispatch } from 'redux';
import { connect } from 'react-redux';
import { RootState } from '@console/internal/redux';
import { isCloudShellExpanded } from '../../redux/reducers/cloud-shell-reducer';
import { toggleCloudShellExpanded } from '../../redux/actions/cloud-shell-actions';
import cloudShellConfirmationModal from './cloudShellConfirmationModal';
import CloudShellDrawer from './CloudShellDrawer';
import CloudShellTerminal from './CloudShellTerminal';

const CloudShell: React.FC = () => {
const [open, setOpen] = React.useState(false);
return (
<>
{/* Remove this button once actual terminal is in place */}
<Button variant="control" onClick={() => setOpen(!open)}>
Open Drawer
</Button>
<CloudShellDrawer open={open} onClose={() => setOpen(false)}>
<CloudShellBody />
</CloudShellDrawer>
</>
);
type StateProps = {
open: boolean;
};

export default CloudShell;
type DispatchProps = {
onClose: () => void;
};

type CloudShellProps = StateProps & DispatchProps;

const CloudShell: React.FC<CloudShellProps> = ({ open, onClose }) => {
const toggleWithModal = () => cloudShellConfirmationModal(onClose);
return open ? (
<CloudShellDrawer onClose={toggleWithModal}>
<CloudShellTerminal />
</CloudShellDrawer>
) : null;
};

const stateToProps = (state: RootState): StateProps => ({
open: isCloudShellExpanded(state),
});

const dispatchToProps = (dispatch: Dispatch): DispatchProps => ({
onClose: () => dispatch(toggleCloudShellExpanded()),
});

export default connect<StateProps, DispatchProps>(stateToProps, dispatchToProps)(CloudShell);

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
.co-cloud-shell-drawer {
&__heading {
padding-left: var(--pf-global--spacer--md);
font-weight: bold;
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import * as React from 'react';
import { Tooltip, Flex, FlexItem, FlexModifiers, Button } from '@patternfly/react-core';
import { CloseIcon, ExternalLinkAltIcon } from '@patternfly/react-icons';
import { Drawer } from '@console/shared';
import Drawer from '@console/shared/src/components/drawer/Drawer';
import MinimizeRestoreButton from './MinimizeRestoreButton';

import './CloudShellDrawer.scss';

export type CloudShellDrawerProps = {
open: boolean;
type CloudShellDrawerProps = {
onClose: () => void;
};

Expand All @@ -17,8 +17,8 @@ const getMastheadHeight = (): number => {
return height;
};

const CloudShellDrawer: React.FC<CloudShellDrawerProps> = ({ open, children, onClose }) => {
const [height, setHeight] = React.useState(318);
const CloudShellDrawer: React.FC<CloudShellDrawerProps> = ({ children, onClose }) => {
const [height, setHeight] = React.useState(326);
const [expanded, setExpanded] = React.useState<boolean>(true);
const onMRButtonClick = (expandedState: boolean) => {
setExpanded(!expandedState);
Expand All @@ -28,16 +28,14 @@ const CloudShellDrawer: React.FC<CloudShellDrawerProps> = ({ open, children, onC
setHeight(resizeHeight);
};
const header = (
<Flex>
<FlexItem className="ocs-terminal-drawer__heading">
<b>Command Line Terminal</b>
</FlexItem>
<Flex style={{ flexGrow: 1 }}>
<FlexItem className="co-cloud-shell-drawer__heading">Command Line Terminal</FlexItem>
<FlexItem breakpointMods={[{ modifier: FlexModifiers['align-right'] }]}>
<Tooltip content="Open terminal in new tab">
<Button
variant="plain"
component="a"
// change this once we can open teeminal in new tab
// change this once we can open terminal in new tab
href={null}
target="_blank"
aria-label="Open terminal in new tab"
Expand All @@ -52,26 +50,30 @@ const CloudShellDrawer: React.FC<CloudShellDrawerProps> = ({ open, children, onC
onClick={onMRButtonClick}
/>
<Tooltip content="Close terminal">
<Button variant="plain" type="button" onClick={onClose} aria-label="Close Terminal">
<Button
variant="plain"
data-test-id="cloudshell-terminal-close"
type="button"
onClick={onClose}
aria-label="Close terminal"
>
<CloseIcon />
</Button>
</Tooltip>
</FlexItem>
</Flex>
);
return (
open && (
<Drawer
open={expanded}
height={height}
header={header}
maxHeight={`calc(100vh - ${getMastheadHeight()}px)`}
onChange={handleChange}
resizable
>
{children}
</Drawer>
)
<Drawer
open={expanded}
height={height}
header={header}
maxHeight={`calc(100vh - ${getMastheadHeight()}px)`}
onChange={handleChange}
resizable
>
{children}
</Drawer>
);
};

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import * as React from 'react';
import { Dispatch } from 'redux';
import { connect } from 'react-redux';
import { RootState } from '@console/internal/redux';
import { TerminalIcon } from '@patternfly/react-icons';
import { isCloudShellExpanded } from '../../redux/reducers/cloud-shell-reducer';
import { Button, ToolbarItem, Tooltip } from '@patternfly/react-core';
import { connectToFlags, WithFlagsProps } from '@console/internal/reducers/features';
import { FLAG_DEVWORKSPACE } from '../../consts';
import { toggleCloudShellExpanded } from '../../redux/actions/cloud-shell-actions';
import { useAccessReview } from '@console/internal/components/utils';
import { WorkspaceModel } from '../../models';
import cloudShellConfirmationModal from './cloudShellConfirmationModal';

type DispatchProps = {
onClick: () => void;
};

type StateProps = {
open?: boolean;
};

type Props = WithFlagsProps & StateProps & DispatchProps;

// TODO use proper namespace and resource name
const namespace = 'che-workspace-controller';
const name = 'cloudshell-userid';

const ClouldShellMastheadButton: React.FC<Props> = ({ flags, onClick, open }) => {
const editAccess = useAccessReview({
group: WorkspaceModel.apiGroup,
resource: WorkspaceModel.plural,
verb: 'create',
name,
namespace,
});

const toggleTerminal = () => {
if (open) {
return cloudShellConfirmationModal(onClick);
}
return onClick();
};

if (!editAccess || !flags[FLAG_DEVWORKSPACE]) {
return null;
}

return (
<ToolbarItem>
<Tooltip content={open ? 'Close Terminal' : 'Open command line terminal'}>
<Button variant="plain" aria-label="Command Line Terminal" onClick={toggleTerminal}>
<TerminalIcon className="co-masthead-icon" />
</Button>
</Tooltip>
</ToolbarItem>
);
};

const cloudshellStateToProps = (state: RootState): StateProps => ({
open: isCloudShellExpanded(state),
});

const cloudshellPropsToState = (dispatch: Dispatch): DispatchProps => ({
onClick: () => dispatch(toggleCloudShellExpanded()),
});

export default connect<StateProps, DispatchProps>(
cloudshellStateToProps,
cloudshellPropsToState,
)(connectToFlags(FLAG_DEVWORKSPACE)(ClouldShellMastheadButton));
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import * as React from 'react';
import { referenceForModel, k8sCreate } from '@console/internal/module/k8s';
import { Firehose, FirehoseResource, FirehoseResult } from '@console/internal/components/utils';

import { WorkspaceModel } from '../../models';
import { newCloudShellWorkSpace, CloudShellResource } from './utils/cloudshell-resource';
import CloudShellTerminalFrame from './CloudShellTerminalFrame';

// TODO use proper namespace and resource name
const namespace = 'che-workspace-controller';
const name = 'cloudshell-userid';

const Inner: React.FC<{ cloudShell?: FirehoseResult<CloudShellResource[]> }> = ({ cloudShell }) => {
const loaded = cloudShell?.loaded;
const data = cloudShell?.data?.[0];
React.useEffect(() => {
if (loaded && data == null) {
k8sCreate(WorkspaceModel, newCloudShellWorkSpace(name, namespace));
}
}, [loaded, data]);

const running = data?.status?.phase === 'Running';
const url = data?.status?.ideUrl;
return <CloudShellTerminalFrame loading={!running} url={url} />;
};

const CloudShellTerminal: React.FC = () => {
const resources: FirehoseResource[] = [
{
kind: referenceForModel(WorkspaceModel),
namespace,
prop: `cloudShell`,
isList: true,
fieldSelector: `metadata.name=${name}`,
},
];

return (
<Firehose resources={resources}>
<Inner />
</Firehose>
);
};

export default CloudShellTerminal;
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
.co-cloud-shell-terminal-frame {
background-color: #000;
height: 100%;

& > iframe {
height: 100%;
width: 100%;
border: 0;
padding: 4px;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import * as React from 'react';
import { LoadingBox } from '@console/internal/components/utils';
import './CloudShellTerminalFrame.scss';

type CloudShellTerminalFrameProps = {
loading?: boolean;
url?: string;
};

const CloudShellTerminalFrame: React.FC<CloudShellTerminalFrameProps> = ({ loading, url }) => (
<div className="co-cloud-shell-terminal-frame">
{loading ? <LoadingBox /> : <iframe title="Command Line Terminal" src={url} />}
</div>
);

export default CloudShellTerminalFrame;
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,11 @@ import * as React from 'react';
import { shallow } from 'enzyme';
import CloudShellDrawer from '../CloudShellDrawer';
import { Drawer } from '@console/shared';
import { Button } from '@patternfly/react-core';

describe('CloudShellDrawerComponent', () => {
it('should not exist if open is not True', () => {
const wrapper = shallow(<CloudShellDrawer open={false} onClose={() => null} />);
expect(wrapper.isEmptyRender()).toEqual(true);
});

it('should exist when open is set to true', () => {
const wrapper = shallow(<CloudShellDrawer open onClose={() => null} />);
expect(wrapper.find(Drawer).exists()).toEqual(true);
});

it('should render children as Drawer children when present', () => {
const wrapper = shallow(
<CloudShellDrawer open onClose={() => null}>
<CloudShellDrawer onClose={() => null}>
<p>Terminal content</p>
</CloudShellDrawer>,
);
Expand All @@ -32,16 +21,16 @@ describe('CloudShellDrawerComponent', () => {
it('should call onClose when clicked on close button', () => {
const onClose = jest.fn();
const wrapper = shallow(
<CloudShellDrawer open onClose={onClose}>
<CloudShellDrawer onClose={onClose}>
<p>Terminal content</p>
</CloudShellDrawer>,
);
const buttons = wrapper
const closeButton = wrapper
.find(Drawer)
.shallow()
.find(Button);
expect(buttons.at(1).props()['aria-label']).toEqual('Close Terminal');
buttons.at(1).simulate('click');
.find('[data-test-id="cloudshell-terminal-close"]');
expect(closeButton.props()['aria-label']).toEqual('Close terminal');
closeButton.simulate('click');
expect(onClose).toHaveBeenCalled();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import * as React from 'react';
import { shallow } from 'enzyme';
import { LoadingBox } from '@console/internal/components/utils';
import CloudShellTerminalFrame from '../CloudShellTerminalFrame';

describe('CloudShellTerminalFrame', () => {
it('should render LoadingBox', () => {
const wrapper = shallow(<CloudShellTerminalFrame loading />);
expect(wrapper.find(LoadingBox).exists()).toBe(true);
expect(wrapper.find('iframe').exists()).toBe(false);

wrapper.setProps({ loading: false });
expect(wrapper.find(LoadingBox).exists()).toBe(false);
});

it('should render iframe', () => {
const wrapper = shallow(<CloudShellTerminalFrame url="test" />);
const iframe = wrapper.find('iframe');
expect(iframe.props().src).toBe('test');
});
});