details.tsx 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  1. import {Fragment} from 'react';
  2. import styled from '@emotion/styled';
  3. import {addErrorMessage} from 'sentry/actionCreators/indicator';
  4. import {openModal} from 'sentry/actionCreators/modal';
  5. import {Alert} from 'sentry/components/alert';
  6. import {Button} from 'sentry/components/button';
  7. import Confirm from 'sentry/components/confirm';
  8. import Form from 'sentry/components/forms/form';
  9. import FormField from 'sentry/components/forms/formField';
  10. import JsonForm from 'sentry/components/forms/jsonForm';
  11. import LoadingError from 'sentry/components/loadingError';
  12. import LoadingIndicator from 'sentry/components/loadingIndicator';
  13. import Panel from 'sentry/components/panels/panel';
  14. import PanelBody from 'sentry/components/panels/panelBody';
  15. import PanelHeader from 'sentry/components/panels/panelHeader';
  16. import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
  17. import TextCopyInput from 'sentry/components/textCopyInput';
  18. import apiApplication from 'sentry/data/forms/apiApplication';
  19. import {t} from 'sentry/locale';
  20. import ConfigStore from 'sentry/stores/configStore';
  21. import type {ApiApplication} from 'sentry/types/user';
  22. import getDynamicText from 'sentry/utils/getDynamicText';
  23. import {
  24. type ApiQueryKey,
  25. useApiQuery,
  26. useMutation,
  27. useQueryClient,
  28. } from 'sentry/utils/queryClient';
  29. import useApi from 'sentry/utils/useApi';
  30. import {useParams} from 'sentry/utils/useParams';
  31. import SettingsPageHeader from 'sentry/views/settings/components/settingsPageHeader';
  32. const PAGE_TITLE = t('Application Details');
  33. function getAppQueryKey(appId: string): ApiQueryKey {
  34. return [`/api-applications/${appId}/`];
  35. }
  36. interface RotateClientSecretResponse {
  37. clientSecret: string;
  38. }
  39. function ApiApplicationsDetails() {
  40. const api = useApi();
  41. const {appId} = useParams<{appId: string}>();
  42. const queryClient = useQueryClient();
  43. const urlPrefix = ConfigStore.get('urlPrefix');
  44. const {
  45. data: app,
  46. isPending,
  47. isError,
  48. refetch,
  49. } = useApiQuery<ApiApplication>(getAppQueryKey(appId), {
  50. staleTime: 0,
  51. });
  52. const {mutate: rotateClientSecret} = useMutation<RotateClientSecretResponse>(
  53. () => {
  54. return api.requestPromise(`/api-applications/${appId}/rotate-secret/`, {
  55. method: 'POST',
  56. });
  57. },
  58. {
  59. onSuccess: data => {
  60. openModal(({Body, Header}) => (
  61. <Fragment>
  62. <Header>{t('Your new Client Secret')}</Header>
  63. <Body>
  64. <Alert type="info" showIcon>
  65. {t('This will be the only time your client secret is visible!')}
  66. </Alert>
  67. <TextCopyInput aria-label={t('new-client-secret')}>
  68. {data.clientSecret}
  69. </TextCopyInput>
  70. </Body>
  71. </Fragment>
  72. ));
  73. },
  74. onError: () => {
  75. addErrorMessage(t('Error rotating secret'));
  76. },
  77. onSettled: () => {
  78. queryClient.invalidateQueries(getAppQueryKey(appId));
  79. },
  80. }
  81. );
  82. if (isPending) {
  83. return <LoadingIndicator />;
  84. }
  85. if (isError) {
  86. return <LoadingError onRetry={refetch} />;
  87. }
  88. return (
  89. <SentryDocumentTitle title={PAGE_TITLE}>
  90. <SettingsPageHeader title={PAGE_TITLE} />
  91. <Form
  92. apiMethod="PUT"
  93. apiEndpoint={`/api-applications/${appId}/`}
  94. saveOnBlur
  95. allowUndo
  96. initialData={app}
  97. onSubmitError={() => addErrorMessage('Unable to save change')}
  98. >
  99. <JsonForm forms={apiApplication} />
  100. <Panel>
  101. <PanelHeader>{t('Credentials')}</PanelHeader>
  102. <PanelBody>
  103. <FormField name="clientID" label="Client ID">
  104. {({value}) => (
  105. <TextCopyInput>
  106. {getDynamicText({value, fixed: 'CI_CLIENT_ID'})}
  107. </TextCopyInput>
  108. )}
  109. </FormField>
  110. <FormField
  111. name="clientSecret"
  112. label="Client Secret"
  113. help={t(`Your secret is only available briefly after application creation. Make
  114. sure to save this value!`)}
  115. >
  116. {({value}) =>
  117. value ? (
  118. <TextCopyInput>
  119. {getDynamicText({value, fixed: 'CI_CLIENT_SECRET'})}
  120. </TextCopyInput>
  121. ) : (
  122. <ClientSecret>
  123. <HiddenSecret>{t('hidden')}</HiddenSecret>
  124. <Confirm
  125. onConfirm={rotateClientSecret}
  126. message={t(
  127. 'Are you sure you want to rotate the client secret? The current one will not be usable anymore, and this cannot be undone.'
  128. )}
  129. >
  130. <Button size="xs" priority="danger">
  131. Rotate client secret
  132. </Button>
  133. </Confirm>
  134. </ClientSecret>
  135. )
  136. }
  137. </FormField>
  138. <FormField name="" label="Authorization URL">
  139. {() => <TextCopyInput>{`${urlPrefix}/oauth/authorize/`}</TextCopyInput>}
  140. </FormField>
  141. <FormField name="" label="Token URL">
  142. {() => <TextCopyInput>{`${urlPrefix}/oauth/token/`}</TextCopyInput>}
  143. </FormField>
  144. </PanelBody>
  145. </Panel>
  146. </Form>
  147. </SentryDocumentTitle>
  148. );
  149. }
  150. const HiddenSecret = styled('span')`
  151. width: 100px;
  152. font-style: italic;
  153. `;
  154. const ClientSecret = styled('div')`
  155. display: flex;
  156. justify-content: right;
  157. align-items: center;
  158. margin-right: 0;
  159. `;
  160. export default ApiApplicationsDetails;