@rancher/shell 3.0.7 → 3.0.8-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 (83) hide show
  1. package/assets/images/vendor/githubapp.svg +13 -0
  2. package/assets/styles/base/_typography.scss +1 -1
  3. package/assets/styles/themes/_modern.scss +5 -5
  4. package/assets/translations/en-us.yaml +91 -11
  5. package/assets/translations/zh-hans.yaml +0 -4
  6. package/components/Inactivity.vue +222 -106
  7. package/components/InstallHelmCharts.vue +2 -2
  8. package/components/ResourceDetail/index.vue +1 -1
  9. package/components/SortableTable/index.vue +17 -2
  10. package/components/fleet/FleetConfigMapSelector.vue +117 -0
  11. package/components/fleet/FleetSecretSelector.vue +127 -0
  12. package/components/fleet/__tests__/FleetConfigMapSelector.test.ts +125 -0
  13. package/components/fleet/__tests__/FleetSecretSelector.test.ts +82 -0
  14. package/components/form/FileImageSelector.vue +13 -4
  15. package/components/form/FileSelector.vue +11 -2
  16. package/components/form/ResourceLabeledSelect.vue +1 -0
  17. package/components/form/__tests__/ResourceLabeledSelect.test.ts +90 -0
  18. package/components/nav/Header.vue +1 -0
  19. package/config/product/auth.js +1 -0
  20. package/config/query-params.js +1 -0
  21. package/config/settings.ts +8 -1
  22. package/config/types.js +2 -0
  23. package/dialog/AddonConfigConfirmationDialog.vue +45 -1
  24. package/edit/__tests__/fleet.cattle.io.helmop.test.ts +52 -11
  25. package/edit/auth/AuthProviderWarningBanners.vue +14 -1
  26. package/edit/auth/github-app-steps.vue +97 -0
  27. package/edit/auth/github-steps.vue +75 -0
  28. package/edit/auth/github.vue +94 -65
  29. package/edit/fleet.cattle.io.helmop.vue +51 -2
  30. package/edit/networking.k8s.io.networkpolicy/PolicyRuleTarget.vue +15 -5
  31. package/edit/provisioning.cattle.io.cluster/__tests__/rke2.test.ts +11 -9
  32. package/edit/provisioning.cattle.io.cluster/rke2.vue +56 -9
  33. package/edit/provisioning.cattle.io.cluster/tabs/AddOnConfig.vue +28 -2
  34. package/list/projectsecret.vue +1 -1
  35. package/machine-config/azure.vue +1 -1
  36. package/mixins/chart.js +1 -1
  37. package/models/__tests__/chart.test.ts +17 -9
  38. package/models/__tests__/compliance.cattle.io.clusterscanprofile.spec.js +30 -0
  39. package/models/catalog.cattle.io.app.js +1 -1
  40. package/models/chart.js +3 -1
  41. package/models/compliance.cattle.io.clusterscanprofile.js +1 -1
  42. package/models/management.cattle.io.authconfig.js +1 -0
  43. package/package.json +2 -2
  44. package/pages/auth/login.vue +5 -2
  45. package/pages/auth/verify.vue +1 -1
  46. package/pages/c/_cluster/apps/charts/AppChartCardSubHeader.vue +3 -2
  47. package/pages/c/_cluster/apps/charts/chart.vue +2 -2
  48. package/pages/c/_cluster/explorer/EventsTable.vue +89 -3
  49. package/pages/c/_cluster/explorer/tools/index.vue +3 -3
  50. package/pages/c/_cluster/settings/performance.vue +12 -25
  51. package/pages/home.vue +313 -12
  52. package/plugins/axios.js +2 -1
  53. package/plugins/dashboard-store/actions.js +1 -1
  54. package/plugins/dashboard-store/resource-class.js +17 -2
  55. package/plugins/steve/steve-pagination-utils.ts +2 -2
  56. package/scripts/extension/publish +1 -1
  57. package/store/auth.js +8 -3
  58. package/store/aws.js +8 -6
  59. package/store/features.js +1 -0
  60. package/store/index.js +9 -3
  61. package/store/prefs.js +6 -0
  62. package/types/kube/kube-api.ts +2 -1
  63. package/types/rancher/index.d.ts +1 -0
  64. package/types/resources/settings.d.ts +29 -7
  65. package/types/shell/index.d.ts +59 -0
  66. package/utils/__tests__/cluster.test.ts +379 -1
  67. package/utils/cluster.js +157 -3
  68. package/utils/dynamic-content/__tests__/config.test.ts +187 -0
  69. package/utils/dynamic-content/__tests__/index.test.ts +390 -0
  70. package/utils/dynamic-content/__tests__/info.test.ts +263 -0
  71. package/utils/dynamic-content/__tests__/new-release.test.ts +216 -0
  72. package/utils/dynamic-content/__tests__/support-notice.test.ts +262 -0
  73. package/utils/dynamic-content/__tests__/util.test.ts +235 -0
  74. package/utils/dynamic-content/config.ts +55 -0
  75. package/utils/dynamic-content/index.ts +273 -0
  76. package/utils/dynamic-content/info.ts +219 -0
  77. package/utils/dynamic-content/new-release.ts +126 -0
  78. package/utils/dynamic-content/support-notice.ts +169 -0
  79. package/utils/dynamic-content/types.d.ts +101 -0
  80. package/utils/dynamic-content/util.ts +122 -0
  81. package/utils/inactivity.ts +104 -0
  82. package/utils/pagination-utils.ts +19 -4
  83. package/utils/release-notes.ts +1 -1
