Skip to content

Commit 09fde75

Browse files
committed
feat(frontend): manual OTA with base image from collection
Introduce UI in a Device page's Software Updates tab that allows to start a manual OTA update by selecting a base image already uploaded to a base image collection. Signed-off-by: Damiano Mason <damiano.mason@secomind.com>
1 parent c4bb427 commit 09fde75

8 files changed

Lines changed: 573 additions & 108 deletions

File tree

frontend/src/components/BaseImageSelect.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/*
22
* This file is part of Edgehog.
33
*
4-
* Copyright 2023-2025 SECO Mind Srl
4+
* Copyright 2023-2026 SECO Mind Srl
55
*
66
* Licensed under the Apache License, Version 2.0 (the "License");
77
* you may not use this file except in compliance with the License.
@@ -74,15 +74,18 @@ const BASE_IMAGE_SELECT_OPTIONS_FRAGMENT = graphql`
7474
id
7575
name
7676
version
77+
url
7778
}
7879
}
7980
}
8081
}
8182
`;
8283

83-
export type BaseImageRecord = NonNullable<
84+
type BaseImageNode = NonNullable<
8485
NonNullable<BaseImageSelect_BaseImagesFragment$data["baseImages"]>["edges"]
8586
>[number]["node"];
87+
type OmitUrl = Omit<BaseImageNode, "url">;
88+
export type BaseImageRecord = OmitUrl & { readonly url?: string };
8689

8790
type BaseImageSelectProps = {
8891
updateCampaignBaseImageOptionsRef: BaseImageSelect_BaseImagesFragment$key | null;
@@ -147,7 +150,7 @@ const BaseImageSelect = ({
147150
return (
148151
paginationData?.baseImages?.edges
149152
?.map((edge) => edge?.node)
150-
.filter((node): node is BaseImageRecord => node != null) ?? []
153+
.filter((node): node is BaseImageNode => node != null) ?? []
151154
);
152155
}, [paginationData]);
153156

frontend/src/components/DeviceTabs/SoftwareUpdateTab.tsx

Lines changed: 81 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/*
22
* This file is part of Edgehog.
33
*
4-
* Copyright 2025 SECO Mind Srl
4+
* Copyright 2025-2026 SECO Mind Srl
55
*
66
* Licensed under the Apache License, Version 2.0 (the "License");
77
* you may not use this file except in compliance with the License.
@@ -18,7 +18,7 @@
1818
* SPDX-License-Identifier: Apache-2.0
1919
*/
2020

