index.tsx 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253
  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 TransactionNameSearchBar from 'sentry/components/performance/searchBar';
  7. import Switch from 'sentry/components/switchButton';
  8. import {t} from 'sentry/locale';
  9. import {space} from 'sentry/styles/space';
  10. import EventView from 'sentry/utils/discover/eventView';
  11. import {
  12. PageErrorAlert,
  13. PageErrorProvider,
  14. } from 'sentry/utils/performance/contexts/pageError';
  15. import {useQuery} from 'sentry/utils/queryClient';
  16. import {MutableSearch} from 'sentry/utils/tokenizeSearch';
  17. import {useLocation} from 'sentry/utils/useLocation';
  18. import useOrganization from 'sentry/utils/useOrganization';
  19. import usePageFilters from 'sentry/utils/usePageFilters';
  20. import {
  21. getDbAggregatesQuery,
  22. useQueryMainTable,
  23. } from 'sentry/views/starfish/modules/databaseModule/queries';
  24. import combineTableDataWithSparklineData from 'sentry/views/starfish/utils/combineTableDataWithSparklineData';
  25. import {HOST} from 'sentry/views/starfish/utils/constants';
  26. import DatabaseChartView from './databaseChartView';
  27. import DatabaseTableView, {DataRow, MainTableSort} from './databaseTableView';
  28. import QueryDetail from './panel';
  29. export type Sort<T> = {
  30. direction: 'desc' | 'asc' | undefined;
  31. sortHeader: T | undefined;
  32. };
  33. function DatabaseModule() {
  34. const location = useLocation();
  35. const organization = useOrganization();
  36. const eventView = EventView.fromLocation(location);
  37. const [table, setTable] = useState<string>('ALL');
  38. const [filterNew, setFilterNew] = useState<boolean>(false);
  39. const [filterOld, setFilterOld] = useState<boolean>(false);
  40. const [transaction, setTransaction] = useState<string>('');
  41. const [sort, setSort] = useState<MainTableSort>({
  42. direction: undefined,
  43. sortHeader: undefined,
  44. });
  45. const [rows, setRows] = useState<{next?: DataRow; prev?: DataRow; selected?: DataRow}>({
  46. selected: undefined,
  47. next: undefined,
  48. prev: undefined,
  49. });
  50. const {
  51. isLoading: isTableDataLoading,
  52. data: tableData,
  53. isRefetching: isTableRefetching,
  54. } = useQueryMainTable({
  55. transaction,
  56. table,
  57. filterNew,
  58. filterOld,
  59. sortKey: sort.sortHeader?.key,
  60. sortDirection: sort.direction,
  61. });
  62. const pageFilters = usePageFilters();
  63. const {data: dbAggregateData} = useQuery({
  64. queryKey: ['dbAggregates', transaction, filterNew, filterOld],
  65. queryFn: () =>
  66. fetch(
  67. `${HOST}/?query=${getDbAggregatesQuery({
  68. datetime: pageFilters.selection.datetime,
  69. transaction,
  70. })}`
  71. ).then(res => res.json()),
  72. retry: false,
  73. initialData: [],
  74. });
  75. const combinedDbData = combineTableDataWithSparklineData(
  76. tableData,
  77. dbAggregateData,
  78. moment.duration(12, 'hours')
  79. );
  80. const aggregatesGroupedByQuery = {};
  81. dbAggregateData.forEach(({description, interval, count, p75}) => {
  82. if (description in aggregatesGroupedByQuery) {
  83. aggregatesGroupedByQuery[description].push({name: interval, count, p75});
  84. } else {
  85. aggregatesGroupedByQuery[description] = [{name: interval, count, p75}];
  86. }
  87. });
  88. useEffect(() => {
  89. function handleKeyDown({keyCode}) {
  90. setRows(currentRow => {
  91. if (currentRow.selected) {
  92. if (currentRow.prev && keyCode === 37) {
  93. return getUpdatedRows(currentRow.prev);
  94. }
  95. if (currentRow.next && keyCode === 39) {
  96. return getUpdatedRows(currentRow.next);
  97. }
  98. }
  99. return currentRow;
  100. });
  101. }
  102. document.addEventListener('keydown', handleKeyDown);
  103. return function cleanup() {
  104. document.removeEventListener('keydown', handleKeyDown);
  105. };
  106. // eslint-disable-next-line react-hooks/exhaustive-deps
  107. }, [tableData]);
  108. const toggleFilterNew = () => {
  109. setFilterNew(!filterNew);
  110. if (!filterNew) {
  111. setFilterOld(false);
  112. }
  113. };
  114. const toggleFilterOld = () => {
  115. setFilterOld(!filterOld);
  116. if (!filterOld) {
  117. setFilterNew(false);
  118. }
  119. };
  120. const getUpdatedRows = (row: DataRow, rowIndex?: number) => {
  121. rowIndex ??= tableData.findIndex(data => data.group_id === row.group_id);
  122. const prevRow = rowIndex > 0 ? tableData[rowIndex - 1] : undefined;
  123. const nextRow = rowIndex < tableData.length - 1 ? tableData[rowIndex + 1] : undefined;
  124. return {selected: row, next: nextRow, prev: prevRow};
  125. };
  126. const setSelectedRow = (row: DataRow, rowIndex?: number) => {
  127. setRows(getUpdatedRows(row, rowIndex));
  128. };
  129. const unsetSelectedSpanGroup = () => setRows({selected: undefined});
  130. const handleSearch = (query: string) => {
  131. const conditions = new MutableSearch(query);
  132. const transactionValues = conditions.getFilterValues('transaction');
  133. if (transactionValues.length) {
  134. setTransaction(transactionValues[0]);
  135. return;
  136. }
  137. if (conditions.freeText.length > 0) {
  138. // so no need to wrap it here
  139. setTransaction(conditions.freeText.join(' '));
  140. return;
  141. }
  142. setTransaction('');
  143. };
  144. return (
  145. <Layout.Page>
  146. <PageErrorProvider>
  147. <Layout.Header>
  148. <Layout.HeaderContent>
  149. <Layout.Title>{t('Database')}</Layout.Title>
  150. </Layout.HeaderContent>
  151. </Layout.Header>
  152. <Layout.Body>
  153. <Layout.Main fullWidth>
  154. <PageErrorAlert />
  155. <FilterOptionsContainer>
  156. <DatePageFilter alignDropdown="left" />
  157. </FilterOptionsContainer>
  158. <DatabaseChartView location={location} table={table} onChange={setTable} />
  159. <SearchFilterContainer>
  160. <LabelledSwitch
  161. label="Filter New Queries"
  162. isActive={filterNew}
  163. size="lg"
  164. toggle={toggleFilterNew}
  165. />
  166. <LabelledSwitch
  167. label="Filter Old Queries"
  168. isActive={filterOld}
  169. size="lg"
  170. toggle={toggleFilterOld}
  171. />
  172. </SearchFilterContainer>
  173. <SearchFilterContainer>
  174. <TransactionNameSearchBar
  175. organization={organization}
  176. eventView={eventView}
  177. onSearch={(query: string) => handleSearch(query)}
  178. query={transaction}
  179. />
  180. </SearchFilterContainer>
  181. <DatabaseTableView
  182. location={location}
  183. data={combinedDbData as DataRow[]}
  184. isDataLoading={isTableDataLoading || isTableRefetching}
  185. onSelect={setSelectedRow}
  186. onSortChange={setSort}
  187. selectedRow={rows.selected}
  188. />
  189. <QueryDetail
  190. isDataLoading={isTableDataLoading || isTableRefetching}
  191. onRowChange={row => {
  192. setSelectedRow(row);
  193. }}
  194. mainTableSort={sort}
  195. row={rows.selected}
  196. nextRow={rows.next}
  197. prevRow={rows.prev}
  198. onClose={unsetSelectedSpanGroup}
  199. transaction={transaction}
  200. />
  201. </Layout.Main>
  202. </Layout.Body>
  203. </PageErrorProvider>
  204. </Layout.Page>
  205. );
  206. }
  207. export default DatabaseModule;
  208. const FilterOptionsContainer = styled('div')`
  209. display: flex;
  210. flex-direction: row;
  211. gap: ${space(1)};
  212. margin-bottom: ${space(2)};
  213. `;
  214. const SearchFilterContainer = styled('div')`
  215. margin-bottom: ${space(2)};
  216. `;
  217. function LabelledSwitch(props) {
  218. return (
  219. <span
  220. style={{
  221. display: 'inline-flex',
  222. gap: space(1),
  223. paddingRight: space(2),
  224. alignItems: 'center',
  225. }}
  226. >
  227. <span>{props.label}</span>
  228. <Switch {...props} />
  229. </span>
  230. );
  231. }