@@ -475,16 +475,17 @@ describe('component: rke2', () => {
475
475
  },
476
476
  provider: 'custom'
477
477
  },
478
- data: () => ({
479
- agentArgs: {
478
+ computed: {
479
+ ...rke2.computed,
480
+ agentArgs: () => ({
480
481
  'cloud-provider-name': {
481
482
  options: [
482
483
  'azure',
483
484
  'amazon'
484
485
  ]
485
486
  }
486
- }
487
- }),
487
+ })
488
+ },
488
489
  global: {
489
490
  mocks: {
490
491
  ...defaultMocks,
@@ -519,9 +520,10 @@ describe('component: rke2', () => {
519
520
  },
520
521
  provider: 'custom'
521
522
  },
522
- data: () => ({
523
- canAzureMigrateOnEdit: true,
524
- agentArgs: {
523
+ computed: {
524
+ ...rke2.computed,
525
+ canAzureMigrateOnEdit: () => true,
526
+ agentArgs: () => ({
525
527
  'cloud-provider-name': {
526
528
  options: [
527
529
  'azure',
@@ -529,8 +531,8 @@ describe('component: rke2', () => {
529
531
  'external'
530
532
  ]
531
533
  }
532
- }
533
- }),
534
+ })
535
+ },
534
536
  global: {
535
537
  mocks: {
536
538
  ...defaultMocks,
@@ -28,7 +28,7 @@ import {
28
28
  } from '@shell/utils/object';
29
29
  import { allHash } from '@shell/utils/promise';
30
30
  import {
31
- getAllOptionsAfterCurrentVersion, filterOutDeprecatedPatchVersions, isHarvesterSatisfiesVersion, labelForAddon, initSchedulingCustomization
31
+ getAllOptionsAfterCurrentVersion, filterOutDeprecatedPatchVersions, isHarvesterSatisfiesVersion, labelForAddon, initSchedulingCustomization, addonConfigPreserve
32
32
  } from '@shell/utils/cluster';
33
33
 
34
34
  import { BadgeState } from '@components/BadgeState';
@@ -162,6 +162,10 @@ export default {
162
162
  this.schedulingCustomizationOriginallyEnabled = sc.schedulingCustomizationOriginallyEnabled;
163
163
  this.errors = this.errors.concat(sc.errors);
164
164
 
165
+ if (this.isEdit) {
166
+ this.originalKubeVersion = this.versionOptions.find((v) => v.value === this.liveValue.spec.kubernetesVersion);
167
+ }
168
+
165
169
  Object.entries(this.chartValues).forEach(([name, value]) => {
166
170
  const key = this.chartVersionKey(name);
167
171
 
@@ -278,6 +282,9 @@ export default {
278
282
  REGISTRIES_TAB_NAME,
279
283
  labelForAddon,
280
284
  etcdConfigValid: true,
285
+ addonConfigDiffs: {},
286
+ originalKubeVersion: null,
287
+ isEmpty,
281
288
  };
282
289
  },
283
290
 
@@ -921,8 +928,22 @@ export default {
921
928
  }
922
929
  },
923
930
 
924
- selectedVersion() {
925
- this.versionInfo = {}; // Invalidate cache such that version info relevent to selected kube version is updated
931
+ async selectedVersion(neu) {
932
+ if (this.isEdit) {
933
+ const {
934
+ addonConfigDiffs, addonNames, userChartValues, $store
935
+ } = this;
936
+
937
+ await addonConfigPreserve(
938
+ {
939
+ addonConfigDiffs, addonNames, userChartValues, $store
940
+ },
941
+ this.originalKubeVersion?.charts,
942
+ neu?.charts
943
+ );
944
+ }
945
+
946
+ this.versionInfo = {}; // Invalidate cache such that version info relevant to selected kube version is updated
926
947
 
927
948
  // Allow time for addonNames to update... then fetch any missing addons
928
949
  this.$nextTick(() => this.initAddons());
@@ -1511,10 +1532,15 @@ export default {
1511
1532
  });
1512
1533
  },
1513
1534
 
1514
- showAddonConfirmation() {
1515
- return new Promise((resolve, reject) => {
1535
+ showAddonConfirmation(addonNames, previousKubeVersion, newKubeVersion) {
1536
+ return new Promise((resolve) => {
1516
1537
  this.$store.dispatch('cluster/promptModal', {
1517
- component: 'AddonConfigConfirmationDialog',
1538
+ component: 'AddonConfigConfirmationDialog',
1539
+ componentProps: {
1540
+ addonNames,
1541
+ previousKubeVersion,
1542
+ newKubeVersion
1543
+ },
1518
1544
  resources: [(value) => resolve(value)]
1519
1545
  });
1520
1546
  });
@@ -1555,10 +1581,28 @@ export default {
1555
1581
  const isEditVersion = this.isEdit && this.liveValue?.spec?.kubernetesVersion !== this.value?.spec?.kubernetesVersion;
1556
1582
 
1557
1583
  if (isEditVersion) {
1558
- const shouldContinue = await this.showAddonConfirmation();
1584
+ const hasDiffs = Object.values(this.addonConfigDiffs).some((d) => !isEmpty(d));
1585
+
1586
+ if (hasDiffs) {
1587
+ const addonNamesWithDiffs = [];
1588
+
1589
+ for (const name in this.addonConfigDiffs) {
1590
+ const diff = this.addonConfigDiffs[name];
1591
+
1592
+ if (!isEmpty(diff)) {
1593
+ addonNamesWithDiffs.push(name);
1594
+ }
1595
+ }
1559
1596
 
1560
- if (!shouldContinue) {
1561
- return btnCb('cancelled');
1597
+ const shouldContinue = await this.showAddonConfirmation(
1598
+ addonNamesWithDiffs,
1599
+ this.liveValue.spec.kubernetesVersion,
1600
+ this.value.spec.kubernetesVersion
1601
+ );
1602
+
1603
+ if (!shouldContinue) {
1604
+ return btnCb('cancelled');
1605
+ }
1562
1606
  }
1563
1607
  }
1564
1608
 
@@ -2521,6 +2565,9 @@ export default {
2521
2565
  :addons-rev="addonsRev"
2522
2566
  :user-chart-values-temp="userChartValuesTemp"
2523
2567
  :init-yaml-editor="initYamlEditor"
2568
+ :has-diff="!isEmpty(addonConfigDiffs[v.name])"
2569
+ :previous-kube-version="liveValue?.spec?.kubernetesVersion"
2570
+ :new-kube-version="value.spec.kubernetesVersion"
2524
2571
  @update:value="$emit('input', $event)"
2525
2572
  @update-questions="syncChartValues"
2526
2573
  @update-values="updateValues"
@@ -48,6 +48,27 @@ export default {
48
48
  initYamlEditor: {
49
49
  type: Function,
50
50
  required: true,
51
+ },
52
+ /**
53
+ * Indicates if a configuration conflict was detected for this addon.
54
+ */
55
+ hasDiff: {
56
+ type: Boolean,
57
+ default: false
58
+ },
59
+ /**
60
+ * The Kubernetes version the user is upgrading from.
61
+ */
62
+ previousKubeVersion: {
63
+ type: String,
64
+ default: ''
65
+ },
66
+ /**
67
+ * The Kubernetes version the user is upgrading to.
68
+ */
69
+ newKubeVersion: {
70
+ type: String,
71
+ default: ''
51
72
  }
52
73
 
53
74
  },
@@ -73,10 +94,15 @@ export default {
73
94
  <template>
74
95
  <div>
75
96
  <Banner
76
- v-if="isEdit"
97
+ v-if="isEdit && hasDiff"
77
98
  color="warning"
78
99
  >
79
- {{ t('cluster.addOns.dependencyBanner') }}
100
+ <span
101
+ v-clean-html="t('cluster.addOns.dependencyBanner', {
102
+ previousKubeVersion,
103
+ newKubeVersion
104
+ }, true)"
105
+ />
80
106
  </Banner>
81
107
  <div
82
108
  v-if="versionInfo && addonVersion"
@@ -199,7 +199,7 @@ export default {
199
199
  }
200
200
 
201
201
  // Filter in if this cluster
202
- if (metadata.namespace.startsWith(this.currentCluster.id)) {
202
+ if (metadata.namespace?.startsWith(this.currentCluster.id)) {
203
203
  return true;
204
204
  }
205
205
 
@@ -35,7 +35,7 @@ const defaultConfig = {
35
35
  dns: '',
36
36
  environment: 'AzurePublicCloud',
37
37
  faultDomainCount: '3',
38
- image: 'canonical:UbuntuServer:18.04-LTS:latest',
38
+ image: 'canonical:ubuntu-24_04-lts:server-gen1:latest',
39
39
  location: 'westus',
40
40
  managedDisks: false,
41
41
  noPublicIp: false,
package/mixins/chart.js CHANGED
@@ -220,7 +220,7 @@ export default {
220
220
  },
221
221
 
222
222
  currentVersion() {
223
- return this.existing?.spec.chart.metadata.version;
223
+ return this.existing?.spec?.chart?.metadata?.version;
224
224
  },
225
225
 
226
226
  targetVersion() {
@@ -12,9 +12,9 @@ type MockChartContext = {
12
12
  };
13
13
 
14
14
  interface CardContent {
15
- subHeaderItems: { label: string }[];
15
+ subHeaderItems: { label: string, labelTooltip?: string}[];
16
16
  footerItems: { labels: string[]; icon?: string }[];
17
- statuses: { tooltip: { key: string }; color: string }[];
17
+ statuses: { tooltip: { key?: string; text?: string }; color: string }[];
18
18
  }
19
19
 
20
20
  const t = jest.fn((key) => key); // mock translation function
@@ -225,7 +225,7 @@ describe('class Chart', () => {
225
225
 
226
226
  const result = chart.cardContent as CardContent;
227
227
 
228
- const deprecatedStatus = result.statuses.find((s) => s.tooltip.key === 'generic.deprecated');
228
+ const deprecatedStatus = result.statuses.find((s) => s.tooltip?.key === 'generic.deprecated');
229
229
 
230
230
  expect(deprecatedStatus).toBeDefined();
231
231
  expect(deprecatedStatus?.color).toBe('error');
@@ -240,10 +240,11 @@ describe('class Chart', () => {
240
240
 
241
241
  const result = chart.cardContent as CardContent;
242
242
 
243
- const installedStatus = result.statuses.find((s) => s.tooltip.key === 'generic.installed');
243
+ const installedStatus = result.statuses.find((s) => s.tooltip?.text?.startsWith('generic.installed'));
244
244
 
245
245
  expect(installedStatus).toBeDefined();
246
246
  expect(installedStatus?.color).toBe('success');
247
+ expect(installedStatus?.tooltip?.text).toContain(installedApp.spec.chart.metadata.version);
247
248
  });
248
249
 
249
250
  it('includes upgradeable status when upgrade is available', () => {
@@ -255,7 +256,7 @@ describe('class Chart', () => {
255
256
 
256
257
  const result = chart.cardContent as CardContent;
257
258
 
258
- const upgradeableStatus = result.statuses.find((s) => s.tooltip.key === 'generic.upgradeable');
259
+ const upgradeableStatus = result.statuses.find((s) => s.tooltip?.key === 'generic.upgradeable');
259
260
 
260
261
  expect(upgradeableStatus).toBeDefined();
261
262
  expect(upgradeableStatus?.color).toBe('info');
@@ -270,9 +271,16 @@ describe('class Chart', () => {
270
271
 
271
272
  const result = chart.cardContent as CardContent;
272
273
 
273
- const keys = result.statuses.map((s) => s.tooltip.key);
274
+ const statuses = result.statuses.map((s) => {
275
+ if (s.tooltip?.key) {
276
+ return s.tooltip.key;
277
+ }
278
+ if (s.tooltip?.text?.startsWith('generic.installed')) {
279
+ return 'generic.installed';
280
+ }
281
+ });
274
282
 
275
- expect(keys).toStrictEqual(expect.arrayContaining([
283
+ expect(statuses).toStrictEqual(expect.arrayContaining([
276
284
  'generic.deprecated',
277
285
  'generic.upgradeable',
278
286
  'generic.installed'
@@ -290,11 +298,11 @@ describe('class Chart', () => {
290
298
  const chart = new Chart(chartWithZeroTime, {
291
299
  rootGetters: {
292
300
  'cluster/all': () => [],
293
- 'i18n/t': (key) => key
301
+ 'i18n/t': (key: string) => key
294
302
  },
295
303
  });
296
304
 
297
- const result = chart.cardContent;
305
+ const result = chart.cardContent as CardContent;
298
306
  const lastUpdatedItem = result.subHeaderItems[1];
299
307
 
300
308
  expect(lastUpdatedItem.label).toBe('generic.na');
@@ -0,0 +1,30 @@
1
+ import ComplianceProfile from '@shell/models/compliance.cattle.io.clusterscanprofile';
2
+
3
+ describe('class: ComplianceProfile', () => {
4
+ describe('getter: numberTestsSkipped', () => {
5
+ it('should return 0 if skipTests is not present in spec', () => {
6
+ const complianceProfile = new ComplianceProfile({ spec: {} });
7
+
8
+ expect(complianceProfile.numberTestsSkipped).toBe(0);
9
+ });
10
+
11
+ it('should return 0 if skipTests is null', () => {
12
+ const complianceProfile = new ComplianceProfile({ spec: { skipTests: null } });
13
+
14
+ expect(complianceProfile.numberTestsSkipped).toBe(0);
15
+ });
16
+
17
+ it('should return 0 if skipTests is an empty array', () => {
18
+ const complianceProfile = new ComplianceProfile({ spec: { skipTests: [] } });
19
+
20
+ expect(complianceProfile.numberTestsSkipped).toBe(0);
21
+ });
22
+
23
+ it('should return the correct number of skipped tests', () => {
24
+ const tests = ['test-1', 'test-2', 'test-3'];
25
+ const complianceProfile = new ComplianceProfile({ spec: { skipTests: tests } });
26
+
27
+ expect(complianceProfile.numberTestsSkipped).toBe(tests.length);
28
+ });
29
+ });
30
+ });
@@ -323,7 +323,7 @@ export default class CatalogApp extends SteveModel {
323
323
 
324
324
  get relatedResourcesToRemove() {
325
325
  return async() => {
326
- const crd = this.spec.chart.metadata.annotations[CATALOG_ANNOTATIONS.AUTO_INSTALL].replace('=match', '');
326
+ const crd = this.spec?.chart?.metadata?.annotations?.[CATALOG_ANNOTATIONS.AUTO_INSTALL]?.replace('=match', '');
327
327
 
328
328
  return await this.$dispatch('find', {
329
329
  type: CATALOG.APP,
package/models/chart.js CHANGED
@@ -195,8 +195,10 @@ export default class Chart extends SteveModel {
195
195
  }
196
196
 
197
197
  if (this.isInstalled) {
198
+ const installedVersion = this.matchingInstalledApps[0]?.spec?.chart?.metadata?.version;
199
+
198
200
  statuses.push({
199
- icon: 'icon-confirmation-alt', color: 'success', tooltip: { key: 'generic.installed' }
201
+ icon: 'icon-confirmation-alt', color: 'success', tooltip: { text: `${ this.t('generic.installed') } (${ installedVersion })` }
200
202
  });
201
203
  }
202
204
 
@@ -11,7 +11,7 @@ export default class ComplianceProfile extends SteveModel {
11
11
  get numberTestsSkipped() {
12
12
  const { skipTests = [] } = this.spec;
13
13
 
14
- return skipTests.length;
14
+ return skipTests?.length || 0;
15
15
  }
16
16
 
17
17
  get benchmarkVersionLink() {
@@ -14,6 +14,7 @@ export const configType = {
14
14
  googleoauth: 'oauth',
15
15
  local: '',
16
16
  github: 'oauth',
17
+ githubapp: 'oauth',
17
18
  keycloakoidc: 'oidc',
18
19
  genericoidc: 'oidc',
19
20
  cognito: 'oidc',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rancher/shell",
3
- "version": "3.0.7",
3
+ "version": "3.0.8-rc.1",
4
4
  "description": "Rancher Dashboard Shell",
5
5
  "repository": "https://github.com/rancherlabs/dashboard",
6
6
  "license": "Apache-2.0",
@@ -38,7 +38,7 @@
38
38
  "@babel/preset-typescript": "7.16.7",
39
39
  "@novnc/novnc": "1.2.0",
40
40
  "@popperjs/core": "2.11.8",
41
- "@rancher/icons": "2.0.38",
41
+ "@rancher/icons": "2.0.49",
42
42
  "@types/is-url": "1.2.30",
43
43
  "@types/node": "20.10.8",
44
44
  "@types/semver": "^7.5.8",
@@ -10,7 +10,7 @@ import CopyCode from '@shell/components/CopyCode';
10
10
  import { Banner } from '@components/Banner';
11
11
  import {
12
12
  LOCAL, LOGGED_OUT, TIMED_OUT, IS_SSO, _FLAGGED,
13
- IS_SLO
13
+ IS_SLO, IS_SESSION_IDLE
14
14
  } from '@shell/config/query-params';
15
15
  import { Checkbox } from '@components/Form/Checkbox';
16
16
  import Password from '@shell/components/form/Password';
@@ -47,6 +47,7 @@ export default {
47
47
 
48
48
  timedOut: this.$route.query[TIMED_OUT] === _FLAGGED,
49
49
  loggedOut: this.$route.query[LOGGED_OUT] === _FLAGGED,
50
+ isSessionIdle: this.$route.query[IS_SESSION_IDLE] === _FLAGGED,
50
51
  isSsoLogout: this.$route.query[IS_SSO] === _FLAGGED,
51
52
  isSlo: this.$route.query[IS_SLO] === _FLAGGED,
52
53
  err: this.$route.query.err,
@@ -67,7 +68,9 @@ export default {
67
68
  ...mapGetters({ t: 'i18n/t', hasMultipleLocales: 'i18n/hasMultipleLocales' }),
68
69
 
69
70
  loggedOutSuccessMsg() {
70
- if (this.isSlo) {
71
+ if (this.isSessionIdle) {
72
+ return this.t('login.loggedOutSessionIdle');
73
+ } else if (this.isSlo) {
71
74
  return this.t('login.loggedOutFromSlo');
72
75
  } else if (this.isSsoLogout) {
73
76
  return this.t('login.loggedOutFromSso');
@@ -10,7 +10,7 @@ import { AUTH_BROADCAST_CHANNEL_NAME } from '@shell/utils/auth';
10
10
 
11
11
  const samlProviders = ['ping', 'adfs', 'keycloak', 'okta', 'shibboleth'];
12
12
 
13
- const oauthProviders = ['github', 'googleoauth', 'azuread'];
13
+ const oauthProviders = ['github', 'githubapp', 'googleoauth', 'azuread'];
14
14
 
15
15
  function reply(err, code) {
16
16
  try {
@@ -19,7 +19,7 @@ defineProps<{
19
19
  v-for="(subHeaderItem, i) in items"
20
20
  :key="i"
21
21
  class="app-chart-card-sub-header-item"
22
- data-testid="app-chart-card-version"
22
+ data-testid="app-chart-card-sub-header-item"
23
23
  >
24
24
  <i
25
25
  v-clean-tooltip="t(subHeaderItem.iconTooltip.key)"
@@ -35,7 +35,8 @@ defineProps<{
35
35
  <style scoped lang="scss">
36
36
  .app-chart-card-sub-header {
37
37
  display: flex;
38
- gap: var(--gap-md);
38
+ flex-wrap: wrap;
39
+ gap: var(--gap) var(--gap-md);
39
40
  color: var(--link-text-secondary);
40
41
  margin-bottom: 8px;
41
42
 
@@ -259,11 +259,11 @@ export default {
259
259
  class="status"
260
260
  >
261
261
  <i
262
- v-clean-tooltip="t(status.tooltip.key)"
262
+ v-clean-tooltip="status.tooltip.key ? t(status.tooltip.key) : status.tooltip.text"
263
263
  :class="['icon', status.icon, status.color]"
264
264
  :style="{color: status.customColor}"
265
265
  role="img"
266
- :aria-label="t(status.tooltip.key)"
266
+ :aria-label="status.tooltip.key ? t(status.tooltip.key) : status.tooltip.text"
267
267
  />
268
268
  </div>
269
269
  </div>
@@ -6,6 +6,8 @@ import PaginatedResourceTable from '@shell/components/PaginatedResourceTable';
6
6
  import { STEVE_EVENT_FIRST_SEEN, STEVE_EVENT_LAST_SEEN, STEVE_EVENT_OBJECT, STEVE_NAME_COL } from '@shell/config/pagination-table-headers';
7
7
  import { headerFromSchemaColString } from '@shell/store/type-map.utils';
8
8
  import { NAME as EXPLORER } from '@shell/config/product/explorer';
9
+ import { ROWS_PER_PAGE } from '@shell/store/prefs';
10
+ import { RcDropdown, RcDropdownTrigger, RcDropdownItem } from '@components/RcDropdown';
9
11
 
10
12
  const reason = {
11
13
  ...REASON,
@@ -29,15 +31,42 @@ const eventHeaders = [
29
31
  },
30
32
  ];
31
33
 
34
+ // Local storage key for the events table row count preference
35
+ const ROWS_COUNT_PREF = 'events-row-count-pref';
36
+ // Value to use when we want to use the user's preference from the table
37
+ const ROWS_PREF_USE_TABLE = -1;
38
+ // Default number of rows to show in the events table when not set in the preference
39
+ const ROWS_COUNT_DEFAULT = 10;
40
+
32
41
  export default {
33
- components: { PaginatedResourceTable },
42
+ components: {
43
+ PaginatedResourceTable,
44
+ RcDropdown,
45
+ RcDropdownItem,
46
+ RcDropdownTrigger
47
+ },
34
48
 
35
49
  data() {
50
+ const tableRowOptions = this.$store.getters['prefs/options'](ROWS_PER_PAGE);
51
+
52
+ let rowsPerPage = ROWS_COUNT_DEFAULT;
53
+
54
+ // Read the current value from localStorage if it exists
55
+ if (window.localStorage.getItem(ROWS_COUNT_PREF)) {
56
+ try {
57
+ rowsPerPage = parseInt(window.localStorage.getItem(ROWS_COUNT_PREF));
58
+ } catch (e) {
59
+ console.error('Error parsing rows count from localStorage:', e); // eslint-disable-line no-console
60
+ }
61
+ }
62
+
36
63
  return {
64
+ rowsPerPage,
37
65
  schema: null,
38
66
  events: [],
39
67
  eventHeaders,
40
68
  paginationHeaders: null,
69
+ options: tableRowOptions || [],
41
70
  allEventsLink: {
42
71
  name: 'c-cluster-product-resource',
43
72
  params: {
@@ -72,7 +101,34 @@ export default {
72
101
  this.dismissRouteHandler = this.$router.beforeEach(this.onRouteChange);
73
102
  },
74
103
 
104
+ computed: {
105
+ userPrefRowsPerPage() {
106
+ return parseInt(this.$store.getters['prefs/get'](ROWS_PER_PAGE), 10) || undefined;
107
+ },
108
+ rowOptions() {
109
+ const rowOptions = [];
110
+
111
+ this.options.forEach((item) => rowOptions.push({
112
+ label: this.t('glance.showXEvents', { count: item }),
113
+ value: item
114
+ }));
115
+
116
+ if (this.userPrefRowsPerPage) {
117
+ rowOptions.push({
118
+ label: this.t('glance.useUserPreference', { count: this.userPrefRowsPerPage }),
119
+ value: ROWS_PREF_USE_TABLE,
120
+ });
121
+ }
122
+
123
+ return rowOptions;
124
+ }
125
+ },
126
+
75
127
  methods: {
128
+ updateRowsCount(val) {
129
+ this.rowsPerPage = val;
130
+ window.localStorage.setItem(ROWS_COUNT_PREF, val);
131
+ },
76
132
  async onRouteChange(to, from, next) {
77
133
  if (this.$route.name !== to.name) {
78
134
  await this.$store.dispatch('cluster/forgetType', EVENT);
@@ -100,7 +156,7 @@ export default {
100
156
  :table-actions="false"
101
157
  :row-actions="false"
102
158
  :groupable="false"
103
- :rows-per-page="10"
159
+ :rows-per-page="rowsPerPage"
104
160
  >
105
161
  <template v-slot:header-right>
106
162
  <router-link
@@ -110,14 +166,44 @@ export default {
110
166
  >
111
167
  <span>{{ t('glance.eventsTable') }}</span>
112
168
  </router-link>
169
+ <rc-dropdown>
170
+ <rc-dropdown-trigger
171
+ data-testid="events-list-row-count-menu-toggle"
172
+ :aria-label="t('glance.changeEventsListRowCount')"
173
+ ghost
174
+ small
175
+ >
176
+ <i class="icon icon-gear" />
177
+ </rc-dropdown-trigger>
178
+ <template #dropdownCollection>
179
+ <rc-dropdown-item
180
+ v-for="(item, i) in rowOptions"
181
+ :key="i"
182
+ :value="item.value"
183
+ @click.stop="updateRowsCount(item.value)"
184
+ >
185
+ <span :class="{ 'selected-pagesize-option': rowsPerPage === item.value }">
186
+ {{ item.label }}
187
+ </span>
188
+ </rc-dropdown-item>
189
+ </template>
190
+ </rc-dropdown>
113
191
  </template>
114
192
  </PaginatedResourceTable>
115
193
  </template>
116
194
 
117
195
  <style lang="scss" scoped>
196
+ .icon.icon-gear {
197
+ color: var(--primary);
198
+ padding: 0 8px;
199
+ }
118
200
  .events-link {
119
201
  align-self: center;
120
- padding-right: 20px;
202
+ margin-right: 10px;
121
203
  white-space: nowrap;
122
204
  }
205
+
206
+ .selected-pagesize-option {
207
+ font-weight: bold;
208
+ }
123
209
  </style>
@@ -96,7 +96,7 @@ export default {
96
96
  title: { text: chart.chartNameDisplay },
97
97
  statuses: chart.cardContent.statuses
98
98
  },
99
- subHeaderItems: chart.cardContent.subHeaderItems.slice(0, 1),
99
+ subHeaderItems: chart.cardContent.subHeaderItems,
100
100
  footerItems: chart.deploysOnWindows ? [{
101
101
  icon: 'icon-tag-alt',
102
102
  iconTooltip: { key: 'generic.tags' },
@@ -151,7 +151,7 @@ export default {
151
151
  action: 'edit',
152
152
  });
153
153
 
154
- const currentVersion = installedApp.spec.chart.metadata.version;
154
+ const currentVersion = installedApp.spec?.chart?.metadata?.version;
155
155
  const versions = rawChart.versions;
156
156
  const currentIndex = versions.findIndex((v) => v.version === currentVersion);
157
157
 
@@ -190,7 +190,7 @@ export default {
190
190
  },
191
191
 
192
192
  downgrade(app, chart) {
193
- const currentVersion = app.spec.chart.metadata.version;
193
+ const currentVersion = app.spec?.chart?.metadata?.version;
194
194
  const versions = chart.versions;
195
195
  const currentIndex = versions.findIndex((v) => v.version === currentVersion);
196
196