group.tsx 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440
  1. import * as Sentry from '@sentry/react';
  2. import isNil from 'lodash/isNil';
  3. import {Client, RequestCallbacks, RequestOptions} from 'sentry/api';
  4. import GroupStore from 'sentry/stores/groupStore';
  5. import {Actor, Group, Member, Note, User} from 'sentry/types';
  6. import {buildTeamId, buildUserId, defined} from 'sentry/utils';
  7. import {uniqueId} from 'sentry/utils/guid';
  8. import {ApiQueryKey, useApiQuery, UseApiQueryOptions} from 'sentry/utils/queryClient';
  9. type AssignedBy = 'suggested_assignee' | 'assignee_selector';
  10. type AssignToUserParams = {
  11. assignedBy: AssignedBy;
  12. /**
  13. * Issue id
  14. */
  15. id: string;
  16. orgSlug: string;
  17. user: User | Actor;
  18. member?: Member;
  19. };
  20. export function assignToUser(params: AssignToUserParams) {
  21. const api = new Client();
  22. const endpoint = `/organizations/${params.orgSlug}/issues/${params.id}/`;
  23. const id = uniqueId();
  24. GroupStore.onAssignTo(id, params.id, {
  25. email: (params.member && params.member.email) || '',
  26. });
  27. const request = api.requestPromise(endpoint, {
  28. method: 'PUT',
  29. // Sending an empty value to assignedTo is the same as "clear",
  30. // so if no member exists, that implies that we want to clear the
  31. // current assignee.
  32. data: {
  33. assignedTo: params.user ? buildUserId(params.user.id) : '',
  34. assignedBy: params.assignedBy,
  35. },
  36. });
  37. request
  38. .then(data => {
  39. GroupStore.onAssignToSuccess(id, params.id, data);
  40. })
  41. .catch(data => {
  42. GroupStore.onAssignToError(id, params.id, data);
  43. });
  44. return request;
  45. }
  46. export function clearAssignment(
  47. groupId: string,
  48. orgSlug: string,
  49. assignedBy: AssignedBy
  50. ) {
  51. const api = new Client();
  52. const endpoint = `/organizations/${orgSlug}/issues/${groupId}/`;
  53. const id = uniqueId();
  54. GroupStore.onAssignTo(id, groupId, {
  55. email: '',
  56. });
  57. const request = api.requestPromise(endpoint, {
  58. method: 'PUT',
  59. // Sending an empty value to assignedTo is the same as "clear"
  60. data: {
  61. assignedTo: '',
  62. assignedBy,
  63. },
  64. });
  65. request
  66. .then(data => {
  67. GroupStore.onAssignToSuccess(id, groupId, data);
  68. })
  69. .catch(data => {
  70. GroupStore.onAssignToError(id, groupId, data);
  71. });
  72. return request;
  73. }
  74. type AssignToActorParams = {
  75. actor: Pick<Actor, 'id' | 'type'>;
  76. assignedBy: AssignedBy;
  77. /**
  78. * Issue id
  79. */
  80. id: string;
  81. orgSlug: string;
  82. };
  83. export function assignToActor({id, actor, assignedBy, orgSlug}: AssignToActorParams) {
  84. const api = new Client();
  85. const endpoint = `/organizations/${orgSlug}/issues/${id}/`;
  86. const guid = uniqueId();
  87. let actorId = '';
  88. GroupStore.onAssignTo(guid, id, {email: ''});
  89. switch (actor.type) {
  90. case 'user':
  91. actorId = buildUserId(actor.id);
  92. break;
  93. case 'team':
  94. actorId = buildTeamId(actor.id);
  95. break;
  96. default:
  97. Sentry.withScope(scope => {
  98. scope.setExtra('actor', actor);
  99. Sentry.captureException('Unknown assignee type');
  100. });
  101. }
  102. return api
  103. .requestPromise(endpoint, {
  104. method: 'PUT',
  105. data: {assignedTo: actorId, assignedBy},
  106. })
  107. .then(data => {
  108. GroupStore.onAssignToSuccess(guid, id, data);
  109. })
  110. .catch(data => {
  111. GroupStore.onAssignToSuccess(guid, id, data);
  112. });
  113. }
  114. export function deleteNote(
  115. api: Client,
  116. orgSlug: string,
  117. group: Group,
  118. id: string,
  119. _oldText: string
  120. ) {
  121. const restore = group.activity.find(activity => activity.id === id);
  122. const index = GroupStore.removeActivity(group.id, id);
  123. if (index === -1 || restore === undefined) {
  124. // I dunno, the id wasn't found in the GroupStore
  125. return Promise.reject(new Error('Group was not found in store'));
  126. }
  127. const promise = api.requestPromise(
  128. `/organizations/${orgSlug}/issues/${group.id}/comments/${id}/`,
  129. {
  130. method: 'DELETE',
  131. }
  132. );
  133. promise.catch(() => GroupStore.addActivity(group.id, restore, index));
  134. return promise;
  135. }
  136. export function createNote(api: Client, orgSlug: string, group: Group, note: Note) {
  137. const promise = api.requestPromise(
  138. `/organizations/${orgSlug}/issues/${group.id}/comments/`,
  139. {
  140. method: 'POST',
  141. data: note,
  142. }
  143. );
  144. promise.then(data => GroupStore.addActivity(group.id, data));
  145. return promise;
  146. }
  147. export function updateNote(
  148. api: Client,
  149. orgSlug: string,
  150. group: Group,
  151. note: Note,
  152. id: string,
  153. oldText: string
  154. ) {
  155. GroupStore.updateActivity(group.id, id, {data: {text: note.text}});
  156. const promise = api.requestPromise(
  157. `/organizations/${orgSlug}/issues/${group.id}/comments/${id}/`,
  158. {
  159. method: 'PUT',
  160. data: note,
  161. }
  162. );
  163. promise.catch(() => GroupStore.updateActivity(group.id, id, {data: {text: oldText}}));
  164. return promise;
  165. }
  166. type ParamsType = {
  167. environment?: string | string[] | null;
  168. itemIds?: string[];
  169. project?: number[] | null;
  170. query?: string;
  171. };
  172. type UpdateParams = ParamsType & {
  173. orgId: string;
  174. projectId?: string;
  175. };
  176. type QueryArgs =
  177. | {
  178. query: string;
  179. environment?: string | Array<string>;
  180. project?: Array<number>;
  181. }
  182. | {
  183. id: Array<number> | Array<string>;
  184. environment?: string | Array<string>;
  185. project?: Array<number>;
  186. }
  187. | {
  188. environment?: string | Array<string>;
  189. project?: Array<number>;
  190. };
  191. /**
  192. * Converts input parameters to API-compatible query arguments
  193. */
  194. export function paramsToQueryArgs(params: ParamsType): QueryArgs {
  195. const p: QueryArgs = params.itemIds
  196. ? {id: params.itemIds} // items matching array of itemids
  197. : params.query
  198. ? {query: params.query} // items matching search query
  199. : {}; // all items
  200. // only include environment if it is not null/undefined
  201. if (params.query && !isNil(params.environment)) {
  202. p.environment = params.environment;
  203. }
  204. // only include projects if it is not null/undefined/an empty array
  205. if (params.project?.length) {
  206. p.project = params.project;
  207. }
  208. // only include date filters if they are not null/undefined
  209. if (params.query) {
  210. ['start', 'end', 'period', 'utc'].forEach(prop => {
  211. if (!isNil(params[prop])) {
  212. p[prop === 'period' ? 'statsPeriod' : prop] = params[prop];
  213. }
  214. });
  215. }
  216. return p;
  217. }
  218. function getUpdateUrl({projectId, orgId}: UpdateParams) {
  219. return projectId
  220. ? `/projects/${orgId}/${projectId}/issues/`
  221. : `/organizations/${orgId}/issues/`;
  222. }
  223. function chainUtil<Args extends any[]>(
  224. ...funcs: Array<((...args: Args) => any) | undefined>
  225. ) {
  226. const filteredFuncs = funcs.filter(
  227. (f): f is (...args: Args) => any => typeof f === 'function'
  228. );
  229. return (...args: Args): void => {
  230. filteredFuncs.forEach(func => {
  231. func.apply(funcs, args);
  232. });
  233. };
  234. }
  235. function wrapRequest(
  236. api: Client,
  237. path: string,
  238. options: RequestOptions,
  239. extraParams: RequestCallbacks = {}
  240. ) {
  241. options.success = chainUtil(options.success, extraParams.success);
  242. options.error = chainUtil(options.error, extraParams.error);
  243. options.complete = chainUtil(options.complete, extraParams.complete);
  244. return api.request(path, options);
  245. }
  246. type BulkDeleteParams = UpdateParams;
  247. export function bulkDelete(
  248. api: Client,
  249. params: BulkDeleteParams,
  250. options: RequestCallbacks
  251. ) {
  252. const {itemIds} = params;
  253. const path = getUpdateUrl(params);
  254. const query: QueryArgs = paramsToQueryArgs(params);
  255. const id = uniqueId();
  256. GroupStore.onDelete(id, itemIds);
  257. return wrapRequest(
  258. api,
  259. path,
  260. {
  261. query,
  262. method: 'DELETE',
  263. success: response => {
  264. GroupStore.onDeleteSuccess(id, itemIds, response);
  265. },
  266. error: error => {
  267. GroupStore.onDeleteError(id, itemIds, error);
  268. },
  269. },
  270. options
  271. );
  272. }
  273. type BulkUpdateParams = UpdateParams & {
  274. data?: any;
  275. failSilently?: boolean;
  276. };
  277. export function bulkUpdate(
  278. api: Client,
  279. params: BulkUpdateParams,
  280. options: RequestCallbacks
  281. ) {
  282. const {itemIds, failSilently, data} = params;
  283. const path = getUpdateUrl(params);
  284. const query: QueryArgs = paramsToQueryArgs(params);
  285. const id = uniqueId();
  286. GroupStore.onUpdate(id, itemIds, data);
  287. return wrapRequest(
  288. api,
  289. path,
  290. {
  291. query,
  292. method: 'PUT',
  293. data,
  294. success: response => {
  295. GroupStore.onUpdateSuccess(id, itemIds, response);
  296. },
  297. error: () => {
  298. GroupStore.onUpdateError(id, itemIds, !!failSilently);
  299. },
  300. },
  301. options
  302. );
  303. }
  304. type MergeGroupsParams = UpdateParams;
  305. export function mergeGroups(
  306. api: Client,
  307. params: MergeGroupsParams,
  308. options: RequestCallbacks
  309. ) {
  310. const {itemIds} = params;
  311. const path = getUpdateUrl(params);
  312. const query: QueryArgs = paramsToQueryArgs(params);
  313. const id = uniqueId();
  314. GroupStore.onMerge(id, itemIds);
  315. return wrapRequest(
  316. api,
  317. path,
  318. {
  319. query,
  320. method: 'PUT',
  321. data: {merge: 1},
  322. success: response => {
  323. GroupStore.onMergeSuccess(id, itemIds, response);
  324. },
  325. error: error => {
  326. GroupStore.onMergeError(id, itemIds, error);
  327. },
  328. },
  329. options
  330. );
  331. }
  332. export type GroupTagResponseItem = {
  333. key: string;
  334. name: string;
  335. topValues: Array<{
  336. count: number;
  337. firstSeen: string;
  338. lastSeen: string;
  339. name: string;
  340. value: string;
  341. readable?: boolean;
  342. }>;
  343. totalValues: number;
  344. };
  345. export type GroupTagsResponse = GroupTagResponseItem[];
  346. type FetchIssueTagsParameters = {
  347. environment: string[];
  348. limit: number;
  349. orgSlug: string;
  350. readable: boolean;
  351. groupId?: string;
  352. };
  353. export const makeFetchIssueTagsQueryKey = ({
  354. groupId,
  355. orgSlug,
  356. environment,
  357. readable,
  358. limit,
  359. }: FetchIssueTagsParameters): ApiQueryKey => [
  360. `/organizations/${orgSlug}/issues/${groupId}/tags/`,
  361. {query: {environment, readable, limit}},
  362. ];
  363. export const useFetchIssueTags = (
  364. parameters: FetchIssueTagsParameters,
  365. {enabled = true, ...options}: Partial<UseApiQueryOptions<GroupTagsResponse>> = {}
  366. ) => {
  367. return useApiQuery<GroupTagsResponse>(makeFetchIssueTagsQueryKey(parameters), {
  368. staleTime: 30000,
  369. enabled: defined(parameters.groupId) && enabled,
  370. ...options,
  371. });
  372. };