Skip to content

Commit 5a94334

Browse files
Merge pull request #312 from RedisInsight/timeseries-plugin
Add RedisTimeseries plugin
2 parents c2e5bd3 + c4bb694 commit 5a94334

File tree

19 files changed

+5306
-0
lines changed

19 files changed

+5306
-0
lines changed
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
node_modules/
2+
dist/
3+
.parcel-cache/
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# RedisTimeseries Plugin for RedisInsight v2
2+
3+
The example has been created using React, TypeScript, and [Elastic UI](https://elastic.github.io/eui/#/).
4+
[Parcel](https://parceljs.org/) is used to build the plugin.
5+
6+
## Running locally
7+
8+
The following commands will install dependencies and start the server to run the plugin locally:
9+
```
10+
yarn
11+
yarn start
12+
```
13+
These commands will install dependencies and start the server.
14+
15+
_Note_: Base styles are included to `index.html` from the repository.
16+
17+
This command will generate the `vendor` folder with styles and fonts of the core app. Add this folder
18+
inside the folder for your plugin and include appropriate styles to the `index.html` file.
19+
20+
```
21+
yarn build:statics - for Linux or MacOs
22+
yarn build:statics:win - for Windows
23+
```
24+
25+
## Build plugin
26+
27+
The following commands will build plugins to be used in RedisInsight:
28+
```
29+
yarn
30+
yarn build
31+
```
32+
33+
[Add](../../../../../docs/plugins/installation.md) the package.json file and the
34+
`dist` folder to the folder with your plugin, which should be located in the `plugins` folder.
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
{
2+
"author": {
3+
"name": "Redis Ltd.",
4+
"email": "[email protected]",
5+
"url": "https://redis.com/redis-enterprise/redis-insight"
6+
},
7+
"bugs": {
8+
"url": "https://github.com/"
9+
},
10+
"description": "RedisTimeseries module",
11+
"source": "./src/main.tsx",
12+
"styles": "./dist/styles.css",
13+
"main": "./dist/index.js",
14+
"name": "redistimeseries",
15+
"version": "0.0.1",
16+
"scripts": {
17+
"start": "cross-env NODE_ENV=development parcel serve src/index.html",
18+
"build": "rimraf dist && cross-env NODE_ENV=production concurrently \"yarn build:js && yarn minify:js\" \"yarn build:css\" \"yarn build:assets\"",
19+
"build:js": "parcel build src/main.tsx --dist-dir dist",
20+
"build:css": "parcel build src/styles/styles.less --dist-dir dist",
21+
"build:assets": "parcel build src/assets/**/* --dist-dir dist",
22+
"minify:js": "terser --compress --mangle -- dist/main.js > dist/index.js && rimraf dist/main.js"
23+
},
24+
"targets": {
25+
"main": false,
26+
"module": {
27+
"includeNodeModules": true
28+
}
29+
},
30+
"visualizations": [
31+
{
32+
"id": "redistimeseries-chart",
33+
"name": "Chart",
34+
"activationMethod": "renderChart",
35+
"matchCommands": [
36+
"TS.MRANGE",
37+
"TS.MREVRANGE",
38+
"TS.RANGE",
39+
"TS.REVRANGE"
40+
],
41+
"description": "Redistimeseries chart view",
42+
"default": true
43+
}
44+
],
45+
"devDependencies": {
46+
"@parcel/compressor-brotli": "^2.0.0",
47+
"@parcel/compressor-gzip": "^2.0.0",
48+
"@parcel/transformer-less": "^2.0.1",
49+
"@parcel/transformer-sass": "2.3.2",
50+
"@types/file-saver": "^2.0.5",
51+
"@types/plotly.js-dist-min": "^2.3.0",
52+
"concurrently": "^6.3.0",
53+
"cross-env": "^7.0.3",
54+
"parcel": "^2.0.0",
55+
"rimraf": "^3.0.2",
56+
"terser": "^5.9.0"
57+
},
58+
"dependencies": {
59+
"@elastic/eui": "34.6.0",
60+
"@emotion/react": "^11.7.1",
61+
"classnames": "^2.3.1",
62+
"date-fns": "^2.28.0",
63+
"file-saver": "^2.0.5",
64+
"fscreen": "^1.2.0",
65+
"plotly.js-dist-min": "^2.9.0",
66+
"react": "^17.0.2",
67+
"react-dom": "^17.0.2"
68+
}
69+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import React from 'react'
2+
import ChartResultView from './components/Chart/ChartResultView'
3+
4+
interface Props {
5+
command: string
6+
result?: { response: any, status: string }[]
7+
}
8+
9+
enum TS_CMD_RANGE_PREFIX {
10+
RANGE = 'TS.RANGE',
11+
REVRANGE = 'TS.REVRANGE',
12+
}
13+
14+
const App = (props: Props) => {
15+
const { result: [{ response = '', status = '' } = {}] = [] } = props
16+
17+
if (status === 'fail') {
18+
return <div className="responseFail">{response}</div>
19+
}
20+
21+
if (status === 'success' && typeof(response) === 'string') {
22+
return <div className="responseFail">{response}</div>
23+
}
24+
25+
function responseParser(command: string, data: any) {
26+
27+
let [cmd, key, ..._] = command.split(' ')
28+
29+
if ([TS_CMD_RANGE_PREFIX.RANGE.toString(), TS_CMD_RANGE_PREFIX.REVRANGE.toString()].includes(cmd)) {
30+
return [{
31+
key,
32+
datapoints: data,
33+
}]
34+
}
35+
36+
return data.map(e => ({
37+
key: e[0],
38+
labels: e[1],
39+
datapoints: e[2],
40+
}))
41+
}
42+
43+
return (
44+
<ChartResultView
45+
data={responseParser(props.command, props.result[0].response) as any}
46+
/>
47+
)
48+
}
49+
50+
export default App
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
import React, { useRef, useEffect } from 'react'
2+
import Plotly from 'plotly.js-dist-min'
3+
import { Legend, LayoutAxis, PlotData, PlotMouseEvent, Layout, PlotRelayoutEvent } from 'plotly.js'
4+
import { format } from 'date-fns'
5+
import { hexToRGBA, IGoodColor, GoodColorPicker, COLORS, COLORS_DARK } from './utils'
6+
7+
import {
8+
Datapoint,
9+
GraphMode,
10+
ChartProps,
11+
PlotlyEvents,
12+
} from './interfaces'
13+
14+
const GRAPH_MODE_MAP: { [mode: string]: 'lines' | 'markers' } = {
15+
[GraphMode.line]: 'lines',
16+
[GraphMode.points]: 'markers',
17+
}
18+
19+
const isDarkTheme = document.body.classList.contains('theme_DARK')
20+
21+
const colorPicker = (COLORS: IGoodColor[]) => {
22+
const color = new GoodColorPicker(COLORS)
23+
return (label: string) => color.getColor(label).color
24+
}
25+
26+
const labelColors = colorPicker(isDarkTheme ? COLORS_DARK : COLORS)
27+
28+
export default function Chart(props: ChartProps) {
29+
const chartContainer = useRef<any>()
30+
31+
const colorPicker = labelColors
32+
33+
useEffect(() => {
34+
Plotly.newPlot(
35+
chartContainer.current,
36+
getData(props),
37+
getLayout(props),
38+
{ displayModeBar: false, autosizable: true, responsive: true, setBackground: () => 'transparent', },
39+
)
40+
chartContainer.current.on(PlotlyEvents.PLOTLY_HOVER, function (eventdata: PlotMouseEvent) {
41+
const points = eventdata.points[0]
42+
const pointNum = points.pointNumber
43+
Plotly.Fx.hover(
44+
chartContainer.current,
45+
props.data.map((_, i) => ({
46+
curveNumber: i,
47+
pointNumber: pointNum
48+
})),
49+
Object.keys((chartContainer.current)._fullLayout._plots))
50+
})
51+
chartContainer.current.on(PlotlyEvents.PLOTLY_RELAYOUT, function (eventdata: PlotRelayoutEvent) {
52+
if (eventdata.autosize === undefined && eventdata['xaxis.autorange'] === undefined) {
53+
props.onRelayout()
54+
}
55+
})
56+
57+
chartContainer.current.on(PlotlyEvents.PLOTLY_DBLCLICK, () => props.onDoubleClick())
58+
}, [props.chartConfig])
59+
60+
function getData(props: ChartProps): Partial<PlotData>[] {
61+
return props.data.map((timeSeries, i) => {
62+
63+
const currentData = chartContainer.current.data
64+
const dataUnchanged = currentData && props.data === props.data
65+
/*
66+
* Time format for inclusion of milliseconds:
67+
* https://github.com/moment/moment/issues/4864#issuecomment-440142542
68+
*/
69+
const x = dataUnchanged ? currentData[i].x
70+
: selectCol(timeSeries.datapoints, 0).map((time: number) => format(time, 'yyyy-MM-dd HH:mm:ss.SSS'))
71+
const y = dataUnchanged ? currentData[i].y : selectCol(timeSeries.datapoints, 1)
72+
73+
return {
74+
x,
75+
y,
76+
yaxis: props.chartConfig.yAxis2 && props.chartConfig.keyToY2Axis[timeSeries.key] ? 'y2' : 'y',
77+
name: timeSeries.key,
78+
type: 'scatter',
79+
marker: { color: colorPicker(timeSeries.key) },
80+
fill: props.chartConfig.fill ? 'tozeroy' : undefined,
81+
fillcolor: hexToRGBA(colorPicker(timeSeries.key), 0.3),
82+
mode: GRAPH_MODE_MAP[props.chartConfig.mode],
83+
line: { shape: props.chartConfig.staircase ? 'hv' : 'spline' },
84+
}
85+
})
86+
}
87+
88+
function getLayout(props: ChartProps): Partial<Layout> {
89+
const axisConfig: { [key: string]: Partial<LayoutAxis> } = {
90+
xaxis: {
91+
title: props.chartConfig.xlabel,
92+
rangeslider: {
93+
visible: true,
94+
thickness: 0.03,
95+
bgcolor: isDarkTheme ? '#3D3D3D' : '#CDD7EA',
96+
bordercolor: 'red',
97+
},
98+
color: isDarkTheme ? '#898A90' : '#527298'
99+
},
100+
yaxis: {
101+
title: props.chartConfig.yAxisConfig.label,
102+
type: props.chartConfig.yAxisConfig.scale,
103+
fixedrange: true,
104+
color: isDarkTheme ? '#898A90' : '#527298',
105+
gridcolor: isDarkTheme ? '#898A90' : '#527298',
106+
},
107+
yaxis2: {
108+
visible: props.chartConfig.yAxis2,
109+
title: props.chartConfig.yAxis2Config.label,
110+
type: props.chartConfig.yAxis2Config.scale,
111+
overlaying: 'y',
112+
side: 'right',
113+
fixedrange: true,
114+
color: isDarkTheme ? '#8191CF' : '#6E6E6E',
115+
gridcolor: isDarkTheme ? '#8191CF' : '#6E6E6E',
116+
} as LayoutAxis,
117+
}
118+
119+
const legend: Partial<Legend> = {
120+
xanchor: 'center',
121+
yanchor: 'top',
122+
x: 0.5,
123+
y: -0.3,
124+
orientation: 'h',
125+
}
126+
return {
127+
...axisConfig,
128+
legend,
129+
showlegend: true,
130+
title: props.chartConfig.title,
131+
uirevision: 1,
132+
autosize: true,
133+
font: { color: isDarkTheme ? 'darkgrey' : 'black' },
134+
paper_bgcolor: 'rgba(0,0,0,0)',
135+
plot_bgcolor: 'rgba(0,0,0,0)',
136+
margin: {
137+
pad: 6
138+
}
139+
}
140+
}
141+
142+
function selectCol(twoDArray: Datapoint[], colIndex: number) {
143+
return twoDArray.map(arr => arr[colIndex])
144+
}
145+
146+
return (
147+
<div ref={chartContainer}></div>
148+
)
149+
}

0 commit comments

Comments
 (0)