mirror of https://github.com/apache/superset.git
Reactify dashboard grid (#523)
* Use react-grid-layout instead of gridster * visualizations show and resize * display slice name and description; links work * positioning of widgets to match gridster, rowHeight matches * Change margins, rowHeight, unpositioned viz, and expandedSlices to match gridster * Saving dashboard, deleting slices, formatting on slices (chart control and resize handle), expanded slices fixed. * responsiveness + use es6 classes * Minor ui fixes + linting * CSS transforms on slices messes up nvd3 tooltip positioning. Turn off CSS transforms for the time being, with a cost of painting speed. Issue is currently being looked at on the nvd3 repo PR: https://github.com/novus/nvd3/pull/1674 * Remove breakpoint listener, fires when it shouldn't (i.e. too often) * resize is no longer buggy, minor cleanup * gridster class, const, landscape error * one source of data for data to front end from python
This commit is contained in:
parent
fe6628b0a4
commit
c78d3682ac
|
@ -4,18 +4,193 @@ var px = require('./modules/caravel.js');
|
||||||
var d3 = require('d3');
|
var d3 = require('d3');
|
||||||
var showModal = require('./modules/utils.js').showModal;
|
var showModal = require('./modules/utils.js').showModal;
|
||||||
require('bootstrap');
|
require('bootstrap');
|
||||||
|
import React from 'react';
|
||||||
|
import { render } from 'react-dom';
|
||||||
|
|
||||||
var ace = require('brace');
|
var ace = require('brace');
|
||||||
require('brace/mode/css');
|
require('brace/mode/css');
|
||||||
require('brace/theme/crimson_editor');
|
require('brace/theme/crimson_editor');
|
||||||
|
|
||||||
require('./caravel-select2.js');
|
require('./caravel-select2.js');
|
||||||
require('../node_modules/gridster/dist/jquery.gridster.min.css');
|
require('../node_modules/react-grid-layout/css/styles.css');
|
||||||
require('../node_modules/gridster/dist/jquery.gridster.min.js');
|
require('../node_modules/react-resizable/css/styles.css');
|
||||||
|
|
||||||
require('../stylesheets/dashboard.css');
|
require('../stylesheets/dashboard.css');
|
||||||
|
|
||||||
|
import { Responsive, WidthProvider } from "react-grid-layout";
|
||||||
|
const ResponsiveReactGridLayout = WidthProvider(Responsive);
|
||||||
|
|
||||||
|
class SliceCell extends React.Component {
|
||||||
|
render() {
|
||||||
|
const slice = this.props.slice,
|
||||||
|
createMarkup = function () {
|
||||||
|
return { __html: slice.description_markeddown };
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="chart-header">
|
||||||
|
<div className="row">
|
||||||
|
<div className="col-md-12 text-center header">
|
||||||
|
{slice.slice_name}
|
||||||
|
</div>
|
||||||
|
<div className="col-md-12 chart-controls">
|
||||||
|
<div className="pull-left">
|
||||||
|
<a title="Move chart" data-toggle="tooltip">
|
||||||
|
<i className="fa fa-arrows drag"/>
|
||||||
|
</a>
|
||||||
|
<a className="refresh" title="Force refresh data" data-toggle="tooltip">
|
||||||
|
<i className="fa fa-repeat"/>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div className="pull-right">
|
||||||
|
{slice.description ?
|
||||||
|
<a title="Toggle chart description">
|
||||||
|
<i className="fa fa-info-circle slice_info" title={slice.description} data-toggle="tooltip"/>
|
||||||
|
</a>
|
||||||
|
: ""}
|
||||||
|
<a href={slice.edit_url} title="Edit chart" data-toggle="tooltip">
|
||||||
|
<i className="fa fa-pencil"/>
|
||||||
|
</a>
|
||||||
|
<a href={slice.slice_url} title="Explore chart" data-toggle="tooltip">
|
||||||
|
<i className="fa fa-share"/>
|
||||||
|
</a>
|
||||||
|
<a className="remove-chart" title="Remove chart from dashboard" data-toggle="tooltip">
|
||||||
|
<i className="fa fa-close" onClick={this.props.removeSlice.bind(null, slice.slice_id)}/>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="slice_description bs-callout bs-callout-default"
|
||||||
|
style={this.props.expandedSlices && this.props.expandedSlices[String(slice.slice_id)] ? {} : { display: "none" }}
|
||||||
|
dangerouslySetInnerHTML={createMarkup()}>
|
||||||
|
</div>
|
||||||
|
<div className="row chart-container">
|
||||||
|
<input type="hidden" value="false"/>
|
||||||
|
<div id={slice.token} className="token col-md-12">
|
||||||
|
<img src={"/static/assets/images/loading.gif"} className="loading" alt="loading"/>
|
||||||
|
<div className="slice_container" id={slice.token + "_con"}></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class GridLayout extends React.Component {
|
||||||
|
removeSlice(sliceId) {
|
||||||
|
$('[data-toggle="tooltip"]').tooltip("hide");
|
||||||
|
this.setState({
|
||||||
|
layout: this.state.layout.filter(function (reactPos) {
|
||||||
|
return reactPos.i !== String(sliceId);
|
||||||
|
}),
|
||||||
|
slices: this.state.slices.filter(function (slice) {
|
||||||
|
return slice.slice_id !== sliceId;
|
||||||
|
}),
|
||||||
|
sliceElements: this.state.sliceElements.filter(function (sliceElement) {
|
||||||
|
return sliceElement.key !== String(sliceId);
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onResizeStop(layout, oldItem, newItem) {
|
||||||
|
if (oldItem.w != newItem.w || oldItem.h != newItem.h) {
|
||||||
|
this.setState({
|
||||||
|
layout: layout
|
||||||
|
}, function () {
|
||||||
|
this.props.dashboard.getSlice(newItem.i).resize();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onDragStop(layout) {
|
||||||
|
this.setState({
|
||||||
|
layout: layout
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
serialize() {
|
||||||
|
return this.state.layout.map(function (reactPos) {
|
||||||
|
return {
|
||||||
|
slice_id: reactPos.i,
|
||||||
|
col: reactPos.x + 1,
|
||||||
|
row: reactPos.y,
|
||||||
|
size_x: reactPos.w,
|
||||||
|
size_y: reactPos.h
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillMount() {
|
||||||
|
var layout = [],
|
||||||
|
sliceElements = [];
|
||||||
|
|
||||||
|
this.props.slices.forEach(function (slice, index) {
|
||||||
|
var pos = this.props.posDict[slice.slice_id];
|
||||||
|
if (!pos) {
|
||||||
|
pos = {
|
||||||
|
col: (index * 4 + 1) % 12,
|
||||||
|
row: Math.floor((index) / 3) * 4,
|
||||||
|
size_x: 4,
|
||||||
|
size_y: 4
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
sliceElements.push(
|
||||||
|
<div
|
||||||
|
id={"slice_" + slice.slice_id}
|
||||||
|
key={slice.slice_id}
|
||||||
|
data-slice-id={slice.slice_id}
|
||||||
|
className={"widget " + slice.viz_name}>
|
||||||
|
<SliceCell
|
||||||
|
slice={slice}
|
||||||
|
removeSlice={this.removeSlice.bind(this)}
|
||||||
|
expandedSlices={this.props.dashboard.metadata.expanded_slices}/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
layout.push({
|
||||||
|
i: String(slice.slice_id),
|
||||||
|
x: pos.col - 1,
|
||||||
|
y: pos.row,
|
||||||
|
w: pos.size_x,
|
||||||
|
minW: 2,
|
||||||
|
h: pos.size_y
|
||||||
|
});
|
||||||
|
}, this);
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
layout: layout,
|
||||||
|
sliceElements: sliceElements,
|
||||||
|
slices: this.props.slices
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<ResponsiveReactGridLayout
|
||||||
|
className="layout"
|
||||||
|
layouts={{ lg: this.state.layout }}
|
||||||
|
onResizeStop={this.onResizeStop.bind(this)}
|
||||||
|
onDragStop={this.onDragStop.bind(this)}
|
||||||
|
cols={{ lg: 12, md: 12, sm: 10, xs: 8, xxs: 6 }}
|
||||||
|
rowHeight={100}
|
||||||
|
autoSize={true}
|
||||||
|
margin={[20, 20]}
|
||||||
|
useCSSTransforms={false}
|
||||||
|
draggableHandle=".drag">
|
||||||
|
{this.state.sliceElements}
|
||||||
|
</ResponsiveReactGridLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var Dashboard = function (dashboardData) {
|
var Dashboard = function (dashboardData) {
|
||||||
|
var reactGridLayout;
|
||||||
|
|
||||||
var dashboard = $.extend(dashboardData, {
|
var dashboard = $.extend(dashboardData, {
|
||||||
filters: {},
|
filters: {},
|
||||||
init: function () {
|
init: function () {
|
||||||
|
@ -128,30 +303,17 @@ var Dashboard = function (dashboardData) {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
initDashboardView: function () {
|
initDashboardView: function () {
|
||||||
|
var posDict = {}
|
||||||
|
this.position_json.forEach(function (position) {
|
||||||
|
posDict[position.slice_id] = position;
|
||||||
|
});
|
||||||
|
|
||||||
|
reactGridLayout = render(
|
||||||
|
<GridLayout slices={this.slices} posDict={posDict} dashboard={dashboard}/>,
|
||||||
|
document.getElementById("grid-container")
|
||||||
|
);
|
||||||
|
|
||||||
dashboard = this;
|
dashboard = this;
|
||||||
var gridster = $(".gridster ul").gridster({
|
|
||||||
autogrow_cols: true,
|
|
||||||
widget_margins: [10, 10],
|
|
||||||
widget_base_dimensions: [95, 95],
|
|
||||||
draggable: {
|
|
||||||
handle: '.drag'
|
|
||||||
},
|
|
||||||
resize: {
|
|
||||||
enabled: true,
|
|
||||||
stop: function (e, ui, element) {
|
|
||||||
dashboard.getSlice($(element).attr('slice_id')).resize();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
serialize_params: function (_w, wgd) {
|
|
||||||
return {
|
|
||||||
slice_id: $(_w).attr('slice_id'),
|
|
||||||
col: wgd.col,
|
|
||||||
row: wgd.row,
|
|
||||||
size_x: wgd.size_x,
|
|
||||||
size_y: wgd.size_y
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}).data('gridster');
|
|
||||||
|
|
||||||
// Displaying widget controls on hover
|
// Displaying widget controls on hover
|
||||||
$('.chart-header').hover(
|
$('.chart-header').hover(
|
||||||
|
@ -162,18 +324,18 @@ var Dashboard = function (dashboardData) {
|
||||||
$(this).find('.chart-controls').fadeOut(300);
|
$(this).find('.chart-controls').fadeOut(300);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
$("div.gridster").css('visibility', 'visible');
|
$("div.grid-container").css('visibility', 'visible');
|
||||||
$("#savedash").click(function () {
|
$("#savedash").click(function () {
|
||||||
var expanded_slices = {};
|
var expanded_slices = {};
|
||||||
$.each($(".slice_info"), function (i, d) {
|
$.each($(".slice_info"), function (i, d) {
|
||||||
var widget = $(this).parents('.widget');
|
var widget = $(this).parents('.widget');
|
||||||
var slice_description = widget.find('.slice_description');
|
var slice_description = widget.find('.slice_description');
|
||||||
if (slice_description.is(":visible")) {
|
if (slice_description.is(":visible")) {
|
||||||
expanded_slices[$(d).attr('slice_id')] = true;
|
expanded_slices[$(widget).attr('data-slice-id')] = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
var data = {
|
var data = {
|
||||||
positions: gridster.serialize(),
|
positions: reactGridLayout.serialize(),
|
||||||
css: editor.getValue(),
|
css: editor.getValue(),
|
||||||
expanded_slices: expanded_slices
|
expanded_slices: expanded_slices
|
||||||
};
|
};
|
||||||
|
@ -236,12 +398,8 @@ var Dashboard = function (dashboardData) {
|
||||||
slice.render(true);
|
slice.render(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
$("a.remove-chart").click(function () {
|
|
||||||
var li = $(this).parents("li");
|
|
||||||
gridster.remove_widget(li);
|
|
||||||
});
|
|
||||||
|
|
||||||
$("li.widget").click(function (e) {
|
$("div.widget").click(function (e) {
|
||||||
var $this = $(this);
|
var $this = $(this);
|
||||||
var $target = $(e.target);
|
var $target = $(e.target);
|
||||||
|
|
|
@ -65,6 +65,8 @@
|
||||||
"react": "^0.14.7",
|
"react": "^0.14.7",
|
||||||
"react-bootstrap": "^0.28.3",
|
"react-bootstrap": "^0.28.3",
|
||||||
"react-dom": "^0.14.7",
|
"react-dom": "^0.14.7",
|
||||||
|
"react-grid-layout": "^0.12.3",
|
||||||
|
"react-resizable": "^1.3.3",
|
||||||
"select2": "3.5",
|
"select2": "3.5",
|
||||||
"select2-bootstrap-css": "^1.4.6",
|
"select2-bootstrap-css": "^1.4.6",
|
||||||
"style-loader": "^0.13.0",
|
"style-loader": "^0.13.0",
|
||||||
|
|
|
@ -170,12 +170,12 @@ li.widget:hover {
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
}
|
}
|
||||||
|
|
||||||
li.widget .chart-header {
|
div.widget .chart-header {
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
background-color: #f1f1f1;
|
background-color: #f1f1f1;
|
||||||
}
|
}
|
||||||
|
|
||||||
li.widget .chart-header a {
|
div.widget .chart-header a {
|
||||||
margin-left: 5px;
|
margin-left: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -183,16 +183,18 @@ li.widget .chart-header a {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
li.widget .chart-controls {
|
div.widget .chart-controls {
|
||||||
|
background-clip: content-box;
|
||||||
background-color: #f1f1f1;
|
background-color: #f1f1f1;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 0;
|
right: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
padding: 0px 5px;
|
top: 5px;
|
||||||
|
padding: 5px 5px;
|
||||||
opacity: 0.75;
|
opacity: 0.75;
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
li.widget .slice_container {
|
div.widget .slice_container {
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,23 +4,22 @@
|
||||||
.dashboard i.drag {
|
.dashboard i.drag {
|
||||||
cursor: move !important;
|
cursor: move !important;
|
||||||
}
|
}
|
||||||
.dashboard .gridster .preview-holder {
|
.dashboard .slice-grid .preview-holder {
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
background-color: #AAA;
|
background-color: #AAA;
|
||||||
border-color: #AAA;
|
border-color: #AAA;
|
||||||
opacity: 0.3;
|
opacity: 0.3;
|
||||||
}
|
}
|
||||||
.gridster li.widget{
|
|
||||||
list-style-type: none;
|
.slice-grid div.widget{
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
margin: 5px;
|
|
||||||
border: 1px solid #ccc;
|
border: 1px solid #ccc;
|
||||||
box-shadow: 2px 1px 5px -2px #aaa;
|
box-shadow: 2px 1px 5px -2px #aaa;
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
}
|
}
|
||||||
.dashboard .gridster .dragging,
|
.dashboard .slice-grid .dragging,
|
||||||
.dashboard .gridster .resizing {
|
.dashboard .slice-grid .resizing {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
.dashboard img.loading {
|
.dashboard img.loading {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
.gridster .widget.pivot_table .slice_container {
|
.slice-grid .widget.pivot_table .slice_container {
|
||||||
overflow: auto !important;
|
overflow: auto !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
.gridster .widget.table .slice_container {
|
.slice-grid .widget.table .slice_container {
|
||||||
overflow: auto !important;
|
overflow: auto !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@ var config = {
|
||||||
// for now generate one compiled js file per entry point / html page
|
// for now generate one compiled js file per entry point / html page
|
||||||
entry: {
|
entry: {
|
||||||
'css-theme': APP_DIR + '/javascripts/css-theme.js',
|
'css-theme': APP_DIR + '/javascripts/css-theme.js',
|
||||||
dashboard: APP_DIR + '/javascripts/dashboard.js',
|
dashboard: APP_DIR + '/javascripts/dashboard.jsx',
|
||||||
explore: APP_DIR + '/javascripts/explore.js',
|
explore: APP_DIR + '/javascripts/explore.js',
|
||||||
welcome: APP_DIR + '/javascripts/welcome.js',
|
welcome: APP_DIR + '/javascripts/welcome.js',
|
||||||
sql: APP_DIR + '/javascripts/sql.js',
|
sql: APP_DIR + '/javascripts/sql.js',
|
||||||
|
|
|
@ -197,6 +197,7 @@ class Slice(Model, AuditMixinNullable):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def data(self):
|
def data(self):
|
||||||
|
"""Data used to render slice in templates"""
|
||||||
d = {}
|
d = {}
|
||||||
self.token = ''
|
self.token = ''
|
||||||
try:
|
try:
|
||||||
|
@ -205,6 +206,11 @@ class Slice(Model, AuditMixinNullable):
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
d['error'] = str(e)
|
d['error'] = str(e)
|
||||||
d['slice_id'] = self.id
|
d['slice_id'] = self.id
|
||||||
|
d['slice_name'] = self.slice_name
|
||||||
|
d['description'] = self.description
|
||||||
|
d['slice_url'] = self.slice_url
|
||||||
|
d['edit_url'] = self.edit_url
|
||||||
|
d['description_markeddown'] = self.description_markeddown
|
||||||
return d
|
return d
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -309,6 +315,7 @@ class Dashboard(Model, AuditMixinNullable):
|
||||||
'dashboard_title': self.dashboard_title,
|
'dashboard_title': self.dashboard_title,
|
||||||
'slug': self.slug,
|
'slug': self.slug,
|
||||||
'slices': [slc.data for slc in self.slices],
|
'slices': [slc.data for slc in self.slices],
|
||||||
|
'position_json': json.loads(self.position_json),
|
||||||
}
|
}
|
||||||
return json.dumps(d)
|
return json.dumps(d)
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block title %}[dashboard] {{ dashboard.dashboard_title }}{% endblock %}
|
{% block title %}[dashboard] {{ dashboard.dashboard_title }}{% endblock %}
|
||||||
{% block body %}
|
{% block body %}
|
||||||
|
|
||||||
<div class="dashboard container-fluid" data-dashboard="{{ dashboard.json_data }}" data-css="{{ dashboard.css }}">
|
<div class="dashboard container-fluid" data-dashboard="{{ dashboard.json_data }}" data-css="{{ dashboard.css }}">
|
||||||
|
|
||||||
<!-- Modal -->
|
<!-- Modal -->
|
||||||
|
@ -97,71 +98,9 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="gridster content_fluid" style="visibility: hidden;">
|
<!-- gridster class used for backwards compatibility -->
|
||||||
<ul>
|
<div id="grid-container" class="slice-grid gridster">
|
||||||
{% for slice in dashboard.slices %}
|
|
||||||
{% set pos = pos_dict.get(slice.id, {}) %}
|
|
||||||
|
|
||||||
|
|
||||||
<li
|
|
||||||
id="slice_{{ slice.id }}"
|
|
||||||
slice_id="{{ slice.id }}"
|
|
||||||
class="widget {{ slice.viz_type }}"
|
|
||||||
data-row="{{ pos.row or 1 }}"
|
|
||||||
data-col="{{ pos.col or loop.index }}"
|
|
||||||
data-sizex="{{ pos.size_x or 4 }}"
|
|
||||||
data-sizey="{{ pos.size_y or 4 }}">
|
|
||||||
|
|
||||||
<div class="chart-header">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-12 text-center header">
|
|
||||||
{{ slice.slice_name }}
|
|
||||||
</div>
|
|
||||||
<div class="col-md-12 chart-controls">
|
|
||||||
<div class="pull-left">
|
|
||||||
<a title="Move chart" data-toggle="tooltip">
|
|
||||||
<i class="fa fa-arrows drag"></i>
|
|
||||||
</a>
|
|
||||||
<a class="refresh" title="Force refresh data" data-toggle="tooltip">
|
|
||||||
<i class="fa fa-repeat"></i>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div class="pull-right">
|
|
||||||
{% if slice.description %}
|
|
||||||
<a title="Toggle chart description">
|
|
||||||
<i class="fa fa-info-circle slice_info" slice_id="{{ slice.id }}" title="{{ slice.description }}" data-toggle="tooltip"></i>
|
|
||||||
</a>
|
|
||||||
{% endif %}
|
|
||||||
<a href="{{ slice.edit_url }}" title="Edit chart" data-toggle="tooltip">
|
|
||||||
<i class="fa fa-pencil"></i>
|
|
||||||
</a>
|
|
||||||
<a href="{{ slice.slice_url }}" title="Explore chart" data-toggle="tooltip">
|
|
||||||
<i class="fa fa-share"></i>
|
|
||||||
</a>
|
|
||||||
<a class="remove-chart" title="Remove chart from dashboard" data-toggle="tooltip">
|
|
||||||
<i class="fa fa-close"></i>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="slice_description bs-callout bs-callout-default"
|
|
||||||
style="{{ 'display: none;' if "{}".format(slice.id) not in dashboard.metadata_dejson.expanded_slices }}">
|
|
||||||
{{ slice.description_markeddown | safe }}
|
|
||||||
</div>
|
|
||||||
<div class="row chart-container">
|
|
||||||
<input type="hidden" slice_id="{{ slice.id }}" value="false">
|
|
||||||
<div id="{{ slice.token }}" class="token col-md-12">
|
|
||||||
<img src="{{ url_for("static", filename="assets/images/loading.gif") }}" class="loading" alt="loading">
|
|
||||||
<div class="slice_container" id="{{ slice.token }}_con"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -883,15 +883,9 @@ class Caravel(BaseView):
|
||||||
pass
|
pass
|
||||||
dashboard(dashboard_id=dash.id)
|
dashboard(dashboard_id=dash.id)
|
||||||
|
|
||||||
pos_dict = {}
|
|
||||||
if dash.position_json:
|
|
||||||
pos_dict = {
|
|
||||||
int(o['slice_id']): o
|
|
||||||
for o in json.loads(dash.position_json)}
|
|
||||||
return self.render_template(
|
return self.render_template(
|
||||||
"caravel/dashboard.html", dashboard=dash,
|
"caravel/dashboard.html", dashboard=dash,
|
||||||
templates=templates,
|
templates=templates,
|
||||||
pos_dict=pos_dict,
|
|
||||||
dash_save_perm=appbuilder.sm.has_access('can_save_dash', 'Caravel'),
|
dash_save_perm=appbuilder.sm.has_access('can_save_dash', 'Caravel'),
|
||||||
dash_edit_perm=appbuilder.sm.has_access('can_edit', 'DashboardModelView'))
|
dash_edit_perm=appbuilder.sm.has_access('can_edit', 'DashboardModelView'))
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue