ServerStats.tsx 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193
  1. import { css } from '@emotion/css';
  2. import React, { useEffect, useState } from 'react';
  3. import { GrafanaTheme2 } from '@grafana/data';
  4. import { config } from '@grafana/runtime';
  5. import { CardContainer, LinkButton, useStyles2 } from '@grafana/ui';
  6. import { AccessControlAction } from 'app/types';
  7. import { contextSrv } from '../../core/services/context_srv';
  8. import { Loader } from '../plugins/admin/components/Loader';
  9. import { CrawlerStatus } from './CrawlerStatus';
  10. import { ExportStatus } from './ExportStatus';
  11. import { getServerStats, ServerStat } from './state/apis';
  12. export const ServerStats = () => {
  13. const [stats, setStats] = useState<ServerStat | null>(null);
  14. const [isLoading, setIsLoading] = useState(false);
  15. const styles = useStyles2(getStyles);
  16. const hasAccessToDataSources = contextSrv.hasAccess(AccessControlAction.DataSourcesRead, contextSrv.isGrafanaAdmin);
  17. const hasAccessToAdminUsers = contextSrv.hasAccess(AccessControlAction.UsersRead, contextSrv.isGrafanaAdmin);
  18. useEffect(() => {
  19. if (contextSrv.hasAccess(AccessControlAction.ActionServerStatsRead, contextSrv.isGrafanaAdmin)) {
  20. setIsLoading(true);
  21. getServerStats().then((stats) => {
  22. setStats(stats);
  23. setIsLoading(false);
  24. });
  25. }
  26. }, []);
  27. if (!contextSrv.hasAccess(AccessControlAction.ActionServerStatsRead, contextSrv.isGrafanaAdmin)) {
  28. return null;
  29. }
  30. return (
  31. <>
  32. <h2 className={styles.title}>Instance statistics</h2>
  33. {isLoading ? (
  34. <div className={styles.loader}>
  35. <Loader text={'Loading instance stats...'} />
  36. </div>
  37. ) : stats ? (
  38. <div className={styles.row}>
  39. <StatCard
  40. content={[
  41. { name: 'Dashboards (starred)', value: `${stats.dashboards} (${stats.stars})` },
  42. { name: 'Tags', value: stats.tags },
  43. { name: 'Playlists', value: stats.playlists },
  44. { name: 'Snapshots', value: stats.snapshots },
  45. ]}
  46. footer={
  47. <LinkButton href={'/dashboards'} variant={'secondary'}>
  48. Manage dashboards
  49. </LinkButton>
  50. }
  51. />
  52. <div className={styles.doubleRow}>
  53. <StatCard
  54. content={[{ name: 'Data sources', value: stats.datasources }]}
  55. footer={
  56. hasAccessToDataSources && (
  57. <LinkButton href={'/datasources'} variant={'secondary'}>
  58. Manage data sources
  59. </LinkButton>
  60. )
  61. }
  62. />
  63. <StatCard
  64. content={[{ name: 'Alerts', value: stats.alerts }]}
  65. footer={
  66. <LinkButton href={'/alerting/list'} variant={'secondary'}>
  67. Alerts
  68. </LinkButton>
  69. }
  70. />
  71. </div>
  72. <StatCard
  73. content={[
  74. { name: 'Organisations', value: stats.orgs },
  75. { name: 'Users total', value: stats.users },
  76. { name: 'Active users in last 30 days', value: stats.activeUsers },
  77. { name: 'Active sessions', value: stats.activeSessions },
  78. ]}
  79. footer={
  80. hasAccessToAdminUsers && (
  81. <LinkButton href={'/admin/users'} variant={'secondary'}>
  82. Manage users
  83. </LinkButton>
  84. )
  85. }
  86. />
  87. </div>
  88. ) : (
  89. <p className={styles.notFound}>No stats found.</p>
  90. )}
  91. {config.featureToggles.dashboardPreviews && config.featureToggles.dashboardPreviewsAdmin && <CrawlerStatus />}
  92. {config.featureToggles.export && <ExportStatus />}
  93. </>
  94. );
  95. };
  96. const getStyles = (theme: GrafanaTheme2) => {
  97. return {
  98. title: css`
  99. margin-bottom: ${theme.spacing(4)};
  100. `,
  101. row: css`
  102. display: flex;
  103. justify-content: space-between;
  104. width: 100%;
  105. & > div:not(:last-of-type) {
  106. margin-right: ${theme.spacing(2)};
  107. }
  108. & > div {
  109. width: 33.3%;
  110. }
  111. `,
  112. doubleRow: css`
  113. display: flex;
  114. flex-direction: column;
  115. & > div:first-of-type {
  116. margin-bottom: ${theme.spacing(2)};
  117. }
  118. `,
  119. loader: css`
  120. height: 290px;
  121. `,
  122. notFound: css`
  123. font-size: ${theme.typography.h6.fontSize};
  124. text-align: center;
  125. height: 290px;
  126. `,
  127. };
  128. };
  129. type StatCardProps = {
  130. content: Array<Record<string, number | string>>;
  131. footer?: JSX.Element | boolean;
  132. };
  133. const StatCard = ({ content, footer }: StatCardProps) => {
  134. const styles = useStyles2(getCardStyles);
  135. return (
  136. <CardContainer className={styles.container} disableHover>
  137. <div className={styles.inner}>
  138. <div className={styles.content}>
  139. {content.map((item) => {
  140. return (
  141. <div key={item.name} className={styles.row}>
  142. <span>{item.name}</span>
  143. <span>{item.value}</span>
  144. </div>
  145. );
  146. })}
  147. </div>
  148. {footer && <div>{footer}</div>}
  149. </div>
  150. </CardContainer>
  151. );
  152. };
  153. const getCardStyles = (theme: GrafanaTheme2) => {
  154. return {
  155. container: css`
  156. padding: ${theme.spacing(2)};
  157. `,
  158. inner: css`
  159. display: flex;
  160. flex-direction: column;
  161. width: 100%;
  162. `,
  163. content: css`
  164. flex: 1 0 auto;
  165. `,
  166. row: css`
  167. display: flex;
  168. justify-content: space-between;
  169. width: 100%;
  170. margin-bottom: ${theme.spacing(2)};
  171. align-items: center;
  172. `,
  173. };
  174. };