diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 00000000..b89acaa0
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,24 @@
+# http://editorconfig.org
+root = true
+
+[*]
+charset = utf-8
+end_of_line = lf
+indent_style = space
+indent_size = 4
+insert_final_newline = true
+trim_trailing_whitespace = true
+max_line_length = 80
+
+[*.{css, scss, js, html}]
+indent_size = 2
+max_line_length = 140
+
+[*.md]
+trim_trailing_whitespace = false
+
+[*.{yml, yaml}]
+indent_size = 2
+
+[*.json]
+indent_size = 2
diff --git a/.eslintignore b/.eslintignore
new file mode 100644
index 00000000..4e7a01d6
--- /dev/null
+++ b/.eslintignore
@@ -0,0 +1,4 @@
+!.*.js
+**/*.min.js
+node_modules/**
+rt_dashboard/templates/rt_dashboard/dashboard.js
diff --git a/.eslintrc.js b/.eslintrc.js
new file mode 100644
index 00000000..45c4f45f
--- /dev/null
+++ b/.eslintrc.js
@@ -0,0 +1,24 @@
+module.exports = {
+ extends: [
+ 'standard',
+ ],
+ parser: 'babel-eslint',
+ parserOptions: {
+ sourceType: 'module',
+ },
+
+ env: {
+ browser: true,
+ es2020: true,
+ },
+
+ rules: {
+ 'comma-dangle': ['error', {
+ arrays: 'always-multiline',
+ objects: 'always-multiline',
+ imports: 'always-multiline',
+ exports: 'always-multiline',
+ functions: 'always-multiline',
+ }],
+ },
+}
diff --git a/.gitignore b/.gitignore
index e90d5e00..b1a4a77b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,3 +8,5 @@ dump.rdb
/.direnv
._*
.pytest_cache
+**/static
+**/node_modules
diff --git a/.nvmrc b/.nvmrc
new file mode 100644
index 00000000..66df3b7a
--- /dev/null
+++ b/.nvmrc
@@ -0,0 +1 @@
+12.16.1
diff --git a/.travis.yml b/.travis.yml
index 130fef63..55550214 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -6,9 +6,13 @@ python:
- "3.6"
- "3.7"
- "3.8"
+node_js: "12"
install:
- pip install -r requirements.txt
- pip install -e .
+ - nvm install $(cat .nvmrc) && nvm use
+ - yarn
script:
- pytest
- flake8 .
+ - yarn lint
diff --git a/README.md b/README.md
deleted file mode 100644
index 959d630c..00000000
--- a/README.md
+++ /dev/null
@@ -1,10 +0,0 @@
-rt-dashboard
-============
-
-[](https://travis-ci.com/djangsters/rt-dashboard)
-[](https://badge.fury.io/py/rt-dashboard)
-
-
-`rt-dashboard` allows you to monitor your [redis-tasks](
-https://github.com/djangsters/redis-tasks) queues, jobs, and workers
- in realtime as well as inspect the historical tasks timeline.
diff --git a/babel.config.js b/babel.config.js
new file mode 100644
index 00000000..92f5f6bd
--- /dev/null
+++ b/babel.config.js
@@ -0,0 +1,29 @@
+module.exports = {
+ env: {
+ production: {
+ presets: [
+ [
+ '@babel/preset-env',
+ {
+ useBuiltIns: 'entry',
+ },
+ ],
+ ],
+ },
+ test: {
+ presets: [
+ [
+ '@babel/preset-env',
+ {
+ targets: {
+ node: 'current',
+ },
+ },
+ ],
+ ],
+ },
+ },
+ plugins: [
+ '@babel/plugin-proposal-nullish-coalescing-operator',
+ ],
+}
diff --git a/package.json b/package.json
new file mode 100644
index 00000000..453077e9
--- /dev/null
+++ b/package.json
@@ -0,0 +1,82 @@
+{
+ "name": "rt-dashboard",
+ "version": "1.0.0",
+ "description": "Redis Tasks Dashboard simple interface to manage and observe tasks",
+ "main": "webpack.config.js",
+ "directories": {
+ "test": "tests"
+ },
+ "scripts": {
+ "test": "jest",
+ "build": "webpack",
+ "start": "webpack-dev-server --watch",
+ "lint": "eslint ."
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/djangsters/rt-dashboard.git"
+ },
+ "keywords": [
+ "rt",
+ "dashboard"
+ ],
+ "author": "Djangsters",
+ "license": "ISC",
+ "bugs": {
+ "url": "https://github.com/djangsters/rt-dashboard/issues"
+ },
+ "homepage": "https://github.com/djangsters/rt-dashboard#readme",
+ "dependencies": {
+ "@babel/plugin-proposal-nullish-coalescing-operator": "^7.8.3",
+ "@fortawesome/fontawesome-free": "^5.13.0",
+ "babel-eslint": "^10.1.0",
+ "bootstrap": "^4.4.1",
+ "command-line-args": "^5.1.1",
+ "css-loader": "^3.4.2",
+ "d3-axis": "^1.0.12",
+ "d3-scale": "^3.2.1",
+ "d3-selection": "^1.4.1",
+ "d3-time": "^1.1.0",
+ "d3-time-format": "^2.2.3",
+ "d3-transition": "^1.3.2",
+ "date-fns": "^2.14.0",
+ "file-loader": "^6.0.0",
+ "html-loader": "^1.0.0",
+ "jquery": "^3.4.1",
+ "popper.js": "^1.16.1"
+ },
+ "devDependencies": {
+ "@babel/core": "^7.9.0",
+ "@babel/preset-env": "^7.9.0",
+ "@babel/preset-react": "^7.9.1",
+ "babel-jest": "^26.0.1",
+ "babel-loader": "^8.1.0",
+ "eslint": "^6.8.0",
+ "eslint-config-standard": "^14.1.1",
+ "eslint-plugin-import": "^2.20.1",
+ "eslint-plugin-node": "^11.0.0",
+ "eslint-plugin-promise": "^4.2.1",
+ "eslint-plugin-standard": "^4.0.1",
+ "jest": "^26.0.1",
+ "mini-css-extract-plugin": "^0.9.0",
+ "node-sass": "^4.13.1",
+ "postcss-loader": "^3.0.0",
+ "sass-loader": "^8.0.2",
+ "style-loader": "^1.1.3",
+ "webpack": "^4.42.0",
+ "webpack-cli": "^3.3.11",
+ "webpack-dev-server": "^3.10.3"
+ },
+ "sideEffects": [
+ "**/*.css",
+ "**/*.scss",
+ "./rt_dashboard/javascript/src/app.js",
+ "./rt_dashboard/javascript/src/components/*/index.js"
+ ],
+ "jest": {
+ "verbose": true,
+ "transform": {
+ "^.+\\.js$": "babel-jest"
+ }
+ }
+}
diff --git a/rt_dashboard/contrib/django/__init__.py b/rt_dashboard/contrib/django/__init__.py
index f698d17d..e896dfcc 100644
--- a/rt_dashboard/contrib/django/__init__.py
+++ b/rt_dashboard/contrib/django/__init__.py
@@ -25,7 +25,7 @@ def view(request):
else:
return render(request, 'rt_dashboard_admin.html', {
'title': 'RT Dashboard',
- 'iframe_src': base_path + '/inner/',
+ 'iframe_src': base_path + '/inner/app',
})
return view
diff --git a/rt_dashboard/contrib/django/templates/rt_dashboard_admin.html b/rt_dashboard/contrib/django/templates/rt_dashboard_admin.html
index 92678fd3..c6455bde 100644
--- a/rt_dashboard/contrib/django/templates/rt_dashboard_admin.html
+++ b/rt_dashboard/contrib/django/templates/rt_dashboard_admin.html
@@ -3,7 +3,7 @@
{% block content %}
+
+
diff --git a/rt_dashboard/javascript/src/components/history/HistoryChart.js b/rt_dashboard/javascript/src/components/history/HistoryChart.js
new file mode 100644
index 00000000..ad879399
--- /dev/null
+++ b/rt_dashboard/javascript/src/components/history/HistoryChart.js
@@ -0,0 +1,105 @@
+import gantt from './gantt'
+import { timeDay, timeHour } from 'd3-time'
+import { loadTemplate } from '../../utils/dom'
+import templateHtml from './HistoryChart.html'
+
+export default class HistoryChart extends HTMLElement {
+ constructor () {
+ super()
+
+ this.attachShadow({ mode: 'open' })
+
+ loadTemplate(this.shadowRoot, templateHtml)
+
+ this._root = this.shadowRoot.getElementById('chart-container')
+ }
+
+ zoomIn () {
+ this._gantt.zoomInOut('in')
+ }
+
+ zoomOut () {
+ this._gantt.zoomInOut('out')
+ }
+
+ panLeft () {
+ this._gantt.panView('left', 0.30)
+ }
+
+ panRight () {
+ this._gantt.panView('right', 0.30)
+ }
+
+ setHistoryData ({ rows }) {
+ const eventTypes = {}
+ const eventList = rows
+ .map((item) => {
+ const key = `${item.group}-${item.subgroup}`
+ if (!(key in eventTypes)) {
+ eventTypes[key] = true
+ }
+ switch (item.type) {
+ case 'range':
+ return {
+ startDate: +item.start,
+ endDate: +item.end,
+ taskName: key,
+ toolTipHTML: item.title,
+ }
+ case 'point':
+ return {
+ startDate: +item.start,
+ endDate: (+item.start) + 1,
+ taskName: key,
+ toolTipHTML: item.title,
+ }
+ default:
+ throw new Error(`Unsupported item type ${item.type}`)
+ }
+ })
+
+ const eventStyleClassList = [
+ 'blue-bar',
+ 'purple-bar',
+ 'red-bar',
+ 'green-bar',
+ 'orange-bar',
+ ]
+
+ const ganttConfig = {
+ root: this._root,
+ eventSettings: {
+ eventList,
+ eventTypes: Object.keys(eventTypes),
+ eventStyleClassList,
+ enableToolTips: true,
+ },
+ sizing: {
+ location: 'GanttChart',
+ margin: {
+ top: 60,
+ right: 40,
+ bottom: 20,
+ left: 120,
+ },
+ height: '400',
+ width: window.innerWidth,
+ },
+ timeDomainSettings: {
+ zoomLevels: ['5:sec', '15:sec', '1:min', '5:min', '15:min', '1:hr', '3:hr', '6:hr', '1:day'],
+ timeDomainStart: timeDay.offset(new Date(), -3),
+ timeDomainEnd: timeHour.offset(new Date(), +3),
+ startingTimeFormat: '%H:%M',
+ startingTimeDomainString: '1day',
+ startingZoomLevel: 8,
+ timeDomainMode: 'fixed',
+ },
+ }
+
+ if (this._gantt) {
+ this._gantt.redraw()
+ } else {
+ this._gantt = gantt(ganttConfig)
+ }
+ }
+}
diff --git a/rt_dashboard/javascript/src/components/history/HistoryPage.html b/rt_dashboard/javascript/src/components/history/HistoryPage.html
new file mode 100644
index 00000000..48426d38
--- /dev/null
+++ b/rt_dashboard/javascript/src/components/history/HistoryPage.html
@@ -0,0 +1,18 @@
+
+
+
+
+
diff --git a/rt_dashboard/javascript/src/components/history/HistoryPage.js b/rt_dashboard/javascript/src/components/history/HistoryPage.js
new file mode 100644
index 00000000..1bb645c7
--- /dev/null
+++ b/rt_dashboard/javascript/src/components/history/HistoryPage.js
@@ -0,0 +1,43 @@
+import templateHtml from './HistoryPage.html'
+import styles from '../../../styles/main.scss'
+import { loadTemplate, whenUpgraded } from '../../utils/dom'
+import { getHistory } from '../../api'
+
+export default class HistoryPage extends HTMLElement {
+ constructor () {
+ super()
+
+ this.attachShadow({ mode: 'open' })
+
+ loadTemplate(this.shadowRoot, templateHtml, styles)
+
+ this._chart = this.shadowRoot.getElementById('chart')
+
+ this._controller = new AbortController()
+
+ this.shadowRoot.getElementById('zoom-in').addEventListener('click', () => this._chart.zoomIn())
+ this.shadowRoot.getElementById('zoom-out').addEventListener('click', () => this._chart.zoomOut())
+ this.shadowRoot.getElementById('pan-left').addEventListener('click', () => this._chart.panLeft())
+ this.shadowRoot.getElementById('pan-right').addEventListener('click', () => this._chart.panRight())
+ }
+
+ async fetchData () {
+ if (process.env.NODE_ENV === 'production') {
+ return getHistory({ signal: this._controller.signal })
+ }
+ return import('./history').then((data) => data.default || data)
+ }
+
+ async connectedCallback () {
+ const data = await this.fetchData()
+ if (data) {
+ await whenUpgraded(this._chart)
+ this._chart.setHistoryData(data)
+ }
+ }
+
+ disconnectedCallback () {
+ // cancel the pending request if any
+ this._controller.abort()
+ }
+}
diff --git a/rt_dashboard/javascript/src/components/history/gantt.js b/rt_dashboard/javascript/src/components/history/gantt.js
new file mode 100644
index 00000000..8c46f75a
--- /dev/null
+++ b/rt_dashboard/javascript/src/components/history/gantt.js
@@ -0,0 +1,406 @@
+import { timeHour, timeSecond } from 'd3-time'
+import { scaleBand, scaleTime } from 'd3-scale'
+import { axisBottom, axisLeft } from 'd3-axis'
+import { timeFormat } from 'd3-time-format'
+import { event, select } from 'd3-selection'
+import 'd3-transition'
+
+/**
+ *
+ * @param config
+ * @returns {any}
+ */
+export default function ganttOuter (config) {
+ var eventList = []
+
+ var eventTypes
+ var eventStyleClasses = ['event']
+ var eventStyleCount
+
+ var height
+ var width
+ var margin
+
+ var minDate = new Date()
+ var maxDate = new Date()
+
+ var currentViewBeginTime// = d3.time.day.offset(getEndDate(), -1);
+ var currentViewEndTime//= getEndDate();
+ gantt.getCurrentViewCenterTime = function () {
+ var centerTime = timeSecond.offset(currentViewBeginTime, (((currentViewEndTime - currentViewBeginTime) / 2) / 1000))
+ return (centerTime)
+ }
+
+ var zoomLevels
+ var currentZoomLevel
+
+ var tickFormat
+
+ if (config != null) {
+ eventList = config.eventSettings.eventList
+ eventTypes = config.eventSettings.eventTypes
+ eventStyleClasses = config.eventSettings.eventStyleClassList
+ eventStyleCount = eventStyleClasses.length
+
+ setMinMaxDate(eventList)
+
+ margin = config.sizing.margin
+ height = config.sizing.height - margin.top - margin.bottom - 5
+ width = config.sizing.width - margin.right - margin.left - 5
+ if (config.eventSettings.eventTypes.length > 9) {
+ height = config.eventSettings.eventTypes.length * 40
+ }
+
+ currentViewBeginTime = timeHour.offset(minDate, -1)
+ currentViewEndTime = timeHour.offset(maxDate, +1)
+
+ currentZoomLevel = config.timeDomainSettings.startingZoomLevel
+
+ zoomLevels = config.timeDomainSettings.zoomLevels
+
+ tickFormat = config.timeDomainSettings.startingTimeFormat
+
+ var x = scaleTime().domain([currentViewBeginTime, currentViewEndTime]).range([0, width]).clamp(true)
+
+ var y = scaleBand().domain(eventTypes).rangeRound([0, height - margin.top - margin.bottom], 0.1)
+
+ var xAxis = axisBottom(x).tickFormat(timeFormat(tickFormat)) // .tickSubdivide(true)
+ .tickSize(8).tickPadding(8)
+
+ var yAxis = axisLeft(y).tickSize(0)
+ }
+
+ //= =========================================================================================================================
+ // Other
+ // --------------------------------------------------------------------------------------------------------------------------
+ var keyFunction = function (d) {
+ return d.startDate + d.taskName + d.endDate
+ }
+
+ var rectTransform = function (d) {
+ return 'translate(' + x(d.startDate) + ',' + y(d.taskName) + ')'
+ }
+
+ function setMinMaxDate (eventList) {
+ eventList.sort(function (a, b) {
+ return a.endDate - b.endDate
+ })
+ maxDate = eventList[eventList.length - 1].endDate
+ eventList.sort(function (a, b) {
+ return a.startDate - b.startDate
+ })
+ minDate = eventList[0].startDate
+ }
+
+ var tooltipdiv = select('body').append('div')
+ .attr('class', 'tooltip')
+ .style('opacity', 0)
+
+ // var drawControls = function () {
+ // select('#' + config.sizing.location).append('div')
+ // .attr('id', config.sizing.location + '-Controls')
+ // .attr('style', 'margin-left:' + width / 2 + 'px; width:400px;')
+ // select('#' + config.sizing.location + '-Controls')
+ // // .append("button")
+ // .append('i')
+ // .attr('class', 'fa fa-arrow-left fa-3x nav')
+ // .attr('style', 'margin-left:20px; border: 2px solid; border-radius: 10px; padding:3px 5px 3px 5px')
+ // .attr('onclick', config.sizing.location + ".panView('left',.30)")
+ // select('#' + config.sizing.location + '-Controls')
+ // .append('i')
+ // .attr('class', 'fa fa-arrow-right fa-3x')
+ // .attr('style', 'margin-left:20px; border: 2px solid; border-radius: 10px; padding:3px 5px 3px 5px')
+ // .attr('onclick', config.sizing.location + ".panView('right',.30)")
+ // select('#' + config.sizing.location + '-Controls')
+ // .append('i')
+ // .attr('class', 'fa fa-search-plus fa-3x')
+ // .attr('style', 'margin-left:20px; border: 2px solid; border-radius: 10px; padding:3px 5px 3px 5px')
+ // .attr('onclick', config.sizing.location + ".zoomInOut('in')")
+ // select('#' + config.sizing.location + '-Controls')
+ // .append('i')
+ // .attr('class', 'fa fa-search-minus fa-3x')
+ // .attr('style', 'margin-left:20px; border: 2px solid; border-radius: 10px; padding:3px 5px 3px 5px')
+ // .attr('onclick', config.sizing.location + ".zoomInOut('out')")
+ // }
+
+ //= =========================================================================================================================
+ // Define sizing and axis
+ // --------------------------------------------------------------------------------------------------------------------------
+
+ var initAxis = function () {
+ x = scaleTime().domain([currentViewBeginTime, currentViewEndTime]).range([0, width]).clamp(true)
+ y = scaleBand().domain(eventTypes).rangeRound([0, (height) - margin.top - margin.bottom], 0.2)
+ xAxis = axisBottom(x).tickFormat(timeFormat(tickFormat)) // .tickSubdivide(true)
+ .tickSize(8).tickPadding(8)
+
+ yAxis = axisLeft(y).tickSize(0)
+ }
+
+ //= =========================================================================================================================
+ // Sizing
+ // --------------------------------------------------------------------------------------------------------------------------
+
+ gantt.width = function (value) {
+ if (!arguments.length) { return width }
+ width = +value
+ return gantt
+ }
+
+ gantt.height = function (value) {
+ if (!arguments.length) { return height }
+ height = +value
+ return gantt
+ }
+
+ gantt.margin = function (value) {
+ if (!arguments.length) { return margin }
+ margin = value
+ return gantt
+ }
+
+ // --------------------------------------------------------------------------------------------------------------------------
+
+ /**
+ * @param {string}
+ * vale The value can be "fit" - the domain fits the data or
+ * "fixed" - fixed domain.
+ */
+
+ gantt.taskTypes = function (value) {
+ if (!arguments.length) { return eventTypes }
+ eventTypes = value
+ return gantt
+ }
+
+ gantt.eventStyles = function (value) {
+ if (!arguments.length) { return eventStyleClasses }
+ eventStyleClasses = value
+ return gantt
+ }
+
+ gantt.tickFormat = function (value) {
+ if (!arguments.length) { return tickFormat }
+ tickFormat = value
+ return gantt
+ }
+
+ // function getEndDate () {
+ // var lastEndDate = Date.now()
+ // if (eventList.length > 0) {
+ // lastEndDate = eventList[eventList.length - 1].endDate
+ // }
+ // return lastEndDate
+ // }
+
+ //= =========================================================================================================================
+ // Draw / ReDraw Code
+ // --------------------------------------------------------------------------------------------------------------------------
+
+ function gantt (eventList) {
+ initAxis()
+
+ gantt._root = select(config.root)
+ gantt._svgRoot = gantt._root
+ .append('svg')
+ .attr('class', 'chart')
+ .attr('id', config.sizing.location + '-ChartId')
+ .attr('width', width + margin.left + margin.right)
+ .attr('height', height + margin.top + margin.bottom)
+
+ gantt._svg = gantt._svgRoot.append('g')
+ .attr('class', 'gantt-chart')
+ .attr('width', width + margin.left + margin.right)
+ .attr('height', height + margin.top + margin.bottom)
+ .attr('transform', 'translate(' + margin.left + ', ' + margin.top + ')')
+
+ gantt._svg.selectAll('.chart')
+ .data(eventList, keyFunction).enter()
+ .append('rect')
+ .attr('rx', 5)
+ .attr('ry', 5)
+ .attr('data-status', function (d) {
+ return d.status
+ })
+ .attr('class', function (d) {
+ var styleId = d.status % eventStyleCount
+ if (eventStyleClasses[styleId] == null) {
+ return 'bar'
+ }
+ return eventStyleClasses[styleId]
+ })
+ .attr('y', 0)
+ .attr('transform', rectTransform)
+ .attr('height', function (d) {
+ return y.bandwidth()
+ })
+ .attr('width', function (d) {
+ return (x(d.endDate) - x(d.startDate))
+ })
+ .on('mouseover', function (d) {
+ if (config.eventSettings.enableToolTips && d.toolTipHTML) {
+ tooltipdiv.transition()
+ .duration(200)
+ .style('opacity', 0.9)
+ tooltipdiv.html(d.toolTipHTML)
+ .style('left', (event.pageX) + 'px')
+ .style('top', (event.pageY - 28) + 'px')
+ }
+ })
+ .on('mouseout', function (d) {
+ tooltipdiv.transition()
+ .duration(500)
+ .style('opacity', 0)
+ })
+ .on('click', function (d) {
+
+ })
+
+ gantt._x = gantt._svg.append('g')
+ .attr('class', 'x axis')
+ .attr('transform', 'translate(0, ' + (height - margin.top - margin.bottom) + ')')
+
+ gantt._x
+ .transition()
+ .call(xAxis)
+
+ gantt._y = gantt._svg.append('g')
+ .attr('class', 'y axis')
+
+ gantt._y
+ .transition()
+ .call(yAxis)
+
+ return gantt
+ }
+
+ gantt.redraw = function (eventList) {
+ initAxis()
+
+ var rect = this._svg.selectAll('rect').data(eventList, keyFunction)
+
+ rect.enter()
+ .insert('rect', ':first-child')
+ .attr('rx', 5)
+ .attr('ry', 5)
+ .attr('class', function (d) {
+ if (eventStyleClasses[d.status % eventStyleCount] == null) {
+ return 'bar'
+ }
+ return eventStyleClasses[d.status % eventStyleCount]
+ })
+ .transition()
+ .attr('y', 0)
+ .attr('transform', rectTransform)
+ .attr('height', function (d) {
+ return y.bandwidth()
+ })
+ .attr('width', function (d) {
+ return (x(d.endDate) - x(d.startDate))
+ })
+
+ rect.transition()
+ .attr('transform', rectTransform)
+ .attr('height', function (d) {
+ return y.bandwidth()
+ })
+ .attr('width', function (d) {
+ return (x(d.endDate) - x(d.startDate))
+ })
+
+ rect.exit().remove()
+
+ this._x.transition().call(xAxis)
+ this._y.transition().call(yAxis)
+
+ return gantt
+ }
+
+ //= ================================================================================================================
+ // Time Domain Code
+ // ------------------------------------------------------------------------------------------------------------------
+ gantt.zoomInOut = function (inOrOut) {
+ if (inOrOut === 'in') {
+ if (currentZoomLevel === 0) {
+ alert("can't zoom in anymore")
+ } else {
+ currentZoomLevel--
+ }
+ } else if (inOrOut === 'out') {
+ if (currentZoomLevel === (zoomLevels.length - 1)) {
+ alert("Can't zoom out anymore")
+ } else {
+ currentZoomLevel++
+ }
+ } else {
+ alert("Error: Not sure if you're trying to zoom in or out. Check zoom function call")
+ }
+ this.setCustomZoom(currentZoomLevel)
+ }
+
+ gantt.setCustomZoom = function (zoomLevel) {
+ currentZoomLevel = zoomLevel
+ var currentCenterTime = this.getCurrentViewCenterTime()
+ currentViewBeginTime = timeSecond.offset(currentCenterTime, -this.convertZoomLevelToOffsetSeconds())
+ currentViewEndTime = timeSecond.offset(currentCenterTime, this.convertZoomLevelToOffsetSeconds())
+ // gantt.timeDomain([currentViewBeginTime, currentViewEndTime]);
+ this.tickFormat(this.determineTimeFormat(zoomLevels[currentZoomLevel].split(':')[1]))
+ this.redraw(eventList)
+ }
+
+ gantt.convertZoomLevelToOffsetSeconds = function () {
+ var zoomLevelString = zoomLevels[currentZoomLevel]
+ var rangeNumber = zoomLevelString.split(':')[0]
+ var rangeScale = zoomLevelString.split(':')[1]
+ var secondsResult
+ switch (rangeScale) {
+ case 'day':
+ secondsResult = rangeNumber * 86400
+ break
+ case 'hr':
+ secondsResult = rangeNumber * 3600
+ break
+ case 'min':
+ secondsResult = rangeNumber * 60
+ break
+ case 'sec':
+ secondsResult = rangeNumber
+ break
+ default:
+ secondsResult = 86400
+ }
+ return Math.round(secondsResult / 2)
+ }
+
+ gantt.determineTimeFormat = function (scale) {
+ switch (scale) {
+ case 'day':
+ return '%H:%M'
+ case 'hr':
+ return '%H:%M'
+ case 'min':
+ return '%H:%M:%S'
+ case 'sec':
+ return '%H:%M:%S'
+ default:
+ return '%H:%M'
+ }
+ }
+ // ------------------------------------------------------------------------------------------------------------------
+
+ gantt.panView = function (direction, percentMove) {
+ // gets the length in MS of X% of the current view. This will be used to determine how far to pan at a time
+ var shiftTimeLength = ((currentViewEndTime - currentViewBeginTime) * percentMove)
+ if (direction === 'left') {
+ shiftTimeLength = -shiftTimeLength
+ }
+ // convert our shift length to Sec and offset our current view start/end times. Once shifted, update the current view to these new times
+ var newStartTime = timeSecond.offset(currentViewBeginTime, (shiftTimeLength / 1000))
+ var newEndTime = timeSecond.offset(currentViewEndTime, (shiftTimeLength / 1000))
+ // this.timeDomain([ newStartTime , newEndTime ]);
+ currentViewBeginTime = newStartTime
+ currentViewEndTime = newEndTime
+ this.redraw(eventList)
+ }
+
+ return gantt(eventList)
+};
diff --git a/rt_dashboard/javascript/src/components/history/history.json b/rt_dashboard/javascript/src/components/history/history.json
new file mode 100644
index 00000000..d69fbcfd
--- /dev/null
+++ b/rt_dashboard/javascript/src/components/history/history.json
@@ -0,0 +1,78 @@
+{
+ "rows": [
+ {
+ "group": "maintenance.feature",
+ "subgroup": 0,
+ "start": 1585200000,
+ "end": 1585300000,
+ "title": "Maintenance of feature 1",
+ "type": "range"
+ },
+ {
+ "group": "maintenance.feature",
+ "subgroup": 1,
+ "start": 1585220000,
+ "end": 1585400000,
+ "title": "Maintenance of feature 2",
+ "type": "range"
+ },
+ {
+ "group": "accident.feature",
+ "subgroup": 0,
+ "start": 1585500000,
+ "end": 1585550000,
+ "title": "Accident of feature 1",
+ "type": "range"
+ },
+ {
+ "group": "maintenance.feature",
+ "subgroup": 0,
+ "start": 1585600000,
+ "end": 1585650000,
+ "title": "Maintenance of feature 1",
+ "type": "range"
+ },
+ {
+ "group": "point.run",
+ "subgroup": 0,
+ "start": "1585210000",
+ "title": "Run 1",
+ "type": "point"
+ },
+ {
+ "group": "point.run",
+ "subgroup": 0,
+ "start": "1585220000",
+ "title": "Run 2",
+ "type": "point"
+ },
+ {
+ "group": "point.run",
+ "subgroup": 0,
+ "start": "1585230000",
+ "title": "Run 3",
+ "type": "point"
+ },
+ {
+ "group": "point.run",
+ "subgroup": 1,
+ "start": "1585215000",
+ "title": "Run 1",
+ "type": "point"
+ },
+ {
+ "group": "point.run",
+ "subgroup": 1,
+ "start": "1585225000",
+ "title": "Run 2",
+ "type": "point"
+ },
+ {
+ "group": "point.run",
+ "subgroup": 1,
+ "start": "1585235000",
+ "title": "Run 3",
+ "type": "point"
+ }
+ ]
+}
diff --git a/rt_dashboard/javascript/src/components/history/index.js b/rt_dashboard/javascript/src/components/history/index.js
new file mode 100644
index 00000000..e2404107
--- /dev/null
+++ b/rt_dashboard/javascript/src/components/history/index.js
@@ -0,0 +1,5 @@
+import HistoryPage from './HistoryPage'
+import HistoryChart from './HistoryChart'
+
+customElements.define('rt-history', HistoryPage)
+customElements.define('rt-history-chart', HistoryChart)
diff --git a/rt_dashboard/javascript/src/components/pager/index.js b/rt_dashboard/javascript/src/components/pager/index.js
new file mode 100644
index 00000000..a191882f
--- /dev/null
+++ b/rt_dashboard/javascript/src/components/pager/index.js
@@ -0,0 +1,3 @@
+import Pager from './pager'
+
+window.customElements.define('pager-component', Pager)
diff --git a/rt_dashboard/javascript/src/components/pager/pager.html b/rt_dashboard/javascript/src/components/pager/pager.html
new file mode 100644
index 00000000..99204abf
--- /dev/null
+++ b/rt_dashboard/javascript/src/components/pager/pager.html
@@ -0,0 +1,4 @@
+
+
+
diff --git a/rt_dashboard/javascript/src/components/pager/pager.js b/rt_dashboard/javascript/src/components/pager/pager.js
new file mode 100644
index 00000000..8e39415d
--- /dev/null
+++ b/rt_dashboard/javascript/src/components/pager/pager.js
@@ -0,0 +1,104 @@
+import templateHtml from './pager.html'
+import styles from '../../../styles/main.scss'
+import { loadTemplate, mapDataToElements, appendElement } from '../../utils/dom'
+
+export default class Pager extends HTMLElement {
+ get pagination () {
+ return this.paging
+ }
+
+ /**
+ * Sets current paggination settings
+ * @param {object} value New paggination settings
+ * @param {{ number: number, url: string }[]} value.pages_in_window Array of page numbers
+ * @param {string} value.next_page Url to the next page data
+ * @param {string} value.prev_page Url to the previous page data
+ * @param {number} value.currentPage Current page number
+ */
+ set pagination (value) {
+ this.paging = value
+ this.update(value)
+ }
+
+ constructor () {
+ super()
+
+ loadTemplate(this.attachShadow({ mode: 'open' }), templateHtml, styles)
+
+ this.mapToPageLinks = this.mapToPageLinks.bind(this)
+ this.onPageClicked = this.onPageClicked.bind(this)
+
+ this.current = 1
+ }
+
+ disconnectedCallback () {
+ this.removeEventListeners()
+ }
+
+ onPageClicked (e) {
+ const { target: pageLink } = e
+ const number = pageLink.page
+ this.current = number
+
+ this.dispatchEvent(
+ new CustomEvent('change', {
+ detail: { number },
+ bubbles: true,
+ }),
+ )
+
+ e.preventDefault()
+ return false
+ }
+
+ update ({
+ pages_in_window: pagesArray,
+ next_page: nextPage,
+ prev_page: prevPage,
+ currentPage,
+ }) {
+ let nextPageNum = null
+ let prevPageNum = null
+ if (prevPage) {
+ prevPageNum = prevPage.url.split('/').pop()
+ }
+ if (nextPage) {
+ nextPageNum = nextPage.url.split('/').pop()
+ }
+ if (currentPage) {
+ this.current = currentPage
+ }
+
+ const pages = [
+ { text: '«', number: prevPageNum },
+ ...pagesArray.map(({ number }) => ({ text: number, number })),
+ { text: '»', number: nextPageNum },
+ ]
+ this.pageLinks = []
+ this.removeEventListeners()
+
+ const pagesList = this.shadowRoot.querySelector('ul.pagination')
+ mapDataToElements(pagesList, pages, this.mapToPageLinks)
+ }
+
+ removeEventListeners () {
+ this.pageLinks.forEach(l => {
+ l.removeEventListener('click', this.onPageClicked)
+ })
+ this.pageLinks = []
+ }
+
+ mapToPageLinks (parent, page) {
+ const disabledClass = page.number ? '' : 'disabled'
+ const activeClass = `${this.current}` === `${page.number}` ? 'active' : ''
+ const li = appendElement('li', parent, `page-item ${disabledClass} ${activeClass}`)
+
+ const link = appendElement('a', li, 'page-link', page.text)
+ if (page.number) {
+ link.setAttribute('href', `#${page.number}`)
+ link.page = page.number
+ link.addEventListener('click', this.onPageClicked)
+ this.pageLinks.push(link)
+ }
+ }
+}
diff --git a/rt_dashboard/javascript/src/components/queues/index.js b/rt_dashboard/javascript/src/components/queues/index.js
new file mode 100644
index 00000000..c9f15fdc
--- /dev/null
+++ b/rt_dashboard/javascript/src/components/queues/index.js
@@ -0,0 +1,3 @@
+import Queues from './queues'
+
+window.customElements.define('queues-component', Queues)
diff --git a/rt_dashboard/javascript/src/components/queues/queues.html b/rt_dashboard/javascript/src/components/queues/queues.html
new file mode 100644
index 00000000..88ac1971
--- /dev/null
+++ b/rt_dashboard/javascript/src/components/queues/queues.html
@@ -0,0 +1,19 @@
+
+
Queues
+
This list below contains all the registered queues with the number of tasks currently
+ in the queue. Select a queue from above to view all tasks currently pending on the queue.
+
+
+
+
+ Queue
+ Tasks
+
+
+
+
+ Loading...
+
+
+
+
diff --git a/rt_dashboard/javascript/src/components/queues/queues.js b/rt_dashboard/javascript/src/components/queues/queues.js
new file mode 100644
index 00000000..a6190f91
--- /dev/null
+++ b/rt_dashboard/javascript/src/components/queues/queues.js
@@ -0,0 +1,107 @@
+import templateHtml from './queues.html'
+import styles from '../../../styles/main.scss'
+import { appendElement, loadTemplate, mapDataToElements, appendNoDataRow } from '../../utils/dom'
+import { getQueues } from '../../api'
+
+export default class Queues extends HTMLElement {
+ constructor () {
+ super()
+
+ loadTemplate(this.attachShadow({ mode: 'open' }), templateHtml, styles)
+
+ this.queuesLinks = []
+
+ this.onQueueClicked = this.onQueueClicked.bind(this)
+ this.onRefresh = this.onRefresh.bind(this)
+ this.refreshQueues = this.refreshQueues.bind(this)
+ this.removeQueueClickHandlers = this.removeQueueClickHandlers.bind(this)
+ }
+
+ connectedCallback () {
+ document.addEventListener('refresh', this.onRefresh)
+ this.refreshQueues()
+ }
+
+ disconnectedCallback () {
+ document.removeEventListener('refresh', this.onRefresh)
+ this.removeQueueClickHandlers()
+ }
+
+ async refreshQueues () {
+ const data = await getQueues()
+ if (!data) {
+ return
+ }
+ const { queues } = data
+ const tbody = this.shadowRoot.querySelector('tbody')
+ this.removeQueueClickHandlers()
+
+ if (!queues || queues.length <= 0) {
+ appendNoDataRow(tbody, 'No queues.', 2)
+ return
+ }
+
+ mapDataToElements(tbody, queues, this.fillRow)
+ Array.from(this.shadowRoot.querySelectorAll('td a')).forEach(link => {
+ this.queuesLinks.push(link)
+ link.addEventListener('click', this.onQueueClicked)
+ })
+
+ let [first] = queues
+ first = queues.find(q => q.name.startsWith('[running')) ?? first
+ const selectedQueue = this.selectedQueue ? queues.find(q => q.name === this.selectedQueue.name) : first
+ this.sendChangedEvent(selectedQueue)
+ }
+
+ onRefresh () {
+ this.refreshQueues()
+ }
+
+ removeQueueClickHandlers () {
+ this.queuesLinks.forEach(link => {
+ link.removeEventListener('click', this.onQueueClicked)
+ })
+ this.queuesLinks = []
+ }
+
+ fillRow (parent, { name, url, count }) {
+ const row = appendElement('tr', parent)
+ row.setAttribute('data-role', 'queue')
+
+ const td = appendElement('td', row)
+ appendElement('i', td, 'fas fa-inbox')
+
+ const link = appendElement('a', td, 'ml-1', name)
+ link.setAttribute('name', name)
+ link.setAttribute('data-count', count)
+ link.setAttribute('href', url)
+
+ appendElement('td', row, 'narrow', count)
+ }
+
+ onQueueClicked (e) {
+ const { target: selectedQueue } = e
+
+ this.sendChangedEvent(selectedQueue)
+ e.preventDefault()
+ return false
+ }
+
+ sendChangedEvent ({ name: queueName, count }) {
+ if (this.selectedQueue && this.selectedQueue.name === queueName && this.selectedQueue.count === count) {
+ return
+ }
+
+ this.dispatchEvent(
+ new CustomEvent('selectedQueueChange', {
+ detail: { queueName, count },
+ bubbles: true,
+ }),
+ )
+
+ this.selectedQueue = {
+ name: queueName,
+ count,
+ }
+ }
+}
diff --git a/rt_dashboard/javascript/src/components/rt-dashboard/index.js b/rt_dashboard/javascript/src/components/rt-dashboard/index.js
new file mode 100644
index 00000000..5760bb6c
--- /dev/null
+++ b/rt_dashboard/javascript/src/components/rt-dashboard/index.js
@@ -0,0 +1,3 @@
+import RtDashboard from './rt-dashboard'
+
+window.customElements.define('rt-dashboard', RtDashboard)
diff --git a/rt_dashboard/javascript/src/components/rt-dashboard/rt-dashboard.html b/rt_dashboard/javascript/src/components/rt-dashboard/rt-dashboard.html
new file mode 100644
index 00000000..578a3249
--- /dev/null
+++ b/rt_dashboard/javascript/src/components/rt-dashboard/rt-dashboard.html
@@ -0,0 +1,9 @@
+
+
+
+
+
diff --git a/rt_dashboard/javascript/src/components/rt-dashboard/rt-dashboard.js b/rt_dashboard/javascript/src/components/rt-dashboard/rt-dashboard.js
new file mode 100644
index 00000000..4c1a320b
--- /dev/null
+++ b/rt_dashboard/javascript/src/components/rt-dashboard/rt-dashboard.js
@@ -0,0 +1,43 @@
+import templateHtml from './rt-dashboard.html'
+import styles from '../../../styles/main.scss'
+import { loadTemplate } from '../../utils/dom'
+
+const DASHBOARD = 'dashboard-component'
+const HISTORY = 'rt-history'
+
+export default class RtDashboard extends HTMLElement {
+ constructor () {
+ super()
+ loadTemplate(this.attachShadow({ mode: 'open' }), templateHtml, styles)
+
+ this.tabClicked = this.tabClicked.bind(this)
+ }
+
+ connectedCallback () {
+ this.navLinks = []
+ Array.from(this.shadowRoot.querySelectorAll('li a.nav-link')).forEach(link => {
+ this.navLinks.push(link)
+ link.addEventListener('click', this.tabClicked)
+ })
+
+ this.panels = []
+ const panels = [DASHBOARD, HISTORY]
+ panels.forEach(panel => {
+ this.panels.push(this.shadowRoot.querySelector(panel))
+ })
+ }
+
+ disconnectedCallback () {
+ this.navLinks.forEach(link => {
+ link.removeEventListener('click', this.tabClicked)
+ })
+ }
+
+ tabClicked (e) {
+ const { target: activeLink } = e
+ this.navLinks.forEach(l => { l.classList.remove('active') })
+ activeLink.classList.add('active')
+ this.panels.forEach(p => { p.hidden = true })
+ this.panels.find(p => p.tagName.toLowerCase() === activeLink.name).hidden = false
+ }
+}
diff --git a/rt_dashboard/javascript/src/components/tasks/index.js b/rt_dashboard/javascript/src/components/tasks/index.js
new file mode 100644
index 00000000..255a8a47
--- /dev/null
+++ b/rt_dashboard/javascript/src/components/tasks/index.js
@@ -0,0 +1,3 @@
+import Tasks from './tasks'
+
+window.customElements.define('tasks-component', Tasks)
diff --git a/rt_dashboard/javascript/src/components/tasks/tasks.html b/rt_dashboard/javascript/src/components/tasks/tasks.html
new file mode 100644
index 00000000..ad353afb
--- /dev/null
+++ b/rt_dashboard/javascript/src/components/tasks/tasks.html
@@ -0,0 +1,38 @@
+
+
+ Tasks on
+
+
+
+ Empty
+
+
+
+ Delete
+
+
+ This list below contains all the registered tasks on queue
+ {{ queue.name }} , sorted by age
+ (newest on top).
+ (oldest on top).
+
+
+
+
+
+ Name
+ Age
+ Actions
+
+
+
+
+ Loading...
+
+
+
+
+
+
diff --git a/rt_dashboard/javascript/src/components/tasks/tasks.js b/rt_dashboard/javascript/src/components/tasks/tasks.js
new file mode 100644
index 00000000..3929fc80
--- /dev/null
+++ b/rt_dashboard/javascript/src/components/tasks/tasks.js
@@ -0,0 +1,232 @@
+import html from './tasks.html'
+import styles from '../../../styles/main.scss'
+import {
+ loadTemplate,
+ appendElement,
+ appendNoDataRow,
+ mapDataToElements,
+ removeChildNodes,
+ createElement,
+} from '../../utils/dom'
+import { getJobs, cancelJob, deleteQueue, emptyQueue } from '../../api'
+import { duration, relative } from '../../utils/utils'
+
+export default class Tasks extends HTMLElement {
+ get queueInfo () {
+ return this.queueInfo_
+ }
+
+ set queueInfo (val) {
+ this.queueInfo_ = val
+ this.queueInfoChanged(val)
+ }
+
+ constructor () {
+ super()
+ loadTemplate(this.attachShadow({ mode: 'open' }), html, styles)
+
+ this.mapToRow = this.mapToRow.bind(this)
+ this.onEmptyClicked = this.onEmptyClicked.bind(this)
+ this.onDeleteClicked = this.onDeleteClicked.bind(this)
+ this.selectedPageChange = this.selectedPageChange.bind(this)
+ }
+
+ connectedCallback () {
+ this.emptyBtn = this.shadowRoot.querySelector('p.intro #empty-btn')
+ this.deleteBtn = this.shadowRoot.querySelector('p.intro #delete-btn')
+ this.emptyBtn.addEventListener('click', this.onEmptyClicked)
+ this.deleteBtn.addEventListener('click', this.onDeleteClicked)
+
+ const pagerComponent = this.shadowRoot.querySelector('pager-component')
+ pagerComponent.addEventListener('change', this.selectedPageChange)
+ }
+
+ disconnectedCallback () {
+ this.removeEventListener.addEventListener('click', this.onEmptyClicked)
+ this.removeEventListener.addEventListener('click', this.onDeleteClicked)
+
+ this.removeTableClickListeners()
+
+ const pagerComponent = this.shadowRoot.querySelector('pager-component')
+ pagerComponent.removeEventListener('selectedPageChanged', this.selectedPageChange)
+ }
+
+ selectedPageChange ({ detail: { number } }) {
+ this.loadQueueTasks(this.queueInfo_.queue, number)
+ }
+
+ queueInfoChanged (queueInfo) {
+ const { queue, count } = queueInfo
+
+ this.queue = queue
+ this.count = count
+ this.updateTitles(queue, count)
+ this.toggleEmptyBtns(queue, count)
+
+ this.loadQueueTasks(queue)
+ }
+
+ onEmptyClicked () {
+ emptyQueue(this.queue)
+ }
+
+ onDeleteClicked (e) {
+ deleteQueue(this.queue)
+ }
+
+ updateTitles (queue, count) {
+ const queueName = this.shadowRoot.querySelector('h1 strong')
+ queueName.innerHTML = queue
+ if (queue === '[failed]') {
+ queueName.classList.add('failed')
+ } else {
+ queueName.classList.remove('failed')
+ }
+
+ const queueNameSubTitle = this.shadowRoot.querySelector('p.intro strong')
+ queueNameSubTitle.innerHTML = queue
+
+ const orderDescInfo = this.shadowRoot.querySelector('p.intro #oldest-on-top')
+ const orderAscInfo = this.shadowRoot.querySelector('p.intro #newes-on-top')
+ orderDescInfo.hidden = queue.startsWith('[')
+ orderAscInfo.hidden = !queue.startsWith('[')
+ }
+
+ toggleEmptyBtns (queue, count) {
+ const emptyBtn = this.shadowRoot.querySelector('p.intro #empty-btn')
+ const deleteBtn = this.shadowRoot.querySelector('p.intro #delete-btn')
+ emptyBtn.hidden = count <= 0
+ deleteBtn.hidden = count > 0 || (queue.startsWith('['))
+ }
+
+ async loadQueueTasks (queue, page = 1) {
+ this.removeTableClickListeners()
+
+ const tbody = this.shadowRoot.querySelector('tbody')
+ if (tbody.childNodes.length === 0) {
+ removeChildNodes(tbody)
+ appendNoDataRow(tbody, 'Loading...', 3)
+ }
+
+ const data = await getJobs(queue, page)
+ if (!data) {
+ return
+ }
+ const { jobs, pagination } = data
+ if (!jobs || jobs.length <= 0) {
+ appendNoDataRow(tbody, 'No jobs.', 3)
+ return
+ }
+ mapDataToElements(tbody, jobs, this.mapToRow)
+
+ Array.from(this.shadowRoot.querySelectorAll('td a')).forEach(link => {
+ this.cancelLinks.push(link)
+ link.addEventListener('click', this.onCancelClicked)
+ })
+
+ this.updatePager(pagination, page)
+ }
+
+ removeTableClickListeners () {
+ this.cancelLinks = this.cancelLinks || []
+ this.cancelLinks.forEach(link => {
+ link.removeEventListener('click', this.onQueueClicked)
+ })
+
+ this.cancelLinks = []
+ }
+
+ onCancelClicked (e) {
+ const { target: cancelTaskBtn } = e
+ cancelJob(cancelTaskBtn.getAttribute('data-jobid'))
+ e.preventDefault()
+ }
+
+ mapToRow (parent, {
+ id,
+ description,
+ origin,
+ worker,
+ status,
+ error_message: error,
+ enqueued_at: enqueued,
+ started_at: started,
+ ended_at: ended,
+ }) {
+ const row = createElement('tr')
+ row.setAttribute('data-role', 'job')
+ row.setAttribute('data-job-id', id)
+
+ this.mapFirstColumn(row, {
+ id,
+ description,
+ origin,
+ worker,
+ status,
+ error,
+ enqueued,
+ started,
+ ended,
+ })
+
+ const col2 = appendElement('td', row)
+ if (status === 'running') {
+ appendElement('span', col2, 'creation_date', `${duration(started, new Date().getTime())}`)
+ } else {
+ appendElement('span', col2, 'creation_date', `${relative(ended ?? enqueued)}`)
+ }
+
+ const col3 = appendElement('td', row, 'actions narrow')
+ const actionLink = appendElement('a', col3, 'btn btn-outline-secondary btn-sm mx-auto', ' Cancel')
+ actionLink.setAttribute('data-role', 'cancel-job-btn')
+ actionLink.setAttribute('data-jobid', id)
+
+ parent.appendChild(row)
+ }
+
+ mapFirstColumn (row, {
+ id,
+ description,
+ origin,
+ worker,
+ status,
+ error,
+ enqueued,
+ started,
+ ended,
+ }) {
+ const td = appendElement('td', row)
+ appendElement('i', td, 'fas fa-file')
+ appendElement('span', td, 'description ml-1', description)
+
+ if (this.queue.startsWith('[')) {
+ let originInfo = ` from ${origin} `
+ if (worker) {
+ originInfo += ` running on ${worker} `
+ }
+ appendElement('span', td, 'origin', originInfo)
+ }
+
+ appendElement('div', td, 'job_id d-block', id)
+
+ if (status === 'running') {
+ appendElement('span', td, 'end_date', `Enqueued ${relative(enqueued)}`)
+ } else if (status === 'failed') {
+ appendElement('span', td, 'end_date',
+ `Enqueued ${relative(enqueued)}, failed ${relative(ended)}, ran for ${duration(started, ended)}`)
+ appendElement('pre', td, 'exc_info', `${error}
`)
+ } else if (status === 'finished') {
+ appendElement('span', td, 'end_date',
+ `Enqueued ${relative(enqueued)}, finished ${relative(ended)}, ran for ${duration(started, ended)}`)
+ }
+ }
+
+ updatePager (pagination, currentPage = 1) {
+ const pagerComponent = this.shadowRoot.querySelector('pager-component')
+ pagerComponent.pagination = {
+ pages_in_window: [{ number: 1 }],
+ ...pagination,
+ currentPage,
+ }
+ }
+}
diff --git a/rt_dashboard/javascript/src/components/workers/index.js b/rt_dashboard/javascript/src/components/workers/index.js
new file mode 100644
index 00000000..8794ca71
--- /dev/null
+++ b/rt_dashboard/javascript/src/components/workers/index.js
@@ -0,0 +1,3 @@
+import Workers from './workers'
+
+window.customElements.define('workers-component', Workers)
diff --git a/rt_dashboard/javascript/src/components/workers/workers.html b/rt_dashboard/javascript/src/components/workers/workers.html
new file mode 100644
index 00000000..9331999a
--- /dev/null
+++ b/rt_dashboard/javascript/src/components/workers/workers.html
@@ -0,0 +1,32 @@
+
+
+
Workers
+
+
+
+ Toggle workers list
+ No workers registered!
+
+
+
+
+
+ State
+ Worker
+ Queues
+
+
+
+
+ Loading...
+
+
+
+
+
+
+
diff --git a/rt_dashboard/javascript/src/components/workers/workers.js b/rt_dashboard/javascript/src/components/workers/workers.js
new file mode 100644
index 00000000..1dcefa3f
--- /dev/null
+++ b/rt_dashboard/javascript/src/components/workers/workers.js
@@ -0,0 +1,80 @@
+import templateHtml from './workers.html'
+import styles from '../../../styles/main.scss'
+import {
+ loadTemplate,
+ mapDataToElements,
+ appendElement,
+ appendNoDataRow,
+} from '../../utils/dom'
+import { getWorkers } from '../../api'
+
+export default class Workers extends HTMLElement {
+ constructor () {
+ super()
+ loadTemplate(this.attachShadow({ mode: 'open' }), templateHtml, styles)
+
+ this.onWorkersBtnClick = this.onWorkersBtnClick.bind(this)
+ this.onRefresh = this.onRefresh.bind(this)
+ this.refreshWorkers = this.refreshWorkers.bind(this)
+ }
+
+ connectedCallback () {
+ const btn = this.shadowRoot.querySelector('button#workers-btn')
+ btn.addEventListener('click', this.onWorkersBtnClick)
+
+ document.addEventListener('refresh', this.onRefresh)
+
+ this.refreshWorkers()
+ }
+
+ disconnectedCallback () {
+ this.removeEventListener('click', this.onWorkersBtnClick)
+ document.removeEventListener('refresh', this.onRefresh)
+ }
+
+ mapToRow (parent, { name, state, queues }) {
+ const row = appendElement('tr', parent)
+ row.setAttribute('data-role', 'worker')
+
+ const stateIcon = state === 'busy' ? 'play' : 'pause'
+ const td = appendElement('td', row)
+ appendElement('i', td, `fas fa-${stateIcon}`)
+
+ appendElement('td', row, null, name)
+
+ appendElement('td', row, null, queues.join(','))
+ }
+
+ async refreshWorkers () {
+ const data = await getWorkers()
+ if (!data) {
+ return
+ }
+ const { workers } = data
+ const tbody = this.shadowRoot.querySelector('tbody')
+ const workersCount = this.shadowRoot.querySelector('#workers-count span')
+
+ if (!workers || workers.length <= 0) {
+ workersCount.innerHTML = 'No workers registered!'
+ appendNoDataRow(tbody, 'No workers.', 3)
+ return
+ }
+
+ mapDataToElements(tbody, workers, this.mapToRow)
+ workersCount.innerHTML = workers.length + ' workers registered'
+ }
+
+ onRefresh () {
+ this.refreshWorkers()
+ }
+
+ onWorkersBtnClick (event) {
+ const workersTable = this.shadowRoot.querySelector('#workers')
+ const isHidden = workersTable.getAttribute('hidden')
+ if (isHidden) {
+ workersTable.removeAttribute('hidden')
+ } else {
+ workersTable.setAttribute('hidden', true)
+ }
+ }
+}
diff --git a/rt_dashboard/javascript/src/utils/dom.js b/rt_dashboard/javascript/src/utils/dom.js
new file mode 100644
index 00000000..0617123d
--- /dev/null
+++ b/rt_dashboard/javascript/src/utils/dom.js
@@ -0,0 +1,53 @@
+
+export const appendElement = (tag, parent, classes = '', innerHtml = null) => {
+ const element = createElement(tag, classes, innerHtml)
+ parent.appendChild(element)
+ return element
+}
+
+export const createElement = (tag, classes = '', innerHtml = null) => {
+ const element = document.createElement(tag)
+ if (classes) {
+ classes.split(' ').filter(c => c && c.length > 0)
+ .forEach((className) => element.classList.add(className))
+ }
+ element.innerHTML = innerHtml
+ return element
+}
+
+export const loadTemplate = (parent, templateHtml, styles) => {
+ const template = document.createElement('template')
+ template.innerHTML = `${styles != null ? `` : ''}${templateHtml}`
+
+ parent.appendChild(template.content.cloneNode(true))
+}
+
+export const removeChildNodes = (node) => {
+ Array.from(node.childNodes).forEach((el) => {
+ node.removeChild(el)
+ })
+}
+
+export const mapDataToElements = (parent, data, itemMapper) => {
+ const parentTemplate = createElement(parent.tagName)
+
+ data.forEach(item => itemMapper(parentTemplate, item))
+
+ parent.innerHTML = parentTemplate.innerHTML
+}
+
+export const appendNoDataRow = (parent, text, colspan) => {
+ Array.from(parent.childNodes).forEach((el) => {
+ parent.removeChild(el)
+ })
+
+ const row = appendElement('tr', parent)
+ const td = appendElement('td', row)
+ td.innerHTML = text
+ td.setAttribute('colspan', colspan)
+}
+
+export const whenUpgraded = async (node) => {
+ await customElements.whenDefined(node.localName)
+ customElements.upgrade(node)
+}
diff --git a/rt_dashboard/javascript/src/utils/utils.js b/rt_dashboard/javascript/src/utils/utils.js
new file mode 100644
index 00000000..f57e3253
--- /dev/null
+++ b/rt_dashboard/javascript/src/utils/utils.js
@@ -0,0 +1,21 @@
+import { formatDistanceStrict, parseISO } from 'date-fns'
+
+export const duration = (startDate, endDate) => {
+ let start = startDate
+ let end = endDate
+ if (typeof startDate === 'string') {
+ start = parseISO(startDate)
+ }
+ if (typeof endDate === 'string') {
+ end = parseISO(endDate)
+ }
+ return `${formatDistanceStrict(start, end)}`
+}
+
+export const relative = (startDate) => {
+ let start = startDate
+ if (typeof startDate === 'string') {
+ start = parseISO(startDate)
+ }
+ return `${formatDistanceStrict(start, new Date(), { addSuffix: true })}`
+}
diff --git a/rt_dashboard/javascript/src/utils/utils.test.js b/rt_dashboard/javascript/src/utils/utils.test.js
new file mode 100644
index 00000000..d61ed2a0
--- /dev/null
+++ b/rt_dashboard/javascript/src/utils/utils.test.js
@@ -0,0 +1,48 @@
+/* globals describe, test, expect */
+import { duration, relative } from './utils'
+
+describe('duration', () => {
+ test('handles miliseconds args ', () => {
+ const interval = 5 * 1000
+ const start = new Date().getTime() - interval
+ const end = new Date().getTime()
+
+ expect(duration(start, end)).toBe('5 seconds')
+ })
+
+ test('handles ISO string arg', () => {
+ const interval = 5 * 1000
+ const start = new Date(new Date().getTime() - interval).toISOString()
+ const end = new Date().toISOString()
+
+ expect(duration(start, end)).toBe('5 seconds')
+ })
+
+ test('handles Date args ', () => {
+ const start = new Date()
+ const end = new Date()
+
+ expect(duration(start, end)).toBe('0 seconds')
+ })
+})
+
+describe('relative', () => {
+ test('handles miliseconds arg', () => {
+ const interval = 5 * 1000
+ const start = new Date().getTime() - interval
+
+ expect(relative(start)).toBe('5 seconds ago')
+ })
+
+ test('handles Date arg', () => {
+ const start = new Date()
+
+ expect(relative(start)).toBe('0 seconds ago')
+ })
+
+ test('handles ISO string arg', () => {
+ const start = new Date().toISOString()
+
+ expect(relative(start)).toBe('0 seconds ago')
+ })
+})
diff --git a/rt_dashboard/javascript/styles/main.scss b/rt_dashboard/javascript/styles/main.scss
new file mode 100644
index 00000000..f0ce3c1e
--- /dev/null
+++ b/rt_dashboard/javascript/styles/main.scss
@@ -0,0 +1,135 @@
+/* @override http://localhost:9181/static/css/main.css */
+
+@import '~bootstrap';
+
+$fa-font-path: "~@fortawesome/fontawesome-free/webfonts";
+@import "~@fortawesome/fontawesome-free/scss/fontawesome";
+@import "~@fortawesome/fontawesome-free/scss/solid";
+@import '~@fortawesome/fontawesome-free/scss/regular';
+
+.container {
+ background: white;
+ margin-top: 0;
+ width: 100%;
+ max-width: 100%;
+}
+
+#content {
+ background: yellow;
+}
+
+.section
+{
+ padding: 2em;
+}
+
+p.intro {
+ min-height: 2em;
+ padding-top: .8em;
+ color: #888;
+}
+
+p.fixed.intro {
+ height: 5em;
+}
+
+h1 {
+ font-family: 'Helvetica Neue', helvetica, sans-serif;
+ font-weight: 300;
+ color: steelblue;
+ text-shadow: 0 -1px #229;
+}
+
+h1 strong {
+ font-weight: 500;
+}
+
+h1 strong.failed {
+ color: red;
+ text-shadow: 0 -1px #922;
+}
+
+table, table tr, table tr td {
+ background-color: snow;
+}
+
+table tbody tr td.narrow {
+ width: 100px;
+}
+
+table#queues {
+ /*width: 300px;*/
+}
+
+table#jobs td.actions a {
+ font-size: 80%;
+}
+
+table#jobs td.actions {
+ width: auto;
+}
+
+table#jobs span.description {
+ color: #555;
+ font-weight: 600;
+}
+
+table#jobs span.origin {
+ color: #888;
+}
+
+table#jobs div.job_id {
+ font-size: 70%;
+ color: gray;
+ margin-left: 24px;
+}
+
+table#jobs pre.exc_info {
+ color: #555;
+ margin-left: 24px;
+ border-left: 2px solid darkorange;
+ background-color: lightgoldenrodyellow;
+}
+
+table#jobs span.creation_date {
+ font-size: 80%;
+}
+
+table#jobs span.end_date {
+ font-size: 80%;
+ margin-left: 24px;
+}
+
+table#workers {
+ /*width: 600px;*/
+}
+
+table tbody tr td {
+ vertical-align: middle;
+}
+
+table tr.failed {
+ color: red;
+ border-top: 2px solid red;
+}
+
+table tr.failed a {
+ color: red;
+}
+
+table tr.failed td {
+ background-color: lightgoldenrodyellow;
+}
+
+span.loading {
+ font-size: .8em;
+ color: #555;
+}
+
+.tooltip {
+ background: #333;
+ border: #111 1px solid;
+ color: #eee;
+ border-radius: 0.25em;
+ padding: 0.25em 0.5em;
+}
diff --git a/rt_dashboard/static/css/main.css b/rt_dashboard/static/css/main.css
deleted file mode 100644
index 91929207..00000000
--- a/rt_dashboard/static/css/main.css
+++ /dev/null
@@ -1,119 +0,0 @@
-/* @override http://localhost:9181/static/css/main.css */
-
-body {
- background-color: black;
-}
-
-.container {
- background: white;
- margin-top: 3em;
-}
-
-#content {
- background: yellow;
-}
-
-.section
-{
- padding: 2em;
-}
-
-p.intro {
- min-height: 2em;
- padding-top: .8em;
- color: #888;
-}
-
-p.fixed.intro {
- height: 5em;
-}
-
-h1 {
- font-family: 'Helvetica Neue', helvetica, sans-serif;
- font-weight: 300;
- color: steelblue;
- text-shadow: 0 -1px #229;
-}
-
-h1 strong {
- font-weight: 500;
-}
-
-h1 strong.failed {
- color: red;
- text-shadow: 0 -1px #922;
-}
-
-table, table tr, table tr td {
- background-color: snow;
-}
-
-table tbody tr td.narrow {
- width: 100px;
-}
-
-table#queues {
- /*width: 300px;*/
-}
-
-table#jobs td.actions a {
- margin-right: 8px;
- margin-bottom: 8px;
-}
-
-table#jobs span.description {
- color: #555;
- font-weight: 600;
-}
-
-table#jobs span.origin {
- color: #888;
-}
-
-table#jobs div.job_id {
- font-size: 70%;
- color: gray;
- margin-left: 24px;
-}
-
-table#jobs pre.exc_info {
- color: #555;
- margin-left: 24px;
- border-left: 2px solid darkorange;
- background-color: lightgoldenrodyellow;
-}
-
-table#jobs span.creation_date {
- font-size: 80%;
-}
-
-table#jobs span.end_date {
- font-size: 80%;
- margin-left: 24px;
-}
-
-table#workers {
- /*width: 600px;*/
-}
-
-table tbody tr td {
- vertical-align: middle;
-}
-
-table tr.failed {
- color: red;
- border-top: 2px solid red;
-}
-
-table tr.failed a {
- color: red;
-}
-
-table tr.failed td {
- background-color: lightgoldenrodyellow;
-}
-
-span.loading {
- font-size: .8em;
- color: #555;
-}
diff --git a/rt_dashboard/static/history.html b/rt_dashboard/static/history.html
new file mode 100644
index 00000000..870c85f3
--- /dev/null
+++ b/rt_dashboard/static/history.html
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+ RQ dashboard
+
+
+
+
+
+
+
+
+
+
diff --git a/rt_dashboard/static/index.html b/rt_dashboard/static/index.html
new file mode 100644
index 00000000..7d02fa59
--- /dev/null
+++ b/rt_dashboard/static/index.html
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+ RQ dashboard
+
+
+
+
+
+
+
+
+
+
diff --git a/rt_dashboard/static/js/api.js b/rt_dashboard/static/js/api.js
deleted file mode 100644
index 5a0aa045..00000000
--- a/rt_dashboard/static/js/api.js
+++ /dev/null
@@ -1,30 +0,0 @@
-define(['jquery'], function($) {
-
- var getQueues = function(cb) {
- $.getJSON('/queues', function(data) { // TODO: Fix static URL
- var queues = data.queues;
- cb(queues);
- });
- };
-
- var getJobs = function(queue_name, cb) {
- $.getJSON('/jobs/' + encodeURIComponent(queue_name), function(data) { // TODO: Fix static URL
- var jobs = data.jobs;
- cb(jobs);
- });
- };
-
- var getWorkers = function(cb) {
- $.getJSON('/workers', function(data) { // TODO: Fix static URL
- var workers = data.workers;
- cb(workers);
- });
- };
-
- return {
- 'getQueues': getQueues,
- 'getJobs': getJobs,
- 'getWorkers': getWorkers
- };
-
-});
diff --git a/rt_dashboard/static/js/bootstrap-tooltip.js b/rt_dashboard/static/js/bootstrap-tooltip.js
deleted file mode 100644
index a3bbd580..00000000
--- a/rt_dashboard/static/js/bootstrap-tooltip.js
+++ /dev/null
@@ -1,361 +0,0 @@
-/* ===========================================================
- * bootstrap-tooltip.js v2.3.2
- * http://twitter.github.com/bootstrap/javascript.html#tooltips
- * Inspired by the original jQuery.tipsy by Jason Frame
- * ===========================================================
- * Copyright 2012 Twitter, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- * ========================================================== */
-
-
-!function ($) {
-
- "use strict"; // jshint ;_;
-
-
- /* TOOLTIP PUBLIC CLASS DEFINITION
- * =============================== */
-
- var Tooltip = function (element, options) {
- this.init('tooltip', element, options)
- }
-
- Tooltip.prototype = {
-
- constructor: Tooltip
-
- , init: function (type, element, options) {
- var eventIn
- , eventOut
- , triggers
- , trigger
- , i
-
- this.type = type
- this.$element = $(element)
- this.options = this.getOptions(options)
- this.enabled = true
-
- triggers = this.options.trigger.split(' ')
-
- for (i = triggers.length; i--;) {
- trigger = triggers[i]
- if (trigger == 'click') {
- this.$element.on('click.' + this.type, this.options.selector, $.proxy(this.toggle, this))
- } else if (trigger != 'manual') {
- eventIn = trigger == 'hover' ? 'mouseenter' : 'focus'
- eventOut = trigger == 'hover' ? 'mouseleave' : 'blur'
- this.$element.on(eventIn + '.' + this.type, this.options.selector, $.proxy(this.enter, this))
- this.$element.on(eventOut + '.' + this.type, this.options.selector, $.proxy(this.leave, this))
- }
- }
-
- this.options.selector ?
- (this._options = $.extend({}, this.options, { trigger: 'manual', selector: '' })) :
- this.fixTitle()
- }
-
- , getOptions: function (options) {
- options = $.extend({}, $.fn[this.type].defaults, this.$element.data(), options)
-
- if (options.delay && typeof options.delay == 'number') {
- options.delay = {
- show: options.delay
- , hide: options.delay
- }
- }
-
- return options
- }
-
- , enter: function (e) {
- var defaults = $.fn[this.type].defaults
- , options = {}
- , self
-
- this._options && $.each(this._options, function (key, value) {
- if (defaults[key] != value) options[key] = value
- }, this)
-
- self = $(e.currentTarget)[this.type](options).data(this.type)
-
- if (!self.options.delay || !self.options.delay.show) return self.show()
-
- clearTimeout(this.timeout)
- self.hoverState = 'in'
- this.timeout = setTimeout(function() {
- if (self.hoverState == 'in') self.show()
- }, self.options.delay.show)
- }
-
- , leave: function (e) {
- var self = $(e.currentTarget)[this.type](this._options).data(this.type)
-
- if (this.timeout) clearTimeout(this.timeout)
- if (!self.options.delay || !self.options.delay.hide) return self.hide()
-
- self.hoverState = 'out'
- this.timeout = setTimeout(function() {
- if (self.hoverState == 'out') self.hide()
- }, self.options.delay.hide)
- }
-
- , show: function () {
- var $tip
- , pos
- , actualWidth
- , actualHeight
- , placement
- , tp
- , e = $.Event('show')
-
- if (this.hasContent() && this.enabled) {
- this.$element.trigger(e)
- if (e.isDefaultPrevented()) return
- $tip = this.tip()
- this.setContent()
-
- if (this.options.animation) {
- $tip.addClass('fade')
- }
-
- placement = typeof this.options.placement == 'function' ?
- this.options.placement.call(this, $tip[0], this.$element[0]) :
- this.options.placement
-
- $tip
- .detach()
- .css({ top: 0, left: 0, display: 'block' })
-
- this.options.container ? $tip.appendTo(this.options.container) : $tip.insertAfter(this.$element)
-
- pos = this.getPosition()
-
- actualWidth = $tip[0].offsetWidth
- actualHeight = $tip[0].offsetHeight
-
- switch (placement) {
- case 'bottom':
- tp = {top: pos.top + pos.height, left: pos.left + pos.width / 2 - actualWidth / 2}
- break
- case 'top':
- tp = {top: pos.top - actualHeight, left: pos.left + pos.width / 2 - actualWidth / 2}
- break
- case 'left':
- tp = {top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left - actualWidth}
- break
- case 'right':
- tp = {top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left + pos.width}
- break
- }
-
- this.applyPlacement(tp, placement)
- this.$element.trigger('shown')
- }
- }
-
- , applyPlacement: function(offset, placement){
- var $tip = this.tip()
- , width = $tip[0].offsetWidth
- , height = $tip[0].offsetHeight
- , actualWidth
- , actualHeight
- , delta
- , replace
-
- $tip
- .offset(offset)
- .addClass(placement)
- .addClass('in')
-
- actualWidth = $tip[0].offsetWidth
- actualHeight = $tip[0].offsetHeight
-
- if (placement == 'top' && actualHeight != height) {
- offset.top = offset.top + height - actualHeight
- replace = true
- }
-
- if (placement == 'bottom' || placement == 'top') {
- delta = 0
-
- if (offset.left < 0){
- delta = offset.left * -2
- offset.left = 0
- $tip.offset(offset)
- actualWidth = $tip[0].offsetWidth
- actualHeight = $tip[0].offsetHeight
- }
-
- this.replaceArrow(delta - width + actualWidth, actualWidth, 'left')
- } else {
- this.replaceArrow(actualHeight - height, actualHeight, 'top')
- }
-
- if (replace) $tip.offset(offset)
- }
-
- , replaceArrow: function(delta, dimension, position){
- this
- .arrow()
- .css(position, delta ? (50 * (1 - delta / dimension) + "%") : '')
- }
-
- , setContent: function () {
- var $tip = this.tip()
- , title = this.getTitle()
-
- $tip.find('.tooltip-inner')[this.options.html ? 'html' : 'text'](title)
- $tip.removeClass('fade in top bottom left right')
- }
-
- , hide: function () {
- var that = this
- , $tip = this.tip()
- , e = $.Event('hide')
-
- this.$element.trigger(e)
- if (e.isDefaultPrevented()) return
-
- $tip.removeClass('in')
-
- function removeWithAnimation() {
- var timeout = setTimeout(function () {
- $tip.off($.support.transition.end).detach()
- }, 500)
-
- $tip.one($.support.transition.end, function () {
- clearTimeout(timeout)
- $tip.detach()
- })
- }
-
- $.support.transition && this.$tip.hasClass('fade') ?
- removeWithAnimation() :
- $tip.detach()
-
- this.$element.trigger('hidden')
-
- return this
- }
-
- , fixTitle: function () {
- var $e = this.$element
- if ($e.attr('title') || typeof($e.attr('data-original-title')) != 'string') {
- $e.attr('data-original-title', $e.attr('title') || '').attr('title', '')
- }
- }
-
- , hasContent: function () {
- return this.getTitle()
- }
-
- , getPosition: function () {
- var el = this.$element[0]
- return $.extend({}, (typeof el.getBoundingClientRect == 'function') ? el.getBoundingClientRect() : {
- width: el.offsetWidth
- , height: el.offsetHeight
- }, this.$element.offset())
- }
-
- , getTitle: function () {
- var title
- , $e = this.$element
- , o = this.options
-
- title = $e.attr('data-original-title')
- || (typeof o.title == 'function' ? o.title.call($e[0]) : o.title)
-
- return title
- }
-
- , tip: function () {
- return this.$tip = this.$tip || $(this.options.template)
- }
-
- , arrow: function(){
- return this.$arrow = this.$arrow || this.tip().find(".tooltip-arrow")
- }
-
- , validate: function () {
- if (!this.$element[0].parentNode) {
- this.hide()
- this.$element = null
- this.options = null
- }
- }
-
- , enable: function () {
- this.enabled = true
- }
-
- , disable: function () {
- this.enabled = false
- }
-
- , toggleEnabled: function () {
- this.enabled = !this.enabled
- }
-
- , toggle: function (e) {
- var self = e ? $(e.currentTarget)[this.type](this._options).data(this.type) : this
- self.tip().hasClass('in') ? self.hide() : self.show()
- }
-
- , destroy: function () {
- this.hide().$element.off('.' + this.type).removeData(this.type)
- }
-
- }
-
-
- /* TOOLTIP PLUGIN DEFINITION
- * ========================= */
-
- var old = $.fn.tooltip
-
- $.fn.tooltip = function ( option ) {
- return this.each(function () {
- var $this = $(this)
- , data = $this.data('tooltip')
- , options = typeof option == 'object' && option
- if (!data) $this.data('tooltip', (data = new Tooltip(this, options)))
- if (typeof option == 'string') data[option]()
- })
- }
-
- $.fn.tooltip.Constructor = Tooltip
-
- $.fn.tooltip.defaults = {
- animation: true
- , placement: 'top'
- , selector: false
- , template: ''
- , trigger: 'hover focus'
- , title: ''
- , delay: 0
- , html: false
- , container: false
- }
-
-
- /* TOOLTIP NO CONFLICT
- * =================== */
-
- $.fn.tooltip.noConflict = function () {
- $.fn.tooltip = old
- return this
- }
-
-}(window.jQuery);
diff --git a/rt_dashboard/templates/rt_dashboard/app.html b/rt_dashboard/templates/rt_dashboard/app.html
new file mode 100644
index 00000000..fbf125cd
--- /dev/null
+++ b/rt_dashboard/templates/rt_dashboard/app.html
@@ -0,0 +1,20 @@
+
+
+
+
+
+ RT dashboard
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/rt_dashboard/templates/rt_dashboard/base.html b/rt_dashboard/templates/rt_dashboard/base.html
index 74eb1206..4b711c70 100644
--- a/rt_dashboard/templates/rt_dashboard/base.html
+++ b/rt_dashboard/templates/rt_dashboard/base.html
@@ -7,7 +7,7 @@
-
+
diff --git a/rt_dashboard/templates/rt_dashboard/dashboard.html b/rt_dashboard/templates/rt_dashboard/dashboard.html
index 43e86930..64f6bcbf 100644
--- a/rt_dashboard/templates/rt_dashboard/dashboard.html
+++ b/rt_dashboard/templates/rt_dashboard/dashboard.html
@@ -2,216 +2,12 @@
{% block content %}
-
-
-
-
-
-
Queues
-
This list below contains all the registered queues with the number of tasks currently in the queue. Select a queue from above to view all tasks currently pending on the queue.
-
-
-
-
- Queue
- Tasks
-
-
-
-
- Loading...
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Workers
-
-
Toggle workers list
-
No workers registered!
-
-
-
-
- State
- Worker
- Queues
-
-
-
-
- Loading...
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Tasks on {{ queue.name }}
-
- {% if queue.count() %}
-
- Empty
-
- {% elif not queue.name.startswith('[') %}
-
- Delete
-
- {% endif %}
- This list below contains all the registered tasks on queue
- {{ queue.name }} , sorted by age
- {% if queue.name.startswith('[') %}
- (newest on top).
- {% else %}
- (oldest on top).
- {% endif %}
-
-
-
-
-
- Name
- Age
- Actions
-
-
-
-
- Loading...
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
{% endblock %}
{% block inline_js %}
-var POLL_INTERVAL = {{ poll_interval }};
-document.addEventListener("DOMContentLoaded", function(){
- {% include "rt_dashboard/dashboard.js" with context %}
-});
+
+
+