analytics.js 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328
  1. import ApolloClient, { gql } from "apollo-boost"
  2. import React from "react"
  3. import ReactDOM from "react-dom"
  4. import Chart from "react-apexcharts"
  5. import { ApolloProvider, Query } from "react-apollo"
  6. import moment from "moment"
  7. const initAnalytics = ({ elementId, errorMessage, labels, title, uri }) => {
  8. const element = document.getElementById(elementId)
  9. if (!element) console.error("Element with id " + element + "doesn't exist!")
  10. const client = new ApolloClient({
  11. credentials: "same-origin",
  12. uri: uri
  13. })
  14. ReactDOM.render(
  15. <ApolloProvider client={client}>
  16. <Analytics errorMessage={errorMessage} labels={labels} title={title} />
  17. </ApolloProvider>,
  18. element
  19. )
  20. }
  21. const getAnalytics = gql`
  22. query getAnalytics($span: Int!) {
  23. analytics(span: $span) {
  24. users {
  25. ...data
  26. }
  27. userDeletions {
  28. ...data
  29. }
  30. threads {
  31. ...data
  32. }
  33. posts {
  34. ...data
  35. }
  36. attachments {
  37. ...data
  38. }
  39. dataDownloads {
  40. ...data
  41. }
  42. }
  43. }
  44. fragment data on AnalyticsData {
  45. current
  46. currentCumulative
  47. previous
  48. previousCumulative
  49. }
  50. `
  51. class Analytics extends React.Component {
  52. state = { span: 30 }
  53. setSpan = span => {
  54. this.setState({ span })
  55. }
  56. render() {
  57. const { errorMessage, labels, title } = this.props
  58. const { span } = this.state
  59. return (
  60. <div className="card card-admin-info">
  61. <div className="card-body">
  62. <div className="row align-items-center">
  63. <div className="col">
  64. <h4 className="card-title">{title}</h4>
  65. </div>
  66. <div className="col-auto">
  67. <SpanPicker span={span} setSpan={this.setSpan} />
  68. </div>
  69. </div>
  70. </div>
  71. <Query query={getAnalytics} variables={{ span }}>
  72. {({ loading, error, data }) => {
  73. if (loading) return <Spinner />
  74. if (error) return <Error message={errorMessage} />
  75. const { analytics } = data
  76. return (
  77. <>
  78. <AnalyticsItem
  79. data={analytics.threads}
  80. name={labels.threads}
  81. span={span}
  82. />
  83. <AnalyticsItem
  84. data={analytics.posts}
  85. name={labels.posts}
  86. span={span}
  87. />
  88. <AnalyticsItem
  89. data={analytics.attachments}
  90. name={labels.attachments}
  91. span={span}
  92. />
  93. <AnalyticsItem
  94. data={analytics.users}
  95. name={labels.users}
  96. span={span}
  97. />
  98. <AnalyticsItem
  99. data={analytics.userDeletions}
  100. name={labels.userDeletions}
  101. negative={true}
  102. span={span}
  103. />
  104. <AnalyticsItem
  105. data={analytics.dataDownloads}
  106. name={labels.dataDownloads}
  107. span={span}
  108. />
  109. </>
  110. )
  111. }}
  112. </Query>
  113. </div>
  114. )
  115. }
  116. }
  117. const SpanPicker = ({ span, setSpan }) => (
  118. <div>
  119. {[30, 90, 180, 360].map(choice => (
  120. <button
  121. key={choice}
  122. className={
  123. choice === span
  124. ? "btn btn-primary btn-sm ml-3"
  125. : "btn btn-light btn-sm ml-3"
  126. }
  127. type="button"
  128. onClick={() => setSpan(choice)}
  129. >
  130. {choice}
  131. </button>
  132. ))}
  133. </div>
  134. )
  135. const Spinner = () => (
  136. <div className="card-body border-top">
  137. <div className="text-center py-5">
  138. <div className="spinner-border text-light" role="status">
  139. <span className="sr-only">Loading...</span>
  140. </div>
  141. </div>
  142. </div>
  143. )
  144. const Error = ({ message }) => (
  145. <div className="card-body border-top">
  146. <div className="text-center py-5">{message}</div>
  147. </div>
  148. )
  149. const CURRENT = "C"
  150. const PREVIOUS = "P"
  151. const CURRENT_SERIES = 0
  152. const AnalyticsItem = ({ data, legend, name, negative, span }) => {
  153. const options = {
  154. legend: {
  155. show: false
  156. },
  157. chart: {
  158. animations: {
  159. enabled: false
  160. },
  161. parentHeightOffset: 0,
  162. toolbar: {
  163. show: false
  164. }
  165. },
  166. colors: ["#6554c0", "#b3d4ff"],
  167. grid: {
  168. padding: {
  169. top: 0
  170. }
  171. },
  172. stroke: {
  173. width: 2
  174. },
  175. tooltip: {
  176. x: {
  177. show: false
  178. },
  179. y: {
  180. title: {
  181. formatter: function(series, { dataPointIndex }) {
  182. const now = moment()
  183. if (series === PREVIOUS) now.subtract(span, "days")
  184. now.subtract(span - dataPointIndex - 1, "days")
  185. return now.format("ll")
  186. }
  187. },
  188. formatter: function(value, { dataPointIndex, seriesIndex }) {
  189. if (seriesIndex === CURRENT_SERIES) {
  190. return data.current[dataPointIndex]
  191. }
  192. return data.previous[dataPointIndex]
  193. }
  194. }
  195. },
  196. xaxis: {
  197. axisBorder: {
  198. show: false
  199. },
  200. axisTicks: {
  201. show: false
  202. },
  203. labels: {
  204. show: false
  205. },
  206. categories: [],
  207. tooltip: {
  208. enabled: false
  209. }
  210. },
  211. yaxis: {
  212. tickAmount: 2,
  213. max: m => m || 1,
  214. show: false
  215. }
  216. }
  217. const series = [
  218. { name: CURRENT, data: data.currentCumulative },
  219. { name: PREVIOUS, data: data.previousCumulative }
  220. ]
  221. return (
  222. <div className="card-body border-top pb-1">
  223. <h5 className="m-0">{name}</h5>
  224. <div className="row align-items-center">
  225. <div className="col-auto">
  226. <Summary data={data} negative={negative} />
  227. </div>
  228. <div className="col">
  229. <ChartContainer>
  230. {({ width }) =>
  231. width > 1 && (
  232. <Chart
  233. options={options}
  234. series={series}
  235. type="line"
  236. width={width}
  237. height={140}
  238. />
  239. )
  240. }
  241. </ChartContainer>
  242. </div>
  243. </div>
  244. </div>
  245. )
  246. }
  247. const Summary = ({ data, negative }) => {
  248. const current = data.current.reduce((a, b) => a + b)
  249. const previous = data.previous.reduce((a, b) => a + b)
  250. const diff = current - previous
  251. let color = "text-light"
  252. let icon = "fas fa-equals"
  253. if (negative) {
  254. if (diff > 0) color = "text-danger"
  255. if (diff < 0) color = "text-success"
  256. } else {
  257. if (diff > 0) color = "text-success"
  258. if (diff < 0) color = "text-danger"
  259. }
  260. if (diff > 0) {
  261. icon = "fas fa-chevron-up"
  262. }
  263. if (diff < 0) {
  264. icon = "fas fa-chevron-down"
  265. }
  266. return (
  267. <div className="card-admin-analytics-summary">
  268. <div>{current}</div>
  269. <small className={color}>
  270. <span className={icon} /> {Math.abs(diff)}
  271. </small>
  272. </div>
  273. )
  274. }
  275. class ChartContainer extends React.Component {
  276. state = { width: 1, height: 1 }
  277. element = React.createRef()
  278. componentDidMount() {
  279. this.timer = window.setInterval(this.updateSize, 3000)
  280. this.updateSize()
  281. }
  282. componentWillUnmount() {
  283. window.clearInterval(this.timer)
  284. }
  285. updateSize = () => {
  286. this.setState({
  287. width: this.element.current.clientWidth,
  288. height: this.element.current.clientHeight
  289. })
  290. }
  291. render() {
  292. return (
  293. <div className="card-admin-analytics-chart" ref={this.element}>
  294. {this.props.children(this.state)}
  295. </div>
  296. )
  297. }
  298. }
  299. export default initAnalytics