Skip to content

ref(tsc): convert organizationDeveloperSettings to FC #83675

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jan 17, 2025
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 63 additions & 72 deletions static/app/views/settings/organizationDeveloperSettings/index.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import {LocationFixture} from 'sentry-fixture/locationFixture';
import {OrganizationFixture} from 'sentry-fixture/organization';
import {RouterFixture} from 'sentry-fixture/routerFixture';
import {SentryAppFixture} from 'sentry-fixture/sentryApp';

import {initializeOrg} from 'sentry-test/initializeOrg';
@@ -14,7 +16,7 @@ import {
import OrganizationDeveloperSettings from 'sentry/views/settings/organizationDeveloperSettings/index';

describe('Organization Developer Settings', function () {
const {organization: org, routerProps, router} = initializeOrg();
const {organization: org} = initializeOrg();
const sentryApp = SentryAppFixture({
scopes: [
'team:read',
@@ -36,7 +38,7 @@ describe('Organization Developer Settings', function () {
url: `/organizations/${org.slug}/sentry-apps/`,
body: [],
});
render(<OrganizationDeveloperSettings {...routerProps} organization={org} />);
render(<OrganizationDeveloperSettings />);
await waitFor(() => {
expect(
screen.getByText('No internal integrations have been created yet.')
@@ -53,25 +55,21 @@ describe('Organization Developer Settings', function () {
});
});

it('internal integrations list is empty', () => {
render(<OrganizationDeveloperSettings {...routerProps} organization={org} />, {
organization: org,
});
it('internal integrations list is empty', async () => {
render(<OrganizationDeveloperSettings />);
expect(
screen.getByText('No internal integrations have been created yet.')
await screen.findByText('No internal integrations have been created yet.')
).toBeInTheDocument();
});

it('public integrations list contains 1 item', () => {
render(
<OrganizationDeveloperSettings
{...routerProps}
organization={org}
location={{...router.location, query: {type: 'public'}}}
/>,
{organization: org}
);
expect(screen.getByText('Sample App')).toBeInTheDocument();
it('public integrations list contains 1 item', async () => {
const router = RouterFixture({
location: LocationFixture({query: {type: 'public'}}),
});
render(<OrganizationDeveloperSettings />, {
router,
});
expect(await screen.findByText('Sample App')).toBeInTheDocument();
expect(screen.getByText('unpublished')).toBeInTheDocument();
});

@@ -81,13 +79,12 @@ describe('Organization Developer Settings', function () {
method: 'DELETE',
body: [],
});
render(
<OrganizationDeveloperSettings
{...routerProps}
organization={org}
location={{...router.location, query: {type: 'public'}}}
/>
);
const router = RouterFixture({
location: LocationFixture({query: {type: 'public'}}),
});
render(<OrganizationDeveloperSettings />, {
router,
});

const deleteButton = await screen.findByRole('button', {name: 'Delete'});
expect(deleteButton).toHaveAttribute('aria-disabled', 'false');
@@ -111,14 +108,13 @@ describe('Organization Developer Settings', function () {
url: `/sentry-apps/${sentryApp.slug}/publish-request/`,
method: 'POST',
});
const router = RouterFixture({
location: LocationFixture({query: {type: 'public'}}),
});

render(
<OrganizationDeveloperSettings
{...routerProps}
organization={org}
location={{...router.location, query: {type: 'public'}}}
/>
);
render(<OrganizationDeveloperSettings />, {
router,
});

const publishButton = await screen.findByRole('button', {name: 'Publish'});

@@ -173,37 +169,34 @@ describe('Organization Developer Settings', function () {
body: [publishedSentryApp],
});
});
it('shows the published status', () => {
render(
<OrganizationDeveloperSettings
{...routerProps}
organization={org}
location={{...router.location, query: {type: 'public'}}}
/>
);
expect(screen.getByText('published')).toBeInTheDocument();
it('shows the published status', async () => {
const router = RouterFixture({
location: LocationFixture({query: {type: 'public'}}),
});
render(<OrganizationDeveloperSettings />, {
router,
});
expect(await screen.findByText('published')).toBeInTheDocument();
});

it('trash button is disabled', async () => {
render(
<OrganizationDeveloperSettings
{...routerProps}
organization={org}
location={{...router.location, query: {type: 'public'}}}
/>
);
const router = RouterFixture({
location: LocationFixture({query: {type: 'public'}}),
});
render(<OrganizationDeveloperSettings />, {
router,
});
const deleteButton = await screen.findByRole('button', {name: 'Delete'});
expect(deleteButton).toHaveAttribute('aria-disabled', 'true');
});

it('publish button is disabled', async () => {
render(
<OrganizationDeveloperSettings
{...routerProps}
organization={org}
location={{...router.location, query: {type: 'public'}}}
/>
);
const router = RouterFixture({
location: LocationFixture({query: {type: 'public'}}),
});
render(<OrganizationDeveloperSettings />, {
router,
});
const publishButton = await screen.findByRole('button', {name: 'Publish'});
expect(publishButton).toHaveAttribute('aria-disabled', 'true');
});
@@ -220,13 +213,13 @@ describe('Organization Developer Settings', function () {
});

it('allows deleting', async () => {
render(<OrganizationDeveloperSettings {...routerProps} organization={org} />);
render(<OrganizationDeveloperSettings />);
const deleteButton = await screen.findByRole('button', {name: 'Delete'});
expect(deleteButton).toHaveAttribute('aria-disabled', 'false');
});

it('publish button does not exist', () => {
render(<OrganizationDeveloperSettings {...routerProps} organization={org} />);
render(<OrganizationDeveloperSettings />);
expect(screen.queryByText('Publish')).not.toBeInTheDocument();
});
});
@@ -240,27 +233,25 @@ describe('Organization Developer Settings', function () {
});
});
it('trash button is disabled', async () => {
render(
<OrganizationDeveloperSettings
{...routerProps}
organization={newOrg}
location={{...router.location, query: {type: 'public'}}}
/>,
{organization: newOrg}
);
const router = RouterFixture({
location: LocationFixture({query: {type: 'public'}}),
});
render(<OrganizationDeveloperSettings />, {
router,
organization: newOrg,
});
const deleteButton = await screen.findByRole('button', {name: 'Delete'});
expect(deleteButton).toHaveAttribute('aria-disabled', 'true');
});

it('publish button is disabled', async () => {
render(
<OrganizationDeveloperSettings
{...routerProps}
organization={newOrg}
location={{...router.location, query: {type: 'public'}}}
/>,
{organization: newOrg}
);
const router = RouterFixture({
location: LocationFixture({query: {type: 'public'}}),
});
render(<OrganizationDeveloperSettings />, {
organization: newOrg,
router,
});
const publishButton = await screen.findByRole('button', {name: 'Publish'});
expect(publishButton).toHaveAttribute('aria-disabled', 'true');
});
233 changes: 109 additions & 124 deletions static/app/views/settings/organizationDeveloperSettings/index.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import {Fragment} from 'react';
import {Fragment, useState} from 'react';
import styled from '@emotion/styled';

import {removeSentryApp} from 'sentry/actionCreators/sentryApps';
import DeprecatedAsyncComponent from 'sentry/components/deprecatedAsyncComponent';
import EmptyMessage from 'sentry/components/emptyMessage';
import ExternalLink from 'sentry/components/links/externalLink';
import LoadingError from 'sentry/components/loadingError';
import LoadingIndicator from 'sentry/components/loadingIndicator';
import NavTabs from 'sentry/components/navTabs';
import Panel from 'sentry/components/panels/panel';
import PanelBody from 'sentry/components/panels/panelBody';
@@ -13,86 +14,80 @@ import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
import {t, tct} from 'sentry/locale';
import {space} from 'sentry/styles/space';
import type {SentryApp} from 'sentry/types/integrations';
import type {RouteComponentProps} from 'sentry/types/legacyReactRouter';
import type {Organization} from 'sentry/types/organization';
import {
platformEventLinkMap,
PlatformEvents,
} from 'sentry/utils/analytics/integrations/platformAnalyticsEvents';
import {trackIntegrationAnalytics} from 'sentry/utils/integrationUtil';
import withOrganization from 'sentry/utils/withOrganization';
import {useApiQuery} from 'sentry/utils/queryClient';
import useApi from 'sentry/utils/useApi';
import {useLocation} from 'sentry/utils/useLocation';
import useOrganization from 'sentry/utils/useOrganization';
import SettingsPageHeader from 'sentry/views/settings/components/settingsPageHeader';
import SentryApplicationRow from 'sentry/views/settings/organizationDeveloperSettings/sentryApplicationRow';
import CreateIntegrationButton from 'sentry/views/settings/organizationIntegrations/createIntegrationButton';
import ExampleIntegrationButton from 'sentry/views/settings/organizationIntegrations/exampleIntegrationButton';

type Props = Omit<DeprecatedAsyncComponent['props'], 'params'> & {
organization: Organization;
} & RouteComponentProps<{}, {}>;

type Tab = 'public' | 'internal';
type State = DeprecatedAsyncComponent['state'] & {
applications: SentryApp[];
tab: Tab;
};

class OrganizationDeveloperSettings extends DeprecatedAsyncComponent<Props, State> {
analyticsView = 'developer_settings' as const;

getDefaultState(): State {
const {location} = this.props;
const value =
(['public', 'internal'] as const).find(tab => tab === location?.query?.type) ||
'internal';

return {
...super.getDefaultState(),
applications: [],
sentryFunctions: [],
tab: value,
};
}

get tab() {
return this.state.tab;
function OrganizationDeveloperSettings() {
const location = useLocation();
const organization = useOrganization();
const api = useApi({persistInFlight: true});

const value =
['public', 'internal'].find(tab => tab === location?.query?.type) || 'internal';
const analyticsView = 'developer_settings';
const tabs: [id: Tab, label: string][] = [
['internal', t('Internal Integration')],
['public', t('Public Integration')],
];

const [tab, setTab] = useState<Tab>(value as Tab);
const [applicationsState, setApplicationsState] = useState<SentryApp[] | undefined>(
undefined
);

const {
data: fetchedApplications,
isPending,
isError,
refetch,
} = useApiQuery<SentryApp[]>([`/organizations/${organization.slug}/sentry-apps/`], {
staleTime: 0,
});

if (isPending) {
return <LoadingIndicator />;
}

getEndpoints(): ReturnType<DeprecatedAsyncComponent['getEndpoints']> {
const {organization} = this.props;
const returnValue: [string, string, any?, any?][] = [
['applications', `/organizations/${organization.slug}/sentry-apps/`],
];
return returnValue;
if (isError) {
return <LoadingError onRetry={refetch} />;
}

removeApp = (app: SentryApp) => {
const apps = this.state.applications.filter(a => a.slug !== app.slug);
removeSentryApp(this.api, app).then(
() => {
this.setState({applications: apps});
},
const applications = applicationsState ?? fetchedApplications;

const removeApp = (app: SentryApp) => {
const apps = applications.filter(a => a.slug !== app.slug);
removeSentryApp(api, app).then(
() => setApplicationsState(apps),
() => {}
);
};

onTabChange = (value: Tab) => {
this.setState({tab: value});
};

renderApplicationRow = (app: SentryApp) => {
const {organization} = this.props;
const renderApplicationRow = (app: SentryApp) => {
return (
<SentryApplicationRow
key={app.uuid}
app={app}
organization={organization}
onRemoveApp={this.removeApp}
onRemoveApp={removeApp}
/>
);
};

renderInternalIntegrations() {
const integrations = this.state.applications.filter(
const renderInternalIntegrations = () => {
const integrations = applications.filter(
(app: SentryApp) => app.status === 'internal'
);
const isEmpty = integrations.length === 0;
@@ -102,7 +97,7 @@ class OrganizationDeveloperSettings extends DeprecatedAsyncComponent<Props, Stat
<PanelHeader>{t('Internal Integrations')}</PanelHeader>
<PanelBody>
{!isEmpty ? (
integrations.map(this.renderApplicationRow)
integrations.map(renderApplicationRow)
) : (
<EmptyMessage>
{t('No internal integrations have been created yet.')}
@@ -111,18 +106,18 @@ class OrganizationDeveloperSettings extends DeprecatedAsyncComponent<Props, Stat
</PanelBody>
</Panel>
);
}
};

renderPublicIntegrations() {
const integrations = this.state.applications.filter(app => app.status !== 'internal');
const renderPublicIntegrations = () => {
const integrations = applications.filter(app => app.status !== 'internal');
const isEmpty = integrations.length === 0;

return (
<Panel>
<PanelHeader>{t('Public Integrations')}</PanelHeader>
<PanelBody>
{!isEmpty ? (
integrations.map(this.renderApplicationRow)
integrations.map(renderApplicationRow)
) : (
<EmptyMessage>
{t('No public integrations have been created yet.')}
@@ -131,82 +126,72 @@ class OrganizationDeveloperSettings extends DeprecatedAsyncComponent<Props, Stat
</PanelBody>
</Panel>
);
}
};

renderTabContent(tab: Tab) {
const renderTabContent = () => {
switch (tab) {
case 'internal':
return this.renderInternalIntegrations();
return renderInternalIntegrations();
case 'public':
default:
return this.renderPublicIntegrations();
return renderPublicIntegrations();
}
}
renderBody() {
const {organization} = this.props;
const tabs: [id: Tab, label: string][] = [
['internal', t('Internal Integration')],
['public', t('Public Integration')],
];
};

return (
<div>
<SentryDocumentTitle
title={t('Custom Integrations')}
orgSlug={organization.slug}
/>
<SettingsPageHeader
title={t('Custom Integrations')}
body={
<Fragment>
{t(
'Create integrations that interact with Sentry using the REST API and webhooks. '
)}
<br />
{tct('For more information [link: see our docs].', {
link: (
<ExternalLink
href={platformEventLinkMap[PlatformEvents.DOCS]}
onClick={() => {
trackIntegrationAnalytics(PlatformEvents.DOCS, {
organization,
view: this.analyticsView,
});
}}
/>
),
})}
</Fragment>
}
action={
<ActionContainer>
<ExampleIntegrationButton
analyticsView={this.analyticsView}
style={{marginRight: space(1)}}
/>
<CreateIntegrationButton analyticsView={this.analyticsView} />
</ActionContainer>
}
/>
<NavTabs underlined>
{tabs.map(([type, label]) => (
<li
key={type}
className={this.tab === type ? 'active' : ''}
onClick={() => this.onTabChange(type)}
>
<a>{label}</a>
</li>
))}
</NavTabs>
{this.renderTabContent(this.tab)}
</div>
);
}
return (
<div>
<SentryDocumentTitle title={t('Custom Integrations')} orgSlug={organization.slug} />
<SettingsPageHeader
title={t('Custom Integrations')}
body={
<Fragment>
{t(
'Create integrations that interact with Sentry using the REST API and webhooks. '
)}
<br />
{tct('For more information [link: see our docs].', {
link: (
<ExternalLink
href={platformEventLinkMap[PlatformEvents.DOCS]}
onClick={() => {
trackIntegrationAnalytics(PlatformEvents.DOCS, {
organization,
view: analyticsView,
});
}}
/>
),
})}
</Fragment>
}
action={
<ActionContainer>
<ExampleIntegrationButton
analyticsView={analyticsView}
style={{marginRight: space(1)}}
/>
<CreateIntegrationButton analyticsView={analyticsView} />
</ActionContainer>
}
/>
<NavTabs underlined>
{tabs.map(([type, label]) => (
<li
key={type}
className={tab === type ? 'active' : ''}
onClick={() => setTab(type)}
>
<a>{label}</a>
</li>
))}
</NavTabs>
{renderTabContent()}
</div>
);
}

const ActionContainer = styled('div')`
display: flex;
`;

export default withOrganization(OrganizationDeveloperSettings);
export default OrganizationDeveloperSettings;