diff --git a/README.md b/README.md index 9c04b7f..29e5097 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ # Labs Widget Pack -![GitHub release (latest SemVer including pre-releases)](https://img.shields.io/github/v/release/newrelic/nr-labs-widget-pack?include_prereleases&sort=semver) +![GitHub release (latest SemVer including pre-releases)](https://img.shields.io/github/v/release/newrelic/nr-labs-widget-pack?include_prereleases&sort=semver) A library of New Relic custom chart widgets created by the New Relic Labs team, for use in New Relic dashboards. @@ -34,7 +34,7 @@ Click on the short description in each section to view chart details. - the X axis represents time - the left Y axis represents the values for the Bar Charts - the right Y axis represents the values for the Line Charts - + The chart allows you to define multiple line and bar queries, so it is highly recommended that the queries are aligned in terms of units and time periods. #### Requirements @@ -83,12 +83,37 @@ Click on the short description in each section to view chart details. --- +### Cumulative Timeseries Chart + +
+ + Trend cumulative values over time as a line or area chart. + + Cumulative chart screenshot + + #### Overview + Use the Cumulative Chart to see running totals, or the total sum of a given data set as it grows with time. + + The Cumulative Timeseries chart supports Line & Area chart types. + + #### Requirements + In order to use this chart, there are a few requirements: + - Each query must use and end with the `TIMESERIES` clause, and also contain the bucket eg. `TIMESERIES 1 second` + - Do not use SINCE or UNTIL clauses as they will automatically be determined based on the time range picker + - If using the LIMIT clause, this should be placed before and not after the TIMESERIES clause + + A valid query for the chart could look like this: + `SELECT count(*) FROM Transaction FACET appName TIMESERIES` + + --- +
+ ### Multiline Compare Chart
Display multiple comparison periods in a single timeseries chart. - + Multi Line Compare chart screenshot --- @@ -98,7 +123,7 @@ Click on the short description in each section to view chart details.
Render events as markers on a line chart. - + Line and Event overlay screenshot --- @@ -108,7 +133,7 @@ Click on the short description in each section to view chart details.
Render events as markers on an area chart. - + Area and Event overlay screenshot --- @@ -118,7 +143,7 @@ Click on the short description in each section to view chart details.
Render events as markers on a scatter chart. - + Scatter and Event overlay screenshot --- @@ -128,7 +153,7 @@ Click on the short description in each section to view chart details.
Plot one or more groups of values over multiple variables, and compare them on a two-dimensional plane. - + Radar chart screenshot --- @@ -137,7 +162,7 @@ Click on the short description in each section to view chart details. ### Map Widget
Plot any data that includes latitude and longitude onto an interactive map, leveraging the Leaflet or Mapbox API. - + #### Overview Map screenshot @@ -150,7 +175,7 @@ Click on the short description in each section to view chart details. - Query should contain one alias with 'name:SOME_VALUE' which will be used as the marker name - Query should have a FACET for latitude and longitude, use precision to ensure the FACET does not round the number ``` - FACET string(lat, precision: 5) as 'lat', string(lng, precision: 5) as 'lng' + FACET string(lat, precision: 5) as 'lat', string(lng, precision: 5) as 'lng' ``` - Rotation can be set using the following alias with 'rotate:SOME_VALUE' (Map Box only) - Example Query: @@ -164,7 +189,7 @@ Click on the short description in each section to view chart details.
Display query results in a list, with smart formatting options. - + #### Overview List view screenshot @@ -176,9 +201,9 @@ Click on the short description in each section to view chart details. - Search bar to filter list to the searched text #### Requirements - - For full details on how to use and format results in this chart, read the [Template String documentation](./list-view-template.md). - + + For full details on how to use and format results in this chart, read the [Template String documentation](./list-view-template.md). + ---
@@ -186,7 +211,7 @@ Click on the short description in each section to view chart details.
Incorporate buttons into your dashboards, with configurable onClick actions. - + #### Overview Incorporate buttons into your dashboards, with configurable onClick actions. @@ -202,7 +227,7 @@ Click on the short description in each section to view chart details. # Enabling this Nerdpack -This pack of visualizations is available via the New Relic Catalog. +This pack of visualizations is available via the New Relic Catalog. To enable it in your account, go to `Add Data > Apps and Visualzations` and search for "Labs Widget Pack". Click the icon and subscribe this to your accounts. @@ -234,5 +259,3 @@ Keep in mind that when you submit your pull request, you'll need to sign the CLA # Open source license This project is distributed under the [Apache 2 license](LICENSE). - - diff --git a/nr1.json b/nr1.json index 3ab0746..cfdf6e3 100644 --- a/nr1.json +++ b/nr1.json @@ -3,4 +3,4 @@ "id": "090369b0-4f5d-464d-a6b2-48d42a8ae2f4", "displayName": "Labs Widget Pack", "description": "A collection of custom visualizations" -} \ No newline at end of file +} diff --git a/package-lock.json b/package-lock.json index be1b1cd..cbb5528 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,7 +24,7 @@ "react-leaflet": "^3.1.0", "react-leaflet-marker": "^1.1.4", "react-map-gl": "^7.0.19", - "recharts": "^2.8.0" + "recharts": "^2.12.7" }, "devDependencies": { "@newrelic/eslint-plugin-newrelic": "^0.3.1", @@ -4318,11 +4318,12 @@ "integrity": "sha512-R8LUSEay/68zE5c8/3BDxiTEvgb4xZTF0RKmAHfiEVN3klfIpXfi2/QCoiWPccVQ0J/ZGdz9OjzL4uJEP/MRAw==" }, "node_modules/dom-helpers": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-3.4.0.tgz", - "integrity": "sha512-LnuPJ+dwqKDIyotW1VzmOZ5TONUN7CwkCR5hrgawTUbkBGYdeoNLZo6nNfGkCrjtE1nXXaj7iMMpDa8/d9WoIA==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", "dependencies": { - "@babel/runtime": "^7.1.2" + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" } }, "node_modules/dot-prop": { @@ -12337,32 +12338,32 @@ } }, "node_modules/react-smooth": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-2.0.5.tgz", - "integrity": "sha512-BMP2Ad42tD60h0JW6BFaib+RJuV5dsXJK9Baxiv/HlNFjvRLqA9xrNKxVWnUIZPQfzUwGXIlU/dSYLU+54YGQA==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.1.tgz", + "integrity": "sha512-OE4hm7XqR0jNOq3Qmk9mFLyd6p2+j6bvbPJ7qlB7+oo0eNcL2l7WQzG6MBnT3EXY6xzkLMUBec3AfewJdA0J8w==", "dependencies": { - "fast-equals": "^5.0.0", - "react-transition-group": "2.9.0" + "fast-equals": "^5.0.1", + "prop-types": "^15.8.1", + "react-transition-group": "^4.4.5" }, "peerDependencies": { - "prop-types": "^15.6.0", - "react": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0" + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, "node_modules/react-transition-group": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-2.9.0.tgz", - "integrity": "sha512-+HzNTCHpeQyl4MJ/bdE0u6XRMe9+XG/+aL4mCxVN4DnPBQ0/5bfHWPDuOZUzYdMj94daZaZdCCc1Dzt9R/xSSg==", + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", "dependencies": { - "dom-helpers": "^3.4.0", + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", "loose-envify": "^1.4.0", - "prop-types": "^15.6.2", - "react-lifecycles-compat": "^3.0.4" + "prop-types": "^15.6.2" }, "peerDependencies": { - "react": ">=15.0.0", - "react-dom": ">=15.0.0" + "react": ">=16.6.0", + "react-dom": ">=16.6.0" } }, "node_modules/react-universal-interface": { @@ -12528,15 +12529,15 @@ "peer": true }, "node_modules/recharts": { - "version": "2.10.4", - "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.10.4.tgz", - "integrity": "sha512-/Q7/wdf8bW91lN3NEeCjL9RWfaiXQViJFgdnas4Eix/I8B9HAI3tHHK/CW/zDfgRMh4fzW1zlfjoz1IAapLO1Q==", + "version": "2.12.7", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.12.7.tgz", + "integrity": "sha512-hlLJMhPQfv4/3NBSAyq3gzGg4h2v69RJh6KU7b3pXYNNAELs9kEoXOjbkxdXpALqKBoVmVptGfLpxdaVYqjmXQ==", "dependencies": { "clsx": "^2.0.0", "eventemitter3": "^4.0.1", - "lodash": "^4.17.19", + "lodash": "^4.17.21", "react-is": "^16.10.2", - "react-smooth": "^2.0.5", + "react-smooth": "^4.0.0", "recharts-scale": "^0.4.4", "tiny-invariant": "^1.3.1", "victory-vendor": "^36.6.8" @@ -12545,7 +12546,6 @@ "node": ">=14" }, "peerDependencies": { - "prop-types": "^15.6.0", "react": "^16.0.0 || ^17.0.0 || ^18.0.0", "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0" } diff --git a/package.json b/package.json index 83a4f16..284facf 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "private": true, "name": "nr-labs-widget-pack", "description": "Nerdpack template", - "version": "1.46.0", + "version": "1.46.4", "scripts": { "start": "nr1 nerdpack:serve", "test": "exit 0", @@ -33,7 +33,7 @@ "react-leaflet": "^3.1.0", "react-leaflet-marker": "^1.1.4", "react-map-gl": "^7.0.19", - "recharts": "^2.8.0" + "recharts": "^2.12.7" }, "ids": { "am": "c2fab1ee-57c3-43d6-bc5c-c7343ecaff0c", diff --git a/screenshots/cumulative_01.png b/screenshots/cumulative_01.png new file mode 100644 index 0000000..2051cd3 Binary files /dev/null and b/screenshots/cumulative_01.png differ diff --git a/visualizations/nr-cumulative-chart/docs.js b/visualizations/nr-cumulative-chart/docs.js new file mode 100644 index 0000000..62ac964 --- /dev/null +++ b/visualizations/nr-cumulative-chart/docs.js @@ -0,0 +1,72 @@ +import React from 'react'; + +import { + Card, + CardHeader, + CardBody, + HeadingText, + BlockText, + Spacing +} from 'nr1'; + +import RenderPropertyInfo from '../../shared/PropertyInfo'; + +const properties = require('./nr1.json'); + +export default function Docs() { + return ( +
+ Documentation + + + + + Use the Cumulative Chart to see running totals, or the total sum of + a given data set as it grows with time. +
+
+ The chart supports Line & Area chart types. +
+
+
+ + + + + In order to populate the chart, there are a few requirements: + +
    +
  • At least 1 timeseries query
  • +
  • + Timeseries queries should contain the TIMESERIES{' '} + clause +
  • +
