feedbackModal.tsx 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
  1. import {Fragment, useMemo, useState} from 'react';
  2. import {css} from '@emotion/react';
  3. import {
  4. BrowserClient,
  5. defaultIntegrations,
  6. defaultStackParser,
  7. makeFetchTransport,
  8. } from '@sentry/react';
  9. import {addSuccessMessage} from 'sentry/actionCreators/indicator';
  10. import {ModalRenderProps} from 'sentry/actionCreators/modal';
  11. import Button from 'sentry/components/button';
  12. import Textarea from 'sentry/components/forms/controls/textarea';
  13. import SelectField from 'sentry/components/forms/selectField';
  14. import {t, tct} from 'sentry/locale';
  15. import ConfigStore from 'sentry/stores/configStore';
  16. import OrganizationStore from 'sentry/stores/organizationStore';
  17. import {useLegacyStore} from 'sentry/stores/useLegacyStore';
  18. import {defined} from 'sentry/utils';
  19. import {useLocation} from 'sentry/utils/useLocation';
  20. import useProjects from 'sentry/utils/useProjects';
  21. import ButtonBar from '../buttonBar';
  22. import Field from '../forms/field';
  23. import ExternalLink from '../links/externalLink';
  24. const feedbackClient = new BrowserClient({
  25. // feedback project under Sentry organization
  26. dsn: 'https://3c5ef4e344a04a0694d187a1272e96de@o1.ingest.sentry.io/6356259',
  27. transport: makeFetchTransport,
  28. stackParser: defaultStackParser,
  29. integrations: defaultIntegrations,
  30. });
  31. const defaultFeedbackTypes = [
  32. t("I don't like this feature"),
  33. t('I like this feature'),
  34. t('Other reason'),
  35. ];
  36. export interface FeedBackModalProps {
  37. featureName: string;
  38. feedbackTypes?: string[];
  39. }
  40. interface Props extends FeedBackModalProps, ModalRenderProps {}
  41. type State = {additionalInfo?: string; subject?: number};
  42. export function FeedbackModal({
  43. Header,
  44. Body,
  45. Footer,
  46. closeModal,
  47. feedbackTypes = defaultFeedbackTypes,
  48. featureName,
  49. }: Props) {
  50. const {organization} = useLegacyStore(OrganizationStore);
  51. const {projects, initiallyLoaded: projectsLoaded} = useProjects();
  52. const location = useLocation();
  53. const {user, isSelfHosted} = ConfigStore.getConfig();
  54. const [state, setState] = useState<State>({
  55. subject: undefined,
  56. additionalInfo: undefined,
  57. });
  58. const project = useMemo(() => {
  59. if (projectsLoaded && location.query.project) {
  60. return projects.find(p => p.id === location.query.project);
  61. }
  62. return undefined;
  63. }, [projectsLoaded, projects, location.query.project]);
  64. function handleSubmit() {
  65. const {subject, additionalInfo} = state;
  66. if (!defined(subject)) {
  67. return;
  68. }
  69. feedbackClient.captureEvent({
  70. message: additionalInfo?.trim()
  71. ? `Feedback: ${feedbackTypes[subject]} - ${additionalInfo}`
  72. : `Feedback: ${feedbackTypes[subject]}`,
  73. request: {
  74. url: location.pathname,
  75. },
  76. extra: {
  77. orgFeatures: organization?.features ?? [],
  78. orgAccess: organization?.access ?? [],
  79. projectFeatures: project?.features ?? [],
  80. },
  81. tags: {
  82. featureName,
  83. },
  84. user,
  85. level: 'info',
  86. });
  87. addSuccessMessage(t('Thanks for taking the time to provide us feedback!'));
  88. closeModal();
  89. }
  90. return (
  91. <Fragment>
  92. <Header closeButton>
  93. <h3>{t('Submit Feedback')}</h3>
  94. </Header>
  95. <Body>
  96. <SelectField
  97. label={t('Type of feedback')}
  98. name="subject"
  99. inline={false}
  100. options={feedbackTypes.map((feedbackType, index) => ({
  101. value: index,
  102. label: feedbackType,
  103. }))}
  104. placeholder={t('Select type of feedback')}
  105. value={state.subject}
  106. onChange={value => setState({...state, subject: value})}
  107. flexibleControlStateSize
  108. stacked
  109. required
  110. />
  111. <Field
  112. label={t('Additional feedback')}
  113. inline={false}
  114. required={false}
  115. flexibleControlStateSize
  116. stacked
  117. >
  118. <Textarea
  119. name="additional-feedback"
  120. value={state.additionalInfo}
  121. rows={5}
  122. autosize
  123. placeholder={t('What did you expect?')}
  124. onChange={event =>
  125. setState({
  126. ...state,
  127. additionalInfo: event.target.value,
  128. })
  129. }
  130. />
  131. </Field>
  132. {isSelfHosted && (
  133. <p>
  134. {tct(
  135. "You agree that any feedback you submit is subject to Sentry's [privacyPolicy:Privacy Policy] and Sentry may use such feedback without restriction or obligation.",
  136. {
  137. privacyPolicy: <ExternalLink href="https://sentry.io/privacy/" />,
  138. }
  139. )}
  140. </p>
  141. )}
  142. </Body>
  143. <Footer>
  144. <ButtonBar gap={1}>
  145. <Button onClick={closeModal}>{t('Cancel')}</Button>
  146. <Button
  147. priority="primary"
  148. onClick={handleSubmit}
  149. disabled={!defined(state.subject)}
  150. >
  151. {t('Submit Feedback')}
  152. </Button>
  153. </ButtonBar>
  154. </Footer>
  155. </Fragment>
  156. );
  157. }
  158. export const modalCss = css`
  159. width: 100%;
  160. max-width: 680px;
  161. `;