Просмотр исходного кода

ref(ui): Remove `grid-emotion` from Account Security section (#15430)

Remove `grid-emotion` and fix ups ome CSS
Billy Vong 5 лет назад
Родитель
Сommit
7c92c4fa2b

+ 27 - 0
docs-ui/components/u2fEnrolledDetails.stories.js

@@ -0,0 +1,27 @@
+import React from 'react';
+
+import {action} from '@storybook/addon-actions';
+import {boolean} from '@storybook/addon-knobs';
+import {storiesOf} from '@storybook/react';
+import {withInfo} from '@storybook/addon-info';
+
+import U2fEnrolledDetails from 'app/views/settings/account/accountSecurity/components/u2fEnrolledDetails';
+
+storiesOf('Other|U2fEnrolledDetails', module).add(
+  'U2fEnrolledDetails',
+  withInfo('U2f details after enrollment', {
+    propTablesExclude: ['Button'],
+  })(() => (
+    <U2fEnrolledDetails
+      isEnrolled={boolean('Is Enrolled', true)}
+      id="u2f"
+      devices={[
+        {
+          name: 'Device 1',
+          timestamp: +new Date(),
+        },
+      ]}
+      onRemoveU2fDevice={action('On Remove Device')}
+    />
+  ))
+);

+ 39 - 30
src/sentry/static/sentry/app/views/settings/account/accountSecurity/accountSecurityDetails.jsx

@@ -4,8 +4,6 @@
  *
  * Also displays 2fa method specific details.
  */
-import {Box, Flex} from 'grid-emotion';
-import {withRouter} from 'react-router';
 import PropTypes from 'prop-types';
 import React from 'react';
 import styled from 'react-emotion';
@@ -22,20 +20,10 @@ import SettingsPageHeader from 'app/views/settings/components/settingsPageHeader
 import TextBlock from 'app/views/settings/components/text/textBlock';
 import Tooltip from 'app/components/tooltip';
 import U2fEnrolledDetails from 'app/views/settings/account/accountSecurity/components/u2fEnrolledDetails';
+import space from 'app/styles/space';
 
 const ENDPOINT = '/users/me/authenticators/';
 
-const DateLabel = styled('span')`
-  font-weight: bold;
-  margin-right: 6px;
-  width: 100px;
-`;
-
-const Phone = styled('span')`
-  font-weight: bold;
-  margin-left: 6px;
-`;
-
 class AuthenticatorDate extends React.Component {
   static propTypes = {
     label: PropTypes.string,
@@ -45,14 +33,15 @@ class AuthenticatorDate extends React.Component {
      */
     date: PropTypes.string,
   };
+
   render() {
     const {label, date} = this.props;
 
     return (
-      <Flex mb={1}>
+      <React.Fragment>
         <DateLabel>{label}</DateLabel>
-        <Box flex="1">{date ? <DateTime date={date} /> : t('never')}</Box>
-      </Flex>
+        <div>{date ? <DateTime date={date} /> : t('never')}</div>
+      </React.Fragment>
     );
   }
 }
@@ -62,10 +51,8 @@ class AccountSecurityDetails extends AsyncView {
     deleteDisabled: PropTypes.bool.isRequired,
     onRegenerateBackupCodes: PropTypes.func.isRequired,
   };
-  constructor(...args) {
-    super(...args);
-    this._form = {};
-  }
+
+  _form = {};
 
   getTitle() {
     return t('Security');
@@ -113,10 +100,6 @@ class AccountSecurityDetails extends AsyncView {
     );
   };
 
-  handleRemoveU2fDevice = () => {
-    // TODO(billy): Implement me
-  };
-
   renderBody() {
     const {authenticator} = this.state;
     const {deleteDisabled, onRegenerateBackupCodes} = this.props;
@@ -127,7 +110,7 @@ class AccountSecurityDetails extends AsyncView {
           title={
             <React.Fragment>
               <span>{authenticator.name}</span>
-              <CircleIndicator css={{marginLeft: 6}} enabled={authenticator.isEnrolled} />
+              <AuthenticatorStatus enabled={authenticator.isEnrolled} />
             </React.Fragment>
           }
           action={
@@ -148,8 +131,11 @@ class AccountSecurityDetails extends AsyncView {
         />
 
         <TextBlock>{authenticator.description}</TextBlock>
-        <AuthenticatorDate label={t('Created at')} date={authenticator.createdAt} />
-        <AuthenticatorDate label={t('Last used')} date={authenticator.lastUsedAt} />
+
+        <AuthenticatorDates>
+          <AuthenticatorDate label={t('Created at')} date={authenticator.createdAt} />
+          <AuthenticatorDate label={t('Last used')} date={authenticator.lastUsedAt} />
+        </AuthenticatorDates>
 
         <U2fEnrolledDetails
           isEnrolled={authenticator.isEnrolled}
@@ -159,10 +145,10 @@ class AccountSecurityDetails extends AsyncView {
         />
 
         {authenticator.isEnrolled && authenticator.phone && (
-          <div css={{marginTop: 30}}>
+          <PhoneWrapper>
             {t('Confirmation codes are sent to the following phone number')}:
             <Phone>{authenticator.phone}</Phone>
-          </div>
+          </PhoneWrapper>
         )}
 
         <RecoveryCodes
@@ -175,4 +161,27 @@ class AccountSecurityDetails extends AsyncView {
   }
 }
 
-export default withRouter(AccountSecurityDetails);
+export default AccountSecurityDetails;
+
+const AuthenticatorStatus = styled(CircleIndicator)`
+  margin-left: ${space(1)};
+`;
+
+const AuthenticatorDates = styled('div')`
+  display: grid;
+  grid-gap: ${space(2)};
+  grid-template-columns: max-content auto;
+`;
+
+const DateLabel = styled('span')`
+  font-weight: bold;
+`;
+
+const PhoneWrapper = styled('div')`
+  margin-top: ${space(4)};
+`;
+
+const Phone = styled('span')`
+  font-weight: bold;
+  margin-left: ${space(1)};
+`;

+ 56 - 39
src/sentry/static/sentry/app/views/settings/account/accountSecurity/accountSecuritySessionHistory.jsx

@@ -1,16 +1,16 @@
-import {Flex, Box} from 'grid-emotion';
-import {withRouter} from 'react-router';
 import PropTypes from 'prop-types';
 import React from 'react';
+import styled from 'react-emotion';
 
+import {Panel, PanelBody, PanelHeader, PanelItem} from 'app/components/panels';
 import {t} from 'app/locale';
 import AsyncView from 'app/views/asyncView';
 import ListLink from 'app/components/links/listLink';
 import NavTabs from 'app/components/navTabs';
 import SettingsPageHeader from 'app/views/settings/components/settingsPageHeader';
-import {Panel, PanelBody, PanelHeader, PanelItem} from 'app/components/panels';
 import TimeSince from 'app/components/timeSince';
 import recreateRoute from 'app/utils/recreateRoute';
+import space from 'app/styles/space';
 
 class SessionRow extends React.Component {
   static propTypes = {
@@ -25,36 +25,42 @@ class SessionRow extends React.Component {
     const {ipAddress, countryCode, regionCode, lastSeen, firstSeen} = this.props;
 
     return (
-      <PanelItem justify="space-between">
-        <Flex align="center" flex={1}>
-          <Box flex="1">
-            <div style={{marginBottom: 5}}>
-              <strong>{ipAddress}</strong>
-            </div>
+      <SessionPanelItem>
+        <IpAndLocation>
+          <div>
+            <IpAddress>{ipAddress}</IpAddress>
             {countryCode && regionCode && (
-              <div>
-                <small>
-                  {countryCode} ({regionCode})
-                </small>
-              </div>
+              <CountryCode>
+                {countryCode} ({regionCode})
+              </CountryCode>
             )}
-          </Box>
-        </Flex>
-        <Flex align="center" w={140} mx={2}>
-          <small>
-            <TimeSince date={firstSeen} />
-          </small>
-        </Flex>
-        <Flex align="center" w={140} mx={2}>
-          <small>
-            <TimeSince date={lastSeen} />
-          </small>
-        </Flex>
-      </PanelItem>
+          </div>
+        </IpAndLocation>
+        <StyledTimeSince date={firstSeen} />
+        <StyledTimeSince date={lastSeen} />
+      </SessionPanelItem>
     );
   }
 }
 
+const IpAddress = styled('div')`
+  margin-bottom: ${space(0.5)};
+  font-weight: bold;
+`;
+const CountryCode = styled('div')`
+  font-size: ${p => p.theme.fontSizeRelativeSmall};
+`;
+
+const StyledTimeSince = styled(TimeSince)`
+  font-size: ${p => p.theme.fontSizeRelativeSmall};
+`;
+
+const IpAndLocation = styled('div')`
+  display: flex;
+  flex-direction: column;
+  flex: 1;
+`;
+
 class AccountSecuritySessionHistory extends AsyncView {
   getTitle() {
     return t('Session History');
@@ -84,17 +90,12 @@ class AccountSecuritySessionHistory extends AsyncView {
         />
 
         <Panel>
-          <PanelHeader>
-            <Flex align="center" flex={1}>
-              {t('Sessions')}
-            </Flex>
-            <Flex w={140} mx={2}>
-              {t('First Seen')}
-            </Flex>
-            <Flex w={140} mx={2}>
-              {t('Last Seen')}
-            </Flex>
-          </PanelHeader>
+          <SessionPanelHeader>
+            <div>{t('Sessions')}</div>
+            <div>{t('First Seen')}</div>
+            <div>{t('Last Seen')}</div>
+          </SessionPanelHeader>
+
           <PanelBody>
             {ipList.map(ipObj => {
               return <SessionRow key={ipObj.id} {...ipObj} />;
@@ -106,4 +107,20 @@ class AccountSecuritySessionHistory extends AsyncView {
   }
 }
 
-export default withRouter(AccountSecuritySessionHistory);
+export default AccountSecuritySessionHistory;
+
+const getTableLayout = () => `
+  display: grid;
+  grid-template-columns: auto 140px 140px;
+  grid-gap ${space(1)};
+  align-items: center;
+`;
+
+const SessionPanelHeader = styled(PanelHeader)`
+  ${getTableLayout}
+  justify-content: initial;
+`;
+
+const SessionPanelItem = styled(PanelItem)`
+  ${getTableLayout}
+`;

+ 54 - 56
src/sentry/static/sentry/app/views/settings/account/accountSecurity/components/recoveryCodes.jsx

@@ -1,14 +1,7 @@
-import {Box, Flex} from 'grid-emotion';
 import PropTypes from 'prop-types';
 import React from 'react';
 import styled from 'react-emotion';
 
-import {t} from 'app/locale';
-import Button from 'app/components/button';
-import Clipboard from 'app/components/clipboard';
-import Confirm from 'app/components/confirm';
-import EmptyMessage from 'app/views/settings/components/emptyMessage';
-import InlineSvg from 'app/components/inlineSvg';
 import {
   Panel,
   PanelBody,
@@ -16,10 +9,13 @@ import {
   PanelItem,
   PanelAlert,
 } from 'app/components/panels';
-
-const Code = styled(props => <PanelItem p={2} {...props} />)`
-  font-family: ${p => p.theme.text.familyMono};
-`;
+import {t} from 'app/locale';
+import Button from 'app/components/button';
+import Clipboard from 'app/components/clipboard';
+import Confirm from 'app/components/confirm';
+import EmptyMessage from 'app/views/settings/components/emptyMessage';
+import InlineSvg from 'app/components/inlineSvg';
+import space from 'app/styles/space';
 
 class RecoveryCodes extends React.Component {
   static propTypes = {
@@ -36,7 +32,7 @@ class RecoveryCodes extends React.Component {
   };
 
   render() {
-    const {isEnrolled, codes} = this.props;
+    const {className, isEnrolled, codes} = this.props;
 
     if (!isEnrolled || !codes) {
       return null;
@@ -45,56 +41,45 @@ class RecoveryCodes extends React.Component {
     const formattedCodes = codes.join(' \n');
 
     return (
-      <Panel css={{marginTop: 30}}>
+      <Panel className={className}>
         <PanelHeader hasButtons>
-          <Flex align="center">
-            <Box>{t('Unused Codes')}</Box>
-          </Flex>
-          <Flex>
-            <Box ml={1}>
-              <Clipboard hideUnsupported value={formattedCodes}>
-                <Button size="small">
-                  <InlineSvg src="icon-copy" />
-                </Button>
-              </Clipboard>
-            </Box>
-            <Box ml={1}>
-              <Button size="small" onClick={this.printCodes}>
-                <InlineSvg src="icon-print" />
+          {t('Unused Codes')}
+
+          <Actions>
+            <Clipboard hideUnsupported value={formattedCodes}>
+              <Button size="small">
+                <InlineSvg src="icon-copy" />
               </Button>
-            </Box>
-            <Box ml={1}>
-              <Button
-                size="small"
-                download="sentry-recovery-codes.txt"
-                href={`data:text/plain;charset=utf-8,${formattedCodes}`}
-              >
-                <InlineSvg src="icon-download" />
+            </Clipboard>
+            <Button size="small" onClick={this.printCodes}>
+              <InlineSvg src="icon-print" />
+            </Button>
+            <Button
+              size="small"
+              download="sentry-recovery-codes.txt"
+              href={`data:text/plain;charset=utf-8,${formattedCodes}`}
+            >
+              <InlineSvg src="icon-download" />
+            </Button>
+            <Confirm
+              onConfirm={this.props.onRegenerateBackupCodes}
+              message={t(
+                'Are you sure you want to regenerate recovery codes? Your old codes will no longer work.'
+              )}
+            >
+              <Button priority="danger" size="small">
+                {t('Regenerate Codes')}
               </Button>
-            </Box>
-            <Box ml={1}>
-              <Confirm
-                onConfirm={this.props.onRegenerateBackupCodes}
-                message={t(
-                  'Are you sure you want to regenerate recovery codes? Your old codes will no longer work.'
-                )}
-              >
-                <Button priority="danger" size="small">
-                  {t('Regenerate Codes')}
-                </Button>
-              </Confirm>
-            </Box>
-          </Flex>
+            </Confirm>
+          </Actions>
         </PanelHeader>
         <PanelBody>
           <PanelAlert type="warning">
-            <Flex align="center" ml={1} flex="1">
-              {t(
-                'Make sure to save a copy of your recovery codes and store them in a safe place.'
-              )}
-            </Flex>
+            {t(
+              'Make sure to save a copy of your recovery codes and store them in a safe place.'
+            )}
           </PanelAlert>
-          <Box>{!!codes.length && codes.map(code => <Code key={code}>{code}</Code>)}</Box>
+          <div>{!!codes.length && codes.map(code => <Code key={code}>{code}</Code>)}</div>
           {!codes.length && (
             <EmptyMessage>{t('You have no more recovery codes to use')}</EmptyMessage>
           )}
@@ -105,4 +90,17 @@ class RecoveryCodes extends React.Component {
   }
 }
 
-export default RecoveryCodes;
+export default styled(RecoveryCodes)`
+  margin-top: ${space(4)};
+`;
+
+const Actions = styled('div')`
+  display: grid;
+  grid-auto-flow: column;
+  grid-gap: ${space(1)};
+`;
+
+const Code = styled(PanelItem)`
+  font-family: ${p => p.theme.text.familyMono};
+  padding: ${space(2)};
+`;

+ 56 - 20
src/sentry/static/sentry/app/views/settings/account/accountSecurity/components/u2fEnrolledDetails.jsx

@@ -1,16 +1,17 @@
-import {Box, Flex} from 'grid-emotion';
 import PropTypes from 'prop-types';
 import React from 'react';
+import styled from 'react-emotion';
 
+import {Panel, PanelBody, PanelHeader, PanelItem} from 'app/components/panels';
 import {t} from 'app/locale';
 import Button from 'app/components/button';
 import Confirm from 'app/components/confirm';
 import ConfirmHeader from 'app/views/settings/account/accountSecurity/components/confirmHeader';
 import DateTime from 'app/components/dateTime';
-import {Panel, PanelBody, PanelHeader, PanelItem} from 'app/components/panels';
-import TextBlock from 'app/views/settings/components/text/textBlock';
 import EmptyMessage from 'app/views/settings/components/emptyMessage';
+import TextBlock from 'app/views/settings/components/text/textBlock';
 import Tooltip from 'app/components/tooltip';
+import space from 'app/styles/space';
 
 /**
  * List u2f devices w/ ability to remove a single device
@@ -29,7 +30,7 @@ class U2fEnrolledDetails extends React.Component {
   };
 
   render() {
-    const {isEnrolled, devices, id, onRemoveU2fDevice} = this.props;
+    const {className, isEnrolled, devices, id, onRemoveU2fDevice} = this.props;
 
     if (id !== 'u2f' || !isEnrolled) {
       return null;
@@ -40,7 +41,7 @@ class U2fEnrolledDetails extends React.Component {
     const isLastDevice = hasDevices === 1;
 
     return (
-      <Panel css={{marginTop: 30}}>
+      <Panel className={className}>
         <PanelHeader>{t('Device name')}</PanelHeader>
 
         <PanelBody>
@@ -49,15 +50,13 @@ class U2fEnrolledDetails extends React.Component {
           )}
           {hasDevices &&
             devices.map(device => (
-              <PanelItem p={0} key={device.name}>
-                <Flex p={2} pr={0} align="center" flex="1">
-                  <Box flex="1">{device.name}</Box>
-                  <div css={{fontSize: '0.8em', opacity: 0.6}}>
-                    <DateTime date={device.timestamp} />
-                  </div>
-                </Flex>
-
-                <Box p={2}>
+              <DevicePanelItem key={device.name}>
+                <DeviceInformation>
+                  <DeviceName>{device.name}</DeviceName>
+                  <FadedDateTime date={device.timestamp} />
+                </DeviceInformation>
+
+                <Actions>
                   <Confirm
                     onConfirm={() => onRemoveU2fDevice(device)}
                     disabled={isLastDevice}
@@ -85,18 +84,55 @@ class U2fEnrolledDetails extends React.Component {
                       </Tooltip>
                     </Button>
                   </Confirm>
-                </Box>
-              </PanelItem>
+                </Actions>
+              </DevicePanelItem>
             ))}
-          <PanelItem justify="flex-end" p={2}>
-            <Button type="button" to="/settings/account/security/mfa/u2f/enroll/">
+          <AddAnotherPanelItem>
+            <Button
+              type="button"
+              to="/settings/account/security/mfa/u2f/enroll/"
+              size="small"
+            >
               {t('Add Another Device')}
             </Button>
-          </PanelItem>
+          </AddAnotherPanelItem>
         </PanelBody>
       </Panel>
     );
   }
 }
 
-export default U2fEnrolledDetails;
+const DevicePanelItem = styled(PanelItem)`
+  padding: 0;
+`;
+
+const DeviceInformation = styled('div')`
+  display: flex;
+  align-items: center;
+  flex: 1;
+
+  padding: ${space(2)};
+  padding-right: 0;
+`;
+
+const FadedDateTime = styled(DateTime)`
+  font-size: ${p => p.theme.fontSizeRelativeSmall};
+  opacity: 0.6;
+`;
+
+const DeviceName = styled('div')`
+  flex: 1;
+`;
+
+const Actions = styled('div')`
+  margin: ${space(2)};
+`;
+
+const AddAnotherPanelItem = styled(PanelItem)`
+  justify-content: flex-end;
+  padding: ${space(2)};
+`;
+
+export default styled(U2fEnrolledDetails)`
+  margin-top: ${space(4)};
+`;

+ 81 - 58
src/sentry/static/sentry/app/views/settings/account/accountSecurity/index.jsx

@@ -1,8 +1,8 @@
-import {Box, Flex} from 'grid-emotion';
+import PropTypes from 'prop-types';
 import React from 'react';
 import styled from 'react-emotion';
-import PropTypes from 'prop-types';
 
+import {Panel, PanelBody, PanelHeader, PanelItem} from 'app/components/panels';
 import {t} from 'app/locale';
 import AsyncView from 'app/views/asyncView';
 import Button from 'app/components/button';
@@ -11,14 +11,14 @@ import EmptyMessage from 'app/views/settings/components/emptyMessage';
 import Field from 'app/views/settings/components/forms/field';
 import ListLink from 'app/components/links/listLink';
 import NavTabs from 'app/components/navTabs';
-import {Panel, PanelBody, PanelHeader, PanelItem} from 'app/components/panels';
+import PasswordForm from 'app/views/settings/account/passwordForm';
+import RemoveConfirm from 'app/views/settings/account/accountSecurity/components/removeConfirm';
 import SettingsPageHeader from 'app/views/settings/components/settingsPageHeader';
 import TextBlock from 'app/views/settings/components/text/textBlock';
 import Tooltip from 'app/components/tooltip';
 import TwoFactorRequired from 'app/views/settings/account/accountSecurity/components/twoFactorRequired';
-import RemoveConfirm from 'app/views/settings/account/accountSecurity/components/removeConfirm';
-import PasswordForm from 'app/views/settings/account/passwordForm';
 import recreateRoute from 'app/utils/recreateRoute';
+import space from 'app/styles/space';
 
 /**
  * Lists 2fa devices + password change form
@@ -102,9 +102,7 @@ class AccountSecurity extends AsyncView {
         </Panel>
 
         <Panel>
-          <PanelHeader>
-            <Box>{t('Two-Factor Authentication')}</Box>
-          </PanelHeader>
+          <PanelHeader>{t('Two-Factor Authentication')}</PanelHeader>
 
           {isEmpty && (
             <EmptyMessage>{t('No available authenticators to add')}</EmptyMessage>
@@ -123,61 +121,57 @@ class AccountSecurity extends AsyncView {
                   name,
                 } = auth;
                 return (
-                  <PanelItem key={id} p={0} direction="column">
-                    <Flex flex="1" p={2} align="center">
-                      <Box flex="1">
-                        <CircleIndicator css={{marginRight: 6}} enabled={isEnrolled} />
+                  <AuthenticatorPanelItem key={id}>
+                    <AuthenticatorHeader>
+                      <AuthenticatorTitle>
+                        <AuthenticatorStatus enabled={isEnrolled} />
                         <AuthenticatorName>{name}</AuthenticatorName>
-                      </Box>
-
-                      {!isBackupInterface && !isEnrolled && (
-                        <Button
-                          to={`/settings/account/security/mfa/${id}/enroll/`}
-                          size="small"
-                          priority="primary"
-                          className="enroll-button"
-                        >
-                          {t('Add')}
-                        </Button>
-                      )}
-
-                      {isEnrolled && authId && (
-                        <Button
-                          to={`/settings/account/security/mfa/${authId}/`}
-                          size="small"
-                          className="details-button"
-                        >
-                          {configureButton}
-                        </Button>
-                      )}
-
-                      {!isBackupInterface && isEnrolled && (
-                        <Tooltip
-                          title={t(
-                            `Two-factor authentication is required for organization(s): ${this.formatOrgSlugs()}.`
-                          )}
-                          disabled={!deleteDisabled}
-                        >
-                          <RemoveConfirm
-                            onConfirm={() => onDisable(auth)}
-                            disabled={deleteDisabled}
+                      </AuthenticatorTitle>
+
+                      <Actions>
+                        {!isBackupInterface && !isEnrolled && (
+                          <Button
+                            to={`/settings/account/security/mfa/${id}/enroll/`}
+                            size="small"
+                            priority="primary"
+                            className="enroll-button"
+                          >
+                            {t('Add')}
+                          </Button>
+                        )}
+
+                        {isEnrolled && authId && (
+                          <Button
+                            to={`/settings/account/security/mfa/${authId}/`}
+                            size="small"
+                            className="details-button"
                           >
-                            <Button
-                              css={{marginLeft: 6}}
-                              size="small"
-                              icon="icon-trash"
-                            />
-                          </RemoveConfirm>
-                        </Tooltip>
-                      )}
+                            {configureButton}
+                          </Button>
+                        )}
+
+                        {!isBackupInterface && isEnrolled && (
+                          <Tooltip
+                            title={t(
+                              `Two-factor authentication is required for organization(s): ${this.formatOrgSlugs()}.`
+                            )}
+                            disabled={!deleteDisabled}
+                          >
+                            <RemoveConfirm
+                              onConfirm={() => onDisable(auth)}
+                              disabled={deleteDisabled}
+                            >
+                              <Button size="small" icon="icon-trash" />
+                            </RemoveConfirm>
+                          </Tooltip>
+                        )}
+                      </Actions>
 
                       {isBackupInterface && !isEnrolled ? t('requires 2FA') : null}
-                    </Flex>
+                    </AuthenticatorHeader>
 
-                    <Box p={2} pt={0}>
-                      <TextBlock css={{marginBottom: 0}}>{description}</TextBlock>
-                    </Box>
-                  </PanelItem>
+                    <Description>{description}</Description>
+                  </AuthenticatorPanelItem>
                 );
               })}
           </PanelBody>
@@ -191,4 +185,33 @@ const AuthenticatorName = styled('span')`
   font-size: 1.2em;
 `;
 
+const AuthenticatorPanelItem = styled(PanelItem)`
+  flex-direction: column;
+`;
+
+const AuthenticatorHeader = styled('div')`
+  display: flex;
+  flex: 1;
+  align-items: center;
+`;
+
+const AuthenticatorTitle = styled('div')`
+  flex: 1;
+`;
+
+const Actions = styled('div')`
+  display: grid;
+  grid-auto-flow: column;
+  grid-gap: ${space(1)};
+`;
+
+const AuthenticatorStatus = styled(CircleIndicator)`
+  margin-right: ${space(1)};
+`;
+
+const Description = styled(TextBlock)`
+  margin-top: ${space(2)};
+  margin-bottom: 0;
+`;
+
 export default AccountSecurity;

+ 8 - 8
tests/js/spec/views/accountSecurity.spec.jsx

@@ -60,7 +60,7 @@ describe('AccountSecurity', function() {
 
     // Remove button
     expect(wrapper.find('Button[icon="icon-trash"]')).toHaveLength(1);
-    expect(wrapper.find('CircleIndicator').prop('enabled')).toBe(true);
+    expect(wrapper.find('AuthenticatorStatus').prop('enabled')).toBe(true);
 
     expect(wrapper.find('TwoFactorRequired')).toHaveLength(0);
   });
@@ -89,7 +89,7 @@ describe('AccountSecurity', function() {
       </AccountSecurityWrapper>,
       TestStubs.routerContext()
     );
-    expect(wrapper.find('CircleIndicator').prop('enabled')).toBe(true);
+    expect(wrapper.find('AuthenticatorStatus').prop('enabled')).toBe(true);
 
     // This will open confirm modal
     wrapper.find('Button[icon="icon-trash"]').simulate('click');
@@ -103,7 +103,7 @@ describe('AccountSecurity', function() {
 
     setTimeout(() => {
       wrapper.update();
-      expect(wrapper.find('CircleIndicator').prop('enabled')).toBe(false);
+      expect(wrapper.find('AuthenticatorStatus').prop('enabled')).toBe(false);
     }, 1);
     // still has another 2fa method
     expect(wrapper.find('TwoFactorRequired')).toHaveLength(0);
@@ -140,7 +140,7 @@ describe('AccountSecurity', function() {
 
     expect(
       wrapper
-        .find('CircleIndicator')
+        .find('AuthenticatorStatus')
         .first()
         .prop('enabled')
     ).toBe(true);
@@ -199,7 +199,7 @@ describe('AccountSecurity', function() {
       </AccountSecurityWrapper>,
       TestStubs.routerContext()
     );
-    expect(wrapper.find('CircleIndicator').prop('enabled')).toBe(true);
+    expect(wrapper.find('AuthenticatorStatus').prop('enabled')).toBe(true);
 
     expect(wrapper.find('RemoveConfirm').prop('disabled')).toBe(true);
     expect(wrapper.find('Tooltip').prop('disabled')).toBe(false);
@@ -233,7 +233,7 @@ describe('AccountSecurity', function() {
         .first()
         .prop('children')
     ).toBe('Add');
-    expect(wrapper.find('CircleIndicator').prop('enabled')).toBe(false);
+    expect(wrapper.find('AuthenticatorStatus').prop('enabled')).toBe(false);
     // user is not 2fa enrolled
     expect(wrapper.find('TwoFactorRequired')).toHaveLength(1);
   });
@@ -255,7 +255,7 @@ describe('AccountSecurity', function() {
 
     // There should be an View Codes button
     expect(wrapper.find('Button[className="details-button"]')).toHaveLength(0);
-    expect(wrapper.find('CircleIndicator').prop('enabled')).toBe(false);
+    expect(wrapper.find('AuthenticatorStatus').prop('enabled')).toBe(false);
     // user is not 2fa enrolled
     expect(wrapper.find('TwoFactorRequired')).toHaveLength(1);
   });
@@ -281,7 +281,7 @@ describe('AccountSecurity', function() {
         .first()
         .prop('children')
     ).toBe('View Codes');
-    expect(wrapper.find('CircleIndicator').prop('enabled')).toBe(true);
+    expect(wrapper.find('AuthenticatorStatus').prop('enabled')).toBe(true);
   });
 
   it('can change password', function() {

+ 34 - 48
tests/js/spec/views/accountSecurityDetails.spec.jsx

@@ -1,7 +1,8 @@
 import React from 'react';
-import {mountWithTheme} from 'sentry-test/enzyme';
 
 import {Client} from 'app/api';
+import {initializeOrg} from 'sentry-test/initializeOrg';
+import {mountWithTheme} from 'sentry-test/enzyme';
 import AccountSecurityDetails from 'app/views/settings/account/accountSecurity/accountSecurityDetails';
 import AccountSecurityWrapper from 'app/views/settings/account/accountSecurity/accountSecurityWrapper';
 
@@ -10,10 +11,23 @@ const ORG_ENDPOINT = '/organizations/';
 
 describe('AccountSecurityDetails', function() {
   let wrapper;
+  let routerContext;
+  let router;
+  let params;
 
   describe('Totp', function() {
-    Client.clearMockResponses();
     beforeAll(function() {
+      Client.clearMockResponses();
+      params = {
+        authId: 15,
+      };
+
+      ({router, routerContext} = initializeOrg({
+        router: {
+          params,
+        },
+      }));
+
       Client.addMockResponse({
         url: ENDPOINT,
         body: TestStubs.AllAuthenticators(),
@@ -28,23 +42,14 @@ describe('AccountSecurityDetails', function() {
       });
       wrapper = mountWithTheme(
         <AccountSecurityWrapper>
-          <AccountSecurityDetails />
+          <AccountSecurityDetails router={router} params={params} />
         </AccountSecurityWrapper>,
-        TestStubs.routerContext([
-          {
-            router: {
-              ...TestStubs.router(),
-              params: {
-                authId: 15,
-              },
-            },
-          },
-        ])
+        routerContext
       );
     });
 
     it('has enrolled circle indicator', function() {
-      expect(wrapper.find('CircleIndicator').prop('enabled')).toBe(true);
+      expect(wrapper.find('AuthenticatorStatus').prop('enabled')).toBe(true);
     });
 
     it('has created and last used dates', function() {
@@ -78,18 +83,9 @@ describe('AccountSecurityDetails', function() {
 
       wrapper = mountWithTheme(
         <AccountSecurityWrapper>
-          <AccountSecurityDetails />
+          <AccountSecurityDetails router={router} params={params} />
         </AccountSecurityWrapper>,
-        TestStubs.routerContext([
-          {
-            router: {
-              ...TestStubs.router(),
-              params: {
-                authId: 15,
-              },
-            },
-          },
-        ])
+        routerContext
       );
 
       wrapper.find('RemoveConfirm Button').simulate('click');
@@ -117,18 +113,9 @@ describe('AccountSecurityDetails', function() {
 
       wrapper = mountWithTheme(
         <AccountSecurityWrapper>
-          <AccountSecurityDetails />
+          <AccountSecurityDetails router={router} params={params} />
         </AccountSecurityWrapper>,
-        TestStubs.routerContext([
-          {
-            router: {
-              ...TestStubs.router(),
-              params: {
-                authId: 15,
-              },
-            },
-          },
-        ])
+        routerContext
       );
 
       wrapper.find('RemoveConfirm Button').simulate('click');
@@ -139,6 +126,13 @@ describe('AccountSecurityDetails', function() {
 
   describe('Recovery', function() {
     beforeEach(function() {
+      params = {authId: 16};
+      ({router, routerContext} = initializeOrg({
+        router: {
+          params,
+        },
+      }));
+
       Client.clearMockResponses();
       Client.addMockResponse({
         url: ENDPOINT,
@@ -152,25 +146,17 @@ describe('AccountSecurityDetails', function() {
         url: `${ENDPOINT}16/`,
         body: TestStubs.Authenticators().Recovery(),
       });
+
       wrapper = mountWithTheme(
         <AccountSecurityWrapper>
-          <AccountSecurityDetails />
+          <AccountSecurityDetails router={router} params={params} />
         </AccountSecurityWrapper>,
-        TestStubs.routerContext([
-          {
-            router: {
-              ...TestStubs.router(),
-              params: {
-                authId: 16,
-              },
-            },
-          },
-        ])
+        routerContext
       );
     });
 
     it('has enrolled circle indicator', function() {
-      expect(wrapper.find('CircleIndicator').prop('enabled')).toBe(true);
+      expect(wrapper.find('AuthenticatorStatus').prop('enabled')).toBe(true);
     });
 
     it('has created and last used dates', function() {