LicenseCard.tsx 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199
  1. import { css, cx } from '@emotion/css';
  2. import React from 'react';
  3. import { GrafanaTheme2 } from '@grafana/data';
  4. import { Alert, CardContainer, Icon, IconName, stylesFactory, Tooltip, useStyles2, useTheme2 } from '@grafana/ui';
  5. import { CustomerSupportButton } from './CustomerSupportButton';
  6. export type CardState = 'warning' | 'error' | 'info' | '';
  7. export interface Props {
  8. title?: string;
  9. footer?: JSX.Element;
  10. state?: CardState;
  11. status?: string;
  12. children: JSX.Element;
  13. className?: string;
  14. }
  15. export const LicenseCard = ({ title, children, footer, state, status, className }: Props) => {
  16. const theme = useTheme2();
  17. const styles = getStyles(theme, state);
  18. return (
  19. <CardContainer className={cx(styles.container, className)} disableHover>
  20. <div className={styles.inner}>
  21. {(title || status) && (
  22. <div className={styles.row}>
  23. {title && <h3 className={styles.title}>{title}</h3>}
  24. {status && <CardStatus state={state} status={status} />}
  25. </div>
  26. )}
  27. <div className={styles.content}>{children}</div>
  28. {footer && <div className={styles.footer}>{footer}</div>}
  29. </div>
  30. </CardContainer>
  31. );
  32. };
  33. const getStyles = stylesFactory((theme: GrafanaTheme2, state?: CardState) => {
  34. return {
  35. container: css`
  36. padding: ${theme.spacing(2)};
  37. margin-bottom: 0;
  38. ${state && `border: 1px solid ${theme.colors[state].border}`};
  39. `,
  40. inner: css`
  41. display: flex;
  42. flex-direction: column;
  43. width: 100%;
  44. `,
  45. title: css`
  46. font-size: ${theme.typography.h3.fontSize};
  47. color: ${theme.colors.text.secondary};
  48. margin-bottom: 0;
  49. `,
  50. row: css`
  51. display: flex;
  52. justify-content: space-between;
  53. width: 100%;
  54. margin-bottom: ${theme.spacing(2)};
  55. align-items: center;
  56. `,
  57. content: css`
  58. display: flex;
  59. flex-direction: column;
  60. flex: 1 0 auto;
  61. width: 100%;
  62. `,
  63. footer: css`
  64. display: flex;
  65. flex-direction: column;
  66. align-items: self-start;
  67. margin-top: ${theme.spacing(2)};
  68. `,
  69. };
  70. });
  71. export type CardStatusProps = {
  72. state?: CardState;
  73. status?: string;
  74. };
  75. export const CardStatus = ({ state, status }: CardStatusProps) => {
  76. const iconMap = new Map<CardState, IconName>([
  77. ['warning', 'bell'],
  78. ['error', 'exclamation-triangle'],
  79. ]);
  80. const theme = useTheme2();
  81. const styles = getStatusStyles(theme, state);
  82. if (state && iconMap.has(state)) {
  83. return (
  84. <div role={'alert'} className={styles.container}>
  85. <Icon name={iconMap.get(state)!} />
  86. {status}
  87. </div>
  88. );
  89. }
  90. return null;
  91. };
  92. const getStatusStyles = (theme: GrafanaTheme2, state?: CardState) => {
  93. return {
  94. container: css`
  95. background-color: ${theme.colors.background.primary};
  96. color: ${state && theme.colors[state].text};
  97. padding: ${theme.spacing(0.5)} ${theme.spacing(1)};
  98. svg {
  99. margin-right: ${theme.spacing(1)};
  100. }
  101. `,
  102. };
  103. };
  104. type CardAlertProps = {
  105. state?: CardState;
  106. title: string;
  107. orgSlug: string;
  108. licenseId: string;
  109. };
  110. export const CardAlert = ({ state, title, orgSlug, licenseId }: CardAlertProps) => {
  111. const styles = useStyles2(getAlertStyles);
  112. return state ? (
  113. <Alert title={title} severity={state} className={styles.container}>
  114. <CustomerSupportButton orgSlug={orgSlug} licenseId={licenseId} />
  115. </Alert>
  116. ) : null;
  117. };
  118. const getAlertStyles = (theme: GrafanaTheme2) => {
  119. return {
  120. container: css`
  121. background: ${theme.colors.secondary.main};
  122. margin-bottom: ${theme.spacing(3)};
  123. `,
  124. };
  125. };
  126. type CardContentItem = {
  127. name: string;
  128. value: string | number | JSX.Element;
  129. highlight?: boolean;
  130. tooltip?: string;
  131. };
  132. export const CardContent = ({ content, state }: { content: Array<CardContentItem | null>; state?: CardState }) => {
  133. const theme = useTheme2();
  134. const styles = getContentStyles(theme, state);
  135. return (
  136. <>
  137. {content.map((item) => {
  138. if (!item) {
  139. return null;
  140. }
  141. return (
  142. <div key={item.name} className={styles.row}>
  143. <span>
  144. {item.name}
  145. {item.tooltip && (
  146. <Tooltip placement="top" content={item.tooltip} theme={'info'}>
  147. <Icon name="info-circle" size="sm" />
  148. </Tooltip>
  149. )}
  150. </span>
  151. <span className={item.highlight ? styles.highlight : ''}>{item.value}</span>
  152. </div>
  153. );
  154. })}
  155. </>
  156. );
  157. };
  158. const getContentStyles = (theme: GrafanaTheme2, state?: CardState) => {
  159. return {
  160. row: css`
  161. display: flex;
  162. justify-content: space-between;
  163. width: 100%;
  164. margin-bottom: ${theme.spacing(2)};
  165. align-items: center;
  166. svg {
  167. margin-left: ${theme.spacing(0.5)};
  168. cursor: pointer;
  169. }
  170. `,
  171. highlight: css`
  172. background-color: ${theme.colors.background.primary};
  173. color: ${state && theme.colors[state].text};
  174. padding: ${theme.spacing(0.5)} ${theme.spacing(1)};
  175. margin-right: -${theme.spacing(1)};
  176. svg {
  177. margin-right: ${theme.spacing(1)};
  178. }
  179. `,
  180. };
  181. };