@@ -174,7 +187,9 @@ function BeatRow({ beat, index }: { beat: Beat; index: number }): JSX.Element {
{/* Real inbox cards, marked as examples and kept inert by the preview wrapper.
Full-width on mobile; indented to align under the beat text from sm up. */}
-
{beat.preview}
+
+
+
{beat.subtext ? (
{beat.subtext}
) : null}
@@ -189,17 +204,33 @@ function BeatRow({ beat, index }: { beat: Beat; index: number }): JSX.Element {
* report list – a plain centered column (not itself a card) that eases in with a subtle scale + fade.
*/
export function InboxOnboardingTakeover(): JSX.Element {
+ const commandRef = useRef(null)
+
useEffect(() => {
captureInboxWelcomeViewed({ variant: 'control' })
}, [])
+ const highlightCommand = useCallback(() => {
+ const el = commandRef.current
+ if (!el) {
+ return
+ }
+ // Jump instead of animating the scroll when the user asked for reduced motion.
+ const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches
+ el.scrollIntoView({ behavior: reduceMotion ? 'auto' : 'smooth', block: 'center' })
+ // Restart the pulse on repeat clicks: drop the class, force a reflow, then add it back.
+ el.classList.remove('InboxOnboarding__commandPulse')
+ void el.offsetWidth
+ el.classList.add('InboxOnboarding__commandPulse')
+ }, [])
+
return (
-
+
{BEATS.map((beat, index) => (
-
+
))}
diff --git a/products/signals/frontend/inbox/components/onboarding/InboxOnboardingPreviews.tsx b/products/signals/frontend/inbox/components/onboarding/InboxOnboardingPreviews.tsx
index 337fd0a1b541..9b69c855ddc4 100644
--- a/products/signals/frontend/inbox/components/onboarding/InboxOnboardingPreviews.tsx
+++ b/products/signals/frontend/inbox/components/onboarding/InboxOnboardingPreviews.tsx
@@ -11,9 +11,9 @@ import { playMeep } from './meep'
* requests / Reports tabs use), fed mock data – so they read as the genuine article rather than a
* lookalike. Because they look real, they're marked plainly as examples: an "Example" tag sits on
* each card, the card itself is made non-interactive (so its Review/Archive buttons don't offer
- * live hover states or misleading tooltips), and a single click surface explains that the real work
- * arrives once you run the setup command. The meep stays as flair, but it's no longer the only sign
- * a click did anything.
+ * live hover states or misleading tooltips), and a single click surface sits on top. A click on that
+ * surface scrolls to the setup command and pulses it, so the click has a visible answer. The meep
+ * stays as flair on top of that.
*
* The sample work is a wink at Silicon Valley (the show): Pied Piper's middle-out compression and
* the ever-looming Hooli.
@@ -62,10 +62,19 @@ const REPORT_SAMPLE: Omit = {
* Wraps a real `ReportCard` and makes it legibly a sample. The card is rendered non-interactive
* (`pointer-events-none`, `aria-hidden`) so its Review/Archive buttons and links no longer offer
* live hover states or misleading tooltips ("Archive this report") that invite dead clicks. An
- * "Example" tag labels it at a glance, and a single click surface on top plays the meep flair while
- * a tooltip explains that real work lands here once the setup command runs.
+ * "Example" tag labels it at a glance, and a single click surface on top scrolls to the setup
+ * command and pulses it (via `onExampleClick`), so a click gets a visible answer rather than only a
+ * sound.
*/
-function PreviewCard({ report, tabKey }: { report: SignalReport; tabKey: 'pulls' | 'reports' }): JSX.Element {
+function PreviewCard({
+ report,
+ tabKey,
+ onExampleClick,
+}: {
+ report: SignalReport
+ tabKey: 'pulls' | 'reports'
+ onExampleClick: () => void
+}): JSX.Element {
return (
// `@container` so ReportCard's `@lg:` row layout resolves against the preview width (it has no
// inbox-list container here). `role="presentation"` – the whole thing is decorative.
@@ -83,26 +92,41 @@ function PreviewCard({ report, tabKey }: { report: SignalReport; tabKey: 'pulls'
Example
- {/* One interactive surface over the whole card: a click plays the meep flair, and the
- tooltip is the real signal – it says this is a preview and how to get the real thing. */}
-
+ {/* One interactive surface over the whole card. A click scrolls to the setup command and
+ pulses it, so the click has a visible answer, then plays the meep flair on top. */}
+
)
}
-export function PullRequestPreview(): JSX.Element {
+export function PullRequestPreview({ onExampleClick }: { onExampleClick: () => void }): JSX.Element {
const landed = landedHoursAgo(2)
- return
+ return (
+
+ )
}
-export function ReportPreview(): JSX.Element {
+export function ReportPreview({ onExampleClick }: { onExampleClick: () => void }): JSX.Element {
const landed = landedHoursAgo(4)
- return
+ return (
+
+ )
}
diff --git a/products/signals/frontend/inbox/signalSourcesLogic.test.ts b/products/signals/frontend/inbox/signalSourcesLogic.test.ts
index 2387ebfac7f5..14952124a96a 100644
--- a/products/signals/frontend/inbox/signalSourcesLogic.test.ts
+++ b/products/signals/frontend/inbox/signalSourcesLogic.test.ts
@@ -315,11 +315,11 @@ describe('signalSourcesLogic', () => {
})
logic.actions.enableSourceTool('error_tracking')
- expect(logic.values.enablingTool).toBe('error_tracking')
+ expect(logic.values.enablingTools.has('error_tracking')).toBe(true)
enablementResponse.resolve([200, { results: { error_tracking: 'enabled' } }])
await teamRequestStarted.promise
- expect(logic.values.enablingTool).toBe('error_tracking')
+ expect(logic.values.enablingTools.has('error_tracking')).toBe(true)
teamResponse.resolve([
200,
@@ -330,7 +330,26 @@ describe('signalSourcesLogic', () => {
])
await expectLogic(logic).toFinishAllListeners()
- expect(logic.values.enablingTool).toBeNull()
+ expect(logic.values.enablingTools.has('error_tracking')).toBe(false)
expect(logic.values.toolStatusBySource.error_tracking?.enabled).toBe(true)
})
+
+ it('tracks concurrent tool enables independently, so one finishing keeps the other loading', () => {
+ // Hang the enable request so the listener stays in flight; drive completion by hand to
+ // reproduce two badges enabled before either resolves.
+ const enableHangs = deferred<[number, { results: Record }]>()
+ useMocks({
+ post: {
+ '/api/projects/:team_id/product_enablement/': () => enableHangs.promise,
+ },
+ })
+
+ logic.actions.enableSourceTool('error_tracking')
+ logic.actions.enableSourceTool('conversations')
+ expect(logic.values.enablingTools).toEqual(new Set(['error_tracking', 'conversations']))
+
+ // The first request completing must clear only its own tool, not the other in-flight one.
+ logic.actions.enableSourceToolComplete('error_tracking')
+ expect(logic.values.enablingTools).toEqual(new Set(['conversations']))
+ })
})
diff --git a/products/signals/frontend/inbox/signalSourcesLogic.ts b/products/signals/frontend/inbox/signalSourcesLogic.ts
index 290378cca08b..74c808af49d3 100644
--- a/products/signals/frontend/inbox/signalSourcesLogic.ts
+++ b/products/signals/frontend/inbox/signalSourcesLogic.ts
@@ -211,7 +211,7 @@ export interface signalSourcesLogicValues {
conversationsConfig: SignalSourceConfig | null
dataSourceSetupSource: WarehouseBackedSource | null
enabledSourcesCount: number
- enablingTool: SourceToolEnablement | null
+ enablingTools: Set
errorTrackingIsFullyEnabled: boolean
errorTrackingTypeStates: {
enabled: boolean
@@ -265,8 +265,8 @@ export interface signalSourcesLogicActions {
enableSourceTool: (enablement: SourceToolEnablement) => {
enablement: SourceToolEnablement
}
- enableSourceToolComplete: () => {
- value: true
+ enableSourceToolComplete: (enablement: SourceToolEnablement) => {
+ enablement: SourceToolEnablement
}
initiateDataWarehouseSourceToggle: (source: WarehouseBackedSource) => {
source: WarehouseBackedSource
@@ -517,7 +517,7 @@ export const signalSourcesLogic = kea([
toggleConversations: true,
toggleAnomalyInvestigation: true,
enableSourceTool: (enablement: SourceToolEnablement) => ({ enablement }),
- enableSourceToolComplete: true,
+ enableSourceToolComplete: (enablement: SourceToolEnablement) => ({ enablement }),
}),
loaders(({ values }) => ({
@@ -626,11 +626,17 @@ export const signalSourcesLogic = kea([
closeSourcesModal: () => null,
},
],
- enablingTool: [
- null as SourceToolEnablement | null,
+ // A set, not a single slot: several sources can have their tool off and be enabled at once,
+ // so each in-flight enablement must keep its own loading state.
+ enablingTools: [
+ new Set(),
{
- enableSourceTool: (_, { enablement }) => enablement,
- enableSourceToolComplete: () => null,
+ enableSourceTool: (state, { enablement }) => new Set(state).add(enablement),
+ enableSourceToolComplete: (state, { enablement }) => {
+ const next = new Set(state)
+ next.delete(enablement)
+ return next
+ },
},
],
toolDataEventsFailed: [
@@ -1315,7 +1321,7 @@ export const signalSourcesLogic = kea([
} catch (error: any) {
lemonToast.error(error?.detail || error?.message || "Couldn't turn this on. Please try again.")
} finally {
- actions.enableSourceToolComplete()
+ actions.enableSourceToolComplete(enablement)
}
},
setDataWarehouseSourceEnabled: ({ source, enabled }) => {