-
Notifications
You must be signed in to change notification settings - Fork 407
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: added notification when scratch org is about to expire #4304
Changes from 10 commits
d2deae5
f34796a
f7faf05
7e921ea
405cdc7
21b9cb1
790e2de
35d7284
255c020
a038e88
74b0398
8551c57
e7d4406
db7fb0a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -105,7 +105,7 @@ import { isSfdxProjectOpened } from './predicates'; | |
import { registerPushOrDeployOnSave, sfdxCoreSettings } from './settings'; | ||
import { taskViewService } from './statuses'; | ||
import { showTelemetryMessage, telemetryService } from './telemetry'; | ||
import { isCLIInstalled } from './util'; | ||
import { isCLIInstalled, setUpOrgExpirationWatcher } from './util'; | ||
import { OrgAuthInfo } from './util/authInfo'; | ||
|
||
const flagOverwrite: FlagParameter<string> = { | ||
|
@@ -661,27 +661,7 @@ export async function activate(extensionContext: vscode.ExtensionContext) { | |
); | ||
|
||
if (sfdxProjectOpened) { | ||
await workspaceContext.initialize(extensionContext); | ||
|
||
// register org picker commands | ||
const orgList = new OrgList(); | ||
extensionContext.subscriptions.push(registerOrgPickerCommands(orgList)); | ||
|
||
await setupOrgBrowser(extensionContext); | ||
await setupConflictView(extensionContext); | ||
|
||
PersistentStorageService.initialize(extensionContext); | ||
|
||
// Register filewatcher for push or deploy on save | ||
|
||
await registerPushOrDeployOnSave(); | ||
decorators.showOrg(); | ||
decorators.monitorOrgConfigChanges(); | ||
|
||
// Demo mode Decorator | ||
if (isDemoMode()) { | ||
decorators.showDemoMode(); | ||
} | ||
await initializeProject(extensionContext); | ||
} | ||
|
||
// Commands | ||
|
@@ -733,6 +713,31 @@ export async function activate(extensionContext: vscode.ExtensionContext) { | |
return api; | ||
} | ||
|
||
async function initializeProject(extensionContext: vscode.ExtensionContext) { | ||
await workspaceContext.initialize(extensionContext); | ||
|
||
// Register org picker commands | ||
const orgList = new OrgList(); | ||
extensionContext.subscriptions.push(registerOrgPickerCommands(orgList)); | ||
|
||
await setupOrgBrowser(extensionContext); | ||
await setupConflictView(extensionContext); | ||
|
||
PersistentStorageService.initialize(extensionContext); | ||
|
||
// Register file watcher for push or deploy on save | ||
await registerPushOrDeployOnSave(); | ||
decorators.showOrg(); | ||
decorators.monitorOrgConfigChanges(); | ||
|
||
await setUpOrgExpirationWatcher(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @randi274 Also, I initially had the impl for |
||
|
||
// Demo mode decorator | ||
if (isDemoMode()) { | ||
decorators.showDemoMode(); | ||
} | ||
} | ||
|
||
export function deactivate(): Promise<void> { | ||
console.log('SFDX CLI Extension Deactivated'); | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -643,5 +643,11 @@ export const messages = { | |
'Unable to rename the component. Try renaming the component manually and then redeploying your changes.', | ||
error_function_type: 'Unable to determine type of executing function.', | ||
error_unable_to_get_started_function: | ||
'Unable to access the function in "{0}".' | ||
'Unable to access the function in "{0}".', | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This text was provided by @sbudhirajadoc |
||
pending_org_expiration_expires_on_message: | ||
'%s\n(expires on %s)', | ||
pending_org_expiration_notification_message: | ||
'Warning: One or more of your orgs expire in the next %s days. For more details, review the Output panel.', | ||
pending_org_expiration_output_channel_message: | ||
'Warning: The following orgs expire in the next %s days:\n\n%s\n\nIf these orgs contain critical data or settings, back them up before the org expires.' | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
/* | ||
* Copyright (c) 2022, salesforce.com, inc. | ||
* All rights reserved. | ||
* Licensed under the BSD 3-Clause license. | ||
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause | ||
*/ | ||
|
||
import { Aliases } from '@salesforce/core'; | ||
import { channelService } from '../channels'; | ||
import { nls } from '../messages'; | ||
import { notificationService } from '../notifications'; | ||
import { FileInfo, OrgList } from '../orgPicker'; | ||
|
||
export async function setUpOrgExpirationWatcher() { | ||
// Run once to start off with. | ||
await checkForExpiredOrgs(); | ||
|
||
/* | ||
Comment this out for now. For now, we are only going to check once on activation, | ||
however it would be helpful if we also checked once a day. If we decide to also | ||
check once a day, uncomment the following code. | ||
|
||
// And run again once every 24 hours. | ||
setInterval(async () => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This would be good, especially for users that keep their projects open. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ^THIS! - I 100% agree, but Ananya doesn't. An argument she brought up was that users complain that our extension is too noisy (too many notifications). The plan is to release w/only displaying on activation, and then see what users think. |
||
await checkForExpiredOrgs(); | ||
}, 1000 * 60 * 60 * 24); | ||
*/ | ||
} | ||
|
||
export async function checkForExpiredOrgs() { | ||
try { | ||
const daysBeforeExpire = 5; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this something we could put behind a custom setting, and maybe allow for no reminder? I usually made my scratch orgs last 7 days (some folks I know make theirs only for 1-2), so it would be a bit more frequent/intrusive. Also cool if that's a better fit for a subsequent item 👍 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it would be good for a followup item. Let's see how the users like this feature, and how they use it. If we add the "also check daily" feature in, it would be good then to revisit this and maybe add a custom setting. I think for right now, I'd like to not make all the options user configurable (too many options = too many choices for the user) |
||
const today = new Date(); | ||
const daysUntilExpiration = new Date(); | ||
daysUntilExpiration.setDate(daysUntilExpiration.getDate() + daysBeforeExpire); | ||
|
||
const orgList = new OrgList(); | ||
const authInfoObjects = await orgList.getAuthInfoObjects(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In the draft PR that updates orgList to not read from the .sfdx directory, the return type of this method changes to return the OrgAuthorization type from core. FileInfo is a type that is created after reading the files in the ~/.sfdx directory. That change would require some reworking of this methods internals, to work with OrgAuthorization type and the AuthFields type for auth fields that are stored there in core v3, like expiration date: https://github.com/forcedotcom/salesforcedx-vscode/pull/4297/files#diff-2b72c15ab3c2e3d66f269772e80aed9902d2d85d9acb2a462116d03541484d4fR97 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe this file could import and use AuthInfo.listAllAuthorizations() There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good find @klewis-sfdc! Looks like this one will be behind the core v3 change in the queue. @jeffb-sfdc did you want to merge that work in now, or wait til we finish sorting out all of the larger issues? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've spoke with Ken about this, and the plan is to merge in now and not be held up, then pull develop into his branch and then address this issue. |
||
|
||
const orgsAboutToExpire = authInfoObjects!.filter((authInfoObject: FileInfo) => { | ||
// Filter out the dev hubs. | ||
if (authInfoObject.isDevHub) { | ||
return false; | ||
} | ||
|
||
// Some dev hubs have isDevHub=false, but no expiration date, so filter them out. | ||
if (!authInfoObject.expirationDate) { | ||
return false; | ||
} | ||
|
||
// Filter out the expired orgs. | ||
const expirationDate = new Date(authInfoObject.expirationDate); | ||
if (expirationDate < today) { | ||
return false; | ||
} | ||
|
||
// Now filter and only return the results that are within the 5 day window. | ||
return expirationDate <= daysUntilExpiration; | ||
}); | ||
|
||
if (orgsAboutToExpire.length < 1) { | ||
return; | ||
} | ||
|
||
const defaultOptions = Aliases.getDefaultOptions(); | ||
const aliases = await Aliases.create(defaultOptions); | ||
|
||
const formattedOrgsToDisplay = orgsAboutToExpire.map((orgAboutToExpire: any) => { | ||
const alias = aliases.getKeysByValue(orgAboutToExpire.username); | ||
const aliasName = alias.length > 0 | ||
? alias.toString() | ||
: orgAboutToExpire.username; | ||
|
||
return nls.localize('pending_org_expiration_expires_on_message', aliasName, orgAboutToExpire.expirationDate); | ||
}).join('\n\n'); | ||
|
||
notificationService.showWarningMessage( | ||
nls.localize('pending_org_expiration_notification_message', daysBeforeExpire) | ||
); | ||
channelService.appendLine( | ||
nls.localize('pending_org_expiration_output_channel_message', daysBeforeExpire, formattedOrgsToDisplay) | ||
); | ||
channelService.showChannelOutput(); | ||
} catch (err) { | ||
console.error(err); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,189 @@ | ||
/* | ||
* Copyright (c) 2022, salesforce.com, inc. | ||
* All rights reserved. | ||
* Licensed under the BSD 3-Clause license. | ||
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause | ||
*/ | ||
|
||
// Imports for testing | ||
import { expect } from 'chai'; | ||
import { createSandbox, SinonSandbox, SinonSpy } from 'sinon'; | ||
import * as vscode from 'vscode'; | ||
import { checkForExpiredOrgs } from '../../../src/util'; | ||
|
||
// Imports from the target source file | ||
import { Aliases } from '@salesforce/core'; | ||
import { channelService } from '../../../src/channels'; | ||
import { OrgList } from '../../../src/orgPicker'; | ||
|
||
describe('orgUtil tests', () => { | ||
let sb: SinonSandbox; | ||
|
||
beforeEach(() => { | ||
sb = createSandbox(); | ||
}); | ||
|
||
afterEach(async () => { | ||
sb.restore(); | ||
}); | ||
|
||
describe('checkForExpiredOrgs tests', () => { | ||
let today: Date; | ||
let showWarningMessageSpy: SinonSpy; | ||
let appendLineSpy: SinonSpy; | ||
let showChannelOutputSpy: SinonSpy; | ||
|
||
beforeEach(() => { | ||
today = new Date(); | ||
showWarningMessageSpy = sb.spy(vscode.window, 'showWarningMessage'); | ||
appendLineSpy = sb.spy(channelService, 'appendLine'); | ||
showChannelOutputSpy = sb.spy(channelService, 'showChannelOutput'); | ||
}); | ||
|
||
afterEach(async () => { | ||
showWarningMessageSpy.restore(); | ||
appendLineSpy.restore(); | ||
showChannelOutputSpy.restore(); | ||
}); | ||
|
||
it('should not display a notification when no orgs are present', async () => { | ||
const getAuthInfoObjectsStub = sb | ||
.stub(OrgList.prototype, 'getAuthInfoObjects') | ||
.resolves([]); | ||
|
||
await checkForExpiredOrgs(); | ||
|
||
expect(showWarningMessageSpy.called).to.equal(false); | ||
expect(appendLineSpy.called).to.equal(false); | ||
expect(showChannelOutputSpy.called).to.equal(false); | ||
|
||
getAuthInfoObjectsStub.restore(); | ||
}); | ||
|
||
it('should not display a notification when dev hubs are present', async () => { | ||
const getAuthInfoObjectsStub = sb | ||
.stub(OrgList.prototype, 'getAuthInfoObjects') | ||
.resolves([ | ||
{ | ||
isDevHub: true | ||
}, | ||
{ | ||
isDevHub: false, | ||
expirationDate: undefined | ||
} | ||
]); | ||
|
||
await checkForExpiredOrgs(); | ||
|
||
expect(showWarningMessageSpy.called).to.equal(false); | ||
expect(appendLineSpy.called).to.equal(false); | ||
expect(showChannelOutputSpy.called).to.equal(false); | ||
|
||
getAuthInfoObjectsStub.restore(); | ||
}); | ||
|
||
it('should not display a notification when the scratch org has already expired', async () => { | ||
const getAuthInfoObjectsStub = sb | ||
.stub(OrgList.prototype, 'getAuthInfoObjects') | ||
.resolves([ | ||
{ | ||
isDevHub: false, | ||
expirationDate: `${today.getFullYear()}-${today.getMonth() + 1}-${today.getDate() - 1}` | ||
} | ||
]); | ||
|
||
await checkForExpiredOrgs(); | ||
|
||
expect(showWarningMessageSpy.called).to.equal(false); | ||
expect(appendLineSpy.called).to.equal(false); | ||
expect(showChannelOutputSpy.called).to.equal(false); | ||
|
||
getAuthInfoObjectsStub.restore(); | ||
}); | ||
|
||
it('should display a notification when the scratch org is about to expire', async () => { | ||
const getAuthInfoObjectsStub = sb | ||
.stub(OrgList.prototype, 'getAuthInfoObjects') | ||
.resolves([ | ||
{ | ||
isDevHub: false, | ||
expirationDate: `${today.getFullYear()}-${today.getMonth() + 1}-${today.getDate() + 3}`, | ||
username: 'foo' | ||
} | ||
]); | ||
const getDefaultOptionsStub = sb | ||
.stub(Aliases, 'getDefaultOptions') | ||
.returns({ | ||
defaultGroup: 'orgs', | ||
filename: 'alias.json', | ||
isGlobal: true, | ||
isState: true | ||
}); | ||
const orgName = 'dreamhouse-org'; | ||
const createStub = sb | ||
.stub(Aliases, 'create') | ||
.resolves({ | ||
getKeysByValue: () => { | ||
return orgName; | ||
} | ||
}); | ||
|
||
await checkForExpiredOrgs(); | ||
|
||
expect(showWarningMessageSpy.called).to.equal(true); | ||
expect(appendLineSpy.called).to.equal(true); | ||
expect(appendLineSpy.args[0][0]).to.contain(orgName); | ||
expect(showChannelOutputSpy.called).to.equal(true); | ||
|
||
getAuthInfoObjectsStub.restore(); | ||
getDefaultOptionsStub.restore(); | ||
createStub.restore(); | ||
}); | ||
|
||
it('should display multiple orgs in the output when there are several scratch orgs about to expire', async () => { | ||
const getAuthInfoObjectsStub = sb | ||
.stub(OrgList.prototype, 'getAuthInfoObjects') | ||
.resolves([ | ||
{ | ||
isDevHub: false, | ||
expirationDate: `${today.getFullYear()}-${today.getMonth() + 1}-${today.getDate() + 3}`, | ||
username: 'foo' | ||
}, | ||
{ | ||
isDevHub: false, | ||
expirationDate: `${today.getFullYear()}-${today.getMonth() + 1}-${today.getDate() + 4}`, | ||
username: 'bar' | ||
} | ||
]); | ||
const getDefaultOptionsStub = sb | ||
.stub(Aliases, 'getDefaultOptions') | ||
.returns({ | ||
defaultGroup: 'orgs', | ||
filename: 'alias.json', | ||
isGlobal: true, | ||
isState: true | ||
}); | ||
const orgName1 = 'dreamhouse-org'; | ||
const orgName2 = 'ebikes-lwc'; | ||
const createStub = sb | ||
.stub(Aliases, 'create') | ||
.resolves({ | ||
getKeysByValue: (key: string) => { | ||
return (key === 'foo') ? orgName1 : orgName2; | ||
} | ||
}); | ||
|
||
await checkForExpiredOrgs(); | ||
|
||
expect(showWarningMessageSpy.called).to.equal(true); | ||
expect(appendLineSpy.called).to.equal(true); | ||
expect(appendLineSpy.args[0][0]).to.contain(orgName1); | ||
expect(appendLineSpy.args[0][0]).to.contain(orgName2); | ||
expect(showChannelOutputSpy.called).to.equal(true); | ||
|
||
getAuthInfoObjectsStub.restore(); | ||
getDefaultOptionsStub.restore(); | ||
createStub.restore(); | ||
}); | ||
}); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍🏽