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.
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" ;
2222import type { Subscription } from "relay-runtime" ;
2323import { FormattedMessage , useIntl } from "react-intl" ;
2424import {
@@ -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" ;
3237import type { SoftwareUpdateTab_PaginationQuery } from "@/api/__generated__/SoftwareUpdateTab_PaginationQuery.graphql" ;
3338import type { SoftwareUpdateTab_otaOperations$key } from "@/api/__generated__/SoftwareUpdateTab_otaOperations.graphql" ;
34- import type { SoftwareUpdateTab_createManualOtaOperation_Mutation } from "@/api/__generated__/SoftwareUpdateTab_createManualOtaOperation_Mutation.graphql" ;
3539
3640import Alert from "@/components/Alert" ;
3741import OperationTable from "@/components/OperationTable" ;
3842import Spinner from "@/components/Spinner" ;
3943import { 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
4248const 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+
93109type DeviceSoftwareUpdateTabProps = {
94110 deviceRef : SoftwareUpdateTab_otaOperations$key ;
111+ getBaseImageCollsQuery : GetBaseImageCollsQueryType ;
95112} ;
96113
97114const 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
0 commit comments