@rancher/shell 3.0.12-rc.1 → 3.0.12-rc.2

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 (134) hide show
  1. package/assets/images/providers/entraid-black.svg +4 -0
  2. package/assets/images/providers/entraid.svg +9 -0
  3. package/assets/images/vendor/entraid.svg +9 -0
  4. package/assets/styles/app.scss +0 -1
  5. package/assets/translations/en-us.yaml +19 -17
  6. package/assets/translations/zh-hans.yaml +4 -8
  7. package/chart/__tests__/S3.test.ts +10 -3
  8. package/components/CountBox.vue +20 -0
  9. package/components/CreateDriver.vue +0 -12
  10. package/components/DetailText.vue +12 -3
  11. package/components/SelectIconGrid.vue +5 -0
  12. package/components/__tests__/CountBox.test.ts +72 -0
  13. package/components/__tests__/DetailText.test.ts +113 -0
  14. package/components/fleet/FleetClusterTargets/index.vue +18 -1
  15. package/components/form/InputWithSelect.vue +18 -10
  16. package/components/form/KeyValue.vue +17 -1
  17. package/components/form/LabeledSelect.vue +82 -24
  18. package/components/form/Select.vue +73 -56
  19. package/components/form/ServiceNameSelect.vue +13 -11
  20. package/components/form/__tests__/KeyValue.test.ts +66 -0
  21. package/components/form/__tests__/NodeScheduling.test.ts +9 -0
  22. package/components/form/labeled-select-utils/useLabeledSelectPagination.ts +138 -0
  23. package/components/nav/Group.vue +7 -6
  24. package/components/nav/Header.vue +24 -3
  25. package/components/nav/NotificationCenter/Notification.vue +4 -1
  26. package/components/nav/NotificationCenter/NotificationHeader.vue +20 -8
  27. package/components/nav/NotificationCenter/__tests__/NotificationHeader.test.ts +80 -0
  28. package/components/nav/Type.vue +8 -7
  29. package/components/nav/WindowManager/index.vue +2 -1
  30. package/components/nav/WorkspaceSwitcher.vue +13 -0
  31. package/components/nav/__tests__/Group.test.ts +67 -0
  32. package/components/nav/__tests__/Header.test.ts +235 -0
  33. package/components/nav/__tests__/Type.test.ts +20 -3
  34. package/components/templates/default.vue +34 -4
  35. package/components/templates/home.vue +12 -25
  36. package/components/templates/plain.vue +13 -26
  37. package/composables/useLabeledFormElement.ts +10 -2
  38. package/composables/useLabeledSelect.ts +60 -0
  39. package/composables/useUserRetentionValidation.ts +1 -49
  40. package/config/cookies.js +0 -1
  41. package/config/labels-annotations.js +1 -0
  42. package/config/query-params.js +1 -0
  43. package/config/router/routes.js +0 -8
  44. package/core/__tests__/plugin-products.test.ts +616 -25
  45. package/core/plugin-products-base.ts +31 -14
  46. package/core/plugin-products-helpers.ts +5 -4
  47. package/core/plugin-types.ts +18 -3
  48. package/core/types.ts +3 -1
  49. package/detail/__tests__/management.cattle.io.fleetworkspace.test.ts +128 -0
  50. package/detail/management.cattle.io.fleetworkspace.vue +49 -0
  51. package/edit/__tests__/fleet.cattle.io.helmop.test.ts +9 -0
  52. package/edit/__tests__/kontainerDriver.test.ts +0 -13
  53. package/edit/__tests__/nodeDriver.test.ts +5 -11
  54. package/edit/__tests__/resources.cattle.io.restore.test.ts +9 -0
  55. package/edit/auditlog.cattle.io.auditpolicy/__tests__/__snapshots__/General.test.ts.snap +6 -0
  56. package/edit/auth/__tests__/oidc.test.ts +54 -0
  57. package/edit/auth/azuread.vue +1 -1
  58. package/edit/auth/oidc.vue +8 -0
  59. package/edit/kontainerDriver.vue +1 -2
  60. package/edit/nodeDriver.vue +0 -2
  61. package/edit/provisioning.cattle.io.cluster/AgentEnv.vue +1 -0
  62. package/edit/provisioning.cattle.io.cluster/__tests__/AgentEnv.test.ts +25 -0
  63. package/edit/provisioning.cattle.io.cluster/index.vue +70 -99
  64. package/initialize/App.vue +29 -2
  65. package/initialize/install-plugins.js +0 -2
  66. package/list/__tests__/management.cattle.io.feature.test.ts +105 -0
  67. package/list/catalog.cattle.io.app.vue +25 -5
  68. package/list/management.cattle.io.feature.vue +1 -1
  69. package/list/management.cattle.io.fleetworkspace.vue +8 -0
  70. package/machine-config/amazonec2.vue +1 -0
  71. package/mixins/chart.js +40 -9
  72. package/models/__tests__/catalog.cattle.io.app.test.ts +15 -1
  73. package/models/__tests__/catalog.cattle.io.clusterrepo.test.ts +84 -0
  74. package/models/__tests__/chart.test.ts +99 -6
  75. package/models/__tests__/management.cattle.io.feature.test.ts +131 -0
  76. package/models/__tests__/monitoring.coreos.com.alertmanagerconfig.test.ts +98 -0
  77. package/models/catalog.cattle.io.app.js +21 -17
  78. package/models/catalog.cattle.io.clusterrepo.js +39 -11
  79. package/models/chart.js +33 -19
  80. package/models/fleet-application.js +1 -1
  81. package/models/fleet.cattle.io.bundle.js +1 -1
  82. package/models/kontainerdriver.js +11 -0
  83. package/models/management.cattle.io.authconfig.js +5 -1
  84. package/models/management.cattle.io.cluster.js +0 -53
  85. package/models/management.cattle.io.feature.js +3 -3
  86. package/models/management.cattle.io.kontainerdriver.js +1 -26
  87. package/models/monitoring.coreos.com.alertmanagerconfig.js +31 -17
  88. package/models/nodedriver.js +7 -0
  89. package/package.json +13 -12
  90. package/pages/c/_cluster/apps/charts/__tests__/chart.test.ts +189 -0
  91. package/pages/c/_cluster/apps/charts/__tests__/index.test.ts +55 -0
  92. package/pages/c/_cluster/apps/charts/__tests__/install.test.ts +53 -0
  93. package/pages/c/_cluster/apps/charts/chart.vue +217 -33
  94. package/pages/c/_cluster/apps/charts/index.vue +2 -2
  95. package/pages/c/_cluster/apps/charts/install.vue +8 -3
  96. package/pages/c/_cluster/auth/user.retention/index.vue +55 -22
  97. package/pages/c/_cluster/manager/drivers/kontainerDriver/index.vue +5 -7
  98. package/pages/c/_cluster/uiplugins/PluginInfoPanel.vue +39 -2
  99. package/pages/c/_cluster/uiplugins/__tests__/PluginInfoPanel.test.ts +61 -0
  100. package/pages/c/_cluster/uiplugins/__tests__/index.test.ts +15 -10
  101. package/pages/c/_cluster/uiplugins/index.vue +23 -25
  102. package/rancher-components/Form/LabeledInput/LabeledInput.test.ts +205 -1
  103. package/rancher-components/Form/LabeledInput/LabeledInput.vue +82 -4
  104. package/rancher-components/Form/ToggleSwitch/ToggleSwitch.vue +1 -1
  105. package/scripts/test-plugins-build.sh +5 -2
  106. package/server/server-middleware.js +2 -2
  107. package/static/humans.txt +1 -0
  108. package/static/robots.txt +34 -0
  109. package/static/welcome-cow.svg +18 -0
  110. package/store/__tests__/catalog.test.ts +161 -11
  111. package/store/auth.js +0 -3
  112. package/store/catalog.js +60 -8
  113. package/types/shell/index.d.ts +26 -22
  114. package/utils/__tests__/git.test.ts +270 -0
  115. package/utils/__tests__/inactivity.test.ts +316 -0
  116. package/utils/__tests__/object.test.ts +77 -0
  117. package/utils/__tests__/time.test.ts +14 -1
  118. package/utils/__tests__/url.test.ts +246 -0
  119. package/utils/object.js +33 -2
  120. package/utils/time.ts +5 -0
  121. package/vue.config.js +0 -9
  122. package/assets/images/providers/azuread-black.svg +0 -22
  123. package/assets/images/providers/azuread.svg +0 -25
  124. package/assets/images/vendor/azuread.svg +0 -18
  125. package/assets/styles/fonts/_dots.scss +0 -18
  126. package/components/EmberPage.vue +0 -622
  127. package/components/EmberPageView.vue +0 -39
  128. package/components/form/labeled-select-utils/labeled-select-pagination.ts +0 -116
  129. package/mixins/labeled-form-element.ts +0 -225
  130. package/pages/c/_cluster/explorer/tools/pages/_page.vue +0 -28
  131. package/pages/c/_cluster/manager/pages/_page.vue +0 -22
  132. package/pages/c/_cluster/mcapps/pages/_page.vue +0 -22
  133. package/plugins/ember-cookie.js +0 -17
  134. package/utils/ember-page.js +0 -30
