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,18 +96,38 @@ 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 ) ;
102120 const intl = useIntl ( ) ;
103121 const relayEnvironment = useRelayEnvironment ( ) ;
104122
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 ) ;
130+
105131 const { data } = usePaginationFragment <
106132 SoftwareUpdateTab_PaginationQuery ,
107133 SoftwareUpdateTab_otaOperations$key
@@ -182,19 +208,28 @@ const DeviceSoftwareUpdateTab = ({
182208 deviceId ,
183209 ] ) ;
184210
211+ const baseImageCollections = usePreloadedQuery (
212+ GET_BASE_IMAGE_COLL_QUERY ,
213+ getBaseImageCollsQuery ,
214+ ) ;
215+
185216 if ( ! data . capabilities . includes ( "SOFTWARE_UPDATES" ) ) {
186217 return null ;
187218 }
188219
189- const launchManualOTAUpdate = ( file : File ) => {
220+ const launchManualOTAUpdate = ( {
221+ imageFile,
222+ imageUrl,
223+ } : OtaOperationInput ) => {
190224 createOtaOperation ( {
191225 variables : {
192226 input : {
193227 deviceId,
194- baseImageFile : file ,
228+ baseImageFile : imageFile ,
229+ baseImageUrl : imageUrl ,
195230 } ,
196231 } ,
197- onCompleted ( data , errors ) {
232+ onCompleted ( _data , errors ) {
198233 if ( errors ) {
199234 const errorFeedback = errors
200235 . map ( ( { fields, message } ) =>
@@ -252,11 +287,44 @@ const DeviceSoftwareUpdateTab = ({
252287 >
253288 { errorFeedback }
254289 </ Alert >
255- < BaseImageForm
256- className = "mt-3"
257- onSubmit = { launchManualOTAUpdate }
258- isLoading = { isCreatingOtaOperation }
259- />
290+ < Suspense fallback = { < Spinner /> } >
291+ < Stack direction = "vertical" className = "mt-3" >
292+ < Form . Group key = "updateMode" >
293+ < Form . Check
294+ name = "updateMode"
295+ inline
296+ type = "radio"
297+ label = "Collection"
298+ id = "Collection"
299+ onChange = { modeOnChange ( "collection" ) }
300+ checked = { updateMode === "collection" }
301+ />
302+ < Form . Check
303+ 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 >
312+ { updateMode === "collection" ? (
313+ < ManualOtaFromCollectionForm
314+ baseImageCollectionsData = { baseImageCollections }
315+ className = "mt-3 w-75"
316+ isLoading = { isCreatingOtaOperation }
317+ onManualOTAImageSubmit = { launchManualOTAUpdate }
318+ />
319+ ) : (
320+ < ManualOtaFromFileForm
321+ className = "mt-3 w-75"
322+ isLoading = { isCreatingOtaOperation }
323+ onManualOTAImageSubmit = { launchManualOTAUpdate }
324+ />
325+ ) }
326+ </ Stack >
327+ </ Suspense >
260328 { currentOperation && (
261329 < div className = "mt-3" >
262330 < FormattedMessage
0 commit comments