|
@@ -1,5 +1,4 @@
|
|
|
import {Fragment} from 'react';
|
|
|
-import type {RouteComponentProps} from 'react-router';
|
|
|
import styled from '@emotion/styled';
|
|
|
|
|
|
import {addErrorMessage} from 'sentry/actionCreators/indicator';
|
|
@@ -10,131 +9,160 @@ import Confirm from 'sentry/components/confirm';
|
|
|
import Form from 'sentry/components/forms/form';
|
|
|
import FormField from 'sentry/components/forms/formField';
|
|
|
import JsonForm from 'sentry/components/forms/jsonForm';
|
|
|
+import LoadingError from 'sentry/components/loadingError';
|
|
|
+import LoadingIndicator from 'sentry/components/loadingIndicator';
|
|
|
import Panel from 'sentry/components/panels/panel';
|
|
|
import PanelBody from 'sentry/components/panels/panelBody';
|
|
|
import PanelHeader from 'sentry/components/panels/panelHeader';
|
|
|
+import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
|
|
|
import TextCopyInput from 'sentry/components/textCopyInput';
|
|
|
import apiApplication from 'sentry/data/forms/apiApplication';
|
|
|
import {t} from 'sentry/locale';
|
|
|
import ConfigStore from 'sentry/stores/configStore';
|
|
|
import type {ApiApplication} from 'sentry/types/user';
|
|
|
import getDynamicText from 'sentry/utils/getDynamicText';
|
|
|
-import DeprecatedAsyncView from 'sentry/views/deprecatedAsyncView';
|
|
|
+import {
|
|
|
+ type ApiQueryKey,
|
|
|
+ useApiQuery,
|
|
|
+ useMutation,
|
|
|
+ useQueryClient,
|
|
|
+} from 'sentry/utils/queryClient';
|
|
|
+import useApi from 'sentry/utils/useApi';
|
|
|
+import {useParams} from 'sentry/utils/useParams';
|
|
|
import SettingsPageHeader from 'sentry/views/settings/components/settingsPageHeader';
|
|
|
|
|
|
-type Props = RouteComponentProps<{appId: string}, {}>;
|
|
|
-type State = {
|
|
|
- app: ApiApplication;
|
|
|
-} & DeprecatedAsyncView['state'];
|
|
|
-
|
|
|
-class ApiApplicationsDetails extends DeprecatedAsyncView<Props, State> {
|
|
|
- rotateClientSecret = async () => {
|
|
|
- try {
|
|
|
- const rotateResponse = await this.api.requestPromise(
|
|
|
- `/api-applications/${this.props.params.appId}/rotate-secret/`,
|
|
|
- {
|
|
|
- method: 'POST',
|
|
|
- }
|
|
|
- );
|
|
|
- openModal(({Body, Header}) => (
|
|
|
- <Fragment>
|
|
|
- <Header>{t('Your new Client Secret')}</Header>
|
|
|
- <Body>
|
|
|
- <Alert type="info" showIcon>
|
|
|
- {t('This will be the only time your client secret is visible!')}
|
|
|
- </Alert>
|
|
|
- <TextCopyInput aria-label={t('new-client-secret')}>
|
|
|
- {rotateResponse.clientSecret}
|
|
|
- </TextCopyInput>
|
|
|
- </Body>
|
|
|
- </Fragment>
|
|
|
- ));
|
|
|
- } catch {
|
|
|
- addErrorMessage(t('Error rotating secret'));
|
|
|
+const PAGE_TITLE = t('Application Details');
|
|
|
+
|
|
|
+function getAppQueryKey(appId: string): ApiQueryKey {
|
|
|
+ return [`/api-applications/${appId}/`];
|
|
|
+}
|
|
|
+
|
|
|
+interface RotateClientSecretResponse {
|
|
|
+ clientSecret: string;
|
|
|
+}
|
|
|
+
|
|
|
+function ApiApplicationsDetails() {
|
|
|
+ const api = useApi();
|
|
|
+ const {appId} = useParams<{appId: string}>();
|
|
|
+ const queryClient = useQueryClient();
|
|
|
+
|
|
|
+ const urlPrefix = ConfigStore.get('urlPrefix');
|
|
|
+
|
|
|
+ const {
|
|
|
+ data: app,
|
|
|
+ isPending,
|
|
|
+ isError,
|
|
|
+ refetch,
|
|
|
+ } = useApiQuery<ApiApplication>(getAppQueryKey(appId), {
|
|
|
+ staleTime: 0,
|
|
|
+ });
|
|
|
+
|
|
|
+ const {mutate: rotateClientSecret} = useMutation<RotateClientSecretResponse>(
|
|
|
+ () => {
|
|
|
+ return api.requestPromise(`/api-applications/${appId}/rotate-secret/`, {
|
|
|
+ method: 'POST',
|
|
|
+ });
|
|
|
+ },
|
|
|
+ {
|
|
|
+ onSuccess: data => {
|
|
|
+ openModal(({Body, Header}) => (
|
|
|
+ <Fragment>
|
|
|
+ <Header>{t('Your new Client Secret')}</Header>
|
|
|
+ <Body>
|
|
|
+ <Alert type="info" showIcon>
|
|
|
+ {t('This will be the only time your client secret is visible!')}
|
|
|
+ </Alert>
|
|
|
+ <TextCopyInput aria-label={t('new-client-secret')}>
|
|
|
+ {data.clientSecret}
|
|
|
+ </TextCopyInput>
|
|
|
+ </Body>
|
|
|
+ </Fragment>
|
|
|
+ ));
|
|
|
+ },
|
|
|
+ onError: () => {
|
|
|
+ addErrorMessage(t('Error rotating secret'));
|
|
|
+ },
|
|
|
+ onSettled: () => {
|
|
|
+ queryClient.invalidateQueries(getAppQueryKey(appId));
|
|
|
+ },
|
|
|
}
|
|
|
- };
|
|
|
+ );
|
|
|
|
|
|
- getEndpoints(): ReturnType<DeprecatedAsyncView['getEndpoints']> {
|
|
|
- return [['app', `/api-applications/${this.props.params.appId}/`]];
|
|
|
+ if (isPending) {
|
|
|
+ return <LoadingIndicator />;
|
|
|
}
|
|
|
|
|
|
- getTitle() {
|
|
|
- return t('Application Details');
|
|
|
+ if (isError) {
|
|
|
+ return <LoadingError onRetry={refetch} />;
|
|
|
}
|
|
|
|
|
|
- renderBody() {
|
|
|
- const urlPrefix = ConfigStore.get('urlPrefix');
|
|
|
-
|
|
|
- return (
|
|
|
- <div>
|
|
|
- <SettingsPageHeader title={this.getTitle()} />
|
|
|
-
|
|
|
- <Form
|
|
|
- apiMethod="PUT"
|
|
|
- apiEndpoint={`/api-applications/${this.props.params.appId}/`}
|
|
|
- saveOnBlur
|
|
|
- allowUndo
|
|
|
- initialData={this.state.app}
|
|
|
- onSubmitError={() => addErrorMessage('Unable to save change')}
|
|
|
- >
|
|
|
- <JsonForm forms={apiApplication} />
|
|
|
-
|
|
|
- <Panel>
|
|
|
- <PanelHeader>{t('Credentials')}</PanelHeader>
|
|
|
-
|
|
|
- <PanelBody>
|
|
|
- <FormField name="clientID" label="Client ID">
|
|
|
- {({value}) => (
|
|
|
- <div>
|
|
|
- <TextCopyInput>
|
|
|
- {getDynamicText({value, fixed: 'CI_CLIENT_ID'})}
|
|
|
- </TextCopyInput>
|
|
|
- </div>
|
|
|
- )}
|
|
|
- </FormField>
|
|
|
-
|
|
|
- <FormField
|
|
|
- name="clientSecret"
|
|
|
- label="Client Secret"
|
|
|
- help={t(`Your secret is only available briefly after application creation. Make
|
|
|
+ return (
|
|
|
+ <SentryDocumentTitle title={PAGE_TITLE}>
|
|
|
+ <SettingsPageHeader title={PAGE_TITLE} />
|
|
|
+
|
|
|
+ <Form
|
|
|
+ apiMethod="PUT"
|
|
|
+ apiEndpoint={`/api-applications/${appId}/`}
|
|
|
+ saveOnBlur
|
|
|
+ allowUndo
|
|
|
+ initialData={app}
|
|
|
+ onSubmitError={() => addErrorMessage('Unable to save change')}
|
|
|
+ >
|
|
|
+ <JsonForm forms={apiApplication} />
|
|
|
+
|
|
|
+ <Panel>
|
|
|
+ <PanelHeader>{t('Credentials')}</PanelHeader>
|
|
|
+
|
|
|
+ <PanelBody>
|
|
|
+ <FormField name="clientID" label="Client ID">
|
|
|
+ {({value}) => (
|
|
|
+ <TextCopyInput>
|
|
|
+ {getDynamicText({value, fixed: 'CI_CLIENT_ID'})}
|
|
|
+ </TextCopyInput>
|
|
|
+ )}
|
|
|
+ </FormField>
|
|
|
+
|
|
|
+ <FormField
|
|
|
+ name="clientSecret"
|
|
|
+ label="Client Secret"
|
|
|
+ help={t(`Your secret is only available briefly after application creation. Make
|
|
|
sure to save this value!`)}
|
|
|
- >
|
|
|
- {({value}) =>
|
|
|
- value ? (
|
|
|
- <TextCopyInput>
|
|
|
- {getDynamicText({value, fixed: 'CI_CLIENT_SECRET'})}
|
|
|
- </TextCopyInput>
|
|
|
- ) : (
|
|
|
- <ClientSecret>
|
|
|
- <HiddenSecret>{t('hidden')}</HiddenSecret>
|
|
|
- <Confirm
|
|
|
- onConfirm={this.rotateClientSecret}
|
|
|
- message={t(
|
|
|
- 'Are you sure you want to rotate the client secret? The current one will not be usable anymore, and this cannot be undone.'
|
|
|
- )}
|
|
|
- >
|
|
|
- <Button size="xs" priority="danger">
|
|
|
- Rotate client secret
|
|
|
- </Button>
|
|
|
- </Confirm>
|
|
|
- </ClientSecret>
|
|
|
- )
|
|
|
- }
|
|
|
- </FormField>
|
|
|
-
|
|
|
- <FormField name="" label="Authorization URL">
|
|
|
- {() => <TextCopyInput>{`${urlPrefix}/oauth/authorize/`}</TextCopyInput>}
|
|
|
- </FormField>
|
|
|
-
|
|
|
- <FormField name="" label="Token URL">
|
|
|
- {() => <TextCopyInput>{`${urlPrefix}/oauth/token/`}</TextCopyInput>}
|
|
|
- </FormField>
|
|
|
- </PanelBody>
|
|
|
- </Panel>
|
|
|
- </Form>
|
|
|
- </div>
|
|
|
- );
|
|
|
- }
|
|
|
+ >
|
|
|
+ {({value}) =>
|
|
|
+ value ? (
|
|
|
+ <TextCopyInput>
|
|
|
+ {getDynamicText({value, fixed: 'CI_CLIENT_SECRET'})}
|
|
|
+ </TextCopyInput>
|
|
|
+ ) : (
|
|
|
+ <ClientSecret>
|
|
|
+ <HiddenSecret>{t('hidden')}</HiddenSecret>
|
|
|
+ <Confirm
|
|
|
+ onConfirm={rotateClientSecret}
|
|
|
+ message={t(
|
|
|
+ 'Are you sure you want to rotate the client secret? The current one will not be usable anymore, and this cannot be undone.'
|
|
|
+ )}
|
|
|
+ >
|
|
|
+ <Button size="xs" priority="danger">
|
|
|
+ Rotate client secret
|
|
|
+ </Button>
|
|
|
+ </Confirm>
|
|
|
+ </ClientSecret>
|
|
|
+ )
|
|
|
+ }
|
|
|
+ </FormField>
|
|
|
+
|
|
|
+ <FormField name="" label="Authorization URL">
|
|
|
+ {() => <TextCopyInput>{`${urlPrefix}/oauth/authorize/`}</TextCopyInput>}
|
|
|
+ </FormField>
|
|
|
+
|
|
|
+ <FormField name="" label="Token URL">
|
|
|
+ {() => <TextCopyInput>{`${urlPrefix}/oauth/token/`}</TextCopyInput>}
|
|
|
+ </FormField>
|
|
|
+ </PanelBody>
|
|
|
+ </Panel>
|
|
|
+ </Form>
|
|
|
+ </SentryDocumentTitle>
|
|
|
+ );
|
|
|
}
|
|
|
|
|
|
const HiddenSecret = styled('span')`
|