Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions redisinsight/ui/src/packages/redistimeseries-app/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules/
dist/
.parcel-cache/
34 changes: 34 additions & 0 deletions redisinsight/ui/src/packages/redistimeseries-app/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# RedisTimeseries Plugin for RedisInsight v2

The example has been created using React, TypeScript, and [Elastic UI](https://elastic.github.io/eui/#/).
[Parcel](https://parceljs.org/) is used to build the plugin.

## Running locally

The following commands will install dependencies and start the server to run the plugin locally:
```
yarn
yarn start
```
These commands will install dependencies and start the server.

_Note_: Base styles are included to `index.html` from the repository.

This command will generate the `vendor` folder with styles and fonts of the core app. Add this folder
inside the folder for your plugin and include appropriate styles to the `index.html` file.

```
yarn build:statics - for Linux or MacOs
yarn build:statics:win - for Windows
```

## Build plugin

The following commands will build plugins to be used in RedisInsight:
```
yarn
yarn build
```

[Add](../../../../../docs/plugins/installation.md) the package.json file and the
`dist` folder to the folder with your plugin, which should be located in the `plugins` folder.
69 changes: 69 additions & 0 deletions redisinsight/ui/src/packages/redistimeseries-app/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
{
"author": {
"name": "Redis Ltd.",
"email": "support@redis.com",
"url": "https://redis.com/redis-enterprise/redis-insight"
},
"bugs": {
"url": "https://github.com/"
},
"description": "RedisTimeseries module",
"source": "./src/main.tsx",
"styles": "./dist/styles.css",
"main": "./dist/index.js",
"name": "redistimeseries",
"version": "0.0.1",
"scripts": {
"start": "cross-env NODE_ENV=development parcel serve src/index.html",
"build": "rimraf dist && cross-env NODE_ENV=production concurrently \"yarn build:js && yarn minify:js\" \"yarn build:css\" \"yarn build:assets\"",
"build:js": "parcel build src/main.tsx --dist-dir dist",
"build:css": "parcel build src/styles/styles.less --dist-dir dist",
"build:assets": "parcel build src/assets/**/* --dist-dir dist",
"minify:js": "terser --compress --mangle -- dist/main.js > dist/index.js && rimraf dist/main.js"
},
"targets": {
"main": false,
"module": {
"includeNodeModules": true
}
},
"visualizations": [
{
"id": "redistimeseries-chart",
"name": "Chart",
"activationMethod": "renderChart",
"matchCommands": [
"TS.MRANGE",
"TS.MREVRANGE",
"TS.RANGE",
"TS.REVRANGE"
],
"description": "Redistimeseries chart view",
"default": true
}
],
"devDependencies": {
"@parcel/compressor-brotli": "^2.0.0",
"@parcel/compressor-gzip": "^2.0.0",
"@parcel/transformer-less": "^2.0.1",
"@parcel/transformer-sass": "2.3.2",
"@types/file-saver": "^2.0.5",
"@types/plotly.js-dist-min": "^2.3.0",
"concurrently": "^6.3.0",
"cross-env": "^7.0.3",
"parcel": "^2.0.0",
"rimraf": "^3.0.2",
"terser": "^5.9.0"
},
"dependencies": {
"@elastic/eui": "34.6.0",
"@emotion/react": "^11.7.1",
"classnames": "^2.3.1",
"date-fns": "^2.28.0",
"file-saver": "^2.0.5",
"fscreen": "^1.2.0",
"plotly.js-dist-min": "^2.9.0",
"react": "^17.0.2",
"react-dom": "^17.0.2"
}
}
50 changes: 50 additions & 0 deletions redisinsight/ui/src/packages/redistimeseries-app/src/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import React from 'react'
import ChartResultView from './components/Chart/ChartResultView'

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please, remove one empty line

interface Props {
command: string
result?: { response: any, status: string }[]
}

enum TS_CMD_RANGE_PREFIX {
RANGE = 'TS.RANGE',
REVRANGE = 'TS.REVRANGE',
}

const App = (props: Props) => {
const { result: [{ response = '', status = '' } = {}] = [] } = props

if (status === 'fail') {
return <div className="responseFail">{response}</div>
}

if (status === 'success' && typeof(response) === 'string') {
return <div className="responseFail">{response}</div>
}

function responseParser(command: string, data: any) {

let [cmd, key, ..._] = command.split(' ')

if ([TS_CMD_RANGE_PREFIX.RANGE.toString(), TS_CMD_RANGE_PREFIX.REVRANGE.toString()].includes(cmd)) {
return [{
key,
datapoints: data,
}]
}

return data.map(e => ({
key: e[0],
labels: e[1],
datapoints: e[2],
}))
}

return (
<ChartResultView
data={responseParser(props.command, props.result[0].response) as any}
/>
)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The same.
Also will be good to handle fail status


export default App
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import React, { useRef, useEffect } from 'react'
import Plotly from 'plotly.js-dist-min'
import { Legend, LayoutAxis, PlotData, PlotMouseEvent, Layout, PlotRelayoutEvent } from 'plotly.js'
import { format } from 'date-fns'
import { hexToRGBA, IGoodColor, GoodColorPicker, COLORS, COLORS_DARK } from './utils'

import {
Datapoint,
GraphMode,
ChartProps,
PlotlyEvents,
} from './interfaces'

const GRAPH_MODE_MAP: { [mode: string]: 'lines' | 'markers' } = {
[GraphMode.line]: 'lines',
[GraphMode.points]: 'markers',
}

const isDarkTheme = document.body.classList.contains('theme_DARK')

const colorPicker = (COLORS: IGoodColor[]) => {
const color = new GoodColorPicker(COLORS)
return (label: string) => color.getColor(label).color
}

const labelColors = colorPicker(isDarkTheme ? COLORS_DARK : COLORS)

export default function Chart(props: ChartProps) {
const chartContainer = useRef<any>()

const colorPicker = labelColors

useEffect(() => {
Plotly.newPlot(
chartContainer.current,
getData(props),
getLayout(props),
{ displayModeBar: false, autosizable: true, responsive: true, setBackground: () => 'transparent', },
)
chartContainer.current.on(PlotlyEvents.PLOTLY_HOVER, function (eventdata: PlotMouseEvent) {
const points = eventdata.points[0]
const pointNum = points.pointNumber
Plotly.Fx.hover(
chartContainer.current,
props.data.map((_, i) => ({
curveNumber: i,
pointNumber: pointNum
})),
Object.keys((chartContainer.current)._fullLayout._plots))
})
chartContainer.current.on(PlotlyEvents.PLOTLY_RELAYOUT, function (eventdata: PlotRelayoutEvent) {
if (eventdata.autosize === undefined && eventdata['xaxis.autorange'] === undefined) {
props.onRelayout()
}
})

chartContainer.current.on(PlotlyEvents.PLOTLY_DBLCLICK, () => props.onDoubleClick())
}, [props.chartConfig])

function getData(props: ChartProps): Partial<PlotData>[] {
return props.data.map((timeSeries, i) => {

const currentData = chartContainer.current.data
const dataUnchanged = currentData && props.data === props.data
/*
* Time format for inclusion of milliseconds:
* https://github.com/moment/moment/issues/4864#issuecomment-440142542
*/
const x = dataUnchanged ? currentData[i].x
: selectCol(timeSeries.datapoints, 0).map((time: number) => format(time, 'yyyy-MM-dd HH:mm:ss.SSS'))
const y = dataUnchanged ? currentData[i].y : selectCol(timeSeries.datapoints, 1)

return {
x,
y,
yaxis: props.chartConfig.yAxis2 && props.chartConfig.keyToY2Axis[timeSeries.key] ? 'y2' : 'y',
name: timeSeries.key,
type: 'scatter',
marker: { color: colorPicker(timeSeries.key) },
fill: props.chartConfig.fill ? 'tozeroy' : undefined,
fillcolor: hexToRGBA(colorPicker(timeSeries.key), 0.3),
mode: GRAPH_MODE_MAP[props.chartConfig.mode],
line: { shape: props.chartConfig.staircase ? 'hv' : 'spline' },
}
})
}

function getLayout(props: ChartProps): Partial<Layout> {
const axisConfig: { [key: string]: Partial<LayoutAxis> } = {
xaxis: {
title: props.chartConfig.xlabel,
rangeslider: {
visible: true,
thickness: 0.03,
bgcolor: isDarkTheme ? '#3D3D3D' : '#CDD7EA',
bordercolor: 'red',
},
color: isDarkTheme ? '#898A90' : '#527298'
},
yaxis: {
title: props.chartConfig.yAxisConfig.label,
type: props.chartConfig.yAxisConfig.scale,
fixedrange: true,
color: isDarkTheme ? '#898A90' : '#527298',
gridcolor: isDarkTheme ? '#898A90' : '#527298',
},
yaxis2: {
visible: props.chartConfig.yAxis2,
title: props.chartConfig.yAxis2Config.label,
type: props.chartConfig.yAxis2Config.scale,
overlaying: 'y',
side: 'right',
fixedrange: true,
color: isDarkTheme ? '#8191CF' : '#6E6E6E',
gridcolor: isDarkTheme ? '#8191CF' : '#6E6E6E',
} as LayoutAxis,
}

const legend: Partial<Legend> = {
xanchor: 'center',
yanchor: 'top',
x: 0.5,
y: -0.3,
orientation: 'h',
}
return {
...axisConfig,
legend,
showlegend: true,
title: props.chartConfig.title,
uirevision: 1,
autosize: true,
font: { color: isDarkTheme ? 'darkgrey' : 'black' },
paper_bgcolor: 'rgba(0,0,0,0)',
plot_bgcolor: 'rgba(0,0,0,0)',
margin: {
pad: 6
}
}
}

function selectCol(twoDArray: Datapoint[], colIndex: number) {
return twoDArray.map(arr => arr[colIndex])
}

return (
<div ref={chartContainer}></div>
)
}
Loading