21-
import React, { useEffect, useState, useRef } from "react";
21+
import React, { useEffect, useState, useRef, Suspense } from "react";
2222
import type { Subscription } from "relay-runtime";
2323
import { FormattedMessage, useIntl } from "react-intl";
2424
import {
@@ -27,17 +27,23 @@ import {
2727
fetchQuery,
2828
useRelayEnvironment,
2929
usePaginationFragment,
30+
usePreloadedQuery,
31+
PreloadedQuery,
3032
} from "react-relay/hooks";
33+
import { Form, Stack } from "react-bootstrap";
3134

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";
3237
import type { SoftwareUpdateTab_PaginationQuery } from "@/api/__generated__/SoftwareUpdateTab_PaginationQuery.graphql";
3338
import type { SoftwareUpdateTab_otaOperations$key } from "@/api/__generated__/SoftwareUpdateTab_otaOperations.graphql";
34-
import type { SoftwareUpdateTab_createManualOtaOperation_Mutation } from "@/api/__generated__/SoftwareUpdateTab_createManualOtaOperation_Mutation.graphql";
3539

3640
import Alert from "@/components/Alert";
3741
import OperationTable from "@/components/OperationTable";
3842
import Spinner from "@/components/Spinner";
3943
import { Tab } from "@/components/Tabs";
40-
import BaseImageForm from "@/forms/BaseImageForm";
44+
import ManualOtaFromCollectionForm from "@/forms/ManualOtaFromCollectionForm";
45+
import ManualOtaFromFileForm from "@/forms/ManualOtaFromFileForm";
46+
import { GET_BASE_IMAGE_COLL_QUERY } from "@/pages/Device";
4147

4248
const DEVICE_OTA_OPERATIONS_FRAGMENT = graphql`
4349
fragment SoftwareUpdateTab_otaOperations on Device
@@ -90,12 +96,24 @@ const DEVICE_CREATE_MANUAL_OTA_OPERATION_MUTATION = graphql`
9096
}
9197
`;
9298

99+
export type GetBaseImageCollsQueryType = PreloadedQuery<
100+
Device_getBaseImageCollections_Query,
101+
Record<string, unknown>
102+
>;
103+
104+
type OtaOperationInput = {
105+
imageFile?: File;
106+
imageUrl?: string;
107+
};
108+
93109
type DeviceSoftwareUpdateTabProps = {
94110
deviceRef: SoftwareUpdateTab_otaOperations$key;
111+
getBaseImageCollsQuery: GetBaseImageCollsQueryType;
95112
};
96113

97114
const DeviceSoftwareUpdateTab = ({
98115
deviceRef,
116+
getBaseImageCollsQuery,
99117
}: DeviceSoftwareUpdateTabProps) => {
100118
const [isRefreshing, setIsRefreshing] = useState(false);
101119
const [errorFeedback, setErrorFeedback] = useState<React.ReactNode>(null);
@@ -182,19 +200,28 @@ const DeviceSoftwareUpdateTab = ({
182200
deviceId,
183201
]);
184202

203+
const baseImageCollections = usePreloadedQuery(
204+
GET_BASE_IMAGE_COLL_QUERY,
205+
getBaseImageCollsQuery,
206+
);
207+
185208
if (!data.capabilities.includes("SOFTWARE_UPDATES")) {
186209
return null;
187210
}
188211

189-
const launchManualOTAUpdate = (file: File) => {
212+
const launchManualOTAUpdate = ({
213+
imageFile,
214+
imageUrl,
215+
}: OtaOperationInput) => {
190216
createOtaOperation({
191217
variables: {
192218
input: {
193219
deviceId,
194-
baseImageFile: file,
220+
baseImageFile: imageFile,
221+
baseImageUrl: imageUrl,
195222
},
196223
},
197-
onCompleted(data, errors) {
224+
onCompleted(_data, errors) {
198225
if (errors) {
199226
const errorFeedback = errors
200227
.map(({ fields, message }) =>
@@ -229,6 +256,15 @@ const DeviceSoftwareUpdateTab = ({
229256
});
230257
};
231258

259+
const [updateMode, setUpdateMode] = useState<"collection" | "file">(
260+
"collection",
261+
);
262+
263+
const modeOnChange =
264+
(mode: "collection" | "file") =>
265+
(_event: React.ChangeEvent<HTMLInputElement>) =>
266+
setUpdateMode(mode);
267+
232268
return (
233269
<Tab
234270
eventKey="device-software-update-tab"
@@ -252,11 +288,44 @@ const DeviceSoftwareUpdateTab = ({
252288
>
253289
{errorFeedback}
254290
</Alert>
255-
<BaseImageForm
256-
className="mt-3"
257-
onSubmit={launchManualOTAUpdate}
258-
isLoading={isCreatingOtaOperation}
259-
/>
291+
<Suspense fallback={<Spinner />}>
292+
<Stack direction="vertical" className="mt-3">
293+
<Form.Group key="updateMode">
294+
<Form.Check
295+
name="updateMode"
296+
inline
297+
type="radio"
298+
label="Collection"
299+
id="Collection"
300+
onChange={modeOnChange("collection")}
301+
checked={updateMode === "collection"}
302+
/>
303+
<Form.Check
304+
name="updateMode"
305+
inline
306+
type="radio"
307+
label="File"
308+
id="File"
309+
onChange={modeOnChange("file")}
310+
checked={updateMode === "file"}
311+
/>
312+
</Form.Group>
313+
{updateMode === "collection" ? (
314+
<ManualOtaFromCollectionForm
315+
baseImageCollectionsData={baseImageCollections}
316+
className="mt-3 w-75"
317+
isLoading={isCreatingOtaOperation}
318+
onManualOTAImageSubmit={launchManualOTAUpdate}
319+
/>
320+
) : (
321+
<ManualOtaFromFileForm
322+
className="mt-3 w-75"
323+
isLoading={isCreatingOtaOperation}
324+
onManualOTAImageSubmit={launchManualOTAUpdate}
325+
/>
326+
)}
327+
</Stack>
328+
</Suspense>
260329
{currentOperation && (
261330
<div className="mt-3">
262331
<FormattedMessage

frontend/src/forms/BaseImageForm.tsx

Lines changed: 0 additions & 82 deletions
This file was deleted.

0 commit comments

Comments
 (0)