@balena/ui-shared-components 14.1.0-build-limit-field-lengths-2d063da12c2b1e3b691fc126191269d24d81aae1-1 → 14.1.0-build-secure-boot-backup-4d430fef3bc217d76b648ea38883d1b586f61a8a-1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,4 +6,5 @@ export declare const ApplicationInstructions: import("react").NamedExoticCompone
6
6
  templateData: {
7
7
  dockerImage: string;
8
8
  };
9
+ secureboot: boolean;
9
10
  }>;
@@ -1,11 +1,12 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import has from 'lodash/has';
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
2
  import { interpolateMustache } from './utils';
4
3
  import { Box, List, Tab, Tabs, Typography } from '@mui/material';
5
4
  import { memo, useEffect, useState } from 'react';
6
5
  import { MUILinkWithTracking } from '../MUILinkWithTracking';
7
6
  import { OrderedListItem } from '../OrderedListItem';
8
7
  import { Markdown } from '../Markdown';
8
+ import { Chip } from '../Chip';
9
+ import { token } from '../../utils/token';
9
10
  export const getUserOs = () => {
10
11
  const platform = window.navigator.platform.toLowerCase();
11
12
  if (platform.includes('win')) {
@@ -19,7 +20,8 @@ export const getUserOs = () => {
19
20
  }
20
21
  return 'Unknown';
21
22
  };
22
- export const ApplicationInstructions = memo(function ApplicationInstructions({ deviceType, templateData, }) {
23
+ const SECURE_BOOT_INSTRUCTION_TO_REPLACE_START = 'Press the F10 key while BIOS';
24
+ export const ApplicationInstructions = memo(function ApplicationInstructions({ deviceType, templateData, secureboot, }) {
23
25
  var _a;
24
26
  const [currentOs, setCurrentOs] = useState(getUserOs());
25
27
  const hasOsSpecificInstructions = !Array.isArray(deviceType.instructions);
@@ -52,37 +54,28 @@ export const ApplicationInstructions = memo(function ApplicationInstructions({ d
52
54
  setCurrentOs(value !== null && value !== void 0 ? value : 'Unknown');
53
55
  }, "aria-label": "os tabs", children: Object.keys(deviceType.instructions).map((os) => {
54
56
  return _jsx(Tab, { label: os, value: os }, os);
55
- }) }) }) })), _jsx(InstructionsList, { instructions: finalInstructions }), _jsx(Box, { mt: 2, children: _jsxs(Typography, { children: ["For more details please refer to our", ' ', _jsx(MUILinkWithTracking, { href: `https://www.balena.io/docs/learn/getting-started/${deviceType.slug}/nodejs/`, children: "Getting Started Guide" }), "."] }) })] }));
57
+ }) }) }) })), _jsx(InstructionsList, { instructions: finalInstructions, secureboot: secureboot }), _jsx(Box, { mt: 2, children: _jsxs(Typography, { children: ["For more details please refer to our", ' ', _jsx(MUILinkWithTracking, { href: `https://www.balena.io/docs/learn/getting-started/${deviceType.slug}/nodejs/`, children: "Getting Started Guide" }), "."] }) })] }));
56
58
  });
