releasesSelectControl.tsx 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133
  1. import {useEffect, useState} from 'react';
  2. import styled from '@emotion/styled';
  3. import debounce from 'lodash/debounce';
  4. import Badge from 'sentry/components/badge';
  5. import {CompactSelect} from 'sentry/components/compactSelect';
  6. import TextOverflow from 'sentry/components/textOverflow';
  7. import {DEFAULT_DEBOUNCE_DURATION} from 'sentry/constants';
  8. import {IconReleases} from 'sentry/icons';
  9. import {t} from 'sentry/locale';
  10. import {space} from 'sentry/styles/space';
  11. import {useReleases} from 'sentry/utils/releases/releasesProvider';
  12. import {DashboardFilterKeys, DashboardFilters} from './types';
  13. type Props = {
  14. selectedReleases: string[];
  15. className?: string;
  16. handleChangeFilter?: (activeFilters: DashboardFilters) => void;
  17. isDisabled?: boolean;
  18. };
  19. const ALIASED_RELEASES = [
  20. {
  21. label: t('Latest Release(s)'),
  22. value: 'latest',
  23. },
  24. ];
  25. function ReleasesSelectControl({
  26. handleChangeFilter,
  27. selectedReleases,
  28. className,
  29. isDisabled,
  30. }: Props) {
  31. const {releases, loading, onSearch} = useReleases();
  32. const [activeReleases, setActiveReleases] = useState<string[]>(selectedReleases);
  33. function resetSearch() {
  34. onSearch('');
  35. }
  36. useEffect(() => {
  37. setActiveReleases(selectedReleases);
  38. }, [selectedReleases]);
  39. const triggerLabel = activeReleases.length ? (
  40. <TextOverflow>{activeReleases[0]} </TextOverflow>
  41. ) : (
  42. t('All Releases')
  43. );
  44. const activeReleasesSet = new Set(activeReleases);
  45. return (
  46. <StyledCompactSelect
  47. multiple
  48. clearable
  49. searchable
  50. disabled={isDisabled}
  51. loading={loading}
  52. menuTitle={<MenuTitleWrapper>{t('Filter Releases')}</MenuTitleWrapper>}
  53. className={className}
  54. onSearch={debounce(val => {
  55. onSearch(val);
  56. }, DEFAULT_DEBOUNCE_DURATION)}
  57. options={[
  58. {
  59. value: '_releases',
  60. label: t('Sorted by date created'),
  61. options: [
  62. ...ALIASED_RELEASES,
  63. ...activeReleases
  64. .filter(version => version !== 'latest')
  65. .map(version => ({
  66. label: version,
  67. value: version,
  68. })),
  69. ...releases
  70. .filter(({version}) => !activeReleasesSet.has(version))
  71. .map(({version}) => ({
  72. label: version,
  73. value: version,
  74. })),
  75. ],
  76. },
  77. ]}
  78. onChange={opts => setActiveReleases(opts.map(opt => opt.value as string))}
  79. onClose={() => {
  80. resetSearch();
  81. handleChangeFilter?.({
  82. [DashboardFilterKeys.RELEASE]: activeReleases,
  83. });
  84. }}
  85. value={activeReleases}
  86. triggerLabel={
  87. <ButtonLabelWrapper>
  88. {triggerLabel}{' '}
  89. {activeReleases.length > 1 && (
  90. <StyledBadge text={`+${activeReleases.length - 1}`} />
  91. )}
  92. </ButtonLabelWrapper>
  93. }
  94. triggerProps={{icon: <IconReleases />}}
  95. />
  96. );
  97. }
  98. export default ReleasesSelectControl;
  99. const StyledBadge = styled(Badge)`
  100. flex-shrink: 0;
  101. `;
  102. const StyledCompactSelect = styled(CompactSelect)`
  103. @media (min-width: ${p => p.theme.breakpoints.small}) {
  104. max-width: 300px;
  105. }
  106. `;
  107. const ButtonLabelWrapper = styled('span')`
  108. width: 100%;
  109. text-align: left;
  110. align-items: center;
  111. display: inline-grid;
  112. grid-template-columns: 1fr auto;
  113. `;
  114. const MenuTitleWrapper = styled('span')`
  115. display: inline-block;
  116. padding-top: ${space(0.5)};
  117. padding-bottom: ${space(0.5)};
  118. `;