@rancher/shell 3.0.12-rc.3 → 3.0.12-rc.4
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.
- package/assets/styles/global/_layout.scss +4 -0
- package/assets/translations/en-us.yaml +144 -41
- package/assets/translations/zh-hans.yaml +1 -7
- package/chart/monitoring/ClusterSelector.vue +0 -21
- package/chart/monitoring/prometheus/index.vue +6 -3
- package/components/CruResource.vue +161 -14
- package/components/ExplorerMembers.vue +8 -4
- package/components/ExplorerProjectsNamespaces.vue +10 -6
- package/components/GrowlManager.vue +4 -0
- package/components/MgmtNodeList.vue +184 -0
- package/components/Resource/Detail/Card/StateCard/__tests__/composables.test.ts +90 -1
- package/components/Resource/Detail/Card/StateCard/composables.ts +57 -87
- package/components/Resource/Detail/Card/StatusCard/__tests__/StatusCard.test.ts +61 -0
- package/components/Resource/Detail/Card/StatusCard/index.vue +61 -15
- package/components/Resource/Detail/Metadata/IdentifyingInformation/index.vue +2 -0
- package/components/Resource/Detail/Metadata/KeyValue.vue +5 -2
- package/components/Resource/Detail/Metadata/KeyValueRow.vue +2 -6
- package/components/ResourceDetail/index.vue +1 -1
- package/components/ResourceList/Masthead.vue +7 -1
- package/components/ResourceList/index.vue +82 -1
- package/components/RichTranslation.vue +5 -2
- package/components/Setting.vue +1 -0
- package/components/SubtleLink.vue +31 -6
- package/components/Tabbed/Tab.vue +29 -3
- package/components/Tabbed/index.vue +25 -3
- package/components/TableOfContents/TableOfContents.vue +109 -0
- package/components/TableOfContents/composables.ts +258 -0
- package/components/Window/ContainerShell.vue +21 -11
- package/components/Window/__tests__/ContainerShell.test.ts +107 -37
- package/components/Wizard.vue +9 -4
- package/components/fleet/AppCoChartGrid.vue +401 -0
- package/components/fleet/AppCoEmptyState.vue +127 -0
- package/components/fleet/AppCoPageHeader.vue +119 -0
- package/components/fleet/AppCoVersionSelect.vue +70 -0
- package/components/fleet/FleetClusterTargets/ClusterSelectionFields.vue +217 -0
- package/components/fleet/FleetClusterTargets/TargetsList.vue +123 -35
- package/components/fleet/FleetClusterTargets/index.vue +189 -146
- package/components/fleet/FleetIntro.vue +7 -3
- package/components/fleet/FleetNoWorkspaces.vue +7 -3
- package/components/fleet/FleetSecretSelector.vue +5 -3
- package/components/fleet/FleetValuesFrom.vue +8 -2
- package/components/fleet/GitRepoTargetTab.vue +0 -2
- package/components/fleet/HelmOpAdvancedTab.vue +19 -53
- package/components/fleet/HelmOpAppCoConfigTab.vue +593 -0
- package/components/fleet/HelmOpAppCoResourcesSection.vue +162 -0
- package/components/fleet/HelmOpResourcesSection.vue +82 -0
- package/components/fleet/HelmOpTargetOptionsSection.vue +89 -0
- package/components/fleet/HelmOpTargetTab.vue +64 -60
- package/components/fleet/HelmOpValuesTab.vue +129 -105
- package/components/fleet/__tests__/AppCoEmptyState.test.ts +71 -0
- package/components/fleet/__tests__/AppCoVersionSelect.test.ts +36 -0
- package/components/fleet/__tests__/ClusterSelectionFields.test.ts +62 -0
- package/components/fleet/__tests__/FleetClusterTargets.test.ts +253 -0
- package/components/fleet/__tests__/FleetSecretSelector.test.ts +16 -0
- package/components/fleet/__tests__/FleetValuesFrom.test.ts +44 -0
- package/components/fleet/__tests__/HelmOpAppCoConfigTab.test.ts +59 -0
- package/components/fleet/__tests__/HelmOpAppCoResourcesSection.test.ts +62 -0
- package/components/fleet/__tests__/HelmOpResourcesSection.test.ts +43 -0
- package/components/fleet/__tests__/HelmOpTargetOptionsSection.test.ts +34 -0
- package/components/fleet/__tests__/HelmOpValuesTab.test.ts +39 -0
- package/components/fleet/__tests__/__snapshots__/AppCoEmptyState.test.ts.snap +97 -0
- package/components/fleet/__tests__/__snapshots__/AppCoVersionSelect.test.ts.snap +30 -0
- package/components/fleet/__tests__/__snapshots__/ClusterSelectionFields.test.ts.snap +209 -0
- package/components/fleet/__tests__/__snapshots__/HelmOpTargetOptionsSection.test.ts.snap +140 -0
- package/components/fleet/dashboard/Empty.vue +8 -4
- package/components/fleet/dashboard/ResourceCard.vue +28 -0
- package/components/fleet/dashboard/ResourceDetails.vue +28 -0
- package/components/fleet/dashboard/__tests__/ResourceCard.test.ts +87 -0
- package/components/form/ArrayList.vue +61 -4
- package/components/form/KeyValue.vue +23 -2
- package/components/form/LabeledSelect.vue +39 -1
- package/components/form/Labels.vue +22 -3
- package/components/form/NameNsDescription.vue +13 -5
- package/components/form/ResourceTabs/index.vue +1 -0
- package/components/form/__tests__/NameNsDescription.test.ts +75 -0
- package/components/formatter/InternalExternalIP.vue +10 -4
- package/components/formatter/ServiceTargets.vue +26 -7
- package/components/formatter/__tests__/InternalExternalIP.test.ts +132 -0
- package/components/formatter/__tests__/ServiceTargets.test.ts +412 -0
- package/components/nav/Header.vue +4 -0
- package/components/nav/TopLevelMenu.vue +7 -2
- package/components/nav/__tests__/Header.test.ts +15 -0
- package/components/nav/__tests__/TopLevelMenu.test.ts +120 -2
- package/components/templates/default.vue +9 -4
- package/components/templates/home.vue +9 -4
- package/components/templates/plain.vue +9 -4
- package/composables/useHelmOpResources.test.ts +56 -0
- package/composables/useHelmOpResources.ts +32 -0
- package/composables/useStateColor.test.ts +325 -0
- package/composables/useStateColor.ts +128 -0
- package/config/home-links.js +1 -1
- package/config/labels-annotations.js +1 -0
- package/config/product/explorer.js +17 -4
- package/config/product/manager.js +2 -0
- package/config/router/index.js +16 -0
- package/config/router/navigation-guards/__tests__/authentication.test.ts +130 -0
- package/config/router/navigation-guards/authentication.js +10 -4
- package/config/router/routes.js +20 -6
- package/config/settings.ts +0 -2
- package/config/table-headers.js +3 -4
- package/config/types.js +9 -0
- package/core/plugin-products-base.ts +3 -3
- package/core/plugin-types.ts +83 -30
- package/core/plugin.ts +3 -0
- package/core/types-provisioning.ts +34 -1
- package/core/types.ts +15 -2
- package/detail/__tests__/provisioning.cattle.io.cluster.test.ts +114 -0
- package/detail/__tests__/workload.test.ts +3 -152
- package/detail/catalog.cattle.io.clusterrepo.vue +1 -1
- package/detail/provisioning.cattle.io.cluster.vue +30 -4
- package/detail/workload/index.vue +12 -55
- package/edit/__tests__/catalog.cattle.io.clusterrepo.test.ts +248 -0
- package/edit/__tests__/fleet.cattle.io.helmop.test.ts +105 -0
- package/edit/auditlog.cattle.io.auditpolicy/__tests__/__snapshots__/General.test.ts.snap +6 -0
- package/edit/auditlog.cattle.io.auditpolicy/__tests__/__snapshots__/index.test.ts.snap +1 -0
- package/edit/auth/__tests__/azuread.test.ts +34 -9
- package/edit/auth/__tests__/github.test.ts +234 -0
- package/edit/auth/__tests__/oidc.test.ts +26 -6
- package/edit/auth/__tests__/saml.test.ts +196 -0
- package/edit/auth/azuread.vue +128 -95
- package/edit/auth/github.vue +72 -13
- package/edit/auth/ldap/__tests__/index.test.ts +206 -0
- package/edit/auth/ldap/config.vue +8 -0
- package/edit/auth/ldap/index.vue +75 -1
- package/edit/auth/oidc.vue +119 -73
- package/edit/auth/saml.vue +76 -12
- package/edit/catalog.cattle.io.clusterrepo.vue +140 -32
- package/edit/fleet.cattle.io.helmop.vue +491 -136
- package/edit/management.cattle.io.user.vue +5 -2
- package/edit/provisioning.cattle.io.cluster/rke2.vue +84 -10
- package/edit/provisioning.cattle.io.cluster/tabs/MachinePool.vue +11 -0
- package/list/group.principal.vue +5 -4
- package/list/harvesterhci.io.management.cluster.vue +8 -9
- package/list/management.cattle.io.user.vue +12 -9
- package/list/provisioning.cattle.io.cluster.vue +16 -10
- package/mixins/__tests__/auth-config.test.ts +90 -0
- package/mixins/__tests__/chart.test.ts +94 -0
- package/mixins/__tests__/resource-fetch-api-pagination.test.ts +48 -0
- package/mixins/auth-config.js +7 -0
- package/mixins/chart.js +11 -2
- package/mixins/child-hook.js +12 -6
- package/mixins/create-edit-view/impl.js +5 -3
- package/mixins/resource-fetch-api-pagination.js +21 -1
- package/models/__tests__/catalog.cattle.io.clusterrepo.test.ts +57 -0
- package/models/__tests__/compliance.cattle.io.clusterscan.test.ts +144 -0
- package/models/__tests__/fleet-application.test.ts +175 -0
- package/models/__tests__/fleet.cattle.io.bundle.test.ts +169 -0
- package/models/__tests__/fleet.cattle.io.helmop.test.ts +84 -0
- package/models/__tests__/management.cattle.io.node.ts +22 -0
- package/models/__tests__/namespace.test.ts +36 -0
- package/models/__tests__/provisioning.cattle.io.cluster.test.ts +49 -0
- package/models/__tests__/workload.test.ts +401 -26
- package/models/catalog.cattle.io.clusterrepo.js +28 -4
- package/models/compliance.cattle.io.clusterscan.js +39 -4
- package/models/fleet-application.js +4 -0
- package/models/fleet.cattle.io.helmop.js +20 -1
- package/models/management.cattle.io.cluster.js +18 -2
- package/models/management.cattle.io.node.js +44 -3
- package/models/namespace.js +1 -1
- package/models/pod.js +33 -1
- package/models/provisioning.cattle.io.cluster.js +5 -5
- package/models/workload.js +108 -13
- package/models/workload.service.js +5 -0
- package/package.json +14 -13
- package/pages/about.vue +5 -6
- package/pages/auth/login.vue +0 -35
- package/pages/auth/setup.vue +11 -0
- package/pages/c/_cluster/apps/charts/AppChartCardFooter.vue +2 -2
- package/pages/c/_cluster/apps/charts/AppChartCardSubHeader.vue +10 -1
- package/pages/c/_cluster/apps/charts/__tests__/index.test.ts +93 -0
- package/pages/c/_cluster/apps/charts/chart.vue +2 -1
- package/pages/c/_cluster/apps/charts/index.vue +48 -10
- package/pages/c/_cluster/apps/charts/install.vue +122 -116
- package/pages/c/_cluster/auth/roles/index.vue +5 -4
- package/pages/c/_cluster/explorer/workload-dashboard/ByNamespaceSection.vue +31 -0
- package/pages/c/_cluster/explorer/workload-dashboard/ByStateSection.vue +138 -0
- package/pages/c/_cluster/explorer/workload-dashboard/ByTypeSection.vue +30 -0
- package/pages/c/_cluster/explorer/workload-dashboard/WorkloadCard.vue +155 -0
- package/pages/c/_cluster/explorer/workload-dashboard/WorkloadNamespaceCard.vue +142 -0
- package/pages/c/_cluster/explorer/workload-dashboard/WorkloadTypeCard.vue +159 -0
- package/pages/c/_cluster/explorer/workload-dashboard/__tests__/composable.test.ts +561 -0
- package/pages/c/_cluster/explorer/workload-dashboard/composable.ts +440 -0
- package/pages/c/_cluster/explorer/workload-dashboard/index.vue +187 -0
- package/pages/c/_cluster/explorer/workload-dashboard/types.ts +80 -0
- package/pages/c/_cluster/fleet/application/create.vue +187 -136
- package/pages/c/_cluster/fleet/application/index.vue +5 -3
- package/pages/c/_cluster/fleet/application/suse-app-collection/ChartDetailBody.vue +338 -0
- package/pages/c/_cluster/fleet/application/suse-app-collection/ChartDetailHeader.vue +121 -0
- package/pages/c/_cluster/fleet/application/suse-app-collection/chart.vue +369 -0
- package/pages/c/_cluster/fleet/application/suse-app-collection/charts.vue +248 -0
- package/pages/c/_cluster/fleet/application/suse-app-collection/credentials.vue +310 -0
- package/pages/c/_cluster/fleet/index.vue +2 -2
- package/pages/c/_cluster/uiplugins/__tests__/index.test.ts +96 -0
- package/pages/c/_cluster/uiplugins/index.vue +15 -0
- package/pages/fail-whale.vue +16 -11
- package/pages/home.vue +16 -46
- package/plugins/clean-html.d.ts +9 -0
- package/plugins/dashboard-store/__tests__/resource-class.test.ts +93 -0
- package/plugins/dashboard-store/resource-class.js +62 -7
- package/plugins/steve/__tests__/actions.test.ts +212 -0
- package/plugins/steve/actions.js +96 -0
- package/plugins/steve/steve-pagination-utils.ts +1 -1
- package/rancher-components/Accordion/Accordion.vue +53 -9
- package/rancher-components/Form/Checkbox/Checkbox.vue +14 -0
- package/rancher-components/Form/Radio/RadioButton.vue +17 -1
- package/rancher-components/Form/Radio/RadioGroup.vue +10 -0
- package/rancher-components/Pill/RcTag/RcTag.vue +3 -2
- package/rancher-components/RcButton/RcButton.test.ts +103 -0
- package/rancher-components/RcButton/RcButton.vue +94 -15
- package/rancher-components/RcButton/types.ts +3 -0
- package/rancher-components/RcItemCard/RcItemCard.test.ts +18 -0
- package/rancher-components/RcItemCard/RcItemCard.vue +2 -2
- package/rancher-components/RcSection/RcSection.vue +28 -3
- package/scripts/extension/helm/package/Dockerfile +1 -1
- package/scripts/test-plugins-build.sh +2 -1
- package/store/__tests__/notifications.test.ts +434 -0
- package/store/catalog.js +57 -0
- package/store/plugins.js +7 -4
- package/types/components/buttonGroup.ts +5 -0
- package/types/shell/index.d.ts +104 -70
- package/utils/__tests__/auth.test.ts +273 -0
- package/utils/__tests__/computed.test.ts +193 -0
- package/utils/__tests__/cspAdaptor.test.ts +163 -0
- package/utils/__tests__/dom.test.ts +81 -0
- package/utils/__tests__/duration.test.ts +37 -1
- package/utils/__tests__/dynamic-importer.test.ts +102 -0
- package/utils/__tests__/fleet-appco.test.ts +312 -0
- package/utils/__tests__/monitoring.test.ts +130 -0
- package/utils/__tests__/object.test.ts +22 -0
- package/utils/__tests__/platform.test.ts +91 -0
- package/utils/__tests__/position.test.ts +237 -0
- package/utils/__tests__/provider.test.ts +51 -1
- package/utils/__tests__/queue.test.ts +232 -0
- package/utils/__tests__/release-notes.test.ts +221 -0
- package/utils/__tests__/router.test.js +254 -1
- package/utils/__tests__/select.test.ts +208 -0
- package/utils/__tests__/time.test.ts +265 -1
- package/utils/__tests__/title.test.ts +47 -0
- package/utils/__tests__/width.test.ts +53 -0
- package/utils/__tests__/window.test.ts +158 -0
- package/utils/__tests__/xccdf.test.ts +126 -6
- package/utils/crypto/__tests__/browserHashUtils.test.ts +98 -0
- package/utils/crypto/__tests__/index.test.ts +144 -0
- package/utils/duration.ts +104 -0
- package/utils/dynamic-content/__tests__/notification-handler.test.ts +196 -0
- package/utils/dynamic-content/info.ts +2 -1
- package/utils/error.js +13 -0
- package/utils/fleet-appco.ts +323 -0
- package/utils/object.js +22 -2
- package/utils/provider.ts +12 -0
- package/utils/validators/__tests__/container-images.test.ts +104 -0
- package/utils/validators/__tests__/flow-output.test.ts +91 -0
- package/utils/validators/__tests__/logging-outputs.test.ts +58 -0
- package/utils/validators/__tests__/monitoring-route.test.ts +119 -0
- package/utils/xccdf.ts +39 -42
- package/vue.config.js +1 -1
- package/pages/support/index.vue +0 -264
- package/utils/duration.js +0 -43
|
@@ -403,6 +403,99 @@ describe('class: Resource', () => {
|
|
|
403
403
|
|
|
404
404
|
expect(cards).toHaveLength(0);
|
|
405
405
|
});
|
|
406
|
+
|
|
407
|
+
it('should include the resources card when relationships exist', () => {
|
|
408
|
+
const resource = new Resource({
|
|
409
|
+
type: 'test',
|
|
410
|
+
metadata: {
|
|
411
|
+
relationships: [
|
|
412
|
+
{
|
|
413
|
+
rel: 'uses', toType: 'svc', toId: 'a'
|
|
414
|
+
},
|
|
415
|
+
{
|
|
416
|
+
rel: 'uses', fromType: 'pod', fromId: 'b'
|
|
417
|
+
},
|
|
418
|
+
]
|
|
419
|
+
}
|
|
420
|
+
}, {
|
|
421
|
+
getters: { schemaFor: () => ({ linkFor: jest.fn() }) },
|
|
422
|
+
dispatch: jest.fn(),
|
|
423
|
+
rootGetters: {
|
|
424
|
+
'i18n/t': (key: string) => key,
|
|
425
|
+
'cluster/all': () => []
|
|
426
|
+
},
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
const cards = resource.cards;
|
|
430
|
+
|
|
431
|
+
expect(cards).toHaveLength(1);
|
|
432
|
+
expect(cards[0].props.title).toBe('component.resource.detail.card.resourcesCard.title');
|
|
433
|
+
});
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
describe('getter: resourcesCard', () => {
|
|
437
|
+
it('should return null when there are no relationships', () => {
|
|
438
|
+
const resource = new Resource({ type: 'test' }, {
|
|
439
|
+
getters: { schemaFor: () => ({ linkFor: jest.fn() }) },
|
|
440
|
+
dispatch: jest.fn(),
|
|
441
|
+
rootGetters: { 'i18n/t': (key: string) => key },
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
expect(resource.resourcesCard).toBeNull();
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
it('should return rows for both referredToBy and refersTo when relationships exist in both directions', () => {
|
|
448
|
+
const resource = new Resource({
|
|
449
|
+
type: 'test',
|
|
450
|
+
metadata: {
|
|
451
|
+
relationships: [
|
|
452
|
+
{
|
|
453
|
+
rel: 'owner', fromType: 'rs', fromId: 'r-1'
|
|
454
|
+
},
|
|
455
|
+
{
|
|
456
|
+
rel: 'uses', toType: 'svc', toId: 's-1'
|
|
457
|
+
},
|
|
458
|
+
{
|
|
459
|
+
rel: 'uses', toType: 'svc', toId: 's-2'
|
|
460
|
+
},
|
|
461
|
+
]
|
|
462
|
+
}
|
|
463
|
+
}, {
|
|
464
|
+
getters: { schemaFor: () => ({ linkFor: jest.fn() }) },
|
|
465
|
+
dispatch: jest.fn(),
|
|
466
|
+
rootGetters: { 'i18n/t': (key: string) => key },
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
const rows = resource.resourcesCardRows;
|
|
470
|
+
|
|
471
|
+
expect(rows).toHaveLength(2);
|
|
472
|
+
expect(rows[0].label).toBe('component.resource.detail.card.resourcesCard.rows.referredToBy');
|
|
473
|
+
expect(rows[0].counts[0].count).toBe(1);
|
|
474
|
+
expect(rows[1].label).toBe('component.resource.detail.card.resourcesCard.rows.refersTo');
|
|
475
|
+
expect(rows[1].counts[0].count).toBe(2);
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
it('should omit a direction with no relationships', () => {
|
|
479
|
+
const resource = new Resource({
|
|
480
|
+
type: 'test',
|
|
481
|
+
metadata: {
|
|
482
|
+
relationships: [
|
|
483
|
+
{
|
|
484
|
+
rel: 'uses', toType: 'svc', toId: 's-1'
|
|
485
|
+
},
|
|
486
|
+
]
|
|
487
|
+
}
|
|
488
|
+
}, {
|
|
489
|
+
getters: { schemaFor: () => ({ linkFor: jest.fn() }) },
|
|
490
|
+
dispatch: jest.fn(),
|
|
491
|
+
rootGetters: { 'i18n/t': (key: string) => key },
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
const rows = resource.resourcesCardRows;
|
|
495
|
+
|
|
496
|
+
expect(rows).toHaveLength(1);
|
|
497
|
+
expect(rows[0].label).toBe('component.resource.detail.card.resourcesCard.rows.refersTo');
|
|
498
|
+
});
|
|
406
499
|
});
|
|
407
500
|
|
|
408
501
|
describe('getter: insightCardProps', () => {
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
AS,
|
|
13
13
|
MODE
|
|
14
14
|
} from '@shell/config/query-params';
|
|
15
|
+
import { EVENT } from '@shell/config/types';
|
|
15
16
|
import { VIEW_IN_API, DEV } from '@shell/store/prefs';
|
|
16
17
|
import { addObject, addObjects, findBy, removeAt } from '@shell/utils/array';
|
|
17
18
|
import CustomValidators from '@shell/utils/custom-validators';
|
|
@@ -39,8 +40,7 @@ import { handleConflict } from '@shell/plugins/dashboard-store/normalize';
|
|
|
39
40
|
import { ExtensionPoint, ActionLocation } from '@shell/core/types';
|
|
40
41
|
import { getApplicableExtensionEnhancements } from '@shell/core/plugin-helpers';
|
|
41
42
|
import { parse } from '@shell/utils/selector';
|
|
42
|
-
import {
|
|
43
|
-
import { useResourceCardRow } from '@shell/components/Resource/Detail/Card/StateCard/composables';
|
|
43
|
+
import { useResourceCardRow, useResourceCardRowFromRelationships } from '@shell/components/Resource/Detail/Card/StateCard/composables';
|
|
44
44
|
|
|
45
45
|
export const DNS_LIKE_TYPES = ['dnsLabel', 'dnsLabelRestricted', 'hostname'];
|
|
46
46
|
|
|
@@ -507,7 +507,11 @@ export function colorForState(state, isError, isTransitioning) {
|
|
|
507
507
|
return `text-${ color }`;
|
|
508
508
|
}
|
|
509
509
|
|
|
510
|
-
export function
|
|
510
|
+
export function simpleColorForState(state, isError = false, isTransitioning = false) {
|
|
511
|
+
return colorForState(state, isError, isTransitioning).replace('text-', '') || 'disabled';
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
export function stateDisplay(state, preserveOriginal = false) {
|
|
511
515
|
// @TODO use translations
|
|
512
516
|
const key = (state || 'active').toLowerCase();
|
|
513
517
|
|
|
@@ -515,6 +519,11 @@ export function stateDisplay(state) {
|
|
|
515
519
|
return REMAP_STATE[key];
|
|
516
520
|
}
|
|
517
521
|
|
|
522
|
+
// Preserves the original state name returned by the
|
|
523
|
+
if ( preserveOriginal ) {
|
|
524
|
+
return ucFirst(state);
|
|
525
|
+
}
|
|
526
|
+
|
|
518
527
|
return key.split(/-/).map(ucFirst).join('-');
|
|
519
528
|
}
|
|
520
529
|
|
|
@@ -754,7 +763,7 @@ export default class Resource {
|
|
|
754
763
|
}
|
|
755
764
|
|
|
756
765
|
get stateSimpleColor() {
|
|
757
|
-
return this.
|
|
766
|
+
return simpleColorForState(this.state, this.stateObj?.error, this.stateObj?.transitioning);
|
|
758
767
|
}
|
|
759
768
|
|
|
760
769
|
get stateBackground() {
|
|
@@ -2073,7 +2082,7 @@ export default class Resource {
|
|
|
2073
2082
|
|
|
2074
2083
|
if ( r.selector ) {
|
|
2075
2084
|
// A selector is a stringified version of a matchLabel (https://github.com/kubernetes/apimachinery/blob/master/pkg/labels/selector.go#L1010)
|
|
2076
|
-
|
|
2085
|
+
addObject(out.selectors, {
|
|
2077
2086
|
type: r.toType,
|
|
2078
2087
|
namespace: r.toNamespace,
|
|
2079
2088
|
selector: r.selector
|
|
@@ -2083,7 +2092,7 @@ export default class Resource {
|
|
|
2083
2092
|
let namespace = r[`${ direction }Namespace`];
|
|
2084
2093
|
let name = r[`${ direction }Id`];
|
|
2085
2094
|
|
|
2086
|
-
if ( !namespace && name
|
|
2095
|
+
if ( !namespace && name?.includes('/') ) {
|
|
2087
2096
|
const idx = name.indexOf('/');
|
|
2088
2097
|
|
|
2089
2098
|
namespace = name.substr(0, idx);
|
|
@@ -2245,12 +2254,58 @@ export default class Resource {
|
|
|
2245
2254
|
};
|
|
2246
2255
|
}
|
|
2247
2256
|
|
|
2257
|
+
get _resourcesCardRows() {
|
|
2258
|
+
const rows = [];
|
|
2259
|
+
const relationships = this.metadata?.relationships || [];
|
|
2260
|
+
|
|
2261
|
+
const referredToByRels = relationships.filter((r) => r.fromType && r.fromId && !r.selector);
|
|
2262
|
+
const refersToRels = relationships.filter((r) => r.toType && r.toId && !r.selector && !r.fromType);
|
|
2263
|
+
|
|
2264
|
+
if (referredToByRels.length) {
|
|
2265
|
+
rows.push(useResourceCardRowFromRelationships(
|
|
2266
|
+
this.t('component.resource.detail.card.resourcesCard.rows.referredToBy'),
|
|
2267
|
+
referredToByRels,
|
|
2268
|
+
{ hash: '#related' }
|
|
2269
|
+
));
|
|
2270
|
+
}
|
|
2271
|
+
|
|
2272
|
+
if (refersToRels.length) {
|
|
2273
|
+
rows.push(useResourceCardRowFromRelationships(
|
|
2274
|
+
this.t('component.resource.detail.card.resourcesCard.rows.refersTo'),
|
|
2275
|
+
refersToRels,
|
|
2276
|
+
{ hash: '#related' }
|
|
2277
|
+
));
|
|
2278
|
+
}
|
|
2279
|
+
|
|
2280
|
+
return rows;
|
|
2281
|
+
}
|
|
2282
|
+
|
|
2283
|
+
get resourcesCardRows() {
|
|
2284
|
+
return this._resourcesCardRows;
|
|
2285
|
+
}
|
|
2286
|
+
|
|
2287
|
+
get resourcesCard() {
|
|
2288
|
+
const rows = this.resourcesCardRows;
|
|
2289
|
+
|
|
2290
|
+
if (!rows.length) {
|
|
2291
|
+
return null;
|
|
2292
|
+
}
|
|
2293
|
+
|
|
2294
|
+
return {
|
|
2295
|
+
component: markRaw(defineAsyncComponent(() => import('@shell/components/Resource/Detail/Card/StateCard/index.vue'))),
|
|
2296
|
+
props: {
|
|
2297
|
+
title: this.t('component.resource.detail.card.resourcesCard.title'),
|
|
2298
|
+
rows
|
|
2299
|
+
}
|
|
2300
|
+
};
|
|
2301
|
+
}
|
|
2302
|
+
|
|
2248
2303
|
get _cards() {
|
|
2249
2304
|
// All cards are opt in, we're leaving the insights card as part of the base resource since it should proliferate to most resources
|
|
2250
2305
|
return [];
|
|
2251
2306
|
}
|
|
2252
2307
|
|
|
2253
2308
|
get cards() {
|
|
2254
|
-
return this._cards;
|
|
2309
|
+
return [this.resourcesCard, ...this._cards].filter((c) => c);
|
|
2255
2310
|
}
|
|
2256
2311
|
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import actions from '@shell/plugins/steve/actions';
|
|
2
|
+
import paginationUtils from '@shell/utils/pagination-utils';
|
|
3
|
+
import stevePaginationUtils from '@shell/plugins/steve/steve-pagination-utils';
|
|
4
|
+
import { PaginationParamFilter } from '@shell/types/store/pagination.types';
|
|
5
|
+
|
|
6
|
+
const { fetchResourceSummary } = actions;
|
|
7
|
+
|
|
8
|
+
describe('steve: actions:', () => {
|
|
9
|
+
describe('fetchResourceSummary', () => {
|
|
10
|
+
const schema = {
|
|
11
|
+
id: 'pod',
|
|
12
|
+
links: { collection: '/v1/pods' },
|
|
13
|
+
attributes: { namespaced: true },
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const baseCtx = () => ({
|
|
17
|
+
getters: {
|
|
18
|
+
normalizeType: (type: string) => type,
|
|
19
|
+
schemaFor: (type: string) => (type === 'pod' ? schema : undefined),
|
|
20
|
+
},
|
|
21
|
+
dispatch: jest.fn(),
|
|
22
|
+
rootGetters: {},
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
let warnSpy: jest.SpyInstance;
|
|
26
|
+
|
|
27
|
+
beforeEach(() => {
|
|
28
|
+
warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
|
|
29
|
+
jest.spyOn(paginationUtils, 'isSteveCacheEnabled').mockReturnValue(true);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
afterEach(() => {
|
|
33
|
+
jest.restoreAllMocks();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('should return undefined and warn when schema is not found', async() => {
|
|
37
|
+
const ctx = baseCtx();
|
|
38
|
+
const result = await fetchResourceSummary.call({}, ctx, { type: 'nonexistent', opt: { summaryField: 'metadata.state.name' } });
|
|
39
|
+
|
|
40
|
+
expect(result).toBeUndefined();
|
|
41
|
+
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('no schema found'));
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('should return undefined and warn when VAI is not enabled', async() => {
|
|
45
|
+
jest.spyOn(paginationUtils, 'isSteveCacheEnabled').mockReturnValue(false);
|
|
46
|
+
const ctx = baseCtx();
|
|
47
|
+
const result = await fetchResourceSummary.call({}, ctx, { type: 'pod', opt: { summaryField: 'metadata.state.name' } });
|
|
48
|
+
|
|
49
|
+
expect(result).toBeUndefined();
|
|
50
|
+
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('VAI is not enabled'));
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should return undefined and warn when summaryField is missing', async() => {
|
|
54
|
+
const ctx = baseCtx();
|
|
55
|
+
const result = await fetchResourceSummary.call({}, ctx, { type: 'pod', opt: {} });
|
|
56
|
+
|
|
57
|
+
expect(result).toBeUndefined();
|
|
58
|
+
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('summaryField is required'));
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('should construct the correct URL with summary and summaryonly params', async() => {
|
|
62
|
+
const ctx = baseCtx();
|
|
63
|
+
|
|
64
|
+
ctx.dispatch.mockResolvedValue({ count: 5, summary: [{ property: 'metadata.state.name', counts: { running: { total: 5 } } }] });
|
|
65
|
+
|
|
66
|
+
await fetchResourceSummary.call({}, ctx, { type: 'pod', opt: { summaryField: 'metadata.state.name' } });
|
|
67
|
+
|
|
68
|
+
const requestUrl = ctx.dispatch.mock.calls[0][1].opt.url;
|
|
69
|
+
|
|
70
|
+
expect(requestUrl).toContain('summary=metadata.state.name');
|
|
71
|
+
expect(requestUrl).toContain('summaryonly=');
|
|
72
|
+
expect(requestUrl).not.toContain('summarynamespaced');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('should not include summaryonly when summaryOnly is false', async() => {
|
|
76
|
+
const ctx = baseCtx();
|
|
77
|
+
|
|
78
|
+
ctx.dispatch.mockResolvedValue({ count: 5, summary: [{ property: 'metadata.state.name', counts: { running: { total: 5 } } }] });
|
|
79
|
+
|
|
80
|
+
await fetchResourceSummary.call({}, ctx, { type: 'pod', opt: { summaryField: 'metadata.state.name', summaryOnly: false } });
|
|
81
|
+
|
|
82
|
+
const requestUrl = ctx.dispatch.mock.calls[0][1].opt.url;
|
|
83
|
+
|
|
84
|
+
expect(requestUrl).not.toContain('summaryonly');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('should include summarynamespaced param when namespaceCounts is true', async() => {
|
|
88
|
+
const ctx = baseCtx();
|
|
89
|
+
|
|
90
|
+
ctx.dispatch.mockResolvedValue({ count: 5, summary: [{ property: 'metadata.state.name', counts: { running: { total: 5 } } }] });
|
|
91
|
+
|
|
92
|
+
await fetchResourceSummary.call({}, ctx, { type: 'pod', opt: { summaryField: 'metadata.state.name', namespaceCounts: true } });
|
|
93
|
+
|
|
94
|
+
const requestUrl = ctx.dispatch.mock.calls[0][1].opt.url;
|
|
95
|
+
|
|
96
|
+
expect(requestUrl).toContain('summarynamespaced=');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('should append namespace to path for namespaced resources', async() => {
|
|
100
|
+
const ctx = baseCtx();
|
|
101
|
+
|
|
102
|
+
ctx.dispatch.mockResolvedValue({ count: 2, summary: null });
|
|
103
|
+
|
|
104
|
+
await fetchResourceSummary.call({}, ctx, { type: 'pod', opt: { summaryField: 'metadata.state.name', namespace: 'cattle-system' } });
|
|
105
|
+
|
|
106
|
+
const requestUrl = ctx.dispatch.mock.calls[0][1].opt.url;
|
|
107
|
+
|
|
108
|
+
expect(requestUrl).toMatch(/\/v1\/pods\/cattle-system\?/);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('should not append namespace when schema is not namespaced', async() => {
|
|
112
|
+
const nonNsSchema = { ...schema, attributes: { namespaced: false } };
|
|
113
|
+
const ctx = baseCtx();
|
|
114
|
+
|
|
115
|
+
ctx.getters.schemaFor = () => nonNsSchema;
|
|
116
|
+
ctx.dispatch.mockResolvedValue({ count: 1, summary: null });
|
|
117
|
+
|
|
118
|
+
await fetchResourceSummary.call({}, ctx, { type: 'pod', opt: { summaryField: 'metadata.state.name', namespace: 'default' } });
|
|
119
|
+
|
|
120
|
+
const requestUrl = ctx.dispatch.mock.calls[0][1].opt.url;
|
|
121
|
+
|
|
122
|
+
expect(requestUrl).not.toContain('/default');
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('should append filter params when filters are provided', async() => {
|
|
126
|
+
const ctx = baseCtx();
|
|
127
|
+
const filters = [PaginationParamFilter.createSingleField({ field: 'metadata.namespace', value: 'default' })];
|
|
128
|
+
|
|
129
|
+
jest.spyOn(stevePaginationUtils, 'convertPaginationParams').mockReturnValue('filter=metadata.namespace%3Ddefault');
|
|
130
|
+
ctx.dispatch.mockResolvedValue({ count: 3, summary: null });
|
|
131
|
+
|
|
132
|
+
await fetchResourceSummary.call({}, ctx, { type: 'pod', opt: { summaryField: 'metadata.state.name', filters } });
|
|
133
|
+
|
|
134
|
+
const requestUrl = ctx.dispatch.mock.calls[0][1].opt.url;
|
|
135
|
+
|
|
136
|
+
expect(requestUrl).toContain('filter=');
|
|
137
|
+
expect(stevePaginationUtils.convertPaginationParams).toHaveBeenCalledWith(expect.objectContaining({ filters }));
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('should return count and summary from the response', async() => {
|
|
141
|
+
const ctx = baseCtx();
|
|
142
|
+
const apiResponse = {
|
|
143
|
+
count: 10,
|
|
144
|
+
summary: [{ property: 'metadata.state.name', counts: { running: { total: 7 }, error: { total: 3 } } }]
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
ctx.dispatch.mockResolvedValue(apiResponse);
|
|
148
|
+
|
|
149
|
+
const result = await fetchResourceSummary.call({}, ctx, { type: 'pod', opt: { summaryField: 'metadata.state.name' } });
|
|
150
|
+
|
|
151
|
+
expect(result).toStrictEqual(apiResponse);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('should pass through object-style counts as-is', async() => {
|
|
155
|
+
const ctx = baseCtx();
|
|
156
|
+
const counts = { running: { total: 7 }, error: { total: 3 } };
|
|
157
|
+
const apiResponse = {
|
|
158
|
+
count: 10,
|
|
159
|
+
summary: [{ property: 'metadata.state.name', counts }]
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
ctx.dispatch.mockResolvedValue(apiResponse);
|
|
163
|
+
|
|
164
|
+
const result = await fetchResourceSummary.call({}, ctx, { type: 'pod', opt: { summaryField: 'metadata.state.name' } });
|
|
165
|
+
|
|
166
|
+
expect(result).toStrictEqual({
|
|
167
|
+
count: 10,
|
|
168
|
+
summary: [{ property: 'metadata.state.name', counts }]
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('should default count to 0 and summary to null when response is empty', async() => {
|
|
173
|
+
const ctx = baseCtx();
|
|
174
|
+
|
|
175
|
+
ctx.dispatch.mockResolvedValue({});
|
|
176
|
+
|
|
177
|
+
const result = await fetchResourceSummary.call({}, ctx, { type: 'pod', opt: { summaryField: 'metadata.state.name' } });
|
|
178
|
+
|
|
179
|
+
expect(result).toStrictEqual({ count: 0, summary: null });
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('should append label selector params when labelSelector is provided', async() => {
|
|
183
|
+
const ctx = baseCtx();
|
|
184
|
+
const labelSelector = {
|
|
185
|
+
matchExpressions: [{
|
|
186
|
+
key: 'app', operator: 'In', values: ['nginx']
|
|
187
|
+
}]
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
jest.spyOn(stevePaginationUtils, 'convertLabelSelectorPaginationParams').mockReturnValue('filter=metadata.labels[app] IN (nginx)');
|
|
191
|
+
ctx.dispatch.mockResolvedValue({ count: 2, summary: null });
|
|
192
|
+
|
|
193
|
+
await fetchResourceSummary.call({}, ctx, { type: 'pod', opt: { summaryField: 'metadata.state.name', labelSelector } });
|
|
194
|
+
|
|
195
|
+
const requestUrl = ctx.dispatch.mock.calls[0][1].opt.url;
|
|
196
|
+
|
|
197
|
+
expect(requestUrl).toContain('filter=');
|
|
198
|
+
expect(stevePaginationUtils.convertLabelSelectorPaginationParams).toHaveBeenCalledWith({ labelSelector });
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('should return undefined and warn when the request fails', async() => {
|
|
202
|
+
const ctx = baseCtx();
|
|
203
|
+
|
|
204
|
+
ctx.dispatch.mockRejectedValue(new Error('network error'));
|
|
205
|
+
|
|
206
|
+
const result = await fetchResourceSummary.call({}, ctx, { type: 'pod', opt: { summaryField: 'metadata.state.name' } });
|
|
207
|
+
|
|
208
|
+
expect(result).toBeUndefined();
|
|
209
|
+
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('summary API request failed'), expect.any(Error));
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
});
|
package/plugins/steve/actions.js
CHANGED
|
@@ -10,6 +10,7 @@ import { NAMESPACE } from '@shell/config/types';
|
|
|
10
10
|
import { handleKubeApiHeaderWarnings } from '@shell/plugins/steve/header-warnings';
|
|
11
11
|
import { steveCleanForDownload } from '@shell/plugins/steve/resource-utils';
|
|
12
12
|
import paginationUtils from '@shell/utils/pagination-utils';
|
|
13
|
+
import stevePaginationUtils from '@shell/plugins/steve/steve-pagination-utils';
|
|
13
14
|
|
|
14
15
|
export default {
|
|
15
16
|
|
|
@@ -221,6 +222,101 @@ export default {
|
|
|
221
222
|
}
|
|
222
223
|
},
|
|
223
224
|
|
|
225
|
+
/**
|
|
226
|
+
* Fetch aggregated state counts for a resource type via the Steve summary API.
|
|
227
|
+
* Requires VAI (ui-sql-cache) to be enabled; returns undefined otherwise.
|
|
228
|
+
*
|
|
229
|
+
* Uses `summaryonly` by default so no resource data is returned.
|
|
230
|
+
*
|
|
231
|
+
* @param {string} type - Resource type (e.g. 'pod', 'service')
|
|
232
|
+
* @param {object} [opt] - Options object
|
|
233
|
+
* @param {string} opt.summaryField - Field to aggregate counts by.
|
|
234
|
+
* Must be a field indexed by the VAI cache (see StevePaginationUtils.VALID_FIELDS in steve-pagination-utils.ts)
|
|
235
|
+
* @param {string} [opt.namespace] - Namespace to scope the request to (only applies to namespaced resource types)
|
|
236
|
+
* @param {boolean} [opt.summaryOnly=true] - Omit resource data from the response (set to false to include data)
|
|
237
|
+
* @param {boolean} [opt.namespaceCounts] - Include per-namespace breakdowns in counts
|
|
238
|
+
* @param {PaginationParamFilter[]} [opt.filters] - Pre-built filters from PaginationParamFilter.createSingleField()
|
|
239
|
+
* @param {KubeLabelSelector} [opt.labelSelector] - Kube label selector to filter by (converted via convertLabelSelectorPaginationParams)
|
|
240
|
+
* @returns {Promise<{ count: number, summary: { property: string, counts: Record<string, { total: number, namespace?: Record<string, number> }> }[] | null } | undefined>}
|
|
241
|
+
*
|
|
242
|
+
* @example
|
|
243
|
+
* const result = await dispatch('fetchResourceSummary', {
|
|
244
|
+
* type: 'pod',
|
|
245
|
+
* opt: { summaryField: 'metadata.state.name', labelSelector: { matchExpressions: podMatchExpression } }
|
|
246
|
+
* });
|
|
247
|
+
* // result.summary[0].counts => { running: { total: 3 }, error: { total: 1 } }
|
|
248
|
+
*
|
|
249
|
+
* // With namespace breakdowns:
|
|
250
|
+
* const result = await dispatch('fetchResourceSummary', {
|
|
251
|
+
* type: 'pod',
|
|
252
|
+
* opt: { summaryField: 'metadata.state.name', namespaceCounts: true }
|
|
253
|
+
* });
|
|
254
|
+
* // result.summary[0].counts => { running: { total: 3, namespace: { default: 2, 'kube-system': 1 } } }
|
|
255
|
+
*/
|
|
256
|
+
async fetchResourceSummary({ getters, dispatch, rootGetters }, { type, opt = {} }) {
|
|
257
|
+
type = getters.normalizeType(type);
|
|
258
|
+
const schema = getters.schemaFor(type);
|
|
259
|
+
|
|
260
|
+
if (!schema) {
|
|
261
|
+
console.warn(`fetchResourceSummary: no schema found for type "${ type }"`); // eslint-disable-line no-console
|
|
262
|
+
|
|
263
|
+
return undefined;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (!paginationUtils.isSteveCacheEnabled({ rootGetters })) {
|
|
267
|
+
console.warn(`fetchResourceSummary: VAI is not enabled, summary API unavailable for type "${ type }"`); // eslint-disable-line no-console
|
|
268
|
+
|
|
269
|
+
return undefined;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (!opt.summaryField) {
|
|
273
|
+
console.warn(`fetchResourceSummary: summaryField is required and must be a string for type "${ type }"`); // eslint-disable-line no-console
|
|
274
|
+
|
|
275
|
+
return undefined;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
try {
|
|
279
|
+
const url = new URL(schema.links.collection, window.location.origin);
|
|
280
|
+
|
|
281
|
+
if (schema.attributes?.namespaced && opt.namespace) {
|
|
282
|
+
url.pathname += `/${ opt.namespace }`;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
url.searchParams.set('summary', opt.summaryField);
|
|
286
|
+
|
|
287
|
+
if (opt.summaryOnly !== false) {
|
|
288
|
+
url.searchParams.set('summaryonly', '');
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (opt.namespaceCounts) {
|
|
292
|
+
url.searchParams.set('summarynamespaced', '');
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (opt.filters?.length) {
|
|
296
|
+
const filterParams = new URLSearchParams(stevePaginationUtils.convertPaginationParams({ schema, filters: opt.filters }));
|
|
297
|
+
|
|
298
|
+
filterParams.forEach((v, k) => url.searchParams.append(k, v));
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (opt.labelSelector) {
|
|
302
|
+
const labelParams = new URLSearchParams(stevePaginationUtils.convertLabelSelectorPaginationParams({ labelSelector: opt.labelSelector }));
|
|
303
|
+
|
|
304
|
+
labelParams.forEach((v, k) => url.searchParams.append(k, v));
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const res = await dispatch('request', { opt: { url: url.pathname + url.search } });
|
|
308
|
+
|
|
309
|
+
return {
|
|
310
|
+
count: res.count ?? 0,
|
|
311
|
+
summary: res.summary || null
|
|
312
|
+
};
|
|
313
|
+
} catch (e) {
|
|
314
|
+
console.warn(`fetchResourceSummary: summary API request failed for type "${ type }"`, e); // eslint-disable-line no-console
|
|
315
|
+
|
|
316
|
+
return undefined;
|
|
317
|
+
}
|
|
318
|
+
},
|
|
319
|
+
|
|
224
320
|
promptRestore({ commit, state }, resources ) {
|
|
225
321
|
commit('action-menu/togglePromptRestore', resources, { root: true });
|
|
226
322
|
},
|
|
@@ -656,7 +656,7 @@ class StevePaginationUtils extends NamespaceProjectFilters {
|
|
|
656
656
|
* A lot of the requirements and details are taken directly from
|
|
657
657
|
* https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/
|
|
658
658
|
*/
|
|
659
|
-
|
|
659
|
+
convertLabelSelectorPaginationParams({ labelSelector }: { labelSelector: KubeLabelSelector}): string {
|
|
660
660
|
// Get a list of matchExpressions
|
|
661
661
|
const expressions: KubeLabelSelectorExpression[] = labelSelector.matchExpressions ? [...labelSelector.matchExpressions] : [];
|
|
662
662
|
|
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import {
|
|
3
|
-
|
|
2
|
+
import {
|
|
3
|
+
computed, defineComponent, nextTick, ref, useTemplateRef
|
|
4
|
+
} from 'vue';
|
|
5
|
+
import { mapGetters, useStore } from 'vuex';
|
|
6
|
+
import { useInSummary } from '@shell/components/TableOfContents/composables';
|
|
7
|
+
import { useI18n } from '@shell/composables/useI18n';
|
|
4
8
|
|
|
5
9
|
export default defineComponent({
|
|
10
|
+
name: 'Accordion',
|
|
11
|
+
|
|
6
12
|
props: {
|
|
7
13
|
title: {
|
|
8
14
|
type: String,
|
|
@@ -20,22 +26,59 @@ export default defineComponent({
|
|
|
20
26
|
}
|
|
21
27
|
},
|
|
22
28
|
|
|
29
|
+
setup(props) {
|
|
30
|
+
const store = useStore();
|
|
31
|
+
const { t } = useI18n(store);
|
|
32
|
+
const label = computed(() => props.titleKey && typeof t === 'function' ? t(props.titleKey) : props.title);
|
|
33
|
+
|
|
34
|
+
const isOpen = ref(props.openInitially);
|
|
35
|
+
const accordionSummarizedContainer = useTemplateRef<HTMLElement>('accordion-summarized-container');
|
|
36
|
+
|
|
37
|
+
const scrollTo = () => {
|
|
38
|
+
isOpen.value = true;
|
|
39
|
+
nextTick(() => {
|
|
40
|
+
accordionSummarizedContainer.value?.scrollIntoView();
|
|
41
|
+
});
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const { summary } = useInSummary({
|
|
45
|
+
scrollTo,
|
|
46
|
+
label,
|
|
47
|
+
elementRef: accordionSummarizedContainer,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
summary,
|
|
52
|
+
isOpen,
|
|
53
|
+
scrollTo,
|
|
54
|
+
};
|
|
55
|
+
},
|
|
56
|
+
|
|
23
57
|
data() {
|
|
24
|
-
return {
|
|
58
|
+
return {};
|
|
25
59
|
},
|
|
26
60
|
|
|
27
|
-
computed: {
|
|
61
|
+
computed: {
|
|
62
|
+
...mapGetters({ t: 'i18n/t' }),
|
|
63
|
+
|
|
64
|
+
displayTitle() {
|
|
65
|
+
return this.titleKey ? this.t(this.titleKey) : this.title;
|
|
66
|
+
},
|
|
67
|
+
},
|
|
28
68
|
|
|
29
69
|
methods: {
|
|
30
70
|
toggle() {
|
|
31
71
|
this.isOpen = !this.isOpen;
|
|
32
|
-
}
|
|
33
|
-
}
|
|
72
|
+
},
|
|
73
|
+
},
|
|
34
74
|
});
|
|
35
75
|
</script>
|
|
36
76
|
|
|
37
77
|
<template>
|
|
38
|
-
<div
|
|
78
|
+
<div
|
|
79
|
+
ref="accordion-summarized-container"
|
|
80
|
+
class="accordion-container"
|
|
81
|
+
>
|
|
39
82
|
<div
|
|
40
83
|
class="accordion-header"
|
|
41
84
|
data-testid="accordion-header"
|
|
@@ -51,7 +94,7 @@ export default defineComponent({
|
|
|
51
94
|
data-testid="accordion-title-slot-content"
|
|
52
95
|
class="mb-0"
|
|
53
96
|
>
|
|
54
|
-
{{
|
|
97
|
+
{{ displayTitle }}
|
|
55
98
|
</h2>
|
|
56
99
|
</slot>
|
|
57
100
|
</div>
|
|
@@ -67,7 +110,8 @@ export default defineComponent({
|
|
|
67
110
|
|
|
68
111
|
<style lang="scss" scoped>
|
|
69
112
|
.accordion-container {
|
|
70
|
-
border: 1px solid var(--border)
|
|
113
|
+
border: 1px solid var(--border);
|
|
114
|
+
border-radius: var(--border-radius);
|
|
71
115
|
}
|
|
72
116
|
.accordion-header {
|
|
73
117
|
padding: 16px 16px 16px 11px;
|