@rancher/shell 3.0.9-rc.4 → 3.0.9-rc.5

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 (109) hide show
  1. package/assets/brand/suse/metadata.json +2 -1
  2. package/assets/translations/en-us.yaml +91 -3
  3. package/components/ActionMenuShell.vue +1 -1
  4. package/components/Inactivity.vue +2 -2
  5. package/components/Resource/Detail/Card/ExtrasCard.vue +49 -15
  6. package/components/Resource/Detail/Card/__tests__/ExtrasCard.test.ts +111 -0
  7. package/components/Resource/Detail/Masthead/__tests__/index.test.ts +0 -17
  8. package/components/Resource/Detail/Masthead/index.vue +11 -4
  9. package/components/Resource/Detail/Metadata/IdentifyingInformation/index.vue +3 -1
  10. package/components/Resource/Detail/Metadata/index.vue +1 -1
  11. package/components/Resource/Detail/ResourceRow.vue +1 -1
  12. package/components/ResourceDetail/Masthead/latest.vue +12 -2
  13. package/components/ResourceList/index.vue +9 -0
  14. package/components/ResourceTable.vue +38 -4
  15. package/components/Tabbed/Tab.vue +4 -0
  16. package/components/Tabbed/index.vue +4 -1
  17. package/components/__tests__/ProjectRow.test.ts +60 -0
  18. package/components/form/ChangePassword.vue +41 -35
  19. package/components/form/ResourceQuota/Project.vue +42 -1
  20. package/components/form/ResourceQuota/ProjectRow.vue +71 -4
  21. package/components/form/ResourceQuota/__tests__/Project.test.ts +63 -0
  22. package/components/form/SelectOrCreateAuthSecret.vue +6 -1
  23. package/components/form/__tests__/SelectOrCreateAuthSecret.test.ts +35 -0
  24. package/components/formatter/MachineSummaryGraph.vue +10 -2
  25. package/components/nav/TopLevelMenu.helper.ts +50 -2
  26. package/components/nav/TopLevelMenu.vue +14 -0
  27. package/components/nav/Type.vue +5 -0
  28. package/components/nav/__tests__/TopLevelMenu.test.ts +3 -3
  29. package/components/nav/__tests__/Type.test.ts +6 -4
  30. package/config/product/explorer.js +4 -3
  31. package/config/product/manager.js +18 -1
  32. package/config/router/navigation-guards/authentication.js +8 -9
  33. package/config/types.js +10 -2
  34. package/detail/auditlog.cattle.io.auditpolicy.vue +19 -0
  35. package/detail/management.cattle.io.user.vue +1 -2
  36. package/detail/node.vue +0 -1
  37. package/detail/provisioning.cattle.io.cluster.vue +2 -1
  38. package/dialog/ChangePasswordDialog.vue +8 -0
  39. package/dialog/GenericPrompt.vue +20 -3
  40. package/dialog/ScaleMachineDownDialog.vue +65 -15
  41. package/dialog/SearchDialog.vue +10 -2
  42. package/dialog/__tests__/ScaleMachineDownDialog.test.ts +184 -0
  43. package/edit/__tests__/fleet.cattle.io.gitrepo.test.ts +89 -0
  44. package/edit/__tests__/management.cattle.io.project.test.js +56 -1
  45. package/edit/auditlog.cattle.io.auditpolicy/AdditionalRedactions.vue +114 -0
  46. package/edit/auditlog.cattle.io.auditpolicy/Filters.vue +119 -0
  47. package/edit/auditlog.cattle.io.auditpolicy/General.vue +180 -0
  48. package/edit/auditlog.cattle.io.auditpolicy/__tests__/AdditionalRedactions.test.ts +327 -0
  49. package/edit/auditlog.cattle.io.auditpolicy/__tests__/Filters.test.ts +449 -0
  50. package/edit/auditlog.cattle.io.auditpolicy/__tests__/General.test.ts +472 -0
  51. package/edit/auditlog.cattle.io.auditpolicy/__tests__/__snapshots__/AdditionalRedactions.test.ts.snap +27 -0
  52. package/edit/auditlog.cattle.io.auditpolicy/__tests__/__snapshots__/Filters.test.ts.snap +39 -0
  53. package/edit/auditlog.cattle.io.auditpolicy/__tests__/__snapshots__/General.test.ts.snap +174 -0
  54. package/edit/auditlog.cattle.io.auditpolicy/__tests__/__snapshots__/index.test.ts.snap +29 -0
  55. package/edit/auditlog.cattle.io.auditpolicy/__tests__/index.test.ts +215 -0
  56. package/edit/auditlog.cattle.io.auditpolicy/index.vue +104 -0
  57. package/edit/auditlog.cattle.io.auditpolicy/types.ts +28 -0
  58. package/edit/fleet.cattle.io.gitrepo.vue +16 -1
  59. package/edit/management.cattle.io.project.vue +8 -2
  60. package/edit/management.cattle.io.user.vue +29 -34
  61. package/edit/provisioning.cattle.io.cluster/__tests__/rke2.test.ts +178 -0
  62. package/edit/provisioning.cattle.io.cluster/rke2.vue +21 -2
  63. package/edit/provisioning.cattle.io.cluster/shared.ts +4 -0
  64. package/edit/provisioning.cattle.io.cluster/tabs/Basics.vue +1 -0
  65. package/edit/provisioning.cattle.io.cluster/tabs/etcd/S3Config.vue +1 -1
  66. package/list/auditlog.cattle.io.auditpolicy.vue +63 -0
  67. package/list/group.principal.vue +11 -15
  68. package/list/management.cattle.io.user.vue +11 -21
  69. package/machine-config/azure.vue +14 -0
  70. package/mixins/browser-tab-visibility.js +5 -4
  71. package/mixins/fetch.client.js +6 -0
  72. package/models/__tests__/auditlog.cattle.io.auditpolicy.test.ts +117 -0
  73. package/models/__tests__/workload.test.ts +49 -6
  74. package/models/auditlog.cattle.io.auditpolicy.js +46 -0
  75. package/models/cluster.x-k8s.io.machine.js +1 -1
  76. package/models/cluster.x-k8s.io.machinedeployment.js +5 -5
  77. package/models/event.js +5 -0
  78. package/models/ext.cattle.io.groupmembershiprefreshrequest.js +15 -0
  79. package/models/ext.cattle.io.passwordchangerequest.js +15 -0
  80. package/models/ext.cattle.io.selfuser.js +15 -0
  81. package/models/fleet-application.js +17 -7
  82. package/models/management.cattle.io.user.js +28 -31
  83. package/models/schema.js +18 -0
  84. package/models/secret.js +27 -24
  85. package/models/steve-schema.ts +39 -2
  86. package/models/workload.js +3 -2
  87. package/package.json +1 -1
  88. package/pages/account/index.vue +23 -16
  89. package/pages/auth/login.vue +15 -8
  90. package/pages/auth/setup.vue +52 -15
  91. package/pages/home.vue +9 -3
  92. package/plugins/dashboard-store/__tests__/resource-class.test.ts +1 -3
  93. package/plugins/dashboard-store/actions.js +7 -0
  94. package/plugins/dashboard-store/getters.js +23 -1
  95. package/plugins/dashboard-store/index.js +3 -2
  96. package/plugins/dashboard-store/mutations.js +4 -0
  97. package/plugins/dashboard-store/resource-class.js +12 -5
  98. package/plugins/steve/__tests__/steve-class.test.ts +167 -0
  99. package/plugins/steve/schema.d.ts +5 -0
  100. package/plugins/steve/steve-class.js +19 -0
  101. package/plugins/steve/steve-pagination-utils.ts +2 -1
  102. package/store/auth.js +57 -19
  103. package/store/notifications.ts +1 -1
  104. package/store/type-map.js +12 -1
  105. package/types/shell/index.d.ts +10 -14
  106. package/types/store/dashboard-store.types.ts +7 -0
  107. package/utils/pagination-wrapper.ts +11 -3
  108. package/vue.config.js +26 -13
  109. package/edit/provisioning.cattle.io.cluster/defaults.ts +0 -1
