-
-
Notifications
You must be signed in to change notification settings - Fork 4.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(dashboards): BigNumberWidget
Layout, UI, Data, State
#77877
Merged
Merged
Changes from all commits
Commits
Show all changes
19 commits
Select commit
Hold shift + click to select a range
bb1dff7
Files
gggritso 033706b
Minimum viable content
gggritso f21959c
Allow missing field units
gggritso 829d662
Add second example to story
gggritso f10d31b
Simplify types
gggritso c312b36
Rename a wrapper component
gggritso 2dae47c
Rename generic type
gggritso 8a3a660
Expand stories
gggritso ae04d23
Rename the visualization component
gggritso b88f885
Extract constant
gggritso 8c42b1b
Another small rename
gggritso 33ac24f
Split up the spec by feature
gggritso 9fed28b
Add support for loading state
gggritso a6016b5
Implement error state
gggritso 231a7c9
Typo
gggritso 6c190bd
Improve tooltip
gggritso 4186656
Add meta field hack
gggritso 23f50ce
Merge branch 'master' into feat/dashboards/big-number-widget
gggritso 28b7472
Revert "Add meta field hack"
gggritso File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
95 changes: 95 additions & 0 deletions
95
static/app/views/dashboards/widgets/bigNumberWidget/bigNumberWidget.spec.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary'; | ||
|
||
import {BigNumberWidget} from 'sentry/views/dashboards/widgets/bigNumberWidget/bigNumberWidget'; | ||
|
||
describe('BigNumberWidget', () => { | ||
describe('Layout', () => { | ||
it('Renders', () => { | ||
render( | ||
<BigNumberWidget | ||
title="EPS" | ||
description="Number of events per second" | ||
data={[ | ||
{ | ||
'eps()': 0.01087819860850493, | ||
}, | ||
]} | ||
meta={{ | ||
fields: { | ||
'eps()': 'rate', | ||
}, | ||
units: { | ||
'eps()': '1/second', | ||
}, | ||
}} | ||
/> | ||
); | ||
|
||
expect(screen.getByText('EPS')).toBeInTheDocument(); | ||
expect(screen.getByText('Number of events per second')).toBeInTheDocument(); | ||
expect(screen.getByText('0.0109/s')).toBeInTheDocument(); | ||
}); | ||
}); | ||
|
||
describe('Visualization', () => { | ||
it('Formats duration data', () => { | ||
render( | ||
<BigNumberWidget | ||
data={[ | ||
{ | ||
'p95(span.duration)': 17.28, | ||
}, | ||
]} | ||
meta={{ | ||
fields: { | ||
'p95(span.duration)': 'duration', | ||
}, | ||
units: { | ||
'p95(span.duration)': 'milliseconds', | ||
}, | ||
}} | ||
/> | ||
); | ||
|
||
expect(screen.getByText('17.28ms')).toBeInTheDocument(); | ||
}); | ||
|
||
it('Shows the full unformatted value on hover', async () => { | ||
render( | ||
<BigNumberWidget | ||
data={[ | ||
{ | ||
'count()': 178451214, | ||
}, | ||
]} | ||
meta={{ | ||
fields: { | ||
'count()': 'integer', | ||
}, | ||
units: { | ||
'count()': null, | ||
}, | ||
}} | ||
/> | ||
); | ||
|
||
await userEvent.hover(screen.getByText('178m')); | ||
|
||
expect(screen.getByText('178451214')).toBeInTheDocument(); | ||
}); | ||
}); | ||
|
||
describe('State', () => { | ||
it('Shows a loading placeholder', () => { | ||
render(<BigNumberWidget isLoading />); | ||
|
||
expect(screen.getByText('—')).toBeInTheDocument(); | ||
}); | ||
|
||
it('Shows an error message', () => { | ||
render(<BigNumberWidget error={new Error('Uh oh')} />); | ||
|
||
expect(screen.getByText('Error: Uh oh')).toBeInTheDocument(); | ||
}); | ||
}); | ||
}); |
122 changes: 122 additions & 0 deletions
122
static/app/views/dashboards/widgets/bigNumberWidget/bigNumberWidget.stories.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
import {Fragment} from 'react'; | ||
import styled from '@emotion/styled'; | ||
|
||
import JSXNode from 'sentry/components/stories/jsxNode'; | ||
import SideBySide from 'sentry/components/stories/sideBySide'; | ||
import SizingWindow from 'sentry/components/stories/sizingWindow'; | ||
import storyBook from 'sentry/stories/storyBook'; | ||
import {BigNumberWidget} from 'sentry/views/dashboards/widgets/bigNumberWidget/bigNumberWidget'; | ||
|
||
export default storyBook(BigNumberWidget, story => { | ||
story('Getting Started', () => { | ||
return ( | ||
<Fragment> | ||
<p> | ||
<JSXNode name="BigNumberWidget" /> is a Dashboard Widget Component. It displays | ||
a single large value. Used in places like Dashboards Big Number widgets, Project | ||
Details pages, and Organization Stats pages. | ||
</p> | ||
</Fragment> | ||
); | ||
}); | ||
|
||
story('Visualization', () => { | ||
return ( | ||
<Fragment> | ||
<p> | ||
The visualization of <JSXNode name="BigNumberWidget" /> a large number, just | ||
like it says on the tin. Depending on the data passed to it, it intelligently | ||
rounds and humanizes the results. If the number is humanized, hovering over the | ||
visualization shows a tooltip with the full value. | ||
</p> | ||
|
||
<SideBySide> | ||
<SmallSizingWindow> | ||
<BigNumberWidget | ||
title="EPS" | ||
description="Number of events per second" | ||
data={[ | ||
{ | ||
'eps()': 0.01087819860850493, | ||
}, | ||
]} | ||
meta={{ | ||
fields: { | ||
'eps()': 'rate', | ||
}, | ||
units: { | ||
'eps()': '1/second', | ||
}, | ||
}} | ||
/> | ||
</SmallSizingWindow> | ||
<SmallSizingWindow> | ||
<BigNumberWidget | ||
title="Count" | ||
data={[ | ||
{ | ||
'count()': 178451214, | ||
}, | ||
]} | ||
meta={{ | ||
fields: { | ||
'count()': 'integer', | ||
}, | ||
units: { | ||
'count()': null, | ||
}, | ||
}} | ||
/> | ||
</SmallSizingWindow> | ||
<SmallSizingWindow> | ||
<BigNumberWidget | ||
title="Query Duration" | ||
description="p95(span.duration)" | ||
data={[ | ||
{ | ||
'p95(span.duration)': 17.28, | ||
}, | ||
]} | ||
meta={{ | ||
fields: { | ||
'p95(span.duration)': 'duration', | ||
}, | ||
units: { | ||
'p95(spa.duration)': 'milliseconds', | ||
}, | ||
}} | ||
/> | ||
</SmallSizingWindow> | ||
</SideBySide> | ||
</Fragment> | ||
); | ||
}); | ||
|
||
story('State', () => { | ||
return ( | ||
<Fragment> | ||
<p> | ||
<JSXNode name="BigNumberWidget" /> supports the usual loading and error states. | ||
The loading state shows a simple placeholder. | ||
</p> | ||
|
||
<SideBySide> | ||
<SmallSizingWindow> | ||
<BigNumberWidget title="Count" isLoading /> | ||
</SmallSizingWindow> | ||
<SmallSizingWindow> | ||
<BigNumberWidget | ||
title="Bad Count" | ||
error={new Error('Something went wrong!')} | ||
/> | ||
</SmallSizingWindow> | ||
</SideBySide> | ||
</Fragment> | ||
); | ||
}); | ||
}); | ||
|
||
const SmallSizingWindow = styled(SizingWindow)` | ||
width: auto; | ||
height: 200px; | ||
`; |
37 changes: 37 additions & 0 deletions
37
static/app/views/dashboards/widgets/bigNumberWidget/bigNumberWidget.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
import styled from '@emotion/styled'; | ||
|
||
import {space} from 'sentry/styles/space'; | ||
import { | ||
BigNumberWidgetVisualization, | ||
type Props as BigNumberWidgetVisualizationProps, | ||
} from 'sentry/views/dashboards/widgets/bigNumberWidget/bigNumberWidgetVisualization'; | ||
import { | ||
type Props as WidgetFrameProps, | ||
WidgetFrame, | ||
} from 'sentry/views/dashboards/widgets/common/widgetFrame'; | ||
|
||
interface Props | ||
extends Omit<WidgetFrameProps, 'children'>, | ||
BigNumberWidgetVisualizationProps {} | ||
|
||
export function BigNumberWidget(props: Props) { | ||
return ( | ||
<WidgetFrame title={props.title} description={props.description}> | ||
<BigNumberResizeWrapper> | ||
<BigNumberWidgetVisualization | ||
data={props.data} | ||
meta={props.meta} | ||
isLoading={props.isLoading} | ||
error={props.error} | ||
/> | ||
</BigNumberResizeWrapper> | ||
</WidgetFrame> | ||
); | ||
} | ||
|
||
const BigNumberResizeWrapper = styled('div')` | ||
flex-grow: 1; | ||
overflow: hidden; | ||
position: relative; | ||
margin: ${space(1)} ${space(3)} ${space(3)} ${space(3)}; | ||
`; |
98 changes: 98 additions & 0 deletions
98
static/app/views/dashboards/widgets/bigNumberWidget/bigNumberWidgetVisualization.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
import styled from '@emotion/styled'; | ||
|
||
import {Tooltip} from 'sentry/components/tooltip'; | ||
import {defined} from 'sentry/utils'; | ||
import type {MetaType} from 'sentry/utils/discover/eventView'; | ||
import {getFieldFormatter} from 'sentry/utils/discover/fieldRenderers'; | ||
import {useLocation} from 'sentry/utils/useLocation'; | ||
import useOrganization from 'sentry/utils/useOrganization'; | ||
import {AutoSizedText} from 'sentry/views/dashboards/widgetCard/autoSizedText'; | ||
import {NO_DATA_PLACEHOLDER} from 'sentry/views/dashboards/widgets/bigNumberWidget/settings'; | ||
import {ErrorPanel} from 'sentry/views/dashboards/widgets/common/errorPanel'; | ||
import type { | ||
Meta, | ||
StateProps, | ||
TableData, | ||
} from 'sentry/views/dashboards/widgets/common/types'; | ||
|
||
export interface Props extends StateProps { | ||
data?: TableData; | ||
meta?: Meta; | ||
} | ||
|
||
export function BigNumberWidgetVisualization(props: Props) { | ||
const {data, meta, isLoading, error} = props; | ||
|
||
const location = useLocation(); | ||
const organization = useOrganization(); | ||
|
||
if (error) { | ||
return <ErrorPanel error={error} />; | ||
} | ||
|
||
// Big Number widgets only show one number, so we only ever look at the first item in the Discover response | ||
const datum = data?.[0]; | ||
// TODO: Instrument getting more than one data key back as an error | ||
|
||
if (isLoading || !defined(datum) || Object.keys(datum).length === 0) { | ||
return ( | ||
<AutoResizeParent> | ||
<AutoSizedText> | ||
<Deemphasize>{NO_DATA_PLACEHOLDER}</Deemphasize> | ||
</AutoSizedText> | ||
</AutoResizeParent> | ||
); | ||
} | ||
|
||
const fields = Object.keys(datum); | ||
const field = fields[0]; | ||
|
||
// TODO: meta as MetaType is a white lie. `MetaType` doesn't know that types can be null, but they can! | ||
const fieldFormatter = meta | ||
? getFieldFormatter(field, meta as MetaType, false) | ||
: value => value.toString(); | ||
|
||
const unit = meta?.units?.[field]; | ||
const rendered = fieldFormatter(datum, { | ||
location, | ||
organization, | ||
unit: unit ?? undefined, // TODO: Field formatters think units can't be null but they can | ||
}); | ||
|
||
return ( | ||
<AutoResizeParent> | ||
<AutoSizedText> | ||
<NumberContainerOverride> | ||
<Tooltip title={datum[field]} isHoverable delay={0}> | ||
{rendered} | ||
</Tooltip> | ||
</NumberContainerOverride> | ||
</AutoSizedText> | ||
</AutoResizeParent> | ||
); | ||
} | ||
|
||
const AutoResizeParent = styled('div')` | ||
position: absolute; | ||
color: ${p => p.theme.headingColor}; | ||
inset: 0; | ||
|
||
* { | ||
line-height: 1; | ||
text-align: left !important; | ||
} | ||
`; | ||
|
||
const NumberContainerOverride = styled('div')` | ||
display: inline-block; | ||
|
||
* { | ||
text-overflow: clip !important; | ||
display: inline; | ||
white-space: nowrap; | ||
} | ||
`; | ||
|
||
const Deemphasize = styled('span')` | ||
color: ${p => p.theme.gray300}; | ||
`; |
1 change: 1 addition & 0 deletions
1
static/app/views/dashboards/widgets/bigNumberWidget/settings.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export const NO_DATA_PLACEHOLDER = '\u2014'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
import styled from '@emotion/styled'; | ||
|
||
import {IconWarning} from 'sentry/icons'; | ||
import {space} from 'sentry/styles/space'; | ||
import type {StateProps} from 'sentry/views/dashboards/widgets/common/types'; | ||
|
||
interface Props { | ||
error: StateProps['error']; | ||
} | ||
|
||
export function ErrorPanel({error}: Props) { | ||
return ( | ||
<Panel> | ||
<IconWarning color="gray500" size="lg" /> | ||
<span>{error?.toString()}</span> | ||
</Panel> | ||
); | ||
} | ||
|
||
const Panel = styled('div')<{height?: string}>` | ||
position: absolute; | ||
inset: 0; | ||
|
||
display: flex; | ||
flex-direction: column; | ||
justify-content: center; | ||
align-items: center; | ||
gap: ${space(0.5)}; | ||
|
||
overflow: hidden; | ||
|
||
color: ${p => p.theme.gray300}; | ||
font-size: ${p => p.theme.fontSizeExtraLarge}; | ||
`; |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A small nit, this block is defines "Layout" stuff, but we can't really test that in RTL right? Shouldn't this just go under the "Visualization" block as well?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh good question! Here "Layout" means the "Layout" feature in the spec. The Jest file is organized by the widget specification features, but "Layout" is one of the more ambiguous ones