details.tsx 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
  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. mutationFn: () => {
  54. return api.requestPromise(`/api-applications/${appId}/rotate-secret/`, {
  55. method: 'POST',
  56. });
  57. },
  58. onSuccess: data => {
  59. openModal(({Body, Header}) => (
  60. <Fragment>
  61. <Header>{t('Your new Client Secret')}</Header>
  62. <Body>
  63. <Alert type="info" showIcon>
  64. {t('This will be the only time your client secret is visible!')}
  65. </Alert>
  66. <TextCopyInput aria-label={t('new-client-secret')}>
  67. {data.clientSecret}
  68. </TextCopyInput>
  69. </Body>
  70. </Fragment>
  71. ));
  72. },
  73. onError: () => {
  74. addErrorMessage(t('Error rotating secret'));
  75. },
  76. onSettled: () => {
  77. queryClient.invalidateQueries({queryKey: getAppQueryKey(appId)});
  78. },
  79. });
  80. if (isPending) {
  81. return <LoadingIndicator />;
  82. }
  83. if (isError) {
  84. return <LoadingError onRetry={refetch} />;
  85. }
  86. return (
  87. <SentryDocumentTitle title={PAGE_TITLE}>
  88. <SettingsPageHeader title={PAGE_TITLE} />
  89. <Form
  90. apiMethod="PUT"
  91. apiEndpoint={`/api-applications/${appId}/`}
  92. saveOnBlur
  93. allowUndo
  94. initialData={app}
  95. onSubmitError={() => addErrorMessage('Unable to save change')}
  96. >
  97. <JsonForm forms={apiApplication} />
  98. <Panel>
  99. <PanelHeader>{t('Credentials')}</PanelHeader>
  100. <PanelBody>
  101. <FormField name="clientID" label="Client ID">
  102. {({value}) => (
  103. <TextCopyInput>
  104. {getDynamicText({value, fixed: 'CI_CLIENT_ID'})}
  105. </TextCopyInput>
  106. )}
  107. </FormField>
  108. <FormField
  109. name="clientSecret"
  110. label={t('Client Secret')}
  111. help={t(`Your secret is only available briefly after application creation. Make
  112. sure to save this value!`)}
  113. >
  114. {({value}) =>
  115. value ? (
  116. <TextCopyInput>
  117. {getDynamicText({value, fixed: 'CI_CLIENT_SECRET'})}
  118. </TextCopyInput>
  119. ) : (
  120. <ClientSecret>
  121. <HiddenSecret>{t('hidden')}</HiddenSecret>
  122. <Confirm
  123. onConfirm={rotateClientSecret}
  124. message={t(
  125. 'Are you sure you want to rotate the client secret? The current one will not be usable anymore, and this cannot be undone.'
  126. )}
  127. >
  128. <Button size="xs" priority="danger">
  129. {t('Rotate client secret')}
  130. </Button>
  131. </Confirm>
  132. </ClientSecret>
  133. )
  134. }
  135. </FormField>
  136. <FormField name="" label={t('Authorization URL')}>
  137. {() => <TextCopyInput>{`${urlPrefix}/oauth/authorize/`}</TextCopyInput>}
  138. </FormField>
  139. <FormField name="" label={t('Token URL')}>
  140. {() => <TextCopyInput>{`${urlPrefix}/oauth/token/`}</TextCopyInput>}
  141. </FormField>
  142. </PanelBody>
  143. </Panel>
  144. </Form>
  145. </SentryDocumentTitle>
  146. );
  147. }
  148. const HiddenSecret = styled('span')`
  149. width: 100px;
  150. font-style: italic;
  151. `;
  152. const ClientSecret = styled('div')`
  153. display: flex;
  154. justify-content: right;
  155. align-items: center;
  156. margin-right: 0;
  157. `;
  158. export default ApiApplicationsDetails;