providerItem.tsx 5.5 KB

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