@@ -30,7 +30,7 @@ import {
30
30
  AUTH_TYPE, NAMESPACE as NAMESPACE_TYPE
31
31
  } from '@shell/config/types';
32
32
  import {
33
- CHART, FROM_CLUSTER, FROM_TOOLS, HIDE_SIDE_NAV, NAMESPACE, REPO, REPO_TYPE, VERSION, _FLAGGED
33
+ CHART, FROM_CLUSTER, FROM_TOOLS, HIDE_SIDE_NAV, NEW_APP_INSTANCE, NAMESPACE, REPO, REPO_TYPE, VERSION, _FLAGGED
34
34
  } from '@shell/config/query-params';
35
35
  import { CATALOG as CATALOG_ANNOTATIONS, PROJECT } from '@shell/config/labels-annotations';
36
36
 
@@ -602,8 +602,11 @@ export default {
602
602
  },
603
603
 
604
604
  showSelectVersionOrChart() {
605
- // Allow the user to choose a version if the app exists OR they've come from tools
606
- return this.existing || (FROM_TOOLS in this.$route.query);
605
+ // Allow the user to choose a version if:
606
+ // - the app exists (editing/upgrading)
607
+ // - OR they've come from tools
608
+ // - OR they're installing a new instance of an already-installed chart
609
+ return this.existing || (FROM_TOOLS in this.$route.query) || (NEW_APP_INSTANCE in this.$route.query);
607
610
  },
