index.tsx 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314
  1. import {useEffect, useState} from 'react';
  2. import styled from '@emotion/styled';
  3. import moment from 'moment';
  4. import DatePageFilter from 'sentry/components/datePageFilter';
  5. import * as Layout from 'sentry/components/layouts/thirds';
  6. import {normalizeDateTimeParams} from 'sentry/components/organizations/pageFilters/parse';
  7. import TransactionNameSearchBar from 'sentry/components/performance/searchBar';
  8. import Switch from 'sentry/components/switchButton';
  9. import {t} from 'sentry/locale';
  10. import {space} from 'sentry/styles/space';
  11. import EventView from 'sentry/utils/discover/eventView';
  12. import {
  13. PageErrorAlert,
  14. PageErrorProvider,
  15. } from 'sentry/utils/performance/contexts/pageError';
  16. import {useApiQuery, useQuery} from 'sentry/utils/queryClient';
  17. import {MutableSearch} from 'sentry/utils/tokenizeSearch';
  18. import {useLocation} from 'sentry/utils/useLocation';
  19. import useOrganization from 'sentry/utils/useOrganization';
  20. import usePageFilters from 'sentry/utils/usePageFilters';
  21. import {
  22. getDbAggregatesQuery,
  23. useQueryMainTable,
  24. } from 'sentry/views/starfish/modules/databaseModule/queries';
  25. import combineTableDataWithSparklineData from 'sentry/views/starfish/utils/combineTableDataWithSparklineData';
  26. import {HOST} from 'sentry/views/starfish/utils/constants';
  27. import {datetimeToClickhouseFilterTimestamps} from 'sentry/views/starfish/utils/dates';
  28. import DatabaseChartView from './databaseChartView';
  29. import DatabaseTableView, {DataRow, MainTableSort} from './databaseTableView';
  30. import QueryDetail from './panel';
  31. export type Sort<T> = {
  32. direction: 'desc' | 'asc' | undefined;
  33. sortHeader: T | undefined;
  34. };
  35. function DatabaseModule() {
  36. const location = useLocation();
  37. const organization = useOrganization();
  38. const eventView = EventView.fromLocation(location);
  39. const [table, setTable] = useState<string>('ALL');
  40. const [filterNew, setFilterNew] = useState<boolean>(false);
  41. const [filterOld, setFilterOld] = useState<boolean>(false);
  42. const [filterOutlier, setFilterOutlier] = useState<boolean>(true);
  43. const [transaction, setTransaction] = useState<string>('');
  44. const [sort, setSort] = useState<MainTableSort>({
  45. direction: undefined,
  46. sortHeader: undefined,
  47. });
  48. const [rows, setRows] = useState<{next?: DataRow; prev?: DataRow; selected?: DataRow}>({
  49. selected: undefined,
  50. next: undefined,
  51. prev: undefined,
  52. });
  53. const {
  54. isLoading: isTableDataLoading,
  55. data: tableData,
  56. isRefetching: isTableRefetching,
  57. } = useQueryMainTable({
  58. transaction,
  59. table,
  60. filterNew,
  61. filterOld,
  62. filterOutlier,
  63. sortKey: sort.sortHeader?.key,
  64. sortDirection: sort.direction,
  65. });
  66. const pageFilters = usePageFilters();
  67. // Experiments
  68. const [p95asNumber, setp95asNumber] = useState<boolean>(false);
  69. const [noP95, setnoP95] = useState<boolean>(false);
  70. const {selection} = pageFilters;
  71. const {projects, environments, datetime} = selection;
  72. useApiQuery<null>(
  73. [
  74. `/organizations/${organization.slug}/events-starfish/`,
  75. {
  76. query: {
  77. ...{
  78. environment: environments,
  79. project: projects.map(proj => String(proj)),
  80. },
  81. ...normalizeDateTimeParams(datetime),
  82. },
  83. },
  84. ],
  85. {
  86. staleTime: 10,
  87. }
  88. );
  89. const {data: dbAggregateData} = useQuery({
  90. queryKey: [
  91. 'dbAggregates',
  92. transaction,
  93. filterNew,
  94. filterOld,
  95. pageFilters.selection.datetime,
  96. ],
  97. queryFn: () =>
  98. fetch(
  99. `${HOST}/?query=${getDbAggregatesQuery({
  100. datetime: pageFilters.selection.datetime,
  101. transaction,
  102. })}`
  103. ).then(res => res.json()),
  104. retry: false,
  105. initialData: [],
  106. });
  107. const {start_timestamp, end_timestamp} = datetimeToClickhouseFilterTimestamps(
  108. pageFilters.selection.datetime
  109. );
  110. const combinedDbData = combineTableDataWithSparklineData(
  111. tableData,
  112. dbAggregateData,
  113. moment.duration(12, 'hours'),
  114. moment(start_timestamp),
  115. moment(end_timestamp)
  116. );
  117. useEffect(() => {
  118. function handleKeyDown({keyCode}) {
  119. setRows(currentRow => {
  120. if (currentRow.selected) {
  121. if (currentRow.prev && keyCode === 37) {
  122. return getUpdatedRows(currentRow.prev);
  123. }
  124. if (currentRow.next && keyCode === 39) {
  125. return getUpdatedRows(currentRow.next);
  126. }
  127. }
  128. return currentRow;
  129. });
  130. }
  131. document.addEventListener('keydown', handleKeyDown);
  132. return function cleanup() {
  133. document.removeEventListener('keydown', handleKeyDown);
  134. };
  135. // eslint-disable-next-line react-hooks/exhaustive-deps
  136. }, [tableData]);
  137. const toggleFilterNew = () => {
  138. setFilterNew(!filterNew);
  139. if (!filterNew) {
  140. setFilterOld(false);
  141. }
  142. };
  143. const toggleFilterOld = () => {
  144. setFilterOld(!filterOld);
  145. if (!filterOld) {
  146. setFilterNew(false);
  147. }
  148. };
  149. const toggleOutlier = () => {
  150. setFilterOutlier(!filterOutlier);
  151. };
  152. const toggleNoP95 = () => {
  153. setnoP95(!noP95);
  154. };
  155. const toggleP95asNumber = () => {
  156. setp95asNumber(!p95asNumber);
  157. };
  158. const getUpdatedRows = (row: DataRow, rowIndex?: number) => {
  159. rowIndex ??= tableData.findIndex(data => data.group_id === row.group_id);
  160. const prevRow = rowIndex > 0 ? tableData[rowIndex - 1] : undefined;
  161. const nextRow = rowIndex < tableData.length - 1 ? tableData[rowIndex + 1] : undefined;
  162. return {selected: row, next: nextRow, prev: prevRow};
  163. };
  164. const setSelectedRow = (row: DataRow, rowIndex?: number) => {
  165. setRows(getUpdatedRows(row, rowIndex));
  166. };
  167. const unsetSelectedSpanGroup = () => setRows({selected: undefined});
  168. const handleSearch = (query: string) => {
  169. const conditions = new MutableSearch(query);
  170. const transactionValues = conditions.getFilterValues('transaction');
  171. if (transactionValues.length) {
  172. setTransaction(transactionValues[0]);
  173. return;
  174. }
  175. if (conditions.freeText.length > 0) {
  176. // so no need to wrap it here
  177. setTransaction(conditions.freeText.join(' '));
  178. return;
  179. }
  180. setTransaction('');
  181. };
  182. return (
  183. <Layout.Page>
  184. <PageErrorProvider>
  185. <Layout.Header>
  186. <Layout.HeaderContent>
  187. <Layout.Title>{t('Database')}</Layout.Title>
  188. </Layout.HeaderContent>
  189. </Layout.Header>
  190. <Layout.Body>
  191. <Layout.Main fullWidth>
  192. <PageErrorAlert />
  193. <FilterOptionsContainer>
  194. <DatePageFilter alignDropdown="left" />
  195. </FilterOptionsContainer>
  196. <DatabaseChartView location={location} table={table} onChange={setTable} />
  197. <SearchFilterContainer>
  198. <LabelledSwitch
  199. label="Filter New Queries"
  200. isActive={filterNew}
  201. size="lg"
  202. toggle={toggleFilterNew}
  203. />
  204. <LabelledSwitch
  205. label="Filter Old Queries"
  206. isActive={filterOld}
  207. size="lg"
  208. toggle={toggleFilterOld}
  209. />
  210. <LabelledSwitch
  211. label="Exclude Outliers"
  212. isActive={filterOutlier}
  213. size="lg"
  214. toggle={toggleOutlier}
  215. />
  216. </SearchFilterContainer>
  217. <SearchFilterContainer>
  218. <TransactionNameSearchBar
  219. organization={organization}
  220. eventView={eventView}
  221. onSearch={(query: string) => handleSearch(query)}
  222. query={transaction}
  223. />
  224. </SearchFilterContainer>
  225. <DatabaseTableView
  226. location={location}
  227. data={combinedDbData as DataRow[]}
  228. isDataLoading={isTableDataLoading || isTableRefetching}
  229. onSelect={setSelectedRow}
  230. onSortChange={setSort}
  231. selectedRow={rows.selected}
  232. p95asNumber={p95asNumber}
  233. noP95={noP95}
  234. />
  235. <QueryDetail
  236. isDataLoading={isTableDataLoading || isTableRefetching}
  237. onRowChange={row => {
  238. setSelectedRow(row);
  239. }}
  240. mainTableSort={sort}
  241. row={rows.selected}
  242. nextRow={rows.next}
  243. prevRow={rows.prev}
  244. onClose={unsetSelectedSpanGroup}
  245. transaction={transaction}
  246. />
  247. <div>Experiments</div>
  248. <LabelledSwitch
  249. label="p95 as number"
  250. isActive={p95asNumber}
  251. size="lg"
  252. toggle={toggleP95asNumber}
  253. />
  254. <LabelledSwitch
  255. label="No p95"
  256. isActive={noP95}
  257. size="lg"
  258. toggle={toggleNoP95}
  259. />
  260. </Layout.Main>
  261. </Layout.Body>
  262. </PageErrorProvider>
  263. </Layout.Page>
  264. );
  265. }
  266. export default DatabaseModule;
  267. const FilterOptionsContainer = styled('div')`
  268. display: flex;
  269. flex-direction: row;
  270. gap: ${space(1)};
  271. margin-bottom: ${space(2)};
  272. `;
  273. const SearchFilterContainer = styled('div')`
  274. margin-bottom: ${space(2)};
  275. `;
  276. function LabelledSwitch(props) {
  277. return (
  278. <span
  279. style={{
  280. display: 'inline-flex',
  281. gap: space(1),
  282. paddingRight: space(2),
  283. alignItems: 'center',
  284. }}
  285. >
  286. <span>{props.label}</span>
  287. <Switch {...props} />
  288. </span>
  289. );
  290. }