devBuilder.tsx 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278
  1. import styled from '@emotion/styled';
  2. import RadioGroup from 'sentry/components/forms/controls/radioGroup';
  3. import SelectField from 'sentry/components/forms/fields/selectField';
  4. import Input from 'sentry/components/input';
  5. import {space} from 'sentry/styles/space';
  6. import {CustomMeasurementsProvider} from 'sentry/utils/customMeasurements/customMeasurementsProvider';
  7. import {type Column, generateFieldAsString} from 'sentry/utils/discover/fields';
  8. import {useLocalStorageState} from 'sentry/utils/useLocalStorageState';
  9. import useOrganization from 'sentry/utils/useOrganization';
  10. import usePageFilters from 'sentry/utils/usePageFilters';
  11. import {getDatasetConfig} from 'sentry/views/dashboards/datasetConfig/base';
  12. import {DisplayType, WidgetType} from 'sentry/views/dashboards/types';
  13. import {ColumnFields} from 'sentry/views/dashboards/widgetBuilder/buildSteps/columnsStep/columnFields';
  14. import {YAxisSelector} from 'sentry/views/dashboards/widgetBuilder/buildSteps/yAxisStep/yAxisSelector';
  15. import {useWidgetBuilderContext} from 'sentry/views/dashboards/widgetBuilder/contexts/widgetBuilderContext';
  16. import {BuilderStateAction} from 'sentry/views/dashboards/widgetBuilder/hooks/useWidgetBuilderState';
  17. import {getDiscoverDatasetFromWidgetType} from 'sentry/views/dashboards/widgetBuilder/utils';
  18. import ResultsSearchQueryBuilder from 'sentry/views/discover/resultsSearchQueryBuilder';
  19. function DevBuilder() {
  20. const {state, dispatch} = useWidgetBuilderContext();
  21. const [showDevBuilder] = useLocalStorageState('showDevBuilder', false);
  22. if (!showDevBuilder) {
  23. return null;
  24. }
  25. return (
  26. <Body>
  27. <Section>
  28. <h1>Title:</h1>
  29. <div style={{flex: 1}}>{state.title}</div>
  30. <SimpleInput
  31. value={state.title}
  32. onChange={e =>
  33. dispatch({type: BuilderStateAction.SET_TITLE, payload: e.target.value})
  34. }
  35. />
  36. </Section>
  37. <Section>
  38. <h1>Description:</h1>
  39. <div style={{flex: 1}}>{state.description}</div>
  40. <SimpleInput
  41. value={state.description}
  42. onChange={e =>
  43. dispatch({
  44. type: BuilderStateAction.SET_DESCRIPTION,
  45. payload: e.target.value,
  46. })
  47. }
  48. />
  49. </Section>
  50. <Section>
  51. <h1>Display Type:</h1>
  52. <div style={{flex: 1}}>{state.displayType}</div>
  53. <SelectField
  54. name="displayType"
  55. value={state.displayType}
  56. options={Object.values(DisplayType).map(value => ({
  57. label: value,
  58. value,
  59. }))}
  60. onChange={newValue =>
  61. dispatch({
  62. type: BuilderStateAction.SET_DISPLAY_TYPE,
  63. payload: newValue,
  64. })
  65. }
  66. style={{width: '200px'}}
  67. />
  68. </Section>
  69. <Section>
  70. <h1>Dataset:</h1>
  71. <div>{state.dataset}</div>
  72. <RadioGroup
  73. label="Dataset"
  74. value={state.dataset ?? null}
  75. choices={[
  76. [WidgetType.DISCOVER, 'Discover'],
  77. [WidgetType.ISSUE, 'Issue'],
  78. [WidgetType.RELEASE, 'Release'],
  79. [WidgetType.METRICS, 'Metrics'],
  80. [WidgetType.ERRORS, 'Errors'],
  81. [WidgetType.TRANSACTIONS, 'Transactions'],
  82. ]}
  83. onChange={newValue =>
  84. dispatch({
  85. type: BuilderStateAction.SET_DATASET,
  86. payload: newValue,
  87. })
  88. }
  89. />
  90. </Section>
  91. <Section>
  92. <h1>Fields:</h1>
  93. <div>{state.fields?.map(generateFieldAsString).join(', ')}</div>
  94. <ColumnSelector
  95. displayType={state.displayType ?? DisplayType.TABLE}
  96. dataset={state.dataset ?? WidgetType.DISCOVER}
  97. fields={state.fields ?? [{field: '', kind: 'field'}]}
  98. onChange={newFields => {
  99. dispatch({
  100. type: BuilderStateAction.SET_FIELDS,
  101. payload: newFields,
  102. });
  103. }}
  104. />
  105. </Section>
  106. <Section>
  107. <h1>Y-Axis:</h1>
  108. <div>{state.yAxis?.map(generateFieldAsString).join(', ')}</div>
  109. <YAxis
  110. displayType={state.displayType ?? DisplayType.TABLE}
  111. widgetType={state.dataset ?? WidgetType.DISCOVER}
  112. aggregates={state.yAxis ?? [{field: '', kind: 'field'}]}
  113. onChange={newAggregates => {
  114. dispatch({
  115. type: BuilderStateAction.SET_Y_AXIS,
  116. payload: newAggregates,
  117. });
  118. }}
  119. />
  120. </Section>
  121. <Section>
  122. <h1>Query:</h1>
  123. <ol>{state.query?.map((query, index) => <li key={index}>{query}</li>)}</ol>
  124. <div>
  125. {state.query?.map((query, index) => (
  126. <div key={index} style={{display: 'flex', flexDirection: 'row'}}>
  127. <QueryField
  128. query={query}
  129. widgetType={state.dataset ?? WidgetType.DISCOVER}
  130. fields={state.fields ?? []}
  131. key={index}
  132. onSearch={queryString => {
  133. dispatch({
  134. type: BuilderStateAction.SET_QUERY,
  135. payload:
  136. state.query?.map((q, i) => (i === index ? queryString : q)) ?? [],
  137. });
  138. }}
  139. />
  140. <button
  141. onClick={() =>
  142. dispatch({
  143. type: BuilderStateAction.SET_QUERY,
  144. payload: state.query?.filter((_, i) => i !== index) ?? [],
  145. })
  146. }
  147. >
  148. Remove
  149. </button>
  150. </div>
  151. ))}
  152. <button
  153. onClick={() =>
  154. dispatch({
  155. type: BuilderStateAction.SET_QUERY,
  156. payload: [...(state.query ?? []), ''],
  157. })
  158. }
  159. >
  160. Add
  161. </button>
  162. </div>
  163. </Section>
  164. </Body>
  165. );
  166. }
  167. function ColumnSelector({
  168. displayType,
  169. fields,
  170. dataset,
  171. onChange,
  172. }: {
  173. dataset: WidgetType;
  174. displayType: DisplayType;
  175. fields: Column[];
  176. onChange: (newFields: Column[]) => void;
  177. }) {
  178. const organization = useOrganization();
  179. const datasetConfig = getDatasetConfig(dataset);
  180. const fieldOptions = datasetConfig.getTableFieldOptions(organization);
  181. return (
  182. <ColumnFields
  183. displayType={displayType ?? DisplayType.TABLE}
  184. organization={organization}
  185. widgetType={dataset ?? WidgetType.DISCOVER}
  186. fields={fields ?? []}
  187. errors={[]}
  188. fieldOptions={fieldOptions}
  189. isOnDemandWidget={false}
  190. filterAggregateParameters={() => true}
  191. filterPrimaryOptions={() => true}
  192. onChange={onChange}
  193. />
  194. );
  195. }
  196. function YAxis({
  197. displayType,
  198. widgetType,
  199. aggregates,
  200. onChange,
  201. }: {
  202. aggregates: Column[];
  203. displayType: DisplayType;
  204. onChange: (newFields: Column[]) => void;
  205. widgetType: WidgetType;
  206. }) {
  207. const organization = useOrganization();
  208. return (
  209. <CustomMeasurementsProvider organization={organization}>
  210. <YAxisSelector
  211. widgetType={widgetType}
  212. displayType={displayType}
  213. aggregates={aggregates}
  214. onChange={onChange}
  215. tags={{}}
  216. errors={[]}
  217. selectedAggregate={undefined}
  218. />
  219. </CustomMeasurementsProvider>
  220. );
  221. }
  222. function QueryField({
  223. query,
  224. widgetType,
  225. fields,
  226. onSearch,
  227. }: {
  228. fields: Column[];
  229. onSearch: (query: string) => void;
  230. query: string;
  231. widgetType: WidgetType;
  232. }) {
  233. const pageFilters = usePageFilters();
  234. return (
  235. <ResultsSearchQueryBuilder
  236. projectIds={pageFilters.selection.projects}
  237. query={query}
  238. fields={fields as any}
  239. onSearch={onSearch}
  240. dataset={getDiscoverDatasetFromWidgetType(widgetType)}
  241. includeTransactions
  242. searchSource="widget_builder"
  243. />
  244. );
  245. }
  246. const Body = styled('div')`
  247. margin: ${space(2)};
  248. padding: ${space(2)};
  249. `;
  250. const Section = styled('section')`
  251. display: flex;
  252. flex-direction: row;
  253. justify-content: space-around;
  254. border: 1px solid ${p => p.theme.border};
  255. align-items: center;
  256. > * {
  257. flex: 1;
  258. }
  259. `;
  260. const SimpleInput = styled(Input)`
  261. width: 100%;
  262. `;
  263. export default DevBuilder;