diff --git a/.circleci/Dockerfile.cypress b/.circleci/Dockerfile.cypress index 3efef14f49..bae7308bc7 100644 --- a/.circleci/Dockerfile.cypress +++ b/.circleci/Dockerfile.cypress @@ -3,7 +3,7 @@ FROM cypress/browsers:chrome67 ENV APP /usr/src/app WORKDIR $APP -RUN npm install --no-save cypress @percy/cypress > /dev/null +RUN npm install --no-save puppeteer@1.10.0 cypress@^3.1.5 @percy/cypress@^0.2.3 > /dev/null COPY cypress $APP/cypress COPY cypress.json $APP/cypress.json diff --git a/.circleci/config.yml b/.circleci/config.yml index ac7e5bb0d1..1242d7cf18 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -73,6 +73,8 @@ jobs: command: | npm run cypress start docker-compose run cypress node ./cypress/cypress.js db-seed + # Make sure the API key is the same so Percy snapshots are consistent + docker-compose -p cypress run postgres psql -U postgres -h postgres -c "update users set api_key = 'secret' where email ='admin@redash.io';" - run: name: Execute Cypress tests command: npm run cypress run-ci diff --git a/.codeclimate.yml b/.codeclimate.yml index c17ce2e20a..8034b775f6 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -1,22 +1,38 @@ -engines: +version: "2" +checks: + complex-logic: + enabled: false + file-lines: + enabled: false + method-complexity: + enabled: false + method-count: + enabled: false + method-lines: + config: + threshold: 100 + nested-control-flow: + enabled: false + identical-code: + enabled: false +plugins: pep8: enabled: true eslint: enabled: true - channel: "eslint-3" + channel: "eslint-5" config: config: client/.eslintrc.js checks: import/no-unresolved: enabled: false -ratings: - paths: - - "redash/**/*.py" - - "client/**/*.js" -exclude_paths: -- tests/**/*.py -- migrations/**/*.py -- old_migrations/**/*.py -- setup/**/* -- bin/**/* - + no-multiple-empty-lines: # TODO: Enable + enabled: false +exclude_patterns: +- "tests/**/*.py" +- "migrations/**/*.py" +- "setup/**/*" +- "bin/**/*" +- "**/node_modules/" +- "client/dist/" +- "**/*.pyc" diff --git a/client/.eslintrc.js b/client/.eslintrc.js index 17552fcd15..525d82292b 100644 --- a/client/.eslintrc.js +++ b/client/.eslintrc.js @@ -26,15 +26,24 @@ module.exports = { "no-lonely-if": "off", "consistent-return": "off", "no-control-regex": "off", + 'no-multiple-empty-lines': 'warn', "no-script-url": "off", // some tags should have href="javascript:void(0)" + 'operator-linebreak': 'off', + 'react/destructuring-assignment': 'off', "react/jsx-filename-extension": "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/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-for": "off", "jsx-a11y/no-static-element-interactions": "off", "max-len": ['error', 120, 2, { @@ -43,6 +52,8 @@ module.exports = { ignoreRegExpLiterals: true, ignoreStrings: true, ignoreTemplateLiterals: true, - }] + }], + "no-else-return": ["error", {"allowElseIf": true}], + "object-curly-newline": ["error", {"consistent": true}], } }; diff --git a/client/app/assets/less/redash/redash-newstyle.less b/client/app/assets/less/redash/redash-newstyle.less index efb824d138..9dec10381c 100644 --- a/client/app/assets/less/redash/redash-newstyle.less +++ b/client/app/assets/less/redash/redash-newstyle.less @@ -472,7 +472,7 @@ body { .label-tag-archived, .label-tag { margin-right: 3px; - display: inline-block; + display: inline; margin-top: 2px; max-width: 24ch; .text-overflow(); @@ -940,3 +940,17 @@ text.slicetext { } } +.ui-select-choices-row > span { + background-color: inherit !important; +} + +.list-group-item.inactive, +.ui-select-choices-row.disabled { + background-color: #eee !important; + border-color: transparent; + opacity: 0.5; + box-shadow: none; + color: #333; + pointer-events: none; + cursor: not-allowed; +} diff --git a/client/app/assets/less/redash/tags-control.less b/client/app/assets/less/redash/tags-control.less index 3aaf402620..fabf788710 100644 --- a/client/app/assets/less/redash/tags-control.less +++ b/client/app/assets/less/redash/tags-control.less @@ -8,6 +8,11 @@ &.inline-tags-control { display: inline-block; - vertical-align: middle; } } + +// This is for using .inline-tags-control in Angular which renders +// a little differently than React (e.g. in Alert.html) +.inline-tags-control .tags-control { + display: inline-block; +} diff --git a/client/app/components/DateTimeRangeInput.jsx b/client/app/components/DateTimeRangeInput.jsx index 51f6c62ddc..668ac9a23e 100644 --- a/client/app/components/DateTimeRangeInput.jsx +++ b/client/app/components/DateTimeRangeInput.jsx @@ -61,4 +61,3 @@ export default function init(ngModule) { } init.init = true; - diff --git a/client/app/components/EditInPlace.jsx b/client/app/components/EditInPlace.jsx index 369f5d470b..5d06dbd87a 100644 --- a/client/app/components/EditInPlace.jsx +++ b/client/app/components/EditInPlace.jsx @@ -18,6 +18,7 @@ export class EditInPlace extends React.Component { placeholder: '', value: '', }; + constructor(props) { super(props); this.state = { @@ -67,14 +68,13 @@ export class EditInPlace extends React.Component { ); - renderEdit = () => - React.createElement(this.props.editor, { - ref: this.inputRef, - className: 'rd-form-control', - defaultValue: this.props.value, - onBlur: this.stopEditing, - onKeyDown: this.keyDown, - }); + 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 ( diff --git a/client/app/components/FavoritesControl.jsx b/client/app/components/FavoritesControl.jsx new file mode 100644 index 0000000000..57abd09ccb --- /dev/null +++ b/client/app/components/FavoritesControl.jsx @@ -0,0 +1,77 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { react2angular } from 'react2angular'; +import { $rootScope } from '@/services/ng'; + +function toggleItem(event, item, callback) { + event.preventDefault(); + event.stopPropagation(); + + const action = item.is_favorite ? item.$unfavorite.bind(item) : item.$favorite.bind(item); + const savedIsFavorite = item.is_favorite; + + action().then(() => { + item.is_favorite = !savedIsFavorite; + $rootScope.$broadcast('reloadFavorites'); + callback(); + }); +} + +export function FavoritesControl({ item, onChange }) { + const icon = item.is_favorite ? 'fa fa-star' : 'fa fa-star-o'; + const title = item.is_favorite ? 'Remove from favorites' : 'Add to favorites'; + return ( + toggleItem(event, item, onChange)} + > +