index.tsx 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170
  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. const [allowAccess, setAllowAccess] = useState(organization.allowSuperuserAccess);
  26. const [allowDate, setAllowDate] = useState<WaiverData>();
  27. const [allowDateFormData, setAllowDateFormData] = useState<string>('');
  28. const {data, refetch} = useApiQuery<WaiverData>(
  29. [`/organizations/${organization.slug}/data-secrecy/`],
  30. {
  31. staleTime: 3000,
  32. retry: (failureCount, error) => failureCount < 3 && error.status !== 404,
  33. }
  34. );
  35. const hasValidTempAccess =
  36. allowDate?.accessEnd && moment().toISOString() < allowDate.accessEnd;
  37. useEffect(() => {
  38. if (data?.accessEnd) {
  39. setAllowDate(data);
  40. // slice it to yyyy-MM-ddThh:mm format (be aware of the timezone)
  41. const localDate = moment(data.accessEnd).local();
  42. setAllowDateFormData(localDate.format('YYYY-MM-DDTHH:mm'));
  43. }
  44. }, [data]);
  45. const updateAllowedAccess = async (value: boolean) => {
  46. try {
  47. await api.requestPromise(`/organizations/${organization.slug}/`, {
  48. method: 'PUT',
  49. data: {allowSuperuserAccess: value},
  50. });
  51. setAllowAccess(value);
  52. addSuccessMessage(t('Successfully updated access.'));
  53. } catch (error) {
  54. addErrorMessage(t('Unable to save changes.'));
  55. }
  56. };
  57. const updateTempAccessDate = async () => {
  58. if (!allowDateFormData) {
  59. try {
  60. await api.requestPromise(`/organizations/${organization.slug}/data-secrecy/`, {
  61. method: 'DELETE',
  62. });
  63. setAllowDate({accessStart: '', accessEnd: ''});
  64. addSuccessMessage(t('Successfully removed temporary access window.'));
  65. } catch (error) {
  66. addErrorMessage(t('Unable to remove temporary access window.'));
  67. }
  68. return;
  69. }
  70. // maintain the standard format of storing the date in UTC
  71. // even though the api should be able to handle the local time
  72. const nextData: WaiverData = {
  73. accessStart: moment().utc().toISOString(),
  74. accessEnd: moment.tz(allowDateFormData, moment.tz.guess()).utc().toISOString(),
  75. };
  76. try {
  77. await await api.requestPromise(
  78. `/organizations/${organization.slug}/data-secrecy/`,
  79. {
  80. method: 'PUT',
  81. data: nextData,
  82. }
  83. );
  84. setAllowDate(nextData);
  85. addSuccessMessage(t('Successfully updated temporary access window.'));
  86. } catch (error) {
  87. addErrorMessage(t('Unable to save changes.'));
  88. setAllowDateFormData('');
  89. }
  90. // refetch to get the latest waiver data
  91. refetch();
  92. };
  93. const allowAccessProps: BooleanFieldProps = {
  94. name: 'allowSuperuserAccess',
  95. label: t('Allow access to Sentry employees'),
  96. help: t(
  97. 'Sentry employees will not have access to your organization unless granted permission'
  98. ),
  99. 'aria-label': t(
  100. 'Sentry employees will not have access to your data unless granted permission'
  101. ),
  102. value: allowAccess,
  103. disabled: !organization.access.includes('org:write'),
  104. onBlur: updateAllowedAccess,
  105. };
  106. const allowTempAccessProps: DateTimeFieldProps = {
  107. name: 'allowTemporarySuperuserAccess',
  108. label: t('Allow temporary access to Sentry employees'),
  109. help: t(
  110. 'Open a temporary time window for Sentry employees to access your organization'
  111. ),
  112. disabled: allowAccess && !organization.access.includes('org:write'),
  113. value: allowAccess ? '' : allowDateFormData,
  114. onBlur: updateTempAccessDate,
  115. onChange: v => {
  116. // the picker doesn't like having a datetime string with seconds+ and a timezone,
  117. // so we remove it -- we will add it back when we save the date
  118. const formattedDate = v ? moment(v).format('YYYY-MM-DDTHH:mm') : '';
  119. setAllowDateFormData(formattedDate);
  120. },
  121. };
  122. return (
  123. <Panel>
  124. <PanelHeader>{t('Support Access')}</PanelHeader>
  125. <PanelBody>
  126. {!allowAccess && (
  127. <PanelAlert>
  128. {hasValidTempAccess
  129. ? tct(`Sentry employees has access to your organization until [date]`, {
  130. date: formatDateTime(allowDate?.accessEnd as string),
  131. })
  132. : t('Sentry employees do not have access to your organization')}
  133. </PanelAlert>
  134. )}
  135. <BooleanField {...(allowAccessProps as BooleanFieldProps)} />
  136. <DateTimeField {...(allowTempAccessProps as DateTimeFieldProps)} />
  137. </PanelBody>
  138. </Panel>
  139. );
  140. }
  141. const formatDateTime = (dateString: string) => {
  142. const date = new Date(dateString);
  143. const options: Intl.DateTimeFormatOptions = {
  144. month: 'short',
  145. day: 'numeric',
  146. year: 'numeric',
  147. hour: 'numeric',
  148. minute: 'numeric',
  149. hour12: true,
  150. timeZoneName: 'short',
  151. };
  152. return new Intl.DateTimeFormat('en-US', options).format(date);
  153. };