Skip to content

Commit

Permalink
Show warning banner on user namespace FailedScheduling event (#1211)
Browse files Browse the repository at this point in the history
Show the notification banner on the workspace startup screen, informing user that workspace startup would take longer due to a new node being provisioned.
  • Loading branch information
vinokurig authored Oct 15, 2024
1 parent 5a39299 commit 50072a8
Show file tree
Hide file tree
Showing 6 changed files with 221 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*
* Copyright (c) 2018-2024 Red Hat, Inc.
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
*/

import React from 'react';

export default class BannerAlertNoNodeAvailable extends React.PureComponent {
public render(): React.ReactElement {
return <div>Mock BannerAlertNoNodeAvailable component</div>;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/*
* Copyright (c) 2018-2024 Red Hat, Inc.
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
*/

import { screen, waitFor } from '@testing-library/react';
import React from 'react';
import { Provider } from 'react-redux';
import { Store } from 'redux';

import BannerAlertNoNodeAvailable from '@/components/BannerAlert/NoNodeAvailable';
import getComponentRenderer from '@/services/__mocks__/getComponentRenderer';
import { DevWorkspaceBuilder } from '@/store/__mocks__/devWorkspaceBuilder';
import { FakeStoreBuilder } from '@/store/__mocks__/storeBuilder';

const { renderComponent } = getComponentRenderer(getComponent);
const text =
'"FailedScheduling" event occurred. If cluster autoscaler is enabled it might be provisioning a new node now and workspace startup will take longer than usual.';

describe('BannerAlertNoNodeAvailable component', () => {
it('should show alert when failedScheduling event is received and hide alert when workspace has started', async () => {
const { reRenderComponent } = renderComponent(new FakeStoreBuilder().build());

const events = [
{
reason: 'FailedScheduling',
message: 'No preemption victims found for incoming pod',
metadata: { uid: 'uid' },
} as any,
];
const store = new FakeStoreBuilder().withEvents({ events }).build();
reRenderComponent(store);

await waitFor(() => expect(screen.queryAllByText(text).length).toEqual(1));
});

it('should hide alert when workspace has started', async () => {
const { reRenderComponent } = renderComponent(new FakeStoreBuilder().build());

const events = [
{
reason: 'FailedScheduling',
message: 'No preemption victims found for incoming pod',
metadata: { uid: 'uid' },
} as any,
];
const workspaces = [
new DevWorkspaceBuilder().withStatus({ phase: 'STARTING', devworkspaceId: 'id' }).build(),
];
const store = new FakeStoreBuilder()
.withEvents({ events })
.withDevWorkspaces({ workspaces })
.build();
reRenderComponent(store);

await waitFor(() => expect(screen.queryAllByText(text).length).toEqual(1));

const nextWorkspaces = [
new DevWorkspaceBuilder().withStatus({ phase: 'RUNNING', devworkspaceId: 'id' }).build(),
];
const nextStore = new FakeStoreBuilder()
.withEvents({ events })
.withDevWorkspaces({ workspaces: nextWorkspaces })
.build();
reRenderComponent(nextStore);

await waitFor(() => expect(screen.queryAllByText(text).length).toEqual(0));
});
});

function getComponent(store: Store<any, any>) {
return (
<Provider store={store}>
<BannerAlertNoNodeAvailable />
</Provider>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/*
* Copyright (c) 2018-2024 Red Hat, Inc.
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
*/

import { Banner } from '@patternfly/react-core';
import React from 'react';
import { connect, ConnectedProps } from 'react-redux';

import { container } from '@/inversify.config';
import { WebsocketClient } from '@/services/backend-client/websocketClient';
import { DevWorkspaceStatus } from '@/services/helpers/types';
import { AppState } from '@/store';
import { selectAllEvents } from '@/store/Events/selectors';
import { selectAllWorkspaces } from '@/store/Workspaces/selectors';

type Props = MappedProps;

type State = {
startingWorkspaces: string[];
};

class BannerAlertNoNodeAvailable extends React.PureComponent<Props, State> {
private readonly websocketClient: WebsocketClient;

constructor(props: Props) {
super(props);
this.websocketClient = container.get(WebsocketClient);
this.state = {
startingWorkspaces: [],
};
}

componentDidUpdate(prevProps: Readonly<Props>) {
this.handleAllEventsChange(prevProps);
this.handleAllWorkspacesChange(prevProps);
}

private handleAllEventsChange(prevProps: Readonly<Props>) {
const allEvents = this.props.allEvents;
const prevAllEvents = prevProps.allEvents;

if (JSON.stringify(allEvents) === JSON.stringify(prevAllEvents)) {
return;
}

const event = allEvents[allEvents.length - 1];
if (
event.message !== undefined &&
event.reason === 'FailedScheduling' &&
event.message.indexOf('No preemption victims found for incoming pod') > -1 &&
this.state.startingWorkspaces.length === 0
) {
this.setState({ startingWorkspaces: [event.metadata!.uid!] });
}
}

private handleAllWorkspacesChange(prevProps: Readonly<Props>) {
const prevAllWorkspaces = prevProps.allWorkspaces;
const allWorkspaces = this.props.allWorkspaces;

if (JSON.stringify(allWorkspaces) === JSON.stringify(prevAllWorkspaces)) {
return;
}

if (
allWorkspaces.some(
workspace =>
workspace.status === DevWorkspaceStatus.RUNNING &&
prevAllWorkspaces.find(
prevWorkspace =>
prevWorkspace.id === workspace.id &&
prevWorkspace.status === DevWorkspaceStatus.STARTING,
),
)
) {
this.setState({ startingWorkspaces: [] });
}
}

render() {
if (this.state.startingWorkspaces.length === 0) {
return null;
}

return (
<Banner className="pf-u-text-align-center" variant="warning">
&quot;FailedScheduling&quot; event occurred. If cluster autoscaler is enabled it might be
provisioning a new node now and workspace startup will take longer than usual.
</Banner>
);
}
}

const mapStateToProps = (state: AppState) => ({
allEvents: selectAllEvents(state),
allWorkspaces: selectAllWorkspaces(state),
});

const connector = connect(mapStateToProps);

type MappedProps = ConnectedProps<typeof connector>;
export default connector(BannerAlertNoNodeAvailable);
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,10 @@ exports[`BannerAlert snapshot 1`] = `
Mock BannerAlertCustomWarning component
</div>
</div>
<div>
<div>
Mock BannerAlertNoNodeAvailable component
</div>
</div>
</div>
`;
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ jest.mock('@/components/BannerAlert/Branding');
jest.mock('@/components/BannerAlert/Custom');
jest.mock('@/components/BannerAlert/NotSupportedBrowser');
jest.mock('@/components/BannerAlert/WebSocket');
jest.mock('@/components/BannerAlert/NoNodeAvailable');

const { createSnapshot } = getComponentRenderer(getComponent);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import React from 'react';

import BannerAlertBranding from '@/components/BannerAlert/Branding';
import BannerAlertCustomWarning from '@/components/BannerAlert/Custom';
import BannerAlertNoNodeAvailable from '@/components/BannerAlert/NoNodeAvailable';
import BannerAlertWebSocket from '@/components/BannerAlert/WebSocket';

type Props = unknown;
Expand All @@ -30,6 +31,7 @@ export class BannerAlert extends React.PureComponent<Props, State> {
<BannerAlertWebSocket key="BannerAlertWebSocket"></BannerAlertWebSocket>,
<BannerAlertBranding key="BannerAlertBranding"></BannerAlertBranding>,
<BannerAlertCustomWarning key="BannerAlertCustomWarning"></BannerAlertCustomWarning>,
<BannerAlertNoNodeAvailable key="BannerAlertNoNodeAvailable"></BannerAlertNoNodeAvailable>,
],
};
}
Expand Down

0 comments on commit 50072a8

Please sign in to comment.