projects.tsx 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407
  1. import {Query} from 'history';
  2. import chunk from 'lodash/chunk';
  3. import debounce from 'lodash/debounce';
  4. import {
  5. addErrorMessage,
  6. addLoadingMessage,
  7. addSuccessMessage,
  8. } from 'sentry/actionCreators/indicator';
  9. import ProjectActions from 'sentry/actions/projectActions';
  10. import {Client} from 'sentry/api';
  11. import {PlatformKey} from 'sentry/data/platformCategories';
  12. import {t, tct} from 'sentry/locale';
  13. import ProjectsStatsStore from 'sentry/stores/projectsStatsStore';
  14. import {Project, Team} from 'sentry/types';
  15. type UpdateParams = {
  16. orgId: string;
  17. projectId: string;
  18. data?: {[key: string]: any};
  19. query?: Query;
  20. };
  21. export function update(api: Client, params: UpdateParams) {
  22. ProjectActions.update(params.projectId, params.data);
  23. const endpoint = `/projects/${params.orgId}/${params.projectId}/`;
  24. return api
  25. .requestPromise(endpoint, {
  26. method: 'PUT',
  27. data: params.data,
  28. })
  29. .then(
  30. data => {
  31. ProjectActions.updateSuccess(data);
  32. return data;
  33. },
  34. err => {
  35. ProjectActions.updateError(err, params.projectId);
  36. throw err;
  37. }
  38. );
  39. }
  40. type StatsParams = Pick<UpdateParams, 'orgId' | 'data' | 'query'>;
  41. export function loadStats(api: Client, params: StatsParams) {
  42. ProjectActions.loadStats(params.orgId, params.data);
  43. const endpoint = `/organizations/${params.orgId}/stats/`;
  44. api.request(endpoint, {
  45. query: params.query,
  46. success: data => {
  47. ProjectActions.loadStatsSuccess(data);
  48. },
  49. error: data => {
  50. ProjectActions.loadStatsError(data);
  51. },
  52. });
  53. }
  54. // This is going to queue up a list of project ids we need to fetch stats for
  55. // Will be cleared when debounced function fires
  56. const _projectStatsToFetch: Set<string> = new Set();
  57. // Max projects to query at a time, otherwise if we fetch too many in the same request
  58. // it can timeout
  59. const MAX_PROJECTS_TO_FETCH = 10;
  60. const _queryForStats = (
  61. api: Client,
  62. projects: string[],
  63. orgId: string,
  64. additionalQuery: Query | undefined
  65. ) => {
  66. const idQueryParams = projects.map(project => `id:${project}`).join(' ');
  67. const endpoint = `/organizations/${orgId}/projects/`;
  68. const query: Query = {
  69. statsPeriod: '24h',
  70. query: idQueryParams,
  71. ...additionalQuery,
  72. };
  73. return api.requestPromise(endpoint, {
  74. query,
  75. });
  76. };
  77. export const _debouncedLoadStats = debounce(
  78. (api: Client, projectSet: Set<string>, params: UpdateParams) => {
  79. const storedProjects: {[key: string]: Project} = ProjectsStatsStore.getAll();
  80. const existingProjectStats = Object.values(storedProjects).map(({id}) => id);
  81. const projects = Array.from(projectSet).filter(
  82. project => !existingProjectStats.includes(project)
  83. );
  84. if (!projects.length) {
  85. _projectStatsToFetch.clear();
  86. return;
  87. }
  88. // Split projects into more manageable chunks to query, otherwise we can
  89. // potentially face server timeouts
  90. const queries = chunk(projects, MAX_PROJECTS_TO_FETCH).map(chunkedProjects =>
  91. _queryForStats(api, chunkedProjects, params.orgId, params.query)
  92. );
  93. Promise.all(queries)
  94. .then(results => {
  95. ProjectActions.loadStatsForProjectSuccess(
  96. results.reduce((acc, result) => acc.concat(result), [])
  97. );
  98. })
  99. .catch(() => {
  100. addErrorMessage(t('Unable to fetch all project stats'));
  101. });
  102. // Reset projects list
  103. _projectStatsToFetch.clear();
  104. },
  105. 50
  106. );
  107. export function loadStatsForProject(api: Client, project: string, params: UpdateParams) {
  108. // Queue up a list of projects that we need stats for
  109. // and call a debounced function to fetch stats for list of projects
  110. _projectStatsToFetch.add(project);
  111. _debouncedLoadStats(api, _projectStatsToFetch, params);
  112. }
  113. export function setActiveProject(project: Project | null) {
  114. ProjectActions.setActive(project);
  115. }
  116. export function removeProject(api: Client, orgId: string, project: Project) {
  117. const endpoint = `/projects/${orgId}/${project.slug}/`;
  118. ProjectActions.removeProject(project);
  119. return api
  120. .requestPromise(endpoint, {
  121. method: 'DELETE',
  122. })
  123. .then(
  124. () => {
  125. ProjectActions.removeProjectSuccess(project);
  126. addSuccessMessage(
  127. tct('[project] was successfully removed', {project: project.slug})
  128. );
  129. },
  130. err => {
  131. ProjectActions.removeProjectError(project);
  132. addErrorMessage(tct('Error removing [project]', {project: project.slug}));
  133. throw err;
  134. }
  135. );
  136. }
  137. export function transferProject(
  138. api: Client,
  139. orgId: string,
  140. project: Project,
  141. email: string
  142. ) {
  143. const endpoint = `/projects/${orgId}/${project.slug}/transfer/`;
  144. return api
  145. .requestPromise(endpoint, {
  146. method: 'POST',
  147. data: {
  148. email,
  149. },
  150. })
  151. .then(
  152. () => {
  153. addSuccessMessage(
  154. tct('A request was sent to move [project] to a different organization', {
  155. project: project.slug,
  156. })
  157. );
  158. },
  159. err => {
  160. let message = '';
  161. // Handle errors with known failures
  162. if (err.status >= 400 && err.status < 500 && err.responseJSON) {
  163. message = err.responseJSON?.detail;
  164. }
  165. if (message) {
  166. addErrorMessage(
  167. tct('Error transferring [project]. [message]', {
  168. project: project.slug,
  169. message,
  170. })
  171. );
  172. } else {
  173. addErrorMessage(
  174. tct('Error transferring [project]', {
  175. project: project.slug,
  176. })
  177. );
  178. }
  179. throw err;
  180. }
  181. );
  182. }
  183. /**
  184. * Associate a team with a project
  185. */
  186. /**
  187. * Adds a team to a project
  188. *
  189. * @param api API Client
  190. * @param orgSlug Organization Slug
  191. * @param projectSlug Project Slug
  192. * @param team Team data object
  193. */
  194. export function addTeamToProject(
  195. api: Client,
  196. orgSlug: string,
  197. projectSlug: string,
  198. team: Team
  199. ) {
  200. const endpoint = `/projects/${orgSlug}/${projectSlug}/teams/${team.slug}/`;
  201. addLoadingMessage();
  202. ProjectActions.addTeam(team);
  203. return api
  204. .requestPromise(endpoint, {
  205. method: 'POST',
  206. })
  207. .then(
  208. project => {
  209. addSuccessMessage(
  210. tct('[team] has been added to the [project] project', {
  211. team: `#${team.slug}`,
  212. project: projectSlug,
  213. })
  214. );
  215. ProjectActions.addTeamSuccess(team, projectSlug);
  216. ProjectActions.updateSuccess(project);
  217. },
  218. err => {
  219. addErrorMessage(
  220. tct('Unable to add [team] to the [project] project', {
  221. team: `#${team.slug}`,
  222. project: projectSlug,
  223. })
  224. );
  225. ProjectActions.addTeamError();
  226. throw err;
  227. }
  228. );
  229. }
  230. /**
  231. * Removes a team from a project
  232. *
  233. * @param api API Client
  234. * @param orgSlug Organization Slug
  235. * @param projectSlug Project Slug
  236. * @param teamSlug Team Slug
  237. */
  238. export function removeTeamFromProject(
  239. api: Client,
  240. orgSlug: string,
  241. projectSlug: string,
  242. teamSlug: string
  243. ) {
  244. const endpoint = `/projects/${orgSlug}/${projectSlug}/teams/${teamSlug}/`;
  245. addLoadingMessage();
  246. ProjectActions.removeTeam(teamSlug);
  247. return api
  248. .requestPromise(endpoint, {
  249. method: 'DELETE',
  250. })
  251. .then(
  252. project => {
  253. addSuccessMessage(
  254. tct('[team] has been removed from the [project] project', {
  255. team: `#${teamSlug}`,
  256. project: projectSlug,
  257. })
  258. );
  259. ProjectActions.removeTeamSuccess(teamSlug, projectSlug);
  260. ProjectActions.updateSuccess(project);
  261. },
  262. err => {
  263. addErrorMessage(
  264. tct('Unable to remove [team] from the [project] project', {
  265. team: `#${teamSlug}`,
  266. project: projectSlug,
  267. })
  268. );
  269. ProjectActions.removeTeamError(err);
  270. throw err;
  271. }
  272. );
  273. }
  274. /**
  275. * Change a project's slug
  276. *
  277. * @param prev Previous slug
  278. * @param next New slug
  279. */
  280. export function changeProjectSlug(prev: string, next: string) {
  281. ProjectActions.changeSlug(prev, next);
  282. }
  283. /**
  284. * Send a sample event
  285. *
  286. * @param api API Client
  287. * @param orgSlug Organization Slug
  288. * @param projectSlug Project Slug
  289. */
  290. export function sendSampleEvent(api: Client, orgSlug: string, projectSlug: string) {
  291. const endpoint = `/projects/${orgSlug}/${projectSlug}/create-sample/`;
  292. return api.requestPromise(endpoint, {
  293. method: 'POST',
  294. });
  295. }
  296. /**
  297. * Creates a project
  298. *
  299. * @param api API Client
  300. * @param orgSlug Organization Slug
  301. * @param team The team slug to assign the project to
  302. * @param name Name of the project
  303. * @param platform The platform key of the project
  304. * @param options Additional options such as creating default alert rules
  305. */
  306. export function createProject(
  307. api: Client,
  308. orgSlug: string,
  309. team: string,
  310. name: string,
  311. platform: string,
  312. options: {defaultRules?: boolean} = {}
  313. ) {
  314. return api.requestPromise(`/teams/${orgSlug}/${team}/projects/`, {
  315. method: 'POST',
  316. data: {name, platform, default_rules: options.defaultRules},
  317. });
  318. }
  319. /**
  320. * Load platform documentation specific to the project. The DSN and various
  321. * other project specific secrets will be included in the documentation.
  322. *
  323. * @param api API Client
  324. * @param orgSlug Organization Slug
  325. * @param projectSlug Project Slug
  326. * @param platform Project platform.
  327. */
  328. export function loadDocs(
  329. api: Client,
  330. orgSlug: string,
  331. projectSlug: string,
  332. platform: PlatformKey
  333. ) {
  334. return api.requestPromise(`/projects/${orgSlug}/${projectSlug}/docs/${platform}/`);
  335. }
  336. /**
  337. * Load the counts of my projects and all projects for the current user
  338. *
  339. * @param api API Client
  340. * @param orgSlug Organization Slug
  341. */
  342. export function fetchProjectsCount(api: Client, orgSlug: string) {
  343. return api.requestPromise(`/organizations/${orgSlug}/projects-count/`);
  344. }
  345. /**
  346. * Check if there are any releases in the last 90 days.
  347. * Used for checking if project is using releases.
  348. *
  349. * @param api API Client
  350. * @param orgSlug Organization Slug
  351. * @param projectId Project Id
  352. */
  353. export async function fetchAnyReleaseExistence(
  354. api: Client,
  355. orgSlug: string,
  356. projectId: number | string
  357. ) {
  358. const data = await api.requestPromise(`/organizations/${orgSlug}/releases/stats/`, {
  359. method: 'GET',
  360. query: {
  361. statsPeriod: '90d',
  362. project: projectId,
  363. per_page: 1,
  364. },
  365. });
  366. return data.length > 0;
  367. }