|
|
|
@ -1,31 +1,27 @@ |
|
|
|
|
import { css } from '@emotion/css'; |
|
|
|
|
import { isEmpty } from 'lodash'; |
|
|
|
|
import React, { useEffect } from 'react'; |
|
|
|
|
import React, { JSX, useEffect } from 'react'; |
|
|
|
|
import { connect, ConnectedProps } from 'react-redux'; |
|
|
|
|
|
|
|
|
|
import { GrafanaTheme2 } from '@grafana/data'; |
|
|
|
|
import { reportInteraction } from '@grafana/runtime'; |
|
|
|
|
import { useStyles2 } from '@grafana/ui'; |
|
|
|
|
import { Grid, TextLink } from '@grafana/ui'; |
|
|
|
|
import { Page } from 'app/core/components/Page/Page'; |
|
|
|
|
import { StoreState } from 'app/types'; |
|
|
|
|
|
|
|
|
|
import ConfigureAuthCTA from './components/ConfigureAuthCTA'; |
|
|
|
|
import { ProviderCard } from './components/ProviderCard'; |
|
|
|
|
import { loadSettings } from './state/actions'; |
|
|
|
|
import { AuthProviderInfo } from './types'; |
|
|
|
|
import { getProviderUrl } from './utils'; |
|
|
|
|
|
|
|
|
|
import { getRegisteredAuthProviders } from '.'; |
|
|
|
|
import { getRegisteredAuthProviders } from './index'; |
|
|
|
|
|
|
|
|
|
interface OwnProps {} |
|
|
|
|
|
|
|
|
|
export type Props = OwnProps & ConnectedProps<typeof connector>; |
|
|
|
|
|
|
|
|
|
function mapStateToProps(state: StoreState) { |
|
|
|
|
const { isLoading, providerStatuses } = state.authConfig; |
|
|
|
|
const { isLoading, providerStatuses, providers } = state.authConfig; |
|
|
|
|
return { |
|
|
|
|
isLoading, |
|
|
|
|
providerStatuses, |
|
|
|
|
providers, |
|
|
|
|
}; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
@ -35,125 +31,65 @@ const mapDispatchToProps = { |
|
|
|
|
|
|
|
|
|
const connector = connect(mapStateToProps, mapDispatchToProps); |
|
|
|
|
|
|
|
|
|
export const AuthConfigPageUnconnected = ({ providerStatuses, isLoading, loadSettings }: Props): JSX.Element => { |
|
|
|
|
const styles = useStyles2(getStyles); |
|
|
|
|
|
|
|
|
|
export const AuthConfigPageUnconnected = ({ |
|
|
|
|
providerStatuses, |
|
|
|
|
isLoading, |
|
|
|
|
loadSettings, |
|
|
|
|
providers, |
|
|
|
|
}: Props): JSX.Element => { |
|
|
|
|
useEffect(() => { |
|
|
|
|
loadSettings(); |
|
|
|
|
}, [loadSettings]); |
|
|
|
|
|
|
|
|
|
const authProviders = getRegisteredAuthProviders(); |
|
|
|
|
const enabledProviders = authProviders.filter((p) => providerStatuses[p.id]?.enabled); |
|
|
|
|
const configuresProviders = authProviders.filter( |
|
|
|
|
(p) => providerStatuses[p.id]?.configured && !providerStatuses[p.id]?.enabled |
|
|
|
|
); |
|
|
|
|
const availableProviders = authProviders.filter( |
|
|
|
|
(p) => !providerStatuses[p.id]?.enabled && !providerStatuses[p.id]?.configured && !providerStatuses[p.id]?.hide |
|
|
|
|
); |
|
|
|
|
const firstAvailableProvider = availableProviders?.length ? availableProviders[0] : null; |
|
|
|
|
|
|
|
|
|
{ |
|
|
|
|
/* TODO: make generic for the provider of the configuration or make the documentation point to a collection of all our providers */ |
|
|
|
|
} |
|
|
|
|
const docsLink = ( |
|
|
|
|
<a |
|
|
|
|
className="external-link" |
|
|
|
|
href="https://grafana.com/docs/grafana/next/setup-grafana/configure-security/configure-authentication/saml-ui/" |
|
|
|
|
target="_blank" |
|
|
|
|
rel="noopener noreferrer" |
|
|
|
|
> |
|
|
|
|
documentation. |
|
|
|
|
</a> |
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
const subTitle = <span>Manage your auth settings and configure single sign-on. Find out more in our {docsLink}</span>; |
|
|
|
|
|
|
|
|
|
const onCTAClick = () => { |
|
|
|
|
reportInteraction('authentication_ui_created', { provider: firstAvailableProvider?.type }); |
|
|
|
|
}; |
|
|
|
|
const onProviderCardClick = (provider: AuthProviderInfo) => { |
|
|
|
|
reportInteraction('authentication_ui_provider_clicked', { provider: provider.type }); |
|
|
|
|
const availableProviders = authProviders.filter((p) => !providerStatuses[p.id]?.hide); |
|
|
|
|
const onProviderCardClick = (providerType: string) => { |
|
|
|
|
reportInteraction('authentication_ui_provider_clicked', { provider: providerType }); |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
const providerList = availableProviders.length |
|
|
|
|
? [ |
|
|
|
|
...availableProviders.map((p) => ({ |
|
|
|
|
provider: p.id, |
|
|
|
|
settings: { ...providerStatuses[p.id], configPath: p.configPath, type: p.type }, |
|
|
|
|
})), |
|
|
|
|
...providers, |
|
|
|
|
] |
|
|
|
|
: providers; |
|
|
|
|
return ( |
|
|
|
|
<Page navId="authentication" subTitle={subTitle}> |
|
|
|
|
<Page |
|
|
|
|
navId="authentication" |
|
|
|
|
subTitle={ |
|
|
|
|
<> |
|
|
|
|
Manage your auth settings and configure single sign-on. Find out more in our{' '} |
|
|
|
|
<TextLink href="https://grafana.com/docs/grafana/next/setup-grafana/configure-security/configure-authentication"> |
|
|
|
|
documentation |
|
|
|
|
</TextLink> |
|
|
|
|
. |
|
|
|
|
</> |
|
|
|
|
} |
|
|
|
|
> |
|
|
|
|
<Page.Contents isLoading={isLoading}> |
|
|
|
|
<h3 className={styles.sectionHeader}>Configured authentication</h3> |
|
|
|
|
{!!enabledProviders?.length && ( |
|
|
|
|
<div className={styles.cardsContainer}> |
|
|
|
|
{enabledProviders.map((provider) => ( |
|
|
|
|
{!providerList.length ? ( |
|
|
|
|
<ConfigureAuthCTA /> |
|
|
|
|
) : ( |
|
|
|
|
<Grid gap={3} minColumnWidth={34}> |
|
|
|
|
{providerList.map(({ provider, settings }) => ( |
|
|
|
|
<ProviderCard |
|
|
|
|
key={provider.id} |
|
|
|
|
providerId={provider.id} |
|
|
|
|
displayName={providerStatuses[provider.id]?.name || provider.displayName} |
|
|
|
|
authType={provider.protocol} |
|
|
|
|
enabled={providerStatuses[provider.id]?.enabled} |
|
|
|
|
configPath={provider.configPath} |
|
|
|
|
key={provider} |
|
|
|
|
authType={settings.type || 'OAuth'} |
|
|
|
|
providerId={provider} |
|
|
|
|
displayName={provider} |
|
|
|
|
enabled={settings.enabled} |
|
|
|
|
onClick={() => onProviderCardClick(provider)} |
|
|
|
|
configPath={settings.configPath} |
|
|
|
|
/> |
|
|
|
|
))} |
|
|
|
|
</div> |
|
|
|
|
)} |
|
|
|
|
{!enabledProviders?.length && firstAvailableProvider && !isEmpty(providerStatuses) && ( |
|
|
|
|
<ConfigureAuthCTA |
|
|
|
|
title={`You have no ${firstAvailableProvider.type} configuration created at the moment`} |
|
|
|
|
buttonIcon="plus-circle" |
|
|
|
|
buttonLink={getProviderUrl(firstAvailableProvider)} |
|
|
|
|
buttonTitle={`Configure ${firstAvailableProvider.type}`} |
|
|
|
|
onClick={onCTAClick} |
|
|
|
|
/> |
|
|
|
|
)} |
|
|
|
|
{!!configuresProviders?.length && ( |
|
|
|
|
<div className={styles.cardsContainer}> |
|
|
|
|
{configuresProviders.map((provider) => ( |
|
|
|
|
<ProviderCard |
|
|
|
|
key={provider.id} |
|
|
|
|
providerId={provider.id} |
|
|
|
|
displayName={providerStatuses[provider.id]?.name || provider.displayName} |
|
|
|
|
authType={provider.protocol} |
|
|
|
|
enabled={providerStatuses[provider.id]?.enabled} |
|
|
|
|
configPath={provider.configPath} |
|
|
|
|
onClick={() => onProviderCardClick(provider)} |
|
|
|
|
/> |
|
|
|
|
))} |
|
|
|
|
</div> |
|
|
|
|
</Grid> |
|
|
|
|
)} |
|
|
|
|
</Page.Contents> |
|
|
|
|
</Page> |
|
|
|
|
); |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
const getStyles = (theme: GrafanaTheme2) => { |
|
|
|
|
return { |
|
|
|
|
cardsContainer: css` |
|
|
|
|
display: grid; |
|
|
|
|
grid-template-columns: repeat(auto-fill, minmax(288px, 1fr)); |
|
|
|
|
gap: ${theme.spacing(3)}; |
|
|
|
|
margin-bottom: ${theme.spacing(3)}; |
|
|
|
|
margin-top: ${theme.spacing(2)}; |
|
|
|
|
`,
|
|
|
|
|
sectionHeader: css` |
|
|
|
|
margin-bottom: ${theme.spacing(3)}; |
|
|
|
|
`,
|
|
|
|
|
settingsSection: css` |
|
|
|
|
margin-top: ${theme.spacing(4)}; |
|
|
|
|
`,
|
|
|
|
|
settingName: css` |
|
|
|
|
padding-left: 25px; |
|
|
|
|
`,
|
|
|
|
|
doclink: css` |
|
|
|
|
padding-bottom: 5px; |
|
|
|
|
padding-top: -5px; |
|
|
|
|
font-size: ${theme.typography.bodySmall.fontSize}; |
|
|
|
|
a { |
|
|
|
|
color: ${theme.colors.info.name}; // use theme link color or any other color
|
|
|
|
|
text-decoration: underline; // underline or none, as you prefer
|
|
|
|
|
} |
|
|
|
|
`,
|
|
|
|
|
settingValue: css` |
|
|
|
|
white-space: break-spaces; |
|
|
|
|
`,
|
|
|
|
|
}; |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
export default connector(AuthConfigPageUnconnected); |
|
|
|
|