@rancher/shell 3.0.9-rc.3 → 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 (128) hide show
  1. package/assets/brand/suse/metadata.json +2 -1
  2. package/assets/translations/en-us.yaml +105 -5
  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/KubeconfigClusters.vue +74 -0
  25. package/components/formatter/MachineSummaryGraph.vue +10 -2
  26. package/components/formatter/__tests__/KubeconfigClusters.test.ts +125 -0
  27. package/components/nav/TopLevelMenu.helper.ts +50 -2
  28. package/components/nav/TopLevelMenu.vue +14 -0
  29. package/components/nav/Type.vue +5 -0
  30. package/components/nav/__tests__/TopLevelMenu.test.ts +3 -3
  31. package/components/nav/__tests__/Type.test.ts +6 -4
  32. package/config/product/explorer.js +4 -3
  33. package/config/product/manager.js +47 -3
  34. package/config/router/navigation-guards/authentication.js +8 -9
  35. package/config/router/routes.js +4 -1
  36. package/config/types.js +10 -2
  37. package/detail/auditlog.cattle.io.auditpolicy.vue +19 -0
  38. package/detail/management.cattle.io.user.vue +1 -2
  39. package/detail/node.vue +0 -1
  40. package/detail/provisioning.cattle.io.cluster.vue +2 -1
  41. package/dialog/ChangePasswordDialog.vue +8 -0
  42. package/dialog/GenericPrompt.vue +20 -3
  43. package/dialog/ScaleMachineDownDialog.vue +65 -15
  44. package/dialog/SearchDialog.vue +10 -2
  45. package/dialog/__tests__/ScaleMachineDownDialog.test.ts +184 -0
  46. package/edit/__tests__/fleet.cattle.io.gitrepo.test.ts +89 -0
  47. package/edit/__tests__/management.cattle.io.project.test.js +56 -1
  48. package/edit/auditlog.cattle.io.auditpolicy/AdditionalRedactions.vue +114 -0
  49. package/edit/auditlog.cattle.io.auditpolicy/Filters.vue +119 -0
  50. package/edit/auditlog.cattle.io.auditpolicy/General.vue +180 -0
  51. package/edit/auditlog.cattle.io.auditpolicy/__tests__/AdditionalRedactions.test.ts +327 -0
  52. package/edit/auditlog.cattle.io.auditpolicy/__tests__/Filters.test.ts +449 -0
  53. package/edit/auditlog.cattle.io.auditpolicy/__tests__/General.test.ts +472 -0
  54. package/edit/auditlog.cattle.io.auditpolicy/__tests__/__snapshots__/AdditionalRedactions.test.ts.snap +27 -0
  55. package/edit/auditlog.cattle.io.auditpolicy/__tests__/__snapshots__/Filters.test.ts.snap +39 -0
  56. package/edit/auditlog.cattle.io.auditpolicy/__tests__/__snapshots__/General.test.ts.snap +174 -0
  57. package/edit/auditlog.cattle.io.auditpolicy/__tests__/__snapshots__/index.test.ts.snap +29 -0
  58. package/edit/auditlog.cattle.io.auditpolicy/__tests__/index.test.ts +215 -0
  59. package/edit/auditlog.cattle.io.auditpolicy/index.vue +104 -0
  60. package/edit/auditlog.cattle.io.auditpolicy/types.ts +28 -0
  61. package/edit/fleet.cattle.io.gitrepo.vue +16 -1
  62. package/edit/management.cattle.io.project.vue +8 -2
  63. package/edit/management.cattle.io.user.vue +29 -34
  64. package/edit/provisioning.cattle.io.cluster/__tests__/rke2.test.ts +178 -0
  65. package/edit/provisioning.cattle.io.cluster/rke2.vue +22 -2
  66. package/edit/provisioning.cattle.io.cluster/shared.ts +4 -0
  67. package/edit/provisioning.cattle.io.cluster/tabs/Basics.vue +1 -0
  68. package/edit/provisioning.cattle.io.cluster/tabs/etcd/S3Config.vue +57 -2
  69. package/edit/provisioning.cattle.io.cluster/tabs/etcd/__tests__/S3Config.test.ts +109 -0
  70. package/edit/provisioning.cattle.io.cluster/tabs/etcd/index.vue +1 -0
  71. package/list/auditlog.cattle.io.auditpolicy.vue +63 -0
  72. package/list/ext.cattle.io.kubeconfig.vue +118 -0
  73. package/list/group.principal.vue +11 -15
  74. package/list/management.cattle.io.user.vue +11 -21
  75. package/machine-config/azure.vue +14 -0
  76. package/mixins/__tests__/chart.test.ts +147 -0
  77. package/mixins/browser-tab-visibility.js +5 -4
  78. package/mixins/chart.js +10 -8
  79. package/mixins/fetch.client.js +6 -0
  80. package/models/__tests__/auditlog.cattle.io.auditpolicy.test.ts +117 -0
  81. package/models/__tests__/ext.cattle.io.kubeconfig.test.ts +364 -0
  82. package/models/__tests__/secret.test.ts +55 -0
  83. package/models/__tests__/workload.test.ts +49 -6
  84. package/models/auditlog.cattle.io.auditpolicy.js +46 -0
  85. package/models/cluster.x-k8s.io.machine.js +1 -1
  86. package/models/cluster.x-k8s.io.machinedeployment.js +5 -5
  87. package/models/event.js +5 -0
  88. package/models/ext.cattle.io.groupmembershiprefreshrequest.js +15 -0
  89. package/models/ext.cattle.io.kubeconfig.ts +97 -0
  90. package/models/ext.cattle.io.passwordchangerequest.js +15 -0
  91. package/models/ext.cattle.io.selfuser.js +15 -0
  92. package/models/fleet-application.js +17 -7
  93. package/models/management.cattle.io.user.js +28 -31
  94. package/models/schema.js +18 -0
  95. package/models/secret.js +28 -25
  96. package/models/steve-schema.ts +39 -2
  97. package/models/workload.js +3 -2
  98. package/package.json +2 -2
  99. package/pages/about.vue +3 -2
  100. package/pages/account/index.vue +23 -16
  101. package/pages/auth/login.vue +15 -8
  102. package/pages/auth/setup.vue +52 -15
  103. package/pages/c/_cluster/apps/charts/AppChartCardFooter.vue +38 -14
  104. package/pages/c/_cluster/apps/charts/index.vue +1 -0
  105. package/pages/home.vue +9 -3
  106. package/plugins/dashboard-store/__tests__/resource-class.test.ts +1 -3
  107. package/plugins/dashboard-store/actions.js +7 -0
  108. package/plugins/dashboard-store/getters.js +23 -1
  109. package/plugins/dashboard-store/index.js +3 -2
  110. package/plugins/dashboard-store/mutations.js +4 -0
  111. package/plugins/dashboard-store/resource-class.js +12 -5
  112. package/plugins/steve/__tests__/steve-class.test.ts +167 -0
  113. package/plugins/steve/schema.d.ts +5 -0
  114. package/plugins/steve/steve-class.js +19 -0
  115. package/plugins/steve/steve-pagination-utils.ts +2 -1
  116. package/rancher-components/RcItemCard/RcItemCard.test.ts +4 -2
  117. package/rancher-components/RcItemCard/RcItemCard.vue +27 -10
  118. package/store/auth.js +57 -19
  119. package/store/notifications.ts +1 -1
  120. package/store/type-map.js +12 -1
  121. package/types/shell/index.d.ts +24 -15
  122. package/types/store/dashboard-store.types.ts +7 -0
  123. package/utils/__tests__/chart.test.ts +96 -0
  124. package/utils/__tests__/version.test.ts +1 -19
  125. package/utils/chart.js +64 -0
  126. package/utils/pagination-wrapper.ts +11 -3
  127. package/utils/version.js +5 -17
  128. package/vue.config.js +26 -13
