Skip to content

Commit 7e4de5e

Browse files
committed
feat: improve feedback during manual OTA operation
- Added a progress bar to the OTA operation table to provide better feedback on the operation's progress. - Updated the operation table to display the error code when an OTA operation fails, giving users more information about the failure. Signed-off-by: ArnelaL <arnela.lisic@secomind.com>
1 parent e9083d7 commit 7e4de5e

4 files changed

Lines changed: 158 additions & 219 deletions

File tree

frontend/src/components/DeviceTabs/SoftwareUpdateTab.tsx

Lines changed: 77 additions & 131 deletions
Original file line numberDiff line numberDiff line change
@@ -19,30 +19,27 @@
1919
*/
2020

2121
import React, {
22+
Suspense,
2223
useCallback,
2324
useEffect,
24-
useRef,
25+
useMemo,
2526
useState,
26-
Suspense,
2727
} from "react";
2828
import { ToggleButton, ToggleButtonGroup } from "react-bootstrap";
29-
import type { Subscription } from "relay-runtime";
3029
import { FormattedMessage, useIntl } from "react-intl";
30+
import type { PreloadedQuery } from "react-relay/hooks";
3131
import {
32+
ConnectionHandler,
3233
graphql,
3334
useMutation,
34-
fetchQuery,
35-
useRelayEnvironment,
3635
usePaginationFragment,
3736
usePreloadedQuery,
3837
useQueryLoader,
38+
useSubscription,
3939
} from "react-relay/hooks";
40-
import type { PreloadedQuery } from "react-relay/hooks";
4140

4241
import type { SoftwareUpdateTab_createManualOtaOperation_Mutation } from "@/api/__generated__/SoftwareUpdateTab_createManualOtaOperation_Mutation.graphql";
4342
import type { SoftwareUpdateTab_getBaseImageCollections_Query } from "@/api/__generated__/SoftwareUpdateTab_getBaseImageCollections_Query.graphql";
44-
import type { SoftwareUpdateTab_PaginationQuery } from "@/api/__generated__/SoftwareUpdateTab_PaginationQuery.graphql";
45-
import type { SoftwareUpdateTab_otaOperations$key } from "@/api/__generated__/SoftwareUpdateTab_otaOperations.graphql";
4643

4744
import Alert from "@/components/Alert";
4845
import OperationTable from "@/components/OperationTable";
@@ -52,37 +49,23 @@ import { Tab } from "@/components/Tabs";
5249
import { RECORDS_TO_LOAD_FIRST } from "@/constants";
5350
import ManualOtaFromCollectionForm from "@/forms/ManualOtaFromCollectionForm";
5451
import ManualOtaFromFileForm from "@/forms/ManualOtaFromFileForm";
52+
import { SoftwareUpdateTab_otaOperations$key } from "@/api/__generated__/SoftwareUpdateTab_otaOperations.graphql";
53+
import { OtaOperations_PaginationQuery } from "@/api/__generated__/OtaOperations_PaginationQuery.graphql";
5554

