Skip to content

Commit f739621

Browse files
authored
chore(selector): refactor to simplify state management (#104)
1 parent 081ea1c commit f739621

File tree

7 files changed

+225
-161
lines changed

7 files changed

+225
-161
lines changed

.eslintrc.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ env:
44
extends:
55
- eslint:recommended
66
- plugin:react/recommended
7+
- plugin:react-hooks/recommended
78
- plugin:@typescript-eslint/recommended
89
- prettier
910
parser: '@typescript-eslint/parser'
@@ -15,6 +16,7 @@ parserOptions:
1516
plugins:
1617
- prettier
1718
- react
19+
- react-hooks
1820
- '@typescript-eslint'
1921
rules:
2022
prettier/prettier:
@@ -26,3 +28,6 @@ rules:
2628
settings:
2729
react:
2830
version: detect
31+
react-hooks:
32+
rules-of-hooks: never
33+
exhaustive-deps: never

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@
101101
},
102102
"dependencies": {
103103
"concurrently": "^9.1.2",
104+
"eslint-plugin-react-hooks": "^5.1.0",
104105
"license-check-and-add": "^4.0.5",
105106
"rxjs": "^7.8.0"
106107
},

src/openshift/components/CryostatContainer.tsx

Lines changed: 135 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,25 @@ import { TargetsService } from '@app/Shared/Services/Targets.service';
3131
import { pluginServices } from '@console-plugin/services/PluginContext';
3232
import { map, Observable, of } from 'rxjs';
3333
import CryostatSelector from './CryostatSelector';
34-
import { Alert, AlertGroup, Card, CardBody, CardTitle, Text, TextVariants } from '@patternfly/react-core';
34+
import {
35+
Alert,
36+
AlertGroup,
37+
Bullseye,
38+
Card,
39+
CardBody,
40+
CardTitle,
41+
Spinner,
42+
Text,
43+
TextVariants,
44+
} from '@patternfly/react-core';
3545
import { DisconnectedIcon } from '@patternfly/react-icons';
3646
import {
3747
getConsoleRequestHeaders,
3848
getCSRFToken,
3949
} from '@openshift-console/dynamic-plugin-sdk/lib/utils/fetch/console-fetch-utils';
4050
import { useSubscriptions } from '@app/utils/hooks/useSubscriptions';
4151
import { Capabilities, CapabilitiesContext } from '@app/Shared/Services/Capabilities';
52+
import { K8sResourceCommon, useActiveNamespace, useK8sWatchResource } from '@openshift-console/dynamic-plugin-sdk';
4253

4354
export const SESSIONSTORAGE_SVC_NS_KEY = 'cryostat-svc-ns';
4455
export const SESSIONSTORAGE_SVC_NAME_KEY = 'cryostat-svc-name';
@@ -92,19 +103,37 @@ const services = (svc: CryostatService): Services => {
92103
};
93104
};
94105

106+
const LoadingState: React.FC = () => {
107+
return (
108+
<Bullseye>
109+
<Spinner />
110+
</Bullseye>
111+
);
112+
};
113+
114+
/* eslint-disable @typescript-eslint/no-explicit-any */
115+
const ErrorState: React.FC<{ err: any }> = (err) => {
116+
return (
117+
<Card>
118+
<CardTitle>Error</CardTitle>
119+
<CardBody>
120+
<Text component={TextVariants.p}>{JSON.stringify(err, null, 2)}</Text>
121+
</CardBody>
122+
</Card>
123+
);
124+
};
125+
95126
const EmptyState: React.FC = () => {
96127
return (
97-
<>
98-
<Card>
99-
<CardTitle>
100-
<DisconnectedIcon />
101-
&nbsp; No instance selected
102-
</CardTitle>
103-
<CardBody>
104-
<Text component={TextVariants.p}>To view this content, select a Cryostat instance.</Text>
105-
</CardBody>
106-
</Card>
107-
</>
128+
<Card>
129+
<CardTitle>
130+
<DisconnectedIcon />
131+
&nbsp; No instance selected
132+
</CardTitle>
133+
<CardBody>
134+
<Text component={TextVariants.p}>To view this content, select a Cryostat instance.</Text>
135+
</CardBody>
136+
</Card>
108137
);
109138
};
110139

