tracesSearchBar.tsx 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170
  1. import styled from '@emotion/styled';
  2. import {Button} from 'sentry/components/button';
  3. import SearchBar, {getHasTag} from 'sentry/components/events/searchBar';
  4. import {IconAdd, IconClose} from 'sentry/icons';
  5. import {t} from 'sentry/locale';
  6. import {space} from 'sentry/styles/space';
  7. import type {TagCollection} from 'sentry/types/group';
  8. import {DiscoverDatasets} from 'sentry/utils/discover/types';
  9. import {type ApiQueryKey, useApiQuery} from 'sentry/utils/queryClient';
  10. import useOrganization from 'sentry/utils/useOrganization';
  11. import usePageFilters from 'sentry/utils/usePageFilters';
  12. import {SpanIndexedField} from 'sentry/views/starfish/types';
  13. interface TracesSearchBarProps {
  14. handleClearSearch: (index: number) => boolean;
  15. handleSearch: (index: number, query: string) => void;
  16. queries: string[];
  17. }
  18. const getSpanName = (index: number) => {
  19. const spanNames = [
  20. t('Find traces where a span is'),
  21. t('and another span where'),
  22. t('and another span where'),
  23. ];
  24. return spanNames[index];
  25. };
  26. const omitSupportedTags = [SpanIndexedField.SPAN_AI_PIPELINE_GROUP];
  27. const getTracesSupportedTags = () => {
  28. const tags: TagCollection = Object.fromEntries(
  29. Object.values(SpanIndexedField)
  30. .filter(v => !omitSupportedTags.includes(v))
  31. .map(v => [v, {key: v, name: v}])
  32. );
  33. tags.has = getHasTag(tags);
  34. return tags;
  35. };
  36. interface SpanFieldEntry {
  37. key: string;
  38. name: string;
  39. }
  40. type SpanFieldsResponse = SpanFieldEntry[];
  41. const getDynamicSpanFieldsEndpoint = (orgSlug: string, selection): ApiQueryKey => [
  42. `/organizations/${orgSlug}/spans/fields/`,
  43. {
  44. query: {
  45. project: selection.projects,
  46. environment: selection.environments,
  47. statsPeriod: '1h', // Hard coded stats period to load recent tags fast
  48. },
  49. },
  50. ];
  51. const useTracesSupportedTags = (): TagCollection => {
  52. const {selection} = usePageFilters();
  53. const organization = useOrganization();
  54. const staticTags = getTracesSupportedTags();
  55. const dynamicTagQuery = useApiQuery<SpanFieldsResponse>(
  56. getDynamicSpanFieldsEndpoint(organization.slug, selection),
  57. {
  58. staleTime: 0,
  59. retry: false,
  60. }
  61. );
  62. if (dynamicTagQuery.isSuccess) {
  63. const dynamicTags: TagCollection = Object.fromEntries(
  64. dynamicTagQuery.data.map(entry => [entry.key, entry])
  65. );
  66. return {
  67. ...dynamicTags,
  68. ...staticTags,
  69. };
  70. }
  71. return staticTags;
  72. };
  73. export function TracesSearchBar({
  74. queries,
  75. handleSearch,
  76. handleClearSearch,
  77. }: TracesSearchBarProps) {
  78. // TODO: load tags for autocompletion
  79. const {selection} = usePageFilters();
  80. const organization = useOrganization();
  81. const canAddMoreQueries = queries.length <= 2;
  82. const localQueries = queries.length ? queries : [''];
  83. const supportedTags = useTracesSupportedTags();
  84. return (
  85. <TraceSearchBarsContainer>
  86. {localQueries.map((query, index) => (
  87. <TraceBar key={index}>
  88. <SpanLetter>{getSpanName(index)}</SpanLetter>
  89. <StyledSearchBar
  90. searchSource="trace-explorer"
  91. query={query}
  92. onSearch={(queryString: string) => handleSearch(index, queryString)}
  93. placeholder={t('Search for span attributes')}
  94. organization={organization}
  95. metricAlert={false}
  96. supportedTags={supportedTags}
  97. dataset={DiscoverDatasets.SPANS_INDEXED}
  98. projectIds={selection.projects}
  99. />
  100. <StyledButton
  101. aria-label={t('Remove span')}
  102. icon={<IconClose size="sm" />}
  103. size="sm"
  104. onClick={() => (queries.length === 0 ? false : handleClearSearch(index))}
  105. />
  106. </TraceBar>
  107. ))}
  108. {canAddMoreQueries ? (
  109. <Button
  110. aria-label={t('Add query')}
  111. icon={<IconAdd size="xs" isCircled />}
  112. size="sm"
  113. onClick={() => handleSearch(localQueries.length, '')}
  114. >
  115. {t('Add Span')}
  116. </Button>
  117. ) : null}
  118. </TraceSearchBarsContainer>
  119. );
  120. }
  121. const TraceSearchBarsContainer = styled('div')`
  122. display: flex;
  123. flex-direction: column;
  124. align-items: flex-start;
  125. justify-content: center;
  126. gap: ${space(1)};
  127. `;
  128. const TraceBar = styled('div')`
  129. display: flex;
  130. flex-direction: row;
  131. align-items: center;
  132. justify-content: flex-start;
  133. width: 100%;
  134. gap: ${space(1)};
  135. `;
  136. const SpanLetter = styled('div')`
  137. background-color: ${p => p.theme.purple100};
  138. border-radius: ${p => p.theme.borderRadius};
  139. padding: ${space(1)} ${space(2)};
  140. text-align: center;
  141. min-width: 220px;
  142. color: ${p => p.theme.purple400};
  143. white-space: nowrap;
  144. font-weight: 800;
  145. `;
  146. const StyledSearchBar = styled(SearchBar)`
  147. width: 100%;
  148. `;
  149. const StyledButton = styled(Button)`
  150. height: 38px;
  151. `;