projects.tsx 9.8 KB

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