57
- const InstructionsItem = ({ node, index }) => {
58
- const hasChildren = has(node, 'children');
59
- let text = null;
60
- if (typeof node === 'string') {
61
- text = node;
62
- }
63
- if (node === null || node === void 0 ? void 0 : node.text) {
64
- text = node.text;
65
- }
66
- return (_jsxs(OrderedListItem, { index: index + 1, sx: { maxWidth: '100%' }, children: [_jsx(Markdown, { components: {
67
- code: (codeProps) => {
68
- return (_jsx("code", Object.assign({ style: {
69
- maxWidth: '100%',
70
- display: 'inline-block',
71
- whiteSpace: 'normal',
72
- wordBreak: 'break-all',
73
- wordWrap: 'break-word',
74
- } }, codeProps)));
75
- },
76
- br: () => {
77
- return _jsx("p", {});
78
- },
79
- p: ({ children }) => (_jsx("p", { style: { marginTop: 0, marginBottom: 0 }, children: children })),
80
- }, children: text }), hasChildren && (_jsx(List, { children: node.children.map((item, i) => {
81
- return _jsx(InstructionsItem, { node: item, index: i }, i);
82
- }) }))] }));
59
+ const InstructionsItem = ({ instruction, index, secureboot, }) => {
60
+ return (_jsx(OrderedListItem, { index: index + 1, sx: { maxWidth: '100%' }, children: _jsx(Markdown, { components: {
61
+ code: (codeProps) => {
62
+ return (_jsx("code", Object.assign({ style: {
63
+ maxWidth: '100%',
64
+ display: 'inline-block',
65
+ whiteSpace: 'normal',
66
+ wordBreak: 'break-all',
67
+ wordWrap: 'break-word',
68
+ } }, codeProps)));
69
+ },
70
+ br: () => {
71
+ return _jsx("p", {});
72
+ },
73
+ p: ({ children }) => (_jsx("p", { style: { marginTop: 0, marginBottom: 0 }, children: secureboot &&
74
+ instruction.includes(SECURE_BOOT_INSTRUCTION_TO_REPLACE_START) ? (_jsxs(_Fragment, { children: [instruction.split(SECURE_BOOT_INSTRUCTION_TO_REPLACE_START)[0], _jsxs(Box, { mt: 2, borderLeft: 1, borderColor: token('color.border.subtle'), pl: 2, children: [_jsx(Chip, { sx: { marginRight: 1 }, label: "Secure Boot", color: "purple" }), "The device needs to be configured in secure boot setup mode. This depends on the UEFI/BIOS implementation, but in general, this involves resetting the UEFI settings to default configuration, configuring the device to boot in UEFI mode, setting the first boot option to the USB key and disabling the restoration of factory keys. Save and Exit the UEFI/BIOS menu and the device should automatically reboot and begin the provisioning process.", ' ', _jsx(MUILinkWithTracking, { href: "https://docs.balena.io/reference/OS/secure-boot-and-full-disk-encryption/generic-x86-64-gpt/#provision-the-device", children: "Read more on UEFI settings for secure boot and full disk encryption." })] })] })) : (children) })),
75
+ }, children: instruction }) }));
83
76
  };
84
- const InstructionsList = ({ instructions }) => {
85
- return (_jsx(List, { children: instructions.map((item, i) => {
86
- return _jsx(InstructionsItem, { node: item, index: i }, `${item}_${i}`);
77
+ const InstructionsList = ({ instructions, secureboot, }) => {
78
+ return (_jsx(List, { children: instructions.map((instruction, i) => {
79
+ return (_jsx(InstructionsItem, { instruction: instruction, index: i, secureboot: secureboot }, `${instruction}_${i}`));
87
80
  }) }));
88
81
  };
@@ -16,5 +16,7 @@ interface ImageFormProps {
16
16
  onSelectedOsTypeChange: (osType: string) => void;
17
17
  onChange: (obj: Partial<DownloadImageFormModel>) => void;
18
18
  }
19
+ export declare const GENERIC_X86_SLUG = "generic-amd64";
20
+ export declare const GENERIC_X86_MINIMUM_SUPPORTED_SECUREBOOT_VERSION = "5.3.15";
19
21
  export declare const ImageForm: import("react").NamedExoticComponent<ImageFormProps>;
20
22
  export {};
@@ -1,6 +1,6 @@
1
1
  import { __rest } from "tslib";
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
- import { Avatar, Box, Checkbox, Chip, Divider, FormControl, FormControlLabel, FormLabel, InputAdornment, Radio, RadioGroup, TextField, Tooltip, Typography, IconButton, Autocomplete, Stack, Accordion, AccordionSummary, AccordionDetails, accordionSummaryClasses, } from '@mui/material';
3
+ import { Avatar, Box, Checkbox, Chip, Divider, FormControl, FormControlLabel, FormLabel, InputAdornment, Radio, RadioGroup, TextField, Tooltip, Typography, IconButton, Autocomplete, Stack, Accordion, AccordionSummary, AccordionDetails, accordionSummaryClasses, Switch, Dialog, DialogTitle, DialogContent, DialogActions, Button, } from '@mui/material';
4
4
  import { memo, useCallback, useEffect, useMemo, useState } from 'react';
5
5
  import { getPreferredVersionOpts, transformVersions } from './version';
6
6
  import { OsTypeSelector } from './OsTypeSelector';
@@ -10,9 +10,14 @@ import { MUILinkWithTracking } from '../MUILinkWithTracking';
10
10
  import { FALLBACK_LOGO_UNKNOWN_DEVICE } from './utils';
11
11
  import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
12
12
  import { faChevronRight, faEye, faEyeSlash, faQuestionCircle, faTriangleExclamation, } from '@fortawesome/free-solid-svg-icons';
13
+ import * as semver from 'balena-semver';
13
14
  import { Callout } from '../Callout';
15
+ import { getFromLocalStorage, setToLocalStorage } from '../../utils/storage';
16
+ import { NewChip } from '../NewChip';
17
+ import { useAnalyticsContext } from '../../contexts/AnalyticsContext';
14
18
  import { token } from '../../utils/token';
15
19
  const POLL_INTERVAL_DOCS = 'https://www.balena.io/docs/reference/supervisor/bandwidth-reduction/#side-effects--warnings';
20
+ const SECURE_BOOT_AND_FULL_DISK_ENCRYPTION_DOCS = 'https://docs.balena.io/reference/OS/secure-boot-and-full-disk-encryption/overview/';
16
21
  const getCategorizedVersions = (deviceTypeOsVersions, deviceType, osType) => {
17
22
  var _a;
18
23
  const osVersions = (_a = deviceTypeOsVersions[deviceType.slug]) !== null && _a !== void 0 ? _a : [];
@@ -32,9 +37,14 @@ const lineMap = {
32
37
  sunset: 'yellow',
33
38
  outdated: 'red',
34
39
  };
40
+ export const GENERIC_X86_SLUG = 'generic-amd64';
41
+ export const GENERIC_X86_MINIMUM_SUPPORTED_SECUREBOOT_VERSION = '5.3.15';
35
42
  export const ImageForm = memo(function ImageForm({ compatibleDeviceTypes, osVersions, isInitialDefault, osType, osTypes, hasEsrVersions, model, formElement, downloadUrl, applicationId, authToken, onSelectedOsTypeChange, onChange, }) {
36
43
  var _a, _b;
44
+ const { state } = useAnalyticsContext();
37
45
  const [showAdvancedSettings, setShowAdvancedSettings] = useState(false);
46
+ const [showSecureBootConfirmationDialog, setShowSecureBootConfirmationDialog,] = useState(false);
47
+ const [dontShowSecureBootWarningAgain, setDontShowSecureBootWarningAgain] = useState(false);
38
48
  const [showPassword, setShowPassword] = useState(false);
39
49
  const [version, setVersion] = useState();
40
50
  const [variant, setVariant] = useState('prod');
@@ -42,6 +52,17 @@ export const ImageForm = memo(function ImageForm({ compatibleDeviceTypes, osVers
42
52
  const { selectionOpts, preferredSelectionOpts } = useMemo(() => getCategorizedVersions(osVersions, model.deviceType, osType), [osVersions, model.deviceType, osType]);
43
53
  const versionSelectionOpts = useMemo(() => (showAllVersions ? selectionOpts : preferredSelectionOpts), [preferredSelectionOpts, selectionOpts, showAllVersions]);
44
54
  const showAllVersionsToggle = useMemo(() => preferredSelectionOpts.length < selectionOpts.length, [preferredSelectionOpts.length, selectionOpts.length]);
55
+ const supportsSecureBoot = useMemo(() => {
56
+ return (model.deviceType.slug === GENERIC_X86_SLUG &&
57
+ semver.gte(model.version, GENERIC_X86_MINIMUM_SUPPORTED_SECUREBOOT_VERSION));
58
+ }, [model.deviceType.slug, model.version]);
59
+ const secureBootDontShowAgainKey = `${model.deviceType.slug}_secureboot_warning_do_not_show_again`;
60
+ const dismissSecureBootWarning = useCallback((accepted, dontShowAgain) => {
61
+ var _a;
62
+ setShowSecureBootConfirmationDialog(false);
63
+ (_a = state.webTracker) === null || _a === void 0 ? void 0 : _a.track('Application Add Device Modal Hide Secure Boot Warning', { accepted, dontShowAgain });
64
+ setDontShowSecureBootWarningAgain(false);
65
+ }, [state.webTracker]);
45
66
  const handleVariantChange = useCallback((newVariant) => {
46
67
  setVariant(newVariant);
47
68
  const newState = {
@@ -112,104 +133,138 @@ export const ImageForm = memo(function ImageForm({ compatibleDeviceTypes, osVers
112
133
  onChange({ deviceType: newDeviceType });
113
134
  }, [compatibleDeviceTypes, model.deviceType.slug, onChange]);
114
135
  const recommendedVersion = useMemo(() => { var _a; return (_a = versionSelectionOpts.find((v) => { var _a; return !((_a = v.knownIssueList) === null || _a === void 0 ? void 0 : _a.length); })) === null || _a === void 0 ? void 0 : _a.value; }, [versionSelectionOpts]);
115
- return (_jsxs(Stack, { action: downloadUrl, method: "post", component: "form", noValidate: true, autoComplete: "off", ref: formElement, gap: 3, children: [_jsx("input", { type: "hidden", name: "deviceType", value: model.deviceType.slug }), _jsx("input", { type: "hidden", name: "_token", value: authToken }), _jsx("input", { type: "hidden", name: "appId", value: applicationId }), _jsx("input", { type: "hidden", name: "fileType", value: ".zip" }), _jsx("input", { type: "hidden", name: "version", value: model.version }), _jsxs(Stack, { direction: "row", flexWrap: "wrap", gap: 2, children: [compatibleDeviceTypes && compatibleDeviceTypes.length > 1 && (_jsx(Autocomplete, { fullWidth: true, value: model.deviceType, options: compatibleDeviceTypes, getOptionLabel: (option) => option.name, renderOption: (props, option) => {
116
- var _a;
117
- return (_jsxs(Box, Object.assign({ component: "li" }, props, { children: [_jsx(Avatar, { variant: "square", src: (_a = option.logo) !== null && _a !== void 0 ? _a : FALLBACK_LOGO_UNKNOWN_DEVICE, sx: { mr: 3, width: '20px', height: '20px' } }), _jsx(Typography, { noWrap: true, children: option.name })] })));
118
- }, renderInput: (_a) => {
119
- var _b;
120
- var { InputProps } = _a, params = __rest(_a, ["InputProps"]);
121
- return (_jsx(TextField, Object.assign({}, params, { label: _jsxs(Stack, { direction: "row", alignItems: "center", gap: 1, children: ["Device type", _jsx(Tooltip, { title: "Applications can support any devices that share the same architecture as their default device type.", children: _jsx(FontAwesomeIcon, { icon: faQuestionCircle, color: "info", fontSize: "1rem" }) })] }), slotProps: {
122
- input: Object.assign(Object.assign({}, InputProps), { startAdornment: (_jsx(Avatar, { variant: "square", src: (_b = model.deviceType.logo) !== null && _b !== void 0 ? _b : FALLBACK_LOGO_UNKNOWN_DEVICE, sx: { mr: 3, width: '20px', height: '20px' } })) }),
123
- } })));
124
- }, onChange: (_event, value) => {
125
- if (!value) {
126
- return;
127
- }
128
- handleSelectedDeviceTypeChange(value);
129
- }, disableClearable: true,
130
- // TODO: consider whether there is a better solution than letting the width vary as you search
131
- slotProps: {
132
- popper: { sx: { width: 'fit-content' } },
133
- }, sx: { flex: 1 } })), (!isInitialDefault || osType) &&
134
- hasEsrVersions &&
135
- model.deviceType && (_jsx(OsTypeSelector, { supportedOsTypes: osTypes, hasEsrVersions: hasEsrVersions !== null && hasEsrVersions !== void 0 ? hasEsrVersions : false, selectedOsTypeSlug: osType, onSelectedOsTypeChange: onSelectedOsTypeChange }))] }), !isInitialDefault && version && (_jsxs(Stack, { direction: "row", flexWrap: "wrap", maxWidth: "100%", gap: 2, alignItems: "center", children: [_jsx(Autocomplete, { fullWidth: true, id: "e2e-download-image-versions-list", value: version, getOptionLabel: (option) => option.value, isOptionEqualToValue: (option, value) => option.value === value.value, options: versionSelectionOpts, onChange: (_event, ver) => {
136
- handleVersionChange(ver);
137
- }, placeholder: "Choose a version...", renderOption: (props, option) => (_jsx(Box, Object.assign({ component: "li" }, props, { children: _jsx(VersionSelectItem, { option: option, isRecommended: option.value === recommendedVersion }) }))), renderInput: (_a) => {
138
- var { InputProps } = _a, params = __rest(_a, ["InputProps"]);
139
- return (_jsx(TextField, Object.assign({}, params, { slotProps: {
140
- input: Object.assign(Object.assign({}, InputProps), { endAdornment: (_jsxs(_Fragment, { children: [version.value === recommendedVersion && (_jsx(Chip, { sx: { ml: 1 }, color: "green", label: "recommended" })), !!(version === null || version === void 0 ? void 0 : version.knownIssueList) && (_jsx(Tooltip, { title: version.knownIssueList, children: _jsx(FontAwesomeIcon, { icon: faTriangleExclamation, color: token('color.icon.warning') }) })), InputProps.endAdornment] })) }),
141
- }, label: "OS version" })));
142
- }, disableClearable: true, sx: { flex: 1 } }), showAllVersionsToggle && (_jsx(FormControlLabel, { control: _jsx(Checkbox, { id: "e2e-show-all-versions-check", checked: showAllVersions, onChange: handleShowAllVersions }), label: "Show outdated versions",
143
- // TODO: Find a better way to center the checkbox with the input only (without the label)
144
- sx: { mt: 3 } }))] })), _jsx(Divider, { variant: "fullWidth", flexItem: true, sx: { borderStyle: 'dashed' } }), (!isInitialDefault || !variant) && (_jsx(VariantSelector, { version: version, variant: variant, onVariantChange: (v) => {
145
- handleVariantChange(v ? 'dev' : 'prod');
146
- } })), _jsx(Divider, { variant: "fullWidth", flexItem: true, sx: { borderStyle: 'dashed' } }), _jsxs(Stack, { children: [_jsxs(FormControl, { children: [_jsx(FormLabel, { id: "network-radio-buttons-group-label", children: _jsx(Typography, { variant: "titleSm", children: "Network" }) }), _jsxs(RadioGroup, { "aria-labelledby": "network-radio-buttons-group-label", value: model.network, name: "network", onChange: (event) => {
147
- onChange({
148
- network: event.target
149
- .value,
150
- });
151
- }, children: [_jsx(FormControlLabel, { value: "ethernet", control: _jsx(Radio, {}), label: "Ethernet only" }), _jsx(FormControlLabel, { value: "wifi", control: _jsx(Radio, {}), label: "Wifi + Ethernet" })] })] }), model.network === 'wifi' && (_jsxs(Stack, { gap: 3, children: [_jsx(TextField, { value: model.wifiSsid, id: "device-wifi-ssid", slotProps: {
152
- htmlInput: {
153
- name: 'wifiSsid',
154
- autoComplete: 'wifiSsid-auto-complete',
155
- },
156
- }, onChange: (event) => {
157
- onChange({ wifiSsid: event.target.value });
158
- }, label: "WiFi SSID" }), _jsx(TextField, { type: showPassword ? 'text' : 'password', id: "device-wifi-password", value: model.wifiKey, slotProps: {
159
- htmlInput: {
160
- name: 'wifiKey',
161
- },
162
- // input and htmlInput are different https://mui.com/material-ui/api/text-field/#text-field-prop-slotProps
163
- input: {
164
- endAdornment: (_jsx(InputAdornment, { position: "end", children: _jsx(IconButton, { onClick: () => {
165
- setShowPassword((show) => !show);
166
- }, onMouseDown: (event) => {
136
+ return (_jsxs(_Fragment, { children: [_jsxs(Stack, { action: downloadUrl, method: "post", component: "form", noValidate: true, autoComplete: "off", ref: formElement, gap: 3, children: [_jsx("input", { type: "hidden", name: "deviceType", value: model.deviceType.slug }), _jsx("input", { type: "hidden", name: "_token", value: authToken }), _jsx("input", { type: "hidden", name: "appId", value: applicationId }), _jsx("input", { type: "hidden", name: "fileType", value: ".zip" }), _jsx("input", { type: "hidden", name: "version", value: model.version }), _jsxs(Stack, { direction: "row", flexWrap: "wrap", gap: 2, children: [compatibleDeviceTypes && compatibleDeviceTypes.length > 1 && (_jsx(Autocomplete, { fullWidth: true, value: model.deviceType, options: compatibleDeviceTypes, getOptionLabel: (option) => option.name, renderOption: (props, option) => {
137
+ var _a;
138
+ return (_jsxs(Box, Object.assign({ component: "li" }, props, { children: [_jsx(Avatar, { variant: "square", src: (_a = option.logo) !== null && _a !== void 0 ? _a : FALLBACK_LOGO_UNKNOWN_DEVICE, sx: { mr: 3, width: '20px', height: '20px' } }), _jsx(Typography, { noWrap: true, children: option.name })] })));
139
+ }, renderInput: (_a) => {
140
+ var _b;
141
+ var { InputProps } = _a, params = __rest(_a, ["InputProps"]);
142
+ return (_jsx(TextField, Object.assign({}, params, { label: _jsxs(Stack, { direction: "row", alignItems: "center", gap: 1, children: ["Device type", _jsx(Tooltip, { title: "Applications can support any devices that share the same architecture as their default device type.", children: _jsx(FontAwesomeIcon, { icon: faQuestionCircle, color: "info", fontSize: "1rem" }) })] }), slotProps: {
143
+ input: Object.assign(Object.assign({}, InputProps), { startAdornment: (_jsx(Avatar, { variant: "square", src: (_b = model.deviceType.logo) !== null && _b !== void 0 ? _b : FALLBACK_LOGO_UNKNOWN_DEVICE, sx: { mr: 3, width: '20px', height: '20px' } })) }),
144
+ } })));
145
+ }, onChange: (_event, value) => {
146
+ if (!value) {
147
+ return;
148
+ }
149
+ handleSelectedDeviceTypeChange(value);
150
+ }, disableClearable: true,
151
+ // TODO: consider whether there is a better solution than letting the width vary as you search
152
+ slotProps: {
153
+ popper: { sx: { width: 'fit-content' } },
154
+ }, sx: { flex: 1 } })), (!isInitialDefault || osType) &&
155
+ hasEsrVersions &&
156
+ model.deviceType && (_jsx(OsTypeSelector, { supportedOsTypes: osTypes, hasEsrVersions: hasEsrVersions !== null && hasEsrVersions !== void 0 ? hasEsrVersions : false, selectedOsTypeSlug: osType, onSelectedOsTypeChange: onSelectedOsTypeChange }))] }), !isInitialDefault && version && (_jsxs(Stack, { direction: "row", flexWrap: "wrap", maxWidth: "100%", gap: 2, alignItems: "center", children: [_jsx(Autocomplete, { fullWidth: true, id: "e2e-download-image-versions-list", value: version, getOptionLabel: (option) => option.value, isOptionEqualToValue: (option, value) => option.value === value.value, options: versionSelectionOpts, onChange: (_event, ver) => {
157
+ handleVersionChange(ver);
158
+ }, placeholder: "Choose a version...", renderOption: (props, option) => (_jsx(Box, Object.assign({ component: "li" }, props, { children: _jsx(VersionSelectItem, { option: option, isRecommended: option.value === recommendedVersion }) }))), renderInput: (_a) => {
159
+ var { InputProps } = _a, params = __rest(_a, ["InputProps"]);
160
+ return (_jsx(TextField, Object.assign({}, params, { slotProps: {
161
+ input: Object.assign(Object.assign({}, InputProps), { endAdornment: (_jsxs(_Fragment, { children: [version.value === recommendedVersion && (_jsx(Chip, { sx: { ml: 1 }, color: "green", label: "recommended" })), !!(version === null || version === void 0 ? void 0 : version.knownIssueList) && (_jsx(Tooltip, { title: version.knownIssueList, children: _jsx(FontAwesomeIcon, { icon: faTriangleExclamation, color: token('color.icon.warning') }) })), InputProps.endAdornment] })) }),
162
+ }, label: "OS version" })));
163
+ }, disableClearable: true, sx: { flex: 1 } }), showAllVersionsToggle && (_jsx(FormControlLabel, { control: _jsx(Checkbox, { id: "e2e-show-all-versions-check", checked: showAllVersions, onChange: handleShowAllVersions }), label: "Show outdated versions",
164
+ // TODO: Find a better way to center the checkbox with the input only (without the label)
165
+ sx: { mt: 3 } }))] })), _jsx(Divider, { variant: "fullWidth", flexItem: true, sx: { borderStyle: 'dashed' } }), (!isInitialDefault || !variant) && (_jsx(VariantSelector, { version: version, variant: variant, onVariantChange: (v) => {
166
+ handleVariantChange(v ? 'dev' : 'prod');
167
+ } })), _jsx(Divider, { variant: "fullWidth", flexItem: true, sx: { borderStyle: 'dashed' } }), _jsxs(Stack, { children: [_jsxs(FormControl, { children: [_jsx(FormLabel, { id: "network-radio-buttons-group-label", children: _jsx(Typography, { variant: "titleSm", children: "Network" }) }), _jsxs(RadioGroup, { "aria-labelledby": "network-radio-buttons-group-label", value: model.network, name: "network", onChange: (event) => {
168
+ onChange({
169
+ network: event.target
170
+ .value,
171
+ });
172
+ }, children: [_jsx(FormControlLabel, { value: "ethernet", control: _jsx(Radio, {}), label: "Ethernet only" }), _jsx(FormControlLabel, { value: "wifi", control: _jsx(Radio, {}), label: "Wifi + Ethernet" })] })] }), model.network === 'wifi' && (_jsxs(Stack, { gap: 3, children: [_jsx(TextField, { value: model.wifiSsid, id: "device-wifi-ssid", slotProps: {
173
+ htmlInput: {
174
+ name: 'wifiSsid',
175
+ autoComplete: 'wifiSsid-auto-complete',
176
+ },
177
+ }, onChange: (event) => {
178
+ onChange({ wifiSsid: event.target.value });
179
+ }, label: "WiFi SSID" }), _jsx(TextField, { type: showPassword ? 'text' : 'password', id: "device-wifi-password", value: model.wifiKey, slotProps: {
180
+ htmlInput: {
181
+ name: 'wifiKey',
182
+ },
183
+ // input and htmlInput are different https://mui.com/material-ui/api/text-field/#text-field-prop-slotProps
184
+ input: {
185
+ endAdornment: (_jsx(InputAdornment, { position: "end", children: _jsx(IconButton, { onClick: () => {
186
+ setShowPassword((show) => !show);
187
+ }, onMouseDown: (event) => {
188
+ event.preventDefault();
189
+ }, edge: "end", children: showPassword ? (_jsx(FontAwesomeIcon, { icon: faEyeSlash })) : (_jsx(FontAwesomeIcon, { icon: faEye })) }) })),
190
+ },
191
+ }, onChange: (event) => {
192
+ onChange({ wifiKey: event.target.value });
193
+ }, label: "Wifi Passphrase" })] }))] }), supportsSecureBoot && (_jsxs(_Fragment, { children: [_jsx(Divider, { variant: "fullWidth", sx: { my: 3, borderStyle: 'dashed' } }), _jsxs(FormControl, { children: [_jsx(FormLabel, { id: "secure-boot-and-full-disk-encryption-label", children: _jsxs(Stack, { direction: "row", alignItems: "center", gap: 1, children: [_jsx(Typography, { variant: "titleSm", children: "Secure Boot and Full Disk Encryption" }), _jsx(NewChip, { expiryTimestamp: "2025-06-01" })] }) }), _jsx(FormControlLabel, { control: _jsx(Switch, { onClick: (event) => {
194
+ if (!model.secureboot &&
195
+ !getFromLocalStorage(secureBootDontShowAgainKey)) {
167
196
  event.preventDefault();
168
- }, edge: "end", children: showPassword ? (_jsx(FontAwesomeIcon, { icon: faEyeSlash })) : (_jsx(FontAwesomeIcon, { icon: faEye })) }) })),
169
- },
170
- }, onChange: (event) => {
171
- onChange({ wifiKey: event.target.value });
172
- }, label: "Wifi Passphrase" })] }))] }), _jsx(Divider, { variant: "fullWidth", sx: { borderStyle: 'dashed' } }), _jsxs(Accordion, { disableGutters: true, elevation: 0, expanded: showAdvancedSettings, onChange: () => {
173
- setShowAdvancedSettings(!showAdvancedSettings);
174
- }, sx: {
175
- border: 'none',
176
- '&::before': {
177
- display: 'none',
178
- },
179
- [`& .${accordionSummaryClasses.expandIconWrapper}.${accordionSummaryClasses.expanded}`]: {
180
- transform: 'rotate(90deg)',
181
- },
182
- }, children: [_jsx(AccordionSummary, { expandIcon: _jsx(FontAwesomeIcon, { icon: faChevronRight }), sx: { flexDirection: 'row-reverse', gap: 2 }, children: _jsx(Typography, { variant: "titleSm", children: "Advanced settings" }) }), _jsxs(AccordionDetails, { sx: { display: 'flex', flexDirection: 'column', gap: 3 }, children: [_jsx(TextField, { value: model.appUpdatePollInterval, slotProps: {
183
- htmlInput: {
184
- name: 'appUpdatePollInterval',
185
- autoComplete: 'appUpdatePollInterval-auto-complete',
186
- },
187
- }, onChange: (event) => {
188
- onChange({
189
- appUpdatePollInterval: parseInt(event.target.value, 10),
190
- });
191
- }, label: _jsxs(Stack, { direction: "row", alignItems: "center", gap: 1, children: ["Check for updates every X minutes", ' ', _jsx(MUILinkWithTracking, { href: POLL_INTERVAL_DOCS, sx: {
192
- display: 'flex',
193
- alignItems: 'center',
194
- height: '1.5rem',
195
- gap: 1,
196
- }, children: _jsx(FontAwesomeIcon, { icon: faFileLines, fontSize: "1.15rem" }) })] }) }), _jsx(TextField, { name: "provisioningKeyName", value: (_a = model.provisioningKeyName) !== null && _a !== void 0 ? _a : '', slotProps: {
197
- htmlInput: {
198
- name: 'provisioningKeyName',
199
- autoComplete: 'provisioningKeyName-auto-complete',
200
- },
201
- }, onChange: (event) => {
202
- onChange({ provisioningKeyName: event.target.value });
203
- }, label: "Provisioning Key name" }), _jsx(TextField, { type: "date", value: (_b = model.provisioningKeyExpiryDate) !== null && _b !== void 0 ? _b : '', slotProps: {
204
- htmlInput: {
205
- name: 'provisioningKeyExpiryDate',
206
- autoComplete: 'provisioningKeyExpiryDate-auto-complete',
207
- },
208
- }, onChange: (event) => {
209
- onChange({ provisioningKeyExpiryDate: event.target.value });
210
- }, label: "Provisioning Key expiring on" })] })] })] }));
197
+ setShowSecureBootConfirmationDialog(true);
198
+ if (state.webTracker) {
199
+ state.webTracker.track('Application Add Device Modal Show Secure Boot Warning');
200
+ }
201
+ }
202
+ }, onChange: (event) => {
203
+ onChange({ secureboot: event.target.checked });
204
+ }, checked: model.secureboot }), label: _jsxs(Stack, { direction: "row", children: [_jsx(Typography, { children: "Enable Secure Boot and Full Disk Encryption" }), _jsx(MUILinkWithTracking, { eventProperties: {
205
+ source: 'Application Add Device Modal Secure Boot Doc Icon',
206
+ }, href: SECURE_BOOT_AND_FULL_DISK_ENCRYPTION_DOCS, sx: {
207
+ display: 'flex',
208
+ alignItems: 'center',
209
+ height: '1.5rem',
210
+ }, children: _jsx(FontAwesomeIcon, { icon: faFileLines, fontSize: "1.15rem" }) })] }) })] })] })), _jsx(Divider, { variant: "fullWidth", sx: { borderStyle: 'dashed' } }), _jsxs(Accordion, { disableGutters: true, elevation: 0, expanded: showAdvancedSettings, onChange: () => {
211
+ setShowAdvancedSettings(!showAdvancedSettings);
212
+ }, sx: {
213
+ border: 'none',
214
+ '&::before': {
215
+ display: 'none',
216
+ },
217
+ [`& .${accordionSummaryClasses.expandIconWrapper}.${accordionSummaryClasses.expanded}`]: {
218
+ transform: 'rotate(90deg)',
219
+ },
220
+ }, children: [_jsx(AccordionSummary, { expandIcon: _jsx(FontAwesomeIcon, { icon: faChevronRight }), sx: { flexDirection: 'row-reverse', gap: 2 }, children: _jsx(Typography, { variant: "titleSm", children: "Advanced settings" }) }), _jsxs(AccordionDetails, { sx: { display: 'flex', flexDirection: 'column', gap: 3 }, children: [_jsx(TextField, { value: model.appUpdatePollInterval, slotProps: {
221
+ htmlInput: {
222
+ name: 'appUpdatePollInterval',
223
+ autoComplete: 'appUpdatePollInterval-auto-complete',
224
+ },
225
+ }, onChange: (event) => {
226
+ onChange({ appUpdatePollInterval: Number(event.target.value) });
227
+ }, label: _jsxs(Stack, { direction: "row", alignItems: "center", gap: 1, children: ["Check for updates every X minutes", ' ', _jsx(MUILinkWithTracking, { eventProperties: {
228
+ source: 'Application Add Device Modal Poll Interval Doc Icon',
229
+ }, href: POLL_INTERVAL_DOCS, sx: {
230
+ display: 'flex',
231
+ alignItems: 'center',
232
+ height: '1.5rem',
233
+ }, children: _jsx(FontAwesomeIcon, { icon: faFileLines, fontSize: "1.15rem" }) })] }) }), _jsx(TextField, { name: "provisioningKeyName", value: (_a = model.provisioningKeyName) !== null && _a !== void 0 ? _a : '', slotProps: {
234
+ htmlInput: {
235
+ name: 'provisioningKeyName',
236
+ autoComplete: 'provisioningKeyName-auto-complete',
237
+ },
238
+ }, onChange: (event) => {
239
+ onChange({ provisioningKeyName: event.target.value });
240
+ }, label: "Provisioning Key name" }), _jsx(TextField, { type: "date", value: (_b = model.provisioningKeyExpiryDate) !== null && _b !== void 0 ? _b : '', slotProps: {
241
+ htmlInput: {
242
+ name: 'provisioningKeyExpiryDate',
243
+ autoComplete: 'provisioningKeyExpiryDate-auto-complete',
244
+ },
245
+ }, onChange: (event) => {
246
+ onChange({ provisioningKeyExpiryDate: event.target.value });
247
+ }, label: "Provisioning Key expiring on" })] })] })] }), _jsxs(Dialog, { open: showSecureBootConfirmationDialog, onClose: () => {
248
+ dismissSecureBootWarning(false, dontShowSecureBootWarningAgain);
249
+ }, children: [_jsx(DialogTitle, { children: "Enabling Secure Boot and Full Disk Encryption" }), _jsx(DialogContent, { children: _jsxs(Stack, { children: ["Enabling Secure Boot and Full Disk Encryption has important implications:", _jsxs("ul", { children: [_jsxs("li", { children: ["The image you are about to download is", ' ', _jsx(MUILinkWithTracking, { eventProperties: {
250
+ source: "Application Add Device Modal Secure Boot Warning signed with balena's main platform key Link",
251
+ }, href: "https://docs.balena.io/reference/OS/secure-boot-and-full-disk-encryption/overview/#keys-and-certificates-in-secure-boot", children: "signed with balena's main platform key" })] }), _jsx("li", { children: "Full Disk Encryption (FDE): All data on the disk will be encrypted with a unique key per device, ensuring that data extraction or retrieval from a powered-off device is impossible." }), _jsxs("li", { children: ["Secure Boot: Ensures only OS images signed by balena can unlock the disks and access data stored on the device. Hardened Mode Limitations:", _jsxs("ul", { children: [_jsx("li", { children: "Unsigned kernel modules cannot be loaded." }), _jsx("li", { children: "Boot parameters cannot be modified." }), _jsx("li", { children: "Debugging early boot processes is practically impossible." })] })] })] }), _jsxs(Typography, { children: ["If you need to load out-of-tree kernel drivers or require a unique signing key,", ' ', _jsx(MUILinkWithTracking, { eventProperties: {
252
+ source: 'Application Add Device Modal Secure Boot Warning Contact Us Link',
253
+ }, href: "mailto:sales@balena.io", target: "_self", children: "contact us" }), ' ', "to discuss your specific requirements."] }), _jsx(MUILinkWithTracking, { eventProperties: {
254
+ source: 'Application Add Device Modal Secure Boot Warning Learn More Link',
255
+ }, href: "https://docs.balena.io/reference/OS/secure-boot-and-full-disk-encryption/overview/", children: "Learn more about Secure Boot and Full Disk Encryption." }), _jsx(FormControlLabel, { control: _jsx(Checkbox, { onChange: (event) => {
256
+ setDontShowSecureBootWarningAgain(event.target.checked);
257
+ } }), label: "Don't show me this warning again for this device type", sx: { mt: 2 } })] }) }), _jsxs(DialogActions, { children: [_jsx(Button, { "aria-label": "Go back and do not acknowledge Secure Boot and Full Disk Encryption warning", onClick: () => {
258
+ dismissSecureBootWarning(false, dontShowSecureBootWarningAgain);
259
+ }, variant: "outlined", color: "secondary", children: "Cancel" }), _jsx(Button, { "aria-label": "Acknowledge Secure Boot and Full Disk Encryption warning", onClick: () => {
260
+ onChange({ secureboot: true });
261
+ if (dontShowSecureBootWarningAgain) {
262
+ setToLocalStorage(secureBootDontShowAgainKey, 'true');
263
+ }
264
+ dismissSecureBootWarning(true, dontShowSecureBootWarningAgain);
265
+ }, children: "I understand and acknowledge" })] })] })] }));
211
266
  });
212
267
  // TODO: We need a better way than just copying the styling. Consider creating a component to export
213
268
  const VersionSelectItem = ({ option, isRecommended, }) => {
214
- return (_jsxs(Stack, { direction: "column", flexWrap: "wrap", maxWidth: "100%", rowGap: 1, children: [_jsxs(Typography, { noWrap: true, maxWidth: "100%", variant: "titleSm", children: [option.title, !!option.line && (_jsx(Chip, { sx: { ml: 1 }, label: option.line, color: lineMap[option.line] })), isRecommended && (_jsx(Chip, { sx: { ml: 1 }, color: "green", label: "recommended" }))] }), !!option.knownIssueList && (_jsx(Callout, { severity: "warning", size: "sm", children: option.knownIssueList }))] }));
269
+ return (_jsxs(Stack, { flexWrap: "wrap", maxWidth: "100%", rowGap: 1, children: [_jsxs(Typography, { noWrap: true, maxWidth: "100%", variant: "titleSm", children: [option.title, !!option.line && (_jsx(Chip, { sx: { ml: 1 }, label: option.line, color: lineMap[option.line] })), isRecommended && (_jsx(Chip, { sx: { ml: 1 }, color: "green", label: "recommended" }))] }), !!option.knownIssueList && (_jsx(Callout, { severity: "warning", size: "sm", children: option.knownIssueList }))] }));
215
270
  };
@@ -11,6 +11,7 @@ export interface DownloadImageFormModel {
11
11
  deviceType: DeviceType;
12
12
  version: string;
13
13
  network: 'ethernet' | 'wifi';
14
+ secureboot: boolean;
14
15
  developmentMode?: boolean;
15
16
  appUpdatePollInterval?: number;
16
17
  wifiSsid?: string;
@@ -2,7 +2,7 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
2
2
  import { Avatar, DialogActions, DialogContent, Divider, Grid2 as Grid, Stack, Typography, } from '@mui/material';
3
3
  import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
4
4
  import { FALLBACK_LOGO_UNKNOWN_DEVICE, isUrlAccessible, stripVersionBuild, } from './utils';
5
- import { ImageForm } from './ImageForm';
5
+ import { GENERIC_X86_MINIMUM_SUPPORTED_SECUREBOOT_VERSION, GENERIC_X86_SLUG, ImageForm, } from './ImageForm';
6
6
  import { ApplicationInstructions } from './ApplicationInstructions';
7
7
  import { DropDownButton } from '../DropDownButton';
8
8
  import pickBy from 'lodash/pickBy';
@@ -16,6 +16,7 @@ import { Callout } from '../Callout';
16
16
  import { Spinner } from '../Spinner';
17
17
  import { faDownload } from '@fortawesome/free-solid-svg-icons';
18
18
  import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
19
+ import * as semver from 'balena-semver';
19
20
  const etcherLogoBase64 = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iODAwIiBoZWlnaHQ9IjYwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KIDxnPgogIDx0aXRsZT5FdGNoZXI8L3RpdGxlPgogIDxnIGlkPSJzdmdfMSIgc3Ryb2tlPSJudWxsIj4KICAgPHBhdGggaWQ9InN2Z18yIiBjbGFzcz0ic3QxIiBkPSJtNDEyLjkwMzgzLDM1OC4wNjcxM2wwLDE3MS40OTU4M2M3LjQ5MjU0LC0xLjY2NTAxIDE0LjE1MjU3LC0zLjMzMDAyIDIwLjgxMjYsLTcuNDkyNTRsMTQyLjM1ODE5LC04MS41ODUzOWMyMC44MTI2LC0xMS42NTUwNiAzMy4zMDAxNiwtMzQuMTMyNjYgMzMuMzAwMTYsLTU4LjI3NTI4bDAsLTE2Mi4zMzgyOGMwLC02LjY2MDAzIC0wLjgzMjUsLTEzLjMyMDA2IC0zLjMzMDAyLC0xOS4xNDc1OWwtMTU0LjAxMzI0LDg5LjA3NzkzYy0zMi40Njc2NiwyMi40Nzc2MSAtMzkuMTI3NjksNDMuMjkwMjEgLTM5LjEyNzY5LDY4LjI2NTMzbDAsLTAuMDAwMDF6IiBmaWxsPSIjQTVERTM3IiBzdHJva2U9Im51bGwiLz4KICAgPHBhdGggaWQ9InN2Z18zIiBjbGFzcz0ic3QyIiBkPSJtNjYyLjY1NTAzLDE2Ny40MjM3MWwtNTYuNjEwMjcsMzIuNDY3NjZjMS42NjUwMSw1LjgyNzUzIDMuMzMwMDIsMTIuNDg3NTYgMy4zMzAwMiwxOS4xNDc1OWwwLDE2My4xNzA3OWMwLDI0LjE0MjYyIC0xMy4zMjAwNiw0Ni42MjAyMiAtMzMuMzAwMTYsNTguMjc1MjhsLTE0Mi4zNTgxOSw4MS41ODUzOWMtNi42NjAwMywzLjMzMDAyIC0xMy4zMjAwNiw1LjgyNzUzIC0yMC44MTI2LDcuNDkyNTRsMCw2NC45MzUzMWM5Ljk5MDA1LC0xLjY2NTAxIDE5Ljk4MDEsLTQuOTk1MDIgMjguMzA1MTQsLTkuOTkwMDVsMTg0LjgxNTg5LC0xMDUuNzI4MDFjMjUuODA3NjIsLTE0Ljk4NTA3IDQxLjYyNTIsLTQyLjQ1NzcgNDEuNjI1MiwtNzIuNDI3ODVsMCwtMjExLjQ1NjAyYzAsLTkuMTU3NTQgLTEuNjY1MDEsLTE4LjMxNTA5IC00Ljk5NTAyLC0yNy40NzI2M2wtMC4wMDAwMSwweiIgZmlsbD0iI0M4RjE3OCIgc3Ryb2tlPSJudWxsIi8+CiAgIDxwYXRoIGlkPSJzdmdfNCIgY2xhc3M9InN0MSIgZD0ibTM5OS41ODM3NiwzMDMuOTU0MzZjOC4zMjUwNCwtMTMuMzIwMDYgMjAuODEyNiwtMjUuODA3NjIgMzkuMTI3NjksLTM2LjYzMDE4bDE1NS42NzgyNSwtODkuOTEwNDNjLTQuOTk1MDIsLTYuNjYwMDMgLTExLjY1NTA2LC0xMi40ODc1NiAtMTguMzE1MDksLTE2LjY1MDA4bC0xNDIuMzU4MTksLTgxLjU4NTM5Yy0yMC44MTI2LC0xMS42NTUwNiAtNDYuNjIwMjIsLTExLjY1NTA2IC02Ny40MzI4MiwwbC0xNDEuNTI1NjgsODEuNTg1MzljLTcuNDkyNTQsNC4xNjI1MiAtMTMuMzIwMDYsOS45OTAwNSAtMTkuMTQ3NTksMTYuNjUwMDhsMTU0Ljg0NTc1LDg5LjkxMDQzYzE4LjMxNTA5LDExLjY1NTA2IDMwLjgwMjY1LDIzLjMxMDExIDM5LjEyNzY5LDM2LjYzMDE4bC0wLjAwMDAxLDB6IiBmaWxsPSIjQTVERTM3IiBzdHJva2U9Im51bGwiLz4KICAgPHBhdGggaWQ9InN2Z181IiBjbGFzcz0ic3QyIiBkPSJtMjI0Ljc1NzkyLDE2MS41OTYxOGwxNDEuNTI1NjgsLTgxLjU4NTM5YzIwLjgxMjYsLTExLjY1NTA2IDQ2LjYyMDIyLC0xMS42NTUwNiA2Ny40MzI4MiwwbDE0Mi4zNTgxOSw4MS41ODUzOWM3LjQ5MjU0LDQuMTYyNTIgMTMuMzIwMDYsOS45OTAwNSAxOC4zMTUwOSwxNi42NTAwOGw1Ni42MTAyNywtMzIuNDY3NjZjLTYuNjYwMDMsLTkuMTU3NTQgLTE0Ljk4NTA3LC0xNi42NTAwOCAtMjQuOTc1MTIsLTIxLjY0NTFsLTE4NC44MTU4OSwtMTA3LjM5MzAyYy0yNS44MDc2MiwtMTQuOTg1MDcgLTU3LjQ0Mjc4LC0xNC45ODUwNyAtODMuMjUwNCwwbC0xODMuMTUwODgsMTA2LjU2MDUxYy05Ljk5MDA1LDUuODI3NTMgLTE4LjMxNTA5LDEzLjMyMDA2IC0yNC45NzUxMiwyMi40Nzc2MWw1Ni42MTAyNywzMi40Njc2NmM0LjE2MjUyLC02LjY2MDAzIDEwLjgyMjU1LC0xMi40ODc1NiAxOC4zMTUwOSwtMTYuNjUwMDh6IiBmaWxsPSIjQzhGMTc4IiBzdHJva2U9Im51bGwiLz4KICAgPHBhdGggaWQ9InN2Z182IiBjbGFzcz0ic3QyIiBkPSJtMzY2LjI4MzYsNTIyLjA3MDQxbC0xNDEuNTI1NjgsLTgxLjU4NTM5Yy0yMC44MTI2LC0xMS42NTUwNiAtMzMuMzAwMTYsLTM0LjEzMjY2IC0zMy4zMDAxNiwtNTguMjc1MjhsMCwtMTYzLjE3MDc5YzAsLTYuNjYwMDMgMC44MzI1LC0xMi40ODc1NiAyLjQ5NzUxLC0xOC4zMTUwOWwtNTYuNjEwMjcsLTMyLjQ2NzY2Yy0zLjMzMDAyLDkuMTU3NTQgLTQuOTk1MDIsMTcuNDgyNTggLTQuOTk1MDIsMjYuNjQwMTNsMCwyMTIuMjg4NTJjMCwyOS45NzAxNCAxNS44MTc1OCw1Ny40NDI3OCA0MS42MjUyLDcxLjU5NTM0bDE4My45ODMzOSwxMDUuNzI4MDFjOC4zMjUwNCw0Ljk5NTAyIDE4LjMxNTA5LDguMzI1MDQgMjguMzA1MTQsOS45OTAwNWwwLC02NC45MzUzMWMtNi42NjAwMywtMC44MzI1IC0xMy4zMjAwNiwtMy4zMzAwMiAtMTkuOTgwMSwtNy40OTI1NGwtMC4wMDAwMSwwLjAwMDAxeiIgZmlsbD0iI0M4RjE3OCIgc3Ryb2tlPSJudWxsIi8+CiAgIDxwYXRoIGlkPSJzdmdfNyIgY2xhc3M9InN0MSIgZD0ibTM0Ny4xMzYwMSwyODguOTY5MjlsLTE1My4xODA3NCwtODguMjQ1NDJjLTEuNjY1MDEsNS44Mjc1MyAtMi40OTc1MSwxMi40ODc1NiAtMi40OTc1MSwxOC4zMTUwOWwwLDE2My4xNzA3OWMwLDI0LjE0MjYyIDEyLjQ4NzU2LDQ2LjYyMDIyIDMzLjMwMDE2LDU4LjI3NTI4bDE0MS41MjU2OCw4MS41ODUzOWM2LjY2MDAzLDMuMzMwMDIgMTMuMzIwMDYsNS44Mjc1MyAyMC44MTI2LDcuNDkyNTRsMCwtMTcxLjQ5NTgzYy0wLjgzMjUsLTI0Ljk3NTEyIC03LjQ5MjU0LC00NS43ODc3MiAtMzkuOTYwMTksLTY5LjA5NzgzbDAsLTAuMDAwMDF6IiBmaWxsPSIjQTVERTM3IiBzdHJva2U9Im51bGwiLz4KICA8L2c+CiA8L2c+Cgo8L3N2Zz4=';
20
21
  const ETCHER_OPEN_IMAGE_URL = 'https://www.balena.io/etcher/open-image-url';
21
22
  export var ActionType;
@@ -65,6 +66,7 @@ const getInitialState = (deviceType, applicationId, releaseId) => ({
65
66
  deviceType,
66
67
  version: '',
67
68
  network: 'ethernet',
69
+ secureboot: false,
68
70
  developmentMode: false,
69
71
  appUpdatePollInterval: 10,
70
72
  wifiSsid: undefined,
@@ -249,7 +251,11 @@ export const DownloadImageDialog = ({ open, applicationId, releaseId, compatible
249
251
  newFormModelState = getInitialState(updatedProps.deviceType, applicationId, releaseId);
250
252
  }
251
253
  else {
252
- newFormModelState = Object.assign(Object.assign({}, formModel), updatedProps);
254
+ newFormModelState = Object.assign(Object.assign(Object.assign({}, formModel), { secureboot: 'version' in updatedProps &&
255
+ formModel.deviceType.slug === GENERIC_X86_SLUG &&
256
+ semver.lt(updatedProps['version'], GENERIC_X86_MINIMUM_SUPPORTED_SECUREBOOT_VERSION)
257
+ ? false
258
+ : formModel.secureboot }), updatedProps);
253
259
  onFieldChange === null || onFieldChange === void 0 ? void 0 : onFieldChange(newFormModelState);
254
260
  }
255
261
  setFormModel(newFormModelState);
@@ -276,7 +282,7 @@ export const DownloadImageDialog = ({ open, applicationId, releaseId, compatible
276
282
  dockerImage: formModel.version
277
283
  ? getDockerArtifact(formModel.deviceType.slug, stripVersionBuild(formModel.version))
278
284
  : '',
279
- } })] }), ((_d = formModel.deviceType.imageDownloadAlerts) !== null && _d !== void 0 ? _d : []).map((alert) => {
285
+ }, secureboot: formModel.secureboot })] }), ((_d = formModel.deviceType.imageDownloadAlerts) !== null && _d !== void 0 ? _d : []).map((alert) => {
280
286
  return (_jsx(Grid, { pt: 0, size: 12, children: _jsx(Callout, { severity: alert.type, children: alert.message }, alert.message) }, alert.message));
281
287
  })] }) }) }), _jsx(DialogActions, { children: _jsx(DropDownButton, { className: "e2e-download-image-submit", items: actions, disabled: isValidatingUrl }) })] }));
