@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
@@ -1,4 +1,6 @@
1
- import { mount } from '@vue/test-utils';
1
+ import { defineComponent, nextTick } from 'vue';
2
+ import { mount, flushPromises } from '@vue/test-utils';
3
+ import { useForm } from 'vee-validate';
2
4
  import { LabeledInput } from './index';
3
5
 
4
6
  describe('component: LabeledInput', () => {
@@ -105,4 +107,206 @@ describe('component: LabeledInput', () => {
105
107
  expect(mainInput.attributes('aria-label')).toBeUndefined();
106
108
  expect(wrapper.find('label').text()).toBe(label);
107
109
  });
110
+
111
+ describe('vee-validate integration', () => {
112
+ const i18nMock = { $store: { getters: { 'i18n/t': jest.fn() } } };
113
+
114
+ it('without name prop: existing rules-based validation message is shown after blur', async() => {
115
+ const errorMessage = 'This field cannot be empty';
116
+ const notEmptyRule = (v: string) => (!v ? errorMessage : undefined);
117
+
118
+ const wrapper = mount(LabeledInput, {
119
+ propsData: {
120
+ rules: [notEmptyRule],
121
+ value: '',
122
+ },
123
+ mocks: i18nMock
124
+ });
125
+
126
+ await wrapper.find('input').trigger('blur');
127
+ await nextTick();
128
+
129
+ expect(wrapper.vm.validationMessage).toBe(errorMessage);
130
+ });
131
+
132
+ it('without name prop: error CSS class is not applied automatically', async() => {
133
+ const notEmptyRule = (v: string) => (!v ? 'Error' : undefined);
134
+
135
+ const wrapper = mount(LabeledInput, {
136
+ propsData: {
137
+ rules: [notEmptyRule],
138
+ value: '',
139
+ },
140
+ mocks: i18nMock
141
+ });
142
+
143
+ await wrapper.find('input').trigger('blur');
144
+ await nextTick();
145
+
146
+ expect(wrapper.find('.labeled-input').classes()).not.toContain('error');
147
+ });
148
+
149
+ it('with name prop: name attribute is set on the input element', () => {
150
+ const wrapper = mount(LabeledInput, {
151
+ propsData: { name: 'myField' },
152
+ mocks: i18nMock
153
+ });
154
+
155
+ expect(wrapper.find('input').attributes('name')).toStrictEqual('myField');
156
+ });
157
+
158
+ it('with name prop: existing rules run through vee-validate and show error message after blur', async() => {
159
+ const errorMessage = 'Field cannot be empty';
160
+ const notEmptyRule = (v: string) => (!v ? errorMessage : undefined);
161
+
162
+ const wrapper = mount(LabeledInput, {
163
+ propsData: {
164
+ name: 'testField',
165
+ rules: [notEmptyRule],
166
+ value: '',
167
+ },
168
+ mocks: i18nMock
169
+ });
170
+
171
+ await wrapper.find('input').trigger('blur');
172
+ await flushPromises();
173
+
174
+ expect(wrapper.vm.validationMessage).toStrictEqual(errorMessage);
175
+ });
176
+
177
+ it('with name prop: no error class when validation passes', async() => {
178
+ const notEmptyRule = (v: string) => (!v ? 'Error' : undefined);
179
+
180
+ const wrapper = mount(LabeledInput, {
181
+ propsData: {
182
+ name: 'testField',
183
+ rules: [notEmptyRule],
184
+ value: 'valid value',
185
+ },
186
+ mocks: i18nMock
187
+ });
188
+
189
+ await wrapper.find('input').trigger('blur');
190
+ await flushPromises();
191
+
192
+ expect(wrapper.find('.labeled-input').classes()).not.toContain('error');
193
+ expect(wrapper.vm.validationMessage).toBeUndefined();
194
+ });
195
+
196
+ it('with name prop: form-level validation schema error is shown when the form validates', async() => {
197
+ const errorMessage = 'Username is required';
198
+ let triggerFormValidation!: () => Promise<unknown>;
199
+
200
+ const TestWrapper = defineComponent({
201
+ components: { LabeledInput },
202
+ setup() {
203
+ const { validate } = useForm({
204
+ validationSchema: { username: (v: string) => (!v ? errorMessage : true) },
205
+ initialValues: { username: '' }
206
+ });
207
+
208
+ triggerFormValidation = validate;
209
+
210
+ return {};
211
+ },
212
+ template: '<LabeledInput name="username" value="" />'
213
+ });
214
+
215
+ const wrapper = mount(TestWrapper, { global: { mocks: { $store: { getters: { 'i18n/t': jest.fn() } } } } });
216
+
217
+ await triggerFormValidation();
218
+ await flushPromises();
219
+
220
+ const labeledInput = wrapper.findComponent(LabeledInput);
221
+
222
+ expect(labeledInput.vm.validationMessage).toStrictEqual(errorMessage);
223
+ });
224
+
225
+ it('without name prop: error clears when a previously invalid value becomes valid', async() => {
226
+ const errorMessage = 'This field cannot be empty';
227
+ const notEmptyRule = (v: string) => (!v ? errorMessage : undefined);
228
+
229
+ const wrapper = mount(LabeledInput, {
230
+ propsData: {
231
+ rules: [notEmptyRule],
232
+ value: '',
233
+ },
234
+ mocks: i18nMock
235
+ });
236
+
237
+ await wrapper.find('input').trigger('blur');
238
+ await nextTick();
239
+
240
+ expect(wrapper.vm.validationMessage).toBe(errorMessage);
241
+
242
+ await wrapper.setProps({ value: 'valid value' });
243
+ await nextTick();
244
+
245
+ expect(wrapper.vm.validationMessage).toBeUndefined();
246
+ });
247
+
248
+ it('with name prop: error clears when a previously invalid value becomes valid', async() => {
249
+ const errorMessage = 'Field cannot be empty';
250
+ const notEmptyRule = (v: string) => (!v ? errorMessage : undefined);
251
+
252
+ const wrapper = mount(LabeledInput, {
253
+ propsData: {
254
+ name: 'testField',
255
+ rules: [notEmptyRule],
256
+ value: '',
257
+ },
258
+ mocks: i18nMock
259
+ });
260
+
261
+ await wrapper.find('input').trigger('blur');
262
+ await flushPromises();
263
+
264
+ expect(wrapper.vm.validationMessage).toStrictEqual(errorMessage);
265
+
266
+ await wrapper.setProps({ value: 'valid value' });
267
+ await flushPromises();
268
+
269
+ expect(wrapper.vm.validationMessage).toBeUndefined();
270
+ });
271
+
272
+ describe('with both name and rules provided', () => {
273
+ it('shows the error message exactly once when invalid (not duplicated across both validation paths)', async() => {
274
+ const errorMessage = 'Field cannot be empty';
275
+ const notEmptyRule = (v: string) => (!v ? errorMessage : undefined);
276
+
277
+ const wrapper = mount(LabeledInput, {
278
+ propsData: {
279
+ name: 'testField',
280
+ rules: [notEmptyRule],
281
+ value: '',
282
+ },
283
+ mocks: i18nMock
284
+ });
285
+
286
+ await wrapper.find('input').trigger('blur');
287
+ await flushPromises();
288
+
289
+ expect(wrapper.vm.validationMessage).toStrictEqual(errorMessage);
290
+ expect(wrapper.vm.validationMessage).not.toContain(`${ errorMessage }, ${ errorMessage }`);
291
+ });
292
+
293
+ it('shows no error when the value satisfies the rules', async() => {
294
+ const notEmptyRule = (v: string) => (!v ? 'Field cannot be empty' : undefined);
295
+
296
+ const wrapper = mount(LabeledInput, {
297
+ propsData: {
298
+ name: 'testField',
299
+ rules: [notEmptyRule],
300
+ value: 'valid value',
301
+ },
302
+ mocks: i18nMock
303
+ });
304
+
305
+ await wrapper.find('input').trigger('blur');
306
+ await flushPromises();
307
+
308
+ expect(wrapper.vm.validationMessage).toBeUndefined();
309
+ });
310
+ });
311
+ });
108
312
  });
