controls.tsx 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255
  1. import {RouteComponentProps} from 'react-router';
  2. import {useTheme} from '@emotion/react';
  3. import styled from '@emotion/styled';
  4. import {LocationDescriptorObject} from 'history';
  5. import pick from 'lodash/pick';
  6. import uniq from 'lodash/uniq';
  7. import moment from 'moment';
  8. import SelectControl from 'sentry/components/forms/controls/selectControl';
  9. import TeamSelector from 'sentry/components/teamSelector';
  10. import {ChangeData, TimeRangeSelector} from 'sentry/components/timeRangeSelector';
  11. import {t} from 'sentry/locale';
  12. import {space} from 'sentry/styles/space';
  13. import {DateString, TeamWithProjects} from 'sentry/types';
  14. import {isActiveSuperuser} from 'sentry/utils/isActiveSuperuser';
  15. import localStorage from 'sentry/utils/localStorage';
  16. import useOrganization from 'sentry/utils/useOrganization';
  17. import useProjects from 'sentry/utils/useProjects';
  18. import {dataDatetime} from './utils';
  19. const INSIGHTS_DEFAULT_STATS_PERIOD = '8w';
  20. const relativeOptions = {
  21. '14d': t('Last 2 weeks'),
  22. '4w': t('Last 4 weeks'),
  23. [INSIGHTS_DEFAULT_STATS_PERIOD]: t('Last 8 weeks'),
  24. '12w': t('Last 12 weeks'),
  25. };
  26. const PAGE_QUERY_PARAMS = [
  27. 'pageStatsPeriod',
  28. 'pageStart',
  29. 'pageEnd',
  30. 'pageUtc',
  31. 'dataCategory',
  32. 'transform',
  33. 'sort',
  34. 'query',
  35. 'cursor',
  36. 'team',
  37. 'environment',
  38. ];
  39. type Props = Pick<RouteComponentProps<{}, {}>, 'router' | 'location'> & {
  40. currentEnvironment?: string;
  41. currentTeam?: TeamWithProjects;
  42. showEnvironment?: boolean;
  43. };
  44. function TeamStatsControls({
  45. location,
  46. router,
  47. currentTeam,
  48. currentEnvironment,
  49. showEnvironment,
  50. }: Props) {
  51. const {projects} = useProjects({
  52. slugs: currentTeam?.projects?.map(project => project.slug) ?? [],
  53. });
  54. const organization = useOrganization();
  55. const isSuperuser = isActiveSuperuser();
  56. const theme = useTheme();
  57. const query = location?.query ?? {};
  58. const localStorageKey = `teamInsightsSelectedTeamId:${organization.slug}`;
  59. function handleChangeTeam(teamId: string) {
  60. localStorage.setItem(localStorageKey, teamId);
  61. // TODO(workflow): Preserve environment if it exists for the new team
  62. setStateOnUrl({team: teamId, environment: undefined});
  63. }
  64. function handleEnvironmentChange({value}: {label: string; value: string}) {
  65. if (value === '') {
  66. setStateOnUrl({environment: undefined});
  67. } else {
  68. setStateOnUrl({environment: value});
  69. }
  70. }
  71. function handleUpdateDatetime(datetime: ChangeData): LocationDescriptorObject {
  72. const {start, end, relative, utc} = datetime;
  73. if (start && end) {
  74. const parser = utc ? moment.utc : moment;
  75. return setStateOnUrl({
  76. pageStatsPeriod: undefined,
  77. pageStart: parser(start).format(),
  78. pageEnd: parser(end).format(),
  79. pageUtc: utc ?? undefined,
  80. });
  81. }
  82. return setStateOnUrl({
  83. pageStatsPeriod: relative || undefined,
  84. pageStart: undefined,
  85. pageEnd: undefined,
  86. pageUtc: undefined,
  87. });
  88. }
  89. function setStateOnUrl(nextState: {
  90. environment?: string;
  91. pageEnd?: DateString;
  92. pageStart?: DateString;
  93. pageStatsPeriod?: string | null;
  94. pageUtc?: boolean | null;
  95. team?: string;
  96. }): LocationDescriptorObject {
  97. const nextQueryParams = pick(nextState, PAGE_QUERY_PARAMS);
  98. const nextLocation = {
  99. ...location,
  100. query: {
  101. ...query,
  102. ...nextQueryParams,
  103. },
  104. };
  105. router.push(nextLocation);
  106. return nextLocation;
  107. }
  108. const {period, start, end, utc} = dataDatetime(query);
  109. const environmentOptions = uniq(
  110. projects.map(project => project.environments).flat()
  111. ).map(env => ({label: env, value: env}));
  112. return (
  113. <ControlsWrapper showEnvironment={showEnvironment}>
  114. <StyledTeamSelector
  115. name="select-team"
  116. inFieldLabel={t('Team: ')}
  117. value={currentTeam?.slug}
  118. onChange={choice => handleChangeTeam(choice.actor.id)}
  119. teamFilter={isSuperuser ? undefined : filterTeam => filterTeam.isMember}
  120. styles={{
  121. singleValue(provided: any) {
  122. const custom = {
  123. display: 'flex',
  124. justifyContent: 'space-between',
  125. alignItems: 'center',
  126. fontSize: theme.fontSizeMedium,
  127. ':before': {
  128. ...provided[':before'],
  129. color: theme.textColor,
  130. marginRight: space(1.5),
  131. marginLeft: space(0.5),
  132. },
  133. };
  134. return {...provided, ...custom};
  135. },
  136. input: (provided: any, state: any) => ({
  137. ...provided,
  138. display: 'grid',
  139. gridTemplateColumns: 'max-content 1fr',
  140. alignItems: 'center',
  141. gridGap: space(1),
  142. ':before': {
  143. backgroundColor: state.theme.backgroundSecondary,
  144. height: 24,
  145. width: 38,
  146. borderRadius: 3,
  147. content: '""',
  148. display: 'block',
  149. },
  150. }),
  151. }}
  152. />
  153. {showEnvironment && (
  154. <SelectControl
  155. options={[
  156. {
  157. value: '',
  158. label: t('All'),
  159. },
  160. ...environmentOptions,
  161. ]}
  162. value={currentEnvironment ?? ''}
  163. onChange={handleEnvironmentChange}
  164. styles={{
  165. input: (provided: any) => ({
  166. ...provided,
  167. display: 'grid',
  168. gridTemplateColumns: 'max-content 1fr',
  169. alignItems: 'center',
  170. gridGap: space(1),
  171. ':before': {
  172. height: 24,
  173. width: 90,
  174. borderRadius: 3,
  175. content: '""',
  176. display: 'block',
  177. },
  178. }),
  179. control: (base: any) => ({
  180. ...base,
  181. boxShadow: 'none',
  182. }),
  183. singleValue: (base: any) => ({
  184. ...base,
  185. fontSize: theme.fontSizeMedium,
  186. display: 'flex',
  187. ':before': {
  188. ...base[':before'],
  189. color: theme.textColor,
  190. marginRight: space(1.5),
  191. },
  192. }),
  193. }}
  194. inFieldLabel={t('Environment:')}
  195. />
  196. )}
  197. <StyledTimeRangeSelector
  198. relative={period ?? ''}
  199. start={start ?? null}
  200. end={end ?? null}
  201. utc={utc ?? null}
  202. onChange={handleUpdateDatetime}
  203. showAbsolute={false}
  204. relativeOptions={relativeOptions}
  205. triggerLabel={period && relativeOptions[period]}
  206. triggerProps={{prefix: t('Date Range')}}
  207. />
  208. </ControlsWrapper>
  209. );
  210. }
  211. export default TeamStatsControls;
  212. const ControlsWrapper = styled('div')<{showEnvironment?: boolean}>`
  213. display: grid;
  214. align-items: center;
  215. gap: ${space(2)};
  216. margin-bottom: ${space(2)};
  217. @media (min-width: ${p => p.theme.breakpoints.small}) {
  218. grid-template-columns: 246px ${p => (p.showEnvironment ? '246px' : '')} 1fr;
  219. }
  220. `;
  221. const StyledTeamSelector = styled(TeamSelector)`
  222. & > div {
  223. box-shadow: ${p => p.theme.dropShadowMedium};
  224. }
  225. `;
  226. const StyledTimeRangeSelector = styled(TimeRangeSelector)`
  227. div {
  228. min-height: unset;
  229. }
  230. `;