diff --git a/.circleci/Dockerfile.cypress b/.circleci/Dockerfile.cypress index ac8efd848b..d4e29881cc 100644 --- a/.circleci/Dockerfile.cypress +++ b/.circleci/Dockerfile.cypress @@ -6,7 +6,7 @@ WORKDIR $APP COPY package.json $APP/package.json RUN npm run cypress:install > /dev/null -COPY cypress $APP/cypress +COPY client/cypress $APP/client/cypress COPY cypress.json $APP/cypress.json RUN ./node_modules/.bin/cypress verify diff --git a/.circleci/config.yml b/.circleci/config.yml index b7f788dfbc..3716753f27 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -39,43 +39,32 @@ 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-unit-tests: + frontend-lint: docker: - image: circleci/node:8 steps: - checkout - - run: sudo apt install python-pip + - run: mkdir -p /tmp/test-results/eslint - 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: ZGRiY2ZmZDQ0OTdjMzM5ZWE0ZGQzNTZiOWNkMDRjOTk4Zjg0ZjMxMWRmMDZiM2RjOTYxNDZhOGExMjI4ZDE3MA== + - run: npm run lint:ci + - store_test_results: + path: /tmp/test-results + frontend-unit-tests: 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 + - run: sudo apt install python-pip + - run: npm install + - run: npm run bundle + - run: npm test + - run: npm run lint build-tarball: docker: - image: circleci/node:8 @@ -86,6 +75,7 @@ jobs: - run: .circleci/update_version - run: npm run bundle - run: npm run build + - run: rm -rf ./node_modules/ - run: .circleci/pack - store_artifacts: path: /tmp/artifacts/ @@ -95,35 +85,77 @@ jobs: steps: - setup_remote_docker - checkout - - run: .circleci/update_version - - run: .circleci/docker_build + - run: .circleci/update_version "master" + - run: .circleci/docker_build "master" + build-docker-image-rc: + docker: + - image: circleci/buildpack-deps:xenial + steps: + - setup_remote_docker + - checkout + - run: .circleci/update_version "rc" + - run: .circleci/docker_build "rc" + build-docker-image-tag: + docker: + - image: circleci/buildpack-deps:xenial + steps: + - setup_remote_docker + - checkout + - run: .circleci/update_version "$CIRCLE_TAG" + - run: .circleci/docker_build "rc" + # Create alias from tag to "latest": + - run: docker tag $DOCKERHUB_REPO:$CIRCLE_TAG $DOCKERHUB_REPO:latest + - run: docker push $DOCKERHUB_REPO:latest workflows: version: 2 build: jobs: - python-flake8-tests - legacy-python-flake8-tests - - backend-unit-tests - - frontend-unit-tests - - frontend-e2e-tests + - backend-unit-tests: + filters: + tags: + only: /^m[0-9]+(\.[0-9]+)?$/ + - frontend-lint: + filters: + tags: + only: /^m[0-9]+(\.[0-9]+)?$/ + - frontend-unit-tests: + requires: + - frontend-lint + filters: + tags: + only: /^m[0-9]+(\.[0-9]+)?$/ - build-tarball: requires: - backend-unit-tests - frontend-unit-tests - - frontend-e2e-tests filters: branches: only: - master - - /release\/.*/ - build-docker-image: requires: - backend-unit-tests - frontend-unit-tests - - frontend-e2e-tests filters: branches: only: - master - - preview-image - - /release\/.*/ + - build-docker-image-rc: + requires: + - backend-unit-tests + - frontend-unit-tests + filters: + branches: + only: + - release + - build-docker-image-tag: + requires: + - backend-unit-tests + - frontend-unit-tests + filters: + branches: + ignore: /.*/ + tags: + only: /^m[0-9]+(\.[0-9]+)?$/ diff --git a/.circleci/docker-compose.cypress.yml b/.circleci/docker-compose.cypress.yml index 2483582ce7..fb0ce5ed2f 100644 --- a/.circleci/docker-compose.cypress.yml +++ b/.circleci/docker-compose.cypress.yml @@ -13,6 +13,7 @@ services: REDASH_LOG_LEVEL: "INFO" REDASH_REDIS_URL: "redis://redis:6379/0" REDASH_DATABASE_URL: "postgresql://postgres@postgres/postgres" + REDASH_RATELIMIT_ENABLED: "false" worker: build: ../ command: scheduler @@ -38,6 +39,8 @@ services: PERCY_BRANCH: ${CIRCLE_BRANCH} PERCY_COMMIT: ${CIRCLE_SHA1} PERCY_PULL_REQUEST: ${CIRCLE_PR_NUMBER} + CYPRESS_PROJECT_ID: ${CYPRESS_PROJECT_ID} + CYPRESS_RECORD_KEY: ${CYPRESS_RECORD_KEY} redis: image: redis:3.0-alpine restart: unless-stopped diff --git a/.circleci/docker_build b/.circleci/docker_build index 50acc2f526..0de850dff8 100755 --- a/.circleci/docker_build +++ b/.circleci/docker_build @@ -1,17 +1,9 @@ #!/bin/bash -VERSION=$(jq -r .version package.json) -VERSION_TAG=$VERSION.b$CIRCLE_BUILD_NUM +VERSION_TAG="$1" docker login -u $DOCKER_USER -p $DOCKER_PASS -if [ $CIRCLE_BRANCH = master ] || [ $CIRCLE_BRANCH = preview-image ] -then - docker build -t redash/redash:preview -t redash/preview:$VERSION_TAG . - docker push redash/redash:preview - docker push redash/preview:$VERSION_TAG -else - docker build -t redash/redash:$VERSION_TAG . - docker push redash/redash:$VERSION_TAG -fi +docker build -t $DOCKERHUB_REPO:$VERSION_TAG . +docker push $DOCKERHUB_REPO:$VERSION_TAG echo "Built: $VERSION_TAG" \ No newline at end of file diff --git a/.circleci/pack b/.circleci/pack index 7d5810b684..16223c5a9b 100755 --- a/.circleci/pack +++ b/.circleci/pack @@ -6,4 +6,4 @@ FILENAME=$NAME.$FULL_VERSION.tar.gz mkdir -p /tmp/artifacts/ -tar -zcv -f /tmp/artifacts/$FILENAME --exclude="optipng*" --exclude=".git*" --exclude="*.pyc" --exclude="*.pyo" --exclude="venv" --exclude="node_modules" * +tar -zcv -f /tmp/artifacts/$FILENAME --exclude=".git" --exclude="optipng*" --exclude="cypress" --exclude="*.pyc" --exclude="*.pyo" --exclude="venv" * diff --git a/.circleci/update_version b/.circleci/update_version index d397fb23df..4bd4340765 100755 --- a/.circleci/update_version +++ b/.circleci/update_version @@ -1,6 +1,9 @@ #!/bin/bash +MOZILLA_VERSION="$1" +bin/dockerflow-version "$MOZILLA_VERSION" + VERSION=$(jq -r .version package.json) -FULL_VERSION=$VERSION+b$CIRCLE_BUILD_NUM +FULL_VERSION=$VERSION+b$CIRCLE_BUILD_NUM-$MOZILLA_VERSION 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/.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/.gitignore b/.gitignore index 7307021b73..350e1f610d 100644 --- a/.gitignore +++ b/.gitignore @@ -24,5 +24,5 @@ node_modules .sass-cache npm-debug.log -cypress/screenshots -cypress/videos +client/cypress/screenshots +client/cypress/videos diff --git a/bin/bundle-extensions b/bin/bundle-extensions index 8416aab776..56173f4c5f 100755 --- a/bin/bundle-extensions +++ b/bin/bundle-extensions @@ -22,9 +22,12 @@ 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) + + split_module_path = entry_point.module_name.split(os.extsep) + root_module = split_module_path.pop(0) + + content_folder_relative = os.path.join(os.path.join( + *split_module_path), 'bundle') if not resource_isdir(root_module, content_folder_relative): continue diff --git a/bin/docker-entrypoint b/bin/docker-entrypoint index c7d0e473b6..670ad670fc 100755 --- a/bin/docker-entrypoint +++ b/bin/docker-entrypoint @@ -2,24 +2,28 @@ set -e worker() { + /app/manage.py db upgrade 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() { + /app/manage.py db upgrade WORKERS_COUNT=${WORKERS_COUNT:-1} QUEUES=${QUEUES:-celery} SCHEDULE_DB=${SCHEDULE_DB:-celerybeat-schedule} 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() { + /app/manage.py db upgrade exec /usr/local/bin/gunicorn -b 0.0.0.0:5000 --name redash -w${REDASH_WEB_WORKERS:-4} redash.wsgi:app } diff --git a/bin/dockerflow-version b/bin/dockerflow-version new file mode 100755 index 0000000000..027d61971f --- /dev/null +++ b/bin/dockerflow-version @@ -0,0 +1,13 @@ +#!/bin/bash + +set -eo pipefail + +VERSION="$1" + +printf '{"commit":"%s","version":"%s","source":"https://github.com/%s/%s","build":"%s"}\n' \ + "$CIRCLE_SHA1" \ + "$VERSION" \ + "$CIRCLE_PROJECT_USERNAME" \ + "$CIRCLE_PROJECT_REPONAME" \ + "$CIRCLE_BUILD_URL" \ +> version.json diff --git a/bin/pack b/bin/pack deleted file mode 100755 index 881e2fd435..0000000000 --- a/bin/pack +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/bash -NAME=redash -VERSION=$(python ./manage.py version) -FULL_VERSION=$VERSION+b$CIRCLE_BUILD_NUM -FILENAME=$NAME.$FULL_VERSION.tar.gz - -sed -ri "s/^__version__ = '([A-Za-z0-9.-]*)'/__version__ = '$FULL_VERSION'/" redash/__init__.py -tar -zcv -f $FILENAME --exclude="optipng*" --exclude=".git*" --exclude="*.pyc" --exclude="*.pyo" --exclude="venv" --exclude="node_modules" * 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/.eslintignore b/client/.eslintignore index 26eb606610..24669a7008 100644 --- a/client/.eslintignore +++ b/client/.eslintignore @@ -1,3 +1,3 @@ build/*.js config/*.js -node_modules +client/dist diff --git a/client/.eslintrc.js b/client/.eslintrc.js index 73caba79e3..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"], + 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,34 +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}], + "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/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 c42a83e2a7..808ce5a208 100644 --- a/client/app/assets/less/ant.less +++ b/client/app/assets/less/ant.less @@ -14,13 +14,17 @@ @import '~antd/lib/radio/style/index'; @import '~antd/lib/time-picker/style/index'; @import '~antd/lib/pagination/style/index'; +@import '~antd/lib/drawer/style/index'; @import '~antd/lib/table/style/index'; @import '~antd/lib/popover/style/index'; @import '~antd/lib/icon/style/index'; @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'; @@ -29,6 +33,7 @@ @import "~antd/lib/card/style/index"; @import "~antd/lib/spin/style/index"; @import "~antd/lib/tabs/style/index"; +@import "~antd/lib/notification/style/index"; @import 'inc/ant-variables'; // Remove bold in labels for Ant checkboxes and radio buttons @@ -217,21 +222,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; + } } } } @@ -247,3 +292,20 @@ .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; + } +} + +.@{btn-prefix-cls} .@{iconfont-css-prefix}-ellipsis { + margin: 0 -7px; +} 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/schema-browser.less b/client/app/assets/less/inc/schema-browser.less index 0034391086..d547a78790 100644 --- a/client/app/assets/less/inc/schema-browser.less +++ b/client/app/assets/less/inc/schema-browser.less @@ -7,14 +7,14 @@ div.table-name { border-radius: @redash-radius; position: relative; - .copy-to-editor { + .copy-to-editor, .info { display: none; } &:hover { background: fade(@redash-gray, 10%); - .copy-to-editor { + .copy-to-editor, .info { display: flex; } } @@ -36,7 +36,7 @@ div.table-name { background: transparent; } - .copy-to-editor { + .copy-to-editor, .info { color: fade(@redash-gray, 90%); cursor: pointer; position: absolute; @@ -49,6 +49,10 @@ div.table-name { justify-content: center; } + .info { + right: 20px + } + .table-open { padding: 0 22px 0 26px; overflow: hidden; @@ -56,14 +60,14 @@ div.table-name { white-space: nowrap; position: relative; - .copy-to-editor { + .copy-to-editor, .info { display: none; } &:hover { background: fade(@redash-gray, 10%); - .copy-to-editor { + .copy-to-editor, .info { display: flex; } } 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 8a6ba78c19..45e5dc3ce5 100644 --- a/client/app/assets/less/main.less +++ b/client/app/assets/less/main.less @@ -7,7 +7,6 @@ /** Load Vendors Dependencies **/ @import '~font-awesome/less/font-awesome'; @import '~ui-select/dist/select.css'; -@import '~angular-toastr/src/toastr'; @import '~angular-resizable/src/angular-resizable.css'; @import '~material-design-iconic-font/dist/css/material-design-iconic-font.css'; @import '~pace-progress/themes/blue/pace-theme-minimal.css'; @@ -45,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'; diff --git a/client/app/assets/less/redash/query.less b/client/app/assets/less/redash/query.less index 157c8b6823..cb7371609d 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; } @@ -460,6 +467,7 @@ a.label-tag { .datasource-small { visibility: hidden; + display: none !important; } .query-fullscreen .query-metadata__mobile { @@ -551,6 +559,10 @@ nav .rg-bottom { text-transform: capitalize; } +.edit-visualization { + margin-right: 5px; +} + // Smaller screens @media (max-width: 880px) { @@ -582,6 +594,11 @@ nav .rg-bottom { display: none; } + .datasource-small { + visibility: visible; + display: inline-block !important; + } + .query-fullscreen { flex-direction: column; overflow: hidden; diff --git a/client/app/assets/less/redash/redash-newstyle.less b/client/app/assets/less/redash/redash-newstyle.less index 97973d3bc0..5cf914c1da 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; - } } } @@ -83,6 +78,10 @@ body { } } +.admin-schema-editor { + padding: 50px 0; +} + .creation-container { h5 { color: #a7a7a7; @@ -242,12 +241,6 @@ body { float: right; } -.database-source { - display: inline-flex; - flex-wrap: wrap; - justify-content: center; -} - .visual-card { background: #FFFFFF; border: 1px solid fade(@redash-gray, 15%); @@ -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; @@ -355,12 +322,6 @@ body { } } -#footer { - height: auto; - line-height: 3; - padding: 20px; -} - .page-header-wrapper, .page-header--new { h3 { margin: 0.2em 0; @@ -975,4 +936,8 @@ text.slicetext { .table-data .label-tag { display: inline-block; max-width: 135px; +} + +.markdown strong { + font-weight: bold; } \ No newline at end of file diff --git a/client/app/components/ColorBox.jsx b/client/app/components/ColorBox.jsx new file mode 100644 index 0000000000..73b3f3681e --- /dev/null +++ b/client/app/components/ColorBox.jsx @@ -0,0 +1,23 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { react2angular } from 'react2angular'; + +import './color-box.less'; + +export function ColorBox({ color }) { + return ; +} + +ColorBox.propTypes = { + color: PropTypes.string, +}; + +ColorBox.defaultProps = { + color: 'transparent', +}; + +export default function init(ngModule) { + ngModule.component('colorBox', react2angular(ColorBox)); +} + +init.init = true; diff --git a/client/app/components/CreateSourceDialog.jsx b/client/app/components/CreateSourceDialog.jsx new file mode 100644 index 0000000000..039fbedec3 --- /dev/null +++ b/client/app/components/CreateSourceDialog.jsx @@ -0,0 +1,191 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { isEmpty, toUpper, includes } from 'lodash'; +import Button from 'antd/lib/button'; +import List from 'antd/lib/list'; +import Modal from 'antd/lib/modal'; +import Input from 'antd/lib/input'; +import Steps from 'antd/lib/steps'; +import { wrap as wrapDialog, DialogPropType } from '@/components/DialogWrapper'; +import { PreviewCard } from '@/components/PreviewCard'; +import EmptyState from '@/components/items-list/components/EmptyState'; +import DynamicForm from '@/components/dynamic-form/DynamicForm'; +import helper from '@/components/dynamic-form/dynamicFormHelper'; +import { HelpTrigger, TYPES as HELP_TRIGGER_TYPES } from '@/components/HelpTrigger'; + +const { Step } = Steps; +const { Search } = Input; + +const StepEnum = { + SELECT_TYPE: 0, + CONFIGURE_IT: 1, + DONE: 2, +}; + +class CreateSourceDialog extends React.Component { + static propTypes = { + dialog: DialogPropType.isRequired, + types: PropTypes.arrayOf(PropTypes.object), + sourceType: PropTypes.string.isRequired, + imageFolder: PropTypes.string.isRequired, + helpTriggerPrefix: PropTypes.string, + onCreate: PropTypes.func.isRequired, + }; + + static defaultProps = { + types: [], + helpTriggerPrefix: null, + }; + + state = { + searchText: '', + selectedType: null, + savingSource: false, + currentStep: StepEnum.SELECT_TYPE, + }; + + selectType = (selectedType) => { + this.setState({ selectedType, currentStep: StepEnum.CONFIGURE_IT }); + }; + + resetType = () => { + if (this.state.currentStep === StepEnum.CONFIGURE_IT) { + this.setState({ searchText: '', selectedType: null, currentStep: StepEnum.SELECT_TYPE }); + } + }; + + createSource = (values, successCallback, errorCallback) => { + const { selectedType, savingSource } = this.state; + if (!savingSource) { + this.setState({ savingSource: true, currentStep: StepEnum.DONE }); + this.props.onCreate(selectedType, values).then((data) => { + successCallback('Saved.'); + this.props.dialog.close({ success: true, data }); + }).catch((error) => { + this.setState({ savingSource: false, currentStep: StepEnum.CONFIGURE_IT }); + errorCallback(error.message); + }); + } + }; + + renderTypeSelector() { + const { types } = this.props; + const { searchText } = this.state; + const filteredTypes = types.filter(type => isEmpty(searchText) || + includes(type.name.toLowerCase(), searchText.toLowerCase())); + return ( +
+ this.setState({ searchText: e.target.value })} + autoFocus + data-test="SearchSource" + /> +
+ {isEmpty(filteredTypes) ? () : ( + this.renderItem(item)} + /> + )} +
+
+ ); + } + + renderForm() { + const { imageFolder, helpTriggerPrefix } = this.props; + const { selectedType } = this.state; + const fields = helper.getFields(selectedType); + const helpTriggerType = `${helpTriggerPrefix}${toUpper(selectedType.type)}`; + return ( +
+
+ {selectedType.name} +

{selectedType.name}

+
+
+ {HELP_TRIGGER_TYPES[helpTriggerType] && ( + + Setup Instructions + + )} +
+ +
+ ); + } + + renderItem(item) { + const { imageFolder } = this.props; + return ( + this.selectType(item)} + > + + + + + ); + } + + render() { + const { currentStep, savingSource } = this.state; + const { dialog, sourceType } = this.props; + return ( + dialog.dismiss()}>Cancel), + (), + ] : [ + (), + ( + + ), + ]} + > +
+ + {currentStep === StepEnum.CONFIGURE_IT ? ( + Type Selection} + className="clickable" + onClick={this.resetType} + /> + ) : ()} + + + + {currentStep === StepEnum.SELECT_TYPE && this.renderTypeSelector()} + {currentStep !== StepEnum.SELECT_TYPE && this.renderForm()} +
+ + ); + } +} + +export default wrapDialog(CreateSourceDialog); diff --git a/client/app/components/EditParameterSettingsDialog.jsx b/client/app/components/EditParameterSettingsDialog.jsx index 01b0504156..4f26ec9803 100644 --- a/client/app/components/EditParameterSettingsDialog.jsx +++ b/client/app/components/EditParameterSettingsDialog.jsx @@ -131,7 +131,7 @@ function EditParameterSettingsDialog(props) { footer={[( ), ( - )]} @@ -153,9 +153,9 @@ function EditParameterSettingsDialog(props) { /> - setParam({ ...param, type })} data-test="ParameterTypeSelect"> + +