diff --git a/services/frontend-service/src/ui/App/index.test.tsx b/services/frontend-service/src/ui/App/index.test.tsx index d291303e61..8a992d8e2f 100644 --- a/services/frontend-service/src/ui/App/index.test.tsx +++ b/services/frontend-service/src/ui/App/index.test.tsx @@ -30,6 +30,7 @@ Spy.mockModule('../utils/AzureAuthProvider', 'AzureAuthProvider'); const mock_GetConfig = Spy('Config'); const mock_StreamOverview = Spy('Overview'); +const mock_GetAllAppLocks = Spy('AllAppLocks'); const mock_StreamStatus = Spy('Status'); jest.mock('../utils/GrpcApi', () => ({ @@ -41,6 +42,7 @@ jest.mock('../utils/GrpcApi', () => ({ }), overviewService: () => ({ StreamOverview: () => mock_StreamOverview(), + GetAllAppLocks: () => mock_GetAllAppLocks(), }), rolloutService: () => ({ StreamStatus: () => mock_StreamStatus(), @@ -73,6 +75,7 @@ describe('App uses the API', () => { observer.next({ applications: 'test-application' }); }) ); + mock_GetAllAppLocks.returns(Promise.resolve('test')); mock_GetConfig.returns(Promise.resolve('test-config')); AzureAuthSub.set({ authReady: true }); mock_StreamStatus.returns( @@ -102,6 +105,7 @@ describe('App uses the API', () => { observer.next({}); }) ); + mock_GetAllAppLocks.returns(Promise.resolve('test')); mock_GetConfig.returns(Promise.resolve('test-config')); AzureAuthSub.set({ authReady: true }); diff --git a/services/frontend-service/src/ui/App/index.tsx b/services/frontend-service/src/ui/App/index.tsx index 890eecc751..b9238b4916 100644 --- a/services/frontend-service/src/ui/App/index.tsx +++ b/services/frontend-service/src/ui/App/index.tsx @@ -24,6 +24,7 @@ import { FlushRolloutStatus, PanicOverview, showSnackbarWarn, + UpdateAllApplicationLocks, updateAppDetails, UpdateFrontendConfig, UpdateOverview, @@ -116,6 +117,15 @@ export const App: React.FC = () => { } }); updateAppDetails.set(details); + // Get App Locks + api.overviewService() + .GetAllAppLocks({}, authHeader) + .then((res) => { + UpdateAllApplicationLocks.set(res.allAppLocks); + }) + .catch((e) => { + PanicOverview.set({ error: JSON.stringify({ msg: 'error in GetAllAppLocks', e }) }); + }); }, (error) => { PanicOverview.set({ error: JSON.stringify({ msg: 'error in streamoverview', error }) }); diff --git a/services/frontend-service/src/ui/Pages/Locks/LocksPage.test.tsx b/services/frontend-service/src/ui/Pages/Locks/LocksPage.test.tsx index 57a7bf43b0..1c00a6fb64 100644 --- a/services/frontend-service/src/ui/Pages/Locks/LocksPage.test.tsx +++ b/services/frontend-service/src/ui/Pages/Locks/LocksPage.test.tsx @@ -17,13 +17,14 @@ import { render, renderHook } from '@testing-library/react'; import { LocksPage } from './LocksPage'; import { DisplayLock, + UpdateAllApplicationLocks, UpdateOverview, useAllLocks, useEnvironmentLock, useFilteredEnvironmentLockIDs, } from '../../utils/store'; import { MemoryRouter } from 'react-router-dom'; -import { Environment, OverviewApplication, Priority } from '../../../api/api'; +import { AllAppLocks, Environment, OverviewApplication, Priority } from '../../../api/api'; import { fakeLoadEverything, enableDexAuth } from '../../../setupTests'; describe('LocksPage', () => { @@ -332,6 +333,9 @@ describe('Test app locks', () => { name: string; envs: Environment[]; OverviewApps: OverviewApplication[]; + AppLocks: { + [key: string]: AllAppLocks; + }; sortOrder: 'oldestToNewest' | 'newestToOldest'; expectedLockIDs: string[]; } @@ -343,6 +347,7 @@ describe('Test app locks', () => { OverviewApps: [], sortOrder: 'oldestToNewest', expectedLockIDs: [], + AppLocks: {}, }, { name: 'get one lock', @@ -366,6 +371,15 @@ describe('Test app locks', () => { priority: 0, }, ], + AppLocks: { + integration: { + appLocks: { + foo: { + locks: [{ message: 'locktest', lockId: 'ui-v2-1337' }], + }, + }, + }, + }, sortOrder: 'oldestToNewest', expectedLockIDs: ['ui-v2-1337'], }, @@ -407,6 +421,31 @@ describe('Test app locks', () => { priority: 0, }, ], + AppLocks: { + integration: { + appLocks: { + foo: { + locks: [ + { + message: 'locktest', + lockId: 'ui-v2-1337', + createdAt: new Date(1995, 11, 17), + }, + { + message: 'lockfoo', + lockId: 'ui-v2-123', + createdAt: new Date(1995, 11, 16), + }, + { + message: 'lockbar', + lockId: 'ui-v2-321', + createdAt: new Date(1995, 11, 15), + }, + ], + }, + }, + }, + }, sortOrder: 'newestToOldest', expectedLockIDs: ['ui-v2-1337', 'ui-v2-123', 'ui-v2-321'], }, @@ -460,6 +499,35 @@ describe('Test app locks', () => { priority: 0, }, ], + AppLocks: { + integration: { + appLocks: { + foo: { + locks: [ + { + message: 'lockbar', + lockId: 'ui-v2-321', + createdAt: new Date(1995, 11, 15), + }, + ], + }, + bar: { + locks: [ + { + message: 'lockfoo', + lockId: 'ui-v2-123', + createdAt: new Date(1995, 11, 16), + }, + { + message: 'locktest', + lockId: 'ui-v2-1337', + createdAt: new Date(1995, 11, 17), + }, + ], + }, + }, + }, + }, sortOrder: 'oldestToNewest', expectedLockIDs: ['ui-v2-321', 'ui-v2-123', 'ui-v2-1337'], }, @@ -480,6 +548,7 @@ describe('Test app locks', () => { }, ], }); + UpdateAllApplicationLocks.set(testcase.AppLocks); // when const obtained = renderHook(() => useAllLocks().appLocks).result.current; diff --git a/services/frontend-service/src/ui/Pages/Locks/LocksPage.tsx b/services/frontend-service/src/ui/Pages/Locks/LocksPage.tsx index 035912480d..3b5f5b67a1 100644 --- a/services/frontend-service/src/ui/Pages/Locks/LocksPage.tsx +++ b/services/frontend-service/src/ui/Pages/Locks/LocksPage.tsx @@ -16,8 +16,10 @@ Copyright freiheit.com*/ import React, { useMemo } from 'react'; import { LocksTable } from '../../components/LocksTable/LocksTable'; import { + DisplayLock, searchCustomFilter, sortLocks, + useAllApplicationLocks, useApplications, useEnvironments, useGlobalLoadingState, @@ -25,6 +27,7 @@ import { } from '../../utils/store'; import { useSearchParams } from 'react-router-dom'; import { TopAppBar } from '../../components/TopAppBar/TopAppBar'; +import { Locks } from '../../../api/api'; const applicationFieldHeaders = [ 'Date', @@ -45,6 +48,7 @@ export const LocksPage: React.FC = () => { const appNameParam = params.get('application'); const envs = useEnvironments(); const allApps = useApplications(); + const allAppLocks = useAllApplicationLocks((map) => map); let teamLocks = useTeamLocks(allApps); const envLocks = useMemo( () => @@ -68,34 +72,30 @@ export const LocksPage: React.FC = () => { teamLocks = useMemo(() => sortLocks(teamLocks, 'oldestToNewest'), [teamLocks]); - //Goes through all envs and all apps and checks for locks for each app - const appLocks = useMemo( - () => - sortLocks( - Object.values(envs) - .map((env) => - allApps - .map((app) => - env.appLocks[app.name] - ? env.appLocks[app.name].locks.map((lock) => ({ - date: lock.createdAt, - environment: env.name, - application: app.name, - lockId: lock.lockId, - message: lock.message, - authorName: lock.createdBy?.name, - authorEmail: lock.createdBy?.email, - })) - : [] - ) - .flat() - ) - .flat() - .filter((lock) => searchCustomFilter(appNameParam, lock.application)), - 'oldestToNewest' - ), - [appNameParam, envs, allApps] - ); + const appLocks = useMemo(() => { + const allAppLocksDisplay: DisplayLock[] = []; + const map = new Map(Object.entries(allAppLocks)); + map.forEach((appLocksForEnv, env): void => { + const currAppLocks = new Map(Object.entries(appLocksForEnv.appLocks)); + currAppLocks.forEach((currentAppInfo, app) => { + currentAppInfo.locks.map((lock) => + allAppLocksDisplay.push({ + date: lock.createdAt, + environment: env, + application: app, + lockId: lock.lockId, + message: lock.message, + authorName: lock.createdBy?.name, + authorEmail: lock.createdBy?.email, + }) + ); + }); + }); + return sortLocks( + allAppLocksDisplay.flat().filter((lock) => searchCustomFilter(appNameParam, lock.application)), + 'oldestToNewest' + ); + }, [allAppLocks, appNameParam]); const element = useGlobalLoadingState(); if (element) { diff --git a/services/frontend-service/src/ui/utils/store.test.tsx b/services/frontend-service/src/ui/utils/store.test.tsx index 9faa7b1cf5..7e4618a582 100644 --- a/services/frontend-service/src/ui/utils/store.test.tsx +++ b/services/frontend-service/src/ui/utils/store.test.tsx @@ -25,6 +25,7 @@ import { SnackbarStatus, UpdateAction, updateActions, + UpdateAllApplicationLocks, updateAppDetails, UpdateOverview, UpdateRolloutStatus, @@ -36,6 +37,7 @@ import { useRolloutStatus, } from './store'; import { + AllAppLocks, BatchAction, Environment, EnvironmentGroup, @@ -59,6 +61,7 @@ describe('Test useLocksSimilarTo', () => { inputAction: BatchAction; // the action we are rendering currently in the sidebar expectedLocks: AllLocks; OverviewApps: OverviewApplication[]; + AppLocks: { [key: string]: AllAppLocks }; }; const testdata: TestDataStore[] = [ @@ -74,6 +77,7 @@ describe('Test useLocksSimilarTo', () => { }, }, OverviewApps: [], + AppLocks: {}, inputEnvGroups: [], expectedLocks: { appLocks: [], @@ -93,6 +97,7 @@ describe('Test useLocksSimilarTo', () => { }, }, OverviewApps: [], + AppLocks: {}, inputEnvGroups: [ { environments: [ @@ -130,6 +135,7 @@ describe('Test useLocksSimilarTo', () => { }, }, OverviewApps: [], + AppLocks: {}, inputEnvGroups: [ { environments: [ @@ -181,6 +187,15 @@ describe('Test useLocksSimilarTo', () => { }, }, }, + AppLocks: { + dev: { + appLocks: { + betty: { + locks: [makeLock({ lockId: 'l1' })], + }, + }, + }, + }, OverviewApps: [{ name: 'betty', team: '' }], inputEnvGroups: [ { @@ -236,6 +251,15 @@ describe('Test useLocksSimilarTo', () => { team: 'test-team', }, ], + AppLocks: { + dev: { + appLocks: { + betty: { + locks: [makeLock({ lockId: 'l1' })], + }, + }, + }, + }, inputEnvGroups: [ { environments: [ @@ -314,6 +338,7 @@ describe('Test useLocksSimilarTo', () => { lightweightApps: testcase.OverviewApps, environmentGroups: testcase.inputEnvGroups, }); + UpdateAllApplicationLocks.set(testcase.AppLocks); // when const actions = renderHook(() => useLocksSimilarTo(testcase.inputAction)).result.current; // then @@ -803,6 +828,9 @@ describe('Test useLocksConflictingWithActions', () => { expectedEnvLocks: DisplayLock[]; environments: Environment[]; OverviewApps: OverviewApplication[]; + AppLocks: { + [key: string]: AllAppLocks; + }; }; const testdata: TestDataStore[] = [ @@ -813,6 +841,7 @@ describe('Test useLocksConflictingWithActions', () => { expectedEnvLocks: [], environments: [], OverviewApps: [], + AppLocks: {}, }, { name: 'deploy action and related app lock and env lock', @@ -860,6 +889,20 @@ describe('Test useLocksConflictingWithActions', () => { priority: 0, }, ], + AppLocks: { + dev: { + appLocks: { + app1: { + locks: [ + makeLock({ + lockId: 'app-lock-id', + message: 'i do not like this app', + }), + ], + }, + }, + }, + }, expectedAppLocks: [ makeDisplayLock({ lockId: 'app-lock-id', @@ -890,7 +933,7 @@ describe('Test useLocksConflictingWithActions', () => { $case: 'deploy', deploy: { environment: 'dev', - application: 'app1', + application: 'app2', version: 1, ignoreAllLocks: false, lockBehavior: LockBehavior.IGNORE, @@ -922,6 +965,20 @@ describe('Test useLocksConflictingWithActions', () => { priority: 0, }, ], + AppLocks: { + staging: { + appLocks: { + anotherapp: { + locks: [ + makeLock({ + lockId: 'app-lock-id', + message: 'i do not like this app', + }), + ], + }, + }, + }, + }, expectedAppLocks: [], expectedEnvLocks: [], }, @@ -942,7 +999,7 @@ describe('Test useLocksConflictingWithActions', () => { }, ], }); - + UpdateAllApplicationLocks.set(testcase.AppLocks); // when const actualLocks = renderHook(() => useLocksConflictingWithActions()).result.current; // then diff --git a/services/frontend-service/src/ui/utils/store.tsx b/services/frontend-service/src/ui/utils/store.tsx index 7f461c732f..2ac7f4b539 100644 --- a/services/frontend-service/src/ui/utils/store.tsx +++ b/services/frontend-service/src/ui/utils/store.tsx @@ -33,6 +33,8 @@ import { GetFailedEslsResponse, GetAppDetailsResponse, OverviewApplication, + AllAppLocks, + Locks, } from '../../api/api'; import * as React from 'react'; import { useCallback, useMemo } from 'react'; @@ -75,11 +77,17 @@ const emptyOverview: EnhancedOverview = { branch: '', manifestRepoUrl: '', }; + const [useOverview, UpdateOverview_] = createStore(emptyOverview); export const UpdateOverview = UpdateOverview_; // we do not want to export "useOverview". The store.tsx should act like a facade to the data. export const useOverviewLoaded = (): boolean => useOverview(({ loaded }) => loaded); +export const emptyAppLocks: { [key: string]: AllAppLocks } = {}; +export const [useAllApplicationLocks, UpdateAllApplicationLocks] = createStore<{ [key: string]: AllAppLocks }>( + emptyAppLocks +); + type TagsResponse = { response: GetGitTagsResponse; tagsReady: boolean; @@ -583,26 +591,26 @@ export const useTeamLocks = (allApps: OverviewApplication[]): DisplayLock[] => ) ); -export const useAppLocks = (allApps: OverviewApplication[]): DisplayLock[] => - Object.values(useEnvironments()) - .map((env) => - allApps - .map((app) => - env.appLocks[app.name] - ? env.appLocks[app.name].locks.map((lock) => ({ - date: lock.createdAt, - environment: env.name, - application: app.name, - lockId: lock.lockId, - message: lock.message, - authorName: lock.createdBy?.name, - authorEmail: lock.createdBy?.email, - })) - : [] - ) - .flat() - ) - .flat(); +export const useAppLocks = (allAppLocks: Map): DisplayLock[] => { + const allAppLocksDisplay: DisplayLock[] = []; + allAppLocks.forEach((appLocksForEnv, env): void => { + const currAppLocks = new Map(Object.entries(appLocksForEnv.appLocks)); + currAppLocks.forEach((currentAppInfo, app) => { + currentAppInfo.locks.map((lock) => + allAppLocksDisplay.push({ + date: lock.createdAt, + environment: env, + application: app, + lockId: lock.lockId, + message: lock.message, + authorName: lock.createdBy?.name, + authorEmail: lock.createdBy?.email, + }) + ); + }); + }); + return allAppLocksDisplay; +}; /** * returns the classname according to the priority of an environment, used to color environments */ @@ -795,9 +803,10 @@ export type AllLocks = { export const useAllLocks = (): AllLocks => { const envs = useEnvironments(); const allApps = useApplications(); + const allAppLocks = useAllApplicationLocks((map) => map); const teamLocks = useTeamLocks(allApps); const environmentLocks: DisplayLock[] = []; - const appLocks = useAppLocks(allApps); + const appLocks = useAppLocks(new Map(Object.entries(allAppLocks))); envs.forEach((env: Environment) => { for (const locksKey in env.locks) { const lock = env.locks[locksKey];