@rancher/shell 3.0.11 → 3.0.12-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/assets/styles/base/_mixins.scss +31 -0
  2. package/assets/styles/base/_variables.scss +2 -0
  3. package/assets/styles/themes/_modern.scss +6 -5
  4. package/assets/translations/en-us.yaml +5 -4
  5. package/assets/translations/zh-hans.yaml +0 -3
  6. package/components/EmptyProductPage.vue +76 -0
  7. package/components/Resource/Detail/CopyToClipboard.vue +1 -2
  8. package/components/Resource/Detail/Metadata/KeyValueRow.vue +9 -3
  9. package/components/Resource/Detail/TitleBar/__tests__/__snapshots__/index.test.ts.snap +31 -0
  10. package/components/Resource/Detail/TitleBar/__tests__/index.test.ts +45 -1
  11. package/components/Resource/Detail/TitleBar/index.vue +1 -1
  12. package/components/Resource/Detail/ViewOptions/__tests__/__snapshots__/index.test.ts.snap +9 -0
  13. package/components/Resource/Detail/ViewOptions/__tests__/index.test.ts +62 -0
  14. package/components/Resource/Detail/ViewOptions/index.vue +2 -1
  15. package/components/ResourceList/Masthead.vue +25 -2
  16. package/components/SideNav.vue +13 -0
  17. package/components/__tests__/PromptModal.test.ts +2 -0
  18. package/components/fleet/FleetClusters.vue +1 -0
  19. package/components/fleet/__tests__/FleetClusters.test.ts +71 -0
  20. package/components/form/NodeScheduling.vue +17 -3
  21. package/components/form/PrivateRegistry.vue +69 -0
  22. package/components/form/__tests__/PrivateRegistry.test.ts +133 -0
  23. package/components/formatter/WorkloadHealthScale.vue +3 -1
  24. package/components/nav/Group.vue +26 -3
  25. package/components/nav/Header.vue +32 -7
  26. package/components/nav/TopLevelMenu.vue +15 -1
  27. package/config/pagination-table-headers.js +8 -1
  28. package/config/product/apps.js +2 -1
  29. package/config/product/auth.js +1 -0
  30. package/config/product/backup.js +1 -0
  31. package/config/product/compliance.js +1 -1
  32. package/config/product/explorer.js +25 -6
  33. package/config/product/fleet.js +1 -0
  34. package/config/product/gatekeeper.js +1 -0
  35. package/config/product/istio.js +1 -0
  36. package/config/product/logging.js +1 -0
  37. package/config/product/longhorn.js +2 -1
  38. package/config/product/manager.js +1 -0
  39. package/config/product/monitoring.js +1 -0
  40. package/config/product/navlinks.js +1 -0
  41. package/config/product/neuvector.js +2 -1
  42. package/config/product/settings.js +1 -0
  43. package/config/product/uiplugins.js +1 -0
  44. package/core/__tests__/plugin-products-helpers.test.ts +454 -0
  45. package/core/__tests__/plugin-products.test.ts +3219 -0
  46. package/core/extension-manager-impl.js +30 -1
  47. package/core/plugin-products-base.ts +375 -0
  48. package/core/plugin-products-extending.ts +44 -0
  49. package/core/plugin-products-helpers.ts +262 -0
  50. package/core/plugin-products-top-level.ts +66 -0
  51. package/core/plugin-products-type-guards.ts +33 -0
  52. package/core/plugin-products.ts +50 -0
  53. package/core/plugin-types.ts +222 -0
  54. package/core/plugin.ts +45 -10
  55. package/core/productDebugger.js +48 -0
  56. package/core/types.ts +95 -11
  57. package/detail/__tests__/__snapshots__/fleet.cattle.io.bundle.test.ts.snap +52 -0
  58. package/detail/__tests__/fleet.cattle.io.bundle.test.ts +171 -0
  59. package/detail/fleet.cattle.io.bundle.vue +21 -34
  60. package/dialog/ExtensionCatalogInstallDialog.vue +1 -1
  61. package/dialog/InstallExtensionDialog.vue +6 -27
  62. package/dialog/UninstallExistingExtensionDialog.vue +141 -0
  63. package/dialog/UninstallExtensionDialog.vue +4 -26
  64. package/dialog/__tests__/UninstallExistingExtensionDialog.test.ts +114 -0
  65. package/edit/__tests__/fleet.cattle.io.gitrepo.test.ts +1 -0
  66. package/edit/provisioning.cattle.io.cluster/__tests__/Ingress.test.ts +176 -0
  67. package/edit/provisioning.cattle.io.cluster/rke2.vue +4 -1
  68. package/edit/provisioning.cattle.io.cluster/tabs/Basics.vue +6 -0
  69. package/edit/provisioning.cattle.io.cluster/tabs/Ingress.vue +7 -2
  70. package/list/provisioning.cattle.io.cluster.vue +0 -1
  71. package/list/workload.vue +11 -4
  72. package/mixins/resource-fetch.js +12 -3
  73. package/models/pod.js +18 -0
  74. package/models/workload.js +20 -2
  75. package/package.json +1 -2
  76. package/pages/c/_cluster/apps/charts/AppChartCardFooter.vue +0 -1
  77. package/pages/c/_cluster/settings/brand.vue +4 -4
  78. package/pages/c/_cluster/uiplugins/__tests__/index.test.ts +231 -13
  79. package/pages/c/_cluster/uiplugins/index.vue +143 -37
  80. package/plugins/dashboard-store/__tests__/resource-class.test.ts +1 -0
  81. package/plugins/dashboard-store/actions.js +3 -2
  82. package/plugins/dashboard-store/resource-class.js +62 -6
  83. package/plugins/plugin.js +16 -0
  84. package/plugins/steve/steve-pagination-utils.ts +7 -0
  85. package/scripts/typegen.sh +13 -1
  86. package/store/__tests__/type-map.test.ts +84 -24
  87. package/store/type-map.js +42 -3
  88. package/tsconfig.paths.json +1 -0
  89. package/types/resources/pod.ts +18 -0
  90. package/types/shell/index.d.ts +8506 -2909
  91. package/types/store/dashboard-store.types.ts +5 -0
  92. package/types/store/pagination.types.ts +6 -0
  93. package/utils/axios.js +1 -4
  94. package/utils/dynamic-importer.js +3 -2
  95. package/utils/pagination-utils.ts +1 -1
  96. package/utils/uiplugins.ts +12 -16
  97. package/utils/validators/__tests__/private-registry.test.ts +76 -0
  98. package/utils/validators/private-registry.ts +28 -0
