From df8cf4d332b56f7b1f7b2adc73345216610bfdb4 Mon Sep 17 00:00:00 2001 From: Oleksii Kurinnyi Date: Tue, 25 Oct 2022 18:08:19 +0300 Subject: [PATCH 1/4] feat: user profile API --- packages/common/src/dto/api.ts | 5 ++ packages/dashboard-backend/src/app.ts | 13 ++-- .../src/devworkspaceClient/__mocks__/index.ts | 6 +- .../__tests__/index.spec.ts | 2 + .../src/devworkspaceClient/index.ts | 50 +++++++-------- .../services/__tests__/userProfileApi.spec.ts | 63 +++++++++++++++++++ .../services/userProfileApi.ts | 44 +++++++++++++ .../src/devworkspaceClient/types/index.ts | 8 +++ .../routes/api/__tests__/userProfile.spec.ts | 40 ++++++++++++ .../__mocks__/getDevWorkspaceClient.ts | 8 +++ .../src/routes/api/userProfile.ts | 34 ++++++++++ 11 files changed, 238 insertions(+), 35 deletions(-) create mode 100644 packages/dashboard-backend/src/devworkspaceClient/services/__tests__/userProfileApi.spec.ts create mode 100644 packages/dashboard-backend/src/devworkspaceClient/services/userProfileApi.ts create mode 100644 packages/dashboard-backend/src/routes/api/__tests__/userProfile.spec.ts create mode 100644 packages/dashboard-backend/src/routes/api/userProfile.ts diff --git a/packages/common/src/dto/api.ts b/packages/common/src/dto/api.ts index 1738e2ac6..520a217d9 100644 --- a/packages/common/src/dto/api.ts +++ b/packages/common/src/dto/api.ts @@ -50,3 +50,8 @@ export interface IServerConfig { }; cheNamespace: string; } + +export interface IUserProfile { + email: string; + username: string; +} diff --git a/packages/dashboard-backend/src/app.ts b/packages/dashboard-backend/src/app.ts index 6a3f0f335..9a83cf948 100644 --- a/packages/dashboard-backend/src/app.ts +++ b/packages/dashboard-backend/src/app.ts @@ -28,6 +28,7 @@ import { registerDockerConfigRoutes } from './routes/api/dockerConfig'; import { registerKubeConfigRoute } from './routes/api/kubeConfig'; import { registerNamespacesRoute } from './routes/api/namespaces'; import { registerServerConfigRoute } from './routes/api/serverConfig'; +import { registerUserProfileRoute } from './routes/api/userProfile'; import { registerYamlResolverRoute } from './routes/api/yamlResolver'; import { registerFactoryAcceptanceRedirect } from './routes/factoryAcceptanceRedirect'; import { registerWorkspaceRedirect } from './routes/workspaceRedirect'; @@ -79,19 +80,21 @@ export default async function buildApp(server: FastifyInstance): Promise { registerNamespacesRoute(server); } - registerDevworkspacesRoutes(server); + registerClusterConfigRoute(server); + + registerClusterInfoRoute(server); registerDevWorkspaceTemplates(server); - registerDockerConfigRoutes(server); + registerDevworkspacesRoutes(server); - registerServerConfigRoute(server); + registerDockerConfigRoutes(server); registerKubeConfigRoute(server); - registerClusterInfoRoute(server); + registerServerConfigRoute(server); - registerClusterConfigRoute(server); + registerUserProfileRoute(server); registerYamlResolverRoute(server); } diff --git a/packages/dashboard-backend/src/devworkspaceClient/__mocks__/index.ts b/packages/dashboard-backend/src/devworkspaceClient/__mocks__/index.ts index fe663041d..0b8976419 100644 --- a/packages/dashboard-backend/src/devworkspaceClient/__mocks__/index.ts +++ b/packages/dashboard-backend/src/devworkspaceClient/__mocks__/index.ts @@ -15,9 +15,10 @@ import { IDevWorkspaceApi, IDevWorkspaceTemplateApi, IDockerConfigApi, - IServerConfigApi, IKubeConfigApi, INamespaceApi, + IServerConfigApi, + IUserProfileApi, } from '../types'; export class DevWorkspaceClient implements IDevWorkspaceClient { @@ -39,4 +40,7 @@ export class DevWorkspaceClient implements IDevWorkspaceClient { get namespaceApi(): INamespaceApi { throw new Error('Method not implemented.'); } + get userProfileApi(): IUserProfileApi { + throw new Error('Method not implemented.'); + } } diff --git a/packages/dashboard-backend/src/devworkspaceClient/__tests__/index.spec.ts b/packages/dashboard-backend/src/devworkspaceClient/__tests__/index.spec.ts index d273d2ca1..32efa8a47 100644 --- a/packages/dashboard-backend/src/devworkspaceClient/__tests__/index.spec.ts +++ b/packages/dashboard-backend/src/devworkspaceClient/__tests__/index.spec.ts @@ -19,6 +19,7 @@ import { DockerConfigApiService } from '../services/dockerConfigApi'; import { KubeConfigApiService } from '../services/kubeConfigApi'; import { NamespaceApiService } from '../services/namespaceApi'; import { ServerConfigApiService } from '../services/serverConfigApi'; +import { UserProfileApiService } from '../services/userProfileApi'; jest.mock('../services/devWorkspaceApi.ts'); @@ -43,5 +44,6 @@ describe('DevWorkspace client', () => { expect(client.namespaceApi).toBeInstanceOf(NamespaceApiService); expect(client.serverConfigApi).toBeInstanceOf(ServerConfigApiService); expect(client.devWorkspaceTemplateApi).toBeInstanceOf(DevWorkspaceTemplateApiService); + expect(client.userProfileApi).toBeInstanceOf(UserProfileApiService); }); }); diff --git a/packages/dashboard-backend/src/devworkspaceClient/index.ts b/packages/dashboard-backend/src/devworkspaceClient/index.ts index c4a0789d7..6e3e74be6 100644 --- a/packages/dashboard-backend/src/devworkspaceClient/index.ts +++ b/packages/dashboard-backend/src/devworkspaceClient/index.ts @@ -11,66 +11,58 @@ */ import * as k8s from '@kubernetes/client-node'; +import { DevWorkspaceApiService } from './services/devWorkspaceApi'; +import { DevWorkspaceTemplateApiService } from './services/devWorkspaceTemplateApi'; +import { DockerConfigApiService } from './services/dockerConfigApi'; +import { KubeConfigApiService } from './services/kubeConfigApi'; +import { NamespaceApiService } from './services/namespaceApi'; +import { ServerConfigApiService } from './services/serverConfigApi'; +import { UserProfileApiService } from './services/userProfileApi'; import { - IServerConfigApi, IDevWorkspaceApi, IDevWorkspaceClient, IDevWorkspaceTemplateApi, IDockerConfigApi, IKubeConfigApi, INamespaceApi, + IServerConfigApi, + IUserProfileApi, } from './types'; -import { DevWorkspaceTemplateApiService } from './services/devWorkspaceTemplateApi'; -import { DevWorkspaceApiService } from './services/devWorkspaceApi'; -import { DockerConfigApiService } from './services/dockerConfigApi'; -import { ServerConfigApiService } from './services/serverConfigApi'; -import { KubeConfigApiService } from './services/kubeConfigApi'; -import { NamespaceApiService } from './services/namespaceApi'; export * from './types'; export class DevWorkspaceClient implements IDevWorkspaceClient { - private apiEnabled: boolean | undefined; - - private readonly _apisApi: k8s.ApisApi; - private readonly _devWorkspaceTemplateApi: IDevWorkspaceTemplateApi; - private readonly _devworkspaceApi: IDevWorkspaceApi; - private readonly _dockerConfigApi: IDockerConfigApi; - private readonly _serverConfigApi: IServerConfigApi; - private readonly _kubeConfigApi: IKubeConfigApi; - private readonly _namespaceApi: INamespaceApi; + private readonly kubeConfig: k8s.KubeConfig; constructor(kc: k8s.KubeConfig) { - this._devWorkspaceTemplateApi = new DevWorkspaceTemplateApiService(kc); - this._devworkspaceApi = new DevWorkspaceApiService(kc); - this._dockerConfigApi = new DockerConfigApiService(kc); - this._serverConfigApi = new ServerConfigApiService(kc); - this._kubeConfigApi = new KubeConfigApiService(kc); - this._namespaceApi = new NamespaceApiService(kc); - this._apisApi = kc.makeApiClient(k8s.ApisApi); + this.kubeConfig = kc; } get devWorkspaceTemplateApi(): IDevWorkspaceTemplateApi { - return this._devWorkspaceTemplateApi; + return new DevWorkspaceTemplateApiService(this.kubeConfig); } get devworkspaceApi(): IDevWorkspaceApi { - return this._devworkspaceApi; + return new DevWorkspaceApiService(this.kubeConfig); } get dockerConfigApi(): IDockerConfigApi { - return this._dockerConfigApi; + return new DockerConfigApiService(this.kubeConfig); } get serverConfigApi(): IServerConfigApi { - return this._serverConfigApi; + return new ServerConfigApiService(this.kubeConfig); } get kubeConfigApi(): IKubeConfigApi { - return this._kubeConfigApi; + return new KubeConfigApiService(this.kubeConfig); } get namespaceApi(): INamespaceApi { - return this._namespaceApi; + return new NamespaceApiService(this.kubeConfig); + } + + get userProfileApi(): IUserProfileApi { + return new UserProfileApiService(this.kubeConfig); } } diff --git a/packages/dashboard-backend/src/devworkspaceClient/services/__tests__/userProfileApi.spec.ts b/packages/dashboard-backend/src/devworkspaceClient/services/__tests__/userProfileApi.spec.ts new file mode 100644 index 000000000..8c6bd6e71 --- /dev/null +++ b/packages/dashboard-backend/src/devworkspaceClient/services/__tests__/userProfileApi.spec.ts @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2018-2021 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 + */ + +/* eslint-disable @typescript-eslint/no-unused-vars */ + +import * as mockClient from '@kubernetes/client-node'; +import { CoreV1Api, V1Secret } from '@kubernetes/client-node'; +import { UserProfileApiService } from '../userProfileApi'; + +jest.mock('../../../helpers/getUserName.ts'); + +const userNamespace = 'user1-che'; + +describe('UserProfile API Service', () => { + let userProfileService: UserProfileApiService; + + beforeEach(() => { + const { KubeConfig } = mockClient; + const kubeConfig = new KubeConfig(); + + kubeConfig.makeApiClient = jest.fn().mockImplementation(_api => { + return { + readNamespacedSecret: (_name, _namespace) => { + return Promise.resolve(buildSecret()); + }, + } as CoreV1Api; + }); + + userProfileService = new UserProfileApiService(kubeConfig); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('user profile object', async () => { + const res = await userProfileService.getUserProfile(userNamespace); + expect(res).toEqual({ email: 'user1@che', username: 'user1' }); + }); +}); + +function buildSecret(): { body: V1Secret } { + return { + body: { + apiVersion: 'v1', + data: { + email: 'dXNlcjFAY2hl', + id: 'Q2dFeEVnVnNiMk5oYkE=', + name: 'dXNlcjE=', + }, + kind: 'Secret', + }, + }; +} diff --git a/packages/dashboard-backend/src/devworkspaceClient/services/userProfileApi.ts b/packages/dashboard-backend/src/devworkspaceClient/services/userProfileApi.ts new file mode 100644 index 000000000..788615154 --- /dev/null +++ b/packages/dashboard-backend/src/devworkspaceClient/services/userProfileApi.ts @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2018-2021 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 { api } from '@eclipse-che/common'; +import * as k8s from '@kubernetes/client-node'; +import { IUserProfileApi } from '../types'; +import { createError } from './helpers/createError'; + +const ERROR_LABEL = 'CORE_V1_API_ERROR'; +const USER_PROFILE_SECRET_NAME = 'user-profile'; + +export class UserProfileApiService implements IUserProfileApi { + private readonly coreV1API: k8s.CoreV1Api; + + constructor(kc: k8s.KubeConfig) { + this.coreV1API = kc.makeApiClient(k8s.CoreV1Api); + } + + async getUserProfile(namespace: string): Promise { + try { + const result = await this.coreV1API.readNamespacedSecret(USER_PROFILE_SECRET_NAME, namespace); + const data = result.body.data; + if (data === undefined) { + throw new Error('Data is empty'); + } + return { + username: Buffer.from(data.name, 'base64').toString(), + email: Buffer.from(data.email, 'base64').toString(), + }; + } catch (e) { + console.error('Unable to get user profile data:', e); + throw createError(e, ERROR_LABEL, 'Unable to get user profile data'); + } + } +} diff --git a/packages/dashboard-backend/src/devworkspaceClient/types/index.ts b/packages/dashboard-backend/src/devworkspaceClient/types/index.ts index 790ce2521..da333ccf6 100644 --- a/packages/dashboard-backend/src/devworkspaceClient/types/index.ts +++ b/packages/dashboard-backend/src/devworkspaceClient/types/index.ts @@ -201,6 +201,13 @@ export interface IKubeConfigApi { injectKubeConfig(namespace: string, devworkspaceId: string): Promise; } +export interface IUserProfileApi { + /** + * Returns user profile object that contains username and email. + */ + getUserProfile(namespace: string): Promise; +} + export type IDevWorkspaceCallbacks = { onModified: (workspace: V1alpha2DevWorkspace) => void; onDeleted: (workspaceId: string) => void; @@ -215,6 +222,7 @@ export interface IDevWorkspaceClient { serverConfigApi: IServerConfigApi; kubeConfigApi: IKubeConfigApi; namespaceApi: INamespaceApi; + userProfileApi: IUserProfileApi; } export interface IDevWorkspaceList { diff --git a/packages/dashboard-backend/src/routes/api/__tests__/userProfile.spec.ts b/packages/dashboard-backend/src/routes/api/__tests__/userProfile.spec.ts new file mode 100644 index 000000000..7dfe68cfc --- /dev/null +++ b/packages/dashboard-backend/src/routes/api/__tests__/userProfile.spec.ts @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2018-2021 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 { FastifyInstance } from 'fastify'; +import { baseApiPath } from '../../../constants/config'; +import { setup, teardown } from '../../../helpers/tests/appBuilder'; +import { stubUserProfile } from '../helpers/__mocks__/getDevWorkspaceClient'; + +jest.mock('../helpers/getDevWorkspaceClient.ts'); +jest.mock('../helpers/getToken.ts'); +jest.mock('../helpers/getServiceAccountToken.ts'); + +describe('UserProfile Route', () => { + let app: FastifyInstance; + const namespace = 'user-che'; + + beforeEach(async () => { + app = await setup(); + }); + + afterEach(() => { + teardown(app); + }); + + test('GET ${baseApiPath}/namespaces', async () => { + const res = await app.inject().get(`${baseApiPath}/userprofile/${namespace}`); + + expect(res.statusCode).toEqual(200); + expect(res.json()).toEqual(stubUserProfile); + }); +}); diff --git a/packages/dashboard-backend/src/routes/api/helpers/__mocks__/getDevWorkspaceClient.ts b/packages/dashboard-backend/src/routes/api/helpers/__mocks__/getDevWorkspaceClient.ts index 84bcc5d1b..ce6227619 100644 --- a/packages/dashboard-backend/src/routes/api/helpers/__mocks__/getDevWorkspaceClient.ts +++ b/packages/dashboard-backend/src/routes/api/helpers/__mocks__/getDevWorkspaceClient.ts @@ -71,6 +71,11 @@ export const stubDockerConfig = {}; export const stubNamespaces = ['user-che']; +export const stubUserProfile: api.IUserProfile = { + email: 'user1@che', + username: 'user1', +}; + export function getDevWorkspaceClient(_args: Parameters): ReturnType { return { serverConfigApi: { @@ -108,5 +113,8 @@ export function getDevWorkspaceClient(_args: Parameters): ReturnT listInNamespace: _namespace => Promise.resolve(stubDevWorkspaceTemplatesList), patch: (_namespace, _name, _patches) => Promise.resolve(stubDevWorkspaceTemplate), } as IDevWorkspaceTemplateApi, + userProfileApi: { + getUserProfile: _namespace => Promise.resolve(stubUserProfile), + }, } as DevWorkspaceClient; } diff --git a/packages/dashboard-backend/src/routes/api/userProfile.ts b/packages/dashboard-backend/src/routes/api/userProfile.ts new file mode 100644 index 000000000..1d276301b --- /dev/null +++ b/packages/dashboard-backend/src/routes/api/userProfile.ts @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2018-2021 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 { FastifyInstance, FastifyRequest } from 'fastify'; +import { baseApiPath } from '../../constants/config'; +import { namespacedSchema } from '../../constants/schemas'; +import { getSchema } from '../../services/helpers'; +import { restParams } from '../../typings/models'; +import { getDevWorkspaceClient } from './helpers/getDevWorkspaceClient'; +import { getServiceAccountToken } from './helpers/getServiceAccountToken'; + +const tags = ['UserProfile']; + +export function registerUserProfileRoute(server: FastifyInstance) { + server.get( + `${baseApiPath}/userprofile/:namespace`, + getSchema({ tags, params: namespacedSchema }), + async function (request: FastifyRequest) { + const { namespace } = request.params as restParams.INamespacedParam; + const serviceAccountToken = getServiceAccountToken(); + const { userProfileApi } = getDevWorkspaceClient(serviceAccountToken); + return userProfileApi.getUserProfile(namespace); + }, + ); +} From bc3993f417f89af9e2f90fa8fab59255eca2e66f Mon Sep 17 00:00:00 2001 From: Oleksii Kurinnyi Date: Wed, 26 Oct 2022 13:04:59 +0300 Subject: [PATCH 2/4] chore: get rid of che-server APIs: /user and /profile; hide the Account page. --- .../Tools/AboutMenu/__tests__/index.spec.tsx | 13 +- .../Layout/Header/Tools/AboutMenu/index.tsx | 25 +- .../Tools/UserMenu/__tests__/index.spec.tsx | 25 +- .../Layout/Header/Tools/UserMenu/index.tsx | 40 +--- .../__snapshots__/index.spec.tsx.snap | 5 - .../Header/Tools/__tests__/index.spec.tsx | 8 +- .../src/Layout/Header/Tools/index.tsx | 17 +- .../Layout/Header/__tests__/index.spec.tsx | 5 - .../src/Layout/Header/index.tsx | 13 +- .../__tests__/index.spec.tsx | 12 +- .../src/Layout/PreloadIssuesAlert/index.tsx | 10 - .../dashboard-frontend/src/Layout/index.tsx | 5 +- .../src/Routes/__tests__/index.spec.tsx | 16 -- .../dashboard-frontend/src/Routes/index.tsx | 6 +- .../__snapshots__/index.spec.tsx.snap | 152 +----------- .../UserAccount/__tests__/index.spec.tsx | 7 +- .../src/pages/UserAccount/index.tsx | 46 +--- .../src/services/bootstrap/index.ts | 16 +- .../userProfileApi.ts | 28 +++ .../src/store/User/index.ts | 127 ---------- .../src/store/User/selectors.ts | 20 -- .../store/UserProfile/__tests__/index.spec.ts | 217 ++++++++++++++++++ .../src/store/UserProfile/index.ts | 67 +++--- .../src/store/UserProfile/selectors.ts | 2 +- .../src/store/__mocks__/storeBuilder.ts | 33 +-- .../dashboard-frontend/src/store/index.ts | 55 +++-- 26 files changed, 383 insertions(+), 587 deletions(-) create mode 100644 packages/dashboard-frontend/src/services/dashboard-backend-client/userProfileApi.ts delete mode 100644 packages/dashboard-frontend/src/store/User/index.ts delete mode 100644 packages/dashboard-frontend/src/store/User/selectors.ts create mode 100644 packages/dashboard-frontend/src/store/UserProfile/__tests__/index.spec.ts diff --git a/packages/dashboard-frontend/src/Layout/Header/Tools/AboutMenu/__tests__/index.spec.tsx b/packages/dashboard-frontend/src/Layout/Header/Tools/AboutMenu/__tests__/index.spec.tsx index 23ceff380..d21bf2d28 100644 --- a/packages/dashboard-frontend/src/Layout/Header/Tools/AboutMenu/__tests__/index.spec.tsx +++ b/packages/dashboard-frontend/src/Layout/Header/Tools/AboutMenu/__tests__/index.spec.tsx @@ -49,20 +49,13 @@ describe('About Menu', () => { const productCli = 'crwctl'; const email = 'johndoe@example.com'; - const name = 'John Doe'; - const store = createStore(productCli, name, email); - const user = { - id: 'test-id', - name: name, - email: email, - links: [], - }; + const username = 'John Doe'; + const store = createStore(productCli, username, email); const branding = selectBranding(store.getState()); - const userProfile = selectUserProfile(store.getState()); const component = ( - + ); diff --git a/packages/dashboard-frontend/src/Layout/Header/Tools/AboutMenu/index.tsx b/packages/dashboard-frontend/src/Layout/Header/Tools/AboutMenu/index.tsx index b72c67bd6..d7f3ed024 100644 --- a/packages/dashboard-frontend/src/Layout/Header/Tools/AboutMenu/index.tsx +++ b/packages/dashboard-frontend/src/Layout/Header/Tools/AboutMenu/index.tsx @@ -22,8 +22,7 @@ import { BrandingData } from '../../../../services/bootstrap/branding.constant'; type Props = { branding: BrandingData; - user: che.User | undefined; - userProfile: api.che.user.Profile | undefined; + username: string; }; type State = { isLauncherOpen: boolean; @@ -40,26 +39,6 @@ export class AboutMenu extends React.PureComponent { }; } - private getUsername(): string { - const { userProfile, user } = this.props; - - let username = ''; - - if (userProfile && userProfile.attributes) { - if (userProfile.attributes.firstName) { - username += userProfile.attributes.firstName; - } - if (userProfile.attributes.lastName) { - username += ' ' + userProfile.attributes.lastName; - } - } - if (!username && user && user.name) { - username += user.name; - } - - return username; - } - private buildLauncherItems(): React.ReactNode[] { const branding = this.props.branding; const items: React.ReactElement[] = []; @@ -109,9 +88,9 @@ export class AboutMenu extends React.PureComponent { } public render(): React.ReactElement { + const { username } = this.props; const { isLauncherOpen, isModalOpen } = this.state; - const username = this.getUsername(); const { logoFile, name, productVersion } = this.props.branding; return ( diff --git a/packages/dashboard-frontend/src/Layout/Header/Tools/UserMenu/__tests__/index.spec.tsx b/packages/dashboard-frontend/src/Layout/Header/Tools/UserMenu/__tests__/index.spec.tsx index 3489c9b09..36797dd9d 100644 --- a/packages/dashboard-frontend/src/Layout/Header/Tools/UserMenu/__tests__/index.spec.tsx +++ b/packages/dashboard-frontend/src/Layout/Header/Tools/UserMenu/__tests__/index.spec.tsx @@ -44,27 +44,14 @@ describe('User Menu', () => { global.open = jest.fn(); const email = 'johndoe@example.com'; - const name = 'John Doe'; - const store = createStore(name, email); + const username = 'John Doe'; + const store = createStore(username, email); const history = createHashHistory(); - const user = { - id: 'test-id', - name: name, - email: email, - links: [], - }; const branding = selectBranding(store.getState()); - const userProfile = selectUserProfile(store.getState()); const component = ( - + ); @@ -79,17 +66,17 @@ describe('User Menu', () => { it('should open the dropdown', () => { render(component); - const menuButton = screen.getByRole('button', { name }); + const menuButton = screen.getByRole('button', { name: username }); fireEvent.click(menuButton); const items = screen.getAllByRole('menuitem'); - expect(items.length).toEqual(3); + expect(items.length).toEqual(2); }); it('should fire the logout event', () => { render(component); - const menuButton = screen.getByRole('button', { name }); + const menuButton = screen.getByRole('button', { name: username }); fireEvent.click(menuButton); const logoutItem = screen.getByRole('menuitem', { name: /logout/i }); diff --git a/packages/dashboard-frontend/src/Layout/Header/Tools/UserMenu/index.tsx b/packages/dashboard-frontend/src/Layout/Header/Tools/UserMenu/index.tsx index a5f42ac9d..5516da6c8 100644 --- a/packages/dashboard-frontend/src/Layout/Header/Tools/UserMenu/index.tsx +++ b/packages/dashboard-frontend/src/Layout/Header/Tools/UserMenu/index.tsx @@ -23,8 +23,7 @@ import { BrandingData } from '../../../../services/bootstrap/branding.constant'; type Props = MappedProps & { branding: BrandingData; history: History; - user: che.User | undefined; - userProfile: api.che.user.Profile | undefined; + username: string; logout: () => void; }; type State = { @@ -54,35 +53,16 @@ export class UserMenu extends React.PureComponent { }); } - private getUsername(): string { - const { userProfile, user } = this.props; - - let username = ''; - - if (userProfile && userProfile.attributes) { - if (userProfile.attributes.firstName) { - username += userProfile.attributes.firstName; - } - if (userProfile.attributes.lastName) { - username += ' ' + userProfile.attributes.lastName; - } - } - if (!username && user && user.name) { - username += user.name; - } - - return username; - } - private buildUserDropdownItems(): Array { return [ - this.props.history.push(ROUTE.USER_ACCOUNT)} - > - Account - , + // temporary hidden, https://github.com/eclipse/che/issues/21595 + // this.props.history.push(ROUTE.USER_ACCOUNT)} + // > + // Account + // , { } private buildUserToggleButton(): React.ReactElement { - const username = this.getUsername(); + const username = this.props.username; return ( this.onUsernameButtonToggle(isOpen)}> {username} diff --git a/packages/dashboard-frontend/src/Layout/Header/Tools/__tests__/__snapshots__/index.spec.tsx.snap b/packages/dashboard-frontend/src/Layout/Header/Tools/__tests__/__snapshots__/index.spec.tsx.snap index af04ce890..a888b2c3c 100644 --- a/packages/dashboard-frontend/src/Layout/Header/Tools/__tests__/__snapshots__/index.spec.tsx.snap +++ b/packages/dashboard-frontend/src/Layout/Header/Tools/__tests__/__snapshots__/index.spec.tsx.snap @@ -78,11 +78,6 @@ exports[`Page header tools should correctly render the component 1`] = ` onKeyDown={[Function]} type="button" > - - John Doe - diff --git a/packages/dashboard-frontend/src/Layout/Header/Tools/__tests__/index.spec.tsx b/packages/dashboard-frontend/src/Layout/Header/Tools/__tests__/index.spec.tsx index 12524f78b..e7da7123d 100644 --- a/packages/dashboard-frontend/src/Layout/Header/Tools/__tests__/index.spec.tsx +++ b/packages/dashboard-frontend/src/Layout/Header/Tools/__tests__/index.spec.tsx @@ -47,16 +47,10 @@ describe('Page header tools', () => { const name = 'John Doe'; const store = createStore(productCli, name, email); const history = createHashHistory(); - const user = { - id: 'test-id', - name: name, - email: email, - links: [], - }; const component = ( - + ); diff --git a/packages/dashboard-frontend/src/Layout/Header/Tools/index.tsx b/packages/dashboard-frontend/src/Layout/Header/Tools/index.tsx index eb550e869..2ec5ff436 100644 --- a/packages/dashboard-frontend/src/Layout/Header/Tools/index.tsx +++ b/packages/dashboard-frontend/src/Layout/Header/Tools/index.tsx @@ -30,7 +30,6 @@ import { selectApplications } from '../../../store/ClusterInfo/selectors'; type Props = MappedProps & { history: History; - user: che.User | undefined; logout: () => void; }; export class HeaderTools extends React.PureComponent { @@ -40,9 +39,10 @@ export class HeaderTools extends React.PureComponent { public render(): React.ReactElement { const { applications, userProfile } = this.props; - const userEmail = userProfile.email || ''; - const imageUrl = userEmail ? gravatarUrl(userEmail, { default: 'retro' }) : ''; - const isUserAuthenticated = !!userEmail; + + const { email, username } = userProfile; + const imageUrl = email ? gravatarUrl(email, { default: 'retro' }) : ''; + const isUserAuthenticated = !!email; return ( <> @@ -50,19 +50,14 @@ export class HeaderTools extends React.PureComponent { {applications.length !== 0 && } - + {isUserAuthenticated && ( this.props.logout()} /> diff --git a/packages/dashboard-frontend/src/Layout/Header/__tests__/index.spec.tsx b/packages/dashboard-frontend/src/Layout/Header/__tests__/index.spec.tsx index 841941177..729879cf0 100644 --- a/packages/dashboard-frontend/src/Layout/Header/__tests__/index.spec.tsx +++ b/packages/dashboard-frontend/src/Layout/Header/__tests__/index.spec.tsx @@ -32,10 +32,6 @@ describe('Page header', () => { const mockToggleNav = jest.fn(); const mockChangeTheme = jest.fn(); - const user = { - email: 'johndoe@example.com', - name: 'John Doe', - } as che.User; const logoUrl = 'branding/logo'; const isHeaderVisible = true; const history = createHashHistory(); @@ -45,7 +41,6 @@ describe('Page header', () => { history={history} isVisible={isHeaderVisible} logoUrl={logoUrl} - user={user} logout={mockLogout} toggleNav={mockToggleNav} changeTheme={mockChangeTheme} diff --git a/packages/dashboard-frontend/src/Layout/Header/index.tsx b/packages/dashboard-frontend/src/Layout/Header/index.tsx index 9f3db1eff..185fc03c7 100644 --- a/packages/dashboard-frontend/src/Layout/Header/index.tsx +++ b/packages/dashboard-frontend/src/Layout/Header/index.tsx @@ -10,19 +10,16 @@ * Red Hat, Inc. - initial API and implementation */ +import { Brand, PageHeader } from '@patternfly/react-core'; import { History } from 'history'; import React from 'react'; -import { Brand, PageHeader } from '@patternfly/react-core'; -import { User } from 'che'; - -import HeaderTools from './Tools'; import { ThemeVariant } from '../themeVariant'; +import HeaderTools from './Tools'; type Props = { history: History; isVisible: boolean; logoUrl: string; - user: User | undefined; logout: () => void; toggleNav: () => void; changeTheme: (theme: ThemeVariant) => void; @@ -65,11 +62,7 @@ export default class Header extends React.PureComponent { showNavToggle={true} onNavToggle={() => this.toggleNav()} headerTools={ - this.props.logout()} - /> + this.props.logout()} /> } /> ); diff --git a/packages/dashboard-frontend/src/Layout/PreloadIssuesAlert/__tests__/index.spec.tsx b/packages/dashboard-frontend/src/Layout/PreloadIssuesAlert/__tests__/index.spec.tsx index 37bb49316..0d802f284 100644 --- a/packages/dashboard-frontend/src/Layout/PreloadIssuesAlert/__tests__/index.spec.tsx +++ b/packages/dashboard-frontend/src/Layout/PreloadIssuesAlert/__tests__/index.spec.tsx @@ -39,8 +39,13 @@ describe('PreloadIssuesAlert component', () => { }) .withInfrastructureNamespace([], false, 'expected error 3') .withPlugins([], false, 'expected error 4') - .withUser({} as che.User, 'expected error 5') - .withUserProfile({}, 'expected error 6') + .withUserProfile( + { + email: 'user1@che', + username: 'user1', + }, + 'expected error 6', + ) .withWorkspacesSettings({} as che.WorkspaceSettings, false, 'expected error 7') .build(); renderComponent(store); @@ -59,9 +64,6 @@ describe('PreloadIssuesAlert component', () => { const pluginsAlert = screen.queryByRole('heading', { name: /expected error 4/i }); expect(pluginsAlert).toBeTruthy(); - const userInfoAlert = screen.queryByRole('heading', { name: /expected error 5/i }); - expect(userInfoAlert).toBeTruthy(); - const userProfileAlert = screen.queryByRole('heading', { name: /expected error 6/i }); expect(userProfileAlert).toBeTruthy(); diff --git a/packages/dashboard-frontend/src/Layout/PreloadIssuesAlert/index.tsx b/packages/dashboard-frontend/src/Layout/PreloadIssuesAlert/index.tsx index 1e9ad710d..f8529aa6d 100644 --- a/packages/dashboard-frontend/src/Layout/PreloadIssuesAlert/index.tsx +++ b/packages/dashboard-frontend/src/Layout/PreloadIssuesAlert/index.tsx @@ -23,7 +23,6 @@ import { selectInfrastructureNamespacesError } from '../../store/InfrastructureN import { selectUserProfileError } from '../../store/UserProfile/selectors'; import { selectWorkspacesSettingsError } from '../../store/Workspaces/Settings/selectors'; import { selectWorkspacesError } from '../../store/Workspaces/selectors'; -import { selectUserError } from '../../store/User/selectors'; import { AlertVariant } from '@patternfly/react-core'; import { lazyInject } from '../../inversify.config'; import { AppAlerts } from '../../services/alerts/appAlerts'; @@ -53,14 +52,6 @@ export class PreloadIssuesAlert extends React.PureComponent { }); }); } - // user info error - if (this.props.userError) { - this.appAlerts.showAlert({ - key: 'user-error', - title: this.props.userError, - variant: AlertVariant.danger, - }); - } // plugins error if (this.props.pluginsError) { this.appAlerts.showAlert({ @@ -117,7 +108,6 @@ export class PreloadIssuesAlert extends React.PureComponent { } const mapStateToProps = (state: AppState) => ({ - userError: selectUserError(state), registriesErrors: selectRegistriesErrors(state), pluginsError: selectPluginsError(state), dwDefaultEditorError: selectDwDefaultEditorError(state), diff --git a/packages/dashboard-frontend/src/Layout/index.tsx b/packages/dashboard-frontend/src/Layout/index.tsx index a424dd0c8..2bb2bbd0e 100644 --- a/packages/dashboard-frontend/src/Layout/index.tsx +++ b/packages/dashboard-frontend/src/Layout/index.tsx @@ -29,7 +29,6 @@ import { BannerAlert } from '../components/BannerAlert'; import { ErrorBoundary } from './ErrorBoundary'; import { DisposableCollection } from '../services/helpers/disposable'; import { ROUTE } from '../Routes/routes'; -import { selectUser } from '../store/User/selectors'; import { selectBranding } from '../store/Branding/selectors'; import { ToggleBarsContext } from '../contexts/ToggleBars'; import { signOut } from '../services/helpers/login'; @@ -171,7 +170,7 @@ export class Layout extends React.PureComponent { } const { isHeaderVisible, isSidebarVisible, theme } = this.state; - const { history, user } = this.props; + const { history } = this.props; const logoUrl = this.props.branding.logoFile; @@ -188,7 +187,6 @@ export class Layout extends React.PureComponent { history={history} isVisible={isHeaderVisible} logoUrl={logoUrl} - user={user} logout={() => signOut()} toggleNav={() => this.toggleNav()} changeTheme={theme => this.changeTheme(theme)} @@ -217,7 +215,6 @@ export class Layout extends React.PureComponent { const mapStateToProps = (state: AppState) => ({ branding: selectBranding(state), - user: selectUser(state), }); const connector = connect(mapStateToProps); diff --git a/packages/dashboard-frontend/src/Routes/__tests__/index.spec.tsx b/packages/dashboard-frontend/src/Routes/__tests__/index.spec.tsx index 3ac517aa4..48d5e50d9 100644 --- a/packages/dashboard-frontend/src/Routes/__tests__/index.spec.tsx +++ b/packages/dashboard-frontend/src/Routes/__tests__/index.spec.tsx @@ -54,11 +54,6 @@ jest.mock('../../pages/UserPreferences', () => { return User Preferences; }; }); -jest.mock('../../pages/UserAccount', () => { - return function UserPreferences() { - return User Account; - }; -}); describe('Routes', () => { afterEach(() => { @@ -198,17 +193,6 @@ describe('Routes', () => { expect(screen.queryByTestId('fallback-spinner')).not.toBeInTheDocument(); }); }); - - describe('User Account route', () => { - it('should handle "/user-account"', async () => { - const path = ROUTE.USER_ACCOUNT; - render(getComponent(path)); - - await waitFor(() => expect(screen.queryByText('User Account')).toBeTruthy()); - - expect(screen.queryByTestId('fallback-spinner')).not.toBeInTheDocument(); - }); - }); }); function getComponent(locationOrPath: Location | string): React.ReactElement { diff --git a/packages/dashboard-frontend/src/Routes/index.tsx b/packages/dashboard-frontend/src/Routes/index.tsx index 80ab3ca27..ef93b9f79 100644 --- a/packages/dashboard-frontend/src/Routes/index.tsx +++ b/packages/dashboard-frontend/src/Routes/index.tsx @@ -20,7 +20,8 @@ const WorkspacesListContainer = React.lazy(() => import('../containers/Workspace const WorkspaceDetailsContainer = React.lazy(() => import('../containers/WorkspaceDetails')); const LoaderContainer = React.lazy(() => import('../containers/Loader')); const UserPreferences = React.lazy(() => import('../pages/UserPreferences')); -const UserAccount = React.lazy(() => import('../pages/UserAccount')); +// temporary hidden, https://github.com/eclipse/che/issues/21595 +// const UserAccount = React.lazy(() => import('../pages/UserAccount')); export interface RouteItem { to: ROUTE; @@ -35,7 +36,8 @@ const items: RouteItem[] = [ { to: ROUTE.IDE_LOADER, component: LoaderContainer }, { to: ROUTE.FACTORY_LOADER, component: LoaderContainer }, { to: ROUTE.USER_PREFERENCES, component: UserPreferences }, - { to: ROUTE.USER_ACCOUNT, component: UserAccount }, + // temporary hidden, https://github.com/eclipse/che/issues/21595 + // { to: ROUTE.USER_ACCOUNT, component: UserAccount }, ]; function Routes(): React.ReactElement { diff --git a/packages/dashboard-frontend/src/pages/UserAccount/__tests__/__snapshots__/index.spec.tsx.snap b/packages/dashboard-frontend/src/pages/UserAccount/__tests__/__snapshots__/index.spec.tsx.snap index ecacbd06b..76346f384 100644 --- a/packages/dashboard-frontend/src/pages/UserAccount/__tests__/__snapshots__/index.spec.tsx.snap +++ b/packages/dashboard-frontend/src/pages/UserAccount/__tests__/__snapshots__/index.spec.tsx.snap @@ -61,7 +61,7 @@ exports[`UserAccount page should correctly render the component which contains p readOnly={false} required={false} type="text" - value="Johnny" + value="john-doe" /> @@ -103,80 +103,6 @@ exports[`UserAccount page should correctly render the component which contains p -
-
- - -
-
- - -
-
-
-
- - -
-
- - -
-
@@ -244,7 +170,6 @@ exports[`UserAccount page should correctly render the component without profile readOnly={false} required={false} type="text" - value="" /> @@ -281,81 +206,6 @@ exports[`UserAccount page should correctly render the component without profile readOnly={false} required={false} type="text" - value="" - /> - - - -
-
- - -
-
- - -
-
-
-
- - -
-
-
diff --git a/packages/dashboard-frontend/src/pages/UserAccount/__tests__/index.spec.tsx b/packages/dashboard-frontend/src/pages/UserAccount/__tests__/index.spec.tsx index 7e6843287..55ab69de1 100644 --- a/packages/dashboard-frontend/src/pages/UserAccount/__tests__/index.spec.tsx +++ b/packages/dashboard-frontend/src/pages/UserAccount/__tests__/index.spec.tsx @@ -59,13 +59,8 @@ describe('UserAccount page', () => { name: 'Product name', } as BrandingData) .withUserProfile({ - attributes: { - firstName: 'John', - lastName: 'Doe', - preferred_username: 'Johnny', - }, email: 'johndoe@test.com', - userId: 'john-doe-id', + username: 'john-doe', }) .build(); const component = getComponent(store); diff --git a/packages/dashboard-frontend/src/pages/UserAccount/index.tsx b/packages/dashboard-frontend/src/pages/UserAccount/index.tsx index 0ea06ca35..f1926f0cf 100644 --- a/packages/dashboard-frontend/src/pages/UserAccount/index.tsx +++ b/packages/dashboard-frontend/src/pages/UserAccount/index.tsx @@ -11,16 +11,16 @@ */ import { - Title, - PageSection, - Text, - TextInput, Form, FormGroup, - StackItem, - Stack, + PageSection, PageSectionVariants, + Stack, + StackItem, + Text, + TextInput, TextVariants, + Title, } from '@patternfly/react-core'; import { History } from 'history'; import React from 'react'; @@ -34,30 +34,10 @@ type Props = { history: History; } & MappedProps; -type State = { - firstName: string; - lastName: string; - email: string; - login: string; -}; - -export class UserAccount extends React.PureComponent { - constructor(props: Props) { - super(props); - - const { userProfile } = this.props; - - const email = userProfile.email || ''; - const login = userProfile.attributes?.preferred_username || ''; - const firstName = userProfile.attributes?.firstName || ''; - const lastName = userProfile.attributes?.lastName || ''; - - this.state = { login, email, lastName, firstName }; - } - +export class UserAccount extends React.PureComponent { render(): React.ReactNode { const productName = this.props.branding.name; - const { firstName, lastName, email, login } = this.state; + const { userProfile } = this.props; return ( @@ -73,16 +53,10 @@ export class UserAccount extends React.PureComponent {
- + - - - - - - - +
diff --git a/packages/dashboard-frontend/src/services/bootstrap/index.ts b/packages/dashboard-frontend/src/services/bootstrap/index.ts index df3249b37..f2ca8262a 100644 --- a/packages/dashboard-frontend/src/services/bootstrap/index.ts +++ b/packages/dashboard-frontend/src/services/bootstrap/index.ts @@ -24,7 +24,6 @@ import * as InfrastructureNamespacesStore from '../../store/InfrastructureNamesp import * as PluginsStore from '../../store/Plugins/chePlugins'; import * as DwPluginsStore from '../../store/Plugins/devWorkspacePlugins'; import * as UserProfileStore from '../../store/UserProfile'; -import * as UserStore from '../../store/User'; import * as WorkspacesStore from '../../store/Workspaces'; import * as DevWorkspacesStore from '../../store/Workspaces/devWorkspaces'; import * as WorkspacesSettingsStore from '../../store/Workspaces/Settings'; @@ -84,7 +83,6 @@ export default class Bootstrap { ]); const results = await Promise.allSettled([ - this.fetchCurrentUser(), this.fetchUserProfile(), this.fetchPlugins().then(() => this.fetchDevfileSchema()), this.fetchDwPlugins(), @@ -166,11 +164,6 @@ export default class Bootstrap { return await this.devWorkspaceClient.subscribeToNamespace({ namespace, callbacks }); } - private async fetchCurrentUser(): Promise { - const { requestUser } = UserStore.actionCreators; - await requestUser()(this.store.dispatch, this.store.getState, undefined); - } - private async fetchWorkspaces(): Promise { const { requestWorkspaces } = WorkspacesStore.actionCreators; await requestWorkspaces()(this.store.dispatch, this.store.getState, undefined); @@ -303,8 +296,15 @@ export default class Bootstrap { } private async fetchUserProfile(): Promise { + const defaultKubernetesNamespace = selectDefaultNamespace(this.store.getState()); + const defaultNamespace = defaultKubernetesNamespace.name; + const { requestUserProfile } = UserProfileStore.actionCreators; - return requestUserProfile()(this.store.dispatch, this.store.getState, undefined); + return requestUserProfile(defaultNamespace)( + this.store.dispatch, + this.store.getState, + undefined, + ); } private checkWorkspaceStopped() { diff --git a/packages/dashboard-frontend/src/services/dashboard-backend-client/userProfileApi.ts b/packages/dashboard-frontend/src/services/dashboard-backend-client/userProfileApi.ts new file mode 100644 index 000000000..fc1380d82 --- /dev/null +++ b/packages/dashboard-frontend/src/services/dashboard-backend-client/userProfileApi.ts @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2018-2021 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 common, { api } from '@eclipse-che/common'; +import axios from 'axios'; +import { prefix } from './const'; + +/** + * Returns object with user profile data. + */ +export async function fetchUserProfile(namespace: string): Promise { + const url = `${prefix}/userprofile/${namespace}`; + try { + const response = await axios.get(url); + return response.data; + } catch (e) { + throw `Failed to fetch the user profile data. ${common.helpers.errors.getMessage(e)}`; + } +} diff --git a/packages/dashboard-frontend/src/store/User/index.ts b/packages/dashboard-frontend/src/store/User/index.ts deleted file mode 100644 index 5b091a9da..000000000 --- a/packages/dashboard-frontend/src/store/User/index.ts +++ /dev/null @@ -1,127 +0,0 @@ -/* - * Copyright (c) 2018-2021 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 { Action, Reducer } from 'redux'; -import common from '@eclipse-che/common'; -import { createObject } from '../helpers'; -import { AppThunk } from '../index'; -import { container } from '../../inversify.config'; -import { CheWorkspaceClient } from '../../services/workspace-client/cheworkspace/cheWorkspaceClient'; - -const WorkspaceClient = container.get(CheWorkspaceClient); - -export interface State { - user: che.User | undefined; - error?: string; - isLoading: boolean; -} - -interface RequestUserAction { - type: 'REQUEST_USER'; -} - -interface ReceiveUserAction { - type: 'RECEIVE_USER'; - user: che.User; -} - -interface ReceiveErrorAction { - type: 'RECEIVE_USER_ERROR'; - error: string; -} - -interface SetUserAction { - type: 'SET_USER'; - user: che.User; -} - -type KnownAction = RequestUserAction | ReceiveUserAction | ReceiveErrorAction | SetUserAction; - -export type ActionCreators = { - requestUser: () => AppThunk>; - setUser: (user: che.User) => AppThunk; -}; - -export const actionCreators: ActionCreators = { - requestUser: - (): AppThunk> => - async (dispatch): Promise => { - dispatch({ type: 'REQUEST_USER' }); - - try { - const user = (await WorkspaceClient.restApiClient.getCurrentUser()) as che.User; - dispatch({ - type: 'RECEIVE_USER', - user, - }); - return; - } catch (e) { - const errorMessage = - 'Failed to fetch currently logged user info, reason: ' + - common.helpers.errors.getMessage(e); - dispatch({ - type: 'RECEIVE_USER_ERROR', - error: errorMessage, - }); - throw errorMessage; - } - }, - - setUser: - (user: che.User): AppThunk => - dispatch => { - dispatch({ - type: 'SET_USER', - user: user, - }); - }, -}; - -const unloadedState: State = { - user: undefined, - isLoading: false, -}; - -export const reducer: Reducer = ( - state: State | undefined, - incomingAction: Action, -): State => { - if (state === undefined) { - return unloadedState; - } - - const action = incomingAction as KnownAction; - switch (action.type) { - case 'REQUEST_USER': - return createObject(state, { - isLoading: true, - error: undefined, - }); - case 'RECEIVE_USER': - return createObject(state, { - isLoading: false, - user: action.user, - }); - case 'RECEIVE_USER_ERROR': - return createObject(state, { - isLoading: false, - error: action.error, - }); - case 'SET_USER': - return createObject(state, { - isLoading: false, - user: action.user, - }); - default: - return state; - } -}; diff --git a/packages/dashboard-frontend/src/store/User/selectors.ts b/packages/dashboard-frontend/src/store/User/selectors.ts deleted file mode 100644 index eccea35a5..000000000 --- a/packages/dashboard-frontend/src/store/User/selectors.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright (c) 2018-2021 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 { createSelector } from 'reselect'; -import { AppState } from '..'; - -const selectState = (state: AppState) => state.user; - -export const selectUser = createSelector(selectState, state => state.user); - -export const selectUserError = createSelector(selectState, state => state.error); diff --git a/packages/dashboard-frontend/src/store/UserProfile/__tests__/index.spec.ts b/packages/dashboard-frontend/src/store/UserProfile/__tests__/index.spec.ts new file mode 100644 index 000000000..f47711cde --- /dev/null +++ b/packages/dashboard-frontend/src/store/UserProfile/__tests__/index.spec.ts @@ -0,0 +1,217 @@ +/* + * Copyright (c) 2018-2021 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 { api } from '@eclipse-che/common'; +import mockAxios, { AxiosError } from 'axios'; +import { AnyAction } from 'redux'; +import { MockStoreEnhanced } from 'redux-mock-store'; +import { ThunkDispatch } from 'redux-thunk'; +import * as testStore from '..'; +import { AppState } from '../..'; +import { FakeStoreBuilder } from '../../__mocks__/storeBuilder'; + +const namespace = 'user1-che'; + +describe('UserPreferences store', () => { + let userProfile: api.IUserProfile; + + beforeEach(() => { + userProfile = { + email: 'user1@che', + username: 'user1', + }; + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('actions', () => { + let appStore: MockStoreEnhanced< + AppState, + ThunkDispatch + >; + + beforeEach(() => { + appStore = new FakeStoreBuilder().build() as MockStoreEnhanced< + AppState, + ThunkDispatch + >; + }); + + it('should create REQUEST_USER_PROFILE and RECEIVE_USER_PROFILE when fetching user profile', async () => { + (mockAxios.get as jest.Mock).mockResolvedValueOnce({ + data: userProfile, + }); + + await appStore.dispatch(testStore.actionCreators.requestUserProfile(namespace)); + + const actions = appStore.getActions(); + const expectedActions: testStore.KnownAction[] = [ + { + type: testStore.Type.REQUEST_USER_PROFILE, + }, + { + type: testStore.Type.RECEIVE_USER_PROFILE, + userProfile, + }, + ]; + + expect(actions).toEqual(expectedActions); + }); + + it('should create REQUEST_USER_PROFILE and RECEIVE_USER_PROFILE_ERROR when fails to fetch user profile', async () => { + (mockAxios.get as jest.Mock).mockRejectedValueOnce({ + isAxiosError: true, + code: '500', + message: 'Something unexpected happened.', + } as AxiosError); + + try { + await appStore.dispatch(testStore.actionCreators.requestUserProfile(namespace)); + } catch (e) { + // noop + } + + const actions = appStore.getActions(); + const expectedActions: testStore.KnownAction[] = [ + { + type: testStore.Type.REQUEST_USER_PROFILE, + }, + { + type: testStore.Type.RECEIVE_USER_PROFILE_ERROR, + error: expect.stringContaining('Something unexpected happened.'), + }, + ]; + + expect(actions).toEqual(expectedActions); + }); + }); + + describe('reducers', () => { + it('should return initial state', () => { + const incomingAction: testStore.RequestUserProfileAction = { + type: testStore.Type.REQUEST_USER_PROFILE, + }; + const initialState = testStore.reducer(undefined, incomingAction); + + const expectedState: testStore.State = { + isLoading: false, + userProfile: { + email: '', + username: 'unknown', + }, + }; + + expect(initialState).toEqual(expectedState); + }); + + it('should return state if action type is not matched', () => { + const initialState: testStore.State = { + isLoading: true, + userProfile: { + email: '', + username: 'unknown', + }, + }; + const incomingAction = { + type: 'OTHER_ACTION', + } as AnyAction; + const newState = testStore.reducer(initialState, incomingAction); + + const expectedState: testStore.State = { + isLoading: true, + userProfile: { + email: '', + username: 'unknown', + }, + }; + expect(newState).toEqual(expectedState); + }); + + it('should handle REQUEST_USER_PROFILE', () => { + const initialState: testStore.State = { + isLoading: false, + userProfile: { + email: '', + username: 'unknown', + }, + error: 'unexpected error', + }; + const incomingAction: testStore.RequestUserProfileAction = { + type: testStore.Type.REQUEST_USER_PROFILE, + }; + + const newState = testStore.reducer(initialState, incomingAction); + + const expectedState: testStore.State = { + isLoading: true, + userProfile: { + email: '', + username: 'unknown', + }, + }; + + expect(newState).toEqual(expectedState); + }); + + it('should handle RECEIVE_USER_PROFILE', () => { + const initialState: testStore.State = { + isLoading: true, + userProfile: { + email: '', + username: 'unknown', + }, + }; + const incomingAction: testStore.ReceiveUserProfileAction = { + type: testStore.Type.RECEIVE_USER_PROFILE, + userProfile, + }; + + const newState = testStore.reducer(initialState, incomingAction); + + const expectedState: testStore.State = { + isLoading: false, + userProfile, + }; + + expect(newState).toEqual(expectedState); + }); + + it('should handle RECEIVE_USER_PROFILE_ERROR', () => { + const initialState: testStore.State = { + isLoading: true, + userProfile: { + email: '', + username: 'unknown', + }, + }; + const incomingAction: testStore.ReceiveUserProfileErrorAction = { + type: testStore.Type.RECEIVE_USER_PROFILE_ERROR, + error: 'unexpected error', + }; + + const newState = testStore.reducer(initialState, incomingAction); + + const expectedState: testStore.State = { + isLoading: false, + userProfile: { + email: '', + username: 'unknown', + }, + error: 'unexpected error', + }; + + expect(newState).toEqual(expectedState); + }); + }); +}); diff --git a/packages/dashboard-frontend/src/store/UserProfile/index.ts b/packages/dashboard-frontend/src/store/UserProfile/index.ts index a05b7237b..f80be8bb1 100644 --- a/packages/dashboard-frontend/src/store/UserProfile/index.ts +++ b/packages/dashboard-frontend/src/store/UserProfile/index.ts @@ -12,71 +12,78 @@ // This state defines the type of data maintained in the Redux store. +import common, { api } from '@eclipse-che/common'; import { Action, Reducer } from 'redux'; -import common from '@eclipse-che/common'; +import { fetchUserProfile } from '../../services/dashboard-backend-client/userProfileApi'; import { createObject } from '../helpers'; import { AppThunk } from '../index'; -import { container } from '../../inversify.config'; -import { CheWorkspaceClient } from '../../services/workspace-client/cheworkspace/cheWorkspaceClient'; -import { che as cheApi } from '@eclipse-che/api'; - -const WorkspaceClient = container.get(CheWorkspaceClient); export interface State { - profile: cheApi.user.Profile; + userProfile: api.IUserProfile; error?: string; isLoading: boolean; } -interface RequestUserProfileAction { - type: 'REQUEST_USER_PROFILE'; +export enum Type { + REQUEST_USER_PROFILE = 'REQUEST_USER_PROFILE', + RECEIVE_USER_PROFILE = 'RECEIVE_USER_PROFILE', + RECEIVE_USER_PROFILE_ERROR = 'RECEIVE_USER_PROFILE_ERROR', +} + +export interface RequestUserProfileAction { + type: Type.REQUEST_USER_PROFILE; } -interface ReceiveUserProfileAction { - type: 'RECEIVE_USER_PROFILE'; - profile: api.che.user.Profile; +export interface ReceiveUserProfileAction { + type: Type.RECEIVE_USER_PROFILE; + userProfile: api.IUserProfile; } -interface ReceiveUserProfileErrorAction { - type: 'RECEIVE_USER_PROFILE_ERROR'; +export interface ReceiveUserProfileErrorAction { + type: Type.RECEIVE_USER_PROFILE_ERROR; error: string; } -type KnownAction = +export type KnownAction = | RequestUserProfileAction | ReceiveUserProfileAction | ReceiveUserProfileErrorAction; export type ActionCreators = { - requestUserProfile: () => AppThunk>; + requestUserProfile: (namespace: string) => AppThunk>; }; export const actionCreators: ActionCreators = { requestUserProfile: - (): AppThunk> => + (namespace: string): AppThunk> => async (dispatch): Promise => { - dispatch({ type: 'REQUEST_USER_PROFILE' }); + dispatch({ type: Type.REQUEST_USER_PROFILE }); try { - const profile = await WorkspaceClient.restApiClient.getCurrentUserProfile(); + const userProfile = await fetchUserProfile(namespace); dispatch({ - type: 'RECEIVE_USER_PROFILE', - profile: profile ? profile : unloadedState.profile, + type: Type.RECEIVE_USER_PROFILE, + userProfile, }); } catch (e) { - const errorMessage = - 'Failed to fetch the user profile, reason: ' + common.helpers.errors.getMessage(e); + const errorMessage = common.helpers.errors.getMessage(e); dispatch({ - type: 'RECEIVE_USER_PROFILE_ERROR', + type: Type.RECEIVE_USER_PROFILE_ERROR, error: errorMessage, }); - throw errorMessage; + if (common.helpers.errors.isError(e)) { + throw e; + } + throw new Error(errorMessage); } }, }; const unloadedState: State = { - profile: { email: '' }, + userProfile: { + email: '', + username: 'unknown', + }, isLoading: false, }; @@ -90,17 +97,17 @@ export const reducer: Reducer = ( const action = incomingAction as KnownAction; switch (action.type) { - case 'REQUEST_USER_PROFILE': + case Type.REQUEST_USER_PROFILE: return createObject(state, { isLoading: true, error: undefined, }); - case 'RECEIVE_USER_PROFILE': + case Type.RECEIVE_USER_PROFILE: return createObject(state, { isLoading: false, - profile: action.profile, + userProfile: action.userProfile, }); - case 'RECEIVE_USER_PROFILE_ERROR': + case Type.RECEIVE_USER_PROFILE_ERROR: return createObject(state, { isLoading: false, error: action.error, diff --git a/packages/dashboard-frontend/src/store/UserProfile/selectors.ts b/packages/dashboard-frontend/src/store/UserProfile/selectors.ts index 79c353557..f613426d4 100644 --- a/packages/dashboard-frontend/src/store/UserProfile/selectors.ts +++ b/packages/dashboard-frontend/src/store/UserProfile/selectors.ts @@ -16,6 +16,6 @@ import { AppState } from '..'; const selectState = (state: AppState) => state.userProfile; export const selectUserProfileState = selectState; -export const selectUserProfile = createSelector(selectState, state => state.profile); +export const selectUserProfile = createSelector(selectState, state => state.userProfile); export const selectUserProfileError = createSelector(selectState, state => state.error); diff --git a/packages/dashboard-frontend/src/store/__mocks__/storeBuilder.ts b/packages/dashboard-frontend/src/store/__mocks__/storeBuilder.ts index 80dc46423..80edb957a 100644 --- a/packages/dashboard-frontend/src/store/__mocks__/storeBuilder.ts +++ b/packages/dashboard-frontend/src/store/__mocks__/storeBuilder.ts @@ -10,23 +10,22 @@ * Red Hat, Inc. - initial API and implementation */ +import { api, ClusterConfig, ClusterInfo } from '@eclipse-che/common'; import { Store } from 'redux'; import createMockStore from 'redux-mock-store'; -import { BrandingData } from '../../services/bootstrap/branding.constant'; import { AppState } from '..'; +import { BrandingData } from '../../services/bootstrap/branding.constant'; +import devfileApi from '../../services/devfileApi'; +import { WorkspacesLogs } from '../../services/helpers/types'; +import { State as BrandingState } from '../Branding'; import { DevWorkspaceResources, State as DevfileRegistriesState } from '../DevfileRegistries/index'; import { RegistryEntry } from '../DockerConfig/types'; -import { State as WorkspacesState } from '../Workspaces/index'; -import { State as BrandingState } from '../Branding'; import { ConvertedState, ResolverState, State as FactoryResolverState } from '../FactoryResolver'; import { State as InfrastructureNamespaceState } from '../InfrastructureNamespaces'; import { State as PluginsState } from '../Plugins/chePlugins'; -import { State as UserState } from '../User'; import { State as UserProfileState } from '../UserProfile'; +import { State as WorkspacesState } from '../Workspaces/index'; import mockThunk from './thunk'; -import devfileApi from '../../services/devfileApi'; -import { api as dashboardBackendApi, ClusterConfig, ClusterInfo } from '@eclipse-che/common'; -import { WorkspacesLogs } from '../../services/helpers/types'; export class FakeStoreBuilder { private state: AppState = { @@ -57,7 +56,7 @@ export class FakeStoreBuilder { runTimeout: -1, }, cheNamespace: '', - } as dashboardBackendApi.IServerConfig, + } as api.IServerConfig, }, clusterInfo: { isLoading: false, @@ -106,13 +105,9 @@ export class FakeStoreBuilder { devWorkspaceResources: {}, schema: {}, } as DevfileRegistriesState, - user: { - isLoading: false, - user: {}, - } as UserState, userProfile: { isLoading: false, - profile: {}, + userProfile: {}, } as UserProfileState, infrastructureNamespaces: { isLoading: false, @@ -140,7 +135,7 @@ export class FakeStoreBuilder { }, }; - public withDwServerConfig(config: dashboardBackendApi.IServerConfig): FakeStoreBuilder { + public withDwServerConfig(config: api.IServerConfig): FakeStoreBuilder { this.state.dwServerConfig = { isLoading: false, config, @@ -250,14 +245,8 @@ export class FakeStoreBuilder { return this; } - public withUser(user: che.User, error?: string): FakeStoreBuilder { - this.state.user.user = Object.assign({}, user); - this.state.user.error = error; - return this; - } - - public withUserProfile(profile: api.che.user.Profile, error?: string): FakeStoreBuilder { - this.state.userProfile.profile = Object.assign({}, profile); + public withUserProfile(profile: api.IUserProfile, error?: string): FakeStoreBuilder { + this.state.userProfile.userProfile = Object.assign({}, profile); this.state.userProfile.error = error; return this; } diff --git a/packages/dashboard-frontend/src/store/index.ts b/packages/dashboard-frontend/src/store/index.ts index 422b6c48e..a6f29a938 100644 --- a/packages/dashboard-frontend/src/store/index.ts +++ b/packages/dashboard-frontend/src/store/index.ts @@ -14,67 +14,64 @@ import { Action } from 'redux'; import { ThunkAction } from 'redux-thunk'; import * as BannerAlertStore from './BannerAlert'; import * as BrandingStore from './Branding'; -import * as ClusterInfo from './ClusterInfo'; import * as ClusterConfig from './ClusterConfig'; +import * as ClusterInfo from './ClusterInfo'; import * as DevfileRegistriesStore from './DevfileRegistries'; +import * as CheDockerConfigStore from './DockerConfig/che'; +import * as DwDockerConfigStore from './DockerConfig/dw'; import * as FactoryResolverStore from './FactoryResolver'; import * as InfrastructureNamespacesStore from './InfrastructureNamespaces'; import * as PluginsStore from './Plugins/chePlugins'; +import * as DwPluginsStore from './Plugins/devWorkspacePlugins'; +import * as DwServerConfigStore from './ServerConfig'; import * as UserPreferences from './UserPreferences'; +import * as UserProfileStore from './UserProfile'; import * as WorkspacesStore from './Workspaces'; import * as CheWorkspacesStore from './Workspaces/cheWorkspaces'; import * as DevWorkspacesStore from './Workspaces/devWorkspaces'; -import * as UserStore from './User'; -import * as UserProfileStore from './UserProfile'; -import * as DwPluginsStore from './Plugins/devWorkspacePlugins'; import * as WorkspacesSettingsStore from './Workspaces/Settings'; -import * as CheDockerConfigStore from './DockerConfig/che'; -import * as DwDockerConfigStore from './DockerConfig/dw'; -import * as DwServerConfigStore from './ServerConfig'; // the top-level state object export interface AppState { bannerAlert: BannerAlertStore.State; branding: BrandingStore.State; - clusterInfo: ClusterInfo.State; + cheDockerConfig: CheDockerConfigStore.State; + cheWorkspaces: CheWorkspacesStore.State; clusterConfig: ClusterConfig.State; + clusterInfo: ClusterInfo.State; + devWorkspaces: DevWorkspacesStore.State; devfileRegistries: DevfileRegistriesStore.State; + dwDockerConfig: DwDockerConfigStore.State; + dwPlugins: DwPluginsStore.State; + dwServerConfig: DwServerConfigStore.State; + factoryResolver: FactoryResolverStore.State; infrastructureNamespaces: InfrastructureNamespacesStore.State; - user: UserStore.State; - userProfile: UserProfileStore.State; - workspaces: WorkspacesStore.State; - cheWorkspaces: CheWorkspacesStore.State; - devWorkspaces: DevWorkspacesStore.State; plugins: PluginsStore.State; - factoryResolver: FactoryResolverStore.State; userPreferences: UserPreferences.State; - dwPlugins: DwPluginsStore.State; + userProfile: UserProfileStore.State; + workspaces: WorkspacesStore.State; workspacesSettings: WorkspacesSettingsStore.State; - cheDockerConfig: CheDockerConfigStore.State; - dwDockerConfig: DwDockerConfigStore.State; - dwServerConfig: DwServerConfigStore.State; } export const reducers = { bannerAlert: BannerAlertStore.reducer, - workspaces: WorkspacesStore.reducer, + branding: BrandingStore.reducer, + cheDockerConfig: CheDockerConfigStore.reducer, cheWorkspaces: CheWorkspacesStore.reducer, + clusterConfig: ClusterConfig.reducer, + clusterInfo: ClusterInfo.reducer, devWorkspaces: DevWorkspacesStore.reducer, devfileRegistries: DevfileRegistriesStore.reducer, - branding: BrandingStore.reducer, - clusterInfo: ClusterInfo.reducer, - clusterConfig: ClusterConfig.reducer, - user: UserStore.reducer, - userProfile: UserProfileStore.reducer, + dwDockerConfig: DwDockerConfigStore.reducer, + dwPlugins: DwPluginsStore.reducer, + dwServerConfig: DwServerConfigStore.reducer, + factoryResolver: FactoryResolverStore.reducer, infrastructureNamespaces: InfrastructureNamespacesStore.reducer, plugins: PluginsStore.reducer, - factoryResolver: FactoryResolverStore.reducer, userPreferences: UserPreferences.reducer, - dwPlugins: DwPluginsStore.reducer, + userProfile: UserProfileStore.reducer, + workspaces: WorkspacesStore.reducer, workspacesSettings: WorkspacesSettingsStore.reducer, - cheDockerConfig: CheDockerConfigStore.reducer, - dwDockerConfig: DwDockerConfigStore.reducer, - dwServerConfig: DwServerConfigStore.reducer, }; export type AppThunk = ThunkAction< From 2b035cf23f8c8a02b73727e7d992bc41e71a7144 Mon Sep 17 00:00:00 2001 From: Oleksii Kurinnyi Date: Mon, 31 Oct 2022 17:04:43 +0200 Subject: [PATCH 3/4] fix: use user token --- packages/dashboard-backend/src/routes/api/userProfile.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/dashboard-backend/src/routes/api/userProfile.ts b/packages/dashboard-backend/src/routes/api/userProfile.ts index 1d276301b..5e7054aa5 100644 --- a/packages/dashboard-backend/src/routes/api/userProfile.ts +++ b/packages/dashboard-backend/src/routes/api/userProfile.ts @@ -16,7 +16,7 @@ import { namespacedSchema } from '../../constants/schemas'; import { getSchema } from '../../services/helpers'; import { restParams } from '../../typings/models'; import { getDevWorkspaceClient } from './helpers/getDevWorkspaceClient'; -import { getServiceAccountToken } from './helpers/getServiceAccountToken'; +import { getToken } from './helpers/getToken'; const tags = ['UserProfile']; @@ -26,8 +26,8 @@ export function registerUserProfileRoute(server: FastifyInstance) { getSchema({ tags, params: namespacedSchema }), async function (request: FastifyRequest) { const { namespace } = request.params as restParams.INamespacedParam; - const serviceAccountToken = getServiceAccountToken(); - const { userProfileApi } = getDevWorkspaceClient(serviceAccountToken); + const token = getToken(request); + const { userProfileApi } = getDevWorkspaceClient(token); return userProfileApi.getUserProfile(namespace); }, ); From 545965c2fe3cd3902b7eb9a9c371b4b6b2dfa96b Mon Sep 17 00:00:00 2001 From: Oleksii Kurinnyi Date: Tue, 1 Nov 2022 17:24:10 +0200 Subject: [PATCH 4/4] chore: code cleanup --- .../Header/Tools/AboutMenu/__tests__/index.spec.tsx | 9 ++++----- .../Header/Tools/UserMenu/__tests__/index.spec.tsx | 9 ++++----- .../services/dashboard-backend-client/serverConfigApi.ts | 2 +- 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/packages/dashboard-frontend/src/Layout/Header/Tools/AboutMenu/__tests__/index.spec.tsx b/packages/dashboard-frontend/src/Layout/Header/Tools/AboutMenu/__tests__/index.spec.tsx index d21bf2d28..075ac8446 100644 --- a/packages/dashboard-frontend/src/Layout/Header/Tools/AboutMenu/__tests__/index.spec.tsx +++ b/packages/dashboard-frontend/src/Layout/Header/Tools/AboutMenu/__tests__/index.spec.tsx @@ -10,21 +10,20 @@ * Red Hat, Inc. - initial API and implementation */ +import { fireEvent, render, screen } from '@testing-library/react'; import React from 'react'; import { Provider } from 'react-redux'; import renderer from 'react-test-renderer'; -import { fireEvent, render, screen } from '@testing-library/react'; import { Action, Store } from 'redux'; import { AboutMenu } from '..'; -import { AppThunk } from '../../../../../store'; -import { FakeStoreBuilder } from '../../../../../store/__mocks__/storeBuilder'; import { BrandingData, BRANDING_DEFAULT, } from '../../../../../services/bootstrap/branding.constant'; -import * as InfrastructureNamespacesStore from '../../../../../store/InfrastructureNamespaces'; +import { AppThunk } from '../../../../../store'; import { selectBranding } from '../../../../../store/Branding/selectors'; -import { selectUserProfile } from '../../../../../store/UserProfile/selectors'; +import * as InfrastructureNamespacesStore from '../../../../../store/InfrastructureNamespaces'; +import { FakeStoreBuilder } from '../../../../../store/__mocks__/storeBuilder'; jest.mock('gravatar-url', () => { return function () { diff --git a/packages/dashboard-frontend/src/Layout/Header/Tools/UserMenu/__tests__/index.spec.tsx b/packages/dashboard-frontend/src/Layout/Header/Tools/UserMenu/__tests__/index.spec.tsx index 36797dd9d..bba2f21f3 100644 --- a/packages/dashboard-frontend/src/Layout/Header/Tools/UserMenu/__tests__/index.spec.tsx +++ b/packages/dashboard-frontend/src/Layout/Header/Tools/UserMenu/__tests__/index.spec.tsx @@ -10,22 +10,21 @@ * Red Hat, Inc. - initial API and implementation */ +import { fireEvent, render, screen } from '@testing-library/react'; import { createHashHistory } from 'history'; import React from 'react'; import { Provider } from 'react-redux'; import renderer from 'react-test-renderer'; -import { fireEvent, render, screen } from '@testing-library/react'; import { Action, Store } from 'redux'; import UserMenu from '..'; -import { AppThunk } from '../../../../../store'; -import { FakeStoreBuilder } from '../../../../../store/__mocks__/storeBuilder'; import { BrandingData, BRANDING_DEFAULT, } from '../../../../../services/bootstrap/branding.constant'; -import * as InfrastructureNamespacesStore from '../../../../../store/InfrastructureNamespaces'; +import { AppThunk } from '../../../../../store'; import { selectBranding } from '../../../../../store/Branding/selectors'; -import { selectUserProfile } from '../../../../../store/UserProfile/selectors'; +import * as InfrastructureNamespacesStore from '../../../../../store/InfrastructureNamespaces'; +import { FakeStoreBuilder } from '../../../../../store/__mocks__/storeBuilder'; jest.mock('../../../../../store/InfrastructureNamespaces', () => { return { diff --git a/packages/dashboard-frontend/src/services/dashboard-backend-client/serverConfigApi.ts b/packages/dashboard-frontend/src/services/dashboard-backend-client/serverConfigApi.ts index f703d9bc3..6d093868b 100644 --- a/packages/dashboard-frontend/src/services/dashboard-backend-client/serverConfigApi.ts +++ b/packages/dashboard-frontend/src/services/dashboard-backend-client/serverConfigApi.ts @@ -10,8 +10,8 @@ * Red Hat, Inc. - initial API and implementation */ +import { api } from '@eclipse-che/common'; import axios from 'axios'; -import common, { api } from '@eclipse-che/common'; import { prefix } from './const'; /**