diff --git a/src/components/DownloadImageDialog/ApplicationInstructions.tsx b/src/components/DownloadImageDialog/ApplicationInstructions.tsx new file mode 100644 index 00000000..4cfcbbba --- /dev/null +++ b/src/components/DownloadImageDialog/ApplicationInstructions.tsx @@ -0,0 +1,220 @@ +import has from 'lodash/has'; +import { interpolateMustache } from './utils'; +import { + Box, + List, + ListItem, + ListItemIcon, + Tab, + Tabs, + Typography, + styled, +} from '@mui/material'; +import { memo, useEffect, useMemo, useState } from 'react'; +import { MUILinkWithTracking } from '../MUILinkWithTracking'; +import { + DeviceType, + KeysOfUnion, + OsSpecificContractInstructions, +} from './models'; +import { OrderedListItem } from '../OrderedListItem'; + +export type OsOptions = ReturnType; + +export const getUserOs = () => { + const platform = window.navigator.platform.toLowerCase(); + if (platform.includes('win')) { + return 'Windows'; + } + + if (platform.includes('mac')) { + return 'MacOS'; + } + + if (platform.includes('x11') || platform.includes('linux')) { + return 'Linux'; + } + + return 'Unknown'; +}; + +const dtJsonTocontractOsKeyMap = { + windows: 'Windows', + osx: 'MacOS', + linux: 'Linux', +} as const; + +export const ApplicationInstructions = memo( + ({ + deviceType, + templateData, + }: { + deviceType: DeviceType; + templateData: { dockerImage: string }; + }) => { + const [currentOs, setCurrentOs] = useState(getUserOs()); + + const instructions = useMemo(() => { + if ( + deviceType?.instructions == null || + Array.isArray(deviceType.instructions) || + typeof deviceType.instructions !== 'object' + ) { + return deviceType?.instructions; + } + + const instructionsByOs = deviceType.instructions; + + return Object.fromEntries( + ( + Object.entries(instructionsByOs) as Array< + [KeysOfUnion, string[]] + > + ).map(([key, value]) => { + const normalizedKey = + key in dtJsonTocontractOsKeyMap + ? dtJsonTocontractOsKeyMap[ + key as keyof typeof dtJsonTocontractOsKeyMap + ] + : (key as keyof OsSpecificContractInstructions); + return [normalizedKey, value]; + }), + ) as OsSpecificContractInstructions; + }, [deviceType?.instructions]); + const hasOsSpecificInstructions = !Array.isArray(instructions); + const normalizedOs = currentOs === 'Unknown' ? 'Linux' : currentOs; + + useEffect(() => { + if (hasOsSpecificInstructions && instructions) { + const oses = Object.keys(instructions) as unknown as OsOptions; + if (!oses.includes(currentOs) && oses.length > 0) { + setCurrentOs(oses[0] as OsOptions); + } + } + }, [currentOs, setCurrentOs, instructions, hasOsSpecificInstructions]); + + if (!deviceType || !instructions) { + return ( + + Instructions for this application are not currently available. Please + try again later. + + ); + } + + const interpolatedInstructions = ( + hasOsSpecificInstructions + ? (instructions as Exclude)[normalizedOs] + : instructions + )?.map((instruction: string) => + interpolateMustache( + templateData, + instruction.replace(/ + + Instructions + + + {hasOsSpecificInstructions && ( + + + key === currentOs, + )} + onChange={(_event, value) => setCurrentOs(value ?? 'Unknown')} + aria-label="os tabs" + > + {(Object.keys(instructions) as OsOptions[]).map((os) => { + return ; + })} + + + + )} + + + + + + For more details please refer to our{' '} + + Getting Started Guide + + . + + + + ); + }, +); + +interface InstructionsItemProps { + node: any; + index: number; +} + +interface InstructionsListProps { + instructions: any[]; +} + +const InstructionsItem = (props: InstructionsItemProps) => { + const { node, index } = props; + + const hasChildren = has(node, 'children'); + let text = null; + + if (typeof node === 'string') { + text = node; + } + + if (node?.text) { + text = node.text; + } + + return ( + + + + {hasChildren && ( + + {(node.children as any[]).map((item, i) => { + return ; + })} + + )} + + ); +}; + +const InstructionsList = (props: InstructionsListProps) => { + const { instructions } = props; + + return ( + // TODO: On 13px the line height is calculated as 19.5px, which breaks the alignment of the number inside the list item due to rounding. + // Remove custom font size once fixed in rendition https://github.com/balena-io-modules/rendition/issues/1025 + + {instructions.map((item, i) => { + return ; + })} + + ); +}; diff --git a/src/components/DownloadImageDialog/DownloadImageDialog.stories.tsx b/src/components/DownloadImageDialog/DownloadImageDialog.stories.tsx new file mode 100644 index 00000000..3e60b453 --- /dev/null +++ b/src/components/DownloadImageDialog/DownloadImageDialog.stories.tsx @@ -0,0 +1,845 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { DownloadImageDialog, DownloadImageDialogProps } from '.'; +import { useState } from 'react'; +import { Button } from '@mui/material'; +import { DeviceType } from './models'; + +const deviceTypes: any[] = [ + { + imageDownloadAlerts: [ + { + type: 'warning', + message: + 'The Raspberry Pi 3 is not capable of connecting to 5GHz WiFi networks unless you use an external WiFi adapter that supports it.', + }, + ], + instructions: [ + 'Insert the SD card to the host machine.', + 'Write the balenaOS file you downloaded to the SD card. We recommend using Etcher.', + 'Wait for writing of balenaOS to complete.', + 'Remove the SD card from the host machine.', + 'Insert the freshly flashed SD card into the Raspberry Pi 3 (using 64bit OS).', + 'Connect power to the Raspberry Pi 3 (using 64bit OS) to boot the device.', + ], + options: [ + { + isGroup: true, + name: 'network', + message: 'Network', + options: [ + { + message: 'Network Connection', + name: 'network', + type: 'list', + choices: ['ethernet', 'wifi'], + }, + { + message: 'Wifi SSID', + name: 'wifiSsid', + type: 'text', + when: { + network: 'wifi', + }, + }, + { + message: 'Wifi Passphrase', + name: 'wifiKey', + type: 'password', + when: { + network: 'wifi', + }, + }, + ], + }, + { + isGroup: true, + isCollapsible: true, + collapsed: true, + name: 'advanced', + message: 'Advanced', + options: [ + { + message: 'Check for updates every X minutes', + name: 'appUpdatePollInterval', + type: 'number', + min: 10, + default: 10, + }, + ], + }, + ], + yocto: { + machine: 'raspberrypi3-64', + image: 'balena-image', + fstype: 'balenaos-img', + version: 'yocto-honister', + deployArtifact: 'balena-image-raspberrypi3-64.balenaos-img', + compressed: true, + }, + is_default_for__application: [ + { + application_tag: [ + { + value: 'esr', + }, + ], + should_be_running__release: [ + { + release_tag: [ + { + tag_key: 'meta-balena-base', + value: 'v2.114.25', + }, + { + tag_key: 'version', + value: '2023.7.0', + }, + ], + }, + ], + is_archived: false, + }, + { + application_tag: [], + should_be_running__release: [ + { + release_tag: [ + { + tag_key: 'version', + value: '3.2.7+rev1', + }, + ], + }, + ], + is_archived: false, + }, + ], + is_of__cpu_architecture: [ + { + slug: 'aarch64', + }, + ], + slug: 'raspberrypi3-64', + name: 'Raspberry Pi 3 (using 64bit OS)', + contract: { + data: { + led: true, + arch: 'aarch64', + hdmi: true, + media: { + altBoot: ['usb_mass_storage', 'network'], + defaultBoot: 'sdcard', + }, + storage: { + internal: false, + }, + is_private: false, + connectivity: { + wifi: true, + bluetooth: true, + }, + }, + name: 'Raspberry Pi 3 (using 64bit OS)', + slug: 'raspberrypi3-64', + type: 'hw.device-type', + assets: { + logo: { + url: 'data:image/svg+xml;base64,PHN2ZyBpZD0iTGF5ZXJfMSIgZGF0YS1uYW1lPSJMYXllciAxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA0MCA0MCI+PGRlZnM+PHN0eWxlPi5jbHMtMXtmaWxsOiM3NWE5Mjg7fS5jbHMtMntmaWxsOiNiYzExNDI7fS5jbHMtM3tmaWxsOiNmZmY7fS5jbHMtNHtmaWxsOiMxYTFhMWE7fTwvc3R5bGU+PC9kZWZzPjxwYXRoIGQ9Ik0xMiwuNWExLjExLDEuMTEsMCwwLDAtLjY1LjI3QTEuNDgsMS40OCwwLDAsMCw5LjY3LjkzYy0uNzktLjExLTEsLjExLTEuMjQuMzUtLjE3LDAtMS4zLS4xOC0xLjgxLjU5LTEuMy0uMTUtMS43MS43Ny0xLjI0LDEuNjJhMS4xMSwxLjExLDAsMCwwLC4wOCwxLjU5Yy0uMjIuNDQtLjA5LjkxLjQzLDEuNDhBMS4yNCwxLjI0LDAsMCwwLDYuNSw3Ljk0Yy0uMDkuODQuNzcsMS4zMywxLDEuNS4xLjQ5LjMxLDEsMS4yOSwxLjIuMTYuNzMuNzUuODUsMS4zMiwxYTYuMTUsNi4xNSwwLDAsMC0zLjQ5LDYuMDZsLS4yOC41YTYsNiwwLDAsMC0xLjA2LDksMTYuMjksMTYuMjksMCwwLDAsLjgzLDIuNyw2LjU2LDYuNTYsMCwwLDAsNC4wOSw1LjI0LDE0LDE0LDAsMCwwLDMuOTIsMi4yM0E2LjU0LDYuNTQsMCwwLDAsMTksMzkuNUgxOWE2LjU0LDYuNTQsMCwwLDAsNC44Mi0yLjE2LDE0LDE0LDAsMCwwLDMuOTItMi4yMyw2LjU2LDYuNTYsMCwwLDAsNC4wOS01LjI0LDE2LjI5LDE2LjI5LDAsMCwwLC44My0yLjcsNiw2LDAsMCwwLTEuMDYtOWwtLjI4LS41YTYuMTUsNi4xNSwwLDAsMC0zLjQ5LTYuMDZjLjU3LS4xNiwxLjE2LS4yOCwxLjMyLTEsMS0uMjUsMS4xOS0uNzEsMS4yOS0xLjIuMjUtLjE3LDEuMTEtLjY2LDEtMS41YTEuMjQsMS4yNCwwLDAsMCwuNjEtMS4zOGMuNTItLjU3LjY1LTEsLjQzLTEuNDhhMS4xMSwxLjExLDAsMCwwLC4wOC0xLjU5Yy40Ny0uODUuMDYtMS43Ny0xLjI0LTEuNjItLjUxLS43Ny0xLjY0LS41OS0xLjgxLS41OS0uMi0uMjQtLjQ1LS40Ni0xLjI0LS4zNUExLjQ4LDEuNDgsMCwwLDAsMjYuNjUuNzdDMjYsLjIyLDI1LjQ5LjY2LDI1LC44M2MtLjg1LS4yOC0xLC4xLTEuNDYuMjUtLjkzLS4xOS0xLjIxLjIzLTEuNjUuNjhoLS41MkE1LjksNS45LDAsMCwwLDE5LDUuMTFhNiw2LDAsMCwwLTIuMzMtMy4zNmgtLjUyYy0uNDQtLjQ1LS43Mi0uODctMS42NS0uNjhDMTQuMDguOTMsMTMuODkuNTUsMTMsLjgzQTMuMzIsMy4zMiwwLDAsMCwxMiwuNVoiLz48cGF0aCBjbGFzcz0iY2xzLTEiIGQ9Ik05LjIyLDQuMTJjMy43LDEuOTEsNS44NSwzLjQ1LDcsNC43Ny0uNiwyLjQyLTMuNzUsMi41My00LjksMi40NmEuODguODgsMCwwLDAsLjUtLjQ0Yy0uMjktLjIxLTEuMzEsMC0yLS40M2EuNjYuNjYsMCwwLDAsLjUzLS4zMSw1LjkzLDUuOTMsMCwwLDEtMS44My0uNzYsMS4wNSwxLjA1LDAsMCwwLC43NS0uMTZBNy4yNCw3LjI0LDAsMCwxLDcuNTEsOC4xN2MuMzIsMCwuNjYsMCwuNzUtLjEyQTcuMDksNy4wOSwwLDAsMSw2Ljg1LDYuOTFhMS4wOCwxLjA4LDAsMCwwLC43My0uMDcsNS41Myw1LjUzLDAsMCwxLTEuMi0xLjMyLjkxLjkxLDAsMCwwLC44NCwwYy0uMTQtLjMyLS43NS0uNTEtMS4xLTEuMjYuMzQsMCwuNy4wNy43NywwYTMuNjEsMy42MSwwLDAsMC0uNy0xLjM5QTEyLjY2LDEyLjY2LDAsMCwwLDgsMi44bC0uNDYtLjQ2YTMuNTYsMy41NiwwLDAsMSwyLC4xOWMuMjQtLjE5LDAtLjQyLS4yOS0uNjdhNi42NCw2LjY0LDAsMCwxLDEuNjUuNDJjLjI3LS4yNC0uMTctLjQ4LS4zOC0uNzJhNC4yMSw0LjIxLDAsMCwxLDEuNzMuNjhjLjI5LS4yOCwwLS41MS0uMTctLjc1YTQuMTcsNC4xNywwLDAsMSwxLjQ1LjkzYy4xMy0uMTcuMzQtLjMuMDktLjcyYTMuNjgsMy42OCwwLDAsMSwxLjE3LDFjLjMxLS4yLjE4LS40Ny4xOC0uNzJBMTEsMTEsMCwwLDEsMTYuMiwzLjMxYy4wOS0uMDYuMTYtLjI2LjIyLS41OCwxLjI1LDEuMjEsMyw0LjI2LjQ1LDUuNDdBMjMuODgsMjMuODgsMCwwLDAsOS4yMiw0LjEyWiIvPjxwYXRoIGNsYXNzPSJjbHMtMSIgZD0iTTI4Ljg2LDQuMTJDMjUuMTYsNiwyMyw3LjU3LDIxLjgzLDguODljLjYsMi40MiwzLjc1LDIuNTMsNC45MSwyLjQ2YS44Ny44NywwLDAsMS0uNTEtLjQ0Yy4yOS0uMjEsMS4zMiwwLDItLjQzYS42Ni42NiwwLDAsMS0uNTMtLjMxLDUuNzMsNS43MywwLDAsMCwxLjgzLS43NiwxLDEsMCwwLDEtLjc0LS4xNiw3LjE5LDcuMTksMCwwLDAsMS43NS0xLjA4Yy0uMzEsMC0uNjUsMC0uNzUtLjEyYTcuMDksNy4wOSwwLDAsMCwxLjQxLTEuMTQsMS4xLDEuMSwwLDAsMS0uNzMtLjA3LDUuNTMsNS41MywwLDAsMCwxLjItMS4zMi45MS45MSwwLDAsMS0uODQsMEMzMSw1LjE5LDMxLjYyLDUsMzIsNC4yNWExLjkxLDEuOTEsMCwwLDEtLjc4LDAsMy42MSwzLjYxLDAsMCwxLC43LTEuMzlBMTIuNTgsMTIuNTgsMCwwLDEsMzAuMSwyLjhsLjQ1LS40NmEzLjU2LDMuNTYsMCwwLDAtMiwuMTljLS4yNC0uMTksMC0uNDIuMjktLjY3YTYuODgsNi44OCwwLDAsMC0xLjY1LjQyYy0uMjctLjI0LjE3LS40OC4zOC0uNzJhNC4yOCw0LjI4LDAsMCwwLTEuNzMuNjhjLS4yOS0uMjgsMC0uNTEuMTgtLjc1YTQuMjMsNC4yMywwLDAsMC0xLjQ2LjkzYy0uMTMtLjE3LS4zMy0uMy0uMDktLjcyYTMuNzQsMy43NCwwLDAsMC0xLjE2LDFjLS4zMS0uMi0uMTktLjQ3LS4xOS0uNzJhMTIuODQsMTIuODQsMCwwLDAtMS4yNiwxLjMyLDEuMTgsMS4xOCwwLDAsMS0uMjItLjU4Yy0xLjI0LDEuMjEtMyw0LjI2LS40NSw1LjQ3YTI0LDI0LDAsMCwxLDcuNjUtNC4wOFoiLz48cGF0aCBjbGFzcz0iY2xzLTIiIGQ9Ik0yMy41MywyOC43NmE0LjI4LDQuMjgsMCwwLDEtNC40NCw0LjA5LDQuMjcsNC4yNywwLDAsMS00LjQzLTQuMDksNC4yNyw0LjI3LDAsMCwxLDQuNDMtNC4wOEE0LjI3LDQuMjcsMCwwLDEsMjMuNTMsMjguNzZaIi8+PHBhdGggY2xhc3M9ImNscy0yIiBkPSJNMTYuNTIsMTcuMDhjMS44NCwxLjIxLDIuMTcsMy45My43NCw2LjFzLTQuMDcsMi45NC01LjkxLDEuNzNoMGMtMS44NC0xLjItMi4xNy0zLjkzLS43NC02LjA5czQuMDgtMi45NCw1LjkxLTEuNzRaIi8+PHBhdGggY2xhc3M9ImNscy0yIiBkPSJNMjEuNDgsMTYuODZjLTEuODMsMS4yMS0yLjE2LDMuOTQtLjc0LDYuMXM0LjA4LDIuOTQsNS45MiwxLjczaDBjMS44NC0xLjIsMi4xNy0zLjkzLjc0LTYuMDlzLTQuMDgtMi45NC01LjkyLTEuNzRaIi8+PHBhdGggY2xhc3M9ImNscy0yIiBkPSJNNy4zNCwxOS4wNWMyLS41My42Nyw4LjIxLS45NCw3LjQ5QTQuNzIsNC43MiwwLDAsMSw3LjM0LDE5LjA1WiIvPjxwYXRoIGNsYXNzPSJjbHMtMiIgZD0iTTMwLjI3LDE4Ljk0Yy0yLS41My0uNjcsOC4yMS45NCw3LjQ5QTQuNzIsNC43MiwwLDAsMCwzMC4yNywxOC45NFoiLz48cGF0aCBjbGFzcz0iY2xzLTIiIGQ9Ik0yMy41MywxMi40M2MzLjQyLS41Nyw2LjI3LDEuNDYsNi4xNiw1LjE3LS4xMiwxLjQzLTcuNDItNS02LjE2LTUuMTdaIi8+PHBhdGggY2xhc3M9ImNscy0yIiBkPSJNMTQuMDcsMTIuMzJjLTMuNDMtLjU3LTYuMjgsMS40Ni02LjE2LDUuMTdDOCwxOC45MiwxNS4zMywxMi41NCwxNC4wNywxMi4zMloiLz48cGF0aCBjbGFzcz0iY2xzLTIiIGQ9Ik0xOSwxMS40NmMtMi0uMDUtNCwxLjUyLTQsMi40MywwLDEuMSwxLjYxLDIuMjMsNCwyLjI2czQtLjksNC0yLTIuMjMtMi42Ni00LTIuNjRaIi8+PHBhdGggY2xhc3M9ImNscy0yIiBkPSJNMTkuMTEsMzQuMTVjMS43OC0uMDgsNC4xNy41Nyw0LjE4LDEuNDNzLTIuMTcsMi43NC00LjMsMi43LTQuMzYtMS44LTQuMzMtMi40NkMxNC42MiwzNC44NiwxNy4zNCwzNC4xLDE5LjExLDM0LjE1WiIvPjxwYXRoIGNsYXNzPSJjbHMtMiIgZD0iTTEyLjUzLDI5YzEuMjcsMS41MywxLjg1LDQuMjIuNzksNS0xLC42LTMuNDQuMzUtNS4xNi0yLjEzLTEuMTctMi4wOC0xLTQuMi0uMi00LjgzQzkuMTgsMjYuMzMsMTEuMDcsMjcuMzMsMTIuNTMsMjlaIi8+PHBhdGggY2xhc3M9ImNscy0yIiBkPSJNMjUuNDQsMjguNTRDMjQuMDYsMzAuMTUsMjMuMywzMy4wOCwyNC4zLDM0YzEsLjc0LDMuNTMuNjMsNS40My0yLDEuMzgtMS43Ny45MS00LjcyLjEzLTUuNTFDMjguNjksMjUuNjEsMjcsMjYuNzcsMjUuNDQsMjguNTRaIi8+PGNpcmNsZSBjbGFzcz0iY2xzLTMiIGN4PSIyOS40NCIgY3k9IjI2LjQ4IiByPSI5LjE3Ii8+PHBhdGggZD0iTTI5LjQ0LDE4YTguNTMsOC41MywwLDEsMS04LjUyLDguNTNBOC41Myw4LjUzLDAsMCwxLDI5LjQ0LDE4bTAtMS4yOGE5LjgxLDkuODEsMCwxLDAsOS44MSw5LjgxLDkuODIsOS44MiwwLDAsMC05LjgxLTkuODFaIi8+PHBhdGggY2xhc3M9ImNscy00IiBkPSJNMjYuNDMsMzAuNTVhMi44NywyLjg3LDAsMCwxLTEuMDctLjIxLDIuNTYsMi41NiwwLDAsMS0uOTEtLjY3LDMuMjYsMy4yNiwwLDAsMS0uNjQtMS4xNyw1LjM0LDUuMzQsMCwwLDEtLjI0LTEuNzFBNS45Myw1LjkzLDAsMCwxLDIzLjgyLDI1YTMuODgsMy44OCwwLDAsMSwuNjgtMS4yOCwyLjc3LDIuNzcsMCwwLDEsMS0uNzQsMi45LDIuOSwwLDAsMSwxLjE1LS4yNSwyLjk0LDIuOTQsMCwwLDEsMS4zMS4yNywzLjIzLDMuMjMsMCwwLDEsLjkuNjRsLS45MiwxLjA1YTEuNzMsMS43MywwLDAsMC0uNTItLjM3LDEuNDEsMS40MSwwLDAsMC0xLjI0LS4wNSwxLjI0LDEuMjQsMCwwLDAtLjQ5LjQsMi42OSwyLjY5LDAsMCwwLS4zNC43NCw0LjczLDQuNzMsMCwwLDAtLjE1LDEuMTEsMS43NywxLjc3LDAsMCwxLC4zMy0uMzQsMi40MiwyLjQyLDAsMCwxLC40LS4yNSwyLjIzLDIuMjMsMCwwLDEsLjQtLjE2LDEuODksMS44OSwwLDAsMSwuMzksMCwyLjkzLDIuOTMsMCwwLDEsLjkuMTQsMiwyLDAsMCwxLC43MS40MywxLjc3LDEuNzcsMCwwLDEsLjQ3LjczLDMuMDcsMy4wNywwLDAsMSwwLDIuMSwyLjUzLDIuNTMsMCwwLDEtLjU1LjgsMi4zNCwyLjM0LDAsMCwxLS43OS41MUEyLjkzLDIuOTMsMCwwLDEsMjYuNDMsMzAuNTVabTAtMS4zMWEuOTIuOTIsMCwwLDAsLjY3LS4yOSwxLjMyLDEuMzIsMCwwLDAsLjI4LS45NCwxLjEsMS4xLDAsMCwwLS4yOC0uODYsMS4wNSwxLjA1LDAsMCwwLS43MS0uMjQsMS4yNCwxLjI0LDAsMCwwLS42LjE3LDEuNTUsMS41NSwwLDAsMC0uNTUuNTgsMy4xNywzLjE3LDAsMCwwLC4xOC43NSwxLjc1LDEuNzUsMCwwLDAsLjI4LjQ5Ljg0Ljg0LDAsMCwwLC4zNC4yNkExLDEsMCwwLDAsMjYuNCwyOS4yNFoiLz48cGF0aCBjbGFzcz0iY2xzLTQiIGQ9Ik0zMi44OSwzMC40MVYyOC41OUgyOS42MXYtMS4ybDIuNzctNC41NmgyLjE1djQuNDNoLjg3djEuMzNoLS44N3YxLjgyWm0tMS42LTMuMTVoMS42VjI2YzAtLjI1LDAtLjUzLDAtLjg2czAtLjYuMDUtLjg0aC0uMDVjLS4wOS4yMS0uMTkuNDItLjI5LjYzbC0uMzMuNjVaIi8+PC9zdmc+', + name: 'logo', + }, + }, + aliases: ['raspberrypi3-64'], + version: '1', + partials: { + bootDevice: ['Connect power to the Raspberry Pi 3 (using 64bit OS)'], + }, + }, + logo: 'data:image/svg+xml;base64,PHN2ZyBpZD0iTGF5ZXJfMSIgZGF0YS1uYW1lPSJMYXllciAxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA0MCA0MCI+PGRlZnM+PHN0eWxlPi5jbHMtMXtmaWxsOiM3NWE5Mjg7fS5jbHMtMntmaWxsOiNiYzExNDI7fS5jbHMtM3tmaWxsOiNmZmY7fS5jbHMtNHtmaWxsOiMxYTFhMWE7fTwvc3R5bGU+PC9kZWZzPjxwYXRoIGQ9Ik0xMiwuNWExLjExLDEuMTEsMCwwLDAtLjY1LjI3QTEuNDgsMS40OCwwLDAsMCw5LjY3LjkzYy0uNzktLjExLTEsLjExLTEuMjQuMzUtLjE3LDAtMS4zLS4xOC0xLjgxLjU5LTEuMy0uMTUtMS43MS43Ny0xLjI0LDEuNjJhMS4xMSwxLjExLDAsMCwwLC4wOCwxLjU5Yy0uMjIuNDQtLjA5LjkxLjQzLDEuNDhBMS4yNCwxLjI0LDAsMCwwLDYuNSw3Ljk0Yy0uMDkuODQuNzcsMS4zMywxLDEuNS4xLjQ5LjMxLDEsMS4yOSwxLjIuMTYuNzMuNzUuODUsMS4zMiwxYTYuMTUsNi4xNSwwLDAsMC0zLjQ5LDYuMDZsLS4yOC41YTYsNiwwLDAsMC0xLjA2LDksMTYuMjksMTYuMjksMCwwLDAsLjgzLDIuNyw2LjU2LDYuNTYsMCwwLDAsNC4wOSw1LjI0LDE0LDE0LDAsMCwwLDMuOTIsMi4yM0E2LjU0LDYuNTQsMCwwLDAsMTksMzkuNUgxOWE2LjU0LDYuNTQsMCwwLDAsNC44Mi0yLjE2LDE0LDE0LDAsMCwwLDMuOTItMi4yMyw2LjU2LDYuNTYsMCwwLDAsNC4wOS01LjI0LDE2LjI5LDE2LjI5LDAsMCwwLC44My0yLjcsNiw2LDAsMCwwLTEuMDYtOWwtLjI4LS41YTYuMTUsNi4xNSwwLDAsMC0zLjQ5LTYuMDZjLjU3LS4xNiwxLjE2LS4yOCwxLjMyLTEsMS0uMjUsMS4xOS0uNzEsMS4yOS0xLjIuMjUtLjE3LDEuMTEtLjY2LDEtMS41YTEuMjQsMS4yNCwwLDAsMCwuNjEtMS4zOGMuNTItLjU3LjY1LTEsLjQzLTEuNDhhMS4xMSwxLjExLDAsMCwwLC4wOC0xLjU5Yy40Ny0uODUuMDYtMS43Ny0xLjI0LTEuNjItLjUxLS43Ny0xLjY0LS41OS0xLjgxLS41OS0uMi0uMjQtLjQ1LS40Ni0xLjI0LS4zNUExLjQ4LDEuNDgsMCwwLDAsMjYuNjUuNzdDMjYsLjIyLDI1LjQ5LjY2LDI1LC44M2MtLjg1LS4yOC0xLC4xLTEuNDYuMjUtLjkzLS4xOS0xLjIxLjIzLTEuNjUuNjhoLS41MkE1LjksNS45LDAsMCwwLDE5LDUuMTFhNiw2LDAsMCwwLTIuMzMtMy4zNmgtLjUyYy0uNDQtLjQ1LS43Mi0uODctMS42NS0uNjhDMTQuMDguOTMsMTMuODkuNTUsMTMsLjgzQTMuMzIsMy4zMiwwLDAsMCwxMiwuNVoiLz48cGF0aCBjbGFzcz0iY2xzLTEiIGQ9Ik05LjIyLDQuMTJjMy43LDEuOTEsNS44NSwzLjQ1LDcsNC43Ny0uNiwyLjQyLTMuNzUsMi41My00LjksMi40NmEuODguODgsMCwwLDAsLjUtLjQ0Yy0uMjktLjIxLTEuMzEsMC0yLS40M2EuNjYuNjYsMCwwLDAsLjUzLS4zMSw1LjkzLDUuOTMsMCwwLDEtMS44My0uNzYsMS4wNSwxLjA1LDAsMCwwLC43NS0uMTZBNy4yNCw3LjI0LDAsMCwxLDcuNTEsOC4xN2MuMzIsMCwuNjYsMCwuNzUtLjEyQTcuMDksNy4wOSwwLDAsMSw2Ljg1LDYuOTFhMS4wOCwxLjA4LDAsMCwwLC43My0uMDcsNS41Myw1LjUzLDAsMCwxLTEuMi0xLjMyLjkxLjkxLDAsMCwwLC44NCwwYy0uMTQtLjMyLS43NS0uNTEtMS4xLTEuMjYuMzQsMCwuNy4wNy43NywwYTMuNjEsMy42MSwwLDAsMC0uNy0xLjM5QTEyLjY2LDEyLjY2LDAsMCwwLDgsMi44bC0uNDYtLjQ2YTMuNTYsMy41NiwwLDAsMSwyLC4xOWMuMjQtLjE5LDAtLjQyLS4yOS0uNjdhNi42NCw2LjY0LDAsMCwxLDEuNjUuNDJjLjI3LS4yNC0uMTctLjQ4LS4zOC0uNzJhNC4yMSw0LjIxLDAsMCwxLDEuNzMuNjhjLjI5LS4yOCwwLS41MS0uMTctLjc1YTQuMTcsNC4xNywwLDAsMSwxLjQ1LjkzYy4xMy0uMTcuMzQtLjMuMDktLjcyYTMuNjgsMy42OCwwLDAsMSwxLjE3LDFjLjMxLS4yLjE4LS40Ny4xOC0uNzJBMTEsMTEsMCwwLDEsMTYuMiwzLjMxYy4wOS0uMDYuMTYtLjI2LjIyLS41OCwxLjI1LDEuMjEsMyw0LjI2LjQ1LDUuNDdBMjMuODgsMjMuODgsMCwwLDAsOS4yMiw0LjEyWiIvPjxwYXRoIGNsYXNzPSJjbHMtMSIgZD0iTTI4Ljg2LDQuMTJDMjUuMTYsNiwyMyw3LjU3LDIxLjgzLDguODljLjYsMi40MiwzLjc1LDIuNTMsNC45MSwyLjQ2YS44Ny44NywwLDAsMS0uNTEtLjQ0Yy4yOS0uMjEsMS4zMiwwLDItLjQzYS42Ni42NiwwLDAsMS0uNTMtLjMxLDUuNzMsNS43MywwLDAsMCwxLjgzLS43NiwxLDEsMCwwLDEtLjc0LS4xNiw3LjE5LDcuMTksMCwwLDAsMS43NS0xLjA4Yy0uMzEsMC0uNjUsMC0uNzUtLjEyYTcuMDksNy4wOSwwLDAsMCwxLjQxLTEuMTQsMS4xLDEuMSwwLDAsMS0uNzMtLjA3LDUuNTMsNS41MywwLDAsMCwxLjItMS4zMi45MS45MSwwLDAsMS0uODQsMEMzMSw1LjE5LDMxLjYyLDUsMzIsNC4yNWExLjkxLDEuOTEsMCwwLDEtLjc4LDAsMy42MSwzLjYxLDAsMCwxLC43LTEuMzlBMTIuNTgsMTIuNTgsMCwwLDEsMzAuMSwyLjhsLjQ1LS40NmEzLjU2LDMuNTYsMCwwLDAtMiwuMTljLS4yNC0uMTksMC0uNDIuMjktLjY3YTYuODgsNi44OCwwLDAsMC0xLjY1LjQyYy0uMjctLjI0LjE3LS40OC4zOC0uNzJhNC4yOCw0LjI4LDAsMCwwLTEuNzMuNjhjLS4yOS0uMjgsMC0uNTEuMTgtLjc1YTQuMjMsNC4yMywwLDAsMC0xLjQ2LjkzYy0uMTMtLjE3LS4zMy0uMy0uMDktLjcyYTMuNzQsMy43NCwwLDAsMC0xLjE2LDFjLS4zMS0uMi0uMTktLjQ3LS4xOS0uNzJhMTIuODQsMTIuODQsMCwwLDAtMS4yNiwxLjMyLDEuMTgsMS4xOCwwLDAsMS0uMjItLjU4Yy0xLjI0LDEuMjEtMyw0LjI2LS40NSw1LjQ3YTI0LDI0LDAsMCwxLDcuNjUtNC4wOFoiLz48cGF0aCBjbGFzcz0iY2xzLTIiIGQ9Ik0yMy41MywyOC43NmE0LjI4LDQuMjgsMCwwLDEtNC40NCw0LjA5LDQuMjcsNC4yNywwLDAsMS00LjQzLTQuMDksNC4yNyw0LjI3LDAsMCwxLDQuNDMtNC4wOEE0LjI3LDQuMjcsMCwwLDEsMjMuNTMsMjguNzZaIi8+PHBhdGggY2xhc3M9ImNscy0yIiBkPSJNMTYuNTIsMTcuMDhjMS44NCwxLjIxLDIuMTcsMy45My43NCw2LjFzLTQuMDcsMi45NC01LjkxLDEuNzNoMGMtMS44NC0xLjItMi4xNy0zLjkzLS43NC02LjA5czQuMDgtMi45NCw1LjkxLTEuNzRaIi8+PHBhdGggY2xhc3M9ImNscy0yIiBkPSJNMjEuNDgsMTYuODZjLTEuODMsMS4yMS0yLjE2LDMuOTQtLjc0LDYuMXM0LjA4LDIuOTQsNS45MiwxLjczaDBjMS44NC0xLjIsMi4xNy0zLjkzLjc0LTYuMDlzLTQuMDgtMi45NC01LjkyLTEuNzRaIi8+PHBhdGggY2xhc3M9ImNscy0yIiBkPSJNNy4zNCwxOS4wNWMyLS41My42Nyw4LjIxLS45NCw3LjQ5QTQuNzIsNC43MiwwLDAsMSw3LjM0LDE5LjA1WiIvPjxwYXRoIGNsYXNzPSJjbHMtMiIgZD0iTTMwLjI3LDE4Ljk0Yy0yLS41My0uNjcsOC4yMS45NCw3LjQ5QTQuNzIsNC43MiwwLDAsMCwzMC4yNywxOC45NFoiLz48cGF0aCBjbGFzcz0iY2xzLTIiIGQ9Ik0yMy41MywxMi40M2MzLjQyLS41Nyw2LjI3LDEuNDYsNi4xNiw1LjE3LS4xMiwxLjQzLTcuNDItNS02LjE2LTUuMTdaIi8+PHBhdGggY2xhc3M9ImNscy0yIiBkPSJNMTQuMDcsMTIuMzJjLTMuNDMtLjU3LTYuMjgsMS40Ni02LjE2LDUuMTdDOCwxOC45MiwxNS4zMywxMi41NCwxNC4wNywxMi4zMloiLz48cGF0aCBjbGFzcz0iY2xzLTIiIGQ9Ik0xOSwxMS40NmMtMi0uMDUtNCwxLjUyLTQsMi40MywwLDEuMSwxLjYxLDIuMjMsNCwyLjI2czQtLjksNC0yLTIuMjMtMi42Ni00LTIuNjRaIi8+PHBhdGggY2xhc3M9ImNscy0yIiBkPSJNMTkuMTEsMzQuMTVjMS43OC0uMDgsNC4xNy41Nyw0LjE4LDEuNDNzLTIuMTcsMi43NC00LjMsMi43LTQuMzYtMS44LTQuMzMtMi40NkMxNC42MiwzNC44NiwxNy4zNCwzNC4xLDE5LjExLDM0LjE1WiIvPjxwYXRoIGNsYXNzPSJjbHMtMiIgZD0iTTEyLjUzLDI5YzEuMjcsMS41MywxLjg1LDQuMjIuNzksNS0xLC42LTMuNDQuMzUtNS4xNi0yLjEzLTEuMTctMi4wOC0xLTQuMi0uMi00LjgzQzkuMTgsMjYuMzMsMTEuMDcsMjcuMzMsMTIuNTMsMjlaIi8+PHBhdGggY2xhc3M9ImNscy0yIiBkPSJNMjUuNDQsMjguNTRDMjQuMDYsMzAuMTUsMjMuMywzMy4wOCwyNC4zLDM0YzEsLjc0LDMuNTMuNjMsNS40My0yLDEuMzgtMS43Ny45MS00LjcyLjEzLTUuNTFDMjguNjksMjUuNjEsMjcsMjYuNzcsMjUuNDQsMjguNTRaIi8+PGNpcmNsZSBjbGFzcz0iY2xzLTMiIGN4PSIyOS40NCIgY3k9IjI2LjQ4IiByPSI5LjE3Ii8+PHBhdGggZD0iTTI5LjQ0LDE4YTguNTMsOC41MywwLDEsMS04LjUyLDguNTNBOC41Myw4LjUzLDAsMCwxLDI5LjQ0LDE4bTAtMS4yOGE5LjgxLDkuODEsMCwxLDAsOS44MSw5LjgxLDkuODIsOS44MiwwLDAsMC05LjgxLTkuODFaIi8+PHBhdGggY2xhc3M9ImNscy00IiBkPSJNMjYuNDMsMzAuNTVhMi44NywyLjg3LDAsMCwxLTEuMDctLjIxLDIuNTYsMi41NiwwLDAsMS0uOTEtLjY3LDMuMjYsMy4yNiwwLDAsMS0uNjQtMS4xNyw1LjM0LDUuMzQsMCwwLDEtLjI0LTEuNzFBNS45Myw1LjkzLDAsMCwxLDIzLjgyLDI1YTMuODgsMy44OCwwLDAsMSwuNjgtMS4yOCwyLjc3LDIuNzcsMCwwLDEsMS0uNzQsMi45LDIuOSwwLDAsMSwxLjE1LS4yNSwyLjk0LDIuOTQsMCwwLDEsMS4zMS4yNywzLjIzLDMuMjMsMCwwLDEsLjkuNjRsLS45MiwxLjA1YTEuNzMsMS43MywwLDAsMC0uNTItLjM3LDEuNDEsMS40MSwwLDAsMC0xLjI0LS4wNSwxLjI0LDEuMjQsMCwwLDAtLjQ5LjQsMi42OSwyLjY5LDAsMCwwLS4zNC43NCw0LjczLDQuNzMsMCwwLDAtLjE1LDEuMTEsMS43NywxLjc3LDAsMCwxLC4zMy0uMzQsMi40MiwyLjQyLDAsMCwxLC40LS4yNSwyLjIzLDIuMjMsMCwwLDEsLjQtLjE2LDEuODksMS44OSwwLDAsMSwuMzksMCwyLjkzLDIuOTMsMCwwLDEsLjkuMTQsMiwyLDAsMCwxLC43MS40MywxLjc3LDEuNzcsMCwwLDEsLjQ3LjczLDMuMDcsMy4wNywwLDAsMSwwLDIuMSwyLjUzLDIuNTMsMCwwLDEtLjU1LjgsMi4zNCwyLjM0LDAsMCwxLS43OS41MUEyLjkzLDIuOTMsMCwwLDEsMjYuNDMsMzAuNTVabTAtMS4zMWEuOTIuOTIsMCwwLDAsLjY3LS4yOSwxLjMyLDEuMzIsMCwwLDAsLjI4LS45NCwxLjEsMS4xLDAsMCwwLS4yOC0uODYsMS4wNSwxLjA1LDAsMCwwLS43MS0uMjQsMS4yNCwxLjI0LDAsMCwwLS42LjE3LDEuNTUsMS41NSwwLDAsMC0uNTUuNTgsMy4xNywzLjE3LDAsMCwwLC4xOC43NSwxLjc1LDEuNzUsMCwwLDAsLjI4LjQ5Ljg0Ljg0LDAsMCwwLC4zNC4yNkExLDEsMCwwLDAsMjYuNCwyOS4yNFoiLz48cGF0aCBjbGFzcz0iY2xzLTQiIGQ9Ik0zMi44OSwzMC40MVYyOC41OUgyOS42MXYtMS4ybDIuNzctNC41NmgyLjE1djQuNDNoLjg3djEuMzNoLS44N3YxLjgyWm0tMS42LTMuMTVoMS42VjI2YzAtLjI1LDAtLjUzLDAtLjg2czAtLjYuMDUtLjg0aC0uMDVjLS4wOS4yMS0uMTkuNDItLjI5LjYzbC0uMzMuNjVaIi8+PC9zdmc+', + }, + { + instructions: [ + 'Insert the SD card to the host machine.', + 'Write the balenaOS file you downloaded to the SD card. We recommend using Etcher.', + 'Wait for writing of balenaOS to complete.', + 'Remove the SD card from the host machine.', + 'Insert the freshly flashed SD card into the Raspberry Pi 4 (using 64bit OS).', + 'Connect power to the Raspberry Pi 4 (using 64bit OS) to boot the device.', + ], + options: [ + { + isGroup: true, + name: 'network', + message: 'Network', + options: [ + { + message: 'Network Connection', + name: 'network', + type: 'list', + choices: ['ethernet', 'wifi'], + }, + { + message: 'Wifi SSID', + name: 'wifiSsid', + type: 'text', + when: { + network: 'wifi', + }, + }, + { + message: 'Wifi Passphrase', + name: 'wifiKey', + type: 'password', + when: { + network: 'wifi', + }, + }, + ], + }, + { + isGroup: true, + isCollapsible: true, + collapsed: true, + name: 'advanced', + message: 'Advanced', + options: [ + { + message: 'Check for updates every X minutes', + name: 'appUpdatePollInterval', + type: 'number', + min: 10, + default: 10, + }, + ], + }, + ], + yocto: { + machine: 'raspberrypi4-64', + image: 'balena-image', + fstype: 'balenaos-img', + version: 'yocto-honister', + deployArtifact: 'balena-image-raspberrypi4-64.balenaos-img', + compressed: true, + }, + is_default_for__application: [ + { + application_tag: [ + { + value: 'esr', + }, + ], + should_be_running__release: [ + { + release_tag: [ + { + tag_key: 'meta-balena-base', + value: 'v2.114.25', + }, + { + tag_key: 'version', + value: '2023.7.0', + }, + ], + }, + ], + is_archived: false, + }, + { + application_tag: [], + should_be_running__release: [ + { + release_tag: [ + { + tag_key: 'version', + value: '3.2.7+rev1', + }, + ], + }, + ], + is_archived: false, + }, + ], + is_of__cpu_architecture: [ + { + slug: 'aarch64', + }, + ], + slug: 'raspberrypi4-64', + name: 'Raspberry Pi 4 (using 64bit OS)', + contract: { + data: { + led: true, + arch: 'aarch64', + hdmi: true, + media: { + altBoot: ['usb_mass_storage', 'network'], + defaultBoot: 'sdcard', + }, + storage: { + internal: false, + }, + is_private: false, + connectivity: { + wifi: true, + bluetooth: true, + }, + }, + name: 'Raspberry Pi 4 (using 64bit OS)', + slug: 'raspberrypi4-64', + type: 'hw.device-type', + assets: { + logo: { + url: 'data:image/svg+xml;base64,PHN2ZyBpZD0iTGF5ZXJfMSIgZGF0YS1uYW1lPSJMYXllciAxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA0MCA0MCI+PGRlZnM+PHN0eWxlPi5jbHMtMXtmaWxsOiM3NWE5Mjg7fS5jbHMtMntmaWxsOiNiYzExNDI7fS5jbHMtM3tmaWxsOiNmZmY7fS5jbHMtNHtpc29sYXRpb246aXNvbGF0ZTt9LmNscy01e2ZpbGw6I2Q4MjI0Yzt9PC9zdHlsZT48L2RlZnM+PHBhdGggZD0iTTEyLC41YTEuMTEsMS4xMSwwLDAsMC0uNjUuMjdBMS40OCwxLjQ4LDAsMCwwLDkuNjcuOTNjLS43OS0uMTEtMSwuMTEtMS4yNC4zNS0uMTcsMC0xLjMtLjE4LTEuODEuNTktMS4zLS4xNS0xLjcxLjc3LTEuMjQsMS42MmExLjExLDEuMTEsMCwwLDAsLjA4LDEuNTljLS4yMi40NC0uMDkuOTEuNDMsMS40OEExLjI0LDEuMjQsMCwwLDAsNi41LDcuOTRjLS4wOS44NC43NywxLjMzLDEsMS41LjEuNDkuMzEsMSwxLjI5LDEuMi4xNi43My43NS44NSwxLjMyLDFhNi4xNSw2LjE1LDAsMCwwLTMuNDksNi4wNmwtLjI4LjVhNiw2LDAsMCwwLTEuMDYsOSwxNi4yOSwxNi4yOSwwLDAsMCwuODMsMi43LDYuNTYsNi41NiwwLDAsMCw0LjA5LDUuMjQsMTQsMTQsMCwwLDAsMy45MiwyLjIzQTYuNTQsNi41NCwwLDAsMCwxOSwzOS41SDE5YTYuNTQsNi41NCwwLDAsMCw0LjgyLTIuMTYsMTQsMTQsMCwwLDAsMy45Mi0yLjIzLDYuNTYsNi41NiwwLDAsMCw0LjA5LTUuMjQsMTYuMjksMTYuMjksMCwwLDAsLjgzLTIuNyw2LDYsMCwwLDAtMS4wNi05bC0uMjgtLjVhNi4xNSw2LjE1LDAsMCwwLTMuNDktNi4wNmMuNTctLjE2LDEuMTYtLjI4LDEuMzItMSwxLS4yNSwxLjE5LS43MSwxLjI5LTEuMi4yNS0uMTcsMS4xMS0uNjYsMS0xLjVhMS4yNCwxLjI0LDAsMCwwLC42MS0xLjM4Yy41Mi0uNTcuNjUtMSwuNDMtMS40OGExLjExLDEuMTEsMCwwLDAsLjA4LTEuNTljLjQ3LS44NS4wNi0xLjc3LTEuMjQtMS42Mi0uNTEtLjc3LTEuNjQtLjU5LTEuODEtLjU5LS4yLS4yNC0uNDUtLjQ2LTEuMjQtLjM1QTEuNDgsMS40OCwwLDAsMCwyNi42NS43N0MyNiwuMjIsMjUuNDkuNjYsMjUsLjgzYy0uODUtLjI4LTEsLjEtMS40Ni4yNS0uOTMtLjE5LTEuMjEuMjMtMS42NS42OGgtLjUyQTUuOSw1LjksMCwwLDAsMTksNS4xMWE2LDYsMCwwLDAtMi4zMy0zLjM2aC0uNTJjLS40NC0uNDUtLjcyLS44Ny0xLjY1LS42OEMxNC4wOC45MywxMy44OS41NSwxMywuODNBMy4zMiwzLjMyLDAsMCwwLDEyLC41WiIvPjxwYXRoIGNsYXNzPSJjbHMtMSIgZD0iTTkuMjIsNC4xMmMzLjcsMS45MSw1Ljg1LDMuNDUsNyw0Ljc3LS42LDIuNDItMy43NSwyLjUzLTQuOSwyLjQ2YS44OC44OCwwLDAsMCwuNS0uNDRjLS4yOS0uMjEtMS4zMSwwLTItLjQzYS42Ni42NiwwLDAsMCwuNTMtLjMxLDUuOTMsNS45MywwLDAsMS0xLjgzLS43NiwxLjA1LDEuMDUsMCwwLDAsLjc1LS4xNkE3LjI0LDcuMjQsMCwwLDEsNy41MSw4LjE3Yy4zMiwwLC42NiwwLC43NS0uMTJBNy4wOSw3LjA5LDAsMCwxLDYuODUsNi45MWExLjA4LDEuMDgsMCwwLDAsLjczLS4wNyw1LjUzLDUuNTMsMCwwLDEtMS4yLTEuMzIuOTEuOTEsMCwwLDAsLjg0LDBjLS4xNC0uMzItLjc1LS41MS0xLjEtMS4yNi4zNCwwLC43LjA3Ljc3LDBhMy42MSwzLjYxLDAsMCwwLS43LTEuMzlBMTIuNjYsMTIuNjYsMCwwLDAsOCwyLjhsLS40Ni0uNDZhMy41NiwzLjU2LDAsMCwxLDIsLjE5Yy4yNC0uMTksMC0uNDItLjI5LS42N2E2LjY0LDYuNjQsMCwwLDEsMS42NS40MmMuMjctLjI0LS4xNy0uNDgtLjM4LS43MmE0LjIxLDQuMjEsMCwwLDEsMS43My42OGMuMjktLjI4LDAtLjUxLS4xNy0uNzVhNC4xNyw0LjE3LDAsMCwxLDEuNDUuOTNjLjEzLS4xNy4zNC0uMy4wOS0uNzJhMy42OCwzLjY4LDAsMCwxLDEuMTcsMWMuMzEtLjIuMTgtLjQ3LjE4LS43MkExMSwxMSwwLDAsMSwxNi4yLDMuMzFjLjA5LS4wNi4xNi0uMjYuMjItLjU4LDEuMjUsMS4yMSwzLDQuMjYuNDUsNS40N0EyMy44OCwyMy44OCwwLDAsMCw5LjIyLDQuMTJaIi8+PHBhdGggY2xhc3M9ImNscy0xIiBkPSJNMjguODYsNC4xMkMyNS4xNiw2LDIzLDcuNTcsMjEuODMsOC44OWMuNiwyLjQyLDMuNzUsMi41Myw0LjkxLDIuNDZhLjg3Ljg3LDAsMCwxLS41MS0uNDRjLjI5LS4yMSwxLjMyLDAsMi0uNDNhLjY2LjY2LDAsMCwxLS41My0uMzEsNS43Myw1LjczLDAsMCwwLDEuODMtLjc2LDEsMSwwLDAsMS0uNzQtLjE2LDcuMTksNy4xOSwwLDAsMCwxLjc1LTEuMDhjLS4zMSwwLS42NSwwLS43NS0uMTJhNy4wOSw3LjA5LDAsMCwwLDEuNDEtMS4xNCwxLjEsMS4xLDAsMCwxLS43My0uMDcsNS41Myw1LjUzLDAsMCwwLDEuMi0xLjMyLjkxLjkxLDAsMCwxLS44NCwwQzMxLDUuMTksMzEuNjIsNSwzMiw0LjI1YTEuOTEsMS45MSwwLDAsMS0uNzgsMCwzLjYxLDMuNjEsMCwwLDEsLjctMS4zOUExMi41OCwxMi41OCwwLDAsMSwzMC4xLDIuOGwuNDUtLjQ2YTMuNTYsMy41NiwwLDAsMC0yLC4xOWMtLjI0LS4xOSwwLS40Mi4yOS0uNjdhNi44OCw2Ljg4LDAsMCwwLTEuNjUuNDJjLS4yNy0uMjQuMTctLjQ4LjM4LS43MmE0LjI4LDQuMjgsMCwwLDAtMS43My42OGMtLjI5LS4yOCwwLS41MS4xOC0uNzVhNC4yMyw0LjIzLDAsMCwwLTEuNDYuOTNjLS4xMy0uMTctLjMzLS4zLS4wOS0uNzJhMy43NCwzLjc0LDAsMCwwLTEuMTYsMWMtLjMxLS4yLS4xOS0uNDctLjE5LS43MmExMi44NCwxMi44NCwwLDAsMC0xLjI2LDEuMzIsMS4xOCwxLjE4LDAsMCwxLS4yMi0uNThjLTEuMjQsMS4yMS0zLDQuMjYtLjQ1LDUuNDdhMjQsMjQsMCwwLDEsNy42NS00LjA4WiIvPjxwYXRoIGNsYXNzPSJjbHMtMiIgZD0iTTIzLjUzLDI4Ljc2YTQuMjgsNC4yOCwwLDAsMS00LjQ0LDQuMDksNC4yNyw0LjI3LDAsMCwxLTQuNDMtNC4wOSw0LjI3LDQuMjcsMCwwLDEsNC40My00LjA4QTQuMjcsNC4yNywwLDAsMSwyMy41MywyOC43NloiLz48cGF0aCBjbGFzcz0iY2xzLTIiIGQ9Ik0xNi41MiwxNy4wOGMxLjg0LDEuMjEsMi4xNywzLjkzLjc0LDYuMXMtNC4wNywyLjk0LTUuOTEsMS43M2gwYy0xLjg0LTEuMi0yLjE3LTMuOTMtLjc0LTYuMDlzNC4wOC0yLjk0LDUuOTEtMS43NFoiLz48cGF0aCBjbGFzcz0iY2xzLTIiIGQ9Ik0yMS40OCwxNi44NmMtMS44MywxLjIxLTIuMTYsMy45NC0uNzQsNi4xczQuMDgsMi45NCw1LjkyLDEuNzNoMGMxLjg0LTEuMiwyLjE3LTMuOTMuNzQtNi4wOXMtNC4wOC0yLjk0LTUuOTItMS43NFoiLz48cGF0aCBjbGFzcz0iY2xzLTIiIGQ9Ik03LjM0LDE5LjA1YzItLjUzLjY3LDguMjEtLjk0LDcuNDlBNC43Miw0LjcyLDAsMCwxLDcuMzQsMTkuMDVaIi8+PHBhdGggY2xhc3M9ImNscy0yIiBkPSJNMzAuMjcsMTguOTRjLTItLjUzLS42Nyw4LjIxLjk0LDcuNDlBNC43Miw0LjcyLDAsMCwwLDMwLjI3LDE4Ljk0WiIvPjxwYXRoIGNsYXNzPSJjbHMtMiIgZD0iTTIzLjUzLDEyLjQzYzMuNDItLjU3LDYuMjcsMS40Niw2LjE2LDUuMTctLjEyLDEuNDMtNy40Mi01LTYuMTYtNS4xN1oiLz48cGF0aCBjbGFzcz0iY2xzLTIiIGQ9Ik0xNC4wNywxMi4zMmMtMy40My0uNTctNi4yOCwxLjQ2LTYuMTYsNS4xN0M4LDE4LjkyLDE1LjMzLDEyLjU0LDE0LjA3LDEyLjMyWiIvPjxwYXRoIGNsYXNzPSJjbHMtMiIgZD0iTTE5LDExLjQ2Yy0yLS4wNS00LDEuNTItNCwyLjQzLDAsMS4xLDEuNjEsMi4yMyw0LDIuMjZzNC0uOSw0LTItMi4yMy0yLjY2LTQtMi42NFoiLz48cGF0aCBjbGFzcz0iY2xzLTIiIGQ9Ik0xOS4xMSwzNC4xNWMxLjc4LS4wOCw0LjE3LjU3LDQuMTgsMS40M3MtMi4xNywyLjc0LTQuMywyLjctNC4zNi0xLjgtNC4zMy0yLjQ2QzE0LjYyLDM0Ljg2LDE3LjM0LDM0LjEsMTkuMTEsMzQuMTVaIi8+PHBhdGggY2xhc3M9ImNscy0yIiBkPSJNMTIuNTMsMjljMS4yNywxLjUzLDEuODUsNC4yMi43OSw1LTEsLjYtMy40NC4zNS01LjE2LTIuMTMtMS4xNy0yLjA4LTEtNC4yLS4yLTQuODNDOS4xOCwyNi4zMywxMS4wNywyNy4zMywxMi41MywyOVoiLz48cGF0aCBjbGFzcz0iY2xzLTIiIGQ9Ik0yNS40NCwyOC41NEMyNC4wNiwzMC4xNSwyMy4zLDMzLjA4LDI0LjMsMzRjMSwuNzQsMy41My42Myw1LjQzLTIsMS4zOC0xLjc3LjkxLTQuNzIuMTMtNS41MUMyOC42OSwyNS42MSwyNywyNi43NywyNS40NCwyOC41NFoiLz48Y2lyY2xlIGNsYXNzPSJjbHMtMyIgY3g9IjI5LjQ0IiBjeT0iMjYuNDgiIHI9IjkuMTciLz48cGF0aCBkPSJNMjkuNDQsMThhOC41Myw4LjUzLDAsMSwxLTguNTIsOC41M0E4LjUzLDguNTMsMCwwLDEsMjkuNDQsMThtMC0xLjI4YTkuODEsOS44MSwwLDEsMCw5LjgxLDkuODEsOS44Miw5LjgyLDAsMCwwLTkuODEtOS44MVoiLz48ZyBjbGFzcz0iY2xzLTQiPjxwYXRoIGNsYXNzPSJjbHMtNSIgZD0iTTI5LjYzLDMxLjYyVjI5LjE4SDI1LjIyVjI3LjU2bDMuNzItNi4xM2gyLjg5djZIMzN2MS43OUgzMS44M3YyLjQ0Wm0tMi4xNS00LjIzaDIuMTVWMjUuNzNxMC0uNSwwLTEuMTRjMC0uNDQsMC0uODEuMDctMS4xNGgtLjA3Yy0uMTMuMjgtLjI2LjU3LS40Ljg1cy0uMjguNTgtLjQzLjg3WiIvPjwvZz48L3N2Zz4=', + name: 'logo', + }, + }, + aliases: ['raspberrypi4-64'], + version: '1', + partials: { + bootDevice: ['Connect power to the Raspberry Pi 4 (using 64bit OS)'], + }, + }, + logo: 'data:image/svg+xml;base64,PHN2ZyBpZD0iTGF5ZXJfMSIgZGF0YS1uYW1lPSJMYXllciAxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA0MCA0MCI+PGRlZnM+PHN0eWxlPi5jbHMtMXtmaWxsOiM3NWE5Mjg7fS5jbHMtMntmaWxsOiNiYzExNDI7fS5jbHMtM3tmaWxsOiNmZmY7fS5jbHMtNHtpc29sYXRpb246aXNvbGF0ZTt9LmNscy01e2ZpbGw6I2Q4MjI0Yzt9PC9zdHlsZT48L2RlZnM+PHBhdGggZD0iTTEyLC41YTEuMTEsMS4xMSwwLDAsMC0uNjUuMjdBMS40OCwxLjQ4LDAsMCwwLDkuNjcuOTNjLS43OS0uMTEtMSwuMTEtMS4yNC4zNS0uMTcsMC0xLjMtLjE4LTEuODEuNTktMS4zLS4xNS0xLjcxLjc3LTEuMjQsMS42MmExLjExLDEuMTEsMCwwLDAsLjA4LDEuNTljLS4yMi40NC0uMDkuOTEuNDMsMS40OEExLjI0LDEuMjQsMCwwLDAsNi41LDcuOTRjLS4wOS44NC43NywxLjMzLDEsMS41LjEuNDkuMzEsMSwxLjI5LDEuMi4xNi43My43NS44NSwxLjMyLDFhNi4xNSw2LjE1LDAsMCwwLTMuNDksNi4wNmwtLjI4LjVhNiw2LDAsMCwwLTEuMDYsOSwxNi4yOSwxNi4yOSwwLDAsMCwuODMsMi43LDYuNTYsNi41NiwwLDAsMCw0LjA5LDUuMjQsMTQsMTQsMCwwLDAsMy45MiwyLjIzQTYuNTQsNi41NCwwLDAsMCwxOSwzOS41SDE5YTYuNTQsNi41NCwwLDAsMCw0LjgyLTIuMTYsMTQsMTQsMCwwLDAsMy45Mi0yLjIzLDYuNTYsNi41NiwwLDAsMCw0LjA5LTUuMjQsMTYuMjksMTYuMjksMCwwLDAsLjgzLTIuNyw2LDYsMCwwLDAtMS4wNi05bC0uMjgtLjVhNi4xNSw2LjE1LDAsMCwwLTMuNDktNi4wNmMuNTctLjE2LDEuMTYtLjI4LDEuMzItMSwxLS4yNSwxLjE5LS43MSwxLjI5LTEuMi4yNS0uMTcsMS4xMS0uNjYsMS0xLjVhMS4yNCwxLjI0LDAsMCwwLC42MS0xLjM4Yy41Mi0uNTcuNjUtMSwuNDMtMS40OGExLjExLDEuMTEsMCwwLDAsLjA4LTEuNTljLjQ3LS44NS4wNi0xLjc3LTEuMjQtMS42Mi0uNTEtLjc3LTEuNjQtLjU5LTEuODEtLjU5LS4yLS4yNC0uNDUtLjQ2LTEuMjQtLjM1QTEuNDgsMS40OCwwLDAsMCwyNi42NS43N0MyNiwuMjIsMjUuNDkuNjYsMjUsLjgzYy0uODUtLjI4LTEsLjEtMS40Ni4yNS0uOTMtLjE5LTEuMjEuMjMtMS42NS42OGgtLjUyQTUuOSw1LjksMCwwLDAsMTksNS4xMWE2LDYsMCwwLDAtMi4zMy0zLjM2aC0uNTJjLS40NC0uNDUtLjcyLS44Ny0xLjY1LS42OEMxNC4wOC45MywxMy44OS41NSwxMywuODNBMy4zMiwzLjMyLDAsMCwwLDEyLC41WiIvPjxwYXRoIGNsYXNzPSJjbHMtMSIgZD0iTTkuMjIsNC4xMmMzLjcsMS45MSw1Ljg1LDMuNDUsNyw0Ljc3LS42LDIuNDItMy43NSwyLjUzLTQuOSwyLjQ2YS44OC44OCwwLDAsMCwuNS0uNDRjLS4yOS0uMjEtMS4zMSwwLTItLjQzYS42Ni42NiwwLDAsMCwuNTMtLjMxLDUuOTMsNS45MywwLDAsMS0xLjgzLS43NiwxLjA1LDEuMDUsMCwwLDAsLjc1LS4xNkE3LjI0LDcuMjQsMCwwLDEsNy41MSw4LjE3Yy4zMiwwLC42NiwwLC43NS0uMTJBNy4wOSw3LjA5LDAsMCwxLDYuODUsNi45MWExLjA4LDEuMDgsMCwwLDAsLjczLS4wNyw1LjUzLDUuNTMsMCwwLDEtMS4yLTEuMzIuOTEuOTEsMCwwLDAsLjg0LDBjLS4xNC0uMzItLjc1LS41MS0xLjEtMS4yNi4zNCwwLC43LjA3Ljc3LDBhMy42MSwzLjYxLDAsMCwwLS43LTEuMzlBMTIuNjYsMTIuNjYsMCwwLDAsOCwyLjhsLS40Ni0uNDZhMy41NiwzLjU2LDAsMCwxLDIsLjE5Yy4yNC0uMTksMC0uNDItLjI5LS42N2E2LjY0LDYuNjQsMCwwLDEsMS42NS40MmMuMjctLjI0LS4xNy0uNDgtLjM4LS43MmE0LjIxLDQuMjEsMCwwLDEsMS43My42OGMuMjktLjI4LDAtLjUxLS4xNy0uNzVhNC4xNyw0LjE3LDAsMCwxLDEuNDUuOTNjLjEzLS4xNy4zNC0uMy4wOS0uNzJhMy42OCwzLjY4LDAsMCwxLDEuMTcsMWMuMzEtLjIuMTgtLjQ3LjE4LS43MkExMSwxMSwwLDAsMSwxNi4yLDMuMzFjLjA5LS4wNi4xNi0uMjYuMjItLjU4LDEuMjUsMS4yMSwzLDQuMjYuNDUsNS40N0EyMy44OCwyMy44OCwwLDAsMCw5LjIyLDQuMTJaIi8+PHBhdGggY2xhc3M9ImNscy0xIiBkPSJNMjguODYsNC4xMkMyNS4xNiw2LDIzLDcuNTcsMjEuODMsOC44OWMuNiwyLjQyLDMuNzUsMi41Myw0LjkxLDIuNDZhLjg3Ljg3LDAsMCwxLS41MS0uNDRjLjI5LS4yMSwxLjMyLDAsMi0uNDNhLjY2LjY2LDAsMCwxLS41My0uMzEsNS43Myw1LjczLDAsMCwwLDEuODMtLjc2LDEsMSwwLDAsMS0uNzQtLjE2LDcuMTksNy4xOSwwLDAsMCwxLjc1LTEuMDhjLS4zMSwwLS42NSwwLS43NS0uMTJhNy4wOSw3LjA5LDAsMCwwLDEuNDEtMS4xNCwxLjEsMS4xLDAsMCwxLS43My0uMDcsNS41Myw1LjUzLDAsMCwwLDEuMi0xLjMyLjkxLjkxLDAsMCwxLS44NCwwQzMxLDUuMTksMzEuNjIsNSwzMiw0LjI1YTEuOTEsMS45MSwwLDAsMS0uNzgsMCwzLjYxLDMuNjEsMCwwLDEsLjctMS4zOUExMi41OCwxMi41OCwwLDAsMSwzMC4xLDIuOGwuNDUtLjQ2YTMuNTYsMy41NiwwLDAsMC0yLC4xOWMtLjI0LS4xOSwwLS40Mi4yOS0uNjdhNi44OCw2Ljg4LDAsMCwwLTEuNjUuNDJjLS4yNy0uMjQuMTctLjQ4LjM4LS43MmE0LjI4LDQuMjgsMCwwLDAtMS43My42OGMtLjI5LS4yOCwwLS41MS4xOC0uNzVhNC4yMyw0LjIzLDAsMCwwLTEuNDYuOTNjLS4xMy0uMTctLjMzLS4zLS4wOS0uNzJhMy43NCwzLjc0LDAsMCwwLTEuMTYsMWMtLjMxLS4yLS4xOS0uNDctLjE5LS43MmExMi44NCwxMi44NCwwLDAsMC0xLjI2LDEuMzIsMS4xOCwxLjE4LDAsMCwxLS4yMi0uNThjLTEuMjQsMS4yMS0zLDQuMjYtLjQ1LDUuNDdhMjQsMjQsMCwwLDEsNy42NS00LjA4WiIvPjxwYXRoIGNsYXNzPSJjbHMtMiIgZD0iTTIzLjUzLDI4Ljc2YTQuMjgsNC4yOCwwLDAsMS00LjQ0LDQuMDksNC4yNyw0LjI3LDAsMCwxLTQuNDMtNC4wOSw0LjI3LDQuMjcsMCwwLDEsNC40My00LjA4QTQuMjcsNC4yNywwLDAsMSwyMy41MywyOC43NloiLz48cGF0aCBjbGFzcz0iY2xzLTIiIGQ9Ik0xNi41MiwxNy4wOGMxLjg0LDEuMjEsMi4xNywzLjkzLjc0LDYuMXMtNC4wNywyLjk0LTUuOTEsMS43M2gwYy0xLjg0LTEuMi0yLjE3LTMuOTMtLjc0LTYuMDlzNC4wOC0yLjk0LDUuOTEtMS43NFoiLz48cGF0aCBjbGFzcz0iY2xzLTIiIGQ9Ik0yMS40OCwxNi44NmMtMS44MywxLjIxLTIuMTYsMy45NC0uNzQsNi4xczQuMDgsMi45NCw1LjkyLDEuNzNoMGMxLjg0LTEuMiwyLjE3LTMuOTMuNzQtNi4wOXMtNC4wOC0yLjk0LTUuOTItMS43NFoiLz48cGF0aCBjbGFzcz0iY2xzLTIiIGQ9Ik03LjM0LDE5LjA1YzItLjUzLjY3LDguMjEtLjk0LDcuNDlBNC43Miw0LjcyLDAsMCwxLDcuMzQsMTkuMDVaIi8+PHBhdGggY2xhc3M9ImNscy0yIiBkPSJNMzAuMjcsMTguOTRjLTItLjUzLS42Nyw4LjIxLjk0LDcuNDlBNC43Miw0LjcyLDAsMCwwLDMwLjI3LDE4Ljk0WiIvPjxwYXRoIGNsYXNzPSJjbHMtMiIgZD0iTTIzLjUzLDEyLjQzYzMuNDItLjU3LDYuMjcsMS40Niw2LjE2LDUuMTctLjEyLDEuNDMtNy40Mi01LTYuMTYtNS4xN1oiLz48cGF0aCBjbGFzcz0iY2xzLTIiIGQ9Ik0xNC4wNywxMi4zMmMtMy40My0uNTctNi4yOCwxLjQ2LTYuMTYsNS4xN0M4LDE4LjkyLDE1LjMzLDEyLjU0LDE0LjA3LDEyLjMyWiIvPjxwYXRoIGNsYXNzPSJjbHMtMiIgZD0iTTE5LDExLjQ2Yy0yLS4wNS00LDEuNTItNCwyLjQzLDAsMS4xLDEuNjEsMi4yMyw0LDIuMjZzNC0uOSw0LTItMi4yMy0yLjY2LTQtMi42NFoiLz48cGF0aCBjbGFzcz0iY2xzLTIiIGQ9Ik0xOS4xMSwzNC4xNWMxLjc4LS4wOCw0LjE3LjU3LDQuMTgsMS40M3MtMi4xNywyLjc0LTQuMywyLjctNC4zNi0xLjgtNC4zMy0yLjQ2QzE0LjYyLDM0Ljg2LDE3LjM0LDM0LjEsMTkuMTEsMzQuMTVaIi8+PHBhdGggY2xhc3M9ImNscy0yIiBkPSJNMTIuNTMsMjljMS4yNywxLjUzLDEuODUsNC4yMi43OSw1LTEsLjYtMy40NC4zNS01LjE2LTIuMTMtMS4xNy0yLjA4LTEtNC4yLS4yLTQuODNDOS4xOCwyNi4zMywxMS4wNywyNy4zMywxMi41MywyOVoiLz48cGF0aCBjbGFzcz0iY2xzLTIiIGQ9Ik0yNS40NCwyOC41NEMyNC4wNiwzMC4xNSwyMy4zLDMzLjA4LDI0LjMsMzRjMSwuNzQsMy41My42Myw1LjQzLTIsMS4zOC0xLjc3LjkxLTQuNzIuMTMtNS41MUMyOC42OSwyNS42MSwyNywyNi43NywyNS40NCwyOC41NFoiLz48Y2lyY2xlIGNsYXNzPSJjbHMtMyIgY3g9IjI5LjQ0IiBjeT0iMjYuNDgiIHI9IjkuMTciLz48cGF0aCBkPSJNMjkuNDQsMThhOC41Myw4LjUzLDAsMSwxLTguNTIsOC41M0E4LjUzLDguNTMsMCwwLDEsMjkuNDQsMThtMC0xLjI4YTkuODEsOS44MSwwLDEsMCw5LjgxLDkuODEsOS44Miw5LjgyLDAsMCwwLTkuODEtOS44MVoiLz48ZyBjbGFzcz0iY2xzLTQiPjxwYXRoIGNsYXNzPSJjbHMtNSIgZD0iTTI5LjYzLDMxLjYyVjI5LjE4SDI1LjIyVjI3LjU2bDMuNzItNi4xM2gyLjg5djZIMzN2MS43OUgzMS44M3YyLjQ0Wm0tMi4xNS00LjIzaDIuMTVWMjUuNzNxMC0uNSwwLTEuMTRjMC0uNDQsMC0uODEuMDctMS4xNGgtLjA3Yy0uMTMuMjgtLjI2LjU3LS40Ljg1cy0uMjguNTgtLjQzLjg3WiIvPjwvZz48L3N2Zz4=', + }, + { + instructions: [ + 'Put the device in recovery mode and connect to the host computer via USB', + 'Unzip the balenaOS image and use the Jetson Flash tool to flash the Nvidia Jetson Nano SD-CARD.', + 'Wait for writing of balenaOS to complete.', + 'Connect power to the Nvidia Jetson Nano SD-CARD to boot the device.', + ], + options: [ + { + isGroup: true, + name: 'network', + message: 'Network', + options: [ + { + message: 'Network Connection', + name: 'network', + type: 'list', + choices: ['ethernet', 'wifi'], + }, + { + message: 'Wifi SSID', + name: 'wifiSsid', + type: 'text', + when: { + network: 'wifi', + }, + }, + { + message: 'Wifi Passphrase', + name: 'wifiKey', + type: 'password', + when: { + network: 'wifi', + }, + }, + ], + }, + { + isGroup: true, + isCollapsible: true, + collapsed: true, + name: 'advanced', + message: 'Advanced', + options: [ + { + message: 'Check for updates every X minutes', + name: 'appUpdatePollInterval', + type: 'number', + min: 10, + default: 10, + }, + ], + }, + ], + yocto: { + machine: 'jetson-nano', + image: 'balena-image', + fstype: 'balenaos-img', + version: 'yocto-honister', + deployArtifact: 'balena-image-jetson-nano.balenaos-img', + compressed: true, + }, + is_default_for__application: [ + { + application_tag: [ + { + value: 'esr', + }, + ], + should_be_running__release: [], + is_archived: false, + }, + { + application_tag: [], + should_be_running__release: [ + { + release_tag: [ + { + tag_key: 'version', + value: '2.103.2', + }, + ], + }, + ], + is_archived: false, + }, + ], + is_of__cpu_architecture: [ + { + slug: 'aarch64', + }, + ], + slug: 'jetson-nano', + name: 'Nvidia Jetson Nano SD-CARD', + contract: { + data: { + led: false, + arch: 'aarch64', + hdmi: true, + media: { + defaultBoot: 'sdcard', + }, + storage: { + internal: false, + }, + is_private: false, + connectivity: { + wifi: false, + bluetooth: false, + }, + flashProtocol: 'jetsonFlash', + }, + name: 'Nvidia Jetson Nano SD-CARD', + slug: 'jetson-nano', + type: 'hw.device-type', + assets: { + logo: { + url: 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPCEtLSBHZW5lcmF0b3I6IEFkb2JlIElsbHVzdHJhdG9yIDIzLjAuMiwgU1ZHIEV4cG9ydCBQbHVnLUluIC4gU1ZHIFZlcnNpb246IDYuMDAgQnVpbGQgMCkgIC0tPgo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkxheWVyXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IgoJIHZpZXdCb3g9IjAgMCA1MyA1NiIgc3R5bGU9ImVuYWJsZS1iYWNrZ3JvdW5kOm5ldyAwIDAgNTMgNTY7IiB4bWw6c3BhY2U9InByZXNlcnZlIj4KPHN0eWxlIHR5cGU9InRleHQvY3NzIj4KCS5zdDB7ZmlsbDojNzdCOTAwO30KPC9zdHlsZT4KPHRpdGxlPk52aWRpYV9sb2dvPC90aXRsZT4KPGRlc2M+Q3JlYXRlZCB3aXRoIFNrZXRjaC48L2Rlc2M+CjxwYXRoIGNsYXNzPSJzdDAiIGQ9Ik01LjgsMTQuOGMwLDAsNC43LTYuOSwxNC03LjZWNC43QzkuNCw1LjYsMC41LDE0LjMsMC41LDE0LjNzNS4xLDE0LjcsMTkuMywxNnYtMi43QzkuMywyNi4zLDUuOCwxNC44LDUuOCwxNC44Cgl6Ii8+CjxwYXRoIGNsYXNzPSJzdDAiIGQ9Ik0xOS44LDIyLjR2Mi40Yy03LjktMS40LTEwLjEtOS42LTEwLjEtOS42czMuOC00LjIsMTAuMS00LjlWMTNsMCwwYy0zLjMtMC40LTUuOSwyLjctNS45LDIuNwoJUzE1LjMsMjAuOSwxOS44LDIyLjRMMTkuOCwyMi40eiIvPgo8cGF0aCBjbGFzcz0ic3QwIiBkPSJNMTkuOCwwLjF2NC42YzAuMywwLDAuNiwwLDAuOS0wLjFjMTEuOC0wLjQsMTkuNCw5LjYsMTkuNCw5LjZTMzEuMywyNSwyMi4xLDI1Yy0wLjgsMC0xLjYtMC4xLTIuNC0wLjJ2Mi45CgljMC42LDAuMSwxLjMsMC4xLDIsMC4xYzguNSwwLDE0LjctNC40LDIwLjctOS41YzEsMC44LDUsMi43LDUuOSwzLjZjLTUuNyw0LjgtMTguOSw4LjYtMjYuNCw4LjZjLTAuNywwLTEuNCwwLTIuMS0wLjF2NGgzMi40VjAuMQoJSDE5Ljh6Ii8+CjxwYXRoIGNsYXNzPSJzdDAiIGQ9Ik0xOS44LDEwLjNWNy4yYzAuMywwLDAuNiwwLDAuOSwwYzguNS0wLjMsMTQsNy4zLDE0LDcuM3MtNiw4LjMtMTIuNCw4LjNjLTAuOSwwLTEuOC0wLjEtMi41LTAuNFYxMwoJYzMuMywwLjQsNCwxLjksNS45LDUuMWw0LjQtMy43YzAsMC0zLjItNC4yLTguNi00LjJDMjAuOSwxMC4yLDIwLjMsMTAuMywxOS44LDEwLjNMMTkuOCwxMC4zeiIvPgo8Zz4KCTxwYXRoIGQ9Ik0xMiw1My4zSDkuMmwtNS41LTguOXY4LjlIMVYzOS43aDIuOGw1LjUsOXYtOUgxMlY1My4zeiIvPgoJPHBhdGggZD0iTTIyLjMsNTAuNWgtNC45bC0wLjksMi44aC0zbDUuMS0xMy42aDIuNmw1LjEsMTMuNmgtM0wyMi4zLDUwLjV6IE0xOC4yLDQ4LjJoMy40bC0xLjctNS4xTDE4LjIsNDguMnoiLz4KCTxwYXRoIGQ9Ik0zOC42LDUzLjNoLTIuOGwtNS41LTguOXY4LjloLTIuOFYzOS43aDIuOGw1LjUsOXYtOWgyLjhWNTMuM3oiLz4KCTxwYXRoIGQ9Ik01Mi4yLDQ2LjhjMCwxLTAuMSwyLTAuNCwyLjhjLTAuMywwLjgtMC43LDEuNS0xLjIsMi4xYy0wLjUsMC42LTEuMSwxLTEuOCwxLjNjLTAuNywwLjMtMS41LDAuNS0yLjMsMC41CgkJYy0wLjksMC0xLjYtMC4yLTIuMy0wLjVjLTAuNy0wLjMtMS4zLTAuNy0xLjgtMS4zYy0wLjUtMC42LTAuOS0xLjMtMS4yLTIuMXMtMC40LTEuOC0wLjQtMi44di0wLjZjMC0xLDAuMS0yLDAuNC0yLjgKCQlzMC43LTEuNSwxLjItMi4xYzAuNS0wLjYsMS4xLTEsMS44LTEuM2MwLjctMC4zLDEuNS0wLjUsMi4zLTAuNWMwLjksMCwxLjYsMC4yLDIuMywwLjVjMC43LDAuMywxLjMsMC44LDEuOCwxLjMKCQljMC41LDAuNiwwLjksMS4zLDEuMiwyLjFjMC4zLDAuOCwwLjQsMS44LDAuNCwyLjhWNDYuOHogTTQ5LjQsNDYuMmMwLTEuNC0wLjMtMi41LTAuOC0zLjJjLTAuNS0wLjctMS4yLTEuMS0yLjItMS4xCgkJcy0xLjcsMC40LTIuMiwxLjFjLTAuNSwwLjctMC44LDEuOC0wLjgsMy4ydjAuNmMwLDAuNywwLjEsMS4zLDAuMiwxLjljMC4xLDAuNSwwLjMsMSwwLjYsMS40YzAuMywwLjQsMC42LDAuNywwLjksMC44CgkJYzAuNCwwLjIsMC44LDAuMywxLjMsMC4zYzAuOSwwLDEuNy0wLjQsMi4yLTEuMWMwLjUtMC43LDAuOC0xLjgsMC44LTMuM1Y0Ni4yeiIvPgo8L2c+Cjwvc3ZnPgo=', + name: 'logo', + }, + }, + aliases: ['jetson-nano'], + version: '1', + partials: { + bootDevice: ['Connect power to the Nvidia Jetson Nano SD-CARD'], + }, + }, + logo: 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPCEtLSBHZW5lcmF0b3I6IEFkb2JlIElsbHVzdHJhdG9yIDIzLjAuMiwgU1ZHIEV4cG9ydCBQbHVnLUluIC4gU1ZHIFZlcnNpb246IDYuMDAgQnVpbGQgMCkgIC0tPgo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkxheWVyXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IgoJIHZpZXdCb3g9IjAgMCA1MyA1NiIgc3R5bGU9ImVuYWJsZS1iYWNrZ3JvdW5kOm5ldyAwIDAgNTMgNTY7IiB4bWw6c3BhY2U9InByZXNlcnZlIj4KPHN0eWxlIHR5cGU9InRleHQvY3NzIj4KCS5zdDB7ZmlsbDojNzdCOTAwO30KPC9zdHlsZT4KPHRpdGxlPk52aWRpYV9sb2dvPC90aXRsZT4KPGRlc2M+Q3JlYXRlZCB3aXRoIFNrZXRjaC48L2Rlc2M+CjxwYXRoIGNsYXNzPSJzdDAiIGQ9Ik01LjgsMTQuOGMwLDAsNC43LTYuOSwxNC03LjZWNC43QzkuNCw1LjYsMC41LDE0LjMsMC41LDE0LjNzNS4xLDE0LjcsMTkuMywxNnYtMi43QzkuMywyNi4zLDUuOCwxNC44LDUuOCwxNC44Cgl6Ii8+CjxwYXRoIGNsYXNzPSJzdDAiIGQ9Ik0xOS44LDIyLjR2Mi40Yy03LjktMS40LTEwLjEtOS42LTEwLjEtOS42czMuOC00LjIsMTAuMS00LjlWMTNsMCwwYy0zLjMtMC40LTUuOSwyLjctNS45LDIuNwoJUzE1LjMsMjAuOSwxOS44LDIyLjRMMTkuOCwyMi40eiIvPgo8cGF0aCBjbGFzcz0ic3QwIiBkPSJNMTkuOCwwLjF2NC42YzAuMywwLDAuNiwwLDAuOS0wLjFjMTEuOC0wLjQsMTkuNCw5LjYsMTkuNCw5LjZTMzEuMywyNSwyMi4xLDI1Yy0wLjgsMC0xLjYtMC4xLTIuNC0wLjJ2Mi45CgljMC42LDAuMSwxLjMsMC4xLDIsMC4xYzguNSwwLDE0LjctNC40LDIwLjctOS41YzEsMC44LDUsMi43LDUuOSwzLjZjLTUuNyw0LjgtMTguOSw4LjYtMjYuNCw4LjZjLTAuNywwLTEuNCwwLTIuMS0wLjF2NGgzMi40VjAuMQoJSDE5Ljh6Ii8+CjxwYXRoIGNsYXNzPSJzdDAiIGQ9Ik0xOS44LDEwLjNWNy4yYzAuMywwLDAuNiwwLDAuOSwwYzguNS0wLjMsMTQsNy4zLDE0LDcuM3MtNiw4LjMtMTIuNCw4LjNjLTAuOSwwLTEuOC0wLjEtMi41LTAuNFYxMwoJYzMuMywwLjQsNCwxLjksNS45LDUuMWw0LjQtMy43YzAsMC0zLjItNC4yLTguNi00LjJDMjAuOSwxMC4yLDIwLjMsMTAuMywxOS44LDEwLjNMMTkuOCwxMC4zeiIvPgo8Zz4KCTxwYXRoIGQ9Ik0xMiw1My4zSDkuMmwtNS41LTguOXY4LjlIMVYzOS43aDIuOGw1LjUsOXYtOUgxMlY1My4zeiIvPgoJPHBhdGggZD0iTTIyLjMsNTAuNWgtNC45bC0wLjksMi44aC0zbDUuMS0xMy42aDIuNmw1LjEsMTMuNmgtM0wyMi4zLDUwLjV6IE0xOC4yLDQ4LjJoMy40bC0xLjctNS4xTDE4LjIsNDguMnoiLz4KCTxwYXRoIGQ9Ik0zOC42LDUzLjNoLTIuOGwtNS41LTguOXY4LjloLTIuOFYzOS43aDIuOGw1LjUsOXYtOWgyLjhWNTMuM3oiLz4KCTxwYXRoIGQ9Ik01Mi4yLDQ2LjhjMCwxLTAuMSwyLTAuNCwyLjhjLTAuMywwLjgtMC43LDEuNS0xLjIsMi4xYy0wLjUsMC42LTEuMSwxLTEuOCwxLjNjLTAuNywwLjMtMS41LDAuNS0yLjMsMC41CgkJYy0wLjksMC0xLjYtMC4yLTIuMy0wLjVjLTAuNy0wLjMtMS4zLTAuNy0xLjgtMS4zYy0wLjUtMC42LTAuOS0xLjMtMS4yLTIuMXMtMC40LTEuOC0wLjQtMi44di0wLjZjMC0xLDAuMS0yLDAuNC0yLjgKCQlzMC43LTEuNSwxLjItMi4xYzAuNS0wLjYsMS4xLTEsMS44LTEuM2MwLjctMC4zLDEuNS0wLjUsMi4zLTAuNWMwLjksMCwxLjYsMC4yLDIuMywwLjVjMC43LDAuMywxLjMsMC44LDEuOCwxLjMKCQljMC41LDAuNiwwLjksMS4zLDEuMiwyLjFjMC4zLDAuOCwwLjQsMS44LDAuNCwyLjhWNDYuOHogTTQ5LjQsNDYuMmMwLTEuNC0wLjMtMi41LTAuOC0zLjJjLTAuNS0wLjctMS4yLTEuMS0yLjItMS4xCgkJcy0xLjcsMC40LTIuMiwxLjFjLTAuNSwwLjctMC44LDEuOC0wLjgsMy4ydjAuNmMwLDAuNywwLjEsMS4zLDAuMiwxLjljMC4xLDAuNSwwLjMsMSwwLjYsMS40YzAuMywwLjQsMC42LDAuNywwLjksMC44CgkJYzAuNCwwLjIsMC44LDAuMywxLjMsMC4zYzAuOSwwLDEuNy0wLjQsMi4yLTEuMWMwLjUtMC43LDAuOC0xLjgsMC44LTMuM1Y0Ni4yeiIvPgo8L2c+Cjwvc3ZnPgo=', + }, + { + instructions: [ + 'Put the device in recovery mode and connect to the host computer via USB', + 'Unzip the balenaOS image and use the Jetson Flash tool to flash the Nvidia Jetson Xavier NX Devkit eMMC.', + 'Wait for writing of balenaOS to complete.', + 'Connect power to the Nvidia Jetson Xavier NX Devkit eMMC to boot the device.', + ], + options: [ + { + isGroup: true, + name: 'network', + message: 'Network', + options: [ + { + message: 'Network Connection', + name: 'network', + type: 'list', + choices: ['ethernet', 'wifi'], + }, + { + message: 'Wifi SSID', + name: 'wifiSsid', + type: 'text', + when: { + network: 'wifi', + }, + }, + { + message: 'Wifi Passphrase', + name: 'wifiKey', + type: 'password', + when: { + network: 'wifi', + }, + }, + ], + }, + { + isGroup: true, + isCollapsible: true, + collapsed: true, + name: 'advanced', + message: 'Advanced', + options: [ + { + message: 'Check for updates every X minutes', + name: 'appUpdatePollInterval', + type: 'number', + min: 10, + default: 10, + }, + ], + }, + ], + yocto: { + machine: 'jetson-xavier-nx-devkit-emmc', + image: 'balena-image', + fstype: 'balenaos-img', + version: 'yocto-honister', + deployArtifact: 'balena-image-jetson-xavier-nx-devkit-emmc.balenaos-img', + compressed: true, + }, + is_default_for__application: [ + { + application_tag: [], + should_be_running__release: [ + { + release_tag: [ + { + tag_key: 'version', + value: '2.107.10', + }, + ], + }, + ], + is_archived: false, + }, + ], + is_of__cpu_architecture: [ + { + slug: 'aarch64', + }, + ], + slug: 'jetson-xavier-nx-devkit-emmc', + name: 'Nvidia Jetson Xavier NX Devkit eMMC', + contract: { + data: { + led: false, + arch: 'aarch64', + hdmi: true, + media: { + defaultBoot: 'internal', + }, + storage: { + internal: true, + }, + is_private: false, + connectivity: { + wifi: false, + bluetooth: false, + }, + flashProtocol: 'jetsonFlash', + }, + name: 'Nvidia Jetson Xavier NX Devkit eMMC', + slug: 'jetson-xavier-nx-devkit-emmc', + type: 'hw.device-type', + assets: { + logo: { + url: 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPCEtLSBHZW5lcmF0b3I6IEFkb2JlIElsbHVzdHJhdG9yIDIzLjAuMSwgU1ZHIEV4cG9ydCBQbHVnLUluIC4gU1ZHIFZlcnNpb246IDYuMDAgQnVpbGQgMCkgIC0tPgo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkxheWVyXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IgoJIHZpZXdCb3g9IjAgMCA1MyA1NiIgc3R5bGU9ImVuYWJsZS1iYWNrZ3JvdW5kOm5ldyAwIDAgNTMgNTY7IiB4bWw6c3BhY2U9InByZXNlcnZlIj4KPHN0eWxlIHR5cGU9InRleHQvY3NzIj4KCS5zdDB7ZmlsbDojNzdCOTAwO30KPC9zdHlsZT4KPHRpdGxlPk52aWRpYV9sb2dvPC90aXRsZT4KPGRlc2M+Q3JlYXRlZCB3aXRoIFNrZXRjaC48L2Rlc2M+CjxwYXRoIGNsYXNzPSJzdDAiIGQ9Ik01LjgsMTQuOGMwLDAsNC43LTYuOSwxNC03LjZWNC43QzkuNCw1LjYsMC41LDE0LjMsMC41LDE0LjNzNS4xLDE0LjcsMTkuMywxNnYtMi43QzkuMywyNi4zLDUuOCwxNC44LDUuOCwxNC44Cgl6Ii8+CjxwYXRoIGNsYXNzPSJzdDAiIGQ9Ik0xOS44LDIyLjR2Mi40Yy03LjktMS40LTEwLjEtOS42LTEwLjEtOS42czMuOC00LjIsMTAuMS00LjlWMTNsMCwwYy0zLjMtMC40LTUuOSwyLjctNS45LDIuNwoJUzE1LjMsMjAuOSwxOS44LDIyLjRMMTkuOCwyMi40eiIvPgo8cGF0aCBjbGFzcz0ic3QwIiBkPSJNMTkuOCwwLjF2NC42YzAuMywwLDAuNiwwLDAuOS0wLjFjMTEuOC0wLjQsMTkuNCw5LjYsMTkuNCw5LjZTMzEuMywyNSwyMi4xLDI1Yy0wLjgsMC0xLjYtMC4xLTIuNC0wLjJ2Mi45CgljMC42LDAuMSwxLjMsMC4xLDIsMC4xYzguNSwwLDE0LjctNC40LDIwLjctOS41YzEsMC44LDUsMi43LDUuOSwzLjZjLTUuNyw0LjgtMTguOSw4LjYtMjYuNCw4LjZjLTAuNywwLTEuNCwwLTIuMS0wLjF2NGgzMi40VjAuMQoJSDE5Ljh6Ii8+CjxwYXRoIGNsYXNzPSJzdDAiIGQ9Ik0xOS44LDEwLjNWNy4yYzAuMywwLDAuNiwwLDAuOSwwYzguNS0wLjMsMTQsNy4zLDE0LDcuM3MtNiw4LjMtMTIuNCw4LjNjLTAuOSwwLTEuOC0wLjEtMi41LTAuNFYxMwoJYzMuMywwLjQsNCwxLjksNS45LDUuMWw0LjQtMy43YzAsMC0zLjItNC4yLTguNi00LjJDMjAuOSwxMC4yLDIwLjMsMTAuMywxOS44LDEwLjNMMTkuOCwxMC4zeiIvPgo8Zz4KCTxwYXRoIGQ9Ik01LjMsNDQuN2wyLjMtNGgyLjJsLTMuMyw1LjRsMy40LDUuNUg3LjdsLTIuNC00bC0yLjQsNEgwLjhsMy40LTUuNWwtMy4zLTUuNEgzTDUuMyw0NC43eiIvPgoJPHBhdGggZD0iTTE3LjQsNDkuMWgtNC4ybC0wLjksMi41aC0ybDQuMS0xMC45aDEuN2w0LjEsMTAuOWgtMkwxNy40LDQ5LjF6IE0xMy43LDQ3LjZoMy4yTDE1LjMsNDNMMTMuNyw0Ny42eiIvPgoJPHBhdGggZD0iTTI0LjgsNDkuM2wyLjgtOC42aDIuMWwtMy45LDEwLjloLTEuOEwyMCw0MC43aDIuMUwyNC44LDQ5LjN6Ii8+Cgk8cGF0aCBkPSJNMzIuOSw1MS42SDMxVjQwLjdoMS45VjUxLjZ6Ii8+Cgk8cGF0aCBkPSJNNDEuNiw0Ni44aC00LjV2My40aDUuMnYxLjVoLTcuMVY0MC43aDcuMXYxLjVoLTUuMnYzaDQuNVY0Ni44eiIvPgoJPHBhdGggZD0iTTQ4LDQ3LjRoLTIuMXY0LjJINDRWNDAuN2gzLjhjMC42LDAsMS4yLDAuMSwxLjcsMC4yYzAuNSwwLjEsMC45LDAuNCwxLjIsMC42YzAuMywwLjMsMC42LDAuNiwwLjgsMQoJCWMwLjIsMC40LDAuMywwLjksMC4zLDEuNGMwLDAuNy0wLjIsMS4zLTAuNSwxLjhjLTAuNCwwLjUtMC44LDAuOS0xLjUsMS4xbDIuNSw0LjZ2MC4xaC0yTDQ4LDQ3LjR6IE00NS45LDQ1LjloMgoJCWMwLjMsMCwwLjYsMCwwLjktMC4xYzAuMy0wLjEsMC41LTAuMiwwLjYtMC40YzAuMi0wLjIsMC4zLTAuNCwwLjQtMC42YzAuMS0wLjIsMC4xLTAuNSwwLjEtMC43YzAtMC4zLDAtMC41LTAuMS0wLjgKCQljLTAuMS0wLjItMC4yLTAuNC0wLjQtMC42Yy0wLjItMC4yLTAuNC0wLjMtMC42LTAuNGMtMC4zLTAuMS0wLjYtMC4xLTAuOS0wLjFoLTEuOVY0NS45eiIvPgo8L2c+Cjwvc3ZnPg==', + name: 'logo', + }, + }, + aliases: ['jetson-xavier-nx-devkit-emmc'], + version: '1', + partials: { + bootDevice: [ + 'Connect power to the Nvidia Jetson Xavier NX Devkit eMMC', + ], + }, + }, + logo: 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPCEtLSBHZW5lcmF0b3I6IEFkb2JlIElsbHVzdHJhdG9yIDIzLjAuMSwgU1ZHIEV4cG9ydCBQbHVnLUluIC4gU1ZHIFZlcnNpb246IDYuMDAgQnVpbGQgMCkgIC0tPgo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkxheWVyXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IgoJIHZpZXdCb3g9IjAgMCA1MyA1NiIgc3R5bGU9ImVuYWJsZS1iYWNrZ3JvdW5kOm5ldyAwIDAgNTMgNTY7IiB4bWw6c3BhY2U9InByZXNlcnZlIj4KPHN0eWxlIHR5cGU9InRleHQvY3NzIj4KCS5zdDB7ZmlsbDojNzdCOTAwO30KPC9zdHlsZT4KPHRpdGxlPk52aWRpYV9sb2dvPC90aXRsZT4KPGRlc2M+Q3JlYXRlZCB3aXRoIFNrZXRjaC48L2Rlc2M+CjxwYXRoIGNsYXNzPSJzdDAiIGQ9Ik01LjgsMTQuOGMwLDAsNC43LTYuOSwxNC03LjZWNC43QzkuNCw1LjYsMC41LDE0LjMsMC41LDE0LjNzNS4xLDE0LjcsMTkuMywxNnYtMi43QzkuMywyNi4zLDUuOCwxNC44LDUuOCwxNC44Cgl6Ii8+CjxwYXRoIGNsYXNzPSJzdDAiIGQ9Ik0xOS44LDIyLjR2Mi40Yy03LjktMS40LTEwLjEtOS42LTEwLjEtOS42czMuOC00LjIsMTAuMS00LjlWMTNsMCwwYy0zLjMtMC40LTUuOSwyLjctNS45LDIuNwoJUzE1LjMsMjAuOSwxOS44LDIyLjRMMTkuOCwyMi40eiIvPgo8cGF0aCBjbGFzcz0ic3QwIiBkPSJNMTkuOCwwLjF2NC42YzAuMywwLDAuNiwwLDAuOS0wLjFjMTEuOC0wLjQsMTkuNCw5LjYsMTkuNCw5LjZTMzEuMywyNSwyMi4xLDI1Yy0wLjgsMC0xLjYtMC4xLTIuNC0wLjJ2Mi45CgljMC42LDAuMSwxLjMsMC4xLDIsMC4xYzguNSwwLDE0LjctNC40LDIwLjctOS41YzEsMC44LDUsMi43LDUuOSwzLjZjLTUuNyw0LjgtMTguOSw4LjYtMjYuNCw4LjZjLTAuNywwLTEuNCwwLTIuMS0wLjF2NGgzMi40VjAuMQoJSDE5Ljh6Ii8+CjxwYXRoIGNsYXNzPSJzdDAiIGQ9Ik0xOS44LDEwLjNWNy4yYzAuMywwLDAuNiwwLDAuOSwwYzguNS0wLjMsMTQsNy4zLDE0LDcuM3MtNiw4LjMtMTIuNCw4LjNjLTAuOSwwLTEuOC0wLjEtMi41LTAuNFYxMwoJYzMuMywwLjQsNCwxLjksNS45LDUuMWw0LjQtMy43YzAsMC0zLjItNC4yLTguNi00LjJDMjAuOSwxMC4yLDIwLjMsMTAuMywxOS44LDEwLjNMMTkuOCwxMC4zeiIvPgo8Zz4KCTxwYXRoIGQ9Ik01LjMsNDQuN2wyLjMtNGgyLjJsLTMuMyw1LjRsMy40LDUuNUg3LjdsLTIuNC00bC0yLjQsNEgwLjhsMy40LTUuNWwtMy4zLTUuNEgzTDUuMyw0NC43eiIvPgoJPHBhdGggZD0iTTE3LjQsNDkuMWgtNC4ybC0wLjksMi41aC0ybDQuMS0xMC45aDEuN2w0LjEsMTAuOWgtMkwxNy40LDQ5LjF6IE0xMy43LDQ3LjZoMy4yTDE1LjMsNDNMMTMuNyw0Ny42eiIvPgoJPHBhdGggZD0iTTI0LjgsNDkuM2wyLjgtOC42aDIuMWwtMy45LDEwLjloLTEuOEwyMCw0MC43aDIuMUwyNC44LDQ5LjN6Ii8+Cgk8cGF0aCBkPSJNMzIuOSw1MS42SDMxVjQwLjdoMS45VjUxLjZ6Ii8+Cgk8cGF0aCBkPSJNNDEuNiw0Ni44aC00LjV2My40aDUuMnYxLjVoLTcuMVY0MC43aDcuMXYxLjVoLTUuMnYzaDQuNVY0Ni44eiIvPgoJPHBhdGggZD0iTTQ4LDQ3LjRoLTIuMXY0LjJINDRWNDAuN2gzLjhjMC42LDAsMS4yLDAuMSwxLjcsMC4yYzAuNSwwLjEsMC45LDAuNCwxLjIsMC42YzAuMywwLjMsMC42LDAuNiwwLjgsMQoJCWMwLjIsMC40LDAuMywwLjksMC4zLDEuNGMwLDAuNy0wLjIsMS4zLTAuNSwxLjhjLTAuNCwwLjUtMC44LDAuOS0xLjUsMS4xbDIuNSw0LjZ2MC4xaC0yTDQ4LDQ3LjR6IE00NS45LDQ1LjloMgoJCWMwLjMsMCwwLjYsMCwwLjktMC4xYzAuMy0wLjEsMC41LTAuMiwwLjYtMC40YzAuMi0wLjIsMC4zLTAuNCwwLjQtMC42YzAuMS0wLjIsMC4xLTAuNSwwLjEtMC43YzAtMC4zLDAtMC41LTAuMS0wLjgKCQljLTAuMS0wLjItMC4yLTAuNC0wLjQtMC42Yy0wLjItMC4yLTAuNC0wLjMtMC42LTAuNGMtMC4zLTAuMS0wLjYtMC4xLTAuOS0wLjFoLTEuOVY0NS45eiIvPgo8L2c+Cjwvc3ZnPg==', + }, +]; + +const initialDeviceType: any = { + instructions: [ + 'Insert the SD card to the host machine.', + 'Write the balenaOS file you downloaded to the SD card. We recommend using Etcher.', + 'Wait for writing of balenaOS to complete.', + 'Remove the SD card from the host machine.', + 'Insert the freshly flashed SD card into the Raspberry Pi 4 (using 64bit OS).', + 'Connect power to the Raspberry Pi 4 (using 64bit OS) to boot the device.', + ], + options: [ + { + isGroup: true, + name: 'network', + message: 'Network', + options: [ + { + message: 'Network Connection', + name: 'network', + type: 'list', + choices: ['ethernet', 'wifi'], + }, + { + message: 'Wifi SSID', + name: 'wifiSsid', + type: 'text', + when: { + network: 'wifi', + }, + }, + { + message: 'Wifi Passphrase', + name: 'wifiKey', + type: 'password', + when: { + network: 'wifi', + }, + }, + ], + }, + { + isGroup: true, + isCollapsible: true, + collapsed: true, + name: 'advanced', + message: 'Advanced', + options: [ + { + message: 'Check for updates every X minutes', + name: 'appUpdatePollInterval', + type: 'number', + min: 10, + default: 10, + }, + ], + }, + ], + yocto: { + machine: 'raspberrypi4-64', + image: 'balena-image', + fstype: 'balenaos-img', + version: 'yocto-honister', + deployArtifact: 'balena-image-raspberrypi4-64.balenaos-img', + compressed: true, + }, + is_default_for__application: [ + { + application_tag: [ + { + value: 'esr', + }, + ], + should_be_running__release: [ + { + release_tag: [ + { + tag_key: 'meta-balena-base', + value: 'v2.114.25', + }, + { + tag_key: 'version', + value: '2023.7.0', + }, + ], + }, + ], + is_archived: false, + }, + { + application_tag: [], + should_be_running__release: [ + { + release_tag: [ + { + tag_key: 'version', + value: '3.2.7+rev1', + }, + ], + }, + ], + is_archived: false, + }, + ], + is_of__cpu_architecture: [ + { + slug: 'aarch64', + }, + ], + slug: 'raspberrypi4-64', + name: 'Raspberry Pi 4 (using 64bit OS)', + contract: { + data: { + led: true, + arch: 'aarch64', + hdmi: true, + media: { + altBoot: ['usb_mass_storage', 'network'], + defaultBoot: 'sdcard', + }, + storage: { + internal: false, + }, + is_private: false, + connectivity: { + wifi: true, + bluetooth: true, + }, + }, + name: 'Raspberry Pi 4 (using 64bit OS)', + slug: 'raspberrypi4-64', + type: 'hw.device-type', + assets: { + logo: { + url: 'data:image/svg+xml;base64,PHN2ZyBpZD0iTGF5ZXJfMSIgZGF0YS1uYW1lPSJMYXllciAxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA0MCA0MCI+PGRlZnM+PHN0eWxlPi5jbHMtMXtmaWxsOiM3NWE5Mjg7fS5jbHMtMntmaWxsOiNiYzExNDI7fS5jbHMtM3tmaWxsOiNmZmY7fS5jbHMtNHtpc29sYXRpb246aXNvbGF0ZTt9LmNscy01e2ZpbGw6I2Q4MjI0Yzt9PC9zdHlsZT48L2RlZnM+PHBhdGggZD0iTTEyLC41YTEuMTEsMS4xMSwwLDAsMC0uNjUuMjdBMS40OCwxLjQ4LDAsMCwwLDkuNjcuOTNjLS43OS0uMTEtMSwuMTEtMS4yNC4zNS0uMTcsMC0xLjMtLjE4LTEuODEuNTktMS4zLS4xNS0xLjcxLjc3LTEuMjQsMS42MmExLjExLDEuMTEsMCwwLDAsLjA4LDEuNTljLS4yMi40NC0uMDkuOTEuNDMsMS40OEExLjI0LDEuMjQsMCwwLDAsNi41LDcuOTRjLS4wOS44NC43NywxLjMzLDEsMS41LjEuNDkuMzEsMSwxLjI5LDEuMi4xNi43My43NS44NSwxLjMyLDFhNi4xNSw2LjE1LDAsMCwwLTMuNDksNi4wNmwtLjI4LjVhNiw2LDAsMCwwLTEuMDYsOSwxNi4yOSwxNi4yOSwwLDAsMCwuODMsMi43LDYuNTYsNi41NiwwLDAsMCw0LjA5LDUuMjQsMTQsMTQsMCwwLDAsMy45MiwyLjIzQTYuNTQsNi41NCwwLDAsMCwxOSwzOS41SDE5YTYuNTQsNi41NCwwLDAsMCw0LjgyLTIuMTYsMTQsMTQsMCwwLDAsMy45Mi0yLjIzLDYuNTYsNi41NiwwLDAsMCw0LjA5LTUuMjQsMTYuMjksMTYuMjksMCwwLDAsLjgzLTIuNyw2LDYsMCwwLDAtMS4wNi05bC0uMjgtLjVhNi4xNSw2LjE1LDAsMCwwLTMuNDktNi4wNmMuNTctLjE2LDEuMTYtLjI4LDEuMzItMSwxLS4yNSwxLjE5LS43MSwxLjI5LTEuMi4yNS0uMTcsMS4xMS0uNjYsMS0xLjVhMS4yNCwxLjI0LDAsMCwwLC42MS0xLjM4Yy41Mi0uNTcuNjUtMSwuNDMtMS40OGExLjExLDEuMTEsMCwwLDAsLjA4LTEuNTljLjQ3LS44NS4wNi0xLjc3LTEuMjQtMS42Mi0uNTEtLjc3LTEuNjQtLjU5LTEuODEtLjU5LS4yLS4yNC0uNDUtLjQ2LTEuMjQtLjM1QTEuNDgsMS40OCwwLDAsMCwyNi42NS43N0MyNiwuMjIsMjUuNDkuNjYsMjUsLjgzYy0uODUtLjI4LTEsLjEtMS40Ni4yNS0uOTMtLjE5LTEuMjEuMjMtMS42NS42OGgtLjUyQTUuOSw1LjksMCwwLDAsMTksNS4xMWE2LDYsMCwwLDAtMi4zMy0zLjM2aC0uNTJjLS40NC0uNDUtLjcyLS44Ny0xLjY1LS42OEMxNC4wOC45MywxMy44OS41NSwxMywuODNBMy4zMiwzLjMyLDAsMCwwLDEyLC41WiIvPjxwYXRoIGNsYXNzPSJjbHMtMSIgZD0iTTkuMjIsNC4xMmMzLjcsMS45MSw1Ljg1LDMuNDUsNyw0Ljc3LS42LDIuNDItMy43NSwyLjUzLTQuOSwyLjQ2YS44OC44OCwwLDAsMCwuNS0uNDRjLS4yOS0uMjEtMS4zMSwwLTItLjQzYS42Ni42NiwwLDAsMCwuNTMtLjMxLDUuOTMsNS45MywwLDAsMS0xLjgzLS43NiwxLjA1LDEuMDUsMCwwLDAsLjc1LS4xNkE3LjI0LDcuMjQsMCwwLDEsNy41MSw4LjE3Yy4zMiwwLC42NiwwLC43NS0uMTJBNy4wOSw3LjA5LDAsMCwxLDYuODUsNi45MWExLjA4LDEuMDgsMCwwLDAsLjczLS4wNyw1LjUzLDUuNTMsMCwwLDEtMS4yLTEuMzIuOTEuOTEsMCwwLDAsLjg0LDBjLS4xNC0uMzItLjc1LS41MS0xLjEtMS4yNi4zNCwwLC43LjA3Ljc3LDBhMy42MSwzLjYxLDAsMCwwLS43LTEuMzlBMTIuNjYsMTIuNjYsMCwwLDAsOCwyLjhsLS40Ni0uNDZhMy41NiwzLjU2LDAsMCwxLDIsLjE5Yy4yNC0uMTksMC0uNDItLjI5LS42N2E2LjY0LDYuNjQsMCwwLDEsMS42NS40MmMuMjctLjI0LS4xNy0uNDgtLjM4LS43MmE0LjIxLDQuMjEsMCwwLDEsMS43My42OGMuMjktLjI4LDAtLjUxLS4xNy0uNzVhNC4xNyw0LjE3LDAsMCwxLDEuNDUuOTNjLjEzLS4xNy4zNC0uMy4wOS0uNzJhMy42OCwzLjY4LDAsMCwxLDEuMTcsMWMuMzEtLjIuMTgtLjQ3LjE4LS43MkExMSwxMSwwLDAsMSwxNi4yLDMuMzFjLjA5LS4wNi4xNi0uMjYuMjItLjU4LDEuMjUsMS4yMSwzLDQuMjYuNDUsNS40N0EyMy44OCwyMy44OCwwLDAsMCw5LjIyLDQuMTJaIi8+PHBhdGggY2xhc3M9ImNscy0xIiBkPSJNMjguODYsNC4xMkMyNS4xNiw2LDIzLDcuNTcsMjEuODMsOC44OWMuNiwyLjQyLDMuNzUsMi41Myw0LjkxLDIuNDZhLjg3Ljg3LDAsMCwxLS41MS0uNDRjLjI5LS4yMSwxLjMyLDAsMi0uNDNhLjY2LjY2LDAsMCwxLS41My0uMzEsNS43Myw1LjczLDAsMCwwLDEuODMtLjc2LDEsMSwwLDAsMS0uNzQtLjE2LDcuMTksNy4xOSwwLDAsMCwxLjc1LTEuMDhjLS4zMSwwLS42NSwwLS43NS0uMTJhNy4wOSw3LjA5LDAsMCwwLDEuNDEtMS4xNCwxLjEsMS4xLDAsMCwxLS43My0uMDcsNS41Myw1LjUzLDAsMCwwLDEuMi0xLjMyLjkxLjkxLDAsMCwxLS44NCwwQzMxLDUuMTksMzEuNjIsNSwzMiw0LjI1YTEuOTEsMS45MSwwLDAsMS0uNzgsMCwzLjYxLDMuNjEsMCwwLDEsLjctMS4zOUExMi41OCwxMi41OCwwLDAsMSwzMC4xLDIuOGwuNDUtLjQ2YTMuNTYsMy41NiwwLDAsMC0yLC4xOWMtLjI0LS4xOSwwLS40Mi4yOS0uNjdhNi44OCw2Ljg4LDAsMCwwLTEuNjUuNDJjLS4yNy0uMjQuMTctLjQ4LjM4LS43MmE0LjI4LDQuMjgsMCwwLDAtMS43My42OGMtLjI5LS4yOCwwLS41MS4xOC0uNzVhNC4yMyw0LjIzLDAsMCwwLTEuNDYuOTNjLS4xMy0uMTctLjMzLS4zLS4wOS0uNzJhMy43NCwzLjc0LDAsMCwwLTEuMTYsMWMtLjMxLS4yLS4xOS0uNDctLjE5LS43MmExMi44NCwxMi44NCwwLDAsMC0xLjI2LDEuMzIsMS4xOCwxLjE4LDAsMCwxLS4yMi0uNThjLTEuMjQsMS4yMS0zLDQuMjYtLjQ1LDUuNDdhMjQsMjQsMCwwLDEsNy42NS00LjA4WiIvPjxwYXRoIGNsYXNzPSJjbHMtMiIgZD0iTTIzLjUzLDI4Ljc2YTQuMjgsNC4yOCwwLDAsMS00LjQ0LDQuMDksNC4yNyw0LjI3LDAsMCwxLTQuNDMtNC4wOSw0LjI3LDQuMjcsMCwwLDEsNC40My00LjA4QTQuMjcsNC4yNywwLDAsMSwyMy41MywyOC43NloiLz48cGF0aCBjbGFzcz0iY2xzLTIiIGQ9Ik0xNi41MiwxNy4wOGMxLjg0LDEuMjEsMi4xNywzLjkzLjc0LDYuMXMtNC4wNywyLjk0LTUuOTEsMS43M2gwYy0xLjg0LTEuMi0yLjE3LTMuOTMtLjc0LTYuMDlzNC4wOC0yLjk0LDUuOTEtMS43NFoiLz48cGF0aCBjbGFzcz0iY2xzLTIiIGQ9Ik0yMS40OCwxNi44NmMtMS44MywxLjIxLTIuMTYsMy45NC0uNzQsNi4xczQuMDgsMi45NCw1LjkyLDEuNzNoMGMxLjg0LTEuMiwyLjE3LTMuOTMuNzQtNi4wOXMtNC4wOC0yLjk0LTUuOTItMS43NFoiLz48cGF0aCBjbGFzcz0iY2xzLTIiIGQ9Ik03LjM0LDE5LjA1YzItLjUzLjY3LDguMjEtLjk0LDcuNDlBNC43Miw0LjcyLDAsMCwxLDcuMzQsMTkuMDVaIi8+PHBhdGggY2xhc3M9ImNscy0yIiBkPSJNMzAuMjcsMTguOTRjLTItLjUzLS42Nyw4LjIxLjk0LDcuNDlBNC43Miw0LjcyLDAsMCwwLDMwLjI3LDE4Ljk0WiIvPjxwYXRoIGNsYXNzPSJjbHMtMiIgZD0iTTIzLjUzLDEyLjQzYzMuNDItLjU3LDYuMjcsMS40Niw2LjE2LDUuMTctLjEyLDEuNDMtNy40Mi01LTYuMTYtNS4xN1oiLz48cGF0aCBjbGFzcz0iY2xzLTIiIGQ9Ik0xNC4wNywxMi4zMmMtMy40My0uNTctNi4yOCwxLjQ2LTYuMTYsNS4xN0M4LDE4LjkyLDE1LjMzLDEyLjU0LDE0LjA3LDEyLjMyWiIvPjxwYXRoIGNsYXNzPSJjbHMtMiIgZD0iTTE5LDExLjQ2Yy0yLS4wNS00LDEuNTItNCwyLjQzLDAsMS4xLDEuNjEsMi4yMyw0LDIuMjZzNC0uOSw0LTItMi4yMy0yLjY2LTQtMi42NFoiLz48cGF0aCBjbGFzcz0iY2xzLTIiIGQ9Ik0xOS4xMSwzNC4xNWMxLjc4LS4wOCw0LjE3LjU3LDQuMTgsMS40M3MtMi4xNywyLjc0LTQuMywyLjctNC4zNi0xLjgtNC4zMy0yLjQ2QzE0LjYyLDM0Ljg2LDE3LjM0LDM0LjEsMTkuMTEsMzQuMTVaIi8+PHBhdGggY2xhc3M9ImNscy0yIiBkPSJNMTIuNTMsMjljMS4yNywxLjUzLDEuODUsNC4yMi43OSw1LTEsLjYtMy40NC4zNS01LjE2LTIuMTMtMS4xNy0yLjA4LTEtNC4yLS4yLTQuODNDOS4xOCwyNi4zMywxMS4wNywyNy4zMywxMi41MywyOVoiLz48cGF0aCBjbGFzcz0iY2xzLTIiIGQ9Ik0yNS40NCwyOC41NEMyNC4wNiwzMC4xNSwyMy4zLDMzLjA4LDI0LjMsMzRjMSwuNzQsMy41My42Myw1LjQzLTIsMS4zOC0xLjc3LjkxLTQuNzIuMTMtNS41MUMyOC42OSwyNS42MSwyNywyNi43NywyNS40NCwyOC41NFoiLz48Y2lyY2xlIGNsYXNzPSJjbHMtMyIgY3g9IjI5LjQ0IiBjeT0iMjYuNDgiIHI9IjkuMTciLz48cGF0aCBkPSJNMjkuNDQsMThhOC41Myw4LjUzLDAsMSwxLTguNTIsOC41M0E4LjUzLDguNTMsMCwwLDEsMjkuNDQsMThtMC0xLjI4YTkuODEsOS44MSwwLDEsMCw5LjgxLDkuODEsOS44Miw5LjgyLDAsMCwwLTkuODEtOS44MVoiLz48ZyBjbGFzcz0iY2xzLTQiPjxwYXRoIGNsYXNzPSJjbHMtNSIgZD0iTTI5LjYzLDMxLjYyVjI5LjE4SDI1LjIyVjI3LjU2bDMuNzItNi4xM2gyLjg5djZIMzN2MS43OUgzMS44M3YyLjQ0Wm0tMi4xNS00LjIzaDIuMTVWMjUuNzNxMC0uNSwwLTEuMTRjMC0uNDQsMC0uODEuMDctMS4xNGgtLjA3Yy0uMTMuMjgtLjI2LjU3LS40Ljg1cy0uMjguNTgtLjQzLjg3WiIvPjwvZz48L3N2Zz4=', + name: 'logo', + }, + }, + aliases: ['raspberrypi4-64'], + version: '1', + partials: { + bootDevice: ['Connect power to the Raspberry Pi 4 (using 64bit OS)'], + }, + }, + logo: 'data:image/svg+xml;base64,PHN2ZyBpZD0iTGF5ZXJfMSIgZGF0YS1uYW1lPSJMYXllciAxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA0MCA0MCI+PGRlZnM+PHN0eWxlPi5jbHMtMXtmaWxsOiM3NWE5Mjg7fS5jbHMtMntmaWxsOiNiYzExNDI7fS5jbHMtM3tmaWxsOiNmZmY7fS5jbHMtNHtpc29sYXRpb246aXNvbGF0ZTt9LmNscy01e2ZpbGw6I2Q4MjI0Yzt9PC9zdHlsZT48L2RlZnM+PHBhdGggZD0iTTEyLC41YTEuMTEsMS4xMSwwLDAsMC0uNjUuMjdBMS40OCwxLjQ4LDAsMCwwLDkuNjcuOTNjLS43OS0uMTEtMSwuMTEtMS4yNC4zNS0uMTcsMC0xLjMtLjE4LTEuODEuNTktMS4zLS4xNS0xLjcxLjc3LTEuMjQsMS42MmExLjExLDEuMTEsMCwwLDAsLjA4LDEuNTljLS4yMi40NC0uMDkuOTEuNDMsMS40OEExLjI0LDEuMjQsMCwwLDAsNi41LDcuOTRjLS4wOS44NC43NywxLjMzLDEsMS41LjEuNDkuMzEsMSwxLjI5LDEuMi4xNi43My43NS44NSwxLjMyLDFhNi4xNSw2LjE1LDAsMCwwLTMuNDksNi4wNmwtLjI4LjVhNiw2LDAsMCwwLTEuMDYsOSwxNi4yOSwxNi4yOSwwLDAsMCwuODMsMi43LDYuNTYsNi41NiwwLDAsMCw0LjA5LDUuMjQsMTQsMTQsMCwwLDAsMy45MiwyLjIzQTYuNTQsNi41NCwwLDAsMCwxOSwzOS41SDE5YTYuNTQsNi41NCwwLDAsMCw0LjgyLTIuMTYsMTQsMTQsMCwwLDAsMy45Mi0yLjIzLDYuNTYsNi41NiwwLDAsMCw0LjA5LTUuMjQsMTYuMjksMTYuMjksMCwwLDAsLjgzLTIuNyw2LDYsMCwwLDAtMS4wNi05bC0uMjgtLjVhNi4xNSw2LjE1LDAsMCwwLTMuNDktNi4wNmMuNTctLjE2LDEuMTYtLjI4LDEuMzItMSwxLS4yNSwxLjE5LS43MSwxLjI5LTEuMi4yNS0uMTcsMS4xMS0uNjYsMS0xLjVhMS4yNCwxLjI0LDAsMCwwLC42MS0xLjM4Yy41Mi0uNTcuNjUtMSwuNDMtMS40OGExLjExLDEuMTEsMCwwLDAsLjA4LTEuNTljLjQ3LS44NS4wNi0xLjc3LTEuMjQtMS42Mi0uNTEtLjc3LTEuNjQtLjU5LTEuODEtLjU5LS4yLS4yNC0uNDUtLjQ2LTEuMjQtLjM1QTEuNDgsMS40OCwwLDAsMCwyNi42NS43N0MyNiwuMjIsMjUuNDkuNjYsMjUsLjgzYy0uODUtLjI4LTEsLjEtMS40Ni4yNS0uOTMtLjE5LTEuMjEuMjMtMS42NS42OGgtLjUyQTUuOSw1LjksMCwwLDAsMTksNS4xMWE2LDYsMCwwLDAtMi4zMy0zLjM2aC0uNTJjLS40NC0uNDUtLjcyLS44Ny0xLjY1LS42OEMxNC4wOC45MywxMy44OS41NSwxMywuODNBMy4zMiwzLjMyLDAsMCwwLDEyLC41WiIvPjxwYXRoIGNsYXNzPSJjbHMtMSIgZD0iTTkuMjIsNC4xMmMzLjcsMS45MSw1Ljg1LDMuNDUsNyw0Ljc3LS42LDIuNDItMy43NSwyLjUzLTQuOSwyLjQ2YS44OC44OCwwLDAsMCwuNS0uNDRjLS4yOS0uMjEtMS4zMSwwLTItLjQzYS42Ni42NiwwLDAsMCwuNTMtLjMxLDUuOTMsNS45MywwLDAsMS0xLjgzLS43NiwxLjA1LDEuMDUsMCwwLDAsLjc1LS4xNkE3LjI0LDcuMjQsMCwwLDEsNy41MSw4LjE3Yy4zMiwwLC42NiwwLC43NS0uMTJBNy4wOSw3LjA5LDAsMCwxLDYuODUsNi45MWExLjA4LDEuMDgsMCwwLDAsLjczLS4wNyw1LjUzLDUuNTMsMCwwLDEtMS4yLTEuMzIuOTEuOTEsMCwwLDAsLjg0LDBjLS4xNC0uMzItLjc1LS41MS0xLjEtMS4yNi4zNCwwLC43LjA3Ljc3LDBhMy42MSwzLjYxLDAsMCwwLS43LTEuMzlBMTIuNjYsMTIuNjYsMCwwLDAsOCwyLjhsLS40Ni0uNDZhMy41NiwzLjU2LDAsMCwxLDIsLjE5Yy4yNC0uMTksMC0uNDItLjI5LS42N2E2LjY0LDYuNjQsMCwwLDEsMS42NS40MmMuMjctLjI0LS4xNy0uNDgtLjM4LS43MmE0LjIxLDQuMjEsMCwwLDEsMS43My42OGMuMjktLjI4LDAtLjUxLS4xNy0uNzVhNC4xNyw0LjE3LDAsMCwxLDEuNDUuOTNjLjEzLS4xNy4zNC0uMy4wOS0uNzJhMy42OCwzLjY4LDAsMCwxLDEuMTcsMWMuMzEtLjIuMTgtLjQ3LjE4LS43MkExMSwxMSwwLDAsMSwxNi4yLDMuMzFjLjA5LS4wNi4xNi0uMjYuMjItLjU4LDEuMjUsMS4yMSwzLDQuMjYuNDUsNS40N0EyMy44OCwyMy44OCwwLDAsMCw5LjIyLDQuMTJaIi8+PHBhdGggY2xhc3M9ImNscy0xIiBkPSJNMjguODYsNC4xMkMyNS4xNiw2LDIzLDcuNTcsMjEuODMsOC44OWMuNiwyLjQyLDMuNzUsMi41Myw0LjkxLDIuNDZhLjg3Ljg3LDAsMCwxLS41MS0uNDRjLjI5LS4yMSwxLjMyLDAsMi0uNDNhLjY2LjY2LDAsMCwxLS41My0uMzEsNS43Myw1LjczLDAsMCwwLDEuODMtLjc2LDEsMSwwLDAsMS0uNzQtLjE2LDcuMTksNy4xOSwwLDAsMCwxLjc1LTEuMDhjLS4zMSwwLS42NSwwLS43NS0uMTJhNy4wOSw3LjA5LDAsMCwwLDEuNDEtMS4xNCwxLjEsMS4xLDAsMCwxLS43My0uMDcsNS41Myw1LjUzLDAsMCwwLDEuMi0xLjMyLjkxLjkxLDAsMCwxLS44NCwwQzMxLDUuMTksMzEuNjIsNSwzMiw0LjI1YTEuOTEsMS45MSwwLDAsMS0uNzgsMCwzLjYxLDMuNjEsMCwwLDEsLjctMS4zOUExMi41OCwxMi41OCwwLDAsMSwzMC4xLDIuOGwuNDUtLjQ2YTMuNTYsMy41NiwwLDAsMC0yLC4xOWMtLjI0LS4xOSwwLS40Mi4yOS0uNjdhNi44OCw2Ljg4LDAsMCwwLTEuNjUuNDJjLS4yNy0uMjQuMTctLjQ4LjM4LS43MmE0LjI4LDQuMjgsMCwwLDAtMS43My42OGMtLjI5LS4yOCwwLS41MS4xOC0uNzVhNC4yMyw0LjIzLDAsMCwwLTEuNDYuOTNjLS4xMy0uMTctLjMzLS4zLS4wOS0uNzJhMy43NCwzLjc0LDAsMCwwLTEuMTYsMWMtLjMxLS4yLS4xOS0uNDctLjE5LS43MmExMi44NCwxMi44NCwwLDAsMC0xLjI2LDEuMzIsMS4xOCwxLjE4LDAsMCwxLS4yMi0uNThjLTEuMjQsMS4yMS0zLDQuMjYtLjQ1LDUuNDdhMjQsMjQsMCwwLDEsNy42NS00LjA4WiIvPjxwYXRoIGNsYXNzPSJjbHMtMiIgZD0iTTIzLjUzLDI4Ljc2YTQuMjgsNC4yOCwwLDAsMS00LjQ0LDQuMDksNC4yNyw0LjI3LDAsMCwxLTQuNDMtNC4wOSw0LjI3LDQuMjcsMCwwLDEsNC40My00LjA4QTQuMjcsNC4yNywwLDAsMSwyMy41MywyOC43NloiLz48cGF0aCBjbGFzcz0iY2xzLTIiIGQ9Ik0xNi41MiwxNy4wOGMxLjg0LDEuMjEsMi4xNywzLjkzLjc0LDYuMXMtNC4wNywyLjk0LTUuOTEsMS43M2gwYy0xLjg0LTEuMi0yLjE3LTMuOTMtLjc0LTYuMDlzNC4wOC0yLjk0LDUuOTEtMS43NFoiLz48cGF0aCBjbGFzcz0iY2xzLTIiIGQ9Ik0yMS40OCwxNi44NmMtMS44MywxLjIxLTIuMTYsMy45NC0uNzQsNi4xczQuMDgsMi45NCw1LjkyLDEuNzNoMGMxLjg0LTEuMiwyLjE3LTMuOTMuNzQtNi4wOXMtNC4wOC0yLjk0LTUuOTItMS43NFoiLz48cGF0aCBjbGFzcz0iY2xzLTIiIGQ9Ik03LjM0LDE5LjA1YzItLjUzLjY3LDguMjEtLjk0LDcuNDlBNC43Miw0LjcyLDAsMCwxLDcuMzQsMTkuMDVaIi8+PHBhdGggY2xhc3M9ImNscy0yIiBkPSJNMzAuMjcsMTguOTRjLTItLjUzLS42Nyw4LjIxLjk0LDcuNDlBNC43Miw0LjcyLDAsMCwwLDMwLjI3LDE4Ljk0WiIvPjxwYXRoIGNsYXNzPSJjbHMtMiIgZD0iTTIzLjUzLDEyLjQzYzMuNDItLjU3LDYuMjcsMS40Niw2LjE2LDUuMTctLjEyLDEuNDMtNy40Mi01LTYuMTYtNS4xN1oiLz48cGF0aCBjbGFzcz0iY2xzLTIiIGQ9Ik0xNC4wNywxMi4zMmMtMy40My0uNTctNi4yOCwxLjQ2LTYuMTYsNS4xN0M4LDE4LjkyLDE1LjMzLDEyLjU0LDE0LjA3LDEyLjMyWiIvPjxwYXRoIGNsYXNzPSJjbHMtMiIgZD0iTTE5LDExLjQ2Yy0yLS4wNS00LDEuNTItNCwyLjQzLDAsMS4xLDEuNjEsMi4yMyw0LDIuMjZzNC0uOSw0LTItMi4yMy0yLjY2LTQtMi42NFoiLz48cGF0aCBjbGFzcz0iY2xzLTIiIGQ9Ik0xOS4xMSwzNC4xNWMxLjc4LS4wOCw0LjE3LjU3LDQuMTgsMS40M3MtMi4xNywyLjc0LTQuMywyLjctNC4zNi0xLjgtNC4zMy0yLjQ2QzE0LjYyLDM0Ljg2LDE3LjM0LDM0LjEsMTkuMTEsMzQuMTVaIi8+PHBhdGggY2xhc3M9ImNscy0yIiBkPSJNMTIuNTMsMjljMS4yNywxLjUzLDEuODUsNC4yMi43OSw1LTEsLjYtMy40NC4zNS01LjE2LTIuMTMtMS4xNy0yLjA4LTEtNC4yLS4yLTQuODNDOS4xOCwyNi4zMywxMS4wNywyNy4zMywxMi41MywyOVoiLz48cGF0aCBjbGFzcz0iY2xzLTIiIGQ9Ik0yNS40NCwyOC41NEMyNC4wNiwzMC4xNSwyMy4zLDMzLjA4LDI0LjMsMzRjMSwuNzQsMy41My42Myw1LjQzLTIsMS4zOC0xLjc3LjkxLTQuNzIuMTMtNS41MUMyOC42OSwyNS42MSwyNywyNi43NywyNS40NCwyOC41NFoiLz48Y2lyY2xlIGNsYXNzPSJjbHMtMyIgY3g9IjI5LjQ0IiBjeT0iMjYuNDgiIHI9IjkuMTciLz48cGF0aCBkPSJNMjkuNDQsMThhOC41Myw4LjUzLDAsMSwxLTguNTIsOC41M0E4LjUzLDguNTMsMCwwLDEsMjkuNDQsMThtMC0xLjI4YTkuODEsOS44MSwwLDEsMCw5LjgxLDkuODEsOS44Miw5LjgyLDAsMCwwLTkuODEtOS44MVoiLz48ZyBjbGFzcz0iY2xzLTQiPjxwYXRoIGNsYXNzPSJjbHMtNSIgZD0iTTI5LjYzLDMxLjYyVjI5LjE4SDI1LjIyVjI3LjU2bDMuNzItNi4xM2gyLjg5djZIMzN2MS43OUgzMS44M3YyLjQ0Wm0tMi4xNS00LjIzaDIuMTVWMjUuNzNxMC0uNSwwLTEuMTRjMC0uNDQsMC0uODEuMDctMS4xNGgtLjA3Yy0uMTMuMjgtLjI2LjU3LS40Ljg1cy0uMjguNTgtLjQzLjg3WiIvPjwvZz48L3N2Zz4=', +}; + +const osVersions = { + 'raspberrypi4-64': [ + { + release_tag: [ + { + tag_key: 'version', + value: '3.2.7+rev1', + }, + ], + id: 2706026, + known_issue_list: null, + raw_version: '3.2.7+rev1', + phase: null, + osType: 'default', + strippedVersion: '3.2.7+rev1', + rawVersion: '3.2.7+rev1', + basedOnVersion: '3.2.7+rev1', + formattedVersion: 'v3.2.7+rev1 (recommended)', + isRecommended: true, + }, + { + release_tag: [ + { + tag_key: 'version', + value: '3.2.7', + }, + ], + id: 2699112, + known_issue_list: null, + raw_version: '3.2.7', + phase: null, + osType: 'default', + strippedVersion: '3.2.7', + rawVersion: '3.2.7', + basedOnVersion: '3.2.7', + formattedVersion: 'v3.2.7', + }, + { + release_tag: [ + { + tag_key: 'version', + value: '3.2.1', + }, + ], + id: 2691761, + known_issue_list: null, + raw_version: '3.2.1', + phase: null, + osType: 'default', + strippedVersion: '3.2.1', + rawVersion: '3.2.1', + basedOnVersion: '3.2.1', + formattedVersion: 'v3.2.1', + }, + { + release_tag: [ + { + tag_key: 'version', + value: '3.2.0', + }, + ], + id: 2690869, + known_issue_list: null, + raw_version: '3.2.0', + phase: null, + osType: 'default', + strippedVersion: '3.2.0', + rawVersion: '3.2.0', + basedOnVersion: '3.2.0', + formattedVersion: 'v3.2.0', + }, + { + release_tag: [ + { + tag_key: 'version', + value: '3.1.12', + }, + ], + id: 2688571, + known_issue_list: null, + raw_version: '3.1.12', + phase: null, + osType: 'default', + strippedVersion: '3.1.12', + rawVersion: '3.1.12', + basedOnVersion: '3.1.12', + formattedVersion: 'v3.1.12', + }, + { + release_tag: [ + { + tag_key: 'version', + value: '3.1.1', + }, + ], + id: 2668490, + known_issue_list: null, + raw_version: '3.1.1', + phase: null, + osType: 'default', + strippedVersion: '3.1.1', + rawVersion: '3.1.1', + basedOnVersion: '3.1.1', + formattedVersion: 'v3.1.1', + }, + ], +}; +const Template = ( + props: Omit, +) => { + const [show, setShow] = useState(false); + + return ( + <> + + setShow(false)} + {...props} + /> + + ); +}; + +const meta = { + title: 'Patterns/DownloadImageDialog', + component: Template, + tags: ['autodocs'], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + compatibleDeviceTypes: deviceTypes as DeviceType[], + applicationId: 1918419, + releaseId: undefined, + downloadUrl: `$API_ENDPOINT}/download`, + initialDeviceType, + initialOsVersions: osVersions as any, + getDockerArtifact: () => '', + isInitialDefault: true, + }, +}; diff --git a/src/components/DownloadImageDialog/ImageForm.tsx b/src/components/DownloadImageDialog/ImageForm.tsx new file mode 100644 index 00000000..ffb40368 --- /dev/null +++ b/src/components/DownloadImageDialog/ImageForm.tsx @@ -0,0 +1,422 @@ +import { + Avatar, + Box, + Button, + ButtonProps, + Checkbox, + Collapse, + Divider, + FormControl, + FormControlLabel, + FormLabel, + InputLabel, + MenuItem, + Radio, + RadioGroup, + Select, + TextField, + Tooltip, + Typography, +} from '@mui/material'; +import HelpIcon from '@mui/icons-material/Help'; +import { memo, useCallback, useEffect, useMemo, useState } from 'react'; +import { + getPreferredVersionOpts, + transformVersions, + VersionSelectionOptions, +} from './version'; +import WarningAmberIcon from '@mui/icons-material/WarningAmber'; +import { Truncate } from '../Truncate'; +import { OsTypeSelector } from './OsTypeSelector'; +import { VariantSelector } from './VariantSelector'; +import { FormModel } from '.'; +import AddIcon from '@mui/icons-material/Add'; +import RemoveIcon from '@mui/icons-material/Remove'; +import ArticleIcon from '@mui/icons-material/Article'; +import isEqual from 'lodash/isEqual'; +import { MUILinkWithTracking } from '../MUILinkWithTracking'; +import { DeviceType, OsVersionsByDeviceType } from './models'; + +const BuildVariants = ['dev', 'prod'] as const; +export type BuildVariant = (typeof BuildVariants)[number]; + +const POLL_INTERVAL_DOCS = + 'https://www.balena.io/docs/reference/supervisor/bandwidth-reduction/#side-effects--warnings'; + +const getCategorizedVersions = ( + deviceTypeOsVersions: OsVersionsByDeviceType, + deviceType: FormModel['deviceType'], + osType: string | null, +) => { + const osVersions = deviceTypeOsVersions[deviceType.slug] ?? []; + const deviceOsVersions = osType + ? osVersions.filter((osVersion) => osVersion.osType === osType) + : osVersions; + + const selectionOpts = transformVersions(deviceOsVersions); + const preferredSelectionOpts = getPreferredVersionOpts(selectionOpts); + + return { + selectionOpts, + preferredSelectionOpts, + }; +}; + +export type DialogAction = Omit & { + label: string; + onClick?: (event: React.MouseEvent, model: FormModel) => void; +}; + +interface ImageFormProps { + applicationId: number; + releaseId?: number; + compatibleDeviceTypes: DeviceType[] | undefined; + osVersions: OsVersionsByDeviceType; + osType: string | null; + osTypes: string[]; + isInitialDefault?: boolean; + model: FormModel; + hasEsrVersions?: boolean; + onSelectedVersionChange: (osVersion: string) => void; + onSelectedOsTypeChange: (osType: string) => void; + onChange: (key: keyof FormModel, value: FormModel[keyof FormModel]) => void; +} + +export const ImageForm: React.FC = memo( + ({ + compatibleDeviceTypes, + osVersions, + isInitialDefault, + osType, + osTypes, + hasEsrVersions, + model, + onSelectedVersionChange, + onSelectedOsTypeChange, + onChange, + }) => { + const [showAdvancedSettings, setShowAdvancedSettings] = useState(false); + const [version, setVersion] = useState< + VersionSelectionOptions | undefined + >(); + const [variant, setVariant] = useState('prod'); + const [showAllVersions, setShowAllVersions] = useState(false); + const { selectionOpts, preferredSelectionOpts } = getCategorizedVersions( + osVersions, + model.deviceType, + osType, + ); + const versionSelectionOpts = useMemo( + () => (showAllVersions ? selectionOpts : preferredSelectionOpts), + [preferredSelectionOpts, selectionOpts, showAllVersions], + ); + const showAllVersionsToggle = useMemo( + () => preferredSelectionOpts.length < selectionOpts.length, + [preferredSelectionOpts.length, selectionOpts.length], + ); + + const handleShowAllVersions = (e: any) => { + const isChecked = e.target.checked; + setShowAllVersions(isChecked); + + if (isChecked || !version) { + return; + } + + const selectedValueIsPreferred = preferredSelectionOpts.some( + (ver) => ver.value === version.value, + ); + if (selectedValueIsPreferred) { + return; + } + + const preferred = + preferredSelectionOpts.find((ver) => ver.isRecommended) ?? + preferredSelectionOpts?.[0]; + if (preferred) { + setVersion(preferred); + } + }; + + const handleSelectedDeviceTypeChange = useCallback( + (deviceType: DeviceType) => { + if (model.deviceType.slug === deviceType.slug) { + return; + } + + const newDeviceType = compatibleDeviceTypes?.find( + (cdt) => cdt.slug === deviceType.slug, + ); + if (!newDeviceType) { + return; + } + + onChange('deviceType', newDeviceType); + }, + [compatibleDeviceTypes, model.deviceType.slug, onChange], + ); + + const handleVersionVariantChange = useCallback( + (key: 'variant' | 'version', value: any) => { + const unifyVariantVersion = ( + version: VersionSelectionOptions | undefined, + variant: 'dev' | 'prod', + ) => { + if (!version) { + return; + } + const versionWithVariant = version.hasPrebuiltVariants + ? version.rawVersions[variant] + : version.rawVersion; + if (versionWithVariant) { + onSelectedVersionChange(versionWithVariant); + onChange('developmentMode', variant === 'dev'); + } + + if (version.hasPrebuiltVariants && !version.rawVersions[variant]) { + setVariant(variant === 'dev' ? 'prod' : 'dev'); + } + }; + + if (key === 'variant') { + unifyVariantVersion(version, value); + return; + } + unifyVariantVersion(value, variant); + }, + [onChange, onSelectedVersionChange, variant, version], + ); + + useEffect(() => { + const newVersion = + versionSelectionOpts.find((ver) => ver.isRecommended) ?? + versionSelectionOpts[0]; + if (isEqual(version, newVersion)) { + return; + } + setVersion(newVersion); + }, [version, versionSelectionOpts]); + + return ( + + + {compatibleDeviceTypes && compatibleDeviceTypes.length > 1 && ( + + + Select device type{' '} + + + + + + + )} + {(!isInitialDefault || osType) && + hasEsrVersions && + model.deviceType && ( + + )} + + {(!isInitialDefault || !version) && ( + + + Select version + + + {showAllVersionsToggle && ( + + + Show outdated versions + + )} + + )} + {(!isInitialDefault || !variant) && ( + + handleVersionVariantChange('variant', variant) + } + /> + )} + + + + Network + + onChange('network', event.target.value)} + > + } + label="Ethernet only" + /> + } + label="Wifi + Ethernet" + /> + + + {model.network === 'wifi' && ( + <> + WiFi SSID + onChange('wifiSsid', event.target.value)} + /> + Wifi Passphrase + onChange('wifiKey', event.target.value)} + /> + + )} + + + + + + + + Check for updates every X minutes{' '} + + + + + + onChange('appUpdatePollInterval', event.target.value) + } + /> + + Provisioning Key name + + onChange('provisioningKeyName', event.target.value) + } + /> + Provisioning Key expiring on + + onChange('provisioningKeyExpiryDate', event.target.value) + } + /> + + + + ); + }, +); + +const DeviceTypeItem: React.FC<{ deviceType: DeviceType }> = ({ + deviceType, +}) => { + return ( + + + {deviceType.name} + + ); +}; diff --git a/src/components/DownloadImageDialog/OsTypeSelector.tsx b/src/components/DownloadImageDialog/OsTypeSelector.tsx new file mode 100644 index 00000000..440a5f99 --- /dev/null +++ b/src/components/DownloadImageDialog/OsTypeSelector.tsx @@ -0,0 +1,146 @@ +import { + Badge, + Box, + InputLabel, + MenuItem, + Select, + SelectProps, + Tooltip, + Typography, +} from '@mui/material'; +import { getOsTypeName } from './utils'; +import ArticleIcon from '@mui/icons-material/Article'; +import { MUILinkWithTracking } from '../MUILinkWithTracking'; +import { OsTypesEnum } from './models'; + +interface OsTypeSelectorProps + extends Omit< + SelectProps, + | 'options' + | 'onChange' + | 'valueKey' + | 'disabledKey' + | 'valueLabel' + | 'children' + > { + supportedOsTypes: string[]; + hasEsrVersions: boolean; + selectedOsTypeSlug: string | null; + onSelectedOsTypeChange: (osType: string) => void; +} + +interface OsTypeObj { + slug: string; + disabled: boolean; + supportedForDeviceType: boolean; + supportedForApp: boolean; +} + +const OsTypeOption = ({ + osType, + bold, +}: { + osType: OsTypeObj | undefined; + bold?: boolean; +}) => { + if (!osType) { + return Select OS type...; + } + return ( + + + {getOsTypeName(osType.slug)} + + + {!osType.supportedForDeviceType && ( + + )} + {!osType.supportedForApp && ( + + )} + + + ); +}; + +export const OsTypeSelector = ({ + supportedOsTypes, + hasEsrVersions, + selectedOsTypeSlug, + onSelectedOsTypeChange, + ...otherProps +}: OsTypeSelectorProps) => { + const selectOsTypes = Object.values({ + default: OsTypesEnum.DEFAULT, + ESR: OsTypesEnum.ESR, + }).map((osType: OsTypesEnum) => { + const supportedForDeviceType = + osType === OsTypesEnum.ESR ? hasEsrVersions : true; + const supportedForApp = supportedOsTypes.includes(osType); + const disabled = !supportedForApp || !supportedForDeviceType; + + return { + slug: osType, + disabled, + supportedForDeviceType, + supportedForApp, + }; + }); + + const selectedOsType = selectOsTypes.find( + (osType) => + osType.slug === selectedOsTypeSlug && osType.supportedForDeviceType, + ); + + return ( + + + Select OS type{' '} + + + + + + id="newAppApplicationType" + fullWidth + sx={{ height: '56px' }} + disabled={supportedOsTypes.length === 0} + value={selectedOsType} + label={ + + + + } + onChange={(event) => { + const osType = selectOsTypes.find( + (os) => os.slug === event.target.value, + )!; + return !osType.disabled && onSelectedOsTypeChange(osType.slug); + }} + {...otherProps} + > + {selectOsTypes.map((option) => ( + + + + + + ))} + + + ); +}; diff --git a/src/components/DownloadImageDialog/VariantSelector.tsx b/src/components/DownloadImageDialog/VariantSelector.tsx new file mode 100644 index 00000000..924c4ca0 --- /dev/null +++ b/src/components/DownloadImageDialog/VariantSelector.tsx @@ -0,0 +1,96 @@ +import { + Alert, + FormControl, + FormControlLabel, + FormLabel, + Radio, + RadioGroup, + Typography, +} from '@mui/material'; +import { MUILinkWithTracking } from '../MUILinkWithTracking'; +import { BuildVariant } from './ImageForm'; +import { getOsVariantDisplayText } from './utils'; +import { VersionSelectionOptions } from './version'; + +interface VaraintSelectorProps { + version: VersionSelectionOptions | undefined; + onVariantChange: (variant: BuildVariant) => void; + variant: BuildVariant; +} + +const BuildVariants = ['dev', 'prod'] as const; + +export const VariantSelector: React.FC = ({ + version, + variant, + onVariantChange, +}) => { + return ( + + Select edition + + onVariantChange(event.target.value as BuildVariant) + } + > + {BuildVariants.map((buildVariant) => { + const label = ( + + {getOsVariantDisplayText(buildVariant)} + + ); + const isDev = buildVariant === 'dev'; + return ( + <> + } + label={ + isDev ? ( + <> + {label} + + Recommended for first time users + + + ) : ( + label + ) + } + /> + {isDev ? ( + + Development images should be used when you are developing an + application and want to use the fast{' '} + + local mode + {' '} + workflow{' '} + + This variant should never be used in production. + + + ) : ( + + Production images are ready for production deployments, but + don't offer easy access for local development. + + )} + + ); + })} + + + ); +}; diff --git a/src/components/DownloadImageDialog/index.tsx b/src/components/DownloadImageDialog/index.tsx new file mode 100644 index 00000000..16c891d0 --- /dev/null +++ b/src/components/DownloadImageDialog/index.tsx @@ -0,0 +1,499 @@ +import { + Alert, + AlertProps, + Avatar, + Box, + Button, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + Grid, + Skeleton, + Typography, +} from '@mui/material'; +import { useCallback, useEffect, useMemo, useState } from 'react'; +import CloseIcon from '@mui/icons-material/Close'; +import { FALLBACK_LOGO_UNKNOWN_DEVICE, stripVersionBuild } from './utils'; +import { ImageForm } from './ImageForm'; +import { ApplicationInstructions } from './ApplicationInstructions'; +import { DropDownButton, DropDownButtonProps } from '../DropDownButton'; +import DownloadIcon from '@mui/icons-material/Download'; +import pickBy from 'lodash/pickBy'; +import debounce from 'lodash/debounce'; +import isEmpty from 'lodash/isEmpty'; +import { + DeviceType, + Dictionary, + OsTypesEnum, + OsVersionsByDeviceType, +} from './models'; +import uniq from 'lodash/uniq'; + +const etcherLogoBase64 = + 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iODAwIiBoZWlnaHQ9IjYwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KIDxnPgogIDx0aXRsZT5FdGNoZXI8L3RpdGxlPgogIDxnIGlkPSJzdmdfMSIgc3Ryb2tlPSJudWxsIj4KICAgPHBhdGggaWQ9InN2Z18yIiBjbGFzcz0ic3QxIiBkPSJtNDEyLjkwMzgzLDM1OC4wNjcxM2wwLDE3MS40OTU4M2M3LjQ5MjU0LC0xLjY2NTAxIDE0LjE1MjU3LC0zLjMzMDAyIDIwLjgxMjYsLTcuNDkyNTRsMTQyLjM1ODE5LC04MS41ODUzOWMyMC44MTI2LC0xMS42NTUwNiAzMy4zMDAxNiwtMzQuMTMyNjYgMzMuMzAwMTYsLTU4LjI3NTI4bDAsLTE2Mi4zMzgyOGMwLC02LjY2MDAzIC0wLjgzMjUsLTEzLjMyMDA2IC0zLjMzMDAyLC0xOS4xNDc1OWwtMTU0LjAxMzI0LDg5LjA3NzkzYy0zMi40Njc2NiwyMi40Nzc2MSAtMzkuMTI3NjksNDMuMjkwMjEgLTM5LjEyNzY5LDY4LjI2NTMzbDAsLTAuMDAwMDF6IiBmaWxsPSIjQTVERTM3IiBzdHJva2U9Im51bGwiLz4KICAgPHBhdGggaWQ9InN2Z18zIiBjbGFzcz0ic3QyIiBkPSJtNjYyLjY1NTAzLDE2Ny40MjM3MWwtNTYuNjEwMjcsMzIuNDY3NjZjMS42NjUwMSw1LjgyNzUzIDMuMzMwMDIsMTIuNDg3NTYgMy4zMzAwMiwxOS4xNDc1OWwwLDE2My4xNzA3OWMwLDI0LjE0MjYyIC0xMy4zMjAwNiw0Ni42MjAyMiAtMzMuMzAwMTYsNTguMjc1MjhsLTE0Mi4zNTgxOSw4MS41ODUzOWMtNi42NjAwMywzLjMzMDAyIC0xMy4zMjAwNiw1LjgyNzUzIC0yMC44MTI2LDcuNDkyNTRsMCw2NC45MzUzMWM5Ljk5MDA1LC0xLjY2NTAxIDE5Ljk4MDEsLTQuOTk1MDIgMjguMzA1MTQsLTkuOTkwMDVsMTg0LjgxNTg5LC0xMDUuNzI4MDFjMjUuODA3NjIsLTE0Ljk4NTA3IDQxLjYyNTIsLTQyLjQ1NzcgNDEuNjI1MiwtNzIuNDI3ODVsMCwtMjExLjQ1NjAyYzAsLTkuMTU3NTQgLTEuNjY1MDEsLTE4LjMxNTA5IC00Ljk5NTAyLC0yNy40NzI2M2wtMC4wMDAwMSwweiIgZmlsbD0iI0M4RjE3OCIgc3Ryb2tlPSJudWxsIi8+CiAgIDxwYXRoIGlkPSJzdmdfNCIgY2xhc3M9InN0MSIgZD0ibTM5OS41ODM3NiwzMDMuOTU0MzZjOC4zMjUwNCwtMTMuMzIwMDYgMjAuODEyNiwtMjUuODA3NjIgMzkuMTI3NjksLTM2LjYzMDE4bDE1NS42NzgyNSwtODkuOTEwNDNjLTQuOTk1MDIsLTYuNjYwMDMgLTExLjY1NTA2LC0xMi40ODc1NiAtMTguMzE1MDksLTE2LjY1MDA4bC0xNDIuMzU4MTksLTgxLjU4NTM5Yy0yMC44MTI2LC0xMS42NTUwNiAtNDYuNjIwMjIsLTExLjY1NTA2IC02Ny40MzI4MiwwbC0xNDEuNTI1NjgsODEuNTg1MzljLTcuNDkyNTQsNC4xNjI1MiAtMTMuMzIwMDYsOS45OTAwNSAtMTkuMTQ3NTksMTYuNjUwMDhsMTU0Ljg0NTc1LDg5LjkxMDQzYzE4LjMxNTA5LDExLjY1NTA2IDMwLjgwMjY1LDIzLjMxMDExIDM5LjEyNzY5LDM2LjYzMDE4bC0wLjAwMDAxLDB6IiBmaWxsPSIjQTVERTM3IiBzdHJva2U9Im51bGwiLz4KICAgPHBhdGggaWQ9InN2Z181IiBjbGFzcz0ic3QyIiBkPSJtMjI0Ljc1NzkyLDE2MS41OTYxOGwxNDEuNTI1NjgsLTgxLjU4NTM5YzIwLjgxMjYsLTExLjY1NTA2IDQ2LjYyMDIyLC0xMS42NTUwNiA2Ny40MzI4MiwwbDE0Mi4zNTgxOSw4MS41ODUzOWM3LjQ5MjU0LDQuMTYyNTIgMTMuMzIwMDYsOS45OTAwNSAxOC4zMTUwOSwxNi42NTAwOGw1Ni42MTAyNywtMzIuNDY3NjZjLTYuNjYwMDMsLTkuMTU3NTQgLTE0Ljk4NTA3LC0xNi42NTAwOCAtMjQuOTc1MTIsLTIxLjY0NTFsLTE4NC44MTU4OSwtMTA3LjM5MzAyYy0yNS44MDc2MiwtMTQuOTg1MDcgLTU3LjQ0Mjc4LC0xNC45ODUwNyAtODMuMjUwNCwwbC0xODMuMTUwODgsMTA2LjU2MDUxYy05Ljk5MDA1LDUuODI3NTMgLTE4LjMxNTA5LDEzLjMyMDA2IC0yNC45NzUxMiwyMi40Nzc2MWw1Ni42MTAyNywzMi40Njc2NmM0LjE2MjUyLC02LjY2MDAzIDEwLjgyMjU1LC0xMi40ODc1NiAxOC4zMTUwOSwtMTYuNjUwMDh6IiBmaWxsPSIjQzhGMTc4IiBzdHJva2U9Im51bGwiLz4KICAgPHBhdGggaWQ9InN2Z182IiBjbGFzcz0ic3QyIiBkPSJtMzY2LjI4MzYsNTIyLjA3MDQxbC0xNDEuNTI1NjgsLTgxLjU4NTM5Yy0yMC44MTI2LC0xMS42NTUwNiAtMzMuMzAwMTYsLTM0LjEzMjY2IC0zMy4zMDAxNiwtNTguMjc1MjhsMCwtMTYzLjE3MDc5YzAsLTYuNjYwMDMgMC44MzI1LC0xMi40ODc1NiAyLjQ5NzUxLC0xOC4zMTUwOWwtNTYuNjEwMjcsLTMyLjQ2NzY2Yy0zLjMzMDAyLDkuMTU3NTQgLTQuOTk1MDIsMTcuNDgyNTggLTQuOTk1MDIsMjYuNjQwMTNsMCwyMTIuMjg4NTJjMCwyOS45NzAxNCAxNS44MTc1OCw1Ny40NDI3OCA0MS42MjUyLDcxLjU5NTM0bDE4My45ODMzOSwxMDUuNzI4MDFjOC4zMjUwNCw0Ljk5NTAyIDE4LjMxNTA5LDguMzI1MDQgMjguMzA1MTQsOS45OTAwNWwwLC02NC45MzUzMWMtNi42NjAwMywtMC44MzI1IC0xMy4zMjAwNiwtMy4zMzAwMiAtMTkuOTgwMSwtNy40OTI1NGwtMC4wMDAwMSwwLjAwMDAxeiIgZmlsbD0iI0M4RjE3OCIgc3Ryb2tlPSJudWxsIi8+CiAgIDxwYXRoIGlkPSJzdmdfNyIgY2xhc3M9InN0MSIgZD0ibTM0Ny4xMzYwMSwyODguOTY5MjlsLTE1My4xODA3NCwtODguMjQ1NDJjLTEuNjY1MDEsNS44Mjc1MyAtMi40OTc1MSwxMi40ODc1NiAtMi40OTc1MSwxOC4zMTUwOWwwLDE2My4xNzA3OWMwLDI0LjE0MjYyIDEyLjQ4NzU2LDQ2LjYyMDIyIDMzLjMwMDE2LDU4LjI3NTI4bDE0MS41MjU2OCw4MS41ODUzOWM2LjY2MDAzLDMuMzMwMDIgMTMuMzIwMDYsNS44Mjc1MyAyMC44MTI2LDcuNDkyNTRsMCwtMTcxLjQ5NTgzYy0wLjgzMjUsLTI0Ljk3NTEyIC03LjQ5MjU0LC00NS43ODc3MiAtMzkuOTYwMTksLTY5LjA5NzgzbDAsLTAuMDAwMDF6IiBmaWxsPSIjQTVERTM3IiBzdHJva2U9Im51bGwiLz4KICA8L2c+CiA8L2c+Cgo8L3N2Zz4='; + +const ETCHER_OPEN_IMAGE_URL = 'https://www.balena.io/etcher/open-image-url'; + +export interface FormModel { + appId: number; + deviceType: DeviceType; + version: string; + network: string; + developmentMode?: boolean; + appUpdatePollInterval?: number; + wifiSsid?: string; + wifiKey?: string; + provisioningKeyName?: string; + provisioningKeyExpiryDate?: string; +} + +const getUniqueOsTypes = ( + osVersions: OsVersionsByDeviceType, + deviceTypeSlug: string | undefined, +) => { + if ( + isEmpty(osVersions) || + !deviceTypeSlug || + isEmpty(osVersions[deviceTypeSlug]) + ) { + return []; + } + + return uniq(osVersions[deviceTypeSlug].map((x) => x.osType)); +}; + +const generateImageUrl = ( + model: Omit & { deviceType: string }, + downloadUrl: string, +) => { + // TODO check if possible to edit Etcher to avoid a double encode on the version. + if (model.version) { + model.version = encodeURIComponent(model.version); + } + if (model.network === 'ethernet') { + model.wifiSsid = undefined; + model.wifiKey = undefined; + } + const queryParams = Object.entries(model) + .map(([key, value]) => (!!value ? `${key}=${value}` : null)) + .filter((param) => !!param) + .join('&'); + return `${downloadUrl}?${queryParams}`; +}; + +const flashWithEtcher = ( + model: FormModel, + downloadUrl: string, + authToken?: string, +) => { + const modelCopy = { ...model, deviceType: model.deviceType.slug }; + const imageUrl = generateImageUrl(modelCopy, downloadUrl); + const axiosConfig = { + method: 'POST', + url: imageUrl, + ...(authToken && { + headers: { + Authorization: `Bearer ${authToken}`, + }, + }), + data: pickBy(modelCopy, (value) => !!value), + }; + // TODO: Check how to remove from resin site the decode and avoid this double encodeURIComponent on a stringified obj + const stringifiedAxiosConfig = encodeURIComponent( + JSON.stringify(axiosConfig), + ); + window.open( + `${ETCHER_OPEN_IMAGE_URL}?imageUrl=${encodeURIComponent( + stringifiedAxiosConfig, + )}`, + '_blank', + ); +}; + +const downlaodOS = async (model: FormModel, downloadUrl: string) => { + const modelCopy = { ...model, deviceType: model.deviceType.slug }; + const url = generateImageUrl(modelCopy, downloadUrl); + window.open(url); +}; + +const debounceDownloadSize = debounce( + async ( + getDownloadSize: NonNullable, + deviceType: DeviceType, + rawVersion: string, + setDownloadSize: (value: string | null) => void, + ) => { + try { + setDownloadSize((await getDownloadSize(deviceType, rawVersion)) ?? null); + } catch { + setDownloadSize(null); + } + }, + 200, + { + trailing: true, + leading: false, + }, +); + +export interface DownloadImageDialogProps { + open: boolean; + applicationId: number; + releaseId?: number; + compatibleDeviceTypes: DeviceType[] | undefined; + initialDeviceType: DeviceType; + initialOsVersions?: OsVersionsByDeviceType; + isInitialDefault?: boolean; + downloadUrl: string; + onDownloadStart?: (downloadConfigOnly: boolean, model: FormModel) => void; + getSupportedOsVersions?: () => Promise; + getSupportedOsTypes?: ( + applicationId: number, + deviceTypeSlug: string, + ) => Promise; + downloadConfig?: (model: FormModel) => Promise; + getDownloadSize?: ( + deviceType: DeviceType, + rawVersion: string | null, + ) => Promise | undefined; + getDockerArtifact: (deviceTypeSlug: string, rawVersion: string) => string; + hasEsrVersions?: (deviceTypeSlugs: string[]) => Promise>; + onClose: () => void; + dialogActions?: DropDownButtonProps['items']; + authToken?: string; +} + +export const DownloadImageDialog: React.FC = ({ + open, + applicationId, + releaseId, + compatibleDeviceTypes, + initialDeviceType, + initialOsVersions, + isInitialDefault, + downloadUrl, + onDownloadStart, + getSupportedOsVersions, + getSupportedOsTypes, + downloadConfig, + getDownloadSize, + getDockerArtifact, + hasEsrVersions, + onClose, + dialogActions, + authToken, +}) => { + const [rawVersion, setRawVersion] = useState(null); + const [formModel, setFromModel] = useState({ + appId: applicationId, + releaseId, + deviceType: initialDeviceType, + version: rawVersion ?? '', + network: 'ethernet', + appUpdatePollInterval: 10, + wifiSsid: undefined, + wifiKey: undefined, + provisioningKeyName: undefined, + provisioningKeyExpiryDate: undefined, + }); + + const [osVersions, setOsVersions] = useState( + initialOsVersions ?? {}, + ); + const [osType, setOsType] = useState(); + const [osTypes, setOsTypes] = useState( + getUniqueOsTypes(osVersions, initialDeviceType?.slug), + ); + + const [deviceTypeHasEsr, setDeviceTypeHasEsr] = useState>( + initialDeviceType?.slug + ? { + [initialDeviceType.slug]: osTypes.includes(OsTypesEnum.ESR), + } + : {}, + ); + const [isDownloadingConfig, setIsDownloadingConfig] = useState(false); + const [isFetching, setIsFetching] = useState(isEmpty(osVersions)); + const [downloadSize, setDownloadSize] = useState(null); + const hasDockerImageDownload = useMemo( + () => formModel.deviceType?.yocto?.deployArtifact === 'docker-image', + [formModel.deviceType?.yocto?.deployArtifact], + ); + + const [downloadConfigOnly, setDownloadConfigOnly] = useState( + hasDockerImageDownload, + ); + + const logoSrc = useMemo( + () => + formModel.deviceType?.logo ?? + formModel.deviceType?.logoUrl ?? + FALLBACK_LOGO_UNKNOWN_DEVICE, + [formModel.deviceType?.logo, formModel.deviceType?.logoUrl], + ); + const defaultDisplayName = useMemo( + () => formModel.deviceType?.name ?? '-', + [formModel.deviceType?.name], + ); + + const actions: DropDownButtonProps['items'] = useMemo(() => { + const startDownload = (downloadConfigOnly: boolean) => { + if (typeof onDownloadStart === 'function') { + onDownloadStart(downloadConfigOnly, formModel); + } + setDownloadConfigOnly(downloadConfigOnly); + }; + + const actions: DropDownButtonProps['items'] = [ + ...(dialogActions ?? []), + { + eventName: 'Flash With Etcher Clicked', + eventProperties: { + appId: formModel.appId, + releaseId: formModel.releaseId, + downloadUrl, + }, + onClick: () => flashWithEtcher(formModel, downloadUrl, authToken), + children: ( + <> + etcher Flash + + ), + disabled: hasDockerImageDownload, + tooltip: hasDockerImageDownload + ? 'This image is deployed to docker so you can only download its config' + : 'Etcher v1.7.2 or greater is required', + }, + { + eventName: 'Download balenaOS Clicked', + eventProperties: { + appId: formModel.appId, + releaseId: formModel.releaseId, + downloadUrl, + }, + onClick: async () => await downlaodOS(formModel, downloadUrl), + children: ( + <> + Download balenaOS{' '} + {rawVersion && downloadSize ? ` (~${downloadSize})` : ''} + + ), + disabled: hasDockerImageDownload, + tooltip: hasDockerImageDownload + ? 'This image is deployed to docker so you can only download its config' + : '', + }, + ]; + + if (!!downloadConfig) { + actions.push({ + eventName: 'Download Configuration File Only', + eventProperties: { + appId: formModel.appId, + releaseId: formModel.releaseId, + }, + onClick: async () => { + if (downloadConfigOnly && downloadConfig) { + setIsDownloadingConfig(true); + await downloadConfig(formModel); + setIsDownloadingConfig(false); + } + startDownload(true); + }, + children: ( + <> + Download configuration file only + + ), + }); + } + + return actions satisfies DropDownButtonProps['items']; + }, [ + authToken, + downloadConfig, + downloadConfigOnly, + downloadSize, + downloadUrl, + formModel, + hasDockerImageDownload, + dialogActions, + onDownloadStart, + rawVersion, + ]); + + useEffect(() => { + if (!rawVersion || !getDownloadSize) { + setDownloadSize(null); + return; + } + + // Debounce as the version changes right after the devicetype does, resulting in multiple requests. + debounceDownloadSize( + getDownloadSize, + formModel.deviceType, + rawVersion, + setDownloadSize, + ); + }, [formModel.deviceType, getDownloadSize, rawVersion]); + + useEffect(() => { + if (!compatibleDeviceTypes || !getSupportedOsVersions) { + return; + } + // const applicationType = getExpanded(application.application_type); + // as soon as the dialog opens, start fetching the osVersions for all + // the compatible device types + getSupportedOsVersions() + .then(setOsVersions) + .catch((e) => { + console.error(e); + }) + .finally(() => setIsFetching(false)); + }, [compatibleDeviceTypes, applicationId, getSupportedOsVersions]); + + useEffect(() => { + if (!getSupportedOsTypes) { + return; + } + // Fetch the supported os types, so we can show the appropriate values in the Select box. + // We only want to do it once, and we rely on the cached data here. + getSupportedOsTypes(applicationId, formModel.deviceType.slug).then( + setOsTypes, + ); + }, [formModel.deviceType.slug, applicationId, getSupportedOsTypes]); + + useEffect(() => { + if (!compatibleDeviceTypes || !hasEsrVersions) { + return; + } + hasEsrVersions(compatibleDeviceTypes.map((dt) => dt.slug)).then( + setDeviceTypeHasEsr, + ); + }, [compatibleDeviceTypes, hasEsrVersions]); + + useEffect(() => { + const osTypes = getUniqueOsTypes(osVersions, formModel.deviceType.slug); + if (!osTypes.length) { + return; + } + if (!!osType) { + if (!osTypes.includes(osType)) { + setOsType(osTypes[0]); + } + } else { + setOsType( + osTypes.includes(OsTypesEnum.ESR) ? OsTypesEnum.ESR : osTypes[0], + ); + } + }, [formModel.deviceType.slug, osType, osVersions]); + + const setRawVersionCallback = useCallback( + (osVersion: string) => setRawVersion(osVersion), + [], + ); + const setOsTypeCallback = useCallback( + (osType: string) => setRawVersion(osType), + [], + ); + + const handleChange = useCallback( + (key: keyof FormModel, value: FormModel[keyof FormModel]) => { + setFromModel((oldState) => ({ + ...oldState, + [key]: value, + })); + }, + [], + ); + + return ( + + + + + Add new device + + + + + + + {(isFetching || isDownloadingConfig) && ( + + )} + {!isFetching && ( + <> + {isEmpty(osVersions) && ( + + No OS versions available for download + + )} + {!!osType && !!compatibleDeviceTypes && ( + + )} + + )} + + + + + + {(formModel.deviceType.imageDownloadAlerts ?? []).map((alert) => { + return ( + + {alert.message} + + ); + })} + + + + + + ); +}; diff --git a/src/components/DownloadImageDialog/models.ts b/src/components/DownloadImageDialog/models.ts new file mode 100644 index 00000000..dfe9de79 --- /dev/null +++ b/src/components/DownloadImageDialog/models.ts @@ -0,0 +1,139 @@ +export interface Dictionary { + [key: string]: T; +} + +export type KeysOfUnion = T extends T ? keyof T : never; + +export interface WithId { + id: number; +} + +export interface PineDeferred { + __id: number; +} + +export type NavigationResource = [T] | PineDeferred; +export type OptionalNavigationResource = + | [] + | [T] + | PineDeferred + | null; + +export enum OsTypesEnum { + DEFAULT = 'default', + ESR = 'esr', +} + +export interface WithId { + id: number; +} + +export interface Contract { + slug: string; + type: string; + name?: string; + version?: string; + externalVersion?: string; + contractVersion?: string; + description?: string; + aliases?: string[]; + tags?: string[]; + data?: any; + assets?: any; + requires?: string[]; + provides?: string[]; + composedOf?: any; + partials?: any; +} + +export declare type OsLines = + | 'next' + | 'current' + | 'sunset' + | 'outdated' + | undefined; +export interface OsVersion { + id: number; + raw_version: string; + strippedVersion: string; + basedOnVersion?: string; + osType: string; + line?: OsLines; + variant: string; + isRecommended?: boolean; + known_issue_list: string | null; +} +export interface OsVersionsByDeviceType { + [deviceTypeSlug: string]: OsVersion[]; +} +export interface Application { + id: number; + app_name: string; + slug: string; + uuid: string; +} + +// the legacy device-type.json format +export interface OsSpecificDeviceTypeJsonInstructions { + linux: string[]; + osx: string[]; + windows: string[]; +} + +export type OsSpecificContractInstructions = Record< + 'Linux' | 'MacOS' | 'Windows', + string[] +>; + +export type OsSpecificDeviceTypeInstructions = + | OsSpecificDeviceTypeJsonInstructions + | OsSpecificContractInstructions; + +export interface DeviceTypeDownloadAlert { + type: string; + message: string; +} +export interface DeviceTypeOptions { + options: DeviceTypeOptionsGroup[]; + collapsed: boolean; + isCollapsible: boolean; + isGroup: boolean; + message: string; + name: string; +} +export interface DeviceTypeOptionsGroup { + default: number | string; + message: string; + name: string; + type: string; + min?: number; + max?: number; + docs?: string; + hidden?: boolean; + when?: Dictionary; + choices?: string[] | number[]; + choicesLabels?: Dictionary; +} +export interface DeviceType { + slug: string; + name: string; + logo?: string | null; + contract?: Record | null; + + /** @deprecated */ + imageDownloadAlerts?: DeviceTypeDownloadAlert[]; + /** @deprecated */ + instructions?: + | string[] + | OsSpecificDeviceTypeJsonInstructions + | OsSpecificContractInstructions; + /** @deprecated */ + options?: DeviceTypeOptions[]; + /** @deprecated */ + yocto?: { + fstype?: string; + deployArtifact: string; + }; + /** @deprecated use DeviceType.logo */ + logoUrl?: string; +} diff --git a/src/components/DownloadImageDialog/utils.ts b/src/components/DownloadImageDialog/utils.ts new file mode 100644 index 00000000..765e029b --- /dev/null +++ b/src/components/DownloadImageDialog/utils.ts @@ -0,0 +1,37 @@ +import template from 'lodash/template'; +import { Dictionary, OptionalNavigationResource, OsTypesEnum } from './models'; + +export const FALLBACK_LOGO_UNKNOWN_DEVICE = + 'https://dashboard.balena-cloud.com/img/unknown-device.svg'; + +export const OS_VARIANT_FULL_DISPLAY_TEXT_MAP: Dictionary = { + dev: 'Development', + prod: 'Production', +}; + +export const getExpanded = (obj: OptionalNavigationResource) => + (Array.isArray(obj) && obj[0]) || undefined; + +export const stripVersionBuild = (version: string) => + version.replace(/(\.dev|\.prod)/, ''); + +// Use lodash templates to simulate moustache templating +export const interpolateMustache = ( + data: { [key: string]: string }, + tpl: string, +) => template(tpl, { interpolate: /{{([\s\S]+?)}}/g })(data); + +export const getOsTypeName = (osTypeSlug: string) => { + switch (osTypeSlug) { + case OsTypesEnum.DEFAULT: + return 'balenaOS'; + case OsTypesEnum.ESR: + return 'balenaOS ESR'; + default: + return 'unknown'; + } +}; + +export const getOsVariantDisplayText = (variant: string): string => { + return OS_VARIANT_FULL_DISPLAY_TEXT_MAP[variant] || variant; +}; diff --git a/src/components/DownloadImageDialog/version.ts b/src/components/DownloadImageDialog/version.ts new file mode 100644 index 00000000..48a2dcaa --- /dev/null +++ b/src/components/DownloadImageDialog/version.ts @@ -0,0 +1,112 @@ +import uniq from 'lodash/uniq'; +import partition from 'lodash/partition'; +import { Dictionary, OsVersion } from './models'; + +export type VersionSelectionOptions = { + title: string; + value: string; + isRecommended?: boolean; + osType: string; + line?: string; + knownIssueList: string | null; +} & ( + | { + hasPrebuiltVariants: false; + rawVersion: string; + } + | { + hasPrebuiltVariants: true; + rawVersions: { + dev?: string; + prod?: string; + }; + } +); + +export const transformVersions = (versions: OsVersion[]) => { + // Get a single object per stripped version, with both variants of it included (if they exist). It expects a sorted ` + const optsByVersion: Dictionary = {}; + versions.forEach((version) => { + const existingSelectionOpt = optsByVersion[version.strippedVersion]; + // We always want to use the 'prod' variant's formatted version as it can contain additional information (such as recommended label). + const title = + (version.variant === 'dev' ? existingSelectionOpt?.title : null) ?? + version.strippedVersion; + + optsByVersion[version.strippedVersion] = { + title, + value: version.strippedVersion, + osType: version.osType, + line: version.line, + knownIssueList: version.known_issue_list, + // Unified releases in the model have variant === '' + // but we also test for nullish for backgwards compatibility w/ the typings. + ...(!version.variant + ? { + hasPrebuiltVariants: false, + rawVersion: version.raw_version, + } + : { + hasPrebuiltVariants: true, + rawVersions: { + ...(existingSelectionOpt != null && + 'rawVersions' in existingSelectionOpt && + existingSelectionOpt.rawVersions), + [version.variant]: version.raw_version, + }, + }), + }; + }); + + return Object.values(optsByVersion); +}; + +const LEGACY_OS_VERSION_MAJOR = 1; + +// This returns the 3 most preferred versions for an os type. For multi-line os types, that would be the latest of each line, otherwise it is the latest 3 versions. +export const getPreferredVersionOpts = ( + versionOpts: VersionSelectionOptions[], +) => { + const [supportedVersions, legacyVersions] = partition(versionOpts, (v) => { + // TODO: check if worth installing semver on rendition; + // const major = semver.major(v.strippedVersion); + const major = v.value.match(/\d+/)?.join(); + return major && parseInt(major, 10) > LEGACY_OS_VERSION_MAJOR; + }); + + const opts = supportedVersions.length ? supportedVersions : legacyVersions; + + const lines = uniq(opts.map((option) => option.line)); + const hasMultipleLines = lines.length > 1; + + if (hasMultipleLines) { + const preferredMultilineOpts: { + [key: string]: VersionSelectionOptions; + } = {}; + + opts.forEach((option) => { + if (option.line && !preferredMultilineOpts[option.line]) { + preferredMultilineOpts[option.line] = option; + } + }); + + return Object.values(preferredMultilineOpts); + } else { + const preferredDefaultOpts: VersionSelectionOptions[] = []; + for (const option of opts) { + if (preferredDefaultOpts.length >= 3) { + break; + } + + const match = option.value.match(/\d+\.\d+\./); + if ( + match && + !preferredDefaultOpts.find((v) => v.value.startsWith(match[0])) + ) { + preferredDefaultOpts.push(option); + } + } + + return preferredDefaultOpts; + } +}; diff --git a/src/components/OrderedListItem/OrderedListItem.stories.tsx b/src/components/OrderedListItem/OrderedListItem.stories.tsx new file mode 100644 index 00000000..7ad1ef17 --- /dev/null +++ b/src/components/OrderedListItem/OrderedListItem.stories.tsx @@ -0,0 +1,19 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { OrderedListItem } from '.'; +import { Typography } from '@mui/material'; + +const meta = { + title: 'Typography/Ordered List Item', + component: OrderedListItem, + tags: ['autodocs'], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + index: 1, + children: Hello world, + }, +}; diff --git a/src/components/OrderedListItem/index.tsx b/src/components/OrderedListItem/index.tsx new file mode 100644 index 00000000..ae7de04b --- /dev/null +++ b/src/components/OrderedListItem/index.tsx @@ -0,0 +1,44 @@ +import { + Avatar, + Box, + ListItem, + ListItemIcon, + ListItemProps, + Typography, +} from '@mui/material'; + +export interface OrderedListItemProps extends ListItemProps { + index: number; +} + +export const OrderedListItem: React.FC = ({ + index, + children, + ...orderedListItemProps +}) => { + return ( + + + + + {index} + + + + {children} + + ); +}; diff --git a/src/theme.ts b/src/theme.ts index e6959e27..492596c5 100644 --- a/src/theme.ts +++ b/src/theme.ts @@ -471,6 +471,7 @@ export const theme = createTheme({ styleOverrides: { tooltip: { backgroundColor: color.palette.neutral['1000'].value, + fontSize: '.75rem', }, arrow: { color: color.palette.neutral['1000'].value, @@ -508,5 +509,24 @@ export const theme = createTheme({ }, }, }, + MuiLink: { + styleOverrides: { + root: { + color: '#00AEEF', + textDecoration: 'none', + '&:hover': { + color: '#008bbf', + }, + }, + }, + }, + MuiListItemIcon: { + styleOverrides: { + root: { + minWidth: 'auto', + marginRight: '.5rem', + }, + }, + }, }, });