@@ -116,18 +145,19 @@ const NotificationGroup: React.FC = () => {
116145

117146
const addSubscription = useSubscriptions();
118147

119-
React.useEffect(() => {
148+
React.useLayoutEffect(() => {
120149
services.notificationChannel.disconnect();
121150
services.notificationChannel.connect();
122151
services.targets.queryForTargets().subscribe();
123152
services.api.testBaseServer();
124-
}, [services.notificationChannel, services.targets, services.api]);
153+
notificationsContext.clearAll();
154+
}, [services.notificationChannel, services.targets, services.api, notificationsContext]);
125155

126156
React.useEffect(() => {
127157
addSubscription(services.settings.visibleNotificationsCount().subscribe(setVisibleNotificationsCount));
128158
}, [addSubscription, services.settings, setVisibleNotificationsCount]);
129159

130-
React.useEffect(() => {
160+
React.useLayoutEffect(() => {
131161
addSubscription(
132162
notificationsContext
133163
.notifications()
@@ -167,7 +197,7 @@ const NotificationGroup: React.FC = () => {
167197
)
168198
.subscribe((n) => setNotifications([...n])),
169199
);
170-
}, [notificationsContext, addSubscription, visibleNotificationsCount]);
200+
}, [services.settings, notificationsContext, addSubscription, visibleNotificationsCount]);
171201

172202
return (
173203
<AlertGroup isToast isLiveRegion>
@@ -180,11 +210,34 @@ const NotificationGroup: React.FC = () => {
180210
);
181211
};
182212

213+
const ALL_NS = '#ALL_NS#';
214+
183215
const pluginCapabilities: Capabilities = {
184216
fileUploads: false,
185217
};
186218

187-
export const CryostatContainer: React.FC = ({ children }) => {
219+
const InstancedContainer: React.FC<{ service: CryostatService; children: React.ReactNode }> = ({
220+
service,
221+
children,
222+
}) => {
223+
return (
224+
<Provider store={store} key={service}>
225+
<CapabilitiesContext.Provider value={pluginCapabilities}>
226+
<ServiceContext.Provider value={services(service)}>
227+
<NotificationsContext.Provider value={NotificationsInstance}>
228+
<NotificationGroup />
229+
<CryostatController key={`${service.namespace}-${service.name}`}>{children}</CryostatController>
230+
</NotificationsContext.Provider>
231+
</ServiceContext.Provider>
232+
</CapabilitiesContext.Provider>
233+
</Provider>
234+
);
235+
};
236+
237+
const NamespacedContainer: React.FC<{ searchNamespace: string; children: React.ReactNode }> = ({
238+
searchNamespace,
239+
children,
240+
}) => {
188241
const [service, setService] = React.useState(() => {
189242
const namespace = sessionStorage.getItem(SESSIONSTORAGE_SVC_NS_KEY);
190243
const name = sessionStorage.getItem(SESSIONSTORAGE_SVC_NAME_KEY);
@@ -195,32 +248,79 @@ export const CryostatContainer: React.FC = ({ children }) => {
195248
return service;
196249
});
197250

198-
React.useEffect(() => {
199-
sessionStorage.setItem(SESSIONSTORAGE_SVC_NS_KEY, service.namespace);
200-
sessionStorage.setItem(SESSIONSTORAGE_SVC_NAME_KEY, service.name);
201-
}, [service, sessionStorage]);
251+
const [instances, instancesLoaded, instancesErr] = useK8sWatchResource<K8sResourceCommon[]>({
252+
isList: true,
253+
namespaced: true,
254+
namespace: searchNamespace === ALL_NS ? undefined : searchNamespace,
255+
groupVersionKind: {
256+
group: '',
257+
kind: 'Service',
258+
version: 'v1',
259+
},
260+
selector: {
261+
matchLabels: {
262+
'app.kubernetes.io/part-of': 'cryostat',
263+
'app.kubernetes.io/component': 'cryostat',
264+
},
265+
},
266+
});
267+
268+
const onSelectInstance = React.useCallback(
269+
(service: CryostatService) => {
270+
sessionStorage.setItem(SESSIONSTORAGE_SVC_NS_KEY, service.namespace);
271+
sessionStorage.setItem(SESSIONSTORAGE_SVC_NAME_KEY, service.name);
272+
setService(service);
273+
},
274+
[setService],
275+
);
276+
277+
React.useLayoutEffect(() => {
278+
if (!instancesLoaded) {
279+
return;
280+
}
281+
const selectedNs = service.namespace;
282+
const selectedName = service.name;
283+
let found = false;
284+
for (const instance of instances) {
285+
if (instance?.metadata?.namespace === selectedNs && instance?.metadata?.name === selectedName) {
286+
found = true;
287+
}
288+
}
289+
if (!found) {
290+
onSelectInstance(NO_INSTANCE);
291+
}
292+
}, [service, instances, onSelectInstance, instancesLoaded]);
202293

203-
const noSelection = React.useMemo(() => {
204-
return service.namespace == NO_INSTANCE.namespace && service.name == NO_INSTANCE.name;
205-
}, [service]);
294+
const noSelection = React.useMemo(
295+
() => service.namespace == NO_INSTANCE.namespace && service.name == NO_INSTANCE.name,
296+
[service],
297+
);
206298

207299
return (
208300
<>
209-
<CryostatSelector setSelectedCryostat={setService} />
210-
<Provider store={store}>
211-
{noSelection ? (
301+
<CryostatSelector
302+
instances={instances}
303+
renderNamespaceLabel={searchNamespace === ALL_NS}
304+
setSelectedCryostat={onSelectInstance}
305+
selection={service}
306+
/>
307+
<Provider store={store} key={service}>
308+
{instancesErr ? (
309+
<ErrorState err={instancesErr} />
310+
) : !instancesLoaded ? (
311+
<LoadingState />
312+
) : noSelection ? (
212313
<EmptyState />
213314
) : (
214-
<CapabilitiesContext.Provider value={pluginCapabilities}>
215-
<ServiceContext.Provider value={services(service)}>
216-
<NotificationsContext.Provider value={NotificationsInstance}>
217-
<NotificationGroup />
218-
<CryostatController key={`${service.namespace}-${service.name}`}>{children}</CryostatController>
219-
</NotificationsContext.Provider>
220-
</ServiceContext.Provider>
221-
</CapabilitiesContext.Provider>
315+
<InstancedContainer service={service}>{children}</InstancedContainer>
222316
)}
223317
</Provider>
224318
</>
225319
);
226320
};
321+
322+
export const CryostatContainer: React.FC = ({ children }) => {
323+
const [namespace] = useActiveNamespace();
324+
325+
return <NamespacedContainer searchNamespace={namespace}>{children}</NamespacedContainer>;
326+
};

0 commit comments

Comments
 (0)