diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f026c908568a7..da403d16f46eb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -173,7 +173,7 @@ Check the [OS dependencies](https://superset.incubator.apache.org/installation.h source env/bin/activate # install for development - python setup.py develop + pip install -e . # Create an admin user fabmanager create-admin --app superset @@ -331,6 +331,8 @@ key is to instrument the strings that need translation using a module, all you have to do is to `_("Wrap your strings")` using the underscore `_` "function". +We use `import {t, tn, TCT} from locales;` in js, JSX file, locales is in `./superset/assets/javascripts/` directory. + To enable changing language in your environment, you can simply add the `LANGUAGES` parameter to your `superset_config.py`. Having more than one options here will add a language selection dropdown on the right side of the @@ -342,6 +344,10 @@ navigation bar. 'zh': {'flag': 'cn', 'name': 'Chinese'}, } +We need to extract the string to be translated, run the following command: + + pybabel extract -F ./babel/babel.cfg -k _ -k __ -k t -k tn -k tct -o ./babel/messages.pot . + As per the [Flask AppBuilder documentation] about translation, to create a new language dictionary, run the following command: @@ -358,6 +364,14 @@ to take effect, they need to be compiled using this command: fabmanager babel-compile --target superset/translations/ +In the case of JS translation, we need to convert the PO file into a JSON file, and we need the global download of the npm package po2json. +We need to be compiled using this command: + + npm install po2json -g + +Execute this command to convert the en PO file into a json file: + + po2json -d superset -f jed1.x superset/translations/en/LC_MESSAGES/messages.po superset/translations/en/LC_MESSAGES/messages.json ## Adding new datasources diff --git a/babel/babel.cfg b/babel/babel.cfg index 790b262731b74..762ac64e77c96 100644 --- a/babel/babel.cfg +++ b/babel/babel.cfg @@ -1,4 +1,8 @@ [ignore: superset/assets/node_modules/**] [python: superset/**.py] [jinja2: superset/**/templates/**.html] +[javascript: superset/assets/javascripts/**.js] +[javascript: superset/assets/javascripts/**.jsx] +[javascript: superset/assets/visualizations/**.js] +[javascript: superset/assets/visualizations/**.jsx] encoding = utf-8 diff --git a/babel/messages.pot b/babel/messages.pot index 5ebccd2646a4e..810ccb0dbbcfa 100755 --- a/babel/messages.pot +++ b/babel/messages.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2017-08-17 03:23+0200\n" +"POT-Creation-Date: 2017-09-12 20:55-0700\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -17,76 +17,77 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.4.0\n" +#: superset/assets/javascripts/explore/stores/controls.jsx:505 #: superset/db_engine_specs.py:192 superset/db_engine_specs.py:223 #: superset/db_engine_specs.py:267 superset/db_engine_specs.py:315 -#: superset/db_engine_specs.py:360 superset/db_engine_specs.py:810 -#: superset/db_engine_specs.py:846 superset/db_engine_specs.py:878 -#: superset/db_engine_specs.py:924 superset/db_engine_specs.py:961 -#: superset/db_engine_specs.py:987 +#: superset/db_engine_specs.py:371 superset/db_engine_specs.py:839 +#: superset/db_engine_specs.py:875 superset/db_engine_specs.py:907 +#: superset/db_engine_specs.py:953 superset/db_engine_specs.py:990 +#: superset/db_engine_specs.py:1016 msgid "Time Column" msgstr "" #: superset/db_engine_specs.py:193 superset/db_engine_specs.py:224 -#: superset/db_engine_specs.py:316 superset/db_engine_specs.py:361 -#: superset/db_engine_specs.py:811 superset/db_engine_specs.py:879 -#: superset/db_engine_specs.py:962 +#: superset/db_engine_specs.py:316 superset/db_engine_specs.py:372 +#: superset/db_engine_specs.py:840 superset/db_engine_specs.py:908 +#: superset/db_engine_specs.py:991 msgid "second" msgstr "" #: superset/db_engine_specs.py:194 superset/db_engine_specs.py:227 -#: superset/db_engine_specs.py:319 superset/db_engine_specs.py:363 -#: superset/db_engine_specs.py:813 superset/db_engine_specs.py:847 -#: superset/db_engine_specs.py:881 superset/db_engine_specs.py:925 -#: superset/db_engine_specs.py:963 superset/db_engine_specs.py:988 +#: superset/db_engine_specs.py:319 superset/db_engine_specs.py:374 +#: superset/db_engine_specs.py:842 superset/db_engine_specs.py:876 +#: superset/db_engine_specs.py:910 superset/db_engine_specs.py:954 +#: superset/db_engine_specs.py:992 superset/db_engine_specs.py:1017 msgid "minute" msgstr "" #: superset/db_engine_specs.py:195 superset/db_engine_specs.py:231 -#: superset/db_engine_specs.py:321 superset/db_engine_specs.py:365 -#: superset/db_engine_specs.py:819 superset/db_engine_specs.py:849 -#: superset/db_engine_specs.py:883 superset/db_engine_specs.py:931 -#: superset/db_engine_specs.py:964 superset/db_engine_specs.py:989 +#: superset/db_engine_specs.py:321 superset/db_engine_specs.py:376 +#: superset/db_engine_specs.py:848 superset/db_engine_specs.py:878 +#: superset/db_engine_specs.py:912 superset/db_engine_specs.py:960 +#: superset/db_engine_specs.py:993 superset/db_engine_specs.py:1018 msgid "hour" msgstr "" #: superset/db_engine_specs.py:196 superset/db_engine_specs.py:236 #: superset/db_engine_specs.py:268 superset/db_engine_specs.py:323 -#: superset/db_engine_specs.py:367 superset/db_engine_specs.py:821 -#: superset/db_engine_specs.py:851 superset/db_engine_specs.py:885 -#: superset/db_engine_specs.py:933 superset/db_engine_specs.py:965 -#: superset/db_engine_specs.py:990 +#: superset/db_engine_specs.py:378 superset/db_engine_specs.py:850 +#: superset/db_engine_specs.py:880 superset/db_engine_specs.py:914 +#: superset/db_engine_specs.py:962 superset/db_engine_specs.py:994 +#: superset/db_engine_specs.py:1019 msgid "day" msgstr "" #: superset/db_engine_specs.py:197 superset/db_engine_specs.py:242 #: superset/db_engine_specs.py:269 superset/db_engine_specs.py:324 -#: superset/db_engine_specs.py:369 superset/db_engine_specs.py:823 -#: superset/db_engine_specs.py:853 superset/db_engine_specs.py:887 -#: superset/db_engine_specs.py:966 superset/db_engine_specs.py:991 +#: superset/db_engine_specs.py:380 superset/db_engine_specs.py:852 +#: superset/db_engine_specs.py:882 superset/db_engine_specs.py:916 +#: superset/db_engine_specs.py:995 superset/db_engine_specs.py:1020 msgid "week" msgstr "" #: superset/db_engine_specs.py:198 superset/db_engine_specs.py:244 #: superset/db_engine_specs.py:271 superset/db_engine_specs.py:326 -#: superset/db_engine_specs.py:371 superset/db_engine_specs.py:825 -#: superset/db_engine_specs.py:855 superset/db_engine_specs.py:889 -#: superset/db_engine_specs.py:935 superset/db_engine_specs.py:967 -#: superset/db_engine_specs.py:992 +#: superset/db_engine_specs.py:382 superset/db_engine_specs.py:854 +#: superset/db_engine_specs.py:884 superset/db_engine_specs.py:918 +#: superset/db_engine_specs.py:964 superset/db_engine_specs.py:996 +#: superset/db_engine_specs.py:1021 msgid "month" msgstr "" #: superset/db_engine_specs.py:199 superset/db_engine_specs.py:246 -#: superset/db_engine_specs.py:328 superset/db_engine_specs.py:373 -#: superset/db_engine_specs.py:827 superset/db_engine_specs.py:857 -#: superset/db_engine_specs.py:891 superset/db_engine_specs.py:937 -#: superset/db_engine_specs.py:968 superset/db_engine_specs.py:993 +#: superset/db_engine_specs.py:328 superset/db_engine_specs.py:384 +#: superset/db_engine_specs.py:856 superset/db_engine_specs.py:886 +#: superset/db_engine_specs.py:920 superset/db_engine_specs.py:966 +#: superset/db_engine_specs.py:997 superset/db_engine_specs.py:1022 msgid "quarter" msgstr "" #: superset/db_engine_specs.py:200 superset/db_engine_specs.py:250 -#: superset/db_engine_specs.py:330 superset/db_engine_specs.py:829 -#: superset/db_engine_specs.py:859 superset/db_engine_specs.py:939 -#: superset/db_engine_specs.py:969 superset/db_engine_specs.py:994 +#: superset/db_engine_specs.py:330 superset/db_engine_specs.py:858 +#: superset/db_engine_specs.py:888 superset/db_engine_specs.py:968 +#: superset/db_engine_specs.py:998 superset/db_engine_specs.py:1023 msgid "year" msgstr "" @@ -94,27 +95,27 @@ msgstr "" msgid "week_start_monday" msgstr "" -#: superset/db_engine_specs.py:375 superset/db_engine_specs.py:893 +#: superset/db_engine_specs.py:386 superset/db_engine_specs.py:922 msgid "week_ending_saturday" msgstr "" -#: superset/db_engine_specs.py:378 superset/db_engine_specs.py:896 +#: superset/db_engine_specs.py:389 superset/db_engine_specs.py:925 msgid "week_start_sunday" msgstr "" -#: superset/db_engine_specs.py:815 superset/db_engine_specs.py:927 +#: superset/db_engine_specs.py:844 superset/db_engine_specs.py:956 msgid "5 minute" msgstr "" -#: superset/db_engine_specs.py:817 +#: superset/db_engine_specs.py:846 msgid "half hour" msgstr "" -#: superset/db_engine_specs.py:929 +#: superset/db_engine_specs.py:958 msgid "10 minute" msgstr "" -#: superset/utils.py:499 +#: superset/utils.py:503 #, python-format msgid "[Superset] Access to the datasource %(name)s was granted" msgstr "" @@ -123,237 +124,2569 @@ msgstr "" msgid "Viz is missing a datasource" msgstr "" -#: superset/viz.py:158 +#: superset/viz.py:163 msgid "From date cannot be larger than to date" msgstr "" -#: superset/viz.py:322 +#: superset/assets/javascripts/explore/stores/visTypes.js:321 +#: superset/viz.py:328 msgid "Table View" msgstr "" -#: superset/viz.py:334 +#: superset/viz.py:340 msgid "Pick a granularity in the Time section or uncheck 'Include Time'" msgstr "" -#: superset/viz.py:344 +#: superset/viz.py:350 msgid "Choose either fields to [Group By] and [Metrics] or [Columns], not both" msgstr "" -#: superset/viz.py:378 +#: superset/assets/javascripts/explore/stores/visTypes.js:372 +#: superset/viz.py:384 msgid "Pivot Table" msgstr "" -#: superset/viz.py:392 +#: superset/viz.py:398 msgid "Please choose at least one \"Group by\" field " msgstr "" -#: superset/viz.py:394 -msgid "Please choose at least one metric" +#: superset/viz.py:400 +msgid "Please choose at least one metric" +msgstr "" + +#: superset/viz.py:404 +msgid "'Group By' and 'Columns' can't overlap" +msgstr "" + +#: superset/assets/javascripts/explore/stores/visTypes.js:359 +#: superset/viz.py:437 +msgid "Markup" +msgstr "" + +#: superset/assets/javascripts/explore/stores/visTypes.js:397 +#: superset/viz.py:456 +msgid "Separator" +msgstr "" + +#: superset/assets/javascripts/explore/stores/visTypes.js:419 +#: superset/viz.py:468 +msgid "Word Cloud" +msgstr "" + +#: superset/assets/javascripts/explore/stores/visTypes.js:440 +#: superset/viz.py:491 +msgid "Treemap" +msgstr "" + +#: superset/assets/javascripts/explore/stores/visTypes.js:467 +#: superset/viz.py:517 +msgid "Calendar Heatmap" +msgstr "" + +#: superset/assets/javascripts/explore/stores/visTypes.js:488 +#: superset/viz.py:575 +msgid "Box Plot" +msgstr "" + +#: superset/assets/javascripts/explore/stores/visTypes.js:509 +#: superset/viz.py:664 +msgid "Bubble Chart" +msgstr "" + +#: superset/viz.py:688 +msgid "Pick a metric for x, y and size" +msgstr "" + +#: superset/assets/javascripts/explore/stores/visTypes.js:560 +#: superset/viz.py:714 +msgid "Bullet Chart" +msgstr "" + +#: superset/viz.py:740 +msgid "Pick a metric to display" +msgstr "" + +#: superset/assets/javascripts/explore/stores/visTypes.js:583 +#: superset/viz.py:763 +msgid "Big Number with Trendline" +msgstr "" + +#: superset/viz.py:771 superset/viz.py:800 +msgid "Pick a metric!" +msgstr "" + +#: superset/assets/javascripts/explore/stores/visTypes.js:608 +#: superset/viz.py:792 +msgid "Big Number" +msgstr "" + +#: superset/assets/javascripts/explore/stores/visTypes.js:149 +#: superset/viz.py:819 +msgid "Time Series - Line Chart" +msgstr "" + +#: superset/viz.py:866 superset/viz.py:1011 +msgid "Pick a time granularity for your time series" +msgstr "" + +#: superset/viz.py:954 +msgid "Time Series - Dual Axis Line Chart" +msgstr "" + +#: superset/viz.py:964 +msgid "Pick a metric for left axis!" +msgstr "" + +#: superset/viz.py:966 +msgid "Pick a metric for right axis!" +msgstr "" + +#: superset/viz.py:968 +msgid "Please choose different metrics on left and right axis" +msgstr "" + +#: superset/assets/javascripts/explore/stores/visTypes.js:228 +#: superset/viz.py:1029 +msgid "Time Series - Bar Chart" +msgstr "" + +#: superset/assets/javascripts/explore/stores/visTypes.js:263 +#: superset/viz.py:1037 +msgid "Time Series - Percent Change" +msgstr "" + +#: superset/assets/javascripts/explore/stores/visTypes.js:285 +#: superset/viz.py:1045 +msgid "Time Series - Stacked" +msgstr "" + +#: superset/viz.py:1054 +msgid "Distribution - NVD3 - Pie Chart" +msgstr "" + +#: superset/assets/javascripts/explore/stores/visTypes.js:633 +#: superset/viz.py:1072 +msgid "Histogram" +msgstr "" + +#: superset/viz.py:1082 +msgid "Must have one numeric column specified" +msgstr "" + +#: superset/assets/javascripts/explore/stores/visTypes.js:88 +#: superset/viz.py:1097 +msgid "Distribution - Bar Chart" +msgstr "" + +#: superset/viz.py:1108 +msgid "Can't have overlap between Series and Breakdowns" +msgstr "" + +#: superset/viz.py:1110 +msgid "Pick at least one metric" +msgstr "" + +#: superset/viz.py:1112 +msgid "Pick at least one field for [Series]" +msgstr "" + +#: superset/assets/javascripts/explore/stores/visTypes.js:665 +#: superset/viz.py:1165 +msgid "Sunburst" +msgstr "" + +#: superset/assets/javascripts/explore/stores/visTypes.js:702 +#: superset/viz.py:1198 +msgid "Sankey" +msgstr "" + +#: superset/viz.py:1205 +msgid "Pick exactly 2 columns as [Source / Target]" +msgstr "" + +#: superset/viz.py:1236 +msgid "" +"There's a loop in your Sankey, please provide a tree. Here's a faulty " +"link: {}" +msgstr "" + +#: superset/assets/javascripts/explore/stores/visTypes.js:729 +#: superset/viz.py:1247 superset/viz.py:1268 +msgid "Directed Force Layout" +msgstr "" + +#: superset/viz.py:1254 +msgid "Pick exactly 2 columns to 'Group By'" +msgstr "" + +#: superset/assets/javascripts/explore/stores/visTypes.js:794 +#: superset/viz.py:1301 +msgid "Country Map" +msgstr "" + +#: superset/assets/javascripts/explore/stores/visTypes.js:827 +#: superset/viz.py:1330 +msgid "World Map" +msgstr "" + +#: superset/assets/javascripts/explore/stores/visTypes.js:72 +#: superset/viz.py:1380 +msgid "Filters" +msgstr "" + +#: superset/viz.py:1388 +msgid "Pick at least one filter field" +msgstr "" + +#: superset/assets/javascripts/explore/stores/visTypes.js:895 +#: superset/viz.py:1415 +msgid "iFrame" +msgstr "" + +#: superset/assets/javascripts/explore/stores/visTypes.js:907 +#: superset/viz.py:1432 +msgid "Parallel Coordinates" +msgstr "" + +#: superset/assets/javascripts/explore/stores/visTypes.js:929 +#: superset/viz.py:1457 +msgid "Heatmap" +msgstr "" + +#: superset/viz.py:1508 +msgid "Horizon Charts" +msgstr "" + +#: superset/assets/javascripts/explore/stores/visTypes.js:974 +#: superset/viz.py:1519 +msgid "Mapbox" +msgstr "" + +#: superset/viz.py:1534 +msgid "Must have a [Group By] column to have 'count' as the [Label]" +msgstr "" + +#: superset/viz.py:1547 +msgid "Choice of [Label] must be present in [Group By]" +msgstr "" + +#: superset/viz.py:1552 +msgid "Choice of [Point Radius] must be present in [Group By]" +msgstr "" + +#: superset/viz.py:1557 +msgid "[Longitude] and [Latitude] columns must be present in [Group By]" +msgstr "" + +#: superset/assets/javascripts/explore/stores/visTypes.js:1045 +#: superset/viz.py:1622 +msgid "Event flow" +msgstr "" + +#: superset/assets/javascripts/SqlLab/actions.js:57 +msgid "Your query was saved" +msgstr "" + +#: superset/assets/javascripts/SqlLab/actions.js:58 +msgid "Your query could not be saved" +msgstr "" + +#: superset/assets/javascripts/SqlLab/actions.js:111 +msgid "Failed at retrieving results from the results backend" +msgstr "" + +#: superset/assets/javascripts/SqlLab/actions.js:157 +msgid "Could not connect to server" +msgstr "" + +#: superset/assets/javascripts/SqlLab/actions.js:162 +msgid "Your session timed out, please refresh your page and try again." +msgstr "" + +#: superset/assets/javascripts/SqlLab/actions.js:181 +msgid "Query was stopped." +msgstr "" + +#: superset/assets/javascripts/SqlLab/actions.js:184 +msgid "Failed at stopping query." +msgstr "" + +#: superset/assets/javascripts/SqlLab/actions.js:297 +#: superset/assets/javascripts/SqlLab/actions.js:310 +msgid "Error occurred while fetching table metadata" +msgstr "" + +#: superset/assets/javascripts/SqlLab/actions.js:364 +msgid "shared query" +msgstr "" + +#: superset/assets/javascripts/SqlLab/actions.js:372 +#: superset/assets/javascripts/SqlLab/actions.js:392 +msgid "The query couldn't be loaded" +msgstr "" + +#: superset/assets/javascripts/SqlLab/actions.js:425 +msgid "An error occurred while creating the data source" +msgstr "" + +#: superset/assets/javascripts/SqlLab/constants.js:30 +msgid "Pick a chart type!" +msgstr "" + +#: superset/assets/javascripts/SqlLab/constants.js:31 +msgid "To use this chart type you need at least one column flagged as a date" +msgstr "" + +#: superset/assets/javascripts/SqlLab/constants.js:32 +msgid "To use this chart type you need at least one dimension" +msgstr "" + +#: superset/assets/javascripts/SqlLab/constants.js:33 +msgid "To use this chart type you need at least one aggregation function" +msgstr "" + +#: superset/assets/javascripts/SqlLab/components/QueryTable.jsx:49 +#: superset/assets/javascripts/SqlLab/reducers.js:11 +msgid "Untitled Query" +msgstr "" + +#: superset/assets/javascripts/SqlLab/reducers.js:44 +#, python-format +msgid "Copy of %s" +msgstr "" + +#: superset/assets/javascripts/SqlLab/components/CopyQueryTabUrl.jsx:30 +msgid "share query" +msgstr "" + +#: superset/assets/javascripts/SqlLab/components/CopyQueryTabUrl.jsx:33 +msgid "copy URL to clipboard" +msgstr "" + +#: superset/assets/javascripts/SqlLab/components/HighlightedSql.jsx:61 +msgid "Raw SQL" +msgstr "" + +#: superset/assets/javascripts/SqlLab/components/HighlightedSql.jsx:71 +msgid "Source SQL" +msgstr "" + +#: superset/assets/javascripts/SqlLab/components/HighlightedSql.jsx:83 +#: superset/assets/javascripts/explore/stores/visTypes.js:40 +msgid "SQL" +msgstr "" + +#: superset/assets/javascripts/SqlLab/components/QueryHistory.jsx:28 +msgid "No query history yet..." +msgstr "" + +#: superset/assets/javascripts/SqlLab/components/QuerySearch.jsx:106 +#: superset/assets/javascripts/SqlLab/components/SqlEditorLeftBar.jsx:66 +msgid "It seems you don't have access to any database" +msgstr "" + +#: superset/assets/javascripts/SqlLab/components/QuerySearch.jsx:154 +#: superset/assets/javascripts/SqlLab/components/ResultSet.jsx:89 +msgid "Search Results" +msgstr "" + +#: superset/assets/javascripts/SqlLab/components/QuerySearch.jsx:160 +msgid "[From]-" +msgstr "" + +#: superset/assets/javascripts/SqlLab/components/QuerySearch.jsx:170 +msgid "[To]-" +msgstr "" + +#: superset/assets/javascripts/SqlLab/components/QuerySearch.jsx:179 +msgid "[Query Status]" +msgstr "" + +#: superset/assets/javascripts/SqlLab/components/QuerySearch.jsx:188 +msgid "Search" +msgstr "" + +#: superset/assets/javascripts/SqlLab/components/QueryTable.jsx:114 +msgid "Open in SQL Editor" +msgstr "" + +#: superset/assets/javascripts/SqlLab/components/QueryTable.jsx:133 +msgid "view results" +msgstr "" + +#: superset/assets/javascripts/SqlLab/components/QueryTable.jsx:136 +msgid "Data preview" +msgstr "" + +#: superset/assets/javascripts/SqlLab/components/QueryTable.jsx:176 +msgid "Visualize the data out of this query" +msgstr "" + +#: superset/assets/javascripts/SqlLab/components/QueryTable.jsx:182 +msgid "Overwrite text in editor with a query on this table" +msgstr "" + +#: superset/assets/javascripts/SqlLab/components/QueryTable.jsx:188 +msgid "Run query in a new tab" +msgstr "" + +#: superset/assets/javascripts/SqlLab/components/QueryTable.jsx:193 +msgid "Remove query from log" +msgstr "" + +#: superset/assets/javascripts/SqlLab/components/ResultSet.jsx:67 +msgid ".CSV" +msgstr "" + +#: superset/assets/javascripts/SqlLab/components/ResultSet.jsx:78 +#: superset/assets/javascripts/SqlLab/components/VisualizeModal.jsx:241 +#: superset/assets/javascripts/SqlLab/components/VisualizeModal.jsx:280 +msgid "Visualize" +msgstr "" + +#: superset/assets/javascripts/SqlLab/components/ResultSet.jsx:162 +#: superset/connectors/sqla/views.py:86 superset/connectors/sqla/views.py:136 +#: superset/connectors/sqla/views.py:214 superset/views/core.py:376 +msgid "Table" +msgstr "" + +#: superset/assets/javascripts/SqlLab/components/ResultSet.jsx:162 +msgid "was created" +msgstr "" + +#: superset/assets/javascripts/SqlLab/components/ResultSet.jsx:169 +msgid "Query in a new tab" +msgstr "" + +#: superset/assets/javascripts/SqlLab/components/ResultSet.jsx:210 +msgid "Fetch data preview" +msgstr "" + +#: superset/assets/javascripts/SqlLab/components/ResultSet.jsx:230 +msgid "Track Job" +msgstr "" + +#: superset/assets/javascripts/SqlLab/components/ResultSet.jsx:236 +msgid "Loading..." +msgstr "" + +#: superset/assets/javascripts/SqlLab/components/RunQueryActionButton.jsx:19 +msgid "Run Selected Query" +msgstr "" + +#: superset/assets/javascripts/SqlLab/components/RunQueryActionButton.jsx:19 +msgid "Run Query" +msgstr "" + +#: superset/assets/javascripts/SqlLab/components/RunQueryActionButton.jsx:22 +msgid "Run query asynchronously" +msgstr "" + +#: superset/assets/javascripts/SqlLab/components/RunQueryActionButton.jsx:57 +msgid "Stop" +msgstr "" + +#: superset/assets/javascripts/SqlLab/components/SaveQuery.jsx:16 +msgid "Undefined" +msgstr "" + +#: superset/assets/javascripts/SqlLab/components/SaveQuery.jsx:66 +#: superset/views/sql_lab.py:53 +msgid "Label" +msgstr "" + +#: superset/assets/javascripts/SqlLab/components/SaveQuery.jsx:71 +msgid "Label for your query" +msgstr "" + +#: superset/assets/javascripts/SqlLab/components/SaveQuery.jsx:81 +#: superset/connectors/druid/views.py:107 +#: superset/connectors/druid/views.py:227 superset/connectors/sqla/views.py:83 +#: superset/connectors/sqla/views.py:132 superset/connectors/sqla/views.py:227 +#: superset/views/core.py:370 superset/views/sql_lab.py:56 +msgid "Description" +msgstr "" + +#: superset/assets/javascripts/SqlLab/components/SaveQuery.jsx:85 +msgid "Write a description for your query" +msgstr "" + +#: superset/assets/javascripts/SqlLab/components/SaveQuery.jsx:99 +#: superset/assets/javascripts/dashboard/components/SaveModal.jsx:155 +#: superset/assets/javascripts/explore/components/SaveModal.jsx:217 +msgid "Save" +msgstr "" + +#: superset/assets/javascripts/SqlLab/components/SaveQuery.jsx:102 +#: superset/templates/superset/request_access.html:16 +msgid "Cancel" +msgstr "" + +#: superset/assets/javascripts/SqlLab/components/SaveQuery.jsx:123 +msgid "Save Query" +msgstr "" + +#: superset/assets/javascripts/SqlLab/components/SouthPane.jsx:52 +msgid "Run a query to display results here" +msgstr "" + +#: superset/assets/javascripts/SqlLab/components/SouthPane.jsx:57 +#, python-format +msgid "Preview for %s" +msgstr "" + +#: superset/assets/javascripts/SqlLab/components/SouthPane.jsx:81 +msgid "Results" +msgstr "" + +#: superset/assets/javascripts/SqlLab/components/SouthPane.jsx:87 +msgid "Query History" +msgstr "" + +#: superset/assets/javascripts/SqlLab/components/SqlEditor.jsx:120 +msgid "Create table as with query results" +msgstr "" + +#: superset/assets/javascripts/SqlLab/components/SqlEditor.jsx:128 +msgid "new table name" +msgstr "" + +#: superset/assets/javascripts/SqlLab/components/SqlEditorLeftBar.jsx:90 +msgid "Error while fetching table list" +msgstr "" + +#: superset/assets/javascripts/SqlLab/components/SqlEditorLeftBar.jsx:131 +msgid "Error while fetching schema list" +msgstr "" + +#: superset/assets/javascripts/SqlLab/components/SqlEditorLeftBar.jsx:153 +msgid "Error while fetching database list" +msgstr "" + +#: superset/assets/javascripts/SqlLab/components/SqlEditorLeftBar.jsx:159 +msgid "Database:" +msgstr "" + +#: superset/assets/javascripts/SqlLab/components/SqlEditorLeftBar.jsx:163 +msgid "Select a database" +msgstr "" + +#: superset/assets/javascripts/SqlLab/components/SqlEditorLeftBar.jsx:170 +#, python-format +msgid "Select a schema (%s)" +msgstr "" + +#: superset/assets/javascripts/SqlLab/components/SqlEditorLeftBar.jsx:175 +msgid "Schema:" +msgstr "" + +#: superset/assets/javascripts/SqlLab/components/SqlEditorLeftBar.jsx:190 +#, python-format +msgid "Add a table (%s)" +msgstr "" + +#: superset/assets/javascripts/SqlLab/components/SqlEditorLeftBar.jsx:203 +msgid "Type to search ..." +msgstr "" + +#: superset/assets/javascripts/SqlLab/components/SqlEditorLeftBar.jsx:226 +msgid "Reset State" +msgstr "" + +#: superset/assets/javascripts/SqlLab/components/TabbedSqlEditors.jsx:105 +msgid "Enter a new title for the tab" +msgstr "" + +#: superset/assets/javascripts/SqlLab/components/TabbedSqlEditors.jsx:124 +#, python-format +msgid "Untitled Query %s" +msgstr "" + +#: superset/assets/javascripts/SqlLab/components/TabbedSqlEditors.jsx:170 +msgid "close tab" +msgstr "" + +#: superset/assets/javascripts/SqlLab/components/TabbedSqlEditors.jsx:173 +msgid "rename tab" +msgstr "" + +#: superset/assets/javascripts/SqlLab/components/TabbedSqlEditors.jsx:181 +msgid "expand tool bar" +msgstr "" + +#: superset/assets/javascripts/SqlLab/components/TabbedSqlEditors.jsx:181 +msgid "hide tool bar" +msgstr "" + +#: superset/assets/javascripts/SqlLab/components/TableElement.jsx:75 +msgid "Copy partition query to clipboard" +msgstr "" + +#: superset/assets/javascripts/SqlLab/components/TableElement.jsx:94 +msgid "latest partition:" +msgstr "" + +#: superset/assets/javascripts/SqlLab/components/TableElement.jsx:110 +msgid "Keys for table" +msgstr "" + +#: superset/assets/javascripts/SqlLab/components/TableElement.jsx:119 +#, python-format +msgid "View keys & indexes (%s)" +msgstr "" + +#: superset/assets/javascripts/SqlLab/components/TableElement.jsx:135 +msgid "Sort columns alphabetically" +msgstr "" + +#: superset/assets/javascripts/SqlLab/components/TableElement.jsx:136 +msgid "Original table column order" +msgstr "" + +#: superset/assets/javascripts/SqlLab/components/TableElement.jsx:146 +msgid "Copy SELECT statement to clipboard" +msgstr "" + +#: superset/assets/javascripts/SqlLab/components/TableElement.jsx:152 +msgid "Remove table preview" +msgstr "" + +#: superset/assets/javascripts/SqlLab/components/VisualizeModal.jsx:90 +#, python-format +msgid "%s is not right as a column name, please alias it (as in SELECT count(*) " +msgstr "" + +#: superset/assets/javascripts/SqlLab/components/VisualizeModal.jsx:91 +msgid "AS my_alias" +msgstr "" + +#: superset/assets/javascripts/SqlLab/components/VisualizeModal.jsx:91 +msgid "using only alphanumeric characters and underscores" +msgstr "" + +#: superset/assets/javascripts/SqlLab/components/VisualizeModal.jsx:166 +msgid "Creating a data source and popping a new tab" +msgstr "" + +#: superset/assets/javascripts/SqlLab/components/VisualizeModal.jsx:196 +msgid "No results available for this query" +msgstr "" + +#: superset/assets/javascripts/SqlLab/components/VisualizeModal.jsx:248 +msgid "Chart Type" +msgstr "" + +#: superset/assets/javascripts/SqlLab/components/VisualizeModal.jsx:251 +msgid "[Chart Type]" +msgstr "" + +#: superset/assets/javascripts/SqlLab/components/VisualizeModal.jsx:259 +msgid "Datasource Name" +msgstr "" + +#: superset/assets/javascripts/SqlLab/components/VisualizeModal.jsx:263 +msgid "datasource name" +msgstr "" + +#: superset/assets/javascripts/components/AsyncSelect.jsx:20 +msgid "Select ..." +msgstr "" + +#: superset/assets/javascripts/components/CachedLabel.jsx:26 +msgid "Loaded data cached" +msgstr "" + +#: superset/assets/javascripts/components/CachedLabel.jsx:28 +msgid "Loaded from cache" +msgstr "" + +#: superset/assets/javascripts/components/CachedLabel.jsx:33 +msgid "Click to force-refresh" +msgstr "" + +#: superset/assets/javascripts/components/CopyToClipboard.jsx:21 +#: superset/assets/javascripts/explore/components/EmbedCodeButton.jsx:67 +#: superset/assets/javascripts/explore/components/URLShortLinkButton.jsx:37 +msgid "Copy to clipboard" +msgstr "" + +#: superset/assets/javascripts/components/CopyToClipboard.jsx:65 +msgid "Not successful" +msgstr "" + +#: superset/assets/javascripts/components/CopyToClipboard.jsx:68 +msgid "Sorry, your browser does not support copying. Use Ctrl / Cmd + C!" +msgstr "" + +#: superset/assets/javascripts/components/CopyToClipboard.jsx:79 +msgid "Copied!" +msgstr "" + +#: superset/assets/javascripts/components/EditableTitle.jsx:12 +#: superset/views/core.py:471 superset/views/core.py:538 +msgid "Title" +msgstr "" + +#: superset/assets/javascripts/components/EditableTitle.jsx:75 +msgid "click to edit title" +msgstr "" + +#: superset/assets/javascripts/components/EditableTitle.jsx:75 +msgid "You don't have the rights to alter this title." +msgstr "" + +#: superset/assets/javascripts/components/FaveStar.jsx:32 +#: superset/assets/javascripts/modules/superset.js:33 +msgid "Click to favorite/unfavorite" +msgstr "" + +#: superset/assets/javascripts/dashboard/Dashboard.jsx:42 +#: superset/assets/javascripts/dashboard/Dashboard.jsx:59 +msgid "You have unsaved changes." +msgstr "" + +#: superset/assets/javascripts/dashboard/Dashboard.jsx:59 +msgid "Click the" +msgstr "" + +#: superset/assets/javascripts/dashboard/Dashboard.jsx:61 +msgid "button on the top right to save your changes." +msgstr "" + +#: superset/assets/javascripts/dashboard/Dashboard.jsx:164 +#, python-format +msgid "Served from data cached %s . Click to force refresh." +msgstr "" + +#: superset/assets/javascripts/dashboard/Dashboard.jsx:169 +msgid "Click to force refresh" +msgstr "" + +#: superset/assets/javascripts/dashboard/Dashboard.jsx:353 +#: superset/assets/javascripts/dashboard/components/SaveModal.jsx:100 +msgid "Error" +msgstr "" + +#: superset/assets/javascripts/dashboard/Dashboard.jsx:354 +#, python-format +msgid "Sorry, there was an error adding slices to this dashboard: %s" +msgstr "" + +#: superset/assets/javascripts/dashboard/components/CodeModal.jsx:35 +msgid "Active Dashboard Filters" +msgstr "" + +#: superset/assets/javascripts/dashboard/components/Controls.jsx:48 +#, python-format +msgid "Checkout this dashboard: %s" +msgstr "" + +#: superset/assets/javascripts/dashboard/components/Controls.jsx:54 +msgid "Force refresh the whole dashboard" +msgstr "" + +#: superset/assets/javascripts/dashboard/components/Controls.jsx:94 +msgid "Edit this dashboard's properties" +msgstr "" + +#: superset/assets/javascripts/dashboard/components/CssEditor.jsx:65 +msgid "Load a template" +msgstr "" + +#: superset/assets/javascripts/dashboard/components/CssEditor.jsx:68 +msgid "Load a CSS template" +msgstr "" + +#: superset/assets/javascripts/dashboard/components/CssEditor.jsx:80 +#: superset/views/core.py:478 +msgid "CSS" +msgstr "" + +#: superset/assets/javascripts/dashboard/components/CssEditor.jsx:86 +msgid "Live CSS Editor" +msgstr "" + +#: superset/assets/javascripts/dashboard/components/RefreshIntervalModal.jsx:19 +msgid "Don't refresh" +msgstr "" + +#: superset/assets/javascripts/dashboard/components/RefreshIntervalModal.jsx:20 +msgid "10 seconds" +msgstr "" + +#: superset/assets/javascripts/dashboard/components/RefreshIntervalModal.jsx:21 +msgid "30 seconds" +msgstr "" + +#: superset/assets/javascripts/dashboard/components/RefreshIntervalModal.jsx:22 +msgid "1 minute" +msgstr "" + +#: superset/assets/javascripts/dashboard/components/RefreshIntervalModal.jsx:23 +msgid "5 minutes" +msgstr "" + +#: superset/assets/javascripts/dashboard/components/RefreshIntervalModal.jsx:38 +msgid "Refresh Interval" +msgstr "" + +#: superset/assets/javascripts/dashboard/components/RefreshIntervalModal.jsx:41 +msgid "Choose the refresh frequency for this dashboard" +msgstr "" + +#: superset/assets/javascripts/dashboard/components/SaveModal.jsx:63 +msgid "This dashboard was saved successfully." +msgstr "" + +#: superset/assets/javascripts/dashboard/components/SaveModal.jsx:69 +msgid "Sorry, there was an error saving this dashboard: " +msgstr "" + +#: superset/assets/javascripts/dashboard/components/SaveModal.jsx:101 +msgid "You must pick a name for the new dashboard" +msgstr "" + +#: superset/assets/javascripts/dashboard/components/SaveModal.jsx:115 +msgid "Save Dashboard" +msgstr "" + +#: superset/assets/javascripts/dashboard/components/SaveModal.jsx:123 +#, python-format +msgid "Overwrite Dashboard [%s]" +msgstr "" + +#: superset/assets/javascripts/dashboard/components/SaveModal.jsx:131 +msgid "Save as:" +msgstr "" + +#: superset/assets/javascripts/dashboard/components/SaveModal.jsx:135 +#: superset/assets/javascripts/explore/components/SaveModal.jsx:205 +msgid "[dashboard name]" +msgstr "" + +#: superset/assets/javascripts/dashboard/components/SliceAdder.jsx:142 +#: superset/views/core.py:375 +msgid "Name" +msgstr "" + +#: superset/assets/javascripts/dashboard/components/SliceAdder.jsx:148 +msgid "Viz" +msgstr "" + +#: superset/assets/javascripts/dashboard/components/SliceAdder.jsx:157 +#: superset/views/core.py:476 superset/views/core.py:540 +#: superset/views/sql_lab.py:57 +msgid "Modified" +msgstr "" + +#: superset/assets/javascripts/dashboard/components/SliceAdder.jsx:167 +msgid "Add Slices" +msgstr "" + +#: superset/assets/javascripts/dashboard/components/SliceAdder.jsx:176 +msgid "Add a new slice to the dashboard" +msgstr "" + +#: superset/assets/javascripts/dashboard/components/SliceAdder.jsx:181 +msgid "Add Slices to Dashboard" +msgstr "" + +#: superset/assets/javascripts/dashboard/components/SliceCell.jsx:24 +msgid "Move chart" +msgstr "" + +#: superset/assets/javascripts/dashboard/components/SliceCell.jsx:27 +msgid "Force refresh data" +msgstr "" + +#: superset/assets/javascripts/dashboard/components/SliceCell.jsx:31 +msgid "Toggle chart description" +msgstr "" + +#: superset/assets/javascripts/dashboard/components/SliceCell.jsx:41 +msgid "Edit chart" +msgstr "" + +#: superset/assets/javascripts/dashboard/components/SliceCell.jsx:46 +msgid "Export CSV" +msgstr "" + +#: superset/assets/javascripts/dashboard/components/SliceCell.jsx:49 +msgid "Explore chart" +msgstr "" + +#: superset/assets/javascripts/dashboard/components/SliceCell.jsx:54 +msgid "Remove chart from dashboard" +msgstr "" + +#: superset/assets/javascripts/explore/components/ChartContainer.jsx:173 +#, python-format +msgid "%s - untitled" +msgstr "" + +#: superset/assets/javascripts/explore/components/ChartContainer.jsx:280 +msgid "Edit slice properties" +msgstr "" + +#: superset/assets/javascripts/explore/components/ControlHeader.jsx:32 +msgid "description" +msgstr "" + +#: superset/assets/javascripts/explore/components/ControlHeader.jsx:42 +msgid "bolt" +msgstr "" + +#: superset/assets/javascripts/explore/components/DisplayQueryButton.jsx:61 +msgid "Error..." +msgstr "" + +#: superset/assets/javascripts/explore/components/DisplayQueryButton.jsx:97 +#: superset/assets/javascripts/explore/stores/visTypes.js:49 +#: superset/assets/javascripts/explore/stores/visTypes.js:129 +#: superset/assets/javascripts/explore/stores/visTypes.js:375 +#: superset/assets/javascripts/explore/stores/visTypes.js:422 +#: superset/assets/javascripts/explore/stores/visTypes.js:443 +#: superset/assets/javascripts/explore/stores/visTypes.js:471 +#: superset/assets/javascripts/explore/stores/visTypes.js:491 +#: superset/assets/javascripts/explore/stores/visTypes.js:512 +#: superset/assets/javascripts/explore/stores/visTypes.js:564 +#: superset/assets/javascripts/explore/stores/visTypes.js:586 +#: superset/assets/javascripts/explore/stores/visTypes.js:611 +#: superset/assets/javascripts/explore/stores/visTypes.js:636 +#: superset/assets/javascripts/explore/stores/visTypes.js:668 +#: superset/assets/javascripts/explore/stores/visTypes.js:705 +#: superset/assets/javascripts/explore/stores/visTypes.js:732 +#: superset/assets/javascripts/explore/stores/visTypes.js:759 +#: superset/assets/javascripts/explore/stores/visTypes.js:797 +#: superset/assets/javascripts/explore/stores/visTypes.js:830 +#: superset/assets/javascripts/explore/stores/visTypes.js:867 +#: superset/assets/javascripts/explore/stores/visTypes.js:910 +#: superset/assets/javascripts/explore/stores/visTypes.js:977 +msgid "Query" +msgstr "" + +#: superset/assets/javascripts/explore/components/EmbedCodeButton.jsx:76 +msgid "Height" +msgstr "" + +#: superset/assets/javascripts/explore/components/EmbedCodeButton.jsx:90 +msgid "Width" +msgstr "" + +#: superset/assets/javascripts/explore/components/ExploreActionButtons.jsx:32 +msgid "Export to .json" +msgstr "" + +#: superset/assets/javascripts/explore/components/ExploreActionButtons.jsx:42 +msgid "Export to .csv format" +msgstr "" + +#: superset/assets/javascripts/explore/components/SaveModal.jsx:74 +msgid "Please enter a slice name" +msgstr "" + +#: superset/assets/javascripts/explore/components/SaveModal.jsx:89 +msgid "Please select a dashboard" +msgstr "" + +#: superset/assets/javascripts/explore/components/SaveModal.jsx:97 +msgid "Please enter a dashboard name" +msgstr "" + +#: superset/assets/javascripts/explore/components/SaveModal.jsx:130 +msgid "Save A Slice" +msgstr "" + +#: superset/assets/javascripts/explore/components/SaveModal.jsx:151 +#, python-format +msgid "Overwrite slice %s" +msgstr "" + +#: superset/assets/javascripts/explore/components/SaveModal.jsx:160 +msgid "Save as" +msgstr "" + +#: superset/assets/javascripts/explore/components/SaveModal.jsx:164 +msgid "[slice name]" +msgstr "" + +#: superset/assets/javascripts/explore/components/SaveModal.jsx:177 +msgid "Do not add to a dashboard" +msgstr "" + +#: superset/assets/javascripts/explore/components/SaveModal.jsx:185 +msgid "Add slice to existing dashboard" +msgstr "" + +#: superset/assets/javascripts/explore/components/SaveModal.jsx:200 +msgid "Add to new dashboard" +msgstr "" + +#: superset/assets/javascripts/explore/components/SaveModal.jsx:226 +msgid "Save & go to dashboard" +msgstr "" + +#: superset/assets/javascripts/explore/components/URLShortLinkButton.jsx:32 +#, python-format +msgid "Check out this slice: %s" +msgstr "" + +#: superset/assets/javascripts/explore/components/controls/BoundsControl.jsx:55 +msgid "`Min` value should be numeric or empty" +msgstr "" + +#: superset/assets/javascripts/explore/components/controls/BoundsControl.jsx:58 +msgid "`Max` value should be numeric or empty" +msgstr "" + +#: superset/assets/javascripts/explore/components/controls/BoundsControl.jsx:75 +#: superset/connectors/druid/views.py:50 superset/connectors/sqla/views.py:89 +msgid "Min" +msgstr "" + +#: superset/assets/javascripts/explore/components/controls/BoundsControl.jsx:83 +#: superset/connectors/druid/views.py:51 superset/connectors/sqla/views.py:90 +msgid "Max" +msgstr "" + +#: superset/assets/javascripts/explore/components/controls/DatasourceControl.jsx:70 +msgid "Something went wrong while fetching the datasource list" +msgstr "" + +#: superset/assets/javascripts/explore/components/controls/DatasourceControl.jsx:95 +msgid "Click to point to another datasource" +msgstr "" + +#: superset/assets/javascripts/explore/components/controls/DatasourceControl.jsx:106 +msgid "Edit the datasource's configuration" +msgstr "" + +#: superset/assets/javascripts/explore/components/controls/DatasourceControl.jsx:122 +msgid "Select a datasource" +msgstr "" + +#: superset/assets/javascripts/explore/components/controls/DatasourceControl.jsx:132 +#: superset/assets/javascripts/explore/components/controls/VizTypeControl.jsx:120 +msgid "Search / Filter" +msgstr "" + +#: superset/assets/javascripts/explore/components/controls/Filter.jsx:118 +msgid "Filter value" +msgstr "" + +#: superset/assets/javascripts/explore/components/controls/Filter.jsx:147 +msgid "Select metric" +msgstr "" + +#: superset/assets/javascripts/explore/components/controls/Filter.jsx:147 +msgid "Select column" +msgstr "" + +#: superset/assets/javascripts/explore/components/controls/Filter.jsx:159 +msgid "Select operator" +msgstr "" + +#: superset/assets/javascripts/explore/components/controls/FilterControl.jsx:70 +#: superset/templates/appbuilder/general/widgets/search.html:6 +msgid "Add Filter" +msgstr "" + +#: superset/assets/javascripts/explore/components/controls/SelectControl.jsx:106 +#, python-format +msgid "Select %s" +msgstr "" + +#: superset/assets/javascripts/explore/components/controls/TextAreaControl.jsx:63 +msgid "textarea" +msgstr "" + +#: superset/assets/javascripts/explore/components/controls/TextAreaControl.jsx:81 +msgid "Edit" +msgstr "" + +#: superset/assets/javascripts/explore/components/controls/TextAreaControl.jsx:81 +msgid "in modal" +msgstr "" + +#: superset/assets/javascripts/explore/components/controls/VizTypeControl.jsx:110 +msgid "Select a visualization type" +msgstr "" + +#: superset/assets/javascripts/explore/reducers/chartReducer.js:32 +msgid "Updating chart was stopped" +msgstr "" + +#: superset/assets/javascripts/explore/reducers/chartReducer.js:38 +#: superset/assets/javascripts/modules/superset.js:222 +#, python-format +msgid "An error occurred while rendering the visualization: %s" +msgstr "" + +#: superset/assets/javascripts/explore/reducers/chartReducer.js:47 +msgid "" +"Perhaps your data has grown, your database is under unusual load, or you " +"are simply querying a data source that is to large to be processed within" +" the timeout range. If that is the case, we recommend that you summarize " +"your data further." +msgstr "" + +#: superset/assets/javascripts/explore/reducers/chartReducer.js:56 +msgid "Network error." +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:36 +msgid "A reference to the [Time] configuration, taking granularity into account" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:44 +msgid "Group by" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:47 +msgid "One or many controls to group by" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:66 +#: superset/connectors/druid/views.py:45 superset/views/core.py:314 +#: superset/views/core.py:338 superset/views/core.py:369 +msgid "Datasource" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:76 +#: superset/views/core.py:377 +msgid "Visualization Type" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:78 +msgid "The type of visualization to display" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:84 +msgid "Metrics" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:93 +#: superset/assets/javascripts/explore/stores/controls.jsx:110 +msgid "One or many metrics to display" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:97 +msgid "Y Axis Bounds" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:100 +msgid "" +"Bounds for the Y axis. When left empty, the bounds are dynamically " +"defined based on the min/max of the data. Note that this feature will " +"only expand the axis range. It won't narrow the data's extent." +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:108 +msgid "Ordering" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:118 +#: superset/assets/javascripts/explore/stores/visTypes.js:818 +#: superset/connectors/druid/views.py:106 superset/connectors/sqla/views.py:131 +msgid "Metric" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:120 +msgid "Choose the metric" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:133 +msgid "Right Axis Metric" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:137 +msgid "Choose a metric for right axis" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:148 +msgid "Stacked Style" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:160 +msgid "Linear Color Scheme" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:177 +msgid "Normalize Across" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:184 +msgid "" +"Color will be rendered based on a ratio of the cell against the sum of " +"across this criteria" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:191 +msgid "Horizon Color Scale" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:198 +msgid "Defines how the color are attributed." +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:203 +msgid "Rendering" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:209 +msgid "" +"image-rendering CSS attribute of the canvas object that defines how the " +"browser scales up the image" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:215 +msgid "XScale Interval" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:218 +msgid "Number of steps to take between ticks when displaying the X scale" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:224 +msgid "YScale Interval" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:227 +msgid "Number of steps to take between ticks when displaying the Y scale" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:233 +msgid "Include Time" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:234 +msgid "Whether to include the time granularity as defined in the time section" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:240 +msgid "Stacked Bars" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:248 +msgid "Show totals" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:251 +msgid "Display total row/column" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:256 +msgid "Show Markers" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:259 +msgid "Show data points as circle markers on the lines" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:264 +msgid "Bar Values" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:267 +msgid "Show the value on top of the bar" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:272 +msgid "Sort Bars" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:274 +msgid "Sort bars by x labels." +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:279 +msgid "Combine Metrics" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:281 +msgid "" +"Display metrics side by side within each column, as opposed to each " +"column being displayed side by side for each metric." +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:287 +msgid "Extra Controls" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:290 +msgid "" +"Whether to show extra controls or not. Extra controls include things like" +" making mulitBar charts stacked or side by side." +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:297 +msgid "Reduce X ticks" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:300 +msgid "" +"Reduces the number of X axis ticks to be rendered. If true, the x axis " +"wont overflow and labels may be missing. If false, a minimum width will " +"be applied to columns and the width may overflow into an horizontal " +"scroll." +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:309 +msgid "Include Series" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:312 +msgid "Include series name as an axis" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:317 +msgid "Color Metric" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:319 +msgid "A metric to use for color" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:326 +msgid "Country Name" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:345 +msgid "The name of country that Superset should display" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:349 +msgid "Country Field Type" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:357 +msgid "" +"The country code standard that Superset should expect to find in the " +"[country] column" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:364 +#: superset/assets/javascripts/explore/stores/controls.jsx:371 +msgid "Columns" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:365 +msgid "One or many controls to pivot as columns" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:373 +#: superset/assets/javascripts/explore/stores/controls.jsx:383 +#: superset/assets/javascripts/explore/stores/controls.jsx:393 +msgid "Columns to display" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:402 +msgid "Origin" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:408 +msgid "" +"Defines the origin where time buckets start, accepts natural dates as in " +"`now`, `sunday` or `1970-01-01`" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:415 +msgid "Bottom Margin" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:419 +msgid "Bottom margin, in pixels, allowing for more room for axis labels" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:425 +msgid "Left Margin" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:429 +msgid "Left margin, in pixels, allowing for more room for axis labels" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:435 +msgid "Time Granularity" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:452 +msgid "" +"The time granularity for the visualization. Note that you can type and " +"use simple natural language as in `10 seconds`, `1 day` or `56 weeks`" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:459 +msgid "Domain" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:462 +msgid "The time unit used for the grouping of blocks" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:467 +msgid "Subdomain" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:470 +msgid "" +"The time unit for each block. Should be a smaller unit than " +"domain_granularity. Should be larger or equal to Time Grain" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:477 +msgid "Link Length" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:480 +msgid "Link length in the force layout" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:486 +msgid "Charge" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:500 +msgid "Charge in the force layout" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:508 +msgid "" +"The time column for the visualization. Note that you can define arbitrary" +" expression that return a DATETIME column in the table or. Also note that" +" the filter below is applied against this column or expression" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:520 +msgid "Time Grain" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:522 +msgid "" +"The time granularity for the visualization. This applies a date " +"transformation to alter your time column and defines a new time " +"granularity. The options here are defined on a per database engine basis " +"in the Superset source code." +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:535 +msgid "Resample Rule" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:538 +msgid "Pandas resample rule" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:544 +msgid "Resample How" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:547 +msgid "Pandas resample how" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:553 +msgid "Resample Fill Method" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:556 +msgid "Pandas resample fill method" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:562 +msgid "Since" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:563 +msgid "7 days ago" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:569 +msgid "Until" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:576 +msgid "Max Bubble Size" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:584 +msgid "Whisker/outlier options" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:586 +msgid "Determines how whiskers and outliers are calculated." +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:597 +msgid "Ratio" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:600 +msgid "Target aspect ratio for treemap tiles." +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:606 +#: superset/assets/javascripts/explore/stores/visTypes.js:602 +#: superset/assets/javascripts/explore/stores/visTypes.js:627 +#: superset/assets/javascripts/explore/stores/visTypes.js:776 +msgid "Number format" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:616 +msgid "Row limit" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:624 +msgid "Series limit" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:627 +msgid "Limits the number of time series that get displayed" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:632 +msgid "Sort By" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:634 +msgid "Metric used to define the top series" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:642 +msgid "Rolling" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:645 +msgid "" +"Defines a rolling window function to apply, works along with the " +"[Periods] text box" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:651 +msgid "Periods" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:653 +msgid "" +"Defines the size of the rolling window function, relative to the time " +"granularity selected" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:659 +msgid "Min Periods" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:661 +msgid "" +"The minimum number of rolling periods required to show a value. For " +"instance if you do a cumulative sum on 7 days you may want your \"Min " +"Period\" to be 7, so that all data points shown are the total of 7 " +"periods. This will hide the \"ramp up\" taking place over the first 7 " +"periods" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:670 +#: superset/assets/javascripts/explore/stores/visTypes.js:115 +msgid "Series" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:672 +msgid "" +"Defines the grouping of entities. Each series is shown as a specific " +"color on the chart and has a legend toggle" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:682 +msgid "Entity" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:685 +msgid "This defines the element to be plotted on the chart" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:693 +#: superset/assets/javascripts/explore/stores/visTypes.js:164 +#: superset/assets/javascripts/explore/stores/visTypes.js:533 +msgid "X Axis" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:694 +msgid "Metric assigned to the [X] axis" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:707 +#: superset/assets/javascripts/explore/stores/visTypes.js:171 +#: superset/assets/javascripts/explore/stores/visTypes.js:541 +msgid "Y Axis" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:710 +msgid "Metric assigned to the [Y] axis" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:721 +msgid "Bubble Size" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:734 +msgid "URL" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:735 +msgid "" +"The URL, this control is templated, so you can integrate {{ width }} " +"and/or {{ height }} in your URL string." +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:742 +msgid "X Axis Label" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:749 +msgid "Y Axis Label" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:756 +msgid "Custom WHERE clause" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:758 +msgid "" +"The text in this box gets included in your query's WHERE clause, as an " +"AND to other criteria. You can include complex expression, parenthesis " +"and anything else supported by the backend it is directed towards." +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:766 +msgid "Custom HAVING clause" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:768 +msgid "" +"The text in this box gets included in your query's HAVING clause, as an " +"AND to other criteria. You can include complex expression, parenthesis " +"and anything else supported by the backend it is directed towards." +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:776 +msgid "Comparison Period Lag" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:778 +msgid "Based on granularity, number of time periods to compare against" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:783 +msgid "Comparison suffix" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:784 +msgid "Suffix to apply after the percentage display" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:790 +msgid "Table Timestamp Format" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:795 +msgid "Timestamp Format" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:801 +msgid "Series Height" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:804 +msgid "Pixel height of each series" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:810 +msgid "Page Length" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:813 +msgid "Rows per page, 0 means no pagination" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:819 +#: superset/assets/javascripts/explore/stores/controls.jsx:829 +msgid "X Axis Format" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:839 +msgid "Y Axis Format" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:849 +msgid "Right Axis Format" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:857 +msgid "Markup Type" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:862 +msgid "Pick your favorite markup language" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:867 +msgid "Rotation" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:870 +msgid "Rotation to apply to words in the cloud" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:875 +msgid "Line Style" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:880 +msgid "Line interpolation as defined by d3.js" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:885 +msgid "Label Type" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:892 +msgid "What should be shown on the label?" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:897 +#: superset/assets/javascripts/explore/stores/visTypes.js:362 +#: superset/assets/javascripts/explore/stores/visTypes.js:400 +msgid "Code" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:898 +msgid "Put your code here" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:907 +msgid "Aggregation function" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:919 +msgid "" +"Aggregate function to apply when pivoting and computing the total rows " +"and columns" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:926 +msgid "Font Size From" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:928 +msgid "Font size for the smallest value in the list" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:934 +msgid "Font Size To" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:936 +msgid "Font size for the biggest value in the list" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:941 +msgid "Instant Filtering" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:952 +msgid "Range Filter" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:955 +msgid "Whether to display the time range interactive selector" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:960 +msgid "Date Filter" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:962 +msgid "Whether to include a time filter" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:967 +msgid "Data Table" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:969 +msgid "Whether to display the interactive data table" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:974 +msgid "Search Box" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:977 +msgid "Whether to include a client side search box" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:982 +msgid "Table Filter" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:984 +msgid "Whether to apply filter when table cell is clicked" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:989 +msgid "Show Bubbles" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:992 +msgid "Whether to display bubbles on top of countries" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:997 +msgid "Legend" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:1000 +msgid "Whether to display the legend (toggles)" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:1005 +msgid "X bounds" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:1008 +msgid "Whether to display the min and max values of the X axis" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:1013 +msgid "Y bounds" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:1016 +msgid "Whether to display the min and max values of the Y axis" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:1021 +msgid "Rich Tooltip" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:1024 +msgid "The rich tooltip shows a list of all series for that point in time" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:1030 +msgid "Y Log Scale" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:1033 +msgid "Use a log scale for the Y axis" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:1038 +msgid "X Log Scale" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:1041 +msgid "Use a log scale for the X axis" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:1046 +msgid "Donut" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:1049 +msgid "Do you want a donut or a pie?" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:1054 +msgid "Put labels outside" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:1057 +msgid "Put the labels outside the pie?" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:1062 +msgid "Contribution" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:1064 +msgid "Compute the contribution to the total" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:1069 +msgid "Period Ratio" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:1072 +msgid "" +"[integer] Number of period to compare against, this is relative to the " +"granularity selected" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:1078 +msgid "Period Ratio Type" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:1081 +msgid "" +"`factor` means (new/previous), `growth` is ((new/previous) - 1), `value` " +"is (new-previous)" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:1087 +msgid "Time Shift" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:1089 +msgid "" +"Overlay a timeseries from a relative time period. Expects relative time " +"delta in natural language (example: 24 hours, 7 days, 56 weeks, 365 " +"days)" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:1097 +msgid "Subheader" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:1098 +msgid "Description text that shows up below your Big Number" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:1104 +msgid "label" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:1106 +msgid "" +"`count` is COUNT(*) if a group by is used. Numerical columns will be " +"aggregated with the aggregator. Non-numerical columns will be used to " +"label points. Leave empty to get a count of points in each cluster." +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:1117 +msgid "Map Style" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:1127 +msgid "Base layer map style" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:1133 +msgid "Clustering Radius" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:1146 +msgid "" +"The radius (in pixels) the algorithm uses to define a cluster. Choose 0 " +"to turn off clustering, but beware that a large number of points (>1000) " +"will cause lag." +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:1153 +msgid "Point Radius" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:1155 +msgid "" +"The radius of individual points (ones that are not in a cluster). Either " +"a numerical column or `Auto`, which scales the point based on the largest" +" cluster" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:1165 +msgid "Point Radius Unit" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:1168 +msgid "The unit of measure for the specified point radius" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:1173 +msgid "Opacity" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:1176 +msgid "Opacity of all clusters, points, and labels. Between 0 and 1." +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:1182 +msgid "Zoom" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:1185 +msgid "Zoom level of the map" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:1191 +msgid "Default latitude" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:1194 +msgid "Latitude of default viewport" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:1200 +msgid "Default longitude" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:1203 +msgid "Longitude of default viewport" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:1209 +msgid "Live render" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:1211 +msgid "Points and clusters will update as viewport is being changed" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:1217 +msgid "RGB Color" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:1227 +msgid "The color for points and clusters in RGB" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:1232 +msgid "Ranges" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:1234 +msgid "Ranges to highlight with shading" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:1239 +msgid "Range labels" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:1241 +msgid "Labels for the ranges" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:1246 +msgid "Markers" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:1248 +msgid "List of values to mark with triangles" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:1253 +msgid "Marker labels" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:1255 +msgid "Labels for the markers" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:1260 +msgid "Marker lines" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:1262 +msgid "List of values to mark with lines" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:1267 +msgid "Marker line labels" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:1269 +msgid "Labels for the marker lines" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:1296 +msgid "Slice ID" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:1298 +msgid "The id of the active slice" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:1303 +msgid "Cache Timeout (seconds)" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:1305 +msgid "The number of seconds before expiring the cache" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:1310 +msgid "Order by entity id" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:1311 +msgid "" +"Important! Select this if the table is not already sorted by entity id, " +"else there is no guarantee that all events for each entity are returned." +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:1319 +msgid "Minimum leaf node event count" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:1322 +msgid "" +"Leaf nodes that represent fewer than this number of events will be " +"initially hidden in the visualization" +msgstr "" + +#: superset/assets/javascripts/explore/stores/controls.jsx:1328 +#: superset/assets/javascripts/explore/stores/visTypes.js:25 +msgid "Color Scheme" msgstr "" -#: superset/viz.py:398 -msgid "'Group By' and 'Columns' can't overlap" +#: superset/assets/javascripts/explore/stores/controls.jsx:1332 +msgid "The color scheme for rendering chart" msgstr "" -#: superset/viz.py:431 -msgid "Markup" +#: superset/assets/javascripts/explore/stores/visTypes.js:7 +#: superset/assets/javascripts/explore/stores/visTypes.js:31 +msgid "Time" msgstr "" -#: superset/viz.py:450 -msgid "Separator" +#: superset/assets/javascripts/explore/stores/visTypes.js:9 +#: superset/assets/javascripts/explore/stores/visTypes.js:32 +msgid "Time related form attributes" msgstr "" -#: superset/viz.py:466 -msgid "Word Cloud" +#: superset/assets/javascripts/explore/stores/visTypes.js:16 +msgid "Datasource & Chart Type" msgstr "" -#: superset/viz.py:489 -msgid "Treemap" +#: superset/assets/javascripts/explore/stores/visTypes.js:45 +msgid "This section exposes ways to include snippets of SQL in your query" msgstr "" -#: superset/viz.py:515 -msgid "Calendar Heatmap" +#: superset/assets/javascripts/explore/stores/visTypes.js:58 +msgid "Advanced Analytics" msgstr "" -#: superset/viz.py:573 -msgid "Box Plot" +#: superset/assets/javascripts/explore/stores/visTypes.js:59 +msgid "" +"This section contains options that allow for advanced analytical post " +"processing of query results" msgstr "" -#: superset/viz.py:662 -msgid "Bubble Chart" +#: superset/assets/javascripts/explore/stores/visTypes.js:77 +msgid "Result Filters" msgstr "" -#: superset/viz.py:686 -msgid "Pick a metric for x, y and size" +#: superset/assets/javascripts/explore/stores/visTypes.js:79 +msgid "" +"The filters to apply after post-aggregation.Leave the value control empty" +" to filter empty strings or nulls" msgstr "" -#: superset/viz.py:712 -msgid "Bullet Chart" +#: superset/assets/javascripts/explore/stores/visTypes.js:92 +#: superset/assets/javascripts/explore/stores/visTypes.js:101 +#: superset/assets/javascripts/explore/stores/visTypes.js:137 +#: superset/assets/javascripts/explore/stores/visTypes.js:155 +#: superset/assets/javascripts/explore/stores/visTypes.js:193 +#: superset/assets/javascripts/explore/stores/visTypes.js:234 +#: superset/assets/javascripts/explore/stores/visTypes.js:268 +#: superset/assets/javascripts/explore/stores/visTypes.js:290 +#: superset/assets/javascripts/explore/stores/visTypes.js:451 +#: superset/assets/javascripts/explore/stores/visTypes.js:499 +#: superset/assets/javascripts/explore/stores/visTypes.js:520 +#: superset/assets/javascripts/explore/stores/visTypes.js:644 +#: superset/assets/javascripts/explore/stores/visTypes.js:677 +#: superset/assets/javascripts/explore/stores/visTypes.js:714 +#: superset/assets/javascripts/explore/stores/visTypes.js:767 +#: superset/assets/javascripts/explore/stores/visTypes.js:965 +msgid "Chart Options" msgstr "" -#: superset/viz.py:738 -msgid "Pick a metric to display" +#: superset/assets/javascripts/explore/stores/visTypes.js:118 +msgid "Breakdowns" msgstr "" -#: superset/viz.py:761 -msgid "Big Number with Trendline" +#: superset/assets/javascripts/explore/stores/visTypes.js:119 +msgid "Defines how each series is broken down" msgstr "" -#: superset/viz.py:769 superset/viz.py:798 -msgid "Pick a metric!" +#: superset/assets/javascripts/explore/stores/visTypes.js:125 +msgid "Pie Chart" msgstr "" -#: superset/viz.py:790 -msgid "Big Number" +#: superset/assets/javascripts/explore/stores/visTypes.js:189 +msgid "Dual Axis Line Chart" msgstr "" -#: superset/viz.py:817 -msgid "Time Series - Line Chart" +#: superset/assets/javascripts/explore/stores/visTypes.js:200 +msgid "Y Axis 1" msgstr "" -#: superset/viz.py:864 superset/viz.py:1001 -msgid "Pick a time granularity for your time series" +#: superset/assets/javascripts/explore/stores/visTypes.js:206 +msgid "Y Axis 2" msgstr "" -#: superset/viz.py:944 -msgid "Time Series - Dual Axis Line Chart" +#: superset/assets/javascripts/explore/stores/visTypes.js:214 +msgid "Left Axis Metric" msgstr "" -#: superset/viz.py:954 -msgid "Pick a metric for left axis!" +#: superset/assets/javascripts/explore/stores/visTypes.js:215 +msgid "Choose a metric for left axis" msgstr "" -#: superset/viz.py:956 -msgid "Pick a metric for right axis!" +#: superset/assets/javascripts/explore/stores/visTypes.js:218 +msgid "Left Axis Format" msgstr "" -#: superset/viz.py:958 -msgid "Please choose different metrics on left and right axis" +#: superset/assets/javascripts/explore/stores/visTypes.js:244 +#: superset/assets/javascripts/explore/stores/visTypes.js:300 +msgid "Axes" msgstr "" -#: superset/viz.py:1019 -msgid "Time Series - Bar Chart" +#: superset/assets/javascripts/explore/stores/visTypes.js:324 +msgid "GROUP BY" msgstr "" -#: superset/viz.py:1027 -msgid "Time Series - Percent Change" +#: superset/assets/javascripts/explore/stores/visTypes.js:325 +msgid "Use this section if you want a query that aggregates" msgstr "" -#: superset/viz.py:1035 -msgid "Time Series - Stacked" +#: superset/assets/javascripts/explore/stores/visTypes.js:332 +msgid "NOT GROUPED BY" msgstr "" -#: superset/viz.py:1044 -msgid "Distribution - NVD3 - Pie Chart" +#: superset/assets/javascripts/explore/stores/visTypes.js:333 +msgid "Use this section if you want to query atomic rows" msgstr "" -#: superset/viz.py:1062 -msgid "Histogram" +#: superset/assets/javascripts/explore/stores/visTypes.js:340 +#: superset/assets/javascripts/explore/stores/visTypes.js:741 +#: superset/assets/javascripts/explore/stores/visTypes.js:805 +#: superset/assets/javascripts/explore/stores/visTypes.js:898 +msgid "Options" msgstr "" -#: superset/viz.py:1072 -msgid "Must have one numeric column specified" +#: superset/assets/javascripts/explore/stores/visTypes.js:527 +#: superset/assets/javascripts/explore/stores/visTypes.js:839 +msgid "Bubbles" msgstr "" -#: superset/viz.py:1087 -msgid "Distribution - Bar Chart" +#: superset/assets/javascripts/explore/stores/visTypes.js:653 +msgid "Numeric Column" msgstr "" -#: superset/viz.py:1098 -msgid "Can't have overlap between Series and Breakdowns" +#: superset/assets/javascripts/explore/stores/visTypes.js:654 +msgid "Select the numeric column to draw the histogram" msgstr "" -#: superset/viz.py:1100 -msgid "Pick at least one metric" +#: superset/assets/javascripts/explore/stores/visTypes.js:657 +msgid "No of Bins" msgstr "" -#: superset/viz.py:1102 -msgid "Pick at least one field for [Series]" +#: superset/assets/javascripts/explore/stores/visTypes.js:658 +msgid "Select number of bins for the histogram" msgstr "" -#: superset/viz.py:1155 -msgid "Sunburst" +#: superset/assets/javascripts/explore/stores/visTypes.js:685 +msgid "Primary Metric" msgstr "" -#: superset/viz.py:1188 -msgid "Sankey" +#: superset/assets/javascripts/explore/stores/visTypes.js:686 +msgid "The primary metric is used to define the arc segment sizes" msgstr "" -#: superset/viz.py:1195 -msgid "Pick exactly 2 columns as [Source / Target]" +#: superset/assets/javascripts/explore/stores/visTypes.js:689 +msgid "Secondary Metric" msgstr "" -#: superset/viz.py:1226 +#: superset/assets/javascripts/explore/stores/visTypes.js:690 msgid "" -"There's a loop in your Sankey, please provide a tree. Here's a faulty " -"link: {}" +"This secondary metric is used to define the color as a ratio against the " +"primary metric. If the two metrics match, color is mapped level groups" msgstr "" -#: superset/viz.py:1237 superset/viz.py:1258 -msgid "Directed Force Layout" +#: superset/assets/javascripts/explore/stores/visTypes.js:695 +msgid "Hierarchy" msgstr "" -#: superset/viz.py:1244 -msgid "Pick exactly 2 columns to 'Group By'" +#: superset/assets/javascripts/explore/stores/visTypes.js:696 +msgid "This defines the level of the hierarchy" msgstr "" -#: superset/viz.py:1291 -msgid "Country Map" +#: superset/assets/javascripts/explore/stores/visTypes.js:722 +#: superset/assets/javascripts/explore/stores/visTypes.js:750 +msgid "Source / Target" msgstr "" -#: superset/viz.py:1320 -msgid "World Map" +#: superset/assets/javascripts/explore/stores/visTypes.js:723 +#: superset/assets/javascripts/explore/stores/visTypes.js:751 +msgid "Choose a source and a target" msgstr "" -#: superset/viz.py:1370 -msgid "Filters" +#: superset/assets/javascripts/explore/stores/visTypes.js:756 +msgid "Chord Diagram" msgstr "" -#: superset/viz.py:1378 -msgid "Pick at least one filter field" +#: superset/assets/javascripts/explore/stores/visTypes.js:777 +msgid "Choose a number format" msgstr "" -#: superset/viz.py:1405 -msgid "iFrame" +#: superset/assets/javascripts/explore/stores/visTypes.js:780 +msgid "Source" msgstr "" -#: superset/viz.py:1422 -msgid "Parallel Coordinates" +#: superset/assets/javascripts/explore/stores/visTypes.js:783 +msgid "Choose a source" msgstr "" -#: superset/viz.py:1447 -msgid "Heatmap" +#: superset/assets/javascripts/explore/stores/visTypes.js:786 +msgid "Target" msgstr "" -#: superset/viz.py:1498 -msgid "Horizon Charts" +#: superset/assets/javascripts/explore/stores/visTypes.js:789 +msgid "Choose a target" msgstr "" -#: superset/viz.py:1509 -msgid "Mapbox" +#: superset/assets/javascripts/explore/stores/visTypes.js:814 +msgid "ISO 3166-1 codes of region/province/department" msgstr "" -#: superset/viz.py:1524 -msgid "Must have a [Group By] column to have 'count' as the [Label]" +#: superset/assets/javascripts/explore/stores/visTypes.js:815 +msgid "" +"It's ISO 3166-1 of your region/province/department in your table. (see " +"documentation for list of ISO 3166-1)" msgstr "" -#: superset/viz.py:1537 -msgid "Choice of [Label] must be present in [Group By]" +#: superset/assets/javascripts/explore/stores/visTypes.js:849 +msgid "Country Control" msgstr "" -#: superset/viz.py:1542 -msgid "Choice of [Point Radius] must be present in [Group By]" +#: superset/assets/javascripts/explore/stores/visTypes.js:850 +msgid "3 letter code of the country" msgstr "" -#: superset/viz.py:1547 -msgid "[Longitude] and [Latitude] columns must be present in [Group By]" +#: superset/assets/javascripts/explore/stores/visTypes.js:853 +msgid "Metric for color" msgstr "" -#: superset/viz.py:1612 -msgid "Event flow" +#: superset/assets/javascripts/explore/stores/visTypes.js:854 +msgid "Metric that defines the color of the country" +msgstr "" + +#: superset/assets/javascripts/explore/stores/visTypes.js:857 +msgid "Bubble size" +msgstr "" + +#: superset/assets/javascripts/explore/stores/visTypes.js:858 +msgid "Metric that defines the size of the bubble" +msgstr "" + +#: superset/assets/javascripts/explore/stores/visTypes.js:864 +msgid "Filter Box" +msgstr "" + +#: superset/assets/javascripts/explore/stores/visTypes.js:883 +msgid "Filter controls" +msgstr "" + +#: superset/assets/javascripts/explore/stores/visTypes.js:884 +msgid "" +"The controls you want to filter on. Note that only columns checked as " +"\"filterable\" will show up on this list." +msgstr "" + +#: superset/assets/javascripts/explore/stores/visTypes.js:932 +msgid "Axis & Metrics" +msgstr "" + +#: superset/assets/javascripts/explore/stores/visTypes.js:940 +msgid "Heatmap Options" +msgstr "" + +#: superset/assets/javascripts/explore/stores/visTypes.js:961 +msgid "Horizon" +msgstr "" + +#: superset/assets/javascripts/explore/stores/visTypes.js:987 +msgid "Points" +msgstr "" + +#: superset/assets/javascripts/explore/stores/visTypes.js:994 +msgid "Labelling" +msgstr "" + +#: superset/assets/javascripts/explore/stores/visTypes.js:1001 +msgid "Visual Tweaks" +msgstr "" + +#: superset/assets/javascripts/explore/stores/visTypes.js:1010 +msgid "Viewport" +msgstr "" + +#: superset/assets/javascripts/explore/stores/visTypes.js:1020 +msgid "Longitude" +msgstr "" + +#: superset/assets/javascripts/explore/stores/visTypes.js:1021 +msgid "Column containing longitude data" +msgstr "" + +#: superset/assets/javascripts/explore/stores/visTypes.js:1024 +msgid "Latitude" +msgstr "" + +#: superset/assets/javascripts/explore/stores/visTypes.js:1025 +msgid "Column containing latitude data" +msgstr "" + +#: superset/assets/javascripts/explore/stores/visTypes.js:1028 +msgid "Cluster label aggregator" +msgstr "" + +#: superset/assets/javascripts/explore/stores/visTypes.js:1029 +msgid "" +"Aggregate function applied to the list of points in each cluster to " +"produce the cluster label." +msgstr "" + +#: superset/assets/javascripts/explore/stores/visTypes.js:1033 +msgid "Tooltip" +msgstr "" + +#: superset/assets/javascripts/explore/stores/visTypes.js:1034 +msgid "Show a tooltip when hovering over points and clusters describing the label" +msgstr "" + +#: superset/assets/javascripts/explore/stores/visTypes.js:1038 +msgid "" +"One or many controls to group by. If grouping, latitude and longitude " +"columns must be present." +msgstr "" + +#: superset/assets/javascripts/explore/stores/visTypes.js:1049 +msgid "Event definition" +msgstr "" + +#: superset/assets/javascripts/explore/stores/visTypes.js:1059 +msgid "Additional meta data" +msgstr "" + +#: superset/assets/javascripts/explore/stores/visTypes.js:1067 +msgid "Column containing entity ids" +msgstr "" + +#: superset/assets/javascripts/explore/stores/visTypes.js:1068 +msgid "e.g., a \"user id\" column" +msgstr "" + +#: superset/assets/javascripts/explore/stores/visTypes.js:1071 +msgid "Column containing event names" +msgstr "" + +#: superset/assets/javascripts/explore/stores/visTypes.js:1079 +msgid "Event count limit" +msgstr "" + +#: superset/assets/javascripts/explore/stores/visTypes.js:1080 +msgid "The maximum number of events to return, equivalent to number of rows" +msgstr "" + +#: superset/assets/javascripts/explore/stores/visTypes.js:1083 +msgid "Meta data" +msgstr "" + +#: superset/assets/javascripts/explore/stores/visTypes.js:1084 +msgid "Select any columns for meta data inspection" +msgstr "" + +#: superset/assets/javascripts/modules/superset.js:134 +msgid "" +"The server could not be reached. You may want to verify your connection " +"and try again." +msgstr "" + +#: superset/assets/javascripts/modules/superset.js:137 +#, python-format +msgid "An unknown error occurred. (Status: %s )" msgstr "" -#: superset/connectors/druid/models.py:975 +#: superset/assets/javascripts/profile/components/App.jsx:24 +msgid "Favorites" +msgstr "" + +#: superset/assets/javascripts/profile/components/App.jsx:30 +msgid "Created Content" +msgstr "" + +#: superset/assets/javascripts/profile/components/App.jsx:37 +msgid "Recent Activity" +msgstr "" + +#: superset/assets/javascripts/profile/components/App.jsx:42 +msgid "Security & Access" +msgstr "" + +#: superset/assets/javascripts/profile/components/CreatedContent.jsx:33 +msgid "No slices" +msgstr "" + +#: superset/assets/javascripts/profile/components/CreatedContent.jsx:49 +msgid "No dashboards" +msgstr "" + +#: superset/assets/javascripts/profile/components/CreatedContent.jsx:58 +#: superset/assets/javascripts/profile/components/Favorites.jsx:59 +#: superset/templates/superset/welcome.html:13 superset/views/core.py:368 +#: superset/views/core.py:528 +msgid "Dashboards" +msgstr "" + +#: superset/assets/javascripts/profile/components/CreatedContent.jsx:61 +#: superset/assets/javascripts/profile/components/Favorites.jsx:62 +#: superset/views/core.py:404 superset/views/core.py:473 +msgid "Slices" +msgstr "" + +#: superset/assets/javascripts/profile/components/Favorites.jsx:34 +msgid "No favorite slices yet, go click on stars!" +msgstr "" + +#: superset/assets/javascripts/profile/components/Favorites.jsx:50 +msgid "No favorite dashboards yet, go click on stars!" +msgstr "" + +#: superset/assets/javascripts/profile/components/Security.jsx:14 +msgid "Roles" +msgstr "" + +#: superset/assets/javascripts/profile/components/Security.jsx:23 +#: superset/views/core.py:280 +msgid "Databases" +msgstr "" + +#: superset/assets/javascripts/profile/components/Security.jsx:34 +msgid "Datasources" +msgstr "" + +#: superset/assets/javascripts/profile/components/UserInfo.jsx:18 +msgid "Profile picture provided by Gravatar" +msgstr "" + +#: superset/assets/javascripts/profile/components/UserInfo.jsx:33 +msgid "joined" +msgstr "" + +#: superset/assets/javascripts/profile/components/UserInfo.jsx:43 +msgid "id:" +msgstr "" + +#: superset/assets/visualizations/EventFlow.jsx:56 +msgid "Sorry, there appears to be no data" +msgstr "" + +#: superset/assets/visualizations/filter_box.jsx:106 +#, python-format +msgid "Select [%s]" +msgstr "" + +#: superset/connectors/druid/models.py:976 msgid "No data was returned." msgstr "" @@ -382,11 +2715,6 @@ msgstr "" msgid "Type" msgstr "" -#: superset/connectors/druid/views.py:45 superset/views/core.py:314 -#: superset/views/core.py:338 superset/views/core.py:369 -msgid "Datasource" -msgstr "" - #: superset/connectors/druid/views.py:46 superset/connectors/sqla/views.py:84 msgid "Groupable" msgstr "" @@ -403,14 +2731,6 @@ msgstr "" msgid "Sum" msgstr "" -#: superset/connectors/druid/views.py:50 superset/connectors/sqla/views.py:89 -msgid "Min" -msgstr "" - -#: superset/connectors/druid/views.py:51 superset/connectors/sqla/views.py:90 -msgid "Max" -msgstr "" - #: superset/connectors/druid/views.py:54 superset/connectors/sqla/views.py:50 msgid "" "Whether this column is exposed in the `Filters` section of the explore " @@ -440,17 +2760,6 @@ msgid "" "metric)' are allowed to access this metric" msgstr "" -#: superset/connectors/druid/views.py:106 superset/connectors/sqla/views.py:131 -msgid "Metric" -msgstr "" - -#: superset/connectors/druid/views.py:107 -#: superset/connectors/druid/views.py:229 superset/connectors/sqla/views.py:83 -#: superset/connectors/sqla/views.py:132 superset/connectors/sqla/views.py:229 -#: superset/views/core.py:370 superset/views/sql_lab.py:56 -msgid "Description" -msgstr "" - #: superset/connectors/druid/views.py:108 superset/connectors/sqla/views.py:82 #: superset/connectors/sqla/views.py:133 msgid "Verbose Name" @@ -481,7 +2790,7 @@ msgid "Edit Druid Cluster" msgstr "" #: superset/connectors/druid/views.py:142 -#: superset/connectors/druid/views.py:228 +#: superset/connectors/druid/views.py:226 msgid "Cluster" msgstr "" @@ -514,8 +2823,8 @@ msgid "Druid Clusters" msgstr "" #: superset/connectors/druid/views.py:166 -#: superset/connectors/druid/views.py:268 -#: superset/connectors/druid/views.py:307 superset/connectors/sqla/views.py:287 +#: superset/connectors/druid/views.py:266 +#: superset/connectors/druid/views.py:305 superset/connectors/sqla/views.py:285 #: superset/views/core.py:283 msgid "Sources" msgstr "" @@ -536,7 +2845,7 @@ msgstr "" msgid "Edit Druid Datasource" msgstr "" -#: superset/connectors/druid/views.py:197 superset/connectors/sqla/views.py:178 +#: superset/connectors/druid/views.py:195 superset/connectors/sqla/views.py:176 msgid "" "The list of slices associated with this table. By altering this " "datasource, you may change how these associated slices behave. Also note " @@ -545,11 +2854,11 @@ msgid "" "datasource for a slice, overwrite the slice from the 'explore view'" msgstr "" -#: superset/connectors/druid/views.py:205 superset/connectors/sqla/views.py:186 +#: superset/connectors/druid/views.py:203 superset/connectors/sqla/views.py:184 msgid "Timezone offset (in hours) for this datasource" msgstr "" -#: superset/connectors/druid/views.py:209 +#: superset/connectors/druid/views.py:207 msgid "" "Time expression to use as a predicate when retrieving distinct values to " "populate the filter component. Only applies when `Enable Filter Select` " @@ -557,71 +2866,71 @@ msgid "" "filter will be populated based on the distinct value over the past week" msgstr "" -#: superset/connectors/druid/views.py:216 superset/connectors/sqla/views.py:208 +#: superset/connectors/druid/views.py:214 superset/connectors/sqla/views.py:206 msgid "" "Whether to populate the filter's dropdown in the explore view's filter " "section with a list of distinct values fetched from the backend on the " "fly" msgstr "" -#: superset/connectors/druid/views.py:220 +#: superset/connectors/druid/views.py:218 msgid "" "Redirects to this endpoint when clicking on the datasource from the " "datasource list" msgstr "" -#: superset/connectors/druid/views.py:226 superset/connectors/sqla/views.py:215 +#: superset/connectors/druid/views.py:224 superset/connectors/sqla/views.py:213 msgid "Associated Slices" msgstr "" -#: superset/connectors/druid/views.py:227 +#: superset/connectors/druid/views.py:225 msgid "Data Source" msgstr "" -#: superset/connectors/druid/views.py:230 superset/connectors/sqla/views.py:227 +#: superset/connectors/druid/views.py:228 superset/connectors/sqla/views.py:225 msgid "Owner" msgstr "" -#: superset/connectors/druid/views.py:231 +#: superset/connectors/druid/views.py:229 msgid "Is Hidden" msgstr "" -#: superset/connectors/druid/views.py:232 superset/connectors/sqla/views.py:220 +#: superset/connectors/druid/views.py:230 superset/connectors/sqla/views.py:218 msgid "Enable Filter Select" msgstr "" -#: superset/connectors/druid/views.py:233 superset/connectors/sqla/views.py:222 +#: superset/connectors/druid/views.py:231 superset/connectors/sqla/views.py:220 msgid "Default Endpoint" msgstr "" -#: superset/connectors/druid/views.py:234 +#: superset/connectors/druid/views.py:232 msgid "Time Offset" msgstr "" -#: superset/connectors/druid/views.py:235 superset/connectors/sqla/views.py:224 +#: superset/connectors/druid/views.py:233 superset/connectors/sqla/views.py:222 #: superset/views/core.py:248 superset/views/core.py:366 msgid "Cache Timeout" msgstr "" -#: superset/connectors/druid/views.py:266 +#: superset/connectors/druid/views.py:264 msgid "Druid Datasources" msgstr "" -#: superset/connectors/druid/views.py:304 +#: superset/connectors/druid/views.py:302 msgid "Refresh Druid Metadata" msgstr "" -#: superset/connectors/sqla/models.py:390 +#: superset/connectors/sqla/models.py:392 msgid "" "Datetime column not provided as part table configuration and is required " "by this type of chart" msgstr "" -#: superset/connectors/sqla/models.py:395 +#: superset/connectors/sqla/models.py:397 msgid "Metric '{}' is not valid" msgstr "" -#: superset/connectors/sqla/models.py:581 +#: superset/connectors/sqla/models.py:585 msgid "" "Table doesn't seem to exist in the specified database, couldn't fetch " "column information" @@ -656,11 +2965,6 @@ msgid "" "most case users should not need to alter this." msgstr "" -#: superset/connectors/sqla/views.py:86 superset/connectors/sqla/views.py:136 -#: superset/connectors/sqla/views.py:216 superset/views/core.py:376 -msgid "Table" -msgstr "" - #: superset/connectors/sqla/views.py:91 msgid "Expression" msgstr "" @@ -721,77 +3025,77 @@ msgstr "" msgid "Edit Table" msgstr "" -#: superset/connectors/sqla/views.py:187 +#: superset/connectors/sqla/views.py:185 msgid "Name of the table that exists in the source database" msgstr "" -#: superset/connectors/sqla/views.py:189 +#: superset/connectors/sqla/views.py:187 msgid "Schema, as used only in some databases like Postgres, Redshift and DB2" msgstr "" -#: superset/connectors/sqla/views.py:195 +#: superset/connectors/sqla/views.py:193 msgid "" "This fields acts a Superset view, meaning that Superset will run a query " "against this string as a subquery." msgstr "" -#: superset/connectors/sqla/views.py:199 +#: superset/connectors/sqla/views.py:197 msgid "" "Predicate applied when fetching distinct value to populate the filter " "control component. Supports jinja template syntax. Applies only when " "`Enable Filter Select` is on." msgstr "" -#: superset/connectors/sqla/views.py:205 +#: superset/connectors/sqla/views.py:203 msgid "Redirects to this endpoint when clicking on the table from the table list" msgstr "" -#: superset/connectors/sqla/views.py:217 +#: superset/connectors/sqla/views.py:215 msgid "Changed By" msgstr "" -#: superset/connectors/sqla/views.py:218 superset/views/core.py:244 +#: superset/connectors/sqla/views.py:216 superset/views/core.py:244 #: superset/views/sql_lab.py:19 superset/views/sql_lab.py:55 msgid "Database" msgstr "" -#: superset/connectors/sqla/views.py:219 superset/views/core.py:246 +#: superset/connectors/sqla/views.py:217 superset/views/core.py:246 msgid "Last Changed" msgstr "" -#: superset/connectors/sqla/views.py:221 +#: superset/connectors/sqla/views.py:219 msgid "Schema" msgstr "" -#: superset/connectors/sqla/views.py:223 +#: superset/connectors/sqla/views.py:221 msgid "Offset" msgstr "" -#: superset/connectors/sqla/views.py:225 +#: superset/connectors/sqla/views.py:223 msgid "Table Name" msgstr "" -#: superset/connectors/sqla/views.py:226 +#: superset/connectors/sqla/views.py:224 msgid "Fetch Values Predicate" msgstr "" -#: superset/connectors/sqla/views.py:228 +#: superset/connectors/sqla/views.py:226 msgid "Main Datetime Column" msgstr "" -#: superset/connectors/sqla/views.py:248 +#: superset/connectors/sqla/views.py:246 msgid "" "Table [{}] could not be found, please double check your database " "connection, schema, and table name" msgstr "" -#: superset/connectors/sqla/views.py:261 +#: superset/connectors/sqla/views.py:259 msgid "" "The table was created. As part of this two phase configuration process, " "you should now click the edit button by the new table to configure it." msgstr "" -#: superset/connectors/sqla/views.py:285 +#: superset/connectors/sqla/views.py:283 msgid "Tables" msgstr "" @@ -815,10 +3119,6 @@ msgstr "" msgid "No records found" msgstr "" -#: superset/templates/appbuilder/general/widgets/search.html:6 -msgid "Add Filter" -msgstr "" - #: superset/templates/superset/import_dashboards.html:11 msgid "Import" msgstr "" @@ -836,37 +3136,28 @@ msgstr "" msgid "Request Permissions" msgstr "" -#: superset/templates/superset/request_access.html:16 -msgid "Cancel" -msgstr "" - #: superset/templates/superset/welcome.html:3 msgid "Welcome!" msgstr "" -#: superset/templates/superset/welcome.html:13 superset/views/core.py:368 -#: superset/views/core.py:528 -msgid "Dashboards" -msgstr "" - #: superset/templates/superset/models/database/macros.html:4 msgid "Test Connection" msgstr "" -#: superset/views/base.py:58 +#: superset/views/base.py:62 #, python-format msgid "Datasource %(name)s already exists" msgstr "" -#: superset/views/base.py:206 +#: superset/views/base.py:221 msgid "json isn't valid" msgstr "" -#: superset/views/base.py:257 +#: superset/views/base.py:272 msgid "Delete" msgstr "" -#: superset/views/base.py:258 +#: superset/views/base.py:273 msgid "Delete all Really?" msgstr "" @@ -994,15 +3285,11 @@ msgstr "" msgid "Import Dashboards" msgstr "" -#: superset/views/core.py:273 superset/views/core.py:2330 +#: superset/views/core.py:273 superset/views/core.py:2334 #: superset/views/sql_lab.py:30 msgid "Manage" msgstr "" -#: superset/views/core.py:280 -msgid "Databases" -msgstr "" - #: superset/views/core.py:311 superset/views/core.py:552 #: superset/views/sql_lab.py:18 superset/views/sql_lab.py:54 msgid "User" @@ -1076,18 +3363,6 @@ msgstr "" msgid "Slice" msgstr "" -#: superset/views/core.py:375 -msgid "Name" -msgstr "" - -#: superset/views/core.py:377 -msgid "Visualization Type" -msgstr "" - -#: superset/views/core.py:404 superset/views/core.py:473 -msgid "Slices" -msgstr "" - #: superset/views/core.py:433 msgid "List Dashboards" msgstr "" @@ -1136,27 +3411,14 @@ msgstr "" msgid "Dashboard" msgstr "" -#: superset/views/core.py:471 superset/views/core.py:538 -msgid "Title" -msgstr "" - #: superset/views/core.py:472 msgid "Slug" msgstr "" -#: superset/views/core.py:476 superset/views/core.py:540 -#: superset/views/sql_lab.py:57 -msgid "Modified" -msgstr "" - #: superset/views/core.py:477 msgid "Position JSON" msgstr "" -#: superset/views/core.py:478 -msgid "CSS" -msgstr "" - #: superset/views/core.py:479 msgid "JSON Metadata" msgstr "" @@ -1205,53 +3467,53 @@ msgstr "" msgid "You have no permission to approve this request" msgstr "" -#: superset/views/core.py:1618 +#: superset/views/core.py:1625 msgid "" "Malformed request. slice_id or table_name and db_name arguments are " "expected" msgstr "" -#: superset/views/core.py:1624 +#: superset/views/core.py:1631 #, python-format msgid "Slice %(id)s not found" msgstr "" -#: superset/views/core.py:1636 +#: superset/views/core.py:1643 #, python-format msgid "Table %(t)s wasn't found in the database %(d)s" msgstr "" -#: superset/views/core.py:1774 +#: superset/views/core.py:1782 #, python-format msgid "Can't find User '%(name)s', please ask your admin to create one." msgstr "" -#: superset/views/core.py:1781 +#: superset/views/core.py:1789 #, python-format msgid "Can't find DruidCluster with cluster_name = '%(name)s'" msgstr "" -#: superset/views/core.py:2042 +#: superset/views/core.py:2050 msgid "Query record was not created as expected." msgstr "" -#: superset/views/core.py:2316 +#: superset/views/core.py:2320 msgid "Template Name" msgstr "" -#: superset/views/core.py:2327 +#: superset/views/core.py:2331 msgid "CSS Templates" msgstr "" -#: superset/views/core.py:2337 +#: superset/views/core.py:2341 msgid "SQL Editor" msgstr "" -#: superset/views/core.py:2342 superset/views/core.py:2351 +#: superset/views/core.py:2346 superset/views/core.py:2355 msgid "SQL Lab" msgstr "" -#: superset/views/core.py:2346 +#: superset/views/core.py:2350 msgid "Query Search" msgstr "" @@ -1287,10 +3549,6 @@ msgstr "" msgid "Edit Saved Query" msgstr "" -#: superset/views/sql_lab.py:53 -msgid "Label" -msgstr "" - #: superset/views/sql_lab.py:59 msgid "Pop Tab Link" msgstr "" diff --git a/dev-reqs.txt b/dev-reqs.txt index c53fdefa1ea47..0705bd2da4a32 100644 --- a/dev-reqs.txt +++ b/dev-reqs.txt @@ -7,7 +7,6 @@ mysqlclient nose psycopg2 pylint -pythrifthiveapi pyyaml redis statsd diff --git a/docs/faq.rst b/docs/faq.rst index 82280ed46bdfe..1c70fc8105a31 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -60,9 +60,9 @@ There are many reasons may cause long query timing out. ``superset runserver -t 300`` -- If you are seeing timeouts (504 Gateway Time-out) when loading dashboard or explore slice, you are probably behind gateway or proxy server (such as Nginx). If it did not receive a timely response from Superset server (which is processing long queries), these web servers will send 504 status code to clients directly. Superset has a client-side timeout limit to address this issue. If query didn't come back within clint-side timeout (45 seconds by default), Superset will display warning message to avoid gateway timeout message. If you have a longer gateway timeout limit, you can change client-side timeout limit settings from ``/superset/superset/assets/javascripts/constants.js`` file and rebuild js package: +- If you are seeing timeouts (504 Gateway Time-out) when loading dashboard or explore slice, you are probably behind gateway or proxy server (such as Nginx). If it did not receive a timely response from Superset server (which is processing long queries), these web servers will send 504 status code to clients directly. Superset has a client-side timeout limit to address this issue. If query didn't come back within clint-side timeout (60 seconds by default), Superset will display warning message to avoid gateway timeout message. If you have a longer gateway timeout limit, you can change the timeout settings in ``superset_config.py``: - ``export const QUERY_TIMEOUT_THRESHOLD = 45000;`` + ``SUPERSET_WEBSERVER_TIMEOUT = 60`` Why is the map not visible in the mapbox visualization? diff --git a/setup.py b/setup.py index 94104776eb473..cb8fc94c8715a 100644 --- a/setup.py +++ b/setup.py @@ -58,6 +58,7 @@ def get_git_sha(): 'future>=0.16.0, <0.17', 'humanize==0.5.1', 'gunicorn==19.7.1', + 'idna==2.5', 'markdown==2.6.8', 'pandas==0.20.2', 'parsedatetime==2.0.0', diff --git a/superset/__init__.py b/superset/__init__.py index af81248c614c2..6988e6b580fbb 100644 --- a/superset/__init__.py +++ b/superset/__init__.py @@ -20,14 +20,12 @@ from superset.connectors.connector_registry import ConnectorRegistry from superset import utils, config # noqa - APP_DIR = os.path.dirname(__file__) CONFIG_MODULE = os.environ.get('SUPERSET_CONFIG', 'superset.config') with open(APP_DIR + '/static/assets/backendSync.json', 'r') as f: frontend_config = json.load(f) - app = Flask(__name__) app.config.from_object(CONFIG_MODULE) conf = app.config @@ -53,14 +51,16 @@ def get_manifest_file(filename): parse_manifest_json() return '/static/assets/dist/' + manifest.get(filename, '') + parse_manifest_json() + @app.context_processor def get_js_manifest(): return dict(js_manifest=get_manifest_file) -################################################################# +################################################################# for bp in conf.get('BLUEPRINTS'): try: @@ -100,10 +100,11 @@ def get_js_manifest(): if app.config.get('ENABLE_TIME_ROTATE'): logging.getLogger().setLevel(app.config.get('TIME_ROTATE_LOG_LEVEL')) - handler = TimedRotatingFileHandler(app.config.get('FILENAME'), - when=app.config.get('ROLLOVER'), - interval=app.config.get('INTERVAL'), - backupCount=app.config.get('BACKUP_COUNT')) + handler = TimedRotatingFileHandler( + app.config.get('FILENAME'), + when=app.config.get('ROLLOVER'), + interval=app.config.get('INTERVAL'), + backupCount=app.config.get('BACKUP_COUNT')) logging.getLogger().addHandler(handler) if app.config.get('ENABLE_CORS'): @@ -114,17 +115,18 @@ def get_js_manifest(): app.wsgi_app = ProxyFix(app.wsgi_app) if app.config.get('ENABLE_CHUNK_ENCODING'): - class ChunkedEncodingFix(object): + class ChunkedEncodingFix(object): def __init__(self, app): self.app = app def __call__(self, environ, start_response): # Setting wsgi.input_terminated tells werkzeug.wsgi to ignore # content-length and read the stream till the end. - if 'chunked' == environ.get('HTTP_TRANSFER_ENCODING', '').lower(): + if environ.get('HTTP_TRANSFER_ENCODING', '').lower() == u'chunked': environ['wsgi.input_terminated'] = True return self.app(environ, start_response) + app.wsgi_app = ChunkedEncodingFix(app.wsgi_app) if app.config.get('UPLOAD_FOLDER'): @@ -142,8 +144,10 @@ class MyIndexView(IndexView): def index(self): return redirect('/superset/welcome') + appbuilder = AppBuilder( - app, db.session, + app, + db.session, base_template='superset/base.html', indexview=MyIndexView, security_manager_class=app.config.get("CUSTOM_SECURITY_MANAGER")) diff --git a/superset/assets/images/viz_thumbnails/paired_ttest.png b/superset/assets/images/viz_thumbnails/paired_ttest.png new file mode 100644 index 0000000000000..4f8ad71b1213f Binary files /dev/null and b/superset/assets/images/viz_thumbnails/paired_ttest.png differ diff --git a/superset/assets/javascripts/SqlLab/actions.js b/superset/assets/javascripts/SqlLab/actions.js index 517c5d28d933d..6438226e74a76 100644 --- a/superset/assets/javascripts/SqlLab/actions.js +++ b/superset/assets/javascripts/SqlLab/actions.js @@ -1,6 +1,7 @@ /* global notify */ import shortid from 'shortid'; import { now } from '../modules/dates'; +import { t } from '../locales'; const $ = require('jquery'); @@ -53,8 +54,8 @@ export function saveQuery(query) { type: 'POST', url, data: query, - success: () => notify.success('Your query was saved'), - error: () => notify.error('Your query could not be saved'), + success: () => notify.success(t('Your query was saved')), + error: () => notify.error(t('Your query could not be saved')), dataType: 'json', }); return { type: SAVE_QUERY }; @@ -107,7 +108,7 @@ export function fetchQueryResults(query) { dispatch(querySuccess(query, results)); }, error(err) { - let msg = 'Failed at retrieving results from the results backend'; + let msg = t('Failed at retrieving results from the results backend'); if (err.responseJSON && err.responseJSON.error) { msg = err.responseJSON.error; } @@ -153,12 +154,12 @@ export function runQuery(query) { } } if (textStatus === 'error' && errorThrown === '') { - msg = 'Could not connect to server'; + msg = t('Could not connect to server'); } else if (msg === null) { msg = `[${textStatus}] ${errorThrown}`; } if (msg.indexOf('CSRF token') > 0) { - msg = 'Your session timed out, please refresh your page and try again.'; + msg = t('Your session timed out, please refresh your page and try again.'); } dispatch(queryFailed(query, msg)); }, @@ -177,10 +178,10 @@ export function postStopQuery(query) { url: stopQueryUrl, data: stopQueryRequestData, success() { - notify.success('Query was stopped.'); + notify.success(t('Query was stopped.')); }, error() { - notify.error('Failed at stopping query.'); + notify.error(t('Failed at stopping query.')); }, }); }; @@ -293,7 +294,7 @@ export function addTable(query, tableName, schemaName) { isMetadataLoading: false, }); dispatch(mergeTable(newTable)); - notify.error('Error occurred while fetching table metadata'); + notify.error(t('Error occurred while fetching table metadata')); }); url = `/superset/extra_table_metadata/${query.dbId}/${tableName}/${schemaName}/`; @@ -306,7 +307,7 @@ export function addTable(query, tableName, schemaName) { isExtraMetadataLoading: false, }); dispatch(mergeTable(newTable)); - notify.error('Error occurred while fetching table metadata'); + notify.error(t('Error occurred while fetching table metadata')); }); }; } @@ -360,7 +361,7 @@ export function popStoredQuery(urlId) { success: (data) => { const newQuery = JSON.parse(data); const queryEditorProps = { - title: newQuery.title ? newQuery.title : 'shared query', + title: newQuery.title ? newQuery.title : t('shared query'), dbId: newQuery.dbId ? parseInt(newQuery.dbId, 10) : null, schema: newQuery.schema ? newQuery.schema : null, autorun: newQuery.autorun ? newQuery.autorun : false, @@ -368,7 +369,7 @@ export function popStoredQuery(urlId) { }; dispatch(addQueryEditor(queryEditorProps)); }, - error: () => notify.error("The query couldn't be loaded"), + error: () => notify.error(t('The query couldn\'t be loaded')), }); }; } @@ -388,7 +389,7 @@ export function popSavedQuery(saveQueryId) { }; dispatch(addQueryEditor(queryEditorProps)); }, - error: () => notify.error("The query couldn't be loaded"), + error: () => notify.error(t('The query couldn\'t be loaded')), }); }; } @@ -421,7 +422,7 @@ export function createDatasource(vizOptions, context) { dispatch(createDatasourceSuccess(resp)); }, error: () => { - dispatch(createDatasourceFailed('An error occurred while creating the data source')); + dispatch(createDatasourceFailed(t('An error occurred while creating the data source'))); }, }); }; diff --git a/superset/assets/javascripts/SqlLab/components/CopyQueryTabUrl.jsx b/superset/assets/javascripts/SqlLab/components/CopyQueryTabUrl.jsx index 5491eed545b3e..66e60c9b2b4f7 100644 --- a/superset/assets/javascripts/SqlLab/components/CopyQueryTabUrl.jsx +++ b/superset/assets/javascripts/SqlLab/components/CopyQueryTabUrl.jsx @@ -2,6 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import CopyToClipboard from '../../components/CopyToClipboard'; import { storeQuery } from '../../../utils/common'; +import { t } from '../../locales'; const propTypes = { queryEditor: PropTypes.object.isRequired, @@ -26,10 +27,10 @@ export default class CopyQueryTabUrl extends React.PureComponent { inMenu copyNode={(
- share query + {t('share query')}
)} - tooltipText="copy URL to clipboard" + tooltipText={t('copy URL to clipboard')} shouldShowText={false} getText={this.getUrl.bind(this)} /> diff --git a/superset/assets/javascripts/SqlLab/components/HighlightedSql.jsx b/superset/assets/javascripts/SqlLab/components/HighlightedSql.jsx index f7d69416de4d4..3efab4c9c341a 100644 --- a/superset/assets/javascripts/SqlLab/components/HighlightedSql.jsx +++ b/superset/assets/javascripts/SqlLab/components/HighlightedSql.jsx @@ -6,6 +6,7 @@ import sql from 'react-syntax-highlighter/dist/languages/sql'; import github from 'react-syntax-highlighter/dist/styles/github'; import ModalTrigger from '../../components/ModalTrigger'; +import { t } from '../../locales'; registerLanguage('sql', sql); @@ -57,7 +58,7 @@ class HighlightedSql extends React.Component { if (this.props.rawSql && this.props.rawSql !== this.props.sql) { rawSql = (
-

Raw SQL

+

{t('Raw SQL')}

{this.props.rawSql} @@ -67,7 +68,7 @@ class HighlightedSql extends React.Component { this.setState({ modalBody: (
-

Source SQL

+

{t('Source SQL')}

{this.props.sql} @@ -79,7 +80,7 @@ class HighlightedSql extends React.Component { render() { return ( { } return ( - No query history yet... + {t('No query history yet...')} ); }; diff --git a/superset/assets/javascripts/SqlLab/components/QuerySearch.jsx b/superset/assets/javascripts/SqlLab/components/QuerySearch.jsx index 5101f09f8b723..10c1c88e79c72 100644 --- a/superset/assets/javascripts/SqlLab/components/QuerySearch.jsx +++ b/superset/assets/javascripts/SqlLab/components/QuerySearch.jsx @@ -7,6 +7,7 @@ import { now, epochTimeXHoursAgo, epochTimeXDaysAgo, epochTimeXYearsAgo } from '../../modules/dates'; import { STATUS_OPTIONS, TIME_OPTIONS } from '../constants'; import AsyncSelect from '../../components/AsyncSelect'; +import { t } from '../../locales'; const $ = window.$ = require('jquery'); @@ -102,7 +103,7 @@ class QuerySearch extends React.PureComponent { if (data.result.length === 0) { this.props.actions.addAlert({ bsStyle: 'danger', - msg: "It seems you don't have access to any database", + msg: t('It seems you don\'t have access to any database'), }); } return options; @@ -150,15 +151,15 @@ class QuerySearch extends React.PureComponent { type="text" onChange={this.changeSearch.bind(this)} className="form-control input-sm" - placeholder="Search Results" + placeholder={t('Search Results')} />
({ value: t, label: t }))} + placeholder={t('[To]-')} + options={TIME_OPTIONS.map(xt => ({ value: xt, label: xt }))} value={this.state.to} autosize={false} onChange={this.changeTo.bind(this)} @@ -175,7 +176,7 @@ class QuerySearch extends React.PureComponent { (
- Schema: {o.label} + {t('Schema:')} {o.label}
)} isLoading={this.state.schemaLoading} @@ -186,7 +187,7 @@ class SqlEditorLeftBar extends React.PureComponent { ref="selectTable" isLoading={this.state.tableLoading} value={this.state.tableName} - placeholder={`Add a table (${this.state.tableOptions.length})`} + placeholder={t('Add a table (%s)', this.state.tableOptions.length)} autosize={false} onChange={this.changeTable.bind(this)} filterOptions={this.state.filterOptions} @@ -199,7 +200,7 @@ class SqlEditorLeftBar extends React.PureComponent { name="async-select-table" ref="selectTable" value={this.state.tableName} - placeholder={'Type to search ...'} + placeholder={t('Type to search ...')} autosize={false} onChange={this.changeTable.bind(this)} loadOptions={this.getTableNamesBySubStr.bind(this)} @@ -222,7 +223,7 @@ class SqlEditorLeftBar extends React.PureComponent {
{shouldShowReset && }
diff --git a/superset/assets/javascripts/SqlLab/components/TabbedSqlEditors.jsx b/superset/assets/javascripts/SqlLab/components/TabbedSqlEditors.jsx index 73ba6bcd29c6f..4f716d9a71ab3 100644 --- a/superset/assets/javascripts/SqlLab/components/TabbedSqlEditors.jsx +++ b/superset/assets/javascripts/SqlLab/components/TabbedSqlEditors.jsx @@ -9,6 +9,7 @@ import * as Actions from '../actions'; import SqlEditor from './SqlEditor'; import CopyQueryTabUrl from './CopyQueryTabUrl'; import { areArraysShallowEqual } from '../../reduxUtils'; +import { t } from '../../locales'; const propTypes = { actions: PropTypes.object.isRequired, @@ -101,7 +102,7 @@ class TabbedSqlEditors extends React.PureComponent { } renameTab(qe) { /* eslint no-alert: 0 */ - const newTitle = prompt('Enter a new title for the tab'); + const newTitle = prompt(t('Enter a new title for the tab')); if (newTitle) { this.props.actions.queryEditorSetTitle(qe, newTitle); } @@ -120,7 +121,7 @@ class TabbedSqlEditors extends React.PureComponent { queryCount++; const activeQueryEditor = this.activeQueryEditor(); const qe = { - title: `Untitled Query ${queryCount}`, + title: t('Untitled Query %s', queryCount), dbId: (activeQueryEditor && activeQueryEditor.dbId) ? activeQueryEditor.dbId : this.props.defaultDbId, @@ -166,10 +167,10 @@ class TabbedSqlEditors extends React.PureComponent { title="" > - close tab + {t('close tab')} - rename tab + {t('rename tab')} {qe && @@ -177,7 +178,7 @@ class TabbedSqlEditors extends React.PureComponent {   - {this.state.hideLeftBar ? 'expand tool bar' : 'hide tool bar'} + {this.state.hideLeftBar ? t('expand tool bar') : t('hide tool bar')} @@ -193,7 +194,7 @@ class TabbedSqlEditors extends React.PureComponent { {isSelected && (t.queryEditorId === qe.id))} + tables={this.props.tables.filter(xt => (xt.queryEditorId === qe.id))} queryEditor={qe} editorQueries={this.state.queriesArray} dataPreviewQueries={this.state.dataPreviewQueries} diff --git a/superset/assets/javascripts/SqlLab/components/TableElement.jsx b/superset/assets/javascripts/SqlLab/components/TableElement.jsx index fc8ae0c669991..624a0ed1c7650 100644 --- a/superset/assets/javascripts/SqlLab/components/TableElement.jsx +++ b/superset/assets/javascripts/SqlLab/components/TableElement.jsx @@ -9,6 +9,7 @@ import Link from './Link'; import ColumnElement from './ColumnElement'; import ModalTrigger from '../../components/ModalTrigger'; import Loading from '../../components/Loading'; +import { t } from '../../locales'; const propTypes = { table: PropTypes.object, @@ -71,7 +72,7 @@ class TableElement extends React.PureComponent { let partitionClipBoard; if (table.partitions.partitionQuery) { partitionQuery = table.partitions.partitionQuery; - const tt = 'Copy partition query to clipboard'; + const tt = t('Copy partition query to clipboard'); partitionClipBoard = (
- latest partition: {latest} + {t('latest partition:')} {latest} {partitionClipBoard}
@@ -106,7 +107,7 @@ class TableElement extends React.PureComponent { - Keys for table {table.name} + {t('Keys for table')} {table.name} } modalBody={table.indexes.map((ix, i) => ( @@ -115,7 +116,7 @@ class TableElement extends React.PureComponent { triggerNode={ } /> @@ -131,8 +132,8 @@ class TableElement extends React.PureComponent { onClick={this.toggleSortColumns.bind(this)} tooltip={ !this.state.sortColumns ? - 'Sort columns alphabetically' : - 'Original table column order'} + t('Sort columns alphabetically') : + t('Original table column order')} href="#" /> {table.selectStar && @@ -142,13 +143,13 @@ class TableElement extends React.PureComponent { } text={table.selectStar} shouldShowText={false} - tooltipText="Copy SELECT statement to clipboard" + tooltipText={t('Copy SELECT statement to clipboard')} /> } diff --git a/superset/assets/javascripts/SqlLab/components/VisualizeModal.jsx b/superset/assets/javascripts/SqlLab/components/VisualizeModal.jsx index ff9119a84ecf4..965718df78884 100644 --- a/superset/assets/javascripts/SqlLab/components/VisualizeModal.jsx +++ b/superset/assets/javascripts/SqlLab/components/VisualizeModal.jsx @@ -13,6 +13,7 @@ import { getExploreUrl } from '../../explore/exploreUtils'; import * as actions from '../actions'; import { VISUALIZE_VALIDATION_ERRORS } from '../constants'; import visTypes from '../../explore/stores/visTypes'; +import { t } from '../../locales'; const CHART_TYPES = Object.keys(visTypes) .filter(typeName => !!visTypes[typeName].showOnExplore) @@ -86,9 +87,9 @@ class VisualizeModal extends React.PureComponent { if (!re.test(colName)) { hints.push(
- "{colName}" is not right as a column name, please alias it - (as in SELECT count(*) AS my_alias) using only - alphanumeric characters and underscores + {t('%s is not right as a column name, please alias it ' + + '(as in SELECT count(*) ', colName)} {t('AS my_alias')}) {t('using only ' + + 'alphanumeric characters and underscores')}
); } }); @@ -162,7 +163,7 @@ class VisualizeModal extends React.PureComponent { if (mainGroupBy) { formData.groupby = [mainGroupBy.name]; } - notify.info('Creating a data source and popping a new tab'); + notify.info(t('Creating a data source and popping a new tab')); window.open(getExploreUrl(formData)); }) @@ -192,7 +193,7 @@ class VisualizeModal extends React.PureComponent {
- No results available for this query + {t('No results available for this query')}
@@ -237,17 +238,17 @@ class VisualizeModal extends React.PureComponent {
- Visualize + {t('Visualize')} {alerts} {this.buildVisualizeAdvise()}
- Chart Type + {t('Chart Type')} @@ -276,7 +277,7 @@ class VisualizeModal extends React.PureComponent { bsStyle="primary" disabled={(this.state.hints.length > 0)} > - Visualize + {t('Visualize')} diff --git a/superset/assets/javascripts/SqlLab/constants.js b/superset/assets/javascripts/SqlLab/constants.js index 6d678067cc7a1..6af44e4651b2e 100644 --- a/superset/assets/javascripts/SqlLab/constants.js +++ b/superset/assets/javascripts/SqlLab/constants.js @@ -1,3 +1,5 @@ +import { t } from '../locales'; + export const STATE_BSSTYLE_MAP = { failed: 'danger', pending: 'info', @@ -25,8 +27,8 @@ export const TIME_OPTIONS = [ ]; export const VISUALIZE_VALIDATION_ERRORS = { - REQUIRE_CHART_TYPE: 'Pick a chart type!', - REQUIRE_TIME: 'To use this chart type you need at least one column flagged as a date', - REQUIRE_DIMENSION: 'To use this chart type you need at least one dimension', - REQUIRE_AGGREGATION_FUNCTION: 'To use this chart type you need at least one aggregation function', + REQUIRE_CHART_TYPE: t('Pick a chart type!'), + REQUIRE_TIME: t('To use this chart type you need at least one column flagged as a date'), + REQUIRE_DIMENSION: t('To use this chart type you need at least one dimension'), + REQUIRE_AGGREGATION_FUNCTION: t('To use this chart type you need at least one aggregation function'), }; diff --git a/superset/assets/javascripts/SqlLab/reducers.js b/superset/assets/javascripts/SqlLab/reducers.js index c85d27d240b50..3a49bd1b881cc 100644 --- a/superset/assets/javascripts/SqlLab/reducers.js +++ b/superset/assets/javascripts/SqlLab/reducers.js @@ -3,11 +3,12 @@ import * as actions from './actions'; import { now } from '../modules/dates'; import { addToObject, alterInObject, alterInArr, removeFromArr, getFromArr, addToArr } from '../reduxUtils'; +import { t } from '../locales'; export function getInitialState(defaultDbId) { const defaultQueryEditor = { id: shortid.generate(), - title: 'Untitled Query', + title: t('Untitled Query'), sql: 'SELECT *\nFROM\nWHERE', selectedText: null, latestQueryId: null, @@ -40,7 +41,7 @@ export const sqlLabReducer = function (state, action) { qe.id === state.tabHistory[state.tabHistory.length - 1]); const qe = { id: shortid.generate(), - title: `Copy of ${progenitor.title}`, + title: t('Copy of %s', progenitor.title), dbId: (action.query.dbId) ? action.query.dbId : null, schema: (action.query.schema) ? action.query.schema : null, autorun: true, @@ -76,13 +77,13 @@ export const sqlLabReducer = function (state, action) { [actions.MERGE_TABLE]() { const at = Object.assign({}, action.table); let existingTable; - state.tables.forEach((t) => { + state.tables.forEach((xt) => { if ( - t.dbId === at.dbId && - t.queryEditorId === at.queryEditorId && - t.schema === at.schema && - t.name === at.name) { - existingTable = t; + xt.dbId === at.dbId && + xt.queryEditorId === at.queryEditorId && + xt.schema === at.schema && + xt.name === at.name) { + existingTable = xt; } }); if (existingTable) { @@ -115,11 +116,11 @@ export const sqlLabReducer = function (state, action) { delete queries[action.oldQueryId]; const newTables = []; - state.tables.forEach((t) => { - if (t.dataPreviewQueryId === action.oldQueryId) { - newTables.push(Object.assign({}, t, { dataPreviewQueryId: action.newQuery.id })); + state.tables.forEach((xt) => { + if (xt.dataPreviewQueryId === action.oldQueryId) { + newTables.push(Object.assign({}, xt, { dataPreviewQueryId: action.newQuery.id })); } else { - newTables.push(t); + newTables.push(xt); } }); return Object.assign( diff --git a/superset/assets/javascripts/addSlice/AddSliceContainer.jsx b/superset/assets/javascripts/addSlice/AddSliceContainer.jsx index c316e2c76ed09..f0fc1218622b2 100644 --- a/superset/assets/javascripts/addSlice/AddSliceContainer.jsx +++ b/superset/assets/javascripts/addSlice/AddSliceContainer.jsx @@ -50,30 +50,30 @@ export default class AddSliceContainer extends React.PureComponent { render() { return (
- Create a new slice}> + {('Create a new slice')}}>
-

Choose a datasource

+

{('Choose a datasource')}

@@ -83,7 +83,7 @@ export default class AddSliceContainer extends React.PureComponent { disabled={this.isBtnDisabled()} onClick={this.gotoSlice.bind(this)} > - Create new slice + {('Create new slice')}

diff --git a/superset/assets/javascripts/common.js b/superset/assets/javascripts/common.js index 8ecfbe5822137..d84f064065e82 100644 --- a/superset/assets/javascripts/common.js +++ b/superset/assets/javascripts/common.js @@ -10,6 +10,17 @@ $(document).ready(function () { const id = $this.attr('id'); utils.toggleCheckbox(prefix, '#' + id); }); + + // for language picker dropdown + $('#language-picker a').click(function (ev) { + ev.preventDefault(); + + const targetUrl = ev.currentTarget.href; + $.ajax(targetUrl) + .then(() => { + location.reload(); + }); + }); }); export function appSetup() { diff --git a/superset/assets/javascripts/components/AsyncSelect.jsx b/superset/assets/javascripts/components/AsyncSelect.jsx index e045dc973c987..007281a116a04 100644 --- a/superset/assets/javascripts/components/AsyncSelect.jsx +++ b/superset/assets/javascripts/components/AsyncSelect.jsx @@ -1,6 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import Select from 'react-select'; +import { t } from '../locales'; const $ = window.$ = require('jquery'); @@ -16,7 +17,7 @@ const propTypes = { }; const defaultProps = { - placeholder: 'Select ...', + placeholder: t('Select ...'), valueRenderer: o => (
{o.label}
), onAsyncError: () => {}, }; diff --git a/superset/assets/javascripts/components/CachedLabel.jsx b/superset/assets/javascripts/components/CachedLabel.jsx index e78e4f7996a81..8d0c0f2f2a740 100644 --- a/superset/assets/javascripts/components/CachedLabel.jsx +++ b/superset/assets/javascripts/components/CachedLabel.jsx @@ -3,6 +3,7 @@ import PropTypes from 'prop-types'; import { Label } from 'react-bootstrap'; import moment from 'moment'; import TooltipWrapper from './TooltipWrapper'; +import { t } from '../locales'; const propTypes = { onClick: PropTypes.func, @@ -22,14 +23,14 @@ class CacheLabel extends React.PureComponent { updateTooltipContent() { const cachedText = this.props.cachedTimestamp ? ( - Loaded data cached {moment.utc(this.props.cachedTimestamp).fromNow()} + t('Loaded data cached') {moment.utc(this.props.cachedTimestamp).fromNow()} ) : - 'Loaded from cache'; + t('Loaded from cache'); const tooltipContent = ( {cachedText}. - Click to force-refresh + {t('Click to force-refresh')} ); this.setState({ tooltipContent }); diff --git a/superset/assets/javascripts/components/CopyToClipboard.jsx b/superset/assets/javascripts/components/CopyToClipboard.jsx index c9120b2b3ef28..d00347d998987 100644 --- a/superset/assets/javascripts/components/CopyToClipboard.jsx +++ b/superset/assets/javascripts/components/CopyToClipboard.jsx @@ -1,6 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { Tooltip, OverlayTrigger, MenuItem } from 'react-bootstrap'; +import { t } from '../locales'; const propTypes = { copyNode: PropTypes.node, @@ -17,7 +18,7 @@ const defaultProps = { onCopyEnd: () => {}, shouldShowText: true, inMenu: false, - tooltipText: 'Copy to clipboard', + tooltipText: t('Copy to clipboard'), }; export default class CopyToClipboard extends React.Component { @@ -61,10 +62,10 @@ export default class CopyToClipboard extends React.Component { textArea.select(); try { if (!document.execCommand('copy')) { - throw new Error('Not successful'); + throw new Error(t('Not successful')); } } catch (err) { - window.alert('Sorry, your browser does not support copying. Use Ctrl / Cmd + C!'); // eslint-disable-line + window.alert(t('Sorry, your browser does not support copying. Use Ctrl / Cmd + C!')); // eslint-disable-line } document.body.removeChild(textArea); @@ -75,7 +76,7 @@ export default class CopyToClipboard extends React.Component { tooltipText() { if (this.state.hasCopied) { - return 'Copied!'; + return t('Copied!'); } return this.props.tooltipText; } diff --git a/superset/assets/javascripts/components/EditableTitle.jsx b/superset/assets/javascripts/components/EditableTitle.jsx index 9d71388828ad2..31c4c53c96e03 100644 --- a/superset/assets/javascripts/components/EditableTitle.jsx +++ b/superset/assets/javascripts/components/EditableTitle.jsx @@ -1,14 +1,16 @@ import React from 'react'; import PropTypes from 'prop-types'; import TooltipWrapper from './TooltipWrapper'; +import { t } from '../locales'; const propTypes = { title: PropTypes.string, canEdit: PropTypes.bool, - onSaveTitle: PropTypes.func.isRequired, + onSaveTitle: PropTypes.func, + noPermitTooltip: PropTypes.string, }; const defaultProps = { - title: 'Title', + title: t('Title'), canEdit: false, }; @@ -25,6 +27,14 @@ class EditableTitle extends React.PureComponent { this.handleChange = this.handleChange.bind(this); this.handleKeyPress = this.handleKeyPress.bind(this); } + componentWillReceiveProps(nextProps) { + if (nextProps.title !== this.state.title) { + this.setState({ + lastTitle: this.state.title, + title: nextProps.title, + }); + } + } handleClick() { if (!this.props.canEdit) { return; @@ -43,6 +53,14 @@ class EditableTitle extends React.PureComponent { isEditing: false, }); + if (!this.state.title.length) { + this.setState({ + title: this.state.lastTitle, + }); + + return; + } + if (this.state.lastTitle !== this.state.title) { this.setState({ lastTitle: this.state.title, @@ -71,7 +89,8 @@ class EditableTitle extends React.PureComponent { {tooltip}} + overlay={ + + {tooltip} + + } > + {metric.warning_text && + + }
); } MetricOption.propTypes = propTypes; diff --git a/superset/assets/javascripts/dashboard/Dashboard.jsx b/superset/assets/javascripts/dashboard/Dashboard.jsx index 8a7000ea8b3fc..eb471dab27d9d 100644 --- a/superset/assets/javascripts/dashboard/Dashboard.jsx +++ b/superset/assets/javascripts/dashboard/Dashboard.jsx @@ -8,7 +8,7 @@ import GridLayout from './components/GridLayout'; import Header from './components/Header'; import { appSetup } from '../common'; import AlertsWrapper from '../components/AlertsWrapper'; - +import { t } from '../locales'; import '../../stylesheets/dashboard.css'; const superset = require('../modules/superset'); @@ -39,7 +39,7 @@ export function getInitialState(boostrapData) { } function unload() { - const message = 'You have unsaved changes.'; + const message = t('You have unsaved changes.'); window.event.returnValue = message; // Gecko + IE return message; // Gecko + Webkit, Safari, Chrome etc. } @@ -56,9 +56,9 @@ function renderAlert() { render(
- You have unsaved changes. Click the  + {t('You have unsaved changes.')} {t('Click the')}     - button on the top right to save your changes. + {t('button on the top right to save your changes.')}
, document.getElementById('alert-container'), @@ -161,13 +161,12 @@ export function dashboardContainer(dashboard, datasources, userid) { .addClass('danger') .attr( 'title', - `Served from data cached ${cachedWhen}. ` + - 'Click to force refresh') + t('Served from data cached %s . Click to force refresh.', cachedWhen)) .tooltip('fixTitle'); } else { refresh .removeClass('danger') - .attr('title', 'Click to force refresh') + .attr('title', t('Click to force refresh')) .tooltip('fixTitle'); } }, @@ -351,8 +350,8 @@ export function dashboardContainer(dashboard, datasources, userid) { error(error) { const errorMsg = getAjaxErrorMsg(error); utils.showModal({ - title: 'Error', - body: 'Sorry, there was an error adding slices to this dashboard: ' + errorMsg, + title: t('Error'), + body: t('Sorry, there was an error adding slices to this dashboard: %s', errorMsg), }); }, }); diff --git a/superset/assets/javascripts/dashboard/components/CodeModal.jsx b/superset/assets/javascripts/dashboard/components/CodeModal.jsx index 77f2dafea08a0..f9c1535a7708a 100644 --- a/superset/assets/javascripts/dashboard/components/CodeModal.jsx +++ b/superset/assets/javascripts/dashboard/components/CodeModal.jsx @@ -2,6 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import ModalTrigger from '../../components/ModalTrigger'; +import { t } from '../../locales'; const propTypes = { triggerNode: PropTypes.node.isRequired, @@ -31,7 +32,7 @@ export default class CodeModal extends React.PureComponent { triggerNode={this.props.triggerNode} isButton beforeOpen={this.beforeOpen.bind(this)} - modalTitle="Active Dashboard Filters" + modalTitle={t('Active Dashboard Filters')} modalBody={
diff --git a/superset/assets/javascripts/dashboard/components/Controls.jsx b/superset/assets/javascripts/dashboard/components/Controls.jsx
index 1169642ff60e5..e18c2701fee9b 100644
--- a/superset/assets/javascripts/dashboard/components/Controls.jsx
+++ b/superset/assets/javascripts/dashboard/components/Controls.jsx
@@ -8,6 +8,7 @@ import RefreshIntervalModal from './RefreshIntervalModal';
 import SaveModal from './SaveModal';
 import CodeModal from './CodeModal';
 import SliceAdder from './SliceAdder';
+import { t } from '../../locales';
 
 const $ = window.$ = require('jquery');
 
@@ -44,13 +45,13 @@ class Controls extends React.PureComponent {
   }
   render() {
     const dashboard = this.props.dashboard;
-    const emailBody = `Checkout this dashboard: ${window.location.href}`;
+    const emailBody = t('Checkout this dashboard: %s', window.location.href);
     const emailLink = 'mailto:?Subject=Superset%20Dashboard%20'
       + `${dashboard.dashboard_title}&Body=${emailBody}`;
     return (
       
         
diff --git a/superset/assets/javascripts/dashboard/components/CssEditor.jsx b/superset/assets/javascripts/dashboard/components/CssEditor.jsx
index b77ab9d0ff5e6..bbcc19f078604 100644
--- a/superset/assets/javascripts/dashboard/components/CssEditor.jsx
+++ b/superset/assets/javascripts/dashboard/components/CssEditor.jsx
@@ -7,6 +7,7 @@ import 'brace/mode/css';
 import 'brace/theme/github';
 
 import ModalTrigger from '../../components/ModalTrigger';
+import { t } from '../../locales';
 
 const propTypes = {
   initialCss: PropTypes.string,
@@ -61,10 +62,10 @@ class CssEditor extends React.PureComponent {
     if (this.props.templates) {
       return (
         
-
Load a template
+
{t('Load a template')}
' + errorMsg); + notify.error(t('Sorry, there was an error saving this dashboard: ') + '' + errorMsg); }, }); } @@ -96,8 +97,8 @@ class SaveModal extends React.PureComponent { if (!newDashboardTitle) { this.modal.close(); showModal({ - title: 'Error', - body: 'You must pick a name for the new dashboard', + title: t('Error'), + body: t('You must pick a name for the new dashboard'), }); } else { data.dashboard_title = newDashboardTitle; @@ -111,7 +112,7 @@ class SaveModal extends React.PureComponent { { this.modal = modal; }} triggerNode={this.props.triggerNode} - modalTitle="Save Dashboard" + modalTitle={t('Save Dashboard')} modalBody={ - Overwrite Dashboard [{this.props.dashboard.dashboard_title}] + {t('Overwrite Dashboard [%s]', this.props.dashboard.dashboard_title)}
- Save as: + {t('Save as:')} { this.saveDashboard(this.state.saveType, this.state.newDashName); }} > - Save + {t('Save')}
} diff --git a/superset/assets/javascripts/dashboard/components/SliceAdder.jsx b/superset/assets/javascripts/dashboard/components/SliceAdder.jsx index 9d8965cce42c6..4c5f462a02ba9 100644 --- a/superset/assets/javascripts/dashboard/components/SliceAdder.jsx +++ b/superset/assets/javascripts/dashboard/components/SliceAdder.jsx @@ -4,6 +4,7 @@ import PropTypes from 'prop-types'; import { BootstrapTable, TableHeaderColumn } from 'react-bootstrap-table'; import ModalTrigger from '../../components/ModalTrigger'; +import { t } from '../../locales'; require('react-bootstrap-table/css/react-bootstrap-table.css'); @@ -138,13 +139,13 @@ class SliceAdder extends React.Component { dataField="sliceName" dataSort > - Name + {t('Name')} - Viz + {t('Viz')} modified} > - Modified + {t('Modified')}
@@ -172,12 +173,12 @@ class SliceAdder extends React.Component { return ( ); } diff --git a/superset/assets/javascripts/dashboard/components/SliceCell.jsx b/superset/assets/javascripts/dashboard/components/SliceCell.jsx index 0a179034825bf..2fbdff31ba030 100644 --- a/superset/assets/javascripts/dashboard/components/SliceCell.jsx +++ b/superset/assets/javascripts/dashboard/components/SliceCell.jsx @@ -2,32 +2,46 @@ import React from 'react'; import PropTypes from 'prop-types'; +import { t } from '../../locales'; import { getExploreUrl } from '../../explore/exploreUtils'; +import EditableTitle from '../../components/EditableTitle'; const propTypes = { slice: PropTypes.object.isRequired, removeSlice: PropTypes.func.isRequired, + updateSliceName: PropTypes.func, expandedSlices: PropTypes.object, }; -function SliceCell({ expandedSlices, removeSlice, slice }) { +const SliceCell = ({ expandedSlices, removeSlice, slice, updateSliceName }) => { + const onSaveTitle = (newTitle) => { + if (updateSliceName) { + updateSliceName(slice.slice_id, newTitle); + } + }; + return (
-
-
-
- {slice.slice_name} +
+
+
+
-
+
); -} +}; SliceCell.propTypes = propTypes; diff --git a/superset/assets/javascripts/explore/actions/exploreActions.js b/superset/assets/javascripts/explore/actions/exploreActions.js index 160828229786f..dbba7b7fbed73 100644 --- a/superset/assets/javascripts/explore/actions/exploreActions.js +++ b/superset/assets/javascripts/explore/actions/exploreActions.js @@ -150,3 +150,8 @@ export const RENDER_TRIGGERED = 'RENDER_TRIGGERED'; export function renderTriggered() { return { type: RENDER_TRIGGERED }; } + +export const CREATE_NEW_SLICE = 'CREATE_NEW_SLICE'; +export function createNewSlice(can_add, can_download, can_overwrite, slice, form_data) { + return { type: CREATE_NEW_SLICE, can_add, can_download, can_overwrite, slice, form_data }; +} diff --git a/superset/assets/javascripts/explore/components/ChartContainer.jsx b/superset/assets/javascripts/explore/components/ChartContainer.jsx index 1dc13455d8650..f3c660adf66bc 100644 --- a/superset/assets/javascripts/explore/components/ChartContainer.jsx +++ b/superset/assets/javascripts/explore/components/ChartContainer.jsx @@ -14,6 +14,7 @@ import Timer from '../../components/Timer'; import { getExploreUrl } from '../exploreUtils'; import { getFormDataFromControls } from '../stores/store'; import CachedLabel from '../../components/CachedLabel'; +import { t } from '../../locales'; const CHART_STATUS_MAP = { failed: 'danger', @@ -152,15 +153,22 @@ class ChartContainer extends React.PureComponent { this.props.actions.runQuery(this.props.formData, true, this.props.timeout); } - updateChartTitle(newTitle) { + updateChartTitleOrSaveSlice(newTitle) { + const isNewSlice = !this.props.slice; const params = { slice_name: newTitle, - action: 'overwrite', + action: isNewSlice ? 'saveas' : 'overwrite', }; const saveUrl = getExploreUrl(this.props.formData, 'base', false, null, params); this.props.actions.saveSlice(saveUrl) - .then(() => { - this.props.actions.updateChartTitle(newTitle); + .then((data) => { + if (isNewSlice) { + this.props.actions.createNewSlice( + data.can_add, data.can_download, data.can_overwrite, + data.slice, data.form_data); + } else { + this.props.actions.updateChartTitle(newTitle); + } }); } @@ -169,7 +177,7 @@ class ChartContainer extends React.PureComponent { if (this.props.slice) { title = this.props.slice.slice_name; } else { - title = `[${this.props.table_name}] - untitled`; + title = t('%s - untitled', this.props.table_name); } return title; } @@ -262,8 +270,8 @@ class ChartContainer extends React.PureComponent { > {this.props.slice && @@ -276,7 +284,7 @@ class ChartContainer extends React.PureComponent { @@ -38,7 +39,7 @@ export default class ControlHeader extends React.Component { {this.props.renderTrigger && { this.setState({ - error: data.responseJSON ? data.responseJSON.error : 'Error...', + error: data.responseJSON ? data.responseJSON.error : t('Error...'), isLoading: false, }); }, @@ -93,7 +94,7 @@ export default class DisplayQueryButton extends React.PureComponent { animation={this.props.animation} isButton triggerNode={View Query} - modalTitle="Query" + modalTitle={t('Query')} bsSize="large" beforeOpen={this.beforeOpen} modalBody={this.renderModalBody()} diff --git a/superset/assets/javascripts/explore/components/EmbedCodeButton.jsx b/superset/assets/javascripts/explore/components/EmbedCodeButton.jsx index 4de5d94831337..c5615dc25abb5 100644 --- a/superset/assets/javascripts/explore/components/EmbedCodeButton.jsx +++ b/superset/assets/javascripts/explore/components/EmbedCodeButton.jsx @@ -2,6 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { Popover, OverlayTrigger } from 'react-bootstrap'; import CopyToClipboard from './../../components/CopyToClipboard'; +import { t } from '../../locales'; const propTypes = { slice: PropTypes.object.isRequired, @@ -63,7 +64,7 @@ export default class EmbedCodeButton extends React.Component { } + copyNode={} />
@@ -72,7 +73,7 @@ export default class EmbedCodeButton extends React.Component {
- +
- + @@ -38,7 +39,7 @@ export default function ExploreActionButtons({ diff --git a/superset/assets/javascripts/explore/components/SaveModal.jsx b/superset/assets/javascripts/explore/components/SaveModal.jsx index 45f5ac138ff2e..9de835381b243 100644 --- a/superset/assets/javascripts/explore/components/SaveModal.jsx +++ b/superset/assets/javascripts/explore/components/SaveModal.jsx @@ -6,6 +6,7 @@ import { connect } from 'react-redux'; import { Modal, Alert, Button, Radio } from 'react-bootstrap'; import Select from 'react-select'; import { getExploreUrl } from '../exploreUtils'; +import { t } from '../../locales'; const propTypes = { can_overwrite: PropTypes.bool, @@ -70,7 +71,7 @@ class SaveModal extends React.Component { if (sliceParams.action === 'saveas') { sliceName = this.state.newSliceName; if (sliceName === '') { - this.setState({ alert: 'Please enter a slice name' }); + this.setState({ alert: t('Please enter a slice name') }); return; } sliceParams.slice_name = sliceName; @@ -85,7 +86,7 @@ class SaveModal extends React.Component { case ('existing'): dashboard = this.state.saveToDashboardId; if (!dashboard) { - this.setState({ alert: 'Please select a dashboard' }); + this.setState({ alert: t('Please select a dashboard') }); return; } sliceParams.save_to_dashboard_id = dashboard; @@ -93,7 +94,7 @@ class SaveModal extends React.Component { case ('new'): dashboard = this.state.newDashboardName; if (dashboard === '') { - this.setState({ alert: 'Please enter a dashboard name' }); + this.setState({ alert: t('Please enter a dashboard name') }); return; } sliceParams.new_dashboard_name = dashboard; @@ -107,7 +108,7 @@ class SaveModal extends React.Component { this.props.actions.saveSlice(saveUrl) .then((data) => { // Go to new slice url or dashboard url - window.location = data; + window.location = data.slice.slice_url; }); this.props.onHide(); } @@ -126,7 +127,7 @@ class SaveModal extends React.Component { > - Save A Slice + {t('Save A Slice')} @@ -147,7 +148,7 @@ class SaveModal extends React.Component { checked={this.state.action === 'overwrite'} onChange={this.changeAction.bind(this, 'overwrite')} > - {`Overwrite slice ${this.props.slice.slice_name}`} + {t('Overwrite slice %s', this.props.slice.slice_name)} } @@ -156,11 +157,11 @@ class SaveModal extends React.Component { inline checked={this.state.action === 'saveas'} onChange={this.changeAction.bind(this, 'saveas')} - > Save as   + > {t('Save as')}   @@ -173,7 +174,7 @@ class SaveModal extends React.Component { checked={this.state.addToDash === 'noSave'} onChange={this.changeDash.bind(this, 'noSave')} > - Do not add to a dashboard + {t('Do not add to a dashboard')} - Add slice to existing dashboard + {t('Add slice to existing dashboard')} + @@ -212,7 +215,7 @@ class SaveModal extends React.Component { className="btn pull-left" onClick={this.saveOrOverwrite.bind(this, false)} > - Save + {t('Save')} diff --git a/superset/assets/javascripts/explore/components/URLShortLinkButton.jsx b/superset/assets/javascripts/explore/components/URLShortLinkButton.jsx index 4dbf0f1103a4e..ddae9a2206429 100644 --- a/superset/assets/javascripts/explore/components/URLShortLinkButton.jsx +++ b/superset/assets/javascripts/explore/components/URLShortLinkButton.jsx @@ -3,6 +3,7 @@ import PropTypes from 'prop-types'; import { Popover, OverlayTrigger } from 'react-bootstrap'; import CopyToClipboard from './../../components/CopyToClipboard'; import { getShortUrl } from '../../../utils/common'; +import { t } from '../../locales'; const propTypes = { slice: PropTypes.object.isRequired, @@ -28,12 +29,12 @@ export default class URLShortLinkButton extends React.Component { } renderPopover() { - const emailBody = `Check out this slice: ${this.state.shortUrl}`; + const emailBody = t('Check out this slice: %s', this.state.shortUrl); return ( } + copyNode={} />    diff --git a/superset/assets/javascripts/explore/components/controls/BoundsControl.jsx b/superset/assets/javascripts/explore/components/controls/BoundsControl.jsx index 313ab93ae3f8e..776f7a499bde0 100644 --- a/superset/assets/javascripts/explore/components/controls/BoundsControl.jsx +++ b/superset/assets/javascripts/explore/components/controls/BoundsControl.jsx @@ -2,6 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { Col, Row, FormGroup, FormControl } from 'react-bootstrap'; import ControlHeader from '../ControlHeader'; +import { t } from '../../../locales'; const propTypes = { name: PropTypes.string.isRequired, @@ -51,10 +52,10 @@ export default class BoundsControl extends React.Component { const mm = this.state.minMax; const errors = []; if (mm[0] && isNaN(mm[0])) { - errors.push('`Min` value should be numeric or empty'); + errors.push(t('`Min` value should be numeric or empty')); } if (mm[1] && isNaN(mm[1])) { - errors.push('`Max` value should be numeric or empty'); + errors.push(t('`Max` value should be numeric or empty')); } if (errors.length === 0) { this.props.onChange([parseFloat(mm[0]), parseFloat(mm[1])], errors); @@ -71,7 +72,7 @@ export default class BoundsControl extends React.Component { @@ -79,7 +80,7 @@ export default class BoundsControl extends React.Component { diff --git a/superset/assets/javascripts/explore/components/controls/DatasourceControl.jsx b/superset/assets/javascripts/explore/components/controls/DatasourceControl.jsx index b00fe3fc79368..eb7a63367cf0f 100644 --- a/superset/assets/javascripts/explore/components/controls/DatasourceControl.jsx +++ b/superset/assets/javascripts/explore/components/controls/DatasourceControl.jsx @@ -5,6 +5,7 @@ import { Table } from 'reactable'; import { Label, FormControl, Modal, OverlayTrigger, Tooltip } from 'react-bootstrap'; import ControlHeader from '../ControlHeader'; +import { t } from '../../../locales'; const propTypes = { description: PropTypes.string, @@ -66,7 +67,7 @@ export default class DatasourceControl extends React.PureComponent { }, error() { that.setState({ loading: false }); - notify.error('Something went wrong while fetching the datasource list'); + notify.error(t('Something went wrong while fetching the datasource list')); }, }); } @@ -91,7 +92,7 @@ export default class DatasourceControl extends React.PureComponent { Click to point to another datasource + {t('Click to point to another datasource')} } >
}> Created Content
+
{t('Created Content')}
} > - Recent Activity
}> + {t('Recent Activity')}
}> - Security & Access
}> + {t('Security & Access')}
}> diff --git a/superset/assets/javascripts/profile/components/CreatedContent.jsx b/superset/assets/javascripts/profile/components/CreatedContent.jsx index 87921c6872e6c..895be78434ce9 100644 --- a/superset/assets/javascripts/profile/components/CreatedContent.jsx +++ b/superset/assets/javascripts/profile/components/CreatedContent.jsx @@ -2,6 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import moment from 'moment'; import TableLoader from './TableLoader'; +import { t } from '../../locales'; const propTypes = { user: PropTypes.object.isRequired, @@ -29,7 +30,7 @@ class CreatedContent extends React.PureComponent { className="table table-condensed" columns={['slice', 'favorited']} mutator={mutator} - noDataText="No slices" + noDataText={t('No slices')} sortable /> ); @@ -45,7 +46,7 @@ class CreatedContent extends React.PureComponent { className="table table-condensed" mutator={mutator} dataEndpoint={`/superset/created_dashboards/${this.props.user.userId}/`} - noDataText="No dashboards" + noDataText={t('No dashboards')} columns={['dashboard', 'favorited']} sortable /> @@ -54,10 +55,10 @@ class CreatedContent extends React.PureComponent { render() { return (
-

Dashboards

+

{t('Dashboards')}

{this.renderDashboardTable()}
-

Slices

+

{t('Slices')}

{this.renderSliceTable()}
); diff --git a/superset/assets/javascripts/profile/components/Favorites.jsx b/superset/assets/javascripts/profile/components/Favorites.jsx index 9039d916518b4..3141bb0c32850 100644 --- a/superset/assets/javascripts/profile/components/Favorites.jsx +++ b/superset/assets/javascripts/profile/components/Favorites.jsx @@ -2,6 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import moment from 'moment'; import TableLoader from './TableLoader'; +import { t } from '../../locales'; const propTypes = { user: PropTypes.object.isRequired, @@ -30,7 +31,7 @@ export default class Favorites extends React.PureComponent { className="table table-condensed" columns={['slice', 'creator', 'favorited']} mutator={mutator} - noDataText="No favorite slices yet, go click on stars!" + noDataText={t('No favorite slices yet, go click on stars!')} sortable /> ); @@ -46,7 +47,7 @@ export default class Favorites extends React.PureComponent { className="table table-condensed" mutator={mutator} dataEndpoint={`/superset/fave_dashboards/${this.props.user.userId}/`} - noDataText="No favorite dashboards yet, go click on stars!" + noDataText={t('No favorite dashboards yet, go click on stars!')} columns={['dashboard', 'creator', 'favorited']} sortable /> @@ -55,10 +56,10 @@ export default class Favorites extends React.PureComponent { render() { return (
-

Dashboards

+

{t('Dashboards')}

{this.renderDashboardTable()}
-

Slices

+

{t('Slices')}

{this.renderSliceTable()}
); diff --git a/superset/assets/javascripts/profile/components/Security.jsx b/superset/assets/javascripts/profile/components/Security.jsx index 0d942dd83fdee..748be6b84043c 100644 --- a/superset/assets/javascripts/profile/components/Security.jsx +++ b/superset/assets/javascripts/profile/components/Security.jsx @@ -1,6 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { Badge, Label } from 'react-bootstrap'; +import { t } from '../../locales'; const propTypes = { user: PropTypes.object.isRequired, @@ -10,7 +11,7 @@ export default function Security({ user }) {

- Roles {Object.keys(user.roles).length} + {t('Roles')} {Object.keys(user.roles).length}

{Object.keys(user.roles).map(role => )}
@@ -19,7 +20,7 @@ export default function Security({ user }) { {user.permissions.database_access &&

- Databases {user.permissions.database_access.length} + {t('Databases')} {user.permissions.database_access.length}

{user.permissions.database_access.map(role => )}
@@ -30,7 +31,7 @@ export default function Security({ user }) { {user.permissions.datasource_access &&

- Datasources {user.permissions.datasource_access.length} + {t('Datasources')} {user.permissions.datasource_access.length}

{user.permissions.datasource_access.map(role => )}
diff --git a/superset/assets/javascripts/profile/components/UserInfo.jsx b/superset/assets/javascripts/profile/components/UserInfo.jsx index 4f751ed0d2391..cf9bde717bb6e 100644 --- a/superset/assets/javascripts/profile/components/UserInfo.jsx +++ b/superset/assets/javascripts/profile/components/UserInfo.jsx @@ -3,6 +3,7 @@ import PropTypes from 'prop-types'; import Gravatar from 'react-gravatar'; import moment from 'moment'; import { Panel } from 'react-bootstrap'; +import { t } from '../../locales'; const propTypes = { user: PropTypes.object.isRequired, @@ -14,7 +15,7 @@ const UserInfo = ({ user }) => ( email={user.email} width="100%" height="" - alt="Profile picture provided by Gravatar" + alt={t('Profile picture provided by Gravatar')} className="img-rounded" style={{ borderRadius: 15 }} /> @@ -29,7 +30,7 @@ const UserInfo = ({ user }) => (

- joined {moment(user.createdOn, 'YYYYMMDD').fromNow()} + {t('joined')} {moment(user.createdOn, 'YYYYMMDD').fromNow()}

{user.email} @@ -39,7 +40,7 @@ const UserInfo = ({ user }) => (

  - id:  + {t('id:')}  {user.userId}

diff --git a/superset/assets/javascripts/profile/index.jsx b/superset/assets/javascripts/profile/index.jsx index adf371e699768..e9ed59bd719b2 100644 --- a/superset/assets/javascripts/profile/index.jsx +++ b/superset/assets/javascripts/profile/index.jsx @@ -1,7 +1,7 @@ /* eslint no-unused-vars: 0 */ import React from 'react'; import ReactDOM from 'react-dom'; -import { Badge, Col, Label, Row, Tabs, Tab, Panel } from 'react-bootstrap'; + import App from './components/App'; import { appSetup } from '../common'; diff --git a/superset/assets/package.json b/superset/assets/package.json index 002d0e8e39ca3..3b1c53b07e8ee 100644 --- a/superset/assets/package.json +++ b/superset/assets/package.json @@ -53,7 +53,10 @@ "d3-tip": "^0.6.7", "datamaps": "^0.5.8", "datatables.net-bs": "^1.10.15", + "distributions": "^1.0.0", "immutable": "^3.8.1", + "jed": "^1.1.1", + "po2json": "^0.4.5", "jquery": "3.1.1", "lodash.throttle": "^4.1.1", "moment": "^2.14.1", @@ -85,6 +88,7 @@ "redux-localstorage": "^0.4.1", "redux-thunk": "^2.1.0", "shortid": "^2.2.6", + "sprintf-js": "^1.1.1", "supercluster": "https://github.com/georgeke/supercluster/tarball/ac3492737e7ce98e07af679623aad452373bbc40", "urijs": "^1.18.10", "viewport-mercator-project": "^2.1.0" diff --git a/superset/assets/spec/javascripts/components/AsyncSelect_spec.jsx b/superset/assets/spec/javascripts/components/AsyncSelect_spec.jsx index c08cc664197c9..35b6f8141108c 100644 --- a/superset/assets/spec/javascripts/components/AsyncSelect_spec.jsx +++ b/superset/assets/spec/javascripts/components/AsyncSelect_spec.jsx @@ -1,6 +1,6 @@ import React from 'react'; import Select from 'react-select'; -import { mount, shallow } from 'enzyme'; +import { shallow } from 'enzyme'; import { describe, it } from 'mocha'; import { expect } from 'chai'; import sinon from 'sinon'; @@ -11,10 +11,12 @@ describe('AsyncSelect', () => { const mockedProps = { dataEndpoint: '/slicemodelview/api/read', onChange: sinon.spy(), + placeholder: 'Select...', mutator: () => [ { value: 1, label: 'main' }, { value: 2, label: 'another' }, ], + valueRenderer: opt => opt.label, }; it('is valid element', () => { expect( @@ -49,34 +51,34 @@ describe('AsyncSelect', () => { server.restore(); }); it('should be off by default', () => { - const wrapper = mount( + const wrapper = shallow( , ); + wrapper.instance().fetchOptions(); const spy = sinon.spy(wrapper.instance(), 'onChange'); expect(spy.callCount).to.equal(0); }); it('should auto select first option', () => { - const wrapper = mount( + const wrapper = shallow( , ); const spy = sinon.spy(wrapper.instance(), 'onChange'); - + wrapper.instance().fetchOptions(); server.respond(); expect(spy.callCount).to.equal(1); expect(spy.calledWith(wrapper.instance().state.options[0])).to.equal(true); }); it('should not auto select when value prop is set', () => { - const wrapper = mount( + const wrapper = shallow( , ); const spy = sinon.spy(wrapper.instance(), 'onChange'); - + wrapper.instance().fetchOptions(); server.respond(); expect(spy.callCount).to.equal(0); expect(wrapper.find(Select)).to.have.length(1); - expect(wrapper.find('.Select-value-label').children().first().text()).to.equal('another'); }); }); }); diff --git a/superset/assets/spec/javascripts/components/MetricOption_spec.jsx b/superset/assets/spec/javascripts/components/MetricOption_spec.jsx index f3fc26e243173..952e9fa125b15 100644 --- a/superset/assets/spec/javascripts/components/MetricOption_spec.jsx +++ b/superset/assets/spec/javascripts/components/MetricOption_spec.jsx @@ -13,6 +13,7 @@ describe('MetricOption', () => { verbose_name: 'Foo', expression: 'SUM(foo)', description: 'Foo is the greatest metric of all', + warning_text: 'Be careful when using foo', }, }; @@ -31,17 +32,22 @@ describe('MetricOption', () => { expect(lbl).to.have.length(1); expect(lbl.first().text()).to.equal('Foo'); }); - it('shows 2 InfoTooltipWithTrigger', () => { - expect(wrapper.find(InfoTooltipWithTrigger)).to.have.length(2); + it('shows 3 InfoTooltipWithTrigger', () => { + expect(wrapper.find(InfoTooltipWithTrigger)).to.have.length(3); }); - it('shows only 1 InfoTooltipWithTrigger when no descr', () => { + it('shows only 2 InfoTooltipWithTrigger when no descr', () => { props.metric.description = null; wrapper = shallow(factory(props)); - expect(wrapper.find(InfoTooltipWithTrigger)).to.have.length(1); + expect(wrapper.find(InfoTooltipWithTrigger)).to.have.length(2); }); it('shows a label with metric_name when no verbose_name', () => { props.metric.verbose_name = null; wrapper = shallow(factory(props)); expect(wrapper.find('.option-label').first().text()).to.equal('foo'); }); + it('shows only 1 InfoTooltipWithTrigger when no descr and no warning', () => { + props.metric.warning_text = null; + wrapper = shallow(factory(props)); + expect(wrapper.find(InfoTooltipWithTrigger)).to.have.length(1); + }); }); diff --git a/superset/assets/spec/javascripts/profile/EditableTitle_spec.jsx b/superset/assets/spec/javascripts/profile/EditableTitle_spec.jsx index edce86a0245a9..9a9e55148a964 100644 --- a/superset/assets/spec/javascripts/profile/EditableTitle_spec.jsx +++ b/superset/assets/spec/javascripts/profile/EditableTitle_spec.jsx @@ -19,7 +19,7 @@ describe('EditableTitle', () => { }, }; const editableWrapper = shallow(); - const notEditableWrapper = shallow(); + const notEditableWrapper = shallow(); it('is valid', () => { expect( React.isValidElement(), @@ -81,5 +81,12 @@ describe('EditableTitle', () => { // no change expect(callback.callCount).to.equal(0); }); + it('should not save empty title', () => { + editableWrapper.setState({ title: '' }); + editableWrapper.find('input').simulate('blur'); + expect(editableWrapper.find('input').props().type).to.equal('button'); + expect(editableWrapper.find('input').props().value).to.equal('my title'); + expect(callback.callCount).to.equal(0); + }); }); }); diff --git a/superset/assets/spec/javascripts/sqllab/SaveQuery_spec.jsx b/superset/assets/spec/javascripts/sqllab/SaveQuery_spec.jsx index 3c8953fbc4abd..98c8758a8db26 100644 --- a/superset/assets/spec/javascripts/sqllab/SaveQuery_spec.jsx +++ b/superset/assets/spec/javascripts/sqllab/SaveQuery_spec.jsx @@ -1,9 +1,8 @@ import React from 'react'; import { Overlay, Popover, FormControl } from 'react-bootstrap'; -import { shallow, mount } from 'enzyme'; +import { shallow } from 'enzyme'; import { describe, it } from 'mocha'; import { expect } from 'chai'; - import SaveQuery from '../../../javascripts/SqlLab/components/SaveQuery'; describe('SavedQuery', () => { @@ -30,11 +29,11 @@ describe('SavedQuery', () => { expect(wrapper.find(Popover)).to.have.length(1); }); it('pops and hides', () => { - const wrapper = mount(); + const wrapper = shallow(); expect(wrapper.state().showSave).to.equal(false); - wrapper.find('.toggleSave').simulate('click'); + wrapper.find('.toggleSave').simulate('click', { target: { value: 'test' } }); expect(wrapper.state().showSave).to.equal(true); - wrapper.find('.toggleSave').simulate('click'); + wrapper.find('.toggleSave').simulate('click', { target: { value: 'test' } }); expect(wrapper.state().showSave).to.equal(false); }); it('has a cancel button', () => { diff --git a/superset/assets/spec/javascripts/sqllab/Timer_spec.jsx b/superset/assets/spec/javascripts/sqllab/Timer_spec.jsx index 21a8a4fb3d442..e9172a928c1c2 100644 --- a/superset/assets/spec/javascripts/sqllab/Timer_spec.jsx +++ b/superset/assets/spec/javascripts/sqllab/Timer_spec.jsx @@ -2,6 +2,7 @@ import React from 'react'; import { mount } from 'enzyme'; import { describe, it, beforeEach } from 'mocha'; import { expect } from 'chai'; +import sinon from 'sinon'; import Timer from '../../../javascripts/components/Timer'; import { now } from '../../../javascripts/modules/dates'; @@ -9,16 +10,21 @@ import { now } from '../../../javascripts/modules/dates'; describe('Timer', () => { let wrapper; + let clock; const mockedProps = { - startTime: now(), endTime: null, isRunning: true, status: 'warning', }; beforeEach(() => { + clock = sinon.useFakeTimers(); + mockedProps.startTime = now() + 1; wrapper = mount(); }); + afterEach(() => { + clock.restore(); + }); it('is a valid element', () => { expect(React.isValidElement()).to.equal(true); @@ -26,9 +32,8 @@ describe('Timer', () => { it('componentWillMount starts timer after 30ms and sets state.clockStr', () => { expect(wrapper.state().clockStr).to.equal(''); - setTimeout(() => { - expect(wrapper.state().clockStr).not.equal(''); - }, 31); + clock.tick(31); + expect(wrapper.state().clockStr).not.equal(''); }); it('calls startTimer on mount', () => { diff --git a/superset/assets/spec/javascripts/sqllab/reducers_spec.js b/superset/assets/spec/javascripts/sqllab/reducers_spec.js index f777503e01308..a3a5dbf7b5771 100644 --- a/superset/assets/spec/javascripts/sqllab/reducers_spec.js +++ b/superset/assets/spec/javascripts/sqllab/reducers_spec.js @@ -1,4 +1,4 @@ -import { beforeEach, describe, it } from 'mocha'; +import { describe, it } from 'mocha'; import { expect } from 'chai'; import * as r from '../../../javascripts/SqlLab/reducers'; @@ -9,7 +9,9 @@ describe('sqlLabReducer', () => { describe('CLONE_QUERY_TO_NEW_TAB', () => { const testQuery = { sql: 'SELECT * FROM...', dbId: 1, id: 'flasj233' }; let newState = Object.assign({}, initialState, { queries: { [testQuery.id]: testQuery } }); - newState = r.sqlLabReducer(newState, actions.cloneQueryToNewTab(testQuery)); + beforeEach(() => { + newState = r.sqlLabReducer(newState, actions.cloneQueryToNewTab(testQuery)); + }); it('should have at most one more tab', () => { expect(newState.queryEditors).have.length(2); diff --git a/superset/assets/stylesheets/dashboard.css b/superset/assets/stylesheets/dashboard.css index ad84807aa14c4..fd47dec38d11c 100644 --- a/superset/assets/stylesheets/dashboard.css +++ b/superset/assets/stylesheets/dashboard.css @@ -16,7 +16,6 @@ div.widget .chart-controls { position: absolute; z-index: 100; right: 0; - left: 0; top: 5px; padding: 5px 5px; opacity: 0.75; @@ -117,6 +116,7 @@ div.widget .chart-controls { .chart-header .header { font-size: 16px; + margin: 0 -10px; } .ace_gutter { z-index: 0; diff --git a/superset/assets/stylesheets/superset.less b/superset/assets/stylesheets/superset.less index b0a4ed5cb7a4f..691dad0122b5e 100644 --- a/superset/assets/stylesheets/superset.less +++ b/superset/assets/stylesheets/superset.less @@ -152,9 +152,6 @@ img.viz-thumb-option { } -div.header { - font-weight: bold; -} li.widget:hover { z-index: 1000; } diff --git a/superset/assets/visualizations/EventFlow.jsx b/superset/assets/visualizations/EventFlow.jsx index 110f4a76482c6..83811a5f465f4 100644 --- a/superset/assets/visualizations/EventFlow.jsx +++ b/superset/assets/visualizations/EventFlow.jsx @@ -9,6 +9,7 @@ import { EVENT_NAME, ENTITY_ID, } from '@data-ui/event-flow'; +import { t } from '../javascripts/locales'; /* * This function takes the slice object and json payload as input and renders a @@ -52,7 +53,7 @@ function renderEventFlow(slice, json) { Component = ; } else { - Component =
Sorry, there appears to be no data
; + Component =
{t('Sorry, there appears to be no data')}
; } ReactDOM.render(Component, container); diff --git a/superset/assets/visualizations/filter_box.jsx b/superset/assets/visualizations/filter_box.jsx index a5de26a810811..1bcaa52fa579a 100644 --- a/superset/assets/visualizations/filter_box.jsx +++ b/superset/assets/visualizations/filter_box.jsx @@ -8,6 +8,7 @@ import { Button } from 'react-bootstrap'; import { TIME_CHOICES } from './constants'; import './filter_box.css'; +import { t } from '../javascripts/locales'; const propTypes = { origSelectedValues: PropTypes.object, @@ -102,7 +103,7 @@ class FilterBox extends React.Component {
{this.props.datasource.verbose_map[filter] || filter} b ? 1 : -1; +} + // Inspired from http://bl.ocks.org/mbostock/3074470 // https://jsfiddle.net/cyril123/h0reyumq/ function heatmapVis(slice, payload) { @@ -52,17 +56,21 @@ function heatmapVis(slice, payload) { function ordScale(k, rangeBands, sortMethod) { let domain = {}; + const actualKeys = {}; // hack to preserve type of keys when number data.forEach((d) => { - domain[d[k]] = domain[d[k]] || 0 + d.v; + domain[d[k]] = (domain[d[k]] || 0) + d.v; + actualKeys[d[k]] = d[k]; }); + // Not usgin object.keys() as it converts to strings + const keys = Object.keys(actualKeys).map(s => actualKeys[s]); if (sortMethod === 'alpha_asc') { - domain = Object.keys(domain).sort(); + domain = keys.sort(cmp); } else if (sortMethod === 'alpha_desc') { - domain = Object.keys(domain).sort().reverse(); + domain = keys.sort(cmp).reverse(); } else if (sortMethod === 'value_desc') { - domain = Object.keys(domain).sort((d1, d2) => domain[d2] - domain[d1]); + domain = Object.keys(domain).sort((a, b) => domain[a] > domain[b] ? -1 : 1); } else if (sortMethod === 'value_asc') { - domain = Object.keys(domain).sort((d1, d2) => domain[d1] - domain[d2]); + domain = Object.keys(domain).sort((a, b) => domain[b] > domain[a] ? -1 : 1); } if (k === 'y' && rangeBands) { diff --git a/superset/assets/visualizations/main.js b/superset/assets/visualizations/main.js index a02f508c33012..d5c3abb1a7e68 100644 --- a/superset/assets/visualizations/main.js +++ b/superset/assets/visualizations/main.js @@ -33,5 +33,6 @@ const vizMap = { world_map: require('./world_map.js'), dual_line: require('./nvd3_vis.js'), event_flow: require('./EventFlow.jsx'), + paired_ttest: require('./paired_ttest.jsx'), }; export default vizMap; diff --git a/superset/assets/visualizations/nvd3_vis.js b/superset/assets/visualizations/nvd3_vis.js index 831b99710e86a..2efc9ff9111a6 100644 --- a/superset/assets/visualizations/nvd3_vis.js +++ b/superset/assets/visualizations/nvd3_vis.js @@ -73,14 +73,41 @@ function getMaxLabelSize(container, axisClass) { return Math.max(...labelDimensions); } +/* eslint-disable camelcase */ +function formatLabel(column, verbose_map) { + let label; + if (verbose_map) { + if (Array.isArray(column) && column.length) { + label = verbose_map[column[0]]; + if (column.length > 1) { + label += `, ${column.slice(1).join(', ')}`; + } + } else { + label = verbose_map[column]; + } + } + return label || column; +} +/* eslint-enable camelcase */ + function nvd3Vis(slice, payload) { let chart; let colorKey = 'key'; const isExplore = $('#explore-container').length === 1; + let data; + if (payload.data) { + data = payload.data.map(x => ({ + ...x, key: formatLabel(x.key, slice.datasource.verbose_map), + })); + } else { + data = []; + } + slice.container.html(''); slice.clearError(); + // Calculates the longest label size for stretching bottom margin function calculateStretchMargins(payloadData) { let stretchMargin = 0; @@ -102,9 +129,9 @@ function nvd3Vis(slice, payload) { const barchartWidth = function () { let bars; if (fd.bar_stacked) { - bars = d3.max(payload.data, function (d) { return d.values.length; }); + bars = d3.max(data, function (d) { return d.values.length; }); } else { - bars = d3.sum(payload.data, function (d) { return d.values.length; }); + bars = d3.sum(data, function (d) { return d.values.length; }); } if (bars * minBarWidth > width) { return bars * minBarWidth; @@ -162,7 +189,7 @@ function nvd3Vis(slice, payload) { if (fd.show_bar_value) { setTimeout(function () { - addTotalBarValues(svg, chart, payload.data, stacked, fd.y_axis_format); + addTotalBarValues(svg, chart, data, stacked, fd.y_axis_format); }, animationTime); } break; @@ -179,13 +206,13 @@ function nvd3Vis(slice, payload) { stacked = fd.bar_stacked; chart.stacked(stacked); if (fd.order_bars) { - payload.data.forEach((d) => { + data.forEach((d) => { d.values.sort((a, b) => tryNumify(a.x) < tryNumify(b.x) ? -1 : 1); }); } if (fd.show_bar_value) { setTimeout(function () { - addTotalBarValues(svg, chart, payload.data, stacked, fd.y_axis_format); + addTotalBarValues(svg, chart, data, stacked, fd.y_axis_format); }, animationTime); } if (!reduceXTicks) { @@ -208,7 +235,7 @@ function nvd3Vis(slice, payload) { if (fd.pie_label_type === 'percent') { let total = 0; - payload.data.forEach((d) => { total += d.y; }); + data.forEach((d) => { total += d.y; }); chart.tooltip.valueFormatter(d => `${((d / total) * 100).toFixed()}%`); } @@ -248,7 +275,7 @@ function nvd3Vis(slice, payload) { return s; }); chart.pointRange([5, fd.max_bubble_size ** 2]); - chart.pointDomain([0, d3.max(payload.data, d => d3.max(d.values, v => v.size))]); + chart.pointDomain([0, d3.max(data, d => d3.max(d.values, v => v.size))]); break; case 'area': @@ -395,7 +422,7 @@ function nvd3Vis(slice, payload) { chart.showLegend(width > BREAKPOINTS.small); } svg - .datum(payload.data) + .datum(data) .transition().duration(500) .attr('height', height) .attr('width', width) @@ -471,7 +498,7 @@ function nvd3Vis(slice, payload) { // render chart svg - .datum(payload.data) + .datum(data) .transition().duration(500) .attr('height', height) .attr('width', width) diff --git a/superset/assets/visualizations/paired_ttest.css b/superset/assets/visualizations/paired_ttest.css new file mode 100644 index 0000000000000..0a2c1b8d28f28 --- /dev/null +++ b/superset/assets/visualizations/paired_ttest.css @@ -0,0 +1,67 @@ +.paired_ttest .scrollbar-container { + overflow: scroll; +} + +.paired-ttest-table .scrollbar-content { + padding-left: 5px; + padding-right: 5px; + margin-bottom: 0; +} + +.paired-ttest-table h1 { + margin-left: 5px; +} + +.reactable-data tr, +.reactable-header-sortable { + -webkit-transition: ease-in-out 0.1s; + transition: ease-in-out 0.1s; +} + +.reactable-data tr:hover { + background-color: #e0e0e0; +} + +.reactable-data tr .false { + color: #f44336; +} + +.reactable-data tr .true { + color: #4caf50; +} + +.reactable-data tr .control { + color: #2196f3; +} + +.reactable-data tr .invalid { + color: #ff9800; +} + +.reactable-data .control td { + background-color: #eeeeee; +} + +.reactable-header-sortable:hover, +.reactable-header-sortable:focus, +.reactable-header-sort-asc, +.reactable-header-sort-desc { + background-color: #e0e0e0; + position: relative; +} + +.reactable-header-sort-asc:after { + content: '\25bc'; + position: absolute; + right: 10px; +} + +.reactable-header-sort-desc:after { + content: '\25b2'; + position: absolute; + right: 10px; +} + +.paired-ttest-table table { + margin-bottom: 0; +} diff --git a/superset/assets/visualizations/paired_ttest.jsx b/superset/assets/visualizations/paired_ttest.jsx new file mode 100644 index 0000000000000..9febc798b0941 --- /dev/null +++ b/superset/assets/visualizations/paired_ttest.jsx @@ -0,0 +1,277 @@ +import d3 from 'd3'; +import dist from 'distributions'; + +import React from 'react'; +import { Table, Tr, Td, Thead, Th } from 'reactable'; +import ReactDOM from 'react-dom'; +import PropTypes from 'prop-types'; + +import './paired_ttest.css'; + +class TTestTable extends React.Component { + + constructor(props) { + super(props); + this.state = { + pValues: [], + liftValues: [], + control: 0, + }; + } + + componentWillMount() { + this.computeTTest(this.state.control); // initially populate table + } + + getLiftStatus(row) { + // Get a css class name for coloring + if (row === this.state.control) { + return 'control'; + } + const liftVal = this.state.liftValues[row]; + if (isNaN(liftVal) || !isFinite(liftVal)) { + return 'invalid'; // infinite or NaN values + } + return liftVal >= 0 ? 'true' : 'false'; // green on true, red on false + } + + getPValueStatus(row) { + if (row === this.state.control) { + return 'control'; + } + const pVal = this.state.pValues[row]; + if (isNaN(pVal) || !isFinite(pVal)) { + return 'invalid'; + } + return ''; // p-values won't normally be colored + } + + getSignificance(row) { + // Color significant as green, else red + if (row === this.state.control) { + return 'control'; + } + // p-values significant below set threshold + return this.state.pValues[row] <= this.props.alpha; + } + + computeLift(values, control) { + // Compute the lift value between two time series + let sumValues = 0; + let sumControl = 0; + for (let i = 0; i < values.length; i++) { + sumValues += values[i].y; + sumControl += control[i].y; + } + return (((sumValues - sumControl) / sumControl) * 100) + .toFixed(this.props.liftValPrec); + } + + computePValue(values, control) { + // Compute the p-value from Student's t-test + // between two time series + let diffSum = 0; + let diffSqSum = 0; + let finiteCount = 0; + for (let i = 0; i < values.length; i++) { + const diff = control[i].y - values[i].y; + if (global.isFinite(diff)) { + finiteCount++; + diffSum += diff; + diffSqSum += diff * diff; + } + } + const tvalue = -Math.abs(diffSum * + Math.sqrt((finiteCount - 1) / + (finiteCount * diffSqSum - diffSum * diffSum))); + try { + return (2 * new dist.Studentt(finiteCount - 1).cdf(tvalue)) + .toFixed(this.props.pValPrec); // two-sided test + } catch (err) { + return NaN; + } + } + + computeTTest(control) { + // Compute lift and p-values for each row + // against the selected control + const data = this.props.data; + const pValues = []; + const liftValues = []; + if (!data) { + return; + } + for (let i = 0; i < data.length; i++) { + if (i === control) { + pValues.push('control'); + liftValues.push('control'); + } else { + pValues.push(this.computePValue(data[i].values, data[control].values)); + liftValues.push(this.computeLift(data[i].values, data[control].values)); + } + } + this.setState({ pValues, liftValues, control }); + } + + render() { + const data = this.props.data; + const metric = this.props.metric; + const groups = this.props.groups; + // Render column header for each group + const columns = groups.map((group, i) => ( + {group} + )); + const numGroups = groups.length; + // Columns for p-value, lift-value, and significance (true/false) + columns.push(p-value); + columns.push(Lift %); + columns.push(Significant); + const rows = data.map((entry, i) => { + const values = groups.map((group, j) => ( // group names + + )); + values.push( + , + ); + values.push( + , + ); + values.push( + , + ); + return ( + + {values} + + ); + }); + // When sorted ascending, 'control' will always be at top + const sortConfig = groups.concat([ + { + column: 'pValue', + sortFunction: (a, b) => { + if (a === 'control') { + return -1; + } + if (b === 'control') { + return 1; + } + return a > b ? 1 : -1; // p-values ascending + }, + }, + { + column: 'liftValue', + sortFunction: (a, b) => { + if (a === 'control') { + return -1; + } + if (b === 'control') { + return 1; + } + return parseFloat(a) > parseFloat(b) ? -1 : 1; // lift values descending + }, + }, + { + column: 'significant', + sortFunction: (a, b) => { + if (a === 'control') { + return -1; + } + if (b === 'control') { + return 1; + } + return a > b ? -1 : 1; // significant values first + }, + }, + ]); + return ( +
+

{metric}

+ + + {columns} + + {rows} +
+
+ ); + } +} + +TTestTable.propTypes = { + metric: PropTypes.string.isRequired, + groups: PropTypes.array.isRequired, + data: PropTypes.array.isRequired, + alpha: PropTypes.number.isRequired, + liftValPrec: PropTypes.number.isRequired, + pValPrec: PropTypes.number.isRequired, +}; +TTestTable.defaultProps = { + metric: '', + groups: [], + data: [], + alpha: 0.05, + liftValPrec: 4, + pValPrec: 6, +}; + +function pairedTTestVis(slice, payload) { + const div = d3.select(slice.selector); + const container = slice.container; + const height = slice.container.height(); + const fd = slice.formData; + const data = payload.data; + const alpha = fd.significance_level; + const pValPrec = fd.pvalue_precision; + const liftValPrec = fd.liftvalue_precision; + const tables = fd.metrics.map((metric, i) => ( // create a table for each metric + 32 ? 32 : pValPrec} + liftValPrec={liftValPrec > 32 ? 32 : liftValPrec} + /> + )); + div.html(''); + ReactDOM.render( +
+
+
+
+ {tables} +
+
+
+
, + div.node(), + ); + container.find('.scrollbar-container').css('max-height', height); +} + +module.exports = pairedTTestVis; diff --git a/superset/config.py b/superset/config.py index d4c019cc90e86..f934e3adedf7a 100644 --- a/superset/config.py +++ b/superset/config.py @@ -159,8 +159,8 @@ LANGUAGES = { 'en': {'flag': 'us', 'name': 'English'}, 'it': {'flag': 'it', 'name': 'Italian'}, - # 'fr': {'flag': 'fr', 'name': 'French'}, - # 'zh': {'flag': 'cn', 'name': 'Chinese'}, + 'fr': {'flag': 'fr', 'name': 'French'}, + 'zh': {'flag': 'cn', 'name': 'Chinese'}, } # --------------------------------------------------- # Image and file configuration diff --git a/superset/connectors/base/models.py b/superset/connectors/base/models.py index 593c722d42bbc..14cac09aba78c 100644 --- a/superset/connectors/base/models.py +++ b/superset/connectors/base/models.py @@ -265,6 +265,7 @@ class BaseMetric(AuditMixinNullable, ImportMixin): description = Column(Text) is_restricted = Column(Boolean, default=False, nullable=True) d3format = Column(String(128)) + warning_text = Column(Text) """ The interface should also declare a datasource relationship pointing @@ -289,5 +290,7 @@ def expression(self): @property def data(self): - attrs = ('metric_name', 'verbose_name', 'description', 'expression') + attrs = ( + 'metric_name', 'verbose_name', 'description', 'expression', + 'warning_text') return {s: getattr(self, s) for s in attrs} diff --git a/superset/connectors/druid/models.py b/superset/connectors/druid/models.py index cc40b83f1f1ee..89e1ed90ccc39 100644 --- a/superset/connectors/druid/models.py +++ b/superset/connectors/druid/models.py @@ -5,12 +5,13 @@ from copy import deepcopy from datetime import datetime, timedelta from six import string_types +from multiprocessing import Pool import requests import sqlalchemy as sa from sqlalchemy import ( Column, Integer, String, ForeignKey, Text, Boolean, - DateTime, + DateTime, or_, and_, ) from sqlalchemy.orm import backref, relationship from dateutil.parser import parse as dparse @@ -39,6 +40,12 @@ DRUID_TZ = conf.get("DRUID_TZ") +# Function wrapper because bound methods cannot +# be passed to processes +def _fetch_metadata_for(datasource): + return datasource.latest_metadata() + + class JavascriptPostAggregator(Postaggregator): def __init__(self, name, field_names, function): self.post_aggregator = { @@ -101,15 +108,99 @@ def get_druid_version(self): ).format(obj=self) return json.loads(requests.get(endpoint).text)['version'] - def refresh_datasources(self, datasource_name=None, merge_flag=False): + def refresh_datasources( + self, + datasource_name=None, + merge_flag=True, + refreshAll=True): """Refresh metadata of all datasources in the cluster If ``datasource_name`` is specified, only that datasource is updated """ self.druid_version = self.get_druid_version() - for datasource in self.get_datasources(): - if datasource not in conf.get('DRUID_DATA_SOURCE_BLACKLIST', []): - if not datasource_name or datasource_name == datasource: - DruidDatasource.sync_to_db(datasource, self, merge_flag) + ds_list = self.get_datasources() + blacklist = conf.get('DRUID_DATA_SOURCE_BLACKLIST', []) + ds_refresh = [] + if not datasource_name: + ds_refresh = list(filter(lambda ds: ds not in blacklist, ds_list)) + elif datasource_name not in blacklist and datasource_name in ds_list: + ds_refresh.append(datasource_name) + else: + return + self.refresh_async(ds_refresh, merge_flag, refreshAll) + + def refresh_async(self, datasource_names, merge_flag, refreshAll): + """ + Fetches metadata for the specified datasources andm + merges to the Superset database + """ + session = db.session + ds_list = ( + session.query(DruidDatasource) + .filter(or_(DruidDatasource.datasource_name == name + for name in datasource_names)) + ) + + ds_map = {ds.name: ds for ds in ds_list} + for ds_name in datasource_names: + datasource = ds_map.get(ds_name, None) + if not datasource: + datasource = DruidDatasource(datasource_name=ds_name) + with session.no_autoflush: + session.add(datasource) + flasher( + "Adding new datasource [{}]".format(ds_name), 'success') + ds_map[ds_name] = datasource + elif refreshAll: + flasher( + "Refreshing datasource [{}]".format(ds_name), 'info') + else: + del ds_map[ds_name] + continue + datasource.cluster = self + datasource.merge_flag = merge_flag + session.flush() + + # Prepare multithreaded executation + pool = Pool() + ds_refresh = list(ds_map.values()) + metadata = pool.map(_fetch_metadata_for, ds_refresh) + pool.close() + pool.join() + + for i in range(0, len(ds_refresh)): + datasource = ds_refresh[i] + cols = metadata[i] + col_objs_list = ( + session.query(DruidColumn) + .filter(DruidColumn.datasource_name == datasource.datasource_name) + .filter(or_(DruidColumn.column_name == col for col in cols)) + ) + col_objs = {col.column_name: col for col in col_objs_list} + for col in cols: + if col == '__time': # skip the time column + continue + col_obj = col_objs.get(col, None) + if not col_obj: + col_obj = DruidColumn( + datasource_name=datasource.datasource_name, + column_name=col) + with session.no_autoflush: + session.add(col_obj) + datatype = cols[col]['type'] + if datatype == 'STRING': + col_obj.groupby = True + col_obj.filterable = True + if datatype == 'hyperUnique' or datatype == 'thetaSketch': + col_obj.count_distinct = True + # Allow sum/min/max for long or double + if datatype == 'LONG' or datatype == 'DOUBLE': + col_obj.sum = True + col_obj.min = True + col_obj.max = True + col_obj.type = datatype + col_obj.datasource = datasource + datasource.generate_metrics_for(col_objs_list) + session.commit() @property def perm(self): @@ -160,16 +251,14 @@ def dimension_spec(self): if self.dimension_spec_json: return json.loads(self.dimension_spec_json) - def generate_metrics(self): - """Generate metrics based on the column metadata""" - M = DruidMetric # noqa - metrics = [] - metrics.append(DruidMetric( + def get_metrics(self): + metrics = {} + metrics['count'] = DruidMetric( metric_name='count', verbose_name='COUNT(*)', metric_type='count', json=json.dumps({'type': 'count', 'name': 'count'}) - )) + ) # Somehow we need to reassign this for UDAFs if self.type in ('DOUBLE', 'FLOAT'): corrected_type = 'DOUBLE' @@ -179,49 +268,49 @@ def generate_metrics(self): if self.sum and self.is_num: mt = corrected_type.lower() + 'Sum' name = 'sum__' + self.column_name - metrics.append(DruidMetric( + metrics[name] = DruidMetric( metric_name=name, metric_type='sum', verbose_name='SUM({})'.format(self.column_name), json=json.dumps({ 'type': mt, 'name': name, 'fieldName': self.column_name}) - )) + ) if self.avg and self.is_num: mt = corrected_type.lower() + 'Avg' name = 'avg__' + self.column_name - metrics.append(DruidMetric( + metrics[name] = DruidMetric( metric_name=name, metric_type='avg', verbose_name='AVG({})'.format(self.column_name), json=json.dumps({ 'type': mt, 'name': name, 'fieldName': self.column_name}) - )) + ) if self.min and self.is_num: mt = corrected_type.lower() + 'Min' name = 'min__' + self.column_name - metrics.append(DruidMetric( + metrics[name] = DruidMetric( metric_name=name, metric_type='min', verbose_name='MIN({})'.format(self.column_name), json=json.dumps({ 'type': mt, 'name': name, 'fieldName': self.column_name}) - )) + ) if self.max and self.is_num: mt = corrected_type.lower() + 'Max' name = 'max__' + self.column_name - metrics.append(DruidMetric( + metrics[name] = DruidMetric( metric_name=name, metric_type='max', verbose_name='MAX({})'.format(self.column_name), json=json.dumps({ 'type': mt, 'name': name, 'fieldName': self.column_name}) - )) + ) if self.count_distinct: name = 'count_distinct__' + self.column_name if self.type == 'hyperUnique' or self.type == 'thetaSketch': - metrics.append(DruidMetric( + metrics[name] = DruidMetric( metric_name=name, verbose_name='COUNT(DISTINCT {})'.format(self.column_name), metric_type=self.type, @@ -230,10 +319,9 @@ def generate_metrics(self): 'name': name, 'fieldName': self.column_name }) - )) + ) else: - mt = 'count_distinct' - metrics.append(DruidMetric( + metrics[name] = DruidMetric( metric_name=name, verbose_name='COUNT(DISTINCT {})'.format(self.column_name), metric_type='count_distinct', @@ -241,22 +329,25 @@ def generate_metrics(self): 'type': 'cardinality', 'name': name, 'fieldNames': [self.column_name]}) - )) - session = get_session() - new_metrics = [] - for metric in metrics: - m = ( - session.query(M) - .filter(M.metric_name == metric.metric_name) - .filter(M.datasource_name == self.datasource_name) - .filter(DruidCluster.cluster_name == self.datasource.cluster_name) - .first() - ) + ) + return metrics + + def generate_metrics(self): + """Generate metrics based on the column metadata""" + metrics = self.get_metrics() + dbmetrics = ( + db.session.query(DruidMetric) + .filter(DruidCluster.cluster_name == self.datasource.cluster_name) + .filter(DruidMetric.datasource_name == self.datasource_name) + .filter(or_( + DruidMetric.metric_name == m for m in metrics + )) + ) + dbmetrics = {metric.metric_name: metric for metric in dbmetrics} + for metric in metrics.values(): metric.datasource_name = self.datasource_name - if not m: - new_metrics.append(metric) - session.add(metric) - session.flush() + if not dbmetrics.get(metric.metric_name, None): + db.session.add(metric) @classmethod def import_obj(cls, i_column): @@ -474,6 +565,7 @@ def int_or_0(v): def latest_metadata(self): """Returns segment metadata from the latest segment""" + logging.info("Syncing datasource [{}]".format(self.datasource_name)) client = self.cluster.get_pydruid_client() results = client.time_boundary(datasource=self.datasource_name) if not results: @@ -485,31 +577,33 @@ def latest_metadata(self): # realtime segments, which triggered a bug (fixed in druid 0.8.2). # https://groups.google.com/forum/#!topic/druid-user/gVCqqspHqOQ lbound = (max_time - timedelta(days=7)).isoformat() - rbound = max_time.isoformat() if not self.version_higher(self.cluster.druid_version, '0.8.2'): rbound = (max_time - timedelta(1)).isoformat() + else: + rbound = max_time.isoformat() segment_metadata = None try: segment_metadata = client.segment_metadata( datasource=self.datasource_name, intervals=lbound + '/' + rbound, merge=self.merge_flag, - analysisTypes=conf.get('DRUID_ANALYSIS_TYPES')) + analysisTypes=[]) except Exception as e: logging.warning("Failed first attempt to get latest segment") logging.exception(e) if not segment_metadata: # if no segments in the past 7 days, look at all segments lbound = datetime(1901, 1, 1).isoformat()[:10] - rbound = datetime(2050, 1, 1).isoformat()[:10] if not self.version_higher(self.cluster.druid_version, '0.8.2'): rbound = datetime.now().isoformat() + else: + rbound = datetime(2050, 1, 1).isoformat()[:10] try: segment_metadata = client.segment_metadata( datasource=self.datasource_name, intervals=lbound + '/' + rbound, merge=self.merge_flag, - analysisTypes=conf.get('DRUID_ANALYSIS_TYPES')) + analysisTypes=[]) except Exception as e: logging.warning("Failed 2nd attempt to get latest segment") logging.exception(e) @@ -517,17 +611,37 @@ def latest_metadata(self): return segment_metadata[-1]['columns'] def generate_metrics(self): - for col in self.columns: - col.generate_metrics() + self.generate_metrics_for(self.columns) + + def generate_metrics_for(self, columns): + metrics = {} + for col in columns: + metrics.update(col.get_metrics()) + dbmetrics = ( + db.session.query(DruidMetric) + .filter(DruidCluster.cluster_name == self.cluster_name) + .filter(DruidMetric.datasource_name == self.datasource_name) + .filter(or_(DruidMetric.metric_name == m for m in metrics)) + ) + dbmetrics = {metric.metric_name: metric for metric in dbmetrics} + for metric in metrics.values(): + metric.datasource_name = self.datasource_name + if not dbmetrics.get(metric.metric_name, None): + with db.session.no_autoflush: + db.session.add(metric) @classmethod - def sync_to_db_from_config(cls, druid_config, user, cluster): + def sync_to_db_from_config( + cls, + druid_config, + user, + cluster, + refresh=True): """Merges the ds config from druid_config into one stored in the db.""" - session = db.session() + session = db.session datasource = ( session.query(cls) - .filter_by( - datasource_name=druid_config['name']) + .filter_by(datasource_name=druid_config['name']) .first() ) # Create a new datasource. @@ -540,16 +654,18 @@ def sync_to_db_from_config(cls, druid_config, user, cluster): created_by_fk=user.id, ) session.add(datasource) + elif not refresh: + return dimensions = druid_config['dimensions'] + col_objs = ( + session.query(DruidColumn) + .filter(DruidColumn.datasource_name == druid_config['name']) + .filter(or_(DruidColumn.column_name == dim for dim in dimensions)) + ) + col_objs = {col.column_name: col for col in col_objs} for dim in dimensions: - col_obj = ( - session.query(DruidColumn) - .filter_by( - datasource_name=druid_config['name'], - column_name=dim) - .first() - ) + col_obj = col_objs.get(dim, None) if not col_obj: col_obj = DruidColumn( datasource_name=druid_config['name'], @@ -562,6 +678,13 @@ def sync_to_db_from_config(cls, druid_config, user, cluster): ) session.add(col_obj) # Import Druid metrics + metric_objs = ( + session.query(DruidMetric) + .filter(DruidMetric.datasource_name == druid_config['name']) + .filter(or_(DruidMetric.metric_name == spec['name'] + for spec in druid_config["metrics_spec"])) + ) + metric_objs = {metric.metric_name: metric for metric in metric_objs} for metric_spec in druid_config["metrics_spec"]: metric_name = metric_spec["name"] metric_type = metric_spec["type"] @@ -575,12 +698,7 @@ def sync_to_db_from_config(cls, druid_config, user, cluster): "fieldName": metric_name, }) - metric_obj = ( - session.query(DruidMetric) - .filter_by( - datasource_name=druid_config['name'], - metric_name=metric_name) - ).first() + metric_obj = metric_objs.get(metric_name, None) if not metric_obj: metric_obj = DruidMetric( metric_name=metric_name, @@ -595,58 +713,6 @@ def sync_to_db_from_config(cls, druid_config, user, cluster): session.add(metric_obj) session.commit() - @classmethod - def sync_to_db(cls, name, cluster, merge): - """Fetches metadata for that datasource and merges the Superset db""" - logging.info("Syncing Druid datasource [{}]".format(name)) - session = get_session() - datasource = session.query(cls).filter_by(datasource_name=name).first() - if not datasource: - datasource = cls(datasource_name=name) - session.add(datasource) - flasher("Adding new datasource [{}]".format(name), "success") - else: - flasher("Refreshing datasource [{}]".format(name), "info") - session.flush() - datasource.cluster = cluster - datasource.merge_flag = merge - session.flush() - - cols = datasource.latest_metadata() - if not cols: - logging.error("Failed at fetching the latest segment") - return - for col in cols: - # Skip the time column - if col == "__time": - continue - col_obj = ( - session - .query(DruidColumn) - .filter_by(datasource_name=name, column_name=col) - .first() - ) - datatype = cols[col]['type'] - if not col_obj: - col_obj = DruidColumn(datasource_name=name, column_name=col) - session.add(col_obj) - if datatype == "STRING": - col_obj.groupby = True - col_obj.filterable = True - if datatype == "hyperUnique" or datatype == "thetaSketch": - col_obj.count_distinct = True - # If long or double, allow sum/min/max - if datatype == "LONG" or datatype == "DOUBLE": - col_obj.sum = True - col_obj.min = True - col_obj.max = True - if col_obj: - col_obj.type = cols[col]['type'] - session.flush() - col_obj.datasource = datasource - col_obj.generate_metrics() - session.flush() - @staticmethod def time_offset(granularity): if granularity == 'week_ending_saturday': diff --git a/superset/connectors/druid/views.py b/superset/connectors/druid/views.py index a06bc391ff355..42fbdbbe987b0 100644 --- a/superset/connectors/druid/views.py +++ b/superset/connectors/druid/views.py @@ -85,7 +85,7 @@ class DruidMetricInlineView(CompactCRUDMixin, SupersetModelView): # noqa list_columns = ['metric_name', 'verbose_name', 'metric_type'] edit_columns = [ 'metric_name', 'description', 'verbose_name', 'metric_type', 'json', - 'datasource', 'd3format', 'is_restricted'] + 'datasource', 'd3format', 'is_restricted', 'warning_text'] add_columns = edit_columns page_size = 500 validators_columns = { @@ -109,6 +109,7 @@ class DruidMetricInlineView(CompactCRUDMixin, SupersetModelView): # noqa 'metric_type': _("Type"), 'json': _("JSON"), 'datasource': _("Druid Datasource"), + 'warning_text': _("Warning Message"), } def post_add(self, metric): @@ -234,17 +235,17 @@ class DruidDatasourceModelView(DatasourceModelView, DeleteMixin): # noqa } def pre_add(self, datasource): - number_of_existing_datasources = db.session.query( - sqla.func.count('*')).filter( - models.DruidDatasource.datasource_name == - datasource.datasource_name, - models.DruidDatasource.cluster_name == datasource.cluster.id - ).scalar() - - # table object is already added to the session - if number_of_existing_datasources > 1: - raise Exception(get_datasource_exist_error_mgs( - datasource.full_name)) + with db.session.no_autoflush: + query = ( + db.session.query(models.DruidDatasource) + .filter(models.DruidDatasource.datasource_name == + datasource.datasource_name, + models.DruidDatasource.cluster_name == + datasource.cluster.id) + ) + if db.session.query(query.exists()).scalar(): + raise Exception(get_datasource_exist_error_mgs( + datasource.full_name)) def post_add(self, datasource): datasource.generate_metrics() @@ -272,14 +273,14 @@ class Druid(BaseSupersetView): @has_access @expose("/refresh_datasources/") - def refresh_datasources(self): + def refresh_datasources(self, refreshAll=True): """endpoint that refreshes druid datasources metadata""" session = db.session() DruidCluster = ConnectorRegistry.sources['druid'].cluster_class for cluster in session.query(DruidCluster).all(): cluster_name = cluster.cluster_name try: - cluster.refresh_datasources() + cluster.refresh_datasources(refreshAll=refreshAll) except Exception as e: flash( "Error while processing cluster '{}'\n{}".format( @@ -295,8 +296,25 @@ def refresh_datasources(self): session.commit() return redirect("/druiddatasourcemodelview/list/") + @has_access + @expose("/scan_new_datasources/") + def scan_new_datasources(self): + """ + Calling this endpoint will cause a scan for new + datasources only and add them. + """ + return self.refresh_datasources(refreshAll=False) + appbuilder.add_view_no_menu(Druid) +appbuilder.add_link( + "Scan New Datasources", + label=__("Scan New Datasources"), + href='/druid/scan_new_datasources/', + category='Sources', + category_label=__("Sources"), + category_icon='fa-database', + icon="fa-refresh") appbuilder.add_link( "Refresh Druid Metadata", label=__("Refresh Druid Metadata"), diff --git a/superset/connectors/sqla/models.py b/superset/connectors/sqla/models.py index 1ffae8585cc11..24fd2bfbabc6d 100644 --- a/superset/connectors/sqla/models.py +++ b/superset/connectors/sqla/models.py @@ -10,7 +10,7 @@ DateTime, ) import sqlalchemy as sa -from sqlalchemy import asc, and_, desc, select +from sqlalchemy import asc, and_, desc, select, or_ from sqlalchemy.sql.expression import TextAsFrom from sqlalchemy.orm import backref, relationship from sqlalchemy.sql import table, literal_column, text, column @@ -588,30 +588,29 @@ def fetch_metadata(self): table = self.get_sqla_table_object() except Exception: raise Exception(_( - "Table doesn't seem to exist in the specified database, " - "couldn't fetch column information")) + "Table [{}] doesn't seem to exist in the specified database, " + "couldn't fetch column information").format(self.table_name)) - TC = TableColumn # noqa shortcut to class M = SqlMetric # noqa metrics = [] any_date_col = None - db_dialect = self.database.get_sqla_engine().dialect + db_dialect = self.database.get_dialect() + dbcols = ( + db.session.query(TableColumn) + .filter(TableColumn.table == self) + .filter(or_(TableColumn.column_name == col.name + for col in table.columns))) + dbcols = {dbcol.column_name: dbcol for dbcol in dbcols} + for col in table.columns: try: - datatype = "{}".format(col.type.compile(dialect=db_dialect)).upper() + datatype = col.type.compile(dialect=db_dialect).upper() except Exception as e: datatype = "UNKNOWN" logging.error( "Unrecognized data type in {}.{}".format(table, col.name)) logging.exception(e) - dbcol = ( - db.session - .query(TC) - .filter(TC.table == self) - .filter(TC.column_name == col.name) - .first() - ) - db.session.flush() + dbcol = dbcols.get(col.name, None) if not dbcol: dbcol = TableColumn(column_name=col.name, type=datatype) dbcol.groupby = dbcol.is_string @@ -619,14 +618,11 @@ def fetch_metadata(self): dbcol.sum = dbcol.is_num dbcol.avg = dbcol.is_num dbcol.is_dttm = dbcol.is_time - - db.session.merge(self) self.columns.append(dbcol) - if not any_date_col and dbcol.is_time: any_date_col = col.name - quoted = "{}".format(col.compile(dialect=db_dialect)) + quoted = str(col.compile(dialect=db_dialect)) if dbcol.sum: metrics.append(M( metric_name='sum__' + dbcol.column_name, @@ -663,8 +659,6 @@ def fetch_metadata(self): expression="COUNT(DISTINCT {})".format(quoted) )) dbcol.type = datatype - db.session.merge(self) - db.session.commit() metrics.append(M( metric_name='count', @@ -672,19 +666,18 @@ def fetch_metadata(self): metric_type='count', expression="COUNT(*)" )) + + dbmetrics = db.session.query(M).filter(M.table_id == self.id).filter( + or_(M.metric_name == metric.metric_name for metric in metrics)) + dbmetrics = {metric.metric_name: metric for metric in dbmetrics} for metric in metrics: - m = ( - db.session.query(M) - .filter(M.metric_name == metric.metric_name) - .filter(M.table_id == self.id) - .first() - ) metric.table_id = self.id - if not m: + if not dbmetrics.get(metric.metric_name, None): db.session.add(metric) - db.session.commit() if not self.main_dttm_col: self.main_dttm_col = any_date_col + db.session.merge(self) + db.session.commit() @classmethod def import_obj(cls, i_datasource, import_time=None): diff --git a/superset/connectors/sqla/views.py b/superset/connectors/sqla/views.py index c49d4d02a56dd..e4ba41b2584df 100644 --- a/superset/connectors/sqla/views.py +++ b/superset/connectors/sqla/views.py @@ -1,6 +1,5 @@ """Views used by the SqlAlchemy connector""" import logging - from past.builtins import basestring from flask import Markup, flash, redirect @@ -108,7 +107,7 @@ class SqlMetricInlineView(CompactCRUDMixin, SupersetModelView): # noqa list_columns = ['metric_name', 'verbose_name', 'metric_type'] edit_columns = [ 'metric_name', 'description', 'verbose_name', 'metric_type', - 'expression', 'table', 'd3format', 'is_restricted'] + 'expression', 'table', 'd3format', 'is_restricted', 'warning_text'] description_columns = { 'expression': utils.markdown( "a valid SQL expression as supported by the underlying backend. " @@ -135,7 +134,8 @@ class SqlMetricInlineView(CompactCRUDMixin, SupersetModelView): # noqa 'expression': _("SQL Expression"), 'table': _("Table"), 'd3format': _("D3 Format"), - 'is_restricted': _('Is Restricted') + 'is_restricted': _('Is Restricted'), + 'warning_text': _('Warning Message'), } def post_add(self, metric): @@ -151,7 +151,7 @@ def post_update(self, metric): class TableModelView(DatasourceModelView, DeleteMixin): # noqa datamodel = SQLAInterface(models.SqlaTable) - + list_title = _('List Tables') show_title = _('Show Table') add_title = _('Add Table') @@ -228,21 +228,17 @@ class TableModelView(DatasourceModelView, DeleteMixin): # noqa } def pre_add(self, table): - number_of_existing_tables = db.session.query( - sa.func.count('*')).filter( - models.SqlaTable.table_name == table.table_name, - models.SqlaTable.schema == table.schema, - models.SqlaTable.database_id == table.database.id - ).scalar() - # table object is already added to the session - if number_of_existing_tables > 1: - raise Exception(get_datasource_exist_error_mgs(table.full_name)) + with db.session.no_autoflush: + table_query = db.session.query(models.SqlaTable).filter( + models.SqlaTable.table_name == table.table_name, + models.SqlaTable.schema == table.schema, + models.SqlaTable.database_id == table.database.id) + if db.session.query(table_query.exists()).scalar(): + raise Exception( + get_datasource_exist_error_mgs(table.full_name)) # Fail before adding if the table can't be found - try: - table.get_sqla_table_object() - except Exception as e: - logging.exception(e) + if not table.database.has_table(table): raise Exception(_( "Table [{}] could not be found, " "please double check your " diff --git a/superset/db_engine_specs.py b/superset/db_engine_specs.py index bf4940b22f381..71c6ab20d9b6f 100644 --- a/superset/db_engine_specs.py +++ b/superset/db_engine_specs.py @@ -439,7 +439,7 @@ def fetch_result_sets(cls, db, datasource_type, force=False): result_set_df = db.get_df( """SELECT table_schema, table_name FROM INFORMATION_SCHEMA.{}S ORDER BY concat(table_schema, '.', table_name)""".format( - datasource_type.upper()), None) + datasource_type.upper()), None) result_sets = defaultdict(list) for unused, row in result_set_df.iterrows(): result_sets[row['table_schema']].append(row['table_name']) @@ -668,7 +668,7 @@ class HiveEngineSpec(PrestoEngineSpec): def patch(cls): from pyhive import hive from superset.db_engines import hive as patched_hive - from pythrifthiveapi.TCLIService import ( + from TCLIService import ( constants as patched_constants, ttypes as patched_ttypes, TCLIService as patched_TCLIService) @@ -1003,8 +1003,7 @@ def convert_dttm(cls, target_type, dttm): tt = target_type.upper() if tt == 'DATE': return "'{}'".format(dttm.strftime('%Y-%m-%d')) - else: - return "'{}'".format(dttm.strftime('%Y-%m-%d %H:%M:%S')) + return "'{}'".format(dttm.strftime('%Y-%m-%d %H:%M:%S')) class ImpalaEngineSpec(BaseEngineSpec): @@ -1028,8 +1027,7 @@ def convert_dttm(cls, target_type, dttm): tt = target_type.upper() if tt == 'DATE': return "'{}'".format(dttm.strftime('%Y-%m-%d')) - else: - return "'{}'".format(dttm.strftime('%Y-%m-%d %H:%M:%S')) + return "'{}'".format(dttm.strftime('%Y-%m-%d %H:%M:%S')) engines = { diff --git a/superset/db_engines/hive.py b/superset/db_engines/hive.py index a31b4d7f323d8..f14608410823a 100644 --- a/superset/db_engines/hive.py +++ b/superset/db_engines/hive.py @@ -1,5 +1,5 @@ from pyhive import hive -from pythrifthiveapi.TCLIService import ttypes +from TCLIService import ttypes from thrift import Thrift diff --git a/superset/legacy.py b/superset/legacy.py index 8de2548c8e160..c398d873cac4c 100644 --- a/superset/legacy.py +++ b/superset/legacy.py @@ -4,8 +4,8 @@ from __future__ import print_function from __future__ import unicode_literals -from superset import frontend_config import re +from superset import frontend_config FORM_DATA_KEY_WHITELIST = list(frontend_config.get('controls').keys()) + ['slice_id'] @@ -79,5 +79,3 @@ def cast_form_data(form_data): if k not in FORM_DATA_KEY_WHITELIST: del d[k] return d - - diff --git a/superset/migrations/versions/19a814813610_adding_metric_warning_text.py b/superset/migrations/versions/19a814813610_adding_metric_warning_text.py new file mode 100644 index 0000000000000..cf39a0e631599 --- /dev/null +++ b/superset/migrations/versions/19a814813610_adding_metric_warning_text.py @@ -0,0 +1,26 @@ +"""Adding metric warning_text + +Revision ID: 19a814813610 +Revises: ca69c70ec99b +Create Date: 2017-09-15 15:09:40.495345 + +""" + +# revision identifiers, used by Alembic. +revision = '19a814813610' +down_revision = 'ca69c70ec99b' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + op.add_column('metrics', sa.Column('warning_text', sa.Text(), nullable=True)) + op.add_column('sql_metrics', sa.Column('warning_text', sa.Text(), nullable=True)) + + +def downgrade(): + with op.batch_alter_table('sql_metrics') as batch_op_sql_metrics: + batch_op_sql_metrics.drop_column('warning_text') + with op.batch_alter_table('metrics') as batch_op_metrics: + batch_op_metrics.drop_column('warning_text') diff --git a/superset/migrations/versions/472d2f73dfd4_.py b/superset/migrations/versions/472d2f73dfd4_.py new file mode 100644 index 0000000000000..d74fd03a7b7c1 --- /dev/null +++ b/superset/migrations/versions/472d2f73dfd4_.py @@ -0,0 +1,22 @@ +"""empty message + +Revision ID: 472d2f73dfd4 +Revises: ('19a814813610', 'a9c47e2c1547') +Create Date: 2017-09-21 18:37:30.844196 + +""" + +# revision identifiers, used by Alembic. +revision = '472d2f73dfd4' +down_revision = ('19a814813610', 'a9c47e2c1547') + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + pass + + +def downgrade(): + pass diff --git a/superset/models/core.py b/superset/models/core.py index b9daa72fdcdf2..b8293b59d34f5 100644 --- a/superset/models/core.py +++ b/superset/models/core.py @@ -33,6 +33,7 @@ from sqlalchemy.pool import NullPool from sqlalchemy.sql import text from sqlalchemy.sql.expression import TextAsFrom +from sqlalchemy.engine import url from sqlalchemy_utils import EncryptedType from superset import app, db, db_engine_specs, utils, sm @@ -743,6 +744,15 @@ def get_perm(self): return ( "[{obj.database_name}].(id:{obj.id})").format(obj=self) + def has_table(self, table): + engine = self.get_sqla_engine() + return engine.dialect.has_table( + engine, table.table_name, table.schema or None) + + def get_dialect(self): + sqla_url = url.make_url(self.sqlalchemy_uri_decrypted) + return sqla_url.get_dialect()() + sqla.event.listen(Database, 'after_insert', set_perm) sqla.event.listen(Database, 'after_update', set_perm) diff --git a/superset/models/sql_lab.py b/superset/models/sql_lab.py index e2e125ad2438a..24aff218e4183 100644 --- a/superset/models/sql_lab.py +++ b/superset/models/sql_lab.py @@ -16,8 +16,7 @@ import sqlalchemy as sqla from sqlalchemy import ( Column, Integer, String, ForeignKey, Text, Boolean, - DateTime, Numeric, -) + DateTime, Numeric, ) from sqlalchemy.orm import backref, relationship from superset import sm @@ -28,7 +27,6 @@ class Query(Model): - """ORM model for SQL query""" __tablename__ = 'query' @@ -39,8 +37,7 @@ class Query(Model): # Store the tmp table into the DB only if the user asks for it. tmp_table_name = Column(String(256)) - user_id = Column( - Integer, ForeignKey('ab_user.id'), nullable=True) + user_id = Column(Integer, ForeignKey('ab_user.id'), nullable=True) status = Column(String(16), default=QueryStatus.PENDING) tab_name = Column(String(256)) sql_editor_id = Column(String(256)) @@ -80,14 +77,11 @@ class Query(Model): database = relationship( 'Database', foreign_keys=[database_id], - backref=backref('queries', cascade='all, delete-orphan') - ) - user = relationship( - sm.user_model, - foreign_keys=[user_id]) + backref=backref('queries', cascade='all, delete-orphan')) + user = relationship(sm.user_model, foreign_keys=[user_id]) __table_args__ = ( - sqla.Index('ti_user_id_changed_on', user_id, changed_on), + sqla.Index('ti_user_id_changed_on', user_id, changed_on), ) @property @@ -128,25 +122,19 @@ def name(self): """Name property""" ts = datetime.now().isoformat() ts = ts.replace('-', '').replace(':', '').split('.')[0] - tab = ( - self.tab_name.replace(' ', '_').lower() - if self.tab_name - else 'notab' - ) + tab = (self.tab_name.replace(' ', '_').lower() + if self.tab_name else 'notab') tab = re.sub(r'\W+', '', tab) return "sqllab_{tab}_{ts}".format(**locals()) class SavedQuery(Model, AuditMixinNullable): - """ORM model for SQL query""" __tablename__ = 'saved_query' id = Column(Integer, primary_key=True) - user_id = Column( - Integer, ForeignKey('ab_user.id'), nullable=True) - db_id = Column( - Integer, ForeignKey('dbs.id'), nullable=True) + user_id = Column(Integer, ForeignKey('ab_user.id'), nullable=True) + db_id = Column(Integer, ForeignKey('dbs.id'), nullable=True) schema = Column(String(128)) label = Column(String(256)) description = Column(Text) @@ -158,8 +146,7 @@ class SavedQuery(Model, AuditMixinNullable): database = relationship( 'Database', foreign_keys=[db_id], - backref=backref('saved_queries', cascade='all, delete-orphan') - ) + backref=backref('saved_queries', cascade='all, delete-orphan')) @property def pop_tab_link(self): diff --git a/superset/security.py b/superset/security.py index 012891143e45c..11b6b647c1f96 100644 --- a/superset/security.py +++ b/superset/security.py @@ -11,7 +11,6 @@ from superset.models import core as models from superset.connectors.connector_registry import ConnectorRegistry - READ_ONLY_MODEL_VIEWS = { 'DatabaseAsync', 'DatabaseView', @@ -104,16 +103,16 @@ def get_or_create_main_db(): def is_admin_only(pvm): # not readonly operations on read only model views allowed only for admins - if (pvm.view_menu.name in READ_ONLY_MODEL_VIEWS and - pvm.permission.name not in READ_ONLY_PERMISSION): + if (pvm.view_menu.name in READ_ONLY_MODEL_VIEWS + and pvm.permission.name not in READ_ONLY_PERMISSION): return True - return (pvm.view_menu.name in ADMIN_ONLY_VIEW_MENUS or - pvm.permission.name in ADMIN_ONLY_PERMISSIONS) + return (pvm.view_menu.name in ADMIN_ONLY_VIEW_MENUS + or pvm.permission.name in ADMIN_ONLY_PERMISSIONS) def is_alpha_only(pvm): - if (pvm.view_menu.name in GAMMA_READ_ONLY_MODEL_VIEWS and - pvm.permission.name not in READ_ONLY_PERMISSION): + if (pvm.view_menu.name in GAMMA_READ_ONLY_MODEL_VIEWS + and pvm.permission.name not in READ_ONLY_PERMISSION): return True return pvm.permission.name in ALPHA_ONLY_PERMISSIONS @@ -133,12 +132,14 @@ def is_gamma_pvm(pvm): def is_sql_lab_pvm(pvm): return pvm.view_menu.name in {'SQL Lab'} or pvm.permission.name in { - 'can_sql_json', 'can_csv', 'can_search_queries'} + 'can_sql_json', 'can_csv', 'can_search_queries' + } def is_granter_pvm(pvm): - return pvm.permission.name in {'can_override_role_permissions', - 'can_approve'} + return pvm.permission.name in { + 'can_override_role_permissions', 'can_approve' + } def set_role(role_name, pvm_check): @@ -191,7 +192,7 @@ def merge_pv(view_menu, perm): metrics += list(db.session.query(datasource_class.metric_class).all()) for metric in metrics: - if (metric.is_restricted): + if metric.is_restricted: merge_pv('metric_access', metric.perm) diff --git a/superset/sql_lab.py b/superset/sql_lab.py index 0f2f1fefa5e23..aeb71f6b79a0e 100644 --- a/superset/sql_lab.py +++ b/superset/sql_lab.py @@ -1,19 +1,21 @@ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function from __future__ import unicode_literals from time import sleep from datetime import datetime import json import logging +import uuid import pandas as pd import sqlalchemy -import uuid -from celery.exceptions import SoftTimeLimitExceeded from sqlalchemy.pool import NullPool from sqlalchemy.orm import sessionmaker +from celery.exceptions import SoftTimeLimitExceeded -from superset import ( - app, db, utils, dataframe, results_backend) +from superset import (app, db, utils, dataframe, results_backend) from superset.models.sql_lab import Query from superset.sql_parse import SupersetQuery from superset.db_engine_specs import LimitMethod @@ -78,10 +80,9 @@ def get_session(nullpool): session_class = sessionmaker() session_class.configure(bind=engine) return session_class() - else: - session = db.session() - session.commit() # HACK - return session + session = db.session() + session.commit() # HACK + return session @celery_app.task(bind=True, soft_time_limit=SQLLAB_TIMEOUT) @@ -103,7 +104,8 @@ def get_sql_results( raise -def execute_sql(ctask, query_id, return_results=True, store_results=False, user_name=None): +def execute_sql( + ctask, query_id, return_results=True, store_results=False, user_name=None): """Executes the sql query returns the results.""" session = get_session(not ctask.request.called_directly) @@ -144,13 +146,11 @@ def handle_error(msg): if not query.tmp_table_name: start_dttm = datetime.fromtimestamp(query.start_time) query.tmp_table_name = 'tmp_{}_table_{}'.format( - query.user_id, - start_dttm.strftime('%Y_%m_%d_%H_%M_%S')) + query.user_id, start_dttm.strftime('%Y_%m_%d_%H_%M_%S')) executed_sql = superset_query.as_create_table(query.tmp_table_name) query.select_as_cta_used = True - elif ( - query.limit and superset_query.is_select() and - db_engine_spec.limit_method == LimitMethod.WRAP_SQL): + elif (query.limit and superset_query.is_select() + and db_engine_spec.limit_method == LimitMethod.WRAP_SQL): executed_sql = database.wrap_sql_limit(executed_sql, query.limit) query.limit_used = True try: @@ -168,9 +168,6 @@ def handle_error(msg): session.merge(query) session.commit() logging.info("Set query to 'running'") - - engine = database.get_sqla_engine( - schema=query.schema, nullpool=not ctask.request.called_directly, user_name=user_name) try: engine = database.get_sqla_engine( schema=query.schema, nullpool=not ctask.request.called_directly, user_name=user_name) @@ -178,8 +175,8 @@ def handle_error(msg): cursor = conn.cursor() logging.info("Running query: \n{}".format(executed_sql)) logging.info(query.executed_sql) - cursor.execute( - query.executed_sql, **db_engine_spec.cursor_execute_kwargs) + cursor.execute(query.executed_sql, + **db_engine_spec.cursor_execute_kwargs) logging.info("Handling cursor") db_engine_spec.handle_cursor(cursor, query, session) logging.info("Fetching data: {}".format(query.to_dict())) @@ -202,29 +199,31 @@ def handle_error(msg): conn.close() if query.status == utils.QueryStatus.STOPPED: - return json.dumps({ - 'query_id': query.id, - 'status': query.status, - 'query': query.to_dict(), - }, default=utils.json_iso_dttm_ser) + return json.dumps( + { + 'query_id': query.id, + 'status': query.status, + 'query': query.to_dict(), + }, + default=utils.json_iso_dttm_ser) column_names = ( [col[0] for col in cursor_description] if cursor_description else []) column_names = dedup(column_names) - cdf = dataframe.SupersetDataFrame(pd.DataFrame( - list(data), columns=column_names)) + cdf = dataframe.SupersetDataFrame( + pd.DataFrame(list(data), columns=column_names)) query.rows = cdf.size query.progress = 100 query.status = QueryStatus.SUCCESS if query.select_as_cta: - query.select_sql = '{}'.format(database.select_star( - query.tmp_table_name, - limit=query.limit, - schema=database.force_ctas_schema, - show_cols=False, - latest_partition=False, - )) + query.select_sql = '{}'.format( + database.select_star( + query.tmp_table_name, + limit=query.limit, + schema=database.force_ctas_schema, + show_cols=False, + latest_partition=False, )) query.end_time = utils.now_as_float() session.merge(query) session.flush() diff --git a/superset/sql_parse.py b/superset/sql_parse.py index d0bf5bb9a06fc..bcf36bfb5db6b 100644 --- a/superset/sql_parse.py +++ b/superset/sql_parse.py @@ -1,3 +1,6 @@ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function from __future__ import unicode_literals import logging diff --git a/superset/stats_logger.py b/superset/stats_logger.py index 76eb7675c5798..a19910d5c0d83 100644 --- a/superset/stats_logger.py +++ b/superset/stats_logger.py @@ -1,5 +1,5 @@ -from colorama import Fore, Style import logging +from colorama import Fore, Style class BaseStatsLogger(object): @@ -27,20 +27,18 @@ def gauge(self, key): class DummyStatsLogger(BaseStatsLogger): - def incr(self, key): logging.info( Fore.CYAN + "[stats_logger] (incr) " + key + Style.RESET_ALL) def decr(self, key): - logging.info( - Fore.CYAN + "[stats_logger] (decr) " + key + Style.RESET_ALL) + logging.info(Fore.CYAN + "[stats_logger] (decr) " + key + + Style.RESET_ALL) def gauge(self, key, value): logging.info(( Fore.CYAN + "[stats_logger] (gauge) " - "{key} | {value}" + Style.RESET_ALL).format(**locals()) - ) + "{key} | {value}" + Style.RESET_ALL).format(**locals())) try: @@ -48,10 +46,7 @@ def gauge(self, key, value): class StatsdStatsLogger(BaseStatsLogger): def __init__(self, host, port, prefix='superset'): - self.client = StatsClient( - host=host, - port=port, - prefix=prefix) + self.client = StatsClient(host=host, port=port, prefix=prefix) def incr(self, key): self.client.incr(key) diff --git a/superset/templates/appbuilder/navbar_right.html b/superset/templates/appbuilder/navbar_right.html index d6d844dad6aae..3e2c2593ab94c 100644 --- a/superset/templates/appbuilder/navbar_right.html +++ b/superset/templates/appbuilder/navbar_right.html @@ -9,7 +9,7 @@
-