controls.tsx 7.1 KB

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