55+
/* eslint-disable relay/unused-fields */
5656
const DEVICE_OTA_OPERATIONS_FRAGMENT = graphql`
5757
fragment SoftwareUpdateTab_otaOperations on Device
58-
@refetchable(queryName: "SoftwareUpdateTab_PaginationQuery") {
58+
@refetchable(queryName: "OtaOperations_PaginationQuery") {
5959
id
6060
capabilities
6161
otaOperations(first: $first, after: $after)
6262
@connection(key: "SoftwareUpdateTab_otaOperations") {
6363
edges {
6464
node {
65-
id
66-
baseImageUrl
67-
status
68-
createdAt
65+
__typename
6966
}
7067
}
71-
}
72-
...OperationTable_otaOperations
73-
}
74-
`;
75-
76-
const GET_DEVICE_OTA_OPERATIONS_QUERY = graphql`
77-
query SoftwareUpdateTab_getDeviceOtaOperations_Query(
78-
$id: ID!
79-
$first: Int
80-
$after: String
81-
) {
82-
device(id: $id) {
83-
id
84-
...Device_connectionStatus
85-
...SoftwareUpdateTab_otaOperations
68+
...OperationTable_otaOperationEdgeFragment
8669
}
8770
}
8871
`;
@@ -97,6 +80,29 @@ const DEVICE_CREATE_MANUAL_OTA_OPERATION_MUTATION = graphql`
9780
baseImageUrl
9881
createdAt
9982
status
83+
statusProgress
84+
statusCode
85+
updatedAt
86+
campaignTarget {
87+
campaign {
88+
id
89+
name
90+
}
91+
}
92+
}
93+
}
94+
}
95+
`;
96+
97+
const OTA_OPERATION_UPDATED_SUBSCRIPTION = graphql`
98+
subscription SoftwareUpdateTab_otaOperation_updated_Subscription {
99+
otaOperation {
100+
updated {
101+
id
102+
baseImageUrl
103+
createdAt
104+
status
105+
statusProgress
100106
statusCode
101107
updatedAt
102108
}
@@ -152,15 +158,13 @@ type DeviceSoftwareUpdateTabProps = {
152158
const DeviceSoftwareUpdateTab = ({
153159
deviceRef,
154160
}: DeviceSoftwareUpdateTabProps) => {
155-
const [isRefreshing, setIsRefreshing] = useState(false);
156161
const [errorFeedback, setErrorFeedback] = useState<React.ReactNode>(null);
157162
const intl = useIntl();
158-
const relayEnvironment = useRelayEnvironment();
159163

160164
const [updateMode, setUpdateMode] = useState<"file" | "collection">("file");
161165

162166
const { data } = usePaginationFragment<
163-
SoftwareUpdateTab_PaginationQuery,
167+
OtaOperations_PaginationQuery,
164168
SoftwareUpdateTab_otaOperations$key
165169
>(DEVICE_OTA_OPERATIONS_FRAGMENT, deviceRef);
166170

@@ -171,74 +175,16 @@ const DeviceSoftwareUpdateTab = ({
171175
DEVICE_CREATE_MANUAL_OTA_OPERATION_MUTATION,
172176
);
173177

174-
const otaOperations = (
175-
data.otaOperations?.edges?.map(({ node }) => node) || []
176-
).sort((a, b) => {
177-
if (a.createdAt > b.createdAt) {
178-
return -1;
179-
}
180-
if (a.createdAt < b.createdAt) {
181-
return 1;
182-
}
183-
return 0;
184-
});
185-
186-
const lastFinishedOperationIndex = otaOperations.findIndex(
187-
({ status }) => status === "SUCCESS" || status === "FAILURE",
178+
useSubscription(
179+
useMemo(
180+
() => ({
181+
subscription: OTA_OPERATION_UPDATED_SUBSCRIPTION,
182+
variables: { deviceId },
183+
}),
184+
[deviceId],
185+
),
188186
);
189187

190-
const currentOperations =
191-
lastFinishedOperationIndex === -1
192-
? otaOperations
193-
: otaOperations.slice(0, lastFinishedOperationIndex);
194-
195-
// For now devices only support 1 update operation at a time
196-
const currentOperation = currentOperations[0] || null;
197-
198-
// TODO: use GraphQL subscription (when available) to get updates about OTA operation
199-
const subscriptionRef = useRef<Subscription | null>(null);
200-
useEffect(() => {
201-
return () => {
202-
if (subscriptionRef.current) {
203-
subscriptionRef.current.unsubscribe();
204-
}
205-
};
206-
}, []);
207-
208-
useEffect(() => {
209-
if (!currentOperation || isRefreshing) {
210-
return;
211-
}
212-
const refreshTimerId = setTimeout(() => {
213-
setIsRefreshing(true);
214-
subscriptionRef.current = fetchQuery(
215-
relayEnvironment,
216-
GET_DEVICE_OTA_OPERATIONS_QUERY,
217-
{
218-
id: deviceId,
219-
first: 10_000,
220-
},
221-
).subscribe({
222-
complete: () => {
223-
setIsRefreshing(false);
224-
},
225-
error: () => {
226-
setIsRefreshing(false);
227-
},
228-
});
229-
}, 10000);
230-
231-
return () => {
232-
clearTimeout(refreshTimerId);
233-
};
234-
}, [
235-
currentOperation,
236-
isRefreshing,
237-
setIsRefreshing,
238-
relayEnvironment,
239-
deviceId,
240-
]);
241-
242188
const [getBaseImageCollsQuery, getBaseImageColls] =
243189
useQueryLoader<SoftwareUpdateTab_getBaseImageCollections_Query>(
244190
GET_BASE_IMAGE_COLL_QUERY,
@@ -290,22 +236,43 @@ const DeviceSoftwareUpdateTab = ({
290236
);
291237
},
292238
updater(store, data) {
293-
const otaOperationId = data?.createManualOtaOperation?.result?.id;
294-
if (otaOperationId) {
295-
const otaOperation = store.get(otaOperationId);
296-
const storedDevice = store.get(deviceId);
297-
const otaOperations = storedDevice?.getLinkedRecords("otaOperations");
298-
if (storedDevice && otaOperation && otaOperations) {
299-
storedDevice.setLinkedRecords(
300-
[otaOperation, ...otaOperations],
301-
"otaOperations",
302-
);
303-
}
304-
}
239+
const newOperationId = data?.createManualOtaOperation?.result?.id;
240+
if (!newOperationId) return;
241+
242+
const newOperation = store.get(newOperationId);
243+
244+
const storedDevice = store.get(deviceId);
245+
if (!storedDevice || !newOperation) return;
246+
247+
const connection = ConnectionHandler.getConnection(
248+
storedDevice,
249+
"SoftwareUpdateTab_otaOperations",
250+
);
251+
if (!connection) return;
252+
253+
const edges = connection.getLinkedRecords("edges") ?? [];
254+
const alreadyPresent = edges.some(
255+
(edge) =>
256+
edge.getLinkedRecord("node")?.getDataID() === newOperationId,
257+
);
258+
if (alreadyPresent) return;
259+
const edge = ConnectionHandler.createEdge(
260+
store,
261+
connection,
262+
newOperation,
263+
"FileDownloadRequestEdge",
264+
);
265+
ConnectionHandler.insertEdgeBefore(connection, edge);
305266
},
306267
});
307268
};
308269

270+
const otaOperationsRef = data?.otaOperations;
271+
272+
if (!otaOperationsRef) {
273+
return null;
274+
}
275+
309276
return (
310277
<Tab
311278
eventKey="device-software-update-tab"
@@ -386,34 +353,13 @@ const DeviceSoftwareUpdateTab = ({
386353
)}
387354
</Stack>
388355
</Suspense>
389-
{currentOperation && (
390-
<div className="mt-3">
391-
<FormattedMessage
392-
id="components.DeviceTabs.SoftwareUpdateTab.updatingTo"
393-
defaultMessage="Updating to image <a>{baseImageName}</a>"
394-
values={{
395-
a: (chunks: React.ReactNode) => (
396-
<a
397-
target="_blank"
398-
rel="noreferrer"
399-
href={currentOperation.baseImageUrl}
400-
>
401-
{chunks}
402-
</a>
403-
),
404-
baseImageName: currentOperation.baseImageUrl.split("/").pop(),
405-
}}
406-
/>
407-
{isRefreshing && <Spinner size="sm" className="ms-2" />}
408-
</div>
409-
)}
410356
<h5 className="mt-4">
411357
<FormattedMessage
412358
id="components.DeviceTabs.SoftwareUpdateTab.updatesHistory"
413359
defaultMessage="History"
414360
/>
415361
</h5>
416-
<OperationTable deviceRef={data} />
362+
<OperationTable otaOperationsRef={otaOperationsRef} />
417363
</div>
418364
</Tab>
419365
);

0 commit comments

Comments
 (0)