sentryAppPublishRequestModal.tsx 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
  1. import {Component, Fragment} from 'react';
  2. import styled from '@emotion/styled';
  3. import intersection from 'lodash/intersection';
  4. import {addErrorMessage, addSuccessMessage} from 'sentry/actionCreators/indicator';
  5. import {ModalRenderProps} from 'sentry/actionCreators/modal';
  6. import Form from 'sentry/components/forms/form';
  7. import JsonForm from 'sentry/components/forms/jsonForm';
  8. import FormModel from 'sentry/components/forms/model';
  9. import {PermissionChoice, SENTRY_APP_PERMISSIONS} from 'sentry/constants';
  10. import {t, tct} from 'sentry/locale';
  11. import space from 'sentry/styles/space';
  12. import {Scope, SentryApp} from 'sentry/types';
  13. /**
  14. * Given an array of scopes, return the choices the user has picked for each option
  15. * @param scopes {Array}
  16. */
  17. const getPermissionSelectionsFromScopes = (scopes: Scope[]) => {
  18. const permissions: string[] = [];
  19. for (const permObj of SENTRY_APP_PERMISSIONS) {
  20. let highestChoice: PermissionChoice | undefined;
  21. for (const perm in permObj.choices) {
  22. const choice = permObj.choices[perm];
  23. const scopesIntersection = intersection(choice.scopes, scopes);
  24. if (
  25. scopesIntersection.length > 0 &&
  26. scopesIntersection.length === choice.scopes.length
  27. ) {
  28. if (!highestChoice || scopesIntersection.length > highestChoice.scopes.length) {
  29. highestChoice = choice;
  30. }
  31. }
  32. }
  33. if (highestChoice) {
  34. // we can remove the read part of "Read & Write"
  35. const label = highestChoice.label.replace('Read & Write', 'Write');
  36. permissions.push(`${permObj.resource} ${label}`);
  37. }
  38. }
  39. return permissions;
  40. };
  41. class PublishRequestFormModel extends FormModel {
  42. getTransformedData() {
  43. const data = this.getData();
  44. // map object to list of questions
  45. const questionnaire = Array.from(this.fieldDescriptor.values()).map(field =>
  46. // we read the meta for the question that has a react node for the label
  47. ({
  48. question: field.meta || field.label,
  49. answer: data[field.name],
  50. })
  51. );
  52. return {questionnaire};
  53. }
  54. }
  55. type Props = ModalRenderProps & {
  56. app: SentryApp;
  57. };
  58. export default class SentryAppPublishRequestModal extends Component<Props> {
  59. form = new PublishRequestFormModel();
  60. get formFields() {
  61. const {app} = this.props;
  62. const permissions = getPermissionSelectionsFromScopes(app.scopes);
  63. const permissionQuestionBaseText =
  64. 'Please justify why you are requesting each of the following permissions: ';
  65. const permissionQuestionPlainText = `${permissionQuestionBaseText}${permissions.join(
  66. ', '
  67. )}.`;
  68. const permissionLabel = (
  69. <Fragment>
  70. <PermissionLabel>{permissionQuestionBaseText}</PermissionLabel>
  71. {permissions.map((permission, i) => (
  72. <Fragment key={permission}>
  73. {i > 0 && ', '} <Permission>{permission}</Permission>
  74. </Fragment>
  75. ))}
  76. .
  77. </Fragment>
  78. );
  79. // No translations since we need to be able to read this email :)
  80. const baseFields: React.ComponentProps<typeof JsonForm>['fields'] = [
  81. {
  82. type: 'textarea',
  83. required: true,
  84. label: 'What does your integration do? Please be as detailed as possible.',
  85. autosize: true,
  86. rows: 1,
  87. inline: false,
  88. name: 'question0',
  89. },
  90. {
  91. type: 'textarea',
  92. required: true,
  93. label: 'What value does it offer customers?',
  94. autosize: true,
  95. rows: 1,
  96. inline: false,
  97. name: 'question1',
  98. },
  99. {
  100. type: 'textarea',
  101. required: true,
  102. label: 'Do you operate the web service your integration communicates with?',
  103. autosize: true,
  104. rows: 1,
  105. inline: false,
  106. name: 'question2',
  107. },
  108. ];
  109. // Only add the permissions question if there are perms to add
  110. if (permissions.length > 0) {
  111. baseFields.push({
  112. type: 'textarea',
  113. required: true,
  114. label: permissionLabel,
  115. autosize: true,
  116. rows: 1,
  117. inline: false,
  118. meta: permissionQuestionPlainText,
  119. name: 'question3',
  120. });
  121. }
  122. return baseFields;
  123. }
  124. handleSubmitSuccess = () => {
  125. addSuccessMessage(t('Request to publish %s successful.', this.props.app.slug));
  126. this.props.closeModal();
  127. };
  128. handleSubmitError = err => {
  129. addErrorMessage(
  130. tct('Request to publish [app] fails. [detail]', {
  131. app: this.props.app.slug,
  132. detail: err?.responseJSON?.detail,
  133. })
  134. );
  135. };
  136. render() {
  137. const {app, Header, Body} = this.props;
  138. const endpoint = `/sentry-apps/${app.slug}/publish-request/`;
  139. const forms = [
  140. {
  141. title: t('Questions to answer'),
  142. fields: this.formFields,
  143. },
  144. ];
  145. return (
  146. <Fragment>
  147. <Header>{t('Publish Request Questionnaire')}</Header>
  148. <Body>
  149. <Explanation>
  150. {t(
  151. `Please fill out this questionnaire in order to get your integration evaluated for publication.
  152. Once your integration has been approved, users outside of your organization will be able to install it.`
  153. )}
  154. </Explanation>
  155. <Form
  156. allowUndo
  157. apiMethod="POST"
  158. apiEndpoint={endpoint}
  159. onSubmitSuccess={this.handleSubmitSuccess}
  160. onSubmitError={this.handleSubmitError}
  161. model={this.form}
  162. submitLabel={t('Request Publication')}
  163. onCancel={() => this.props.closeModal()}
  164. >
  165. <JsonForm forms={forms} />
  166. </Form>
  167. </Body>
  168. </Fragment>
  169. );
  170. }
  171. }
  172. const Explanation = styled('div')`
  173. margin: ${space(1.5)} 0px;
  174. font-size: 18px;
  175. `;
  176. const PermissionLabel = styled('span')`
  177. line-height: 24px;
  178. `;
  179. const Permission = styled('code')`
  180. line-height: 24px;
  181. `;