From b0848c9726df3c0ac085846be2e50bddd4b2fb60 Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Mon, 11 Mar 2024 14:46:53 +0000 Subject: [PATCH] Empty state: Add animation for Grot (#83770) * add animation for grot not found * extract default interval out into constant * improve relative path * extract magic numbers out into constants + throttle properly * better width/height definitions * use consistent background + apply to PageNotFound as well * increase gap in command palette empty state --- .../components/GrotNotFound/GrotNotFound.tsx | 62 +++++++++ .../GrotNotFound/useMousePosition.ts | 29 ++++ .../PageNotFound/EntityNotFound.tsx | 10 +- .../features/commandPalette/EmptyState.tsx | 6 +- public/img/grot-404-dark.svg | 126 +++++++++--------- public/img/grot-404-light.svg | 126 +++++++++--------- public/img/grot-not-found.svg | 29 ---- 7 files changed, 233 insertions(+), 155 deletions(-) create mode 100644 public/app/core/components/GrotNotFound/GrotNotFound.tsx create mode 100644 public/app/core/components/GrotNotFound/useMousePosition.ts delete mode 100644 public/img/grot-not-found.svg diff --git a/public/app/core/components/GrotNotFound/GrotNotFound.tsx b/public/app/core/components/GrotNotFound/GrotNotFound.tsx new file mode 100644 index 00000000000..f0c4c16f68c --- /dev/null +++ b/public/app/core/components/GrotNotFound/GrotNotFound.tsx @@ -0,0 +1,62 @@ +import { css } from '@emotion/css'; +import React, { SVGProps } from 'react'; +import SVG from 'react-inlinesvg'; + +import { GrafanaTheme2 } from '@grafana/data'; +import { useStyles2, useTheme2 } from '@grafana/ui'; + +import dark404 from '../../../../img/grot-404-dark.svg'; +import light404 from '../../../../img/grot-404-light.svg'; + +import useMousePosition from './useMousePosition'; + +const MIN_ARM_ROTATION = -20; +const MAX_ARM_ROTATION = 5; +const MIN_ARM_TRANSLATION = -5; +const MAX_ARM_TRANSLATION = 5; + +export interface Props { + width?: SVGProps['width']; + height?: SVGProps['height']; + show404?: boolean; +} + +export const GrotNotFound = ({ width = 'auto', height, show404 = false }: Props) => { + const theme = useTheme2(); + const { x, y } = useMousePosition(); + const styles = useStyles2(getStyles, x, y, show404); + return ; +}; + +GrotNotFound.displayName = 'GrotNotFound'; + +const getStyles = (theme: GrafanaTheme2, xPos: number | null, yPos: number | null, show404: boolean) => { + const { innerWidth, innerHeight } = window; + const heightRatio = yPos && yPos / innerHeight; + const widthRatio = xPos && xPos / innerWidth; + const rotation = heightRatio !== null ? getIntermediateValue(heightRatio, MIN_ARM_ROTATION, MAX_ARM_ROTATION) : 0; + const translation = + widthRatio !== null ? getIntermediateValue(widthRatio, MIN_ARM_TRANSLATION, MAX_ARM_TRANSLATION) : 0; + + return { + svg: css({ + '#grot-404-arm, #grot-404-magnifier': { + transform: `rotate(${rotation}deg) translateX(${translation}%)`, + transformOrigin: 'center', + transition: 'transform 50ms linear', + }, + '#grot-404-text': { + display: show404 ? 'block' : 'none', + }, + }), + }; +}; + +/** + * Given a start value, end value, and a ratio, return the intermediate value + * Works with negative and inverted start/end values + */ +const getIntermediateValue = (ratio: number, start: number, end: number) => { + const value = ratio * (end - start) + start; + return value; +}; diff --git a/public/app/core/components/GrotNotFound/useMousePosition.ts b/public/app/core/components/GrotNotFound/useMousePosition.ts new file mode 100644 index 00000000000..15ee04b4b9e --- /dev/null +++ b/public/app/core/components/GrotNotFound/useMousePosition.ts @@ -0,0 +1,29 @@ +import { throttle } from 'lodash'; +import { useState, useEffect } from 'react'; + +interface MousePosition { + x: number | null; + y: number | null; +} + +// For performance reasons, we throttle the mouse position updates +const DEFAULT_THROTTLE_INTERVAL_MS = 50; + +const useMousePosition = (throttleInterval = DEFAULT_THROTTLE_INTERVAL_MS) => { + const [mousePosition, setMousePosition] = useState({ x: null, y: null }); + + useEffect(() => { + const updateMousePosition = throttle((event: MouseEvent) => { + setMousePosition({ x: event.clientX, y: event.clientY }); + }, throttleInterval); + window.addEventListener('mousemove', updateMousePosition); + + return () => { + window.removeEventListener('mousemove', updateMousePosition); + }; + }, [throttleInterval]); + + return mousePosition; +}; + +export default useMousePosition; diff --git a/public/app/core/components/PageNotFound/EntityNotFound.tsx b/public/app/core/components/PageNotFound/EntityNotFound.tsx index 4ed5d97f59a..13bc119e5c7 100644 --- a/public/app/core/components/PageNotFound/EntityNotFound.tsx +++ b/public/app/core/components/PageNotFound/EntityNotFound.tsx @@ -2,7 +2,9 @@ import { css } from '@emotion/css'; import React from 'react'; import { GrafanaTheme2 } from '@grafana/data'; -import { useStyles2, useTheme2 } from '@grafana/ui'; +import { useStyles2 } from '@grafana/ui'; + +import { GrotNotFound } from '../GrotNotFound/GrotNotFound'; export interface Props { /** @@ -13,7 +15,6 @@ export interface Props { export function EntityNotFound({ entity = 'Page' }: Props) { const styles = useStyles2(getStyles); - const theme = useTheme2(); return (
@@ -29,7 +30,7 @@ export function EntityNotFound({ entity = 'Page' }: Props) {
- grot +
); @@ -52,9 +53,10 @@ export function getStyles(theme: GrafanaTheme2) { textAlign: 'center', }), grot: css({ + alignSelf: 'center', maxWidth: '450px', paddingTop: theme.spacing(8), - margin: '0 auto', + width: '100%', }), }; } diff --git a/public/app/features/commandPalette/EmptyState.tsx b/public/app/features/commandPalette/EmptyState.tsx index 15439b049d7..b7843840e04 100644 --- a/public/app/features/commandPalette/EmptyState.tsx +++ b/public/app/features/commandPalette/EmptyState.tsx @@ -3,13 +3,15 @@ import React from 'react'; import { Box, Stack, Text } from '@grafana/ui'; import { Trans } from 'app/core/internationalization'; +import { GrotNotFound } from '../../core/components/GrotNotFound/GrotNotFound'; + export interface Props {} export const EmptyState = ({}: Props) => { return ( - - grot + + No results found diff --git a/public/img/grot-404-dark.svg b/public/img/grot-404-dark.svg index dd6e81f4f0e..4ae37d64cef 100644 --- a/public/img/grot-404-dark.svg +++ b/public/img/grot-404-dark.svg @@ -1,61 +1,67 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/img/grot-404-light.svg b/public/img/grot-404-light.svg index 2c9ca391204..b2e54046cb8 100644 --- a/public/img/grot-404-light.svg +++ b/public/img/grot-404-light.svg @@ -1,61 +1,67 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/img/grot-not-found.svg b/public/img/grot-not-found.svg deleted file mode 100644 index 12106ef9a7f..00000000000 --- a/public/img/grot-not-found.svg +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - -