widgetTemplatesList.tsx 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
  1. import {Fragment, useCallback, useEffect, useState} from 'react';
  2. import {useParams} from 'react-router-dom';
  3. import styled from '@emotion/styled';
  4. import {validateWidget} from 'sentry/actionCreators/dashboards';
  5. import {addErrorMessage} from 'sentry/actionCreators/indicator';
  6. import {Button} from 'sentry/components/button';
  7. import {t} from 'sentry/locale';
  8. import {space} from 'sentry/styles/space';
  9. import {trackAnalytics} from 'sentry/utils/analytics';
  10. import theme from 'sentry/utils/theme';
  11. import useApi from 'sentry/utils/useApi';
  12. import useOrganization from 'sentry/utils/useOrganization';
  13. import type {Widget} from 'sentry/views/dashboards/types';
  14. import {useWidgetBuilderContext} from 'sentry/views/dashboards/widgetBuilder/contexts/widgetBuilderContext';
  15. import {BuilderStateAction} from 'sentry/views/dashboards/widgetBuilder/hooks/useWidgetBuilderState';
  16. import {convertWidgetToBuilderStateParams} from 'sentry/views/dashboards/widgetBuilder/utils/convertWidgetToBuilderStateParams';
  17. import {getTopNConvertedDefaultWidgets} from 'sentry/views/dashboards/widgetLibrary/data';
  18. import {getWidgetIcon} from 'sentry/views/dashboards/widgetLibrary/widgetCard';
  19. interface WidgetTemplatesListProps {
  20. onSave: ({index, widget}: {index: number; widget: Widget}) => void;
  21. setIsPreviewDraggable: (isPreviewDraggable: boolean) => void;
  22. setOpenWidgetTemplates: (openWidgetTemplates: boolean) => void;
  23. }
  24. function WidgetTemplatesList({
  25. onSave,
  26. setOpenWidgetTemplates,
  27. setIsPreviewDraggable,
  28. }: WidgetTemplatesListProps) {
  29. const organization = useOrganization();
  30. const [selectedWidget, setSelectedWidget] = useState<number | null>(null);
  31. const {dispatch} = useWidgetBuilderContext();
  32. const {widgetIndex} = useParams();
  33. const api = useApi();
  34. useEffect(() => {
  35. trackAnalytics('dashboards_views.widget_builder.templates.open', {
  36. organization,
  37. });
  38. // We only want to track this once when the component is mounted
  39. // eslint-disable-next-line react-hooks/exhaustive-deps
  40. }, []);
  41. const widgets = getTopNConvertedDefaultWidgets(organization);
  42. const handleSave = useCallback(
  43. async (widget: Widget) => {
  44. try {
  45. const newWidget = {...widget, id: undefined};
  46. await validateWidget(api, organization.slug, newWidget);
  47. onSave({index: Number(widgetIndex), widget: newWidget});
  48. } catch (error) {
  49. addErrorMessage(t('Unable to add widget'));
  50. }
  51. },
  52. [api, organization.slug, widgetIndex, onSave]
  53. );
  54. return (
  55. <Fragment>
  56. {widgets.map((widget, index) => {
  57. const iconColor = theme.charts.getColorPalette(widgets.length - 2)?.[index]!;
  58. const Icon = getWidgetIcon(widget.displayType);
  59. const lastWidget = index === widgets.length - 1;
  60. return (
  61. <TemplateContainer key={widget.id} lastWidget={lastWidget}>
  62. <TemplateCard
  63. selected={selectedWidget === index}
  64. onClick={() => {
  65. setSelectedWidget(index);
  66. dispatch({
  67. type: BuilderStateAction.SET_STATE,
  68. payload: convertWidgetToBuilderStateParams(widget),
  69. });
  70. trackAnalytics('dashboards_views.widget_builder.templates.selected', {
  71. title: widget.title,
  72. widget_type: widget.widgetType ?? '',
  73. organization,
  74. });
  75. }}
  76. >
  77. <IconWrapper backgroundColor={iconColor}>
  78. <Icon color="white" />
  79. </IconWrapper>
  80. <div>
  81. <WidgetTitle>{widget.title}</WidgetTitle>
  82. <WidgetDescription>{widget.description}</WidgetDescription>
  83. {selectedWidget === index && (
  84. <ButtonsWrapper>
  85. <Button
  86. size="sm"
  87. onClick={e => {
  88. e.stopPropagation();
  89. setOpenWidgetTemplates(false);
  90. // reset preview when customizing templates
  91. setIsPreviewDraggable(false);
  92. trackAnalytics(
  93. 'dashboards_views.widget_builder.templates.customize',
  94. {
  95. title: widget.title,
  96. widget_type: widget.widgetType ?? '',
  97. organization,
  98. }
  99. );
  100. }}
  101. >
  102. {t('Customize')}
  103. </Button>
  104. <Button
  105. size="sm"
  106. onClick={e => {
  107. e.stopPropagation();
  108. handleSave(widget);
  109. trackAnalytics(
  110. 'dashboards_views.widget_builder.templates.add_to_dashboard',
  111. {
  112. title: widget.title,
  113. widget_type: widget.widgetType ?? '',
  114. organization,
  115. }
  116. );
  117. }}
  118. >
  119. {t('Add to dashboard')}
  120. </Button>
  121. </ButtonsWrapper>
  122. )}
  123. </div>
  124. </TemplateCard>
  125. </TemplateContainer>
  126. );
  127. })}
  128. </Fragment>
  129. );
  130. }
  131. export default WidgetTemplatesList;
  132. const TemplateContainer = styled('div')<{lastWidget: boolean}>`
  133. border-bottom: ${p => (p.lastWidget ? 'none' : `1px solid ${p.theme.border}`)};
  134. `;
  135. const TemplateCard = styled('div')<{selected: boolean}>`
  136. display: flex;
  137. flex-direction: row;
  138. gap: ${space(1.5)};
  139. padding: ${space(2)};
  140. border: none;
  141. border-radius: ${p => p.theme.borderRadius};
  142. background-color: ${p => (p.selected ? p.theme.purple100 : p.theme.background)};
  143. margin: ${p => (p.selected ? space(2) : space(0.5))} 0px;
  144. cursor: pointer;
  145. &:focus,
  146. &:hover {
  147. background-color: ${p => p.theme.purple100};
  148. outline: none;
  149. }
  150. &:active {
  151. background-color: ${p => p.theme.purple100};
  152. }
  153. `;
  154. const WidgetTitle = styled('h3')`
  155. font-size: ${p => p.theme.fontSizeLarge};
  156. font-weight: ${p => p.theme.fontWeightNormal};
  157. margin-bottom: ${space(0.25)};
  158. `;
  159. const WidgetDescription = styled('p')`
  160. font-size: ${p => p.theme.fontSizeMedium};
  161. color: ${p => p.theme.gray300};
  162. margin-bottom: 0;
  163. `;
  164. const IconWrapper = styled('div')<{backgroundColor: string}>`
  165. display: flex;
  166. justify-content: center;
  167. align-items: center;
  168. padding: ${space(1)};
  169. min-width: 40px;
  170. height: 40px;
  171. border-radius: ${p => p.theme.borderRadius};
  172. background: ${p => p.backgroundColor};
  173. `;
  174. const ButtonsWrapper = styled('div')`
  175. display: flex;
  176. flex-direction: row;
  177. gap: ${space(3)};
  178. margin-top: ${space(2)};
  179. `;