@@ -8,6 +8,7 @@
8
8
  "bannerClass": "suse-login-banner-graphic",
9
9
  "logo": "rancher-logo.svg",
10
10
  "logoClass": "suse-logo-login",
11
- "welcomeLabelKey": "login.login"
11
+ "welcomeLabelKey": "login.login",
12
+ "setupLabelKey": "setup.setup"
12
13
  }
13
14
  }
@@ -163,6 +163,9 @@ generic:
163
163
  externalIps: External IPs
164
164
  internalIps: Internal IPs
165
165
  opensInNewTab: Opens in a new tab
166
+ autogeneratedCreated:
167
+ title: "{resource} created"
168
+ message: "{id} has been created."
166
169
 
167
170
  tabs:
168
171
  addItem: Add a new tab item
@@ -307,6 +310,7 @@ nav:
307
310
  clusterNotFound: Cluster { clusterId } not found
308
311
  productNotFound: Product { productNotFound } not found
309
312
  resourceNotFound: Resource type { resource } not found
313
+ resourceListNotListable: Resource type { resource } cannot be listed
310
314
  resourceListNotFound: Resource type { resource } not found, unable to display list
311
315
  resourceIdNotFound: Resource { resource } with id { fqid } not found, unable to display resource details
312
316
  reload: Reload
