diff --git a/cypress/e2e/dashboard-shared.cy.ts b/cypress/e2e/dashboard-shared.cy.ts index 755297f5c52dd..986ea46b1f8ed 100644 --- a/cypress/e2e/dashboard-shared.cy.ts +++ b/cypress/e2e/dashboard-shared.cy.ts @@ -61,7 +61,8 @@ describe('Shared dashboard', () => { }) cy.get('.InsightCard').should('have.length', 6) + // Make sure no element with text "There are no matching events for this query" exists - cy.get('.insight-empty-state').should('not.exist') + cy.get('[data-attr="insight-empty-state"]').should('not.exist') }) }) diff --git a/cypress/e2e/insights-navigation-open-directly.cy.ts b/cypress/e2e/insights-navigation-open-directly.cy.ts index 1a2ab16fe7c95..ec350aaa67758 100644 --- a/cypress/e2e/insights-navigation-open-directly.cy.ts +++ b/cypress/e2e/insights-navigation-open-directly.cy.ts @@ -27,7 +27,7 @@ describe('Insights', () => { it('can open a new funnels insight', () => { insight.newInsight('FUNNELS') - cy.get('.funnels-empty-state__title').should('exist') + cy.get('[data-attr="insight-empty-state"]').find('h2').should('exist') }) it('can open a new paths insight', () => { diff --git a/cypress/e2e/insights-navigation-open-sql-insight-first.cy.ts b/cypress/e2e/insights-navigation-open-sql-insight-first.cy.ts index a902e861bacd6..9f07607a3b4c0 100644 --- a/cypress/e2e/insights-navigation-open-sql-insight-first.cy.ts +++ b/cypress/e2e/insights-navigation-open-sql-insight-first.cy.ts @@ -40,7 +40,7 @@ describe('Insights', () => { it('can open a new funnels insight', () => { insight.clickTab('FUNNELS') - cy.get('.funnels-empty-state__title').should('exist') + cy.get('[data-attr="insight-empty-state"]').find('h2').should('exist') }) it('can open a new retention insight', () => { diff --git a/cypress/e2e/insights-saved.cy.ts b/cypress/e2e/insights-saved.cy.ts index adaf2bf3c59c0..0ecd06b1f31c6 100644 --- a/cypress/e2e/insights-saved.cy.ts +++ b/cypress/e2e/insights-saved.cy.ts @@ -13,7 +13,8 @@ describe('Insights - saved', () => { }) cy.task('resetInsightCache').then(() => { cy.visit(urls.insightView(newInsightId)) // Full refresh - cy.get('.insight-empty-state').should('exist') // There should be a loading state for a moment + + cy.get('[data-attr="insight-empty-state"]').should('exist') // There should be a loading state for a moment cy.wait('@getInsightsRefreshAsync').then(() => { cy.get('[data-attr=trend-line-graph]').should('exist') }) diff --git a/frontend/src/lib/components/Cards/InsightCard/InsightCard.scss b/frontend/src/lib/components/Cards/InsightCard/InsightCard.scss index 1a56aa13c8fd4..c2549c2b8616a 100644 --- a/frontend/src/lib/components/Cards/InsightCard/InsightCard.scss +++ b/frontend/src/lib/components/Cards/InsightCard/InsightCard.scss @@ -37,13 +37,6 @@ padding: 0.5rem; } - .insight-empty-state { - height: 100%; // Fix wonkiness when SpinnerOverlay is shown - padding-top: 0; - padding-bottom: 0; - font-size: 0.875rem; // Reduce font size - } - .LemonTable { background: none; border: none; diff --git a/frontend/src/queries/nodes/InsightViz/InsightVizDisplay.tsx b/frontend/src/queries/nodes/InsightViz/InsightVizDisplay.tsx index 6f6531a3669ac..09ad1d61ada55 100644 --- a/frontend/src/queries/nodes/InsightViz/InsightVizDisplay.tsx +++ b/frontend/src/queries/nodes/InsightViz/InsightVizDisplay.tsx @@ -83,11 +83,7 @@ export function InsightVizDisplay({ // Empty states that completely replace the graph const BlockingEmptyState = (() => { if (insightDataLoading) { - return ( -
- -
- ) + return } if (validationError) { diff --git a/frontend/src/queries/nodes/WebVitals/WebVitalsContent.tsx b/frontend/src/queries/nodes/WebVitals/WebVitalsContent.tsx index 6d79435cfd0eb..c3242a3f0f2b5 100644 --- a/frontend/src/queries/nodes/WebVitals/WebVitalsContent.tsx +++ b/frontend/src/queries/nodes/WebVitals/WebVitalsContent.tsx @@ -43,11 +43,7 @@ export const WebVitalsContent = ({ webVitalsQueryResponse }: WebVitalsContentPro // NOTE: `band` will only return `none` if the value is undefined, // so this is basically the same check twice, but we need that to make TS happy if (value === undefined || band === 'none') { - return ( -
- -
- ) + return } const grade = GRADE_PER_BAND[band] @@ -60,7 +56,7 @@ export const WebVitalsContent = ({ webVitalsQueryResponse }: WebVitalsContentPro const experience = EXPERIENCE_PER_BAND[band] return ( -
+
{LONG_METRIC_NAME[webVitalsTab]} diff --git a/frontend/src/scenes/data-warehouse/editor/OutputPane.tsx b/frontend/src/scenes/data-warehouse/editor/OutputPane.tsx index 5c4d23dd069b9..2d23653387e54 100644 --- a/frontend/src/scenes/data-warehouse/editor/OutputPane.tsx +++ b/frontend/src/scenes/data-warehouse/editor/OutputPane.tsx @@ -367,7 +367,9 @@ const Content = ({ } return responseLoading ? ( - +
+ +
) : !response ? (
Query results will appear here diff --git a/frontend/src/scenes/insights/EmptyStates/EmptyStates.scss b/frontend/src/scenes/insights/EmptyStates/EmptyStates.scss index de2ede18a2fb8..542643ddd3ef5 100644 --- a/frontend/src/scenes/insights/EmptyStates/EmptyStates.scss +++ b/frontend/src/scenes/insights/EmptyStates/EmptyStates.scss @@ -1,50 +1,4 @@ -.insight-empty-state { - display: flex; - flex-direction: column; - flex-grow: 1; - align-items: center; - justify-content: center; - padding: 1rem; - font-size: 1.1em; - color: var(--muted); - - &.error { - h2 { - color: var(--danger); - } - } - - &.warning { - h2 { - color: var(--warning); - } - } - - h2 { - font-size: 1.5rem; - font-weight: 600; - line-height: 1.6rem; - color: var(--primary-alt); - } - - .empty-state-inner { - display: flex; - flex-direction: column; - align-items: center; - max-width: 600px; - - .illustration-main { - font-size: 2.5rem; - } - - h2 { - width: 100%; - text-align: center; - word-wrap: break-word; - } - - ol { - margin: 0.5rem 0; - } - } +/* TODO: Migrate this to a CSS variable (with a `bg-` Tailwind class) once @adamleithp finishes his Tailwind work */ +.insights-empty-state { + background-color: rgb(129 129 129 / 20%); } diff --git a/frontend/src/scenes/insights/EmptyStates/EmptyStates.stories.tsx b/frontend/src/scenes/insights/EmptyStates/EmptyStates.stories.tsx index 8a0e5c91af302..aae87f4f8f46c 100644 --- a/frontend/src/scenes/insights/EmptyStates/EmptyStates.stories.tsx +++ b/frontend/src/scenes/insights/EmptyStates/EmptyStates.stories.tsx @@ -18,7 +18,7 @@ const meta: Meta = { layout: 'fullscreen', viewMode: 'story', testOptions: { - waitForSelector: '.empty-state-inner', + waitForSelector: '[data-attr="insight-empty-state"]', }, }, } diff --git a/frontend/src/scenes/insights/EmptyStates/EmptyStates.tsx b/frontend/src/scenes/insights/EmptyStates/EmptyStates.tsx index da29f417bf1f3..89ccc054d8219 100644 --- a/frontend/src/scenes/insights/EmptyStates/EmptyStates.tsx +++ b/frontend/src/scenes/insights/EmptyStates/EmptyStates.tsx @@ -1,15 +1,8 @@ import './EmptyStates.scss' -import { - IconArchive, - IconInfo, - IconPieChart, - IconPlus, - IconPlusSmall, - IconPlusSquare, - IconWarning, -} from '@posthog/icons' +import { IconArchive, IconPieChart, IconPlus, IconPlusSmall, IconPlusSquare, IconWarning } from '@posthog/icons' import { LemonButton } from '@posthog/lemon-ui' +import clsx from 'clsx' import { useActions, useValues } from 'kea' import { BuilderHog3 } from 'lib/components/hedgehogs' import { supportLogic } from 'lib/components/Support/supportLogic' @@ -18,7 +11,7 @@ import { IconErrorOutline, IconOpenInNew } from 'lib/lemon-ui/icons' import { Link } from 'lib/lemon-ui/Link' import { LoadingBar } from 'lib/lemon-ui/LoadingBar' import { Tooltip } from 'lib/lemon-ui/Tooltip' -import { humanFriendlyNumber } from 'lib/utils' +import { humanFriendlyNumber, humanizeBytes } from 'lib/utils' import posthog from 'posthog-js' import { useEffect, useState } from 'react' import { funnelDataLogic } from 'scenes/funnels/funnelDataLogic' @@ -46,12 +39,13 @@ export function InsightEmptyState({ detail?: string }): JSX.Element { return ( -
-
- -

{heading}

-

{detail}

-
+
+ +

{heading}

+

{detail}

) } @@ -65,6 +59,7 @@ function SamplingLink({ insightProps }: { insightProps: InsightLogicProps }): JS placement="bottom" > { setSamplingPercentage(suggestedSamplingPercentage) posthog.capture('sampling_enabled_on_slow_query', { @@ -77,29 +72,80 @@ function SamplingLink({ insightProps }: { insightProps: InsightLogicProps }): JS ) } -function humanFileSize(size: number): string { - const i = size == 0 ? 0 : Math.floor(Math.log(size) / Math.log(1024)) - return (+(size / Math.pow(1024, i))).toFixed(2) + ' ' + ['B', 'kB', 'MB', 'GB', 'TB'][i] + +function QueryIdDisplay({ + queryId, + compact = false, +}: { + queryId?: string | null + compact?: boolean +}): JSX.Element | null { + if (queryId == null) { + return null + } + + return ( +
+ Query ID: {queryId} +
+ ) } +function QueryDebuggerButton({ query }: { query?: Record | null }): JSX.Element | null { + if (!query) { + return null + } + + return ( + + Open in query debugger + + ) +} + +const LOADING_MESSAGES = [ + 'Crunching through hogloads of data…', + 'Teaching hedgehogs to count…', + 'Waking up the hibernating data hogs…', + 'Polishing graphs with tiny hedgehog paws…', + 'Rolling through data like a spiky ball of insights…', + 'Gathering nuts and numbers from the data forest…', +] + export function StatelessInsightLoadingState({ queryId, pollResponse, suggestion, + compact = false, }: { queryId?: string | null pollResponse?: Record | null suggestion?: JSX.Element + compact?: boolean }): JSX.Element { const [rowsRead, setRowsRead] = useState(0) const [bytesRead, setBytesRead] = useState(0) const [secondsElapsed, setSecondsElapsed] = useState(0) + const [loadingMessageIndex, setLoadingMessageIndex] = useState(() => + Math.floor(Math.random() * LOADING_MESSAGES.length) + ) + const [isLoadingMessageVisible, setIsLoadingMessageVisible] = useState(true) + useEffect(() => { const status = pollResponse?.status?.query_progress const previousStatus = pollResponse?.previousStatus?.query_progress setRowsRead(previousStatus?.rows_read || 0) setBytesRead(previousStatus?.bytes_read || 0) + const interval = setInterval(() => { setRowsRead((rowsRead) => { const diff = (status?.rows_read || 0) - (previousStatus?.rows_read || 0) @@ -117,6 +163,30 @@ export function StatelessInsightLoadingState({ return () => clearInterval(interval) }, [pollResponse]) + // Toggle between loading messages every 3 seconds, with 300ms fade out, then change text, keep in sync with the transition duration below + useEffect(() => { + const TOGGLE_INTERVAL = 3000 + const FADE_OUT_DURATION = 300 + + const interval = setInterval(() => { + setIsLoadingMessageVisible(false) + setTimeout(() => { + setLoadingMessageIndex((current) => { + // Attempt to do random messages, but don't do the same message twice + let newIndex = Math.floor(Math.random() * LOADING_MESSAGES.length) + if (newIndex === current) { + newIndex = (newIndex + 1) % LOADING_MESSAGES.length + } + + return newIndex + }) + setIsLoadingMessageVisible(true) + }, FADE_OUT_DURATION) + }, TOGGLE_INTERVAL) + + return () => clearInterval(interval) + }, []) + const bytesPerSecond = (bytesRead / (secondsElapsed || 1)) * 1000 const estimatedRows = pollResponse?.status?.query_progress?.estimated_rows_total @@ -126,39 +196,46 @@ export function StatelessInsightLoadingState({ 10000 return ( -
-
-

Crunching through hogloads of data…

- -

- {rowsRead > 0 && bytesRead > 0 && ( - <> - {humanFriendlyNumber(rowsRead || 0, 0)}{' '} - {estimatedRows && estimatedRows >= rowsRead - ? `/ ${humanFriendlyNumber(estimatedRows)} ` - : null}{' '} - rows -
- {humanFileSize(bytesRead || 0)} ({humanFileSize(bytesPerSecond || 0)}/s) -
- CPU {humanFriendlyNumber(cpuUtilization, 0)}% - +

+
+ + > + {LOADING_MESSAGES[loadingMessageIndex]} + {suggestion ? ( suggestion ) : ( -
- +

Need to speed things up? Try reducing the date range.

)} - {queryId ? ( -
- Query ID: {queryId} -
- ) : null}
+ + +

+ {rowsRead > 0 && bytesRead > 0 && ( + <> + {humanFriendlyNumber(rowsRead || 0, 0)} + + {estimatedRows && estimatedRows >= rowsRead ? ( + / ${humanFriendlyNumber(estimatedRows)} + ) : null} + + rows +
+ {humanizeBytes(bytesRead || 0)} + ({humanizeBytes(bytesPerSecond || 0)}/s) +
+ CPU {humanFriendlyNumber(cpuUtilization, 0)}% + + )} +

+ +
) } @@ -178,48 +255,41 @@ export function InsightLoadingState({ currentTeam?.modifiers?.personsOnEventsMode ?? currentTeam?.default_modifiers?.personsOnEventsMode ?? 'disabled' return ( -
- - {personsOnEventsMode === 'person_id_override_properties_joined' ? ( - <> - -

- You can speed this query up by changing the{' '} - person properties mode{' '} - setting. -

- - ) : ( - <> - -

- {suggestedSamplingPercentage && !samplingPercentage ? ( - - Need to speed things up? Try reducing the date range, removing breakdowns, - or turning on . - - ) : suggestedSamplingPercentage && samplingPercentage ? ( - <> - Still waiting around? You must have lots of data! Kick it up a notch with{' '} - . Or try reducing the date range - and removing breakdowns. - - ) : ( - <> - Need to speed things up? Try reducing the date range or removing breakdowns. - - )} -

- - )} -
- } - /> -
+ + {personsOnEventsMode === 'person_id_override_properties_joined' ? ( + <> +

+ You can speed this query up by changing the{' '} + person properties mode setting. +

+ + ) : ( + <> +

+ {suggestedSamplingPercentage && !samplingPercentage ? ( + + Need to speed things up? Try reducing the date range, removing breakdowns, or + turning on to speed things up. + + ) : suggestedSamplingPercentage && samplingPercentage ? ( + <> + Still waiting around? You must have lots of data! Kick it up a notch with{' '} + . Or try reducing the date range and + removing breakdowns. + + ) : ( + <>Need to speed things up? Try reducing the date range or removing breakdowns. + )} +

+ + )} +
+ } + /> ) } @@ -227,37 +297,61 @@ export function InsightTimeoutState({ queryId }: { queryId?: string | null }): J const { openSupportForm } = useActions(supportLogic) return ( -
-
- <> -
- -
-

Your query took too long to complete

- -
- -

- <> - Sometimes this happens. Try refreshing the page, reducing the date range, or removing - breakdowns. If you're still having issues,{' '} - { - openSupportForm({ kind: 'bug', target_area: 'analytics' }) - }} - > - let us know - - . - -

-
- {queryId ? ( -
- Query ID: {queryId} -
- ) : null} +
+

+ + Your query took too long to complete +

+ +
+ Sometimes this happens. Try refreshing the page, reducing the date range, or removing breakdowns. If + you're still having issues,{' '} + { + openSupportForm({ kind: 'bug', target_area: 'analytics' }) + }} + > + let us know + + .
+ + +
+ ) +} + +export function InsightValidationError({ + detail, + query, +}: { + detail: string + query?: Record | null +}): JSX.Element { + return ( +
+

+ + There is a problem with this query + {/* Note that this phrasing above signals the issue is not intermittent, */} + {/* but rather that it's something with the definition of the query itself */} +

+ +

{detail}

+ + + {detail.includes('Exclusion') && ( +
+ + Learn more about funnels in PostHog docs + + +
+ )}
) } @@ -278,53 +372,42 @@ export function InsightErrorState({ excludeDetail, title, query, queryId }: Insi } return ( -
-
-
- -
-

{title || 'There was a problem completing this query'}

+
+

+ + + {title || There was a problem completing this query} + {/* Note that this default phrasing above signals the issue is intermittent, */} {/* and that perhaps the query will complete on retry */} - {!excludeDetail && ( -
- We apologize for this unexpected situation. There are a couple of things you can do: -
    -
  1. - First and foremost you can try again. We recommend you wait a moment before doing - so. -
  2. -
  3. - { - openSupportForm({ kind: 'bug', target_area: 'analytics' }) - }} - > - If this persists, submit a bug report. - -
  4. -
-
- )} - {queryId && ( -
- Query ID: {queryId} -
- )} - {query && ( - - Open in query debugger - - )} -

+ + + {!excludeDetail && ( +
+ We apologize for this unexpected situation. There are a couple of things you can do: +
    +
  1. + First and foremost you can try again. We recommend you wait a moment before doing so. +
  2. +
  3. + { + openSupportForm({ kind: 'bug', target_area: 'analytics' }) + }} + > + If this persists, submit a bug report. + +
  4. +
+
+ )} + + +
) } @@ -346,90 +429,43 @@ export function FunnelSingleStepState({ actionable = true }: FunnelSingleStepSta const { addFilter } = useActions(entityFilterLogic({ setFilters, filters, typeKey: 'EditFunnel-action' })) return ( -
-
-
- -
-

Add another step!

-

- You’re almost there! Funnels require at least two steps before calculating. - {actionable && - ' Once you have two steps defined, additional changes will recalculate automatically.'} -

+
+
+ +
+

Add another step!

+

+ You're almost there! Funnels require at least two steps before calculating. {actionable && ( -

- addFilter()} - data-attr="add-action-event-button-empty-state" - icon={} - > - Add funnel step - -
+ <> +
+ Once you have two steps defined, additional changes will recalculate automatically. + )} -
- + {actionable && ( +
+ addFilter()} + data-attr="add-action-event-button-empty-state" + icon={} > - Learn more about funnels in PostHog docs - + Add funnel step +
-
-
- ) -} - -export function InsightValidationError({ - detail, - query, -}: { - detail: string - query?: Record | null -}): JSX.Element { - return ( -
-
-
- -
-

- There is a problem with this query - {/* Note that this phrasing above signals the issue is not intermittent, */} - {/* but rather that it's something with the definition of the query itself */} -

-

{detail}

- {query ? ( -

- - Open in query debugger - -

- ) : null} - {detail.includes('Exclusion') && ( -
- - Learn more about funnels in PostHog docs - - -
- )} + )} +
+ + Learn more about funnels in PostHog docs +
) @@ -462,41 +498,41 @@ export function SavedInsightsEmptyState(): JSX.Element { const { title, description } = SAVED_INSIGHTS_COPY[tab] ?? {} return ( -
-
-
- -
-

- {usingFilters - ? searchString - ? title.replace('$CONDITION', `matching "${searchString}"`) - : title.replace('$CONDITION', `matching these filters`) - : title.replace('$CONDITION', 'for this project')} -

- {usingFilters ? ( -

- Refine your keyword search, or try using other filters such as type, last modified or created - by. -

- ) : ( -

{description}

- )} - {tab !== SavedInsightsTabs.Favorites && ( -
- - } - className="add-insight-button" - > - New insight - - -
- )} +
+
+
+

+ {usingFilters + ? searchString + ? title.replace('$CONDITION', `matching "${searchString}"`) + : title.replace('$CONDITION', `matching these filters`) + : title.replace('$CONDITION', 'for this project')} +

+ {usingFilters ? ( +

+ Refine your keyword search, or try using other filters such as type, last modified or created by. +

+ ) : ( +

{description}

+ )} + {tab !== SavedInsightsTabs.Favorites && ( +
+ + } + className="add-insight-button" + > + New insight + + +
+ )}
) } diff --git a/frontend/src/scenes/saved-insights/SavedInsights.scss b/frontend/src/scenes/saved-insights/SavedInsights.scss index 6c8dcebef7afd..187161c0e4cdc 100644 --- a/frontend/src/scenes/saved-insights/SavedInsights.scss +++ b/frontend/src/scenes/saved-insights/SavedInsights.scss @@ -16,33 +16,6 @@ align-items: center; justify-content: center; text-align: center; - - .insight-empty-state__wrapper { - max-width: 600px; - margin-top: 5rem; - margin-bottom: 17rem; - - .illustration-main { - margin-bottom: 1rem; - font-size: 5rem; - line-height: 1em; - color: var(--border); - text-align: center; - } - - .empty-state__title { - font-size: 1.5rem; - font-weight: 600; - line-height: 1.6rem; - } - - .empty-state__description { - font-size: 1rem; - font-weight: 500; - line-height: 1.7rem; - color: var(--muted); - } - } } }