@flightctl/ui-components 1.1.0-rc2 → 1.1.0-rc3

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.
Files changed (190) hide show
  1. package/dist/types/imagebuilder/index.d.ts +1 -0
  2. package/dist/types/imagebuilder/index.d.ts.map +1 -1
  3. package/dist/types/imagebuilder/index.js +3 -1
  4. package/dist/types/imagebuilder/index.js.map +1 -1
  5. package/dist/types/imagebuilder/models/ApiVersion.d.ts +8 -0
  6. package/dist/types/imagebuilder/models/ApiVersion.d.ts.map +1 -0
  7. package/dist/types/imagebuilder/models/ApiVersion.js +16 -0
  8. package/dist/types/imagebuilder/models/ApiVersion.js.map +1 -0
  9. package/dist/types/imagebuilder/models/ImageBuild.d.ts +2 -4
  10. package/dist/types/imagebuilder/models/ImageBuild.d.ts.map +1 -1
  11. package/dist/types/imagebuilder/models/ImageBuildList.d.ts +2 -4
  12. package/dist/types/imagebuilder/models/ImageBuildList.d.ts.map +1 -1
  13. package/dist/types/imagebuilder/models/ImageExport.d.ts +2 -4
  14. package/dist/types/imagebuilder/models/ImageExport.d.ts.map +1 -1
  15. package/dist/types/imagebuilder/models/ImageExportList.d.ts +2 -4
  16. package/dist/types/imagebuilder/models/ImageExportList.d.ts.map +1 -1
  17. package/dist/types/imagebuilder/models/Status.d.ts +2 -4
  18. package/dist/types/imagebuilder/models/Status.d.ts.map +1 -1
  19. package/dist/types/index.d.ts +1 -0
  20. package/dist/types/index.d.ts.map +1 -1
  21. package/dist/types/index.js +3 -1
  22. package/dist/types/index.js.map +1 -1
  23. package/dist/types/models/ApiVersion.d.ts +9 -0
  24. package/dist/types/models/ApiVersion.d.ts.map +1 -0
  25. package/dist/types/models/ApiVersion.js +17 -0
  26. package/dist/types/models/ApiVersion.js.map +1 -0
  27. package/dist/types/models/AuthConfig.d.ts +2 -4
  28. package/dist/types/models/AuthConfig.d.ts.map +1 -1
  29. package/dist/types/models/AuthProvider.d.ts +2 -4
  30. package/dist/types/models/AuthProvider.d.ts.map +1 -1
  31. package/dist/types/models/AuthProviderList.d.ts +2 -4
  32. package/dist/types/models/AuthProviderList.d.ts.map +1 -1
  33. package/dist/types/models/CertificateSigningRequest.d.ts +2 -4
  34. package/dist/types/models/CertificateSigningRequest.d.ts.map +1 -1
  35. package/dist/types/models/CertificateSigningRequestList.d.ts +2 -4
  36. package/dist/types/models/CertificateSigningRequestList.d.ts.map +1 -1
  37. package/dist/types/models/Device.d.ts +2 -4
  38. package/dist/types/models/Device.d.ts.map +1 -1
  39. package/dist/types/models/DeviceList.d.ts +2 -4
  40. package/dist/types/models/DeviceList.d.ts.map +1 -1
  41. package/dist/types/models/EnrollmentRequest.d.ts +2 -4
  42. package/dist/types/models/EnrollmentRequest.d.ts.map +1 -1
  43. package/dist/types/models/EnrollmentRequestList.d.ts +2 -4
  44. package/dist/types/models/EnrollmentRequestList.d.ts.map +1 -1
  45. package/dist/types/models/Event.d.ts +2 -4
  46. package/dist/types/models/Event.d.ts.map +1 -1
  47. package/dist/types/models/Event.js.map +1 -1
  48. package/dist/types/models/EventList.d.ts +2 -4
  49. package/dist/types/models/EventList.d.ts.map +1 -1
  50. package/dist/types/models/Fleet.d.ts +2 -4
  51. package/dist/types/models/Fleet.d.ts.map +1 -1
  52. package/dist/types/models/FleetList.d.ts +2 -4
  53. package/dist/types/models/FleetList.d.ts.map +1 -1
  54. package/dist/types/models/Organization.d.ts +2 -4
  55. package/dist/types/models/Organization.d.ts.map +1 -1
  56. package/dist/types/models/OrganizationList.d.ts +2 -4
  57. package/dist/types/models/OrganizationList.d.ts.map +1 -1
  58. package/dist/types/models/Repository.d.ts +2 -4
  59. package/dist/types/models/Repository.d.ts.map +1 -1
  60. package/dist/types/models/RepositoryList.d.ts +2 -4
  61. package/dist/types/models/RepositoryList.d.ts.map +1 -1
  62. package/dist/types/models/ResourceSync.d.ts +2 -4
  63. package/dist/types/models/ResourceSync.d.ts.map +1 -1
  64. package/dist/types/models/ResourceSyncList.d.ts +2 -4
  65. package/dist/types/models/ResourceSyncList.d.ts.map +1 -1
  66. package/dist/types/models/Status.d.ts +2 -4
  67. package/dist/types/models/Status.d.ts.map +1 -1
  68. package/dist/types/models/TemplateVersion.d.ts +2 -4
  69. package/dist/types/models/TemplateVersion.d.ts.map +1 -1
  70. package/dist/types/models/TemplateVersionList.d.ts +2 -4
  71. package/dist/types/models/TemplateVersionList.d.ts.map +1 -1
  72. package/dist/ui-components/src/components/AuthProvider/CreateAuthProvider/utils.d.ts.map +1 -1
  73. package/dist/ui-components/src/components/AuthProvider/CreateAuthProvider/utils.js +51 -51
  74. package/dist/ui-components/src/components/AuthProvider/CreateAuthProvider/utils.js.map +1 -1
  75. package/dist/ui-components/src/components/Device/DeviceDetails/TerminalTab.d.ts.map +1 -1
  76. package/dist/ui-components/src/components/Device/DeviceDetails/TerminalTab.js +5 -1
  77. package/dist/ui-components/src/components/Device/DeviceDetails/TerminalTab.js.map +1 -1
  78. package/dist/ui-components/src/components/Device/EditDeviceWizard/deviceSpecUtils.d.ts.map +1 -1
  79. package/dist/ui-components/src/components/Device/EditDeviceWizard/deviceSpecUtils.js +3 -1
  80. package/dist/ui-components/src/components/Device/EditDeviceWizard/deviceSpecUtils.js.map +1 -1
  81. package/dist/ui-components/src/components/Device/EditDeviceWizard/steps/ApplicationHelmForm.js +1 -1
  82. package/dist/ui-components/src/components/Device/EditDeviceWizard/steps/ApplicationHelmForm.js.map +1 -1
  83. package/dist/ui-components/src/components/ErrorAlert/ErrorAlert.d.ts +4 -2
  84. package/dist/ui-components/src/components/ErrorAlert/ErrorAlert.d.ts.map +1 -1
  85. package/dist/ui-components/src/components/ErrorAlert/ErrorAlert.js +2 -2
  86. package/dist/ui-components/src/components/ErrorAlert/ErrorAlert.js.map +1 -1
  87. package/dist/ui-components/src/components/Fleet/CreateFleet/utils.d.ts +1 -1
  88. package/dist/ui-components/src/components/Fleet/CreateFleet/utils.d.ts.map +1 -1
  89. package/dist/ui-components/src/components/Fleet/CreateFleet/utils.js +2 -2
  90. package/dist/ui-components/src/components/Fleet/CreateFleet/utils.js.map +1 -1
  91. package/dist/ui-components/src/components/ImageBuilds/ConfirmImageExportModal/ConfirmImageExportModal.d.ts +3 -3
  92. package/dist/ui-components/src/components/ImageBuilds/ConfirmImageExportModal/ConfirmImageExportModal.d.ts.map +1 -1
  93. package/dist/ui-components/src/components/ImageBuilds/ConfirmImageExportModal/ConfirmImageExportModal.js +20 -13
  94. package/dist/ui-components/src/components/ImageBuilds/ConfirmImageExportModal/ConfirmImageExportModal.js.map +1 -1
  95. package/dist/ui-components/src/components/ImageBuilds/CreateImageBuildWizard/CreateImageBuildWizard.d.ts.map +1 -1
  96. package/dist/ui-components/src/components/ImageBuilds/CreateImageBuildWizard/CreateImageBuildWizard.js +4 -3
  97. package/dist/ui-components/src/components/ImageBuilds/CreateImageBuildWizard/CreateImageBuildWizard.js.map +1 -1
  98. package/dist/ui-components/src/components/ImageBuilds/CreateImageBuildWizard/steps/OutputImageStep.js +1 -1
  99. package/dist/ui-components/src/components/ImageBuilds/CreateImageBuildWizard/steps/OutputImageStep.js.map +1 -1
  100. package/dist/ui-components/src/components/ImageBuilds/CreateImageBuildWizard/steps/RegistrationStep.d.ts.map +1 -1
  101. package/dist/ui-components/src/components/ImageBuilds/CreateImageBuildWizard/steps/RegistrationStep.js +13 -10
  102. package/dist/ui-components/src/components/ImageBuilds/CreateImageBuildWizard/steps/RegistrationStep.js.map +1 -1
  103. package/dist/ui-components/src/components/ImageBuilds/CreateImageBuildWizard/steps/ReviewStep.d.ts.map +1 -1
  104. package/dist/ui-components/src/components/ImageBuilds/CreateImageBuildWizard/steps/ReviewStep.js +4 -2
  105. package/dist/ui-components/src/components/ImageBuilds/CreateImageBuildWizard/steps/ReviewStep.js.map +1 -1
  106. package/dist/ui-components/src/components/ImageBuilds/CreateImageBuildWizard/steps/SourceImageStep.d.ts.map +1 -1
  107. package/dist/ui-components/src/components/ImageBuilds/CreateImageBuildWizard/steps/SourceImageStep.js +7 -1
  108. package/dist/ui-components/src/components/ImageBuilds/CreateImageBuildWizard/steps/SourceImageStep.js.map +1 -1
  109. package/dist/ui-components/src/components/ImageBuilds/CreateImageBuildWizard/types.d.ts +3 -5
  110. package/dist/ui-components/src/components/ImageBuilds/CreateImageBuildWizard/types.d.ts.map +1 -1
  111. package/dist/ui-components/src/components/ImageBuilds/CreateImageBuildWizard/utils.d.ts +3 -2
  112. package/dist/ui-components/src/components/ImageBuilds/CreateImageBuildWizard/utils.d.ts.map +1 -1
  113. package/dist/ui-components/src/components/ImageBuilds/CreateImageBuildWizard/utils.js +139 -34
  114. package/dist/ui-components/src/components/ImageBuilds/CreateImageBuildWizard/utils.js.map +1 -1
  115. package/dist/ui-components/src/components/ImageBuilds/ImageBuildDetails/ImageBuildDetailsPage.js +1 -1
  116. package/dist/ui-components/src/components/ImageBuilds/ImageBuildDetails/ImageBuildDetailsPage.js.map +1 -1
  117. package/dist/ui-components/src/components/ImageBuilds/ImageBuildDetails/ImageBuildExportsGallery.d.ts.map +1 -1
  118. package/dist/ui-components/src/components/ImageBuilds/ImageBuildDetails/ImageBuildExportsGallery.js +25 -16
  119. package/dist/ui-components/src/components/ImageBuilds/ImageBuildDetails/ImageBuildExportsGallery.js.map +1 -1
  120. package/dist/ui-components/src/components/ImageBuilds/ImageBuildDetails/ImageBuildLogsTab.d.ts +1 -0
  121. package/dist/ui-components/src/components/ImageBuilds/ImageBuildDetails/ImageBuildLogsTab.d.ts.map +1 -1
  122. package/dist/ui-components/src/components/ImageBuilds/ImageBuildDetails/ImageBuildLogsTab.js +17 -18
  123. package/dist/ui-components/src/components/ImageBuilds/ImageBuildDetails/ImageBuildLogsTab.js.map +1 -1
  124. package/dist/ui-components/src/components/ImageBuilds/ImageExportCards.d.ts.map +1 -1
  125. package/dist/ui-components/src/components/ImageBuilds/ImageExportCards.js +22 -10
  126. package/dist/ui-components/src/components/ImageBuilds/ImageExportCards.js.map +1 -1
  127. package/dist/ui-components/src/components/Repository/CreateRepository/CreateRepositoryForm.d.ts +2 -1
  128. package/dist/ui-components/src/components/Repository/CreateRepository/CreateRepositoryForm.d.ts.map +1 -1
  129. package/dist/ui-components/src/components/Repository/CreateRepository/CreateRepositoryForm.js +9 -3
  130. package/dist/ui-components/src/components/Repository/CreateRepository/CreateRepositoryForm.js.map +1 -1
  131. package/dist/ui-components/src/components/Repository/CreateRepository/utils.d.ts.map +1 -1
  132. package/dist/ui-components/src/components/Repository/CreateRepository/utils.js +3 -4
  133. package/dist/ui-components/src/components/Repository/CreateRepository/utils.js.map +1 -1
  134. package/dist/ui-components/src/components/form/RepositorySelect.d.ts.map +1 -1
  135. package/dist/ui-components/src/components/form/RepositorySelect.js +1 -1
  136. package/dist/ui-components/src/components/form/RepositorySelect.js.map +1 -1
  137. package/dist/ui-components/src/components/form/UploadField.d.ts.map +1 -1
  138. package/dist/ui-components/src/components/form/UploadField.js +25 -16
  139. package/dist/ui-components/src/components/form/UploadField.js.map +1 -1
  140. package/dist/ui-components/src/components/form/validations.d.ts +5 -0
  141. package/dist/ui-components/src/components/form/validations.d.ts.map +1 -1
  142. package/dist/ui-components/src/components/form/validations.js +40 -23
  143. package/dist/ui-components/src/components/form/validations.js.map +1 -1
  144. package/dist/ui-components/src/components/modals/CreateRepositoryModal/CreateRepositoryModal.d.ts +2 -1
  145. package/dist/ui-components/src/components/modals/CreateRepositoryModal/CreateRepositoryModal.d.ts.map +1 -1
  146. package/dist/ui-components/src/components/modals/CreateRepositoryModal/CreateRepositoryModal.js +2 -2
  147. package/dist/ui-components/src/components/modals/CreateRepositoryModal/CreateRepositoryModal.js.map +1 -1
  148. package/dist/ui-components/src/constants.d.ts +0 -2
  149. package/dist/ui-components/src/constants.d.ts.map +1 -1
  150. package/dist/ui-components/src/constants.js +5 -5
  151. package/dist/ui-components/src/constants.js.map +1 -1
  152. package/dist/ui-components/src/hooks/useWebSocket.d.ts.map +1 -1
  153. package/dist/ui-components/src/hooks/useWebSocket.js +25 -4
  154. package/dist/ui-components/src/hooks/useWebSocket.js.map +1 -1
  155. package/dist/ui-components/src/utils/imageBuilds.d.ts.map +1 -1
  156. package/dist/ui-components/src/utils/imageBuilds.js +13 -28
  157. package/dist/ui-components/src/utils/imageBuilds.js.map +1 -1
  158. package/dist/ui-components/src/utils/search.d.ts +2 -1
  159. package/dist/ui-components/src/utils/search.d.ts.map +1 -1
  160. package/dist/ui-components/src/utils/search.js +2 -2
  161. package/dist/ui-components/src/utils/search.js.map +1 -1
  162. package/package.json +2 -2
  163. package/src/components/AuthProvider/CreateAuthProvider/utils.ts +2 -2
  164. package/src/components/Device/DeviceDetails/TerminalTab.tsx +9 -1
  165. package/src/components/Device/EditDeviceWizard/deviceSpecUtils.ts +3 -1
  166. package/src/components/Device/EditDeviceWizard/steps/ApplicationHelmForm.tsx +1 -1
  167. package/src/components/ErrorAlert/ErrorAlert.tsx +13 -3
  168. package/src/components/Fleet/CreateFleet/utils.ts +2 -3
  169. package/src/components/ImageBuilds/ConfirmImageExportModal/ConfirmImageExportModal.tsx +28 -15
  170. package/src/components/ImageBuilds/CreateImageBuildWizard/CreateImageBuildWizard.tsx +8 -3
  171. package/src/components/ImageBuilds/CreateImageBuildWizard/steps/OutputImageStep.tsx +1 -1
  172. package/src/components/ImageBuilds/CreateImageBuildWizard/steps/RegistrationStep.tsx +18 -10
  173. package/src/components/ImageBuilds/CreateImageBuildWizard/steps/ReviewStep.tsx +5 -1
  174. package/src/components/ImageBuilds/CreateImageBuildWizard/steps/SourceImageStep.tsx +13 -1
  175. package/src/components/ImageBuilds/CreateImageBuildWizard/types.ts +3 -6
  176. package/src/components/ImageBuilds/CreateImageBuildWizard/utils.ts +161 -37
  177. package/src/components/ImageBuilds/ImageBuildDetails/ImageBuildDetailsPage.tsx +1 -1
  178. package/src/components/ImageBuilds/ImageBuildDetails/ImageBuildExportsGallery.tsx +27 -18
  179. package/src/components/ImageBuilds/ImageBuildDetails/ImageBuildLogsTab.tsx +22 -26
  180. package/src/components/ImageBuilds/ImageExportCards.tsx +31 -12
  181. package/src/components/Repository/CreateRepository/CreateRepositoryForm.tsx +13 -4
  182. package/src/components/Repository/CreateRepository/utils.ts +4 -4
  183. package/src/components/form/RepositorySelect.tsx +1 -0
  184. package/src/components/form/UploadField.tsx +29 -30
  185. package/src/components/form/validations.ts +44 -24
  186. package/src/components/modals/CreateRepositoryModal/CreateRepositoryModal.tsx +3 -1
  187. package/src/constants.ts +5 -5
  188. package/src/hooks/useWebSocket.ts +25 -3
  189. package/src/utils/imageBuilds.ts +14 -32
  190. package/src/utils/search.ts +2 -2