282
288
  };
@@ -8,7 +8,6 @@ import { useQuery } from '@tanstack/react-query';
8
8
  import { Spinner } from '../../Spinner';
9
9
  import { TagManagementDialog } from '../../TagManagementDialog';
10
10
  export const Tags = ({ selected, rjstContext, schema, setIsBusyMessage, refresh, onDone, }) => {
11
- var _a;
12
11
  const { t } = useTranslation();
13
12
  const { sdk, internalPineFilter, checkedState } = rjstContext;
14
13
  const getAllTags = (sdk === null || sdk === void 0 ? void 0 : sdk.tags) && 'getAll' in sdk.tags ? sdk.tags.getAll : null;
@@ -82,10 +81,7 @@ export const Tags = ({ selected, rjstContext, schema, setIsBusyMessage, refresh,
82
81
  if (!rjstContext.tagField || !rjstContext.nameField || !items) {
83
82
  return null;
84
83
  }
85
- const tagSchema = (_a = schema.properties) === null || _a === void 0 ? void 0 : _a[rjstContext.tagField];
86
- return (_jsx(Spinner, { show: isPending, sx: { width: '100%', height: '100%' }, children: _jsx(TagManagementDialog, { items: items, itemType: rjstContext.resource, titleField: getItemName !== null && getItemName !== void 0 ? getItemName : rjstContext.nameField, tagField: rjstContext.tagField, tagSchema: tagSchema != null && typeof tagSchema === 'object'
87
- ? tagSchema
88
- : undefined, done: async (tagSubmitInfo) => {
84
+ return (_jsx(Spinner, { show: isPending, sx: { width: '100%', height: '100%' }, children: _jsx(TagManagementDialog, { items: items, itemType: rjstContext.resource, titleField: getItemName !== null && getItemName !== void 0 ? getItemName : rjstContext.nameField, tagField: rjstContext.tagField, done: async (tagSubmitInfo) => {
89
85
  await changeTags(tagSubmitInfo);
90
86
  onDone();
91
87
  }, cancel: () => {
@@ -1,16 +1,11 @@
1
1
  import type { ResourceTagInfo } from './models';
2
- import type { JSONSchema7 as JSONSchema } from 'json-schema';
2
+ import type { TFunction } from '../../hooks/useTranslations';
3
3
  interface AddTagFormProps<T> {
4
+ t: TFunction;
4
5
  itemType: string;
5
- /**
6
- * This is atm only used for constraint validation,
7
- * but in the future it would be great if this becomes mandatory
8
- * and we use an autogenerated form.
9
- */
10
- schema?: JSONSchema;
11
6
  existingTags: Array<ResourceTagInfo<T>>;
12
7
  overridableTags?: Array<ResourceTagInfo<T>>;
13
8
  addTag: (tag: ResourceTagInfo<T>) => void;
14
9
  }
15
- export declare const AddTagForm: <T extends object>({ itemType, schema, existingTags, overridableTags, addTag, }: AddTagFormProps<T>) => import("react/jsx-runtime").JSX.Element;
10
+ export declare const AddTagForm: <T extends object>({ itemType, existingTags, overridableTags, addTag, }: AddTagFormProps<T>) => import("react/jsx-runtime").JSX.Element;
16
11
  export {};
@@ -10,63 +10,33 @@ import { stopKeyDownEvent, withPreventDefault, } from '../../utils/eventHandling
10
10
  import { SimpleConfirmationDialog, } from '../SimpleConfirmationDialog';
11
11
  import { useRandomUUID } from '../../hooks/useRandomUUID';
12
12
  const RESERVED_NAMESPACES = ['io.resin.', 'io.balena.'];
13
- const newTagValidationRules = (t, schema, existingTags, key, value) => {
14
- var _a, _b;
15
- const tagKeySchema = (_a = schema === null || schema === void 0 ? void 0 : schema.properties) === null || _a === void 0 ? void 0 : _a.tag_key;
16
- const tagValueSchema = (_b = schema === null || schema === void 0 ? void 0 : schema.properties) === null || _b === void 0 ? void 0 : _b.value;
17
- const tagKeyMaxLength = tagKeySchema != null && typeof tagKeySchema === 'object'
18
- ? tagKeySchema.maxLength
19
- : null;
20
- const tagValueMaxLength = tagValueSchema != null && typeof tagValueSchema === 'object'
21
- ? tagValueSchema.maxLength
22
- : null;
13
+ const newTagValidationRules = (t, key, existingTags) => {
23
14
  return [
24
15
  {
25
16
  test: () => !key || isEmpty(key),
26
- field: 'tag_key',
27
17
  message: t('fields_errors.tag_name_cannot_be_empty'),
28
18
  },
29
19
  {
30
20
  test: () => /\s/.test(key),
31
- field: 'tag_key',
32
21
  message: t('fields_errors.tag_names_cannot_contain_whitespace'),
33
22
  },
34
23
  {
35
24
  test: () => RESERVED_NAMESPACES.some((reserved) => startsWith(key, reserved)),
36
- field: 'tag_key',
37
25
  message: t(`fields_errors.some_tag_keys_are_reserved`, {
38
26
  namespace: RESERVED_NAMESPACES.join(', '),
39
27
  }),
40
28
  },
41
29
  {
42
30
  test: () => existingTags.some((tag) => tag.state !== 'deleted' && tag.tag_key === key),
43
- field: 'tag_key',
44
31
  message: t('fields_errors.tag_with_same_name_exists'),
45
32
  },
46
- ...(tagKeyMaxLength != null
47
- ? [
48
- {
49
- test: () => key.length > tagKeyMaxLength,
50
- field: 'tag_key',
51
- message: t('fields_errors.tag_name_cannot_longer_than_maximum_characters', { maximum: tagKeyMaxLength }),
52
- },
53
- ]
54
- : []),
55
- ...(tagValueMaxLength != null
56
- ? [
57
- {
58
- test: () => value.length > tagValueMaxLength,
59
- field: 'value',
60
- message: t('fields_errors.tag_value_cannot_longer_than_maximum_characters', { maximum: tagValueMaxLength }),
61
- },
62
- ]
63
- : []),
64
33
  ];
65
34
  };
66
- export const AddTagForm = ({ itemType, schema, existingTags, overridableTags = [], addTag, }) => {
35
+ export const AddTagForm = ({ itemType, existingTags, overridableTags = [], addTag, }) => {
67
36
  const { t } = useTranslation();
68
37
  const [tagKey, setTagKey] = React.useState('');
69
38
  const [value, setValue] = React.useState('');
39
+ const [tagKeyIsInvalid, setTagKeyIsInvalid] = React.useState(false);
70
40
  const [error, setError] = React.useState();
71
41
  const [canSubmit, setCanSubmit] = React.useState(false);
72
42
  const [confirmationDialogOptions, setConfirmationDialogOptions] = React.useState();
@@ -75,8 +45,9 @@ export const AddTagForm = ({ itemType, schema, existingTags, overridableTags = [
75
45
  const formId = useRandomUUID();
76
46
  const formUuid = `add-tag-form-${formId}`;
77
47
  const checkNewTagValidity = (key) => {
78
- const failedRule = newTagValidationRules(t, schema, existingTags, key, value).find((rule) => rule.test());
48
+ const failedRule = newTagValidationRules(t, key, existingTags).find((rule) => rule.test());
79
49
  const hasErrors = !!failedRule;
50
+ setTagKeyIsInvalid(hasErrors);
80
51
  setError(failedRule);
81
52
  setCanSubmit(!hasErrors);
82
53
  return hasErrors;
@@ -121,6 +92,7 @@ export const AddTagForm = ({ itemType, schema, existingTags, overridableTags = [
121
92
  });
122
93
  setTagKey('');
123
94
  setValue('');
95
+ setTagKeyIsInvalid(false);
124
96
  setError(undefined);
125
97
  setCanSubmit(false);
126
98
  if (tagKeyInput === null || tagKeyInput === void 0 ? void 0 : tagKeyInput.current) {
@@ -140,13 +112,13 @@ export const AddTagForm = ({ itemType, schema, existingTags, overridableTags = [
140
112
  }, fullWidth: true, ref: tagKeyInput, onChange: (e) => {
141
113
  setTagKey(e.target.value);
142
114
  checkNewTagValidity(e.target.value);
143
- }, value: tagKey, error: (error === null || error === void 0 ? void 0 : error.field) === 'tag_key', placeholder: t('labels.tag_name') }), _jsx(TextField, { slotProps: {
115
+ }, value: tagKey, error: tagKeyIsInvalid, placeholder: t('labels.tag_name') }), _jsx(TextField, { slotProps: {
144
116
  htmlInput: {
145
117
  form: formUuid,
146
118
  },
147
119
  }, fullWidth: true, ref: valueInput, onChange: (e) => {
148
120
  setValue(e.target.value);
149
- }, value: value, error: (error === null || error === void 0 ? void 0 : error.field) === 'value', placeholder: t('labels.value') }), _jsx("form", { id: formUuid, onSubmit: internalAddTag, children: _jsx(Button, { sx: {
121
+ }, value: value, placeholder: t('labels.value') }), _jsx("form", { id: formUuid, onSubmit: internalAddTag, children: _jsx(Button, { sx: {
150
122
  width: 120,
151
123
  }, onClick: internalAddTag, disabled: !canSubmit, children: t('actions.add_tag') }) }), confirmationDialogOptions && (_jsx(SimpleConfirmationDialog, Object.assign({}, confirmationDialogOptions)))] }), error && _jsx(Callout, { severity: "danger", children: error.message })] }));
152
124
  };
@@ -1,4 +1,3 @@
1
- import type { JSONSchema7 as JSONSchema } from 'json-schema';
2
1
  import type { ResourceTagSubmitInfo, SubmitInfo, TaggedResource } from './models';
3
2
  export interface TagManagementDialogProps<T> {
4
3
  /** Selected items to tag */
@@ -9,11 +8,9 @@ export interface TagManagementDialogProps<T> {
9
8
  titleField: keyof T | ((item: T) => string);
10
9
  /** Tags property in the selected item */
11
10
  tagField: keyof T;
12
- /** The schema of the tag resource */
13
- tagSchema?: JSONSchema;
14
11
  /** On cancel press event */
15
12
  cancel: () => void;
16
13
  /** On done press event */
17
14
  done: (tagSubmitInfo: SubmitInfo<ResourceTagSubmitInfo, ResourceTagSubmitInfo>) => void;
18
15
  }
19
- export declare const TagManagementDialog: <T extends TaggedResource>({ items, itemType, titleField, tagField, tagSchema, cancel, done, }: TagManagementDialogProps<T>) => import("react/jsx-runtime").JSX.Element | null;
16
+ export declare const TagManagementDialog: <T extends TaggedResource>({ items, itemType, titleField, tagField, cancel, done, }: TagManagementDialogProps<T>) => import("react/jsx-runtime").JSX.Element | null;
@@ -98,16 +98,11 @@ const PreviousTagProperty = styled(TagProperty) `
98
98
  }
99
99
  }
100
100
  `;
101
- export const TagManagementDialog = ({ items, itemType, titleField, tagField, tagSchema, cancel, done, }) => {
102
- var _a;
101
+ export const TagManagementDialog = ({ items, itemType, titleField, tagField, cancel, done, }) => {
103
102
  const { t } = useTranslation();
104
103
  const [editingTag, setEditingTag] = React.useState();
105
104
  const [tags, setTags] = React.useState();
106
105
  const [partialTags, setPartialTags] = React.useState();
107
- const tagValueSchema = (_a = tagSchema === null || tagSchema === void 0 ? void 0 : tagSchema.properties) === null || _a === void 0 ? void 0 : _a.value;
108
- const tagValueMaxLength = tagValueSchema != null && typeof tagValueSchema === 'object'
109
- ? tagValueSchema.maxLength
110
- : null;
111
106
  const tagDiffs = React.useMemo(() => getResourceTagSubmitInfo(tags !== null && tags !== void 0 ? tags : []), [tags]);
112
107
  React.useEffect(() => {
113
108
  const allTags = groupResourcesByTags(items, tagField);
@@ -196,7 +191,7 @@ export const TagManagementDialog = ({ items, itemType, titleField, tagField, tag
196
191
  : toString(item[titleField]);
197
192
  return title || `(${t('no_data.no_name_set')})`;
198
193
  };
199
- return (_jsxs(DialogWithCloseButton, { open: true, title: _jsxs(Stack, { children: [_jsxs(Typography, { variant: "h3", mt: 0, mb: 10, children: [items.length > 1 && _jsxs("span", { children: [t('labels.shared'), " "] }), t('labels.tags')] }), _jsx(CollectionSummary, { items: items.map(getItemTitle).sort(), itemsType: t('resource.' + itemType, { count: items.length }), maxVisibleItemCount: 10 })] }), children: [_jsxs(DialogContent, { children: [_jsx(AddTagForm, { itemType: itemType, schema: tagSchema, existingTags: tags, overridableTags: partialTags, addTag: addTag }), _jsxs(Table, { children: [_jsxs(TableHead, { children: [_jsx(TableCell, {}), _jsx(TableCell, { children: t('labels.tag_name') }), _jsx(TableCell, { children: t('labels.value') }), _jsx(TableCell, {})] }), _jsx(TableBody, { children: !tags.length ? (_jsxs(TableRow, { children: [_jsx(TableCell, {}), _jsx(TableCell, { children: t(`errors.no_tags_for_selected_itemtype`, {
194
+ return (_jsxs(DialogWithCloseButton, { open: true, title: _jsxs(Stack, { children: [_jsxs(Typography, { variant: "h3", mt: 0, mb: 10, children: [items.length > 1 && _jsxs("span", { children: [t('labels.shared'), " "] }), t('labels.tags')] }), _jsx(CollectionSummary, { items: items.map(getItemTitle).sort(), itemsType: t('resource.' + itemType, { count: items.length }), maxVisibleItemCount: 10 })] }), children: [_jsxs(DialogContent, { children: [_jsx(AddTagForm, { itemType: itemType, existingTags: tags, overridableTags: partialTags, addTag: addTag, t: t }), _jsxs(Table, { children: [_jsxs(TableHead, { children: [_jsx(TableCell, {}), _jsx(TableCell, { children: t('labels.tag_name') }), _jsx(TableCell, { children: t('labels.value') }), _jsx(TableCell, {})] }), _jsx(TableBody, { children: !tags.length ? (_jsxs(TableRow, { children: [_jsx(TableCell, {}), _jsx(TableCell, { children: t(`errors.no_tags_for_selected_itemtype`, {
200
195
  count: items.length,
201
196
  itemType,
202
197
  }) })] })) : (tags.map((tag) => {
@@ -221,9 +216,7 @@ export const TagManagementDialog = ({ items, itemType, titleField, tagField, tag
221
216
  setEditingTagValue(e.target.value);
222
217
  }, onBlur: () => {
223
218
  endTagEdit();
224
- }, value: editingTag.value, placeholder: t('labels.tag_value'), inputProps: tagValueMaxLength != null
225
- ? { maxLength: tagValueMaxLength }
226
- : undefined }))] }), _jsx(TableCell, { children: tag.state && (_jsx(Button, { variant: "text", startIcon: _jsx(FontAwesomeIcon, { icon: faUndo }), onClick: () => {
219
+ }, value: editingTag.value, placeholder: t('labels.tag_value') }))] }), _jsx(TableCell, { children: tag.state && (_jsx(Button, { variant: "text", startIcon: _jsx(FontAwesomeIcon, { icon: faUndo }), onClick: () => {
227
220
  undoTagChanges(tag);
228
221
  }, sx: {
229
222
  width: UndoButtonMinWidth,
@@ -31,8 +31,6 @@ const translationMap = {
31
31
  'errors.no_tags_for_selected_itemtype_plural': 'The selected {{itemType}}s have no tags in common',
32
32
  'fields_errors.tag_name_cannot_be_empty': "The tag name can't be empty.",
33
33
  'fields_errors.tag_names_cannot_contain_whitespace': 'Tag names cannot contain whitespace',
34
- 'fields_errors.tag_name_cannot_longer_than_maximum_characters': `The tag name can't be longer than {{maximum}} characters.`,
35
- 'fields_errors.tag_value_cannot_longer_than_maximum_characters': `The tag value can't be longer than {{maximum}} characters.`,
36
34
  'fields_errors.some_tag_keys_are_reserved': 'Tag names beginning with {{namespace}} are reserved',
37
35
  'fields_errors.tag_with_same_name_exists': 'A tag with the same name already exists',
38
36
  'fields_errors.does_not_satisfy_minimum': 'Must be greater than or equal to {{minimum}}',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@balena/ui-shared-components",
3
- "version": "14.1.0-build-limit-field-lengths-2d063da12c2b1e3b691fc126191269d24d81aae1-1",
3
+ "version": "14.1.0-build-secure-boot-backup-4d430fef3bc217d76b648ea38883d1b586f61a8a-1",
4
4
  "main": "./dist/index.js",
5
5
  "sideEffects": false,
6
6
  "files": [
@@ -32,6 +32,7 @@
32
32
  "ajv-formats": "^3.0.1",
33
33
  "ajv-keywords": "^5.1.0",
34
34
  "analytics-client": "^3.1.2",
35
+ "balena-semver": "^3.0.10",
35
36
  "color": "^5.0.0",
36
37
  "color-hash": "^2.0.2",
37
38
  "date-fns": "^4.1.0",
@@ -54,7 +55,7 @@
54
55
  "zxcvbn": "^4.4.2"
55
56
  },
56
57
  "devDependencies": {
57
- "@balena/lint": "^9.3.0",
58
+ "@balena/lint": "^9.3.8",
58
59
  "@storybook/addon-docs": "^9.0.0",
59
60
  "@storybook/addon-links": "^9.0.0",
60
61
  "@storybook/addon-onboarding": "^9.0.0",
@@ -132,7 +133,7 @@
132
133
  },
133
134
  "homepage": "https://github.com/balena-io/ui-shared-components#readme",
134
135
  "versionist": {
135
- "publishedAt": "2025-09-15T12:30:23.623Z"
136
+ "publishedAt": "2025-09-30T12:29:36.790Z"
136
137
  },
137
138
  "overrides": {
138
139
  "storybook": "$storybook"