diff --git a/.circleci/config.yml b/.circleci/config.yml index 48fc6be062..2c8b15a75f 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,6 +1,19 @@ version: 2.0 + +flake8-steps: &steps + - checkout + - run: sudo pip install flake8 + - run: ./bin/flake8_tests.sh jobs: - unit-tests: + python-flake8-tests: + docker: + - image: circleci/python:3.7.0 + steps: *steps + legacy-python-flake8-tests: + docker: + - image: circleci/python:2.7.15 + steps: *steps + backend-unit-tests: environment: COMPOSE_FILE: .circleci/docker-compose.circle.yml COMPOSE_PROJECT_NAME: redash @@ -13,6 +26,7 @@ jobs: name: Build Docker Images command: | set -x + docker-compose build --build-arg skip_ds_deps=true docker-compose up -d sleep 10 - run: @@ -20,25 +34,58 @@ jobs: command: docker-compose run --rm postgres psql -h postgres -U postgres -c "create database tests;" - run: name: Run Tests - command: docker-compose run --name tests redash tests --junitxml=junit.xml tests/ + command: docker-compose run --name tests redash tests --junitxml=junit.xml --cov-report xml --cov=redash --cov-config .coveragerc tests/ - run: name: Copy Test Results command: | mkdir -p /tmp/test-results/unit-tests - docker cp tests:/app/coverage.xml ./coverage.xml + docker cp tests:/app/coverage.xml ./coverage.xml docker cp tests:/app/junit.xml /tmp/test-results/unit-tests/results.xml - store_test_results: path: /tmp/test-results - store_artifacts: path: coverage.xml + frontend-unit-tests: + docker: + - image: circleci/node:8 + steps: + - checkout + - run: sudo apt install python-pip + - run: npm install + - run: npm run bundle + - run: npm test + frontend-e2e-tests: + environment: + COMPOSE_FILE: .circleci/docker-compose.cypress.yml + COMPOSE_PROJECT_NAME: cypress + PERCY_TOKEN_ENCODED: MWM3OGUzNzk4ZWQ2NTE4YTBhMDAwZDNiNWE1Nzc4ZjEzZjYyMzY1MjE0NjY0NDRiOGE5ODc5ZGYzYTU4ZmE4NQ== + docker: + - image: circleci/node:8 + steps: + - setup_remote_docker + - checkout + - run: + name: Install npm dependencies + command: | + npm install + - run: + name: Setup Redash server + command: | + npm run cypress start + docker-compose run cypress node ./cypress/cypress.js db-seed + - run: + name: Execute Cypress tests + command: npm run cypress run-ci build-tarball: docker: - image: circleci/node:8 steps: - checkout + - run: sudo apt install python-pip - run: npm install - - run: npm run build - run: .circleci/update_version + - run: npm run bundle + - run: npm run build - run: .circleci/pack - store_artifacts: path: /tmp/artifacts/ @@ -52,63 +99,18 @@ jobs: - run: docker login -u $DOCKER_USER -p $DOCKER_PASS - run: docker build -t redash/redash:$(.circleci/docker_tag) . - run: docker push redash/redash:$(.circleci/docker_tag) - integration-tests: - working_directory: ~/redash - machine: true - environment: - REDASH_SERVER_URL : "http://127.0.0.1:5000/" - DOCKER_IMAGE: mozilla/redash-ui-tests - steps: - - checkout - - run: - name: Install Docker Compose - command: | - set -x - pip install --upgrade pip - pip install docker-compose>=1.18 - docker-compose --version - - run: - name: Pull redash images - command: | - set -x - docker-compose -f docker-compose.yml up --no-start - sleep 10 - - run: - name: Pull redash-ui-tests - command: docker pull "${DOCKER_IMAGE}":latest - - run: - name: Setup redash instance - command: | - set -x - docker-compose run --rm --user root server create_db - docker-compose run --rm postgres psql -h postgres -U postgres -c "create database tests" - docker-compose run --rm --user root server /app/manage.py users create_root root@example.com "rootuser" --password "IAMROOT" --org default - docker-compose run --rm --user root server /app/manage.py ds new "ui-tests" --type "url" --options '{"title": "uitests"}' - docker-compose run -d -p 5000:5000 --user root server - docker-compose start postgres - docker-compose run --rm --user root server npm install - docker-compose run --rm --user root server npm run build - - run: - name: Run tests - command: | - set -x - docker run --net="host" --env REDASH_SERVER_URL="${REDASH_SERVER_URL}" "${DOCKER_IMAGE}" - - store_artifacts: - path: report.html workflows: version: 2 - integration_tests: - jobs: - - integration-tests: - filters: - branches: - only: master build: jobs: - - unit-tests + - python-flake8-tests + - legacy-python-flake8-tests + - backend-unit-tests + - frontend-unit-tests + - frontend-e2e-tests - build-tarball: requires: - - unit-tests + - backend-unit-tests filters: tags: only: /v[0-9]+(\.[0-9\-a-z]+)*/ @@ -118,8 +120,10 @@ workflows: - /release\/.*/ - build-docker-image: requires: - - unit-tests + - backend-unit-tests filters: branches: only: + - master + - preview-build - /release\/.*/ diff --git a/.circleci/docker-compose.circle.yml b/.circleci/docker-compose.circle.yml index 166b22a98b..e756a92ff3 100644 --- a/.circleci/docker-compose.circle.yml +++ b/.circleci/docker-compose.circle.yml @@ -1,4 +1,4 @@ -version: '2' +version: '3' services: redash: build: ../ diff --git a/.circleci/docker-compose.cypress.yml b/.circleci/docker-compose.cypress.yml new file mode 100644 index 0000000000..2581d5d1a9 --- /dev/null +++ b/.circleci/docker-compose.cypress.yml @@ -0,0 +1,47 @@ +version: '3' +services: + server: + build: ../ + command: dev_server + depends_on: + - postgres + - redis + ports: + - "5000:5000" + environment: + PYTHONUNBUFFERED: 0 + REDASH_LOG_LEVEL: "INFO" + REDASH_REDIS_URL: "redis://redis:6379/0" + REDASH_DATABASE_URL: "postgresql://postgres@postgres/postgres" + worker: + build: ../ + command: scheduler + depends_on: + - server + environment: + PYTHONUNBUFFERED: 0 + REDASH_LOG_LEVEL: "INFO" + REDASH_REDIS_URL: "redis://redis:6379/0" + REDASH_DATABASE_URL: "postgresql://postgres@postgres/postgres" + QUEUES: "queries,scheduled_queries,celery" + WORKERS_COUNT: 2 + cypress: + build: + context: ../ + dockerfile: Dockerfile.cypress + depends_on: + - server + - worker + environment: + CYPRESS_baseUrl: "http://server:5000" + PERCY_TOKEN: ${PERCY_TOKEN} + PERCY_BRANCH: ${CIRCLE_BRANCH} + PERCY_COMMIT: ${CIRCLE_SHA1} + PERCY_PULL_REQUEST: ${CIRCLE_PR_NUMBER} + redis: + image: redis:3.0-alpine + restart: unless-stopped + postgres: + image: postgres:9.5.6-alpine + command: "postgres -c fsync=off -c full_page_writes=off -c synchronous_commit=OFF" + restart: unless-stopped diff --git a/.circleci/docker_tag b/.circleci/docker_tag index 540f5d45ff..5f20a48bd0 100755 --- a/.circleci/docker_tag +++ b/.circleci/docker_tag @@ -1,5 +1,10 @@ #!/bin/bash -VERSION=$(jq -r .version package.json) -FULL_VERSION=$VERSION.b$CIRCLE_BUILD_NUM +if [ $CIRCLE_BRANCH = master ] || [ $CIRCLE_BRANCH = preview-build ] +then + FULL_VERSION='preview' +else + VERSION=$(jq -r .version package.json) + FULL_VERSION=$VERSION.b$CIRCLE_BUILD_NUM +fi echo $FULL_VERSION diff --git a/.circleci/update_version b/.circleci/update_version index 997ca5f290..d397fb23df 100755 --- a/.circleci/update_version +++ b/.circleci/update_version @@ -2,4 +2,5 @@ VERSION=$(jq -r .version package.json) FULL_VERSION=$VERSION+b$CIRCLE_BUILD_NUM -sed -ri "s/^__version__ = '([A-Za-z0-9.-]*)'/__version__ = '$FULL_VERSION'/" redash/__init__.py \ No newline at end of file +sed -ri "s/^__version__ = '([A-Za-z0-9.-]*)'/__version__ = '$FULL_VERSION'/" redash/__init__.py +sed -i "s/dev/$CIRCLE_SHA1/" client/app/version.json diff --git a/.dockerignore b/.dockerignore index 1ff2e91ddd..69c145ad11 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,4 +1,6 @@ client/.tmp/ +client/dist/ node_modules/ .tmp/ +.venv/ .git/ diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md deleted file mode 100644 index 1f52a61269..0000000000 --- a/.github/ISSUE_TEMPLATE.md +++ /dev/null @@ -1,34 +0,0 @@ - - -### Issue Summary - -A summary of the issue and the browser/OS environment in which it occurs. - -### Steps to Reproduce - -1. This is the first step -2. This is the second step, etc. - -Any other info e.g. Why do you consider this to be a bug? What did you expect to happen instead? - -### Technical details: - -* Redash Version: -* Browser/OS: -* How did you install Redash: diff --git a/.github/ISSUE_TEMPLATE/---bug_report.md b/.github/ISSUE_TEMPLATE/---bug_report.md new file mode 100644 index 0000000000..f376d6f1ce --- /dev/null +++ b/.github/ISSUE_TEMPLATE/---bug_report.md @@ -0,0 +1,34 @@ +--- +name: "\U0001F41B Bug report" +about: Report reproducible software issues so we can improve +--- + + + +### Issue Summary + +A summary of the issue and the browser/OS environment in which it occurs. + +### Steps to Reproduce + +1. This is the first step +2. This is the second step, etc. + +Any other info e.g. Why do you consider this to be a bug? What did you expect to happen instead? + +### Technical details: + +* Redash Version: +* Browser/OS: +* How did you install Redash: diff --git a/.github/ISSUE_TEMPLATE/--anything_else.md b/.github/ISSUE_TEMPLATE/--anything_else.md new file mode 100644 index 0000000000..9db411b781 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/--anything_else.md @@ -0,0 +1,17 @@ +--- +name: "\U0001F4A1Anything else" +about: "For help, support, features & ideas - please use https://discuss.redash.io \U0001F46B " +labels: "Support Question" +--- + +We use GitHub only for bug reports 🐛 + +Anything else should be posted to https://discuss.redash.io 👫 + +🚨For support, help & questions use https://discuss.redash.io/c/support +💡For feature requests & ideas use https://discuss.redash.io/c/feature-requests + +Alternatively, check out these resources below. Thanks! 😁. + +- [Forum](https://disucss.redash.io) +- [Knowledge Base](https://redash.io/help) diff --git a/.gitignore b/.gitignore index e405ce2850..7307021b73 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,5 @@ node_modules .sass-cache npm-debug.log +cypress/screenshots +cypress/videos diff --git a/CHANGELOG.md b/CHANGELOG.md index d80f2726a5..9341e1c32d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,231 @@ # Change Log +## v6.0.0 - 2018-12-16 + +v6.0.0 release version. Mainly includes fixes for regressions from the beta version. + +This release had contributions from 5 people: @rauchy, @denisov-vlad, @arikfr, @ariarijp, and @gabrieldutra. Thank you, everyone 🙏 + +### Changed + +* #3183 Make refresh_queries less noisey in logs. @arikfr + +### Fixed + +* #3163 Include correct version in production builds. @rauchy +* #3161 Clickhouse: fix int() conversion error. @denisov-vlad +* #3166 Directly using record_event task requires timestamp. @arikfr +* #3167 Alert.evaluate failing when the column is missing. @arikfr +* ##3162 Remove API permissions for users who have been disabled. @rauchy +* #3171 Reject empty query name. @ariarijp +* #3175, #3186 Fix disable error message. @rauchy, @gabrieldutra +* #3182 [Redshift] support for schema names with dots. @arikfr +* #3187 Safely create_app in Celery code (try to fetch current_app first). @arikfr + +### Other + +* #3155 Add DB Seed to Cypress and setup Percy. @gabrieldutra +* #3180 Remove coverage from pytest terminal output. @rauchy + + +## v6.0.0-beta - 2018-12-03 + +This release was 2 months in the making and it is full with good stuff! + +* We have 5 new data sources: Databricks, IBM DB2, Kylin, Druid and Rockset. ⌗ +* There are fixes and improvements to 11 existing data sources (MySQL, Redshift, Postgres, MongoDB, Google BigQuery, Vertica, TreasureData, Presto, ClickHouse, Google Sheets and Google Analytics). +* The Query Results data source can now load cached results, just use the `cached_query_` prefix instead of `query_`. +* On the visualizations front we added a Heatmap visualization and did updated the table and counter visualizations. +* Alerts got some fixes and a new destination: PagerDuty. +* If the live autocomplete in the code editor annoys you, you can disable it now (although we're working to make it better, see #3092). +* Fast queries will now load faster. 🏃‍♂️ +* We improved the layout of visualizations and content on smaller screen sizes. 📱 +* For those of you who like sharing, you can now enable the ability to share ownership of queries and dashboards and let others to edit them. Check the Settings page to enable this feature. + +There were also important changes to the code and infrastructure: + +* More components moved to React. +* We switched to Webpack 4 with the help of @dmonego. +* We upgraded to Celery 4 with the help of @emtwo, @jezdez, @mashrikt and @atharvai. +* We started moving towards Python 3 for our backend. The first step was to make sure our code pass basic sanity tests with Flake 8, which was implemented by @cclauss. +* We improved our testing on the frontend by adding setup for Jest tests and E2E testing using Cypress (@gabrieldutra). +* Each pull request now gets a deploy preview using Netlify to easily test frontend changes. + +This is just a summary, you're welcome to review the full list below. ⬇ + +This release had contributions from 38 people: @arikfr, @kravets-levko, @jezdez, @kyoshidajp, @kocsmy, @alison985, @gabrieldutra, @washort, @GitSumito, @emtwo, @rauchy, @alexanderlz, @denisov-vlad, @ariarijp, @yoavbls, @zhujunsan, @sjakthol, @koooge, @SakuradaJun, @dmonego, @Udomomo, @cclauss, @combineads, @zaimy, @Trigl, @ralphilius, @jodevsa, @deecay, @igorcanadi, @pashaxp, @hoangphuoc25, @toph, @burnash, @wankdanker, @Yossi-a, @Rovel, @kadrach, and @nicof38. Thank you, everyone 🙏 + +### Added + +* #2747, #3143 Add a new Databricks query runner. @alison985, @jezdez, @arikfr +* #2767 Add ability to add viz to dashboard from query edit page. @alison985, @jezdez +* #2780 Add a query autocomplete toggle. @alison985, @jezdez, @arikfr +* #2768 Add authentication via JWT providers. @SakuradaJun +* #2790 Add the ability to sort favorited queries, paginate the dashboard list and improve UI inconsistencies. @jezdez +* #2681 Add ability to search table column names in schema browser. @alison985 +* #2855 Add option to query cached results. @yoavbls +* #2740 Add ability for extensions to add periodic tasks. @emtwo +* #2924 Google Spreadsheets: Add support for opening by URL. @alexanderlz +* #2903 Add PagerDuty as an Alert Destination. @alexanderlz +* #2824 Add support for expanding dashboard visualizations. @sjakthol +* #2900 Add ability to specify a counter label. @ralphilius +* #2565 Add frontend extension capabilities. @emtwo +* #2848 Add IBM Db2 as a data source using the ibm-db Python package. @nicof38 +* #2959 Add option to auto reload widget data in shared dashboards. @arikfr +* #2993 Add page size settings. @kyoshidajp +* #2080 New Heatmap chart visualization with Plotly. @deecay +* #2991 Show users in CLI group list. @GitSumito +* #2342 New SQLPARSE_FORMAT_OPTIONS setting to configure query formatter. @ariarijp +* #3031 Add some tests for Query Results. @ariarijp +* #2936 Add Kylin data source. @Trigl +* #3047 Add Druid data source. @rauchy +* #3077 New user interface for the feature flag of the share edit permissions feature. @arikfr +* #3007 Add permissions to the result of "manage.py groups list" command. @Udomomo +* #3088 Add get_current_user() fuction for the Python query runner. @kyoshidajp +* #3114 Add event tracking to autocomplete toggle. @arikfr +* #3068 Add Rockset query runner. @igorcanadi, @arikfr +* #3105 Display frontend version. @rauchy + +### Changed + +* #2636 Rewrite query editor with React. @washort, @arikfr +* #2637 Convert edit-in-place component to React. @washort, @arikfr +* #2766 Suitable events are now being recorded server side instead of in the frontend. @alison985, @jezdez +* #2796 Change placement (right/bottom) of chart legend depending on chart width. @kravets-levko +* #2833 Uses server side sort order for tag list and show count of tagged items. @jezdez +* #2318 Support authentication for the URL data source. @jezdez +* #2884 Rename Yandex Metrika to Metrica. @jezdez +* #2909 MySQL: hide sys tables. @arikfr +* #2817 Consistently use simplejson for loading and dumping JSON. @jezdez +* #2872 Use Plotly's function to clean y-values (x may be category or date/time). @kravets-levko +* #2938 Auto focus tag input. @kyoshidajp +* #2927 Design refinements for queries pages. @kocsmy +* #2950 Show activity status in CLI user list. @GitSumito +* #2968 Presto data source: setting protocol (http/https), safe loading of error messages. @arikfr +* #2967 Show groups in CLI user list. @GitSumito +* #2603 MongoDB: Update requirements to support srv. @arikfr +* #2961 MongoDB: Skip system collections when loading schema. @arikfr +* #2960 Add timeout to various HTTP requests. @arikfr +* #2983 Databricks: New logo, updated name and enabled by default. @arikfr +* #2982 Table visualization: change default size to 25 and add more size options. @arikfr +* #2866 Redshift: Hide tables the configured user cannot access. @sjakthol +* #3058 Mustache: don't html-escape query parameters values. @kravets-levko +* #3079 Always use basic autocomplete, as well as the live autocomplete. @arikfr +* #3084 Support tel://, sms://, mailto:// links in query results. @zhujunsan +* #3083 Clickhouse: Add WITH TOTALS option support. @denisov-vlad +* #3063 Allow setting colors for bubble charts. @toph +* #3085 BigQuery: Switch to Standard SQL as the default. @kyoshidajp +* #3094 Tags autocomplete: Show note when creating a new label. @kravets-levko +* #2984 Autocomplete toggle improvements. @arikfr +* #3089 Open new tab when forking a query. @kyoshidajp +* #3126 MongoDB: add support for sorting columns. @arikfr +* #3128 Improve backoff algorithm of query results polling to speed it up. @arikfr +* #3125 Vertica: update driver & add support for connection timeout. @arikfr +* #3124 Support unicode in Postgres/Redshift schema. @arikfr +* #3138 Migrate all tags components to React. @kravets-levko +* #3139 Better manage permissions modal. @kocsmy +* #3149 Improve tag link colors and fix group tags on Users page. @kocsmy +* #3146 Update, replace and fix new alert destination logos so it fits better. @kocsmy +* #3147 Add and improve recent db logos that didn't fit in size properly. @kocsmy +* #3148 Fix label positioning on no found screen. @kocsmy +* #3156 json_dumps: add support for serializing buffer objects. @arikfr + +### Fixed + +* #2849 Fix invalid reference to alert.to_dict() in webhook. @wankdanker +* #2840 Improve counter visualization text scaling. @kravets-levko +* #2854 Widget titles are no longer rendered wrong on public dashboards. @kravets-levko +* #2318 Removed redundant exception handling in data sources since that's handled in the query backend. @jezdez +* #2886 Fix Javascript build that broke because registerAll tried to run EditInPlace component. @arikfr +* #2911 Don’t show “Add to dashboard” in dropdown to unsaved queries. @jezdez +* #2916 Fix export query results output file name. @gabrieldutra +* #2917 Fix output file name not changing after rename query. @gabrieldutra +* #2868 Address edge case when retrieving Glue schemas for Athena data source. @kadrach +* #2929 Fix: date value in a filter is duplicated. @combineads +* #2875 Unbreak charts with long legend break in horizontal mode. Update plotly.js. @kravets-levko +* #2937 Fix event recording in admin API backend. @kyoshidajp +* #2953 Minor fixes for the Clickhouse data source. @denisov-vlad +* #2941 Bring back fix to Box plot hover. @arikfr +* #2957 Apply missing CSS classes to EditInPlace component. @arikfr +* #2897 Show "Add description" only after saving the query. @arikfr +* #2922 Query page layout improvements for small screens. @kravets-levko +* #2956 Clickhouse: move timeout to params. @denisov-vlad +* #2964 Fix no tags shown when having empty set. @gabrieldutra +* #2757 Use full text search ranking when searching in list views. @jezdez +* #2969 Query Results data source: improved errors, quoted column names. @arikfr +* #2906 Preventing open redirection in loging process. @kyoshidajp +* #2867 TreasureData: Deduplicate column names. @zaimy +* #2994 Fix scheme of various URLs from http to https. @kyoshidajp +* #2992 Fix an invalid prop type warning in new version notifier. @kyoshidajp +* #3022 Fix Toolbox covering part of a chart. @kravets-levko +* #2998 Fix charts losing responsive features after refreshing the dashboard. @kravets-levko +* #3034 Postgres: handle NaN/Infinity values. @kravets-levko +* #2745 Sort columns with undefined values. @Yossi-a +* #3041 Sort CLI output of lists. @GitSumito +* #2803, #3006 Address various tag display issues on query list page. @kocsmy, @alison985 +* #3049 Fix edit-in-place component which ignored isEditable flag and didn't work on Groups page. @kravets-levko +* #2965 Google Analytics: Fix crash when no results are returned. @alexanderlz +* #3061 Fix table visualization so that the horizontal scrollbar is not be always visible. @kravets-levko +* #3076 Add white-space padding to separators in the footer. @burnash +* #2919 Fix URL data source to not require a URL. @arikfr +* #3098 Force AngularJS to update query editor properly. @washort +* #3100 Delete redundant regex segment in query result frontend. @zhujunsan +* #2978 Prevent the query update timestamp from changing when it is linked to new query results. @rauchy +* #3046 Fix query page header. @kravets-levko +* #3097 Mongo: Fix collection fields retreival bug when Views are present. @jodevsa +* #3107 Keep query text in local state for now. @washort +* #3111 Fix mobile padding issues on Query results. @kocsmy +* #3122 Show menu divider only if query is archived. @jezdez +* #3120 Fix tag counts for dashboards and queries. @jezdez +* #3141 Fix schema refresh to work on MySQL 8. @hoangphuoc25 +* #3142 Fix: editing dashboard title results in the visualizations being replaced by the loading markers. @kravets-levko + +### Other + +* #2850 The setup scripts are now based on Ubuntu 18.04 LTS and Docker. @pashaxp, @arikfr +* #2985 Add Jest based tests to our stack. @arikfr +* #2999 Add netlify configuration. @arikfr +* #3000 Initial Cypress based E2E test infrastructure. @gabrieldutra +* #2898 Move Ant styles into a central location. @arikfr +* #2910 Fix webpack build error about BigMessage. @jezdez +* #2928 Speed up builds by skipping installing requirements_all_ds.txt in CI unit tests. @arikfr +* #2963 Fix tarball build failure. @emtwo +* #2996 Fix setup.sh failures when run as root. @arikfr +* #2989 Rearrange make targets. @koooge +* #3036 Update Flask-Admin to 1.5.2. @yoavbls +* #2901 Fix documentation links. @kravets-levko +* #3073 Remove only Redash containers in clean Make task. @ariarijp +* #3048 Remove pytest-watch dependency to workaround an issue with watchdog. @rauchy +* #2905 Update development docker-compose.yml file to use latest Redis and Postgres servers and specify working volume explictly. @Rovel +* #3032 Makefile: Add make targets for test. @koooge +* #2933 Switch to Webpack 4. @dmonego +* #2908 Update setup files. @arikfr +* #2946 Update snowflake_connector_python version. @arikfr +* #2773 Upgrade to Celery 4.2.1. @emtwo, @jezdez +* #2881 CircleCI: Make flake8 tests pass on Legacy Python and Python 3. @cclauss +* #2907 Remove unused dependencies (honcho, wsgiref). @arikfr +* #3039 Build docker image on master branch. @arikfr +* #3106 Fix registerAll failures after minification. @arikfr + + +## v5.0.2 - 2018-10-18 + +### Security + +* Fix: prevent Open Redirect vulnerability. + + +## v5.0.1 - 2018-09-27 + +### Added + +* Added support for JWT authentication (for services like Cloudflare Access or Google IAP). + +### Changed + +* Upgraded Celery version to 3.1.26 to make upgrade to Celery 4 easier. + + ## v5.0.0 - 2018-09-21 Final release for V5. Most of the changes were already in the beta release of V5, but this includes several fixes along diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e92c1b4e90..1288782f5f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -9,7 +9,7 @@ The following is a set of guidelines for contributing to Redash. These are guide - [Feature Roadmap](https://trello.com/b/b2LUHU7A/redash-roadmap) - [Feature Requests](https://discuss.redash.io/c/feature-requests) - [Documentation](https://redash.io/help/) -- [Blog](http://blog.redash.io/) +- [Blog](https://blog.redash.io/) - [Twitter](https://twitter.com/getredash) --- @@ -67,7 +67,7 @@ The project's documentation can be found at [https://redash.io/help/](https://re ### Release Method -We publish a stable release every ~2 months, although the goal is to get to a stable release every month. You can see the change log on [GitHub releases page](http://github.com/getredash/redash/releases). +We publish a stable release every ~2 months, although the goal is to get to a stable release every month. You can see the change log on [GitHub releases page](https://github.com/getredash/redash/releases). Every build of the master branch updates the latest *RC release*. These releases are usually stable, but might contain regressions and therefore recommended for "advanced users" only. @@ -75,4 +75,4 @@ When we release a new stable release, we also update the *latest* Docker image t ## Code of Conduct -This project adheres to the Contributor Covenant [code of conduct](http://redash.io/community/code_of_conduct). By participating, you are expected to uphold this code. Please report unacceptable behavior to team@redash.io. +This project adheres to the Contributor Covenant [code of conduct](https://redash.io/community/code_of_conduct). By participating, you are expected to uphold this code. Please report unacceptable behavior to team@redash.io. diff --git a/Dockerfile b/Dockerfile index a82661e527..5176e7df46 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,12 +1,16 @@ FROM redash/base:latest +# Controls whether to install extra dependencies needed for all data sources. +ARG skip_ds_deps + # We first copy only the requirements file, to avoid rebuilding on every file # change. COPY requirements.txt requirements_dev.txt requirements_all_ds.txt ./ -RUN pip install -r requirements.txt -r requirements_dev.txt -r requirements_all_ds.txt +RUN pip install -r requirements.txt -r requirements_dev.txt +RUN if [ "x$skip_ds_deps" = "x" ] ; then pip install -r requirements_all_ds.txt ; else echo "Skipping pip install -r requirements_all_ds.txt" ; fi COPY . ./ -RUN npm install && npm run build && rm -rf node_modules +RUN npm install && npm run bundle && npm run build && rm -rf node_modules RUN chown -R redash /app USER redash diff --git a/Dockerfile.cypress b/Dockerfile.cypress new file mode 100644 index 0000000000..3efef14f49 --- /dev/null +++ b/Dockerfile.cypress @@ -0,0 +1,11 @@ +FROM cypress/browsers:chrome67 + +ENV APP /usr/src/app +WORKDIR $APP + +RUN npm install --no-save cypress @percy/cypress > /dev/null + +COPY cypress $APP/cypress +COPY cypress.json $APP/cypress.json + +RUN ./node_modules/.bin/cypress verify diff --git a/Makefile b/Makefile new file mode 100644 index 0000000000..d13ecacf31 --- /dev/null +++ b/Makefile @@ -0,0 +1,51 @@ +.PHONY: compose_build up test_db create_database clean down bundle tests lint backend-unit-tests frontend-unit-tests test build watch start + +compose_build: + docker-compose build + +up: + docker-compose up -d --build + +test_db: + @for i in `seq 1 5`; do \ + if (docker-compose exec postgres sh -c 'psql -U postgres -c "select 1;"' 2>&1 > /dev/null) then break; \ + else echo "postgres initializing..."; sleep 5; fi \ + done + docker-compose exec postgres sh -c 'psql -U postgres -c "drop database if exists tests;" && psql -U postgres -c "create database tests;"' + +create_database: + docker-compose run server create_db + +clean: + docker-compose down && docker-compose rm + +down: + docker-compose down + +bundle: + docker-compose run server bin/bundle-extensions + +tests: + docker-compose run server tests + +lint: + ./bin/flake8_tests.sh + +backend-unit-tests: up test_db + docker-compose run --rm --name tests server tests + +frontend-unit-tests: bundle + npm install + npm run bundle + npm test + +test: lint backend-unit-tests frontend-unit-tests + +build: bundle + npm run build + +watch: bundle + npm run watch + +start: bundle + npm run start diff --git a/README.md b/README.md index 6f68a344bc..cd2e5ae0de 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ Today **_Redash_** has support for querying multiple databases, including: Redsh **_Redash_** consists of two parts: -1. **Query Editor**: think of [JS Fiddle](http://jsfiddle.net) for SQL queries. It's your way to share data in the organization in an open way, by sharing both the dataset and the query that generated it. This way everyone can peer review not only the resulting dataset but also the process that generated it. Also it's possible to fork it and generate new datasets and reach new insights. +1. **Query Editor**: think of [JS Fiddle](https://jsfiddle.net) for SQL queries. It's your way to share data in the organization in an open way, by sharing both the dataset and the query that generated it. This way everyone can peer review not only the resulting dataset but also the process that generated it. Also it's possible to fork it and generate new datasets and reach new insights. 2. **Visualizations and Dashboards**: once you have a dataset, you can create different visualizations out of it, and then combine several visualizations into a single dashboard. Currently Redash supports charts, pivot table, cohorts and [more](https://redash.io/help/user-guide/visualizations/visualization-types). @@ -31,13 +31,12 @@ Today **_Redash_** has support for querying multiple databases, including: Redsh ## Supported Data Sources -Redash supports more than 25 [data sources](https://redash.io/help/data-sources/supported-data-sources). +Redash supports more than 35 [data sources](https://redash.io/help/data-sources/supported-data-sources). ## Getting Help * Issues: https://github.com/getredash/redash/issues * Discussion Forum: https://discuss.redash.io/ -* Slack: http://slack.redash.io/ ## Reporting Bugs and Contributing Code diff --git a/bin/bundle-extensions b/bin/bundle-extensions new file mode 100755 index 0000000000..8416aab776 --- /dev/null +++ b/bin/bundle-extensions @@ -0,0 +1,39 @@ +#!/usr/bin/env python + +import os +from subprocess import call +from distutils.dir_util import copy_tree + +from pkg_resources import iter_entry_points, resource_filename, resource_isdir + + + +# Make a directory for extensions and set it as an environment variable +# to be picked up by webpack. +EXTENSIONS_RELATIVE_PATH = os.path.join('client', 'app', 'extensions') +EXTENSIONS_DIRECTORY = os.path.join( + os.path.dirname(os.path.dirname(__file__)), + EXTENSIONS_RELATIVE_PATH) + +if not os.path.exists(EXTENSIONS_DIRECTORY): + os.makedirs(EXTENSIONS_DIRECTORY) +os.environ["EXTENSIONS_DIRECTORY"] = EXTENSIONS_RELATIVE_PATH + +for entry_point in iter_entry_points('redash.extensions'): + # This is where the frontend code for an extension lives + # inside of its package. + content_folder_relative = os.path.join( + entry_point.name, 'bundle') + (root_module, _) = os.path.splitext(entry_point.module_name) + + if not resource_isdir(root_module, content_folder_relative): + continue + + content_folder = resource_filename(root_module, content_folder_relative) + + # This is where we place our extensions folder. + destination = os.path.join( + EXTENSIONS_DIRECTORY, + entry_point.name) + + copy_tree(content_folder, destination) diff --git a/bin/flake8_tests.sh b/bin/flake8_tests.sh new file mode 100755 index 0000000000..3d6be0d2a9 --- /dev/null +++ b/bin/flake8_tests.sh @@ -0,0 +1,7 @@ +#!/bin/sh + +flake8 --version ; pip --version +# stop the build if there are Python syntax errors or undefined names +flake8 . --count --select=E901,E999,F821,F822,F823 --show-source --statistics +# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide +flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics diff --git a/bin/get_changes.py b/bin/get_changes.py index 1de784fb78..6d98d8672b 100644 --- a/bin/get_changes.py +++ b/bin/get_changes.py @@ -1,4 +1,5 @@ #!/bin/env python +from __future__ import print_function import sys import re import subprocess @@ -32,4 +33,4 @@ def get_change_log(previous_sha): changes = get_change_log(previous_sha) for change in changes: - print change \ No newline at end of file + print(change) \ No newline at end of file diff --git a/bin/release_manager.py b/bin/release_manager.py index 521a9b8b52..df00169da7 100644 --- a/bin/release_manager.py +++ b/bin/release_manager.py @@ -1,10 +1,10 @@ from __future__ import print_function import os import sys -import json import re import subprocess import requests +import simplejson github_token = os.environ['GITHUB_TOKEN'] auth = (github_token, 'x-oauth-basic') @@ -17,7 +17,7 @@ def _github_request(method, path, params=None, headers={}): url = path if params is not None: - params = json.dumps(params) + params = simplejson.dumps(params) response = requests.request(method, url, data=params, auth=auth) return response diff --git a/client/.eslintrc.js b/client/.eslintrc.js index ed429e2aee..10fae2b4c6 100644 --- a/client/.eslintrc.js +++ b/client/.eslintrc.js @@ -1,11 +1,14 @@ module.exports = { root: true, - extends: "airbnb", + extends: ["airbnb", "plugin:jest/recommended"], + plugins: ["jest", "cypress"], settings: { "import/resolver": "webpack" }, parser: "babel-eslint", env: { + "jest/globals": true, + "cypress/globals": true, "browser": true, "node": true }, diff --git a/client/app/assets/images/db-logos/databricks.png b/client/app/assets/images/db-logos/databricks.png new file mode 100644 index 0000000000..f2031b5fd1 Binary files /dev/null and b/client/app/assets/images/db-logos/databricks.png differ diff --git a/client/app/assets/images/db-logos/db2.png b/client/app/assets/images/db-logos/db2.png new file mode 100644 index 0000000000..b5995ce6a4 Binary files /dev/null and b/client/app/assets/images/db-logos/db2.png differ diff --git a/client/app/assets/images/db-logos/druid.png b/client/app/assets/images/db-logos/druid.png new file mode 100644 index 0000000000..d5ded0fad0 Binary files /dev/null and b/client/app/assets/images/db-logos/druid.png differ diff --git a/client/app/assets/images/db-logos/hive_http.png b/client/app/assets/images/db-logos/hive_http.png new file mode 100644 index 0000000000..c9a127db3f Binary files /dev/null and b/client/app/assets/images/db-logos/hive_http.png differ diff --git a/client/app/assets/images/db-logos/kylin.png b/client/app/assets/images/db-logos/kylin.png new file mode 100644 index 0000000000..914787a679 Binary files /dev/null and b/client/app/assets/images/db-logos/kylin.png differ diff --git a/client/app/assets/images/db-logos/mongodb.png b/client/app/assets/images/db-logos/mongodb.png index 40c1de8daa..70f56999b0 100644 Binary files a/client/app/assets/images/db-logos/mongodb.png and b/client/app/assets/images/db-logos/mongodb.png differ diff --git a/client/app/assets/images/db-logos/rockset.png b/client/app/assets/images/db-logos/rockset.png new file mode 100644 index 0000000000..75c5930613 Binary files /dev/null and b/client/app/assets/images/db-logos/rockset.png differ diff --git a/client/app/assets/images/destinations/chatwork.png b/client/app/assets/images/destinations/chatwork.png index 2490ca39d6..e54a31565d 100644 Binary files a/client/app/assets/images/destinations/chatwork.png and b/client/app/assets/images/destinations/chatwork.png differ diff --git a/client/app/assets/images/destinations/mattermost.png b/client/app/assets/images/destinations/mattermost.png index bafb9b52c5..c6eaf73b43 100644 Binary files a/client/app/assets/images/destinations/mattermost.png and b/client/app/assets/images/destinations/mattermost.png differ diff --git a/client/app/assets/images/destinations/pagerduty.png b/client/app/assets/images/destinations/pagerduty.png new file mode 100644 index 0000000000..5ea3c03e36 Binary files /dev/null and b/client/app/assets/images/destinations/pagerduty.png differ diff --git a/client/app/assets/less/inc/bootstrap-overrides.less b/client/app/assets/less/inc/bootstrap-overrides.less index 9df2292135..1702d631cb 100755 --- a/client/app/assets/less/inc/bootstrap-overrides.less +++ b/client/app/assets/less/inc/bootstrap-overrides.less @@ -40,3 +40,10 @@ vertical-align: top; margin-left: 0; } + +// Hide URLs next to links when printing (override `bootstrap` rules) +@media print { + a[href]:after { + content: none !important; + } +} diff --git a/client/app/assets/less/inc/edit-in-place.less b/client/app/assets/less/inc/edit-in-place.less index 42589cc8e7..55d2887dd3 100755 --- a/client/app/assets/less/inc/edit-in-place.less +++ b/client/app/assets/less/inc/edit-in-place.less @@ -15,15 +15,6 @@ border-radius: @redash-radius; } -.edit-in-place input, -.edit-in-place textarea { - display: none; -} - -.edit-in-place.active span { - display: none; -} - .edit-in-place.active input, .edit-in-place.active textarea { display: inline-block; diff --git a/client/app/assets/less/inc/list-group.less b/client/app/assets/less/inc/list-group.less index 7b32d033e4..85635c7ee3 100755 --- a/client/app/assets/less/inc/list-group.less +++ b/client/app/assets/less/inc/list-group.less @@ -1,60 +1,78 @@ -.list-group { - margin-bottom: 0; - - &.lg-alt .list-group-item { - border: 0; - } - - &:not(.lg-alt) { - &.lg-listview .list-group-item { - border-left: 0; - border-right: 0; - - &:last-child { - border-bottom: 0; - } - } - } -} - - .list-group-item { - &.active { - button { - color: white; - } - } - .cr-alt { - line-height: 100%; - margin-top: 2px; - } - } - -.list-group-item-heading { - margin-bottom: 2px; - color: #333; - - & > small { - font-size: 11px; - color: #C5C5C5; - margin-left: 10px; - } -} - -.list-group-item-heading, -.list-group-item-text { - .text-overflow(); -} - -.list-group-item-text { - display: block; - - &:not(:last-child) { - margin-bottom: 4px; - } -} - -.list-group-img { - width: 38px; - height: 38px; - border-radius: 2px; -} +.list-group { + margin-bottom: 0; + + &.lg-alt .list-group-item { + border: 0; + } + + &:not(.lg-alt) { + &.lg-listview .list-group-item { + border-left: 0; + border-right: 0; + + &:last-child { + border-bottom: 0; + } + } + } +} + +tags-list { + a { + line-height: 1.1; + } +} + +.tags-list__name { + display: inline-block; + overflow: hidden; + text-overflow: ellipsis; + width: 88%; + line-height: 1.3; +} + +.max-character { + .text-overflow(); +} + +.list-group-item { + &.active { + button { + color: white; + } + } + .cr-alt { + line-height: 100%; + margin-top: 2px; + } +} + +.list-group-item-heading { + margin-bottom: 2px; + color: #333; + + & > small { + font-size: 11px; + color: #C5C5C5; + margin-left: 10px; + } +} + +.list-group-item-heading, +.list-group-item-text { + .text-overflow(); +} + +.list-group-item-text { + display: block; + + &:not(:last-child) { + margin-bottom: 4px; + } +} + +.list-group-img { + width: 38px; + height: 38px; + border-radius: 2px; +} diff --git a/client/app/assets/less/inc/visualizations/counter-render.less b/client/app/assets/less/inc/visualizations/counter-render.less index f123093e90..c57c297a98 100755 --- a/client/app/assets/less/inc/visualizations/counter-render.less +++ b/client/app/assets/less/inc/visualizations/counter-render.less @@ -3,52 +3,43 @@ counter-renderer { text-align: center; padding: 15px 10px; overflow: hidden; -} -counter-renderer counter { - margin: 0; - padding: 0; - display: block; - font-size: 80px; - overflow: hidden; - height: 200px; -} + counter { + margin: 0; + padding: 0; + font-size: 80px; + line-height: normal; + overflow: hidden; + display: flex; + align-items: center; + justify-content: center; -counter-renderer value, -counter-renderer counter-name, -counter-renderer counter-target { - font-size: 1em; - line-height: 1em; - display: block; -} + value, + counter-target { + font-size: 1em; + display: block; + } -counter-renderer value .ruler, -counter-renderer counter-name .ruler, -counter-renderer counter-target .ruler { - display: block; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - font: inherit; - line-height: inherit; - margin: 0; - padding: 0; -} + counter-name { + font-size: 0.5em; + display: block; + } -counter-renderer counter-target { - color: #ccc; -} + &.positive value { + color: #5cb85c; + } -counter-renderer counter.positive value { - color: #5cb85c; -} + &.negative value { + color: #d9534f; + } + } -counter-renderer counter.negative value { - color: #d9534f; - margin-right: 15px; -} + counter-target { + color: #ccc; + } -counter-renderer counter-name { - font-size: 0.5em; - display: block; + counter-name { + font-size: 0.5em; + display: block; + } } diff --git a/client/app/assets/less/main.less b/client/app/assets/less/main.less index 11f67bcf16..db3d55c294 100644 --- a/client/app/assets/less/main.less +++ b/client/app/assets/less/main.less @@ -1,6 +1,3 @@ -@import '~antd/lib/style/core/iconfont.less'; -@import '~antd/lib/input/style/index.less'; -@import '~antd/lib/date-picker/style/index.less'; @import 'redash/ant'; /** LESS Plugins **/ @@ -14,8 +11,8 @@ @import '~ui-select/dist/select.css'; @import '~angular-toastr/src/toastr'; @import '~angular-resizable/src/angular-resizable.css'; -@import '~pace-progress/themes/blue/pace-theme-minimal.css'; @import '~material-design-iconic-font/dist/css/material-design-iconic-font.css'; +@import '~pace-progress/themes/blue/pace-theme-minimal.css'; @import 'inc/angular'; @import 'inc/variables'; @@ -81,6 +78,7 @@ @import 'redash/redash-newstyle'; @import 'redash/redash-table'; @import 'redash/query'; +@import 'redash/tags-control'; diff --git a/client/app/assets/less/redash/ant.less b/client/app/assets/less/redash/ant.less index 9d961c032f..deb8d75555 100644 --- a/client/app/assets/less/redash/ant.less +++ b/client/app/assets/less/redash/ant.less @@ -1,3 +1,10 @@ +@import '~antd/lib/style/core/iconfont.less'; +@import '~antd/lib/style/core/motion.less'; +@import '~antd/lib/input/style/index.less'; +@import '~antd/lib/date-picker/style/index.less'; +@import '~antd/lib/tooltip/style/index.less'; +@import '~antd/lib/select/style/index.less'; + // Overwritting Ant Design defaults to fit into Redash current style @font-family-no-number : @redash-font; @font-family : @redash-font; @@ -7,3 +14,25 @@ @border-color-base : #e8e8e8; @primary-color : @blue; + +// Fix for disabled button styles inside Tooltip component. +// Tooltip wraps disabled buttons with `` and moves all styles +// and classes to that ``. This resets all button styles and +// turns it into simple inline element (because now it's wrapper is a button) +.btn { + button[disabled] { + -moz-appearance: none !important; + -webkit-appearance: none !important; + appearance: none !important; + border: 0 !important; + outline: none !important; + background: transparent !important; + margin: 0 !important; + padding: 0 !important; + } +} + +// Fix for Ant dropdowns when they are used in Boootstrap modals +.ant-dropdown-in-bootstrap-modal { + z-index: 1050; +} diff --git a/client/app/assets/less/redash/query.less b/client/app/assets/less/redash/query.less index 30231788a3..20791a8c47 100644 --- a/client/app/assets/less/redash/query.less +++ b/client/app/assets/less/redash/query.less @@ -95,10 +95,6 @@ edit-in-place p.editable:hover { margin-bottom: 10px; } -.source-control { - -} - .ace_editor.ace_autocomplete .ace_completion-highlight { text-shadow: none !important; background: #ffff005e; @@ -225,26 +221,29 @@ edit-in-place p.editable:hover { } .page-header--new { - .label-default { - margin-right: 3px; + .query-tags, + .query-tags__mobile { + .label-default, + .label-warning { + margin-right: 3px; + } } } .page-header--query { - display: flex; - align-items: center; - - h3 { - display: inline-block; + .page-title { + display: block; + margin-left: 15px; + margin-right: 15px; } } a.label-tag { background: fade(@redash-gray, 15%); - color: #333; + color: darken(@redash-gray, 15%); &:hover { - color: #333; + color: darken(@redash-gray, 15%); background: fade(@redash-gray, 25%); } } @@ -527,58 +526,89 @@ nav .rg-bottom { } } -// Smaller screens - -@media (max-width: 880px) { - .query-fullscreen { - flex-direction: column; - overflow: hidden; +.query-tags { + display: inline-block; + vertical-align: middle; + margin-top: -3px; // padding-top of tags +} - nav { - display: none; - } +.query-tags__mobile { + display: none; + margin: -5px 0 0 0; + padding: 0 0 0 23px; +} - .schema-container { - display: none; - } +.table--permission { + .profile__image { + margin-right: 0; } +} - .query-page-wrapper { - .container { +.mp__permission-type { + text-transform: capitalize; +} + +// Smaller screens + +@media (max-width: 880px) { + .page-header--query { + .page-title { margin-left: 0; margin-right: 0; } } - .query-fullscreen .query-metadata__mobile { - display: block; - border-bottom: 1px solid #efefef; - padding: 10px 0; - min-height: 0 !important; - flex-shrink: 0; - } - - a.navbar-brand { + .query-tags:not(.query-tags__empty) { display: none; } - .page-header--query { - padding-left: 0px !important; + .query-tags__mobile:not(.query-tags__empty) { + display: block; + } - h3 { - line-height: 1.25; - } + .btn--showhide, + .query-actions-menu .dropdown-toggle { + margin-bottom: 5px; } - .query-fullscreen .content { - height: 100%; + .btn-publish { + display: none; } - .datasource-small { - visibility: visible; + .tab-nav .tab-new-vis { + display: none; } .query-fullscreen { + flex-direction: column; + overflow: hidden; + + nav { + display: none; + } + + .schema-container { + display: none; + } + + .query-metadata__mobile { + border-bottom: 1px solid #efefef; + min-height: 0 !important; + flex-shrink: 0; + padding: 10px 15px; + display: flex; + flex-wrap: nowrap; + justify-content: space-between; + align-items: center; + + .profile__image_thumb { + margin: 0 5px 0 0; + } + + .query-metadata__property { + white-space: nowrap; + } + } main { flex-direction: column-reverse; @@ -600,18 +630,31 @@ nav .rg-bottom { .content { width: 100%; + height: 100%; + + .static-position__mobile { + position: static !important; + } + } + + .bottom-controller-container { + z-index: 9; } } -} + .query-page-wrapper { + .container { + margin-left: 0; + margin-right: 0; + } + } -@media (max-width: 438px) { - .btn--showhide { - margin-bottom: 5px; + a.navbar-brand { + display: none; } - .btn-publish { - margin-bottom: 5px; + .datasource-small { + visibility: visible; } } @@ -629,6 +672,6 @@ nav .rg-bottom { } .btn-edit-visualisation { - display: none; + } } diff --git a/client/app/assets/less/redash/redash-newstyle.less b/client/app/assets/less/redash/redash-newstyle.less index 38a8ce650b..2f18c05ad4 100644 --- a/client/app/assets/less/redash/redash-newstyle.less +++ b/client/app/assets/less/redash/redash-newstyle.less @@ -1,3 +1,5 @@ +@import (reference, less) '~bootstrap/less/labels.less'; + // Variables @redash-gray: rgba(102, 136, 153, 1); @redash-orange: rgba(255, 120, 100, 1); @@ -366,8 +368,8 @@ body { page-header, .page-header--new { h3 { - margin: 0; - line-height: 1.75; + margin: 0.2em 0; + line-height: 1.3; font-weight: 500; } } @@ -449,10 +451,27 @@ page-header, .page-header--new { background: fade(@redash-gray, 85%); } +.label-tag-unpublished { + background: fade(@redash-gray, 85%); +} + +.label-tag-archived { + .label-warning(); +} + .label-tag { background: fade(@redash-gray, 10%); color: fade(@redash-gray, 75%); +} + +.label-tag-unpublished, +.label-tag-archived, +.label-tag { margin-right: 3px; + display: inline-block; + margin-top: 2px; + max-width: 24ch; + .text-overflow(); } .tab-nav > li > a { @@ -732,6 +751,14 @@ page-header, .page-header--new { margin-top: 2px; } +.tags-list { + + .badge-light { + background: fade(@redash-gray, 10%); + color: fade(@redash-gray, 75%); + } +} + .dropdown-menu--profile { li { width: 200px; @@ -805,10 +832,6 @@ text.slicetext { } .query-page-wrapper { - .page-header--query { - padding-bottom: 5px !important; - } - h3 { font-size: 18px; } @@ -860,6 +883,16 @@ text.slicetext { } } +@media (min-width: 768px) and (max-width: 850px) { + .menu-search { + width: 175px; + } + + a.navbar-brand { + display: none !important; + } +} + @media (max-width: 1084px) { .dropdown--profile__username { display: none; diff --git a/client/app/assets/less/redash/tags-control.less b/client/app/assets/less/redash/tags-control.less new file mode 100644 index 0000000000..3aaf402620 --- /dev/null +++ b/client/app/assets/less/redash/tags-control.less @@ -0,0 +1,13 @@ +.tags-control { + display: flex; + flex-direction: row; + flex-wrap: wrap; + align-items: stretch; + justify-content: flex-start; + line-height: 1em; + + &.inline-tags-control { + display: inline-block; + vertical-align: middle; + } +} diff --git a/client/app/components/AutocompleteToggle.jsx b/client/app/components/AutocompleteToggle.jsx new file mode 100644 index 0000000000..e74e0a9047 --- /dev/null +++ b/client/app/components/AutocompleteToggle.jsx @@ -0,0 +1,43 @@ +import React from 'react'; +import Tooltip from 'antd/lib/tooltip'; +import PropTypes from 'prop-types'; +import '@/redash-font/style.less'; +import recordEvent from '@/lib/recordEvent'; + +export default function AutocompleteToggle({ state, disabled, onToggle }) { + let tooltipMessage = 'Live Autocomplete Enabled'; + let icon = 'icon-flash'; + if (!state) { + tooltipMessage = 'Live Autocomplete Disabled'; + icon = 'icon-flash-off'; + } + + if (disabled) { + tooltipMessage = 'Live Autocomplete Not Available (Use Ctrl+Space to Trigger)'; + icon = 'icon-flash-off'; + } + + const toggle = (newState) => { + recordEvent('toggle_autocomplete', 'screen', 'query_editor', { state: newState }); + onToggle(newState); + }; + + return ( + + + + ); +} + +AutocompleteToggle.propTypes = { + state: PropTypes.bool.isRequired, + disabled: PropTypes.bool.isRequired, + onToggle: PropTypes.func.isRequired, +}; diff --git a/client/app/components/BigMessage.jsx b/client/app/components/BigMessage.jsx index 37d88e7ee5..e667113e6b 100644 --- a/client/app/components/BigMessage.jsx +++ b/client/app/components/BigMessage.jsx @@ -29,3 +29,5 @@ BigMessage.defaultProps = { export default function init(ngModule) { ngModule.component('bigMessage', react2angular(BigMessage)); } + +init.init = true; diff --git a/client/app/components/DateInput.jsx b/client/app/components/DateInput.jsx index 603990a0a4..e72dc4112e 100644 --- a/client/app/components/DateInput.jsx +++ b/client/app/components/DateInput.jsx @@ -45,3 +45,4 @@ export default function init(ngModule) { ngModule.component('dateInput', react2angular(DateInput, null, ['clientConfig'])); } +init.init = true; diff --git a/client/app/components/DateRangeInput.jsx b/client/app/components/DateRangeInput.jsx index 30e7151c41..0d864216ad 100644 --- a/client/app/components/DateRangeInput.jsx +++ b/client/app/components/DateRangeInput.jsx @@ -4,9 +4,6 @@ import React from 'react'; import PropTypes from 'prop-types'; import { react2angular } from 'react2angular'; import { RangePicker } from 'antd/lib/date-picker'; -import 'antd/lib/style/core/iconfont.less'; -import 'antd/lib/input/style/index.less'; -import 'antd/lib/date-picker/style/index.less'; function DateRangeInput({ value, @@ -53,3 +50,4 @@ export default function init(ngModule) { ngModule.component('dateRangeInput', react2angular(DateRangeInput, null, ['clientConfig'])); } +init.init = true; diff --git a/client/app/components/DateTimeInput.jsx b/client/app/components/DateTimeInput.jsx index 56c26ce6f9..739ec17f8d 100644 --- a/client/app/components/DateTimeInput.jsx +++ b/client/app/components/DateTimeInput.jsx @@ -50,3 +50,4 @@ export default function init(ngModule) { ngModule.component('dateTimeInput', react2angular(DateTimeInput, null, ['clientConfig'])); } +init.init = true; diff --git a/client/app/components/DateTimeRangeInput.jsx b/client/app/components/DateTimeRangeInput.jsx index 0bfba09811..5cab36c0f6 100644 --- a/client/app/components/DateTimeRangeInput.jsx +++ b/client/app/components/DateTimeRangeInput.jsx @@ -4,9 +4,6 @@ import React from 'react'; import PropTypes from 'prop-types'; import { react2angular } from 'react2angular'; import { RangePicker } from 'antd/lib/date-picker'; -import 'antd/lib/style/core/iconfont.less'; -import 'antd/lib/input/style/index.less'; -import 'antd/lib/date-picker/style/index.less'; function DateTimeRangeInput({ value, @@ -58,3 +55,5 @@ export default function init(ngModule) { ngModule.component('dateTimeRangeInput', react2angular(DateTimeRangeInput, null, ['clientConfig'])); } +init.init = true; + diff --git a/client/app/components/EditInPlace.jsx b/client/app/components/EditInPlace.jsx new file mode 100644 index 0000000000..369f5d470b --- /dev/null +++ b/client/app/components/EditInPlace.jsx @@ -0,0 +1,92 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { react2angular } from 'react2angular'; + +export class EditInPlace extends React.Component { + static propTypes = { + ignoreBlanks: PropTypes.bool, + isEditable: PropTypes.bool, + editor: PropTypes.string.isRequired, + placeholder: PropTypes.string, + value: PropTypes.string, + onDone: PropTypes.func.isRequired, + }; + + static defaultProps = { + ignoreBlanks: false, + isEditable: true, + placeholder: '', + value: '', + }; + constructor(props) { + super(props); + this.state = { + editing: false, + }; + this.inputRef = React.createRef(); + const self = this; + this.componentDidUpdate = (prevProps, prevState) => { + if (self.state.editing && !prevState.editing) { + self.inputRef.current.focus(); + } + }; + } + + startEditing = () => { + if (this.props.isEditable) { + this.setState({ editing: true }); + } + }; + + stopEditing = () => { + const newValue = this.inputRef.current.value; + const ignorableBlank = this.props.ignoreBlanks && newValue === ''; + if (!ignorableBlank && newValue !== this.props.value) { + this.props.onDone(newValue); + } + this.setState({ editing: false }); + }; + + keyDown = (event) => { + if (event.keyCode === 13 && !event.shiftKey) { + event.preventDefault(); + this.stopEditing(); + } else if (event.keyCode === 27) { + this.setState({ editing: false }); + } + }; + + renderNormal = () => ( + + {this.props.value || this.props.placeholder} + + ); + + renderEdit = () => + React.createElement(this.props.editor, { + ref: this.inputRef, + className: 'rd-form-control', + defaultValue: this.props.value, + onBlur: this.stopEditing, + onKeyDown: this.keyDown, + }); + + render() { + return ( + + {this.state.editing ? this.renderEdit() : this.renderNormal()} + + ); + } +} + +export default function init(ngModule) { + ngModule.component('editInPlace', react2angular(EditInPlace)); +} + +init.init = true; diff --git a/client/app/components/footer.js b/client/app/components/Footer.jsx similarity index 69% rename from client/app/components/footer.js rename to client/app/components/Footer.jsx index 622c03eee8..bd1f06efda 100644 --- a/client/app/components/footer.js +++ b/client/app/components/Footer.jsx @@ -3,9 +3,12 @@ import PropTypes from 'prop-types'; import { react2angular } from 'react2angular'; -function Footer({ clientConfig, currentUser }) { - const version = clientConfig.version; +import frontendVersion from '../version.json'; + +export function Footer({ clientConfig, currentUser }) { + const backendVersion = clientConfig.version; const newVersionAvailable = clientConfig.newVersionAvailable && currentUser.isAdmin; + const separator = ' \u2022 '; let newVersionString = ''; if (newVersionAvailable) { @@ -18,12 +21,12 @@ function Footer({ clientConfig, currentUser }) { return ( ); } @@ -41,3 +44,5 @@ Footer.propTypes = { export default function init(ngModule) { ngModule.component('footer', react2angular(Footer, [], ['clientConfig', 'currentUser'])); } + +init.init = true; diff --git a/client/app/components/Footer.test.js b/client/app/components/Footer.test.js new file mode 100644 index 0000000000..81a157ebfc --- /dev/null +++ b/client/app/components/Footer.test.js @@ -0,0 +1,16 @@ +import React from 'react'; +import renderer from 'react-test-renderer'; +import { Footer } from './Footer'; + +test('Footer renders', () => { + const clientConfig = { + version: '5.0.1', + newVersionAvailable: true, + }; + const currentUser = { + isAdmin: true, + }; + const component = renderer.create(