@@ -1,12 +1,22 @@
1
1
  import * as React from 'react';
2
- import { Alert } from '@patternfly/react-core';
2
+ import { Alert, AlertActionLink } from '@patternfly/react-core';
3
3
  import { useTranslation } from '../../hooks/useTranslation';
4
4
  import { getErrorMessage } from '../../utils/error';
5
5
 
6
- const ErrorAlert = ({ error }: { error: unknown }) => {
6
+ type ErrorAlertProps = {
7
+ error: unknown;
8
+ onRetry?: VoidFunction;
9
+ };
10
+
11
+ const ErrorAlert = ({ error, onRetry }: ErrorAlertProps) => {
7
12
  const { t } = useTranslation();
8
13
  return (
9
- <Alert isInline variant="danger" title={t('Unexpected error occurred')}>
14
+ <Alert
15
+ isInline
16
+ variant="danger"
17
+ title={t('Unexpected error occurred')}
18
+ actionLinks={onRetry ? <AlertActionLink onClick={onRetry}>{t('Try again')}</AlertActionLink> : undefined}
19
+ >
10
20
  {getErrorMessage(error)}
11
21
  </Alert>
12
22
  );
@@ -1,7 +1,6 @@
1
- import { Fleet, PatchRequest } from '@flightctl/types';
2
1
  import { TFunction } from 'i18next';
3
2
  import * as Yup from 'yup';
4
- import { CORE_API_VERSION } from '../../../constants';
3
+ import { ApiVersion, Fleet, PatchRequest } from '@flightctl/types';
5
4
  import { toAPILabel } from '../../../utils/labels';
6
5
  import {
7
6
  systemdUnitListValidationSchema,
@@ -178,7 +177,7 @@ export const getFleetResource = (values: FleetFormValues): Fleet => {
178
177
  },
179
178
  };
180
179
  const fleet: Fleet = {
181
- apiVersion: CORE_API_VERSION,
180
+ apiVersion: ApiVersion.ApiVersionV1beta1,
182
181
  kind: 'Fleet',
183
182
  metadata: {
184
183
  name: values.name,
@@ -3,9 +3,9 @@ import { Button, Modal, ModalBody, ModalFooter, ModalHeader } from '@patternfly/
3
3
 
4
4
  import { useTranslation } from '../../../hooks/useTranslation';
5
5
 
6
- export type ConfirmImageExportAction = 'cancel' | 'delete';
6
+ export type ConfirmImageExportAction = 'cancel' | 'delete' | 'rebuild';
7
7
 
8
- const ConfirmDeleteOrCancelImageExportModal = ({
8
+ const ConfirmImageExportActionModal = ({
9
9
  action,
10
10
  onClose,
11
11
  }: {
@@ -17,16 +17,29 @@ const ConfirmDeleteOrCancelImageExportModal = ({
17
17
  let title = '';
18
18
  let message = '';
19
19
  let confirmButtonTitle = '';
20
- if (action === 'cancel') {
21
- title = t('Cancel image export?');
22
- message = t('This will immediately stop the current process. As a result, no images will be exported.');
23
- confirmButtonTitle = t('Cancel image export');
24
- } else if (action === 'delete') {
25
- title = t('Delete image export?');
26
- message = t(
27
- 'This image export will be permanently removed. The actual image files in your storage will not be deleted.',
28
- );
29
- confirmButtonTitle = t('Delete');
20
+
21
+ switch (action) {
22
+ case 'cancel':
23
+ title = t('Cancel image export?');
24
+ message = t(
25
+ 'This will immediately stop the current process. As a result, the image will not be exported in this format.',
26
+ );
27
+ confirmButtonTitle = t('Cancel image export');
28
+ break;
29
+ case 'delete':
30
+ title = t('Delete image export?');
31
+ message = t(
32
+ 'This image export will be permanently removed. The actual image files in your storage will not be deleted.',
33
+ );
34
+ confirmButtonTitle = t('Delete');
35
+ break;
36
+ case 'rebuild':
37
+ title = t('Rebuild image export?');
38
+ message = t(
39
+ 'Rebuilding updates the image currently displayed in the console. Previous versions remain accessible via the flightctl CLI.',
40
+ );
41
+ confirmButtonTitle = t('Rebuild');
42
+ break;
30
43
  }
31
44
 
32
45
  return (
@@ -34,15 +47,15 @@ const ConfirmDeleteOrCancelImageExportModal = ({
34
47
  <ModalHeader title={title} />
35
48
  <ModalBody>{message}</ModalBody>
36
49
  <ModalFooter>
37
- <Button key="confirm" variant="danger" onClick={() => onClose(true)}>
50
+ <Button key="confirm" variant={action === 'rebuild' ? 'primary' : 'danger'} onClick={() => onClose(true)}>
38
51
  {confirmButtonTitle}
39
52
  </Button>
40
53
  <Button key="cancel" variant="link" onClick={() => onClose(false)}>
41
- {t('Close')}
54
+ {action === 'cancel' ? t('Close') : t('Cancel')}
42
55
  </Button>
43
56
  </ModalFooter>
44
57
  </Modal>
45
58
  );
46
59
  };
47
60
 
48
- export default ConfirmDeleteOrCancelImageExportModal;
61
+ export default ConfirmImageExportActionModal;
@@ -70,11 +70,16 @@ const CreateImageBuildWizard = () => {
70
70
  const [error, setError] = React.useState<ImageBuildWizardError>();
71
71
  const [currentStep, setCurrentStep] = React.useState<WizardStepType>();
72
72
  const [imageBuildId, imageBuild, imageBuildLoading, editError] = useEditImageBuild();
73
- const { isLoading: registriesLoading, error: registriesError } = useOciRegistriesContext();
73
+ const { ociRegistries, isLoading: registriesLoading, error: registriesError } = useOciRegistriesContext();
74
74
 
75
75
  const isEdit = !!imageBuildId;
76
76
  const buildReason = imageBuild ? getImageBuildStatusReason(imageBuild) : undefined;
77
77
 
78
+ const availableRepositoryIds = React.useMemo(
79
+ () => new Set(ociRegistries.map((r) => r.metadata?.name as string)),
80
+ [ociRegistries],
81
+ );
82
+
78
83
  let title: string;
79
84
  if (isEdit) {
80
85
  title =
@@ -117,7 +122,7 @@ const CreateImageBuildWizard = () => {
117
122
  </Alert>
118
123
  ) : (
119
124
  <Formik<ImageBuildFormValues>
120
- initialValues={getInitialValues(imageBuild)}
125
+ initialValues={getInitialValues(imageBuild, availableRepositoryIds)}
121
126
  validationSchema={getValidationSchema(t)}
122
127
  validateOnMount
123
128
  onSubmit={async (values) => {
@@ -184,7 +189,7 @@ const CreateImageBuildWizard = () => {
184
189
  setCurrentStep(step);
185
190
  }}
186
191
  >
187
- <WizardStep name={t('Image details')} id={sourceImageStepId}>
192
+ <WizardStep name={t('Base image')} id={sourceImageStepId}>
188
193
  {(!currentStep || currentStep?.id === sourceImageStepId) && <SourceImageStep />}
189
194
  </WizardStep>
190
195
  <WizardStep
@@ -37,7 +37,7 @@ const OutputImageStep = () => {
37
37
  const writableRepoValidation = React.useCallback(
38
38
  (repo: Repository) => {
39
39
  if (isOciRepoSpec(repo.spec) && repo.spec.accessMode === OciRepoSpec.accessMode.READ) {
40
- return t('Repository is read-only and cannot be used as the target repository.');
40
+ return t('Repositories used for output images must be writable.');
41
41
  }
42
42
  return undefined;
43
43
  },
@@ -22,6 +22,7 @@ import { FormikErrors, useFormikContext } from 'formik';
22
22
 
23
23
  import { BindingType } from '@flightctl/types/imagebuilder';
24
24
  import { ImageBuildFormValues } from '../types';
25
+ import { PUBLIC_KEY_MAX_LENGTH } from '../utils';
25
26
  import { useTranslation } from '../../../../hooks/useTranslation';
26
27
  import FlightCtlForm from '../../../form/FlightCtlForm';
27
28
  import TextField from '../../../form/TextField';
@@ -31,7 +32,14 @@ import { CERTIFICATE_VALIDITY_IN_YEARS } from '../../../../constants';
31
32
 
32
33
  export const registrationStepId = 'registration';
33
34
 
34
- export const isRegistrationStepValid = (errors: FormikErrors<ImageBuildFormValues>) => !errors.bindingType;
35
+ export const isRegistrationStepValid = (errors: FormikErrors<ImageBuildFormValues>) => {
36
+ const { userConfiguration } = errors;
37
+ if (!userConfiguration) {
38
+ return true;
39
+ }
40
+
41
+ return !userConfiguration.username && !userConfiguration.publickey;
42
+ };
35
43
 
36
44
  const RegistrationStep = () => {
37
45
  const { t } = useTranslation();
@@ -52,11 +60,7 @@ const RegistrationStep = () => {
52
60
  };
53
61
 
54
62
  const handleRemoteAccessToggle = (enabled: boolean) => {
55
- if (enabled && !values.userConfiguration) {
56
- setFieldValue('userConfiguration', { username: '', publickey: '', enabled: true });
57
- return;
58
- }
59
- setFieldValue('userConfiguration.enabled', enabled);
63
+ setFieldValue('remoteAccessEnabled', enabled);
60
64
  };
61
65
 
62
66
  return (
@@ -158,19 +162,23 @@ const RegistrationStep = () => {
158
162
  <Grid lg={5} span={8}>
159
163
  <FormSection title={t('Remote access')}>
160
164
  <CheckboxField
161
- name="userConfiguration.enabled"
165
+ name="remoteAccessEnabled"
162
166
  label={t('Provide an SSH public key to enable passwordless login once your image is deployed.')}
163
167
  onChangeCustom={handleRemoteAccessToggle}
164
168
  >
165
- <FormGroup label={t('Username')} fieldId="user-config-username">
169
+ <FormGroup label={t('Username')} fieldId="user-config-username" isRequired>
166
170
  <TextField
167
171
  name="userConfiguration.username"
168
172
  aria-label={t('Username')}
169
173
  helperText={t('The username for the user account')}
170
174
  />
171
175
  </FormGroup>
172
- <FormGroup label={t('SSH public key')} fieldId="user-config-publickey">
173
- <UploadField name="userConfiguration.publickey" ariaLabel={t('SSH public key')} />
176
+ <FormGroup label={t('SSH public key')} fieldId="user-config-publickey" isRequired>
177
+ <UploadField
178
+ name="userConfiguration.publickey"
179
+ ariaLabel={t('SSH public key')}
180
+ maxFileBytes={PUBLIC_KEY_MAX_LENGTH}
181
+ />
174
182
  </FormGroup>
175
183
  </CheckboxField>
176
184
  <Content component="small">
@@ -54,7 +54,7 @@ const ReviewStep = ({ error }: ReviewStepProps) => {
54
54
  );
55
55
 
56
56
  const isEarlyBinding = values.bindingType === BindingType.BindingTypeEarly;
57
- const remoteAccessUsername = values.userConfiguration?.enabled ? values.userConfiguration?.username || '' : '';
57
+ const remoteAccessUsername = values.remoteAccessEnabled ? values.userConfiguration.username || '' : '';
58
58
 
59
59
  return (
60
60
  <Stack hasGutter>
@@ -63,6 +63,10 @@ const ReviewStep = ({ error }: ReviewStepProps) => {
63
63
  <CardTitle>{t('Base image')}</CardTitle>
64
64
  <CardBody>
65
65
  <FlightCtlDescriptionList isHorizontal isCompact>
66
+ <DescriptionListGroup>
67
+ <DescriptionListTerm>{t('Build name')}</DescriptionListTerm>
68
+ <DescriptionListDescription>{values.buildName}</DescriptionListDescription>
69
+ </DescriptionListGroup>
66
70
  <DescriptionListGroup>
67
71
  <DescriptionListTerm>{t('Source repository')}</DescriptionListTerm>
68
72
  <DescriptionListDescription>{values.source.repository}</DescriptionListDescription>
@@ -6,6 +6,7 @@ import { RepoSpecType } from '@flightctl/types';
6
6
  import { ImageBuildFormValues } from '../types';
7
7
  import { useTranslation } from '../../../../hooks/useTranslation';
8
8
  import FlightCtlForm from '../../../form/FlightCtlForm';
9
+ import NameField from '../../../form/NameField';
9
10
  import TextField from '../../../form/TextField';
10
11
  import RepositorySelect from '../../../form/RepositorySelect';
11
12
  import { usePermissionsContext } from '../../../common/PermissionsContext';
@@ -13,11 +14,15 @@ import { RESOURCE, VERB } from '../../../../types/rbac';
13
14
  import { getImageReference } from '../../../../utils/imageBuilds';
14
15
  import ImageUrlCard from '../../ImageUrlCard';
15
16
  import { useOciRegistriesContext } from '../../OciRegistriesContext';
17
+ import { getBuildNameValidations } from '../../../form/validations';
16
18
 
17
19
  export const sourceImageStepId = 'source-image';
18
20
 
19
21
  export const isSourceImageStepValid = (errors: FormikErrors<ImageBuildFormValues>) => {
20
- const { source } = errors;
22
+ const { buildName, source } = errors;
23
+ if (buildName) {
24
+ return false;
25
+ }
21
26
  if (!source) {
22
27
  return true;
23
28
  }
@@ -39,6 +44,13 @@ const SourceImageStep = () => {
39
44
  <FlightCtlForm>
40
45
  <Grid lg={5} span={8}>
41
46
  <FormSection>
47
+ <NameField
48
+ name="buildName"
49
+ aria-label={t('Build name')}
50
+ isRequired
51
+ resourceType="imagebuilds"
52
+ validations={getBuildNameValidations(t)}
53
+ />
42
54
  <RepositorySelect
43
55
  name="source.repository"
44
56
  label={t('Source repository')}
@@ -6,17 +6,14 @@ import {
6
6
  } from '@flightctl/types/imagebuilder';
7
7
  import { ExportFormatType } from '@flightctl/types/imagebuilder';
8
8
 
9
- type ImageBuildUserConfigurationForm = ImageBuildUserConfiguration & {
10
- enabled?: boolean;
11
- };
12
-
13
9
  export type ImageBuildFormValues = {
14
- // name is autogenereated by us
10
+ buildName: string;
15
11
  source: ImageBuildSource;
16
12
  destination: ImageBuildDestination;
17
13
  bindingType: BindingType;
18
14
  exportFormats: ExportFormatType[];
19
- userConfiguration?: ImageBuildUserConfigurationForm;
15
+ remoteAccessEnabled: boolean;
16
+ userConfiguration: ImageBuildUserConfiguration;
20
17
  };
21
18
 
22
19
  export type ImageBuildWizardError =
@@ -2,32 +2,148 @@ import { TFunction } from 'i18next';
2
2
  import * as Yup from 'yup';
3
3
 
4
4
  import {
5
+ ApiVersion,
5
6
  BindingType,
6
7
  ExportFormatType,
7
8
  ImageBuild,
8
9
  ImageBuildDestination,
9
10
  ImageBuildSource,
11
+ ImageBuildUserConfiguration,
10
12
  ImageExport,
11
13
  ResourceKind,
12
14
  } from '@flightctl/types/imagebuilder';
13
- import { IMAGEBUILDER_API_VERSION } from '../../../constants';
15
+ import { validImageBuildName } from '../../form/validations';
14
16
  import { ImageBuildFormValues } from './types';
15
17
  import { ImageBuildWithExports } from '../../../types/extraTypes';
16
18
 
17
- export const getValidationSchema = (t: TFunction) => {
18
- return Yup.object<ImageBuildFormValues>({
19
- source: Yup.object<ImageBuildSource>({
20
- repository: Yup.string().required(t('Source repository is required')),
21
- imageName: Yup.string().required(t('Image name is required')),
22
- imageTag: Yup.string().required(t('Image tag is required')),
23
- }).required(t('Source image is required')),
24
- destination: Yup.object<ImageBuildDestination>({
25
- repository: Yup.string().required(t('Target repository is required')),
26
- imageName: Yup.string().required(t('Image name is required')),
27
- imageTag: Yup.string().required(t('Image tag is required')),
28
- }).required(t('Target image is required')),
29
- bindingType: Yup.string<BindingType>().required(t('Binding type is required')),
19
+ export const PUBLIC_KEY_MAX_LENGTH = 8 * 1024; // (8 KB)
20
+ const VALID_SSH_PUBLIC_KEY_TYPES = [
21
+ 'ssh-rsa',
22
+ 'ssh-ed25519',
23
+ 'ecdsa-sha2-nistp256',
24
+ 'ecdsa-sha2-nistp384',
25
+ 'ecdsa-sha2-nistp521',
26
+ 'ssh-dss',
27
+ ];
28
+
29
+ const SSH_PUBLIC_KEY_BASE64_DATA_REGEX = /^(?=.{50,}$)[A-Za-z0-9+/]+=*$/;
30
+ // Characters that could be used for injection attacks
31
+ const MALICIOUS_PUBLIC_KEY_CHARACTERS = /[;|&`()[\]{}<>"'\\\t$]/;
32
+
33
+ const OCI_IMAGE_NAME_MAX_LENGTH = 255;
34
+ const OCI_IMAGE_NAME_REGEX = /^[a-z0-9]+(?:[._-]+[a-z0-9]+)*(?:\/[a-z0-9]+(?:[._-]+[a-z0-9]+)*)*$/;
35
+ const OCI_IMAGE_NAME_VALID_CHARS = /^[a-z0-9._/-]+$/;
36
+
37
+ const OCI_IMAGE_TAG_MAX_LENGTH = 128;
38
+ const OCI_IMAGE_TAG_REGEX = /^[\w][\w.-]{0,127}$/;
39
+ const OCI_IMAGE_TAG_VALID_CHARS = /^[\w.-]+$/;
40
+
41
+ /** Returns an error message for image name: invalid chars, or invalid format. */
42
+ const getImageNameValidationError = (value: string, t: TFunction): string | undefined => {
43
+ if (!value) return undefined;
44
+ if (value.length > OCI_IMAGE_NAME_MAX_LENGTH) {
45
+ return t('Image name must not exceed {{max}} characters.', { max: OCI_IMAGE_NAME_MAX_LENGTH });
46
+ }
47
+ if (!OCI_IMAGE_NAME_VALID_CHARS.test(value)) {
48
+ return t('Image name may only contain alphanumeric characters, dots, underscores, hyphens, and slashes.');
49
+ }
50
+ if (!OCI_IMAGE_NAME_REGEX.test(value)) {
51
+ return t('Only alphanumeric characters are allowed at the start and end of each path component.');
52
+ }
53
+ return undefined;
54
+ };
55
+
56
+ const getImageTagValidationError = (value: string, t: TFunction): string | undefined => {
57
+ if (!value) return undefined;
58
+ if (value.length > OCI_IMAGE_TAG_MAX_LENGTH) {
59
+ return t('Image tag must not exceed {{max}} characters.', { max: OCI_IMAGE_TAG_MAX_LENGTH });
60
+ }
61
+ if (!OCI_IMAGE_TAG_VALID_CHARS.test(value)) {
62
+ return t('Image tag may only contain letters, numbers, underscores, dots, and hyphens.');
63
+ }
64
+ if (!OCI_IMAGE_TAG_REGEX.test(value)) {
65
+ return t('Image tag must start with a letter, number, or underscore.');
66
+ }
67
+ return undefined;
68
+ };
69
+
70
+ const getPublicKeyValidationError = (publicKey: string, t: TFunction): string | undefined => {
71
+ if (publicKey.length > PUBLIC_KEY_MAX_LENGTH) {
72
+ return t('SSH public key is too long');
73
+ }
74
+
75
+ // Allow newlines only at the end
76
+ const trimmedKey = publicKey.replace(/[\r\n]+$/g, '');
77
+ if (/[\r\n]/.test(trimmedKey)) {
78
+ return t('A single public key can be provided only');
79
+ }
80
+
81
+ if (MALICIOUS_PUBLIC_KEY_CHARACTERS.test(trimmedKey)) {
82
+ return t('Invalid SSH public key');
83
+ }
84
+
85
+ const parts = trimmedKey.trim().split(/\s+/);
86
+ if (parts.length < 2) {
87
+ return t('Invalid SSH public key format. Expected: "[TYPE] key [comment]"');
88
+ }
89
+
90
+ const keyType = parts[0];
91
+ if (!VALID_SSH_PUBLIC_KEY_TYPES.includes(keyType)) {
92
+ return t('Unsupported SSH public key type. Supported types: {{supportedTypes}}', {
93
+ supportedTypes: VALID_SSH_PUBLIC_KEY_TYPES.join(', '),
94
+ });
95
+ }
96
+
97
+ const base64Data = parts[1];
98
+ if (!SSH_PUBLIC_KEY_BASE64_DATA_REGEX.test(base64Data)) {
99
+ return t('Invalid SSH public key data');
100
+ }
101
+
102
+ return undefined;
103
+ };
104
+
105
+ const validImageBuildImageFields = (t: TFunction) =>
106
+ Yup.object<ImageBuildSource | ImageBuildDestination>({
107
+ repository: Yup.string().required(t('Repository is required')),
108
+ imageName: Yup.string()
109
+ .required(t('Image name is required'))
110
+ .test('oci-image-name', function (value) {
111
+ if (!value) return true;
112
+ const error = getImageNameValidationError(value, t);
113
+ return error ? this.createError({ message: error }) : true;
114
+ }),
115
+ imageTag: Yup.string()
116
+ .required(t('Image tag is required'))
117
+ .test('oci-image-tag', function (value) {
118
+ if (!value) return true;
119
+ const error = getImageTagValidationError(value, t);
120
+ return error ? this.createError({ message: error }) : true;
121
+ }),
30
122
  });
123
+
124
+ export const getValidationSchema = (t: TFunction) => {
125
+ return Yup.lazy((values: ImageBuildFormValues) =>
126
+ Yup.object<ImageBuildFormValues>({
127
+ buildName: validImageBuildName(t),
128
+ source: validImageBuildImageFields(t).required(t('Source image is required')),
129
+ destination: validImageBuildImageFields(t).required(t('Target image is required')),
130
+ bindingType: Yup.string<BindingType>().required(t('Binding type is required')),
131
+ userConfiguration: Yup.object<ImageBuildUserConfiguration>({
132
+ username: values.remoteAccessEnabled ? Yup.string().required(t('Username is required')) : Yup.string(),
133
+ publickey: values.remoteAccessEnabled
134
+ ? Yup.string()
135
+ .required(t('SSH public key is required'))
136
+ .test('flightctl-ssh-public-key', function (publicKey) {
137
+ if (!publicKey) {
138
+ return true;
139
+ }
140
+ const error = getPublicKeyValidationError(publicKey, t);
141
+ return error ? this.createError({ message: error }) : true;
142
+ })
143
+ : Yup.string(),
144
+ }),
145
+ }),
146
+ );
31
147
  };
32
148
 
33
149
  // Returns an array with one item per format (VMDK, QCOW2, ISO), where each item is either
@@ -84,32 +200,40 @@ export const toImageBuildWithExports = (imageBuild: ImageBuild): ImageBuildWithE
84
200
  };
85
201
  };
86
202
 
87
- export const getInitialValues = (imageBuild?: ImageBuildWithExports): ImageBuildFormValues => {
203
+ const getExistingImageData = (image: ImageBuildSource | ImageBuildDestination, repoIds: Set<string>) => {
204
+ if (repoIds.has(image.repository)) {
205
+ return image;
206
+ }
207
+ // When copying the image build, drop the reference to the repository if it doesn't exist anymore
208
+ return {
209
+ ...image,
210
+ repository: '',
211
+ };
212
+ };
213
+
214
+ export const getInitialValues = (
215
+ imageBuild: ImageBuildWithExports | undefined,
216
+ repoIds: Set<string>,
217
+ ): ImageBuildFormValues => {
88
218
  if (imageBuild) {
89
219
  const exportFormats = imageBuild.imageExports
90
220
  .filter((ie): ie is ImageExport => ie !== undefined)
91
221
  .map((imageExport) => imageExport.spec.format);
92
222
  const userConfig = imageBuild.spec.userConfiguration;
93
- const userConfiguration = userConfig
94
- ? {
95
- ...userConfig,
96
- enabled: !!(userConfig.username || userConfig.publickey),
97
- }
98
- : {
99
- username: '',
100
- publickey: '',
101
- enabled: false,
102
- };
103
223
  return {
104
- source: imageBuild.spec.source,
105
- destination: imageBuild.spec.destination,
224
+ // Since we're always creating new imageBuilds, we don't copy the current name
225
+ buildName: '',
226
+ source: getExistingImageData(imageBuild.spec.source, repoIds),
227
+ destination: getExistingImageData(imageBuild.spec.destination, repoIds),
106
228
  bindingType: imageBuild.spec.binding.type as BindingType,
107
229
  exportFormats: exportFormats || [],
108
- userConfiguration,
230
+ remoteAccessEnabled: !!(userConfig?.username || userConfig?.publickey),
231
+ userConfiguration: userConfig || { username: '', publickey: '' },
109
232
  };
110
233
  }
111
234
 
112
235
  return {
236
+ buildName: '',
113
237
  source: {
114
238
  repository: '',
115
239
  imageName: '',
@@ -122,10 +246,10 @@ export const getInitialValues = (imageBuild?: ImageBuildWithExports): ImageBuild
122
246
  },
123
247
  bindingType: BindingType.BindingTypeEarly,
124
248
  exportFormats: [],
249
+ remoteAccessEnabled: false,
125
250
  userConfiguration: {
126
251
  username: '',
127
252
  publickey: '',
128
- enabled: false,
129
253
  },
130
254
  };
131
255
  };
@@ -136,13 +260,13 @@ const getHash = () =>
136
260
  .toString(16)
137
261
  .padStart(6, '0');
138
262
 
139
- const generateBuildName = () => `imagebuild-${getHash()}`;
140
263
  const generateExportName = (imageBuildName: string, format: ExportFormatType) => {
141
- return `${imageBuildName}-${format}-${getHash()}`;
264
+ const formatKey = format === ExportFormatType.ExportFormatTypeQCOW2DiskContainer ? 'qcow2-disk' : format;
265
+ return `${imageBuildName}-${formatKey}-${getHash()}`;
142
266
  };
143
267
 
144
268
  export const getImageBuildResource = (values: ImageBuildFormValues): ImageBuild => {
145
- const name = generateBuildName();
269
+ const name = values.buildName;
146
270
  const spec: ImageBuild['spec'] = {
147
271
  source: values.source,
148
272
  destination: values.destination,
@@ -152,9 +276,9 @@ export const getImageBuildResource = (values: ImageBuildFormValues): ImageBuild
152
276
  };
153
277
 
154
278
  // Allow the user to uncheck the toggle without having cleared the fields
155
- const username = values.userConfiguration?.username || '';
156
- const publickey = values.userConfiguration?.publickey || '';
157
- if (values.userConfiguration?.enabled && username && publickey) {
279
+ const username = values.userConfiguration.username || '';
280
+ const publickey = values.userConfiguration.publickey || '';
281
+ if (values.remoteAccessEnabled && username && publickey) {
158
282
  spec.userConfiguration = {
159
283
  username,
160
284
  publickey,
@@ -162,7 +286,7 @@ export const getImageBuildResource = (values: ImageBuildFormValues): ImageBuild
162
286
  }
163
287
 
164
288
  return {
165
- apiVersion: IMAGEBUILDER_API_VERSION,
289
+ apiVersion: ApiVersion.ApiVersionV1alpha1,
166
290
  kind: ResourceKind.IMAGE_BUILD,
167
291
  metadata: {
168
292
  name,
@@ -175,7 +299,7 @@ export const getImageExportResource = (imageBuildName: string, format: ExportFor
175
299
  const exportName = generateExportName(imageBuildName, format);
176
300
 
177
301
  return {
178
- apiVersion: IMAGEBUILDER_API_VERSION,
302
+ apiVersion: ApiVersion.ApiVersionV1alpha1,
179
303
  kind: ResourceKind.IMAGE_EXPORT,
180
304
  metadata: {
181
305
  name: exportName,
@@ -60,7 +60,7 @@ const ImageBuildDetailsPageContent = () => {
60
60
  resourceTypeLabel={t('Image builds')}
61
61
  nav={
62
62
  <TabsNav aria-label="Image build details tabs" tabKeys={tabKeys}>
63
- <Tab eventKey="details" title={t('Image details')} />
63
+ <Tab eventKey="details" title={t('Base image')} />
64
64
  <Tab eventKey="exports" title={t('Export images')} />
65
65
  <Tab eventKey="yaml" title={t('YAML')} />
66
66
  {canViewLogs && <Tab eventKey="logs" title={t('Logs')} />}