diff --git a/frontend/src/lib/components/home/ImportProjectModal.svelte b/frontend/src/lib/components/home/ImportProjectModal.svelte index 7d3f0a1406..445383f70d 100644 --- a/frontend/src/lib/components/home/ImportProjectModal.svelte +++ b/frontend/src/lib/components/home/ImportProjectModal.svelte @@ -57,9 +57,17 @@ function dismiss() { // Where they left, which is the half of the funnel Finish cannot report: `running` // walked out on an import in progress, `setup` on the credentials it asked for, - // `idle` opened the dialog and picked nothing up. + // `done` closed a landed import instead of pressing Finish, `idle` opened the dialog + // and picked nothing up. `done` is its own bucket because it is a normal exit — folded + // into `idle` it would read as people bouncing off a dialog they never used. if (!finishing) { - const stage = execution?.running ? 'running' : onSetupStep ? 'setup' : 'idle' + const stage: AbandonStage = execution?.running + ? 'running' + : onSetupStep + ? 'setup' + : execution?.done + ? 'done' + : 'idle' logFeatureUsage('home', 'template_abandon', { key: stage }) } if (execution?.running) { @@ -117,10 +125,17 @@ : undefined) ) + // Asked for on the first pick, not at init: this dialog is mounted by the home list for + // every user on every arrival, and the host is a string only the project card renders. let hubHost = $state('hub.windmill.dev') - void hubBrowserUrl() - .then((u) => (hubHost = new URL(u).host)) - .catch(() => {}) + let hubHostAsked = false + $effect(() => { + if (!slug || hubHostAsked) return + hubHostAsked = true + void hubBrowserUrl() + .then((u) => (hubHost = new URL(u).host)) + .catch(() => {}) + }) // Each open is its own import: a dialog reopened for another project must not inherit // the previous run, or its step would offer to resume a bundle from a different slug. @@ -132,21 +147,37 @@ } }) + // The counters' key vocabularies, enumerated here so the whole set is reviewable at once. + type AbandonStage = 'running' | 'setup' | 'done' | 'idle' + type SetupOutcome = 'filled' | 'skipped' | 'none' + type SetupBucket = 'filled' | 'none' | 'skipped_1' | 'skipped_2_5' | 'skipped_6plus' + // Set for the closing that Finish itself asks for, since that closing reaches `dismiss()` // by the same falling edge as the X. let finishing = false /** * How the credentials step ended, counted alongside the import itself: `filled` only when - * nothing was left outstanding — the step disables Finish until then — `skipped` carrying - * how many rows were walked away from, and `none` where the project asked for nothing. - * Skipping with one credential left and skipping with eight are different problems. + * nothing was left outstanding — the step disables Finish until then — `none` where the + * project asked for nothing, and a `skipped_*` bucket carrying roughly how many rows were + * walked away from, since skipping with one credential left and skipping with eight are + * different problems. + * + * A bucket rather than `value`: `value` is an increment, so counting rows there would make + * `skipped` a sum of rows while its siblings count imports — two units in one counter, and + * no way to recover filled-versus-skipped. */ - function finish(setupOutcome: 'filled' | 'skipped' | 'none', outstanding = 1) { + function setupKey(outcome: SetupOutcome, outstanding: number): SetupBucket { + if (outcome !== 'skipped') return outcome + if (outstanding <= 1) return 'skipped_1' + return outstanding <= 5 ? 'skipped_2_5' : 'skipped_6plus' + } + + function finish(setupOutcome: SetupOutcome, outstanding = 1) { // On the way out rather than on the pick: what is worth counting is an import that // landed, not a dialog that was opened and abandoned. if (slug) logFeatureUsage('home', 'template_import', { key: slug }) - logFeatureUsage('home', 'template_setup', { key: setupOutcome, value: outstanding }) + logFeatureUsage('home', 'template_setup', { key: setupKey(setupOutcome, outstanding) }) finishing = true onImported?.() onClose() diff --git a/frontend/src/lib/components/home/ItemsList.svelte b/frontend/src/lib/components/home/ItemsList.svelte index 8b47c19ef7..6efa27f733 100644 --- a/frontend/src/lib/components/home/ItemsList.svelte +++ b/frontend/src/lib/components/home/ItemsList.svelte @@ -975,15 +975,13 @@ // until the first response instead — it is fetched in parallel with the listing, // so it costs no extra wait in practice. Only the first load gates: `current` // survives a refetch, so an in-place scope change refreshes without flashing. + let treeCountsPending = $derived( + treeLazyMode && ownerCountsRes.current == undefined && ownerCountsRes.loading + ) + // An import just landed, so the rows about to replace the empty state are all new: they // fade in one after another rather than appearing as a finished list. Cleared on a timer // because nothing else marks the end — the reload resolves before the rows animate. - // The hub import, owned here rather than by either entry point: the empty state's link and - // the create menu's Import section open the same dialog, and mounting one per entry point - // would put two of them on the page at once while the workspace is still empty. - let hubPick = $state(undefined) - let hubPickerOpen = $state(false) - let justImported = $state(false) let justImportedTimer: ReturnType | undefined function onImported() { @@ -993,9 +991,11 @@ justImportedTimer = setTimeout(() => (justImported = false), 2500) } - let treeCountsPending = $derived( - treeLazyMode && ownerCountsRes.current == undefined && ownerCountsRes.loading - ) + // The hub import, owned here rather than by either entry point: the empty state's link and + // the create menu's Import section open the same dialog, and mounting one per entry point + // would put two of them on the page at once while the workspace is still empty. + let hubPick = $state(undefined) + let hubPickerOpen = $state(false) // The workspace itself holds nothing — no filter is narrowing the list away. It stays // false until the first load resolves: a skeleton already means "loading", and the diff --git a/frontend/src/routes/(root)/(logged)/user/(user)/workspaces/+page.svelte b/frontend/src/routes/(root)/(logged)/user/(user)/workspaces/+page.svelte index 21e194c2ee..a6400cab2c 100644 --- a/frontend/src/routes/(root)/(logged)/user/(user)/workspaces/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/user/(user)/workspaces/+page.svelte @@ -240,7 +240,8 @@ > {#snippet subtitleSnippet()} + accent action: leaving is not what anyone came here to do. Shown in both states; the + picker also carries it in the settings menu below, which the create state hides. --> Logged in as {$usersWorkspaceStore?.email} · @@ -481,8 +482,8 @@ {/if} + to do, so this row stands down for the create state. Logging out is in this menu as + well as on the subtitle line, which is the only one of the two the create state has. --> {#if !showCreate}
{#if $superadmin}