feedbackModal.tsx 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174
  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. export interface FeedBackModalProps {
  32. featureName: string;
  33. feedbackTypes: string[];
  34. }
  35. interface Props
  36. extends FeedBackModalProps,
  37. Pick<ModalRenderProps, 'Header' | 'Body' | 'Footer' | 'closeModal'> {}
  38. type State = {additionalInfo?: string; subject?: number};
  39. export function FeedbackModal({
  40. Header,
  41. Body,
  42. Footer,
  43. closeModal,
  44. feedbackTypes,
  45. featureName,
  46. }: Props) {
  47. const {organization} = useLegacyStore(OrganizationStore);
  48. const {projects, initiallyLoaded: projectsLoaded} = useProjects();
  49. const location = useLocation();
  50. const {user, isSelfHosted} = ConfigStore.getConfig();
  51. const [state, setState] = useState<State>({
  52. subject: undefined,
  53. additionalInfo: undefined,
  54. });
  55. const project = useMemo(() => {
  56. if (projectsLoaded && location.query.project) {
  57. return projects.find(p => p.id === location.query.project);
  58. }
  59. return undefined;
  60. }, [projectsLoaded, projects, location.query.project]);
  61. function handleSubmit() {
  62. const {subject, additionalInfo} = state;
  63. if (!defined(subject)) {
  64. return;
  65. }
  66. feedbackClient.captureEvent({
  67. message: additionalInfo?.trim()
  68. ? `Feedback: ${feedbackTypes[subject]} - ${additionalInfo}`
  69. : `Feedback: ${feedbackTypes[subject]}`,
  70. request: {
  71. url: location.pathname,
  72. },
  73. extra: {
  74. orgFeatures: organization?.features ?? [],
  75. orgAccess: organization?.access ?? [],
  76. projectFeatures: project?.features ?? [],
  77. },
  78. tags: {
  79. featureName,
  80. },
  81. user,
  82. level: 'info',
  83. });
  84. addSuccessMessage(t('Thanks for taking the time to provide us feedback!'));
  85. closeModal();
  86. }
  87. return (
  88. <Fragment>
  89. <Header closeButton>
  90. <h3>{t('Submit Feedback')}</h3>
  91. </Header>
  92. <Body>
  93. <SelectField
  94. label={t('Type of feedback')}
  95. name="subject"
  96. inline={false}
  97. options={feedbackTypes.map((feedbackType, index) => ({
  98. value: index,
  99. label: feedbackType,
  100. }))}
  101. placeholder={t('Select type of feedback')}
  102. value={state.subject}
  103. onChange={value => setState({...state, subject: value})}
  104. flexibleControlStateSize
  105. stacked
  106. required
  107. />
  108. <Field
  109. label={t('Additional feedback')}
  110. inline={false}
  111. required={false}
  112. flexibleControlStateSize
  113. stacked
  114. >
  115. <Textarea
  116. name="additional-feedback"
  117. value={state.additionalInfo}
  118. rows={5}
  119. autosize
  120. placeholder={t('What did you expect?')}
  121. onChange={event =>
  122. setState({
  123. ...state,
  124. additionalInfo: event.target.value,
  125. })
  126. }
  127. />
  128. </Field>
  129. {isSelfHosted && (
  130. <p>
  131. {tct(
  132. "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.",
  133. {
  134. privacyPolicy: <ExternalLink href="https://sentry.io/privacy/" />,
  135. }
  136. )}
  137. </p>
  138. )}
  139. </Body>
  140. <Footer>
  141. <ButtonBar gap={1}>
  142. <Button onClick={closeModal}>{t('Cancel')}</Button>
  143. <Button
  144. priority="primary"
  145. onClick={handleSubmit}
  146. disabled={!defined(state.subject)}
  147. >
  148. {t('Submit Feedback')}
  149. </Button>
  150. </ButtonBar>
  151. </Footer>
  152. </Fragment>
  153. );
  154. }
  155. export const modalCss = css`
  156. width: 100%;
  157. max-width: 680px;
  158. `;