projects.tsx 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410
  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,
  278. name,
  279. options = {},
  280. orgSlug,
  281. platform,
  282. team,
  283. }: {
  284. api: Client;
  285. name: string;
  286. options: {defaultRules?: boolean};
  287. orgSlug: string;
  288. platform: string;
  289. team: string;
  290. }) {
  291. return api.requestPromise(`/teams/${orgSlug}/${team}/projects/`, {
  292. method: 'POST',
  293. data: {name, platform, default_rules: options.defaultRules},
  294. });
  295. }
  296. /**
  297. * Deletes a project
  298. *
  299. * @param api API Client
  300. * @param orgSlug Organization Slug
  301. * @param projectSlug Project Slug
  302. */
  303. export function removeProject({
  304. api,
  305. orgSlug,
  306. projectSlug,
  307. origin,
  308. }: {
  309. api: Client;
  310. orgSlug: string;
  311. origin: 'onboarding' | 'settings' | 'getting_started';
  312. projectSlug: Project['slug'];
  313. }) {
  314. return api.requestPromise(`/projects/${orgSlug}/${projectSlug}/`, {
  315. method: 'DELETE',
  316. data: {origin},
  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,
  330. orgSlug,
  331. projectSlug,
  332. platform,
  333. }: {
  334. api: Client;
  335. orgSlug: string;
  336. platform: PlatformKey;
  337. projectSlug: string;
  338. }) {
  339. return api.requestPromise(`/projects/${orgSlug}/${projectSlug}/docs/${platform}/`);
  340. }
  341. /**
  342. * Load the counts of my projects and all projects for the current user
  343. *
  344. * @param api API Client
  345. * @param orgSlug Organization Slug
  346. */
  347. export function fetchProjectsCount(api: Client, orgSlug: string) {
  348. return api.requestPromise(`/organizations/${orgSlug}/projects-count/`);
  349. }
  350. /**
  351. * Check if there are any releases in the last 90 days.
  352. * Used for checking if project is using releases.
  353. *
  354. * @param api API Client
  355. * @param orgSlug Organization Slug
  356. * @param projectId Project Id
  357. */
  358. export async function fetchAnyReleaseExistence(
  359. api: Client,
  360. orgSlug: string,
  361. projectId: number | string
  362. ) {
  363. const data = await api.requestPromise(`/organizations/${orgSlug}/releases/stats/`, {
  364. method: 'GET',
  365. query: {
  366. statsPeriod: '90d',
  367. project: projectId,
  368. per_page: 1,
  369. },
  370. });
  371. return data.length > 0;
  372. }