@@ -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
@@ -1183,7 +1187,7 @@ catalog:
1183
1187
  subHeaderItem:
1184
1188
  missingVersionDate: Last updated date is not available for this chart
1185
1189
  footerItem:
1186
- ariaLabel: Apply filter
1190
+ ariaLabel: 'Apply {filter} filter'
1187
1191
  statusFilterCautions:
1188
1192
  installation: Installation status cannot be determined with 100% accuracy
1189
1193
  upgradeable: Upgradeable status cannot be determined with 100% accuracy
@@ -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:
@@ -2543,7 +2550,7 @@ cluster:
2543
2550
  snapshotScheduleCron:
2544
2551
  label: Cron Schedule
2545
2552
  snapshotRetention:
2546
- label: Snapshot retention count
2553
+ label: Local snapshot retention count
2547
2554
  tooltip: Each backup records 1 snapshot per etcd node. If you specify 3 snapshots and you have 3 etcd nodes you will only retain 1 full backup.
2548
2555
  exportMetric:
2549
2556
  label: Metrics
@@ -2552,6 +2559,13 @@ cluster:
2552
2559
  s3backups:
2553
2560
  label: Save Backups to S3
2554
2561
  s3config:
2562
+ snapshotRetention:
2563
+ title: S3 Snapshot Retention
2564
+ label: S3 Snapshot Retention Count
2565
+ options:
2566
+ manual: Set manually
2567
+ localDefined: Same as local ({count} Snaphots)
2568
+ localUndefined: Same as local
2555
2569
  bucket:
