diff --git a/packages/common/package.json b/packages/common/package.json index 4b5bda377..98a80ca78 100644 --- a/packages/common/package.json +++ b/packages/common/package.json @@ -28,7 +28,7 @@ "eslint-config-prettier": "^9.0.0", "eslint-plugin-notice": "^0.9.10", "eslint-plugin-prettier": "^5.0.0", - "jest": "^29.6.2", + "jest": "^29.7.0", "prettier": "^3.0.1", "rimraf": "^5.0.1", "ts-jest": "^29.1.1", diff --git a/packages/dashboard-backend/package.json b/packages/dashboard-backend/package.json index 09f6bc84a..d953bef5a 100644 --- a/packages/dashboard-backend/package.json +++ b/packages/dashboard-backend/package.json @@ -65,7 +65,7 @@ "eslint-plugin-prettier": "^5.0.0", "eslint-webpack-plugin": "^4.0.1", "file-loader": "^6.2.0", - "jest": "^29.6.2", + "jest": "^29.7.0", "json-schema": "^0.4.0", "nodemon": "^3.0.1", "prettier": "^3.0.2", diff --git a/packages/dashboard-frontend/src/Layout/ErrorBoundary/__tests__/index.spec.tsx b/packages/dashboard-frontend/src/Layout/ErrorBoundary/__tests__/index.spec.tsx index 3adad8e11..58caff090 100644 --- a/packages/dashboard-frontend/src/Layout/ErrorBoundary/__tests__/index.spec.tsx +++ b/packages/dashboard-frontend/src/Layout/ErrorBoundary/__tests__/index.spec.tsx @@ -37,8 +37,9 @@ export class NoResourceComponent extends React.Component { // mute the outputs console.error = jest.fn(); +const mockTestBackends = jest.fn(); function wrapComponent(componentToWrap: React.ReactNode) { - return {componentToWrap}; + return {componentToWrap}; } describe('Error boundary', () => { @@ -66,6 +67,7 @@ describe('Error boundary', () => { const showDetailsAction = screen.getByRole('button', { name: 'View stack' }); userEvent.click(showDetailsAction); + expect(mockTestBackends).not.toHaveBeenCalled(); expect(screen.queryByText('in BadComponent', { exact: false })).toBeTruthy(); expect(screen.queryByText('in ErrorBoundary', { exact: false })).toBeTruthy(); @@ -98,6 +100,7 @@ describe('Error boundary', () => { const errorBoundary = wrapComponent(); render(errorBoundary); + expect(mockTestBackends).toHaveBeenCalledWith('Loading chunk 23 failed.'); expect( screen.queryByText('The application has been likely updated on the server.', { exact: false, @@ -111,6 +114,7 @@ describe('Error boundary', () => { const errorBoundary = wrapComponent(); render(errorBoundary); + expect(mockTestBackends).toHaveBeenCalledWith('Loading chunk 23 failed.'); expect( screen.queryByText('Refreshing a page to get newer resources in', { exact: false }), ).toBeTruthy(); @@ -127,6 +131,7 @@ describe('Error boundary', () => { const stopCountdownAction = screen.getByRole('button', { name: 'Stop countdown' }); userEvent.click(stopCountdownAction); + expect(mockTestBackends).toHaveBeenCalledWith('Loading chunk 23 failed.'); expect( screen.queryByText('Refreshing a page to get newer resources in', { exact: false }), ).toBeFalsy(); @@ -148,6 +153,7 @@ describe('Error boundary', () => { jest.advanceTimersByTime(35000); + expect(mockTestBackends).toHaveBeenCalledWith('Loading chunk 23 failed.'); expect(window.location.reload).toHaveBeenCalled(); expect(window.location.reload).toHaveBeenCalledTimes(1); }); @@ -161,6 +167,7 @@ describe('Error boundary', () => { userEvent.click(reloadNowAction); userEvent.click(reloadNowAction); + expect(mockTestBackends).toHaveBeenCalledWith('Loading chunk 23 failed.'); expect(window.location.reload).toHaveBeenCalled(); expect(window.location.reload).toHaveBeenCalledTimes(3); }); @@ -185,6 +192,7 @@ describe('Error boundary', () => { window.dispatchEvent(new Event('beforeunload')); render(errorBoundary); + expect(mockTestBackends).toHaveBeenCalledWith('Loading chunk 23 failed.'); expect( screen.queryByText( 'Contact an administrator if refreshing continues after the next load.', @@ -225,6 +233,7 @@ describe('Error boundary', () => { window.dispatchEvent(new Event('beforeunload')); render(goodComponent); + expect(mockTestBackends).toHaveBeenCalledWith('Loading chunk 23 failed.'); expect(sessionStorage.getItem(STORAGE_KEY_RELOAD_NUMBER)).toBeNull(); }); }); diff --git a/packages/dashboard-frontend/src/Layout/ErrorBoundary/index.tsx b/packages/dashboard-frontend/src/Layout/ErrorBoundary/index.tsx index 527ab24a5..faeccf21c 100644 --- a/packages/dashboard-frontend/src/Layout/ErrorBoundary/index.tsx +++ b/packages/dashboard-frontend/src/Layout/ErrorBoundary/index.tsx @@ -28,7 +28,9 @@ export const STORAGE_KEY_RELOAD_NUMBER = 'UD:ErrorBoundary:reloaded'; const RELOAD_TIMEOUT_SEC = 30; const RELOADS_FOR_EXTENDED_MESSAGE = 2; -type Props = PropsWithChildren; +type Props = PropsWithChildren & { + testBackends: (error?: string) => void; +}; type State = { hasError: boolean; error?: Error; @@ -43,7 +45,6 @@ type State = { export class ErrorBoundary extends React.PureComponent { private readonly toDispose = new DisposableCollection(); - private notFoundCallback: (error?: Error) => void; constructor(props: Props) { super(props); @@ -75,9 +76,7 @@ export class ErrorBoundary extends React.PureComponent { }); if (this.testResourceNotFound(error)) { - if (this.notFoundCallback) { - this.notFoundCallback(error); - } + this.props.testBackends(error.message); this.setState({ shouldReload: true, }); @@ -99,13 +98,6 @@ export class ErrorBoundary extends React.PureComponent { this.toDispose.dispose(); } - /** - * This method is used from parent component by reference. - */ - public setCallback(notFoundCallback: (error?: Error) => void): void { - this.notFoundCallback = notFoundCallback; - } - private testResourceNotFound(error: Error): boolean { return /loading chunk [\d]+ failed/i.test(error.message); } diff --git a/packages/dashboard-frontend/src/Layout/index.tsx b/packages/dashboard-frontend/src/Layout/index.tsx index 1a8f34b5c..17d6e9e4b 100644 --- a/packages/dashboard-frontend/src/Layout/index.tsx +++ b/packages/dashboard-frontend/src/Layout/index.tsx @@ -31,6 +31,7 @@ import { ToggleBarsContext } from '../contexts/ToggleBars'; import { signOut } from '../services/helpers/login'; import { selectDashboardLogo } from '../store/ServerConfig/selectors'; import * as SanityCheckStore from '../store/SanityCheck'; +import { selectSanityCheckError } from '../store/SanityCheck/selectors'; const IS_MANAGED_SIDEBAR = false; @@ -46,13 +47,10 @@ type State = { export class Layout extends React.PureComponent { @lazyInject(IssuesReporterService) private readonly issuesReporterService: IssuesReporterService; - private readonly errorBoundaryRef: React.RefObject; constructor(props: Props) { super(props); - this.errorBoundaryRef = React.createRef(); - this.state = { isHeaderVisible: true, isSidebarVisible: true, @@ -89,11 +87,13 @@ export class Layout extends React.PureComponent { if (matchFactoryLoaderPath !== null || matchIdeLoaderPath !== null) { this.hideAllBars(); } - this.errorBoundaryRef.current?.setCallback((error?: Error) => { - if (error?.message) { - console.error(error.message); + } + private testBackends(error?: string): void { + this.props.testBackends().catch(() => { + if (error) { + console.error(error); } - this.props.testBackends(); + console.error('Error testing backends:', this.props.sanityCheckError); }); } @@ -145,7 +145,7 @@ export class Layout extends React.PureComponent { } isManagedSidebar={IS_MANAGED_SIDEBAR} > - + this.testBackends(error)}> {this.props.children} @@ -159,6 +159,7 @@ export class Layout extends React.PureComponent { const mapStateToProps = (state: AppState) => ({ branding: selectBranding(state), dashboardLogo: selectDashboardLogo(state), + sanityCheckError: selectSanityCheckError(state), }); const connector = connect(mapStateToProps, SanityCheckStore.actionCreators); diff --git a/packages/dashboard-frontend/src/services/helpers/types.ts b/packages/dashboard-frontend/src/services/helpers/types.ts index 80a2b4af6..8c4cf7511 100644 --- a/packages/dashboard-frontend/src/services/helpers/types.ts +++ b/packages/dashboard-frontend/src/services/helpers/types.ts @@ -13,6 +13,7 @@ import { AlertVariant } from '@patternfly/react-core'; import * as React from 'react'; import devfileApi from '../devfileApi'; +import * as cheApi from '@eclipse-che/api'; export type ActionCallback = { title: string; @@ -35,7 +36,7 @@ export interface FactoryResolver { devfile: devfileApi.Devfile | che.WorkspaceDevfile; location?: string; scm_info?: FactoryResolverScmInfo; - links: api.che.core.rest.Link[]; + links: cheApi.che.core.rest.Link[]; } export type FactoryResolverScmInfo = { diff --git a/packages/dashboard-frontend/src/services/workspace-client/__tests__/helpers.spec.ts b/packages/dashboard-frontend/src/services/workspace-client/__tests__/helpers.spec.ts index a3cf0a4c3..527384f0a 100644 --- a/packages/dashboard-frontend/src/services/workspace-client/__tests__/helpers.spec.ts +++ b/packages/dashboard-frontend/src/services/workspace-client/__tests__/helpers.spec.ts @@ -13,6 +13,7 @@ import { getCustomEditor, hasLoginPage, + getErrorMessage, isForbidden, isInternalServerError, isUnauthorized, @@ -24,6 +25,28 @@ import devfileApi from '../../devfileApi'; import { FakeStoreBuilder } from '../../../store/__mocks__/storeBuilder'; describe('Workspace-client helpers', () => { + describe('get an error message', () => { + it('should return the default error message', () => { + expect(getErrorMessage(undefined)).toEqual('Check the browser logs message.'); + }); + it('should return the unknown error message', () => { + expect(getErrorMessage({})).toEqual('Unexpected error type. Please report a bug.'); + }); + it('should return unknown an error message', () => { + expect( + getErrorMessage({ + response: { + status: 401, + }, + request: { + responseURL: 'http://dummyurl.com', + }, + }), + ).toEqual( + 'HTTP Error code 401. Endpoint which throws an error http://dummyurl.com. Check the browser logs message.', + ); + }); + }); describe('checks for HTML login page in response data', () => { it('should return false without HTML login page', () => { expect( diff --git a/packages/dashboard-frontend/src/services/workspace-client/helpers.ts b/packages/dashboard-frontend/src/services/workspace-client/helpers.ts index d1befeef4..c64cc1df6 100644 --- a/packages/dashboard-frontend/src/services/workspace-client/helpers.ts +++ b/packages/dashboard-frontend/src/services/workspace-client/helpers.ts @@ -31,7 +31,11 @@ export function getErrorMessage(error: unknown): string { const code = response?.status ? response?.status : response?.request?.status; const endpoint = request?.responseURL ? request?.responseURL : response?.request?.responseURL; - errorMessage = `HTTP Error code ${code}. Endpoint which throw an error ${endpoint}. ${errorMessage}`; + if (!code || !endpoint) { + return 'Unexpected error type. Please report a bug.'; + } + + errorMessage = `HTTP Error code ${code}. Endpoint which throws an error ${endpoint}. ${errorMessage}`; } if (isUnauthorized(error) || isForbidden(error)) { diff --git a/packages/dashboard-frontend/src/store/DevfileRegistries/__tests__/index.spec.ts b/packages/dashboard-frontend/src/store/DevfileRegistries/__tests__/index.spec.ts index 58fa8b3f8..ce2a9380c 100644 --- a/packages/dashboard-frontend/src/store/DevfileRegistries/__tests__/index.spec.ts +++ b/packages/dashboard-frontend/src/store/DevfileRegistries/__tests__/index.spec.ts @@ -58,6 +58,14 @@ describe('Devfile registries', () => { const actions = store.getActions(); const expectedActions: devfileRegistriesStore.KnownAction[] = [ + { + type: devfileRegistriesStore.Type.REQUEST_REGISTRY_METADATA, + check: AUTHORIZED, + }, + { + type: devfileRegistriesStore.Type.REQUEST_REGISTRY_METADATA, + check: AUTHORIZED, + }, { type: devfileRegistriesStore.Type.REQUEST_REGISTRY_METADATA, check: AUTHORIZED, @@ -116,6 +124,14 @@ describe('Devfile registries', () => { const actions = store.getActions(); const expectedActions: devfileRegistriesStore.KnownAction[] = [ + { + type: devfileRegistriesStore.Type.REQUEST_REGISTRY_METADATA, + check: AUTHORIZED, + }, + { + type: devfileRegistriesStore.Type.REQUEST_REGISTRY_METADATA, + check: AUTHORIZED, + }, { type: devfileRegistriesStore.Type.REQUEST_REGISTRY_METADATA, check: AUTHORIZED, diff --git a/packages/dashboard-frontend/src/store/DevfileRegistries/index.ts b/packages/dashboard-frontend/src/store/DevfileRegistries/index.ts index 1363e8628..dc6128eca 100644 --- a/packages/dashboard-frontend/src/store/DevfileRegistries/index.ts +++ b/packages/dashboard-frontend/src/store/DevfileRegistries/index.ts @@ -19,6 +19,7 @@ import fetchAndUpdateDevfileSchema from './fetchAndUpdateDevfileSchema'; import devfileApi from '../../services/devfileApi'; import { fetchResources, loadResourcesContent } from '../../services/registry/resources'; import { AUTHORIZED, SanityCheckAction } from '../sanityCheckMiddleware'; +import { selectAsyncIsAuthorized, selectSanityCheckError } from '../SanityCheck/selectors'; export const DEFAULT_REGISTRY = '/dashboard/devfile-registry/'; @@ -170,12 +171,15 @@ export const actionCreators: ActionCreators = { */ requestRegistriesMetadata: (registryUrls: string, isExternal: boolean): AppThunk> => - async (dispatch): Promise => { - await dispatch({ type: Type.REQUEST_REGISTRY_METADATA, check: AUTHORIZED }); - + async (dispatch, getState): Promise => { const registries: string[] = registryUrls.split(' '); const promises = registries.map(async url => { try { + await dispatch({ type: Type.REQUEST_REGISTRY_METADATA, check: AUTHORIZED }); + if (!(await selectAsyncIsAuthorized(getState()))) { + const error = selectSanityCheckError(getState()); + throw new Error(error); + } const metadata: che.DevfileMetaData[] = await fetchRegistryMetadata(url, isExternal); if (!Array.isArray(metadata) || metadata.length === 0) { return; @@ -205,9 +209,13 @@ export const actionCreators: ActionCreators = { requestDevfile: (url: string): AppThunk> => - async (dispatch): Promise => { - await dispatch({ type: Type.REQUEST_DEVFILE, check: AUTHORIZED }); + async (dispatch, getState): Promise => { try { + await dispatch({ type: Type.REQUEST_DEVFILE, check: AUTHORIZED }); + if (!(await selectAsyncIsAuthorized(getState()))) { + const error = selectSanityCheckError(getState()); + throw new Error(error); + } const devfile = await fetchDevfile(url); dispatch({ type: Type.RECEIVE_DEVFILE, devfile, url }); return devfile; @@ -218,10 +226,13 @@ export const actionCreators: ActionCreators = { requestResources: (resourcesUrl: string): AppThunk> => - async (dispatch): Promise => { - await dispatch({ type: Type.REQUEST_RESOURCES, check: AUTHORIZED }); - + async (dispatch, getState): Promise => { try { + await dispatch({ type: Type.REQUEST_RESOURCES, check: AUTHORIZED }); + if (!(await selectAsyncIsAuthorized(getState()))) { + const error = selectSanityCheckError(getState()); + throw new Error(error); + } const resourcesContent = await fetchResources(resourcesUrl); const resources = loadResourcesContent(resourcesContent); @@ -259,9 +270,13 @@ export const actionCreators: ActionCreators = { requestJsonSchema: (): AppThunk => - async (dispatch): Promise => { - await dispatch({ type: Type.REQUEST_SCHEMA, check: AUTHORIZED }); + async (dispatch, getState): Promise => { try { + await dispatch({ type: Type.REQUEST_SCHEMA, check: AUTHORIZED }); + if (!(await selectAsyncIsAuthorized(getState()))) { + const error = selectSanityCheckError(getState()); + throw new Error(error); + } const schemav200 = await fetchAndUpdateDevfileSchema('2.0.0'); const schemav210 = await fetchAndUpdateDevfileSchema('2.1.0'); const schemav220 = await fetchAndUpdateDevfileSchema('2.2.0'); diff --git a/packages/dashboard-frontend/src/store/DockerConfig/index.ts b/packages/dashboard-frontend/src/store/DockerConfig/index.ts index e708eb668..c1f4ff8ec 100644 --- a/packages/dashboard-frontend/src/store/DockerConfig/index.ts +++ b/packages/dashboard-frontend/src/store/DockerConfig/index.ts @@ -59,19 +59,13 @@ export const actionCreators: ActionCreators = { requestCredentials: (): AppThunk> => async (dispatch, getState): Promise => { - dispatch({ type: Type.REQUEST_DEVWORKSPACE_CREDENTIALS, check: AUTHORIZED }); - const state = getState(); - if (!(await selectAsyncIsAuthorized(getState()))) { - const error = selectSanityCheckError(getState()); - dispatch({ - type: Type.RECEIVE_DEVWORKSPACE_CREDENTIALS_ERROR, - error, - }); - throw new Error(error); - } - - const namespace = selectDefaultNamespace(state).name; + const namespace = selectDefaultNamespace(getState()).name; try { + await dispatch({ type: Type.REQUEST_DEVWORKSPACE_CREDENTIALS, check: AUTHORIZED }); + if (!(await selectAsyncIsAuthorized(getState()))) { + const error = selectSanityCheckError(getState()); + throw new Error(error); + } const { registries, resourceVersion } = await getDockerConfig(namespace); dispatch({ type: Type.SET_DEVWORKSPACE_CREDENTIALS, @@ -91,19 +85,14 @@ export const actionCreators: ActionCreators = { updateCredentials: (registries: RegistryEntry[]): AppThunk> => async (dispatch, getState): Promise => { - dispatch({ type: Type.REQUEST_DEVWORKSPACE_CREDENTIALS, check: AUTHORIZED }); const state = getState(); - if (!(await selectAsyncIsAuthorized(getState()))) { - const error = selectSanityCheckError(getState()); - dispatch({ - type: Type.RECEIVE_DEVWORKSPACE_CREDENTIALS_ERROR, - error, - }); - throw new Error(error); - } - const namespace = selectDefaultNamespace(state).name; try { + await dispatch({ type: Type.REQUEST_DEVWORKSPACE_CREDENTIALS, check: AUTHORIZED }); + if (!(await selectAsyncIsAuthorized(getState()))) { + const error = selectSanityCheckError(getState()); + throw new Error(error); + } const { resourceVersion } = await putDockerConfig( namespace, registries, diff --git a/packages/dashboard-frontend/src/store/FactoryResolver/index.ts b/packages/dashboard-frontend/src/store/FactoryResolver/index.ts index 87b86a3e5..aa52b9eeb 100644 --- a/packages/dashboard-frontend/src/store/FactoryResolver/index.ts +++ b/packages/dashboard-frontend/src/store/FactoryResolver/index.ts @@ -30,6 +30,7 @@ import { CHE_EDITOR_YAML_PATH } from '../../services/workspace-client/helpers'; import { FactoryParams } from '../../services/helpers/factoryFlow/buildFactoryParams'; import { getFactoryResolver } from '../../services/backend-client/factoryApi'; import { selectAsyncIsAuthorized, selectSanityCheckError } from '../SanityCheck/selectors'; +import * as cheApi from '@eclipse-che/api'; export type OAuthResponse = { attributes: { @@ -88,7 +89,7 @@ export type ActionCreators = { }; export async function grabLink( - links: api.che.core.rest.Link, + links: cheApi.che.core.rest.Link[], filename: string, ): Promise { // handle servers not yet providing links @@ -96,8 +97,8 @@ export async function grabLink( return undefined; } // grab the one matching - const foundLink = links.find(link => link.href.includes(`file=${filename}`)); - if (!foundLink) { + const foundLink = links.find(link => link.href?.includes(`file=${filename}`)); + if (!foundLink || !foundLink.href) { return undefined; } @@ -130,16 +131,6 @@ export const actionCreators: ActionCreators = { factoryParams: Partial = {}, ): AppThunk> => async (dispatch, getState): Promise => { - dispatch({ type: 'REQUEST_FACTORY_RESOLVER', check: AUTHORIZED }); - if (!(await selectAsyncIsAuthorized(getState()))) { - const error = selectSanityCheckError(getState()); - dispatch({ - type: 'RECEIVE_FACTORY_RESOLVER_ERROR', - error, - }); - throw new Error(error); - } - const state = getState(); const namespace = selectDefaultNamespace(state).name; const optionalFilesContent = {}; @@ -167,6 +158,11 @@ export const actionCreators: ActionCreators = { }; try { + await dispatch({ type: 'REQUEST_FACTORY_RESOLVER', check: AUTHORIZED }); + if (!(await selectAsyncIsAuthorized(getState()))) { + const error = selectSanityCheckError(getState()); + throw new Error(error); + } let data: FactoryResolver; if (isDevfileRegistryLocation(location)) { data = await getYamlResolver(namespace, location); diff --git a/packages/dashboard-frontend/src/store/GitConfig/index.ts b/packages/dashboard-frontend/src/store/GitConfig/index.ts index 2181cba47..479a469e6 100644 --- a/packages/dashboard-frontend/src/store/GitConfig/index.ts +++ b/packages/dashboard-frontend/src/store/GitConfig/index.ts @@ -29,19 +29,14 @@ export const actionCreators: ActionCreators = { requestGitConfig: (): AppThunk> => async (dispatch, getState): Promise => { - dispatch({ type: Type.REQUEST_GITCONFIG, check: AUTHORIZED }); - if (!(await selectAsyncIsAuthorized(getState()))) { - const error = selectSanityCheckError(getState()); - dispatch({ - type: Type.RECEIVE_GITCONFIG_ERROR, - error, - }); - throw new Error(error); - } - const state = getState(); const namespace = selectDefaultNamespace(state).name; try { + await dispatch({ type: Type.REQUEST_GITCONFIG, check: AUTHORIZED }); + if (!(await selectAsyncIsAuthorized(getState()))) { + const error = selectSanityCheckError(getState()); + throw new Error(error); + } const config = await fetchGitConfig(namespace); dispatch({ type: Type.RECEIVE_GITCONFIG, @@ -68,16 +63,6 @@ export const actionCreators: ActionCreators = { updateGitConfig: (changedGitConfig: GitConfigUser): AppThunk> => async (dispatch, getState): Promise => { - dispatch({ type: Type.REQUEST_GITCONFIG, check: AUTHORIZED }); - if (!(await selectAsyncIsAuthorized(getState()))) { - const error = selectSanityCheckError(getState()); - dispatch({ - type: Type.RECEIVE_GITCONFIG_ERROR, - error, - }); - throw new Error(error); - } - const state = getState(); const namespace = selectDefaultNamespace(state).name; const { gitConfig } = state; @@ -87,6 +72,11 @@ export const actionCreators: ActionCreators = { }, } as api.IGitConfig); try { + await dispatch({ type: Type.REQUEST_GITCONFIG, check: AUTHORIZED }); + if (!(await selectAsyncIsAuthorized(getState()))) { + const error = selectSanityCheckError(getState()); + throw new Error(error); + } const updated = await patchGitConfig(namespace, gitconfig); dispatch({ type: Type.RECEIVE_GITCONFIG, diff --git a/packages/dashboard-frontend/src/store/GitOauthConfig/types.ts b/packages/dashboard-frontend/src/store/GitOauthConfig/types.ts index 5a1bef2b1..c470f1961 100644 --- a/packages/dashboard-frontend/src/store/GitOauthConfig/types.ts +++ b/packages/dashboard-frontend/src/store/GitOauthConfig/types.ts @@ -11,9 +11,10 @@ */ import { api as commonApi } from '@eclipse-che/common'; +import * as cheApi from '@eclipse-che/api'; export interface IGitOauth { name: commonApi.GitOauthProvider; endpointUrl: string; - links?: api.che.core.rest.Link[]; + links?: cheApi.che.core.rest.Link[]; } diff --git a/packages/dashboard-frontend/src/store/InfrastructureNamespaces/index.ts b/packages/dashboard-frontend/src/store/InfrastructureNamespaces/index.ts index 66b3fcb7b..94a6a2e5c 100644 --- a/packages/dashboard-frontend/src/store/InfrastructureNamespaces/index.ts +++ b/packages/dashboard-frontend/src/store/InfrastructureNamespaces/index.ts @@ -16,6 +16,7 @@ import { AppThunk } from '..'; import { createObject } from '../helpers'; import { AUTHORIZED, SanityCheckAction } from '../sanityCheckMiddleware'; import { getKubernetesNamespace } from '../../services/backend-client/kubernetesNamespaceApi'; +import { selectAsyncIsAuthorized, selectSanityCheckError } from '../SanityCheck/selectors'; export interface State { isLoading: boolean; @@ -46,10 +47,13 @@ export type ActionCreators = { export const actionCreators: ActionCreators = { requestNamespaces: (): AppThunk>> => - async (dispatch): Promise> => { - await dispatch({ type: 'REQUEST_NAMESPACES', check: AUTHORIZED }); - + async (dispatch, getState): Promise> => { try { + await dispatch({ type: 'REQUEST_NAMESPACES', check: AUTHORIZED }); + if (!(await selectAsyncIsAuthorized(getState()))) { + const error = selectSanityCheckError(getState()); + throw new Error(error); + } const namespaces = await getKubernetesNamespace(); dispatch({ type: 'RECEIVE_NAMESPACES', diff --git a/packages/dashboard-frontend/src/store/Plugins/chePlugins/index.ts b/packages/dashboard-frontend/src/store/Plugins/chePlugins/index.ts index 545702b32..4b9a0783b 100644 --- a/packages/dashboard-frontend/src/store/Plugins/chePlugins/index.ts +++ b/packages/dashboard-frontend/src/store/Plugins/chePlugins/index.ts @@ -16,6 +16,7 @@ import common from '@eclipse-che/common'; import { AppThunk } from '../..'; import { createObject } from '../../helpers'; import { AUTHORIZED, SanityCheckAction } from '../../sanityCheckMiddleware'; +import { selectAsyncIsAuthorized, selectSanityCheckError } from '../../SanityCheck/selectors'; // create new instance of `axios` to avoid adding an authorization header const axiosInstance = axios.create(); @@ -49,10 +50,13 @@ export type ActionCreators = { export const actionCreators: ActionCreators = { requestPlugins: (registryUrl: string): AppThunk> => - async (dispatch): Promise => { - await dispatch({ type: 'REQUEST_PLUGINS', check: AUTHORIZED }); - + async (dispatch, getState): Promise => { try { + await dispatch({ type: 'REQUEST_PLUGINS', check: AUTHORIZED }); + if (!(await selectAsyncIsAuthorized(getState()))) { + const error = selectSanityCheckError(getState()); + throw new Error(error); + } const response = await axiosInstance.request({ method: 'GET', url: `${registryUrl}/plugins/`, diff --git a/packages/dashboard-frontend/src/store/User/Id/index.ts b/packages/dashboard-frontend/src/store/User/Id/index.ts index 4827bcfe6..7a697e6e7 100644 --- a/packages/dashboard-frontend/src/store/User/Id/index.ts +++ b/packages/dashboard-frontend/src/store/User/Id/index.ts @@ -16,6 +16,7 @@ import { fetchCheUserId } from '../../../services/che-user-id'; import { createObject } from '../../helpers'; import { AppThunk } from '../../index'; import { AUTHORIZED, SanityCheckAction } from '../../sanityCheckMiddleware'; +import { selectAsyncIsAuthorized, selectSanityCheckError } from '../../SanityCheck/selectors'; export interface State { cheUserId: string; @@ -52,10 +53,13 @@ export type ActionCreators = { export const actionCreators: ActionCreators = { requestCheUserId: (): AppThunk> => - async (dispatch): Promise => { - await dispatch({ type: Type.REQUEST_CHE_USER_ID, check: AUTHORIZED }); - + async (dispatch, getState): Promise => { try { + await dispatch({ type: Type.REQUEST_CHE_USER_ID, check: AUTHORIZED }); + if (!(await selectAsyncIsAuthorized(getState()))) { + const error = selectSanityCheckError(getState()); + throw new Error(error); + } const cheUserId = await fetchCheUserId(); dispatch({ type: Type.RECEIVE_CHE_USER_ID, diff --git a/packages/dashboard-frontend/src/store/User/Profile/index.ts b/packages/dashboard-frontend/src/store/User/Profile/index.ts index ccf0e7b47..f4ff0e890 100644 --- a/packages/dashboard-frontend/src/store/User/Profile/index.ts +++ b/packages/dashboard-frontend/src/store/User/Profile/index.ts @@ -18,6 +18,7 @@ import { fetchUserProfile } from '../../../services/backend-client/userProfileAp import { createObject } from '../../helpers'; import { AppThunk } from '../../index'; import { AUTHORIZED, SanityCheckAction } from '../../sanityCheckMiddleware'; +import { selectAsyncIsAuthorized, selectSanityCheckError } from '../../SanityCheck/selectors'; export interface State { userProfile: api.IUserProfile; @@ -57,10 +58,13 @@ export type ActionCreators = { export const actionCreators: ActionCreators = { requestUserProfile: (namespace: string): AppThunk> => - async (dispatch): Promise => { - await dispatch({ type: Type.REQUEST_USER_PROFILE, check: AUTHORIZED }); - + async (dispatch, getState): Promise => { try { + await dispatch({ type: Type.REQUEST_USER_PROFILE, check: AUTHORIZED }); + if (!(await selectAsyncIsAuthorized(getState()))) { + const error = selectSanityCheckError(getState()); + throw new Error(error); + } const userProfile = await fetchUserProfile(namespace); dispatch({ type: Type.RECEIVE_USER_PROFILE, diff --git a/packages/dashboard-frontend/src/store/Workspaces/devWorkspaces/index.ts b/packages/dashboard-frontend/src/store/Workspaces/devWorkspaces/index.ts index 43db44faa..d291052b3 100644 --- a/packages/dashboard-frontend/src/store/Workspaces/devWorkspaces/index.ts +++ b/packages/dashboard-frontend/src/store/Workspaces/devWorkspaces/index.ts @@ -58,6 +58,7 @@ import { selectDefaultDevfile } from '../../DevfileRegistries/selectors'; import * as DwApi from '../../../services/backend-client/devWorkspaceApi'; import { selectDefaultEditor } from '../../Plugins/devWorkspacePlugins/selectors'; import { DEVWORKSPACE_STORAGE_TYPE_ATTR } from '../../../services/devfileApi/devWorkspace/spec/template'; +import { selectAsyncIsAuthorized, selectSanityCheckError } from '../../SanityCheck/selectors'; export const onStatusChangeCallbacks = new Map void>(); @@ -193,9 +194,12 @@ export const actionCreators: ActionCreators = { requestWorkspaces: (): AppThunk> => async (dispatch, getState): Promise => { - await dispatch({ type: Type.REQUEST_DEVWORKSPACE, check: AUTHORIZED }); - try { + await dispatch({ type: Type.REQUEST_DEVWORKSPACE, check: AUTHORIZED }); + if (!(await selectAsyncIsAuthorized(getState()))) { + const error = selectSanityCheckError(getState()); + throw new Error(error); + } const defaultKubernetesNamespace = selectDefaultNamespace(getState()); const defaultNamespace = defaultKubernetesNamespace.name; const { workspaces, resourceVersion } = defaultNamespace @@ -239,10 +243,13 @@ export const actionCreators: ActionCreators = { requestWorkspace: (workspace: devfileApi.DevWorkspace): AppThunk> => - async (dispatch): Promise => { - await dispatch({ type: Type.REQUEST_DEVWORKSPACE, check: AUTHORIZED }); - + async (dispatch, getState): Promise => { try { + await dispatch({ type: Type.REQUEST_DEVWORKSPACE, check: AUTHORIZED }); + if (!(await selectAsyncIsAuthorized(getState()))) { + const error = selectSanityCheckError(getState()); + throw new Error(error); + } const namespace = workspace.metadata.namespace; const name = workspace.metadata.name; const update = await getDevWorkspaceClient().getWorkspaceByName(namespace, name); @@ -287,8 +294,12 @@ export const actionCreators: ActionCreators = { return; } await OAuthService.refreshTokenIfNeeded(workspace); - await dispatch({ type: Type.REQUEST_DEVWORKSPACE, check: AUTHORIZED }); try { + await dispatch({ type: Type.REQUEST_DEVWORKSPACE, check: AUTHORIZED }); + if (!(await selectAsyncIsAuthorized(getState()))) { + const error = selectSanityCheckError(getState()); + throw new Error(error); + } checkRunningWorkspacesLimit(getState()); if (workspace.metadata.annotations?.[DEVWORKSPACE_NEXT_START_ANNOTATION]) { @@ -432,10 +443,13 @@ export const actionCreators: ActionCreators = { terminateWorkspace: (workspace: devfileApi.DevWorkspace): AppThunk> => - async (dispatch): Promise => { - await dispatch({ type: Type.REQUEST_DEVWORKSPACE, check: AUTHORIZED }); - + async (dispatch, getState): Promise => { try { + await dispatch({ type: Type.REQUEST_DEVWORKSPACE, check: AUTHORIZED }); + if (!(await selectAsyncIsAuthorized(getState()))) { + const error = selectSanityCheckError(getState()); + throw new Error(error); + } const namespace = workspace.metadata.namespace; const name = workspace.metadata.name; await getDevWorkspaceClient().delete(namespace, name); @@ -460,10 +474,13 @@ export const actionCreators: ActionCreators = { updateWorkspaceAnnotation: (workspace: devfileApi.DevWorkspace): AppThunk> => - async (dispatch): Promise => { - await dispatch({ type: Type.REQUEST_DEVWORKSPACE, check: AUTHORIZED }); - + async (dispatch, getState): Promise => { try { + await dispatch({ type: Type.REQUEST_DEVWORKSPACE, check: AUTHORIZED }); + if (!(await selectAsyncIsAuthorized(getState()))) { + const error = selectSanityCheckError(getState()); + throw new Error(error); + } const updated = await getDevWorkspaceClient().updateAnnotation(workspace); dispatch({ type: Type.UPDATE_DEVWORKSPACE, @@ -483,10 +500,13 @@ export const actionCreators: ActionCreators = { updateWorkspace: (workspace: devfileApi.DevWorkspace): AppThunk> => - async (dispatch): Promise => { - await dispatch({ type: Type.REQUEST_DEVWORKSPACE, check: AUTHORIZED }); - + async (dispatch, getState): Promise => { try { + await dispatch({ type: Type.REQUEST_DEVWORKSPACE, check: AUTHORIZED }); + if (!(await selectAsyncIsAuthorized(getState()))) { + const error = selectSanityCheckError(getState()); + throw new Error(error); + } const updated = await getDevWorkspaceClient().update(workspace); dispatch({ type: Type.UPDATE_DEVWORKSPACE, @@ -518,9 +538,12 @@ export const actionCreators: ActionCreators = { const pluginRegistryInternalUrl = selectPluginRegistryInternalUrl(state); const defaultNamespace = defaultKubernetesNamespace.name; - await dispatch({ type: Type.REQUEST_DEVWORKSPACE, check: AUTHORIZED }); - try { + await dispatch({ type: Type.REQUEST_DEVWORKSPACE, check: AUTHORIZED }); + if (!(await selectAsyncIsAuthorized(getState()))) { + const error = selectSanityCheckError(getState()); + throw new Error(error); + } /* create a new DevWorkspace */ const createResp = await getDevWorkspaceClient().createDevWorkspace( defaultNamespace, @@ -611,9 +634,12 @@ export const actionCreators: ActionCreators = { let devWorkspaceResource: devfileApi.DevWorkspace; let devWorkspaceTemplateResource: devfileApi.DevWorkspaceTemplate; - await dispatch({ type: Type.REQUEST_DEVWORKSPACE, check: AUTHORIZED }); - try { + await dispatch({ type: Type.REQUEST_DEVWORKSPACE, check: AUTHORIZED }); + if (!(await selectAsyncIsAuthorized(getState()))) { + const error = selectSanityCheckError(getState()); + throw new Error(error); + } const response = await getEditor(defaultsEditor, dispatch, getState, pluginRegistryUrl); if (response.content) { editorContent = response.content; @@ -793,8 +819,6 @@ export const actionCreators: ActionCreators = { }, ): AppThunk> => async (dispatch, getState): Promise => { - await dispatch({ type: Type.REQUEST_DEVWORKSPACE, check: AUTHORIZED }); - const state = getState(); const pluginRegistryUrl = state.dwServerConfig.config.pluginRegistryURL; let devWorkspaceResource: devfileApi.DevWorkspace; @@ -843,6 +867,11 @@ export const actionCreators: ActionCreators = { } try { + await dispatch({ type: Type.REQUEST_DEVWORKSPACE, check: AUTHORIZED }); + if (!(await selectAsyncIsAuthorized(getState()))) { + const error = selectSanityCheckError(getState()); + throw new Error(error); + } const resourcesContent = await fetchResources({ pluginRegistryUrl, devfileContent: dump(devfile),