From 29687903f87d904fd4fe48ba6ad0b3ab84ea15a4 Mon Sep 17 00:00:00 2001 From: Emil Tullstedt Date: Wed, 15 Jan 2020 14:50:44 +0100 Subject: [PATCH] Admin: Add promotional page for Grafana Enterprise (#21422) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Torkel Ödegaard --- .../grafana-ui/src/components/Forms/index.ts | 3 +- pkg/api/login_test.go | 11 +- pkg/extensions/main.go | 4 +- pkg/infra/usagestats/usage_stats_test.go | 3 +- pkg/models/licensing.go | 18 -- pkg/services/licensing/oss.go | 41 ++++ public/app/features/admin/LicenseChrome.tsx | 105 ++++++++++ public/app/features/admin/UpgradePage.tsx | 188 ++++++++++++++++++ public/app/routes/routes.ts | 6 + public/img/licensing/checkmark.svg | 3 + public/img/licensing/customer_support.svg | 5 + public/img/licensing/handinhand_support.svg | 11 + public/img/licensing/header_dark.svg | 53 +++++ public/img/licensing/header_light.svg | 52 +++++ public/img/licensing/plugin_enterprise.svg | 5 + public/img/licensing/sla.svg | 12 ++ 16 files changed, 493 insertions(+), 27 deletions(-) create mode 100644 pkg/services/licensing/oss.go create mode 100644 public/app/features/admin/LicenseChrome.tsx create mode 100644 public/app/features/admin/UpgradePage.tsx create mode 100644 public/img/licensing/checkmark.svg create mode 100644 public/img/licensing/customer_support.svg create mode 100644 public/img/licensing/handinhand_support.svg create mode 100644 public/img/licensing/header_dark.svg create mode 100644 public/img/licensing/header_light.svg create mode 100644 public/img/licensing/plugin_enterprise.svg create mode 100644 public/img/licensing/sla.svg diff --git a/packages/grafana-ui/src/components/Forms/index.ts b/packages/grafana-ui/src/components/Forms/index.ts index f6232fa773a..6a37fb7d9a3 100644 --- a/packages/grafana-ui/src/components/Forms/index.ts +++ b/packages/grafana-ui/src/components/Forms/index.ts @@ -4,7 +4,7 @@ import { Input } from './Input/Input'; import { Select } from './Select/Select'; import { Form } from './Form'; import { Field } from './Field'; -import { Button } from './Button'; +import { Button, LinkButton } from './Button'; const Forms = { getFormStyles, @@ -13,6 +13,7 @@ const Forms = { Form, Field, Button, + LinkButton, Select, }; diff --git a/pkg/api/login_test.go b/pkg/api/login_test.go index 05aa4a0345d..09565935400 100644 --- a/pkg/api/login_test.go +++ b/pkg/api/login_test.go @@ -4,6 +4,7 @@ import ( "encoding/hex" "errors" "fmt" + "github.com/grafana/grafana/pkg/services/licensing" "io/ioutil" "net/http" "net/http/httptest" @@ -83,7 +84,7 @@ func TestLoginErrorCookieApiEndpoint(t *testing.T) { sc := setupScenarioContext("/login") hs := &HTTPServer{ Cfg: setting.NewCfg(), - License: models.OSSLicensingService{}, + License: &licensing.OSSLicensingService{}, } sc.defaultHandler = Wrap(func(w http.ResponseWriter, c *models.ReqContext) { @@ -133,7 +134,7 @@ func TestLoginViewRedirect(t *testing.T) { sc := setupScenarioContext("/login") hs := &HTTPServer{ Cfg: setting.NewCfg(), - License: models.OSSLicensingService{}, + License: &licensing.OSSLicensingService{}, } hs.Cfg.CookieSecure = true @@ -247,7 +248,7 @@ func TestLoginPostRedirect(t *testing.T) { hs := &HTTPServer{ log: &FakeLogger{}, Cfg: setting.NewCfg(), - License: models.OSSLicensingService{}, + License: &licensing.OSSLicensingService{}, AuthTokenService: auth.NewFakeUserAuthTokenService(), } hs.Cfg.CookieSecure = true @@ -350,7 +351,7 @@ func TestLoginOAuthRedirect(t *testing.T) { sc := setupScenarioContext("/login") hs := &HTTPServer{ Cfg: setting.NewCfg(), - License: models.OSSLicensingService{}, + License: &licensing.OSSLicensingService{}, } sc.defaultHandler = Wrap(func(c *models.ReqContext) { @@ -408,7 +409,7 @@ func setupAuthProxyLoginTest(enableLoginToken bool) *scenarioContext { sc := setupScenarioContext("/login") hs := &HTTPServer{ Cfg: setting.NewCfg(), - License: models.OSSLicensingService{}, + License: &licensing.OSSLicensingService{}, AuthTokenService: auth.NewFakeUserAuthTokenService(), log: log.New("hello"), } diff --git a/pkg/extensions/main.go b/pkg/extensions/main.go index 18bf50d68bf..a88f67ff9ed 100644 --- a/pkg/extensions/main.go +++ b/pkg/extensions/main.go @@ -6,8 +6,8 @@ import ( _ "github.com/crewjam/saml" _ "github.com/gobwas/glob" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/registry" + "github.com/grafana/grafana/pkg/services/licensing" _ "github.com/jung-kurt/gofpdf" _ "github.com/linkedin/goavro/v2" _ "github.com/pkg/errors" @@ -18,7 +18,7 @@ import ( ) func init() { - registry.RegisterService(&models.OSSLicensingService{}) + registry.RegisterService(&licensing.OSSLicensingService{}) } var IsEnterprise bool = false diff --git a/pkg/infra/usagestats/usage_stats_test.go b/pkg/infra/usagestats/usage_stats_test.go index 39e44332b9f..ffb2d5b5d77 100644 --- a/pkg/infra/usagestats/usage_stats_test.go +++ b/pkg/infra/usagestats/usage_stats_test.go @@ -2,6 +2,7 @@ package usagestats import ( "bytes" + "github.com/grafana/grafana/pkg/services/licensing" "io/ioutil" "runtime" "sync" @@ -25,7 +26,7 @@ func TestMetrics(t *testing.T) { uss := &UsageStatsService{ Bus: bus.New(), SQLStore: sqlstore.InitTestDB(t), - License: models.OSSLicensingService{}, + License: &licensing.OSSLicensingService{}, } var getSystemStatsQuery *models.GetSystemStatsQuery diff --git a/pkg/models/licensing.go b/pkg/models/licensing.go index 691b48c101a..21d62a2546d 100644 --- a/pkg/models/licensing.go +++ b/pkg/models/licensing.go @@ -10,21 +10,3 @@ type Licensing interface { // Expiry returns the unix epoch timestamp when the license expires, or 0 if no valid license is provided Expiry() int64 } - -type OSSLicensingService struct{} - -func (OSSLicensingService) HasLicense() bool { - return false -} - -func (OSSLicensingService) Expiry() int64 { - return 0 -} - -func (OSSLicensingService) Init() error { - return nil -} - -func (OSSLicensingService) HasValidLicense() bool { - return false -} diff --git a/pkg/services/licensing/oss.go b/pkg/services/licensing/oss.go new file mode 100644 index 00000000000..c60cda6dfd1 --- /dev/null +++ b/pkg/services/licensing/oss.go @@ -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 +} diff --git a/public/app/features/admin/LicenseChrome.tsx b/public/app/features/admin/LicenseChrome.tsx new file mode 100644 index 00000000000..526dd620e07 --- /dev/null +++ b/public/app/features/admin/LicenseChrome.tsx @@ -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 = ({ header, editionNotice, subheader, children }) => { + const theme = useTheme(); + const styles = getStyles(theme); + + return ( + <> +
+

{header}

+ {subheader &&

{subheader}

} + + + Grafana + +
+ +
{children}
+ + {editionNotice &&
{editionNotice}
} + + ); +}; + +interface CircleProps { + size: string; + style?: React.CSSProperties; +} + +export const Circle: React.FC = ({ size, style, children }) => { + return ( +
+ {children} +
+ ); +}; diff --git a/public/app/features/admin/UpgradePage.tsx b/public/app/features/admin/UpgradePage.tsx new file mode 100644 index 00000000000..c24692ae11c --- /dev/null +++ b/public/app/features/admin/UpgradePage.tsx @@ -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 = ({ navModel }) => { + return ( + + + + + + ); +}; + +const titleStyles = { fontWeight: 500, fontSize: '26px', lineHeight: '123%' }; + +interface UpgradeInfoProps { + editionNotice?: string; +} + +export const UpgradeInfo: React.FC = ({ editionNotice }) => { + return ( + + + + + ); +}; + +const GetEnterprise: React.FC = () => { + return ( +
+

Get Grafana Enterprise

+ +

+ You can use the trial version for free for 30 days. We will remind you about it{' '} + 5 days before the trial period ends. +

+
+ ); +}; + +const CallToAction: React.FC = () => { + return ( + + Contact us and get a free trial + + ); +}; + +const ServiceInfo: React.FC = () => { + return ( +
+

At your service

+ + + + + + 24x7x365 support via + + + + + + + + in the upgrade process + + + +
+ Also included: +
+ Indemnification, working with Grafana Labs on future prioritization, and training from the core Grafana team. +
+
+ ); +}; + +const FeatureInfo: React.FC = () => { + return ( +
+

Enhanced Functionality

+ + + +
+ ); +}; + +const FeatureListing: React.FC = () => { + return ( + + + + + + LDAP, GitHub OAuth, Auth Proxy + + + + + + + + + + + + + ); +}; + +interface ListProps { + nested?: boolean; +} + +const List: React.FC = ({ children, nested }) => { + const listStyle = css` + display: flex; + flex-direction: column; + padding-top: 8px; + + > div { + margin-bottom: ${nested ? 0 : 8}px; + } + `; + + return
{children}
; +}; + +interface ItemProps { + title: string; + image?: string; +} + +const Item: React.FC = ({ 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 ( +
+ +
+
{title}
+ {children} +
+
+ ); +}; + +const mapStateToProps = (state: StoreState) => ({ + navModel: getNavModel(state.navIndex, 'upgrading'), +}); + +export default hot(module)(connect(mapStateToProps)(UpgradePage)); diff --git a/public/app/routes/routes.ts b/public/app/routes/routes.ts index 7966be16bee..bde0cc7dd39 100644 --- a/public/app/routes/routes.ts +++ b/public/app/routes/routes.ts @@ -290,6 +290,12 @@ export function setupAngularRoutes($routeProvider: route.IRouteProvider, $locati SafeDynamicImport(import(/* webpackChunkName: "AdminSettings" */ 'app/features/admin/AdminSettings')), }, }) + .when('/admin/upgrading', { + template: '', + resolve: { + component: () => SafeDynamicImport(import('app/features/admin/UpgradePage')), + }, + }) .when('/admin/users', { templateUrl: 'public/app/features/admin/partials/users.html', controller: 'AdminListUsersCtrl', diff --git a/public/img/licensing/checkmark.svg b/public/img/licensing/checkmark.svg new file mode 100644 index 00000000000..9c0eff35e85 --- /dev/null +++ b/public/img/licensing/checkmark.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/img/licensing/customer_support.svg b/public/img/licensing/customer_support.svg new file mode 100644 index 00000000000..c3c92b74aa3 --- /dev/null +++ b/public/img/licensing/customer_support.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/img/licensing/handinhand_support.svg b/public/img/licensing/handinhand_support.svg new file mode 100644 index 00000000000..b83b84240b2 --- /dev/null +++ b/public/img/licensing/handinhand_support.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/public/img/licensing/header_dark.svg b/public/img/licensing/header_dark.svg new file mode 100644 index 00000000000..1643277ba49 --- /dev/null +++ b/public/img/licensing/header_dark.svg @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/img/licensing/header_light.svg b/public/img/licensing/header_light.svg new file mode 100644 index 00000000000..6ddc58e3e15 --- /dev/null +++ b/public/img/licensing/header_light.svg @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/img/licensing/plugin_enterprise.svg b/public/img/licensing/plugin_enterprise.svg new file mode 100644 index 00000000000..c48ebb0cfd4 --- /dev/null +++ b/public/img/licensing/plugin_enterprise.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/img/licensing/sla.svg b/public/img/licensing/sla.svg new file mode 100644 index 00000000000..8e40621099e --- /dev/null +++ b/public/img/licensing/sla.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + +