widgetFrame.tsx 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
  1. import {Fragment} from 'react';
  2. import type {BadgeProps} from 'sentry/components/badge/badge';
  3. import {LinkButton} from 'sentry/components/button';
  4. import {DropdownMenu, type MenuItemProps} from 'sentry/components/dropdownMenu';
  5. import ErrorBoundary from 'sentry/components/errorBoundary';
  6. import {Tooltip} from 'sentry/components/tooltip';
  7. import {IconEllipsis, IconExpand, IconWarning} from 'sentry/icons';
  8. import {t} from 'sentry/locale';
  9. import {ErrorPanel} from '../widgetLayout/errorPanel';
  10. import {WidgetBadge} from '../widgetLayout/widgetBadge';
  11. import {WidgetButton} from '../widgetLayout/widgetButton';
  12. import {
  13. WidgetDescription,
  14. type WidgetDescriptionProps,
  15. } from '../widgetLayout/widgetDescription';
  16. import {WidgetLayout} from '../widgetLayout/widgetLayout';
  17. import {WidgetTitle} from '../widgetLayout/widgetTitle';
  18. import {WIDGET_RENDER_ERROR_MESSAGE} from './settings';
  19. import {TooltipIconTrigger} from './tooltipIconTrigger';
  20. import type {StateProps} from './types';
  21. import {WarningsList} from './warningsList';
  22. export interface WidgetFrameProps extends StateProps, WidgetDescriptionProps {
  23. actions?: MenuItemProps[];
  24. actionsDisabled?: boolean;
  25. actionsMessage?: string;
  26. badgeProps?: BadgeProps | BadgeProps[];
  27. borderless?: boolean;
  28. children?: React.ReactNode;
  29. onFullScreenViewClick?: () => void | Promise<void>;
  30. title?: string;
  31. warnings?: string[];
  32. }
  33. export function WidgetFrame(props: WidgetFrameProps) {
  34. const {error} = props;
  35. // The error state has its own set of available actions
  36. const actions =
  37. (error
  38. ? props.onRetry
  39. ? [
  40. {
  41. key: 'retry',
  42. label: t('Retry'),
  43. onAction: props.onRetry,
  44. },
  45. ]
  46. : props.actions
  47. : props.actions) ?? [];
  48. const shouldShowFullScreenViewButton =
  49. Boolean(props.onFullScreenViewClick) && !props.error;
  50. const shouldShowActions = actions && actions.length > 0;
  51. return (
  52. <WidgetLayout
  53. ariaLabel="Widget panel"
  54. forceShowActions={props.forceDescriptionTooltip}
  55. Title={
  56. <Fragment>
  57. {props.warnings && props.warnings.length > 0 && (
  58. <Tooltip title={<WarningsList warnings={props.warnings} />} isHoverable>
  59. <TooltipIconTrigger aria-label={t('Widget warnings')}>
  60. <IconWarning color="warningText" />
  61. </TooltipIconTrigger>
  62. </Tooltip>
  63. )}
  64. <WidgetTitle title={props.title} />
  65. {props.badgeProps &&
  66. (Array.isArray(props.badgeProps) ? props.badgeProps : [props.badgeProps]).map(
  67. (currentBadgeProps, i) => <WidgetBadge key={i} {...currentBadgeProps} />
  68. )}
  69. </Fragment>
  70. }
  71. Actions={
  72. <Fragment>
  73. {props.description && (
  74. // Ideally we'd use `QuestionTooltip` but we need to firstly paint the icon dark, give it 100% opacity, and remove hover behaviour.
  75. <WidgetDescription
  76. title={props.title}
  77. description={props.description}
  78. forceDescriptionTooltip={props.forceDescriptionTooltip}
  79. />
  80. )}
  81. {shouldShowActions && (
  82. <TitleActionsWrapper
  83. disabled={Boolean(props.actionsDisabled)}
  84. disabledMessage={props.actionsMessage ?? ''}
  85. >
  86. {actions.length === 1 ? (
  87. actions[0]!.to ? (
  88. <LinkButton
  89. size="xs"
  90. disabled={props.actionsDisabled}
  91. onClick={actions[0]!.onAction}
  92. to={actions[0]!.to}
  93. >
  94. {actions[0]!.label}
  95. </LinkButton>
  96. ) : (
  97. <WidgetButton
  98. disabled={props.actionsDisabled}
  99. onClick={actions[0]!.onAction}
  100. >
  101. {actions[0]!.label}
  102. </WidgetButton>
  103. )
  104. ) : null}
  105. {actions.length > 1 ? (
  106. <DropdownMenu
  107. items={actions}
  108. isDisabled={props.actionsDisabled}
  109. triggerProps={{
  110. 'aria-label': t('Widget actions'),
  111. size: 'xs',
  112. borderless: true,
  113. showChevron: false,
  114. icon: <IconEllipsis direction="down" size="sm" />,
  115. }}
  116. position="bottom-end"
  117. />
  118. ) : null}
  119. </TitleActionsWrapper>
  120. )}
  121. {shouldShowFullScreenViewButton && (
  122. <WidgetButton
  123. aria-label={t('Open Full-Screen View')}
  124. borderless
  125. icon={<IconExpand />}
  126. onClick={() => {
  127. props.onFullScreenViewClick?.();
  128. }}
  129. />
  130. )}
  131. </Fragment>
  132. }
  133. Visualization={
  134. props.error ? (
  135. <ErrorPanel error={error} />
  136. ) : (
  137. <ErrorBoundary
  138. customComponent={<ErrorPanel error={WIDGET_RENDER_ERROR_MESSAGE} />}
  139. >
  140. {props.children}
  141. </ErrorBoundary>
  142. )
  143. }
  144. />
  145. );
  146. }
  147. interface TitleActionsProps {
  148. children: React.ReactNode;
  149. disabled: boolean;
  150. disabledMessage: string;
  151. }
  152. function TitleActionsWrapper({disabled, disabledMessage, children}: TitleActionsProps) {
  153. if (!disabled || !disabledMessage) {
  154. return children;
  155. }
  156. return (
  157. <Tooltip title={disabledMessage} isHoverable>
  158. {children}
  159. </Tooltip>
  160. );
  161. }