Skip to content

Commit e9083d7

Browse files
committed
refactor: Redesign manual OTA update flow and form UX
- Move base image collections query ownership into SoftwareUpdateTab - Remove base image collection query loading from Device page,removing a spinner from the device details page and improving perceived performance - Add File/Collection mode toggle for manual OTA, defaulting to File - Refactor manual OTA forms to use shared row layout and aligned submit actions - Simplify search refetch payload construction using conditional filter spreading - Cancel pending debounced refetch calls on unmount to avoid stale network requests - Fix audit vulnerabilities in dependencies Used command: ``` npm audit fix ``` Signed-off-by: Omar <omar.brbutovic@secomind.com>
1 parent 9d78a52 commit e9083d7

7 files changed

Lines changed: 309 additions & 279 deletions

File tree

frontend/package-lock.json

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

frontend/src/components/DeviceTabs/SoftwareUpdateTab.tsx

Lines changed: 109 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,14 @@
1818
* SPDX-License-Identifier: Apache-2.0
1919
*/
2020

21-
import React, { useEffect, useState, useRef, Suspense } from "react";
21+
import React, {
22+
useCallback,
23+
useEffect,
24+
useRef,
25+
useState,
26+
Suspense,
27+
} from "react";
28+
import { ToggleButton, ToggleButtonGroup } from "react-bootstrap";
2229
import type { Subscription } from "relay-runtime";
2330
import { FormattedMessage, useIntl } from "react-intl";
2431
import {
@@ -28,22 +35,23 @@ import {
2835
useRelayEnvironment,
2936
usePaginationFragment,
3037
usePreloadedQuery,
31-
PreloadedQuery,
38+
useQueryLoader,
3239
} from "react-relay/hooks";
33-
import { Form, Stack } from "react-bootstrap";
40+
import type { PreloadedQuery } from "react-relay/hooks";
3441

35-
import type { Device_getBaseImageCollections_Query } from "@/api/__generated__/Device_getBaseImageCollections_Query.graphql";
36-
import { SoftwareUpdateTab_createManualOtaOperation_Mutation } from "@/api/__generated__/SoftwareUpdateTab_createManualOtaOperation_Mutation.graphql";
42+
import type { SoftwareUpdateTab_createManualOtaOperation_Mutation } from "@/api/__generated__/SoftwareUpdateTab_createManualOtaOperation_Mutation.graphql";
43+
import type { SoftwareUpdateTab_getBaseImageCollections_Query } from "@/api/__generated__/SoftwareUpdateTab_getBaseImageCollections_Query.graphql";
3744
import type { SoftwareUpdateTab_PaginationQuery } from "@/api/__generated__/SoftwareUpdateTab_PaginationQuery.graphql";
3845
import type { SoftwareUpdateTab_otaOperations$key } from "@/api/__generated__/SoftwareUpdateTab_otaOperations.graphql";
3946

4047
import Alert from "@/components/Alert";
4148
import OperationTable from "@/components/OperationTable";
4249
import Spinner from "@/components/Spinner";
50+
import Stack from "@/components/Stack";
4351
import { Tab } from "@/components/Tabs";
52+
import { RECORDS_TO_LOAD_FIRST } from "@/constants";
4453
import ManualOtaFromCollectionForm from "@/forms/ManualOtaFromCollectionForm";
4554
import ManualOtaFromFileForm from "@/forms/ManualOtaFromFileForm";
46-
import { GET_BASE_IMAGE_COLL_QUERY } from "@/pages/Device";
4755

4856
const DEVICE_OTA_OPERATIONS_FRAGMENT = graphql`
4957
fragment SoftwareUpdateTab_otaOperations on Device
@@ -96,37 +104,60 @@ const DEVICE_CREATE_MANUAL_OTA_OPERATION_MUTATION = graphql`
96104
}
97105
`;
98106

99-
export type GetBaseImageCollsQueryType = PreloadedQuery<
100-
Device_getBaseImageCollections_Query,
101-
Record<string, unknown>
102-
>;
107+
const GET_BASE_IMAGE_COLL_QUERY = graphql`
108+
query SoftwareUpdateTab_getBaseImageCollections_Query(
109+
$first: Int
110+
$after: String
111+
$filterBaseImageCollections: BaseImageCollectionFilterInput = {}
112+
) {
113+
...ManualOtaFromCollectionForm_baseImageCollections_Fragment
114+
@arguments(filter: $filterBaseImageCollections)
115+
}
116+
`;
103117

104118
type OtaOperationInput = {
105119
imageFile?: File;
106120
imageUrl?: string;
107121
};
108122

123+
type ManualOtaFromCollectionFormWrapperProps = {
124+
baseImageCollsQueryRef: PreloadedQuery<SoftwareUpdateTab_getBaseImageCollections_Query>;
125+
isCreatingOtaOperation: boolean;
126+
launchManualOTAUpdate: (input: OtaOperationInput) => void;
127+
};
128+
129+
const ManualOtaFromCollectionFormWrapper = ({
130+
baseImageCollsQueryRef,
131+
isCreatingOtaOperation,
132+
launchManualOTAUpdate,
133+
}: ManualOtaFromCollectionFormWrapperProps) => {
134+
const baseImageCollections = usePreloadedQuery(
135+
GET_BASE_IMAGE_COLL_QUERY,
136+
baseImageCollsQueryRef,
137+
);
138+
139+
return (
140+
<ManualOtaFromCollectionForm
141+
baseImageCollectionsData={baseImageCollections}
142+
isLoading={isCreatingOtaOperation}
143+
onManualOTAImageSubmit={launchManualOTAUpdate}
144+
/>
145+
);
146+
};
147+
109148
type DeviceSoftwareUpdateTabProps = {
110149
deviceRef: SoftwareUpdateTab_otaOperations$key;
111-
getBaseImageCollsQuery: GetBaseImageCollsQueryType;
112150
};
113151

114152
const DeviceSoftwareUpdateTab = ({
115153
deviceRef,
116-
getBaseImageCollsQuery,
117154
}: DeviceSoftwareUpdateTabProps) => {
118155
const [isRefreshing, setIsRefreshing] = useState(false);
119156
const [errorFeedback, setErrorFeedback] = useState<React.ReactNode>(null);
120157
const intl = useIntl();
121158
const relayEnvironment = useRelayEnvironment();
122159

123-
const [updateMode, setUpdateMode] = useState<"collection" | "file">(
124-
"collection",
125-
);
126-
const modeOnChange =
127-
(mode: "collection" | "file") =>
128-
(_event: React.ChangeEvent<HTMLInputElement>) =>
129-
setUpdateMode(mode);
160+
const [updateMode, setUpdateMode] = useState<"file" | "collection">("file");
130161

131162
const { data } = usePaginationFragment<
132163
SoftwareUpdateTab_PaginationQuery,
@@ -208,11 +239,22 @@ const DeviceSoftwareUpdateTab = ({
208239
deviceId,
209240
]);
210241

211-
const baseImageCollections = usePreloadedQuery(
212-
GET_BASE_IMAGE_COLL_QUERY,
213-
getBaseImageCollsQuery,
242+
const [getBaseImageCollsQuery, getBaseImageColls] =
243+
useQueryLoader<SoftwareUpdateTab_getBaseImageCollections_Query>(
244+
GET_BASE_IMAGE_COLL_QUERY,
245+
);
246+
247+
const fetchBaseImageColls = useCallback(
248+
() =>
249+
getBaseImageColls(
250+
{ first: RECORDS_TO_LOAD_FIRST },
251+
{ fetchPolicy: "store-and-network" },
252+
),
253+
[getBaseImageColls],
214254
);
215255

256+
useEffect(fetchBaseImageColls, [fetchBaseImageColls]);
257+
216258
if (!data.capabilities.includes("SOFTWARE_UPDATES")) {
217259
return null;
218260
}
@@ -288,37 +330,56 @@ const DeviceSoftwareUpdateTab = ({
288330
{errorFeedback}
289331
</Alert>
290332
<Suspense fallback={<Spinner />}>
291-
<Stack direction="vertical" className="mt-3">
292-
<Form.Group key="updateMode">
293-
<Form.Check
294-
name="updateMode"
295-
inline
333+
<Stack direction="vertical" gap={3} className="mt-3">
334+
<div>
335+
<ToggleButtonGroup
296336
type="radio"
297-
label="Collection"
298-
id="Collection"
299-
onChange={modeOnChange("collection")}
300-
checked={updateMode === "collection"}
301-
/>
302-
<Form.Check
303337
name="updateMode"
304-
inline
305-
type="radio"
306-
label="File"
307-
id="File"
308-
onChange={modeOnChange("file")}
309-
checked={updateMode === "file"}
310-
/>
311-
</Form.Group>
338+
value={updateMode}
339+
onChange={setUpdateMode}
340+
size="sm"
341+
>
342+
<ToggleButton
343+
id="mode-file"
344+
value="file"
345+
variant={
346+
updateMode === "file" ? "primary" : "outline-secondary"
347+
}
348+
className="fw-medium px-3"
349+
>
350+
<FormattedMessage
351+
id="components.DeviceTabs.SoftwareUpdateTab.modeFile"
352+
defaultMessage="File"
353+
/>
354+
</ToggleButton>
355+
356+
<ToggleButton
357+
id="mode-collection"
358+
value="collection"
359+
variant={
360+
updateMode === "collection"
361+
? "primary"
362+
: "outline-secondary"
363+
}
364+
className="fw-medium px-3"
365+
>
366+
<FormattedMessage
367+
id="components.DeviceTabs.SoftwareUpdateTab.modeCollection"
368+
defaultMessage="Collection"
369+
/>
370+
</ToggleButton>
371+
</ToggleButtonGroup>
372+
</div>
312373
{updateMode === "collection" ? (
313-
<ManualOtaFromCollectionForm
314-
baseImageCollectionsData={baseImageCollections}
315-
className="mt-3 w-75"
316-
isLoading={isCreatingOtaOperation}
317-
onManualOTAImageSubmit={launchManualOTAUpdate}
318-
/>
374+
getBaseImageCollsQuery && (
375+
<ManualOtaFromCollectionFormWrapper
376+
baseImageCollsQueryRef={getBaseImageCollsQuery}
377+
isCreatingOtaOperation={isCreatingOtaOperation}
378+
launchManualOTAUpdate={launchManualOTAUpdate}
379+
/>
380+
)
319381
) : (
320382
<ManualOtaFromFileForm
321-
className="mt-3 w-75"
322383
isLoading={isCreatingOtaOperation}
323384
onManualOTAImageSubmit={launchManualOTAUpdate}
324385
/>

frontend/src/forms/ManualFileDownloadRequestFromRepositoryForm.tsx

Lines changed: 13 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -143,26 +143,23 @@ const ManualFileDownloadRequestFromRepositoryForm = ({
143143
const debounceRepositoryRefetch = useMemo(
144144
() =>
145145
_.debounce((text: string) => {
146-
if (text === "") {
147-
refetchRepositories(
148-
{
149-
first: RECORDS_TO_LOAD_FIRST,
150-
},
151-
{ fetchPolicy: "network-only" },
152-
);
153-
} else {
154-
refetchRepositories(
155-
{
156-
first: RECORDS_TO_LOAD_FIRST,
157-
filter: { name: { ilike: `%${text}%` } },
158-
},
159-
{ fetchPolicy: "network-only" },
160-
);
161-
}
146+
refetchRepositories(
147+
{
148+
first: RECORDS_TO_LOAD_FIRST,
149+
...(text && { filter: { name: { ilike: `%${text}%` } } }),
150+
},
151+
{ fetchPolicy: "network-only" },
152+
);
162153
}, 500),
163154
[refetchRepositories],
164155
);
165156

157+
useEffect(() => {
158+
return () => {
159+
debounceRepositoryRefetch.cancel();
160+
};
161+
}, [debounceRepositoryRefetch]);
162+
166163
useEffect(() => {
167164
if (searchRepositoryText !== null) {
168165
debounceRepositoryRefetch(searchRepositoryText);

0 commit comments

Comments
 (0)