loginForm.tsx 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157
  1. import {useState} from 'react';
  2. import styled from '@emotion/styled';
  3. import {Alert} from 'sentry/components/alert';
  4. import {Button} from 'sentry/components/button';
  5. import SecretField from 'sentry/components/forms/fields/secretField';
  6. import TextField from 'sentry/components/forms/fields/textField';
  7. import Form from 'sentry/components/forms/form';
  8. import Link from 'sentry/components/links/link';
  9. import {IconGithub, IconGoogle, IconVsts} from 'sentry/icons';
  10. import {t} from 'sentry/locale';
  11. import ConfigStore from 'sentry/stores/configStore';
  12. import {space} from 'sentry/styles/space';
  13. import type {AuthConfig} from 'sentry/types';
  14. import {browserHistory} from 'sentry/utils/browserHistory';
  15. type LoginProvidersProps = Partial<
  16. Pick<AuthConfig, 'vstsLoginLink' | 'githubLoginLink' | 'googleLoginLink'>
  17. >;
  18. // TODO(epurkhiser): The abstraction here would be much nicer if we just
  19. // exposed a configuration object telling us what auth providers there are.
  20. function LoginProviders({
  21. vstsLoginLink,
  22. githubLoginLink,
  23. googleLoginLink,
  24. }: LoginProvidersProps) {
  25. return (
  26. <ProviderWrapper>
  27. <ProviderHeading>{t('External Account Login')}</ProviderHeading>
  28. {googleLoginLink && (
  29. <Button size="sm" icon={<IconGoogle />} href={googleLoginLink}>
  30. {t('Sign in with Google')}
  31. </Button>
  32. )}
  33. {githubLoginLink && (
  34. <Button size="sm" icon={<IconGithub />} href={githubLoginLink}>
  35. {t('Sign in with GitHub')}
  36. </Button>
  37. )}
  38. {vstsLoginLink && (
  39. <Button size="sm" icon={<IconVsts />} href={vstsLoginLink}>
  40. {t('Sign in with Azure DevOps')}
  41. </Button>
  42. )}
  43. </ProviderWrapper>
  44. );
  45. }
  46. type Props = {
  47. authConfig: AuthConfig;
  48. };
  49. function LoginForm({authConfig}: Props) {
  50. const [error, setError] = useState('');
  51. const {githubLoginLink, vstsLoginLink} = authConfig;
  52. const hasLoginProvider = !!(githubLoginLink || vstsLoginLink);
  53. return (
  54. <FormWrapper hasLoginProvider={hasLoginProvider}>
  55. <Form
  56. submitLabel={t('Continue')}
  57. apiEndpoint="/auth/login/"
  58. apiMethod="POST"
  59. onSubmitSuccess={response => {
  60. // TODO(epurkhiser): There is likely more that needs to happen to update
  61. // the application state after user login.
  62. ConfigStore.set('user', response.user);
  63. // TODO(epurkhiser): Reconfigure sentry SDK identity
  64. browserHistory.push({pathname: response.nextUri});
  65. }}
  66. onSubmitError={response => {
  67. // TODO(epurkhiser): Need much better error handling here
  68. setError(response.responseJSON.errors.__all__);
  69. }}
  70. footerStyle={{
  71. borderTop: 'none',
  72. alignItems: 'center',
  73. marginBottom: 0,
  74. padding: 0,
  75. }}
  76. extraButton={
  77. <LostPasswordLink to="/account/recover/">
  78. {t('Lost your password?')}
  79. </LostPasswordLink>
  80. }
  81. >
  82. {error && <Alert type="error">{error}</Alert>}
  83. <TextField
  84. name="username"
  85. placeholder={t('username or email')}
  86. label={t('Account')}
  87. stacked
  88. inline={false}
  89. hideControlState
  90. required
  91. />
  92. <SecretField
  93. name="password"
  94. placeholder={t('password')}
  95. label={t('Password')}
  96. stacked
  97. inline={false}
  98. hideControlState
  99. required
  100. />
  101. </Form>
  102. {hasLoginProvider && <LoginProviders {...{vstsLoginLink, githubLoginLink}} />}
  103. </FormWrapper>
  104. );
  105. }
  106. const FormWrapper = styled('div')<{hasLoginProvider: boolean}>`
  107. display: grid;
  108. gap: 60px;
  109. grid-template-columns: ${p => (p.hasLoginProvider ? '1fr 0.8fr' : '1fr')};
  110. `;
  111. const ProviderHeading = styled('div')`
  112. margin: 0;
  113. font-size: 15px;
  114. font-weight: ${p => p.theme.fontWeightBold};
  115. line-height: 24px;
  116. `;
  117. const ProviderWrapper = styled('div')`
  118. position: relative;
  119. display: grid;
  120. grid-auto-rows: max-content;
  121. gap: ${space(1.5)};
  122. &:before {
  123. position: absolute;
  124. display: block;
  125. content: '';
  126. top: 0;
  127. bottom: 0;
  128. left: -30px;
  129. border-left: 1px solid ${p => p.theme.border};
  130. }
  131. `;
  132. const LostPasswordLink = styled(Link)`
  133. color: ${p => p.theme.gray300};
  134. font-size: ${p => p.theme.fontSizeMedium};
  135. &:hover {
  136. color: ${p => p.theme.textColor};
  137. }
  138. `;
  139. export default LoginForm;