diff --git a/.github/workflows/bashlib.sh b/.github/workflows/bashlib.sh index 5633041f7bfb..df4beaf0ed02 100644 --- a/.github/workflows/bashlib.sh +++ b/.github/workflows/bashlib.sh @@ -129,12 +129,6 @@ testdata() { say "::endgroup::" } -codecov() { - say "::group::Upload code coverage" - bash ".github/workflows/codecov.sh" "$@" - say "::endgroup::" -} - cypress-install() { cd "$GITHUB_WORKSPACE/superset-frontend/cypress-base" @@ -203,11 +197,6 @@ cypress-run-all() { cypress-run "sqllab/*" "Backend persist" - # Upload code coverage separately so each page can have separate flags - # -c will clean existing coverage reports, -F means add flags - # || true to prevent CI failure on codecov upload - codecov -c -F "cypress" || true - say "::group::Flask log for backend persist" cat "$flasklog" say "::endgroup::" @@ -237,8 +226,6 @@ cypress-run-applitools() { $cypress --spec "cypress/e2e/*/**/*.applitools.test.ts" --browser "$browser" --headless --config ignoreTestFiles="[]" - codecov -c -F "cypress" || true - say "::group::Flask log for default run" cat "$flasklog" say "::endgroup::" diff --git a/.github/workflows/check_db_migration_confict.yml b/.github/workflows/check_db_migration_confict.yml index 637252ab3b06..079ba954edcf 100644 --- a/.github/workflows/check_db_migration_confict.yml +++ b/.github/workflows/check_db_migration_confict.yml @@ -4,7 +4,8 @@ on: paths: - "superset/migrations/**" branches: - - 'master' + - "master" + - "[0-9].[0-9]" pull_request: paths: - "superset/migrations/**" diff --git a/.github/workflows/chromatic-master.yml b/.github/workflows/chromatic-master.yml deleted file mode 100644 index d08d2be76ddf..000000000000 --- a/.github/workflows/chromatic-master.yml +++ /dev/null @@ -1,72 +0,0 @@ -# .github/workflows/chromatic.yml -# see https://www.chromatic.com/docs/github-actions -# -# Licensed to the Apache Software Foundation (ASF) under one or more -# contributor license agreements. See the NOTICE file distributed with -# this work for additional information regarding copyright ownership. -# The ASF licenses this file to You under the Apache License, Version 2.0 -# (the "License"); you may not use this file except in compliance with -# the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -# Workflow name -name: 'Chromatic Storybook Master' - -# Event for the workflow -# Only run if changes were made in superset-frontend folder of repo on merge to Master -on: - # This will trigger when a branch merges to master when the PR has changes in the frontend folder updating the chromatic baseline - push: - branches: - - master - paths: - - "superset-frontend/**" - -# List of jobs -jobs: - config: - runs-on: "ubuntu-latest" - outputs: - has-secrets: ${{ steps.check.outputs.has-secrets }} - steps: - - name: "Check for secrets" - id: check - shell: bash - run: | - if [ -n "${{ (secrets.CHROMATIC_PROJECT_TOKEN != '') || '' }}" ]; then - echo "has-secrets=1" >> "$GITHUB_OUTPUT" - fi - - chromatic-deployment: - needs: config - if: needs.config.outputs.has-secrets - # Operating System - runs-on: ubuntu-latest - # Job steps - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 # 👈 Required to retrieve git history - - name: Install dependencies - run: npm ci - working-directory: superset-frontend - # 👇 Build and publish Storybook to Chromatic - - name: Build and publish Storybook to Chromatic - id: chromatic-master - uses: chromaui/action@v10 - # Required options for the Chromatic GitHub Action - with: - # 👇 Location of package.json from root of mono-repo - workingDir: superset-frontend - # 👇 Chromatic projectToken, refer to the manage page to obtain it. - projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }} - exitZeroOnChanges: true # 👈 Option to prevent the workflow from failing - autoAcceptChanges: true # 👈 Option to accept all changes when merging to master diff --git a/.github/workflows/codecov.sh b/.github/workflows/codecov.sh deleted file mode 100755 index 75c13b7643af..000000000000 --- a/.github/workflows/codecov.sh +++ /dev/null @@ -1,1903 +0,0 @@ -#!/usr/bin/env bash -# -# Licensed to the Apache Software Foundation (ASF) under one or more -# contributor license agreements. See the NOTICE file distributed with -# this work for additional information regarding copyright ownership. -# The ASF licenses this file to You under the Apache License, Version 2.0 -# (the "License"); you may not use this file except in compliance with -# the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -# -# Note: this script was fetched from https://codecov.io/bash in response -# to a breach that occurred around it in Jan 2021 (https://about.codecov.io/security-update/) -# This script should probably be periodically updated and reviewed from the above URL :) -# - -set -e +o pipefail - -VERSION="1.0.1" - -codecov_flags=( ) -url="https://codecov.io" -env="$CODECOV_ENV" -service="" -token="" -search_in="" -# shellcheck disable=SC2153 -flags="$CODECOV_FLAGS" -exit_with=0 -curlargs="" -curlawsargs="" -dump="0" -clean="0" -curl_s="-s" -name="$CODECOV_NAME" -include_cov="" -exclude_cov="" -ddp="$HOME/Library/Developer/Xcode/DerivedData" -xp="" -files="" -save_to="" -direct_file_upload="" -cacert="$CODECOV_CA_BUNDLE" -gcov_ignore="-not -path './bower_components/**' -not -path './node_modules/**' -not -path './vendor/**'" -gcov_include="" - -ft_gcov="1" -ft_coveragepy="1" -ft_fix="1" -ft_search="1" -ft_s3="1" -ft_network="1" -ft_xcodellvm="1" -ft_xcodeplist="0" -ft_gcovout="1" -ft_html="0" -ft_yaml="0" - -_git_root=$(git rev-parse --show-toplevel 2>/dev/null || hg root 2>/dev/null || echo "$PWD") -git_root="$_git_root" -remote_addr="" -if [ "$git_root" = "$PWD" ]; -then - git_root="." -fi - -branch_o="" -build_o="" -commit_o="" -pr_o="" -prefix_o="" -network_filter_o="" -search_in_o="" -slug_o="" -tag_o="" -url_o="" -git_ls_files_recurse_submodules_o="" -package="bash" - -commit="$VCS_COMMIT_ID" -branch="$VCS_BRANCH_NAME" -pr="$VCS_PULL_REQUEST" -slug="$VCS_SLUG" -tag="$VCS_TAG" -build_url="$CI_BUILD_URL" -build="$CI_BUILD_ID" -job="$CI_JOB_ID" - -beta_xcode_partials="" - -proj_root="$git_root" -gcov_exe="gcov" -gcov_arg="" - -b="\033[0;36m" -g="\033[0;32m" -r="\033[0;31m" -e="\033[0;90m" -y="\033[0;33m" -x="\033[0m" - -show_help() { -cat << EOF - - Codecov Bash $VERSION - - Global report uploading tool for Codecov - Documentation at https://docs.codecov.io/docs - Contribute at https://github.com/codecov/codecov-bash - - - -h Display this help and exit - -f FILE Target file(s) to upload - - -f "path/to/file" only upload this file - skips searching unless provided patterns below - - -f '!*.bar' ignore all files at pattern *.bar - -f '*.foo' include all files at pattern *.foo - Must use single quotes. - This is non-exclusive, use -s "*.foo" to match specific paths. - - -s DIR Directory to search for coverage reports. - Already searches project root and artifact folders. - -t TOKEN Set the private repository token - (option) set environment variable CODECOV_TOKEN=:uuid - - -t @/path/to/token_file - -t uuid - - -n NAME Custom defined name of the upload. Visible in Codecov UI - - -e ENV Specify environment variables to be included with this build - Also accepting environment variables: CODECOV_ENV=VAR,VAR2 - - -e VAR,VAR2 - - -k prefix Prefix filepaths to help resolve path fixing - - -i prefix Only include files in the network with a certain prefix. Useful for upload-specific path fixing - - -X feature Toggle functionalities - - -X gcov Disable gcov - -X coveragepy Disable python coverage - -X fix Disable report fixing - -X search Disable searching for reports - -X xcode Disable xcode processing - -X network Disable uploading the file network - -X gcovout Disable gcov output - -X html Enable coverage for HTML files - -X recursesubs Enable recurse submodules in git projects when searching for source files - -X yaml Enable coverage for YAML files - - -N The commit SHA of the parent for which you are uploading coverage. If not present, - the parent will be determined using the API of your repository provider. - When using the repository provider's API, the parent is determined via finding - the closest ancestor to the commit. - - -R root dir Used when not in git/hg project to identify project root directory - -F flag Flag the upload to group coverage metrics - - -F unittests This upload is only unittests - -F integration This upload is only integration tests - -F ui,chrome This upload is Chrome - UI tests - - -c Move discovered coverage reports to the trash - -z FILE Upload specified file directly to Codecov and bypass all report generation. - This is intended to be used only with a pre-formatted Codecov report and is not - expected to work under any other circumstances. - -Z Exit with 1 if not successful. Default will Exit with 0 - - -- xcode -- - -D Custom Derived Data Path for Coverage.profdata and gcov processing - Default '~/Library/Developer/Xcode/DerivedData' - -J Specify packages to build coverage. Uploader will only build these packages. - This can significantly reduces time to build coverage reports. - - -J 'MyAppName' Will match "MyAppName" and "MyAppNameTests" - -J '^ExampleApp$' Will match only "ExampleApp" not "ExampleAppTests" - - -- gcov -- - -g GLOB Paths to ignore during gcov gathering - -G GLOB Paths to include during gcov gathering - -p dir Project root directory - Also used when preparing gcov - -x gcovexe gcov executable to run. Defaults to 'gcov' - -a gcovargs extra arguments to pass to gcov - - -- Override CI Environment Variables -- - These variables are automatically detected by popular CI providers - - -B branch Specify the branch name - -C sha Specify the commit sha - -P pr Specify the pull request number - -b build Specify the build number - -T tag Specify the git tag - - -- Enterprise -- - -u URL Set the target url for Enterprise customers - Not required when retrieving the bash uploader from your CCE - (option) Set environment variable CODECOV_URL=https://my-hosted-codecov.com - -r SLUG owner/repo slug used instead of the private repo token in Enterprise - (option) set environment variable CODECOV_SLUG=:owner/:repo - (option) set in your codecov.yml "codecov.slug" - -S PATH File path to your cacert.pem file used to verify ssl with Codecov Enterprise (optional) - (option) Set environment variable: CODECOV_CA_BUNDLE="/path/to/ca.pem" - -U curlargs Extra curl arguments to communicate with Codecov. e.g., -U "--proxy http://http-proxy" - -A curlargs Extra curl arguments to communicate with AWS. - - -- Debugging -- - -d Don't upload, but dump upload file to stdout - -q PATH Write upload file to path - -K Remove color from the output - -v Verbose mode - -EOF -} - - -say() { - echo -e "$1" -} - - -urlencode() { - echo "$1" | curl -Gso /dev/null -w "%{url_effective}" --data-urlencode @- "" | cut -c 3- | sed -e 's/%0A//' -} - -swiftcov() { - _dir=$(dirname "$1" | sed 's/\(Build\).*/\1/g') - for _type in app framework xctest - do - find "$_dir" -name "*.$_type" | while read -r f - do - _proj=${f##*/} - _proj=${_proj%."$_type"} - if [ "$2" = "" ] || [ "$(echo "$_proj" | grep -i "$2")" != "" ]; - then - say " $g+$x Building reports for $_proj $_type" - dest=$([ -f "$f/$_proj" ] && echo "$f/$_proj" || echo "$f/Contents/MacOS/$_proj") - # shellcheck disable=SC2001 - _proj_name=$(echo "$_proj" | sed -e 's/[[:space:]]//g') - # shellcheck disable=SC2086 - xcrun llvm-cov show $beta_xcode_partials -instr-profile "$1" "$dest" > "$_proj_name.$_type.coverage.txt" \ - || say " ${r}x>${x} llvm-cov failed to produce results for $dest" - fi - done - done -} - - -# Credits to: https://gist.github.com/pkuczynski/8665367 -parse_yaml() { - local prefix=$2 - local s='[[:space:]]*' w='[a-zA-Z0-9_]*' - local fs - fs=$(echo @|tr @ '\034') - sed -ne "s|^\($s\)\($w\)$s:$s\"\(.*\)\"$s\$|\1$fs\2$fs\3|p" \ - -e "s|^\($s\)\($w\)$s:$s\(.*\)$s\$|\1$fs\2$fs\3|p" "$1" | - awk -F"$fs" '{ - indent = length($1)/2; - vname[indent] = $2; - for (i in vname) {if (i > indent) {delete vname[i]}} - if (length($3) > 0) { - vn=""; if (indent > 0) {vn=(vn)(vname[0])("_")} - printf("%s%s%s=\"%s\"\n", "'"$prefix"'",vn, $2, $3); - } - }' -} - -if [ $# != 0 ]; -then - while getopts "a:A:b:B:cC:dD:e:f:F:g:G:hi:J:k:Kn:p:P:Q:q:r:R:s:S:t:T:u:U:vx:X:Zz:N:-" o - do - codecov_flags+=( "$o" ) - case "$o" in - "-") - echo -e "${r}Long options are not supported${x}" - exit 2 - ;; - "?") - ;; - "N") - parent=$OPTARG - ;; - "a") - gcov_arg=$OPTARG - ;; - "A") - curlawsargs="$OPTARG" - ;; - "b") - build_o="$OPTARG" - ;; - "B") - branch_o="$OPTARG" - ;; - "c") - clean="1" - ;; - "C") - commit_o="$OPTARG" - ;; - "d") - dump="1" - ;; - "D") - ddp="$OPTARG" - ;; - "e") - env="$env,$OPTARG" - ;; - "f") - if [ "${OPTARG::1}" = "!" ]; - then - exclude_cov="$exclude_cov -not -path '${OPTARG:1}'" - - elif [[ "$OPTARG" = *"*"* ]]; - then - include_cov="$include_cov -or -path '$OPTARG'" - - else - ft_search=0 - if [ "$files" = "" ]; - then - files="$OPTARG" - else - files="$files -$OPTARG" - fi - fi - ;; - "F") - if [ "$flags" = "" ]; - then - flags="$OPTARG" - else - flags="$flags,$OPTARG" - fi - ;; - "g") - gcov_ignore="$gcov_ignore -not -path '$OPTARG'" - ;; - "G") - gcov_include="$gcov_include -path '$OPTARG'" - ;; - "h") - show_help - exit 0; - ;; - "i") - network_filter_o="$OPTARG" - ;; - "J") - ft_xcodellvm="1" - ft_xcodeplist="0" - if [ "$xp" = "" ]; - then - xp="$OPTARG" - else - xp="$xp\|$OPTARG" - fi - ;; - "k") - prefix_o=$(echo "$OPTARG" | sed -e 's:^/*::' -e 's:/*$::') - ;; - "K") - b="" - g="" - r="" - e="" - x="" - ;; - "n") - name="$OPTARG" - ;; - "p") - proj_root="$OPTARG" - ;; - "P") - pr_o="$OPTARG" - ;; - "Q") - # this is only meant for Codecov packages to overwrite - package="$OPTARG" - ;; - "q") - save_to="$OPTARG" - ;; - "r") - slug_o="$OPTARG" - ;; - "R") - git_root="$OPTARG" - ;; - "s") - if [ "$search_in_o" = "" ]; - then - search_in_o="$OPTARG" - else - search_in_o="$search_in_o $OPTARG" - fi - ;; - "S") - # shellcheck disable=SC2089 - cacert="--cacert \"$OPTARG\"" - ;; - "t") - if [ "${OPTARG::1}" = "@" ]; - then - token=$(< "${OPTARG:1}" tr -d ' \n') - else - token="$OPTARG" - fi - ;; - "T") - tag_o="$OPTARG" - ;; - "u") - url_o=$(echo "$OPTARG" | sed -e 's/\/$//') - ;; - "U") - curlargs="$OPTARG" - ;; - "v") - set -x - curl_s="" - ;; - "x") - gcov_exe=$OPTARG - ;; - "X") - if [ "$OPTARG" = "gcov" ]; - then - ft_gcov="0" - elif [ "$OPTARG" = "coveragepy" ] || [ "$OPTARG" = "py" ]; - then - ft_coveragepy="0" - elif [ "$OPTARG" = "gcovout" ]; - then - ft_gcovout="0" - elif [ "$OPTARG" = "xcodellvm" ]; - then - ft_xcodellvm="1" - ft_xcodeplist="0" - elif [ "$OPTARG" = "fix" ] || [ "$OPTARG" = "fixes" ]; - then - ft_fix="0" - elif [ "$OPTARG" = "xcode" ]; - then - ft_xcodellvm="0" - ft_xcodeplist="0" - elif [ "$OPTARG" = "search" ]; - then - ft_search="0" - elif [ "$OPTARG" = "xcodepartials" ]; - then - beta_xcode_partials="-use-color" - elif [ "$OPTARG" = "network" ]; - then - ft_network="0" - elif [ "$OPTARG" = "s3" ]; - then - ft_s3="0" - elif [ "$OPTARG" = "html" ]; - then - ft_html="1" - elif [ "$OPTARG" = "recursesubs" ]; - then - git_ls_files_recurse_submodules_o="--recurse-submodules" - elif [ "$OPTARG" = "yaml" ]; - then - ft_yaml="1" - fi - ;; - "Z") - exit_with=1 - ;; - "z") - direct_file_upload="$OPTARG" - ft_gcov="0" - ft_coveragepy="0" - ft_fix="0" - ft_search="0" - ft_network="0" - ft_xcodellvm="0" - ft_gcovout="0" - include_cov="" - ;; - *) - echo -e "${r}Unexpected flag not supported${x}" - ;; - esac - done -fi - -say " - _____ _ - / ____| | | -| | ___ __| | ___ ___ _____ __ -| | / _ \\ / _\` |/ _ \\/ __/ _ \\ \\ / / -| |___| (_) | (_| | __/ (_| (_) \\ V / - \\_____\\___/ \\__,_|\\___|\\___\\___/ \\_/ - Bash-$VERSION - -" - -# check for installed tools -# git/hg -if [ "$direct_file_upload" = "" ]; -then - if [ -x "$(command -v git)" ]; - then - say "$b==>$x $(git --version) found" - else - say "$y==>$x git not installed, testing for mercurial" - if [ -x "$(command -v hg)" ]; - then - say "$b==>$x $(hg --version) found" - else - say "$r==>$x git nor mercurial are installed. Uploader may fail or have unintended consequences" - fi - fi -fi -# curl -if [ -x "$(command -v curl)" ]; -then - say "$b==>$x $(curl --version)" -else - say "$r==>$x curl not installed. Exiting." - exit ${exit_with}; -fi - -search_in="$proj_root" - -#shellcheck disable=SC2154 -if [ "$JENKINS_URL" != "" ]; -then - say "$e==>$x Jenkins CI detected." - # https://wiki.jenkins-ci.org/display/JENKINS/Building+a+software+project - # https://wiki.jenkins-ci.org/display/JENKINS/GitHub+pull+request+builder+plugin#GitHubpullrequestbuilderplugin-EnvironmentVariables - service="jenkins" - - # shellcheck disable=SC2154 - if [ "$ghprbSourceBranch" != "" ]; - then - branch="$ghprbSourceBranch" - elif [ "$GIT_BRANCH" != "" ]; - then - branch="$GIT_BRANCH" - elif [ "$BRANCH_NAME" != "" ]; - then - branch="$BRANCH_NAME" - fi - - # shellcheck disable=SC2154 - if [ "$ghprbActualCommit" != "" ]; - then - commit="$ghprbActualCommit" - elif [ "$GIT_COMMIT" != "" ]; - then - commit="$GIT_COMMIT" - fi - - # shellcheck disable=SC2154 - if [ "$ghprbPullId" != "" ]; - then - pr="$ghprbPullId" - elif [ "$CHANGE_ID" != "" ]; - then - pr="$CHANGE_ID" - fi - - build="$BUILD_NUMBER" - # shellcheck disable=SC2153 - build_url=$(urlencode "$BUILD_URL") - -elif [ "$CI" = "true" ] && [ "$TRAVIS" = "true" ] && [ "$SHIPPABLE" != "true" ]; -then - say "$e==>$x Travis CI detected." - # https://docs.travis-ci.com/user/environment-variables/ - service="travis" - commit="${TRAVIS_PULL_REQUEST_SHA:-$TRAVIS_COMMIT}" - build="$TRAVIS_JOB_NUMBER" - pr="$TRAVIS_PULL_REQUEST" - job="$TRAVIS_JOB_ID" - slug="$TRAVIS_REPO_SLUG" - env="$env,TRAVIS_OS_NAME" - tag="$TRAVIS_TAG" - if [ "$TRAVIS_BRANCH" != "$TRAVIS_TAG" ]; - then - branch="${TRAVIS_PULL_REQUEST_BRANCH:-$TRAVIS_BRANCH}" - fi - - language=$(compgen -A variable | grep "^TRAVIS_.*_VERSION$" | head -1) - if [ "$language" != "" ]; - then - env="$env,${!language}" - fi - -elif [ "$CODEBUILD_CI" = "true" ]; -then - say "$e==>$x AWS Codebuild detected." - # https://docs.aws.amazon.com/codebuild/latest/userguide/build-env-ref-env-vars.html - service="codebuild" - commit="$CODEBUILD_RESOLVED_SOURCE_VERSION" - build="$CODEBUILD_BUILD_ID" - branch="$(echo "$CODEBUILD_WEBHOOK_HEAD_REF" | sed 's/^refs\/heads\///')" - if [ "${CODEBUILD_SOURCE_VERSION/pr}" = "$CODEBUILD_SOURCE_VERSION" ] ; then - pr="false" - else - pr="$(echo "$CODEBUILD_SOURCE_VERSION" | sed 's/^pr\///')" - fi - job="$CODEBUILD_BUILD_ID" - slug="$(echo "$CODEBUILD_SOURCE_REPO_URL" | sed 's/^.*:\/\/[^\/]*\///' | sed 's/\.git$//')" - -elif [ "$CI" = "true" ] && [ "$CI_NAME" = "codeship" ]; -then - say "$e==>$x Codeship CI detected." - # https://www.codeship.io/documentation/continuous-integration/set-environment-variables/ - service="codeship" - branch="$CI_BRANCH" - build="$CI_BUILD_NUMBER" - build_url=$(urlencode "$CI_BUILD_URL") - commit="$CI_COMMIT_ID" - -elif [ -n "$CF_BUILD_URL" ] && [ -n "$CF_BUILD_ID" ]; -then - say "$e==>$x Codefresh CI detected." - # https://docs.codefresh.io/v1.0/docs/variables - service="codefresh" - branch="$CF_BRANCH" - build="$CF_BUILD_ID" - build_url=$(urlencode "$CF_BUILD_URL") - commit="$CF_REVISION" - -elif [ "$TEAMCITY_VERSION" != "" ]; -then - say "$e==>$x TeamCity CI detected." - # https://confluence.jetbrains.com/display/TCD8/Predefined+Build+Parameters - # https://confluence.jetbrains.com/plugins/servlet/mobile#content/view/74847298 - if [ "$TEAMCITY_BUILD_BRANCH" = '' ]; - then - echo " Teamcity does not automatically make build parameters available as environment variables." - echo " Add the following environment parameters to the build configuration" - echo " env.TEAMCITY_BUILD_BRANCH = %teamcity.build.branch%" - echo " env.TEAMCITY_BUILD_ID = %teamcity.build.id%" - echo " env.TEAMCITY_BUILD_URL = %teamcity.serverUrl%/viewLog.html?buildId=%teamcity.build.id%" - echo " env.TEAMCITY_BUILD_COMMIT = %system.build.vcs.number%" - echo " env.TEAMCITY_BUILD_REPOSITORY = %vcsroot..url%" - fi - service="teamcity" - branch="$TEAMCITY_BUILD_BRANCH" - build="$TEAMCITY_BUILD_ID" - build_url=$(urlencode "$TEAMCITY_BUILD_URL") - if [ "$TEAMCITY_BUILD_COMMIT" != "" ]; - then - commit="$TEAMCITY_BUILD_COMMIT" - else - commit="$BUILD_VCS_NUMBER" - fi - remote_addr="$TEAMCITY_BUILD_REPOSITORY" - -elif [ "$CI" = "true" ] && [ "$CIRCLECI" = "true" ]; -then - say "$e==>$x Circle CI detected." - # https://circleci.com/docs/environment-variables - service="circleci" - branch="$CIRCLE_BRANCH" - build="$CIRCLE_BUILD_NUM" - job="$CIRCLE_NODE_INDEX" - if [ "$CIRCLE_PROJECT_REPONAME" != "" ]; - then - slug="$CIRCLE_PROJECT_USERNAME/$CIRCLE_PROJECT_REPONAME" - else - # git@github.com:owner/repo.git - slug="${CIRCLE_REPOSITORY_URL##*:}" - # owner/repo.git - slug="${slug%%.git}" - fi - pr="${CIRCLE_PULL_REQUEST##*/}" - commit="$CIRCLE_SHA1" - search_in="$search_in $CIRCLE_ARTIFACTS $CIRCLE_TEST_REPORTS" - -elif [ "$BUDDYBUILD_BRANCH" != "" ]; -then - say "$e==>$x buddybuild detected" - # http://docs.buddybuild.com/v6/docs/custom-prebuild-and-postbuild-steps - service="buddybuild" - branch="$BUDDYBUILD_BRANCH" - build="$BUDDYBUILD_BUILD_NUMBER" - build_url="https://dashboard.buddybuild.com/public/apps/$BUDDYBUILD_APP_ID/build/$BUDDYBUILD_BUILD_ID" - # BUDDYBUILD_TRIGGERED_BY - if [ "$ddp" = "$HOME/Library/Developer/Xcode/DerivedData" ]; - then - ddp="/private/tmp/sandbox/${BUDDYBUILD_APP_ID}/bbtest" - fi - -elif [ "${bamboo_planRepository_revision}" != "" ]; -then - say "$e==>$x Bamboo detected" - # https://confluence.atlassian.com/bamboo/bamboo-variables-289277087.html#Bamboovariables-Build-specificvariables - service="bamboo" - commit="${bamboo_planRepository_revision}" - # shellcheck disable=SC2154 - branch="${bamboo_planRepository_branch}" - # shellcheck disable=SC2154 - build="${bamboo_buildNumber}" - # shellcheck disable=SC2154 - build_url="${bamboo_buildResultsUrl}" - # shellcheck disable=SC2154 - remote_addr="${bamboo_planRepository_repositoryUrl}" - -elif [ "$CI" = "true" ] && [ "$BITRISE_IO" = "true" ]; -then - # http://devcenter.bitrise.io/faq/available-environment-variables/ - say "$e==>$x Bitrise CI detected." - service="bitrise" - branch="$BITRISE_GIT_BRANCH" - build="$BITRISE_BUILD_NUMBER" - build_url=$(urlencode "$BITRISE_BUILD_URL") - pr="$BITRISE_PULL_REQUEST" - if [ "$GIT_CLONE_COMMIT_HASH" != "" ]; - then - commit="$GIT_CLONE_COMMIT_HASH" - fi - -elif [ "$CI" = "true" ] && [ "$SEMAPHORE" = "true" ]; -then - say "$e==>$x Semaphore CI detected." -# https://docs.semaphoreci.com/ci-cd-environment/environment-variables/#semaphore-related - service="semaphore" - branch="$SEMAPHORE_GIT_BRANCH" - build="$SEMAPHORE_WORKFLOW_NUMBER" - job="$SEMAPHORE_JOB_ID" - pr="$PULL_REQUEST_NUMBER" - slug="$SEMAPHORE_REPO_SLUG" - commit="$REVISION" - env="$env,SEMAPHORE_TRIGGER_SOURCE" - -elif [ "$CI" = "true" ] && [ "$BUILDKITE" = "true" ]; -then - say "$e==>$x Buildkite CI detected." - # https://buildkite.com/docs/guides/environment-variables - service="buildkite" - branch="$BUILDKITE_BRANCH" - build="$BUILDKITE_BUILD_NUMBER" - job="$BUILDKITE_JOB_ID" - build_url=$(urlencode "$BUILDKITE_BUILD_URL") - slug="$BUILDKITE_PROJECT_SLUG" - commit="$BUILDKITE_COMMIT" - if [[ "$BUILDKITE_PULL_REQUEST" != "false" ]]; then - pr="$BUILDKITE_PULL_REQUEST" - fi - tag="$BUILDKITE_TAG" - -elif [ "$CI" = "drone" ] || [ "$DRONE" = "true" ]; -then - say "$e==>$x Drone CI detected." - # http://docs.drone.io/env.html - # drone commits are not full shas - service="drone.io" - branch="$DRONE_BRANCH" - build="$DRONE_BUILD_NUMBER" - build_url=$(urlencode "${DRONE_BUILD_LINK}") - pr="$DRONE_PULL_REQUEST" - job="$DRONE_JOB_NUMBER" - tag="$DRONE_TAG" - -elif [ "$CI" = "true" ] && [ "$HEROKU_TEST_RUN_BRANCH" != "" ]; -then - say "$e==>$x Heroku CI detected." - # https://devcenter.heroku.com/articles/heroku-ci#environment-variables - service="heroku" - branch="$HEROKU_TEST_RUN_BRANCH" - build="$HEROKU_TEST_RUN_ID" - commit="$HEROKU_TEST_RUN_COMMIT_VERSION" - -elif [[ "$CI" = "true" || "$CI" = "True" ]] && [[ "$APPVEYOR" = "true" || "$APPVEYOR" = "True" ]]; -then - say "$e==>$x Appveyor CI detected." - # http://www.appveyor.com/docs/environment-variables - service="appveyor" - branch="$APPVEYOR_REPO_BRANCH" - build=$(urlencode "$APPVEYOR_JOB_ID") - pr="$APPVEYOR_PULL_REQUEST_NUMBER" - job="$APPVEYOR_ACCOUNT_NAME%2F$APPVEYOR_PROJECT_SLUG%2F$APPVEYOR_BUILD_VERSION" - slug="$APPVEYOR_REPO_NAME" - commit="$APPVEYOR_REPO_COMMIT" - build_url=$(urlencode "${APPVEYOR_URL}/project/${APPVEYOR_REPO_NAME}/builds/$APPVEYOR_BUILD_ID/job/${APPVEYOR_JOB_ID}") - -elif [ "$CI" = "true" ] && [ "$WERCKER_GIT_BRANCH" != "" ]; -then - say "$e==>$x Wercker CI detected." - # http://devcenter.wercker.com/articles/steps/variables.html - service="wercker" - branch="$WERCKER_GIT_BRANCH" - build="$WERCKER_MAIN_PIPELINE_STARTED" - slug="$WERCKER_GIT_OWNER/$WERCKER_GIT_REPOSITORY" - commit="$WERCKER_GIT_COMMIT" - -elif [ "$CI" = "true" ] && [ "$MAGNUM" = "true" ]; -then - say "$e==>$x Magnum CI detected." - # https://magnum-ci.com/docs/environment - service="magnum" - branch="$CI_BRANCH" - build="$CI_BUILD_NUMBER" - commit="$CI_COMMIT" - -elif [ "$SHIPPABLE" = "true" ]; -then - say "$e==>$x Shippable CI detected." - # http://docs.shippable.com/ci_configure/ - service="shippable" - # shellcheck disable=SC2153 - branch=$([ "$HEAD_BRANCH" != "" ] && echo "$HEAD_BRANCH" || echo "$BRANCH") - build="$BUILD_NUMBER" - build_url=$(urlencode "$BUILD_URL") - pr="$PULL_REQUEST" - slug="$REPO_FULL_NAME" - # shellcheck disable=SC2153 - commit="$COMMIT" - -elif [ "$TDDIUM" = "true" ]; -then - say "Solano CI detected." - # http://docs.solanolabs.com/Setup/tddium-set-environment-variables/ - service="solano" - commit="$TDDIUM_CURRENT_COMMIT" - branch="$TDDIUM_CURRENT_BRANCH" - build="$TDDIUM_TID" - pr="$TDDIUM_PR_ID" - -elif [ "$GREENHOUSE" = "true" ]; -then - say "$e==>$x Greenhouse CI detected." - # http://docs.greenhouseci.com/docs/environment-variables-files - service="greenhouse" - branch="$GREENHOUSE_BRANCH" - build="$GREENHOUSE_BUILD_NUMBER" - build_url=$(urlencode "$GREENHOUSE_BUILD_URL") - pr="$GREENHOUSE_PULL_REQUEST" - commit="$GREENHOUSE_COMMIT" - search_in="$search_in $GREENHOUSE_EXPORT_DIR" - -elif [ "$GITLAB_CI" != "" ]; -then - say "$e==>$x GitLab CI detected." - # http://doc.gitlab.com/ce/ci/variables/README.html - service="gitlab" - branch="${CI_BUILD_REF_NAME:-$CI_COMMIT_REF_NAME}" - build="${CI_BUILD_ID:-$CI_JOB_ID}" - remote_addr="${CI_BUILD_REPO:-$CI_REPOSITORY_URL}" - commit="${CI_BUILD_REF:-$CI_COMMIT_SHA}" - slug="${CI_PROJECT_PATH}" - -elif [ "$GITHUB_ACTIONS" != "" ]; -then - say "$e==>$x GitHub Actions detected." - say " Env vars used:" - say " -> GITHUB_ACTIONS: ${GITHUB_ACTIONS}" - say " -> GITHUB_HEAD_REF: ${GITHUB_HEAD_REF}" - say " -> GITHUB_REF: ${GITHUB_REF}" - say " -> GITHUB_REPOSITORY: ${GITHUB_REPOSITORY}" - say " -> GITHUB_RUN_ID: ${GITHUB_RUN_ID}" - say " -> GITHUB_SHA: ${GITHUB_SHA}" - say " -> GITHUB_WORKFLOW: ${GITHUB_WORKFLOW}" - - # https://github.com/features/actions - service="github-actions" - - # https://help.github.com/en/articles/virtual-environments-for-github-actions#environment-variables - branch="${GITHUB_REF#refs/heads/}" - if [ "$GITHUB_HEAD_REF" != "" ]; - then - # PR refs are in the format: refs/pull/7/merge - pr="${GITHUB_REF#refs/pull/}" - pr="${pr%/merge}" - branch="${GITHUB_HEAD_REF}" - fi - commit="${GITHUB_SHA}" - slug="${GITHUB_REPOSITORY}" - build="${GITHUB_RUN_ID}" - build_url=$(urlencode "http://github.com/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}") - job="$(urlencode "${GITHUB_WORKFLOW}")" - - # actions/checkout runs in detached HEAD - mc= - if [ -n "$pr" ] && [ "$pr" != false ] && [ "$commit_o" == "" ]; - then - mc=$(git show --no-patch --format="%P" 2>/dev/null || echo "") - - if [[ "$mc" =~ ^[a-z0-9]{40}[[:space:]][a-z0-9]{40}$ ]]; - then - mc=$(echo "$mc" | cut -d' ' -f2) - say " Fixing merge commit SHA $commit -> $mc" - commit=$mc - elif [[ "$mc" = "" ]]; - then - say "$r-> Issue detecting commit SHA. Please run actions/checkout with fetch-depth > 1 or set to 0$x" - fi - fi - -elif [ "$SYSTEM_TEAMFOUNDATIONSERVERURI" != "" ]; -then - say "$e==>$x Azure Pipelines detected." - # https://docs.microsoft.com/en-us/azure/devops/pipelines/build/variables?view=vsts - # https://docs.microsoft.com/en-us/azure/devops/pipelines/build/variables?view=azure-devops&viewFallbackFrom=vsts&tabs=yaml - service="azure_pipelines" - commit="$BUILD_SOURCEVERSION" - build="$BUILD_BUILDNUMBER" - if [ -z "$SYSTEM_PULLREQUEST_PULLREQUESTNUMBER" ]; - then - pr="$SYSTEM_PULLREQUEST_PULLREQUESTID" - else - pr="$SYSTEM_PULLREQUEST_PULLREQUESTNUMBER" - fi - project="${SYSTEM_TEAMPROJECT}" - server_uri="${SYSTEM_TEAMFOUNDATIONSERVERURI}" - job="${BUILD_BUILDID}" - branch="${BUILD_SOURCEBRANCH#"refs/heads/"}" - build_url=$(urlencode "${SYSTEM_TEAMFOUNDATIONSERVERURI}${SYSTEM_TEAMPROJECT}/_build/results?buildId=${BUILD_BUILDID}") - - # azure/pipelines runs in detached HEAD - mc= - if [ -n "$pr" ] && [ "$pr" != false ]; - then - mc=$(git show --no-patch --format="%P" 2>/dev/null || echo "") - - if [[ "$mc" =~ ^[a-z0-9]{40}[[:space:]][a-z0-9]{40}$ ]]; - then - mc=$(echo "$mc" | cut -d' ' -f2) - say " Fixing merge commit SHA $commit -> $mc" - commit=$mc - fi - fi - -elif [ "$CI" = "true" ] && [ "$BITBUCKET_BUILD_NUMBER" != "" ]; -then - say "$e==>$x Bitbucket detected." - # https://confluence.atlassian.com/bitbucket/variables-in-pipelines-794502608.html - service="bitbucket" - branch="$BITBUCKET_BRANCH" - build="$BITBUCKET_BUILD_NUMBER" - slug="$BITBUCKET_REPO_OWNER/$BITBUCKET_REPO_SLUG" - job="$BITBUCKET_BUILD_NUMBER" - pr="$BITBUCKET_PR_ID" - commit="$BITBUCKET_COMMIT" - # See https://jira.atlassian.com/browse/BCLOUD-19393 - if [ "${#commit}" = 12 ]; - then - commit=$(git rev-parse "$BITBUCKET_COMMIT") - fi - -elif [ "$CI" = "true" ] && [ "$BUDDY" = "true" ]; -then - say "$e==>$x Buddy CI detected." - # https://buddy.works/docs/pipelines/environment-variables - service="buddy" - branch="$BUDDY_EXECUTION_BRANCH" - build="$BUDDY_EXECUTION_ID" - build_url=$(urlencode "$BUDDY_EXECUTION_URL") - commit="$BUDDY_EXECUTION_REVISION" - pr="$BUDDY_EXECUTION_PULL_REQUEST_NO" - tag="$BUDDY_EXECUTION_TAG" - slug="$BUDDY_REPO_SLUG" - -elif [ "$CIRRUS_CI" != "" ]; -then - say "$e==>$x Cirrus CI detected." - # https://cirrus-ci.org/guide/writing-tasks/#environment-variables - service="cirrus-ci" - slug="$CIRRUS_REPO_FULL_NAME" - branch="$CIRRUS_BRANCH" - pr="$CIRRUS_PR" - commit="$CIRRUS_CHANGE_IN_REPO" - build="$CIRRUS_BUILD_ID" - build_url=$(urlencode "https://cirrus-ci.com/task/$CIRRUS_TASK_ID") - job="$CIRRUS_TASK_NAME" - -elif [ "$DOCKER_REPO" != "" ]; -then - say "$e==>$x Docker detected." - # https://docs.docker.com/docker-cloud/builds/advanced/ - service="docker" - branch="$SOURCE_BRANCH" - commit="$SOURCE_COMMIT" - slug="$DOCKER_REPO" - tag="$CACHE_TAG" - env="$env,IMAGE_NAME" - -else - say "${r}x>${x} No CI provider detected." - say " Testing inside Docker? ${b}http://docs.codecov.io/docs/testing-with-docker${x}" - say " Testing with Tox? ${b}https://docs.codecov.io/docs/python#section-testing-with-tox${x}" - -fi - -say " ${e}project root:${x} $git_root" - -# find branch, commit, repo from git command -if [ "$GIT_BRANCH" != "" ]; -then - branch="$GIT_BRANCH" - -elif [ "$branch" = "" ]; -then - branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || hg branch 2>/dev/null || echo "") - if [ "$branch" = "HEAD" ]; - then - branch="" - fi -fi - -if [ "$commit_o" = "" ]; -then - if [ "$GIT_COMMIT" != "" ]; - then - commit="$GIT_COMMIT" - elif [ "$commit" = "" ]; - then - commit=$(git log -1 --format="%H" 2>/dev/null || hg id -i --debug 2>/dev/null | tr -d '+' || echo "") - fi -else - commit="$commit_o" -fi - -if [ "$CODECOV_TOKEN" != "" ] && [ "$token" = "" ]; -then - say "${e}-->${x} token set from env" - token="$CODECOV_TOKEN" -fi - -if [ "$CODECOV_URL" != "" ] && [ "$url_o" = "" ]; -then - say "${e}-->${x} url set from env" - url_o=$(echo "$CODECOV_URL" | sed -e 's/\/$//') -fi - -if [ "$CODECOV_SLUG" != "" ]; -then - say "${e}-->${x} slug set from env" - slug_o="$CODECOV_SLUG" - -elif [ "$slug" = "" ]; -then - if [ "$remote_addr" = "" ]; - then - remote_addr=$(git config --get remote.origin.url || hg paths default || echo '') - fi - if [ "$remote_addr" != "" ]; - then - if echo "$remote_addr" | grep -q "//"; then - # https - slug=$(echo "$remote_addr" | cut -d / -f 4,5 | sed -e 's/\.git$//') - else - # ssh - slug=$(echo "$remote_addr" | cut -d : -f 2 | sed -e 's/\.git$//') - fi - fi - if [ "$slug" = "/" ]; - then - slug="" - fi -fi - -yaml=$(cd "$git_root" && \ - git ls-files "*codecov.yml" "*codecov.yaml" 2>/dev/null \ - || hg locate "*codecov.yml" "*codecov.yaml" 2>/dev/null \ - || cd "$proj_root" && find . -maxdepth 1 -type f -name '*codecov.y*ml' 2>/dev/null \ - || echo '') -yaml=$(echo "$yaml" | head -1) - -if [ "$yaml" != "" ]; -then - say " ${e}Yaml found at:${x} $yaml" - if [[ "$yaml" != /* ]]; then - # relative path for yaml file given, assume relative to the repo root - yaml="$git_root/$yaml" - fi - config=$(parse_yaml "$yaml" || echo '') - - # TODO validate the yaml here - - if [ "$(echo "$config" | grep 'codecov_token="')" != "" ] && [ "$token" = "" ]; - then - say "${e}-->${x} token set from yaml" - token="$(echo "$config" | grep 'codecov_token="' | sed -e 's/codecov_token="//' | sed -e 's/"\.*//')" - fi - - if [ "$(echo "$config" | grep 'codecov_url="')" != "" ] && [ "$url_o" = "" ]; - then - say "${e}-->${x} url set from yaml" - url_o="$(echo "$config" | grep 'codecov_url="' | sed -e 's/codecov_url="//' | sed -e 's/"\.*//')" - fi - - if [ "$(echo "$config" | grep 'codecov_slug="')" != "" ] && [ "$slug_o" = "" ]; - then - say "${e}-->${x} slug set from yaml" - slug_o="$(echo "$config" | grep 'codecov_slug="' | sed -e 's/codecov_slug="//' | sed -e 's/"\.*//')" - fi -else - say " ${g}Yaml not found, that's ok! Learn more at${x} ${b}http://docs.codecov.io/docs/codecov-yaml${x}" -fi - -if [ "$branch_o" != "" ]; -then - branch=$(urlencode "$branch_o") -else - branch=$(urlencode "$branch") -fi - -if [ "$slug_o" = "" ]; -then - urlencoded_slug=$(urlencode "$slug") -else - urlencoded_slug=$(urlencode "$slug_o") -fi - -query="branch=$branch\ - &commit=$commit\ - &build=$([ "$build_o" = "" ] && echo "$build" || echo "$build_o")\ - &build_url=$build_url\ - &name=$(urlencode "$name")\ - &tag=$([ "$tag_o" = "" ] && echo "$tag" || echo "$tag_o")\ - &slug=$urlencoded_slug\ - &service=$service\ - &flags=$flags\ - &pr=$([ "$pr_o" = "" ] && echo "${pr##\#}" || echo "${pr_o##\#}")\ - &job=$job\ - &cmd_args=$(IFS=,; echo "${codecov_flags[*]}")" - -if [ -n "$project" ] && [ -n "$server_uri" ]; -then - query=$(echo "$query&project=$project&server_uri=$server_uri" | tr -d ' ') -fi - -if [ "$parent" != "" ]; -then - query=$(echo "parent=$parent&$query" | tr -d ' ') -fi - -if [ "$ft_search" = "1" ]; -then - # detect bower components location - bower_components="bower_components" - bower_rc=$(cd "$git_root" && cat .bowerrc 2>/dev/null || echo "") - if [ "$bower_rc" != "" ]; - then - bower_components=$(echo "$bower_rc" | tr -d '\n' | grep '"directory"' | cut -d'"' -f4 | sed -e 's/\/$//') - if [ "$bower_components" = "" ]; - then - bower_components="bower_components" - fi - fi - - # Swift Coverage - if [ "$ft_xcodellvm" = "1" ] && [ -d "$ddp" ]; - then - say "${e}==>${x} Processing Xcode reports via llvm-cov" - say " DerivedData folder: $ddp" - profdata_files=$(find "$ddp" -name '*.profdata' 2>/dev/null || echo '') - if [ "$profdata_files" != "" ]; - then - # xcode via profdata - if [ "$xp" = "" ]; - then - # xp=$(xcodebuild -showBuildSettings 2>/dev/null | grep -i "^\s*PRODUCT_NAME" | sed -e 's/.*= \(.*\)/\1/') - # say " ${e}->${x} Speed up Xcode processing by adding ${e}-J '$xp'${x}" - say " ${g}hint${x} Speed up Swift processing by using use ${g}-J 'AppName'${x} (regexp accepted)" - say " ${g}hint${x} This will remove Pods/ from your report. Also ${b}https://docs.codecov.io/docs/ignoring-paths${x}" - fi - while read -r profdata; - do - if [ "$profdata" != "" ]; - then - swiftcov "$profdata" "$xp" - fi - done <<< "$profdata_files" - else - say " ${e}->${x} No Swift coverage found" - fi - - # Obj-C Gcov Coverage - if [ "$ft_gcov" = "1" ]; - then - say " ${e}->${x} Running $gcov_exe for Obj-C" - if [ "$ft_gcovout" = "0" ]; - then - # suppress gcov output - bash -c "find $ddp -type f -name '*.gcda' $gcov_include $gcov_ignore -exec $gcov_exe -p $gcov_arg {} +" >/dev/null 2>&1 || true - else - bash -c "find $ddp -type f -name '*.gcda' $gcov_include $gcov_ignore -exec $gcov_exe -p $gcov_arg {} +" || true - fi - fi - fi - - if [ "$ft_xcodeplist" = "1" ] && [ -d "$ddp" ]; - then - say "${e}==>${x} Processing Xcode plists" - plists_files=$(find "$ddp" -name '*.xccoverage' 2>/dev/null || echo '') - if [ "$plists_files" != "" ]; - then - while read -r plist; - do - if [ "$plist" != "" ]; - then - say " ${g}Found${x} plist file at $plist" - plutil -convert xml1 -o "$(basename "$plist").plist" -- "$plist" - fi - done <<< "$plists_files" - fi - fi - - # Gcov Coverage - if [ "$ft_gcov" = "1" ]; - then - say "${e}==>${x} Running $gcov_exe in $proj_root ${e}(disable via -X gcov)${x}" - if [ "$ft_gcovout" = "0" ]; - then - # suppress gcov output - bash -c "find $proj_root -type f -name '*.gcno' $gcov_include $gcov_ignore -exec $gcov_exe -pb $gcov_arg {} +" >/dev/null 2>&1 || true - else - bash -c "find $proj_root -type f -name '*.gcno' $gcov_include $gcov_ignore -exec $gcov_exe -pb $gcov_arg {} +" || true - fi - else - say "${e}==>${x} gcov disabled" - fi - - # Python Coverage - if [ "$ft_coveragepy" = "1" ]; - then - if [ ! -f coverage.xml ]; - then - if command -v coverage >/dev/null 2>&1; - then - say "${e}==>${x} Python coveragepy exists ${e}disable via -X coveragepy${x}" - - dotcoverage=$(find "$git_root" -name '.coverage' -or -name '.coverage.*' | head -1 || echo '') - if [ "$dotcoverage" != "" ]; - then - cd "$(dirname "$dotcoverage")" - if [ ! -f .coverage ]; - then - say " ${e}->${x} Running coverage combine" - coverage combine -a - fi - say " ${e}->${x} Running coverage xml" - if [ "$(coverage xml -i)" != "No data to report." ]; - then - files="$files -$PWD/coverage.xml" - else - say " ${r}No data to report.${x}" - fi - cd "$proj_root" - else - say " ${r}No .coverage file found.${x}" - fi - else - say "${e}==>${x} Python coveragepy not found" - fi - fi - else - say "${e}==>${x} Python coveragepy disabled" - fi - - if [ "$search_in_o" != "" ]; - then - # location override - search_in="$search_in_o" - fi - - say "$e==>$x Searching for coverage reports in:" - for _path in $search_in - do - say " ${g}+${x} $_path" - done - - patterns="find $search_in \( \ - -name vendor \ - -or -name '$bower_components' \ - -or -name '.egg-info*' \ - -or -name 'conftest_*.c.gcov' \ - -or -name .env \ - -or -name .envs \ - -or -name .git \ - -or -name .hg \ - -or -name .tox \ - -or -name .venv \ - -or -name .venvs \ - -or -name .virtualenv \ - -or -name .virtualenvs \ - -or -name .yarn-cache \ - -or -name __pycache__ \ - -or -name env \ - -or -name envs \ - -or -name htmlcov \ - -or -name js/generated/coverage \ - -or -name node_modules \ - -or -name venv \ - -or -name venvs \ - -or -name virtualenv \ - -or -name virtualenvs \ - \) -prune -or \ - -type f \( -name '*coverage*.*' \ - -or -name '*.clover' \ - -or -name '*.codecov.*' \ - -or -name '*.gcov' \ - -or -name '*.lcov' \ - -or -name '*.lst' \ - -or -name 'clover.xml' \ - -or -name 'cobertura.xml' \ - -or -name 'codecov.*' \ - -or -name 'cover.out' \ - -or -name 'codecov-result.json' \ - -or -name 'coverage-final.json' \ - -or -name 'excoveralls.json' \ - -or -name 'gcov.info' \ - -or -name 'jacoco*.xml' \ - -or -name '*Jacoco*.xml' \ - -or -name 'lcov.dat' \ - -or -name 'lcov.info' \ - -or -name 'luacov.report.out' \ - -or -name 'naxsi.info' \ - -or -name 'nosetests.xml' \ - -or -name 'report.xml' \ - $include_cov \) \ - $exclude_cov \ - -not -name '*.am' \ - -not -name '*.bash' \ - -not -name '*.bat' \ - -not -name '*.bw' \ - -not -name '*.cfg' \ - -not -name '*.class' \ - -not -name '*.cmake' \ - -not -name '*.cmake' \ - -not -name '*.conf' \ - -not -name '*.coverage' \ - -not -name '*.cp' \ - -not -name '*.cpp' \ - -not -name '*.crt' \ - -not -name '*.css' \ - -not -name '*.csv' \ - -not -name '*.csv' \ - -not -name '*.data' \ - -not -name '*.db' \ - -not -name '*.dox' \ - -not -name '*.ec' \ - -not -name '*.ec' \ - -not -name '*.egg' \ - -not -name '*.el' \ - -not -name '*.env' \ - -not -name '*.erb' \ - -not -name '*.exe' \ - -not -name '*.ftl' \ - -not -name '*.gif' \ - -not -name '*.gradle' \ - -not -name '*.gz' \ - -not -name '*.h' \ - -not -name '*.html' \ - -not -name '*.in' \ - -not -name '*.jade' \ - -not -name '*.jar*' \ - -not -name '*.jpeg' \ - -not -name '*.jpg' \ - -not -name '*.js' \ - -not -name '*.less' \ - -not -name '*.log' \ - -not -name '*.m4' \ - -not -name '*.mak*' \ - -not -name '*.md' \ - -not -name '*.o' \ - -not -name '*.p12' \ - -not -name '*.pem' \ - -not -name '*.png' \ - -not -name '*.pom*' \ - -not -name '*.profdata' \ - -not -name '*.proto' \ - -not -name '*.ps1' \ - -not -name '*.pth' \ - -not -name '*.py' \ - -not -name '*.pyc' \ - -not -name '*.pyo' \ - -not -name '*.rb' \ - -not -name '*.rsp' \ - -not -name '*.rst' \ - -not -name '*.ru' \ - -not -name '*.sbt' \ - -not -name '*.scss' \ - -not -name '*.scss' \ - -not -name '*.serialized' \ - -not -name '*.sh' \ - -not -name '*.snapshot' \ - -not -name '*.sql' \ - -not -name '*.svg' \ - -not -name '*.tar.tz' \ - -not -name '*.template' \ - -not -name '*.whl' \ - -not -name '*.xcconfig' \ - -not -name '*.xcoverage.*' \ - -not -name '*/classycle/report.xml' \ - -not -name '*codecov.yml' \ - -not -name '*~' \ - -not -name '.*coveragerc' \ - -not -name '.coverage*' \ - -not -name 'coverage-summary.json' \ - -not -name 'createdFiles.lst' \ - -not -name 'fullLocaleNames.lst' \ - -not -name 'include.lst' \ - -not -name 'inputFiles.lst' \ - -not -name 'phpunit-code-coverage.xml' \ - -not -name 'phpunit-coverage.xml' \ - -not -name 'remapInstanbul.coverage*.json' \ - -not -name 'scoverage.measurements.*' \ - -not -name 'test_*_coverage.txt' \ - -not -name 'testrunner-coverage*' \ - -print 2>/dev/null" - files=$(eval "$patterns" || echo '') - -elif [ "$include_cov" != "" ]; -then - files=$(eval "find $search_in -type f \( ${include_cov:5} \)$exclude_cov 2>/dev/null" || echo '') -elif [ "$direct_file_upload" != "" ]; -then - files=$direct_file_upload -fi - -num_of_files=$(echo "$files" | wc -l | tr -d ' ') -if [ "$num_of_files" != '' ] && [ "$files" != '' ]; -then - say " ${e}->${x} Found $num_of_files reports" -fi - -# no files found -if [ "$files" = "" ]; -then - say "${r}-->${x} No coverage report found." - say " Please visit ${b}http://docs.codecov.io/docs/supported-languages${x}" - exit ${exit_with}; -fi - -if [ "$ft_network" == "1" ]; -then - say "${e}==>${x} Detecting git/mercurial file structure" - network=$(cd "$git_root" && git ls-files $git_ls_files_recurse_submodules_o 2>/dev/null || hg locate 2>/dev/null || echo "") - if [ "$network" = "" ]; - then - network=$(find "$git_root" \( \ - -name virtualenv \ - -name .virtualenv \ - -name virtualenvs \ - -name .virtualenvs \ - -name '*.png' \ - -name '*.gif' \ - -name '*.jpg' \ - -name '*.jpeg' \ - -name '*.md' \ - -name .env \ - -name .envs \ - -name env \ - -name envs \ - -name .venv \ - -name .venvs \ - -name venv \ - -name venvs \ - -name .git \ - -name .egg-info \ - -name shunit2-2.1.6 \ - -name vendor \ - -name __pycache__ \ - -name node_modules \ - -path "*/$bower_components/*" \ - -path '*/target/delombok/*' \ - -path '*/build/lib/*' \ - -path '*/js/generated/coverage/*' \ - \) -prune -or \ - -type f -print 2>/dev/null || echo '') - fi - - if [ "$network_filter_o" != "" ]; - then - network=$(echo "$network" | grep -e "$network_filter_o/*") - fi - if [ "$prefix_o" != "" ]; - then - network=$(echo "$network" | awk "{print \"$prefix_o/\"\$0}") - fi -fi - -upload_file=$(mktemp /tmp/codecov.XXXXXX) -adjustments_file=$(mktemp /tmp/codecov.adjustments.XXXXXX) - -cleanup() { - rm -f "$upload_file" "$adjustments_file" "$upload_file.gz" -} - -trap cleanup INT ABRT TERM - - -if [ "$env" != "" ]; -then - inc_env="" - say "${e}==>${x} Appending build variables" - for varname in $(echo "$env" | tr ',' ' ') - do - if [ "$varname" != "" ]; - then - say " ${g}+${x} $varname" - inc_env="${inc_env}${varname}=$(eval echo "\$${varname}") -" - fi - done - echo "$inc_env<<<<<< ENV" >> "$upload_file" -fi - -# Append git file list -# write discovered yaml location -if [ "$direct_file_upload" = "" ]; -then - echo "$yaml" >> "$upload_file" -fi - -if [ "$ft_network" == "1" ]; -then - i="woff|eot|otf" # fonts - i="$i|gif|png|jpg|jpeg|psd" # images - i="$i|ptt|pptx|numbers|pages|md|txt|xlsx|docx|doc|pdf|csv" # docs - i="$i|.gitignore" # supporting docs - - if [ "$ft_html" != "1" ]; - then - i="$i|html" - fi - - if [ "$ft_yaml" != "1" ]; - then - i="$i|yml|yaml" - fi - - echo "$network" | grep -vwE "($i)$" >> "$upload_file" -fi -echo "<<<<<< network" >> "$upload_file" - -if [ "$direct_file_upload" = "" ]; -then - fr=0 - say "${e}==>${x} Reading reports" - while IFS='' read -r file; - do - # read the coverage file - if [ "$(echo "$file" | tr -d ' ')" != '' ]; - then - if [ -f "$file" ]; - then - report_len=$(wc -c < "$file") - if [ "$report_len" -ne 0 ]; - then - say " ${g}+${x} $file ${e}bytes=$(echo "$report_len" | tr -d ' ')${x}" - # append to to upload - _filename=$(basename "$file") - if [ "${_filename##*.}" = 'gcov' ]; - then - { - echo "# path=$(echo "$file.reduced" | sed "s|^$git_root/||")"; - # get file name - head -1 "$file"; - } >> "$upload_file" - # 1. remove source code - # 2. remove ending bracket lines - # 3. remove whitespace - # 4. remove contextual lines - # 5. remove function names - awk -F': *' '{print $1":"$2":"}' "$file" \ - | sed '\/: *} *$/d' \ - | sed 's/^ *//' \ - | sed '/^-/d' \ - | sed 's/^function.*/func/' >> "$upload_file" - else - { - echo "# path=${file//^$git_root/||}"; - cat "$file"; - } >> "$upload_file" - fi - echo "<<<<<< EOF" >> "$upload_file" - fr=1 - if [ "$clean" = "1" ]; - then - rm "$file" - fi - else - say " ${r}-${x} Skipping empty file $file" - fi - else - say " ${r}-${x} file not found at $file" - fi - fi - done <<< "$(echo -e "$files")" - - if [ "$fr" = "0" ]; - then - say "${r}-->${x} No coverage data found." - say " Please visit ${b}http://docs.codecov.io/docs/supported-languages${x}" - say " search for your projects language to learn how to collect reports." - exit ${exit_with}; - fi -else - cp "$direct_file_upload" "$upload_file" - if [ "$clean" = "1" ]; - then - rm "$direct_file_upload" - fi -fi - -if [ "$ft_fix" = "1" ]; -then - say "${e}==>${x} Appending adjustments" - say " ${b}https://docs.codecov.io/docs/fixing-reports${x}" - - empty_line='^[[:space:]]*$' - # // - syntax_comment='^[[:space:]]*//.*' - # /* or */ - syntax_comment_block='^[[:space:]]*(\/\*|\*\/)[[:space:]]*$' - # { or } - syntax_bracket='^[[:space:]]*[\{\}][[:space:]]*(//.*)?$' - # [ or ] - syntax_list='^[[:space:]]*[][][[:space:]]*(//.*)?$' - # func ... { - syntax_go_func='^[[:space:]]*[func].*[\{][[:space:]]*$' - - # shellcheck disable=SC2089 - skip_dirs="-not -path '*/$bower_components/*' \ - -not -path '*/node_modules/*'" - - cut_and_join() { - awk 'BEGIN { FS=":" } - $3 ~ /\/\*/ || $3 ~ /\*\// { print $0 ; next } - $1!=key { if (key!="") print out ; key=$1 ; out=$1":"$2 ; next } - { out=out","$2 } - END { print out }' 2>/dev/null - } - - if echo "$network" | grep -m1 '.kt$' 1>/dev/null; - then - # skip brackets and comments - cd "$git_root" && \ - find . -type f \ - -name '*.kt' \ - -exec \ - grep -nIHE -e "$syntax_bracket" \ - -e "$syntax_comment_block" {} \; \ - | cut_and_join \ - >> "$adjustments_file" \ - || echo '' - - # last line in file - cd "$git_root" && \ - find . -type f \ - -name '*.kt' -exec \ - wc -l {} \; \ - | while read -r l; do echo "EOF: $l"; done \ - 2>/dev/null \ - >> "$adjustments_file" \ - || echo '' - fi - - if echo "$network" | grep -m1 '.go$' 1>/dev/null; - then - # skip empty lines, comments, and brackets - cd "$git_root" && \ - find . -type f \ - -not -path '*/vendor/*' \ - -not -path '*/caches/*' \ - -name '*.go' \ - -exec \ - grep -nIHE \ - -e "$empty_line" \ - -e "$syntax_comment" \ - -e "$syntax_comment_block" \ - -e "$syntax_bracket" \ - -e "$syntax_go_func" \ - {} \; \ - | cut_and_join \ - >> "$adjustments_file" \ - || echo '' - fi - - if echo "$network" | grep -m1 '.dart$' 1>/dev/null; - then - # skip brackets - cd "$git_root" && \ - find . -type f \ - -name '*.dart' \ - -exec \ - grep -nIHE \ - -e "$syntax_bracket" \ - {} \; \ - | cut_and_join \ - >> "$adjustments_file" \ - || echo '' - fi - - if echo "$network" | grep -m1 '.php$' 1>/dev/null; - then - # skip empty lines, comments, and brackets - cd "$git_root" && \ - find . -type f \ - -not -path "*/vendor/*" \ - -name '*.php' \ - -exec \ - grep -nIHE \ - -e "$syntax_list" \ - -e "$syntax_bracket" \ - -e '^[[:space:]]*\);[[:space:]]*(//.*)?$' \ - {} \; \ - | cut_and_join \ - >> "$adjustments_file" \ - || echo '' - fi - - if echo "$network" | grep -m1 '\(.c\.cpp\|.cxx\|.h\|.hpp\|.m\|.swift\|.vala\)$' 1>/dev/null; - then - # skip brackets - # shellcheck disable=SC2086,SC2090 - cd "$git_root" && \ - find . -type f \ - $skip_dirs \ - \( \ - -name '*.c' \ - -or -name '*.cpp' \ - -or -name '*.cxx' \ - -or -name '*.h' \ - -or -name '*.hpp' \ - -or -name '*.m' \ - -or -name '*.swift' \ - -or -name '*.vala' \ - \) -exec \ - grep -nIHE \ - -e "$empty_line" \ - -e "$syntax_bracket" \ - -e '// LCOV_EXCL' \ - {} \; \ - | cut_and_join \ - >> "$adjustments_file" \ - || echo '' - - # skip brackets - # shellcheck disable=SC2086,SC2090 - cd "$git_root" && \ - find . -type f \ - $skip_dirs \ - \( \ - -name '*.c' \ - -or -name '*.cpp' \ - -or -name '*.cxx' \ - -or -name '*.h' \ - -or -name '*.hpp' \ - -or -name '*.m' \ - -or -name '*.swift' \ - -or -name '*.vala' \ - \) -exec \ - grep -nIH '// LCOV_EXCL' \ - {} \; \ - >> "$adjustments_file" \ - || echo '' - - fi - - found=$(< "$adjustments_file" tr -d ' ') - - if [ "$found" != "" ]; - then - say " ${g}+${x} Found adjustments" - { - echo "# path=fixes"; - cat "$adjustments_file"; - echo "<<<<<< EOF"; - } >> "$upload_file" - rm -rf "$adjustments_file" - else - say " ${e}->${x} No adjustments found" - fi -fi - -if [ "$url_o" != "" ]; -then - url="$url_o" -fi - -if [ "$dump" != "0" ]; -then - # trim whitespace from query - say " ${e}->${x} Dumping upload file (no upload)" - echo "$url/upload/v4?$(echo "package=$package-$VERSION&token=$token&$query" | tr -d ' ')" - cat "$upload_file" -else - if [ "$save_to" != "" ]; - then - say "${e}==>${x} Copying upload file to ${save_to}" - mkdir -p "$(dirname "$save_to")" - cp "$upload_file" "$save_to" - fi - - say "${e}==>${x} Gzipping contents" - gzip -nf9 "$upload_file" - say " $(du -h "$upload_file.gz")" - - query=$(echo "${query}" | tr -d ' ') - say "${e}==>${x} Uploading reports" - say " ${e}url:${x} $url" - say " ${e}query:${x} $query" - - # Full query without token (to display on terminal output) - queryNoToken=$(echo "package=$package-$VERSION&token=secret&$query" | tr -d ' ') - # now add token to query - query=$(echo "package=$package-$VERSION&token=$token&$query" | tr -d ' ') - - if [ "$ft_s3" = "1" ]; - then - say "${e}->${x} Pinging Codecov" - say "$url/upload/v4?$queryNoToken" - # shellcheck disable=SC2086,2090 - res=$(curl $curl_s -X POST $cacert \ - --retry 5 --retry-delay 2 --connect-timeout 2 \ - -H 'X-Reduced-Redundancy: false' \ - -H 'X-Content-Type: application/x-gzip' \ - -H 'Content-Length: 0' \ - --write-out "\n%{response_code}\n" \ - $curlargs \ - "$url/upload/v4?$query" || true) - # a good reply is "https://codecov.io" + "\n" + "https://storage.googleapis.com/codecov/..." - s3target=$(echo "$res" | sed -n 2p) - status=$(tail -n1 <<< "$res") - - if [ "$status" = "200" ] && [ "$s3target" != "" ]; - then - say "${e}->${x} Uploading to" - say "${s3target}" - - # shellcheck disable=SC2086 - s3=$(curl -fiX PUT \ - --data-binary @"$upload_file.gz" \ - -H 'Content-Type: application/x-gzip' \ - -H 'Content-Encoding: gzip' \ - $curlawsargs \ - "$s3target" || true) - - if [ "$s3" != "" ]; - then - say " ${g}->${x} Reports have been successfully queued for processing at ${b}$(echo "$res" | sed -n 1p)${x}" - exit 0 - else - say " ${r}X>${x} Failed to upload" - fi - elif [ "$status" = "400" ]; - then - # 400 Error - say "${r}${res}${x}" - exit ${exit_with} - else - say "${r}${res}${x}" - fi - fi - - say "${e}==>${x} Uploading to Codecov" - - # shellcheck disable=SC2086,2090 - res=$(curl -X POST $cacert \ - --data-binary @"$upload_file.gz" \ - --retry 5 --retry-delay 2 --connect-timeout 2 \ - -H 'Content-Type: text/plain' \ - -H 'Content-Encoding: gzip' \ - -H 'X-Content-Encoding: gzip' \ - -H 'Accept: text/plain' \ - $curlargs \ - "$url/upload/v2?$query&attempt=$i" || echo 'HTTP 500') - # HTTP 200 - # http://.... - status=$(echo "$res" | head -1 | cut -d' ' -f2) - if [ "$status" = "" ] || [ "$status" = "200" ]; - then - say " Reports have been successfully queued for processing at ${b}$(echo "$res" | head -2 | tail -1)${x}" - exit 0 - else - say " ${g}${res}${x}" - exit ${exit_with} - fi - - say " ${r}X> Failed to upload coverage reports${x}" -fi - -exit ${exit_with} diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 15193a195e7a..c200f9cb36d7 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -2,16 +2,16 @@ name: "CodeQL" on: push: - branches: [ "master" ] + branches: ["master", "[0-9].[0-9]"] paths: - - 'superset/**' + - "superset/**" pull_request: # The branches below must be a subset of the branches above - branches: [ "master" ] + branches: ["master"] paths: - - 'superset/**' + - "superset/**" schedule: - - cron: '0 4 * * *' + - cron: "0 4 * * *" # cancel previous workflow jobs for PRs concurrency: @@ -30,7 +30,7 @@ jobs: strategy: fail-fast: false matrix: - language: [ 'python', 'javascript' ] + language: ["python", "javascript"] # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support steps: diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index ae54835e0fb7..d02ad7326551 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -3,9 +3,11 @@ name: Docker on: push: branches: - - 'master' + - "master" + - "[0-9].[0-9]" pull_request: - types: [synchronize, opened, reopened, ready_for_review] + branches: + - "master" concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }} diff --git a/.github/workflows/embedded-sdk-release.yml b/.github/workflows/embedded-sdk-release.yml index 39ee461f6b83..dcf7346e8f45 100644 --- a/.github/workflows/embedded-sdk-release.yml +++ b/.github/workflows/embedded-sdk-release.yml @@ -3,7 +3,8 @@ name: Embedded SDK Release on: push: branches: - - 'master' + - "master" + - "[0-9].[0-9]" jobs: config: @@ -31,7 +32,7 @@ jobs: - uses: actions/setup-node@v4 with: node-version: "16" - registry-url: 'https://registry.npmjs.org' + registry-url: "https://registry.npmjs.org" - run: npm ci - run: npm run ci:release env: diff --git a/.github/workflows/generate-FOSSA-report.yml b/.github/workflows/generate-FOSSA-report.yml index 5c7693459bb9..fb30d7dcc5be 100644 --- a/.github/workflows/generate-FOSSA-report.yml +++ b/.github/workflows/generate-FOSSA-report.yml @@ -4,6 +4,7 @@ on: push: branches: - "master" + - "[0-9].[0-9]" jobs: config: @@ -33,8 +34,8 @@ jobs: - name: Setup Java uses: actions/setup-java@v4 with: - distribution: 'temurin' - java-version: '11' + distribution: "temurin" + java-version: "11" - name: Generate fossa report env: FOSSA_API_KEY: ${{ secrets.FOSSA_API_KEY }} diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index 3e2c98970b1a..f9a3681ce0c1 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -3,7 +3,8 @@ name: pre-commit checks on: push: branches: - - 'master' + - "master" + - "[0-9].[0-9]" pull_request: types: [synchronize, opened, reopened, ready_for_review] diff --git a/.github/workflows/prefer-typescript.yml b/.github/workflows/prefer-typescript.yml index 51abea6f8798..f3179d3bccdc 100644 --- a/.github/workflows/prefer-typescript.yml +++ b/.github/workflows/prefer-typescript.yml @@ -3,7 +3,8 @@ name: Prefer TypeScript on: push: branches: - - 'master' + - "master" + - "[0-9].[0-9]" paths: - "superset-frontend/src/**" pull_request: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e504c93b495f..bbf0d7ba2dbc 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -3,7 +3,8 @@ name: release-workflow on: push: branches: - - 'master' + - "master" + - "[0-9].[0-9]" jobs: config: diff --git a/.github/workflows/superset-cli.yml b/.github/workflows/superset-cli.yml index bfe3dedc183b..9c52f3649769 100644 --- a/.github/workflows/superset-cli.yml +++ b/.github/workflows/superset-cli.yml @@ -3,7 +3,8 @@ name: Superset CLI tests on: push: branches: - - 'master' + - "master" + - "[0-9].[0-9]" pull_request: types: [synchronize, opened, reopened, ready_for_review] @@ -55,8 +56,8 @@ jobs: uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - cache: 'pip' - cache-dependency-path: 'requirements/testing.txt' + cache: "pip" + cache-dependency-path: "requirements/testing.txt" - name: Install dependencies if: steps.check.outcome == 'failure' uses: ./.github/actions/cached-dependencies diff --git a/.github/workflows/superset-docs-deploy.yml b/.github/workflows/superset-docs-deploy.yml index aa733eba2378..d8bcf7e644fa 100644 --- a/.github/workflows/superset-docs-deploy.yml +++ b/.github/workflows/superset-docs-deploy.yml @@ -6,6 +6,7 @@ on: - "docs/**" branches: - "master" + - "[0-9].[0-9]" jobs: config: @@ -38,7 +39,7 @@ jobs: - name: Set up Node.js 16 uses: actions/setup-node@v4 with: - node-version: '16' + node-version: "16" - name: yarn install run: | yarn install --check-cache diff --git a/.github/workflows/superset-e2e.yml b/.github/workflows/superset-e2e.yml index 3126b9a6a00f..2d31632a3d42 100644 --- a/.github/workflows/superset-e2e.yml +++ b/.github/workflows/superset-e2e.yml @@ -3,7 +3,8 @@ name: E2E on: push: branches: - - 'master' + - "master" + - "[0-9].[0-9]" pull_request: types: [synchronize, opened, reopened, ready_for_review] @@ -117,7 +118,7 @@ jobs: uses: ./.github/actions/cached-dependencies env: CYPRESS_BROWSER: ${{ matrix.browser }} - CYPRESS_KEY: YjljODE2MzAtODcwOC00NTA3LWE4NmMtMTU3YmFmMjIzOTRhCg== + CYPRESS_KEY: ${{ secrets.CYPRESS_RECORD_KEY }} with: run: cypress-run-all - name: Upload Artifacts diff --git a/.github/workflows/superset-frontend.yml b/.github/workflows/superset-frontend.yml index 3ac99de33c7f..773984a8fb02 100644 --- a/.github/workflows/superset-frontend.yml +++ b/.github/workflows/superset-frontend.yml @@ -4,6 +4,7 @@ on: push: branches: - "master" + - "[0-9].[0-9]" paths: - "superset-frontend/**" pull_request: @@ -83,6 +84,8 @@ jobs: working-directory: ./superset-frontend/packages/generator-superset run: npx jest - name: Upload code coverage - if: steps.check.outcome == 'failure' - working-directory: ./superset-frontend - run: ../.github/workflows/codecov.sh -c -F javascript + uses: codecov/codecov-action@v4 + with: + flags: javascript + token: ${{ secrets.CODECOV_TOKEN }} + verbose: true diff --git a/.github/workflows/superset-helm-release.yml b/.github/workflows/superset-helm-release.yml index b8db3d218665..fd60f82d498b 100644 --- a/.github/workflows/superset-helm-release.yml +++ b/.github/workflows/superset-helm-release.yml @@ -4,6 +4,7 @@ on: push: branches: - "master" + - "[0-9].[0-9]" paths: - "helm/**" diff --git a/.github/workflows/superset-python-integrationtest.yml b/.github/workflows/superset-python-integrationtest.yml index 385edf89b57b..4a4588fef3f7 100644 --- a/.github/workflows/superset-python-integrationtest.yml +++ b/.github/workflows/superset-python-integrationtest.yml @@ -4,7 +4,8 @@ name: Python-Integration on: push: branches: - - 'master' + - "master" + - "[0-9].[0-9]" pull_request: types: [synchronize, opened, reopened, ready_for_review] @@ -54,8 +55,8 @@ jobs: uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - cache: 'pip' - cache-dependency-path: 'requirements/testing.txt' + cache: "pip" + cache-dependency-path: "requirements/testing.txt" - name: Install dependencies if: steps.check.outcome == 'failure' uses: ./.github/actions/cached-dependencies @@ -74,10 +75,11 @@ jobs: run: | ./scripts/python_tests.sh - name: Upload code coverage - if: steps.check.outcome == 'failure' - run: | - bash .github/workflows/codecov.sh -c -F python -F mysql - + uses: codecov/codecov-action@v4 + with: + flags: python,mysql + token: ${{ secrets.CODECOV_TOKEN }} + verbose: true test-postgres: runs-on: ubuntu-20.04 strategy: @@ -120,8 +122,8 @@ jobs: uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - cache: 'pip' - cache-dependency-path: 'requirements/testing.txt' + cache: "pip" + cache-dependency-path: "requirements/testing.txt" - name: Install dependencies if: steps.check.outcome == 'failure' uses: ./.github/actions/cached-dependencies @@ -140,9 +142,11 @@ jobs: run: | ./scripts/python_tests.sh - name: Upload code coverage - if: steps.check.outcome == 'failure' - run: | - bash .github/workflows/codecov.sh -c -F python -F postgres + uses: codecov/codecov-action@v4 + with: + flags: python,postgres + token: ${{ secrets.CODECOV_TOKEN }} + verbose: true test-sqlite: runs-on: ubuntu-20.04 @@ -180,8 +184,8 @@ jobs: uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - cache: 'pip' - cache-dependency-path: 'requirements/testing.txt' + cache: "pip" + cache-dependency-path: "requirements/testing.txt" - name: Install dependencies if: steps.check.outcome == 'failure' uses: ./.github/actions/cached-dependencies @@ -200,6 +204,8 @@ jobs: run: | ./scripts/python_tests.sh - name: Upload code coverage - if: steps.check.outcome == 'failure' - run: | - bash .github/workflows/codecov.sh -c -F python -F sqlite + uses: codecov/codecov-action@v4 + with: + flags: python,sqlite + token: ${{ secrets.CODECOV_TOKEN }} + verbose: true diff --git a/.github/workflows/superset-python-misc.yml b/.github/workflows/superset-python-misc.yml index eb5da6f18fc0..e05e05f48bc4 100644 --- a/.github/workflows/superset-python-misc.yml +++ b/.github/workflows/superset-python-misc.yml @@ -4,7 +4,8 @@ name: Python Misc on: push: branches: - - 'master' + - "master" + - "[0-9].[0-9]" paths: - "superset/**" pull_request: diff --git a/.github/workflows/superset-python-presto-hive.yml b/.github/workflows/superset-python-presto-hive.yml index 57d4e0541461..27880dcdb85b 100644 --- a/.github/workflows/superset-python-presto-hive.yml +++ b/.github/workflows/superset-python-presto-hive.yml @@ -4,7 +4,8 @@ name: Python Presto/Hive on: push: branches: - - 'master' + - "master" + - "[0-9].[0-9]" paths: - "superset/**" pull_request: @@ -70,8 +71,8 @@ jobs: uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - cache: 'pip' - cache-dependency-path: 'requirements/testing.txt' + cache: "pip" + cache-dependency-path: "requirements/testing.txt" - name: Install dependencies if: steps.check.outcome == 'failure' uses: ./.github/actions/cached-dependencies @@ -90,9 +91,11 @@ jobs: run: | ./scripts/python_tests.sh -m 'chart_data_flow or sql_json_flow' - name: Upload code coverage - if: steps.check.outcome == 'failure' - run: | - bash .github/workflows/codecov.sh -c -F python -F presto + uses: codecov/codecov-action@v4 + with: + flags: python,presto + token: ${{ secrets.CODECOV_TOKEN }} + verbose: true test-postgres-hive: runs-on: ubuntu-20.04 @@ -147,8 +150,8 @@ jobs: uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - cache: 'pip' - cache-dependency-path: 'requirements/testing.txt' + cache: "pip" + cache-dependency-path: "requirements/testing.txt" - name: Install dependencies if: steps.check.outcome == 'failure' uses: ./.github/actions/cached-dependencies @@ -167,6 +170,8 @@ jobs: run: | ./scripts/python_tests.sh -m 'chart_data_flow or sql_json_flow' - name: Upload code coverage - if: steps.check.outcome == 'failure' - run: | - bash .github/workflows/codecov.sh -c -F python -F hive + uses: codecov/codecov-action@v4 + with: + flags: python,hive + token: ${{ secrets.CODECOV_TOKEN }} + verbose: true diff --git a/.github/workflows/superset-python-unittest.yml b/.github/workflows/superset-python-unittest.yml index 548f128eac22..5c422bfa7138 100644 --- a/.github/workflows/superset-python-unittest.yml +++ b/.github/workflows/superset-python-unittest.yml @@ -4,7 +4,8 @@ name: Python-Unit on: push: branches: - - 'master' + - "master" + - "[0-9].[0-9]" paths: - "superset/**" pull_request: @@ -43,9 +44,9 @@ jobs: uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - cache: 'pip' - cache-dependency-path: 'requirements/testing.txt' -# TODO: separated requirements.txt file just for unit tests + cache: "pip" + cache-dependency-path: "requirements/testing.txt" + # TODO: separated requirements.txt file just for unit tests - name: Install dependencies if: steps.check.outcome == 'failure' uses: ./.github/actions/cached-dependencies @@ -63,6 +64,8 @@ jobs: run: | pytest --durations-min=0.5 --cov-report= --cov=superset ./tests/common ./tests/unit_tests --cache-clear - name: Upload code coverage - if: steps.check.outcome == 'failure' - run: | - bash .github/workflows/codecov.sh -c -F python -F unit + uses: codecov/codecov-action@v4 + with: + flags: python,unit + token: ${{ secrets.CODECOV_TOKEN }} + verbose: true diff --git a/.github/workflows/superset-translations.yml b/.github/workflows/superset-translations.yml index 647e27b3d722..4a3d9cc21a10 100644 --- a/.github/workflows/superset-translations.yml +++ b/.github/workflows/superset-translations.yml @@ -3,7 +3,8 @@ name: Translations on: push: branches: - - 'master' + - "master" + - "[0-9].[0-9]" pull_request: types: [synchronize, opened, reopened, ready_for_review] @@ -24,7 +25,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: '16' + node-version: "16" - name: Install dependencies uses: ./.github/actions/cached-dependencies with: diff --git a/.github/workflows/superset-websocket.yml b/.github/workflows/superset-websocket.yml index d0bf6f2f4e6d..c35573ada11c 100644 --- a/.github/workflows/superset-websocket.yml +++ b/.github/workflows/superset-websocket.yml @@ -2,7 +2,8 @@ name: WebSocket server on: push: branches: - - 'master' + - "master" + - "[0-9].[0-9]" paths: - "superset-websocket/**" pull_request: diff --git a/.github/workflows/tech-debt.yml b/.github/workflows/tech-debt.yml index ccbf0ba18193..1c6a9edd4614 100644 --- a/.github/workflows/tech-debt.yml +++ b/.github/workflows/tech-debt.yml @@ -4,6 +4,7 @@ on: push: branches: - master + - "[0-9].[0-9]" jobs: config: @@ -31,7 +32,7 @@ jobs: - name: Set up Node.js uses: actions/setup-node@v4 with: - node-version: '16' + node-version: "16" - name: Install Dependencies run: npm install @@ -39,7 +40,7 @@ jobs: - name: Run Script env: - SPREADSHEET_ID: '1oABNnzxJYzwUrHjr_c9wfYEq9dFL1ScVof9LlaAdxvo' + SPREADSHEET_ID: "1oABNnzxJYzwUrHjr_c9wfYEq9dFL1ScVof9LlaAdxvo" SERVICE_ACCOUNT_KEY: ${{ secrets.GSHEET_KEY }} run: npm run lint-stats continue-on-error: true diff --git a/CHANGELOG.md b/CHANGELOG.md index 83a04e6ae652..222887a4680a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,3 +37,6 @@ under the License. - [3.0.3](./CHANGELOG/3.0.3.md) - [3.0.4](./CHANGELOG/3.0.4.md) - [3.1.0](./CHANGELOG/3.1.0.md) +- [4.0.0](./CHANGELOG/4.0.0.md) +- [4.0.1](./CHANGELOG/4.0.1.md) +- [4.0.2](./CHANGELOG/4.0.2.md) diff --git a/CHANGELOG/4.0.0.md b/CHANGELOG/4.0.0.md new file mode 100644 index 000000000000..c51a3c2d1861 --- /dev/null +++ b/CHANGELOG/4.0.0.md @@ -0,0 +1,472 @@ + + +## Change Log + +### 4.0 (Mon Apr 1 10:04:00 2024 -0500) + +**Database Migrations** + +- [#27119](https://github.com/apache/superset/pull/27119) refactor: Updates some database columns to MediumText (@michael-s-molina) +- [#27029](https://github.com/apache/superset/pull/27029) chore: Add granular permissions for actions in Dashboard (@geido) +- [#26654](https://github.com/apache/superset/pull/26654) chore: add unique constraint to tagged_objects (@mistercrunch) +- [#26377](https://github.com/apache/superset/pull/26377) refactor: Removes the deprecated redirect endpoint (@michael-s-molina) +- [#26328](https://github.com/apache/superset/pull/26328) refactor: Removes the Filter Box code (@michael-s-molina) +- [#26350](https://github.com/apache/superset/pull/26350) refactor: Migrates legacy Sunburst charts to ECharts and removes legacy code (@michael-s-molina) +- [#26369](https://github.com/apache/superset/pull/26369) refactor: Removes the filters set feature (@michael-s-molina) +- [#26416](https://github.com/apache/superset/pull/26416) fix: improve performance on reports log queries (@dpgaspar) +- [#26290](https://github.com/apache/superset/pull/26290) feat(echarts-funnel): Implement % calculation type (@kgabryje) +- [#26288](https://github.com/apache/superset/pull/26288) chore: Ensure Mixins are ordered according to the MRO (@john-bodley) + +**Features** + +- [#27159](https://github.com/apache/superset/pull/27159) feat: bump FAB to 4.4.0 (@dpgaspar) +- [#26202](https://github.com/apache/superset/pull/26202) feat(Alerts and Reports): Modal redesign (@rtexelm) +- [#26907](https://github.com/apache/superset/pull/26907) feat(storybook): Co-habitating/Upgrading Storybooks to v7 (dependency madness ensues) (@rusackas) +- [#27092](https://github.com/apache/superset/pull/27092) feat(plugins): Tooltips on BigNumber with Time Comparison chart (@Antonio-RiveroMartnez) +- [#27052](https://github.com/apache/superset/pull/27052) feat(plugins): Adding colors to BigNumber with Time Comparison chart (@Antonio-RiveroMartnez) +- [#27054](https://github.com/apache/superset/pull/27054) feat(plugins): Update custom controls for BigNumber with Time Comparison chart (@Antonio-RiveroMartnez) +- [#27055](https://github.com/apache/superset/pull/27055) feat(docker): allow for docker release builds to be multi-platform (@mistercrunch) +- [#26923](https://github.com/apache/superset/pull/26923) feat: docker image tags documentation + tweaks (@mistercrunch) +- [#26945](https://github.com/apache/superset/pull/26945) feat(ci): kill duplicate CI jobs on PRs (@mistercrunch) +- [#26639](https://github.com/apache/superset/pull/26639) feat(components): Add static class name with button style (@mskelton) +- [#26908](https://github.com/apache/superset/pull/26908) feat: Period over Period Big Number comparison chart (@eschutho) +- [#26912](https://github.com/apache/superset/pull/26912) feat(ci): unleash dependabot on our github actions (@mistercrunch) +- [#26300](https://github.com/apache/superset/pull/26300) feat(maps): Consolidating all country maps (and TS) into the Jupyter notebook workflow. (@rusackas) +- [#26877](https://github.com/apache/superset/pull/26877) feat(ci): add a check to make sure there's no hold label on the PR (@mistercrunch) +- [#26880](https://github.com/apache/superset/pull/26880) feat: configuring an extensible PR auto-labeler (@mistercrunch) +- [#25323](https://github.com/apache/superset/pull/25323) feat(i18n): add ukranian translations (@GlugovGrGlib) +- [#26443](https://github.com/apache/superset/pull/26443) feat: add chart id and dataset id to global logs (@eschutho) +- [#26754](https://github.com/apache/superset/pull/26754) feat: Stop editor scrolling to top (@puridach-w) +- [#26745](https://github.com/apache/superset/pull/26745) feat: auto-label PRs that contain db migrations (@mistercrunch) +- [#26418](https://github.com/apache/superset/pull/26418) feat: global logs context (@eschutho) +- [#26604](https://github.com/apache/superset/pull/26604) feat(celery): upgrade celery and its dependencies packages (@Musa10) +- [#26407](https://github.com/apache/superset/pull/26407) feat: Add ValuePercent option to LABEL TYPE for Pie and Funnel charts (@kainchow) +- [#26278](https://github.com/apache/superset/pull/26278) feat(releasing): adding SHA512 and RSA signature validation script to verify releases (@rusackas) +- [#26011](https://github.com/apache/superset/pull/26011) feat(telemetry): Adding Scarf based telemetry to Superset (@rusackas) +- [#26196](https://github.com/apache/superset/pull/26196) feat(docker): Add ARM builds (@alekseyolg) +- [#26161](https://github.com/apache/superset/pull/26161) feat: Create db_engine_spec ibmi.py (@wAVeckx) + +**Fixes** + +- [#27706](https://github.com/apache/superset/pull/27706) fix: Select onChange is fired when the same item is selected in single mode (@michael-s-molina) +- [#27763](https://github.com/apache/superset/pull/27763) fix: Removes filter plugins from viz gallery (@michael-s-molina) +- [#27744](https://github.com/apache/superset/pull/27744) fix: reduce alert error to warning (@eschutho) +- [#27558](https://github.com/apache/superset/pull/27558) fix(explore): drag and drop indicator UX (@justinpark) +- [#27644](https://github.com/apache/superset/pull/27644) fix: Provide more inclusive error handling for saved queries (@john-bodley) +- [#27646](https://github.com/apache/superset/pull/27646) fix: Leverage actual database for rendering Jinjarized SQL (@john-bodley) +- [#27550](https://github.com/apache/superset/pull/27550) fix(AlertReports): disabling value when not null option is active (@fisjac) +- [#27636](https://github.com/apache/superset/pull/27636) fix(sqllab): unable to remove table (@justinpark) +- [#27022](https://github.com/apache/superset/pull/27022) fix(Chart Annotation modal): Table and Superset annotation options will paginate, exceeding previous max limit 100 (@rtexelm) +- [#27552](https://github.com/apache/superset/pull/27552) fix(AlertReports): defaulting grace period to undefined (@fisjac) +- [#27551](https://github.com/apache/superset/pull/27551) fix(AlertReports): clearing custom_width when disabled (@fisjac) +- [#27470](https://github.com/apache/superset/pull/27470) fix(sql_parse): Ensure table extraction handles Jinja templating (@john-bodley) +- [#27601](https://github.com/apache/superset/pull/27601) fix: Persist query params appended to permalink (@kgabryje) +- [#27613](https://github.com/apache/superset/pull/27613) fix(Dashboard): Add editMode conditional for translate3d fix on charts to allow intended Fullscreen (@rtexelm) +- [#27388](https://github.com/apache/superset/pull/27388) fix(utils): fix off-by-one error in how rolling window's min_periods truncates dataframe (@sfirke) +- [#19595](https://github.com/apache/superset/pull/19595) fix: Volatile datasource ordering in dashboard export (@pnadolny13) +- [#27577](https://github.com/apache/superset/pull/27577) fix: sqlglot SQL Server (@betodealmeida) +- [#27576](https://github.com/apache/superset/pull/27576) fix: bump sqlglot to support materialized CTEs (@betodealmeida) +- [#27567](https://github.com/apache/superset/pull/27567) fix(db_engine_specs): Update convert_dttm to work correctly with CrateDB (@hlcianfagna) +- [#27605](https://github.com/apache/superset/pull/27605) fix: Skips Hive tests that are blocking PRs (@michael-s-molina) +- [#27566](https://github.com/apache/superset/pull/27566) fix: guest queries (@betodealmeida) +- [#27464](https://github.com/apache/superset/pull/27464) fix: pass valid SQL to SM (@betodealmeida) +- [#26748](https://github.com/apache/superset/pull/26748) fix: `improve _extract_tables_from_sql` (@betodealmeida) +- [#27539](https://github.com/apache/superset/pull/27539) fix(explore): Allow only saved metrics and columns (@justinpark) +- [#27260](https://github.com/apache/superset/pull/27260) fix(alerts/reports): implementing custom_width as an Antd number input (@fisjac) +- [#27487](https://github.com/apache/superset/pull/27487) fix(postprocessing): resample with holes (@villebro) +- [#27484](https://github.com/apache/superset/pull/27484) fix: check if guest user modified query (@betodealmeida) +- [#27471](https://github.com/apache/superset/pull/27471) fix(webpack): remove double-dotted file extensions in webpack config (@rusackas) +- [#27186](https://github.com/apache/superset/pull/27186) fix: SSH Tunnel configuration settings (@geido) +- [#27411](https://github.com/apache/superset/pull/27411) fix(dashboard): Only fetch CSS templates for dashboard header menu when in edit mode (@mskelton) +- [#27315](https://github.com/apache/superset/pull/27315) fix(deps): resolving canvg and html2canvas module not found (@fisjac) +- [#27403](https://github.com/apache/superset/pull/27403) fix: missing shared color in mixed timeseries (@justinpark) +- [#27402](https://github.com/apache/superset/pull/27402) fix: typescript errors in 4.0 (@justinpark) +- [#27390](https://github.com/apache/superset/pull/27390) fix: Re-enable CI checks on release branches (@michael-s-molina) +- [#27391](https://github.com/apache/superset/pull/27391) fix(sqllab): Close already removed tab (@justinpark) +- [#27364](https://github.com/apache/superset/pull/27364) fix(API): Updating assets via the API should preserve ownership configuration (@Vitor-Avila) +- [#27395](https://github.com/apache/superset/pull/27395) fix: improve explore REST api validations (@dpgaspar) +- [#27262](https://github.com/apache/superset/pull/27262) fix(Alerts & Reports): Fixing bug that resets cron value to default when empty (@fisjac) +- [#27366](https://github.com/apache/superset/pull/27366) fix: Results section in Explore shows an infinite spinner (@michael-s-molina) +- [#27187](https://github.com/apache/superset/pull/27187) fix: numexpr to fix CVE-2023-39631⁠ (2.8.4 => 2.9.0) (@nigzak) +- [#27361](https://github.com/apache/superset/pull/27361) fix: Missing SQL Lab permission (@michael-s-molina) +- [#27360](https://github.com/apache/superset/pull/27360) fix: Heatmap numeric sorting (@michael-s-molina) +- [#27313](https://github.com/apache/superset/pull/27313) fix(sqllab): Missing empty query result state (@justinpark) +- [#27308](https://github.com/apache/superset/pull/27308) fix(dashboard): table chart drag preview overflowing container (@rtexelm) +- [#27295](https://github.com/apache/superset/pull/27295) fix(sqllab): invalid dump sql shown after closing tab (@justinpark) +- [#27285](https://github.com/apache/superset/pull/27285) fix(plugin-chart-echarts): calculate Gauge Chart intervals correctly when min value is set (@goto-loop) +- [#27307](https://github.com/apache/superset/pull/27307) fix: Incorrect data type on import page (@michael-s-molina) +- [#27291](https://github.com/apache/superset/pull/27291) fix: Data zoom with horizontal orientation (@michael-s-molina) +- [#27273](https://github.com/apache/superset/pull/27273) fix: Navigating to an invalid page index in lists (@michael-s-molina) +- [#27271](https://github.com/apache/superset/pull/27271) fix: Inoperable dashboard filter slider when range is <= 1 (@michael-s-molina) +- [#27154](https://github.com/apache/superset/pull/27154) fix(import-datasources): Use "admin" user as default for importing datasources (@ddxv) +- [#27258](https://github.com/apache/superset/pull/27258) fix: Sorting charts/dashboards makes the applied filters ineffective (@michael-s-molina) +- [#27213](https://github.com/apache/superset/pull/27213) fix(trino): bumping trino to fix hudi schema fetching (@rusackas) +- [#27236](https://github.com/apache/superset/pull/27236) fix(reports): fixing unit test (@fisjac) +- [#27217](https://github.com/apache/superset/pull/27217) fix(sqlglot): Address regressions introduced in #26476 (@john-bodley) +- [#27233](https://github.com/apache/superset/pull/27233) fix: bump FAB to 4.4.1 (perf issue) (@dpgaspar) +- [#27167](https://github.com/apache/superset/pull/27167) fix: setting important lower bounds versions on requirements (@dpgaspar) +- [#27215](https://github.com/apache/superset/pull/27215) fix: no limit in SELECT * for TOP dbs (@betodealmeida) +- [#27214](https://github.com/apache/superset/pull/27214) fix(releasing): fixes npm script for release validation (@rusackas) +- [#26074](https://github.com/apache/superset/pull/26074) fix: Translations related to the date range filter (@Ralkion) +- [#26699](https://github.com/apache/superset/pull/26699) fix(dashboard): drag and drop indicator UX (@justinpark) +- [#27191](https://github.com/apache/superset/pull/27191) fix: Failed to execute importScripts on worker-css (@michael-s-molina) +- [#27181](https://github.com/apache/superset/pull/27181) fix(sqllab): typeahead search is broken in db selector (@justinpark) +- [#27164](https://github.com/apache/superset/pull/27164) fix: unlock and bump werkzeug (@dpgaspar) +- [#27161](https://github.com/apache/superset/pull/27161) fix(ci): mypy pre-commit issues (@dpgaspar) +- [#27138](https://github.com/apache/superset/pull/27138) fix(plugins): Apply dashboard filters to comparison query in BigNumber with Time Comparison chart (@Antonio-RiveroMartnez) +- [#27135](https://github.com/apache/superset/pull/27135) fix: Duplicated toast messages (@michael-s-molina) +- [#27132](https://github.com/apache/superset/pull/27132) fix: Plain error message when visiting a dashboard via permalink without permissions (@michael-s-molina) +- [#27130](https://github.com/apache/superset/pull/27130) fix: ID param for DELETE ssh_tunnel endpoint (@geido) +- [#22840](https://github.com/apache/superset/pull/22840) fix(pivot-table-v2): Added forgotten translation pivot table v2 (@Always-prog) +- [#27128](https://github.com/apache/superset/pull/27128) fix: RLS modal overflow (@michael-s-molina) +- [#27112](https://github.com/apache/superset/pull/27112) fix: gevent upgrade to 23.9.1 (@dpgaspar) +- [#27117](https://github.com/apache/superset/pull/27117) fix: removes old deprecated sqllab endpoints (@dpgaspar) +- [#27124](https://github.com/apache/superset/pull/27124) fix: bump grpcio, urllib3 and paramiko (@dpgaspar) +- [#27116](https://github.com/apache/superset/pull/27116) fix(docker): \*-dev tags target right stage from Dockerfile (@lodu) +- [#26791](https://github.com/apache/superset/pull/26791) fix(sqllab): flaky json explore modal due to over-rendering (@justinpark) +- [#27113](https://github.com/apache/superset/pull/27113) fix: upgrade cryptography to major 42 (@dpgaspar) +- [#27106](https://github.com/apache/superset/pull/27106) fix: Timeseries Y-axis format with contribution mode (@michael-s-molina) +- [#27098](https://github.com/apache/superset/pull/27098) fix: try to fix cypress with magic (@mistercrunch) +- [#27094](https://github.com/apache/superset/pull/27094) fix(helm): typo on ssl_cert_reqs variable (@pcop00) +- [#27091](https://github.com/apache/superset/pull/27091) fix(deps): un-bumping dom-to-pdf ro resolve missing file warnings (@rusackas) +- [#27087](https://github.com/apache/superset/pull/27087) fix(ci): Docker master builds fail while checking version (@mistercrunch) +- [#27085](https://github.com/apache/superset/pull/27085) fix(ci): new PR comments cancel ongoing ephemeral builds (@dpgaspar) +- [#26663](https://github.com/apache/superset/pull/26663) fix(helm): Include option to use Redis with SSL (@shakeelansari63) +- [#27060](https://github.com/apache/superset/pull/27060) fix(ephemeral): last try fixing this GH action (@mistercrunch) +- [#27058](https://github.com/apache/superset/pull/27058) fix(ephemeral): point to the full tag name (@mistercrunch) +- [#27057](https://github.com/apache/superset/pull/27057) fix(ephemeral): fix tagging command for ECR (@mistercrunch) +- [#27056](https://github.com/apache/superset/pull/27056) fix(ephemeral): fix ephemeral builds in PR (@mistercrunch) +- [#27048](https://github.com/apache/superset/pull/27048) fix(actions): correcting malformed labeler configs (@rusackas) +- [#19744](https://github.com/apache/superset/pull/19744) fix(webpack-dev-server): parse env args (@jdbranham) +- [#27042](https://github.com/apache/superset/pull/27042) fix(ci): fix action script v7 breaking changes v3 (@dpgaspar) +- [#27040](https://github.com/apache/superset/pull/27040) fix(ci): fix action script v7 breaking changes v2 (@dpgaspar) +- [#27014](https://github.com/apache/superset/pull/27014) fix(maps): france_regions.geojson generated with the notebook, from natural earth data (@qleroy) +- [#26966](https://github.com/apache/superset/pull/26966) fix(actions): make tech debt uploader not block CI and skip w/o creds (@rusackas) +- [#27001](https://github.com/apache/superset/pull/27001) fix(cypress): resolving random dri3 error on cypress runner (@rusackas) +- [#27013](https://github.com/apache/superset/pull/27013) fix(plugins): Fix dashboard filter in Period Over Period KPI plugin (@Antonio-RiveroMartnez) +- [#27005](https://github.com/apache/superset/pull/27005) fix(helm): Fix inconsistency for the chart appVersion and default image tag (@dnskr) +- [#26995](https://github.com/apache/superset/pull/26995) fix(maps): Move Overseas department and regions closer to France mainland (@qleroy) +- [#26987](https://github.com/apache/superset/pull/26987) fix(ci): typo in my bash script (@mistercrunch) +- [#26985](https://github.com/apache/superset/pull/26985) fix(plugin): Period Over Period KPI Plugin Feature flag value (@Antonio-RiveroMartnez) +- [#26969](https://github.com/apache/superset/pull/26969) fix(ci): support action/script v5 breaking change v2 (@dpgaspar) +- [#26968](https://github.com/apache/superset/pull/26968) fix(ci): support action/script v5 breaking change (@dpgaspar) +- [#26963](https://github.com/apache/superset/pull/26963) fix(plugin-chart-table): Revert "fix(chart table in dashboard): improve screen reading of table (#26453)" (@kgabryje) +- [#26949](https://github.com/apache/superset/pull/26949) fix(actions): specify branch on monorepo lockfile pusher (@rusackas) +- [#26921](https://github.com/apache/superset/pull/26921) fix(ci): remove deprecated set-output on github workflows (@dpgaspar) +- [#26920](https://github.com/apache/superset/pull/26920) fix(ci): lint issue on update-monorepo-lockfiles.yml (@dpgaspar) +- [#26919](https://github.com/apache/superset/pull/26919) fix(ci): ephemeral env build and up dependency (@dpgaspar) +- [#26852](https://github.com/apache/superset/pull/26852) fix(ci): ephemeral env build (@dpgaspar) +- [#26917](https://github.com/apache/superset/pull/26917) fix: remove ephemeral docker build from required workflow (@dpgaspar) +- [#26787](https://github.com/apache/superset/pull/26787) fix(docker): improve docker tags to be cleared and avoid conflicts (@mistercrunch) +- [#26904](https://github.com/apache/superset/pull/26904) fix(dependabot): lockfile updater won't fail when there's nothing to push (@rusackas) +- [#26888](https://github.com/apache/superset/pull/26888) fix(dependencies): adding auth for dependabot lockfile action (@rusackas) +- [#26901](https://github.com/apache/superset/pull/26901) fix(svg): reformatting svgs to allow license without breaking images (@rusackas) +- [#26453](https://github.com/apache/superset/pull/26453) fix(chart table in dashboard): improve screen reading of table (@ncar285) +- [#26801](https://github.com/apache/superset/pull/26801) fix: docker should always run, even in forks (@mistercrunch) +- [#26752](https://github.com/apache/superset/pull/26752) fix: add user to latest-release-tag workflow (@eschutho) +- [#26772](https://github.com/apache/superset/pull/26772) fix(docker): credentials issues around superset-cache in forks (@mistercrunch) +- [#25510](https://github.com/apache/superset/pull/25510) fix: change the validation logic for python_date_format (@mapledan) +- [#26710](https://github.com/apache/superset/pull/26710) fix(dependencies): stopping (and preventing) full lodash library import... now using only method level imports. (@rusackas) +- [#26473](https://github.com/apache/superset/pull/26473) fix: docker ephemeral environment, push only on testenv comment (@dpgaspar) +- [#26682](https://github.com/apache/superset/pull/26682) fix: Revert "build(deps): bump @mdx-js/react from 1.6.22 to 3.0.0 in /docs" (@rusackas) +- [#26679](https://github.com/apache/superset/pull/26679) fix: Revert "buld(deps): bump swagger-ui-react from 4.1.3 to 5.11.0 in docs (#26552) (@michael-s-molina) +- [#26648](https://github.com/apache/superset/pull/26648) fix: Removes unused cache cleanup (@michael-s-molina) +- [#26649](https://github.com/apache/superset/pull/26649) fix: remove possible unnecessary file 1 (@dpgaspar) +- [#26351](https://github.com/apache/superset/pull/26351) fix: stringify scarf pixel value (@eschutho) +- [#26205](https://github.com/apache/superset/pull/26205) fix(docker): Remove race condition when building image (@alekseyolg) + +**Others** + +- [#27441](https://github.com/apache/superset/pull/27441) chore: Adds the 4.0 release notes (@michael-s-molina) +- [#27768](https://github.com/apache/superset/pull/27768) chore(docs): Cleanup UPDATING.md (@john-bodley) +- [#27625](https://github.com/apache/superset/pull/27625) perf(explore): virtualized datasource field sections (@justinpark) +- [#27488](https://github.com/apache/superset/pull/27488) perf(sqllab): reduce bootstrap data delay by queries (@justinpark) +- [#27281](https://github.com/apache/superset/pull/27281) chore: bump cryptography minimum to 42.0.4 (@sadpandajoe) +- [#27232](https://github.com/apache/superset/pull/27232) chore: Removes Chromatic workflow and dependencies (@michael-s-molina) +- [#27169](https://github.com/apache/superset/pull/27169) chore: Updates CHANGELOG.md with 3.0.4 data (@michael-s-molina) +- [#27166](https://github.com/apache/superset/pull/27166) docs: add Dropit Shopping to users list (@IlyaDropit) +- [#27143](https://github.com/apache/superset/pull/27143) refactor: Migrate ErrorBoundary to typescript (@EnxDev) +- [#27136](https://github.com/apache/superset/pull/27136) chore(tests): Remove unnecessary explicit Flask-SQLAlchemy session expunges (@john-bodley) +- [#27134](https://github.com/apache/superset/pull/27134) docs: add Geotab to users list (@JZ6) +- [#26693](https://github.com/apache/superset/pull/26693) chore(hail mary): Update package-lock.json via npm-audit-fix (@rusackas) +- [#27129](https://github.com/apache/superset/pull/27129) chore: lower cryptography min version to 41.0.2 (@sadpandajoe) +- [#27120](https://github.com/apache/superset/pull/27120) docs(miscellaneous): Export Datasoruces: export datasources exports to ZIP (@ddxv) +- [#27078](https://github.com/apache/superset/pull/27078) chore(internet_port): added new ports and removed unnecessary string class (@anirudh-hegde) +- [#27118](https://github.com/apache/superset/pull/27118) chore: bump firebolt-sqlalchemy to support service account auth (@Vitor-Avila) +- [#27090](https://github.com/apache/superset/pull/27090) chore(plugins): Update dropdown control for BigNumber with Time Comparison range (@Antonio-RiveroMartnez) +- [#26909](https://github.com/apache/superset/pull/26909) refactor: Ensure Flask framework leverages the Flask-SQLAlchemy session (Phase II) (@john-bodley) +- [#27030](https://github.com/apache/superset/pull/27030) chore: Migrate AlteredSliceTag to typescript (@EnxDev) +- [#26773](https://github.com/apache/superset/pull/26773) chore(translations): updating pot -> po -> json files (babel 2.9.1) (@rusackas) +- [#27071](https://github.com/apache/superset/pull/27071) chore(docs): adding meta db to Feature Flags page (@rusackas) +- [#27072](https://github.com/apache/superset/pull/27072) docs(installation): document multi-platform support in Docker builds (@mistercrunch) +- [#27053](https://github.com/apache/superset/pull/27053) chore: prevent prophet from logging non-errors as errors (@betodealmeida) +- [#27038](https://github.com/apache/superset/pull/27038) chore(docs): bump version number in docs example (@sfirke) +- [#26973](https://github.com/apache/superset/pull/26973) build(deps-dev): bump @types/jest from 26.0.24 to 29.5.12 in /superset-frontend/plugins/plugin-chart-handlebars (@dependabot[bot]) +- [#26260](https://github.com/apache/superset/pull/26260) chore(dashboard): migrate enzyme to RTL (@justinpark) +- [#26989](https://github.com/apache/superset/pull/26989) chore: Remove database ID dependency for SSH Tunnel creation (@geido) +- [#26981](https://github.com/apache/superset/pull/26981) build(deps): bump react-js-cron from 1.2.0 to 2.1.2 in /superset-frontend (@dependabot[bot]) +- [#26893](https://github.com/apache/superset/pull/26893) build(deps-dev): bump copy-webpack-plugin from 9.1.0 to 12.0.2 in /superset-frontend (@dependabot[bot]) +- [#26171](https://github.com/apache/superset/pull/26171) chore(sqllab): migrate to typescript (@justinpark) +- [#27021](https://github.com/apache/superset/pull/27021) chore(plugins): Description, Category and Tags for BigNumber with Period Time Comparison plugin (@Antonio-RiveroMartnez) +- [#27020](https://github.com/apache/superset/pull/27020) docs: add a note about database drivers in Docker builds (@mistercrunch) +- [#26979](https://github.com/apache/superset/pull/26979) build(deps): bump @types/seedrandom from 2.4.30 to 3.0.8 in /superset-frontend (@dependabot[bot]) +- [#27000](https://github.com/apache/superset/pull/27000) chore(github): adding code owners for translation and country map wor… (@rusackas) +- [#26998](https://github.com/apache/superset/pull/26998) docs: add notes to RELEASING about how to deploy docker images (@mistercrunch) +- [#26996](https://github.com/apache/superset/pull/26996) build(deps): bump react-intersection-observer from 9.4.1 to 9.6.0 in /superset-frontend (@dependabot[bot]) +- [#26986](https://github.com/apache/superset/pull/26986) docs(presto): add Presto SSL connection details (@rusackas) +- [#26526](https://github.com/apache/superset/pull/26526) build(deps): bump @vx/legend from 0.0.198 to 0.0.199 in /superset-frontend/plugins/legacy-plugin-chart-histogram (@dependabot[bot]) +- [#26903](https://github.com/apache/superset/pull/26903) chore(dependencies): bump encodable to 0.7.8 (@rusackas) +- [#26977](https://github.com/apache/superset/pull/26977) build(deps-dev): bump webpack from 5.90.0 to 5.90.1 in /docs (@dependabot[bot]) +- [#26974](https://github.com/apache/superset/pull/26974) build(deps-dev): bump @types/node from 20.11.14 to 20.11.16 in /superset-websocket (@dependabot[bot]) +- [#26971](https://github.com/apache/superset/pull/26971) build(deps): bump actions/checkout from 2 to 4 (@dependabot[bot]) +- [#26972](https://github.com/apache/superset/pull/26972) build(deps): bump actions/cache from 1 to 4 (@dependabot[bot]) +- [#26970](https://github.com/apache/superset/pull/26970) build(deps): bump actions/setup-python from 4 to 5 (@dependabot[bot]) +- [#26950](https://github.com/apache/superset/pull/26950) chore(actions): getting fancier with labels (@rusackas) +- [#26952](https://github.com/apache/superset/pull/26952) build(deps): bump actions/setup-java from 1 to 4 (@dependabot[bot]) +- [#26958](https://github.com/apache/superset/pull/26958) build(deps-dev): bump mock-socket from 9.0.3 to 9.3.1 in /superset-frontend (@dependabot[bot]) +- [#26953](https://github.com/apache/superset/pull/26953) build(deps): bump actions/github-script from 3 to 7 (@dependabot[bot]) +- [#26927](https://github.com/apache/superset/pull/26927) build(deps): bump actions/setup-node from 2 to 4 (@dependabot[bot]) +- [#26954](https://github.com/apache/superset/pull/26954) build(deps): bump aws-actions/configure-aws-credentials from 1 to 4 (@dependabot[bot]) +- [#26955](https://github.com/apache/superset/pull/26955) build(deps): bump aws-actions/amazon-ecr-login from 1 to 2 (@dependabot[bot]) +- [#26956](https://github.com/apache/superset/pull/26956) build(deps): bump github/codeql-action from 2 to 3 (@dependabot[bot]) +- [#26938](https://github.com/apache/superset/pull/26938) build(deps): bump moment from 2.29.4 to 2.30.1 in /superset-frontend (@dependabot[bot]) +- [#26943](https://github.com/apache/superset/pull/26943) chore(dependencies): Push lockfile for monorepo updates on rebuild/rebase (@rusackas) +- [#26875](https://github.com/apache/superset/pull/26875) chore: make TS enums strictly PascalCase (@villebro) +- [#26942](https://github.com/apache/superset/pull/26942) chore(ci): run pre-commit across the repo (@mistercrunch) +- [#26935](https://github.com/apache/superset/pull/26935) build(deps): bump interweave from 13.0.0 to 13.1.0 in /superset-frontend (@dependabot[bot]) +- [#26941](https://github.com/apache/superset/pull/26941) build(deps): bump emotion-rgba from 0.0.9 to 0.0.12 in /superset-frontend (@dependabot[bot]) +- [#26939](https://github.com/apache/superset/pull/26939) build(deps-dev): bump @babel/core from 7.22.8 to 7.23.9 in /superset-frontend (@dependabot[bot]) +- [#26940](https://github.com/apache/superset/pull/26940) build(deps): bump shortid from 2.2.14 to 2.2.16 in /superset-frontend (@dependabot[bot]) +- [#26924](https://github.com/apache/superset/pull/26924) build(deps-dev): bump @types/node from 20.11.10 to 20.11.14 in /superset-websocket (@dependabot[bot]) +- [#26928](https://github.com/apache/superset/pull/26928) build(deps): bump chromaui/action from 1 to 10 (@dependabot[bot]) +- [#26929](https://github.com/apache/superset/pull/26929) build(deps): bump azure/setup-helm from 1 to 3 (@dependabot[bot]) +- [#26930](https://github.com/apache/superset/pull/26930) build(deps): bump actions/upload-artifact from 3 to 4 (@dependabot[bot]) +- [#26931](https://github.com/apache/superset/pull/26931) build(deps): bump actions/dependency-review-action from 2 to 4 (@dependabot[bot]) +- [#26918](https://github.com/apache/superset/pull/26918) chore(ci): notify PMCs of changes on required workflows (@dpgaspar) +- [#26372](https://github.com/apache/superset/pull/26372) refactor: Removes the deprecated GENERIC_CHART_AXES feature flag (@michael-s-molina) +- [#26881](https://github.com/apache/superset/pull/26881) build(deps-dev): update @babel/types requirement from ^7.13.12 to ^7.23.9 in /superset-frontend/plugins/plugin-chart-pivot-table (@dependabot[bot]) +- [#26727](https://github.com/apache/superset/pull/26727) build(deps): bump @ant-design/icons from 5.0.1 to 5.2.6 in /superset-frontend (@dependabot[bot]) +- [#26894](https://github.com/apache/superset/pull/26894) build(deps): bump @vx/scale from 0.0.197 to 0.0.199 in /superset-frontend (@dependabot[bot]) +- [#26840](https://github.com/apache/superset/pull/26840) build(deps): bump d3-selection from 1.4.2 to 3.0.0 in /superset-frontend (@dependabot[bot]) +- [#26861](https://github.com/apache/superset/pull/26861) build(deps): bump @visx/axis from 3.5.0 to 3.8.0 in /superset-frontend (@dependabot[bot]) +- [#26272](https://github.com/apache/superset/pull/26272) chore(explore): migrate enzyme to RTL (@justinpark) +- [#26899](https://github.com/apache/superset/pull/26899) build(deps): bump @types/rison from 0.0.6 to 0.0.9 in /superset-frontend (@dependabot[bot]) +- [#26831](https://github.com/apache/superset/pull/26831) build(deps): bump @types/rison from 0.0.6 to 0.0.9 in /superset-frontend/packages/superset-ui-core (@dependabot[bot]) +- [#26869](https://github.com/apache/superset/pull/26869) build(deps): bump dom-to-image-more from 2.16.0 to 3.2.0 in /superset-frontend (@dependabot[bot]) +- [#26902](https://github.com/apache/superset/pull/26902) chore(docs): remove misplaced k8s installation instructions (@sfirke) +- [#26897](https://github.com/apache/superset/pull/26897) build(deps-dev): bump webpack-bundle-analyzer from 4.9.0 to 4.10.1 in /superset-frontend (@dependabot[bot]) +- [#26900](https://github.com/apache/superset/pull/26900) chore(ci): make action/labeler work on fork PRs (@mistercrunch) +- [#26879](https://github.com/apache/superset/pull/26879) chore(dependabot): ignore css-minimizer-webpack-plugin (@mistercrunch) +- [#26860](https://github.com/apache/superset/pull/26860) build(deps): bump rehype-sanitize from 5.0.1 to 6.0.0 in /superset-frontend (@dependabot[bot]) +- [#26872](https://github.com/apache/superset/pull/26872) chore(dependabot): auto-update lockfiles for monorepo package bumps (@rusackas) +- [#26859](https://github.com/apache/superset/pull/26859) build(deps): bump @types/enzyme from 3.10.10 to 3.10.18 in /superset-frontend (@dependabot[bot]) +- [#26874](https://github.com/apache/superset/pull/26874) chore(license): adding a missing license blurb to a translation file (@rusackas) +- [#26870](https://github.com/apache/superset/pull/26870) build(deps): bump yargs and @types/yargs in /superset-frontend (@dependabot[bot]) +- [#26841](https://github.com/apache/superset/pull/26841) chore(dependencies): bump less from 3.12.2 to 4.2.0 in /superset-frontend (@dependabot[bot]) +- [#26868](https://github.com/apache/superset/pull/26868) chore(actions): run docs actions on Node 16 to conform with the project (@rusackas) +- [#26857](https://github.com/apache/superset/pull/26857) chore(actions): generate FOSSA report on master, and ALWAYS check for… (@rusackas) +- [#26826](https://github.com/apache/superset/pull/26826) build(deps-dev): bump @types/uuid from 9.0.7 to 9.0.8 in /superset-websocket (@dependabot[bot]) +- [#26867](https://github.com/apache/superset/pull/26867) build(deps): bump @testing-library/react-hooks from 5.0.3 to 5.1.3 in /superset-frontend (@dependabot[bot]) +- [#26866](https://github.com/apache/superset/pull/26866) build(deps): bump mousetrap and @types/mousetrap in /superset-frontend (@dependabot[bot]) +- [#26865](https://github.com/apache/superset/pull/26865) build(deps): bump react-redux from 7.2.8 to 7.2.9 in /superset-frontend (@dependabot[bot]) +- [#26855](https://github.com/apache/superset/pull/26855) chore(dependabot): lowering bump cadence from weekly to monthly (@rusackas) +- [#26854](https://github.com/apache/superset/pull/26854) chore(CI): get docs building on ALL branches. (@rusackas) +- [#26825](https://github.com/apache/superset/pull/26825) build(deps-dev): bump @types/node from 20.11.5 to 20.11.10 in /superset-websocket (@dependabot[bot]) +- [#26820](https://github.com/apache/superset/pull/26820) chore(lint/a11y): fixing and locking down jsx-a11y/anchor-is-valid (@rusackas) +- [#26819](https://github.com/apache/superset/pull/26819) chore(dependencies): bumps match-sorter (@rusackas) +- [#26798](https://github.com/apache/superset/pull/26798) chore: Add permission to view and drill on Dashboard context (@geido) +- [#26827](https://github.com/apache/superset/pull/26827) build(deps): bump use-immer from 0.8.1 to 0.9.0 in /superset-frontend (@dependabot[bot]) +- [#24272](https://github.com/apache/superset/pull/24272) chore(deps): bump typescript to 4.8.4 (@jansule) +- [#26832](https://github.com/apache/superset/pull/26832) build(deps): bump @types/react-table from 7.0.29 to 7.7.19 in /superset-frontend (@dependabot[bot]) +- [#26834](https://github.com/apache/superset/pull/26834) build(deps-dev): bump @docusaurus/module-type-aliases from 3.1.0 to 3.1.1 in /docs (@dependabot[bot]) +- [#26839](https://github.com/apache/superset/pull/26839) build(deps-dev): bump webpack from 5.89.0 to 5.90.0 in /docs (@dependabot[bot]) +- [#23873](https://github.com/apache/superset/pull/23873) chore: Slovenian translation update (@dkrat7) +- [#26702](https://github.com/apache/superset/pull/26702) chore: fix GitHub 'Unchanged files with check annotations' reports in PR (@mistercrunch) +- [#26726](https://github.com/apache/superset/pull/26726) build(deps): bump prism-react-renderer from 1.2.1 to 2.3.1 in /docs (@dependabot[bot]) +- [#26813](https://github.com/apache/superset/pull/26813) chore(ci): change code owners for .github (@dpgaspar) +- [#26794](https://github.com/apache/superset/pull/26794) chore(dependencies): bumping jinja2 (@rusackas) +- [#26816](https://github.com/apache/superset/pull/26816) chore: add google-auth for new example dashboard (@betodealmeida) +- [#26815](https://github.com/apache/superset/pull/26815) chore: Reformat changelogs (@geido) +- [#26793](https://github.com/apache/superset/pull/26793) chore(dependencies): bumping fonttools (@rusackas) +- [#26442](https://github.com/apache/superset/pull/26442) chore: Technical Debt Metrics (@rusackas) +- [#26800](https://github.com/apache/superset/pull/26800) chore: Splits the CHANGELOG into multiple files (@michael-s-molina) +- [#26621](https://github.com/apache/superset/pull/26621) build(deps): update jquery requirement from ^3.4.1 to ^3.7.1 in /superset-frontend/packages/superset-ui-demo (@dependabot[bot]) +- [#26789](https://github.com/apache/superset/pull/26789) chore(RESOURCES): fix markdown for table formatting (@qleroy) +- [#26759](https://github.com/apache/superset/pull/26759) chore: Add Embed Modal extension override and tests (@geido) +- [#26656](https://github.com/apache/superset/pull/26656) build(deps-dev): bump css-minimizer-webpack-plugin from 3.4.1 to 6.0.0 in /superset-frontend (@dependabot[bot]) +- [#26704](https://github.com/apache/superset/pull/26704) chore: improve/decouple eslint and tsc 'npm run' commands (@mistercrunch) +- [#26728](https://github.com/apache/superset/pull/26728) build(deps): bump @visx/grid from 3.0.1 to 3.5.0 in /superset-frontend (@dependabot[bot]) +- [#26729](https://github.com/apache/superset/pull/26729) build(deps): update classnames requirement from ^2.3.2 to ^2.5.1 in /superset-frontend/plugins/plugin-chart-table (@dependabot[bot]) +- [#26766](https://github.com/apache/superset/pull/26766) chore: prevent CI double runs on push + pull_request (@mistercrunch) +- [#26528](https://github.com/apache/superset/pull/26528) build(deps-dev): bump jest from 26.6.3 to 29.7.0 in /superset-frontend/plugins/plugin-chart-handlebars (@dependabot[bot]) +- [#26513](https://github.com/apache/superset/pull/26513) build(deps): bump d3-color from 1.4.1 to 3.1.0 in /superset-frontend/plugins/legacy-plugin-chart-world-map (@dependabot[bot]) +- [#26596](https://github.com/apache/superset/pull/26596) build(deps): update @types/math-expression-evaluator requirement from ^1.2.1 to ^1.3.3 in /superset-frontend/packages/superset-ui-core (@dependabot[bot]) +- [#26595](https://github.com/apache/superset/pull/26595) build(deps-dev): update @types/lodash requirement from ^4.14.149 to ^4.14.202 in /superset-frontend/plugins/plugin-chart-handlebars (@dependabot[bot]) +- [#26698](https://github.com/apache/superset/pull/26698) build: Parallelize the CI image builds (continued) (@mistercrunch) +- [#26499](https://github.com/apache/superset/pull/26499) build(deps): update d3-cloud requirement from ^1.2.5 to ^1.2.7 in /superset-frontend/plugins/plugin-chart-word-cloud (@dependabot[bot]) +- [#26481](https://github.com/apache/superset/pull/26481) build(deps-dev): bump @types/jest from 26.0.24 to 29.5.11 in /superset-frontend/plugins/plugin-chart-pivot-table (@dependabot[bot]) +- [#26546](https://github.com/apache/superset/pull/26546) build(deps-dev): bump @docusaurus/module-type-aliases from 2.4.1 to 3.1.0 in /docs (@dependabot[bot]) +- [#26105](https://github.com/apache/superset/pull/26105) docs(storybook): fix typo in TimeFormatStories.tsx (@HurSungYun) +- [#26594](https://github.com/apache/superset/pull/26594) build(deps): update whatwg-fetch requirement from ^3.0.0 to ^3.6.20 in /superset-frontend/packages/superset-ui-core (@dependabot[bot]) +- [#26753](https://github.com/apache/superset/pull/26753) chore: do not mark helm releases as github latest (@eschutho) +- [#26718](https://github.com/apache/superset/pull/26718) build(deps): bump @svgr/webpack from 5.5.0 to 8.1.0 in /docs (@dependabot[bot]) +- [#26714](https://github.com/apache/superset/pull/26714) build(deps): bump @visx/axis from 3.0.1 to 3.5.0 in /superset-frontend (@dependabot[bot]) +- [#26760](https://github.com/apache/superset/pull/26760) docs: update fixed CVEs for version 3.0.3 (@dpgaspar) +- [#26483](https://github.com/apache/superset/pull/26483) build(deps): update @types/d3-cloud requirement from ^1.2.1 to ^1.2.9 in /superset-frontend/plugins/plugin-chart-word-cloud (@dependabot[bot]) +- [#26616](https://github.com/apache/superset/pull/26616) build(deps): bump fuse.js from 6.4.6 to 7.0.0 in /superset-frontend (@dependabot[bot]) +- [#26717](https://github.com/apache/superset/pull/26717) build(deps-dev): bump webpack from 5.76.0 to 5.89.0 in /docs (@dependabot[bot]) +- [#26570](https://github.com/apache/superset/pull/26570) build(deps-dev): bump prettier-plugin-packagejson from 2.2.15 to 2.4.9 in /superset-frontend (@dependabot[bot]) +- [#26556](https://github.com/apache/superset/pull/26556) build(deps-dev): bump @babel/register from 7.22.5 to 7.23.7 in /superset-frontend (@dependabot[bot]) +- [#26522](https://github.com/apache/superset/pull/26522) build(deps): update react-table requirement from ^7.6.3 to ^7.8.0 in /superset-frontend/plugins/plugin-chart-table (@dependabot[bot]) +- [#26613](https://github.com/apache/superset/pull/26613) build(deps): bump react-github-btn from 1.2.1 to 1.4.0 in /docs (@dependabot[bot]) +- [#26572](https://github.com/apache/superset/pull/26572) build(deps-dev): bump eslint-plugin-react-hooks from 4.2.0 to 4.6.0 in /superset-frontend (@dependabot[bot]) +- [#26576](https://github.com/apache/superset/pull/26576) build(deps): bump @emotion/babel-preset-css-prop from 11.2.0 to 11.11.0 in /superset-frontend (@dependabot[bot]) +- [#26724](https://github.com/apache/superset/pull/26724) build(deps): bump @saucelabs/theme-github-codeblock from 0.1.1 to 0.2.3 in /docs (@dependabot[bot]) +- [#26720](https://github.com/apache/superset/pull/26720) build(deps): bump @docsearch/react from 3.3.3 to 3.5.2 in /docs (@dependabot[bot]) +- [#26708](https://github.com/apache/superset/pull/26708) chore(dependencies): loosen constraints on dependency checker (@rusackas) +- [#25665](https://github.com/apache/superset/pull/25665) build(deps): bump @babel/traverse from 7.22.8 to 7.23.2 in /superset-frontend (@dependabot[bot]) +- [#26694](https://github.com/apache/superset/pull/26694) chore(dependencies): removes unsued d3-color and d3-array (@rusackas) +- [#26692](https://github.com/apache/superset/pull/26692) chore(dependencies): removes unused minimist (@rusackas) +- [#26690](https://github.com/apache/superset/pull/26690) chore(dependencies): remove unused global-box (@rusackas) +- [#26689](https://github.com/apache/superset/pull/26689) chore(dependencies): remove unused lodash-es (@rusackas) +- [#26688](https://github.com/apache/superset/pull/26688) chore(dependencies): remove unused react-datetime (@rusackas) +- [#26687](https://github.com/apache/superset/pull/26687) chore(dependencies): remove unused ansi-regex (@rusackas) +- [#26686](https://github.com/apache/superset/pull/26686) chore(dependencies): removes unused @visx/tooltip (@rusackas) +- [#26685](https://github.com/apache/superset/pull/26685) chore(dependencies): remove unused @babel/runtime-corejs3 (@rusackas) +- [#26684](https://github.com/apache/superset/pull/26684) chore(dependencies): removes unused bootstrap-slider (@rusackas) +- [#26691](https://github.com/apache/superset/pull/26691) chore(dependencies): npm audit fix for superset-ui-demo (@rusackas) +- [#26703](https://github.com/apache/superset/pull/26703) chore: silence SECRET_KEY warning when running tests (@mistercrunch) +- [#26733](https://github.com/apache/superset/pull/26733) build(deps-dev): bump @types/node from 20.11.1 to 20.11.5 in /superset-websocket (@dependabot[bot]) +- [#26329](https://github.com/apache/superset/pull/26329) refactor: Removes the deprecated DASHBOARD_NATIVE_FILTERS feature flag (@michael-s-molina) +- [#26347](https://github.com/apache/superset/pull/26347) refactor: Removes the deprecated VERSIONED_EXPORT feature flag (@michael-s-molina) +- [#26677](https://github.com/apache/superset/pull/26677) chore: Updates the Release Process link in the issue template (@michael-s-molina) +- [#26375](https://github.com/apache/superset/pull/26375) chore: Updates the bug report template (@michael-s-molina) +- [#26462](https://github.com/apache/superset/pull/26462) refactor: Removes the Profile feature (@michael-s-molina) +- [#26665](https://github.com/apache/superset/pull/26665) build(deps): bump the npm_and_yarn group group in /superset-frontend with 2 updates (@dependabot[bot]) +- [#26661](https://github.com/apache/superset/pull/26661) chore: Updates CHANGELOG.md and UPDATING.md with 3.1.0 data (@michael-s-molina) +- [#26330](https://github.com/apache/superset/pull/26330) refactor: Removes the deprecated DASHBOARD_FILTERS_EXPERIMENTAL feature flag (@michael-s-molina) +- [#26547](https://github.com/apache/superset/pull/26547) build(deps): bump @mdx-js/react from 1.6.22 to 3.0.0 in /docs (@dependabot[bot]) +- [#26552](https://github.com/apache/superset/pull/26552) build(deps): bump swagger-ui-react from 4.1.3 to 5.11.0 in /docs (@dependabot[bot]) +- [#26555](https://github.com/apache/superset/pull/26555) build(deps-dev): bump @tsconfig/docusaurus from 1.0.7 to 2.0.2 in /docs (@dependabot[bot]) +- [#26344](https://github.com/apache/superset/pull/26344) refactor: Removes the deprecated ENABLE_EXPLORE_JSON_CSRF_PROTECTION feature flag (@michael-s-molina) +- [#26345](https://github.com/apache/superset/pull/26345) refactor: Removes the deprecated ENABLE_TEMPLATE_REMOVE_FILTERS feature flag (@michael-s-molina) +- [#25800](https://github.com/apache/superset/pull/25800) docs: update embedded readme with user params context (@jbat) +- [#12175](https://github.com/apache/superset/pull/12175) build(deps): bump node-notifier from 8.0.0 to 8.0.1 in /superset-frontend (@dependabot[bot]) +- [#26549](https://github.com/apache/superset/pull/26549) build(deps): bump clsx from 1.1.1 to 2.1.0 in /docs (@dependabot[bot]) +- [#26560](https://github.com/apache/superset/pull/26560) build(deps-dev): bump typescript from 4.4.4 to 5.3.3 in /docs (@dependabot[bot]) +- [#26650](https://github.com/apache/superset/pull/26650) chore: Updates CHANGELOG.md and UPDATING.md with 3.0.3 data (@michael-s-molina) +- [#26346](https://github.com/apache/superset/pull/26346) refactor: Removes the deprecated REMOVE_SLICE_LEVEL_LABEL_COLORS feature flag (@michael-s-molina) +- [#26200](https://github.com/apache/superset/pull/26200) refactor: Ensure Flask framework leverages the Flask-SQLAlchemy session (Phase I) (@john-bodley) +- [#26633](https://github.com/apache/superset/pull/26633) chore: Deprecates the DASHBOARD_CROSS_FILTERS feature flag (@michael-s-molina) +- [#26635](https://github.com/apache/superset/pull/26635) chore: Deprecates the ENABLE_JAVASCRIPT_CONTROLS feature flag (@michael-s-molina) +- [#26636](https://github.com/apache/superset/pull/26636) chore: Sets DASHBOARD_VIRTUALIZATION feature flag to True by default (@michael-s-molina) +- [#26540](https://github.com/apache/superset/pull/26540) chore(API): Include changed_by.id in Get Charts and Get Datasets API responses (@Vitor-Avila) +- [#26637](https://github.com/apache/superset/pull/26637) chore: Sets the DRILL_BY feature flag to True by default (@michael-s-molina) +- [#26186](https://github.com/apache/superset/pull/26186) refactor: Ensure Celery leverages the Flask-SQLAlchemy session (@john-bodley) +- [#26500](https://github.com/apache/superset/pull/26500) build(deps): update datamaps requirement from ^0.5.8 to ^0.5.9 in /superset-frontend/plugins/legacy-plugin-chart-world-map (@dependabot[bot]) +- [#25663](https://github.com/apache/superset/pull/25663) build(deps-dev): bump @babel/traverse from 7.16.10 to 7.23.2 in /superset-embedded-sdk (@dependabot[bot]) +- [#25664](https://github.com/apache/superset/pull/25664) build(deps): bump @babel/traverse from 7.21.4 to 7.23.2 in /superset-frontend/cypress-base (@dependabot[bot]) +- [#25662](https://github.com/apache/superset/pull/25662) build(deps): bump @babel/traverse from 7.16.3 to 7.23.2 in /docs (@dependabot[bot]) +- [#26606](https://github.com/apache/superset/pull/26606) docs: fix links (@fenilgmehta) +- [#26348](https://github.com/apache/superset/pull/26348) refactor: Removes the deprecated CLIENT_CACHE feature flag (@michael-s-molina) +- [#26349](https://github.com/apache/superset/pull/26349) refactor: Removes the deprecated DASHBOARD_CACHE feature flag (@michael-s-molina) +- [#26450](https://github.com/apache/superset/pull/26450) chore: Deprecates the KV_STORE feature flag (@michael-s-molina) +- [#26343](https://github.com/apache/superset/pull/26343) refactor: Removes the deprecated ENABLE_EXPLORE_DRAG_AND_DROP feature flag (@michael-s-molina) +- [#26331](https://github.com/apache/superset/pull/26331) refactor: Removes the deprecated DISABLE_DATASET_SOURCE_EDIT feature flag (@michael-s-molina) +- [#26589](https://github.com/apache/superset/pull/26589) build(deps): update lodash requirement from ^4.17.11 to ^4.17.21 in /superset-frontend/plugins/legacy-preset-chart-nvd3 (@dependabot[bot]) +- [#26506](https://github.com/apache/superset/pull/26506) build(deps): update urijs requirement from ^1.19.8 to ^1.19.11 in /superset-frontend/plugins/legacy-preset-chart-nvd3 (@dependabot[bot]) +- [#26520](https://github.com/apache/superset/pull/26520) build(deps-dev): bump style-loader from 3.3.3 to 3.3.4 in /superset-frontend (@dependabot[bot]) +- [#26538](https://github.com/apache/superset/pull/26538) build(deps-dev): bump @types/urijs from 1.19.19 to 1.19.25 in /superset-frontend (@dependabot[bot]) +- [#26530](https://github.com/apache/superset/pull/26530) build(deps): update lodash requirement from ^4.17.15 to ^4.17.21 in /superset-frontend/packages/superset-ui-chart-controls (@dependabot[bot]) +- [#26539](https://github.com/apache/superset/pull/26539) build(deps): update xss requirement from ^1.0.10 to ^1.0.14 in /superset-frontend/plugins/plugin-chart-table (@dependabot[bot]) +- [#26545](https://github.com/apache/superset/pull/26545) build(deps): bump moment-timezone from 0.5.37 to 0.5.44 in /superset-frontend (@dependabot[bot]) +- [#26562](https://github.com/apache/superset/pull/26562) build(deps): bump less from 4.1.3 to 4.2.0 in /docs (@dependabot[bot]) +- [#26612](https://github.com/apache/superset/pull/26612) build(deps): bump @docusaurus/preset-classic from 2.4.1 to 2.4.3 in /docs (@dependabot[bot]) +- [#26619](https://github.com/apache/superset/pull/26619) build(deps-dev): bump @types/node from 20.11.0 to 20.11.1 in /superset-websocket (@dependabot[bot]) +- [#26503](https://github.com/apache/superset/pull/26503) build(deps): update prop-types requirement from ^15.6.2 to ^15.8.1 in /superset-frontend/plugins/legacy-plugin-chart-sunburst (@dependabot[bot]) +- [#26509](https://github.com/apache/superset/pull/26509) build(deps): update prop-types requirement from ^15.6.2 to ^15.8.1 in /superset-frontend/plugins/legacy-plugin-chart-rose (@dependabot[bot]) +- [#26515](https://github.com/apache/superset/pull/26515) build(deps): update prop-types requirement from ^15.6.2 to ^15.8.1 in /superset-frontend/plugins/legacy-plugin-chart-country-map (@dependabot[bot]) +- [#26524](https://github.com/apache/superset/pull/26524) build(deps): update prop-types requirement from ^15.6.2 to ^15.8.1 in /superset-frontend/plugins/legacy-plugin-chart-partition (@dependabot[bot]) +- [#26525](https://github.com/apache/superset/pull/26525) build(deps): update prop-types requirement from ^15.6.2 to ^15.8.1 in /superset-frontend/plugins/legacy-plugin-chart-chord (@dependabot[bot]) +- [#26535](https://github.com/apache/superset/pull/26535) build(deps): update prop-types requirement from ^15.6.2 to ^15.8.1 in /superset-frontend/plugins/legacy-plugin-chart-histogram (@dependabot[bot]) +- [#26536](https://github.com/apache/superset/pull/26536) build(deps): update prop-types requirement from ^15.6.2 to ^15.8.1 in /superset-frontend/plugins/legacy-plugin-chart-calendar (@dependabot[bot]) +- [#26541](https://github.com/apache/superset/pull/26541) build(deps): update lodash requirement from ^4.17.15 to ^4.17.21 in /superset-frontend/plugins/plugin-chart-echarts (@dependabot[bot]) +- [#26569](https://github.com/apache/superset/pull/26569) build(deps): update prop-types requirement from ^15.6.2 to ^15.8.1 in /superset-frontend/plugins/legacy-plugin-chart-map-box (@dependabot[bot]) +- [#26574](https://github.com/apache/superset/pull/26574) build(deps): update prop-types requirement from ^15.7.2 to ^15.8.1 in /superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates (@dependabot[bot]) +- [#26580](https://github.com/apache/superset/pull/26580) build(deps): update prop-types requirement from ^15.6.2 to ^15.8.1 in /superset-frontend/plugins/legacy-plugin-chart-horizon (@dependabot[bot]) +- [#26587](https://github.com/apache/superset/pull/26587) build(deps): update prop-types requirement from ^15.7.2 to ^15.8.1 in /superset-frontend/packages/superset-ui-chart-controls (@dependabot[bot]) +- [#26477](https://github.com/apache/superset/pull/26477) build(deps): update prop-types requirement from ^15.6.2 to ^15.8.1 in /superset-frontend/plugins/legacy-plugin-chart-sankey (@dependabot[bot]) +- [#26480](https://github.com/apache/superset/pull/26480) build(deps): update prop-types requirement from ^15.6.2 to ^15.8.1 in /superset-frontend/plugins/legacy-preset-chart-nvd3 (@dependabot[bot]) +- [#26484](https://github.com/apache/superset/pull/26484) build(deps): update prop-types requirement from ^15.6.2 to ^15.8.1 in /superset-frontend/plugins/legacy-plugin-chart-world-map (@dependabot[bot]) +- [#26486](https://github.com/apache/superset/pull/26486) build(deps): update prop-types requirement from ^15.6.2 to ^15.8.1 in /superset-frontend/plugins/legacy-plugin-chart-event-flow (@dependabot[bot]) +- [#26488](https://github.com/apache/superset/pull/26488) build(deps): update prop-types requirement from ^15.6.2 to ^15.8.1 in /superset-frontend/plugins/legacy-plugin-chart-heatmap (@dependabot[bot]) +- [#26492](https://github.com/apache/superset/pull/26492) build(deps): update prop-types requirement from ^15.6.2 to ^15.8.1 in /superset-frontend/plugins/legacy-plugin-chart-sankey-loop (@dependabot[bot]) +- [#26558](https://github.com/apache/superset/pull/26558) build(deps): bump @docusaurus/plugin-client-redirects from 2.4.1 to 2.4.3 in /docs (@dependabot[bot]) +- [#26554](https://github.com/apache/superset/pull/26554) build(deps): bump @algolia/client-search from 4.13.0 to 4.22.1 in /docs (@dependabot[bot]) +- [#26559](https://github.com/apache/superset/pull/26559) build(deps): bump react-draggable from 4.4.3 to 4.4.6 in /superset-frontend (@dependabot[bot]) +- [#26568](https://github.com/apache/superset/pull/26568) build(deps): bump react-resizable from 3.0.4 to 3.0.5 in /superset-frontend (@dependabot[bot]) +- [#26591](https://github.com/apache/superset/pull/26591) build(deps): update lodash requirement from ^4.17.11 to ^4.17.21 in /superset-frontend/packages/generator-superset (@dependabot[bot]) +- [#26592](https://github.com/apache/superset/pull/26592) build(deps): update prop-types requirement from ^15.6.2 to ^15.8.1 in /superset-frontend/plugins/legacy-plugin-chart-paired-t-test (@dependabot[bot]) +- [#26600](https://github.com/apache/superset/pull/26600) build(deps-dev): update yeoman-assert requirement from ^3.1.0 to ^3.1.1 in /superset-frontend/packages/generator-superset (@dependabot[bot]) +- [#26601](https://github.com/apache/superset/pull/26601) build(deps): update fast-safe-stringify requirement from ^2.0.6 to ^2.1.1 in /superset-frontend/plugins/legacy-preset-chart-nvd3 (@dependabot[bot]) +- [#26444](https://github.com/apache/superset/pull/26444) chore(deps): adding dependabot for plugins/packages and upping PR limits. (@rusackas) +- [#26468](https://github.com/apache/superset/pull/26468) docs: Update installing-superset-from-scratch.mdx (@nytai) +- [#26455](https://github.com/apache/superset/pull/26455) build(deps-dev): bump @types/node from 20.10.8 to 20.11.0 in /superset-websocket (@dependabot[bot]) +- [#26447](https://github.com/apache/superset/pull/26447) build(deps-dev): bump @types/node from 20.10.7 to 20.10.8 in /superset-websocket (@dependabot[bot]) +- [#26441](https://github.com/apache/superset/pull/26441) build(deps): bump follow-redirects from 1.15.2 to 1.15.4 in /superset-frontend (@dependabot[bot]) +- [#26440](https://github.com/apache/superset/pull/26440) build(deps-dev): bump follow-redirects from 1.15.3 to 1.15.4 in /superset-embedded-sdk (@dependabot[bot]) +- [#26438](https://github.com/apache/superset/pull/26438) build(deps): bump follow-redirects from 1.14.8 to 1.15.4 in /docs (@dependabot[bot]) +- [#26428](https://github.com/apache/superset/pull/26428) chore(docs): remove incorrect answer from FAQ (@sfirke) +- [#26425](https://github.com/apache/superset/pull/26425) build(deps-dev): bump @types/node from 20.10.6 to 20.10.7 in /superset-websocket (@dependabot[bot]) +- [#24605](https://github.com/apache/superset/pull/24605) chore: Reenable SQLite tests which leverage foreign key constraints et al. (@john-bodley) +- [#26386](https://github.com/apache/superset/pull/26386) build(deps-dev): bump @types/node from 20.10.5 to 20.10.6 in /superset-websocket (@dependabot[bot]) +- [#26381](https://github.com/apache/superset/pull/26381) docs: fix spelling and grammar (@fenilgmehta) +- [#26363](https://github.com/apache/superset/pull/26363) build(deps): bump ws from 8.15.0 to 8.16.0 in /superset-websocket (@dependabot[bot]) +- [#26371](https://github.com/apache/superset/pull/26371) docs: fix config webdriver snippet in install on K8s (@dbaltor) +- [#26368](https://github.com/apache/superset/pull/26368) chore(docs): point to correct StackOverflow page (@sfirke) +- [#26308](https://github.com/apache/superset/pull/26308) docs: update CVEs fixed on 3.0.2 and 2.1.3 (@dpgaspar) +- [#26305](https://github.com/apache/superset/pull/26305) build(deps-dev): bump @types/node from 20.10.4 to 20.10.5 in /superset-websocket (@dependabot[bot]) +- [#26301](https://github.com/apache/superset/pull/26301) chore(sqlalchemy): import from correct path (@villebro) +- [#26294](https://github.com/apache/superset/pull/26294) build(deps-dev): bump eslint from 8.55.0 to 8.56.0 in /superset-websocket (@dependabot[bot]) +- [#26293](https://github.com/apache/superset/pull/26293) chore(cleanup): removing redundant rendering logic in telemetry pixel (@rusackas) +- [#26285](https://github.com/apache/superset/pull/26285) chore(docs): fix typo "loader balancer" -> "load balancer" (@sfirke) +- [#26280](https://github.com/apache/superset/pull/26280) chore(in the wild): Making it even easer to add a name (and cleanup) (@rusackas) +- [#26253](https://github.com/apache/superset/pull/26253) chore(docs): add troubleshooting guide to alerts & reports (@sfirke) +- [#26259](https://github.com/apache/superset/pull/26259) chore(async queries): sending statsd event for async events API call (@zephyring) +- [#25628](https://github.com/apache/superset/pull/25628) chore: adding 'no-experimental-fetch' node option by default (@rusackas) +- [#26220](https://github.com/apache/superset/pull/26220) chore(tests): Add tests to the column denormalization flow (@Vitor-Avila) +- [#26078](https://github.com/apache/superset/pull/26078) chore: add class component tasklist file (@eschutho) +- [#26234](https://github.com/apache/superset/pull/26234) build(deps): bump ws from 8.14.2 to 8.15.0 in /superset-websocket (@dependabot[bot]) +- [#26233](https://github.com/apache/superset/pull/26233) build(deps-dev): bump ts-node from 10.9.1 to 10.9.2 in /superset-websocket (@dependabot[bot]) +- [#26204](https://github.com/apache/superset/pull/26204) build(deps-dev): bump @types/node from 20.10.3 to 20.10.4 in /superset-websocket (@dependabot[bot]) +- [#26174](https://github.com/apache/superset/pull/26174) build(deps-dev): bump eslint from 8.54.0 to 8.55.0 in /superset-websocket (@dependabot[bot]) +- [#26150](https://github.com/apache/superset/pull/26150) docs: update CHANGELOG for 2.1.2 (@dpgaspar) +- [#26166](https://github.com/apache/superset/pull/26166) build(deps-dev): bump eslint-config-prettier from 9.0.0 to 9.1.0 in /superset-websocket (@dependabot[bot]) +- [#26167](https://github.com/apache/superset/pull/26167) build(deps-dev): bump @types/node from 20.10.1 to 20.10.3 in /superset-websocket (@dependabot[bot]) +- [#26129](https://github.com/apache/superset/pull/26129) docs: add quickstart (@artofcomputing) +- [#26143](https://github.com/apache/superset/pull/26143) build(deps-dev): bump @types/node from 20.10.0 to 20.10.1 in /superset-websocket (@dependabot[bot]) +- [#26149](https://github.com/apache/superset/pull/26149) docs: update CVEs fixed on 3.0.0 (@dpgaspar) +- [#26038](https://github.com/apache/superset/pull/26038) docs(drivers): refresh guide on adding a db driver in docker (@sfirke) +- [#26124](https://github.com/apache/superset/pull/26124) docs: add Increff to INTHEWILD (@ishansinghania) +- [#26112](https://github.com/apache/superset/pull/26112) docs: add Onebeat to users list (@GuyAttia) +- [#26119](https://github.com/apache/superset/pull/26119) docs: Update Trino Kerberos configuration (@john-bodley) +- [#26100](https://github.com/apache/superset/pull/26100) build(deps-dev): bump @types/node from 20.9.4 to 20.10.0 in /superset-websocket (@dependabot[bot]) +- [#26099](https://github.com/apache/superset/pull/26099) build(deps-dev): bump @types/cookie from 0.5.4 to 0.6.0 in /superset-websocket (@dependabot[bot]) +- [#26104](https://github.com/apache/superset/pull/26104) docs: update CVEs fixed on 2.1.2 (@dpgaspar) diff --git a/CHANGELOG/4.0.1.md b/CHANGELOG/4.0.1.md new file mode 100644 index 000000000000..b2dee5abb420 --- /dev/null +++ b/CHANGELOG/4.0.1.md @@ -0,0 +1,61 @@ + + +## Change Log + +### 4.0.1 (Tue Apr 30 15:32:33 2024 -0700) + +**Fixes** + +- [#28269](https://github.com/apache/superset/pull/28269) fix(explore): cannot reorder dnd of Metrics (@justinpark) +- [#28271](https://github.com/apache/superset/pull/28271) fix: % replace in `values_for_column` (@betodealmeida) +- [#28277](https://github.com/apache/superset/pull/28277) fix(ci): adding codecov token (@rusackas) +- [#28242](https://github.com/apache/superset/pull/28242) fix(dashboard): unable to drop tabs in columns (@justinpark) +- [#28241](https://github.com/apache/superset/pull/28241) fix(explore): temporal column mixin (@justinpark) +- [#28226](https://github.com/apache/superset/pull/28226) fix(maps): adds Crimea back to Ukraine 🇺🇦 (@rusackas) +- [#28156](https://github.com/apache/superset/pull/28156) fix(sqllab): invalid css scope for ace editor autocomplete (@justinpark) +- [#28222](https://github.com/apache/superset/pull/28222) fix: Dremio alias (@betodealmeida) +- [#28152](https://github.com/apache/superset/pull/28152) fix(sql_parse): Provide more lenient logic when extracting latest[_sub]\_partition (@john-bodley) +- [#27554](https://github.com/apache/superset/pull/27554) fix(AlertsReports): making log retention "None" option valid (@fisjac) +- [#28117](https://github.com/apache/superset/pull/28117) fix(sql_parse): Support Jinja format() filter when extracting latest[_sub]\_partition (@john-bodley) +- [#28036](https://github.com/apache/superset/pull/28036) fix: Dynamic filter does not show all values on blur/clear events (@michael-s-molina) +- [#28018](https://github.com/apache/superset/pull/28018) fix: bump client side chart timeouts to use the SUPERSET_WEBSERVER_TIMEOUT (@eschutho) +- [#28017](https://github.com/apache/superset/pull/28017) fix: Select is accepting unknown pasted values when `allowNewOptions` is false (@michael-s-molina) +- [#27996](https://github.com/apache/superset/pull/27996) fix: Incorrect onChange value when an unloaded value is pasted into AsyncSelect (@michael-s-molina) +- [#27934](https://github.com/apache/superset/pull/27934) fix(time_offset): improved LIMIT-handling in advanced analytics (@Antonio-RiveroMartnez) +- [#27941](https://github.com/apache/superset/pull/27941) fix(drillby): Enable DrillBy in charts w/o filters (dimensions) (@sowo) +- [#27239](https://github.com/apache/superset/pull/27239) fix(alerts/reports): removing duplicate notification method options (@fisjac) +- [#27968](https://github.com/apache/superset/pull/27968) fix(Dashboard): Add aria-label to filters and search forms (@geido) +- [#27701](https://github.com/apache/superset/pull/27701) fix: useTruncation infinite loop, reenable dashboard cross links on ChartList (@kgabryje) +- [#27926](https://github.com/apache/superset/pull/27926) fix: Locale sent to frontend (@michael-s-molina) +- [#25407](https://github.com/apache/superset/pull/25407) fix(frontend): allow "constructor" property in response data (@SpencerTorres) +- [#27919](https://github.com/apache/superset/pull/27919) fix: add mariadb engine spec same as MySQL (@dpgaspar) +- [#27593](https://github.com/apache/superset/pull/27593) fix(Dashboard): Add border to row when hovering HoverMenu in edit mode (@rtexelm) +- [#27883](https://github.com/apache/superset/pull/27883) fix(bar-chart): change legend padding for horizontal orientation (@lilykuang) +- [#27700](https://github.com/apache/superset/pull/27700) fix: row limits & row count labels are confusing (@mistercrunch) +- [#27845](https://github.com/apache/superset/pull/27845) fix(dashboard): missing null check in error extra (@justinpark) + +**Others** + +- [#28278](https://github.com/apache/superset/pull/28278) chore: allow codecov to detect SHA (@mistercrunch) +- [#27717](https://github.com/apache/superset/pull/27717) chore(explore): Hide non-droppable metric and column list (@justinpark) +- [#27725](https://github.com/apache/superset/pull/27725) chore(sqllab): Do not strip comments when executing SQL statements (@john-bodley) +- [#27843](https://github.com/apache/superset/pull/27843) chore: Default to engine specification regarding using wildcard (@john-bodley) +- [#27858](https://github.com/apache/superset/pull/27858) chore(sql_parse): Provide more meaningful SQLGlot errors (@john-bodley) +- [#27842](https://github.com/apache/superset/pull/27842) chore(sql_parse): Strip leading/trailing whitespace in Jinja macro extraction (@john-bodley) diff --git a/CHANGELOG/4.0.2.md b/CHANGELOG/4.0.2.md new file mode 100644 index 000000000000..f2512c12bc55 --- /dev/null +++ b/CHANGELOG/4.0.2.md @@ -0,0 +1,78 @@ + + +## Change Log + +### 4.0.2 (Wed Jun 26 14:26:58 2024 -0300) + +**Fixes** + +- [#28639](https://github.com/apache/superset/pull/28639) fix: adds the ability to disallow SQL functions per engine (@dpgaspar) +- [#29367](https://github.com/apache/superset/pull/29367) fix(explore): don't respect y-axis formatting (@justinpark) +- [#29345](https://github.com/apache/superset/pull/29345) fix(revert 27883): Excess padding in horizontal Bar charts (@michael-s-molina) +- [#29349](https://github.com/apache/superset/pull/29349) fix(explore): restored hidden field values has discarded (@justinpark) +- [#29346](https://github.com/apache/superset/pull/29346) fix: Cannot delete empty column inside a tab using the dashboard editor (@michael-s-molina) +- [#29314](https://github.com/apache/superset/pull/29314) fix: Remove recursive repr call (@jessie-ross) +- [#29301](https://github.com/apache/superset/pull/29301) fix(metastore-cache): prune before add (@villebro) +- [#29278](https://github.com/apache/superset/pull/29278) fix(sqllab): invalid empty state on switch tab (@justinpark) +- [#29291](https://github.com/apache/superset/pull/29291) fix: filters not updating with force update when caching is enabled (@ka-weihe) +- [#28744](https://github.com/apache/superset/pull/28744) fix(permalink): adding anchor to dashboard permalink generation (@fisjac) +- [#29260](https://github.com/apache/superset/pull/29260) fix: Custom SQL filter control (@michael-s-molina) +- [#29248](https://github.com/apache/superset/pull/29248) fix(sqllab): Do not strip comments when executing SQL statements (@john-bodley) +- [#28755](https://github.com/apache/superset/pull/28755) fix: Workaround for Pandas.DataFrame.to_csv bug (@john-bodley) +- [#29234](https://github.com/apache/superset/pull/29234) fix(Explore): Keep necessary form data to allow query mode switching (@rtexelm) +- [#29230](https://github.com/apache/superset/pull/29230) fix(sqllab): run previous state query (@justinpark) +- [#29119](https://github.com/apache/superset/pull/29119) fix(mixed-timeseries-plugin): Second query stacks stacked on top of first query series (@kgabryje) +- [#28932](https://github.com/apache/superset/pull/28932) fix(embedded): add missing GUEST_TOKEN_HEADER_NAME to bootstrap data (@hexcafe) +- [#29084](https://github.com/apache/superset/pull/29084) fix: Remove BASE_AXIS from pre-query (@john-bodley) +- [#29081](https://github.com/apache/superset/pull/29081) fix(explore): Drill to detail truncates int64 IDs (@justinpark) +- [#28771](https://github.com/apache/superset/pull/28771) fix(Mixed Chart Filter Control): Allow delete condition for `adhoc_filters_b` (@rtexelm) +- [#28772](https://github.com/apache/superset/pull/28772) fix(dashboard): unable to resize due to the overlapped droptarget (@justinpark) +- [#28750](https://github.com/apache/superset/pull/28750) fix: do not close database modal on mask click (@eschutho) +- [#28745](https://github.com/apache/superset/pull/28745) fix(reports): Update the element class to wait for when taking a screenshot (@Vitor-Avila) +- [#28749](https://github.com/apache/superset/pull/28749) fix(sqllab): Sort db selector options by the API order (@justinpark) +- [#28653](https://github.com/apache/superset/pull/28653) fix: Handling of column types for Presto, Trino, et al. (@john-bodley) +- [#28422](https://github.com/apache/superset/pull/28422) fix: Update migration logic in #27119 (@john-bodley) +- [#28349](https://github.com/apache/superset/pull/28349) fix: Add back description column to saved queries #12431 (@imancrsrk) +- [#28512](https://github.com/apache/superset/pull/28512) fix: improve df to records performance (@dpgaspar) +- [#28613](https://github.com/apache/superset/pull/28613) fix: revert fix(presto preview): re-enable schema previsualization for Trino/Presto table/schemas" (@john-bodley) +- [#28567](https://github.com/apache/superset/pull/28567) fix: Revert "fix: don't strip SQL comments in Explore (#28363)" (@michael-s-molina) +- [#28555](https://github.com/apache/superset/pull/28555) fix(explore): hide a control wrapped with StashFormDataContainer correctly (@justinpark) +- [#28507](https://github.com/apache/superset/pull/28507) fix(dashboard): invalid drop item on a tab (@justinpark) +- [#28432](https://github.com/apache/superset/pull/28432) fix: Time shifts calculation for ECharts plugins (@michael-s-molina) +- [#26782](https://github.com/apache/superset/pull/26782) fix(presto preview): re-enable schema previsualization for Trino/Presto table/schemas (@brouberol) +- [#28409](https://github.com/apache/superset/pull/28409) fix(ar-modal): updateNotificationSettings not updating state (@fisjac) +- [#28395](https://github.com/apache/superset/pull/28395) fix(dashboard): Change class name on last Droppable in a column (@rtexelm) +- [#28396](https://github.com/apache/superset/pull/28396) fix: type annotation breaking on py3.9 (@dpgaspar) +- [#28368](https://github.com/apache/superset/pull/28368) fix: Contribution percentages for ECharts plugins (@michael-s-molina) +- [#28386](https://github.com/apache/superset/pull/28386) fix: Scroll to top when selecting a global dashboard tab (@michael-s-molina) +- [#28312](https://github.com/apache/superset/pull/28312) fix(explore): hide advanced analytics for non temporal xaxis (@justinpark) +- [#28363](https://github.com/apache/superset/pull/28363) fix: don't strip SQL comments in Explore (@mistercrunch) +- [#28341](https://github.com/apache/superset/pull/28341) fix: Remedy logic for UpdateDatasetCommand uniqueness check (@john-bodley) +- [#28334](https://github.com/apache/superset/pull/28334) fix: Small tweaks for Line and Area chart migrations (ECharts) (@michael-s-molina) +- [#28266](https://github.com/apache/superset/pull/28266) fix: use pessimistic json encoder in SQL Lab (@mistercrunch) +- [#28113](https://github.com/apache/superset/pull/28113) fix: Rename legacy line and area charts (@john-bodley) +- [#28279](https://github.com/apache/superset/pull/28279) fix(sql_parse): Ignore USE SQL keyword when determining SELECT statement (@john-bodley) +- [#28322](https://github.com/apache/superset/pull/28322) fix(sql_parse): Add Apache Spark to SQLGlot dialect mapping (@john-bodley) + +**Others** + +- [#29360](https://github.com/apache/superset/pull/29360) chore: Rename Totals to Summary in table chart (@michael-s-molina) +- [#29249](https://github.com/apache/superset/pull/29249) test(Explorer): Fix minor errors in ExploreViewContainer syntax, add tests (@rtexelm) +- [#28876](https://github.com/apache/superset/pull/28876) chore(sqllab): Add logging for actions (@justinpark) diff --git a/RELEASING/release-notes-4-0/README.md b/RELEASING/release-notes-4-0/README.md new file mode 100644 index 000000000000..d5b5b609e16f --- /dev/null +++ b/RELEASING/release-notes-4-0/README.md @@ -0,0 +1,151 @@ + + +# Release Notes for Superset 4.0.0 + +4.0.0 brings a plethora of exciting changes to Superset. We have introduced several breaking changes to improve the overall architecture and scalability of our codebase. These changes may require some code updates, but they are designed to enhance performance and maintainability in the long run. We have also upgraded various dependencies to their latest versions and deprecated certain features that are no longer aligned with our long-term roadmap. We encourage all developers to carefully review the `CHANGELOG.md` and `UPDATING.md` files and update their code accordingly. While our main focus was on code cleanup, this release also contains exciting new features and marks a significant milestone for the project. + +Here are some of the highlights of this release. + +### Alerts and Reports modal redesign + +The Alerts and Reports modal has been [redesigned](https://github.com/apache/superset/discussions/25729) to improve the user experience and make it more intuitive. The new design has the following goals: + +- Declutter the interface by providing a cleaner, more organized layout +- Create a linear setup process with the necessary options in a step-by-step manner to make alert/report setup more intuitive +- Prepare the interface for additional features that will be introduced in future releases, like the ability to pre-filter a dashboard being sent. + +
+ Image + Image + Image +
+ +### Tags + +Tags are available using the `TAGGING_SYSTEM` feature flag. They address many of the [requests made by the community](https://github.com/apache/superset/discussions/19194) and aim to make it easier to organize and curate charts, dashboards, and saved queries, allowing for effortless data discovery and collaboration within an organization. Users can create flexible and customizable tags for each piece of content, enabling different ways of organizing assets. Programmatic access to tag-related operations are supported via the RESTful API. + +
+ Image + Image +
+ +### New CHANGELOG format + +We changed the structure of the `CHANGELOG.md` file in [#26800](https://github.com/apache/superset/pull/26800) to better organize the contents of each release and also to deal with GitHub size limitations when displaying the file. Now every release will have its own file at `CHANGELOG/.md`. The main `CHANGELOG.md` file is now an index with links to all releases. + +### Improved drag and drop experience when editing a dashboard + +When a component was being dragged towards the edge of the tab container or the row/column containers, multiple drop indicators were often displayed. This created confusion about the exact insertion point of the element. To fix this, we built in [#26699](https://github.com/apache/superset/pull/26699) and [#26313](https://github.com/apache/superset/pull/26313) a distinct, non-conflicting area for the drop zone, which is highlighted during the dragging process to clearly indicate where the element will be placed. We also improved the forbidden drop zones to prevent users from dropping elements in invalid locations. + +
+ Image + Image +
+ +### Improved drag and drop experience when editing a chart + +Now, during dragging, all droppable zones are highlighted, with distinct colors indicating available and unavailable drop locations. This enhancement clarifies potential drop points and helps avoid inadvertent placements in invalid areas. The update also aligns the drag-over feedback with the dashboard's drag-and-drop modifications, ensuring a uniform and enhanced user experience. + +![Drag and drop](media/explore-dnd.png) + +### Dropping support for 3.0.X versions + +In accordance with our [release process](https://github.com/apache/superset/wiki/Release-Process), we are dropping support for the 3.0.X versions. As a result, we will no longer be providing bug fixes for these versions. We strongly recommend that all users upgrade to the latest version to take advantage of the newest features and bug fixes. Moving forward, the supported versions will be 3.1.X and 4.0.X. Bug fixes will continue to be backported to 3.1.X until the next minor release. For more information, please refer to our [release schedule](https://github.com/apache/superset/wiki/Release-Process#schedule). + +### Feature flag changes + +Following our 4.0 proposals, the following feature flags were removed, i.e., the feature was permanently enabled or removed. + +- `VERSIONED_EXPORT` +- `DASHBOARD_FILTERS_EXPERIMENTAL` +- `ENABLE_EXPLORE_JSON_CSRF_PROTECTION` +- `ENABLE_TEMPLATE_REMOVE_FILTERS` +- `REMOVE_SLICE_LEVEL_LABEL_COLORS` +- `CLIENT_CACHE` +- `DASHBOARD_CACHE` +- `DASHBOARD_NATIVE_FILTERS_SET` +- `ENABLE_EXPLORE_DRAG_AND_DROP` +- `DISABLE_DATASET_SOURCE_EDIT` +- `DASHBOARD_NATIVE_FILTERS` +- `GENERIC_CHART_AXES` + +The following feature flags were deprecated: + +- `DASHBOARD_CROSS_FILTERS` +- `ENABLE_JAVASCRIPT_CONTROLS` +- `KV_STORE` + +The following feature flags were enabled by default: + +- `DASHBOARD_VIRTUALIZATION` +- `DRILL_BY` + +### Removed features + +As part of the 4.0 approved initiatives, the following features were removed from Superset: + +- Filter Box: [#26328](https://github.com/apache/superset/pull/26328) removed the Filter Box code and its associated dependencies `react-select` and `array-move`. It also removed the `DeprecatedSelect` and `AsyncSelect` components that were exclusively used by filter boxes. Existing filter boxes will be automatically migrated to native dashboard filters. + +- Filter Sets: [#26369](https://github.com/apache/superset/pull/26369) removed the Filters Set feature including the deprecated `DASHBOARD_NATIVE_FILTERS_SET` feature flag and all related API endpoints. The feature is permanently removed as it was not being actively maintained, it was not widely used, and it was full of bugs. We also considered that if we were to provide a similar feature, it would be better to re-implement it from scratch given the amount of technical debt that the implementation had. + +- Profile: [#26462](https://github.com/apache/superset/pull/26462) removed the Profile feature given that it was not actively maintained nor widely used. + +- Redirect API: [#26377](https://github.com/apache/superset/pull/26377) removed the deprecated Redirect API that supported short URLs (`/r`) and the `url` metadata table used to store them that was used before the permalink feature. Users lost the ability to generate R links ~1.5 years ago which seems sufficient time to remove the API. + +### Business logic improvements + +As part of [[SIP-99] Proposal for correctly handling business logic](https://github.com/apache/superset/issues/25048) (specifically [SIP-99A](https://github.com/apache/superset/issues/25107) and [SIP-99B](https://github.com/apache/superset/issues/25108)), this release contains many improvements to the handling of business logic in Superset, specifically related to SQLAlchemy sessions and transactions. The goal of these efforts is to simplify the code, improve code quality, ensure a consistent "unit of work" approach, and provide clear guidance and examples of accepted code standards. These changes aim to improve developer experience by making the code simpler, improving testing, and ensuring a more streamlined and reliable system. We still have a long way to go to fully implement the SIP-99 proposal, but we are making progress and we are excited about the improvements that have been made so far. + +### All country maps are now managed via Jupyter Notebook + +In this release we made updates to the Jupyter Notebook to ensure reliable execution by removing deprecated methods, adding new countries, including missing maps, and fixing filename inconsistencies. This will make it easier to add more countries, dynamically add them to the country map plugin, and update map regions periodically. You can check [#26300](https://github.com/apache/superset/pull/26300) for more details. + +### Sunburst chart migrated to ECharts + +The ECharts version of the Sunburst chart was introduced by [#22833](https://github.com/apache/superset/pull/22833) as part of our efforts to complete [SIP-50](https://github.com/apache/superset/issues/10418). In 4.0, legacy Sunburst charts are [automatically migrated](https://github.com/apache/superset/pull/26350) to ECharts and the legacy version was removed. + +![Sunburst](media/sunburst.png) + +### Some cool stats + +- ~15K lines of code were removed by PRs related to 4.0 proposals +- We reduced the number of NPM packages vulnerabilities by 72% + - 3.1: 90 vulnerabilities (42 moderate, 34 high, 14 critical) + - 4.0: 25 vulnerabilities (16 moderate, 8 high, 1 critical) +- 40+ dependency changes (upgrades, additions, and removals) + +### How to upgrade + +As with any Superset version upgrade, the process is simple in the broadest strokes, as outlined in the documentation. However, as with any upgrade, we expect to see numerous speed bumps along that path depending on your configuration, your infrastructure, your databases in use, and other customizations/configurations. To make a safe leap to this version, we'd suggest the following steps: + +- Back up your databases +- Carefully read `CHANGELOG.md` for all the incremental changes in this version (and any prior versions between your current installation and 4.0.0). +- Similarly, review `UPDATING.md` to keep an eye out for all changes that have been explicitly marked as breaking changes. +- Adjust your feature flags and configurations to meet your feature requirements and preferences. +- Execute the migrations +- If you have third-party apps interacting with Superset, check for relevant dependency updates or API endpoint changes that may affect compatibility. + +Your mileage may vary depending on: + +- How you install and deploy Superset (e.g. docker vs. pip vs. helm) +- How you’ve configured Superset +- What integrations, databases, etc. you're using + +Reach out in `#deploying-superset` on Slack in case you find any problems, and if you find a reproducible bug, please file a new issue on GitHub. diff --git a/RELEASING/release-notes-4-0/media/alert-modal-1.png b/RELEASING/release-notes-4-0/media/alert-modal-1.png new file mode 100644 index 000000000000..a6e56d964ee1 Binary files /dev/null and b/RELEASING/release-notes-4-0/media/alert-modal-1.png differ diff --git a/RELEASING/release-notes-4-0/media/alert-modal-2.png b/RELEASING/release-notes-4-0/media/alert-modal-2.png new file mode 100644 index 000000000000..4e5f6871aa88 Binary files /dev/null and b/RELEASING/release-notes-4-0/media/alert-modal-2.png differ diff --git a/RELEASING/release-notes-4-0/media/alert-modal-3.png b/RELEASING/release-notes-4-0/media/alert-modal-3.png new file mode 100644 index 000000000000..40fe83cf343b Binary files /dev/null and b/RELEASING/release-notes-4-0/media/alert-modal-3.png differ diff --git a/RELEASING/release-notes-4-0/media/dashboard-dnd-1.png b/RELEASING/release-notes-4-0/media/dashboard-dnd-1.png new file mode 100644 index 000000000000..6b5a6ab88297 Binary files /dev/null and b/RELEASING/release-notes-4-0/media/dashboard-dnd-1.png differ diff --git a/RELEASING/release-notes-4-0/media/dashboard-dnd-2.png b/RELEASING/release-notes-4-0/media/dashboard-dnd-2.png new file mode 100644 index 000000000000..5e3994a50625 Binary files /dev/null and b/RELEASING/release-notes-4-0/media/dashboard-dnd-2.png differ diff --git a/RELEASING/release-notes-4-0/media/explore-dnd.png b/RELEASING/release-notes-4-0/media/explore-dnd.png new file mode 100644 index 000000000000..0b4d15f5f016 Binary files /dev/null and b/RELEASING/release-notes-4-0/media/explore-dnd.png differ diff --git a/RELEASING/release-notes-4-0/media/sunburst.png b/RELEASING/release-notes-4-0/media/sunburst.png new file mode 100644 index 000000000000..bc8881565fe4 Binary files /dev/null and b/RELEASING/release-notes-4-0/media/sunburst.png differ diff --git a/RELEASING/release-notes-4-0/media/tags-1.png b/RELEASING/release-notes-4-0/media/tags-1.png new file mode 100644 index 000000000000..a6c42cf4215a Binary files /dev/null and b/RELEASING/release-notes-4-0/media/tags-1.png differ diff --git a/RELEASING/release-notes-4-0/media/tags-2.png b/RELEASING/release-notes-4-0/media/tags-2.png new file mode 100644 index 000000000000..a5ecf065df9a Binary files /dev/null and b/RELEASING/release-notes-4-0/media/tags-2.png differ diff --git a/RELEASING/validate_this_release.sh b/RELEASING/validate_this_release.sh index 6d2b6fca5f4d..98c502be2a24 100755 --- a/RELEASING/validate_this_release.sh +++ b/RELEASING/validate_this_release.sh @@ -39,7 +39,7 @@ PYTHON=$(get_python_command) PIP=$(get_pip_command) # Get the release directory's path. If you unzip an Apache release and just run the npm script to validate the release, this will be a file name like `apache-superset-x.x.xrcx-source.tar.gz` -RELEASE_DIR_NAME="../../$(basename "$(dirname "$(pwd)")").tar.gz" +RELEASE_ZIP_PATH="../../$(basename "$(dirname "$(pwd)")")-source.tar.gz" # Install dependencies from requirements.txt if the file exists if [ -f "path/to/requirements.txt" ]; then @@ -47,8 +47,5 @@ if [ -f "path/to/requirements.txt" ]; then $PYTHON -m $PIP install -r path/to/requirements.txt fi -# echo $PYTHON -# echo $RELEASE_DIR_NAME - # Run the Python script with the parent directory name as an argument -$PYTHON ../RELEASING/verify_release.py "$RELEASE_DIR_NAME" +$PYTHON ../RELEASING/verify_release.py "$RELEASE_ZIP_PATH" diff --git a/UPDATING.md b/UPDATING.md index 4dd68340bcdb..0260be7b4394 100644 --- a/UPDATING.md +++ b/UPDATING.md @@ -22,7 +22,7 @@ under the License. This file documents any backwards-incompatible changes in Superset and assists people when migrating to a new version. -## Next +## 4.0.0 - [27119](https://github.com/apache/superset/pull/27119): Updates various database columns to use the `MediumText` type, potentially requiring a table lock on MySQL dbs or taking some time to complete on large deployments. @@ -30,7 +30,7 @@ assists people when migrating to a new version. ### Breaking Changes -- [27130](https://github.com/apache/superset/pull/27130): Fixes the DELETE `/database/{id}/ssh_tunnel/`` endpoint to now correctly accept a database ID as a parameter, rather than an SSH tunnel ID. +- [27130](https://github.com/apache/superset/pull/27130): Fixes the DELETE `/database/{id}/ssh_tunnel/` endpoint to now correctly accept a database ID as a parameter, rather than an SSH tunnel ID. - [27117](https://github.com/apache/superset/pull/27117): Removes the following deprecated endpoints: `/superset/sqllab`, `/superset/sqllab/history`, `/sqllab/my_queries` use `/sqllab`, `/sqllab/history`, `/savedqueryview/list/?_flt_0_user={get_user_id()}` instead. - [26347](https://github.com/apache/superset/issues/26347): Removes the deprecated `VERSIONED_EXPORT` feature flag. The previous value of the feature flag was `True` and now the feature is permanently enabled. - [26328](https://github.com/apache/superset/issues/26328): Removes the deprecated Filter Box code and it's associated dependencies `react-select` and `array-move`. It also removes the `DeprecatedSelect` and `AsyncSelect` components that were exclusively used by filter boxes. Existing filter boxes will be automatically migrated to native filters. @@ -53,7 +53,7 @@ assists people when migrating to a new version. ### Potential Downtime -- [26416](https://github.com/apache/superset/pull/26416): adds 2 database indexes to report_execution_log and 1 to report_recipient to improve performance, this may cause downtime on large deployments. +- [26416](https://github.com/apache/superset/pull/26416): Adds two database indexes to the `report_execution_log` table and one database index to the `report_recipient` to improve performance. Scheduled downtime may be required for large deployments. ## 3.1.0 diff --git a/docs/static/resources/openapi.json b/docs/static/resources/openapi.json index 13d5c8c21cea..ad4c8ebbcf00 100644 --- a/docs/static/resources/openapi.json +++ b/docs/static/resources/openapi.json @@ -7583,7 +7583,7 @@ "Europe/Istanbul", "Europe/Jersey", "Europe/Kaliningrad", - "Europe/Kiev", + "Europe/Kyiv", "Europe/Kirov", "Europe/Lisbon", "Europe/Ljubljana", @@ -8297,7 +8297,7 @@ "Europe/Istanbul", "Europe/Jersey", "Europe/Kaliningrad", - "Europe/Kiev", + "Europe/Kyiv", "Europe/Kirov", "Europe/Lisbon", "Europe/Ljubljana", diff --git a/null_byte.csv b/null_byte.csv new file mode 100644 index 000000000000..55132aaa6398 Binary files /dev/null and b/null_byte.csv differ diff --git a/requirements/base.in b/requirements/base.in index 9d8313823765..d0f710884059 100644 --- a/requirements/base.in +++ b/requirements/base.in @@ -17,3 +17,6 @@ # under the License. # -e file:. +urllib3>=1.26.18 +werkzeug>=3.0.1 +numexpr>=2.9.0 diff --git a/requirements/base.txt b/requirements/base.txt index f32e509db1da..980dd9f115e4 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -1,4 +1,4 @@ -# SHA1:a9dde048f1ee1f00586264d726d0e89f16e56183 +# SHA1:85649679306ea016e401f37adfbad832028d2e5f # # This file is autogenerated by pip-compile-multi # To update, run: @@ -78,7 +78,7 @@ cron-descriptor==1.2.24 # via apache-superset croniter==1.0.15 # via apache-superset -cryptography==42.0.2 +cryptography==42.0.4 # via # apache-superset # paramiko @@ -104,7 +104,7 @@ flask==2.2.5 # flask-session # flask-sqlalchemy # flask-wtf -flask-appbuilder==4.4.0 +flask-appbuilder==4.4.1 # via apache-superset flask-babel==1.0.0 # via flask-appbuilder @@ -143,7 +143,9 @@ geopy==2.2.0 google-auth==2.27.0 # via shillelagh greenlet==3.0.3 - # via shillelagh + # via + # shillelagh + # sqlalchemy gunicorn==21.2.0 # via apache-superset hashids==1.3.1 @@ -208,8 +210,10 @@ nh3==0.2.11 # via apache-superset numba==0.57.1 # via pandas -numexpr==2.8.4 - # via pandas +numexpr==2.9.0 + # via + # -r requirements/base.in + # pandas numpy==1.23.5 # via # apache-superset @@ -232,7 +236,9 @@ packaging==23.1 pandas[performance]==2.0.3 # via apache-superset paramiko==3.4.0 - # via sshtunnel + # via + # apache-superset + # sshtunnel parsedatetime==2.6 # via apache-superset pgsanity==0.2.9 @@ -336,7 +342,7 @@ sqlalchemy-utils==0.38.3 # via # apache-superset # flask-appbuilder -sqlglot==20.8.0 +sqlglot==23.0.2 # via apache-superset sqlparse==0.4.4 # via apache-superset @@ -358,6 +364,7 @@ url-normalize==1.4.3 # via requests-cache urllib3==1.26.18 # via + # -r requirements/base.in # requests # requests-cache # selenium @@ -370,6 +377,7 @@ wcwidth==0.2.5 # via prompt-toolkit werkzeug==3.0.1 # via + # -r requirements/base.in # flask # flask-appbuilder # flask-jwt-extended diff --git a/requirements/development.txt b/requirements/development.txt index f481ab8b8047..ef4ff840650f 100644 --- a/requirements/development.txt +++ b/requirements/development.txt @@ -82,10 +82,6 @@ ptyprocess==0.7.0 # via pexpect pure-eval==0.2.2 # via stack-data -pure-sasl==0.6.2 - # via - # pyhive - # thrift-sasl pydruid==0.6.5 # via apache-superset pyhive[hive_pure_sasl]==0.7.0 @@ -109,12 +105,7 @@ tableschema==1.20.2 tabulator==1.53.5 # via tableschema thrift==0.16.0 - # via - # apache-superset - # pyhive - # thrift-sasl -thrift-sasl==0.4.3 - # via pyhive + # via apache-superset tomlkit==0.11.8 # via pylint traitlets==5.9.0 diff --git a/requirements/docker.in b/requirements/docker.in index b7e893f3e9c3..5b65cc68718f 100644 --- a/requirements/docker.in +++ b/requirements/docker.in @@ -15,6 +15,5 @@ # limitations under the License. # -r base.in --e .[postgres] -gevent +-e .[postgres,gevent] greenlet>=2.0.2 diff --git a/requirements/docker.txt b/requirements/docker.txt index be65b7d36b1b..27c135e04c75 100644 --- a/requirements/docker.txt +++ b/requirements/docker.txt @@ -1,4 +1,4 @@ -# SHA1:439e3ee196ce81f342c935117ba5e0eeee8c385b +# SHA1:f00a57c70a52607d638c19f64f426f887382927e # # This file is autogenerated by pip-compile-multi # To update, run: @@ -11,7 +11,7 @@ # -r requirements/base.in # -r requirements/docker.in gevent==23.9.1 - # via -r requirements/docker.in + # via apache-superset psycopg2-binary==2.9.6 # via apache-superset zope-event==4.5.0 diff --git a/requirements/testing.in b/requirements/testing.in index 5b498bb09005..3245886ec854 100644 --- a/requirements/testing.in +++ b/requirements/testing.in @@ -20,6 +20,7 @@ docker flask-testing freezegun +grpcio>=1.55.3 openapi-spec-validator parameterized pyfakefs diff --git a/requirements/testing.txt b/requirements/testing.txt index 1e7215e00098..7abc48412fc7 100644 --- a/requirements/testing.txt +++ b/requirements/testing.txt @@ -1,4 +1,4 @@ -# SHA1:95300275481abb1413eb98a5c79fb7cf96814cdd +# SHA1:a37a1037f359c1101162ef43288178fbf00c487d # # This file is autogenerated by pip-compile-multi # To update, run: @@ -62,6 +62,7 @@ googleapis-common-protos==1.59.0 # grpcio-status grpcio==1.60.1 # via + # -r requirements/testing.in # google-api-core # google-cloud-bigquery # grpcio-status @@ -132,7 +133,7 @@ tqdm==4.65.0 # via # cmdstanpy # prophet -trino==0.324.0 +trino==0.328.0 # via apache-superset tzlocal==4.3 # via trino diff --git a/setup.py b/setup.py index 3917d8a3f2f3..1ecf23f284f4 100644 --- a/setup.py +++ b/setup.py @@ -80,11 +80,10 @@ def get_git_sha() -> str: "colorama", "croniter>=0.3.28", "cron-descriptor", - # snowflake-connector-python as of 3.7.0 doesn't support >=42.* therefore lowering the min to 41.0.2 - "cryptography>=41.0.2, <43.0.0", + "cryptography>=42.0.4, <43.0.0", "deprecation>=2.1.0, <2.2.0", "flask>=2.2.5, <3.0.0", - "flask-appbuilder>=4.4.0, <5.0.0", + "flask-appbuilder>=4.4.1, <5.0.0", "flask-caching>=2.1.0, <3", "flask-compress>=1.13, <2.0", "flask-talisman>=1.0.0, <2.0", @@ -108,6 +107,7 @@ def get_git_sha() -> str: "packaging", "pandas[performance]>=2.0.3, <2.1", "parsedatetime", + "paramiko>=3.4.0", "pgsanity", "polyline>=2.0.0, <3.0", "pyparsing>=3.0.6, <4", @@ -126,7 +126,7 @@ def get_git_sha() -> str: "slack_sdk>=3.19.0, <4", "sqlalchemy>=1.4, <2", "sqlalchemy-utils>=0.38.3, <0.39", - "sqlglot>=20,<21", + "sqlglot>=23.0.2,<24", "sqlparse>=0.4.4, <0.5", "tabulate>=0.8.9, <0.9", "typing-extensions>=4, <5", @@ -189,7 +189,7 @@ def get_git_sha() -> str: "playwright": ["playwright>=1.37.0, <2"], "postgres": ["psycopg2-binary==2.9.6"], "presto": ["pyhive[presto]>=0.6.5"], - "trino": ["trino>=0.324.0"], + "trino": ["trino>=0.328.0"], "prophet": ["prophet>=1.1.5, <2"], "redshift": ["sqlalchemy-redshift>=0.8.1, <0.9"], "rockset": ["rockset-sqlalchemy>=0.0.1, <1"], diff --git a/superset-frontend/cypress-base/cypress.config.ts b/superset-frontend/cypress-base/cypress.config.ts index 96b74938ee98..6cf0c6ba86ff 100644 --- a/superset-frontend/cypress-base/cypress.config.ts +++ b/superset-frontend/cypress-base/cypress.config.ts @@ -29,7 +29,7 @@ export default defineConfig({ videoUploadOnPasses: false, viewportWidth: 1280, viewportHeight: 1024, - projectId: 'ukwxzo', + projectId: 'ud5x2f', retries: { runMode: 2, openMode: 0, diff --git a/superset-frontend/cypress-base/cypress/e2e/chart_list/filter.test.ts b/superset-frontend/cypress-base/cypress/e2e/chart_list/filter.test.ts index 00b09e2fb8d0..9ee39862aeeb 100644 --- a/superset-frontend/cypress-base/cypress/e2e/chart_list/filter.test.ts +++ b/superset-frontend/cypress-base/cypress/e2e/chart_list/filter.test.ts @@ -41,7 +41,7 @@ describe('Charts filters', () => { }); it('should allow filtering by "Type" correctly', () => { - setFilter('Type', 'Area Chart (legacy)'); + setFilter('Type', 'Area Chart'); setFilter('Type', 'Bubble Chart'); }); diff --git a/superset-frontend/cypress-base/cypress/e2e/chart_list/list.test.ts b/superset-frontend/cypress-base/cypress/e2e/chart_list/list.test.ts index 44f348edc50f..4e1dc17410ba 100644 --- a/superset-frontend/cypress-base/cypress/e2e/chart_list/list.test.ts +++ b/superset-frontend/cypress-base/cypress/e2e/chart_list/list.test.ts @@ -54,7 +54,7 @@ function visitChartList() { } describe('Charts list', () => { - describe.skip('Cross-referenced dashboards', () => { + describe('Cross-referenced dashboards', () => { beforeEach(() => { cy.createSampleDashboards([0, 1, 2, 3]); cy.createSampleCharts([0]); @@ -112,9 +112,10 @@ describe('Charts list', () => { cy.getBySel('sort-header').eq(1).contains('Name'); cy.getBySel('sort-header').eq(2).contains('Type'); cy.getBySel('sort-header').eq(3).contains('Dataset'); - cy.getBySel('sort-header').eq(4).contains('Owners'); - cy.getBySel('sort-header').eq(5).contains('Last modified'); - cy.getBySel('sort-header').eq(6).contains('Actions'); + cy.getBySel('sort-header').eq(4).contains('On dashboards'); + cy.getBySel('sort-header').eq(5).contains('Owners'); + cy.getBySel('sort-header').eq(6).contains('Last modified'); + cy.getBySel('sort-header').eq(7).contains('Actions'); }); it('should sort correctly in list mode', () => { @@ -173,6 +174,13 @@ describe('Charts list', () => { orderAlphabetical(); cy.getBySel('styled-card').first().contains('% Rural'); }); + + it('should preserve other filters when sorting', () => { + cy.getBySel('styled-card').should('have.length', 25); + setFilter('Type', 'Big Number'); + setFilter('Sort', 'Least recently modified'); + cy.getBySel('styled-card').should('have.length', 3); + }); }); describe('common actions', () => { diff --git a/superset-frontend/cypress-base/cypress/e2e/dashboard_list/list.test.ts b/superset-frontend/cypress-base/cypress/e2e/dashboard_list/list.test.ts index 7dfb7cd673d7..917ca104550d 100644 --- a/superset-frontend/cypress-base/cypress/e2e/dashboard_list/list.test.ts +++ b/superset-frontend/cypress-base/cypress/e2e/dashboard_list/list.test.ts @@ -117,6 +117,13 @@ describe('Dashboards list', () => { orderAlphabetical(); cy.getBySel('styled-card').first().contains('Supported Charts Dashboard'); }); + + it('should preserve other filters when sorting', () => { + cy.getBySel('styled-card').should('have.length', 5); + setFilter('Status', 'Published'); + setFilter('Sort', 'Least recently modified'); + cy.getBySel('styled-card').should('have.length', 3); + }); }); describe('common actions', () => { diff --git a/superset-frontend/cypress-base/cypress/e2e/explore/chart.test.js b/superset-frontend/cypress-base/cypress/e2e/explore/chart.test.js index d198672ef3ed..14c386e0ea62 100644 --- a/superset-frontend/cypress-base/cypress/e2e/explore/chart.test.js +++ b/superset-frontend/cypress-base/cypress/e2e/explore/chart.test.js @@ -31,13 +31,13 @@ const SAMPLE_DASHBOARDS_INDEXES = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; function openDashboardsAddedTo() { cy.getBySel('actions-trigger').click(); cy.get('.ant-dropdown-menu-submenu-title') - .contains('Dashboards added to') + .contains('On dashboards') .trigger('mouseover', { force: true }); } function closeDashboardsAddedTo() { cy.get('.ant-dropdown-menu-submenu-title') - .contains('Dashboards added to') + .contains('On dashboards') .trigger('mouseout', { force: true }); cy.getBySel('actions-trigger').click(); } diff --git a/superset-frontend/cypress-base/cypress/e2e/explore/control.test.ts b/superset-frontend/cypress-base/cypress/e2e/explore/control.test.ts index faee1f6f4ee4..4d641a6b5a24 100644 --- a/superset-frontend/cypress-base/cypress/e2e/explore/control.test.ts +++ b/superset-frontend/cypress-base/cypress/e2e/explore/control.test.ts @@ -210,7 +210,7 @@ describe('Time range filter', () => { .click() .then(() => { cy.get('.ant-radio-group').children().its('length').should('eq', 5); - cy.get('.ant-radio-checked + span').contains('last year'); + cy.get('.ant-radio-checked + span').contains('Last year'); cy.get('[data-test=cancel-button]').click(); }); }); diff --git a/superset-frontend/package-lock.json b/superset-frontend/package-lock.json index 644963e3eb6a..9148048f4707 100644 --- a/superset-frontend/package-lock.json +++ b/superset-frontend/package-lock.json @@ -200,6 +200,7 @@ "@types/react-table": "^7.7.19", "@types/react-transition-group": "^4.4.10", "@types/react-ultimate-pagination": "^1.2.0", + "@types/react-virtualized-auto-sizer": "^1.0.4", "@types/react-window": "^1.8.5", "@types/redux-localstorage": "^1.0.8", "@types/redux-mock-store": "^1.0.2", @@ -215,7 +216,6 @@ "babel-plugin-dynamic-import-node": "^2.3.3", "babel-plugin-jsx-remove-data-test-id": "^2.1.3", "babel-plugin-lodash": "^3.3.4", - "chromatic": "^6.7.4", "copy-webpack-plugin": "^12.0.2", "cross-env": "^5.2.1", "css-loader": "^6.8.1", @@ -313,46 +313,6 @@ "node": ">=0.10.0" } }, - "node_modules/@actions/core": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.9.1.tgz", - "integrity": "sha512-5ad+U2YGrmmiw6du20AQW5XuWo7UKN2052FjSV7MX+Wfjf8sCqcsZe62NfgHys4QI4/Y+vQvLKYL8jWtA1ZBTA==", - "dev": true, - "dependencies": { - "@actions/http-client": "^2.0.1", - "uuid": "^8.3.2" - } - }, - "node_modules/@actions/core/node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "dev": true, - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/@actions/github": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/@actions/github/-/github-5.0.3.tgz", - "integrity": "sha512-myjA/pdLQfhUGLtRZC/J4L1RXOG4o6aYdiEq+zr5wVVKljzbFld+xv10k1FX6IkIJtNxbAq44BdwSNpQ015P0A==", - "dev": true, - "dependencies": { - "@actions/http-client": "^2.0.1", - "@octokit/core": "^3.6.0", - "@octokit/plugin-paginate-rest": "^2.17.0", - "@octokit/plugin-rest-endpoint-methods": "^5.13.0" - } - }, - "node_modules/@actions/http-client": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.0.1.tgz", - "integrity": "sha512-PIXiMVtz6VvyaRsGY268qvj57hXQEpsYogYOu2nrQhlf+XCGmZstmuZBbAybUl1nQGnvS1k1eEsQ69ZoD7xlSw==", - "dev": true, - "dependencies": { - "tunnel": "^0.0.6" - } - }, "node_modules/@ampproject/remapping": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", @@ -3809,155 +3769,6 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, - "node_modules/@chromaui/localtunnel": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@chromaui/localtunnel/-/localtunnel-2.0.4.tgz", - "integrity": "sha512-92AI1cIzI8XmKnsuKhIOysdZ+ecc8iCqRnoUnZ4/6Nr9PEd/CStJtK6OBAanw1QYPiojzegfeAW3uBSVFxLm4g==", - "dev": true, - "dependencies": { - "axios": "0.21.4", - "debug": "4.3.1", - "openurl": "1.1.1", - "yargs": "16.2.0" - }, - "bin": { - "lt": "bin/lt.js" - }, - "engines": { - "node": ">=8.3.0" - } - }, - "node_modules/@chromaui/localtunnel/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/@chromaui/localtunnel/node_modules/cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", - "dev": true, - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^7.0.0" - } - }, - "node_modules/@chromaui/localtunnel/node_modules/debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", - "dev": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/@chromaui/localtunnel/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/@chromaui/localtunnel/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "node_modules/@chromaui/localtunnel/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@chromaui/localtunnel/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@chromaui/localtunnel/node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/@chromaui/localtunnel/node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/@chromaui/localtunnel/node_modules/yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", - "dev": true, - "dependencies": { - "cliui": "^7.0.2", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@chromaui/localtunnel/node_modules/yargs-parser": { - "version": "20.2.9", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", - "dev": true, - "engines": { - "node": ">=10" - } - }, "node_modules/@cnakazawa/watch": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/@cnakazawa/watch/-/watch-1.0.4.tgz", @@ -13590,26 +13401,6 @@ "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", "dev": true }, - "node_modules/@samverschueren/stream-to-observable": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@samverschueren/stream-to-observable/-/stream-to-observable-0.3.1.tgz", - "integrity": "sha512-c/qwwcHyafOQuVQJj0IlBjf5yYgBI7YPJ77k4fOJYesb41jio65eaJODRUmfYKhTOFBrIZ66kgvGPlNbjuoRdQ==", - "dev": true, - "dependencies": { - "any-observable": "^0.3.0" - }, - "engines": { - "node": ">=6" - }, - "peerDependenciesMeta": { - "rxjs": { - "optional": true - }, - "zen-observable": { - "optional": true - } - } - }, "node_modules/@scarf/scarf": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.3.0.tgz", @@ -22926,8 +22717,7 @@ "node_modules/@types/raf": { "version": "3.4.2", "resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.2.tgz", - "integrity": "sha512-sM4HyDVlDFl4goOXPF+g9nNHJFZQGot+HgySjM4cRjqXzjdatcEvYrtG4Ia8XumR9T6k8G2tW9B7hnUj51Uf0A==", - "optional": true + "integrity": "sha512-sM4HyDVlDFl4goOXPF+g9nNHJFZQGot+HgySjM4cRjqXzjdatcEvYrtG4Ia8XumR9T6k8G2tW9B7hnUj51Uf0A==" }, "node_modules/@types/range-parser": { "version": "1.2.4", @@ -23064,6 +22854,15 @@ "@types/react": "*" } }, + "node_modules/@types/react-virtualized-auto-sizer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@types/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.4.tgz", + "integrity": "sha512-nhYwlFiYa8M3S+O2T9QO/e1FQUYMr/wJENUdf/O0dhRi1RS/93rjrYQFYdbUqtdFySuhrtnEDX29P6eKOttY+A==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/react-window": { "version": "1.8.5", "resolved": "https://registry.npmjs.org/@types/react-window/-/react-window-1.8.5.tgz", @@ -23290,12 +23089,6 @@ "source-map": "^0.6.0" } }, - "node_modules/@types/webpack-env": { - "version": "1.18.0", - "resolved": "https://registry.npmjs.org/@types/webpack-env/-/webpack-env-1.18.0.tgz", - "integrity": "sha512-56/MAlX5WMsPVbOg7tAxnYvNYMMWr/QJiIp6BxVSW3JJXUVzzOn64qW8TzQyMSqSUFM2+PVI4aUHcHOzIz/1tg==", - "dev": true - }, "node_modules/@types/webpack-sources": { "version": "0.1.5", "resolved": "https://registry.npmjs.org/@types/webpack-sources/-/webpack-sources-0.1.5.tgz", @@ -25760,15 +25553,6 @@ "react-dom": "*" } }, - "node_modules/any-observable": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/any-observable/-/any-observable-0.3.0.tgz", - "integrity": "sha512-/FQM1EDkTsf63Ub2C6O7GuYFDsSXUwsaZDurV0np41ocwq0jthUAYCmhBX9f+KwlaCgIuWyr/4WlUQUBfKfZog==", - "dev": true, - "engines": { - "node": ">=6" - } - }, "node_modules/anymatch": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", @@ -26257,15 +26041,6 @@ "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==", "dev": true }, - "node_modules/async-retry": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz", - "integrity": "sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==", - "dev": true, - "dependencies": { - "retry": "0.13.1" - } - }, "node_modules/async-validator": { "version": "3.5.1", "resolved": "https://registry.npmjs.org/async-validator/-/async-validator-3.5.1.tgz", @@ -26384,15 +26159,6 @@ "node": ">=4" } }, - "node_modules/axios": { - "version": "0.21.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", - "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", - "dev": true, - "dependencies": { - "follow-redirects": "^1.14.0" - } - }, "node_modules/axobject-query": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz", @@ -27060,7 +26826,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", - "optional": true, "engines": { "node": ">= 0.6.0" } @@ -27818,7 +27583,6 @@ "version": "3.0.10", "resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.10.tgz", "integrity": "sha512-qwR2FRNO9NlzTeKIPIKpnTY6fqwuYSequ8Ru8c0YkYU7U0oW+hLUvWadLvAu1Rl72OMNiFhoLu4f8eUjQ7l/+Q==", - "optional": true, "dependencies": { "@babel/runtime": "^7.12.5", "@types/raf": "^3.4.0", @@ -28136,21 +27900,6 @@ "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", "dev": true }, - "node_modules/chromatic": { - "version": "6.7.4", - "resolved": "https://registry.npmjs.org/chromatic/-/chromatic-6.7.4.tgz", - "integrity": "sha512-QW4i8RQsON0JVnFnRf+8y70aIJptvC0Oi/26YJ669Dl03WmJRpobNO5qWFPTiv3KFKMc1Qf6/qFsRVZCtn+bfA==", - "dev": true, - "dependencies": { - "@discoveryjs/json-ext": "^0.5.7", - "@types/webpack-env": "^1.17.0" - }, - "bin": { - "chroma": "bin/main.cjs", - "chromatic": "bin/main.cjs", - "chromatic-cli": "bin/main.cjs" - } - }, "node_modules/chrome-trace-event": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.2.tgz", @@ -30768,7 +30517,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz", "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==", - "optional": true, "dependencies": { "utrie": "^1.0.2" } @@ -31899,13 +31647,6 @@ "url": "https://opencollective.com/date-fns" } }, - "node_modules/date-format": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/date-format/-/date-format-0.0.2.tgz", - "integrity": "sha512-M4obuJx8jU5T91lcbwi0+QPNVaWOY1DQYz5xUuKYWO93osVzB2ZPqyDUc5T+mDjbA1X8VOb4JDZ+8r2MrSOp7Q==", - "deprecated": "0.x is no longer supported. Please upgrade to 4.x or higher.", - "dev": true - }, "node_modules/dateformat": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-3.0.3.tgz", @@ -33047,15 +32788,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/dotenv": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.6.0.tgz", - "integrity": "sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==", - "dev": true, - "engines": { - "node": ">=10" - } - }, "node_modules/dotenv-expand": { "version": "10.0.0", "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-10.0.0.tgz", @@ -33191,15 +32923,6 @@ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.640.tgz", "integrity": "sha512-z/6oZ/Muqk4BaE7P69bXhUhpJbUM9ZJeka43ZwxsDshKtePns4mhBlh8bU5+yrnOnz3fhG82XLzGUXazOmsWnA==" }, - "node_modules/elegant-spinner": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/elegant-spinner/-/elegant-spinner-1.0.1.tgz", - "integrity": "sha512-B+ZM+RXvRqQaAmkMlO/oSe5nMUOaUnyfGYCEHoR8wrXsZR2mA0XVibsxV1bvTwxdRWah1PkQqso2EzhILGHtEQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/email-addresses": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/email-addresses/-/email-addresses-3.1.0.tgz", @@ -33410,147 +33133,6 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, - "node_modules/env-ci": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/env-ci/-/env-ci-5.5.0.tgz", - "integrity": "sha512-o0JdWIbOLP+WJKIUt36hz1ImQQFuN92nhsfTkHHap+J8CiI8WgGpH/a9jEGHh4/TU5BUUGjlnKXNoDb57+ne+A==", - "dev": true, - "dependencies": { - "execa": "^5.0.0", - "fromentries": "^1.3.2", - "java-properties": "^1.0.0" - }, - "engines": { - "node": ">=10.17" - } - }, - "node_modules/env-ci/node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dev": true, - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/env-ci/node_modules/execa": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", - "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", - "dev": true, - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.0", - "human-signals": "^2.1.0", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.1", - "onetime": "^5.1.2", - "signal-exit": "^3.0.3", - "strip-final-newline": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/env-ci/node_modules/get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/env-ci/node_modules/human-signals": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", - "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", - "dev": true, - "engines": { - "node": ">=10.17.0" - } - }, - "node_modules/env-ci/node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/env-ci/node_modules/npm-run-path": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", - "dev": true, - "dependencies": { - "path-key": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/env-ci/node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/env-ci/node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/env-ci/node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/env-ci/node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/env-paths": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", @@ -35453,15 +35035,6 @@ "node": ">= 8" } }, - "node_modules/esm": { - "version": "3.2.25", - "resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz", - "integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==", - "dev": true, - "engines": { - "node": ">=6" - } - }, "node_modules/espree": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", @@ -36094,18 +35667,6 @@ "node >=0.6.0" ] }, - "node_modules/fake-tag": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fake-tag/-/fake-tag-2.0.0.tgz", - "integrity": "sha512-QDz+8qiNQ9AfBZ31EXlID5JIbUkU3e1nXDWk4tidFzd2gy8XJaEUW1HCuDY6DUy6t2Y0nvhD6PsUc+2WYy5w0w==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/jaydenseric" - } - }, "node_modules/falafel": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/falafel/-/falafel-2.2.4.tgz", @@ -37487,7 +37048,8 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "peer": true }, "node_modules/fs-constants": { "version": "1.0.0", @@ -39511,7 +39073,6 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz", "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==", - "optional": true, "dependencies": { "css-line-break": "^2.1.0", "text-segmentation": "^1.0.3" @@ -40900,18 +40461,6 @@ "node": ">=8" } }, - "node_modules/is-observable": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-observable/-/is-observable-1.1.0.tgz", - "integrity": "sha512-NqCa4Sa2d+u7BWc6CukaObG3Fh+CU9bvixbpcXYhy2VvYS7vVGIdAgnIS5Ks3A/cqk4rebLJ9s8zBstT2aKnIA==", - "dev": true, - "dependencies": { - "symbol-observable": "^1.1.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/is-path-cwd": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz", @@ -40957,12 +40506,6 @@ "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", "dev": true }, - "node_modules/is-promise": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", - "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==", - "dev": true - }, "node_modules/is-reference": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.2.tgz", @@ -41548,15 +41091,6 @@ "node": ">=10" } }, - "node_modules/java-properties": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/java-properties/-/java-properties-1.0.2.tgz", - "integrity": "sha512-qjdpeo2yKlYTH7nFdK0vbZWuTCesk4o63v5iVOlhMQPfuIZQfW/HI35SjfhA+4qpg36rnFSvUK5b1m+ckIblQQ==", - "dev": true, - "engines": { - "node": ">= 0.6.0" - } - }, "node_modules/jed": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/jed/-/jed-1.1.1.tgz", @@ -47143,13 +46677,13 @@ "@babel/runtime": "^7.14.0", "atob": "^2.1.2", "btoa": "^1.2.1", - "fflate": "^0.4.8" + "canvg": "^3.0.6", + "fflate": "^0.4.8", + "html2canvas": "^1.0.0-rc.5" }, "optionalDependencies": { - "canvg": "^3.0.6", "core-js": "^3.6.0", - "dompurify": "^2.2.0", - "html2canvas": "^1.0.0-rc.5" + "dompurify": "^2.2.0" } }, "node_modules/jsprim": { @@ -47180,33 +46714,6 @@ "node": ">=4.0" } }, - "node_modules/junit-report-builder": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/junit-report-builder/-/junit-report-builder-2.1.0.tgz", - "integrity": "sha512-Ioj5I4w18ZcHFaaisqCKdh1z+ipzN7sA2JB+h+WOlGcOMWm0FFN1dfxkgc2I4EXfhSP/mOfM3W43uFzEdz4sTw==", - "dev": true, - "dependencies": { - "date-format": "0.0.2", - "lodash": "^4.17.15", - "make-dir": "^1.3.0", - "xmlbuilder": "^10.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/junit-report-builder/node_modules/make-dir": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", - "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", - "dev": true, - "dependencies": { - "pify": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/just-diff": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/just-diff/-/just-diff-3.1.1.tgz", @@ -50257,415 +49764,6 @@ "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz", "integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=" }, - "node_modules/listr": { - "version": "0.14.3", - "resolved": "https://registry.npmjs.org/listr/-/listr-0.14.3.tgz", - "integrity": "sha512-RmAl7su35BFd/xoMamRjpIE4j3v+L28o8CT5YhAXQJm1fD+1l9ngXY8JAQRJ+tFK2i5njvi0iRUKV09vPwA0iA==", - "dev": true, - "dependencies": { - "@samverschueren/stream-to-observable": "^0.3.0", - "is-observable": "^1.1.0", - "is-promise": "^2.1.0", - "is-stream": "^1.1.0", - "listr-silent-renderer": "^1.1.1", - "listr-update-renderer": "^0.5.0", - "listr-verbose-renderer": "^0.5.0", - "p-map": "^2.0.0", - "rxjs": "^6.3.3" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/listr-silent-renderer": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/listr-silent-renderer/-/listr-silent-renderer-1.1.1.tgz", - "integrity": "sha512-L26cIFm7/oZeSNVhWB6faeorXhMg4HNlb/dS/7jHhr708jxlXrtrBWo4YUxZQkc6dGoxEAe6J/D3juTRBUzjtA==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/listr-update-renderer": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/listr-update-renderer/-/listr-update-renderer-0.5.0.tgz", - "integrity": "sha512-tKRsZpKz8GSGqoI/+caPmfrypiaq+OQCbd+CovEC24uk1h952lVj5sC7SqyFUm+OaJ5HN/a1YLt5cit2FMNsFA==", - "dev": true, - "dependencies": { - "chalk": "^1.1.3", - "cli-truncate": "^0.2.1", - "elegant-spinner": "^1.0.1", - "figures": "^1.7.0", - "indent-string": "^3.0.0", - "log-symbols": "^1.0.2", - "log-update": "^2.3.0", - "strip-ansi": "^3.0.1" - }, - "engines": { - "node": ">=6" - }, - "peerDependencies": { - "listr": "^0.14.2" - } - }, - "node_modules/listr-update-renderer/node_modules/ansi-escapes": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", - "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/listr-update-renderer/node_modules/ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/listr-update-renderer/node_modules/chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==", - "dev": true, - "dependencies": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/listr-update-renderer/node_modules/cli-cursor": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", - "integrity": "sha512-8lgKz8LmCRYZZQDpRyT2m5rKJ08TnU4tR9FFFW2rxpxR1FzWi4PQ/NfyODchAatHaUgnSPVcx/R5w6NuTBzFiw==", - "dev": true, - "dependencies": { - "restore-cursor": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/listr-update-renderer/node_modules/cli-truncate": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-0.2.1.tgz", - "integrity": "sha512-f4r4yJnbT++qUPI9NR4XLDLq41gQ+uqnPItWG0F5ZkehuNiTTa3EY0S4AqTSUOeJ7/zU41oWPQSNkW5BqPL9bg==", - "dev": true, - "dependencies": { - "slice-ansi": "0.0.4", - "string-width": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/listr-update-renderer/node_modules/figures": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz", - "integrity": "sha512-UxKlfCRuCBxSXU4C6t9scbDyWZ4VlaFFdojKtzJuSkuOBQ5CNFum+zZXFwHjo+CxBC1t6zlYPgHIgFjL8ggoEQ==", - "dev": true, - "dependencies": { - "escape-string-regexp": "^1.0.5", - "object-assign": "^4.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/listr-update-renderer/node_modules/indent-string": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-3.2.0.tgz", - "integrity": "sha512-BYqTHXTGUIvg7t1r4sJNKcbDZkL92nkXA8YtRpbjFHRHGDL/NtUeiBJMeE60kIFN/Mg8ESaWQvftaYMGJzQZCQ==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/listr-update-renderer/node_modules/is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha512-1pqUqRjkhPJ9miNq9SwMfdvi6lBJcd6eFxvfaivQhaH3SgisfiuudvFntdKOmxuee/77l+FPjKrQjWvmPjWrRw==", - "dev": true, - "dependencies": { - "number-is-nan": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/listr-update-renderer/node_modules/log-symbols": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-1.0.2.tgz", - "integrity": "sha512-mmPrW0Fh2fxOzdBbFv4g1m6pR72haFLPJ2G5SJEELf1y+iaQrDG6cWCPjy54RHYbZAt7X+ls690Kw62AdWXBzQ==", - "dev": true, - "dependencies": { - "chalk": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/listr-update-renderer/node_modules/log-update": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/log-update/-/log-update-2.3.0.tgz", - "integrity": "sha512-vlP11XfFGyeNQlmEn9tJ66rEW1coA/79m5z6BCkudjbAGE83uhAcGYrBFwfs3AdLiLzGRusRPAbSPK9xZteCmg==", - "dev": true, - "dependencies": { - "ansi-escapes": "^3.0.0", - "cli-cursor": "^2.0.0", - "wrap-ansi": "^3.0.1" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/listr-update-renderer/node_modules/mimic-fn": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", - "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/listr-update-renderer/node_modules/onetime": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", - "integrity": "sha512-oyyPpiMaKARvvcgip+JV+7zci5L8D1W9RZIz2l1o08AM3pfspitVWnPt3mzHcBPp12oYMTy0pqrFs/C+m3EwsQ==", - "dev": true, - "dependencies": { - "mimic-fn": "^1.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/listr-update-renderer/node_modules/restore-cursor": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", - "integrity": "sha512-6IzJLuGi4+R14vwagDHX+JrXmPVtPpn4mffDJ1UdR7/Edm87fl6yi8mMBIVvFtJaNTUvjughmW4hwLhRG7gC1Q==", - "dev": true, - "dependencies": { - "onetime": "^2.0.0", - "signal-exit": "^3.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/listr-update-renderer/node_modules/slice-ansi": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-0.0.4.tgz", - "integrity": "sha512-up04hB2hR92PgjpyU3y/eg91yIBILyjVY26NvvciY3EVVPjybkMszMpXQ9QAkcS3I5rtJBDLoTxxg+qvW8c7rw==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/listr-update-renderer/node_modules/string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha512-0XsVpQLnVCXHJfyEs8tC0zpTVIr5PKKsQtkT29IwupnPTjtPmQ3xT/4yCREF9hYkV/3M3kzcUTSAZT6a6h81tw==", - "dev": true, - "dependencies": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/listr-update-renderer/node_modules/supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/listr-update-renderer/node_modules/wrap-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-3.0.1.tgz", - "integrity": "sha512-iXR3tDXpbnTpzjKSylUJRkLuOrEC7hwEB221cgn6wtF8wpmz28puFXAEfPT5zrjM3wahygB//VuWEr1vTkDcNQ==", - "dev": true, - "dependencies": { - "string-width": "^2.1.1", - "strip-ansi": "^4.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/listr-update-renderer/node_modules/wrap-ansi/node_modules/is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/listr-update-renderer/node_modules/wrap-ansi/node_modules/string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "dev": true, - "dependencies": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/listr-update-renderer/node_modules/wrap-ansi/node_modules/strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow==", - "dev": true, - "dependencies": { - "ansi-regex": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/listr-verbose-renderer": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/listr-verbose-renderer/-/listr-verbose-renderer-0.5.0.tgz", - "integrity": "sha512-04PDPqSlsqIOaaaGZ+41vq5FejI9auqTInicFRndCBgE3bXG8D6W1I+mWhk+1nqbHmyhla/6BUrd5OSiHwKRXw==", - "dev": true, - "dependencies": { - "chalk": "^2.4.1", - "cli-cursor": "^2.1.0", - "date-fns": "^1.27.2", - "figures": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/listr-verbose-renderer/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/listr-verbose-renderer/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/listr-verbose-renderer/node_modules/cli-cursor": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", - "integrity": "sha512-8lgKz8LmCRYZZQDpRyT2m5rKJ08TnU4tR9FFFW2rxpxR1FzWi4PQ/NfyODchAatHaUgnSPVcx/R5w6NuTBzFiw==", - "dev": true, - "dependencies": { - "restore-cursor": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/listr-verbose-renderer/node_modules/date-fns": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-1.30.1.tgz", - "integrity": "sha512-hBSVCvSmWC+QypYObzwGOd9wqdDpOt+0wl0KbU+R+uuZBS1jN8VsD1ss3irQDknRj5NvxiTF6oj/nDRnN/UQNw==", - "dev": true - }, - "node_modules/listr-verbose-renderer/node_modules/figures": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", - "integrity": "sha512-Oa2M9atig69ZkfwiApY8F2Yy+tzMbazyvqv21R0NsSC8floSOC09BbT1ITWAdoMGQvJ/aZnR1KMwdx9tvHnTNA==", - "dev": true, - "dependencies": { - "escape-string-regexp": "^1.0.5" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/listr-verbose-renderer/node_modules/mimic-fn": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", - "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/listr-verbose-renderer/node_modules/onetime": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", - "integrity": "sha512-oyyPpiMaKARvvcgip+JV+7zci5L8D1W9RZIz2l1o08AM3pfspitVWnPt3mzHcBPp12oYMTy0pqrFs/C+m3EwsQ==", - "dev": true, - "dependencies": { - "mimic-fn": "^1.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/listr-verbose-renderer/node_modules/restore-cursor": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", - "integrity": "sha512-6IzJLuGi4+R14vwagDHX+JrXmPVtPpn4mffDJ1UdR7/Edm87fl6yi8mMBIVvFtJaNTUvjughmW4hwLhRG7gC1Q==", - "dev": true, - "dependencies": { - "onetime": "^2.0.0", - "signal-exit": "^3.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/listr-verbose-renderer/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/listr/node_modules/p-map": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz", - "integrity": "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==", - "dev": true, - "engines": { - "node": ">=6" - } - }, "node_modules/listr2": { "version": "3.12.2", "resolved": "https://registry.npmjs.org/listr2/-/listr2-3.12.2.tgz", @@ -53726,24 +52824,6 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.1.0.tgz", "integrity": "sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==" }, - "node_modules/no-proxy": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/no-proxy/-/no-proxy-1.0.3.tgz", - "integrity": "sha512-JPr13PIb/cENY5+WjuxzhQH74guHYPpyfk+7f7lR7SIpDE1kH0BL9jO7yztANg3jFT8jf58UEimbCBflW5UiTw==", - "dev": true, - "dependencies": { - "wildcard": "^1.1.2" - }, - "optionalDependencies": { - "url-parse": "^1.2.0" - } - }, - "node_modules/no-proxy/node_modules/wildcard": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-1.1.2.tgz", - "integrity": "sha512-DXukZJxpHA8LuotRwL0pP1+rS6CS7FF2qStDDE1C7DDg2rLud2PXRMuEDYIPhgEezwnlHNL4c+N6MfMTjCGTng==", - "dev": true - }, "node_modules/node-abort-controller": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz", @@ -53756,12 +52836,6 @@ "integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==", "dev": true }, - "node_modules/node-ask": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/node-ask/-/node-ask-1.0.1.tgz", - "integrity": "sha512-+0eqgEdgPiixrNysGDTPo3T2qyEHGVgs4ONlc5tTfcluvC/Rgq1x2ELdANUMwhR2CYLwaQnMS32O/h7adasnFQ==", - "dev": true - }, "node_modules/node-cleanup": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/node-cleanup/-/node-cleanup-2.1.2.tgz", @@ -53967,20 +53041,6 @@ "integrity": "sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs=", "dev": true }, - "node_modules/node-loggly-bulk": { - "version": "2.2.5", - "resolved": "https://registry.npmjs.org/node-loggly-bulk/-/node-loggly-bulk-2.2.5.tgz", - "integrity": "sha512-N6RjZfjqwhAYwT9nM8PFKXpWfaGFaDHnzwj2JBgsNq04xsEZNGMlI+rds90p5/TTkYAS8Ya6tbJChXFRqTSmiA==", - "dev": true, - "dependencies": { - "json-stringify-safe": "5.0.x", - "moment": "^2.18.1", - "request": ">=2.76.0 <3.0.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, "node_modules/node-notifier": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-8.0.2.tgz", @@ -55826,12 +54886,6 @@ "opener": "bin/opener-bin.js" } }, - "node_modules/openurl": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/openurl/-/openurl-1.1.1.tgz", - "integrity": "sha512-d/gTkTb1i1GKz5k3XE3XFV/PxQ1k45zDqGP2OA7YhgsaLoqm6qRvARAZOFer1fcXritWlGBRCu/UgeS4HAnXAA==", - "dev": true - }, "node_modules/optimist": { "version": "0.3.7", "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.3.7.tgz", @@ -56944,15 +55998,6 @@ "node": ">=6" } }, - "node_modules/pluralize": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", - "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/pn": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/pn/-/pn-1.1.0.tgz", @@ -57798,16 +56843,6 @@ "node": ">=0.4.0" } }, - "node_modules/progress-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/progress-stream/-/progress-stream-2.0.0.tgz", - "integrity": "sha512-xJwOWR46jcXUq6EH9yYyqp+I52skPySOeHfkxOZ2IY1AiBi/sFJhbhAKHoV3OTw/omQ45KTio9215dRJ2Yxd3Q==", - "dev": true, - "dependencies": { - "speedometer": "~1.0.0", - "through2": "~2.0.3" - } - }, "node_modules/promise": { "version": "7.3.1", "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", @@ -61339,7 +60374,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz", "integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==", - "optional": true, "engines": { "node": ">= 0.8.15" } @@ -61426,6 +60460,7 @@ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", "dev": true, + "peer": true, "dependencies": { "tslib": "^1.9.0" }, @@ -62718,12 +61753,6 @@ "webpack": "^1 || ^2 || ^3 || ^4 || ^5" } }, - "node_modules/speedometer": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/speedometer/-/speedometer-1.0.0.tgz", - "integrity": "sha512-lgxErLl/7A5+vgIIXsh9MbeukOaCb2axgQ+bKCdIE+ibNT4XNYGNCR1qFEGq6F+YDASXK3Fh/c5FgtZchFolxw==", - "dev": true - }, "node_modules/split": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/split/-/split-1.0.1.tgz", @@ -62859,7 +61888,6 @@ "version": "2.6.0", "resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.6.0.tgz", "integrity": "sha512-8S1aIA+UoF6erJYnglGPug6MaHYGo1Ot7h5fuXx4fUPvcvQfcdw2o/ppCse63+eZf8PPidSu4v1JnmEVtEDnpg==", - "optional": true, "engines": { "node": ">=0.1.14" } @@ -63069,15 +62097,6 @@ "safe-buffer": "~5.1.0" } }, - "node_modules/string-argv": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.1.tgz", - "integrity": "sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg==", - "dev": true, - "engines": { - "node": ">=0.6.19" - } - }, "node_modules/string-convert": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/string-convert/-/string-convert-0.2.1.tgz", @@ -63577,7 +62596,6 @@ "version": "6.0.3", "resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz", "integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==", - "optional": true, "engines": { "node": ">=12.0.0" } @@ -64101,7 +63119,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz", "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==", - "optional": true, "dependencies": { "utrie": "^1.0.2" } @@ -64279,15 +63296,6 @@ "node": ">=8.17.0" } }, - "node_modules/tmp-promise": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/tmp-promise/-/tmp-promise-3.0.2.tgz", - "integrity": "sha512-OyCLAKU1HzBjL6Ev3gxUeraJNlbNingmi8IrHHEsYH8LTmEuhvYfqvhn2F/je+mjf4N58UmZ96OMEy1JanSCpA==", - "dev": true, - "dependencies": { - "tmp": "^0.2.0" - } - }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -64464,15 +63472,6 @@ "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.6.6.tgz", "integrity": "sha1-y99WD9e5r2MlAv7UD5GMFX6pcTc=" }, - "node_modules/tree-kill": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", - "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", - "dev": true, - "bin": { - "tree-kill": "cli.js" - } - }, "node_modules/treeverse": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/treeverse/-/treeverse-1.0.4.tgz", @@ -64782,15 +63781,6 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, - "node_modules/tunnel": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", - "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", - "dev": true, - "engines": { - "node": ">=0.6.11 <=0.7.0 || >=0.7.3" - } - }, "node_modules/tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", @@ -65601,7 +64591,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz", "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==", - "optional": true, "dependencies": { "base64-arraybuffer": "^1.0.2" } @@ -67600,15 +66589,6 @@ "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==", "dev": true }, - "node_modules/xmlbuilder": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-10.1.1.tgz", - "integrity": "sha512-OyzrcFLL/nb6fMGHbiRDuPup9ljBycsdCypwuyg5AAHvyWzGfChJpCXMG88AGTIMFhGZ9RccFN1e6lhg3hkwKg==", - "dev": true, - "engines": { - "node": ">=4.0" - } - }, "node_modules/xmlchars": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", @@ -67730,105 +66710,6 @@ "node": ">=10" } }, - "node_modules/yarn-or-npm": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/yarn-or-npm/-/yarn-or-npm-3.0.1.tgz", - "integrity": "sha512-fTiQP6WbDAh5QZAVdbMQkecZoahnbOjClTQhzv74WX5h2Uaidj1isf9FDes11TKtsZ0/ZVfZsqZ+O3x6aLERHQ==", - "dev": true, - "dependencies": { - "cross-spawn": "^6.0.5", - "pkg-dir": "^4.2.0" - }, - "bin": { - "yarn-or-npm": "bin/index.js", - "yon": "bin/index.js" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/yarn-or-npm/node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/yarn-or-npm/node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/yarn-or-npm/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/yarn-or-npm/node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/yarn-or-npm/node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/yarn-or-npm/node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/yarn-or-npm/node_modules/pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "dev": true, - "dependencies": { - "find-up": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/yauzl": { "version": "2.10.0", "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", @@ -69294,7 +68175,6 @@ "@babel/preset-typescript": "^7.23.3", "@storybook/react-webpack5": "^7.6.13", "babel-loader": "^8.1.0", - "chromatic": "^5.4.0", "fork-ts-checker-webpack-plugin": "^5.0.7", "ts-loader": "^7.0.4", "typescript": "^4.5.4" @@ -69346,15 +68226,6 @@ "react": "^16.3.1" } }, - "packages/superset-ui-demo/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "packages/superset-ui-demo/node_modules/ansi-styles": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", @@ -69379,84 +68250,6 @@ "node": ">=8" } }, - "packages/superset-ui-demo/node_modules/chromatic": { - "version": "5.10.2", - "resolved": "https://registry.npmjs.org/chromatic/-/chromatic-5.10.2.tgz", - "integrity": "sha512-JHFtZ16VanQX0X9qjacIJOrH9rVUJACilPs8dBwwQgJTZzgCZAdwgmE+WwLcxe/LuK7vM56BDTHbxC+XcnTsjw==", - "dev": true, - "dependencies": { - "@actions/core": "^1.5.0", - "@actions/github": "^5.0.0", - "@babel/preset-typescript": "^7.15.0", - "@babel/runtime": "^7.15.3", - "@chromaui/localtunnel": "^2.0.3", - "async-retry": "^1.3.3", - "chalk": "^4.1.2", - "cross-spawn": "^7.0.2", - "debug": "^4.3.2", - "dotenv": "^8.2.0", - "env-ci": "^5.0.2", - "esm": "^3.2.25", - "execa": "^5.0.0", - "fake-tag": "^2.0.0", - "fs-extra": "^10.0.0", - "https-proxy-agent": "^5.0.0", - "jsonfile": "^6.0.1", - "junit-report-builder": "2.1.0", - "listr": "0.14.3", - "meow": "^8.0.0", - "no-proxy": "^1.0.3", - "node-ask": "^1.0.1", - "node-fetch": "2.6.0", - "node-loggly-bulk": "^2.2.4", - "p-limit": "3.1.0", - "picomatch": "2.2.2", - "pkg-up": "^3.1.0", - "pluralize": "^8.0.0", - "progress-stream": "^2.0.0", - "semver": "^7.3.5", - "slash": "^3.0.0", - "string-argv": "^0.3.1", - "strip-ansi": "6.0.0", - "tmp-promise": "3.0.2", - "tree-kill": "^1.2.2", - "ts-dedent": "^1.0.0", - "util-deprecate": "^1.0.2", - "uuid": "^8.3.2", - "yarn-or-npm": "^3.0.1" - }, - "bin": { - "chroma": "bin/register.js", - "chromatic": "bin/register.js", - "chromatic-cli": "bin/register.js" - } - }, - "packages/superset-ui-demo/node_modules/chromatic/node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "dev": true, - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "packages/superset-ui-demo/node_modules/chromatic/node_modules/picomatch": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", - "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==", - "dev": true, - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "packages/superset-ui-demo/node_modules/core-js": { "version": "3.8.3", "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.8.3.tgz", @@ -69483,20 +68276,6 @@ "node": ">=8" } }, - "packages/superset-ui-demo/node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dev": true, - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, "packages/superset-ui-demo/node_modules/d3-array": { "version": "2.12.1", "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz", @@ -69528,23 +68307,6 @@ "d3-array": "2" } }, - "packages/superset-ui-demo/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, "packages/superset-ui-demo/node_modules/deepmerge": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz", @@ -69554,29 +68316,6 @@ "node": ">=0.10.0" } }, - "packages/superset-ui-demo/node_modules/execa": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", - "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", - "dev": true, - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.0", - "human-signals": "^2.1.0", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.1", - "onetime": "^5.1.2", - "signal-exit": "^3.0.3", - "strip-final-newline": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, "packages/superset-ui-demo/node_modules/fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -69589,18 +68328,6 @@ "node": ">=8" } }, - "packages/superset-ui-demo/node_modules/find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "dev": true, - "dependencies": { - "locate-path": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, "packages/superset-ui-demo/node_modules/fork-ts-checker-webpack-plugin": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-5.2.1.tgz", @@ -69639,27 +68366,6 @@ "node": ">=10" } }, - "packages/superset-ui-demo/node_modules/get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "packages/superset-ui-demo/node_modules/human-signals": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", - "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", - "dev": true, - "engines": { - "node": ">=10.17.0" - } - }, "packages/superset-ui-demo/node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -69669,36 +68375,11 @@ "node": ">=0.12.0" } }, - "packages/superset-ui-demo/node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "packages/superset-ui-demo/node_modules/jquery": { "version": "3.7.1", "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz", "integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==" }, - "packages/superset-ui-demo/node_modules/locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", - "dev": true, - "dependencies": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, "packages/superset-ui-demo/node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -69724,12 +68405,6 @@ "node": ">=8.6" } }, - "packages/superset-ui-demo/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, "packages/superset-ui-demo/node_modules/mustache": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz", @@ -69739,78 +68414,6 @@ "mustache": "bin/mustache" } }, - "packages/superset-ui-demo/node_modules/node-fetch": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz", - "integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==", - "dev": true, - "engines": { - "node": "4.x || >=6.0.0" - } - }, - "packages/superset-ui-demo/node_modules/npm-run-path": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", - "dev": true, - "dependencies": { - "path-key": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "packages/superset-ui-demo/node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "packages/superset-ui-demo/node_modules/p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "dev": true, - "dependencies": { - "p-limit": "^2.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "packages/superset-ui-demo/node_modules/p-locate/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "packages/superset-ui-demo/node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, "packages/superset-ui-demo/node_modules/parse-json": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", @@ -69829,15 +68432,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "packages/superset-ui-demo/node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "packages/superset-ui-demo/node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -69847,18 +68441,6 @@ "node": ">=8" } }, - "packages/superset-ui-demo/node_modules/pkg-up": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-3.1.0.tgz", - "integrity": "sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==", - "dev": true, - "dependencies": { - "find-up": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, "packages/superset-ui-demo/node_modules/schema-utils": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.0.tgz", @@ -69892,48 +68474,6 @@ "node": ">=10" } }, - "packages/superset-ui-demo/node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "packages/superset-ui-demo/node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "packages/superset-ui-demo/node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "packages/superset-ui-demo/node_modules/strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", - "dev": true, - "dependencies": { - "ansi-regex": "^5.0.0" - }, - "engines": { - "node": ">=8" - } - }, "packages/superset-ui-demo/node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -69958,15 +68498,6 @@ "node": ">=8.0" } }, - "packages/superset-ui-demo/node_modules/ts-dedent": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-1.2.0.tgz", - "integrity": "sha512-6zSJp23uQI+Txyz5LlXMXAHpUhY4Hi0oluXny0OgIR7g/Cromq4vDBnhtbBdyIV34g0pgwxUvnvg+jLJe4c1NA==", - "dev": true, - "engines": { - "node": ">=6.10" - } - }, "packages/superset-ui-demo/node_modules/ts-loader": { "version": "7.0.5", "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-7.0.5.tgz", @@ -70009,30 +68540,6 @@ "semver": "bin/semver.js" } }, - "packages/superset-ui-demo/node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "dev": true, - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "packages/superset-ui-demo/node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, "packages/superset-ui-demo/node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", @@ -72513,45 +71020,6 @@ "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", "dev": true }, - "@actions/core": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.9.1.tgz", - "integrity": "sha512-5ad+U2YGrmmiw6du20AQW5XuWo7UKN2052FjSV7MX+Wfjf8sCqcsZe62NfgHys4QI4/Y+vQvLKYL8jWtA1ZBTA==", - "dev": true, - "requires": { - "@actions/http-client": "^2.0.1", - "uuid": "^8.3.2" - }, - "dependencies": { - "uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "dev": true - } - } - }, - "@actions/github": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/@actions/github/-/github-5.0.3.tgz", - "integrity": "sha512-myjA/pdLQfhUGLtRZC/J4L1RXOG4o6aYdiEq+zr5wVVKljzbFld+xv10k1FX6IkIJtNxbAq44BdwSNpQ015P0A==", - "dev": true, - "requires": { - "@actions/http-client": "^2.0.1", - "@octokit/core": "^3.6.0", - "@octokit/plugin-paginate-rest": "^2.17.0", - "@octokit/plugin-rest-endpoint-methods": "^5.13.0" - } - }, - "@actions/http-client": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.0.1.tgz", - "integrity": "sha512-PIXiMVtz6VvyaRsGY268qvj57hXQEpsYogYOu2nrQhlf+XCGmZstmuZBbAybUl1nQGnvS1k1eEsQ69ZoD7xlSw==", - "dev": true, - "requires": { - "tunnel": "^0.0.6" - } - }, "@ampproject/remapping": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", @@ -75017,116 +73485,6 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, - "@chromaui/localtunnel": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@chromaui/localtunnel/-/localtunnel-2.0.4.tgz", - "integrity": "sha512-92AI1cIzI8XmKnsuKhIOysdZ+ecc8iCqRnoUnZ4/6Nr9PEd/CStJtK6OBAanw1QYPiojzegfeAW3uBSVFxLm4g==", - "dev": true, - "requires": { - "axios": "0.21.4", - "debug": "4.3.1", - "openurl": "1.1.1", - "yargs": "16.2.0" - }, - "dependencies": { - "ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true - }, - "cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", - "dev": true, - "requires": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^7.0.0" - } - }, - "debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - } - }, - "strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.1" - } - }, - "wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "requires": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - } - }, - "y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true - }, - "yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", - "dev": true, - "requires": { - "cliui": "^7.0.2", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" - } - }, - "yargs-parser": { - "version": "20.2.9", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", - "dev": true - } - } - }, "@cnakazawa/watch": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/@cnakazawa/watch/-/watch-1.0.4.tgz", @@ -82475,15 +80833,6 @@ } } }, - "@samverschueren/stream-to-observable": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@samverschueren/stream-to-observable/-/stream-to-observable-0.3.1.tgz", - "integrity": "sha512-c/qwwcHyafOQuVQJj0IlBjf5yYgBI7YPJ77k4fOJYesb41jio65eaJODRUmfYKhTOFBrIZ66kgvGPlNbjuoRdQ==", - "dev": true, - "requires": { - "any-observable": "^0.3.0" - } - }, "@scarf/scarf": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.3.0.tgz", @@ -87844,7 +86193,6 @@ "antd": "4.10.3", "babel-loader": "^8.1.0", "bootstrap": "^3.4.1", - "chromatic": "^5.4.0", "core-js": "3.8.3", "fork-ts-checker-webpack-plugin": "^5.0.7", "gh-pages": "^3.0.0", @@ -87876,12 +86224,6 @@ "reactable-arc": "^0.15.0" } }, - "ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true - }, "ansi-styles": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", @@ -87900,72 +86242,6 @@ "fill-range": "^7.0.1" } }, - "chromatic": { - "version": "5.10.2", - "resolved": "https://registry.npmjs.org/chromatic/-/chromatic-5.10.2.tgz", - "integrity": "sha512-JHFtZ16VanQX0X9qjacIJOrH9rVUJACilPs8dBwwQgJTZzgCZAdwgmE+WwLcxe/LuK7vM56BDTHbxC+XcnTsjw==", - "dev": true, - "requires": { - "@actions/core": "^1.5.0", - "@actions/github": "^5.0.0", - "@babel/preset-typescript": "^7.15.0", - "@babel/runtime": "^7.15.3", - "@chromaui/localtunnel": "^2.0.3", - "async-retry": "^1.3.3", - "chalk": "^4.1.2", - "cross-spawn": "^7.0.2", - "debug": "^4.3.2", - "dotenv": "^8.2.0", - "env-ci": "^5.0.2", - "esm": "^3.2.25", - "execa": "^5.0.0", - "fake-tag": "^2.0.0", - "fs-extra": "^10.0.0", - "https-proxy-agent": "^5.0.0", - "jsonfile": "^6.0.1", - "junit-report-builder": "2.1.0", - "listr": "0.14.3", - "meow": "^8.0.0", - "no-proxy": "^1.0.3", - "node-ask": "^1.0.1", - "node-fetch": "2.6.0", - "node-loggly-bulk": "^2.2.4", - "p-limit": "3.1.0", - "picomatch": "2.2.2", - "pkg-up": "^3.1.0", - "pluralize": "^8.0.0", - "progress-stream": "^2.0.0", - "semver": "^7.3.5", - "slash": "^3.0.0", - "string-argv": "^0.3.1", - "strip-ansi": "6.0.0", - "tmp-promise": "3.0.2", - "tree-kill": "^1.2.2", - "ts-dedent": "^1.0.0", - "util-deprecate": "^1.0.2", - "uuid": "^8.3.2", - "yarn-or-npm": "^3.0.1" - }, - "dependencies": { - "fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "dev": true, - "requires": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - } - }, - "picomatch": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", - "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==", - "dev": true - } - } - }, "core-js": { "version": "3.8.3", "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.8.3.tgz", @@ -87984,17 +86260,6 @@ "yaml": "^1.7.2" } }, - "cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dev": true, - "requires": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - } - }, "d3-array": { "version": "2.12.1", "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz", @@ -88026,38 +86291,12 @@ "d3-array": "2" } }, - "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, "deepmerge": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz", "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==", "dev": true }, - "execa": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", - "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", - "dev": true, - "requires": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.0", - "human-signals": "^2.1.0", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.1", - "onetime": "^5.1.2", - "signal-exit": "^3.0.3", - "strip-final-newline": "^2.0.0" - } - }, "fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -88067,15 +86306,6 @@ "to-regex-range": "^5.0.1" } }, - "find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "dev": true, - "requires": { - "locate-path": "^3.0.0" - } - }, "fork-ts-checker-webpack-plugin": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-5.2.1.tgz", @@ -88107,45 +86337,17 @@ "universalify": "^2.0.0" } }, - "get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "dev": true - }, - "human-signals": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", - "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", - "dev": true - }, "is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true }, - "is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "dev": true - }, "jquery": { "version": "3.7.1", "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz", "integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==" }, - "locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", - "dev": true, - "requires": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - } - }, "lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -88165,68 +86367,12 @@ "picomatch": "^2.2.3" } }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, "mustache": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz", "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==", "peer": true }, - "node-fetch": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz", - "integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==", - "dev": true - }, - "npm-run-path": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", - "dev": true, - "requires": { - "path-key": "^3.0.0" - } - }, - "p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "requires": { - "yocto-queue": "^0.1.0" - } - }, - "p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "dev": true, - "requires": { - "p-limit": "^2.0.0" - }, - "dependencies": { - "p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "requires": { - "p-try": "^2.0.0" - } - } - } - }, - "p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true - }, "parse-json": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", @@ -88239,27 +86385,12 @@ "lines-and-columns": "^1.1.6" } }, - "path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true - }, "path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", "dev": true }, - "pkg-up": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-3.1.0.tgz", - "integrity": "sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==", - "dev": true, - "requires": { - "find-up": "^3.0.0" - } - }, "schema-utils": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.0.tgz", @@ -88280,36 +86411,6 @@ "lru-cache": "^6.0.0" } }, - "shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "requires": { - "shebang-regex": "^3.0.0" - } - }, - "shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true - }, - "slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true - }, - "strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.0" - } - }, "supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -88328,12 +86429,6 @@ "is-number": "^7.0.0" } }, - "ts-dedent": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-1.2.0.tgz", - "integrity": "sha512-6zSJp23uQI+Txyz5LlXMXAHpUhY4Hi0oluXny0OgIR7g/Cromq4vDBnhtbBdyIV34g0pgwxUvnvg+jLJe4c1NA==", - "dev": true - }, "ts-loader": { "version": "7.0.5", "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-7.0.5.tgz", @@ -88366,21 +86461,6 @@ } } }, - "uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "dev": true - }, - "which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - }, "yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", @@ -91393,8 +89473,7 @@ "@types/raf": { "version": "3.4.2", "resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.2.tgz", - "integrity": "sha512-sM4HyDVlDFl4goOXPF+g9nNHJFZQGot+HgySjM4cRjqXzjdatcEvYrtG4Ia8XumR9T6k8G2tW9B7hnUj51Uf0A==", - "optional": true + "integrity": "sha512-sM4HyDVlDFl4goOXPF+g9nNHJFZQGot+HgySjM4cRjqXzjdatcEvYrtG4Ia8XumR9T6k8G2tW9B7hnUj51Uf0A==" }, "@types/range-parser": { "version": "1.2.4", @@ -91531,6 +89610,15 @@ "@types/react": "*" } }, + "@types/react-virtualized-auto-sizer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@types/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.4.tgz", + "integrity": "sha512-nhYwlFiYa8M3S+O2T9QO/e1FQUYMr/wJENUdf/O0dhRi1RS/93rjrYQFYdbUqtdFySuhrtnEDX29P6eKOttY+A==", + "dev": true, + "requires": { + "@types/react": "*" + } + }, "@types/react-window": { "version": "1.8.5", "resolved": "https://registry.npmjs.org/@types/react-window/-/react-window-1.8.5.tgz", @@ -91779,12 +89867,6 @@ } } }, - "@types/webpack-env": { - "version": "1.18.0", - "resolved": "https://registry.npmjs.org/@types/webpack-env/-/webpack-env-1.18.0.tgz", - "integrity": "sha512-56/MAlX5WMsPVbOg7tAxnYvNYMMWr/QJiIp6BxVSW3JJXUVzzOn64qW8TzQyMSqSUFM2+PVI4aUHcHOzIz/1tg==", - "dev": true - }, "@types/webpack-sources": { "version": "0.1.5", "resolved": "https://registry.npmjs.org/@types/webpack-sources/-/webpack-sources-0.1.5.tgz", @@ -93679,12 +91761,6 @@ } } }, - "any-observable": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/any-observable/-/any-observable-0.3.0.tgz", - "integrity": "sha512-/FQM1EDkTsf63Ub2C6O7GuYFDsSXUwsaZDurV0np41ocwq0jthUAYCmhBX9f+KwlaCgIuWyr/4WlUQUBfKfZog==", - "dev": true - }, "anymatch": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", @@ -94066,15 +92142,6 @@ "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==", "dev": true }, - "async-retry": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz", - "integrity": "sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==", - "dev": true, - "requires": { - "retry": "0.13.1" - } - }, "async-validator": { "version": "3.5.1", "resolved": "https://registry.npmjs.org/async-validator/-/async-validator-3.5.1.tgz", @@ -94163,15 +92230,6 @@ "integrity": "sha512-gd1kmb21kwNuWr6BQz8fv6GNECPBnUasepcoLbekws23NVBLODdsClRZ+bQ8+9Uomf3Sm3+Vwn0oYG9NvwnJCw==", "dev": true }, - "axios": { - "version": "0.21.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", - "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", - "dev": true, - "requires": { - "follow-redirects": "^1.14.0" - } - }, "axobject-query": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz", @@ -94703,8 +92761,7 @@ "base64-arraybuffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", - "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", - "optional": true + "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==" }, "base64-js": { "version": "1.5.1", @@ -95280,7 +93337,6 @@ "version": "3.0.10", "resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.10.tgz", "integrity": "sha512-qwR2FRNO9NlzTeKIPIKpnTY6fqwuYSequ8Ru8c0YkYU7U0oW+hLUvWadLvAu1Rl72OMNiFhoLu4f8eUjQ7l/+Q==", - "optional": true, "requires": { "@babel/runtime": "^7.12.5", "@types/raf": "^3.4.0", @@ -95520,16 +93576,6 @@ "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", "dev": true }, - "chromatic": { - "version": "6.7.4", - "resolved": "https://registry.npmjs.org/chromatic/-/chromatic-6.7.4.tgz", - "integrity": "sha512-QW4i8RQsON0JVnFnRf+8y70aIJptvC0Oi/26YJ669Dl03WmJRpobNO5qWFPTiv3KFKMc1Qf6/qFsRVZCtn+bfA==", - "dev": true, - "requires": { - "@discoveryjs/json-ext": "^0.5.7", - "@types/webpack-env": "^1.17.0" - } - }, "chrome-trace-event": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.2.tgz", @@ -97604,7 +95650,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz", "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==", - "optional": true, "requires": { "utrie": "^1.0.2" } @@ -98475,12 +96520,6 @@ "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.29.3.tgz", "integrity": "sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA==" }, - "date-format": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/date-format/-/date-format-0.0.2.tgz", - "integrity": "sha512-M4obuJx8jU5T91lcbwi0+QPNVaWOY1DQYz5xUuKYWO93osVzB2ZPqyDUc5T+mDjbA1X8VOb4JDZ+8r2MrSOp7Q==", - "dev": true - }, "dateformat": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-3.0.3.tgz", @@ -99355,12 +97394,6 @@ "is-obj": "^2.0.0" } }, - "dotenv": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.6.0.tgz", - "integrity": "sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==", - "dev": true - }, "dotenv-expand": { "version": "10.0.0", "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-10.0.0.tgz", @@ -99488,12 +97521,6 @@ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.640.tgz", "integrity": "sha512-z/6oZ/Muqk4BaE7P69bXhUhpJbUM9ZJeka43ZwxsDshKtePns4mhBlh8bU5+yrnOnz3fhG82XLzGUXazOmsWnA==" }, - "elegant-spinner": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/elegant-spinner/-/elegant-spinner-1.0.1.tgz", - "integrity": "sha512-B+ZM+RXvRqQaAmkMlO/oSe5nMUOaUnyfGYCEHoR8wrXsZR2mA0XVibsxV1bvTwxdRWah1PkQqso2EzhILGHtEQ==", - "dev": true - }, "email-addresses": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/email-addresses/-/email-addresses-3.1.0.tgz", @@ -99676,104 +97703,6 @@ "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==" }, - "env-ci": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/env-ci/-/env-ci-5.5.0.tgz", - "integrity": "sha512-o0JdWIbOLP+WJKIUt36hz1ImQQFuN92nhsfTkHHap+J8CiI8WgGpH/a9jEGHh4/TU5BUUGjlnKXNoDb57+ne+A==", - "dev": true, - "requires": { - "execa": "^5.0.0", - "fromentries": "^1.3.2", - "java-properties": "^1.0.0" - }, - "dependencies": { - "cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dev": true, - "requires": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - } - }, - "execa": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", - "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", - "dev": true, - "requires": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.0", - "human-signals": "^2.1.0", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.1", - "onetime": "^5.1.2", - "signal-exit": "^3.0.3", - "strip-final-newline": "^2.0.0" - } - }, - "get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "dev": true - }, - "human-signals": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", - "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", - "dev": true - }, - "is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "dev": true - }, - "npm-run-path": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", - "dev": true, - "requires": { - "path-key": "^3.0.0" - } - }, - "path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true - }, - "shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "requires": { - "shebang-regex": "^3.0.0" - } - }, - "shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true - }, - "which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - } - } - }, "env-paths": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", @@ -101074,12 +99003,6 @@ "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true }, - "esm": { - "version": "3.2.25", - "resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz", - "integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==", - "dev": true - }, "espree": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", @@ -101573,12 +99496,6 @@ "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", "dev": true }, - "fake-tag": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fake-tag/-/fake-tag-2.0.0.tgz", - "integrity": "sha512-QDz+8qiNQ9AfBZ31EXlID5JIbUkU3e1nXDWk4tidFzd2gy8XJaEUW1HCuDY6DUy6t2Y0nvhD6PsUc+2WYy5w0w==", - "dev": true - }, "falafel": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/falafel/-/falafel-2.2.4.tgz", @@ -102654,7 +100571,8 @@ "version": "1.3.2", "resolved": "https://registry.npmjs.org/fromentries/-/fromentries-1.3.2.tgz", "integrity": "sha512-cHEpEQHUg0f8XdtZCc2ZAhrHzKzT0MrFUTcvx+hfxYu7rGMDc5SKoXFh+n4YigxsHXRzc6OrCshdR1bWH6HHyg==", - "dev": true + "dev": true, + "peer": true }, "fs-constants": { "version": "1.0.0", @@ -104169,7 +102087,6 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz", "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==", - "optional": true, "requires": { "css-line-break": "^2.1.0", "text-segmentation": "^1.0.3" @@ -105189,15 +103106,6 @@ "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", "dev": true }, - "is-observable": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-observable/-/is-observable-1.1.0.tgz", - "integrity": "sha512-NqCa4Sa2d+u7BWc6CukaObG3Fh+CU9bvixbpcXYhy2VvYS7vVGIdAgnIS5Ks3A/cqk4rebLJ9s8zBstT2aKnIA==", - "dev": true, - "requires": { - "symbol-observable": "^1.1.0" - } - }, "is-path-cwd": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz", @@ -105231,12 +103139,6 @@ "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", "dev": true }, - "is-promise": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", - "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==", - "dev": true - }, "is-reference": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.2.tgz", @@ -105675,12 +103577,6 @@ "minimatch": "^3.0.4" } }, - "java-properties": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/java-properties/-/java-properties-1.0.2.tgz", - "integrity": "sha512-qjdpeo2yKlYTH7nFdK0vbZWuTCesk4o63v5iVOlhMQPfuIZQfW/HI35SjfhA+4qpg36rnFSvUK5b1m+ckIblQQ==", - "dev": true - }, "jed": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/jed/-/jed-1.1.1.tgz", @@ -110030,29 +107926,6 @@ "object.assign": "^4.1.2" } }, - "junit-report-builder": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/junit-report-builder/-/junit-report-builder-2.1.0.tgz", - "integrity": "sha512-Ioj5I4w18ZcHFaaisqCKdh1z+ipzN7sA2JB+h+WOlGcOMWm0FFN1dfxkgc2I4EXfhSP/mOfM3W43uFzEdz4sTw==", - "dev": true, - "requires": { - "date-format": "0.0.2", - "lodash": "^4.17.15", - "make-dir": "^1.3.0", - "xmlbuilder": "^10.0.0" - }, - "dependencies": { - "make-dir": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", - "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", - "dev": true, - "requires": { - "pify": "^3.0.0" - } - } - } - }, "just-diff": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/just-diff/-/just-diff-3.1.1.tgz", @@ -112423,321 +110296,6 @@ "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz", "integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=" }, - "listr": { - "version": "0.14.3", - "resolved": "https://registry.npmjs.org/listr/-/listr-0.14.3.tgz", - "integrity": "sha512-RmAl7su35BFd/xoMamRjpIE4j3v+L28o8CT5YhAXQJm1fD+1l9ngXY8JAQRJ+tFK2i5njvi0iRUKV09vPwA0iA==", - "dev": true, - "requires": { - "@samverschueren/stream-to-observable": "^0.3.0", - "is-observable": "^1.1.0", - "is-promise": "^2.1.0", - "is-stream": "^1.1.0", - "listr-silent-renderer": "^1.1.1", - "listr-update-renderer": "^0.5.0", - "listr-verbose-renderer": "^0.5.0", - "p-map": "^2.0.0", - "rxjs": "^6.3.3" - }, - "dependencies": { - "p-map": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz", - "integrity": "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==", - "dev": true - } - } - }, - "listr-silent-renderer": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/listr-silent-renderer/-/listr-silent-renderer-1.1.1.tgz", - "integrity": "sha512-L26cIFm7/oZeSNVhWB6faeorXhMg4HNlb/dS/7jHhr708jxlXrtrBWo4YUxZQkc6dGoxEAe6J/D3juTRBUzjtA==", - "dev": true - }, - "listr-update-renderer": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/listr-update-renderer/-/listr-update-renderer-0.5.0.tgz", - "integrity": "sha512-tKRsZpKz8GSGqoI/+caPmfrypiaq+OQCbd+CovEC24uk1h952lVj5sC7SqyFUm+OaJ5HN/a1YLt5cit2FMNsFA==", - "dev": true, - "requires": { - "chalk": "^1.1.3", - "cli-truncate": "^0.2.1", - "elegant-spinner": "^1.0.1", - "figures": "^1.7.0", - "indent-string": "^3.0.0", - "log-symbols": "^1.0.2", - "log-update": "^2.3.0", - "strip-ansi": "^3.0.1" - }, - "dependencies": { - "ansi-escapes": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", - "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==", - "dev": true - }, - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==", - "dev": true - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==", - "dev": true, - "requires": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - } - }, - "cli-cursor": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", - "integrity": "sha512-8lgKz8LmCRYZZQDpRyT2m5rKJ08TnU4tR9FFFW2rxpxR1FzWi4PQ/NfyODchAatHaUgnSPVcx/R5w6NuTBzFiw==", - "dev": true, - "requires": { - "restore-cursor": "^2.0.0" - } - }, - "cli-truncate": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-0.2.1.tgz", - "integrity": "sha512-f4r4yJnbT++qUPI9NR4XLDLq41gQ+uqnPItWG0F5ZkehuNiTTa3EY0S4AqTSUOeJ7/zU41oWPQSNkW5BqPL9bg==", - "dev": true, - "requires": { - "slice-ansi": "0.0.4", - "string-width": "^1.0.1" - } - }, - "figures": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz", - "integrity": "sha512-UxKlfCRuCBxSXU4C6t9scbDyWZ4VlaFFdojKtzJuSkuOBQ5CNFum+zZXFwHjo+CxBC1t6zlYPgHIgFjL8ggoEQ==", - "dev": true, - "requires": { - "escape-string-regexp": "^1.0.5", - "object-assign": "^4.1.0" - } - }, - "indent-string": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-3.2.0.tgz", - "integrity": "sha512-BYqTHXTGUIvg7t1r4sJNKcbDZkL92nkXA8YtRpbjFHRHGDL/NtUeiBJMeE60kIFN/Mg8ESaWQvftaYMGJzQZCQ==", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha512-1pqUqRjkhPJ9miNq9SwMfdvi6lBJcd6eFxvfaivQhaH3SgisfiuudvFntdKOmxuee/77l+FPjKrQjWvmPjWrRw==", - "dev": true, - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "log-symbols": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-1.0.2.tgz", - "integrity": "sha512-mmPrW0Fh2fxOzdBbFv4g1m6pR72haFLPJ2G5SJEELf1y+iaQrDG6cWCPjy54RHYbZAt7X+ls690Kw62AdWXBzQ==", - "dev": true, - "requires": { - "chalk": "^1.0.0" - } - }, - "log-update": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/log-update/-/log-update-2.3.0.tgz", - "integrity": "sha512-vlP11XfFGyeNQlmEn9tJ66rEW1coA/79m5z6BCkudjbAGE83uhAcGYrBFwfs3AdLiLzGRusRPAbSPK9xZteCmg==", - "dev": true, - "requires": { - "ansi-escapes": "^3.0.0", - "cli-cursor": "^2.0.0", - "wrap-ansi": "^3.0.1" - } - }, - "mimic-fn": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", - "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", - "dev": true - }, - "onetime": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", - "integrity": "sha512-oyyPpiMaKARvvcgip+JV+7zci5L8D1W9RZIz2l1o08AM3pfspitVWnPt3mzHcBPp12oYMTy0pqrFs/C+m3EwsQ==", - "dev": true, - "requires": { - "mimic-fn": "^1.0.0" - } - }, - "restore-cursor": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", - "integrity": "sha512-6IzJLuGi4+R14vwagDHX+JrXmPVtPpn4mffDJ1UdR7/Edm87fl6yi8mMBIVvFtJaNTUvjughmW4hwLhRG7gC1Q==", - "dev": true, - "requires": { - "onetime": "^2.0.0", - "signal-exit": "^3.0.2" - } - }, - "slice-ansi": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-0.0.4.tgz", - "integrity": "sha512-up04hB2hR92PgjpyU3y/eg91yIBILyjVY26NvvciY3EVVPjybkMszMpXQ9QAkcS3I5rtJBDLoTxxg+qvW8c7rw==", - "dev": true - }, - "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha512-0XsVpQLnVCXHJfyEs8tC0zpTVIr5PKKsQtkT29IwupnPTjtPmQ3xT/4yCREF9hYkV/3M3kzcUTSAZT6a6h81tw==", - "dev": true, - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - }, - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==", - "dev": true - }, - "wrap-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-3.0.1.tgz", - "integrity": "sha512-iXR3tDXpbnTpzjKSylUJRkLuOrEC7hwEB221cgn6wtF8wpmz28puFXAEfPT5zrjM3wahygB//VuWEr1vTkDcNQ==", - "dev": true, - "requires": { - "string-width": "^2.1.1", - "strip-ansi": "^4.0.0" - }, - "dependencies": { - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==", - "dev": true - }, - "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "dev": true, - "requires": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" - } - }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow==", - "dev": true, - "requires": { - "ansi-regex": "^3.0.0" - } - } - } - } - } - }, - "listr-verbose-renderer": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/listr-verbose-renderer/-/listr-verbose-renderer-0.5.0.tgz", - "integrity": "sha512-04PDPqSlsqIOaaaGZ+41vq5FejI9auqTInicFRndCBgE3bXG8D6W1I+mWhk+1nqbHmyhla/6BUrd5OSiHwKRXw==", - "dev": true, - "requires": { - "chalk": "^2.4.1", - "cli-cursor": "^2.1.0", - "date-fns": "^1.27.2", - "figures": "^2.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "cli-cursor": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", - "integrity": "sha512-8lgKz8LmCRYZZQDpRyT2m5rKJ08TnU4tR9FFFW2rxpxR1FzWi4PQ/NfyODchAatHaUgnSPVcx/R5w6NuTBzFiw==", - "dev": true, - "requires": { - "restore-cursor": "^2.0.0" - } - }, - "date-fns": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-1.30.1.tgz", - "integrity": "sha512-hBSVCvSmWC+QypYObzwGOd9wqdDpOt+0wl0KbU+R+uuZBS1jN8VsD1ss3irQDknRj5NvxiTF6oj/nDRnN/UQNw==", - "dev": true - }, - "figures": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", - "integrity": "sha512-Oa2M9atig69ZkfwiApY8F2Yy+tzMbazyvqv21R0NsSC8floSOC09BbT1ITWAdoMGQvJ/aZnR1KMwdx9tvHnTNA==", - "dev": true, - "requires": { - "escape-string-regexp": "^1.0.5" - } - }, - "mimic-fn": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", - "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", - "dev": true - }, - "onetime": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", - "integrity": "sha512-oyyPpiMaKARvvcgip+JV+7zci5L8D1W9RZIz2l1o08AM3pfspitVWnPt3mzHcBPp12oYMTy0pqrFs/C+m3EwsQ==", - "dev": true, - "requires": { - "mimic-fn": "^1.0.0" - } - }, - "restore-cursor": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", - "integrity": "sha512-6IzJLuGi4+R14vwagDHX+JrXmPVtPpn4mffDJ1UdR7/Edm87fl6yi8mMBIVvFtJaNTUvjughmW4hwLhRG7gC1Q==", - "dev": true, - "requires": { - "onetime": "^2.0.0", - "signal-exit": "^3.0.2" - } - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, "listr2": { "version": "3.12.2", "resolved": "https://registry.npmjs.org/listr2/-/listr2-3.12.2.tgz", @@ -115038,24 +112596,6 @@ } } }, - "no-proxy": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/no-proxy/-/no-proxy-1.0.3.tgz", - "integrity": "sha512-JPr13PIb/cENY5+WjuxzhQH74guHYPpyfk+7f7lR7SIpDE1kH0BL9jO7yztANg3jFT8jf58UEimbCBflW5UiTw==", - "dev": true, - "requires": { - "url-parse": "^1.2.0", - "wildcard": "^1.1.2" - }, - "dependencies": { - "wildcard": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-1.1.2.tgz", - "integrity": "sha512-DXukZJxpHA8LuotRwL0pP1+rS6CS7FF2qStDDE1C7DDg2rLud2PXRMuEDYIPhgEezwnlHNL4c+N6MfMTjCGTng==", - "dev": true - } - } - }, "node-abort-controller": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz", @@ -115068,12 +112608,6 @@ "integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==", "dev": true }, - "node-ask": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/node-ask/-/node-ask-1.0.1.tgz", - "integrity": "sha512-+0eqgEdgPiixrNysGDTPo3T2qyEHGVgs4ONlc5tTfcluvC/Rgq1x2ELdANUMwhR2CYLwaQnMS32O/h7adasnFQ==", - "dev": true - }, "node-cleanup": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/node-cleanup/-/node-cleanup-2.1.2.tgz", @@ -115221,17 +112755,6 @@ "integrity": "sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs=", "dev": true }, - "node-loggly-bulk": { - "version": "2.2.5", - "resolved": "https://registry.npmjs.org/node-loggly-bulk/-/node-loggly-bulk-2.2.5.tgz", - "integrity": "sha512-N6RjZfjqwhAYwT9nM8PFKXpWfaGFaDHnzwj2JBgsNq04xsEZNGMlI+rds90p5/TTkYAS8Ya6tbJChXFRqTSmiA==", - "dev": true, - "requires": { - "json-stringify-safe": "5.0.x", - "moment": "^2.18.1", - "request": ">=2.76.0 <3.0.0" - } - }, "node-notifier": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-8.0.2.tgz", @@ -116633,12 +114156,6 @@ "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==", "dev": true }, - "openurl": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/openurl/-/openurl-1.1.1.tgz", - "integrity": "sha512-d/gTkTb1i1GKz5k3XE3XFV/PxQ1k45zDqGP2OA7YhgsaLoqm6qRvARAZOFer1fcXritWlGBRCu/UgeS4HAnXAA==", - "dev": true - }, "optimist": { "version": "0.3.7", "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.3.7.tgz", @@ -117489,12 +115006,6 @@ } } }, - "pluralize": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", - "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", - "dev": true - }, "pn": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/pn/-/pn-1.1.0.tgz", @@ -118042,16 +115553,6 @@ "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", "dev": true }, - "progress-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/progress-stream/-/progress-stream-2.0.0.tgz", - "integrity": "sha512-xJwOWR46jcXUq6EH9yYyqp+I52skPySOeHfkxOZ2IY1AiBi/sFJhbhAKHoV3OTw/omQ45KTio9215dRJ2Yxd3Q==", - "dev": true, - "requires": { - "speedometer": "~1.0.0", - "through2": "~2.0.3" - } - }, "promise": { "version": "7.3.1", "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", @@ -120756,8 +118257,7 @@ "rgbcolor": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz", - "integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==", - "optional": true + "integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==" }, "rimraf": { "version": "3.0.2", @@ -120815,6 +118315,7 @@ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", "dev": true, + "peer": true, "requires": { "tslib": "^1.9.0" } @@ -121856,12 +119357,6 @@ "chalk": "^4.1.0" } }, - "speedometer": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/speedometer/-/speedometer-1.0.0.tgz", - "integrity": "sha512-lgxErLl/7A5+vgIIXsh9MbeukOaCb2axgQ+bKCdIE+ibNT4XNYGNCR1qFEGq6F+YDASXK3Fh/c5FgtZchFolxw==", - "dev": true - }, "split": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/split/-/split-1.0.1.tgz", @@ -121970,8 +119465,7 @@ "stackblur-canvas": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.6.0.tgz", - "integrity": "sha512-8S1aIA+UoF6erJYnglGPug6MaHYGo1Ot7h5fuXx4fUPvcvQfcdw2o/ppCse63+eZf8PPidSu4v1JnmEVtEDnpg==", - "optional": true + "integrity": "sha512-8S1aIA+UoF6erJYnglGPug6MaHYGo1Ot7h5fuXx4fUPvcvQfcdw2o/ppCse63+eZf8PPidSu4v1JnmEVtEDnpg==" }, "stackframe": { "version": "1.3.4", @@ -122127,12 +119621,6 @@ "safe-buffer": "~5.1.0" } }, - "string-argv": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.1.tgz", - "integrity": "sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg==", - "dev": true - }, "string-convert": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/string-convert/-/string-convert-0.2.1.tgz", @@ -122511,8 +119999,7 @@ "svg-pathdata": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz", - "integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==", - "optional": true + "integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==" }, "svgo": { "version": "3.2.0", @@ -122897,7 +120384,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz", "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==", - "optional": true, "requires": { "utrie": "^1.0.2" } @@ -123035,15 +120521,6 @@ "rimraf": "^3.0.0" } }, - "tmp-promise": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/tmp-promise/-/tmp-promise-3.0.2.tgz", - "integrity": "sha512-OyCLAKU1HzBjL6Ev3gxUeraJNlbNingmi8IrHHEsYH8LTmEuhvYfqvhn2F/je+mjf4N58UmZ96OMEy1JanSCpA==", - "dev": true, - "requires": { - "tmp": "^0.2.0" - } - }, "tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -123186,12 +120663,6 @@ "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.6.6.tgz", "integrity": "sha1-y99WD9e5r2MlAv7UD5GMFX6pcTc=" }, - "tree-kill": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", - "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", - "dev": true - }, "treeverse": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/treeverse/-/treeverse-1.0.4.tgz", @@ -123411,12 +120882,6 @@ } } }, - "tunnel": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", - "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", - "dev": true - }, "tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", @@ -124011,7 +121476,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz", "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==", - "optional": true, "requires": { "base64-arraybuffer": "^1.0.2" } @@ -125562,12 +123026,6 @@ "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==", "dev": true }, - "xmlbuilder": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-10.1.1.tgz", - "integrity": "sha512-OyzrcFLL/nb6fMGHbiRDuPup9ljBycsdCypwuyg5AAHvyWzGfChJpCXMG88AGTIMFhGZ9RccFN1e6lhg3hkwKg==", - "dev": true - }, "xmlchars": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", @@ -125658,76 +123116,6 @@ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==" }, - "yarn-or-npm": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/yarn-or-npm/-/yarn-or-npm-3.0.1.tgz", - "integrity": "sha512-fTiQP6WbDAh5QZAVdbMQkecZoahnbOjClTQhzv74WX5h2Uaidj1isf9FDes11TKtsZ0/ZVfZsqZ+O3x6aLERHQ==", - "dev": true, - "requires": { - "cross-spawn": "^6.0.5", - "pkg-dir": "^4.2.0" - }, - "dependencies": { - "find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - } - }, - "locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "requires": { - "p-locate": "^4.1.0" - } - }, - "p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "requires": { - "p-try": "^2.0.0" - } - }, - "p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "requires": { - "p-limit": "^2.2.0" - } - }, - "p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true - }, - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true - }, - "pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "dev": true, - "requires": { - "find-up": "^4.0.0" - } - } - } - }, "yauzl": { "version": "2.10.0", "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", diff --git a/superset-frontend/package.json b/superset-frontend/package.json index 8f792d06a052..11491e9fffc7 100644 --- a/superset-frontend/package.json +++ b/superset-frontend/package.json @@ -1,6 +1,6 @@ { "name": "superset", - "version": "0.0.0-dev", + "version": "4.0.2", "description": "Superset is a data exploration platform designed to be visual, intuitive, and interactive.", "keywords": [ "big", @@ -43,7 +43,6 @@ "build-instrumented": "cross-env NODE_ENV=production BABEL_ENV=instrumented webpack --mode=production --color", "build-storybook": "storybook build", "check-translation": "prettier --check ../superset/translations/**/LC_MESSAGES/*.json", - "chromatic": "npx chromatic --skip 'dependabot/**' --only-changed", "clean-translation": "prettier --write ../superset/translations/**/LC_MESSAGES/*.json", "core:cover": "cross-env NODE_ENV=test jest --coverage --coverageThreshold='{\"global\":{\"statements\":100,\"branches\":100,\"functions\":100,\"lines\":100}}' --collectCoverageFrom='[\"packages/**/src/**/*.{js,ts}\", \"!packages/superset-ui-demo/**/*\"]' packages", "cover": "cross-env NODE_ENV=test jest --coverage", @@ -57,7 +56,6 @@ "plugins:build": "node ./scripts/build.js", "plugins:build-assets": "node ./scripts/copyAssets.js", "plugins:build-storybook": "cd packages/superset-ui-demo && npm run build-storybook", - "plugins:chromatic": "cd packages/superset-ui-demo && npm run chromatic", "plugins:create-conventional-version": "npm run prune && lerna version --conventional-commits --create-release github --no-private --yes", "plugins:create-minor-version": "npm run prune && lerna version minor --no-private --yes", "plugins:create-patch-version": "npm run prune && lerna version patch --no-private --yes", @@ -268,6 +266,7 @@ "@types/react-table": "^7.7.19", "@types/react-transition-group": "^4.4.10", "@types/react-ultimate-pagination": "^1.2.0", + "@types/react-virtualized-auto-sizer": "^1.0.4", "@types/react-window": "^1.8.5", "@types/redux-localstorage": "^1.0.8", "@types/redux-mock-store": "^1.0.2", @@ -283,7 +282,6 @@ "babel-plugin-dynamic-import-node": "^2.3.3", "babel-plugin-jsx-remove-data-test-id": "^2.1.3", "babel-plugin-lodash": "^3.3.4", - "chromatic": "^6.7.4", "copy-webpack-plugin": "^12.0.2", "cross-env": "^5.2.1", "css-loader": "^6.8.1", diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/operators/contributionOperator.ts b/superset-frontend/packages/superset-ui-chart-controls/src/operators/contributionOperator.ts index a72a17be6b62..a709dfb846a5 100644 --- a/superset-frontend/packages/superset-ui-chart-controls/src/operators/contributionOperator.ts +++ b/superset-frontend/packages/superset-ui-chart-controls/src/operators/contributionOperator.ts @@ -22,12 +22,13 @@ import { PostProcessingFactory } from './types'; /* eslint-disable @typescript-eslint/no-unused-vars */ export const contributionOperator: PostProcessingFactory< PostProcessingContribution -> = (formData, queryObject) => { +> = (formData, queryObject, time_shifts) => { if (formData.contributionMode) { return { operation: 'contribution', options: { orientation: formData.contributionMode, + time_shifts, }, }; } diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/operators/types.ts b/superset-frontend/packages/superset-ui-chart-controls/src/operators/types.ts index 34f632ff8f38..0c5285a2a1e8 100644 --- a/superset-frontend/packages/superset-ui-chart-controls/src/operators/types.ts +++ b/superset-frontend/packages/superset-ui-chart-controls/src/operators/types.ts @@ -19,5 +19,5 @@ import { QueryFormData, QueryObject } from '@superset-ui/core'; export interface PostProcessingFactory { - (formData: QueryFormData, queryObject: QueryObject): T; + (formData: QueryFormData, queryObject: QueryObject, options?: any): T; } diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/operators/utils/index.ts b/superset-frontend/packages/superset-ui-chart-controls/src/operators/utils/index.ts index 1d91a6965f52..f461db0c5a63 100644 --- a/superset-frontend/packages/superset-ui-chart-controls/src/operators/utils/index.ts +++ b/superset-frontend/packages/superset-ui-chart-controls/src/operators/utils/index.ts @@ -21,5 +21,5 @@ export { getMetricOffsetsMap } from './getMetricOffsetsMap'; export { isTimeComparison } from './isTimeComparison'; export { isDerivedSeries } from './isDerivedSeries'; export { extractExtraMetrics } from './extractExtraMetrics'; -export { getOriginalSeries, hasTimeOffset } from './timeOffset'; +export { getOriginalSeries, hasTimeOffset, getTimeOffset } from './timeOffset'; export { TIME_COMPARISON_SEPARATOR } from './constants'; diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/operators/utils/timeOffset.ts b/superset-frontend/packages/superset-ui-chart-controls/src/operators/utils/timeOffset.ts index b11572c6dda6..8a7d9a964f8b 100644 --- a/superset-frontend/packages/superset-ui-chart-controls/src/operators/utils/timeOffset.ts +++ b/superset-frontend/packages/superset-ui-chart-controls/src/operators/utils/timeOffset.ts @@ -20,19 +20,23 @@ import { JsonObject } from '@superset-ui/core'; import { isString } from 'lodash'; +export const getTimeOffset = ( + series: JsonObject, + timeCompare: string[], +): string | undefined => + timeCompare.find( + timeOffset => + // offset is represented as , group by list + series.name.includes(`${timeOffset},`) || + // offset is represented as __ + series.name.includes(`__${timeOffset}`), + ); + export const hasTimeOffset = ( series: JsonObject, timeCompare: string[], ): boolean => - isString(series.name) - ? !!timeCompare.find( - timeOffset => - // offset is represented as , group by list - series.name.includes(`${timeOffset},`) || - // offset is represented as __ - series.name.includes(`__${timeOffset}`), - ) - : false; + isString(series.name) ? !!getTimeOffset(series, timeCompare) : false; export const getOriginalSeries = ( seriesName: string, diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/sections/advancedAnalytics.tsx b/superset-frontend/packages/superset-ui-chart-controls/src/sections/advancedAnalytics.tsx index 326e26fd5dc4..926488f51e09 100644 --- a/superset-frontend/packages/superset-ui-chart-controls/src/sections/advancedAnalytics.tsx +++ b/superset-frontend/packages/superset-ui-chart-controls/src/sections/advancedAnalytics.tsx @@ -21,7 +21,7 @@ import { t, RollingType, ComparisonType } from '@superset-ui/core'; import { ControlSubSectionHeader } from '../components/ControlSubSectionHeader'; import { ControlPanelSectionConfig } from '../types'; -import { formatSelectOptions } from '../utils'; +import { formatSelectOptions, displayTimeRelatedControls } from '../utils'; export const advancedAnalyticsControls: ControlPanelSectionConfig = { label: t('Advanced analytics'), @@ -31,6 +31,7 @@ export const advancedAnalyticsControls: ControlPanelSectionConfig = { 'that allow for advanced analytical post processing ' + 'of query results', ), + visibility: displayTimeRelatedControls, controlSetRows: [ [{t('Rolling window')}], [ diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/sections/forecastInterval.tsx b/superset-frontend/packages/superset-ui-chart-controls/src/sections/forecastInterval.tsx index 1dff19b83c41..67c64725c0a9 100644 --- a/superset-frontend/packages/superset-ui-chart-controls/src/sections/forecastInterval.tsx +++ b/superset-frontend/packages/superset-ui-chart-controls/src/sections/forecastInterval.tsx @@ -22,6 +22,7 @@ import { t, } from '@superset-ui/core'; import { ControlPanelSectionConfig } from '../types'; +import { displayTimeRelatedControls } from '../utils'; export const FORECAST_DEFAULT_DATA = { forecastEnabled: false, @@ -35,6 +36,7 @@ export const FORECAST_DEFAULT_DATA = { export const forecastIntervalControls: ControlPanelSectionConfig = { label: t('Predictive Analytics'), expanded: false, + visibility: displayTimeRelatedControls, controlSetRows: [ [ { diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/mixins.tsx b/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/mixins.tsx index d9396270e059..6830d01974ce 100644 --- a/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/mixins.tsx +++ b/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/mixins.tsx @@ -23,7 +23,12 @@ import { t, validateNonEmpty, } from '@superset-ui/core'; -import { BaseControlConfig, ControlPanelState, ControlState } from '../types'; +import { + BaseControlConfig, + ControlPanelState, + ControlState, + ExtraControlProps, +} from '../types'; import { getTemporalColumns } from '../utils'; const getAxisLabel = ( @@ -52,14 +57,15 @@ export const xAxisMixin = { default: undefined, }; -export const temporalColumnMixin: Pick = { +export const temporalColumnMixin: Pick & + Partial = { + isTemporal: true, mapStateToProps: ({ datasource }) => { const payload = getTemporalColumns(datasource); return { options: payload.temporalColumns, default: payload.defaultTemporalColumn, - isTemporal: true, }; }, }; diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/sharedControls.tsx b/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/sharedControls.tsx index 2be91cf5d4de..c01b4052d124 100644 --- a/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/sharedControls.tsx +++ b/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/sharedControls.tsx @@ -41,8 +41,6 @@ import { SequentialScheme, legacyValidateInteger, ComparisonType, - isAdhocColumn, - isPhysicalColumn, ensureIsArray, isDefined, NO_TIME_RANGE, @@ -51,6 +49,7 @@ import { import { formatSelectOptions, + displayTimeRelatedControls, D3_FORMAT_OPTIONS, D3_FORMAT_DOCS, D3_TIME_FORMAT_OPTIONS, @@ -62,7 +61,6 @@ import { DEFAULT_MAX_ROW, TIME_FILTER_LABELS } from '../constants'; import { SharedControlConfig, Dataset, - ColumnMeta, ControlState, ControlPanelState, } from '../types'; @@ -203,23 +201,7 @@ const time_grain_sqla: SharedControlConfig<'SelectControl'> = { mapStateToProps: ({ datasource }) => ({ choices: (datasource as Dataset)?.time_grain_sqla || [], }), - visibility: ({ controls }) => { - if (!controls?.x_axis) { - return true; - } - - const xAxis = controls?.x_axis; - const xAxisValue = xAxis?.value; - if (isAdhocColumn(xAxisValue)) { - return true; - } - if (isPhysicalColumn(xAxisValue)) { - return !!(xAxis?.options ?? []).find( - (col: ColumnMeta) => col?.column_name === xAxisValue, - )?.is_dttm; - } - return false; - }, + visibility: displayTimeRelatedControls, }; const time_range: SharedControlConfig<'DateFilterControl'> = { diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/types.ts b/superset-frontend/packages/superset-ui-chart-controls/src/types.ts index 3d149b12995f..fa8154677db9 100644 --- a/superset-frontend/packages/superset-ui-chart-controls/src/types.ts +++ b/superset-frontend/packages/superset-ui-chart-controls/src/types.ts @@ -376,6 +376,10 @@ export interface ControlPanelSectionConfig { expanded?: boolean; tabOverride?: TabOverride; controlSetRows: ControlSetRow[]; + visibility?: ( + props: ControlPanelsContainerProps, + controlData: AnyDict, + ) => boolean; } export interface StandardizedControls { diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/utils/displayTimeRelatedControls.ts b/superset-frontend/packages/superset-ui-chart-controls/src/utils/displayTimeRelatedControls.ts new file mode 100644 index 000000000000..e5e430d15891 --- /dev/null +++ b/superset-frontend/packages/superset-ui-chart-controls/src/utils/displayTimeRelatedControls.ts @@ -0,0 +1,40 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { isAdhocColumn, isPhysicalColumn } from '@superset-ui/core'; +import type { ColumnMeta, ControlPanelsContainerProps } from '../types'; + +export default function displayTimeRelatedControls({ + controls, +}: ControlPanelsContainerProps) { + if (!controls?.x_axis) { + return true; + } + + const xAxis = controls?.x_axis; + const xAxisValue = xAxis?.value; + if (isAdhocColumn(xAxisValue)) { + return true; + } + if (isPhysicalColumn(xAxisValue)) { + return !!(xAxis?.options ?? []).find( + (col: ColumnMeta) => col?.column_name === xAxisValue, + )?.is_dttm; + } + return false; +} diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/utils/index.ts b/superset-frontend/packages/superset-ui-chart-controls/src/utils/index.ts index 208d708a9685..fb829ea05738 100644 --- a/superset-frontend/packages/superset-ui-chart-controls/src/utils/index.ts +++ b/superset-frontend/packages/superset-ui-chart-controls/src/utils/index.ts @@ -26,3 +26,4 @@ export { default as columnChoices } from './columnChoices'; export * from './defineSavedMetrics'; export * from './getStandardizedControls'; export * from './getTemporalColumns'; +export { default as displayTimeRelatedControls } from './displayTimeRelatedControls'; diff --git a/superset-frontend/packages/superset-ui-chart-controls/test/utils/displayTimeRelatedControls.test.ts b/superset-frontend/packages/superset-ui-chart-controls/test/utils/displayTimeRelatedControls.test.ts new file mode 100644 index 000000000000..f96049293fbf --- /dev/null +++ b/superset-frontend/packages/superset-ui-chart-controls/test/utils/displayTimeRelatedControls.test.ts @@ -0,0 +1,118 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { displayTimeRelatedControls } from '../../src'; + +const mockData = { + actions: { + setDatasource: jest.fn(), + }, + controls: { + x_axis: { + type: 'SelectControl' as const, + value: 'not_temporal', + options: [ + { column_name: 'not_temporal', is_dttm: false }, + { column_name: 'ds', is_dttm: true }, + ], + }, + }, + exportState: {}, + form_data: { + datasource: '22__table', + viz_type: 'table', + }, +}; + +test('returns true when no x-axis exists', () => { + expect( + displayTimeRelatedControls({ + ...mockData, + controls: { + control_options: { + type: 'SelectControl', + value: 'not_temporal', + options: [], + }, + }, + }), + ).toBeTruthy(); +}); + +test('returns false when x-axis value is not temporal', () => { + expect(displayTimeRelatedControls(mockData)).toBeFalsy(); +}); +test('returns true when x-axis value is temporal', () => { + expect( + displayTimeRelatedControls({ + ...mockData, + controls: { + x_axis: { + ...mockData.controls.x_axis, + value: 'ds', + }, + }, + }), + ).toBeTruthy(); +}); + +test('returns false when x-axis value without options', () => { + expect( + displayTimeRelatedControls({ + ...mockData, + controls: { + x_axis: { + type: 'SelectControl' as const, + value: 'not_temporal', + }, + }, + }), + ).toBeFalsy(); +}); + +test('returns true when x-axis is ad-hoc column', () => { + expect( + displayTimeRelatedControls({ + ...mockData, + controls: { + x_axis: { + ...mockData.controls.x_axis, + value: { + sqlExpression: 'ds', + label: 'ds', + expressionType: 'SQL', + }, + }, + }, + }), + ).toBeTruthy(); +}); + +test('returns false when the x-axis is neither an ad-hoc column nor a physical column', () => { + expect( + displayTimeRelatedControls({ + ...mockData, + controls: { + x_axis: { + ...mockData.controls.x_axis, + value: {}, + }, + }, + }), + ).toBeFalsy(); +}); diff --git a/superset-frontend/packages/superset-ui-core/src/connection/callApi/parseResponse.ts b/superset-frontend/packages/superset-ui-core/src/connection/callApi/parseResponse.ts index 82060d379bdc..52dc34808415 100644 --- a/superset-frontend/packages/superset-ui-core/src/connection/callApi/parseResponse.ts +++ b/superset-frontend/packages/superset-ui-core/src/connection/callApi/parseResponse.ts @@ -16,11 +16,15 @@ * specific language governing permissions and limitations * under the License. */ -import JSONbig from 'json-bigint'; +import _JSONbig from 'json-bigint'; import { cloneDeepWith } from 'lodash'; import { ParseMethod, TextResponse, JsonResponse } from '../types'; +const JSONbig = _JSONbig({ + constructorAction: 'preserve', +}); + export default async function parseResponse( apiPromise: Promise, parseMethod?: T, diff --git a/superset-frontend/packages/superset-ui-core/src/hooks/useTruncation/useChildElementTruncation.test.ts b/superset-frontend/packages/superset-ui-core/src/hooks/useTruncation/useChildElementTruncation.test.ts index ee3e95139f15..7441c259874c 100644 --- a/superset-frontend/packages/superset-ui-core/src/hooks/useTruncation/useChildElementTruncation.test.ts +++ b/superset-frontend/packages/superset-ui-core/src/hooks/useTruncation/useChildElementTruncation.test.ts @@ -20,6 +20,10 @@ import { renderHook } from '@testing-library/react-hooks'; import { RefObject } from 'react'; import useChildElementTruncation from './useChildElementTruncation'; +let observeMock: jest.Mock; +let disconnectMock: jest.Mock; +let originalResizeObserver: typeof ResizeObserver; + const genElements = ( scrollWidth: number, clientWidth: number, @@ -34,26 +38,87 @@ const genElements = ( }; return [elementRef, plusRef]; }; -const useTruncation = (elementRef: any, plusRef: any) => - useChildElementTruncation( - elementRef as RefObject, - plusRef as RefObject, + +const testTruncationHookWithInitialValues = ( + [scrollWidth, clientWidth, offsetWidth, childNodes = []]: [ + number, + number, + number | undefined, + any?, + ], + expectedElementsTruncated: number, + shouldHaveHiddenElements: boolean, +) => { + const [elementRef, plusRef] = genElements( + scrollWidth, + clientWidth, + offsetWidth, + childNodes, ); + const { result, rerender } = renderHook(() => useChildElementTruncation()); + + Object.defineProperty(result.current[0], 'current', { + value: elementRef.current, + }); + Object.defineProperty(result.current[1], 'current', { + value: plusRef.current, + }); + + rerender(); + + expect(result.current).toEqual([ + elementRef, + plusRef, + expectedElementsTruncated, + shouldHaveHiddenElements, + ]); +}; + +beforeAll(() => { + // Store the original ResizeObserver + originalResizeObserver = window.ResizeObserver; + + // Mock ResizeObserver + observeMock = jest.fn(); + disconnectMock = jest.fn(); + window.ResizeObserver = jest.fn(() => ({ + observe: observeMock, + disconnect: disconnectMock, + })) as unknown as typeof ResizeObserver; +}); + +afterAll(() => { + // Restore original ResizeObserver after all tests are done + window.ResizeObserver = originalResizeObserver; +}); + +afterEach(() => { + observeMock.mockClear(); + disconnectMock.mockClear(); +}); test('should return [0, false] when elementRef.current is not defined', () => { - const { result } = renderHook(() => - useTruncation({ current: undefined }, { current: undefined }), - ); + const { result } = renderHook(() => useChildElementTruncation()); + expect(result.current).toEqual([ + { current: null }, + { current: null }, + 0, + false, + ]); - expect(result.current).toEqual([0, false]); + expect(observeMock).not.toHaveBeenCalled(); }); test('should not recompute when previousEffectInfo is the same as previous', () => { - const elementRef = { current: document.createElement('div') }; - const plusRef = { current: document.createElement('div') }; - const { result, rerender } = renderHook(() => - useTruncation(elementRef, plusRef), - ); + const { result, rerender } = renderHook(() => useChildElementTruncation()); + + Object.defineProperty(result.current[0], 'current', { + value: document.createElement('div'), + }); + Object.defineProperty(result.current[1], 'current', { + value: document.createElement('div'), + }); + const previousEffectInfo = result.current; rerender(); @@ -62,41 +127,96 @@ test('should not recompute when previousEffectInfo is the same as previous', () }); test('should return [0, false] when there are no truncated/hidden elements', () => { - const [elementRef, plusRef] = genElements(100, 100, 10); - const { result } = renderHook(() => useTruncation(elementRef, plusRef)); - expect(result.current).toEqual([0, false]); + testTruncationHookWithInitialValues([100, 100, 10], 0, false); }); test('should return [1, false] when there is only one truncated element', () => { - const [elementRef, plusRef] = genElements(150, 100, 10); - const { result } = renderHook(() => useTruncation(elementRef, plusRef)); - expect(result.current).toEqual([1, false]); + testTruncationHookWithInitialValues([150, 100, 10], 1, false); }); test('should return [1, true] with one truncated and hidden elements', () => { - const [elementRef, plusRef] = genElements(150, 100, 10, [ - { offsetWidth: 150 } as HTMLElement, - { offsetWidth: 150 } as HTMLElement, - ]); - const { result } = renderHook(() => useTruncation(elementRef, plusRef)); - expect(result.current).toEqual([1, true]); + testTruncationHookWithInitialValues( + [ + 150, + 100, + 10, + [ + { offsetWidth: 150 } as HTMLElement, + { offsetWidth: 150 } as HTMLElement, + ], + ], + 1, + true, + ); }); test('should return [2, true] with 2 truncated and hidden elements', () => { - const [elementRef, plusRef] = genElements(150, 100, 10, [ - { offsetWidth: 150 } as HTMLElement, - { offsetWidth: 150 } as HTMLElement, - { offsetWidth: 150 } as HTMLElement, - ]); - const { result } = renderHook(() => useTruncation(elementRef, plusRef)); - expect(result.current).toEqual([2, true]); + testTruncationHookWithInitialValues( + [ + 150, + 100, + 10, + [ + { offsetWidth: 150 } as HTMLElement, + { offsetWidth: 150 } as HTMLElement, + { offsetWidth: 150 } as HTMLElement, + ], + ], + 2, + true, + ); }); test('should return [1, true] with plusSize offsetWidth undefined', () => { - const [elementRef, plusRef] = genElements(150, 100, undefined, [ - { offsetWidth: 150 } as HTMLElement, - { offsetWidth: 150 } as HTMLElement, - ]); - const { result } = renderHook(() => useTruncation(elementRef, plusRef)); - expect(result.current).toEqual([1, true]); + testTruncationHookWithInitialValues( + [ + 150, + 100, + undefined, + [ + { offsetWidth: 150 } as HTMLElement, + { offsetWidth: 150 } as HTMLElement, + ], + ], + 1, + true, + ); +}); + +test('should call ResizeObserver.observe on element parent', () => { + const elementRef = { current: document.createElement('div') }; + Object.defineProperty(elementRef.current, 'parentElement', { + value: document.createElement('div'), + }); + const plusRef = { current: document.createElement('div') }; + const { result, rerender } = renderHook(() => useChildElementTruncation()); + + Object.defineProperty(result.current[0], 'current', { + value: elementRef.current, + }); + Object.defineProperty(result.current[1], 'current', { + value: plusRef.current, + }); + + rerender(); + + expect(observeMock).toHaveBeenCalled(); + expect(observeMock).toHaveBeenCalledWith(elementRef.current.parentElement); +}); + +test('should not call ResizeObserver.observe if element parent is undefined', () => { + const elementRef = { current: document.createElement('div') }; + const plusRef = { current: document.createElement('div') }; + const { result, rerender } = renderHook(() => useChildElementTruncation()); + + Object.defineProperty(result.current[0], 'current', { + value: elementRef.current, + }); + Object.defineProperty(result.current[1], 'current', { + value: plusRef.current, + }); + + rerender(); + + expect(observeMock).not.toHaveBeenCalled(); }); diff --git a/superset-frontend/packages/superset-ui-core/src/hooks/useTruncation/useChildElementTruncation.ts b/superset-frontend/packages/superset-ui-core/src/hooks/useTruncation/useChildElementTruncation.ts index 4f6b628642ab..2c95aa98b055 100644 --- a/superset-frontend/packages/superset-ui-core/src/hooks/useTruncation/useChildElementTruncation.ts +++ b/superset-frontend/packages/superset-ui-core/src/hooks/useTruncation/useChildElementTruncation.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { RefObject, useLayoutEffect, useState, useRef } from 'react'; +import { useLayoutEffect, useRef, useState } from 'react'; /** * This hook encapsulates logic to support truncation of child HTML @@ -27,92 +27,68 @@ import { RefObject, useLayoutEffect, useState, useRef } from 'react'; * (including those completely hidden) and whether any elements * are completely hidden. */ -const useChildElementTruncation = ( - elementRef: RefObject, - plusRef?: RefObject, -) => { +const useChildElementTruncation = () => { const [elementsTruncated, setElementsTruncated] = useState(0); const [hasHiddenElements, setHasHiddenElements] = useState(false); - - const previousEffectInfoRef = useRef({ - scrollWidth: 0, - parentElementWidth: 0, - plusRefWidth: 0, - }); + const elementRef = useRef(null); + const plusRef = useRef(null); useLayoutEffect(() => { - const currentElement = elementRef.current; - const plusRefElement = plusRef?.current; - - if (!currentElement) { - return; - } - - const { scrollWidth, clientWidth, childNodes } = currentElement; - - // By using the result of this effect to truncate content - // we're effectively changing it's size. - // That will trigger another pass at this effect. - // Depending on the content elements width, that second rerender could - // yield a different truncate count, thus potentially leading to a - // rendering loop. - // There's only a need to recompute if the parent width or the width of - // the child nodes changes. - const previousEffectInfo = previousEffectInfoRef.current; - const parentElementWidth = currentElement.parentElement?.clientWidth || 0; - const plusRefWidth = plusRefElement?.offsetWidth || 0; - previousEffectInfoRef.current = { - scrollWidth, - parentElementWidth, - plusRefWidth, - }; - - if ( - previousEffectInfo.parentElementWidth === parentElementWidth && - previousEffectInfo.scrollWidth === scrollWidth && - previousEffectInfo.plusRefWidth === plusRefWidth - ) { - return; - } + const onResize = () => { + const currentElement = elementRef.current; + if (!currentElement) { + return; + } + const plusRefElement = plusRef.current; + const { scrollWidth, clientWidth, childNodes } = currentElement; - if (scrollWidth > clientWidth) { - // "..." is around 6px wide - const truncationWidth = 6; - const plusSize = plusRefElement?.offsetWidth || 0; - const maxWidth = clientWidth - truncationWidth; - const elementsCount = childNodes.length; + if (scrollWidth > clientWidth) { + // "..." is around 6px wide + const truncationWidth = 6; + const plusSize = plusRefElement?.offsetWidth || 0; + const maxWidth = clientWidth - truncationWidth; + const elementsCount = childNodes.length; - let width = 0; - let hiddenElements = 0; - for (let i = 0; i < elementsCount; i += 1) { - const itemWidth = (childNodes[i] as HTMLElement).offsetWidth; - const remainingWidth = maxWidth - truncationWidth - width - plusSize; + let width = 0; + let hiddenElements = 0; + for (let i = 0; i < elementsCount; i += 1) { + const itemWidth = (childNodes[i] as HTMLElement).offsetWidth; + const remainingWidth = maxWidth - width - plusSize; - // assures it shows +{number} only when the item is not visible - if (remainingWidth <= 0) { - hiddenElements += 1; + // assures it shows +{number} only when the item is not visible + if (remainingWidth <= 0) { + hiddenElements += 1; + } + width += itemWidth; } - width += itemWidth; - } - if (elementsCount > 1 && hiddenElements) { - setHasHiddenElements(true); - setElementsTruncated(hiddenElements); + if (elementsCount > 1 && hiddenElements) { + setHasHiddenElements(true); + setElementsTruncated(hiddenElements); + } else { + setHasHiddenElements(false); + setElementsTruncated(1); + } } else { setHasHiddenElements(false); - setElementsTruncated(1); + setElementsTruncated(0); } - } else { - setHasHiddenElements(false); - setElementsTruncated(0); + }; + const obs = new ResizeObserver(onResize); + + const element = elementRef.current?.parentElement; + if (element) { + obs.observe(element); } - }, [ - elementRef.current?.offsetWidth, - elementRef.current?.clientWidth, - elementRef, - ]); - return [elementsTruncated, hasHiddenElements]; + onResize(); + + return () => { + obs.disconnect(); + }; + }, [plusRef.current]); // plus is rendered dynamically - the component rerenders the hook when plus appears, this makes sure that useLayoutEffect is rerun + + return [elementRef, plusRef, elementsTruncated, hasHiddenElements] as const; }; export default useChildElementTruncation; diff --git a/superset-frontend/packages/superset-ui-core/src/query/api/v1/makeApi.ts b/superset-frontend/packages/superset-ui-core/src/query/api/v1/makeApi.ts index 900197fcb6b3..dc62b9094b29 100644 --- a/superset-frontend/packages/superset-ui-core/src/query/api/v1/makeApi.ts +++ b/superset-frontend/packages/superset-ui-core/src/query/api/v1/makeApi.ts @@ -115,11 +115,11 @@ export default function makeApi< jsonPayload: undefined as JsonObject | undefined, }; if (requestType === 'search') { - requestConfig.searchParams = payload as URLSearchParams; + requestConfig.searchParams = payload as unknown as URLSearchParams; } else if (requestType === 'rison') { requestConfig.endpoint = `${endpoint}?q=${rison.encode(payload)}`; } else if (requestType === 'form') { - requestConfig.postPayload = payload as FormData; + requestConfig.postPayload = payload as unknown as FormData; } else { requestConfig.jsonPayload = payload as JsonObject; } diff --git a/superset-frontend/packages/superset-ui-core/src/query/types/QueryResponse.ts b/superset-frontend/packages/superset-ui-core/src/query/types/QueryResponse.ts index 1705814df11e..93c7475d42e1 100644 --- a/superset-frontend/packages/superset-ui-core/src/query/types/QueryResponse.ts +++ b/superset-frontend/packages/superset-ui-core/src/query/types/QueryResponse.ts @@ -67,6 +67,7 @@ export interface ChartDataResponseResult { is_cached: boolean; query: string; rowcount: number; + sql_rowcount: number; stacktrace: string | null; status: | 'stopped' diff --git a/superset-frontend/packages/superset-ui-core/src/ui-overrides/types.ts b/superset-frontend/packages/superset-ui-core/src/ui-overrides/types.ts index 45ec06e90ed7..60598bd4e12a 100644 --- a/superset-frontend/packages/superset-ui-core/src/ui-overrides/types.ts +++ b/superset-frontend/packages/superset-ui-core/src/ui-overrides/types.ts @@ -44,15 +44,15 @@ interface MenuObjectChildProps { disable?: boolean; } -export interface SwitchProps { - isEditMode: boolean; - dbFetched: any; - disableSSHTunnelingForEngine?: boolean; - useSSHTunneling: boolean; - setUseSSHTunneling: React.Dispatch>; - setDB: React.Dispatch; - isSSHTunneling: boolean; -} +// loose typing to avoid any circular dependencies +// refer to SSHTunnelSwitch component for strict typing +type SwitchProps = { + db: object; + changeMethods: { + onParametersChange: (event: any) => void; + }; + clearValidationErrors: () => void; +}; type ConfigDetailsProps = { embeddedId: string; diff --git a/superset-frontend/packages/superset-ui-core/test/connection/callApi/parseResponse.test.ts b/superset-frontend/packages/superset-ui-core/test/connection/callApi/parseResponse.test.ts index e13964ecf730..b08b5b8cb80c 100644 --- a/superset-frontend/packages/superset-ui-core/test/connection/callApi/parseResponse.test.ts +++ b/superset-frontend/packages/superset-ui-core/test/connection/callApi/parseResponse.test.ts @@ -139,8 +139,12 @@ describe('parseResponse()', () => { it('resolves to big number value if `parseMethod=json-bigint`', async () => { const mockBigIntUrl = '/mock/get/bigInt'; - const mockGetBigIntPayload = - '{ "value": 9223372036854775807, "minus": { "value": -483729382918228373892, "str": "something" }, "number": 1234, "floatValue": { "plus": 0.3452211361231223, "minus": -0.3452211361231223 } }'; + const mockGetBigIntPayload = `{ + "value": 9223372036854775807, "minus": { "value": -483729382918228373892, "str": "something" }, + "number": 1234, "floatValue": { "plus": 0.3452211361231223, "minus": -0.3452211361231223 }, + "string.constructor": "data.constructor", + "constructor": "constructor" + }`; fetchMock.get(mockBigIntUrl, mockGetBigIntPayload); const responseBigNumber = await parseResponse( callApi({ url: mockBigIntUrl, method: 'GET' }), @@ -167,6 +171,10 @@ describe('parseResponse()', () => { expect(Math.abs(responseBigNumber.json.floatValue.minus)).toEqual( responseBigNumber.json.floatValue.plus, ); + expect(responseBigNumber.json['string.constructor']).toEqual( + 'data.constructor', + ); + expect(responseBigNumber.json.constructor).toEqual('constructor'); }); it('rejects if request.ok=false', async () => { diff --git a/superset-frontend/packages/superset-ui-demo/package.json b/superset-frontend/packages/superset-ui-demo/package.json index 2c25081d68dd..4614e73944fb 100644 --- a/superset-frontend/packages/superset-ui-demo/package.json +++ b/superset-frontend/packages/superset-ui-demo/package.json @@ -62,7 +62,6 @@ "@babel/preset-typescript": "^7.23.3", "@storybook/react-webpack5": "^7.6.13", "babel-loader": "^8.1.0", - "chromatic": "^5.4.0", "fork-ts-checker-webpack-plugin": "^5.0.7", "ts-loader": "^7.0.4", "typescript": "^4.5.4" diff --git a/superset-frontend/packages/superset-ui-demo/storybook/shared/components/createQuery.story.tsx b/superset-frontend/packages/superset-ui-demo/storybook/shared/components/createQuery.story.tsx index e3691bc86da7..dfb0dea1f335 100644 --- a/superset-frontend/packages/superset-ui-demo/storybook/shared/components/createQuery.story.tsx +++ b/superset-frontend/packages/superset-ui-demo/storybook/shared/components/createQuery.story.tsx @@ -90,9 +90,6 @@ export default function createQueryStory({ ); }; - story.parameters = { - chromatic: { disable: true }, - }; story.args = { host: 'localhost:8088', mode: keys[0], diff --git a/superset-frontend/packages/superset-ui-demo/storybook/stories/superset-ui-connection/Connection.stories.tsx b/superset-frontend/packages/superset-ui-demo/storybook/stories/superset-ui-connection/Connection.stories.tsx index 5e655c5ec1bf..863afe52265b 100644 --- a/superset-frontend/packages/superset-ui-demo/storybook/stories/superset-ui-connection/Connection.stories.tsx +++ b/superset-frontend/packages/superset-ui-demo/storybook/stories/superset-ui-connection/Connection.stories.tsx @@ -40,7 +40,7 @@ export default { ], }; -export const configureCORS = ({ +export const ConfigureCORS = ({ host, selectEndpoint, customEndpoint, @@ -84,18 +84,14 @@ export const configureCORS = ({ ); }; - -configureCORS.parameters = { - chromatic: { disable: true }, -}; -configureCORS.args = { +ConfigureCORS.args = { host: 'localhost:8088', selectEndpoint: '/api/v1/chart/data', customEndpoint: '', methodOption: 'POST', // TODO disable when custonEndpoint and selectEndpoint are empty postPayloadContents: JSON.stringify({ form_data: bigNumberFormData }), }; -configureCORS.argTypes = { +ConfigureCORS.argTypes = { host: { control: 'text', description: 'Set Superset App host for CORS request', @@ -122,4 +118,4 @@ configureCORS.argTypes = { description: 'Set POST payload contents', }, }; -configureCORS.storyName = 'Verify CORS'; +ConfigureCORS.storyName = 'Verify CORS'; diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/ukraine.geojson b/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/ukraine.geojson index a4b3c47b99a2..62646a440031 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/ukraine.geojson +++ b/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/ukraine.geojson @@ -6,7 +6,7 @@ { "type": "Feature", "properties": { "ISO": "UA-07", "NAME_1": "Volyn" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 25.183299601000044, 51.949750060999989 ], [ 25.351971477000063, 51.921483053000102 ], [ 25.547396408000054, 51.919441982000066 ], [ 25.563897331796397, 51.897039293128842 ], [ 25.662599317604418, 51.890579738963766 ], [ 25.630921665302594, 51.872079576511226 ], [ 25.638363071398601, 51.816785793829297 ], [ 25.509895461206554, 51.698653469581927 ], [ 25.511600782649964, 51.618115750341587 ], [ 25.557851189680662, 51.612457180054776 ], [ 25.602861362361978, 51.581916409314829 ], [ 25.551494989202354, 51.525434068227639 ], [ 25.566067743031908, 51.457660426787243 ], [ 25.55568078024379, 51.349346625446401 ], [ 25.582965935929167, 51.33270681406816 ], [ 25.63123172366511, 51.366658230393 ], [ 25.700736524970637, 51.362188219410768 ], [ 25.738770378650031, 51.388698228740225 ], [ 25.772618442187365, 51.370378933440975 ], [ 25.82026411319822, 51.384254055280394 ], [ 25.813391147883124, 51.322604072120214 ], [ 25.928887973800954, 51.239560044860184 ], [ 26.059370964698246, 51.181139838332854 ], [ 26.047588738829234, 51.136000475341632 ], [ 25.950902133726458, 51.108095201132585 ], [ 25.949816929008023, 51.072593492095962 ], [ 26.03327436741813, 51.070578111390716 ], [ 26.09223717630465, 51.004690660345659 ], [ 26.089756706706623, 50.976010239780692 ], [ 25.974931675256585, 50.945107733834845 ], [ 26.014670851278765, 50.907228908887021 ], [ 25.965784945918529, 50.857826240488578 ], [ 26.003198682872892, 50.834649360579476 ], [ 25.942995639637047, 50.82589020396972 ], [ 25.875454543092644, 50.792558906168551 ], [ 25.869563428809158, 50.696905828940828 ], [ 25.827240431300822, 50.631173408426037 ], [ 25.712260370219894, 50.664814765488984 ], [ 25.655623000401135, 50.707034410209815 ], [ 25.602861362361978, 50.706879381478245 ], [ 25.448607212352954, 50.654737861063438 ], [ 25.363392774756733, 50.678870755381013 ], [ 25.35254072397521, 50.632026069147742 ], [ 25.383753289182948, 50.614921169776153 ], [ 25.289857212040658, 50.613965156266943 ], [ 25.305515170588649, 50.563373928563919 ], [ 25.286601596986088, 50.543039252559424 ], [ 25.132502475708691, 50.548801174734422 ], [ 25.123200717639008, 50.527407132433211 ], [ 25.168365919951214, 50.500432034210974 ], [ 25.136843296381016, 50.468082587441415 ], [ 25.091833122800381, 50.461803901328949 ], [ 25.203557569726058, 50.380749417251764 ], [ 25.063462762396568, 50.294811510143063 ], [ 25.063772820759027, 50.334499010221123 ], [ 25.043774040639448, 50.348865058475667 ], [ 25.003879835885698, 50.345712796208545 ], [ 24.931894565881464, 50.370155747989372 ], [ 24.923988070892733, 50.349485175200641 ], [ 24.723793573114904, 50.343723253025701 ], [ 24.634238315745677, 50.413357245540396 ], [ 24.583543735255148, 50.418783270931158 ], [ 24.564113396815799, 50.436714993502108 ], [ 24.54892052626127, 50.436275743031104 ], [ 24.541789177628402, 50.410928452785868 ], [ 24.508561231715476, 50.407931220149692 ], [ 24.503755323949008, 50.435836494358682 ], [ 24.450373569184876, 50.469839585728266 ], [ 24.535432977150151, 50.500121974949138 ], [ 24.535743036411986, 50.518260403095098 ], [ 24.500809767256953, 50.545545558780475 ], [ 24.434250522643481, 50.549007880309375 ], [ 24.358544548691953, 50.609676012438001 ], [ 24.258912387796443, 50.575569566482272 ], [ 24.170855747194935, 50.621458238307014 ], [ 24.164706252291694, 50.640216783177948 ], [ 24.108165422895922, 50.630287410735377 ], [ 24.074943482000094, 50.690188701000054 ], [ 24.081041301000141, 50.712978007000046 ], [ 24.026884400000114, 50.72801584900013 ], [ 24.012828410000111, 50.74331207300007 ], [ 24.025850871000046, 50.767031555000031 ], [ 23.974277791000134, 50.776178284000096 ], [ 23.957637980000101, 50.808010967000072 ], [ 23.99288130700009, 50.836226298000028 ], [ 24.130857381000084, 50.839068502000018 ], [ 24.143156372000107, 50.856431783000019 ], [ 23.979342081000112, 50.937512106000057 ], [ 23.95505415800011, 50.983555807000059 ], [ 23.91174930800014, 51.006810201000079 ], [ 23.904204549000099, 51.062724101000057 ], [ 23.854491821000096, 51.121531881000053 ], [ 23.874748983000103, 51.136130473000023 ], [ 23.863586873000145, 51.148274434000101 ], [ 23.742664022000099, 51.216254780999989 ], [ 23.687266886000145, 51.292400005000061 ], [ 23.635177043000084, 51.304698996000113 ], [ 23.634040161000087, 51.339296367000102 ], [ 23.683856242000019, 51.370276388000022 ], [ 23.678895304000065, 51.394073385000084 ], [ 23.697602173000064, 51.404434509000012 ], [ 23.648716268000072, 51.45396637000006 ], [ 23.662772258000103, 51.480192159000083 ], [ 23.614816528000063, 51.497219544000117 ], [ 23.606238241000142, 51.517399191000024 ], [ 23.624118286000083, 51.515900574000099 ], [ 23.628665812000122, 51.531222636000066 ], [ 23.594352661000102, 51.604964905000102 ], [ 23.628975871000108, 51.629046122000048 ], [ 23.749692017000115, 51.644471538000047 ], [ 23.884877563000146, 51.619873556000059 ], [ 23.941308228000082, 51.581917216000036 ], [ 23.981305786000036, 51.585999654000048 ], [ 24.244132121000064, 51.718213807000055 ], [ 24.272347452000105, 51.742889303000069 ], [ 24.311518189000054, 51.827561136999989 ], [ 24.369602498000063, 51.875103455 ], [ 24.639766886000046, 51.892130839000018 ], [ 24.721829061000051, 51.882338156000074 ], [ 25.002742147000077, 51.910475973000089 ], [ 25.183299601000044, 51.949750060999989 ] ] ] } }, { "type": "Feature", "properties": { "ISO": "UA-56", "NAME_1": "Rivne" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 25.547396408000054, 51.919441982000066 ], [ 25.76791508000008, 51.9285110470001 ], [ 26.050585165000086, 51.90481740400007 ], [ 26.175332072000089, 51.856706645000102 ], [ 26.407772665000039, 51.850608826000055 ], [ 26.419244832000061, 51.82092071600006 ], [ 26.445599813000115, 51.805598654 ], [ 26.665741415000127, 51.801387024000078 ], [ 26.920816284000125, 51.742527568000085 ], [ 27.021585327000111, 51.764541728000054 ], [ 27.151086466000095, 51.756764425000043 ], [ 27.177854858000075, 51.747075094000095 ], [ 27.189016968000089, 51.663772685000069 ], [ 27.277487020000137, 51.651137797000032 ], [ 27.254025920000061, 51.595378927000084 ], [ 27.26746179200012, 51.587498271000058 ], [ 27.409003540000072, 51.591709900000083 ], [ 27.477268107000015, 51.623671774000073 ], [ 27.512304728000032, 51.623129171000031 ], [ 27.676842488000148, 51.594810486000043 ], [ 27.705161174000125, 51.568352153000049 ], [ 27.66614627483068, 51.49019074250873 ], [ 27.585944452374576, 51.472284858359501 ], [ 27.576797723036464, 51.429367580749215 ], [ 27.600207146942239, 51.391850491007347 ], [ 27.50212527785925, 51.440219632430058 ], [ 27.488327671284992, 51.375314033315931 ], [ 27.509463332067128, 51.342086087403004 ], [ 27.482746616263285, 51.316764635579432 ], [ 27.416652458743954, 51.302140204007173 ], [ 27.451895786261503, 51.253073432392966 ], [ 27.449105259200337, 51.227932847722741 ], [ 27.397583856409824, 51.192172757166986 ], [ 27.358154737850839, 51.141969103091583 ], [ 27.349628127036397, 51.099826971837274 ], [ 27.299657016957724, 51.047194525906605 ], [ 27.223175896650275, 51.027221585108066 ], [ 27.198784620813569, 50.994045315139147 ], [ 27.236353387398879, 50.934514065471717 ], [ 27.215992872972663, 50.915910550231672 ], [ 27.239919060815964, 50.890382391933827 ], [ 27.225346306986467, 50.801447252188893 ], [ 27.271700066804613, 50.737368476274128 ], [ 27.193513625053754, 50.622026679087924 ], [ 27.235578241042958, 50.583010973477599 ], [ 27.192273389805052, 50.541204739007469 ], [ 27.137961459953374, 50.550971585070556 ], [ 27.126179234084361, 50.581977443803964 ], [ 27.097602166306899, 50.586602485316405 ], [ 27.072177361695879, 50.558723050428398 ], [ 26.97642093257997, 50.519190579081908 ], [ 26.901490105883681, 50.531747952206217 ], [ 26.807128940747987, 50.500897122204378 ], [ 26.796586948328979, 50.46146800454477 ], [ 26.739794548879331, 50.46487864833091 ], [ 26.703000928649942, 50.414054877530532 ], [ 26.650084262778478, 50.384418443456354 ], [ 26.643107943776556, 50.358993638845334 ], [ 26.479862094959685, 50.253573717353163 ], [ 26.383537225062867, 50.244142768074255 ], [ 26.303438755394211, 50.175542304333874 ], [ 26.233158806833444, 50.152287910058988 ], [ 26.230368279772279, 50.18236359280553 ], [ 26.201791212894136, 50.209080309508636 ], [ 26.230988397396629, 50.248561103112365 ], [ 26.221841668058516, 50.261686917017585 ], [ 26.154197218726665, 50.24158478500982 ], [ 26.065262078981732, 50.26525259133399 ], [ 26.021957227743826, 50.253367010878833 ], [ 25.947181430678484, 50.264529120022871 ], [ 25.809050327210798, 50.179159653695024 ], [ 25.675776808353021, 50.170581366936517 ], [ 25.635882602699951, 50.150789293291211 ], [ 25.606892123772468, 50.160892036138534 ], [ 25.566687859756883, 50.14076406480973 ], [ 25.453413120119365, 50.16272654879117 ], [ 25.441475863720143, 50.132263292417008 ], [ 25.467055697962053, 50.105210679828986 ], [ 25.416309442426837, 50.069192205955517 ], [ 25.410728387405129, 50.027954413165617 ], [ 25.374761590375101, 50.035499172948448 ], [ 25.367578565798169, 50.013123276917668 ], [ 25.19317060693794, 50.102704372708615 ], [ 25.192550490212909, 50.186962795896306 ], [ 25.164955275265697, 50.217193508273738 ], [ 25.207484979248363, 50.250111395824206 ], [ 25.19565107563659, 50.287990220772031 ], [ 25.104132114405559, 50.279360257170083 ], [ 25.063462762396568, 50.294811510143063 ], [ 25.203557569726058, 50.380749417251764 ], [ 25.091833122800381, 50.461803901328949 ], [ 25.136843296381016, 50.468082587441415 ], [ 25.168365919951214, 50.500432034210974 ], [ 25.122425571283088, 50.54009369586737 ], [ 25.143096144071762, 50.552857774566633 ], [ 25.270788608807209, 50.539628607873908 ], [ 25.297453647767611, 50.549266261828507 ], [ 25.305205112226133, 50.571538805071782 ], [ 25.289857212040658, 50.613965156266943 ], [ 25.383753289182948, 50.614921169776153 ], [ 25.35254072397521, 50.632026069147742 ], [ 25.363392774756733, 50.678870755381013 ], [ 25.448607212352954, 50.654737861063438 ], [ 25.602861362361978, 50.706879381478245 ], [ 25.655623000401135, 50.707034410209815 ], [ 25.712260370219894, 50.664814765488984 ], [ 25.83282148632253, 50.633188788231962 ], [ 25.870648635326233, 50.700678208832244 ], [ 25.875454543092644, 50.792558906168551 ], [ 25.942995639637047, 50.82589020396972 ], [ 26.003198682872892, 50.834649360579476 ], [ 25.965784945918529, 50.857826240488578 ], [ 26.014205763285304, 50.903275661842315 ], [ 25.978962436667075, 50.952497463986731 ], [ 26.067897577311328, 50.96676015945377 ], [ 26.095802849721736, 50.985156969118805 ], [ 26.073943719427064, 51.037065945536938 ], [ 26.041335890239111, 51.066340644405216 ], [ 25.946716342685022, 51.075177314481436 ], [ 25.952452427337619, 51.114141344147697 ], [ 26.047588738829234, 51.136000475341632 ], [ 26.059370964698246, 51.181139838332854 ], [ 25.928887973800954, 51.239560044860184 ], [ 25.813391147883124, 51.322604072120214 ], [ 25.82026411319822, 51.384254055280394 ], [ 25.772618442187365, 51.370378933440975 ], [ 25.738770378650031, 51.388698228740225 ], [ 25.700736524970637, 51.362188219410768 ], [ 25.63123172366511, 51.366658230393 ], [ 25.582965935929167, 51.33270681406816 ], [ 25.55568078024379, 51.349346625446401 ], [ 25.566067743031908, 51.457660426787243 ], [ 25.551494989202354, 51.525434068227639 ], [ 25.602861362361978, 51.581916409314829 ], [ 25.557851189680662, 51.612457180054776 ], [ 25.511600782649964, 51.618115750341587 ], [ 25.509895461206554, 51.698653469581927 ], [ 25.638363071398601, 51.816785793829297 ], [ 25.630921665302594, 51.872079576511226 ], [ 25.662599317604418, 51.890579738963766 ], [ 25.563897331796397, 51.897039293128842 ], [ 25.547396408000054, 51.919441982000066 ] ] ] } }, { "type": "Feature", "properties": { "ISO": "UA-18", "NAME_1": "Zhytomyr" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 28.210039917000103, 51.651964621000062 ], [ 28.245128214000147, 51.640621643000074 ], [ 28.257788940000097, 51.601760966000128 ], [ 28.310912313000131, 51.574449971000021 ], [ 28.333856649000069, 51.528380433000095 ], [ 28.46123905400006, 51.571659444000048 ], [ 28.603917684000123, 51.553546855000022 ], [ 28.637300659000118, 51.449625550000079 ], [ 28.66355228600014, 51.43399342900004 ], [ 28.703963257000112, 51.44262339300009 ], [ 28.728767945000072, 51.401256409000055 ], [ 28.751608928000081, 51.428748271000117 ], [ 28.752229045000036, 51.483706157000043 ], [ 28.79987471600009, 51.532617899000044 ], [ 28.863643432000117, 51.558921204000072 ], [ 28.980845581000068, 51.569463197000047 ], [ 29.043580770000091, 51.626178081000049 ], [ 29.123885945000012, 51.625041199000052 ], [ 29.160421183000039, 51.603311259000051 ], [ 29.226515340000105, 51.519052836000057 ], [ 29.221037638000041, 51.466962992 ], [ 29.264859253000111, 51.432908224000059 ], [ 29.297001994000141, 51.373712870000062 ], [ 29.390123732324355, 51.365082099259439 ], [ 29.374775832138937, 51.284802761538174 ], [ 29.270492791309266, 51.236872869687147 ], [ 29.310852084955741, 51.118740546339097 ], [ 29.342478062212763, 51.143751938900834 ], [ 29.362838575739659, 51.141633206307404 ], [ 29.48758548378305, 51.066547349980169 ], [ 29.464589471027239, 51.02417267472913 ], [ 29.462005650440403, 50.969705716145882 ], [ 29.393069289016466, 50.970119127295845 ], [ 29.505103794304659, 50.870254422403661 ], [ 29.489910922850811, 50.780828356243603 ], [ 29.567477247876695, 50.772095038055511 ], [ 29.592333611706806, 50.711091010042026 ], [ 29.480764194412018, 50.601692003083429 ], [ 29.51316531802496, 50.567353014030346 ], [ 29.496577182590897, 50.534383450535813 ], [ 29.4734778179469, 50.527097073171376 ], [ 29.498902621658658, 50.491698716922201 ], [ 29.481074252774533, 50.463535061194079 ], [ 29.444125603813518, 50.46257904678555 ], [ 29.46675988136343, 50.441288357271844 ], [ 29.426142206197824, 50.421987210041664 ], [ 29.444900750169438, 50.411109320838477 ], [ 29.536626417874857, 50.424209296321919 ], [ 29.567322219145069, 50.40689769137532 ], [ 29.608301628617255, 50.321476549103409 ], [ 29.642149693053852, 50.315895494081758 ], [ 29.69491133019369, 50.266854559989952 ], [ 29.69144900866479, 50.182156887230519 ], [ 29.723281690597503, 50.161124579235945 ], [ 29.734443800640861, 50.12148875600127 ], [ 29.66912479037677, 50.08291229906331 ], [ 29.684472690562188, 50.036842760085278 ], [ 29.671915318337255, 50.016275540084109 ], [ 29.733203566291536, 49.935298570372709 ], [ 29.630057407023685, 49.863468329100101 ], [ 29.441955194376703, 49.798356025310284 ], [ 29.449086541210875, 49.758565172444719 ], [ 29.418390740839982, 49.710118517555543 ], [ 29.478438755344257, 49.708955797572003 ], [ 29.483089634379098, 49.697431953222065 ], [ 29.447226190136519, 49.650871486929532 ], [ 29.362683547008089, 49.618909613787594 ], [ 29.281809930084194, 49.611649074844877 ], [ 29.264291619562584, 49.58454478631279 ], [ 28.992886997237122, 49.593252265179842 ], [ 28.978779331401029, 49.60382009512125 ], [ 28.995677525197607, 49.629115709422422 ], [ 28.947411736562401, 49.678466701876744 ], [ 29.004514194374565, 49.734199734130414 ], [ 28.942295770433418, 49.787969062522166 ], [ 28.939660272103879, 49.831144720752263 ], [ 28.909584587558641, 49.872382514441483 ], [ 28.835377232173528, 49.881400050771674 ], [ 28.753935173569403, 49.834891262221959 ], [ 28.720242139663014, 49.793963527794574 ], [ 28.629498324787846, 49.801043199583944 ], [ 28.571000603894731, 49.786651312907736 ], [ 28.519014113110813, 49.807296048174067 ], [ 28.430854119721801, 49.807761135268152 ], [ 28.390494826075326, 49.777142849263043 ], [ 28.353442824326862, 49.771484279875608 ], [ 28.26745324127404, 49.788227444041297 ], [ 28.02540083218264, 49.760373847574954 ], [ 27.868046094951296, 49.760063788313175 ], [ 27.660720249439919, 49.798950304512914 ], [ 27.59912194222386, 49.866465563534916 ], [ 27.597416619881074, 49.892820543233483 ], [ 27.540934278793941, 49.915093085577439 ], [ 27.545585157828725, 49.940982978181864 ], [ 27.580828485346331, 49.951447455335767 ], [ 27.543879836385315, 50.007077134801932 ], [ 27.609353875381032, 50.003666490116416 ], [ 27.66366580613203, 50.032553616256337 ], [ 27.66056521980903, 50.057694200027299 ], [ 27.621187778093486, 50.079346624746904 ], [ 27.647852817953151, 50.116708685757203 ], [ 27.627233921108541, 50.137870184961059 ], [ 27.649713169027507, 50.160556139354298 ], [ 27.607803581769872, 50.202155667350155 ], [ 27.607493524306676, 50.249336249468286 ], [ 27.582533806789741, 50.254555569284094 ], [ 27.549925977601788, 50.218382065779679 ], [ 27.488482700016561, 50.223756415226376 ], [ 27.48057620592715, 50.247940986387391 ], [ 27.400064325108474, 50.276983141258938 ], [ 27.403785028156506, 50.298170477985138 ], [ 27.308183627772166, 50.340338446761905 ], [ 27.288649937444632, 50.367881984865676 ], [ 27.228291863678521, 50.38684723621094 ], [ 27.253096550665248, 50.448032132277035 ], [ 27.304617954355081, 50.477978623814408 ], [ 27.260382928029685, 50.490148424210361 ], [ 27.192273389805052, 50.541204739007469 ], [ 27.235578241042958, 50.583010973477599 ], [ 27.193513625053754, 50.622026679087924 ], [ 27.271700066804613, 50.737368476274128 ], [ 27.225346306986467, 50.801447252188893 ], [ 27.239919060815964, 50.890382391933827 ], [ 27.215992872972663, 50.915910550231672 ], [ 27.236353387398879, 50.934514065471717 ], [ 27.198784620813569, 50.994045315139147 ], [ 27.223175896650275, 51.027221585108066 ], [ 27.299657016957724, 51.047194525906605 ], [ 27.349628127036397, 51.099826971837274 ], [ 27.358154737850839, 51.141969103091583 ], [ 27.397583856409824, 51.192172757166986 ], [ 27.449105259200337, 51.227932847722741 ], [ 27.451895786261503, 51.253073432392966 ], [ 27.416652458743954, 51.302140204007173 ], [ 27.482746616263285, 51.316764635579432 ], [ 27.509463332067128, 51.342086087403004 ], [ 27.488327671284992, 51.375314033315931 ], [ 27.50212527785925, 51.440219632430058 ], [ 27.600207146942239, 51.391850491007347 ], [ 27.576797723036464, 51.429367580749215 ], [ 27.585944452374576, 51.472284858359501 ], [ 27.66614627483068, 51.49019074250873 ], [ 27.730120890000109, 51.465154317000028 ], [ 27.792546021000078, 51.517295838000067 ], [ 27.799522339000077, 51.585198669000121 ], [ 27.831251668000107, 51.612923076000058 ], [ 27.87559004700006, 51.608039653000034 ], [ 27.954758342000048, 51.560781555000077 ], [ 28.070823608000097, 51.557629293000033 ], [ 28.210039917000103, 51.651964621000062 ] ] ] } }, -{ "type": "Feature", "properties": { "ISO": "UA-32", "NAME_1": "Kiev" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 29.660804078000126, 51.49313710600002 ], [ 29.69997481300004, 51.483706157000043 ], [ 29.737905314000045, 51.439496969000047 ], [ 29.828649129000098, 51.429962668000044 ], [ 29.886268351000069, 51.464792582000101 ], [ 29.92507735200013, 51.457867941000032 ], [ 30.00868981900004, 51.48207835 ], [ 30.177310018000128, 51.479494527 ], [ 30.320195353000145, 51.402083232000066 ], [ 30.330013876000123, 51.370353903000066 ], [ 30.317094767000128, 51.340588277000066 ], [ 30.355128621000063, 51.305267436000079 ], [ 30.428095744000075, 51.29175404900009 ], [ 30.539613484000085, 51.235168356000102 ], [ 30.554656844000078, 51.242507427000092 ], [ 30.553773634645495, 51.229121406127945 ], [ 30.514809604979291, 51.220336411995788 ], [ 30.531397738614714, 51.180287176711829 ], [ 30.511243930662829, 51.108043525188521 ], [ 30.51356936973059, 51.013165595216037 ], [ 30.55578901535074, 51.017170519104127 ], [ 30.649064975767999, 50.985570380268825 ], [ 30.663947787960012, 50.957458401384088 ], [ 30.655111117883791, 50.891984361489051 ], [ 30.780168084289642, 50.843563544122219 ], [ 30.740893996260922, 50.757367255494444 ], [ 30.793965691763219, 50.759641017718764 ], [ 30.836340366114996, 50.737006741068228 ], [ 30.871738722364114, 50.755300197945758 ], [ 30.93085656088158, 50.74646352786948 ], [ 31.089399854719488, 50.7672891302891 ], [ 31.113636101824625, 50.745378323151044 ], [ 31.11317101383122, 50.723674220688736 ], [ 31.158491244875052, 50.715457669136072 ], [ 31.212958205256939, 50.658768622473929 ], [ 31.218539260278646, 50.622879339809685 ], [ 31.179885288075582, 50.58766185251244 ], [ 31.219624464997025, 50.561539415011964 ], [ 31.258381789088276, 50.557741196698828 ], [ 31.275176629198086, 50.520766710215469 ], [ 31.328248324700382, 50.50844188108789 ], [ 31.462917108437807, 50.500225327736644 ], [ 31.496610142344196, 50.517898667889199 ], [ 31.613450555398799, 50.517640286370067 ], [ 31.686521030121412, 50.552831936144912 ], [ 31.748429395700043, 50.553193671350812 ], [ 31.783362664855076, 50.576448066525018 ], [ 31.796695184335249, 50.614223537786017 ], [ 31.847441440769842, 50.623938707005721 ], [ 31.961956414756742, 50.550093085027811 ], [ 32.028360629739268, 50.54508047078707 ], [ 32.030841099337238, 50.50441111877808 ], [ 31.993013950333534, 50.500432034210974 ], [ 31.973583611894128, 50.475343126384132 ], [ 32.007586704163032, 50.451055203334931 ], [ 32.020144077287341, 50.419765122862088 ], [ 32.089493848961922, 50.405760809813501 ], [ 32.072750684796233, 50.378010566134719 ], [ 32.139154900678079, 50.350105291925672 ], [ 32.069495069741663, 50.238690904261773 ], [ 31.999370151711105, 50.24654572240712 ], [ 31.973273552632349, 50.216056626711918 ], [ 31.995339390300614, 50.186497707902845 ], [ 31.987536248998651, 50.169289455743751 ], [ 31.954359979029789, 50.147533678236641 ], [ 31.91431074374583, 50.147275295818247 ], [ 31.853487582885634, 50.099991360013234 ], [ 31.871987746237494, 50.023458563761665 ], [ 31.810027703815479, 50.00563019487754 ], [ 31.806772087861532, 49.96020661104626 ], [ 31.716648389711395, 49.848533840963967 ], [ 31.613140497036284, 49.858145657396165 ], [ 31.596552361602164, 49.897626450999894 ], [ 31.570920851416133, 49.862693182744181 ], [ 31.5193994477263, 49.866465563534916 ], [ 31.496920199807334, 49.840033066772605 ], [ 31.430205925562973, 49.906101385870215 ], [ 31.335999790058224, 49.896747951856469 ], [ 31.21202802927013, 49.853959866354728 ], [ 31.178645053726257, 49.818251450843775 ], [ 31.198850539420846, 49.804350491481955 ], [ 31.182055698411716, 49.773809718943369 ], [ 31.196938510603786, 49.753449205416473 ], [ 31.169808383649979, 49.695519925304268 ], [ 31.174614292315709, 49.671748766192593 ], [ 31.128208855654123, 49.614439601906099 ], [ 31.129759149265283, 49.595784409822613 ], [ 31.156785923431585, 49.585190742358861 ], [ 31.155855747444775, 49.556381131483988 ], [ 30.950390252108434, 49.414296780072334 ], [ 30.908015577756714, 49.346135565903637 ], [ 30.868173048947028, 49.36063080536735 ], [ 30.725391066444558, 49.320788276557664 ], [ 30.684566684804622, 49.353835354418038 ], [ 30.619402704171421, 49.35853791029632 ], [ 30.592737665211075, 49.329935004097138 ], [ 30.550207961228409, 49.340657864568755 ], [ 30.498221470444491, 49.323863023559682 ], [ 30.437708367946811, 49.348512681814782 ], [ 30.419879999062687, 49.335955308690529 ], [ 30.382001174114862, 49.232964179053567 ], [ 30.349548373658479, 49.258053086880409 ], [ 30.169249302313403, 49.284123847537501 ], [ 30.182891880156149, 49.3214859085478 ], [ 30.15188602142274, 49.329547431368837 ], [ 30.104395379143398, 49.30128042285321 ], [ 30.111526726876946, 49.274977119098764 ], [ 30.038611280885902, 49.285105699468431 ], [ 29.920065544589193, 49.246245021690356 ], [ 29.871489699390111, 49.19684235329197 ], [ 29.869009229792084, 49.175913398084845 ], [ 29.761470574807163, 49.174621486892079 ], [ 29.729482863243504, 49.195214545314968 ], [ 29.740800002018432, 49.216815294090509 ], [ 29.641994663422963, 49.245108141027856 ], [ 29.593573846056131, 49.303709214708476 ], [ 29.525464307831498, 49.321847642854436 ], [ 29.499832797645468, 49.365669257130492 ], [ 29.507739291734879, 49.388794461095472 ], [ 29.56887251185691, 49.421634833380836 ], [ 29.540037061661053, 49.487780666844287 ], [ 29.564686720815473, 49.505970770934312 ], [ 29.56716718861486, 49.528088284546641 ], [ 29.496267124228382, 49.572788397966178 ], [ 29.51068484932631, 49.64027781856646 ], [ 29.447226190136519, 49.650871486929532 ], [ 29.482779575117263, 49.702651272138496 ], [ 29.419320915927528, 49.708077298428577 ], [ 29.449086541210875, 49.758565172444719 ], [ 29.437459344073432, 49.790837103949173 ], [ 29.630057407023685, 49.863468329100101 ], [ 29.727467481638939, 49.928787340263511 ], [ 29.733513624654051, 49.94744253234694 ], [ 29.671915318337255, 50.016275540084109 ], [ 29.684472690562188, 50.036842760085278 ], [ 29.66927981910834, 50.089785265277726 ], [ 29.734133742278345, 50.125829575774276 ], [ 29.723281690597503, 50.161124579235945 ], [ 29.69144900866479, 50.182156887230519 ], [ 29.69491133019369, 50.266854559989952 ], [ 29.642149693053852, 50.315895494081758 ], [ 29.608301628617255, 50.321476549103409 ], [ 29.567322219145069, 50.40689769137532 ], [ 29.536626417874857, 50.424209296321919 ], [ 29.444900750169438, 50.411109320838477 ], [ 29.426142206197824, 50.421987210041664 ], [ 29.46675988136343, 50.441288357271844 ], [ 29.444125603813518, 50.46257904678555 ], [ 29.481074252774533, 50.463535061194079 ], [ 29.498902621658658, 50.491698716922201 ], [ 29.4734778179469, 50.527097073171376 ], [ 29.496577182590897, 50.534383450535813 ], [ 29.51316531802496, 50.567353014030346 ], [ 29.480764194412018, 50.601692003083429 ], [ 29.592333611706806, 50.711091010042026 ], [ 29.567477247876695, 50.772095038055511 ], [ 29.489910922850811, 50.780828356243603 ], [ 29.505103794304659, 50.870254422403661 ], [ 29.393069289016466, 50.970119127295845 ], [ 29.462005650440403, 50.969705716145882 ], [ 29.464589471027239, 51.02417267472913 ], [ 29.48758548378305, 51.066547349980169 ], [ 29.362838575739659, 51.141633206307404 ], [ 29.342478062212763, 51.143751938900834 ], [ 29.310852084955741, 51.118740546339097 ], [ 29.270492791309266, 51.236872869687147 ], [ 29.349454380315365, 51.265966702301455 ], [ 29.384697706933594, 51.298161119440067 ], [ 29.39136396667368, 51.362110704145664 ], [ 29.353260212000123, 51.377055123000034 ], [ 29.402731975000108, 51.39614044200006 ], [ 29.466293986000039, 51.38505584800005 ], [ 29.505464722000113, 51.437455750000069 ], [ 29.660804078000126, 51.49313710600002 ] ], [ [ 30.753676885180084, 50.2891579715631 ], [ 30.791965799182265, 50.292555746950143 ], [ 30.790160822064252, 50.332587875330148 ], [ 30.830268158049023, 50.332724846574365 ], [ 30.846045721033192, 50.353492985013247 ], [ 30.817012763528169, 50.367226506797806 ], [ 30.760252259434196, 50.43988011157785 ], [ 30.768079276837113, 50.488703509559286 ], [ 30.750571941641795, 50.506299492727408 ], [ 30.77560546292176, 50.544198930136474 ], [ 30.842339119354847, 50.583978551369 ], [ 30.834853259770114, 50.63879710530216 ], [ 30.738579569391618, 50.668153050722708 ], [ 30.71559759865022, 50.651805437171163 ], [ 30.718544131106739, 50.62352455391499 ], [ 30.664327235313635, 50.602694650937337 ], [ 30.647182495975414, 50.575179282297995 ], [ 30.551813308637236, 50.580783641451887 ], [ 30.538686942548338, 50.571958808252361 ], [ 30.548079872987728, 50.548982771237888 ], [ 30.475767426025868, 50.595864218159136 ], [ 30.462512034202973, 50.630365880181216 ], [ 30.437266331376065, 50.634241784630603 ], [ 30.432651793045807, 50.66244745938269 ], [ 30.334013856054582, 50.663014239115796 ], [ 30.333473963352731, 50.637918952397683 ], [ 30.270164967539188, 50.63339306383881 ], [ 30.291309143102467, 50.609275474183733 ], [ 30.285561019225611, 50.588952523540115 ], [ 30.267873699111931, 50.608004352618764 ], [ 30.249909840166993, 50.600723265965939 ], [ 30.258627859088392, 50.565877710388008 ], [ 30.217438779875749, 50.542431728141707 ], [ 30.185993737221168, 50.387245468603453 ], [ 30.19660342970451, 50.374352670839073 ], [ 30.223303712196525, 50.382020633245588 ], [ 30.238241797620844, 50.414672186572147 ], [ 30.318757298210699, 50.40827410826347 ], [ 30.332238312847835, 50.368768432217337 ], [ 30.413718066535523, 50.303914962305612 ], [ 30.41059423723425, 50.261989207036265 ], [ 30.445622347093604, 50.263568562239357 ], [ 30.476911529143763, 50.199794880429351 ], [ 30.520806118907217, 50.190074235237716 ], [ 30.511085473715639, 50.146179644574886 ], [ 30.536180761333071, 50.145639750973714 ], [ 30.541697786423697, 50.097418012031596 ], [ 30.568478920665655, 50.091072387580596 ], [ 30.585565493652609, 50.014154717466226 ], [ 30.648197621922691, 50.03369265169448 ], [ 30.620784347019992, 50.122712035432073 ], [ 30.639759213017271, 50.12409198125988 ], [ 30.654383518684426, 50.155983494142902 ], [ 30.605901274923895, 50.267452462561096 ], [ 30.718108877222903, 50.262483327843256 ], [ 30.724343095457471, 50.309563514144827 ], [ 30.753676885180084, 50.2891579715631 ] ] ] } }, +{ "type": "Feature", "properties": { "ISO": "UA-32", "NAME_1": "Kyiv Oblast" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 29.660804078000126, 51.49313710600002 ], [ 29.69997481300004, 51.483706157000043 ], [ 29.737905314000045, 51.439496969000047 ], [ 29.828649129000098, 51.429962668000044 ], [ 29.886268351000069, 51.464792582000101 ], [ 29.92507735200013, 51.457867941000032 ], [ 30.00868981900004, 51.48207835 ], [ 30.177310018000128, 51.479494527 ], [ 30.320195353000145, 51.402083232000066 ], [ 30.330013876000123, 51.370353903000066 ], [ 30.317094767000128, 51.340588277000066 ], [ 30.355128621000063, 51.305267436000079 ], [ 30.428095744000075, 51.29175404900009 ], [ 30.539613484000085, 51.235168356000102 ], [ 30.554656844000078, 51.242507427000092 ], [ 30.553773634645495, 51.229121406127945 ], [ 30.514809604979291, 51.220336411995788 ], [ 30.531397738614714, 51.180287176711829 ], [ 30.511243930662829, 51.108043525188521 ], [ 30.51356936973059, 51.013165595216037 ], [ 30.55578901535074, 51.017170519104127 ], [ 30.649064975767999, 50.985570380268825 ], [ 30.663947787960012, 50.957458401384088 ], [ 30.655111117883791, 50.891984361489051 ], [ 30.780168084289642, 50.843563544122219 ], [ 30.740893996260922, 50.757367255494444 ], [ 30.793965691763219, 50.759641017718764 ], [ 30.836340366114996, 50.737006741068228 ], [ 30.871738722364114, 50.755300197945758 ], [ 30.93085656088158, 50.74646352786948 ], [ 31.089399854719488, 50.7672891302891 ], [ 31.113636101824625, 50.745378323151044 ], [ 31.11317101383122, 50.723674220688736 ], [ 31.158491244875052, 50.715457669136072 ], [ 31.212958205256939, 50.658768622473929 ], [ 31.218539260278646, 50.622879339809685 ], [ 31.179885288075582, 50.58766185251244 ], [ 31.219624464997025, 50.561539415011964 ], [ 31.258381789088276, 50.557741196698828 ], [ 31.275176629198086, 50.520766710215469 ], [ 31.328248324700382, 50.50844188108789 ], [ 31.462917108437807, 50.500225327736644 ], [ 31.496610142344196, 50.517898667889199 ], [ 31.613450555398799, 50.517640286370067 ], [ 31.686521030121412, 50.552831936144912 ], [ 31.748429395700043, 50.553193671350812 ], [ 31.783362664855076, 50.576448066525018 ], [ 31.796695184335249, 50.614223537786017 ], [ 31.847441440769842, 50.623938707005721 ], [ 31.961956414756742, 50.550093085027811 ], [ 32.028360629739268, 50.54508047078707 ], [ 32.030841099337238, 50.50441111877808 ], [ 31.993013950333534, 50.500432034210974 ], [ 31.973583611894128, 50.475343126384132 ], [ 32.007586704163032, 50.451055203334931 ], [ 32.020144077287341, 50.419765122862088 ], [ 32.089493848961922, 50.405760809813501 ], [ 32.072750684796233, 50.378010566134719 ], [ 32.139154900678079, 50.350105291925672 ], [ 32.069495069741663, 50.238690904261773 ], [ 31.999370151711105, 50.24654572240712 ], [ 31.973273552632349, 50.216056626711918 ], [ 31.995339390300614, 50.186497707902845 ], [ 31.987536248998651, 50.169289455743751 ], [ 31.954359979029789, 50.147533678236641 ], [ 31.91431074374583, 50.147275295818247 ], [ 31.853487582885634, 50.099991360013234 ], [ 31.871987746237494, 50.023458563761665 ], [ 31.810027703815479, 50.00563019487754 ], [ 31.806772087861532, 49.96020661104626 ], [ 31.716648389711395, 49.848533840963967 ], [ 31.613140497036284, 49.858145657396165 ], [ 31.596552361602164, 49.897626450999894 ], [ 31.570920851416133, 49.862693182744181 ], [ 31.5193994477263, 49.866465563534916 ], [ 31.496920199807334, 49.840033066772605 ], [ 31.430205925562973, 49.906101385870215 ], [ 31.335999790058224, 49.896747951856469 ], [ 31.21202802927013, 49.853959866354728 ], [ 31.178645053726257, 49.818251450843775 ], [ 31.198850539420846, 49.804350491481955 ], [ 31.182055698411716, 49.773809718943369 ], [ 31.196938510603786, 49.753449205416473 ], [ 31.169808383649979, 49.695519925304268 ], [ 31.174614292315709, 49.671748766192593 ], [ 31.128208855654123, 49.614439601906099 ], [ 31.129759149265283, 49.595784409822613 ], [ 31.156785923431585, 49.585190742358861 ], [ 31.155855747444775, 49.556381131483988 ], [ 30.950390252108434, 49.414296780072334 ], [ 30.908015577756714, 49.346135565903637 ], [ 30.868173048947028, 49.36063080536735 ], [ 30.725391066444558, 49.320788276557664 ], [ 30.684566684804622, 49.353835354418038 ], [ 30.619402704171421, 49.35853791029632 ], [ 30.592737665211075, 49.329935004097138 ], [ 30.550207961228409, 49.340657864568755 ], [ 30.498221470444491, 49.323863023559682 ], [ 30.437708367946811, 49.348512681814782 ], [ 30.419879999062687, 49.335955308690529 ], [ 30.382001174114862, 49.232964179053567 ], [ 30.349548373658479, 49.258053086880409 ], [ 30.169249302313403, 49.284123847537501 ], [ 30.182891880156149, 49.3214859085478 ], [ 30.15188602142274, 49.329547431368837 ], [ 30.104395379143398, 49.30128042285321 ], [ 30.111526726876946, 49.274977119098764 ], [ 30.038611280885902, 49.285105699468431 ], [ 29.920065544589193, 49.246245021690356 ], [ 29.871489699390111, 49.19684235329197 ], [ 29.869009229792084, 49.175913398084845 ], [ 29.761470574807163, 49.174621486892079 ], [ 29.729482863243504, 49.195214545314968 ], [ 29.740800002018432, 49.216815294090509 ], [ 29.641994663422963, 49.245108141027856 ], [ 29.593573846056131, 49.303709214708476 ], [ 29.525464307831498, 49.321847642854436 ], [ 29.499832797645468, 49.365669257130492 ], [ 29.507739291734879, 49.388794461095472 ], [ 29.56887251185691, 49.421634833380836 ], [ 29.540037061661053, 49.487780666844287 ], [ 29.564686720815473, 49.505970770934312 ], [ 29.56716718861486, 49.528088284546641 ], [ 29.496267124228382, 49.572788397966178 ], [ 29.51068484932631, 49.64027781856646 ], [ 29.447226190136519, 49.650871486929532 ], [ 29.482779575117263, 49.702651272138496 ], [ 29.419320915927528, 49.708077298428577 ], [ 29.449086541210875, 49.758565172444719 ], [ 29.437459344073432, 49.790837103949173 ], [ 29.630057407023685, 49.863468329100101 ], [ 29.727467481638939, 49.928787340263511 ], [ 29.733513624654051, 49.94744253234694 ], [ 29.671915318337255, 50.016275540084109 ], [ 29.684472690562188, 50.036842760085278 ], [ 29.66927981910834, 50.089785265277726 ], [ 29.734133742278345, 50.125829575774276 ], [ 29.723281690597503, 50.161124579235945 ], [ 29.69144900866479, 50.182156887230519 ], [ 29.69491133019369, 50.266854559989952 ], [ 29.642149693053852, 50.315895494081758 ], [ 29.608301628617255, 50.321476549103409 ], [ 29.567322219145069, 50.40689769137532 ], [ 29.536626417874857, 50.424209296321919 ], [ 29.444900750169438, 50.411109320838477 ], [ 29.426142206197824, 50.421987210041664 ], [ 29.46675988136343, 50.441288357271844 ], [ 29.444125603813518, 50.46257904678555 ], [ 29.481074252774533, 50.463535061194079 ], [ 29.498902621658658, 50.491698716922201 ], [ 29.4734778179469, 50.527097073171376 ], [ 29.496577182590897, 50.534383450535813 ], [ 29.51316531802496, 50.567353014030346 ], [ 29.480764194412018, 50.601692003083429 ], [ 29.592333611706806, 50.711091010042026 ], [ 29.567477247876695, 50.772095038055511 ], [ 29.489910922850811, 50.780828356243603 ], [ 29.505103794304659, 50.870254422403661 ], [ 29.393069289016466, 50.970119127295845 ], [ 29.462005650440403, 50.969705716145882 ], [ 29.464589471027239, 51.02417267472913 ], [ 29.48758548378305, 51.066547349980169 ], [ 29.362838575739659, 51.141633206307404 ], [ 29.342478062212763, 51.143751938900834 ], [ 29.310852084955741, 51.118740546339097 ], [ 29.270492791309266, 51.236872869687147 ], [ 29.349454380315365, 51.265966702301455 ], [ 29.384697706933594, 51.298161119440067 ], [ 29.39136396667368, 51.362110704145664 ], [ 29.353260212000123, 51.377055123000034 ], [ 29.402731975000108, 51.39614044200006 ], [ 29.466293986000039, 51.38505584800005 ], [ 29.505464722000113, 51.437455750000069 ], [ 29.660804078000126, 51.49313710600002 ] ], [ [ 30.753676885180084, 50.2891579715631 ], [ 30.791965799182265, 50.292555746950143 ], [ 30.790160822064252, 50.332587875330148 ], [ 30.830268158049023, 50.332724846574365 ], [ 30.846045721033192, 50.353492985013247 ], [ 30.817012763528169, 50.367226506797806 ], [ 30.760252259434196, 50.43988011157785 ], [ 30.768079276837113, 50.488703509559286 ], [ 30.750571941641795, 50.506299492727408 ], [ 30.77560546292176, 50.544198930136474 ], [ 30.842339119354847, 50.583978551369 ], [ 30.834853259770114, 50.63879710530216 ], [ 30.738579569391618, 50.668153050722708 ], [ 30.71559759865022, 50.651805437171163 ], [ 30.718544131106739, 50.62352455391499 ], [ 30.664327235313635, 50.602694650937337 ], [ 30.647182495975414, 50.575179282297995 ], [ 30.551813308637236, 50.580783641451887 ], [ 30.538686942548338, 50.571958808252361 ], [ 30.548079872987728, 50.548982771237888 ], [ 30.475767426025868, 50.595864218159136 ], [ 30.462512034202973, 50.630365880181216 ], [ 30.437266331376065, 50.634241784630603 ], [ 30.432651793045807, 50.66244745938269 ], [ 30.334013856054582, 50.663014239115796 ], [ 30.333473963352731, 50.637918952397683 ], [ 30.270164967539188, 50.63339306383881 ], [ 30.291309143102467, 50.609275474183733 ], [ 30.285561019225611, 50.588952523540115 ], [ 30.267873699111931, 50.608004352618764 ], [ 30.249909840166993, 50.600723265965939 ], [ 30.258627859088392, 50.565877710388008 ], [ 30.217438779875749, 50.542431728141707 ], [ 30.185993737221168, 50.387245468603453 ], [ 30.19660342970451, 50.374352670839073 ], [ 30.223303712196525, 50.382020633245588 ], [ 30.238241797620844, 50.414672186572147 ], [ 30.318757298210699, 50.40827410826347 ], [ 30.332238312847835, 50.368768432217337 ], [ 30.413718066535523, 50.303914962305612 ], [ 30.41059423723425, 50.261989207036265 ], [ 30.445622347093604, 50.263568562239357 ], [ 30.476911529143763, 50.199794880429351 ], [ 30.520806118907217, 50.190074235237716 ], [ 30.511085473715639, 50.146179644574886 ], [ 30.536180761333071, 50.145639750973714 ], [ 30.541697786423697, 50.097418012031596 ], [ 30.568478920665655, 50.091072387580596 ], [ 30.585565493652609, 50.014154717466226 ], [ 30.648197621922691, 50.03369265169448 ], [ 30.620784347019992, 50.122712035432073 ], [ 30.639759213017271, 50.12409198125988 ], [ 30.654383518684426, 50.155983494142902 ], [ 30.605901274923895, 50.267452462561096 ], [ 30.718108877222903, 50.262483327843256 ], [ 30.724343095457471, 50.309563514144827 ], [ 30.753676885180084, 50.2891579715631 ] ] ] } }, { "type": "Feature", "properties": { "ISO": "UA-21", "NAME_1": "Transcarpathia" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 22.13283980300011, 48.404798483 ], [ 22.148342733000106, 48.508823141000065 ], [ 22.1387309160001, 48.569594625000079 ], [ 22.243220663000102, 48.651191711000038 ], [ 22.310296671000117, 48.68168080700012 ], [ 22.330760539000067, 48.756404928000038 ], [ 22.365693807000071, 48.794387106000059 ], [ 22.36889774600013, 48.856450501000026 ], [ 22.411789184000099, 48.887766419000016 ], [ 22.448996216000097, 48.971430562000037 ], [ 22.520516398000041, 48.992927958000095 ], [ 22.531988566000052, 49.055714824000049 ], [ 22.560720663000069, 49.085532125000057 ], [ 22.642886190000098, 49.043157451000027 ], [ 22.755334106000134, 49.044707743000046 ], [ 22.835019165000091, 48.999749248000043 ], [ 22.883130731298195, 49.005949611785127 ], [ 22.901269159444155, 48.991351020433228 ], [ 22.895378045160612, 48.929313462746109 ], [ 22.982142774569354, 48.85898183914054 ], [ 23.112315708003507, 48.870092272340457 ], [ 23.201560907010219, 48.77710053186405 ], [ 23.298247511213674, 48.781157131696261 ], [ 23.308479445270166, 48.750177110485254 ], [ 23.356745233006109, 48.770718492064759 ], [ 23.385735711034215, 48.734338283884654 ], [ 23.573786247737132, 48.71581228301045 ], [ 23.693003777602257, 48.644679673727921 ], [ 23.771500277715631, 48.644705512149642 ], [ 23.79149905783521, 48.59380422608416 ], [ 23.902293328774078, 48.543290514545561 ], [ 23.917227816910213, 48.474354153121624 ], [ 23.952419467584321, 48.472855536353904 ], [ 24.129307896042576, 48.533420314795649 ], [ 24.140831740392514, 48.47528432910849 ], [ 24.132408482365577, 48.41399608025489 ], [ 24.151063674449006, 48.368882554786069 ], [ 24.234056023966332, 48.350537421065098 ], [ 24.278446079023297, 48.360872707909095 ], [ 24.301597121409998, 48.387382717238609 ], [ 24.331672804156597, 48.374825344114299 ], [ 24.34826093869134, 48.337540799268425 ], [ 24.384899530189159, 48.334517727311265 ], [ 24.499414504176002, 48.262945869356372 ], [ 24.543184441608673, 48.209150703442162 ], [ 24.521583692833133, 48.171065172020064 ], [ 24.528456659047549, 48.146131292924792 ], [ 24.641111281060773, 48.052777818141749 ], [ 24.588194614289989, 47.99593374274798 ], [ 24.569808452598011, 47.937288541133228 ], [ 24.409186646000109, 47.952061259000075 ], [ 24.347174926000065, 47.920874532000099 ], [ 24.209302205000085, 47.89759430000008 ], [ 24.008797648000012, 47.961207988000055 ], [ 23.855215291000093, 47.934232890000047 ], [ 23.780077759000079, 47.987511291000018 ], [ 23.56324344900014, 48.005753072000076 ], [ 23.460820760000018, 47.971336569000115 ], [ 23.360051717000147, 47.993144023000056 ], [ 23.231377401000088, 48.079727886000072 ], [ 23.139083292000095, 48.098124695000067 ], [ 23.098982381000098, 48.071227112000045 ], [ 23.06311893700007, 48.007458395000029 ], [ 23.004414510000117, 47.983067119000012 ], [ 22.924109334000093, 48.004822896000022 ], [ 22.91584110500014, 47.95919260700002 ], [ 22.877600546000053, 47.946738587000127 ], [ 22.8324353430001, 47.978933004000069 ], [ 22.861580851000099, 48.028387350000017 ], [ 22.85475956200014, 48.047300924000055 ], [ 22.801429484000067, 48.090967509000038 ], [ 22.745722290000117, 48.116288961000109 ], [ 22.605472453000118, 48.097039490000057 ], [ 22.555759725000115, 48.177163799000041 ], [ 22.481449015000123, 48.242586162000023 ], [ 22.357115519000047, 48.243102926000049 ], [ 22.308126261000041, 48.293694153 ], [ 22.29872115100008, 48.34914296500007 ], [ 22.256759888000062, 48.357282003000066 ], [ 22.271849406000086, 48.403454895000024 ], [ 22.201982869000091, 48.418156840000037 ], [ 22.13283980300011, 48.404798483 ] ] ] } }, { "type": "Feature", "properties": { "ISO": "UA-77", "NAME_1": "Chernivtsi" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 26.617889038000015, 48.258967591000058 ], [ 26.303489624000093, 48.212045390000114 ], [ 26.270726766000081, 48.093603007000027 ], [ 26.173058309000055, 47.993144023000056 ], [ 25.901240275000106, 47.965962219000048 ], [ 25.752515503000097, 47.934594626000049 ], [ 25.261744426000064, 47.898576152000047 ], [ 25.218956339000016, 47.87847402000007 ], [ 25.079946737000057, 47.742926738000065 ], [ 24.928857956000115, 47.713937702000109 ], [ 24.932514682606495, 47.754036363287526 ], [ 24.984811231752929, 47.840387682445566 ], [ 24.967241245287198, 47.906662706218867 ], [ 24.934840121674256, 47.947409573493019 ], [ 24.92817386193417, 47.996967272421671 ], [ 24.989152053324574, 48.100165106734266 ], [ 25.140408969798102, 48.181710517226577 ], [ 25.140874057791507, 48.231940008824324 ], [ 25.369645623346798, 48.380199693560996 ], [ 25.49284223777903, 48.403712470254277 ], [ 25.585136346265358, 48.392860419472754 ], [ 25.605806919054032, 48.521586412083252 ], [ 25.565447625407558, 48.625301012131956 ], [ 25.636192661062466, 48.678114326115235 ], [ 25.71505089728106, 48.664471747373227 ], [ 25.742336052966436, 48.629719347170123 ], [ 25.793547398293754, 48.670052802394878 ], [ 25.859589878070381, 48.618738105179375 ], [ 25.863000522755897, 48.600806383507745 ], [ 25.927854445026583, 48.586853746403222 ], [ 25.985628696407218, 48.623518175423442 ], [ 26.019786818307011, 48.616050930006395 ], [ 26.049139032440394, 48.647651068841753 ], [ 26.062316522289677, 48.628349920712253 ], [ 26.052601353069974, 48.571712550893494 ], [ 26.079783155967846, 48.547295437534387 ], [ 26.11724856886633, 48.541946926509411 ], [ 26.129650913259013, 48.554607652421225 ], [ 26.102314080730196, 48.606568304783423 ], [ 26.133061557944529, 48.609229641534682 ], [ 26.149494662848383, 48.539569811497586 ], [ 26.180862257687011, 48.526702379111498 ], [ 26.256723260370165, 48.533420314795649 ], [ 26.355011834128845, 48.510114244576698 ], [ 26.404001092276587, 48.534143785207505 ], [ 26.470560336890003, 48.53807119383049 ], [ 26.51526045120886, 48.514868476399045 ], [ 26.55282921689485, 48.465775865463797 ], [ 26.605745883665577, 48.451590685261863 ], [ 26.621868931106292, 48.458256944102686 ], [ 26.606986118014902, 48.492337550737375 ], [ 26.61566775935961, 48.506832791100408 ], [ 26.654063348244904, 48.491304022862323 ], [ 26.708995395720933, 48.496885077884031 ], [ 26.612567173036609, 48.550886949373194 ], [ 26.632824333775943, 48.562410793723132 ], [ 26.72181115126358, 48.535177313981819 ], [ 26.7391744321543, 48.557708237844849 ], [ 26.71721194817286, 48.59476024049269 ], [ 26.772764113273183, 48.550990302160699 ], [ 26.787595248621813, 48.601788235438676 ], [ 26.81875613788543, 48.606645820048527 ], [ 26.827954543167607, 48.561325589004696 ], [ 26.859270461162851, 48.540344956954186 ], [ 26.945260044215672, 48.581376044169076 ], [ 27.023394810022467, 48.560860501011291 ], [ 27.122768589398845, 48.56080882416785 ], [ 27.146953158761221, 48.581944484949986 ], [ 27.247567173386301, 48.570420641499368 ], [ 27.261778192009956, 48.59574209242362 ], [ 27.234389682637698, 48.618479722760981 ], [ 27.335003696363458, 48.601788235438676 ], [ 27.370402052612633, 48.629719347170123 ], [ 27.43820153157543, 48.602537543372875 ], [ 27.503829794000126, 48.472365418000052 ], [ 27.403474161000076, 48.411490581000109 ], [ 27.342185913000037, 48.436114401000097 ], [ 27.306012411000097, 48.423660380000015 ], [ 27.20855065900011, 48.360615133000081 ], [ 27.037398315000104, 48.399682516 ], [ 27.025409383000039, 48.389863994000038 ], [ 27.033057495000037, 48.36020172200007 ], [ 26.943243856000038, 48.351261699000034 ], [ 26.831932821000066, 48.39136261000003 ], [ 26.816946655000095, 48.371725566000109 ], [ 26.777517538000097, 48.366170350000104 ], [ 26.785734090000119, 48.294107564000015 ], [ 26.735918010000091, 48.291833801000095 ], [ 26.679797404000055, 48.330177714000015 ], [ 26.625123739000088, 48.282893778000059 ], [ 26.617889038000015, 48.258967591000058 ] ] ] } }, { "type": "Feature", "properties": { "ISO": "UA-26", "NAME_1": "Ivano-Frankivs'k" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 24.928857956000115, 47.713937702000109 ], [ 24.877788533000114, 47.718742167000087 ], [ 24.793659302000066, 47.804473369000064 ], [ 24.679144328000064, 47.840130107000064 ], [ 24.649275350000039, 47.895217184000032 ], [ 24.569808452598011, 47.937288541133228 ], [ 24.588194614289989, 47.99593374274798 ], [ 24.641111281060773, 48.052777818141749 ], [ 24.528456659047549, 48.146131292924792 ], [ 24.521583692833133, 48.171065172020064 ], [ 24.541479120165263, 48.212768052803369 ], [ 24.494918653872787, 48.266614895560963 ], [ 24.384899530189159, 48.334517727311265 ], [ 24.34826093869134, 48.337540799268425 ], [ 24.335703565567087, 48.370742905860368 ], [ 24.309503614600089, 48.387382717238609 ], [ 24.256586947829362, 48.35309540502891 ], [ 24.177728713409351, 48.35461985931903 ], [ 24.151063674449006, 48.368882554786069 ], [ 24.130858188754416, 48.425985011698913 ], [ 24.133648715815582, 48.531094875727888 ], [ 24.001460401676184, 48.500140692938601 ], [ 23.952419467584321, 48.472855536353904 ], [ 23.917227816910213, 48.474354153121624 ], [ 23.90678917817803, 48.537967841042985 ], [ 23.79149905783521, 48.59380422608416 ], [ 23.776616244743877, 48.64214752908515 ], [ 23.693003777602257, 48.644679673727921 ], [ 23.612440219940197, 48.703823351566371 ], [ 23.532858514209067, 48.723692939577461 ], [ 23.575336541348292, 48.833867091992659 ], [ 23.548361444025431, 48.884949246110807 ], [ 23.576473422910112, 48.968639228517532 ], [ 23.641637404442633, 49.010755520450857 ], [ 23.684115430682539, 49.086771551865581 ], [ 23.710987176117271, 49.10428986328651 ], [ 23.905703972560275, 49.107183743135238 ], [ 23.955675082638948, 49.128009345554801 ], [ 24.003320753649859, 49.116433824361479 ], [ 24.084504428936214, 49.145579332020532 ], [ 24.158195021283177, 49.134985662758083 ], [ 24.441226840746083, 49.194103502174869 ], [ 24.44618777814344, 49.208469550429356 ], [ 24.417662388108681, 49.235212103755543 ], [ 24.356942580036048, 49.241025702773925 ], [ 24.362885370263598, 49.297353014230225 ], [ 24.339579299145328, 49.303605861920971 ], [ 24.299840122223827, 49.285209052255937 ], [ 24.281856723708813, 49.301022040434816 ], [ 24.369551629104365, 49.326136785784001 ], [ 24.423243442231069, 49.366444404385732 ], [ 24.383349236577999, 49.393781236015229 ], [ 24.426499058184959, 49.449462592324778 ], [ 24.413269890592971, 49.48338817022784 ], [ 24.438746372047433, 49.515685940154015 ], [ 24.584577264029519, 49.523980007871046 ], [ 24.707050408949215, 49.491785589833057 ], [ 24.804150425201954, 49.388871975461257 ], [ 24.832882520811665, 49.339572658950999 ], [ 24.823425734010414, 49.313786119134079 ], [ 24.838308547101747, 49.262213040399445 ], [ 24.879132927842306, 49.229398504737162 ], [ 24.880838250185093, 49.201751613845886 ], [ 24.851175977689195, 49.175293281359814 ], [ 24.860012647765473, 49.119379381053591 ], [ 24.917115105577636, 49.122686672951545 ], [ 24.981555616698358, 49.099199733780665 ], [ 24.985586379008168, 49.051734930822363 ], [ 24.903627557365837, 49.061062527313766 ], [ 24.897581414350782, 49.039849352165845 ], [ 24.95034305238994, 49.020754910510675 ], [ 24.99256269711077, 48.969621080448462 ], [ 25.067028435813654, 48.972463284353069 ], [ 25.119635044221923, 48.951379300414374 ], [ 25.122994012063998, 48.907609361183063 ], [ 25.165575391990728, 48.865493069249737 ], [ 25.197356397979377, 48.91773794245205 ], [ 25.245777215346209, 48.931380520294738 ], [ 25.225313348132488, 48.84200613097812 ], [ 25.250066359175094, 48.859446926234625 ], [ 25.327736036988483, 48.84200613097812 ], [ 25.250066359175094, 48.817692369507199 ], [ 25.238955925975176, 48.801052558129015 ], [ 25.25099653516196, 48.77516266552459 ], [ 25.327736036988483, 48.814695135971704 ], [ 25.35409101668705, 48.813868312772399 ], [ 25.356106398291615, 48.854589342524093 ], [ 25.430778842569509, 48.849447537074127 ], [ 25.440390659001707, 48.869937241810248 ], [ 25.47108646027192, 48.862495835714242 ], [ 25.450002476333225, 48.835779119910455 ], [ 25.461112908633822, 48.825288805234152 ], [ 25.565447625407558, 48.793430283980399 ], [ 25.640068393741387, 48.745035305035287 ], [ 25.627614373404583, 48.71826691328738 ], [ 25.646269566387389, 48.69726044371447 ], [ 25.636192661062466, 48.678114326115235 ], [ 25.565447625407558, 48.625301012131956 ], [ 25.605806919054032, 48.521586412083252 ], [ 25.585136346265358, 48.392860419472754 ], [ 25.49284223777903, 48.403712470254277 ], [ 25.369645623346798, 48.380199693560996 ], [ 25.140874057791507, 48.231940008824324 ], [ 25.140408969798102, 48.181710517226577 ], [ 24.989152053324574, 48.100165106734266 ], [ 24.92817386193417, 47.996967272421671 ], [ 24.934840121674256, 47.947409573493019 ], [ 24.967241245287198, 47.906662706218867 ], [ 24.984811231752929, 47.840387682445566 ], [ 24.932514682606495, 47.754036363287526 ], [ 24.928857956000115, 47.713937702000109 ] ] ] } }, @@ -26,6 +26,8 @@ { "type": "Feature", "properties": { "ISO": "UA-12", "NAME_1": "Dnipropetrovs'k" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 34.983942499047089, 49.163717760166492 ], [ 35.169822625413815, 49.147232978419197 ], [ 35.361335483645632, 49.022020982382401 ], [ 35.462466262208238, 48.974892076208334 ], [ 35.579203321576017, 48.975822252195144 ], [ 35.794383986132061, 48.934222724199344 ], [ 35.930654737626014, 48.976390692976054 ], [ 35.952513868819949, 48.968690904461653 ], [ 36.022018670125533, 48.900116279142935 ], [ 35.961970655621258, 48.854899399987289 ], [ 36.072144809835095, 48.829112861069632 ], [ 36.163715447909567, 48.767256171435122 ], [ 36.170691766012169, 48.757747707790429 ], [ 36.132812941064344, 48.723692939577461 ], [ 36.285671827992473, 48.634111842887194 ], [ 36.267533399846513, 48.59780914997225 ], [ 36.307582635130473, 48.569903875763202 ], [ 36.27404463085503, 48.541042588944322 ], [ 36.284276564012202, 48.528252671823338 ], [ 36.374761997368239, 48.541636868146895 ], [ 36.364530064211067, 48.558457547577746 ], [ 36.382358433095192, 48.580549220969772 ], [ 36.477184686224291, 48.646875922485833 ], [ 36.583431430915766, 48.602666734582101 ], [ 36.725438267062373, 48.625921128856987 ], [ 36.738305697649821, 48.601581528964346 ], [ 36.787191603010058, 48.603700263356416 ], [ 36.749312778062233, 48.563961087334292 ], [ 36.751173130035909, 48.529234523754269 ], [ 36.795098097099469, 48.53481557877592 ], [ 36.81917931457366, 48.578249620323675 ], [ 36.883103061756856, 48.57930898751971 ], [ 36.891009555846324, 48.533988756475878 ], [ 36.854215935616935, 48.51652212189839 ], [ 36.858556756289261, 48.492673448420874 ], [ 36.835457390745944, 48.476343696304525 ], [ 36.847239618413653, 48.433969021053429 ], [ 36.808947381416488, 48.413143419533185 ], [ 36.81421837717636, 48.319014798394221 ], [ 36.901086460271927, 48.300307929467351 ], [ 36.913798862127749, 48.263824368499797 ], [ 36.927183057552043, 48.201502590871826 ], [ 36.869770542276683, 48.161039944437846 ], [ 36.892559848558165, 48.067557278445634 ], [ 36.829411248630208, 48.032107245353075 ], [ 36.665080194195582, 48.088072822502738 ], [ 36.589064161881538, 48.078926093164625 ], [ 36.57505984883295, 48.058229681954231 ], [ 36.57661014244411, 47.970715643711969 ], [ 36.61340376177418, 47.95702138902584 ], [ 36.63402265771947, 47.924258531106318 ], [ 36.534442172768024, 47.905034898241922 ], [ 36.53304690878781, 47.883640855041392 ], [ 36.569478793811243, 47.867362778869108 ], [ 36.576145053551386, 47.844211738281047 ], [ 36.39197024862807, 47.825711574929187 ], [ 36.361222772313056, 47.839870916709401 ], [ 36.285981887254252, 47.813309231435881 ], [ 36.214926792337565, 47.830310777120644 ], [ 36.203454623931748, 47.862763577577027 ], [ 36.15224328040307, 47.837907212847597 ], [ 36.078656039944292, 47.845167751790314 ], [ 36.060827671060167, 47.87377065709012 ], [ 36.098396436746157, 47.873357245040779 ], [ 36.117620069610496, 47.938676256204246 ], [ 36.059122348717381, 47.951130276540994 ], [ 36.053076205702325, 47.993039862899309 ], [ 36.021295199713677, 48.004227810465068 ], [ 36.044704623619452, 48.043295192918777 ], [ 35.974579705588894, 48.028644924723494 ], [ 35.959180128560035, 48.090708319933015 ], [ 35.808853387174054, 48.066833808033778 ], [ 35.674132928391884, 48.126985175325558 ], [ 35.514762811354615, 48.079391181158087 ], [ 35.254571974117312, 48.129672350498538 ], [ 35.164551628754623, 48.119130357180211 ], [ 35.118146192992413, 48.129052231974924 ], [ 35.017428827378467, 48.116649889380881 ], [ 34.943118117407153, 48.080993149813992 ], [ 34.886015658695669, 48.080889797925806 ], [ 34.869892612154331, 48.119337062755221 ], [ 34.851444125645855, 48.122747708340057 ], [ 34.835889519885427, 48.108588364761204 ], [ 34.840230339658433, 48.041305649735932 ], [ 34.813410271966461, 48.015519110818275 ], [ 34.842245721262998, 48.006553250432148 ], [ 34.857025180667506, 47.978389593804707 ], [ 34.90839155472645, 47.966684882301479 ], [ 34.916298048815861, 47.947151191074568 ], [ 34.819766473344032, 47.906740221484029 ], [ 34.808139276206589, 47.877543036082216 ], [ 34.901880323717933, 47.83082754195749 ], [ 34.886325717957504, 47.760134182246702 ], [ 34.945288526844024, 47.742770901355982 ], [ 34.895317416765351, 47.597250067736411 ], [ 34.929630568296091, 47.563298652310948 ], [ 34.898262974356783, 47.523249417026932 ], [ 34.594612258049267, 47.568621324014885 ], [ 34.445939162162574, 47.522887681821032 ], [ 34.157481317308907, 47.47389842457261 ], [ 33.901786330374819, 47.514464422894775 ], [ 33.654411248680162, 47.486817532003499 ], [ 33.624335565034301, 47.511622218990169 ], [ 33.584441359381231, 47.51472280531317 ], [ 33.575294630942437, 47.537589626859756 ], [ 33.588782180053556, 47.560043036357001 ], [ 33.562892287449074, 47.575365099020075 ], [ 33.494265985286972, 47.564151313032653 ], [ 33.485274285579806, 47.532732042249904 ], [ 33.365953402927175, 47.51436107010727 ], [ 33.339443393597719, 47.49508576129881 ], [ 33.283426141403311, 47.539320786724886 ], [ 32.994193150193723, 47.595441393505496 ], [ 33.02070315862386, 47.642596137201963 ], [ 32.983754509662845, 47.710008043436403 ], [ 33.086952345774137, 47.731712144999449 ], [ 33.060287305914414, 47.775585436118945 ], [ 33.0827665547327, 47.89593984574725 ], [ 33.031710239036272, 47.919736843280646 ], [ 33.057496778853249, 48.025802720818888 ], [ 33.134287956623893, 48.076962389302821 ], [ 33.2222929221806, 48.090449938413883 ], [ 33.241981642139081, 48.173907375924614 ], [ 33.268801710730372, 48.102723089798758 ], [ 33.348125034043051, 48.185947984212078 ], [ 33.40290205278751, 48.175250963061444 ], [ 33.470546502119362, 48.227986761779619 ], [ 33.513386265363863, 48.238502915776905 ], [ 33.525943637588796, 48.297414048719361 ], [ 33.500002069040249, 48.311211656192938 ], [ 33.488529900634376, 48.403945014251008 ], [ 33.471476678106171, 48.422961941540336 ], [ 33.484499139223885, 48.443916734269862 ], [ 33.461554803311515, 48.497763577027456 ], [ 33.443261346433985, 48.504274807136596 ], [ 33.4454317558708, 48.525307115131227 ], [ 33.580410597071364, 48.559413561086956 ], [ 33.821067743081869, 48.668399155996212 ], [ 33.769701369022926, 48.680543117970501 ], [ 33.756523879173642, 48.697699693286211 ], [ 33.598549025217324, 48.721832586704465 ], [ 33.557776320420828, 48.762295234037822 ], [ 33.572194044619437, 48.787074083502148 ], [ 33.619374626737624, 48.784231878698222 ], [ 33.664798210568961, 48.807615465081597 ], [ 33.70887820816273, 48.782500718833091 ], [ 33.760399610953243, 48.807098701144128 ], [ 33.774817336051171, 48.77898672136007 ], [ 33.801844110217473, 48.796143297575099 ], [ 33.810990837756947, 48.761985174775987 ], [ 33.82571862211671, 48.759453030133216 ], [ 33.919459669628054, 48.865803128511573 ], [ 34.040175816260899, 48.807822171555927 ], [ 34.161977165813539, 48.784386909228431 ], [ 34.237476434190057, 48.746275540283932 ], [ 34.293752068802917, 48.778314927791655 ], [ 34.275872023075408, 48.809992580992798 ], [ 34.315921258359367, 48.839603175745935 ], [ 34.308789910625876, 48.867353421223413 ], [ 34.274321730363567, 48.891227932223273 ], [ 34.277732375049027, 48.922285467800066 ], [ 34.308169793001525, 48.970964666685973 ], [ 34.367907749143285, 48.991066800492433 ], [ 34.34449832523751, 49.008533434170602 ], [ 34.402841017398998, 49.050132962166458 ], [ 34.40826704278976, 49.076539618708409 ], [ 34.72649051292683, 49.145992744069872 ], [ 34.784058057833079, 49.174569810948014 ], [ 34.864776646025348, 49.173407090964474 ], [ 34.867412144354944, 49.144907538452117 ], [ 34.891131625723915, 49.148266507193512 ], [ 34.926219923610574, 49.183044744919016 ], [ 34.983942499047089, 49.163717760166492 ] ] ] } }, { "type": "Feature", "properties": { "ISO": "UA-71", "NAME_1": "Cherkasy" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 32.019213902199795, 50.251454982961093 ], [ 32.094609815990168, 50.236933905974979 ], [ 32.140705194289239, 50.182311916861465 ], [ 32.215842725661219, 50.147533678236641 ], [ 32.269689569318132, 50.156835436306324 ], [ 32.271704950023377, 50.131358953952542 ], [ 32.230570510020982, 50.118594876152599 ], [ 32.255065137745873, 50.062836005477266 ], [ 32.329427524560572, 50.051777249120732 ], [ 32.381207309769536, 49.987026678738232 ], [ 32.415675490031845, 49.96749298841064 ], [ 32.407148879217402, 49.893440659958458 ], [ 32.368649936645227, 49.887497869730851 ], [ 32.372680698055774, 49.86362335873099 ], [ 32.431023390217263, 49.805177313781996 ], [ 32.468902215165087, 49.806572577762211 ], [ 32.503525425058285, 49.78001089069005 ], [ 32.562488233944805, 49.766600856844036 ], [ 32.571479932752652, 49.705881048771346 ], [ 32.692971225741474, 49.671645413405088 ], [ 32.710799594625598, 49.619968980983685 ], [ 32.752089064258882, 49.593820705960809 ], [ 32.754414504225963, 49.547156886880828 ], [ 32.675918003213269, 49.477135322537094 ], [ 32.747593214854987, 49.406131904463791 ], [ 32.789347771582356, 49.333888251141843 ], [ 32.788727654857382, 49.261799628350161 ], [ 32.752554152252287, 49.181287747531485 ], [ 32.789502801213303, 49.146767890425792 ], [ 32.800354851994825, 49.07599701634922 ], [ 32.827174919686797, 49.027705390191556 ], [ 32.853891636389903, 49.021375027235706 ], [ 32.850791050066903, 49.005820421475221 ], [ 32.775240105746263, 48.961714586359051 ], [ 32.689405552324388, 48.987449449332587 ], [ 32.634318475217412, 48.946004950967676 ], [ 32.567759229704677, 48.957942206467635 ], [ 32.547088656915946, 48.929597682686904 ], [ 32.526934848964061, 48.927711494090147 ], [ 32.456189813309209, 49.018429470543595 ], [ 32.452934198254638, 49.056127428338129 ], [ 32.369115024638631, 49.045792141494132 ], [ 32.331907993259279, 49.089768785401077 ], [ 32.288758171652262, 49.070028387699892 ], [ 32.284572380610882, 49.047962550930947 ], [ 32.241422559903185, 49.052044990084198 ], [ 32.192536655442268, 48.999774278460166 ], [ 32.142565545363595, 48.975615545720814 ], [ 32.132953728931398, 48.961146145578141 ], [ 32.143650750082031, 48.935798855332905 ], [ 32.121326531794011, 48.910270697934322 ], [ 32.026190220302396, 48.928047389975063 ], [ 31.886715528798618, 48.85975698549646 ], [ 31.841550327385676, 48.909934801150143 ], [ 31.732306349158648, 48.942645982226281 ], [ 31.683575474328677, 48.943007717432181 ], [ 31.651122673872294, 48.917427883190214 ], [ 31.544100782824842, 48.924197495717806 ], [ 31.529476353051223, 48.904043687765977 ], [ 31.527305942715088, 48.835210680028808 ], [ 31.481210565315337, 48.803481349984281 ], [ 31.462296990813456, 48.761106676531881 ], [ 31.332899203735224, 48.723847968309087 ], [ 31.309128044623549, 48.742813218755032 ], [ 31.223655226407573, 48.753871975111565 ], [ 31.067385694793984, 48.727413641726173 ], [ 30.93814293824596, 48.756223253500309 ], [ 30.820062289942712, 48.749789536857634 ], [ 30.702291700901242, 48.765189113886493 ], [ 30.606845331047168, 48.714520371817684 ], [ 30.599403924051842, 48.659975897969275 ], [ 30.540906203158727, 48.619254869116901 ], [ 30.576097852933572, 48.604320380081447 ], [ 30.567571242119129, 48.561919868207326 ], [ 30.484423862071537, 48.55352244680347 ], [ 30.414712355191, 48.571919257367824 ], [ 30.384946729907654, 48.530526434946978 ], [ 30.247280715332749, 48.47755809223213 ], [ 30.193640578150166, 48.493474433198514 ], [ 30.108426140553945, 48.45332184512705 ], [ 30.036905959442436, 48.473785712340714 ], [ 29.957892693592896, 48.459316311298721 ], [ 30.021661411145203, 48.533627021269979 ], [ 29.963628778245493, 48.609849758259713 ], [ 29.958202751955412, 48.655325018934434 ], [ 29.859087354997428, 48.698319810910505 ], [ 29.857537062285587, 48.71769847340579 ], [ 29.884047071615043, 48.743278306748437 ], [ 29.846323276298165, 48.764827379579856 ], [ 29.772477655219575, 48.769297390562087 ], [ 29.745295851422384, 48.848543199509038 ], [ 29.703592970639079, 48.867482612432582 ], [ 29.722351514610693, 48.89696401597655 ], [ 29.716150343763331, 48.914378973710654 ], [ 29.635948521307171, 48.947038478842671 ], [ 29.672690463793856, 48.983522039810282 ], [ 29.67083011182018, 49.00140208643711 ], [ 29.593108758062726, 49.031477770082972 ], [ 29.6095418638659, 49.062121894509801 ], [ 29.670675083088611, 49.085169583209677 ], [ 29.67486087412999, 49.114625149231244 ], [ 29.707158644056165, 49.131678371759449 ], [ 29.710569288741681, 49.182760524978221 ], [ 29.674550815767532, 49.209709784778738 ], [ 29.697236769261451, 49.227279772143731 ], [ 29.745295851422384, 49.213378810983329 ], [ 29.729482863243504, 49.195214545314968 ], [ 29.761470574807163, 49.174621486892079 ], [ 29.862342970951318, 49.173846341435478 ], [ 29.920065544589193, 49.246245021690356 ], [ 30.038611280885902, 49.285105699468431 ], [ 30.111526726876946, 49.274977119098764 ], [ 30.104395379143398, 49.30128042285321 ], [ 30.15188602142274, 49.329547431368837 ], [ 30.182891880156149, 49.3214859085478 ], [ 30.169249302313403, 49.284123847537501 ], [ 30.349548373658479, 49.258053086880409 ], [ 30.382001174114862, 49.232964179053567 ], [ 30.419879999062687, 49.335955308690529 ], [ 30.437708367946811, 49.348512681814782 ], [ 30.498221470444491, 49.323863023559682 ], [ 30.550207961228409, 49.340657864568755 ], [ 30.592737665211075, 49.329935004097138 ], [ 30.619402704171421, 49.35853791029632 ], [ 30.684566684804622, 49.353835354418038 ], [ 30.725391066444558, 49.320788276557664 ], [ 30.868173048947028, 49.36063080536735 ], [ 30.908015577756714, 49.346135565903637 ], [ 30.950390252108434, 49.414296780072334 ], [ 31.155855747444775, 49.556381131483988 ], [ 31.156785923431585, 49.585190742358861 ], [ 31.129759149265283, 49.595784409822613 ], [ 31.128208855654123, 49.614439601906099 ], [ 31.174614292315709, 49.671748766192593 ], [ 31.169808383649979, 49.695519925304268 ], [ 31.196938510603786, 49.753449205416473 ], [ 31.182055698411716, 49.773809718943369 ], [ 31.198850539420846, 49.804350491481955 ], [ 31.178645053726257, 49.818251450843775 ], [ 31.21202802927013, 49.853959866354728 ], [ 31.335999790058224, 49.896747951856469 ], [ 31.430205925562973, 49.906101385870215 ], [ 31.496920199807334, 49.840033066772605 ], [ 31.5193994477263, 49.866465563534916 ], [ 31.570920851416133, 49.862693182744181 ], [ 31.596552361602164, 49.897626450999894 ], [ 31.613140497036284, 49.858145657396165 ], [ 31.716648389711395, 49.848533840963967 ], [ 31.806772087861532, 49.96020661104626 ], [ 31.810027703815479, 50.00563019487754 ], [ 31.871987746237494, 50.023458563761665 ], [ 31.849611851106033, 50.094978745772494 ], [ 31.91431074374583, 50.147275295818247 ], [ 31.954359979029789, 50.147533678236641 ], [ 31.987536248998651, 50.169289455743751 ], [ 31.995339390300614, 50.186497707902845 ], [ 31.975133904605968, 50.224066474488154 ], [ 32.019213902199795, 50.251454982961093 ] ] ] } }, { "type": "Feature", "properties": { "ISO": "UA-35", "NAME_1": "Kirovohrad" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 32.788727654857382, 49.261799628350161 ], [ 33.038221470044789, 49.18195954020058 ], [ 33.149325799346116, 49.125632228744337 ], [ 33.204102818090576, 49.070906886843318 ], [ 33.297068719245999, 49.064860744727582 ], [ 33.34161380393391, 49.044035143207282 ], [ 33.345954623706916, 49.032666328488233 ], [ 33.31923790880245, 49.025586655799486 ], [ 33.29939415921308, 48.995459296209503 ], [ 33.378820835313263, 48.939958807952621 ], [ 33.425071242343961, 48.950914212420969 ], [ 33.548422886407081, 48.901873277429786 ], [ 33.596998731606163, 48.922388821486891 ], [ 33.651620720719677, 48.899289455943631 ], [ 33.668880649722212, 48.869058743566143 ], [ 33.694615512695748, 48.905800686052771 ], [ 33.778486362255819, 48.92254385021846 ], [ 33.807425165239181, 48.903449409462667 ], [ 33.892019484311732, 48.891744697060119 ], [ 33.919459669628054, 48.865803128511573 ], [ 33.82571862211671, 48.759453030133216 ], [ 33.810990837756947, 48.761985174775987 ], [ 33.801844110217473, 48.796143297575099 ], [ 33.774817336051171, 48.77898672136007 ], [ 33.773732131332793, 48.801414293334972 ], [ 33.754043409575672, 48.807563788238213 ], [ 33.70887820816273, 48.782500718833091 ], [ 33.670844353584016, 48.807408759506643 ], [ 33.619374626737624, 48.784231878698222 ], [ 33.584441359381231, 48.794489651176434 ], [ 33.557569613946498, 48.771984564835748 ], [ 33.598549025217324, 48.721832586704465 ], [ 33.756523879173642, 48.697699693286211 ], [ 33.769701369022926, 48.680543117970501 ], [ 33.821067743081869, 48.668399155996212 ], [ 33.580410597071364, 48.559413561086956 ], [ 33.4454317558708, 48.525307115131227 ], [ 33.443261346433985, 48.504274807136596 ], [ 33.461554803311515, 48.497763577027456 ], [ 33.484499139223885, 48.443916734269862 ], [ 33.471476678106171, 48.422961941540336 ], [ 33.488529900634376, 48.403945014251008 ], [ 33.500002069040249, 48.311211656192938 ], [ 33.525943637588796, 48.297414048719361 ], [ 33.513386265363863, 48.238502915776905 ], [ 33.470546502119362, 48.227986761779619 ], [ 33.40290205278751, 48.175250963061444 ], [ 33.348125034043051, 48.185947984212078 ], [ 33.268801710730372, 48.102723089798758 ], [ 33.241981642139081, 48.173907375924614 ], [ 33.2222929221806, 48.090449938413883 ], [ 33.111291944767402, 48.064198309704182 ], [ 33.057496778853249, 48.025802720818888 ], [ 33.035275913352677, 47.966529853569853 ], [ 32.975692986841864, 48.044509588846438 ], [ 32.888204787021266, 48.032158922196459 ], [ 32.882778761630505, 47.993970037986855 ], [ 32.844434848689275, 47.986218574427653 ], [ 32.829345330922251, 47.966684882301479 ], [ 32.777565544814024, 47.989319159851334 ], [ 32.696071812064474, 47.970302232561949 ], [ 32.681964146228324, 47.943223782451582 ], [ 32.714727004147903, 47.929736233340464 ], [ 32.715760532022898, 47.91521515635435 ], [ 32.662740513363985, 47.900254827997912 ], [ 32.68227420369152, 47.845865382881072 ], [ 32.651991815369968, 47.83733877206663 ], [ 32.644550409273961, 47.800648505524066 ], [ 32.467041864090731, 47.781088975875491 ], [ 32.342915072772371, 47.796178494541891 ], [ 32.287517938202257, 47.822455959874617 ], [ 32.234446241800583, 47.820440579169372 ], [ 32.169127232435812, 47.757705390391436 ], [ 32.138689812684675, 47.747525133178328 ], [ 32.021849399630071, 47.809846909906923 ], [ 31.862324252961912, 47.795842596858336 ], [ 31.839069858687026, 47.845968736567897 ], [ 31.863409457680291, 47.89438955303541 ], [ 31.821241488903581, 47.945187486313444 ], [ 31.768169793401228, 47.952577216465329 ], [ 31.708070102952888, 48.019756577803776 ], [ 31.769254999018983, 48.047041734388529 ], [ 31.758041213031561, 48.09541087491192 ], [ 31.69861331615158, 48.093757229412574 ], [ 31.61655114082248, 48.118742784451911 ], [ 31.591281365842349, 48.109570216692134 ], [ 31.584873487621337, 48.086212470529063 ], [ 31.533662144092659, 48.086780911309972 ], [ 31.511803012898724, 48.055051581265445 ], [ 31.499710727767877, 48.063888251341723 ], [ 31.503741489178367, 48.118096829305216 ], [ 31.414496291070975, 48.107709866517098 ], [ 31.384265577794167, 48.124943956198592 ], [ 31.343906284147693, 48.111792303871709 ], [ 31.250320265367918, 48.120939033209766 ], [ 31.231096633402899, 48.131636054360399 ], [ 31.228151075811468, 48.158895372523375 ], [ 31.189807162870238, 48.171478583170028 ], [ 31.185466343097232, 48.205068264288968 ], [ 31.168568150199974, 48.211682848084934 ], [ 31.041805860552017, 48.21912425418094 ], [ 30.906155225783039, 48.157112534915541 ], [ 30.857114291691175, 48.186258043473913 ], [ 30.816961703619711, 48.159076240576042 ], [ 30.789004754365919, 48.177783107704215 ], [ 30.704772169599948, 48.181581326017351 ], [ 30.619867791265563, 48.145046088206357 ], [ 30.501787143861577, 48.163572089080617 ], [ 30.347532992953234, 48.163572089080617 ], [ 30.322314893917223, 48.134607449474174 ], [ 30.083466423936272, 48.143728339491247 ], [ 30.04780968616808, 48.155200506997801 ], [ 29.996753371370971, 48.220984605255296 ], [ 29.959442987204056, 48.209589952114584 ], [ 29.924096306899003, 48.221553046036206 ], [ 29.882186721440007, 48.18000519488379 ], [ 29.825859409084444, 48.210106716052053 ], [ 29.774803094287336, 48.203104560427107 ], [ 29.786430291424779, 48.250801907382083 ], [ 29.738939650044813, 48.267079983554368 ], [ 29.775423211012367, 48.316896064002151 ], [ 29.779298943691288, 48.35309540502891 ], [ 29.883426954890012, 48.432599596394255 ], [ 29.916964959165512, 48.428310452565313 ], [ 29.940064324708771, 48.375600491369539 ], [ 29.998096957608482, 48.42205760397519 ], [ 30.038766309617472, 48.427690334940962 ], [ 30.076128370627771, 48.455078844313164 ], [ 30.120828484946628, 48.45639659302833 ], [ 30.184752232129767, 48.492492581267584 ], [ 30.247280715332749, 48.47755809223213 ], [ 30.384946729907654, 48.530526434946978 ], [ 30.408821241806834, 48.570213935025038 ], [ 30.456466912817746, 48.570782375805948 ], [ 30.484423862071537, 48.55352244680347 ], [ 30.57408247222827, 48.565149644840233 ], [ 30.576097852933572, 48.604320380081447 ], [ 30.540906203158727, 48.619254869116901 ], [ 30.599403924051842, 48.659975897969275 ], [ 30.606845331047168, 48.714520371817684 ], [ 30.702291700901242, 48.765189113886493 ], [ 30.820062289942712, 48.749789536857634 ], [ 30.93814293824596, 48.756223253500309 ], [ 31.067385694793984, 48.727413641726173 ], [ 31.223655226407573, 48.753871975111565 ], [ 31.309128044623549, 48.742813218755032 ], [ 31.332899203735224, 48.723847968309087 ], [ 31.462296990813456, 48.761106676531881 ], [ 31.481210565315337, 48.803481349984281 ], [ 31.527305942715088, 48.835210680028808 ], [ 31.529476353051223, 48.904043687765977 ], [ 31.544100782824842, 48.924197495717806 ], [ 31.651122673872294, 48.917427883190214 ], [ 31.683575474328677, 48.943007717432181 ], [ 31.732306349158648, 48.942645982226281 ], [ 31.841550327385676, 48.909934801150143 ], [ 31.886715528798618, 48.85975698549646 ], [ 32.026190220302396, 48.928047389975063 ], [ 32.129233025883423, 48.912518621736979 ], [ 32.142565545363595, 48.975615545720814 ], [ 32.192536655442268, 48.999774278460166 ], [ 32.241422559903185, 49.052044990084198 ], [ 32.284572380610882, 49.047962550930947 ], [ 32.288758171652262, 49.070028387699892 ], [ 32.331907993259279, 49.089768785401077 ], [ 32.369115024638631, 49.045792141494132 ], [ 32.452934198254638, 49.056127428338129 ], [ 32.454484490966479, 49.02160757123238 ], [ 32.503525425058285, 48.9730834010781 ], [ 32.51980350123057, 48.929106757171098 ], [ 32.547088656915946, 48.929597682686904 ], [ 32.567759229704677, 48.957942206467635 ], [ 32.634318475217412, 48.946004950967676 ], [ 32.689405552324388, 48.987449449332587 ], [ 32.782371454379074, 48.963729967064296 ], [ 32.855441929101744, 49.018403632121874 ], [ 32.82469445188741, 49.030392564465274 ], [ 32.800354851994825, 49.07599701634922 ], [ 32.789502801213303, 49.146767890425792 ], [ 32.752554152252287, 49.181287747531485 ], [ 32.788727654857382, 49.261799628350161 ] ] ] } }, -{ "type": "Feature", "properties": { "ISO": "UA-30", "NAME_1": "Kiev City" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 30.738579569391618, 50.668153050722708 ], [ 30.834853259770114, 50.63879710530216 ], [ 30.842339119354847, 50.583978551369 ], [ 30.77560546292176, 50.544198930136474 ], [ 30.750571941641795, 50.506299492727408 ], [ 30.768079276837113, 50.488703509559286 ], [ 30.760252259434196, 50.43988011157785 ], [ 30.817012763528169, 50.367226506797806 ], [ 30.846045721033192, 50.353492985013247 ], [ 30.830268158049023, 50.332724846574365 ], [ 30.790160822064252, 50.332587875330148 ], [ 30.791965799182265, 50.292555746950143 ], [ 30.753676885180084, 50.2891579715631 ], [ 30.724343095457471, 50.309563514144827 ], [ 30.718108877222903, 50.262483327843256 ], [ 30.605901274923895, 50.267452462561096 ], [ 30.654383518684426, 50.155983494142902 ], [ 30.639759213017271, 50.12409198125988 ], [ 30.620784347019992, 50.122712035432073 ], [ 30.648197621922691, 50.03369265169448 ], [ 30.585565493652609, 50.014154717466226 ], [ 30.568478920665655, 50.091072387580596 ], [ 30.541697786423697, 50.097418012031596 ], [ 30.536180761333071, 50.145639750973714 ], [ 30.511085473715639, 50.146179644574886 ], [ 30.520806118907217, 50.190074235237716 ], [ 30.476911529143763, 50.199794880429351 ], [ 30.445622347093604, 50.263568562239357 ], [ 30.41059423723425, 50.261989207036265 ], [ 30.413718066535523, 50.303914962305612 ], [ 30.332238312847835, 50.368768432217337 ], [ 30.318757298210699, 50.40827410826347 ], [ 30.238241797620844, 50.414672186572147 ], [ 30.223303712196525, 50.382020633245588 ], [ 30.19660342970451, 50.374352670839073 ], [ 30.185993737221168, 50.387245468603453 ], [ 30.20028059567295, 50.478145037963088 ], [ 30.217438779875749, 50.542431728141707 ], [ 30.258627859088392, 50.565877710388008 ], [ 30.249909840166993, 50.600723265965939 ], [ 30.267873699111931, 50.608004352618764 ], [ 30.285561019225611, 50.588952523540115 ], [ 30.291309143102467, 50.609275474183733 ], [ 30.270164967539188, 50.63339306383881 ], [ 30.333473963352731, 50.637918952397683 ], [ 30.334013856054582, 50.663014239115796 ], [ 30.432651793045807, 50.66244745938269 ], [ 30.437266331376065, 50.634241784630603 ], [ 30.462512034202973, 50.630365880181216 ], [ 30.475767426025868, 50.595864218159136 ], [ 30.548079872987728, 50.548982771237888 ], [ 30.538686942548338, 50.571958808252361 ], [ 30.551813308637236, 50.580783641451887 ], [ 30.647182495975414, 50.575179282297995 ], [ 30.664327235313635, 50.602694650937337 ], [ 30.718544131106739, 50.62352455391499 ], [ 30.71559759865022, 50.651805437171163 ], [ 30.738579569391618, 50.668153050722708 ] ] ] } } +{ "type": "Feature", "properties": { "ISO": "UA-30", "NAME_1": "Kyiv" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 30.738579569391618, 50.668153050722708 ], [ 30.834853259770114, 50.63879710530216 ], [ 30.842339119354847, 50.583978551369 ], [ 30.77560546292176, 50.544198930136474 ], [ 30.750571941641795, 50.506299492727408 ], [ 30.768079276837113, 50.488703509559286 ], [ 30.760252259434196, 50.43988011157785 ], [ 30.817012763528169, 50.367226506797806 ], [ 30.846045721033192, 50.353492985013247 ], [ 30.830268158049023, 50.332724846574365 ], [ 30.790160822064252, 50.332587875330148 ], [ 30.791965799182265, 50.292555746950143 ], [ 30.753676885180084, 50.2891579715631 ], [ 30.724343095457471, 50.309563514144827 ], [ 30.718108877222903, 50.262483327843256 ], [ 30.605901274923895, 50.267452462561096 ], [ 30.654383518684426, 50.155983494142902 ], [ 30.639759213017271, 50.12409198125988 ], [ 30.620784347019992, 50.122712035432073 ], [ 30.648197621922691, 50.03369265169448 ], [ 30.585565493652609, 50.014154717466226 ], [ 30.568478920665655, 50.091072387580596 ], [ 30.541697786423697, 50.097418012031596 ], [ 30.536180761333071, 50.145639750973714 ], [ 30.511085473715639, 50.146179644574886 ], [ 30.520806118907217, 50.190074235237716 ], [ 30.476911529143763, 50.199794880429351 ], [ 30.445622347093604, 50.263568562239357 ], [ 30.41059423723425, 50.261989207036265 ], [ 30.413718066535523, 50.303914962305612 ], [ 30.332238312847835, 50.368768432217337 ], [ 30.318757298210699, 50.40827410826347 ], [ 30.238241797620844, 50.414672186572147 ], [ 30.223303712196525, 50.382020633245588 ], [ 30.19660342970451, 50.374352670839073 ], [ 30.185993737221168, 50.387245468603453 ], [ 30.20028059567295, 50.478145037963088 ], [ 30.217438779875749, 50.542431728141707 ], [ 30.258627859088392, 50.565877710388008 ], [ 30.249909840166993, 50.600723265965939 ], [ 30.267873699111931, 50.608004352618764 ], [ 30.285561019225611, 50.588952523540115 ], [ 30.291309143102467, 50.609275474183733 ], [ 30.270164967539188, 50.63339306383881 ], [ 30.333473963352731, 50.637918952397683 ], [ 30.334013856054582, 50.663014239115796 ], [ 30.432651793045807, 50.66244745938269 ], [ 30.437266331376065, 50.634241784630603 ], [ 30.462512034202973, 50.630365880181216 ], [ 30.475767426025868, 50.595864218159136 ], [ 30.548079872987728, 50.548982771237888 ], [ 30.538686942548338, 50.571958808252361 ], [ 30.551813308637236, 50.580783641451887 ], [ 30.647182495975414, 50.575179282297995 ], [ 30.664327235313635, 50.602694650937337 ], [ 30.718544131106739, 50.62352455391499 ], [ 30.71559759865022, 50.651805437171163 ], [ 30.738579569391618, 50.668153050722708 ] ] ] } }, +{ "type": "Feature", "properties": { "ISO": "UA-43", "NAME_1": "Crimea" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 33.745771311300061, 44.402323389538829 ], [ 33.852068870481958, 44.431631863719531 ], [ 33.805710906333218, 44.527306810660775 ], [ 33.712994978035795, 44.581555492092264 ], [ 33.709049619844052, 44.666380702207618 ], [ 33.611401992907645, 44.720629383639107 ], [ 33.67452773116986, 44.791645839184127 ], [ 33.588428605039795, 44.842004786558498 ], [ 33.612207031250023, 44.9078125 ], [ 33.601171875, 44.981494140624996 ], [ 33.55517578125, 45.09765625 ], [ 33.392480468750023, 45.187841796874999 ], [ 33.261523437500017, 45.170751953124999 ], [ 33.186914062500023, 45.194775390624997 ], [ 32.918652343750011, 45.34814453125 ], [ 32.772656250000011, 45.358984375 ], [ 32.611328125, 45.328076171874997 ], [ 32.551855468750006, 45.350390625 ], [ 32.508007812500011, 45.40380859375 ], [ 32.828027343750023, 45.593017578125 ], [ 33.142285156250011, 45.74921875 ], [ 33.280078125000017, 45.765234375 ], [ 33.466210937500023, 45.837939453124996 ], [ 33.664843750000017, 45.947070312499996 ], [ 33.63671875, 46.032861328124994 ], [ 33.594140625000023, 46.096240234374996 ], [ 33.654322809145384, 46.146221891615326 ], [ 33.659965189140848, 46.21957283155632 ], [ 33.806667069022836, 46.208288071565399 ], [ 34.026719888845811, 46.106725231647104 ], [ 34.128282728764113, 46.089798091660718 ], [ 34.22420318868695, 46.10108285165164 ], [ 34.353977928582552, 46.061586191683411 ], [ 34.449898388505389, 45.965665731760573 ], [ 34.68687834831475, 45.976950491751495 ], [ 34.794083568228508, 45.89231479181958 ], [ 34.799725948223973, 45.790751951901285 ], [ 34.946427828105953, 45.728685771951213 ], [ 35.001674239407095, 45.733383205653169 ], [ 35.260156250000023, 45.446923828124994 ], [ 35.373925781250023, 45.353613281249999 ], [ 35.45751953125, 45.316308593749994 ], [ 35.558007812500023, 45.310888671874999 ], [ 35.7509765625, 45.389355468749997 ], [ 35.83349609375, 45.401611328125 ], [ 36.012890625000011, 45.371679687499999 ], [ 36.0771484375, 45.424121093749996 ], [ 36.170507812500006, 45.453076171874997 ], [ 36.290332031250017, 45.456738281249997 ], [ 36.575, 45.3935546875 ], [ 36.45078125, 45.232324218749994 ], [ 36.393359375000017, 45.065380859374997 ], [ 36.229882812500023, 45.025976562499999 ], [ 36.054785156250006, 45.030810546874996 ], [ 35.8701171875, 45.005322265624997 ], [ 35.677539062500017, 45.102001953124997 ], [ 35.569531250000011, 45.119335937499997 ], [ 35.472558593750023, 45.098486328124999 ], [ 35.357812500000023, 44.978417968749994 ], [ 35.15478515625, 44.896337890624999 ], [ 35.087695312500017, 44.802636718749994 ], [ 34.887792968750006, 44.823583984374999 ], [ 34.716894531250006, 44.80712890625 ], [ 34.469921875000011, 44.7216796875 ], [ 34.28173828125, 44.538427734374999 ], [ 34.074414062500011, 44.423828125 ], [ 33.909960937500017, 44.387597656249994 ], [ 33.745771311300061, 44.402323389538829 ] ] ] } }, +{ "type": "Feature", "properties": { "ISO": "UA-40", "NAME_1": "Sevastopol" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 33.588428605039795, 44.842004786558498 ], [ 33.67452773116986, 44.791645839184127 ], [ 33.611401992907645, 44.720629383639107 ], [ 33.709049619844052, 44.666380702207618 ], [ 33.712994978035795, 44.581555492092264 ], [ 33.805710906333218, 44.527306810660775 ], [ 33.852068870481958, 44.431631863719531 ], [ 33.745771311300061, 44.402323389538829 ], [ 33.655859375, 44.433203125 ], [ 33.45068359375, 44.553662109374997 ], [ 33.462695312500017, 44.596826171874994 ], [ 33.530078125000017, 44.680517578124999 ], [ 33.588428605039795, 44.842004786558498 ] ] ] } } ] } diff --git a/superset-frontend/plugins/legacy-plugin-chart-heatmap/src/Heatmap.js b/superset-frontend/plugins/legacy-plugin-chart-heatmap/src/Heatmap.js index 18493f0602ef..ef2c76ad68a3 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-heatmap/src/Heatmap.js +++ b/superset-frontend/plugins/legacy-plugin-chart-heatmap/src/Heatmap.js @@ -177,15 +177,12 @@ function Heatmap(element, props) { } } - function ordScale(k, rangeBands, sortMethod) { + function ordScale(k, rangeBands, sortMethod, formatter) { let domain = {}; - const actualKeys = {}; // hack to preserve type of keys when number records.forEach(d => { domain[d[k]] = (domain[d[k]] || 0) + d.v; - actualKeys[d[k]] = d[k]; }); - // Not using object.keys() as it converts to strings - const keys = Object.keys(actualKeys).map(s => actualKeys[s]); + const keys = Object.keys(domain).map(k => formatter(k)); if (sortMethod === 'alpha_asc') { domain = keys.sort(cmp); } else if (sortMethod === 'alpha_desc') { @@ -252,10 +249,10 @@ function Heatmap(element, props) { const fp = getNumberFormatter(NumberFormats.PERCENT_2_POINT); - const xScale = ordScale('x', null, sortXAxis); - const yScale = ordScale('y', null, sortYAxis); - const xRbScale = ordScale('x', [0, hmWidth], sortXAxis); - const yRbScale = ordScale('y', [hmHeight, 0], sortYAxis); + const xScale = ordScale('x', null, sortXAxis, xAxisFormatter); + const yScale = ordScale('y', null, sortYAxis, yAxisFormatter); + const xRbScale = ordScale('x', [0, hmWidth], sortXAxis, xAxisFormatter); + const yRbScale = ordScale('y', [hmHeight, 0], sortYAxis, yAxisFormatter); const X = 0; const Y = 1; const heatmapDim = [xRbScale.domain().length, yRbScale.domain().length]; diff --git a/superset-frontend/plugins/legacy-plugin-chart-heatmap/src/transformProps.js b/superset-frontend/plugins/legacy-plugin-chart-heatmap/src/transformProps.js index 0ec12b6c4d37..8b925f297405 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-heatmap/src/transformProps.js +++ b/superset-frontend/plugins/legacy-plugin-chart-heatmap/src/transformProps.js @@ -57,11 +57,15 @@ export default function transformProps(chartProps) { const xAxisFormatter = coltypes[0] === GenericDataType.Temporal ? getTimeFormatter(timeFormat) - : String; + : coltypes[0] === GenericDataType.Numeric + ? Number + : String; const yAxisFormatter = coltypes[1] === GenericDataType.Temporal ? getTimeFormatter(timeFormat) - : String; + : coltypes[1] === GenericDataType.Numeric + ? Number + : String; return { width, height, diff --git a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/Area/index.js b/superset-frontend/plugins/legacy-preset-chart-nvd3/src/Area/index.js index b85a64579240..2f69301b5125 100644 --- a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/Area/index.js +++ b/superset-frontend/plugins/legacy-preset-chart-nvd3/src/Area/index.js @@ -39,7 +39,7 @@ const metadata = new ChartMetadata({ { url: example4, caption: t('Vehicle Types') }, ], label: ChartLabel.Deprecated, - name: t('Area Chart (legacy)'), + name: t('Time-series Area Chart (legacy)'), supportedAnnotationTypes: [ANNOTATION_TYPES.INTERVAL, ANNOTATION_TYPES.EVENT], tags: [ t('Aesthetic'), diff --git a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/Line/index.js b/superset-frontend/plugins/legacy-preset-chart-nvd3/src/Line/index.js index c848e7accd7b..fefd48021907 100644 --- a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/Line/index.js +++ b/superset-frontend/plugins/legacy-preset-chart-nvd3/src/Line/index.js @@ -36,7 +36,7 @@ const metadata = new ChartMetadata({ { url: battery, caption: t('Battery level over time') }, ], label: ChartLabel.Deprecated, - name: t('Line Chart (legacy)'), + name: t('Time-series Line Chart (legacy)'), supportedAnnotationTypes: [ ANNOTATION_TYPES.TIME_SERIES, ANNOTATION_TYPES.INTERVAL, diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Gauge/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Gauge/transformProps.ts index 269e2a8a1869..09eed22b973c 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Gauge/transformProps.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Gauge/transformProps.ts @@ -48,11 +48,12 @@ import { getDefaultTooltip } from '../utils/tooltip'; import { Refs } from '../types'; import { getColtypesMapping } from '../utils/series'; -const setIntervalBoundsAndColors = ( +export const getIntervalBoundsAndColors = ( intervals: string, intervalColorIndices: string, colorFn: CategoricalColorScale, - normalizer: number, + min: number, + max: number, ): Array<[number, string]> => { let intervalBoundsNonNormalized; let intervalColorIndicesArray; @@ -65,7 +66,7 @@ const setIntervalBoundsAndColors = ( } const intervalBounds = intervalBoundsNonNormalized.map( - bound => bound / normalizer, + bound => (bound - min) / (max - min), ); const intervalColors = intervalColorIndicesArray.map( ind => colorFn.colors[(ind - 1) % colorFn.colors.length], @@ -221,12 +222,12 @@ export default function transformProps( const axisLabelLength = Math.max( ...axisLabels.map(label => numberFormatter(label).length).concat([1]), ); - const normalizer = max; - const intervalBoundsAndColors = setIntervalBoundsAndColors( + const intervalBoundsAndColors = getIntervalBoundsAndColors( intervals, intervalColorIndices, colorFn, - normalizer, + min, + max, ); const splitLineDistance = axisLineWidth + splitLineLength + OFFSETS.ticksFromLine; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/transformProps.ts index bb719c23e1ad..a0aa94f3610f 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/transformProps.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/transformProps.ts @@ -388,6 +388,7 @@ export default function transformProps( seriesType, showValue, stack: Boolean(stack), + stackIdSuffix: '\na', yAxisIndex, filterState, seriesKey: entry.name, @@ -410,8 +411,9 @@ export default function transformProps( rawSeriesB.forEach(entry => { const entryName = String(entry.name || ''); - const seriesName = `${inverted[entryName] || entryName} (1)`; - const colorScaleKey = getOriginalSeries(seriesName, array); + const seriesEntry = inverted[entryName] || entryName; + const seriesName = `${seriesEntry} (1)`; + const colorScaleKey = getOriginalSeries(seriesEntry, array); const seriesFormatter = getFormatter( customFormattersSecondary, @@ -433,6 +435,7 @@ export default function transformProps( seriesType: seriesTypeB, showValue: showValueB, stack: Boolean(stackB), + stackIdSuffix: '\nb', yAxisIndex: yAxisIndexB, filterState, seriesKey: primarySeries.has(entry.name as string) diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/EchartsTimeseries.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/EchartsTimeseries.tsx index 78a57367086a..14ec5ac32d8a 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/EchartsTimeseries.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/EchartsTimeseries.tsx @@ -25,6 +25,7 @@ import { getColumnLabel, getNumberFormatter, LegendState, + ensureIsArray, } from '@superset-ui/core'; import { ViewRootGroup } from 'echarts/types/src/util/types'; import GlobalModel from 'echarts/types/src/model/Global'; @@ -173,6 +174,7 @@ export default function EchartsTimeseries({ ...(eventParams.name ? [eventParams.name] : []), ...(labelMap[seriesName] ?? []), ]; + const groupBy = ensureIsArray(formData.groupby); if (data && xAxis.type === AxisType.Time) { drillToDetailFilters.push({ col: @@ -188,7 +190,7 @@ export default function EchartsTimeseries({ } [ ...(xAxis.type === AxisType.Category && data ? [xAxis.label] : []), - ...formData.groupby, + ...groupBy, ].forEach((dimension, i) => drillToDetailFilters.push({ col: dimension, @@ -197,7 +199,7 @@ export default function EchartsTimeseries({ formattedVal: String(values[i]), }), ); - formData.groupby.forEach((dimension, i) => { + groupBy.forEach((dimension, i) => { const val = labelMap[seriesName][i]; drillByFilters.push({ col: dimension, diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/buildQuery.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/buildQuery.ts index 69a8020657b8..781d5678e225 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/buildQuery.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/buildQuery.ts @@ -78,6 +78,10 @@ export default function buildQuery(formData: QueryFormData) { ...ensureIsArray(groupby), ]; + const time_offsets = isTimeComparison(formData, baseQueryObject) + ? formData.time_compare + : []; + return [ { ...baseQueryObject, @@ -87,9 +91,7 @@ export default function buildQuery(formData: QueryFormData) { ...(isXAxisSet(formData) ? {} : { is_timeseries: true }), // todo: move `normalizeOrderBy to extractQueryFields` orderby: normalizeOrderBy(baseQueryObject).orderby, - time_offsets: isTimeComparison(formData, baseQueryObject) - ? formData.time_compare - : [], + time_offsets, /* Note that: 1. The resample, rolling, cum, timeCompare operators should be after pivot. 2. the flatOperator makes multiIndex Dataframe into flat Dataframe @@ -100,7 +102,7 @@ export default function buildQuery(formData: QueryFormData) { timeCompareOperator(formData, baseQueryObject), resampleOperator(formData, baseQueryObject), renameOperator(formData, baseQueryObject), - contributionOperator(formData, baseQueryObject), + contributionOperator(formData, baseQueryObject, time_offsets), sortOperator(formData, baseQueryObject), flattenOperator(formData, baseQueryObject), // todo: move prophet before flatten diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts index 3ede7810d920..3c12de9a3a63 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts @@ -43,9 +43,10 @@ import { extractExtraMetrics, getOriginalSeries, isDerivedSeries, + getTimeOffset, } from '@superset-ui/chart-controls'; import { EChartsCoreOption, SeriesOption } from 'echarts'; -import { ZRLineType } from 'echarts/types/src/util/types'; +import { LineStyleOption } from 'echarts/types/src/util/types'; import { EchartsTimeseriesChartProps, EchartsTimeseriesFormData, @@ -184,10 +185,10 @@ export default function transformProps( zoomable, }: EchartsTimeseriesFormData = { ...DEFAULT_FORM_DATA, ...formData }; const refs: Refs = {}; - + const groupBy = ensureIsArray(groupby); const labelMap = Object.entries(label_map).reduce((acc, entry) => { if ( - entry[1].length > groupby.length && + entry[1].length > groupBy.length && Array.isArray(timeCompare) && timeCompare.includes(entry[1][0]) ) { @@ -219,7 +220,7 @@ export default function transformProps( getMetricLabel, ); - const isMultiSeries = groupby?.length || metrics?.length > 1; + const isMultiSeries = groupBy.length || metrics?.length > 1; const [rawSeries, sortedTotalValues, minPositiveValue] = extractSeries( rebasedData, @@ -269,10 +270,22 @@ export default function transformProps( const array = ensureIsArray(chartProps.rawFormData?.time_compare); const inverted = invert(verboseMap); + const offsetLineWidths = {}; + rawSeries.forEach(entry => { - const lineStyle = isDerivedSeries(entry, chartProps.rawFormData) - ? { type: 'dashed' as ZRLineType } - : {}; + const derivedSeries = isDerivedSeries(entry, chartProps.rawFormData); + const lineStyle: LineStyleOption = {}; + if (derivedSeries) { + const offset = getTimeOffset( + entry, + ensureIsArray(chartProps.rawFormData?.time_compare), + )!; + if (!offsetLineWidths[offset]) { + offsetLineWidths[offset] = Object.keys(offsetLineWidths).length + 1; + } + lineStyle.type = 'dashed'; + lineStyle.width = offsetLineWidths[offset]; + } const entryName = String(entry.name || ''); const seriesName = inverted[entryName] || entryName; @@ -284,6 +297,7 @@ export default function transformProps( colorScaleKey, { area, + connectNulls: derivedSeries, filterState, seriesContexts, markerEnabled, @@ -499,7 +513,6 @@ export default function transformProps( if (isHorizontal) { [xAxis, yAxis] = [yAxis, xAxis]; [padding.bottom, padding.left] = [padding.left, padding.bottom]; - yAxis.inverse = true; } const echartOptions: EChartsCoreOption = { @@ -537,7 +550,7 @@ export default function transformProps( // if there are no dimensions, key is a verbose name of a metric, // otherwise it is a comma separated string where the first part is metric name const formatterKey = - groupby.length === 0 ? inverted[key] : labelMap[key]?.[0]; + groupBy.length === 0 ? inverted[key] : labelMap[key]?.[0]; const content = formatForecastTooltipSeries({ ...value, seriesName: key, @@ -575,7 +588,7 @@ export default function transformProps( right: TIMESERIES_CONSTANTS.toolboxRight, feature: { dataZoom: { - yAxisIndex: false, + ...(stack ? { yAxisIndex: false } : {}), // disable y-axis zoom for stacked charts title: { zoom: t('zoom area'), back: t('restore zoom'), @@ -590,6 +603,7 @@ export default function transformProps( start: TIMESERIES_CONSTANTS.dataZoomStart, end: TIMESERIES_CONSTANTS.dataZoomEnd, bottom: TIMESERIES_CONSTANTS.zoomBottom, + yAxisIndex: isHorizontal ? 0 : undefined, }, ] : [], @@ -603,7 +617,7 @@ export default function transformProps( echartOptions, emitCrossFilters, formData, - groupby, + groupby: groupBy, height, labelMap, selectedValues, diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformers.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformers.ts index 5b9fa43047a3..bc441da3c0db 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformers.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformers.ts @@ -143,6 +143,7 @@ export function transformSeries( colorScaleKey: string, opts: { area?: boolean; + connectNulls?: boolean; filterState?: FilterState; seriesContexts?: { [key: string]: ForecastSeriesEnum[] }; markerEnabled?: boolean; @@ -150,6 +151,7 @@ export function transformSeries( areaOpacity?: number; seriesType?: EchartsTimeseriesSeriesType; stack?: StackType; + stackIdSuffix?: string; yAxisIndex?: number; showValue?: boolean; onlyTotal?: boolean; @@ -169,6 +171,7 @@ export function transformSeries( const { name } = series; const { area, + connectNulls, filterState, seriesContexts = {}, markerEnabled, @@ -176,6 +179,7 @@ export function transformSeries( areaOpacity = 1, seriesType, stack, + stackIdSuffix, yAxisIndex = 0, showValue, onlyTotal, @@ -221,6 +225,9 @@ export function transformSeries( } else if (stack && isTrend) { stackId = forecastSeries.type; } + if (stackId && stackIdSuffix) { + stackId += stackIdSuffix; + } let plotType; if ( !isConfidenceBand && @@ -266,6 +273,7 @@ export function transformSeries( : { ...opts.lineStyle, opacity }; return { ...series, + connectNulls, queryIndex, yAxisIndex, name: forecastSeries.name, @@ -561,6 +569,7 @@ export function getPadding( ? TIMESERIES_CONSTANTS.yAxisLabelTopOffset : 0; const xAxisOffset = addXAxisTitleOffset ? Number(xAxisTitleMargin) || 0 : 0; + return getChartPadding( showLegend, legendOrientation, @@ -570,9 +579,10 @@ export function getPadding( yAxisTitlePosition && yAxisTitlePosition === 'Top' ? TIMESERIES_CONSTANTS.gridOffsetTop + (Number(yAxisTitleMargin) || 0) : TIMESERIES_CONSTANTS.gridOffsetTop + yAxisOffset, - bottom: zoomable - ? TIMESERIES_CONSTANTS.gridOffsetBottomZoomable + xAxisOffset - : TIMESERIES_CONSTANTS.gridOffsetBottom + xAxisOffset, + bottom: + zoomable && !isHorizontal + ? TIMESERIES_CONSTANTS.gridOffsetBottomZoomable + xAxisOffset + : TIMESERIES_CONSTANTS.gridOffsetBottom + xAxisOffset, left: yAxisTitlePosition === 'Left' ? TIMESERIES_CONSTANTS.gridOffsetLeft + diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/types.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/types.ts index dbecb483dac2..6ca9650db62e 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/types.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/types.ts @@ -21,7 +21,6 @@ import { AnnotationLayer, AxisType, ContributionType, - QueryFormColumn, QueryFormData, QueryFormMetric, TimeFormatter, @@ -87,7 +86,6 @@ export type EchartsTimeseriesFormData = QueryFormData & { zoomable: boolean; richTooltip: boolean; xAxisLabelRotation: number; - groupby: QueryFormColumn[]; showValue: boolean; onlyTotal: boolean; showExtraControls: boolean; diff --git a/superset-frontend/plugins/plugin-chart-echarts/test/Gauge/transformProps.test.ts b/superset-frontend/plugins/plugin-chart-echarts/test/Gauge/transformProps.test.ts index 915a8b9e9dd3..760e3ff93c2b 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/test/Gauge/transformProps.test.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/test/Gauge/transformProps.test.ts @@ -16,8 +16,15 @@ * specific language governing permissions and limitations * under the License. */ -import { ChartProps, SqlaFormData, supersetTheme } from '@superset-ui/core'; -import transformProps from '../../src/Gauge/transformProps'; +import { + CategoricalColorNamespace, + ChartProps, + SqlaFormData, + supersetTheme, +} from '@superset-ui/core'; +import transformProps, { + getIntervalBoundsAndColors, +} from '../../src/Gauge/transformProps'; import { EchartsGaugeChartProps } from '../../src/Gauge/types'; describe('Echarts Gauge transformProps', () => { @@ -256,8 +263,9 @@ describe('Echarts Gauge transformProps', () => { const formData: SqlaFormData = { ...baseFormData, groupby: ['year', 'platform'], - intervals: '50,100', + intervals: '60,100', intervalColorIndices: '1,2', + minVal: 20, }; const queriesData = [ { @@ -342,3 +350,43 @@ describe('Echarts Gauge transformProps', () => { ); }); }); + +describe('getIntervalBoundsAndColors', () => { + it('should generate correct interval bounds and colors', () => { + const colorFn = CategoricalColorNamespace.getScale( + 'supersetColors' as string, + ); + expect(getIntervalBoundsAndColors('', '', colorFn, 0, 10)).toEqual([]); + expect(getIntervalBoundsAndColors('4, 10', '1, 2', colorFn, 0, 10)).toEqual( + [ + [0.4, '#1f77b4'], + [1, '#ff7f0e'], + ], + ); + expect( + getIntervalBoundsAndColors('4, 8, 10', '9, 8, 7', colorFn, 0, 10), + ).toEqual([ + [0.4, '#bcbd22'], + [0.8, '#7f7f7f'], + [1, '#e377c2'], + ]); + expect(getIntervalBoundsAndColors('4, 10', '1, 2', colorFn, 2, 10)).toEqual( + [ + [0.25, '#1f77b4'], + [1, '#ff7f0e'], + ], + ); + expect( + getIntervalBoundsAndColors('-4, 0', '1, 2', colorFn, -10, 0), + ).toEqual([ + [0.6, '#1f77b4'], + [1, '#ff7f0e'], + ]); + expect( + getIntervalBoundsAndColors('-4, -2', '1, 2', colorFn, -10, -2), + ).toEqual([ + [0.75, '#1f77b4'], + [1, '#ff7f0e'], + ]); + }); +}); diff --git a/superset-frontend/plugins/plugin-chart-echarts/test/MixedTimeseries/transformProps.test.ts b/superset-frontend/plugins/plugin-chart-echarts/test/MixedTimeseries/transformProps.test.ts new file mode 100644 index 000000000000..422eb6a4805b --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-echarts/test/MixedTimeseries/transformProps.test.ts @@ -0,0 +1,161 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { ChartProps, supersetTheme } from '@superset-ui/core'; +import { + LegendOrientation, + LegendType, + EchartsTimeseriesSeriesType, +} from '@superset-ui/plugin-chart-echarts'; +import transformProps from '../../src/MixedTimeseries/transformProps'; +import { + EchartsMixedTimeseriesFormData, + EchartsMixedTimeseriesProps, +} from '../../src/MixedTimeseries/types'; + +const formData: EchartsMixedTimeseriesFormData = { + annotationLayers: [], + area: false, + areaB: false, + legendMargin: null, + logAxis: false, + logAxisSecondary: false, + markerEnabled: false, + markerEnabledB: false, + markerSize: 0, + markerSizeB: 0, + minorSplitLine: false, + minorTicks: false, + opacity: 0, + opacityB: 0, + orderDesc: false, + orderDescB: false, + richTooltip: false, + rowLimit: 0, + rowLimitB: 0, + legendOrientation: LegendOrientation.Top, + legendType: LegendType.Scroll, + showLegend: false, + showValue: false, + showValueB: false, + stack: true, + stackB: true, + truncateYAxis: false, + truncateYAxisSecondary: false, + xAxisLabelRotation: 0, + xAxisTitle: '', + xAxisTitleMargin: 0, + yAxisBounds: [undefined, undefined], + yAxisBoundsSecondary: [undefined, undefined], + yAxisTitle: '', + yAxisTitleMargin: 0, + yAxisTitlePosition: '', + yAxisTitleSecondary: '', + zoomable: false, + colorScheme: 'bnbColors', + datasource: '3__table', + x_axis: 'ds', + metrics: ['sum__num'], + metricsB: ['sum__num'], + groupby: ['gender'], + groupbyB: ['gender'], + seriesType: EchartsTimeseriesSeriesType.Line, + seriesTypeB: EchartsTimeseriesSeriesType.Bar, + viz_type: 'mixed_timeseries', + forecastEnabled: false, + forecastPeriods: [], + forecastInterval: 0, + forecastSeasonalityDaily: 0, +}; + +const queriesData = [ + { + data: [ + { boy: 1, girl: 2, ds: 599616000000 }, + { boy: 3, girl: 4, ds: 599916000000 }, + ], + label_map: { + ds: ['ds'], + boy: ['boy'], + girl: ['girl'], + }, + }, + { + data: [ + { boy: 1, girl: 2, ds: 599616000000 }, + { boy: 3, girl: 4, ds: 599916000000 }, + ], + label_map: { + ds: ['ds'], + boy: ['boy'], + girl: ['girl'], + }, + }, +]; + +const chartPropsConfig = { + formData, + width: 800, + height: 600, + queriesData, + theme: supersetTheme, +}; + +it('should transform chart props for viz', () => { + const chartProps = new ChartProps(chartPropsConfig); + expect(transformProps(chartProps as EchartsMixedTimeseriesProps)).toEqual( + expect.objectContaining({ + echartOptions: expect.objectContaining({ + series: expect.arrayContaining([ + expect.objectContaining({ + data: [ + [599616000000, 1], + [599916000000, 3], + ], + id: 'boy', + stack: 'obs\na', + }), + expect.objectContaining({ + data: [ + [599616000000, 2], + [599916000000, 4], + ], + id: 'girl', + stack: 'obs\na', + }), + expect.objectContaining({ + data: [ + [599616000000, 1], + [599916000000, 3], + ], + id: 'boy (1)', + stack: 'obs\nb', + }), + expect.objectContaining({ + data: [ + [599616000000, 2], + [599916000000, 4], + ], + id: 'girl (1)', + stack: 'obs\nb', + }), + ]), + }), + }), + ); +}); diff --git a/superset-frontend/plugins/plugin-chart-echarts/test/Timeseries/transformProps.test.ts b/superset-frontend/plugins/plugin-chart-echarts/test/Timeseries/transformProps.test.ts index cda213d72bb6..bbb6271cc3c2 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/test/Timeseries/transformProps.test.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/test/Timeseries/transformProps.test.ts @@ -119,7 +119,6 @@ describe('EchartsTimeseries transformProps', () => { name: 'New York', }), ]), - yAxis: expect.objectContaining({ inverse: true }), }), }), ); diff --git a/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controls/metrics.tsx b/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controls/metrics.tsx index 67a55cff0d3a..dd1516098a53 100644 --- a/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controls/metrics.tsx +++ b/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controls/metrics.tsx @@ -102,7 +102,7 @@ export const showTotalsControlSetItem: ControlSetItem = { name: 'show_totals', config: { type: 'CheckboxControl', - label: t('Show totals'), + label: t('Show summary'), default: false, description: t( 'Show total aggregations of selected metrics. Note that row limit does not apply to the result.', diff --git a/superset-frontend/plugins/plugin-chart-table/package.json b/superset-frontend/plugins/plugin-chart-table/package.json index 6df1ceb33e2c..d43e33808e29 100644 --- a/superset-frontend/plugins/plugin-chart-table/package.json +++ b/superset-frontend/plugins/plugin-chart-table/package.json @@ -37,6 +37,7 @@ "xss": "^1.0.14" }, "peerDependencies": { + "@ant-design/icons": "^5.0.1", "@superset-ui/chart-controls": "*", "@superset-ui/core": "*", "@testing-library/dom": "^7.29.4", diff --git a/superset-frontend/plugins/plugin-chart-table/src/TableChart.tsx b/superset-frontend/plugins/plugin-chart-table/src/TableChart.tsx index 840020cad852..a6a27eb8a377 100644 --- a/superset-frontend/plugins/plugin-chart-table/src/TableChart.tsx +++ b/superset-frontend/plugins/plugin-chart-table/src/TableChart.tsx @@ -48,8 +48,11 @@ import { css, t, tn, + useTheme, } from '@superset-ui/core'; - +import { isNumber } from 'lodash'; +import { InfoCircleOutlined } from '@ant-design/icons'; +import { Tooltip } from '@superset-ui/chart-controls'; import { DataColumnMeta, TableChartTransformedProps } from './types'; import DataTable, { DataTableProps, @@ -168,6 +171,7 @@ function SearchInput({ count, value, onChange }: SearchInputProps) { className="form-control input-sm" placeholder={tn('search.num_records', count)} value={value} + aria-label={t('Search %s records', count)} onChange={onChange} /> @@ -249,6 +253,7 @@ export default function TableChart( }); // keep track of whether column order changed, so that column widths can too const [columnOrderToggle, setColumnOrderToggle] = useState(false); + const theme = useTheme(); // only take relevant page size options const pageSizeOptions = useMemo(() => { @@ -571,7 +576,7 @@ export default function TableChart( /* The following classes are added to support custom CSS styling */ className={cx( 'cell-bar', - value && value < 0 ? 'negative' : 'positive', + isNumber(value) && value < 0 ? 'negative' : 'positive', )} css={cellBarStyles} /> @@ -638,7 +643,27 @@ export default function TableChart( ), Footer: totals ? ( i === 0 ? ( - {t('Totals')} + +
+ {t('Summary')} + + + +
+ ) : ( {formatColumnValue(column, totals[key])[1]} diff --git a/superset-frontend/plugins/plugin-chart-table/src/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-table/src/controlPanel.tsx index ad39b504cbac..e58bb6751a65 100644 --- a/superset-frontend/plugins/plugin-chart-table/src/controlPanel.tsx +++ b/superset-frontend/plugins/plugin-chart-table/src/controlPanel.tsx @@ -339,7 +339,7 @@ const config: ControlPanelConfig = { name: 'show_totals', config: { type: 'CheckboxControl', - label: t('Show totals'), + label: t('Show summary'), default: false, description: t( 'Show total aggregations of selected metrics. Note that row limit does not apply to the result.', diff --git a/superset-frontend/plugins/plugin-chart-table/test/testData.ts b/superset-frontend/plugins/plugin-chart-table/test/testData.ts index 24abc3381e49..6c76e865e801 100644 --- a/superset-frontend/plugins/plugin-chart-table/test/testData.ts +++ b/superset-frontend/plugins/plugin-chart-table/test/testData.ts @@ -80,6 +80,7 @@ const basicQueryResult: ChartDataResponseResult = { is_cached: false, query: 'SELECT ...', rowcount: 100, + sql_rowcount: 100, stacktrace: null, status: 'success', from_dttm: null, diff --git a/superset-frontend/spec/helpers/testing-library.tsx b/superset-frontend/spec/helpers/testing-library.tsx index 9a8649a8623f..8564e62ab897 100644 --- a/superset-frontend/spec/helpers/testing-library.tsx +++ b/superset-frontend/spec/helpers/testing-library.tsx @@ -18,7 +18,13 @@ */ import '@testing-library/jest-dom/extend-expect'; import React, { ReactNode, ReactElement } from 'react'; -import { render, RenderOptions } from '@testing-library/react'; +import { + render, + RenderOptions, + screen, + waitFor, + within, +} from '@testing-library/react'; import { ThemeProvider, supersetTheme } from '@superset-ui/core'; import { BrowserRouter } from 'react-router-dom'; import { Provider } from 'react-redux'; @@ -28,6 +34,7 @@ import reducerIndex from 'spec/helpers/reducerIndex'; import { QueryParamProvider } from 'use-query-params'; import { configureStore, Store } from '@reduxjs/toolkit'; import { api } from 'src/hooks/apiResources/queryApi'; +import userEvent from '@testing-library/user-event'; type Options = Omit & { useRedux?: boolean; @@ -102,3 +109,18 @@ export function sleep(time: number) { export * from '@testing-library/react'; export { customRender as render }; + +export async function selectOption(option: string, selectName?: string) { + const select = screen.getByRole( + 'combobox', + selectName ? { name: selectName } : {}, + ); + await userEvent.click(select); + const item = await waitFor(() => + within( + // eslint-disable-next-line testing-library/no-node-access + document.querySelector('.rc-virtual-list')!, + ).getByText(option), + ); + await userEvent.click(item); +} diff --git a/superset-frontend/src/SqlLab/actions/sqlLab.js b/superset-frontend/src/SqlLab/actions/sqlLab.js index 9cfd76e2e2d2..edc3098884b8 100644 --- a/superset-frontend/src/SqlLab/actions/sqlLab.js +++ b/superset-frontend/src/SqlLab/actions/sqlLab.js @@ -133,11 +133,12 @@ export const convertQueryToClient = fieldConverter(queryClientMapping); export function getUpToDateQuery(rootState, queryEditor, key) { const { - sqlLab: { unsavedQueryEditor }, + sqlLab: { unsavedQueryEditor, queryEditors }, } = rootState; const id = key ?? queryEditor.id; return { - ...queryEditor, + id, + ...queryEditors.find(qe => qe.id === id), ...(id === unsavedQueryEditor.id && unsavedQueryEditor), }; } @@ -743,15 +744,18 @@ export function removeQueryEditor(queryEditor) { return sync .then(() => dispatch({ type: REMOVE_QUERY_EDITOR, queryEditor })) - .catch(() => - dispatch( - addDangerToast( - t( - 'An error occurred while removing tab. Please contact your administrator.', + .catch(({ status }) => { + if (status !== 404) { + return dispatch( + addDangerToast( + t( + 'An error occurred while removing tab. Please contact your administrator.', + ), ), - ), - ), - ); + ); + } + return dispatch({ type: REMOVE_QUERY_EDITOR, queryEditor }); + }); }; } @@ -1127,9 +1131,11 @@ export function removeTables(tables) { const sync = isFeatureEnabled(FeatureFlag.SqllabBackendPersistence) ? Promise.all( tablesToRemove.map(table => - SupersetClient.delete({ - endpoint: encodeURI(`/tableschemaview/${table.id}`), - }), + table.initialized + ? SupersetClient.delete({ + endpoint: encodeURI(`/tableschemaview/${table.id}`), + }) + : Promise.resolve(), ), ) : Promise.resolve(); diff --git a/superset-frontend/src/SqlLab/actions/sqlLab.test.js b/superset-frontend/src/SqlLab/actions/sqlLab.test.js index dd48ed8c7b69..5bb42f758081 100644 --- a/superset-frontend/src/SqlLab/actions/sqlLab.test.js +++ b/superset-frontend/src/SqlLab/actions/sqlLab.test.js @@ -36,6 +36,29 @@ import { const middlewares = [thunk]; const mockStore = configureMockStore(middlewares); +describe('getUpToDateQuery', () => { + test('should return the up to date query editor state', () => { + const outOfUpdatedQueryEditor = { + ...defaultQueryEditor, + schema: null, + sql: 'SELECT ...', + }; + const queryEditor = { + ...defaultQueryEditor, + sql: 'SELECT * FROM table', + }; + const state = { + sqlLab: { + queryEditors: [queryEditor], + unsavedQueryEditor: {}, + }, + }; + expect(actions.getUpToDateQuery(state, outOfUpdatedQueryEditor)).toEqual( + queryEditor, + ); + }); +}); + describe('async actions', () => { const mockBigNumber = '9223372036854775807'; const queryEditor = { @@ -675,7 +698,13 @@ describe('async actions', () => { it('updates the tab state in the backend', () => { expect.assertions(2); - const store = mockStore(initialState); + const store = mockStore({ + ...initialState, + sqlLab: { + ...initialState.sqlLab, + queryEditors: [queryEditor], + }, + }); const request = actions.queryEditorSetAndSaveSql(queryEditor, sql); return request(store.dispatch, store.getState).then(() => { expect(store.getActions()).toEqual(expectedActions); @@ -691,7 +720,13 @@ describe('async actions', () => { feature => !(feature === 'SQLLAB_BACKEND_PERSISTENCE'), ); - const store = mockStore(initialState); + const store = mockStore({ + ...initialState, + sqlLab: { + ...initialState.sqlLab, + queryEditors: [queryEditor], + }, + }); const request = actions.queryEditorSetAndSaveSql(queryEditor, sql); request(store.dispatch, store.getState); @@ -883,7 +918,7 @@ describe('async actions', () => { it('updates the table schema state in the backend', () => { expect.assertions(2); - const table = { id: 1 }; + const table = { id: 1, initialized: true }; const store = mockStore({}); const expectedActions = [ { @@ -900,7 +935,10 @@ describe('async actions', () => { it('deletes multiple tables and updates the table schema state in the backend', () => { expect.assertions(2); - const tables = [{ id: 1 }, { id: 2 }]; + const tables = [ + { id: 1, initialized: true }, + { id: 2, initialized: true }, + ]; const store = mockStore({}); const expectedActions = [ { @@ -913,6 +951,23 @@ describe('async actions', () => { expect(fetchMock.calls(updateTableSchemaEndpoint)).toHaveLength(2); }); }); + + it('only updates the initialized table schema state in the backend', () => { + expect.assertions(2); + + const tables = [{ id: 1 }, { id: 2, initialized: true }]; + const store = mockStore({}); + const expectedActions = [ + { + type: actions.REMOVE_TABLES, + tables, + }, + ]; + return store.dispatch(actions.removeTables(tables)).then(() => { + expect(store.getActions()).toEqual(expectedActions); + expect(fetchMock.calls(updateTableSchemaEndpoint)).toHaveLength(1); + }); + }); }); describe('migrateQueryEditorFromLocalStorage', () => { diff --git a/superset-frontend/src/SqlLab/components/AceEditorWrapper/index.tsx b/superset-frontend/src/SqlLab/components/AceEditorWrapper/index.tsx index a01b1cee76f6..354d6b1c8fb8 100644 --- a/superset-frontend/src/SqlLab/components/AceEditorWrapper/index.tsx +++ b/superset-frontend/src/SqlLab/components/AceEditorWrapper/index.tsx @@ -19,8 +19,10 @@ import React, { useState, useEffect, useRef } from 'react'; import type { IAceEditor } from 'react-ace/lib/types'; import { useDispatch } from 'react-redux'; -import { css, styled, usePrevious } from '@superset-ui/core'; +import { css, styled, usePrevious, useTheme } from '@superset-ui/core'; +import { Global } from '@emotion/react'; +import { SQL_EDITOR_LEFTBAR_WIDTH } from 'src/SqlLab/constants'; import { queryEditorSetSelectedText } from 'src/SqlLab/actions/sqlLab'; import { FullSQLEditor as AceEditor } from 'src/components/AsyncAceEditor'; import type { KeyboardShortcut } from 'src/SqlLab/components/KeyboardShortcutButton'; @@ -54,16 +56,6 @@ const StyledAceEditor = styled(AceEditor)` font-feature-settings: 'liga' off, 'calt' off; - - &.ace_autocomplete { - // Use !important because Ace Editor applies extra CSS at the last second - // when opening the autocomplete. - width: ${theme.gridUnit * 130}px !important; - } - - .ace_scroller { - background-color: ${theme.colors.grayscale.light4}; - } } `} `; @@ -182,20 +174,44 @@ const AceEditorWrapper = ({ }, !autocomplete, ); + const theme = useTheme(); return ( - + <> + + + ); }; diff --git a/superset-frontend/src/SqlLab/components/QueryHistory/QueryHistory.test.tsx b/superset-frontend/src/SqlLab/components/QueryHistory/QueryHistory.test.tsx index ad1881b5d93e..110e7b4ae2f2 100644 --- a/superset-frontend/src/SqlLab/components/QueryHistory/QueryHistory.test.tsx +++ b/superset-frontend/src/SqlLab/components/QueryHistory/QueryHistory.test.tsx @@ -17,7 +17,10 @@ * under the License. */ import React from 'react'; -import { render, screen } from 'spec/helpers/testing-library'; +import fetchMock from 'fetch-mock'; +import * as uiCore from '@superset-ui/core'; +import { FeatureFlag, QueryState } from '@superset-ui/core'; +import { render, screen, waitFor } from 'spec/helpers/testing-library'; import QueryHistory from 'src/SqlLab/components/QueryHistory'; import { initialState } from 'src/SqlLab/fixtures'; @@ -27,18 +30,72 @@ const mockedProps = { latestQueryId: 'yhMUZCGb', }; +const fakeApiResult = { + count: 4, + ids: [692], + result: [ + { + changed_on: '2024-03-12T20:01:02.497775', + client_id: 'b0ZDzRYzn', + database: { + database_name: 'examples', + id: 1, + }, + end_time: '1710273662496.047852', + error_message: null, + executed_sql: 'SELECT * from "FCC 2018 Survey"\nLIMIT 1001', + id: 692, + limit: 1000, + limiting_factor: 'DROPDOWN', + progress: 100, + results_key: null, + rows: 443, + schema: 'main', + select_as_cta: false, + sql: 'SELECT * from "FCC 2018 Survey" ', + sql_editor_id: '22', + start_time: '1710273662445.992920', + status: QueryState.Success, + tab_name: 'Untitled Query 16', + tmp_table_name: null, + tracking_url: null, + user: { + first_name: 'admin', + id: 1, + last_name: 'user', + }, + }, + ], +}; + const setup = (overrides = {}) => ( ); -describe('QueryHistory', () => { - it('Renders an empty state for query history', () => { - render(setup(), { useRedux: true, initialState }); +test('Renders an empty state for query history', () => { + render(setup(), { useRedux: true, initialState }); + + const emptyStateText = screen.getByText( + /run a query to display query history/i, + ); + + expect(emptyStateText).toBeVisible(); +}); - const emptyStateText = screen.getByText( - /run a query to display query history/i, +test('fetches the query history when the persistence mode is enabled', async () => { + const isFeatureEnabledMock = jest + .spyOn(uiCore, 'isFeatureEnabled') + .mockImplementation( + featureFlag => featureFlag === FeatureFlag.SqllabBackendPersistence, ); - expect(emptyStateText).toBeVisible(); - }); + const editorQueryApiRoute = `glob:*/api/v1/query/?q=*`; + fetchMock.get(editorQueryApiRoute, fakeApiResult); + render(setup(), { useRedux: true, initialState }); + await waitFor(() => + expect(fetchMock.calls(editorQueryApiRoute).length).toBe(1), + ); + const queryResultText = screen.getByText(fakeApiResult.result[0].rows); + expect(queryResultText).toBeInTheDocument(); + isFeatureEnabledMock.mockClear(); }); diff --git a/superset-frontend/src/SqlLab/components/QueryHistory/index.tsx b/superset-frontend/src/SqlLab/components/QueryHistory/index.tsx index 311a125d556c..e020c3302ff3 100644 --- a/superset-frontend/src/SqlLab/components/QueryHistory/index.tsx +++ b/superset-frontend/src/SqlLab/components/QueryHistory/index.tsx @@ -16,12 +16,23 @@ * specific language governing permissions and limitations * under the License. */ -import React, { useMemo } from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; import { shallowEqual, useSelector } from 'react-redux'; +import { useInView } from 'react-intersection-observer'; +import { omit } from 'lodash'; import { EmptyStateMedium } from 'src/components/EmptyState'; -import { t, styled } from '@superset-ui/core'; +import { + t, + styled, + css, + FeatureFlag, + isFeatureEnabled, +} from '@superset-ui/core'; import QueryTable from 'src/SqlLab/components/QueryTable'; import { SqlLabRootState } from 'src/SqlLab/types'; +import { useEditorQueriesQuery } from 'src/hooks/apiResources/queries'; +import { Skeleton } from 'src/components'; +import useEffectEvent from 'src/hooks/useEffectEvent'; interface QueryHistoryProps { queryEditorId: string | number; @@ -40,39 +51,92 @@ const StyledEmptyStateWrapper = styled.div` } `; +const getEditorQueries = ( + queries: SqlLabRootState['sqlLab']['queries'], + queryEditorId: string | number, +) => + Object.values(queries).filter( + ({ sqlEditorId }) => String(sqlEditorId) === String(queryEditorId), + ); + const QueryHistory = ({ queryEditorId, displayLimit, latestQueryId, }: QueryHistoryProps) => { + const [ref, hasReachedBottom] = useInView({ threshold: 0 }); + const [pageIndex, setPageIndex] = useState(0); const queries = useSelector( ({ sqlLab: { queries } }: SqlLabRootState) => queries, shallowEqual, ); + const { data, isLoading, isFetching } = useEditorQueriesQuery( + { editorId: `${queryEditorId}`, pageIndex }, + { + skip: !isFeatureEnabled(FeatureFlag.SqllabBackendPersistence), + }, + ); const editorQueries = useMemo( () => - Object.values(queries).filter( - ({ sqlEditorId }) => String(sqlEditorId) === String(queryEditorId), - ), - [queries, queryEditorId], + data + ? getEditorQueries( + omit( + queries, + data.result.map(({ id }) => id), + ), + queryEditorId, + ) + .concat(data.result) + .reverse() + : getEditorQueries(queries, queryEditorId), + [queries, data, queryEditorId], ); + const loadNext = useEffectEvent(() => { + setPageIndex(pageIndex + 1); + }); + + const loadedDataCount = data?.result.length || 0; + const totalCount = data?.count || 0; + + useEffect(() => { + if (hasReachedBottom && loadedDataCount < totalCount) { + loadNext(); + } + }, [hasReachedBottom, loadNext, loadedDataCount, totalCount]); + + if (!editorQueries.length && isLoading) { + return ; + } + return editorQueries.length > 0 ? ( - + <> + + {data && loadedDataCount < totalCount && ( +
+ )} + {isFetching && } + ) : ( { if ( @@ -249,7 +257,7 @@ const ResultSet = ({ const { results } = query; const openInNewWindow = clickEvent.metaKey; - + logAction(LOG_ACTIONS_SQLLAB_CREATE_CHART, {}); if (results?.query_id) { const key = await postFormData(results.query_id, 'query', { ...EXPLORE_CHART_DEFAULT, @@ -312,7 +320,11 @@ const ResultSet = ({ /> )} {csv && ( - )} @@ -326,6 +338,9 @@ const ResultSet = ({ } hideTooltip + onCopyEnd={() => + logAction(LOG_ACTIONS_SQLLAB_COPY_RESULT_TO_CLIPBOARD, {}) + } /> {search && ( diff --git a/superset-frontend/src/SqlLab/components/RunQueryActionButton/index.tsx b/superset-frontend/src/SqlLab/components/RunQueryActionButton/index.tsx index 30f812113216..0d1740cbac63 100644 --- a/superset-frontend/src/SqlLab/components/RunQueryActionButton/index.tsx +++ b/superset-frontend/src/SqlLab/components/RunQueryActionButton/index.tsx @@ -25,6 +25,11 @@ import { DropdownButton } from 'src/components/DropdownButton'; import { detectOS } from 'src/utils/common'; import { QueryButtonProps } from 'src/SqlLab/types'; import useQueryEditor from 'src/SqlLab/hooks/useQueryEditor'; +import { + LOG_ACTIONS_SQLLAB_RUN_QUERY, + LOG_ACTIONS_SQLLAB_STOP_QUERY, +} from 'src/logger/LogUtils'; +import useLogAction from 'src/logger/useLogAction'; export interface RunQueryActionButtonProps { queryEditorId: string; @@ -57,7 +62,13 @@ const onClick = ( allowAsync: boolean, runQuery: (c?: boolean) => void = () => undefined, stopQuery = () => {}, + logAction: (name: string, payload: Record) => void, ): void => { + const eventName = shouldShowStopButton + ? LOG_ACTIONS_SQLLAB_STOP_QUERY + : LOG_ACTIONS_SQLLAB_RUN_QUERY; + + logAction(eventName, { shortcut: false }); if (shouldShowStopButton) return stopQuery(); if (allowAsync) { return runQuery(true); @@ -89,6 +100,7 @@ const RunQueryActionButton = ({ stopQuery, }: RunQueryActionButtonProps) => { const theme = useTheme(); + const logAction = useLogAction({ queryEditorId }); const userOS = detectOS(); const { selectedText, sql } = useQueryEditor(queryEditorId, [ @@ -121,7 +133,7 @@ const RunQueryActionButton = ({ - onClick(shouldShowStopBtn, allowAsync, runQuery, stopQuery) + onClick(shouldShowStopBtn, allowAsync, runQuery, stopQuery, logAction) } disabled={isDisabled} tooltip={ diff --git a/superset-frontend/src/SqlLab/components/SaveQuery/index.tsx b/superset-frontend/src/SqlLab/components/SaveQuery/index.tsx index a7ac8b1b2a89..766c37cfbd77 100644 --- a/superset-frontend/src/SqlLab/components/SaveQuery/index.tsx +++ b/superset-frontend/src/SqlLab/components/SaveQuery/index.tsx @@ -33,6 +33,11 @@ import { import { getDatasourceAsSaveableDataset } from 'src/utils/datasourceUtils'; import useQueryEditor from 'src/SqlLab/hooks/useQueryEditor'; import { QueryEditor } from 'src/SqlLab/types'; +import useLogAction from 'src/logger/useLogAction'; +import { + LOG_ACTIONS_SQLLAB_CREATE_CHART, + LOG_ACTIONS_SQLLAB_SAVE_QUERY, +} from 'src/logger/LogUtils'; interface SaveQueryProps { queryEditorId: string; @@ -90,6 +95,7 @@ const SaveQuery = ({ }), [queryEditor, columns], ); + const logAction = useLogAction({ queryEditorId }); const defaultLabel = query.name || query.description || t('Undefined'); const [description, setDescription] = useState( query.description || '', @@ -104,7 +110,12 @@ const SaveQuery = ({ const overlayMenu = ( - setShowSaveDatasetModal(true)}> + { + logAction(LOG_ACTIONS_SQLLAB_CREATE_CHART, {}); + setShowSaveDatasetModal(true); + }} + > {t('Save dataset')} @@ -127,6 +138,7 @@ const SaveQuery = ({ const close = () => setShowSave(false); const onSaveWrapper = () => { + logAction(LOG_ACTIONS_SQLLAB_SAVE_QUERY, {}); onSave(queryPayload(), query.id); close(); }; diff --git a/superset-frontend/src/SqlLab/components/ShareSqlLabQuery/index.tsx b/superset-frontend/src/SqlLab/components/ShareSqlLabQuery/index.tsx index 8f4cde1a66f1..b0a63f56e0ff 100644 --- a/superset-frontend/src/SqlLab/components/ShareSqlLabQuery/index.tsx +++ b/superset-frontend/src/SqlLab/components/ShareSqlLabQuery/index.tsx @@ -31,6 +31,8 @@ import CopyToClipboard from 'src/components/CopyToClipboard'; import { storeQuery } from 'src/utils/common'; import { getClientErrorObject } from 'src/utils/getClientErrorObject'; import useQueryEditor from 'src/SqlLab/hooks/useQueryEditor'; +import { LOG_ACTIONS_SQLLAB_COPY_LINK } from 'src/logger/LogUtils'; +import useLogAction from 'src/logger/useLogAction'; interface ShareSqlLabQueryProps { queryEditorId: string; @@ -52,7 +54,7 @@ const ShareSqlLabQuery = ({ addDangerToast, }: ShareSqlLabQueryProps) => { const theme = useTheme(); - + const logAction = useLogAction({ queryEditorId }); const { dbId, name, schema, autorun, sql, remoteId, templateParams } = useQueryEditor(queryEditorId, [ 'dbId', @@ -92,6 +94,9 @@ const ShareSqlLabQuery = ({ } }; const getCopyUrl = (callback: Function) => { + logAction(LOG_ACTIONS_SQLLAB_COPY_LINK, { + shortcut: false, + }); if (isFeatureEnabled(FeatureFlag.ShareQueriesViaKvStore)) { return getCopyUrlForKvStore(callback); } diff --git a/superset-frontend/src/SqlLab/components/SouthPane/Results.tsx b/superset-frontend/src/SqlLab/components/SouthPane/Results.tsx index b5167be61f4b..44e92ceaf91c 100644 --- a/superset-frontend/src/SqlLab/components/SouthPane/Results.tsx +++ b/superset-frontend/src/SqlLab/components/SouthPane/Results.tsx @@ -29,7 +29,7 @@ import { LOCALSTORAGE_MAX_QUERY_AGE_MS } from '../../constants'; const EXTRA_HEIGHT_RESULTS = 8; // we need extra height in RESULTS tab. because the height from props was calculated based on PREVIEW tab. type Props = { - latestQueryId: string; + latestQueryId?: string; height: number; displayLimit: number; defaultQueryLimit: number; diff --git a/superset-frontend/src/SqlLab/components/SouthPane/SouthPane.test.tsx b/superset-frontend/src/SqlLab/components/SouthPane/SouthPane.test.tsx index c978a4ca3233..2e66c2f33c3a 100644 --- a/superset-frontend/src/SqlLab/components/SouthPane/SouthPane.test.tsx +++ b/superset-frontend/src/SqlLab/components/SouthPane/SouthPane.test.tsx @@ -123,6 +123,19 @@ test('should render offline when the state is offline', async () => { expect(getByText(STATUS_OPTIONS.offline)).toBeVisible(); }); +test('should render empty result state when latestQuery is empty', () => { + const { getAllByRole } = render( + , + { + useRedux: true, + initialState: mockState, + }, + ); + + const resultPanel = getAllByRole('tabpanel')[0]; + expect(resultPanel).toHaveTextContent('Run a query to display results'); +}); + test('should render tabs for table preview queries', () => { const { getAllByRole } = render(, { useRedux: true, diff --git a/superset-frontend/src/SqlLab/components/SouthPane/index.tsx b/superset-frontend/src/SqlLab/components/SouthPane/index.tsx index 0bbce99b1c43..941f0f7523c6 100644 --- a/superset-frontend/src/SqlLab/components/SouthPane/index.tsx +++ b/superset-frontend/src/SqlLab/components/SouthPane/index.tsx @@ -144,14 +144,12 @@ const SouthPane = ({ animated={false} > - {latestQueryId && ( - - )} + { await findByText('sqleditor.extension.form extension component'), ).toBeInTheDocument(); }); + + describe('with SqllabBackendPersistence enabled', () => { + let isFeatureEnabledMock: jest.MockInstance< + boolean, + [feature: FeatureFlag] + >; + beforeEach(() => { + isFeatureEnabledMock = jest + .spyOn(uiCore, 'isFeatureEnabled') + .mockImplementation( + featureFlag => + featureFlag === uiCore.FeatureFlag.SqllabBackendPersistence, + ); + }); + afterEach(() => { + isFeatureEnabledMock.mockClear(); + }); + + it('should render loading state when its Editor is not loaded', async () => { + const switchTabApi = `glob:*/tabstateview/${defaultQueryEditor.id}/activate`; + fetchMock.post(switchTabApi, {}); + const { getByTestId } = setup( + { + ...mockedProps, + queryEditor: { + ...mockedProps.queryEditor, + loaded: false, + }, + }, + store, + ); + const indicator = getByTestId('sqlEditor-loading'); + expect(indicator).toBeInTheDocument(); + await waitFor(() => + expect(fetchMock.calls('glob:*/tabstateview/*').length).toBe(1), + ); + expect(fetchMock.calls(switchTabApi).length).toBe(1); + }); + }); }); diff --git a/superset-frontend/src/SqlLab/components/SqlEditor/index.tsx b/superset-frontend/src/SqlLab/components/SqlEditor/index.tsx index 8e9d84bd7411..bd8ea71179f6 100644 --- a/superset-frontend/src/SqlLab/components/SqlEditor/index.tsx +++ b/superset-frontend/src/SqlLab/components/SqlEditor/index.tsx @@ -54,7 +54,7 @@ import Mousetrap from 'mousetrap'; import Button from 'src/components/Button'; import Timer from 'src/components/Timer'; import ResizableSidebar from 'src/components/ResizableSidebar'; -import { AntdDropdown, AntdSwitch } from 'src/components'; +import { AntdDropdown, AntdSwitch, Skeleton } from 'src/components'; import { Input } from 'src/components/Input'; import { Menu } from 'src/components/Menu'; import Icons from 'src/components/Icons'; @@ -77,6 +77,7 @@ import { setActiveSouthPaneTab, updateSavedQuery, formatQuery, + switchQueryEditor, } from 'src/SqlLab/actions/sqlLab'; import { STATE_TYPE_MAP, @@ -97,6 +98,17 @@ import { } from 'src/utils/localStorageHelpers'; import { EmptyStateBig } from 'src/components/EmptyState'; import getBootstrapData from 'src/utils/getBootstrapData'; +import useLogAction from 'src/logger/useLogAction'; +import { + LOG_ACTIONS_SQLLAB_CREATE_TABLE_AS, + LOG_ACTIONS_SQLLAB_CREATE_VIEW_AS, + LOG_ACTIONS_SQLLAB_ESTIMATE_QUERY_COST, + LOG_ACTIONS_SQLLAB_FORMAT_SQL, + LOG_ACTIONS_SQLLAB_LOAD_TAB_STATE, + LOG_ACTIONS_SQLLAB_RUN_QUERY, + LOG_ACTIONS_SQLLAB_STOP_QUERY, + Logger, +} from 'src/logger/LogUtils'; import TemplateParamsEditor from '../TemplateParamsEditor'; import SouthPane from '../SouthPane'; import SaveQuery, { QueryPayload } from '../SaveQuery'; @@ -270,6 +282,7 @@ const SqlEditor: React.FC = ({ }; }, shallowEqual); + const logAction = useLogAction({ queryEditorId: queryEditor.id }); const isActive = currentQueryEditorId === queryEditor.id; const [height, setHeight] = useState(0); const [autorun, setAutorun] = useState(queryEditor.autorun); @@ -285,7 +298,10 @@ const SqlEditor: React.FC = ({ ); const [showCreateAsModal, setShowCreateAsModal] = useState(false); const [createAs, setCreateAs] = useState(''); - const [showEmptyState, setShowEmptyState] = useState(false); + const showEmptyState = useMemo( + () => !database || isEmpty(database), + [database], + ); const sqlEditorRef = useRef(null); const northPaneRef = useRef(null); @@ -313,9 +329,15 @@ const SqlEditor: React.FC = ({ [ctas, database, defaultQueryLimit, dispatch, queryEditor], ); - const formatCurrentQuery = useCallback(() => { - dispatch(formatQuery(queryEditor)); - }, [dispatch, queryEditor]); + const formatCurrentQuery = useCallback( + (useShortcut?: boolean) => { + logAction(LOG_ACTIONS_SQLLAB_FORMAT_SQL, { + shortcut: Boolean(useShortcut), + }); + dispatch(formatQuery(queryEditor)); + }, + [dispatch, queryEditor, logAction], + ); const stopQuery = useCallback(() => { if (latestQuery && ['running', 'pending'].indexOf(latestQuery.state) >= 0) { @@ -354,6 +376,7 @@ const SqlEditor: React.FC = ({ descr: KEY_MAP[KeyboardShortcut.CtrlR], func: () => { if (queryEditor.sql.trim() !== '') { + logAction(LOG_ACTIONS_SQLLAB_RUN_QUERY, { shortcut: true }); startQuery(); } }, @@ -364,6 +387,7 @@ const SqlEditor: React.FC = ({ descr: KEY_MAP[KeyboardShortcut.CtrlEnter], func: () => { if (queryEditor.sql.trim() !== '') { + logAction(LOG_ACTIONS_SQLLAB_RUN_QUERY, { shortcut: true }); startQuery(); } }, @@ -380,6 +404,7 @@ const SqlEditor: React.FC = ({ descr: KEY_MAP[KeyboardShortcut.CtrlT], }), func: () => { + Logger.markTimeOrigin(); dispatch(addNewQueryEditor()); }, }, @@ -394,14 +419,17 @@ const SqlEditor: React.FC = ({ key: KeyboardShortcut.CtrlE, descr: KEY_MAP[KeyboardShortcut.CtrlE], }), - func: stopQuery, + func: () => { + logAction(LOG_ACTIONS_SQLLAB_STOP_QUERY, { shortcut: true }); + stopQuery(); + }, }, { name: 'formatQuery', key: KeyboardShortcut.CtrlShiftF, descr: KEY_MAP[KeyboardShortcut.CtrlShiftF], func: () => { - formatCurrentQuery(); + formatCurrentQuery(true); }, }, ]; @@ -494,6 +522,23 @@ const SqlEditor: React.FC = ({ } }); + const shouldLoadQueryEditor = + isFeatureEnabled(FeatureFlag.SqllabBackendPersistence) && + !queryEditor.loaded; + + const loadQueryEditor = useEffectEvent(() => { + const duration = Logger.getTimestamp(); + logAction(LOG_ACTIONS_SQLLAB_LOAD_TAB_STATE, { + duration, + queryEditorId: queryEditor.id, + inLocalStorage: Boolean(queryEditor.inLocalStorage), + hasLoaded: !shouldLoadQueryEditor, + }); + if (shouldLoadQueryEditor) { + dispatch(switchQueryEditor(queryEditor, displayLimit)); + } + }); + useEffect(() => { // We need to measure the height of the sql editor post render to figure the height of // the south pane so it gets rendered properly @@ -503,6 +548,7 @@ const SqlEditor: React.FC = ({ WINDOW_RESIZE_THROTTLE_MS, ); if (isActive) { + loadQueryEditor(); window.addEventListener('resize', handleWindowResizeWithThrottle); window.addEventListener('beforeunload', onBeforeUnload); } @@ -512,13 +558,7 @@ const SqlEditor: React.FC = ({ window.removeEventListener('beforeunload', onBeforeUnload); }; // TODO: Remove useEffectEvent deps once https://github.com/facebook/react/pull/25881 is released - }, [onBeforeUnload, isActive]); - - useEffect(() => { - if (!database || isEmpty(database)) { - setShowEmptyState(true); - } - }, [database]); + }, [onBeforeUnload, loadQueryEditor, isActive]); useEffect(() => { // setup hotkeys @@ -585,6 +625,7 @@ const SqlEditor: React.FC = ({ }); const getQueryCostEstimate = () => { + logAction(LOG_ACTIONS_SQLLAB_ESTIMATE_QUERY_COST, { shortcut: false }); if (database) { dispatch(estimateQueryCost(queryEditor)); } @@ -638,7 +679,9 @@ const SqlEditor: React.FC = ({ /> )} - {t('Format SQL')} + formatCurrentQuery()}> + {t('Format SQL')} + {!isEmpty(scheduledQueriesConf) && ( = ({ {allowCTAS && ( { + logAction(LOG_ACTIONS_SQLLAB_CREATE_TABLE_AS, { + shortcut: false, + }); setShowCreateAsModal(true); setCreateAs(CtasEnum.Table); }} @@ -687,6 +733,9 @@ const SqlEditor: React.FC = ({ {allowCVAS && ( { + logAction(LOG_ACTIONS_SQLLAB_CREATE_VIEW_AS, { + shortcut: false, + }); setShowCreateAsModal(true); setCreateAs(CtasEnum.View); }} @@ -841,13 +890,22 @@ const SqlEditor: React.FC = ({ setShowEmptyState(bool)} /> )} - {showEmptyState ? ( + {shouldLoadQueryEditor ? ( +
+ +
+ ) : showEmptyState ? ( >; } const StyledScrollbarContainer = styled.div` @@ -107,7 +99,6 @@ const SqlEditorLeftBar = ({ database, queryEditorId, height = 500, - setEmptyState, }: SqlEditorLeftBarProps) => { const tables = useSelector( ({ sqlLab }) => @@ -143,7 +134,6 @@ const SqlEditorLeftBar = ({ }; const onDbChange = ({ id: dbId }: { id: number }) => { - setEmptyState?.(false); dispatch(queryEditorSetDb(queryEditor, dbId)); }; diff --git a/superset-frontend/src/SqlLab/components/TabbedSqlEditors/index.tsx b/superset-frontend/src/SqlLab/components/TabbedSqlEditors/index.tsx index 078276ad26ab..c9532fc2f21b 100644 --- a/superset-frontend/src/SqlLab/components/TabbedSqlEditors/index.tsx +++ b/superset-frontend/src/SqlLab/components/TabbedSqlEditors/index.tsx @@ -23,6 +23,7 @@ import { connect } from 'react-redux'; import URI from 'urijs'; import type { QueryEditor, SqlLabRootState } from 'src/SqlLab/types'; import { FeatureFlag, styled, t, isFeatureEnabled } from '@superset-ui/core'; +import { Logger } from 'src/logger/LogUtils'; import { Tooltip } from 'src/components/Tooltip'; import { detectOS } from 'src/utils/common'; import * as Actions from 'src/SqlLab/actions/sqlLab'; @@ -220,6 +221,7 @@ class TabbedSqlEditors extends React.PureComponent { } } if (action === 'add') { + Logger.markTimeOrigin(); this.newQueryEditor(); } } @@ -228,6 +230,14 @@ class TabbedSqlEditors extends React.PureComponent { this.props.actions.removeQueryEditor(qe); } + onTabClicked = () => { + Logger.markTimeOrigin(); + const noQueryEditors = this.props.queryEditors?.length === 0; + if (noQueryEditors) { + this.newQueryEditor(); + } + }; + render() { const noQueryEditors = this.props.queryEditors?.length === 0; const editors = this.props.queryEditors?.map(qe => ( @@ -288,7 +298,7 @@ class TabbedSqlEditors extends React.PureComponent { onChange={this.handleSelect} fullWidth={false} hideAdd={this.props.offline} - onTabClick={() => noQueryEditors && this.newQueryEditor()} + onTabClick={this.onTabClicked} onEdit={this.handleEdit} type={noQueryEditors ? 'card' : 'editable-card'} addIcon={ diff --git a/superset-frontend/src/SqlLab/fixtures.ts b/superset-frontend/src/SqlLab/fixtures.ts index 845e2209b5c8..742145c58ca6 100644 --- a/superset-frontend/src/SqlLab/fixtures.ts +++ b/superset-frontend/src/SqlLab/fixtures.ts @@ -679,6 +679,7 @@ export const initialState = { DISPLAY_MAX_ROW: 100, SQLALCHEMY_DOCS_URL: 'test_SQLALCHEMY_DOCS_URL', SQLALCHEMY_DISPLAY_TEXT: 'test_SQLALCHEMY_DISPLAY_TEXT', + SUPERSET_WEBSERVER_TIMEOUT: '300', }, }, }; diff --git a/superset-frontend/src/SqlLab/reducers/getInitialState.test.ts b/superset-frontend/src/SqlLab/reducers/getInitialState.test.ts index 1dd3220fcc46..6d6e65bad3bd 100644 --- a/superset-frontend/src/SqlLab/reducers/getInitialState.test.ts +++ b/superset-frontend/src/SqlLab/reducers/getInitialState.test.ts @@ -25,7 +25,6 @@ const apiData = { common: DEFAULT_COMMON_BOOTSTRAP_DATA, tab_state_ids: [], databases: [], - queries: {}, user: { userId: 1, username: 'some name', @@ -220,18 +219,20 @@ describe('getInitialState', () => { }), ); + const latestQuery = { + ...runningQuery, + id: 'latestPersisted', + startDttm: Number(startDttmInStr), + endDttm: Number(endDttmInStr), + }; const initializedQueries = getInitialState({ - ...apiData, - queries: { - backendPersisted: { - ...runningQuery, - id: 'backendPersisted', - startDttm: startDttmInStr, - endDttm: endDttmInStr, - }, + ...apiDataWithTabState, + active_tab: { + ...apiDataWithTabState.active_tab, + latest_query: latestQuery, }, }).sqlLab.queries; - expect(initializedQueries.backendPersisted).toEqual( + expect(initializedQueries.latestPersisted).toEqual( expect.objectContaining({ startDttm: Number(startDttmInStr), endDttm: Number(endDttmInStr), diff --git a/superset-frontend/src/SqlLab/reducers/getInitialState.ts b/superset-frontend/src/SqlLab/reducers/getInitialState.ts index f92f2b7f1523..947e7b7a0f2b 100644 --- a/superset-frontend/src/SqlLab/reducers/getInitialState.ts +++ b/superset-frontend/src/SqlLab/reducers/getInitialState.ts @@ -57,7 +57,7 @@ export default function getInitialState({ version: LatestQueryEditorVersion, loaded: true, name: t('Untitled query'), - sql: 'SELECT *\nFROM\nWHERE', + sql: '', latestQueryId: null, autorun: false, dbId: common.conf.SQLLAB_DEFAULT_DBID, @@ -101,6 +101,7 @@ export default function getInitialState({ id: id.toString(), loaded: false, name: label, + dbId: undefined, }; } queryEditors = { @@ -136,7 +137,12 @@ export default function getInitialState({ }); } - const queries = { ...queries_ }; + const queries = { + ...queries_, + ...(activeTab?.latest_query && { + [activeTab.latest_query.id]: activeTab.latest_query, + }), + }; /** * If the `SQLLAB_BACKEND_PERSISTENCE` feature flag is off, or if the user diff --git a/superset-frontend/src/SqlLab/reducers/sqlLab.js b/superset-frontend/src/SqlLab/reducers/sqlLab.js index 40a4dfb29d15..9ffcb4dfcb85 100644 --- a/superset-frontend/src/SqlLab/reducers/sqlLab.js +++ b/superset-frontend/src/SqlLab/reducers/sqlLab.js @@ -152,7 +152,10 @@ export default function sqlLabReducer(state = {}, action) { newState = { ...newState, - tabHistory, + tabHistory: + tabHistory.length === 0 && newState.queryEditors.length > 0 + ? newState.queryEditors.slice(-1).map(qe => qe.id) + : tabHistory, tables, queries, unsavedQueryEditor: { diff --git a/superset-frontend/src/SqlLab/reducers/sqlLab.test.js b/superset-frontend/src/SqlLab/reducers/sqlLab.test.js index 485094adafec..87e0212b1642 100644 --- a/superset-frontend/src/SqlLab/reducers/sqlLab.test.js +++ b/superset-frontend/src/SqlLab/reducers/sqlLab.test.js @@ -75,6 +75,25 @@ describe('sqlLabReducer', () => { initialState.queryEditors.length, ); }); + it('should select the latest query editor when tabHistory is empty', () => { + const currentQE = newState.queryEditors[0]; + newState = { + ...initialState, + tabHistory: [initialState.queryEditors[0]], + }; + const action = { + type: actions.REMOVE_QUERY_EDITOR, + queryEditor: currentQE, + }; + newState = sqlLabReducer(newState, action); + expect(newState.queryEditors).toHaveLength( + initialState.queryEditors.length - 1, + ); + expect(newState.queryEditors.map(qe => qe.id)).not.toContainEqual( + currentQE.id, + ); + expect(newState.tabHistory).toEqual([initialState.queryEditors[2].id]); + }); it('should remove a query editor including unsaved changes', () => { expect(newState.queryEditors).toHaveLength( initialState.queryEditors.length + 1, diff --git a/superset-frontend/src/components/AlteredSliceTag/AlteredSliceTagMocks.ts b/superset-frontend/src/components/AlteredSliceTag/AlteredSliceTagMocks.ts index 233f519446d7..6428b503b85a 100644 --- a/superset-frontend/src/components/AlteredSliceTag/AlteredSliceTagMocks.ts +++ b/superset-frontend/src/components/AlteredSliceTag/AlteredSliceTagMocks.ts @@ -17,7 +17,7 @@ * under the License. */ import { QueryFormData } from '@superset-ui/core'; -import { ControlPanelConfig } from 'packages/superset-ui-chart-controls/src/types'; +import { ControlPanelConfig } from '@superset-ui/chart-controls'; import { DiffType, RowType } from './index'; export const defaultProps: Record> = { diff --git a/superset-frontend/src/components/AlteredSliceTag/index.tsx b/superset-frontend/src/components/AlteredSliceTag/index.tsx index dfedc9f5b651..28f47657b971 100644 --- a/superset-frontend/src/components/AlteredSliceTag/index.tsx +++ b/superset-frontend/src/components/AlteredSliceTag/index.tsx @@ -179,7 +179,7 @@ class AlteredSliceTag extends React.Component< return '[]'; } return value - .map(v => { + .map((v: FilterItemType) => { const filterVal = v.comparator && v.comparator.constructor === Array ? `[${v.comparator.join(', ')}]` @@ -198,14 +198,14 @@ class AlteredSliceTag extends React.Component< return value.map(v => safeStringify(v)).join(', '); } if (controlsMap[key]?.type === 'MetricsControl' && Array.isArray(value)) { - const formattedValue = value.map(v => v?.label ?? v); + const formattedValue = value.map((v: FilterItemType) => v?.label ?? v); return formattedValue.length ? formattedValue.join(', ') : '[]'; } if (typeof value === 'boolean') { return value ? 'true' : 'false'; } if (Array.isArray(value)) { - const formattedValue = value.map(v => v?.label ?? v); + const formattedValue = value.map((v: FilterItemType) => v?.label ?? v); return formattedValue.length ? formattedValue.join(', ') : '[]'; } if (typeof value === 'string' || typeof value === 'number') { diff --git a/superset-frontend/src/components/AsyncAceEditor/index.tsx b/superset-frontend/src/components/AsyncAceEditor/index.tsx index 2e499e150bb4..1b755f50ef3d 100644 --- a/superset-frontend/src/components/AsyncAceEditor/index.tsx +++ b/superset-frontend/src/components/AsyncAceEditor/index.tsx @@ -24,11 +24,15 @@ import { TextMode as OrigTextMode, } from 'brace'; import AceEditor, { IAceEditorProps } from 'react-ace'; +import { config } from 'ace-builds'; import { acequire } from 'ace-builds/src-noconflict/ace'; import AsyncEsmComponent, { PlaceholderProps, } from 'src/components/AsyncEsmComponent'; import useEffectEvent from 'src/hooks/useEffectEvent'; +import cssWorkerUrl from 'ace-builds/src-noconflict/worker-css'; + +config.setModuleUrl('ace/mode/css_worker', cssWorkerUrl); export interface AceCompleterKeywordData { name: string; diff --git a/superset-frontend/src/components/Chart/DrillBy/DrillByMenuItems.test.tsx b/superset-frontend/src/components/Chart/DrillBy/DrillByMenuItems.test.tsx index 41cc6b3c08ea..c98ca4847265 100644 --- a/superset-frontend/src/components/Chart/DrillBy/DrillByMenuItems.test.tsx +++ b/superset-frontend/src/components/Chart/DrillBy/DrillByMenuItems.test.tsx @@ -131,9 +131,9 @@ test('render disabled menu item for unsupported chart', async () => { ); }); -test('render disabled menu item for supported chart, no filters', async () => { +test('render enabled menu item for supported chart, no filters', async () => { renderMenu({ drillByConfig: { filters: [], groupbyFieldName: 'groupby' } }); - await expectDrillByDisabled('Drill by is not available for this data point'); + await expectDrillByEnabled(); }); test('render disabled menu item for supported chart, no columns', async () => { diff --git a/superset-frontend/src/components/Chart/DrillBy/DrillByMenuItems.tsx b/superset-frontend/src/components/Chart/DrillBy/DrillByMenuItems.tsx index 2fe711474e11..26ae3e2f32ff 100644 --- a/superset-frontend/src/components/Chart/DrillBy/DrillByMenuItems.tsx +++ b/superset-frontend/src/components/Chart/DrillBy/DrillByMenuItems.tsx @@ -107,9 +107,7 @@ export const DrillByMenuItems = ({ setSearchInput(''); }, [columns.length]); - const hasDrillBy = - ensureIsArray(drillByConfig?.filters).length && - drillByConfig?.groupbyFieldName; + const hasDrillBy = drillByConfig?.groupbyFieldName; const handlesDimensionContextMenu = useMemo( () => diff --git a/superset-frontend/src/components/Chart/DrillBy/useDrillByBreadcrumbs.tsx b/superset-frontend/src/components/Chart/DrillBy/useDrillByBreadcrumbs.tsx index fc7c0b2bf550..36f86371f8ec 100644 --- a/superset-frontend/src/components/Chart/DrillBy/useDrillByBreadcrumbs.tsx +++ b/superset-frontend/src/components/Chart/DrillBy/useDrillByBreadcrumbs.tsx @@ -34,8 +34,11 @@ export interface DrillByBreadcrumb { filters?: BinaryQueryObjectFilterClause[]; } -const BreadcrumbItem = styled(AntdBreadcrumb.Item)<{ isClickable: boolean }>` - ${({ theme, isClickable }) => css` +const BreadcrumbItem = styled(AntdBreadcrumb.Item)<{ + isClickable: boolean; + isHidden: boolean; +}>` + ${({ theme, isClickable, isHidden }) => css` cursor: ${isClickable ? 'pointer' : 'auto'}; color: ${theme.colors.grayscale.light1}; transition: color ease-in ${theme.transitionTiming}s; @@ -45,6 +48,7 @@ const BreadcrumbItem = styled(AntdBreadcrumb.Item)<{ isClickable: boolean }>` &:hover { color: ${isClickable ? theme.colors.grayscale.dark1 : 'inherit'}; } + visibility: ${isHidden ? 'hidden' : 'visible'}; `} `; @@ -58,6 +62,9 @@ export const useDrillByBreadcrumbs = ( useMemo(() => { // the last breadcrumb is not clickable const isClickable = (index: number) => index < breadcrumbsData.length - 1; + const isHidden = (breadcumb: DrillByBreadcrumb) => + ensureIsArray(breadcumb.groupby).length === 0 && + ensureIsArray(breadcumb.filters).length === 0; const getBreadcrumbText = (breadcrumb: DrillByBreadcrumb) => `${ensureIsArray(breadcrumb.groupby) .map(column => column.verbose_name || column.column_name) @@ -74,20 +81,23 @@ export const useDrillByBreadcrumbs = ( margin: ${theme.gridUnit * 2}px 0 ${theme.gridUnit * 4}px; `} > - {breadcrumbsData.map((breadcrumb, index) => ( - onBreadcrumbClick(breadcrumb, index) - : noOp - } - data-test="drill-by-breadcrumb-item" - > - {getBreadcrumbText(breadcrumb)} - - ))} + {breadcrumbsData + .map((breadcrumb, index) => ( + onBreadcrumbClick(breadcrumb, index) + : noOp + } + data-test="drill-by-breadcrumb-item" + > + {getBreadcrumbText(breadcrumb)} + + )) + .filter(item => item.props.isHidden === false)} ); }, [breadcrumbsData, onBreadcrumbClick]); diff --git a/superset-frontend/src/components/Chart/DrillBy/useResultsTableView.tsx b/superset-frontend/src/components/Chart/DrillBy/useResultsTableView.tsx index b424b95ea558..bab24fdd479a 100644 --- a/superset-frontend/src/components/Chart/DrillBy/useResultsTableView.tsx +++ b/superset-frontend/src/components/Chart/DrillBy/useResultsTableView.tsx @@ -44,6 +44,7 @@ export const useResultsTableView = ( { + return async (dispatch, getState) => { const logStart = Logger.getTimestamp(); const controller = new AbortController(); + const queryTimeout = + timeout || getState().common.conf.SUPERSET_WEBSERVER_TIMEOUT; const requestParams = { signal: controller.signal, - timeout: timeout * 1000, + timeout: queryTimeout * 1000, }; if (dashboardId) requestParams.dashboard_id = dashboardId; @@ -519,7 +524,7 @@ export const POST_CHART_FORM_DATA = 'POST_CHART_FORM_DATA'; export function postChartFormData( formData, force = false, - timeout = 60, + timeout, key, dashboardId, ownState, @@ -604,6 +609,7 @@ export const getDatasourceSamples = async ( endpoint: '/datasource/samples', jsonPayload, searchParams, + parseMethod: 'json-bigint', }); return response.json.result; diff --git a/superset-frontend/src/components/Chart/chartActions.test.js b/superset-frontend/src/components/Chart/chartActions.test.js index c2a58e609440..129c17dbe0b4 100644 --- a/superset-frontend/src/components/Chart/chartActions.test.js +++ b/superset-frontend/src/components/Chart/chartActions.test.js @@ -28,6 +28,27 @@ import * as actions from 'src/components/Chart/chartAction'; import * as asyncEvent from 'src/middleware/asyncEvent'; import { handleChartDataResponse } from 'src/components/Chart/chartAction'; +import configureMockStore from 'redux-mock-store'; +import thunk from 'redux-thunk'; +import { initialState } from 'src/SqlLab/fixtures'; + +const middlewares = [thunk]; +const mockStore = configureMockStore(middlewares); + +const mockGetState = () => ({ + charts: { + chartKey: { + latestQueryFormData: { + time_grain_sqla: 'P1D', + granularity_sqla: 'Date', + }, + }, + }, + common: { + conf: {}, + }, +}); + describe('chart actions', () => { const MOCK_URL = '/mockURL'; let dispatch; @@ -94,7 +115,7 @@ describe('chart actions', () => { it('should query with the built query', async () => { const actionThunk = actions.postChartFormData({}, null); - await actionThunk(dispatch); + await actionThunk(dispatch, mockGetState); expect(fetchMock.calls(MOCK_URL)).toHaveLength(1); expect(fetchMock.calls(MOCK_URL)[0][1].body).toBe( @@ -165,7 +186,7 @@ describe('chart actions', () => { it('should dispatch CHART_UPDATE_STARTED action before the query', () => { const actionThunk = actions.postChartFormData({}); - return actionThunk(dispatch).then(() => { + return actionThunk(dispatch, mockGetState).then(() => { // chart update, trigger query, update form data, success expect(dispatch.callCount).toBe(5); expect(fetchMock.calls(MOCK_URL)).toHaveLength(1); @@ -175,7 +196,7 @@ describe('chart actions', () => { it('should dispatch TRIGGER_QUERY action with the query', () => { const actionThunk = actions.postChartFormData({}); - return actionThunk(dispatch).then(() => { + return actionThunk(dispatch, mockGetState).then(() => { // chart update, trigger query, update form data, success expect(dispatch.callCount).toBe(5); expect(fetchMock.calls(MOCK_URL)).toHaveLength(1); @@ -185,7 +206,7 @@ describe('chart actions', () => { it('should dispatch UPDATE_QUERY_FORM_DATA action with the query', () => { const actionThunk = actions.postChartFormData({}); - return actionThunk(dispatch).then(() => { + return actionThunk(dispatch, mockGetState).then(() => { // chart update, trigger query, update form data, success expect(dispatch.callCount).toBe(5); expect(fetchMock.calls(MOCK_URL)).toHaveLength(1); @@ -195,7 +216,7 @@ describe('chart actions', () => { it('should dispatch logEvent async action', () => { const actionThunk = actions.postChartFormData({}); - return actionThunk(dispatch).then(() => { + return actionThunk(dispatch, mockGetState).then(() => { // chart update, trigger query, update form data, success expect(dispatch.callCount).toBe(5); expect(fetchMock.calls(MOCK_URL)).toHaveLength(1); @@ -209,7 +230,7 @@ describe('chart actions', () => { it('should dispatch CHART_UPDATE_SUCCEEDED action upon success', () => { const actionThunk = actions.postChartFormData({}); - return actionThunk(dispatch).then(() => { + return actionThunk(dispatch, mockGetState).then(() => { // chart update, trigger query, update form data, success expect(dispatch.callCount).toBe(5); expect(fetchMock.calls(MOCK_URL)).toHaveLength(1); @@ -226,7 +247,7 @@ describe('chart actions', () => { const timeoutInSec = 1 / 1000; const actionThunk = actions.postChartFormData({}, false, timeoutInSec); - return actionThunk(dispatch).then(() => { + return actionThunk(dispatch, mockGetState).then(() => { // chart update, trigger query, update form data, fail expect(fetchMock.calls(MOCK_URL)).toHaveLength(1); expect(dispatch.callCount).toBe(5); @@ -245,7 +266,7 @@ describe('chart actions', () => { const timeoutInSec = 100; // Set to a time that is longer than the time this will take to fail const actionThunk = actions.postChartFormData({}, false, timeoutInSec); - return actionThunk(dispatch).then(() => { + return actionThunk(dispatch, mockGetState).then(() => { // chart update, trigger query, update form data, fail expect(dispatch.callCount).toBe(5); const updateFailedAction = dispatch.args[4][0]; @@ -278,17 +299,6 @@ describe('chart actions', () => { describe('runAnnotationQuery', () => { const mockDispatch = jest.fn(); - const mockGetState = () => ({ - charts: { - chartKey: { - latestQueryFormData: { - time_grain_sqla: 'P1D', - granularity_sqla: 'Date', - }, - }, - }, - }); - beforeEach(() => { jest.clearAllMocks(); }); @@ -342,3 +352,72 @@ describe('chart actions', () => { }); }); }); + +describe('chart actions timeout', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should use the timeout from arguments when given', () => { + const postSpy = jest.spyOn(SupersetClient, 'post'); + postSpy.mockImplementation(() => Promise.resolve({ json: { result: [] } })); + const timeout = 10; // Set the timeout value here + const formData = { datasource: 'table__1' }; // Set the formData here + const key = 'chartKey'; // Set the chart key here + + const store = mockStore(initialState); + store.dispatch( + actions.runAnnotationQuery({ + annotation: { + value: 'annotationValue', + sourceType: 'Event', + overrides: {}, + }, + timeout, + formData, + key, + }), + ); + + const expectedPayload = { + url: expect.any(String), + signal: expect.any(AbortSignal), + timeout: timeout * 1000, + headers: { 'Content-Type': 'application/json' }, + jsonPayload: expect.any(Object), + }; + + expect(postSpy).toHaveBeenCalledWith(expectedPayload); + }); + + it('should use the timeout from common.conf when not passed as an argument', () => { + const postSpy = jest.spyOn(SupersetClient, 'post'); + postSpy.mockImplementation(() => Promise.resolve({ json: { result: [] } })); + const formData = { datasource: 'table__1' }; // Set the formData here + const key = 'chartKey'; // Set the chart key here + + const store = mockStore(initialState); + store.dispatch( + actions.runAnnotationQuery({ + annotation: { + value: 'annotationValue', + sourceType: 'Event', + overrides: {}, + }, + undefined, + formData, + key, + }), + ); + + const expectedPayload = { + url: expect.any(String), + signal: expect.any(AbortSignal), + timeout: initialState.common.conf.SUPERSET_WEBSERVER_TIMEOUT * 1000, + headers: { 'Content-Type': 'application/json' }, + jsonPayload: expect.any(Object), + }; + + expect(postSpy).toHaveBeenCalledWith(expectedPayload); + }); +}); diff --git a/superset-frontend/src/components/DatabaseSelector/DatabaseSelector.test.tsx b/superset-frontend/src/components/DatabaseSelector/DatabaseSelector.test.tsx index 874d22ea6bb2..2581bc6bd63f 100644 --- a/superset-frontend/src/components/DatabaseSelector/DatabaseSelector.test.tsx +++ b/superset-frontend/src/components/DatabaseSelector/DatabaseSelector.test.tsx @@ -149,6 +149,12 @@ const fakeDatabaseApiResult = { ], }; +const fakeDatabaseApiResultInReverseOrder = { + ...fakeDatabaseApiResult, + ids: [2, 1], + result: [...fakeDatabaseApiResult.result].reverse(), +}; + const fakeSchemaApiResult = { count: 2, result: ['information_schema', 'public'], @@ -158,7 +164,8 @@ const fakeFunctionNamesApiResult = { function_names: [], }; -const databaseApiRoute = 'glob:*/api/v1/database/?*'; +const databaseApiRoute = + 'glob:*/api/v1/database/?*order_column:database_name,order_direction*'; const schemaApiRoute = 'glob:*/api/v1/database/*/schemas/?*'; const tablesApiRoute = 'glob:*/api/v1/database/*/tables/*'; @@ -229,6 +236,56 @@ test('Should database select display options', async () => { expect(await screen.findByText('test-mysql')).toBeInTheDocument(); }); +test('should display options in order of the api response', async () => { + fetchMock.get(databaseApiRoute, fakeDatabaseApiResultInReverseOrder, { + overwriteRoutes: true, + }); + const props = createProps(); + render(, { + useRedux: true, + store, + }); + const select = screen.getByRole('combobox', { + name: 'Select database or type to search databases', + }); + expect(select).toBeInTheDocument(); + userEvent.click(select); + const options = await screen.findAllByRole('option'); + + expect(options[0]).toHaveTextContent( + `${fakeDatabaseApiResultInReverseOrder.result[0].id}`, + ); + expect(options[1]).toHaveTextContent( + `${fakeDatabaseApiResultInReverseOrder.result[1].id}`, + ); +}); + +test('Should fetch the search keyword when total count exceeds initial options', async () => { + fetchMock.get( + databaseApiRoute, + { + ...fakeDatabaseApiResult, + count: fakeDatabaseApiResult.result.length + 1, + }, + { overwriteRoutes: true }, + ); + + const props = createProps(); + render(, { useRedux: true, store }); + const select = screen.getByRole('combobox', { + name: 'Select database or type to search databases', + }); + await waitFor(() => + expect(fetchMock.calls(databaseApiRoute)).toHaveLength(1), + ); + expect(select).toBeInTheDocument(); + userEvent.type(select, 'keywordtest'); + await waitFor(() => + expect(fetchMock.calls(databaseApiRoute)).toHaveLength(2), + ); + expect(fetchMock.calls(databaseApiRoute)[1][0]).toContain('keywordtest'); +}); + test('should show empty state if there are no options', async () => { fetchMock.reset(); fetchMock.get(databaseApiRoute, { result: [] }); diff --git a/superset-frontend/src/components/DatabaseSelector/index.tsx b/superset-frontend/src/components/DatabaseSelector/index.tsx index 7b4afd9af05a..5df987cbb93b 100644 --- a/superset-frontend/src/components/DatabaseSelector/index.tsx +++ b/superset-frontend/src/components/DatabaseSelector/index.tsx @@ -16,8 +16,16 @@ * specific language governing permissions and limitations * under the License. */ -import React, { ReactNode, useState, useMemo, useEffect, useRef } from 'react'; +import React, { + ReactNode, + useState, + useMemo, + useEffect, + useRef, + useCallback, +} from 'react'; import { styled, SupersetClient, t } from '@superset-ui/core'; +import type { LabeledValue as AntdLabeledValue } from 'antd/lib/select'; import rison from 'rison'; import { AsyncSelect, Select } from 'src/components'; import Label from 'src/components/Label'; @@ -115,6 +123,10 @@ const SelectLabel = ({ const EMPTY_SCHEMA_OPTIONS: SchemaOption[] = []; +interface AntdLabeledValueWithOrder extends AntdLabeledValue { + order: number; +} + export default function DatabaseSelector({ db, formMode = false, @@ -136,6 +148,11 @@ export default function DatabaseSelector({ const schemaRef = useRef(schema); schemaRef.current = schema; const { addSuccessToast } = useToasts(); + const sortComparator = useCallback( + (itemA: AntdLabeledValueWithOrder, itemB: AntdLabeledValueWithOrder) => + itemA.order - itemB.order, + [], + ); const loadDatabases = useMemo( () => @@ -148,7 +165,7 @@ export default function DatabaseSelector({ totalCount: number; }> => { const queryParams = rison.encode({ - order_columns: 'database_name', + order_column: 'database_name', order_direction: 'asc', page, page_size: pageSize, @@ -167,14 +184,15 @@ export default function DatabaseSelector({ }); const endpoint = `/api/v1/database/?q=${queryParams}`; return SupersetClient.get({ endpoint }).then(({ json }) => { - const { result } = json; + const { result, count } = json; if (getDbList) { getDbList(result); } if (result.length === 0) { if (onEmptyResults) onEmptyResults(search); } - const options = result.map((row: DatabaseObject) => ({ + + const options = result.map((row: DatabaseObject, order: number) => ({ label: ( , null, ); diff --git a/superset-frontend/src/components/ErrorMessage/DatabaseErrorMessage.test.tsx b/superset-frontend/src/components/ErrorMessage/DatabaseErrorMessage.test.tsx index 78a02a27ee7a..0a1ad2729929 100644 --- a/superset-frontend/src/components/ErrorMessage/DatabaseErrorMessage.test.tsx +++ b/superset-frontend/src/components/ErrorMessage/DatabaseErrorMessage.test.tsx @@ -56,7 +56,14 @@ const mockedProps = { }; test('should render', () => { - const { container } = render(); + const nullExtraProps = { + ...mockedProps, + error: { + ...mockedProps.error, + extra: null, + }, + }; + const { container } = render(); expect(container).toBeInTheDocument(); }); diff --git a/superset-frontend/src/components/ErrorMessage/DatabaseErrorMessage.tsx b/superset-frontend/src/components/ErrorMessage/DatabaseErrorMessage.tsx index a7a0a4199f5d..bf2d013fd047 100644 --- a/superset-frontend/src/components/ErrorMessage/DatabaseErrorMessage.tsx +++ b/superset-frontend/src/components/ErrorMessage/DatabaseErrorMessage.tsx @@ -36,7 +36,7 @@ function DatabaseErrorMessage({ error, source = 'dashboard', subtitle, -}: ErrorMessageComponentProps) { +}: ErrorMessageComponentProps) { const { extra, level, message } = error; const isVisualization = ['dashboard', 'explore'].includes(source); @@ -47,7 +47,7 @@ function DatabaseErrorMessage({ {t('This may be triggered by:')}
{extra.issue_codes - .map(issueCode => ( + ?.map(issueCode => ( )) .reduce((prev, curr) => [prev,
, curr])} diff --git a/superset-frontend/src/components/FilterableTable/index.tsx b/superset-frontend/src/components/FilterableTable/index.tsx index d731313bdea4..0ae4406de200 100644 --- a/superset-frontend/src/components/FilterableTable/index.tsx +++ b/superset-frontend/src/components/FilterableTable/index.tsx @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import JSONbig from 'json-bigint'; +import _JSONbig from 'json-bigint'; import React, { useEffect, useRef, useState, useMemo } from 'react'; import { getMultipleTextDimensions, styled } from '@superset-ui/core'; import { useDebounceValue } from 'src/hooks/useDebounceValue'; @@ -24,6 +24,11 @@ import { useCellContentParser } from './useCellContentParser'; import { renderResultCell } from './utils'; import { Table, TableSize } from '../Table'; +const JSONbig = _JSONbig({ + storeAsString: true, + constructorAction: 'preserve', +}); + const SCROLL_BAR_HEIGHT = 15; // This regex handles all possible number formats in javascript, including ints, floats, // exponential notation, NaN, and Infinity. diff --git a/superset-frontend/src/components/ListView/CardSortSelect.tsx b/superset-frontend/src/components/ListView/CardSortSelect.tsx index 479355881f90..2a77d51cfb94 100644 --- a/superset-frontend/src/components/ListView/CardSortSelect.tsx +++ b/superset-frontend/src/components/ListView/CardSortSelect.tsx @@ -21,7 +21,7 @@ import { styled, t } from '@superset-ui/core'; import { Select } from 'src/components'; import { FormLabel } from 'src/components/Form'; import { SELECT_WIDTH } from './utils'; -import { CardSortSelectOption, FetchDataConfig, SortColumn } from './types'; +import { CardSortSelectOption, SortColumn } from './types'; const SortContainer = styled.div` display: inline-flex; @@ -32,22 +32,22 @@ const SortContainer = styled.div` `; interface CardViewSelectSortProps { - onChange: (conf: FetchDataConfig) => any; + onChange: (value: SortColumn[]) => void; options: Array; initialSort?: SortColumn[]; - pageIndex: number; - pageSize: number; } export const CardSortSelect = ({ initialSort, onChange, options, - pageIndex, - pageSize, }: CardViewSelectSortProps) => { const defaultSort = - (initialSort && options.find(({ id }) => id === initialSort[0].id)) || + (initialSort && + options.find( + ({ id, desc }) => + id === initialSort[0].id && desc === initialSort[0].desc, + )) || options[0]; const [value, setValue] = useState({ @@ -72,7 +72,7 @@ export const CardSortSelect = ({ desc: originalOption.desc, }, ]; - onChange({ pageIndex, pageSize, sortBy, filters: [] }); + onChange(sortBy); } }; @@ -82,7 +82,7 @@ export const CardSortSelect = ({ ariaLabel={t('Sort')} header={{t('Sort')}} labelInValue - onChange={(value: CardSortSelectOption) => handleOnChange(value)} + onChange={handleOnChange} options={formattedOptions} showSearch value={value} diff --git a/superset-frontend/src/components/ListView/CrossLinks.tsx b/superset-frontend/src/components/ListView/CrossLinks.tsx index e3157506742b..6b3eb5e4b150 100644 --- a/superset-frontend/src/components/ListView/CrossLinks.tsx +++ b/superset-frontend/src/components/ListView/CrossLinks.tsx @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import React, { useMemo, useRef } from 'react'; +import React, { useMemo } from 'react'; import { styled, useTruncation } from '@superset-ui/core'; import { Link } from 'react-router-dom'; import CrossLinksTooltip from './CrossLinksTooltip'; @@ -60,17 +60,13 @@ const StyledCrossLinks = styled.div` `} `; -export default function CrossLinks({ +function CrossLinks({ crossLinks, maxLinks = 20, linkPrefix = '/superset/dashboard/', }: CrossLinksProps) { - const crossLinksRef = useRef(null); - const plusRef = useRef(null); - const [elementsTruncated, hasHiddenElements] = useTruncation( - crossLinksRef, - plusRef, - ); + const [crossLinksRef, plusRef, elementsTruncated, hasHiddenElements] = + useTruncation(); const hasMoreItems = useMemo( () => crossLinks.length > maxLinks ? crossLinks.length - maxLinks : undefined, @@ -80,18 +76,13 @@ export default function CrossLinks({ () => ( {crossLinks.map((link, index) => ( - + {index === 0 ? link.title : `, ${link.title}`} ))} ), - [crossLinks], + [crossLinks, crossLinksRef, linkPrefix], ); const tooltipLinks = useMemo( () => @@ -99,7 +90,7 @@ export default function CrossLinks({ title: l.title, to: linkPrefix + l.id, })), - [crossLinks, maxLinks], + [crossLinks, linkPrefix, maxLinks], ); return ( @@ -119,3 +110,5 @@ export default function CrossLinks({ ); } + +export default React.memo(CrossLinks); diff --git a/superset-frontend/src/components/ListView/DashboardCrossLinks.tsx b/superset-frontend/src/components/ListView/DashboardCrossLinks.tsx new file mode 100644 index 000000000000..409f24bfb72f --- /dev/null +++ b/superset-frontend/src/components/ListView/DashboardCrossLinks.tsx @@ -0,0 +1,37 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { useMemo } from 'react'; +import { ensureIsArray } from '@superset-ui/core'; +import { ChartLinkedDashboard } from 'src/types/Chart'; +import CrossLinks from './CrossLinks'; + +export const DashboardCrossLinks = React.memo( + ({ dashboards }: { dashboards: ChartLinkedDashboard[] }) => { + const crossLinks = useMemo( + () => + ensureIsArray(dashboards).map((d: ChartLinkedDashboard) => ({ + title: d.dashboard_title, + id: d.id, + })), + [dashboards], + ); + return ; + }, +); diff --git a/superset-frontend/src/components/ListView/Filters/Search.tsx b/superset-frontend/src/components/ListView/Filters/Search.tsx index 60cfe41bac07..9fd864c44a41 100644 --- a/superset-frontend/src/components/ListView/Filters/Search.tsx +++ b/superset-frontend/src/components/ListView/Filters/Search.tsx @@ -22,12 +22,14 @@ import Icons from 'src/components/Icons'; import { AntdInput } from 'src/components'; import { SELECT_WIDTH } from 'src/components/ListView/utils'; import { FormLabel } from 'src/components/Form'; +import InfoTooltip from 'src/components/InfoTooltip'; import { BaseFilter, FilterHandler } from './Base'; interface SearchHeaderProps extends BaseFilter { Header: string; onSubmit: (val: string) => void; name: string; + toolTipDescription: string | undefined; } const Container = styled.div` @@ -43,7 +45,13 @@ const StyledInput = styled(AntdInput)` `; function SearchFilter( - { Header, name, initialValue, onSubmit }: SearchHeaderProps, + { + Header, + name, + initialValue, + toolTipDescription, + onSubmit, + }: SearchHeaderProps, ref: React.RefObject, ) { const [value, setValue] = useState(initialValue || ''); @@ -69,6 +77,9 @@ function SearchFilter( return ( {Header} + {toolTipDescription && ( + + )} { if (onFilterUpdate) { onFilterUpdate(value); diff --git a/superset-frontend/src/components/ListView/ListView.test.tsx b/superset-frontend/src/components/ListView/ListView.test.tsx new file mode 100644 index 000000000000..9f4da16140f2 --- /dev/null +++ b/superset-frontend/src/components/ListView/ListView.test.tsx @@ -0,0 +1,74 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React from 'react'; +import { render, waitFor } from 'spec/helpers/testing-library'; +import ListView from './ListView'; + +const mockedProps = { + title: 'Data Table', + columns: [ + { + accessor: 'id', + Header: 'ID', + sortable: true, + }, + { + accessor: 'age', + Header: 'Age', + }, + { + accessor: 'name', + Header: 'Name', + }, + { + accessor: 'time', + Header: 'Time', + }, + ], + data: [ + { id: 1, name: 'data 1', age: 10, time: '2020-11-18T07:53:45.354Z' }, + { id: 2, name: 'data 2', age: 1, time: '2020-11-18T07:53:45.354Z' }, + ], + count: 2, + pageSize: 1, + loading: false, + refreshData: jest.fn(), + addSuccessToast: jest.fn(), + addDangerToast: jest.fn(), +}; + +test('redirects to first page when page index is invalid', async () => { + const fetchData = jest.fn(); + window.history.pushState({}, '', '/?pageIndex=9'); + render(, { + useRouter: true, + useQueryParams: true, + }); + await waitFor(() => { + expect(window.location.search).toEqual('?pageIndex=0'); + expect(fetchData).toBeCalledTimes(2); + expect(fetchData).toHaveBeenCalledWith( + expect.objectContaining({ pageIndex: 9 }), + ); + expect(fetchData).toHaveBeenCalledWith( + expect.objectContaining({ pageIndex: 0 }), + ); + }); + fetchData.mockClear(); +}); diff --git a/superset-frontend/src/components/ListView/ListView.tsx b/superset-frontend/src/components/ListView/ListView.tsx index 82cf6b187846..0cdb4ba034d4 100644 --- a/superset-frontend/src/components/ListView/ListView.tsx +++ b/superset-frontend/src/components/ListView/ListView.tsx @@ -271,10 +271,11 @@ function ListView({ pageCount = 1, gotoPage, applyFilterValue, + setSortBy, selectedFlatRows, toggleAllRowsSelected, setViewMode, - state: { pageIndex, pageSize, internalFilters, viewMode }, + state: { pageIndex, pageSize, internalFilters, sortBy, viewMode }, query, } = useListViewState({ bulkSelectColumnConfig, @@ -321,6 +322,12 @@ function ListView({ if (!bulkSelectEnabled) toggleAllRowsSelected(false); }, [bulkSelectEnabled, toggleAllRowsSelected]); + useEffect(() => { + if (!loading && pageIndex > pageCount - 1 && pageCount > 0) { + gotoPage(0); + } + }, [gotoPage, loading, pageCount, pageIndex]); + return ( {allowBulkTagActions && ( @@ -350,11 +357,9 @@ function ListView({ )} {viewMode === 'card' && cardSortSelectOptions && ( setSortBy(value)} options={cardSortSelectOptions} - pageIndex={pageIndex} - pageSize={pageSize} /> )}
@@ -464,7 +469,7 @@ function ListView({
gotoPage(p - 1)} hideFirstAndLastPageLinks /> diff --git a/superset-frontend/src/components/ListView/types.ts b/superset-frontend/src/components/ListView/types.ts index 005d8fde7cc0..ca3a8b3c7092 100644 --- a/superset-frontend/src/components/ListView/types.ts +++ b/superset-frontend/src/components/ListView/types.ts @@ -23,8 +23,6 @@ export interface SortColumn { desc?: boolean; } -export type SortColumns = SortColumn[]; - export interface SelectOption { label: string; value: any; @@ -41,6 +39,7 @@ export interface Filter { Header: ReactNode; key: string; id: string; + toolTipDescription?: string; urlDisplay?: string; operator?: FilterOperator; input?: @@ -84,7 +83,7 @@ export interface FilterValue { export interface FetchDataConfig { pageIndex: number; pageSize: number; - sortBy: SortColumns; + sortBy: SortColumn[]; filters: FilterValue[]; } diff --git a/superset-frontend/src/components/ListView/utils.ts b/superset-frontend/src/components/ListView/utils.ts index 31a9368a1ab4..03540125df26 100644 --- a/superset-frontend/src/components/ListView/utils.ts +++ b/superset-frontend/src/components/ListView/utils.ts @@ -220,7 +220,7 @@ export function useListViewState({ query.sortColumn && query.sortOrder ? [{ id: query.sortColumn, desc: query.sortOrder === 'desc' }] : initialSort, - [query.sortColumn, query.sortOrder], + [initialSort, query.sortColumn, query.sortOrder], ); const initialState = { @@ -256,6 +256,7 @@ export function useListViewState({ pageCount, gotoPage, setAllFilters, + setSortBy, selectedFlatRows, toggleAllRowsSelected, state: { pageIndex, pageSize, sortBy, filters }, @@ -373,6 +374,7 @@ export function useListViewState({ rows, selectedFlatRows, setAllFilters, + setSortBy, state: { pageIndex, pageSize, sortBy, filters, internalFilters, viewMode }, toggleAllRowsSelected, applyFilterValue, diff --git a/superset-frontend/src/components/Select/AsyncSelect.test.tsx b/superset-frontend/src/components/Select/AsyncSelect.test.tsx index 4a2ba0007c43..652b1f0ea25a 100644 --- a/superset-frontend/src/components/Select/AsyncSelect.test.tsx +++ b/superset-frontend/src/components/Select/AsyncSelect.test.tsx @@ -384,12 +384,14 @@ test('removes duplicated values', async () => { }, }); fireEvent(input, paste); - const values = await findAllSelectValues(); - expect(values.length).toBe(4); - expect(values[0]).toHaveTextContent('a'); - expect(values[1]).toHaveTextContent('b'); - expect(values[2]).toHaveTextContent('c'); - expect(values[3]).toHaveTextContent('d'); + await waitFor(async () => { + const values = await findAllSelectValues(); + expect(values.length).toBe(4); + expect(values[0]).toHaveTextContent('a'); + expect(values[1]).toHaveTextContent('b'); + expect(values[2]).toHaveTextContent('c'); + expect(values[3]).toHaveTextContent('d'); + }); }); test('renders a custom label', async () => { @@ -879,7 +881,7 @@ test('fires onChange when pasting a selection', async () => { }, }); fireEvent(input, paste); - expect(onChange).toHaveBeenCalledTimes(1); + await waitFor(() => expect(onChange).toHaveBeenCalledTimes(1)); }); test('does not duplicate options when using numeric values', async () => { @@ -926,7 +928,14 @@ test('pasting an existing option does not duplicate it in multiple mode', async ], totalCount: 3, })); - render(); + render( + , + ); await open(); const input = getElementByClassName('.ant-select-selection-search-input'); const paste = createEvent.paste(input, { @@ -935,8 +944,61 @@ test('pasting an existing option does not duplicate it in multiple mode', async }, }); fireEvent(input, paste); - // Only Peter should be added - expect(await findAllSelectOptions()).toHaveLength(4); + await waitFor(async () => + // Only Peter should be added + expect(await findAllSelectOptions()).toHaveLength(4), + ); +}); + +test('pasting an non-existent option should not add it if allowNewOptions is false', async () => { + render( + ({ data: [], totalCount: 0 })} + />, + ); + await open(); + const input = getElementByClassName('.ant-select-selection-search-input'); + const paste = createEvent.paste(input, { + clipboardData: { + getData: () => 'John', + }, + }); + await waitFor(() => fireEvent(input, paste)); + expect(await findAllSelectOptions()).toHaveLength(0); +}); + +test('onChange is called with the value property when pasting an option that was not loaded yet', async () => { + const onChange = jest.fn(); + render(); + await open(); + const input = getElementByClassName('.ant-select-selection-search-input'); + const lastOption = OPTIONS[OPTIONS.length - 1]; + const paste = createEvent.paste(input, { + clipboardData: { + getData: () => lastOption.label, + }, + }); + fireEvent(input, paste); + await waitFor(() => + expect(onChange).toHaveBeenCalledWith( + expect.objectContaining({ value: lastOption.value }), + expect.anything(), + ), + ); +}); + +test('does not fire onChange if the same value is selected in single mode', async () => { + const onChange = jest.fn(); + render(); + const optionText = 'Emma'; + await open(); + expect(onChange).toHaveBeenCalledTimes(0); + userEvent.click(await findSelectOption(optionText)); + expect(onChange).toHaveBeenCalledTimes(1); + userEvent.click(await findSelectOption(optionText)); + expect(onChange).toHaveBeenCalledTimes(1); }); /* diff --git a/superset-frontend/src/components/Select/AsyncSelect.tsx b/superset-frontend/src/components/Select/AsyncSelect.tsx index d41c87f0478c..5468528893a8 100644 --- a/superset-frontend/src/components/Select/AsyncSelect.tsx +++ b/superset-frontend/src/components/Select/AsyncSelect.tsx @@ -50,10 +50,12 @@ import { mapOptions, getOption, isObject, + isEqual as utilsIsEqual, } from './utils'; import { AsyncSelectProps, AsyncSelectRef, + RawValue, SelectOptionsPagePromise, SelectOptionsType, SelectOptionsTypePage, @@ -220,7 +222,16 @@ const AsyncSelect = forwardRef( const handleOnSelect: SelectProps['onSelect'] = (selectedItem, option) => { if (isSingleMode) { + // on select is fired in single value mode if the same value is selected + const valueChanged = !utilsIsEqual( + selectedItem, + selectValue as RawValue | AntdLabeledValue, + 'value', + ); setSelectValue(selectedItem); + if (valueChanged) { + fireOnChange(); + } } else { setSelectValue(previousState => { const array = ensureIsArray(previousState); @@ -234,8 +245,8 @@ const AsyncSelect = forwardRef( } return previousState; }); + fireOnChange(); } - fireOnChange(); onSelect?.(selectedItem, option); }; @@ -523,8 +534,18 @@ const AsyncSelect = forwardRef( ); const getPastedTextValue = useCallback( - (text: string) => { - const option = getOption(text, fullSelectOptions, true); + async (text: string) => { + let option = getOption(text, fullSelectOptions, true); + if (!option && !allValuesLoaded) { + const fetchOptions = options as SelectOptionsPagePromise; + option = await fetchOptions(text, 0, pageSize).then( + ({ data }: SelectOptionsTypePage) => + data.find(item => item.label === text), + ); + } + if (!option && !allowNewOptions) { + return undefined; + } const value: AntdLabeledValue = { label: text, value: text, @@ -535,20 +556,25 @@ const AsyncSelect = forwardRef( } return value; }, - [fullSelectOptions], + [allValuesLoaded, allowNewOptions, fullSelectOptions, options, pageSize], ); - const onPaste = (e: ClipboardEvent) => { + const onPaste = async (e: ClipboardEvent) => { const pastedText = e.clipboardData.getData('text'); if (isSingleMode) { - setSelectValue(getPastedTextValue(pastedText)); + const value = await getPastedTextValue(pastedText); + if (value) { + setSelectValue(value); + } } else { const token = tokenSeparators.find(token => pastedText.includes(token)); const array = token ? uniq(pastedText.split(token)) : [pastedText]; - const values = array.map(item => getPastedTextValue(item)); + const values = ( + await Promise.all(array.map(item => getPastedTextValue(item))) + ).filter(item => item !== undefined) as AntdLabeledValue[]; setSelectValue(previous => [ ...((previous || []) as AntdLabeledValue[]), - ...values, + ...values.filter(value => !hasOption(value.value, previous)), ]); } fireOnChange(); diff --git a/superset-frontend/src/components/Select/Select.test.tsx b/superset-frontend/src/components/Select/Select.test.tsx index 291035329518..bbe09a27f4f8 100644 --- a/superset-frontend/src/components/Select/Select.test.tsx +++ b/superset-frontend/src/components/Select/Select.test.tsx @@ -1039,6 +1039,7 @@ test('pasting an existing option does not duplicate it in multiple mode', async options={options} mode="multiple" allowSelectAll={false} + allowNewOptions />, ); await open(); @@ -1053,6 +1054,31 @@ test('pasting an existing option does not duplicate it in multiple mode', async expect(await findAllSelectOptions()).toHaveLength(4); }); +test('pasting an non-existent option should not add it if allowNewOptions is false', async () => { + render(); + const optionText = 'Emma'; + await open(); + expect(onChange).toHaveBeenCalledTimes(0); + userEvent.click(await findSelectOption(optionText)); + expect(onChange).toHaveBeenCalledTimes(1); + userEvent.click(await findSelectOption(optionText)); + expect(onChange).toHaveBeenCalledTimes(1); +}); + /* TODO: Add tests that require scroll interaction. Needs further investigation. - Fetches more data when scrolling and more data is available diff --git a/superset-frontend/src/components/Select/Select.tsx b/superset-frontend/src/components/Select/Select.tsx index f4f9565abb8a..d92b86318d0a 100644 --- a/superset-frontend/src/components/Select/Select.tsx +++ b/superset-frontend/src/components/Select/Select.tsx @@ -53,6 +53,7 @@ import { hasCustomLabels, getOption, isObject, + isEqual as utilsIsEqual, } from './utils'; import { RawValue, SelectOptionsType, SelectProps } from './types'; import { @@ -227,7 +228,16 @@ const Select = forwardRef( const handleOnSelect: SelectProps['onSelect'] = (selectedItem, option) => { if (isSingleMode) { + // on select is fired in single value mode if the same value is selected + const valueChanged = !utilsIsEqual( + selectedItem, + selectValue as RawValue | AntdLabeledValue, + 'value', + ); setSelectValue(selectedItem); + if (valueChanged) { + fireOnChange(); + } } else { setSelectValue(previousState => { const array = ensureIsArray(previousState); @@ -259,8 +269,8 @@ const Select = forwardRef( } return previousState; }); + fireOnChange(); } - fireOnChange(); onSelect?.(selectedItem, option); }; @@ -533,6 +543,9 @@ const Select = forwardRef( const getPastedTextValue = useCallback( (text: string) => { const option = getOption(text, fullSelectOptions, true); + if (!option && !allowNewOptions) { + return undefined; + } if (labelInValue) { const value: AntdLabeledValue = { label: text, @@ -546,17 +559,22 @@ const Select = forwardRef( } return option ? (isObject(option) ? option.value! : option) : text; }, - [fullSelectOptions, labelInValue], + [allowNewOptions, fullSelectOptions, labelInValue], ); const onPaste = (e: ClipboardEvent) => { const pastedText = e.clipboardData.getData('text'); if (isSingleMode) { - setSelectValue(getPastedTextValue(pastedText)); + const value = getPastedTextValue(pastedText); + if (value) { + setSelectValue(value); + } } else { const token = tokenSeparators.find(token => pastedText.includes(token)); const array = token ? uniq(pastedText.split(token)) : [pastedText]; - const values = array.map(item => getPastedTextValue(item)); + const values = array + .map(item => getPastedTextValue(item)) + .filter(item => item !== undefined); if (labelInValue) { setSelectValue(previous => [ ...((previous || []) as AntdLabeledValue[]), diff --git a/superset-frontend/src/components/Select/utils.tsx b/superset-frontend/src/components/Select/utils.tsx index 0b638f4f0128..67b2a0191b38 100644 --- a/superset-frontend/src/components/Select/utils.tsx +++ b/superset-frontend/src/components/Select/utils.tsx @@ -49,22 +49,24 @@ export function getValue( return isLabeledValue(option) ? option.value : option; } +export function isEqual(a: V | LabeledValue, b: V | LabeledValue, key: string) { + const actualA = isObject(a) && key in a ? a[key] : a; + const actualB = isObject(b) && key in b ? b[key] : b; + // When comparing the values we use the equality + // operator to automatically convert different types + // eslint-disable-next-line eqeqeq + return actualA == actualB; +} + export function getOption( value: V, options?: V | LabeledValue | (V | LabeledValue)[], checkLabel = false, ): V | LabeledValue { const optionsArray = ensureIsArray(options); - // When comparing the values we use the equality - // operator to automatically convert different types return optionsArray.find( x => - // eslint-disable-next-line eqeqeq - x == value || - (isObject(x) && - // eslint-disable-next-line eqeqeq - (('value' in x && x.value == value) || - (checkLabel && 'label' in x && x.label === value))), + isEqual(x, value, 'value') || (checkLabel && isEqual(x, value, 'label')), ); } diff --git a/superset-frontend/src/components/TelemetryPixel/index.tsx b/superset-frontend/src/components/TelemetryPixel/index.tsx index 6c7ce106e696..f0223ac70d39 100644 --- a/superset-frontend/src/components/TelemetryPixel/index.tsx +++ b/superset-frontend/src/components/TelemetryPixel/index.tsx @@ -47,6 +47,7 @@ const TelemetryPixel = ({ const pixelPath = `https://apachesuperset.gateway.scarf.sh/pixel/${PIXEL_ID}/${version}/${sha}/${build}`; return process.env.SCARF_ANALYTICS === 'false' ? null : ( ({ getKey = item => item as unknown as React.Key, maxLinks = 20, }: TruncatedListProps) { - const itemsNotInTooltipRef = useRef(null); - const plusRef = useRef(null); - const [elementsTruncated, hasHiddenElements] = useTruncation( - itemsNotInTooltipRef, - plusRef, - ) as [number, boolean]; + const [itemsNotInTooltipRef, plusRef, elementsTruncated, hasHiddenElements] = + useTruncation(); const nMoreItems = useMemo( () => (items.length > maxLinks ? items.length - maxLinks : undefined), diff --git a/superset-frontend/src/constants.ts b/superset-frontend/src/constants.ts index b64cf913755b..ed7d62dbe83a 100644 --- a/superset-frontend/src/constants.ts +++ b/superset-frontend/src/constants.ts @@ -189,3 +189,11 @@ export const DEFAULT_COMMON_BOOTSTRAP_DATA: CommonBootstrapData = { export const DEFAULT_BOOTSTRAP_DATA: BootstrapData = { common: DEFAULT_COMMON_BOOTSTRAP_DATA, }; + +export enum FilterPlugins { + Select = 'filter_select', + Range = 'filter_range', + Time = 'filter_time', + TimeColumn = 'filter_timecolumn', + TimeGrain = 'filter_timegrain', +} diff --git a/superset-frontend/src/dashboard/actions/dashboardState.js b/superset-frontend/src/dashboard/actions/dashboardState.js index bdbf43090310..a086a85c1614 100644 --- a/superset-frontend/src/dashboard/actions/dashboardState.js +++ b/superset-frontend/src/dashboard/actions/dashboardState.js @@ -603,11 +603,6 @@ export function setActiveTab(tabId, prevTabId) { return { type: SET_ACTIVE_TAB, tabId, prevTabId }; } -export const SET_ACTIVE_TABS = 'SET_ACTIVE_TABS'; -export function setActiveTabs(activeTabs) { - return { type: SET_ACTIVE_TABS, activeTabs }; -} - export const SET_FOCUSED_FILTER_FIELD = 'SET_FOCUSED_FILTER_FIELD'; export function setFocusedFilterField(chartId, column) { return { type: SET_FOCUSED_FILTER_FIELD, chartId, column }; diff --git a/superset-frontend/src/dashboard/components/CssEditor/CssEditor.test.tsx b/superset-frontend/src/dashboard/components/CssEditor/CssEditor.test.tsx index 16b2a1afbb7c..28ac7672f691 100644 --- a/superset-frontend/src/dashboard/components/CssEditor/CssEditor.test.tsx +++ b/superset-frontend/src/dashboard/components/CssEditor/CssEditor.test.tsx @@ -21,6 +21,7 @@ import { render, screen, waitFor } from 'spec/helpers/testing-library'; import { CssEditor as AceCssEditor } from 'src/components/AsyncAceEditor'; import { IAceEditorProps } from 'react-ace'; import userEvent from '@testing-library/user-event'; +import fetchMock from 'fetch-mock'; import CssEditor from '.'; jest.mock('src/components/AsyncAceEditor', () => ({ @@ -33,46 +34,59 @@ jest.mock('src/components/AsyncAceEditor', () => ({ })); const templates = [ - { label: 'Template A', css: 'background-color: red;' }, - { label: 'Template B', css: 'background-color: blue;' }, - { label: 'Template C', css: 'background-color: yellow;' }, + { template_name: 'Template A', css: 'background-color: red;' }, + { template_name: 'Template B', css: 'background-color: blue;' }, + { template_name: 'Template C', css: 'background-color: yellow;' }, ]; +fetchMock.get('glob:*/csstemplateasyncmodelview/api/read', { + result: templates, +}); + AceCssEditor.preload = () => new Promise(() => {}); -test('renders with default props', () => { - render(Click} />); +const defaultProps = { + triggerNode: <>Click, + addDangerToast: jest.fn(), +}; + +test('renders with default props', async () => { + await waitFor(() => render()); expect(screen.getByRole('button', { name: 'Click' })).toBeInTheDocument(); }); -test('renders with initial CSS', () => { +test('renders with initial CSS', async () => { const initialCss = 'margin: 10px;'; - render(Click} initialCss={initialCss} />); + await waitFor(() => + render(), + ); userEvent.click(screen.getByRole('button', { name: 'Click' })); expect(screen.getByText(initialCss)).toBeInTheDocument(); }); test('renders with templates', async () => { - render(Click} templates={templates} />); + await waitFor(() => render()); userEvent.click(screen.getByRole('button', { name: 'Click' })); userEvent.hover(screen.getByText('Load a CSS template')); await waitFor(() => { templates.forEach(template => - expect(screen.getByText(template.label)).toBeInTheDocument(), + expect(screen.getByText(template.template_name)).toBeInTheDocument(), ); }); }); -test('triggers onChange when using the editor', () => { +test('triggers onChange when using the editor', async () => { const onChange = jest.fn(); const initialCss = 'margin: 10px;'; const additionalCss = 'color: red;'; - render( - Click} - initialCss={initialCss} - onChange={onChange} - />, + await waitFor(() => + render( + , + ), ); userEvent.click(screen.getByRole('button', { name: 'Click' })); expect(onChange).not.toHaveBeenCalled(); @@ -82,12 +96,8 @@ test('triggers onChange when using the editor', () => { test('triggers onChange when selecting a template', async () => { const onChange = jest.fn(); - render( - Click} - templates={templates} - onChange={onChange} - />, + await waitFor(() => + render(), ); userEvent.click(screen.getByRole('button', { name: 'Click' })); userEvent.click(screen.getByText('Load a CSS template')); diff --git a/superset-frontend/src/dashboard/components/CssEditor/index.jsx b/superset-frontend/src/dashboard/components/CssEditor/index.jsx index ad12cb6c78a9..9fcd1768a898 100644 --- a/superset-frontend/src/dashboard/components/CssEditor/index.jsx +++ b/superset-frontend/src/dashboard/components/CssEditor/index.jsx @@ -21,7 +21,7 @@ import PropTypes from 'prop-types'; import { AntdDropdown } from 'src/components'; import { Menu } from 'src/components/Menu'; import Button from 'src/components/Button'; -import { t, styled } from '@superset-ui/core'; +import { t, styled, SupersetClient } from '@superset-ui/core'; import ModalTrigger from 'src/components/ModalTrigger'; import { CssEditor as AceCssEditor } from 'src/components/AsyncAceEditor'; @@ -47,7 +47,7 @@ const propTypes = { initialCss: PropTypes.string, triggerNode: PropTypes.node.isRequired, onChange: PropTypes.func, - templates: PropTypes.array, + addDangerToast: PropTypes.func.isRequired, }; const defaultProps = { @@ -60,6 +60,7 @@ class CssEditor extends React.PureComponent { super(props); this.state = { css: props.initialCss, + templates: [], }; this.changeCss = this.changeCss.bind(this); this.changeCssTemplate = this.changeCssTemplate.bind(this); @@ -67,6 +68,22 @@ class CssEditor extends React.PureComponent { componentDidMount() { AceCssEditor.preload(); + + SupersetClient.get({ endpoint: '/csstemplateasyncmodelview/api/read' }) + .then(({ json }) => { + const templates = json.result.map(row => ({ + value: row.template_name, + css: row.css, + label: row.template_name, + })); + + this.setState({ templates }); + }) + .catch(() => { + this.props.addDangerToast( + t('An error occurred while fetching available CSS templates'), + ); + }); } changeCss(css) { @@ -80,10 +97,10 @@ class CssEditor extends React.PureComponent { } renderTemplateSelector() { - if (this.props.templates) { + if (this.state.templates) { const menu = ( - {this.props.templates.map(template => ( + {this.state.templates.map(template => ( {template.label} ))} diff --git a/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx b/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx index 2756e7135967..fa6a311e77a7 100644 --- a/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx +++ b/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx @@ -44,7 +44,7 @@ import BuilderComponentPane from 'src/dashboard/components/BuilderComponentPane' import DashboardHeader from 'src/dashboard/containers/DashboardHeader'; import Icons from 'src/components/Icons'; import IconButton from 'src/dashboard/components/IconButton'; -import DragDroppable from 'src/dashboard/components/dnd/DragDroppable'; +import { Droppable } from 'src/dashboard/components/dnd/DragDroppable'; import DashboardComponent from 'src/dashboard/containers/DashboardComponent'; import WithPopoverMenu from 'src/dashboard/components/menu/WithPopoverMenu'; import getDirectPathToTabIndex from 'src/dashboard/util/getDirectPathToTabIndex'; @@ -81,6 +81,7 @@ import { MAIN_HEADER_HEIGHT, OPEN_FILTER_BAR_MAX_WIDTH, OPEN_FILTER_BAR_WIDTH, + EMPTY_CONTAINER_Z_INDEX, } from 'src/dashboard/constants'; import { getRootLevelTabsComponent, shouldFocusTabs } from './utils'; import DashboardContainer from './DashboardContainer'; @@ -107,12 +108,27 @@ const StickyPanel = styled.div<{ width: number }>` // @z-index-above-dashboard-popovers (99) + 1 = 100 const StyledHeader = styled.div` - grid-column: 2; - grid-row: 1; - position: sticky; - top: 0; - z-index: 100; - max-width: 100vw; + ${({ theme }) => css` + grid-column: 2; + grid-row: 1; + position: sticky; + top: 0; + z-index: 100; + max-width: 100vw; + + .empty-droptarget:before { + position: absolute; + content: ''; + display: none; + width: calc(100% - ${theme.gridUnit * 2}px); + height: calc(100% - ${theme.gridUnit * 2}px); + left: ${theme.gridUnit}px; + top: ${theme.gridUnit}px; + border: 1px dashed transparent; + border-radius: ${theme.gridUnit}px; + opacity: 0.5; + } + `} `; const StyledContent = styled.div<{ @@ -170,6 +186,11 @@ const DashboardContentWrapper = styled.div` pointer-events: none; } + .grid-row.grid-row--hovered:after, + .dashboard-component-tabs > .grid-row--hovered:after { + border: 2px dashed ${theme.colors.primary.base}; + } + .resizable-container { & .dashboard-component-chart-holder { .dashboard-chart { @@ -211,13 +232,9 @@ const DashboardContentWrapper = styled.div` /* provide hit area in case row contents is edge to edge */ .dashboard-component-tabs-content { - .dragdroppable-row { + > .dragdroppable-row { padding-top: ${theme.gridUnit * 4}px; } - - & > div:not(:last-child):not(.empty-droptarget) { - margin-bottom: ${theme.gridUnit * 4}px; - } } .dashboard-component-chart-holder { @@ -250,25 +267,21 @@ const DashboardContentWrapper = styled.div` } & > .empty-droptarget { + z-index: ${EMPTY_CONTAINER_Z_INDEX}; position: absolute; width: 100%; } & > .empty-droptarget:first-child:not(.empty-droptarget--full) { height: ${theme.gridUnit * 4}px; - top: -2px; - z-index: 10; + top: 0; } & > .empty-droptarget:last-child { - height: ${theme.gridUnit * 3}px; - bottom: 0; + height: ${theme.gridUnit * 4}px; + bottom: ${-theme.gridUnit * 4}px; } } - - .empty-droptarget:first-child .drop-indicator--bottom { - top: ${theme.gridUnit * 6}px; - } `} `; @@ -399,6 +412,7 @@ const DashboardBuilder: FC = () => { const handleChangeTab = useCallback( ({ pathToTabIndex }: { pathToTabIndex: string[] }) => { dispatch(setDirectPathToChild(pathToTabIndex)); + window.scrollTo(0, 0); }, [dispatch], ); @@ -616,8 +630,9 @@ const DashboardBuilder: FC = () => { )} {/* @ts-ignore */} - = () => { style={draggableStyle} > {renderDraggableContent} - + { + jest.useFakeTimers(); +}); + +afterAll(() => { + jest.useRealTimers(); +}); + test('should render children', () => { const { getByTestId } = render( @@ -32,7 +40,7 @@ test('should render children', () => { expect(getByTestId('mock-children')).toBeInTheDocument(); }); -test('should update the style on dragging state', () => { +test('should update the style on dragging state', async () => { const defaultProps = { label: Test label, tooltipTitle: 'This is a tooltip title', @@ -69,7 +77,13 @@ test('should update the style on dragging state', () => { container.getElementsByClassName('dragdroppable--dragging'), ).toHaveLength(0); fireEvent.dragStart(getByText('Label 1')); + await waitFor(() => jest.runAllTimers()); expect( container.getElementsByClassName('dragdroppable--dragging'), ).toHaveLength(1); + fireEvent.dragEnd(getByText('Label 1')); + // immediately discards dragging state after dragEnd + expect( + container.getElementsByClassName('dragdroppable--dragging'), + ).toHaveLength(0); }); diff --git a/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardWrapper.tsx b/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardWrapper.tsx index 5bb193de1bd4..0015000162a1 100644 --- a/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardWrapper.tsx +++ b/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardWrapper.tsx @@ -17,11 +17,12 @@ * under the License. */ import React, { useEffect } from 'react'; -import { css, styled } from '@superset-ui/core'; +import { FAST_DEBOUNCE, css, styled } from '@superset-ui/core'; import { RootState } from 'src/dashboard/types'; import { useSelector } from 'react-redux'; import { useDragDropManager } from 'react-dnd'; import classNames from 'classnames'; +import { debounce } from 'lodash'; const StyledDiv = styled.div` ${({ theme }) => css` @@ -32,10 +33,26 @@ const StyledDiv = styled.div` flex: 1; /* Special cases */ - &.dragdroppable--dragging - .dashboard-component-tabs-content - > .empty-droptarget.empty-droptarget--full { - height: 100%; + &.dragdroppable--dragging { + & + .dashboard-component-tabs-content + > .empty-droptarget.empty-droptarget--full { + height: 100%; + } + & .empty-droptarget:before { + display: block; + border-color: ${theme.colors.primary.light1}; + background-color: ${theme.colors.primary.light3}; + } + & .grid-row:after { + border-style: hidden; + } + & .droptarget-side:last-child { + inset-inline-end: 0; + } + & .droptarget-edge:last-child { + inset-block-end: 0; + } } /* A row within a column has inset hover menu */ @@ -106,12 +123,22 @@ const DashboardWrapper: React.FC = ({ children }) => { useEffect(() => { const monitor = dragDropManager.getMonitor(); + const debouncedSetIsDragged = debounce(setIsDragged, FAST_DEBOUNCE); const unsub = monitor.subscribeToStateChange(() => { - setIsDragged(monitor.isDragging()); + const isDragging = monitor.isDragging(); + if (isDragging) { + // set a debounced function to prevent HTML5 drag source + // from interfering with the drop zone highlighting + debouncedSetIsDragged(true); + } else { + debouncedSetIsDragged.cancel(); + setIsDragged(false); + } }); return () => { unsub(); + debouncedSetIsDragged.cancel(); }; }, [dragDropManager]); diff --git a/superset-frontend/src/dashboard/components/DashboardGrid.jsx b/superset-frontend/src/dashboard/components/DashboardGrid.jsx index 70cf65218f2c..93444c74238a 100644 --- a/superset-frontend/src/dashboard/components/DashboardGrid.jsx +++ b/superset-frontend/src/dashboard/components/DashboardGrid.jsx @@ -23,7 +23,7 @@ import { addAlpha, css, styled, t } from '@superset-ui/core'; import { EmptyStateBig } from 'src/components/EmptyState'; import { componentShape } from '../util/propShapes'; import DashboardComponent from '../containers/DashboardComponent'; -import DragDroppable from './dnd/DragDroppable'; +import { Droppable } from './dnd/DragDroppable'; import { GRID_GUTTER_SIZE, GRID_COLUMN_COUNT } from '../util/constants'; import { TAB_TYPE } from '../util/componentTypes'; @@ -41,15 +41,8 @@ const propTypes = { const defaultProps = {}; -const renderDraggableContentBottom = dropProps => - dropProps.dropIndicatorProps && ( -
- ); - -const renderDraggableContentTop = dropProps => - dropProps.dropIndicatorProps && ( -
- ); +const renderDraggableContent = dropProps => + dropProps.dropIndicatorProps &&
; const DashboardEmptyStateContainer = styled.div` position: absolute; @@ -60,28 +53,42 @@ const DashboardEmptyStateContainer = styled.div` `; const GridContent = styled.div` - ${({ theme }) => css` + ${({ theme, editMode }) => css` display: flex; flex-direction: column; /* gutters between rows */ & > div:not(:last-child):not(.empty-droptarget) { - margin-bottom: ${theme.gridUnit * 4}px; + ${!editMode && `margin-bottom: ${theme.gridUnit * 4}px`}; } - & > .empty-droptarget { + .empty-droptarget { width: 100%; - height: 100%; + height: ${theme.gridUnit * 4}px; + display: flex; + align-items: center; + justify-content: center; + border-radius: ${theme.gridUnit}px; + overflow: hidden; + + &:before { + content: ''; + display: block; + width: calc(100% - ${theme.gridUnit * 2}px); + height: calc(100% - ${theme.gridUnit * 2}px); + border: 1px dashed transparent; + border-radius: ${theme.gridUnit}px; + opacity: 0.5; + } } & > .empty-droptarget:first-child { - height: ${theme.gridUnit * 12}px; - margin-top: ${theme.gridUnit * -6}px; + height: ${theme.gridUnit * 4}px; + margin-top: ${theme.gridUnit * -4}px; } & > .empty-droptarget:last-child { - height: ${theme.gridUnit * 12}px; - margin-top: ${theme.gridUnit * -6}px; + height: ${theme.gridUnit * 24}px; } & > .empty-droptarget.empty-droptarget--full:only-child { @@ -265,10 +272,14 @@ class DashboardGrid extends React.PureComponent { )}
- + {/* make the area above components droppable */} {editMode && ( - - {renderDraggableContentTop} - + {renderDraggableContent} + )} {gridComponent?.children?.map((id, index) => ( - + + + {/* make the area below components droppable */} + {editMode && ( + + {renderDraggableContent} + + )} + ))} - {/* make the area below components droppable */} - {editMode && gridComponent?.children?.length > 0 && ( - - {renderDraggableContentBottom} - - )} {isResizing && Array(GRID_COLUMN_COUNT) .fill(null) diff --git a/superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/HeaderActionsDropdown.test.tsx b/superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/HeaderActionsDropdown.test.tsx index cefdbafa73d4..2a64f75309d5 100644 --- a/superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/HeaderActionsDropdown.test.tsx +++ b/superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/HeaderActionsDropdown.test.tsx @@ -24,9 +24,9 @@ import userEvent from '@testing-library/user-event'; import fetchMock from 'fetch-mock'; import { HeaderDropdownProps } from 'src/dashboard/components/Header/types'; import injectCustomCss from 'src/dashboard/util/injectCustomCss'; -import HeaderActionsDropdown from '.'; +import { HeaderActionsDropdown } from '.'; -const createProps = () => ({ +const createProps = (): HeaderDropdownProps => ({ addSuccessToast: jest.fn(), addDangerToast: jest.fn(), customCss: '.ant-menu {margin-left: 100px;}', @@ -67,6 +67,7 @@ const createProps = () => ({ userCanCurate: false, lastModifiedTime: 0, isDropdownVisible: true, + manageEmbedded: jest.fn(), dataMask: {}, logEvent: jest.fn(), }); @@ -229,9 +230,9 @@ test('should show the properties modal', async () => { describe('UNSAFE_componentWillReceiveProps', () => { let wrapper: any; + const mockedProps = createProps(); const props = { ...mockedProps, customCss: '' }; - beforeEach(() => { wrapper = shallow(); wrapper.setState({ css: props.customCss }); diff --git a/superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/index.jsx b/superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/index.jsx index f1a3f59039bd..1d201f53e290 100644 --- a/superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/index.jsx +++ b/superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/index.jsx @@ -16,10 +16,11 @@ * specific language governing permissions and limitations * under the License. */ -import React from 'react'; +import React, { PureComponent } from 'react'; import PropTypes from 'prop-types'; import { isEmpty } from 'lodash'; -import { SupersetClient, t } from '@superset-ui/core'; +import { connect } from 'react-redux'; +import { t } from '@superset-ui/core'; import { Menu } from 'src/components/Menu'; import { URL_PARAMS } from 'src/constants'; import ShareMenuItems from 'src/dashboard/components/menu/ShareMenuItems'; @@ -45,6 +46,7 @@ const propTypes = { customCss: PropTypes.string, colorNamespace: PropTypes.string, colorScheme: PropTypes.string, + directPathToChild: PropTypes.array, onChange: PropTypes.func.isRequired, updateCss: PropTypes.func.isRequired, forceRefreshAllCharts: PropTypes.func.isRequired, @@ -90,7 +92,11 @@ const MENU_KEYS = { MANAGE_EMAIL_REPORT: 'manage-email-report', }; -class HeaderActionsDropdown extends React.PureComponent { +const mapStateToProps = state => ({ + directPathToChild: state.dashboardState.directPathToChild, +}); + +export class HeaderActionsDropdown extends PureComponent { static discardChanges() { window.location.reload(); } @@ -99,7 +105,6 @@ class HeaderActionsDropdown extends React.PureComponent { super(props); this.state = { css: props.customCss, - cssTemplates: [], showReportSubMenu: null, }; @@ -109,23 +114,6 @@ class HeaderActionsDropdown extends React.PureComponent { this.setShowReportSubMenu = this.setShowReportSubMenu.bind(this); } - UNSAFE_componentWillMount() { - SupersetClient.get({ endpoint: '/csstemplateasyncmodelview/api/read' }) - .then(({ json }) => { - const cssTemplates = json.result.map(row => ({ - value: row.template_name, - css: row.css, - label: row.template_name, - })); - this.setState({ cssTemplates }); - }) - .catch(() => { - this.props.addDangerToast( - t('An error occurred while fetching available CSS templates'), - ); - }); - } - UNSAFE_componentWillReceiveProps(nextProps) { if (this.props.customCss !== nextProps.customCss) { this.setState({ css: nextProps.customCss }, () => { @@ -204,6 +192,7 @@ class HeaderActionsDropdown extends React.PureComponent { addDangerToast, setIsDropdownVisible, isDropdownVisible, + directPathToChild, ...rest } = this.props; @@ -222,6 +211,8 @@ class HeaderActionsDropdown extends React.PureComponent { const refreshIntervalOptions = dashboardInfo.common?.conf?.DASHBOARD_AUTO_REFRESH_INTERVALS; + const dashboardComponentId = [...(directPathToChild || [])].pop(); + return ( {!editMode && ( @@ -257,8 +248,8 @@ class HeaderActionsDropdown extends React.PureComponent { {t('Edit CSS')}} initialCss={this.state.css} - templates={this.state.cssTemplates} onChange={this.changeCss} + addDangerToast={addDangerToast} /> )} @@ -317,6 +308,7 @@ class HeaderActionsDropdown extends React.PureComponent { addSuccessToast={addSuccessToast} addDangerToast={addDangerToast} dashboardId={dashboardId} + dashboardComponentId={dashboardComponentId} /> )} @@ -385,4 +377,4 @@ class HeaderActionsDropdown extends React.PureComponent { HeaderActionsDropdown.propTypes = propTypes; HeaderActionsDropdown.defaultProps = defaultProps; -export default HeaderActionsDropdown; +export default connect(mapStateToProps)(HeaderActionsDropdown); diff --git a/superset-frontend/src/dashboard/components/Header/index.jsx b/superset-frontend/src/dashboard/components/Header/index.jsx index 485185d6073f..e7d11299c8f7 100644 --- a/superset-frontend/src/dashboard/components/Header/index.jsx +++ b/superset-frontend/src/dashboard/components/Header/index.jsx @@ -41,7 +41,7 @@ import { AntdButton } from 'src/components/'; import { findPermission } from 'src/utils/findPermission'; import { Tooltip } from 'src/components/Tooltip'; import { safeStringify } from 'src/utils/safeStringify'; -import HeaderActionsDropdown from 'src/dashboard/components/Header/HeaderActionsDropdown'; +import ConnectedHeaderActionsDropdown from 'src/dashboard/components/Header/HeaderActionsDropdown'; import PublishedStatus from 'src/dashboard/components/PublishedStatus'; import UndoRedoKeyListeners from 'src/dashboard/components/UndoRedoKeyListeners'; import PropertiesModal from 'src/dashboard/components/PropertiesModal'; @@ -643,7 +643,7 @@ class Header extends React.PureComponent { onVisibleChange: this.setIsDropdownVisible, }} additionalActionsMenu={ - void; userCanEdit: boolean; userCanSave: boolean; + userCanShare: boolean; + userCanCurate: boolean; + isDropdownVisible: boolean; + manageEmbedded: () => void; + dataMask: any; lastModifiedTime: number; + logEvent: () => void; } export interface HeaderProps { diff --git a/superset-frontend/src/dashboard/components/RefreshIntervalModal.test.tsx b/superset-frontend/src/dashboard/components/RefreshIntervalModal.test.tsx index 17c08e0701f8..ef419ba20f0b 100644 --- a/superset-frontend/src/dashboard/components/RefreshIntervalModal.test.tsx +++ b/superset-frontend/src/dashboard/components/RefreshIntervalModal.test.tsx @@ -22,7 +22,7 @@ import userEvent from '@testing-library/user-event'; import fetchMock from 'fetch-mock'; import RefreshIntervalModal from 'src/dashboard/components/RefreshIntervalModal'; -import HeaderActionsDropdown from 'src/dashboard/components/Header/HeaderActionsDropdown'; +import { HeaderActionsDropdown } from 'src/dashboard/components/Header/HeaderActionsDropdown'; const createProps = () => ({ addSuccessToast: jest.fn(), diff --git a/superset-frontend/src/dashboard/components/dnd/DragDroppable.jsx b/superset-frontend/src/dashboard/components/dnd/DragDroppable.jsx index 6a49f9887550..b5b7373f40a5 100644 --- a/superset-frontend/src/dashboard/components/dnd/DragDroppable.jsx +++ b/superset-frontend/src/dashboard/components/dnd/DragDroppable.jsx @@ -25,12 +25,7 @@ import { css, styled } from '@superset-ui/core'; import { componentShape } from '../../util/propShapes'; import { dragConfig, dropConfig } from './dragDroppableConfig'; -import { - DROP_TOP, - DROP_RIGHT, - DROP_BOTTOM, - DROP_LEFT, -} from '../../util/getDropPosition'; +import { DROP_FORBIDDEN } from '../../util/getDropPosition'; const propTypes = { children: PropTypes.func, @@ -39,6 +34,7 @@ const propTypes = { parentComponent: componentShape, depth: PropTypes.number.isRequired, disableDragDrop: PropTypes.bool, + dropToChild: PropTypes.bool, orientation: PropTypes.oneOf(['row', 'column']), index: PropTypes.number.isRequired, style: PropTypes.object, @@ -61,6 +57,7 @@ const defaultProps = { style: null, parentComponent: null, disableDragDrop: false, + dropToChild: false, children() {}, onDrop() {}, onHover() {}, @@ -77,6 +74,14 @@ const defaultProps = { const DragDroppableStyles = styled.div` ${({ theme }) => css` position: relative; + /* + Next line is a workaround for a bug in react-dnd where the drag + preview expands outside of the bounds of the drag source card, see: + https://github.com/react-dnd/react-dnd/issues/832#issuecomment-442071628 + */ + &.dragdroppable--edit-mode { + transform: translate3d(0, 0, 0); + } &.dragdroppable--dragging { opacity: 0.2; @@ -90,49 +95,18 @@ const DragDroppableStyles = styled.div` z-index: 10; } - &.empty-droptarget--full > .drop-indicator--top { - height: 100%; - opacity: 0.3; - } - & { .drop-indicator { display: block; background-color: ${theme.colors.primary.base}; position: absolute; z-index: 10; - } - - .drop-indicator--top { - top: ${-theme.gridUnit - 2}px; - left: 0; - height: ${theme.gridUnit}px; - width: 100%; - min-width: ${theme.gridUnit * 4}px; - } - - .drop-indicator--bottom { - bottom: ${-theme.gridUnit - 2}px; - left: 0; - height: ${theme.gridUnit}px; + opacity: 0.3; width: 100%; - min-width: ${theme.gridUnit * 4}px; - } - - .drop-indicator--right { - top: 0; - left: calc(100% - ${theme.gridUnit}px); height: 100%; - width: ${theme.gridUnit}px; - min-height: ${theme.gridUnit * 4}px; - } - - .drop-indicator--left { - top: 0; - left: 0; - height: 100%; - width: ${theme.gridUnit}px; - min-height: ${theme.gridUnit * 4}px; + &.drop-indicator--forbidden { + background-color: ${theme.colors.error.light1}; + } } } `}; @@ -189,10 +163,7 @@ export class UnwrappedDragDroppable extends React.PureComponent { ? { className: cx( 'drop-indicator', - dropIndicator === DROP_TOP && 'drop-indicator--top', - dropIndicator === DROP_BOTTOM && 'drop-indicator--bottom', - dropIndicator === DROP_LEFT && 'drop-indicator--left', - dropIndicator === DROP_RIGHT && 'drop-indicator--right', + dropIndicator === DROP_FORBIDDEN && 'drop-indicator--forbidden', ), } : null; @@ -211,6 +182,7 @@ export class UnwrappedDragDroppable extends React.PureComponent { data-test="dragdroppable-object" className={cx( 'dragdroppable', + editMode && 'dragdroppable--edit-mode', orientation === 'row' && 'dragdroppable-row', orientation === 'column' && 'dragdroppable-column', isDragging && 'dragdroppable--dragging', @@ -226,6 +198,9 @@ export class UnwrappedDragDroppable extends React.PureComponent { UnwrappedDragDroppable.propTypes = propTypes; UnwrappedDragDroppable.defaultProps = defaultProps; +export const Draggable = DragSource(...dragConfig)(UnwrappedDragDroppable); +export const Droppable = DropTarget(...dropConfig)(UnwrappedDragDroppable); + // note that the composition order here determines using // component.method() vs decoratedComponentInstance.method() in the drag/drop config export default DragSource(...dragConfig)( diff --git a/superset-frontend/src/dashboard/components/dnd/handleDrop.js b/superset-frontend/src/dashboard/components/dnd/handleDrop.js index 450a60867c4d..f4b847cd6d41 100644 --- a/superset-frontend/src/dashboard/components/dnd/handleDrop.js +++ b/superset-frontend/src/dashboard/components/dnd/handleDrop.js @@ -18,10 +18,7 @@ */ import getDropPosition, { clearDropCache, - DROP_TOP, - DROP_RIGHT, - DROP_BOTTOM, - DROP_LEFT, + DROP_FORBIDDEN, } from '../../util/getDropPosition'; export default function handleDrop(props, monitor, Component) { @@ -31,7 +28,7 @@ export default function handleDrop(props, monitor, Component) { Component.setState(() => ({ dropIndicator: null })); const dropPosition = getDropPosition(monitor, Component); - if (!dropPosition) { + if (!dropPosition || dropPosition === DROP_FORBIDDEN) { return undefined; } @@ -40,19 +37,11 @@ export default function handleDrop(props, monitor, Component) { component, index: componentIndex, onDrop, - orientation, + dropToChild, } = Component.props; const draggingItem = monitor.getItem(); - const dropAsChildOrSibling = - (orientation === 'row' && - (dropPosition === DROP_TOP || dropPosition === DROP_BOTTOM)) || - (orientation === 'column' && - (dropPosition === DROP_LEFT || dropPosition === DROP_RIGHT)) - ? 'sibling' - : 'child'; - const dropResult = { source: { id: draggingItem.parentId, @@ -66,25 +55,35 @@ export default function handleDrop(props, monitor, Component) { }, }; + const shouldAppendToChildren = + typeof dropToChild === 'function' ? dropToChild(draggingItem) : dropToChild; + // simplest case, append as child - if (dropAsChildOrSibling === 'child') { + if (shouldAppendToChildren) { dropResult.destination = { id: component.id, type: component.type, index: component.children.length, }; + } else if (!parentComponent) { + dropResult.destination = { + id: component.id, + type: component.type, + index: componentIndex, + }; } else { // if the item is in the same list with a smaller index, you must account for the // "missing" index upon movement within the list const sameParent = parentComponent && draggingItem.parentId === parentComponent.id; const sameParentLowerIndex = - sameParent && draggingItem.index < componentIndex; + sameParent && + draggingItem.index < componentIndex && + draggingItem.type !== component.type; - let nextIndex = sameParentLowerIndex ? componentIndex - 1 : componentIndex; - if (dropPosition === DROP_BOTTOM || dropPosition === DROP_RIGHT) { - nextIndex += 1; - } + const nextIndex = sameParentLowerIndex + ? componentIndex - 1 + : componentIndex; dropResult.destination = { id: parentComponent.id, diff --git a/superset-frontend/src/dashboard/components/gridComponents/ChartHolder.tsx b/superset-frontend/src/dashboard/components/gridComponents/ChartHolder.tsx index a93f3b0b8db6..bcc58d0691da 100644 --- a/superset-frontend/src/dashboard/components/gridComponents/ChartHolder.tsx +++ b/superset-frontend/src/dashboard/components/gridComponents/ChartHolder.tsx @@ -25,7 +25,7 @@ import { LayoutItem, RootState } from 'src/dashboard/types'; import AnchorLink from 'src/dashboard/components/AnchorLink'; import Chart from 'src/dashboard/containers/Chart'; import DeleteComponentButton from 'src/dashboard/components/DeleteComponentButton'; -import DragDroppable from 'src/dashboard/components/dnd/DragDroppable'; +import { Draggable } from 'src/dashboard/components/dnd/DragDroppable'; import HoverMenu from 'src/dashboard/components/menu/HoverMenu'; import ResizableContainer from 'src/dashboard/components/resizable/ResizableContainer'; import getChartAndLabelComponentIdFromPath from 'src/dashboard/util/getChartAndLabelComponentIdFromPath'; @@ -243,7 +243,7 @@ const ChartHolder: React.FC = ({ }, []); return ( - = ({ disableDragDrop={false} editMode={editMode} > - {({ dropIndicatorProps, dragSourceRef }) => ( + {({ dragSourceRef }) => ( = ({ )}
- {dropIndicatorProps &&
} )} - + ); }; diff --git a/superset-frontend/src/dashboard/components/gridComponents/Column.jsx b/superset-frontend/src/dashboard/components/gridComponents/Column.jsx index 1883531404f7..98f2b84e8d92 100644 --- a/superset-frontend/src/dashboard/components/gridComponents/Column.jsx +++ b/superset-frontend/src/dashboard/components/gridComponents/Column.jsx @@ -23,7 +23,10 @@ import { css, styled, t } from '@superset-ui/core'; import Icons from 'src/components/Icons'; import DashboardComponent from 'src/dashboard/containers/DashboardComponent'; import DeleteComponentButton from 'src/dashboard/components/DeleteComponentButton'; -import DragDroppable from 'src/dashboard/components/dnd/DragDroppable'; +import { + Draggable, + Droppable, +} from 'src/dashboard/components/dnd/DragDroppable'; import DragHandle from 'src/dashboard/components/dnd/DragHandle'; import HoverMenu from 'src/dashboard/components/menu/HoverMenu'; import IconButton from 'src/dashboard/components/IconButton'; @@ -33,6 +36,7 @@ import WithPopoverMenu from 'src/dashboard/components/menu/WithPopoverMenu'; import backgroundStyleOptions from 'src/dashboard/util/backgroundStyleOptions'; import { componentShape } from 'src/dashboard/util/propShapes'; import { BACKGROUND_TRANSPARENT } from 'src/dashboard/util/constants'; +import { EMPTY_CONTAINER_Z_INDEX } from 'src/dashboard/constants'; const propTypes = { id: PropTypes.string.isRequired, @@ -60,13 +64,13 @@ const propTypes = { const defaultProps = {}; const ColumnStyles = styled.div` - ${({ theme }) => css` + ${({ theme, editMode }) => css` &.grid-column { width: 100%; position: relative; & > :not(.hover-menu):not(:last-child) { - margin-bottom: ${theme.gridUnit * 4}px; + ${!editMode && `margin-bottom: ${theme.gridUnit * 4}px;`} } } @@ -86,6 +90,22 @@ const ColumnStyles = styled.div` border: 1px dashed ${theme.colors.primary.base}; z-index: 2; } + + & .empty-droptarget { + &.droptarget-edge { + position: absolute; + z-index: ${EMPTY_CONTAINER_Z_INDEX}; + &:first-child { + inset-block-start: 0; + } + } + &:first-child:not(.droptarget-edge) { + position: absolute; + z-index: ${EMPTY_CONTAINER_Z_INDEX}; + width: 100%; + height: 100%; + } + } `} `; @@ -163,7 +183,7 @@ class Column extends React.PureComponent { ); return ( - - {({ dropIndicatorProps, dragSourceRef }) => ( + {({ dragSourceRef }) => ( + {editMode && ( + 0 && 'droptarget-edge', + )} + editMode + > + {({ dropIndicatorProps }) => + dropIndicatorProps &&
+ } + + )} {columnItems.length === 0 ? (
{t('Empty column')}
) : ( columnItems.map((componentId, itemIndex) => ( - + + + {editMode && ( + + {({ dropIndicatorProps }) => + dropIndicatorProps && ( +
+ ) + } + + )} + )) )} - - {dropIndicatorProps &&
} )} - + ); } } diff --git a/superset-frontend/src/dashboard/components/gridComponents/Column.test.jsx b/superset-frontend/src/dashboard/components/gridComponents/Column.test.jsx index 2d759b812a43..294b1f1dea14 100644 --- a/superset-frontend/src/dashboard/components/gridComponents/Column.test.jsx +++ b/superset-frontend/src/dashboard/components/gridComponents/Column.test.jsx @@ -27,6 +27,16 @@ import { getMockStore } from 'spec/fixtures/mockStore'; import { dashboardLayout as mockLayout } from 'spec/fixtures/mockDashboardLayout'; import { initialState } from 'src/SqlLab/fixtures'; +jest.mock('src/dashboard/components/dnd/DragDroppable', () => ({ + Draggable: ({ children }) => ( +
{children({})}
+ ), + Droppable: ({ children, depth }) => ( +
+ {children({})} +
+ ), +})); jest.mock( 'src/dashboard/containers/DashboardComponent', () => @@ -92,10 +102,12 @@ function setup(overrideProps) { }); } -test('should render a DragDroppable', () => { - // don't count child DragDroppables - const { getByTestId } = setup({ component: columnWithoutChildren }); - expect(getByTestId('dragdroppable-object')).toBeInTheDocument(); +test('should render a Draggable', () => { + const { getByTestId, queryByTestId } = setup({ + component: columnWithoutChildren, + }); + expect(getByTestId('mock-draggable')).toBeInTheDocument(); + expect(queryByTestId('mock-droppable')).not.toBeInTheDocument(); }); test('should skip rendering HoverMenu and DeleteComponentButton when not in editMode', () => { @@ -120,11 +132,20 @@ test('should render a ResizableContainer', () => { test('should render a HoverMenu in editMode', () => { // we cannot set props on the Row because of the WithDragDropContext wrapper - const { container } = setup({ + const { container, getAllByTestId, getByTestId } = setup({ component: columnWithoutChildren, editMode: true, }); expect(container.querySelector('.hover-menu')).toBeInTheDocument(); + + // Droppable area enabled in editMode + expect(getAllByTestId('mock-droppable').length).toBe(1); + + // pass the same depth of its droppable area + expect(getByTestId('mock-droppable')).toHaveAttribute( + 'depth', + `${props.depth}`, + ); }); test('should render a DeleteComponentButton in editMode', () => { diff --git a/superset-frontend/src/dashboard/components/gridComponents/Divider.jsx b/superset-frontend/src/dashboard/components/gridComponents/Divider.jsx index 078405be3e4a..638527aa3b85 100644 --- a/superset-frontend/src/dashboard/components/gridComponents/Divider.jsx +++ b/superset-frontend/src/dashboard/components/gridComponents/Divider.jsx @@ -20,7 +20,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { css, styled } from '@superset-ui/core'; -import DragDroppable from '../dnd/DragDroppable'; +import { Draggable } from '../dnd/DragDroppable'; import HoverMenu from '../menu/HoverMenu'; import DeleteComponentButton from '../DeleteComponentButton'; import { componentShape } from '../../util/propShapes'; @@ -84,7 +84,7 @@ class Divider extends React.PureComponent { } = this.props; return ( - - {({ dropIndicatorProps, dragSourceRef }) => ( + {({ dragSourceRef }) => (
{editMode && ( )} - - - {dropIndicatorProps &&
}
)} - + ); } } diff --git a/superset-frontend/src/dashboard/components/gridComponents/Divider.test.jsx b/superset-frontend/src/dashboard/components/gridComponents/Divider.test.jsx index 6331f6883295..f98378ab7295 100644 --- a/superset-frontend/src/dashboard/components/gridComponents/Divider.test.jsx +++ b/superset-frontend/src/dashboard/components/gridComponents/Divider.test.jsx @@ -24,7 +24,7 @@ import { HTML5Backend } from 'react-dnd-html5-backend'; import DeleteComponentButton from 'src/dashboard/components/DeleteComponentButton'; import HoverMenu from 'src/dashboard/components/menu/HoverMenu'; -import DragDroppable from 'src/dashboard/components/dnd/DragDroppable'; +import { Draggable } from 'src/dashboard/components/dnd/DragDroppable'; import Divider from 'src/dashboard/components/gridComponents/Divider'; import newComponentFactory from 'src/dashboard/util/newComponentFactory'; import { @@ -56,9 +56,9 @@ describe('Divider', () => { return wrapper; } - it('should render a DragDroppable', () => { + it('should render a Draggable', () => { const wrapper = setup(); - expect(wrapper.find(DragDroppable)).toExist(); + expect(wrapper.find(Draggable)).toExist(); }); it('should render a div with class "dashboard-component-divider"', () => { diff --git a/superset-frontend/src/dashboard/components/gridComponents/DynamicComponent.tsx b/superset-frontend/src/dashboard/components/gridComponents/DynamicComponent.tsx index 707597d8c0ca..f1752588d2da 100644 --- a/superset-frontend/src/dashboard/components/gridComponents/DynamicComponent.tsx +++ b/superset-frontend/src/dashboard/components/gridComponents/DynamicComponent.tsx @@ -21,7 +21,7 @@ import { DashboardComponentMetadata, JsonObject, t } from '@superset-ui/core'; import backgroundStyleOptions from 'src/dashboard/util/backgroundStyleOptions'; import cx from 'classnames'; import { useSelector } from 'react-redux'; -import DragDroppable from '../dnd/DragDroppable'; +import { Draggable } from '../dnd/DragDroppable'; import { COLUMN_TYPE, ROW_TYPE } from '../../util/componentTypes'; import WithPopoverMenu from '../menu/WithPopoverMenu'; import ResizableContainer from '../resizable/ResizableContainer'; @@ -106,7 +106,7 @@ const DynamicComponent: FC = ({ ); return ( - = ({ onDrop={handleComponentDrop} editMode={editMode} > - {({ dropIndicatorProps, dragSourceRef }) => ( + {({ dragSourceRef }) => ( = ({
- {dropIndicatorProps &&
} )} - + ); }; export default DynamicComponent; diff --git a/superset-frontend/src/dashboard/components/gridComponents/Header.jsx b/superset-frontend/src/dashboard/components/gridComponents/Header.jsx index 253f377fc2f7..85f4cd7cc730 100644 --- a/superset-frontend/src/dashboard/components/gridComponents/Header.jsx +++ b/superset-frontend/src/dashboard/components/gridComponents/Header.jsx @@ -23,7 +23,7 @@ import { css, styled } from '@superset-ui/core'; import PopoverDropdown from 'src/components/PopoverDropdown'; import EditableTitle from 'src/components/EditableTitle'; -import DragDroppable from 'src/dashboard/components/dnd/DragDroppable'; +import { Draggable } from 'src/dashboard/components/dnd/DragDroppable'; import DragHandle from 'src/dashboard/components/dnd/DragHandle'; import AnchorLink from 'src/dashboard/components/AnchorLink'; import HoverMenu from 'src/dashboard/components/menu/HoverMenu'; @@ -178,7 +178,7 @@ class Header extends React.PureComponent { ); return ( - - {({ dropIndicatorProps, dragSourceRef }) => ( + {({ dragSourceRef }) => (
{editMode && depth <= 2 && ( // drag handle looks bad when nested @@ -239,11 +239,9 @@ class Header extends React.PureComponent { )} - - {dropIndicatorProps &&
}
)} - + ); } } diff --git a/superset-frontend/src/dashboard/components/gridComponents/Header.test.jsx b/superset-frontend/src/dashboard/components/gridComponents/Header.test.jsx index 6f903a05c1c2..64ff712129ca 100644 --- a/superset-frontend/src/dashboard/components/gridComponents/Header.test.jsx +++ b/superset-frontend/src/dashboard/components/gridComponents/Header.test.jsx @@ -27,7 +27,7 @@ import DeleteComponentButton from 'src/dashboard/components/DeleteComponentButto import EditableTitle from 'src/components/EditableTitle'; import HoverMenu from 'src/dashboard/components/menu/HoverMenu'; import WithPopoverMenu from 'src/dashboard/components/menu/WithPopoverMenu'; -import DragDroppable from 'src/dashboard/components/dnd/DragDroppable'; +import { Draggable } from 'src/dashboard/components/dnd/DragDroppable'; import Header from 'src/dashboard/components/gridComponents/Header'; import newComponentFactory from 'src/dashboard/util/newComponentFactory'; import { @@ -65,9 +65,9 @@ describe('Header', () => { return wrapper; } - it('should render a DragDroppable', () => { + it('should render a Draggable', () => { const wrapper = setup(); - expect(wrapper.find(DragDroppable)).toExist(); + expect(wrapper.find(Draggable)).toExist(); }); it('should render a WithPopoverMenu', () => { diff --git a/superset-frontend/src/dashboard/components/gridComponents/Markdown.jsx b/superset-frontend/src/dashboard/components/gridComponents/Markdown.jsx index 9febfacf909d..23a06a0f7d1a 100644 --- a/superset-frontend/src/dashboard/components/gridComponents/Markdown.jsx +++ b/superset-frontend/src/dashboard/components/gridComponents/Markdown.jsx @@ -26,7 +26,7 @@ import { Logger, LOG_ACTIONS_RENDER_CHART } from 'src/logger/LogUtils'; import { MarkdownEditor } from 'src/components/AsyncAceEditor'; import DeleteComponentButton from 'src/dashboard/components/DeleteComponentButton'; -import DragDroppable from 'src/dashboard/components/dnd/DragDroppable'; +import { Draggable } from 'src/dashboard/components/dnd/DragDroppable'; import HoverMenu from 'src/dashboard/components/menu/HoverMenu'; import ResizableContainer from 'src/dashboard/components/resizable/ResizableContainer'; import MarkdownModeDropdown from 'src/dashboard/components/menu/MarkdownModeDropdown'; @@ -332,7 +332,7 @@ class Markdown extends React.PureComponent { const isEditing = editorMode === 'edit'; return ( - - {({ dropIndicatorProps, dragSourceRef }) => ( + {({ dragSourceRef }) => ( - {dropIndicatorProps &&
} )} - + ); } } diff --git a/superset-frontend/src/dashboard/components/gridComponents/Markdown.test.jsx b/superset-frontend/src/dashboard/components/gridComponents/Markdown.test.jsx index ac97c38a4afe..b15c4c504f69 100644 --- a/superset-frontend/src/dashboard/components/gridComponents/Markdown.test.jsx +++ b/superset-frontend/src/dashboard/components/gridComponents/Markdown.test.jsx @@ -30,7 +30,7 @@ import MarkdownConnected from 'src/dashboard/components/gridComponents/Markdown' import MarkdownModeDropdown from 'src/dashboard/components/menu/MarkdownModeDropdown'; import DeleteComponentButton from 'src/dashboard/components/DeleteComponentButton'; import waitForComponentToPaint from 'spec/helpers/waitForComponentToPaint'; -import DragDroppable from 'src/dashboard/components/dnd/DragDroppable'; +import { Draggable } from 'src/dashboard/components/dnd/DragDroppable'; import WithPopoverMenu from 'src/dashboard/components/menu/WithPopoverMenu'; import ResizableContainer from 'src/dashboard/components/resizable/ResizableContainer'; @@ -62,7 +62,7 @@ describe('Markdown', () => { function setup(overrideProps) { // We have to wrap provide DragDropContext for the underlying DragDroppable - // otherwise we cannot assert on DragDroppable children + // otherwise we cannot assert on Droppable children const wrapper = mount( @@ -73,9 +73,9 @@ describe('Markdown', () => { return wrapper; } - it('should render a DragDroppable', () => { + it('should render a Draggable', () => { const wrapper = setup(); - expect(wrapper.find(DragDroppable)).toExist(); + expect(wrapper.find(Draggable)).toExist(); }); it('should render a WithPopoverMenu', () => { diff --git a/superset-frontend/src/dashboard/components/gridComponents/Row.jsx b/superset-frontend/src/dashboard/components/gridComponents/Row.jsx index d645a1a2cfc6..bad32d4cdce5 100644 --- a/superset-frontend/src/dashboard/components/gridComponents/Row.jsx +++ b/superset-frontend/src/dashboard/components/gridComponents/Row.jsx @@ -19,15 +19,20 @@ import React from 'react'; import PropTypes from 'prop-types'; import cx from 'classnames'; +import { debounce } from 'lodash'; import { css, + FAST_DEBOUNCE, FeatureFlag, isFeatureEnabled, styled, t, } from '@superset-ui/core'; -import DragDroppable from 'src/dashboard/components/dnd/DragDroppable'; +import { + Draggable, + Droppable, +} from 'src/dashboard/components/dnd/DragDroppable'; import DragHandle from 'src/dashboard/components/dnd/DragHandle'; import DashboardComponent from 'src/dashboard/containers/DashboardComponent'; import DeleteComponentButton from 'src/dashboard/components/DeleteComponentButton'; @@ -39,6 +44,7 @@ import WithPopoverMenu from 'src/dashboard/components/menu/WithPopoverMenu'; import { componentShape } from 'src/dashboard/util/propShapes'; import backgroundStyleOptions from 'src/dashboard/util/backgroundStyleOptions'; import { BACKGROUND_TRANSPARENT } from 'src/dashboard/util/constants'; +import { EMPTY_CONTAINER_Z_INDEX } from 'src/dashboard/constants'; import { isCurrentUserBot } from 'src/utils/isBot'; const propTypes = { @@ -57,6 +63,7 @@ const propTypes = { onResizeStart: PropTypes.func.isRequired, onResize: PropTypes.func.isRequired, onResizeStop: PropTypes.func.isRequired, + maxChildrenHeight: PropTypes.number.isRequired, // dnd handleComponentDrop: PropTypes.func.isRequired, @@ -65,7 +72,7 @@ const propTypes = { }; const GridRow = styled.div` - ${({ theme }) => css` + ${({ theme, editMode }) => css` position: relative; display: flex; flex-direction: row; @@ -75,7 +82,32 @@ const GridRow = styled.div` height: fit-content; & > :not(:last-child):not(.hover-menu) { - margin-right: ${theme.gridUnit * 4}px; + ${!editMode && `margin-right: ${theme.gridUnit * 4}px;`} + } + + & .empty-droptarget { + position: relative; + align-self: center; + &.empty-droptarget--vertical { + min-width: ${theme.gridUnit * 4}px; + &:not(:last-child) { + width: ${theme.gridUnit * 4}px; + } + &:first-child:not(.droptarget-side) { + z-index: ${EMPTY_CONTAINER_Z_INDEX}; + position: absolute; + width: 100%; + height: 100%; + } + } + &.droptarget-side { + z-index: ${EMPTY_CONTAINER_Z_INDEX}; + position: absolute; + width: ${theme.gridUnit * 4}px; + &:first-child { + inset-inline-start: 0; + } + } } &.grid-row--empty { @@ -100,6 +132,7 @@ class Row extends React.PureComponent { this.state = { isFocused: false, isInView: false, + hoverMenuHovered: false, }; this.handleDeleteComponent = this.handleDeleteComponent.bind(this); this.handleUpdateMeta = this.handleUpdateMeta.bind(this); @@ -108,6 +141,11 @@ class Row extends React.PureComponent { 'background', ); this.handleChangeFocus = this.handleChangeFocus.bind(this); + this.handleMenuHover = this.handleMenuHover.bind(this); + this.setVerticalEmptyContainerHeight = debounce( + this.setVerticalEmptyContainerHeight.bind(this), + FAST_DEBOUNCE, + ); this.containerRef = React.createRef(); this.observerEnabler = null; @@ -145,10 +183,28 @@ class Row extends React.PureComponent { if (element) { this.observerEnabler.observe(element); this.observerDisabler.observe(element); + this.setVerticalEmptyContainerHeight(); } } } + componentDidUpdate() { + this.setVerticalEmptyContainerHeight(); + } + + setVerticalEmptyContainerHeight() { + const { containerHeight } = this.state; + const { editMode } = this.props; + const updatedHeight = this.containerRef.current?.clientHeight; + if ( + editMode && + this.containerRef.current && + updatedHeight !== containerHeight + ) { + this.setState({ containerHeight: updatedHeight }); + } + } + componentWillUnmount() { this.observerEnabler?.disconnect(); this.observerDisabler?.disconnect(); @@ -178,6 +234,11 @@ class Row extends React.PureComponent { deleteComponent(component.id, parentId); } + handleMenuHover = hovered => { + const { isHovered } = hovered; + this.setState(() => ({ hoverMenuHovered: isHovered })); + }; + render() { const { component: rowComponent, @@ -195,6 +256,7 @@ class Row extends React.PureComponent { onChangeTab, isComponentVisible, } = this.props; + const { containerHeight, hoverMenuHovered } = this.state; const rowItems = rowComponent.children || []; @@ -202,9 +264,10 @@ class Row extends React.PureComponent { opt => opt.value === (rowComponent.meta.background || BACKGROUND_TRANSPARENT), ); + const remainColumnCount = availableColumnCount - occupiedColumnCount; return ( - - {({ dropIndicatorProps, dragSourceRef }) => ( + {({ dragSourceRef }) => ( {editMode && ( - + - {rowItems.length === 0 ? ( + {editMode && ( + 0 && 'droptarget-side', + )} + editMode + style={{ + height: rowItems.length > 0 ? containerHeight : '100%', + ...(rowItems.length > 0 && { width: 16 }), + }} + > + {({ dropIndicatorProps }) => + dropIndicatorProps &&
+ } + + )} + {rowItems.length === 0 && (
{t('Empty row')}
- ) : ( - rowItems.map((componentId, itemIndex) => ( - - )) )} - - {dropIndicatorProps &&
} + {rowItems.length > 0 && + rowItems.map((componentId, itemIndex) => ( + + + {editMode && ( + + {({ dropIndicatorProps }) => + dropIndicatorProps &&
+ } + + )} + + ))} )} - + ); } } diff --git a/superset-frontend/src/dashboard/components/gridComponents/Row.test.jsx b/superset-frontend/src/dashboard/components/gridComponents/Row.test.jsx index b69a8f7e9734..e3cc9fb7351a 100644 --- a/superset-frontend/src/dashboard/components/gridComponents/Row.test.jsx +++ b/superset-frontend/src/dashboard/components/gridComponents/Row.test.jsx @@ -28,6 +28,16 @@ import { getMockStore } from 'spec/fixtures/mockStore'; import { dashboardLayout as mockLayout } from 'spec/fixtures/mockDashboardLayout'; import { initialState } from 'src/SqlLab/fixtures'; +jest.mock('src/dashboard/components/dnd/DragDroppable', () => ({ + Draggable: ({ children }) => ( +
{children({})}
+ ), + Droppable: ({ children, depth }) => ( +
+ {children({})} +
+ ), +})); jest.mock( 'src/dashboard/containers/DashboardComponent', () => @@ -92,10 +102,14 @@ function setup(overrideProps) { }); } -test('should render a DragDroppable', () => { +test('should render a Draggable', () => { // don't count child DragDroppables - const { getByTestId } = setup({ component: rowWithoutChildren }); - expect(getByTestId('dragdroppable-object')).toBeInTheDocument(); + const { getByTestId, queryByTestId } = setup({ + component: rowWithoutChildren, + }); + + expect(getByTestId('mock-draggable')).toBeInTheDocument(); + expect(queryByTestId('mock-droppable')).not.toBeInTheDocument(); }); test('should skip rendering HoverMenu and DeleteComponentButton when not in editMode', () => { @@ -113,11 +127,20 @@ test('should render a WithPopoverMenu', () => { }); test('should render a HoverMenu in editMode', () => { - const { container } = setup({ + const { container, getAllByTestId, getByTestId } = setup({ component: rowWithoutChildren, editMode: true, }); expect(container.querySelector('.hover-menu')).toBeInTheDocument(); + + // Droppable area enabled in editMode + expect(getAllByTestId('mock-droppable').length).toBe(1); + + // pass the same depth of its droppable area + expect(getByTestId('mock-droppable')).toHaveAttribute( + 'depth', + `${props.depth}`, + ); }); test('should render a DeleteComponentButton in editMode', () => { diff --git a/superset-frontend/src/dashboard/components/gridComponents/Tab.jsx b/superset-frontend/src/dashboard/components/gridComponents/Tab.jsx index d1d08176baa9..bd71afa08167 100644 --- a/superset-frontend/src/dashboard/components/gridComponents/Tab.jsx +++ b/superset-frontend/src/dashboard/components/gridComponents/Tab.jsx @@ -28,8 +28,11 @@ import EditableTitle from 'src/components/EditableTitle'; import { setEditMode } from 'src/dashboard/actions/dashboardState'; import DashboardComponent from 'src/dashboard/containers/DashboardComponent'; import AnchorLink from 'src/dashboard/components/AnchorLink'; -import DragDroppable from 'src/dashboard/components/dnd/DragDroppable'; +import DragDroppable, { + Droppable, +} from 'src/dashboard/components/dnd/DragDroppable'; import { componentShape } from 'src/dashboard/util/propShapes'; +import { TAB_TYPE } from 'src/dashboard/util/componentTypes'; export const RENDER_TAB = 'RENDER_TAB'; export const RENDER_TAB_CONTENT = 'RENDER_TAB_CONTENT'; @@ -83,15 +86,8 @@ const TabTitleContainer = styled.div` `} `; -const renderDraggableContentBottom = dropProps => - dropProps.dropIndicatorProps && ( -
- ); - -const renderDraggableContentTop = dropProps => - dropProps.dropIndicatorProps && ( -
- ); +const renderDraggableContent = dropProps => + dropProps.dropIndicatorProps &&
; class Tab extends React.PureComponent { constructor(props) { @@ -144,10 +140,13 @@ class Tab extends React.PureComponent { } } + shouldDropToChild(item) { + return item.type !== TAB_TYPE; + } + renderTabContent() { const { component: tabComponent, - parentComponent: tabParentComponent, depth, availableColumnCount, columnWidth, @@ -166,21 +165,25 @@ class Tab extends React.PureComponent {
{/* Make top of tab droppable */} {editMode && ( - - {renderDraggableContentTop} - + {renderDraggableContent} + )} {shouldDisplayEmptyState && ( )} {tabComponent.children.map((componentId, componentIndex) => ( - + + + {/* Make bottom of tab droppable */} + {editMode && ( + + {renderDraggableContent} + + )} + ))} - {/* Make bottom of tab droppable */} - {editMode && tabComponent.children.length > 0 && ( - - {renderDraggableContentBottom} - - )}
); } @@ -278,6 +280,7 @@ class Tab extends React.PureComponent { onDrop={this.handleDrop} onHover={this.handleOnHover} editMode={editMode} + dropToChild={this.shouldDropToChild} > {({ dropIndicatorProps, dragSourceRef }) => ( jest.fn(() =>
), @@ -37,8 +42,9 @@ jest.mock('src/components/EditableTitle', () => )), ); -jest.mock('src/dashboard/components/dnd/DragDroppable', () => - jest.fn(props => { +jest.mock('src/dashboard/components/dnd/DragDroppable', () => ({ + ...jest.requireActual('src/dashboard/components/dnd/DragDroppable'), + Droppable: jest.fn(props => { const childProps = props.editMode ? { dragSourceRef: props.dragSourceRef, @@ -47,14 +53,14 @@ jest.mock('src/dashboard/components/dnd/DragDroppable', () => : {}; return (
- {props.children(childProps)}
); }), -); +})); jest.mock('src/dashboard/actions/dashboardState', () => ({ setEditMode: jest.fn(() => ({ type: 'SET_EDIT_MODE', @@ -106,30 +112,115 @@ beforeEach(() => { test('Render tab (no content)', () => { const props = createProps(); props.renderType = 'RENDER_TAB'; - render(, { useRedux: true, useDnd: true }); + const { getByTestId } = render(, { + useRedux: true, + useDnd: true, + }); expect(screen.getByText('🚀 Aspiring Developers')).toBeInTheDocument(); expect(EditableTitle).toBeCalledTimes(1); - expect(DragDroppable).toBeCalledTimes(1); + expect(getByTestId('dragdroppable-object')).toBeInTheDocument(); }); test('Render tab (no content) editMode:true', () => { const props = createProps(); props.editMode = true; props.renderType = 'RENDER_TAB'; - render(, { useRedux: true, useDnd: true }); + const { getByTestId } = render(, { + useRedux: true, + useDnd: true, + }); expect(screen.getByText('🚀 Aspiring Developers')).toBeInTheDocument(); expect(EditableTitle).toBeCalledTimes(1); - expect(DragDroppable).toBeCalledTimes(1); + expect(getByTestId('dragdroppable-object')).toBeInTheDocument(); +}); + +test('Drop on a tab', async () => { + const props = createProps(); + const mockOnDropOnTab = jest.fn(); + render( + <> + + + + , + { + useRedux: true, + useDnd: true, + }, + ); + + fireEvent.dragStart(screen.getByText('🚀 Aspiring Developers')); + fireEvent.drop(screen.getByText('Next Tab')); + await waitFor(() => expect(mockOnDropOnTab).toHaveBeenCalled()); + expect(mockOnDropOnTab).toHaveBeenCalledWith( + expect.objectContaining({ + destination: { id: props.parentComponent.id, index: 2, type: 'TABS' }, + }), + ); + + fireEvent.dragStart(screen.getByText('Dashboard Component')); + fireEvent.drop(screen.getByText('Next Tab')); + await waitFor(() => expect(mockOnDropOnTab).toHaveBeenCalledTimes(2)); + expect(mockOnDropOnTab).toHaveBeenLastCalledWith( + expect.objectContaining({ + destination: { + id: 'TAB-Next-', + index: props.component.children.length, + type: 'TAB', + }, + }), + ); }); test('Edit table title', () => { const props = createProps(); props.editMode = true; props.renderType = 'RENDER_TAB'; - render(, { useRedux: true, useDnd: true }); + const { getByTestId } = render(, { + useRedux: true, + useDnd: true, + }); expect(EditableTitle).toBeCalledTimes(1); - expect(DragDroppable).toBeCalledTimes(1); + expect(getByTestId('dragdroppable-object')).toBeInTheDocument(); expect(props.updateComponents).not.toBeCalled(); userEvent.click(screen.getByText('🚀 Aspiring Developers')); @@ -139,7 +230,10 @@ test('Edit table title', () => { test('Render tab (with content)', () => { const props = createProps(); props.isFocused = true; - render(, { useRedux: true, useDnd: true }); + const { queryByTestId } = render(, { + useRedux: true, + useDnd: true, + }); expect(DashboardComponent).toBeCalledTimes(2); expect(DashboardComponent).toHaveBeenNthCalledWith( 1, @@ -177,7 +271,7 @@ test('Render tab (with content)', () => { }), {}, ); - expect(DragDroppable).toBeCalledTimes(0); + expect(queryByTestId('dragdroppable-object')).not.toBeInTheDocument(); }); test('Render tab content with no children', () => { @@ -215,7 +309,10 @@ test('Render tab (with content) editMode:true', () => { const props = createProps(); props.isFocused = true; props.editMode = true; - render(, { useRedux: true, useDnd: true }); + const { getAllByTestId } = render(, { + useRedux: true, + useDnd: true, + }); expect(DashboardComponent).toBeCalledTimes(2); expect(DashboardComponent).toHaveBeenNthCalledWith( 1, @@ -253,20 +350,28 @@ test('Render tab (with content) editMode:true', () => { }), {}, ); - expect(DragDroppable).toBeCalledTimes(2); + // 3 droppable area exists for two child components + expect(getAllByTestId('MockDroppable')).toHaveLength(3); }); test('Should call "handleDrop" and "handleTopDropTargetDrop"', () => { const props = createProps(); props.isFocused = true; props.editMode = true; - render(, { useRedux: true, useDnd: true }); + const { getAllByTestId, rerender } = render( + , + { + useRedux: true, + useDnd: true, + }, + ); expect(props.handleComponentDrop).not.toBeCalled(); - userEvent.click(screen.getAllByRole('button')[0]); + userEvent.click(getAllByTestId('MockDroppable')[0]); expect(props.handleComponentDrop).toBeCalledTimes(1); expect(props.onDropOnTab).not.toBeCalled(); - userEvent.click(screen.getAllByRole('button')[1]); + rerender(); + userEvent.click(getAllByTestId('MockDroppable')[1]); expect(props.onDropOnTab).toBeCalledTimes(1); expect(props.handleComponentDrop).toBeCalledTimes(2); }); diff --git a/superset-frontend/src/dashboard/components/gridComponents/Tabs.jsx b/superset-frontend/src/dashboard/components/gridComponents/Tabs.jsx index 67f4b3c598bd..c9f35d3ba56f 100644 --- a/superset-frontend/src/dashboard/components/gridComponents/Tabs.jsx +++ b/superset-frontend/src/dashboard/components/gridComponents/Tabs.jsx @@ -23,7 +23,7 @@ import { connect } from 'react-redux'; import { LineEditableTabs } from 'src/components/Tabs'; import { LOG_ACTIONS_SELECT_DASHBOARD_TAB } from 'src/logger/LogUtils'; import { AntdModal } from 'src/components'; -import DragDroppable from '../dnd/DragDroppable'; +import { Draggable } from '../dnd/DragDroppable'; import DragHandle from '../dnd/DragHandle'; import DashboardComponent from '../../containers/DashboardComponent'; import DeleteComponentButton from '../DeleteComponentButton'; @@ -32,7 +32,7 @@ import findTabIndexByComponentId from '../../util/findTabIndexByComponentId'; import getDirectPathToTabIndex from '../../util/getDirectPathToTabIndex'; import getLeafComponentIdFromPath from '../../util/getLeafComponentIdFromPath'; import { componentShape } from '../../util/propShapes'; -import { NEW_TAB_ID, DASHBOARD_ROOT_ID } from '../../util/constants'; +import { NEW_TAB_ID } from '../../util/constants'; import { RENDER_TAB, RENDER_TAB_CONTENT } from './Tab'; import { TABS_TYPE, TAB_TYPE } from '../../util/componentTypes'; @@ -339,7 +339,7 @@ export class Tabs extends React.PureComponent { tabsToHighlight = nativeFilters.filters[highlightedFilterId]?.tabsInScope; } return ( - - {({ - dropIndicatorProps: tabsDropIndicatorProps, - dragSourceRef: tabsDragSourceRef, - }) => ( + {({ dragSourceRef: tabsDragSourceRef }) => ( ))} - - {/* don't indicate that a drop on root is allowed when tabs already exist */} - {tabsDropIndicatorProps && - parentComponent.id !== DASHBOARD_ROOT_ID && ( -
- )} )} - + ); } } diff --git a/superset-frontend/src/dashboard/components/gridComponents/Tabs.test.jsx b/superset-frontend/src/dashboard/components/gridComponents/Tabs.test.jsx index 127b4d42db64..7f39bdcc6807 100644 --- a/superset-frontend/src/dashboard/components/gridComponents/Tabs.test.jsx +++ b/superset-frontend/src/dashboard/components/gridComponents/Tabs.test.jsx @@ -29,6 +29,14 @@ import { getMockStore } from 'spec/fixtures/mockStore'; import { nativeFilters } from 'spec/fixtures/mockNativeFilters'; import { initialState } from 'src/SqlLab/fixtures'; +jest.mock('src/dashboard/components/dnd/DragDroppable', () => ({ + Draggable: ({ children }) => ( +
{children({})}
+ ), + Droppable: ({ children }) => ( +
{children({})}
+ ), +})); jest.mock('src/dashboard/containers/DashboardComponent', () => ({ id }) => (
{id}
)); @@ -88,12 +96,12 @@ function setup(overrideProps) { }); } -test('should render a DragDroppable', () => { - // test just Tabs with no children DragDroppables +test('should render a Draggable', () => { + // test just Tabs with no children Draggable const { getByTestId } = setup({ component: { ...props.component, children: [] }, }); - expect(getByTestId('dragdroppable-object')).toBeInTheDocument(); + expect(getByTestId('mock-draggable')).toBeInTheDocument(); }); test('should render non-editable tabs', () => { diff --git a/superset-frontend/src/dashboard/components/gridComponents/Tabs.test.tsx b/superset-frontend/src/dashboard/components/gridComponents/Tabs.test.tsx index 9ee9fc6866db..6aef193b4496 100644 --- a/superset-frontend/src/dashboard/components/gridComponents/Tabs.test.tsx +++ b/superset-frontend/src/dashboard/components/gridComponents/Tabs.test.tsx @@ -22,7 +22,7 @@ import React from 'react'; import { render, screen, waitFor } from 'spec/helpers/testing-library'; import { nativeFiltersInfo } from 'src/dashboard/fixtures/mockNativeFilters'; import DashboardComponent from 'src/dashboard/containers/DashboardComponent'; -import DragDroppable from 'src/dashboard/components/dnd/DragDroppable'; +import { Draggable } from 'src/dashboard/components/dnd/DragDroppable'; import DeleteComponentButton from 'src/dashboard/components/DeleteComponentButton'; import getLeafComponentIdFromPath from 'src/dashboard/util/getLeafComponentIdFromPath'; import emptyDashboardLayout from 'src/dashboard/fixtures/emptyDashboardLayout'; @@ -55,8 +55,8 @@ jest.mock('src/dashboard/components/DeleteComponentButton', () => ); jest.mock('src/dashboard/util/getLeafComponentIdFromPath', () => jest.fn()); -jest.mock('src/dashboard/components/dnd/DragDroppable', () => - jest.fn(props => { +jest.mock('src/dashboard/components/dnd/DragDroppable', () => ({ + Draggable: jest.fn(props => { const childProps = props.editMode ? { dragSourceRef: props.dragSourceRef, @@ -72,7 +72,7 @@ jest.mock('src/dashboard/components/dnd/DragDroppable', () =>
); }), -); +})); const createProps = () => ({ id: 'TABS-L-d9eyOE-b', @@ -123,7 +123,7 @@ test('Should render editMode:true', () => { const props = createProps(); render(, { useRedux: true, useDnd: true }); expect(screen.getAllByRole('tab')).toHaveLength(3); - expect(DragDroppable).toBeCalledTimes(1); + expect(Draggable).toBeCalledTimes(1); expect(DashboardComponent).toBeCalledTimes(4); expect(DeleteComponentButton).toBeCalledTimes(1); expect(screen.getAllByRole('button', { name: 'remove' })).toHaveLength(3); @@ -135,7 +135,7 @@ test('Should render editMode:false', () => { props.editMode = false; render(, { useRedux: true, useDnd: true }); expect(screen.getAllByRole('tab')).toHaveLength(3); - expect(DragDroppable).toBeCalledTimes(1); + expect(Draggable).toBeCalledTimes(1); expect(DashboardComponent).toBeCalledTimes(4); expect(DeleteComponentButton).not.toBeCalled(); expect( diff --git a/superset-frontend/src/dashboard/components/menu/HoverMenu.test.tsx b/superset-frontend/src/dashboard/components/menu/HoverMenu.test.tsx index adf34938f55c..5b7a64e64f8d 100644 --- a/superset-frontend/src/dashboard/components/menu/HoverMenu.test.tsx +++ b/superset-frontend/src/dashboard/components/menu/HoverMenu.test.tsx @@ -17,7 +17,8 @@ * under the License. */ import React from 'react'; -import { render } from 'spec/helpers/testing-library'; +import { render, screen } from 'spec/helpers/testing-library'; +import userEvent from '@testing-library/user-event'; import HoverMenu from 'src/dashboard/components/menu/HoverMenu'; @@ -25,3 +26,16 @@ test('should render a div.hover-menu', () => { const { container } = render(); expect(container.querySelector('.hover-menu')).toBeInTheDocument(); }); + +test('should call onHover when mouse enters and leaves', () => { + const onHover = jest.fn(); + render(); + + const hoverMenu = screen.getByTestId('hover-menu'); + + userEvent.hover(hoverMenu); + expect(onHover).toBeCalledWith({ isHovered: true }); + + userEvent.unhover(hoverMenu); + expect(onHover).toBeCalledWith({ isHovered: false }); +}); diff --git a/superset-frontend/src/dashboard/components/menu/HoverMenu.tsx b/superset-frontend/src/dashboard/components/menu/HoverMenu.tsx index 5fbc37cbbfd5..23e71ba9a6ad 100644 --- a/superset-frontend/src/dashboard/components/menu/HoverMenu.tsx +++ b/superset-frontend/src/dashboard/components/menu/HoverMenu.tsx @@ -1,3 +1,4 @@ +/* eslint-disable react/no-unused-state */ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file @@ -24,13 +25,14 @@ interface HoverMenuProps { position: 'left' | 'top'; innerRef: RefObject; children: React.ReactNode; + onHover?: (data: { isHovered: boolean }) => void; } const HoverStyleOverrides = styled.div` .hover-menu { opacity: 0; position: absolute; - z-index: 10; + z-index: 11; // one more than DragDroppable font-size: ${({ theme }) => theme.typography.sizes.m}; } @@ -70,6 +72,20 @@ export default class HoverMenu extends React.PureComponent { children: null, }; + handleMouseEnter = () => { + const { onHover } = this.props; + if (onHover) { + onHover({ isHovered: true }); + } + }; + + handleMouseLeave = () => { + const { onHover } = this.props; + if (onHover) { + onHover({ isHovered: false }); + } + }; + render() { const { innerRef, position, children } = this.props; return ( @@ -81,6 +97,9 @@ export default class HoverMenu extends React.PureComponent { position === 'left' && 'hover-menu--left', position === 'top' && 'hover-menu--top', )} + onMouseEnter={this.handleMouseEnter} + onMouseLeave={this.handleMouseLeave} + data-test="hover-menu" > {children}
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CrossFilters/ScopingModal/ScopingModal.test.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CrossFilters/ScopingModal/ScopingModal.test.tsx index eef5d1fa07ff..3cc45f81683a 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CrossFilters/ScopingModal/ScopingModal.test.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CrossFilters/ScopingModal/ScopingModal.test.tsx @@ -19,7 +19,13 @@ import React from 'react'; import fetchMock from 'fetch-mock'; import userEvent from '@testing-library/user-event'; -import { render, screen, waitFor, within } from 'spec/helpers/testing-library'; +import { + render, + screen, + selectOption, + waitFor, + within, +} from 'spec/helpers/testing-library'; import { CHART_TYPE, DASHBOARD_ROOT_TYPE, @@ -200,21 +206,7 @@ it('add new custom scoping', async () => { expect(screen.getByText('[new custom scoping]')).toBeInTheDocument(); expect(screen.getByText('[new custom scoping]')).toHaveClass('active'); - await waitFor(() => - userEvent.click(screen.getByRole('combobox', { name: 'Select chart' })), - ); - await waitFor(() => { - userEvent.click( - within(document.querySelector('.rc-virtual-list')!).getByText('chart 1'), - ); - }); - - expect( - within(document.querySelector('.ant-select-selection-item')!).getByText( - 'chart 1', - ), - ).toBeInTheDocument(); - + await selectOption('chart 1', 'Select chart'); expect( document.querySelectorAll( '[data-test="scoping-tree-panel"] .ant-tree-checkbox-checked', @@ -251,14 +243,8 @@ it('edit scope and save', async () => { // create custom scoping for chart 1 with unselected chart 2 (from global) and chart 4 userEvent.click(screen.getByText('Add custom scoping')); - await waitFor(() => - userEvent.click(screen.getByRole('combobox', { name: 'Select chart' })), - ); - await waitFor(() => { - userEvent.click( - within(document.querySelector('.rc-virtual-list')!).getByText('chart 1'), - ); - }); + await selectOption('chart 1', 'Select chart'); + userEvent.click( within(document.querySelector('.ant-tree')!).getByText('chart 4'), ); diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterControl.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterControl.tsx index bc66d07c08b4..b9229153b8cc 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterControl.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterControl.tsx @@ -315,6 +315,7 @@ const FilterControl = ({
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterValue.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterValue.tsx index 5041fa3c97ee..2e626636c7d9 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterValue.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterValue.tsx @@ -174,7 +174,7 @@ const FilterValue: React.FC = ({ setIsRefreshing(true); getChartDataRequest({ formData: newFormData, - force: false, + force: shouldRefresh, ownState: filterOwnState, }) .then(({ response, json }) => { diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterCard/DependenciesRow.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterCard/DependenciesRow.tsx index 253ce4649d98..3ac76882ba0f 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterCard/DependenciesRow.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterCard/DependenciesRow.tsx @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import React, { useCallback, useMemo, useRef } from 'react'; +import React, { useCallback, useMemo } from 'react'; import { useDispatch } from 'react-redux'; import { css, t, useTheme, useTruncation } from '@superset-ui/core'; import Icons from 'src/components/Icons'; @@ -53,12 +53,8 @@ const DependencyValue = ({ export const DependenciesRow = React.memo(({ filter }: FilterCardRowProps) => { const dependencies = useFilterDependencies(filter); - const dependenciesRef = useRef(null); - const plusRef = useRef(null); - const [elementsTruncated, hasHiddenElements] = useTruncation( - dependenciesRef, - plusRef, - ); + const [dependenciesRef, plusRef, elementsTruncated, hasHiddenElements] = + useTruncation(); const theme = useTheme(); const tooltipText = useMemo( diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterCard/NameRow.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterCard/NameRow.tsx index 37f18eda29e1..58e9969b91d1 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterCard/NameRow.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterCard/NameRow.tsx @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import React, { useRef } from 'react'; +import React from 'react'; import { useSelector } from 'react-redux'; import { css, SupersetTheme, useTheme, useTruncation } from '@superset-ui/core'; import Icons from 'src/components/Icons'; @@ -31,8 +31,7 @@ export const NameRow = ({ hidePopover, }: FilterCardRowProps & { hidePopover: () => void }) => { const theme = useTheme(); - const filterNameRef = useRef(null); - const [elementsTruncated] = useTruncation(filterNameRef); + const [filterNameRef, , elementsTruncated] = useTruncation(); const dashboardId = useSelector( ({ dashboardInfo }) => dashboardInfo.id, ); diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterCard/ScopeRow.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterCard/ScopeRow.tsx index ff5c1142a538..910fb99aaa61 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterCard/ScopeRow.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterCard/ScopeRow.tsx @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import React, { useMemo, useRef } from 'react'; +import React, { useMemo } from 'react'; import { t, useTruncation } from '@superset-ui/core'; import { useFilterScope } from './useFilterScope'; import { @@ -44,13 +44,9 @@ const getTooltipSection = (items: string[] | undefined, label: string) => export const ScopeRow = React.memo(({ filter }: FilterCardRowProps) => { const scope = useFilterScope(filter); - const scopeRef = useRef(null); - const plusRef = useRef(null); - const [elementsTruncated, hasHiddenElements] = useTruncation( - scopeRef, - plusRef, - ); + const [scopeRef, plusRef, elementsTruncated, hasHiddenElements] = + useTruncation(); const tooltipText = useMemo(() => { if (elementsTruncated === 0 || !scope) { return null; @@ -81,7 +77,7 @@ export const ScopeRow = React.memo(({ filter }: FilterCardRowProps) => { )) : t('None')} - {hasHiddenElements > 0 && ( + {hasHiddenElements && ( +{elementsTruncated} diff --git a/superset-frontend/src/dashboard/constants.ts b/superset-frontend/src/dashboard/constants.ts index d4e5fed4b632..d512c2a8a6cc 100644 --- a/superset-frontend/src/dashboard/constants.ts +++ b/superset-frontend/src/dashboard/constants.ts @@ -43,6 +43,7 @@ export const FILTER_BAR_HEADER_HEIGHT = 80; export const FILTER_BAR_TABS_HEIGHT = 46; export const BUILDER_SIDEPANEL_WIDTH = 374; export const OVERWRITE_INSPECT_FIELDS = ['css', 'json_metadata.filter_scopes']; +export const EMPTY_CONTAINER_Z_INDEX = 10; export const DEFAULT_CROSS_FILTER_SCOPING: NativeFilterScope = { rootPath: [DASHBOARD_ROOT_ID], diff --git a/superset-frontend/src/dashboard/reducers/dashboardState.js b/superset-frontend/src/dashboard/reducers/dashboardState.js index 015cb9822c58..ebef4db03570 100644 --- a/superset-frontend/src/dashboard/reducers/dashboardState.js +++ b/superset-frontend/src/dashboard/reducers/dashboardState.js @@ -38,7 +38,6 @@ import { SET_FOCUSED_FILTER_FIELD, UNSET_FOCUSED_FILTER_FIELD, SET_ACTIVE_TAB, - SET_ACTIVE_TABS, SET_FULL_SIZE_CHART_ID, ON_FILTERS_REFRESH, ON_FILTERS_REFRESH_SUCCESS, @@ -189,12 +188,6 @@ export default function dashboardStateReducer(state = {}, action) { activeTabs: Array.from(newActiveTabs), }; }, - [SET_ACTIVE_TABS]() { - return { - ...state, - activeTabs: action.activeTabs, - }; - }, [SET_OVERRIDE_CONFIRM]() { return { ...state, diff --git a/superset-frontend/src/dashboard/reducers/dashboardState.test.ts b/superset-frontend/src/dashboard/reducers/dashboardState.test.ts index 3a8adc6cbbdc..378c5978f939 100644 --- a/superset-frontend/src/dashboard/reducers/dashboardState.test.ts +++ b/superset-frontend/src/dashboard/reducers/dashboardState.test.ts @@ -18,7 +18,7 @@ */ import dashboardStateReducer from './dashboardState'; -import { setActiveTab, setActiveTabs } from '../actions/dashboardState'; +import { setActiveTab } from '../actions/dashboardState'; describe('DashboardState reducer', () => { it('SET_ACTIVE_TAB', () => { @@ -35,16 +35,4 @@ describe('DashboardState reducer', () => { ), ).toEqual({ activeTabs: ['tab2'] }); }); - - it('SET_ACTIVE_TABS', () => { - expect( - dashboardStateReducer({ activeTabs: [] }, setActiveTabs(['tab1'])), - ).toEqual({ activeTabs: ['tab1'] }); - expect( - dashboardStateReducer( - { activeTabs: ['tab1', 'tab2'] }, - setActiveTabs(['tab3', 'tab4']), - ), - ).toEqual({ activeTabs: ['tab3', 'tab4'] }); - }); }); diff --git a/superset-frontend/src/dashboard/util/getDropPosition.js b/superset-frontend/src/dashboard/util/getDropPosition.js index 81cbc48f4694..9a68808b05dc 100644 --- a/superset-frontend/src/dashboard/util/getDropPosition.js +++ b/superset-frontend/src/dashboard/util/getDropPosition.js @@ -23,6 +23,7 @@ export const DROP_TOP = 'DROP_TOP'; export const DROP_RIGHT = 'DROP_RIGHT'; export const DROP_BOTTOM = 'DROP_BOTTOM'; export const DROP_LEFT = 'DROP_LEFT'; +export const DROP_FORBIDDEN = 'DROP_FORBIDDEN'; // this defines how close the mouse must be to the edge of a component to display // a sibling type drop indicator @@ -72,7 +73,7 @@ export default function getDropPosition(monitor, Component) { }); if (!validChild && !validSibling) { - return null; + return DROP_FORBIDDEN; } const hasChildren = (component.children || []).length > 0; @@ -81,7 +82,7 @@ export default function getDropPosition(monitor, Component) { const siblingDropOrientation = orientation === 'row' ? 'horizontal' : 'vertical'; - if (isDraggingOverShallow && validChild && !validSibling) { + if (validChild && !validSibling) { // easiest case, insert as child if (childDropOrientation === 'vertical') { return hasChildren ? DROP_RIGHT : DROP_LEFT; diff --git a/superset-frontend/src/dashboard/util/getDropPosition.test.js b/superset-frontend/src/dashboard/util/getDropPosition.test.js index 71f506b1bd7e..9fdd02afa6f6 100644 --- a/superset-frontend/src/dashboard/util/getDropPosition.test.js +++ b/superset-frontend/src/dashboard/util/getDropPosition.test.js @@ -21,6 +21,7 @@ import getDropPosition, { DROP_RIGHT, DROP_BOTTOM, DROP_LEFT, + DROP_FORBIDDEN, } from 'src/dashboard/util/getDropPosition'; import { @@ -80,7 +81,7 @@ describe('getDropPosition', () => { } describe('invalid child + invalid sibling', () => { - it('should return null', () => { + it('should return DROP_FORBIDDEN', () => { const result = getDropPosition( // TAB is an invalid child + sibling of GRID > ROW ...getMocks({ @@ -89,7 +90,7 @@ describe('getDropPosition', () => { draggingType: TAB_TYPE, }), ); - expect(result).toBeNull(); + expect(result).toBe(DROP_FORBIDDEN); }); }); diff --git a/superset-frontend/src/explore/actions/exploreActions.test.js b/superset-frontend/src/explore/actions/exploreActions.test.js index 54cf8f16c5c3..12bbdce4e7df 100644 --- a/superset-frontend/src/explore/actions/exploreActions.test.js +++ b/superset-frontend/src/explore/actions/exploreActions.test.js @@ -217,4 +217,23 @@ describe('reducers', () => { expectedColumnConfig, ); }); + + test('setStashFormData works as expected with fieldNames', () => { + const newState = exploreReducer( + defaultState, + actions.setStashFormData(true, ['y_axis_format']), + ); + expect(newState.hiddenFormData).toEqual({ + y_axis_format: defaultState.form_data.y_axis_format, + }); + expect(newState.form_data.y_axis_format).toBeFalsy(); + const updatedState = exploreReducer( + newState, + actions.setStashFormData(false, ['y_axis_format']), + ); + expect(updatedState.hiddenFormData.y_axis_format).toBeFalsy(); + expect(updatedState.form_data.y_axis_format).toEqual( + defaultState.form_data.y_axis_format, + ); + }); }); diff --git a/superset-frontend/src/explore/actions/exploreActions.ts b/superset-frontend/src/explore/actions/exploreActions.ts index 36300b4a123a..da702ac16ff5 100644 --- a/superset-frontend/src/explore/actions/exploreActions.ts +++ b/superset-frontend/src/explore/actions/exploreActions.ts @@ -152,6 +152,18 @@ export function setForceQuery(force: boolean) { }; } +export const SET_STASH_FORM_DATA = 'SET_STASH_FORM_DATA'; +export function setStashFormData( + isHidden: boolean, + fieldNames: ReadonlyArray, +) { + return { + type: SET_STASH_FORM_DATA, + isHidden, + fieldNames, + }; +} + export const exploreActions = { ...toastActions, fetchDatasourcesStarted, @@ -161,6 +173,7 @@ export const exploreActions = { saveFaveStar, setControlValue, setExploreControls, + setStashFormData, updateChartTitle, createNewSlice, sliceUpdated, diff --git a/superset-frontend/src/explore/components/ChartPills.tsx b/superset-frontend/src/explore/components/ChartPills.tsx index 99f7c5aad5a6..489ae146aa5b 100644 --- a/superset-frontend/src/explore/components/ChartPills.tsx +++ b/superset-frontend/src/explore/components/ChartPills.tsx @@ -66,7 +66,7 @@ export const ChartPills = forwardRef( > {!isLoading && firstQueryResponse && ( )} diff --git a/superset-frontend/src/explore/components/ControlPanelsContainer.test.tsx b/superset-frontend/src/explore/components/ControlPanelsContainer.test.tsx index 333d3ec799c2..37bdfb4fc055 100644 --- a/superset-frontend/src/explore/components/ControlPanelsContainer.test.tsx +++ b/superset-frontend/src/explore/components/ControlPanelsContainer.test.tsx @@ -17,6 +17,7 @@ * under the License. */ import React from 'react'; +import { useSelector } from 'react-redux'; import userEvent from '@testing-library/user-event'; import { render, screen } from 'spec/helpers/testing-library'; import { @@ -24,13 +25,22 @@ import { getChartControlPanelRegistry, t, } from '@superset-ui/core'; -import { defaultControls } from 'src/explore/store'; +import { defaultControls, defaultState } from 'src/explore/store'; +import { ExplorePageState } from 'src/explore/types'; import { getFormDataFromControls } from 'src/explore/controlUtils'; import { ControlPanelsContainer, ControlPanelsContainerProps, } from 'src/explore/components/ControlPanelsContainer'; +const FormDataMock = () => { + const formData = useSelector( + (state: ExplorePageState) => state.explore.form_data, + ); + + return
{Object.keys(formData).join(':')}
; +}; + describe('ControlPanelsContainer', () => { beforeAll(() => { getChartControlPanelRegistry().registerValue('table', { @@ -144,4 +154,54 @@ describe('ControlPanelsContainer', () => { await screen.findAllByTestId('collapsible-control-panel-header'), ).toHaveLength(2); }); + + test('visibility of panels is correctly applied', async () => { + getChartControlPanelRegistry().registerValue('table', { + controlPanelSections: [ + { + label: t('Advanced analytics'), + description: t('Advanced analytics post processing'), + expanded: true, + controlSetRows: [['groupby'], ['metrics'], ['percent_metrics']], + visibility: () => false, + }, + { + label: t('Chart Title'), + visibility: () => true, + controlSetRows: [['timeseries_limit_metric', 'row_limit']], + }, + { + label: t('Chart Options'), + controlSetRows: [['include_time', 'order_desc']], + }, + ], + }); + const { getByTestId } = render( + <> + + + , + { + useRedux: true, + initialState: { explore: { form_data: defaultState.form_data } }, + }, + ); + + const disabledSection = screen.queryByRole('button', { + name: /advanced analytics/i, + }); + expect(disabledSection).not.toBeInTheDocument(); + expect( + screen.getByRole('button', { name: /chart title/i }), + ).toBeInTheDocument(); + expect( + screen.queryByRole('button', { name: /chart options/i }), + ).toBeInTheDocument(); + + expect(getByTestId('mock-formdata')).not.toHaveTextContent('groupby'); + expect(getByTestId('mock-formdata')).not.toHaveTextContent('metrics'); + expect(getByTestId('mock-formdata')).not.toHaveTextContent( + 'percent_metrics', + ); + }); }); diff --git a/superset-frontend/src/explore/components/ControlPanelsContainer.tsx b/superset-frontend/src/explore/components/ControlPanelsContainer.tsx index cdc9259c0aef..1d12d5e32f47 100644 --- a/superset-frontend/src/explore/components/ControlPanelsContainer.tsx +++ b/superset-frontend/src/explore/components/ControlPanelsContainer.tsx @@ -71,6 +71,7 @@ import { ExploreAlert } from './ExploreAlert'; import { RunQueryButton } from './RunQueryButton'; import { Operators } from '../constants'; import { Clauses } from './controls/FilterControl/types'; +import StashFormDataContainer from './StashFormDataContainer'; const { confirm } = Modal; @@ -484,7 +485,7 @@ export const ControlPanelsContainer = (props: ControlPanelsContainerProps) => { ? baseDescription(exploreState, controls[name], chart) : baseDescription; - if (name === 'adhoc_filters') { + if (name.includes('adhoc_filters')) { restProps.canDelete = ( valueToBeDeleted: Record, values: Record[], @@ -504,16 +505,22 @@ export const ControlPanelsContainer = (props: ControlPanelsContainerProps) => { } return ( - + + + ); }; @@ -526,13 +533,13 @@ export const ControlPanelsContainer = (props: ControlPanelsContainerProps) => { section: ExpandedControlPanelSectionConfig, ) => { const { controls } = props; - const { label, description } = section; + const { label, description, visibility } = section; // Section label can be a ReactNode but in some places we want to // have a string ID. Using forced type conversion for now, // should probably add a `id` field to sections in the future. const sectionId = String(label); - + const isVisible = visibility?.call(this, props, controls) !== false; const hasErrors = section.controlSetRows.some(rows => rows.some(item => { const controlName = @@ -590,67 +597,85 @@ export const ControlPanelsContainer = (props: ControlPanelsContainerProps) => { ); return ( - css` - margin-bottom: 0; - box-shadow: none; - - &:last-child { - padding-bottom: ${theme.gridUnit * 16}px; - border-bottom: 0; - } + <> + + item && typeof item === 'object' + ? 'name' in item + ? item.name + : '' + : String(item || ''), + ) + .filter(Boolean)} + /> + {isVisible && ( + css` + margin-bottom: 0; + box-shadow: none; + + &:last-child { + padding-bottom: ${theme.gridUnit * 16}px; + border-bottom: 0; + } - .panel-body { - margin-left: ${theme.gridUnit * 4}px; - padding-bottom: 0; - } + .panel-body { + margin-left: ${theme.gridUnit * 4}px; + padding-bottom: 0; + } - span.label { - display: inline-block; - } - ${!section.label && - ` + span.label { + display: inline-block; + } + ${!section.label && + ` .ant-collapse-header { display: none; } `} - `} - header={} - key={sectionId} - > - {section.controlSetRows.map((controlSets, i) => { - const renderedControls = controlSets - .map(controlItem => { - if (!controlItem) { - // When the item is invalid + `} + header={} + key={sectionId} + > + {section.controlSetRows.map((controlSets, i) => { + const renderedControls = controlSets + .map(controlItem => { + if (!controlItem) { + // When the item is invalid + return null; + } + if (React.isValidElement(controlItem)) { + // When the item is a React element + return controlItem; + } + if ( + controlItem.name && + controlItem.config && + controlItem.name !== 'datasource' + ) { + return renderControl(controlItem); + } + return null; + }) + .filter(x => x !== null); + // don't show the row if it is empty + if (renderedControls.length === 0) { return null; } - if (React.isValidElement(controlItem)) { - // When the item is a React element - return controlItem; - } - if ( - controlItem.name && - controlItem.config && - controlItem.name !== 'datasource' - ) { - return renderControl(controlItem); - } - return null; - }) - .filter(x => x !== null); - // don't show the row if it is empty - if (renderedControls.length === 0) { - return null; - } - return ( - - ); - })} - + return ( + + ); + })} + + )} + ); }; diff --git a/superset-frontend/src/explore/components/ControlRow.test.tsx b/superset-frontend/src/explore/components/ControlRow.test.tsx index 0b5707867654..bbd911b3aeaf 100644 --- a/superset-frontend/src/explore/components/ControlRow.test.tsx +++ b/superset-frontend/src/explore/components/ControlRow.test.tsx @@ -19,49 +19,73 @@ import React from 'react'; import { render, screen } from 'spec/helpers/testing-library'; import ControlSetRow from 'src/explore/components/ControlRow'; +import StashFormDataContainer from './StashFormDataContainer'; const MockControl = (props: { children: React.ReactElement; type?: string; isVisible?: boolean; }) =>
{props.children}
; -describe('ControlSetRow', () => { - it('renders a single row with one element', () => { - render(My Control 1

]} />); - expect(screen.getAllByText('My Control 1').length).toBe(1); - }); - it('renders a single row with two elements', () => { - render( - My Control 1

,

My Control 2

]} />, - ); - expect(screen.getAllByText(/My Control/)).toHaveLength(2); - }); +test('renders a single row with one element', () => { + render(My Control 1

]} />); + expect(screen.getAllByText('My Control 1').length).toBe(1); +}); +test('renders a single row with two elements', () => { + render( + My Control 1

,

My Control 2

]} />, + ); + expect(screen.getAllByText(/My Control/)).toHaveLength(2); + expect(screen.getAllByText(/My Control/)[0]).toBeVisible(); + expect(screen.getAllByText(/My Control/)[1]).toBeVisible(); +}); - it('renders a single row with one elements if is HiddenControl', () => { - render( - My Control 1

, - -

My Control 2

-
, - ]} - />, - ); - expect(screen.getAllByText(/My Control/)).toHaveLength(2); - }); +test('renders a single row with one elements if is HiddenControl', () => { + render( + My Control 1

, + +

My Control 2

+
, + ]} + />, + ); + expect(screen.getAllByText(/My Control/)).toHaveLength(2); + expect(screen.getAllByText(/My Control/)[0]).toBeVisible(); + expect(screen.getAllByText(/My Control/)[1]).not.toBeVisible(); +}); + +test('renders a single row with one elements if is invisible', () => { + render( + My Control 1

, + +

My Control 2

+
, + ]} + />, + ); + expect(screen.getAllByText(/My Control/)).toHaveLength(2); + expect(screen.getAllByText(/My Control/)[0]).toBeVisible(); + expect(screen.getAllByText(/My Control/)[1]).not.toBeVisible(); +}); - it('renders a single row with one elements if is invisible', () => { - render( - My Control 1

, +test('renders a single row with one element wrapping with StashContainer if is invisible', () => { + render( + My Control 1

, +

My Control 2

-
, - ]} - />, - ); - expect(screen.getAllByText(/My Control/)).toHaveLength(2); - }); + +
, + ]} + />, + { useRedux: true }, + ); + expect(screen.getAllByText(/My Control/)).toHaveLength(2); + expect(screen.getAllByText(/My Control/)[0]).toBeVisible(); + expect(screen.getAllByText(/My Control/)[1]).not.toBeVisible(); }); diff --git a/superset-frontend/src/explore/components/ControlRow.tsx b/superset-frontend/src/explore/components/ControlRow.tsx index 5721b5de2893..291faa811585 100644 --- a/superset-frontend/src/explore/components/ControlRow.tsx +++ b/superset-frontend/src/explore/components/ControlRow.tsx @@ -23,12 +23,13 @@ const NUM_COLUMNS = 12; type Control = React.ReactElement | null; export default function ControlRow({ controls }: { controls: Control[] }) { - const isHiddenControl = useCallback( - (control: Control) => - control?.props.type === 'HiddenControl' || - control?.props.isVisible === false, - [], - ); + const isHiddenControl = useCallback((control: Control) => { + const props = + control && 'shouldStash' in control.props + ? control.props.children.props + : control?.props; + return props?.type === 'HiddenControl' || props?.isVisible === false; + }, []); // Invisible control should not be counted // in the columns number const countableControls = controls.filter( @@ -41,6 +42,7 @@ export default function ControlRow({ controls }: { controls: Control[] }) {
{controls.map((control, i) => (
[]; - loading: boolean; -}) => ; - enum FormatPickerValue { Formatted = 'formatted', Original = 'original', diff --git a/superset-frontend/src/explore/components/DataTablesPane/DataTablesPane.tsx b/superset-frontend/src/explore/components/DataTablesPane/DataTablesPane.tsx index 15148be880cf..162f6269017f 100644 --- a/superset-frontend/src/explore/components/DataTablesPane/DataTablesPane.tsx +++ b/superset-frontend/src/explore/components/DataTablesPane/DataTablesPane.tsx @@ -123,7 +123,8 @@ export const DataTablesPane = ({ if ( panelOpen && activeTabKey.startsWith(ResultTypes.Results) && - chartStatus === 'rendered' + chartStatus && + chartStatus !== 'loading' ) { setIsRequest({ results: true, diff --git a/superset-frontend/src/explore/components/DataTablesPane/components/DataTableControls.tsx b/superset-frontend/src/explore/components/DataTablesPane/components/DataTableControls.tsx index 7a02114a2f52..b1ff73e29e5d 100644 --- a/superset-frontend/src/explore/components/DataTablesPane/components/DataTableControls.tsx +++ b/superset-frontend/src/explore/components/DataTablesPane/components/DataTableControls.tsx @@ -22,10 +22,10 @@ import { css, GenericDataType, styled } from '@superset-ui/core'; import { CopyToClipboardButton, FilterInput, - RowCount, } from 'src/explore/components/DataTableControl'; import { applyFormattingToTabularData } from 'src/utils/common'; import { getTimeColumns } from 'src/explore/components/DataTableControl/utils'; +import RowCountLabel from 'src/explore/components/RowCountLabel'; import { TableControlsProps } from '../types'; export const TableControlsWrapper = styled.div` @@ -47,6 +47,7 @@ export const TableControls = ({ onInputChange, columnNames, columnTypes, + rowcount, isLoading, }: TableControlsProps) => { const originalTimeColumns = getTimeColumns(datasourceId); @@ -74,7 +75,7 @@ export const TableControls = ({ align-items: center; `} > - +
diff --git a/superset-frontend/src/explore/components/DataTablesPane/components/SamplesPane.tsx b/superset-frontend/src/explore/components/DataTablesPane/components/SamplesPane.tsx index b542aad99643..eaae35bd2fa5 100644 --- a/superset-frontend/src/explore/components/DataTablesPane/components/SamplesPane.tsx +++ b/superset-frontend/src/explore/components/DataTablesPane/components/SamplesPane.tsx @@ -48,6 +48,7 @@ export const SamplesPane = ({ const [colnames, setColnames] = useState([]); const [coltypes, setColtypes] = useState([]); const [isLoading, setIsLoading] = useState(false); + const [rowcount, setRowCount] = useState(0); const [responseError, setResponseError] = useState(''); const datasourceId = useMemo( () => `${datasource.id}__${datasource.type}`, @@ -66,6 +67,7 @@ export const SamplesPane = ({ setData(ensureIsArray(response.data)); setColnames(ensureIsArray(response.colnames)); setColtypes(ensureIsArray(response.coltypes)); + setRowCount(response.rowcount); setResponseError(''); cache.add(datasource); if (queryForce && actions) { @@ -108,6 +110,7 @@ export const SamplesPane = ({ data={filteredData} columnNames={colnames} columnTypes={coltypes} + rowcount={rowcount} datasourceId={datasourceId} onInputChange={input => setFilterText(input)} isLoading={isLoading} @@ -128,6 +131,7 @@ export const SamplesPane = ({ data={filteredData} columnNames={colnames} columnTypes={coltypes} + rowcount={rowcount} datasourceId={datasourceId} onInputChange={input => setFilterText(input)} isLoading={isLoading} diff --git a/superset-frontend/src/explore/components/DataTablesPane/components/SingleQueryResultPane.tsx b/superset-frontend/src/explore/components/DataTablesPane/components/SingleQueryResultPane.tsx index c2614dfda6ca..b54316784044 100644 --- a/superset-frontend/src/explore/components/DataTablesPane/components/SingleQueryResultPane.tsx +++ b/superset-frontend/src/explore/components/DataTablesPane/components/SingleQueryResultPane.tsx @@ -30,6 +30,7 @@ export const SingleQueryResultPane = ({ data, colnames, coltypes, + rowcount, datasourceId, dataSize = 50, isVisible, @@ -55,6 +56,7 @@ export const SingleQueryResultPane = ({ data={filteredData} columnNames={colnames} columnTypes={coltypes} + rowcount={rowcount} datasourceId={datasourceId} onInputChange={input => setFilterText(input)} isLoading={false} diff --git a/superset-frontend/src/explore/components/DataTablesPane/components/useResultsPane.tsx b/superset-frontend/src/explore/components/DataTablesPane/components/useResultsPane.tsx index 9b6b597c7900..bae6b6fd7b32 100644 --- a/superset-frontend/src/explore/components/DataTablesPane/components/useResultsPane.tsx +++ b/superset-frontend/src/explore/components/DataTablesPane/components/useResultsPane.tsx @@ -119,6 +119,7 @@ export const useResultsPane = ({ data={[]} columnNames={[]} columnTypes={[]} + rowcount={0} datasourceId={queryFormData.datasource} onInputChange={() => {}} isLoading={false} @@ -135,7 +136,6 @@ export const useResultsPane = ({ , ); } - return resultResp .slice(0, queryCount) .map((result, idx) => ( @@ -143,6 +143,7 @@ export const useResultsPane = ({ data={result.data} colnames={result.colnames} coltypes={result.coltypes} + rowcount={result.rowcount} dataSize={dataSize} datasourceId={queryFormData.datasource} key={idx} diff --git a/superset-frontend/src/explore/components/DataTablesPane/test/DataTablesPane.test.tsx b/superset-frontend/src/explore/components/DataTablesPane/test/DataTablesPane.test.tsx index 74358af959e1..40370638e51e 100644 --- a/superset-frontend/src/explore/components/DataTablesPane/test/DataTablesPane.test.tsx +++ b/superset-frontend/src/explore/components/DataTablesPane/test/DataTablesPane.test.tsx @@ -93,6 +93,8 @@ describe('DataTablesPane', () => { data: [{ __timestamp: 1230768000000, genre: 'Action' }], colnames: ['__timestamp', 'genre'], coltypes: [2, 1], + rowcount: 1, + sql_rowcount: 1, }, ], }, @@ -125,6 +127,8 @@ describe('DataTablesPane', () => { ], colnames: ['__timestamp', 'genre'], coltypes: [2, 1], + rowcount: 2, + sql_rowcount: 2, }, ], }, @@ -135,6 +139,7 @@ describe('DataTablesPane', () => { }); userEvent.click(screen.getByText('Results')); expect(await screen.findByText('2 rows')).toBeVisible(); + expect(screen.getByText('Action')).toBeVisible(); expect(screen.getByText('Horror')).toBeVisible(); diff --git a/superset-frontend/src/explore/components/DataTablesPane/test/ResultsPaneOnDashboard.test.tsx b/superset-frontend/src/explore/components/DataTablesPane/test/ResultsPaneOnDashboard.test.tsx index 839126bb6253..a6f9830d2e17 100644 --- a/superset-frontend/src/explore/components/DataTablesPane/test/ResultsPaneOnDashboard.test.tsx +++ b/superset-frontend/src/explore/components/DataTablesPane/test/ResultsPaneOnDashboard.test.tsx @@ -50,6 +50,8 @@ describe('ResultsPaneOnDashboard', () => { ], colnames: ['__timestamp', 'genre'], coltypes: [2, 1], + rowcount: 2, + sql_rowcount: 2, }, ], }, @@ -78,6 +80,8 @@ describe('ResultsPaneOnDashboard', () => { data: [{ genre: 'Action' }, { genre: 'Horror' }], colnames: ['genre'], coltypes: [1], + rowcount: 2, + sql_rowcount: 2, }, ], }, diff --git a/superset-frontend/src/explore/components/DataTablesPane/test/SamplesPane.test.tsx b/superset-frontend/src/explore/components/DataTablesPane/test/SamplesPane.test.tsx index eed6e84808b7..02c421aeec8d 100644 --- a/superset-frontend/src/explore/components/DataTablesPane/test/SamplesPane.test.tsx +++ b/superset-frontend/src/explore/components/DataTablesPane/test/SamplesPane.test.tsx @@ -50,6 +50,8 @@ describe('SamplesPane', () => { ], colnames: ['__timestamp', 'genre'], coltypes: [2, 1], + rowcount: 2, + sql_rowcount: 2, }, }, ); diff --git a/superset-frontend/src/explore/components/DataTablesPane/types.ts b/superset-frontend/src/explore/components/DataTablesPane/types.ts index 4e6062ba4a25..4939b7d7488c 100644 --- a/superset-frontend/src/explore/components/DataTablesPane/types.ts +++ b/superset-frontend/src/explore/components/DataTablesPane/types.ts @@ -71,11 +71,13 @@ export interface TableControlsProps { columnNames: string[]; columnTypes: GenericDataType[]; isLoading: boolean; + rowcount: number; } export interface QueryResultInterface { colnames: string[]; coltypes: GenericDataType[]; + rowcount: number; data: Record[][]; } diff --git a/superset-frontend/src/explore/components/DatasourcePanel/DatasourcePanel.test.tsx b/superset-frontend/src/explore/components/DatasourcePanel/DatasourcePanel.test.tsx index 95258f443ec4..a0c7d707d550 100644 --- a/superset-frontend/src/explore/components/DatasourcePanel/DatasourcePanel.test.tsx +++ b/superset-frontend/src/explore/components/DatasourcePanel/DatasourcePanel.test.tsx @@ -17,7 +17,7 @@ * under the License. */ import React from 'react'; -import { render, screen, waitFor } from 'spec/helpers/testing-library'; +import { render, screen, waitFor, within } from 'spec/helpers/testing-library'; import userEvent from '@testing-library/user-event'; import DatasourcePanel, { IDatasource, @@ -29,6 +29,22 @@ import { } from 'src/explore/components/DatasourcePanel/fixtures'; import { DatasourceType } from '@superset-ui/core'; import DatasourceControl from 'src/explore/components/controls/DatasourceControl'; +import ExploreContainer from '../ExploreContainer'; +import { + DndColumnSelect, + DndMetricSelect, +} from '../controls/DndColumnSelectControl'; + +jest.mock( + 'react-virtualized-auto-sizer', + () => + ({ + children, + }: { + children: (params: { height: number }) => React.ReactChild; + }) => + children({ height: 500 }), +); const datasource: IDatasource = { id: 1, @@ -69,6 +85,13 @@ const props: DatasourcePanelProps = { actions: { setControlValue: jest.fn(), }, + width: 300, +}; + +const metricProps = { + savedMetrics: [], + columns: [], + onChange: jest.fn(), }; const search = (value: string, input: HTMLElement) => { @@ -92,7 +115,13 @@ test('should display items in controls', async () => { }); test('should render the metrics', async () => { - render(, { useRedux: true, useDnd: true }); + render( + + + + , + { useRedux: true, useDnd: true }, + ); const metricsNum = metrics.length; metrics.forEach(metric => expect(screen.getByText(metric.metric_name)).toBeInTheDocument(), @@ -103,7 +132,13 @@ test('should render the metrics', async () => { }); test('should render the columns', async () => { - render(, { useRedux: true, useDnd: true }); + render( + + + + , + { useRedux: true, useDnd: true }, + ); const columnsNum = columns.length; columns.forEach(col => expect(screen.getByText(col.column_name)).toBeInTheDocument(), @@ -122,7 +157,13 @@ test('should render 0 search results', async () => { }); test('should search and render matching columns', async () => { - render(, { useRedux: true, useDnd: true }); + render( + + + + , + { useRedux: true, useDnd: true }, + ); const searchInput = screen.getByPlaceholderText('Search Metrics & Columns'); search(columns[0].column_name, searchInput); @@ -134,7 +175,13 @@ test('should search and render matching columns', async () => { }); test('should search and render matching metrics', async () => { - render(, { useRedux: true, useDnd: true }); + render( + + + + , + { useRedux: true, useDnd: true }, + ); const searchInput = screen.getByPlaceholderText('Search Metrics & Columns'); search(metrics[0].metric_name, searchInput); @@ -199,3 +246,41 @@ test('should not render a save dataset modal when datasource is not query or dat expect(screen.queryByText(/create a dataset/i)).toBe(null); }); + +test('should render only droppable metrics and columns', async () => { + const column1FilterProps = { + type: 'DndColumnSelect' as const, + name: 'Filter', + onChange: jest.fn(), + options: [{ column_name: columns[1].column_name }], + actions: { setControlValue: jest.fn() }, + }; + const column2FilterProps = { + type: 'DndColumnSelect' as const, + name: 'Filter', + onChange: jest.fn(), + options: [ + { column_name: columns[1].column_name }, + { column_name: columns[2].column_name }, + ], + actions: { setControlValue: jest.fn() }, + }; + const { getByTestId } = render( + + + + + , + { useRedux: true, useDnd: true }, + ); + const selections = getByTestId('fieldSelections'); + expect( + within(selections).queryByText(columns[0].column_name), + ).not.toBeInTheDocument(); + expect( + within(selections).queryByText(columns[1].column_name), + ).toBeInTheDocument(); + expect( + within(selections).queryByText(columns[2].column_name), + ).toBeInTheDocument(); +}); diff --git a/superset-frontend/src/explore/components/DatasourcePanel/DatasourcePanelItem.test.tsx b/superset-frontend/src/explore/components/DatasourcePanel/DatasourcePanelItem.test.tsx new file mode 100644 index 000000000000..abe3207e4d18 --- /dev/null +++ b/superset-frontend/src/explore/components/DatasourcePanel/DatasourcePanelItem.test.tsx @@ -0,0 +1,199 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React from 'react'; + +import { + columns, + metrics, +} from 'src/explore/components/DatasourcePanel/fixtures'; +import { fireEvent, render, within } from 'spec/helpers/testing-library'; +import DatasourcePanelItem from './DatasourcePanelItem'; + +const mockData = { + metricSlice: metrics, + columnSlice: columns, + totalMetrics: Math.max(metrics.length, 10), + totalColumns: Math.max(columns.length, 13), + width: 300, + showAllMetrics: false, + onShowAllMetricsChange: jest.fn(), + showAllColumns: false, + onShowAllColumnsChange: jest.fn(), + collapseMetrics: false, + onCollapseMetricsChange: jest.fn(), + collapseColumns: false, + onCollapseColumnsChange: jest.fn(), + hiddenMetricCount: 0, + hiddenColumnCount: 0, +}; + +test('renders each item accordingly', () => { + const { getByText, getByTestId, rerender, container } = render( + , + { useDnd: true }, + ); + + expect(getByText('Metrics')).toBeInTheDocument(); + rerender(); + expect( + getByText( + `Showing ${mockData.metricSlice.length} of ${mockData.totalMetrics}`, + ), + ).toBeInTheDocument(); + mockData.metricSlice.forEach((metric, metricIndex) => { + rerender( + , + ); + expect(getByTestId('DatasourcePanelDragOption')).toBeInTheDocument(); + expect( + within(getByTestId('DatasourcePanelDragOption')).getByText( + metric.metric_name, + ), + ).toBeInTheDocument(); + }); + rerender( + , + ); + expect(container).toHaveTextContent(''); + + const startIndexOfColumnSection = mockData.metricSlice.length + 3; + rerender( + , + ); + expect(getByText('Columns')).toBeInTheDocument(); + rerender( + , + ); + expect( + getByText( + `Showing ${mockData.columnSlice.length} of ${mockData.totalColumns}`, + ), + ).toBeInTheDocument(); + mockData.columnSlice.forEach((column, columnIndex) => { + rerender( + , + ); + expect(getByTestId('DatasourcePanelDragOption')).toBeInTheDocument(); + expect( + within(getByTestId('DatasourcePanelDragOption')).getByText( + column.column_name, + ), + ).toBeInTheDocument(); + }); +}); + +test('can collapse metrics and columns', () => { + mockData.onCollapseMetricsChange.mockClear(); + mockData.onCollapseColumnsChange.mockClear(); + const { queryByText, getByRole, rerender } = render( + , + { useDnd: true }, + ); + fireEvent.click(getByRole('button')); + expect(mockData.onCollapseMetricsChange).toBeCalled(); + expect(mockData.onCollapseColumnsChange).not.toBeCalled(); + + const startIndexOfColumnSection = mockData.metricSlice.length + 3; + rerender( + , + ); + fireEvent.click(getByRole('button')); + expect(mockData.onCollapseColumnsChange).toBeCalled(); + + rerender( + , + ); + expect( + queryByText( + `Showing ${mockData.metricSlice.length} of ${mockData.totalMetrics}`, + ), + ).not.toBeInTheDocument(); + + rerender( + , + ); + expect(queryByText('Columns')).toBeInTheDocument(); +}); + +test('shows ineligible items count', () => { + const hiddenColumnCount = 3; + const hiddenMetricCount = 1; + const dataWithHiddenItems = { + ...mockData, + hiddenColumnCount, + hiddenMetricCount, + }; + const { getByText, rerender } = render( + , + { useDnd: true }, + ); + expect( + getByText(`${hiddenMetricCount} ineligible item(s) are hidden`), + ).toBeInTheDocument(); + + const startIndexOfColumnSection = mockData.metricSlice.length + 3; + rerender( + , + ); + expect( + getByText(`${hiddenColumnCount} ineligible item(s) are hidden`), + ).toBeInTheDocument(); +}); diff --git a/superset-frontend/src/explore/components/DatasourcePanel/DatasourcePanelItem.tsx b/superset-frontend/src/explore/components/DatasourcePanel/DatasourcePanelItem.tsx new file mode 100644 index 000000000000..85fd8dc3dc94 --- /dev/null +++ b/superset-frontend/src/explore/components/DatasourcePanel/DatasourcePanelItem.tsx @@ -0,0 +1,269 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React, { CSSProperties } from 'react'; +import { css, Metric, styled, t, useTheme } from '@superset-ui/core'; + +import Icons from 'src/components/Icons'; +import DatasourcePanelDragOption from './DatasourcePanelDragOption'; +import { DndItemType } from '../DndItemType'; +import { DndItemValue } from './types'; + +export type DataSourcePanelColumn = { + is_dttm?: boolean | null; + description?: string | null; + expression?: string | null; + is_certified?: number | null; + column_name?: string | null; + name?: string | null; + type?: string; +}; + +type Props = { + index: number; + style: CSSProperties; + data: { + metricSlice: Metric[]; + columnSlice: DataSourcePanelColumn[]; + totalMetrics: number; + totalColumns: number; + width: number; + showAllMetrics: boolean; + onShowAllMetricsChange: (showAll: boolean) => void; + showAllColumns: boolean; + onShowAllColumnsChange: (showAll: boolean) => void; + collapseMetrics: boolean; + onCollapseMetricsChange: (collapse: boolean) => void; + collapseColumns: boolean; + onCollapseColumnsChange: (collapse: boolean) => void; + hiddenMetricCount: number; + hiddenColumnCount: number; + }; +}; + +export const DEFAULT_MAX_COLUMNS_LENGTH = 50; +export const DEFAULT_MAX_METRICS_LENGTH = 50; +export const ITEM_HEIGHT = 30; + +const Button = styled.button` + background: none; + border: none; + text-decoration: underline; + color: ${({ theme }) => theme.colors.primary.dark1}; +`; + +const ButtonContainer = styled.div` + text-align: center; + padding-top: 2px; +`; + +const LabelWrapper = styled.div` + ${({ theme }) => css` + overflow: hidden; + text-overflow: ellipsis; + font-size: ${theme.typography.sizes.s}px; + background-color: ${theme.colors.grayscale.light4}; + margin: ${theme.gridUnit * 2}px 0; + border-radius: 4px; + padding: 0 ${theme.gridUnit}px; + + &:first-of-type { + margin-top: 0; + } + &:last-of-type { + margin-bottom: 0; + } + + padding: 0; + cursor: pointer; + &:hover { + background-color: ${theme.colors.grayscale.light3}; + } + + & > span { + white-space: nowrap; + } + + .option-label { + display: inline; + } + + .metric-option { + & > svg { + min-width: ${theme.gridUnit * 4}px; + } + & > .option-label { + overflow: hidden; + text-overflow: ellipsis; + } + } + `} +`; + +const SectionHeaderButton = styled.button` + display: flex; + justify-content: space-between; + align-items: center; + border: none; + background: transparent; + width: 100%; + padding-inline: 0px; +`; + +const SectionHeader = styled.span` + ${({ theme }) => ` + font-size: ${theme.typography.sizes.m}px; + line-height: 1.3; + `} +`; + +const Box = styled.div` + ${({ theme }) => ` + border: 1px ${theme.colors.grayscale.light4} solid; + border-radius: ${theme.gridUnit}px; + font-size: ${theme.typography.sizes.s}px; + padding: ${theme.gridUnit}px; + color: ${theme.colors.grayscale.light1}; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + `} +`; + +const DatasourcePanelItem: React.FC = ({ index, style, data }) => { + const { + metricSlice: _metricSlice, + columnSlice, + totalMetrics, + totalColumns, + width, + showAllMetrics, + onShowAllMetricsChange, + showAllColumns, + onShowAllColumnsChange, + collapseMetrics, + onCollapseMetricsChange, + collapseColumns, + onCollapseColumnsChange, + hiddenMetricCount, + hiddenColumnCount, + } = data; + const metricSlice = collapseMetrics ? [] : _metricSlice; + + const EXTRA_LINES = collapseMetrics ? 1 : 2; + const isColumnSection = collapseMetrics + ? index >= 1 + : index > metricSlice.length + EXTRA_LINES; + const HEADER_LINE = isColumnSection + ? metricSlice.length + EXTRA_LINES + 1 + : 0; + const SUBTITLE_LINE = HEADER_LINE + 1; + const BOTTOM_LINE = + (isColumnSection ? columnSlice.length : metricSlice.length) + + (collapseMetrics ? HEADER_LINE : SUBTITLE_LINE) + + 1; + const collapsed = isColumnSection ? collapseColumns : collapseMetrics; + const setCollapse = isColumnSection + ? onCollapseColumnsChange + : onCollapseMetricsChange; + const showAll = isColumnSection ? showAllColumns : showAllMetrics; + const setShowAll = isColumnSection + ? onShowAllColumnsChange + : onShowAllMetricsChange; + const theme = useTheme(); + const hiddenCount = isColumnSection ? hiddenColumnCount : hiddenMetricCount; + + return ( +
+ {index === HEADER_LINE && ( + setCollapse(!collapsed)}> + + {isColumnSection ? t('Columns') : t('Metrics')} + + {collapsed ? ( + + ) : ( + + )} + + )} + {index === SUBTITLE_LINE && !collapsed && ( +
+
+ {isColumnSection + ? t(`Showing %s of %s`, columnSlice?.length, totalColumns) + : t(`Showing %s of %s`, metricSlice?.length, totalMetrics)} +
+ {hiddenCount > 0 && ( + {t(`%s ineligible item(s) are hidden`, hiddenCount)} + )} +
+ )} + {index > SUBTITLE_LINE && index < BOTTOM_LINE && ( + + + + )} + {index === BOTTOM_LINE && + !collapsed && + (isColumnSection + ? totalColumns > DEFAULT_MAX_COLUMNS_LENGTH + : totalMetrics > DEFAULT_MAX_METRICS_LENGTH) && ( + + + + )} +
+ ); +}; + +export default DatasourcePanelItem; diff --git a/superset-frontend/src/explore/components/DatasourcePanel/index.tsx b/superset-frontend/src/explore/components/DatasourcePanel/index.tsx index 99f6b48b8927..c82f27d01127 100644 --- a/superset-frontend/src/explore/components/DatasourcePanel/index.tsx +++ b/superset-frontend/src/explore/components/DatasourcePanel/index.tsx @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import React, { useEffect, useMemo, useRef, useState } from 'react'; +import React, { useContext, useMemo, useState } from 'react'; import { css, DatasourceType, @@ -27,10 +27,11 @@ import { } from '@superset-ui/core'; import { ControlConfig } from '@superset-ui/chart-controls'; +import AutoSizer from 'react-virtualized-auto-sizer'; +import { FixedSizeList as List } from 'react-window'; -import { debounce, isArray } from 'lodash'; +import { isArray } from 'lodash'; import { matchSorter, rankings } from 'match-sorter'; -import Collapse from 'src/components/Collapse'; import Alert from 'src/components/Alert'; import { SaveDatasetModal } from 'src/SqlLab/components/SaveDatasetModal'; import { getDatasourceAsSaveableDataset } from 'src/utils/datasourceUtils'; @@ -38,23 +39,20 @@ import { Input } from 'src/components/Input'; import { FAST_DEBOUNCE } from 'src/constants'; import { ExploreActions } from 'src/explore/actions/exploreActions'; import Control from 'src/explore/components/Control'; -import DatasourcePanelDragOption from './DatasourcePanelDragOption'; +import { useDebounceValue } from 'src/hooks/useDebounceValue'; +import DatasourcePanelItem, { + ITEM_HEIGHT, + DataSourcePanelColumn, + DEFAULT_MAX_COLUMNS_LENGTH, + DEFAULT_MAX_METRICS_LENGTH, +} from './DatasourcePanelItem'; import { DndItemType } from '../DndItemType'; import { DndItemValue } from './types'; +import { DropzoneContext } from '../ExploreContainer'; interface DatasourceControl extends ControlConfig { datasource?: IDatasource; } - -export interface DataSourcePanelColumn { - is_dttm?: boolean | null; - description?: string | null; - expression?: string | null; - is_certified?: number | null; - column_name?: string | null; - name?: string | null; - type?: string; -} export interface IDatasource { metrics: Metric[]; columns: DataSourcePanelColumn[]; @@ -76,22 +74,10 @@ export interface Props { }; actions: Partial & Pick; // we use this props control force update when this panel resize - shouldForceUpdate?: number; + width: number; formData?: QueryFormData; } -const Button = styled.button` - background: none; - border: none; - text-decoration: underline; - color: ${({ theme }) => theme.colors.primary.dark1}; -`; - -const ButtonContainer = styled.div` - text-align: center; - padding-top: 2px; -`; - const DatasourceContainer = styled.div` ${({ theme }) => css` background-color: ${theme.colors.grayscale.light5}; @@ -104,8 +90,9 @@ const DatasourceContainer = styled.div` height: auto; } .field-selections { - padding: 0 0 ${4 * theme.gridUnit}px; + padding: 0 0 ${theme.gridUnit}px; overflow: auto; + height: 100%; } .field-length { margin-bottom: ${theme.gridUnit * 2}px; @@ -127,56 +114,6 @@ const DatasourceContainer = styled.div` `}; `; -const LabelWrapper = styled.div` - ${({ theme }) => css` - overflow: hidden; - text-overflow: ellipsis; - font-size: ${theme.typography.sizes.s}px; - background-color: ${theme.colors.grayscale.light4}; - margin: ${theme.gridUnit * 2}px 0; - border-radius: 4px; - padding: 0 ${theme.gridUnit}px; - - &:first-of-type { - margin-top: 0; - } - &:last-of-type { - margin-bottom: 0; - } - - padding: 0; - cursor: pointer; - &:hover { - background-color: ${theme.colors.grayscale.light3}; - } - - & > span { - white-space: nowrap; - } - - .option-label { - display: inline; - } - - .metric-option { - & > svg { - min-width: ${theme.gridUnit * 4}px; - } - & > .option-label { - overflow: hidden; - text-overflow: ellipsis; - } - } - `} -`; - -const SectionHeader = styled.span` - ${({ theme }) => ` - font-size: ${theme.typography.sizes.m}px; - line-height: 1.3; - `} -`; - const StyledInfoboxWrapper = styled.div` ${({ theme }) => css` margin: 0 ${theme.gridUnit * 2.5}px; @@ -187,33 +124,38 @@ const StyledInfoboxWrapper = styled.div` `} `; -const LabelContainer = (props: { - children: React.ReactElement; - className: string; -}) => { - const labelRef = useRef(null); - const extendedProps = { - labelRef, - }; - return ( - - {React.cloneElement(props.children, extendedProps)} - - ); -}; +const BORDER_WIDTH = 2; + +const sortCertifiedFirst = (slice: DataSourcePanelColumn[]) => + slice.sort((a, b) => (b?.is_certified ?? 0) - (a?.is_certified ?? 0)); export default function DataSourcePanel({ datasource, formData, controls: { datasource: datasourceControl }, actions, - shouldForceUpdate, + width, }: Props) { + const [dropzones] = useContext(DropzoneContext); const { columns: _columns, metrics } = datasource; + + const allowedColumns = useMemo(() => { + const validators = Object.values(dropzones); + if (!isArray(_columns)) return []; + return _columns.filter(column => + validators.some(validator => + validator({ + value: column as DndItemValue, + type: DndItemType.Column, + }), + ), + ); + }, [dropzones, _columns]); + // display temporal column first const columns = useMemo( () => - [...(isArray(_columns) ? _columns : [])].sort((col1, col2) => { + [...allowedColumns].sort((col1, col2) => { if (col1?.is_dttm && !col2?.is_dttm) { return -1; } @@ -222,107 +164,102 @@ export default function DataSourcePanel({ } return 0; }), - [_columns], + [allowedColumns], ); + const allowedMetrics = useMemo(() => { + const validators = Object.values(dropzones); + return metrics.filter(metric => + validators.some(validator => + validator({ value: metric, type: DndItemType.Metric }), + ), + ); + }, [dropzones, metrics]); + + const hiddenColumnCount = _columns.length - allowedColumns.length; + const hiddenMetricCount = metrics.length - allowedMetrics.length; const [showSaveDatasetModal, setShowSaveDatasetModal] = useState(false); const [inputValue, setInputValue] = useState(''); - const [lists, setList] = useState({ - columns, - metrics, - }); const [showAllMetrics, setShowAllMetrics] = useState(false); const [showAllColumns, setShowAllColumns] = useState(false); + const [collapseMetrics, setCollapseMetrics] = useState(false); + const [collapseColumns, setCollapseColumns] = useState(false); + const searchKeyword = useDebounceValue(inputValue, FAST_DEBOUNCE); - const DEFAULT_MAX_COLUMNS_LENGTH = 50; - const DEFAULT_MAX_METRICS_LENGTH = 50; - - const search = useMemo( - () => - debounce((value: string) => { - if (value === '') { - setList({ columns, metrics }); - return; - } - setList({ - columns: matchSorter(columns, value, { - keys: [ - { - key: 'verbose_name', - threshold: rankings.CONTAINS, - }, - { - key: 'column_name', - threshold: rankings.CONTAINS, - }, - { - key: item => - [item?.description ?? '', item?.expression ?? ''].map( - x => x?.replace(/[_\n\s]+/g, ' ') || '', - ), - threshold: rankings.CONTAINS, - maxRanking: rankings.CONTAINS, - }, - ], - keepDiacritics: true, - }), - metrics: matchSorter(metrics, value, { - keys: [ - { - key: 'verbose_name', - threshold: rankings.CONTAINS, - }, - { - key: 'metric_name', - threshold: rankings.CONTAINS, - }, - { - key: item => - [item?.description ?? '', item?.expression ?? ''].map( - x => x?.replace(/[_\n\s]+/g, ' ') || '', - ), - threshold: rankings.CONTAINS, - maxRanking: rankings.CONTAINS, - }, - ], - keepDiacritics: true, - baseSort: (a, b) => - Number(b?.item?.is_certified ?? 0) - - Number(a?.item?.is_certified ?? 0) || - String(a?.rankedValue ?? '').localeCompare(b?.rankedValue ?? ''), - }), - }); - }, FAST_DEBOUNCE), - [columns, metrics], - ); - - useEffect(() => { - setList({ - columns, - metrics, + const filteredColumns = useMemo(() => { + if (!searchKeyword) { + return columns ?? []; + } + return matchSorter(columns, searchKeyword, { + keys: [ + { + key: 'verbose_name', + threshold: rankings.CONTAINS, + }, + { + key: 'column_name', + threshold: rankings.CONTAINS, + }, + { + key: item => + [item?.description ?? '', item?.expression ?? ''].map( + x => x?.replace(/[_\n\s]+/g, ' ') || '', + ), + threshold: rankings.CONTAINS, + maxRanking: rankings.CONTAINS, + }, + ], + keepDiacritics: true, }); - setInputValue(''); - }, [columns, datasource, metrics]); + }, [columns, searchKeyword]); - const sortCertifiedFirst = (slice: DataSourcePanelColumn[]) => - slice.sort((a, b) => (b?.is_certified ?? 0) - (a?.is_certified ?? 0)); + const filteredMetrics = useMemo(() => { + if (!searchKeyword) { + return allowedMetrics ?? []; + } + return matchSorter(allowedMetrics, searchKeyword, { + keys: [ + { + key: 'verbose_name', + threshold: rankings.CONTAINS, + }, + { + key: 'metric_name', + threshold: rankings.CONTAINS, + }, + { + key: item => + [item?.description ?? '', item?.expression ?? ''].map( + x => x?.replace(/[_\n\s]+/g, ' ') || '', + ), + threshold: rankings.CONTAINS, + maxRanking: rankings.CONTAINS, + }, + ], + keepDiacritics: true, + baseSort: (a, b) => + Number(b?.item?.is_certified ?? 0) - + Number(a?.item?.is_certified ?? 0) || + String(a?.rankedValue ?? '').localeCompare(b?.rankedValue ?? ''), + }); + }, [allowedMetrics, searchKeyword]); const metricSlice = useMemo( () => showAllMetrics - ? lists?.metrics - : lists?.metrics?.slice?.(0, DEFAULT_MAX_METRICS_LENGTH), - [lists?.metrics, showAllMetrics], + ? filteredMetrics + : filteredMetrics?.slice?.(0, DEFAULT_MAX_METRICS_LENGTH), + [filteredMetrics, showAllMetrics], ); const columnSlice = useMemo( () => showAllColumns - ? sortCertifiedFirst(lists?.columns) + ? sortCertifiedFirst(filteredColumns) : sortCertifiedFirst( - lists?.columns?.slice?.(0, DEFAULT_MAX_COLUMNS_LENGTH), + filteredColumns?.slice?.(0, DEFAULT_MAX_COLUMNS_LENGTH), ), - [lists.columns, showAllColumns], + [filteredColumns, showAllColumns], ); const showInfoboxCheck = () => { @@ -349,13 +286,12 @@ export default function DataSourcePanel({ allowClear onChange={evt => { setInputValue(evt.target.value); - search(evt.target.value); }} value={inputValue} className="form-control input-md" placeholder={t('Search Metrics & Columns')} /> -
+
{datasourceIsSaveable && showInfoboxCheck() && ( )} - - {metrics?.length && ( - {t('Metrics')}} - key="metrics" + + {({ height }) => ( + -
- {t( - `Showing %s of %s`, - metricSlice?.length, - lists?.metrics.length, - )} -
- {metricSlice?.map?.((m: Metric) => ( - - - - ))} - {lists?.metrics?.length > DEFAULT_MAX_METRICS_LENGTH ? ( - - - - ) : ( - <> - )} -
+ {DatasourcePanelItem} + )} - {t('Columns')}} - key="column" - > -
- {t( - `Showing %s of %s`, - columnSlice.length, - lists.columns.length, - )} -
- {columnSlice.map(col => ( - - - - ))} - {lists.columns.length > DEFAULT_MAX_COLUMNS_LENGTH ? ( - - - - ) : ( - <> - )} -
-
+
), @@ -464,14 +364,15 @@ export default function DataSourcePanel({ [ columnSlice, inputValue, - lists.columns.length, - lists?.metrics?.length, + filteredColumns.length, + filteredMetrics.length, metricSlice, - search, showAllColumns, showAllMetrics, + collapseMetrics, + collapseColumns, datasourceIsSaveable, - shouldForceUpdate, + width, ], ); diff --git a/superset-frontend/src/explore/components/ExploreContainer/ExploreContainer.test.tsx b/superset-frontend/src/explore/components/ExploreContainer/ExploreContainer.test.tsx new file mode 100644 index 000000000000..bf9e66adc1d5 --- /dev/null +++ b/superset-frontend/src/explore/components/ExploreContainer/ExploreContainer.test.tsx @@ -0,0 +1,132 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React from 'react'; +import { fireEvent, render } from 'spec/helpers/testing-library'; +import { OptionControlLabel } from 'src/explore/components/controls/OptionControls'; + +import ExploreContainer, { DraggingContext, DropzoneContext } from '.'; +import OptionWrapper from '../controls/DndColumnSelectControl/OptionWrapper'; +import DatasourcePanelDragOption from '../DatasourcePanel/DatasourcePanelDragOption'; +import { DndItemType } from '../DndItemType'; + +const MockChildren = () => { + const dragging = React.useContext(DraggingContext); + return ( +
+ {dragging ? 'dragging' : 'not dragging'} +
+ ); +}; + +const MockChildren2 = () => { + const [zones, dispatch] = React.useContext(DropzoneContext); + return ( + <> +
{Object.keys(zones).join(':')}
+ + + + ); +}; + +test('should render children', () => { + const { getByTestId, getByText } = render( + + + , + { useRedux: true, useDnd: true }, + ); + expect(getByTestId('mock-children')).toBeInTheDocument(); + expect(getByText('not dragging')).toBeInTheDocument(); +}); + +test('should only propagate dragging state when dragging the panel option', () => { + const defaultProps = { + label: Test label, + tooltipTitle: 'This is a tooltip title', + onRemove: jest.fn(), + onMoveLabel: jest.fn(), + onDropLabel: jest.fn(), + type: 'test', + index: 0, + }; + const { container, getByText } = render( + + + Metric item} + /> + {}} + onShiftOptions={() => {}} + /> + + , + { + useRedux: true, + useDnd: true, + }, + ); + expect(container.getElementsByClassName('dragging')).toHaveLength(0); + fireEvent.dragStart(getByText('panel option')); + expect(container.getElementsByClassName('dragging')).toHaveLength(1); + fireEvent.dragEnd(getByText('panel option')); + fireEvent.dragStart(getByText('Metric item')); + expect(container.getElementsByClassName('dragging')).toHaveLength(0); + fireEvent.dragEnd(getByText('Metric item')); + expect(container.getElementsByClassName('dragging')).toHaveLength(0); + // don't show dragging state for the sorting item + fireEvent.dragStart(getByText('Column item')); + expect(container.getElementsByClassName('dragging')).toHaveLength(0); +}); + +test('should manage the dropValidators', () => { + const { queryByText, getByText } = render( + + + , + { + useRedux: true, + useDnd: true, + }, + ); + + expect(queryByText('test_item_1')).not.toBeInTheDocument(); + const addDropValidatorButton = getByText('Add'); + fireEvent.click(addDropValidatorButton); + expect(getByText('test_item_1')).toBeInTheDocument(); + const removeDropValidatorButton = getByText('Remove'); + fireEvent.click(removeDropValidatorButton); + expect(queryByText('test_item_1')).not.toBeInTheDocument(); +}); diff --git a/superset-frontend/src/explore/components/ExploreContainer/index.tsx b/superset-frontend/src/explore/components/ExploreContainer/index.tsx new file mode 100644 index 000000000000..6f4bb7a37057 --- /dev/null +++ b/superset-frontend/src/explore/components/ExploreContainer/index.tsx @@ -0,0 +1,88 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React, { useEffect, Dispatch, useReducer } from 'react'; +import { styled } from '@superset-ui/core'; +import { useDragDropManager } from 'react-dnd'; +import { DatasourcePanelDndItem } from '../DatasourcePanel/types'; + +type CanDropValidator = (item: DatasourcePanelDndItem) => boolean; +type DropzoneSet = Record; +type Action = { key: string; canDrop?: CanDropValidator }; + +export const DraggingContext = React.createContext(false); +export const DropzoneContext = React.createContext< + [DropzoneSet, Dispatch] +>([{}, () => {}]); +const StyledDiv = styled.div` + display: flex; + flex-direction: column; + height: 100%; + min-height: 0; +`; + +const reducer = (state: DropzoneSet = {}, action: Action) => { + if (action.canDrop) { + return { + ...state, + [action.key]: action.canDrop, + }; + } + if (action.key) { + const newState = { ...state }; + delete newState[action.key]; + return newState; + } + return state; +}; + +const ExploreContainer: React.FC<{}> = ({ children }) => { + const dragDropManager = useDragDropManager(); + const [dragging, setDragging] = React.useState( + dragDropManager.getMonitor().isDragging(), + ); + + useEffect(() => { + const monitor = dragDropManager.getMonitor(); + const unsub = monitor.subscribeToStateChange(() => { + const item = monitor.getItem() || {}; + // don't show dragging state for the sorting item + if ('dragIndex' in item) { + return; + } + const isDragging = monitor.isDragging(); + setDragging(isDragging); + }); + + return () => { + unsub(); + }; + }, [dragDropManager]); + + const dropzoneValue = useReducer(reducer, {}); + + return ( + + + {children} + + + ); +}; + +export default ExploreContainer; diff --git a/superset-frontend/src/explore/components/ExploreViewContainer/ExploreViewContainer.test.tsx b/superset-frontend/src/explore/components/ExploreViewContainer/ExploreViewContainer.test.tsx index c58f8e04f515..6b6577110d4b 100644 --- a/superset-frontend/src/explore/components/ExploreViewContainer/ExploreViewContainer.test.tsx +++ b/superset-frontend/src/explore/components/ExploreViewContainer/ExploreViewContainer.test.tsx @@ -23,6 +23,7 @@ import { getChartMetadataRegistry, ChartMetadata, } from '@superset-ui/core'; +import { QUERY_MODE_REQUISITES } from 'src/explore/constants'; import { MemoryRouter, Route } from 'react-router-dom'; import { render, screen, waitFor } from 'spec/helpers/testing-library'; import userEvent from '@testing-library/user-event'; @@ -222,3 +223,75 @@ test('preserves unknown parameters', async () => { ); replaceState.mockRestore(); }); + +test('retains query mode requirements when query_mode is enabled', async () => { + const customState = { + ...reduxState, + explore: { + ...reduxState.explore, + controls: { + ...reduxState.explore.controls, + query_mode: { value: 'raw' }, + optional_key1: { value: 'value1' }, + all_columns: { value: ['all_columns'] }, + groupby: { value: ['groupby'] }, + }, + hiddenFormData: { + all_columns: ['all_columns'], + groupby: ['groupby'], + optional_key1: 'value1', + }, + }, + }; + + await waitFor(() => renderWithRouter({ initialState: customState })); + + const formDataEndpointCalls = fetchMock.calls(/api\/v1\/explore\/form_data/); + expect(formDataEndpointCalls.length).toBeGreaterThan(0); + const lastCall = formDataEndpointCalls[formDataEndpointCalls.length - 1]; + + const body = JSON.parse(lastCall[1]?.body as string); + const formData = JSON.parse(body.form_data); + + const queryModeFields = Object.keys( + customState.explore.hiddenFormData, + ).filter(key => QUERY_MODE_REQUISITES.has(key)); + + queryModeFields.forEach(key => { + expect(formData[key]).toBeDefined(); + }); + expect(formData.optional_key1).toBeUndefined(); +}); + +test('does omit hiddenFormData when query_mode is not enabled', async () => { + const customState = { + ...reduxState, + explore: { + ...reduxState.explore, + controls: { + ...reduxState.explore.controls, + optional_key1: { value: 'value1' }, + all_columns: { value: ['all_columns'] }, + groupby: { value: ['groupby'] }, + }, + hiddenFormData: { + all_columns: ['all_columns'], + groupby: ['groupby'], + optional_key1: 'value1', + }, + }, + }; + + await waitFor(() => renderWithRouter({ initialState: customState })); + + const formDataEndpointCalls = fetchMock.calls(/api\/v1\/explore\/form_data/); + expect(formDataEndpointCalls.length).toBeGreaterThan(0); + const lastCall = formDataEndpointCalls[formDataEndpointCalls.length - 1]; + + const body = JSON.parse(lastCall[1]?.body as string); + const formData = JSON.parse(body.form_data); + + Object.keys(customState.explore.hiddenFormData).forEach(key => { + expect(formData[key]).toBeUndefined(); + }); +}); diff --git a/superset-frontend/src/explore/components/ExploreViewContainer/index.jsx b/superset-frontend/src/explore/components/ExploreViewContainer/index.jsx index d3a32a2bd64a..626325c53adf 100644 --- a/superset-frontend/src/explore/components/ExploreViewContainer/index.jsx +++ b/superset-frontend/src/explore/components/ExploreViewContainer/index.jsx @@ -31,7 +31,7 @@ import { useComponentDidMount, usePrevious, } from '@superset-ui/core'; -import { debounce, pick } from 'lodash'; +import { debounce, omit, pick } from 'lodash'; import { Resizable } from 're-resizable'; import { usePluginContext } from 'src/components/DynamicPlugins'; import { Global } from '@emotion/react'; @@ -43,6 +43,7 @@ import { LocalStorageKeys, } from 'src/utils/localStorageHelpers'; import { RESERVED_CHART_URL_PARAMS, URL_PARAMS } from 'src/constants'; +import { QUERY_MODE_REQUISITES } from 'src/explore/constants'; import { areObjectsEqual } from 'src/reduxUtils'; import * as logActions from 'src/logger/actions'; import { @@ -68,6 +69,7 @@ import ConnectedControlPanelsContainer from '../ControlPanelsContainer'; import SaveModal from '../SaveModal'; import DataSourcePanel from '../DatasourcePanel'; import ConnectedExploreChartHeader from '../ExploreChartHeader'; +import ExploreContainer from '../ExploreContainer'; const propTypes = { ...ExploreChartPanel.propTypes, @@ -90,13 +92,6 @@ const propTypes = { isSaveModalVisible: PropTypes.bool, }; -const ExploreContainer = styled.div` - display: flex; - flex-direction: column; - height: 100%; - min-height: 0; -`; - const ExplorePanelContainer = styled.div` ${({ theme }) => css` background: ${theme.colors.grayscale.light5}; @@ -235,6 +230,20 @@ const updateHistory = debounce( 1000, ); +const defaultSidebarsWidth = { + controls_width: 320, + datasource_width: 300, +}; + +function getSidebarWidths(key) { + return getItem(key, defaultSidebarsWidth[key]); +} + +function setSidebarWidths(key, dimension) { + const newDimension = Number(getSidebarWidths(key)) + dimension.width; + setItem(key, newDimension); +} + function ExploreViewContainer(props) { const dynamicPluginContext = usePluginContext(); const dynamicPlugin = dynamicPluginContext.dynamicPlugins[props.vizType]; @@ -249,16 +258,13 @@ function ExploreViewContainer(props) { ); const [isCollapsed, setIsCollapsed] = useState(false); - const [shouldForceUpdate, setShouldForceUpdate] = useState(-1); + const [width, setWidth] = useState( + getSidebarWidths(LocalStorageKeys.DatasourceWidth), + ); const tabId = useTabId(); const theme = useTheme(); - const defaultSidebarsWidth = { - controls_width: 320, - datasource_width: 300, - }; - const addHistory = useCallback( async ({ isReplace = false, title } = {}) => { const formData = props.dashboardId @@ -540,15 +546,6 @@ function ExploreViewContainer(props) { ); } - function getSidebarWidths(key) { - return getItem(key, defaultSidebarsWidth[key]); - } - - function setSidebarWidths(key, dimension) { - const newDimension = Number(getSidebarWidths(key)) + dimension.width; - setItem(key, newDimension); - } - if (props.standalone) { return renderChartContainer(); } @@ -599,7 +596,7 @@ function ExploreViewContainer(props) { /> { - setShouldForceUpdate(d?.width); + setWidth(ref.getBoundingClientRect().width); setSidebarWidths(LocalStorageKeys.DatasourceWidth, d); }} defaultSize={{ @@ -633,7 +630,7 @@ function ExploreViewContainer(props) { datasource={props.datasource} controls={props.controls} actions={props.actions} - shouldForceUpdate={shouldForceUpdate} + width={width} user={props.user} /> @@ -708,6 +705,11 @@ function ExploreViewContainer(props) { ExploreViewContainer.propTypes = propTypes; +const retainQueryModeRequirements = hiddenFormData => + Object.keys(hiddenFormData ?? {}).filter( + key => !QUERY_MODE_REQUISITES.has(key), + ); + function mapStateToProps(state) { const { explore, @@ -719,8 +721,12 @@ function mapStateToProps(state) { user, saveModal, } = state; - const { controls, slice, datasource, metadata } = explore; - const form_data = getFormDataFromControls(controls); + const { controls, slice, datasource, metadata, hiddenFormData } = explore; + const hasQueryMode = !!controls.query_mode?.value; + const fieldsToOmit = hasQueryMode + ? retainQueryModeRequirements(hiddenFormData) + : Object.keys(hiddenFormData ?? {}); + const form_data = omit(getFormDataFromControls(controls), fieldsToOmit); const slice_id = form_data.slice_id ?? slice?.slice_id ?? 0; // 0 - unsaved chart form_data.extra_form_data = mergeExtraFormData( { ...form_data.extra_form_data }, diff --git a/superset-frontend/src/explore/components/RowCountLabel/RowCountLabel.test.tsx b/superset-frontend/src/explore/components/RowCountLabel/RowCountLabel.test.tsx index 2e7a44c15e15..8e3e3b0c1d7b 100644 --- a/superset-frontend/src/explore/components/RowCountLabel/RowCountLabel.test.tsx +++ b/superset-frontend/src/explore/components/RowCountLabel/RowCountLabel.test.tsx @@ -52,7 +52,7 @@ test('RowCountLabel renders limit with danger and tooltip', async () => { expect(screen.getByText(expectedText)).toBeInTheDocument(); userEvent.hover(screen.getByText(expectedText)); const tooltip = await screen.findByRole('tooltip'); - expect(tooltip).toHaveTextContent('Limit reached'); + expect(tooltip).toHaveTextContent('The row limit'); expect(tooltip).toHaveStyle('background: rgba(0, 0, 0, 0.902);'); }); diff --git a/superset-frontend/src/explore/components/RowCountLabel/index.tsx b/superset-frontend/src/explore/components/RowCountLabel/index.tsx index be41be0ba67d..80fc36295946 100644 --- a/superset-frontend/src/explore/components/RowCountLabel/index.tsx +++ b/superset-frontend/src/explore/components/RowCountLabel/index.tsx @@ -28,9 +28,13 @@ type RowCountLabelProps = { loading?: boolean; }; +const limitReachedMsg = t( + 'The row limit set for the chart was reached. The chart may show partial data.', +); + export default function RowCountLabel(props: RowCountLabelProps) { - const { rowcount = 0, limit, loading } = props; - const limitReached = rowcount === limit; + const { rowcount = 0, limit = null, loading } = props; + const limitReached = limit && rowcount >= limit; const type = limitReached || (rowcount === 0 && !loading) ? 'danger' : 'default'; const formattedRowCount = getNumberFormatter()(rowcount); @@ -46,7 +50,7 @@ export default function RowCountLabel(props: RowCountLabelProps) { ); return limitReached ? ( - {t('Limit reached')}}> + {limitReachedMsg}}> {label} ) : ( diff --git a/superset-frontend/src/explore/components/StashFormDataContainer/StashFormDataContainer.test.tsx b/superset-frontend/src/explore/components/StashFormDataContainer/StashFormDataContainer.test.tsx new file mode 100644 index 000000000000..2d7358b149ec --- /dev/null +++ b/superset-frontend/src/explore/components/StashFormDataContainer/StashFormDataContainer.test.tsx @@ -0,0 +1,85 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React from 'react'; +import { defaultState } from 'src/explore/store'; +import { render, waitFor } from 'spec/helpers/testing-library'; +import { useSelector } from 'react-redux'; +import { ExplorePageState } from 'src/explore/types'; +import StashFormDataContainer from '.'; + +const FormDataMock = () => { + const formData = useSelector( + (state: ExplorePageState) => state.explore.form_data, + ); + + return
{Object.keys(formData).join(':')}
; +}; + +test('should stash form data from fieldNames', () => { + const { rerender, container } = render( + + + , + { + useRedux: true, + initialState: { explore: { form_data: defaultState.form_data } }, + }, + ); + expect(container.querySelector('div')).toHaveTextContent('granularity_sqla'); + + rerender( + + + , + ); + expect(container.querySelector('div')).not.toHaveTextContent( + 'granularity_sqla', + ); +}); + +test('should restore form data from fieldNames', async () => { + const { granularity_sqla, ...formData } = defaultState.form_data; + const { container } = render( + + + , + { + useRedux: true, + initialState: { + explore: { + form_data: formData, + hiddenFormData: { + granularity_sqla, + }, + }, + }, + }, + ); + await waitFor(() => + expect(container.querySelector('div')).toHaveTextContent( + 'granularity_sqla', + ), + ); +}); diff --git a/superset-frontend/src/explore/components/StashFormDataContainer/index.tsx b/superset-frontend/src/explore/components/StashFormDataContainer/index.tsx new file mode 100644 index 000000000000..114eaa68bd08 --- /dev/null +++ b/superset-frontend/src/explore/components/StashFormDataContainer/index.tsx @@ -0,0 +1,45 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React, { useEffect } from 'react'; +import { useDispatch } from 'react-redux'; +import { setStashFormData } from 'src/explore/actions/exploreActions'; +import useEffectEvent from 'src/hooks/useEffectEvent'; + +type Props = { + shouldStash: boolean; + fieldNames: ReadonlyArray; +}; + +const StashFormDataContainer: React.FC = ({ + shouldStash, + fieldNames, + children, +}) => { + const dispatch = useDispatch(); + const onVisibleUpdate = useEffectEvent((shouldStash: boolean) => + dispatch(setStashFormData(shouldStash, fieldNames)), + ); + useEffect(() => { + onVisibleUpdate(shouldStash); + }, [shouldStash, onVisibleUpdate]); + + return <>{children}; +}; + +export default StashFormDataContainer; diff --git a/superset-frontend/src/explore/components/controls/AnnotationLayerControl/AnnotationLayer.jsx b/superset-frontend/src/explore/components/controls/AnnotationLayerControl/AnnotationLayer.jsx index 563b94682385..e29befc808a2 100644 --- a/superset-frontend/src/explore/components/controls/AnnotationLayerControl/AnnotationLayer.jsx +++ b/superset-frontend/src/explore/components/controls/AnnotationLayerControl/AnnotationLayer.jsx @@ -33,12 +33,12 @@ import { withTheme, } from '@superset-ui/core'; import SelectControl from 'src/explore/components/controls/SelectControl'; +import { AsyncSelect } from 'src/components'; import TextControl from 'src/explore/components/controls/TextControl'; import CheckboxControl from 'src/explore/components/controls/CheckboxControl'; import PopoverSection from 'src/components/PopoverSection'; import ControlHeader from 'src/explore/components/ControlHeader'; import { EmptyStateSmall } from 'src/components/EmptyState'; -import { FILTER_OPTIONS_LIMIT } from 'src/explore/constants'; import { ANNOTATION_SOURCE_TYPES, ANNOTATION_TYPES, @@ -194,28 +194,46 @@ class AnnotationLayer extends React.PureComponent { hideLine, // refData isNew: !name, - isLoadingOptions: true, - valueOptions: [], + slice: null, }; this.submitAnnotation = this.submitAnnotation.bind(this); this.deleteAnnotation = this.deleteAnnotation.bind(this); this.applyAnnotation = this.applyAnnotation.bind(this); - this.fetchOptions = this.fetchOptions.bind(this); + this.isValidForm = this.isValidForm.bind(this); + // Handlers this.handleAnnotationType = this.handleAnnotationType.bind(this); this.handleAnnotationSourceType = this.handleAnnotationSourceType.bind(this); - this.handleValue = this.handleValue.bind(this); - this.isValidForm = this.isValidForm.bind(this); + this.handleSelectValue = this.handleSelectValue.bind(this); + this.handleTextValue = this.handleTextValue.bind(this); + // Fetch related functions + this.fetchOptions = this.fetchOptions.bind(this); + this.fetchCharts = this.fetchCharts.bind(this); + this.fetchNativeAnnotations = this.fetchNativeAnnotations.bind(this); + this.fetchAppliedAnnotation = this.fetchAppliedAnnotation.bind(this); + this.fetchSliceData = this.fetchSliceData.bind(this); + this.shouldFetchSliceData = this.shouldFetchSliceData.bind(this); + this.fetchAppliedChart = this.fetchAppliedChart.bind(this); + this.fetchAppliedNativeAnnotation = + this.fetchAppliedNativeAnnotation.bind(this); + this.shouldFetchAppliedAnnotation = + this.shouldFetchAppliedAnnotation.bind(this); } componentDidMount() { - const { annotationType, sourceType, isLoadingOptions } = this.state; - this.fetchOptions(annotationType, sourceType, isLoadingOptions); + if (this.shouldFetchAppliedAnnotation()) { + const { value } = this.state; + /* The value prop is the id of the chart/native. This function will set + value in state to an object with the id as value.value to be used by + AsyncSelect */ + this.fetchAppliedAnnotation(value); + } } componentDidUpdate(prevProps, prevState) { - if (prevState.sourceType !== this.state.sourceType) { - this.fetchOptions(this.state.annotationType, this.state.sourceType, true); + if (this.shouldFetchSliceData(prevState)) { + const { value } = this.state; + this.fetchSliceData(value.value); } } @@ -237,6 +255,20 @@ class AnnotationLayer extends React.PureComponent { return sources; } + shouldFetchAppliedAnnotation() { + const { value, sourceType } = this.state; + return value && requiresQuery(sourceType); + } + + shouldFetchSliceData(prevState) { + const { value, sourceType } = this.state; + const isChart = + sourceType !== ANNOTATION_SOURCE_TYPES.NATIVE && + requiresQuery(sourceType); + const valueIsNew = value && prevState.value !== value; + return valueIsNew && isChart; + } + isValidFormulaAnnotation(expression, annotationType) { if (annotationType === ANNOTATION_TYPES.FORMULA) { return isValidExpression(expression); @@ -276,6 +308,7 @@ class AnnotationLayer extends React.PureComponent { annotationType, sourceType: null, value: null, + slice: null, }); } @@ -283,13 +316,17 @@ class AnnotationLayer extends React.PureComponent { const { sourceType: prevSourceType } = this.state; if (prevSourceType !== sourceType) { - this.setState({ sourceType, value: null, isLoadingOptions: true }); + this.setState({ + sourceType, + value: null, + slice: null, + }); } } - handleValue(value) { + handleSelectValue(selectedValueObject) { this.setState({ - value, + value: selectedValueObject, descriptionColumns: [], intervalEndColumn: null, timeColumn: null, @@ -298,74 +335,172 @@ class AnnotationLayer extends React.PureComponent { }); } - fetchOptions(annotationType, sourceType, isLoadingOptions) { - if (isLoadingOptions) { - if (sourceType === ANNOTATION_SOURCE_TYPES.NATIVE) { - const queryParams = rison.encode({ - page: 0, - page_size: FILTER_OPTIONS_LIMIT, - }); - SupersetClient.get({ - endpoint: `/api/v1/annotation_layer/?q=${queryParams}`, - }).then(({ json }) => { - const layers = json - ? json.result.map(layer => ({ - value: layer.id, - label: layer.name, - })) - : []; - this.setState({ - isLoadingOptions: false, - valueOptions: layers, - }); - }); - } else if (requiresQuery(sourceType)) { - const queryParams = rison.encode({ - filters: [ - { - col: 'id', - opr: 'chart_owned_created_favored_by_me', - value: true, - }, - ], - order_column: 'slice_name', - order_direction: 'asc', - page: 0, - page_size: FILTER_OPTIONS_LIMIT, - }); - SupersetClient.get({ - endpoint: `/api/v1/chart/?q=${queryParams}`, - }).then(({ json }) => { - const registry = getChartMetadataRegistry(); - this.setState({ - isLoadingOptions: false, - valueOptions: json.result - .filter(x => { - const metadata = registry.get(x.viz_type); - return metadata && metadata.canBeAnnotationType(annotationType); - }) - .map(x => ({ - value: x.id, - label: x.slice_name, - slice: { - ...x, - data: { - ...x.form_data, - groupby: x.form_data.groupby?.map(column => - getColumnLabel(column), - ), - }, - }, - })), - }); - }); - } else { + handleTextValue(inputValue) { + this.setState({ + value: inputValue, + }); + } + + fetchNativeAnnotations = async (search, page, pageSize) => { + const queryParams = rison.encode({ + filters: [ + { + col: 'name', + opr: 'ct', + value: search, + }, + ], + columns: ['id', 'name'], + page, + page_size: pageSize, + }); + + const { json } = await SupersetClient.get({ + endpoint: `/api/v1/annotation_layer/?q=${queryParams}`, + }); + + const { result, count } = json; + + const layersArray = result.map(layer => ({ + value: layer.id, + label: layer.name, + })); + + return { + data: layersArray, + totalCount: count, + }; + }; + + fetchCharts = async (search, page, pageSize) => { + const { annotationType } = this.state; + + const queryParams = rison.encode({ + filters: [ + { col: 'slice_name', opr: 'chart_all_text', value: search }, + { + col: 'id', + opr: 'chart_owned_created_favored_by_me', + value: true, + }, + ], + columns: ['id', 'slice_name', 'viz_type'], + order_column: 'slice_name', + order_direction: 'asc', + page, + page_size: pageSize, + }); + const { json } = await SupersetClient.get({ + endpoint: `/api/v1/chart/?q=${queryParams}`, + }); + + const { result, count } = json; + const registry = getChartMetadataRegistry(); + + const chartsArray = result + .filter(chart => { + const metadata = registry.get(chart.viz_type); + return metadata && metadata.canBeAnnotationType(annotationType); + }) + .map(chart => ({ + value: chart.id, + label: chart.slice_name, + viz_type: chart.viz_type, + })); + + return { + data: chartsArray, + totalCount: count, + }; + }; + + fetchOptions = (search, page, pageSize) => { + const { sourceType } = this.state; + + if (sourceType === ANNOTATION_SOURCE_TYPES.NATIVE) { + return this.fetchNativeAnnotations(search, page, pageSize); + } + return this.fetchCharts(search, page, pageSize); + }; + + fetchSliceData = id => { + const queryParams = rison.encode({ + columns: ['query_context'], + }); + SupersetClient.get({ + endpoint: `/api/v1/chart/${id}?q=${queryParams}`, + }).then(({ json }) => { + const { result } = json; + const queryContext = result.query_context; + const formData = JSON.parse(queryContext).form_data; + const dataObject = { + data: { + ...formData, + groupby: formData.groupby?.map(column => getColumnLabel(column)), + }, + }; + this.setState({ + slice: dataObject, + }); + }); + }; + + fetchAppliedChart(id) { + const { annotationType } = this.state; + const registry = getChartMetadataRegistry(); + const queryParams = rison.encode({ + columns: ['slice_name', 'query_context', 'viz_type'], + }); + SupersetClient.get({ + endpoint: `/api/v1/chart/${id}?q=${queryParams}`, + }).then(({ json }) => { + const { result } = json; + const sliceName = result.slice_name; + const queryContext = result.query_context; + const vizType = result.viz_type; + const formData = JSON.parse(queryContext).form_data; + const metadata = registry.get(vizType); + const canBeAnnotationType = + metadata && metadata.canBeAnnotationType(annotationType); + if (canBeAnnotationType) { this.setState({ - isLoadingOptions: false, - valueOptions: [], + value: { + value: id, + label: sliceName, + }, + slice: { + data: { + ...formData, + groupby: formData.groupby?.map(column => getColumnLabel(column)), + }, + }, }); } + }); + } + + fetchAppliedNativeAnnotation(id) { + SupersetClient.get({ + endpoint: `/api/v1/annotation_layer/${id}`, + }).then(({ json }) => { + const { result } = json; + const layer = result; + this.setState({ + value: { + value: layer.id, + label: layer.name, + }, + }); + }); + } + + fetchAppliedAnnotation(id) { + const { sourceType } = this.state; + + if (sourceType === ANNOTATION_SOURCE_TYPES.NATIVE) { + return this.fetchAppliedNativeAnnotation(id); } + return this.fetchAppliedChart(id); } deleteAnnotation() { @@ -374,6 +509,7 @@ class AnnotationLayer extends React.PureComponent { } applyAnnotation() { + const { value, sourceType } = this.state; if (this.isValidForm()) { const annotationFields = [ 'name', @@ -385,7 +521,6 @@ class AnnotationLayer extends React.PureComponent { 'width', 'showMarkers', 'hideLine', - 'value', 'overrides', 'show', 'showLabel', @@ -401,6 +536,10 @@ class AnnotationLayer extends React.PureComponent { } }); + // Prepare newAnnotation.value for use in runAnnotationQuery() + const applicableValue = requiresQuery(sourceType) ? value.value : value; + newAnnotation.value = applicableValue; + if (newAnnotation.color === AUTOMATIC_COLOR) { newAnnotation.color = null; } @@ -415,29 +554,19 @@ class AnnotationLayer extends React.PureComponent { this.props.close(); } - renderOption(option) { + renderChartHeader(label, description, value) { return ( - - {option.label} - + ); } renderValueConfiguration() { - const { - annotationType, - sourceType, - value, - valueOptions, - isLoadingOptions, - } = this.state; + const { annotationType, sourceType, value } = this.state; let label = ''; let description = ''; if (requiresQuery(sourceType)) { @@ -462,20 +591,15 @@ class AnnotationLayer extends React.PureComponent { } if (requiresQuery(sourceType)) { return ( - } /> ); @@ -490,7 +614,7 @@ class AnnotationLayer extends React.PureComponent { label={label} placeholder="" value={value} - onChange={this.handleValue} + onChange={this.handleTextValue} validationErrors={ !this.isValidFormulaAnnotation(value, annotationType) ? [t('Bad formula.')] @@ -507,14 +631,18 @@ class AnnotationLayer extends React.PureComponent { annotationType, sourceType, value, - valueOptions, + slice, overrides, titleColumn, timeColumn, intervalEndColumn, descriptionColumns, } = this.state; - const { slice } = valueOptions.find(x => x.value === value) || {}; + + if (!slice || !value) { + return ''; + } + if (sourceType !== ANNOTATION_SOURCE_TYPES.NATIVE && slice) { const columns = (slice.data.groupby || []) .concat(slice.data.all_columns || []) diff --git a/superset-frontend/src/explore/components/controls/AnnotationLayerControl/AnnotationLayer.test.tsx b/superset-frontend/src/explore/components/controls/AnnotationLayerControl/AnnotationLayer.test.tsx index 914be7361919..813a8ebcb9b9 100644 --- a/superset-frontend/src/explore/components/controls/AnnotationLayerControl/AnnotationLayer.test.tsx +++ b/superset-frontend/src/explore/components/controls/AnnotationLayerControl/AnnotationLayer.test.tsx @@ -31,21 +31,37 @@ const defaultProps = { annotationType: ANNOTATION_TYPES_METADATA.FORMULA.value, }; +const nativeLayerApiRoute = 'glob:*/api/v1/annotation_layer/*'; +const chartApiRoute = /\/api\/v1\/chart\/\?q=.+/; +const chartApiWithIdRoute = /\/api\/v1\/chart\/\w+\?q=.+/; + +const withIdResult = { + result: { + slice_name: 'Mocked Slice', + query_context: JSON.stringify({ + form_data: { + groupby: ['country'], + }, + }), + viz_type: 'line', + }, +}; + beforeAll(() => { const supportedAnnotationTypes = Object.values(ANNOTATION_TYPES_METADATA).map( value => value.value, ); - fetchMock.get('glob:*/api/v1/annotation_layer/*', { - result: [{ label: 'Chart A', value: 'a' }], + fetchMock.get(nativeLayerApiRoute, { + result: [{ name: 'Chart A', id: 'a' }], }); - fetchMock.get('glob:*/api/v1/chart/*', { - result: [ - { id: 'a', slice_name: 'Chart A', viz_type: 'table', form_data: {} }, - ], + fetchMock.get(chartApiRoute, { + result: [{ id: 'a', slice_name: 'Chart A', viz_type: 'table' }], }); + fetchMock.get(chartApiWithIdRoute, withIdResult); + setupColors(); getChartMetadataRegistry().registerValue( @@ -144,7 +160,7 @@ test('triggers removeAnnotationLayer and close when remove button is clicked', a expect(close).toHaveBeenCalled(); }); -test('renders chart options', async () => { +test('fetches Superset annotation layer options', async () => { await waitForRender({ annotationType: ANNOTATION_TYPES_METADATA.EVENT.value, }); @@ -153,12 +169,37 @@ test('renders chart options', async () => { ); userEvent.click(screen.getByText('Superset annotation')); expect(await screen.findByText('Annotation layer')).toBeInTheDocument(); + userEvent.click( + screen.getByRole('combobox', { name: 'Annotation layer value' }), + ); + expect(await screen.findByText('Chart A')).toBeInTheDocument(); + expect(fetchMock.calls(nativeLayerApiRoute).length).toBe(1); +}); +test('fetches chart options', async () => { + await waitForRender({ + annotationType: ANNOTATION_TYPES_METADATA.EVENT.value, + }); userEvent.click( screen.getByRole('combobox', { name: 'Annotation source type' }), ); userEvent.click(screen.getByText('Table')); expect(await screen.findByText('Chart')).toBeInTheDocument(); + userEvent.click( + screen.getByRole('combobox', { name: 'Annotation layer value' }), + ); + expect(await screen.findByText('Chart A')).toBeInTheDocument(); + expect(fetchMock.calls(chartApiRoute).length).toBe(1); +}); + +test('fetches chart on mount if value present', async () => { + await waitForRender({ + name: 'Test', + value: 'a', + annotationType: ANNOTATION_TYPES_METADATA.EVENT.value, + sourceType: 'Table', + }); + expect(fetchMock.calls(chartApiWithIdRoute).length).toBe(1); }); test('keeps apply disabled when missing required fields', async () => { @@ -171,7 +212,7 @@ test('keeps apply disabled when missing required fields', async () => { ); expect(await screen.findByText('Chart A')).toBeInTheDocument(); userEvent.click(screen.getByText('Chart A')); - + await screen.findByText(/title column/i); userEvent.click(screen.getByRole('button', { name: 'Automatic Color' })); userEvent.click( screen.getByRole('combobox', { name: 'Annotation layer title column' }), @@ -197,47 +238,61 @@ test('keeps apply disabled when missing required fields', async () => { expect(screen.getByRole('button', { name: 'Apply' })).toBeDisabled(); }); -test.skip('Disable apply button if formula is incorrect', async () => { - // TODO: fix flaky test that passes locally but fails on CI +test('Disable apply button if formula is incorrect', async () => { await waitForRender({ name: 'test' }); - userEvent.clear(screen.getByLabelText('Formula')); - userEvent.type(screen.getByLabelText('Formula'), 'x+1'); + const formulaInput = screen.getByRole('textbox', { name: 'Formula' }); + const applyButton = screen.getByRole('button', { name: 'Apply' }); + const okButton = screen.getByRole('button', { name: 'OK' }); + + userEvent.type(formulaInput, 'x+1'); + expect(formulaInput).toHaveValue('x+1'); await waitFor(() => { - expect(screen.getByLabelText('Formula')).toHaveValue('x+1'); - expect(screen.getByRole('button', { name: 'OK' })).toBeEnabled(); - expect(screen.getByRole('button', { name: 'Apply' })).toBeEnabled(); + expect(okButton).toBeEnabled(); + expect(applyButton).toBeEnabled(); }); - userEvent.clear(screen.getByLabelText('Formula')); - userEvent.type(screen.getByLabelText('Formula'), 'y = x*2+1'); + userEvent.clear(formulaInput); await waitFor(() => { - expect(screen.getByLabelText('Formula')).toHaveValue('y = x*2+1'); - expect(screen.getByRole('button', { name: 'OK' })).toBeEnabled(); - expect(screen.getByRole('button', { name: 'Apply' })).toBeEnabled(); + expect(formulaInput).toHaveValue(''); + }); + userEvent.type(formulaInput, 'y = x*2+1'); + expect(formulaInput).toHaveValue('y = x*2+1'); + await waitFor(() => { + expect(okButton).toBeEnabled(); + expect(applyButton).toBeEnabled(); }); - userEvent.clear(screen.getByLabelText('Formula')); - userEvent.type(screen.getByLabelText('Formula'), 'y+1'); + userEvent.clear(formulaInput); + await waitFor(() => { + expect(formulaInput).toHaveValue(''); + }); + userEvent.type(formulaInput, 'y+1'); + expect(formulaInput).toHaveValue('y+1'); await waitFor(() => { - expect(screen.getByLabelText('Formula')).toHaveValue('y+1'); - expect(screen.getByRole('button', { name: 'OK' })).toBeDisabled(); - expect(screen.getByRole('button', { name: 'Apply' })).toBeDisabled(); + expect(okButton).toBeDisabled(); + expect(applyButton).toBeDisabled(); }); - userEvent.clear(screen.getByLabelText('Formula')); - userEvent.type(screen.getByLabelText('Formula'), 'x+'); + userEvent.clear(formulaInput); await waitFor(() => { - expect(screen.getByLabelText('Formula')).toHaveValue('x+'); - expect(screen.getByRole('button', { name: 'OK' })).toBeDisabled(); - expect(screen.getByRole('button', { name: 'Apply' })).toBeDisabled(); + expect(formulaInput).toHaveValue(''); + }); + userEvent.type(formulaInput, 'x+'); + expect(formulaInput).toHaveValue('x+'); + await waitFor(() => { + expect(okButton).toBeDisabled(); + expect(applyButton).toBeDisabled(); }); - userEvent.clear(screen.getByLabelText('Formula')); - userEvent.type(screen.getByLabelText('Formula'), 'y = z+1'); + userEvent.clear(formulaInput); + await waitFor(() => { + expect(formulaInput).toHaveValue(''); + }); + userEvent.type(formulaInput, 'y = z+1'); + expect(formulaInput).toHaveValue('y = z+1'); await waitFor(() => { - expect(screen.getByLabelText('Formula')).toHaveValue('y = z+1'); - expect(screen.getByRole('button', { name: 'OK' })).toBeDisabled(); - expect(screen.getByRole('button', { name: 'Apply' })).toBeDisabled(); + expect(okButton).toBeDisabled(); + expect(applyButton).toBeDisabled(); }); }); diff --git a/superset-frontend/src/explore/components/controls/DateFilterControl/components/DateLabel.tsx b/superset-frontend/src/explore/components/controls/DateFilterControl/components/DateLabel.tsx index 2c31f8030f56..27ed7704125f 100644 --- a/superset-frontend/src/explore/components/controls/DateFilterControl/components/DateLabel.tsx +++ b/superset-frontend/src/explore/components/controls/DateFilterControl/components/DateLabel.tsx @@ -18,7 +18,7 @@ */ import React, { forwardRef, ReactNode, RefObject } from 'react'; -import { css, styled, useTheme } from '@superset-ui/core'; +import { css, styled, useTheme, t } from '@superset-ui/core'; import Icons from 'src/components/Icons'; export type DateLabelProps = { @@ -88,7 +88,7 @@ export const DateLabel = forwardRef( return ( - {props.label} + {typeof props.label === 'string' ? t(props.label) : props.label} value), diff --git a/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndFilterSelect.test.tsx b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndFilterSelect.test.tsx index f47c3a010193..6be82472d9f6 100644 --- a/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndFilterSelect.test.tsx +++ b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndFilterSelect.test.tsx @@ -18,7 +18,6 @@ */ import React from 'react'; import thunk from 'redux-thunk'; -import { Provider } from 'react-redux'; import configureStore from 'redux-mock-store'; import { @@ -29,7 +28,13 @@ import { import { ColumnMeta } from '@superset-ui/chart-controls'; import { TimeseriesDefaultFormData } from '@superset-ui/plugin-chart-echarts'; -import { render, screen } from 'spec/helpers/testing-library'; +import { + fireEvent, + render, + screen, + within, +} from 'spec/helpers/testing-library'; +import type { AsyncAceEditorProps } from 'src/components/AsyncAceEditor'; import AdhocMetric from 'src/explore/components/controls/MetricControl/AdhocMetric'; import AdhocFilter from 'src/explore/components/controls/FilterControl/AdhocFilter'; import { @@ -38,13 +43,21 @@ import { } from 'src/explore/components/controls/DndColumnSelectControl/DndFilterSelect'; import { PLACEHOLDER_DATASOURCE } from 'src/dashboard/constants'; import { ExpressionTypes } from '../FilterControl/types'; +import { Datasource } from '../../../types'; +import { DndItemType } from '../../DndItemType'; +import DatasourcePanelDragOption from '../../DatasourcePanel/DatasourcePanelDragOption'; + +jest.mock('src/components/AsyncAceEditor', () => ({ + SQLEditor: (props: AsyncAceEditorProps) => ( +
{props.value}
+ ), +})); -const defaultProps: DndFilterSelectProps = { +const defaultProps: Omit = { type: 'DndFilterSelect', name: 'Filter', value: [], columns: [], - datasource: PLACEHOLDER_DATASOURCE, formData: null, savedMetrics: [], selectedMetrics: [], @@ -64,25 +77,26 @@ function setup({ value = undefined, formData = baseFormData, columns = [], + datasource = PLACEHOLDER_DATASOURCE, }: { value?: AdhocFilter; formData?: QueryFormData; columns?: ColumnMeta[]; + datasource?: Datasource; } = {}) { return ( - - - + ); } test('renders with default props', async () => { - render(setup(), { useDnd: true }); + render(setup(), { useDnd: true, store }); expect( await screen.findByText('Drop columns/metrics here or click'), ).toBeInTheDocument(); @@ -95,6 +109,7 @@ test('renders with value', async () => { }); render(setup({ value }), { useDnd: true, + store, }); expect(await screen.findByText('COUNT(*)')).toBeInTheDocument(); }); @@ -110,6 +125,7 @@ test('renders options with saved metric', async () => { }), { useDnd: true, + store, }, ); expect( @@ -131,6 +147,7 @@ test('renders options with column', async () => { }), { useDnd: true, + store, }, ); expect( @@ -153,9 +170,187 @@ test('renders options with adhoc metric', async () => { }), { useDnd: true, + store, }, ); expect( await screen.findByText('Drop columns/metrics here or click'), ).toBeInTheDocument(); }); + +test('cannot drop a column that is not part of the simple column selection', () => { + const adhocMetric = new AdhocMetric({ + expression: 'AVG(birth_names.num)', + metric_name: 'avg__num', + }); + const { getByTestId, getAllByTestId } = render( + <> + + + + {setup({ + formData: { + ...baseFormData, + ...TimeseriesDefaultFormData, + metrics: [adhocMetric], + }, + columns: [{ column_name: 'order_date' }], + })} + , + { + useDnd: true, + store, + }, + ); + + const selections = getAllByTestId('DatasourcePanelDragOption'); + const acceptableColumn = selections[0]; + const unacceptableColumn = selections[1]; + const metricType = selections[2]; + const currentMetric = getByTestId('dnd-labels-container'); + + fireEvent.dragStart(unacceptableColumn); + fireEvent.dragOver(currentMetric); + fireEvent.drop(currentMetric); + + expect(screen.queryByTestId('filter-edit-popover')).not.toBeInTheDocument(); + + fireEvent.dragStart(acceptableColumn); + fireEvent.dragOver(currentMetric); + fireEvent.drop(currentMetric); + + const filterConfigPopup = screen.getByTestId('filter-edit-popover'); + expect(within(filterConfigPopup).getByText('order_date')).toBeInTheDocument(); + + fireEvent.keyDown(filterConfigPopup, { + key: 'Escape', + code: 'Escape', + keyCode: 27, + charCode: 27, + }); + expect(screen.queryByTestId('filter-edit-popover')).not.toBeInTheDocument(); + + fireEvent.dragStart(metricType); + fireEvent.dragOver(currentMetric); + fireEvent.drop(currentMetric); + + expect( + within(screen.getByTestId('filter-edit-popover')).getByTestId('react-ace'), + ).toHaveTextContent('AGG(metric_a)'); +}); + +describe('when disallow_adhoc_metrics is set', () => { + test('can drop a column type from the simple column selection', () => { + const adhocMetric = new AdhocMetric({ + expression: 'AVG(birth_names.num)', + metric_name: 'avg__num', + }); + const { getByTestId } = render( + <> + + {setup({ + formData: { + ...baseFormData, + ...TimeseriesDefaultFormData, + metrics: [adhocMetric], + }, + datasource: { + ...PLACEHOLDER_DATASOURCE, + extra: '{ "disallow_adhoc_metrics": true }', + }, + columns: [{ column_name: 'column_a' }, { column_name: 'column_b' }], + })} + , + { + useDnd: true, + store, + }, + ); + + const acceptableColumn = getByTestId('DatasourcePanelDragOption'); + const currentMetric = getByTestId('dnd-labels-container'); + + fireEvent.dragStart(acceptableColumn); + fireEvent.dragOver(currentMetric); + fireEvent.drop(currentMetric); + + const filterConfigPopup = screen.getByTestId('filter-edit-popover'); + expect(within(filterConfigPopup).getByText('column_b')).toBeInTheDocument(); + }); + + test('cannot drop any other types of selections apart from simple column selection', () => { + const adhocMetric = new AdhocMetric({ + expression: 'AVG(birth_names.num)', + metric_name: 'avg__num', + }); + const { getByTestId, getAllByTestId } = render( + <> + + + + {setup({ + formData: { + ...baseFormData, + ...TimeseriesDefaultFormData, + metrics: [adhocMetric], + }, + datasource: { + ...PLACEHOLDER_DATASOURCE, + extra: '{ "disallow_adhoc_metrics": true }', + }, + columns: [{ column_name: 'column_a' }, { column_name: 'column_c' }], + })} + , + { + useDnd: true, + store, + }, + ); + + const selections = getAllByTestId('DatasourcePanelDragOption'); + const acceptableColumn = selections[0]; + const unacceptableMetric = selections[1]; + const unacceptableType = selections[2]; + const currentMetric = getByTestId('dnd-labels-container'); + + fireEvent.dragStart(unacceptableMetric); + fireEvent.dragOver(currentMetric); + fireEvent.drop(currentMetric); + + expect(screen.queryByTestId('filter-edit-popover')).not.toBeInTheDocument(); + + fireEvent.dragStart(unacceptableType); + fireEvent.dragOver(currentMetric); + fireEvent.drop(currentMetric); + + expect(screen.queryByTestId('filter-edit-popover')).not.toBeInTheDocument(); + + fireEvent.dragStart(acceptableColumn); + fireEvent.dragOver(currentMetric); + fireEvent.drop(currentMetric); + + const filterConfigPopup = screen.getByTestId('filter-edit-popover'); + expect(within(filterConfigPopup).getByText('column_c')).toBeInTheDocument(); + }); +}); diff --git a/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndFilterSelect.tsx b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndFilterSelect.tsx index 5295bd6dae2d..955c480724e6 100644 --- a/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndFilterSelect.tsx +++ b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndFilterSelect.tsx @@ -85,6 +85,16 @@ const DndFilterSelect = (props: DndFilterSelectProps) => { canDelete, } = props; + const extra = useMemo<{ disallow_adhoc_metrics?: boolean }>(() => { + let extra = {}; + if (datasource?.extra) { + try { + extra = JSON.parse(datasource.extra); + } catch {} // eslint-disable-line no-empty + } + return extra; + }, [datasource?.extra]); + const propsValues = Array.from(props.value ?? []); const [values, setValues] = useState( propsValues.map((filter: OptionValueType) => @@ -149,6 +159,17 @@ const DndFilterSelect = (props: DndFilterSelectProps) => { optionsForSelect(props.columns, props.formData), ); + const availableColumnSet = useMemo( + () => + new Set( + options.map( + ({ column_name, filterOptionName }) => + column_name ?? filterOptionName, + ), + ), + [options], + ); + useEffect(() => { if (datasource && datasource.type === 'table') { const dbId = datasource.database?.id; @@ -382,7 +403,25 @@ const DndFilterSelect = (props: DndFilterSelectProps) => { return new AdhocFilter(config); }, [droppedItem]); - const canDrop = useCallback(() => true, []); + const canDrop = useCallback( + (item: DatasourcePanelDndItem) => { + if ( + extra.disallow_adhoc_metrics && + (item.type !== DndItemType.Column || + !availableColumnSet.has((item.value as ColumnMeta).column_name)) + ) { + return false; + } + + if (item.type === DndItemType.Column) { + const columnName = (item.value as ColumnMeta).column_name; + return availableColumnSet.has(columnName); + } + return true; + }, + [availableColumnSet, extra], + ); + const handleDrop = useCallback( (item: DatasourcePanelDndItem) => { setDroppedItem(item.value); diff --git a/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndMetricSelect.test.tsx b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndMetricSelect.test.tsx index 30be2cebb05c..9d6a7423f03c 100644 --- a/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndMetricSelect.test.tsx +++ b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndMetricSelect.test.tsx @@ -28,6 +28,8 @@ import { import { DndMetricSelect } from 'src/explore/components/controls/DndColumnSelectControl/DndMetricSelect'; import { AGGREGATES } from 'src/explore/constants'; import { EXPRESSION_TYPES } from '../MetricControl/AdhocMetric'; +import DatasourcePanelDragOption from '../../DatasourcePanel/DatasourcePanelDragOption'; +import { DndItemType } from '../../DndItemType'; const defaultProps = { savedMetrics: [ @@ -307,6 +309,125 @@ test('can drag metrics', async () => { expect(within(lastMetric).getByText('metric_a')).toBeVisible(); }); +test('cannot drop a duplicated item', () => { + const metricValues = ['metric_a']; + const { getByTestId } = render( + <> + + + , + { + useDnd: true, + }, + ); + + const acceptableMetric = getByTestId('DatasourcePanelDragOption'); + const currentMetric = getByTestId('dnd-labels-container'); + + const currentMetricSelection = currentMetric.children.length; + + fireEvent.dragStart(acceptableMetric); + fireEvent.dragOver(currentMetric); + fireEvent.drop(currentMetric); + + expect(currentMetric.children).toHaveLength(currentMetricSelection); + expect(currentMetric).toHaveTextContent('metric_a'); +}); + +test('can drop a saved metric when disallow_adhoc_metrics', () => { + const metricValues = ['metric_b']; + const { getByTestId } = render( + <> + + + , + { + useDnd: true, + }, + ); + + const acceptableMetric = getByTestId('DatasourcePanelDragOption'); + const currentMetric = getByTestId('dnd-labels-container'); + + const currentMetricSelection = currentMetric.children.length; + + fireEvent.dragStart(acceptableMetric); + fireEvent.dragOver(currentMetric); + fireEvent.drop(currentMetric); + + expect(currentMetric.children).toHaveLength(currentMetricSelection + 1); + expect(currentMetric.children[1]).toHaveTextContent('metric_a'); +}); + +test('cannot drop non-saved metrics when disallow_adhoc_metrics', () => { + const metricValues = ['metric_b']; + const { getByTestId, getAllByTestId } = render( + <> + + + + + , + { + useDnd: true, + }, + ); + + const selections = getAllByTestId('DatasourcePanelDragOption'); + const acceptableMetric = selections[0]; + const unacceptableMetric = selections[1]; + const unacceptableType = selections[2]; + const currentMetric = getByTestId('dnd-labels-container'); + + const currentMetricSelection = currentMetric.children.length; + + fireEvent.dragStart(unacceptableMetric); + fireEvent.dragOver(currentMetric); + fireEvent.drop(currentMetric); + + expect(currentMetric.children).toHaveLength(currentMetricSelection); + expect(currentMetric).not.toHaveTextContent('metric_c'); + + fireEvent.dragStart(unacceptableType); + fireEvent.dragOver(currentMetric); + fireEvent.drop(currentMetric); + + expect(currentMetric.children).toHaveLength(currentMetricSelection); + expect(currentMetric).not.toHaveTextContent('column_1'); + + fireEvent.dragStart(acceptableMetric); + fireEvent.dragOver(currentMetric); + fireEvent.drop(currentMetric); + + expect(currentMetric.children).toHaveLength(currentMetricSelection + 1); + expect(currentMetric).toHaveTextContent('metric_a'); +}); + test('title changes on custom SQL text change', async () => { let metricValues = [adhocMetricA, 'metric_b']; const onChange = (val: any[]) => { diff --git a/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndMetricSelect.tsx b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndMetricSelect.tsx index 8f489773e83c..2c98ea4c48d4 100644 --- a/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndMetricSelect.tsx +++ b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndMetricSelect.tsx @@ -105,7 +105,27 @@ const getOptionsForSavedMetrics = ( type ValueType = Metric | AdhocMetric | QueryFormMetric; const DndMetricSelect = (props: any) => { - const { onChange, multi } = props; + const { onChange, multi, datasource, savedMetrics } = props; + + const extra = useMemo<{ disallow_adhoc_metrics?: boolean }>(() => { + let extra = {}; + if (datasource?.extra) { + try { + extra = JSON.parse(datasource.extra); + } catch {} // eslint-disable-line no-empty + } + return extra; + }, [datasource?.extra]); + + const savedMetricSet = useMemo( + () => + new Set( + (savedMetrics as savedMetricType[]).map( + ({ metric_name }) => metric_name, + ), + ), + [savedMetrics], + ); const handleChange = useCallback( opts => { @@ -148,11 +168,19 @@ const DndMetricSelect = (props: any) => { const canDrop = useCallback( (item: DatasourcePanelDndItem) => { + if ( + extra.disallow_adhoc_metrics && + (item.type !== DndItemType.Metric || + !savedMetricSet.has(item.value.metric_name)) + ) { + return false; + } + const isMetricAlreadyInValues = item.type === 'metric' ? value.includes(item.value.metric_name) : false; return !isMetricAlreadyInValues; }, - [value], + [value, extra, savedMetricSet], ); const onNewMetric = useCallback( diff --git a/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndSelectLabel.test.tsx b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndSelectLabel.test.tsx index dcf0e4d1eecf..689c76d6c888 100644 --- a/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndSelectLabel.test.tsx +++ b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndSelectLabel.test.tsx @@ -23,6 +23,7 @@ import { DndItemType } from 'src/explore/components/DndItemType'; import DndSelectLabel, { DndSelectLabelProps, } from 'src/explore/components/controls/DndColumnSelectControl/DndSelectLabel'; +import ExploreContainer, { DropzoneContext } from '../../ExploreContainer'; const defaultProps: DndSelectLabelProps = { name: 'Column', @@ -33,6 +34,23 @@ const defaultProps: DndSelectLabelProps = { ghostButtonText: 'Drop columns here or click', onClickGhostButton: jest.fn(), }; +const MockChildren = () => { + const [zones] = React.useContext(DropzoneContext); + return ( + <> + {Object.keys(zones).map(key => ( +
+ {String( + zones[key]({ + value: { column_name: 'test' }, + type: DndItemType.Column, + }), + )} +
+ ))} + + ); +}; test('renders with default props', () => { render(, { useDnd: true }); @@ -62,3 +80,25 @@ test('Handles ghost button click', () => { userEvent.click(screen.getByText('Drop columns here or click')); expect(defaultProps.onClickGhostButton).toHaveBeenCalled(); }); + +test('updates dropValidator on changes', () => { + const { getByTestId, rerender } = render( + + + + , + { useDnd: true }, + ); + expect(getByTestId(`mock-result-${defaultProps.name}`)).toHaveTextContent( + 'false', + ); + rerender( + + true} /> + + , + ); + expect(getByTestId(`mock-result-${defaultProps.name}`)).toHaveTextContent( + 'true', + ); +}); diff --git a/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndSelectLabel.tsx b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndSelectLabel.tsx index 397d6f54e05e..51ad92f87942 100644 --- a/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndSelectLabel.tsx +++ b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndSelectLabel.tsx @@ -16,7 +16,13 @@ * specific language governing permissions and limitations * under the License. */ -import React, { ReactNode, useMemo } from 'react'; +import React, { + ReactNode, + useCallback, + useContext, + useEffect, + useMemo, +} from 'react'; import { useDrop } from 'react-dnd'; import { t, useTheme } from '@superset-ui/core'; import ControlHeader from 'src/explore/components/ControlHeader'; @@ -31,6 +37,7 @@ import { } from 'src/explore/components/DatasourcePanel/types'; import Icons from 'src/components/Icons'; import { DndItemType } from '../../DndItemType'; +import { DraggingContext, DropzoneContext } from '../../ExploreContainer'; export type DndSelectLabelProps = { name: string; @@ -43,26 +50,35 @@ export type DndSelectLabelProps = { valuesRenderer: () => ReactNode; displayGhostButton?: boolean; onClickGhostButton: () => void; + isLoading?: boolean; }; export default function DndSelectLabel({ displayGhostButton = true, accept, valuesRenderer, + isLoading, ...props }: DndSelectLabelProps) { const theme = useTheme(); + const canDropProp = props.canDrop; + const canDropValueProp = props.canDropValue; + + const dropValidator = useCallback( + (item: DatasourcePanelDndItem) => + canDropProp(item) && (canDropValueProp?.(item.value) ?? true), + [canDropProp, canDropValueProp], + ); const [{ isOver, canDrop }, datasourcePanelDrop] = useDrop({ - accept, + accept: isLoading ? [] : accept, drop: (item: DatasourcePanelDndItem) => { props.onDrop(item); props.onDropValue?.(item.value); }, - canDrop: (item: DatasourcePanelDndItem) => - props.canDrop(item) && (props.canDropValue?.(item.value) ?? true), + canDrop: dropValidator, collect: monitor => ({ isOver: monitor.isOver(), @@ -71,6 +87,17 @@ export default function DndSelectLabel({ }), }); + const dispatch = useContext(DropzoneContext)[1]; + + useEffect(() => { + dispatch({ key: props.name, canDrop: dropValidator }); + return () => { + dispatch({ key: props.name }); + }; + }, [dispatch, props.name, dropValidator]); + + const isDragging = useContext(DraggingContext); + const values = useMemo(() => valuesRenderer(), [valuesRenderer]); function renderGhostButton() { @@ -94,6 +121,8 @@ export default function DndSelectLabel({ data-test="dnd-labels-container" canDrop={canDrop} isOver={isOver} + isDragging={isDragging} + isLoading={isLoading} > {values} {displayGhostButton && renderGhostButton()} diff --git a/superset-frontend/src/explore/components/controls/FilterControl/AdhocFilter/index.js b/superset-frontend/src/explore/components/controls/FilterControl/AdhocFilter/index.js index 627cc50c44a4..92cb69eebcab 100644 --- a/superset-frontend/src/explore/components/controls/FilterControl/AdhocFilter/index.js +++ b/superset-frontend/src/explore/components/controls/FilterControl/AdhocFilter/index.js @@ -92,6 +92,7 @@ export default class AdhocFilter { equals(adhocFilter) { return ( + adhocFilter.clause === this.clause && adhocFilter.expressionType === this.expressionType && adhocFilter.sqlExpression === this.sqlExpression && adhocFilter.operator === this.operator && diff --git a/superset-frontend/src/explore/components/controls/FilterControl/AdhocFilterEditPopover/index.jsx b/superset-frontend/src/explore/components/controls/FilterControl/AdhocFilterEditPopover/index.jsx index 94b04b1114ac..9333bfb5dbb5 100644 --- a/superset-frontend/src/explore/components/controls/FilterControl/AdhocFilterEditPopover/index.jsx +++ b/superset-frontend/src/explore/components/controls/FilterControl/AdhocFilterEditPopover/index.jsx @@ -73,11 +73,12 @@ const FilterPopoverContentContainer = styled.div` .filter-edit-clause-info { font-size: ${({ theme }) => theme.typography.sizes.xs}px; - padding-left: ${({ theme }) => theme.gridUnit}px; } .filter-edit-clause-section { - display: inline-flex; + display: flex; + flex-direction: row; + gap: ${({ theme }) => theme.gridUnit * 5}px; } .adhoc-filter-simple-column-dropdown { diff --git a/superset-frontend/src/explore/components/controls/FilterControl/AdhocFilterEditPopoverSqlTabContent/AdhocFilterEditPopoverSqlTabContent.test.jsx b/superset-frontend/src/explore/components/controls/FilterControl/AdhocFilterEditPopoverSqlTabContent/AdhocFilterEditPopoverSqlTabContent.test.jsx deleted file mode 100644 index 9bdd20c0f0ef..000000000000 --- a/superset-frontend/src/explore/components/controls/FilterControl/AdhocFilterEditPopoverSqlTabContent/AdhocFilterEditPopoverSqlTabContent.test.jsx +++ /dev/null @@ -1,74 +0,0 @@ -/** - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -/* eslint-disable no-unused-expressions */ -import React from 'react'; -import sinon from 'sinon'; -import { shallow } from 'enzyme'; - -import AdhocFilter from 'src/explore/components/controls/FilterControl/AdhocFilter'; -import AdhocFilterEditPopoverSqlTabContent from '.'; -import { Clauses, ExpressionTypes } from '../types'; - -const sqlAdhocFilter = new AdhocFilter({ - expressionType: ExpressionTypes.Sql, - sqlExpression: 'value > 10', - clause: Clauses.Where, -}); - -function setup(overrides) { - const onChange = sinon.spy(); - const props = { - adhocFilter: sqlAdhocFilter, - onChange, - options: [], - height: 100, - ...overrides, - }; - const wrapper = shallow(); - return { wrapper, onChange }; -} - -describe('AdhocFilterEditPopoverSqlTabContent', () => { - it('renders the sql tab form', () => { - const { wrapper } = setup(); - expect(wrapper).toExist(); - }); - - it('passes the new clause to onChange after onSqlExpressionClauseChange', () => { - const { wrapper, onChange } = setup(); - wrapper.instance().onSqlExpressionClauseChange(Clauses.Having); - expect(onChange.calledOnce).toBe(true); - expect( - onChange.lastCall.args[0].equals( - sqlAdhocFilter.duplicateWith({ clause: Clauses.Having }), - ), - ).toBe(true); - }); - - it('passes the new query to onChange after onSqlExpressionChange', () => { - const { wrapper, onChange } = setup(); - wrapper.instance().onSqlExpressionChange('value < 5'); - expect(onChange.calledOnce).toBe(true); - expect( - onChange.lastCall.args[0].equals( - sqlAdhocFilter.duplicateWith({ sqlExpression: 'value < 5' }), - ), - ).toBe(true); - }); -}); diff --git a/superset-frontend/src/explore/components/controls/FilterControl/AdhocFilterEditPopoverSqlTabContent/AdhocFilterEditPopoverSqlTabContent.test.tsx b/superset-frontend/src/explore/components/controls/FilterControl/AdhocFilterEditPopoverSqlTabContent/AdhocFilterEditPopoverSqlTabContent.test.tsx new file mode 100644 index 000000000000..0bd8c59c2b08 --- /dev/null +++ b/superset-frontend/src/explore/components/controls/FilterControl/AdhocFilterEditPopoverSqlTabContent/AdhocFilterEditPopoverSqlTabContent.test.tsx @@ -0,0 +1,76 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React from 'react'; +import { render, screen, selectOption } from 'spec/helpers/testing-library'; +import userEvent from '@testing-library/user-event'; +import { IAceEditorProps } from 'react-ace'; +import AdhocFilter from '../AdhocFilter'; +import { Clauses, ExpressionTypes } from '../types'; +import AdhocFilterEditPopoverSqlTabContent from '.'; + +jest.mock('src/components/AsyncAceEditor', () => ({ + SQLEditor: ({ value, onChange }: IAceEditorProps) => ( +