diff --git a/.circleci/config.yml b/.circleci/config.yml index 5788c5b52e..b7d7033e9e 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -39,26 +39,40 @@ jobs: 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-lint: + docker: + - image: circleci/node:8 + steps: + - checkout + - run: mkdir -p /tmp/test-results/eslint + - run: npm install + - run: npm run lint:ci + - store_test_results: + path: /tmp/test-results frontend-unit-tests: docker: - image: circleci/node:8 steps: - checkout - run: sudo apt install python-pip + - run: sudo pip install -r requirements_bundles.txt - run: npm install - run: npm run bundle - run: npm test + - run: npm run lint frontend-e2e-tests: environment: COMPOSE_FILE: .circleci/docker-compose.cypress.yml COMPOSE_PROJECT_NAME: cypress PERCY_TOKEN_ENCODED: ZGRiY2ZmZDQ0OTdjMzM5ZWE0ZGQzNTZiOWNkMDRjOTk4Zjg0ZjMxMWRmMDZiM2RjOTYxNDZhOGExMjI4ZDE3MA== + CYPRESS_PROJECT_ID_ENCODED: OTI0Y2th + CYPRESS_RECORD_KEY_ENCODED: YzA1OTIxMTUtYTA1Yy00NzQ2LWEyMDMtZmZjMDgwZGI2ODgx docker: - image: circleci/node:8 steps: @@ -82,6 +96,7 @@ jobs: steps: - checkout - run: sudo apt install python-pip + - run: sudo pip install -r requirements_bundles.txt - run: npm install - run: .circleci/update_version - run: npm run bundle @@ -92,11 +107,14 @@ jobs: path: /tmp/artifacts/ build-docker-image: docker: - - image: circleci/buildpack-deps:xenial + - image: circleci/node:8 steps: - setup_remote_docker - checkout + - run: sudo apt install python-pip + - run: sudo pip install -r requirements_bundles.txt - run: .circleci/update_version + - run: npm run bundle - run: .circleci/docker_build workflows: version: 2 @@ -105,8 +123,13 @@ workflows: - python-flake8-tests - legacy-python-flake8-tests - backend-unit-tests - - frontend-unit-tests - - frontend-e2e-tests + - frontend-lint + - frontend-unit-tests: + requires: + - frontend-lint + - frontend-e2e-tests: + requires: + - frontend-lint - build-tarball: requires: - backend-unit-tests diff --git a/.circleci/docker-compose.cypress.yml b/.circleci/docker-compose.cypress.yml index eab54f9de6..15d8cef48b 100644 --- a/.circleci/docker-compose.cypress.yml +++ b/.circleci/docker-compose.cypress.yml @@ -39,6 +39,12 @@ services: PERCY_BRANCH: ${CIRCLE_BRANCH} PERCY_COMMIT: ${CIRCLE_SHA1} PERCY_PULL_REQUEST: ${CIRCLE_PR_NUMBER} + COMMIT_INFO_BRANCH: ${CIRCLE_BRANCH} + COMMIT_INFO_AUTHOR: ${CIRCLE_USERNAME} + COMMIT_INFO_SHA: ${CIRCLE_SHA1} + COMMIT_INFO_REMOTE: ${CIRCLE_REPOSITORY_URL} + CYPRESS_PROJECT_ID: ${CYPRESS_PROJECT_ID} + CYPRESS_RECORD_KEY: ${CYPRESS_RECORD_KEY} redis: image: redis:3.0-alpine restart: unless-stopped diff --git a/.codeclimate.yml b/.codeclimate.yml index c7def082ae..22f2e1f0cf 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -21,20 +21,12 @@ plugins: pep8: enabled: true eslint: - enabled: true - channel: "eslint-5" - config: - config: client/.eslintrc.js - checks: - import/no-unresolved: - enabled: false - no-multiple-empty-lines: # TODO: Enable - enabled: false + enabled: false exclude_patterns: -- "tests/**/*.py" -- "migrations/**/*.py" -- "setup/**/*" -- "bin/**/*" -- "**/node_modules/" -- "client/dist/" -- "**/*.pyc" + - "tests/**/*.py" + - "migrations/**/*.py" + - "setup/**/*" + - "bin/**/*" + - "**/node_modules/" + - "client/dist/" + - "**/*.pyc" diff --git a/.editorconfig b/.editorconfig index b662606c1c..b56b2eb3f1 100644 --- a/.editorconfig +++ b/.editorconfig @@ -9,6 +9,6 @@ trim_trailing_whitespace = true indent_style = space indent_size = 4 -[*.{js,css,html}] +[*.{js,jsx,css,less,html}] indent_style = space indent_size = 2 diff --git a/Dockerfile b/Dockerfile index 2f719909a3..b9fc56431a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,17 +4,18 @@ WORKDIR /frontend COPY package.json package-lock.json /frontend/ RUN npm install -COPY . /frontend +COPY client /frontend/client +COPY webpack.config.js /frontend/ RUN npm run build -FROM redash/base:latest +FROM redash/base:debian # 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 ./ +COPY requirements.txt requirements_bundles.txt requirements_dev.txt 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 diff --git a/README.md b/README.md index 243fbc42ef..221fab98f6 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,10 @@
-
- -
[![Documentation](https://img.shields.io/badge/docs-redash.io/help-brightgreen.svg)](https://redash.io/help/) +[![Datree](https://s3.amazonaws.com/catalog.static.datree.io/datree-badge-20px.svg)](https://datree.io/?src=badge) +![Build Status](https://circleci.com/gh/getredash/redash.png?circle-token=8a695aa5ec2cbfa89b48c275aea298318016f040) **_Redash_** is our take on freeing the data within our company in a way that will better fit our culture and usage patterns. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000000..2bfe8d534f --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,5 @@ +# Security Policy + +## Reporting a Vulnerability + +Please email security@redash.io to report any security vulnerabilities. We will acknowledge receipt of your vulnerability and strive to send you regular updates about our progress. If you're curious about the status of your disclosure please feel free to email us again. If you want to encrypt your disclosure email, you can use [this PGP key](https://keybase.io/arikfr/key.asc). diff --git a/bin/bundle-extensions b/bin/bundle-extensions index 8416aab776..f597784647 100755 --- a/bin/bundle-extensions +++ b/bin/bundle-extensions @@ -1,39 +1,118 @@ #!/usr/bin/env python - +# -*- coding: utf-8 -*- +"""Copy bundle extension files to the client/app/extension directory""" +import logging import os -from subprocess import call -from distutils.dir_util import copy_tree +from pathlib2 import Path +from shutil import copy +from collections import OrderedDict as odict + +from importlib_metadata import entry_points +from importlib_resources import contents, is_resource, path -from pkg_resources import iter_entry_points, resource_filename, resource_isdir +# Name of the subdirectory +BUNDLE_DIRECTORY = "bundle" +logger = logging.getLogger(__name__) # 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 +extensions_relative_path = Path('client', 'app', 'extensions') +extensions_directory = Path(__file__).parent.parent / extensions_relative_path + +if not extensions_directory.exists(): + extensions_directory.mkdir() +os.environ["EXTENSIONS_DIRECTORY"] = str(extensions_relative_path) + + +def resource_isdir(module, resource): + """Whether a given resource is a directory in the given module + + https://importlib-resources.readthedocs.io/en/latest/migration.html#pkg-resources-resource-isdir + """ + try: + return resource in contents(module) and not is_resource(module, resource) + except (ImportError, TypeError): + # module isn't a package, so can't have a subdirectory/-package + return False + + +def entry_point_module(entry_point): + """Returns the dotted module path for the given entry point""" + return entry_point.pattern.match(entry_point.value).group("module") + + +def load_bundles(): + """"Load bundles as defined in Redash extensions. - content_folder = resource_filename(root_module, content_folder_relative) + The bundle entry point can be defined as a dotted path to a module + or a callable, but it won't be called but just used as a means + to find the files under its file system path. + + The name of the directory it looks for files in is "bundle". + + So a Python package with an extension bundle could look like this:: + + my_extensions/ + ├── __init__.py + └── wide_footer + ├── __init__.py + └── bundle + ├── extension.js + └── styles.css + + and would then need to register the bundle with an entry point + under the "redash.periodic_tasks" group, e.g. in your setup.py:: + + setup( + # ... + entry_points={ + "redash.bundles": [ + "wide_footer = my_extensions.wide_footer", + ] + # ... + }, + # ... + ) + + """ + bundles = odict() + for entry_point in entry_points().get("redash.bundles", []): + logger.info('Loading Redash bundle "%s".', entry_point.name) + module = entry_point_module(entry_point) + # Try to get a list of bundle files + if not resource_isdir(module, BUNDLE_DIRECTORY): + logger.error( + 'Redash bundle directory "%s" could not be found.', entry_point.name + ) + continue + with path(module, BUNDLE_DIRECTORY) as bundle_dir: + bundles[entry_point.name] = list(bundle_dir.rglob("*")) + + return bundles + + +bundles = load_bundles().items() +if bundles: + print('Number of extension bundles found: {}'.format(len(bundles))) +else: + print('No extension bundles found.') + +for bundle_name, paths in bundles: + # Shortcut in case not paths were found for the bundle + if not paths: + print('No paths found for bundle "{}".'.format(bundle_name)) + continue - # This is where we place our extensions folder. - destination = os.path.join( - EXTENSIONS_DIRECTORY, - entry_point.name) + # The destination for the bundle files with the entry point name as the subdirectory + destination = Path(extensions_directory, bundle_name) + if not destination.exists(): + destination.mkdir() - copy_tree(content_folder, destination) + # Copy the bundle directory from the module to its destination. + print('Copying "{}" bundle to {}:'.format(bundle_name, destination.resolve())) + for src_path in paths: + dest_path = destination / src_path.name + print(" - {} -> {}".format(src_path, dest_path)) + copy(str(src_path), str(dest_path)) diff --git a/bin/docker-entrypoint b/bin/docker-entrypoint index c7d0e473b6..62d80e9526 100755 --- a/bin/docker-entrypoint +++ b/bin/docker-entrypoint @@ -4,9 +4,10 @@ set -e worker() { WORKERS_COUNT=${WORKERS_COUNT:-2} QUEUES=${QUEUES:-queries,scheduled_queries,celery,schemas} + WORKER_EXTRA_OPTIONS=${WORKER_EXTRA_OPTIONS:-} echo "Starting $WORKERS_COUNT workers for queues: $QUEUES..." - exec /usr/local/bin/celery worker --app=redash.worker -c$WORKERS_COUNT -Q$QUEUES -linfo --maxtasksperchild=10 -Ofair + exec /usr/local/bin/celery worker --app=redash.worker -c$WORKERS_COUNT -Q$QUEUES -linfo --max-tasks-per-child=10 -Ofair $WORKER_EXTRA_OPTIONS } scheduler() { @@ -16,7 +17,7 @@ scheduler() { echo "Starting scheduler and $WORKERS_COUNT workers for queues: $QUEUES..." - exec /usr/local/bin/celery worker --app=redash.worker --beat -s$SCHEDULE_DB -c$WORKERS_COUNT -Q$QUEUES -linfo --maxtasksperchild=10 -Ofair + exec /usr/local/bin/celery worker --app=redash.worker --beat -s$SCHEDULE_DB -c$WORKERS_COUNT -Q$QUEUES -linfo --max-tasks-per-child=10 -Ofair } server() { diff --git a/client/.babelrc b/client/.babelrc index 7ba3d2057b..4de7bd4792 100644 --- a/client/.babelrc +++ b/client/.babelrc @@ -1,7 +1,10 @@ { "presets": [ ["@babel/preset-env", { - "targets": "> 0.5%, last 2 versions, Firefox ESR, ie 11, not dead", + "exclude": [ + "@babel/plugin-transform-async-to-generator", + "@babel/plugin-transform-arrow-functions" + ], "useBuiltIns": "usage" }], "@babel/preset-react" diff --git a/client/.eslintrc.js b/client/.eslintrc.js index ff41b8c6d3..410455db7b 100644 --- a/client/.eslintrc.js +++ b/client/.eslintrc.js @@ -1,23 +1,21 @@ module.exports = { root: true, - extends: ["airbnb", "plugin:jest/recommended"], - plugins: ["jest", "cypress", "chai-friendly"], + extends: ["airbnb", "plugin:compat/recommended"], + plugins: ["jest", "compat"], settings: { "import/resolver": "webpack" }, parser: "babel-eslint", env: { - "jest/globals": true, - "cypress/globals": true, - "browser": true, - "node": true + browser: true, + node: true }, rules: { // allow debugger during development - 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0, - 'no-param-reassign': 0, - 'no-mixed-operators': 0, - 'no-underscore-dangle': 0, + "no-debugger": process.env.NODE_ENV === "production" ? 2 : 0, + "no-param-reassign": 0, + "no-mixed-operators": 0, + "no-underscore-dangle": 0, "no-use-before-define": ["error", "nofunc"], "prefer-destructuring": "off", "prefer-template": "off", @@ -27,38 +25,42 @@ module.exports = { "no-lonely-if": "off", "consistent-return": "off", "no-control-regex": "off", - 'no-multiple-empty-lines': 'warn', + "no-multiple-empty-lines": "warn", "no-script-url": "off", // some tags should have href="javascript:void(0)" - 'operator-linebreak': 'off', - 'react/destructuring-assignment': 'off', + "operator-linebreak": "off", + "react/destructuring-assignment": "off", "react/jsx-filename-extension": "off", - 'react/jsx-one-expression-per-line': 'off', + "react/jsx-one-expression-per-line": "off", "react/jsx-uses-react": "error", "react/jsx-uses-vars": "error", - 'react/jsx-wrap-multilines': 'warn', - 'react/no-access-state-in-setstate': 'warn', + "react/jsx-wrap-multilines": "warn", + "react/no-access-state-in-setstate": "warn", "react/prefer-stateless-function": "warn", "react/forbid-prop-types": "warn", "react/prop-types": "warn", "jsx-a11y/anchor-is-valid": "off", "jsx-a11y/click-events-have-key-events": "off", - "jsx-a11y/label-has-associated-control": ["warn", { - "controlComponents": true - }], + "jsx-a11y/label-has-associated-control": [ + "warn", + { + controlComponents: true + } + ], "jsx-a11y/label-has-for": "off", "jsx-a11y/no-static-element-interactions": "off", - "max-len": ['error', 120, 2, { - ignoreUrls: true, - ignoreComments: false, - ignoreRegExpLiterals: true, - ignoreStrings: true, - ignoreTemplateLiterals: true, - }], - "no-else-return": ["error", {"allowElseIf": true}], - "object-curly-newline": ["error", {"consistent": true}], - // needed for cypress tests - "func-names": "off", - "no-unused-expressions": 0, - "chai-friendly/no-unused-expressions": 2 + "max-len": [ + "error", + 120, + 2, + { + ignoreUrls: true, + ignoreComments: false, + ignoreRegExpLiterals: true, + ignoreStrings: true, + ignoreTemplateLiterals: true + } + ], + "no-else-return": ["error", { allowElseIf: true }], + "object-curly-newline": ["error", { consistent: true }] } }; diff --git a/client/app/.eslintrc.js b/client/app/.eslintrc.js new file mode 100644 index 0000000000..904b0635dc --- /dev/null +++ b/client/app/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + extends: ["plugin:jest/recommended"], + plugins: ["jest"], + env: { + "jest/globals": true, + }, +}; \ No newline at end of file diff --git a/client/app/assets/images/db-logos/bigquery_gce.png b/client/app/assets/images/db-logos/bigquery_gce.png new file mode 100644 index 0000000000..35d4cf8fae Binary files /dev/null and b/client/app/assets/images/db-logos/bigquery_gce.png differ diff --git a/client/app/assets/images/db-logos/couchbase.png b/client/app/assets/images/db-logos/couchbase.png new file mode 100644 index 0000000000..d8e444e964 Binary files /dev/null and b/client/app/assets/images/db-logos/couchbase.png differ diff --git a/client/app/assets/images/db-logos/phoenix.png b/client/app/assets/images/db-logos/phoenix.png new file mode 100644 index 0000000000..eb56de298e Binary files /dev/null and b/client/app/assets/images/db-logos/phoenix.png differ diff --git a/client/app/assets/less/ant.less b/client/app/assets/less/ant.less index 337bc1d744..508a1e3660 100644 --- a/client/app/assets/less/ant.less +++ b/client/app/assets/less/ant.less @@ -20,7 +20,10 @@ @import '~antd/lib/tag/style/index'; @import '~antd/lib/grid/style/index'; @import '~antd/lib/switch/style/index'; +@import '~antd/lib/empty/style/index'; @import '~antd/lib/drawer/style/index'; +@import '~antd/lib/card/style/index'; +@import '~antd/lib/steps/style/index'; @import '~antd/lib/divider/style/index'; @import '~antd/lib/dropdown/style/index'; @import '~antd/lib/menu/style/index'; @@ -30,8 +33,26 @@ @import "~antd/lib/spin/style/index"; @import "~antd/lib/tabs/style/index"; @import "~antd/lib/notification/style/index"; +@import "~antd/lib/collapse/style/index"; +@import "~antd/lib/progress/style/index"; @import 'inc/ant-variables'; +// Increase z-indexes to avoid conflicts with some other libraries (e.g. Plotly) +@zindex-modal: 2000; +@zindex-modal-mask: 2000; +@zindex-message: 2010; +@zindex-notification: 2010; +@zindex-popover: 2030; +@zindex-dropdown: 2050; +@zindex-picker: 2050; +@zindex-tooltip: 2060; + +.@{drawer-prefix-cls} { + &.help-drawer { + z-index: @zindex-tooltip; // help drawer should be topmost + } +} + // Remove bold in labels for Ant checkboxes and radio buttons .ant-checkbox-wrapper, .ant-radio-wrapper { @@ -56,6 +77,7 @@ } // Fix for Ant dropdowns when they are used in Boootstrap modals +// ANGULAR_REMOVE_ME Remove when all dialogs will be migrated to React (also search and remove usages) .ant-dropdown-in-bootstrap-modal { z-index: 1050; } @@ -218,21 +240,61 @@ } } -// styling for short modals (no lines) -.@{dialog-prefix-cls}.shortModal { - .@{dialog-prefix-cls} { - &-header, - &-footer { - border: none; - padding: 16px; - } - &-body { - padding: 10px 16px; +.@{dialog-prefix-cls} { + // styling for short modals (no lines) + &.shortModal { + .@{dialog-prefix-cls} { + &-header, + &-footer { + border: none; + padding: 16px; + } + + &-body { + padding: 10px 16px; + } + + &-close-x { + width: 46px; + height: 46px; + line-height: 46px; + } } - &-close-x { - width: 46px; - height: 46px; - line-height: 46px; + } + + // fullscreen modals + &-fullscreen { + .@{dialog-prefix-cls} { + position: absolute; + left: 15px; + top: 15px; + right: 15px; + bottom: 15px; + width: auto !important; + height: auto !important; + max-width: none; + max-height: none; + margin: 0; + padding: 0; + + .@{dialog-prefix-cls}-content { + position: absolute; + left: 0; + top: 0; + right: 0; + bottom: 0; + width: auto; + height: auto; + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + } + + .@{dialog-prefix-cls}-body { + flex: 1 1 auto; + overflow: auto; + } } } } @@ -245,15 +307,41 @@ margin-top: 4px; } -.ant-popover { - z-index: 1000; // make sure it doesn't cover drawer +// Notification overrides +.@{notification-prefix-cls} { + // vertical centering + &-notice-close { + top: 20px; + right: 20px; + } + + &-notice-description { + max-width: 484px; + } } -// flexible width for notifications -.@{notification-prefix-cls} { - width: auto; +.@{btn-prefix-cls} .@{iconfont-css-prefix}-ellipsis { + margin: 0 -7px; +} + +// Collapse - &-notice { - padding-right: 48px; +.@{collapse-prefix-cls} { + &&-headerless { + border: 0; + background: none; + + .@{collapse-prefix-cls}-header { + display: none; + } + + .@{collapse-prefix-cls}-item, + .@{collapse-prefix-cls}-content { + border: 0; + } + + .@{collapse-prefix-cls}-content-box { + padding: 0; + } } } diff --git a/client/app/assets/less/inc/ant-variables.less b/client/app/assets/less/inc/ant-variables.less index 4e86e0b8b2..428990094a 100644 --- a/client/app/assets/less/inc/ant-variables.less +++ b/client/app/assets/less/inc/ant-variables.less @@ -72,3 +72,9 @@ @table-row-hover-bg: fade(@redash-gray, 5%); @table-padding-vertical: 7px; @table-padding-horizontal: 10px; + +/* -------------------------------------------------------- + Notification +-----------------------------------------------------------*/ +@notification-padding: @notification-padding-vertical 48px @notification-padding-vertical 17px; +@notification-width: auto; \ No newline at end of file diff --git a/client/app/assets/less/inc/base.less b/client/app/assets/less/inc/base.less index 46865650a9..b189802eb5 100755 --- a/client/app/assets/less/inc/base.less +++ b/client/app/assets/less/inc/base.less @@ -21,19 +21,24 @@ html, body { body { padding-top: @header-height; position: relative; - padding-bottom: @footer-height; &.headless { padding-top: 0; - padding-bottom: 0; .nav.app-header { display: none; } - div#footer { - display: none; - } } } +app-view { + min-height: 100vh; +} + +app-view, #app-content { + display: flex; + flex-direction: column; + flex-grow: 1; +} + strong { font-weight: 500; } diff --git a/client/app/assets/less/inc/footer.less b/client/app/assets/less/inc/footer.less deleted file mode 100755 index cbc1e10259..0000000000 --- a/client/app/assets/less/inc/footer.less +++ /dev/null @@ -1,48 +0,0 @@ -#footer { - position: absolute; - bottom: 0; - text-align: center; - width: 100%; - height: @footer-height; - color: #a2a2a2; - padding-top: 10px; - padding-bottom: 15px; - - .f-menu { - display: block; - width: 100%; - .list-inline(); - margin-top: 8px; - - & > li > a { - color: #a2a2a2; - - &:hover { - color: #777; - } - } - } - - @media (min-width: (@screen-lg-min + 80px)) { - padding-left: (@sidebar-left-width + @grid-gutter-width); - } - - @media (min-width: @screen-sm-min) and (max-width: (@screen-md-max + 80px)) { - padding-left: (@sidebar-left-mid-width + @grid-gutter-width); - } - - @media (max-width: (@screen-sm-min)) { - padding-left: @grid-gutter-width/2; - } -} - -.footer { - color: #818d9f; - padding-bottom: 30px; - a { - color: #818d9f; - margin-left: 20px; - } -} - - diff --git a/client/app/assets/less/inc/misc.less b/client/app/assets/less/inc/misc.less index 1594661000..cc7359b369 100755 --- a/client/app/assets/less/inc/misc.less +++ b/client/app/assets/less/inc/misc.less @@ -225,4 +225,13 @@ height: 37px; border-radius: 2px; width: 37px; +} + +/* -------------------------------------------------------- + Percy +-----------------------------------------------------------*/ +@media only percy { + .hide-in-percy, .pace { + visibility: hidden; + } } \ No newline at end of file diff --git a/client/app/assets/less/inc/print.less b/client/app/assets/less/inc/print.less index a41a8ba7ca..7209ae5f0d 100755 --- a/client/app/assets/less/inc/print.less +++ b/client/app/assets/less/inc/print.less @@ -12,7 +12,6 @@ #header, - #footer, #sidebar, #chat, .growl-animated, diff --git a/client/app/assets/less/inc/variables.less b/client/app/assets/less/inc/variables.less index f93cf25226..3bd9a746dd 100755 --- a/client/app/assets/less/inc/variables.less +++ b/client/app/assets/less/inc/variables.less @@ -17,7 +17,6 @@ Template Variables -----------------------------------------------------------*/ @header-height: 60px; -@footer-height: 95px; @sidebar-left-width: 240px; @sidebar-left-mid-width: 64px; @logo-width: @sidebar-left-width; diff --git a/client/app/assets/less/inc/visualizations/misc.less b/client/app/assets/less/inc/visualizations/misc.less index d439837db0..cc5600bd16 100644 --- a/client/app/assets/less/inc/visualizations/misc.less +++ b/client/app/assets/less/inc/visualizations/misc.less @@ -1,4 +1,6 @@ visualization-renderer { + display: block; + .pagination, .ant-pagination { margin: 0; diff --git a/client/app/assets/less/inc/visualizations/pivot-table.less b/client/app/assets/less/inc/visualizations/pivot-table.less index 9fa85ae5ca..7400914f47 100644 --- a/client/app/assets/less/inc/visualizations/pivot-table.less +++ b/client/app/assets/less/inc/visualizations/pivot-table.less @@ -1,3 +1,3 @@ -pivot-table-renderer > table, grid-renderer > div, visualization-renderer > div { +.pivot-table-renderer > table, grid-renderer > div, visualization-renderer > div { overflow: auto; } diff --git a/client/app/assets/less/main.less b/client/app/assets/less/main.less index fe84375419..4b7ef25beb 100644 --- a/client/app/assets/less/main.less +++ b/client/app/assets/less/main.less @@ -44,7 +44,6 @@ @import 'inc/jumbotron'; @import 'inc/profile'; @import 'inc/404'; -@import 'inc/footer'; @import 'inc/ie-warning'; @import 'inc/navbar'; @import 'inc/edit-in-place'; @@ -76,6 +75,7 @@ @import 'redash/redash-table'; @import 'redash/query'; @import 'redash/tags-control'; +@import 'redash/css-logo'; diff --git a/client/app/assets/less/redash/css-logo.less b/client/app/assets/less/redash/css-logo.less new file mode 100644 index 0000000000..36d3553b73 --- /dev/null +++ b/client/app/assets/less/redash/css-logo.less @@ -0,0 +1,88 @@ +// based on https://github.com/outbrain/tech-companies-logos-in-css/pull/28 + +@primary: #ff7964; +@shadow: #ef6c58; +@bar: white; + +#css-logo { + width: 100px; + height: 100px; + position: relative; + + #circle { + width: 79px; + height: 79px; + background-color: @shadow; + border-radius: 50%; + margin: auto; + overflow: hidden; + position: relative; + + & > div { + width: 79px; + height: 73px; + background-color: @primary; + border-radius: 50%; + position: absolute; + top: 0; + } + } + + #bars { + position: absolute; + left: 0; + top: 24px; + right: 0; + height: 33px; + display: flex; + padding: 0 22px 0; + + .bar { + background: @bar; + box-shadow: 0px 2px 0 0 @shadow; + display: inline-block; + border-radius: 1px; + align-self: flex-end; + flex: 1; + margin: 0 2px; + border-radius: 3px; + + &:nth-child(1) { + height: 32%; + } + + &:nth-child(2) { + height: 71%; + } + + &:nth-child(3) { + height: 50%; + } + + &:nth-child(4) { + height: 100%; + } + } + } + + #point, + #point > div { + position: absolute; + width: 0; + height: 0; + border: 17px solid @shadow; + border-right-color: transparent !important; + border-bottom-color: transparent !important; + bottom: 0; + left: 48px; + transform: scaleX(0.87); + transform-origin: left; + } + + #point > div { + bottom: -12px; + border-color: @primary; + transform: scaleX(1.04); + left: -17px; + } +} diff --git a/client/app/assets/less/redash/query.less b/client/app/assets/less/redash/query.less index 157c8b6823..5ef3b93699 100644 --- a/client/app/assets/less/redash/query.less +++ b/client/app/assets/less/redash/query.less @@ -172,6 +172,12 @@ edit-in-place p.editable:hover { } } +.query-log-line { + font-family: monospace; + white-space: pre; + margin: 0; +} + .paginator-container { text-align: center; } @@ -216,7 +222,7 @@ edit-in-place p.editable:hover { .widget-wrapper { .body-container { - filters { + .filters-wrapper { display: block; padding-left: 15px; } @@ -258,6 +264,7 @@ a.label-tag { .query-page-wrapper { display: flex; flex-direction: column; + flex-grow: 1; } .query-fullscreen { @@ -336,7 +343,7 @@ a.label-tag { border-bottom: 1px solid #efefef; } - pivot-table-renderer > table, grid-renderer > div, visualization-renderer > div { + .pivot-table-renderer > table, grid-renderer > div, visualization-renderer > div { overflow: visible; } @@ -551,6 +558,10 @@ nav .rg-bottom { text-transform: capitalize; } +.edit-visualization { + margin-right: 5px; +} + // Smaller screens @media (max-width: 880px) { diff --git a/client/app/assets/less/redash/redash-newstyle.less b/client/app/assets/less/redash/redash-newstyle.less index 97973d3bc0..86422405b9 100644 --- a/client/app/assets/less/redash/redash-newstyle.less +++ b/client/app/assets/less/redash/redash-newstyle.less @@ -23,15 +23,10 @@ body { &.headless { padding-top: 10px; - padding-bottom: 15px; .navbar { display: none !important; } - - div#footer { - display: none; - } } } @@ -242,10 +237,8 @@ body { float: right; } -.database-source { - display: inline-flex; - flex-wrap: wrap; - justify-content: center; +.visual-card-list { + margin: -5px 0 0 -5px; // compensate for .visual-card spacing } .visual-card { @@ -262,7 +255,6 @@ body { transition-property: box-shadow; display: flex; - //flex-direction: row; align-items: center; &:hover { @@ -284,31 +276,6 @@ body { } } -.visual-card--selected { - background: fade(@redash-gray, 3%); - border: 1px solid fade(@redash-gray, 15%); - border-radius: 3px; - padding: 0 15px; - box-shadow: none; - - display: flex; - flex-direction: row; - align-items: center; - - justify-content: space-around; - margin-bottom: 15px; - width: 100%; - - img { - width: 64px; - height: 64px; - } - - a { - cursor: pointer; - } -} - @media (max-width: 1200px) { .visual-card { width: 217px; @@ -336,7 +303,6 @@ body { .visual-card { width: 100%; padding: 5px; - margin: 5px 0; img { width: 48px; @@ -355,12 +321,6 @@ body { } } -#footer { - height: auto; - line-height: 3; - padding: 20px; -} - .page-header-wrapper, .page-header--new { h3 { margin: 0.2em 0; @@ -975,4 +935,64 @@ text.slicetext { .table-data .label-tag { display: inline-block; max-width: 135px; +} + +.markdown strong { + font-weight: bold; +} + +.markdown img { + max-width: 100%; +} + +.loading-indicator { + position: fixed; + top: 50%; + left: 50%; + margin: -50px 0 0 -50px; // center + width: 100px; + height: 100px; + transition-duration: 150ms; + transition-timing-function: linear; + transition-property: opacity, transform; + + #css-logo { + animation: hover 2s infinite; + } + + #shadow { + width: 33px; + height: 12px; + border-radius: 50%; + background-color: black; + opacity: 0.25; + display: block; + position: absolute; + left: 34px; + top: 115px; + animation: shadow 2s infinite; + } + + @keyframes hover { + 50% { + transform: translateY(-5px); + } + } + @keyframes shadow { + 50% { + transform: scaleX(0.9); + opacity: 0.2; + } + } +} + +// hide indicator when app-view has content +app-view:not(:empty) ~ .loading-indicator { + opacity: 0; + transform: scale(0.9); + pointer-events: none; + + * { + animation: none !important; + } } \ No newline at end of file diff --git a/client/app/components/CodeBlock.jsx b/client/app/components/CodeBlock.jsx new file mode 100644 index 0000000000..53b65589aa --- /dev/null +++ b/client/app/components/CodeBlock.jsx @@ -0,0 +1,80 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import Button from 'antd/lib/button'; +import Tooltip from 'antd/lib/tooltip'; +import './CodeBlock.less'; + +export default class CodeBlock extends React.Component { + static propTypes = { + copyable: PropTypes.bool, + children: PropTypes.node, + }; + + static defaultProps = { + copyable: false, + children: null, + }; + + state = { copied: null }; + + constructor(props) { + super(props); + this.ref = React.createRef(); + this.copyFeatureEnabled = props.copyable && document.queryCommandSupported('copy'); + this.resetCopyState = null; + } + + componentWillUnmount() { + if (this.resetCopyState) { + clearTimeout(this.resetCopyState); + } + } + + copy = () => { + // select text + window.getSelection().selectAllChildren(this.ref.current); + + // copy + try { + const success = document.execCommand('copy'); + if (!success) { + throw new Error(); + } + this.setState({ copied: 'Copied!' }); + } catch (err) { + this.setState({ + copied: 'Copy failed', + }); + } + + // reset selection + window.getSelection().removeAllRanges(); + + // reset tooltip + this.resetCopyState = setTimeout(() => this.setState({ copied: null }), 2000); + }; + + render() { + const { copyable, children, ...props } = this.props; + + const copyButton = ( +
+ {children}
+
+ {this.copyFeatureEnabled && copyButton}
+