widgetLayout.tsx 2.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105
  1. import {Fragment} from 'react';
  2. import styled from '@emotion/styled';
  3. import {space} from 'sentry/styles/space';
  4. import {MIN_HEIGHT, MIN_WIDTH, X_GUTTER, Y_GUTTER} from '../common/settings';
  5. export interface WidgetLayoutProps {
  6. Actions?: React.ReactNode;
  7. Footer?: React.ReactNode;
  8. Title?: React.ReactNode;
  9. Visualization?: React.ReactNode;
  10. ariaLabel?: string;
  11. forceShowActions?: boolean;
  12. height?: number;
  13. }
  14. export function WidgetLayout(props: WidgetLayoutProps) {
  15. return (
  16. <Frame
  17. aria-label={props.ariaLabel}
  18. height={props.height}
  19. forceShowActions={props.forceShowActions}
  20. >
  21. <Header>
  22. {props.Title && <Fragment>{props.Title}</Fragment>}
  23. {props.Actions && <TitleHoverItems>{props.Actions}</TitleHoverItems>}
  24. </Header>
  25. {props.Visualization && (
  26. <VisualizationWrapper>{props.Visualization}</VisualizationWrapper>
  27. )}
  28. {props.Footer && <FooterWrapper>{props.Footer}</FooterWrapper>}
  29. </Frame>
  30. );
  31. }
  32. const HEADER_HEIGHT = '26px';
  33. const TitleHoverItems = styled('div')`
  34. display: flex;
  35. align-items: center;
  36. gap: ${space(0.5)};
  37. margin-left: auto;
  38. opacity: 1;
  39. transition: opacity 0.1s;
  40. `;
  41. const Frame = styled('div')<{forceShowActions?: boolean; height?: number}>`
  42. position: relative;
  43. display: flex;
  44. flex-direction: column;
  45. height: ${p => (p.height ? `${p.height}px` : '100%')};
  46. min-height: ${MIN_HEIGHT}px;
  47. width: 100%;
  48. min-width: ${MIN_WIDTH}px;
  49. border-radius: ${p => p.theme.panelBorderRadius};
  50. border: 1px solid ${p => p.theme.border};
  51. background: ${p => p.theme.background};
  52. :hover {
  53. background-color: ${p => p.theme.surface200};
  54. transition:
  55. background-color 100ms linear,
  56. box-shadow 100ms linear;
  57. box-shadow: ${p => p.theme.dropShadowLight};
  58. }
  59. ${p =>
  60. !p.forceShowActions &&
  61. `&:not(:hover):not(:focus-within) {
  62. ${TitleHoverItems} {
  63. opacity: 0;
  64. ${p.theme.visuallyHidden}
  65. }
  66. }`}
  67. `;
  68. const Header = styled('div')`
  69. display: flex;
  70. align-items: center;
  71. height: calc(${HEADER_HEIGHT} + ${Y_GUTTER});
  72. flex-shrink: 0;
  73. gap: ${space(0.75)};
  74. padding: ${X_GUTTER} ${Y_GUTTER} 0 ${X_GUTTER};
  75. `;
  76. const VisualizationWrapper = styled('div')`
  77. display: flex;
  78. flex-direction: column;
  79. flex-grow: 1;
  80. min-height: 0;
  81. position: relative;
  82. padding: 0;
  83. `;
  84. const FooterWrapper = styled('div')`
  85. margin: 0;
  86. border-top: 1px solid ${p => p.theme.border};
  87. `;