@@ -1447,6 +1451,8 @@ changePassword:
1447
1451
  failedToChange: Failed to change password
1448
1452
  failedDeleteKey: Failed to delete key
1449
1453
  failedDeleteKeys: Failed to delete keys
1454
+ cannotChange: This user does not have permissions to change their password
1455
+ cannotFetchSelf: This user does not have permissions to get their user information
1450
1456
 
1451
1457
  chartHeading:
1452
1458
  overview: Overview
@@ -1859,7 +1865,7 @@ cluster:
1859
1865
  acceleratedNetworking:
1860
1866
  label: Accelerated Networking
1861
1867
  availabilitySet:
1862
- label: Availability Set (unmanaged)
1868
+ label: Availability Set
1863
1869
  description: Availability sets are used to protect applications from hardware failures within an Azure data center.
1864
1870
  availabilityZone:
1865
1871
  label: Availability Zone
@@ -1884,6 +1890,7 @@ cluster:
1884
1890
  label: Location
1885
1891
  managedDisks:
1886
1892
  label: Use Managed Disks
1893
+ deprecationWarning: 'Azure is retiring unmanaged disk support on March 31, 2026. After this date, VMs using unmanaged disks will be stopped and deallocated. Enable "Use Managed Disks" to avoid service disruption.'
1887
1894
  managedDisksSize:
1888
1895
  label: Managed Disk Size
1889
1896
  nsg:
@@ -3112,6 +3119,7 @@ fleet:
3112
3119
  label: Authentication
3113
3120
  git: Git Authentication
3114
3121
  helm: Helm Authentication
3122
+ githubdotcomPasswordBanner: 'GitHub no longer supports password authentication for repositories. Please enter a personal access token with the required permissions instead of your GitHub password.'
3115
3123
  caBundle:
3116
3124
  label: Certificates
3117
3125
  placeholder: "Paste in one or more certificates, starting with -----BEGIN CERTIFICATE----"
@@ -3636,6 +3644,71 @@ import:
3636
3644
  other {# Resources}
3637
3645
  }
3638
3646
 
3647
+ auditPolicy:
3648
+ active: Active
3649
+ inactive: Inactive
3650
+ reasons:
3651
+ PolicyNotYetActivated: Not yet activated
3652
+ PolicyIsActive: Active
3653
+ PolicyIsInvalid: Invalid
3654
+ PolicyWasDisabled: Disabled
3655
+ general:
3656
+ title: General
3657
+ enabled:
3658
+ label: Enabled
3659
+ title: Enabled
3660
+ checkbox: Enables this audit policy
3661
+ verbosity:
3662
+ label: Log Verbosity
3663
+ title: Log Verbosity
3664
+ banner: Audit Log Verbosity and install time Audit Log settings are additive.
3665
+ level:
3666
+ 0: 0 - Log request and response metadata
3667
+ 1: 1 - Log request and response headers
3668
+ 2: 2 - Log request body
3669
+ 3: 3 - Log response body
3670
+ title: Log Levels
3671
+ label: Level
3672
+ tooltip: Each log level is cumulative, higher log levels include the data from lower levels. Each log entry contains both request and response information.
3673
+ requestResponse:
3674
+ tooltip: Override the Log Level and explicitly include Headers / Body
3675
+ request:
3676
+ title: Request
3677
+ requestHeaders: Request Headers
3678
+ requestBody: Request Body
3679
+ response:
3680
+ title: Response
3681
+ responseHeaders: Response Headers
3682
+ responseBody: Response Body
3683
+ filters:
3684
+ add: Add Filter
3685
+ title: Filters
3686
+ action:
3687
+ title: Action
3688
+ label: Action
3689
+ allow: Allow
3690
+ deny: Deny
3691
+ placeholder: Allow/Deny
3692
+ requestURI:
3693
+ title: Request URI
3694
+ label: Request URI
3695
+ placeholder: e.g. /foo/.*
3696
+ additionalRedactions:
3697
+ title: Additional Redactions
3698
+ headers:
3699
+ title: Headers
3700
+ label: Headers
3701
+ placeholder: e.g. Cache.*
3702
+ add: Add Header
3703
+ paths:
3704
+ title: Paths
3705
+ label: Paths
3706
+ tooltip: Paths redacts information from request and response bodies based on json path expressions
3707
+ placeholder: e.g. $.gitCommit
3708
+ add: Add Path
3709
+ error:
3710
+ enableOrDisable: "{flag, select, enable {Error when enabling - {id}} disable {Error when disabling - {id}} other {Error - {id}}}"
3711
+
3639
3712
  ingress:
3640
3713
  description: Ingresses route incoming traffic from the internet to Services within the cluster based on the hostname and path specified in the request. You can expose multiple Services on the same external IP address and port.
3641
3714
  certificates:
