0
0
mirror of https://github.com/PostHog/posthog.git synced 2024-11-21 13:39:22 +01:00

feat(frontend): use lottie for animation (#9904)

* add lottie-react

* test loading states

* add new animations

* adopt the laptophog as the loadinghog

* handle opacity via a class

* move all lottiefiles to lib/animations

* new animation component

* add storybook

* use sportshog and laptophog animations for loading

* jest also wants to ignore these files

* clarify text

* support canvas in jsdom / jest

* add width/height to animations

* clarify

* use a mocked canvas instead of installing new debian packages to get this to compile

* I posted a wrong answer on the internet

Co-authored-by: Michael Matloka <dev@twixes.com>
This commit is contained in:
Marius Andra 2022-06-06 12:50:13 +02:00 committed by GitHub
parent 4431c09f22
commit 6ba92b4766
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 242 additions and 16 deletions

View File

@ -15,3 +15,9 @@ declare module '*.mp3' {
const content: any
export default content
}
// This fixes TS errors when importing an .lottie file
declare module '*.lottie' {
const content: any
export default content
}

View File

@ -0,0 +1,42 @@
import laptophog from './laptophog.lottie'
import musichog from './musichog.lottie'
import sportshog from './sportshog.lottie'
/**
* We're keeping lottiefiles in this folder.
*
* Even though these are `.json` files, we keep their filenames as `.lottie`. Doing otherwise makes prettier
* explode their size. We're just `fetch`-ing these files, so let's treat them as binaries.
*
* See more: https://lottiefiles.com/
*/
export enum AnimationType {
LaptopHog = 'laptophog',
MusicHog = 'musichog',
SportsHog = 'sportshog',
}
export const animations: Record<AnimationType, { url: string; width: number; height: number }> = {
laptophog: { url: laptophog, width: 800, height: 800 },
musichog: { url: musichog, width: 800, height: 800 },
sportshog: { url: sportshog, width: 800, height: 800 },
}
const animationCache: Record<string, Record<string, any>> = {}
const fetchCache: Record<string, Promise<Record<string, any>>> = {}
async function fetchJson(url: string): Promise<Record<string, any>> {
const response = await window.fetch(url)
return await response.json()
}
export async function getAnimationSource(animation: AnimationType): Promise<Record<string, any>> {
if (!animationCache[animation]) {
if (!fetchCache[animation]) {
fetchCache[animation] = fetchJson(animations[animation].url)
}
animationCache[animation] = await fetchCache[animation]
}
return animationCache[animation]
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,26 @@
.Animation {
width: 100%;
max-width: 300px;
// A correct aspect-ratio is be passed via a style prop. This is as a fallback.
aspect-ratio: 1 / 1;
overflow: hidden;
opacity: 1;
transition: 400ms ease opacity;
display: inline-flex;
align-items: center;
justify-content: center;
&.Animation--hidden {
opacity: 0;
}
.Animation__player {
display: block;
width: 100%;
height: 100%;
svg {
display: block;
}
}
}

View File

@ -0,0 +1,44 @@
import * as React from 'react'
import { animations, AnimationType } from '../../animations/animations'
import { Meta } from '@storybook/react'
import { LemonTable } from '../LemonTable'
import { Animation } from 'lib/components/Animation/Animation'
export default {
title: 'Layout/Animations',
parameters: {
options: { showPanel: false },
docs: {
description: {
component:
'Animations are [LottieFiles.com](https://lottiefiles.com/) animations that we load asynchronously.',
},
},
},
} as Meta
export function Animations(): JSX.Element {
return (
<LemonTable
dataSource={Object.keys(animations).map((key) => ({ key }))}
columns={[
{
title: 'Code',
key: 'code',
dataIndex: 'key',
render: function RenderCode(name) {
return <code>{`<Animation type="${name as string}" />`}</code>
},
},
{
title: 'Animation',
key: 'animation',
dataIndex: 'key',
render: function RenderAnimation(key) {
return <Animation type={key as AnimationType} />
},
},
]}
/>
)
}

View File

@ -0,0 +1,71 @@
import './Animation.scss'
import { Player } from '@lottiefiles/react-lottie-player'
import React, { useEffect, useState } from 'react'
import clsx from 'clsx'
import { AnimationType, getAnimationSource, animations } from 'lib/animations/animations'
import { Spinner } from 'lib/components/Spinner/Spinner'
export interface AnimationProps {
/** Animation to show */
type?: AnimationType
/** Milliseconds to wait before showing the animation. Can be 0, defaults to 300. */
delay?: number
className?: string
style?: React.CSSProperties
}
export function Animation({
className,
style,
delay = 300,
type = AnimationType.LaptopHog,
}: AnimationProps): JSX.Element {
const [visible, setVisible] = useState(delay === 0)
const [source, setSource] = useState<null | Record<string, any>>(null)
const [showFallbackSpinner, setShowFallbackSpinner] = useState(false)
const { width, height } = animations[type]
// Delay 300ms before showing Animation, to not confuse users with subliminal hedgehogs
// that flash before their eyes. Then take 400ms to fade in the animation.
useEffect(() => {
if (delay) {
const timeout = window.setTimeout(() => setVisible(true), delay)
return () => window.clearTimeout(timeout)
}
}, [delay])
// Actually fetch the animation. Uses a cache to avoid multiple requests for the same file.
// Show a fallback spinner if failed to fetch.
useEffect(() => {
let unmounted = false
async function loadAnimation(): Promise<void> {
try {
const source = await getAnimationSource(type)
!unmounted && setSource(source)
} catch (e) {
!unmounted && setShowFallbackSpinner(true)
}
}
loadAnimation()
return () => {
unmounted = true
}
}, [type])
return (
<div
className={clsx(
'Animation',
{ 'Animation--hidden': !(visible && (source || showFallbackSpinner)) },
className
)}
style={{ aspectRatio: `${width} / ${height}`, ...style }}
>
{source ? (
<Player className="Animation__player" autoplay loop src={source} />
) : showFallbackSpinner ? (
<Spinner />
) : null}
</div>
)
}

View File

@ -1,7 +1,7 @@
import { useActions, useValues } from 'kea'
import React from 'react'
import { PlusCircleOutlined, WarningOutlined } from '@ant-design/icons'
import { IconTrendUp, IconOpenInNew, IconErrorOutline } from 'lib/components/icons'
import { IconErrorOutline, IconOpenInNew, IconTrendUp } from 'lib/components/icons'
import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic'
import { funnelLogic } from 'scenes/funnels/funnelLogic'
import { entityFilterLogic } from 'scenes/insights/ActionFilter/entityFilterLogic'
@ -14,9 +14,10 @@ import { LemonButton } from 'lib/components/LemonButton'
import { deleteWithUndo } from 'lib/utils'
import { teamLogic } from 'scenes/teamLogic'
import './EmptyStates.scss'
import { Spinner } from 'lib/components/Spinner/Spinner'
import { urls } from 'scenes/urls'
import { Link } from 'lib/components/Link'
import { Animation } from 'lib/components/Animation/Animation'
import { AnimationType } from 'lib/animations/animations'
export function InsightEmptyState(): JSX.Element {
return (
@ -78,7 +79,9 @@ export function InsightTimeoutState({ isLoading }: { isLoading: boolean }): JSX.
return (
<div className="insight-empty-state warning">
<div className="empty-state-inner">
<div className="illustration-main">{isLoading ? <Spinner size="lg" /> : <IconErrorOutline />}</div>
<div className="illustration-main" style={{ height: 'auto' }}>
{isLoading ? <Animation type={AnimationType.SportsHog} /> : <IconErrorOutline />}
</div>
<h2>{isLoading ? 'Looks like things are a little slow…' : 'Your query took too long to complete'}</h2>
{isLoading ? (
<>

View File

@ -20,7 +20,6 @@ import {
InsightErrorState,
InsightTimeoutState,
} from 'scenes/insights/EmptyStates'
import { Loading } from 'lib/utils'
import { funnelLogic } from 'scenes/funnels/funnelLogic'
import clsx from 'clsx'
import { featureFlagLogic } from 'lib/logic/featureFlagLogic'
@ -32,6 +31,8 @@ import { Tooltip } from 'lib/components/Tooltip'
import { LemonButton } from 'lib/components/LemonButton'
import { IconExport } from 'lib/components/icons'
import { FunnelStepsTable } from './InsightTabs/FunnelTab/FunnelStepsTable'
import { Animation } from 'lib/components/Animation/Animation'
import { AnimationType } from 'lib/animations/animations'
const VIEW_MAP = {
[`${InsightType.TRENDS}`]: <TrendInsight view={InsightType.TRENDS} />,
@ -74,14 +75,9 @@ export function InsightContainer(
const BlockingEmptyState = (() => {
if (activeView !== loadedView || (insightLoading && !showTimeoutMessage)) {
return (
<>
{filters.display !== ChartDisplayType.ActionsTable &&
filters.display !== ChartDisplayType.WorldMap && (
/* Tables and world map don't need this padding, but graphs do for sizing */
<div className="trends-insights-container" />
)}
<Loading />
</>
<div className="text-center">
<Animation type={AnimationType.LaptopHog} />
</div>
)
}
// Insight specific empty states - note order is important here

View File

@ -135,6 +135,7 @@ export const commonConfig = {
'.woff': 'file',
'.woff2': 'file',
'.mp3': 'file',
'.lottie': 'file',
},
metafile: true,
}

View File

@ -82,7 +82,7 @@ export default {
// A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module
moduleNameMapper: {
'^.+\\.(css|less|scss|svg|png)$': '<rootDir>/test/mocks/styleMock.js',
'^.+\\.(css|less|scss|svg|png|lottie)$': '<rootDir>/test/mocks/styleMock.js',
'^~/(.*)$': '<rootDir>/$1',
'^lib/(.*)$': '<rootDir>/lib/$1',
'^scenes/(.*)$': '<rootDir>/scenes/$1',

View File

@ -1,3 +1,4 @@
import 'whatwg-fetch'
import 'jest-canvas-mock'
window.scrollTo = jest.fn()

View File

@ -61,6 +61,7 @@
"dependencies": {
"@babel/core": "^7.17.10",
"@babel/runtime": "^7.17.9",
"@lottiefiles/react-lottie-player": "^3.4.7",
"@monaco-editor/react": "^4.1.3",
"@popperjs/core": "^2.9.2",
"@posthog/chart.js": "^2.9.6",
@ -182,6 +183,7 @@
"html-webpack-plugin": "^4.5.2",
"husky": "^7.0.4",
"jest": "^26.6.3",
"jest-canvas-mock": "^2.4.0",
"kea-test-utils": "^0.2.2",
"kea-typegen": "^3.1.0",
"less": "^3.12.2",

View File

@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/no-var-requires */
/* global require, module, process, __dirname */
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
@ -106,7 +105,7 @@ function createEntry(entry) {
{
// Now we apply rule for images
test: /\.(png|jpe?g|gif|svg)$/,
test: /\.(png|jpe?g|gif|svg|lottie)$/,
use: [
{
// Using file-loader for these files

View File

@ -2657,6 +2657,13 @@
resolved "https://registry.yarnpkg.com/@juggle/resize-observer/-/resize-observer-3.3.1.tgz#b50a781709c81e10701004214340f25475a171a0"
integrity sha512-zMM9Ds+SawiUkakS7y94Ymqx+S0ORzpG3frZirN3l+UlXUmSUR7hF4wxCVqW+ei94JzV5kt0uXBcoOEAuiydrw==
"@lottiefiles/react-lottie-player@^3.4.7":
version "3.4.7"
resolved "https://registry.yarnpkg.com/@lottiefiles/react-lottie-player/-/react-lottie-player-3.4.7.tgz#c8c377e2bc8a636c2bf62ce95dd0e39f720b09c1"
integrity sha512-KqkwRiCQPDNzimsXnNSgeJjJlZQ6Fr9JE3OtZdOaGrXovZJ+zDeZNxIwxID8Up0eAdm4zJjudOSc5EJSiGw9RA==
dependencies:
lottie-web "^5.7.8"
"@maxmind/geoip2-node@^3.0.0":
version "3.4.0"
resolved "https://registry.yarnpkg.com/@maxmind/geoip2-node/-/geoip2-node-3.4.0.tgz#e0b2dd6e46931cbb84ccb46cbb154a5009e75bcc"
@ -6603,7 +6610,7 @@ color-name@1.1.3:
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=
color-name@^1.0.0, color-name@~1.1.4:
color-name@^1.0.0, color-name@^1.1.4, color-name@~1.1.4:
version "1.1.4"
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
@ -7135,6 +7142,11 @@ cssesc@^3.0.0:
resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee"
integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==
cssfontparser@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/cssfontparser/-/cssfontparser-1.2.1.tgz#f4022fc8f9700c68029d542084afbaf425a3f3e3"
integrity sha512-6tun4LoZnj7VN6YeegOVb67KBX/7JJsqvj+pv3ZA7F878/eN33AbGa5b/S/wXxS/tcp8nc40xRUrsPlxIyNUPg==
cssnano-preset-default@^4.0.7:
version "4.0.7"
resolved "https://registry.yarnpkg.com/cssnano-preset-default/-/cssnano-preset-default-4.0.7.tgz#51ec662ccfca0f88b396dcd9679cdb931be17f76"
@ -10779,6 +10791,14 @@ iterate-value@^1.0.2:
es-get-iterator "^1.0.2"
iterate-iterator "^1.0.1"
jest-canvas-mock@^2.4.0:
version "2.4.0"
resolved "https://registry.yarnpkg.com/jest-canvas-mock/-/jest-canvas-mock-2.4.0.tgz#947b71442d7719f8e055decaecdb334809465341"
integrity sha512-mmMpZzpmLzn5vepIaHk5HoH3Ka4WykbSoLuG/EKoJd0x0ID/t+INo1l8ByfcUJuDM+RIsL4QDg/gDnBbrj2/IQ==
dependencies:
cssfontparser "^1.2.1"
moo-color "^1.0.2"
jest-changed-files@^26.6.2:
version "26.6.2"
resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-26.6.2.tgz#f6198479e1cc66f22f9ae1e22acaa0b429c042d0"
@ -11802,6 +11822,11 @@ loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0:
dependencies:
js-tokens "^3.0.0 || ^4.0.0"
lottie-web@^5.7.8:
version "5.9.4"
resolved "https://registry.yarnpkg.com/lottie-web/-/lottie-web-5.9.4.tgz#c01478ee5dd47f916cb4bb79040c11d427872d57"
integrity sha512-bSs1ZTPifnBVejO1MnQHdfrKfcn02YTCmgOh2wcAEICqRA0V7GzDh8FnwXY6+EzT+i8XOunaIloo/5xC5YNbsA==
lower-case@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-2.0.1.tgz#39eeb36e396115cc05e29422eaea9e692c9408c7"
@ -12257,6 +12282,13 @@ moment@^2.10.2, moment@^2.24.0, moment@^2.25.3:
resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.2.tgz#00910c60b20843bcba52d37d58c628b47b1f20e4"
integrity sha512-UgzG4rvxYpN15jgCmVJwac49h9ly9NurikMWGPdVxm8GZD6XjkKPxDTjQQ43gtGgnV3X0cAyWDdP2Wexoquifg==
moo-color@^1.0.2:
version "1.0.3"
resolved "https://registry.yarnpkg.com/moo-color/-/moo-color-1.0.3.tgz#d56435f8359c8284d83ac58016df7427febece74"
integrity sha512-i/+ZKXMDf6aqYtBhuOcej71YSlbjT3wCO/4H1j8rPvxDJEifdwgg5MaFyu6iYAT8GBZJg2z0dkgK4YMzvURALQ==
dependencies:
color-name "^1.1.4"
move-concurrently@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/move-concurrently/-/move-concurrently-1.0.1.tgz#be2c005fda32e0b29af1f05d7c4b33214c701f92"