Admin: Add promotional page for Grafana Enterprise (#21422)
Co-authored-by: Torkel Ödegaard <torkel@grafana.com>pull/21509/head
@ -0,0 +1,41 @@ |
||||
package licensing |
||||
|
||||
import ( |
||||
"github.com/grafana/grafana/pkg/api/dtos" |
||||
"github.com/grafana/grafana/pkg/services/hooks" |
||||
"github.com/grafana/grafana/pkg/setting" |
||||
) |
||||
|
||||
type OSSLicensingService struct { |
||||
Cfg *setting.Cfg `inject:""` |
||||
HooksService *hooks.HooksService `inject:""` |
||||
} |
||||
|
||||
func (*OSSLicensingService) HasLicense() bool { |
||||
return false |
||||
} |
||||
|
||||
func (*OSSLicensingService) Expiry() int64 { |
||||
return 0 |
||||
} |
||||
|
||||
func (l *OSSLicensingService) Init() error { |
||||
l.HooksService.AddIndexDataHook(func(indexData *dtos.IndexViewData) { |
||||
for _, node := range indexData.NavTree { |
||||
if node.Id == "admin" { |
||||
node.Children = append(node.Children, &dtos.NavLink{ |
||||
Text: "Upgrade", |
||||
Id: "upgrading", |
||||
Url: l.Cfg.AppSubUrl + "/admin/upgrading", |
||||
Icon: "fa fa-fw fa-unlock-alt", |
||||
}) |
||||
} |
||||
} |
||||
}) |
||||
|
||||
return nil |
||||
} |
||||
|
||||
func (*OSSLicensingService) HasValidLicense() bool { |
||||
return false |
||||
} |
@ -0,0 +1,105 @@ |
||||
import React from 'react'; |
||||
import { stylesFactory, useTheme } from '@grafana/ui'; |
||||
import { css } from 'emotion'; |
||||
import { GrafanaTheme } from '@grafana/data'; |
||||
|
||||
const title = { fontWeight: 500, fontSize: '26px', lineHeight: '123%' }; |
||||
|
||||
const getStyles = stylesFactory((theme: GrafanaTheme) => { |
||||
const background = theme.colors.panelBg; |
||||
const backgroundUrl = theme.isDark |
||||
? '/public/img/licensing/header_dark.svg' |
||||
: '/public/img/licensing/header_light.svg'; |
||||
const footerBg = theme.isDark ? theme.colors.dark9 : theme.colors.gray6; |
||||
|
||||
return { |
||||
container: css` |
||||
display: grid; |
||||
grid-template-columns: 100%; |
||||
column-gap: 20px; |
||||
row-gap: 40px; |
||||
padding: 42px 20px 0 77px; |
||||
background-color: ${background}; |
||||
@media (min-width: 1050px) { |
||||
grid-template-columns: 50% 50%; |
||||
} |
||||
`,
|
||||
footer: css` |
||||
text-align: center; |
||||
padding: 16px; |
||||
background: ${footerBg}; |
||||
`,
|
||||
header: css` |
||||
height: 137px; |
||||
padding: 40px 0 0 79px; |
||||
position: relative; |
||||
background: url('${backgroundUrl}') right; |
||||
`,
|
||||
}; |
||||
}); |
||||
|
||||
interface Props { |
||||
header: string; |
||||
subheader?: string; |
||||
editionNotice?: string; |
||||
} |
||||
|
||||
export const LicenseChrome: React.FC<Props> = ({ header, editionNotice, subheader, children }) => { |
||||
const theme = useTheme(); |
||||
const styles = getStyles(theme); |
||||
|
||||
return ( |
||||
<> |
||||
<div className={styles.header}> |
||||
<h2 style={title}>{header}</h2> |
||||
{subheader && <h3>{subheader}</h3>} |
||||
|
||||
<Circle |
||||
size="128px" |
||||
style={{ |
||||
boxShadow: '0px 0px 24px rgba(24, 58, 110, 0.45)', |
||||
background: '#0A1C36', |
||||
position: 'absolute', |
||||
top: '19px', |
||||
left: '71%', |
||||
}} |
||||
> |
||||
<img |
||||
className="logo-icon" |
||||
src="/public/img/grafana_icon.svg" |
||||
alt="Grafana" |
||||
width="80px" |
||||
style={{ position: 'absolute', left: '23px', top: '20px' }} |
||||
/> |
||||
</Circle> |
||||
</div> |
||||
|
||||
<div className={styles.container}>{children}</div> |
||||
|
||||
{editionNotice && <div className={styles.footer}>{editionNotice}</div>} |
||||
</> |
||||
); |
||||
}; |
||||
|
||||
interface CircleProps { |
||||
size: string; |
||||
style?: React.CSSProperties; |
||||
} |
||||
|
||||
export const Circle: React.FC<CircleProps> = ({ size, style, children }) => { |
||||
return ( |
||||
<div |
||||
style={{ |
||||
width: size, |
||||
height: size, |
||||
position: 'absolute', |
||||
bottom: 0, |
||||
right: 0, |
||||
borderRadius: '50%', |
||||
...style, |
||||
}} |
||||
> |
||||
{children} |
||||
</div> |
||||
); |
||||
}; |
@ -0,0 +1,188 @@ |
||||
import React from 'react'; |
||||
import { css } from 'emotion'; |
||||
import { NavModel } from '@grafana/data'; |
||||
import Page from '../../core/components/Page/Page'; |
||||
import { LicenseChrome } from './LicenseChrome'; |
||||
import { Forms } from '@grafana/ui'; |
||||
import { hot } from 'react-hot-loader'; |
||||
import { StoreState } from '../../types'; |
||||
import { getNavModel } from '../../core/selectors/navModel'; |
||||
import { connect } from 'react-redux'; |
||||
|
||||
interface Props { |
||||
navModel: NavModel; |
||||
} |
||||
|
||||
export const UpgradePage: React.FC<Props> = ({ navModel }) => { |
||||
return ( |
||||
<Page navModel={navModel}> |
||||
<Page.Contents> |
||||
<UpgradeInfo |
||||
editionNotice="You are running the open-source version of Grafana. |
||||
You have to install the Enterprise edition in order enable Enterprise features." |
||||
/> |
||||
</Page.Contents> |
||||
</Page> |
||||
); |
||||
}; |
||||
|
||||
const titleStyles = { fontWeight: 500, fontSize: '26px', lineHeight: '123%' }; |
||||
|
||||
interface UpgradeInfoProps { |
||||
editionNotice?: string; |
||||
} |
||||
|
||||
export const UpgradeInfo: React.FC<UpgradeInfoProps> = ({ editionNotice }) => { |
||||
return ( |
||||
<LicenseChrome header="Grafana Enterprise" subheader="Get your free trial" editionNotice={editionNotice}> |
||||
<FeatureInfo /> |
||||
<ServiceInfo /> |
||||
</LicenseChrome> |
||||
); |
||||
}; |
||||
|
||||
const GetEnterprise: React.FC = () => { |
||||
return ( |
||||
<div style={{ marginTop: '60px', marginBottom: '60px' }}> |
||||
<h2 style={titleStyles}>Get Grafana Enterprise</h2> |
||||
<CallToAction /> |
||||
<p style={{ paddingTop: '12px' }}> |
||||
You can use the trial version for free for <strong>30 days</strong>. We will remind you about it{' '} |
||||
<strong>5 days before the trial period ends</strong>. |
||||
</p> |
||||
</div> |
||||
); |
||||
}; |
||||
|
||||
const CallToAction: React.FC = () => { |
||||
return ( |
||||
<Forms.LinkButton |
||||
variant="primary" |
||||
size="lg" |
||||
href="https://grafana.com/contact?about=grafana-enterprise&utm_source=grafana-upgrade-page" |
||||
> |
||||
Contact us and get a free trial |
||||
</Forms.LinkButton> |
||||
); |
||||
}; |
||||
|
||||
const ServiceInfo: React.FC = () => { |
||||
return ( |
||||
<div> |
||||
<h4>At your service</h4> |
||||
|
||||
<List> |
||||
<Item title="Premium Plugins" image="/public/img/licensing/plugin_enterprise.svg" /> |
||||
<Item title="Critical SLA: 2 hours" image="/public/img/licensing/sla.svg" /> |
||||
<Item title="Unlimited Expert Support" image="/public/img/licensing/customer_support.svg"> |
||||
24x7x365 support via |
||||
<List nested={true}> |
||||
<Item title="Email" /> |
||||
<Item title="Private slack channel" /> |
||||
<Item title="Phone" /> |
||||
</List> |
||||
</Item> |
||||
<Item title="Hand-in-hand support" image="/public/img/licensing/handinhand_support.svg"> |
||||
in the upgrade process |
||||
</Item> |
||||
</List> |
||||
|
||||
<div style={{ marginTop: '20px' }}> |
||||
<strong>Also included:</strong> |
||||
<br /> |
||||
Indemnification, working with Grafana Labs on future prioritization, and training from the core Grafana team. |
||||
</div> |
||||
</div> |
||||
); |
||||
}; |
||||
|
||||
const FeatureInfo: React.FC = () => { |
||||
return ( |
||||
<div style={{ paddingRight: '11px' }}> |
||||
<h4>Enhanced Functionality</h4> |
||||
<FeatureListing /> |
||||
|
||||
<GetEnterprise /> |
||||
</div> |
||||
); |
||||
}; |
||||
|
||||
const FeatureListing: React.FC = () => { |
||||
return ( |
||||
<List> |
||||
<Item title="Data source permissions" /> |
||||
<Item title="Reporting" /> |
||||
<Item title="SAML Authentication" /> |
||||
<Item title="Enhanced LDAP Integration" /> |
||||
<Item title="Team Sync">LDAP, GitHub OAuth, Auth Proxy</Item> |
||||
<Item title="White labeling" /> |
||||
<Item title="Premium Plugins"> |
||||
<List nested={true}> |
||||
<Item title="Oracle" /> |
||||
<Item title="Splunk" /> |
||||
<Item title="Service Now" /> |
||||
<Item title="Dynatrace" /> |
||||
<Item title="DataDog" /> |
||||
<Item title="AppDynamics" /> |
||||
</List> |
||||
</Item> |
||||
</List> |
||||
); |
||||
}; |
||||
|
||||
interface ListProps { |
||||
nested?: boolean; |
||||
} |
||||
|
||||
const List: React.FC<ListProps> = ({ children, nested }) => { |
||||
const listStyle = css` |
||||
display: flex; |
||||
flex-direction: column; |
||||
padding-top: 8px; |
||||
|
||||
> div { |
||||
margin-bottom: ${nested ? 0 : 8}px; |
||||
} |
||||
`;
|
||||
|
||||
return <div className={listStyle}>{children}</div>; |
||||
}; |
||||
|
||||
interface ItemProps { |
||||
title: string; |
||||
image?: string; |
||||
} |
||||
|
||||
const Item: React.FC<ItemProps> = ({ children, title, image }) => { |
||||
const imageUrl = image ? image : '/public/img/licensing/checkmark.svg'; |
||||
const itemStyle = css` |
||||
display: flex; |
||||
|
||||
> img { |
||||
display: block; |
||||
height: 22px; |
||||
flex-grow: 0; |
||||
padding-right: 12px; |
||||
} |
||||
`;
|
||||
const titleStyle = css` |
||||
font-weight: 500; |
||||
line-height: 1.7; |
||||
`;
|
||||
|
||||
return ( |
||||
<div className={itemStyle}> |
||||
<img src={imageUrl} /> |
||||
<div> |
||||
<div className={titleStyle}>{title}</div> |
||||
{children} |
||||
</div> |
||||
</div> |
||||
); |
||||
}; |
||||
|
||||
const mapStateToProps = (state: StoreState) => ({ |
||||
navModel: getNavModel(state.navIndex, 'upgrading'), |
||||
}); |
||||
|
||||
export default hot(module)(connect(mapStateToProps)(UpgradePage)); |
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 2.6 KiB |
After Width: | Height: | Size: 2.7 KiB |
After Width: | Height: | Size: 6.7 KiB |
After Width: | Height: | Size: 6.5 KiB |
After Width: | Height: | Size: 2.5 KiB |
After Width: | Height: | Size: 3.6 KiB |