diff --git a/packages/dashboard-frontend/src/components/BannerAlert/NoNodeAvailable/__mocks__/index.tsx b/packages/dashboard-frontend/src/components/BannerAlert/NoNodeAvailable/__mocks__/index.tsx
new file mode 100644
index 000000000..dddc380bb
--- /dev/null
+++ b/packages/dashboard-frontend/src/components/BannerAlert/NoNodeAvailable/__mocks__/index.tsx
@@ -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
Mock BannerAlertNoNodeAvailable component
;
+ }
+}
diff --git a/packages/dashboard-frontend/src/components/BannerAlert/NoNodeAvailable/__tests__/index.spec.tsx b/packages/dashboard-frontend/src/components/BannerAlert/NoNodeAvailable/__tests__/index.spec.tsx
new file mode 100644
index 000000000..99e3cb30c
--- /dev/null
+++ b/packages/dashboard-frontend/src/components/BannerAlert/NoNodeAvailable/__tests__/index.spec.tsx
@@ -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) {
+ return (
+
+
+
+ );
+}
diff --git a/packages/dashboard-frontend/src/components/BannerAlert/NoNodeAvailable/index.tsx b/packages/dashboard-frontend/src/components/BannerAlert/NoNodeAvailable/index.tsx
new file mode 100644
index 000000000..a3f9a528d
--- /dev/null
+++ b/packages/dashboard-frontend/src/components/BannerAlert/NoNodeAvailable/index.tsx
@@ -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 {
+ private readonly websocketClient: WebsocketClient;
+
+ constructor(props: Props) {
+ super(props);
+ this.websocketClient = container.get(WebsocketClient);
+ this.state = {
+ startingWorkspaces: [],
+ };
+ }
+
+ componentDidUpdate(prevProps: Readonly) {
+ this.handleAllEventsChange(prevProps);
+ this.handleAllWorkspacesChange(prevProps);
+ }
+
+ private handleAllEventsChange(prevProps: Readonly) {
+ 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) {
+ 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 (
+
+ "FailedScheduling" event occurred. If cluster autoscaler is enabled it might be
+ provisioning a new node now and workspace startup will take longer than usual.
+
+ );
+ }
+}
+
+const mapStateToProps = (state: AppState) => ({
+ allEvents: selectAllEvents(state),
+ allWorkspaces: selectAllWorkspaces(state),
+});
+
+const connector = connect(mapStateToProps);
+
+type MappedProps = ConnectedProps;
+export default connector(BannerAlertNoNodeAvailable);
diff --git a/packages/dashboard-frontend/src/components/BannerAlert/__tests__/__snapshots__/index.spec.tsx.snap b/packages/dashboard-frontend/src/components/BannerAlert/__tests__/__snapshots__/index.spec.tsx.snap
index 059d03ff7..f1887c158 100644
--- a/packages/dashboard-frontend/src/components/BannerAlert/__tests__/__snapshots__/index.spec.tsx.snap
+++ b/packages/dashboard-frontend/src/components/BannerAlert/__tests__/__snapshots__/index.spec.tsx.snap
@@ -17,5 +17,10 @@ exports[`BannerAlert snapshot 1`] = `
Mock BannerAlertCustomWarning component
+
+
+ Mock BannerAlertNoNodeAvailable component
+
+
`;
diff --git a/packages/dashboard-frontend/src/components/BannerAlert/__tests__/index.spec.tsx b/packages/dashboard-frontend/src/components/BannerAlert/__tests__/index.spec.tsx
index 027de9efb..13b9b1c5a 100644
--- a/packages/dashboard-frontend/src/components/BannerAlert/__tests__/index.spec.tsx
+++ b/packages/dashboard-frontend/src/components/BannerAlert/__tests__/index.spec.tsx
@@ -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);
diff --git a/packages/dashboard-frontend/src/components/BannerAlert/index.tsx b/packages/dashboard-frontend/src/components/BannerAlert/index.tsx
index bb7cea494..67476acf3 100644
--- a/packages/dashboard-frontend/src/components/BannerAlert/index.tsx
+++ b/packages/dashboard-frontend/src/components/BannerAlert/index.tsx
@@ -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;
@@ -30,6 +31,7 @@ export class BannerAlert extends React.PureComponent {
,
,
,
+ ,
],
};
}