group.tsx 9.4 KB

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