group.tsx 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362
  1. import * as Sentry from '@sentry/react';
  2. import type {RequestCallbacks, RequestOptions} from 'sentry/api';
  3. import {Client} from 'sentry/api';
  4. import GroupStore from 'sentry/stores/groupStore';
  5. import type {Actor} from 'sentry/types/core';
  6. import type {Group, Tag as GroupTag, TagValue} from 'sentry/types/group';
  7. import {buildTeamId, buildUserId} from 'sentry/utils';
  8. import {uniqueId} from 'sentry/utils/guid';
  9. import type {ApiQueryKey, UseApiQueryOptions} from 'sentry/utils/queryClient';
  10. import {useApiQuery} from 'sentry/utils/queryClient';
  11. type AssignedBy = 'suggested_assignee' | 'assignee_selector';
  12. export function clearAssignment(
  13. groupId: string,
  14. orgSlug: string,
  15. assignedBy: AssignedBy
  16. ): Promise<Group> {
  17. const api = new Client();
  18. const endpoint = `/organizations/${orgSlug}/issues/${groupId}/`;
  19. const id = uniqueId();
  20. GroupStore.onAssignTo(id, groupId, {
  21. email: '',
  22. });
  23. const request = api.requestPromise(endpoint, {
  24. method: 'PUT',
  25. // Sending an empty value to assignedTo is the same as "clear"
  26. data: {
  27. assignedTo: '',
  28. assignedBy,
  29. },
  30. });
  31. request
  32. .then(data => {
  33. GroupStore.onAssignToSuccess(id, groupId, data);
  34. return data;
  35. })
  36. .catch(data => {
  37. GroupStore.onAssignToError(id, groupId, data);
  38. throw data;
  39. });
  40. return request;
  41. }
  42. type AssignToActorParams = {
  43. actor: Pick<Actor, 'id' | 'type'>;
  44. assignedBy: AssignedBy;
  45. /**
  46. * Issue id
  47. */
  48. id: string;
  49. orgSlug: string;
  50. };
  51. export function assignToActor({
  52. id,
  53. actor,
  54. assignedBy,
  55. orgSlug,
  56. }: AssignToActorParams): Promise<Group> {
  57. const api = new Client();
  58. const endpoint = `/organizations/${orgSlug}/issues/${id}/`;
  59. const guid = uniqueId();
  60. let actorId = '';
  61. GroupStore.onAssignTo(guid, id, {email: ''});
  62. switch (actor.type) {
  63. case 'user':
  64. actorId = buildUserId(actor.id);
  65. break;
  66. case 'team':
  67. actorId = buildTeamId(actor.id);
  68. break;
  69. default:
  70. Sentry.withScope(scope => {
  71. scope.setExtra('actor', actor);
  72. Sentry.captureException('Unknown assignee type');
  73. });
  74. }
  75. return api
  76. .requestPromise(endpoint, {
  77. method: 'PUT',
  78. data: {assignedTo: actorId, assignedBy},
  79. })
  80. .then(data => {
  81. GroupStore.onAssignToSuccess(guid, id, data);
  82. return data;
  83. })
  84. .catch(data => {
  85. GroupStore.onAssignToSuccess(guid, id, data);
  86. throw data;
  87. });
  88. }
  89. type ParamsType = {
  90. environment?: string | string[] | null;
  91. itemIds?: string[];
  92. project?: number[] | string[] | null;
  93. query?: string;
  94. };
  95. type UpdateParams = ParamsType & {
  96. orgId: string;
  97. projectId?: string;
  98. };
  99. type QueryArgs =
  100. | {
  101. query: string;
  102. environment?: string | string[];
  103. project?: Array<number | string>;
  104. }
  105. | {
  106. id: number[] | string[];
  107. environment?: string | string[];
  108. project?: Array<number | string>;
  109. }
  110. | {
  111. environment?: string | string[];
  112. project?: Array<number | string>;
  113. };
  114. /**
  115. * Converts input parameters to API-compatible query arguments
  116. */
  117. export function paramsToQueryArgs(params: ParamsType): QueryArgs {
  118. const p: QueryArgs = params.itemIds
  119. ? {id: params.itemIds} // items matching array of itemids
  120. : params.query
  121. ? {query: params.query} // items matching search query
  122. : {}; // all items
  123. // only include environment if it is not null/undefined
  124. if (params.query && params.environment !== null && params.environment !== undefined) {
  125. p.environment = params.environment;
  126. }
  127. // only include projects if it is not null/undefined/an empty array
  128. if (params.project?.length) {
  129. p.project = params.project;
  130. }
  131. // only include date filters if they are not null/undefined
  132. if (params.query) {
  133. ['start', 'end', 'period', 'utc'].forEach(prop => {
  134. if (
  135. params[prop as keyof typeof params] !== null &&
  136. params[prop as keyof typeof params] !== undefined
  137. ) {
  138. (p as any)[prop === 'period' ? 'statsPeriod' : prop] =
  139. params[prop as keyof typeof params];
  140. }
  141. });
  142. }
  143. return p;
  144. }
  145. function getUpdateUrl({projectId, orgId}: UpdateParams) {
  146. return projectId
  147. ? `/projects/${orgId}/${projectId}/issues/`
  148. : `/organizations/${orgId}/issues/`;
  149. }
  150. function chainUtil<Args extends any[]>(
  151. ...funcs: Array<((...args: Args) => any) | undefined>
  152. ) {
  153. const filteredFuncs = funcs.filter(
  154. (f): f is (...args: Args) => any => typeof f === 'function'
  155. );
  156. return (...args: Args): void => {
  157. filteredFuncs.forEach(func => {
  158. func.apply(funcs, args);
  159. });
  160. };
  161. }
  162. function wrapRequest(
  163. api: Client,
  164. path: string,
  165. options: RequestOptions,
  166. extraParams: RequestCallbacks = {}
  167. ) {
  168. options.success = chainUtil(options.success, extraParams.success);
  169. options.error = chainUtil(options.error, extraParams.error);
  170. options.complete = chainUtil(options.complete, extraParams.complete);
  171. return api.request(path, options);
  172. }
  173. type BulkDeleteParams = UpdateParams;
  174. export function bulkDelete(
  175. api: Client,
  176. params: BulkDeleteParams,
  177. options: RequestCallbacks
  178. ) {
  179. const {itemIds} = params;
  180. const path = getUpdateUrl(params);
  181. const query: QueryArgs = paramsToQueryArgs(params);
  182. const id = uniqueId();
  183. GroupStore.onDelete(id, itemIds);
  184. return wrapRequest(
  185. api,
  186. path,
  187. {
  188. query,
  189. method: 'DELETE',
  190. success: response => {
  191. GroupStore.onDeleteSuccess(id, itemIds, response);
  192. },
  193. error: error => {
  194. GroupStore.onDeleteError(id, itemIds, error);
  195. },
  196. },
  197. options
  198. );
  199. }
  200. type BulkUpdateParams = UpdateParams & {
  201. data?: any;
  202. failSilently?: boolean;
  203. };
  204. export function bulkUpdate(
  205. api: Client,
  206. params: BulkUpdateParams,
  207. options: RequestCallbacks
  208. ) {
  209. const {itemIds, failSilently, data} = params;
  210. const path = getUpdateUrl(params);
  211. const query: QueryArgs = paramsToQueryArgs(params);
  212. const id = uniqueId();
  213. GroupStore.onUpdate(id, itemIds, data);
  214. return wrapRequest(
  215. api,
  216. path,
  217. {
  218. query,
  219. method: 'PUT',
  220. data,
  221. success: response => {
  222. GroupStore.onUpdateSuccess(id, itemIds, response);
  223. },
  224. error: () => {
  225. GroupStore.onUpdateError(id, itemIds, !!failSilently);
  226. },
  227. },
  228. options
  229. );
  230. }
  231. type MergeGroupsParams = UpdateParams;
  232. export function mergeGroups(
  233. api: Client,
  234. params: MergeGroupsParams,
  235. options: RequestCallbacks
  236. ) {
  237. const {itemIds} = params;
  238. const path = getUpdateUrl(params);
  239. const query: QueryArgs = paramsToQueryArgs(params);
  240. const id = uniqueId();
  241. GroupStore.onMerge(id, itemIds);
  242. return wrapRequest(
  243. api,
  244. path,
  245. {
  246. query,
  247. method: 'PUT',
  248. data: {merge: 1},
  249. success: response => {
  250. GroupStore.onMergeSuccess(id, itemIds, response);
  251. },
  252. error: error => {
  253. GroupStore.onMergeError(id, itemIds, error);
  254. },
  255. },
  256. options
  257. );
  258. }
  259. type FetchIssueTagValuesParameters = {
  260. groupId: string;
  261. orgSlug: string;
  262. tagKey: string;
  263. cursor?: string;
  264. environment?: string[];
  265. sort?: string | string[];
  266. };
  267. export const makeFetchIssueTagValuesQueryKey = ({
  268. orgSlug,
  269. groupId,
  270. tagKey,
  271. environment,
  272. sort,
  273. cursor,
  274. }: FetchIssueTagValuesParameters): ApiQueryKey => [
  275. `/organizations/${orgSlug}/issues/${groupId}/tags/${tagKey}/values/`,
  276. {query: {environment, sort, cursor}},
  277. ];
  278. export function useFetchIssueTagValues(
  279. parameters: FetchIssueTagValuesParameters,
  280. options: Partial<UseApiQueryOptions<TagValue[]>> = {}
  281. ) {
  282. return useApiQuery<TagValue[]>(makeFetchIssueTagValuesQueryKey(parameters), {
  283. staleTime: 0,
  284. retry: false,
  285. ...options,
  286. });
  287. }
  288. type FetchIssueTagParameters = {
  289. groupId: string;
  290. orgSlug: string;
  291. tagKey: string;
  292. };
  293. export const makeFetchIssueTagQueryKey = ({
  294. orgSlug,
  295. groupId,
  296. tagKey,
  297. environment,
  298. sort,
  299. }: FetchIssueTagValuesParameters): ApiQueryKey => [
  300. `/organizations/${orgSlug}/issues/${groupId}/tags/${tagKey}/`,
  301. {query: {environment, sort}},
  302. ];
  303. export function useFetchIssueTag(
  304. parameters: FetchIssueTagParameters,
  305. options: Partial<UseApiQueryOptions<GroupTag>> = {}
  306. ) {
  307. return useApiQuery<GroupTag>(makeFetchIssueTagQueryKey(parameters), {
  308. staleTime: 0,
  309. retry: false,
  310. ...options,
  311. });
  312. }