@@ -1,5 +1,6 @@
1
1
  <script lang="ts">
2
- import { defineComponent, inject } from 'vue';
2
+ import { defineComponent, inject, computed, watch } from 'vue';
3
+ import { useField } from 'vee-validate';
3
4
  import TextAreaAutoGrow from '@components/Form/TextArea/TextAreaAutoGrow.vue';
4
5
  import LabeledTooltip from '@components/LabeledTooltip/LabeledTooltip.vue';
5
6
  import { escapeHtml, generateRandomAlphaString } from '@shell/utils/string';
@@ -114,6 +115,15 @@ export default defineComponent({
114
115
  ariaLabel: {
115
116
  type: String,
116
117
  default: ''
118
+ },
119
+
120
+ /**
121
+ * The field name used for vee-validate integration. When provided, the
122
+ * component registers with a parent vee-validate form context
123
+ */
124
+ name: {
125
+ type: String,
126
+ default: null
117
127
  }
118
128
  },
119
129
 
@@ -132,15 +142,72 @@ export default defineComponent({
132
142
 
133
143
  const onInput = inject('onInput', provideProps.onInput);
134
144
 
145
+ // Stable fallback name so useField is always called unconditionally.
146
+ // When no name prop is given the field won't match any form-schema path.
147
+ const standaloneFieldId = `__field__${ generateRandomAlphaString(12) }`;
148
+ const veeFieldName = computed(() => props.name || standaloneFieldId);
149
+
150
+ // Pass existing rules to vee-validate so field-level validation still runs
151
+ const veeValidator = (value: unknown): boolean | string => {
152
+ // Return true when name is absent to avoid duplicating
153
+ // useLabeledFormElement validation
154
+ if (!props.name) {
155
+ return true;
156
+ }
157
+ for (const rule of props.rules as Array<(v: unknown) => string | undefined>) {
158
+ const msg = rule(value);
159
+
160
+ if (msg) {
161
+ return msg;
162
+ }
163
+ }
164
+
165
+ return true;
166
+ };
167
+
168
+ const {
169
+ errorMessage: veeError,
170
+ handleBlur: veeHandleBlur,
171
+ validate: veeValidate,
172
+ value: veeValue,
173
+ } = useField<string | number | Record<string, unknown>>(
174
+ veeFieldName,
175
+ veeValidator,
176
+ {
177
+ initialValue: props.value as string | number,
178
+ validateOnValueUpdate: true,
179
+ }
180
+ );
181
+
182
+ // Keep vee-validate's internal value in sync with the controlled prop value.
183
+ watch(() => props.value, (v) => {
184
+ if (veeValue.value !== v) {
185
+ veeValue.value = v as string | number;
186
+ }
187
+ });
188
+
189
+ const effectiveValidationMessage = computed(() => {
190
+ if (props.name && veeError.value) {
191
+ return veeError.value;
192
+ }
193
+
194
+ return validationMessage.value;
195
+ });
196
+
197
+ const effectiveStatus = computed(() => props.status);
198
+
135
199
  return {
136
200
  focused,
137
201
  onFocusLabeled,
138
202
  onBlurLabeled,
139
203
  onInput,
140
204
  isDisabled,
141
- validationMessage,
205
+ validationMessage: effectiveValidationMessage,
142
206
  requiredField,
143
207
  isCompact,
208
+ veeHandleBlur,
209
+ veeValidate,
210
+ effectiveStatus,
144
211
  };
145
212
  },
146
213
 
@@ -339,6 +406,11 @@ export default defineComponent({
339
406
  onBlur(event: string | FocusEvent): void {
340
407
  this.$emit('blur', event);
341
408
  this.onBlurLabeled();
409
+ // Mark the field as touched in vee-validate without relying on its
410
+ // 'validated-only' guard, then run validation unconditionally so
411
+ // errors surface on the first blur (matching useLabeledFormElement behavior).
412
+ this.veeHandleBlur(event instanceof FocusEvent ? event : undefined, false);
413
+ this.veeValidate();
342
414
  },
343
415
 
344
416
  escapeHtml
@@ -353,7 +425,7 @@ export default defineComponent({
353
425
  focused,
354
426
  [mode]: true,
355
427
  disabled: isDisabled,
356
- [status]: status,
428
+ [effectiveStatus]: effectiveStatus,
357
429
  suffix: hasSuffix,
358
430
  'has-clean-tooltip': hasTooltip,
359
431
  'compact-input': isCompact,
@@ -389,13 +461,14 @@ export default defineComponent({
389
461
  ref="value"
390
462
  v-bind="$attrs"
391
463
  v-stripped-aria-label="!hasLabel && ariaLabel ? ariaLabel : undefined"
464
+ :name="name || undefined"
392
465
  :maxlength="_maxlength"
393
466
  :disabled="isDisabled"
394
467
  :aria-disabled="isDisabled"
395
468
  :value="value || ''"
396
469
  :placeholder="_placeholder"
397
470
  autocapitalize="off"
398
- :class="{ conceal: type === 'multiline-password' }"
471
+ :class="{ 'multiline-password': type === 'multiline-password' }"
399
472
  :aria-describedby="ariaDescribedBy"
400
473
  :aria-required="requiredField"
401
474
  @update:value="onInput"
@@ -410,6 +483,7 @@ export default defineComponent({
410
483
  :role="type === 'number' ? undefined : 'textbox'"
411
484
  :class="{ 'no-label': !hasLabel }"
412
485
  v-bind="$attrs"
486
+ :name="name || undefined"
413
487
  :maxlength="_maxlength"
414
488
  :disabled="isDisabled"
415
489
  :aria-disabled="isDisabled"
@@ -464,6 +538,10 @@ export default defineComponent({
464
538
  </div>
465
539
  </template>
466
540
  <style scoped lang="scss">
541
+ .multiline-password:not(:focus) {
542
+ -webkit-text-security: disc;
543
+ }
544
+
467
545
  .labeled-input.view {
468
546
  input {
469
547
  text-overflow: ellipsis;
@@ -6,7 +6,7 @@ type StateType = boolean | 'true' | 'false' | undefined;
6
6
  export default defineComponent({
7
7
  props: {
8
8
  value: {
9
- type: [Boolean, String, Number],
9
+ type: [Boolean, String, Number, null],
10
10
  default: false
11
11
  },
12
12
 
@@ -236,10 +236,13 @@ function clone_repo_test_extension_build() {
236
236
  # Here we just add the extension that we want to include as a check (all our official extensions should be included here)
237
237
  # Don't forget to add the unit tests exception to clone_repo_test_extension_build function if a new extension has those
238
238
  # TODO: ISSUE #16858 - Reenable the tests as packages migrate to node version 24
239
- # clone_repo_test_extension_build "rancher" "kubewarden-ui" "kubewarden"
239
+ clone_repo_test_extension_build "rancher" "kubewarden-ui" "kubewarden"
240
240
  # clone_repo_test_extension_build "rancher" "elemental-ui" "elemental"
241
241
  clone_repo_test_extension_build "neuvector" "manager-ext" "neuvector-ui-ext"
242
242
  # clone_repo_test_extension_build "StackVista" "rancher-extension-stackstate" "observability"
243
- # clone_repo_test_extension_build "harvester" "harvester-ui-extension" "harvester"
243
+ clone_repo_test_extension_build "harvester" "harvester-ui-extension" "harvester"
244
+ clone_repo_test_extension_build "rancher" "ali-ui" "ali"
245
+ clone_repo_test_extension_build "rancher" "virtual-clusters-ui" "virtual-clusters"
246
+ # clone_repo_test_extension_build "rancher" "rancher-ai-ui" "rancher-ai-ui"
244
247
 
245
248
  echo "All done"
@@ -1,6 +1,6 @@
1
1
  module.exports = function(req, res, next) {
2
- // We do this redirect so that /verify-auth can work with both standalone and
3
- // while dashboard is nested under ember.
2
+ // We do this redirect so that /verify-auth can work with both standalone
3
+ // and hosted configurations.
4
4
  if (req.url.includes('/verify-auth') || req.url.includes('/verify-auth-azure')) {
5
5
  res.writeHead(301, { Location: req.url.replace(/verify-auth(-azure)?/, 'auth/verify') });
6
6
  res.end();
@@ -0,0 +1 @@
1
+ This product was created by the inspirational people at Rancher by SUSE.
@@ -0,0 +1,34 @@
1
+ # ::::::::::::::::::::::::::::::;;;;;;;:::::::::::::::::::;:::::::::::::::::::
2
+ # ::::::::::::::::::::::;+xxxxxxxxxxxxxxxxxxxx+;:::::::::+xxx+::::::::::::::::
3
+ # :::::::::::::::::;xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx;::::+xxxxxxx+::::::::::::
4
+ # ::::::::::::::+xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx;+xxxxxxxxxxx+::::::::
5
+ # :::::::::::+xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx;::;++;:::::
6
+ # :::::::::+xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx+::::::;+;::::
7
+ # :::::::;xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx::::;++:++::::
8
+ # ::::::+xxxxxxxxxx++xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx+::::;:;xx::::
9
+ # :::::+xxxxxx+;:::::::::;;xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx+;;;+xxx;:::
10
+ # ::::;xxxxx+:::::::::::::::;+xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx+;;+xxxxxxxx;:::
11
+ # ::::xxxxx;::::;+xxxxxx++::::;xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx++++++++;::::
12
+ # ::::xxxx+::::;xxx+;;;+xxx;::::xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx+;;::::::
13
+ # ::::xxxx+::::+xx;:::::;+xx;::::xxxxxxxxxxxxxxxxxxxxxxx;:::;;;;;;;:::::::::::
14
+ # ::::xxxxx;:::+xx+::::::;xxx;::::xxxxxx;:::::::;;+xxxxx;:::::::::::::::::::::
15
+ # ::::;xxxx+;:::;++::::::;xxx;::::;xxxxx+;:::::::::;+xxxx;::::::::::::::::::::
16
+ # :::::+xxxx+;::::::::::;xxxx;:::::;+xxxxxx+;:::::::;+xxxxx+;:::::::::::::::::
17
+ # ::::::;xxxxx++;;:::;;+xxxx;:::::::::::::::::::::::::::::::::::::::::::::::::
18
+ # :::::::;+xxxxxxxxxxxxxxxx;::::::::::::::::::::::::::::::::::::::::::::::::::
19
+ # :::::::::;++xxxxxxxxxx++::::::::::::::::::::::::::::::::::::::::::::::::::::
20
+ # ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
21
+ # ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
22
+ # ::::::::;;;;;::::::::::;;:::::::::;;:::::::::;;;;;::::::::::::;;;;;;;;;:::::
23
+ # :::::+X&&$$$$&$+::::::x&$+:::::::X&X;:::::;X$&$$$$&$x;::::::x&&$$$$$$$$x::::
24
+ # ::::x&$X;::::x$&X:::::x&$+:::::::X&X;::::+$&$;::::+$$X;::::;&$;:::::::::::::
25
+ # :::;$&X;::::::::::::::x&$+:::::::X&X;::::X&$;::::::::::::::+&$::::::::::::::
26
+ # ::::x&&$X;;:::::::::::x&$+:::::::X&X;::::+$&&X+;:::::::::::+&$;:::::::::::::
27
+ # :::::;X$&&&&&&$;::::::x&$+:::::::X&X;::::::x$&&&&&&$+::::::+&&&&&&&&&&+:::::
28
+ # :::::::::::;x$&&$:::::x&$+:::::::X&X;::::::::::::+$&&$+::::+&$::::::::::::::
29
+ # :::::;::::::::X$&;::::+$$x:::::::$&X;:::::;::::::::x$$+::::+&$::::::::::::::
30
+ # :::;$&$X;:::;X$&$:::::;X&$X;:::x$&$+::::;X&$X;:::;x$&$+::::;&$x;;;;;;;;:::::
31
+ # :::::+X&&&&&&&X;::::::::+X$&&&&&$x;:::::::;X$&&&&&&$+:::::::;$&&&&&&&&&x::::
32
+ # :::::::::;;;::::::::::::::::;;:::::::::::::::::;;::::::::::::::::::::::::::;
33
+ User-Agent: *
34
+ Disallow: /
@@ -0,0 +1,18 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <!-- Generator: Adobe Illustrator 19.2.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
3
+ <svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
4
+ viewBox="0 0 305.3 143.2" style="enable-background:new 0 0 305.3 143.2;" xml:space="preserve">
5
+ <style type="text/css">
6
+ .st0{fill:#0876A9;}
7
+ </style>
8
+ <g>
9
+ <path class="st0" d="M305.2,31.5l-3.3-19.9C300.8,5.2,298.3,0,296.4,0c-2,0-3.6,5.3-3.6,11.8v5.2c0,6.5-5.3,11.8-11.8,11.8h-5.2
10
+ c-0.4,0-0.7,0-1.1,0V43c0.4,0,0.7,0,1.1,0h19.5C301.8,43.1,306.2,37.8,305.2,31.5"/>
11
+ <path class="st0" d="M258.3,14.6h-31.7c-0.3,0-0.5,0-0.8,0h-32.5c-0.4,0-0.8,0-1.1,0.1v-3c0-6.5-1.6-11.8-3.6-11.8
12
+ s-4.5,5.2-5.5,11.6l-3.3,19.9c-1.1,6.4,3.4,11.6,9.8,11.6h19.5c2,0,3.9-0.3,5.6-0.9c-0.6,3.3-3.5,5.9-7,5.9h-27.4
13
+ c-4.4,0-7.8-4-7.1-8.4L176,23c0.7-4.4-2.6-8.4-7.1-8.4H32.5c-2.9,0-5.4,1.7-6.5,4.2L0.3,58c-0.4,0.7-0.4,1.5,0.1,2.1l5,5.9
14
+ c0.6,0.7,1.7,0.8,2.5,0.2l17.5-13.8v83.6c0,4,3.2,7.2,7.2,7.2h38.7c4,0,7.2-3.2,7.2-7.2V107c0-4,3.2-7.2,7.2-7.2h96.7
15
+ c4,0,7.2,3.2,7.2,7.2v29.1c0,4,3.2,7.2,7.2,7.2h38.7c4,0,7.2-3.2,7.2-7.2v-31.3h-20.6c-6.5,0-11.8-5.3-11.8-11.8V72.9
16
+ c0-3.8,1.9-7.2,4.7-9.4v24c0,6.5,5.3,11.8,11.8,11.8h31.7c6.5,0,11.8-5.3,11.8-11.8V26.4C270.1,19.9,264.8,14.6,258.3,14.6"/>
17
+ </g>
18
+ </svg>
@@ -1,30 +1,54 @@
1
1
  import { CATALOG } from '@shell/config/types';
2
+ import { CATALOG as CATALOG_ANNOTATIONS } from '@shell/config/labels-annotations';
2
3
  import {
3
4
  state, getters, actions, mutations, filterAndArrangeCharts
4
5
  } from '../catalog';
5
6
  import { createStore } from 'vuex';
6
7
 
7
8
  const clusterRepo = { _key: 'testClusterRepo' };
8
- const repoChartName = 'abc';
9
+ const repoChartName = 'regular-chart';
9
10
  const repoChart = {
10
11
  name: repoChartName,
11
12
  type: 'namespaced',
12
13
  version: 1,
13
14
  metadata: { name: repoChartName }
14
15
  };
15
- const repoCharts = [repoChart];
16
+ const deprecatedByFieldChartName = 'deprecated-by-field-chart';
17
+ const deprecatedByFieldChart = {
18
+ name: deprecatedByFieldChartName,
19
+ type: 'namespaced',
20
+ version: 1,
21
+ deprecated: true,
22
+ metadata: { name: deprecatedByFieldChartName }
23
+ };
24
+ const deprecatedByAnnotationChartName = 'deprecated-by-annotation-chart';
25
+ const deprecatedByAnnotationChart = {
26
+ name: deprecatedByAnnotationChartName,
27
+ type: 'namespaced',
28
+ version: 1,
29
+ annotations: { [CATALOG_ANNOTATIONS.DEPRECATED]: 'true' },
30
+ metadata: { name: deprecatedByAnnotationChartName }
31
+ };
16
32
  const repo = {
17
33
  metadata: { name: 'testRepo' },
18
34
  _key: 'testRepo',
19
35
  links: { index: 'fetchindex' },
20
36
  canLoad: true,
21
- followLink: () => ({ entries: { [repoChartName]: repoCharts } })
37
+ followLink: () => ({
38
+ entries: {
39
+ [repoChartName]: [repoChart],
40
+ [deprecatedByFieldChartName]: [deprecatedByFieldChart],
41
+ [deprecatedByAnnotationChartName]: [deprecatedByAnnotationChart]
42
+ }
43
+ })
22
44
  };
23
45
 
24
46
  const catalogStoreName = 'catalog';
25
47
  const clusterStore = 'cluster';
26
48
 
27
49
  const expectedChartKey = `namespace/${ repo._key }/${ repoChartName }`;
50
+ const expectedDeprecatedByFieldChartKey = `namespace/${ repo._key }/${ deprecatedByFieldChartName }`;
51
+ const expectedDeprecatedByAnnotationChartKey = `namespace/${ repo._key }/${ deprecatedByAnnotationChartName }`;
28
52
  const initialVersionInfo = { junk: true };
29
53
  const initialRawChartName = 'cde';
30
54
  const initialRawChart = {
@@ -160,10 +184,20 @@ describe('catalog', () => {
160
184
  expect(rawCharts[expectedChartKey]).toBeDefined();
161
185
  expect(rawCharts[expectedChartKey].id).toBe(expectedChartKey);
162
186
  expect(rawCharts[expectedChartKey].versions[0].version).toBe(repoChart.version);
163
- expect(charts[0]).toBeDefined();
164
- expect(charts[0].id).toBe(initialRawChart.id);
165
- expect(charts[1]).toBeDefined();
166
- expect(charts[1].id).toBe(expectedChartKey);
187
+
188
+ expect(rawCharts[expectedDeprecatedByFieldChartKey]).toBeDefined();
189
+ expect(rawCharts[expectedDeprecatedByFieldChartKey].id).toBe(expectedDeprecatedByFieldChartKey);
190
+ expect(rawCharts[expectedDeprecatedByFieldChartKey].deprecated).toBe(true);
191
+
192
+ expect(rawCharts[expectedDeprecatedByAnnotationChartKey]).toBeDefined();
193
+ expect(rawCharts[expectedDeprecatedByAnnotationChartKey].id).toBe(expectedDeprecatedByAnnotationChartKey);
194
+ expect(rawCharts[expectedDeprecatedByAnnotationChartKey].deprecated).toBe(true);
195
+
196
+ expect(charts).toHaveLength(4);
197
+ expect(charts.find((c: any) => c.id === initialRawChart.id)).toBeDefined();
198
+ expect(charts.find((c: any) => c.id === expectedChartKey)).toBeDefined();
199
+ expect(charts.find((c: any) => c.id === expectedDeprecatedByFieldChartKey)).toBeDefined();
200
+ expect(charts.find((c: any) => c.id === expectedDeprecatedByAnnotationChartKey)).toBeDefined();
167
201
 
168
202
  // Version info should be unchanged
169
203
  expect(store.state[catalogStoreName].versionInfos).toStrictEqual(initialVersionInfo);
@@ -197,14 +231,130 @@ describe('catalog', () => {
197
231
  expect(rawCharts[expectedChartKey].id).toBe(expectedChartKey);
198
232
  expect(rawCharts[expectedChartKey].versions[0].version).toBe(repoChart.version);
199
233
 
200
- expect(charts).toHaveLength(1);
201
- expect(charts[0]).toBeDefined();
202
- expect(charts[0].id).toBe(expectedChartKey);
203
- expect(charts[0].versions[0].version).toBe(repoChart.version);
234
+ expect(rawCharts[expectedDeprecatedByFieldChartKey]).toBeDefined();
235
+ expect(rawCharts[expectedDeprecatedByFieldChartKey].id).toBe(expectedDeprecatedByFieldChartKey);
236
+ expect(rawCharts[expectedDeprecatedByFieldChartKey].deprecated).toBe(true);
237
+
238
+ expect(rawCharts[expectedDeprecatedByAnnotationChartKey]).toBeDefined();
239
+ expect(rawCharts[expectedDeprecatedByAnnotationChartKey].id).toBe(expectedDeprecatedByAnnotationChartKey);
240
+ expect(rawCharts[expectedDeprecatedByAnnotationChartKey].deprecated).toBe(true);
241
+
242
+ expect(charts).toHaveLength(3);
243
+ expect(charts.find((c: any) => c.id === expectedChartKey)).toBeDefined();
244
+ expect(charts.find((c: any) => c.id === expectedDeprecatedByFieldChartKey)).toBeDefined();
245
+ expect(charts.find((c: any) => c.id === expectedDeprecatedByAnnotationChartKey)).toBeDefined();
204
246
 
205
247
  // Version info should be changed (it's now empty given reset)
206
248
  expect(store.state[catalogStoreName].versionInfos).toStrictEqual({ });
207
249
  });
250
+
251
+ it('repoKeys provided', async() => {
252
+ const store = createStore(constructStore());
253
+
254
+ // Validate initial state of store
255
+ expect(store.getters[`${ catalogStoreName }/rawCharts`]).toStrictEqual(initialRawCharts);
256
+ expect(store.getters[`${ catalogStoreName }/charts`]).toStrictEqual([]);
257
+
258
+ // Make the request targeting specific repo (we provide repo._key)
259
+ await store.dispatch(`${ catalogStoreName }/load`, {
260
+ force: true,
261
+ repoKeys: [repo._key]
262
+ });
263
+
264
+ const rawCharts = store.getters[`${ catalogStoreName }/rawCharts`];
265
+ const charts = store.getters[`${ catalogStoreName }/charts`];
266
+
267
+ // We expect the old chart belonging to this repo to be wiped out
268
+ // and replaced entirely by the newly fetched chart.
269
+ expect(rawCharts[initialRawChart.id]).not.toBeDefined();
270
+ expect(rawCharts[expectedChartKey]).toBeDefined();
271
+ expect(rawCharts[expectedChartKey].id).toBe(expectedChartKey);
272
+ expect(rawCharts[expectedChartKey].versions[0].version).toBe(repoChart.version);
273
+
274
+ expect(rawCharts[expectedDeprecatedByFieldChartKey]).toBeDefined();
275
+ expect(rawCharts[expectedDeprecatedByFieldChartKey].id).toBe(expectedDeprecatedByFieldChartKey);
276
+ expect(rawCharts[expectedDeprecatedByFieldChartKey].deprecated).toBe(true);
277
+
278
+ expect(rawCharts[expectedDeprecatedByAnnotationChartKey]).toBeDefined();
279
+ expect(rawCharts[expectedDeprecatedByAnnotationChartKey].id).toBe(expectedDeprecatedByAnnotationChartKey);
280
+ expect(rawCharts[expectedDeprecatedByAnnotationChartKey].deprecated).toBe(true);
281
+
282
+ expect(charts).toHaveLength(3);
283
+ expect(charts.find((c: any) => c.id === expectedChartKey)).toBeDefined();
284
+ expect(charts.find((c: any) => c.id === expectedDeprecatedByFieldChartKey)).toBeDefined();
285
+ expect(charts.find((c: any) => c.id === expectedDeprecatedByAnnotationChartKey)).toBeDefined();
286
+ });
287
+
288
+ it('repoKeys provided, ignoring unrelated charts and versions', async() => {
289
+ // We will manually inject an unrelated chart to prove it isn't wiped out
290
+ const store = createStore(constructStore());
291
+ const unrelatedChart = {
292
+ id: `namespace/unrelatedRepo/unrelatedChart`,
293
+ repoKey: 'unrelatedRepo',
294
+ type: 'namespaced',
295
+ name: 'unrelatedChart',
296
+ version: 1,
297
+ metadata: { name: 'unrelatedChart' }
298
+ };
299
+
300
+ const targetedVersionKey = `namespace/testRepo/myChart/1.0.0`;
301
+ const unrelatedVersionKey = `namespace/unrelatedRepo/unrelatedChart/1.0.0`;
302
+
303
+ store.state[catalogStoreName].charts = {
304
+ ...initialRawCharts,
305
+ [unrelatedChart.id]: unrelatedChart
306
+ };
307
+
308
+ store.state[catalogStoreName].versionInfos = {
309
+ [targetedVersionKey]: { data: 'old-data' },
310
+ [unrelatedVersionKey]: { data: 'keep-me' }
311
+ };
312
+
313
+ // Make the request targeting specific repo
314
+ await store.dispatch(`${ catalogStoreName }/load`, {
315
+ force: true,
316
+ repoKeys: [repo._key]
317
+ });
318
+
319
+ const rawCharts = store.getters[`${ catalogStoreName }/rawCharts`];
320
+ const versionInfos = store.state[catalogStoreName].versionInfos;
321
+
322
+ // The unrelated chart should NOT be wiped
323
+ expect(rawCharts[unrelatedChart.id]).toBeDefined();
324
+ expect(rawCharts[unrelatedChart.id].repoKey).toBe('unrelatedRepo');
325
+
326
+ // The old initialRawChart (with repoKey: repo._key) SHOULD be wiped and replaced
327
+ expect(rawCharts[initialRawChart.id]).not.toBeDefined();
328
+ expect(rawCharts[expectedChartKey]).toBeDefined();
329
+
330
+ expect(rawCharts[expectedDeprecatedByFieldChartKey]).toBeDefined();
331
+ expect(rawCharts[expectedDeprecatedByAnnotationChartKey]).toBeDefined();
332
+
333
+ // The targeted version info SHOULD be wiped
334
+ expect(versionInfos[targetedVersionKey]).not.toBeDefined();
335
+ // The unrelated version info should NOT be wiped
336
+ expect(versionInfos[unrelatedVersionKey]).toBeDefined();
337
+ expect(versionInfos[unrelatedVersionKey].data).toBe('keep-me');
338
+ });
339
+ });
340
+
341
+ describe('refresh', () => {
342
+ it('calls refresh(false) on all repos and then dispatches a reset load', async() => {
343
+ const mockRepo1 = { refresh: jest.fn().mockResolvedValue(true) };
344
+ const mockRepo2 = { refresh: jest.fn().mockResolvedValue(true) };
345
+
346
+ const getters = { repos: [mockRepo1, mockRepo2] };
347
+ const dispatch = jest.fn().mockResolvedValue(true);
348
+ const commit = jest.fn();
349
+
350
+ await actions.refresh({
351
+ getters, commit, dispatch
352
+ });
353
+
354
+ expect(mockRepo1.refresh).toHaveBeenCalledWith(false);
355
+ expect(mockRepo2.refresh).toHaveBeenCalledWith(false);
356
+ expect(dispatch).toHaveBeenCalledWith('load', { force: true, reset: true });
357
+ });
208
358
  });
209
359
 
210
360
  describe('filterAndArrangeCharts', () => {