diff --git a/redisinsight/ui/src/packages/redistimeseries-app/.gitignore b/redisinsight/ui/src/packages/redistimeseries-app/.gitignore
new file mode 100644
index 0000000000..9287d0d225
--- /dev/null
+++ b/redisinsight/ui/src/packages/redistimeseries-app/.gitignore
@@ -0,0 +1,3 @@
+node_modules/
+dist/
+.parcel-cache/
\ No newline at end of file
diff --git a/redisinsight/ui/src/packages/redistimeseries-app/README.md b/redisinsight/ui/src/packages/redistimeseries-app/README.md
new file mode 100644
index 0000000000..2648730ab8
--- /dev/null
+++ b/redisinsight/ui/src/packages/redistimeseries-app/README.md
@@ -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.
diff --git a/redisinsight/ui/src/packages/redistimeseries-app/package.json b/redisinsight/ui/src/packages/redistimeseries-app/package.json
new file mode 100644
index 0000000000..3322e5c932
--- /dev/null
+++ b/redisinsight/ui/src/packages/redistimeseries-app/package.json
@@ -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"
+ }
+}
diff --git a/redisinsight/ui/src/packages/redistimeseries-app/src/App.tsx b/redisinsight/ui/src/packages/redistimeseries-app/src/App.tsx
new file mode 100644
index 0000000000..40bcb69453
--- /dev/null
+++ b/redisinsight/ui/src/packages/redistimeseries-app/src/App.tsx
@@ -0,0 +1,50 @@
+import React from 'react'
+import ChartResultView from './components/Chart/ChartResultView'
+
+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
{response}
+ }
+
+ if (status === 'success' && typeof(response) === 'string') {
+ return {response}
+ }
+
+ 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 (
+
+ )
+}
+
+export default App
diff --git a/redisinsight/ui/src/packages/redistimeseries-app/src/components/Chart/Chart.tsx b/redisinsight/ui/src/packages/redistimeseries-app/src/components/Chart/Chart.tsx
new file mode 100644
index 0000000000..929187defe
--- /dev/null
+++ b/redisinsight/ui/src/packages/redistimeseries-app/src/components/Chart/Chart.tsx
@@ -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()
+
+ 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[] {
+ 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 {
+ const axisConfig: { [key: string]: Partial } = {
+ 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