Skip to content

Commit 4149c92

Browse files
committed
PMM-15326: Take out the OM plugin's duplicated and dead weight
Review notes from #5817, all of them things the package said twice or said wrong. `isRefreshActive` was byte-identical to `isRunActive` - same body, same signature, both on the public surface. A collection pass and an inventory refresh are different operations but report the same `RunStatus`, so "is it still going" is one question. Kept `isRunActive`, which is named for the type it takes, and pointed the seven callers at it. `RunStatusBadge` was open-coding the same comparison inline and now uses it too. Query keys were the only lower-camel module constants in either hooks file, three lines below `OM_BASE` and `RUN_POLL_MS`. Uppercased. `useOmTopology`'s docstring called the topology document "the estate", which is this package's word for the inventory side - the thing SEP's probe collects by running a payload on hosts. Using it for both is what makes the two hook files look interchangeable, and it is the likeliest reason a reviewer asked why this hook was not on the other one. Said which is which. Both triggers sent a body they do not need. `body: "*"` plus grpc-gateway's decoder treating io.EOF as an empty request means an unscoped call can omit it; the scoped inventory refresh still sends `{node_ids: [...]}`, which is what the Hosts row action needs. `toWireValue` now records why `Number(draft)` is right for a `timedelta`: SEP annotates those fields with a PlainSerializer to integer seconds, so the wire carries `1800` rather than an ISO-8601 duration. It is a dependency on the annotation, not on the type, and nothing else in either repo would have said so. Signed-off-by: Pawel Lebioda <pawel.lebioda@percona.com>
1 parent c700ad7 commit 4149c92

7 files changed

Lines changed: 76 additions & 60 deletions

File tree

ui/packages/plugins/om/src/HostsPage.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,14 +46,13 @@ import { Unavailable } from './components/Unavailable';
4646
import { formatDuration } from './format';
4747
import { ageSeconds, isFailing, toHostRows } from './inventory';
4848
import {
49-
isRefreshActive,
5049
useForgetHost,
5150
useOmInventoryHosts,
5251
useInvalidateEstateOnRefreshEnd,
5352
useOmInventoryRuns,
5453
useRefreshInventory,
5554
} from './inventoryHooks';
56-
import { OmApiError } from './hooks';
55+
import { isRunActive, OmApiError } from './hooks';
5756
import type { OmHostRow } from './types';
5857

