@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
@@ -2,8 +2,9 @@ import * as React from 'react';
2
2
  import { Gallery } from '@patternfly/react-core';
3
3
  import { saveAs } from 'file-saver';
4
4
 
5
- import { ExportFormatType, ImageExport } from '@flightctl/types/imagebuilder';
5
+ import { ExportFormatType, ImageBuildConditionReason, ImageExport } from '@flightctl/types/imagebuilder';
6
6
  import { ImageBuildWithExports } from '../../../types/extraTypes';
7
+ import { getImageBuildStatusReason } from '../../../utils/imageBuilds';
7
8
  import { RESOURCE, VERB } from '../../../types/rbac';
8
9
  import { useFetch } from '../../../hooks/useFetch';
9
10
  import { usePermissionsContext } from '../../common/PermissionsContext';
@@ -13,8 +14,8 @@ import { ImageExportAction, ViewImageBuildExportCard } from '../ImageExportCards
13
14
  import { useOciRegistriesContext } from '../OciRegistriesContext';
14
15
  import { showSpinnerBriefly } from '../../../utils/time';
15
16
  import { getAllExportFormats, getExportDownloadResult, getImageReference } from '../../../utils/imageBuilds';
16
- import { useAppContext } from '../../../hooks/useAppContext';
17
- import { ROUTE } from '../../../hooks/useNavigate';
17
+ import { ROUTE, useNavigate } from '../../../hooks/useNavigate';
18
+ import { IMAGE_EXPORT_ID_PARAM } from './ImageBuildLogsTab';
18
19
 
