Skip to content

Commit 53e8c5b

Browse files
authoredOct 17, 2024
ACM-14858-Application-detailed-page-are-not-loaded-with-new-appsubs (stolostron#3970)
* ACM-14858-Application-detailed-page-are-not-loaded-with-new-appsubs Signed-off-by: John Swanke <jswanke@redhat.com> * fix tests Signed-off-by: John Swanke <jswanke@redhat.com> * snap Signed-off-by: John Swanke <jswanke@redhat.com> * snap Signed-off-by: John Swanke <jswanke@redhat.com> --------- Signed-off-by: John Swanke <jswanke@redhat.com>
1 parent e5b57ff commit 53e8c5b

File tree

7 files changed

+114
-67
lines changed

7 files changed

+114
-67
lines changed
 

‎backend/src/lib/server-side-events.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@ import { randomString } from './random-string'
1111
// TODO - RESET EVENT
1212
// TODO BOOKMARK EVENT
1313

14-
// If a client hasn't finished receiving a broadcast in over an hour
14+
// If a client hasn't finished receiving a broadcast in PURGE_CLIENT_TIMEOUT
1515
// assume the browser has been refreshed or closed
16-
const PURGE_CLIENT_TIMEOUT = 60 * 60 * 1000
16+
const PURGE_CLIENT_TIMEOUT = 10 * 60 * 1000
1717

1818
const instanceID = randomString(8)
1919

‎backend/src/routes/aggregators/applications.ts

+8-19
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ appKeys.forEach((key) => {
8888

8989
export function getApplications() {
9090
const items: ITransformedResource[] = []
91-
aggregateKubeApplications(false)
91+
aggregateKubeApplications()
9292
Object.keys(applicationCache).forEach((key) => {
9393
if (applicationCache[key].resources) {
9494
items.push(...applicationCache[key].resources)
@@ -102,18 +102,9 @@ export function getApplications() {
102102

103103
export function startAggregatingApplications() {
104104
void discoverSystemAppNamespacePrefixes()
105-
void localKubeLoop()
106105
void searchAPILoop()
107106
}
108107

109-
// aggregate local applications found in kube every 5 seconds
110-
async function localKubeLoop(): Promise<void> {
111-
while (!stopping) {
112-
aggregateKubeApplications(true)
113-
await new Promise((r) => setTimeout(r, 5000))
114-
}
115-
}
116-
117108
async function searchAPILoop(): Promise<void> {
118109
let pass = 1
119110
while (!stopping) {
@@ -122,18 +113,16 @@ async function searchAPILoop(): Promise<void> {
122113
}
123114
}
124115

125-
export function aggregateKubeApplications(force: boolean) {
116+
export function aggregateKubeApplications() {
126117
// ACM Apps
127-
let resources = getKubeResources('Application', 'app.k8s.io/v1beta1')
128-
if (force || resources.length !== applicationCache['subscription'].resources.length) {
129-
applicationCache['subscription'] = generateTransforms(structuredClone(resources))
130-
}
118+
applicationCache['subscription'] = generateTransforms(
119+
structuredClone(getKubeResources('Application', 'app.k8s.io/v1beta1'))
120+
)
131121

132122
// AppSets
133-
resources = getKubeResources('ApplicationSet', 'argoproj.io/v1alpha1')
134-
if (force || resources.length !== applicationCache['appset'].resources.length) {
135-
applicationCache['appset'] = generateTransforms(structuredClone(resources))
136-
}
123+
applicationCache['appset'] = generateTransforms(
124+
structuredClone(getKubeResources('ApplicationSet', 'argoproj.io/v1alpha1'))
125+
)
137126
}
138127

139128
export async function aggregateSearchAPIApplications(pass: number) {

‎backend/test/routes/aggregator.test.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ describe(`aggregator Route`, function () {
1919
setupNocks()
2020

2121
// fill in application cache from resourceCache and search api mocks
22-
aggregateKubeApplications(true)
22+
aggregateKubeApplications()
2323
await aggregateSearchAPIApplications(1)
2424

2525
// NO FILTER
@@ -44,7 +44,7 @@ describe(`aggregator Route`, function () {
4444
setupNocks()
4545

4646
// fill in application cache from resourceCache and search api mocks
47-
aggregateKubeApplications(true)
47+
aggregateKubeApplications()
4848
await aggregateSearchAPIApplications(1)
4949

5050
// FILTERED
@@ -75,7 +75,7 @@ describe(`aggregator Route`, function () {
7575
// fill in application cache from resourceCache and search api mocks
7676
const prefixes = await discoverSystemAppNamespacePrefixes()
7777
expect(JSON.stringify(prefixes)).toEqual(JSON.stringify(systemPrefixes))
78-
aggregateKubeApplications(true)
78+
aggregateKubeApplications()
7979
await aggregateSearchAPIApplications(1)
8080

8181
// FILTERED

‎frontend/src/routes/Applications/CreateApplication/CreateApplicationArgo.tsx

+28-10
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { useData, useItem } from '@patternfly-labs/react-form-wizard'
44
import { ArgoWizard } from '../../../wizards/Argo/ArgoWizard'
55
import { AcmToastContext } from '../../../ui-components'
66
import moment from 'moment-timezone'
7-
import { useContext } from 'react'
7+
import { useContext, useEffect, useState } from 'react'
88
import { generatePath, useNavigate } from 'react-router-dom-v5-compat'
99
import { useRecoilValue, useSharedAtoms, useSharedSelectors } from '../../../shared-recoil'
1010
import { SyncEditor } from '../../../components/SyncEditor/SyncEditor'
@@ -22,6 +22,7 @@ import {
2222
import { argoAppSetQueryString } from './actions'
2323
import schema from './schema.json'
2424
import { LostChangesContext } from '../../../components/LostChanges'
25+
import { LoadingPage } from '../../../components/LoadingPage'
2526

2627
export default function CreateArgoApplicationSetPage() {
2728
return <CreateApplicationArgo />
@@ -94,8 +95,32 @@ export function CreateApplicationArgo() {
9495
: moment.tz.names()
9596

9697
const { cancelForm, submitForm } = useContext(LostChangesContext)
98+
const [createdResource, setCreatedResource] = useState<any>()
9799

98-
return (
100+
// don't navigate to details page until application exists in recoil
101+
useEffect(() => {
102+
if (createdResource) {
103+
if (
104+
applicationSets.findIndex(
105+
(appset) =>
106+
appset.metadata.name === createdResource.metadata.name! &&
107+
appset.metadata.namespace === createdResource.metadata.namespace!
108+
) !== -1
109+
) {
110+
navigate({
111+
pathname: generatePath(NavigationPath.applicationOverview, {
112+
namespace: createdResource?.metadata?.namespace ?? '',
113+
name: createdResource?.metadata?.name ?? '',
114+
}),
115+
search: argoAppSetQueryString,
116+
})
117+
}
118+
}
119+
}, [applicationSets, createdResource, navigate])
120+
121+
return createdResource ? (
122+
<LoadingPage />
123+
) : (
99124
<ArgoWizard
100125
createClusterSetCallback={() => open(NavigationPath.clusterSets, '_blank')}
101126
ansibleCredentials={availableAnsibleCredentials}
@@ -131,14 +156,7 @@ export function CreateApplicationArgo() {
131156
})
132157
}
133158
submitForm()
134-
135-
navigate({
136-
pathname: generatePath(NavigationPath.applicationOverview, {
137-
namespace: applicationSet?.metadata?.namespace ?? '',
138-
name: applicationSet?.metadata?.name ?? '',
139-
}),
140-
search: argoAppSetQueryString,
141-
})
159+
setCreatedResource(applicationSet)
142160
})
143161
}}
144162
timeZones={timeZones}

‎frontend/src/routes/Applications/CreateApplication/CreateApplicationArgoPullModel.tsx

+28-10
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { useData, useItem } from '@patternfly-labs/react-form-wizard'
44
import { ArgoWizard } from '../../../wizards/Argo/ArgoWizard'
55
import { AcmToastContext } from '../../../ui-components'
66
import moment from 'moment-timezone'
7-
import { useContext } from 'react'
7+
import { useContext, useEffect, useState } from 'react'
88
import { generatePath, useNavigate } from 'react-router-dom-v5-compat'
99
import { useRecoilValue, useSharedAtoms, useSharedSelectors } from '../../../shared-recoil'
1010
import { SyncEditor } from '../../../components/SyncEditor/SyncEditor'
@@ -22,6 +22,7 @@ import {
2222
import { argoAppSetQueryString } from './actions'
2323
import schema from './pullmodelschema.json'
2424
import { LostChangesContext } from '../../../components/LostChanges'
25+
import { LoadingPage } from '../../../components/LoadingPage'
2526

2627
export default function CreateArgoApplicationSetPullModelPage() {
2728
return <CreateApplicationArgoPullModel />
@@ -94,8 +95,32 @@ export function CreateApplicationArgoPullModel() {
9495
: moment.tz.names()
9596

9697
const { cancelForm, submitForm } = useContext(LostChangesContext)
98+
const [createdResource, setCreatedResource] = useState<any>()
9799

98-
return (
100+
// don't navigate to details page until application exists in recoil
101+
useEffect(() => {
102+
if (createdResource) {
103+
if (
104+
applicationSets.findIndex(
105+
(appset) =>
106+
appset.metadata.name === createdResource.metadata.name! &&
107+
appset.metadata.namespace === createdResource.metadata.namespace!
108+
) !== -1
109+
) {
110+
navigate({
111+
pathname: generatePath(NavigationPath.applicationOverview, {
112+
namespace: createdResource?.metadata?.namespace ?? '',
113+
name: createdResource?.metadata?.name ?? '',
114+
}),
115+
search: argoAppSetQueryString,
116+
})
117+
}
118+
}
119+
}, [applicationSets, createdResource, navigate])
120+
121+
return createdResource ? (
122+
<LoadingPage />
123+
) : (
99124
<ArgoWizard
100125
createClusterSetCallback={() => open(NavigationPath.clusterSets, '_blank')}
101126
ansibleCredentials={availableAnsibleCredentials}
@@ -131,14 +156,7 @@ export function CreateApplicationArgoPullModel() {
131156
})
132157
}
133158
submitForm()
134-
135-
navigate({
136-
pathname: generatePath(NavigationPath.applicationOverview, {
137-
namespace: applicationSet?.metadata?.namespace ?? '',
138-
name: applicationSet?.metadata?.name ?? '',
139-
}),
140-
search: argoAppSetQueryString,
141-
})
159+
setCreatedResource(applicationSet)
142160
})
143161
}}
144162
timeZones={timeZones}

‎frontend/src/routes/Applications/Overview.tsx

+18-15
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,7 @@ export default function ApplicationsOverview() {
341341
})
342342

343343
const [requestedView, setRequestedView] = useState<IRequestListView>()
344+
const [deletedApps, setDeletedApps] = useState<IResource[]>([])
344345

345346
const [pluginModal, setPluginModal] = useState<JSX.Element>()
346347

@@ -430,17 +431,23 @@ export default function ApplicationsOverview() {
430431
resultCounts.itemCount = resultView.processedItemCount
431432
const { systemAppNSPrefixes } = resultCounts
432433
const allApplications = resultView.items
433-
const { refresh: listRefresh } = resultView
434-
const { refresh: countRefresh } = resultCounts
435434

436435
const fetchAggregateForExport = async (requestedExport: IRequestListView) => {
437436
return fetchAggregate(SupportedAggregate.applications, backendUrl, requestedExport)
438437
}
439438

440-
const tableItems: IResource[] = useMemo(
441-
() => [...allApplications.map((app) => generateTransformData(app))],
442-
[allApplications, generateTransformData]
443-
)
439+
const tableItems: IResource[] = useMemo(() => {
440+
const items = allApplications
441+
/* istanbul ignore next */
442+
deletedApps.forEach((dapp) => {
443+
const inx = items.findIndex((app) => dapp.metadata?.uid === app.metadata?.uid)
444+
if (inx !== -1) {
445+
items.splice(inx, 1)
446+
resultCounts.itemCount -= 1
447+
}
448+
})
449+
return items.map((app) => generateTransformData(app))
450+
}, [allApplications, deletedApps, generateTransformData, resultCounts])
444451

445452
const keyFn = useCallback(
446453
(resource: IResource) => resource.metadata!.uid ?? `${resource.metadata!.namespace}/${resource.metadata!.name}`,
@@ -927,11 +934,11 @@ export default function ApplicationsOverview() {
927934
appSetsSharingPlacement: appSetRelatedResources[1],
928935
appKind: resource.kind,
929936
appSetApps: getAppSetApps(argoApplications, resource.metadata?.name!),
930-
deleted: /* istanbul ignore next */ () => {
931-
resultView.refresh()
932-
resultCounts.refresh()
933-
listRefresh()
934-
countRefresh()
937+
deleted: /* istanbul ignore next */ (app: IResource) => {
938+
setDeletedApps((arr) => {
939+
arr = [app, ...arr].slice(0, 10)
940+
return arr
941+
})
935942
},
936943
close: () => {
937944
setModalProps({ open: false })
@@ -979,10 +986,6 @@ export default function ApplicationsOverview() {
979986
channels,
980987
applicationSets,
981988
argoApplications,
982-
resultView,
983-
resultCounts,
984-
listRefresh,
985-
countRefresh,
986989
canCreateApplication,
987990
]
988991
)

‎frontend/src/routes/Applications/SubscriptionApplication.tsx

+27-8
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ import { useAllClusters } from '../Infrastructure/Clusters/ManagedClusters/compo
5050
import { CredentialsForm } from '../Credentials/CredentialsForm'
5151
import { useProjects } from '../../hooks/useProjects'
5252
import { setAvailableConnections } from '../Infrastructure/Clusters/ManagedClusters/CreateCluster/controlData/ControlDataHelpers'
53+
import { LoadingPage } from '../../components/LoadingPage'
5354

5455
interface CreationStatus {
5556
status: string
@@ -178,6 +179,7 @@ export function CreateSubscriptionApplication(
178179
) {
179180
const navigate = useNavigate()
180181
const { t } = useTranslation()
182+
const [createdResource, setCreatedResource] = useState<any>()
181183
const {
182184
ansibleJobState,
183185
applicationsState,
@@ -218,13 +220,7 @@ export function CreateSubscriptionApplication(
218220
type: 'success',
219221
autoClose: true,
220222
})
221-
navigate({
222-
pathname: generatePath(NavigationPath.applicationOverview, {
223-
namespace: applicationResourceJSON.metadata.namespace!,
224-
name: applicationResourceJSON.metadata.name!,
225-
}),
226-
search: location.search,
227-
})
223+
setCreatedResource(applicationResourceJSON)
228224
})
229225
.catch((err) => {
230226
const errorInfo = getErrorInfo(err, t)
@@ -392,6 +388,27 @@ export function CreateSubscriptionApplication(
392388
const editApplication = getEditApplication(location)
393389
const searchParams = useSearchParams()
394390

391+
// don't navigate to details page until application exists in recoil
392+
useEffect(() => {
393+
if (createdResource) {
394+
if (
395+
applications.findIndex(
396+
(app) =>
397+
app.metadata.name === createdResource.metadata.name! &&
398+
app.metadata.namespace === createdResource.metadata.namespace!
399+
) !== -1
400+
) {
401+
navigate({
402+
pathname: generatePath(NavigationPath.applicationOverview, {
403+
namespace: createdResource.metadata.namespace!,
404+
name: createdResource.metadata.name!,
405+
}),
406+
search: location.search,
407+
})
408+
}
409+
}
410+
}, [applications, createdResource, location, navigate])
411+
395412
useEffect(() => {
396413
if (editApplication) {
397414
const { selectedAppName, selectedAppNamespace } = editApplication
@@ -468,7 +485,9 @@ export function CreateSubscriptionApplication(
468485

469486
const isFetchControl = editApplication ? fetchControl : true
470487

471-
return (
488+
return createdResource ? (
489+
<LoadingPage />
490+
) : (
472491
isFetchControl && (
473492
<TemplateEditor
474493
type={'application'}

0 commit comments

Comments
 (0)