diff --git a/caravel/assets/.babelrc b/caravel/assets/.babelrc index 338435d875a53..6aed7f6b37db4 100644 --- a/caravel/assets/.babelrc +++ b/caravel/assets/.babelrc @@ -1,3 +1,3 @@ { - "presets" : ["airbnb", "es2015", "react"] + "presets" : ["airbnb", "es2015", "react"], } diff --git a/caravel/assets/javascripts/components/Button.jsx b/caravel/assets/javascripts/components/Button.jsx new file mode 100644 index 0000000000000..d431c65f6457d --- /dev/null +++ b/caravel/assets/javascripts/components/Button.jsx @@ -0,0 +1,40 @@ +import React, { PropTypes } from 'react'; +import { Button as BootstrapButton, Tooltip, OverlayTrigger } from 'react-bootstrap'; +import { slugify } from '../modules/utils'; + +const propTypes = { + tooltip: PropTypes.node, + placement: PropTypes.string, +}; +const defaultProps = { + bsSize: 'sm', + placement: 'top', +}; + +export default function Button(props) { + const buttonProps = Object.assign({}, props); + const tooltip = props.tooltip; + const placement = props.placement; + delete buttonProps.tooltip; + delete buttonProps.placement; + + let button = ( + + {props.children} + + ); + if (props.tooltip) { + button = ( + {tooltip}} + > + {button} + + ); + } + return button; +} + +Button.propTypes = propTypes; +Button.defaultProps = defaultProps; diff --git a/caravel/assets/javascripts/components/ModalTrigger.jsx b/caravel/assets/javascripts/components/ModalTrigger.jsx index 246c36b998d8f..b9ab32c3e66ad 100644 --- a/caravel/assets/javascripts/components/ModalTrigger.jsx +++ b/caravel/assets/javascripts/components/ModalTrigger.jsx @@ -1,5 +1,6 @@ import React, { PropTypes } from 'react'; import { Modal } from 'react-bootstrap'; +import Button from './Button'; import cx from 'classnames'; const propTypes = { @@ -11,6 +12,7 @@ const propTypes = { isButton: PropTypes.bool, bsSize: PropTypes.string, className: PropTypes.string, + tooltip: PropTypes.string, }; const defaultProps = { @@ -40,28 +42,41 @@ export default class ModalTrigger extends React.Component { this.props.beforeOpen(); this.setState({ showModal: true }); } + renderModal() { + return ( + + + {this.props.modalTitle} + + + {this.props.modalBody} + + + ); + } render() { const classNames = cx({ 'btn btn-default btn-sm': this.props.isButton, }); - return ( - + if (this.props.isButton) { + return ( + + ); + } + return ( + + {this.props.triggerNode} + {this.renderModal()} ); } diff --git a/caravel/assets/javascripts/dashboard/Dashboard.jsx b/caravel/assets/javascripts/dashboard/Dashboard.jsx index c95824ac3ea3c..b8e297b169b3f 100644 --- a/caravel/assets/javascripts/dashboard/Dashboard.jsx +++ b/caravel/assets/javascripts/dashboard/Dashboard.jsx @@ -1,69 +1,95 @@ const $ = window.$ = require('jquery'); const jQuery = window.jQuery = require('jquery'); // eslint-disable-line -const px = require('../modules/caravel.js'); +const px = require('../modules/caravel'); const d3 = require('d3'); const urlLib = require('url'); -const utils = require('../modules/utils.js'); +const utils = require('../modules/utils'); import React from 'react'; import { render } from 'react-dom'; -import SliceAdder from './components/SliceAdder.jsx'; -import GridLayout from './components/GridLayout.jsx'; +import GridLayout from './components/GridLayout'; +import Header from './components/Header'; -const ace = require('brace'); require('bootstrap'); -require('brace/mode/css'); -require('brace/theme/crimson_editor'); require('../../stylesheets/dashboard.css'); require('../caravel-select2.js'); -// Injects the passed css string into a style sheet with the specified className -// If a stylesheet doesn't exist with the passed className, one will be injected into -function injectCss(className, css) { - const head = document.head || document.getElementsByTagName('head')[0]; - let style = document.querySelector('.' + className); +export function getInitialState(dashboardData, context) { + const dashboard = Object.assign({ context }, utils.controllerInterface, dashboardData); + dashboard.firstLoad = true; - if (!style) { - if (className.split(' ').length > 1) { - throw new Error('This method only supports selections with a single class name.'); + dashboard.posDict = {}; + dashboard.position_json.forEach(position => { + dashboard.posDict[position.slice_id] = position; + }); + dashboard.curUserId = dashboard.context.user_id; + dashboard.refreshTimer = null; + + const state = { + dashboard, + }; + return state; +} + +function initDashboardView(dashboard) { + render( +
, + document.getElementById('dashboard-header') + ); + // eslint-disable-next-line no-param-reassign + dashboard.reactGridLayout = render( + , + document.getElementById('grid-container') + ); + + // Displaying widget controls on hover + $('.react-grid-item').hover( + function () { + $(this).find('.chart-controls').fadeIn(300); + }, + function () { + $(this).find('.chart-controls').fadeOut(300); } - style = document.createElement('style'); - style.className = className; - style.type = 'text/css'; - head.appendChild(style); - } + ); + $('div.grid-container').css('visibility', 'visible'); + + $('.select2').select2({ + dropdownAutoWidth: true, + }); + $('div.widget').click(function (e) { + const $this = $(this); + const $target = $(e.target); - if (style.styleSheet) { - style.styleSheet.cssText = css; - } else { - style.innerHTML = css; - } + if ($target.hasClass('slice_info')) { + $this.find('.slice_description').slideToggle(0, function () { + $this.find('.refresh').click(); + }); + } else if ($target.hasClass('controls-toggle')) { + $this.find('.chart-controls').toggle(); + } + }); + px.initFavStars(); + $('[data-toggle="tooltip"]').tooltip({ container: 'body' }); } -function dashboardContainer(dashboardData) { - let dashboard = Object.assign({}, utils.controllerInterface, dashboardData, { +function dashboardContainer(dashboard) { + return Object.assign({}, dashboard, { type: 'dashboard', filters: {}, init() { - this.initDashboardView(); - this.firstLoad = true; - px.initFavStars(); - const sliceObjects = []; - const dash = this; + this.sliceObjects = []; dashboard.slices.forEach((data) => { if (data.error) { const html = '
' + data.error + '
'; $('#slice_' + data.slice_id).find('.token').html(html); } else { - const slice = px.Slice(data, dash); + const slice = px.Slice(data, this); $('#slice_' + data.slice_id).find('a.refresh').click(() => { slice.render(true); }); - sliceObjects.push(slice); + this.sliceObjects.push(slice); } }); - this.slices = sliceObjects; - this.refreshTimer = null; this.loadPreSelectFilters(); this.startPeriodicRender(0); this.bindResizeToWindowResize(); @@ -143,7 +169,7 @@ function dashboardContainer(dashboardData) { }, readFilters() { // Returns a list of human readable active filters - return JSON.stringify(this.filters, null, 4); + return JSON.stringify(this.filters, null, ' '); }, updateFilterParamsInUrl() { const urlObj = urlLib.parse(location.href, true); @@ -158,7 +184,7 @@ function dashboardContainer(dashboardData) { $(window).on('resize', () => { clearTimeout(resizeTimer); resizeTimer = setTimeout(() => { - dash.slices.forEach((slice) => { + dash.sliceObjects.forEach((slice) => { slice.resize(); }); }, 500); @@ -173,11 +199,11 @@ function dashboardContainer(dashboardData) { startPeriodicRender(interval) { this.stopPeriodicRender(); const dash = this; - const maxRandomDelay = Math.min(interval * 0.2, 5000); - const refreshAll = function () { - dash.slices.forEach(function (slice) { + const maxRandomDelay = Math.max(interval * 0.2, 5000); + const refreshAll = () => { + dash.sliceObjects.forEach(slice => { const force = !dash.firstLoad; - setTimeout(function () { + setTimeout(() => { slice.render(force); }, // Randomize to prevent all widgets refreshing at the same time @@ -198,7 +224,7 @@ function dashboardContainer(dashboardData) { }, refreshExcept(sliceId) { const immune = this.metadata.filter_immune_slices || []; - this.slices.forEach(function (slice) { + this.sliceObjects.forEach(slice => { if (slice.data.slice_id !== sliceId && immune.indexOf(slice.data.slice_id) === -1) { slice.render(); const sliceSeletor = $(`#${slice.data.token}-cell`); @@ -243,17 +269,6 @@ function dashboardContainer(dashboardData) { } return slice; }, - showAddSlice() { - const slicesOnDashMap = {}; - const layoutPositions = this.reactGridLayout.serialize(); - layoutPositions.forEach((position) => { - slicesOnDashMap[position.slice_id] = true; - }); - render( - , - document.getElementById('add-slice-container') - ); - }, getAjaxErrorMsg(error) { const respJSON = error.responseJSON; return (respJSON && respJSON.message) ? respJSON.message : @@ -263,7 +278,7 @@ function dashboardContainer(dashboardData) { const getAjaxErrorMsg = this.getAjaxErrorMsg; $.ajax({ type: 'POST', - url: '/caravel/add_slices/' + dashboard.id + '/', + url: `/caravel/add_slices/${dashboard.id}/`, data: { data: JSON.stringify({ slice_ids: sliceIds }), }, @@ -280,137 +295,16 @@ function dashboardContainer(dashboardData) { }, }); }, - saveDashboard() { - const expandedSlices = {}; - $.each($('.slice_info'), function () { - const widget = $(this).parents('.widget'); - const sliceDescription = widget.find('.slice_description'); - if (sliceDescription.is(':visible')) { - expandedSlices[$(widget).attr('data-slice-id')] = true; - } - }); - const positions = this.reactGridLayout.serialize(); - const data = { - positions, - css: this.editor.getValue(), - expanded_slices: expandedSlices, - }; - $.ajax({ - type: 'POST', - url: '/caravel/save_dash/' + dashboard.id + '/', - data: { - data: JSON.stringify(data), - }, - success() { - utils.showModal({ - title: 'Success', - body: 'This dashboard was saved successfully.', - }); - }, - error(error) { - const errorMsg = this.getAjaxErrorMsg(error); - utils.showModal({ - title: 'Error', - body: 'Sorry, there was an error saving this dashboard: ' + errorMsg, - }); - }, - }); - }, - initDashboardView() { - this.posDict = {}; - this.position_json.forEach(function (position) { - this.posDict[position.slice_id] = position; - }, this); - - this.reactGridLayout = render( - , - document.getElementById('grid-container') - ); - - this.curUserId = $('.dashboard').data('user'); - - dashboard = this; - - // Displaying widget controls on hover - $('.react-grid-item').hover( - function () { - $(this).find('.chart-controls').fadeIn(300); - }, - function () { - $(this).find('.chart-controls').fadeOut(300); - } - ); - $('div.grid-container').css('visibility', 'visible'); - $('#savedash').click(this.saveDashboard.bind(this)); - $('#add-slice').click(this.showAddSlice.bind(this)); - - const editor = ace.edit('dash_css'); - this.editor = editor; - editor.$blockScrolling = Infinity; - - editor.setTheme('ace/theme/crimson_editor'); - editor.setOptions({ - minLines: 16, - maxLines: Infinity, - useWorker: false, - }); - editor.getSession().setMode('ace/mode/css'); - - $('.select2').select2({ - dropdownAutoWidth: true, - }); - $('#css_template').on('change', function () { - const css = $(this).find('option:selected').data('css'); - editor.setValue(css); - - $('#dash_css').val(css); - injectCss('dashboard-template', css); - }); - $('#filters').click(() => { - utils.showModal({ - title: ' Current Global Filters', - body: 'The following global filters are currently applied:
' + - dashboard.readFilters(), - }); - }); - $('#refresh_dash_interval').on('change', function () { - const interval = $(this).find('option:selected').val() * 1000; - dashboard.startPeriodicRender(interval); - }); - $('#refresh_dash').click(() => { - dashboard.slices.forEach((slice) => { - slice.render(true); - }); - }); - - $('div.widget').click(function (e) { - const $this = $(this); - const $target = $(e.target); - - if ($target.hasClass('slice_info')) { - $this.find('.slice_description').slideToggle(0, function () { - $this.find('.refresh').click(); - }); - } else if ($target.hasClass('controls-toggle')) { - $this.find('.chart-controls').toggle(); - } - }); - - editor.on('change', function () { - const css = editor.getValue(); - $('#dash_css').val(css); - injectCss('dashboard-template', css); - }); - - const css = $('.dashboard').data('css'); - injectCss('dashboard-template', css); - }, }); - dashboard.init(); - return dashboard; } $(document).ready(() => { - dashboardContainer($('.dashboard').data('dashboard')); - $('[data-toggle="tooltip"]').tooltip({ container: 'body' }); + // Getting bootstrapped data from the DOM + const dashboardData = $('.dashboard').data('dashboard'); + const contextData = $('.dashboard').data('context'); + + const state = getInitialState(dashboardData, contextData); + const dashboard = dashboardContainer(state.dashboard); + initDashboardView(dashboard); + dashboard.init(); }); diff --git a/caravel/assets/javascripts/dashboard/components/CodeModal.jsx b/caravel/assets/javascripts/dashboard/components/CodeModal.jsx new file mode 100644 index 0000000000000..3346dabf5e6df --- /dev/null +++ b/caravel/assets/javascripts/dashboard/components/CodeModal.jsx @@ -0,0 +1,47 @@ +import React from 'react'; + +import ModalTrigger from '../../components/ModalTrigger'; + +const propTypes = { + triggerNode: React.PropTypes.node.isRequired, + code: React.PropTypes.string, + codeCallback: React.PropTypes.func, +}; + +const defaultProps = { +}; + +export default class CodeModal extends React.PureComponent { + constructor(props) { + super(props); + this.state = { + refreshFrequency: props.initialRefreshFrequency, + }; + } + beforeOpen() { + let code = this.props.code; + if (this.props.codeCallback) { + code = this.props.codeCallback(); + } + this.setState({ code }); + } + render() { + return ( + +
+              {this.state.code}
+            
+ + } + /> + ); + } +} +CodeModal.propTypes = propTypes; +CodeModal.defaultProps = defaultProps; diff --git a/caravel/assets/javascripts/dashboard/components/Controls.jsx b/caravel/assets/javascripts/dashboard/components/Controls.jsx new file mode 100644 index 0000000000000..6409e8d501218 --- /dev/null +++ b/caravel/assets/javascripts/dashboard/components/Controls.jsx @@ -0,0 +1,143 @@ +const $ = window.$ = require('jquery'); +import React from 'react'; + +import { ButtonGroup } from 'react-bootstrap'; +import Button from '../../components/Button'; +import { showModal } from '../../modules/utils'; +import CssEditor from './CssEditor'; +import RefreshIntervalModal from './RefreshIntervalModal'; +import CodeModal from './CodeModal'; +import SliceAdder from './SliceAdder'; + +const propTypes = { + table: React.PropTypes.object, + dashboard: React.PropTypes.object.isRequired, +}; + +const defaultProps = { + actions: {}, +}; + +class Controls extends React.PureComponent { + constructor(props) { + super(props); + this.state = { + css: props.dashboard.css, + cssTemplates: [], + }; + } + refresh() { + this.props.dashboard.sliceObjects.forEach((slice) => { + slice.render(true); + }); + } + componentWillMount() { + $.get('/csstemplateasyncmodelview/api/read', (data) => { + const cssTemplates = data.result.map((row) => ({ + value: row.template_name, + css: row.css, + label: row.template_name, + })); + this.setState({ cssTemplates }); + }); + } + save() { + const dashboard = this.props.dashboard; + const expandedSlices = {}; + $.each($('.slice_info'), function () { + const widget = $(this).parents('.widget'); + const sliceDescription = widget.find('.slice_description'); + if (sliceDescription.is(':visible')) { + expandedSlices[$(widget).attr('data-slice-id')] = true; + } + }); + const positions = dashboard.reactGridLayout.serialize(); + const data = { + positions, + css: this.state.css, + expanded_slices: expandedSlices, + }; + $.ajax({ + type: 'POST', + url: '/caravel/save_dash/' + dashboard.id + '/', + data: { + data: JSON.stringify(data), + }, + success() { + showModal({ + title: 'Success', + body: 'This dashboard was saved successfully.', + }); + }, + error(error) { + const errorMsg = this.getAjaxErrorMsg(error); + showModal({ + title: 'Error', + body: 'Sorry, there was an error saving this dashboard: ' + errorMsg, + }); + }, + }); + } + changeCss(css) { + this.setState({ css }); + } + render() { + const dashboard = this.props.dashboard; + const canSave = dashboard.context.dash_save_perm; + return ( + + + + } + /> + dashboard.startPeriodicRender(refreshInterval * 1000)} + triggerNode={ + + } + /> + } + /> + + } + initialCss={dashboard.css} + templates={this.state.cssTemplates} + onChange={this.changeCss.bind(this)} + /> + + + + ); + } +} +Controls.propTypes = propTypes; +Controls.defaultProps = defaultProps; + +export default Controls; diff --git a/caravel/assets/javascripts/dashboard/components/CssEditor.jsx b/caravel/assets/javascripts/dashboard/components/CssEditor.jsx new file mode 100644 index 0000000000000..e65a4dbd1663c --- /dev/null +++ b/caravel/assets/javascripts/dashboard/components/CssEditor.jsx @@ -0,0 +1,110 @@ +import React from 'react'; + +import ModalTrigger from '../../components/ModalTrigger'; +import Select from 'react-select'; + +import AceEditor from 'react-ace'; +import 'brace/mode/css'; +import 'brace/theme/github'; + + +const propTypes = { + initialCss: React.PropTypes.string, + triggerNode: React.PropTypes.node.isRequired, + onChange: React.PropTypes.func, + templates: React.PropTypes.array, +}; + +const defaultProps = { + initialCss: '', + onChange: () => {}, + templates: [], +}; + +class CssEditor extends React.PureComponent { + constructor(props) { + super(props); + this.state = { + css: props.initialCss, + cssTemplateOptions: [], + }; + } + componentWillMount() { + this.updateDom(); + } + changeCss(css) { + this.setState({ css }, this.updateDom); + this.props.onChange(css); + } + updateDom() { + const css = this.state.css; + const className = 'CssEditor-css'; + const head = document.head || document.getElementsByTagName('head')[0]; + let style = document.querySelector('.' + className); + + if (!style) { + style = document.createElement('style'); + style.className = className; + style.type = 'text/css'; + head.appendChild(style); + } + if (style.styleSheet) { + style.styleSheet.cssText = css; + } else { + style.innerHTML = css; + } + } + changeCssTemplate(opt) { + this.changeCss(opt.css); + } + renderTemplateSelector() { + if (this.props.templates) { + return ( +
+
Load a template
+ { + this.setState({ refreshFrequency: opt.value }); + this.props.onChange(opt.value); + }} + /> +
+ } + /> + ); + } +} +RefreshIntervalModal.propTypes = propTypes; +RefreshIntervalModal.defaultProps = defaultProps; + +export default RefreshIntervalModal; diff --git a/caravel/assets/javascripts/dashboard/components/SliceAdder.jsx b/caravel/assets/javascripts/dashboard/components/SliceAdder.jsx index eb4bba816f0ee..69959642765ae 100644 --- a/caravel/assets/javascripts/dashboard/components/SliceAdder.jsx +++ b/caravel/assets/javascripts/dashboard/components/SliceAdder.jsx @@ -1,13 +1,12 @@ import $ from 'jquery'; import React, { PropTypes } from 'react'; -import update from 'immutability-helper'; import { BootstrapTable, TableHeaderColumn } from 'react-bootstrap-table'; -import Modal from './Modal.jsx'; +import ModalTrigger from '../../components/ModalTrigger'; require('react-bootstrap-table/css/react-bootstrap-table.css'); const propTypes = { dashboard: PropTypes.object.isRequired, - caravel: PropTypes.object.isRequired, + triggerNode: PropTypes.node.isRequired, }; class SliceAdder extends React.Component { @@ -15,22 +14,23 @@ class SliceAdder extends React.Component { super(props); this.state = { slices: [], + slicesLoaded: false, + selectionMap: {}, + }; + + this.options = { + defaultSortOrder: 'desc', + defaultSortName: 'modified', + sizePerPage: 10, }; this.addSlices = this.addSlices.bind(this); this.toggleSlice = this.toggleSlice.bind(this); - this.toggleAllSlices = this.toggleAllSlices.bind(this); - this.slicesLoaded = false; + this.selectRowProp = { mode: 'checkbox', clickToSelect: true, onSelect: this.toggleSlice, - onSelectAll: this.toggleAllSlices, - }; - this.options = { - defaultSortOrder: 'desc', - defaultSortName: 'modified', - sizePerPage: 10, }; } @@ -39,30 +39,27 @@ class SliceAdder extends React.Component { this.slicesRequest = $.ajax({ url: uri, type: 'GET', - success: function (response) { - this.slicesLoaded = true; + success: response => { // Prepare slice data for table - const slices = response.result.map(function (slice) { - return { - id: slice.data.slice_id, - sliceName: slice.data.slice_name, - vizType: slice.viz_type, - modified: slice.modified, - data: slice.data, - }; - }); + const slices = response.result.map(slice => ({ + id: slice.id, + sliceName: slice.slice_name, + vizType: slice.viz_type, + modified: slice.modified, + })); this.setState({ slices, selectionMap: {}, + slicesLoaded: true, }); - }.bind(this), - error: function (error) { + }, + error: error => { this.errored = true; this.setState({ errorMsg: this.props.dashboard.getAjaxErrorMsg(error), }); - }.bind(this), + }, }); } @@ -71,42 +68,13 @@ class SliceAdder extends React.Component { } addSlices() { - const slices = this.state.slices.filter(function (slice) { - return this.state.selectionMap[slice.id]; - }, this); - slices.forEach(function (slice) { - const sliceObj = this.props.caravel.Slice(slice.data, this.props.dashboard); - $('#slice_' + slice.data.slice_id).find('a.refresh').click(function () { - sliceObj.render(true); - }); - this.props.dashboard.slices.push(sliceObj); - }, this); - this.props.dashboard.addSlicesToDashboard(Object.keys(this.state.selectionMap)); } toggleSlice(slice) { - this.setState({ - selectionMap: update(this.state.selectionMap, { - [slice.id]: { - $set: !this.state.selectionMap[slice.id], - }, - }), - }); - } - - toggleAllSlices(value) { - const updatePayload = {}; - - this.state.slices.forEach(function (slice) { - updatePayload[slice.id] = { - $set: value, - }; - }, this); - - this.setState({ - selectionMap: update(this.state.selectionMap, updatePayload), - }); + const selectionMap = Object.assign({}, this.state.selectionMap); + selectionMap[slice.id] = !selectionMap[slice.id]; + this.setState({ selectionMap }); } modifiedDateComparator(a, b, order) { @@ -128,7 +96,7 @@ class SliceAdder extends React.Component { } render() { - const hideLoad = this.slicesLoaded || this.errored; + const hideLoad = this.state.slicesLoaded || this.errored; let enableAddSlice = this.state.selectionMap && Object.keys(this.state.selectionMap); if (enableAddSlice) { enableAddSlice = enableAddSlice.some(function (key) { @@ -145,7 +113,7 @@ class SliceAdder extends React.Component {
{this.state.errorMsg}
-
+
+
); - const customButton = ( - - ); return ( - ); } diff --git a/caravel/assets/javascripts/dashboard/components/SliceCell.js b/caravel/assets/javascripts/dashboard/components/SliceCell.jsx similarity index 99% rename from caravel/assets/javascripts/dashboard/components/SliceCell.js rename to caravel/assets/javascripts/dashboard/components/SliceCell.jsx index 08aff96a760fe..10abe1cafc534 100644 --- a/caravel/assets/javascripts/dashboard/components/SliceCell.js +++ b/caravel/assets/javascripts/dashboard/components/SliceCell.jsx @@ -53,7 +53,6 @@ function SliceCell({ expandedSlices, removeSlice, slice }) { -
{ + const mockedProps = { + slice, + removeSlice: () => {}, + expandedSlices: () => {}, + }; + it('is valid', () => { + expect( + React.isValidElement() + ).to.equal(true); + }); + it('renders five links', () => { + const wrapper = mount(); + expect(wrapper.find('a')).to.have.length(5); + }); +}); diff --git a/caravel/assets/spec/javascripts/dashboard/fixtures.jsx b/caravel/assets/spec/javascripts/dashboard/fixtures.jsx new file mode 100644 index 0000000000000..7ac259e9416bb --- /dev/null +++ b/caravel/assets/spec/javascripts/dashboard/fixtures.jsx @@ -0,0 +1,69 @@ +export const slice = { + token: 'token_089ec8c1', + csv_endpoint: '', + edit_url: '/slicemodelview/edit/39', + viz_name: 'filter_box', + json_endpoint: '', + slice_id: 39, + standalone_endpoint: '', + description_markeddown: '', + form_data: { + collapsed_fieldsets: null, + time_grain_sqla: 'Time Column', + granularity_sqla: 'year', + standalone: null, + date_filter: false, + until: '2014-01-02', + extra_filters: null, + force: null, + where: '', + since: '2014-01-01', + async: null, + slice_id: null, + json: null, + having: '', + flt_op_2: 'in', + previous_viz_type: 'filter_box', + groupby: [ + 'region', + 'country_name', + ], + flt_col_7: '', + slice_name: null, + viz_type: 'filter_box', + metric: 'sum__SP_POP_TOTL', + flt_col_8: '', + }, + slice_url: '', + slice_name: 'Region Filter', + description: null, + column_formats: {}, +}; +export const dashboardData = { + css: '', + metadata: { + filter_immune_slices: [], + filter_immune_slice_fields: {}, + expanded_slices: {}, + }, + slug: 'births', + position_json: [ + { + size_x: 2, + slice_id: '52', + row: 0, + size_y: 2, + col: 1, + }, + ], + id: 2, + slices: [slice], + dashboard_title: 'Births', +}; + +export const contextData = { + dash_save_perm: true, + standalone_mode: false, + dash_edit_perm: true, + user_id: '1', +}; diff --git a/caravel/assets/stylesheets/caravel.css b/caravel/assets/stylesheets/caravel.css index 94e7018137b1c..37863fce1a804 100644 --- a/caravel/assets/stylesheets/caravel.css +++ b/caravel/assets/stylesheets/caravel.css @@ -156,7 +156,6 @@ li.widget:hover { div.widget .chart-header { padding-top: 8px; - background-color: #fff; color: #333; border-bottom: 1px solid #aaa; margin: 0 10px; diff --git a/caravel/assets/stylesheets/dashboard.css b/caravel/assets/stylesheets/dashboard.css index b09d9444a21fe..b140e77808fb5 100644 --- a/caravel/assets/stylesheets/dashboard.css +++ b/caravel/assets/stylesheets/dashboard.css @@ -118,3 +118,12 @@ div.widget .chart-controls { .chart-header .header { font-size: 16px; } +.ace_gutter { + z-index: 0; +} +.ace_content { + z-index: 0; +} +.ace_scrollbar { + z-index: 0; +} diff --git a/caravel/assets/visualizations/big_number.js b/caravel/assets/visualizations/big_number.js index d8c2a6a8f9b92..2a5c68161c813 100644 --- a/caravel/assets/visualizations/big_number.js +++ b/caravel/assets/visualizations/big_number.js @@ -4,9 +4,8 @@ import { formatDate } from '../javascripts/modules/dates'; require('./big_number.css'); function bigNumberVis(slice) { - const div = d3.select(slice.selector); - function render() { + const div = d3.select(slice.selector); d3.json(slice.jsonEndpoint(), function (error, payload) { // Define the percentage bounds that define color from red to green if (error !== null) { diff --git a/caravel/models.py b/caravel/models.py index 8494d4716a81b..00440cb3809a6 100644 --- a/caravel/models.py +++ b/caravel/models.py @@ -251,6 +251,7 @@ def data(self): d = self.viz.data self.token = d.get('token') except Exception as e: + logging.exception(e) d['error'] = str(e) d['slice_id'] = self.id d['slice_name'] = self.slice_name @@ -433,13 +434,17 @@ def dashboard_link(self): @property def json_data(self): + positions = self.position_json + if positions: + positions = json.loads(positions) d = { 'id': self.id, 'metadata': self.params_dict, + 'css': self.css, 'dashboard_title': self.dashboard_title, 'slug': self.slug, 'slices': [slc.data for slc in self.slices], - 'position_json': json.loads(self.position_json) if self.position_json else [], + 'position_json': positions, } return json.dumps(d) @@ -714,8 +719,6 @@ def select_star( self, table_name, schema=None, limit=100, show_cols=False, indent=True): """Generates a ``select *`` statement in the proper dialect""" - for i in range(10): - print(schema) quote = self.get_quoter() fields = '*' table = self.get_table(table_name, schema=schema) diff --git a/caravel/templates/caravel/dashboard.html b/caravel/templates/caravel/dashboard.html index f6c65fcd21be3..2e141ce6b7471 100644 --- a/caravel/templates/caravel/dashboard.html +++ b/caravel/templates/caravel/dashboard.html @@ -12,108 +12,11 @@
{% include 'caravel/flash_wrapper.html' %} - - - - {% if not standalone_mode %} -
- {% endif %} - -
-
-
-

- - {{ dashboard.dashboard_title }} - - -

-
- {% if not standalone_mode %} -
-
- - - - - - - - - -
-
- {% endif %} -
-
+
diff --git a/caravel/utils.py b/caravel/utils.py index 0fbe27687b5dc..af4fdfce27359 100644 --- a/caravel/utils.py +++ b/caravel/utils.py @@ -16,16 +16,17 @@ import uuid from sqlalchemy import event, exc -from sqlalchemy.pool import Pool import parsedatetime import sqlalchemy as sa from dateutil.parser import parse from flask import flash, Markup from flask_appbuilder.security.sqla import models as ab_models -from markdown import markdown as md +import markdown as md from sqlalchemy.types import TypeDecorator, TEXT from pydruid.utils.having import Having +logging.getLogger('MARKDOWN').setLevel(logging.INFO) + EPOCH = datetime(1970, 1, 1) @@ -426,8 +427,7 @@ def error_msg_from_exception(e): def markdown(s, markup_wrap=False): - s = s or '' - s = md(s, [ + s = md.markdown(s or '', [ 'markdown.extensions.tables', 'markdown.extensions.fenced_code', 'markdown.extensions.codehilite', diff --git a/caravel/views.py b/caravel/views.py index 92ffea518b0c3..b0f58d03bb963 100755 --- a/caravel/views.py +++ b/caravel/views.py @@ -832,13 +832,8 @@ class SliceAsync(SliceModelView): # noqa class SliceAddView(SliceModelView): # noqa list_columns = [ - 'slice_link', 'viz_type', - 'owners', 'modified', 'data', 'changed_on'] - label_columns = { - 'icons': ' ', - 'slice_link': _('Slice'), - 'viz_type': _('Visualization Type'), - } + 'id', 'slice_name', 'slice_link', 'viz_type', + 'owners', 'modified', 'changed_on'] appbuilder.add_view_no_menu(SliceAddView) @@ -1705,7 +1700,6 @@ def dashboard(self, dashboard_id): else: qry = qry.filter_by(slug=dashboard_id) - templates = session.query(models.CssTemplate).all() dash = qry.one() datasources = {slc.datasource for slc in dash.slices} for datasource in datasources: @@ -1727,13 +1721,17 @@ def dashboard(**kwargs): # noqa dash_save_perm = \ dash_edit_perm and self.can_access('can_save_dash', 'Caravel') standalone = request.args.get("standalone") == "true" - return self.render_template( - "caravel/dashboard.html", dashboard=dash, + context = dict( user_id=g.user.get_id(), - templates=templates, dash_save_perm=dash_save_perm, dash_edit_perm=dash_edit_perm, - standalone_mode=standalone) + standalone_mode=standalone, + ) + return self.render_template( + "caravel/dashboard.html", + dashboard=dash, + context=json.dumps(context), + ) @has_access @expose("/sync_druid/", methods=['POST']) @@ -2293,6 +2291,10 @@ class CssTemplateModelView(CaravelModelView, DeleteMixin): edit_columns = ['template_name', 'css'] add_columns = edit_columns + +class CssTemplateAsyncModelView(CssTemplateModelView): + list_columns = ['template_name', 'css'] + appbuilder.add_separator("Sources") appbuilder.add_view( CssTemplateModelView, @@ -2303,6 +2305,8 @@ class CssTemplateModelView(CaravelModelView, DeleteMixin): category_label=__("Manage"), category_icon='') +appbuilder.add_view_no_menu(CssTemplateAsyncModelView) + appbuilder.add_link( 'SQL Editor', href='/caravel/sqllab', @@ -2337,9 +2341,4 @@ def __init__(self, url_map, *items): @app.route('/') def panoramix(url): # noqa return redirect(request.full_path.replace('panoramix', 'caravel')) - - -@app.route('/') -def dashed(url): # noqa - return redirect(request.full_path.replace('dashed', 'caravel')) # ---------------------------------------------------------------------