@@ -5714,7 +5787,11 @@ promptScaleMachineDown:
5714
5787
  attemptingToRemove: "You are attempting to delete {count} {type}"
5715
5788
  retainedMachine1: At least one Machine must exist for roles Control Plane and Etcd.
5716
5789
  retainedMachine2: <b>{ name }</b> will remain
5717
-
5790
+ scaling: |-
5791
+ {count, plural,
5792
+ =1 {This machine pool is still reconciling. A different node may be deleted instead of the one you selected. It’s best to wait until reconciliation finishes.<br> Do you still want to mark this node for deletion?}
5793
+ other {At least one of these machine pools is still reconciling. Different nodes may be deleted instead of the ones you selected. It’s best to wait until reconciliation finishes.<br> Do you still want to mark these nodes for deletion?}
5794
+ }
5718
5795
  promptSlo:
5719
5796
  title: "Log out"
5720
5797
  text: "Log out of only Rancher, or all {name} applications."
@@ -5932,6 +6009,9 @@ rbac:
5932
6009
  label: Login Access
5933
6010
  clustertemplaterevisions-create:
5934
6011
  label: Create RKE Template Revisions
6012
+ proxy-endpoints-manage:
6013
+ label: Manage Rancher Proxy
6014
+ description: Allows the user to manage settings for proxying HTTP requests via Rancher
5935
6015
  errors:
5936
6016
  escalation: You cannot assign Global Permissions that are higher than your own. Please verify the permissions you are attempting to assign.
5937
6017
 
@@ -6178,6 +6258,7 @@ selectOrCreateAuthSecret:
6178
6258
  basic:
6179
6259
  username: Username
6180
6260
  password: Password
6261
+ passwordPersonalAccessToken: Password/Personal Access Token
6181
6262
  rke:
6182
6263
  info: "An RKE Auth Config secret contains the username and password concatenated and base64 encoded into the secret's 'auth' key"
6183
6264
  namespaceGroup: "Namespace: {name}"
@@ -6332,6 +6413,7 @@ setup:
6332
6413
  useRandom: Use a randomly generated password
6333
6414
  copyRandom: Copy random password to clipboard
6334
6415
  welcome: Welcome to {vendor}!
6416
+ setup: Setup
6335
6417
 
6336
6418
  sortableTable:
6337
6419
  ariaLabel:
@@ -7881,6 +7963,7 @@ typeDescription:
7881
7963
  logging.banzaicloud.io.output: An output defines which logging providers that logs can be sent to. The output needs to be in the same namespace as the flow that is using it.
7882
7964
  group.principal: Assigning global roles to a group only works with external auth providers that support groups. Local authorization does not support groups.
7883
7965
  management.cattle.io.oidcclient: Here you can add applications to Rancher's single sign-on identity provider
7966
+ auditlog.cattle.io.auditpolicy: Define rules that determine which Rancher API events are logged and the level of detail they contain.
7884
7967
 
7885
7968
  typeLabel:
7886
7969
  management.cattle.io.oidcclient: |-
@@ -8033,6 +8116,11 @@ typeLabel:
8033
8116
  one { Resource Quota }
8034
8117
  other { Resource Quotas }
8035
8118
  }
8119
+ auditlog.cattle.io.auditpolicy: |-
8120
+ {count, plural,
8121
+ one { Audit Log Policy }
8122
+ other { Audit Log Policies }
8123
+ }
8036
8124
  # pruh-mee-thee-eyes https://www.prometheus.io/docs/introduction/faq/#what-is-the-plural-of-prometheus
8037
8125
  monitoring.coreos.com.prometheus: |-
