123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413 |
- 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<HTMLInputElement>) => {
- 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 <Loader text={'Loading licensing info...'} />;
- }
- 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) ? (
- <>
- <UpgradeInfo editionNotice={editionNotice} />
- <div className={styles.uploadWrapper}>
- <LicenseTokenUpload
- title="Have a license?"
- onFileUpload={onFileUpload}
- isUploading={isUploading}
- isDisabled={!isLicensingEditor}
- licensedUrl={licensedUrl}
- />
- </div>
- </>
- ) : (
- <div>
- <h2 className={styles.title}>License details</h2>
- <PageAlert {...getUtilStatus(token, stats)} orgSlug={token.slug} licenseId={token.lid} />
- <PageAlert {...getTokenStatus(token)} orgSlug={token.slug} licenseId={token.lid} />
- <PageAlert {...getTrialStatus(token)} orgSlug={token.slug} licenseId={token.lid} />
- {tokenUpdated && (
- <Alert
- title="License token uploaded. Restart Grafana for the changes to take effect."
- severity="success"
- onRemove={() => locationService.partial({ tokenUpdated: null })}
- />
- )}
- {tokenRenewed && (
- <Alert
- title="License token renewed."
- severity="success"
- onRemove={() => locationService.partial({ tokenRenewed: null })}
- />
- )}
- <div className={styles.row}>
- <LicenseCard
- title={'License'}
- className={styles.licenseCard}
- footer={
- <LinkButton
- variant="secondary"
- href={token.details_url || `${token.iss}/licenses/${token.lid}`}
- aria-label="View details about your license in Grafana Cloud"
- target="_blank"
- rel="noopener noreferrer"
- >
- License details
- </LinkButton>
- }
- >
- <CardContent
- content={[
- {
- name: token.prod?.length <= 1 ? 'Product' : 'Products',
- value:
- token.prod?.length <= 1 ? (
- token.prod[0] || 'None'
- ) : (
- <ul>
- {token.prod?.map((product) => (
- <li key={product}>{product}</li>
- ))}
- </ul>
- ),
- },
- 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.',
- },
- ]}
- />
- </LicenseCard>
- <LicenseCard
- {...getTokenStatus(token)}
- title={'Token'}
- footer={
- <div className={styles.row}>
- {token.iss !== AWS_MARKEPLACE_ISSUER && (
- <LicenseTokenUpload
- onFileUpload={onFileUpload}
- isUploading={isUploading}
- isDisabled={!isLicensingEditor}
- />
- )}
- {isRenewing ? (
- <span> (Renewing...)</span>
- ) : (
- <Button variant="secondary" onClick={onRenewClick} disabled={!isLicensingEditor}>
- Renew token
- </Button>
- )}
- </div>
- }
- >
- <>
- {tokenState && (
- <CardAlert
- title={'Contact support to renew your token, or visit the Cloud portal to learn more.'}
- state={tokenState}
- orgSlug={token.slug}
- licenseId={token.lid}
- />
- )}
- <div className={styles.message}>
- <Icon name={'document-info'} />
- Read about{' '}
- <a
- href={'https://grafana.com/docs/grafana/latest/enterprise/license/license-expiration/'}
- target="_blank"
- rel="noreferrer noopener"
- >
- license expiration
- </a>{' '}
- and{' '}
- <a
- href={'https://grafana.com/docs/grafana/latest/enterprise/license/activate-license/'}
- target="_blank"
- rel="noreferrer noopener"
- >
- license activation
- </a>
- .
- </div>
- <CardContent
- content={[
- { name: 'Token ID', value: token.jti },
- { name: 'Token issued', value: dateTimeFormat(token.iat * 1000) },
- {
- name: 'Token expires',
- value: dateTimeFormat(token.exp * 1000),
- highlight: !!getTokenStatus(token)?.state,
- tooltip:
- 'Grafana automatically updates the token before it expires. If your token is not updating, contact support.',
- },
- ]}
- state={tokenState}
- />
- </>
- </LicenseCard>
- <LicenseCard
- {...getUtilStatus(token, stats)}
- title={'Utilization'}
- footer={
- <small className={styles.footerText}>
- Utilization of licenced users is determined based on signed-in users' activity in the past 30 days.
- </small>
- }
- >
- <>
- <div className={styles.message}>
- <Icon name={'document-info'} />
- Read about{' '}
- <a
- href={
- 'https://grafana.com/docs/grafana/latest/enterprise/license/license-restrictions/#active-users-limit'
- }
- target="_blank"
- rel="noreferrer noopener"
- >
- active user limits
- </a>{' '}
- and{' '}
- <a
- href={
- 'https://grafana.com/docs/grafana/latest/enterprise/license/license-restrictions/#concurrent-sessions-limit'
- }
- target="_blank"
- rel="noreferrer noopener"
- >
- concurrent session limits
- </a>
- .
- </div>
- {token.limit_by === LIMIT_BY_USERS && (
- <CardContent
- content={[
- {
- name: 'Users',
- value: getUserStatMessage(token.included_users, stats?.active_users),
- highlight: getRate(token.included_users, stats?.active_users) >= WARNING_RATE,
- },
- ]}
- state={utilState}
- />
- )}
- </>
- </LicenseCard>
- </div>
- </div>
- );
- };
- 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 (
- <Alert title={title} severity={state || undefined}>
- <div className={styles.container}>
- <div>
- <p>{message}</p>
- <a
- className={styles.link}
- href={'https://grafana.com/docs/grafana/latest/enterprise/license/license-restrictions/'}
- target="_blank"
- rel="noopener noreferrer"
- >
- Learn about Enterprise licenses
- </a>
- </div>
- <CustomerSupportButton orgSlug={orgSlug} licenseId={licenseId} />
- </div>
- </Alert>
- );
- };
- 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};
- }
- `,
- };
- };
|