@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
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
import { getters, mutations, state } from '@shell/store/notifications';
|
|
2
|
+
import { NotificationLevel, Notification, StoredNotification } from '@shell/types/notifications';
|
|
3
|
+
|
|
4
|
+
jest.mock('@shell/utils/string', () => ({ randomStr: jest.fn(() => 'mock-random-id') }));
|
|
5
|
+
jest.mock('@shell/utils/crypto', () => ({ md5: jest.fn((v: string) => `hash-${ v }`) }));
|
|
6
|
+
jest.mock('@shell/utils/crypto/encryption', () => ({
|
|
7
|
+
encrypt: jest.fn(),
|
|
8
|
+
decrypt: jest.fn(),
|
|
9
|
+
deriveKey: jest.fn(),
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
function makeNotification(overrides: Partial<Notification> = {}): Notification {
|
|
13
|
+
return {
|
|
14
|
+
id: 'test-id',
|
|
15
|
+
title: 'test notification',
|
|
16
|
+
level: NotificationLevel.Info,
|
|
17
|
+
...overrides,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function makeStoredNotification(overrides: Partial<StoredNotification> = {}): StoredNotification {
|
|
22
|
+
return {
|
|
23
|
+
...makeNotification(),
|
|
24
|
+
read: false,
|
|
25
|
+
created: new Date('2026-01-01'),
|
|
26
|
+
...overrides,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe('notifications store', () => {
|
|
31
|
+
describe('state', () => {
|
|
32
|
+
it('returns initial state with empty notifications', () => {
|
|
33
|
+
const result = state();
|
|
34
|
+
|
|
35
|
+
expect(result.notifications).toStrictEqual([]);
|
|
36
|
+
expect(result.localStorageKey).toStrictEqual('');
|
|
37
|
+
expect(result.userId).toStrictEqual('');
|
|
38
|
+
expect(result.encryptionKey).toBeUndefined();
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe('getters', () => {
|
|
43
|
+
let mockState: ReturnType<typeof state>;
|
|
44
|
+
const hidden = makeStoredNotification({
|
|
45
|
+
id: 'h1', level: NotificationLevel.Hidden, read: false
|
|
46
|
+
});
|
|
47
|
+
const unreadVisible = makeStoredNotification({
|
|
48
|
+
id: 'u1', level: NotificationLevel.Info, read: false
|
|
49
|
+
});
|
|
50
|
+
const readVisible = makeStoredNotification({
|
|
51
|
+
id: 'r1', level: NotificationLevel.Warning, read: true
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
beforeEach(() => {
|
|
55
|
+
mockState = state();
|
|
56
|
+
mockState.notifications = [hidden, unreadVisible, readVisible];
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe('all', () => {
|
|
60
|
+
it('returns all notifications including hidden', () => {
|
|
61
|
+
expect(getters.all(mockState)).toStrictEqual([hidden, unreadVisible, readVisible]);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('returns empty array when there are no notifications', () => {
|
|
65
|
+
mockState.notifications = [];
|
|
66
|
+
expect(getters.all(mockState)).toStrictEqual([]);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe('visible', () => {
|
|
71
|
+
it('returns only non-hidden notifications', () => {
|
|
72
|
+
expect(getters.visible(mockState)).toStrictEqual([unreadVisible, readVisible]);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('returns empty array when all notifications are hidden', () => {
|
|
76
|
+
mockState.notifications = [hidden];
|
|
77
|
+
expect(getters.visible(mockState)).toStrictEqual([]);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe('hidden', () => {
|
|
82
|
+
it('returns only hidden notifications', () => {
|
|
83
|
+
expect(getters.hidden(mockState)).toStrictEqual([hidden]);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('returns empty array when no notifications are hidden', () => {
|
|
87
|
+
mockState.notifications = [unreadVisible, readVisible];
|
|
88
|
+
expect(getters.hidden(mockState)).toStrictEqual([]);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe('item', () => {
|
|
93
|
+
it('returns a function that finds a notification by id', () => {
|
|
94
|
+
const itemFn = getters.item(mockState);
|
|
95
|
+
|
|
96
|
+
expect(itemFn('u1')).toStrictEqual(unreadVisible);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('returns undefined when the notification id does not exist', () => {
|
|
100
|
+
const itemFn = getters.item(mockState);
|
|
101
|
+
|
|
102
|
+
expect(itemFn('nonexistent')).toBeUndefined();
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
describe('unreadCount', () => {
|
|
107
|
+
it('counts only unread visible notifications', () => {
|
|
108
|
+
expect(getters.unreadCount(mockState)).toStrictEqual(1);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('returns zero when all visible notifications are read', () => {
|
|
112
|
+
mockState.notifications = [readVisible];
|
|
113
|
+
expect(getters.unreadCount(mockState)).toStrictEqual(0);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('excludes hidden notifications from the unread count', () => {
|
|
117
|
+
mockState.notifications = [hidden]; // hidden and unread
|
|
118
|
+
expect(getters.unreadCount(mockState)).toStrictEqual(0);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
describe('localStorageKey', () => {
|
|
123
|
+
it('returns the localStorageKey from state', () => {
|
|
124
|
+
mockState.localStorageKey = 'rancher-notifications-abc';
|
|
125
|
+
expect(getters.localStorageKey(mockState)).toStrictEqual('rancher-notifications-abc');
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
describe('userId', () => {
|
|
130
|
+
it('returns the userId from state', () => {
|
|
131
|
+
mockState.userId = 'user-123';
|
|
132
|
+
expect(getters.userId(mockState)).toStrictEqual('user-123');
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
describe('encryptionKey', () => {
|
|
137
|
+
it('returns undefined by default', () => {
|
|
138
|
+
expect(getters.encryptionKey(mockState)).toBeUndefined();
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('returns the encryptionKey when set', () => {
|
|
142
|
+
const mockKey = {} as CryptoKey;
|
|
143
|
+
|
|
144
|
+
mockState.encryptionKey = mockKey;
|
|
145
|
+
expect(getters.encryptionKey(mockState)).toStrictEqual(mockKey);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
describe('mutations', () => {
|
|
151
|
+
let mockState: ReturnType<typeof state>;
|
|
152
|
+
|
|
153
|
+
beforeEach(() => {
|
|
154
|
+
mockState = state();
|
|
155
|
+
mockState.localStorageKey = 'rancher-notifications-test';
|
|
156
|
+
localStorage.clear();
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
describe('localStorageKey', () => {
|
|
160
|
+
it('sets localStorageKey with the store prefix', () => {
|
|
161
|
+
mutations.localStorageKey(mockState, 'abc123');
|
|
162
|
+
expect(mockState.localStorageKey).toStrictEqual('rancher-notifications-abc123');
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
describe('userId', () => {
|
|
167
|
+
it('sets the userId on state', () => {
|
|
168
|
+
mutations.userId(mockState, 'user-456');
|
|
169
|
+
expect(mockState.userId).toStrictEqual('user-456');
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
describe('encryptionKey', () => {
|
|
174
|
+
it('sets the encryptionKey on state', () => {
|
|
175
|
+
const mockKey = {} as CryptoKey;
|
|
176
|
+
|
|
177
|
+
mutations.encryptionKey(mockState, mockKey);
|
|
178
|
+
expect(mockState.encryptionKey).toStrictEqual(mockKey);
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
describe('load', () => {
|
|
183
|
+
it('replaces all notifications with the provided list', () => {
|
|
184
|
+
mockState.notifications = [makeStoredNotification({ id: 'old' })];
|
|
185
|
+
const newNotifications = [makeStoredNotification({ id: 'n1' }), makeStoredNotification({ id: 'n2' })];
|
|
186
|
+
|
|
187
|
+
mutations.load(mockState, newNotifications);
|
|
188
|
+
expect(mockState.notifications).toStrictEqual(newNotifications);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('clears notifications when given an empty array', () => {
|
|
192
|
+
mockState.notifications = [makeStoredNotification({ id: 'old' })];
|
|
193
|
+
mutations.load(mockState, []);
|
|
194
|
+
expect(mockState.notifications).toStrictEqual([]);
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
describe('add', () => {
|
|
199
|
+
it('adds a notification with the provided id to the front of the list', () => {
|
|
200
|
+
mutations.add(mockState, makeNotification({ id: 'custom-id' }));
|
|
201
|
+
expect(mockState.notifications[0].id).toStrictEqual('custom-id');
|
|
202
|
+
expect(mockState.notifications).toHaveLength(1);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('generates an id via randomStr when none is provided', () => {
|
|
206
|
+
mutations.add(mockState, makeNotification({ id: '' as any }));
|
|
207
|
+
expect(mockState.notifications[0].id).toStrictEqual('mock-random-id');
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('sets read to false on the newly added notification', () => {
|
|
211
|
+
mutations.add(mockState, makeNotification({ id: 'n1' }));
|
|
212
|
+
expect(mockState.notifications[0].read).toStrictEqual(false);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it('prepends the new notification before existing ones', () => {
|
|
216
|
+
mockState.notifications = [makeStoredNotification({ id: 'existing' })];
|
|
217
|
+
mutations.add(mockState, makeNotification({ id: 'new' }));
|
|
218
|
+
expect(mockState.notifications[0].id).toStrictEqual('new');
|
|
219
|
+
expect(mockState.notifications[1].id).toStrictEqual('existing');
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('does not add a notification whose id already exists', () => {
|
|
223
|
+
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
|
|
224
|
+
|
|
225
|
+
mockState.notifications = [makeStoredNotification({ id: 'dup' })];
|
|
226
|
+
mutations.add(mockState, makeNotification({ id: 'dup' }));
|
|
227
|
+
expect(mockState.notifications).toHaveLength(1);
|
|
228
|
+
consoleErrorSpy.mockRestore();
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it('enforces the maximum of 50 notifications by removing the oldest', () => {
|
|
232
|
+
for (let i = 0; i < 50; i++) {
|
|
233
|
+
mockState.notifications.push(makeStoredNotification({ id: `n${ i }` }));
|
|
234
|
+
}
|
|
235
|
+
mutations.add(mockState, makeNotification({ id: 'newest' }));
|
|
236
|
+
expect(mockState.notifications).toHaveLength(50);
|
|
237
|
+
expect(mockState.notifications[0].id).toStrictEqual('newest');
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it('removes the oldest notification from localStorage when the limit is exceeded', () => {
|
|
241
|
+
const removeItemSpy = jest.spyOn(Storage.prototype, 'removeItem');
|
|
242
|
+
|
|
243
|
+
for (let i = 0; i < 50; i++) {
|
|
244
|
+
mockState.notifications.push(makeStoredNotification({ id: `n${ i }` }));
|
|
245
|
+
}
|
|
246
|
+
mutations.add(mockState, makeNotification({ id: 'newest' }));
|
|
247
|
+
expect(removeItemSpy).toHaveBeenCalledWith('rancher-notifications-test-n49');
|
|
248
|
+
removeItemSpy.mockRestore();
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it('syncs the notifications index to localStorage after adding', () => {
|
|
252
|
+
const setItemSpy = jest.spyOn(Storage.prototype, 'setItem');
|
|
253
|
+
|
|
254
|
+
mutations.add(mockState, makeNotification({ id: 'n1' }));
|
|
255
|
+
expect(setItemSpy).toHaveBeenCalledWith('rancher-notifications-test', expect.any(String));
|
|
256
|
+
setItemSpy.mockRestore();
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it('stores id, created, read, and progress fields in the localStorage index', () => {
|
|
260
|
+
mutations.add(mockState, makeNotification({ id: 'n1', progress: 42 }));
|
|
261
|
+
const stored = JSON.parse(localStorage.getItem('rancher-notifications-test') || '[]');
|
|
262
|
+
|
|
263
|
+
expect(stored[0]).toStrictEqual({
|
|
264
|
+
id: 'n1',
|
|
265
|
+
created: expect.any(String),
|
|
266
|
+
read: false,
|
|
267
|
+
progress: 42,
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
describe('markRead', () => {
|
|
273
|
+
it('marks a notification as read', () => {
|
|
274
|
+
mockState.notifications = [makeStoredNotification({ id: 'n1', read: false })];
|
|
275
|
+
mutations.markRead(mockState, 'n1');
|
|
276
|
+
expect(mockState.notifications[0].read).toStrictEqual(true);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it('does not modify state when the notification is not found', () => {
|
|
280
|
+
mockState.notifications = [makeStoredNotification({ id: 'n1', read: false })];
|
|
281
|
+
mutations.markRead(mockState, 'nonexistent');
|
|
282
|
+
expect(mockState.notifications[0].read).toStrictEqual(false);
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it('does not change a notification that is already read', () => {
|
|
286
|
+
mockState.notifications = [makeStoredNotification({ id: 'n1', read: true })];
|
|
287
|
+
mutations.markRead(mockState, 'n1');
|
|
288
|
+
expect(mockState.notifications[0].read).toStrictEqual(true);
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
describe('markUnread', () => {
|
|
293
|
+
it('marks a notification as unread', () => {
|
|
294
|
+
mockState.notifications = [makeStoredNotification({ id: 'n1', read: true })];
|
|
295
|
+
mutations.markUnread(mockState, 'n1');
|
|
296
|
+
expect(mockState.notifications[0].read).toStrictEqual(false);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it('does not modify state when the notification is not found', () => {
|
|
300
|
+
mockState.notifications = [makeStoredNotification({ id: 'n1', read: true })];
|
|
301
|
+
mutations.markUnread(mockState, 'nonexistent');
|
|
302
|
+
expect(mockState.notifications[0].read).toStrictEqual(true);
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it('does not change a notification that is already unread', () => {
|
|
306
|
+
mockState.notifications = [makeStoredNotification({ id: 'n1', read: false })];
|
|
307
|
+
mutations.markUnread(mockState, 'n1');
|
|
308
|
+
expect(mockState.notifications[0].read).toStrictEqual(false);
|
|
309
|
+
});
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
describe('markAllRead', () => {
|
|
313
|
+
it('marks all visible unread notifications as read', () => {
|
|
314
|
+
mockState.notifications = [
|
|
315
|
+
makeStoredNotification({
|
|
316
|
+
id: 'n1', level: NotificationLevel.Info, read: false
|
|
317
|
+
}),
|
|
318
|
+
makeStoredNotification({
|
|
319
|
+
id: 'n2', level: NotificationLevel.Warning, read: false
|
|
320
|
+
}),
|
|
321
|
+
];
|
|
322
|
+
mutations.markAllRead(mockState);
|
|
323
|
+
expect(mockState.notifications[0].read).toStrictEqual(true);
|
|
324
|
+
expect(mockState.notifications[1].read).toStrictEqual(true);
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it('does not mark hidden notifications as read', () => {
|
|
328
|
+
mockState.notifications = [
|
|
329
|
+
makeStoredNotification({
|
|
330
|
+
id: 'h1', level: NotificationLevel.Hidden, read: false
|
|
331
|
+
}),
|
|
332
|
+
];
|
|
333
|
+
mutations.markAllRead(mockState);
|
|
334
|
+
expect(mockState.notifications[0].read).toStrictEqual(false);
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
it('leaves already-read visible notifications unchanged', () => {
|
|
338
|
+
mockState.notifications = [
|
|
339
|
+
makeStoredNotification({
|
|
340
|
+
id: 'n1', level: NotificationLevel.Info, read: true
|
|
341
|
+
}),
|
|
342
|
+
];
|
|
343
|
+
mutations.markAllRead(mockState);
|
|
344
|
+
expect(mockState.notifications[0].read).toStrictEqual(true);
|
|
345
|
+
});
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
describe('update', () => {
|
|
349
|
+
it('updates notification fields by id', () => {
|
|
350
|
+
mockState.notifications = [makeStoredNotification({ id: 'n1', title: 'original' })];
|
|
351
|
+
mutations.update(mockState, { id: 'n1', title: 'updated' } as any);
|
|
352
|
+
expect(mockState.notifications[0].title).toStrictEqual('updated');
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
it('preserves fields not included in the update', () => {
|
|
356
|
+
mockState.notifications = [makeStoredNotification({
|
|
357
|
+
id: 'n1',
|
|
358
|
+
title: 'original',
|
|
359
|
+
level: NotificationLevel.Success,
|
|
360
|
+
})];
|
|
361
|
+
mutations.update(mockState, { id: 'n1', title: 'updated' } as any);
|
|
362
|
+
expect(mockState.notifications[0].level).toStrictEqual(NotificationLevel.Success);
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
it('does not modify state when the id is not found', () => {
|
|
366
|
+
mockState.notifications = [makeStoredNotification({ id: 'n1', title: 'original' })];
|
|
367
|
+
mutations.update(mockState, { id: 'nonexistent', title: 'updated' } as any);
|
|
368
|
+
expect(mockState.notifications[0].title).toStrictEqual('original');
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
it('does nothing when no id is provided', () => {
|
|
372
|
+
mockState.notifications = [makeStoredNotification({ id: 'n1', title: 'original' })];
|
|
373
|
+
mutations.update(mockState, {} as any);
|
|
374
|
+
expect(mockState.notifications[0].title).toStrictEqual('original');
|
|
375
|
+
});
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
describe('remove', () => {
|
|
379
|
+
it('removes the notification with the given id', () => {
|
|
380
|
+
mockState.notifications = [
|
|
381
|
+
makeStoredNotification({ id: 'n1' }),
|
|
382
|
+
makeStoredNotification({ id: 'n2' }),
|
|
383
|
+
];
|
|
384
|
+
mutations.remove(mockState, 'n1');
|
|
385
|
+
expect(mockState.notifications).toHaveLength(1);
|
|
386
|
+
expect(mockState.notifications[0].id).toStrictEqual('n2');
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
it('removes the encrypted notification entry from localStorage', () => {
|
|
390
|
+
const removeItemSpy = jest.spyOn(Storage.prototype, 'removeItem');
|
|
391
|
+
|
|
392
|
+
mockState.notifications = [makeStoredNotification({ id: 'n1' })];
|
|
393
|
+
mutations.remove(mockState, 'n1');
|
|
394
|
+
expect(removeItemSpy).toHaveBeenCalledWith('rancher-notifications-test-n1');
|
|
395
|
+
removeItemSpy.mockRestore();
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
it('does not change state when the id is not found', () => {
|
|
399
|
+
mockState.notifications = [makeStoredNotification({ id: 'n1' })];
|
|
400
|
+
mutations.remove(mockState, 'nonexistent');
|
|
401
|
+
expect(mockState.notifications).toHaveLength(1);
|
|
402
|
+
});
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
describe('clearAll', () => {
|
|
406
|
+
it('removes all notifications from state', () => {
|
|
407
|
+
mockState.notifications = [
|
|
408
|
+
makeStoredNotification({ id: 'n1' }),
|
|
409
|
+
makeStoredNotification({ id: 'n2' }),
|
|
410
|
+
];
|
|
411
|
+
mutations.clearAll(mockState);
|
|
412
|
+
expect(mockState.notifications).toStrictEqual([]);
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
it('removes each notification encrypted entry from localStorage', () => {
|
|
416
|
+
const removeItemSpy = jest.spyOn(Storage.prototype, 'removeItem');
|
|
417
|
+
|
|
418
|
+
mockState.notifications = [
|
|
419
|
+
makeStoredNotification({ id: 'n1' }),
|
|
420
|
+
makeStoredNotification({ id: 'n2' }),
|
|
421
|
+
];
|
|
422
|
+
mutations.clearAll(mockState);
|
|
423
|
+
expect(removeItemSpy).toHaveBeenCalledWith('rancher-notifications-test-n1');
|
|
424
|
+
expect(removeItemSpy).toHaveBeenCalledWith('rancher-notifications-test-n2');
|
|
425
|
+
removeItemSpy.mockRestore();
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
it('does nothing when there are no notifications', () => {
|
|
429
|
+
mutations.clearAll(mockState);
|
|
430
|
+
expect(mockState.notifications).toStrictEqual([]);
|
|
431
|
+
});
|
|
432
|
+
});
|
|
433
|
+
});
|
|
434
|
+
});
|
package/store/catalog.js
CHANGED
|
@@ -302,6 +302,16 @@ export const mutations = {
|
|
|
302
302
|
state.namespacedRepos = namespaced;
|
|
303
303
|
},
|
|
304
304
|
|
|
305
|
+
addClusterRepo(state, repo) {
|
|
306
|
+
if (!state.clusterRepos) {
|
|
307
|
+
state.clusterRepos = [];
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (!state.clusterRepos.find((r) => r.metadata?.name === repo.metadata?.name)) {
|
|
311
|
+
state.clusterRepos.push(repo);
|
|
312
|
+
}
|
|
313
|
+
},
|
|
314
|
+
|
|
305
315
|
setCharts(state, { charts, errors = [], loaded = [] }) {
|
|
306
316
|
state.charts = charts;
|
|
307
317
|
state.errors = errors;
|
|
@@ -445,6 +455,53 @@ export const actions = {
|
|
|
445
455
|
}
|
|
446
456
|
},
|
|
447
457
|
|
|
458
|
+
async loadRepo(ctx, { repoName }) {
|
|
459
|
+
const {
|
|
460
|
+
state, getters, rootGetters, commit, dispatch
|
|
461
|
+
} = ctx;
|
|
462
|
+
|
|
463
|
+
const inStore = rootGetters['currentCluster'] ? rootGetters['currentProduct'].inStore : 'management';
|
|
464
|
+
|
|
465
|
+
let repo = rootGetters[`${ inStore }/byId`](CATALOG.CLUSTER_REPO, repoName);
|
|
466
|
+
|
|
467
|
+
if (!repo) {
|
|
468
|
+
try {
|
|
469
|
+
repo = await dispatch(`${ inStore }/find`, { type: CATALOG.CLUSTER_REPO, id: repoName }, { root: true });
|
|
470
|
+
} catch (e) {
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
if (!repo) {
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
commit('addClusterRepo', repo);
|
|
480
|
+
|
|
481
|
+
if (getters.isLoaded(repo)) {
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
try {
|
|
486
|
+
const index = await repo.followLink('index');
|
|
487
|
+
const charts = { ...state.charts };
|
|
488
|
+
|
|
489
|
+
for (const k in index?.entries) {
|
|
490
|
+
for (const entry of index.entries[k]) {
|
|
491
|
+
addChart(ctx, charts, entry, repo);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
commit('setCharts', {
|
|
496
|
+
charts,
|
|
497
|
+
errors: state.errors,
|
|
498
|
+
loaded: [repo],
|
|
499
|
+
});
|
|
500
|
+
} catch (e) {
|
|
501
|
+
console.error(`Failed to load repo ${ repoName }:`, e); // eslint-disable-line no-console
|
|
502
|
+
}
|
|
503
|
+
},
|
|
504
|
+
|
|
448
505
|
/**
|
|
449
506
|
* Globally refreshes all loaded repositories by triggering their refresh actions concurrently,
|
|
450
507
|
* bypassing individual catalog loads, and then performs a single, global catalog/load.
|
package/store/plugins.js
CHANGED
|
@@ -172,14 +172,14 @@ export const getters = {
|
|
|
172
172
|
return async(name) => {
|
|
173
173
|
const schema = getters.schemaForDriver(name);
|
|
174
174
|
|
|
175
|
-
await schema.fetchResourceFields();
|
|
176
|
-
|
|
177
175
|
if ( !schema ) {
|
|
178
176
|
// eslint-disable-next-line no-console
|
|
179
177
|
console.error(`Machine Driver Config schema not found for ${ name }`);
|
|
180
178
|
|
|
181
179
|
return [];
|
|
182
180
|
}
|
|
181
|
+
await schema.fetchResourceFields();
|
|
182
|
+
|
|
183
183
|
// This is used in places where `createPopulated` has been called, which has called fetchResourceFields to populate resourceFields
|
|
184
184
|
const out = Object.keys(schema?.resourceFields || {});
|
|
185
185
|
|
|
@@ -191,13 +191,16 @@ export const getters = {
|
|
|
191
191
|
|
|
192
192
|
fieldsForDriver(state, getters) {
|
|
193
193
|
return async(name) => {
|
|
194
|
+
const out = {};
|
|
194
195
|
const schema = getters.schemaForDriver(name);
|
|
195
196
|
|
|
197
|
+
if ( !schema ) {
|
|
198
|
+
return out;
|
|
199
|
+
}
|
|
200
|
+
|
|
196
201
|
await schema.fetchResourceFields();
|
|
197
202
|
const names = await getters.fieldNamesForDriver(name);
|
|
198
203
|
|
|
199
|
-
const out = {};
|
|
200
|
-
|
|
201
204
|
for ( const n of names ) {
|
|
202
205
|
out[n] = schema.resourceFields[n];
|
|
203
206
|
}
|