From b86dd45c7bb550c2c46deaf7ee576cd718eda0e8 Mon Sep 17 00:00:00 2001 From: Ryan Vandersmith Date: Thu, 7 Sep 2023 14:49:06 -0600 Subject: [PATCH] Dynamically change installer origin when loading projects from various sources (#193) * Convert 'allowedOrigins' to TypeScript * Add 'setOrigin' workplace reducer action * Progress * Refactor example projects to use example name in origin * Misc * Use URL origin in place of editor key for 'playground:post' * Reintroduce details for 'tag' and 'git' origins * Keep track of 'document.referrer' and 'window.opener' * Misc --- service/pool/Logs.mo | 7 ++-- service/pool/Main.mo | 6 +-- src/App.tsx | 22 ++++++++++- src/build.ts | 17 ++++++--- src/components/DeployModal.tsx | 5 ++- src/components/ProjectModal.tsx | 20 ++++++++-- src/contexts/WorkplaceState.ts | 17 ++++++++- src/examples.ts | 38 ++++++++++++------- .../{allowedOrigins.js => allowedOrigins.ts} | 2 +- src/integrations/editorIntegration.ts | 12 +++++- 10 files changed, 111 insertions(+), 35 deletions(-) rename src/integrations/{allowedOrigins.js => allowedOrigins.ts} (90%) diff --git a/service/pool/Logs.mo b/service/pool/Logs.mo index c87e2b79..86d5b3cd 100644 --- a/service/pool/Logs.mo +++ b/service/pool/Logs.mo @@ -20,15 +20,16 @@ module { case (?n) { canisters.put(origin, n + 1) }; } }; - public func addInstall(origin: Text) { + public func addInstall(origin: Text, referrer: ?Text) { + // TODO: keep track of `referrer` switch (installs.get(origin)) { case null { installs.put(origin, 1) }; case (?n) { installs.put(origin, n + 1) }; } }; public func dump() : ([(Text, Nat)], [(Text, Nat)]) { - (canisters.entries() |> toArray<(Text, Nat)>(_), - installs.entries() |> toArray<(Text, Nat)>(_)) + (toArray<(Text, Nat)>(canisters.entries()), + toArray<(Text, Nat)>(installs.entries())) }; public func metrics() : Text { var result = ""; diff --git a/service/pool/Main.mo b/service/pool/Main.mo index c282a205..13349c52 100644 --- a/service/pool/Main.mo +++ b/service/pool/Main.mo @@ -135,7 +135,7 @@ shared (creator) actor class Self(opt_params : ?Types.InitParams) = this { await getExpiredCanisterInfo(origin); }; - type InstallConfig = { profiling: Bool; is_whitelisted: Bool; origin: Text }; + type InstallConfig = { profiling: Bool; is_whitelisted: Bool; origin: Text; referrer: ?Text }; public shared ({ caller }) func installCode(info : Types.CanisterInfo, args : Types.InstallArgs, install_config : InstallConfig) : async Types.CanisterInfo { if (install_config.origin == "") { throw Error.reject "Please specify an origin"; @@ -169,7 +169,7 @@ shared (creator) actor class Self(opt_params : ?Types.InitParams) = this { }; await IC.install_code newArgs; stats := Logs.updateStats(stats, #install); - statsByOrigin.addInstall(install_config.origin); + statsByOrigin.addInstall(install_config.origin, install_config.referrer); switch (pool.refresh(info, install_config.profiling)) { case (?newInfo) { updateTimer(newInfo); @@ -335,7 +335,7 @@ shared (creator) actor class Self(opt_params : ?Types.InitParams) = this { switch (sanitizeInputs(caller, canister_id)) { case (#ok info) { let args = { arg; wasm_module; mode; canister_id }; - let config = { profiling = pool.profiling caller; is_whitelisted = false; origin = "spawned" }; + let config = { profiling = pool.profiling caller; is_whitelisted = false; origin = "spawned"; referrer = null }; ignore await installCode(info, args, config); // inherit the profiling of the parent }; case (#err makeMsg) throw Error.reject(makeMsg "install_code"); diff --git a/src/App.tsx b/src/App.tsx index 7134e636..9582535e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -18,6 +18,7 @@ import { getActorAliases, getDeployedCanisters, getShareableProject, + WorkplaceReducerAction, } from "./contexts/WorkplaceState"; import { ProjectModal } from "./components/ProjectModal"; import { DeployModal, DeploySetter } from "./components/DeployModal"; @@ -70,13 +71,21 @@ const hasUrlParams = !!( urlParams.get("post") ); async function fetchFromUrlParams( - dispatch: (WorkplaceReducerAction) => void + dispatch: (action: WorkplaceReducerAction) => void ): Promise | undefined> { const git = urlParams.get("git"); const tag = urlParams.get("tag"); const editorKey = urlParams.get("post"); if (editorKey) { - return setupEditorIntegration(editorKey, dispatch, worker); + const result = await setupEditorIntegration(editorKey, dispatch, worker); + if (result) { + const { origin, files } = result; + await dispatch({ + type: "setOrigin", + payload: { origin: `playground:post:${origin}` }, + }); + return files; + } } if (git) { const repo = { @@ -84,6 +93,10 @@ async function fetchFromUrlParams( branch: urlParams.get("branch") || "main", dir: urlParams.get("dir") || "", }; + await dispatch({ + type: "setOrigin", + payload: { origin: `playground:git:${git}` }, + }); return await worker.fetchGithub(repo); } if (tag) { @@ -132,6 +145,10 @@ async function fetchFromUrlParams( }); } } + await dispatch({ + type: "setOrigin", + payload: { origin: `playground:tag:${tag}` }, + }); return files; } } @@ -290,6 +307,7 @@ export function App() { candid={candidCode} initTypes={initTypes} logger={logger} + origin={workplaceState.origin} /> { try { logger.log(`Deploying code...`); @@ -126,7 +127,8 @@ export async function deploy( args, "install", profiling, - logger + logger, + origin ); } else { if (mode !== "reinstall" && mode !== "upgrade") { @@ -138,7 +140,8 @@ export async function deploy( args, mode, profiling, - logger + logger, + origin ); } //updatedState.candid = candid_source; @@ -175,11 +178,13 @@ async function install( args: Uint8Array, mode: string, profiling: boolean, - logger: ILoggingStore + logger: ILoggingStore, + origin: string | undefined ): Promise { if (!canisterInfo) { throw new Error("no canister id"); } + origin ||= "playground"; const canisterId = canisterInfo.id; const installArgs = { arg: [...args], @@ -190,7 +195,8 @@ async function install( const installConfig = { profiling, is_whitelisted: false, - origin: "playground", + origin, + referrer: document.referrer || (window.opener && "(opener)") || undefined, }; const new_info = await backend.installCode( canisterInfo, @@ -199,6 +205,7 @@ async function install( ); canisterInfo = new_info; logger.log(`Code installed at canister id ${canisterInfo.id}`); + console.log("Installed with origin:", origin); return canisterInfo; } diff --git a/src/components/DeployModal.tsx b/src/components/DeployModal.tsx index 2b84d388..8ba8ea5b 100644 --- a/src/components/DeployModal.tsx +++ b/src/components/DeployModal.tsx @@ -100,6 +100,7 @@ interface DeployModalProps { candid: string; initTypes: Array; logger: ILoggingStore; + origin: string | undefined; } const MAX_CANISTERS = 3; @@ -116,6 +117,7 @@ export function DeployModal({ candid, initTypes, logger, + origin, }: DeployModalProps) { const [canisterName, setCanisterName] = useState(""); const [inputs, setInputs] = useState([]); @@ -244,7 +246,8 @@ export function DeployModal({ mode, compileResult.wasm, profiling, - logger + logger, + origin ); await isDeploy(false); if (info) { diff --git a/src/components/ProjectModal.tsx b/src/components/ProjectModal.tsx index 219b4b1d..bd9feefc 100644 --- a/src/components/ProjectModal.tsx +++ b/src/components/ProjectModal.tsx @@ -9,7 +9,10 @@ import { Tab, Tabs } from "./shared/Tabs"; import { ImportGitHub } from "./ImportGithub"; import { fetchExample, exampleProjects, ExampleProject } from "../examples"; -import { WorkerContext } from "../contexts/WorkplaceState"; +import { + WorkerContext, + WorkplaceDispatchContext, +} from "../contexts/WorkplaceState"; import iconCaretRight from "../assets/images/icon-caret-right.svg"; const ModalContainer = styled.div` @@ -63,16 +66,25 @@ export function ProjectModal({ isFirstOpen, }: ProjectModalProps) { const worker = useContext(WorkerContext); + const dispatch = useContext(WorkplaceDispatchContext); async function handleSelectProjectAndClose(project: ExampleProject) { const files = await fetchExample(worker, project); if (files) { await importCode(files); close(); } + await dispatch({ + type: "setOrigin", + payload: { origin: `playground:example:${project.name}` }, + }); } async function emptyProject() { await importCode({ "Main.mo": "" }); close(); + await dispatch({ + type: "setOrigin", + payload: { origin: "playground:new" }, + }); } const welcomeText = ( @@ -123,12 +135,12 @@ export function ProjectModal({ New Motoko project - {Object.entries(exampleProjects).map(([name, project]) => ( + {exampleProjects.map((project) => ( handleSelectProjectAndClose(project)} > - {name} + {project.name} ))} diff --git a/src/contexts/WorkplaceState.ts b/src/contexts/WorkplaceState.ts index ea5281eb..6348f7a3 100644 --- a/src/contexts/WorkplaceState.ts +++ b/src/contexts/WorkplaceState.ts @@ -8,6 +8,7 @@ export interface WorkplaceState { canisters: Record; selectedCanister: string | null; packages: Record; + origin: string | undefined; } export function getActorAliases( canisters: Record @@ -121,9 +122,15 @@ export type WorkplaceReducerAction = payload: { /** path of file that should be updated. Should correspond to a property in state.files */ canister: CanisterInfo; - do_not_select?: bool; + do_not_select?: boolean; /** new contents of file */ }; + } + | { + type: "setOrigin"; + payload: { + origin: string | undefined; + }; }; function selectFirstFile(files: Record): string | null { @@ -157,6 +164,7 @@ export const workplaceReducer = { canisters, selectedCanister: null, packages: {}, + origin: undefined, }; }, /** Return updated state based on an action */ @@ -221,6 +229,13 @@ export const workplaceReducer = { }, }; } + case "setOrigin": { + const { origin } = action.payload; + return { + ...state, + origin, + }; + } default: // this should never be reached. If there is a type error here, add a 'case' // eslint-disable-next-line @typescript-eslint/no-unused-vars diff --git a/src/examples.ts b/src/examples.ts index 41105d30..cd68bc4d 100644 --- a/src/examples.ts +++ b/src/examples.ts @@ -1,6 +1,7 @@ import { RepoInfo } from "./workers/file"; export interface ExampleProject { + name: string; repo: RepoInfo; readme?: string; } @@ -12,52 +13,63 @@ const example = { const readmeURL = "https://raw.githubusercontent.com/dfinity/examples/master/motoko"; -export const exampleProjects: Record = { - "Hello, world": { +export const exampleProjects: ExampleProject[] = [ + { + name: "Hello, world", repo: { dir: "motoko/echo/src", ...example }, readme: `${readmeURL}/echo/README.md`, }, - Counter: { + { + name: "Counter", repo: { dir: "motoko/counter/src", ...example }, readme: `${readmeURL}/counter/README.md`, }, - Calculator: { + { + name: "Calculator", repo: { dir: "motoko/calc/src", ...example }, readme: `${readmeURL}/calc/README.md`, }, - "Who am I?": { + { + name: "Who am I?", repo: { dir: "motoko/whoami/src", ...example }, readme: `${readmeURL}/whoami/README.md`, }, - "Phone Book": { + { + name: "Phone Book", repo: { dir: "motoko/phone-book/src/phone-book", ...example }, readme: `${readmeURL}/phone-book/README.md`, }, - "Super Heroes": { + { + name: "Super Heroes", repo: { dir: "motoko/superheroes/src/superheroes", ...example }, readme: `${readmeURL}/superheroes/README.md`, }, - "Random Maze": { + { + name: "Random Maze", repo: { dir: "motoko/random_maze/src/random_maze", ...example }, readme: `${readmeURL}/random_maze/README.md`, }, - "Game of Life": { + { + name: "Game of Life", repo: { dir: "motoko/life", ...example }, readme: `${readmeURL}/life/README.md`, }, - "Publisher and Subscriber": { + { + name: "Publisher and Subscriber", repo: { dir: "motoko/pub-sub/src", ...example }, readme: `${readmeURL}/pub-sub/README.md`, }, - "Actor Classes": { + { + name: "Actor Classes", repo: { dir: "motoko/classes/src", ...example }, readme: `${readmeURL}/classes/README.md`, }, - "Basic DAO": { + { + name: "Basic DAO", repo: { dir: "motoko/basic_dao/src", ...example }, readme: `${readmeURL}/basic_dao/README.md`, }, -}; +]; export async function fetchExample( worker, diff --git a/src/integrations/allowedOrigins.js b/src/integrations/allowedOrigins.ts similarity index 90% rename from src/integrations/allowedOrigins.js rename to src/integrations/allowedOrigins.ts index 2fd5c20f..f8193b03 100644 --- a/src/integrations/allowedOrigins.js +++ b/src/integrations/allowedOrigins.ts @@ -2,7 +2,7 @@ // please submit a PR including the URL prefix for your application. // Read more: https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage#security_concerns -const ALLOWED_ORIGINS = [ +const ALLOWED_ORIGINS: (string | RegExp)[] = [ /^https?:\/\/(localhost|127\.0\.0\.1)(:[0-9]+)?$/, // Localhost "https://blocks-editor.github.io", // Blocks (visual Motoko smart contract editor) ]; diff --git a/src/integrations/editorIntegration.ts b/src/integrations/editorIntegration.ts index f43ada0f..7c0799fe 100644 --- a/src/integrations/editorIntegration.ts +++ b/src/integrations/editorIntegration.ts @@ -18,6 +18,11 @@ type EditorIntegrationResponse = { acknowledge: number; }; +export interface EditorIntegrationResult { + origin: string; + files: Record; +} + export const INTEGRATION_HOOKS: Partial = {}; // Cached return value to ensure at most one initialization @@ -35,7 +40,7 @@ export async function setupEditorIntegration( editorKey: string, dispatch: (WorkplaceReducerAction) => void, worker // MocWorker -): Promise | undefined> { +): Promise { if (previousResult) { return previousResult; } @@ -111,7 +116,10 @@ export async function setupEditorIntegration( // Load a default empty project previousResult = { - "Main.mo": "", + origin, + files: { + "Main.mo": "", + }, }; return previousResult; }