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 , {
22+ useEffect ,
23+ useState ,
24+ useRef ,
25+ useCallback ,
26+ Suspense ,
27+ useMemo ,
28+ } from "react" ;
2229import type { Subscription } from "relay-runtime" ;
2330import { FormattedMessage , useIntl } from "react-intl" ;
2431import {
@@ -27,17 +34,29 @@ import {
2734 fetchQuery ,
2835 useRelayEnvironment ,
2936 usePaginationFragment ,
37+ usePreloadedQuery ,
38+ PreloadedQuery ,
39+ UseMutationConfig ,
3040} from "react-relay/hooks" ;
3141
42+ import type { Device_getBaseImageCollections_Query } from "@/api/__generated__/Device_getBaseImageCollections_Query.graphql" ;
43+ import type {
44+ SoftwareUpdateTab_createManualOtaOperationNoExistingBaseImage_Mutation ,
45+ SoftwareUpdateTab_createManualOtaOperationNoExistingBaseImage_Mutation$data ,
46+ } from "@/api/__generated__/SoftwareUpdateTab_createManualOtaOperationNoExistingBaseImage_Mutation.graphql" ;
47+ import type {
48+ SoftwareUpdateTab_createManualOtaOperationExistingBaseImage_Mutation ,
49+ SoftwareUpdateTab_createManualOtaOperationExistingBaseImage_Mutation$data ,
50+ } from "@/api/__generated__/SoftwareUpdateTab_createManualOtaOperationExistingBaseImage_Mutation.graphql" ;
3251import type { SoftwareUpdateTab_PaginationQuery } from "@/api/__generated__/SoftwareUpdateTab_PaginationQuery.graphql" ;
3352import type { SoftwareUpdateTab_otaOperations$key } from "@/api/__generated__/SoftwareUpdateTab_otaOperations.graphql" ;
34- import type { SoftwareUpdateTab_createManualOtaOperation_Mutation } from "@/api/__generated__/SoftwareUpdateTab_createManualOtaOperation_Mutation.graphql" ;
3553
3654import Alert from "@/components/Alert" ;
3755import OperationTable from "@/components/OperationTable" ;
3856import Spinner from "@/components/Spinner" ;
3957import { Tab } from "@/components/Tabs" ;
4058import BaseImageForm from "@/forms/BaseImageForm" ;
59+ import { GET_BASE_IMAGE_COLL_QUERY } from "@/pages/Device" ;
4160
4261const DEVICE_OTA_OPERATIONS_FRAGMENT = graphql `
4362 fragment SoftwareUpdateTab_otaOperations on Device
@@ -73,11 +92,11 @@ const GET_DEVICE_OTA_OPERATIONS_QUERY = graphql`
7392 }
7493` ;
7594
76- const DEVICE_CREATE_MANUAL_OTA_OPERATION_MUTATION = graphql `
77- mutation SoftwareUpdateTab_createManualOtaOperation_Mutation (
78- $input: CreateManualOtaOperationInput !
95+ const DEVICE_CREATE_MANUAL_OTA_OPERATION_NO_EXISTING_IMAGE_MUTATION = graphql `
96+ mutation SoftwareUpdateTab_createManualOtaOperationNoExistingBaseImage_Mutation (
97+ $input: CreateManualOtaOperationNoExistingBaseImageInput !
7998 ) {
80- createManualOtaOperation (input: $input) {
99+ createManualOtaOperationNoExistingBaseImage (input: $input) {
81100 result {
82101 id
83102 baseImageUrl
@@ -90,12 +109,48 @@ const DEVICE_CREATE_MANUAL_OTA_OPERATION_MUTATION = graphql`
90109 }
91110` ;
92111
112+ const DEVICE_CREATE_MANUAL_OTA_OPERATION_EXISTING_IMAGE_MUTATION = graphql `
113+ mutation SoftwareUpdateTab_createManualOtaOperationExistingBaseImage_Mutation(
114+ $input: CreateManualOtaOperationExistingBaseImageInput!
115+ ) {
116+ createManualOtaOperationExistingBaseImage(input: $input) {
117+ result {
118+ id
119+ baseImageUrl
120+ createdAt
121+ status
122+ statusCode
123+ updatedAt
124+ }
125+ }
126+ }
127+ ` ;
128+
129+ export type GetBaseImageCollsQueryType = PreloadedQuery <
130+ Device_getBaseImageCollections_Query ,
131+ Record < string , unknown >
132+ > ;
133+
134+ type OTAOperationFunctionIncompleteVariables = {
135+ input : {
136+ deviceId : string ;
137+ baseImageFile ?: File ;
138+ baseImageUrl ?: string ;
139+ } ;
140+ } ;
141+
142+ type OTAOperationFunctionVariables =
143+ UseMutationConfig < SoftwareUpdateTab_createManualOtaOperationNoExistingBaseImage_Mutation > [ "variables" ] &
144+ UseMutationConfig < SoftwareUpdateTab_createManualOtaOperationExistingBaseImage_Mutation > [ "variables" ] ;
145+
93146type DeviceSoftwareUpdateTabProps = {
94147 deviceRef : SoftwareUpdateTab_otaOperations$key ;
148+ getBaseImageCollsQuery : GetBaseImageCollsQueryType ;
95149} ;
96150
97151const DeviceSoftwareUpdateTab = ( {
98152 deviceRef,
153+ getBaseImageCollsQuery,
99154} : DeviceSoftwareUpdateTabProps ) => {
100155 const [ isRefreshing , setIsRefreshing ] = useState ( false ) ;
101156 const [ errorFeedback , setErrorFeedback ] = useState < React . ReactNode > ( null ) ;
@@ -109,10 +164,60 @@ const DeviceSoftwareUpdateTab = ({
109164
110165 const deviceId = data . id ;
111166
112- const [ createOtaOperation , isCreatingOtaOperation ] =
113- useMutation < SoftwareUpdateTab_createManualOtaOperation_Mutation > (
114- DEVICE_CREATE_MANUAL_OTA_OPERATION_MUTATION ,
167+ const [
168+ createOtaOperationNoExistingImage ,
169+ isCreatingOtaOperationNoExistingImage ,
170+ ] =
171+ useMutation < SoftwareUpdateTab_createManualOtaOperationNoExistingBaseImage_Mutation > (
172+ DEVICE_CREATE_MANUAL_OTA_OPERATION_NO_EXISTING_IMAGE_MUTATION ,
115173 ) ;
174+ const [ createOtaOperationExistingImage , isCreatingOtaOperationExistingImage ] =
175+ useMutation < SoftwareUpdateTab_createManualOtaOperationExistingBaseImage_Mutation > (
176+ DEVICE_CREATE_MANUAL_OTA_OPERATION_EXISTING_IMAGE_MUTATION ,
177+ ) ;
178+ const pickOTAOperationFunction = useCallback (
179+ ( ...input : Array < File | string > ) => {
180+ const variables : OTAOperationFunctionIncompleteVariables = {
181+ input : {
182+ deviceId,
183+ } ,
184+ } ;
185+ if ( input . length === 1 ) {
186+ const [ baseImage ] = input ;
187+ if ( baseImage instanceof File ) {
188+ variables . input . baseImageFile = baseImage ;
189+ return {
190+ func : createOtaOperationNoExistingImage ,
191+ vars : variables as UseMutationConfig < SoftwareUpdateTab_createManualOtaOperationNoExistingBaseImage_Mutation > [ "variables" ] ,
192+ } ;
193+ } else if ( typeof baseImage === "string" ) {
194+ variables . input . baseImageUrl = baseImage ;
195+ return {
196+ func : createOtaOperationExistingImage ,
197+ vars : variables as UseMutationConfig < SoftwareUpdateTab_createManualOtaOperationExistingBaseImage_Mutation > [ "variables" ] ,
198+ } ;
199+ }
200+ }
201+ throw new TypeError (
202+ "Only one Base Image can be submitted for an update." ,
203+ ) ;
204+ } ,
205+ [
206+ createOtaOperationNoExistingImage ,
207+ createOtaOperationExistingImage ,
208+ deviceId ,
209+ ] ,
210+ ) ;
211+
212+ const isCreatingOtaOperation = useMemo (
213+ ( ) =>
214+ isCreatingOtaOperationNoExistingImage ||
215+ isCreatingOtaOperationExistingImage ,
216+ [
217+ isCreatingOtaOperationNoExistingImage ,
218+ isCreatingOtaOperationExistingImage ,
219+ ] ,
220+ ) ;
116221
117222 const otaOperations = (
118223 data . otaOperations ?. edges ?. map ( ( { node } ) => node ) || [ ]
@@ -182,19 +287,21 @@ const DeviceSoftwareUpdateTab = ({
182287 deviceId ,
183288 ] ) ;
184289
290+ const baseImageCollections = usePreloadedQuery (
291+ GET_BASE_IMAGE_COLL_QUERY ,
292+ getBaseImageCollsQuery ,
293+ ) ;
294+
185295 if ( ! data . capabilities . includes ( "SOFTWARE_UPDATES" ) ) {
186296 return null ;
187297 }
188298
189- const launchManualOTAUpdate = ( file : File ) => {
299+ const launchManualOTAUpdate = ( ...input : Array < File | string > ) => {
300+ const { func : createOtaOperation , vars : variables } =
301+ pickOTAOperationFunction ( ...input ) ;
190302 createOtaOperation ( {
191- variables : {
192- input : {
193- deviceId,
194- baseImageFile : file ,
195- } ,
196- } ,
197- onCompleted ( data , errors ) {
303+ variables : variables as OTAOperationFunctionVariables ,
304+ onCompleted ( _data , errors ) {
198305 if ( errors ) {
199306 const errorFeedback = errors
200307 . map ( ( { fields, message } ) =>
@@ -213,16 +320,31 @@ const DeviceSoftwareUpdateTab = ({
213320 ) ;
214321 } ,
215322 updater ( store , data ) {
216- const otaOperationId = data ?. createManualOtaOperation ?. result ?. id ;
217- if ( otaOperationId ) {
218- const otaOperation = store . get ( otaOperationId ) ;
219- const storedDevice = store . get ( deviceId ) ;
220- const otaOperations = storedDevice ?. getLinkedRecords ( "otaOperations" ) ;
221- if ( storedDevice && otaOperation && otaOperations ) {
222- storedDevice . setLinkedRecords (
223- [ otaOperation , ...otaOperations ] ,
224- "otaOperations" ,
225- ) ;
323+ if ( data ) {
324+ let mutData :
325+ | SoftwareUpdateTab_createManualOtaOperationNoExistingBaseImage_Mutation$data [ "createManualOtaOperationNoExistingBaseImage" ]
326+ | SoftwareUpdateTab_createManualOtaOperationExistingBaseImage_Mutation$data [ "createManualOtaOperationExistingBaseImage" ]
327+ | undefined ;
328+ if ( "createManualOtaOperationNoExistingBaseImage" in data ) {
329+ mutData = data . createManualOtaOperationNoExistingBaseImage ;
330+ } else if (
331+ data &&
332+ "createManualOtaOperationExistingBaseImage" in data
333+ ) {
334+ mutData = data . createManualOtaOperationExistingBaseImage ;
335+ }
336+ const otaOperationId = mutData ?. result ?. id ;
337+ if ( otaOperationId ) {
338+ const otaOperation = store . get ( otaOperationId ) ;
339+ const storedDevice = store . get ( deviceId ) ;
340+ const otaOperations =
341+ storedDevice ?. getLinkedRecords ( "otaOperations" ) ;
342+ if ( storedDevice && otaOperation && otaOperations ) {
343+ storedDevice . setLinkedRecords (
344+ [ otaOperation , ...otaOperations ] ,
345+ "otaOperations" ,
346+ ) ;
347+ }
226348 }
227349 }
228350 } ,
@@ -252,11 +374,14 @@ const DeviceSoftwareUpdateTab = ({
252374 >
253375 { errorFeedback }
254376 </ Alert >
255- < BaseImageForm
256- className = "mt-3"
257- onSubmit = { launchManualOTAUpdate }
258- isLoading = { isCreatingOtaOperation }
259- />
377+ < Suspense fallback = { < Spinner /> } >
378+ < BaseImageForm
379+ className = "mt-3"
380+ onManualOTAImageSubmit = { launchManualOTAUpdate }
381+ isLoading = { isCreatingOtaOperation }
382+ baseImageCollectionsData = { baseImageCollections }
383+ />
384+ </ Suspense >
260385 { currentOperation && (
261386 < div className = "mt-3" >
262387 < FormattedMessage
@@ -290,4 +415,18 @@ const DeviceSoftwareUpdateTab = ({
290415 ) ;
291416} ;
292417
418+ export const DisabledDeviceSoftwareTab = ( ) => {
419+ const intl = useIntl ( ) ;
420+ return (
421+ < Tab
422+ className = "disabled"
423+ eventKey = "device-software-update-tab"
424+ title = { intl . formatMessage ( {
425+ id : "components.DeviceTabs.SoftwareUpdateTab" ,
426+ defaultMessage : "Software Updates" ,
427+ } ) }
428+ />
429+ ) ;
430+ } ;
431+
293432export default DeviceSoftwareUpdateTab ;
0 commit comments