import { css } from '@emotion/css'; import React, { FormEvent, useState } from 'react'; import { dateTimeFormat, GrafanaTheme2 } from '@grafana/data'; import { locationService } from '@grafana/runtime'; import { Alert, Button, LinkButton, useStyles2, Icon } from '@grafana/ui'; import { contextSrv } from 'app/core/services/context_srv'; import { UpgradeInfo } from 'app/features/admin/UpgradePage'; import { Loader } from 'app/features/plugins/admin/components/Loader'; import { AccessControlAction } from '../types'; import { CustomerSupportButton } from './CustomerSupportButton'; import { CardAlert, CardContent, CardState, LicenseCard } from './LicenseCard'; import { LicenseTokenUpload } from './LicenseTokenUpload'; import { EXPIRED, VALID, WARNING_RATE, LIMIT_BY_USERS, AWS_MARKEPLACE_ISSUER } from './constants'; import { postLicenseToken, renewLicenseToken } from './state/api'; import { ActiveUserStats, LicenseToken } from './types'; import { getRate, getTokenStatus, getTrialStatus, getUserStatMessage, getUtilStatus } from './utils'; export interface Props { token: LicenseToken | null; stats: ActiveUserStats | null; tokenRenewed?: boolean; tokenUpdated?: boolean; isLoading?: boolean; licensedUrl?: string; } export const LicenseInfo = ({ token, stats, tokenRenewed, tokenUpdated, isLoading, licensedUrl }: Props) => { const [isUploading, setIsUploading] = useState(false); const [isRenewing, setIsRenewing] = useState(false); const tokenState = getTokenStatus(token).state; const utilState = getUtilStatus(token, stats).state; const isLicensingEditor = contextSrv.hasAccess(AccessControlAction.LicensingWrite, contextSrv.isGrafanaAdmin); const styles = useStyles2(getStyles); const onFileUpload = (event: FormEvent) => { const file = event.currentTarget?.files?.[0]; if (file) { locationService.partial({ tokenUpdated: null, tokenRenewed: null }); const reader = new FileReader(); const readerOnLoad = () => { return async (e: any) => { setIsUploading(true); try { await postLicenseToken(e.target.result); locationService.partial({ tokenUpdated: true }); setTimeout(() => { // reload from server to pick up the new token location.reload(); }, 1000); } catch (error) { setIsUploading(false); throw error; } }; }; reader.onload = readerOnLoad(); reader.readAsText(file); } }; const onRenewClick = async () => { locationService.partial({ tokenUpdated: null, tokenRenewed: null }); setIsRenewing(true); try { await renewLicenseToken(); locationService.partial({ tokenRenewed: true }); setTimeout(() => { // reload from server to pick up the new token location.reload(); }, 1000); } catch (error) { setIsRenewing(false); throw error; } }; if (!contextSrv.hasAccess(AccessControlAction.LicensingRead, contextSrv.isGrafanaAdmin)) { return null; } if (isLoading) { return ; } let editionNotice = 'You are running Grafana Enterprise without a license. The Enterprise features are disabled.'; if (token && ![VALID, EXPIRED].includes(token.status)) { editionNotice = 'There is a problem with your Enterprise license token. The Enterprise features are disabled.'; } return !token || ![VALID, EXPIRED].includes(token.status) ? ( <>
) : (

License details

{tokenUpdated && ( locationService.partial({ tokenUpdated: null })} /> )} {tokenRenewed && ( locationService.partial({ tokenRenewed: null })} /> )}
License details } > {token.prod?.map((product) => (
  • {product}
  • ))} ), }, token.iss === AWS_MARKEPLACE_ISSUER && token.account ? { name: 'AWS Account', value: token.account } : { name: 'Company', value: token.company }, { name: 'License ID', value: token.lid }, token.iss === AWS_MARKEPLACE_ISSUER ? null : { name: 'URL', value: token.sub, tooltip: 'License URL is the root URL of your Grafana instance. The license will not work on an instance of Grafana with a different root URL.', }, { name: 'Purchase date', value: dateTimeFormat(token.nbf * 1000) }, token.iss === AWS_MARKEPLACE_ISSUER ? null : { name: 'License expires', value: dateTimeFormat(token.lexp * 1000), highlight: !!getTokenStatus(token)?.state, tooltip: 'The license expiration date is the date when the current license is no longer active. As the license expiration date approaches, Grafana Enterprise displays a banner.', }, token.iss === AWS_MARKEPLACE_ISSUER ? null : { name: 'Usage billing', value: token.usage_billing ? 'On' : 'Off', tooltip: 'You can request Grafana Labs to turn on usage billing to allow an unlimited number of active users. When usage billing is enabled, Grafana does not enforce active user limits or display warning banners. Instead, you are charged for active users above the limit, according to your customer contract.', }, ]} />
    {token.iss !== AWS_MARKEPLACE_ISSUER && ( )} {isRenewing ? ( (Renewing...) ) : ( )}
    } > <> {tokenState && ( )}
    Read about{' '} license expiration {' '} and{' '} license activation .
    Utilization of licenced users is determined based on signed-in users' activity in the past 30 days. } > <>
    Read about{' '} active user limits {' '} and{' '} concurrent session limits .
    {token.limit_by === LIMIT_BY_USERS && ( = WARNING_RATE, }, ]} state={utilState} /> )}
    ); }; const getStyles = (theme: GrafanaTheme2) => { return { title: css` margin: ${theme.spacing(4)} 0; `, infoText: css` font-size: ${theme.typography.size.sm}; `, uploadWrapper: css` margin-left: 79px; `, row: css` display: flex; justify-content: space-between; width: 100%; flex-wrap: wrap; gap: ${theme.spacing(2)}; & > div { flex: 1 1 340px; } `, footerText: css` margin-bottom: ${theme.spacing(2)}; `, licenseCard: css` background: url('/public/img/licensing/card-bg-${theme.isLight ? 'light' : 'dark'}.svg') center no-repeat; background-size: cover; `, message: css` height: 70px; a { color: ${theme.colors.text.link}; &:hover { text-decoration: underline; } } svg { margin-right: ${theme.spacing(0.5)}; } `, }; }; type PageAlertProps = { state?: CardState; message?: string; title: string; orgSlug: string; licenseId: string; }; const PageAlert = ({ state, message, title, orgSlug, licenseId }: PageAlertProps) => { const styles = useStyles2(getPageAlertStyles); if (!state) { return null; } return (
    ); }; const getPageAlertStyles = (theme: GrafanaTheme2) => { return { container: css` display: flex; align-items: flex-start; justify-content: space-between; width: 100%; `, link: css` font-size: ${theme.typography.bodySmall.fontSize}; text-decoration: underline; color: ${theme.colors.text.secondary}; &:hover { color: ${theme.colors.text.primary}; } `, }; };