providerItem.tsx 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204
  1. import styled from '@emotion/styled';
  2. import Access from 'sentry/components/acl/access';
  3. import Feature from 'sentry/components/acl/feature';
  4. import FeatureDisabled from 'sentry/components/acl/featureDisabled';
  5. import Tag from 'sentry/components/badge/tag';
  6. import {Button} from 'sentry/components/button';
  7. import {Hovercard} from 'sentry/components/hovercard';
  8. import PanelItem from 'sentry/components/panels/panelItem';
  9. import {IconLock} from 'sentry/icons';
  10. import {t} from 'sentry/locale';
  11. import {space} from 'sentry/styles/space';
  12. import type {AuthProvider} from 'sentry/types/auth';
  13. import type {FeatureDisabledHooks} from 'sentry/types/hooks';
  14. import {descopeFeatureName} from 'sentry/utils';
  15. type RenderInstallButtonProps = {
  16. hasFeature: boolean;
  17. /**
  18. * We pass the provider so that it may be passed into any hook provided
  19. * callbacks.
  20. */
  21. provider: AuthProvider;
  22. };
  23. type LockedFeatureProps = {
  24. features: string[];
  25. provider: AuthProvider;
  26. className?: string;
  27. };
  28. type Props = {
  29. active: boolean;
  30. provider: AuthProvider;
  31. onConfigure?: (providerKey: string, e: React.MouseEvent) => void;
  32. };
  33. function ProviderItem({provider, active, onConfigure}: Props) {
  34. const handleConfigure = (e: React.MouseEvent) => {
  35. onConfigure?.(provider.key, e);
  36. };
  37. const renderDisabledLock = (p: LockedFeatureProps) => (
  38. <LockedFeature provider={p.provider} features={p.features} />
  39. );
  40. const defaultRenderInstallButton = ({hasFeature}: RenderInstallButtonProps) => (
  41. <Access access={['org:write']}>
  42. {({hasAccess}) => (
  43. <Button
  44. type="submit"
  45. name="provider"
  46. size="sm"
  47. value={provider.key}
  48. disabled={!hasFeature || !hasAccess}
  49. onClick={handleConfigure}
  50. >
  51. {t('Configure')}
  52. </Button>
  53. )}
  54. </Access>
  55. );
  56. // TODO(epurkhiser): We should probably use a more explicit hook name,
  57. // instead of just the feature names (sso-basic, sso-saml2, etc).
  58. const featureKey = provider.requiredFeature;
  59. const hookName = featureKey
  60. ? (`feature-disabled:${descopeFeatureName(featureKey)}` as keyof FeatureDisabledHooks)
  61. : null;
  62. const featureProps = hookName ? {hookName} : {};
  63. const getProviderDescription = providerName => {
  64. if (providerName === 'SAML2') {
  65. return t(
  66. 'your preferred SAML2 compliant provider like Ping Identity, Google SAML, Keycloak, or VMware Identity Manager'
  67. );
  68. }
  69. if (providerName === 'Google') {
  70. return t('Google (OAuth)');
  71. }
  72. return providerName;
  73. };
  74. return (
  75. <Feature
  76. {...featureProps}
  77. features={[featureKey].filter(f => f)}
  78. renderDisabled={({children, ...props}) =>
  79. typeof children === 'function' &&
  80. // TODO(ts): the Feature component isn't correctly templatized to allow
  81. // for custom props in the renderDisabled function
  82. children({...props, renderDisabled: renderDisabledLock as any})
  83. }
  84. >
  85. {({hasFeature, features, renderDisabled, renderInstallButton}) => (
  86. <PanelItem center>
  87. <ProviderInfo>
  88. <ProviderLogo
  89. className={`provider-logo ${provider.name
  90. .replace(/\s/g, '-')
  91. .toLowerCase()}`}
  92. />
  93. <div>
  94. <ProviderName>{provider.name}</ProviderName>
  95. <ProviderDescription>
  96. {t(
  97. 'Enable your organization to sign in with %s.',
  98. getProviderDescription(provider.name)
  99. )}
  100. </ProviderDescription>
  101. </div>
  102. </ProviderInfo>
  103. <FeatureBadge>
  104. {!hasFeature &&
  105. // renderDisabled is overridden by renderDisabled above
  106. (renderDisabled as typeof renderDisabledLock)({provider, features})}
  107. </FeatureBadge>
  108. <div>
  109. {active ? (
  110. <ActiveIndicator />
  111. ) : (
  112. // renderInstallButton is overridden by renderDisabled above
  113. (
  114. (renderInstallButton as typeof defaultRenderInstallButton) ??
  115. defaultRenderInstallButton
  116. )({provider, hasFeature})
  117. )}
  118. </div>
  119. </PanelItem>
  120. )}
  121. </Feature>
  122. );
  123. }
  124. export default ProviderItem;
  125. const ProviderInfo = styled('div')`
  126. flex: 1;
  127. display: grid;
  128. grid-template-columns: max-content 1fr;
  129. gap: ${space(2)};
  130. `;
  131. const ProviderLogo = styled('div')`
  132. height: 36px;
  133. width: 36px;
  134. border-radius: 3px;
  135. margin-right: 0;
  136. top: auto;
  137. `;
  138. const ProviderName = styled('div')`
  139. font-weight: ${p => p.theme.fontWeightBold};
  140. `;
  141. const ProviderDescription = styled('div')`
  142. font-size: ${p => p.theme.fontSizeSmall};
  143. color: ${p => p.theme.subText};
  144. `;
  145. const FeatureBadge = styled('div')`
  146. flex: 1;
  147. `;
  148. const ActiveIndicator = styled('div')`
  149. background: ${p => p.theme.green300};
  150. color: ${p => p.theme.white};
  151. padding: ${space(1)} ${space(1.5)};
  152. border-radius: 2px;
  153. font-size: 0.8em;
  154. `;
  155. ActiveIndicator.defaultProps = {
  156. children: t('Active'),
  157. };
  158. const DisabledHovercard = styled(Hovercard)`
  159. width: 350px;
  160. `;
  161. function LockedFeature({provider, features, className}: LockedFeatureProps) {
  162. return (
  163. <DisabledHovercard
  164. containerClassName={className}
  165. body={
  166. <FeatureDisabled
  167. features={features}
  168. hideHelpToggle
  169. message={t('%s SSO is disabled.', provider.name)}
  170. featureName={t('SSO Auth')}
  171. />
  172. }
  173. >
  174. <Tag role="status" icon={<IconLock locked />}>
  175. {t('disabled')}
  176. </Tag>
  177. </DisabledHovercard>
  178. );
  179. }