@rancher/shell 3.0.7 → 3.0.8-rc.1
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/images/vendor/githubapp.svg +13 -0
- package/assets/styles/base/_typography.scss +1 -1
- package/assets/styles/themes/_modern.scss +5 -5
- package/assets/translations/en-us.yaml +91 -11
- package/assets/translations/zh-hans.yaml +0 -4
- package/components/Inactivity.vue +222 -106
- package/components/InstallHelmCharts.vue +2 -2
- package/components/ResourceDetail/index.vue +1 -1
- package/components/SortableTable/index.vue +17 -2
- 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/nav/Header.vue +1 -0
- package/config/product/auth.js +1 -0
- package/config/query-params.js +1 -0
- package/config/settings.ts +8 -1
- package/config/types.js +2 -0
- package/dialog/AddonConfigConfirmationDialog.vue +45 -1
- package/edit/__tests__/fleet.cattle.io.helmop.test.ts +52 -11
- package/edit/auth/AuthProviderWarningBanners.vue +14 -1
- package/edit/auth/github-app-steps.vue +97 -0
- package/edit/auth/github-steps.vue +75 -0
- package/edit/auth/github.vue +94 -65
- package/edit/fleet.cattle.io.helmop.vue +51 -2
- package/edit/networking.k8s.io.networkpolicy/PolicyRuleTarget.vue +15 -5
- package/edit/provisioning.cattle.io.cluster/__tests__/rke2.test.ts +11 -9
- package/edit/provisioning.cattle.io.cluster/rke2.vue +56 -9
- package/edit/provisioning.cattle.io.cluster/tabs/AddOnConfig.vue +28 -2
- package/list/projectsecret.vue +1 -1
- package/machine-config/azure.vue +1 -1
- package/mixins/chart.js +1 -1
- package/models/__tests__/chart.test.ts +17 -9
- package/models/__tests__/compliance.cattle.io.clusterscanprofile.spec.js +30 -0
- package/models/catalog.cattle.io.app.js +1 -1
- package/models/chart.js +3 -1
- package/models/compliance.cattle.io.clusterscanprofile.js +1 -1
- package/models/management.cattle.io.authconfig.js +1 -0
- package/package.json +2 -2
- package/pages/auth/login.vue +5 -2
- package/pages/auth/verify.vue +1 -1
- package/pages/c/_cluster/apps/charts/AppChartCardSubHeader.vue +3 -2
- package/pages/c/_cluster/apps/charts/chart.vue +2 -2
- package/pages/c/_cluster/explorer/EventsTable.vue +89 -3
- package/pages/c/_cluster/explorer/tools/index.vue +3 -3
- package/pages/c/_cluster/settings/performance.vue +12 -25
- package/pages/home.vue +313 -12
- package/plugins/axios.js +2 -1
- package/plugins/dashboard-store/actions.js +1 -1
- package/plugins/dashboard-store/resource-class.js +17 -2
- package/plugins/steve/steve-pagination-utils.ts +2 -2
- package/scripts/extension/publish +1 -1
- package/store/auth.js +8 -3
- package/store/aws.js +8 -6
- package/store/features.js +1 -0
- package/store/index.js +9 -3
- package/store/prefs.js +6 -0
- package/types/kube/kube-api.ts +2 -1
- package/types/rancher/index.d.ts +1 -0
- package/types/resources/settings.d.ts +29 -7
- package/types/shell/index.d.ts +59 -0
- package/utils/__tests__/cluster.test.ts +379 -1
- package/utils/cluster.js +157 -3
- 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 +263 -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/config.ts +55 -0
- package/utils/dynamic-content/index.ts +273 -0
- package/utils/dynamic-content/info.ts +219 -0
- package/utils/dynamic-content/new-release.ts +126 -0
- package/utils/dynamic-content/support-notice.ts +169 -0
- package/utils/dynamic-content/types.d.ts +101 -0
- package/utils/dynamic-content/util.ts +122 -0
- package/utils/inactivity.ts +104 -0
- package/utils/pagination-utils.ts +19 -4
- package/utils/release-notes.ts +1 -1
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helper for collecting system information and formatting into to a query string
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { sha256 } from '@shell/utils/crypto';
|
|
6
|
+
import {
|
|
7
|
+
COUNT,
|
|
8
|
+
MANAGEMENT,
|
|
9
|
+
} from '@shell/config/types';
|
|
10
|
+
import { SETTING } from '@shell/config/settings';
|
|
11
|
+
import { getVersionData } from '@shell/config/version';
|
|
12
|
+
import { SettingsInfo } from '@shell/utils/dynamic-content/types';
|
|
13
|
+
|
|
14
|
+
const QS_VERSION = 'v1'; // Include a version number in the query string in case we want to version the set of params we are sending
|
|
15
|
+
const UNKNOWN = 'unknown';
|
|
16
|
+
|
|
17
|
+
// List of known UI extensions from SUSE
|
|
18
|
+
const SUSE_EXTENSIONS = [
|
|
19
|
+
'capi',
|
|
20
|
+
'elemental',
|
|
21
|
+
'harvester',
|
|
22
|
+
'kubewarden',
|
|
23
|
+
'neuvector-ui-ext',
|
|
24
|
+
'observability',
|
|
25
|
+
'supportability-review-app',
|
|
26
|
+
'virtual-clusters'
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* System information that is collected and which can then be encoded into a query string in the dyanmic content request
|
|
31
|
+
*/
|
|
32
|
+
type SystemInfo = {
|
|
33
|
+
systemUUID: string;
|
|
34
|
+
systemHash: string;
|
|
35
|
+
serverVersionType: string;
|
|
36
|
+
userHash: string;
|
|
37
|
+
version: string;
|
|
38
|
+
isDeveloperVersion: boolean;
|
|
39
|
+
isPrime: boolean;
|
|
40
|
+
isLTS: boolean;
|
|
41
|
+
clusterCount: number;
|
|
42
|
+
localCluster: any;
|
|
43
|
+
extensions?: {
|
|
44
|
+
knownInstalled: string[];
|
|
45
|
+
customCount: number;
|
|
46
|
+
};
|
|
47
|
+
browserSize: string;
|
|
48
|
+
screenSize: string;
|
|
49
|
+
language: string;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Helper that will gather system information and provided the `buildQueryString` method to format as a query string
|
|
54
|
+
*/
|
|
55
|
+
export class SystemInfoProvider {
|
|
56
|
+
private info: SystemInfo;
|
|
57
|
+
|
|
58
|
+
constructor(getters: any, settings: Partial<SettingsInfo>) {
|
|
59
|
+
this.info = this.getSystemData(getters, settings);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Get the data that we need from the system
|
|
64
|
+
* @param getters Store getters to access the store
|
|
65
|
+
* @returns System data
|
|
66
|
+
*/
|
|
67
|
+
private getSystemData(getters: any, settingsInfo: Partial<SettingsInfo>): SystemInfo {
|
|
68
|
+
let url;
|
|
69
|
+
let systemUUID = UNKNOWN;
|
|
70
|
+
let serverVersionType = UNKNOWN;
|
|
71
|
+
|
|
72
|
+
const serverUrlSetting = getters['management/byId'](MANAGEMENT.SETTING, SETTING.SERVER_URL);
|
|
73
|
+
|
|
74
|
+
// Server URL
|
|
75
|
+
if (serverUrlSetting) {
|
|
76
|
+
url = serverUrlSetting.value || UNKNOWN;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// UUID
|
|
80
|
+
const uuidSetting = getters['management/byId'](MANAGEMENT.SETTING, 'install-uuid');
|
|
81
|
+
|
|
82
|
+
if (uuidSetting) {
|
|
83
|
+
systemUUID = uuidSetting.value || UNKNOWN;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Server Version Type
|
|
87
|
+
const serverVersionTypeSetting = getters['management/byId'](MANAGEMENT.SETTING, 'server-version-type');
|
|
88
|
+
|
|
89
|
+
if (serverVersionTypeSetting) {
|
|
90
|
+
serverVersionType = serverVersionTypeSetting.value || UNKNOWN;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Otherwise, use the window location's host
|
|
94
|
+
url = url || window.location?.host;
|
|
95
|
+
|
|
96
|
+
// System and User hashes
|
|
97
|
+
const systemHash = (sha256(url, 'hex') as string).substring(0, 32);
|
|
98
|
+
const currentPrincipal = getters['auth/principalId'] || UNKNOWN;
|
|
99
|
+
const userHash = (sha256(currentPrincipal, 'hex') as string).substring(0, 32);
|
|
100
|
+
|
|
101
|
+
// Version info
|
|
102
|
+
const versionData = getVersionData();
|
|
103
|
+
const vers = versionData.Version.split('-');
|
|
104
|
+
|
|
105
|
+
// General stats that can help us shape content delivery
|
|
106
|
+
|
|
107
|
+
// High-level information from clusters
|
|
108
|
+
const counts = this.getAll(getters, COUNT)?.[0]?.counts || {};
|
|
109
|
+
const clusterCount = counts[MANAGEMENT.CLUSTER] || {};
|
|
110
|
+
const all = this.getAll(getters, MANAGEMENT.CLUSTER);
|
|
111
|
+
const localCluster = all ? all.find((c: any) => c.isLocal) : undefined;
|
|
112
|
+
|
|
113
|
+
// Stats for installed extensions
|
|
114
|
+
const uiExtensionList = getters['uiplugins/plugins'];
|
|
115
|
+
let extensions;
|
|
116
|
+
|
|
117
|
+
if (uiExtensionList) {
|
|
118
|
+
const suseExtensions = [
|
|
119
|
+
...SUSE_EXTENSIONS,
|
|
120
|
+
...settingsInfo?.suseExtensions || []
|
|
121
|
+
];
|
|
122
|
+
const notBuiltIn = uiExtensionList.filter((e: any) => !e.builtin);
|
|
123
|
+
const suseNames = notBuiltIn.filter((e: any) => suseExtensions.includes(e.name)).map((e: any) => e.name);
|
|
124
|
+
const customCount = notBuiltIn.length - suseNames.length;
|
|
125
|
+
|
|
126
|
+
extensions = {
|
|
127
|
+
knownInstalled: suseNames,
|
|
128
|
+
customCount,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const screenSize = `${ window.screen?.width || '?' }x${ window.screen?.height || '?' }`;
|
|
133
|
+
const browserSize = `${ window.innerWidth }x${ window.innerHeight }`;
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
systemUUID,
|
|
137
|
+
userHash,
|
|
138
|
+
systemHash,
|
|
139
|
+
serverVersionType,
|
|
140
|
+
version: vers[0],
|
|
141
|
+
isDeveloperVersion: vers.length > 1,
|
|
142
|
+
isPrime: versionData.RancherPrime === 'true',
|
|
143
|
+
isLTS: false,
|
|
144
|
+
clusterCount: clusterCount?.summary?.count,
|
|
145
|
+
localCluster,
|
|
146
|
+
extensions,
|
|
147
|
+
screenSize,
|
|
148
|
+
browserSize,
|
|
149
|
+
language: window.navigator?.language,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Helper to get all resources of a type only if they are available
|
|
154
|
+
private getAll(getters: any, typeName: string): any {
|
|
155
|
+
if (getters['management/typeRegistered'](typeName)) {
|
|
156
|
+
return getters['management/all'](typeName);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return undefined;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Build query string of params to send so that we can deliver better content
|
|
164
|
+
*/
|
|
165
|
+
public buildQueryString(): string {
|
|
166
|
+
const systemData = this.info;
|
|
167
|
+
const params = [`dcv=${ QS_VERSION }`];
|
|
168
|
+
|
|
169
|
+
// System and User
|
|
170
|
+
params.push(`s=${ systemData.systemHash }`);
|
|
171
|
+
params.push(`u=${ systemData.userHash }`);
|
|
172
|
+
|
|
173
|
+
// Install UUID
|
|
174
|
+
if (systemData.systemUUID !== UNKNOWN) {
|
|
175
|
+
params.push(`uuid=${ systemData.systemUUID }`);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Server Version Type
|
|
179
|
+
if (systemData.serverVersionType !== UNKNOWN) {
|
|
180
|
+
params.push(`svt=${ systemData.serverVersionType }`);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Version info
|
|
184
|
+
params.push(`v=${ systemData.version }`);
|
|
185
|
+
params.push(`dev=${ systemData.isDeveloperVersion }`);
|
|
186
|
+
params.push(`p=${ systemData.isPrime }`);
|
|
187
|
+
|
|
188
|
+
// Remove LTS for now, until we can determine LTS status
|
|
189
|
+
// params.push(`lts=${ systemData.isLTS }`);
|
|
190
|
+
|
|
191
|
+
// Clusters
|
|
192
|
+
params.push(`cc=${ systemData.clusterCount }`);
|
|
193
|
+
|
|
194
|
+
if (systemData.localCluster) {
|
|
195
|
+
params.push(`lkv=${ systemData.localCluster.kubernetesVersionBase || UNKNOWN }`);
|
|
196
|
+
params.push(`lcp=${ systemData.localCluster.provisioner || UNKNOWN }`);
|
|
197
|
+
params.push(`lnc=${ systemData.localCluster.status.nodeCount || 0 }`);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Extensions
|
|
201
|
+
if (systemData.extensions) {
|
|
202
|
+
params.push(`xkn=${ systemData.extensions.knownInstalled.join(',') }`);
|
|
203
|
+
params.push(`xcc=${ systemData.extensions.customCount }`);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Browser Language
|
|
207
|
+
params.push(`bl=${ systemData.language }`);
|
|
208
|
+
|
|
209
|
+
// Browser size
|
|
210
|
+
params.push(`bs=${ systemData.browserSize }`);
|
|
211
|
+
|
|
212
|
+
// Screen size
|
|
213
|
+
if (systemData.screenSize !== '?x?') {
|
|
214
|
+
params.push(`ss=${ systemData.screenSize }`);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return params.join('&');
|
|
218
|
+
}
|
|
219
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
*
|
|
3
|
+
* The code in this file is responsible for adding New Release notifications driven off of the dynamic content metadata
|
|
4
|
+
*
|
|
5
|
+
* We handle two cases:
|
|
6
|
+
*
|
|
7
|
+
* 1. There is a new patch release available for the current Rancher version (e.g. user is in 2.12.0 and we release 2.12.1)
|
|
8
|
+
* 2. There is a new patch release available for the current Rancher version AND there is a newer version for a high minor releases
|
|
9
|
+
* > this often occurs because we release monthly releases in parallel with the new minor releases
|
|
10
|
+
*
|
|
11
|
+
* We show slightly different messages in these 2 cases.
|
|
12
|
+
*
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import semver from 'semver';
|
|
16
|
+
import { NotificationLevel } from '@shell/types/notifications';
|
|
17
|
+
import { READ_NEW_RELEASE } from '@shell/store/prefs';
|
|
18
|
+
import { Context, ReleaseInfo, VersionInfo } from './types';
|
|
19
|
+
import { removeMatchingNotifications } from './util';
|
|
20
|
+
|
|
21
|
+
export async function processReleaseVersion(context: Context, releaseInfo: ReleaseInfo[] | undefined, versionInfo: VersionInfo) {
|
|
22
|
+
if (!releaseInfo || !versionInfo?.version || !Array.isArray(releaseInfo)) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const { version } = versionInfo;
|
|
27
|
+
const versions = releaseInfo.map((v: any) => semver.coerce(v.name));
|
|
28
|
+
|
|
29
|
+
// Sort the versions, so that the newest is first in the list
|
|
30
|
+
versions.sort((a: any, b: any) => semver.gt(b, a) ? 1 : -1);
|
|
31
|
+
|
|
32
|
+
// Find first newer version
|
|
33
|
+
const newer = versions.find((v: any) => semver.gt(v, version));
|
|
34
|
+
|
|
35
|
+
// Find newest patch version for the current version (if available)
|
|
36
|
+
const newerPatch = versions.find((v: any) => {
|
|
37
|
+
const newVersion = semver.coerce(v);
|
|
38
|
+
|
|
39
|
+
return newVersion && newVersion.major === version.major && newVersion.minor === version.minor && semver.gt(v, version);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
if (newer) {
|
|
43
|
+
context.logger.info(`Found a newer release: ${ newer.version }`);
|
|
44
|
+
|
|
45
|
+
if (newerPatch && newer !== newerPatch) {
|
|
46
|
+
context.logger.info(`Also found a newer patch release: ${ newerPatch.version }`);
|
|
47
|
+
// There is a new patch release and a newer release
|
|
48
|
+
await addNewMultipleReleasesNotification(context, newerPatch.version, newer.version);
|
|
49
|
+
} else {
|
|
50
|
+
// There is a new release (but no newer patch release)
|
|
51
|
+
await addNewReleaseNotification(context, newer.version);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function addNewReleaseNotification(context: Context, version: string) {
|
|
57
|
+
const prefix = 'new-release-';
|
|
58
|
+
const releaseNotesUrl = context.settings.releaseNotesUrl.replace('$version', version);
|
|
59
|
+
const { dispatch, getters, logger } = context;
|
|
60
|
+
|
|
61
|
+
// TODO: Get the preference
|
|
62
|
+
const lastReadVersion = getters['prefs/get'](READ_NEW_RELEASE) || '';
|
|
63
|
+
const t = getters['i18n/t'];
|
|
64
|
+
|
|
65
|
+
// Delete notification(s) for old release notes
|
|
66
|
+
// This shouldn't happen normally, as we release less often than notifications should expire
|
|
67
|
+
if (!await removeMatchingNotifications(context, prefix, version) && lastReadVersion !== version) {
|
|
68
|
+
logger.debug(`Adding new release notification for ${ version } because one did not exist`);
|
|
69
|
+
|
|
70
|
+
const notification = {
|
|
71
|
+
id: `${ prefix }${ version }`,
|
|
72
|
+
level: NotificationLevel.Announcement,
|
|
73
|
+
title: t('dynamicContent.newRelease.title', { version }),
|
|
74
|
+
message: t('dynamicContent.newRelease.message', { version }),
|
|
75
|
+
preference: {
|
|
76
|
+
key: READ_NEW_RELEASE,
|
|
77
|
+
value: version
|
|
78
|
+
},
|
|
79
|
+
primaryAction: {
|
|
80
|
+
label: t('dynamicContent.newRelease.moreInfo'),
|
|
81
|
+
target: releaseNotesUrl
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
await dispatch('notifications/add', notification);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function addNewMultipleReleasesNotification(context: Context, version1: string, version2: string) {
|
|
90
|
+
const prefix = 'new-release-';
|
|
91
|
+
const key = `${ version1 }-${ version2 }`;
|
|
92
|
+
const releaseNotesUrl1 = context.settings.releaseNotesUrl.replace('$version', version1);
|
|
93
|
+
const releaseNotesUrl2 = context.settings.releaseNotesUrl.replace('$version', version2);
|
|
94
|
+
const { dispatch, getters, logger } = context;
|
|
95
|
+
|
|
96
|
+
// TODO: Get the preference
|
|
97
|
+
const lastReadVersion = getters['prefs/get'](READ_NEW_RELEASE) || '';
|
|
98
|
+
const t = getters['i18n/t'];
|
|
99
|
+
|
|
100
|
+
// Delete notification(s) for old release notes
|
|
101
|
+
// This shouldn't happen normally, as we release less often than notifications should expire
|
|
102
|
+
if (!await removeMatchingNotifications(context, prefix, key) && lastReadVersion !== key) {
|
|
103
|
+
logger.info(`Adding new multiple release notification for ${ version1 } and ${ version2 }`);
|
|
104
|
+
|
|
105
|
+
const notification = {
|
|
106
|
+
id: `${ prefix }${ key }`,
|
|
107
|
+
level: NotificationLevel.Announcement,
|
|
108
|
+
title: t('dynamicContent.multipleNewReleases.title'),
|
|
109
|
+
message: t('dynamicContent.multipleNewReleases.message', { version1, version2 }),
|
|
110
|
+
preference: {
|
|
111
|
+
key: READ_NEW_RELEASE,
|
|
112
|
+
value: key
|
|
113
|
+
},
|
|
114
|
+
primaryAction: {
|
|
115
|
+
label: t('dynamicContent.multipleNewReleases.moreInfo', { version: version1 }),
|
|
116
|
+
target: releaseNotesUrl1
|
|
117
|
+
},
|
|
118
|
+
secondaryAction: {
|
|
119
|
+
label: t('dynamicContent.multipleNewReleases.moreInfo', { version: version2 }),
|
|
120
|
+
target: releaseNotesUrl2
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
await dispatch('notifications/add', notification);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
*
|
|
3
|
+
* The code in this file is responsible for adding Support notifications driven off of the dynamic content metadata
|
|
4
|
+
*
|
|
5
|
+
* This covers these cases:
|
|
6
|
+
*
|
|
7
|
+
* 1. Current release has reached End of Maintenance (EOL)
|
|
8
|
+
* 2. Current release has reached End of Maintenance (EOM)
|
|
9
|
+
* 3. Current release is approaching End of Maintenance (EOL)
|
|
10
|
+
* 4. Current release is approaching End of Maintenance (EOM)
|
|
11
|
+
*
|
|
12
|
+
* Note that we process in the order to that shown above, and stop at the first one that is active - so reaching EOL will show a notification
|
|
13
|
+
* and we won't look at the others.
|
|
14
|
+
*
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import semver from 'semver';
|
|
18
|
+
import day from 'dayjs';
|
|
19
|
+
import { NotificationLevel } from '@shell/types/notifications';
|
|
20
|
+
import { READ_SUPPORT_NOTICE, READ_UPCOMING_SUPPORT_NOTICE } from '@shell/store/prefs';
|
|
21
|
+
import { removeMatchingNotifications } from './util';
|
|
22
|
+
import { Context, VersionInfo, UpcomingSupportInfo, SupportInfo } from './types';
|
|
23
|
+
import { UPDATE_DATE_FORMAT } from './index';
|
|
24
|
+
|
|
25
|
+
// Number of days ahead of upcoming EOM or EOL that we will notify the user
|
|
26
|
+
const DEFAULT_UPCOMING_WINDOW = 30;
|
|
27
|
+
|
|
28
|
+
// Prefixes used in the notifications IDs created here
|
|
29
|
+
const SUPPORT_NOTICE_PREFIX = 'support-notice-';
|
|
30
|
+
const UPCOMING_SUPPORT_NOTICE_PREFIX = 'upcoming-support-notice-';
|
|
31
|
+
|
|
32
|
+
// Prefixes used in the value of the user preference to track which notifications the user has read
|
|
33
|
+
const PREFIX = {
|
|
34
|
+
EOM: 'eom',
|
|
35
|
+
EOL: 'eol',
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// Internal type used with convenience functions
|
|
39
|
+
type Config = {
|
|
40
|
+
prefValuePrefix?: string;
|
|
41
|
+
pref: any;
|
|
42
|
+
notificationPrefix: string;
|
|
43
|
+
titleKey: string;
|
|
44
|
+
messageKey: string;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Main exported function that will process the support (stateInfo)
|
|
49
|
+
*
|
|
50
|
+
* @param context Context helper providing access to config, logger, store
|
|
51
|
+
* @param statusInfo Support information
|
|
52
|
+
* @param versionInfo Version information
|
|
53
|
+
*/
|
|
54
|
+
|
|
55
|
+
export async function processSupportNotices(context: Context, statusInfo: SupportInfo | undefined, versionInfo: VersionInfo): Promise<void> {
|
|
56
|
+
if (!statusInfo || !versionInfo?.version) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const { version } = versionInfo;
|
|
61
|
+
const { logger } = context;
|
|
62
|
+
|
|
63
|
+
// TODO: ****************************************************************************************
|
|
64
|
+
// TODO: Check if the user is an admin if we are Prime - we only notify admins of EOM and EOL
|
|
65
|
+
// TODO: ****************************************************************************************
|
|
66
|
+
|
|
67
|
+
const status = statusInfo.status || {};
|
|
68
|
+
const majorMinor = `${ semver.major(version) }.${ semver.minor(version) }`;
|
|
69
|
+
|
|
70
|
+
// Check if this version is EOL - we warn of EOL
|
|
71
|
+
// If a version is EOL, then is has passed EOM, so we don't need to check that
|
|
72
|
+
if (status.eol && semver.satisfies(version, status.eol)) {
|
|
73
|
+
logger.info(`This version (${ version }) is End of Life`);
|
|
74
|
+
|
|
75
|
+
return await checkAndAddNotification(context, {
|
|
76
|
+
prefValuePrefix: PREFIX.EOL,
|
|
77
|
+
pref: READ_SUPPORT_NOTICE,
|
|
78
|
+
notificationPrefix: SUPPORT_NOTICE_PREFIX,
|
|
79
|
+
titleKey: 'dynamicContent.eol.title',
|
|
80
|
+
messageKey: 'dynamicContent.eol.message',
|
|
81
|
+
}, majorMinor);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (status.eom && semver.satisfies(version, status.eom)) {
|
|
85
|
+
logger.info(`This version (${ version }) is End of Maintenance`);
|
|
86
|
+
|
|
87
|
+
return await checkAndAddNotification(context, {
|
|
88
|
+
prefValuePrefix: PREFIX.EOM,
|
|
89
|
+
pref: READ_SUPPORT_NOTICE,
|
|
90
|
+
notificationPrefix: SUPPORT_NOTICE_PREFIX,
|
|
91
|
+
titleKey: 'dynamicContent.eom.title',
|
|
92
|
+
messageKey: 'dynamicContent.eom.message',
|
|
93
|
+
}, majorMinor);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Now check for upcoming EOL or EOM
|
|
97
|
+
|
|
98
|
+
// Upcoming EOL
|
|
99
|
+
if (statusInfo.upcoming?.eol && semver.satisfies(version, statusInfo.upcoming.eol.version)) {
|
|
100
|
+
if (await checkAndAddUpcomingNotification(context, statusInfo.upcoming.eol, {
|
|
101
|
+
prefValuePrefix: PREFIX.EOL,
|
|
102
|
+
pref: READ_UPCOMING_SUPPORT_NOTICE,
|
|
103
|
+
notificationPrefix: UPCOMING_SUPPORT_NOTICE_PREFIX,
|
|
104
|
+
titleKey: 'dynamicContent.upcomingEol.title',
|
|
105
|
+
messageKey: 'dynamicContent.upcomingEol.message',
|
|
106
|
+
}, majorMinor)) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Upcoming EOM
|
|
112
|
+
if (statusInfo.upcoming?.eom && semver.satisfies(version, statusInfo.upcoming.eom.version)) {
|
|
113
|
+
await checkAndAddUpcomingNotification(context, statusInfo.upcoming.eom, {
|
|
114
|
+
prefValuePrefix: PREFIX.EOM,
|
|
115
|
+
pref: READ_UPCOMING_SUPPORT_NOTICE,
|
|
116
|
+
notificationPrefix: UPCOMING_SUPPORT_NOTICE_PREFIX,
|
|
117
|
+
titleKey: 'dynamicContent.upcomingEom.title',
|
|
118
|
+
messageKey: 'dynamicContent.upcomingEom.message',
|
|
119
|
+
}, majorMinor);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function checkAndAddUpcomingNotification(context: Context, info: UpcomingSupportInfo, config: Config, majorMinor: string): Promise<boolean> {
|
|
124
|
+
const now = day(day().format(UPDATE_DATE_FORMAT));
|
|
125
|
+
const upcomingDate = day(day(info.date).format(UPDATE_DATE_FORMAT));
|
|
126
|
+
const distance = upcomingDate.diff(now, 'day');
|
|
127
|
+
const noticeWindow = info.noticeDays || DEFAULT_UPCOMING_WINDOW;
|
|
128
|
+
|
|
129
|
+
// If we've passed the upcoming date, then ignore, as this should have been covered by the eom status
|
|
130
|
+
if (distance > 0 && distance < noticeWindow) {
|
|
131
|
+
await checkAndAddNotification(context, config, majorMinor, distance);
|
|
132
|
+
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Check if a notification already exists or has already been read and if not, add it
|
|
141
|
+
*
|
|
142
|
+
* @param context Context helper providing access to config, logger, store
|
|
143
|
+
* @param config Configuration for the notification
|
|
144
|
+
* @param majorMinor Major.Minor version number
|
|
145
|
+
* @param distance Number of days until the event occurs (for upcoming support events)
|
|
146
|
+
*/
|
|
147
|
+
async function checkAndAddNotification(context: Context, config: Config, majorMinor: string, distance?: number) {
|
|
148
|
+
const { dispatch, getters, logger } = context;
|
|
149
|
+
const t = getters['i18n/t'];
|
|
150
|
+
const lastReadNotice = getters['prefs/get'](config.pref) || '';
|
|
151
|
+
const prefValue = config.prefValuePrefix ? `${ config.prefValuePrefix }-${ majorMinor }` : majorMinor;
|
|
152
|
+
|
|
153
|
+
if (!await removeMatchingNotifications(context, config.notificationPrefix, prefValue) && lastReadNotice !== prefValue) {
|
|
154
|
+
const notification = {
|
|
155
|
+
id: `${ config.notificationPrefix }${ prefValue }`,
|
|
156
|
+
level: NotificationLevel.Warning,
|
|
157
|
+
title: t(config.titleKey, { version: majorMinor, days: distance }),
|
|
158
|
+
message: t(config.messageKey, { version: majorMinor, days: distance }),
|
|
159
|
+
preference: {
|
|
160
|
+
key: config.pref,
|
|
161
|
+
value: prefValue
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
logger.info(`Adding support notification for ${ majorMinor } (${ config.notificationPrefix }${ config.prefValuePrefix })`);
|
|
166
|
+
|
|
167
|
+
await dispatch('notifications/add', notification);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared types for Dynamic Content
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { SemVer } from 'semver';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Logger interface for dynamic content
|
|
9
|
+
*/
|
|
10
|
+
export interface Logger {
|
|
11
|
+
error: Function,
|
|
12
|
+
info: Function,
|
|
13
|
+
debug: Function,
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Distribution type
|
|
18
|
+
*/
|
|
19
|
+
export type Distribution = 'community' | 'prime';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Dynamic Content configuration
|
|
23
|
+
*/
|
|
24
|
+
export type Configuration = {
|
|
25
|
+
enabled: boolean;
|
|
26
|
+
debug: boolean;
|
|
27
|
+
log: boolean;
|
|
28
|
+
endpoint: string;
|
|
29
|
+
prime: boolean;
|
|
30
|
+
distribution: Distribution;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Settings configuration that can be supplied in the dynamic content package
|
|
35
|
+
*/
|
|
36
|
+
export type SettingsInfo = {
|
|
37
|
+
releaseNotesUrl: string; // URL format to use when generating release note links for new releases
|
|
38
|
+
suseExtensions: string[]; // Names of extra SUSE UI extensions on top of the list built-in
|
|
39
|
+
debugVersion?: string;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Common context passed through various functions
|
|
44
|
+
*/
|
|
45
|
+
export type Context = {
|
|
46
|
+
dispatch: Function,
|
|
47
|
+
getters: any,
|
|
48
|
+
axios: any,
|
|
49
|
+
logger: Logger,
|
|
50
|
+
isAdmin: boolean,
|
|
51
|
+
config: Configuration,
|
|
52
|
+
settings: SettingsInfo,
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Version information
|
|
57
|
+
*/
|
|
58
|
+
export type VersionInfo = {
|
|
59
|
+
version: SemVer | null;
|
|
60
|
+
isPrime: boolean;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Information for a new release
|
|
65
|
+
*/
|
|
66
|
+
export type ReleaseInfo = {
|
|
67
|
+
name: string;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Information for an upcoming release
|
|
72
|
+
*/
|
|
73
|
+
export type UpcomingSupportInfo = {
|
|
74
|
+
version: string, // Version number: semver (e.g. '<= 2.12')
|
|
75
|
+
date: Date, // Date when the eom/eol takes place (note, only the day part is used, time is ignored)
|
|
76
|
+
noticeDays?: number, // Number of days in advance to notify the user (if not specified, the default is used)
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Support information covering current EOM/EOL and upcoming EOM/EOL information
|
|
81
|
+
*/
|
|
82
|
+
export type SupportInfo = {
|
|
83
|
+
status: {
|
|
84
|
+
eom: string,
|
|
85
|
+
eol: string,
|
|
86
|
+
},
|
|
87
|
+
upcoming: {
|
|
88
|
+
eom: UpcomingSupportInfo,
|
|
89
|
+
eol: UpcomingSupportInfo,
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Main type for the metadata that is retrieved from the dynamic content endpoint
|
|
95
|
+
*/
|
|
96
|
+
export type DynamicContent = {
|
|
97
|
+
version: string;
|
|
98
|
+
releases: ReleaseInfo[],
|
|
99
|
+
support: SupportInfo,
|
|
100
|
+
settings?: Partial<SettingsInfo>,
|
|
101
|
+
};
|