Skip to content

ref(dashboards): Namespaced Widget components #85238

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

Merged
merged 13 commits into from
Feb 14, 2025
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@ import {
} from '../common/settings';
import type {StateProps} from '../common/types';

import {DEEMPHASIS_COLOR_NAME, LOADING_PLACEHOLDER} from './settings';

export interface BigNumberWidgetProps
extends StateProps,
Omit<WidgetFrameProps, 'children'>,
Expand All @@ -36,7 +34,7 @@ export function BigNumberWidget(props: BigNumberWidgetProps) {
revealActions={props.revealActions}
revealTooltip={props.revealTooltip}
>
<LoadingPlaceholder>{LOADING_PLACEHOLDER}</LoadingPlaceholder>
<BigNumberWidgetVisualization.Placeholder />
</WidgetFrame>
);
}
Expand Down Expand Up @@ -91,8 +89,3 @@ const BigNumberResizeWrapper = styled('div')`
position: relative;
flex-grow: 1;
`;

const LoadingPlaceholder = styled('span')`
color: ${p => p.theme[DEEMPHASIS_COLOR_NAME]};
font-size: ${p => p.theme.fontSizeLarge};
`;
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import type {
Thresholds,
} from 'sentry/views/dashboards/widgets/common/types';

import {DEEMPHASIS_COLOR_NAME, LOADING_PLACEHOLDER} from './settings';
import {ThresholdsIndicator} from './thresholdsIndicator';

