controls.tsx 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260
  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 moment from 'moment';
  7. import SelectControl from 'sentry/components/forms/controls/selectControl';
  8. import TeamSelector from 'sentry/components/teamSelector';
  9. import {ChangeData, TimeRangeSelector} from 'sentry/components/timeRangeSelector';
  10. import {t} from 'sentry/locale';
  11. import {space} from 'sentry/styles/space';
  12. import {DateString, TeamWithProjects} from 'sentry/types';
  13. import {uniq} from 'sentry/utils/array/uniq';
  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(projects.flatMap(project => project.environments)).map(
  110. env => ({label: env, value: env})
  111. );
  112. // org:admin is a unique scope that only org owners have
  113. const isOrgOwner = organization.access.includes('org:admin');
  114. return (
  115. <ControlsWrapper showEnvironment={showEnvironment}>
  116. <StyledTeamSelector
  117. name="select-team"
  118. inFieldLabel={t('Team: ')}
  119. value={currentTeam?.slug}
  120. onChange={choice => handleChangeTeam(choice.actor.id)}
  121. teamFilter={
  122. isSuperuser || isOrgOwner ? undefined : filterTeam => filterTeam.isMember
  123. }
  124. styles={{
  125. singleValue(provided: any) {
  126. const custom = {
  127. display: 'flex',
  128. justifyContent: 'space-between',
  129. alignItems: 'center',
  130. fontSize: theme.fontSizeMedium,
  131. ':before': {
  132. ...provided[':before'],
  133. color: theme.textColor,
  134. marginRight: space(1.5),
  135. marginLeft: space(0.5),
  136. },
  137. };
  138. return {...provided, ...custom};
  139. },
  140. input: (provided: any, state: any) => ({
  141. ...provided,
  142. display: 'grid',
  143. gridTemplateColumns: 'max-content 1fr',
  144. alignItems: 'center',
  145. gridGap: space(1),
  146. ':before': {
  147. backgroundColor: state.theme.backgroundSecondary,
  148. height: 24,
  149. width: 38,
  150. borderRadius: 3,
  151. content: '""',
  152. display: 'block',
  153. },
  154. }),
  155. }}
  156. />
  157. {showEnvironment && (
  158. <SelectControl
  159. options={[
  160. {
  161. value: '',
  162. label: t('All'),
  163. },
  164. ...environmentOptions,
  165. ]}
  166. value={currentEnvironment ?? ''}
  167. onChange={handleEnvironmentChange}
  168. styles={{
  169. input: (provided: any) => ({
  170. ...provided,
  171. display: 'grid',
  172. gridTemplateColumns: 'max-content 1fr',
  173. alignItems: 'center',
  174. gridGap: space(1),
  175. ':before': {
  176. height: 24,
  177. width: 90,
  178. borderRadius: 3,
  179. content: '""',
  180. display: 'block',
  181. },
  182. }),
  183. control: (base: any) => ({
  184. ...base,
  185. boxShadow: 'none',
  186. }),
  187. singleValue: (base: any) => ({
  188. ...base,
  189. fontSize: theme.fontSizeMedium,
  190. display: 'flex',
  191. ':before': {
  192. ...base[':before'],
  193. color: theme.textColor,
  194. marginRight: space(1.5),
  195. },
  196. }),
  197. }}
  198. inFieldLabel={t('Environment:')}
  199. />
  200. )}
  201. <StyledTimeRangeSelector
  202. relative={period ?? ''}
  203. start={start ?? null}
  204. end={end ?? null}
  205. utc={utc ?? null}
  206. onChange={handleUpdateDatetime}
  207. showAbsolute={false}
  208. relativeOptions={relativeOptions}
  209. triggerLabel={period && relativeOptions[period]}
  210. triggerProps={{prefix: t('Date Range')}}
  211. />
  212. </ControlsWrapper>
  213. );
  214. }
  215. export default TeamStatsControls;
  216. const ControlsWrapper = styled('div')<{showEnvironment?: boolean}>`
  217. display: grid;
  218. align-items: center;
  219. gap: ${space(2)};
  220. margin-bottom: ${space(2)};
  221. @media (min-width: ${p => p.theme.breakpoints.small}) {
  222. grid-template-columns: 246px ${p => (p.showEnvironment ? '246px' : '')} 1fr;
  223. }
  224. `;
  225. const StyledTeamSelector = styled(TeamSelector)`
  226. & > div {
  227. box-shadow: ${p => p.theme.dropShadowMedium};
  228. }
  229. `;
  230. const StyledTimeRangeSelector = styled(TimeRangeSelector)`
  231. div {
  232. min-height: unset;
  233. }
  234. `;