608
611
 
609
612
  showNameEditor() {
@@ -1110,6 +1113,8 @@ export default {
1110
1113
  this.$router.replace(this.clusterToolsLocation());
1111
1114
  } else if (this.$route.query[FROM_CLUSTER] === _FLAGGED) {
1112
1115
  this.$router.replace(this.clustersLocation());
1116
+ } else if (!this.chart) {
1117
+ this.$router.replace(this.appLocation());
1113
1118
  } else {
1114
1119
  this.$router.replace(this.chartLocation(false));
1115
1120
  }
@@ -1,6 +1,9 @@
1
1
  <script lang="ts" setup>
2
- import { ref, reactive, watch, onMounted } from 'vue';
2
+ import {
3
+ ref, reactive, watch, onMounted, computed
4
+ } from 'vue';
3
5
  import { useRouter, onBeforeRouteUpdate } from 'vue-router';
6
+ import { useForm } from 'vee-validate';
4
7
 
5
8
  import UserRetentionHeader from '@shell/components/user.retention/user-retention-header.vue';
6
9
  import Footer from '@shell/components/form/Footer.vue';
@@ -20,8 +23,15 @@ import { ToggleSwitch } from '@components/Form/ToggleSwitch';
20
23
 
21
24
  import dayjs from 'dayjs';
22
25
 
26
+ type UserRetentionSettingId =
27
+ | typeof SETTING.DISABLE_INACTIVE_USER_AFTER
28
+ | typeof SETTING.DELETE_INACTIVE_USER_AFTER
29
+ | typeof SETTING.USER_RETENTION_CRON
30
+ | typeof SETTING.USER_RETENTION_DRY_RUN
31
+ | typeof SETTING.USER_LAST_LOGIN_DEFAULT;
32
+
23
33
  const store = useStore();
24
- const userRetentionSettings = reactive<{[id: string]: string | null }>({
34
+ const userRetentionSettings = reactive<Record<UserRetentionSettingId, string | null>>({
25
35
  [SETTING.DISABLE_INACTIVE_USER_AFTER]: null,
26
36
  [SETTING.DELETE_INACTIVE_USER_AFTER]: null,
27
37
  [SETTING.USER_RETENTION_CRON]: null,
@@ -38,18 +48,28 @@ const {
38
48
  validateDeleteInactiveUserAfterDuration,
39
49
  validateDeleteInactiveUserAfter,
40
50
  validateDurationAgainstAuthUserSession,
41
- setValidation,
42
- removeValidation,
43
- addValidation,
44
- isFormValid,
45
51
  } = useUserRetentionValidation(disableAfterPeriod, deleteAfterPeriod, authUserSessionTtlMinutes);
52
+
53
+ const { errors, validate: validateForm, validateField } = useForm({
54
+ validationSchema: {
55
+ [SETTING.DISABLE_INACTIVE_USER_AFTER]: (value: string) => validateDisableInactiveUserAfterDuration(value) ??
56
+ validateDurationAgainstAuthUserSession(value) ??
57
+ true,
58
+ [SETTING.DELETE_INACTIVE_USER_AFTER]: (value: string) => validateDeleteInactiveUserAfterDuration(value) ??
59
+ validateDurationAgainstAuthUserSession(value) ??
60
+ validateDeleteInactiveUserAfter(value) ??
61
+ true,
62
+ [SETTING.USER_RETENTION_CRON]: (value: string) => validateUserRetentionCron(value) ?? true,
63
+ },
64
+ });
65
+
46
66
  let settings: { [id: string]: Setting } = {};
47
67
 
48
68
  /**
49
69
  * Watches the disable after period and removes the value if the checkbox is
50
70
  * not selected. Lookup the value when the checkbox is selected.
51
71
  */
52
- watch(disableAfterPeriod, (newVal) => {
72
+ watch(disableAfterPeriod, async(newVal) => {
53
73
  if (!newVal) {
54
74
  userRetentionSettings[SETTING.DISABLE_INACTIVE_USER_AFTER] = null;
55
75
 
@@ -57,13 +77,14 @@ watch(disableAfterPeriod, (newVal) => {
57
77
  }
58
78
 
59
79
  userRetentionSettings[SETTING.DISABLE_INACTIVE_USER_AFTER] = settings[SETTING.DISABLE_INACTIVE_USER_AFTER].value;
80
+ await validateField(SETTING.DISABLE_INACTIVE_USER_AFTER);
60
81
  });
61
82
 
62
83
  /**
63
84
  * Watches the delete after period and removes the value if the checkbox is
64
85
  * not selected. Lookup the value when the checkbox is selected.
65
86
  */
66
- watch(deleteAfterPeriod, (newVal) => {
87
+ watch(deleteAfterPeriod, async(newVal) => {
67
88
  if (!newVal) {
68
89
  userRetentionSettings[SETTING.DELETE_INACTIVE_USER_AFTER] = null;
69
90
 
@@ -71,6 +92,8 @@ watch(deleteAfterPeriod, (newVal) => {
71
92
  }
72
93
 
73
94
  userRetentionSettings[SETTING.DELETE_INACTIVE_USER_AFTER] = settings[SETTING.DELETE_INACTIVE_USER_AFTER].value;
95
+ await validateField(SETTING.DELETE_INACTIVE_USER_AFTER);
96
+ await validateField(SETTING.USER_RETENTION_CRON);
74
97
  });
75
98
 
76
99
  /**
@@ -84,18 +107,19 @@ watch([disableAfterPeriod, deleteAfterPeriod], ([newDisableAfterPeriod, newDelet
84
107
  userRetentionSettings[key] = null;
85
108
  });
86
109
 
87
- removeValidation(SETTING.USER_RETENTION_CRON);
88
-
89
110
  return;
90
111
  }
91
112
 
92
- ids.filter((id) => ![SETTING.DISABLE_INACTIVE_USER_AFTER, SETTING.DELETE_INACTIVE_USER_AFTER].includes(id))
93
- .forEach(assignSettings);
113
+ const skippedIds: readonly UserRetentionSettingId[] = [
114
+ SETTING.DISABLE_INACTIVE_USER_AFTER,
115
+ SETTING.DELETE_INACTIVE_USER_AFTER,
116
+ ];
94
117
 
95
- addValidation(SETTING.USER_RETENTION_CRON);
118
+ ids.filter((id) => !skippedIds.includes(id))
119
+ .forEach(assignSettings);
96
120
  });
97
121
 
98
- const assignSettings = (key: string) => {
122
+ const assignSettings = (key: UserRetentionSettingId) => {
99
123
  if (settings[key].id === SETTING.USER_LAST_LOGIN_DEFAULT && settings[key].value && typeof settings[key].value === 'string') {
100
124
  const value = settings[key].value as string;
101
125
 
@@ -111,7 +135,7 @@ const fetchSetting = async(id: string) => {
111
135
  return await store.dispatch('management/find', { type: MANAGEMENT.SETTING, id });
112
136
  };
113
137
 
114
- const ids = Object.keys(userRetentionSettings);
138
+ const ids = Object.keys(userRetentionSettings) as UserRetentionSettingId[];
115
139
  const settingPromises = ids.map((id) => fetchSetting(id));
116
140
 
117
141
  onMounted(async() => {
@@ -136,6 +160,14 @@ onMounted(async() => {
136
160
  const { t } = useI18n(store);
137
161
  const error = ref<string | null>(null);
138
162
  const save = async(btnCB: (arg: boolean) => void) => {
163
+ const { valid } = await validateForm();
164
+
165
+ if (!valid) {
166
+ btnCB(false);
167
+
168
+ return;
169
+ }
170
+
139
171
  try {
140
172
  error.value = null;
141
173
  ids.forEach((key) => {
@@ -164,6 +196,10 @@ const save = async(btnCB: (arg: boolean) => void) => {
164
196
  }
165
197
  };
166
198
 
199
+ const isFormInvalid = computed(() => {
200
+ return Object.keys(errors.value).length > 0;
201
+ });
202
+
167
203
  const router = useRouter();
168
204
  const routeBack = () => {
169
205
  router.back();
@@ -199,12 +235,11 @@ onBeforeRouteUpdate((_to: unknown, _from: unknown) => {
199
235
  <labeled-input
200
236
  v-model:value="userRetentionSettings[SETTING.DISABLE_INACTIVE_USER_AFTER]"
201
237
  data-testid="disableAfterPeriodInput"
238
+ :name="SETTING.DISABLE_INACTIVE_USER_AFTER"
202
239
  :tooltip="t('user.retention.edit.form.disableAfter.input.tooltip')"
203
240
  class="input-field"
204
241
  :label="t('user.retention.edit.form.disableAfter.input.label')"
205
242
  :disabled="!disableAfterPeriod"
206
- :rules="[validateDisableInactiveUserAfterDuration, validateDurationAgainstAuthUserSession]"
207
- @update:validation="e => setValidation(SETTING.DISABLE_INACTIVE_USER_AFTER, e)"
208
243
  />
209
244
  </div>
210
245
  <div class="input-fieldset">
@@ -216,13 +251,12 @@ onBeforeRouteUpdate((_to: unknown, _from: unknown) => {
216
251
  <labeled-input
217
252
  v-model:value="userRetentionSettings[SETTING.DELETE_INACTIVE_USER_AFTER]"
218
253
  data-testid="deleteAfterPeriodInput"
254
+ :name="SETTING.DELETE_INACTIVE_USER_AFTER"
219
255
  :tooltip="t('user.retention.edit.form.deleteAfter.input.tooltip')"
220
256
  class="input-field"
221
257
  :label="t('user.retention.edit.form.deleteAfter.input.label')"
222
258
  :sub-label="t('user.retention.edit.form.deleteAfter.input.subLabel')"
223
259
  :disabled="!deleteAfterPeriod"
224
- :rules="[validateDeleteInactiveUserAfterDuration, validateDurationAgainstAuthUserSession, validateDeleteInactiveUserAfter]"
225
- @update:validation="e => setValidation(SETTING.DELETE_INACTIVE_USER_AFTER, e)"
226
260
  />
227
261
  </div>
228
262
  <template
@@ -232,14 +266,13 @@ onBeforeRouteUpdate((_to: unknown, _from: unknown) => {
232
266
  <labeled-input
233
267
  v-model:value="userRetentionSettings[SETTING.USER_RETENTION_CRON]"
234
268
  data-testid="userRetentionCron"
269
+ :name="SETTING.USER_RETENTION_CRON"
235
270
  class="input-field"
236
271
  required
237
272
  type="cron"
238
273
  :tooltip="t('user.retention.edit.form.cron.subLabel')"
239
- :rules="[validateUserRetentionCron]"
240
274
  :label="t('user.retention.edit.form.cron.label')"
241
275
  :require-dirty="false"
242
- @update:validation="e => setValidation(SETTING.USER_RETENTION_CRON, e)"
243
276
  />
244
277
  </div>
245
278
  <div class="input-fieldset condensed pt-12">
@@ -268,7 +301,7 @@ onBeforeRouteUpdate((_to: unknown, _from: unknown) => {
268
301
  <Footer
269
302
  class="footer-user-retention"
270
303
  mode="edit"
271
- :disable-save="!isFormValid"
304
+ :disable-save="isFormInvalid"
272
305
  @save="save"
273
306
  @done="routeBack"
274
307
  />
@@ -1,6 +1,5 @@
1
1
  <script>
2
2
  import { NORMAN } from '@shell/config/types';
3
- import { isAdminUser } from '@shell/store/type-map';
4
3
  import ResourceTable from '@shell/components/ResourceTable';
5
4
  import AsyncButton from '@shell/components/AsyncButton';
6
5
  import Loading from '@shell/components/Loading';
@@ -20,18 +19,19 @@ export default {
20
19
  data() {
21
20
  return {
22
21
  allDrivers: null,
23
- canRefreshK8sMetadata: true,
24
22
  resource: NORMAN.KONTAINER_DRIVER,
25
23
  schema: this.$store.getters['rancher/schemaFor'](NORMAN.KONTAINER_DRIVER),
26
24
  useQueryParamsForSimpleFiltering: false,
27
25
  forceUpdateLiveAndDelayed: 10,
28
- showDeprecationBanner: isAdminUser(this.$store.getters),
29
26
  };
30
27
  },
31
28
  computed: {
32
29
  rows() {
33
30
  return this.allDrivers || [];
34
31
  },
32
+ hasEmberUiDrivers() {
33
+ return this.rows.some((driver) => driver.active && driver.isEmber);
34
+ },
35
35
  },
36
36
  methods: {
37
37
  async refreshK8sMetadata(buttonDone) {
@@ -63,7 +63,6 @@ export default {
63
63
  >
64
64
  <template #extraActions>
65
65
  <AsyncButton
66
- v-if="canRefreshK8sMetadata"
67
66
  mode="refresh"
68
67
  :action-label="t('drivers.actions.refresh')"
69
68
  :waiting-label="t('drivers.actions.refresh')"
@@ -75,10 +74,9 @@ export default {
75
74
  </template>
76
75
  </Masthead>
77
76
  <Banner
78
- v-if="showDeprecationBanner"
77
+ v-if="hasEmberUiDrivers"
79
78
  color="warning"
80
- label-key="drivers.kontainer.emberDeprecationMessage"
81
- data-testid="kontainer-driver-ember-deprecation-banner"
79
+ label-key="drivers.kontainer.emberRemovalMessage"
82
80
  />
83
81
  <ResourceTable
84
82
  :schema="schema"
@@ -7,7 +7,9 @@ import genericPluginSvg from '~shell/assets/images/generic-plugin.svg';
7
7
  import { SETTING } from '@shell/config/settings';
8
8
  import { useWatcherBasedSetupFocusTrapWithDestroyIncluded } from '@shell/composables/focusTrap';
9
9
  import { getPluginChartVersionLabel, getPluginChartVersion } from '@shell/utils/uiplugins';
10
- import { isChartVersionHigher } from '@shell/config/uiplugins';
10
+ import { isChartVersionHigher, uiPluginHasAnnotation } from '@shell/config/uiplugins';
11
+ import { CATALOG as CATALOG_ANNOTATIONS } from '@shell/config/labels-annotations';
12
+ import Banner from '@components/Banner/Banner.vue';
11
13
  import RcButton from '@components/RcButton/RcButton.vue';
12
14
  import AppChartCardFooter from '@shell/pages/c/_cluster/apps/charts/AppChartCardFooter.vue';
13
15
 
@@ -25,6 +27,7 @@ export default {
25
27
  }
26
28
  },
27
29
  components: {
30
+ Banner,
28
31
  ChartReadme,
29
32
  LazyImage,
30
33
  RcButton,
@@ -48,6 +51,24 @@ export default {
48
51
  computed: {
49
52
  ...mapGetters({ theme: 'prefs/theme' }),
50
53
 
54
+ errorMessage() {
55
+ return this.info?.installedError || (this.info?.helmError ? this.t('plugins.helmError') : null);
56
+ },
57
+
58
+ warningMessages() {
59
+ const warnings = [];
60
+
61
+ if (uiPluginHasAnnotation(this.info?.chart, CATALOG_ANNOTATIONS.DEPRECATED, 'true')) {
62
+ warnings.push(this.t('plugins.deprecatedExtension'));
63
+ }
64
+
65
+ if (this.info?.incompatibilityMessage) {
66
+ warnings.push(this.info.incompatibilityMessage);
67
+ }
68
+
69
+ return warnings;
70
+ },
71
+
51
72
  applyDarkModeBg() {
52
73
  if (this.theme === 'dark') {
53
74
  return { 'dark-mode': true };
@@ -307,6 +328,20 @@ export default {
307
328
  :items="info.tags"
308
329
  class="plugin-tags-container"
309
330
  />
331
+ <Banner
332
+ v-for="(msg, i) in warningMessages"
333
+ :key="i"
334
+ color="warning"
335
+ >
336
+ {{ msg }}
337
+ </Banner>
338
+
339
+ <Banner
340
+ v-if="errorMessage"
341
+ color="error"
342
+ >
343
+ {{ errorMessage }}
344
+ </Banner>
310
345
 
311
346
  <div class="plugin-versions-container">
312
347
  <h3>
@@ -452,8 +487,10 @@ export default {
452
487
  flex-direction: column;
453
488
  overflow: hidden;
454
489
 
455
- .banner.warning {
490
+ .banner.warning,
491
+ .banner.error {
456
492
  margin-top: 0;
493
+ margin-bottom: 32px;
457
494
  }
458
495
 
459
496
  .plugin-info-detail {
@@ -1,5 +1,6 @@
1
1
  import { shallowMount, VueWrapper } from '@vue/test-utils';
2
2
  import PluginInfoPanel from '@shell/pages/c/_cluster/uiplugins/PluginInfoPanel.vue';
3
+ import { CATALOG as CATALOG_ANNOTATIONS } from '@shell/config/labels-annotations';
3
4
 
4
5
  jest.mock('@shell/config/uiplugins', () => ({
5
6
  ...jest.requireActual('@shell/config/uiplugins'),
@@ -99,4 +100,64 @@ describe('component: PluginInfoPanel', () => {
99
100
  expect(label).toBe('1.0.0 (plugins.labels.current)');
100
101
  });
101
102
  });
103
+
104
+ describe('errorMessage', () => {
105
+ beforeEach(() => {
106
+ wrapper = mountComponent();
107
+ });
108
+
109
+ it('should return installedError if present', () => {
110
+ wrapper.vm.info = { installedError: 'install error' };
111
+
112
+ expect(wrapper.vm.errorMessage).toBe('install error');
113
+ });
114
+
115
+ it('should return translated helmError if present', () => {
116
+ wrapper.vm.info = { helmError: true };
117
+
118
+ expect(wrapper.vm.errorMessage).toBe('plugins.helmError');
119
+ });
120
+
121
+ it('should return null if no error', () => {
122
+ wrapper.vm.info = {};
123
+
124
+ expect(wrapper.vm.errorMessage).toBeNull();
125
+ });
126
+ });
127
+
128
+ describe('warningMessages', () => {
129
+ beforeEach(() => {
130
+ wrapper = mountComponent();
131
+ });
132
+
133
+ it('should include deprecated message if the extension chart has the deprecated annotation', () => {
134
+ wrapper.vm.info = { chart: { versions: [{ annotations: { [CATALOG_ANNOTATIONS.DEPRECATED]: 'true' } }] } };
135
+
136
+ expect(wrapper.vm.warningMessages).toContain('plugins.deprecatedExtension');
137
+ });
138
+
139
+ it('should include incompatibilityMessage if present', () => {
140
+ wrapper.vm.info = { incompatibilityMessage: 'incompatibility error' };
141
+
142
+ expect(wrapper.vm.warningMessages).toContain('incompatibility error');
143
+ });
144
+
145
+ it('should include both deprecated and incompatibility messages if both are present', () => {
146
+ wrapper.vm.info = {
147
+ chart: { versions: [{ annotations: { [CATALOG_ANNOTATIONS.DEPRECATED]: 'true' } }] },
148
+ incompatibilityMessage: 'incompatibility error'
149
+ };
150
+
151
+ expect(wrapper.vm.warningMessages).toStrictEqual([
152
+ 'plugins.deprecatedExtension',
153
+ 'incompatibility error'
154
+ ]);
155
+ });
156
+
157
+ it('should return an empty array if neither is present', () => {
158
+ wrapper.vm.info = { chart: { versions: [{ annotations: { [CATALOG_ANNOTATIONS.CERTIFIED]: 'rancher' } }] } };
159
+
160
+ expect(wrapper.vm.warningMessages).toStrictEqual([]);
161
+ });
162
+ });
102
163
  });
@@ -1,6 +1,7 @@
1
1
  import { shallowMount, VueWrapper } from '@vue/test-utils';
2
2
  import UiPluginsPage from '@shell/pages/c/_cluster/uiplugins/index.vue';
3
3
  import { UI_PLUGIN_NAMESPACE } from '@shell/config/uiplugins';
4
+ import { CATALOG as CATALOG_ANNOTATIONS } from '@shell/config/labels-annotations';
4
5
 
5
6
  const t = (key: string, args: Object) => {
6
7
  if (args) {
@@ -292,10 +293,11 @@ describe('page: UI plugins/Extensions', () => {
292
293
  });
293
294
 
294
295
  it('should return "deprecated" status for deprecated plugins', () => {
295
- const plugin = { chart: { deprecated: true } };
296
+ const plugin = { chart: { versions: [{ annotations: { [CATALOG_ANNOTATIONS.DEPRECATED]: 'true' } }] } };
296
297
  const statuses = wrapper.vm.getStatuses(plugin);
297
298
 
298
- expect(statuses[0].tooltip.key).toBe('generic.deprecated');
299
+ expect(statuses[0].tooltip.text).toBe('generic.deprecated');
300
+ expect(statuses[0].color).toBe('error');
299
301
  });
300
302
 
301
303
  it('should return error status for installedError', () => {
@@ -303,15 +305,17 @@ describe('page: UI plugins/Extensions', () => {
303
305
  const statuses = wrapper.vm.getStatuses(plugin);
304
306
 
305
307
  expect(statuses[0].icon).toBe('icon-alert-alt');
306
- expect(statuses[0].tooltip.text).toBe('generic.error: An error occurred');
308
+ expect(statuses[0].color).toBe('error');
309
+ expect(statuses[0].tooltip.text).toBe('An error occurred');
307
310
  });
308
311
 
309
- it('should return error status for incompatibilityMessage', () => {
312
+ it('should return warning status for incompatibilityMessage', () => {
310
313
  const plugin = { incompatibilityMessage: 'Incompatible version' };
311
314
  const statuses = wrapper.vm.getStatuses(plugin);
312
315
 
313
316
  expect(statuses[0].icon).toBe('icon-alert-alt');
314
- expect(statuses[0].tooltip.text).toBe('generic.error: Incompatible version');
317
+ expect(statuses[0].color).toBe('error');
318
+ expect(statuses[0].tooltip.text).toBe('Incompatible version');
315
319
  });
316
320
 
317
321
  it('should return error status for helmError', () => {
@@ -319,15 +323,16 @@ describe('page: UI plugins/Extensions', () => {
319
323
  const statuses = wrapper.vm.getStatuses(plugin);
320
324
 
321
325
  expect(statuses[0].icon).toBe('icon-alert-alt');
322
- expect(statuses[0].tooltip.text).toBe('generic.error: plugins.helmError');
326
+ expect(statuses[0].color).toBe('error');
327
+ expect(statuses[0].tooltip.text).toBe('plugins.helmError');
323
328
  });
324
329
 
325
- it('should combine deprecated and other errors in tooltip', () => {
326
- const plugin = { chart: { deprecated: true }, helmError: true };
330
+ it('should combine deprecated and error messages in a single tooltip', () => {
331
+ const plugin = { chart: { versions: [{ annotations: { [CATALOG_ANNOTATIONS.DEPRECATED]: 'true' } }] }, helmError: true };
327
332
  const statuses = wrapper.vm.getStatuses(plugin);
328
- const warningStatus = statuses.find((status: any) => status.icon === 'icon-alert-alt');
333
+ const errorStatus = statuses.find((status: any) => status.color === 'error');
329
334
 
330
- expect(warningStatus.tooltip.text).toBe('generic.deprecated. generic.error: plugins.helmError');
335
+ expect(errorStatus.tooltip.text).toBe('generic.deprecated<br/>plugins.helmError');
331
336
  });
332
337
  });
333
338
 
@@ -5,7 +5,8 @@ import { mapPref, PLUGIN_DEVELOPER } from '@shell/store/prefs';
5
5
  import { sortBy } from '@shell/utils/sort';
6
6
  import genericPluginSvg from '~shell/assets/images/generic-plugin.svg';
7
7
  import { allHash } from '@shell/utils/promise';
8
- import { CATALOG, UI_PLUGIN, MANAGEMENT, ZERO_TIME } from '@shell/config/types';
8
+ import { CATALOG, UI_PLUGIN, MANAGEMENT } from '@shell/config/types';
9
+ import { isMissingDate } from '@shell/utils/time';
9
10
  import { SETTING } from '@shell/config/settings';
10
11
  import { fetchOrCreateSetting } from '@shell/utils/settings';
11
12
  import { getVersionData, isRancherPrime } from '@shell/config/version';
@@ -896,18 +897,12 @@ export default {
896
897
  label: plugin.displayVersionLabel,
897
898
  }];
898
899
 
899
- if (plugin.created) {
900
- const hasZeroTime = plugin.created === ZERO_TIME;
901
- const lastUpdatedItem = {
900
+ if (!isMissingDate(plugin.created)) {
901
+ items.push({
902
902
  icon: 'icon-refresh-alt',
903
903
  iconTooltip: { key: 'tableHeaders.lastUpdated' },
904
- label: hasZeroTime ? this.t('generic.na') : day(plugin.created).format('MMM D, YYYY')
905
- };
906
-
907
- if (hasZeroTime) {
908
- lastUpdatedItem.labelTooltip = this.t('catalog.charts.appChartCard.subHeaderItem.missingVersionDate');
909
- }
910
- items.push(lastUpdatedItem);
904
+ label: day(plugin.created).format('MMM D, YYYY')
905
+ });
911
906
  }
912
907
 
913
908
  if (plugin.installing) {
@@ -989,24 +984,27 @@ export default {
989
984
  getStatuses(plugin) {
990
985
  const statuses = [];
991
986
 
992
- const errorTooltip = plugin.installedError || plugin.incompatibilityMessage || (plugin.helmError ? this.t('plugins.helmError') : null);
993
- const isDeprecated = plugin?.chart?.deprecated;
987
+ const errorMsg = plugin.installedError || (plugin.helmError ? this.t('plugins.helmError') : null);
988
+ const incompatibilityMsg = plugin.incompatibilityMessage;
989
+ const isDeprecated = uiPluginHasAnnotation(plugin?.chart, CATALOG_ANNOTATIONS.DEPRECATED, 'true');
994
990
 
995
- if (isDeprecated || errorTooltip) {
996
- let tooltip;
991
+ const tooltipMsgs = [];
997
992
 
998
- if (isDeprecated && errorTooltip) {
999
- tooltip = { text: `${ this.t('generic.deprecated') }. ${ this.t('generic.error') }: ${ errorTooltip }` };
1000
- } else if (isDeprecated) {
1001
- tooltip = { key: 'generic.deprecated' };
1002
- } else { // errorTooltip is present
1003
- tooltip = { text: `${ this.t('generic.error') }: ${ errorTooltip }` };
1004
- }
993
+ if (isDeprecated) {
994
+ tooltipMsgs.push(this.t('generic.deprecated'));
995
+ }
996
+
997
+ if (errorMsg) {
998
+ tooltipMsgs.push(errorMsg);
999
+ } else if (incompatibilityMsg) {
1000
+ tooltipMsgs.push(incompatibilityMsg);
1001
+ }
1005
1002
 
1003
+ if (tooltipMsgs.length) {
1006
1004
  statuses.push({
1007
- icon: 'icon-alert-alt',
1008
- color: 'error',
1009
- tooltip
1005
+ icon: 'icon-alert-alt',
1006
+ color: 'error',
1007
+ tooltip: { text: tooltipMsgs.join('<br/>') }
1010
1008
  });
1011
1009
  }
1012
1010