@rancher/shell 3.0.7 → 3.0.8-rc.10
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/brand/suse/banner.svg +1 -0
- package/assets/brand/suse/dark/banner.svg +1 -0
- package/assets/brand/suse/dark/login-landscape.svg +1 -0
- package/assets/brand/suse/dark/rancher-logo.svg +1 -1
- package/assets/brand/suse/favicon.png +0 -0
- package/assets/brand/suse/login-landscape.svg +1 -0
- package/assets/brand/suse/metadata.json +11 -1
- package/assets/brand/suse/rancher-logo.svg +1 -1
- package/assets/fonts/suse/suse-v2-latin-300.woff +0 -0
- package/assets/fonts/suse/suse-v2-latin-300.woff2 +0 -0
- package/assets/fonts/suse/suse-v2-latin-600.woff +0 -0
- package/assets/fonts/suse/suse-v2-latin-600.woff2 +0 -0
- package/assets/fonts/suse/suse-v2-latin-700.woff +0 -0
- package/assets/fonts/suse/suse-v2-latin-700.woff2 +0 -0
- package/assets/fonts/suse/suse-v2-latin-800.woff +0 -0
- package/assets/fonts/suse/suse-v2-latin-800.woff2 +0 -0
- package/assets/fonts/suse/suse-v2-latin-regular.woff +0 -0
- package/assets/fonts/suse/suse-v2-latin-regular.woff2 +0 -0
- package/assets/images/content/README.md +5 -0
- package/assets/images/content/cloud-native.svg +84 -0
- package/assets/images/content/dark/cloud-native.svg +21 -0
- package/assets/images/content/dark/shield.svg +59 -0
- package/assets/images/content/dark/suse.svg +10 -0
- package/assets/images/content/shield.svg +59 -0
- package/assets/images/content/suse.svg +10 -0
- package/assets/images/vendor/githubapp.svg +13 -0
- package/assets/styles/base/_typography.scss +2 -1
- package/assets/styles/fonts/_fontstack.scss +53 -1
- package/assets/styles/global/_cards.scss +0 -3
- package/assets/styles/global/_layout.scss +21 -35
- package/assets/styles/themes/_dark.scss +1 -1
- package/assets/styles/themes/_light.scss +1 -1
- package/assets/styles/themes/_modern.scss +16 -8
- package/assets/styles/themes/_suse.scss +116 -24
- package/assets/translations/en-us.yaml +185 -21
- package/assets/translations/zh-hans.yaml +0 -4
- package/components/AutoscalerCard.vue +113 -0
- package/components/AutoscalerTab.vue +94 -0
- package/components/BackLink.vue +8 -0
- package/components/BannerGraphic.vue +36 -21
- package/components/BrandImage.vue +17 -6
- package/components/ClusterIconMenu.vue +1 -1
- package/components/ClusterProviderIcon.vue +1 -1
- package/components/Cron/CronExpressionEditor.vue +1 -1
- package/components/Cron/CronExpressionEditorModal.vue +1 -1
- package/components/Drawer/Chrome.vue +2 -6
- package/components/Drawer/ResourceDetailDrawer/ConfigTab.vue +4 -9
- package/components/Drawer/ResourceDetailDrawer/YamlTab.vue +3 -8
- package/components/Drawer/ResourceDetailDrawer/composables.ts +3 -4
- package/components/Drawer/ResourceDetailDrawer/index.vue +4 -9
- package/components/Drawer/ResourceDetailDrawer/types.ts +17 -0
- package/components/Drawer/types.ts +3 -0
- package/components/DynamicContent/DynamicContentBanner.vue +102 -0
- package/components/DynamicContent/DynamicContentCloseButton.vue +42 -0
- package/components/DynamicContent/DynamicContentIcon.vue +132 -0
- package/components/DynamicContent/DynamicContentPanel.vue +112 -0
- package/components/DynamicContent/content.ts +78 -0
- package/components/EmberPage.vue +1 -1
- package/components/IconOrSvg.vue +2 -2
- package/components/Inactivity.vue +222 -106
- package/components/InstallHelmCharts.vue +2 -2
- package/components/PaginatedResourceTable.vue +2 -6
- package/components/PopoverCard.vue +192 -0
- package/components/Questions/__tests__/index.test.ts +159 -0
- package/components/Resource/Detail/CopyToClipboard.vue +4 -1
- package/components/Resource/Detail/FetchLoader/composables.ts +18 -4
- package/components/Resource/Detail/Metadata/Annotations/index.vue +2 -2
- package/components/Resource/Detail/Metadata/IdentifyingInformation/__tests__/identifying-fields.test.ts +1 -1
- package/components/Resource/Detail/Metadata/IdentifyingInformation/identifying-fields.ts +4 -0
- package/components/Resource/Detail/Metadata/KeyValueRow.vue +1 -1
- package/components/Resource/Detail/Metadata/Labels/index.vue +2 -2
- package/components/Resource/Detail/Metadata/composables.ts +9 -9
- package/components/Resource/Detail/Metadata/index.vue +3 -3
- package/components/Resource/Detail/ResourcePopover/ResourcePopoverCard.vue +2 -19
- package/components/Resource/Detail/ResourcePopover/__tests__/ResourcePopoverCard.test.ts +0 -29
- package/components/Resource/Detail/ResourcePopover/__tests__/index.test.ts +132 -150
- package/components/Resource/Detail/ResourcePopover/index.vue +54 -159
- package/components/Resource/Detail/TitleBar/__tests__/index.test.ts +0 -2
- package/components/Resource/Detail/TitleBar/composables.ts +2 -1
- package/components/Resource/Detail/TitleBar/index.vue +10 -6
- package/components/Resource/Detail/composables.ts +12 -0
- package/components/ResourceDetail/Masthead/latest.vue +29 -0
- package/components/ResourceDetail/index.vue +5 -2
- package/components/ResourceList/Masthead.vue +1 -1
- package/components/SortableTable/index.vue +18 -2
- package/components/Tabbed/__tests__/index.test.ts +86 -0
- package/components/{nav/WindowManager → Window}/ContainerLogs.vue +1 -1
- package/components/{nav/WindowManager → Window}/ContainerLogsActions.vue +1 -0
- package/components/{nav/WindowManager → Window}/__tests__/ContainerLogs.test.ts +1 -1
- package/components/{nav/WindowManager → Window}/__tests__/ContainerShell.test.ts +2 -2
- package/components/__tests__/AutoscalerCard.test.ts +154 -0
- package/components/__tests__/AutoscalerTab.test.ts +125 -0
- package/components/__tests__/PopoverCard.test.ts +204 -0
- package/components/auth/SelectPrincipal.vue +24 -6
- package/components/auth/__tests__/SelectPrincipal.test.ts +119 -0
- package/components/fleet/FleetConfigMapSelector.vue +117 -0
- package/components/fleet/FleetSecretSelector.vue +127 -0
- package/components/fleet/__tests__/FleetConfigMapSelector.test.ts +125 -0
- package/components/fleet/__tests__/FleetSecretSelector.test.ts +82 -0
- package/components/form/FileImageSelector.vue +13 -4
- package/components/form/FileSelector.vue +11 -2
- package/components/form/ResourceLabeledSelect.vue +1 -0
- package/components/form/__tests__/ResourceLabeledSelect.test.ts +90 -0
- package/components/formatter/Autoscaler.vue +97 -0
- package/components/formatter/InternalExternalIP.vue +198 -24
- package/components/formatter/__tests__/Autoscaler.test.ts +156 -0
- package/components/formatter/__tests__/InternalExternalIP.test.ts +133 -0
- package/components/google/util/__tests__/formatter.test.ts +47 -0
- package/components/google/util/formatter.ts +5 -2
- package/components/nav/Group.vue +12 -3
- package/components/nav/Header.vue +37 -16
- package/components/nav/NamespaceFilter.vue +13 -1
- package/components/nav/NotificationCenter/index.vue +2 -1
- package/components/nav/TopLevelMenu.helper.ts +16 -6
- package/components/nav/TopLevelMenu.vue +4 -2
- package/components/{DraggableZone.vue → nav/WindowManager/PinArea.vue} +47 -80
- package/components/nav/WindowManager/composables/useComponentsMount.ts +70 -0
- package/components/nav/WindowManager/composables/useDimensionsHandler.ts +105 -0
- package/components/nav/WindowManager/composables/useDragHandler.ts +99 -0
- package/components/nav/WindowManager/composables/usePanelHandler.ts +72 -0
- package/components/nav/WindowManager/composables/usePanelsHandler.ts +14 -0
- package/components/nav/WindowManager/composables/useResizeHandler.ts +167 -0
- package/components/nav/WindowManager/composables/useTabsHandler.ts +51 -0
- package/components/nav/WindowManager/constants.ts +23 -0
- package/components/nav/WindowManager/index.vue +61 -575
- package/components/nav/WindowManager/panels/HorizontalPanel.vue +265 -0
- package/components/nav/WindowManager/panels/TabBodyContainer.vue +39 -0
- package/components/nav/WindowManager/panels/VerticalPanel.vue +308 -0
- package/components/templates/default.vue +4 -40
- package/components/templates/home.vue +31 -5
- package/components/templates/plain.vue +30 -4
- package/components/templates/standalone.vue +1 -1
- package/composables/useI18n.ts +10 -1
- package/composables/useInterval.ts +15 -0
- package/config/__test__/uiplugins.test.ts +309 -0
- package/config/labels-annotations.js +9 -1
- package/config/product/auth.js +1 -0
- package/config/product/explorer.js +3 -1
- package/config/product/manager.js +20 -9
- package/config/query-params.js +1 -0
- package/config/router/routes.js +10 -2
- package/config/settings.ts +10 -2
- package/config/store.js +4 -2
- package/config/table-headers.js +8 -0
- package/config/types.js +11 -0
- package/config/uiplugins.js +46 -2
- package/config/version.js +1 -1
- package/core/__test__/extension-manager-impl.test.js +236 -0
- package/core/extension-manager-impl.js +23 -6
- package/core/plugin-helpers.ts +2 -0
- package/core/types-provisioning.ts +4 -1
- package/detail/pod.vue +1 -0
- package/detail/provisioning.cattle.io.cluster.vue +13 -1
- package/dialog/AddonConfigConfirmationDialog.vue +45 -1
- package/dialog/DeveloperLoadExtensionDialog.vue +12 -3
- package/dialog/RollbackWorkloadDialog.vue +2 -5
- package/directives/ui-context.ts +103 -0
- package/edit/__tests__/fleet.cattle.io.helmop.test.ts +52 -11
- package/edit/auth/AuthProviderWarningBanners.vue +14 -1
- package/edit/auth/__tests__/oidc.test.ts +26 -0
- package/edit/auth/github-app-steps.vue +97 -0
- package/edit/auth/github-steps.vue +75 -0
- package/edit/auth/github.vue +99 -65
- package/edit/auth/oidc.vue +5 -1
- package/edit/autoscaling.horizontalpodautoscaler/index.vue +1 -0
- package/edit/cloudcredential.vue +1 -1
- package/edit/configmap.vue +1 -0
- package/edit/constraints.gatekeeper.sh.constraint/index.vue +1 -0
- package/edit/fleet.cattle.io.gitrepo.vue +0 -10
- package/edit/fleet.cattle.io.helmop.vue +51 -2
- package/edit/helm.cattle.io.projecthelmchart.vue +1 -0
- package/edit/k8s.cni.cncf.io.networkattachmentdefinition.vue +1 -0
- package/edit/logging-flow/index.vue +1 -0
- package/edit/logging.banzaicloud.io.output/index.vue +1 -0
- package/edit/management.cattle.io.fleetworkspace.vue +1 -1
- package/edit/management.cattle.io.project.vue +1 -0
- package/edit/monitoring.coreos.com.alertmanagerconfig/index.vue +4 -1
- package/edit/monitoring.coreos.com.alertmanagerconfig/receiverConfig.vue +2 -1
- package/edit/monitoring.coreos.com.prometheusrule/index.vue +1 -0
- package/edit/monitoring.coreos.com.receiver/index.vue +2 -1
- package/edit/monitoring.coreos.com.route.vue +1 -1
- package/edit/namespace.vue +1 -0
- package/edit/networking.istio.io.destinationrule/index.vue +1 -0
- package/edit/networking.k8s.io.ingress/index.vue +1 -0
- package/edit/networking.k8s.io.networkpolicy/PolicyRuleTarget.vue +15 -5
- package/edit/networking.k8s.io.networkpolicy/PolicyRules.vue +1 -0
- package/edit/networking.k8s.io.networkpolicy/index.vue +1 -0
- package/edit/node.vue +1 -0
- package/edit/persistentvolume/index.vue +27 -22
- package/edit/persistentvolume/plugins/awsElasticBlockStore.vue +13 -14
- package/edit/persistentvolume/plugins/azureDisk.vue +49 -48
- package/edit/persistentvolume/plugins/azureFile.vue +15 -14
- package/edit/persistentvolume/plugins/cephfs.vue +15 -14
- package/edit/persistentvolume/plugins/cinder.vue +15 -14
- package/edit/persistentvolume/plugins/csi.vue +18 -16
- package/edit/persistentvolume/plugins/fc.vue +13 -14
- package/edit/persistentvolume/plugins/flexVolume.vue +15 -14
- package/edit/persistentvolume/plugins/flocker.vue +1 -3
- package/edit/persistentvolume/plugins/gcePersistentDisk.vue +13 -14
- package/edit/persistentvolume/plugins/glusterfs.vue +15 -14
- package/edit/persistentvolume/plugins/hostPath.vue +40 -39
- package/edit/persistentvolume/plugins/iscsi.vue +13 -14
- package/edit/persistentvolume/plugins/local.vue +1 -3
- package/edit/persistentvolume/plugins/longhorn.vue +23 -22
- package/edit/persistentvolume/plugins/nfs.vue +15 -14
- package/edit/persistentvolume/plugins/photonPersistentDisk.vue +1 -14
- package/edit/persistentvolume/plugins/portworxVolume.vue +15 -14
- package/edit/persistentvolume/plugins/quobyte.vue +15 -14
- package/edit/persistentvolume/plugins/rbd.vue +15 -14
- package/edit/persistentvolume/plugins/scaleIO.vue +15 -14
- package/edit/persistentvolume/plugins/storageos.vue +15 -14
- package/edit/persistentvolume/plugins/vsphereVolume.vue +1 -3
- package/edit/provisioning.cattle.io.cluster/CustomCommand.vue +32 -5
- package/edit/provisioning.cattle.io.cluster/__tests__/CustomCommand.test.ts +35 -0
- package/edit/provisioning.cattle.io.cluster/__tests__/Networking.test.ts +155 -0
- package/edit/provisioning.cattle.io.cluster/__tests__/rke2.test.ts +11 -9
- package/edit/provisioning.cattle.io.cluster/index.vue +25 -15
- package/edit/provisioning.cattle.io.cluster/rke2.vue +98 -17
- package/edit/provisioning.cattle.io.cluster/tabs/AddOnConfig.vue +28 -2
- package/edit/provisioning.cattle.io.cluster/tabs/MachinePool.vue +107 -5
- package/edit/provisioning.cattle.io.cluster/tabs/networking/index.vue +92 -4
- package/edit/secret/index.vue +1 -1
- package/edit/service.vue +9 -4
- package/edit/serviceaccount.vue +1 -0
- package/edit/storage.k8s.io.storageclass/index.vue +1 -0
- package/edit/workload/index.vue +2 -1
- package/edit/workload/mixins/workload.js +1 -1
- package/initialize/App.vue +4 -4
- package/initialize/install-directives.js +2 -0
- package/initialize/install-plugins.js +19 -2
- package/list/projectsecret.vue +1 -1
- package/list/provisioning.cattle.io.cluster.vue +15 -2
- package/machine-config/amazonec2.vue +42 -135
- package/machine-config/azure.vue +1 -1
- package/machine-config/components/EC2Networking.vue +490 -0
- package/machine-config/components/__tests__/EC2Networking.test.ts +148 -0
- package/machine-config/components/__tests__/utils/vpcSubnetMockData.js +294 -0
- package/machine-config/digitalocean.vue +11 -0
- package/machine-config/google.vue +1 -1
- package/mixins/__tests__/brand.spec.ts +2 -2
- package/mixins/__tests__/chart.test.ts +21 -0
- package/mixins/brand.js +1 -7
- package/mixins/chart.js +8 -2
- package/mixins/create-edit-view/index.js +5 -0
- package/models/__tests__/chart.test.ts +49 -12
- package/models/__tests__/compliance.cattle.io.clusterscanprofile.spec.js +30 -0
- package/models/__tests__/provisioning.cattle.io.cluster.test.ts +112 -5
- package/models/catalog.cattle.io.app.js +1 -1
- package/models/chart.js +28 -14
- package/models/cluster/node.js +13 -6
- package/models/cluster.x-k8s.io.machine.js +10 -20
- package/models/cluster.x-k8s.io.machinedeployment.js +5 -1
- package/models/compliance.cattle.io.clusterscanprofile.js +1 -1
- package/models/management.cattle.io.authconfig.js +1 -0
- package/models/management.cattle.io.cluster.js +21 -3
- package/models/management.cattle.io.kontainerdriver.js +1 -0
- package/models/provisioning.cattle.io.cluster.js +249 -33
- package/package.json +6 -5
- package/pages/auth/login.vue +43 -4
- package/pages/auth/verify.vue +1 -1
- package/pages/c/_cluster/apps/charts/AppChartCardSubHeader.vue +3 -2
- package/pages/c/_cluster/apps/charts/__tests__/chart.test.ts +135 -0
- package/pages/c/_cluster/apps/charts/chart.vue +35 -17
- package/pages/c/_cluster/apps/charts/index.vue +11 -13
- package/pages/c/_cluster/apps/charts/install.vue +1 -1
- package/pages/c/_cluster/explorer/EventsTable.vue +89 -3
- package/pages/c/_cluster/explorer/index.vue +8 -6
- package/pages/c/_cluster/explorer/tools/index.vue +3 -3
- package/pages/c/_cluster/manager/hostedprovider/index.vue +220 -0
- package/pages/c/_cluster/settings/brand.vue +1 -1
- package/pages/c/_cluster/settings/performance.vue +12 -25
- package/pages/c/_cluster/uiplugins/__tests__/index.test.ts +7 -0
- package/pages/c/_cluster/uiplugins/catalogs.vue +147 -0
- package/pages/c/_cluster/uiplugins/index.vue +126 -184
- package/pages/home.vue +327 -16
- package/pkg/dynamic-importer.lib.js +4 -0
- package/plugins/axios.js +2 -1
- package/plugins/dashboard-client-init.js +3 -0
- package/plugins/dashboard-store/actions.js +1 -1
- package/plugins/dashboard-store/getters.js +18 -1
- package/plugins/dashboard-store/resource-class.js +21 -6
- package/plugins/dynamic-content.js +13 -0
- package/plugins/i18n.js +8 -0
- package/plugins/steve/__tests__/steve-pagination-utils.test.ts +333 -0
- package/plugins/steve/steve-pagination-utils.ts +41 -22
- package/plugins/steve/subscribe.js +17 -9
- package/plugins/subscribe-events.ts +4 -2
- package/rancher-components/Form/Checkbox/Checkbox.vue +1 -1
- package/rancher-components/Pill/RcStatusBadge/RcStatusBadge.vue +6 -34
- package/rancher-components/Pill/RcStatusBadge/index.ts +0 -1
- package/rancher-components/Pill/RcStatusBadge/types.ts +1 -1
- package/rancher-components/Pill/RcStatusIndicator/RcStatusIndicator.vue +5 -28
- package/rancher-components/Pill/RcStatusIndicator/types.ts +2 -1
- package/rancher-components/Pill/types.ts +0 -1
- package/rancher-components/RcDropdown/RcDropdownItem.vue +1 -0
- package/rancher-components/RcDropdown/RcDropdownItemSelect.vue +5 -1
- package/rancher-components/RcIcon/RcIcon.test.ts +51 -0
- package/rancher-components/RcIcon/RcIcon.vue +46 -0
- package/rancher-components/RcIcon/index.ts +1 -0
- package/rancher-components/RcIcon/types.ts +160 -0
- package/rancher-components/utils/status.test.ts +67 -0
- package/rancher-components/utils/status.ts +77 -0
- package/scripts/extension/publish +1 -1
- package/scripts/typegen.sh +1 -0
- package/store/action-menu.js +8 -0
- package/store/auth.js +11 -6
- package/store/aws.js +8 -6
- package/store/catalog.js +6 -0
- package/store/features.js +2 -0
- package/store/index.js +45 -20
- package/store/notifications.ts +51 -4
- package/store/plugins.js +7 -3
- package/store/prefs.js +12 -6
- package/store/type-map.js +3 -3
- package/store/ui-context.ts +86 -0
- package/store/wm.ts +244 -0
- package/types/kube/kube-api.ts +2 -1
- package/types/notifications/index.ts +27 -3
- package/types/rancher/index.d.ts +1 -0
- package/types/resources/settings.d.ts +29 -7
- package/types/shell/index.d.ts +138 -4
- package/types/store/__tests__/pagination.types.spec.ts +137 -0
- package/types/store/pagination.types.ts +157 -9
- package/types/store/subscribe-events.types.ts +8 -1
- package/types/store/subscribe.types.ts +1 -0
- package/types/window-manager.ts +24 -0
- package/utils/__tests__/cluster.test.ts +379 -1
- package/utils/__tests__/object.test.ts +19 -0
- package/utils/__tests__/provider.test.ts +98 -0
- package/utils/__tests__/selector-typed.test.ts +263 -0
- package/utils/__tests__/version.test.ts +19 -1
- package/utils/autoscaler-utils.ts +7 -0
- package/utils/back-off.ts +3 -3
- package/utils/brand.ts +29 -0
- package/utils/chart.js +18 -0
- package/utils/cluster.js +157 -3
- package/utils/color.js +1 -1
- package/utils/dynamic-content/__tests__/announcement.test.ts +498 -0
- package/utils/dynamic-content/__tests__/config.test.ts +187 -0
- package/utils/dynamic-content/__tests__/index.test.ts +390 -0
- package/utils/dynamic-content/__tests__/info.test.ts +275 -0
- package/utils/dynamic-content/__tests__/new-release.test.ts +216 -0
- package/utils/dynamic-content/__tests__/support-notice.test.ts +262 -0
- package/utils/dynamic-content/__tests__/util.test.ts +235 -0
- package/utils/dynamic-content/announcement.ts +142 -0
- package/utils/dynamic-content/config.ts +55 -0
- package/utils/dynamic-content/example.json +40 -0
- package/utils/dynamic-content/index.ts +277 -0
- package/utils/dynamic-content/info.ts +261 -0
- package/utils/dynamic-content/new-release.ts +126 -0
- package/utils/dynamic-content/notification-handler.ts +48 -0
- package/utils/dynamic-content/support-notice.ts +169 -0
- package/utils/dynamic-content/types.d.ts +153 -0
- package/utils/dynamic-content/util.ts +122 -0
- package/utils/dynamic-importer.js +2 -2
- package/utils/favicon.js +4 -4
- package/utils/inactivity.ts +104 -0
- package/utils/object.js +20 -2
- package/utils/pagination-utils.ts +19 -4
- package/utils/pagination-wrapper.ts +12 -8
- package/utils/provider.ts +14 -0
- package/utils/release-notes.ts +1 -1
- package/utils/scroll.js +7 -0
- package/utils/selector-typed.ts +6 -2
- package/utils/settings.ts +15 -0
- package/utils/validators/machine-pool.ts +13 -3
- package/utils/version.js +15 -0
- package/assets/images/icons/document.svg +0 -3
- package/plugins/nuxt-client-init.js +0 -3
- package/store/wm.js +0 -95
- /package/components/{nav/WindowManager → Window}/ChartReadme.vue +0 -0
- /package/components/{nav/WindowManager → Window}/ContainerShell.vue +0 -0
- /package/components/{nav/WindowManager → Window}/KubectlShell.vue +0 -0
- /package/components/{nav/WindowManager → Window}/MachineSsh.vue +0 -0
- /package/components/{nav/WindowManager → Window}/Window.vue +0 -0
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { removeMatchingNotifications, createLogger, LOCAL_STORAGE_CONTENT_DEBUG_LOG } from '../util';
|
|
2
|
+
import { Context, Configuration } from '../types';
|
|
3
|
+
|
|
4
|
+
describe('util.ts', () => {
|
|
5
|
+
describe('removeMatchingNotifications', () => {
|
|
6
|
+
let mockContext: Context;
|
|
7
|
+
let mockDispatch: jest.Mock;
|
|
8
|
+
let mockGetters: any;
|
|
9
|
+
let mockLogger: any;
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
mockDispatch = jest.fn();
|
|
13
|
+
mockGetters = { 'notifications/all': [] };
|
|
14
|
+
mockLogger = { debug: jest.fn() };
|
|
15
|
+
mockContext = {
|
|
16
|
+
dispatch: mockDispatch,
|
|
17
|
+
getters: mockGetters,
|
|
18
|
+
logger: mockLogger,
|
|
19
|
+
// The following properties are not used by this function but are required by the type
|
|
20
|
+
axios: {},
|
|
21
|
+
isAdmin: true,
|
|
22
|
+
config: {} as any,
|
|
23
|
+
settings: {} as any,
|
|
24
|
+
};
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('should return false and not remove anything if no notifications exist', async() => {
|
|
28
|
+
const found = await removeMatchingNotifications(mockContext, 'prefix-', 'current');
|
|
29
|
+
|
|
30
|
+
expect(found).toBe(false);
|
|
31
|
+
expect(mockDispatch).not.toHaveBeenCalled();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should return false and not remove anything if no notifications match the prefix', async() => {
|
|
35
|
+
mockGetters['notifications/all'] = [
|
|
36
|
+
{ id: 'other-1' },
|
|
37
|
+
{ id: 'other-2' },
|
|
38
|
+
];
|
|
39
|
+
const found = await removeMatchingNotifications(mockContext, 'prefix-', 'current');
|
|
40
|
+
|
|
41
|
+
expect(found).toBe(false);
|
|
42
|
+
expect(mockDispatch).not.toHaveBeenCalled();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should return true and not remove anything if the current notification is the only one matching', async() => {
|
|
46
|
+
mockGetters['notifications/all'] = [
|
|
47
|
+
{ id: 'prefix-current' },
|
|
48
|
+
{ id: 'other-1' },
|
|
49
|
+
];
|
|
50
|
+
const found = await removeMatchingNotifications(mockContext, 'prefix-', 'current');
|
|
51
|
+
|
|
52
|
+
expect(found).toBe(true);
|
|
53
|
+
expect(mockDispatch).not.toHaveBeenCalled();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('should return false and remove a notification that matches the prefix but not the currentId', async() => {
|
|
57
|
+
mockGetters['notifications/all'] = [
|
|
58
|
+
{ id: 'prefix-old' },
|
|
59
|
+
{ id: 'other-1' },
|
|
60
|
+
];
|
|
61
|
+
const found = await removeMatchingNotifications(mockContext, 'prefix-', 'current');
|
|
62
|
+
|
|
63
|
+
expect(found).toBe(false);
|
|
64
|
+
expect(mockDispatch).toHaveBeenCalledTimes(1);
|
|
65
|
+
expect(mockDispatch).toHaveBeenCalledWith('notifications/remove', 'prefix-old');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('should return true and remove old notifications when the current one also exists', async() => {
|
|
69
|
+
mockGetters['notifications/all'] = [
|
|
70
|
+
{ id: 'prefix-old-1' },
|
|
71
|
+
{ id: 'prefix-current' },
|
|
72
|
+
{ id: 'prefix-old-2' },
|
|
73
|
+
{ id: 'other-1' },
|
|
74
|
+
];
|
|
75
|
+
const found = await removeMatchingNotifications(mockContext, 'prefix-', 'current');
|
|
76
|
+
|
|
77
|
+
expect(found).toBe(true);
|
|
78
|
+
expect(mockDispatch).toHaveBeenCalledTimes(2);
|
|
79
|
+
expect(mockDispatch).toHaveBeenCalledWith('notifications/remove', 'prefix-old-1');
|
|
80
|
+
expect(mockDispatch).toHaveBeenCalledWith('notifications/remove', 'prefix-old-2');
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe('createLogger / log', () => {
|
|
85
|
+
let mockLocalStorage: { [key: string]: string };
|
|
86
|
+
let consoleErrorSpy: jest.SpyInstance;
|
|
87
|
+
let consoleInfoSpy: jest.SpyInstance;
|
|
88
|
+
let consoleDebugSpy: jest.SpyInstance;
|
|
89
|
+
let dispatchEventSpy: jest.SpyInstance;
|
|
90
|
+
|
|
91
|
+
beforeEach(() => {
|
|
92
|
+
// Mock localStorage
|
|
93
|
+
mockLocalStorage = {};
|
|
94
|
+
jest.spyOn(Storage.prototype, 'getItem').mockImplementation((key) => mockLocalStorage[key] || null);
|
|
95
|
+
jest.spyOn(Storage.prototype, 'setItem').mockImplementation((key, value) => {
|
|
96
|
+
mockLocalStorage[key] = value;
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// Mock console
|
|
100
|
+
consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
|
|
101
|
+
consoleInfoSpy = jest.spyOn(console, 'info').mockImplementation(() => {});
|
|
102
|
+
consoleDebugSpy = jest.spyOn(console, 'debug').mockImplementation(() => {});
|
|
103
|
+
|
|
104
|
+
// Mock dispatchEvent
|
|
105
|
+
dispatchEventSpy = jest.spyOn(window, 'dispatchEvent').mockImplementation(() => true);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
afterEach(() => {
|
|
109
|
+
jest.restoreAllMocks();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('should always log errors to console, but only to localStorage if config.log is true', () => {
|
|
113
|
+
const config: Configuration = {
|
|
114
|
+
enabled: true, debug: false, log: false, endpoint: '', prime: false, distribution: 'community'
|
|
115
|
+
};
|
|
116
|
+
const logger = createLogger(config);
|
|
117
|
+
|
|
118
|
+
logger.error('test error');
|
|
119
|
+
|
|
120
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith('test error');
|
|
121
|
+
expect(mockLocalStorage[LOCAL_STORAGE_CONTENT_DEBUG_LOG]).toBeUndefined();
|
|
122
|
+
|
|
123
|
+
// Test with arg
|
|
124
|
+
logger.error('test error', 'with arg');
|
|
125
|
+
|
|
126
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith('test error', 'with arg');
|
|
127
|
+
expect(mockLocalStorage[LOCAL_STORAGE_CONTENT_DEBUG_LOG]).toBeUndefined();
|
|
128
|
+
|
|
129
|
+
config.log = true;
|
|
130
|
+
|
|
131
|
+
logger.error('test error with log');
|
|
132
|
+
|
|
133
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith('test error with log');
|
|
134
|
+
expect(mockLocalStorage[LOCAL_STORAGE_CONTENT_DEBUG_LOG]).toBeDefined();
|
|
135
|
+
expect(JSON.parse(mockLocalStorage[LOCAL_STORAGE_CONTENT_DEBUG_LOG])[0].message).toBe('test error with log');
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('should log info to console and localStorage only if config.log is true', () => {
|
|
139
|
+
const config: Configuration = {
|
|
140
|
+
enabled: true, debug: false, log: false, endpoint: '', prime: false, distribution: 'community'
|
|
141
|
+
};
|
|
142
|
+
const logger = createLogger(config);
|
|
143
|
+
|
|
144
|
+
logger.info('test info');
|
|
145
|
+
|
|
146
|
+
expect(consoleInfoSpy).not.toHaveBeenCalled();
|
|
147
|
+
expect(mockLocalStorage[LOCAL_STORAGE_CONTENT_DEBUG_LOG]).toBeUndefined();
|
|
148
|
+
|
|
149
|
+
config.log = true;
|
|
150
|
+
logger.info('test info with log');
|
|
151
|
+
|
|
152
|
+
expect(consoleInfoSpy).toHaveBeenCalledWith('test info with log');
|
|
153
|
+
expect(mockLocalStorage[LOCAL_STORAGE_CONTENT_DEBUG_LOG]).toBeDefined();
|
|
154
|
+
expect(JSON.parse(mockLocalStorage[LOCAL_STORAGE_CONTENT_DEBUG_LOG])[0].message).toBe('test info with log');
|
|
155
|
+
|
|
156
|
+
// Test with arg
|
|
157
|
+
logger.info('test info', 'with arg');
|
|
158
|
+
|
|
159
|
+
expect(consoleInfoSpy).toHaveBeenCalledWith('test info', 'with arg');
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('should log debug to console only if config.debug is true, and localStorage if config.log is true', () => {
|
|
163
|
+
const config: Configuration = {
|
|
164
|
+
enabled: true, debug: false, log: false, endpoint: '', prime: false, distribution: 'community'
|
|
165
|
+
};
|
|
166
|
+
const logger = createLogger(config);
|
|
167
|
+
|
|
168
|
+
logger.debug('test debug');
|
|
169
|
+
expect(consoleDebugSpy).not.toHaveBeenCalled();
|
|
170
|
+
expect(mockLocalStorage[LOCAL_STORAGE_CONTENT_DEBUG_LOG]).toBeUndefined();
|
|
171
|
+
|
|
172
|
+
config.log = true;
|
|
173
|
+
logger.debug('test debug with log');
|
|
174
|
+
expect(consoleDebugSpy).not.toHaveBeenCalled();
|
|
175
|
+
expect(mockLocalStorage[LOCAL_STORAGE_CONTENT_DEBUG_LOG]).toBeDefined();
|
|
176
|
+
expect(JSON.parse(mockLocalStorage[LOCAL_STORAGE_CONTENT_DEBUG_LOG])[0].message).toBe('test debug with log');
|
|
177
|
+
|
|
178
|
+
config.debug = true;
|
|
179
|
+
logger.debug('test debug with debug and log');
|
|
180
|
+
expect(consoleDebugSpy).toHaveBeenCalledWith('test debug with debug and log');
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('should dispatch a custom event when logging to localStorage', () => {
|
|
184
|
+
const config: Configuration = {
|
|
185
|
+
enabled: true, debug: false, log: true, endpoint: '', prime: false, distribution: 'community'
|
|
186
|
+
};
|
|
187
|
+
const logger = createLogger(config);
|
|
188
|
+
|
|
189
|
+
logger.info('test event');
|
|
190
|
+
|
|
191
|
+
expect(dispatchEventSpy).toHaveBeenCalledTimes(1);
|
|
192
|
+
const event = dispatchEventSpy.mock.calls[0][0] as CustomEvent;
|
|
193
|
+
|
|
194
|
+
expect(event.type).toBe('dynamicContentLog');
|
|
195
|
+
expect(event.detail.message).toBe('test event');
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('should limit the number of log messages in localStorage', () => {
|
|
199
|
+
const config: Configuration = {
|
|
200
|
+
enabled: true, debug: false, log: true, endpoint: '', prime: false, distribution: 'community'
|
|
201
|
+
};
|
|
202
|
+
const logger = createLogger(config);
|
|
203
|
+
|
|
204
|
+
// MAX_LOG_MESSAGES is 50
|
|
205
|
+
for (let i = 0; i < 60; i++) {
|
|
206
|
+
logger.info(`message ${ i }`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const logs = JSON.parse(mockLocalStorage[LOCAL_STORAGE_CONTENT_DEBUG_LOG]);
|
|
210
|
+
|
|
211
|
+
expect(logs).toHaveLength(50);
|
|
212
|
+
expect(logs[0].message).toBe('message 59'); // Most recent
|
|
213
|
+
expect(logs[49].message).toBe('message 10'); // Oldest
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('should not throw if localStorage is corrupted', () => {
|
|
217
|
+
const config: Configuration = {
|
|
218
|
+
enabled: true, debug: false, log: true, endpoint: '', prime: false, distribution: 'community'
|
|
219
|
+
};
|
|
220
|
+
const logger = createLogger(config);
|
|
221
|
+
|
|
222
|
+
mockLocalStorage[LOCAL_STORAGE_CONTENT_DEBUG_LOG] = 'this is not valid json';
|
|
223
|
+
|
|
224
|
+
expect(() => {
|
|
225
|
+
logger.info('test message');
|
|
226
|
+
}).not.toThrow();
|
|
227
|
+
|
|
228
|
+
// It should have overwritten the bad data
|
|
229
|
+
const logs = JSON.parse(mockLocalStorage[LOCAL_STORAGE_CONTENT_DEBUG_LOG]);
|
|
230
|
+
|
|
231
|
+
expect(logs).toHaveLength(1);
|
|
232
|
+
expect(logs[0].message).toBe('test message');
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
});
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
*
|
|
3
|
+
* The code in this file is responsible for adding 'announcement 'notifications driven off of the dynamic content metadata
|
|
4
|
+
*
|
|
5
|
+
* Announcements will be able to be shown in different places in the UI
|
|
6
|
+
*
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import semver from 'semver';
|
|
10
|
+
import { NotificationLevel, Notification } from '@shell/types/notifications';
|
|
11
|
+
import { READ_ANNOUNCEMENTS } from '@shell/store/prefs';
|
|
12
|
+
import { Context, VersionInfo, Announcement } from './types';
|
|
13
|
+
import { DynamicContentAnnouncementHandlerName } from './notification-handler';
|
|
14
|
+
|
|
15
|
+
// Prefixes used in the notifications IDs created here
|
|
16
|
+
export const ANNOUNCEMENT_PREFIX = 'announcement-';
|
|
17
|
+
|
|
18
|
+
const TARGET_NOTIFICATION_CENTER = 'notification';
|
|
19
|
+
const TARGET_HOME_PAGE = 'homepage';
|
|
20
|
+
const ALLOWED_TARGETS = [TARGET_NOTIFICATION_CENTER, TARGET_HOME_PAGE];
|
|
21
|
+
|
|
22
|
+
const ALLOWED_NOTIFICATIONS: Record<string, NotificationLevel> = {
|
|
23
|
+
announcement: NotificationLevel.Announcement,
|
|
24
|
+
info: NotificationLevel.Info,
|
|
25
|
+
warning: NotificationLevel.Warning,
|
|
26
|
+
homepage: NotificationLevel.Hidden,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Main exported function that will process the announcements
|
|
31
|
+
*
|
|
32
|
+
* @param context Context helper providing access to config, logger, store
|
|
33
|
+
* @param announcements Announcement information
|
|
34
|
+
* @param versionInfo Version information
|
|
35
|
+
*/
|
|
36
|
+
export async function processAnnouncements(context: Context, announcements: Announcement[] | undefined, versionInfo: VersionInfo): Promise<void> {
|
|
37
|
+
if (!announcements || !announcements.length || !versionInfo?.version) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const { dispatch, getters, logger } = context;
|
|
42
|
+
|
|
43
|
+
// Process each announcement
|
|
44
|
+
await Promise.all(announcements.map(async(announcement: Announcement) => {
|
|
45
|
+
// Check version
|
|
46
|
+
if (announcement.version && !semver.satisfies(versionInfo.version, announcement.version)) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Check audience (currently only admin or all, but may add more in the future)
|
|
51
|
+
if (announcement.audience === 'admin' && !context.isAdmin) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (!announcement.id) {
|
|
56
|
+
logger.error(`No ID For announcement - not going to add a notification for the announcement`);
|
|
57
|
+
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Check type
|
|
62
|
+
const targetSplit = announcement.target.split('/');
|
|
63
|
+
const target = targetSplit[0];
|
|
64
|
+
|
|
65
|
+
// Make sure that the target is supported
|
|
66
|
+
if (ALLOWED_TARGETS.includes(target)) {
|
|
67
|
+
let level = NotificationLevel.Announcement;
|
|
68
|
+
let data: any = {};
|
|
69
|
+
|
|
70
|
+
if (target === TARGET_NOTIFICATION_CENTER) {
|
|
71
|
+
// Show a notification
|
|
72
|
+
const subType = targetSplit.length === 2 ? targetSplit[1] : 'announcement';
|
|
73
|
+
|
|
74
|
+
if (!(subType in ALLOWED_NOTIFICATIONS)) {
|
|
75
|
+
logger.error(`Announcement notification type ${ subType } is not supported`);
|
|
76
|
+
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
level = ALLOWED_NOTIFICATIONS[subType];
|
|
81
|
+
} else if (target === TARGET_HOME_PAGE) {
|
|
82
|
+
level = NotificationLevel.Hidden;
|
|
83
|
+
data = {
|
|
84
|
+
icon: announcement.icon,
|
|
85
|
+
location: targetSplit.length === 2 ? targetSplit[1] : 'banner',
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
if (announcement.style) {
|
|
89
|
+
data.style = announcement.style;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
logger.info(`Going to add a notification for announcement ${ announcement.target }`);
|
|
94
|
+
|
|
95
|
+
// We should check if the notification already exists
|
|
96
|
+
const id = `${ ANNOUNCEMENT_PREFIX }${ announcement.id }`;
|
|
97
|
+
const existing = getters['notifications/item'](id);
|
|
98
|
+
|
|
99
|
+
// Check if the pref for 'read announcements' has the id
|
|
100
|
+
const pref = getters['prefs/get'](READ_ANNOUNCEMENTS) || '';
|
|
101
|
+
const prefExists = pref.split(',').includes(announcement.id);
|
|
102
|
+
|
|
103
|
+
if (existing || prefExists) {
|
|
104
|
+
logger.info(`Not adding announcement with ID ${ id } as it already exists or has been read previously (title: ${ announcement.title })`);
|
|
105
|
+
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const notification: Notification = {
|
|
110
|
+
id,
|
|
111
|
+
level,
|
|
112
|
+
title: announcement.title,
|
|
113
|
+
message: announcement.message,
|
|
114
|
+
handlerName: DynamicContentAnnouncementHandlerName,
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
if (data && Object.keys(data).length > 0) {
|
|
118
|
+
notification.data = data;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (announcement.cta?.primary) {
|
|
122
|
+
notification.primaryAction = {
|
|
123
|
+
label: announcement.cta.primary.action,
|
|
124
|
+
target: announcement.cta.primary.link,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (announcement.cta?.secondary) {
|
|
129
|
+
notification.secondaryAction = {
|
|
130
|
+
label: announcement.cta.secondary.action,
|
|
131
|
+
target: announcement.cta.secondary.link,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
logger.info(`Adding announcement with ID ${ id } (title: ${ announcement.title }, target: ${ announcement.target })`);
|
|
136
|
+
|
|
137
|
+
await dispatch('notifications/add', notification);
|
|
138
|
+
} else {
|
|
139
|
+
logger.error(`Announcement type ${ announcement.target } is not supported`);
|
|
140
|
+
}
|
|
141
|
+
}));
|
|
142
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { SETTING } from '@shell/config/settings';
|
|
2
|
+
import { isRancherPrime } from '@shell/config/version';
|
|
3
|
+
import { Configuration, Distribution } from './types';
|
|
4
|
+
import { MANAGEMENT } from '@shell/config/types';
|
|
5
|
+
|
|
6
|
+
// Default endpoint ($dist is either 'community' or 'prime')
|
|
7
|
+
const DEFAULT_ENDPOINT = 'https://updates.rancher.io/rancher/$dist/updates';
|
|
8
|
+
|
|
9
|
+
// We only support retrieving content from secure endpoints
|
|
10
|
+
const HTTPS_PREFIX = 'https://';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Get configuration data based on the distribution and Rancher settings
|
|
14
|
+
*
|
|
15
|
+
* @param getters Store getters to access the store
|
|
16
|
+
* @returns Dynamic Content configuration
|
|
17
|
+
*/
|
|
18
|
+
export function getConfig(getters: any): Configuration {
|
|
19
|
+
const prime = isRancherPrime();
|
|
20
|
+
const distribution: Distribution = prime ? 'prime' : 'community';
|
|
21
|
+
|
|
22
|
+
// Default configuration
|
|
23
|
+
const config: Configuration = {
|
|
24
|
+
enabled: true,
|
|
25
|
+
debug: false,
|
|
26
|
+
log: false,
|
|
27
|
+
endpoint: DEFAULT_ENDPOINT,
|
|
28
|
+
prime,
|
|
29
|
+
distribution,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// Update 'enabled' and 'endpoint' from Rancher settings, if applicable
|
|
33
|
+
try {
|
|
34
|
+
const enabledSetting = getters['management/byId'](MANAGEMENT.SETTING, SETTING.DYNAMIC_CONTENT_ENABLED);
|
|
35
|
+
|
|
36
|
+
if (enabledSetting?.value) {
|
|
37
|
+
// Any value other than 'false' means enabled (can't disable on Prime)
|
|
38
|
+
config.enabled = config.prime ? enabledSetting.value !== 'false' : true;
|
|
39
|
+
config.debug = enabledSetting.value === 'debug';
|
|
40
|
+
config.log = enabledSetting.value === 'log' || config.debug;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Can only override the url when Prime
|
|
44
|
+
const urlSetting = getters['management/byId'](MANAGEMENT.SETTING, SETTING.DYNAMIC_CONTENT_ENDPOINT);
|
|
45
|
+
|
|
46
|
+
// Are we Prime, do we have a URL and does the URL start with the https prefix? If so, use it
|
|
47
|
+
if (prime && urlSetting?.value && urlSetting.value.startsWith(HTTPS_PREFIX)) {
|
|
48
|
+
config.endpoint = urlSetting.value;
|
|
49
|
+
}
|
|
50
|
+
} catch (e) {
|
|
51
|
+
console.error('Error reading dynamic content settings', e); // eslint-disable-line no-console
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return config;
|
|
55
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"releases": [
|
|
4
|
+
{
|
|
5
|
+
"name": "2.12.2"
|
|
6
|
+
},
|
|
7
|
+
{
|
|
8
|
+
"name": "2.11.3"
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"name": "2.10.3"
|
|
12
|
+
}
|
|
13
|
+
],
|
|
14
|
+
"announcements": [
|
|
15
|
+
{
|
|
16
|
+
"id": "security-update",
|
|
17
|
+
"target": "notification/announcement",
|
|
18
|
+
"title": "Important Security Update",
|
|
19
|
+
"message": "A critical security vulnerability has been discovered in version 2.10.1. Users are strongly advised to update to version 2.12.2 immediately to ensure their systems remain secure.",
|
|
20
|
+
"cta": {
|
|
21
|
+
"primary" : {
|
|
22
|
+
"action": "Update Now",
|
|
23
|
+
"link": "https://www.suse.com/"
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"id": "suse-rocks",
|
|
29
|
+
"target": "homepage/banner",
|
|
30
|
+
"title": "Important Security Update",
|
|
31
|
+
"message": "A critical security vulnerability has been discovered in version 2.10.1. Users are strongly advised to update to version 2.12.2 immediately to ensure their systems remain secure.",
|
|
32
|
+
"cta": {
|
|
33
|
+
"primary" : {
|
|
34
|
+
"action": "Update Now",
|
|
35
|
+
"link": "https://www.suse.com/"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
]
|
|
40
|
+
}
|