widgetFrame.tsx 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185
  1. import styled from '@emotion/styled';
  2. import Badge, {type BadgeProps} from 'sentry/components/badge/badge';
  3. import {Button, LinkButton} from 'sentry/components/button';
  4. import {HeaderTitle} from 'sentry/components/charts/styles';
  5. import {DropdownMenu, type MenuItemProps} from 'sentry/components/dropdownMenu';
  6. import QuestionTooltip from 'sentry/components/questionTooltip';
  7. import {Tooltip} from 'sentry/components/tooltip';
  8. import {IconEllipsis, IconExpand, IconWarning} from 'sentry/icons';
  9. import {t} from 'sentry/locale';
  10. import {space} from 'sentry/styles/space';
  11. import {ErrorPanel} from './errorPanel';
  12. import {MIN_HEIGHT, MIN_WIDTH} from './settings';
  13. import {TooltipIconTrigger} from './tooltipIconTrigger';
  14. import type {StateProps} from './types';
  15. import {WarningsList} from './warningsList';
  16. export interface WidgetFrameProps extends StateProps {
  17. actions?: MenuItemProps[];
  18. badgeProps?: BadgeProps;
  19. children?: React.ReactNode;
  20. description?: string;
  21. onFullScreenViewClick?: () => void;
  22. title?: string;
  23. warnings?: string[];
  24. }
  25. export function WidgetFrame(props: WidgetFrameProps) {
  26. const {error} = props;
  27. // The error state has its own set of available actions
  28. const actions =
  29. (error
  30. ? props.onRetry
  31. ? [
  32. {
  33. key: 'retry',
  34. label: t('Retry'),
  35. onAction: props.onRetry,
  36. },
  37. ]
  38. : []
  39. : props.actions) ?? [];
  40. return (
  41. <Frame>
  42. <Header>
  43. {props.warnings && props.warnings.length > 0 && (
  44. <Tooltip title={<WarningsList warnings={props.warnings} />} isHoverable>
  45. <TooltipIconTrigger aria-label={t('Widget warnings')}>
  46. <IconWarning color="warningText" />
  47. </TooltipIconTrigger>
  48. </Tooltip>
  49. )}
  50. <Tooltip title={props.title} containerDisplayMode="grid" showOnlyOnOverflow>
  51. <TitleText>{props.title}</TitleText>
  52. </Tooltip>
  53. {props.badgeProps && <RigidBadge {...props.badgeProps} />}
  54. {(props.description ||
  55. props.onFullScreenViewClick ||
  56. (actions && actions.length > 0)) && (
  57. <TitleActions>
  58. {props.description && (
  59. <QuestionTooltip title={props.description} size="sm" icon="info" />
  60. )}
  61. {actions.length === 1 ? (
  62. actions[0].to ? (
  63. <LinkButton size="xs" onClick={actions[0].onAction} to={actions[0].to}>
  64. {actions[0].label}
  65. </LinkButton>
  66. ) : (
  67. <Button size="xs" onClick={actions[0].onAction}>
  68. {actions[0].label}
  69. </Button>
  70. )
  71. ) : null}
  72. {actions.length > 1 ? (
  73. <DropdownMenu
  74. items={actions}
  75. triggerProps={{
  76. 'aria-label': t('Actions'),
  77. size: 'xs',
  78. borderless: true,
  79. showChevron: false,
  80. icon: <IconEllipsis direction="down" size="sm" />,
  81. }}
  82. position="bottom-end"
  83. />
  84. ) : null}
  85. {props.onFullScreenViewClick && (
  86. <Button
  87. aria-label={t('Open Full-Screen View')}
  88. borderless
  89. size="xs"
  90. icon={<IconExpand />}
  91. onClick={() => {
  92. props.onFullScreenViewClick?.();
  93. }}
  94. />
  95. )}
  96. </TitleActions>
  97. )}
  98. </Header>
  99. <VisualizationWrapper>
  100. {props.error ? <ErrorPanel error={error} /> : props.children}
  101. </VisualizationWrapper>
  102. </Frame>
  103. );
  104. }
  105. const TitleActions = styled('div')`
  106. display: flex;
  107. align-items: center;
  108. gap: ${space(0.5)};
  109. margin-left: auto;
  110. opacity: 1;
  111. transition: opacity 0.1s;
  112. `;
  113. const Frame = styled('div')`
  114. position: relative;
  115. display: flex;
  116. flex-direction: column;
  117. height: 100%;
  118. min-height: ${MIN_HEIGHT}px;
  119. width: 100%;
  120. min-width: ${MIN_WIDTH}px;
  121. padding: ${space(1.5)} ${space(2)};
  122. border-radius: ${p => p.theme.panelBorderRadius};
  123. border: ${p => p.theme.border};
  124. border: 1px ${p => 'solid ' + p.theme.border};
  125. background: ${p => p.theme.background};
  126. :hover {
  127. background-color: ${p => p.theme.surface200};
  128. transition:
  129. background-color 100ms linear,
  130. box-shadow 100ms linear;
  131. box-shadow: ${p => p.theme.dropShadowLight};
  132. }
  133. &:not(:hover):not(:focus-within) {
  134. ${TitleActions} {
  135. opacity: 0;
  136. ${p => p.theme.visuallyHidden}
  137. }
  138. }
  139. `;
  140. const HEADER_HEIGHT = 26;
  141. const Header = styled('div')`
  142. display: flex;
  143. align-items: center;
  144. height: ${HEADER_HEIGHT}px;
  145. gap: ${space(0.75)};
  146. `;
  147. const TitleText = styled(HeaderTitle)`
  148. ${p => p.theme.overflowEllipsis};
  149. font-weight: ${p => p.theme.fontWeightBold};
  150. `;
  151. const RigidBadge = styled(Badge)`
  152. flex-shrink: 0;
  153. `;
  154. const VisualizationWrapper = styled('div')`
  155. display: flex;
  156. flex-grow: 1;
  157. position: relative;
  158. `;