123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328 |
- import ApolloClient, { gql } from "apollo-boost"
- import React from "react"
- import ReactDOM from "react-dom"
- import Chart from "react-apexcharts"
- import { ApolloProvider, Query } from "react-apollo"
- import moment from "moment"
- const initAnalytics = ({ elementId, errorMessage, labels, title, uri }) => {
- const element = document.getElementById(elementId)
- if (!element) console.error("Element with id " + element + "doesn't exist!")
- const client = new ApolloClient({
- credentials: "same-origin",
- uri: uri
- })
- ReactDOM.render(
- <ApolloProvider client={client}>
- <Analytics errorMessage={errorMessage} labels={labels} title={title} />
- </ApolloProvider>,
- element
- )
- }
- const getAnalytics = gql`
- query getAnalytics($span: Int!) {
- analytics(span: $span) {
- users {
- ...data
- }
- userDeletions {
- ...data
- }
- threads {
- ...data
- }
- posts {
- ...data
- }
- attachments {
- ...data
- }
- dataDownloads {
- ...data
- }
- }
- }
- fragment data on AnalyticsData {
- current
- currentCumulative
- previous
- previousCumulative
- }
- `
- class Analytics extends React.Component {
- state = { span: 30 }
- setSpan = span => {
- this.setState({ span })
- }
- render() {
- const { errorMessage, labels, title } = this.props
- const { span } = this.state
- return (
- <div className="card card-admin-info">
- <div className="card-body">
- <div className="row align-items-center">
- <div className="col">
- <h4 className="card-title">{title}</h4>
- </div>
- <div className="col-auto">
- <SpanPicker span={span} setSpan={this.setSpan} />
- </div>
- </div>
- </div>
- <Query query={getAnalytics} variables={{ span }}>
- {({ loading, error, data }) => {
- if (loading) return <Spinner />
- if (error) return <Error message={errorMessage} />
- const { analytics } = data
- return (
- <>
- <AnalyticsItem
- data={analytics.threads}
- name={labels.threads}
- span={span}
- />
- <AnalyticsItem
- data={analytics.posts}
- name={labels.posts}
- span={span}
- />
- <AnalyticsItem
- data={analytics.attachments}
- name={labels.attachments}
- span={span}
- />
- <AnalyticsItem
- data={analytics.users}
- name={labels.users}
- span={span}
- />
- <AnalyticsItem
- data={analytics.userDeletions}
- name={labels.userDeletions}
- negative={true}
- span={span}
- />
- <AnalyticsItem
- data={analytics.dataDownloads}
- name={labels.dataDownloads}
- span={span}
- />
- </>
- )
- }}
- </Query>
- </div>
- )
- }
- }
- const SpanPicker = ({ span, setSpan }) => (
- <div>
- {[30, 90, 180, 360].map(choice => (
- <button
- key={choice}
- className={
- choice === span
- ? "btn btn-primary btn-sm ml-3"
- : "btn btn-light btn-sm ml-3"
- }
- type="button"
- onClick={() => setSpan(choice)}
- >
- {choice}
- </button>
- ))}
- </div>
- )
- const Spinner = () => (
- <div className="card-body border-top">
- <div className="text-center py-5">
- <div className="spinner-border text-light" role="status">
- <span className="sr-only">Loading...</span>
- </div>
- </div>
- </div>
- )
- const Error = ({ message }) => (
- <div className="card-body border-top">
- <div className="text-center py-5">{message}</div>
- </div>
- )
- const CURRENT = "C"
- const PREVIOUS = "P"
- const CURRENT_SERIES = 0
- const AnalyticsItem = ({ data, legend, name, negative, span }) => {
- const options = {
- legend: {
- show: false
- },
- chart: {
- animations: {
- enabled: false
- },
- parentHeightOffset: 0,
- toolbar: {
- show: false
- }
- },
- colors: ["#6554c0", "#b3d4ff"],
- grid: {
- padding: {
- top: 0
- }
- },
- stroke: {
- width: 2
- },
- tooltip: {
- x: {
- show: false
- },
- y: {
- title: {
- formatter: function(series, { dataPointIndex }) {
- const now = moment()
- if (series === PREVIOUS) now.subtract(span, "days")
- now.subtract(span - dataPointIndex - 1, "days")
- return now.format("ll")
- }
- },
- formatter: function(value, { dataPointIndex, seriesIndex }) {
- if (seriesIndex === CURRENT_SERIES) {
- return data.current[dataPointIndex]
- }
- return data.previous[dataPointIndex]
- }
- }
- },
- xaxis: {
- axisBorder: {
- show: false
- },
- axisTicks: {
- show: false
- },
- labels: {
- show: false
- },
- categories: [],
- tooltip: {
- enabled: false
- }
- },
- yaxis: {
- tickAmount: 2,
- max: m => m || 1,
- show: false
- }
- }
- const series = [
- { name: CURRENT, data: data.currentCumulative },
- { name: PREVIOUS, data: data.previousCumulative }
- ]
- return (
- <div className="card-body border-top pb-1">
- <h5 className="m-0">{name}</h5>
- <div className="row align-items-center">
- <div className="col-auto">
- <Summary data={data} negative={negative} />
- </div>
- <div className="col">
- <ChartContainer>
- {({ width }) =>
- width > 1 && (
- <Chart
- options={options}
- series={series}
- type="line"
- width={width}
- height={140}
- />
- )
- }
- </ChartContainer>
- </div>
- </div>
- </div>
- )
- }
- const Summary = ({ data, negative }) => {
- const current = data.current.reduce((a, b) => a + b)
- const previous = data.previous.reduce((a, b) => a + b)
- const diff = current - previous
- let color = "text-light"
- let icon = "fas fa-equals"
- if (negative) {
- if (diff > 0) color = "text-danger"
- if (diff < 0) color = "text-success"
- } else {
- if (diff > 0) color = "text-success"
- if (diff < 0) color = "text-danger"
- }
- if (diff > 0) {
- icon = "fas fa-chevron-up"
- }
- if (diff < 0) {
- icon = "fas fa-chevron-down"
- }
- return (
- <div className="card-admin-analytics-summary">
- <div>{current}</div>
- <small className={color}>
- <span className={icon} /> {Math.abs(diff)}
- </small>
- </div>
- )
- }
- class ChartContainer extends React.Component {
- state = { width: 1, height: 1 }
- element = React.createRef()
- componentDidMount() {
- this.timer = window.setInterval(this.updateSize, 3000)
- this.updateSize()
- }
- componentWillUnmount() {
- window.clearInterval(this.timer)
- }
- updateSize = () => {
- this.setState({
- width: this.element.current.clientWidth,
- height: this.element.current.clientHeight
- })
- }
- render() {
- return (
- <div className="card-admin-analytics-chart" ref={this.element}>
- {this.props.children(this.state)}
- </div>
- )
- }
- }
- export default initAnalytics
|