8038
8126
  {count, plural,
@@ -9182,7 +9270,7 @@ component:
9182
9270
  events: Events
9183
9271
  extrasCard:
9184
9272
  title: Extras
9185
- message: 'Consider installing additional <a class="secondary text-deemphasized" href="{extensionsUrl}">extensions</a> and / or <a class="secondary-text-link" href="{clusterToolsUrl}">cluster tools</a> to enrich your Rancher experience.'
9273
+ message: 'Consider installing additional <extensionsLink>extensions</extensionsLink> and / or <clusterToolsLink>cluster tools</clusterToolsLink> to enrich your Rancher experience.'
9186
9274
  scaler:
9187
9275
  ariaLabel:
9188
9276
  increase: Increase {resourceName}
@@ -97,7 +97,7 @@ const menuOptions = () => {
97
97
  <template>
98
98
  <rc-dropdown-menu
99
99
  :button-variant="buttonVariant || 'link'"
100
- :button-size="buttonSize || 'small'"
100
+ :button-size="buttonSize || 'medium'"
101
101
  :button-aria-label="buttonAriaLabel"
102
102
  :dropdown-aria-label="dropdownAriaLabel"
103
103
  :options="menuOptions()"
@@ -120,10 +120,10 @@ export default {
120
120
 
121
121
  methods: {
122
122
  async initializeInactivityData() {
123
- const canListUserAct = this.$store.getters[`management/canList`](EXT.USER_ACTIVITY);
123
+ const canGetUserAct = this.$store.getters[`management/canGet`](EXT.USER_ACTIVITY);
124
124
  const canListTokens = this.$store.getters[`rancher/canList`](NORMAN.TOKEN);
125
125
 
126
- if (canListUserAct && canListTokens) {
126
+ if (canGetUserAct && canListTokens) {
127
127
  const tokens = await this.$store.dispatch('rancher/findAll', { type: NORMAN.TOKEN, opt: { watch: false } });
128
128
 
129
129
  this.tokens = tokens;
@@ -3,37 +3,71 @@ import Card from '@shell/components/Resource/Detail/Card/index.vue';
3
3
  import { useI18n } from '@shell/composables/useI18n';
4
4
  import { useStore } from 'vuex';
5
5
  import { BLANK_CLUSTER } from '@shell/store/store-types';
6
+ import { isAdminUser } from '@shell/store/type-map';
7
+ import { DOCS_BASE } from '@shell/config/private-label';
6
8
  </script>
7
9
  <script setup lang="ts">
8
- import { useRouter } from 'vue-router';
10
+ import RichTranslation from '@shell/components/RichTranslation.vue';
11
+ import { computed } from 'vue';
9
12
 
10
13
  const store = useStore();
11
- const router = useRouter();
12
14
  const i18n = useI18n(store);
15
+ const isAdmin = computed(() => isAdminUser(store.getters));
13
16
 
14
- const extensionsUrl = router.resolve({
15
- name: 'c-cluster-uiplugins',
16
- params: { cluster: BLANK_CLUSTER }
17
- }).href;
17
+ const extensionsRoute = { name: 'c-cluster-uiplugins', params: { cluster: BLANK_CLUSTER } };
18
+ const extensionsDocsUrl = `${ DOCS_BASE }/integrations-in-rancher/rancher-extensions`;
18
19
 
19
- const clusterToolsUrl = router.resolve({
20
- name: 'c-cluster-apps-charts',
21
- params: { cluster: BLANK_CLUSTER }
22
- }).href;
20
+ const clusterToolsRoute = { name: 'c-cluster-explorer-tools' };
21
+ const clusterToolsDocsUrl = `${ DOCS_BASE }/reference-guides/rancher-cluster-tools`;
23
22
  </script>
24
23
 
25
24
  <template>
26
25
  <Card :title="i18n.t('component.resource.detail.card.extrasCard.title')">
27
- <p
28
- v-clean-html="i18n.t('component.resource.detail.card.extrasCard.message', { extensionsUrl, clusterToolsUrl }, true)"
29
- class="message text-deemphasized"
30
- />
26
+ <p class="message text-deemphasized">
27
+ <RichTranslation k="component.resource.detail.card.extrasCard.message">
28
+ <template #extensionsLink="{ content }">
29
+ <router-link
30
+ v-if="isAdmin"
31
+ class="secondary text-deemphasized"
32
+ :to="extensionsRoute"
33
+ >
34
+ {{ content }}
35
+ </router-link>
36
+ <a
37
+ v-else
38
+ class="secondary text-deemphasized"
39
+ :href="extensionsDocsUrl"
40
+ target="_blank"
41
+ >
42
+ {{ content }}
43
+ </a>
44
+ </template>
45
+ <template #clusterToolsLink="{ content }">
46
+ <router-link
47
+ v-if="isAdmin"
48
+ class="secondary-text-link"
49
+ :to="clusterToolsRoute"
50
+ >
51
+ {{ content }}
52
+ </router-link>
53
+ <a
54
+ v-else
55
+ class="secondary-text-link"
56
+ :href="clusterToolsDocsUrl"
57
+ target="_blank"
58
+ >
59
+ {{ content }}
60
+ </a>
61
+ </template>
62
+ </RichTranslation>
63
+ </p>
31
64
  </Card>
32
65
  </template>
33
66
 
34
67
  <style lang="scss" scoped>
35
68
  .message {
36
69
  margin: 0;
37
- line-height: 1.5;
70
+ margin-top: -2px;
71
+ line-height: 20px;
38
72
  }
39
73
  </style>
@@ -0,0 +1,111 @@
1
+ import { mount } from '@vue/test-utils';
2
+ import { createStore } from 'vuex';
3
+ import ExtrasCard from '@shell/components/Resource/Detail/Card/ExtrasCard.vue';
4
+ import Card from '@shell/components/Resource/Detail/Card/index.vue';
5
+ import RichTranslation from '@shell/components/RichTranslation.vue';
6
+
7
+ const TRANSLATION_KEY = 'component.resource.detail.card.extrasCard.message';
8
+ const TRANSLATION_VALUE = 'Consider installing additional <extensionsLink>extensions</extensionsLink> and / or <clusterToolsLink>cluster tools</clusterToolsLink> to enrich your Rancher experience.';
9
+
10
+ jest.mock('@shell/store/type-map', () => ({ isAdminUser: jest.fn() }));
11
+ jest.mock('@shell/config/private-label', () => ({ DOCS_BASE: 'https://docs.example.com' }));
12
+
13
+ const { isAdminUser } = require('@shell/store/type-map');
14
+
15
+ function createMockStore() {
16
+ return createStore({
17
+ getters: {
18
+ 'i18n/t': () => (key: string) => {
19
+ if (key === TRANSLATION_KEY) {
20
+ return TRANSLATION_VALUE;
21
+ }
22
+
23
+ return key;
24
+ },
25
+ },
26
+ });
27
+ }
28
+
29
+ function mountExtrasCard({ admin = false } = {}) {
30
+ isAdminUser.mockReturnValue(admin);
31
+
32
+ const store = createMockStore();
33
+
34
+ return mount(ExtrasCard, {
35
+ global: {
36
+ plugins: [store],
37
+ stubs: { 'router-link': { template: '<a :href="JSON.stringify(to)" :class="$attrs.class"><slot /></a>', props: ['to'] } },
38
+ },
39
+ });
40
+ }
41
+
42
+ describe('component: ExtrasCard', () => {
43
+ it('should render the Card component with the correct title', () => {
44
+ const wrapper = mountExtrasCard();
45
+ const card = wrapper.findComponent(Card);
46
+
47
+ expect(card.exists()).toBe(true);
48
+ expect(card.props('title')).toStrictEqual('component.resource.detail.card.extrasCard.title');
49
+ });
50
+
51
+ it('should render the RichTranslation component with the correct key', () => {
52
+ const wrapper = mountExtrasCard();
53
+ const richTranslation = wrapper.findComponent(RichTranslation);
54
+
55
+ expect(richTranslation.exists()).toBe(true);
56
+ expect(richTranslation.props('k')).toStrictEqual(TRANSLATION_KEY);
57
+ });
58
+
59
+ describe('when user is admin', () => {
60
+ it('should render router-link for extensions', () => {
61
+ const wrapper = mountExtrasCard({ admin: true });
62
+ const links = wrapper.findAll('a');
63
+ const extensionsLink = links.find((l) => l.text() === 'extensions');
64
+
65
+ expect(extensionsLink).toBeDefined();
66
+ expect(extensionsLink!.classes()).toContain('secondary');
67
+ expect(extensionsLink!.classes()).toContain('text-deemphasized');
68
+ expect(extensionsLink!.attributes('href')).toContain('c-cluster-uiplugins');
69
+ });
70
+
71
+ it('should render router-link for cluster tools', () => {
72
+ const wrapper = mountExtrasCard({ admin: true });
73
+ const links = wrapper.findAll('a');
74
+ const clusterToolsLink = links.find((l) => l.text() === 'cluster tools');
75
+
76
+ expect(clusterToolsLink).toBeDefined();
77
+ expect(clusterToolsLink!.classes()).toContain('secondary-text-link');
78
+ expect(clusterToolsLink!.attributes('href')).toContain('c-cluster-explorer-tools');
79
+ });
80
+
81
+ it('should not render external anchor tags', () => {
82
+ const wrapper = mountExtrasCard({ admin: true });
83
+ const anchors = wrapper.findAll('a[target="_blank"]');
84
+
85
+ expect(anchors).toHaveLength(0);
86
+ });
87
+ });
88
+
89
+ describe('when user is not admin', () => {
90
+ it('should render an external link for extensions', () => {
91
+ const wrapper = mountExtrasCard({ admin: false });
92
+ const links = wrapper.findAll('a[target="_blank"]');
93
+ const extensionsLink = links.find((l) => l.text() === 'extensions');
94
+
95
+ expect(extensionsLink).toBeDefined();
96
+ expect(extensionsLink!.attributes('href')).toStrictEqual('https://docs.example.com/integrations-in-rancher/rancher-extensions');
97
+ expect(extensionsLink!.classes()).toContain('secondary');
98
+ expect(extensionsLink!.classes()).toContain('text-deemphasized');
99
+ });
100
+
101
+ it('should render an external link for cluster tools', () => {
102
+ const wrapper = mountExtrasCard({ admin: false });
103
+ const links = wrapper.findAll('a[target="_blank"]');
104
+ const clusterToolsLink = links.find((l) => l.text() === 'cluster tools');
105
+
106
+ expect(clusterToolsLink).toBeDefined();
107
+ expect(clusterToolsLink!.attributes('href')).toStrictEqual('https://docs.example.com/reference-guides/rancher-cluster-tools');
108
+ expect(clusterToolsLink!.classes()).toContain('secondary-text-link');
109
+ });
110
+ });
111
+ });
@@ -50,21 +50,4 @@ describe('component: Masthead/index', () => {
50
50
 
51
51
  expect(cardsComponent.props('resource')).toStrictEqual(mockResource);
52
52
  });
53
-
54
- it('should render Cards with mb-20 class', () => {
55
- const wrapper = mount(Masthead, {
56
- props: defaultProps,
57
- global: {
58
- stubs: {
59
- TitleBar: true,
60
- Metadata: true,
61
- Cards: true
62
- }
63
- }
64
- });
65
-
66
- const cardsComponent = wrapper.findComponent(Cards);
67
-
68
- expect(cardsComponent.classes()).toContain('mb-20');
69
- });
70
53
  });
@@ -24,10 +24,11 @@ const props = defineProps<MastheadProps>();
24
24
  </template>
25
25
  </TitleBar>
26
26
  <Metadata
27
+ class="metadata-section"
27
28
  v-bind="props.metadataProps"
28
29
  />
29
30
  <Cards
30
- class="mb-20"
31
+ class="cards-section"
31
32
  :resource="props.titleBarProps.resource"
32
33
  />
33
34
  </div>
@@ -35,8 +36,14 @@ const props = defineProps<MastheadProps>();
35
36
 
36
37
  <style lang='scss' scoped>
37
38
  .masthead {
38
- :deep().metadata {
39
- margin-top: 24px;
40
- }
39
+ :deep() .metadata-section {
40
+ margin-top: 16px;
41
+ margin-bottom: 24px;
42
+ }
43
+
44
+ .cards-section {
45
+ margin: 0;
46
+ margin-bottom: 24px;
47
+ }
41
48
  }
42
49
  </style>
@@ -103,7 +103,9 @@ const getRowValueId = (row:Row): string => `value-${ row.label }:${ row.value }`
103
103
  flex-direction: column;
104
104
 
105
105
  .row {
106
- margin-bottom: 8px;
106
+ &:not(:last-of-type) {
107
+ margin-bottom: 8px;
108
+ }
107
109
 
108
110
  .full-custom-value {
109
111
  flex: 1;
@@ -31,7 +31,7 @@ const showBothEmpty = computed(() => labels.length === 0 && annotations.length =
31
31
 
32
32
  <template>
33
33
  <SpacedRow
34
- class="metadata ppb-3"
34
+ class="metadata"
35
35
  v-bind="$attrs"
36
36
  >
37
37
  <div
@@ -57,7 +57,7 @@ const displayCounts = computed(() => {
57
57
  v-if="!counts || counts.length == 0"
58
58
  class="text-muted"
59
59
  >
60
- &mdash;
60
+ 0
61
61
  </div>
62
62
  <div
63
63
  v-else
@@ -64,11 +64,11 @@ const store = useStore();
64
64
  />
65
65
  <Metadata
66
66
  v-bind="metadataProps"
67
- class="mmt-4"
67
+ class="metadata-section"
68
68
  />
69
69
  <Cards
70
70
  v-if="props.isCustomDetailOrEdit"
71
- class="mb-20"
71
+ class="cards-section"
72
72
  :resource="props.value"
73
73
  />
74
74
  </div>
@@ -79,4 +79,14 @@ const store = useStore();
79
79
  margin: 0;
80
80
  margin-top: 16px;
81
81
  }
82
+
83
+ :deep() .metadata-section {
84
+ margin-top: 16px;
85
+ margin-bottom: 24px;
86
+ }
87
+
88
+ .cards-section {
89
+ margin: 0;
90
+ margin-bottom: 24px;
91
+ }
82
92
  </style>
@@ -86,6 +86,15 @@ export default {
86
86
  }
87
87
  },
88
88
 
89
+ beforeMount() {
90
+ const inStore = this.$store.getters['currentStore'](this.resource);
91
+ const canList = this.$store.getters[`${ inStore }/canList`](this.resource);
92
+
93
+ if (!canList) {
94
+ this.$store.dispatch('loadingError', new Error(this.t('nav.failWhale.resourceListNotListable', { resource: this.schema.id }, true)));
95
+ }
96
+ },
97
+
89
98
  data() {
90
99
  const getters = this.$store.getters;
91
100
  const params = { ...this.$route.params };
@@ -5,6 +5,7 @@ import { mapPref, GROUP_RESOURCES } from '@shell/store/prefs';
5
5
  import ButtonGroup from '@shell/components/ButtonGroup';
6
6
  import SortableTable from '@shell/components/SortableTable';
7
7
  import { NAMESPACE, AGE } from '@shell/config/table-headers';
8
+ import { COUNT } from '@shell/config/types';
8
9
  import { findBy } from '@shell/utils/array';
9
10
  import { ExtensionPoint, TableColumnLocation, TableLocation } from '@shell/core/types';
10
11
  import { getApplicableExtensionEnhancements } from '@shell/core/plugin-helpers';
@@ -238,6 +239,7 @@ export default {
238
239
  */
239
240
  sortGeneration: undefined,
240
241
  listAutoRefreshToggleEnabled: paginationUtils.listAutoRefreshToggleEnabled({ rootGetters: this.$store.getters }),
242
+ hasSearchFilter: false,
241
243
  };
242
244
  },
243
245
 
@@ -600,6 +602,26 @@ export default {
600
602
  pluralLabel: this.$store.getters['type-map/labelFor'](this.schema, 99),
601
603
  };
602
604
  },
605
+
606
+ /**
607
+ * Get the counts data by namespace for the current resource type
608
+ */
609
+ namespaceCounts() {
610
+ if (!this.inStore || !this.schema?.id) {
611
+ return {};
612
+ }
613
+
614
+ const counts = this.$store.getters[`${ this.inStore }/all`](COUNT)?.[0]?.counts || {};
615
+
616
+ return counts[this.schema.id]?.namespaces || {};
617
+ },
618
+
619
+ /**
620
+ * Whether we should show namespace counts in group tabs
621
+ */
622
+ showNamespaceCounts() {
623
+ return (this.group === 'namespace' || this.group === 'metadata.namespace') && this.isNamespaced && !this.hasSearchFilter;
624
+ },
603
625
  },
604
626
 
605
627
  methods: {
@@ -671,6 +693,8 @@ export default {
671
693
  }
672
694
  });
673
695
  }
696
+
697
+ this.hasSearchFilter = !!arg?.filtering?.searchQuery;
674
698
  }
675
699
  }
676
700
  };
@@ -750,10 +774,15 @@ export default {
750
774
  </template>
751
775
 
752
776
  <template #group-by="{group: thisGroup}">
753
- <div
754
- v-clean-html="thisGroup.ref"
755
- class="group-tab"
756
- />
777
+ <div class="group-tab">
778
+ <span v-clean-html="thisGroup.ref" />
779
+ <span
780
+ v-if="showNamespaceCounts && Number.isInteger(namespaceCounts[thisGroup.rows?.[0]?.metadata?.namespace]?.count)"
781
+ class="count"
782
+ >
783
+ ({{ namespaceCounts[thisGroup.rows?.[0]?.metadata?.namespace]?.count }})
784
+ </span>
785
+ </div>
757
786
  </template>
758
787
 
759
788
  <!-- Pass down templates provided by the caller -->
@@ -799,4 +828,9 @@ export default {
799
828
  .auto-update {
800
829
  min-width: 150px; height: 40px
801
830
  }
831
+
832
+ .group-tab .count {
833
+ opacity: 0.7;
834
+ margin-left: 2px;
835
+ }
802
836
  </style>
@@ -44,6 +44,10 @@ export default {
44
44
  type: Boolean,
45
45
  default: false
46
46
  },
47
+ errorIconTooltip: {
48
+ type: String,
49
+ default: ''
50
+ },
47
51
  badge: {
48
52
  default: 0,
49
53
  required: false,
@@ -359,7 +359,7 @@ export default {
359
359
  >{{ tab.badge }}</span>
360
360
  <i
361
361
  v-if="hasErrorIcon(tab)"
362
- v-clean-tooltip="t('validation.tab')"
362
+ v-clean-tooltip="tab.errorIconTooltip || t('validation.tab')"
363
363
  class="conditions-alert-icon icon-error"
364
364
  />
365
365
  </a>
@@ -417,11 +417,13 @@ export default {
417
417
  :name="tab.name"
418
418
  :label="tab.label"
419
419
  :label-key="tab.labelKey"
420
+ :label-icon="tab.labelIcon"
420
421
  :weight="tab.weight"
421
422
  :tooltip="tab.tooltip"
422
423
  :show-header="tab.showHeader"
423
424
  :display-alert-icon="tab.displayAlertIcon"
424
425
  :error="tab.error"
426
+ :error-icon-tooltip="tab.errorIconTooltip"
425
427
  :badge="tab.badge"
426
428
  >
427
429
  <component
@@ -504,6 +506,7 @@ export default {
504
506
  .conditions-alert-icon {
505
507
  color: var(--error);
506
508
  padding-left: 4px;
509
+ margin-left: auto;
507
510
  }
508
511
 
509
512
  &:last-child {