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 { , , , + , ], }; }