@@ -2,6 +2,7 @@
2
2
  import { mapGetters } from 'vuex';
3
3
  import { RadioGroup } from '@components/Form/Radio';
4
4
  import ResourceLabeledSelect from '@shell/components/form/ResourceLabeledSelect.vue';
5
+ import LabeledSelect from '@shell/components/form/LabeledSelect.vue';
5
6
  import NodeAffinity from '@shell/components/form/NodeAffinity.vue';
6
7
  import { HARVESTER_NAME as VIRTUAL } from '@shell/config/features';
7
8
  import { _VIEW } from '@shell/config/query-params';
@@ -18,6 +19,7 @@ const parseNode = (node: string | KubeNode) => typeof node === 'string' ? node :
18
19
  export default {
19
20
  components: {
20
21
  RadioGroup,
22
+ LabeledSelect,
21
23
  ResourceLabeledSelect,
22
24
  NodeAffinity,
23
25
  },
@@ -35,7 +37,7 @@ export default {
35
37
  */
36
38
  nodes: {
37
39
  type: Array,
38
- default: () => []
40
+ default: () => null
39
41
  },
40
42
 
41
43
  mode: {
@@ -218,14 +220,14 @@ export default {
218
220
  handler(nodeSelector) {
219
221
  // Harvester specific code should not live in rancher/dashboard components
220
222
  // This was brought into harvester/dashboard via https://github.com/harvester/dashboard/pull/342
221
- // rancher/dashboard via https://github.com/rancher/dashboard/pull/6310
223
+ // and then rancher/dashboard via https://github.com/rancher/dashboard/pull/6310
222
224
  if (this.isHarvester && nodeSelector?.[HOSTNAME]) {
223
225
  this.selectNode = 'nodeSelector';
224
226
  const nodeName = nodeSelector[HOSTNAME];
225
227
 
226
228
  this.nodeName = nodeName;
227
229
 
228
- const array = this.nodes.map((n) => n.value);
230
+ const array = this.nodes?.map((n) => n.value) || [];
229
231
 
230
232
  if (nodeName && !array.includes(nodeName)) {
231
233
  this.$store.dispatch('growl/error', {
@@ -259,7 +261,19 @@ export default {
259
261
  <template v-if="selectNode === 'nodeSelector'">
260
262
  <div class="row">
261
263
  <div class="col span-6">
264
+ <LabeledSelect
265
+ v-if="nodes"
266
+ v-model:value="nodeName"
267
+ :label="t('workload.scheduling.affinity.nodeName')"
268
+ :options="nodes || []"
269
+ :mode="mode"
270
+ :multiple="false"
271
+ :loading="loading"
272
+ :data-testid="'node-scheduling-nodeSelector'"
273
+ @update:value="update"
274
+ />
262
275
  <ResourceLabeledSelect
276
+ v-else
263
277
  v-model:value="nodeName"
264
278
  :label="t('workload.scheduling.affinity.nodeName')"
265
279
  :resource-type="NODE"
@@ -0,0 +1,69 @@
1
+ <script setup lang="ts">
2
+ import { ref, watch } from 'vue';
3
+ import Banner from '@components/Banner/Banner.vue';
4
+ import { Checkbox } from '@components/Form/Checkbox';
5
+ import LabeledInput from '@components/Form/LabeledInput/LabeledInput.vue';
6
+
7
+ const props = defineProps<{
8
+ value?: string | null;
9
+ enabled?: boolean;
10
+ mode?: string;
11
+ rules?: Function[];
12
+ checkboxTestId?: string;
13
+ inputTestId?: string;
14
+ }>();
15
+
16
+ const emit = defineEmits<{
17
+ 'update:value': [val: string | null];
18
+ 'update:enabled': [val: boolean];
19
+ }>();
20
+
21
+ const showInput = ref(!!props.value);
22
+
23
+ watch(() => props.enabled, (neu) => {
24
+ if (typeof neu === 'boolean' && neu !== showInput.value) {
25
+ showInput.value = neu;
26
+ }
27
+ });
28
+
29
+ watch(showInput, (neu, old) => {
30
+ if (neu !== props.enabled) {
31
+ emit('update:enabled', neu);
32
+ }
33
+ if (!neu && old && props.value) {
34
+ emit('update:value', null);
35
+ }
36
+ });
37
+
38
+ watch(() => props.value, (neu) => {
39
+ if (!!neu && !showInput.value) {
40
+ showInput.value = true;
41
+ }
42
+ });
43
+ </script>
44
+
45
+ <template>
46
+ <Banner
47
+ color="info"
48
+ class="mt-0"
49
+ label-key="cluster.privateRegistry.importedDescription"
50
+ />
51
+ <Checkbox
52
+ v-model:value="showInput"
53
+ class="mb-20"
54
+ :mode="mode"
55
+ :label="t('cluster.privateRegistry.label')"
56
+ :data-testid="checkboxTestId"
57
+ />
58
+ <LabeledInput
59
+ v-if="showInput"
60
+ :value="value as string"
61
+ :mode="mode"
62
+ :rules="rules"
63
+ :required="true"
64
+ label-key="catalog.chart.registry.custom.inputLabel"
65
+ :data-testid="inputTestId"
66
+ :placeholder="t('catalog.chart.registry.custom.placeholder')"
67
+ @update:value="(val) => emit('update:value', val)"
68
+ />
69
+ </template>
@@ -0,0 +1,133 @@
1
+ import { shallowMount } from '@vue/test-utils';
2
+ import PrivateRegistry from '@shell/components/form/PrivateRegistry.vue';
3
+ import { Checkbox } from '@components/Form/Checkbox';
4
+ import LabeledInput from '@components/Form/LabeledInput/LabeledInput.vue';
5
+
6
+ const defaultMocks = {
7
+ $store: {
8
+ getters: {
9
+ 'i18n/t': (text: string) => text,
10
+ t: (text: string) => text,
11
+ }
12
+ }
13
+ };
14
+
15
+ const mountPrivateRegistry = (props = {}) => {
16
+ return shallowMount(PrivateRegistry, {
17
+ props: {
18
+ mode: 'edit',
19
+ ...props
20
+ },
21
+ global: { mocks: defaultMocks }
22
+ });
23
+ };
24
+
25
+ describe('privateRegistry', () => {
26
+ it('should render the info banner', () => {
27
+ const wrapper = mountPrivateRegistry();
28
+ const banner = wrapper.find('[color="info"]');
29
+
30
+ expect(banner.exists()).toBe(true);
31
+ });
32
+
33
+ it('should render the enable checkbox', () => {
34
+ const wrapper = mountPrivateRegistry();
35
+ const checkbox = wrapper.findComponent(Checkbox);
36
+
37
+ expect(checkbox.exists()).toBe(true);
38
+ });
39
+
40
+ it('should not show the URL input when no value is provided', () => {
41
+ const wrapper = mountPrivateRegistry();
42
+
43
+ expect(wrapper.findComponent(LabeledInput).exists()).toBe(false);
44
+ });
45
+
46
+ it('should show the URL input when a value is provided', () => {
47
+ const wrapper = mountPrivateRegistry({ value: 'registry.example.com' });
48
+
49
+ expect(wrapper.findComponent(LabeledInput).exists()).toBe(true);
50
+ });
51
+
52
+ it('should show the URL input when checkbox is checked', async() => {
53
+ const wrapper = mountPrivateRegistry();
54
+
55
+ expect(wrapper.findComponent(LabeledInput).exists()).toBe(false);
56
+
57
+ const checkbox = wrapper.findComponent(Checkbox);
58
+
59
+ await checkbox.vm.$emit('update:value', true);
60
+ await wrapper.vm.$nextTick();
61
+
62
+ expect(wrapper.findComponent(LabeledInput).exists()).toBe(true);
63
+ });
64
+
65
+ it('should emit update:value with null when checkbox is unchecked', async() => {
66
+ const wrapper = mountPrivateRegistry({ value: 'registry.example.com' });
67
+
68
+ const checkbox = wrapper.findComponent(Checkbox);
69
+
70
+ await checkbox.vm.$emit('update:value', false);
71
+ await wrapper.vm.$nextTick();
72
+
73
+ expect(wrapper.emitted('update:value')).toHaveLength(1);
74
+ expect(wrapper.emitted('update:value')![0]).toStrictEqual([null]);
75
+ });
76
+
77
+ it('should emit update:value when the URL input changes', async() => {
78
+ const wrapper = mountPrivateRegistry({ value: 'registry.example.com' });
79
+ const input = wrapper.findComponent(LabeledInput);
80
+
81
+ await input.vm.$emit('update:value', 'new-registry.example.com');
82
+
83
+ expect(wrapper.emitted('update:value')).toHaveLength(1);
84
+ expect(wrapper.emitted('update:value')![0]).toStrictEqual(['new-registry.example.com']);
85
+ });
86
+
87
+ it('should auto-enable the checkbox when value changes from null to a string', async() => {
88
+ const wrapper = mountPrivateRegistry();
89
+
90
+ expect(wrapper.findComponent(LabeledInput).exists()).toBe(false);
91
+
92
+ await wrapper.setProps({ value: 'registry.example.com' });
93
+
94
+ expect(wrapper.findComponent(LabeledInput).exists()).toBe(true);
95
+ });
96
+
97
+ it('should pass rules to the URL input', () => {
98
+ const mockRule = jest.fn();
99
+ const wrapper = mountPrivateRegistry({
100
+ value: 'registry.example.com',
101
+ rules: [mockRule]
102
+ });
103
+ const input = wrapper.findComponent(LabeledInput);
104
+
105
+ expect(input.attributes('rules')).toBeDefined();
106
+ });
107
+
108
+ it('should apply custom data-testid to checkbox when provided', () => {
109
+ const wrapper = mountPrivateRegistry({ checkboxTestId: 'my-checkbox' });
110
+ const checkbox = wrapper.findComponent(Checkbox);
111
+
112
+ expect(checkbox.attributes('data-testid')).toBe('my-checkbox');
113
+ });
114
+
115
+ it('should apply custom data-testid to input when provided', () => {
116
+ const wrapper = mountPrivateRegistry({
117
+ value: 'registry.example.com',
118
+ inputTestId: 'my-input'
119
+ });
120
+ const input = wrapper.findComponent(LabeledInput);
121
+
122
+ expect(input.attributes('data-testid')).toBe('my-input');
123
+ });
124
+
125
+ it('should not set data-testid when not provided', () => {
126
+ const wrapper = mountPrivateRegistry({ value: 'registry.example.com' });
127
+ const checkbox = wrapper.findComponent(Checkbox);
128
+ const input = wrapper.findComponent(LabeledInput);
129
+
130
+ expect(checkbox.attributes('data-testid')).toBeUndefined();
131
+ expect(input.attributes('data-testid')).toBeUndefined();
132
+ });
133
+ });
@@ -67,7 +67,9 @@ export default {
67
67
  color: `bg-${ value.color }`,
68
68
  value: value.count || 0,
69
69
  label: ucFirst(name)
70
- })).filter((x) => x.value > 0);
70
+ }))
71
+ .filter((x) => x.value > 0)
72
+ .sort((a, b) => a.label.localeCompare(b.label));
71
73
 
72
74
  return 5;
73
75
  },
@@ -269,8 +269,9 @@ export default {
269
269
  @keyup.space="groupSelected()"
270
270
  >
271
271
  <slot name="header">
272
+ <!-- Group overview with link -->
272
273
  <router-link
273
- v-if="hasOverview"
274
+ v-if="hasOverview && hasChildren"
274
275
  :to="headerRoute"
275
276
  :exact="group.children[0].exact"
276
277
  :tabindex="-1"
@@ -279,15 +280,32 @@ export default {
279
280
  <span v-clean-html="group.labelDisplay || group.label" />
280
281
  </h6>
281
282
  </router-link>
283
+ <!-- Non-linked group header -->
282
284
  <h6
283
- v-else
285
+ v-else-if="hasChildren"
284
286
  >
285
287
  <span v-clean-html="group.labelDisplay || group.label" />
286
288
  </h6>
289
+ <!-- Simple child (nav item) -->
290
+ <ul
291
+ v-else
292
+ class="list-unstyled body root-depth"
293
+ v-bind="$attrs"
294
+ >
295
+ <Type
296
+
297
+ :key="id+'_' + group.name + '_type'"
298
+ :is-root="depth == 0 && !showHeader"
299
+ :type="group"
300
+ :depth="depth"
301
+ :highlight-route="highlightRoute"
302
+ @selected="selectType($event)"
303
+ />
304
+ </ul>
287
305
  </slot>
288
306
  </div>
289
307
  <i
290
- v-if="!onlyHasOverview && canCollapse"
308
+ v-if="!onlyHasOverview && canCollapse && hasChildren"
291
309
  class="icon toggle toggle-accordion"
292
310
  :class="{'icon-chevron-right': !isExpanded, 'icon-chevron-down': isExpanded}"
293
311
  role="button"
@@ -377,6 +395,7 @@ export default {
377
395
  display: block;
378
396
  box-sizing:border-box;
379
397
  height: 100%;
398
+
380
399
  &:hover{
381
400
  text-decoration: none;
382
401
  }
@@ -484,6 +503,10 @@ export default {
484
503
  }
485
504
  }
486
505
  }
506
+
507
+ .root-depth :deep() > .child.nav-type a {
508
+ padding-left: 14px;
509
+ }
487
510
  }
488
511
 
489
512
  &.depth-1 {
@@ -203,12 +203,6 @@ export default {
203
203
  return !!this.currentCluster?.actions?.apply;
204
204
  },
205
205
 
206
- prod() {
207
- const name = this.rootProduct.name;
208
-
209
- return this.$store.getters['i18n/withFallback'](`product."${ name }"`, null, ucFirst(name));
210
- },
211
-
212
206
  showSearch() {
213
207
  return this.rootProduct?.inStore === 'cluster';
214
208
  },
@@ -239,6 +233,30 @@ export default {
239
233
  isHarvester() {
240
234
  return this.$store.getters['currentProduct'].inStore === HARVESTER;
241
235
  },
236
+
237
+ productLabel() {
238
+ const name = this.rootProduct.name;
239
+
240
+ // single products do their own thing, which is the previous default behavior as per next line
241
+ if (this.isSingleProduct) {
242
+ return this.$store.getters['i18n/withFallback'](`product."${ name }"`, null, ucFirst(name));
243
+ } else {
244
+ if (this.rootProduct?.label) {
245
+ return this.rootProduct.label;
246
+ }
247
+ if (this.rootProduct?.labelKey) {
248
+ return this.$store.getters['i18n/t'](this.rootProduct.labelKey);
249
+ }
250
+
251
+ return this.$store.getters['i18n/withFallback'](`product."${ name }"`, null, ucFirst(name));
252
+ }
253
+ },
254
+
255
+ // Determine if we are on a route that shows the logo instead of the product label
256
+ // This is to enforce the logo display on certain routes like home, about, prefs, account, etc
257
+ isLogoRoute() {
258
+ return !this.$route.name.includes('c-cluster');
259
+ }
242
260
  },
243
261
 
244
262
  watch: {
@@ -518,7 +536,7 @@ export default {
518
536
  :alt="t('branding.logos.label')"
519
537
  >
520
538
  <div class="product-name">
521
- {{ prod }}
539
+ {{ productLabel }}
522
540
  </div>
523
541
  </div>
524
542
  </div>
@@ -534,6 +552,13 @@ export default {
534
552
  {{ t(isSingleProduct.productNameKey) }}
535
553
  </div>
536
554
 
555
+ <div
556
+ v-else-if="productLabel && !isLogoRoute"
557
+ class="product-name"
558
+ >
559
+ {{ productLabel }}
560
+ </div>
561
+
537
562
  <div
538
563
  v-else
539
564
  class="side-menu-logo"
@@ -186,8 +186,22 @@ export default {
186
186
  to.params.product = p.name;
187
187
  }
188
188
 
189
+ let label;
190
+
191
+ // Allow product to specify its label (old DSL product() did not have "label" or "labelKey")
192
+ // new extensions product registration supports both "label" and "labelKey" (with "labelKey" taking precedence if both are provided)
193
+ if (p.labelKey) {
194
+ label = this.$store.getters['i18n/t'](p.labelKey);
195
+ } else if (p.label) {
196
+ label = p.label;
197
+ }
198
+
199
+ if (!label) {
200
+ label = this.$store.getters['i18n/withFallback'](`product.${ p.name }`, null, ucFirst(p.name));
201
+ }
202
+
189
203
  return {
190
- label: this.$store.getters['i18n/withFallback'](`product."${ p.name }"`, null, ucFirst(p.name)),
204
+ label,
191
205
  icon: `icon-${ p.icon || 'copy' }`,
192
206
  svg: p.svg,
193
207
  value: p.name,
@@ -4,7 +4,8 @@ import {
4
4
  EVENT_LAST_SEEN_TIME,
5
5
  EVENT_TYPE,
6
6
  SECRET_ORIGIN,
7
- EVENT_FIRST_SEEN_TIME
7
+ EVENT_FIRST_SEEN_TIME,
8
+ WORKLOAD_HEALTH_SCALE
8
9
  } from '@shell/config/table-headers';
9
10
 
10
11
  // This file contains table headers
@@ -95,3 +96,9 @@ export const STEVE_SECRET_ORIGIN = {
95
96
  // So we sort by the 'UI_PROJECT_SECRET_COPY' annotation (management.cattle.io/project-scoped-secret-copy) which at least groups the copies.
96
97
  sort: `metadata.annotations[${ UI_PROJECT_SECRET_COPY }]:desc`,
97
98
  };
99
+
100
+ export const STEVE_WORKLOAD_HEALTH_SCALE = {
101
+ ...WORKLOAD_HEALTH_SCALE,
102
+ sort: false,
103
+ search: false,
104
+ };
@@ -29,7 +29,8 @@ export function init(store) {
29
29
  weight: 97,
30
30
  ifHaveGroup: 'catalog.cattle.io',
31
31
  icon: 'marketplace',
32
- showNamespaceFilter: true
32
+ showNamespaceFilter: true,
33
+ extendable: true,
33
34
  });
34
35
 
35
36
  virtualType({
@@ -34,6 +34,7 @@ export function init(store) {
34
34
  removable: false,
35
35
  showClusterSwitcher: false,
36
36
  category: 'configuration',
37
+ extendable: true,
37
38
  });
38
39
 
39
40
  virtualType({
@@ -16,6 +16,7 @@ export function init(store) {
16
16
  product({
17
17
  ifHaveGroup: /^(.*\.)*resources\.cattle\.io$/,
18
18
  icon: 'backup-restore',
19
+ extendable: true,
19
20
  });
20
21
 
21
22
  weightType(BACKUP_RESTORE.BACKUP, 99, true);
@@ -14,7 +14,7 @@ export function init(store) {
14
14
  headers
15
15
  } = DSL(store, NAME);
16
16
 
17
- product({ ifHaveGroup: /^(.*\.)*compliance\.cattle\.io$/ });
17
+ product({ ifHaveGroup: /^(.*\.)*compliance\.cattle\.io$/, extendable: true });
18
18
 
19
19
  weightType(COMPLIANCE.CLUSTER_SCAN, 3, true);
20
20
  weightType(COMPLIANCE.CLUSTER_SCAN_PROFILE, 2, true);
@@ -27,13 +27,15 @@ import {
27
27
 
28
28
  import { DSL } from '@shell/store/type-map';
29
29
  import {
30
- STEVE_AGE_COL, STEVE_EVENT_FIRST_SEEN, STEVE_EVENT_LAST_SEEN, STEVE_EVENT_OBJECT, STEVE_EVENT_TYPE, STEVE_LIST_GROUPS, STEVE_NAMESPACE_COL, STEVE_NAME_COL, STEVE_STATE_COL
30
+ STEVE_AGE_COL, STEVE_EVENT_FIRST_SEEN, STEVE_EVENT_LAST_SEEN, STEVE_EVENT_OBJECT, STEVE_EVENT_TYPE, STEVE_LIST_GROUPS, STEVE_NAMESPACE_COL, STEVE_NAME_COL, STEVE_STATE_COL,
31
+ STEVE_WORKLOAD_HEALTH_SCALE
31
32
  } from '@shell/config/pagination-table-headers';
32
33
 
33
34
  import { COLUMN_BREAKPOINTS } from '@shell/types/store/type-map';
34
35
  import { STEVE_CACHE } from '@shell/store/features';
35
36
  import { configureConditionalDepaginate } from '@shell/store/type-map.utils';
36
37
  import { CATTLE_PUBLIC_ENDPOINTS, STORAGE } from '@shell/config/labels-annotations';
38
+ import { POD_LAST_RESTART_FIELD as POD_RESTARTS_LAST_FIELD, POD_RESTART_FIELD as POD_RESTARTS_COUNT_FIELD } from '@shell/types/resources/pod';
37
39
 
38
40
  export const NAME = 'explorer';
39
41
 
@@ -58,6 +60,7 @@ export function init(store) {
58
60
  weight: 3,
59
61
  showNamespaceFilter: true,
60
62
  icon: 'compass',
63
+ extendable: true,
61
64
  typeStoreMap: {
62
65
  [MANAGEMENT.PROJECT]: 'management',
63
66
  [MANAGEMENT.CLUSTER_ROLE_TEMPLATE_BINDING]: 'management',
@@ -386,11 +389,11 @@ export function init(store) {
386
389
  headers(WORKLOAD, [STATE, NAME_COL, NAMESPACE_COL, TYPE, WORKLOAD_IMAGES, WORKLOAD_ENDPOINTS, POD_RESTARTS, AGE, WORKLOAD_HEALTH_SCALE]);
387
390
  headers(WORKLOAD_TYPES.DEPLOYMENT,
388
391
  [STATE, NAME_COL, NAMESPACE_COL, WORKLOAD_IMAGES, WORKLOAD_ENDPOINTS, 'Ready', 'Up-to-date', 'Available', POD_RESTARTS, AGE, WORKLOAD_HEALTH_SCALE],
389
- [STEVE_STATE_COL, STEVE_NAME_COL, STEVE_NAMESPACE_COL, createSteveWorkloadImageCol(6), STEVE_WORKLOAD_ENDPOINTS, 'Ready', 'Up-to-date', 'Available', STEVE_AGE_COL],
392
+ [STEVE_STATE_COL, STEVE_NAME_COL, STEVE_NAMESPACE_COL, createSteveWorkloadImageCol(6), STEVE_WORKLOAD_ENDPOINTS, 'Ready', 'Up-to-date', 'Available', STEVE_AGE_COL, STEVE_WORKLOAD_HEALTH_SCALE],
390
393
  );
391
394
  headers(WORKLOAD_TYPES.DAEMON_SET,
392
395
  [STATE, NAME_COL, NAMESPACE_COL, WORKLOAD_IMAGES, WORKLOAD_ENDPOINTS, 'Ready', 'Current', 'Desired', POD_RESTARTS, AGE, WORKLOAD_HEALTH_SCALE],
393
- [STEVE_STATE_COL, STEVE_NAME_COL, STEVE_NAMESPACE_COL, createSteveWorkloadImageCol(9), STEVE_WORKLOAD_ENDPOINTS, 'Ready', 'Current', 'Desired', STEVE_AGE_COL]
396
+ [STEVE_STATE_COL, STEVE_NAME_COL, STEVE_NAMESPACE_COL, createSteveWorkloadImageCol(9), STEVE_WORKLOAD_ENDPOINTS, 'Ready', 'Current', 'Desired', STEVE_AGE_COL, STEVE_WORKLOAD_HEALTH_SCALE]
394
397
  );
395
398
  headers(WORKLOAD_TYPES.REPLICA_SET,
396
399
  [STATE, NAME_COL, NAMESPACE_COL, WORKLOAD_IMAGES, WORKLOAD_ENDPOINTS, 'Ready', 'Current', 'Desired', POD_RESTARTS, AGE, WORKLOAD_HEALTH_SCALE],
@@ -398,7 +401,7 @@ export function init(store) {
398
401
  );
399
402
  headers(WORKLOAD_TYPES.STATEFUL_SET,
400
403
  [STATE, NAME_COL, NAMESPACE_COL, WORKLOAD_IMAGES, WORKLOAD_ENDPOINTS, 'Ready', POD_RESTARTS, AGE, WORKLOAD_HEALTH_SCALE],
401
- [STEVE_STATE_COL, STEVE_NAME_COL, STEVE_NAMESPACE_COL, createSteveWorkloadImageCol(4), STEVE_WORKLOAD_ENDPOINTS, 'Ready', STEVE_AGE_COL],
404
+ [STEVE_STATE_COL, STEVE_NAME_COL, STEVE_NAMESPACE_COL, createSteveWorkloadImageCol(4), STEVE_WORKLOAD_ENDPOINTS, 'Ready', STEVE_AGE_COL, STEVE_WORKLOAD_HEALTH_SCALE],
402
405
  );
403
406
  headers(WORKLOAD_TYPES.JOB,
404
407
  [STATE, NAME_COL, NAMESPACE_COL, WORKLOAD_IMAGES, WORKLOAD_ENDPOINTS, 'Completions', DURATION, POD_RESTARTS, AGE, WORKLOAD_HEALTH_SCALE],
@@ -408,7 +411,7 @@ export function init(store) {
408
411
  sort: 'metadata.fields.3',
409
412
  search: 'metadata.fields.3',
410
413
  formatter: undefined, // Now that sort/search is remote we're not doing weird things with start time (see `duration` in model)
411
- }, STEVE_AGE_COL],
414
+ }, STEVE_AGE_COL, STEVE_WORKLOAD_HEALTH_SCALE],
412
415
  );
413
416
  headers(WORKLOAD_TYPES.CRON_JOB,
414
417
  [STATE, NAME_COL, NAMESPACE_COL, WORKLOAD_IMAGES, WORKLOAD_ENDPOINTS, 'Schedule', 'Last Schedule', POD_RESTARTS, AGE, WORKLOAD_HEALTH_SCALE],
@@ -428,7 +431,23 @@ export function init(store) {
428
431
  ...POD_IMAGES,
429
432
  sort: false,
430
433
  search: 'spec.containers.image'
431
- }, 'Ready', 'Restarts', 'IP', {
434
+ },
435
+ 'Ready',
436
+ {
437
+ name: 'pod-restart',
438
+ labelKey: 'tableHeaders.podRestarts',
439
+ search: false,
440
+ sort: [POD_RESTARTS_COUNT_FIELD, POD_RESTARTS_LAST_FIELD, 'metadata.name'],
441
+ value: 'restartsCount',
442
+ }, {
443
+ name: 'pod-last-restart',
444
+ labelKey: 'tableHeaders.podLastRestart',
445
+ value: 'restartsLaster',
446
+ search: false,
447
+ sort: [POD_RESTARTS_LAST_FIELD, POD_RESTARTS_COUNT_FIELD, 'metadata.name'],
448
+ },
449
+ 'IP',
450
+ {
432
451
  ...NODE_COL,
433
452
  search: 'spec.nodeName'
434
453
  },
@@ -32,6 +32,7 @@ export function init(store) {
32
32
  removable: false,
33
33
  showClusterSwitcher: false,
34
34
  showWorkspaceSwitcher: true,
35
+ extendable: true,
35
36
  to: {
36
37
  name: 'c-cluster-fleet',
37
38
  params: { cluster: BLANK_CLUSTER }
@@ -21,6 +21,7 @@ export function init(store) {
21
21
  product({
22
22
  ifHaveGroup: /^(.*\.)?gatekeeper\.sh$/,
23
23
  icon: 'gatekeeper',
24
+ extendable: true,
24
25
  });
25
26
 
26
27
  mapGroup(/^(.*\.)?gatekeeper\.sh$/, 'OPA Gatekeeper');
@@ -18,6 +18,7 @@ export function init(store) {
18
18
  ifHave: IF_HAVE.NOT_V1_ISTIO,
19
19
  icon: 'istio',
20
20
  showNamespaceFilter: true,
21
+ extendable: true,
21
22
  });
22
23
 
23
24
  virtualType({
@@ -21,6 +21,7 @@ export function init(store) {
21
21
  icon: 'logging',
22
22
  showNamespaceFilter: true,
23
23
  weight: 89,
24
+ extendable: true,
24
25
  });
25
26
 
26
27
  basicType([
@@ -20,7 +20,8 @@ export function init(store) {
20
20
 
21
21
  product({
22
22
  ifHaveGroup: 'longhorn.io',
23
- icon: 'longhorn'
23
+ icon: 'longhorn',
24
+ extendable: true,
24
25
  });
25
26
 
26
27
  virtualType({
@@ -38,6 +38,7 @@ export function init(store) {
38
38
  removable: false,
39
39
  showClusterSwitcher: false,
40
40
  weight: -1, // Place at the top
41
+ extendable: true,
41
42
  to: {
42
43
  name: 'c-cluster-product-resource',
43
44
  params: {
@@ -38,6 +38,7 @@ export function init(store) {
38
38
  icon: 'monitoring',
39
39
  showNamespaceFilter: true,
40
40
  weight: 90,
41
+ extendable: true,
41
42
  });
42
43
 
43
44
  virtualType({
@@ -10,6 +10,7 @@ export function init(store) {
10
10
  product({
11
11
  ifHaveType: UI.NAV_LINK,
12
12
  icon: 'external-link',
13
+ extendable: false,
13
14
  });
14
15
 
15
16
  headers(UI.NAV_LINK, [
@@ -12,7 +12,8 @@ export function init(store) {
12
12
  product({
13
13
  ifHaveGroup: 'neuvector.com',
14
14
  ifHave: IF_HAVE.NEUVECTOR_NAMESPACE,
15
- icon: 'neuvector'
15
+ icon: 'neuvector',
16
+ extendable: true,
16
17
  });
17
18
 
18
19
  virtualType({
@@ -28,6 +28,7 @@ export function init(store) {
28
28
  showClusterSwitcher: false,
29
29
  category: 'configuration',
30
30
  weight: 100,
31
+ extendable: true,
31
32
  });
32
33
 
33
34
  virtualType({
@@ -14,5 +14,6 @@ export function init(store) {
14
14
  showClusterSwitcher: false,
15
15
  category: 'configuration',
16
16
  weight: 50,
17
+ extendable: false,
17
18
  });
18
19
  }