index.tsx 8.2 KB

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