2556
2570
  label: Bucket
2557
2571
  folder:
@@ -3105,6 +3119,7 @@ fleet:
3105
3119
  label: Authentication
3106
3120
  git: Git Authentication
3107
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.'
3108
3123
  caBundle:
3109
3124
  label: Certificates
3110
3125
  placeholder: "Paste in one or more certificates, starting with -----BEGIN CERTIFICATE----"
@@ -3629,6 +3644,71 @@ import:
3629
3644
  other {# Resources}
3630
3645
  }
3631
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
+
3632
3712
  ingress:
3633
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.
3634
3714
  certificates:
@@ -5707,7 +5787,11 @@ promptScaleMachineDown:
5707
5787
  attemptingToRemove: "You are attempting to delete {count} {type}"
5708
5788
  retainedMachine1: At least one Machine must exist for roles Control Plane and Etcd.
5709
5789
  retainedMachine2: <b>{ name }</b> will remain
5710
-
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
+ }
5711
5795
  promptSlo:
5712
5796
  title: "Log out"
5713
5797
  text: "Log out of only Rancher, or all {name} applications."
@@ -5925,6 +6009,9 @@ rbac:
5925
6009
  label: Login Access
5926
6010
  clustertemplaterevisions-create:
5927
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
5928
6015
  errors:
5929
6016
  escalation: You cannot assign Global Permissions that are higher than your own. Please verify the permissions you are attempting to assign.
5930
6017
 
@@ -6171,6 +6258,7 @@ selectOrCreateAuthSecret:
6171
6258
  basic:
6172
6259
  username: Username
6173
6260
  password: Password
6261
+ passwordPersonalAccessToken: Password/Personal Access Token
6174
6262
  rke:
6175
6263
  info: "An RKE Auth Config secret contains the username and password concatenated and base64 encoded into the secret's 'auth' key"
6176
6264
  namespaceGroup: "Namespace: {name}"
@@ -6325,6 +6413,7 @@ setup:
6325
6413
  useRandom: Use a randomly generated password
6326
6414
  copyRandom: Copy random password to clipboard
6327
6415
  welcome: Welcome to {vendor}!
6416
+ setup: Setup
6328
6417
 
6329
6418
  sortableTable:
6330
6419
  ariaLabel:
@@ -6888,6 +6977,7 @@ tableHeaders:
6888
6977
  targetPort: Target
6889
6978
  template: Template
6890
6979
  totalSnapshotQuota: Total Snapshot Quota
6980
+ ttl: TTL
6891
6981
  type: Type
6892
6982
  updated: Updated
6893
6983
  up-to-date: Up To Date
@@ -7873,6 +7963,7 @@ typeDescription:
7873
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.
7874
7964
  group.principal: Assigning global roles to a group only works with external auth providers that support groups. Local authorization does not support groups.
7875
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.
7876
7967
 
7877
7968
  typeLabel:
7878
7969
  management.cattle.io.oidcclient: |-
@@ -8025,6 +8116,11 @@ typeLabel:
8025
8116
  one { Resource Quota }
8026
8117
  other { Resource Quotas }
8027
8118
  }
8119
+ auditlog.cattle.io.auditpolicy: |-
8120
+ {count, plural,
8121
+ one { Audit Log Policy }
8122
+ other { Audit Log Policies }
8123
+ }
8028
8124
  # pruh-mee-thee-eyes https://www.prometheus.io/docs/introduction/faq/#what-is-the-plural-of-prometheus
8029
8125
  monitoring.coreos.com.prometheus: |-
8030
8126
  {count, plural,
@@ -9174,7 +9270,7 @@ component:
9174
9270
  events: Events
9175
9271
  extrasCard:
9176
9272
  title: Extras
9177
- 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.'
9178
9274
  scaler:
9179
9275
  ariaLabel:
9180
9276
  increase: Increase {resourceName}
@@ -9308,3 +9404,7 @@ autoscaler:
9308
9404
  unavailable: Unavailable
9309
9405
  tab:
9310
9406
  title: Autoscaler
9407
+
9408
+ ext.cattle.io.kubeconfig:
9409
+ deleted: "{name} (deleted)"
9410
+ moreClusterCount: " + {remainingCount} more"
@@ -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>