projects.tsx 9.9 KB

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