Skip to content
Merged
Show file tree
Hide file tree
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
3 changes: 1 addition & 2 deletions static/app/router/routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -135,18 +135,17 @@ function buildRoutes(): RouteObject[] {
{
index: true,
component: make(() => import('sentry/views/auth/login')),
deprecatedRouteProps: true,
},
{
path: ':orgId/',
component: make(() => import('sentry/views/auth/login')),
deprecatedRouteProps: true,
},
];
const experimentalSpaRoutes: SentryRouteObject = EXPERIMENTAL_SPA
? {
path: '/auth/login/',
component: errorHandler(AuthLayout),
deprecatedRouteProps: true,
children: experimentalSpaChildRoutes,
}
: {};
Expand Down
2 changes: 1 addition & 1 deletion static/app/views/auth/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {AppBodyContent} from 'sentry/views/app/appBodyContent';

const BODY_CLASSES = ['narrow'];

function Layout({children}: any) {
function Layout({children}: {children: React.ReactNode}) {
useEffect(() => {
document.body.classList.add(...BODY_CLASSES);
return () => document.body.classList.remove(...BODY_CLASSES);
Expand Down
13 changes: 5 additions & 8 deletions static/app/views/auth/login.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
import {initializeOrg} from 'sentry-test/initializeOrg';
import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary';

import Login from 'sentry/views/auth/login';

describe('Login', () => {
const {routerProps} = initializeOrg();

afterEach(() => {
MockApiClient.clearMockResponses();
});
Expand All @@ -16,7 +13,7 @@ describe('Login', () => {
body: {},
});

render(<Login {...routerProps} />);
render(<Login />);

expect(screen.getByTestId('loading-indicator')).toBeInTheDocument();
expect(await screen.findByText('Lost your password?')).toBeInTheDocument();
Expand All @@ -28,7 +25,7 @@ describe('Login', () => {
statusCode: 500,
});

render(<Login {...routerProps} />);
render(<Login />);

expect(
await screen.findByText('Unable to load authentication configuration')
Expand All @@ -41,7 +38,7 @@ describe('Login', () => {
body: {canRegister: false},
});

render(<Login {...routerProps} />);
render(<Login />);

expect(await screen.findByText('Lost your password?')).toBeInTheDocument();
expect(screen.queryByRole('tab', {name: 'Register'})).not.toBeInTheDocument();
Expand All @@ -53,7 +50,7 @@ describe('Login', () => {
body: {canRegister: true},
});

render(<Login {...routerProps} />);
render(<Login />);

expect(await screen.findByRole('tab', {name: 'Register'})).toBeInTheDocument();
});
Expand All @@ -64,7 +61,7 @@ describe('Login', () => {
body: {canRegister: true},
});

render(<Login {...routerProps} />);
render(<Login />);

// Default tab is login
expect(await screen.findByPlaceholderText('username or email')).toBeInTheDocument();
Expand Down
227 changes: 104 additions & 123 deletions static/app/views/auth/login.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import {Component, Fragment} from 'react';
import {Fragment, useMemo, useState} from 'react';
import styled from '@emotion/styled';

import type {Client} from 'sentry/api';
import {Alert} from 'sentry/components/core/alert';
import {LinkButton} from 'sentry/components/core/button/linkButton';
import {TabList, Tabs} from 'sentry/components/core/tabs';
Expand All @@ -10,8 +9,8 @@ import LoadingIndicator from 'sentry/components/loadingIndicator';
import {t, tct} from 'sentry/locale';
import {space} from 'sentry/styles/space';
import type {AuthConfig} from 'sentry/types/auth';
import type {RouteComponentProps} from 'sentry/types/legacyReactRouter';
import withApi from 'sentry/utils/withApi';
import {useApiQuery} from 'sentry/utils/queryClient';
import {useParams} from 'sentry/utils/useParams';

import LoginForm from './loginForm';
import RegisterForm from './registerForm';
Expand All @@ -27,128 +26,110 @@ type ActiveTab = keyof typeof FORM_COMPONENTS;

type TabConfig = [key: ActiveTab, label: string, disabled?: boolean];

type Props = RouteComponentProps<{orgId?: string}> & {
api: Client;
type AuthConfigResponse = Omit<
AuthConfig,
'vstsLoginLink' | 'githubLoginLink' | 'googleLoginLink'
> & {
github_login_link?: string;
google_login_link?: string;
vsts_login_link?: string;
};

type State = {
activeTab: ActiveTab;
authConfig: null | AuthConfig;
error: null | boolean;
loading: boolean;
};

class Login extends Component<Props, State> {
state: State = {
loading: true,
error: null,
activeTab: 'login',
authConfig: null,
};

componentDidMount() {
this.fetchData();
}

fetchData = async () => {
const {api} = this.props;
try {
const response = await api.requestPromise('/auth/config/');

const {vsts_login_link, github_login_link, google_login_link, ...config} = response;
const authConfig = {
...config,
vstsLoginLink: vsts_login_link,
githubLoginLink: github_login_link,
googleLoginLink: google_login_link,
};

this.setState({authConfig});
} catch (e) {
this.setState({error: true});
}

this.setState({loading: false});
};

get hasAuthProviders() {
if (this.state.authConfig === null) {
return false;
function Login() {
const {orgId} = useParams<{orgId?: string}>();
const [activeTab, setActiveTab] = useState<ActiveTab>('login');

const {
data: response,
isPending,
isError,
refetch,
} = useApiQuery<AuthConfigResponse>(['/auth/config/'], {
staleTime: 0,
});

const authConfig = useMemo((): AuthConfig | null => {
if (!response) {
return null;
}

const {githubLoginLink, googleLoginLink, vstsLoginLink} = this.state.authConfig;
return !!(githubLoginLink || vstsLoginLink || googleLoginLink);
}

render() {
const {loading, error, activeTab, authConfig} = this.state;
const {orgId} = this.props.params;

const FormComponent = FORM_COMPONENTS[activeTab];

const tabs: TabConfig[] = [
['login', t('Login')],
['sso', t('Single Sign-On')],
['register', t('Register'), !authConfig?.canRegister],
];

return (
<Fragment>
<Header>
<Heading>{t('Sign in to continue')}</Heading>
<TabsContainer>
<Tabs
value={activeTab}
onChange={tab => {
this.setState({activeTab: tab});
}}
>
<TabList>
{tabs
.map(([key, label, disabled]) => {
if (disabled) {
return null;
}
return <TabList.Item key={key}>{label}</TabList.Item>;
})
.filter(n => !!n)}
</TabList>
</Tabs>
</TabsContainer>
</Header>
{loading && <LoadingIndicator />}

{error && (
<StyledLoadingError
message={t('Unable to load authentication configuration')}
onRetry={this.fetchData}
/>
)}
{!loading && authConfig !== null && !error && (
<FormWrapper hasAuthProviders={this.hasAuthProviders}>
{orgId !== undefined && (
<Alert.Container>
<Alert
type="warning"
trailingItems={
<LinkButton to="/" size="xs">
Reload
</LinkButton>
const {vsts_login_link, github_login_link, google_login_link, ...config} = response;
return {
...config,
vstsLoginLink: vsts_login_link ?? '',
githubLoginLink: github_login_link ?? '',
googleLoginLink: google_login_link ?? '',
};
}, [response]);

const hasAuthProviders =
!!authConfig?.githubLoginLink ||
!!authConfig?.vstsLoginLink ||
!!authConfig?.googleLoginLink;

const FormComponent = FORM_COMPONENTS[activeTab];

const tabs: TabConfig[] = [
['login', t('Login')],
['sso', t('Single Sign-On')],
['register', t('Register'), !authConfig?.canRegister],
];

return (
<Fragment>
<Header>
<Heading>{t('Sign in to continue')}</Heading>
<TabsContainer>
<Tabs
value={activeTab}
onChange={tab => {
setActiveTab(tab);
}}
>
<TabList>
{tabs
.map(([key, label, disabled]) => {
if (disabled) {
return null;
}
>
{tct(
"Experimental SPA mode does not currently support SSO style login. To develop against the [org] you'll need to copy your production session cookie.",
{org: this.props.params.orgId}
)}
</Alert>
</Alert.Container>
)}
<FormComponent {...{authConfig}} />
</FormWrapper>
)}
</Fragment>
);
}
return <TabList.Item key={key}>{label}</TabList.Item>;
})
.filter(n => !!n)}
</TabList>
</Tabs>
</TabsContainer>
</Header>
{isPending && <LoadingIndicator />}

{isError && (
<StyledLoadingError
message={t('Unable to load authentication configuration')}
onRetry={refetch}
/>
)}
{!isPending && authConfig !== null && !isError && (
<FormWrapper hasAuthProviders={hasAuthProviders}>
{orgId !== undefined && (
<Alert.Container>
<Alert
type="warning"
trailingItems={
<LinkButton to="/" size="xs">
Reload
</LinkButton>
}
>
{tct(
"Experimental SPA mode does not currently support SSO style login. To develop against the [org] you'll need to copy your production session cookie.",
{org: orgId}
)}
</Alert>
</Alert.Container>
)}
<FormComponent {...{authConfig}} />
</FormWrapper>
)}
</Fragment>
);
}

const StyledLoadingError = styled(LoadingError)`
Expand All @@ -174,4 +155,4 @@ const FormWrapper = styled('div')<{hasAuthProviders: boolean}>`
width: ${p => (p.hasAuthProviders ? '600px' : '490px')};
`;

export default withApi(Login);
export default Login;
Loading