Skip to content

Commit ec7662b

Browse files
authored
feat(issues): Convert ApiKeys to FC (#83151)
1 parent e3690c1 commit ec7662b

File tree

6 files changed

+182
-230
lines changed

6 files changed

+182
-230
lines changed

static/app/views/settings/organizationApiKeys/index.spec.tsx

Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,15 @@
11
import {DeprecatedApiKeyFixture} from 'sentry-fixture/deprecatedApiKey';
22

3-
import {initializeOrg} from 'sentry-test/initializeOrg';
43
import {
54
render,
65
renderGlobalModal,
76
screen,
87
userEvent,
98
} from 'sentry-test/reactTestingLibrary';
109

11-
import type {RouteWithName} from 'sentry/views/settings/components/settingsBreadcrumb/types';
1210
import OrganizationApiKeys from 'sentry/views/settings/organizationApiKeys';
1311

14-
const routes: RouteWithName[] = [
15-
{path: '/'},
16-
{path: '/:orgId/'},
17-
{path: '/organizations/:orgId/'},
18-
{path: 'api-keys/', name: 'API Key'},
19-
];
20-
2112
describe('OrganizationApiKeys', function () {
22-
const {routerProps} = initializeOrg();
2313
let getMock: jest.Mock;
2414
let deleteMock: jest.Mock;
2515

@@ -41,20 +31,20 @@ describe('OrganizationApiKeys', function () {
4131
});
4232
});
4333

44-
it('fetches api keys', function () {
45-
render(<OrganizationApiKeys {...routerProps} routes={routes} />);
34+
it('fetches api keys', async function () {
35+
render(<OrganizationApiKeys />);
4636

47-
expect(screen.getByRole('textbox')).toBeInTheDocument();
37+
expect(await screen.findByRole('textbox')).toBeInTheDocument();
4838
expect(getMock).toHaveBeenCalledTimes(1);
4939
});
5040

5141
it('can delete a key', async function () {
52-
render(<OrganizationApiKeys {...routerProps} routes={routes} />);
42+
render(<OrganizationApiKeys />);
43+
renderGlobalModal();
5344

45+
await userEvent.click(await screen.findByRole('link', {name: 'Remove API Key?'}));
5446
expect(deleteMock).toHaveBeenCalledTimes(0);
55-
await userEvent.click(screen.getByRole('link', {name: 'Remove API Key?'}));
5647

57-
renderGlobalModal();
5848
await userEvent.click(screen.getByTestId('confirm-button'));
5949

6050
expect(deleteMock).toHaveBeenCalledTimes(1);
Lines changed: 83 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -1,107 +1,108 @@
1-
import {addErrorMessage, addSuccessMessage} from 'sentry/actionCreators/indicator';
2-
import DeprecatedAsyncComponent from 'sentry/components/deprecatedAsyncComponent';
1+
import {
2+
addErrorMessage,
3+
addLoadingMessage,
4+
addSuccessMessage,
5+
} from 'sentry/actionCreators/indicator';
6+
import LoadingError from 'sentry/components/loadingError';
37
import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
48
import {t} from 'sentry/locale';
5-
import type {RouteComponentProps} from 'sentry/types/legacyReactRouter';
6-
import type {Organization} from 'sentry/types/organization';
7-
import {browserHistory} from 'sentry/utils/browserHistory';
8-
import recreateRoute from 'sentry/utils/recreateRoute';
9-
import withOrganization from 'sentry/utils/withOrganization';
9+
import {
10+
setApiQueryData,
11+
useApiQuery,
12+
useMutation,
13+
useQueryClient,
14+
} from 'sentry/utils/queryClient';
15+
import useApi from 'sentry/utils/useApi';
16+
import {useNavigate} from 'sentry/utils/useNavigate';
17+
import useOrganization from 'sentry/utils/useOrganization';
1018

1119
import OrganizationApiKeysList from './organizationApiKeysList';
1220
import type {DeprecatedApiKey} from './types';
1321

14-
type Props = RouteComponentProps<{}, {}> & {
15-
organization: Organization;
16-
};
17-
18-
type State = {
19-
keys: DeprecatedApiKey[];
20-
} & DeprecatedAsyncComponent['state'];
21-
2222
/**
2323
* API Keys are deprecated, but there may be some legacy customers that still use it
2424
*/
25-
class OrganizationApiKeys extends DeprecatedAsyncComponent<Props, State> {
26-
getEndpoints(): ReturnType<DeprecatedAsyncComponent['getEndpoints']> {
27-
const {organization} = this.props;
28-
return [['keys', `/organizations/${organization.slug}/api-keys/`]];
29-
}
30-
31-
handleRemove = async (id: string) => {
32-
const {organization} = this.props;
33-
const oldKeys = [...this.state.keys];
25+
function OrganizationApiKeys() {
26+
const api = useApi();
27+
const organization = useOrganization();
28+
const navigate = useNavigate();
29+
const queryClient = useQueryClient();
30+
const {
31+
data: apiKeys = [],
32+
isPending,
33+
isError,
34+
refetch,
35+
} = useApiQuery<DeprecatedApiKey[]>([`/organizations/${organization.slug}/api-keys/`], {
36+
staleTime: 0,
37+
});
3438

35-
this.setState(state => ({
36-
keys: state.keys.filter(({id: existingId}) => existingId !== id),
37-
}));
38-
39-
try {
40-
await this.api.requestPromise(
41-
`/organizations/${organization.slug}/api-keys/${id}/`,
39+
const removeMutation = useMutation({
40+
mutationFn: ({removedId}: {removedId: string}) => {
41+
return api.requestPromise(
42+
`/organizations/${organization.slug}/api-keys/${removedId}/`,
4243
{
4344
method: 'DELETE',
4445
data: {},
4546
}
4647
);
47-
} catch {
48-
this.setState({keys: oldKeys, busy: false});
49-
addErrorMessage(t('Error removing key'));
50-
}
51-
};
52-
53-
handleAddApiKey = async () => {
54-
this.setState({
55-
busy: true,
56-
});
57-
const {organization} = this.props;
48+
},
49+
onMutate: () => {
50+
addLoadingMessage(t('Removing API key'));
51+
},
52+
onSuccess: (_data, {removedId}) => {
53+
setApiQueryData<DeprecatedApiKey[]>(
54+
queryClient,
55+
[`/organizations/${organization.slug}/api-keys/`],
56+
oldData => {
57+
if (!oldData) {
58+
return oldData;
59+
}
5860

59-
try {
60-
const data = await this.api.requestPromise(
61-
`/organizations/${organization.slug}/api-keys/`,
62-
{
63-
method: 'POST',
64-
data: {},
61+
return oldData.filter(({id}) => id !== removedId);
6562
}
6663
);
64+
},
65+
onError: () => {
66+
addErrorMessage(t('Error removing key'));
67+
},
68+
});
6769

68-
if (data) {
69-
this.setState({busy: false});
70-
browserHistory.push(
71-
recreateRoute(`${data.id}/`, {
72-
params: {orgId: organization.slug},
73-
routes: this.props.routes,
74-
})
75-
);
76-
addSuccessMessage(t('Created a new API key "%s"', data.label));
70+
const addMutation = useMutation({
71+
mutationFn: (): Promise<DeprecatedApiKey> => {
72+
return api.requestPromise(`/organizations/${organization.slug}/api-keys/`, {
73+
method: 'POST',
74+
data: {},
75+
});
76+
},
77+
onSuccess: data => {
78+
if (!data) {
79+
return;
7780
}
78-
} catch {
79-
this.setState({busy: false});
80-
}
81-
};
8281

83-
renderLoading() {
84-
return this.renderBody();
85-
}
86-
87-
renderBody() {
88-
const {organization} = this.props;
89-
const params = {orgId: organization.slug};
82+
navigate(`/settings/${organization.slug}/api-keys/${data.id}/`);
83+
addSuccessMessage(t('Created a new API key "%s"', data.label));
84+
},
85+
onError: () => {
86+
addErrorMessage(t('Error creating key'));
87+
},
88+
});
9089

91-
return (
92-
<SentryDocumentTitle title={t('Api Keys')} orgSlug={organization.slug}>
93-
<OrganizationApiKeysList
94-
{...this.props}
95-
params={params}
96-
loading={this.state.loading}
97-
busy={this.state.busy}
98-
keys={this.state.keys}
99-
onRemove={this.handleRemove}
100-
onAddApiKey={this.handleAddApiKey}
101-
/>
102-
</SentryDocumentTitle>
103-
);
90+
if (isError) {
91+
return <LoadingError onRetry={refetch} />;
10492
}
93+
94+
return (
95+
<SentryDocumentTitle title={t('Api Keys')} orgSlug={organization.slug}>
96+
<OrganizationApiKeysList
97+
organization={organization}
98+
loading={isPending}
99+
busy={addMutation.isPending}
100+
keys={apiKeys}
101+
onRemove={id => removeMutation.mutateAsync({removedId: id})}
102+
onAddApiKey={addMutation.mutateAsync}
103+
/>
104+
</SentryDocumentTitle>
105+
);
105106
}
106107

107-
export default withOrganization(OrganizationApiKeys);
108+
export default OrganizationApiKeys;
Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,30 @@
11
import {DeprecatedApiKeyFixture} from 'sentry-fixture/deprecatedApiKey';
2+
import {RouterFixture} from 'sentry-fixture/routerFixture';
23

3-
import {initializeOrg} from 'sentry-test/initializeOrg';
44
import {render, screen} from 'sentry-test/reactTestingLibrary';
55

66
import OrganizationApiKeyDetails from 'sentry/views/settings/organizationApiKeys/organizationApiKeyDetails';
77

88
describe('OrganizationApiKeyDetails', function () {
9+
const apiKey = DeprecatedApiKeyFixture();
10+
const router = RouterFixture({
11+
params: {
12+
apiKey: apiKey.id,
13+
},
14+
});
915
beforeEach(function () {
1016
MockApiClient.clearMockResponses();
1117
MockApiClient.addMockResponse({
12-
url: '/organizations/org-slug/api-keys/1/',
18+
url: `/organizations/org-slug/api-keys/${apiKey.id}/`,
1319
method: 'GET',
14-
body: DeprecatedApiKeyFixture(),
20+
body: apiKey,
1521
});
1622
});
1723

18-
it('renders', function () {
19-
const {organization, router, routerProps} = initializeOrg();
20-
render(<OrganizationApiKeyDetails {...routerProps} params={{apiKey: '1'}} />, {
21-
router,
22-
organization,
23-
});
24+
it('renders', async function () {
25+
render(<OrganizationApiKeyDetails />, {router});
2426

25-
expect(screen.queryByTestId('loading-indicator')).not.toBeInTheDocument();
27+
expect(await screen.findByRole('textbox', {name: 'API Key'})).toBeInTheDocument();
28+
expect(screen.getByRole('textbox', {name: 'API Key'})).toHaveValue(apiKey.key);
2629
});
2730
});

0 commit comments

Comments
 (0)