+
+ +
+ A valid timeseries query for the chart could look like this:{' '} +
+
+ + SELECT count(*) FROM Transaction TIMESERIES + +
+
+
+ + + + + {properties.configuration + .filter(c => c.name !== 'showDocs') + .map(config => { + return RenderPropertyInfo(config, false, {}); + })} + + + +
+
+ ); +} diff --git a/visualizations/nr-cumulative-chart/index.js b/visualizations/nr-cumulative-chart/index.js new file mode 100644 index 0000000..c100c02 --- /dev/null +++ b/visualizations/nr-cumulative-chart/index.js @@ -0,0 +1,146 @@ +import React, { useContext, useState, useEffect } from 'react'; +import { + NrqlQuery, + AreaChart, + LineChart, + Spinner, + NerdletStateContext, + PlatformStateContext +} from 'nr1'; +import Docs from './docs'; +import ErrorState from '../../shared/ErrorState'; +import { discoverErrors } from './utils'; +import { subVariables } from '../shared/utils'; +import { useInterval } from '@mantine/hooks'; + +const MINUTE = 60000; +const HOUR = 60 * MINUTE; +const DAY = 24 * HOUR; + +const timeRangeToNrql = timeRange => { + if (!timeRange) { + return ''; + } + + if (timeRange.beginTime && timeRange.endTime) { + return `SINCE ${timeRange.beginTime} UNTIL ${timeRange.endTime}`; + } else if (timeRange.begin_time && timeRange.end_time) { + return `SINCE ${timeRange.begin_time} UNTIL ${timeRange.end_time}`; + } else if (timeRange.duration <= HOUR) { + return `SINCE ${timeRange.duration / MINUTE} MINUTES AGO`; + } else if (timeRange.duration <= DAY) { + return `SINCE ${timeRange.duration / HOUR} HOURS AGO`; + } else { + return `SINCE ${timeRange.duration / DAY} DAYS AGO`; + } +}; + +function CumulativeChart(props) { + const { + showDocs, + accountId, + query, + chartType, + enableFilters, + pollInterval + } = props; + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [finalQuery, setQuery] = useState(null); + const [errors, setErrors] = useState([]); + const platformContext = useContext(PlatformStateContext); + const { filters, selectedVariables } = useContext(NerdletStateContext); + const { timeRange } = platformContext; + const filterClause = filters ? `WHERE ${filters}` : ''; + + const interval = useInterval(() => { + fetchData(); + }, (pollInterval || 60) * 1000); + + useEffect(() => { + if (query) { + let tempQuery = subVariables(query, selectedVariables); + const sinceClause = timeRangeToNrql(timeRange); + + if (enableFilters) { + tempQuery += ` ${filterClause}`; + } + + if (sinceClause !== '') { + tempQuery += ` ${sinceClause}`; + } + + setQuery(tempQuery); + } + + const inputErrors = discoverErrors(props); + if (inputErrors.length > 0) { + const errorObj = { name: `Input Errors`, errors: inputErrors }; + setErrors([errorObj]); + } else { + setErrors([]); + } + }, [query, selectedVariables, enableFilters, filterClause, timeRange]); + + useEffect(async () => { + fetchData(); + interval.stop(); + interval.start(); + return interval.stop; + }, [accountId, finalQuery, pollInterval, timeRange]); + + const fetchData = async () => { + if (finalQuery && accountId) { + setLoading(true); + const resp = await NrqlQuery.query({ + query: finalQuery, + accountIds: [parseInt(accountId)], + formatType: NrqlQuery.FORMAT_TYPE.CHART + }); + + if (resp.error) { + const dataError = { name: `Data Fetch Errors`, errors: resp.error }; + setErrors([dataError]); + } + + if (resp.data && !resp.error) { + resp.data.forEach(series => { + if (series.cumulativeApplied) return; + let cumulative = 0; + series.data.forEach(d => { + cumulative += d.y; + d.y = cumulative; + }); + series.cumulativeApplied = true; + }); + setData(resp.data); + setLoading(false); + } + } + }; + + const renderChart = (chartType, data) => { + if (!chartType || chartType === 'line') { + return ; + } else if (chartType === 'area') { + return ; + } + }; + + if (loading && !data) { + return ; + } + + if (errors.length > 0) { + return ; + } + + return ( + <> + {showDocs && } + {renderChart(chartType, data)} + + ); +} + +export default CumulativeChart; diff --git a/visualizations/nr-cumulative-chart/nr1.json b/visualizations/nr-cumulative-chart/nr1.json new file mode 100644 index 0000000..7aba1fa --- /dev/null +++ b/visualizations/nr-cumulative-chart/nr1.json @@ -0,0 +1,88 @@ +{ + "schemaType": "VISUALIZATION", + "id": "nr-cumulative-line-chart", + "displayName": "Cumulative Chart", + "description": "Labs Widget Pack - Cumulative timeseries chart w/ custom poll intervals", + "configuration": [ + { + "name": "showDocs", + "title": "Show Documentation", + "description": "", + "type": "boolean" + }, + { + "name": "accountId", + "title": "Account ID", + "description": "Account ID to be associated with the query", + "type": "account-id" + }, + { + "name": "query", + "title": "Query", + "description": "NRQL query for the visualization", + "type": "nrql" + }, + { + "name": "chartType", + "title": "Chart Type (default: line)", + "description": "", + "type": "enum", + "items": [ + { + "title": "line", + "value": "line" + }, + { + "title": "area", + "value": "area" + } + ] + }, + { + "name": "enableFilters", + "title": "Enable dashboard filters", + "description": "Allows the use of dashboard filters", + "type": "boolean" + }, + { + "name": "pollInterval", + "title": "Poll Interval (default: 60s)", + "description": "Frequency at which data is refreshed.", + "type": "enum", + "items": [ + { + "title": "Select", + "value": 60 + }, + { + "title": "5s", + "value": 5 + }, + { + "title": "10s", + "value": 10 + }, + { + "title": "15s", + "value": 15 + }, + { + "title": "30s", + "value": 30 + }, + { + "title": "45s", + "value": 45 + }, + { + "title": "1m", + "value": 60 + }, + { + "title": "5m", + "value": 300 + } + ] + } + ] +} diff --git a/visualizations/nr-cumulative-chart/styles.scss b/visualizations/nr-cumulative-chart/styles.scss new file mode 100644 index 0000000..d6a218c --- /dev/null +++ b/visualizations/nr-cumulative-chart/styles.scss @@ -0,0 +1,17 @@ +.EmptyState, +.ErrorState { + height: 100%; + text-align: center; + + &-cardBody { + height: 100%; + box-sizing: border-box; + display: flex; + flex-direction: column; + justify-content: center; + } +} + +.ErrorState-headingText { + color: darkgrey; +} diff --git a/visualizations/nr-cumulative-chart/utils.js b/visualizations/nr-cumulative-chart/utils.js new file mode 100644 index 0000000..f0a485b --- /dev/null +++ b/visualizations/nr-cumulative-chart/utils.js @@ -0,0 +1,18 @@ +export const discoverErrors = props => { + const { accountId, query } = props; + const lowerQuery = (query || '').toLowerCase(); + + const errors = []; + + if (!accountId) { + errors.push('Account ID required'); + } + + if (!query) { + errors.push('Query required'); + } else if (!lowerQuery.includes('timeseries')) { + errors.push('TIMESERIES keyword required'); + } + + return errors; +};