index.tsx 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200
  1. import {useEffect, useState} from 'react';
  2. import moment from 'moment-timezone';
  3. import {addErrorMessage, addSuccessMessage} from 'sentry/actionCreators/indicator';
  4. import BooleanField, {
  5. type BooleanFieldProps,
  6. } from 'sentry/components/forms/fields/booleanField';
  7. import DateTimeField, {
  8. type DateTimeFieldProps,
  9. } from 'sentry/components/forms/fields/dateTimeField';
  10. import Panel from 'sentry/components/panels/panel';
  11. import PanelAlert from 'sentry/components/panels/panelAlert';
  12. import PanelBody from 'sentry/components/panels/panelBody';
  13. import PanelHeader from 'sentry/components/panels/panelHeader';
  14. import {t, tct} from 'sentry/locale';
  15. import {useApiQuery} from 'sentry/utils/queryClient';
  16. import useApi from 'sentry/utils/useApi';
  17. import useOrganization from 'sentry/utils/useOrganization';
  18. type WaiverData = {
  19. accessEnd: string | null;
  20. accessStart: string | null;
  21. };
  22. export default function DataSecrecy() {
  23. const api = useApi();
  24. const organization = useOrganization();
  25. // state for the allowSuperuserAccess bit field
  26. const [allowAccess, setAllowAccess] = useState(organization.allowSuperuserAccess);
  27. // state of the data secrecy waiver
  28. const [waiver, setWaiver] = useState<WaiverData>();
  29. // state for the allowDateFormData field
  30. const [allowDateFormData, setAllowDateFormData] = useState<string>('');
  31. const {data, refetch} = useApiQuery<WaiverData>(
  32. [`/organizations/${organization.slug}/data-secrecy/`],
  33. {
  34. staleTime: 3000,
  35. retry: (failureCount, error) => failureCount < 3 && error.status !== 404,
  36. }
  37. );
  38. useEffect(() => {
  39. if (data?.accessEnd) {
  40. setWaiver(data);
  41. // slice it to yyyy-MM-ddThh:mm format (be aware of the timezone)
  42. const localDate = moment(data.accessEnd).local();
  43. setAllowDateFormData(localDate.format('YYYY-MM-DDTHH:mm'));
  44. }
  45. }, [data]);
  46. const updateAllowedAccess = async (value: boolean) => {
  47. try {
  48. await api.requestPromise(`/organizations/${organization.slug}/`, {
  49. method: 'PUT',
  50. data: {allowSuperuserAccess: value},
  51. });
  52. setAllowAccess(value);
  53. // if the user has allowed access, we need to remove the temporary access window
  54. // only if there is an existing waiver
  55. if (value && waiver) {
  56. await api.requestPromise(`/organizations/${organization.slug}/data-secrecy/`, {
  57. method: 'DELETE',
  58. });
  59. setAllowDateFormData('');
  60. setWaiver(undefined);
  61. }
  62. addSuccessMessage(
  63. value
  64. ? waiver
  65. ? t(
  66. 'Successfully removed temporary access window and allowed support access.'
  67. )
  68. : t('Successfully allowed support access.')
  69. : t('Successfully removed support access.')
  70. );
  71. } catch (error) {
  72. addErrorMessage(t('Unable to save changes.'));
  73. }
  74. // refetch to get the latest waiver data
  75. refetch();
  76. };
  77. const updateTempAccessDate = async () => {
  78. if (!allowDateFormData) {
  79. try {
  80. await api.requestPromise(`/organizations/${organization.slug}/data-secrecy/`, {
  81. method: 'DELETE',
  82. });
  83. setWaiver({accessStart: '', accessEnd: ''});
  84. addSuccessMessage(t('Successfully removed temporary access window.'));
  85. } catch (error) {
  86. addErrorMessage(t('Unable to remove temporary access window.'));
  87. }
  88. return;
  89. }
  90. // maintain the standard format of storing the date in UTC
  91. // even though the api should be able to handle the local time
  92. const nextData: WaiverData = {
  93. accessStart: moment().utc().toISOString(),
  94. accessEnd: moment.tz(allowDateFormData, moment.tz.guess()).utc().toISOString(),
  95. };
  96. try {
  97. await api.requestPromise(`/organizations/${organization.slug}/data-secrecy/`, {
  98. method: 'PUT',
  99. data: nextData,
  100. });
  101. setWaiver(nextData);
  102. addSuccessMessage(t('Successfully updated temporary access window.'));
  103. } catch (error) {
  104. addErrorMessage(t('Unable to save changes.'));
  105. setAllowDateFormData('');
  106. }
  107. // refetch to get the latest waiver data
  108. refetch();
  109. };
  110. const allowAccessProps: BooleanFieldProps = {
  111. name: 'allowSuperuserAccess',
  112. label: t('Allow access to Sentry employees'),
  113. help: t(
  114. 'Sentry employees will not have access to your organization unless granted permission'
  115. ),
  116. 'aria-label': t(
  117. 'Sentry employees will not have access to your data unless granted permission'
  118. ),
  119. value: allowAccess,
  120. disabled: !organization.access.includes('org:write'),
  121. onBlur: updateAllowedAccess,
  122. };
  123. const allowTempAccessProps: DateTimeFieldProps = {
  124. name: 'allowTemporarySuperuserAccess',
  125. label: t('Allow temporary access to Sentry employees'),
  126. help: t(
  127. 'Open a temporary time window for Sentry employees to access your organization'
  128. ),
  129. // disable the field if the user has allowed access or if the user does not have org:write access
  130. disabled: allowAccess || !organization.access.includes('org:write'),
  131. disabledReason: allowAccess
  132. ? t('Disable permanent access first to set temporary access')
  133. : !organization.access.includes('org:write')
  134. ? t('You do not have permission to modify access settings')
  135. : undefined,
  136. value: allowDateFormData,
  137. onBlur: updateTempAccessDate,
  138. onChange: (v: any) => {
  139. // Don't allow the user to set the date if they have allowed access
  140. if (allowAccess) {
  141. return;
  142. }
  143. // the picker doesn't like having a datetime string with seconds+ and a timezone,
  144. // so we remove it -- we will add it back when we save the date
  145. const formattedDate = v ? moment(v).format('YYYY-MM-DDTHH:mm') : '';
  146. setAllowDateFormData(formattedDate);
  147. },
  148. };
  149. return (
  150. <Panel>
  151. <PanelHeader>{t('Support Access')}</PanelHeader>
  152. <PanelBody>
  153. {!allowAccess && (
  154. <PanelAlert>
  155. {waiver?.accessEnd && moment().isBefore(moment(waiver.accessEnd))
  156. ? tct(`Sentry employees has access to your organization until [date]`, {
  157. date: formatDateTime(waiver?.accessEnd),
  158. })
  159. : t('Sentry employees do not have access to your organization')}
  160. </PanelAlert>
  161. )}
  162. <BooleanField {...allowAccessProps} />
  163. <DateTimeField {...allowTempAccessProps} />
  164. </PanelBody>
  165. </Panel>
  166. );
  167. }
  168. const formatDateTime = (dateString: string) => {
  169. const date = new Date(dateString);
  170. const options: Intl.DateTimeFormatOptions = {
  171. month: 'short',
  172. day: 'numeric',
  173. year: 'numeric',
  174. hour: 'numeric',
  175. minute: 'numeric',
  176. hour12: true,
  177. timeZoneName: 'short',
  178. };
  179. return new Intl.DateTimeFormat('en-US', options).format(date);
  180. };