export interface BigNumberWidgetVisualizationProps {
Expand Down Expand Up @@ -169,3 +170,12 @@ const NumberContainerOverride = styled('div')`
white-space: nowrap;
}
`;

const LoadingPlaceholder = styled('span')`
color: ${p => p.theme[DEEMPHASIS_COLOR_NAME]};
font-size: ${p => p.theme.fontSizeLarge};
`;

BigNumberWidgetVisualization.Placeholder = function () {
return <LoadingPlaceholder>{LOADING_PLACEHOLDER}</LoadingPlaceholder>;
};
39 changes: 20 additions & 19 deletions static/app/views/dashboards/widgets/common/widgetFrame.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,24 @@
import {Fragment} from 'react';
import styled from '@emotion/styled';

import type {BadgeProps} from 'sentry/components/badge/badge';
import {LinkButton} from 'sentry/components/button';
import Badge from 'sentry/components/badge/badge';
import {Button, LinkButton} from 'sentry/components/button';
import {DropdownMenu, type MenuItemProps} from 'sentry/components/dropdownMenu';
import ErrorBoundary from 'sentry/components/errorBoundary';
import {Tooltip} from 'sentry/components/tooltip';
import {IconEllipsis, IconExpand, IconWarning} from 'sentry/icons';
import {t} from 'sentry/locale';

import {ErrorPanel} from '../widgetLayout/errorPanel';
import {WidgetBadge} from '../widgetLayout/widgetBadge';
import {WidgetButton} from '../widgetLayout/widgetButton';
import {
WidgetDescription,
type WidgetDescriptionProps,
} from '../widgetLayout/widgetDescription';
import {WidgetLayout} from '../widgetLayout/widgetLayout';
import {WidgetTitle} from '../widgetLayout/widgetTitle';
import type {DescriptionProps} from '../widget/description';
import {Widget} from '../widget/widget';

import {WIDGET_RENDER_ERROR_MESSAGE} from './settings';
import {TooltipIconTrigger} from './tooltipIconTrigger';
import type {StateProps} from './types';
import {WarningsList} from './warningsList';

export interface WidgetFrameProps extends StateProps, WidgetDescriptionProps {
export interface WidgetFrameProps extends StateProps, DescriptionProps {
actions?: MenuItemProps[];
actionsDisabled?: boolean;
actionsMessage?: string;
Expand Down Expand Up @@ -61,7 +56,7 @@ export function WidgetFrame(props: WidgetFrameProps) {
const shouldShowActions = actions && actions.length > 0;

return (
<WidgetLayout
<Widget
ariaLabel="Widget panel"
borderless={props.borderless}
Title={
Expand All @@ -74,7 +69,7 @@ export function WidgetFrame(props: WidgetFrameProps) {
</Tooltip>
)}

<WidgetTitle title={props.title} />
<Widget.TextTitle title={props.title} />

{props.badgeProps &&
(Array.isArray(props.badgeProps) ? props.badgeProps : [props.badgeProps]).map(
Expand All @@ -89,7 +84,7 @@ export function WidgetFrame(props: WidgetFrameProps) {
<Fragment>
{props.description && (
// Ideally we'd use `QuestionTooltip` but we need to firstly paint the icon dark, give it 100% opacity, and remove hover behaviour.
<WidgetDescription
<Widget.Description
title={props.title}
description={props.description}
revealTooltip={props.revealTooltip ?? 'hover'}
Expand All @@ -112,12 +107,13 @@ export function WidgetFrame(props: WidgetFrameProps) {
{actions[0]!.label}
</LinkButton>
) : (
<WidgetButton
<Button
size="xs"
disabled={props.actionsDisabled}
onClick={actions[0]!.onAction}
>
{actions[0]!.label}
</WidgetButton>
</Button>
)
) : null}

Expand All @@ -139,7 +135,8 @@ export function WidgetFrame(props: WidgetFrameProps) {
)}

{shouldShowFullScreenViewButton && (
<WidgetButton
<Button
size="xs"
aria-label={t('Open Full-Screen View')}
borderless
icon={<IconExpand />}
Expand All @@ -152,10 +149,10 @@ export function WidgetFrame(props: WidgetFrameProps) {
}
Visualization={
props.error ? (
<ErrorPanel error={error} />
<Widget.Error error={error} />
) : (
<ErrorBoundary
customComponent={() => <ErrorPanel error={WIDGET_RENDER_ERROR_MESSAGE} />}
customComponent={() => <Widget.Error error={WIDGET_RENDER_ERROR_MESSAGE} />}
>
{props.children}
</ErrorBoundary>
Expand All @@ -172,6 +169,10 @@ interface TitleActionsProps {
disabledMessage: string;
}

const WidgetBadge = styled(Badge)`
flex-shrink: 0;
`;

function TitleActionsWrapper({disabled, disabledMessage, children}: TitleActionsProps) {
if (!disabled || !disabledMessage) {
return children;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import {

import {MISSING_DATA_MESSAGE, NO_PLOTTABLE_VALUES} from '../common/settings';
import type {StateProps} from '../common/types';
import {LoadingPanel} from '../widgetLayout/loadingPanel';

export interface TimeSeriesWidgetProps
extends StateProps,
Expand All @@ -30,7 +29,7 @@ export function TimeSeriesWidget(props: TimeSeriesWidgetProps) {
revealActions={props.revealActions}
revealTooltip={props.revealTooltip}
>
<LoadingPanel />
<TimeSeriesWidgetVisualization.Placeholder />
</WidgetFrame>
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {useRef} from 'react';
import {useNavigate} from 'react-router-dom';
import {useTheme} from '@emotion/react';
import styled from '@emotion/styled';
import type {BarSeriesOption, LineSeriesOption} from 'echarts';
import type {
TooltipFormatterCallback,
Expand All @@ -11,8 +12,10 @@ import type EChartsReactCore from 'echarts-for-react/lib/core';
import BaseChart from 'sentry/components/charts/baseChart';
import {getFormatter} from 'sentry/components/charts/components/tooltip';
import LineSeries from 'sentry/components/charts/series/lineSeries';
import TransparentLoadingMask from 'sentry/components/charts/transparentLoadingMask';
import {useChartZoom} from 'sentry/components/charts/useChartZoom';
import {isChartHovered, truncationFormatter} from 'sentry/components/charts/utils';
import LoadingIndicator from 'sentry/components/loadingIndicator';
import type {Series} from 'sentry/types/echarts';
import {defined} from 'sentry/utils';
import {uniq} from 'sentry/utils/array/uniq';
Expand All @@ -27,6 +30,7 @@ import useOrganization from 'sentry/utils/useOrganization';
import usePageFilters from 'sentry/utils/usePageFilters';

import {useWidgetSyncContext} from '../../contexts/widgetSyncContext';
import {X_GUTTER, Y_GUTTER} from '../common/settings';
import type {Aliases, Release, TimeSeries, TimeseriesSelection} from '../common/types';

import {BarChartWidgetSeries} from './seriesConstructors/barChartWidgetSeries';
Expand Down Expand Up @@ -338,4 +342,30 @@ export function TimeSeriesWidgetVisualization(props: TimeSeriesWidgetVisualizati
);
}

function LoadingPanel() {
return (
<LoadingPlaceholder>
<LoadingMask visible />
<LoadingIndicator mini />
</LoadingPlaceholder>
);
}

const LoadingPlaceholder = styled('div')`
position: absolute;
inset: 0;

display: flex;
justify-content: center;
align-items: center;

padding: ${Y_GUTTER} ${X_GUTTER};
`;

const LoadingMask = styled(TransparentLoadingMask)`
background: ${p => p.theme.background};
`;

TimeSeriesWidgetVisualization.Placeholder = LoadingPanel;

const GLOBAL_STACK_NAME = 'time-series-visualization-widget-stack';
Original file line number Diff line number Diff line change
Expand Up @@ -6,28 +6,28 @@ import {IconInfo} from 'sentry/icons';
import {t} from 'sentry/locale';
import {space} from 'sentry/styles/space';

export interface WidgetDescriptionProps {
export interface DescriptionProps {
description?: React.ReactElement | string;
revealTooltip?: 'hover' | 'always';
title?: string;
}

export function WidgetDescription(props: WidgetDescriptionProps) {
export function Description(props: DescriptionProps) {
return (
<Tooltip
title={
<WidgetTooltipContents>
{props.title && <WidgetTooltipTitle>{props.title}</WidgetTooltipTitle>}
<TooltipContents>
{props.title && <TooltipTitle>{props.title}</TooltipTitle>}
{props.description && (
<WidgetTooltipDescription>{props.description}</WidgetTooltipDescription>
<TooltipDescription>{props.description}</TooltipDescription>
)}
</WidgetTooltipContents>
</TooltipContents>
}
containerDisplayMode="grid"
isHoverable
forceVisible={props.revealTooltip === 'always' ? true : undefined}
>
<WidgetTooltipButton
<TooltipButton
aria-label={t('Widget description')}
borderless
size="xs"
Expand All @@ -37,27 +37,27 @@ export function WidgetDescription(props: WidgetDescriptionProps) {
);
}

const WidgetTooltipContents = styled('div')`
const TooltipContents = styled('div')`
display: flex;
flex-direction: column;
gap: ${space(0.5)};
max-height: 33vh;
overflow: hidden;
`;

const WidgetTooltipTitle = styled('div')`
const TooltipTitle = styled('div')`
font-weight: bold;
font-size: ${p => p.theme.fontSizeMedium};
text-align: left;
`;

const WidgetTooltipDescription = styled('div')`
const TooltipDescription = styled('div')`
font-size: ${p => p.theme.fontSizeSmall};
text-align: left;
`;

// We're using a button here to preserve tab accessibility
const WidgetTooltipButton = styled(Button)`
const TooltipButton = styled(Button)`
pointer-events: none;
padding-top: 0;
padding-bottom: 0;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ import type {StateProps} from 'sentry/views/dashboards/widgets/common/types';

import {X_GUTTER, Y_GUTTER} from '../common/settings';

interface ErrorPanelProps {
interface ErrorProps {
error: StateProps['error'];
}

export function ErrorPanel({error}: ErrorPanelProps) {
export function Error({error}: ErrorProps) {
return (
<Panel>
<NonShrinkingWarningIcon color={DEEMPHASIS_COLOR_NAME} size="md" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ import styled from '@emotion/styled';
import {HeaderTitle} from 'sentry/components/charts/styles';
import {Tooltip} from 'sentry/components/tooltip';

export interface WidgetTitleProps {
export interface TextTitleProps {
title?: string;
}

export function WidgetTitle(props: WidgetTitleProps) {
export function TextTitle(props: TextTitleProps) {
return (
<Tooltip title={props.title} containerDisplayMode="grid" showOnlyOnOverflow>
<TitleText>{props.title}</TitleText>
Expand Down
8 changes: 8 additions & 0 deletions static/app/views/dashboards/widgets/widget/toolbar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import styled from '@emotion/styled';

import {space} from 'sentry/styles/space';

export const Toolbar = styled('div')`
display: flex;
gap: ${space(0.5)};
`;
Loading
Loading