projects.tsx 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391
  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 transferProject(
  112. api: Client,
  113. orgId: string,
  114. project: Project,
  115. email: string
  116. ) {
  117. const endpoint = `/projects/${orgId}/${project.slug}/transfer/`;
  118. return api
  119. .requestPromise(endpoint, {
  120. method: 'POST',
  121. data: {
  122. email,
  123. },
  124. })
  125. .then(
  126. () => {
  127. addSuccessMessage(
  128. tct('A request was sent to move [project] to a different organization', {
  129. project: project.slug,
  130. })
  131. );
  132. },
  133. err => {
  134. let message = '';
  135. // Handle errors with known failures
  136. if (err.status >= 400 && err.status < 500 && err.responseJSON) {
  137. message = err.responseJSON?.detail;
  138. }
  139. if (message) {
  140. addErrorMessage(
  141. tct('Error transferring [project]. [message]', {
  142. project: project.slug,
  143. message,
  144. })
  145. );
  146. } else {
  147. addErrorMessage(
  148. tct('Error transferring [project]', {
  149. project: project.slug,
  150. })
  151. );
  152. }
  153. throw err;
  154. }
  155. );
  156. }
  157. /**
  158. * Associate a team with a project
  159. */
  160. /**
  161. * Adds a team to a project
  162. *
  163. * @param api API Client
  164. * @param orgSlug Organization Slug
  165. * @param projectSlug Project Slug
  166. * @param team Team data object
  167. */
  168. export function addTeamToProject(
  169. api: Client,
  170. orgSlug: string,
  171. projectSlug: string,
  172. team: Team
  173. ) {
  174. const endpoint = `/projects/${orgSlug}/${projectSlug}/teams/${team.slug}/`;
  175. addLoadingMessage();
  176. return api
  177. .requestPromise(endpoint, {
  178. method: 'POST',
  179. })
  180. .then(
  181. project => {
  182. addSuccessMessage(
  183. tct('[team] has been added to the [project] project', {
  184. team: `#${team.slug}`,
  185. project: projectSlug,
  186. })
  187. );
  188. ProjectsStore.onAddTeam(team, projectSlug);
  189. ProjectsStore.onUpdateSuccess(project);
  190. },
  191. err => {
  192. addErrorMessage(
  193. tct('Unable to add [team] to the [project] project', {
  194. team: `#${team.slug}`,
  195. project: projectSlug,
  196. })
  197. );
  198. throw err;
  199. }
  200. );
  201. }
  202. /**
  203. * Removes a team from a project
  204. *
  205. * @param api API Client
  206. * @param orgSlug Organization Slug
  207. * @param projectSlug Project Slug
  208. * @param teamSlug Team Slug
  209. */
  210. export function removeTeamFromProject(
  211. api: Client,
  212. orgSlug: string,
  213. projectSlug: string,
  214. teamSlug: string
  215. ) {
  216. const endpoint = `/projects/${orgSlug}/${projectSlug}/teams/${teamSlug}/`;
  217. addLoadingMessage();
  218. return api
  219. .requestPromise(endpoint, {
  220. method: 'DELETE',
  221. })
  222. .then(
  223. project => {
  224. addSuccessMessage(
  225. tct('[team] has been removed from the [project] project', {
  226. team: `#${teamSlug}`,
  227. project: projectSlug,
  228. })
  229. );
  230. ProjectsStore.onRemoveTeam(teamSlug, projectSlug);
  231. ProjectsStore.onUpdateSuccess(project);
  232. },
  233. err => {
  234. addErrorMessage(
  235. tct('Unable to remove [team] from the [project] project', {
  236. team: `#${teamSlug}`,
  237. project: projectSlug,
  238. })
  239. );
  240. throw err;
  241. }
  242. );
  243. }
  244. /**
  245. * Change a project's slug
  246. *
  247. * @param prev Previous slug
  248. * @param next New slug
  249. */
  250. export function changeProjectSlug(prev: string, next: string) {
  251. ProjectsStore.onChangeSlug(prev, next);
  252. }
  253. /**
  254. * Send a sample event
  255. *
  256. * @param api API Client
  257. * @param orgSlug Organization Slug
  258. * @param projectSlug Project Slug
  259. */
  260. export function sendSampleEvent(api: Client, orgSlug: string, projectSlug: string) {
  261. const endpoint = `/projects/${orgSlug}/${projectSlug}/create-sample/`;
  262. return api.requestPromise(endpoint, {
  263. method: 'POST',
  264. });
  265. }
  266. /**
  267. * Creates a project
  268. *
  269. * @param api API Client
  270. * @param orgSlug Organization Slug
  271. * @param team The team slug to assign the project to
  272. * @param name Name of the project
  273. * @param platform The platform key of the project
  274. * @param options Additional options such as creating default alert rules
  275. */
  276. export function createProject(
  277. api: Client,
  278. orgSlug: string,
  279. team: string,
  280. name: string,
  281. platform: string,
  282. options: {defaultRules?: boolean} = {}
  283. ) {
  284. return api.requestPromise(`/teams/${orgSlug}/${team}/projects/`, {
  285. method: 'POST',
  286. data: {name, platform, default_rules: options.defaultRules},
  287. });
  288. }
  289. /**
  290. * Deletes a project
  291. *
  292. * @param api API Client
  293. * @param orgSlug Organization Slug
  294. * @param projectSlug Project Slug
  295. */
  296. export function removeProject(
  297. api: Client,
  298. orgSlug: string,
  299. projectSlug: Project['slug']
  300. ) {
  301. return api.requestPromise(`/projects/${orgSlug}/${projectSlug}/`, {
  302. method: 'DELETE',
  303. });
  304. }
  305. /**
  306. * Load platform documentation specific to the project. The DSN and various
  307. * other project specific secrets will be included in the documentation.
  308. *
  309. * @param api API Client
  310. * @param orgSlug Organization Slug
  311. * @param projectSlug Project Slug
  312. * @param platform Project platform.
  313. */
  314. export function loadDocs(
  315. api: Client,
  316. orgSlug: string,
  317. projectSlug: string,
  318. platform: PlatformKey
  319. ) {
  320. return api.requestPromise(`/projects/${orgSlug}/${projectSlug}/docs/${platform}/`);
  321. }
  322. /**
  323. * Load the counts of my projects and all projects for the current user
  324. *
  325. * @param api API Client
  326. * @param orgSlug Organization Slug
  327. */
  328. export function fetchProjectsCount(api: Client, orgSlug: string) {
  329. return api.requestPromise(`/organizations/${orgSlug}/projects-count/`);
  330. }
  331. /**
  332. * Check if there are any releases in the last 90 days.
  333. * Used for checking if project is using releases.
  334. *
  335. * @param api API Client
  336. * @param orgSlug Organization Slug
  337. * @param projectId Project Id
  338. */
  339. export async function fetchAnyReleaseExistence(
  340. api: Client,
  341. orgSlug: string,
  342. projectId: number | string
  343. ) {
  344. const data = await api.requestPromise(`/organizations/${orgSlug}/releases/stats/`, {
  345. method: 'GET',
  346. query: {
  347. statsPeriod: '90d',
  348. project: projectId,
  349. per_page: 1,
  350. },
  351. });
  352. return data.length > 0;
  353. }