Skip to content

Commit

Permalink
Switch to existing tab with the same workspace (#1111)
Browse files Browse the repository at this point in the history
* feat: open existing editor tab

Signed-off-by: Oleksii Kurinnyi <okurinny@redhat.com>

* fix: workspace actions menu - open - always enabled

Signed-off-by: Oleksii Kurinnyi <okurinny@redhat.com>

* fixup! feat: open existing editor tab

* fixup! fixup! feat: open existing editor tab

* fixup! fixup! fixup! feat: open existing editor tab

* fixup! fixup! fixup! fixup! feat: open existing editor tab

---------

Signed-off-by: Oleksii Kurinnyi <okurinny@redhat.com>
  • Loading branch information
akurinnoy authored May 23, 2024
1 parent b6b5a38 commit 9f59c7a
Show file tree
Hide file tree
Showing 22 changed files with 351 additions and 90 deletions.
2 changes: 2 additions & 0 deletions .deps/dev.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,8 @@
| [`@types/parse-json@4.0.1`](https://github.com/DefinitelyTyped/DefinitelyTyped.git) | MIT | clearlydefined |
| [`@types/qs@6.9.9`](https://github.com/DefinitelyTyped/DefinitelyTyped.git) | MIT | #13991 |
| [`@types/react-copy-to-clipboard@4.3.0`](https://github.com/DefinitelyTyped/DefinitelyTyped.git) | MIT | clearlydefined |
| [`@types/react-router-dom@5.3.3`](https://github.com/DefinitelyTyped/DefinitelyTyped.git) | MIT | clearlydefined |
| [`@types/react-router@5.1.20`](https://github.com/DefinitelyTyped/DefinitelyTyped.git) | MIT | clearlydefined |
| [`@types/react-test-renderer@18.0.5`](https://github.com/DefinitelyTyped/DefinitelyTyped.git) | MIT | clearlydefined |
| [`@types/redux-mock-store@1.0.5`](https://github.com/DefinitelyTyped/DefinitelyTyped.git) | MIT | clearlydefined |
| [`@types/request@2.48.12`](https://github.com/DefinitelyTyped/DefinitelyTyped.git) | MIT | clearlydefined |
Expand Down
1 change: 1 addition & 0 deletions packages/dashboard-frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@
"@types/qs": "^6.9.7",
"@types/react-copy-to-clipboard": "^4.3.0",
"@types/react-test-renderer": "^18.0.0",
"@types/react-router-dom": "^5.3.3",
"@types/redux-mock-store": "^1.0.2",
"@types/sanitize-html": "^2.9.0",
"@types/testing-library__jest-dom": "^5.14.9",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export type Props = {
text: string;
};

export class TitleWithHover extends React.PureComponent<Props> {
export class TitleWithHover extends React.Component<Props> {
public render(): React.ReactElement {
const { text } = this.props;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
*/

import { Nav } from '@patternfly/react-core';
import { createMemoryHistory } from 'history';
import React from 'react';
import { Provider } from 'react-redux';
import { MemoryRouter } from 'react-router';
Expand Down Expand Up @@ -112,11 +113,12 @@ describe('Navigation Item', () => {

function getComponent(item: NavigationRecentItemObject, activeItem = ''): React.ReactElement {
const store = new FakeStoreBuilder().build();
const history = createMemoryHistory();
return (
<Provider store={store}>
<MemoryRouter>
<Nav>
<NavigationRecentItem item={item} activePath={activeItem} />
<NavigationRecentItem history={history} item={item} activePath={activeItem} />
</Nav>
</MemoryRouter>
</Provider>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,24 +11,34 @@
*/

import { NavItem } from '@patternfly/react-core';
import { History } from 'history';
import React from 'react';

import { WorkspaceStatusIndicator } from '@/components/Workspace/Status/Indicator';
import { lazyInject } from '@/inversify.config';
import { NavigationRecentItemObject } from '@/Layout/Navigation';
import styles from '@/Layout/Navigation/index.module.css';
import getActivity from '@/Layout/Navigation/isActive';
import { TitleWithHover } from '@/Layout/Navigation/RecentItem/TitleWithHover';
import { RecentItemWorkspaceActions } from '@/Layout/Navigation/RecentItem/WorkspaceActions';
import { buildIdeLoaderLocation, toHref } from '@/services/helpers/location';
import { TabManager } from '@/services/tabManager';
import { Workspace } from '@/services/workspace-adapter';

export type Props = {
history: History;
item: NavigationRecentItemObject;
activePath: string;
};

export class NavigationRecentItem extends React.PureComponent<Props> {
private handleClick(location: string, workspaceUID: string) {
const link = `#${location}`;
window.open(link, workspaceUID);
@lazyInject(TabManager)
private readonly tabManager: TabManager;

private handleClick(workspace: Workspace) {
const location = buildIdeLoaderLocation(workspace);
const href = toHref(this.props.history, location);
this.tabManager.open(href);
}

render(): React.ReactElement {
Expand All @@ -37,23 +47,21 @@ export class NavigationRecentItem extends React.PureComponent<Props> {
const isActive = getActivity(item.to, activePath);

return (
<React.Fragment>
<NavItem
id={item.to}
data-testid={item.to}
itemId={item.to}
isActive={isActive}
className={styles.navItem}
preventDefault={true}
onClick={() => this.handleClick(item.to, item.workspace.uid)}
>
<span data-testid="recent-workspace-item">
<WorkspaceStatusIndicator status={item.workspace.status} />
<TitleWithHover text={item.label} isActive={isActive} />
</span>
<RecentItemWorkspaceActions item={item} />
</NavItem>
</React.Fragment>
<NavItem
id={item.to}
data-testid={item.to}
itemId={item.to}
isActive={isActive}
className={styles.navItem}
preventDefault={true}
onClick={() => this.handleClick(item.workspace)}
>
<span data-testid="recent-workspace-item">
<WorkspaceStatusIndicator status={item.workspace.status} />
<TitleWithHover text={item.label} isActive={isActive} />
</span>
<RecentItemWorkspaceActions item={item} />
</NavItem>
);
}
}
48 changes: 28 additions & 20 deletions packages/dashboard-frontend/src/Layout/Navigation/RecentList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,38 +12,46 @@

import { NavGroup, NavList } from '@patternfly/react-core';
import React from 'react';
import { useHistory } from 'react-router-dom';

import { NavigationRecentItem } from '@/Layout/Navigation/RecentItem';
import { ROUTE } from '@/Routes/routes';
import { Workspace } from '@/services/workspace-adapter';

import { NavigationRecentItemObject } from '.';

function buildRecentWorkspacesItems(
workspaces: Array<Workspace>,
activePath: string,
): Array<React.ReactElement> {
return workspaces.map(workspace => {
const workspaceName = workspace.name;
const namespace = workspace.namespace;
const navigateTo = ROUTE.IDE_LOADER.replace(':namespace', namespace).replace(
':workspaceName',
workspaceName,
);
const item: NavigationRecentItemObject = {
to: navigateTo,
label: workspaceName,
workspace,
};
return <NavigationRecentItem key={item.to} item={item} activePath={activePath} />;
});
function RecentWorkspaceItem(props: {
workspace: Workspace;
activePath: string;
}): React.ReactElement {
const workspaceName = props.workspace.name;
const namespace = props.workspace.namespace;
const navigateTo = ROUTE.IDE_LOADER.replace(':namespace', namespace).replace(
':workspaceName',
workspaceName,
);
const item: NavigationRecentItemObject = {
to: navigateTo,
label: workspaceName,
workspace: props.workspace,
};
const history = useHistory();
return <NavigationRecentItem history={history} item={item} activePath={props.activePath} />;
}

function NavigationRecentList(props: {
workspaces: Array<Workspace>;
activePath: string;
workspaces: Array<Workspace>;
}): React.ReactElement {
const recentWorkspaceItems = buildRecentWorkspacesItems(props.workspaces, props.activePath);
const recentWorkspaceItems = props.workspaces.map(workspace => {
return (
<RecentWorkspaceItem
key={workspace.name}
workspace={workspace}
activePath={props.activePath}
/>
);
});
return (
<NavList>
<NavGroup title="RECENT WORKSPACES" style={{ marginTop: '25px' }}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ export class Navigation extends React.PureComponent<Props, State> {
return (
<Nav aria-label="Navigation" onSelect={selected => this.handleNavSelect(selected)}>
<NavigationMainList activePath={activeLocation.pathname} />
<NavigationRecentList workspaces={recentWorkspaces} activePath={activeLocation.pathname} />
<NavigationRecentList activePath={activeLocation.pathname} workspaces={recentWorkspaces} />
</Nav>
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
} from '@/components/WorkspaceProgress/ProgressStep';
import { ProgressStepTitle } from '@/components/WorkspaceProgress/StepTitle';
import { TimeLimit } from '@/components/WorkspaceProgress/TimeLimit';
import { lazyInject } from '@/inversify.config';
import devfileApi from '@/services/devfileApi';
import { FactoryLocationAdapter } from '@/services/factory-location-adapter';
import {
Expand All @@ -37,8 +38,9 @@ import {
USE_DEFAULT_DEVFILE,
} from '@/services/helpers/factoryFlow/buildFactoryParams';
import { findTargetWorkspace } from '@/services/helpers/factoryFlow/findTargetWorkspace';
import { buildIdeLoaderLocation } from '@/services/helpers/location';
import { buildIdeLoaderLocation, toHref } from '@/services/helpers/location';
import { AlertItem } from '@/services/helpers/types';
import { TabManager } from '@/services/tabManager';
import { Workspace } from '@/services/workspace-adapter';
import { AppState } from '@/store';
import { selectDefaultDevfile } from '@/store/DevfileRegistries/selectors';
Expand Down Expand Up @@ -71,6 +73,9 @@ export type State = ProgressStepState & {
class CreatingStepApplyDevfile extends ProgressStep<Props, State> {
protected readonly name = 'Generating a DevWorkspace from the Devfile';

@lazyInject(TabManager)
private readonly tabManager: TabManager;

constructor(props: Props) {
super(props);

Expand Down Expand Up @@ -243,6 +248,10 @@ class CreatingStepApplyDevfile extends ProgressStep<Props, State> {
const nextLocation = buildIdeLoaderLocation(workspace);
this.props.history.location.pathname = nextLocation.pathname;
this.props.history.location.search = '';

const url = toHref(this.props.history, nextLocation);
this.tabManager.rename(url);

return true;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,15 @@ import {
} from '@/components/WorkspaceProgress/ProgressStep';
import { ProgressStepTitle } from '@/components/WorkspaceProgress/StepTitle';
import { TimeLimit } from '@/components/WorkspaceProgress/TimeLimit';
import { lazyInject } from '@/inversify.config';
import {
buildFactoryParams,
FactoryParams,
} from '@/services/helpers/factoryFlow/buildFactoryParams';
import { findTargetWorkspace } from '@/services/helpers/factoryFlow/findTargetWorkspace';
import { buildIdeLoaderLocation } from '@/services/helpers/location';
import { buildIdeLoaderLocation, toHref } from '@/services/helpers/location';
import { AlertItem } from '@/services/helpers/types';
import { TabManager } from '@/services/tabManager';
import { Workspace } from '@/services/workspace-adapter';
import { AppState } from '@/store';
import * as DevfileRegistriesStore from '@/store/DevfileRegistries';
Expand Down Expand Up @@ -60,6 +62,9 @@ export type State = ProgressStepState & {
class CreatingStepApplyResources extends ProgressStep<Props, State> {
protected readonly name = 'Applying resources';

@lazyInject(TabManager)
private readonly tabManager: TabManager;

constructor(props: Props) {
super(props);

Expand Down Expand Up @@ -189,6 +194,10 @@ class CreatingStepApplyResources extends ProgressStep<Props, State> {
const nextLocation = buildIdeLoaderLocation(targetWorkspace);
this.props.history.location.pathname = nextLocation.pathname;
this.props.history.location.search = '';

const url = toHref(this.props.history, nextLocation);
this.tabManager.rename(url);

return true;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,12 @@ import { Provider } from 'react-redux';
import { Store } from 'redux';

import { MIN_STEP_DURATION_MS, TIMEOUT_TO_GET_URL_SEC } from '@/components/WorkspaceProgress/const';
import { container } from '@/inversify.config';
import { WorkspaceParams } from '@/Routes/routes';
import getComponentRenderer from '@/services/__mocks__/getComponentRenderer';
import { getDefer } from '@/services/helpers/deferred';
import { AlertItem } from '@/services/helpers/types';
import { TabManager } from '@/services/tabManager';
import { DevWorkspaceBuilder } from '@/store/__mocks__/devWorkspaceBuilder';
import { FakeStoreBuilder } from '@/store/__mocks__/storeBuilder';

Expand All @@ -41,8 +43,6 @@ const mockOnRestart = jest.fn();
const mockOnError = jest.fn();
const mockOnHideError = jest.fn();

const mockLocationReplace = jest.fn();

const namespace = 'che-user';
const workspaceName = 'test-workspace';
const matchParams: WorkspaceParams = {
Expand All @@ -51,9 +51,12 @@ const matchParams: WorkspaceParams = {
};

describe('Starting steps, opening an editor', () => {
let tabManager: TabManager;

beforeEach(() => {
delete (window as any).location;
(window.location as any) = { replace: mockLocationReplace };
container.snapshot();
tabManager = container.get(TabManager);
tabManager.replace = jest.fn();

jest.useFakeTimers();
});
Expand All @@ -62,6 +65,7 @@ describe('Starting steps, opening an editor', () => {
jest.clearAllMocks();
jest.clearAllTimers();
jest.useRealTimers();
container.restore();
});

describe('workspace not found', () => {
Expand Down Expand Up @@ -188,7 +192,7 @@ describe('Starting steps, opening an editor', () => {

await jest.advanceTimersByTimeAsync(MIN_STEP_DURATION_MS);

expect(mockLocationReplace).toHaveBeenCalledWith('main-url');
await waitFor(() => expect(tabManager.replace).toHaveBeenCalledWith('main-url'));

expect(mockOnNextStep).toHaveBeenCalled();
expect(mockOnError).not.toHaveBeenCalled();
Expand Down Expand Up @@ -256,7 +260,7 @@ describe('Starting steps, opening an editor', () => {
await jest.advanceTimersByTimeAsync(MIN_STEP_DURATION_MS);

// wait for opening IDE url
await waitFor(() => expect(mockLocationReplace).toHaveBeenCalledWith('main-url'));
await waitFor(() => expect(tabManager.replace).toHaveBeenCalledWith('main-url'));
});

test(`mainUrl is propagated after some time`, async () => {
Expand Down Expand Up @@ -295,7 +299,7 @@ describe('Starting steps, opening an editor', () => {
await jest.advanceTimersByTimeAsync(MIN_STEP_DURATION_MS);

// wait for opening IDE url
await waitFor(() => expect(mockLocationReplace).toHaveBeenCalledWith('main-url'));
await waitFor(() => expect(tabManager.replace).toHaveBeenCalledWith('main-url'));

expect(mockOnError).not.toHaveBeenCalled();
});
Expand Down Expand Up @@ -324,7 +328,7 @@ describe('Starting steps, opening an editor', () => {
await jest.advanceTimersByTimeAsync(MIN_STEP_DURATION_MS);

// IDE is not opened
expect(mockLocationReplace).not.toHaveBeenCalled();
expect(tabManager.replace).not.toHaveBeenCalled();
});

test('mainUrl is propagated after some time', async () => {
Expand Down Expand Up @@ -363,7 +367,7 @@ describe('Starting steps, opening an editor', () => {
await jest.advanceTimersByTimeAsync(MIN_STEP_DURATION_MS);

// IDE is not opened
expect(mockLocationReplace).not.toHaveBeenCalled();
expect(tabManager.replace).not.toHaveBeenCalled();
});
});

Expand Down
Loading

0 comments on commit 9f59c7a

Please sign in to comment.