-
Notifications
You must be signed in to change notification settings - Fork 22
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
Add onUpdate event listener for installing starter blueprints on Playground urls #4249
Changes from 12 commits
61d9d76
841cfa8
814e831
121ce90
44847c0
e255c71
00af9ef
cd48429
9da961c
381c180
df0222f
84c0171
0d81973
dc0c521
efcf5df
5ba96b4
271a258
f35c539
c50cdf1
0ef4033
e72687f
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 |
---|---|---|
|
@@ -26,71 +26,122 @@ import reportError from "@/telemetry/reportError"; | |
|
||
const { reducer, actions } = extensionsSlice; | ||
|
||
function installStarterBlueprint( | ||
const PLAYGROUND_URL = "https://www.pixiebrix.com/playground"; | ||
|
||
function installBlueprint( | ||
state: ExtensionOptionsState, | ||
starterBlueprint: RecipeDefinition | ||
blueprint: RecipeDefinition | ||
): ExtensionOptionsState { | ||
return reducer( | ||
state, | ||
actions.installRecipe({ | ||
recipe: starterBlueprint, | ||
extensionPoints: starterBlueprint.extensionPoints, | ||
recipe: blueprint, | ||
extensionPoints: blueprint.extensionPoints, | ||
}) | ||
); | ||
} | ||
|
||
export async function installStarterBlueprints(): Promise<void> { | ||
async function installBlueprints( | ||
blueprints: RecipeDefinition[] | ||
): Promise<boolean> { | ||
let installed = false; | ||
if (blueprints.length === 0) { | ||
return installed; | ||
} | ||
|
||
let extensionsState = await loadOptions(); | ||
for (const blueprint of blueprints) { | ||
const blueprintAlreadyInstalled = extensionsState.extensions.some( | ||
(extension) => extension._recipe.id === blueprint.metadata.id | ||
); | ||
|
||
if (!blueprintAlreadyInstalled) { | ||
extensionsState = installBlueprint(extensionsState, blueprint); | ||
installed = true; | ||
} | ||
} | ||
|
||
await saveOptions(extensionsState); | ||
await forEachTab(queueReactivateTab); | ||
return installed; | ||
} | ||
|
||
async function getShouldFirstTimeInstall(): Promise<boolean> { | ||
const client = await maybeGetLinkedApiClient(); | ||
if (client == null) { | ||
console.debug( | ||
"Skipping starter blueprint installation because the extension is not linked to the PixieBrix service" | ||
); | ||
return; | ||
return false; | ||
} | ||
|
||
try { | ||
const { data: starterBlueprints } = await client.get<RecipeDefinition[]>( | ||
"/api/onboarding/starter-blueprints/" | ||
); | ||
const { | ||
data: { install_starter_blueprints: shouldInstall }, | ||
} = await client.get("/api/onboarding/starter-blueprints/install/"); | ||
|
||
// If the starter blueprint request fails for some reason, or the user's primary organization | ||
// gets removed, we'd still like to mark starter blueprints as installed for this user | ||
// so that they don't see onboarding views/randomly have starter blueprints installed | ||
// the next time they open the extension | ||
await client.post("/api/onboarding/starter-blueprints/install/"); | ||
|
||
if (starterBlueprints.length === 0) { | ||
return; | ||
if (shouldInstall) { | ||
// If the starter blueprint request fails for some reason, or the user's primary organization | ||
// gets removed, we'd still like to mark starter blueprints as installed for this user | ||
// so that they don't see onboarding views/randomly have starter blueprints installed | ||
// the next time they open the extension | ||
await client.post("/api/onboarding/starter-blueprints/install/"); | ||
} | ||
|
||
let extensionsState = await loadOptions(); | ||
return shouldInstall; | ||
} catch (error) { | ||
reportError(error); | ||
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. What is going to be returned from this function when it catches an error? I think 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. 👍 TypeScript in loose mode misses a lot of such warnings. It should have complained that the signature is 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. Thanks for catching this Ben! This was indeed a mistake. |
||
} | ||
} | ||
|
||
for (const starterBlueprint of starterBlueprints) { | ||
const blueprintAlreadyInstalled = extensionsState.extensions.some( | ||
(extension) => extension._recipe.id === starterBlueprint.metadata.id | ||
); | ||
async function getStarterBlueprints(): Promise<RecipeDefinition[]> { | ||
const client = await maybeGetLinkedApiClient(); | ||
if (client == null) { | ||
console.debug( | ||
"Skipping starter blueprint installation because the extension is not linked to the PixieBrix service" | ||
); | ||
return []; | ||
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. 💙 |
||
} | ||
|
||
if (!blueprintAlreadyInstalled) { | ||
extensionsState = installStarterBlueprint( | ||
extensionsState, | ||
starterBlueprint | ||
); | ||
} | ||
} | ||
try { | ||
const { data: starterBlueprints } = await client.get<RecipeDefinition[]>( | ||
"/api/onboarding/starter-blueprints/" | ||
); | ||
return starterBlueprints; | ||
} catch (error) { | ||
reportError(error); | ||
return []; | ||
} | ||
} | ||
|
||
async function installStarterBlueprints(): Promise<boolean> { | ||
const starterBlueprints = await getStarterBlueprints(); | ||
return installBlueprints(starterBlueprints); | ||
} | ||
|
||
export async function firstTimeInstallStarterBlueprints(): Promise<void> { | ||
const shouldInstall = await getShouldFirstTimeInstall(); | ||
if (!shouldInstall) { | ||
return; | ||
} | ||
|
||
await saveOptions(extensionsState); | ||
const installed = await installStarterBlueprints(); | ||
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. While unlikely, it could be called concurrently with |
||
|
||
await forEachTab(queueReactivateTab); | ||
if (installed) { | ||
void browser.tabs.create({ | ||
url: "https://www.pixiebrix.com/playground", | ||
url: PLAYGROUND_URL, | ||
}); | ||
} catch (error) { | ||
reportError(error); | ||
} | ||
} | ||
|
||
function initStarterBlueprints(): void { | ||
void installStarterBlueprints(); | ||
browser.tabs.onUpdated.addListener((tabId, changeInfo, tab) => { | ||
if (tab?.url?.startsWith(PLAYGROUND_URL)) { | ||
void installStarterBlueprints(); | ||
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 my comment got lost: This listener will run How about this pattern: async function initStarterBlueprints() {
if (await isInstalled()) {
return
}
if (await shouldInstall() || await onPlayGroundOpen()) {
install()
}
}
async function onPlayGroundOpen() {
return new Promise(resolve => {
const listener = (id, info, tab) => {
if (tab?.url?.startsWith(PLAYGROUND_URL)) {
browser.tabs.onUpdated.removeListener(listener)
resolve(true)
}
}
browser.tabs.onUpdated.addListener(listener)
})
} To note:
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. The function will run, but I think the check on line 54 prevents any blueprint from being installed twice, right? What kind of trouble are you imagining this will cause? Can you explain how your code example here avoids that trouble? 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.
At this point line 54 hasn't been reached yet, so any logic there is futile. My example avoids the trouble because it does call 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.
Some possibilities:
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. @fregante I'm not sure the pattern you suggested fits the needs of the feature, unless I'm misunderstanding your pseudocode. We need users to always have Starter Blueprints installed for them when visiting the Playground page, to catch the cases if they were to uninstall the Blueprints and revisit the Playground, or if an error like a network request failed to install them the first time. In this case, I think I might go with a locking (and/or maybe debounce?) pattern in order to solve the concerns you outlined above. 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 didn't think about that, although it would only be hit if they visit the playground, delete the blueprint, and then visit the playground again — all while never quitting the browser. I guess it's fine to debounce it to once per minute, that would work even better. 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. Thanks for the discussion Federico! Yeah, debounce sounds good to me also. 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. Yeah thanks federico! Great considerations here |
||
} | ||
}); | ||
|
||
void firstTimeInstallStarterBlueprints(); | ||
} | ||
|
||
export default initStarterBlueprints; |
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.
To ensure that it's not called undebounced (as it currently is on line 150), we can prepend
_
to the name.I generally inline the function in the
debounce
definition (debounce(() => {}, opts)
) but for long functions it's less readable.