5958
/** Identifiers and long text the table carries but does not open with. */
@@ -390,7 +389,7 @@ export function HostsPage() {
390389
// Any active run, matching InventoryPage's button: firing an estate-wide sweep into
391390
// one already in flight only earns a 409, and the row actions below cannot succeed
392391
// against a host that sweep already holds.
393-
const refreshing = (runs ?? []).some((run) => isRefreshActive(run.status));
392+
const refreshing = (runs ?? []).some((run) => isRunActive(run.status));
394393
const [forgetting, setForgetting] = useState<OmHostRow | null>(null);
395394
const columns = useColumns();
396395
const rows = useMemo(() => toHostRows(data), [data]);

ui/packages/plugins/om/src/InventoryPage.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,11 @@ import {
3131
import PlayArrowIcon from '@mui/icons-material/PlayArrow';
3232
import { Table, type MRT_ColumnDef } from '@percona/percona-ui';
3333
import {
34-
isRefreshActive,
3534
useInvalidateEstateOnRefreshEnd,
3635
useOmInventoryRuns,
3736
useRefreshInventory,
3837
} from './inventoryHooks';
39-
import { OmApiError } from './hooks';
38+
import { isRunActive, OmApiError } from './hooks';
4039
import { ConfigForm } from './components/ConfigForm';
4140
import { RunStatusBadge } from './components/HealthBadge';
4241
import { RunEntities } from './components/RunEntities';
@@ -172,7 +171,7 @@ function RefreshButton() {
172171
// and a narrow one started later can reach a terminal status while a broader one is
173172
// still probing - `runs[0]` would re-enable this button into a sweep that must
174173
// conflict with it. Same reason `useOmInventoryRuns` polls on the whole collection.
175-
const running = (runs ?? []).some((run) => isRefreshActive(run.status));
174+
const running = (runs ?? []).some((run) => isRunActive(run.status));
176175
// The rows this page's sibling tabs render change when the refresh finishes, not when
177176
// it is accepted, so the estate is invalidated on that edge rather than on the mutation.
178177
useInvalidateEstateOnRefreshEnd(runs);
@@ -235,7 +234,7 @@ function LastRun({ run }: { run: OmInventoryRun | undefined }) {
235234
);
236235
}
237236
const age = ageSeconds(run.start_time);
238-
const active = isRefreshActive(run.status);
237+
const active = isRunActive(run.status);
239238
return (
240239
<Stack
241240
direction="row"

ui/packages/plugins/om/src/components/ConfigForm.tsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,19 @@ function inputKind(setting: OmInventorySetting): InputKind {
6767
return 'text';
6868
}
6969

70-
/** Parse an edited value back into what the app expects on the wire. */
70+
/**
71+
* Parse an edited value back into what the app expects on the wire.
72+
*
73+
* `Number(draft)` is right for a `timedelta` because a `timedelta` never reaches this
74+
* form as a duration string. SEP declares those fields as
75+
* `Annotated[timedelta, PlainSerializer(lambda v: round(v.total_seconds()), int)]`, so
76+
* the configuration API dumps `1800`, not `"PT30M"`, and the box holds `"1800"`. The
77+
* write goes back as the same integer and the app coerces it.
78+
*
79+
* That is a load-bearing dependency on SEP's annotation rather than on its type: drop
80+
* the serialiser and pydantic's ISO-8601 default takes over, `Number` yields NaN, and
81+
* every duration field renders as invalid. Named here because nothing else would say so.
82+
*/
7183
function toWireValue(setting: OmInventorySetting, draft: string): unknown {
7284
switch (inputKind(setting)) {
7385
case 'bool':

ui/packages/plugins/om/src/components/HealthBadge.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
SERVICE_STATUS_COLOR,
2323
SERVICE_STATUS_LABEL,
2424
} from '../constants';
25+
import { isRunActive } from '../hooks';
2526
import type { OmTopologyRunStatus, OmServiceStatus } from '../types';
2627

2728
/**
@@ -49,7 +50,7 @@ export function RunStatusBadge({ status }: { status: OmTopologyRunStatus }) {
4950
size="small"
5051
label={RUN_STATUS_LABEL[status] ?? status}
5152
color={RUN_STATUS_COLOR[status] ?? 'default'}
52-
variant={status === 'RUN_STATUS_RUNNING' ? 'outlined' : 'filled'}
53+
variant={isRunActive(status) ? 'outlined' : 'filled'}
5354
/>
5455
);
5556
}

ui/packages/plugins/om/src/hooks.ts

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,8 @@ const RUN_POLL_MS = 3000;
6464
// ever shows what it happened to load with.
6565
const SNAPSHOT_POLL_MS = 30000;
6666

67-
const topologyKey = ['om', 'topology'] as const;
68-
const runsKey = ['om', 'runs'] as const;
67+
const TOPOLOGY_KEY = ['om', 'topology'] as const;
68+
const RUNS_KEY = ['om', 'runs'] as const;
6969

7070
/**
7171
* One request against pmm-managed.
@@ -118,6 +118,12 @@ export async function request<T>(path: string, init?: RequestInit): Promise<T> {
118118
/**
119119
* True while a run has not reached a terminal status.
120120
*
121+
* Covers both kinds. A collection pass and an inventory refresh are very different
122+
* operations, but they report the same `RunStatus` enum, so asking "is it still going"
123+
* is one question and this is the one place that answers it. `inventoryHooks.ts` used
124+
* to carry a byte-identical `isRefreshActive`, which meant two functions to keep in
125+
* step with one wire contract.
126+
*
121127
* Typed on the union rather than on `string`, because the compiler is the only thing
122128
* that would have caught this comparison going stale when the wire values changed.
123129
*/
@@ -126,15 +132,21 @@ export function isRunActive(status: OmTopologyRunStatus | undefined): boolean {
126132
}
127133

128134
/**
129-
* The whole estate from the latest terminal snapshot.
135+
* The whole topology document from the latest terminal snapshot.
136+
*
137+
* "Estate" is this package's word for the *inventory* side - what SEP's probe found by
138+
* running a payload on the hosts. This is the other thing: pmm-managed's own derivation
139+
* from PMM inventory and VictoriaMetrics, which touches no host and is rebuilt per
140+
* request. Calling both an estate is what makes the two hook files look interchangeable
141+
* when they are not.
130142
*
131143
* One request, one document: the API assembles the tree per run, so there is no
132144
* per-cluster fetch and nothing to paginate. Keeping the previous page in place stops
133145
* the table collapsing to a spinner while a refetch is in flight.
134146
*/
135147
export function useOmTopology() {
136148
return useQuery<OmTopologyResponse>({
137-
queryKey: topologyKey,
149+
queryKey: TOPOLOGY_KEY,
138150
placeholderData: keepPreviousData,
139151
queryFn: () => request<OmTopologyResponse>('/topology'),
140152
// pmm-managed rebuilds the document on its own timer, so a page left open goes
@@ -292,7 +304,7 @@ export function toEnvironmentSections(
292304
*/
293305
export function useOmTopologyRuns(limit: number = OM_TOPOLOGY_RUNS_LIMIT) {
294306
return useQuery<OmTopologyRun[]>({
295-
queryKey: [...runsKey, limit],
307+
queryKey: [...RUNS_KEY, limit],
296308
queryFn: async () => {
297309
const { runs } = await request<{ runs: OmTopologyRun[] }>(
298310
`/topology/runs?limit=${limit}`
@@ -320,19 +332,21 @@ export function useOmTopologyRuns(limit: number = OM_TOPOLOGY_RUNS_LIMIT) {
320332
export function useTriggerOmTopologyRun() {
321333
const queryClient = useQueryClient();
322334
return useMutation<OmTopologyRunAccepted, Error, void>({
335+
// No body. The method declares `body: "*"`, and grpc-gateway's generated decoder
336+
// treats io.EOF as an empty request rather than an error, so an empty
337+
// TriggerTopologyCollectionRequest is what binds either way.
323338
mutationFn: () =>
324339
request<OmTopologyRunAccepted>('/topology/runs:collect', {
325340
method: 'POST',
326-
body: '{}',
327341
}),
328342
// onSettled, not onSuccess. A 409 means pmm-managed's own timer holds the lock, so
329343
// a run is in flight and the document is about to change -- the one case where a
330344
// refetch is most obviously wanted, and the one the success-only path skipped. The
331345
// request is cheap and the answer is idempotent, so refetching after a failure that
332346
// is not a conflict costs nothing either.
333347
onSettled: () => {
334-
queryClient.invalidateQueries({ queryKey: runsKey });
335-
queryClient.invalidateQueries({ queryKey: topologyKey });
348+
queryClient.invalidateQueries({ queryKey: RUNS_KEY });
349+
queryClient.invalidateQueries({ queryKey: TOPOLOGY_KEY });
336350
},
337351
});
338352
}
@@ -341,7 +355,7 @@ export function useTriggerOmTopologyRun() {
341355
export function useInvalidateOmTopologySnapshot() {
342356
const queryClient = useQueryClient();
343357
return () => {
344-
queryClient.invalidateQueries({ queryKey: topologyKey });
345-
queryClient.invalidateQueries({ queryKey: runsKey });
358+
queryClient.invalidateQueries({ queryKey: TOPOLOGY_KEY });
359+
queryClient.invalidateQueries({ queryKey: RUNS_KEY });
346360
};
347361
}

ui/packages/plugins/om/src/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,6 @@ export {
6969
useRefreshInventory,
7070
useForgetHost,
7171
useForgetService,
72-
isRefreshActive,
7372
} from './inventoryHooks';
7473
export type { OmHostFilters } from './inventoryHooks';
7574
export {

ui/packages/plugins/om/src/inventoryHooks.ts

Lines changed: 32 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -39,21 +39,20 @@ import {
3939
useQueryClient,
4040
} from '@tanstack/react-query';
4141
import { useEffect, useRef } from 'react';
42-
import { request } from './hooks';
42+
import { isRunActive, request } from './hooks';
4343
import type {
4444
OmInventoryHost,
4545
OmInventoryRun,
4646
OmInventoryRunAccepted,
4747
OmInventoryRunDetail,
4848
OmInventoryService,
4949
OmInventorySetting,
50-
OmTopologyRunStatus,
5150
} from './types';
5251

53-
const hostsKey = ['om', 'inventory', 'hosts'] as const;
54-
const servicesKey = ['om', 'inventory', 'services'] as const;
55-
const runsKey = ['om', 'inventory', 'runs'] as const;
56-
const configKey = ['om', 'inventory', 'config'] as const;
52+
const HOSTS_KEY = ['om', 'inventory', 'hosts'] as const;
53+
const SERVICES_KEY = ['om', 'inventory', 'services'] as const;
54+
const RUNS_KEY = ['om', 'inventory', 'runs'] as const;
55+
const CONFIG_KEY = ['om', 'inventory', 'config'] as const;
5756

5857
/** Poll cadence while a refresh is in flight (ms). */
5958
const REFRESH_POLL_MS = 3000;
@@ -113,7 +112,7 @@ function toQuery(filters: OmHostFilters): string {
113112
export function useOmInventoryHosts(filters: OmHostFilters = {}) {
114113
const query = toQuery(filters);
115114
return useQuery<OmInventoryHost[]>({
116-
queryKey: [...hostsKey, query],
115+
queryKey: [...HOSTS_KEY, query],
117116
placeholderData: keepPreviousData,
118117
queryFn: async () => {
119118
const { hosts } = await request<{ hosts: OmInventoryHost[] }>(
@@ -137,7 +136,7 @@ export function useOmInventoryHosts(filters: OmHostFilters = {}) {
137136
*/
138137
export function useOmInventoryServices() {
139138
return useQuery<OmInventoryService[]>({
140-
queryKey: servicesKey,
139+
queryKey: SERVICES_KEY,
141140
placeholderData: keepPreviousData,
142141
queryFn: async () => {
143142
const { services } = await request<{ services: OmInventoryService[] }>(
@@ -149,28 +148,16 @@ export function useOmInventoryServices() {
149148
});
150149
}
151150

152-
/**
153-
* True while a refresh has not reached a terminal status.
154-
*
155-
* Typed on the union rather than on `string`, because the compiler is the only thing
156-
* that would have caught this comparison going stale when the wire values changed.
157-
*/
158-
export function isRefreshActive(
159-
status: OmTopologyRunStatus | undefined
160-
): boolean {
161-
return status === 'RUN_STATUS_RUNNING';
162-
}
163-
164151
/**
165152
* Refresh history, newest first.
166153
*
167-
* Polls fast while the newest refresh is in flight and slowly otherwise, rather than
168-
* stopping: the schedule starts sweeps nobody here asked for, and a history that only
169-
* updated on reload could not show them.
154+
* Polls fast while any refresh is in flight and slowly otherwise, rather than stopping:
155+
* the schedule starts sweeps nobody here asked for, and a history that only updated on
156+
* reload could not show them.
170157
*/
171158
export function useOmInventoryRuns(limit = 25) {
172159
return useQuery<OmInventoryRun[]>({
173-
queryKey: [...runsKey, limit],
160+
queryKey: [...RUNS_KEY, limit],
174161
queryFn: async () => {
175162
const { runs } = await request<{ runs: OmInventoryRun[] }>(
176163
`/inventory/runs?limit=${limit}`
@@ -181,7 +168,7 @@ export function useOmInventoryRuns(limit = 25) {
181168
// overlap -- and a narrow one started later can finish first, which would leave
182169
// data[0] terminal while a broader run is still probing.
183170
refetchInterval: (query) =>
184-
(query.state.data ?? []).some((run) => isRefreshActive(run.status))
171+
(query.state.data ?? []).some((run) => isRunActive(run.status))
185172
? REFRESH_POLL_MS
186173
: ESTATE_POLL_MS,
187174
});
@@ -211,12 +198,12 @@ export function useInvalidateEstateOnRefreshEnd(
211198
const queryClient = useQueryClient();
212199
const wasActive = useRef(false);
213200

214-
const anyActive = (runs ?? []).some((run) => isRefreshActive(run.status));
201+
const anyActive = (runs ?? []).some((run) => isRunActive(run.status));
215202

216203
useEffect(() => {
217204
if (wasActive.current && !anyActive) {
218-
queryClient.invalidateQueries({ queryKey: hostsKey });
219-
queryClient.invalidateQueries({ queryKey: servicesKey });
205+
queryClient.invalidateQueries({ queryKey: HOSTS_KEY });
206+
queryClient.invalidateQueries({ queryKey: SERVICES_KEY });
220207
}
221208
wasActive.current = anyActive;
222209
}, [anyActive, queryClient]);
@@ -238,15 +225,20 @@ export function useInvalidateEstateOnRefreshEnd(
238225
export function useRefreshInventory() {
239226
const queryClient = useQueryClient();
240227
return useMutation<OmInventoryRunAccepted, Error, string[] | undefined>({
228+
// A body only when there is a scope to state. `body: "*"` on the method plus
229+
// grpc-gateway tolerating io.EOF means an unscoped sweep needs no `{}` to say
230+
// "everything"; the absent body already says it.
241231
mutationFn: (nodeIds) =>
242232
request<OmInventoryRunAccepted>('/inventory/runs:trigger', {
243233
method: 'POST',
244-
body: JSON.stringify(nodeIds?.length ? { node_ids: nodeIds } : {}),
234+
body: nodeIds?.length
235+
? JSON.stringify({ node_ids: nodeIds })
236+
: undefined,
245237
}),
246238
// onSettled rather than onSuccess: a 409 means a sweep is in flight and the estate
247239
// is about to change, which is exactly when a refetch is most wanted.
248240
onSettled: () => {
249-
queryClient.invalidateQueries({ queryKey: runsKey });
241+
queryClient.invalidateQueries({ queryKey: RUNS_KEY });
250242
},
251243
});
252244
}
@@ -267,8 +259,8 @@ export function useForgetHost() {
267259
method: 'DELETE',
268260
}),
269261
onSuccess: () => {
270-
queryClient.invalidateQueries({ queryKey: hostsKey });
271-
queryClient.invalidateQueries({ queryKey: servicesKey });
262+
queryClient.invalidateQueries({ queryKey: HOSTS_KEY });
263+
queryClient.invalidateQueries({ queryKey: SERVICES_KEY });
272264
},
273265
});
274266
}
@@ -282,8 +274,8 @@ export function useForgetService() {
282274
method: 'DELETE',
283275
}),
284276
onSuccess: () => {
285-
queryClient.invalidateQueries({ queryKey: hostsKey });
286-
queryClient.invalidateQueries({ queryKey: servicesKey });
277+
queryClient.invalidateQueries({ queryKey: HOSTS_KEY });
278+
queryClient.invalidateQueries({ queryKey: SERVICES_KEY });
287279
},
288280
});
289281
}
@@ -298,13 +290,13 @@ export function useForgetService() {
298290
*/
299291
export function useOmInventoryRun(runId: string | undefined) {
300292
return useQuery<OmInventoryRunDetail>({
301-
queryKey: [...runsKey, 'detail', runId],
293+
queryKey: [...RUNS_KEY, 'detail', runId],
302294
enabled: Boolean(runId),
303295
queryFn: () => request<OmInventoryRunDetail>(`/inventory/runs/${runId}`),
304296
// A run still going gains entities as its dispatches land, so the open panel
305297
// follows it; a finished one never changes again.
306298
refetchInterval: (query) =>
307-
isRefreshActive(query.state.data?.run.status) ? REFRESH_POLL_MS : false,
299+
isRunActive(query.state.data?.run.status) ? REFRESH_POLL_MS : false,
308300
});
309301
}
310302

@@ -317,7 +309,7 @@ export function useOmInventoryRun(runId: string | undefined) {
317309
*/
318310
export function useOmInventoryConfig() {
319311
return useQuery<OmInventorySetting[]>({
320-
queryKey: configKey,
312+
queryKey: CONFIG_KEY,
321313
queryFn: async () => {
322314
const { settings } = await request<{ settings: OmInventorySetting[] }>(
323315
'/inventory/config'
@@ -358,10 +350,10 @@ export function useUpdateOmInventoryConfig() {
358350
body: JSON.stringify(values),
359351
}),
360352
onSettled: () => {
361-
queryClient.invalidateQueries({ queryKey: configKey });
353+
queryClient.invalidateQueries({ queryKey: CONFIG_KEY });
362354
// The schedule drives when the next refresh runs, so the history's idea of
363355
// "due" changes with it.
364-
queryClient.invalidateQueries({ queryKey: runsKey });
356+
queryClient.invalidateQueries({ queryKey: RUNS_KEY });
365357
},
366358
});
367359
}
@@ -375,7 +367,7 @@ export function useResetOmInventoryConfig() {
375367
method: 'DELETE',
376368
}),
377369
onSettled: () => {
378-
queryClient.invalidateQueries({ queryKey: configKey });
370+
queryClient.invalidateQueries({ queryKey: CONFIG_KEY });
379371
},
380372
});
381373
}

0 commit comments

Comments
 (0)