19
20
  type ImageBuildExportsGalleryProps = {
20
21
  imageBuild: ImageBuildWithExports;
@@ -50,8 +51,21 @@ const ImageBuildExportsGallery = ({ imageBuild, refetch }: ImageBuildExportsGall
50
51
  const [canCreateExport, canDelete, canViewLogs, canDownload, canCancel] =
51
52
  checkPermissions(imageBuildExportsPermissions);
52
53
 
54
+ const buildReason = getImageBuildStatusReason(imageBuild);
55
+
53
56
  const actionPermissions = React.useMemo(() => {
54
57
  const actions: ImageExportAction[] = [];
58
+ if (
59
+ buildReason === ImageBuildConditionReason.ImageBuildConditionReasonFailed ||
60
+ buildReason === ImageBuildConditionReason.ImageBuildConditionReasonCanceled ||
61
+ buildReason === ImageBuildConditionReason.ImageBuildConditionReasonCanceling
62
+ ) {
63
+ if (canDelete) {
64
+ actions.push('delete');
65
+ }
66
+ return actions;
67
+ }
68
+
55
69
  if (canCreateExport) {
56
70
  actions.push('createExport');
57
71
  actions.push('rebuild');
@@ -70,12 +84,9 @@ const ImageBuildExportsGallery = ({ imageBuild, refetch }: ImageBuildExportsGall
70
84
  actions.push('cancel');
71
85
  }
72
86
  return actions;
73
- }, [canCreateExport, canDelete, canViewLogs, canDownload, canCancel]);
87
+ }, [buildReason, canCreateExport, canDelete, canViewLogs, canDownload, canCancel]);
74
88
 
75
- const {
76
- router: { useNavigate: useRouterNavigate, appRoutes },
77
- } = useAppContext();
78
- const routerNavigate = useRouterNavigate();
89
+ const navigate = useNavigate();
79
90
  const [error, setError] = React.useState<{
80
91
  format: ExportFormatType;
81
92
  action: ImageExportAction;
@@ -88,11 +99,6 @@ const ImageBuildExportsGallery = ({ imageBuild, refetch }: ImageBuildExportsGall
88
99
  >();
89
100
  const imageBuildId = imageBuild.metadata.name as string;
90
101
 
91
- const handleViewLogs = () => {
92
- const baseRoute = appRoutes[ROUTE.IMAGE_BUILD_DETAILS];
93
- routerNavigate(`${baseRoute}/${imageBuildId}/logs`);
94
- };
95
-
96
102
  const handleCreateNewExport = async (format: ExportFormatType) => {
97
103
  try {
98
104
  const imageExport = getImageExportResource(imageBuildId, format);
@@ -110,14 +116,13 @@ const ImageBuildExportsGallery = ({ imageBuild, refetch }: ImageBuildExportsGall
110
116
  const response = await proxyFetch(`imagebuilder/api/v1/imageexports/${ieName}/download`, {
111
117
  method: 'GET',
112
118
  credentials: 'include',
113
- redirect: 'manual', // Prevent automatic redirect following to avoid CORS issues
114
119
  });
115
-
116
120
  const downloadResult = await getExportDownloadResult(response);
117
121
  if (downloadResult === null) {
118
122
  await showSpinnerBriefly(DOWNLOAD_REDIRECT_DELAY);
119
123
  throw new Error(`Download failed: ${response.status} ${response.statusText}`);
120
- } else if (downloadResult.type === 'redirect') {
124
+ }
125
+ if (downloadResult.type === 'redirect') {
121
126
  createDownloadLink(downloadResult.url);
122
127
  await showSpinnerBriefly(DOWNLOAD_REDIRECT_DELAY);
123
128
  } else {
@@ -164,9 +169,13 @@ const ImageBuildExportsGallery = ({ imageBuild, refetch }: ImageBuildExportsGall
164
169
  case 'delete':
165
170
  await handleDelete(ieName);
166
171
  break;
167
- case 'viewLogs':
168
- handleViewLogs();
172
+ case 'viewLogs': {
173
+ const searchParams = new URLSearchParams({
174
+ [IMAGE_EXPORT_ID_PARAM]: ieName,
175
+ });
176
+ navigate({ route: ROUTE.IMAGE_BUILD_DETAILS, postfix: `${imageBuildId}/logs?${searchParams.toString()}` });
169
177
  break;
178
+ }
170
179
  }
171
180
  } catch (error) {
172
181
  setError({ format, message: getErrorMessage(error), action });
@@ -1,5 +1,4 @@
1
1
  import * as React from 'react';
2
- import { TFunction } from 'react-i18next';
3
2
  import {
4
3
  Alert,
5
4
  Bullseye,
@@ -25,6 +24,7 @@ import {
25
24
  import { ExportFormatType, ImageBuildConditionReason, ImageExportConditionReason } from '@flightctl/types/imagebuilder';
26
25
  import { useTranslation } from '../../../hooks/useTranslation';
27
26
  import {
27
+ getExportFormatLabel,
28
28
  getImageBuildStatusReason,
29
29
  getImageExportStatusReason,
30
30
  isImageBuildActiveReason,
@@ -34,6 +34,9 @@ import { ImageBuildWithExports } from '../../../types/extraTypes';
34
34
  import { LogResourceType, useImageBuildLogs } from '../useImageBuildLogs';
35
35
  import { StatusDisplayContent } from '../../Status/StatusDisplay';
36
36
  import { StatusLevel } from '../../../utils/status/common';
37
+ import { useAppContext } from '../../../hooks/useAppContext';
38
+
39
+ export const IMAGE_EXPORT_ID_PARAM = 'exportId';
37
40
 
38
41
  type LogEntity = {
39
42
  type: LogResourceType;
@@ -43,19 +46,6 @@ type LogEntity = {
43
46
  status: ImageBuildConditionReason | ImageExportConditionReason;
44
47
  };
45
48
 
46
- const getExportFormatText = (t: TFunction, format: ExportFormatType) => {
47
- switch (format) {
48
- case ExportFormatType.ExportFormatTypeVMDK:
49
- return t('Virtualization (VMDK)');
50
- case ExportFormatType.ExportFormatTypeQCOW2:
51
- return t('OpenStack/KVM (QCOW2)');
52
- case ExportFormatType.ExportFormatTypeQCOW2DiskContainer:
53
- return t('OpenShift Virtualization (QCOW2)');
54
- case ExportFormatType.ExportFormatTypeISO:
55
- return t('Metal installer (ISO)');
56
- }
57
- };
58
-
59
49
  const ImageBuildAndExportLogStatus = ({
60
50
  status,
61
51
  }: {
@@ -85,18 +75,23 @@ const ImageBuildAndExportLogStatus = ({
85
75
 
86
76
  const ImageBuildLogsTab = ({ imageBuild }: { imageBuild: ImageBuildWithExports }) => {
87
77
  const { t } = useTranslation();
78
+ const {
79
+ router: { useSearchParams },
80
+ } = useAppContext();
81
+ const [searchParams] = useSearchParams();
88
82
  const [isLogSelectOpen, setIsLogSelectOpen] = React.useState(false);
89
83
  const logsRef = React.useRef<HTMLTextAreaElement>(null);
90
84
 
85
+ const buildName = imageBuild.metadata.name as string;
86
+
91
87
  const { selectableEntities, hasExports } = React.useMemo(() => {
92
88
  const entities: LogEntity[] = [];
93
- const buildName = imageBuild.metadata.name as string;
94
89
 
95
90
  const buildReason = getImageBuildStatusReason(imageBuild);
96
91
  entities.push({
97
92
  type: LogResourceType.BUILD,
98
93
  id: buildName,
99
- label: buildName,
94
+ label: t('Image build'),
100
95
  isActive: isImageBuildActiveReason(buildReason),
101
96
  status: buildReason,
102
97
  });
@@ -115,7 +110,7 @@ const ImageBuildLogsTab = ({ imageBuild }: { imageBuild: ImageBuildWithExports }
115
110
  entities.push({
116
111
  type: LogResourceType.EXPORT,
117
112
  id: ie.metadata.name as string,
118
- label: getExportFormatText(t, format),
113
+ label: getExportFormatLabel(t, format),
119
114
  isActive: isImageExportActiveReason(exportReason),
120
115
  status: exportReason,
121
116
  });
@@ -125,9 +120,15 @@ const ImageBuildLogsTab = ({ imageBuild }: { imageBuild: ImageBuildWithExports }
125
120
  selectableEntities: entities,
126
121
  hasExports,
127
122
  };
128
- }, [imageBuild, t]);
123
+ }, [imageBuild, buildName, t]);
129
124
 
130
- const [selectedEntityId, setSelectedEntityId] = React.useState<string>(imageBuild.metadata.name as string);
125
+ const [selectedEntityId, setSelectedEntityId] = React.useState<string>(() => {
126
+ const selectedId = searchParams.get(IMAGE_EXPORT_ID_PARAM);
127
+ if (selectedId && selectableEntities.some((e) => e.id === selectedId)) {
128
+ return selectedId;
129
+ }
130
+ return buildName;
131
+ });
131
132
  const selectedEntity = selectableEntities.find((entity) => entity.id === selectedEntityId) || selectableEntities[0];
132
133
  const { logs, isLoading, error, isStreaming } = useImageBuildLogs(
133
134
  selectedEntity.id,
@@ -209,13 +210,8 @@ const ImageBuildLogsTab = ({ imageBuild }: { imageBuild: ImageBuildWithExports }
209
210
  {selectableEntities
210
211
  .filter((entity) => entity.type === LogResourceType.BUILD)
211
212
  .map((entity) => (
212
- <MenuItem
213
- key={entity.id}
214
- itemId={entity.id}
215
- isSelected={selectedEntityId === entity.id}
216
- description={entity.id}
217
- >
218
- {entity.label}
213
+ <MenuItem key={entity.id} itemId={entity.id} isSelected={selectedEntityId === entity.id}>
214
+ {entity.id}
219
215
  </MenuItem>
220
216
  ))}
221
217
  </MenuList>
@@ -26,12 +26,14 @@ import { VirtualMachineIcon } from '@patternfly/react-icons/dist/js/icons/virtua
26
26
  import { CloudSecurityIcon } from '@patternfly/react-icons/dist/js/icons/cloud-security-icon';
27
27
  import { ServerGroupIcon } from '@patternfly/react-icons/dist/js/icons/server-group-icon';
28
28
  import { BuilderImageIcon } from '@patternfly/react-icons/dist/js/icons/builder-image-icon';
29
+ import { InfoCircleIcon } from '@patternfly/react-icons/dist/js/icons/info-circle-icon';
29
30
 
30
31
  import { ExportFormatType, ImageExport, ImageExportConditionReason } from '@flightctl/types/imagebuilder';
31
32
  import { getExportFormatDescription, getExportFormatLabel, getImageExportStatusReason } from '../../utils/imageBuilds';
32
33
  import { getDateDisplay } from '../../utils/dates';
33
34
  import { useTranslation } from '../../hooks/useTranslation';
34
- import ConfirmDeleteOrCancelImageExportModal, {
35
+ import WithTooltip from '../common/WithTooltip';
36
+ import ConfirmImageExportActionModal, {
35
37
  ConfirmImageExportAction,
36
38
  } from './ConfirmImageExportModal/ConfirmImageExportModal';
37
39
  import { ImageExportStatusDisplay } from './ImageBuildAndExportStatus';
@@ -41,6 +43,7 @@ import './ImageExportCards.css';
41
43
  export type ImageExportAction = 'cancel' | 'delete' | 'viewLogs' | 'download' | 'retry' | 'rebuild' | 'createExport';
42
44
 
43
45
  const getActionsForStatus = (
46
+ format: ExportFormatType,
44
47
  exportReason: ImageExportConditionReason | undefined,
45
48
  actionPermissions: ImageExportAction[],
46
49
  ): ImageExportAction[] => {
@@ -54,7 +57,10 @@ const getActionsForStatus = (
54
57
  actions.push('cancel', 'viewLogs');
55
58
  break;
56
59
  case ImageExportConditionReason.ImageExportConditionReasonCompleted:
57
- actions.push('download', 'viewLogs', 'delete', 'rebuild');
60
+ if (format !== ExportFormatType.ExportFormatTypeQCOW2DiskContainer) {
61
+ actions.push('download');
62
+ }
63
+ actions.push('viewLogs', 'delete', 'rebuild');
58
64
  break;
59
65
  case ImageExportConditionReason.ImageExportConditionReasonFailed:
60
66
  case ImageExportConditionReason.ImageExportConditionReasonCanceled:
@@ -102,7 +108,7 @@ const getActionTitle = (t: TFunction, action: ImageExportAction, inProgress: boo
102
108
  case 'createExport':
103
109
  return inProgress ? t('Exporting...') : t('Export image');
104
110
  default:
105
- return t('Action');
111
+ return t('Actions');
106
112
  }
107
113
  };
108
114
  const iconMap: Record<ExportFormatType, React.ReactElement> = {
@@ -173,9 +179,14 @@ export const ViewImageBuildExportCard = ({
173
179
  const [actionsDropdownOpen, setActionsDropdownOpen] = React.useState(false);
174
180
  const [pendingConfirmAction, setPendingConfirmAction] = React.useState<ConfirmImageExportAction>();
175
181
  const exists = !!imageExport;
182
+ const exportReason = exists ? getImageExportStatusReason(imageExport) : undefined;
176
183
 
177
184
  const handleCardAction = (action: ImageExportAction) => {
178
- if (action === 'cancel' || action === 'delete') {
185
+ if (
186
+ action === 'cancel' ||
187
+ action === 'delete' ||
188
+ (action === 'rebuild' && exportReason === ImageExportConditionReason.ImageExportConditionReasonCompleted)
189
+ ) {
179
190
  setPendingConfirmAction(action);
180
191
  } else {
181
192
  onCardAction({ format, action });
@@ -189,13 +200,12 @@ export const ViewImageBuildExportCard = ({
189
200
  setPendingConfirmAction(undefined);
190
201
  };
191
202
 
192
- const exportReason = exists ? getImageExportStatusReason(imageExport) : undefined;
193
203
  const { primaryAction, remainingActions } = React.useMemo(() => {
194
- const allActions = getActionsForStatus(exportReason, actionPermissions);
204
+ const allActions = getActionsForStatus(format, exportReason, actionPermissions);
195
205
  const primaryAction = allActions.length > 0 ? allActions[0] : undefined;
196
206
  const remainingActions = allActions.length > 1 ? allActions.slice(1) : [];
197
207
  return { primaryAction, remainingActions };
198
- }, [exportReason, actionPermissions]);
208
+ }, [format, exportReason, actionPermissions]);
199
209
 
200
210
  const renderActionButton = (exportAction: ImageExportAction, variant: 'primary' | 'secondary' = 'secondary') => {
201
211
  const isDisabled = activeAction !== undefined;
@@ -259,9 +269,18 @@ export const ViewImageBuildExportCard = ({
259
269
  </Flex>
260
270
  </FlexItem>
261
271
  <FlexItem>
262
- <Content>
263
- <Content component={ContentVariants.h2}>{getExportFormatLabel(t, format)}</Content>
264
- </Content>
272
+ <Flex gap={{ default: 'gapSm' }}>
273
+ <FlexItem>
274
+ <Content component={ContentVariants.h2}>{getExportFormatLabel(t, format)}</Content>
275
+ </FlexItem>
276
+ {imageExport?.metadata?.name && (
277
+ <FlexItem>
278
+ <WithTooltip showTooltip content={imageExport.metadata.name}>
279
+ <InfoCircleIcon />
280
+ </WithTooltip>
281
+ </FlexItem>
282
+ )}
283
+ </Flex>
265
284
  </FlexItem>
266
285
  </Flex>
267
286
  </CardHeader>
@@ -287,7 +306,7 @@ export const ViewImageBuildExportCard = ({
287
306
  variant="secondary"
288
307
  isDisabled={activeAction !== undefined}
289
308
  >
290
- {t('Action')}
309
+ {t('Actions')}
291
310
  </MenuToggle>
292
311
  )}
293
312
  >
@@ -318,7 +337,7 @@ export const ViewImageBuildExportCard = ({
318
337
  </Stack>
319
338
  </CardFooter>
320
339
  {pendingConfirmAction && (
321
- <ConfirmDeleteOrCancelImageExportModal action={pendingConfirmAction} onClose={handleConfirmAction} />
340
+ <ConfirmImageExportActionModal action={pendingConfirmAction} onClose={handleConfirmAction} />
322
341
  )}
323
342
  </Card>
324
343
  );
@@ -439,6 +439,8 @@ export type CreateRepositoryFormProps = {
439
439
  onSuccess: (repository: Repository) => void;
440
440
  repository?: Repository;
441
441
  resourceSyncs?: ResourceSync[];
442
+ // If provided, the repository submission will be blocked if the validation fails
443
+ validateBefore?: (repo: Repository) => string | undefined;
442
444
  options?: {
443
445
  isReadOnly?: boolean;
444
446
  canUseResourceSyncs?: boolean;
@@ -447,13 +449,14 @@ export type CreateRepositoryFormProps = {
447
449
  };
448
450
  };
449
451
 
450
- const CreateRepositoryForm: React.FC<CreateRepositoryFormProps> = ({
452
+ const CreateRepositoryForm = ({
451
453
  repository,
452
454
  resourceSyncs,
455
+ validateBefore,
453
456
  options,
454
457
  onClose,
455
458
  onSuccess,
456
- }) => {
459
+ }: CreateRepositoryFormProps) => {
457
460
  const [errors, setErrors] = React.useState<string[]>();
458
461
  const { patch, remove, post } = useFetch();
459
462
  const { t } = useTranslation();
@@ -513,8 +516,14 @@ const CreateRepositoryForm: React.FC<CreateRepositoryFormProps> = ({
513
516
  setErrors([getErrorMessage(e)]);
514
517
  }
515
518
  } else {
519
+ const repoToCreate = getRepository(values);
520
+ const validationError = validateBefore?.(repoToCreate);
521
+ if (validationError) {
522
+ setErrors([validationError]);
523
+ return;
524
+ }
516
525
  try {
517
- const repo = await post<Repository>('repositories', getRepository(values));
526
+ const repo = await post<Repository>('repositories', repoToCreate);
518
527
  if (values.useResourceSyncs) {
519
528
  const resourceSyncPromises = values.resourceSyncs.map((rs) =>
520
529
  post<ResourceSync>('resourcesyncs', getResourceSync(values.name, rs)),
@@ -534,7 +543,7 @@ const CreateRepositoryForm: React.FC<CreateRepositoryFormProps> = ({
534
543
  >
535
544
  <CreateRepositoryFormContent isEdit={!!repository} onClose={onClose} isReadOnly={!!options?.isReadOnly}>
536
545
  {errors?.length && (
537
- <Alert isInline variant="danger" title={t('An error occurred')}>
546
+ <Alert isInline variant="danger" title={t('Repository could not be saved')}>
538
547
  {errors.map((e, index) => (
539
548
  <div key={index}>{e}</div>
540
549
  ))}
@@ -1,6 +1,7 @@
1
1
  import * as Yup from 'yup';
2
2
  import { TFunction } from 'i18next';
3
3
  import {
4
+ ApiVersion,
4
5
  DockerAuth,
5
6
  GitRepoSpec,
6
7
  HttpConfig,
@@ -16,7 +17,6 @@ import {
16
17
  } from '@flightctl/types';
17
18
 
18
19
  import { RepositoryFormValues, ResourceSyncFormValue } from './types';
19
- import { CORE_API_VERSION } from '../../../constants';
20
20
  import { getErrorMessage } from '../../../utils/error';
21
21
  import { appendJSONPatch } from '../../../utils/patch';
22
22
  import { MAX_TARGET_REVISION_LENGTH, maxLengthString, validKubernetesDnsSubdomain } from '../../form/validations';
@@ -779,7 +779,7 @@ export const getRepository = (values: Omit<RepositoryFormValues, 'useResourceSyn
779
779
  }
780
780
 
781
781
  return {
782
- apiVersion: CORE_API_VERSION,
782
+ apiVersion: ApiVersion.ApiVersionV1beta1,
783
783
  kind: 'Repository',
784
784
  metadata: {
785
785
  name: values.name,
@@ -877,7 +877,7 @@ export const getRepository = (values: Omit<RepositoryFormValues, 'useResourceSyn
877
877
  }
878
878
 
879
879
  return {
880
- apiVersion: CORE_API_VERSION,
880
+ apiVersion: ApiVersion.ApiVersionV1beta1,
881
881
  kind: 'Repository',
882
882
  metadata: {
883
883
  name: values.name,
@@ -888,7 +888,7 @@ export const getRepository = (values: Omit<RepositoryFormValues, 'useResourceSyn
888
888
 
889
889
  export const getResourceSync = (repositoryId: string, values: ResourceSyncFormValue): ResourceSync => {
890
890
  return {
891
- apiVersion: CORE_API_VERSION,
891
+ apiVersion: ApiVersion.ApiVersionV1beta1,
892
892
  kind: 'ResourceSync',
893
893
  metadata: {
894
894
  name: values.name,
@@ -214,6 +214,7 @@ const RepositorySelect = ({
214
214
  type={repoType}
215
215
  onClose={() => setCreateRepoModalOpen(false)}
216
216
  onSuccess={handleCreateRepository}
217
+ validateBeforeCreate={validateRepoSelection}
217
218
  />
218
219
  )}
219
220
  </>
@@ -15,44 +15,38 @@ type UploadFieldProps = {
15
15
  maxFileBytes?: number;
16
16
  };
17
17
 
18
- const UploadHelperText = ({
18
+ const ONE_MB = 1024 * 1024;
19
+
20
+ const UploadMaxFileSizeHelperText = ({ maxFileBytes }: { maxFileBytes: number }) => {
21
+ const { t } = useTranslation();
22
+
23
+ const showKB = maxFileBytes < ONE_MB;
24
+ const maxFileSize = maxFileBytes / (showKB ? 1024 : ONE_MB);
25
+
26
+ const helperText = showKB
27
+ ? t('Max file size {{ maxFileSize }} KB', { maxFileSize })
28
+ : t('Max file size {{ maxFileSize }} MB', { maxFileSize });
29
+
30
+ return <DefaultHelperText helperText={helperText} />;
31
+ };
32
+
33
+ const UploadErrorHelperText = ({
19
34
  meta,
20
35
  isUploading,
21
36
  isRejected,
22
- maxFileBytes,
23
37
  }: {
24
38
  meta?: FieldMetaProps<unknown>;
25
39
  isUploading: boolean;
26
40
  isRejected: boolean;
27
- maxFileBytes?: number;
28
41
  }) => {
29
42
  const { t } = useTranslation();
30
43
 
31
- const maxFileMB = (maxFileBytes || 0) / (1024 * 1024);
32
-
33
- const defaultContent = maxFileBytes ? (
34
- <DefaultHelperText helperText={t('Max file size {{ maxFileSize }} MB', { maxFileSize: maxFileMB })} />
35
- ) : null;
36
44
  if (isRejected) {
37
- return (
38
- <>
39
- {defaultContent}
40
- <ErrorHelperText
41
- error={t('Content exceeds max file size of {{ maxFileSize }} MB', {
42
- maxFileSize: maxFileMB,
43
- })}
44
- />
45
- </>
46
- );
45
+ return <ErrorHelperText error={t('File exceeds maximum allowed size.')} />;
47
46
  } else if (!isUploading && !!meta?.error) {
48
- return (
49
- <>
50
- {defaultContent}
51
- <ErrorHelperText meta={meta} />
52
- </>
53
- );
47
+ return <ErrorHelperText meta={meta} />;
54
48
  }
55
- return defaultContent;
49
+ return null;
56
50
  };
57
51
 
58
52
  const UploadField = ({ ariaLabel, maxFileBytes, isRequired, name }: UploadFieldProps) => {
@@ -83,8 +77,9 @@ const UploadField = ({ ariaLabel, maxFileBytes, isRequired, name }: UploadFieldP
83
77
  }
84
78
  }
85
79
 
86
- void setValue(file);
80
+ // To prevent timing issues with validations, we must touch the field before we set the new value
87
81
  void setTouched(true);
82
+ void setValue(file, true);
88
83
  setIsRejected(false);
89
84
  };
90
85
 
@@ -105,7 +100,9 @@ const UploadField = ({ ariaLabel, maxFileBytes, isRequired, name }: UploadFieldP
105
100
  onDataChange={handleFileChange}
106
101
  onTextChange={handleFileChange}
107
102
  onFileInputChange={(_event, file) => {
108
- if (!maxFileBytes || file.size <= maxFileBytes) {
103
+ if (maxFileBytes && file.size > maxFileBytes) {
104
+ handleFileRejected(true);
105
+ } else {
109
106
  setFilename(file.name);
110
107
  setIsRejected(false);
111
108
  }
@@ -122,12 +119,14 @@ const UploadField = ({ ariaLabel, maxFileBytes, isRequired, name }: UploadFieldP
122
119
  }}
123
120
  onClearClick={() => {
124
121
  setFilename('');
122
+ if (!isRequired) {
123
+ void setTouched(false);
124
+ }
125
125
  void setValue('');
126
- void setTouched(false);
127
126
  }}
128
127
  />
129
-
130
- <UploadHelperText maxFileBytes={maxFileBytes} meta={meta} isUploading={isFileUploading} isRejected={isRejected} />
128
+ {maxFileBytes && <UploadMaxFileSizeHelperText maxFileBytes={maxFileBytes} />}
129
+ <UploadErrorHelperText meta={meta} isUploading={isFileUploading} isRejected={isRejected} />
131
130
  </FormGroup>
132
131
  );
133
132
  };
@@ -47,9 +47,9 @@ const K8S_DNS_SUBDOMAIN_START_END = /^[a-z0-9](.*[a-z0-9])?$/;
47
47
  const K8S_DNS_SUBDOMAIN_ALLOWED_CHARACTERS = /^[a-z0-9.-]*$/;
48
48
  const K8S_DNS_SUBDOMAIN_VALUE_MAX_LENGTH = 253;
49
49
 
50
- // https://issues.redhat.com/browse/MGMT-18349 to make the validation more robust
51
- const BASIC_DEVICE_OS_IMAGE_REGEXP = /^[a-zA-Z0-9.\-\/:@_+]*$/;
52
- const APPLICATION_IMAGE_REGEXP = BASIC_DEVICE_OS_IMAGE_REGEXP;
50
+ const OCI_IMAGE_FULL_REGEXP = /^(?![./_])[a-zA-Z0-9.\-\/:@_+]*$/;
51
+ // Accepts all characters from the above regex, but it rejects leading dot, slash, or underscore
52
+ const OCI_IMAGE_ALLOWED_CHARS_REGEXP = /^[a-zA-Z0-9.\-\/:@_+]*$/;
53
53
  const APPLICATION_NAME_REGEXP = /^[a-z0-9]([-a-z0-9]*[a-z0-9])?$/;
54
54
  const APPLICATION_VAR_NAME_REGEXP = /^[a-zA-Z_]+[a-zA-Z0-9_]*$/;
55
55
 
@@ -230,6 +230,23 @@ export const validApplicationAndVolumeName = (t: TFunction) =>
230
230
  t('Use lowercase alphanumeric characters, or dash (-). Must start and end with an alphanumeric character.'),
231
231
  );
232
232
 
233
+ export const getBuildNameValidations = (t: TFunction) => [
234
+ {
235
+ key: 'imageBuildName',
236
+ message: t(
237
+ 'Use lowercase alphanumeric characters, or dash (-). Must start and end with an alphanumeric character.',
238
+ ),
239
+ },
240
+ ];
241
+
242
+ export const validImageBuildName = (t: TFunction) =>
243
+ Yup.string()
244
+ .required(t('Build name is required'))
245
+ .test('imageBuildName', function (value: string | undefined) {
246
+ if (!value) return true;
247
+ return APPLICATION_NAME_REGEXP.test(value) || this.createError({ message: { imageBuildName: 'failed' } });
248
+ });
249
+
233
250
  export const maxLengthString = (t: TFunction, props: { maxLength: number; fieldName: string }) =>
234
251
  Yup.string().max(props.maxLength, t('{{ fieldName }} must not exceed {{ maxLength }} characters', props));
235
252
 
@@ -258,7 +275,7 @@ export const validOsImage = (t: TFunction, { isFleet }: { isFleet: boolean }) =>
258
275
  }
259
276
  }
260
277
 
261
- return BASIC_DEVICE_OS_IMAGE_REGEXP.test(validateOsImage);
278
+ return OCI_IMAGE_FULL_REGEXP.test(validateOsImage);
262
279
  },
263
280
  );
264
281
 
@@ -654,17 +671,27 @@ export const validateMemoryLimit = (memory: string | undefined): boolean => {
654
671
  return true;
655
672
  };
656
673
 
657
- // Common volume validation helpers
658
- const volumeNameSchema = (t: TFunction) => validApplicationAndVolumeName(t).required(t('Volume name is required'));
659
-
660
- const optionalImageRefSchema = (t: TFunction) =>
661
- Yup.string().test('image-ref-format', t('Image reference includes invalid characters.'), (value) => {
662
- if (!value || value.length === 0) {
663
- return true; // Empty is allowed
674
+ const ociImageSchema = (t: TFunction) =>
675
+ Yup.string().test('oci-image-format', (value, testContext) => {
676
+ if (!value) return true;
677
+ if (!OCI_IMAGE_ALLOWED_CHARS_REGEXP.test(value)) {
678
+ return testContext.createError({
679
+ message: t('Image includes invalid characters.'),
680
+ });
681
+ }
682
+ if (!OCI_IMAGE_FULL_REGEXP.test(value)) {
683
+ return testContext.createError({
684
+ message: t('Image must not start with a dot (.), slash (/), or underscore (_).'),
685
+ });
664
686
  }
665
- return APPLICATION_IMAGE_REGEXP.test(value);
687
+ return true;
666
688
  });
667
689
 
690
+ const requiredOciImageSchema = (t: TFunction, requiredMessage?: string) =>
691
+ ociImageSchema(t).required(requiredMessage || t('Image is required.'));
692
+
693
+ const volumeNameSchema = (t: TFunction) => validApplicationAndVolumeName(t).required(t('Volume name is required'));
694
+
668
695
  const imagePullPolicySchema = (t: TFunction) =>
669
696
  Yup.string()
670
697
  .oneOf(
@@ -678,12 +705,11 @@ const mountPathSchema = (t: TFunction, isRequired: boolean = true) => {
678
705
  return isRequired ? schema.required(t('Mount path is required for this volume type')) : schema;
679
706
  };
680
707
 
681
- // Volume validation schemas
682
708
  const singleContainerVolumesSchema = (t: TFunction) =>
683
709
  Yup.array().of(
684
710
  Yup.object().shape({
685
711
  name: volumeNameSchema(t),
686
- imageRef: optionalImageRefSchema(t),
712
+ imageRef: ociImageSchema(t),
687
713
  imagePullPolicy: Yup.string(),
688
714
  mountPath: mountPathSchema(t, true),
689
715
  }),
@@ -693,7 +719,7 @@ const composeQuadletVolumesSchema = (t: TFunction) =>
693
719
  Yup.array().of(
694
720
  Yup.object().shape({
695
721
  name: volumeNameSchema(t),
696
- imageRef: optionalImageRefSchema(t).required(t('Image reference is required for this volume type')),
722
+ imageRef: requiredOciImageSchema(t, t('Image reference is required for this volume type')),
697
723
  imagePullPolicy: imagePullPolicySchema(t),
698
724
  }),
699
725
  );
@@ -710,9 +736,7 @@ export const validApplicationsSchema = (t: TFunction) => {
710
736
  .required(t('Definition source must be image for this type of applications')),
711
737
  appType: Yup.string().oneOf([AppType.AppTypeContainer]).required(t('Application type is required')),
712
738
  name: validApplicationAndVolumeName(t),
713
- image: Yup.string()
714
- .required(t('Image is required.'))
715
- .matches(APPLICATION_IMAGE_REGEXP, t('Application image includes invalid characters.')),
739
+ image: requiredOciImageSchema(t),
716
740
  ports: Yup.array().of(
717
741
  Yup.object()
718
742
  .shape({
@@ -749,9 +773,7 @@ export const validApplicationsSchema = (t: TFunction) => {
749
773
  .required(t('Definition source must be image for this type of applications')),
750
774
  appType: Yup.string().oneOf([AppType.AppTypeHelm]).required(t('Application type is required')),
751
775
  name: validApplicationAndVolumeName(t),
752
- image: Yup.string()
753
- .required(t('Image is required.'))
754
- .matches(APPLICATION_IMAGE_REGEXP, t('Application image includes invalid characters.')),
776
+ image: requiredOciImageSchema(t),
755
777
  namespace: validHelmNamespace(t),
756
778
  valuesYaml: Yup.string().test('valid-yaml', t('YAML content is invalid.'), (value) => {
757
779
  if (!value || value.trim() === '') {
@@ -778,9 +800,7 @@ export const validApplicationsSchema = (t: TFunction) => {
778
800
  .oneOf([AppType.AppTypeCompose, AppType.AppTypeQuadlet])
779
801
  .required(t('Application type is required')),
780
802
  name: validApplicationAndVolumeName(t),
781
- image: Yup.string()
782
- .required(t('Image is required.'))
783
- .matches(APPLICATION_IMAGE_REGEXP, t('Application image includes invalid characters.')),
803
+ image: requiredOciImageSchema(t),
784
804
  volumes: composeQuadletVolumesSchema(t),
785
805
  variables: appVariablesSchema(t),
786
806
  });