tracesSearchBar.tsx 4.6 KB

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